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

183 lines
16 KiB
Markdown
Raw Permalink 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.
# Стек і структура
- **Не 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`).