diff --git a/src/RoomMap.cs b/src/RoomMap.cs index 078b3b7..6f8159e 100644 --- a/src/RoomMap.cs +++ b/src/RoomMap.cs @@ -37,26 +37,54 @@ public partial class RoomMap : Node2D { public Node2D Node; public AnimationPlayer Ap; - public string Name, BaseScene; + public string Name, BaseScene, DisplayName; 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; + public CType Type; } - // тематическая комната со слотами + // тематическая комната с типизированными слотами private class Room { public string Type; - public int Col, Row, Span, Cap; + public int Col, Row, Span; public string[] States; // приоритет состояний-скелетов - public readonly List Occ = new(); + public CType[] SlotTypes; // тип, разрешённый в каждом слоте + public Pawn[] SlotOcc; // кто в слоте (null = свободно) + public Label Header; + public int Cap => SlotTypes.Length; + public void Init() => SlotOcc = new Pawn[SlotTypes.Length]; + public int Count() { int n = 0; foreach (var s in SlotOcc) if (s != null) n++; return n; } + public int SlotOf(Pawn p) => System.Array.IndexOf(SlotOcc, p); + public int FreeSlotFor(CType t) + { + for (int i = 0; i < SlotTypes.Length; i++) if (SlotOcc[i] == null && SlotTypes[i] == t) return i; + return -1; + } } private List _rooms; + // типы персонажей + private enum CType { Player, Slave, Monster, Child } + private static string TypeName(CType t) => t switch + { + CType.Player => "Игрок", CType.Slave => "Раб", CType.Monster => "Монстр", CType.Child => "Ребёнок", _ => "?" + }; + private static readonly string[] Monsters = { "orc", "goblin", "horse", "oni", "man" }; + private static readonly string[] SlaveNames = { "Ариа", "Луна", "Мира", "Элара", "Ника", "Рина", "Кая", "Тэя", "Лира", "Мэй", "Иврис", "Сая" }; + private static readonly string[] MonsterNames = { "Грок", "Зуг", "Морг", "Рарг", "Тхок", "Гнар", "Урук", "Драк" }; + private static readonly string[] ChildNames = { "Пип", "Тимо", "Лулу", "Бэн", "Ния" }; + + // инвентарь снятых персонажей + private class InvItem { public string Name, Scene; public CType Type; } + private readonly List _inventory = new(); + private VBoxContainer _invBox; + public override void _Ready() { var bg = new ColorRect @@ -79,9 +107,86 @@ public partial class RoomMap : Node2D AddChild(_label); BuildInfoPanel(); + BuildInventoryPanel(); ResetView(); } + private void BuildRoomHeaders() + { + foreach (var r in _rooms) + { + var lbl = new Label { Position = new Vector2(r.Col * Cell + 6, r.Row * Cell - 34) }; + lbl.AddThemeFontSizeOverride("font_size", 26); + lbl.AddThemeConstantOverride("outline_size", 5); + lbl.AddThemeColorOverride("font_outline_color", Colors.Black); + _world.AddChild(lbl); + r.Header = lbl; + UpdateRoomHeader(r); + } + } + + private void UpdateRoomHeader(Room r) + { + if (r.Header == null) return; + var names = new List(); + foreach (var s in r.SlotOcc) if (s != null) names.Add($"{s.DisplayName} ({TypeName(s.Type)})"); + r.Header.Text = names.Count == 0 ? $"{r.Type}: пусто" : $"{r.Type}: " + string.Join(", ", names); + } + + // ---------- инвентарь ---------- + private void BuildInventoryPanel() + { + var panel = new PanelContainer(); + panel.SetAnchorsPreset(Control.LayoutPreset.TopRight); + panel.Position = new Vector2(-262, 12); + panel.CustomMinimumSize = new Vector2(250, 0); + var margin = new MarginContainer(); + foreach (var s in new[] { "margin_left", "margin_right", "margin_top", "margin_bottom" }) + margin.AddThemeConstantOverride(s, 8); + panel.AddChild(margin); + _invBox = new VBoxContainer(); + margin.AddChild(_invBox); + AddChild(panel); + RefreshInventory(); + } + + private void RefreshInventory() + { + foreach (var c in _invBox.GetChildren()) c.QueueFree(); + var title = new Label { Text = $"Инвентарь ({_inventory.Count})" }; + title.AddThemeColorOverride("font_color", new Color(0.95f, 0.9f, 0.7f)); + _invBox.AddChild(title); + if (_inventory.Count == 0) + { + var e = new Label { Text = "пусто · ПКМ по персонажу" }; + e.AddThemeColorOverride("font_color", new Color(0.7f, 0.72f, 0.78f)); + _invBox.AddChild(e); + } + foreach (var it in _inventory.ToList()) + { + var item = it; + var b = new Button { Text = $"{it.Name} · {TypeName(it.Type)}" }; + b.Pressed += () => RespawnFromInventory(item); + _invBox.AddChild(b); + } + } + + private void RespawnFromInventory(InvItem it) + { + var p = SpawnPawn(it.Scene, _rng.Next(Rows), it.Name, it.Type); + if (p != null) { _inventory.Remove(it); RefreshInventory(); } + } + + private void SendToInventory(Pawn p) + { + RemoveFromRoom(p); + _pawns.Remove(p); + if (_selected == p) { _selected = null; _infoPanel.Visible = false; } + p.Node.QueueFree(); + _inventory.Add(new InvItem { Name = p.DisplayName, Type = p.Type, Scene = p.BaseScene }); + RefreshInventory(); + } + private void BuildInfoPanel() { _infoPanel = new PanelContainer { Position = new Vector2(16, 64), Visible = false }; @@ -97,14 +202,15 @@ public partial class RoomMap : Node2D private void ShowInfo(Pawn p) { _selected = p; + string head = $"{p.DisplayName} · {TypeName(p.Type)}"; 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тащи мышью → убрать"; + string mode = r.Count() >= 2 ? "групповая" : "одиночная"; + _infoLabel.Text = $"{head}\nНазначен: {r.Type} (слот {r.SlotOf(p) + 1}/{r.Cap})\nанимация: {mode}\nЛКМ тащить · ПКМ → инвентарь"; } else - _infoLabel.Text = $"Персонаж: {p.Name}\nЭтаж: {p.Row + 1}/{Rows}\nтащи мышью → назначить в комнату"; + _infoLabel.Text = $"{head}\nЭтаж: {p.Row + 1}/{Rows}\nЛКМ тащить/назначить · ПКМ → инвентарь"; _infoPanel.Visible = true; } @@ -131,10 +237,15 @@ public partial class RoomMap : Node2D _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[] { "milk", "birth", "down" } }, - new Room { Type = "Дойка", Col = 4, Row = 3, Span = 1, Cap = 2, States = new[] { "milk", "milk2" } }, + new Room { Type = "Спальня", Col = 1, Row = 0, Span = 3, States = new[] { "stand", "combat" }, + SlotTypes = new[] { CType.Player, CType.Slave } }, + new Room { Type = "Логово", Col = 0, Row = 2, Span = 1, States = new[] { "suckle_left", "milk", "birth", "combat", "stand" }, + SlotTypes = new[] { CType.Slave, CType.Monster } }, + new Room { Type = "Дойка", Col = 4, Row = 3, Span = 1, States = new[] { "milk", "milk2" }, + SlotTypes = new[] { CType.Slave, CType.Slave, CType.Child, CType.Child, CType.Child, CType.Child } }, }; + foreach (var r in _rooms) r.Init(); + BuildRoomHeaders(); // рамки поверх всех ячеек — читаемость сетки for (int r = 0; r < Rows; r++) @@ -195,40 +306,57 @@ public partial class RoomMap : Node2D _rng = new System.Random(20260803); (string path, int row)[] picks = { - ("res://characters/elf1/elf1_combat_native.tscn", 0), - ("res://characters/knight/knight_combat_native.tscn", 1), - ("res://characters/shortstack/shortstack_combat_native.tscn", 2), - ("res://characters/sister/sister_combat_native.tscn", 3), - ("res://characters/satyress/satyress_combat_native.tscn", 4), + ("res://characters/elf1/elf1_combat_native.tscn", 0), // раб + ("res://characters/sister/sister_combat_native.tscn", 1), // раб + ("res://characters/satyress/satyress_combat_native.tscn", 2), // раб + ("res://characters/knight/knight_combat_native.tscn", 3), // раб + ("res://characters/orc/orc_combat_native.tscn", 2), // монстр + ("res://characters/baby/human_baby_native.tscn", 1), // ребёнок + ("res://characters/baby/human_baby_native.tscn", 3), // ребёнок }; + foreach (var (path, row) in picks) SpawnPawn(path, row); + } + + private CType TypeOf(string name) => + name == "player" ? CType.Player : name == "baby" ? CType.Child : + Monsters.Contains(name) ? CType.Monster : CType.Slave; + + private string GenName(CType t) + { + var pool = t == CType.Monster ? MonsterNames : t == CType.Child ? ChildNames + : t == CType.Player ? new[] { "Игрок" } : SlaveNames; + return pool[_rng.Next(pool.Length)]; + } + + private Pawn SpawnPawn(string path, int row, string name = null, CType? type = null) + { + if (!ResourceLoader.Exists(path)) return null; + var scene = ResourceLoader.Load(path).Instantiate(); + scene.Position = -FootAnchor * CharScale; + scene.Scale = new Vector2(CharScale, CharScale); + var node = new Node2D(); + node.AddChild(scene); + _world.AddChild(node); + float minX = Cell * 0.4f, maxX = Cols * Cell - Cell * 0.4f; + float x = (float)(_rng.NextDouble() * (maxX - minX) + minX); + node.Position = new Vector2(x, FloorY(row)); - foreach (var (path, row) in picks) + var ap = scene.GetNodeOrNull("AnimationPlayer"); + string[] anims = ap != null ? ap.GetAnimationList() : System.Array.Empty(); + string cname = path.Split('/')[3]; + CType t = type ?? TypeOf(cname); + var p = new Pawn { - if (!ResourceLoader.Exists(path)) continue; - var scene = ResourceLoader.Load(path).Instantiate(); - scene.Position = -FootAnchor * CharScale; - scene.Scale = new Vector2(CharScale, CharScale); - - var node = new Node2D(); - node.AddChild(scene); - _world.AddChild(node); - - float x = (float)(_rng.NextDouble() * (maxX - minX) + minX); - node.Position = new Vector2(x, FloorY(row)); - - var ap = scene.GetNodeOrNull("AnimationPlayer"); - string[] anims = ap != null ? ap.GetAnimationList() : System.Array.Empty(); - var p = new Pawn - { - 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") - }; - Decide(p); - _pawns.Add(p); - } + Node = node, Ap = ap, Name = cname, 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"), + Type = t, DisplayName = name ?? GenName(t) + }; + Decide(p); + _pawns.Add(p); + return p; } private static string Pick(string[] anims, string want) @@ -341,11 +469,17 @@ public partial class RoomMap : Node2D 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)) + if (room != null) { - AssignToRoom(p, room); - if (_selected == p) ShowInfo(p); - return; + int slot = room.SlotOf(p); + if (slot < 0) slot = room.FreeSlotFor(p.Type); + if (slot >= 0) + { + AssignToRoom(p, room, slot); + if (_selected == p) ShowInfo(p); + return; + } + // нет свободного слота нужного типа — роняем на пол } // обычный сброс: снять назначение и вернуть боевую сцену @@ -365,10 +499,10 @@ public partial class RoomMap : Node2D return null; } - private void AssignToRoom(Pawn p, Room room) + private void AssignToRoom(Pawn p, Room room, int slot) { if (p.AssignedRoom != null && p.AssignedRoom != room) RemoveFromRoom(p); - if (!room.Occ.Contains(p)) room.Occ.Add(p); + room.SlotOcc[slot] = p; p.AssignedRoom = room; p.State = PState.Assigned; @@ -381,31 +515,41 @@ public partial class RoomMap : Node2D { var room = p.AssignedRoom; if (room == null) return; - room.Occ.Remove(p); + int slot = room.SlotOf(p); + if (slot >= 0) room.SlotOcc[slot] = null; p.AssignedRoom = null; - if (room.Occ.Count > 0) LayoutRoom(room); + LayoutRoom(room); } private static readonly Vector2 CenterAnchor = new Vector2(640f, 360f); // центр кадра 1280x720 - // расставить по слотам (центрируя в комнате) и включить анимацию: 1 — одиночную, 2+ — групповую + // расставить по слотам: основные (Игрок/Раб/Монстр) в ряд, дети — ниже и мельче private void LayoutRoom(Room room) { - int n = room.Occ.Count; - bool group = n >= 2; - float center = room.Col * Cell + room.Span * Cell * 0.5f; - float band = room.Span * Cell * 0.5f; - float cellY = room.Row * Cell + Cell * 0.5f; - for (int i = 0; i < n; i++) + bool group = room.Count() >= 2; + var main = new List(); + var kids = new List(); + for (int i = 0; i < room.Cap; i++) + (room.SlotTypes[i] == CType.Child ? kids : main).Add(i); + PlaceGroup(room, main, group, Cell * 0.74f, 1f); + PlaceGroup(room, kids, group, Cell * 0.9f, 0.55f); + UpdateRoomHeader(room); + } + + private void PlaceGroup(Room room, List slots, bool group, float yOff, float scale) + { + int n = slots.Count; + for (int k = 0; k < n; k++) { - var q = room.Occ[i]; - float sx = n == 1 ? center : center - band / 2f + band * (i / (float)(n - 1)); - q.X = sx; - // центр-анкор: центр персонажа (любого состояния) — в центр ячейки + var q = room.SlotOcc[slots[k]]; + if (q == null) continue; + float t = n == 1 ? 0.5f : (k + 0.5f) / n; + float sx = room.Col * Cell + t * (room.Span * Cell); if (q.Node.GetChildCount() > 0 && q.Node.GetChild(0) is Node2D sc) sc.Position = -CenterAnchor * CharScale; - q.Node.Position = new Vector2(sx, cellY); - q.Node.Scale = new Vector2(1f, 1f); + q.X = sx; + q.Node.Position = new Vector2(sx, room.Row * Cell + yOff); + q.Node.Scale = new Vector2(scale, scale); 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); @@ -548,6 +692,11 @@ public partial class RoomMap : Node2D _dragging = false; } } + else if (mb.ButtonIndex == MouseButton.Right && mb.Pressed) + { + var hit = PawnAt(ScreenToWorld(mb.Position)); + if (hit != null) SendToInventory(hit); + } else if (mb.Pressed && mb.ButtonIndex == MouseButton.WheelUp) ZoomAt(mb.Position, 1.1f); else if (mb.Pressed && mb.ButtonIndex == MouseButton.WheelDown) @@ -556,7 +705,17 @@ public partial class RoomMap : Node2D else if (@event is InputEventMouseMotion mm) { if (_dragPawn != null) + { _dragPawn.Node.Position = ScreenToWorld(mm.Position) + _dragOffset; + var pos = _dragPawn.Node.Position; + int dcol = (int)(pos.X / Cell), drow = (int)((pos.Y - Cell * 0.45f) / Cell); + var room = RoomAt(Mathf.Clamp(dcol, 0, Cols - 1), Mathf.Clamp(drow, 0, Rows - 1)); + _infoLabel.Text = + $"{_dragPawn.DisplayName} · {TypeName(_dragPawn.Type)}\n" + + $"DEBUG коорд: x={pos.X:0} y={pos.Y:0}\n" + + $"клетка: col={dcol} row={drow}" + (room != null ? $" → {room.Type}" : ""); + _infoPanel.Visible = true; + } else if (_dragging) _world.Position += mm.Relative; }