Document Day 7/8 work in CLAUDE.md (weapon trail, resource pickup redesign, HUD overhaul)
Was committed stale in the previous push — catches it up with WeaponTrailC, SparkleFxC pooling, ResourceC's bounce/flash/shrink/perspective-compensation redesign, the HudC UI overhaul, and PayZoneC's deposit-flight fixes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,9 +27,12 @@
|
|||||||
src/controllers/
|
src/controllers/
|
||||||
PlayerC.ts - джойстик-рух (nipplejs), AABB-колізії з "colliders",
|
PlayerC.ts - джойстик-рух (nipplejs), AABB-колізії з "colliders",
|
||||||
бій: неперервний AoE-swing по всіх breakable-цілях в
|
бій: неперервний AoE-swing по всіх breakable-цілях в
|
||||||
зоні ураження, перебивається рухом
|
зоні ураження (240°-конус, FACING_DOT_THRESHOLD=-0.5,
|
||||||
|
Day 7 — див. "Кут ураження"), перебивається рухом
|
||||||
TestSceneC.ts - завантажує ZombiePunk_Map.glb, /collider/i -> hidden+colliders,
|
TestSceneC.ts - завантажує ZombiePunk_Map.glb, /collider/i -> hidden+colliders,
|
||||||
ставить cannon Wall-тіла на все, крім Lootable-крейтів
|
ставить cannon Wall-тіла на все, крім Lootable-крейтів;
|
||||||
|
ініціалізує всі VFX/HUD-контролери (PropVfxC,
|
||||||
|
SparkleFxC, WeaponTrailC, HudC) в правильному порядку
|
||||||
CameraFollowC.ts - камера-слідкувач, obstacle-avoidance через Raycaster
|
CameraFollowC.ts - камера-слідкувач, obstacle-avoidance через Raycaster
|
||||||
по тому ж масиву colliders (НЕ торкались)
|
по тому ж масиву colliders (НЕ торкались)
|
||||||
PhysicsC.ts - PhysicsLayer enum, PhysicsBody (Box/Sphere+cannon Body
|
PhysicsC.ts - PhysicsLayer enum, PhysicsBody (Box/Sphere+cannon Body
|
||||||
@@ -37,26 +40,63 @@ src/controllers/
|
|||||||
BreakablePropC.ts - парсить групу "Lootable" в мапі, S1/S2/S3 damage-stages,
|
BreakablePropC.ts - парсить групу "Lootable" в мапі, S1/S2/S3 damage-stages,
|
||||||
2 хіти на стейдж, hit() advance stage / повне
|
2 хіти на стейдж, hit() advance stage / повне
|
||||||
знищення (tween-джус, Day 6), дропає ресурси
|
знищення (tween-джус, Day 6), дропає ресурси
|
||||||
ResourceC.ts - спавн ресурсів "розльотом" від цілі + автозбір
|
WeaponTrailC.ts - Day 7: hand-rolled screen-facing ribbon-шлейф за
|
||||||
(Day 5, незмінно) — НЕ знає про Pay Zone/гравця;
|
битою під час замаху (samples PlayerC.getWeaponAnchor(),
|
||||||
візуал — клон реального "UI_Wood" з мапи
|
обчислює "кінець бити" з реальної local bbox, не
|
||||||
(fallback-куб про запас), createVisual() — публічний
|
вгадує offset), тає/звужується за час, не тон-мап,
|
||||||
доступ до цього візуалу для інших контролерів
|
toggled через PlayerC.updateCombat (isAttacking)
|
||||||
PayZoneC.ts - Day 6: окремо зливає ResourceC.getCollectedCount() в
|
ResourceC.ts - Day 7: спавн ресурсу з крейта -> scatter -> 2 баунси
|
||||||
|
(відчутні, БЕЗ ротейту) -> на 2-му баунсі: білий
|
||||||
|
флеш (окремий additive-оверлей-меш, БЕЗ
|
||||||
|
squash-and-widen — чистий uniform shrink, тримається,
|
||||||
|
не пружинить назад) + SparkleFxC-вибух зірочок ->
|
||||||
|
staggered (по черзі, не купою) політ до
|
||||||
|
HudC.getWorldAnchorPosition() (ціль пере-семплюється
|
||||||
|
ЩОКАДРУ, не застигає, якщо камера рухається) з
|
||||||
|
компенсацією перспективи (див. RESOURCE_ICON_BASE_SCALE,
|
||||||
|
нижче) -> яскравіє й зникає рівно на іконці. НЕ знає
|
||||||
|
про Pay Zone/гравця; візуал — клон реального
|
||||||
|
"UI_Wood" з мапи; createVisual() — публічний доступ
|
||||||
|
до цього візуалу (без баунс/флеш начинки) для інших
|
||||||
|
контролерів (PayZoneC)
|
||||||
|
PayZoneC.ts - Day 6/8: окремо зливає ResourceC.getCollectedCount() в
|
||||||
Pay Zone (вузол мапи "UI_Interactive_Zone_02"), по
|
Pay Zone (вузол мапи "UI_Interactive_Zone_02"), по
|
||||||
одному ресурсу, тільки поки гравець стоїть в зоні;
|
одному ресурсу, тільки поки гравець стоїть в зоні;
|
||||||
зникає після RESOURCES_TO_CLOSE_ZONE доставлених
|
зникає після RESOURCES_TO_CLOSE_ZONE доставлених
|
||||||
(зараз 20, підкручено після Day 6 — див. секцію
|
(зараз 20); росте плаский fill-overlay квад по мірі
|
||||||
"прогрес-заповнення зони"); росте плаский
|
заповнення; політ депозиту (HudC.getWorldAnchorPosition()
|
||||||
fill-overlay квад по мірі заповнення; політ депозиту
|
-> Pay Zone) БЕЗ ротейту, з тією ж компенсацією
|
||||||
летить з HudC.getWorldAnchorPosition(), не з гравця
|
перспективи й базовим розміром, що й ResourceC
|
||||||
HudC.ts - лічильник ресурсу (іконка+число) у правому верхньому
|
(RESOURCE_ICON_BASE_SCALE, спільна константа —
|
||||||
куті, DOM-оверлей в #ui; володіє "світовою" точкою
|
інакше токен виглядає більшим за ресурс, що щойно
|
||||||
(дитина камери), куди летять зібрані ресурси і
|
був на тому ж місці), плюс окремий
|
||||||
звідки стартує політ депозиту в Pay Zone
|
DEPOSIT_FLIGHT_SCALE_MULTIPLIER для локального тюнінгу
|
||||||
|
HudC.ts - Day 8: лічильник ресурсу — реальний дизайнерський
|
||||||
|
ассет (resourceCounterBgSrc, Tool_15.webp з temp/,
|
||||||
|
планка вже вбудована в картинку, не окрема іконка),
|
||||||
|
DOM-оверлей в #ui, стилі в src/css/ui.css (не inline
|
||||||
|
<style>); getWorldAnchorPosition() — СПРАВЖНЯ
|
||||||
|
screen-to-world проєкція (getBoundingClientRect()
|
||||||
|
правого краю плейта + unproject через камеру), НЕ
|
||||||
|
вгаданий локальний офсет від камери (той підхід
|
||||||
|
видалений повністю, разом з WORLD_ANCHOR_LOCAL_OFFSET)
|
||||||
|
SparkleFxC.ts - Day 7: пул із 30 зіркових мешів/матеріалів (pop-in
|
||||||
|
Back.Out -> hold -> fade-out через TweenC), жодних
|
||||||
|
нових Mesh/Material під час гри — spawnBurst() бере
|
||||||
|
вільний слот з пулу; використовується ResourceC на
|
||||||
|
2-му баунсі
|
||||||
|
PropVfxC.ts - Day 6: завантажує дизайнерські quarks.art VFX-префаби
|
||||||
|
(JSON з temp/, sparks/dust/debris) для хіту/знищення
|
||||||
|
крейтів
|
||||||
ThreeC.ts/CameraC.ts - базовий сетап three.js/камери від SDK
|
ThreeC.ts/CameraC.ts - базовий сетап three.js/камери від SDK
|
||||||
resources/meshes/ - ZombiePunk_Map.glb, ZombiePunk_Character.glb
|
resources/meshes/ - ZombiePunk_Map.glb, ZombiePunk_Character.glb
|
||||||
resources/images/ - icon_wood.webp (Icon_Wood, витягнутий з мапи), images.ts
|
resources/images/ - resource_counter_bg.webp (Tool_15.webp з temp/,
|
||||||
|
дизайнерський плейт лічильника), images.ts
|
||||||
|
(icon_wood.webp/iconWoodSrc видалені — стали
|
||||||
|
осиротілими після заміни на resourceCounterBgSrc)
|
||||||
|
resources/vfx/ - VFX_Lootable_Hit.json, VFX_Lootable_Destroy.json
|
||||||
|
(quarks.art префаби, копії з temp/, реально
|
||||||
|
використовуються з коду — див. PropVfxC)
|
||||||
```
|
```
|
||||||
|
|
||||||
`TriggerC.ts` (beginContact/endContact -> onEnter/onExit) і кінематичне
|
`TriggerC.ts` (beginContact/endContact -> onEnter/onExit) і кінематичне
|
||||||
@@ -870,8 +910,11 @@ position прямо з GLB node transforms). Жодна з цих гіпотез
|
|||||||
- `ResourceC`/`BreakablePropC` — нові файли. `PlayerC.ts`/`TestSceneC.ts`/
|
- `ResourceC`/`BreakablePropC` — нові файли. `PlayerC.ts`/`TestSceneC.ts`/
|
||||||
`PhysicsC.ts` — модифіковані. `CameraFollowC.ts`/`ThreeC.ts` — не торкались.
|
`PhysicsC.ts` — модифіковані. `CameraFollowC.ts`/`ThreeC.ts` — не торкались.
|
||||||
`TriggerC.ts` — був створений і видалений в межах цієї ж сесії (див. "Файли").
|
`TriggerC.ts` — був створений і видалений в межах цієї ж сесії (див. "Файли").
|
||||||
- Ще не закомічено в git (working tree) — користувач сам вирішує коли
|
- Закомічено й запушено в `origin/master` 2026-08-12 (commit `4a22a9b`,
|
||||||
комітити.
|
"Add physics/combat foundation, resource pickup flow, VFX polish, and
|
||||||
|
HUD") — усе, що описано в цьому файлі до Day 8 включно, вже в репо.
|
||||||
|
`stats.html` (rollup-visualizer, артефакт білда) свідомо НЕ закомічено —
|
||||||
|
не вихідний код, генерується заново при білді.
|
||||||
- Якщо наступного разу знову треба буде правити combat: зона ураження —
|
- Якщо наступного разу знову треба буде правити combat: зона ураження —
|
||||||
та сама `INTERACTION_REACH`-аура, що й раніше використовувалась для
|
та сама `INTERACTION_REACH`-аура, що й раніше використовувалась для
|
||||||
single-target пошуку (`findNearbyObstacle`, лишився для косметичного
|
single-target пошуку (`findNearbyObstacle`, лишився для косметичного
|
||||||
@@ -908,3 +951,211 @@ position прямо з GLB node transforms). Жодна з цих гіпотез
|
|||||||
відсутність зайвих текстур навколо Pay Zone) — стан на кінець Day 5/6;
|
відсутність зайвих текстур навколо Pay Zone) — стан на кінець Day 5/6;
|
||||||
усе, що сталось після (HUD, камера, VFX, прогрес-бар зони), має власні
|
усе, що сталось після (HUD, камера, VFX, прогрес-бар зони), має власні
|
||||||
"Перевірено" нотатки нижче за текстом.
|
"Перевірено" нотатки нижче за текстом.
|
||||||
|
|
||||||
|
## 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()` враховано одразу).
|
||||||
|
|
||||||
|
## Day 8: UI — резорс-каунтер (2026-08-12)
|
||||||
|
|
||||||
|
Референс — скріншот іншої гри ("Zombie Invasion") з двома стековани
|
||||||
|
лічильниками (дерево + метал); у нас лише один тип ресурсу, тож зроблено
|
||||||
|
тільки один лічильник, без плейсхолдера під другий (за проханням
|
||||||
|
користувача, поки не підтверджено, що другий ресурс дійсно буде).
|
||||||
|
|
||||||
|
### Реальний дизайнерський ассет замість CSS-пілла
|
||||||
|
Користувач: "Tool_15 знайди цей файл в temp. Це юай для каунтера
|
||||||
|
ресурсу." `temp/Tool_15.webp` (184×71, темна округла плата з діагональною
|
||||||
|
деревʼяною планкою, вже вбудованою в саму картинку) — скопійований у
|
||||||
|
`src/resources/images/resource_counter_bg.webp`, підключений через
|
||||||
|
`images.ts` (`resourceCounterBgSrc`, той самий `ConvertToBase64WhenRelease`-
|
||||||
|
паттерн). Стара кругла іконка-рамка + окремий `<img>` (з `icon_wood.webp`)
|
||||||
|
прибрані повністю — планка вже частина фонового зображення.
|
||||||
|
`icon_wood.webp`/`iconWoodSrc` після цього стали осиротілими і видалені.
|
||||||
|
|
||||||
|
### Стилі винесені в `ui.css`, живлені реальним аспектом і CSS-змінною
|
||||||
|
Користувач: "Всі стилі для юай мають бути в css файлі, у нас є відповідний
|
||||||
|
файл вже" — весь inline `document.createElement("style")` з `HudC.ts`
|
||||||
|
прибраний, стилі перенесені в `src/css/ui.css` (вже підключений в
|
||||||
|
`index.html`). `HudC.ts` лишає в JS тільки `holder.style.backgroundImage`
|
||||||
|
(шлях до ассету відомий лише в runtime через build-пайплайн, статичний
|
||||||
|
CSS-файл цього не може). Розмір плейта: спочатку `width`+`height` окремо
|
||||||
|
в `vh` (руками підібрана пропорція 184:71) — користувач: "не подобається,
|
||||||
|
що задана і висота і ширина, давай контролювати через аспект рейтіо" ->
|
||||||
|
`aspect-ratio: 184/71` замість другого числа. Потім: CSS custom property
|
||||||
|
`--hud-width` на `.resource-hud`, і `font-size: calc(var(--hud-width) *
|
||||||
|
0.175)` замість фіксованого `vh` — користувач змінював `width`, шрифт не
|
||||||
|
слідував; тепер один параметр (`--hud-width`) керує і розміром плати, і
|
||||||
|
шрифтом (успадковується вниз по DOM).
|
||||||
|
|
||||||
|
### Дві CSS-анімації замість JS-коду (Day 8 технічна вимога)
|
||||||
|
"Use built-in animations/transitions instead of changing elements through
|
||||||
|
code" — (1) `.resource-hud` грає `resource-hud-enter` (fade+slide) один
|
||||||
|
раз при монтуванні, самостійно, без JS-таймингу; (2) `.resource-hud__plate`
|
||||||
|
(НЕ сам `.resource-hud` — окремо, дивись нижче чому) грає
|
||||||
|
`resource-hud-bump` при кожній зміні числа — `HudC.update()` лише
|
||||||
|
toggle-ить клас `is-bumping` (remove -> force reflow через `offsetWidth`
|
||||||
|
-> add), сам keyframe рахує браузер.
|
||||||
|
|
||||||
|
**⚠️ Знахідка: анімацію не можна вішати на ТОЙ САМИЙ елемент, що вже має іншу**
|
||||||
|
Спочатку bump-анімація була на `.resource-hud__count` (сам текст) —
|
||||||
|
користувач: "Анімація має бути на всю юайку не тільки на каунтер". Але
|
||||||
|
просто перенести на `.resource-hud` (той самий елемент, що вже грає
|
||||||
|
`resource-hud-enter`) НЕ можна: перемикання класу, що змінює
|
||||||
|
`animation-name` (навіть тимчасово, назад до того самого значення),
|
||||||
|
ЗАВЖДИ перезапускає анімацію — тобто кожен збір ресурсу знову
|
||||||
|
проганяв би fade+slide-in ("з'явлення") поверх бампу. **Фікс**: два
|
||||||
|
вкладені div — зовнішній `.resource-hud` (позиція/розмір + enter-анімація,
|
||||||
|
статична), внутрішній `.resource-hud__plate` (фон+число разом,
|
||||||
|
bump-анімація) — дві незалежні `animation`-лінії на двох елементах.
|
||||||
|
|
||||||
|
### `getWorldAnchorPosition()` — справжня screen-to-world проєкція
|
||||||
|
Раніше (Day "HUD-лічильник"): фіктивний `Object3D`, дитина камери, з
|
||||||
|
вгаданим локальним офсетом (`WORLD_ANCHOR_LOCAL_OFFSET`, підбирався на
|
||||||
|
око, документований як "не точний screen-to-world розрахунок").
|
||||||
|
Користувач: "Треба дістати позицію правого края юайки і направляти
|
||||||
|
ресурси саме туди." **Фінал**: `getBoundingClientRect()` правого краю
|
||||||
|
`.resource-hud__plate` (реальний DOM), конвертація в NDC відносно
|
||||||
|
`ThreeC.renderer.domElement.getBoundingClientRect()` (canvas, НЕ
|
||||||
|
`window.innerWidth/Height` — SDK рендерить у letterboxed 9:16 бокс,
|
||||||
|
canvas-rect гарантовано збігається з тим, що бачить камера),
|
||||||
|
`new Vector3(ndcX,ndcY,0.5).unproject(camera)` -> напрямок від камери ->
|
||||||
|
точка на фіксованій `ANCHOR_DISTANCE=2.5` вздовж цього променя.
|
||||||
|
Рахується ЖИВО при кожному викликі (не раз при init) — автоматично
|
||||||
|
лишається коректним при resize/зміні орієнтації, без окремого resize-
|
||||||
|
слухача. `Object3D`/`WORLD_ANCHOR_LOCAL_OFFSET`-підхід видалений
|
||||||
|
повністю.
|
||||||
|
|
||||||
|
## PayZoneC: уніфікація депозит-флайту з ResourceC (2026-08-12)
|
||||||
|
|
||||||
|
`playDepositFlight` (політ токена з HUD в Pay Zone) використовував
|
||||||
|
окремий TweenJS-based шлях, не звʼязаний з новою логікою `ResourceC`:
|
||||||
|
мав ротейт (`mesh.rotation.y += 0.3`) і стартовий `scale=1` (дефолт
|
||||||
|
свіжого `ResourceC.createVisual()`, без жодного стиску).
|
||||||
|
|
||||||
|
- **Ротейт прибрано** повністю (користувач: "ротейту не має бути").
|
||||||
|
- **Компенсація перспективи** — та ж сама техніка, що й у
|
||||||
|
`ResourceC.toTarget`, але в ПРОТИЛЕЖНИЙ бік: цей політ стартує БЛИЗЬКО
|
||||||
|
до камери (HUD-анкер) і летить ДАЛІ (Pay Zone, зазвичай далі від
|
||||||
|
камери) — без компенсації токен би візуально ЗМЕНШУВАВСЯ на льоту.
|
||||||
|
- **⚠️ Знайдений і виправлений баг: токен був значно більшим за ресурс з наземної анімації**
|
||||||
|
Причина: `ResourceC.createVisual()` не проходить через `SHRINK_AMOUNT`-
|
||||||
|
стиск (той стосується лише "живих" `FlyingResource` з баунсом) — токен
|
||||||
|
стартував з `scale=1`, тоді як ресурс, який щойно долетів на те саме
|
||||||
|
місце з `ResourceC`, виглядав як `0.85`. **Фікс**: `RESOURCE_ICON_BASE_SCALE
|
||||||
|
= 1 - SHRINK_AMOUNT` (0.85) винесений з `ResourceC.ts` як `export`,
|
||||||
|
`PayZoneC` множить на нього. Плюс окремий `DEPOSIT_FLIGHT_SCALE_MULTIPLIER`
|
||||||
|
(зараз `1`, користувач підкрутив до `0.3` в UI-сесії) — локальний
|
||||||
|
тюнер розміру САМЕ цього польоту, не торкаючись спільної константи
|
||||||
|
(яка вплинула б і на "земля->юай" анімацію).
|
||||||
|
- **Урок**: коли два різні контролери візуально показують "той самий"
|
||||||
|
ресурс в різних сценах польоту — тримати спільну константу базового
|
||||||
|
розміру (`export`), а не дублювати магічне число, інакше вони
|
||||||
|
розходяться, як тут.
|
||||||
|
|||||||
Reference in New Issue
Block a user