99 lines
9.0 KiB
Markdown
99 lines
9.0 KiB
Markdown
# Day 5: Cannon.js — реалізовано (2026-08-11, доопрацьовано того ж дня)
|
||
|
||
Перша ітерація зробила всі 8 пунктів чек-листа буквально (single-target
|
||
дискретний свінг + proximity-тригери на ресурси). Користувач одразу дав
|
||
конкретні правки під реальний геймплей-дизайн (враховані в описі нижче) —
|
||
**фінальний стан коду відображає ці правки, не буквальний чек-лист**. Білд
|
||
(`vite build`) проходить чисто після кожної ітерації. Ручна перевірка в
|
||
браузері — **ще не підтверджена користувачем**; я зробив лише один короткий
|
||
automated прогін (Playwright, в scratchpad, не в репо) до першої ревізії —
|
||
рендер і консоль були чисті, а саме AoE/continuous-attack/resource-burst
|
||
поведінку (фінальну версію) користувач попросив перевірити самостійно.
|
||
|
||
## 1. Колайдери на об'єктах карти — ✅
|
||
`TestSceneC.createMap()`: всі `/collider/i` ноди, які НЕ належать `Lootable`,
|
||
отримують `PhysicsBody(node, false, 0, PhysicsLayer.Wall, Player|Enemy)`.
|
||
Lootable-крейти отримують свої Wall-тіла всередині `BreakablePropC` (щоб
|
||
можна було `destroy()` саме це тіло при знищенні крейта, без дублювання).
|
||
|
||
При ревайві `PhysicsC.PhysicsBody` знайшов і виправив реальний баг:
|
||
конструктор рахував size/rotation з `Box3` в world-space, обнуляючи лише
|
||
**власний** quaternion об'єкта — обертання батьків (напр. кожен
|
||
`Wooden_Box_XXX` root сам повернутий по Y) все одно потрапляло в bbox, тобто
|
||
box виходив перекошеним/більшим за реальний. Тепер: size — з
|
||
`mesh.geometry.boundingBox` (локальний, без жодних обертань) × world scale,
|
||
rotation — з `getWorldQuaternion()`. Коректно для будь-якої глибини вкладеності.
|
||
|
||
## 2. Колайдери на гравці й персонажах — ✅
|
||
Рух гравця **не переписаний** — досі ручний AABB `tryMove()`, як і був. Окреме
|
||
cannon-тіло гравця (яке було для тригерів) **видалено** разом з `TriggerC`
|
||
(див. вище) — зараз у гравця взагалі немає cannon `Body`, тільки AABB.
|
||
|
||
## 3–4. Тригери + start/stop events — ⚠️ зроблено, потім видалено
|
||
Був зроблений `TriggerC.ts` на `beginContact`/`endContact`, використовувався
|
||
для ресурсів. Після уточнення від користувача ("персонажу не потрібно
|
||
підходити для збору") ресурси більше не потребують proximity-тригера —
|
||
дивись секцію "Файли" вище. Формально пункт "invisible triggers" з
|
||
чек-листа Day 5 **не представлений в фінальному коді** — свідоме рішення
|
||
під конкретні вимоги гри, а не пропуск.
|
||
|
||
## 5. Розбиття пропсів — ✅ (AoE, не single-target)
|
||
Атака гравця (`PlayerC.updateCombat`) знаходить **усі** breakable-цілі в
|
||
зоні ураження (`findBreakableTargetsInZone` — та сама `INTERACTION_REACH`
|
||
AABB-аура навколо гравця, без обмеження кількості цілей) і при кожному
|
||
"пульсі" (раз на цикл анімації) хітає їх усі одразу через
|
||
`BreakablePropC.hit(prop)` для кожної.
|
||
|
||
`BreakablePropC.hit(prop)`: якщо є наступний stage — ховає поточний, показує
|
||
наступний, дропає 1-2 ресурси (`STAGE_HIT_RESOURCE_RANGE`); якщо це вже
|
||
останній stage — дропає 2-5 ресурсів (`DESTROY_RESOURCE_RANGE`), знищує
|
||
`physicsBody`, видаляє з `colliders`/`obstacles` (як і раніше) І **ховає
|
||
весь `prop.root`** (`root.visible = false`) — крейт зникає повністю, а не
|
||
лишається візуально на останньому stage-меші без колайдера.
|
||
|
||
## 6. Анімація атаки — ✅ (неперервна, перебивається рухом)
|
||
`Loot`-кліп — знову `LoopRepeat, Infinity` (НЕ `LoopOnce` — це був перший
|
||
варіант, користувач попросив назад неперервний свінг). Логіка в
|
||
`updateCombat`:
|
||
- Старт: гравець зупинений, є хоч одна breakable-ціль в зоні, і гравець
|
||
дивиться на найближчу з них (`isFacing`).
|
||
- Поки атакує: рух повністю розблокований (`updateMovement` виконується
|
||
щокадру незалежно від `isAttacking` — на відміну від першої версії, де рух
|
||
заморожувався на час свінгу). Будь-який стік-інпут -> `stopped=false` ->
|
||
атака миттєво скасовується (`equipPistol()`), без "дограти удар".
|
||
- Раз на цикл кліпу (на позначці `HIT_TIME_FRACTION`, тобто 50% циклу) —
|
||
"пульс" хіта по всіх поточних breakable-цілях в зоні (жива вибірка щокадру,
|
||
тож знищені цілі природно випадають з наступного пульсу).
|
||
- Зупиняється сама, коли `zoneTargets.length === 0` (усі цілі знищені) —
|
||
окремого cooldown між атаками нема, це не дискретний свінг.
|
||
|
||
## 7–8. Спавн і збір ресурсів — ✅ (розліт + автозбір, без тригерів)
|
||
`ResourceC.spawnBurst(origin, count)` / `spawnBurstInRange(origin, min, max)`:
|
||
кожен ресурс — клон реального `UI_Wood` (іконка дерева з мапи, див.
|
||
"Ключова знахідка" вище; заглушка-куб лишилась тільки на випадок відсутності
|
||
цього ноду), що летить від точки спавну по випадковому
|
||
напрямку в XZ на `SCATTER_MIN/MAX_DISTANCE` (0.5–1.2) з невеликою дугою
|
||
вгору-вниз (`SCATTER_ARC_HEIGHT`) за `FLY_DURATION` (0.45с), потім чекає
|
||
`SETTLE_DELAY` (0.5с) на місці і зараховується (`collected++`,
|
||
`console.log`) та зникає. **Гравцю не треба підходити** — це чистий
|
||
visual+timer ефект, без cannon body/тригера взагалі. Лічильник поки лише в
|
||
пам'яті/консолі — немає UI/economy hookup, це прототип.
|
||
|
||
## Best practices — де застосовано
|
||
- **Low-poly shapes**: усюди `Box`/`Sphere`, ніколи трімеш з реальної геометрії.
|
||
- **Fixed timestep**: вже було з коробки (`Physics_internal` кличе
|
||
`fixedStep()` без аргументів кожен кадр) — нічого додатково не робив.
|
||
- **Sync з рендером**: `PhysicsObjPair` (не використовується в Day 5 — нічого
|
||
фізика не рухає, ані гравець ані ресурси більше не мають cannon-тіл).
|
||
- **Sleep**: НЕ налаштовував `allowSleep`/sleep-ліміти явно — cannon-es має
|
||
дефолти (`allowSleep: false` за замовчуванням у `World`!), тобто зараз
|
||
**нічого не спить**. З десятками статичних Wall-тіл (тільки для колізій
|
||
карти/крейтів, п.1) це навряд чи проблема на цьому масштабі, але якщо буде
|
||
помітний perf-хіт — увімкнути `world.allowSleep = true` і виставити
|
||
sleep-ліміти на статичних тілах.
|
||
- **Debug visualizer**: не трогав прапорець (`debug.physics: false` в
|
||
`src/index.ts`) — поставити `true` вручну, коли треба візуально звірити
|
||
cannon-боксі з мапою.
|
||
- **Collision layers**: використаний існуючий `PhysicsLayer` enum без змін
|
||
(хоча `Trigger` тепер ніде не використовується після видалення тригерів).
|