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:
@@ -5,8 +5,8 @@
|
||||
## Что это за проект
|
||||
|
||||
Двумерная игра с видом сбоку на Godot 4.7. Референс — Fallout Shelter: разрез убежища,
|
||||
комнаты в сетке, жители. **Игра полностью 2D.** Если встречаете 3D-узлы — это остатки
|
||||
заглушки, а не направление проекта.
|
||||
комнаты в сетке, жители. **Игра полностью 2D**, рендерер Mobile. Новые игровые сцены —
|
||||
на `Node2D`/`Control`; 3D-узлам в проекте места нет.
|
||||
|
||||
## Работа через MCP, а не через файлы
|
||||
|
||||
@@ -46,12 +46,27 @@
|
||||
- Единая тема `themes/main_menu.theme.tres` на корне каждого экрана. Цвета в узлах не хардкодим.
|
||||
- Экран = сцена со своим скриптом, наружу торчат `open()` / `close()` и сигнал `closed`.
|
||||
- Оверлеи центрируются `CenterContainer`, а **не** пресетом `center`: пресет ставит левый
|
||||
верхний угол карточки в центр экрана.
|
||||
верхний угол карточки в центр экрана. Та же ловушка у `bottom_wide` — он прижимает к нижнему
|
||||
краю *верх* панели, и она уезжает за экран; для нижней панели используйте `MarginContainer`
|
||||
во весь экран + `HBoxContainer` с `size_flags_vertical = SIZE_SHRINK_END` у содержимого.
|
||||
- Контейнеры не раскладывают скрытые узлы — после `visible = true` нужен
|
||||
`await get_tree().process_frame`, прежде чем читать `size` (например, для `pivot_offset`).
|
||||
- Наведение мышью делается фокусом (`mouse_entered` → `grab_focus`), чтобы у мыши и клавиатуры
|
||||
была одна подсветка.
|
||||
|
||||
## Игровой слой
|
||||
|
||||
Три файла в `scripts/game/`, разделены намеренно:
|
||||
|
||||
- `vault_grid.gd` — только данные и правила (`RefCounted`, без узлов). Сюда идут изменения
|
||||
логики: типы комнат, размещение, ресурсы.
|
||||
- `vault_view.gd` — только `_draw()`. Сюда идут изменения картинки. Когда появится графика,
|
||||
переписывается этот файл, остальные не трогаются.
|
||||
- `vault_scene.gd` — связка модели, вида, камеры и HUD плюс контракт сохранения.
|
||||
|
||||
Новый тип комнаты добавляется одной записью в `VaultGrid.KINDS`: кнопка в панели строительства
|
||||
и подсчёт ресурсов подхватятся сами.
|
||||
|
||||
## Автозагрузки
|
||||
|
||||
`GameSettings` (`GameSettingsService`) и `SaveManager` (`SaveManagerService`).
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
разрез убежища, комнаты в сетке, жители, которых расселяют по этажам, и мир, реагирующий на
|
||||
решения игрока.
|
||||
|
||||
> Статус: ранняя альфа. Готов весь каркас меню; игровой слой пока заглушка.
|
||||
> Статус: ранняя альфа. Готов каркас меню и первый игровой слой — сетка комнат в разрезе.
|
||||
|
||||
---
|
||||
|
||||
## Что уже работает
|
||||
|
||||
| Экран | Файл | Что умеет |
|
||||
| Часть | Файл | Что умеет |
|
||||
|---|---|---|
|
||||
| Убежище | `scenes/game/vault.tscn` | разрез на 6 этажей, лифтовая шахта, строительство и снос комнат, сводка ресурсов |
|
||||
| Главное меню | `scenes/ui/main_menu.tscn` | анимированный фон на шейдере, появление по стаггеру, единая подсветка для мыши и клавиатуры |
|
||||
| Настройки | `scenes/ui/settings_menu.tscn` | видео, звук, переназначение клавиш; сохраняется в `user://settings.cfg` |
|
||||
| Пауза | `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)
|
||||
@@ -35,11 +53,11 @@
|
||||
```
|
||||
scenes/
|
||||
ui/ main_menu, settings_menu, pause_menu, save_menu
|
||||
game/ game.tscn — заглушка вместо игрового слоя
|
||||
game/ vault.tscn — убежище
|
||||
scripts/
|
||||
autoload/ game_settings.gd, save_manager.gd
|
||||
ui/ по скрипту на экран
|
||||
game/ game_stub.gd
|
||||
game/ vault_grid.gd, vault_view.gd, vault_camera.gd, vault_scene.gd
|
||||
shaders/ menu_background.gdshader + материал
|
||||
themes/ main_menu.theme.tres — единая тема всех экранов
|
||||
default_bus_layout.tres — шины микшера: Master → Music, SFX
|
||||
@@ -78,18 +96,24 @@ func load_data(data: Dictionary) -> void:
|
||||
`SaveManager` обходит группу `persistent`, собирает словари по путям узлов и пишет JSON.
|
||||
Миниатюра слота — кадр, снятый в момент вызова паузы, до показа оверлея.
|
||||
|
||||
**Действия ввода** (`project.godot` → Input Map): `move_left`, `move_right`, `move_forward`,
|
||||
`move_back`, `jump`, `sprint`, `interact`, `pause`. Переназначаются в настройках.
|
||||
**Действия ввода** (`project.godot` → Input Map): `move_left`, `move_right`, `move_up`,
|
||||
`move_down` (прокрутка камеры), `interact`, `pause`. Переназначаются в настройках; список
|
||||
в UI строится из `GameSettingsService.ACTION_LABELS`, так что новое действие нужно добавить
|
||||
и туда.
|
||||
|
||||
**Рендерер** — Mobile: для 2D он легче Forward+. Смена рендерера требует перезапуска редактора.
|
||||
|
||||
**Слияния.** `.tscn` и `.tres` — текстовые, но построчный авто-мердж по ним даёт мусор.
|
||||
Конфликтующие сцены лучше разрешать, открывая обе версии в редакторе, а не в diff-вьювере.
|
||||
|
||||
## Что дальше
|
||||
|
||||
- [ ] Перевести игровой слой на 2D: сетка комнат в разрезе, вид сбоку
|
||||
- [ ] Жители: найм, характеристики, распределение по комнатам
|
||||
- [ ] Ресурсы и производство, циклы времени
|
||||
- [ ] Заменить `scenes/game/game.tscn` на настоящую сцену
|
||||
- [x] Игровой слой на 2D: сетка комнат в разрезе, вид сбоку
|
||||
- [ ] Жители: найм, характеристики, расселение по комнатам
|
||||
- [ ] Время и производство: ресурсы должны копиться, а не только считаться в сводке
|
||||
- [ ] Слияние одинаковых соседних комнат, как в референсе
|
||||
- [ ] Стоимость строительства и ограничение по ресурсам
|
||||
- [ ] Графика вместо процедурной отрисовки в `vault_view.gd`
|
||||
- [ ] Звук: подключить шины Music и SFX
|
||||
- [ ] Лицензия не выбрана
|
||||
|
||||
|
||||
+12
-21
@@ -12,7 +12,7 @@ config_version=5
|
||||
|
||||
config/name="GWorld"
|
||||
run/main_scene="uid://b6mhe7465wwge"
|
||||
config/features=PackedStringArray("4.7", "Forward Plus")
|
||||
config/features=PackedStringArray("4.7", "Mobile")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[autoload]
|
||||
@@ -38,16 +38,6 @@ enabled=PackedStringArray("res://addons/godot_ai/plugin.cfg")
|
||||
|
||||
[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={
|
||||
"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)
|
||||
@@ -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)
|
||||
]
|
||||
}
|
||||
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={
|
||||
"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)
|
||||
@@ -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)
|
||||
]
|
||||
}
|
||||
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]
|
||||
|
||||
@@ -86,3 +76,4 @@ pause={
|
||||
[rendering]
|
||||
|
||||
rendering_device/driver.windows="d3d12"
|
||||
renderer/rendering_method="mobile"
|
||||
|
||||
@@ -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 — пауза."
|
||||
@@ -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")]
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[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="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="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"]
|
||||
|
||||
@@ -15,12 +15,10 @@ const WINDOW_MODE_LABELS: Array[String] = ["Оконный", "Без рамки"
|
||||
|
||||
## Действия, доступные для переназначения, и их подписи в UI.
|
||||
const ACTION_LABELS := {
|
||||
"move_forward": "Вперёд",
|
||||
"move_back": "Назад",
|
||||
"move_left": "Влево",
|
||||
"move_right": "Вправо",
|
||||
"jump": "Прыжок",
|
||||
"sprint": "Бег",
|
||||
"move_left": "Камера влево",
|
||||
"move_right": "Камера вправо",
|
||||
"move_up": "Камера вверх",
|
||||
"move_down": "Камера вниз",
|
||||
"interact": "Взаимодействие",
|
||||
"pause": "Пауза",
|
||||
}
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
uid://1viyqekbic0n
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dg751mcd0v178
|
||||
@@ -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)),
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
uid://0futdc17rt4h
|
||||
@@ -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 = "Убежище загружено."
|
||||
@@ -0,0 +1 @@
|
||||
uid://lsclvc1dgviq
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bvimo3kgmefgf
|
||||
Reference in New Issue
Block a user