# 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` тепер ніде не використовується після видалення тригерів).