Files
onboarding-project/docs/worklog/00-stack-and-structure.md
T
2026-08-14 15:03:43 +03:00

16 KiB
Raw Blame History

Стек і структура

  • Не Babylon.js — це three.js (three@0.185) обгорнутий у пропрієтарний SDK @hitplay/playable_template (плейбл-реклама, HitPlay).
  • Фізика: cannon-es@0.20 + cannon-es-debugger. World піднятий через SDK (Physics_internal.init(...) в src/templateConfig/beforeResourcesLoadedCb.ts:27), дебаг-рендер тригериться прапорцем Template.initConfig({ debug: { physics: false } }) в src/index.ts (поки false — увімкнути під час дебагу фізики). SDK сам кличе world.fixedStep() кожен кадр (Physics_internal.update, всередині пакету) — свій world.step()/fixedStep() НЕ викликати, це вже фіксований таймстеп з коробки.
  • UpdateController.Instance.onUpdate — єдиний update-event, delta в секундах (з THREE.Clock.getDelta()), без фіксованого таймстепу на цьому рівні (фіксований степ — тільки всередині cannon, через fixedStep()).
  • ThreeC.removeFromScene(obj) існує (просто obj.removeFromParent()) — використовувати для видалення спавнених/знищених мешів.

Файли

src/controllers/
  PlayerC.ts         - джойстик-рух (nipplejs), AABB-колізії з "colliders",
                       бій: неперервний AoE-swing по всіх breakable-цілях в
                       зоні ураження (240°-конус, FACING_DOT_THRESHOLD=-0.5,
                       Day 7 — див. "Кут ураження"), перебивається рухом
  TestSceneC.ts      - завантажує ZombiePunk_Map.glb, /collider/i -> hidden+colliders,
                       ставить cannon Wall-тіла на все, крім Lootable-крейтів;
                       ініціалізує всі VFX/HUD-контролери (PropVfxC,
                       SparkleFxC, WeaponTrailC, HudC) в правильному порядку
  CameraFollowC.ts   - камера-слідкувач, obstacle-avoidance через Raycaster
                       по тому ж масиву colliders (НЕ торкались)
  PhysicsC.ts        - PhysicsLayer enum, PhysicsBody (Box/Sphere+cannon Body
                       з коректним world-space size/rotation), PhysicsObjPair
  BreakablePropC.ts  - парсить групу "Lootable" в мапі, S1/S2/S3 damage-stages,
                       2 хіти на стейдж, hit() advance stage / повне
                       знищення (tween-джус, Day 6), дропає ресурси
  WeaponTrailC.ts    - Day 7: hand-rolled screen-facing ribbon-шлейф за
                       битою під час замаху (samples PlayerC.getWeaponAnchor(),
                       обчислює "кінець бити" з реальної local bbox, не
                       вгадує offset), тає/звужується за час, не тон-мап,
                       toggled через PlayerC.updateCombat (isAttacking)
  ResourceC.ts       - Day 7: спавн ресурсу з крейта -> scatter -> 2 баунси
                       (відчутні, БЕЗ ротейту) -> на 2-му баунсі: білий
                       флеш (окремий additive-оверлей-меш, БЕЗ
                       squash-and-widen — чистий uniform shrink, тримається,
                       не пружинить назад) + SparkleFxC-вибух зірочок ->
                       staggered (по черзі, не купою) політ до
                       HudC.getWorldAnchorPosition() (ціль пере-семплюється
                       ЩОКАДРУ, не застигає, якщо камера рухається) з
                       компенсацією перспективи (див. RESOURCE_ICON_BASE_SCALE,
                       нижче) -> яскравіє й зникає рівно на іконці. НЕ знає
                       про Pay Zone/гравця; візуал — клон реального
                       "UI_Wood" з мапи; createVisual() — публічний доступ
                       до цього візуалу (без баунс/флеш начинки) для інших
                       контролерів (PayZoneC)
  PayZoneC.ts        - Day 6/8: окремо зливає ResourceC.getCollectedCount() в
                       Pay Zone (вузол мапи "UI_Interactive_Zone_02"), по
                       одному ресурсу, тільки поки гравець стоїть в зоні;
                       зникає після RESOURCES_TO_CLOSE_ZONE доставлених
                       (зараз 20); росте плаский fill-overlay квад по мірі
                       заповнення; політ депозиту (HudC.getWorldAnchorPosition()
                       -> Pay Zone) БЕЗ ротейту, з тією ж компенсацією
                       перспективи й базовим розміром, що й ResourceC
                       (RESOURCE_ICON_BASE_SCALE, спільна константа —
                       інакше токен виглядає більшим за ресурс, що щойно
                       був на тому ж місці), плюс окремий
                       DEPOSIT_FLIGHT_SCALE_MULTIPLIER для локального тюнінгу
  HudC.ts            - Day 8: лічильник ресурсу — реальний дизайнерський
                       ассет (resourceCounterBgSrc, Tool_15.webp з temp/,
                       планка вже вбудована в картинку, не окрема іконка),
                       DOM-оверлей в #ui, стилі в src/css/ui.css (не inline
                       <style>); getWorldAnchorPosition() — СПРАВЖНЯ
                       screen-to-world проєкція (getBoundingClientRect()
                       правого краю плейта + unproject через камеру), НЕ
                       вгаданий локальний офсет від камери (той підхід
                       видалений повністю, разом з WORLD_ANCHOR_LOCAL_OFFSET)
  SparkleFxC.ts      - Day 7: пул із 30 зіркових мешів/матеріалів (pop-in
                       Back.Out -> hold -> fade-out через TweenC), жодних
                       нових Mesh/Material під час гри — spawnBurst() бере
                       вільний слот з пулу; використовується ResourceC на
                       2-му баунсі
  PropVfxC.ts        - Day 6: завантажує дизайнерські quarks.art VFX-префаби
                       (JSON з temp/, sparks/dust/debris) для хіту/знищення
                       крейтів
  WeaponHudC.ts      - Day 8: статична юайка внизу-зліва — восьмикутна
                       плита (weaponHudBgSrc, ZombiePunk_Icon-Bottom.webp з
                       temp/) + іконка бити (weaponIconBatSrc, Tool_2.webp)
                       + хардкоджений текст "LVL 0" (нема системи рівнів
                       зброї в проєкті — суто візуал під референс). Іконка
                       завжди бита, не стежить за поточною екіпірованою
                       зброєю (pistol/bat) — окреме, свідомо не зроблене
                       рішення
  ZombieInvasionHudC.ts - Day 8: топ-центр "ZOMBIE INVASION" — заголовок +
                       таймер-бар. Голова зомбі (zombieHeadIconSrc,
                       REF_1.png з temp/ — файл НАСПРАВДІ webp, не png,
                       розширення в джерелі помилкове) їде з
                       right:100% -> left:0% за TIMER_DURATION_S (120с)
                       через CSS transition (JS лише раз перемикає
                       значення); окремий .zombie-invasion__fill росте
                       0%->100% width з right:0 в ТОЙ САМИЙ момент/тривалість
                       — їх ліва межа завжди математично збігається з
                       позицією голови, без пофреймової синхронізації.
                       Останні WARNING_FRACTION=0.05 (6с) — pulse+червоний
                       флеш через setTimeout+клас (CSS keyframes самі
                       крутяться в infinite, ніхто їх не зупиняє після
                       заповнення — свідомо, за проханням користувача).
                       Немає дизайнерського ассету під сам бар/трек —
                       чистий CSS pill, лише голова — реальна картинка
  ThreeC.ts/CameraC.ts - базовий сетап three.js/камери від SDK
