mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 10:32:41 +00:00
Fix parallel bot boat attacks of nations 🐛 (#4297)
## Description: When a nation attacked multiple bots via boat attacks in parallel, each boat attack computed its troop allocation independently using `player.troops() / 5` without subtracting `botAttackTroopsSent`. The cumulative troop commitment could exceed the nation's actual troop count, and when the queued `AttackExecution`s ran `init()`, they drained the nation to zero. Planetary Realignment found this bug by accident, here Russia has only 39 troops: <img width="1189" height="654" alt="image" src="https://github.com/user-attachments/assets/07b85e00-6734-4ddd-a16e-fe53309e0ef8" /> The land attack path already handled this correctly. The bug in `sendBoatAttack` was introduced by #3786, which made nations see and attack enemies across rivers via boats, and changed `attackBots()` from `.neighbors()` to `.nearby()`. So the bug was on prod for the entirety of v31. This fix extracts the shared attack troop calculation (reserve, bot-aware allocation, troopSendCap, isAttackTooWeak, emoji) into a new `calculateAttackTroops` method, with a callback for the non-bot troop default (land: `player.troops() - targetTroops`, boat: `player.troops() / 5`). Bot targets in both paths now go through the same reserve-aware calculation. ## 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:
@@ -927,7 +927,10 @@ export class AiAttackBehavior {
|
|||||||
return cap;
|
return cap;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendLandAttack(target: Player | TerraNullius): boolean {
|
private calculateAttackTroops(
|
||||||
|
target: Player | TerraNullius,
|
||||||
|
nonBotTroops: (targetTroops: number) => number,
|
||||||
|
): number | null {
|
||||||
const maxTroops = this.game.config().maxTroops(this.player);
|
const maxTroops = this.game.config().maxTroops(this.player);
|
||||||
const botWithStructures =
|
const botWithStructures =
|
||||||
target.isPlayer() &&
|
target.isPlayer() &&
|
||||||
@@ -950,7 +953,7 @@ export class AiAttackBehavior {
|
|||||||
this.player.troops() - targetTroops - this.botAttackTroopsSent,
|
this.player.troops() - targetTroops - this.botAttackTroopsSent,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
troops = this.player.troops() - targetTroops;
|
troops = nonBotTroops(targetTroops);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hard & Impossible: don't drop below neighbor troop threshold (players only)
|
// Hard & Impossible: don't drop below neighbor troop threshold (players only)
|
||||||
@@ -959,12 +962,12 @@ export class AiAttackBehavior {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (troops < 1) {
|
if (troops < 1) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hard & Impossible: don't attack if we'd send less than 20% of target's troops
|
// Hard & Impossible: don't attack if we'd send less than 20% of target's troops
|
||||||
if (target.isPlayer() && this.isAttackTooWeak(troops, target)) {
|
if (target.isPlayer() && this.isAttackTooWeak(troops, target)) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target.isPlayer() && this.player.type() === PlayerType.Nation) {
|
if (target.isPlayer() && this.player.type() === PlayerType.Nation) {
|
||||||
@@ -972,6 +975,18 @@ export class AiAttackBehavior {
|
|||||||
this.emojiBehavior.maybeSendAttackEmoji(target);
|
this.emojiBehavior.maybeSendAttackEmoji(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return troops;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendLandAttack(target: Player | TerraNullius): boolean {
|
||||||
|
const troops = this.calculateAttackTroops(
|
||||||
|
target,
|
||||||
|
(targetTroops) => this.player.troops() - targetTroops,
|
||||||
|
);
|
||||||
|
if (troops === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
this.game.addExecution(
|
this.game.addExecution(
|
||||||
new AttackExecution(
|
new AttackExecution(
|
||||||
troops,
|
troops,
|
||||||
@@ -1000,30 +1015,14 @@ export class AiAttackBehavior {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let troops;
|
const troops = this.calculateAttackTroops(
|
||||||
if (target.type() === PlayerType.Bot) {
|
target,
|
||||||
troops = this.calculateBotAttackTroops(target, this.player.troops() / 5);
|
() => this.player.troops() / 5,
|
||||||
} else {
|
);
|
||||||
troops = this.player.troops() / 5;
|
if (troops === null) {
|
||||||
}
|
|
||||||
|
|
||||||
// Hard & Impossible: don't drop below neighbor troop threshold
|
|
||||||
troops = Math.min(troops, this.troopSendCap());
|
|
||||||
|
|
||||||
if (troops < 1) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hard & Impossible: don't attack if we'd send less than 20% of target's troops
|
|
||||||
if (this.isAttackTooWeak(troops, target)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.isPlayer() && this.player.type() === PlayerType.Nation) {
|
|
||||||
if (this.emojiBehavior === undefined) throw new Error("not initialized");
|
|
||||||
this.emojiBehavior.maybeSendAttackEmoji(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.game.addExecution(
|
this.game.addExecution(
|
||||||
new TransportShipExecution(this.player, closest.y, troops),
|
new TransportShipExecution(this.player, closest.y, troops),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user