Compare commits

...
15 Commits
Author SHA1 Message Date
Leonid PershinandClaude Opus 4.8 7a754d618d Комнаты: ручные фикс-позиции слотов (Room.SlotPos), по ногам как в дебаге
- Каждый слот может иметь точную позицию (перекрывает авто-распределение)
- Дойка слот раба 1 = (1100, 918)
- Фикс-позиция ставит НОГИ персонажа в координату (совпадает с DEBUG при drag),
  авто-слоты по-прежнему центрируются

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 05:32:57 +03:00
Leonid PershinandClaude Opus 4.8 214d6e3c5b Спавнер: выбор конкретного персонажа (выпадающий список + Заспавнить)
Кроме типовых кнопок (случайный из ростера) добавлен OptionButton со всеми
спавнящимися персонажами (у кого есть полноростовая сцена) и кнопка Заспавнить.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 05:25:25 +03:00
Leonid PershinandClaude Opus 4.8 bb16605129 Спавнер: выбирать полноростовую сцену, а не портрет
У player скелеты названы player1_*, поэтому фолбэк брал первую по алфавиту
(портрет). ResolveScene теперь исключает portrait/fetus/suckle/_body/_head/...
и приоритетит combat -> stand -> любую полноростовую.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 05:23:00 +03:00
Leonid PershinandClaude Opus 4.8 4350f4a9e7 Подземелье: убрано блуждание, спавнер персонажей, idle-стойка
- Персонажи больше не ходят по карте: старт пустой, никакого AI-движения
- Убрана тёмная шахта/телепорт (движение больше не нужно)
- Спавнер (панель внизу): + Раб / Монстр / Ребёнок / Игрок — создаёт персонажа,
  который стоит в idle на случайном этаже
- Дроп на обычную клетку -> персонаж просто стоит (idle), дроп в слот -> назначение
- Счётчик персонажей в шапке обновляется

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 05:17:09 +03:00
Leonid PershinandClaude Opus 4.8 0882ecdf87 Подземелье: типы персонажей, имена, окна-заголовки, инвентарь, типизированные слоты
- Типы: Игрок/Раб/Монстр/Ребёнок (по персонажу), генерация имён
- Заголовки-окна над комнатами со списком занявших (имя · тип)
- ПКМ по персонажу -> в инвентарь (панель справа, клик -> вернуть на карту)
- Типизированные слоты: Дойка = 2 раба + 4 ребёнка, Логово = 1 раб + 1 монстр
  (персонаж занимает только слот своего типа; иначе не назначается)
- Персонажи в комнатах опущены ниже (не висят), дети мельче и ниже
- DEBUG: координаты/клетка при перетаскивании в панели

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 18:01:29 +03:00
Leonid PershinandClaude Opus 4.8 4df5a73ec0 Комнаты: Дойка на 2 слота, центрирование персонажей, без idle
- Дойка (доильня) теперь на 2 персонажа
- Персонажи центрируются в ячейке (center-anchor вместо foot-anchor — состояния
  разного размера больше не смещаются)
- Логово/Дойка используют milk-состояние с action-анимациями (не idle)
- orc -> shortstack среди ходоков (у orc нет milk-состояния)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 17:48:29 +03:00
Leonid PershinandClaude Opus 4.8 70b65fde0c Подземелье: назначение персонажей в тематические комнаты + анимации
- Комнаты Спальня/Логово/Дойка со слотами
- Перетаскивание в комнату: персонаж занимает слот, скелет меняется на состояние
  комнаты (milk/birth/...), играет одиночную (1) или групповую (2+) анимацию
- Клетка сброса берётся по центру тела (фикс съезда на строку ниже)
- Панель показывает назначение/слот/режим анимации

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 17:44:00 +03:00
Leonid PershinandClaude Opus 4.8 a254309ec9 Подземелье: телепорт внутри тёмной шахты (fade исчезновение/появление)
Персонаж доходит до тёмной шахты, плавно исчезает (alpha 1->0), переносится
в шахту на случайном другом этаже и плавно появляется (0->1). Без вертикального прохода.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 17:30:35 +03:00
Leonid PershinandClaude Opus 4.8 085cf3e5fa Подземелье: тёмная шахта (комнаты друг над другом) вместо телепорта
- Тёмные комнаты теперь одной колонкой = скрытая вертикальная шахта
- Персонаж идёт к шахте на своём этаже и ПРОХОДИТ по ней вертикально на другой этаж
- Движение (и горизонтальное, и по шахте) анимируется боевым walk_0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 17:22:43 +03:00
Leonid PershinandClaude Opus 4.8 2a629adf2c Подземелье: тёмные комнаты-телепорты вместо лестниц
- Убраны процедурные лестницы
- По одной тёмной комнате на этаж (затемнение + фиолетовое портальное свечение)
- Персонаж идёт в тёмную комнату своего этажа и телепортируется в тёмную комнату
  случайного другого этажа

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 17:17:34 +03:00
Leonid PershinandClaude Opus 4.8 46fdb230fe Подземелье: ходьба между этажами по лестницам + клик/перетаскивание персонажей
- Машина состояний ходоков: гулять по этажу / идти к лестнице / лезть вверх-вниз
- ЛКМ по персонажу — панель с именем и этажом
- Перетаскивание персонажа мышью → бросок в ближайшую комнату (снап к ячейке)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 17:13:27 +03:00
Leonid PershinandClaude Opus 4.8 935f4afc7e Убрать растянутое небо над картой (тот синий кусок)
background_sky растягивался в тонкую полосу и выглядел как синий диагональный
артефакт. Заменил на тонкую тёмную земляную кромку над верхним рядом.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 17:08:22 +03:00
Leonid PershinandClaude Opus 4.8 b048a83966 Подземелье: лестницы, объекты и гуляющие персонажи
- Процедурные лестницы-шахты между этажами
- Объекты в комнатах (фонари Lantern, свечение Light)
- 5 нативных персонажей патрулируют этажи (walk_0/idle_0, разворот, простое блуждание)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 17:05:37 +03:00
Leonid PershinandClaude Opus 4.8 a0e08cf146 Удалить осиротевший scenes/Main.tscn (ссылался на удалённый AppRoot.cs)
Редактор пересоздал старую сцену при открытии; главная сцена — MainMenu.tscn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 16:45:01 +03:00
Leonid Pershin edce6e7b6b add plugin 2026-08-03 16:44:27 +03:00
4 changed files with 581 additions and 16 deletions
+9 -1
View File
@@ -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 персонажей нативно:
+5
View File
@@ -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"
-6
View File
@@ -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
View File
@@ -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;
} }