089da3cae0
- add WebP frame-sequence assets for lightning, electric accent, fire, sunbeam, poison, rain, water, and rounded fog effects - replace procedural lightning rendering with VFX frame playback - align lightning impact to the click point - remove the full-screen lightning blur/flash overlay - add electric accent playback for lightning impact dispersion - keep lightning scorch marks as the persistent ground trace after the action effect - consolidate the lightning tool into a single public `Молния` action - replace procedural fire with the VFX fire implementation - remove the separate `Огонь 2` tool and keep the VFX fire under `Огонь` - keep VFX fire brightness fixed at full intensity - preserve fire as a field effect with brush placement and eraser support - replace procedural sunbeam with the Pulse Discharge VFX implementation - remove the separate `Луч света 2` tool and keep the VFX sunbeam under `Луч света` - align sunbeam impact to the click point instead of the raw frame center - remove the full-screen sunbeam flash overlay - add Dust Burst VFX playback for poison cloud - tint poison smoke green - keep the skull glyph rendered above the poison smoke - align poison smoke bottom to the click point - remove the old procedural poison cloud implementation - remove the separate `Яд 2` tool and keep the VFX poison under `Яд` - replace procedural rain strokes with medium-rain VFX frame playback - recolor rain frames to gray/light-gray so rain no longer renders black - preserve rain as a field effect with brush placement and eraser support - replace static water fill with generated animated water VFX frames - render water VFX through the existing stroke mask so water keeps the painted brush shape - preserve water as a field effect with brush placement and eraser support - replace procedural fog texture generation with Smokey Atmosphere VFX frames - generate rounded fog VFX frames with soft alpha falloff at the edges - size each fog VFX stamp to the brush diameter - remove per-stamp fog drift and rotation so fog elements stay fixed in place - keep fog animation limited to internal frame playback - preload VFX frame textures for smoother first use - cache VFX textures per Pixi instance to avoid duplicate decoding - filter expired action instances before node synchronization to stop old effects from flashing back during rapid casts - add brush-stroke erasing for field effects using the same model as scene darkness reveal - keep whole-instance erasing for action effects - update effect hit tests for the consolidated VFX action effects - update effect store pruning for renamed and consolidated action effects - update control-panel buttons for consolidated fire, sunbeam, poison, and VFX-backed field effects - update editor localization for removed and renamed effect tools - add tests for field-effect brush erasing - update eraser hit-test coverage for VFX-backed effects - update effects store lifetime pruning tests for consolidated poison - update control-panel tests for consolidated effect buttons - include the field-effect eraser test in the default test script Co-authored-by: Cursor <cursoragent@cursor.com>
166 lines
7.5 KiB
TypeScript
166 lines
7.5 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
function readControlApp(): string {
|
|
return fs.readFileSync(path.join(here, 'ControlApp.tsx'), 'utf8');
|
|
}
|
|
|
|
function readControlAppCss(): string {
|
|
return fs.readFileSync(path.join(here, 'ControlApp.module.css'), 'utf8');
|
|
}
|
|
|
|
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)', () => {
|
|
const src = readControlApp();
|
|
assert.ok(src.includes('zamorozka.mp3'));
|
|
assert.ok(src.includes('playFreezeEffectSound'));
|
|
});
|
|
|
|
void test('ControlApp: звук луча света (public/luch_sveta.mp3)', () => {
|
|
const appSrc = readControlApp();
|
|
const sfxSrc = fs.readFileSync(path.join(here, 'sunbeamSfx.ts'), 'utf8');
|
|
assert.ok(appSrc.includes('playSunbeamEffectSound'));
|
|
assert.ok(appSrc.includes('getSunbeamEffectLifeMs'));
|
|
assert.ok(sfxSrc.includes('luch_sveta.mp3'));
|
|
assert.ok(sfxSrc.includes('playbackRate'));
|
|
assert.ok(sfxSrc.includes('SUNBEAM_PLAYBACK_RATE'));
|
|
});
|
|
|
|
void test('ControlApp: звук облака яда (public/oblako-yada.mp3)', () => {
|
|
const appSrc = readControlApp();
|
|
const sfxSrc = fs.readFileSync(path.join(here, 'poisonCloudSfx.ts'), 'utf8');
|
|
assert.ok(appSrc.includes('getPoisonCloudEffectLifeMs'));
|
|
assert.ok(appSrc.includes('playPoisonCloudEffectSound'));
|
|
assert.ok(sfxSrc.includes('oblako-yada.mp3'));
|
|
assert.ok(sfxSrc.includes('playbackRate'));
|
|
});
|
|
|
|
void test('ControlApp: эффекты в пульте, иконки с тултипами и подписью для a11y', () => {
|
|
const src = readControlApp();
|
|
assert.ok(src.includes("t('control.effects')"));
|
|
assert.ok(src.includes("t('control.tools')"));
|
|
assert.ok(src.includes("t('control.fieldEffects')"));
|
|
assert.ok(src.includes("t('control.actionEffects')"));
|
|
assert.ok(src.includes("t('control.darknessControl')"));
|
|
assert.ok(src.includes("t('control.explorerBrush')"));
|
|
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')}"));
|
|
assert.ok(src.includes("ariaLabel={t('control.fog')}"));
|
|
assert.ok(src.includes('iconOnly'));
|
|
assert.ok(src.includes("title={t('control.clearEffects')}"));
|
|
assert.ok(src.includes("ariaLabel={t('control.clearEffects')}"));
|
|
assert.ok(src.includes('#e5484d'));
|
|
const fx = src.indexOf("t('control.effects')");
|
|
const story = src.indexOf("t('control.storyLine')");
|
|
assert.ok(fx !== -1 && story !== -1 && fx < story, 'Блок эффектов должен быть выше сюжетной линии');
|
|
});
|
|
|
|
void test('ControlApp: сюжетная линия — колонка сверху вниз и фон как у карточек ветвления', () => {
|
|
const src = readControlApp();
|
|
const css = readControlAppCss();
|
|
const story = src.indexOf("t('control.storyLine')");
|
|
assert.ok(story !== -1);
|
|
assert.ok(src.includes('className={styles.storyScroll}'));
|
|
assert.match(css, /\.storyScroll[\s\S]*?justify-content:\s*flex-start/);
|
|
assert.match(css, /\.storyScroll[\s\S]*?background:\s*var\(--color-overlay-dark-2\)/);
|
|
assert.match(css, /\.branchCard[\s\S]*?background:\s*var\(--color-overlay-dark-2\)/);
|
|
});
|
|
|
|
void test('ControlApp: клик по истории добавляет новый шаг, не подавляет запись', () => {
|
|
const src = readControlApp();
|
|
assert.ok(!src.includes('suppressNextHistoryPushRef'));
|
|
assert.ok(!src.includes('arr.includes(cur)'));
|
|
assert.ok(src.includes('historyRef.current = [...arr, cur]'));
|
|
});
|
|
|
|
void test('ControlApp: текущая сцена в истории — последнее вхождение', () => {
|
|
const src = readControlApp();
|
|
assert.ok(src.includes('history.lastIndexOf(currentGraphNodeId)'));
|
|
assert.ok(src.includes('idx === currentHistoryIdx'));
|
|
});
|
|
|
|
void test('ControlApp: сюжетная линия не меняет граф и сцены проекта', () => {
|
|
const src = readControlApp();
|
|
const story = src.indexOf("t('control.storyLine')");
|
|
assert.ok(story !== -1);
|
|
const tail = src.slice(story);
|
|
assert.ok(!tail.includes('addSceneGraphNode'));
|
|
assert.ok(!tail.includes('removeSceneGraphNode'));
|
|
assert.ok(!tail.includes('addSceneGraphEdge'));
|
|
assert.ok(!tail.includes('removeSceneGraphEdge'));
|
|
assert.ok(!tail.includes('updateProject'));
|
|
assert.ok(tail.includes('ipcChannels.project.setCurrentGraphNode'));
|
|
});
|
|
|
|
void test('ControlApp: слой кисти не использует курсор not-allowed (ластик тоже crosshair)', () => {
|
|
const src = readControlApp();
|
|
const css = readControlAppCss();
|
|
assert.ok(!src.includes("tool.tool === 'eraser' ? 'not-allowed'"));
|
|
assert.ok(src.includes('className={styles.brushLayer}'));
|
|
assert.match(css, /\.brushLayer[\s\S]*?cursor:\s*crosshair/);
|
|
});
|
|
|
|
void test('ControlApp: радиус кисти не в блоке предпросмотра', () => {
|
|
const src = readControlApp();
|
|
const previewLabel = src.indexOf("t('control.screenPreview')");
|
|
const radius = src.indexOf("t('control.brushRadius')");
|
|
assert.ok(previewLabel !== -1 && radius !== -1);
|
|
assert.ok(
|
|
radius < previewLabel,
|
|
'Слайдер радиуса должен быть в пульте (файл: выше заголовка предпросмотра)',
|
|
);
|
|
});
|
|
|
|
void test('ControlApp: музыка разделена на сцену и кампанию', () => {
|
|
const src = readControlApp();
|
|
assert.ok(src.includes("t('control.sceneMusic')"));
|
|
assert.ok(src.includes("t('control.gameMusic')"));
|
|
// при музыке сцены — кампанию ставим на паузу
|
|
assert.ok(src.includes('allowCampaignAudio'));
|
|
assert.ok(
|
|
src.includes('campaignAudioSpecKey'),
|
|
'кампания: перезагрузка аудио привязана к списку треков, не к смене сцены',
|
|
);
|
|
assert.match(src, /pause campaign\./i);
|
|
});
|
|
|
|
void test('ControlApp: загрузка камп. аудио — useEffect зависит только от api и campaignAudioSpecKey', () => {
|
|
const src = readControlApp();
|
|
const re = /\/\/ Campaign elements:[\s\S]*?useEffect\(\(\) => \{[\s\S]*?\}\s*,\s*\[([^\]]*)\]\s*\)\s*;/;
|
|
const m = re.exec(src);
|
|
assert.ok(m, 'ожидается useEffect загрузки кампании после комментария Campaign elements');
|
|
const depList = m[1];
|
|
assert.ok(depList !== undefined);
|
|
const deps = depList
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
assert.deepEqual(
|
|
deps,
|
|
['api', 'campaignAudioSpecKey'],
|
|
'смена сцены / allowCampaignAudio / campaignAudioRefs не должны перезапускать загрузку кампании',
|
|
);
|
|
assert.ok(!/\ballowCampaignAudio\b/.test(depList));
|
|
assert.ok(!/\bcurrentScene\b/.test(depList));
|
|
assert.ok(!/\bproject\b/.test(depList));
|
|
assert.ok(!/\bcampaignAudioRefs\b/.test(depList));
|
|
});
|