Nation ship improvements (#3724)

## Description:

Nations preform poorly on large water maps, these changes aims to
improve how they handle and react to water combat.

Adding the ability for hard and impossible nations to retaliate against
incoming transport ships.
Adding the ability for ships to be moved if the nation is not able to
build a new ship.

## 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:

babyboucher

---------

Co-authored-by: iamlewis <lewismmmm@gmail.com>
Co-authored-by: FloPinguin <25036848+FloPinguin@users.noreply.github.com>
This commit is contained in:
babyboucher
2026-04-29 13:18:00 -05:00
committed by GitHub
parent 3476bfc674
commit bd6c63b6ea
2 changed files with 113 additions and 3 deletions
+1
View File
@@ -93,6 +93,7 @@ export class NationExecution implements Execution {
this.player !== null &&
this.player.isAlive() &&
this.mg.config().gameConfig().difficulty !== Difficulty.Easy &&
this.player.unitsConstructed(UnitType.Port) &&
!this.mg.config().isUnitDisabled(UnitType.Warship)
) {
this.warshipBehavior.trackShipsAndRetaliate();
@@ -21,6 +21,10 @@ export class NationWarshipBehavior {
private trackedTransportShips: Set<Unit> = new Set();
// Track our trade ships we currently own
private trackedTradeShips: Set<Unit> = new Set();
// Track incoming transport ships
private trackedIncomingTransportShips: Set<Unit> = new Set();
// Track incoming transport ships we have dealt with
private dealtWithTransportShip: Set<Unit> = new Set();
constructor(
private random: PseudoRandom,
@@ -45,7 +49,7 @@ export class NationWarshipBehavior {
this.player.gold() > this.cost(UnitType.Warship)
) {
const port = this.random.randElement(ports);
const targetTile = this.warshipSpawnTile(port.tile());
const targetTile = this.warshipSpawnTile(port.tile(), 250);
if (targetTile === null) {
return false;
}
@@ -61,8 +65,7 @@ export class NationWarshipBehavior {
return false;
}
private warshipSpawnTile(portTile: TileRef): TileRef | null {
const radius = 250;
private warshipSpawnTile(portTile: TileRef, radius: number): TileRef | null {
for (let attempts = 0; attempts < 50; attempts++) {
const randX = this.random.nextInt(
this.game.x(portTile) - radius,
@@ -88,6 +91,7 @@ export class NationWarshipBehavior {
trackShipsAndRetaliate(): void {
this.trackTransportShipsAndRetaliate();
this.trackTradeShipsAndRetaliate();
this.trackIncomingTransportsAndRetaliate();
}
// Send out a warship if our transport ship got captured
@@ -137,6 +141,82 @@ export class NationWarshipBehavior {
}
}
private trackIncomingTransportsAndRetaliate(): void {
// Add any transports which are targeting us to our tracking map
this.game
.units(UnitType.TransportShip)
.filter((p) => {
const target = p.targetTile();
return (
target &&
p.isActive() &&
!p.retreating() &&
this.game.ownerID(target) === this.player?.smallID() &&
p.owner().smallID() !== this.player?.smallID()
);
})
.forEach((p) => this.trackedIncomingTransportShips.add(p));
for (const transport of Array.from(this.trackedIncomingTransportShips)) {
const target = transport.targetTile();
if (
!transport.isActive() ||
target === undefined ||
transport.retreating()
) {
this.trackedIncomingTransportShips.delete(transport);
this.dealtWithTransportShip.delete(transport);
continue;
}
// Transport has already been dealt with
if (this.dealtWithTransportShip.has(transport)) {
continue;
}
const distanceToTarget = this.game.manhattanDist(
transport.tile(),
target,
);
// Too close to deal with
if (distanceToTarget < 20) {
this.dealtWithTransportShip.add(transport);
continue;
}
// Possible dock snipe counter? Too niche?
if (!transport.owner().isAlliedWith(this.player)) {
if (
this.game.hasUnitNearby(
target,
90,
UnitType.Warship,
this.player.id(),
true,
) ||
this.player.units(UnitType.Warship).filter((p) => {
const patrolTile = p.patrolTile();
return (
patrolTile !== undefined &&
this.game.manhattanDist(target, patrolTile) < 90
);
}).length > 0
) {
this.dealtWithTransportShip.add(transport);
continue;
}
const oceanTiles = this.warshipSpawnTile(target, 30);
if (oceanTiles === null) continue;
this.maybeRetaliateWithWarship(
oceanTiles,
transport.owner(),
"transport",
);
this.dealtWithTransportShip.add(transport);
break;
}
}
}
private maybeRetaliateWithWarship(
tile: TileRef,
enemy: Player,
@@ -149,6 +229,7 @@ export class NationWarshipBehavior {
// Don't send too many warships
if (this.player.units(UnitType.Warship).length >= 10) {
this.maybeMoveWarship(tile);
return;
}
@@ -161,6 +242,7 @@ export class NationWarshipBehavior {
) {
const canBuild = this.player.canBuild(UnitType.Warship, tile);
if (canBuild === false) {
this.maybeMoveWarship(tile);
return;
}
this.game.addExecution(
@@ -171,6 +253,32 @@ export class NationWarshipBehavior {
}
}
private maybeMoveWarship(tile: TileRef): void {
// Make sure we are targeting water
if (this.game.isWater(tile)) {
const warship = this.player
.units(UnitType.Warship)
.filter((p) => {
const patrolTile = p.patrolTile();
return (
patrolTile !== undefined &&
// Dont send ships which are already traveling
this.game.manhattanDist(p.tile(), patrolTile) < 130
);
})
.sort((a, b) => {
// Sort by distance (closest first)
const distA = this.game.manhattanDist(a.tile(), tile);
const distB = this.game.manhattanDist(b.tile(), tile);
return distA - distB;
})[0];
if (warship) {
warship.setPatrolTile(tile);
}
}
}
// Prevent warship infestations: if current player is one of the 3 richest and an enemy has too many warships, send a counter-warship.
// What is a warship infestation? A player tries to dominate the entire ocean to block all trade and transport boats.
counterWarshipInfestation(): void {
@@ -335,6 +443,7 @@ export class NationWarshipBehavior {
target.warship.tile(),
);
if (canBuild === false) {
this.maybeMoveWarship(target.warship.tile());
return;
}