Files
onboarding-project/docs/worklog/07-hit-feedback-sparks-hpbar-damage-numbers.md
2026-08-14 15:03:43 +03:00

149 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Розширений 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/`,
ще не імпортований) — ймовірно призначений саме для цього.