From 0891637eb2ad644889f728dc3f5181c94fbe4ad4 Mon Sep 17 00:00:00 2001 From: Readixyee <49241765+Readixyee@users.noreply.github.com> Date: Sat, 29 Mar 2025 23:19:07 +0100 Subject: [PATCH] MissileSilo cooldown (#309) changed the sam cooldown to be a general cooldown function and added a missilesilo cooldown The function either adds the current tick as the starting point of the cooldown or sets the cooldown to null and updates the unit. That way getcooldown function can be used to get back the tick the cooldown was started and can be compared to the cooldown set in the config. changed the sam test / added a missilesilo test --------- Co-authored-by: evanpelle --- .../images/buildings/silo1-reloading.png | Bin 0 -> 157 bytes src/client/graphics/layers/StructureLayer.ts | 23 ++++- src/core/configuration/Config.ts | 3 +- src/core/configuration/DefaultConfig.ts | 10 +- src/core/execution/MissileSiloExecution.ts | 11 +- src/core/execution/NukeExecution.ts | 6 ++ src/core/execution/SAMLauncherExecution.ts | 51 ++++------ src/core/game/Game.ts | 6 +- src/core/game/GameUpdates.ts | 2 +- src/core/game/GameView.ts | 7 +- src/core/game/PlayerImpl.ts | 4 + src/core/game/UnitImpl.ts | 31 ++++-- tests/MissileSilo.test.ts | 95 ++++++++++++++++++ tests/SAM.test.ts | 14 ++- 14 files changed, 204 insertions(+), 59 deletions(-) create mode 100644 resources/images/buildings/silo1-reloading.png create mode 100644 tests/MissileSilo.test.ts diff --git a/resources/images/buildings/silo1-reloading.png b/resources/images/buildings/silo1-reloading.png new file mode 100644 index 0000000000000000000000000000000000000000..28d1aef99096804326ba4d93094581f5c2dfa546 GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`DV{ElAr}70DGLM)f>z8=?2z~n z=JwS7%<4n_?-uNswLsiqmgB61wh1W=KE`LtO6)vj4zuo<)9q{A#<(QwSSG_J`@)+% zpT({@sE8Dt=;Tk=dC+Y&iN~Se?8_2=16hGn+RXV3406mig2krW&jama@O1TaS?83{ F1ONvBG;RO@ literal 0 HcmV?d00001 diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index 87190a303..2f646c1ee 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -7,6 +7,7 @@ import anchorIcon from "../../../../resources/images/buildings/port1.png"; import missileSiloIcon from "../../../../resources/images/buildings/silo1.png"; import SAMMissileIcon from "../../../../resources/images/buildings/silo4.png"; import SAMMissileReloadingIcon from "../../../../resources/images/buildings/silo4-reloading.png"; +import MissileSiloReloadingIcon from "../../../../resources/images/buildings/silo1-reloading.png"; import shieldIcon from "../../../../resources/images/buildings/fortAlt2.png"; import cityIcon from "../../../../resources/images/buildings/cityAlt1.png"; import { GameView, UnitView } from "../../../core/game/GameView"; @@ -90,6 +91,12 @@ export class StructureLayer implements Layer { territoryRadius: 6.525, borderType: UnitBorderType.Square, }); + this.loadIcon("reloadingSilo", { + icon: MissileSiloReloadingIcon, + borderRadius: 8.525, + territoryRadius: 6.525, + borderType: UnitBorderType.Square, + }); } private loadIcon(unitType: string, config: UnitRenderConfig) { @@ -215,12 +222,18 @@ export class StructureLayer implements Layer { const config = this.unitConfigs[unitType]; let icon: ImageData; - if (unitType == UnitType.SAMLauncher && unit.isSamCooldown()) { + if (unitType == UnitType.SAMLauncher && unit.isCooldown()) { icon = this.unitIcons.get("reloadingSam"); } else { icon = this.unitIcons.get(iconType); } + if (unitType == UnitType.MissileSilo && unit.isCooldown()) { + icon = this.unitIcons.get("reloadingSilo"); + } else { + icon = this.unitIcons.get(iconType); + } + if (!config || !icon) return; const drawFunction = this.getDrawFN(config.borderType); @@ -235,7 +248,13 @@ export class StructureLayer implements Layer { if (!unit.isActive()) return; let borderColor = this.theme.borderColor(unit.owner()); - if (unitType == UnitType.SAMLauncher && unit.isSamCooldown()) { + if (unitType == UnitType.SAMLauncher && unit.isCooldown()) { + borderColor = reloadingColor; + } else if (unit.type() == UnitType.Construction) { + borderColor = underConstructionColor; + } + + if (unitType == UnitType.MissileSilo && unit.isCooldown()) { borderColor = reloadingColor; } else if (unit.type() == UnitType.Construction) { borderColor = underConstructionColor; diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 71756b3aa..95b5d7554 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -47,7 +47,6 @@ export interface ServerConfig { export interface Config { samHittingChance(): number; - samCooldown(): Tick; spawnImmunityDuration(): Tick; serverConfig(): ServerConfig; gameConfig(): GameConfig; @@ -106,6 +105,8 @@ export interface Config { tradeShipGold(dist: number): Gold; tradeShipSpawnRate(numberOfPorts: number): number; defensePostRange(): number; + SAMCooldown(): number; + SiloCooldown(): number; defensePostDefenseBonus(): number; falloutDefenseModifier(percentOfFallout: number): number; difficultyModifier(difficulty: Difficulty): number; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 14b64c44e..03517c79b 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -93,10 +93,6 @@ export class DefaultConfig implements Config { return 0.8; } - samCooldown(): Tick { - return 100; - } - traitorDefenseDebuff(): number { return 0.8; } @@ -138,6 +134,12 @@ export class DefaultConfig implements Config { // So defense modifier is between [5, 2.5] return 5 - falloutRatio * 2; } + SAMCooldown(): number { + return 75; + } + SiloCooldown(): number { + return 75; + } defensePostRange(): number { return 30; diff --git a/src/core/execution/MissileSiloExecution.ts b/src/core/execution/MissileSiloExecution.ts index f9ae6f3d5..b88ea38bd 100644 --- a/src/core/execution/MissileSiloExecution.ts +++ b/src/core/execution/MissileSiloExecution.ts @@ -41,12 +41,21 @@ export class MissileSiloExecution implements Execution { this.active = false; return; } - this.silo = this.player.buildUnit(UnitType.MissileSilo, 0, this.tile); + this.silo = this.player.buildUnit(UnitType.MissileSilo, 0, this.tile, { + cooldownDuration: this.mg.config().SiloCooldown(), + }); if (this.player != this.silo.owner()) { this.player = this.silo.owner(); } } + + if ( + this.silo.isCooldown() && + this.silo.ticksLeftInCooldown(this.mg.config().SiloCooldown()) == 0 + ) { + this.silo.setCooldown(false); + } } isActive(): boolean { diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 3efc43525..bda5eca39 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -83,6 +83,12 @@ export class NukeExecution implements Execution { this.nuke.type() as NukeType, ); } + + // after sending an nuke set the missilesilo on cooldown + const silo = this.player + .units(UnitType.MissileSilo) + .find((silo) => silo.tile() === spawn); + silo.setCooldown(true); } // make the nuke unactive if it was intercepted diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts index 16b241bbd..a733e224b 100644 --- a/src/core/execution/SAMLauncherExecution.ts +++ b/src/core/execution/SAMLauncherExecution.ts @@ -16,15 +16,13 @@ import { PseudoRandom } from "../PseudoRandom"; export class SAMLauncherExecution implements Execution { private player: Player; private mg: Game; - private post: Unit; + private sam: Unit; private active: boolean = true; private target: Unit = null; private searchRangeRadius = 75; - private lastMissileAttack = 0; - private pseudoRandom: PseudoRandom; constructor( @@ -43,30 +41,32 @@ export class SAMLauncherExecution implements Execution { } tick(ticks: number): void { - if (this.post == null) { + if (this.sam == null) { const spawnTile = this.player.canBuild(UnitType.SAMLauncher, this.tile); if (spawnTile == false) { consolex.warn("cannot build SAM Launcher"); this.active = false; return; } - this.post = this.player.buildUnit(UnitType.SAMLauncher, 0, spawnTile); + this.sam = this.player.buildUnit(UnitType.SAMLauncher, 0, spawnTile, { + cooldownDuration: this.mg.config().SAMCooldown(), + }); } - if (!this.post.isActive()) { + if (!this.sam.isActive()) { this.active = false; return; } - if (this.player != this.post.owner()) { - this.player = this.post.owner(); + if (this.player != this.sam.owner()) { + this.player = this.sam.owner(); } if (!this.pseudoRandom) { - this.pseudoRandom = new PseudoRandom(this.post.id()); + this.pseudoRandom = new PseudoRandom(this.sam.id()); } const nukes = this.mg - .nearbyUnits(this.post.tile(), this.searchRangeRadius, [ + .nearbyUnits(this.sam.tile(), this.searchRangeRadius, [ UnitType.AtomBomb, UnitType.HydrogenBomb, ]) @@ -96,39 +96,30 @@ export class SAMLauncherExecution implements Execution { return distA - distB; })[0]?.unit ?? null; - const cooldown = - this.lastMissileAttack != 0 && - this.mg.ticks() - this.lastMissileAttack <= - this.mg.config().samCooldown(); - - if (this.post.isSamCooldown() && !cooldown) { - this.post.setSamCooldown(false); + if ( + this.sam.isCooldown() && + this.sam.ticksLeftInCooldown(this.mg.config().SAMCooldown()) == 0 + ) { + this.sam.setCooldown(false); } - if ( - this.target && - !this.post.isSamCooldown() && - !this.target.targetedBySAM() - ) { - this.lastMissileAttack = this.mg.ticks(); - this.post.setSamCooldown(true); + if (this.target && !this.sam.isCooldown() && !this.target.targetedBySAM()) { + this.sam.setCooldown(true); const random = this.pseudoRandom.next(); const hit = random < this.mg.config().samHittingChance(); - - this.lastMissileAttack = this.mg.ticks(); if (!hit) { this.mg.displayMessage( `Missile failed to intercept ${this.target.type()}`, MessageType.ERROR, - this.post.owner().id(), + this.sam.owner().id(), ); } else { this.target.setTargetedBySAM(true); this.mg.addExecution( new SAMMissileExecution( - this.post.tile(), - this.post.owner(), - this.post, + this.sam.tile(), + this.sam.owner(), + this.sam, this.target, ), ); diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 03c8a2c6c..38a827aef 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -228,6 +228,7 @@ export interface UnitSpecificInfos { dstPort?: Unit; // Only for trade ships detonationDst?: TileRef; // Only for nukes warshipTarget?: Unit; + cooldownDuration?: number; } export interface Unit { @@ -253,8 +254,9 @@ export interface Unit { setWarshipTarget(target: Unit): void; // warship only warshipTarget(): Unit; - setSamCooldown(isCoolingDown: boolean): void; // Only for sam - isSamCooldown(): boolean; + setCooldown(triggerCooldown: boolean): void; + ticksLeftInCooldown(cooldownDuration: number): Tick; + isCooldown(): boolean; setDstPort(dstPort: Unit): void; dstPort(): Unit; // Only for trade ships detonationDst(): TileRef; // Only for nukes diff --git a/src/core/game/GameUpdates.ts b/src/core/game/GameUpdates.ts index cea31f9f9..0dd2b111f 100644 --- a/src/core/game/GameUpdates.ts +++ b/src/core/game/GameUpdates.ts @@ -75,7 +75,7 @@ export interface UnitUpdate { warshipTargetId?: number; health?: number; constructionType?: UnitType; - isSamCooldown?: boolean; + ticksLeftInCooldown?: Tick; } export interface AttackUpdate { diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index 573653a1b..b9a3556c9 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -114,8 +114,11 @@ export class UnitView { } return this.data.warshipTargetId; } - isSamCooldown(): boolean { - return this.data.isSamCooldown; + ticksLeftInCooldown(): Tick { + return this.data.ticksLeftInCooldown; + } + isCooldown(): boolean { + return this.data.ticksLeftInCooldown > 0; } } diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 6bcb93076..03dab715b 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -741,8 +741,12 @@ export class PlayerImpl implements Player { } nukeSpawn(tile: TileRef): TileRef | false { + // only get missilesilos that are not on cooldown const spawns = this.units(UnitType.MissileSilo) .map((u) => u as Unit) + .filter((silo) => { + return !silo.isCooldown(); + }) .sort(distSortUnit(this.mg, tile)); if (spawns.length == 0) { return false; diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index cc7502623..9b5492ac9 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -1,4 +1,4 @@ -import { MessageType, UnitSpecificInfos } from "./Game"; +import { MessageType, Tick, UnitSpecificInfos } from "./Game"; import { UnitUpdate } from "./GameUpdates"; import { GameUpdateType } from "./GameUpdates"; import { simpleHash, toInt, within, withinInt } from "../Util"; @@ -19,10 +19,11 @@ export class UnitImpl implements Unit { private _constructionType: UnitType = undefined; - private _isSamCooldown: boolean = false; + private _cooldownTick: Tick | null = null; private _dstPort: Unit | null = null; // Only for trade ships private _detonationDst: TileRef | null = null; // Only for nukes private _warshipTarget: Unit | null = null; + private _cooldownDuration: number | null = null; constructor( private _type: UnitType, @@ -38,6 +39,7 @@ export class UnitImpl implements Unit { this._dstPort = unitsSpecificInfos.dstPort; this._detonationDst = unitsSpecificInfos.detonationDst; this._warshipTarget = unitsSpecificInfos.warshipTarget; + this._cooldownDuration = unitsSpecificInfos.cooldownDuration; } id() { @@ -61,7 +63,7 @@ export class UnitImpl implements Unit { dstPortId: dstPort ? dstPort.id() : null, warshipTargetId: warshipTarget ? warshipTarget.id() : null, detonationDst: this.detonationDst(), - isSamCooldown: this.isSamCooldown() ? this.isSamCooldown() : null, + ticksLeftInCooldown: this.ticksLeftInCooldown(this._cooldownDuration), }; } @@ -183,13 +185,26 @@ export class UnitImpl implements Unit { return this._dstPort; } - setSamCooldown(cooldown: boolean): void { - this._isSamCooldown = cooldown; - this.mg.addUpdate(this.toUpdate()); + // 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()); + } else { + this._cooldownTick = null; + this.mg.addUpdate(this.toUpdate()); + } } - isSamCooldown(): boolean { - return this._isSamCooldown; + ticksLeftInCooldown(cooldownDuration: number): Tick { + return Math.max( + 0, + cooldownDuration - (this.mg.ticks() - this._cooldownTick), + ); + } + + isCooldown(): boolean { + return this._cooldownTick ? true : false; } setDstPort(dstPort: Unit): void { diff --git a/tests/MissileSilo.test.ts b/tests/MissileSilo.test.ts new file mode 100644 index 000000000..707f295a1 --- /dev/null +++ b/tests/MissileSilo.test.ts @@ -0,0 +1,95 @@ +import { + Game, + Player, + PlayerInfo, + PlayerType, + UnitType, +} from "../src/core/game/Game"; +import { SpawnExecution } from "../src/core/execution/SpawnExecution"; +import { setup } from "./util/Setup"; +import { constructionExecution } from "./util/utils"; +import { NukeExecution } from "../src/core/execution/NukeExecution"; +import { TileRef } from "../src/core/game/GameMap"; + +let game: Game; +let attacker: Player; + +function attackerBuildsNuke( + source: TileRef, + target: TileRef, + initialize = true, +) { + game.addExecution( + new NukeExecution(UnitType.AtomBomb, attacker.id(), target, source), + ); + if (initialize) { + game.executeNextTick(); + game.executeNextTick(); + } +} + +describe("MissileSilo", () => { + beforeEach(async () => { + game = await setup("Plains", { infiniteGold: true, instantBuild: true }); + const attacker_info = new PlayerInfo( + "fr", + "attacker_id", + PlayerType.Human, + null, + "attacker_id", + ); + game.addPlayer(attacker_info, 1000); + + game.addExecution( + new SpawnExecution(game.player(attacker_info.id).info(), game.ref(1, 1)), + ); + + while (game.inSpawnPhase()) { + game.executeNextTick(); + } + + attacker = game.player("attacker_id"); + + constructionExecution(game, attacker.id(), 1, 1, UnitType.MissileSilo); + }); + + test("missilesilo should launch nuke", async () => { + attackerBuildsNuke(null, game.ref(7, 7)); + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1); + //because of initilization the nuke already moved so it should be at 204 when starting from 101 + expect(attacker.units(UnitType.AtomBomb)[0].tile()).toBe( + game.map().ref(5, 1), + ); + + for (let i = 0; i < 3; i++) { + game.executeNextTick(); + } + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0); + }); + + test("missilesilo should only launch one nuke at a time", async () => { + attackerBuildsNuke(null, game.ref(7, 7)); + attackerBuildsNuke(null, game.ref(7, 7)); + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1); + }); + + test("missilesilo should cooldown as long as configured", async () => { + expect(attacker.units(UnitType.MissileSilo)[0].isCooldown()).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 < 24; i++) { + game.executeNextTick(); + } + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0); + + for (let i = 0; i < game.config().SiloCooldown() - 25; i++) { + game.executeNextTick(); + expect(attacker.units(UnitType.MissileSilo)[0].isCooldown()).toBeTruthy(); + } + + game.executeNextTick(); + expect(attacker.units(UnitType.MissileSilo)[0].isCooldown()).toBeFalsy(); + }); +}); diff --git a/tests/SAM.test.ts b/tests/SAM.test.ts index 3e033c61c..5bbc058e6 100644 --- a/tests/SAM.test.ts +++ b/tests/SAM.test.ts @@ -91,7 +91,7 @@ describe("SAM", () => { test("sam should cooldown as long as configured", async () => { defenderBuildsSam(1, 1); - expect(defender.units(UnitType.SAMLauncher)[0].isSamCooldown()).toBe(false); + expect(defender.units(UnitType.SAMLauncher)[0].isCooldown()).toBeFalsy(); attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1)); expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1); @@ -99,15 +99,13 @@ describe("SAM", () => { game.executeNextTick(); expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0); - for (let i = 0; i < game.config().samCooldown() - 1; i++) { + for (let i = 0; i < game.config().SAMCooldown() - 2; i++) { game.executeNextTick(); - expect(defender.units(UnitType.SAMLauncher)[0].isSamCooldown()).toBe( - true, - ); + expect(defender.units(UnitType.SAMLauncher)[0].isCooldown()).toBeTruthy(); } game.executeNextTick(); - expect(defender.units(UnitType.SAMLauncher)[0].isSamCooldown()).toBe(false); + expect(defender.units(UnitType.SAMLauncher)[0].isCooldown()).toBeFalsy(); }); test("two sams should not target twice same nuke", async () => { @@ -125,8 +123,8 @@ describe("SAM", () => { const sams = defender.units(UnitType.SAMLauncher); // Only one sam must have shot expect( - (sams[0].isSamCooldown() && !sams[1].isSamCooldown()) || - (sams[1].isSamCooldown() && !sams[0].isSamCooldown()), + (sams[0].isCooldown() && !sams[1].isCooldown()) || + (sams[1].isCooldown() && !sams[0].isCooldown()), ).toBe(true); }); });