Improve "Better troop management for nations 🤖" (#4278)

## Description:

**Allow Hard/Impossible nations to retaliate and expand freely**

Previously, nations on Hard/Impossible difficulty could be stuck unable
to fight back if their `troopSendCap` or `isAttackTooWeak` checks
blocked them from sending enough troops. **@legan320** on the main
discord noticed it. Now:

- `troopSendCap` raises the cap to at least the total incoming attack
troops, so nations can match the force being used against them
- `isAttackTooWeak` bypasses the 20% minimum check entirely when under
attack
- `troopSendCap` no longer applies when attacking Terra Nullius, so
nations can always expand into unowned land with full troops

All checks still apply normally for unprovoked attacks against other
players.

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

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

FloPinguin
This commit is contained in:
FloPinguin
2026-06-15 03:53:01 +02:00
committed by GitHub
parent 5161d78d84
commit 094aa766ce
2 changed files with 63 additions and 11 deletions
+29 -11
View File
@@ -137,12 +137,12 @@ export class AiAttackBehavior {
}
}
// Hard & Impossible: don't drop below neighbor troop threshold
const troops = Math.min(this.player.troops() / 5, this.troopSendCap());
const owner = this.game.owner(dst);
const cap = owner.isPlayer() ? this.troopSendCap() : Infinity;
const troops = Math.min(this.player.troops() / 5, cap);
if (troops < 1) return;
// Hard & Impossible: don't attack if we'd send less than 20% of target's troops
const owner = this.game.owner(dst);
if (owner.isPlayer() && this.isAttackTooWeak(troops, owner)) {
return;
}
@@ -813,8 +813,7 @@ export class AiAttackBehavior {
if (this.game.hasFallout(tile)) continue;
if (!canBuildTransportShip(this.game, this.player, tile)) continue;
// Hard & Impossible: don't drop below neighbor troop threshold
const troops = Math.min(this.player.troops() / 5, this.troopSendCap());
const troops = this.player.troops() / 5;
if (troops < 1) return false;
this.game.addExecution(
@@ -859,7 +858,8 @@ export class AiAttackBehavior {
if (this.player.type() === PlayerType.Bot) return false;
if (this.game.config().gameConfig().gameMode === GameMode.Team)
return false;
// Nations under attack may retaliate freely
if (this.player.incomingAttacks().length > 0) return false;
const { difficulty } = this.game.config().gameConfig();
return (
(difficulty === Difficulty.Hard ||
@@ -875,6 +875,9 @@ export class AiAttackBehavior {
* Impossible: 90%). Allied players and bot neighbors are not considered
* threats. Bots and team games are entirely exempt. Returns Infinity when
* no cap applies.
*
* Nations under attack may retaliate with at least the total incoming
* attack troops, even if that exceeds the neighbor-based cap.
*/
private troopSendCap(): number {
if (this.player.type() === PlayerType.Bot) return Infinity;
@@ -905,10 +908,23 @@ export class AiAttackBehavior {
maxNeighborTroops = n.troops();
}
}
if (maxNeighborTroops === 0) return Infinity;
const minRetained = Math.ceil(maxNeighborTroops * retainFraction);
return Math.max(0, this.player.troops() - minRetained);
let cap: number;
if (maxNeighborTroops === 0) {
cap = Infinity;
} else {
const minRetained = Math.ceil(maxNeighborTroops * retainFraction);
cap = Math.max(0, this.player.troops() - minRetained);
}
// Nations under attack may retaliate with at least the incoming troops
const incoming = this.player.incomingAttacks();
if (incoming.length > 0) {
const totalIncoming = incoming.reduce((sum, a) => sum + a.troops(), 0);
cap = Math.max(cap, totalIncoming);
}
return cap;
}
private sendLandAttack(target: Player | TerraNullius): boolean {
@@ -937,8 +953,10 @@ export class AiAttackBehavior {
troops = this.player.troops() - targetTroops;
}
// Hard & Impossible: don't drop below neighbor troop threshold
troops = Math.min(troops, this.troopSendCap());
// Hard & Impossible: don't drop below neighbor troop threshold (players only)
if (target.isPlayer()) {
troops = Math.min(troops, this.troopSendCap());
}
if (troops < 1) {
return false;