Подземелье: типы персонажей, имена, окна-заголовки, инвентарь, типизированные слоты
- Типы: Игрок/Раб/Монстр/Ребёнок (по персонажу), генерация имён - Заголовки-окна над комнатами со списком занявших (имя · тип) - ПКМ по персонажу -> в инвентарь (панель справа, клик -> вернуть на карту) - Типизированные слоты: Дойка = 2 раба + 4 ребёнка, Логово = 1 раб + 1 монстр (персонаж занимает только слот своего типа; иначе не назначается) - Персонажи в комнатах опущены ниже (не висят), дети мельче и ниже - DEBUG: координаты/клетка при перетаскивании в панели Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4df5a73ec0
commit
0882ecdf87
+220
-61
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user