resources/meshes/    - ZombiePunk_Map.glb, ZombiePunk_Character.glb
resources/images/    - resource_counter_bg.webp (Tool_15.webp з temp/,
                       дизайнерський плейт лічильника), weapon_hud_bg.webp,
                       weapon_icon_bat.webp, zombie_head_icon.webp,
                       images.ts (icon_wood.webp/iconWoodSrc видалені —
                       стали осиротілими після заміни на resourceCounterBgSrc)
resources/fonts/     - PassionOne-Black.otf (з temp/) — див. "gameFont"
                       нижче
resources/vfx/       - VFX_Lootable_Hit.json, VFX_Lootable_Destroy.json
                       (quarks.art префаби, копії з temp/, реально
                       використовуються з коду — див. PropVfxC)

TriggerC.ts (beginContact/endContact -> onEnter/onExit) і кінематичне cannon-тіло гравця для тригерів — були написані й видалені в межах цієї ж сесії, ще до коміту: перший дизайн ресурсів вимагав підходити до пікапа й стояти в тригер-зоні; користувач уточнив, що гравцю підходити не треба (ресурси самі розлітаються і зараховуються по таймеру), тож фізичні тригери виявились непотрібні для Day 5. Якщо колись знадобиться proximity-based тригер для чогось іншого (вода, зона урону, детект ворога) — писати заново, але сам підхід (world.addEventListener('beginContact'/'endContact') + мапа body.id -> handlers) вже перевірений робочим, просто не лишений в коді.

