Utility and polishing + bag fixes
This commit is contained in:
@@ -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()` враховано одразу).
|
||||
Reference in New Issue
Block a user