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,148 @@
# Розширений hit-feedback на крейтах: спарки, HP-бар, числа урону (2026-08-12)
Користувач показав референс-скріншоти з іншої гри (damage-numbers, HP-бар,
іскри в точці контакту) і сказав, що зараз у нас із фідбеку на хіт **лише
невеликий нахил** — флеш кольору, який мав бути з Day 6, візуально не
працює. Плейл поступово (по одному ефекту, з перевіркою між кроками).
## ⚠️ Знайдений і виправлений баг: флеш кольору був тихим no-op з самого Day 6
`playHitFeedback` лерпив `material.color` від `baseColor` до `FLASH_COLOR`
(білий) — але залогувавши реальний матеріал crate-мешів (`MeshBasicMaterial`,
`color: #ffffff`, без `emissive` — це не Standard/Phong), виявилось, що
**`baseColor` вже й був чистим білим**. Текстура дає весь реальний колір,
тінт-колір лишався на дефолтному білому — лерп "білий -> білий" не змінює
візуально НІЧОГО, tween виконувався справно (перевірено таймингом), просто
результат був непомітний. `MeshBasicMaterial` — unlit, `color` множиться на
текстуру, тож "яскравіше за білий" через цей канал взагалі неможливо.
**Виправлення**: замість лерпу `.color` — `StageVisual.flashMesh`, клон тієї
самої mesh-геометрії (`mesh.clone()`), покладений зверху з власним
`MeshBasicMaterial({ color: FLASH_COLOR, transparent: true, opacity: 0,
blending: AdditiveBlending, depthWrite: false })`. Флеш тепер анімує
`flashMesh.material.opacity` (0→1→0) замість кольору базового меша —
працює незалежно від того, який в оригінала колір/текстура, бо це
адитивний шар зверху, а не множник. **Побічний ефект**: `flashMesh` — це
ще один mesh під `map`, тож `TestSceneC`'s `ThreeC.setShadowsStateForChildren
(map, true, true)` (виконується ПІСЛЯ `BreakablePropC.init()`) увімкнув би
йому shadows теж — додано `BreakablePropC.disableFlashShadows()`, викликаний
з `TestSceneC` одразу після цього рядка, щоб вимкнути назад (даремний
shadow-caster на майже завжди прозорому оверлеї).
Перевірено (Playwright, серія скріншотів кожні 80мс під час бою): крейт у
кадрі точно на позначці очікуваного хіта помітно біліший/яскравіший, ніж у
кадрах до/після — флеш реально видно, на відміну від попередньої версії.
**Урок**: тінт-колір (`material.color`) на unlit-текстурованому меші не дає
"флешу", якщо колір вже білий (звичайний дефолт) — для видимого
hit-flash-у на такому матеріалі потрібен окремий адитивний оверлей-шар, а
не лерп самого кольору. Якщо десь ще знадобиться подібний ефект на
текстурованому unlit-меші — той самий підхід (clone + additive overlay).
## Іскри в точці контакту — v1 hand-rolled, замінено (2026-08-12)
Перша версія — новий `SparkFxC.ts`, ручно зібраний `ParticleSystem` (білі
квадратні частинки, `SphereEmitter(radius:0.05)`, звичайний `BillBoard`).
Технічно працював (Playwright підтвердив спалах у момент хіта), але
користувач подивився і сказав: **"Іскри не підходять, це мають бути як
промені. Саме такі як на скріні"** — потрібні тонкі промені-стріки, не
блоб з крапок. Перебудував рендер на `RenderMode.StretchedBillBoard`
(розтягує квад уздовж власної швидкості частинки — це і дає "промінь", а
не крапку) + `speedFactor`, `ColorOverLife`+`Gradient` для згасання альфи.
Це вже виглядало краще, але тут користувач відкрив `temp/VFX_Lootable_Destroy.json`
у редакторі й запитав, що в цих файлах — і виявилось, що це змінює все.
## ⚠️ Знахідка: в `temp/` лежали готові дизайнерські VFX-префаби, повністю невикористані
`temp/VFX_Lootable_Hit.json`, `temp/VFX_Lootable_Destroy.json` (+
`PassionOne-Black.otf` — шрифт, ще не використаний, ймовірно для майбутніх
чисел урону/HUD) — це **справжні експорти з редактора quarks.art**
(`Object3D.toJSON()` на `Group` з `ParticleEmitter`-дітьми, кожен зі своїм
готовим, підібраним дизайнером `ps` — shape/speed/color/behaviors, і
реальними текстурами, вбудованими як base64: `cfxr stretch smoke arc
dissolve.webp`, `novalines.webp` — назва "novalines" **буквально "лінії
нової" = промені-риски**, це саме те, що користувач мав на увазі скріном).
`VFX_Lootable_Hit` = 2 емітери ("StretchSmoke" + "SharpImpact" — це і є
ray-burst). `VFX_Lootable_Destroy` = 4 емітери (дим, уламки-Pieces,
Ground_Dirt як реальна 3D-mesh геометрія (`renderMode: Mesh`), пилова хмара).
**Жодного SDK/іншого коду проєкту ці файли не використовували взагалі** —
чисто сирі ассети, що чекали на підключення. Замінив увесь ручний
`SparkFxC` на завантаження цих реальних префабів — жодна власноруч
підібрана крива/швидкість не заміняє справжній дизайнерський ассет.
**Що таке VFX**: "visual effects" — тут конкретно означає one-shot
частинкові ефекти (спалахи/дим/уламки), авторовані окремо в
редакторі quarks.art і експортовані як самодостатній JSON (геометрія +
матеріали + текстури-base64 + вже налаштовані `ParticleSystem` параметри
для кожного емітера) — не код, чистий даних-ассет, який рушій (`three.quarks`)
вміє розпарсити назад у робочі `Object3D`.
## `PropVfxC.ts` — новий контролер, замінив `SparkFxC.ts`
Завантажує обидва префаби ОДИН РАЗ при `init()` через сирий
`QuarksLoader` (`three.quarks`) + `Template3d.manager` (`@hitplay/playable_template`),
**НЕ** через SDK-хелпер `quarksLoader()`/`ConvertToBase64WhenRelease`
(паттерн `meshes.ts`/`images.ts`) — свідомо, з причини:
**⚠️ Чому не той самий паттерн, що і images/meshes**: `quarksLoader(base64String)`
жорстко очікує рядок формату `data:...;base64,XXXX` (робить
`base64String.split(",")[1]` потім `atob(...)`). Це працює лише ПІСЛЯ
build-time AST-трансформації (`convertToBase64InAST`, підключена в
`vite.config.js`), яка й замінює виклик `ConvertToBase64WhenRelease()` на
реальний base64 рядок. У **звичайному `vite dev`** (як я весь час тестую
через Playwright!) сам хелпер — лише прохідний passthrough, що повертає
ГОЛИЙ ШЛЯХ (`"resources/vfx/....json"`) без коми — `.split(",")[1]` дав би
`undefined`, і `atob(undefined)` зламав би завантаження саме в dev-режимі,
хоча в build/export він працював би. Замість цього ризику — звичайний
Vite JSON-імпорт (`import json from "../resources/vfx/VFX_Lootable_Hit.json"`),
який Vite вміє інлайнити нативно в БУДЬ-ЯКОМУ режимі (dev/build/export)
без жодного fetch — і сирий `new QuarksLoader(Template3d.manager).parse(json)`
(синхронний, повертає `Object3D` напряму, `onLoad` callback не потрібен,
бо всі текстури вже base64 в самому JSON, немає мережевого чекання).
Додав `"resolveJsonModule": true` в `tsconfig.json` (був відсутній) — без
цього `tsc` (не сам vite/esbuild — ті й так все розуміють) скаржився б на
JSON-імпорт типами.
`PropVfxC.spawnHit(point)`/`spawnDestroy(point)` — клонують відповідний
завантажений шаблон (`.clone()` на `Group`), виставляють `position`,
форсують `QuarksUtil.setAutoDestroy(instance, true)` (в оригінальних
префабах `autoDestroy: false` — дизайнер, ймовірно, керував завершенням
вручну в своєму інструменті; нам потрібен чистий "one-shot і забути"),
`QuarksUtil.addToBatchRenderer(instance, renderer)` (реєструє КОЖЕН
`ParticleEmitter` в дереві з рендерером — без цього нічого не малюється),
`QuarksUtil.play(instance)`. Кожен `ParticleSystem` з `autoDestroy`
прибирає СЕБЕ (свій `ParticleEmitter`-нод) з дерева, коли його частинки
померли — але порожній корінь-`Group`, який я додав в сцену, сам не
видаляється (нема кому), тож прибираю його окремим `setTimeout`
(`CLEANUP_DELAY_MS=1500`, з запасом під найдовший `duration+life` в обох
префабах).
`BreakablePropC.playHitFeedback` кличе `PropVfxC.spawnHit(contactPoint)` на
кожному хіті (як і раніше з `SparkFxC`); `BreakablePropC.destroy()`
додатково кличе `PropVfxC.spawnDestroy(...)` в позиції крейта — цього не
було в hand-rolled версії взагалі (не було Destroy-ефекту), додав одразу,
бо це буквально друга половина того самого знайденого ассета.
## ⚠️ Знахідка (потребує рішення користувача, ще не виправлено): "StretchSmoke"-шар вилітає далеко від крейта і виглядає як артефакт
Перевірено в браузері (Playwright): "SharpImpact" (промені/nova-спалах,
`novalines.webp`) рендериться коректно ПРЯМО на крейті в момент хіта —
саме той ефект, що відповідає скріну користувача. Але другий емітер того
самого префаба, "StretchSmoke" (`startSpeed:14`, `worldSpace:true`, життя
0.2-0.3с — тобто до ~4 world units прольоту по прямій!), у нашій сцені
летить ДАЛЕКО від крейта (видно на скріні як сірувата паличка десь у
верхній частині екрана, повністю відірвана від точки удару) — швидше за
все, дизайнер тюнив цей ассет під інший масштаб сцени/камери. Це
відтворюється на КОЖНОМУ хіті (не витік/застигла частинка — просто кожен
новий хіт заново вилітає в приблизно те саме "неправильне" місце).
**Ще не виправлено** — варіанти: (а) лишити як є (автентичний ассет), (б)
притлумити цей конкретний емітер (зменшити `startSpeed`/життя клоном
префаба перед грою), (в) прибрати "StretchSmoke" зовсім і лишити тільки
"SharpImpact". Потребує візуальної перевірки користувачем — не вирішував
сам, це пряма зміна дизайнерського ассета.
Destroy VFX (`VFX_Lootable_Destroy`, 4 емітери включно з mesh-based
"Ground_Dirt") підключений і не кидає помилок в консолі, але **ще не
підтверджений візуально** — не вдалось надійно спричинити повне
знищення крейта в тестовому прогоні за відведений час.
## Наступні кроки (ще не реалізовано, чекає на пріоритети користувача)
HP-бар (потребує рішення: дискретні стейджі vs. реальний HP-пул?) і числа
урону (DOM чи sprite в 3D-просторі? теж потребує реального numeric HP,
якого зараз нема) — ще не почато. `PassionOne-Black.otf` (в `temp/`,
ще не імпортований) — ймовірно призначений саме для цього.