From 46fdb230fe7cbc6932cf341f42ca8c964a35c592 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Mon, 3 Aug 2026 17:13:27 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=B7=D0=B5=D0=BC=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=D0=B5:=20=D1=85=D0=BE=D0=B4=D1=8C=D0=B1=D0=B0=20?= =?UTF-8?q?=D0=BC=D0=B5=D0=B6=D0=B4=D1=83=20=D1=8D=D1=82=D0=B0=D0=B6=D0=B0?= =?UTF-8?q?=D0=BC=D0=B8=20=D0=BF=D0=BE=20=D0=BB=D0=B5=D1=81=D1=82=D0=BD?= =?UTF-8?q?=D0=B8=D1=86=D0=B0=D0=BC=20+=20=D0=BA=D0=BB=D0=B8=D0=BA/=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D1=82=D0=B0=D1=81=D0=BA=D0=B8=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=B5=D1=80=D1=81=D0=BE=D0=BD=D0=B0?= =?UTF-8?q?=D0=B6=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Машина состояний ходоков: гулять по этажу / идти к лестнице / лезть вверх-вниз - ЛКМ по персонажу — панель с именем и этажом - Перетаскивание персонажа мышью → бросок в ближайшую комнату (снап к ячейке) Co-Authored-By: Claude Opus 4.8 --- src/RoomMap.cs | 193 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 158 insertions(+), 35 deletions(-) diff --git a/src/RoomMap.cs b/src/RoomMap.cs index 9c097a4..d91b180 100644 --- a/src/RoomMap.cs +++ b/src/RoomMap.cs @@ -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 _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"); string[] anims = ap != null ? ap.GetAnimationList() : System.Array.Empty(); 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; }