diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index b7cdbbe33..437d6cf12 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -1,6 +1,7 @@ import { Execution, Game, + isStructureType, MessageType, Player, TerraNullius, @@ -12,6 +13,8 @@ import { ParabolaPathFinder } from "../pathfinding/PathFinding"; import { PseudoRandom } from "../PseudoRandom"; import { NukeType } from "../StatsSchemas"; +const SPRITE_RADIUS = 16; + export class NukeExecution implements Execution { private active = true; private mg: Game; @@ -221,6 +224,8 @@ export class NukeExecution implements Execution { } } } + + this.redrawBuildings(magnitude.outer + SPRITE_RADIUS); this.active = false; this.nuke.setReachedTarget(); this.nuke.delete(false); @@ -231,6 +236,19 @@ export class NukeExecution implements Execution { .bombLand(this.player, this.target(), this.nuke.type() as NukeType); } + private redrawBuildings(range: number) { + const rangeSquared = range * range; + for (const unit of this.mg.units()) { + if (isStructureType(unit.type())) { + if ( + this.mg.euclideanDistSquared(this.dst, unit.tile()) < rangeSquared + ) { + unit.touch(); + } + } + } + } + owner(): Player { return this.player; } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 1a0dcb411..ab03b752e 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -151,6 +151,19 @@ export enum UnitType { Construction = "Construction", } +const _structureTypes: ReadonlySet = new Set([ + UnitType.City, + UnitType.Construction, + UnitType.DefensePost, + UnitType.SAMLauncher, + UnitType.MissileSilo, + UnitType.Port, +]); + +export function isStructureType(type: UnitType): boolean { + return _structureTypes.has(type); +} + export interface OwnerComp { owner: Player; } diff --git a/tests/core/executions/NukeExecution.test.ts b/tests/core/executions/NukeExecution.test.ts new file mode 100644 index 000000000..5cb1e6c9c --- /dev/null +++ b/tests/core/executions/NukeExecution.test.ts @@ -0,0 +1,72 @@ +import { NukeExecution } from "../../../src/core/execution/NukeExecution"; +import { + Game, + Player, + PlayerInfo, + PlayerType, + UnitType, +} from "../../../src/core/game/Game"; +import { setup } from "../../util/Setup"; +import { TestConfig } from "../../util/TestConfig"; +import { executeTicks } from "../../util/utils"; + +let game: Game; +let player: Player; + +describe("NukeExecution", () => { + beforeEach(async () => { + game = await setup("BigPlains", { infiniteGold: true, instantBuild: true }); + + (game.config() as TestConfig).nukeMagnitudes = jest.fn(() => ({ + inner: 10, + outer: 10, + })); + const player_info = new PlayerInfo( + "us", + "player_id", + PlayerType.Human, + null, + "player_id", + ); + game.addPlayer(player_info); + + while (game.inSpawnPhase()) { + game.executeNextTick(); + } + + player = game.player("player_id"); + }); + + test("nuke should destroy buildings and redraw out of range buildings", async () => { + // Build a city at (1,1) + player.buildUnit(UnitType.City, game.ref(1, 1), {}); + // Build a missile silo in range + player.buildUnit(UnitType.MissileSilo, game.ref(1, 10), {}); + // Build a SAM out of range + const sam = player.buildUnit(UnitType.SAMLauncher, game.ref(1, 11), {}); + sam.touch = jest.fn(); + // Build a Defense post out of range AND out of redraw range + const defensePost = player.buildUnit( + UnitType.DefensePost, + game.ref(1, 27), + {}, + ); + defensePost.touch = jest.fn(); + // Add a nuke execution targeting the city + const nukeExec = new NukeExecution( + UnitType.AtomBomb, + player, + game.ref(1, 1), + game.ref(1, 2), + ); + game.addExecution(nukeExec); + // Run enough ticks for the nuke to detonate + executeTicks(game, 10); + // The city and silo should be destroyed + expect(player.units(UnitType.City)).toHaveLength(0); + expect(player.units(UnitType.MissileSilo)).toHaveLength(0); + expect(player.units(UnitType.SAMLauncher)).toHaveLength(1); + expect(sam.touch).toHaveBeenCalled(); + expect(defensePost.touch).not.toHaveBeenCalled(); + }); +});