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
@@ -0,0 +1,104 @@
# HUD-лічильник + перенаправлення анімацій (2026-08-12)
Користувач попросив: (1) справжню 2D UI-іконку ресурсу з лічильником у
правому верхньому куті, "як на скріні" (темна округла табличка + іконка в
круглій рамці зліва + число справа), (2) ресурс замість миттєвого
зникнення має анімовано летіти ДО цього лічильника, (3) політ у Pay Zone
має бути ВІД лічильника до зони (не від гравця).
## Іконка
Витягнув сам PNG/WebP з тієї ж текстури `Icon_Wood`, яку вже юзає 3D-меш
`UI_Wood` (`src/resources/meshes/ZombiePunk_Map.glb`, `bufferView` з
`images[6]`) — маленьким одноразовим Node-скриптом (парсинг glb JSON-чанка,
пошук `images[].name === "Icon_Wood"`, зріз байтів з BIN-чанка за
`bufferViews[bufferView]`). Зберіг як `src/resources/images/icon_wood.webp`
(webp з альфа-каналом, 512×512). Новий `src/resources/images/images.ts`
експортує `iconWoodSrc = ConvertToBase64WhenRelease("./icon_wood.webp")` —
**той самий паттерн, що і `meshes.ts`** (той же helper з `@hitplay/ads_common`,
викликаний з файлу в ТІЙ САМІЙ директорії, що й ассет — важливо: цей
хелпер в dev/build режимі просто рядково замінює провідний `.` на
`resources` (`ConvertToBase64WhenRelease.js`), а окремий AST-плагін
(`convertToBase64InAST`), підключений через `defineConfigTemplate` в
`vite.config.js`, на build-time переписує сам виклик у справжній inline
`data:...;base64,...` — це працює для ВСІХ режимів (dev/build/export), не
тільки для "export").
## HudC.ts — новий контролер
DOM-паттерн підглянутий у `InstallBanner` з SDK (немає спільного
DOM-builder helper-а в пакетах, усе руками через `document.createElement`
+ власний `<style>`, що вставляється в `<head>`): статичний клас, монтує
DOM-елемент у `#ui` (SDK-контейнер, `position:absolute`, зафіксований
9:16 blocks — `calc(100vh*9/16)` × `100vh`, центрований — це і є "екран
гри", не весь браузер, тож `top/right` у % рахуються відносно НЬОГО).
`.resource-hud { pointer-events:none }` — щоб не перехоплював тач/клік від
джойстика під ним. Стиль — власний CSS, вигаданий (в проєкті НЕ було
жодного готового "плата/банер" стилю для запозичення — перевірено, в
`ui.css`/`main.css` нуль хітів на `border-radius`).
Другий обов'язок `HudC` — **світова точка** (`worldAnchor`, порожній
`Object3D`, дитина камери через `CameraC_internal.getCamera().add(...)`,
локальний офсет — спочатку `(1.0, 0.8, -2.2)`, потім підкручено до
`(1.0, 1.2, -2.0)` (див. нижче, чому)), яка приблизно проєктується туди,
де візуально сидить DOM-іконка. **Це не точний screen-to-world розрахунок**
(FOV/aspect не враховані математично) — просто підібраний на око офсет;
якщо HUD переїде/зміниться розмір екрану, можливо треба підкрутити
координати ще раз. `getWorldAnchorPosition()` — це і є точка, куди тепер
летять ресурси (`ResourceC.setFlyTarget`) і звідки стартує депозит-політ
(`PayZoneC.playDepositFlight`).
## Що змінилось у ResourceC/PayZoneC
- `ResourceC`: `scatter` -> settle -> (якщо `flyTarget` заданий, а тепер
він завжди заданий — `HudC.getWorldAnchorPosition`) новий стейт
`toTarget` (0.5с, smoothstep + згасаюча дуга вгору, прямо до лічильника)
-> `collect()`. Без `flyTarget` — стара миттєва поведінка (fallback).
**На відміну від попередньої версії — НЕ телепортується на позицію
гравця перед польотом** (той крок був заточений під "летить З гравця",
зараз відповідь користувача про сам HUD не згадувала цей крок, тож
прибрав його — політ іде прямо з місця, де ресурс осів, до лічильника).
- `PayZoneC.playDepositFlight`: `from` тепер `HudC.getWorldAnchorPosition()`
замість позиції гравця — "від лічильника до пей зони", як попросив
користувач. Тригер (proximity до зони) не змінився — усе ще потребує
стояння гравця в зоні, змінилась лише точка ВИЛЬОТУ візуалу.
Перевірено в браузері (Playwright, scratchpad): іконка рендериться коректно
(512×512 webp, валідний `data:` URI, `naturalWidth`/`naturalHeight` не 0),
лічильник в DOM оновлюється синхронно з `console.log`-ами збору (звірено
скріншотом — "8" на екрані == 8-й `[ResourceC] gathered wood` в консолі).
## Доопрацювання: лічильник має бути "живим балансом", не lifetime-total (2026-08-12)
Спочатку `HudC` показував `ResourceC.getCollectedCount()` напряму — тобто
lifetime-суму, яка ТІЛЬКИ росте (депозит в Pay Zone на неї не впливав).
Користувач попросив: число має рости при зборі І **зменшуватись при
депозиті** — тобто показувати поточний "баланс", а не історичний тотал.
Розв'язання без нової мутабельної змінної (і без циклічного імпорту —
`PayZoneC` вже імпортує `HudC` для `getWorldAnchorPosition()`, тож
`HudC` імпортувати `PayZoneC` напряму означало б цикл):
- `PayZoneC.getDepositedCount()` — новий публічний гетер, повертає
внутрішній `deposited` (лічильник, який і раніше рахував "скільки вже
влетіло в зону", просто не був назовні доступний).
- `HudC.setBalanceGetter(fn)` — інжектований гетер (той самий паттерн, що
й `ResourceC.setFlyTarget`/`onDestroyed` в `BreakablePropC`), замінив
пряму залежність `HudC -> ResourceC`. `HudC` більше НЕ імпортує
`ResourceC` взагалі.
- `TestSceneC` зв'язує: `HudC.setBalanceGetter(() =>
ResourceC.getCollectedCount() - PayZoneC.getDepositedCount())` — чиста
похідна величина, рахується на льоту щокадру в `HudC.update()`, без
жодного окремого "decrement"-виклику. Росте коли `collected` росте
(щось зібрали), падає коли `deposited` росте (щось долетіло в зону) —
саме по собі, без спеціальної синхронізації.
Перевірено: зібрав 7 ресурсів (консоль: `gathered wood (7 total)`), весь
цей час стояв в Pay Zone -> усі 7 злились в зону протягом ~2.1с
(`DEPOSIT_INTERVAL=0.3` × 7) -> лічильник на екрані повернувся до **0**,
хоча `collected` лишився 7 — підтверджує, що баланс дійсно "живий", а не
lifetime-сума.
## Доопрацювання: точка вильоту ресурсів була занизько (2026-08-12)
`WORLD_ANCHOR_LOCAL_OFFSET` підняли з `(1.0, 0.8, -2.2)` до
`(1.0, 1.2, -2.0)` — більший Y (вище в camera-space) і трохи менший |Z|
(ближче до камери, тому той самий Y дає більше вертикальне зміщення на
екрані через перспективу) — все ще підібрано на око, не розраховано
математично з FOV/aspect. **Це найбільш "на око" підібрана частина
роботи — якщо після цього фіксу політ все ще не влучає точно в іконку,
підкрутити ці три числа ще раз** (в `HudC.ts`).