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 <evanpelle@gmail.com>
This commit is contained in:
Readixyee
2025-03-29 23:19:07 +01:00
committed by GitHub
parent 72016f3dd4
commit 0891637eb2
14 changed files with 204 additions and 59 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

+21 -2
View File
@@ -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;
+2 -1
View File
@@ -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;
+6 -4
View File
@@ -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;
+10 -1
View File
@@ -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 {
+6
View File
@@ -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
+21 -30
View File
@@ -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,
),
);
+4 -2
View File
@@ -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
+1 -1
View File
@@ -75,7 +75,7 @@ export interface UnitUpdate {
warshipTargetId?: number;
health?: number;
constructionType?: UnitType;
isSamCooldown?: boolean;
ticksLeftInCooldown?: Tick;
}
export interface AttackUpdate {
+5 -2
View File
@@ -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;
}
}
+4
View File
@@ -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;
+23 -8
View File
@@ -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 {
+95
View File
@@ -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();
});
});
+6 -8
View File
@@ -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);
});
});