Подземелье: назначение персонажей в тематические комнаты + анимации

- Комнаты Спальня/Логово/Дойка со слотами
- Перетаскивание в комнату: персонаж занимает слот, скелет меняется на состояние
  комнаты (milk/birth/...), играет одиночную (1) или групповую (2+) анимацию
- Клетка сброса берётся по центру тела (фикс съезда на строку ниже)
- Панель показывает назначение/слот/режим анимации

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-08-03 17:44:00 +03:00
co-authored by Claude Opus 4.8
parent a254309ec9
commit 70b65fde0c
2 changed files with 132 additions and 8 deletions
+4 -2
View File
@@ -29,8 +29,10 @@ D:\Godot_v4.7.1\Godot_v4.7.1-stable_mono_win64.exe
комнат, **тёмная шахта** (колонка тёмных комнат друг над другом = скрытая лестница; комнат, **тёмная шахта** (колонка тёмных комнат друг над другом = скрытая лестница;
персонаж заходит и проходит по ней на другой этаж, анимация — боевой `walk_0`), персонаж заходит и проходит по ней на другой этаж, анимация — боевой `walk_0`),
**объекты** (фонари/свет) и **несколько персонажей** на этажах. **объекты** (фонари/свет) и **несколько персонажей** на этажах.
ЛКМ по персонажу — выбрать/тащить (перенос в комнату), ЛКМ по фону/WASD — панорама, ЛКМ по персонажу — выбрать/тащить, ЛКМ по фону/WASD — панорама, колесо — зум, `R` — сброс.
колесо — зум, `R` — сброс. **Тематические комнаты** (Спальня/Логово/Дойка) имеют слоты: при перетаскивании
персонажа в такую комнату он занимает слот, **меняет скелет на состояние комнаты**
и играет анимацию — **одиночную** (1 в комнате) или **групповую** (2+).
2. **Просмотр спрайтов** (`scenes/SpriteViewer.tscn`) — галерея вытащенных изображений 2. **Просмотр спрайтов** (`scenes/SpriteViewer.tscn`) — галерея вытащенных изображений
(UI, фоны, эффекты, нарезанные спрайты). `A/D` — картинка, `W/S` — категория, колесо — зум. (UI, фоны, эффекты, нарезанные спрайты). `A/D` — картинка, `W/S` — категория, колесо — зум.
3. **Нативные анимации** (`scenes/NativeViewer.tscn`) — браузер всех 38 персонажей нативно: 3. **Нативные анимации** (`scenes/NativeViewer.tscn`) — браузер всех 38 персонажей нативно:
+128 -6
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Godot; using Godot;
// Демо-карта «убежища» в разрезе (à la Fallout Shelter): комнаты, лестницы, объекты // Демо-карта «убежища» в разрезе (à la Fallout Shelter): комнаты, лестницы, объекты
@@ -30,20 +31,32 @@ public partial class RoomMap : Node2D
private PanelContainer _infoPanel; private PanelContainer _infoPanel;
private Label _infoLabel; private Label _infoLabel;
private enum PState { Idle, Walk, GoShaft, Teleport, Held } private enum PState { Idle, Walk, GoShaft, Teleport, Held, Assigned }
private class Pawn private class Pawn
{ {
public Node2D Node; public Node2D Node;
public AnimationPlayer Ap; public AnimationPlayer Ap;
public string Name; public string Name, BaseScene;
public int Row, TargetRow; public int Row, TargetRow;
public float X, TargetX, Speed, MinX, MaxX, IdleTimer, FadeT; public float X, TargetX, Speed, MinX, MaxX, IdleTimer, FadeT;
public bool Swapped; public bool Swapped;
public PState State; public PState State;
public string WalkAnim, IdleAnim; 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<Pawn> Occ = new();
}
private List<Room> _rooms;
public override void _Ready() public override void _Ready()
{ {
var bg = new ColorRect var bg = new ColorRect
@@ -84,7 +97,14 @@ public partial class RoomMap : Node2D
private void ShowInfo(Pawn p) private void ShowInfo(Pawn p)
{ {
_selected = 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; _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/LactationRoom.png", 4, 3, 1, 1);
AddCell("res://assets/room_sliced/Light.png", 2, 2, 1, 1); AddCell("res://assets/room_sliced/Light.png", 2, 2, 1, 1);
_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[] { "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 r = 0; r < Rows; r++)
for (int c = 0; c < Cols; c++) 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<string>(); string[] anims = ap != null ? ap.GetAnimationList() : System.Array.Empty<string>();
var p = new Pawn 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, X = x, MinX = minX, MaxX = maxX,
Speed = 70f + (float)_rng.NextDouble() * 55f, Speed = 70f + (float)_rng.NextDouble() * 55f,
WalkAnim = Pick(anims, "walk_0"), IdleAnim = Pick(anims, "idle_0") WalkAnim = Pick(anims, "walk_0"), IdleAnim = Pick(anims, "idle_0")
@@ -237,6 +264,7 @@ public partial class RoomMap : Node2D
switch (p.State) switch (p.State)
{ {
case PState.Held: case PState.Held:
case PState.Assigned:
break; break;
case PState.Walk: case PState.Walk:
@@ -307,16 +335,110 @@ public partial class RoomMap : Node2D
private void DropPawn(Pawn p) private void DropPawn(Pawn p)
{ {
p.Node.Modulate = Colors.White;
// клетку берём по ЦЕНТРУ тела, а не по ногам (иначе съезжает на строку ниже)
int col = Mathf.Clamp((int)(p.Node.Position.X / Cell), 0, Cols - 1); 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.Row = row;
p.X = Mathf.Clamp(col * Cell + Cell * 0.5f, p.MinX, p.MaxX); p.X = Mathf.Clamp(col * Cell + Cell * 0.5f, p.MinX, p.MaxX);
p.Node.Position = new Vector2(p.X, FloorY(row)); p.Node.Position = new Vector2(p.X, FloorY(row));
p.Node.Modulate = Colors.White;
p.State = PState.Idle; p.IdleTimer = 0.5f; PlayIdle(p); p.State = PState.Idle; p.IdleTimer = 0.5f; PlayIdle(p);
if (_selected == p) ShowInfo(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>();
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<PackedScene>(path).Instantiate<Node2D>();
scene.Position = -FootAnchor * CharScale;
scene.Scale = new Vector2(CharScale, CharScale);
p.Node.AddChild(scene);
p.Ap = scene.GetNodeOrNull<AnimationPlayer>("AnimationPlayer");
var anims = p.Ap != null ? p.Ap.GetAnimationList() : System.Array.Empty<string>();
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) 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); AddTex(path, new Rect2(col * Cell, row * Cell, cw * Cell, ch * Cell), _world, modulate ?? Colors.White);