mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 13:30:43 +00:00
Fix long-distance nuke interception (#1900)
## Description: Fix a bug introduced by an optimization that caused some incoming nukes to be ignored. As a result, long-distance nukes could evade in-range SAM. Changes: Replaced the ignored nukes list with a list of precomputed nukes. For incoming long-distance nukes, the interception tile is now computed in advance. When the nuke enters range, the precomputed interception tile is used for interception. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: IngloriousTom --------- Co-authored-by: evanpelle <evanpelle@gmail.com>
This commit is contained in:
@@ -16,31 +16,38 @@ type Target = {
|
||||
tile: TileRef;
|
||||
};
|
||||
|
||||
type InterceptionTile = {
|
||||
tile: TileRef;
|
||||
tick: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Smart SAM targeting system preshoting nukes so its range is strictly enforced
|
||||
*/
|
||||
class SAMTargetingSystem {
|
||||
// Store unreachable nukes so the SAM won't compute an interception point for them every frame
|
||||
private nukesToIgnore: Set<number> = new Set();
|
||||
// Interception tiles are computed a single time, but it may not be reachable yet.
|
||||
// Store the result so it can be intercepted at the proper time, rather than recomputing each ticks
|
||||
// Null interception tile means there are no interception tiles in range. Store it to
|
||||
private readonly precomputedNukes: Map<number, InterceptionTile | null> =
|
||||
new Map();
|
||||
private readonly missileSpeed: number;
|
||||
|
||||
constructor(
|
||||
private mg: Game,
|
||||
private sam: Unit,
|
||||
) {}
|
||||
private readonly mg: Game,
|
||||
private readonly sam: Unit,
|
||||
) {
|
||||
this.missileSpeed = this.mg.config().defaultSamMissileSpeed();
|
||||
}
|
||||
|
||||
updateUnreachableNukes(nearbyUnits: { unit: Unit; distSquared: number }[]) {
|
||||
const nearbyUnitSet = new Set(nearbyUnits.map((u) => u.unit.id()));
|
||||
for (const nukeId of this.nukesToIgnore) {
|
||||
for (const nukeId of this.precomputedNukes.keys()) {
|
||||
if (!nearbyUnitSet.has(nukeId)) {
|
||||
this.nukesToIgnore.delete(nukeId);
|
||||
this.precomputedNukes.delete(nukeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private storeUnreachableNukes(nukeId: number) {
|
||||
this.nukesToIgnore.add(nukeId);
|
||||
}
|
||||
|
||||
private isInRange(tile: TileRef) {
|
||||
const samTile = this.sam.tile();
|
||||
const rangeSquared = this.mg.config().defaultSamRange() ** 2;
|
||||
@@ -48,32 +55,33 @@ class SAMTargetingSystem {
|
||||
}
|
||||
|
||||
private tickToReach(currentTile: TileRef, tile: TileRef): number {
|
||||
const missileSpeed = this.mg.config().defaultSamMissileSpeed();
|
||||
return Math.ceil(this.mg.manhattanDist(currentTile, tile) / missileSpeed);
|
||||
return Math.ceil(
|
||||
this.mg.manhattanDist(currentTile, tile) / this.missileSpeed,
|
||||
);
|
||||
}
|
||||
|
||||
private computeInterceptionTile(unit: Unit): TileRef | undefined {
|
||||
private computeInterceptionTile(unit: Unit): InterceptionTile | undefined {
|
||||
const trajectory = unit.trajectory();
|
||||
const samTile = this.sam.tile();
|
||||
const currentIndex = unit.trajectoryIndex();
|
||||
const explosionTick: number = trajectory.length - currentIndex;
|
||||
for (let i = unit.trajectoryIndex(); i < trajectory.length; i++) {
|
||||
for (let i = currentIndex; i < trajectory.length; i++) {
|
||||
const trajectoryTile = trajectory[i];
|
||||
if (trajectoryTile.targetable && this.isInRange(trajectoryTile.tile)) {
|
||||
const nukeTickToReach = i - currentIndex;
|
||||
const samTickToReach = this.tickToReach(samTile, trajectoryTile.tile);
|
||||
const reachableOnTime = Math.abs(nukeTickToReach - samTickToReach) <= 1;
|
||||
if (reachableOnTime && samTickToReach < explosionTick) {
|
||||
return trajectoryTile.tile;
|
||||
const tickBeforeShooting = nukeTickToReach - samTickToReach;
|
||||
if (samTickToReach < explosionTick && tickBeforeShooting >= 0) {
|
||||
return { tick: tickBeforeShooting, tile: trajectoryTile.tile };
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getSingleTarget(): Target | null {
|
||||
public getSingleTarget(ticks: number): Target | null {
|
||||
// Look beyond the SAM range so it can preshot nukes
|
||||
const detectionRange = this.mg.config().defaultSamRange() * 1.5;
|
||||
const detectionRange = this.mg.config().defaultSamRange() * 2;
|
||||
const nukes = this.mg.nearbyUnits(
|
||||
this.sam.tile(),
|
||||
detectionRange,
|
||||
@@ -92,15 +100,42 @@ class SAMTargetingSystem {
|
||||
|
||||
const targets: Array<Target> = [];
|
||||
for (const nuke of nukes) {
|
||||
if (this.nukesToIgnore.has(nuke.unit.id())) {
|
||||
continue;
|
||||
const nukeId = nuke.unit.id();
|
||||
const cached = this.precomputedNukes.get(nukeId);
|
||||
if (cached !== undefined) {
|
||||
if (cached === null) {
|
||||
// Known unreachable, skip.
|
||||
continue;
|
||||
}
|
||||
if (cached.tick === ticks) {
|
||||
// Time to shoot!
|
||||
targets.push({ tile: cached.tile, unit: nuke.unit });
|
||||
this.precomputedNukes.delete(nukeId);
|
||||
continue;
|
||||
}
|
||||
if (cached.tick > ticks) {
|
||||
// Not due yet, skip for now.
|
||||
continue;
|
||||
}
|
||||
// Missed the planned tick (e.g was on cooldown), recompute a new interception tile if possible
|
||||
this.precomputedNukes.delete(nukeId);
|
||||
}
|
||||
const interceptionTile = this.computeInterceptionTile(nuke.unit);
|
||||
if (interceptionTile !== undefined) {
|
||||
targets.push({ unit: nuke.unit, tile: interceptionTile });
|
||||
if (interceptionTile.tick <= 1) {
|
||||
// Shoot instantly
|
||||
|
||||
targets.push({ unit: nuke.unit, tile: interceptionTile.tile });
|
||||
} else {
|
||||
// Nuke will be reachable but not yet. Store the result.
|
||||
this.precomputedNukes.set(nukeId, {
|
||||
tick: interceptionTile.tick + ticks,
|
||||
tile: interceptionTile.tile,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Store unreachable nukes in order to prevent useless interception computation
|
||||
this.storeUnreachableNukes(nuke.unit.id());
|
||||
this.precomputedNukes.set(nukeId, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +260,10 @@ export class SAMLauncherExecution implements Execution {
|
||||
|
||||
let target: Target | null = null;
|
||||
if (mirvWarheadTargets.length === 0) {
|
||||
target = this.targetingSystem.getSingleTarget();
|
||||
target = this.targetingSystem.getSingleTarget(ticks);
|
||||
if (target !== null) {
|
||||
console.log("Target acquired");
|
||||
}
|
||||
}
|
||||
|
||||
const isSingleTarget = target && !target.unit.targetedBySAM();
|
||||
|
||||
Reference in New Issue
Block a user