diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index 2ecf734ba..f76c1e86e 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -382,7 +382,7 @@ export class UnitLayer implements Layer { let newTrailSize = 1; const trail = this.unitToTrail.get(unit); - // The nuke can move faster than 1 pixel, draw a line for the trail or else it will be dotted + // It can move faster than 1 pixel, draw a line for the trail or else it will be dotted if (trail.length >= 1) { const currentX = this.game.x(unit.lastTile()); const currentY = this.game.y(unit.lastTile()); diff --git a/src/core/pathfinding/PathFinding.ts b/src/core/pathfinding/PathFinding.ts index 0933c457f..1f3e90585 100644 --- a/src/core/pathfinding/PathFinding.ts +++ b/src/core/pathfinding/PathFinding.ts @@ -2,35 +2,37 @@ import { consolex } from "../Consolex"; import { Game } from "../game/Game"; import { GameMap, TileRef } from "../game/GameMap"; import { PseudoRandom } from "../PseudoRandom"; -import { BezierCurve } from "../utilities/Line"; +import { DistanceBasedBezierCurve } from "../utilities/Line"; import { AStar, PathFindResultType, TileResult } from "./AStar"; import { MiniAStar } from "./MiniAStar"; +const parabolaMinHeight = 50; + export class ParabolaPathFinder { constructor(private mg: GameMap) {} - private curve: BezierCurve; - private distance: number; + private curve: DistanceBasedBezierCurve; computeControlPoints( orig: TileRef, dst: TileRef, - distanceBasedVertex = true, + distanceBasedHeight = true, ) { const origX = this.mg.x(orig); const origY = this.mg.y(orig); const dstX = this.mg.x(dst); const dstY = this.mg.y(dst); - - this.curve = new BezierCurve(origX, origY, dstX, dstY); + this.curve = new DistanceBasedBezierCurve(origX, origY, dstX, dstY); const dx = dstX - origX; const dy = dstY - origY; - this.distance = Math.sqrt(dx * dx + dy * dy); - + const distance = Math.sqrt(dx * dx + dy * dy); + const maxHeight = distanceBasedHeight + ? Math.max(distance / 3, parabolaMinHeight) + : 0; + // Use a bezier curve always pointing up const x0 = origX + (dstX - origX) / 4; - const maxVertex = distanceBasedVertex ? Math.max(this.distance / 3, 50) : 0; - const y0 = Math.max(origY + (dstY - origY) / 4 - maxVertex, 0); + const y0 = Math.max(origY + (dstY - origY) / 4 - maxHeight, 0); const x1 = origX + ((dstX - origX) * 3) / 4; - const y1 = Math.max(origY + ((dstY - origY) * 3) / 4 - maxVertex, 0); + const y1 = Math.max(origY + ((dstY - origY) * 3) / 4 - maxHeight, 0); this.curve.setControlPoint0(x0, y0); this.curve.setControlPoint1(x1, y1); @@ -40,8 +42,7 @@ export class ParabolaPathFinder { if (!this.curve) { return; } - const incr = speed / (this.distance * 2); - const nextPoint = this.curve.increment(incr); + const nextPoint = this.curve.increment(speed); if (!nextPoint) { return true; } diff --git a/src/core/utilities/Line.ts b/src/core/utilities/Line.ts index ef590f60d..bfe1ff7e2 100644 --- a/src/core/utilities/Line.ts +++ b/src/core/utilities/Line.ts @@ -54,12 +54,8 @@ export class BezierCurve { this.controlPoint0Y = y0; this.controlPoint1X = x1; this.controlPoint1Y = y1; - const dx = this.x1 - this.x0; - const dy = this.y1 - this.y0; - const dist = Math.abs(this.x1 - this.x0); } - private t: number = 0; private controlPoint0X: number; private controlPoint0Y: number; private controlPoint1X: number; @@ -75,23 +71,96 @@ export class BezierCurve { this.controlPoint1Y = y; } - increment(incr: number): { x: number; y: number } { - // Calculate the next point on the Bézier curve - // const incr = speed / (this.distance * 2); - this.t = this.t + incr; - if (this.t >= 1) { - return null; // end reached - } - const nextX = - Math.pow(1 - this.t, 3) * this.x0 + - 3 * Math.pow(1 - this.t, 2) * this.t * this.controlPoint0X + - 3 * (1 - this.t) * Math.pow(this.t, 2) * this.controlPoint1X + - Math.pow(this.t, 3) * this.x1; - const nextY = - Math.pow(1 - this.t, 3) * this.y0 + - 3 * Math.pow(1 - this.t, 2) * this.t * this.controlPoint0Y + - 3 * (1 - this.t) * Math.pow(this.t, 2) * this.controlPoint1Y + - Math.pow(this.t, 3) * this.y1; - return { x: nextX, y: nextY }; + getPointAt(t: number): { x: number; y: number } { + const x = + Math.pow(1 - t, 3) * this.x0 + + 3 * Math.pow(1 - t, 2) * t * this.controlPoint0X + + 3 * (1 - t) * Math.pow(t, 2) * this.controlPoint1X + + Math.pow(t, 3) * this.x1; + const y = + Math.pow(1 - t, 3) * this.y0 + + 3 * Math.pow(1 - t, 2) * t * this.controlPoint0Y + + 3 * (1 - t) * Math.pow(t, 2) * this.controlPoint1Y + + Math.pow(t, 3) * this.y1; + return { x, y }; + } +} + +/** + * Use a cumulative distance LUT to approximate the traveled distance + */ +export class DistanceBasedBezierCurve extends BezierCurve { + private totalDistance: number = 0; + private cumulativeDistanceLUT: Array<{ t: number; distance: number }> = []; + private lastFoundIndex: number = 0; // To keep track of the last found index + + increment(distance: number): { x: number; y: number } { + this.totalDistance += distance; + const targetDistance = Math.min( + this.totalDistance, + this.cumulativeDistanceLUT[this.cumulativeDistanceLUT.length - 1] + ?.distance || 0, + ); + const t = this.computeTForDistance(targetDistance); + if (t >= 1) { + return null; // end reached + } + return this.getPointAt(t); + } + + generateCumulativeDistanceLUT(numSteps: number = 500): void { + this.cumulativeDistanceLUT = []; + let cumulativeDistance = 0; + let prevPoint = this.getPointAt(0); + + for (let i = 1; i <= numSteps; i++) { + const t = i / numSteps; + const currentPoint = this.getPointAt(t); + + const dx = currentPoint.x - prevPoint.x; + const dy = currentPoint.y - prevPoint.y; + const segmentLength = Math.sqrt(dx * dx + dy * dy); + + cumulativeDistance += segmentLength; + this.cumulativeDistanceLUT.push({ t, distance: cumulativeDistance }); + prevPoint = currentPoint; + } + } + + computeTForDistance(distance: number): number { + if (this.cumulativeDistanceLUT.length === 0) { + this.generateCumulativeDistanceLUT(); + } + if (distance <= 0) return 0; + if ( + distance >= + this.cumulativeDistanceLUT[this.cumulativeDistanceLUT.length - 1].distance + ) { + return 1; + } + + let lowerIndex = this.lastFoundIndex; + let upperIndex = this.cumulativeDistanceLUT.length - 1; + // Binary search for the closest range + while (upperIndex - lowerIndex > 1) { + const midIndex = Math.floor((upperIndex + lowerIndex) / 2); + if (this.cumulativeDistanceLUT[midIndex].distance < distance) { + lowerIndex = midIndex; + } else { + upperIndex = midIndex; + } + } + + // Interpolate between these two points + const lower = this.cumulativeDistanceLUT[lowerIndex]; + const upper = this.cumulativeDistanceLUT[upperIndex]; + this.lastFoundIndex = lowerIndex; + + // Linear interpolation of t based on the distance + const t = + lower.t + + ((distance - lower.distance) * (upper.t - lower.t)) / + (upper.distance - lower.distance); + return t; } }