Подземелье: ходьба между этажами по лестницам + клик/перетаскивание персонажей
- Машина состояний ходоков: гулять по этажу / идти к лестнице / лезть вверх-вниз - ЛКМ по персонажу — панель с именем и этажом - Перетаскивание персонажа мышью → бросок в ближайшую комнату (снап к ячейке) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
935f4afc7e
commit
46fdb230fe
+158
-35
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user