Skip to content

Commit

Permalink
[bugfix] Improve homing missile by making it feel less accuriate
Browse files Browse the repository at this point in the history
  • Loading branch information
Half-Shot committed Dec 30, 2024
1 parent 598d6fd commit 90063d9
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 38 deletions.
57 changes: 19 additions & 38 deletions src/entities/phys/homingMissile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,27 @@ import {
import { Coordinate, MetersValue } from "../../utils/coodinate";
import { AssetPack } from "../../assets";
import { WormInstance } from "../../logic/teams";
import { angleForVector } from "../../utils";
import { angleForVector, mult } from "../../utils";
import { EntityType } from "../type";
import Logger from "../../log";
import globalFlags, { DebugLevel } from "../../flags";
import { pointsOnBezierCurves } from "points-on-curve";
import { arcPoints } from "../../utils/arc";

const logger = new Logger("HomingMissile");

const ACTIVATION_TIME_MS = 50;
const ACTIVATION_TIME_MS = 65;
const ADJUSTMENT_TIME_MS = 6;
const forceMult = new Vector2(7, 7);

/**
* Homing missile that attempts to hit a point target.
*/
export class HomingMissile extends TimedExplosive {

public static getMissilePath(start: Coordinate, target: Coordinate): Coordinate[] {
return arcPoints([start.worldX, start.worldY], [target.worldX, target.worldY], 0.550).map(v => Coordinate.fromWorld(v[0], v[1]));
}

public static readAssets(assets: AssetPack) {
HomingMissile.textureInactive = assets.textures.missileInactive;
HomingMissile.textureActive = assets.textures.missileActive;
Expand Down Expand Up @@ -102,7 +109,7 @@ export class HomingMissile extends TimedExplosive {
);
this.sprite.x = position.screenX;
this.sprite.y = position.screenY;
this.sprite.scale.set(0.5, 0.5);
this.sprite.scale.set(0.75, 0.75);
this.sprite.anchor.set(0.5, 0.5);

// Align sprite with body.
Expand All @@ -116,41 +123,13 @@ export class HomingMissile extends TimedExplosive {
return;
}
this.lastPathAdjustment += dt;

if (!this.hasActivated && this.lastPathAdjustment >= ACTIVATION_TIME_MS) {
this.hasActivated = true;
const { target } = this;
const { position } = this.sprite;
this.sprite.texture = HomingMissile.textureActive;
const diff = {
x: Math.abs(position.x - target.screenX),
y: Math.abs(position.y - target.screenY),
};

// TODO: This makes a triangle when the target is behind position??
const midPointA = Coordinate.fromScreen(
(position.x > target.screenX ? target.screenX : position.x) +
diff.x * 0.25,
(position.y > target.screenY ? target.screenY : position.y) +
diff.y * 0.25 -
250,
);
const midPointB = Coordinate.fromScreen(
(position.x > target.screenX ? target.screenX : position.x) +
diff.x * 0.75,
(position.y > target.screenY ? target.screenY : position.y) +
diff.y * 0.75 -
250,
);
this.forcePath = pointsOnBezierCurves(
[
[position.x, position.y],
[midPointA.screenX, midPointA.screenY],
[midPointB.screenX, midPointB.screenY],
[target.screenX, target.screenY],
],
0.05,
).map(([x, y]) => Coordinate.fromScreen(x, y));
this.body.sleep();
this.forcePath = HomingMissile.getMissilePath(Coordinate.fromScreen(position.x, position.y), target);
// Draw paths once
const start = this.forcePath.pop()!;
this.debugGfx.moveTo(start.screenX, start.screenY);
Expand All @@ -161,13 +140,15 @@ export class HomingMissile extends TimedExplosive {
}
logger.debug("Activated!");
}
if (this.hasActivated) {


if (this.hasActivated && this.lastPathAdjustment >= ADJUSTMENT_TIME_MS) {
this.lastPathAdjustment = 0;
const [nextOrLastItem] = this.forcePath.splice(0, 1);
if (nextOrLastItem) {
this.body.setTranslation(nextOrLastItem.toWorldVector(), false);
} else {
this.body.wakeUp();
const translation = this.body.translation();
const impulse = mult(new Vector2(nextOrLastItem.worldX - translation.x, nextOrLastItem.worldY - translation.y), forceMult);
this.body.setLinvel(impulse, true);
}
}
this.body.setRotation(angleForVector(this.body.linvel()), false);
Expand Down
96 changes: 96 additions & 0 deletions src/utils/arc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@

function yIntercept(a: [number, number], b: [number, number]): number {
// returns the y intercept of the perpendicular bisector of the line from point A to B
let m = inverseSlope(a, b);

Check failure on line 4 in src/utils/arc.ts

View workflow job for this annotation

GitHub Actions / ci

'm' is never reassigned. Use 'const' instead
let p = midpoint(a, b);

Check failure on line 5 in src/utils/arc.ts

View workflow job for this annotation

GitHub Actions / ci

'p' is never reassigned. Use 'const' instead
let x = p[0];

Check failure on line 6 in src/utils/arc.ts

View workflow job for this annotation

GitHub Actions / ci

'x' is never reassigned. Use 'const' instead
let y = p[1];

Check failure on line 7 in src/utils/arc.ts

View workflow job for this annotation

GitHub Actions / ci

'y' is never reassigned. Use 'const' instead
return y - m * x;
}

function inverseSlope(a: [number, number], b: [number, number]): number {
// returns the inverse of the slope of the line from point A to B
// which is the slope of the perpendicular bisector
return -1 * (1 / slope(a, b));
}

function slope(a: [number, number], b: [number, number]): number {
// returns the slope of the line from point A to B
return (b[1] - a[1]) / (b[0] - a[0]);
}

function getP3(a: [number, number], b: [number, number], frac: number): [number, number] {
let mid = midpoint(a, b);

Check failure on line 23 in src/utils/arc.ts

View workflow job for this annotation

GitHub Actions / ci

'mid' is never reassigned. Use 'const' instead
let m = inverseSlope(a, b);

Check failure on line 24 in src/utils/arc.ts

View workflow job for this annotation

GitHub Actions / ci

'm' is never reassigned. Use 'const' instead
// check if B is below A
let bLower = b[1] < a[1] ? -1 : 1;

Check failure on line 26 in src/utils/arc.ts

View workflow job for this annotation

GitHub Actions / ci

'bLower' is never reassigned. Use 'const' instead

// distance from midpoint along slope: between 0 and half the distance between the two points
let d = 0.5 * dist(a, b) * frac;

Check failure on line 29 in src/utils/arc.ts

View workflow job for this annotation

GitHub Actions / ci

'd' is never reassigned. Use 'const' instead

let x = d / Math.sqrt(1 + Math.pow(m, 2));

Check failure on line 31 in src/utils/arc.ts

View workflow job for this annotation

GitHub Actions / ci

'x' is never reassigned. Use 'const' instead
let y = m * x;
return [bLower * x + mid[0], bLower * y + mid[1]];
// return [mid[0] + d, mid[1] - (d * (b[0] - a[0])) / (b[1] - a[1])];
}

function dist(a: [number, number], b: [number, number]): number {
return Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2));
}

function midpoint(a: [number, number], b: [number, number]): [number, number] {
return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
}

function getCenter(a: [number, number], b: [number, number], frac: number): [number, number] {
let c = getP3(a, b, frac);
let b1 = yIntercept(a, b);
let b2 = yIntercept(a, c);
let m1 = inverseSlope(a, b);
let m2 = inverseSlope(a, c);

// find the intersection of the two perpendicular bisectors
// i.e. solve m1 * x + b2 = m2 * x + b2 for x
let x = (b2 - b1) / (m1 - m2);
// sub x back into one of the linear equations to get y
let y = m1 * x + b1;

return [x, y];
}

export function arcPoints(a: [number, number], b: [number, number], r_frac: number): [number, number][] {
// a: origin point
// b: destination point
// r_frac: arc radius as a fraction of half the distance between a and b
// -- 1 results in a semicircle arc, the arc flattens out the closer to 0 the number is set, 0 is invalid
// n: number of points to sample from arc

let invert = a[0] > b[0];
let c = getCenter(a, b, r_frac);
let r = dist(c, a);

const n = Math.round(r / 2);

let aAngle, bAngle = 0;

if (invert) {
aAngle = Math.atan2(b[1] - c[1], b[0] - c[0]);
bAngle = Math.atan2(a[1] - c[1], a[0] - c[0]);
} else {
aAngle = Math.atan2(a[1] - c[1], a[0] - c[0]);
bAngle = Math.atan2(b[1] - c[1], b[0] - c[0]);
}

if (aAngle > bAngle) {
bAngle += 2 * Math.PI;
}

const increments = (bAngle - aAngle) / n;
const samples: [number, number][] = Array.from({length: n}).map((_v, index) => aAngle + (increments*index))
.map((d) => [Math.cos(d) * r + c[0], Math.sin(d) * r + c[1]]);

if (invert) {
return samples.reverse();
}
return samples;
}

0 comments on commit 90063d9

Please sign in to comment.