9.0 KiB
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: використаний існуючий
PhysicsLayerenum без змін (хочаTriggerтепер ніде не використовується після видалення тригерів).