some changes

This commit is contained in:
Oleksandr Vlasiuk
2026-08-17 12:36:56 +03:00
parent 1f72420da0
commit 1d38990d7f
24 changed files with 3498 additions and 674 deletions
+245 -203
View File
@@ -1,243 +1,285 @@
# MyGamePlayable — контекст проєкту
# MyGamePlayable — Project Instructions
Playable ad на three.js + пропрієтарний SDK HitPlay. Цей файл вантажиться в
кожну сесію — тримай його **інструкцією, а не щоденником**.
## Context rules
This file contains persistent project rules and non-obvious constraints, not history.
**Keep this file under 200 lines.**
Add only:
* rules that affect future implementation decisions;
* non-obvious SDK/library/toolchain pitfalls;
* structural conventions;
* public SDK APIs that are not obvious from code.
Do not add:
* development history, dates, or completed tasks;
* test/build results;
* current tuning values;
* information obvious from nearby code;
* temporary project state.
If knowledge is specific to one subsystem, prefer `docs/<topic>.md`.
If it explains why a decision was made, put the detailed history in `docs/worklog/`.
When a pitfall is fixed by a utility, replace the explanation with one short rule
pointing to that utility.
Worklog files:
`docs/worklog/NN-short-english-name.md`
Update `docs/worklog/README.md` when adding one.
---
## ПРАВИЛА ВЕДЕННЯ ЦЬОГО ФАЙЛУ
## Stack
**Стеля — 300 рядків.** Перевищив — перенеси найстаріше й уже усталене в
`docs/worklog/`. Історія розробки живе там, не тут.
* Three.js `0.185` via `@hitplay/playable_template` + `@hitplay/ads_common`.
* Physics: `cannon-es`.
* Tweens: `@tweenjs/tween.js` via SDK `TweenC`.
* VFX: `three.quarks`, designer `.json` prefabs.
* Audio: `howler`.
* Input: `nipplejs`.
* Debug UI: `lil-gui`.
* Analytics: `bytebrew-web-sdk`.
* Toolchain: Vite 6 + TypeScript 7.
### Що ДОДАВАТИ сюди
### Runtime rules
- правило або конвенцію, що змінює майбутні рішення («використовуй утиліту X,
не пиши формулу заново»);
- граблі, які **вкусять ще раз** (поведінка SDK/бібліотеки/тулчейна, а не
разовий баг у власному коді);
- зміну структури: новий контролер, нова утиліта, новий конфіг-параметр;
- нову команду або публічний API SDK, який не очевидний з коду.
### Що НЕ додавати
- «сьогодні зробив…», «Day N», хронологію, цитати з чату;
- «перевірено в браузері / білд пройшов» — це не знання, це подія;
- підібрані числа (`CAMERA_OFFSET`, тривалості, відсотки) — вони живуть у коді
й там же змінюються; сюди — тільки *де* важіль, не його поточне значення;
- переказ того, що й так видно з коду за 10 секунд.
### Куди що писати
| Знання | Місце |
|---|---|
| Пояснює один файл/рядок | коментар у коді |
| Перетинає кілька файлів або є політикою проєкту | `CLAUDE.md` |
| «Чому ми дійшли саме до цього» | `docs/worklog/` |
### Коли граблі усунені в корені
Якщо гочтя закрита утилітою — **заміни розповідь одним рядком** з посиланням на
утиліту. Приклад: `.chain()`-баг → `src/utils/tweenChain.ts`. Довга версія
лишається в `docs/worklog/02-day6-tween-animations.md`.
### Новий запис у worklog
`docs/worklog/NN-коротка-англійська-назва.md`, `NN` — наступний номер.
Додай рядок у `docs/worklog/README.md`. Один H1 на файл.
* `UpdateController.Instance.onUpdate` is the only update event. `delta` is seconds.
* SDK calls `world.fixedStep()` every frame via `Physics_internal.update`.
Never call `step()` / `fixedStep()` manually.
* Physics initialization: `Physics_internal.init(...)` in `beforeResourcesLoadedCb.ts`.
* Call `TweenC.init()` exactly once. It is already called in
`beforeResourcesLoadedCb.ts`.
* Use `ThreeC.removeFromScene(obj)` to remove spawned meshes.
---
## Стек
## Commands
- **three.js `0.185`** (НЕ Babylon), обгорнутий у SDK `@hitplay/playable_template`
+ `@hitplay/ads_common` (приватні пакети HitPlay).
- Фізика: `cannon-es`. **SDK сам кличе `world.fixedStep()` щокадру**
(`Physics_internal.update`) — свій `step()`/`fixedStep()` НЕ викликати.
Ініт — `Physics_internal.init(...)` у `beforeResourcesLoadedCb.ts`.
- Твіни: `@tweenjs/tween.js` через SDK-обгортку `TweenC`. **`TweenC.init()`
треба покликати один раз** (є в `beforeResourcesLoadedCb.ts`), інакше
`TweenC.add()` тихо нічого не анімує.
- VFX: `three.quarks` — дизайнерські префаби з quarks.art (`.json`).
- Звук `howler`, ввід `nipplejs`, дебаг-панель `lil-gui`, аналітика `bytebrew-web-sdk`.
- Тулчейн: Vite 6 + TypeScript 7.
| Command | Purpose |
| ---------------- | --------------------------------------------------------- |
| `npm start` | Vite dev server |
| `npm run export` | Single-file HTML → `export/output_export_browser_en.html` |
| `npm run build` | 24ADS platform zip → `zip/` |
| `npm run update` | Update HitPlay SDK packages |
`UpdateController.Instance.onUpdate` — **єдиний** update-івент, `delta` в секундах.
`ThreeC.removeFromScene(obj)` — видалення спавнених мешів.
Verify changes with `npx vite build`.
## Команди
Use `stats.html` when investigating bundle-size growth.
| Команда | Що робить |
|---|---|
| `npm start` | vite dev-сервер |
| `npm run export` | single-file HTML → `export/output_export_browser_en.html` |
| `npm run build` | zip для платформи 24ADS → `zip/` |
| `npm run update` | оновити `@hitplay/ads_common` + `playable_template` |
---
Перевірка змін — `npx vite build` (у проєкті прийнято верифікувати саме білдом).
`stats.html` — rollup-visualizer, дивитись коли розпухає бандл.
## Structure
## Структура
```
src/index.ts точка входу, Template.initConfig({...})
src/controllers/*C.ts одна система = один файл, суфікс C, плоский список
src/configUIParams/ категорії конфігу, які рендерить платформа 24ADS
src/templateConfig/*Cb.ts лайфсайкл-колбеки SDK
src/utils/ спільні формули (див. нижче)
src/resources/ meshes/images/fonts/vfx/sounds
src/css/ main.css, ui.css, health-bar.css
docs/worklog/ архів історії розробки
```text
src/index.ts entry point / Template.initConfig()
src/controllers/*C.ts one system per file, flat structure
src/configUIParams/ 24ADS config categories
src/templateConfig/*Cb.ts SDK lifecycle callbacks
src/utils/ shared utilities
src/resources/ meshes/images/fonts/VFX/sounds
src/css/ main.css, ui.css, health-bar.css
docs/worklog/ development history
```
Підпапки в `controllers/` **свідомо не зроблені** — SDK сам плаский, вигоди нуль.
Do not create controller subdirectories. The SDK uses a flat structure and
there is no benefit in mirroring artificial subfolders.
### Утиліти — використовувати, не переписувати
---
- `perspectiveScale.ts` — компенсація перспективи при польоті до/від камери.
- `screenWorldProjection.ts` — world↔screen в обидва боки.
- `tweenChain.ts` — коректна реєстрація ланцюжка твінів.
- `flashOverlayMaterial.ts` — адитивний флеш-оверлей.
## Utilities — reuse, don't reimplement
### DI-патерн
* `perspectiveScale.ts` — perspective compensation for flight toward/away from camera.
* `screenWorldProjection.ts` — world↔screen projection.
* `tweenChain.ts` — safe tween-chain registration.
* `flashOverlayMaterial.ts` — additive hit-flash overlay.
* `pickupFlight.ts` — shared drop → scatter → bounce → fly-to-target animation,
driven by both `ResourceC` (wood) and `ZombiePartC` (zombie-part chunks).
Never write a second one.
Контролери не імпортують одне одного напряму, коли це створить цикл — інжектиться
геттер/колбек, а зв'язує `TestSceneC`:
`ResourceC.setFlyTarget()`, `HudC.setBalanceGetter()`,
`CameraFollowC.setFacingTargetGetter()`, `BreakablePropC.onHit/onDestroyed`.
`HealthBarC` is shared by crates, zombies, and player. Do not create another
health-bar implementation.
## Конфіг платформи ↔ код
Two weapon progressions (bat level from wood, pistol level from zombie-part
chunks), each with its own fixed Pay Zone (`PayZoneC`, instance-based — no
relocation), and crate respawn: see `docs/progression.md`.
Платформа 24ADS рендерить ліву панель із категорій. Частина — наші
(`src/configUIParams/`), частина — стандартні від SDK.
`HealthBarOptions.variant` controls colors via `health-bar.css`.
`follow` re-samples the anchor every frame for moving owners.
Health bars appear on first hit and hide after `HIDE_DELAY_S`; full-HP entities
do not show a bar.
| Вкладка на платформі | Джерело |
|---|---|
| Analytics params | `analytics.ts` |
| Install Banner Settings | `installBanner.ts` |
| **Scene settings** | `globalSettings.ts` (id категорії — `global`) |
| Gameplay tuning | `gameplay.ts` |
| Audio Settings | `sounds.ts` |
| Screen Orientation | `orientation.ts` |
| Filter scene, Redirect Settings, Loader Settings, Font Settings | SDK, не наш код |
---
Читати значення в коді — `Template.getCategory("<id категорії>")["<id параметра>"]`
(приклад — `DebugConfigC.init()`).
## Dependency injection
**Scene settings** (`globalSettings.ts`): `light_intensity`, `light_color`,
`camera_fov_p/l`, `camera_position_p/l`, `camera_rotation_p/l`.
Controllers must not directly import each other when that would create a cycle.
Inject getters/callbacks instead and wire them from `TestSceneC`.
**Формат `configUIParams` ≠ формат рантайму.** У визначенні кожна вісь масивного
параметра — це `[min, max, default]`; у грі `Template.getCategory()` віддає
готовий `[x, y, z]`. Не переплутати: `[-20, 70, -6]` — це не координати.
Existing DI examples:
**Камера: рівно один писар на `camera.rotation` — `CameraFollowC`.**
`camera_position_*` — це офсет **відносно гравця** (не абсолютна позиція, камера
слідкує), `camera_rotation_*` — фіксований кут у градусах, `camera_fov_*` — FOV.
Розподіл власності: FOV застосовує `CameraC.setCamera()`, риг —
`CameraFollowC.applyRigFromConfig()`. Не повертати position/rotation у
`CameraC.setCamera()`: він перезапускається на кожну зміну будь-якого `camera_*`
поля, і два писарі на кут ламали камеру назавжди до кінця сесії.
`CAMERA_OFFSET` у `TestSceneC` лишився тільки як фолбек.
* `ResourceC.setFlyTarget()`
* `HudC.setBalanceGetter()`
* `CameraFollowC.setFacingTargetGetter()`
* `BreakablePropC.onHit / onDestroyed / onRespawned`
* `ZombieC.init(bounds, getPlayerPosition, damagePlayer, isPlayerAlive)`
* `ZombieInvasionHudC.setOnComplete()`
* `ZombiePartC.setFlyTarget()` / `PistolHudC.setLevelGetter()`
**Gameplay tuning** (`gameplay.ts`) дублює те, що дев-панель `DebugConfigC`
(клавіша `C`, тільки lil-gui, не потрапляє в білд) показує локально. Панель
сідить свої дефолти з цієї категорії при старті — один раз, без live-sync.
`ZombieInvasionHudC.setOnComplete()` must be called **before** `init()`;
`init()` immediately starts its countdown.
## Живі граблі
---
Кожна вже кусала. Довгі розбори — в `docs/worklog/`.
## Platform config
**glTF-завантажувач стирає крапки з імен нодів.** У сирому GLB `UI_Wood.001`, у
живій сцені — `UI_Wood001`. `getObjectByName()` тихо повертає `undefined`, гард
`if (node)` завжди хибний, код «виглядає правильним». Перевіряти реальне ім'я
через `scene.traverse`, не через GLB JSON.
24ADS renders config categories from `src/configUIParams/` plus SDK categories.
**Матеріали шаряться між усіма нодами з GLB.** Хочеш змінити матеріал одного
об'єкта — клонуй його один раз при білді, інакше зачепиш усі 18 крейтів.
| Platform tab | Source |
| --------------------------------------- | ------------------------------ |
| Analytics params | `analytics.ts` |
| Install Banner Settings | `installBanner.ts` |
| Scene settings | `globalSettings.ts` (`global`) |
| Gameplay tuning | `gameplay.ts` |
| Audio Settings | `sounds.ts` |
| Screen Orientation | `orientation.ts` |
| Filter scene / Redirect / Loader / Font | SDK |
**`material.color` на `MeshBasicMaterial` не дає флешу.** Матеріал unlit, колір
множиться на текстуру, дефолт уже білий — лерп «білий → білий» невидимий.
Для hit-flash — окремий адитивний оверлей-меш (`flashOverlayMaterial.ts`).
Read runtime values with:
**«Чи гравець у зоні X» — тільки через `Box3().setFromObject(node)`.** Радіус на
око зі скріншота не збігається з реальним футпринтом (і центр може бути
несиметричний відносно пивота).
**Перспектива сама змінює видимий розмір.** Об'єкт, що летить до камери,
«росте» без жодного дотику до `.scale`. Треба явно компенсувати —
`perspectiveScale.ts`.
**Ціль польоту пере-семплювати щокадру.** Камера рухається разом з гравцем;
взята один раз позиція HUD-анкера застигає й політ промахується.
**`tween.onComplete` зрідка не спрацьовує** (причина всередині tween.js не
знайдена) — для критичного клінапу дублювати `setTimeout` на тривалість + запас.
`removeFromScene` двічі — нешкідливо.
**screen↔world рахувати від `renderer.domElement.getBoundingClientRect()`**, не
від `window.innerWidth/Height` — SDK рендерить у letterboxed 9:16 бокс.
**nipplejs `force` не обмежений радіусом зони** — за краєм кільця росте далі,
хоч точка вже запінена. Клампити один раз при записі, не в кожному `calc()`.
**`Math.atan2` і CSS `conic-gradient` мають різний нуль** (схід vs північ) —
різниця рівно 90°.
**Дві CSS-анімації на одному елементі не живуть разом** — перемикання класу,
що чіпає `animation-name`, перезапускає анімацію. Розділяти на вкладені div.
**Анімувати RGB, не альфу**, якщо треба «згасання до базового кольору» — інакше
на нулі отримаєш прозору дірку замість кольору.
**У CSS — `--lvh`/`--lvw`, не сирі `vh`/`vw`/`vmin`** (ротація/адаптивність).
**Експорт — це gzip-блоб, а не текст. НЕ грепати `export/*.html` напряму.**
`npm run export` пакує весь бандл у base64+gzip (`decompressString("H4sIA...")`,
розпаковується `pako` в рантаймі). Будь-який пошук по цьому файлу дасть **0
збігів** — для всього, включно з класами самого SDK. Прочитати цей нуль як
«purgecss вирізав наш CSS» — легка й дуже дорога помилка (перевірено: після
розпаковки всі кастомні класи й `@font-face` на місці, кількість збігається з
`vite build`). Перевіряти тільки після gunzip.
**purgecss справді вирізає `.bannerButton`** — але саме його, точково (клас
призначається всередині мінифікованого SDK-бандла). У розпакованому експорті:
`.bannerButton{` — 0, `.bannerLogo{` — 1, `.bannerButtonText{` — 1. Не писати
CSS-правила під `.bannerButton`, це мертвий код. На решту класів не поширюється.
**`Template.setUpFont()` працює тільки коли `!release`** — тобто шрифт через
`customFont.ts`/`formFontFamily` реєструється для **прев'ю платформи**, а не для
експорту. У реальному білді шрифт малює наш власний `@font-face` з `main.css`
(інлайн base64). Це два незалежні механізми на дві різні поверхні — потрібні
обидва, і поломка одного не видна з іншого.
**Інлайн ассетів:** картинки — `ConvertToBase64WhenRelease("./file.webp")`, і
викликати обов'язково **з файлу в тій самій директорії**, що й ассет. Шрифти —
нічого не треба, `assetsInlineLimit` у конфігу SDK інлайнить `url()` у CSS сам.
## Ігрові дані
`ZombiePunk_Map.glb`, група **`Lootable`** → ~18 × `Wooden_Box_XXX`:
```ts
Template.getCategory("<categoryId>")["<parameterId>"]
```
Wooden_Box_XXX
BoxCollider.NNN obstacle-бокс
Wooden_Box_States_XXX
Wooden_Box_XXX_S1..S3 стейджі пошкодження, не в усіх є всі три
```
Крейти можуть стартувати вже пошкодженими (тільки S2/S3) — `BreakablePropC`
рахує стартовий HP з реальних номерів `_SN`, не з припущення про 3 стейджі.
`UI_Wood` — реальна іконка ресурсу (текстура `Icon_Wood`), клонується на кожен
спавн. `UI_Interactive_Zone_02` — Pay Zone. `UI_Tool_Zone`, `UI_Wood001`, група
`UI` — приховані як зайва декорація.
### Config format
## Поточний стан
`configUIParams` array parameters use `[min, max, default]` per axis.
Runtime `Template.getCategory()` returns the actual `[x, y, z]` value.
Never interpret `[min, max, default]` as runtime coordinates.
### Camera ownership
There must be exactly one writer of `camera.rotation`: `CameraFollowC`.
* `CameraC.setCamera()` owns FOV only.
* `CameraFollowC.applyRigFromConfig()` owns camera position/rotation.
* `camera_position_*` is a player-relative offset, not an absolute position.
* `camera_rotation_*` is a fixed angle in degrees.
* `camera_fov_*` is FOV.
* Do not write camera position/rotation from `CameraC.setCamera()`.
`setCamera()` can run again when any `camera_*` config changes.
* `TestSceneC.CAMERA_OFFSET` is fallback only.
### Gameplay tuning
`gameplay.ts` is the source for platform tuning defaults.
`DebugConfigC` mirrors these values locally in lil-gui (`C` key, dev-only).
The debug panel copies defaults once at startup; it does not live-sync.
---
## Critical SDK / runtime pitfalls
* GLTF loader removes dots from node names:
`UI_Wood.001` → `UI_Wood001`.
Inspect runtime names with `scene.traverse()`, not GLB JSON.
* GLB materials are shared between nodes. Clone a material before modifying
one instance.
* `MeshBasicMaterial.color` cannot produce a visible white→white hit flash.
Use `src/utils/flashOverlayMaterial.ts`.
* Zone checks must use `new Box3().setFromObject(node)`, not visual/radius estimates.
* Perspective changes apparent object size. Use `perspectiveScale.ts` when compensating
for flight toward/away from the camera.
* Flight targets must be re-sampled every frame because the camera follows the player.
* Do not rely solely on tween `onComplete` for critical cleanup.
Use a timeout fallback with a small safety margin.
`removeFromScene()` is safe to call twice.
* screen↔world projection must use
`renderer.domElement.getBoundingClientRect()`, never `window.innerWidth/Height`.
SDK uses a letterboxed 9:16 viewport.
* `nipplejs.force` can exceed the visual joystick radius.
Clamp it when storing input, not inside every calculation.
* `Math.atan2` and CSS `conic-gradient` use different zero directions.
Account for the 90° offset.
* Do not stack independent CSS animations by changing `animation-name` on the
same element. Use nested elements.
* To fade to a base color, animate RGB rather than alpha.
* Use `--lvh` / `--lvw`; avoid raw `vh` / `vw` / `vmin`.
---
## Export / CSS / fonts
* Export HTML is gzip+base64, not searchable source.
Never grep `export/*.html` to verify bundled content.
Inspect the decompressed payload instead.
* `.bannerButton` is purged from the release export.
Do not add CSS targeting it.
`.bannerLogo` and `.bannerButtonText` remain.
* `Template.setUpFont()` works only when `!release`.
It is for platform preview.
Release uses our own `@font-face` from `main.css`.
* Release images must use:
`ConvertToBase64WhenRelease("./file.webp")`
from the same directory as the asset.
* CSS fonts are inlined automatically by the configured `assetsInlineLimit`.
---
## Asset rules
`ZombiePunk_Map.glb`:
* `Lootable` contains approximately 18 `Wooden_Box_XXX` crates.
* Crates may start at S2/S3. `BreakablePropC` must derive initial HP from
actual `_SN` nodes; never assume three stages.
* `UI_Wood` is the resource icon and must be cloned per spawn.
* `UI_Interactive_Zone_02` is the pistol's Pay Zone marker (halved in scale);
the bat's own Pay Zone is a runtime clone of it, not a second authored node
— see `TestSceneC.createPayZoneNodes`.
* `UI_Tool_Zone`, `UI_Wood001`, and `UI` are hidden decorative nodes.
`ZombiePunk_Character.glb`:
* Existing clips include `Idle`, `SlowWalk`, `Loot`, `WeaponTakeOut`,
`PistolShoot`, `WeaponPutDown`, `Death`,
`PistolBack/Forward/Left/Right`.
* Do not invent character states before checking existing clips.
* `Character_Pistol` is a skinned mesh (`skin:0`), not a bone prop.
Skeleton animation moves it with the hand.
* Bat uses `Tool_1` / `Tool_2` visibility switching.
* `Bullet` is a separate hidden rig.
---
## Change discipline
* Prefer minimal, focused changes.
* Do not refactor unrelated code.
* Reuse existing utilities and controllers before creating new ones.
* Preserve the existing DI pattern.
* Before adding a new SDK workaround, check `src/utils/` and this file.
* When a new persistent rule is discovered, update this file only if it affects
future decisions; otherwise document it in the appropriate source file or
`docs/worklog/`.
- Working tree брудний, багато незакомічених змін.
- `DebugConfigC.ts` і `configUIParams/gameplay.ts` — нові, ще не в git.
- **Відомі борги** (відкладені свідомо, за рішенням користувача):
- `ui.css` / `health-bar.css` — досі на `vh`, без landscape-медіазапиту;
- VFX-емітер `StretchSmoke` у `VFX_Lootable_Hit` вилітає далеко від крейта
(дизайнерський ассет тюнили під інший масштаб сцени) — рішення за
користувачем: лишити / притлумити / прибрати емітер;
- Destroy-VFX підключений, але візуально не підтверджений.