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

- Персонажи больше не ходят по карте: старт пустой, никакого AI-движения
- Убрана тёмная шахта/телепорт (движение больше не нужно)
- Спавнер (панель внизу): + Раб / Монстр / Ребёнок / Игрок — создаёт персонажа,
  который стоит в idle на случайном этаже
- Дроп на обычную клетку -> персонаж просто стоит (idle), дроп в слот -> назначение
- Счётчик персонажей в шапке обновляется

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-08-04 05:17:09 +03:00
co-authored by Claude Opus 4.8
parent 0882ecdf87
commit 4350f4a9e7
2 changed files with 71 additions and 132 deletions
+9 -7
View File
@@ -26,13 +26,15 @@ D:\Godot_v4.7.1\Godot_v4.7.1-stable_mono_win64.exe
Главное меню (`scenes/MainMenu.tscn`) ведёт на три сцены (назад в меню — `Esc`):
1. **Подземелье** (`scenes/Dungeon.tscn`) — разрез убежища à la Fallout Shelter: сетка 6×5
комнат, **тёмная шахта** (колонка тёмных комнат друг над другом = скрытая лестница;
персонаж заходит и проходит по ней на другой этаж, анимация — боевой `walk_0`),
**объекты** (фонари/свет) и **несколько персонажей** на этажах.
ЛКМ по персонажу — выбрать/тащить, ЛКМ по фону/WASD — панорама, колесо — зум, `R` — сброс.
**Тематические комнаты** (Спальня/Логово/Дойка) имеют слоты: при перетаскивании
персонажа в такую комнату он занимает слот, **меняет скелет на состояние комнаты**
и играет анимацию — **одиночную** (1 в комнате) или **групповую** (2+).
комнат + объекты (фонари/свет). Карта стартует пустой; персонажи создаются **спавнером**
(панель внизу: `+ Раб / Монстр / Ребёнок / Игрок`) и **стоят в idle**, пока их не перетащат.
- **Типы персонажей** (Игрок/Раб/Монстр/Ребёнок) + генерация имён.
- **Тематические комнаты со слотами по типу**: Дойка = 2 раба + 4 ребёнка,
Логово = 1 раб + 1 монстр. Перетаскивание в подходящий слот меняет скелет на
состояние комнаты и играет одиночную/групповую анимацию. Над комнатами — окна с именами.
- **ЛКМ** — выбрать/тащить (в комнату или на пол — там персонаж просто стоит).
**ПКМ** — убрать персонажа в **инвентарь** (панель справа; клик — вернуть).
- `WASD`/ЛКМ-фон — панорама, колесо — зум, `R` — сброс, `Esc` — меню.
2. **Просмотр спрайтов** (`scenes/SpriteViewer.tscn`) — галерея вытащенных изображений
(UI, фоны, эффекты, нарезанные спрайты). `A/D` — картинка, `W/S` — категория, колесо — зум.
3. **Нативные анимации** (`scenes/NativeViewer.tscn`) — браузер всех 38 персонажей нативно:
+62 -125
View File
@@ -21,7 +21,6 @@ public partial class RoomMap : Node2D
private Vector2 _homePos;
private static float FloorY(int row) => row * Cell + Cell * 0.9f;
private float _shaftX; // X тёмной шахты (комнаты друг над другом = скрытая лестница)
private readonly List<Pawn> _pawns = new();
private System.Random _rng;
@@ -31,7 +30,7 @@ public partial class RoomMap : Node2D
private PanelContainer _infoPanel;
private Label _infoLabel;
private enum PState { Idle, Walk, GoShaft, Teleport, Held, Assigned }
private enum PState { Idle, Held, Assigned }
private class Pawn
{
@@ -80,6 +79,12 @@ public partial class RoomMap : Node2D
private static readonly string[] MonsterNames = { "Грок", "Зуг", "Морг", "Рарг", "Тхок", "Гнар", "Урук", "Драк" };
private static readonly string[] ChildNames = { "Пип", "Тимо", "Лулу", "Бэн", "Ния" };
// ростеры для спавнера
private static readonly string[] SlaveRoster = { "elf1", "sister", "satyress", "knight", "shortstack" };
private static readonly string[] MonsterRoster = { "orc", "oni", "man" };
private static readonly string[] ChildRoster = { "baby" };
private static readonly string[] PlayerRoster = { "player" };
// инвентарь снятых персонажей
private class InvItem { public string Name, Scene; public CType Type; }
private readonly List<InvItem> _inventory = new();
@@ -108,9 +113,56 @@ public partial class RoomMap : Node2D
BuildInfoPanel();
BuildInventoryPanel();
BuildSpawner();
ResetView();
}
// ---------- спавнер персонажей ----------
private void BuildSpawner()
{
var bar = new HBoxContainer { Position = new Vector2(16, GetViewportRect().Size.Y - 46) };
bar.AddThemeConstantOverride("separation", 8);
var lbl = new Label { Text = "Спавн:" };
lbl.AddThemeColorOverride("font_color", Colors.White);
lbl.AddThemeConstantOverride("outline_size", 3);
lbl.AddThemeColorOverride("font_outline_color", Colors.Black);
bar.AddChild(lbl);
foreach (var t in new[] { CType.Slave, CType.Monster, CType.Child, CType.Player })
{
var tt = t;
var b = new Button { Text = "+ " + TypeName(t) };
b.Pressed += () => SpawnCharacter(tt);
bar.AddChild(b);
}
AddChild(bar);
}
private void SpawnCharacter(CType type)
{
string[] roster = type == CType.Monster ? MonsterRoster : type == CType.Child ? ChildRoster
: type == CType.Player ? PlayerRoster : SlaveRoster;
string name = roster[_rng.Next(roster.Length)];
string path = ResolveScene(name);
if (path == null) return;
var p = SpawnPawn(path, _rng.Next(Rows));
if (p != null) ShowInfo(p);
}
private static string ResolveScene(string name)
{
if (name == "baby") return "res://characters/baby/human_baby_native.tscn";
foreach (var s in new[] { "combat", "stand" })
{
string path = $"res://characters/{name}/{name}_{s}_native.tscn";
if (ResourceLoader.Exists(path)) return path;
}
var dir = DirAccess.Open($"res://characters/{name}");
if (dir != null)
foreach (var f in dir.GetFiles())
if (f.EndsWith("_native.tscn")) return $"res://characters/{name}/{f}";
return null;
}
private void BuildRoomHeaders()
{
foreach (var r in _rooms)
@@ -185,6 +237,7 @@ public partial class RoomMap : Node2D
p.Node.QueueFree();
_inventory.Add(new InvItem { Name = p.DisplayName, Type = p.Type, Scene = p.BaseScene });
RefreshInventory();
UpdateLabel();
}
private void BuildInfoPanel()
@@ -252,25 +305,8 @@ public partial class RoomMap : Node2D
for (int c = 0; c < Cols; c++)
AddCell("res://assets/room_sliced/Frame.png", c, r, 1, 1, new Color(1, 1, 1, 0.5f));
BuildDarkRooms();
BuildObjects();
BuildPawns();
}
// ---------- тёмная шахта: колонка тёмных комнат друг над другом (скрытая лестница) ----------
private void BuildDarkRooms()
{
const int col = 5; // одна колонка на всю высоту
_shaftX = col * Cell + Cell * 0.5f;
for (int r = 0; r < Rows; r++)
{
// затемнение интерьера ячейки
AddRect(new Rect2(col * Cell + 8, r * Cell + 8, Cell - 16, Cell - 16), new Color(0.02f, 0.02f, 0.05f, 0.94f));
// слабое свечение по центру — вход в шахту
AddTex("res://assets/room_sliced/Light.png",
new Rect2(col * Cell + Cell * 0.3f, r * Cell + Cell * 0.3f, Cell * 0.4f, Cell * 0.4f),
_world, new Color(0.45f, 0.38f, 0.9f, 0.4f));
}
_rng = new System.Random(20260803);
}
private void AddRect(Rect2 r, Color c)
@@ -300,23 +336,6 @@ public partial class RoomMap : Node2D
new Rect2(c * Cell + Cell * 0.28f, r * Cell + Cell * 0.02f, Cell * 0.44f, Cell * 0.44f), _world, new Color(1, 1, 1, 0.75f));
}
// ---------- персонажи, гуляющие по этажам ----------
private void BuildPawns()
{
_rng = new System.Random(20260803);
(string path, int row)[] picks =
{
("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;
@@ -354,8 +373,10 @@ public partial class RoomMap : Node2D
WalkAnim = Pick(anims, "walk_0"), IdleAnim = Pick(anims, "idle_0"),
Type = t, DisplayName = name ?? GenName(t)
};
Decide(p);
p.State = PState.Idle; // просто стоит (никакого блуждания)
PlayIdle(p);
_pawns.Add(p);
UpdateLabel();
return p;
}
@@ -365,90 +386,8 @@ public partial class RoomMap : Node2D
return anims.Length > 0 ? anims[0] : null;
}
private void PlayWalk(Pawn p) { if (p.Ap != null && p.WalkAnim != null) p.Ap.Play(p.WalkAnim); }
private void PlayIdle(Pawn p) { if (p.Ap != null && p.IdleAnim != null) p.Ap.Play(p.IdleAnim); }
// решить: гулять по этажу или уйти в тёмную шахту и перейти на другой этаж
private void Decide(Pawn p)
{
if (Rows > 1 && _rng.NextDouble() < 0.45)
{
p.TargetX = _shaftX;
p.State = PState.GoShaft;
PlayWalk(p);
}
else
{
p.TargetX = (float)(_rng.NextDouble() * (p.MaxX - p.MinX) + p.MinX);
p.State = PState.Walk;
PlayWalk(p);
}
}
private void UpdatePawns(float dt)
{
foreach (var p in _pawns)
{
switch (p.State)
{
case PState.Held:
case PState.Assigned:
break;
case PState.Walk:
case PState.GoShaft:
{
float d = Mathf.Sign(p.TargetX - p.X);
p.X += d * p.Speed * dt;
p.Node.Scale = new Vector2(d >= 0 ? 1f : -1f, 1f);
p.Node.Position = new Vector2(p.X, FloorY(p.Row));
if (Mathf.Abs(p.TargetX - p.X) < 4f)
{
if (p.State == PState.GoShaft)
{
p.X = _shaftX;
do { p.TargetRow = _rng.Next(Rows); } while (p.TargetRow == p.Row);
p.State = PState.Teleport;
p.FadeT = 0f; p.Swapped = false;
PlayIdle(p);
}
else { p.State = PState.Idle; p.IdleTimer = 0.8f + (float)_rng.NextDouble() * 2.2f; PlayIdle(p); }
}
break;
}
case PState.Teleport: // исчезнуть в шахте на одном этаже и появиться на другом
{
const float dur = 0.5f;
p.FadeT += dt / dur;
if (!p.Swapped && p.FadeT >= 0.5f)
{
p.Swapped = true;
p.Row = p.TargetRow;
p.X = _shaftX;
p.Node.Position = new Vector2(_shaftX, FloorY(p.Row));
if (_selected == p) ShowInfo(p);
}
float a = p.FadeT < 0.5f ? 1f - p.FadeT * 2f : (p.FadeT - 0.5f) * 2f;
p.Node.Modulate = new Color(1f, 1f, 1f, Mathf.Clamp(a, 0f, 1f));
if (p.FadeT >= 1f)
{
p.Node.Modulate = Colors.White;
p.State = PState.Idle;
p.IdleTimer = 0.4f + (float)_rng.NextDouble() * 1.5f;
PlayIdle(p);
}
break;
}
case PState.Idle:
p.IdleTimer -= dt;
if (p.IdleTimer <= 0f) Decide(p);
break;
}
}
}
private Pawn PawnAt(Vector2 worldPos)
{
float w = Cell * 0.5f, h = Cell * 0.85f;
@@ -487,7 +426,7 @@ public partial class RoomMap : Node2D
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.State = PState.Idle; p.IdleTimer = 0.5f; PlayIdle(p);
p.State = PState.Idle; PlayIdle(p);
if (_selected == p) ShowInfo(p);
}
@@ -638,8 +577,8 @@ public partial class RoomMap : Node2D
private void UpdateLabel()
{
_label.Text =
$"Карта убежища — {Cols}×{Rows} комнат, тёмная шахта (скрытая лестница), {_pawns.Count} персонажа\n" +
$"ЛКМ по персонажу — выбрать/тащить · ЛКМ по фону/WASD — панорама · колесо — зум · R — сброс · Esc — меню";
$"Карта убежища — {Cols}×{Rows} комнат · персонажей на карте: {_pawns.Count}\n" +
$"Спавн внизу · ЛКМ тащить в комнату/на пол · ПКМ → инвентарь · WASD/ЛКМ-фон — панорама · колесо — зум · Esc — меню";
}
public override void _Process(double delta)
@@ -653,8 +592,6 @@ public partial class RoomMap : Node2D
if (Input.IsKeyPressed(Key.S) || Input.IsKeyPressed(Key.Down)) dir.Y -= 1;
if (dir != Vector2.Zero)
_world.Position += dir * (float)delta * 600f;
UpdatePawns((float)delta);
}
public override void _UnhandledInput(InputEvent @event)