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>
97 lines
2.7 KiB
TypeScript
97 lines
2.7 KiB
TypeScript
import type { EffectInstance } from './types/effects';
|
|
|
|
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;
|
|
}
|
|
|
|
/** Минимальная квадрат дистанции от точки (норм. координаты) до «тела» эффекта — для ластика. */
|
|
export function minDistSqEffectToPoint(inst: EffectInstance, p: { x: number; y: number }): number {
|
|
switch (inst.type) {
|
|
case 'fog':
|
|
case 'fire':
|
|
case 'rain':
|
|
case 'water': {
|
|
let best = Number.POSITIVE_INFINITY;
|
|
for (const q of inst.points) {
|
|
const dx = q.x - p.x;
|
|
const dy = q.y - p.y;
|
|
best = Math.min(best, dx * dx + dy * dy);
|
|
}
|
|
return best;
|
|
}
|
|
case 'lightning':
|
|
case 'sunbeam':
|
|
return distSqPointToSegment(p.x, p.y, inst.start.x, inst.start.y, inst.end.x, inst.end.y);
|
|
case 'freeze':
|
|
case 'darkness': {
|
|
const dx = inst.at.x - p.x;
|
|
const dy = inst.at.y - p.y;
|
|
return dx * dx + dy * dy;
|
|
}
|
|
case 'scorch':
|
|
case 'ice':
|
|
case 'shadow':
|
|
case 'poisonCloud': {
|
|
const dx = inst.at.x - p.x;
|
|
const dy = inst.at.y - p.y;
|
|
return dx * dx + dy * dy;
|
|
}
|
|
default:
|
|
return Number.POSITIVE_INFINITY;
|
|
}
|
|
}
|
|
|
|
function eraseHitThresholdSq(inst: EffectInstance, toolRadiusN: number): number {
|
|
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`. Эффекты поля стираются кистью отдельно. */
|
|
export function pickEraseTargetId(
|
|
instances: readonly EffectInstance[],
|
|
p: { x: number; y: number },
|
|
toolRadiusN: number,
|
|
): 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)) {
|
|
best = { id: inst.id, dd };
|
|
}
|
|
}
|
|
return best?.id ?? null;
|
|
}
|