Utility and polishing + bag fixes
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
# Day 6: Tween Animations — реалізовано (2026-08-11, виправлено того ж дня)
|
||||
|
||||
`@tweenjs/tween.js` був установлений, але **ніде не використовувався** до
|
||||
цього дня. SDK має готову обгортку `TweenC` (`@hitplay/playable_template`,
|
||||
`import { TweenC } from "@hitplay/playable_template"`) з власною `Group` і
|
||||
автопідпискою на `UpdateController` — **треба викликати `TweenC.init()`
|
||||
один раз** (додано в `beforeResourcesLoadedCb.ts`, поруч з
|
||||
`Physics_internal.init(...)`), інакше `TweenC.add()`/`.create()` тихо
|
||||
нічого не анімують.
|
||||
|
||||
## ⚠️ Знайдений і виправлений баг: `.chain()` НЕ реєструє другу ланку в групі
|
||||
Перша реалізація хіт-фідбеку й disappear-анімації використовувала
|
||||
`tweenA.chain(tweenB); TweenC.add(tweenA); tweenA.start();` — і крейти
|
||||
переставали зникати повністю (застигали розтягнутими на punch-фазі),
|
||||
а флеш/нахил не виглядав як задуманий ефект (застигав на пікові й лишався).
|
||||
Причина, перевірена читанням `tween.cjs`: `.chain()` каже tweenA лише
|
||||
покликати `tweenB.start()` при завершенні — `.start()` ставить
|
||||
`_isPlaying=true`, але **не додає tweenB в жодну `Group`**. `Group.update()`
|
||||
ітерує тільки тіли, додані через `Group.add()`, тож tweenB ніколи не
|
||||
отримує `.update()`, назавжди застигаючи "playing" в останньому кадрі
|
||||
tweenA. **Виправлення: додавати кожну ланку ланцюжка в групу окремо**
|
||||
(`TweenC.add(tweenA); TweenC.add(tweenB);` — обидва, до `tweenA.start()`).
|
||||
Підтверджено repro+fix через Playwright у scratchpad (консоль показувала
|
||||
`onUpdate` для другої ланки, що ніколи не спрацьовував до фіксу).
|
||||
|
||||
`Tween.stop()` **каскадно зупиняє весь `.chain()`**, навіть якщо зараз
|
||||
виконується вже ланка ланцюжка, а не голова (`stop()` завжди спочатку
|
||||
кличе `stopChainedTweens()`, до перевірки `_isPlaying`) — це підтверджено
|
||||
і лишається правильним механізмом "always kill or reuse tweens": досить
|
||||
тримати посилання на ГОЛОВУ ланцюжка і кликати на ній `.stop()` (саме
|
||||
зупинку це чіпляє коректно; сама помилка була тільки в реєстрації в групі).
|
||||
|
||||
Перед реалізацією користувач попросив **питати по кожному з 4 пунктів**, а
|
||||
згодом дав ще правки під час перевірки — відповіді й правки (важливі для
|
||||
майбутніх сесій):
|
||||
- "Гроші" з Day 6 = те саме дерево, що вже рахує `ResourceC` (не окрема валюта).
|
||||
- Pay Zone: гейм-дизайн "що буде після" — **не важливо**, лише сам механізм.
|
||||
- HUD не існує і Day 6 його НЕ додає.
|
||||
- Hit-feedback: короткий білий флеш ін-аут + нахил у протилежну від удару
|
||||
сторону і повернення.
|
||||
- (правка) Гроші мають летіти **прямо з персонажа в Pay Zone** — без
|
||||
проміжної "UI-точки", яка була в першій версії.
|
||||
- (правка) Pay Zone потребує **100** зібраних ресурсів, щоб зникнути (не
|
||||
просто ">0", як було спочатку). (Пізніше значення `RESOURCES_TO_CLOSE_ZONE`
|
||||
підкручено до **20** — див. секцію "прогрес-заповнення зони" нижче;
|
||||
сам механізм — "N доставлених закриває зону" — не змінився.)
|
||||
- (правка) Кожен damage-stage крейта потребує **2 хіти**, не 1.
|
||||
|
||||
## 1. Hit feedback + disappearing (крейти) — ✅
|
||||
`BreakablePropC`: матеріал кожного stage-меша **клонується один раз при
|
||||
білді** (`buildStageVisual`) — без цього флеш одного крейта підсвітив би
|
||||
ВСІ 18 крейтів одразу, бо всі стейджі всіх крейтів шарять один Material
|
||||
з glb (перевірено по індексу матеріалу в самому glb). `HITS_PER_STAGE = 2`
|
||||
— `hit()` рахує `prop.hitsOnStage`, і тільки на 2-му хіті стейдж
|
||||
просувається/крейт руйнується; **кожен** хіт (і 1-й, і 2-й, якщо не
|
||||
руйнує) грає `playHitFeedback` — один `Tween<{t}>` ланцюжком (flash-in
|
||||
90мс -> flash-out 150мс, **обидві ланки додані в `TweenC` окремо**, див.
|
||||
баг вище), в `onUpdate` одночасно виставляє `root.rotation` як
|
||||
`baseRotation + tilt*t` (`baseRotation` теж збережений один раз при білді,
|
||||
щоб повторні хіти під час ще не завершеного нахилу не накопичували дрейф
|
||||
кута) і, зараз, **опасіті окремого additive-оверлей-меша** (`flashMesh`,
|
||||
не лерп `material.color` — це вже пізніша правка, `material.color` на цих
|
||||
unlit-крейтах виявився тихим no-op, див. секцію "Розширений hit-feedback"
|
||||
нижче за повним поясненням). `tilt` рахується з `hitDirection`, який
|
||||
передає `PlayerC` (`hitDirectionTo()` — attacker->target, XZ, нормалізований).
|
||||
|
||||
На хіті, що руйнує крейт (2-й хіт останнього стейджу) — окремий ефект
|
||||
замість флешу/нахилу: **(оновлено пізніше)** зараз це sink+topple —
|
||||
`root.position.y` занурюється вниз (`DESTROY_SINK_DEPTH`, `Quadratic.In`,
|
||||
`DESTROY_SINK_MS`) з одночасним нахилом від удару (`DESTROY_TILT_ANGLE`,
|
||||
той самий `tilt`-принцип, що й у hit-feedback), `root.visible=false`
|
||||
виставляється в `onComplete`, а НЕ миттєво — фізика/обстакл-клінап
|
||||
(`physicsBody.destroy()`, видалення з масивів) лишились синхронними в
|
||||
момент хіта (гравець вже не натикається на нього, поки крейт ще візуально
|
||||
занурюється). Це заміна першої версії (punch-scale 1 -> 1.15 `Back.Out` ->
|
||||
0 `Quadratic.In`) — коли й чому саме на sink+topple, в чаті цієї сесії не
|
||||
зафіксовано; якщо матимеш контекст, допиши сюди.
|
||||
|
||||
## 2–3. Money flying out of the player into the Pay Zone — ✅ (два ОКРЕМІ механізми)
|
||||
Проміжна версія об'єднала "збір ресурсу" і "передачу в Pay Zone" в один
|
||||
конвеєр, гейтований стоянням в зоні (`ResourceC` мав стейт `holding`, що
|
||||
чекав `inZoneCheck()` перш ніж рахувати ресурс — тобто лічильник взагалі
|
||||
не рухався, поки гравець не заходив в зону). Це **зламало базовий збір**:
|
||||
ресурси лежали на землі й не зникали, якщо гравець ламав ящики далеко від
|
||||
зони. Користувач уточнив: **збір має працювати як і раніше** (Day 5,
|
||||
нічого спільного з Pay Zone), а гейтинг по стоянню в зоні має стосуватись
|
||||
**лише окремого механізму "передачі" вже зібраного в зону**.
|
||||
|
||||
Фінальний дизайн (на момент Day 6) — `ResourceC` і `PayZoneC` повністю
|
||||
незалежні:
|
||||
- **`ResourceC`** — Day 5 логіка збору (`scatter` -> settle) без жодної
|
||||
згадки про Pay Zone/гравця/зону — просто "зібрав ресурс". Що саме
|
||||
відбувається ПІСЛЯ settle (миттєво рахується, чи летить кудись) —
|
||||
контролюється ззовні через `setFlyTarget()` (додано пізніше, див.
|
||||
"HUD" нижче — спочатку тут стояв просто `collect()` без польоту).
|
||||
`createVisual()` — публічний метод, що віддає клон іконки-ресурсу для
|
||||
чужого використання (зараз юзає `PayZoneC`).
|
||||
- **`PayZoneC`** — окремо "зливає" вже зібране (`ResourceC.getCollectedCount()`)
|
||||
в зону, по одному ресурсу за раз, **тільки поки гравець стоїть в зоні**
|
||||
і є "хвіст" (`collected - deposited > 0`): кожні `DEPOSIT_INTERVAL`
|
||||
(0.3с) — новий `ResourceC.createVisual()` летить в зону (`Quadratic.InOut`,
|
||||
500мс) і зникає, `deposited++`. Звідки саме стартує цей політ — теж
|
||||
змінилось пізніше (див. "HUD" нижче).
|
||||
|
||||
## ⚠️ Знайдений і виправлений баг: `isPlayerInside` мав фіксований радіус замалий за фактичний розмір зони
|
||||
Перша версія `isPlayerInside` рахувала XZ-відстань до пивота ноду й
|
||||
порівнювала з `ZONE_RADIUS = 2` (підібраним на око зі скріншота). За
|
||||
фактом гравець міг зібрати 20+ ресурсів, зайти всередину видимого
|
||||
пунктирного квадрата — і нічого не відбувалось, бо реальний розмір зони
|
||||
значно більший за коло радіусом 2. Перевірено вимірюванням: `new
|
||||
Box3().setFromObject(payZoneNode)` дав `size ≈ [5.96, 0.09, 4.60]` (X/Z),
|
||||
тобто прямокутник ~6×4.6, з центром зсунутим від origin (`min.x≈-2.89,
|
||||
max.x≈3.06` — не симетрично!). Коло радіусом 2 покривало тільки малу
|
||||
частку видимого квадрата, переважно по X.
|
||||
|
||||
**Виправлення**: `isPlayerInside` тепер рахує `Box3` один раз в `init()`
|
||||
(`new Box3().setFromObject(payZoneNode)` — бере фактичну геометрію з
|
||||
урахуванням трансформів, а не вгадану цифру) і перевіряє просте
|
||||
point-in-rectangle по X/Z (`min <= pos <= max`), ігноруючи Y. Ніяких
|
||||
магічних констант радіуса більше нема — footprint завжди відповідає
|
||||
реальній мапі, навіть якщо художник поміняє розмір/форму зони.
|
||||
**Урок**: для будь-якої майбутньої "чи гравець в зоні X" перевірки —
|
||||
рахувати `Box3().setFromObject()` з реального ноду, не вгадувати
|
||||
радіус/розмір на око зі скріншота.
|
||||
|
||||
## 4. Payzone disappearing — ✅ (потребує `RESOURCES_TO_CLOSE_ZONE` *доставлених*, не просто зібраних)
|
||||
`PayZoneC.update()`: коли `deposited >= RESOURCES_TO_CLOSE_ZONE` (значення
|
||||
на момент Day 6 було 100, зараз в коді **20** — підкручено пізніше, див.
|
||||
"прогрес-заповнення зони"; в будь-якому разі це "N ресурсів фактично
|
||||
долетіли в зону", не просто зібрані десь на мапі) — одноразово (`closed`
|
||||
флаг) запускає `playDisappear()`: `scale 1->0` (`Quadratic.In`, 400мс),
|
||||
`visible=false` в `onComplete`. Одноразово, назавжди (немає ре-спавну/
|
||||
циклу — "що буде після" лишається не реалізованим за проханням
|
||||
користувача). Пізніше (див. "прогрес-заповнення зони") цей самий
|
||||
`playDisappear` розширений — тепер синхронно стискає ще й fill-overlay
|
||||
квад, не тільки сам маркер зони.
|
||||
|
||||
## Best practices — де застосовано
|
||||
- **Sequences**: усюди `.chain()` замість ручного стейт-машину (flash in->out,
|
||||
punch->shrink) — але **обов'язково додавати кожну ланку в `TweenC` group
|
||||
окремо** (див. баг вище) — `.chain()` сам лише стартує наступну ланку,
|
||||
не реєструє її для update-тіків.
|
||||
- **Kill or reuse tweens**: `BreakableProp.hitTween` зберігає голову
|
||||
ланцюжка; `.stop()` перед кожним новим хітом і при `destroy()` (каскадно
|
||||
зупиняє й активну ланку, це підтверджено робочим). `PayZoneC`'s
|
||||
disappear — одноразовий по флагу (`closed`), тому без явного kill.
|
||||
Reference in New Issue
Block a user