mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 07:50:45 +00:00
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>
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
import { Execution, Game, Gold, Player, PlayerID } from "../game/Game";
|
||||
import { toInt } from "../Util";
|
||||
import {
|
||||
Difficulty,
|
||||
Execution,
|
||||
Game,
|
||||
Gold,
|
||||
Player,
|
||||
PlayerID,
|
||||
} from "../game/Game";
|
||||
import { assertNever, toInt } from "../Util";
|
||||
|
||||
export class DonateGoldExecution implements Execution {
|
||||
private recipient: Player;
|
||||
private mg: Game;
|
||||
|
||||
private active = true;
|
||||
private gold: Gold;
|
||||
@@ -16,8 +24,12 @@ export class DonateGoldExecution implements Execution {
|
||||
}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
|
||||
if (!mg.hasPlayer(this.recipientID)) {
|
||||
console.warn(`DonateExecution recipient ${this.recipientID} not found`);
|
||||
console.warn(
|
||||
`DonateGoldExecution recipient ${this.recipientID} not found`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
@@ -32,7 +44,11 @@ export class DonateGoldExecution implements Execution {
|
||||
this.sender.canDonateGold(this.recipient) &&
|
||||
this.sender.donateGold(this.recipient, this.gold)
|
||||
) {
|
||||
this.recipient.updateRelation(this.sender, 50);
|
||||
// Give relation points based on how much gold was donated
|
||||
const relationUpdate = this.calculateRelationUpdate(this.gold, ticks);
|
||||
if (relationUpdate > 0) {
|
||||
this.recipient.updateRelation(this.sender, relationUpdate);
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`cannot send gold from ${this.sender.name()} to ${this.recipient.name()}`,
|
||||
@@ -41,6 +57,39 @@ export class DonateGoldExecution implements Execution {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
private getGoldChunkSize(): number {
|
||||
const { difficulty } = this.mg.config().gameConfig();
|
||||
switch (difficulty) {
|
||||
case Difficulty.Easy:
|
||||
return 2_500;
|
||||
case Difficulty.Medium:
|
||||
return 5_000;
|
||||
case Difficulty.Hard:
|
||||
return 12_500;
|
||||
case Difficulty.Impossible:
|
||||
return 25_000;
|
||||
default:
|
||||
assertNever(difficulty);
|
||||
}
|
||||
}
|
||||
|
||||
private calculateRelationUpdate(goldSent: Gold, ticks: number): number {
|
||||
const chunkSize = this.getGoldChunkSize();
|
||||
// For every 5 minutes that pass, multiply the chunk size to scale with game progression
|
||||
const chunkSizeMultiplier =
|
||||
ticks / (3000 + this.mg.config().numSpawnPhaseTurns());
|
||||
const adjustedChunkSize = BigInt(
|
||||
Math.round(chunkSize + chunkSize * chunkSizeMultiplier),
|
||||
);
|
||||
// Calculate how many complete chunks were donated
|
||||
const chunks = Number(goldSent / adjustedChunkSize);
|
||||
// Each chunk gives 5 relation points
|
||||
const relationUpdate = chunks * 5;
|
||||
// Cap at 100 relation points
|
||||
if (relationUpdate > 100) return 100;
|
||||
return relationUpdate;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Execution, Game, Player, PlayerID } from "../game/Game";
|
||||
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;
|
||||
|
||||
@@ -12,8 +16,13 @@ export class DonateTroopsExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
this.random = new PseudoRandom(mg.ticks());
|
||||
|
||||
if (!mg.hasPlayer(this.recipientID)) {
|
||||
console.warn(`DonateExecution recipient ${this.recipientID} not found`);
|
||||
console.warn(
|
||||
`DonateTroopExecution recipient ${this.recipientID} not found`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
@@ -27,11 +36,17 @@ export class DonateTroopsExecution implements Execution {
|
||||
|
||||
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)
|
||||
) {
|
||||
this.recipient.updateRelation(this.sender, 50);
|
||||
// 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}`,
|
||||
@@ -40,6 +55,40 @@ export class DonateTroopsExecution implements Execution {
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user