Compare commits
13
Commits
a0e08cf146
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a754d618d | ||
|
|
214d6e3c5b | ||
|
|
bb16605129 | ||
|
|
4350f4a9e7 | ||
|
|
0882ecdf87 | ||
|
|
4df5a73ec0 | ||
|
|
70b65fde0c | ||
|
|
a254309ec9 | ||
|
|
085cf3e5fa | ||
|
|
2a629adf2c | ||
|
|
46fdb230fe | ||
|
|
935f4afc7e | ||
|
|
b048a83966 |
@@ -26,7 +26,15 @@ D:\Godot_v4.7.1\Godot_v4.7.1-stable_mono_win64.exe
|
||||
Главное меню (`scenes/MainMenu.tscn`) ведёт на три сцены (назад в меню — `Esc`):
|
||||
|
||||
1. **Подземелье** (`scenes/Dungeon.tscn`) — разрез убежища à la Fallout Shelter: сетка 6×5
|
||||
комнат. ЛКМ/WASD — панорама, колесо — зум, `R` — сброс.
|
||||
комнат + объекты (фонари/свет). Карта стартует пустой; персонажи создаются **спавнером**
|
||||
(панель внизу: кнопки типов `+ Раб/Монстр/Ребёнок/Игрок` для случайного, или выпадающий список конкретного персонажа + «Заспавнить») и **стоят в idle**, пока их не перетащат.
|
||||
- **Типы персонажей** (Игрок/Раб/Монстр/Ребёнок) + генерация имён.
|
||||
- **Тематические комнаты со слотами по типу**: Дойка = 2 раба + 4 ребёнка,
|
||||
Логово = 1 раб + 1 монстр. Перетаскивание в подходящий слот меняет скелет на
|
||||
состояние комнаты и играет одиночную/групповую анимацию. Над комнатами — окна с именами.
|
||||
- **ЛКМ** — выбрать/тащить (в комнату или на пол — там персонаж просто стоит).
|
||||
**ПКМ** — убрать персонажа в **инвентарь** (панель справа; клик — вернуть).
|
||||
- `WASD`/ЛКМ-фон — панорама, колесо — зум, `R` — сброс, `Esc` — меню.
|
||||
2. **Просмотр спрайтов** (`scenes/SpriteViewer.tscn`) — галерея вытащенных изображений
|
||||
(UI, фоны, эффекты, нарезанные спрайты). `A/D` — картинка, `W/S` — категория, колесо — зум.
|
||||
3. **Нативные анимации** (`scenes/NativeViewer.tscn`) — браузер всех 38 персонажей нативно:
|
||||
|
||||
@@ -19,6 +19,11 @@ config/icon="res://icon.svg"
|
||||
|
||||
project/assembly_name="ProjectRR"
|
||||
|
||||
[editor]
|
||||
|
||||
version_control/plugin_name="GitPlugin"
|
||||
version_control/autoload_on_startup=true
|
||||
|
||||
[physics]
|
||||
|
||||
3d/physics_engine="Jolt Physics"
|
||||
|
||||
+567
-9
@@ -1,6 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Godot;
|
||||
|
||||
// Демо-карта «убежища» в разрезе (à la Fallout Shelter): сетка комнат из игровых текстур.
|
||||
// Демо-карта «убежища» в разрезе (à la Fallout Shelter): комнаты, лестницы, объекты
|
||||
// и несколько персонажей, гуляющих по этажам.
|
||||
// ЛКМ-перетаскивание или WASD/стрелки — панорама
|
||||
// колесо мыши — зум, R — сброс вида
|
||||
public partial class RoomMap : Node2D
|
||||
@@ -8,6 +11,8 @@ 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;
|
||||
@@ -15,6 +20,77 @@ public partial class RoomMap : Node2D
|
||||
private float _viewZoom = 1f;
|
||||
private Vector2 _homePos;
|
||||
|
||||
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, Held, Assigned }
|
||||
|
||||
private class Pawn
|
||||
{
|
||||
public Node2D Node;
|
||||
public AnimationPlayer Ap;
|
||||
public string Name, BaseScene, DisplayName;
|
||||
public int Row, TargetRow;
|
||||
public float X, TargetX, Speed, MinX, MaxX, IdleTimer, FadeT;
|
||||
public bool Swapped;
|
||||
public PState State;
|
||||
public string WalkAnim, IdleAnim;
|
||||
public Room AssignedRoom;
|
||||
public CType Type;
|
||||
}
|
||||
|
||||
// тематическая комната с типизированными слотами
|
||||
private class Room
|
||||
{
|
||||
public string Type;
|
||||
public int Col, Row, Span;
|
||||
public string[] States; // приоритет состояний-скелетов
|
||||
public CType[] SlotTypes; // тип, разрешённый в каждом слоте
|
||||
public Pawn[] SlotOcc; // кто в слоте (null = свободно)
|
||||
public Vector2?[] SlotPos; // точная позиция слота (null = авто)
|
||||
public Label Header;
|
||||
public int Cap => SlotTypes.Length;
|
||||
public void Init() { SlotOcc = new Pawn[SlotTypes.Length]; SlotPos = new Vector2?[SlotTypes.Length]; }
|
||||
public int Count() { int n = 0; foreach (var s in SlotOcc) if (s != null) n++; return n; }
|
||||
public int SlotOf(Pawn p) => System.Array.IndexOf(SlotOcc, p);
|
||||
public int FreeSlotFor(CType t)
|
||||
{
|
||||
for (int i = 0; i < SlotTypes.Length; i++) if (SlotOcc[i] == null && SlotTypes[i] == t) return i;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Room> _rooms;
|
||||
|
||||
// типы персонажей
|
||||
private enum CType { Player, Slave, Monster, Child }
|
||||
private static string TypeName(CType t) => t switch
|
||||
{
|
||||
CType.Player => "Игрок", CType.Slave => "Раб", CType.Monster => "Монстр", CType.Child => "Ребёнок", _ => "?"
|
||||
};
|
||||
private static readonly string[] Monsters = { "orc", "goblin", "horse", "oni", "man" };
|
||||
private static readonly string[] SlaveNames = { "Ариа", "Луна", "Мира", "Элара", "Ника", "Рина", "Кая", "Тэя", "Лира", "Мэй", "Иврис", "Сая" };
|
||||
private static readonly string[] MonsterNames = { "Грок", "Зуг", "Морг", "Рарг", "Тхок", "Гнар", "Урук", "Драк" };
|
||||
private static readonly string[] ChildNames = { "Пип", "Тимо", "Лулу", "Бэн", "Ния" };
|
||||
|
||||
// ростеры для спавнера
|
||||
private static readonly string[] SlaveRoster = { "elf1", "sister", "satyress", "knight", "shortstack" };
|
||||
private static readonly string[] MonsterRoster = { "orc", "oni", "man" };
|
||||
private static readonly string[] ChildRoster = { "baby" };
|
||||
private static readonly string[] PlayerRoster = { "player" };
|
||||
|
||||
// инвентарь снятых персонажей
|
||||
private class InvItem { public string Name, Scene; public CType Type; }
|
||||
private readonly List<InvItem> _inventory = new();
|
||||
private VBoxContainer _invBox;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
var bg = new ColorRect
|
||||
@@ -36,14 +112,206 @@ public partial class RoomMap : Node2D
|
||||
_label.AddThemeColorOverride("font_outline_color", Colors.Black);
|
||||
AddChild(_label);
|
||||
|
||||
BuildInfoPanel();
|
||||
BuildInventoryPanel();
|
||||
BuildSpawner();
|
||||
ResetView();
|
||||
}
|
||||
|
||||
// ---------- спавнер персонажей ----------
|
||||
private OptionButton _charPick;
|
||||
|
||||
private void BuildSpawner()
|
||||
{
|
||||
var bar = new HBoxContainer { Position = new Vector2(16, GetViewportRect().Size.Y - 46) };
|
||||
bar.AddThemeConstantOverride("separation", 8);
|
||||
bar.AddChild(BarLabel("Спавн (тип):"));
|
||||
foreach (var t in new[] { CType.Slave, CType.Monster, CType.Child, CType.Player })
|
||||
{
|
||||
var tt = t;
|
||||
var b = new Button { Text = "+ " + TypeName(t) };
|
||||
b.Pressed += () => SpawnCharacter(tt);
|
||||
bar.AddChild(b);
|
||||
}
|
||||
bar.AddChild(BarLabel("| персонаж:"));
|
||||
_charPick = new OptionButton();
|
||||
foreach (var name in SpawnableCharacters()) _charPick.AddItem(name);
|
||||
bar.AddChild(_charPick);
|
||||
var spawnBtn = new Button { Text = "Заспавнить" };
|
||||
spawnBtn.Pressed += () =>
|
||||
{
|
||||
if (_charPick.Selected >= 0) SpawnByName(_charPick.GetItemText(_charPick.Selected));
|
||||
};
|
||||
bar.AddChild(spawnBtn);
|
||||
AddChild(bar);
|
||||
}
|
||||
|
||||
private static Label BarLabel(string text)
|
||||
{
|
||||
var l = new Label { Text = text };
|
||||
l.AddThemeColorOverride("font_color", Colors.White);
|
||||
l.AddThemeConstantOverride("outline_size", 3);
|
||||
l.AddThemeColorOverride("font_outline_color", Colors.Black);
|
||||
return l;
|
||||
}
|
||||
|
||||
private static List<string> SpawnableCharacters()
|
||||
{
|
||||
var list = new List<string>();
|
||||
var root = DirAccess.Open("res://characters");
|
||||
if (root != null)
|
||||
foreach (var d in root.GetDirectories())
|
||||
if (ResolveScene(d) != null) list.Add(d);
|
||||
list.Sort();
|
||||
return list;
|
||||
}
|
||||
|
||||
private void SpawnCharacter(CType type)
|
||||
{
|
||||
string[] roster = type == CType.Monster ? MonsterRoster : type == CType.Child ? ChildRoster
|
||||
: type == CType.Player ? PlayerRoster : SlaveRoster;
|
||||
SpawnByName(roster[_rng.Next(roster.Length)]);
|
||||
}
|
||||
|
||||
private void SpawnByName(string name)
|
||||
{
|
||||
string path = ResolveScene(name);
|
||||
if (path == null) return;
|
||||
var p = SpawnPawn(path, _rng.Next(Rows));
|
||||
if (p != null) ShowInfo(p);
|
||||
}
|
||||
|
||||
private static readonly string[] BadStems =
|
||||
{ "portrait", "fetus", "suckle", "_body", "_head", "dead", "birth", "milk", "down", "baby" };
|
||||
|
||||
private static string ResolveScene(string name)
|
||||
{
|
||||
if (name == "baby") return "res://characters/baby/human_baby_native.tscn";
|
||||
var dir = DirAccess.Open($"res://characters/{name}");
|
||||
if (dir == null) return null;
|
||||
var files = new List<string>();
|
||||
foreach (var f in dir.GetFiles())
|
||||
if (f.EndsWith("_native.tscn")) files.Add(f);
|
||||
if (files.Count == 0) return null;
|
||||
|
||||
bool Good(string f) { var l = f.ToLower(); foreach (var b in BadStems) if (l.Contains(b)) return false; return true; }
|
||||
// приоритет: combat -> stand -> любая полноростовая (не портрет/не частичная)
|
||||
foreach (var key in new[] { "combat", "stand" })
|
||||
foreach (var f in files)
|
||||
if (Good(f) && f.ToLower().Contains(key)) return $"res://characters/{name}/{f}";
|
||||
foreach (var f in files)
|
||||
if (Good(f)) return $"res://characters/{name}/{f}";
|
||||
return $"res://characters/{name}/{files[0]}";
|
||||
}
|
||||
|
||||
private void BuildRoomHeaders()
|
||||
{
|
||||
foreach (var r in _rooms)
|
||||
{
|
||||
var lbl = new Label { Position = new Vector2(r.Col * Cell + 6, r.Row * Cell - 34) };
|
||||
lbl.AddThemeFontSizeOverride("font_size", 26);
|
||||
lbl.AddThemeConstantOverride("outline_size", 5);
|
||||
lbl.AddThemeColorOverride("font_outline_color", Colors.Black);
|
||||
_world.AddChild(lbl);
|
||||
r.Header = lbl;
|
||||
UpdateRoomHeader(r);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateRoomHeader(Room r)
|
||||
{
|
||||
if (r.Header == null) return;
|
||||
var names = new List<string>();
|
||||
foreach (var s in r.SlotOcc) if (s != null) names.Add($"{s.DisplayName} ({TypeName(s.Type)})");
|
||||
r.Header.Text = names.Count == 0 ? $"{r.Type}: пусто" : $"{r.Type}: " + string.Join(", ", names);
|
||||
}
|
||||
|
||||
// ---------- инвентарь ----------
|
||||
private void BuildInventoryPanel()
|
||||
{
|
||||
var panel = new PanelContainer();
|
||||
panel.SetAnchorsPreset(Control.LayoutPreset.TopRight);
|
||||
panel.Position = new Vector2(-262, 12);
|
||||
panel.CustomMinimumSize = new Vector2(250, 0);
|
||||
var margin = new MarginContainer();
|
||||
foreach (var s in new[] { "margin_left", "margin_right", "margin_top", "margin_bottom" })
|
||||
margin.AddThemeConstantOverride(s, 8);
|
||||
panel.AddChild(margin);
|
||||
_invBox = new VBoxContainer();
|
||||
margin.AddChild(_invBox);
|
||||
AddChild(panel);
|
||||
RefreshInventory();
|
||||
}
|
||||
|
||||
private void RefreshInventory()
|
||||
{
|
||||
foreach (var c in _invBox.GetChildren()) c.QueueFree();
|
||||
var title = new Label { Text = $"Инвентарь ({_inventory.Count})" };
|
||||
title.AddThemeColorOverride("font_color", new Color(0.95f, 0.9f, 0.7f));
|
||||
_invBox.AddChild(title);
|
||||
if (_inventory.Count == 0)
|
||||
{
|
||||
var e = new Label { Text = "пусто · ПКМ по персонажу" };
|
||||
e.AddThemeColorOverride("font_color", new Color(0.7f, 0.72f, 0.78f));
|
||||
_invBox.AddChild(e);
|
||||
}
|
||||
foreach (var it in _inventory.ToList())
|
||||
{
|
||||
var item = it;
|
||||
var b = new Button { Text = $"{it.Name} · {TypeName(it.Type)}" };
|
||||
b.Pressed += () => RespawnFromInventory(item);
|
||||
_invBox.AddChild(b);
|
||||
}
|
||||
}
|
||||
|
||||
private void RespawnFromInventory(InvItem it)
|
||||
{
|
||||
var p = SpawnPawn(it.Scene, _rng.Next(Rows), it.Name, it.Type);
|
||||
if (p != null) { _inventory.Remove(it); RefreshInventory(); }
|
||||
}
|
||||
|
||||
private void SendToInventory(Pawn p)
|
||||
{
|
||||
RemoveFromRoom(p);
|
||||
_pawns.Remove(p);
|
||||
if (_selected == p) { _selected = null; _infoPanel.Visible = false; }
|
||||
p.Node.QueueFree();
|
||||
_inventory.Add(new InvItem { Name = p.DisplayName, Type = p.Type, Scene = p.BaseScene });
|
||||
RefreshInventory();
|
||||
UpdateLabel();
|
||||
}
|
||||
|
||||
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;
|
||||
string head = $"{p.DisplayName} · {TypeName(p.Type)}";
|
||||
if (p.AssignedRoom != null)
|
||||
{
|
||||
var r = p.AssignedRoom;
|
||||
string mode = r.Count() >= 2 ? "групповая" : "одиночная";
|
||||
_infoLabel.Text = $"{head}\nНазначен: {r.Type} (слот {r.SlotOf(p) + 1}/{r.Cap})\nанимация: {mode}\nЛКМ тащить · ПКМ → инвентарь";
|
||||
}
|
||||
else
|
||||
_infoLabel.Text = $"{head}\nЭтаж: {p.Row + 1}/{Rows}\nЛКМ тащить/назначить · ПКМ → инвентарь";
|
||||
_infoPanel.Visible = true;
|
||||
}
|
||||
|
||||
private void BuildVault()
|
||||
{
|
||||
// «поверхность» над убежищем
|
||||
AddTex("res://assets/backgrounds/background_sky.png",
|
||||
new Rect2(-Cell, -Cell * 1.2f, Cols * Cell + Cell * 2, Cell * 1.2f), _world, new Color(1, 1, 1, 0.9f));
|
||||
// тонкая земляная «крышка» над верхним рядом (вместо неба)
|
||||
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++)
|
||||
@@ -61,10 +329,256 @@ public partial class RoomMap : Node2D
|
||||
AddCell("res://assets/room_sliced/LactationRoom.png", 4, 3, 1, 1);
|
||||
AddCell("res://assets/room_sliced/Light.png", 2, 2, 1, 1);
|
||||
|
||||
_rooms = new List<Room>
|
||||
{
|
||||
new Room { Type = "Спальня", Col = 1, Row = 0, Span = 3, States = new[] { "stand", "combat" },
|
||||
SlotTypes = new[] { CType.Player, CType.Slave } },
|
||||
new Room { Type = "Логово", Col = 0, Row = 2, Span = 1, States = new[] { "suckle_left", "milk", "birth", "combat", "stand" },
|
||||
SlotTypes = new[] { CType.Slave, CType.Monster } },
|
||||
new Room { Type = "Дойка", Col = 4, Row = 3, Span = 1, States = new[] { "milk", "milk2" },
|
||||
SlotTypes = new[] { CType.Slave, CType.Slave, CType.Child, CType.Child, CType.Child, CType.Child } },
|
||||
};
|
||||
foreach (var r in _rooms) r.Init();
|
||||
|
||||
// ручные позиции посадки (фикс, по ногам как в дебаге)
|
||||
var doyka = _rooms.First(r => r.Type == "Дойка");
|
||||
doyka.SlotPos[0] = new Vector2(1100, 918); // раб, слот 1
|
||||
|
||||
BuildRoomHeaders();
|
||||
|
||||
// рамки поверх всех ячеек — читаемость сетки
|
||||
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));
|
||||
|
||||
BuildObjects();
|
||||
_rng = new System.Random(20260803);
|
||||
}
|
||||
|
||||
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 CType TypeOf(string name) =>
|
||||
name == "player" ? CType.Player : name == "baby" ? CType.Child :
|
||||
Monsters.Contains(name) ? CType.Monster : CType.Slave;
|
||||
|
||||
private string GenName(CType t)
|
||||
{
|
||||
var pool = t == CType.Monster ? MonsterNames : t == CType.Child ? ChildNames
|
||||
: t == CType.Player ? new[] { "Игрок" } : SlaveNames;
|
||||
return pool[_rng.Next(pool.Length)];
|
||||
}
|
||||
|
||||
private Pawn SpawnPawn(string path, int row, string name = null, CType? type = null)
|
||||
{
|
||||
if (!ResourceLoader.Exists(path)) return null;
|
||||
var scene = ResourceLoader.Load<PackedScene>(path).Instantiate<Node2D>();
|
||||
scene.Position = -FootAnchor * CharScale;
|
||||
scene.Scale = new Vector2(CharScale, CharScale);
|
||||
var node = new Node2D();
|
||||
node.AddChild(scene);
|
||||
_world.AddChild(node);
|
||||
|
||||
float minX = Cell * 0.4f, maxX = Cols * Cell - Cell * 0.4f;
|
||||
float x = (float)(_rng.NextDouble() * (maxX - minX) + minX);
|
||||
node.Position = new Vector2(x, FloorY(row));
|
||||
|
||||
var ap = scene.GetNodeOrNull<AnimationPlayer>("AnimationPlayer");
|
||||
string[] anims = ap != null ? ap.GetAnimationList() : System.Array.Empty<string>();
|
||||
string cname = path.Split('/')[3];
|
||||
CType t = type ?? TypeOf(cname);
|
||||
var p = new Pawn
|
||||
{
|
||||
Node = node, Ap = ap, Name = cname, BaseScene = path, Row = row,
|
||||
X = x, MinX = minX, MaxX = maxX,
|
||||
Speed = 70f + (float)_rng.NextDouble() * 55f,
|
||||
WalkAnim = Pick(anims, "walk_0"), IdleAnim = Pick(anims, "idle_0"),
|
||||
Type = t, DisplayName = name ?? GenName(t)
|
||||
};
|
||||
p.State = PState.Idle; // просто стоит (никакого блуждания)
|
||||
PlayIdle(p);
|
||||
_pawns.Add(p);
|
||||
UpdateLabel();
|
||||
return 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 PlayIdle(Pawn p) { if (p.Ap != null && p.IdleAnim != null) p.Ap.Play(p.IdleAnim); }
|
||||
|
||||
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)
|
||||
{
|
||||
p.Node.Modulate = Colors.White;
|
||||
// клетку берём по ЦЕНТРУ тела, а не по ногам (иначе съезжает на строку ниже)
|
||||
int col = Mathf.Clamp((int)(p.Node.Position.X / Cell), 0, Cols - 1);
|
||||
int row = Mathf.Clamp((int)((p.Node.Position.Y - Cell * 0.45f) / Cell), 0, Rows - 1);
|
||||
|
||||
var room = RoomAt(col, row);
|
||||
if (room != null)
|
||||
{
|
||||
int slot = room.SlotOf(p);
|
||||
if (slot < 0) slot = room.FreeSlotFor(p.Type);
|
||||
if (slot >= 0)
|
||||
{
|
||||
AssignToRoom(p, room, slot);
|
||||
if (_selected == p) ShowInfo(p);
|
||||
return;
|
||||
}
|
||||
// нет свободного слота нужного типа — роняем на пол
|
||||
}
|
||||
|
||||
// обычный сброс: снять назначение и вернуть боевую сцену
|
||||
if (p.AssignedRoom != null) { RemoveFromRoom(p); SwapScene(p, p.BaseScene); }
|
||||
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; PlayIdle(p);
|
||||
if (_selected == p) ShowInfo(p);
|
||||
}
|
||||
|
||||
private Room RoomAt(int col, int row)
|
||||
{
|
||||
if (_rooms == null) return null;
|
||||
foreach (var r in _rooms)
|
||||
if (row == r.Row && col >= r.Col && col < r.Col + r.Span) return r;
|
||||
return null;
|
||||
}
|
||||
|
||||
private void AssignToRoom(Pawn p, Room room, int slot)
|
||||
{
|
||||
if (p.AssignedRoom != null && p.AssignedRoom != room) RemoveFromRoom(p);
|
||||
room.SlotOcc[slot] = p;
|
||||
p.AssignedRoom = room;
|
||||
p.State = PState.Assigned;
|
||||
|
||||
string sp = StateScene(p.Name, room.States) ?? p.BaseScene;
|
||||
SwapScene(p, sp);
|
||||
LayoutRoom(room);
|
||||
}
|
||||
|
||||
private void RemoveFromRoom(Pawn p)
|
||||
{
|
||||
var room = p.AssignedRoom;
|
||||
if (room == null) return;
|
||||
int slot = room.SlotOf(p);
|
||||
if (slot >= 0) room.SlotOcc[slot] = null;
|
||||
p.AssignedRoom = null;
|
||||
LayoutRoom(room);
|
||||
}
|
||||
|
||||
private static readonly Vector2 CenterAnchor = new Vector2(640f, 360f); // центр кадра 1280x720
|
||||
|
||||
// расставить по слотам: фикс-позиция (SlotPos, по ногам) либо авто-распределение (по центру)
|
||||
private void LayoutRoom(Room room)
|
||||
{
|
||||
bool group = room.Count() >= 2;
|
||||
for (int i = 0; i < room.Cap; i++)
|
||||
{
|
||||
var q = room.SlotOcc[i];
|
||||
if (q == null) continue;
|
||||
bool child = room.SlotTypes[i] == CType.Child;
|
||||
bool fixedPos = room.SlotPos != null && room.SlotPos[i].HasValue;
|
||||
Vector2 pos = fixedPos ? room.SlotPos[i].Value : AutoSlotPos(room, i);
|
||||
Vector2 anchor = fixedPos ? FootAnchor : CenterAnchor; // фикс — по ногам (как в дебаге)
|
||||
if (q.Node.GetChildCount() > 0 && q.Node.GetChild(0) is Node2D sc)
|
||||
sc.Position = -anchor * CharScale;
|
||||
float scale = child ? 0.55f : 1f;
|
||||
q.X = pos.X;
|
||||
q.Node.Position = pos;
|
||||
q.Node.Scale = new Vector2(scale, scale);
|
||||
var anims = q.Ap != null ? q.Ap.GetAnimationList() : System.Array.Empty<string>();
|
||||
string a = PickAction(anims, group);
|
||||
if (q.Ap != null && a != null) q.Ap.Play(a);
|
||||
}
|
||||
UpdateRoomHeader(room);
|
||||
}
|
||||
|
||||
private static Vector2 AutoSlotPos(Room room, int slot)
|
||||
{
|
||||
bool child = room.SlotTypes[slot] == CType.Child;
|
||||
var grp = new List<int>();
|
||||
for (int i = 0; i < room.Cap; i++)
|
||||
if ((room.SlotTypes[i] == CType.Child) == child) grp.Add(i);
|
||||
int idx = grp.IndexOf(slot), n = grp.Count;
|
||||
float t = n == 1 ? 0.5f : (idx + 0.5f) / n;
|
||||
float sx = room.Col * Cell + t * (room.Span * Cell);
|
||||
float yOff = child ? Cell * 0.9f : Cell * 0.74f;
|
||||
return new Vector2(sx, room.Row * Cell + yOff);
|
||||
}
|
||||
|
||||
private static string StateScene(string name, string[] states)
|
||||
{
|
||||
foreach (var s in states)
|
||||
{
|
||||
string path = $"res://characters/{name}/{name}_{s}_native.tscn";
|
||||
if (ResourceLoader.Exists(path)) return path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// заменить скелет-сцену персонажа (боевая ↔ состояние комнаты)
|
||||
private void SwapScene(Pawn p, string path)
|
||||
{
|
||||
if (p.Node.GetChildCount() > 0) p.Node.GetChild(0).Free();
|
||||
var scene = ResourceLoader.Load<PackedScene>(path).Instantiate<Node2D>();
|
||||
scene.Position = -FootAnchor * CharScale;
|
||||
scene.Scale = new Vector2(CharScale, CharScale);
|
||||
p.Node.AddChild(scene);
|
||||
p.Ap = scene.GetNodeOrNull<AnimationPlayer>("AnimationPlayer");
|
||||
var anims = p.Ap != null ? p.Ap.GetAnimationList() : System.Array.Empty<string>();
|
||||
p.WalkAnim = Pick(anims, "walk_0");
|
||||
p.IdleAnim = Pick(anims, "idle_0");
|
||||
}
|
||||
|
||||
private static readonly string[] NotAction =
|
||||
{ "walk", "attack", "idle", "dd", "dfdfa", "ddd", "test", "animation", "information", "portrait", "stun", "knockback", "dead", "down" };
|
||||
|
||||
private static string PickAction(string[] anims, bool group)
|
||||
{
|
||||
var actions = anims.Where(a => !NotAction.Any(n => a.StartsWith(n))).ToList();
|
||||
if (actions.Count == 0) actions = anims.Where(a => a.StartsWith("idle")).ToList();
|
||||
if (actions.Count == 0) return anims.Length > 0 ? anims[0] : null;
|
||||
return group && actions.Count > 1 ? actions[1] : actions[0];
|
||||
}
|
||||
|
||||
private void AddCell(string path, int col, int row, int cw, int ch, Color? modulate = null)
|
||||
@@ -114,8 +628,8 @@ public partial class RoomMap : Node2D
|
||||
private void UpdateLabel()
|
||||
{
|
||||
_label.Text =
|
||||
$"Карта убежища — {Cols}×{Rows} комнат из игровых текстур\n" +
|
||||
$"ЛКМ/WASD — панорама · колесо — зум (x{_viewZoom:0.00}) · R — сброс · Esc — меню";
|
||||
$"Карта убежища — {Cols}×{Rows} комнат · персонажей на карте: {_pawns.Count}\n" +
|
||||
$"Спавн внизу · ЛКМ тащить в комнату/на пол · ПКМ → инвентарь · WASD/ЛКМ-фон — панорама · колесо — зум · Esc — меню";
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
@@ -141,19 +655,63 @@ 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.ButtonIndex == MouseButton.Right && mb.Pressed)
|
||||
{
|
||||
var hit = PawnAt(ScreenToWorld(mb.Position));
|
||||
if (hit != null) SendToInventory(hit);
|
||||
}
|
||||
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;
|
||||
var pos = _dragPawn.Node.Position;
|
||||
int dcol = (int)(pos.X / Cell), drow = (int)((pos.Y - Cell * 0.45f) / Cell);
|
||||
var room = RoomAt(Mathf.Clamp(dcol, 0, Cols - 1), Mathf.Clamp(drow, 0, Rows - 1));
|
||||
_infoLabel.Text =
|
||||
$"{_dragPawn.DisplayName} · {TypeName(_dragPawn.Type)}\n" +
|
||||
$"DEBUG коорд: x={pos.X:0} y={pos.Y:0}\n" +
|
||||
$"клетка: col={dcol} row={drow}" + (room != null ? $" → {room.Type}" : "");
|
||||
_infoPanel.Visible = true;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user