Ключова знахідка: структура glb вже готова під breaking/loot

Мапа (ZombiePunk_Map.glb) містить групу Lootable (пряма дитина кореня Map) з ~18 дітьми Wooden_Box_XXX (XXX = 000..017). Кожен має:

Wooden_Box_XXX
  BoxCollider.NNN            <- obstacle-бокс, він же в загальному /collider/i списку
  Wooden_Box_States_XXX
    Wooden_Box_XXX_S1        <- найменше пошкоджений (не завжди є)
    Wooden_Box_XXX_S2
    Wooden_Box_XXX_S3        <- найбільш пошкоджений/уламки (завжди є хоча б цей)

Не всі крейти мають усі 3 стейджі — деякі стартують вже пошкодженими (тільки S2/S3) або взагалі як декоративні уламки (тільки S3). BreakablePropC сортує дітей ..._States_XXX за номером у назві й показує лише перший — кожен хіт просуває на наступний, а хіт по останньому стейджу знищує крейт.

Root-рівень gltf-сцени (getObject("map") = gltf.scene, НЕ сам "Map"-нод) містить ще 3 сиблінги "Map": UI_Tool_Zone, UI_Interactive_Zone_02, UI_Wood/UI_Wood.001 — плоскі quad-меші на позиції origin. В грі це видно як пунктирний білий квадрат, що лежить на дорозі біля крейтів (скріншот при першому запуску) плюс текстуру дерева поверх нього.

UI_Wood (mesh Plane.002) — це реальний іконка-ресурсу, не сміття. Матеріал Material (index 6) використовує текстуру з назвою Icon_Wood (unlit, alphaMode BLEND, doubleSided) — художник підготував конкретно "іконку дерева" саме для цього. Тепер підключено: TestSceneC бере цей нод, віддає його в ResourceC.setPickupTemplate() (клонується на кожен спавн ресурсу — .clone()) і ховає оригінал (visible=false, він більше не статична декорація, а шаблон для клонування). ResourceC.createPickupMesh() примусово виставляє clone.visible = true, бо .clone() копіює і visible:false з прихованого оригіналу.

UI_Wood.001 (mesh Plane.008, матеріал M_Items/текстура T_items_icons — схоже на спрайт-атлас з кількома іконками) і UI_Tool_Zone — призначення досі не зрозуміле, але користувач попросив прибрати їх як "зайві текстури" навколо Pay Zone — тепер ховаються в TestSceneC (visible=false) поруч з UI_Wood. UI_Interactive_Zone_02 (пунктирний квадрат) — тепер підключений в Day 6 як Pay Zone (див. нижче).

Ще одна знахідка (не сиблінг, а дитина Map -> "UI"): группа UI (UI_Background/UI_Foreground/UI_Middleground, всі матеріал _Part2 — той самий атлас, що й крейти) — окремо повернута група (власний quaternion), локально приблизно (0.31, 1.21, -1.9), тобто підвішена в повітрі на висоті голови персонажа. Схоже на фейковий install-button мокап (типовий playable-ad прийом), рендерився завжди, візуально "прямокутна синя пластина над плей зоною" з якою скаржився користувач. Теж ховається тепер (map.getObjectByName("UI"), visible=false).