Refactor game structure and update project settings for 2D gameplay. Removed the game stub scene, introduced a new vault scene, and organized game scripts into distinct files for grid management, rendering, and scene control. Updated input actions for camera movement and changed rendering method to Mobile for better performance.

This commit is contained in:
Leonid Pershin
2026-08-10 21:36:56 +03:00
parent adfbbbcec3
commit 044dde7b5f
17 changed files with 838 additions and 158 deletions
+18 -3
View File
@@ -5,8 +5,8 @@
## Что это за проект ## Что это за проект
Двумерная игра с видом сбоку на Godot 4.7. Референс — Fallout Shelter: разрез убежища, Двумерная игра с видом сбоку на Godot 4.7. Референс — Fallout Shelter: разрез убежища,
комнаты в сетке, жители. **Игра полностью 2D.** Если встречаете 3D-узлы — это остатки комнаты в сетке, жители. **Игра полностью 2D**, рендерер Mobile. Новые игровые сцены —
заглушки, а не направление проекта. на `Node2D`/`Control`; 3D-узлам в проекте места нет.
## Работа через MCP, а не через файлы ## Работа через MCP, а не через файлы
@@ -46,12 +46,27 @@
- Единая тема `themes/main_menu.theme.tres` на корне каждого экрана. Цвета в узлах не хардкодим. - Единая тема `themes/main_menu.theme.tres` на корне каждого экрана. Цвета в узлах не хардкодим.
- Экран = сцена со своим скриптом, наружу торчат `open()` / `close()` и сигнал `closed`. - Экран = сцена со своим скриптом, наружу торчат `open()` / `close()` и сигнал `closed`.
- Оверлеи центрируются `CenterContainer`, а **не** пресетом `center`: пресет ставит левый - Оверлеи центрируются `CenterContainer`, а **не** пресетом `center`: пресет ставит левый
верхний угол карточки в центр экрана. верхний угол карточки в центр экрана. Та же ловушка у `bottom_wide` — он прижимает к нижнему
краю *верх* панели, и она уезжает за экран; для нижней панели используйте `MarginContainer`
во весь экран + `HBoxContainer` с `size_flags_vertical = SIZE_SHRINK_END` у содержимого.
- Контейнеры не раскладывают скрытые узлы — после `visible = true` нужен - Контейнеры не раскладывают скрытые узлы — после `visible = true` нужен
`await get_tree().process_frame`, прежде чем читать `size` (например, для `pivot_offset`). `await get_tree().process_frame`, прежде чем читать `size` (например, для `pivot_offset`).
- Наведение мышью делается фокусом (`mouse_entered``grab_focus`), чтобы у мыши и клавиатуры - Наведение мышью делается фокусом (`mouse_entered``grab_focus`), чтобы у мыши и клавиатуры
была одна подсветка. была одна подсветка.
## Игровой слой
Три файла в `scripts/game/`, разделены намеренно:
- `vault_grid.gd` — только данные и правила (`RefCounted`, без узлов). Сюда идут изменения
логики: типы комнат, размещение, ресурсы.
- `vault_view.gd` — только `_draw()`. Сюда идут изменения картинки. Когда появится графика,
переписывается этот файл, остальные не трогаются.
- `vault_scene.gd` — связка модели, вида, камеры и HUD плюс контракт сохранения.
Новый тип комнаты добавляется одной записью в `VaultGrid.KINDS`: кнопка в панели строительства
и подсчёт ресурсов подхватятся сами.
## Автозагрузки ## Автозагрузки
`GameSettings` (`GameSettingsService`) и `SaveManager` (`SaveManagerService`). `GameSettings` (`GameSettingsService`) и `SaveManager` (`SaveManagerService`).
+34 -10
View File
@@ -4,14 +4,15 @@
разрез убежища, комнаты в сетке, жители, которых расселяют по этажам, и мир, реагирующий на разрез убежища, комнаты в сетке, жители, которых расселяют по этажам, и мир, реагирующий на
решения игрока. решения игрока.
> Статус: ранняя альфа. Готов весь каркас меню; игровой слой пока заглушка. > Статус: ранняя альфа. Готов каркас меню и первый игровой слой — сетка комнат в разрезе.
--- ---
## Что уже работает ## Что уже работает
| Экран | Файл | Что умеет | | Часть | Файл | Что умеет |
|---|---|---| |---|---|---|
| Убежище | `scenes/game/vault.tscn` | разрез на 6 этажей, лифтовая шахта, строительство и снос комнат, сводка ресурсов |
| Главное меню | `scenes/ui/main_menu.tscn` | анимированный фон на шейдере, появление по стаггеру, единая подсветка для мыши и клавиатуры | | Главное меню | `scenes/ui/main_menu.tscn` | анимированный фон на шейдере, появление по стаггеру, единая подсветка для мыши и клавиатуры |
| Настройки | `scenes/ui/settings_menu.tscn` | видео, звук, переназначение клавиш; сохраняется в `user://settings.cfg` | | Настройки | `scenes/ui/settings_menu.tscn` | видео, звук, переназначение клавиш; сохраняется в `user://settings.cfg` |
| Пауза | `scenes/ui/pause_menu.tscn` | останавливает дерево, переиспользует экраны настроек и сохранений | | Пауза | `scenes/ui/pause_menu.tscn` | останавливает дерево, переиспользует экраны настроек и сохранений |
@@ -20,6 +21,23 @@
Экраны настроек и сохранений — отдельные сцены, вложенные инстансами и в главное меню, и в паузу. Экраны настроек и сохранений — отдельные сцены, вложенные инстансами и в главное меню, и в паузу.
Дублирования нет: правка в одном месте видна везде. Дублирования нет: правка в одном месте видна везде.
## Игровой слой
Убежище разделено на три части, чтобы графику можно было заменить, не трогая логику:
- **`vault_grid.gd`** (`VaultGrid`) — модель: сетка 9×6, список комнат, правила размещения,
баланс ресурсов, сериализация. Ни одного узла, ни одной отрисовки.
- **`vault_view.gd`** (`VaultView`) — весь `_draw()`: небо, порода, шахта, комнаты, призрак
строительства. Пока рисуется процедурно — ассетов нет. Появится графика — меняется только
этот файл.
- **`vault_scene.gd`** — связывает модель, вид, камеру и HUD, реализует контракт сохранения.
Управление: **ЛКМ** — построить или снести, **ПКМ/СКМ** — тащить вид, **колесо** — зум,
**WASD / стрелки** — прокрутка, **Esc** — пауза.
Типы комнат и их баланс ресурсов заданы одним словарём `VaultGrid.KINDS` — новый тип добавляется
одной записью, кнопка в панели строительства и подсчёт ресурсов подхватываются сами.
## Требования ## Требования
- **Godot 4.7** (Forward+, .NET-сборка не обязательна — код на GDScript) - **Godot 4.7** (Forward+, .NET-сборка не обязательна — код на GDScript)
@@ -35,11 +53,11 @@
``` ```
scenes/ scenes/
ui/ main_menu, settings_menu, pause_menu, save_menu ui/ main_menu, settings_menu, pause_menu, save_menu
game/ game.tscn — заглушка вместо игрового слоя game/ vault.tscn — убежище
scripts/ scripts/
autoload/ game_settings.gd, save_manager.gd autoload/ game_settings.gd, save_manager.gd
ui/ по скрипту на экран ui/ по скрипту на экран
game/ game_stub.gd game/ vault_grid.gd, vault_view.gd, vault_camera.gd, vault_scene.gd
shaders/ menu_background.gdshader + материал shaders/ menu_background.gdshader + материал
themes/ main_menu.theme.tres — единая тема всех экранов themes/ main_menu.theme.tres — единая тема всех экранов
default_bus_layout.tres — шины микшера: Master → Music, SFX default_bus_layout.tres — шины микшера: Master → Music, SFX
@@ -78,18 +96,24 @@ func load_data(data: Dictionary) -> void:
`SaveManager` обходит группу `persistent`, собирает словари по путям узлов и пишет JSON. `SaveManager` обходит группу `persistent`, собирает словари по путям узлов и пишет JSON.
Миниатюра слота — кадр, снятый в момент вызова паузы, до показа оверлея. Миниатюра слота — кадр, снятый в момент вызова паузы, до показа оверлея.
**Действия ввода** (`project.godot` → Input Map): `move_left`, `move_right`, `move_forward`, **Действия ввода** (`project.godot` → Input Map): `move_left`, `move_right`, `move_up`,
`move_back`, `jump`, `sprint`, `interact`, `pause`. Переназначаются в настройках. `move_down` (прокрутка камеры), `interact`, `pause`. Переназначаются в настройках; список
в UI строится из `GameSettingsService.ACTION_LABELS`, так что новое действие нужно добавить
и туда.
**Рендерер** — Mobile: для 2D он легче Forward+. Смена рендерера требует перезапуска редактора.
**Слияния.** `.tscn` и `.tres` — текстовые, но построчный авто-мердж по ним даёт мусор. **Слияния.** `.tscn` и `.tres` — текстовые, но построчный авто-мердж по ним даёт мусор.
Конфликтующие сцены лучше разрешать, открывая обе версии в редакторе, а не в diff-вьювере. Конфликтующие сцены лучше разрешать, открывая обе версии в редакторе, а не в diff-вьювере.
## Что дальше ## Что дальше
- [ ] Перевести игровой слой на 2D: сетка комнат в разрезе, вид сбоку - [x] Игровой слой на 2D: сетка комнат в разрезе, вид сбоку
- [ ] Жители: найм, характеристики, распределение по комнатам - [ ] Жители: найм, характеристики, расселение по комнатам
- [ ] Ресурсы и производство, циклы времени - [ ] Время и производство: ресурсы должны копиться, а не только считаться в сводке
- [ ] Заменить `scenes/game/game.tscn` на настоящую сцену - [ ] Слияние одинаковых соседних комнат, как в референсе
- [ ] Стоимость строительства и ограничение по ресурсам
- [ ] Графика вместо процедурной отрисовки в `vault_view.gd`
- [ ] Звук: подключить шины Music и SFX - [ ] Звук: подключить шины Music и SFX
- [ ] Лицензия не выбрана - [ ] Лицензия не выбрана
+12 -21
View File
@@ -12,7 +12,7 @@ config_version=5
config/name="GWorld" config/name="GWorld"
run/main_scene="uid://b6mhe7465wwge" run/main_scene="uid://b6mhe7465wwge"
config/features=PackedStringArray("4.7", "Forward Plus") config/features=PackedStringArray("4.7", "Mobile")
config/icon="res://icon.svg" config/icon="res://icon.svg"
[autoload] [autoload]
@@ -38,16 +38,6 @@ enabled=PackedStringArray("res://addons/godot_ai/plugin.cfg")
[input] [input]
move_forward={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":87,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
move_back={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":83,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
move_left={ move_left={
"deadzone": 0.5, "deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":65,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":65,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
@@ -58,16 +48,6 @@ move_right={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":68,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":68,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
] ]
} }
jump={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
sprint={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194325,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
interact={ interact={
"deadzone": 0.5, "deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":69,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":69,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
@@ -78,6 +58,16 @@ pause={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194305,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194305,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
] ]
} }
move_up={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":87,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
move_down={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":83,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
[physics] [physics]
@@ -86,3 +76,4 @@ pause={
[rendering] [rendering]
rendering_device/driver.windows="d3d12" rendering_device/driver.windows="d3d12"
renderer/rendering_method="mobile"
-88
View File
@@ -1,88 +0,0 @@
[gd_scene format=3 uid="uid://bcryjj0nongyh"]
[ext_resource type="Script" uid="uid://1viyqekbic0n" path="res://scripts/game/game_stub.gd" id="1_0sr1o"]
[ext_resource type="PackedScene" uid="uid://cvx4ot7ugyprk" path="res://scenes/ui/pause_menu.tscn" id="1_7h61r"]
[ext_resource type="Theme" uid="uid://c6kkopjj3snag" path="res://themes/main_menu.theme.tres" id="2_0sr1o"]
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_kelw5"]
sky_top_color = Color(0.039215688, 0.078431375, 0.15686275, 1)
sky_horizon_color = Color(0.07058824, 0.1882353, 0.2509804, 1)
ground_bottom_color = Color(0.019607844, 0.039215688, 0.07058824, 1)
ground_horizon_color = Color(0.07058824, 0.1882353, 0.2509804, 1)
[sub_resource type="Sky" id="Sky_lfrn8"]
sky_material = SubResource("ProceduralSkyMaterial_kelw5")
[sub_resource type="Environment" id="Environment_5i0tg"]
background_mode = 2
sky = SubResource("Sky_lfrn8")
ambient_light_source = 2
ambient_light_color = Color(0.2, 0.22, 0.35, 1)
ambient_light_energy = 0.4
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_bdhmt"]
albedo_color = Color(0.07450981, 0.16470589, 0.2, 1)
roughness = 0.95
[sub_resource type="PlaneMesh" id="PlaneMesh_87uf6"]
size = Vector2(60, 60)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_2a8rs"]
albedo_color = Color(0.18039216, 0.9019608, 0.77254903, 1)
metallic = 0.1
roughness = 0.4
[sub_resource type="BoxMesh" id="BoxMesh_4j704"]
size = Vector3(2, 2, 2)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_kp23o"]
albedo_color = Color(0.48235294, 0.41960785, 0.9607843, 1)
metallic = 0.1
roughness = 0.5
[sub_resource type="BoxMesh" id="BoxMesh_5jirj"]
size = Vector3(1.2, 3.4, 1.2)
[node name="Game" type="Node3D" unique_id=1106458558]
script = ExtResource("1_0sr1o")
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1519444008]
environment = SubResource("Environment_5i0tg")
[node name="Sun" type="DirectionalLight3D" parent="." unique_id=1139733157]
transform = Transform3D(-0.6946583, 0.53457373, -0.48133233, 0, 0.66913056, 0.74314487, 0.7193399, 0.5162317, -0.46481708, 0, 0, 0)
light_color = Color(0.7490196, 0.9019608, 1, 1)
light_energy = 0.9
shadow_enabled = true
[node name="Camera3D" type="Camera3D" parent="." unique_id=2037025428]
transform = Transform3D(0.809017, -0.18163563, 0.559017, 0, 0.95105654, 0.309017, -0.58778524, -0.25, 0.7694209, 6.5, 4.2, 8.5)
current = true
[node name="Ground" type="MeshInstance3D" parent="." unique_id=1318815356]
material_override = SubResource("StandardMaterial3D_bdhmt")
mesh = SubResource("PlaneMesh_87uf6")
[node name="BoxA" type="MeshInstance3D" parent="." unique_id=1331389928]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
material_override = SubResource("StandardMaterial3D_2a8rs")
mesh = SubResource("BoxMesh_4j704")
[node name="BoxB" type="MeshInstance3D" parent="." unique_id=535287837]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3.2, 1.7, -2.4)
material_override = SubResource("StandardMaterial3D_kp23o")
mesh = SubResource("BoxMesh_5jirj")
[node name="PauseMenu" parent="." unique_id=1649378803 instance=ExtResource("1_7h61r")]
[node name="Hud" type="CanvasLayer" parent="." unique_id=824330719]
[node name="Hint" type="Label" parent="Hud" unique_id=47403933]
offset_left = 40.0
offset_top = 40.0
offset_right = 279.0
offset_bottom = 63.0
theme = ExtResource("2_0sr1o")
theme_override_colors/font_color = Color(0.56078434, 0.64705884, 0.7294118, 1)
theme_override_font_sizes/font_size = 18
text = "Сцена-заглушка. Esc — пауза."
+107
View File
@@ -0,0 +1,107 @@
[gd_scene format=3 uid="uid://dprxap0ioe3ps"]
[ext_resource type="Script" uid="uid://lsclvc1dgviq" path="res://scripts/game/vault_scene.gd" id="1_o8yry"]
[ext_resource type="Script" uid="uid://bvimo3kgmefgf" path="res://scripts/game/vault_view.gd" id="2_mkhle"]
[ext_resource type="Script" uid="uid://dg751mcd0v178" path="res://scripts/game/vault_camera.gd" id="3_x7j0k"]
[ext_resource type="Theme" uid="uid://c6kkopjj3snag" path="res://themes/main_menu.theme.tres" id="4_sch52"]
[ext_resource type="PackedScene" uid="uid://cvx4ot7ugyprk" path="res://scenes/ui/pause_menu.tscn" id="5_lffpp"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lvdr8"]
content_margin_left = 22.0
content_margin_top = 12.0
content_margin_right = 22.0
content_margin_bottom = 12.0
bg_color = Color(0.043137256, 0.07450981, 0.13333334, 0.8784314)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.18039216, 0.9019608, 0.77254903, 0.2)
corner_radius_top_left = 12
corner_radius_top_right = 12
corner_radius_bottom_right = 12
corner_radius_bottom_left = 12
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_m0p8k"]
content_margin_left = 20.0
content_margin_top = 14.0
content_margin_right = 20.0
content_margin_bottom = 12.0
bg_color = Color(0.043137256, 0.07450981, 0.13333334, 0.9019608)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.18039216, 0.9019608, 0.77254903, 0.2)
corner_radius_top_left = 14
corner_radius_top_right = 14
corner_radius_bottom_right = 14
corner_radius_bottom_left = 14
[node name="Vault" type="Node2D" unique_id=1382797618]
script = ExtResource("1_o8yry")
[node name="View" type="Node2D" parent="." unique_id=1238774489]
script = ExtResource("2_mkhle")
[node name="Camera" type="Camera2D" parent="." unique_id=702468079]
script = ExtResource("3_x7j0k")
[node name="Hud" type="CanvasLayer" parent="." unique_id=1492666604]
[node name="Root" type="Control" parent="Hud" unique_id=344458851]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
theme = ExtResource("4_sch52")
[node name="TopBar" type="PanelContainer" parent="Hud/Root" unique_id=54306824]
layout_mode = 0
offset_left = 36.0
offset_top = 36.0
offset_right = 36.0
offset_bottom = 36.0
theme_override_styles/panel = SubResource("StyleBoxFlat_lvdr8")
[node name="Stats" type="HBoxContainer" parent="Hud/Root/TopBar" unique_id=1335064924]
layout_mode = 2
theme_override_constants/separation = 34
[node name="BottomAnchor" type="MarginContainer" parent="Hud/Root" unique_id=1579352404]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
theme_override_constants/margin_left = 36
theme_override_constants/margin_top = 36
theme_override_constants/margin_right = 36
theme_override_constants/margin_bottom = 36
[node name="Row" type="HBoxContainer" parent="Hud/Root/BottomAnchor" unique_id=1936091014]
layout_mode = 2
mouse_filter = 2
alignment = 1
[node name="BuildBar" type="PanelContainer" parent="Hud/Root/BottomAnchor/Row" unique_id=2023620303]
layout_mode = 2
size_flags_vertical = 8
theme_override_styles/panel = SubResource("StyleBoxFlat_m0p8k")
[node name="Panel" type="VBoxContainer" parent="Hud/Root/BottomAnchor/Row/BuildBar" unique_id=1051865182]
layout_mode = 2
theme_override_constants/separation = 10
[node name="Tools" type="HBoxContainer" parent="Hud/Root/BottomAnchor/Row/BuildBar/Panel" unique_id=375147046]
layout_mode = 2
theme_override_constants/separation = 8
alignment = 1
[node name="Status" type="Label" parent="Hud/Root/BottomAnchor/Row/BuildBar/Panel" unique_id=1253597500]
layout_mode = 2
theme_override_colors/font_color = Color(0.3647059, 0.44705883, 0.53333336, 1)
theme_override_font_sizes/font_size = 15
horizontal_alignment = 1
[node name="PauseMenu" parent="." unique_id=1649378803 instance=ExtResource("5_lffpp")]
+1 -1
View File
@@ -2,7 +2,7 @@
[ext_resource type="Theme" uid="uid://c6kkopjj3snag" path="res://themes/main_menu.theme.tres" id="1_1g8jr"] [ext_resource type="Theme" uid="uid://c6kkopjj3snag" path="res://themes/main_menu.theme.tres" id="1_1g8jr"]
[ext_resource type="Script" uid="uid://3plo51qjjlpl" path="res://scripts/ui/main_menu.gd" id="2_m1kxf"] [ext_resource type="Script" uid="uid://3plo51qjjlpl" path="res://scripts/ui/main_menu.gd" id="2_m1kxf"]
[ext_resource type="PackedScene" uid="uid://bcryjj0nongyh" path="res://scenes/game/game.tscn" id="3_uhagc"] [ext_resource type="PackedScene" uid="uid://dprxap0ioe3ps" path="res://scenes/game/vault.tscn" id="3_uhagc"]
[ext_resource type="Material" uid="uid://6nshffib001t" path="res://shaders/menu_background_material.tres" id="3_x2yvf"] [ext_resource type="Material" uid="uid://6nshffib001t" path="res://shaders/menu_background_material.tres" id="3_x2yvf"]
[ext_resource type="PackedScene" uid="uid://o1xvvu6dik4j" path="res://scenes/ui/settings_menu.tscn" id="4_m1kxf"] [ext_resource type="PackedScene" uid="uid://o1xvvu6dik4j" path="res://scenes/ui/settings_menu.tscn" id="4_m1kxf"]
[ext_resource type="PackedScene" uid="uid://c5o5t7wvc84wx" path="res://scenes/ui/save_menu.tscn" id="6_uhagc"] [ext_resource type="PackedScene" uid="uid://c5o5t7wvc84wx" path="res://scenes/ui/save_menu.tscn" id="6_uhagc"]
+4 -6
View File
@@ -15,12 +15,10 @@ const WINDOW_MODE_LABELS: Array[String] = ["Оконный", "Без рамки"
## Действия, доступные для переназначения, и их подписи в UI. ## Действия, доступные для переназначения, и их подписи в UI.
const ACTION_LABELS := { const ACTION_LABELS := {
"move_forward": "Вперёд", "move_left": "Камера влево",
"move_back": "Назад", "move_right": "Камера вправо",
"move_left": "Влево", "move_up": "Камера вверх",
"move_right": "Вправо", "move_down": "Камера вниз",
"jump": "Прыжок",
"sprint": "Бег",
"interact": "Взаимодействие", "interact": "Взаимодействие",
"pause": "Пауза", "pause": "Пауза",
} }
-28
View File
@@ -1,28 +0,0 @@
extends Node3D
## Сцена-заглушка вместо настоящей игры.
##
## Нужна ровно для двух вещей: показать контракт с SaveManager и дать
## меню паузы что останавливать. Куб крутится только когда игра не на паузе,
## а его угол попадает в сохранение — по нему видно, что загрузка работает.
const SPIN_SPEED := 0.6
@onready var _saves: SaveManagerService = get_node("/root/SaveManager")
@onready var _box: Node3D = $BoxA
func _ready() -> void:
add_to_group(SaveManagerService.PERSIST_GROUP)
_saves.begin_session(scene_file_path)
func _process(delta: float) -> void:
_box.rotate_y(delta * SPIN_SPEED)
func save_data() -> Dictionary:
return {"box_rotation": _box.rotation.y}
func load_data(data: Dictionary) -> void:
_box.rotation.y = float(data.get("box_rotation", 0.0))
-1
View File
@@ -1 +0,0 @@
uid://1viyqekbic0n
+56
View File
@@ -0,0 +1,56 @@
class_name VaultCamera
extends Camera2D
## Камера убежища: перетаскивание, зум колесом и клавиатурная прокрутка.
##
## Границы задаются встроенными limit_* по размеру убежища, поэтому вручную
## позицию не зажимаем — движок делает это сам.
const ZOOM_STEP := 1.12
const MIN_ZOOM := 0.45
const MAX_ZOOM := 2.2
const KEY_PAN_SPEED := 900.0
const EDGE_MARGIN := 420.0
var _dragging := false
func _ready() -> void:
var bounds := VaultGrid.bounds()
limit_left = int(bounds.position.x - EDGE_MARGIN)
limit_right = int(bounds.end.x + EDGE_MARGIN)
limit_top = int(bounds.position.y - EDGE_MARGIN)
limit_bottom = int(bounds.end.y + EDGE_MARGIN)
position = bounds.get_center()
func _process(delta: float) -> void:
var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
if direction != Vector2.ZERO:
position += direction * KEY_PAN_SPEED * delta / zoom.x
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
_handle_mouse_button(event as InputEventMouseButton)
elif event is InputEventMouseMotion and _dragging:
position -= (event as InputEventMouseMotion).relative / zoom
get_viewport().set_input_as_handled()
func _handle_mouse_button(event: InputEventMouseButton) -> void:
match event.button_index:
MOUSE_BUTTON_WHEEL_UP:
_zoom_by(ZOOM_STEP)
get_viewport().set_input_as_handled()
MOUSE_BUTTON_WHEEL_DOWN:
_zoom_by(1.0 / ZOOM_STEP)
get_viewport().set_input_as_handled()
MOUSE_BUTTON_RIGHT, MOUSE_BUTTON_MIDDLE:
# Правая и средняя кнопки таскают вид; левая остаётся строительству.
_dragging = event.pressed
get_viewport().set_input_as_handled()
func _zoom_by(factor: float) -> void:
var target := clampf(zoom.x * factor, MIN_ZOOM, MAX_ZOOM)
zoom = Vector2(target, target)
+1
View File
@@ -0,0 +1 @@
uid://dg751mcd0v178
+177
View File
@@ -0,0 +1,177 @@
class_name VaultGrid
extends RefCounted
## Модель убежища: сетка этажей и комнаты на ней.
##
## Здесь нет ни узлов, ни отрисовки — только данные и правила размещения.
## Вид (vault_view.gd) читает эту модель, а сохранение сериализует её целиком.
## Размер одной ячейки сетки в пикселях мира.
const CELL := Vector2(140.0, 120.0)
## Колонка 0 — лифтовая шахта, комнаты занимают 1..COLUMNS-1.
const COLUMNS := 9
const FLOORS := 6
const ELEVATOR_COLUMN := 0
## Типы комнат: подпись, цвет, ширина в ячейках и баланс ресурсов за цикл.
const KINDS := {
"living": {
"label": "Жилой блок", "color": "#2ee6c5", "width": 2,
"power": -2, "water": 0, "food": 0,
},
"power": {
"label": "Генератор", "color": "#f2b134", "width": 2,
"power": 8, "water": 0, "food": 0,
},
"water": {
"label": "Водоочистка", "color": "#4aa8f2", "width": 2,
"power": -3, "water": 6, "food": 0,
},
"diner": {
"label": "Столовая", "color": "#f2704a", "width": 2,
"power": -3, "water": 0, "food": 6,
},
"storage": {
"label": "Склад", "color": "#9b8cf5", "width": 1,
"power": -1, "water": 0, "food": 0,
},
}
## Комнаты: [{"kind": String, "floor": int, "column": int}, ...]
var rooms: Array[Dictionary] = []
## Стартовая застройка: без неё убежище выглядит пустой ямой.
func reset_to_starter() -> void:
rooms.clear()
place("living", 0, 1)
place("power", 0, 3)
place("diner", 1, 1)
place("water", 1, 3)
# --- Правила размещения -----------------------------------------------------
static func kind_width(kind: String) -> int:
return int(KINDS[kind]["width"])
func in_bounds(a_floor: int, column: int, width: int) -> bool:
if a_floor < 0 or a_floor >= FLOORS:
return false
if column <= ELEVATOR_COLUMN:
return false
return column + width <= COLUMNS
func can_place(kind: String, a_floor: int, column: int) -> bool:
if not KINDS.has(kind):
return false
var width := kind_width(kind)
if not in_bounds(a_floor, column, width):
return false
for offset in width:
if room_index_at(a_floor, column + offset) >= 0:
return false
return true
func place(kind: String, a_floor: int, column: int) -> bool:
if not can_place(kind, a_floor, column):
return false
rooms.append({"kind": kind, "floor": a_floor, "column": column})
return true
## Индекс комнаты, накрывающей ячейку, или -1.
func room_index_at(a_floor: int, column: int) -> int:
for index in rooms.size():
var room: Dictionary = rooms[index]
if int(room["floor"]) != a_floor:
continue
var start := int(room["column"])
if column >= start and column < start + kind_width(String(room["kind"])):
return index
return -1
func remove_at(a_floor: int, column: int) -> bool:
var index := room_index_at(a_floor, column)
if index < 0:
return false
rooms.remove_at(index)
return true
# --- Геометрия --------------------------------------------------------------
static func cell_rect(a_floor: int, column: int, width: int = 1) -> Rect2:
return Rect2(
Vector2(column * CELL.x, a_floor * CELL.y),
Vector2(CELL.x * width, CELL.y)
)
static func room_rect(room: Dictionary) -> Rect2:
return cell_rect(int(room["floor"]), int(room["column"]), kind_width(String(room["kind"])))
## Ячейка под точкой в координатах вида. Может оказаться за пределами сетки —
## проверяйте in_bounds().
static func cell_at(local_position: Vector2) -> Vector2i:
return Vector2i(
int(floor(local_position.x / CELL.x)),
int(floor(local_position.y / CELL.y))
)
## Прямоугольник всего убежища в координатах вида.
static func bounds() -> Rect2:
return Rect2(Vector2.ZERO, Vector2(COLUMNS * CELL.x, FLOORS * CELL.y))
# --- Сводка по ресурсам -----------------------------------------------------
func totals() -> Dictionary:
var result := {"power": 0, "water": 0, "food": 0}
for room: Dictionary in rooms:
var kind: Dictionary = KINDS[String(room["kind"])]
for key: String in result:
result[key] = int(result[key]) + int(kind[key])
return result
func count_of(kind: String) -> int:
var total := 0
for room: Dictionary in rooms:
if String(room["kind"]) == kind:
total += 1
return total
# --- Сериализация -----------------------------------------------------------
func to_array() -> Array:
var data := []
for room: Dictionary in rooms:
data.append({
"kind": String(room["kind"]),
"floor": int(room["floor"]),
"column": int(room["column"]),
})
return data
func from_array(data: Array) -> void:
rooms.clear()
for entry: Variant in data:
if not (entry is Dictionary):
continue
var room: Dictionary = entry
var kind := String(room.get("kind", ""))
if not KINDS.has(kind):
continue
rooms.append({
"kind": kind,
"floor": int(room.get("floor", 0)),
"column": int(room.get("column", 1)),
})
+1
View File
@@ -0,0 +1 @@
uid://0futdc17rt4h
+183
View File
@@ -0,0 +1,183 @@
extends Node2D
## Игровой слой: убежище в разрезе, вид сбоку.
##
## Связывает три части: модель (VaultGrid), отрисовку (VaultView) и HUD.
## Здесь же живёт контракт сохранения — сцена лежит в группе "persistent".
##
## Управление: ЛКМ — построить или снести, ПКМ/СКМ — тащить вид,
## колесо — зум, WASD и стрелки — прокрутка.
## Строки сводки ресурсов: ключ из VaultGrid.totals() → подпись и цвет.
const STAT_ROWS := {
"power": {"label": "Питание", "color": "#f2b134"},
"water": {"label": "Вода", "color": "#4aa8f2"},
"food": {"label": "Еда", "color": "#f2704a"},
}
const HINT := "ЛКМ — строить · ПКМ — тащить вид · колесо — зум"
@onready var _saves: SaveManagerService = get_node("/root/SaveManager")
@onready var _vault: VaultView = $View
@onready var _camera: VaultCamera = $Camera
@onready var _stats: HBoxContainer = $Hud/Root/TopBar/Stats
@onready var _tools: HBoxContainer = $Hud/Root/BottomAnchor/Row/BuildBar/Panel/Tools
@onready var _status: Label = $Hud/Root/BottomAnchor/Row/BuildBar/Panel/Status
var _grid := VaultGrid.new()
var _selected_kind := "living"
var _demolish := false
var _stat_labels := {}
func _ready() -> void:
_grid.reset_to_starter()
_vault.grid = _grid
add_to_group(SaveManagerService.PERSIST_GROUP)
_saves.begin_session(scene_file_path)
_build_stats()
_build_tools()
_refresh_stats()
_status.text = HINT
func _process(_delta: float) -> void:
_vault.set_ghost(_selected_kind, _cell_under_mouse(), _demolish)
func _unhandled_input(event: InputEvent) -> void:
if not (event is InputEventMouseButton):
return
var button := event as InputEventMouseButton
if button.button_index != MOUSE_BUTTON_LEFT or not button.pressed:
return
_apply_at(_cell_under_mouse())
get_viewport().set_input_as_handled()
# --- Строительство ----------------------------------------------------------
func _cell_under_mouse() -> Vector2i:
return VaultGrid.cell_at(_vault.get_local_mouse_position())
func _apply_at(cell: Vector2i) -> void:
if _demolish:
if _grid.remove_at(cell.y, cell.x):
_after_change("Комната снесена.")
else:
_status.text = "Здесь нечего сносить."
return
if _grid.place(_selected_kind, cell.y, cell.x):
_after_change("Построено: %s" % VaultGrid.KINDS[_selected_kind]["label"])
else:
_status.text = "Здесь строить нельзя: занято или вплотную к шахте."
func _after_change(message: String) -> void:
_vault.queue_redraw()
_refresh_stats()
_status.text = message
# --- HUD --------------------------------------------------------------------
func _build_stats() -> void:
for key: String in STAT_ROWS:
var row := VBoxContainer.new()
row.add_theme_constant_override("separation", 0)
var caption := Label.new()
caption.text = String(STAT_ROWS[key]["label"])
caption.add_theme_font_size_override("font_size", 14)
caption.add_theme_color_override("font_color", Color("#8fa5ba"))
row.add_child(caption)
var value := Label.new()
value.add_theme_font_size_override("font_size", 24)
value.add_theme_color_override("font_color", Color(String(STAT_ROWS[key]["color"])))
row.add_child(value)
_stat_labels[key] = value
_stats.add_child(row)
func _refresh_stats() -> void:
var totals := _grid.totals()
for key: String in _stat_labels:
var amount := int(totals[key])
var label: Label = _stat_labels[key]
label.text = "%+d" % amount
# Дефицит важнее оформления: красим в тревожный цвет, а не в цвет ресурса.
if amount < 0:
label.add_theme_color_override("font_color", Color("#f2704a"))
else:
label.add_theme_color_override("font_color", Color(String(STAT_ROWS[key]["color"])))
func _build_tools() -> void:
var group := ButtonGroup.new()
for kind: String in VaultGrid.KINDS:
var button := Button.new()
button.text = String(VaultGrid.KINDS[kind]["label"])
button.toggle_mode = true
button.button_group = group
button.add_theme_font_size_override("font_size", 18)
button.button_pressed = kind == _selected_kind
button.pressed.connect(_on_kind_selected.bind(kind))
_tools.add_child(button)
var separator := Control.new()
separator.custom_minimum_size.x = 20
_tools.add_child(separator)
var demolish := Button.new()
demolish.text = "Снести"
demolish.toggle_mode = true
demolish.button_group = group
demolish.add_theme_font_size_override("font_size", 18)
demolish.pressed.connect(_on_demolish_selected)
_tools.add_child(demolish)
func _on_kind_selected(kind: String) -> void:
_selected_kind = kind
_demolish = false
_status.text = HINT
func _on_demolish_selected() -> void:
_demolish = true
_status.text = "Режим сноса: кликните по комнате."
# --- Сохранение -------------------------------------------------------------
func save_data() -> Dictionary:
return {
"rooms": _grid.to_array(),
"camera": {
"x": _camera.position.x,
"y": _camera.position.y,
"zoom": _camera.zoom.x,
},
}
func load_data(data: Dictionary) -> void:
_grid.from_array(data.get("rooms", []))
var camera_state: Dictionary = data.get("camera", {})
if not camera_state.is_empty():
_camera.position = Vector2(
float(camera_state.get("x", 0.0)), float(camera_state.get("y", 0.0))
)
var level := float(camera_state.get("zoom", 1.0))
_camera.zoom = Vector2(level, level)
_vault.queue_redraw()
_refresh_stats()
_status.text = "Убежище загружено."
+1
View File
@@ -0,0 +1 @@
uid://lsclvc1dgviq
+242
View File
@@ -0,0 +1,242 @@
class_name VaultView
extends Node2D
## Отрисовка убежища в разрезе: небо, порода, лифтовая шахта, комнаты
## и призрак строительства.
##
## Ассетов у проекта пока нет, поэтому всё рисуется процедурно в _draw().
## Когда появится графика, заменять нужно только этот файл — модель
## (VaultGrid) и логика сцены от него не зависят.
const SKY_TOP := Color("#050912")
const SKY_HORIZON := Color("#16333f")
const DIRT_TOP := Color("#2a231d")
const DIRT_BOTTOM := Color("#100d0b")
const SURFACE := Color("#3d4f3a")
const SHELL := Color("#1b262c")
const WALL := Color("#2c3b45")
const OUTLINE := Color("#080d11")
const SHAFT := Color("#141c21")
const RUNG := Color("#c9a227", 0.45)
const CABIN := Color("#c9a227", 0.22)
const EMPTY_FILL := Color("#ffffff", 0.02)
const EMPTY_LINE := Color("#ffffff", 0.06)
const GHOST_OK := Color("#2ee6c5")
const GHOST_BAD := Color("#f2704a")
const MARGIN := 1200.0
const SKY_HEIGHT := 900.0
const DIRT_DEPTH := 460.0
const PEBBLE_SEED := 20260810
## Модель, которую рисуем. Ставится сценой сразу после создания.
var grid: VaultGrid
var _ghost_kind := ""
var _ghost_cell := Vector2i(-1, -1)
var _demolish := false
var _pebbles: Array[Dictionary] = []
func _ready() -> void:
_generate_pebbles()
## Подсветка под курсором: тип строящейся комнаты (пустая строка — ничего),
## ячейка и режим сноса.
func set_ghost(kind: String, cell: Vector2i, demolish: bool) -> void:
if kind == _ghost_kind and cell == _ghost_cell and demolish == _demolish:
return
_ghost_kind = kind
_ghost_cell = cell
_demolish = demolish
queue_redraw()
func _draw() -> void:
if grid == null:
return
_draw_sky()
_draw_dirt()
_draw_shell()
_draw_empty_cells()
_draw_shaft()
_draw_floor_numbers()
for room: Dictionary in grid.rooms:
_draw_room(room)
_draw_ghost()
# --- Фон --------------------------------------------------------------------
func _draw_sky() -> void:
var bounds := VaultGrid.bounds()
var left := -MARGIN
var right := bounds.size.x + MARGIN
_fill_gradient(Rect2(Vector2(left, -SKY_HEIGHT), Vector2(right - left, SKY_HEIGHT)),
SKY_TOP, SKY_HORIZON)
func _draw_dirt() -> void:
var bounds := VaultGrid.bounds()
var left := -MARGIN
var right := bounds.size.x + MARGIN
var depth := bounds.size.y + DIRT_DEPTH
_fill_gradient(Rect2(Vector2(left, 0.0), Vector2(right - left, depth)), DIRT_TOP, DIRT_BOTTOM)
for pebble: Dictionary in _pebbles:
draw_circle(pebble["position"], float(pebble["radius"]), pebble["color"])
# Полоса поверхности — граница земли и неба.
draw_line(Vector2(left, 0.0), Vector2(right, 0.0), SURFACE, 5.0)
## Заливка прямоугольника вертикальным градиентом: draw_rect умеет только
## сплошной цвет, поэтому идём через полигон с цветами вершин.
func _fill_gradient(rect: Rect2, top: Color, bottom: Color) -> void:
var points := PackedVector2Array([
rect.position,
Vector2(rect.end.x, rect.position.y),
rect.end,
Vector2(rect.position.x, rect.end.y),
])
draw_polygon(points, PackedColorArray([top, top, bottom, bottom]))
func _generate_pebbles() -> void:
var rng := RandomNumberGenerator.new()
rng.seed = PEBBLE_SEED
var bounds := VaultGrid.bounds()
for i in 260:
_pebbles.append({
"position": Vector2(
rng.randf_range(-MARGIN, bounds.size.x + MARGIN),
rng.randf_range(6.0, bounds.size.y + DIRT_DEPTH)
),
"radius": rng.randf_range(1.5, 5.0),
"color": Color("#000000", rng.randf_range(0.10, 0.28)),
})
# --- Убежище ----------------------------------------------------------------
func _draw_shell() -> void:
draw_rect(VaultGrid.bounds().grow(10.0), SHELL)
func _draw_empty_cells() -> void:
for a_floor in VaultGrid.FLOORS:
for column in range(VaultGrid.ELEVATOR_COLUMN + 1, VaultGrid.COLUMNS):
if grid.room_index_at(a_floor, column) >= 0:
continue
var rect := VaultGrid.cell_rect(a_floor, column).grow(-3.0)
draw_rect(rect, EMPTY_FILL)
draw_rect(rect, EMPTY_LINE, false, 1.0)
func _draw_shaft() -> void:
var rect := Rect2(
Vector2(VaultGrid.ELEVATOR_COLUMN * VaultGrid.CELL.x, 0.0),
Vector2(VaultGrid.CELL.x, VaultGrid.FLOORS * VaultGrid.CELL.y)
)
draw_rect(rect, SHAFT)
var y := 16.0
while y < rect.size.y:
draw_line(Vector2(rect.position.x + 18.0, y), Vector2(rect.end.x - 18.0, y), RUNG, 2.0)
y += 26.0
# Кабина стоит на верхнем этаже.
var cabin := Rect2(rect.position + Vector2(22.0, 14.0), Vector2(rect.size.x - 44.0, 92.0))
draw_rect(cabin, CABIN)
draw_rect(cabin, RUNG, false, 2.0)
draw_rect(rect, OUTLINE, false, 2.0)
func _draw_floor_numbers() -> void:
var font := ThemeDB.fallback_font
for a_floor in VaultGrid.FLOORS:
var pos := Vector2(-46.0, a_floor * VaultGrid.CELL.y + VaultGrid.CELL.y * 0.5 + 7.0)
draw_string(font, pos, str(a_floor + 1), HORIZONTAL_ALIGNMENT_LEFT, -1, 20,
Color("#ffffff", 0.28))
func _draw_room(room: Dictionary) -> void:
var rect := VaultGrid.room_rect(room)
var kind: Dictionary = VaultGrid.KINDS[String(room["kind"])]
var accent := Color(String(kind["color"]))
draw_rect(rect, WALL)
var inner := rect.grow(-7.0)
_fill_gradient(inner, accent.lerp(Color("#0b1116"), 0.58), accent.lerp(Color("#060a0d"), 0.86))
# Свет от потолочной лампы.
var lamp := Vector2(inner.position.x + inner.size.x * 0.5, inner.position.y + 2.0)
draw_polygon(
PackedVector2Array([
lamp + Vector2(-16.0, 0.0),
lamp + Vector2(16.0, 0.0),
Vector2(inner.end.x - 10.0, inner.end.y),
Vector2(inner.position.x + 10.0, inner.end.y),
]),
PackedColorArray([
Color(accent, 0.13), Color(accent, 0.13), Color(accent, 0.0), Color(accent, 0.0),
])
)
_draw_equipment(inner, accent, VaultGrid.kind_width(String(room["kind"])))
# Пол.
draw_rect(
Rect2(Vector2(inner.position.x, inner.end.y - 7.0), Vector2(inner.size.x, 7.0)),
accent.lerp(Color("#05080a"), 0.55)
)
draw_rect(rect, OUTLINE, false, 2.0)
draw_string(ThemeDB.fallback_font, rect.position + Vector2(14.0, 30.0),
String(kind["label"]), HORIZONTAL_ALIGNMENT_LEFT, rect.size.x - 20.0, 17,
Color("#e6f1f7", 0.92))
## Силуэты оборудования у пола — чтобы комнаты не выглядели пустыми коробками.
func _draw_equipment(inner: Rect2, accent: Color, width: int) -> void:
var body := accent.lerp(Color("#04070a"), 0.62)
var glow := Color(accent, 0.55)
var baseline := inner.end.y - 7.0
var count := 3 if width > 1 else 2
var step := inner.size.x / float(count + 1)
for i in range(1, count + 1):
var center_x := inner.position.x + step * i
var height := 26.0 + float((i * 13) % 22)
var machine := Rect2(Vector2(center_x - 17.0, baseline - height), Vector2(34.0, height))
draw_rect(machine, body)
draw_rect(Rect2(machine.position + Vector2(6.0, 6.0), Vector2(22.0, 4.0)), glow)
# --- Строительство ----------------------------------------------------------
func _draw_ghost() -> void:
if _ghost_cell.x < 0:
return
if _demolish:
var index := grid.room_index_at(_ghost_cell.y, _ghost_cell.x)
if index < 0:
return
var target := VaultGrid.room_rect(grid.rooms[index])
draw_rect(target, Color(GHOST_BAD, 0.22))
draw_rect(target, GHOST_BAD, false, 3.0)
return
if _ghost_kind.is_empty():
return
var width := VaultGrid.kind_width(_ghost_kind)
var rect := VaultGrid.cell_rect(_ghost_cell.y, _ghost_cell.x, width)
var allowed := grid.can_place(_ghost_kind, _ghost_cell.y, _ghost_cell.x)
var tint := GHOST_OK if allowed else GHOST_BAD
draw_rect(rect, Color(tint, 0.16))
draw_rect(rect, tint, false, 3.0)
+1
View File
@@ -0,0 +1 @@
uid://bvimo3kgmefgf