Files
onboarding-project/CLAUDE.md
T
2026-08-14 15:03:43 +03:00

244 lines
16 KiB
Markdown
Raw 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.
# 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 підключений, але візуально не підтверджений.