Files
DndGamePlayer/app/shared/effectEraserHitTest.ts
T
Ivan Fontosh fed5674468 feat(traps): activation VFX/SFX, explosion effect, and help section
Wire mimic/pit/arrow/laser media on activate, poison/explosion via effects, and document the scene editor and traps in Instructions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-25 12:35:22 +08:00

99 lines
2.8 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':
case 'explosion': {
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' ||
inst.type === 'explosion'
) {
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;
}