diff --git a/app/main/effects/effectsStore.test.ts b/app/main/effects/effectsStore.test.ts index a089d0e..bbba108 100644 --- a/app/main/effects/effectsStore.test.ts +++ b/app/main/effects/effectsStore.test.ts @@ -64,7 +64,7 @@ void test('pruneExpired: луч света удаляется после lifetim assert.equal(store.getState().instances.length, 0); }); -void test('pruneExpired: облако яда удаляется после lifetime', () => { +void test('pruneExpired: яд удаляется после lifetime', () => { const store = new EffectsStore(); store.dispatch({ kind: 'instance.add', diff --git a/app/main/effects/effectsStore.ts b/app/main/effects/effectsStore.ts index 38257d8..8f976e9 100644 --- a/app/main/effects/effectsStore.ts +++ b/app/main/effects/effectsStore.ts @@ -1,5 +1,6 @@ import crypto from 'node:crypto'; +import { applyFieldEffectEraserStroke } from '../../shared/fieldEffectEraser'; import type { EffectsEvent, EffectsState, EffectToolState } from '../../shared/types'; function nowMs(): number { @@ -35,7 +36,7 @@ export class EffectsStore { dispatch(event: EffectsEvent): EffectsState { const s = this.state; - const next: EffectsState = applyEvent(s, event); + const next: EffectsState = applyEvent(s, event, (prefix) => this.makeId(prefix)); this.state = next; return next; } @@ -47,13 +48,18 @@ export class EffectsStore { const kept = this.state.instances.filter((i) => { // Пятно льда не истекает по таймеру (только «очистить все» или ластик в UI). if (i.type === 'ice' || i.type === 'shadow') return true; - if (i.type === 'lightning' || i.type === 'sunbeam' || i.type === 'poisonCloud' || i.type === 'darkness') { + if ( + i.type === 'lightning' || + i.type === 'sunbeam' || + i.type === 'poisonCloud' || + i.type === 'darkness' + ) { return now - i.createdAtMs < i.lifetimeMs; } if (i.type === 'scorch') { return now - i.createdAtMs < i.lifetimeMs; } - if (i.type === 'fog' || i.type === 'water') { + if (i.type === 'fog' || i.type === 'fire' || i.type === 'rain' || i.type === 'water') { if (i.lifetimeMs === null) return true; return now - i.createdAtMs < i.lifetimeMs; } @@ -74,7 +80,11 @@ export class EffectsStore { } } -function applyEvent(state: EffectsState, event: EffectsEvent): EffectsState { +function applyEvent( + state: EffectsState, + event: EffectsEvent, + makeId: (prefix: string) => string, +): EffectsState { const bump = (patch: Omit): EffectsState => ({ ...patch, revision: state.revision + 1, @@ -89,6 +99,11 @@ function applyEvent(state: EffectsState, event: EffectsEvent): EffectsState { return bump({ ...state, instances: [...state.instances, event.instance] }); case 'instance.remove': return bump({ ...state, instances: state.instances.filter((i) => i.id !== event.id) }); + case 'field.erase': + return bump({ + ...state, + instances: applyFieldEffectEraserStroke(state.instances, event.points, event.radiusN, makeId), + }); default: { // Exhaustiveness // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/app/renderer/control/ControlApp.tsx b/app/renderer/control/ControlApp.tsx index 564d774..5dbdde5 100644 --- a/app/renderer/control/ControlApp.tsx +++ b/app/renderer/control/ControlApp.tsx @@ -19,8 +19,8 @@ import { getFreezeEffectLifeMs, playFreezeEffectSound } from './freezeSfx'; import { getPoisonCloudEffectLifeMs, playPoisonCloudEffectSound } from './poisonCloudSfx'; import { getSunbeamEffectLifeMs, playSunbeamEffectSound } from './sunbeamSfx'; -/** Длительность визуала молнии (мс). */ -const LIGHTNING_EFFECT_MS = 180; +/** Длительность молнии: быстрый удар + акцент в точке попадания. */ +const LIGHTNING_EFFECT_MS = 840; function formatTime(sec: number): string { if (!Number.isFinite(sec) || sec < 0) return '0:00'; @@ -154,8 +154,7 @@ export function ControlApp() { const project = session?.project ?? null; const currentGraphNodeId = project?.currentGraphNodeId ?? null; - const currentHistoryIdx = - currentGraphNodeId != null ? history.lastIndexOf(currentGraphNodeId) : -1; + const currentHistoryIdx = currentGraphNodeId != null ? history.lastIndexOf(currentGraphNodeId) : -1; const currentScene = project && session?.currentSceneId ? project.scenes[session.currentSceneId] : undefined; const isVideoPreviewScene = currentScene?.previewAssetType === 'video'; @@ -660,6 +659,16 @@ export function ControlApp() { return { x: Math.max(0, Math.min(1, x)), y: Math.max(0, Math.min(1, y)) }; } + function dispatchFieldEraserPoints(points: { x: number; y: number; tMs: number }[]): void { + if (points.length === 0) return; + void fx.dispatch({ kind: 'field.erase', points, radiusN: toolRef.current.radiusN }); + } + + function tryEraseActionEffect(p: { x: number; y: number }): void { + const id = pickEraseTargetId(fxState?.instances ?? [], p, toolRef.current.radiusN); + if (id) void fx.dispatch({ kind: 'instance.remove', id }); + } + async function commitStroke(): Promise { if (isVideoPreviewScene) { brushRef.current = null; @@ -669,6 +678,10 @@ export function ControlApp() { if (!fxState) return; const b = brushRef.current; if (!b) return; + if (b.tool === 'eraser') { + brushRef.current = null; + return; + } const createdAtMs = Date.now(); const seed = Math.floor(Math.random() * 1_000_000_000); @@ -697,8 +710,7 @@ export function ControlApp() { createdAtMs, points: b.points, radiusN: tool.radiusN, - // Огонь визуально ярче, но всё равно ограничиваемся безопасными пределами. - opacity: Math.max(0.12, Math.min(0.95, tool.intensity)), + opacity: 1, lifetimeMs: null, }, }); @@ -790,8 +802,8 @@ export function ControlApp() { createdAtMs, start, end, - widthN: Math.max(0.01, tool.radiusN * 0.9), - intensity: Math.max(0.9, Math.min(1.2, tool.intensity * 1.35)), + widthN: Math.max(0.014, tool.radiusN * 1.15), + intensity: Math.max(1, Math.min(1.4, tool.intensity * 1.45)), lifetimeMs: LIGHTNING_EFFECT_MS, }, }); @@ -800,12 +812,12 @@ export function ControlApp() { instance: { id: `sc_${String(createdAtMs)}_${String(seed)}`, type: 'scorch', - seed: seed ^ 0x5a5a5a, + seed: seed ^ 0x7a7a7a, createdAtMs, at: end, - radiusN: Math.max(0.03, tool.radiusN * 0.625), - opacity: 0.92, - lifetimeMs: 60_000, + radiusN: Math.max(0.04, tool.radiusN * 0.82), + opacity: 0.96, + lifetimeMs: 90_000, }, }); playLightningEffectSound(); @@ -913,7 +925,7 @@ export function ControlApp() { createdAtMs, points: b.points, radiusN: tool.radiusN, - opacity: Math.max(0.12, Math.min(0.75, tool.intensity * 0.85)), + opacity: 1, lifetimeMs: null, }; } @@ -964,8 +976,8 @@ export function ControlApp() { createdAtMs, start: { x: last.x, y: 0 }, end: { x: last.x, y: last.y }, - widthN: Math.max(0.01, tool.radiusN * 0.9), - intensity: Math.max(0.9, Math.min(1.2, tool.intensity * 1.35)), + widthN: Math.max(0.014, tool.radiusN * 1.15), + intensity: Math.max(1, Math.min(1.4, tool.intensity * 1.45)), lifetimeMs: LIGHTNING_EFFECT_MS, }; } @@ -1154,7 +1166,9 @@ export function ControlApp() { iconOnly title={t('control.darkness')} ariaLabel={t('control.darkness')} - onClick={() => void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'darkness' } })} + onClick={() => + void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'darkness' } }) + } > 🌑 @@ -1288,11 +1302,7 @@ export function ControlApp() { } /> {previewContentRect ? ( - + ) : null}
= minStep * minStep) { + b.points.push(pt); + dispatchFieldEraserPoints([last, pt]); + } + } + } + tryEraseActionEffect(p); return; } const b = brushRef.current; diff --git a/app/renderer/control/controlApp.effectsPanel.test.ts b/app/renderer/control/controlApp.effectsPanel.test.ts index 7d3f722..a5eebf0 100644 --- a/app/renderer/control/controlApp.effectsPanel.test.ts +++ b/app/renderer/control/controlApp.effectsPanel.test.ts @@ -18,6 +18,7 @@ void test('ControlApp: звук молнии (public/molniya.mp3)', () => { const src = readControlApp(); assert.ok(src.includes('molniya.mp3')); assert.ok(src.includes('playLightningEffectSound')); + assert.ok(src.includes('LIGHTNING_EFFECT_MS')); }); void test('ControlApp: звук заморозки (public/zamorozka.mp3)', () => { @@ -56,7 +57,9 @@ void test('ControlApp: эффекты в пульте, иконки с тулт assert.ok(src.includes('SceneDarknessOverlay')); assert.ok(src.includes('useSceneDarknessState')); assert.ok(src.includes("t('control.sunbeam')")); + assert.ok(src.includes("t('control.lightning')")); assert.ok(src.includes("title={t('control.water')}")); + assert.ok(src.includes("title={t('control.fire')}")); assert.ok(src.includes("title={t('control.darkness')}")); assert.ok(src.includes("title={t('control.poisonCloud')}")); assert.ok(src.includes("title={t('control.fog')}")); diff --git a/app/renderer/editor/i18n/editorMessages.ts b/app/renderer/editor/i18n/editorMessages.ts index cf8d80b..ee58427 100644 --- a/app/renderer/editor/i18n/editorMessages.ts +++ b/app/renderer/editor/i18n/editorMessages.ts @@ -187,7 +187,7 @@ export const EDITOR_MESSAGES: Record> = { 'help.section.effects.title': 'Эффекты поля и действий', 'help.section.effects.body': - 'Эффекты работают на сценах с картинкой, не на видео. Рисуйте в «Предпросмотр экрана» — игроки увидят то же на презентации.\n\nВыберите инструмент слева:\n• Эффекты поля (туман, дождь, огонь, вода) — зажмите левую кнопку и ведите по карте.\n• Эффекты действий (молния, луч света, заморозка, тьма, облако яда) — короткий клик или штрих; у некоторых есть звук.\n\nЕсли у сцены в свойствах включено «Затемнить сцену», появится блок «Управление затемнением» с «Кистью Открытия» 🔦. Водите по предпросмотру — тьма снимается на обоих экранах сразу. У игроков нераскрытое остаётся чёрным, у вас на пульте — полузатемнённым. Уже открытые участки сохраняются, пока идёт показ и вы снова попадаете на ту же карточку сцены на карте. Это не то же самое, что эффект «Тьма» 🌑 в блоке действий.\n\nЛастик 🧹 — клик или проведение по эффекту, чтобы убрать. «Очистить эффекты» — снять всё сразу.\n\n«Радиус кисти» под панелью — чем больше число, тем шире мазок.', + 'Эффекты работают на сценах с картинкой, не на видео. Рисуйте в «Предпросмотр экрана» — игроки увидят то же на презентации.\n\nВыберите инструмент слева:\n• Эффекты поля (туман, дождь, огонь, вода) — зажмите левую кнопку и ведите по карте.\n• Эффекты действий (молния, луч света, заморозка, тьма, облако яда) — короткий клик или штрих; у некоторых есть звук.\n\nЕсли у сцены в свойствах включено «Затемнить сцену», появится блок «Управление затемнением» с «Кистью Открытия» 🔦. Водите по предпросмотру — тьма снимается на обоих экранах сразу. У игроков нераскрытое остаётся чёрным, у вас на пульте — полузатемнённым. Уже открытые участки сохраняются, пока идёт показ и вы снова попадаете на ту же карточку сцены на карте. Это не то же самое, что эффект «Тьма» 🌑 в блоке действий.\n\nЛастик 🧹 — для эффектов поля (туман, дождь, огонь, вода) водите кистью, как «Кистью Открытия» для затемнения: стирается только пройденный участок. Эффекты действий (молния, луч и т.д.) убираются целиком при клике или проведении по ним. «Очистить эффекты» — снять всё сразу.\n\n«Радиус кисти» под панелью — чем больше число, тем шире мазок.', 'help.section.presentation.title': 'Экран презентации', 'help.section.presentation.body': @@ -509,7 +509,7 @@ export const EDITOR_MESSAGES: Record> = { 'help.section.effects.title': 'Field and action effects', 'help.section.effects.body': - 'Effects work on image scenes, not video. Paint in Screen preview — players see the same on presentation.\n\nPick a tool on the left:\n• Field effects (fog, rain, fire, water) — hold the left button and brush on the map.\n• Action effects (lightning, sunbeam, freeze, darkness, poison cloud) — click or short stroke; some include sound.\n\nIf Darken scene is enabled in scene properties, a Darkness control section appears with the Opening brush 🔦. Brush on the preview to clear darkness on both screens at once. Unrevealed areas stay fully black for players and half-dark on your preview. Revealed areas are remembered while the show runs and you return to the same graph card. This is not the same as the Darkness 🌑 action effect.\n\nEraser 🧹 — click or drag over an effect to remove it. Clear effects removes everything at once.\n\nBrush radius under the panel — higher values mean a wider stroke.', + 'Effects work on image scenes, not video. Paint in Screen preview — players see the same on presentation.\n\nPick a tool on the left:\n• Field effects (fog, rain, fire, water) — hold the left button and brush on the map.\n• Action effects (lightning, sunbeam, freeze, darkness, poison cloud) — click or short stroke; some include sound.\n\nIf Darken scene is enabled in scene properties, a Darkness control section appears with the Opening brush 🔦. Brush on the preview to clear darkness on both screens at once. Unrevealed areas stay fully black for players and half-dark on your preview. Revealed areas are remembered while the show runs and you return to the same graph card. This is not the same as the Darkness 🌑 action effect.\n\nEraser 🧹 — for field effects (fog, rain, fire, water), brush like the Opening brush for darkness: only the stroke area is erased. Action effects (lightning, sunbeam, etc.) are removed whole when you click or drag over them. Clear effects removes everything at once.\n\nBrush radius under the panel — higher values mean a wider stroke.', 'help.section.presentation.title': 'Presentation screen', 'help.section.presentation.body': diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_001.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_001.webp new file mode 100644 index 0000000..295e79b Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_001.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_002.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_002.webp new file mode 100644 index 0000000..f74d3df Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_002.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_003.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_003.webp new file mode 100644 index 0000000..ec7e3f7 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_003.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_004.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_004.webp new file mode 100644 index 0000000..2b83aa6 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_004.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_005.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_005.webp new file mode 100644 index 0000000..d8e7ad1 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_005.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_006.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_006.webp new file mode 100644 index 0000000..682783a Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_006.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_007.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_007.webp new file mode 100644 index 0000000..75f29da Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_007.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_008.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_008.webp new file mode 100644 index 0000000..a98a75d Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_008.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_009.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_009.webp new file mode 100644 index 0000000..a722993 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_009.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_010.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_010.webp new file mode 100644 index 0000000..04105ce Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_010.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_011.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_011.webp new file mode 100644 index 0000000..9ae1e4a Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_011.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_012.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_012.webp new file mode 100644 index 0000000..9e52dfb Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_012.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_013.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_013.webp new file mode 100644 index 0000000..23c4470 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_013.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_014.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_014.webp new file mode 100644 index 0000000..501cbde Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_014.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_015.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_015.webp new file mode 100644 index 0000000..00bafdd Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_015.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_016.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_016.webp new file mode 100644 index 0000000..e0c9595 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_016.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_017.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_017.webp new file mode 100644 index 0000000..8aed8f0 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_017.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_018.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_018.webp new file mode 100644 index 0000000..07f5993 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_018.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_019.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_019.webp new file mode 100644 index 0000000..88bd167 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_019.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_020.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_020.webp new file mode 100644 index 0000000..e96ef9f Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_020.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_021.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_021.webp new file mode 100644 index 0000000..1962cbd Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_021.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_022.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_022.webp new file mode 100644 index 0000000..70f57a8 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_022.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_023.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_023.webp new file mode 100644 index 0000000..467fccc Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_023.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_024.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_024.webp new file mode 100644 index 0000000..1cb53e6 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_024.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_025.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_025.webp new file mode 100644 index 0000000..cc708ad Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_025.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_026.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_026.webp new file mode 100644 index 0000000..054b6f6 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_026.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_027.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_027.webp new file mode 100644 index 0000000..cd03b4f Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_027.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_028.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_028.webp new file mode 100644 index 0000000..7f49d0a Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_028.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_029.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_029.webp new file mode 100644 index 0000000..53846e0 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_029.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_030.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_030.webp new file mode 100644 index 0000000..bd58895 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_030.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_031.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_031.webp new file mode 100644 index 0000000..5ec27bd Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_031.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_032.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_032.webp new file mode 100644 index 0000000..7179272 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_032.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_033.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_033.webp new file mode 100644 index 0000000..51407be Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_033.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_034.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_034.webp new file mode 100644 index 0000000..6589e88 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_034.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_035.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_035.webp new file mode 100644 index 0000000..410aa1b Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_035.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_036.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_036.webp new file mode 100644 index 0000000..244ffb0 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_036.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_037.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_037.webp new file mode 100644 index 0000000..a8256d4 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_037.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_038.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_038.webp new file mode 100644 index 0000000..57dd199 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_038.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_039.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_039.webp new file mode 100644 index 0000000..6a58292 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_039.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_040.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_040.webp new file mode 100644 index 0000000..7d59b9f Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_040.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_041.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_041.webp new file mode 100644 index 0000000..434549a Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_041.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_042.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_042.webp new file mode 100644 index 0000000..e70cb01 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_042.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_043.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_043.webp new file mode 100644 index 0000000..f8e2478 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_043.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_044.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_044.webp new file mode 100644 index 0000000..666db69 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_044.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_045.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_045.webp new file mode 100644 index 0000000..839b765 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_045.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_046.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_046.webp new file mode 100644 index 0000000..67b4989 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_046.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_047.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_047.webp new file mode 100644 index 0000000..2c3d4bd Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_047.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_048.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_048.webp new file mode 100644 index 0000000..febd220 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_048.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_049.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_049.webp new file mode 100644 index 0000000..763804f Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_049.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_050.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_050.webp new file mode 100644 index 0000000..56f0afc Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_050.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_051.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_051.webp new file mode 100644 index 0000000..04bbbed Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_051.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_052.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_052.webp new file mode 100644 index 0000000..3bf2deb Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_052.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_053.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_053.webp new file mode 100644 index 0000000..591ec09 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_053.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_054.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_054.webp new file mode 100644 index 0000000..e77c39f Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_054.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_055.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_055.webp new file mode 100644 index 0000000..37e0dd2 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_055.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_056.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_056.webp new file mode 100644 index 0000000..b0f35c2 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_056.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_057.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_057.webp new file mode 100644 index 0000000..515c1e6 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_057.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_058.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_058.webp new file mode 100644 index 0000000..dea5562 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_058.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_059.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_059.webp new file mode 100644 index 0000000..9dd7291 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_059.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_060.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_060.webp new file mode 100644 index 0000000..c2deb8f Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_060.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_061.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_061.webp new file mode 100644 index 0000000..326fdf5 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_061.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_062.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_062.webp new file mode 100644 index 0000000..02edf55 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_062.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_063.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_063.webp new file mode 100644 index 0000000..d7dc7c8 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_063.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_064.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_064.webp new file mode 100644 index 0000000..ba8cee6 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_064.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_065.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_065.webp new file mode 100644 index 0000000..c307555 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_065.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_066.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_066.webp new file mode 100644 index 0000000..9d53f0d Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_066.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_067.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_067.webp new file mode 100644 index 0000000..343c3c9 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_067.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_068.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_068.webp new file mode 100644 index 0000000..fb0a6b9 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_068.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_069.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_069.webp new file mode 100644 index 0000000..817c769 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_069.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_070.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_070.webp new file mode 100644 index 0000000..4c2e02d Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_070.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_071.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_071.webp new file mode 100644 index 0000000..a67722e Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_071.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_072.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_072.webp new file mode 100644 index 0000000..a5bd210 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_072.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_073.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_073.webp new file mode 100644 index 0000000..e521504 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_073.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_074.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_074.webp new file mode 100644 index 0000000..e521504 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_074.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_075.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_075.webp new file mode 100644 index 0000000..e521504 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_075.webp differ diff --git a/app/renderer/public/vfx/dust-burst-large-2/frame_076.webp b/app/renderer/public/vfx/dust-burst-large-2/frame_076.webp new file mode 100644 index 0000000..e521504 Binary files /dev/null and b/app/renderer/public/vfx/dust-burst-large-2/frame_076.webp differ diff --git a/app/renderer/public/vfx/electric-accent-10/frame_01.webp b/app/renderer/public/vfx/electric-accent-10/frame_01.webp new file mode 100644 index 0000000..b740f89 Binary files /dev/null and b/app/renderer/public/vfx/electric-accent-10/frame_01.webp differ diff --git a/app/renderer/public/vfx/electric-accent-10/frame_02.webp b/app/renderer/public/vfx/electric-accent-10/frame_02.webp new file mode 100644 index 0000000..10f6112 Binary files /dev/null and b/app/renderer/public/vfx/electric-accent-10/frame_02.webp differ diff --git a/app/renderer/public/vfx/electric-accent-10/frame_03.webp b/app/renderer/public/vfx/electric-accent-10/frame_03.webp new file mode 100644 index 0000000..74649c7 Binary files /dev/null and b/app/renderer/public/vfx/electric-accent-10/frame_03.webp differ diff --git a/app/renderer/public/vfx/electric-accent-10/frame_04.webp b/app/renderer/public/vfx/electric-accent-10/frame_04.webp new file mode 100644 index 0000000..ec2d034 Binary files /dev/null and b/app/renderer/public/vfx/electric-accent-10/frame_04.webp differ diff --git a/app/renderer/public/vfx/electric-accent-10/frame_05.webp b/app/renderer/public/vfx/electric-accent-10/frame_05.webp new file mode 100644 index 0000000..09dcc78 Binary files /dev/null and b/app/renderer/public/vfx/electric-accent-10/frame_05.webp differ diff --git a/app/renderer/public/vfx/electric-accent-10/frame_06.webp b/app/renderer/public/vfx/electric-accent-10/frame_06.webp new file mode 100644 index 0000000..4e99efe Binary files /dev/null and b/app/renderer/public/vfx/electric-accent-10/frame_06.webp differ diff --git a/app/renderer/public/vfx/electric-accent-10/frame_07.webp b/app/renderer/public/vfx/electric-accent-10/frame_07.webp new file mode 100644 index 0000000..a912f87 Binary files /dev/null and b/app/renderer/public/vfx/electric-accent-10/frame_07.webp differ diff --git a/app/renderer/public/vfx/electric-accent-10/frame_08.webp b/app/renderer/public/vfx/electric-accent-10/frame_08.webp new file mode 100644 index 0000000..ca2270c Binary files /dev/null and b/app/renderer/public/vfx/electric-accent-10/frame_08.webp differ diff --git a/app/renderer/public/vfx/electric-accent-10/frame_09.webp b/app/renderer/public/vfx/electric-accent-10/frame_09.webp new file mode 100644 index 0000000..1cfbeea Binary files /dev/null and b/app/renderer/public/vfx/electric-accent-10/frame_09.webp differ diff --git a/app/renderer/public/vfx/electric-accent-10/frame_10.webp b/app/renderer/public/vfx/electric-accent-10/frame_10.webp new file mode 100644 index 0000000..1cfbeea Binary files /dev/null and b/app/renderer/public/vfx/electric-accent-10/frame_10.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_001.webp b/app/renderer/public/vfx/generated-water/frame_001.webp new file mode 100644 index 0000000..f08dc74 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_001.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_002.webp b/app/renderer/public/vfx/generated-water/frame_002.webp new file mode 100644 index 0000000..b5321bb Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_002.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_003.webp b/app/renderer/public/vfx/generated-water/frame_003.webp new file mode 100644 index 0000000..201f7ab Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_003.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_004.webp b/app/renderer/public/vfx/generated-water/frame_004.webp new file mode 100644 index 0000000..d117c45 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_004.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_005.webp b/app/renderer/public/vfx/generated-water/frame_005.webp new file mode 100644 index 0000000..a2befed Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_005.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_006.webp b/app/renderer/public/vfx/generated-water/frame_006.webp new file mode 100644 index 0000000..e7a5311 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_006.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_007.webp b/app/renderer/public/vfx/generated-water/frame_007.webp new file mode 100644 index 0000000..f1c550f Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_007.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_008.webp b/app/renderer/public/vfx/generated-water/frame_008.webp new file mode 100644 index 0000000..501749d Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_008.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_009.webp b/app/renderer/public/vfx/generated-water/frame_009.webp new file mode 100644 index 0000000..e674a3e Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_009.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_010.webp b/app/renderer/public/vfx/generated-water/frame_010.webp new file mode 100644 index 0000000..d908475 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_010.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_011.webp b/app/renderer/public/vfx/generated-water/frame_011.webp new file mode 100644 index 0000000..e37bdc0 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_011.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_012.webp b/app/renderer/public/vfx/generated-water/frame_012.webp new file mode 100644 index 0000000..38fd583 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_012.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_013.webp b/app/renderer/public/vfx/generated-water/frame_013.webp new file mode 100644 index 0000000..4d857ed Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_013.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_014.webp b/app/renderer/public/vfx/generated-water/frame_014.webp new file mode 100644 index 0000000..933521e Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_014.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_015.webp b/app/renderer/public/vfx/generated-water/frame_015.webp new file mode 100644 index 0000000..788de14 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_015.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_016.webp b/app/renderer/public/vfx/generated-water/frame_016.webp new file mode 100644 index 0000000..5cc2209 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_016.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_017.webp b/app/renderer/public/vfx/generated-water/frame_017.webp new file mode 100644 index 0000000..d0a51d0 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_017.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_018.webp b/app/renderer/public/vfx/generated-water/frame_018.webp new file mode 100644 index 0000000..7cea736 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_018.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_019.webp b/app/renderer/public/vfx/generated-water/frame_019.webp new file mode 100644 index 0000000..a823c23 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_019.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_020.webp b/app/renderer/public/vfx/generated-water/frame_020.webp new file mode 100644 index 0000000..d9a5afe Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_020.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_021.webp b/app/renderer/public/vfx/generated-water/frame_021.webp new file mode 100644 index 0000000..6a8c7e6 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_021.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_022.webp b/app/renderer/public/vfx/generated-water/frame_022.webp new file mode 100644 index 0000000..b1438d5 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_022.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_023.webp b/app/renderer/public/vfx/generated-water/frame_023.webp new file mode 100644 index 0000000..7804360 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_023.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_024.webp b/app/renderer/public/vfx/generated-water/frame_024.webp new file mode 100644 index 0000000..fb0f1a8 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_024.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_025.webp b/app/renderer/public/vfx/generated-water/frame_025.webp new file mode 100644 index 0000000..5cc7143 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_025.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_026.webp b/app/renderer/public/vfx/generated-water/frame_026.webp new file mode 100644 index 0000000..6b43f6e Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_026.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_027.webp b/app/renderer/public/vfx/generated-water/frame_027.webp new file mode 100644 index 0000000..9f8ab51 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_027.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_028.webp b/app/renderer/public/vfx/generated-water/frame_028.webp new file mode 100644 index 0000000..734d29a Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_028.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_029.webp b/app/renderer/public/vfx/generated-water/frame_029.webp new file mode 100644 index 0000000..353f897 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_029.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_030.webp b/app/renderer/public/vfx/generated-water/frame_030.webp new file mode 100644 index 0000000..34d18ae Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_030.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_031.webp b/app/renderer/public/vfx/generated-water/frame_031.webp new file mode 100644 index 0000000..efcdd56 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_031.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_032.webp b/app/renderer/public/vfx/generated-water/frame_032.webp new file mode 100644 index 0000000..8a3f19a Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_032.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_033.webp b/app/renderer/public/vfx/generated-water/frame_033.webp new file mode 100644 index 0000000..6bc9429 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_033.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_034.webp b/app/renderer/public/vfx/generated-water/frame_034.webp new file mode 100644 index 0000000..0dc0ad5 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_034.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_035.webp b/app/renderer/public/vfx/generated-water/frame_035.webp new file mode 100644 index 0000000..3635c79 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_035.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_036.webp b/app/renderer/public/vfx/generated-water/frame_036.webp new file mode 100644 index 0000000..2039d98 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_036.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_037.webp b/app/renderer/public/vfx/generated-water/frame_037.webp new file mode 100644 index 0000000..5a6260b Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_037.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_038.webp b/app/renderer/public/vfx/generated-water/frame_038.webp new file mode 100644 index 0000000..b6369a1 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_038.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_039.webp b/app/renderer/public/vfx/generated-water/frame_039.webp new file mode 100644 index 0000000..aabc063 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_039.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_040.webp b/app/renderer/public/vfx/generated-water/frame_040.webp new file mode 100644 index 0000000..71cb05e Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_040.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_041.webp b/app/renderer/public/vfx/generated-water/frame_041.webp new file mode 100644 index 0000000..50a7d21 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_041.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_042.webp b/app/renderer/public/vfx/generated-water/frame_042.webp new file mode 100644 index 0000000..5d77f7a Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_042.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_043.webp b/app/renderer/public/vfx/generated-water/frame_043.webp new file mode 100644 index 0000000..ab2d125 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_043.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_044.webp b/app/renderer/public/vfx/generated-water/frame_044.webp new file mode 100644 index 0000000..dcb7dfd Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_044.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_045.webp b/app/renderer/public/vfx/generated-water/frame_045.webp new file mode 100644 index 0000000..45361d8 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_045.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_046.webp b/app/renderer/public/vfx/generated-water/frame_046.webp new file mode 100644 index 0000000..6ee23cb Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_046.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_047.webp b/app/renderer/public/vfx/generated-water/frame_047.webp new file mode 100644 index 0000000..d45449c Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_047.webp differ diff --git a/app/renderer/public/vfx/generated-water/frame_048.webp b/app/renderer/public/vfx/generated-water/frame_048.webp new file mode 100644 index 0000000..b86e979 Binary files /dev/null and b/app/renderer/public/vfx/generated-water/frame_048.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_01.webp b/app/renderer/public/vfx/lightning-trail-01/frame_01.webp new file mode 100644 index 0000000..ee1e226 Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_01.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_02.webp b/app/renderer/public/vfx/lightning-trail-01/frame_02.webp new file mode 100644 index 0000000..df084fa Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_02.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_03.webp b/app/renderer/public/vfx/lightning-trail-01/frame_03.webp new file mode 100644 index 0000000..37c3f29 Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_03.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_04.webp b/app/renderer/public/vfx/lightning-trail-01/frame_04.webp new file mode 100644 index 0000000..cfe88a5 Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_04.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_05.webp b/app/renderer/public/vfx/lightning-trail-01/frame_05.webp new file mode 100644 index 0000000..c9f4de6 Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_05.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_06.webp b/app/renderer/public/vfx/lightning-trail-01/frame_06.webp new file mode 100644 index 0000000..d460f8a Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_06.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_07.webp b/app/renderer/public/vfx/lightning-trail-01/frame_07.webp new file mode 100644 index 0000000..2cca6f0 Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_07.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_08.webp b/app/renderer/public/vfx/lightning-trail-01/frame_08.webp new file mode 100644 index 0000000..9c67515 Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_08.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_09.webp b/app/renderer/public/vfx/lightning-trail-01/frame_09.webp new file mode 100644 index 0000000..687eca7 Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_09.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_10.webp b/app/renderer/public/vfx/lightning-trail-01/frame_10.webp new file mode 100644 index 0000000..d62cabc Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_10.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_11.webp b/app/renderer/public/vfx/lightning-trail-01/frame_11.webp new file mode 100644 index 0000000..a617603 Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_11.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_12.webp b/app/renderer/public/vfx/lightning-trail-01/frame_12.webp new file mode 100644 index 0000000..3eb383c Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_12.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_13.webp b/app/renderer/public/vfx/lightning-trail-01/frame_13.webp new file mode 100644 index 0000000..473a004 Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_13.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_14.webp b/app/renderer/public/vfx/lightning-trail-01/frame_14.webp new file mode 100644 index 0000000..2fbc57a Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_14.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_15.webp b/app/renderer/public/vfx/lightning-trail-01/frame_15.webp new file mode 100644 index 0000000..fff8515 Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_15.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_16.webp b/app/renderer/public/vfx/lightning-trail-01/frame_16.webp new file mode 100644 index 0000000..d90e634 Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_16.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_17.webp b/app/renderer/public/vfx/lightning-trail-01/frame_17.webp new file mode 100644 index 0000000..ee1e226 Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_17.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_18.webp b/app/renderer/public/vfx/lightning-trail-01/frame_18.webp new file mode 100644 index 0000000..ee1e226 Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_18.webp differ diff --git a/app/renderer/public/vfx/lightning-trail-01/frame_19.webp b/app/renderer/public/vfx/lightning-trail-01/frame_19.webp new file mode 100644 index 0000000..ee1e226 Binary files /dev/null and b/app/renderer/public/vfx/lightning-trail-01/frame_19.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_001.webp b/app/renderer/public/vfx/pulse-discharge/frame_001.webp new file mode 100644 index 0000000..b15c7a4 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_001.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_002.webp b/app/renderer/public/vfx/pulse-discharge/frame_002.webp new file mode 100644 index 0000000..3cdc13f Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_002.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_003.webp b/app/renderer/public/vfx/pulse-discharge/frame_003.webp new file mode 100644 index 0000000..7da52f1 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_003.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_004.webp b/app/renderer/public/vfx/pulse-discharge/frame_004.webp new file mode 100644 index 0000000..764d04b Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_004.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_005.webp b/app/renderer/public/vfx/pulse-discharge/frame_005.webp new file mode 100644 index 0000000..0709dd7 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_005.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_006.webp b/app/renderer/public/vfx/pulse-discharge/frame_006.webp new file mode 100644 index 0000000..9f450a4 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_006.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_007.webp b/app/renderer/public/vfx/pulse-discharge/frame_007.webp new file mode 100644 index 0000000..4125f49 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_007.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_008.webp b/app/renderer/public/vfx/pulse-discharge/frame_008.webp new file mode 100644 index 0000000..c00fc52 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_008.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_009.webp b/app/renderer/public/vfx/pulse-discharge/frame_009.webp new file mode 100644 index 0000000..859a1ee Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_009.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_010.webp b/app/renderer/public/vfx/pulse-discharge/frame_010.webp new file mode 100644 index 0000000..5ef65ba Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_010.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_011.webp b/app/renderer/public/vfx/pulse-discharge/frame_011.webp new file mode 100644 index 0000000..0ac4bfd Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_011.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_012.webp b/app/renderer/public/vfx/pulse-discharge/frame_012.webp new file mode 100644 index 0000000..da7b669 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_012.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_013.webp b/app/renderer/public/vfx/pulse-discharge/frame_013.webp new file mode 100644 index 0000000..c10b606 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_013.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_014.webp b/app/renderer/public/vfx/pulse-discharge/frame_014.webp new file mode 100644 index 0000000..85f6ce5 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_014.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_015.webp b/app/renderer/public/vfx/pulse-discharge/frame_015.webp new file mode 100644 index 0000000..880786d Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_015.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_016.webp b/app/renderer/public/vfx/pulse-discharge/frame_016.webp new file mode 100644 index 0000000..d6a2b52 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_016.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_017.webp b/app/renderer/public/vfx/pulse-discharge/frame_017.webp new file mode 100644 index 0000000..f224943 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_017.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_018.webp b/app/renderer/public/vfx/pulse-discharge/frame_018.webp new file mode 100644 index 0000000..5ce26c7 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_018.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_019.webp b/app/renderer/public/vfx/pulse-discharge/frame_019.webp new file mode 100644 index 0000000..e0a93f9 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_019.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_020.webp b/app/renderer/public/vfx/pulse-discharge/frame_020.webp new file mode 100644 index 0000000..92f22ab Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_020.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_021.webp b/app/renderer/public/vfx/pulse-discharge/frame_021.webp new file mode 100644 index 0000000..31f3084 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_021.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_022.webp b/app/renderer/public/vfx/pulse-discharge/frame_022.webp new file mode 100644 index 0000000..7f7b8f9 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_022.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_023.webp b/app/renderer/public/vfx/pulse-discharge/frame_023.webp new file mode 100644 index 0000000..ee1e2c7 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_023.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_024.webp b/app/renderer/public/vfx/pulse-discharge/frame_024.webp new file mode 100644 index 0000000..42fab48 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_024.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_025.webp b/app/renderer/public/vfx/pulse-discharge/frame_025.webp new file mode 100644 index 0000000..0c6355e Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_025.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_026.webp b/app/renderer/public/vfx/pulse-discharge/frame_026.webp new file mode 100644 index 0000000..1f66e4c Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_026.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_027.webp b/app/renderer/public/vfx/pulse-discharge/frame_027.webp new file mode 100644 index 0000000..2721fde Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_027.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_028.webp b/app/renderer/public/vfx/pulse-discharge/frame_028.webp new file mode 100644 index 0000000..3f28cd5 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_028.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_029.webp b/app/renderer/public/vfx/pulse-discharge/frame_029.webp new file mode 100644 index 0000000..1f475a8 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_029.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_030.webp b/app/renderer/public/vfx/pulse-discharge/frame_030.webp new file mode 100644 index 0000000..353bd45 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_030.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_031.webp b/app/renderer/public/vfx/pulse-discharge/frame_031.webp new file mode 100644 index 0000000..1e622b0 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_031.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_032.webp b/app/renderer/public/vfx/pulse-discharge/frame_032.webp new file mode 100644 index 0000000..54afb15 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_032.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_033.webp b/app/renderer/public/vfx/pulse-discharge/frame_033.webp new file mode 100644 index 0000000..1f2c2e6 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_033.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_034.webp b/app/renderer/public/vfx/pulse-discharge/frame_034.webp new file mode 100644 index 0000000..529be88 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_034.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_035.webp b/app/renderer/public/vfx/pulse-discharge/frame_035.webp new file mode 100644 index 0000000..5e77e2a Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_035.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_036.webp b/app/renderer/public/vfx/pulse-discharge/frame_036.webp new file mode 100644 index 0000000..42242b7 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_036.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_037.webp b/app/renderer/public/vfx/pulse-discharge/frame_037.webp new file mode 100644 index 0000000..c215f5c Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_037.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_038.webp b/app/renderer/public/vfx/pulse-discharge/frame_038.webp new file mode 100644 index 0000000..77f61fd Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_038.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_039.webp b/app/renderer/public/vfx/pulse-discharge/frame_039.webp new file mode 100644 index 0000000..40a5d62 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_039.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_040.webp b/app/renderer/public/vfx/pulse-discharge/frame_040.webp new file mode 100644 index 0000000..4ee7ec1 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_040.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_041.webp b/app/renderer/public/vfx/pulse-discharge/frame_041.webp new file mode 100644 index 0000000..2eacfcb Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_041.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_042.webp b/app/renderer/public/vfx/pulse-discharge/frame_042.webp new file mode 100644 index 0000000..ff72c1f Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_042.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_043.webp b/app/renderer/public/vfx/pulse-discharge/frame_043.webp new file mode 100644 index 0000000..293e754 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_043.webp differ diff --git a/app/renderer/public/vfx/pulse-discharge/frame_044.webp b/app/renderer/public/vfx/pulse-discharge/frame_044.webp new file mode 100644 index 0000000..b15c7a4 Binary files /dev/null and b/app/renderer/public/vfx/pulse-discharge/frame_044.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_001.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_001.webp new file mode 100644 index 0000000..bf1aeae Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_001.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_002.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_002.webp new file mode 100644 index 0000000..7416114 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_002.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_003.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_003.webp new file mode 100644 index 0000000..fc7b478 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_003.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_004.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_004.webp new file mode 100644 index 0000000..052ac0c Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_004.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_005.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_005.webp new file mode 100644 index 0000000..919cf1c Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_005.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_006.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_006.webp new file mode 100644 index 0000000..22a2387 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_006.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_007.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_007.webp new file mode 100644 index 0000000..9791721 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_007.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_008.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_008.webp new file mode 100644 index 0000000..4404815 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_008.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_009.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_009.webp new file mode 100644 index 0000000..e57b23b Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_009.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_010.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_010.webp new file mode 100644 index 0000000..d1c8e50 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_010.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_011.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_011.webp new file mode 100644 index 0000000..56f4031 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_011.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_012.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_012.webp new file mode 100644 index 0000000..44f9352 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_012.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_013.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_013.webp new file mode 100644 index 0000000..1a7a43d Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_013.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_014.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_014.webp new file mode 100644 index 0000000..4d23e0e Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_014.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_015.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_015.webp new file mode 100644 index 0000000..9a60834 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_015.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_016.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_016.webp new file mode 100644 index 0000000..6242796 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_016.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_017.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_017.webp new file mode 100644 index 0000000..3c2b8b1 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_017.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_018.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_018.webp new file mode 100644 index 0000000..7afd64c Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_018.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_019.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_019.webp new file mode 100644 index 0000000..67eac6c Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_019.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_020.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_020.webp new file mode 100644 index 0000000..0c7c021 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_020.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_021.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_021.webp new file mode 100644 index 0000000..8ca90ae Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_021.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_022.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_022.webp new file mode 100644 index 0000000..2eeeada Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_022.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_023.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_023.webp new file mode 100644 index 0000000..8a23b7a Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_023.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_024.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_024.webp new file mode 100644 index 0000000..824a95f Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_024.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_025.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_025.webp new file mode 100644 index 0000000..0ac1c24 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_025.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_026.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_026.webp new file mode 100644 index 0000000..80dbefc Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_026.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_027.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_027.webp new file mode 100644 index 0000000..6f17561 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_027.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_028.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_028.webp new file mode 100644 index 0000000..dcc3705 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_028.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_029.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_029.webp new file mode 100644 index 0000000..2bde709 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_029.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_030.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_030.webp new file mode 100644 index 0000000..9abc6d7 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_030.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_031.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_031.webp new file mode 100644 index 0000000..8eabe91 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_031.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_032.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_032.webp new file mode 100644 index 0000000..2dfa45c Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_032.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_033.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_033.webp new file mode 100644 index 0000000..9daca9a Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_033.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_034.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_034.webp new file mode 100644 index 0000000..c6cf351 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_034.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_035.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_035.webp new file mode 100644 index 0000000..2bf3aa1 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_035.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_036.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_036.webp new file mode 100644 index 0000000..77c3ac9 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_036.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_037.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_037.webp new file mode 100644 index 0000000..ee51fbc Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_037.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_038.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_038.webp new file mode 100644 index 0000000..5170880 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_038.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_039.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_039.webp new file mode 100644 index 0000000..ac63160 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_039.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_040.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_040.webp new file mode 100644 index 0000000..eaa0295 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_040.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_041.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_041.webp new file mode 100644 index 0000000..0d8890f Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_041.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_042.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_042.webp new file mode 100644 index 0000000..78039f3 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_042.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_043.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_043.webp new file mode 100644 index 0000000..4be6415 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_043.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_044.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_044.webp new file mode 100644 index 0000000..f4c5499 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_044.webp differ diff --git a/app/renderer/public/vfx/real-medium-rain-1/frame_045.webp b/app/renderer/public/vfx/real-medium-rain-1/frame_045.webp new file mode 100644 index 0000000..0550618 Binary files /dev/null and b/app/renderer/public/vfx/real-medium-rain-1/frame_045.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_001.webp b/app/renderer/public/vfx/small-fire/frame_001.webp new file mode 100644 index 0000000..a884d2d Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_001.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_002.webp b/app/renderer/public/vfx/small-fire/frame_002.webp new file mode 100644 index 0000000..76fe273 Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_002.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_003.webp b/app/renderer/public/vfx/small-fire/frame_003.webp new file mode 100644 index 0000000..69bb281 Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_003.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_004.webp b/app/renderer/public/vfx/small-fire/frame_004.webp new file mode 100644 index 0000000..e98cf5a Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_004.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_005.webp b/app/renderer/public/vfx/small-fire/frame_005.webp new file mode 100644 index 0000000..ae02eca Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_005.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_006.webp b/app/renderer/public/vfx/small-fire/frame_006.webp new file mode 100644 index 0000000..c84b082 Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_006.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_007.webp b/app/renderer/public/vfx/small-fire/frame_007.webp new file mode 100644 index 0000000..748e56f Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_007.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_008.webp b/app/renderer/public/vfx/small-fire/frame_008.webp new file mode 100644 index 0000000..113b0ad Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_008.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_009.webp b/app/renderer/public/vfx/small-fire/frame_009.webp new file mode 100644 index 0000000..0f3a8cc Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_009.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_010.webp b/app/renderer/public/vfx/small-fire/frame_010.webp new file mode 100644 index 0000000..3f043ce Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_010.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_011.webp b/app/renderer/public/vfx/small-fire/frame_011.webp new file mode 100644 index 0000000..ed62add Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_011.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_012.webp b/app/renderer/public/vfx/small-fire/frame_012.webp new file mode 100644 index 0000000..0d6ce6d Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_012.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_013.webp b/app/renderer/public/vfx/small-fire/frame_013.webp new file mode 100644 index 0000000..92409b4 Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_013.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_014.webp b/app/renderer/public/vfx/small-fire/frame_014.webp new file mode 100644 index 0000000..4bde10d Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_014.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_015.webp b/app/renderer/public/vfx/small-fire/frame_015.webp new file mode 100644 index 0000000..5fa0fcd Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_015.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_016.webp b/app/renderer/public/vfx/small-fire/frame_016.webp new file mode 100644 index 0000000..0a30a1b Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_016.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_017.webp b/app/renderer/public/vfx/small-fire/frame_017.webp new file mode 100644 index 0000000..4ac65d2 Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_017.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_018.webp b/app/renderer/public/vfx/small-fire/frame_018.webp new file mode 100644 index 0000000..679e723 Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_018.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_019.webp b/app/renderer/public/vfx/small-fire/frame_019.webp new file mode 100644 index 0000000..1aa389c Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_019.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_020.webp b/app/renderer/public/vfx/small-fire/frame_020.webp new file mode 100644 index 0000000..c30e7ad Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_020.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_021.webp b/app/renderer/public/vfx/small-fire/frame_021.webp new file mode 100644 index 0000000..d7c87b4 Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_021.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_022.webp b/app/renderer/public/vfx/small-fire/frame_022.webp new file mode 100644 index 0000000..dca513c Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_022.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_023.webp b/app/renderer/public/vfx/small-fire/frame_023.webp new file mode 100644 index 0000000..05878b4 Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_023.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_024.webp b/app/renderer/public/vfx/small-fire/frame_024.webp new file mode 100644 index 0000000..c020a84 Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_024.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_025.webp b/app/renderer/public/vfx/small-fire/frame_025.webp new file mode 100644 index 0000000..10cbc32 Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_025.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_026.webp b/app/renderer/public/vfx/small-fire/frame_026.webp new file mode 100644 index 0000000..9eb12d2 Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_026.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_027.webp b/app/renderer/public/vfx/small-fire/frame_027.webp new file mode 100644 index 0000000..a1769a1 Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_027.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_028.webp b/app/renderer/public/vfx/small-fire/frame_028.webp new file mode 100644 index 0000000..5e08779 Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_028.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_029.webp b/app/renderer/public/vfx/small-fire/frame_029.webp new file mode 100644 index 0000000..828a97a Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_029.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_030.webp b/app/renderer/public/vfx/small-fire/frame_030.webp new file mode 100644 index 0000000..ccc04fb Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_030.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_031.webp b/app/renderer/public/vfx/small-fire/frame_031.webp new file mode 100644 index 0000000..1d0ed81 Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_031.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_032.webp b/app/renderer/public/vfx/small-fire/frame_032.webp new file mode 100644 index 0000000..0c836fe Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_032.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_033.webp b/app/renderer/public/vfx/small-fire/frame_033.webp new file mode 100644 index 0000000..d694ac3 Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_033.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_034.webp b/app/renderer/public/vfx/small-fire/frame_034.webp new file mode 100644 index 0000000..f438da9 Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_034.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_035.webp b/app/renderer/public/vfx/small-fire/frame_035.webp new file mode 100644 index 0000000..cd46a6b Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_035.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_036.webp b/app/renderer/public/vfx/small-fire/frame_036.webp new file mode 100644 index 0000000..e0bcc8d Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_036.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_037.webp b/app/renderer/public/vfx/small-fire/frame_037.webp new file mode 100644 index 0000000..2cb49cd Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_037.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_038.webp b/app/renderer/public/vfx/small-fire/frame_038.webp new file mode 100644 index 0000000..c146d2a Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_038.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_039.webp b/app/renderer/public/vfx/small-fire/frame_039.webp new file mode 100644 index 0000000..0151d3e Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_039.webp differ diff --git a/app/renderer/public/vfx/small-fire/frame_040.webp b/app/renderer/public/vfx/small-fire/frame_040.webp new file mode 100644 index 0000000..ebc883c Binary files /dev/null and b/app/renderer/public/vfx/small-fire/frame_040.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_001.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_001.webp new file mode 100644 index 0000000..88fd92d Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_001.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_002.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_002.webp new file mode 100644 index 0000000..41e4727 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_002.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_003.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_003.webp new file mode 100644 index 0000000..8016e00 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_003.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_004.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_004.webp new file mode 100644 index 0000000..ab8b5a6 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_004.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_005.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_005.webp new file mode 100644 index 0000000..0b68468 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_005.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_006.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_006.webp new file mode 100644 index 0000000..cca50fa Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_006.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_007.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_007.webp new file mode 100644 index 0000000..c5f7f45 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_007.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_008.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_008.webp new file mode 100644 index 0000000..81d6073 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_008.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_009.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_009.webp new file mode 100644 index 0000000..5fc59d9 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_009.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_010.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_010.webp new file mode 100644 index 0000000..7da0726 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_010.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_011.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_011.webp new file mode 100644 index 0000000..9f1b747 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_011.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_012.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_012.webp new file mode 100644 index 0000000..c2e6582 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_012.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_013.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_013.webp new file mode 100644 index 0000000..64f6a7e Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_013.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_014.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_014.webp new file mode 100644 index 0000000..4c9e4cc Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_014.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_015.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_015.webp new file mode 100644 index 0000000..aac7bc3 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_015.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_016.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_016.webp new file mode 100644 index 0000000..25b597e Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_016.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_017.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_017.webp new file mode 100644 index 0000000..b9c6a14 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_017.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_018.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_018.webp new file mode 100644 index 0000000..fc7d56f Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_018.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_019.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_019.webp new file mode 100644 index 0000000..52e1304 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_019.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_020.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_020.webp new file mode 100644 index 0000000..83c907e Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_020.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_021.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_021.webp new file mode 100644 index 0000000..4dacb29 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_021.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_022.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_022.webp new file mode 100644 index 0000000..55ecb03 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_022.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_023.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_023.webp new file mode 100644 index 0000000..1e9e678 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_023.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_024.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_024.webp new file mode 100644 index 0000000..3730595 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_024.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_025.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_025.webp new file mode 100644 index 0000000..35e0816 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_025.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_026.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_026.webp new file mode 100644 index 0000000..f33edb5 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_026.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_027.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_027.webp new file mode 100644 index 0000000..47216e3 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_027.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_028.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_028.webp new file mode 100644 index 0000000..81caad2 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_028.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_029.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_029.webp new file mode 100644 index 0000000..d814d3b Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_029.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_030.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_030.webp new file mode 100644 index 0000000..e8ad6f7 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_030.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_031.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_031.webp new file mode 100644 index 0000000..d9bb8a3 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_031.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_032.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_032.webp new file mode 100644 index 0000000..b1ebbbc Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_032.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_033.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_033.webp new file mode 100644 index 0000000..1d5706e Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_033.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_034.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_034.webp new file mode 100644 index 0000000..08b8f04 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_034.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_035.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_035.webp new file mode 100644 index 0000000..a3c0117 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_035.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_036.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_036.webp new file mode 100644 index 0000000..1321ac3 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_036.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_037.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_037.webp new file mode 100644 index 0000000..e08020a Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_037.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_038.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_038.webp new file mode 100644 index 0000000..f0a0764 Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_038.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_039.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_039.webp new file mode 100644 index 0000000..c4b6adf Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_039.webp differ diff --git a/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_040.webp b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_040.webp new file mode 100644 index 0000000..c057bfb Binary files /dev/null and b/app/renderer/public/vfx/smokey-atmosphere-1-round/frame_040.webp differ diff --git a/app/renderer/shared/effects/PxiEffectsOverlay.tsx b/app/renderer/shared/effects/PxiEffectsOverlay.tsx index 3424657..e072236 100644 --- a/app/renderer/shared/effects/PxiEffectsOverlay.tsx +++ b/app/renderer/shared/effects/PxiEffectsOverlay.tsx @@ -4,16 +4,40 @@ import type { EffectsState, EffectInstance } from '../../../shared/types/effects import styles from './PxiEffectsOverlay.module.css'; -type PoisonParticleFx = { - s: any; - kind: 'stem' | 'cap'; - uStem: number; - side: number; - capAng: number; - capDist: number; - sz: number; - flick: number; -}; +const LIGHTNING_VFX_FRAME_COUNT = 19; +const LIGHTNING_VFX_FRAME_ASPECT = 420 / 473; +const LIGHTNING_VFX_STRIKE_MS = 320; +const LIGHTNING_VFX_IMPACT_ANCHOR_Y = 437 / 473; +const ELECTRIC_ACCENT_START_MS = 8; +const ELECTRIC_ACCENT_DURATION_MS = 740; +const ELECTRIC_ACCENT_FRAME_COUNT = 10; +const ELECTRIC_ACCENT_FRAME_ASPECT = 480 / 360; +const FOG_VFX_FRAME_COUNT = 40; +const FOG_VFX_LOOP_MS = 5000; +const GROUND_FIRE_VFX_FRAME_COUNT = 40; +const GROUND_FIRE_VFX_FRAME_ASPECT = 872 / 480; +const GROUND_FIRE_VFX_LOOP_MS = 3333; +const RAIN_VFX_FRAME_COUNT = 45; +const RAIN_VFX_FRAME_ASPECT = 640 / 360; +const RAIN_VFX_LOOP_MS = 3000; +const WATER_VFX_FRAME_COUNT = 48; +const WATER_VFX_LOOP_MS = 2400; +const PULSE_DISCHARGE_VFX_FRAME_COUNT = 44; +const PULSE_DISCHARGE_VFX_FRAME_ASPECT = 960 / 540; +const PULSE_DISCHARGE_VFX_DURATION_MS = 2933; +const PULSE_DISCHARGE_IMPACT_ANCHOR_X = 0.885; +const PULSE_DISCHARGE_IMPACT_ANCHOR_Y = 0.49; +const DUST_BURST_VFX_FRAME_COUNT = 76; +const DUST_BURST_VFX_FRAME_ASPECT = 480 / 270; +const DUST_BURST_VFX_DURATION_MS = 5067; +const lightningVfxFrameTextureCache = new WeakMap }>(); +const electricAccentFrameTextureCache = new WeakMap }>(); +const fogVfxFrameTextureCache = new WeakMap }>(); +const groundFireVfxFrameTextureCache = new WeakMap }>(); +const rainVfxFrameTextureCache = new WeakMap }>(); +const waterVfxFrameTextureCache = new WeakMap }>(); +const pulseDischargeFrameTextureCache = new WeakMap }>(); +const dustBurstFrameTextureCache = new WeakMap }>(); type Props = { state: EffectsState | null; @@ -98,6 +122,14 @@ export function PixiEffectsOverlay({ state, interactive = false, style, viewport viewportRef.current = { x: 0, y: 0, w: sizeRef.current.w, h: sizeRef.current.h }; } syncNodes(pixi, root, nodesRef.current, stateRef.current, sizeRef.current, viewportRef.current); + void preloadLightningVfxFrameTextures(pixi); + void preloadElectricAccentFrameTextures(pixi); + void preloadFogVfxFrameTextures(pixi); + void preloadGroundFireVfxFrameTextures(pixi); + void preloadRainVfxFrameTextures(pixi); + void preloadWaterVfxFrameTextures(pixi); + void preloadPulseDischargeFrameTextures(pixi); + void preloadDustBurstFrameTextures(pixi); // Animation loop: на каждом кадре обновляем свойства инстансов (alpha/дрейф/фликер). app.ticker.add(() => { @@ -185,19 +217,31 @@ function syncNodes( size: { w: number; h: number }, viewport: { x: number; y: number; w: number; h: number }, ) { - const desired = new Set((state?.instances ?? []).map((i) => i.id)); + const nowMs = state?.serverNowMs ?? Date.now(); + const liveInstances = (state?.instances ?? []).filter((i) => isRenderableInstance(i, nowMs)); + const desired = new Set(liveInstances.map((i) => i.id)); for (const [id, node] of nodes.entries()) { if (desired.has(id)) continue; + const fx = (node as any).__fx; + fx?.video?.pause?.(); + if (fx?.video) { + fx.video.src = ''; + } root.removeChild(node); node.destroy?.({ children: true }); nodes.delete(id); } if (!state) return; - for (const inst of state.instances) { + for (const inst of liveInstances) { const sig = instanceSig(inst, viewport); const existing = nodes.get(inst.id); if (existing && (existing as any).__sig === sig) continue; if (existing) { + const fx = (existing as any).__fx; + fx?.video?.pause?.(); + if (fx?.video) { + fx.video.src = ''; + } root.removeChild(existing); existing.destroy?.({ children: true }); nodes.delete(inst.id); @@ -210,6 +254,11 @@ function syncNodes( } } +function isRenderableInstance(inst: EffectInstance, nowMs: number): boolean { + if (!('lifetimeMs' in inst) || inst.lifetimeMs === null) return true; + return nowMs - inst.createdAtMs < inst.lifetimeMs; +} + function createInstanceNode( pixi: any, inst: EffectInstance, @@ -219,123 +268,130 @@ function createInstanceNode( const { x: vx, y: vy, w, h } = viewport; if (inst.type === 'fog') { const base = inst.opacity; - const r = inst.radiusN * Math.min(w, h); - const tex = getFogTexture(pixi); - // Вместо кругов используем Sprite — визуально мягче и быстрее. + const minDim = Math.min(w, h); + const r = inst.radiusN * minDim; + const textures = getFogVfxFrameTextures(pixi); const c = new pixi.Container(); for (let i = 0; i < inst.points.length; i += 1) { const p = inst.points[i]; if (!p) continue; - const s = new pixi.Sprite(tex); + const s = new pixi.Sprite(textures[0] ?? pixi.Texture.EMPTY); s.anchor?.set?.(0.5, 0.5); s.x = vx + p.x * w; s.y = vy + p.y * h; - const scale = r / Math.max(1, tex.width * 0.5); - s.scale?.set?.(scale, scale); - s.rotation = hash01(inst.seed ^ 0x3141592, i) * Math.PI * 2; + const fogSize = Math.max(4, r * 2); + s.width = fogSize; + s.height = fogSize; s.alpha = base; - s.blendMode = pixi.BLEND_MODES?.SCREEN ?? 2; - // Сохраним параметры анимации прямо на объекте (простая практика для Pixi). + s.tint = 0xd8d8d8; + s.blendMode = pixi.BLEND_MODES?.NORMAL ?? 0; (s as any).__fx = { - bx: s.x, - by: s.y, - phase: hash01(inst.seed, i) * Math.PI * 2, - drift: 6 + 26 * hash01(inst.seed ^ 0x9e3779b9, i), - rot0: s.rotation, - rotSpeed: (hash01(inst.seed ^ 0x2718281, i) - 0.5) * 0.35, + phase: hash01(inst.seed, i), + w0: s.width, + h0: s.height, }; c.addChild(s); } - (c as any).__fx = { id: inst.id, type: inst.type }; - c.alpha = Math.max(0, Math.min(1, inst.opacity)); + (c as any).__fx = { id: inst.id, type: inst.type, textures }; + c.alpha = 1; + void preloadFogVfxFrameTextures(pixi).then((loadedTextures) => { + const fx = (c as any).__fx; + if (fx?.type !== 'fog') return; + fx.textures = loadedTextures; + for (const child of c.children ?? []) { + if (loadedTextures[0] && child.texture === pixi.Texture.EMPTY) { + child.texture = loadedTextures[0]; + } + } + }); return c; } if (inst.type === 'fire') { - const base = inst.opacity; - const r = inst.radiusN * Math.min(w, h); - const tex = getFireTexture(pixi); + const base = 1; + const minDim = Math.min(w, h); + const r = inst.radiusN * minDim; + const textures = getGroundFireVfxFrameTextures(pixi); const c = new pixi.Container(); for (let i = 0; i < inst.points.length; i += 1) { const p = inst.points[i]; if (!p) continue; - const s = new pixi.Sprite(tex); - s.anchor?.set?.(0.5, 0.5); + const s = new pixi.Sprite(textures[0] ?? pixi.Texture.EMPTY); + s.anchor?.set?.(0.5, 0.62); s.x = vx + p.x * w; s.y = vy + p.y * h; - const scale = r / Math.max(1, tex.width * 0.5); - s.scale?.set?.(scale, scale); - s.rotation = (hash01(inst.seed ^ 0x1f1f1f1f, i) - 0.5) * 0.35; + const flameWidth = Math.max(r * 3.2, minDim * 0.05); + s.width = flameWidth; + s.height = flameWidth / GROUND_FIRE_VFX_FRAME_ASPECT; + s.rotation = (hash01(inst.seed ^ 0x2f171717, i) - 0.5) * 0.18; s.alpha = base; - // Огонь “подсвечивает” — используем аддитивное смешивание. s.blendMode = pixi.BLEND_MODES?.ADD ?? 1; - // Тёплый тинт: от оранжевого к жёлтому. - const warm = hash01(inst.seed ^ 0x2222222, i); - s.tint = warm > 0.5 ? 0xffa000 : 0xff5a00; (s as any).__fx = { bx: s.x, by: s.y, - phase: hash01(inst.seed, i) * Math.PI * 2, - phase2: hash01(inst.seed ^ 0x1234567, i) * Math.PI * 2, - drift: 4 + 18 * hash01(inst.seed ^ 0x9e3779b9, i), - flicker: 0.7 + 0.6 * hash01(inst.seed ^ 0x3141592, i), - flickerSpeed: 0.0022 + 0.0038 * hash01(inst.seed ^ 0x7777777, i), - flickerAmp: 0.35 + 0.55 * hash01(inst.seed ^ 0x8888888, i), - tongue: 0.55 + 0.65 * hash01(inst.seed ^ 0x9999999, i), // “высота языка” (индивидуально для точки) - rot0: s.rotation, - rotSpeed: (hash01(inst.seed ^ 0x2718281, i) - 0.5) * 0.65, - sy0: s.scale?.y ?? 1, + phase: hash01(inst.seed ^ 0x5317f00d, i), + wobble: 1 + 4 * hash01(inst.seed ^ 0x17171717, i), + w0: s.width, + h0: s.height, }; c.addChild(s); } - (c as any).__fx = { id: inst.id, type: inst.type }; - c.alpha = Math.max(0, Math.min(1, inst.opacity)); + (c as any).__fx = { id: inst.id, type: inst.type, textures }; + c.alpha = 1; + void preloadGroundFireVfxFrameTextures(pixi).then((loadedTextures) => { + const fx = (c as any).__fx; + if (fx?.type !== 'fire') return; + fx.textures = loadedTextures; + for (const child of c.children ?? []) { + if (loadedTextures[0] && child.texture === pixi.Texture.EMPTY) { + child.texture = loadedTextures[0]; + } + } + }); return c; } if (inst.type === 'rain') { const base = inst.opacity; - const r = inst.radiusN * Math.min(w, h); - const tex = getRainTexture(pixi); + const minDim = Math.min(w, h); + const r = inst.radiusN * minDim; + const textures = getRainVfxFrameTextures(pixi); const c = new pixi.Container(); for (let i = 0; i < inst.points.length; i += 1) { const p = inst.points[i]; if (!p) continue; - const s = new pixi.Sprite(tex); + const s = new pixi.Sprite(textures[0] ?? pixi.Texture.EMPTY); s.anchor?.set?.(0.5, 0.5); s.x = vx + p.x * w; s.y = vy + p.y * h; - const scale = r / Math.max(1, tex.width * 0.5); - s.scale?.set?.(scale, scale); - s.rotation = (-12 * Math.PI) / 180; // лёгкий наклон “ветра” + const rainWidth = Math.max(r * 3.8, minDim * 0.08); + s.width = rainWidth; + s.height = rainWidth / RAIN_VFX_FRAME_ASPECT; + s.rotation = (hash01(inst.seed ^ 0x2a1f6a11, i) - 0.5) * 0.12; s.alpha = base; - // Тёмные линии дождя должны быть заметны на карте — рисуем обычным смешиванием. + s.tint = 0xffffff; s.blendMode = pixi.BLEND_MODES?.NORMAL ?? 0; - // Цвет капли: рандом (детерминированный) с более заметным разбегом, - // чтобы было видно, что линии разные (тёмно-синий -> водянистый голубовато-синий). - const ct = hash01(inst.seed ^ 0x6d2b79f5, i); - const r0 = 0x08; - const g0 = 0x1a; - const b0 = 0x2e; - const r1 = 0x78; - const g1 = 0xb8; - const b1 = 0xf0; - const rr = Math.round(r0 + (r1 - r0) * ct); - const gg = Math.round(g0 + (g1 - g0) * ct); - const bb = Math.round(b0 + (b1 - b0) * ct); - s.tint = (rr << 16) | (gg << 8) | bb; (s as any).__fx = { bx: s.x, by: s.y, - phase: hash01(inst.seed ^ 0x55aa55aa, i) * Math.PI * 2, - drift: 3 + 10 * hash01(inst.seed ^ 0x9e3779b9, i), - fallSpeed: 0.35 + 1.25 * hash01(inst.seed ^ 0x1020304, i), // норм. к радиусу - spread: 0.45 + 0.75 * hash01(inst.seed ^ 0x2030405, i), - alphaMul: 1.05 + 0.7 * hash01(inst.seed ^ 0x3040506, i), - rot0: s.rotation, + phase: hash01(inst.seed ^ 0x55aa55aa, i), + drift: 2 + 8 * hash01(inst.seed ^ 0x9e3779b9, i), + alphaMul: 0.9 + 0.45 * hash01(inst.seed ^ 0x3040506, i), + w0: s.width, + h0: s.height, }; c.addChild(s); } - (c as any).__fx = { id: inst.id, type: inst.type }; - c.alpha = Math.max(0, Math.min(1, inst.opacity)); + (c as any).__fx = { id: inst.id, type: inst.type, textures }; + c.alpha = 1; + void preloadRainVfxFrameTextures(pixi).then((loadedTextures) => { + const fx = (c as any).__fx; + if (fx?.type !== 'rain') return; + fx.textures = loadedTextures; + for (const child of c.children ?? []) { + if (loadedTextures[0] && child.texture === pixi.Texture.EMPTY) { + child.texture = loadedTextures[0]; + } + } + }); return c; } if (inst.type === 'water') { @@ -353,9 +409,23 @@ function createInstanceNode( const built = buildWaterFillSprite(pixi, inst, viewport, halfW); if (!built) return null; c.addChild(built.sprite); - (c as any).__fx = { kind: 'waterSolid' }; + c.addChild(built.mask); + (c as any).__fx = { + kind: 'waterVfx', + sprite: built.sprite, + textures: built.textures, + phase: hash01(inst.seed ^ 0x5ea0001, 0), + }; c.alpha = Math.max(0, Math.min(1, inst.opacity)); c.blendMode = pixi.BLEND_MODES?.NORMAL ?? 0; + void preloadWaterVfxFrameTextures(pixi).then((loadedTextures) => { + const fx = (c as any).__fx; + if (fx?.kind !== 'waterVfx') return; + fx.textures = loadedTextures; + if (loadedTextures[0] && built.sprite.texture === pixi.Texture.EMPTY) { + built.sprite.texture = loadedTextures[0]; + } + }); return c; } if (inst.type === 'darkness') { @@ -372,59 +442,84 @@ function createInstanceNode( return s; } if (inst.type === 'lightning') { + const cont = new pixi.Container(); + const textures = getLightningVfxFrameTextures(pixi); + const sprite = new pixi.Sprite(textures[0] ?? pixi.Texture.EMPTY); + sprite.anchor?.set?.(0.5, 0.5); + sprite.blendMode = pixi.BLEND_MODES?.ADD ?? 1; + const accentTextures = getElectricAccentFrameTextures(pixi); + const accentSprite = new pixi.Sprite(accentTextures[0] ?? pixi.Texture.EMPTY); + accentSprite.anchor?.set?.(0.5, 0.5); + accentSprite.blendMode = pixi.BLEND_MODES?.ADD ?? 1; + accentSprite.alpha = 0; const g = new pixi.Graphics(); g.blendMode = pixi.BLEND_MODES?.ADD ?? 1; - (g as any).__fx = { + cont.addChild(sprite); + cont.addChild(accentSprite); + cont.addChild(g); + (cont as any).__fx = { id: inst.id, type: inst.type, - start: inst.start, - end: inst.end, - widthN: inst.widthN, + sprite, + accentSprite, + g, + textures, + accentTextures, }; - // Первичная отрисовка, дальше будет обновляться в animateNodes. - redrawLightning(pixi, g, inst, viewport, 0, Math.max(1, inst.lifetimeMs)); - return g; + void preloadLightningVfxFrameTextures(pixi).then((loadedTextures) => { + const fx = (cont as any).__fx; + if (fx?.type !== 'lightning') return; + fx.textures = loadedTextures; + if (loadedTextures[0] && sprite.texture === pixi.Texture.EMPTY) { + sprite.texture = loadedTextures[0]; + } + }); + void preloadElectricAccentFrameTextures(pixi).then((loadedTextures) => { + const fx = (cont as any).__fx; + if (fx?.type !== 'lightning') return; + fx.accentTextures = loadedTextures; + if (loadedTextures[0] && accentSprite.texture === pixi.Texture.EMPTY) { + accentSprite.texture = loadedTextures[0]; + } + }); + redrawLightningVfx(cont, inst, viewport, 0, Math.max(1, inst.lifetimeMs)); + return cont; } if (inst.type === 'sunbeam') { + const cont = new pixi.Container(); + const textures = getPulseDischargeFrameTextures(pixi); + const sprite = new pixi.Sprite(textures[0] ?? pixi.Texture.EMPTY); + sprite.anchor?.set?.(PULSE_DISCHARGE_IMPACT_ANCHOR_X, PULSE_DISCHARGE_IMPACT_ANCHOR_Y); + sprite.blendMode = pixi.BLEND_MODES?.ADD ?? 1; const g = new pixi.Graphics(); g.blendMode = pixi.BLEND_MODES?.ADD ?? 1; - (g as any).__fx = { + cont.addChild(sprite); + cont.addChild(g); + (cont as any).__fx = { id: inst.id, type: inst.type, - start: inst.start, - end: inst.end, - widthN: inst.widthN, + sprite, + g, + textures, }; - redrawSunbeam(pixi, g, inst, viewport, 0, Math.max(1, inst.lifetimeMs)); - return g; + void preloadPulseDischargeFrameTextures(pixi).then((loadedTextures) => { + const fx = (cont as any).__fx; + if (fx?.type !== 'sunbeam') return; + fx.textures = loadedTextures; + if (loadedTextures[0] && sprite.texture === pixi.Texture.EMPTY) { + sprite.texture = loadedTextures[0]; + } + }); + redrawPulseDischargeVfx(cont, inst, viewport, 0, Math.max(1, inst.lifetimeMs)); + return cont; } if (inst.type === 'poisonCloud') { const cont = new pixi.Container(); - const tex = getPoisonParticleTexture(pixi); - /** Меньше спрайтов — быстрее тик; картина остаётся плотной. */ - const particleCount = 400; - const particles: PoisonParticleFx[] = []; - for (let i = 0; i < particleCount; i += 1) { - const s = new pixi.Sprite(tex); - s.anchor?.set?.(0.5, 0.5); - const kind: 'stem' | 'cap' = hash01(inst.seed ^ 0x50f3757f, i) < 0.44 ? 'stem' : 'cap'; - const gVar = hash01(inst.seed ^ 0xdead00, i); - const tint = gVar < 0.33 ? 0x33ff99 : gVar < 0.66 ? 0x22ee77 : 0x44ffaa; - s.tint = tint; - s.blendMode = pixi.BLEND_MODES?.ADD ?? 1; - particles.push({ - s, - kind, - uStem: hash01(inst.seed ^ 0x111111, i), - side: hash01(inst.seed ^ 0x222222, i) * 2 - 1, - capAng: hash01(inst.seed ^ 0x333333, i) * Math.PI * 2, - capDist: Math.sqrt(hash01(inst.seed ^ 0x444444, i)), - // Мелкие точки: узкий разброс размера (раньше до ~3.7 давало гигантские блики). - sz: 0.22 + hash01(inst.seed ^ 0x555555, i) * 0.75, - flick: hash01(inst.seed ^ 0x666666, i) * Math.PI * 2, - }); - cont.addChild(s); - } + const textures = getDustBurstFrameTextures(pixi); + const sprite = new pixi.Sprite(textures[0] ?? pixi.Texture.EMPTY); + sprite.anchor?.set?.(0.5, 1); + sprite.blendMode = pixi.BLEND_MODES?.ADD ?? 1; + sprite.tint = 0x66ff99; const TextApi = (pixi as { Text?: new (opts: object) => any }).Text; let skull: any = null; if (typeof TextApi === 'function') { @@ -439,11 +534,19 @@ function createInstanceNode( }); skull.anchor?.set?.(0.5, 1); skull.visible = false; - cont.addChild(skull); } - (cont as any).__fx = { particles, skull }; - cont.blendMode = pixi.BLEND_MODES?.NORMAL ?? 0; - redrawPoisonCloud(pixi, cont, inst, viewport, 0, Math.max(1, inst.lifetimeMs)); + cont.addChild(sprite); + if (skull) cont.addChild(skull); + (cont as any).__fx = { id: inst.id, type: inst.type, sprite, skull, textures }; + void preloadDustBurstFrameTextures(pixi).then((loadedTextures) => { + const fx = (cont as any).__fx; + if (fx?.type !== 'poisonCloud') return; + fx.textures = loadedTextures; + if (loadedTextures[0] && sprite.texture === pixi.Texture.EMPTY) { + sprite.texture = loadedTextures[0]; + } + }); + redrawPoisonCloud(cont, inst, viewport, 0, Math.max(1, inst.lifetimeMs)); return cont; } if (inst.type === 'freeze') { @@ -521,7 +624,6 @@ function animateNodes( const instById = new Map(state.instances.map((i) => [i.id, i])); void pixi; void size; - const { w, h } = viewport; for (const [id, node] of nodes.entries()) { const inst = instById.get(id); @@ -530,58 +632,56 @@ function animateNodes( if (inst.type === 'fog') { const cont = node; - const pulse = 0.88 + 0.14 * Math.sin(0.0012 * t + hash01(inst.seed, 0) * 6.28); - cont.alpha = Math.max(0, Math.min(1, inst.opacity * pulse)); + const fxRoot = (cont as any).__fx; + const textures = Array.isArray(fxRoot?.textures) ? fxRoot.textures : []; + const pulse = 0.92 + 0.08 * Math.sin(0.0012 * t + hash01(inst.seed, 0) * 6.28); + cont.alpha = 1; for (const child of cont.children ?? []) { const fx = (child as any).__fx; if (!fx) continue; - const wob = fx.drift; - child.x = fx.bx + Math.sin(0.0009 * t + fx.phase) * wob; - child.y = fx.by + Math.cos(0.0011 * t + fx.phase) * wob; - child.alpha = cont.alpha; - if (typeof fx.rot0 === 'number' && typeof fx.rotSpeed === 'number') { - child.rotation = fx.rot0 + fx.rotSpeed * (t / 1000); + if (textures.length > 0) { + const frameT = (t + (fx.phase ?? 0) * FOG_VFX_LOOP_MS) % FOG_VFX_LOOP_MS; + const frameIndex = Math.min( + textures.length - 1, + Math.floor((frameT / FOG_VFX_LOOP_MS) * textures.length), + ); + const frame = textures[frameIndex]; + if (frame && child.texture !== frame) { + child.texture = frame; + } } + child.alpha = Math.max(0.05, Math.min(0.9, inst.opacity * pulse)); + child.width = typeof fx.w0 === 'number' ? fx.w0 : child.width; + child.height = typeof fx.h0 === 'number' ? fx.h0 : child.height; } } if (inst.type === 'fire') { const cont = node; - // Огонь: общий фон + per-sprite “языки” и неравномерная пульсация. - const baseFlick = 0.78 + 0.22 * Math.sin(0.0062 * t + hash01(inst.seed ^ 0xabcdef, 0) * 6.28); - // Не даём “пламени” уходить в почти ноль — всегда остаётся базовое свечение. - cont.alpha = Math.max(0, Math.min(1, inst.opacity * Math.max(0.55, baseFlick))); + const fxRoot = (cont as any).__fx; + const textures = Array.isArray(fxRoot?.textures) ? fxRoot.textures : []; + cont.alpha = 1; for (const child of cont.children ?? []) { const fx = (child as any).__fx; if (!fx) continue; - const wob = fx.drift; - // Пламя чуть “тянется” вверх, но без изменения конечной позиции кисти. - // Лифт делаем “языками”: сильнее при вспышках конкретного спрайта. - const sF = typeof fx.flickerSpeed === 'number' ? fx.flickerSpeed * 1.75 : 0.0058; - const sA = typeof fx.flickerAmp === 'number' ? fx.flickerAmp : 0.6; - const tongue = typeof fx.tongue === 'number' ? fx.tongue : 1; - const local = - 0.55 + - 0.45 * Math.sin(sF * t + (fx.phase ?? 0)) * sA + - 0.25 * Math.sin(sF * 1.9 * t + (fx.phase2 ?? 0)) * (0.35 + 0.65 * tongue); - const local01 = Math.max(0, Math.min(1.2, local)); - // Без накопления: язык “дышит” вверх-вниз, но не уплывает со временем. - const lift = - -(4 + 10 * local01) * (0.55 + 0.65 * tongue) - - (1.5 + 2.5 * tongue) * Math.sin(0.0045 * t + (fx.phase ?? 0)); - child.x = fx.bx + Math.sin(0.0018 * t + fx.phase) * wob; - child.y = fx.by + Math.cos(0.0022 * t + fx.phase) * wob + lift; - // Неравномерная пульсация без ступенчатого шума (ступени давали заметное моргание). - const nPhase = hash01(inst.seed ^ 0xf00baa, (fx.bx | 0) + (fx.by | 0)) * Math.PI * 2; - const noise = 0.11 * Math.sin(0.0031 * t + nPhase) + 0.07 * Math.sin(0.0077 * t + nPhase * 1.7); - child.alpha = Math.max(0, Math.min(1, cont.alpha * (0.85 + 0.45 * local01 + noise))); - // “Языки” виднее, если чуть пульсировать высоту спрайта. - const sy0 = typeof fx.sy0 === 'number' ? fx.sy0 : 1; - const stretch = 0.85 + 0.35 * local01 * (0.7 + 0.3 * tongue); - child.scale?.set?.(child.scale.x, sy0 * stretch); - if (typeof fx.rot0 === 'number' && typeof fx.rotSpeed === 'number') { - child.rotation = fx.rot0 + fx.rotSpeed * (t / 1000); + if (textures.length > 0) { + const frameT = (t + (fx.phase ?? 0) * GROUND_FIRE_VFX_LOOP_MS) % GROUND_FIRE_VFX_LOOP_MS; + const frameIndex = Math.min( + textures.length - 1, + Math.floor((frameT / GROUND_FIRE_VFX_LOOP_MS) * textures.length), + ); + const frame = textures[frameIndex]; + if (frame && child.texture !== frame) { + child.texture = frame; + } } + const wobble = typeof fx.wobble === 'number' ? fx.wobble : 2; + child.x = fx.bx + Math.sin(0.0016 * t + (fx.phase ?? 0) * 6.28) * wobble; + child.y = fx.by + Math.cos(0.0019 * t + (fx.phase ?? 0) * 6.28) * wobble * 0.45; + const local = 0.93 + 0.07 * Math.sin(0.0042 * t + (fx.phase ?? 0) * 12.56); + child.width = (typeof fx.w0 === 'number' ? fx.w0 : child.width) * local; + child.height = (typeof fx.h0 === 'number' ? fx.h0 : child.height) * (0.96 + (local - 0.93) * 0.7); + child.alpha = 1; } } @@ -589,56 +689,72 @@ function animateNodes( const cont = node; const pulse = 0.94 + 0.06 * Math.sin(0.0011 * t + hash01(inst.seed ^ 0xc001d00d, 0) * 6.28); cont.alpha = Math.max(0, Math.min(1, inst.opacity * pulse)); + const fxRoot = (cont as any).__fx; + const sprite = fxRoot?.sprite; + const textures = Array.isArray(fxRoot?.textures) ? fxRoot.textures : []; + if (sprite && textures.length > 0) { + const frameT = (t + (fxRoot.phase ?? 0) * WATER_VFX_LOOP_MS) % WATER_VFX_LOOP_MS; + const frameIndex = Math.min( + textures.length - 1, + Math.floor((frameT / WATER_VFX_LOOP_MS) * textures.length), + ); + const frame = textures[frameIndex]; + if (frame && sprite.texture !== frame) { + sprite.texture = frame; + } + } } if (inst.type === 'rain') { const cont = node; - // Дождь: стабильная “шторка” + движение капель вниз с лёгким дрейфом. - const base = 0.85 + 0.15 * Math.sin(0.0018 * t + hash01(inst.seed ^ 0xdeadbeef, 0) * 6.28); - cont.alpha = Math.max(0, Math.min(1, inst.opacity * Math.max(1.35, base * 1.28))); - const rPx = inst.radiusN * Math.min(w, h); - const wrap = Math.max(18, rPx * 1.8); + const fxRoot = (cont as any).__fx; + const textures = Array.isArray(fxRoot?.textures) ? fxRoot.textures : []; + const base = 0.94 + 0.06 * Math.sin(0.0018 * t + hash01(inst.seed ^ 0xdeadbeef, 0) * 6.28); + cont.alpha = 1; for (const child of cont.children ?? []) { const fx = (child as any).__fx; if (!fx) continue; - const fall = typeof fx.fallSpeed === 'number' ? fx.fallSpeed : 1; - const spread = typeof fx.spread === 'number' ? fx.spread : 1; + if (textures.length > 0) { + const frameT = (t + (fx.phase ?? 0) * RAIN_VFX_LOOP_MS) % RAIN_VFX_LOOP_MS; + const frameIndex = Math.min( + textures.length - 1, + Math.floor((frameT / RAIN_VFX_LOOP_MS) * textures.length), + ); + const frame = textures[frameIndex]; + if (frame && child.texture !== frame) { + child.texture = frame; + } + } const alphaMul = typeof fx.alphaMul === 'number' ? fx.alphaMul : 1; const wob = typeof fx.drift === 'number' ? fx.drift : 0; - - // Позиция: вокруг базовой точки, капля “перезапускается” по кругу вниз. - const yOff = ((t * 0.04 * fall + (fx.phase ?? 0) * 120) % wrap) - wrap / 2; - const xOff = Math.sin(0.0022 * t + (fx.phase ?? 0)) * wob * 0.6; - const gust = Math.sin(0.00055 * t + hash01(inst.seed ^ 0xabcdef01, fx.bx | 0) * 6.28) * wob; - child.x = fx.bx + xOff + gust; - child.y = fx.by + yOff * spread; - - // Альфа: немного “мерцает”, но не пропадает полностью. - const a = cont.alpha * (0.72 + 0.28 * Math.sin(0.0032 * t + (fx.phase ?? 0))) * alphaMul; - child.alpha = Math.max(0.68, Math.min(1, a)); + child.x = fx.bx + Math.sin(0.0011 * t + (fx.phase ?? 0) * 6.28) * wob; + child.y = fx.by + Math.cos(0.0014 * t + (fx.phase ?? 0) * 6.28) * wob * 0.35; + child.width = typeof fx.w0 === 'number' ? fx.w0 : child.width; + child.height = typeof fx.h0 === 'number' ? fx.h0 : child.height; + child.alpha = Math.max(0.25, Math.min(1, inst.opacity * base * alphaMul)); } } if (inst.type === 'lightning') { - const g = node; + const cont = node; const life = Math.max(1, inst.lifetimeMs); if (t >= life) { - g.visible = false; + cont.visible = false; continue; } - g.visible = true; - redrawLightning(pixi, g, inst, viewport, t, life); + cont.visible = true; + redrawLightningVfx(cont, inst, viewport, t, life); } if (inst.type === 'sunbeam') { - const g = node; + const cont = node; const life = Math.max(1, inst.lifetimeMs); if (t >= life) { - g.visible = false; + cont.visible = false; continue; } - g.visible = true; - redrawSunbeam(pixi, g, inst, viewport, t, life); + cont.visible = true; + redrawPulseDischargeVfx(cont, inst, viewport, t, life); } if (inst.type === 'poisonCloud') { @@ -649,7 +765,7 @@ function animateNodes( continue; } cont.visible = true; - redrawPoisonCloud(pixi, cont, inst, viewport, t, life); + redrawPoisonCloud(cont, inst, viewport, t, life); } if (inst.type === 'freeze') { @@ -761,7 +877,7 @@ function buildWaterFillSprite( inst: Extract, viewport: { x: number; y: number; w: number; h: number }, halfW: number, -): { sprite: any } | null { +): { sprite: any; mask: any; textures: any[] } | null { const pts = inst.points; if (pts.length === 0) return null; const { x: vx, y: vy, w, h } = viewport; @@ -788,11 +904,10 @@ function buildWaterFillSprite( if (!ctx) return null; const ox = -minX + pad / 2; const oy = -minY + pad / 2; - const alpha = Math.max(0.08, Math.min(0.82, inst.opacity)); ctx.lineCap = 'round'; ctx.lineJoin = 'round'; - ctx.strokeStyle = `rgba(95, 195, 255, ${String(alpha)})`; - ctx.fillStyle = `rgba(95, 195, 255, ${String(alpha)})`; + ctx.strokeStyle = 'rgba(255, 255, 255, 1)'; + ctx.fillStyle = 'rgba(255, 255, 255, 1)'; ctx.lineWidth = halfW * 2; if (pxPts.length === 1) { @@ -810,276 +925,596 @@ function buildWaterFillSprite( ctx.stroke(); } - const texture = pixi.Texture.from(canvas); - const sprite = new pixi.Sprite(texture); + const maskTexture = pixi.Texture.from(canvas); + const mask = new pixi.Sprite(maskTexture); + mask.x = minX - pad / 2; + mask.y = minY - pad / 2; + mask.width = cw; + mask.height = ch; + mask.renderable = false; + + const textures = getWaterVfxFrameTextures(pixi); + const sprite = new pixi.Sprite(textures[0] ?? pixi.Texture.EMPTY); sprite.x = minX - pad / 2; sprite.y = minY - pad / 2; - return { sprite }; + sprite.width = cw; + sprite.height = ch; + sprite.blendMode = pixi.BLEND_MODES?.NORMAL ?? 0; + sprite.mask = mask; + return { sprite, mask, textures }; } function redrawPoisonCloud( - pixi: any, cont: any, inst: Extract, viewport: { x: number; y: number; w: number; h: number }, t: number, life: number, ) { - void pixi; - const fx = (cont as any).__fx as { particles: PoisonParticleFx[]; skull: any }; - const particles = fx.particles; - const skull = fx.skull; + const fx = (cont as any).__fx; + const sprite = fx?.sprite; + const skull = fx?.skull; + const textures = Array.isArray(fx?.textures) ? fx.textures : []; + if (!sprite) return; + const { x: vx, y: vy, w, h } = viewport; + const minDim = Math.min(w, h); const cx = vx + inst.at.x * w; const cy = vy + inst.at.y * h; - const R = Math.max(14, inst.radiusN * Math.min(w, h) * 1.15); - const inten = Math.max(0, Math.min(1.2, inst.intensity)); + const R = Math.max(18, inst.radiusN * minDim * 1.35); + const p = Math.max(0, Math.min(1, t / Math.min(life, DUST_BURST_VFX_DURATION_MS))); + const fadeIn = Math.min(1, p / 0.08); + const fadeOut = Math.min(1, (1 - p) / 0.18); + const alpha = Math.max(0, Math.min(1, fadeIn * fadeOut * inst.intensity)); - const u = t / Math.max(1e-6, life); - const stemPhase = smoothstep01(Math.min(1, u / 0.34)); - const capPhase = u < 0.14 ? 0 : smoothstep01((u - 0.14) / Math.max(1e-6, 0.44)); - - let alphaM = 1; - if (u >= 0.66) { - alphaM = 1 - smoothstep01((u - 0.66) / Math.max(1e-6, 0.34)); - } - const aGlobal = Math.max(0, Math.min(1, inten * alphaM)); - if (aGlobal < 0.003) { - if (skull) skull.visible = false; - for (const p of particles) p.s.visible = false; - return; + const frame = textures[Math.min(textures.length - 1, Math.floor(p * textures.length))]; + if (frame && sprite.texture !== frame) { + sprite.texture = frame; } - const stemH = R * 1.1; - const baseHalfW = R * 0.72; - const topHalfW = R * 0.1; - const stemTopY = cy - stemH * stemPhase; - const capCenterY = stemTopY - R * 0.12 * Math.max(0.2, capPhase); - const capRad = R * 1.25 * capPhase; - const wobble = 0.04 * Math.sin(t * 0.014 + inst.seed * 0.01); - const particleR = 0.32; - - for (const p of particles) { - const s = p.s; - if (p.kind === 'stem') { - s.visible = stemPhase > 0.02; - if (!s.visible) continue; - const frac = p.uStem; - const z = frac * stemH * stemPhase; - const y = cy - z; - const mix = frac; - const halfAtY = baseHalfW * (1 - mix) + topHalfW * mix; - const spread = halfAtY * (0.82 + 0.18 * Math.sin(p.flick + t * 0.008)); - s.x = cx + p.side * spread + wobble * R * 0.08; - s.y = y + Math.sin(p.flick * 1.7 + t * 0.01) * 2.5; - const pr = stemPhase * (0.2 + 0.8 * smoothstep01(frac * 1.15)); - s.alpha = Math.max(0, Math.min(1, aGlobal * 0.55 * pr * (0.65 + 0.35 * stemPhase))); - s.scale?.set?.(p.sz * (0.75 + 0.35 * stemPhase) * particleR); - } else { - const vis = capPhase; - if (vis < 0.02) { - s.visible = false; - continue; - } - s.visible = true; - const ang = p.capAng + wobble * 0.4; - const rd = capRad * Math.min(1, p.capDist * 1.02 + 0.02); - s.x = cx + Math.cos(ang) * rd + Math.sin(t * 0.011 + p.flick) * 3; - s.y = capCenterY + Math.sin(ang) * rd * 0.48; - s.alpha = Math.max( - 0, - Math.min(1, aGlobal * 0.52 * vis * (0.75 + 0.25 * Math.sin(t * 0.019 + p.flick))), - ); - s.scale?.set?.(p.sz * (0.55 + 0.55 * vis) * particleR); - } - } + const width = Math.max(R * 5.2, minDim * 0.18); + sprite.x = cx; + sprite.y = cy; + sprite.width = width; + sprite.height = width / DUST_BURST_VFX_FRAME_ASPECT; + sprite.alpha = alpha; if (skull) { - const show = stemPhase > 0.72 && capPhase > 0.28; + const show = p > 0.18 && p < 0.92; skull.visible = show; - skull.alpha = aGlobal * (show ? 0.94 : 0); - skull.x = cx + wobble * R * 0.05; - skull.y = capCenterY - capRad * 0.72 - R * 0.28; - const fs = Math.max(16, Math.min(86, R * 0.48 + capRad * 0.38)); + skull.alpha = show ? Math.max(0, Math.min(1, alpha * 0.95)) : 0; + skull.x = cx; + skull.y = cy - R * 0.72; + const fs = Math.max(18, Math.min(92, R * 0.92)); if (skull.style && typeof skull.style === 'object' && 'fontSize' in skull.style) { (skull.style as { fontSize: number }).fontSize = fs; } } } -function redrawSunbeam( - pixi: any, - g: any, +function redrawPulseDischargeVfx( + cont: any, inst: Extract, viewport: { x: number; y: number; w: number; h: number }, t: number, life: number, ) { - void pixi; + const fx = (cont as any).__fx; + const sprite = fx?.sprite; + const g = fx?.g; + const textures = Array.isArray(fx?.textures) ? fx.textures : []; + if (!sprite || !g) return; + const { x: vx, y: vy, w, h } = viewport; + const minDim = Math.min(w, h); const sx = vx + inst.start.x * w; const sy = vy + inst.start.y * h; const ex = vx + inst.end.x * w; const ey = vy + inst.end.y * h; - const width = Math.max(1, inst.widthN * Math.min(w, h)); - const inten = Math.max(0, Math.min(1.2, inst.intensity)); + const dx = ex - sx; + const dy = ey - sy; + const len = Math.max(1, Math.hypot(dx, dy)); + const p = Math.max(0, Math.min(1, t / Math.min(life, PULSE_DISCHARGE_VFX_DURATION_MS))); + const fadeIn = Math.min(1, p / 0.08); + const fadeOut = Math.min(1, (1 - p) / 0.16); + const alpha = Math.max(0, Math.min(1, fadeIn * fadeOut * inst.intensity)); + + const frame = textures[Math.min(textures.length - 1, Math.floor(p * textures.length))]; + if (frame && sprite.texture !== frame) { + sprite.texture = frame; + } + + sprite.x = ex; + sprite.y = ey; + sprite.rotation = Math.atan2(dy, dx); + sprite.width = Math.max(len * 1.04, minDim * 0.28); + sprite.height = Math.max(inst.widthN * minDim * 20, sprite.width / PULSE_DISCHARGE_VFX_FRAME_ASPECT); + sprite.alpha = alpha; + g.clear(); - - const lifePos = Math.max(1e-6, life); - // Фаза 1: рост сверху вниз; фаза 2: полный луч + лёгкая пульсация; фаза 3: затухание. - const revealMs = lifePos * 0.28; - const pulseMs = lifePos * 0.47; - const fadeMs = lifePos * 0.25; - const tRevealEnd = revealMs; - const tPulseEnd = revealMs + pulseMs; - - const revealLin = Math.min(1, t / Math.max(1e-6, revealMs)); - const revealEase = t < tRevealEnd ? smoothstep01(revealLin) : 1; - - let pulseMul = 1; - if (t >= tRevealEnd && t < tPulseEnd) { - const u = t - tRevealEnd; - const pulses = 5; - const omega = (pulses * 2 * Math.PI) / Math.max(1e-6, pulseMs); - const phase = (inst.seed & 4095) * 0.00153; - pulseMul = 1 + 0.1 * Math.sin(u * omega + phase); - } - - const vis = t < tPulseEnd ? 1 : 1 - smoothstep01((t - tPulseEnd) / Math.max(1e-6, fadeMs)); - if (vis < 0.002) return; - - const bx = sx + (ex - sx) * revealEase; - const by = sy + (ey - sy) * revealEase; - const intenVis = inten * vis * pulseMul; - - // Засветка кадра: нарастает с лучом; в фазе пульса слегка «дышит»; затем гаснет. - const veilMod = t >= tRevealEnd && t < tPulseEnd ? pulseMul : 1; - const veilA = Math.max(0, Math.min(0.24, 0.22 * revealEase * vis * inten * veilMod)); - if (veilA > 0.002) { - g.rect(vx, vy, w, h); - g.fill({ color: 0xfff4b8, alpha: veilA }); - } - - const minDim = Math.min(w, h); - const segLen = Math.hypot(bx - sx, by - sy); - if (segLen < 0.8) return; - - const outerW = Math.max(width * 26, minDim * 0.9); - g.moveTo(sx, sy); - g.lineTo(bx, by); - g.stroke({ - color: 0xffec78, - width: outerW, - alpha: Math.min(1, 0.034 * intenVis), - cap: 'round', - }); - - const haloLayers: { mult: number; color: number; a: number }[] = [ - { mult: 18, color: 0xffe040, a: 0.048 * intenVis }, - { mult: 12, color: 0xffea65, a: 0.075 * intenVis }, - { mult: 7, color: 0xfff090, a: 0.11 * intenVis }, - { mult: 4, color: 0xfffacd, a: 0.16 * intenVis }, - { mult: 2.2, color: 0xffffee, a: 0.22 * intenVis }, - ]; - for (const layer of haloLayers) { - const lw = Math.max(width * layer.mult, width + minDim * 0.025); - const a = Math.min(1, layer.a); - if (a < 0.004) continue; - g.moveTo(sx, sy); - g.lineTo(bx, by); - g.stroke({ color: layer.color, width: lw, alpha: a, cap: 'round' }); - } - - const coreLayers: { wmul: number; color: number; a: number }[] = [ - { wmul: 2.4, color: 0xffffcc, a: Math.min(1, 0.45 * intenVis) }, - { wmul: 1.05, color: 0xffffaa, a: Math.min(1, 0.78 * intenVis) }, - { wmul: 0.4, color: 0xffffff, a: Math.min(1, 0.98 * intenVis) }, - ]; - for (const cl of coreLayers) { - const lw = Math.max(1.2, width * cl.wmul); - g.moveTo(sx, sy); - g.lineTo(bx, by); - g.stroke({ color: cl.color, width: lw, alpha: cl.a, cap: 'round' }); - } } -function redrawLightning( - pixi: any, - g: any, +function lightningTrailVfxFrameUrl(frameIndex: number): string { + return new URL( + `vfx/lightning-trail-01/frame_${String(frameIndex).padStart(2, '0')}.webp`, + window.location.href, + ).href; +} + +function electricAccentFrameUrl(frameIndex: number): string { + return new URL( + `vfx/electric-accent-10/frame_${String(frameIndex).padStart(2, '0')}.webp`, + window.location.href, + ).href; +} + +function fogVfxFrameUrl(frameIndex: number): string { + return new URL( + `vfx/smokey-atmosphere-1-round/frame_${String(frameIndex).padStart(3, '0')}.webp`, + window.location.href, + ).href; +} + +function groundFireVfxFrameUrl(frameIndex: number): string { + return new URL(`vfx/small-fire/frame_${String(frameIndex).padStart(3, '0')}.webp`, window.location.href) + .href; +} + +function rainVfxFrameUrl(frameIndex: number): string { + return new URL( + `vfx/real-medium-rain-1/frame_${String(frameIndex).padStart(3, '0')}.webp`, + window.location.href, + ).href; +} + +function waterVfxFrameUrl(frameIndex: number): string { + return new URL( + `vfx/generated-water/frame_${String(frameIndex).padStart(3, '0')}.webp`, + window.location.href, + ).href; +} + +function pulseDischargeFrameUrl(frameIndex: number): string { + return new URL( + `vfx/pulse-discharge/frame_${String(frameIndex).padStart(3, '0')}.webp`, + window.location.href, + ).href; +} + +function dustBurstFrameUrl(frameIndex: number): string { + return new URL( + `vfx/dust-burst-large-2/frame_${String(frameIndex).padStart(3, '0')}.webp`, + window.location.href, + ).href; +} + +function getLightningVfxFrameTextures(pixi: any): any[] { + if (pixi !== null && (typeof pixi === 'object' || typeof pixi === 'function')) { + const existing = lightningVfxFrameTextureCache.get(pixi); + if (existing) return existing.textures; + } + + const textures: any[] = []; + if (pixi !== null && (typeof pixi === 'object' || typeof pixi === 'function')) { + lightningVfxFrameTextureCache.set(pixi, { textures }); + } + void preloadLightningVfxFrameTextures(pixi); + return textures; +} + +function getElectricAccentFrameTextures(pixi: any): any[] { + if (pixi !== null && (typeof pixi === 'object' || typeof pixi === 'function')) { + const existing = electricAccentFrameTextureCache.get(pixi); + if (existing) return existing.textures; + } + + const textures: any[] = []; + if (pixi !== null && (typeof pixi === 'object' || typeof pixi === 'function')) { + electricAccentFrameTextureCache.set(pixi, { textures }); + } + void preloadElectricAccentFrameTextures(pixi); + return textures; +} + +function getFogVfxFrameTextures(pixi: any): any[] { + if (pixi !== null && (typeof pixi === 'object' || typeof pixi === 'function')) { + const existing = fogVfxFrameTextureCache.get(pixi); + if (existing) return existing.textures; + } + + const textures: any[] = []; + if (pixi !== null && (typeof pixi === 'object' || typeof pixi === 'function')) { + fogVfxFrameTextureCache.set(pixi, { textures }); + } + void preloadFogVfxFrameTextures(pixi); + return textures; +} + +function getGroundFireVfxFrameTextures(pixi: any): any[] { + if (pixi !== null && (typeof pixi === 'object' || typeof pixi === 'function')) { + const existing = groundFireVfxFrameTextureCache.get(pixi); + if (existing) return existing.textures; + } + + const textures: any[] = []; + if (pixi !== null && (typeof pixi === 'object' || typeof pixi === 'function')) { + groundFireVfxFrameTextureCache.set(pixi, { textures }); + } + void preloadGroundFireVfxFrameTextures(pixi); + return textures; +} + +function getRainVfxFrameTextures(pixi: any): any[] { + if (pixi !== null && (typeof pixi === 'object' || typeof pixi === 'function')) { + const existing = rainVfxFrameTextureCache.get(pixi); + if (existing) return existing.textures; + } + + const textures: any[] = []; + if (pixi !== null && (typeof pixi === 'object' || typeof pixi === 'function')) { + rainVfxFrameTextureCache.set(pixi, { textures }); + } + void preloadRainVfxFrameTextures(pixi); + return textures; +} + +function getWaterVfxFrameTextures(pixi: any): any[] { + if (pixi !== null && (typeof pixi === 'object' || typeof pixi === 'function')) { + const existing = waterVfxFrameTextureCache.get(pixi); + if (existing) return existing.textures; + } + + const textures: any[] = []; + if (pixi !== null && (typeof pixi === 'object' || typeof pixi === 'function')) { + waterVfxFrameTextureCache.set(pixi, { textures }); + } + void preloadWaterVfxFrameTextures(pixi); + return textures; +} + +function getPulseDischargeFrameTextures(pixi: any): any[] { + if (pixi !== null && (typeof pixi === 'object' || typeof pixi === 'function')) { + const existing = pulseDischargeFrameTextureCache.get(pixi); + if (existing) return existing.textures; + } + + const textures: any[] = []; + if (pixi !== null && (typeof pixi === 'object' || typeof pixi === 'function')) { + pulseDischargeFrameTextureCache.set(pixi, { textures }); + } + void preloadPulseDischargeFrameTextures(pixi); + return textures; +} + +function getDustBurstFrameTextures(pixi: any): any[] { + if (pixi !== null && (typeof pixi === 'object' || typeof pixi === 'function')) { + const existing = dustBurstFrameTextureCache.get(pixi); + if (existing) return existing.textures; + } + + const textures: any[] = []; + if (pixi !== null && (typeof pixi === 'object' || typeof pixi === 'function')) { + dustBurstFrameTextureCache.set(pixi, { textures }); + } + void preloadDustBurstFrameTextures(pixi); + return textures; +} + +function preloadLightningVfxFrameTextures(pixi: any): Promise { + if (pixi === null || (typeof pixi !== 'object' && typeof pixi !== 'function')) { + return Promise.resolve([]); + } + + const cached = lightningVfxFrameTextureCache.get(pixi); + if (cached?.textures.length) return Promise.resolve(cached.textures); + if (cached?.loading) return cached.loading; + + const record = cached ?? { textures: [] }; + const loading = Promise.all( + Array.from({ length: LIGHTNING_VFX_FRAME_COUNT }, async (_, i) => { + const image = await loadImageElement(lightningTrailVfxFrameUrl(i + 1)); + return pixi.Texture.from(image) ?? pixi.Texture.EMPTY; + }), + ) + .then((textures) => { + record.textures.splice(0, record.textures.length, ...textures); + return record.textures; + }) + .catch(() => record.textures); + + record.loading = loading; + lightningVfxFrameTextureCache.set(pixi, record); + return loading; +} + +function preloadElectricAccentFrameTextures(pixi: any): Promise { + if (pixi === null || (typeof pixi !== 'object' && typeof pixi !== 'function')) { + return Promise.resolve([]); + } + + const cached = electricAccentFrameTextureCache.get(pixi); + if (cached?.textures.length) return Promise.resolve(cached.textures); + if (cached?.loading) return cached.loading; + + const record = cached ?? { textures: [] }; + const loading = Promise.all( + Array.from({ length: ELECTRIC_ACCENT_FRAME_COUNT }, async (_, i) => { + const image = await loadImageElement(electricAccentFrameUrl(i + 1)); + return pixi.Texture.from(image) ?? pixi.Texture.EMPTY; + }), + ) + .then((textures) => { + record.textures.splice(0, record.textures.length, ...textures); + return record.textures; + }) + .catch(() => record.textures); + + record.loading = loading; + electricAccentFrameTextureCache.set(pixi, record); + return loading; +} + +function preloadFogVfxFrameTextures(pixi: any): Promise { + if (pixi === null || (typeof pixi !== 'object' && typeof pixi !== 'function')) { + return Promise.resolve([]); + } + + const cached = fogVfxFrameTextureCache.get(pixi); + if (cached?.textures.length) return Promise.resolve(cached.textures); + if (cached?.loading) return cached.loading; + + const record = cached ?? { textures: [] }; + const loading = Promise.all( + Array.from({ length: FOG_VFX_FRAME_COUNT }, async (_, i) => { + const image = await loadImageElement(fogVfxFrameUrl(i + 1)); + return pixi.Texture.from(image) ?? pixi.Texture.EMPTY; + }), + ) + .then((textures) => { + record.textures.splice(0, record.textures.length, ...textures); + return record.textures; + }) + .catch(() => record.textures); + + record.loading = loading; + fogVfxFrameTextureCache.set(pixi, record); + return loading; +} + +function preloadGroundFireVfxFrameTextures(pixi: any): Promise { + if (pixi === null || (typeof pixi !== 'object' && typeof pixi !== 'function')) { + return Promise.resolve([]); + } + + const cached = groundFireVfxFrameTextureCache.get(pixi); + if (cached?.textures.length) return Promise.resolve(cached.textures); + if (cached?.loading) return cached.loading; + + const record = cached ?? { textures: [] }; + const loading = Promise.all( + Array.from({ length: GROUND_FIRE_VFX_FRAME_COUNT }, async (_, i) => { + const image = await loadImageElement(groundFireVfxFrameUrl(i + 1)); + return pixi.Texture.from(image) ?? pixi.Texture.EMPTY; + }), + ) + .then((textures) => { + record.textures.splice(0, record.textures.length, ...textures); + return record.textures; + }) + .catch(() => record.textures); + + record.loading = loading; + groundFireVfxFrameTextureCache.set(pixi, record); + return loading; +} + +function preloadRainVfxFrameTextures(pixi: any): Promise { + if (pixi === null || (typeof pixi !== 'object' && typeof pixi !== 'function')) { + return Promise.resolve([]); + } + + const cached = rainVfxFrameTextureCache.get(pixi); + if (cached?.textures.length) return Promise.resolve(cached.textures); + if (cached?.loading) return cached.loading; + + const record = cached ?? { textures: [] }; + const loading = Promise.all( + Array.from({ length: RAIN_VFX_FRAME_COUNT }, async (_, i) => { + const image = await loadImageElement(rainVfxFrameUrl(i + 1)); + return pixi.Texture.from(image) ?? pixi.Texture.EMPTY; + }), + ) + .then((textures) => { + record.textures.splice(0, record.textures.length, ...textures); + return record.textures; + }) + .catch(() => record.textures); + + record.loading = loading; + rainVfxFrameTextureCache.set(pixi, record); + return loading; +} + +function preloadWaterVfxFrameTextures(pixi: any): Promise { + if (pixi === null || (typeof pixi !== 'object' && typeof pixi !== 'function')) { + return Promise.resolve([]); + } + + const cached = waterVfxFrameTextureCache.get(pixi); + if (cached?.textures.length) return Promise.resolve(cached.textures); + if (cached?.loading) return cached.loading; + + const record = cached ?? { textures: [] }; + const loading = Promise.all( + Array.from({ length: WATER_VFX_FRAME_COUNT }, async (_, i) => { + const image = await loadImageElement(waterVfxFrameUrl(i + 1)); + return pixi.Texture.from(image) ?? pixi.Texture.EMPTY; + }), + ) + .then((textures) => { + record.textures.splice(0, record.textures.length, ...textures); + return record.textures; + }) + .catch(() => record.textures); + + record.loading = loading; + waterVfxFrameTextureCache.set(pixi, record); + return loading; +} + +function preloadPulseDischargeFrameTextures(pixi: any): Promise { + if (pixi === null || (typeof pixi !== 'object' && typeof pixi !== 'function')) { + return Promise.resolve([]); + } + + const cached = pulseDischargeFrameTextureCache.get(pixi); + if (cached?.textures.length) return Promise.resolve(cached.textures); + if (cached?.loading) return cached.loading; + + const record = cached ?? { textures: [] }; + const loading = Promise.all( + Array.from({ length: PULSE_DISCHARGE_VFX_FRAME_COUNT }, async (_, i) => { + const image = await loadImageElement(pulseDischargeFrameUrl(i + 1)); + return pixi.Texture.from(image) ?? pixi.Texture.EMPTY; + }), + ) + .then((textures) => { + record.textures.splice(0, record.textures.length, ...textures); + return record.textures; + }) + .catch(() => record.textures); + + record.loading = loading; + pulseDischargeFrameTextureCache.set(pixi, record); + return loading; +} + +function preloadDustBurstFrameTextures(pixi: any): Promise { + if (pixi === null || (typeof pixi !== 'object' && typeof pixi !== 'function')) { + return Promise.resolve([]); + } + + const cached = dustBurstFrameTextureCache.get(pixi); + if (cached?.textures.length) return Promise.resolve(cached.textures); + if (cached?.loading) return cached.loading; + + const record = cached ?? { textures: [] }; + const loading = Promise.all( + Array.from({ length: DUST_BURST_VFX_FRAME_COUNT }, async (_, i) => { + const image = await loadImageElement(dustBurstFrameUrl(i + 1)); + return pixi.Texture.from(image) ?? pixi.Texture.EMPTY; + }), + ) + .then((textures) => { + record.textures.splice(0, record.textures.length, ...textures); + return record.textures; + }) + .catch(() => record.textures); + + record.loading = loading; + dustBurstFrameTextureCache.set(pixi, record); + return loading; +} + +async function loadImageElement(src: string): Promise { + const image = new Image(); + image.decoding = 'async'; + image.src = src; + if (!image.complete) { + await new Promise((resolve, reject) => { + image.addEventListener('load', () => resolve(), { once: true }); + image.addEventListener('error', () => reject(new Error(`Failed to load image: ${src}`)), { + once: true, + }); + }); + } + await image.decode?.().catch(() => undefined); + return image; +} + +function redrawLightningVfx( + cont: any, inst: Extract, viewport: { x: number; y: number; w: number; h: number }, t: number, life: number, ) { - void pixi; + const fx = (cont as any).__fx; + const sprite = fx?.sprite; + const accentSprite = fx?.accentSprite; + const g = fx?.g; + const textures = Array.isArray(fx?.textures) ? fx.textures : []; + const accentTextures = Array.isArray(fx?.accentTextures) ? fx.accentTextures : []; + if (!sprite || !g) return; + const { x: vx, y: vy, w, h } = viewport; + const minDim = Math.min(w, h); const ex = vx + inst.end.x * w; const ey = vy + inst.end.y * h; - const width = Math.max(1, inst.widthN * Math.min(w, h)); - g.clear(); + const strikeLife = Math.min(Math.max(1, life), LIGHTNING_VFX_STRIKE_MS); + const p = Math.max(0, Math.min(1, t / strikeLife)); + const fadeIn = Math.min(1, p / 0.08); + const fadeOut = Math.min(1, (1 - p) / 0.18); + const alpha = Math.max(0, Math.min(1, fadeIn * fadeOut * inst.intensity)); + const hasFrameTextures = textures.length >= LIGHTNING_VFX_FRAME_COUNT; - // Фаза удара: короткая яркая вспышка, которая на мгновение “заслоняет” весь экран. - const flashMs = Math.min(140, Math.max(60, Math.floor(life * 0.8))); - const flashT = Math.max(0, Math.min(1, 1 - t / flashMs)); - const overlayAlpha = Math.max(0, Math.min(0.5, 0.5 * flashT * inst.intensity)); - if (overlayAlpha > 0.001) { - g.rect(vx, vy, w, h); - g.fill({ color: 0xffe066, alpha: overlayAlpha }); + const frame = textures[Math.min(textures.length - 1, Math.floor(p * textures.length))]; + if (frame && sprite.texture !== frame) { + sprite.texture = frame; } - // Тонкая “молния”: ломаная сверху -> середина -> отвод вправо -> точка удара. - const sx = ex; - const sy = vy; - const coreAlpha = Math.max(0, Math.min(1, (0.9 + 0.1 * flashT) * inst.intensity)); + const strikeHeight = Math.max(minDim * 0.22, ey - vy); + sprite.anchor?.set?.(0.5, LIGHTNING_VFX_IMPACT_ANCHOR_Y); + sprite.x = ex; + sprite.y = ey; + sprite.rotation = 0; + sprite.height = strikeHeight * 1.04; + sprite.width = Math.max(inst.widthN * minDim * 18, strikeHeight * LIGHTNING_VFX_FRAME_ASPECT * 0.72); + sprite.alpha = Math.min(1, alpha); - // Небольшой дрожащий джиттер, чтобы молния выглядела живой. - const n = Math.floor(t / 16); - const jx0 = (hash01(inst.seed ^ 0x111, n) - 0.5) * width * 0.35; - const jy0 = (hash01(inst.seed ^ 0x222, n) - 0.5) * width * 0.25; - const jx1 = (hash01(inst.seed ^ 0x333, n + 1) - 0.5) * width * 0.35; - const jy1 = (hash01(inst.seed ^ 0x444, n + 1) - 0.5) * width * 0.25; + if (accentSprite) { + const accentEndMs = ELECTRIC_ACCENT_START_MS + ELECTRIC_ACCENT_DURATION_MS; + const accentP = Math.max(0, Math.min(1, (t - ELECTRIC_ACCENT_START_MS) / ELECTRIC_ACCENT_DURATION_MS)); + const accentFrame = + accentTextures[Math.min(accentTextures.length - 1, Math.floor(accentP * accentTextures.length))]; + if (accentFrame && accentSprite.texture !== accentFrame) { + accentSprite.texture = accentFrame; + } + const accentFadeOut = Math.min(1, (accentEndMs - t) / 130); + const accentAlpha = + t >= ELECTRIC_ACCENT_START_MS && + t <= accentEndMs && + accentTextures.length >= ELECTRIC_ACCENT_FRAME_COUNT + ? Math.max(0, Math.min(1, accentFadeOut * inst.intensity)) + : 0; + const accentWidth = Math.max(minDim * 0.2, Math.min(minDim * 0.48, inst.widthN * minDim * 18)); + accentSprite.x = ex; + accentSprite.y = ey; + accentSprite.rotation = 0; + accentSprite.width = accentWidth; + accentSprite.height = accentWidth / ELECTRIC_ACCENT_FRAME_ASPECT; + accentSprite.alpha = accentAlpha; + } - const midY = ey * 0.5; - const midX = sx + (hash01(inst.seed ^ 0x777, 0) - 0.5) * width * 0.35; - const kinkY = ey * 0.62; - const kinkDx = (0.12 + 0.08 * hash01(inst.seed ^ 0x888, 0)) * Math.min(w, h); - const kinkX = midX + kinkDx; // “сворачиваем вправо” - - // Рисуем ломаную как одну линию. - g.moveTo(sx + jx0, sy + jy0); - g.lineTo(midX + jx1, midY + jy1); - g.lineTo(kinkX + jx0, kinkY + jy0); - g.lineTo(ex - jx1, ey - jy1); - - // Тонкое белое ядро + жёлтый ореол (тоже тонкий), чтобы выглядело как молния, а не маркер. - const thin = Math.max(1, width * 0.28); - g.stroke({ color: 0xffffff, width: thin, alpha: Math.min(1, coreAlpha) }); - g.stroke({ color: 0xffd000, width: Math.max(1.5, thin * 1.9), alpha: Math.min(1, coreAlpha * 0.28) }); - - // Сеть разрядов от точки удара: короткие “ветки”, которые быстро исчезают. - // Оставляем только scorch-инстанс (он живёт отдельно), поэтому здесь всё затухает по flashT. - const branchAlpha = Math.min(1, coreAlpha) * flashT; - if (branchAlpha > 0.01) { - const baseR = (0.06 + 0.08 * hash01(inst.seed ^ 0x999, 0)) * Math.min(w, h); - const branches = 10; - for (let i = 0; i < branches; i += 1) { - const a0 = (i / branches) * Math.PI * 2 + (hash01(inst.seed ^ 0x5151, i) - 0.5) * 0.45; - const r0 = baseR * (0.55 + 0.65 * hash01(inst.seed ^ 0x6161, i)); - const r1 = r0 * (0.55 + 0.4 * hash01(inst.seed ^ 0x7171, i)); - - const x1 = ex + Math.cos(a0) * r0; - const y1 = ey + Math.sin(a0) * r0; - // маленький “зубчик” посередине, чтобы ветка была ломаной - const a1 = a0 + (hash01(inst.seed ^ 0x8181, i) - 0.5) * 1.0; - const x2 = ex + Math.cos(a1) * r1; - const y2 = ey + Math.sin(a1) * r1; - - g.moveTo(ex, ey); - g.lineTo(x2, y2); - g.lineTo(x1, y1); - const bw = Math.max(1, thin * 0.85); - g.stroke({ color: 0xffffff, width: bw, alpha: Math.min(1, branchAlpha * 0.75) }); - g.stroke({ color: 0xffd000, width: Math.max(1.5, bw * 1.8), alpha: Math.min(1, branchAlpha * 0.18) }); + g.clear(); + if (!hasFrameTextures) { + const boltAlpha = Math.max(0, Math.min(1, alpha * 1.15)); + const topY = Math.max(vy, ey - strikeHeight); + const segments = 10; + let px = ex; + let py = topY; + for (let i = 1; i <= segments; i += 1) { + const u = i / segments; + const nextY = topY + (ey - topY) * u; + const wobble = + (hash01(inst.seed ^ 0x91e10f, i) - 0.5) * inst.widthN * minDim * 7 * (1 - Math.abs(u - 0.5)); + const nextX = ex + wobble; + g.moveTo(px, py); + g.lineTo(nextX, nextY); + g.stroke({ color: 0x7eeeff, width: Math.max(2, inst.widthN * minDim * 2.5), alpha: boltAlpha * 0.65 }); + g.stroke({ color: 0xffffff, width: Math.max(1, inst.widthN * minDim * 1.1), alpha: boltAlpha }); + px = nextX; + py = nextY; } } } @@ -1158,10 +1593,10 @@ function computeSceneShake( if (inst.type !== 'lightning') continue; const t = Math.max(0, nowMs - inst.createdAtMs); const life = Math.max(1, inst.lifetimeMs); - const flashMs = Math.min(140, Math.max(60, Math.floor(life * 0.8))); + const flashMs = Math.min(420, Math.max(160, Math.floor(life * 0.42))); if (t > flashMs) continue; const flashT = Math.max(0, Math.min(1, 1 - t / flashMs)); - const k = flashT * Math.max(0, inst.intensity); + const k = flashT * Math.max(0, inst.intensity) * 1.4; if (k > best) { best = k; bestSeed = inst.seed; @@ -1177,228 +1612,12 @@ function computeSceneShake( return { x: ox, y: oy }; } -let fogTextureCache: { key: string; texture: any } | null = null; -let fireTextureCache: { key: string; texture: any } | null = null; -let rainTextureCache: { key: string; texture: any } | null = null; -let poisonParticleTextureCache: { key: string; texture: any } | null = null; let scorchTextureCache: { key: string; texture: any } | null = null; let iceTextureCache: { key: string; texture: any } | null = null; let shadowTextureCache: { key: string; texture: any } | null = null; let freezeScreenTextureCache: Map | null = null; let darknessScreenTextureCache: Map | null = null; -/** Круглая мягкая точка для частиц яда (не квадрат `Texture.WHITE`). */ -function getPoisonParticleTexture(pixi: any): any { - const key = 'poison_particle_round_v1'; - if (poisonParticleTextureCache?.key === key) return poisonParticleTextureCache.texture; - - const size = 48; - const canvas = document.createElement('canvas'); - canvas.width = size; - canvas.height = size; - const ctx = canvas.getContext('2d'); - if (!ctx) { - const t = pixi.Texture.WHITE; - poisonParticleTextureCache = { key, texture: t }; - return t; - } - const cx = size / 2; - const cy = size / 2; - const r = size / 2 - 0.5; - ctx.clearRect(0, 0, size, size); - const grd = ctx.createRadialGradient(cx, cy, 0, cx, cy, r); - grd.addColorStop(0, 'rgba(255,255,255,1)'); - grd.addColorStop(0.88, 'rgba(255,255,255,0.95)'); - grd.addColorStop(1, 'rgba(255,255,255,0)'); - ctx.fillStyle = grd; - ctx.beginPath(); - ctx.arc(cx, cy, r, 0, Math.PI * 2); - ctx.fill(); - - const tex = pixi.Texture.from(canvas); - poisonParticleTextureCache = { key, texture: tex }; - return tex; -} - -function getFogTexture(pixi: any): any { - // Кэшируем один раз на процесс. Текстура — “дымка” с мягкой альфой. - const key = 'fog_v1'; - if (fogTextureCache?.key === key) return fogTextureCache.texture; - - const size = 256; - const canvas = document.createElement('canvas'); - canvas.width = size; - canvas.height = size; - const ctx = canvas.getContext('2d'); - if (!ctx) { - const t = pixi.Texture.WHITE; - fogTextureCache = { key, texture: t }; - return t; - } - - ctx.clearRect(0, 0, size, size); - // База: мягкий радиальный градиент. - const g = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2); - g.addColorStop(0, 'rgba(255,255,255,0.85)'); - g.addColorStop(0.55, 'rgba(255,255,255,0.35)'); - g.addColorStop(1, 'rgba(255,255,255,0.00)'); - ctx.fillStyle = g; - ctx.beginPath(); - ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2); - ctx.fill(); - - // Деталь: лёгкий шум, чтобы туман не выглядел “идеальным”. - const img = ctx.getImageData(0, 0, size, size); - for (let i = 0; i < img.data.length; i += 4) { - const a = img.data[i + 3] ?? 0; - if (a === 0) continue; - const n = (Math.random() - 0.5) * 28; - img.data[i] = clamp255((img.data[i] ?? 0) + n); - img.data[i + 1] = clamp255((img.data[i + 1] ?? 0) + n); - img.data[i + 2] = clamp255((img.data[i + 2] ?? 0) + n); - } - ctx.putImageData(img, 0, 0); - - const tex = pixi.Texture.from(canvas); - fogTextureCache = { key, texture: tex }; - return tex; -} - -function getFireTexture(pixi: any): any { - const key = 'fire_v2'; - if (fireTextureCache?.key === key) return fireTextureCache.texture; - - const size = 256; - const canvas = document.createElement('canvas'); - canvas.width = size; - canvas.height = size; - const ctx = canvas.getContext('2d'); - if (!ctx) { - const t = pixi.Texture.WHITE; - fireTextureCache = { key, texture: t }; - return t; - } - - ctx.clearRect(0, 0, size, size); - - // Градиент “пламени”: ярче внизу, мягко исчезает вверх/по краям. - const g = ctx.createRadialGradient(size / 2, size * 0.68, size * 0.04, size / 2, size * 0.62, size * 0.55); - g.addColorStop(0, 'rgba(255,255,255,0.95)'); - g.addColorStop(0.18, 'rgba(255,210,120,0.90)'); - g.addColorStop(0.42, 'rgba(255,140,40,0.70)'); - g.addColorStop(0.72, 'rgba(255,60,0,0.25)'); - g.addColorStop(1, 'rgba(255,0,0,0.00)'); - ctx.fillStyle = g; - ctx.beginPath(); - ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2); - ctx.fill(); - - // “Языки” пламени: рисуем полупрозрачные лепестки, сильнее внизу. - // Делается до шума, чтобы шум потом “рвал” край и делал пламя живым. - ctx.save(); - ctx.globalCompositeOperation = 'source-atop'; - for (let k = 0; k < 22; k += 1) { - const cx = size * (0.22 + 0.56 * Math.random()); - const baseY = size * (0.62 + 0.22 * Math.random()); - const height = size * (0.18 + 0.36 * Math.random()); - const width = size * (0.03 + 0.06 * Math.random()); - const bend = (Math.random() - 0.5) * width * 3.0; - const tipX = cx + bend; - const tipY = baseY - height; - - const grad = ctx.createLinearGradient(cx, baseY, tipX, tipY); - grad.addColorStop(0, 'rgba(255,255,255,0.32)'); - grad.addColorStop(0.35, 'rgba(255,190,90,0.22)'); - grad.addColorStop(1, 'rgba(255,50,0,0.00)'); - ctx.fillStyle = grad; - - ctx.beginPath(); - ctx.moveTo(cx - width, baseY); - ctx.quadraticCurveTo(cx - width * 0.4, baseY - height * 0.55, tipX, tipY); - ctx.quadraticCurveTo(cx + width * 0.45, baseY - height * 0.45, cx + width, baseY); - ctx.closePath(); - ctx.fill(); - } - ctx.restore(); - - // Немного “рваности” — шум по альфе + вертикальные полосы. - const img = ctx.getImageData(0, 0, size, size); - for (let y = 0; y < size; y += 1) { - for (let x = 0; x < size; x += 1) { - const i = (y * size + x) * 4; - const a = img.data[i + 3] ?? 0; - if (a === 0) continue; - const vy = 1 - y / (size - 1); - const stripe = Math.sin((x / size) * Math.PI * 10 + (y / size) * Math.PI * 1.5) * 0.5 + 0.5; - const n = (Math.random() - 0.5) * 110; - const boost = (0.25 + 0.75 * stripe) * (0.18 + 0.82 * vy); - img.data[i] = clamp255((img.data[i] ?? 0) + n * 0.28); - img.data[i + 1] = clamp255((img.data[i + 1] ?? 0) + n * 0.2); - img.data[i + 2] = clamp255((img.data[i + 2] ?? 0) + n * 0.07); - img.data[i + 3] = clamp255(a * boost + (Math.random() - 0.5) * 22); - } - } - ctx.putImageData(img, 0, 0); - - const tex = pixi.Texture.from(canvas); - fireTextureCache = { key, texture: tex }; - return tex; -} - -function getRainTexture(pixi: any): any { - const key = 'rain_v2'; - if (rainTextureCache?.key === key) return rainTextureCache.texture; - - const w = 64; - const h = 256; - const canvas = document.createElement('canvas'); - canvas.width = w; - canvas.height = h; - const ctx = canvas.getContext('2d'); - if (!ctx) { - const t = pixi.Texture.WHITE; - rainTextureCache = { key, texture: t }; - return t; - } - - ctx.clearRect(0, 0, w, h); - - // Несколько “полос” разной толщины и длины — одна текстура на спрайт. - ctx.lineCap = 'round'; - for (let i = 0; i < 42; i += 1) { - const x = w * (0.1 + 0.8 * Math.random()); - const y0 = h * (0.05 + 0.9 * Math.random()); - const len = h * (0.12 + 0.32 * Math.random()); - // Прозрачность каждой линии — случайно 0.5..1 (множитель), чтобы дождь был разнообразнее. - const a = (0.22 + 0.45 * Math.random()) * (0.5 + 0.5 * Math.random()); - const lw = 1.6 + Math.random() * 2.4; - const grad = ctx.createLinearGradient(x, y0, x, y0 + len); - // Цвет задаём tint’ом на спрайте (пер-капля), поэтому текстура — нейтрально-белая. - grad.addColorStop(0, `rgba(255,255,255,${String(a * 0.0)})`); - grad.addColorStop(0.2, `rgba(255,255,255,${String(a)})`); - grad.addColorStop(1, `rgba(255,255,255,${String(a * 0.0)})`); - ctx.strokeStyle = grad; - ctx.lineWidth = lw; - ctx.beginPath(); - ctx.moveTo(x, y0); - ctx.lineTo(x, y0 + len); - ctx.stroke(); - } - - // Чуть зерна по альфе — чтобы дождь не был слишком “компьютерным”. - const img = ctx.getImageData(0, 0, w, h); - for (let i = 0; i < img.data.length; i += 4) { - const a = img.data[i + 3] ?? 0; - if (a === 0) continue; - img.data[i + 3] = clamp255(a + (Math.random() - 0.5) * 65); - } - ctx.putImageData(img, 0, 0); - - const tex = pixi.Texture.from(canvas); - rainTextureCache = { key, texture: tex }; - return tex; -} - function getIceTexture(pixi: any): any { const key = 'ice_v3'; if (iceTextureCache?.key === key) return iceTextureCache.texture; diff --git a/app/shared/effectEraserHitTest.test.ts b/app/shared/effectEraserHitTest.test.ts index 75605d9..498a12f 100644 --- a/app/shared/effectEraserHitTest.test.ts +++ b/app/shared/effectEraserHitTest.test.ts @@ -6,7 +6,7 @@ import type { EffectInstance } from './types/effects'; const base = { seed: 1, createdAtMs: 0 }; -void test('pickEraseTargetId: fire/rain по штриху как туман', () => { +void test('pickEraseTargetId: fire/rain/water — кистью, не целиком', () => { const fire: EffectInstance = { ...base, id: 'f1', @@ -17,10 +17,10 @@ void test('pickEraseTargetId: fire/rain по штриху как туман', () lifetimeMs: null, }; const id = pickEraseTargetId([fire], { x: 0.51, y: 0.5 }, 0.05); - assert.equal(id, 'f1'); + assert.equal(id, null); }); -void test('pickEraseTargetId: вода по штриху как туман', () => { +void test('pickEraseTargetId: вода — кистью, не целиком', () => { const water: EffectInstance = { ...base, id: 'w1', @@ -31,7 +31,7 @@ void test('pickEraseTargetId: вода по штриху как туман', () lifetimeMs: null, }; const id = pickEraseTargetId([water], { x: 0.41, y: 0.55 }, 0.05); - assert.equal(id, 'w1'); + assert.equal(id, null); }); void test('pickEraseTargetId: тьма как заморозка — точка удара', () => { @@ -108,3 +108,17 @@ void test('pickEraseTargetId: scorch с учётом inst.radiusN', () => { const id = pickEraseTargetId([sc], { x: 0.59, y: 0.5 }, 0.02); assert.equal(id, 's1'); }); + +void test('pickEraseTargetId: яд с учётом inst.radiusN', () => { + const pc: EffectInstance = { + ...base, + id: 'pc', + type: 'poisonCloud', + at: { x: 0.5, y: 0.5 }, + radiusN: 0.08, + intensity: 1, + lifetimeMs: 1000, + }; + const id = pickEraseTargetId([pc], { x: 0.59, y: 0.5 }, 0.02); + assert.equal(id, 'pc'); +}); diff --git a/app/shared/effectEraserHitTest.ts b/app/shared/effectEraserHitTest.ts index 3c0d2db..c9446e4 100644 --- a/app/shared/effectEraserHitTest.ts +++ b/app/shared/effectEraserHitTest.ts @@ -63,14 +63,19 @@ export function minDistSqEffectToPoint(inst: EffectInstance, p: { x: number; y: } function eraseHitThresholdSq(inst: EffectInstance, toolRadiusN: number): number { - if (inst.type === 'scorch' || inst.type === 'ice' || inst.type === 'shadow' || inst.type === 'poisonCloud') { + if ( + inst.type === 'scorch' || + inst.type === 'ice' || + inst.type === 'shadow' || + inst.type === 'poisonCloud' + ) { const r = toolRadiusN + inst.radiusN; return r * r; } return toolRadiusN * toolRadiusN; } -/** Ближайший эффект в пределах радиуса ластика, иначе `null`. */ +/** Ближайший эффект действия в пределах радиуса ластика, иначе `null`. Эффекты поля стираются кистью отдельно. */ export function pickEraseTargetId( instances: readonly EffectInstance[], p: { x: number; y: number }, @@ -78,6 +83,9 @@ export function pickEraseTargetId( ): string | null { let best: { id: string; dd: number } | null = null; for (const inst of instances) { + if (inst.type === 'fog' || inst.type === 'fire' || inst.type === 'rain' || inst.type === 'water') { + continue; + } const dd = minDistSqEffectToPoint(inst, p); const th = eraseHitThresholdSq(inst, toolRadiusN); if (dd <= th && (!best || dd < best.dd)) { diff --git a/app/shared/fieldEffectEraser.test.ts b/app/shared/fieldEffectEraser.test.ts new file mode 100644 index 0000000..6beb36c --- /dev/null +++ b/app/shared/fieldEffectEraser.test.ts @@ -0,0 +1,125 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { applyFieldEffectEraserStroke } from './fieldEffectEraser'; +import type { EffectInstance } from './types/effects'; + +const base = { seed: 1, createdAtMs: 0 }; +const makeId = (prefix: string) => `${prefix}_new`; + +void test('applyFieldEffectEraserStroke: удаляет точки в радиусе кисти', () => { + const fog: EffectInstance = { + ...base, + id: 'f1', + type: 'fog', + points: [ + { x: 0.2, y: 0.5, tMs: 0 }, + { x: 0.5, y: 0.5, tMs: 100 }, + { x: 0.8, y: 0.5, tMs: 200 }, + ], + radiusN: 0.02, + opacity: 0.5, + lifetimeMs: null, + }; + const next = applyFieldEffectEraserStroke([fog], [{ x: 0.5, y: 0.5, tMs: 0 }], 0.05, makeId); + assert.equal(next.length, 2); + const left = next[0]; + const right = next[1]; + assert.ok(left); + assert.ok(right); + assert.equal(left.type, 'fog'); + assert.equal(right.type, 'fog'); + assert.equal(left.points.length, 1); + assert.equal(left.points[0]?.x, 0.2); + assert.equal(right.points.length, 1); + assert.equal(right.points[0]?.x, 0.8); +}); + +void test('applyFieldEffectEraserStroke: стирает вдоль сегмента штриха', () => { + const rain: EffectInstance = { + ...base, + id: 'r1', + type: 'rain', + points: [ + { x: 0.1, y: 0.5, tMs: 0 }, + { x: 0.3, y: 0.5, tMs: 50 }, + { x: 0.5, y: 0.5, tMs: 100 }, + { x: 0.7, y: 0.5, tMs: 150 }, + { x: 0.9, y: 0.5, tMs: 200 }, + ], + radiusN: 0.01, + opacity: 0.6, + lifetimeMs: null, + }; + const next = applyFieldEffectEraserStroke( + [rain], + [ + { x: 0.25, y: 0.5, tMs: 0 }, + { x: 0.75, y: 0.5, tMs: 50 }, + ], + 0.04, + makeId, + ); + assert.equal(next.length, 2); + const xs = next.flatMap((i) => (i.type === 'rain' ? i.points.map((p) => p.x) : [])); + assert.deepEqual(xs, [0.1, 0.9]); +}); + +void test('applyFieldEffectEraserStroke: VFX-огонь стирается как полевой эффект', () => { + const fire: EffectInstance = { + ...base, + id: 'fire_vfx', + type: 'fire', + points: [ + { x: 0.2, y: 0.5, tMs: 0 }, + { x: 0.5, y: 0.5, tMs: 100 }, + ], + radiusN: 0.02, + opacity: 0.8, + lifetimeMs: null, + }; + const next = applyFieldEffectEraserStroke([fire], [{ x: 0.5, y: 0.5, tMs: 0 }], 0.05, makeId); + assert.equal(next.length, 1); + const kept = next[0]; + assert.equal(kept?.type === 'fire' && kept.points[0]?.x, 0.2); +}); + +void test('applyFieldEffectEraserStroke: не трогает эффекты действия', () => { + const fog: EffectInstance = { + ...base, + id: 'f1', + type: 'fog', + points: [{ x: 0.5, y: 0.5, tMs: 0 }], + radiusN: 0.05, + opacity: 1, + lifetimeMs: null, + }; + const bolt: EffectInstance = { + ...base, + id: 'L1', + type: 'lightning', + start: { x: 0.5, y: 0 }, + end: { x: 0.5, y: 0.8 }, + widthN: 0.02, + intensity: 1, + lifetimeMs: 500, + }; + const next = applyFieldEffectEraserStroke([fog, bolt], [{ x: 0.5, y: 0.5, tMs: 0 }], 0.1, makeId); + assert.equal(next.length, 1); + assert.equal(next[0]?.id, 'L1'); +}); + +void test('applyFieldEffectEraserStroke: пустой штрих — без изменений', () => { + const water: EffectInstance = { + ...base, + id: 'w1', + type: 'water', + points: [{ x: 0.4, y: 0.4, tMs: 0 }], + radiusN: 0.05, + opacity: 0.5, + lifetimeMs: null, + }; + const next = applyFieldEffectEraserStroke([water], [], 0.05, makeId); + assert.equal(next.length, 1); + assert.equal(next[0]?.id, 'w1'); +}); diff --git a/app/shared/fieldEffectEraser.ts b/app/shared/fieldEffectEraser.ts new file mode 100644 index 0000000..47bcb34 --- /dev/null +++ b/app/shared/fieldEffectEraser.ts @@ -0,0 +1,119 @@ +import type { EffectInstance, NPoint } from './types/effects'; + +type FieldEffectInstance = Extract; + +function distSqPointToSegment( + px: number, + py: number, + x1: number, + y1: number, + x2: number, + y2: number, +): number { + const dx = x2 - x1; + const dy = y2 - y1; + const len2 = dx * dx + dy * dy; + if (len2 < 1e-18) { + const ex = px - x1; + const ey = py - y1; + return ex * ex + ey * ey; + } + let t = ((px - x1) * dx + (py - y1) * dy) / len2; + t = Math.max(0, Math.min(1, t)); + const qx = x1 + t * dx; + const qy = y1 + t * dy; + const ex = px - qx; + const ey = py - qy; + return ex * ex + ey * ey; +} + +function minDistToEraserStroke( + p: { x: number; y: number }, + eraserPoints: readonly { x: number; y: number }[], +): number { + if (eraserPoints.length === 0) return Number.POSITIVE_INFINITY; + let bestSq = Number.POSITIVE_INFINITY; + for (const ep of eraserPoints) { + const dx = p.x - ep.x; + const dy = p.y - ep.y; + bestSq = Math.min(bestSq, dx * dx + dy * dy); + } + for (let i = 1; i < eraserPoints.length; i += 1) { + const a = eraserPoints[i - 1]; + const b = eraserPoints[i]; + if (!a || !b) continue; + bestSq = Math.min(bestSq, distSqPointToSegment(p.x, p.y, a.x, a.y, b.x, b.y)); + } + return Math.sqrt(bestSq); +} + +function isPointErased( + p: { x: number; y: number }, + instRadiusN: number, + eraserPoints: readonly { x: number; y: number }[], + eraserRadiusN: number, +): boolean { + const threshold = eraserRadiusN + instRadiusN; + return minDistToEraserStroke(p, eraserPoints) <= threshold; +} + +function splitKeptPointRuns( + points: NPoint[], + eraserPoints: readonly { x: number; y: number }[], + eraserRadiusN: number, + instRadiusN: number, +): NPoint[][] { + const runs: NPoint[][] = []; + let current: NPoint[] = []; + for (const p of points) { + if (isPointErased(p, instRadiusN, eraserPoints, eraserRadiusN)) { + if (current.length > 0) { + runs.push(current); + current = []; + } + continue; + } + current.push(p); + } + if (current.length > 0) runs.push(current); + return runs; +} + +function isFieldEffect(inst: EffectInstance): inst is FieldEffectInstance { + return inst.type === 'fog' || inst.type === 'fire' || inst.type === 'rain' || inst.type === 'water'; +} + +/** Стирает эффекты поля (туман/дождь/огонь/вода) кистью, как «Кисть Открытия» для затемнения. */ +export function applyFieldEffectEraserStroke( + instances: readonly EffectInstance[], + eraserPoints: readonly NPoint[], + eraserRadiusN: number, + makeId: (prefix: string) => string, +): EffectInstance[] { + if (eraserPoints.length === 0) return [...instances]; + + const out: EffectInstance[] = []; + for (const inst of instances) { + if (!isFieldEffect(inst)) { + out.push(inst); + continue; + } + + const runs = splitKeptPointRuns(inst.points, eraserPoints, eraserRadiusN, inst.radiusN); + if (runs.length === 0) continue; + if (runs.length === 1 && runs[0]?.length === inst.points.length) { + out.push(inst); + continue; + } + + for (const run of runs) { + if (run.length === 0) continue; + out.push({ + ...inst, + id: makeId(inst.type), + points: run, + }); + } + } + return out; +} diff --git a/app/shared/types/effects.ts b/app/shared/types/effects.ts index 1275b7d..ad0f7d9 100644 --- a/app/shared/types/effects.ts +++ b/app/shared/types/effects.ts @@ -171,4 +171,5 @@ export type EffectsEvent = | { kind: 'tool.set'; tool: EffectToolState } | { kind: 'instances.clear' } | { kind: 'instance.add'; instance: EffectInstance } - | { kind: 'instance.remove'; id: string }; + | { kind: 'instance.remove'; id: string } + | { kind: 'field.erase'; points: NPoint[]; radiusN: number }; diff --git a/package.json b/package.json index 4076582..e4ebd15 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "build:obfuscate": "node scripts/build.mjs --production --obfuscate", "lint": "eslint . --max-warnings 0", "typecheck": "tsc -p tsconfig.eslint.json --noEmit", - "test": "tsx --test app/renderer/shared/ui/controls.tooltip.test.ts app/renderer/editor/state/projectState.race.test.ts app/renderer/editor/graph/sceneCardById.test.ts app/renderer/editor/i18n/editorMessages.locale.test.ts app/shared/ipc/contracts.mediaRemoval.test.ts app/shared/effectEraserHitTest.test.ts app/renderer/control/controlApp.effectsPanel.test.ts app/renderer/shared/effects/PxiEffectsOverlay.pointer.test.ts app/main/windows/createWindows.editorClose.test.ts app/main/windows/bootWindow.test.ts app/main/effects/effectsStore.test.ts app/main/effects/sceneDarknessStore.test.ts app/main/project/assetPrune.test.ts app/main/project/optimizeImageImport.test.ts app/main/project/scenePreviewThumbnail.test.ts app/main/project/fsRetry.test.ts app/main/project/zipRead.test.ts app/main/project/replaceFileAtomic.test.ts app/main/project/zipStore.legacyContract.test.ts app/shared/package.build.test.ts app/shared/license/canonicalJson.test.ts app/shared/license/productKey.test.ts app/shared/license/licenseService.networkRegression.test.ts app/shared/video/videoPlaybackPerf.networkRegression.test.ts app/shared/video/videoPlaybackLoop.networkRegression.test.ts app/main/license/verifyLicenseToken.test.ts && node --test scripts/build-env.test.mjs scripts/obfuscate-main.test.mjs scripts/release-mac-prep.test.mjs", + "test": "tsx --test app/renderer/shared/ui/controls.tooltip.test.ts app/renderer/editor/state/projectState.race.test.ts app/renderer/editor/graph/sceneCardById.test.ts app/renderer/editor/i18n/editorMessages.locale.test.ts app/shared/ipc/contracts.mediaRemoval.test.ts app/shared/effectEraserHitTest.test.ts app/shared/fieldEffectEraser.test.ts app/renderer/control/controlApp.effectsPanel.test.ts app/renderer/shared/effects/PxiEffectsOverlay.pointer.test.ts app/main/windows/createWindows.editorClose.test.ts app/main/windows/bootWindow.test.ts app/main/effects/effectsStore.test.ts app/main/effects/sceneDarknessStore.test.ts app/main/project/assetPrune.test.ts app/main/project/optimizeImageImport.test.ts app/main/project/scenePreviewThumbnail.test.ts app/main/project/fsRetry.test.ts app/main/project/zipRead.test.ts app/main/project/replaceFileAtomic.test.ts app/main/project/zipStore.legacyContract.test.ts app/shared/package.build.test.ts app/shared/license/canonicalJson.test.ts app/shared/license/productKey.test.ts app/shared/license/licenseService.networkRegression.test.ts app/shared/video/videoPlaybackPerf.networkRegression.test.ts app/shared/video/videoPlaybackLoop.networkRegression.test.ts app/main/license/verifyLicenseToken.test.ts && node --test scripts/build-env.test.mjs scripts/obfuscate-main.test.mjs scripts/release-mac-prep.test.mjs", "format": "prettier . --check", "format:write": "prettier . --write", "postinstall": "patch-package",