# 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.