Подземелье: ходьба между этажами по лестницам + клик/перетаскивание персонажей
- Машина состояний ходоков: гулять по этажу / идти к лестнице / лезть вверх-вниз - ЛКМ по персонажу — панель с именем и этажом - Перетаскивание персонажа мышью → бросок в ближайшую комнату (снап к ячейке) 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
+154
-31
@@ -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,41 +212,104 @@ 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);
|
||||
case PState.Held:
|
||||
break;
|
||||
|
||||
case PState.Walk:
|
||||
case PState.GoLadder:
|
||||
{
|
||||
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, p.FloorY);
|
||||
if (Mathf.Abs(p.TargetX - p.X) < 4f) StartIdle(p);
|
||||
}
|
||||
else
|
||||
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)
|
||||
if (p.IdleTimer <= 0f) Decide(p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Pawn PawnAt(Vector2 worldPos)
|
||||
{
|
||||
p.TargetX = (float)(_rng.NextDouble() * (p.MaxX - p.MinX) + p.MinX);
|
||||
StartWalk(p);
|
||||
}
|
||||
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)
|
||||
@@ -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,14 +388,41 @@ 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)
|
||||
{
|
||||
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)
|
||||
@@ -309,4 +430,6 @@ public partial class RoomMap : Node2D
|
||||
ResetView();
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 ScreenToWorld(Vector2 s) => (s - _world.Position) / _world.Scale;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user