diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index b071fd9cc..df41da1b2 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -6,6 +6,7 @@ import { UnitType } from "../../../core/game/Game"; import { TileRef } from "../../../core/game/GameMap"; import { GameUpdateType } from "../../../core/game/GameUpdates"; import { GameView, PlayerView, UnitView } from "../../../core/game/GameView"; +import { BezenhamLine } from "../../../core/utilities/Line"; import { AlternateViewEvent, MouseUpEvent, @@ -31,9 +32,9 @@ export class UnitLayer implements Layer { private canvas: HTMLCanvasElement; private context: CanvasRenderingContext2D; private transportShipTrailCanvas: HTMLCanvasElement; - private transportShipTrailContext: CanvasRenderingContext2D; + private unitTrailContext: CanvasRenderingContext2D; - private boatToTrail = new Map(); + private unitToTrail = new Map(); private theme: Theme = null; @@ -190,8 +191,7 @@ export class UnitLayer implements Layer { this.canvas = document.createElement("canvas"); this.context = this.canvas.getContext("2d"); this.transportShipTrailCanvas = document.createElement("canvas"); - this.transportShipTrailContext = - this.transportShipTrailCanvas.getContext("2d"); + this.unitTrailContext = this.transportShipTrailCanvas.getContext("2d"); this.canvas.width = this.game.width(); this.canvas.height = this.game.height(); @@ -200,7 +200,7 @@ export class UnitLayer implements Layer { this.updateUnitsSprites(); - this.boatToTrail.forEach((trail, unit) => { + this.unitToTrail.forEach((trail, unit) => { for (const t of trail) { this.paintCell( this.game.x(t), @@ -208,7 +208,7 @@ export class UnitLayer implements Layer { this.relationship(unit), this.theme.territoryColor(unit.owner()), 150, - this.transportShipTrailContext, + this.unitTrailContext, ); } }); @@ -333,8 +333,92 @@ export class UnitLayer implements Layer { this.drawSprite(unit); } + private drawTrail(trail: number[], color: Colord, rel: Relationship) { + // Paint new trail + for (const t of trail) { + this.paintCell( + this.game.x(t), + this.game.y(t), + rel, + color, + 150, + this.unitTrailContext, + ); + } + } + + private clearTrail(unit: UnitView) { + const trail = this.unitToTrail.get(unit); + const rel = this.relationship(unit); + for (const t of trail) { + this.clearCell(this.game.x(t), this.game.y(t), this.unitTrailContext); + } + this.unitToTrail.delete(unit); + + // Repaint overlapping trails + const trailSet = new Set(trail); + for (const [other, trail] of this.unitToTrail) { + for (const t of trail) { + if (trailSet.has(t)) { + this.paintCell( + this.game.x(t), + this.game.y(t), + rel, + this.theme.territoryColor(other.owner()), + 150, + this.unitTrailContext, + ); + } + } + } + } + private handleNuke(unit: UnitView) { + const rel = this.relationship(unit); + + if (!this.unitToTrail.has(unit)) { + this.unitToTrail.set(unit, []); + } + + 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 + if (trail.length >= 1) { + const currentX = this.game.x(unit.lastTile()); + const currentY = this.game.y(unit.lastTile()); + const lastX = this.game.x(trail[trail.length - 1]); + const lastY = this.game.y(trail[trail.length - 1]); + const line = new BezenhamLine(lastX, lastY, currentX, currentY); + let point = line.increment(); + while (point !== true) { + trail.push(this.game.ref(point.x, point.y)); + point = line.increment(); + } + newTrailSize = line.size(); + } else { + trail.push(unit.lastTile()); + } + + // Paint new trail + for (const t of trail.slice(-newTrailSize)) { + this.paintCell( + this.game.x(t), + this.game.y(t), + rel, + this.theme.territoryColor(unit.owner()), + 150, + this.unitTrailContext, + ); + } + this.drawTrail( + trail.slice(-newTrailSize), + this.theme.territoryColor(unit.owner()), + rel, + ); this.drawSprite(unit); + if (!unit.isActive()) { + this.clearTrail(unit); + } } private handleMIRVWarhead(unit: UnitView) { @@ -361,52 +445,22 @@ export class UnitLayer implements Layer { private handleBoatEvent(unit: UnitView) { const rel = this.relationship(unit); - if (!this.boatToTrail.has(unit)) { - this.boatToTrail.set(unit, []); + if (!this.unitToTrail.has(unit)) { + this.unitToTrail.set(unit, []); } - const trail = this.boatToTrail.get(unit); + const trail = this.unitToTrail.get(unit); trail.push(unit.lastTile()); // Paint trail - for (const t of trail.slice(-1)) { - this.paintCell( - this.game.x(t), - this.game.y(t), - rel, - this.theme.territoryColor(unit.owner()), - 150, - this.transportShipTrailContext, - ); - } - + this.drawTrail( + trail.slice(-1), + this.theme.territoryColor(unit.owner()), + rel, + ); this.drawSprite(unit); if (!unit.isActive()) { - for (const t of trail) { - this.clearCell( - this.game.x(t), - this.game.y(t), - this.transportShipTrailContext, - ); - } - this.boatToTrail.delete(unit); - - // Repaint overlapping trails - const trailSet = new Set(trail); - for (const [other, trail] of this.boatToTrail) { - for (const t of trail) { - if (trailSet.has(t)) { - this.paintCell( - this.game.x(t), - this.game.y(t), - rel, - this.theme.territoryColor(other.owner()), - 150, - this.transportShipTrailContext, - ); - } - } - } + this.clearTrail(unit); } } diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 8795e349f..caa787c3d 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -11,7 +11,7 @@ import { UnitType, } from "../game/Game"; import { TileRef } from "../game/GameMap"; -import { AirPathFinder } from "../pathfinding/PathFinding"; +import { ParabolaPathFinder } from "../pathfinding/PathFinding"; import { PseudoRandom } from "../PseudoRandom"; export class NukeExecution implements Execution { @@ -21,7 +21,7 @@ export class NukeExecution implements Execution { private nuke: Unit; private random: PseudoRandom; - private pathFinder: AirPathFinder; + private pathFinder: ParabolaPathFinder; constructor( private type: NukeType, @@ -45,7 +45,7 @@ export class NukeExecution implements Execution { if (this.speed == -1) { this.speed = this.mg.config().defaultNukeSpeed(); } - this.pathFinder = new AirPathFinder(mg, this.random); + this.pathFinder = new ParabolaPathFinder(mg); } public target(): Player | TerraNullius { @@ -95,6 +95,7 @@ export class NukeExecution implements Execution { this.active = false; return; } + this.pathFinder.computeControlPoints(spawn, this.dst); this.nuke = this.player.buildUnit(this.type, 0, spawn, { detonationDst: this.dst, }); @@ -146,15 +147,13 @@ export class NukeExecution implements Execution { return; } - for (let i = 0; i < this.speed; i++) { - // Move to next tile - const nextTile = this.pathFinder.nextTile(this.nuke.tile(), this.dst); - if (nextTile === true) { - this.detonate(); - return; - } else { - this.nuke.move(nextTile); - } + // Move to next tile + const nextTile = this.pathFinder.nextTile(this.speed); + if (nextTile === true) { + this.detonate(); + return; + } else { + this.nuke.move(nextTile); } } diff --git a/src/core/pathfinding/PathFinding.ts b/src/core/pathfinding/PathFinding.ts index 1a255c8dd..4f9823a78 100644 --- a/src/core/pathfinding/PathFinding.ts +++ b/src/core/pathfinding/PathFinding.ts @@ -2,9 +2,54 @@ 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 { AStar, PathFindResultType, TileResult } from "./AStar"; import { MiniAStar } from "./MiniAStar"; +export class ParabolaPathFinder { + constructor(private mg: GameMap) {} + private curve: BezierCurve; + private distance: number; + + computeControlPoints(orig: TileRef, dst: TileRef) { + 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); + const dx = dstX - origX; + const dy = dstY - origY; + this.distance = Math.sqrt(dx * dx + dy * dy); + + const x0 = origX + (dstX - origX) / 4; + const y0 = Math.max( + origY + (dstY - origY) / 4 - Math.max(this.distance / 3, 50), + 0, + ); + const x1 = origX + ((dstX - origX) * 3) / 4; + const y1 = Math.max( + origY + ((dstY - origY) * 3) / 4 - Math.max(this.distance / 3, 50), + 0, + ); + + this.curve.setControlPoint0(x0, y0); + this.curve.setControlPoint1(x1, y1); + } + + nextTile(speed: number): TileRef | true { + if (!this.curve) { + return; + } + const incr = speed / (this.distance * 2); + const nextPoint = this.curve.increment(incr); + if (!nextPoint) { + return true; + } + return this.mg.ref(Math.floor(nextPoint.x), Math.floor(nextPoint.y)); + } +} + export class AirPathFinder { constructor( private mg: GameMap, diff --git a/src/core/utilities/Line.ts b/src/core/utilities/Line.ts new file mode 100644 index 000000000..5d44e2a79 --- /dev/null +++ b/src/core/utilities/Line.ts @@ -0,0 +1,93 @@ +export class BezenhamLine { + constructor( + private x0: number, + private y0: number, + private x1: number, + private y1: number, + ) { + this.dx = Math.abs(this.x1 - this.x0); + this.dy = Math.abs(this.y1 - this.y0); + this.sx = this.x0 < this.x1 ? 1 : -1; + this.sy = this.y0 < this.y1 ? 1 : -1; + this.error = this.dx - this.dy; + } + + private dx: number; + private dy: number; + private sx: number; + private sy: number; + private error: number; + + size() { + return Math.max(this.dx, this.dy) + 1; + } + + // Increment either by 1 in x or y + increment(): { x: number; y: number } | true { + if (this.x0 === this.x1 && this.y0 === this.y1) { + return true; + } + const x = this.x0; + const y = this.y0; + const err2 = 2 * this.error; + + if (err2 > -this.dy) { + this.error -= this.dy; + this.x0 += this.sx; + } + if (err2 < this.dx) { + this.error += this.dx; + this.y0 += this.sy; + } + return { x, y }; + } +} + +export class BezierCurve { + constructor( + private x0: number, + private y0: number, + private x1: number, + private y2: number, + ) { + const dx = this.x1 - this.x0; + const dy = this.y2 - this.y0; + const dist = Math.abs(this.x1 - this.x0); + } + + private t: number = 0; + private controlPoint0X: number; + private controlPoint0Y: number; + private controlPoint1X: number; + private controlPoint1Y: number; + + setControlPoint0(x, y) { + this.controlPoint0X = x; + this.controlPoint0Y = y; + } + + setControlPoint1(x, y) { + this.controlPoint1X = x; + 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.y2; + return { x: nextX, y: nextY }; + } +}