Подземелье: ходьба между этажами по лестницам + клик/перетаскивание персонажей

- Машина состояний ходоков: гулять по этажу / идти к лестнице / лезть вверх-вниз
- ЛКМ по персонажу — панель с именем и этажом
- Перетаскивание персонажа мышью → бросок в ближайшую комнату (снап к ячейке)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-08-03 17:13:27 +03:00
co-authored by Claude Opus 4.8
parent 935f4afc7e
commit 46fdb230fe
+158 -35
View File
@@ -19,15 +19,27 @@ public partial class RoomMap : Node2D
private float _viewZoom = 1f; private float _viewZoom = 1f;
private Vector2 _homePos; private Vector2 _homePos;
private static readonly float[] LadderXs = { 2f * Cell, 4f * Cell };
private static float FloorY(int row) => row * Cell + Cell * 0.9f;
private readonly List<Pawn> _pawns = new(); private readonly List<Pawn> _pawns = new();
private System.Random _rng; private System.Random _rng;
private Pawn _dragPawn, _selected;
private Vector2 _dragOffset;
private PanelContainer _infoPanel;
private Label _infoLabel;
private enum PState { Idle, Walk, GoLadder, Climb, Held }
private class Pawn private class Pawn
{ {
public Node2D Node; public Node2D Node;
public AnimationPlayer Ap; public AnimationPlayer Ap;
public float X, TargetX, FloorY, Speed, MinX, MaxX, IdleTimer; public string Name;
public bool Walking; public int Row, TargetRow;
public float X, TargetX, TargetY, Speed, MinX, MaxX, IdleTimer, LadderX;
public PState State;
public string WalkAnim, IdleAnim; public string WalkAnim, IdleAnim;
} }
@@ -52,9 +64,29 @@ public partial class RoomMap : Node2D
_label.AddThemeColorOverride("font_outline_color", Colors.Black); _label.AddThemeColorOverride("font_outline_color", Colors.Black);
AddChild(_label); AddChild(_label);
BuildInfoPanel();
ResetView(); ResetView();
} }
private void BuildInfoPanel()
{
_infoPanel = new PanelContainer { Position = new Vector2(16, 64), Visible = false };
var margin = new MarginContainer();
foreach (var s in new[] { "margin_left", "margin_right", "margin_top", "margin_bottom" })
margin.AddThemeConstantOverride(s, 10);
_infoPanel.AddChild(margin);
_infoLabel = new Label();
margin.AddChild(_infoLabel);
AddChild(_infoPanel);
}
private void ShowInfo(Pawn p)
{
_selected = p;
_infoLabel.Text = $"Персонаж: {p.Name}\nЭтаж: {p.Row + 1}/{Rows}\nтащи мышью → перенести в комнату";
_infoPanel.Visible = true;
}
private void BuildVault() private void BuildVault()
{ {
// тонкая земляная «крышка» над верхним рядом (вместо неба) // тонкая земляная «крышка» над верхним рядом (вместо неба)
@@ -157,20 +189,19 @@ public partial class RoomMap : Node2D
node.AddChild(scene); node.AddChild(scene);
_world.AddChild(node); _world.AddChild(node);
float floorY = row * Cell + Cell * 0.9f;
float x = (float)(_rng.NextDouble() * (maxX - minX) + minX); float x = (float)(_rng.NextDouble() * (maxX - minX) + minX);
node.Position = new Vector2(x, floorY); node.Position = new Vector2(x, FloorY(row));
var ap = scene.GetNodeOrNull<AnimationPlayer>("AnimationPlayer"); var ap = scene.GetNodeOrNull<AnimationPlayer>("AnimationPlayer");
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, X = x, FloorY = floorY, MinX = minX, MaxX = maxX, Node = node, Ap = ap, Name = path.Split('/')[3], Row = row,
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")
TargetX = (float)(_rng.NextDouble() * (maxX - minX) + minX)
}; };
StartWalk(p); Decide(p);
_pawns.Add(p); _pawns.Add(p);
} }
} }
@@ -181,43 +212,106 @@ public partial class RoomMap : Node2D
return anims.Length > 0 ? anims[0] : null; return anims.Length > 0 ? anims[0] : null;
} }
private static void StartWalk(Pawn p) 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); }
p.Walking = true;
if (p.Ap != null && p.WalkAnim != null) p.Ap.Play(p.WalkAnim);
}
private void StartIdle(Pawn p) // решить, что делать после простоя: гулять по этажу или лезть на другой этаж
private void Decide(Pawn p)
{ {
p.Walking = false; bool canUp = p.Row > 0, canDown = p.Row < Rows - 1;
p.IdleTimer = 0.8f + (float)_rng.NextDouble() * 2.2f; if (_rng.NextDouble() < 0.5 && (canUp || canDown))
if (p.Ap != null && p.IdleAnim != null) p.Ap.Play(p.IdleAnim); {
p.LadderX = LadderXs[_rng.Next(LadderXs.Length)];
int dir = (canUp && canDown) ? (_rng.NextDouble() < 0.5 ? -1 : 1) : (canUp ? -1 : 1);
p.TargetRow = p.Row + dir;
p.TargetX = p.LadderX;
p.State = PState.GoLadder;
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) private void UpdatePawns(float dt)
{ {
foreach (var p in _pawns) foreach (var p in _pawns)
{ {
if (p.Walking) switch (p.State)
{ {
float d = Mathf.Sign(p.TargetX - p.X); case PState.Held:
p.X += d * p.Speed * dt; break;
p.Node.Scale = new Vector2(d >= 0 ? 1f : -1f, 1f);
p.Node.Position = new Vector2(p.X, p.FloorY); case PState.Walk:
if (Mathf.Abs(p.TargetX - p.X) < 4f) StartIdle(p); case PState.GoLadder:
}
else
{
p.IdleTimer -= dt;
if (p.IdleTimer <= 0f)
{ {
p.TargetX = (float)(_rng.NextDouble() * (p.MaxX - p.MinX) + p.MinX); float tx = p.State == PState.GoLadder ? p.LadderX : p.TargetX;
StartWalk(p); float d = Mathf.Sign(tx - 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(tx - p.X) < 4f)
{
if (p.State == PState.GoLadder)
{
p.X = p.LadderX;
p.TargetY = FloorY(p.TargetRow);
p.State = PState.Climb;
}
else { p.State = PState.Idle; p.IdleTimer = 0.8f + (float)_rng.NextDouble() * 2.2f; PlayIdle(p); }
}
break;
} }
case PState.Climb:
{
float cy = p.Node.Position.Y;
cy += Mathf.Sign(p.TargetY - cy) * p.Speed * dt;
p.Node.Position = new Vector2(p.LadderX, cy);
p.Node.Scale = new Vector2(1f, 1f);
if (Mathf.Abs(p.TargetY - cy) < 4f)
{
p.Row = p.TargetRow;
p.Node.Position = new Vector2(p.LadderX, FloorY(p.Row));
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;
for (int i = _pawns.Count - 1; i >= 0; i--)
{
var p = _pawns[i];
var r = new Rect2(p.Node.Position.X - w / 2f, p.Node.Position.Y - h, w, h);
if (r.HasPoint(worldPos)) return p;
}
return null;
}
private void DropPawn(Pawn p)
{
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);
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);
if (_selected == p) ShowInfo(p);
}
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);
@@ -265,8 +359,8 @@ public partial class RoomMap : Node2D
private void UpdateLabel() private void UpdateLabel()
{ {
_label.Text = _label.Text =
$"Карта убежища — {Cols}×{Rows} комнат, лестницы, объекты, {_pawns.Count} персонажа гуляют\n" + $"Карта убежища — {Cols}×{Rows} комнат, лестницы, {_pawns.Count} персонажа (ходят по этажам)\n" +
$"ЛКМ/WASD — панорама · колесо — зум (x{_viewZoom:0.00}) · R — сброс · Esc — меню"; $"ЛКМ по персонажу — выбрать/тащить · ЛКМ по фону/WASD — панорама · колесо — зум · R — сброс · Esc — меню";
} }
public override void _Process(double delta) public override void _Process(double delta)
@@ -294,19 +388,48 @@ public partial class RoomMap : Node2D
if (@event is InputEventMouseButton mb) if (@event is InputEventMouseButton mb)
{ {
if (mb.ButtonIndex == MouseButton.Left) if (mb.ButtonIndex == MouseButton.Left)
_dragging = mb.Pressed; {
if (mb.Pressed)
{
var hit = PawnAt(ScreenToWorld(mb.Position));
if (hit != null)
{
_dragPawn = hit;
hit.State = PState.Held;
_dragOffset = hit.Node.Position - ScreenToWorld(mb.Position);
PlayIdle(hit);
ShowInfo(hit);
}
else
{
_dragging = true;
_infoPanel.Visible = false;
_selected = null;
}
}
else
{
if (_dragPawn != null) { DropPawn(_dragPawn); _dragPawn = null; }
_dragging = false;
}
}
else if (mb.Pressed && mb.ButtonIndex == MouseButton.WheelUp) else if (mb.Pressed && mb.ButtonIndex == MouseButton.WheelUp)
ZoomAt(mb.Position, 1.1f); ZoomAt(mb.Position, 1.1f);
else if (mb.Pressed && mb.ButtonIndex == MouseButton.WheelDown) else if (mb.Pressed && mb.ButtonIndex == MouseButton.WheelDown)
ZoomAt(mb.Position, 1f / 1.1f); ZoomAt(mb.Position, 1f / 1.1f);
} }
else if (@event is InputEventMouseMotion mm && _dragging) else if (@event is InputEventMouseMotion mm)
{ {
_world.Position += mm.Relative; if (_dragPawn != null)
_dragPawn.Node.Position = ScreenToWorld(mm.Position) + _dragOffset;
else if (_dragging)
_world.Position += mm.Relative;
} }
else if (@event is InputEventKey k && k.Pressed && !k.Echo && k.Keycode == Key.R) else if (@event is InputEventKey k && k.Pressed && !k.Echo && k.Keycode == Key.R)
{ {
ResetView(); ResetView();
} }
} }
private Vector2 ScreenToWorld(Vector2 s) => (s - _world.Position) / _world.Scale;
} }