Utility and polishing + bag fixes

This commit is contained in:
Oleksandr Vlasiuk
2026-08-14 15:03:43 +03:00
parent 250f4a0d2c
commit 1f72420da0
45 changed files with 2542 additions and 1618 deletions
+98
View File
@@ -0,0 +1,98 @@
# 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` тепер ніде не використовується після видалення тригерів).