# Розширений 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/`, ще не імпортований) — ймовірно призначений саме для цього.