Let nations send retaliation warships! (#2376)

## Description:

This PR is intended to increase singleplayer fun.
This is a follow-up to my previous PR #2161, which made the
"Hiding-Strategy on small islands" harder.

But its still possible. You can easily warship-infest the map and get
rich from the trade, even though you are playing on impossible
difficulty.
Also you can easily hinder the nations to, for example, get from the
left to the right side of the world map. So you are always in full
control of the game.

This PR makes nations send a warship if their troop transport boat got
destroyed. The warship will travel to the location where the boat got
destroyed.
Because the nations send more boats now (previous PR), this actual has
an impact and there is more action on the map now :)

The chance of retaliation is based on the game difficulty.

### COMPARISON

[Youtube Video](https://www.youtube.com/watch?v=F4_iP54LGNU) of me
playing the infestation-strat without this PR.

[Youtube Video](https://www.youtube.com/watch?v=VHesXJwPtcA) of me
playing the infestation-strat with this PR (Its sill possible but not
that easy, I gave up lol).

## Please complete the following:

- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

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

FloPinguin
This commit is contained in:
FloPinguin
2025-11-11 23:41:10 +01:00
committed by GitHub
parent d49566b73b
commit 0200df3ce1
3 changed files with 90 additions and 0 deletions
+80
View File
@@ -1,5 +1,6 @@
import {
Cell,
Difficulty,
Execution,
Game,
Gold,
@@ -46,6 +47,11 @@ export class FakeHumanExecution implements Execution {
private readonly lastMIRVSent: [Tick, TileRef][] = [];
private readonly embargoMalusApplied = new Set<PlayerID>();
// Track our transport ships we currently own
private trackedTransportShips: Set<Unit> = new Set();
// Track our trade ships we currently own
private trackedTradeShips: Set<Unit> = new Set();
/** MIRV Strategy Constants */
/** Ticks until MIRV can be attempted again */
@@ -133,6 +139,16 @@ export class FakeHumanExecution implements Execution {
}
tick(ticks: number) {
// Ship tracking
if (
this.player !== null &&
this.player.isAlive() &&
this.mg.config().gameConfig().difficulty !== Difficulty.Easy
) {
this.trackTransportShipsAndRetaliate();
this.trackTradeShipsAndRetaliate();
}
if (ticks % this.attackRate !== this.attackTick) {
return;
}
@@ -901,6 +917,70 @@ export class FakeHumanExecution implements Execution {
}
}
// Send out a warship if our transport ship got captured
private trackTransportShipsAndRetaliate(): void {
if (this.player === null) return;
// Add any currently owned transport ships to our tracking set
this.player
.units(UnitType.TransportShip)
.forEach((u) => this.trackedTransportShips.add(u));
// Iterate tracked transport ships; if it got destroyed by an enemy: retaliate
for (const ship of Array.from(this.trackedTransportShips)) {
if (!ship.isActive()) {
// Distinguish between arrival/retreat and enemy destruction
if (ship.wasDestroyedByEnemy()) {
this.maybeRetaliateWithWarship(ship.tile());
}
this.trackedTransportShips.delete(ship);
}
}
}
// Send out a warship if our trade ship got captured
private trackTradeShipsAndRetaliate(): void {
if (this.player === null) return;
// Add any currently owned trade ships to our tracking map
this.player
.units(UnitType.TradeShip)
.forEach((u) => this.trackedTradeShips.add(u));
// Iterate tracked trade ships; if we no longer own it, it was captured: retaliate
for (const ship of Array.from(this.trackedTradeShips)) {
if (!ship.isActive()) {
this.trackedTradeShips.delete(ship);
continue;
}
if (ship.owner().id() !== this.player.id()) {
// Ship was ours and is now owned by someone else -> captured
this.maybeRetaliateWithWarship(ship.tile());
this.trackedTradeShips.delete(ship);
}
}
}
private maybeRetaliateWithWarship(tile: TileRef): void {
if (this.player === null) return;
const { difficulty } = this.mg.config().gameConfig();
// In Easy never retaliate. In Medium retaliate with 15% chance. Hard with 50%, Impossible with 80%.
if (
(difficulty === Difficulty.Medium && this.random.nextInt(0, 100) < 15) ||
(difficulty === Difficulty.Hard && this.random.nextInt(0, 100) < 50) ||
(difficulty === Difficulty.Impossible && this.random.nextInt(0, 100) < 80)
) {
const canBuild = this.player.canBuild(UnitType.Warship, tile);
if (canBuild === false) {
return;
}
this.mg.addExecution(
new ConstructionExecution(this.player, UnitType.Warship, tile),
);
}
}
isActive(): boolean {
return this.active;
}
+1
View File
@@ -451,6 +451,7 @@ export interface Unit {
toUpdate(): UnitUpdate;
hasTrainStation(): boolean;
setTrainStation(trainStation: boolean): void;
wasDestroyedByEnemy(): boolean;
// Train
trainType(): TrainType | undefined;
+9
View File
@@ -24,6 +24,7 @@ export class UnitImpl implements Unit {
private _retreating: boolean = false;
private _targetedBySAM = false;
private _reachedTarget = false;
private _wasDestroyedByEnemy: boolean = false;
private _lastSetSafeFromPirates: number; // Only for trade ships
private _constructionType: UnitType | undefined;
private _lastOwner: PlayerImpl | null = null;
@@ -252,6 +253,10 @@ export class UnitImpl implements Unit {
if (!this.isActive()) {
throw new Error(`cannot delete ${this} not active`);
}
// Record whether this unit was destroyed by an enemy (vs. arrived / retreated)
this._wasDestroyedByEnemy = destroyer !== undefined;
this._owner._units = this._owner._units.filter((b) => b !== this);
this._active = false;
this.mg.addUpdate(this.toUpdate());
@@ -291,6 +296,10 @@ export class UnitImpl implements Unit {
return this._active;
}
wasDestroyedByEnemy(): boolean {
return this._wasDestroyedByEnemy;
}
retreating(): boolean {
return this._retreating;
}