2c0e6fbf09
Water and rain erase fire under a tight brush hit radius; add looping fire/rain ambients and an Effects sound slider that also scales one-shot effect SFX. Co-authored-by: Cursor <cursoragent@cursor.com>
192 lines
5.0 KiB
TypeScript
192 lines
5.0 KiB
TypeScript
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');
|
|
});
|
|
|
|
void test('applyFieldEffectEraserStroke: types=["fire"] гасит только огонь', () => {
|
|
const fire: EffectInstance = {
|
|
...base,
|
|
id: 'fire1',
|
|
type: 'fire',
|
|
points: [
|
|
{ x: 0.5, y: 0.5, tMs: 0 },
|
|
{ x: 0.55, y: 0.5, tMs: 50 },
|
|
],
|
|
radiusN: 0.03,
|
|
opacity: 1,
|
|
lifetimeMs: null,
|
|
};
|
|
const fog: EffectInstance = {
|
|
...base,
|
|
id: 'fog1',
|
|
type: 'fog',
|
|
points: [{ x: 0.5, y: 0.5, tMs: 0 }],
|
|
radiusN: 0.03,
|
|
opacity: 0.5,
|
|
lifetimeMs: null,
|
|
};
|
|
const next = applyFieldEffectEraserStroke(
|
|
[fire, fog],
|
|
[{ x: 0.5, y: 0.5, tMs: 0 }],
|
|
0.08,
|
|
makeId,
|
|
['fire'],
|
|
);
|
|
assert.equal(next.length, 1);
|
|
assert.equal(next[0]?.id, 'fog1');
|
|
});
|
|
|
|
void test('applyFieldEffectEraserStroke: hitMode=brush не гасит издалека', () => {
|
|
const fire: EffectInstance = {
|
|
...base,
|
|
id: 'fire1',
|
|
type: 'fire',
|
|
points: [{ x: 0.5, y: 0.5, tMs: 0 }],
|
|
radiusN: 0.08,
|
|
opacity: 1,
|
|
lifetimeMs: null,
|
|
};
|
|
// brush ≈ 0.38 * 0.08 ≈ 0.030; dist 0.06 — мимо, dist 0.02 — попадание.
|
|
const far = applyFieldEffectEraserStroke(
|
|
[fire],
|
|
[{ x: 0.5, y: 0.56, tMs: 0 }],
|
|
0.08,
|
|
makeId,
|
|
['fire'],
|
|
'brush',
|
|
);
|
|
assert.equal(far.length, 1);
|
|
assert.equal(far[0]?.id, 'fire1');
|
|
|
|
const near = applyFieldEffectEraserStroke(
|
|
[fire],
|
|
[{ x: 0.5, y: 0.52, tMs: 0 }],
|
|
0.08,
|
|
makeId,
|
|
['fire'],
|
|
'brush',
|
|
);
|
|
assert.equal(near.length, 0);
|
|
});
|