Files
onboarding-project/docs/worklog/09-day7-vfx-weapon-trail.md
T
2026-08-14 15:03:43 +03:00

11 KiB
Raw Blame History

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() враховано одразу).