diff --git a/.editorconfig b/.editorconfig index f28239b..9eeae51 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,3 +2,23 @@ root = true [*] charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# GDScript требует табов для отступов. +[*.{gd,gdshader}] +indent_style = tab +indent_size = 4 + +# Сцены и ресурсы Godot редактирует сам — не трогаем пробелы. +[*.{tscn,tres,godot,import,uid}] +trim_trailing_whitespace = false +insert_final_newline = false + +[*.{md,yml,yaml,json}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes index 8ad74f7..092bc29 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,32 @@ -# Normalize EOL for all files that Git considers text files. +# Нормализуем переводы строк во всех текстовых файлах. * text=auto eol=lf + +# Текстовые форматы Godot — держим явно, чтобы Git не гадал. +*.gd text eol=lf +*.gdshader text eol=lf +*.tscn text eol=lf +*.tres text eol=lf +*.godot text eol=lf +*.cfg text eol=lf +*.import text eol=lf +*.uid text eol=lf +*.svg text eol=lf + +# Бинарные ассеты: не нормализовать и не пытаться сливать построчно. +*.png binary +*.jpg binary +*.jpeg binary +*.webp binary +*.ogg binary +*.wav binary +*.mp3 binary +*.ttf binary +*.otf binary +*.res binary +*.scn binary +*.ctex binary +*.glb binary +*.blend binary + +# Плагины — не наши исходники, не учитывать в статистике языков GitHub. +addons/** linguist-vendored diff --git a/.gitignore b/.gitignore index 0af181c..7d3a2d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,38 @@ -# Godot 4+ specific ignores +# --- Godot 4+ --------------------------------------------------------------- .godot/ /android/ + +# Сборки +/build/ +/builds/ +/export/ + +# Может содержать пароли от keystore и пути конкретной машины. +# Если понадобится общий пресет экспорта — храните его отдельно, без секретов. +export_presets.cfg + +# Скомпилированные переводы: собираются из исходных .csv/.po +*.translation + +# --- .NET / C# -------------------------------------------------------------- +.mono/ +data_*/ +[Bb]in/ +[Oo]bj/ +*.csproj.user +*.sln.DotSettings.user + +# --- Локальные настройки инструментов --------------------------------------- +.claude/settings.local.json +.vscode/ +.idea/ +.vs/ + +# --- Мусор ОС --------------------------------------------------------------- +.DS_Store +Thumbs.db +desktop.ini + +# --- Прочее ----------------------------------------------------------------- +*.log +*.tmp diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..dc72dda --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,67 @@ +# CLAUDE.md + +Заметки для агентов, работающих с этим репозиторием. + +## Что это за проект + +Двумерная игра с видом сбоку на Godot 4.7. Референс — Fallout Shelter: разрез убежища, +комнаты в сетке, жители. **Игра полностью 2D.** Если встречаете 3D-узлы — это остатки +заглушки, а не направление проекта. + +## Работа через MCP, а не через файлы + +В проекте включён плагин `addons/godot_ai`, редактор Godot обычно запущен и подключён. +Сцены и ресурсы правьте инструментами MCP (`scene_manage`, `ui_manage`, `node_create`, +`theme_manage`, `resource_manage`), а не редактированием `.tscn` руками — иначе открытый +редактор перезапишет изменения при сохранении. + +Скрипты — через `script_create` / `script_patch`: они возвращают диагностику парсера сразу. + +Проверять результат — `project_run` + `editor_screenshot(source="game")`. Ввод в запущенную +игру шлётся через `game_manage(op="input_key")`; **`input_action` не двигает фокус Control-ов**, +для навигации по UI нужны настоящие события клавиш. + +### Известные особенности + +- `node_set_property` не принимает целочисленный `0` — используйте `batch_execute` + с командой `set_property`. +- Правка скрипта, который зарегистрирован автозагрузкой, даёт в ответе + `GDScript reload failed with error code 43`. Это неудачная горячая перезагрузка в редакторе, + а не ошибка разбора: проверяйте `logs_read(source="editor")`, игра при этом запускается чисто. +- `project_manage(op="settings_set")` отказывается менять `application/run/main_scene`. +- Большие `ui_manage(op="build_layout")` иногда не доезжают целиком — стройте дерево + несколькими вызовами поменьше. +- Запуск из редактора отдаёт встроенное окно: `Embedded window can't be resized` в логе — норма. + +## Конвенции кода + +- Отступы — табы (см. `.editorconfig`), статическая типизация везде, где возможно. +- Комментарии и весь UI-текст — по-русски; имена узлов, файлов, методов, сигналов — по-английски. +- Комментарий объясняет **почему**, а не пересказывает код. +- Приватные члены — с подчёркиванием: `_card`, `_focus_first_button()`. +- Документирующие комментарии `##` — на классах, сигналах и неочевидных методах. + +## Архитектура UI + +- Единая тема `themes/main_menu.theme.tres` на корне каждого экрана. Цвета в узлах не хардкодим. +- Экран = сцена со своим скриптом, наружу торчат `open()` / `close()` и сигнал `closed`. +- Оверлеи центрируются `CenterContainer`, а **не** пресетом `center`: пресет ставит левый + верхний угол карточки в центр экрана. +- Контейнеры не раскладывают скрытые узлы — после `visible = true` нужен + `await get_tree().process_frame`, прежде чем читать `size` (например, для `pivot_offset`). +- Наведение мышью делается фокусом (`mouse_entered` → `grab_focus`), чтобы у мыши и клавиатуры + была одна подсветка. + +## Автозагрузки + +`GameSettings` (`GameSettingsService`) и `SaveManager` (`SaveManagerService`). +У обеих есть `class_name`: константы, перечисления и типы берём через него, экземпляр — +через `get_node("/root/...")`. Имя автозагрузки редактор не резолвит без перезапуска проекта. + +Контракт сохранения игровой сцены описан в [README.md](README.md). + +## Проверка + +Изменения в UI считаются сделанными только после запуска и скриншота. Логи смотреть в обоих +источниках: `logs_read(source="game")` и `logs_read(source="editor")` — ошибки загрузки скриптов +попадают только во второй. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f6fe0c6 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# G-WORLD + +Двумерная игра с видом сбоку на Godot 4.7. Референс по подаче и структуре — **Fallout Shelter**: +разрез убежища, комнаты в сетке, жители, которых расселяют по этажам, и мир, реагирующий на +решения игрока. + +> Статус: ранняя альфа. Готов весь каркас меню; игровой слой пока заглушка. + +--- + +## Что уже работает + +| Экран | Файл | Что умеет | +|---|---|---| +| Главное меню | `scenes/ui/main_menu.tscn` | анимированный фон на шейдере, появление по стаггеру, единая подсветка для мыши и клавиатуры | +| Настройки | `scenes/ui/settings_menu.tscn` | видео, звук, переназначение клавиш; сохраняется в `user://settings.cfg` | +| Пауза | `scenes/ui/pause_menu.tscn` | останавливает дерево, переиспользует экраны настроек и сохранений | +| Сохранения | `scenes/ui/save_menu.tscn` | 6 слотов с миниатюрами, датой и наигранным временем, запись/чтение/удаление | + +Экраны настроек и сохранений — отдельные сцены, вложенные инстансами и в главное меню, и в паузу. +Дублирования нет: правка в одном месте видна везде. + +## Требования + +- **Godot 4.7** (Forward+, .NET-сборка не обязательна — код на GDScript) +- Windows / Linux / macOS + +## Запуск + +Откройте папку проекта в Godot и нажмите F5 — главная сцена уже прописана +(`res://scenes/ui/main_menu.tscn`). + +## Структура + +``` +scenes/ + ui/ main_menu, settings_menu, pause_menu, save_menu + game/ game.tscn — заглушка вместо игрового слоя +scripts/ + autoload/ game_settings.gd, save_manager.gd + ui/ по скрипту на экран + game/ game_stub.gd +shaders/ menu_background.gdshader + материал +themes/ main_menu.theme.tres — единая тема всех экранов +default_bus_layout.tres — шины микшера: Master → Music, SFX +``` + +## Соглашения + +**Тема.** Все экраны наследуют `themes/main_menu.theme.tres`. Цвета не прописываются в узлах: +акцент `#2ee6c5`, второй акцент `#7b6bf5`, текст `#dbe7f0`, приглушённый `#8fa5ba`. +Локальные `theme_override_*` — только там, где элемент должен отличаться намеренно. + +**Язык.** Интерфейс и комментарии — русские, имена узлов, файлов и методов — английские. + +**Автозагрузки.** + +- `GameSettings` (`GameSettingsService`) — настройки: чтение, применение к движку, `user://settings.cfg` +- `SaveManager` (`SaveManagerService`) — слоты сохранений в `user://saves/` + +У обеих есть `class_name`: константы и типы берутся через него, экземпляр — через +`get_node("/root/GameSettings")`. Так редактор резолвит имена без перезапуска проекта. + +**Контракт игровой сцены.** Чтобы сцена умела сохраняться: + +```gdscript +func _ready() -> void: + add_to_group(SaveManagerService.PERSIST_GROUP) + SaveManager.begin_session(scene_file_path) + +func save_data() -> Dictionary: + return {"...": ...} + +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`. Переназначаются в настройках. + +**Слияния.** `.tscn` и `.tres` — текстовые, но построчный авто-мердж по ним даёт мусор. +Конфликтующие сцены лучше разрешать, открывая обе версии в редакторе, а не в diff-вьювере. + +## Что дальше + +- [ ] Перевести игровой слой на 2D: сетка комнат в разрезе, вид сбоку +- [ ] Жители: найм, характеристики, распределение по комнатам +- [ ] Ресурсы и производство, циклы времени +- [ ] Заменить `scenes/game/game.tscn` на настоящую сцену +- [ ] Звук: подключить шины Music и SFX +- [ ] Лицензия не выбрана + +## Лицензия + +Не определена. Плагин `addons/godot_ai` поставляется со своей лицензией — см. +`addons/godot_ai/LICENSE`. diff --git a/scripts/ui/save_menu.gd b/scripts/ui/save_menu.gd index 46aa191..70a5a26 100644 --- a/scripts/ui/save_menu.gd +++ b/scripts/ui/save_menu.gd @@ -72,11 +72,33 @@ func close() -> void: # --- Список слотов ---------------------------------------------------------- -func _refresh() -> void: +## Перестраивает список. focus_row/focus_delete возвращают фокус на ту же +## кнопку, что была нажата, — иначе после перерисовки он теряется. +func _refresh(focus_row: int = -1, focus_delete: bool = false) -> void: for child in _slots_box.get_children(): + _slots_box.remove_child(child) child.queue_free() + + var rows: Array[Node] = [] for info: Dictionary in _saves.list_slots(): - _slots_box.add_child(_make_row(info)) + var row := _make_row(info) + _slots_box.add_child(row) + rows.append(row) + + if focus_row >= 0 and focus_row < rows.size(): + _focus_row_button(rows[focus_row], focus_delete) + + +func _focus_row_button(row: Node, delete: bool) -> void: + var buttons: Array[Button] = [] + for child: Node in row.get_child(0).get_children(): + if child is Button: + buttons.append(child as Button) + if buttons.is_empty(): + return + var target := buttons[1] if delete and buttons.size() > 1 else buttons[0] + if not target.disabled: + target.grab_focus() func _make_row(info: Dictionary) -> PanelContainer: @@ -196,7 +218,7 @@ func _on_action_pressed(index: int) -> void: _status.text = "Сохранено в слот %d." % (index + 1) else: _status.text = "Не удалось сохранить в слот %d." % (index + 1) - _refresh() + _refresh(index) func _on_delete_pressed(index: int) -> void: @@ -207,13 +229,13 @@ func _on_delete_pressed(index: int) -> void: _pending = {} _saves.delete_slot(index) _status.text = "Слот %d удалён." % (index + 1) - _refresh() + _refresh(index) func _set_pending(action: String, index: int, message: String) -> void: _pending = {"action": action, "index": index} _status.text = message - _refresh() + _refresh(index, action == "delete") func _is_pending(action: String, index: int) -> bool: