Files
onboarding-project/docs/worklog/01-day5-cannon-physics.md
T
2026-08-14 15:03:43 +03:00

9.0 KiB
Raw Blame History

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