Files
OpenFrontIO/src/core/execution/DonateTroopExecution.ts
T
FloPinguin 058bb44273 Fix nation relation exploit 🔧 (#2523)
## Description:

Saw this in an Enzo video about beating impossible nations:

You can just donate 1% gold / 1% troops to a nation to get a friendly
relation with them.

This PR adds randomized minimum donation requirements based on the
difficulty.
Randomized in a way that there is a minimum someone has to donate to
surely get the relation improvement, but you can also gamble and send
less.

For troop donations, the minimum is calculated based on what percentage
of their troops the sender donated.

For gold donations, it's fixed values. We cannot use percentages here
because “having nearly no gold” is a usual case. Donating 100% of your
gold wouldn’t hurt if you just spent all your gold on buildings.

I tried to add tests for this but it's really horrible. Because the test
would have to wait until the relation update from the alliance accepting
is gone (we need to have an alliance to send stuff), has returned to
Neutral, and then changes back to Friendly after the donation.

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

---------

Co-authored-by: Evan <evanpelle@gmail.com>
2025-12-16 09:01:18 -08:00

100 lines
2.8 KiB
TypeScript

import { Difficulty, Execution, Game, Player, PlayerID } from "../game/Game";
import { PseudoRandom } from "../PseudoRandom";
import { assertNever } from "../Util";
export class DonateTroopsExecution implements Execution {
private recipient: Player;
private random: PseudoRandom;
private mg: Game;
private active = true;
constructor(
private sender: Player,
private recipientID: PlayerID,
private troops: number | null,
) {}
init(mg: Game, ticks: number): void {
this.mg = mg;
this.random = new PseudoRandom(mg.ticks());
if (!mg.hasPlayer(this.recipientID)) {
console.warn(
`DonateTroopExecution recipient ${this.recipientID} not found`,
);
this.active = false;
return;
}
this.recipient = mg.player(this.recipientID);
this.troops ??= mg.config().defaultDonationAmount(this.sender);
const maxDonation =
mg.config().maxTroops(this.recipient) - this.recipient.troops();
this.troops = Math.min(this.troops, maxDonation);
}
tick(ticks: number): void {
if (this.troops === null) throw new Error("not initialized");
const minTroops = this.getMinTroopsForRelationUpdate();
if (
this.sender.canDonateTroops(this.recipient) &&
this.sender.donateTroops(this.recipient, this.troops)
) {
// Prevent players from just buying a good relation by sending 1% troops. Instead, a minimum is needed, and it's random.
if (this.troops >= minTroops) {
this.recipient.updateRelation(this.sender, 50);
}
} else {
console.warn(
`cannot send troops from ${this.sender} to ${this.recipient}`,
);
}
this.active = false;
}
private getMinTroopsForRelationUpdate(): number {
const { difficulty } = this.mg.config().gameConfig();
const recipientMaxTroops = this.mg.config().maxTroops(this.recipient);
switch (difficulty) {
// ~7.7k - ~9.1k troops (for 100k troops)
case Difficulty.Easy:
return this.random.nextInt(
recipientMaxTroops / 13,
recipientMaxTroops / 11,
);
// ~9.1k - ~11.1k troops (for 100k troops)
case Difficulty.Medium:
return this.random.nextInt(
recipientMaxTroops / 11,
recipientMaxTroops / 9,
);
// ~11.1k - ~14.3k troops (for 100k troops)
case Difficulty.Hard:
return this.random.nextInt(
recipientMaxTroops / 9,
recipientMaxTroops / 7,
);
// ~14.3k - ~20k troops (for 100k troops)
case Difficulty.Impossible:
return this.random.nextInt(
recipientMaxTroops / 7,
recipientMaxTroops / 5,
);
default:
assertNever(difficulty);
}
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false;
}
}