Clean up and refactor the Unit class (#769)

## Description:

* Merged similar fields so they can be reused (eg warshipTarget =>
targetUnit)
* simplified isCooldown api
* added "touch" method to send update to UI layer
* standardized on "undefined"


## Please complete the following:

- [ ] I have added screenshots for all UI updates
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:

<DISCORD USERNAME>
evan

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Refactor**
- Unified and simplified how unit targets and cooldowns are managed
across all unit types, resulting in more consistent in-game behavior for
nukes, warships, trade ships, and SAM launchers.
- Updated naming and logic for unit targeting and cooldowns, improving
clarity in status displays and interactions.
- Reorganized unit interface and streamlined cooldown handling for
smoother gameplay experience.
- **Bug Fixes**
- Corrected visual indicators for nukes and warships to accurately
reflect their targets.
- **Tests**
- Updated automated tests to align with the new cooldown and targeting
logic.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
evanpelle
2025-05-17 17:45:10 -07:00
committed by GitHub
parent 0871c08858
commit 500b5fcfde
15 changed files with 167 additions and 192 deletions
+1 -1
View File
@@ -461,7 +461,7 @@ export class NameLayer implements Layer {
);
});
const isMyPlayerTarget = nukesSentByOtherPlayer.find((unit) => {
const detonationDst = unit.detonationDst();
const detonationDst = unit.targetTile();
if (detonationDst === undefined) return false;
const targetId = this.game.owner(detonationDst).id();
return myPlayer && targetId === myPlayer.id();
+2 -2
View File
@@ -291,7 +291,7 @@ export class UnitLayer implements Layer {
}
private handleWarShipEvent(unit: UnitView) {
if (unit.warshipTargetId()) {
if (unit.targetUnitId()) {
this.drawSprite(unit, colord({ r: 200, b: 0, g: 0 }));
} else {
this.drawSprite(unit);
@@ -502,7 +502,7 @@ export class UnitLayer implements Layer {
if (this.alternateView) {
let rel = this.relationship(unit);
const dstPortId = unit.dstPortId();
const dstPortId = unit.targetUnitId();
if (unit.type() === UnitType.TradeShip && dstPortId !== undefined) {
const target = this.game.unit(dstPortId)?.owner();
const myPlayer = this.game.myPlayer();
+2 -5
View File
@@ -53,11 +53,8 @@ export class MissileSiloExecution implements Execution {
}
}
if (
this.silo.isCooldown() &&
this.silo.ticksLeftInCooldown(this.mg.config().SiloCooldown()) === 0
) {
this.silo.setCooldown(false);
if (this.silo.ticksLeftInCooldown() === 0) {
this.silo.touch();
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ export class MoveWarshipExecution implements Execution {
console.log("MoveWarshipExecution: warship is already dead");
return;
}
warship.setMoveTarget(this.position);
warship.setTargetTile(this.position);
this.active = false;
}
+1 -1
View File
@@ -156,7 +156,7 @@ export class NukeExecution implements Execution {
.units(UnitType.MissileSilo)
.find((silo) => silo.tile() === spawn);
if (silo) {
silo.setCooldown(true);
silo.launch();
}
return;
}
+7 -9
View File
@@ -135,10 +135,10 @@ export class SAMLauncherExecution implements Execution {
unit.owner() !== this.player && !this.player.isFriendly(unit.owner()),
)
.filter((unit) => {
const dst = unit.detonationDst();
const dst = unit.targetTile();
return (
this.sam !== null &&
dst !== null &&
dst !== undefined &&
this.mg.manhattanDist(dst, this.sam.tile()) <
this.MIRVWarheadProtectionRadius
);
@@ -149,19 +149,17 @@ export class SAMLauncherExecution implements Execution {
target = this.getSingleTarget();
}
if (
this.sam.isCooldown() &&
this.sam.ticksLeftInCooldown(this.mg.config().SAMCooldown()) === 0
) {
this.sam.setCooldown(false);
if (this.sam.ticksLeftInCooldown() === 0) {
// Touch SAM to update sprite to show not in cooldown.
this.sam.touch();
}
const isSingleTarget = target && !target.targetedBySAM();
if (
(isSingleTarget || mirvWarheadTargets.length > 0) &&
!this.sam.isCooldown()
!this.sam.isInCooldown()
) {
this.sam.setCooldown(true);
this.sam.launch();
const type =
mirvWarheadTargets.length > 0 ? UnitType.MIRVWarhead : target?.type();
if (type === undefined) throw new Error("Unknown unit type");
+2 -2
View File
@@ -94,7 +94,7 @@ export class TradeShipExecution implements Execution {
return;
} else {
this._dstPort = ports[0];
this.tradeShip.setDstPort(this._dstPort);
this.tradeShip.setTargetUnit(this._dstPort);
}
}
@@ -122,7 +122,7 @@ export class TradeShipExecution implements Execution {
break;
case PathFindResultType.Pending:
// Fire unit event to rerender.
this.tradeShip.move(this.tradeShip.tile());
this.tradeShip.touch();
break;
case PathFindResultType.NextTile:
this._dstPort.cachePut(this.tradeShip.tile(), result.tile);
+58 -53
View File
@@ -19,9 +19,9 @@ export class WarshipExecution implements Execution {
private _owner: Player;
private active = true;
private warship: Unit | null = null;
private mg: Game | null = null;
private mg: Game;
private target: Unit | null = null;
private target: Unit | undefined = undefined;
private pathfinder: PathFinder | null = null;
private patrolTile: TileRef | undefined;
@@ -35,6 +35,7 @@ export class WarshipExecution implements Execution {
) {}
init(mg: Game, ticks: number): void {
this.mg = mg;
if (!mg.hasPlayer(this.playerID)) {
console.log(`WarshipExecution: player ${this.playerID} not found`);
this.active = false;
@@ -42,7 +43,6 @@ export class WarshipExecution implements Execution {
}
this.pathfinder = PathFinder.Mini(mg, 5000);
this._owner = mg.player(this.playerID);
this.mg = mg;
this.patrolTile = this.patrolCenterTile;
this.random = new PseudoRandom(mg.ticks());
}
@@ -56,14 +56,14 @@ export class WarshipExecution implements Execution {
const result = this.pathfinder.nextTile(this.warship.tile(), target);
switch (result.type) {
case PathFindResultType.Completed:
this.warship.setMoveTarget(null);
this.warship.move(this.warship.tile());
this.warship.setTargetTile(undefined);
this.warship.touch();
return;
case PathFindResultType.NextTile:
this.warship.move(result.tile);
break;
case PathFindResultType.Pending:
this.warship.move(this.warship.tile());
this.warship.touch();
break;
case PathFindResultType.PathNotFound:
consolex.log(`path not found to target`);
@@ -72,7 +72,11 @@ export class WarshipExecution implements Execution {
}
private shoot() {
if (this.mg === null || this.warship === null || this.target === null) {
if (
this.mg === null ||
this.warship === null ||
this.target === undefined
) {
throw new Error("Warship not initialized");
}
const shellAttackRate = this.mg.config().warshipShellAttackRate();
@@ -89,7 +93,7 @@ export class WarshipExecution implements Execution {
if (!this.target.hasHealth()) {
// Don't send multiple shells to target that can be oneshotted
this.alreadySentShell.add(this.target);
this.target = null;
this.target = undefined;
return;
}
}
@@ -105,8 +109,11 @@ export class WarshipExecution implements Execution {
return;
}
}
this.warship.setWarshipTarget(this.target);
if (this.target === null || this.target.type() !== UnitType.TradeShip) {
this.warship.setTargetUnit(this.target);
if (
this.target === undefined ||
this.target.type() !== UnitType.TradeShip
) {
// Patrol unless we are hunting down a tradeship
const result = this.pathfinder.nextTile(
this.warship.tile(),
@@ -115,13 +122,13 @@ export class WarshipExecution implements Execution {
switch (result.type) {
case PathFindResultType.Completed:
this.patrolTile = undefined;
this.warship.move(this.warship.tile());
this.warship.touch();
break;
case PathFindResultType.NextTile:
this.warship.move(result.tile);
break;
case PathFindResultType.Pending:
this.warship.move(this.warship.tile());
this.warship.touch();
return;
case PathFindResultType.PathNotFound:
consolex.log(`path not found to patrol tile`);
@@ -153,13 +160,12 @@ export class WarshipExecution implements Execution {
this.active = false;
return;
}
if (this.target !== null && !this.target.isActive()) {
this.target = null;
if (this.target !== undefined && !this.target.isActive()) {
this.target = undefined;
}
const hasPort = this._owner.units(UnitType.Port).length > 0;
if (this.mg === null) throw new Error("Game not initialized");
const warship = this.warship;
if (warship === null) throw new Error("Warship not initialized");
if (warship === undefined) throw new Error("Warship not initialized");
const ships = this.mg
.nearbyUnits(
this.warship.tile(),
@@ -175,68 +181,67 @@ export class WarshipExecution implements Execution {
(unit.type() !== UnitType.TradeShip ||
(hasPort &&
this.warship !== null &&
unit.dstPort()?.owner() !== this.warship.owner() &&
!unit.dstPort()?.owner().isFriendly(this.warship.owner()) &&
unit.targetUnit()?.owner() !== this.warship.owner() &&
!unit.targetUnit()?.owner().isFriendly(this.warship.owner()) &&
unit.isSafeFromPirates() !== true)),
);
this.target =
ships.sort((a, b) => {
const { unit: unitA, distSquared: distA } = a;
const { unit: unitB, distSquared: distB } = b;
this.target = ships.sort((a, b) => {
const { unit: unitA, distSquared: distA } = a;
const { unit: unitB, distSquared: distB } = b;
// Prioritize Warships
if (
unitA.type() === UnitType.Warship &&
unitB.type() !== UnitType.Warship
)
return -1;
if (
unitA.type() !== UnitType.Warship &&
unitB.type() === UnitType.Warship
)
return 1;
// Prioritize Warships
if (
unitA.type() === UnitType.Warship &&
unitB.type() !== UnitType.Warship
)
return -1;
if (
unitA.type() !== UnitType.Warship &&
unitB.type() === UnitType.Warship
)
return 1;
// Then favor Transport Ships over Trade Ships
if (
unitA.type() === UnitType.TransportShip &&
unitB.type() !== UnitType.TransportShip
)
return -1;
if (
unitA.type() !== UnitType.TransportShip &&
unitB.type() === UnitType.TransportShip
)
return 1;
// Then favor Transport Ships over Trade Ships
if (
unitA.type() === UnitType.TransportShip &&
unitB.type() !== UnitType.TransportShip
)
return -1;
if (
unitA.type() !== UnitType.TransportShip &&
unitB.type() === UnitType.TransportShip
)
return 1;
// If both are the same type, sort by distance (lower `distSquared` means closer)
return distA - distB;
})[0]?.unit ?? null;
// If both are the same type, sort by distance (lower `distSquared` means closer)
return distA - distB;
})[0]?.unit;
const moveTarget = this.warship.moveTarget();
const moveTarget = this.warship.targetTile();
if (moveTarget) {
this.goToMoveTarget(moveTarget);
// If we have a "move target" then we cannot target trade ships as it
// requires moving.
if (this.target && this.target.type() === UnitType.TradeShip) {
this.target = null;
this.target = undefined;
}
} else if (!this.target || this.target.type() !== UnitType.TradeShip) {
this.patrol();
}
if (
this.target === null ||
this.target === undefined ||
!this.target.isActive() ||
this.target.owner() === this._owner ||
this.target.isSafeFromPirates() === true
) {
// In case another warship captured or destroyed target, or the target escaped into safe waters
this.target = null;
this.target = undefined;
return;
}
this.warship.setWarshipTarget(this.target);
this.warship.setTargetUnit(this.target);
// If we have a move target we do not want to go after trading ships
if (!this.target) {
@@ -258,7 +263,7 @@ export class WarshipExecution implements Execution {
switch (result.type) {
case PathFindResultType.Completed:
this._owner.captureUnit(this.target);
this.target = null;
this.target = undefined;
this.warship.move(this.warship.tile());
return;
case PathFindResultType.NextTile:
+30 -32
View File
@@ -322,57 +322,55 @@ export class PlayerInfo {
}
export interface Unit {
// Common properties.
id(): number;
// Properties
type(): UnitType;
troops(): number;
owner(): Player;
info(): UnitInfo;
// Location
delete(displayerMessage?: boolean): void;
tile(): TileRef;
lastTile(): TileRef;
move(tile: TileRef): void;
// State
isActive(): boolean;
setOwner(owner: Player): void;
touch(): void;
toUpdate(): UnitUpdate;
// Targeting
setTargetTile(cell: TileRef | undefined): void;
targetTile(): TileRef | undefined;
setTargetUnit(unit: Unit | undefined): void;
targetUnit(): Unit | undefined;
setTargetedBySAM(targeted: boolean): void;
targetedBySAM(): boolean;
// Health
hasHealth(): boolean;
health(): number;
modifyHealth(delta: number): void;
setWarshipTarget(target: Unit | null): void; // warship only
warshipTarget(): Unit | null;
// Troops
setTroops(troops: number): void;
troops(): number;
setOwner(owner: Player): void;
setCooldown(triggerCooldown: boolean): void;
ticksLeftInCooldown(cooldownDuration: number): Tick;
isCooldown(): boolean;
setDstPort(dstPort: Unit): void;
dstPort(): Unit | null; // Only for trade ships
// --- UNIT SPECIFIC ---
// SAMs & Missile Silos
launch(): void;
ticksLeftInCooldown(): Tick | undefined;
isInCooldown(): boolean;
// Trade Ships
setSafeFromPirates(): void; // Only for trade ships
isSafeFromPirates(): boolean; // Only for trade ships
detonationDst(): TileRef | null; // Only for nukes
setMoveTarget(cell: TileRef | null): void;
moveTarget(): TileRef | null;
setTargetedBySAM(targeted: boolean): void;
targetedBySAM(): boolean;
// Mutations
setTroops(troops: number): void;
delete(displayerMessage?: boolean): void;
// Only for Construction type
// Construction
constructionType(): UnitType | null;
setConstructionType(type: UnitType): void;
// Updates
toUpdate(): UnitUpdate;
cachePut(from: TileRef, to: TileRef): void; // ports only
cacheGet(from: TileRef): TileRef | undefined; // ports only
// Ports
cachePut(from: TileRef, to: TileRef): void;
cacheGet(from: TileRef): TileRef | undefined;
}
export interface TerraNullius {
+2 -3
View File
@@ -73,9 +73,8 @@ export interface UnitUpdate {
pos: TileRef;
lastPos: TileRef;
isActive: boolean;
dstPortId?: number; // Only for trade ships
detonationDst?: TileRef; // Only for nukes
warshipTargetId?: number;
targetUnitId?: number; // Only for trade ships
targetTile?: TileRef; // Only for nukes
health?: number;
constructionType?: UnitType;
ticksLeftInCooldown?: Tick;
+5 -15
View File
@@ -8,7 +8,6 @@ import {
GameUpdates,
Gold,
NameViewData,
nukeTypes,
Player,
PlayerActions,
PlayerBorderTiles,
@@ -83,7 +82,7 @@ export class UnitView {
return this.data.pos;
}
owner(): PlayerView {
return this.gameView.playerBySmallID(this.data.ownerID) as PlayerView;
return this.gameView.playerBySmallID(this.data.ownerID)! as PlayerView;
}
isActive(): boolean {
return this.data.isActive;
@@ -97,20 +96,11 @@ export class UnitView {
constructionType(): UnitType | undefined {
return this.data.constructionType;
}
dstPortId(): number | undefined {
return this.data.dstPortId;
targetUnitId(): number | undefined {
return this.data.targetUnitId;
}
detonationDst(): TileRef | undefined {
if (!nukeTypes.includes(this.type())) {
throw Error("Must be a nuke");
}
return this.data.detonationDst;
}
warshipTargetId(): number | undefined {
if (this.type() !== UnitType.Warship) {
throw Error("Must be a warship");
}
return this.data.warshipTargetId;
targetTile(): TileRef | undefined {
return this.data.targetTile;
}
ticksLeftInCooldown(): Tick | undefined {
return this.data.ticksLeftInCooldown;
+1 -1
View File
@@ -796,7 +796,7 @@ export class PlayerImpl implements Player {
// only get missilesilos that are not on cooldown
const spawns = this.units(UnitType.MissileSilo)
.filter((silo) => {
return !silo.isCooldown();
return !silo.isInCooldown();
})
.sort(distSortUnit(this.mg, tile));
if (spawns.length === 0) {
+44 -58
View File
@@ -14,20 +14,16 @@ import { PlayerImpl } from "./PlayerImpl";
export class UnitImpl implements Unit {
private _active = true;
private _targetTile: TileRef | undefined;
private _targetUnit: Unit | undefined;
private _health: bigint;
private _lastTile: TileRef;
private _moveTarget: TileRef | null = null;
private _targetedBySAM = false;
private _safeFromPiratesCooldown: number; // Only for trade ships
private _lastSetSafeFromPirates: number; // Only for trade ships
private _constructionType: UnitType | undefined;
private _lastOwner: PlayerImpl | null = null;
private _troops: number;
private _cooldownTick: Tick | null = null;
private _dstPort: Unit | undefined = undefined; // Only for trade ships
private _detonationDst: TileRef | undefined = undefined; // Only for nukes
private _warshipTarget: Unit | undefined = undefined;
private _cooldownDuration: number | undefined = undefined;
private _cooldownStartTick: Tick | null = null;
private _pathCache: Map<TileRef, TileRef> = new Map();
constructor(
@@ -40,19 +36,22 @@ export class UnitImpl implements Unit {
) {
this._lastTile = _tile;
this._health = toInt(this.mg.unitInfo(_type).maxHealth ?? 1);
this._safeFromPiratesCooldown = this.mg
.config()
.safeFromPiratesCooldownMax();
this._troops = "troops" in params ? (params.troops ?? 0) : 0;
this._dstPort = "dstPort" in params ? params.dstPort : undefined;
this._cooldownDuration =
"cooldownDuration" in params ? params.cooldownDuration : undefined;
this._lastSetSafeFromPirates =
"lastSetSafeFromPirates" in params
? (params.lastSetSafeFromPirates ?? 0)
: 0;
}
touch(): void {
this.mg.addUpdate(this.toUpdate());
}
setTileTarget(tile: TileRef | undefined): void {
this._targetTile = tile;
}
tileTarget(): TileRef | undefined {
return this._targetTile;
}
cachePut(from: TileRef, to: TileRef): void {
this._pathCache.set(from, to);
@@ -66,13 +65,6 @@ export class UnitImpl implements Unit {
}
toUpdate(): UnitUpdate {
const warshipTarget = this.warshipTarget();
const dstPort = this.dstPort();
if (this._lastTile === null) throw new Error("null _lastTile");
const ticksLeftInCooldown =
this._cooldownDuration !== undefined
? this.ticksLeftInCooldown(this._cooldownDuration)
: undefined;
return {
type: GameUpdateType.Unit,
unitType: this._type,
@@ -85,10 +77,9 @@ export class UnitImpl implements Unit {
lastPos: this._lastTile,
health: this.hasHealth() ? Number(this._health) : undefined,
constructionType: this._constructionType,
dstPortId: dstPort?.id() ?? undefined,
warshipTargetId: warshipTarget?.id() ?? undefined,
detonationDst: this.detonationDst() ?? undefined,
ticksLeftInCooldown,
targetUnitId: this._targetUnit?.id() ?? undefined,
targetTile: this.targetTile() ?? undefined,
ticksLeftInCooldown: this.ticksLeftInCooldown() ?? undefined,
};
}
@@ -97,7 +88,6 @@ export class UnitImpl implements Unit {
}
lastTile(): TileRef {
if (this._lastTile === null) throw new Error("null _lastTile");
return this._lastTile;
}
@@ -111,6 +101,7 @@ export class UnitImpl implements Unit {
this.mg.addUnit(this);
this.mg.addUpdate(this.toUpdate());
}
setTroops(troops: number): void {
this._troops = troops;
}
@@ -203,52 +194,47 @@ export class UnitImpl implements Unit {
return `Unit:${this._type},owner:${this.owner().name()}`;
}
setWarshipTarget(target: Unit) {
this._warshipTarget = target;
launch(): void {
this._cooldownStartTick = this.mg.ticks();
this.mg.addUpdate(this.toUpdate());
}
warshipTarget(): Unit | null {
return this._warshipTarget ?? null;
}
detonationDst(): TileRef | null {
return this._detonationDst ?? null;
}
dstPort(): Unit | null {
return this._dstPort ?? null;
}
// set the cooldown to the current tick or remove it
setCooldown(triggerCooldown: boolean): void {
if (triggerCooldown) {
this._cooldownTick = this.mg.ticks();
this.mg.addUpdate(this.toUpdate());
ticksLeftInCooldown(): Tick | undefined {
let cooldownDuration = 0;
if (this.type() === UnitType.SAMLauncher) {
cooldownDuration = this.mg.config().SAMCooldown();
} else if (this.type() === UnitType.MissileSilo) {
cooldownDuration = this.mg.config().SiloCooldown();
} else {
this._cooldownTick = null;
this.mg.addUpdate(this.toUpdate());
return undefined;
}
if (!this._cooldownStartTick) {
return undefined;
}
return cooldownDuration - (this.mg.ticks() - this._cooldownStartTick);
}
ticksLeftInCooldown(cooldownDuration: number): Tick {
const cooldownTick = this._cooldownTick ?? 0;
return Math.max(0, cooldownDuration - (this.mg.ticks() - cooldownTick));
isInCooldown(): boolean {
const ticksLeft = this.ticksLeftInCooldown();
return ticksLeft !== undefined && ticksLeft > 0;
}
isCooldown(): boolean {
return this._cooldownTick ? true : false;
setTargetTile(targetTile: TileRef | undefined) {
this._targetTile = targetTile;
}
setDstPort(dstPort: Unit): void {
this._dstPort = dstPort;
targetTile(): TileRef | undefined {
return this._targetTile;
}
setMoveTarget(moveTarget: TileRef) {
this._moveTarget = moveTarget;
setTargetUnit(target: Unit | undefined): void {
this._targetUnit = target;
}
moveTarget(): TileRef | null {
return this._moveTarget;
targetUnit(): Unit | undefined {
return this._targetUnit;
}
setTargetedBySAM(targeted: boolean): void {
@@ -266,7 +252,7 @@ export class UnitImpl implements Unit {
isSafeFromPirates(): boolean {
return (
this.mg.ticks() - this._lastSetSafeFromPirates <
this._safeFromPiratesCooldown
this.mg.config().safeFromPiratesCooldownMax()
);
}
}
+6 -4
View File
@@ -73,17 +73,19 @@ describe("MissileSilo", () => {
});
test("missilesilo should cooldown as long as configured", async () => {
expect(attacker.units(UnitType.MissileSilo)[0].isCooldown()).toBeFalsy();
expect(attacker.units(UnitType.MissileSilo)[0].isInCooldown()).toBeFalsy();
// send the nuke far enough away so it doesnt destroy the silo
attackerBuildsNuke(null, game.ref(50, 50));
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
for (let i = 0; i < game.config().SiloCooldown() - 1; i++) {
for (let i = 0; i < game.config().SiloCooldown() - 2; i++) {
game.executeNextTick();
expect(attacker.units(UnitType.MissileSilo)[0].isCooldown()).toBeTruthy();
expect(
attacker.units(UnitType.MissileSilo)[0].isInCooldown(),
).toBeTruthy();
}
game.executeNextTick();
expect(attacker.units(UnitType.MissileSilo)[0].isCooldown()).toBeFalsy();
expect(attacker.units(UnitType.MissileSilo)[0].isInCooldown()).toBeFalsy();
});
});
+5 -5
View File
@@ -78,7 +78,7 @@ describe("SAM", () => {
test("sam should cooldown as long as configured", async () => {
const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {});
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
expect(sam.isCooldown()).toBeFalsy();
expect(sam.isInCooldown()).toBeFalsy();
const nuke = attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 2), {
detonationDst: game.ref(1, 2),
});
@@ -86,14 +86,14 @@ describe("SAM", () => {
executeTicks(game, 3);
expect(nuke.isActive()).toBeFalsy();
for (let i = 0; i < game.config().SAMCooldown() - 2; i++) {
for (let i = 0; i < game.config().SAMCooldown() - 3; i++) {
game.executeNextTick();
expect(sam.isCooldown()).toBeTruthy();
expect(sam.isInCooldown()).toBeTruthy();
}
executeTicks(game, 2);
expect(sam.isCooldown()).toBeFalsy();
expect(sam.isInCooldown()).toBeFalsy();
});
test("two sams should not target twice same nuke", async () => {
@@ -110,6 +110,6 @@ describe("SAM", () => {
executeTicks(game, 3);
expect(nuke.isActive()).toBeFalsy();
expect([sam1, sam2].filter((s) => s.isCooldown())).toHaveLength(1);
expect([sam1, sam2].filter((s) => s.isInCooldown())).toHaveLength(1);
});
});