diff --git a/CLAUDE.md b/CLAUDE.md index dc72dda..7a063ad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`). diff --git a/README.md b/README.md index f6fe0c6..d873556 100644 --- a/README.md +++ b/README.md @@ -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 - [ ] Лицензия не выбрана diff --git a/project.godot b/project.godot index 3a32c01..dcedb4d 100644 --- a/project.godot +++ b/project.godot @@ -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" diff --git a/scenes/game/game.tscn b/scenes/game/game.tscn deleted file mode 100644 index 780f066..0000000 --- a/scenes/game/game.tscn +++ /dev/null @@ -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 — пауза." diff --git a/scenes/game/vault.tscn b/scenes/game/vault.tscn new file mode 100644 index 0000000..4a12d05 --- /dev/null +++ b/scenes/game/vault.tscn @@ -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")] diff --git a/scenes/ui/main_menu.tscn b/scenes/ui/main_menu.tscn index 78fde11..bdf24cd 100644 --- a/scenes/ui/main_menu.tscn +++ b/scenes/ui/main_menu.tscn @@ -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"] diff --git a/scripts/autoload/game_settings.gd b/scripts/autoload/game_settings.gd index 531726d..10e0e3d 100644 --- a/scripts/autoload/game_settings.gd +++ b/scripts/autoload/game_settings.gd @@ -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": "Пауза", } diff --git a/scripts/game/game_stub.gd b/scripts/game/game_stub.gd deleted file mode 100644 index 60aa537..0000000 --- a/scripts/game/game_stub.gd +++ /dev/null @@ -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)) diff --git a/scripts/game/game_stub.gd.uid b/scripts/game/game_stub.gd.uid deleted file mode 100644 index 549a115..0000000 --- a/scripts/game/game_stub.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://1viyqekbic0n diff --git a/scripts/game/vault_camera.gd b/scripts/game/vault_camera.gd new file mode 100644 index 0000000..1c1f5c6 --- /dev/null +++ b/scripts/game/vault_camera.gd @@ -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) diff --git a/scripts/game/vault_camera.gd.uid b/scripts/game/vault_camera.gd.uid new file mode 100644 index 0000000..66546f3 --- /dev/null +++ b/scripts/game/vault_camera.gd.uid @@ -0,0 +1 @@ +uid://dg751mcd0v178 diff --git a/scripts/game/vault_grid.gd b/scripts/game/vault_grid.gd new file mode 100644 index 0000000..6c767f3 --- /dev/null +++ b/scripts/game/vault_grid.gd @@ -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)), + }) diff --git a/scripts/game/vault_grid.gd.uid b/scripts/game/vault_grid.gd.uid new file mode 100644 index 0000000..2411194 --- /dev/null +++ b/scripts/game/vault_grid.gd.uid @@ -0,0 +1 @@ +uid://0futdc17rt4h diff --git a/scripts/game/vault_scene.gd b/scripts/game/vault_scene.gd new file mode 100644 index 0000000..8f13304 --- /dev/null +++ b/scripts/game/vault_scene.gd @@ -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 = "Убежище загружено." diff --git a/scripts/game/vault_scene.gd.uid b/scripts/game/vault_scene.gd.uid new file mode 100644 index 0000000..4a56566 --- /dev/null +++ b/scripts/game/vault_scene.gd.uid @@ -0,0 +1 @@ +uid://lsclvc1dgviq diff --git a/scripts/game/vault_view.gd b/scripts/game/vault_view.gd new file mode 100644 index 0000000..7eaa4a8 --- /dev/null +++ b/scripts/game/vault_view.gd @@ -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) diff --git a/scripts/game/vault_view.gd.uid b/scripts/game/vault_view.gd.uid new file mode 100644 index 0000000..f7cf9ca --- /dev/null +++ b/scripts/game/vault_view.gd.uid @@ -0,0 +1 @@ +uid://bvimo3kgmefgf