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

16 KiB
Raw Blame History

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