244 lines
16 KiB
Markdown
244 lines
16 KiB
Markdown
# 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("<id категорії>")["<id параметра>"]`
|
||
(приклад — `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 підключений, але візуально не підтверджений.
|