Compare commits
15
Commits
b7c9aaea2e
...
main
@@ -26,7 +26,15 @@ D:\Godot_v4.7.1\Godot_v4.7.1-stable_mono_win64.exe
|
|||||||
Главное меню (`scenes/MainMenu.tscn`) ведёт на три сцены (назад в меню — `Esc`):
|
Главное меню (`scenes/MainMenu.tscn`) ведёт на три сцены (назад в меню — `Esc`):
|
||||||
|
|
||||||
1. **Подземелье** (`scenes/Dungeon.tscn`) — разрез убежища à la Fallout Shelter: сетка 6×5
|
1. **Подземелье** (`scenes/Dungeon.tscn`) — разрез убежища à la Fallout Shelter: сетка 6×5
|
||||||
комнат. ЛКМ/WASD — панорама, колесо — зум, `R` — сброс.
|
комнат + объекты (фонари/свет). Карта стартует пустой; персонажи создаются **спавнером**
|
||||||
|
(панель внизу: кнопки типов `+ Раб/Монстр/Ребёнок/Игрок` для случайного, или выпадающий список конкретного персонажа + «Заспавнить») и **стоят в idle**, пока их не перетащат.
|
||||||
|
- **Типы персонажей** (Игрок/Раб/Монстр/Ребёнок) + генерация имён.
|
||||||
|
- **Тематические комнаты со слотами по типу**: Дойка = 2 раба + 4 ребёнка,
|
||||||
|
Логово = 1 раб + 1 монстр. Перетаскивание в подходящий слот меняет скелет на
|
||||||
|
состояние комнаты и играет одиночную/групповую анимацию. Над комнатами — окна с именами.
|
||||||
|
- **ЛКМ** — выбрать/тащить (в комнату или на пол — там персонаж просто стоит).
|
||||||
|
**ПКМ** — убрать персонажа в **инвентарь** (панель справа; клик — вернуть).
|
||||||
|
- `WASD`/ЛКМ-фон — панорама, колесо — зум, `R` — сброс, `Esc` — меню.
|
||||||
2. **Просмотр спрайтов** (`scenes/SpriteViewer.tscn`) — галерея вытащенных изображений
|
2. **Просмотр спрайтов** (`scenes/SpriteViewer.tscn`) — галерея вытащенных изображений
|
||||||
(UI, фоны, эффекты, нарезанные спрайты). `A/D` — картинка, `W/S` — категория, колесо — зум.
|
(UI, фоны, эффекты, нарезанные спрайты). `A/D` — картинка, `W/S` — категория, колесо — зум.
|
||||||
3. **Нативные анимации** (`scenes/NativeViewer.tscn`) — браузер всех 38 персонажей нативно:
|
3. **Нативные анимации** (`scenes/NativeViewer.tscn`) — браузер всех 38 персонажей нативно:
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ config/icon="res://icon.svg"
|
|||||||
|
|
||||||
project/assembly_name="ProjectRR"
|
project/assembly_name="ProjectRR"
|
||||||
|
|
||||||
|
[editor]
|
||||||
|
|
||||||
|
version_control/plugin_name="GitPlugin"
|
||||||
|
version_control/autoload_on_startup=true
|
||||||
|
|
||||||
[physics]
|
[physics]
|
||||||
|
|
||||||
3d/physics_engine="Jolt Physics"
|
3d/physics_engine="Jolt Physics"
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
[gd_scene format=3 uid="uid://cofo0rj1wugor"]
|
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://src/AppRoot.cs" id="1_app"]
|
|
||||||
|
|
||||||
[node name="Main" type="Node2D" unique_id=2133344123]
|
|
||||||
script = ExtResource("1_app")
|
|
||||||
+567
-9
@@ -1,6 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using Godot;
|
using Godot;
|
||||||
|
|
||||||
// Демо-карта «убежища» в разрезе (à la Fallout Shelter): сетка комнат из игровых текстур.
|
// Демо-карта «убежища» в разрезе (à la Fallout Shelter): комнаты, лестницы, объекты
|
||||||
|
// и несколько персонажей, гуляющих по этажам.
|
||||||
// ЛКМ-перетаскивание или WASD/стрелки — панорама
|
// ЛКМ-перетаскивание или WASD/стрелки — панорама
|
||||||
// колесо мыши — зум, R — сброс вида
|
// колесо мыши — зум, R — сброс вида
|
||||||
public partial class RoomMap : Node2D
|
public partial class RoomMap : Node2D
|
||||||
@@ -8,6 +11,8 @@ public partial class RoomMap : Node2D
|
|||||||
private const float Cell = 256f;
|
private const float Cell = 256f;
|
||||||
private const int Cols = 6;
|
private const int Cols = 6;
|
||||||
private const int Rows = 5;
|
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 Node2D _world;
|
||||||
private Label _label;
|
private Label _label;
|
||||||
@@ -15,6 +20,77 @@ public partial class RoomMap : Node2D
|
|||||||
private float _viewZoom = 1f;
|
private float _viewZoom = 1f;
|
||||||
private Vector2 _homePos;
|
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()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
var bg = new ColorRect
|
var bg = new ColorRect
|
||||||
@@ -36,14 +112,206 @@ public partial class RoomMap : Node2D
|
|||||||
_label.AddThemeColorOverride("font_outline_color", Colors.Black);
|
_label.AddThemeColorOverride("font_outline_color", Colors.Black);
|
||||||
AddChild(_label);
|
AddChild(_label);
|
||||||
|
|
||||||
|
BuildInfoPanel();
|
||||||
|
BuildInventoryPanel();
|
||||||
|
BuildSpawner();
|
||||||
ResetView();
|
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()
|
private void BuildVault()
|
||||||
{
|
{
|
||||||
// «поверхность» над убежищем
|
// тонкая земляная «крышка» над верхним рядом (вместо неба)
|
||||||
AddTex("res://assets/backgrounds/background_sky.png",
|
AddRect(new Rect2(-Cell * 0.5f, -22, Cols * Cell + Cell, 22), new Color(0.11f, 0.09f, 0.07f));
|
||||||
new Rect2(-Cell, -Cell * 1.2f, Cols * Cell + Cell * 2, Cell * 1.2f), _world, new Color(1, 1, 1, 0.9f));
|
|
||||||
|
|
||||||
// пол каждой ячейки — пустая комната (шахматкой две вариации)
|
// пол каждой ячейки — пустая комната (шахматкой две вариации)
|
||||||
for (int r = 0; r < Rows; r++)
|
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/LactationRoom.png", 4, 3, 1, 1);
|
||||||
AddCell("res://assets/room_sliced/Light.png", 2, 2, 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 r = 0; r < Rows; r++)
|
||||||
for (int c = 0; c < Cols; c++)
|
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));
|
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)
|
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()
|
private void UpdateLabel()
|
||||||
{
|
{
|
||||||
_label.Text =
|
_label.Text =
|
||||||
$"Карта убежища — {Cols}×{Rows} комнат из игровых текстур\n" +
|
$"Карта убежища — {Cols}×{Rows} комнат · персонажей на карте: {_pawns.Count}\n" +
|
||||||
$"ЛКМ/WASD — панорама · колесо — зум (x{_viewZoom:0.00}) · R — сброс · Esc — меню";
|
$"Спавн внизу · ЛКМ тащить в комнату/на пол · ПКМ → инвентарь · WASD/ЛКМ-фон — панорама · колесо — зум · Esc — меню";
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void _Process(double delta)
|
public override void _Process(double delta)
|
||||||
@@ -141,19 +655,63 @@ 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.ButtonIndex == MouseButton.Right && mb.Pressed)
|
||||||
|
{
|
||||||
|
var hit = PawnAt(ScreenToWorld(mb.Position));
|
||||||
|
if (hit != null) SendToInventory(hit);
|
||||||
|
}
|
||||||
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;
|
||||||
|
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)
|
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