# Day 7: VFX — шлейф зброї, доопрацювання ресурсів (2026-08-12) **Важливо про верифікацію в цій секції і нижче**: користувач явно попросив припинити самостійні Playwright/агент-перевірки ("Досить самостійно щось перевіряти і тестувати" — [[feedback-verification-pace]] в пам'яті) — тож усе нижче перевірено ВИКЛЮЧНО через живий фідбек користувача в грі (він тестує, повідомляє що не так, я виправляю), а НЕ через автоматичні скріншоти/Playwright, як було в Day 5/6. Це свідоме рішення, не пропуск. ## `WeaponTrailC.ts` — новий контролер, шлейф-"вітер" за битою під час замаху Користувач попросив ефект вітру за зброєю під час замаху — hand-rolled screen-facing ribbon (не `three.quarks`, бо трейл має слідкувати за bone-driven точкою щокадру, а не грати фіксований дизайнерський burst, як `PropVfxC`). Технічно: кожен кадр поки `PlayerC.isAttacking` — семплить world-позицію "кінця бити" (обчислюється з реальної local bounding box всіх мешів під `Tool_1`, НЕ вгаданий offset — `computeLocalTipOffset()`, з підстроюваним `TIP_INSET_FRACTION` — трохи не на самий кінець, за проханням користувача), тримає ring buffer точок з віком, будує ribbon- геометрію (пара left/right вершин на точку, перпендикуляр = cross(tangent, viewDirection камери — камера не обертається, тож напрямок погляду захоплений один раз при `init()`), затухання ширини/кольору за `FADE_POWER`-кривою (не лінійно — "не таким плавним", різкіший спад), additive blending, без тон-мапінгу. `PlayerC.updateCombat` викликає `WeaponTrailC.setActive(true/false)` на початку/кінці атаки. ## `ResourceC.ts` — повний редизайн pickup-анімації (кілька раундів фідбеку) Стара версія (Day 5): scatter -> settle -> instant collect/fly, з ротейтом (`SPIN_SPEED`). Користувач попросив: прибрати ротейт, додати 2 баунси від землі, на 2-му баунсі — білий флеш + стиск + вибух зірочок, потім зникнення. Фінальний стейт-машин: `scatter` -> `bounce` (2 цикли, `BOUNCE_HEIGHTS`/`BOUNCE_DURATIONS`, index 0->1) -> `impactHold` (флеш + стиск на 2-му баунсі) -> `toTarget` (політ до HUD). **⚠️ Знайдений і виправлений баг: флеш-оверлей був зміщений, а не поверх ресурсу** `buildFlashOverlays()` клонував меш і чіпляв клон як ДИТИНУ САМОГО МЕША (`mesh.add(overlay)`), а не як сиблінга під тим самим батьком (як робить `BreakablePropC.buildStageVisual`). `mesh.clone()` копіює ЛОКАЛЬНУ трансформацію меша (відносно ЙОГО батька) — вставлена на рівень глибше, та сама трансформація інтерпретувалась вже у ВЛАСНІЙ системі координат меша, тобто оверлей був зміщений/спотворений, а не коінцидентний з мешем. **Фікс**: `overlay.position/rotation/scale.set(...)` в identity одразу після clone(), перед `mesh.add(overlay)`. **Стиск (squash) — три ітерації**: 1. Squash-and-widen (X/Z шириться, Y стискається, пружинить назад) — користувач: "ресурси на прикінці анімації збільшуються, мають навпаки зменшуватись" — розтягнення X/Z понад 1.0 читалось як РІСТ. 2. Uniform shrink (`scale.setScalar(1-shrink)`, без розтягування) — краще, але користувач ще раз: "має бути одного розміру від початку до кінця" про `toTarget`-фазу конкретно. 3. **Фінал**: стиск ЛИШЕ на 2-му баунсі (`impactHold`, тримається, не пружинить назад — "без повернення до початкового розміру"), а сама `toTarget`-фаза взагалі НЕ чіпає scale (тільки position + opacity флешу-конвергенції). Одного разу я неправильно прибрав СТИСК НА БАУНСІ замість того, щоб залишити його — користувач поправив: "Ти прибрав зміну розміру при баунсі. Я тобі писав не про це" — стиск на баунсі БУЛО повернено, лишається бажаним ефектом. **⚠️ Знайдений і виправлений баг: "зміна розміру під час польоту" — насправді перспектива, не scale** Після всіх фіксів вище користувач далі бачив "зростання" саме під час `toTarget`-польоту — а код `toTarget` взагалі не чіпав `.scale`. Причина: камера перспективна, а ціль польоту (`HudC.getWorldAnchorPosition()`) сидить БЛИЗЬКО до камери (`ANCHOR_DISTANCE=2.5`), тоді як крейт (звідки ресурс вилітає) — далеко (~12+ одиниць, через `CAMERA_OFFSET=(0,9,-9)`). Наближення до камери саме по собі візуально збільшує об'єкт (perspective foreshortening), без жодної зміни `.scale` в коді. **Фікс**: компенсація — кожен кадр рахую `currentDistance = camera.position.distanceTo(mesh.position)`, зберігаю `startDistanceFromCamera` (дистанція в момент старту `toTarget`) і `baseScale` (розмір після баунсу), виставляю `scale = baseScale * (currentDistance / startDistanceFromCamera)` — компенсує наближення так, що АПАРЕНТНИЙ (екранний) розмір лишається сталим, яким був щойно після баунсу, незалежно від реальної world-space дистанції. **Урок**: "resource visually grows/shrinks" не завжди означає "хтось десь чіпає .scale" — перспективна камера сама змінює апарентний розмір при зміні дистанції, це треба явно компенсувати, якщо небажано. **⚠️ Знайдений і виправлений баг: ціль польоту застигала, якщо камера рухалась** `resource.to` виставлявся ОДИН РАЗ (`this.flyTarget()`) при вході в `toTarget` — якщо камера рухалась під час ~0.5с польоту (гравець йде, camera-follow), ресурс летів у застарілу world-точку, яка вже не збігалась з реальною позицією іконки на екрані (користувач: "втрачають потрібну позицію і летять не туди"). **Фікс**: `this.flyTarget()` викликається ЩОКАДРУ всередині `toTarget`-блоку, не один раз — ресурс щокадру перецілюється на актуальну позицію. **Інші правки за фідбеком**: баунси зроблені відчутнішими (висота `[0.16,0.08]->[0.4,0.22]`); флеш подовжений і РОЗВʼЯЗАНИЙ від швидкості стиску (`FLASH_DURATION` vs `SHRINK_RAMP_DURATION` — окремі константи, інакше подовження одного тягне інше); одна спроба зробити політ "пришвидшеним" (ease-in `t²` замість smoothstep) БУЛА ВІДКОЧЕНА — причина: `CONVERGE_START_FRACTION`-конвергенція тригериться по часу `t`, а ease-in робить просторовий прогрес значно повільнішим за часовий, тож ресурс зникав, не доїхавши до цілі ("зникають раніше часу") — **урок**: якщо якийсь ефект прив'язаний до сирого `t`, зміна кривої `eased` для позиції може розсинхронізувати їх — тримати узгодженим або привʼязувати ефект до фактичного пройденого шляху, не до часу. ## `SparkleFxC.ts` — новий контролер, пул зіркових спалахів Спочатку — наївна версія (`new Mesh`/`new MeshBasicMaterial` на кожен вибух, 5 зірок на баунс). Користувач попросив пулінг (Day 7 бриф явно рекомендує "use pools for VFX", і кожен крейт може дати 2-5 ресурсів, кожен зі своїм вибухом = 10-25 create+GC циклів за секунду). **Фінал**: `POOL_SIZE=30` зіркових мешів/матеріалів створюються ОДИН РАЗ в `init()` (викликається з `TestSceneC`), приховані/scale=0; `spawnBurst()` шукає вільні слоти (`inUse`), активує (позиція/lookAt/scale/opacity), звільняє в `fadeOut.onComplete`. Форма зірки — `ShapeGeometry` з 4-промінним контуром (outer/inner radius alternating), не sprite-атлас (немає такого ассету в проєкті). Анімація: pop-in (`Back.Out`) -> hold -> fade-out, через `TweenC` (обидві ланки чейну додані окремо — той самий Day 6 баг з `.chain()` враховано одразу).