using System.Collections.Generic; using Godot; // Демо-карта «убежища» в разрезе (à la Fallout Shelter): комнаты, лестницы, объекты // и несколько персонажей, гуляющих по этажам. // ЛКМ-перетаскивание или WASD/стрелки — панорама // колесо мыши — зум, R — сброс вида public partial class RoomMap : Node2D { private const float Cell = 256f; private const int Cols = 6; private const int Rows = 5; private const float CharScale = 0.34f; private static readonly Vector2 FootAnchor = new Vector2(640f, 604f); // ноги в кадре 1280x720 private Node2D _world; private Label _label; private bool _dragging; private float _viewZoom = 1f; private Vector2 _homePos; private static float FloorY(int row) => row * Cell + Cell * 0.9f; private float _shaftX; // X тёмной шахты (комнаты друг над другом = скрытая лестница) 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, GoShaft, Teleport, Held } private class Pawn { public Node2D Node; public AnimationPlayer Ap; public string Name; public int Row, TargetRow; public float X, TargetX, Speed, MinX, MaxX, IdleTimer, FadeT; public bool Swapped; public PState State; public string WalkAnim, IdleAnim; } public override void _Ready() { var bg = new ColorRect { Color = new Color(0.06f, 0.05f, 0.04f), // земля вокруг убежища AnchorRight = 1, AnchorBottom = 1, MouseFilter = Control.MouseFilterEnum.Ignore }; AddChild(bg); _world = new Node2D(); AddChild(_world); BuildVault(); _label = new Label { Position = new Vector2(16, 12) }; _label.AddThemeFontSizeOverride("font_size", 18); _label.AddThemeConstantOverride("outline_size", 4); _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() { // тонкая земляная «крышка» над верхним рядом (вместо неба) AddRect(new Rect2(-Cell * 0.5f, -22, Cols * Cell + Cell, 22), new Color(0.11f, 0.09f, 0.07f)); // пол каждой ячейки — пустая комната (шахматкой две вариации) for (int r = 0; r < Rows; r++) for (int c = 0; c < Cols; c++) { string tex = (r + c) % 2 == 0 ? "res://assets/room_sliced/Room_normal.png" : "res://assets/room_sliced/Room_normal2.png"; AddCell(tex, c, r, 1, 1); } // несколько «обставленных» комнат-акцентов поверх пустых AddCell("res://assets/room_sliced/PlayerRoom.png", 1, 0, 3, 1); // широкая на 3 клетки AddCell("res://assets/room_sliced/BreedingRoom.png", 0, 2, 1, 1); AddCell("res://assets/room_sliced/LactationRoom.png", 4, 3, 1, 1); AddCell("res://assets/room_sliced/Light.png", 2, 2, 1, 1); // рамки поверх всех ячеек — читаемость сетки for (int r = 0; r < Rows; r++) for (int c = 0; c < Cols; c++) AddCell("res://assets/room_sliced/Frame.png", c, r, 1, 1, new Color(1, 1, 1, 0.5f)); BuildDarkRooms(); BuildObjects(); BuildPawns(); } // ---------- тёмная шахта: колонка тёмных комнат друг над другом (скрытая лестница) ---------- private void BuildDarkRooms() { const int col = 5; // одна колонка на всю высоту _shaftX = col * Cell + Cell * 0.5f; for (int r = 0; r < Rows; r++) { // затемнение интерьера ячейки AddRect(new Rect2(col * Cell + 8, r * Cell + 8, Cell - 16, Cell - 16), new Color(0.02f, 0.02f, 0.05f, 0.94f)); // слабое свечение по центру — вход в шахту AddTex("res://assets/room_sliced/Light.png", new Rect2(col * Cell + Cell * 0.3f, r * Cell + Cell * 0.3f, Cell * 0.4f, Cell * 0.4f), _world, new Color(0.45f, 0.38f, 0.9f, 0.4f)); } } private void AddRect(Rect2 r, Color c) { var p = new Polygon2D { Color = c, Polygon = new[] { r.Position, new Vector2(r.End.X, r.Position.Y), r.End, new Vector2(r.Position.X, r.End.Y) } }; _world.AddChild(p); } // ---------- объекты в комнатах ---------- private void BuildObjects() { (int c, int r)[] lanterns = { (0, 0), (5, 0), (3, 1), (5, 2), (1, 3), (0, 4), (5, 4) }; foreach (var (c, r) in lanterns) AddTex("res://assets/room_sliced/Lantern.png", new Rect2(c * Cell + Cell * 0.72f, r * Cell + Cell * 0.06f, Cell * 0.16f, Cell * 0.22f), _world, Colors.White); (int c, int r)[] lights = { (1, 1), (3, 3), (4, 0) }; foreach (var (c, r) in lights) AddTex("res://assets/room_sliced/Light.png", new Rect2(c * Cell + Cell * 0.28f, r * Cell + Cell * 0.02f, Cell * 0.44f, Cell * 0.44f), _world, new Color(1, 1, 1, 0.75f)); } // ---------- персонажи, гуляющие по этажам ---------- private void BuildPawns() { _rng = new System.Random(20260803); (string path, int row)[] picks = { ("res://characters/elf1/elf1_combat_native.tscn", 0), ("res://characters/knight/knight_combat_native.tscn", 1), ("res://characters/orc/orc_combat_native.tscn", 2), ("res://characters/sister/sister_combat_native.tscn", 3), ("res://characters/satyress/satyress_combat_native.tscn", 4), }; float minX = Cell * 0.4f, maxX = Cols * Cell - Cell * 0.4f; foreach (var (path, row) in picks) { if (!ResourceLoader.Exists(path)) continue; var scene = ResourceLoader.Load(path).Instantiate(); scene.Position = -FootAnchor * CharScale; scene.Scale = new Vector2(CharScale, CharScale); var node = new Node2D(); node.AddChild(scene); _world.AddChild(node); float x = (float)(_rng.NextDouble() * (maxX - minX) + minX); 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, 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") }; Decide(p); _pawns.Add(p); } } private static string Pick(string[] anims, string want) { foreach (var a in anims) if (a == want) return a; return anims.Length > 0 ? anims[0] : null; } 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 Decide(Pawn p) { if (Rows > 1 && _rng.NextDouble() < 0.45) { p.TargetX = _shaftX; p.State = PState.GoShaft; 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) { switch (p.State) { case PState.Held: break; case PState.Walk: case PState.GoShaft: { 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, FloorY(p.Row)); if (Mathf.Abs(p.TargetX - p.X) < 4f) { if (p.State == PState.GoShaft) { p.X = _shaftX; do { p.TargetRow = _rng.Next(Rows); } while (p.TargetRow == p.Row); p.State = PState.Teleport; p.FadeT = 0f; p.Swapped = false; PlayIdle(p); } else { p.State = PState.Idle; p.IdleTimer = 0.8f + (float)_rng.NextDouble() * 2.2f; PlayIdle(p); } } break; } case PState.Teleport: // исчезнуть в шахте на одном этаже и появиться на другом { const float dur = 0.5f; p.FadeT += dt / dur; if (!p.Swapped && p.FadeT >= 0.5f) { p.Swapped = true; p.Row = p.TargetRow; p.X = _shaftX; p.Node.Position = new Vector2(_shaftX, FloorY(p.Row)); if (_selected == p) ShowInfo(p); } float a = p.FadeT < 0.5f ? 1f - p.FadeT * 2f : (p.FadeT - 0.5f) * 2f; p.Node.Modulate = new Color(1f, 1f, 1f, Mathf.Clamp(a, 0f, 1f)); if (p.FadeT >= 1f) { p.Node.Modulate = Colors.White; 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.Node.Modulate = Colors.White; 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); } private static void AddTex(string path, Rect2 dest, Node2D parent, Color modulate) { var tex = ResourceLoader.Exists(path) ? ResourceLoader.Load(path) : null; if (tex == null) return; var s = new Sprite2D { Texture = tex, Centered = false, Position = dest.Position, Scale = new Vector2(dest.Size.X / tex.GetWidth(), dest.Size.Y / tex.GetHeight()), Modulate = modulate }; parent.AddChild(s); } private void ResetView() { Vector2 vp = GetViewportRect().Size; Vector2 gridSize = new Vector2(Cols * Cell, Rows * Cell); float fit = Mathf.Min(vp.X * 0.9f / gridSize.X, vp.Y * 0.8f / gridSize.Y); _viewZoom = fit; _world.Scale = new Vector2(fit, fit); _homePos = (vp - gridSize * fit) * 0.5f; _world.Position = _homePos; UpdateLabel(); } private void ZoomAt(Vector2 screenPoint, float factor) { float newZoom = Mathf.Clamp(_viewZoom * factor, 0.1f, 3f); factor = newZoom / _viewZoom; _viewZoom = newZoom; // удерживаем точку под курсором на месте _world.Position = screenPoint - (screenPoint - _world.Position) * factor; _world.Scale = new Vector2(_viewZoom, _viewZoom); UpdateLabel(); } private void UpdateLabel() { _label.Text = $"Карта убежища — {Cols}×{Rows} комнат, тёмная шахта (скрытая лестница), {_pawns.Count} персонажа\n" + $"ЛКМ по персонажу — выбрать/тащить · ЛКМ по фону/WASD — панорама · колесо — зум · R — сброс · Esc — меню"; } public override void _Process(double delta) { if (!Visible) return; Vector2 dir = Vector2.Zero; if (Input.IsKeyPressed(Key.A) || Input.IsKeyPressed(Key.Left)) dir.X += 1; if (Input.IsKeyPressed(Key.D) || Input.IsKeyPressed(Key.Right)) dir.X -= 1; if (Input.IsKeyPressed(Key.W) || Input.IsKeyPressed(Key.Up)) dir.Y += 1; if (Input.IsKeyPressed(Key.S) || Input.IsKeyPressed(Key.Down)) dir.Y -= 1; if (dir != Vector2.Zero) _world.Position += dir * (float)delta * 600f; UpdatePawns((float)delta); } public override void _UnhandledInput(InputEvent @event) { if (@event is InputEventKey esc && esc.Pressed && esc.Keycode == Key.Escape) { GetTree().ChangeSceneToFile("res://scenes/MainMenu.tscn"); return; } if (@event is InputEventMouseButton mb) { if (mb.ButtonIndex == MouseButton.Left) { 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) { 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; }