485 lines
43 KiB
Markdown
485 lines
43 KiB
Markdown
# Внешние DLC-темы Exchanger
|
||
|
||
## Архитектура
|
||
|
||
Основной проект Exchanger не содержит редактируемых ресурсов тем. В нём остаются только:
|
||
|
||
- `BuiltInFallbackTheme` — единственная минимальная тема, создаваемая кодом;
|
||
- `VendingThemeDefinition` — runtime-представление уже загруженной темы;
|
||
- `ThemeDlcLoader` и валидатор manifest;
|
||
- `ThemeManager`, выбирающий fallback либо один внешний пакет при запуске.
|
||
- `ThemeBindableScreen` и `ThemeBindingResolver`, связывающие app-owned логику с визуальными
|
||
сценами DLC по стабильным binding-id.
|
||
|
||
Все исходники визуальных тем находятся в соседнем проекте `../ExchangerThemeDlcBuilder/`. В Exchanger отсутствуют `themes/`, исходные `.tres`-темы, фоновые картинки и шаблоны их разработки.
|
||
|
||
## Путь из пользовательских файлов
|
||
|
||
Монтажный способ выбора темы задаётся в `user://config/technical_settings.toml`:
|
||
|
||
```toml
|
||
[theme]
|
||
enabled = true
|
||
directory = "user://theme_dlc"
|
||
file_name = "active_theme"
|
||
```
|
||
|
||
Путь собирается как `<directory>/<file_name>.pck`; расширение в `file_name` не указывается.
|
||
Разрешены `user://` и абсолютный локальный каталог, включая Linux-путь вроде
|
||
`/opt/exchanger/themes`. Если optional раздел `[theme]` отсутствует в старом TOML версии 1,
|
||
используется прежняя настройка из `appsettings.json`:
|
||
|
||
Для Windows разрешены `directory = 'D:\Exchanger\themes'` (literal TOML-строка без
|
||
экранирования), `directory = "D:/Exchanger/themes"` либо двойные обратные слеши внутри обычной
|
||
двойной строки.
|
||
|
||
Настройка хранится в `%APPDATA%\Godot\app_userdata\Exchanger\config\appsettings.json`. Значение по умолчанию:
|
||
|
||
```json
|
||
{
|
||
"Themes": {
|
||
"ReducedEffects": false,
|
||
"ExternalDlcEnabled": true,
|
||
"ExternalDlcPath": "user://theme_dlc/active_theme.pck"
|
||
}
|
||
}
|
||
```
|
||
|
||
На Windows этот `user://` соответствует `%APPDATA%\Godot\app_userdata\Exchanger\theme_dlc\active_theme.pck`. Также разрешён полный абсолютный путь. Относительные пути и `res://` отклоняются. Технический TOML имеет приоритет над JSON, а разовые `--theme-dlc=D:\ExchangerThemes\active_theme.pck` и `--no-theme-dlc` — над TOML.
|
||
|
||
В debug-сборке эти параметры доступны во вкладке `F10 → Тема`. Корректный путь разрешено
|
||
сохранить до появления пакета: при следующем запуске отсутствующий файл безопасно оставит
|
||
встроенную тему. Включение DLC и изменение пути применяются только после перезапуска.
|
||
`ReducedEffects` имеет немедленный предварительный просмотр; кнопка «Отменить» возвращает
|
||
сохранённое значение.
|
||
|
||
## Формат пакета
|
||
|
||
DLC является доверенным Godot resource pack `.pck`:
|
||
|
||
```text
|
||
res://exchanger_theme_dlc/
|
||
├── manifest.json
|
||
├── ui_theme.tres
|
||
├── background.png|webp|jpg|svg # необязательно, только legacy-композиция
|
||
├── background_decoration.tscn # необязательно, только legacy-композиция
|
||
├── components/ # необязательные theme-owned runtime-компоненты
|
||
│ └── interactive_pet.tscn # рекомендуемое имя сцены питомца
|
||
├── visuals/ # необязательные runtime-сцены экранов
|
||
├── scripts/ # GDScript темы
|
||
├── contracts/ # обязательно для manifest v2
|
||
│ └── screen_bindings.v2.json
|
||
└── scenes/ # обязательно для manifest v2
|
||
├── screen_home.tscn
|
||
├── screen_cash_payment.tscn
|
||
├── screen_card_amount.tscn
|
||
├── screen_card_custom_amount.tscn
|
||
├── screen_card_terminal.tscn
|
||
├── screen_success.tscn
|
||
├── screen_error.tscn
|
||
├── screen_service_access.tscn
|
||
└── screen_settings.tscn
|
||
```
|
||
|
||
Manifest версии 1:
|
||
|
||
```json
|
||
{
|
||
"Format": "ExchangerThemeDlc",
|
||
"FormatVersion": 1,
|
||
"ThemeId": "sample-theme",
|
||
"DisplayName": "Название темы",
|
||
"UiThemePath": "res://exchanger_theme_dlc/ui_theme.tres",
|
||
"BackgroundTexturePath": "res://exchanger_theme_dlc/background.svg",
|
||
"BackgroundDecorationPath": "res://exchanger_theme_dlc/background_decoration.tscn",
|
||
"BackgroundDecorationScale": 1.0,
|
||
"InteractivePetScene": "res://exchanger_theme_dlc/components/interactive_pet.tscn",
|
||
"VisualSlots": {
|
||
"home.top": "res://exchanger_theme_dlc/visuals/home_top.tscn",
|
||
"card.terminal": "res://exchanger_theme_dlc/visuals/card_terminal.tscn"
|
||
},
|
||
"FallbackColor": "#061229",
|
||
"UseNearestTextureFilter": false
|
||
}
|
||
```
|
||
|
||
Версия 1 остаётся совместимой и меняет стили, фон и visual slots встроенных экранов. Для новой
|
||
темы следует использовать manifest версии 2: он передаёт теме всё видимое дерево каждого экрана.
|
||
|
||
```json
|
||
{
|
||
"Format": "ExchangerThemeDlc",
|
||
"FormatVersion": 2,
|
||
"ThemeId": "sample-theme",
|
||
"DisplayName": "Название темы",
|
||
"UiThemePath": "res://exchanger_theme_dlc/ui_theme.tres",
|
||
"InteractivePetScene": "res://exchanger_theme_dlc/components/interactive_pet.tscn",
|
||
"ScreenScenes": {
|
||
"screen.home": "res://exchanger_theme_dlc/scenes/screen_home.tscn",
|
||
"screen.cash_payment": "res://exchanger_theme_dlc/scenes/screen_cash_payment.tscn",
|
||
"screen.card_amount": "res://exchanger_theme_dlc/scenes/screen_card_amount.tscn",
|
||
"screen.card_custom_amount": "res://exchanger_theme_dlc/scenes/screen_card_custom_amount.tscn",
|
||
"screen.card_terminal": "res://exchanger_theme_dlc/scenes/screen_card_terminal.tscn",
|
||
"screen.success": "res://exchanger_theme_dlc/scenes/screen_success.tscn",
|
||
"screen.error": "res://exchanger_theme_dlc/scenes/screen_error.tscn",
|
||
"screen.service_access": "res://exchanger_theme_dlc/scenes/screen_service_access.tscn",
|
||
"screen.settings": "res://exchanger_theme_dlc/scenes/screen_settings.tscn"
|
||
},
|
||
"VisualSlots": {},
|
||
"FallbackColor": "#061229",
|
||
"UseNearestTextureFilter": true
|
||
}
|
||
```
|
||
|
||
`UiThemePath` обязателен. Фон и декорация могут быть пустыми строками.
|
||
`UseNearestTextureFilter` необязателен и по умолчанию равен `false`. Его следует включать
|
||
только для pixel-art тем; тогда приложение применяет nearest-фильтрацию к фону и Control-узлам,
|
||
не меняя глобальные настройки рендера для других тем.
|
||
`BackgroundDecorationScale` необязателен, по умолчанию равен `1.0` и допускает значения 0.1–4.0.
|
||
Он нужен только для decoration, подготовленной не в координатах host 720×1280.
|
||
|
||
`InteractivePetScene` необязателен. Он указывает на `PackedScene` внутри
|
||
`res://exchanger_theme_dlc/`; рекомендуемый путь — `components/interactive_pet.tscn`, но manifest
|
||
может объявить любое другое имя сцены внутри package. Exchanger безопасно проверяет namespace,
|
||
расширение и наличие ресурса, загружает сцену в `VendingThemeDefinition.InteractivePetScene`, но
|
||
показывает её на всех экранах, кроме `screen.settings`, для которых тема предоставила корректный
|
||
навигационный граф. Актуальный Builder требует такой граф в каждой из восьми non-settings сцен
|
||
темы с питомцем; host сохраняет совместимость со старыми пакетами и скрывает питомца только на
|
||
конкретной сцене без графа.
|
||
Обычная навигация и ввод остаются app-owned, а сцена питомца, анимации, размеры и геометрия
|
||
доступных поверхностей — theme-owned. Для уникальной многошаговой реакции тема может временно
|
||
управлять экранной опорной точкой через scoped runtime, не получая ссылок на внутренние узлы host.
|
||
|
||
Корень компонента питомца — `Node2D`. Нейтральный API состоит из методов
|
||
`get_pet_animation_names`, `get_pet_animation_frame_count`, `has_pet_animation`,
|
||
`play_pet_animation`, `pause_pet_animation`, `stop_pet_animation`,
|
||
`get_current_pet_animation`, `get_current_pet_frame` и сигналов `pet_animation_started` /
|
||
`pet_animation_completed`. Имена и количество анимаций определяет тема. Сцена напрямую ссылается
|
||
на свои ресурсы, поэтому каталог текстур может называться как угодно и не входит в контракт.
|
||
|
||
Для поведения тема дополнительно может реализовать методы `get_pet_action_animation`,
|
||
`get_pet_runtime_scale`, `get_pet_walk_speed`, `get_pet_climb_speed`, `get_pet_hit_size` и
|
||
`get_pet_ground_offset`. Первый метод сопоставляет нейтральные действия приложения с
|
||
фактическими именами клипов конкретной темы; в том числе `sit` для посадки после приземления.
|
||
Это сохраняет совместимость с уже выпущенными DLC.
|
||
|
||
Во время обычного падения host запрашивает действие `drop`, после контакта с объявленной
|
||
поверхностью — один полный цикл `landing`, а затем включает `idle`. Даже если при смене экрана
|
||
anchor уже совпадает с новой поверхностью, состояние `landing` не пропускается. Если старая тема
|
||
не содержит mapping или клип `landing`, host выдерживает безопасную фазу `Landing` в 0,5 секунды
|
||
и пишет предупреждение вместо немедленного перехода к `idle`. `celebrate` запускается при переходе
|
||
к клиентской выдаче жетонов: host последовательно проигрывает четыре полных цикла. Если тема не
|
||
содержит его, для обратной совместимости используется `interact`. Длительность цикла определяется
|
||
обязательным `get_pet_animation_frame_count` и optional `get_pet_animation_frames_per_second`; при
|
||
отсутствии корректной скорости действует безопасный fallback. Запрошенное во время `falling` или `landing`
|
||
празднование ждёт завершения приземления и только затем начинает первый цикл.
|
||
|
||
После завершения обычного маршрута host выбирает паузу независимо от темы: с вероятностью 35%
|
||
он показывает `idle` на 3–5 секунд, в остальных случаях — спокойный `sitting` на 1,5–3,5 секунды.
|
||
Темы без `sitting` остаются совместимыми: в этой ветке используется `idle`.
|
||
|
||
Необязательный составной контракт позволяет теме самой выполнить действие из нескольких клипов,
|
||
визуальных преобразований и перемещений. Он принимается только целиком: методы
|
||
`has_pet_composite_action`, `start_pet_composite_action`, `cancel_pet_composite_action` и сигналы
|
||
`pet_composite_action_started` / `pet_composite_action_finished`. При отсутствии или неполной
|
||
реализации Exchanger использует одиночную анимацию с тем же каноническим именем.
|
||
|
||
`start_pet_composite_action(action, runtime, parameters)` возвращает run id переданного runtime.
|
||
Для `interact` параметры содержат `action_repetitions` (1–16) и `reduced_effects`. Scoped runtime
|
||
даёт методы `GetRunId`, `IsActive`, `GetViewportRect`, `GetAnchor`, `GetHighestSupport`,
|
||
`SetMotionPolicy`, `SetAnchor`, `MoveAnchor` и `LandOnSupport`. Политика отдельно управляет обычной
|
||
навигацией (`suspended`/`theme_driven`), контактами с опорами (`ignore`, `solid`,
|
||
`after_vertical_wrap`), границами (`clamp`, `allow`, `wrap_vertical_once`) и пользовательским
|
||
вводом. Для посадки тема указывает `landing_surface_id`; host проверяет, что такая поверхность
|
||
существует и сейчас доступна.
|
||
|
||
Каждый runtime привязан к одному запуску. Смена экрана/темы, начало drag и конфликтующее действие
|
||
инвалидируют его; последующие команды старого runtime отклоняются. После сигнала успешного
|
||
завершения host возвращает обычное состояние Sitting. Поэтому GDScript темы обязан проверять
|
||
`IsActive()` после каждого `await`, корректно обрабатывать `cancel_pet_composite_action` и не
|
||
сохранять runtime для будущих запусков.
|
||
|
||
Маршруты задаются отдельными невидимыми metadata-маркерами внутри каждой готовой сцены, кроме
|
||
`screen.settings`:
|
||
|
||
- один root с `exchanger_pet_navigation_root = true`, версией, `Rect2` границами и id
|
||
поверхности по умолчанию;
|
||
- поверхности с уникальным id, диапазоном X, координатой опорной точки Y и необязательными
|
||
точками посадки;
|
||
- двунаправленные рёбра подъёма с id исходной/целевой поверхности, X края и двумя Y.
|
||
|
||
Поверхность может дополнительно объявить строковую metadata
|
||
`exchanger_pet_surface_visibility_binding_id`. Она должна ссылаться на `CanvasItem` этой же
|
||
экранной сцены по `exchanger_binding_id`. Если готовый визуальный элемент скрыт пользовательской
|
||
настройкой, host исключает такую поверхность и все связанные с ней рёбра из активного графа.
|
||
|
||
Все координаты описывают экранную опорную точку питомца в системе 720×1280. Host не вычисляет
|
||
поверхности из имён или геометрии `Control` и не меняет визуалы темы. Он ходит по объявленным
|
||
поверхностям, строит путь по рёбрам, поднимается и спускается у заданных краёв, а после
|
||
перетаскивания первым пальцем или зажатой ЛКМ сажает питомца на ближайшую нижнюю поверхность.
|
||
На каждом ненулевом участке подъёма или спуска host выбирает случайную точку между 20% и 80%
|
||
пути, приостанавливает в ней перемещение и последовательно запрашивает действия
|
||
`climbing_action_start`, `climbing_action` и `climbing_action_stop`. Средняя анимация выполняется
|
||
один полный цикл, после чего питомец продолжает тот же маршрут и направление. Последовательность
|
||
включается только при наличии всех трёх действий и клипов; тема без любого из них сохраняет
|
||
обычное непрерывное движение для обратной совместимости.
|
||
При переключении экрана host выбирает граф новой сцены, сохраняет текущую экранную позицию
|
||
питомца и запускает `falling` до её поверхности по умолчанию; после посадки доступен весь новый граф.
|
||
На `screen.settings` питомец скрыт, а его сохранённая позиция используется при выходе из настроек.
|
||
`hanging` действует только пока пользователь удерживает питомца, а `falling` — во время его падения
|
||
к выбранной поверхности после отпускания.
|
||
Режим reduced-effects отключает автономное блуждание, сохраняя явное взаимодействие и drag.
|
||
|
||
Новые темы используют самодостаточную композицию экранов. В этом режиме каждый root из
|
||
`ScreenScenes` имеет metadata `exchanger_self_contained_visuals = true`, назначенный `Theme` и
|
||
два прямых дочерних узла: `ThemeBackground` типа `TextureRect` с текстурой и
|
||
`BackgroundDecoration` типа `CanvasItem`. Все девять сцен пакета обязаны выбрать одинаковый
|
||
режим. `BackgroundTexturePath` и `BackgroundDecorationPath` при этом оставляют пустыми, иначе
|
||
приложение отклонит DLC как дублирующую визуальную композицию. Смешивать самодостаточные и
|
||
legacy-сцены в одном пакете также нельзя.
|
||
|
||
После инстанцирования host сохраняет присутствующее визуальное дерево самодостаточной сцены в том
|
||
виде, в каком оно собрано в Theme Builder. Он не назначает другой `Theme`, `ThemeTypeVariation`,
|
||
размеры, отступы или локальные theme overrides. Разрешены функциональные операции через
|
||
binding-id: подключение сигналов, обновление данных, видимости, доступности и состояния. Если
|
||
обязательного binding-id нет, host сначала принимает совместимый узел старой темы по закреплённому
|
||
`FallbackPath`, а при отсутствии самого узла переносит на его место готовый элемент встроенной
|
||
fallback-сцены с её оформлением. Внешние элементы, у которых binding-id присутствует, не меняются.
|
||
|
||
Для управления видимостью Home приложение использует готовые bindings
|
||
`home.scroll.content.stock_status`, `home.scroll.content.advertisement_panel` и необязательный
|
||
`home.decoration.custom_text_block`. Последний объединяет theme-owned фон пользовательской
|
||
речевой плашки и персонажа справа; для старых тем без этой группы сохранён fallback
|
||
`home.decoration.right_character`. Экран Settings предоставляет готовую
|
||
панель `settings.scroll.content.menu_visibility_panel` и три `CheckButton` с окончаниями
|
||
`show_stock_status`, legacy `show_right_character` (пользовательский текст и персонаж) и
|
||
`show_promotion_block`. Host только читает состояния
|
||
переключателей и меняет `Visible` соответствующих готовых элементов.
|
||
|
||
Фиксированный футер Settings задаётся supplemental bindings `settings.footer` и
|
||
`settings.footer.save_and_exit_button`; он является корневым соседом `SafeMargin`, а не потомком
|
||
`settings.scroll`. В конец прокручиваемого `Content` добавляется
|
||
`settings.scroll.content.footer_clearance`, чтобы нижние поля не перекрывались футером. Все три
|
||
узла обязательны для новых тем; в уже выпущенной теме без этих ID host переносит готовый футер и
|
||
зазор встроенной темы, не переоформляя остальные элементы DLC. Старый полноэкранный Scroll при
|
||
этом может проходить под непрозрачным футером; `footer_clearance` позволяет прокрутить последние
|
||
поля выше него без изменения геометрии выпущенной сцены.
|
||
|
||
Сервисный переключатель учёта использует supplemental binding
|
||
`settings.scroll.content.service_inventory_panel.margin.content.inventory_enabled` типа
|
||
`CheckButton`; независимый переключатель ограничения покупки —
|
||
`settings.scroll.content.service_inventory_panel.margin.content.purchase_limit_enabled` того же
|
||
типа. Изменяемое пояснение рядом с ними —
|
||
`settings.scroll.content.service_inventory_panel.margin.content.description` типа `Label`.
|
||
При выключении host меняет только функциональные `Visible`, `Disabled`, `ButtonPressed` и текст
|
||
готовых элементов этой панели. Старые PCK без metadata получают совместимые узлы по каноническим
|
||
fallback-путям; закреплённый JSON-каталог остаётся на 481 binding.
|
||
|
||
На всех пользовательских экранах, кроме Home и Settings, предусмотрена готовая кнопка
|
||
`home_navigation_button` с изображением дома. Его основа — `KeypadButton`; в `space-pixel` этот
|
||
вариант использует готовый `assets/ui/button_square.png`. На CashPayment, CardAmount, CardCustomAmount и
|
||
CardTerminal она расположена рядом с `BackButton`; на Success, Error и ServiceAccess занимает
|
||
обычную верхнюю левую позицию. Host только подписывается на `pressed`: незавершённую оплату он
|
||
безопасно отменяет, а на Success до завершения фактической выдачи кнопка недоступна. Новые темы
|
||
обязаны объявить семь соответствующих supplemental ID; старые PCK получают готовые fallback-узлы
|
||
из встроенных сцен.
|
||
|
||
Тексты нижних анимированных плашек с НЛО на CashPayment, CardAmount, CardCustomAmount и
|
||
CardTerminal настраиваются в разделе «Общие» через готовую панель темы
|
||
`settings.scroll.content.animated_banner_texts_panel`. Текст CashPayment выводится в
|
||
`cash_payment.content.banner_text`; для таймера CardTerminal в шаблоне нужно оставить
|
||
`{seconds}`, которое host заменяет количеством оставшихся секунд. Эти два новых supplemental ID
|
||
не изменяют immutable JSON-каталог; старой DLC-теме host подставляет встроенные узлы.
|
||
|
||
`VisualSlots` — необязательный словарь именованных `PackedScene`. Поддерживаемые host-слоты:
|
||
|
||
| Slot | Область preview |
|
||
|---|---|
|
||
| `home.top` | НЛО и реплика на главном экране |
|
||
| `home.advertisement` | тематическая рамка промо-блока |
|
||
| `cash.instructions` | купюра и монета |
|
||
| `cash.tip` | маскот и предупреждение о сдаче |
|
||
| `card.terminal` | анимация карты у терминала |
|
||
| `result.success` | анимация успешной выдачи |
|
||
| `result.error` | анимация ошибки |
|
||
|
||
Сцены слотов располагаются в нативной системе координат 720×1280 либо в локальном размере
|
||
соответствующего контейнера. Они не заменяют интерактивные узлы приложения: суммы, состояния,
|
||
кнопки и переходы всегда принадлежат host.
|
||
|
||
## Полносценовый контракт v2
|
||
|
||
Приложение всегда инстанцирует собственный C#-контроллер экрана, содержащий бизнес-логику,
|
||
навигацию и обращения к оборудованию. До входа контроллера в SceneTree его встроенный визуальный
|
||
корень `SafeMargin` заменяется сценой из DLC. Поэтому тема полностью определяет композицию и
|
||
вложенность `Control`, но не получает доступ к оплате, COM-порту или состоянию сессии.
|
||
|
||
Имена файлов, ключи `ScreenScenes` и имена корневых узлов фиксированы:
|
||
|
||
| Scene binding | Файл | Имя root |
|
||
|---|---|---|
|
||
| `screen.home` | `screen_home.tscn` | `ThemeScreen_Home` |
|
||
| `screen.cash_payment` | `screen_cash_payment.tscn` | `ThemeScreen_CashPayment` |
|
||
| `screen.card_amount` | `screen_card_amount.tscn` | `ThemeScreen_CardAmount` |
|
||
| `screen.card_custom_amount` | `screen_card_custom_amount.tscn` | `ThemeScreen_CardCustomAmount` |
|
||
| `screen.card_terminal` | `screen_card_terminal.tscn` | `ThemeScreen_CardTerminal` |
|
||
| `screen.success` | `screen_success.tscn` | `ThemeScreen_Success` |
|
||
| `screen.error` | `screen_error.tscn` | `ThemeScreen_Error` |
|
||
| `screen.service_access` | `screen_service_access.tscn` | `ThemeScreen_ServiceAccess` |
|
||
| `screen.settings` | `screen_settings.tscn` | `ThemeScreen_Settings` |
|
||
|
||
Root каждой сцены — `Control` с metadata `exchanger_screen_binding_id`, равной ключу из таблицы.
|
||
Интерактивные и динамические узлы получают уникальную metadata `exchanger_binding_id`.
|
||
Пример кнопки наличной оплаты:
|
||
|
||
```text
|
||
exchanger_binding_id = "home.scroll.content.payment_panel.margin.content.buttons.cash_button"
|
||
```
|
||
|
||
Binding-id вычисляется из стабильного fallback NodePath: префикс `SafeMargin/` удаляется,
|
||
каждый оставшийся сегмент переводится из PascalCase в snake_case, перед ним добавляется ключ
|
||
экрана. Фактическое положение узла в тематической сцене на binding-id не влияет. Точный
|
||
машиночитаемый каталог находится в `../ExchangerThemeDlcBuilder/contracts/screen_bindings.v2.json`,
|
||
а готовый шаблон — в `../ExchangerThemeDlcBuilder/themes/example/`. Preview-overlay является
|
||
инструментом Builder и не входит в production-сцены.
|
||
|
||
Manifest v2 обязан включать точную копию каталога в
|
||
`res://exchanger_theme_dlc/contracts/screen_bindings.v2.json`. Приложение закрепляет SHA-256
|
||
этого файла и проверяет девять экранов. Для присутствующих биндингов проверяются ID и Godot-тип;
|
||
неизвестные ID, дубли и неверные типы отклоняются. Отсутствующий обязательный ID не отключает весь
|
||
DLC: он разрешается через канонический `FallbackPath` из закреплённого каталога. Если точечная
|
||
подстановка невозможна, встроенная сцена используется только для проблемного экрана. Корневой
|
||
контракт сцены, self-contained композиция и подлинность каталога остаются строгими.
|
||
|
||
Все присутствующие визуальные узлы принадлежат теме. Приложение не переделывает их стили или
|
||
компоновку: оно находит узлы по binding-id, подключает сигналы и обновляет функциональные значения.
|
||
Единственное исключение — отсутствующий обязательный узел: для обратной совместимости на его
|
||
каноническое место переносится готовый элемент встроенной темы-заглушки.
|
||
|
||
Для итогов наличной оплаты предусмотрены три независимых обязательных `Label`:
|
||
|
||
- `cash_payment.content.summary.margin.values.balance` получает только внесённую сумму в рублях;
|
||
- `cash_payment.content.summary.margin.values.tokens` получает базовое количество жетонов;
|
||
- `cash_payment.content.summary.margin.values.bonus` получает бонус в формате
|
||
`+ N ЖЕТОНОВ\nВ ПОДАРОК` либо `БЕЗ БОНУСА`.
|
||
|
||
Контроллер вычисляет жетоны через общую `PricingPolicy`, заполняет все три узла и не объединяет
|
||
значения в одну строку. Геометрия, перенос строк, выравнивание и оформление остаются частью темы.
|
||
|
||
Панель «К ВЫДАЧЕ» на экране готовых карточных сумм использует существующий обязательный Label
|
||
`card_amount.content.reward_panel.content.value` только для базового количества жетонов. Бонус
|
||
выводится отдельно через supplemental runtime binding
|
||
`card_amount.content.reward_panel.content.bonus` типа `Label` с каноническим fallback-путём
|
||
`SafeMargin/Content/RewardPanel/Content/Bonus`. Формат совпадает с наличной оплатой:
|
||
`+ N ЖЕТОНОВ\nВ ПОДАРОК` либо `БЕЗ БОНУСА`. Supplemental ID не входит в закреплённый JSON-каталог
|
||
481 binding; старая тема получает готовый встроенный Label побиндинговым fallback.
|
||
|
||
На экране ручной карточной суммы введённое число выводится отдельно через обязательный Label
|
||
`card_custom_amount.content.keypad_panel.content.input_value`. Верхние Label
|
||
`...amount_panel.margin.values.amount` и `...tokens` получают соответственно базовые и бонусные
|
||
жетоны; второй Label использует тот же двухстрочный формат либо текст `БЕЗ БОНУСА`. Таким образом
|
||
верхняя панель не содержит рублей, а поле под «ВВЕДИТЕ СУММУ» всегда отражает фактически набранное
|
||
значение.
|
||
|
||
Изменяемые списки представлены заранее созданными фиксированными слотами: 16 кнопок сумм,
|
||
16 правил дополнительных жетонов, 16 сумм оплаты и 12 промо-роликов. Неиспользуемые слоты
|
||
скрыты. Home аналогично содержит готовые узлы состояния запаса, связи, постера, видео и
|
||
резервного текста. Благодаря этому внешний вид полной актуальной темы совпадает с предпросмотром;
|
||
дерево дополняется только при отсутствии обязательного binding в старом пакете.
|
||
|
||
Строка запаса Home состоит из готовых узлов темы: текстового состояния слева, полоски по центру
|
||
и Label приблизительного общего остатка справа. Для последнего используется supplemental
|
||
runtime binding home.scroll.content.stock_status.approximate_count; приложение меняет только
|
||
его Text и скрывает Label при отключённом учёте. В Settings, напротив, сервисная панель получает
|
||
точное учётное количество, а при отключённом учёте скрывает остатки, пересчёт и пополнение.
|
||
|
||
Экран Settings дополнительно содержит две готовые панели `KeyboardOverlay`: текстовую и цифровую.
|
||
Они дают 57 обязательных биндингов: 33 символьные клавиши, 10 цифр, две кнопки скрытия и
|
||
12 служебных действий. Панели по умолчанию скрыты, все их кнопки имеют `focus_mode = None`,
|
||
чтобы нажатие не отнимало каретку у редактируемого поля. Host подключает `Pressed` и меняет только
|
||
`Visible`, `Text`, `Disabled` и `ButtonPressed`; создание, перестановка и стилизация узлов runtime
|
||
запрещены. В preview Builder предусмотрены состояния `KEYBOARD: OFF/TEXT/NUM`.
|
||
|
||
## Семантический UI-контракт
|
||
|
||
Runtime применяет один `Theme` ко всем девяти экранам, включая `ServiceAccess` и `Settings`.
|
||
Тема может оформлять следующие стабильные type variations:
|
||
|
||
| Variation | Базовый Godot-тип | Назначение |
|
||
|---|---|---|
|
||
| `ScreenTitle` | `Label` | главный заголовок экрана |
|
||
| `ScreenDescription` | `Label` | пояснение под заголовком |
|
||
| `PanelTitle` | `Label` | заголовок панели |
|
||
| `PanelText` | `Label` | крупное содержимое пользовательской панели |
|
||
| `FieldLabel` | `Label` | подпись или обычный текст поля настроек |
|
||
| `SupportText` | `Label` | подсказка и вторичный текст |
|
||
| `StatusText` | `Label` | текущее состояние операции или оборудования |
|
||
| `WarningText` | `Label` | предупреждение |
|
||
| `ErrorText` | `Label` | ошибка валидации или операции |
|
||
| `PrimaryButton` | `Button` | главное действие |
|
||
| `CompactButton` | `Button` | цифровая клавиша или компактное действие |
|
||
| `BackButton` | `Button` | компактный возврат с пользовательского экрана |
|
||
| `SectionTabButton` | `Button` | вкладка сервисных настроек |
|
||
| `SecondaryButton` | `Button` | отмена, закрытие или сброс |
|
||
| `DangerButton` | `Button` | удаление/необратимое действие |
|
||
| `WhitePanel` | `PanelContainer` | основная смысловая панель |
|
||
| `AdvertisementPanel` | `PanelContainer` | контейнер постера, текста или промо-ролика OGV/MP4/MOV/M4V |
|
||
| `FooterPanel` | `PanelContainer` | техподдержка и служебный footer Home |
|
||
| `StatusPanel` | `PanelContainer` | компактный индикатор связи/состояния |
|
||
|
||
Host всегда регистрирует базовый тип каждой variation. Поэтому старый корректный DLC v1,
|
||
в котором новый семантический тип ещё не оформлен, наследует безопасный стандартный стиль и
|
||
остаётся совместимым.
|
||
|
||
Экран настроек дополнительно использует базовые классы `LineEdit`, `SpinBox`, `OptionButton`,
|
||
`CheckButton`, `HSlider`, `VScrollBar` и `HScrollBar`. Полноценная тема должна явно задавать их
|
||
normal/focus/pressed/disabled-состояния и сохранять читаемость внутри `WhitePanel`.
|
||
|
||
## Доверенное выполнение и fallback
|
||
|
||
- Пакет монтируется с `replace_files=false` и не может заменить ресурсы Exchanger.
|
||
- Все пути manifest обязаны находиться внутри `res://exchanger_theme_dlc/`; `..`, обратные слеши и ссылки на сцены приложения запрещены.
|
||
- Decoration, visual slots и полносценовые view могут содержать GDScript и любые стандартные
|
||
runtime-узлы Godot. Код начинает исполняться при инстанцировании сцены.
|
||
- Проверяются формат, версия, ThemeId, цвет, расширения, существование и тип каждого ресурса.
|
||
- Для manifest v2 проверяются закреплённый каталог биндингов, namespace, дубли и типы присутствующих
|
||
узлов. Неизвестный либо неверно типизированный биндинг отключает DLC; отсутствующий обязательный
|
||
биндинг восстанавливается по `FallbackPath` встроенным элементом.
|
||
- Самодостаточная сцена дополнительно проверяется на собственный `Theme`, текстурированный
|
||
`ThemeBackground` и `BackgroundDecoration`; смешанный режим девяти сцен и одновременное
|
||
объявление глобального фона или decoration запрещены.
|
||
- При отсутствии, повреждении или несовместимости DLC приложение продолжает запуск со встроенной fallback-темой.
|
||
- В журнал не записывается полный пользовательский путь к DLC.
|
||
- Одновременно загружается не более одного пакета. После замены `.pck` приложение нужно перезапустить.
|
||
|
||
## Runtime-возможности темы
|
||
|
||
В legacy-композиции `BackgroundTexturePath` задаёт общий фон, а `BackgroundDecorationPath` может
|
||
ссылаться на произвольную доверенную `PackedScene`. В самодостаточной композиции те же роли
|
||
выполняют `ThemeBackground` и `BackgroundDecoration` внутри каждой сцены.
|
||
|
||
Разрешены GDScript, сигналы, `Timer`, `Tween`, `AnimatedSprite2D`, `AnimationPlayer`, method tracks,
|
||
аудио, видео, частицы, шейдеры, процедурная графика, сетевые и файловые API Godot. Host не
|
||
применяет лимиты количества узлов, кадров, FPS или скорости анимации и не ограничивает размер PCK.
|
||
|
||
При `ReducedEffects=true` глобальная decoration не создаётся, а узел `BackgroundDecoration`
|
||
самодостаточного экрана скрывается и перестаёт обрабатываться. Остальная скриптовая логика сцены
|
||
продолжает работать; тема при желании может самостоятельно учитывать эту настройку.
|
||
|
||
## Разработка и сборка
|
||
|
||
Откройте соседний `ExchangerThemeDlcBuilder/project.godot` в Godot 4.7.1. После редактирования выполните:
|
||
|
||
```powershell
|
||
powershell -ExecutionPolicy Bypass -File .\build-theme.ps1 -ThemeId space-pixel
|
||
```
|
||
|
||
Пакет появится в `build/space-pixel.pck`; совместимая копия будет записана в
|
||
`build/active_theme.pck`. Для одновременной установки в стандартный пользовательский путь:
|
||
|
||
```powershell
|
||
powershell -ExecutionPolicy Bypass -File .\build-theme.ps1 -ThemeId space-pixel -InstallToDefaultUserPath
|
||
```
|
||
|
||
Сборщик проверяет, что запущен настоящий Godot 4.7, а не MCP-сервер `godot-ai`. Сборка DLC не требует C#-кода или ссылки на проект Exchanger.
|