From 70b65fde0cdc6106e0aa37a31a2e8fae2e13db61 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Mon, 3 Aug 2026 17:44:00 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=B7=D0=B5=D0=BC=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=D0=B5:=20=D0=BD=D0=B0=D0=B7=D0=BD=D0=B0=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=B5=D1=80=D1=81=D0=BE=D0=BD?= =?UTF-8?q?=D0=B0=D0=B6=D0=B5=D0=B9=20=D0=B2=20=D1=82=D0=B5=D0=BC=D0=B0?= =?UTF-8?q?=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D0=B5=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BD=D0=B0=D1=82=D1=8B=20+=20=D0=B0=D0=BD=D0=B8=D0=BC?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Комнаты Спальня/Логово/Дойка со слотами - Перетаскивание в комнату: персонаж занимает слот, скелет меняется на состояние комнаты (milk/birth/...), играет одиночную (1) или групповую (2+) анимацию - Клетка сброса берётся по центру тела (фикс съезда на строку ниже) - Панель показывает назначение/слот/режим анимации Co-Authored-By: Claude Opus 4.8 --- README.md | 6 ++- src/RoomMap.cs | 134 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 132 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5ae7a4b..a551b9d 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,10 @@ D:\Godot_v4.7.1\Godot_v4.7.1-stable_mono_win64.exe комнат, **тёмная шахта** (колонка тёмных комнат друг над другом = скрытая лестница; персонаж заходит и проходит по ней на другой этаж, анимация — боевой `walk_0`), **объекты** (фонари/свет) и **несколько персонажей** на этажах. - ЛКМ по персонажу — выбрать/тащить (перенос в комнату), ЛКМ по фону/WASD — панорама, - колесо — зум, `R` — сброс. + ЛКМ по персонажу — выбрать/тащить, ЛКМ по фону/WASD — панорама, колесо — зум, `R` — сброс. + **Тематические комнаты** (Спальня/Логово/Дойка) имеют слоты: при перетаскивании + персонажа в такую комнату он занимает слот, **меняет скелет на состояние комнаты** + и играет анимацию — **одиночную** (1 в комнате) или **групповую** (2+). 2. **Просмотр спрайтов** (`scenes/SpriteViewer.tscn`) — галерея вытащенных изображений (UI, фоны, эффекты, нарезанные спрайты). `A/D` — картинка, `W/S` — категория, колесо — зум. 3. **Нативные анимации** (`scenes/NativeViewer.tscn`) — браузер всех 38 персонажей нативно: diff --git a/src/RoomMap.cs b/src/RoomMap.cs index e45dcfb..647a14c 100644 --- a/src/RoomMap.cs +++ b/src/RoomMap.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Godot; // Демо-карта «убежища» в разрезе (à la Fallout Shelter): комнаты, лестницы, объекты @@ -30,20 +31,32 @@ public partial class RoomMap : Node2D private PanelContainer _infoPanel; private Label _infoLabel; - private enum PState { Idle, Walk, GoShaft, Teleport, Held } + private enum PState { Idle, Walk, GoShaft, Teleport, Held, Assigned } private class Pawn { public Node2D Node; public AnimationPlayer Ap; - public string Name; + public string Name, BaseScene; public int Row, TargetRow; public float X, TargetX, Speed, MinX, MaxX, IdleTimer, FadeT; public bool Swapped; public PState State; public string WalkAnim, IdleAnim; + public Room AssignedRoom; } + // тематическая комната со слотами + private class Room + { + public string Type; + public int Col, Row, Span, Cap; + public string[] States; // приоритет состояний-скелетов + public readonly List Occ = new(); + } + + private List _rooms; + public override void _Ready() { var bg = new ColorRect @@ -84,7 +97,14 @@ public partial class RoomMap : Node2D private void ShowInfo(Pawn p) { _selected = p; - _infoLabel.Text = $"Персонаж: {p.Name}\nЭтаж: {p.Row + 1}/{Rows}\nтащи мышью → перенести в комнату"; + if (p.AssignedRoom != null) + { + var r = p.AssignedRoom; + string mode = r.Occ.Count >= 2 ? "групповая" : "одиночная"; + _infoLabel.Text = $"Персонаж: {p.Name}\nНазначен: {r.Type} (слот {r.Occ.IndexOf(p) + 1}/{r.Cap})\nанимация: {mode}\nтащи мышью → убрать"; + } + else + _infoLabel.Text = $"Персонаж: {p.Name}\nЭтаж: {p.Row + 1}/{Rows}\nтащи мышью → назначить в комнату"; _infoPanel.Visible = true; } @@ -109,6 +129,13 @@ public partial class RoomMap : Node2D AddCell("res://assets/room_sliced/LactationRoom.png", 4, 3, 1, 1); AddCell("res://assets/room_sliced/Light.png", 2, 2, 1, 1); + _rooms = new List + { + new Room { Type = "Спальня", Col = 1, Row = 0, Span = 3, Cap = 2, States = new[] { "stand", "combat" } }, + new Room { Type = "Логово", Col = 0, Row = 2, Span = 1, Cap = 2, States = new[] { "birth", "down", "milk" } }, + new Room { Type = "Дойка", Col = 4, Row = 3, Span = 1, Cap = 1, States = new[] { "milk", "milk2" } }, + }; + // рамки поверх всех ячеек — читаемость сетки for (int r = 0; r < Rows; r++) for (int c = 0; c < Cols; c++) @@ -194,7 +221,7 @@ public partial class RoomMap : Node2D string[] anims = ap != null ? ap.GetAnimationList() : System.Array.Empty(); var p = new Pawn { - Node = node, Ap = ap, Name = path.Split('/')[3], Row = row, + Node = node, Ap = ap, Name = path.Split('/')[3], BaseScene = path, Row = row, X = x, MinX = minX, MaxX = maxX, Speed = 70f + (float)_rng.NextDouble() * 55f, WalkAnim = Pick(anims, "walk_0"), IdleAnim = Pick(anims, "idle_0") @@ -237,6 +264,7 @@ public partial class RoomMap : Node2D switch (p.State) { case PState.Held: + case PState.Assigned: break; case PState.Walk: @@ -307,16 +335,110 @@ public partial class RoomMap : Node2D private void DropPawn(Pawn p) { + p.Node.Modulate = Colors.White; + // клетку берём по ЦЕНТРУ тела, а не по ногам (иначе съезжает на строку ниже) int col = Mathf.Clamp((int)(p.Node.Position.X / Cell), 0, Cols - 1); - int row = Mathf.Clamp((int)(p.Node.Position.Y / Cell), 0, Rows - 1); + int row = Mathf.Clamp((int)((p.Node.Position.Y - Cell * 0.45f) / Cell), 0, Rows - 1); + + var room = RoomAt(col, row); + if (room != null && (room.Occ.Contains(p) || room.Occ.Count < room.Cap)) + { + AssignToRoom(p, room); + if (_selected == p) ShowInfo(p); + return; + } + + // обычный сброс: снять назначение и вернуть боевую сцену + if (p.AssignedRoom != null) { RemoveFromRoom(p); SwapScene(p, p.BaseScene); } p.Row = row; p.X = Mathf.Clamp(col * Cell + Cell * 0.5f, p.MinX, p.MaxX); p.Node.Position = new Vector2(p.X, FloorY(row)); - p.Node.Modulate = Colors.White; p.State = PState.Idle; p.IdleTimer = 0.5f; PlayIdle(p); if (_selected == p) ShowInfo(p); } + private Room RoomAt(int col, int row) + { + if (_rooms == null) return null; + foreach (var r in _rooms) + if (row == r.Row && col >= r.Col && col < r.Col + r.Span) return r; + return null; + } + + private void AssignToRoom(Pawn p, Room room) + { + if (p.AssignedRoom != null && p.AssignedRoom != room) RemoveFromRoom(p); + if (!room.Occ.Contains(p)) room.Occ.Add(p); + p.AssignedRoom = room; + p.State = PState.Assigned; + + string sp = StateScene(p.Name, room.States) ?? p.BaseScene; + SwapScene(p, sp); + LayoutRoom(room); + } + + private void RemoveFromRoom(Pawn p) + { + var room = p.AssignedRoom; + if (room == null) return; + room.Occ.Remove(p); + p.AssignedRoom = null; + if (room.Occ.Count > 0) LayoutRoom(room); + } + + // расставить по слотам и включить анимацию: 1 в комнате — одиночную, 2+ — групповую + private void LayoutRoom(Room room) + { + int n = room.Occ.Count; + bool group = n >= 2; + for (int i = 0; i < n; i++) + { + var q = room.Occ[i]; + float sx = room.Col * Cell + (i + 1f) / (n + 1f) * (room.Span * Cell); + q.X = sx; + q.Node.Position = new Vector2(sx, FloorY(room.Row)); + q.Node.Scale = new Vector2(1f, 1f); + var anims = q.Ap != null ? q.Ap.GetAnimationList() : System.Array.Empty(); + string a = PickAction(anims, group); + if (q.Ap != null && a != null) q.Ap.Play(a); + } + } + + private static string StateScene(string name, string[] states) + { + foreach (var s in states) + { + string path = $"res://characters/{name}/{name}_{s}_native.tscn"; + if (ResourceLoader.Exists(path)) return path; + } + return null; + } + + // заменить скелет-сцену персонажа (боевая ↔ состояние комнаты) + private void SwapScene(Pawn p, string path) + { + if (p.Node.GetChildCount() > 0) p.Node.GetChild(0).Free(); + var scene = ResourceLoader.Load(path).Instantiate(); + scene.Position = -FootAnchor * CharScale; + scene.Scale = new Vector2(CharScale, CharScale); + p.Node.AddChild(scene); + p.Ap = scene.GetNodeOrNull("AnimationPlayer"); + var anims = p.Ap != null ? p.Ap.GetAnimationList() : System.Array.Empty(); + p.WalkAnim = Pick(anims, "walk_0"); + p.IdleAnim = Pick(anims, "idle_0"); + } + + private static readonly string[] NotAction = + { "walk", "attack", "idle", "dd", "dfdfa", "ddd", "test", "animation", "information", "portrait", "stun", "knockback", "dead", "down" }; + + private static string PickAction(string[] anims, bool group) + { + var actions = anims.Where(a => !NotAction.Any(n => a.StartsWith(n))).ToList(); + if (actions.Count == 0) actions = anims.Where(a => a.StartsWith("idle")).ToList(); + if (actions.Count == 0) return anims.Length > 0 ? anims[0] : null; + return group && actions.Count > 1 ? actions[1] : actions[0]; + } + private void AddCell(string path, int col, int row, int cw, int ch, Color? modulate = null) { AddTex(path, new Rect2(col * Cell, row * Cell, cw * Cell, ch * Cell), _world, modulate ?? Colors.White);