mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 10:00:44 +00:00
sam do not target twice same nuke (#270)
This commit is contained in:
@@ -46,6 +46,8 @@ export interface ServerConfig {
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
samHittingChance(): number;
|
||||
samCooldown(): Tick;
|
||||
spawnImmunityDuration(): Tick;
|
||||
serverConfig(): ServerConfig;
|
||||
gameConfig(): GameConfig;
|
||||
|
||||
@@ -89,6 +89,14 @@ export class DefaultConfig implements Config {
|
||||
private _userSettings: UserSettings,
|
||||
) {}
|
||||
|
||||
samHittingChance(): number {
|
||||
return 0.8;
|
||||
}
|
||||
|
||||
samCooldown(): Tick {
|
||||
return 100;
|
||||
}
|
||||
|
||||
traitorDefenseDebuff(): number {
|
||||
return 0.8;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export class ConstructionExecution implements Execution {
|
||||
}
|
||||
const spawnTile = this.player.canBuild(this.constructionType, this.tile);
|
||||
if (spawnTile == false) {
|
||||
consolex.warn(`cannot build ${UnitType.Construction}`);
|
||||
consolex.warn(`cannot build ${this.constructionType}`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ export class NukeExecution implements Execution {
|
||||
|
||||
// make the nuke unactive if it was intercepted
|
||||
if (!this.nuke.isActive()) {
|
||||
consolex.warn(`Nuke destroyed before reaching target`);
|
||||
consolex.log(`Nuke destroyed before reaching target`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Unit,
|
||||
PlayerID,
|
||||
UnitType,
|
||||
MessageType,
|
||||
} from "../game/Game";
|
||||
import { manhattanDistFN, TileRef } from "../game/GameMap";
|
||||
import { SAMMissileExecution } from "./SAMMissileExecution";
|
||||
@@ -22,7 +23,6 @@ export class SAMLauncherExecution implements Execution {
|
||||
|
||||
private searchRangeRadius = 75;
|
||||
|
||||
private missileAttackRate = 75; // 7.5 seconds
|
||||
private lastMissileAttack = 0;
|
||||
|
||||
private pseudoRandom: PseudoRandom;
|
||||
@@ -100,22 +100,38 @@ export class SAMLauncherExecution implements Execution {
|
||||
|
||||
const cooldown =
|
||||
this.lastMissileAttack != 0 &&
|
||||
this.mg.ticks() - this.lastMissileAttack <= this.missileAttackRate;
|
||||
if (this.post.isSamCooldown() != cooldown) {
|
||||
this.post.setSamCooldown(cooldown);
|
||||
this.mg.ticks() - this.lastMissileAttack <=
|
||||
this.mg.config().samCooldown();
|
||||
|
||||
if (this.post.isSamCooldown() && !cooldown) {
|
||||
this.post.setSamCooldown(false);
|
||||
}
|
||||
|
||||
if (this.target != null) {
|
||||
if (!this.post.isSamCooldown()) {
|
||||
this.lastMissileAttack = this.mg.ticks();
|
||||
if (
|
||||
this.target &&
|
||||
!this.post.isSamCooldown() &&
|
||||
!this.target.targetedBySAM()
|
||||
) {
|
||||
this.lastMissileAttack = this.mg.ticks();
|
||||
this.post.setSamCooldown(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(),
|
||||
);
|
||||
} else {
|
||||
this.target.setTargetedBySAM(true);
|
||||
this.mg.addExecution(
|
||||
new SAMMissileExecution(
|
||||
this.post.tile(),
|
||||
this.post.owner(),
|
||||
this.post,
|
||||
this.target,
|
||||
this.mg,
|
||||
this.pseudoRandom.next(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,20 +15,19 @@ export class SAMMissileExecution implements Execution {
|
||||
private active = true;
|
||||
private pathFinder: PathFinder;
|
||||
private SAMMissile: Unit;
|
||||
private mg: Game;
|
||||
|
||||
constructor(
|
||||
private spawn: TileRef,
|
||||
private _owner: Player,
|
||||
private ownerUnit: Unit,
|
||||
private target: Unit,
|
||||
private mg: Game,
|
||||
private pseudoRandom: number,
|
||||
private speed: number = 12,
|
||||
private hittingChance: number = 0.75,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.pathFinder = PathFinder.Mini(mg, 2000, true, 10);
|
||||
this.mg = mg;
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
@@ -63,22 +62,13 @@ export class SAMMissileExecution implements Execution {
|
||||
);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
this.mg.displayMessage(
|
||||
`Missile intercepted ${this.target.type()}`,
|
||||
MessageType.SUCCESS,
|
||||
this._owner.id(),
|
||||
);
|
||||
this.active = false;
|
||||
if (this.pseudoRandom < this.hittingChance) {
|
||||
this.target.delete();
|
||||
|
||||
this.mg.displayMessage(
|
||||
`Missile succesfully intercepted ${this.target.type()}`,
|
||||
MessageType.SUCCESS,
|
||||
this._owner.id(),
|
||||
);
|
||||
} else {
|
||||
this.mg.displayMessage(
|
||||
`Missile failed to target ${this.target.type()}`,
|
||||
MessageType.ERROR,
|
||||
this._owner.id(),
|
||||
);
|
||||
}
|
||||
this.target.delete();
|
||||
this.SAMMissile.delete(false);
|
||||
return;
|
||||
case PathFindResultType.NextTile:
|
||||
|
||||
@@ -244,6 +244,9 @@ export interface Unit {
|
||||
setMoveTarget(cell: TileRef): void;
|
||||
moveTarget(): TileRef | null;
|
||||
|
||||
setTargetedBySAM(targeted: boolean): void;
|
||||
targetedBySAM(): boolean;
|
||||
|
||||
// Mutations
|
||||
setTroops(troops: number): void;
|
||||
delete(displayerMessage?: boolean): void;
|
||||
|
||||
@@ -15,10 +15,11 @@ export class UnitImpl implements Unit {
|
||||
// Currently only warship use it
|
||||
private _target: Unit = null;
|
||||
private _moveTarget: TileRef = null;
|
||||
private _targetedBySAM = false;
|
||||
|
||||
private _constructionType: UnitType = undefined;
|
||||
|
||||
private _isSamCooldown: boolean;
|
||||
private _isSamCooldown: boolean = false;
|
||||
private _dstPort: Unit | null = null; // Only for trade ships
|
||||
private _detonationDst: TileRef | null = null; // Only for nukes
|
||||
private _warshipTarget: Unit | null = null;
|
||||
@@ -204,4 +205,12 @@ export class UnitImpl implements Unit {
|
||||
moveTarget(): TileRef | null {
|
||||
return this._moveTarget;
|
||||
}
|
||||
|
||||
setTargetedBySAM(targeted: boolean): void {
|
||||
this._targetedBySAM = targeted;
|
||||
}
|
||||
|
||||
targetedBySAM(): boolean {
|
||||
return this._targetedBySAM;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
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;
|
||||
let defender: 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();
|
||||
}
|
||||
}
|
||||
|
||||
function defenderBuildsSam(x: number, y: number) {
|
||||
constructionExecution(game, defender.id(), x, y, UnitType.SAMLauncher);
|
||||
}
|
||||
|
||||
describe("SAM", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup("Plains", { infiniteGold: true, instantBuild: true });
|
||||
const defender_info = new PlayerInfo(
|
||||
"us",
|
||||
"defender_id",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"defender_id",
|
||||
);
|
||||
const attacker_info = new PlayerInfo(
|
||||
"fr",
|
||||
"attacker_id",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"attacker_id",
|
||||
);
|
||||
game.addPlayer(defender_info, 1000);
|
||||
game.addPlayer(attacker_info, 1000);
|
||||
|
||||
game.addExecution(
|
||||
new SpawnExecution(game.player(defender_info.id).info(), game.ref(1, 1)),
|
||||
new SpawnExecution(game.player(attacker_info.id).info(), game.ref(7, 7)),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
defender = game.player("defender_id");
|
||||
attacker = game.player("attacker_id");
|
||||
|
||||
constructionExecution(game, attacker.id(), 7, 7, UnitType.MissileSilo);
|
||||
});
|
||||
|
||||
test("one sam should take down one nuke", async () => {
|
||||
defenderBuildsSam(1, 1);
|
||||
attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1));
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
|
||||
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("sam should only get one nuke at a time", async () => {
|
||||
defenderBuildsSam(1, 1);
|
||||
attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1), false);
|
||||
attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1));
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(2);
|
||||
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("sam should cooldown as long as configured", async () => {
|
||||
defenderBuildsSam(1, 1);
|
||||
expect(defender.units(UnitType.SAMLauncher)[0].isSamCooldown()).toBe(false);
|
||||
attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1));
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
|
||||
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0);
|
||||
|
||||
for (let i = 0; i < game.config().samCooldown() - 1; i++) {
|
||||
game.executeNextTick();
|
||||
expect(defender.units(UnitType.SAMLauncher)[0].isSamCooldown()).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
game.executeNextTick();
|
||||
expect(defender.units(UnitType.SAMLauncher)[0].isSamCooldown()).toBe(false);
|
||||
});
|
||||
|
||||
test("two sams should not target twice same nuke", async () => {
|
||||
defenderBuildsSam(1, 1);
|
||||
defenderBuildsSam(1, 2);
|
||||
attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1));
|
||||
|
||||
expect(defender.units(UnitType.SAMLauncher)).toHaveLength(2);
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
|
||||
|
||||
game.executeNextTick();
|
||||
game.executeNextTick();
|
||||
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0);
|
||||
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()),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,7 @@
|
||||
import { DefaultConfig } from "../../src/core/configuration/DefaultConfig";
|
||||
|
||||
export class TestConfig extends DefaultConfig {}
|
||||
export class TestConfig extends DefaultConfig {
|
||||
samHittingChance(): number {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -15,9 +15,12 @@ export function constructionExecution(
|
||||
unit: UnitType,
|
||||
) {
|
||||
game.addExecution(new ConstructionExecution(playerID, game.ref(x, y), unit));
|
||||
// Init
|
||||
// Init exec
|
||||
game.executeNextTick();
|
||||
// Exec
|
||||
// Exec construction execution
|
||||
game.executeNextTick();
|
||||
// Add the execution related to the building
|
||||
game.executeNextTick();
|
||||
// First tick of the execution of the constructed structure/unit
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user