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
+110
View File
@@ -0,0 +1,110 @@
# 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()` враховано одразу).