Files
MatchMaster/assets/Script/LaserRules.ts
T
2026-10-20 11:24:09 +08:00

126 lines
5.2 KiB
TypeScript

export type LaserPoint = { x: number; y: number };
export type LaserCollider =
| { type: 'rect'; minX: number; minY: number; maxX: number; maxY: number }
| { type: 'circle'; center: LaserPoint; radius: number }
| { type: 'polygon'; points: LaserPoint[] };
export type LaserHit = {
ratio: number;
distance: number;
point: LaserPoint;
colliderIndex: number;
};
const EPSILON = 1e-7;
function cross(ax: number, ay: number, bx: number, by: number): number {
return ax * by - ay * bx;
}
function pointOnEdge(point: LaserPoint, a: LaserPoint, b: LaserPoint): boolean {
const dx = b.x - a.x, dy = b.y - a.y;
const px = point.x - a.x, py = point.y - a.y;
const length = Math.sqrt(dx * dx + dy * dy);
if (length < EPSILON) return px * px + py * py <= EPSILON * EPSILON;
if (Math.abs(cross(dx, dy, px, py)) > EPSILON * length) return false;
const projection = (px * dx + py * dy) / length;
return projection >= -EPSILON && projection <= length + EPSILON;
}
function pointInPolygon(point: LaserPoint, points: LaserPoint[]): boolean {
let inside = false;
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
const a = points[j], b = points[i];
if (pointOnEdge(point, a, b)) return true;
if ((a.y > point.y) !== (b.y > point.y)
&& point.x < (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x) inside = !inside;
}
return inside;
}
function polygonHit(start: LaserPoint, end: LaserPoint, points: LaserPoint[]): number | null {
if (points.length < 3) return null;
if (pointInPolygon(start, points)) return 0;
const dx = end.x - start.x, dy = end.y - start.y;
const lengthSquared = dx * dx + dy * dy;
if (lengthSquared < EPSILON * EPSILON) return null;
let nearest = Infinity;
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
const a = points[j], b = points[i];
const ex = b.x - a.x, ey = b.y - a.y;
const px = a.x - start.x, py = a.y - start.y;
const determinant = cross(dx, dy, ex, ey);
if (Math.abs(determinant) <= EPSILON) {
// A beam running along a polygon edge still touches the solid.
if (Math.abs(cross(px, py, dx, dy)) > EPSILON) continue;
const t1 = (px * dx + py * dy) / lengthSquared;
const t2 = ((b.x - start.x) * dx + (b.y - start.y) * dy) / lengthSquared;
const entry = Math.max(0, Math.min(t1, t2));
if (entry <= Math.min(1, Math.max(t1, t2))) nearest = Math.min(nearest, entry);
} else {
const ratio = cross(px, py, ex, ey) / determinant;
const edgeRatio = cross(px, py, dx, dy) / determinant;
if (ratio >= -EPSILON && ratio <= 1 + EPSILON
&& edgeRatio >= -EPSILON && edgeRatio <= 1 + EPSILON) {
nearest = Math.min(nearest, Math.max(0, Math.min(1, ratio)));
}
}
}
return nearest === Infinity ? null : nearest;
}
function colliderHit(start: LaserPoint, end: LaserPoint, collider: LaserCollider): number | null {
const dx = end.x - start.x, dy = end.y - start.y;
if (collider.type === 'circle') {
const x = start.x - collider.center.x, y = start.y - collider.center.y;
const c = x * x + y * y - collider.radius * collider.radius;
if (c <= 0) return 0;
const a = dx * dx + dy * dy;
if (a < EPSILON * EPSILON) return null;
const b = x * dx + y * dy;
const discriminant = b * b - a * c;
if (discriminant < -EPSILON) return null;
const ratio = (-b - Math.sqrt(Math.max(0, discriminant))) / a;
return ratio >= -EPSILON && ratio <= 1 + EPSILON ? Math.max(0, Math.min(1, ratio)) : null;
}
if (collider.type === 'polygon') return polygonHit(start, end, collider.points);
let entry = 0, exit = 1;
for (const axis of ['x', 'y']) {
const origin = axis === 'x' ? start.x : start.y;
const delta = axis === 'x' ? dx : dy;
const min = axis === 'x' ? collider.minX : collider.minY;
const max = axis === 'x' ? collider.maxX : collider.maxY;
if (Math.abs(delta) < EPSILON) {
if (origin < min - EPSILON || origin > max + EPSILON) return null;
continue;
}
const t1 = (min - origin) / delta, t2 = (max - origin) / delta;
entry = Math.max(entry, Math.min(t1, t2));
exit = Math.min(exit, Math.max(t1, t2));
if (entry > exit) return null;
}
return entry;
}
/** World-space beam centerline only: visual beam thickness never affects blocking. */
export function nearestLaserHit(start: LaserPoint, end: LaserPoint, colliders: LaserCollider[]): LaserHit | null {
let ratio = Infinity, colliderIndex = -1;
for (let i = 0; i < colliders.length; i++) {
const hit = colliderHit(start, end, colliders[i]);
if (hit !== null && hit < ratio) {
ratio = hit;
colliderIndex = i;
}
}
if (colliderIndex < 0) return null;
const dx = end.x - start.x, dy = end.y - start.y;
return {
ratio,
distance: Math.sqrt(dx * dx + dy * dy) * ratio,
point: { x: start.x + dx * ratio, y: start.y + dy * ratio },
colliderIndex,
};
}