Подземелье: типы персонажей, имена, окна-заголовки, инвентарь, типизированные слоты

- Типы: Игрок/Раб/Монстр/Ребёнок (по персонажу), генерация имён
- Заголовки-окна над комнатами со списком занявших (имя · тип)
- ПКМ по персонажу -> в инвентарь (панель справа, клик -> вернуть на карту)
- Типизированные слоты: Дойка = 2 раба + 4 ребёнка, Логово = 1 раб + 1 монстр
  (персонаж занимает только слот своего типа; иначе не назначается)
- Персонажи в комнатах опущены ниже (не висят), дети мельче и ниже
- DEBUG: координаты/клетка при перетаскивании в панели

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-08-03 18:01:29 +03:00
co-authored by Claude Opus 4.8
parent 4df5a73ec0
commit 0882ecdf87
+220 -61
View File
@@ -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<Pawn> 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<Room> _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<InvItem> _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<string>();
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<Room>
{
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<PackedScene>(path).Instantiate<Node2D>();
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>("AnimationPlayer");
string[] anims = ap != null ? ap.GetAnimationList() : System.Array.Empty<string>();
string cname = path.Split('/')[3];
CType t = type ?? TypeOf(cname);
var p = new Pawn
{
if (!ResourceLoader.Exists(path)) continue;
var scene = ResourceLoader.Load<PackedScene>(path).Instantiate<Node2D>();
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>("AnimationPlayer");
string[] anims = ap != null ? ap.GetAnimationList() : System.Array.Empty<string>();
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<int>();
var kids = new List<int>();
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<int> 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>();
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;
}