exchanger-themes/docs/SCRIPTING.md
Danil Iarosh ca52efbedc Initial commit: Exchanger Theme DLC Builder
Godot 4.7 project for authoring, previewing, validating, and building Exchanger PCK themes. Includes the example and space-pixel themes, binding contracts, editor tooling, validation/build scripts, and project documentation.
2026-09-22 11:43:13 +03:00

281 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Скрипты и runtime-логика темы
Темы Exchanger считаются доверенными. Любая production-сцена или вложенный компонент внутри
`themes/<theme-id>/package/` может содержать GDScript и стандартные runtime-узлы Godot.
## Где хранить скрипты
Рекомендуемая структура:
```text
themes/<theme-id>/package/
├── scripts/ GDScript темы
├── components/ переиспользуемые сцены со скриптами
├── scenes/ девять обязательных экранов
└── background_decoration.tscn
```
Прикрепляйте скрипт к узлу обычным действием Godot `Attach Script`. Путь должен находиться внутри
`package/`, например:
```text
res://themes/space-pixel/package/scripts/alien_controller.gd
```
В текстовых `.tscn` зависимости храните относительным путём, например
`../scripts/alien_controller.gd` из `scenes/` или `scripts/alien_controller.gd` из корня package.
Во время сборки package переносится в `res://exchanger_theme_dlc/`, поэтому жёсткая builder-ссылка
`res://themes/<id>/package/...` не является переносимой. Build-workspace автоматически заменяет
известный package-префикс на runtime namespace, если Godot Editor всё же пересохранил такой путь,
но относительные ссылки остаются предпочтительными и понятнее при переносе темы между проектами.
## Что разрешено
- `_ready()`, `_process()` и `_physics_process()`;
- собственные сигналы и подключения сигналов Godot;
- `Tween`, `AnimationPlayer`, `AnimatedSprite2D` и method tracks;
- `Timer`, аудиоплееры, видео, частицы, шейдеры и процедурная графика;
- загрузка ресурсов темы, ввод и остальные API Godot, доступные приложению;
- сетевые и файловые операции стандартными API Godot.
Builder больше не накладывает лимит на количество узлов, кадров, `AnimationPlayer`, FPS или
`speed_scale`. Exchanger также не применяет decoration budget.
## Жизненный цикл
Preview инстанцирует те же production-сцены, которые затем загружает Exchanger. Поэтому скрипт
работает в обоих окружениях без отдельной preview-копии. Изменения `.gd`, `.gdshader`, сцен,
ресурсов, изображений, шрифтов, аудио и OGV отслеживаются live reload preview.
При проверке контракта PackedScene предварительно инстанцируется, чтобы найти binding metadata.
Из-за этого код `_init()` может выполниться ещё до реального показа экрана. Логику, которой нужен
SceneTree, autoload или готовые дочерние узлы, размещайте в `_ready()`. Освобождайте сигналы и
внешние ссылки в `_exit_tree()`, если скрипт подключается к объектам вне своей сцены.
Экран создаётся заново при переходе к нему, если так работает текущий host-flow. Не храните
важное постоянное состояние только в полях узла темы: для него нужен app-owned сервис либо явно
выбранное внешнее хранилище.
## Взаимодействие с Exchanger
Exchanger продолжает подключать оплату, навигацию, настройки и оборудование к 481 обязательному
узлам через `exchanger_binding_id`. Скрипты темы могут параллельно подключаться к сигналам своих
узлов и менять своё дерево.
Прямые пути вроде `/root/ThemeManager` или внутренние пути C#-контроллеров технически доступны,
но не являются стабильным контрактом темы. При такой интеграции фиксируйте требуемую версию
Exchanger в документации темы.
## Пример
В шаблоне находится скрипт:
```text
themes/example/package/scripts/decoration_controller.gd
```
Он прикреплён к `background_decoration.tscn`, экспортирует параметры и показывает простую
анимацию через `_process()`. Свойство `animation_enabled` выключено по умолчанию; добавьте в
decoration дочерний спрайт и включите его, чтобы увидеть движение.
В `space-pixel` скрипт `scripts/ufo_flight_controller.gd` управляет независимыми Tween-пролётами
двух `AnimatedSprite2D`. Он не вмешивается в их покадровое воспроизведение: после каждого прохода
спрайт разворачивается, выбирает новую случайную высоту и возвращается с противоположной стороны.
Отдельный `scripts/home_ufo_bobbing.gd` прикреплён к `visuals/home_top.tscn` и добавляет двум
референсным `TextureRect` лёгкое независимое вертикальное покачивание с округлением позиции до
целого пикселя.
Скрипт `scripts/stable_banner_animation.gd` прикреплён к двухкадровым `Banner` экранов Cash,
CardAmount, CardCustomAmount и CardTerminal и компенсирует различие видимых границ PNG через
frame-dependent scale/position, не заменяя и не отключая исходные кадры.
### Нейтральный контракт интерактивного питомца
Каждая тема может предоставить собственного питомца через необязательное поле manifest
`InteractivePetScene`. Рекомендуемый runtime-путь —
`res://exchanger_theme_dlc/components/interactive_pet.tscn`, однако сам manifest является
источником истины. Корень сцены наследует `Node2D`; анимационный компонент сам не меняет позицию
root, потому что перемещением управляет host. Он предоставляет методы `play_pet_animation()`, `pause_pet_animation()`,
`stop_pet_animation()`, `get_pet_animation_names()`, `get_pet_animation_frame_count()`,
`has_pet_animation()`, `get_current_pet_animation()` и `get_current_pet_frame()`. Сигналы
`pet_animation_started` и `pet_animation_completed` позволяют приложению строить интерактивное
поведение поверх визуального компонента.
Viewer дополнительно использует необязательные методы `is_pet_animation_looping()`,
`get_pet_animation_frames_per_second()` и `get_pet_preview_size()`. Без них базовые ручное
переключение, пауза и повтор работают, но viewer не обязан знать длительность цикла или исходный
размер сцены. Список кнопок всегда строится из `get_pet_animation_names()` и не фиксирует имена
или количество состояний.
Тема также может предоставить пару read-only методов `get_pet_animation_visual_offset(animation_name)`
и `get_pet_visual_offset()`. Если присутствует один из них, verifier требует оба. Первый сообщает
локальную поправку каждой анимации, второй — текущую; viewer использует объединённую область всех
объявленных поправок, чтобы вписать состояния разной высоты в stage без clipping.
Для runtime-поведения компонент может дополнительно предоставить методы:
```gdscript
get_pet_action_animation(action_name: StringName) -> StringName
get_pet_runtime_scale() -> float
get_pet_walk_speed() -> float
get_pet_climb_speed() -> float
get_pet_hit_size() -> Vector2
get_pet_ground_offset() -> Vector2
```
Все они необязательны для обратной совместимости. `get_pet_action_animation()` сопоставляет
нейтральные действия host с фактическими именами клипов конкретной темы. Для полного поведения
используются действия `idle`, `sit`, `interact`, `celebrate`, `walk_left`, `walk_right`,
`walk_start_left`, `walk_start_right`, `walk_finish_left`, `walk_finish_right`, `climb_start`,
`climb_up`, `climb_down`, `climb_finish`, `drag`, `drop` и `landing`.
`hanging` активно только во время удержания питомца. `falling` проигрывается во время падения
после отпускания и при падении со сохранённой позиции после смены экрана. После контакта с
опорой host один раз проигрывает прямую анимацию `landing`, затем переходит в `idle`; старые темы,
у которых `landing` отсутствует в списке анимаций, сохраняют fallback в `idle`. Запрос `dancing`,
поступивший во время `landing`, ставится в очередь до завершения приземления, после чего host явно
перезапускает праздничную анимацию четыре раза. Масштаб и обе скорости задаются положительными конечными числами;
`hit_size` — локальная область захвата пальцем/ЛКМ, а `ground_offset` — локальная точка контакта
лап с поверхностью. Host размещает root в `contact_point - ground_offset * runtime_scale`.
После завершённой ходьбы актуальный Exchanger выбирает состояние простоя вероятностно: с
вероятностью `35%` запускает `idle`, а с вероятностью `65%` — `sitting`. `idle` удобно
использовать для активного ожидания стоя, а `sitting` — для спокойной посадки персонажа.
`sitting` остаётся необязательной: если её нет в списке анимаций, host использует `idle`, поэтому
ранее собранные темы продолжают работать без изменений.
### Составные действия питомца
Если вы собираете последовательность со стороны дизайна, начните с отдельного пошагового гайда
[COMPOSITE_PET_ANIMATIONS.md](COMPOSITE_PET_ANIMATIONS.md). Ниже приведён точный технический
контракт между темой и Exchanger.
Уникальную последовательность конкретного вида питомца можно полностью оставить в теме. Для
этого компонент реализует весь необязательный контракт целиком:
```gdscript
signal pet_composite_action_started(action_name: StringName, run_id: int)
signal pet_composite_action_finished(action_name: StringName, run_id: int, completed: bool)
has_pet_composite_action(action_name: StringName) -> bool
start_pet_composite_action(action_name: StringName, runtime: Object, parameters: Dictionary) -> int
cancel_pet_composite_action(run_id: int, reason: StringName) -> void
```
Частичный набор методов или сигналов является ошибкой валидатора. `start_*` возвращает ровно
`runtime.GetRunId()` при принятии запуска и `0` при отказе. Тема испускает `started` после
принятия, а `finished(..., true)` — только после полного завершения. При отмене она прекращает
корутину, сбрасывает локальные визуальные преобразования и больше не вызывает переданный runtime.
Host также инвалидирует runtime при смене экрана, выгрузке темы, drag или конфликтующем действии,
поэтому после каждого `await` нужно проверять `runtime.IsActive()`.
Переданный scoped runtime предоставляет строго ограниченный интерфейс ко внешнему движению:
```text
GetRunId() -> int
IsActive() -> bool
GetViewportRect() -> Rect2
GetAnchor() -> Vector2
GetHighestSupport() -> Dictionary
SetMotionPolicy(policy: Dictionary) -> bool
SetAnchor(anchor: Vector2) -> Dictionary
MoveAnchor(delta: Vector2) -> Dictionary
LandOnSupport(surface_id: String) -> Dictionary
```
`anchor` — экранная точка контакта питомца, а не позиция его root. `GetHighestSupport()` возвращает
`accepted`, `valid`, `surface_id` и `anchor`; верхней считается доступная поверхность с наименьшим
Y, а при равенстве выбирается ближайшая по X. Результат `MoveAnchor()` содержит `accepted`,
фактический `anchor`, `wrapped`, `landed` и `surface_id`. `LandOnSupport()` выполняет явную
финальную привязку к существующей доступной поверхности.
Поддерживаемые поля `SetMotionPolicy()`:
- `navigation`: `suspended` запрещает обычную app-owned навигацию, `theme_driven` разрешает теме
перемещать anchor;
- `support_contacts`: `ignore`, `solid` или `after_vertical_wrap`;
- `screen_bounds`: `clamp`, `allow` или `wrap_vertical_once`;
- `input_enabled`: разрешает или запрещает drag/tap во время составного действия;
- `landing_surface_id`: целевая опора для контактов после wrap;
- `wrap_margin`: внешний вертикальный запас в пикселях.
Это политика host-навигации, а не доступ к внутренним узлам приложения. Тема определяет порядок,
число повторов, визуальный scale и момент смены политики; приложение сохраняет экранную позицию,
проверяет границы/опоры и автоматически отзывает полномочия старого запуска. Неизвестные или старые
темы без этого API продолжают использовать одиночную semantic-анимацию `interact`.
Маршрут принадлежит не персонажу, а конкретному экрану темы. Поэтому каждая production-сцена,
кроме Settings, объявляет собственный невидимый граф поверхностей и вертикальных переходов через
v1 metadata, описанные в
[BINDING_CONTRACT.md](BINDING_CONTRACT.md). Горизонтальные отрезки могут совпадать с полом,
верхними гранями кнопок и окон; отдельные sit points разрешают питомцу останавливаться и сидеть.
Climb edges связывают поверхности и задают, у какого левого/правого края разрешён подъём. Host не
вычисляет эти места по NodePath и не меняет визуальные `Control` темы. При смене экрана он
сохраняет текущую экранную позицию, включает граф новой сцены и проигрывает `falling` до её default
surface, затем `landing` и `idle`; Settings скрывает питомца, не сбрасывая сохранённую позицию.
Текстуры, SpriteFrames и внутренняя иерархия не входят в публичный контракт. Сцена может ссылаться
на изображения из любой подпапки собственного `package/`; переименовывать каталог в `alien_pet`
не требуется. Важно лишь, чтобы все зависимости оставались внутри package и использовали
переносимые относительные либо канонические runtime-ссылки.
Реализация `space-pixel` находится в `components/interactive_pet.tscn`; её скрипт
`scripts/interactive_pet.gd` собирает 22 sprite-sheet и предоставляет следующий собственный набор
анимаций:
```text
action, climbing_action, climbing_action_start, climbing_action_stop,
climbing_down, climbing_start, climbing_up, come_closer, dancing, falling, hanging, idle,
landing, sitting, turn_left, turn_right,
walking_left, walking_left_start, walking_left_stop, walking_right,
walking_start_right, walking_right_stop
```
Через `get_pet_action_animation()` тема сопоставляет эти клипы нейтральным действиям host: `idle`
и `sitting` для ожидания, `action` для обычного действия, `dancing` для выдачи, парные
walking-анимации для ходьбы,
`climbing_start/up/down` для подъёма, `hanging` при удержании/перетаскивании, `falling` при
падении и `landing` для одноразового перехода к `idle` после обычной посадки. Актуальный host
после подъёма или спуска
сразу продолжает маршрут и не запрашивает `climb_finish`. Runtime-профиль рассчитан на сцены
`720×1280`: scale `0.845`, walk speed `88 px/s`, climb speed `72 px/s`, hit size `128×128`,
ground offset `(64,96)`. Для `sitting` визуальная поправка равна `(0,24)`, для каждой другой
анимации — `(0,0)`; root и `GROUND_OFFSET` при переключении не меняются. Поправка `sitting`
опускает только это состояние на `20,28` экранного пикселя при scale `0.845`. Прежняя поправка
`(0,-24)` у non-sitting состояний остаётся удалённой.
`come_closer`, `landing`, `turn_left`, `turn_right`, `climbing_action_start` и
`climbing_action_stop` являются one-shot-переходами. `idle`, `action` и `climbing_action`
зациклены. `space-pixel` предоставляет составной `interact`: учитывает последнее направление,
проигрывает `turn_left`/`turn_right`, при `come_closer` увеличивает `VisualRoot` до ×2, повторяет
`action`, вычисляет число циклов `falling` по полной длине пути, проходит через нижнюю и верхнюю
границы без контакта с промежуточными опорами, садится на самую высокую доступную поверхность,
проигрывает `landing` и возвращается в `idle`. При `reduced_effects=true` приближение и проход
через границу пропускаются, но действие, посадка и возврат в idle сохраняются.
Стартовые и завершающие состояния проигрываются один раз; остальные состояния зациклены. Экспорт
`return_to_sitting_after_one_shot` при необходимости автоматически возвращает питомца к `sitting`.
Все состояния можно проверить в отдельной сцене `tools/preview/interactive_pet_preview.tscn` или открыть
кнопкой `PET` в верхней панели основной галереи. Просмотрщик вписывает объединённую область всех
объявленных per-animation visual offsets в Stage; это только tooling и не меняет production-компонент.
Кнопка «СОСТАВНОЕ ДЕЙСТВИЕ: INTERACT» передаёт компоненту тестовый runtime с viewport и верхней
опорой, поэтому полный theme-owned сценарий можно проверить без запуска Exchanger.
## Проверка и сборка
```powershell
.\tools\Test-PackagedBindingContract.ps1
.\test-theme.ps1 -ThemeId <theme-id>
.\build-theme.ps1 -ThemeId <theme-id>
```
`test-theme.ps1` импортирует скрипты в изолированном Godot-проекте и обнаруживает parser/resource
errors. Для проверки `_ready()`, сигналов и runtime-поведения дополнительно запустите preview, а
после сборки — Exchanger с установленным PCK.
Готовый PCK можно отдельно смонтировать, инстанцировать все девять экранов и проверить наличие
прикреплённых скриптов:
```powershell
godot --headless --path . --script res://tools/verify_theme_pck.gd -- `
--pack (Resolve-Path .\build\example.pck) --require-scripts
```