mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 10:00:44 +00:00
nukes now reduce attacking troops and transport ships (same as rest of pop) (#350)
Co-authored-by: Evan <evanpelle@gmail.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
PlayerInfo,
|
||||
TerraNullius,
|
||||
Tick,
|
||||
Unit,
|
||||
UnitInfo,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
@@ -45,6 +46,11 @@ export interface ServerConfig {
|
||||
r2SecretKey(): string;
|
||||
}
|
||||
|
||||
export interface NukeMagnitude {
|
||||
inner: number;
|
||||
outer: number;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
samHittingChance(): number;
|
||||
spawnImmunityDuration(): Tick;
|
||||
@@ -112,6 +118,9 @@ export interface Config {
|
||||
difficultyModifier(difficulty: Difficulty): number;
|
||||
// 0-1
|
||||
traitorDefenseDebuff(): number;
|
||||
nukeMagnitudes(unitType: UnitType): NukeMagnitude;
|
||||
defaultNukeSpeed(): number;
|
||||
nukeDeathFactor(humans: number, tilesOwned: number): number;
|
||||
}
|
||||
|
||||
export interface Theme {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
TerrainType,
|
||||
TerraNullius,
|
||||
Tick,
|
||||
Unit,
|
||||
UnitInfo,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
@@ -18,7 +19,7 @@ import { PlayerView } from "../game/GameView";
|
||||
import { UserSettings } from "../game/UserSettings";
|
||||
import { GameConfig, GameID } from "../Schemas";
|
||||
import { assertNever, simpleHash, within } from "../Util";
|
||||
import { Config, GameEnv, ServerConfig, Theme } from "./Config";
|
||||
import { Config, GameEnv, NukeMagnitude, ServerConfig, Theme } from "./Config";
|
||||
import { pastelTheme } from "./PastelTheme";
|
||||
import { pastelThemeDark } from "./PastelThemeDark";
|
||||
|
||||
@@ -592,4 +593,24 @@ export class DefaultConfig implements Config {
|
||||
}
|
||||
return adjustment;
|
||||
}
|
||||
|
||||
nukeMagnitudes(unitType: UnitType): NukeMagnitude {
|
||||
switch (unitType) {
|
||||
case UnitType.MIRVWarhead:
|
||||
return { inner: 25, outer: 30 };
|
||||
case UnitType.AtomBomb:
|
||||
return { inner: 12, outer: 30 };
|
||||
case UnitType.HydrogenBomb:
|
||||
return { inner: 80, outer: 100 };
|
||||
}
|
||||
}
|
||||
|
||||
defaultNukeSpeed(): number {
|
||||
return 4;
|
||||
}
|
||||
|
||||
// Humans can be population, soldiers attacking, soldiers in boat etc.
|
||||
nukeDeathFactor(humans: number, tilesOwned: number): number {
|
||||
return (5 * humans) / Math.max(1, tilesOwned);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export class NukeExecution implements Execution {
|
||||
private senderID: PlayerID,
|
||||
private dst: TileRef,
|
||||
private src?: TileRef,
|
||||
private speed: number = 4,
|
||||
private speed: number = -1,
|
||||
private waitTicks = 0,
|
||||
) {}
|
||||
|
||||
@@ -41,12 +41,55 @@ export class NukeExecution implements Execution {
|
||||
this.mg = mg;
|
||||
this.player = mg.player(this.senderID);
|
||||
this.random = new PseudoRandom(ticks);
|
||||
if (this.speed == -1) {
|
||||
this.speed = this.mg.config().defaultNukeSpeed();
|
||||
}
|
||||
}
|
||||
|
||||
public target(): Player | TerraNullius {
|
||||
return this.mg.owner(this.dst);
|
||||
}
|
||||
|
||||
private tilesToDestroy(): Set<TileRef> {
|
||||
const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type());
|
||||
const rand = new PseudoRandom(this.mg.ticks());
|
||||
return this.mg.bfs(this.dst, (_, n: TileRef) => {
|
||||
const d = this.mg.euclideanDist(this.dst, n);
|
||||
return (d <= magnitude.inner || rand.chance(2)) && d <= magnitude.outer;
|
||||
});
|
||||
}
|
||||
|
||||
private getAttackedTiles() {
|
||||
const toDestroy = this.tilesToDestroy();
|
||||
|
||||
const attacked = new Map<Player, number>();
|
||||
for (const tile of toDestroy) {
|
||||
const owner = this.mg.owner(tile);
|
||||
if (owner.isPlayer()) {
|
||||
const mp = this.mg.player(owner.id());
|
||||
const prev = attacked.get(mp) ?? 0;
|
||||
attacked.set(mp, prev + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return attacked;
|
||||
}
|
||||
|
||||
private breakAlliances() {
|
||||
for (const [other, tilesDestroyed] of this.getAttackedTiles()) {
|
||||
if (tilesDestroyed > 100 && this.nuke.type() != UnitType.MIRVWarhead) {
|
||||
// Mirv warheads shouldn't break alliances
|
||||
const alliance = this.player.allianceWith(other);
|
||||
if (alliance != null) {
|
||||
this.player.breakAlliance(alliance);
|
||||
}
|
||||
if (other != this.player) {
|
||||
other.updateRelation(this.player, -100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
if (this.nuke == null) {
|
||||
const spawn = this.src ?? this.player.canBuild(this.type, this.dst);
|
||||
@@ -91,6 +134,7 @@ export class NukeExecution implements Execution {
|
||||
if (silo) {
|
||||
silo.setCooldown(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// make the nuke unactive if it was intercepted
|
||||
@@ -100,6 +144,8 @@ export class NukeExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
this.breakAlliances();
|
||||
|
||||
if (this.waitTicks > 0) {
|
||||
this.waitTicks--;
|
||||
return;
|
||||
@@ -149,55 +195,38 @@ export class NukeExecution implements Execution {
|
||||
}
|
||||
|
||||
private detonate() {
|
||||
let magnitude;
|
||||
switch (this.type) {
|
||||
case UnitType.MIRVWarhead:
|
||||
magnitude = { inner: 25, outer: 30 };
|
||||
break;
|
||||
case UnitType.AtomBomb:
|
||||
magnitude = { inner: 12, outer: 30 };
|
||||
break;
|
||||
case UnitType.HydrogenBomb:
|
||||
magnitude = { inner: 80, outer: 100 };
|
||||
break;
|
||||
}
|
||||
const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type());
|
||||
const toDestroy = this.tilesToDestroy();
|
||||
|
||||
const rand = new PseudoRandom(this.mg.ticks());
|
||||
const toDestroy = this.mg.bfs(this.dst, (_, n: TileRef) => {
|
||||
const d = this.mg.euclideanDist(this.dst, n);
|
||||
return (d <= magnitude.inner || rand.chance(2)) && d <= magnitude.outer;
|
||||
});
|
||||
|
||||
const attacked = new Map<Player, number>();
|
||||
for (const tile of toDestroy) {
|
||||
const owner = this.mg.owner(tile);
|
||||
if (owner.isPlayer()) {
|
||||
const mp = this.mg.player(owner.id());
|
||||
mp.relinquish(tile);
|
||||
mp.removeTroops((5 * mp.population()) / mp.numTilesOwned());
|
||||
if (!attacked.has(mp)) {
|
||||
attacked.set(mp, 0);
|
||||
}
|
||||
const prev = attacked.get(mp);
|
||||
attacked.set(mp, prev + 1);
|
||||
mp.removeTroops(
|
||||
this.mg.config().nukeDeathFactor(mp.troops(), mp.numTilesOwned()),
|
||||
);
|
||||
mp.removeWorkers(
|
||||
this.mg.config().nukeDeathFactor(mp.workers(), mp.numTilesOwned()),
|
||||
);
|
||||
mp.outgoingAttacks().forEach((attack) => {
|
||||
const deaths = this.mg
|
||||
.config()
|
||||
.nukeDeathFactor(attack.troops(), mp.numTilesOwned());
|
||||
attack.setTroops(attack.troops() - deaths);
|
||||
});
|
||||
mp.units(UnitType.TransportShip).forEach((attack) => {
|
||||
const deaths = this.mg
|
||||
.config()
|
||||
.nukeDeathFactor(attack.troops(), mp.numTilesOwned());
|
||||
attack.setTroops(attack.troops() - deaths);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.mg.isLand(tile)) {
|
||||
this.mg.setFallout(tile, true);
|
||||
}
|
||||
}
|
||||
for (const [other, tilesDestroyed] of attacked) {
|
||||
if (tilesDestroyed > 100 && this.nuke.type() != UnitType.MIRVWarhead) {
|
||||
// Mirv warheads shouldn't break alliances
|
||||
const alliance = this.player.allianceWith(other);
|
||||
if (alliance != null) {
|
||||
this.player.breakAlliance(alliance);
|
||||
}
|
||||
if (other != this.player) {
|
||||
other.updateRelation(this.player, -100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const unit of this.mg.units()) {
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
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 { TransportShipExecution } from "../src/core/execution/TransportShipExecution";
|
||||
import { TileRef } from "../src/core/game/GameMap";
|
||||
import { AttackExecution } from "../src/core/execution/AttackExecution";
|
||||
import { TestConfig } from "./util/TestConfig";
|
||||
|
||||
let game: Game;
|
||||
let attacker: Player;
|
||||
let defender: Player;
|
||||
let defenderSpawn: TileRef;
|
||||
let attackerSpawn: TileRef;
|
||||
|
||||
function sendBoat(target: TileRef, troops: number) {
|
||||
game.addExecution(
|
||||
new TransportShipExecution(defender.id(), null, target, troops),
|
||||
);
|
||||
}
|
||||
|
||||
describe("Attack", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup("ocean_and_land", {
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
infiniteTroops: true,
|
||||
});
|
||||
const attackerInfo = new PlayerInfo(
|
||||
"us",
|
||||
"attacker dude",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"attacker_id",
|
||||
);
|
||||
game.addPlayer(attackerInfo, 1000);
|
||||
const defenderInfo = new PlayerInfo(
|
||||
"us",
|
||||
"defender dude",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"defender_id",
|
||||
);
|
||||
game.addPlayer(defenderInfo, 1000);
|
||||
|
||||
defenderSpawn = game.ref(0, 15);
|
||||
attackerSpawn = game.ref(0, 10);
|
||||
|
||||
game.addExecution(
|
||||
new SpawnExecution(game.player(attackerInfo.id).info(), attackerSpawn),
|
||||
new SpawnExecution(game.player(defenderInfo.id).info(), defenderSpawn),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
attacker = game.player(attackerInfo.id);
|
||||
defender = game.player(defenderInfo.id);
|
||||
|
||||
game.addExecution(new AttackExecution(100, defender.id(), null));
|
||||
game.executeNextTick();
|
||||
while (defender.outgoingAttacks().length > 0) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
(game.config() as TestConfig).setDefaultNukeSpeed(50);
|
||||
});
|
||||
|
||||
test("Nuke reduce attacking troop counts", async () => {
|
||||
// Not building exactly spawn to it's better protected from attacks (but still
|
||||
// on defender territory)
|
||||
constructionExecution(game, defender.id(), 1, 1, UnitType.MissileSilo);
|
||||
expect(defender.units(UnitType.MissileSilo)).toHaveLength(1);
|
||||
game.addExecution(new AttackExecution(100, attacker.id(), defender.id()));
|
||||
constructionExecution(game, defender.id(), 0, 15, UnitType.AtomBomb, 3);
|
||||
const nuke = defender.units(UnitType.AtomBomb)[0];
|
||||
expect(nuke.isActive()).toBe(true);
|
||||
|
||||
expect(attacker.outgoingAttacks()).toHaveLength(1);
|
||||
expect(attacker.outgoingAttacks()[0].troops()).toBe(98);
|
||||
|
||||
// Make the nuke go kaboom
|
||||
game.executeNextTick();
|
||||
expect(nuke.isActive()).toBe(false);
|
||||
expect(attacker.outgoingAttacks()[0].troops()).not.toBe(97);
|
||||
expect(attacker.outgoingAttacks()[0].troops()).toBeLessThan(90);
|
||||
});
|
||||
|
||||
test("Nuke reduce attacking boat troop count", async () => {
|
||||
constructionExecution(game, defender.id(), 1, 1, UnitType.MissileSilo);
|
||||
expect(defender.units(UnitType.MissileSilo)).toHaveLength(1);
|
||||
|
||||
sendBoat(game.ref(15, 8), 100);
|
||||
|
||||
constructionExecution(game, defender.id(), 0, 15, UnitType.AtomBomb, 3);
|
||||
const nuke = defender.units(UnitType.AtomBomb)[0];
|
||||
expect(nuke.isActive()).toBe(true);
|
||||
|
||||
const ship = defender.units(UnitType.TransportShip)[0];
|
||||
expect(ship.troops()).toBe(100);
|
||||
|
||||
game.executeNextTick();
|
||||
|
||||
expect(nuke.isActive()).toBe(false);
|
||||
expect(defender.units(UnitType.TransportShip)[0].troops()).toBeLessThan(90);
|
||||
});
|
||||
});
|
||||
@@ -56,12 +56,11 @@ describe("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),
|
||||
expect(attacker.units(UnitType.AtomBomb)[0].tile()).not.toBe(
|
||||
game.map().ref(7, 7),
|
||||
);
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0);
|
||||
@@ -79,12 +78,7 @@ describe("MissileSilo", () => {
|
||||
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++) {
|
||||
for (let i = 0; i < game.config().SiloCooldown() - 1; i++) {
|
||||
game.executeNextTick();
|
||||
expect(attacker.units(UnitType.MissileSilo)[0].isCooldown()).toBeTruthy();
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 120 B |
@@ -1,7 +1,17 @@
|
||||
import { NukeMagnitude } from "../../src/core/configuration/Config";
|
||||
import { DefaultConfig } from "../../src/core/configuration/DefaultConfig";
|
||||
import {
|
||||
Game,
|
||||
Player,
|
||||
TerraNullius,
|
||||
Tick,
|
||||
UnitType,
|
||||
} from "../../src/core/game/Game";
|
||||
import { TileRef } from "../../src/core/game/GameMap";
|
||||
|
||||
export class TestConfig extends DefaultConfig {
|
||||
_proximityBonusPortsNb: number = 0;
|
||||
private _proximityBonusPortsNb: number = 0;
|
||||
private _defaultNukeSpeed: number = 4;
|
||||
|
||||
samHittingChance(): number {
|
||||
return 1;
|
||||
@@ -19,4 +29,43 @@ export class TestConfig extends DefaultConfig {
|
||||
setProximityBonusPortsNb(nb: number): void {
|
||||
this._proximityBonusPortsNb = nb;
|
||||
}
|
||||
|
||||
nukeMagnitudes(_: UnitType): NukeMagnitude {
|
||||
return { inner: 1, outer: 1 };
|
||||
}
|
||||
|
||||
setDefaultNukeSpeed(speed: number): void {
|
||||
this._defaultNukeSpeed = speed;
|
||||
}
|
||||
|
||||
defaultNukeSpeed(): number {
|
||||
return this._defaultNukeSpeed;
|
||||
}
|
||||
|
||||
spawnImmunityDuration(): Tick {
|
||||
return 0;
|
||||
}
|
||||
|
||||
attackLogic(
|
||||
gm: Game,
|
||||
attackTroops: number,
|
||||
attacker: Player,
|
||||
defender: Player | TerraNullius,
|
||||
tileToConquer: TileRef,
|
||||
): {
|
||||
attackerTroopLoss: number;
|
||||
defenderTroopLoss: number;
|
||||
tilesPerTickUsed: number;
|
||||
} {
|
||||
return { attackerTroopLoss: 1, defenderTroopLoss: 1, tilesPerTickUsed: 1 };
|
||||
}
|
||||
|
||||
attackTilesPerTick(
|
||||
attackTroops: number,
|
||||
attacker: Player,
|
||||
defender: Player | TerraNullius,
|
||||
numAdjacentTilesWithEnemy: number,
|
||||
): number {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-7
@@ -13,14 +13,18 @@ export function constructionExecution(
|
||||
x: number,
|
||||
y: number,
|
||||
unit: UnitType,
|
||||
ticks = 4,
|
||||
) {
|
||||
game.addExecution(new ConstructionExecution(playerID, game.ref(x, y), unit));
|
||||
// Init exec
|
||||
game.executeNextTick();
|
||||
|
||||
// 4 ticks by default as it usually goes like this
|
||||
// Init of construction execution
|
||||
// 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();
|
||||
// Tick of construction execution which adds the execution related to the building/unit
|
||||
// First tick of the execution of the constructed building/unit
|
||||
// (sometimes step 3 and 4 are merged in one)
|
||||
|
||||
for (let i = 0; i < ticks; i++) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user