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

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

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 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 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
{
public Node2D Node;
public AnimationPlayer Ap;
public float X, TargetX, FloorY, Speed, MinX, MaxX, IdleTimer;
public bool Walking;
public string Name;
public int Row, TargetRow;
public float X, TargetX, TargetY, Speed, MinX, MaxX, IdleTimer, LadderX;
public PState State;
public string WalkAnim, IdleAnim;
}
@@ -52,9 +64,29 @@ public partial class RoomMap : Node2D
_label.AddThemeColorOverride("font_outline_color", Colors.Black);
AddChild(_label);
BuildInfoPanel();
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()
{
// тонкая земляная «крышка» над верхним рядом (вместо неба)
@@ -157,20 +189,19 @@ public partial class RoomMap : Node2D
node.AddChild(scene);
_world.AddChild(node);
float floorY = row * Cell + Cell * 0.9f;
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");
string[] anims = ap != null ? ap.GetAnimationList() : System.Array.Empty<string>();
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,
WalkAnim = Pick(anims, "walk_0"), IdleAnim = Pick(anims, "idle_0"),
TargetX = (float)(_rng.NextDouble() * (maxX - minX) + minX)
WalkAnim = Pick(anims, "walk_0"), IdleAnim = Pick(anims, "idle_0")
};
StartWalk(p);
Decide(p);
_pawns.Add(p);
}
}
@@ -181,43 +212,106 @@ public partial class RoomMap : Node2D
return anims.Length > 0 ? anims[0] : null;
}
private static void StartWalk(Pawn p)
{
p.Walking = true;
if (p.Ap != null && p.WalkAnim != null) p.Ap.Play(p.WalkAnim);
}
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 StartIdle(Pawn p)
// решить, что делать после простоя: гулять по этажу или лезть на другой этаж
private void Decide(Pawn p)
{
p.Walking = false;
p.IdleTimer = 0.8f + (float)_rng.NextDouble() * 2.2f;
if (p.Ap != null && p.IdleAnim != null) p.Ap.Play(p.IdleAnim);
bool canUp = p.Row > 0, canDown = p.Row < Rows - 1;
if (_rng.NextDouble() < 0.5 && (canUp || canDown))
{
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)
{
foreach (var p in _pawns)
{
if (p.Walking)
switch (p.State)
{
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, p.FloorY);
if (Mathf.Abs(p.TargetX - p.X) < 4f) StartIdle(p);
}
else
{
p.IdleTimer -= dt;
if (p.IdleTimer <= 0f)
case PState.Held:
break;
case PState.Walk:
case PState.GoLadder:
{
p.TargetX = (float)(_rng.NextDouble() * (p.MaxX - p.MinX) + p.MinX);
StartWalk(p);
float tx = p.State == PState.GoLadder ? p.LadderX : p.TargetX;
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)
{
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()
{
_label.Text =
$"Карта убежища — {Cols}×{Rows} комнат, лестницы, объекты, {_pawns.Count} персонажа гуляют\n" +
$"ЛКМ/WASD — панорама · колесо — зум (x{_viewZoom:0.00}) · R — сброс · Esc — меню";
$"Карта убежища — {Cols}×{Rows} комнат, лестницы, {_pawns.Count} персонажа (ходят по этажам)\n" +
$"ЛКМ по персонажу — выбрать/тащить · ЛКМ по фону/WASD — панорама · колесо — зум · R — сброс · Esc — меню";
}
public override void _Process(double delta)
@@ -294,19 +388,48 @@ public partial class RoomMap : Node2D
if (@event is InputEventMouseButton mb)
{
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)
ZoomAt(mb.Position, 1.1f);
else if (mb.Pressed && mb.ButtonIndex == MouseButton.WheelDown)
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)
{
ResetView();
}
}
private Vector2 ScreenToWorld(Vector2 s) => (s - _world.Position) / _world.Scale;
}