# MyGamePlayable — контекст проєкту Playable ad на three.js + пропрієтарний SDK HitPlay. Цей файл вантажиться в кожну сесію — тримай його **інструкцією, а не щоденником**. --- ## ПРАВИЛА ВЕДЕННЯ ЦЬОГО ФАЙЛУ **Стеля — 300 рядків.** Перевищив — перенеси найстаріше й уже усталене в `docs/worklog/`. Історія розробки живе там, не тут. ### Що ДОДАВАТИ сюди - правило або конвенцію, що змінює майбутні рішення («використовуй утиліту 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 на файл. --- ## Стек - **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. `UpdateController.Instance.onUpdate` — **єдиний** update-івент, `delta` в секундах. `ThreeC.removeFromScene(obj)` — видалення спавнених мешів. ## Команди | Команда | Що робить | |---|---| | `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, дивитись коли розпухає бандл. ## Структура ``` 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/ архів історії розробки ``` Підпапки в `controllers/` **свідомо не зроблені** — SDK сам плаский, вигоди нуль. ### Утиліти — використовувати, не переписувати - `perspectiveScale.ts` — компенсація перспективи при польоті до/від камери. - `screenWorldProjection.ts` — world↔screen в обидва боки. - `tweenChain.ts` — коректна реєстрація ланцюжка твінів. - `flashOverlayMaterial.ts` — адитивний флеш-оверлей. ### DI-патерн Контролери не імпортують одне одного напряму, коли це створить цикл — інжектиться геттер/колбек, а зв'язує `TestSceneC`: `ResourceC.setFlyTarget()`, `HudC.setBalanceGetter()`, `CameraFollowC.setFacingTargetGetter()`, `BreakablePropC.onHit/onDestroyed`. ## Конфіг платформи ↔ код Платформа 24ADS рендерить ліву панель із категорій. Частина — наші (`src/configUIParams/`), частина — стандартні від SDK. | Вкладка на платформі | Джерело | |---|---| | 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("")[""]` (приклад — `DebugConfigC.init()`). **Scene settings** (`globalSettings.ts`): `light_intensity`, `light_color`, `camera_fov_p/l`, `camera_position_p/l`, `camera_rotation_p/l`. **Формат `configUIParams` ≠ формат рантайму.** У визначенні кожна вісь масивного параметра — це `[min, max, default]`; у грі `Template.getCategory()` віддає готовий `[x, y, z]`. Не переплутати: `[-20, 70, -6]` — це не координати. **Камера: рівно один писар на `camera.rotation` — `CameraFollowC`.** `camera_position_*` — це офсет **відносно гравця** (не абсолютна позиція, камера слідкує), `camera_rotation_*` — фіксований кут у градусах, `camera_fov_*` — FOV. Розподіл власності: FOV застосовує `CameraC.setCamera()`, риг — `CameraFollowC.applyRigFromConfig()`. Не повертати position/rotation у `CameraC.setCamera()`: він перезапускається на кожну зміну будь-якого `camera_*` поля, і два писарі на кут ламали камеру назавжди до кінця сесії. `CAMERA_OFFSET` у `TestSceneC` лишився тільки як фолбек. **Gameplay tuning** (`gameplay.ts`) дублює те, що дев-панель `DebugConfigC` (клавіша `C`, тільки lil-gui, не потрапляє в білд) показує локально. Панель сідить свої дефолти з цієї категорії при старті — один раз, без live-sync. ## Живі граблі Кожна вже кусала. Довгі розбори — в `docs/worklog/`. **glTF-завантажувач стирає крапки з імен нодів.** У сирому GLB `UI_Wood.001`, у живій сцені — `UI_Wood001`. `getObjectByName()` тихо повертає `undefined`, гард `if (node)` завжди хибний, код «виглядає правильним». Перевіряти реальне ім'я через `scene.traverse`, не через GLB JSON. **Матеріали шаряться між усіма нодами з GLB.** Хочеш змінити матеріал одного об'єкта — клонуй його один раз при білді, інакше зачепиш усі 18 крейтів. **`material.color` на `MeshBasicMaterial` не дає флешу.** Матеріал unlit, колір множиться на текстуру, дефолт уже білий — лерп «білий → білий» невидимий. Для hit-flash — окремий адитивний оверлей-меш (`flashOverlayMaterial.ts`). **«Чи гравець у зоні 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`: ``` 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` — приховані як зайва декорація. ## Поточний стан - Working tree брудний, багато незакомічених змін. - `DebugConfigC.ts` і `configUIParams/gameplay.ts` — нові, ще не в git. - **Відомі борги** (відкладені свідомо, за рішенням користувача): - `ui.css` / `health-bar.css` — досі на `vh`, без landscape-медіазапиту; - VFX-емітер `StretchSmoke` у `VFX_Lootable_Hit` вилітає далеко від крейта (дизайнерський ассет тюнили під інший масштаб сцени) — рішення за користувачем: лишити / притлумити / прибрати емітер; - Destroy-VFX підключений, але візуально не підтверджений.