Rebalance HvN (#3433)

## Description:

For the next v30 fix version

<img width="868" height="364" alt="imaege"
src="https://github.com/user-attachments/assets/520a999c-67e7-4c57-8651-895ad9eeb73a"
/>

HvN balancing for the revamped difficulty steps of v30 sadly doesn't
really work out...
In medium difficulty games humans nearly always win (boring)
In hard difficulty games humans usually lose
It was intended differently...

So lets get rid of medium difficulty HvN, always use hard difficulty and
disable the donation-capability for public game nations.
That will tune the human winrate towards a middle ground at about 65% I
think. Which should be nice.
Easier than in v29 (was frustrating sometimes) but not as easy as it's
now.

We can only test this in prod lol

## 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
This commit is contained in:
FloPinguin
2026-03-16 05:28:40 +01:00
committed by evanpelle
parent 90822fc522
commit 71e5faf4ec
2 changed files with 24 additions and 32 deletions
@@ -2,6 +2,7 @@ import {
Difficulty,
Game,
GameMode,
GameType,
HumansVsNations,
Player,
PlayerID,
@@ -846,6 +847,11 @@ export class AiAttackBehavior {
return false;
}
// Don't donate in public games (To balance HvN)
if (this.game.config().gameConfig().gameType === GameType.Public) {
return false;
}
// Check if donating troops is allowed
if (this.game.config().donateTroops() === false) {
return false;
+18 -32
View File
@@ -125,9 +125,6 @@ const MUTUALLY_EXCLUSIVE_MODIFIERS: [ModifierKey, ModifierKey][] = [
["isHardNations", "startingGoldHigh"],
];
// Probability of hard nations modifier in HumansVsNations games.
const HARD_NATIONS_HVN_PROBABILITY = 0.2; // 20%
export class MapPlaylist {
private playlists: Record<PublicGameType, GameMapType[]> = {
ffa: [],
@@ -159,8 +156,8 @@ export class MapPlaylist {
isRandomSpawn = false;
}
// Hard nations modifier only applies when nations are present
if (mode === GameMode.Team && playerTeams !== HumansVsNations) {
// Hard nations modifier only applies when nations are present (not HvN, which is always hard)
if (mode === GameMode.Team) {
isHardNations = false;
}
@@ -204,7 +201,10 @@ export class MapPlaylist {
isAlliancesDisabled: false,
},
startingGold,
difficulty: isHardNations ? Difficulty.Hard : Difficulty.Medium,
difficulty:
isHardNations || playerTeams === HumansVsNations
? Difficulty.Hard
: Difficulty.Medium,
infiniteGold: false,
infiniteTroops: false,
maxTimerValue: undefined,
@@ -248,26 +248,15 @@ export class MapPlaylist {
excludedModifiers.push("isRandomSpawn");
}
// Hard nations: excluded for non-HvN team modes (no nations present).
// For HumansVsNations: rolled independently (not via pool).
// For FFA: stays in the pool for normal ticket-based selection.
let hardNationsFromIndependentRoll: boolean | undefined;
let poolCountReduction = 0;
if (mode === GameMode.Team && playerTeams !== HumansVsNations) {
excludedModifiers.push("isHardNations");
} else if (playerTeams === HumansVsNations) {
// Hard nations modifier only applies when nations are present (not HvN, which is always hard)
if (mode === GameMode.Team) {
excludedModifiers.push("isHardNations");
}
if (playerTeams === HumansVsNations) {
excludedModifiers.push("startingGoldHigh"); // Nations are disabled if that modifier is active
hardNationsFromIndependentRoll =
Math.random() < HARD_NATIONS_HVN_PROBABILITY;
poolCountReduction = hardNationsFromIndependentRoll ? 1 : 0;
}
const poolResult = this.getRandomSpecialGameModifiers(
excludedModifiers,
undefined,
poolCountReduction,
);
const poolResult = this.getRandomSpecialGameModifiers(excludedModifiers);
let {
isCrowded,
startingGold,
@@ -275,9 +264,8 @@ export class MapPlaylist {
isRandomSpawn,
goldMultiplier,
isAlliancesDisabled,
isHardNations,
} = poolResult;
let isHardNations =
hardNationsFromIndependentRoll ?? poolResult.isHardNations;
let crowdedMaxPlayers: number | undefined;
if (isCrowded) {
@@ -300,7 +288,6 @@ export class MapPlaylist {
const fallback = this.getRandomSpecialGameModifiers(
excludedModifiers,
1,
poolCountReduction,
);
({
isRandomSpawn,
@@ -309,8 +296,7 @@ export class MapPlaylist {
goldMultiplier,
isAlliancesDisabled,
} = fallback);
isHardNations =
hardNationsFromIndependentRoll ?? fallback.isHardNations;
({ isHardNations } = fallback);
}
}
}
@@ -347,7 +333,10 @@ export class MapPlaylist {
startingGold,
goldMultiplier,
disableAlliances: isAlliancesDisabled,
difficulty: isHardNations ? Difficulty.Hard : Difficulty.Medium,
difficulty:
isHardNations || playerTeams === HumansVsNations
? Difficulty.Hard
: Difficulty.Medium,
infiniteGold: false,
infiniteTroops: false,
maxTimerValue: undefined,
@@ -502,10 +491,7 @@ export class MapPlaylist {
isCompact: Math.random() < 0.05, // 5% chance
isCrowded: Math.random() < 0.05, // 5% chance
startingGold: Math.random() < 0.05 ? 5_000_000 : undefined, // 5% chance
isHardNations:
playerTeams === HumansVsNations
? Math.random() < HARD_NATIONS_HVN_PROBABILITY
: Math.random() < 0.025, // 2.5% chance
isHardNations: Math.random() < 0.025, // 2.5% chance
isAlliancesDisabled: false,
};
}