/** * 战场防御线:反弓曲线路径 + 锯齿齿形 */ export type LngLat = [number, number] /** 二次贝塞尔 B(t) = (1-t)²P0 + 2(1-t)t P1 + t² P2 */ function quadraticBezier(P0: LngLat, P1: LngLat, P2: LngLat, t: number): LngLat { const u = 1 - t return [ u * u * P0[0] + 2 * u * t * P1[0] + t * t * P2[0], u * u * P0[1] + 2 * u * t * P1[1] + t * t * P2[1], ] } /** * 生成反弓曲线路径:通过 path 各点,弧面向进攻方向(西侧)凸出 * @param path 关键点 [[lng, lat], ...](如大不里士→萨南达季→克尔曼沙赫) * @param bulgeWest 向西凸出量(经度,正值表示弧顶在西侧/面向进攻方) * @param samplesPerSegment 每段贝塞尔采样点数 */ export function createCurvedDefensePath( path: LngLat[], bulgeWest: number = 0.35, samplesPerSegment: number = 24 ): LngLat[] { if (path.length < 2) return path const out: LngLat[] = [] for (let i = 0; i < path.length - 1; i++) { const a = path[i] const b = path[i + 1] const midLng = (a[0] + b[0]) / 2 const midLat = (a[1] + b[1]) / 2 const control: LngLat = [midLng - bulgeWest, midLat] const startK = i === 0 ? 0 : 1 for (let k = startK; k <= samplesPerSegment; k++) { out.push(quadraticBezier(a, control, b, k / samplesPerSegment)) } } return out } /** * 沿路径线性插值增加点(直线段) */ export function interpolatePath(path: LngLat[], stepsPerSegment: number = 4): LngLat[] { if (path.length < 2) return path const out: LngLat[] = [] for (let i = 0; i < path.length - 1; i++) { const a = path[i] const b = path[i + 1] const startK = i === 0 ? 0 : 1 for (let k = startK; k <= stepsPerSegment; k++) { const t = k / stepsPerSegment out.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]) } } return out } /** * 锯齿防御线:沿 path 每 toothWidth(比例)生成齿,齿高 toothHeight,朝向法向(敌方侧) * @param path 路径点 [[lng, lat], ...](可先经 interpolatePath 插值) * @param toothWidth 齿宽(与段长同单位,经纬度) * @param toothHeight 齿高(法向伸出) */ export function createDefenseLine( path: number[][], toothWidth: number, toothHeight: number ): number[][] { const coords: number[][] = [] for (let i = 0; i < path.length - 1; i++) { const start = path[i] as [number, number] const end = path[i + 1] as [number, number] const dx = end[0] - start[0] const dy = end[1] - start[1] const L = Math.sqrt(dx * dx + dy * dy) if (L < 1e-9) continue const ux = dx / L const uy = dy / L const nx = -uy const ny = ux const numTeeth = Math.max(1, Math.floor(L / toothWidth)) const actualWidth = L / numTeeth for (let j = 0; j < numTeeth; j++) { const t1 = j * actualWidth const t2 = t1 + actualWidth * 0.5 const t3 = (j + 1) * actualWidth coords.push([start[0] + ux * t1, start[1] + uy * t1]) coords.push([ start[0] + ux * t2 + nx * toothHeight, start[1] + uy * t2 + ny * toothHeight, ]) coords.push([start[0] + ux * t3, start[1] + uy * t3]) } } coords.push(path[path.length - 1] as [number, number]) return coords }