mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-23 02:05:40 +00:00
d321c08d92
## Description: - **Nations now specifically target AFK players, traitors, and players who are already being attacked (they gang up on them).** Depends on the difficulty. - Added ally assistance directly to the attack order (instead of calling it separately beforehand). - Added ally assistance to `findBestNukeTarget`. - Removed some checks from `findBestNukeTarget` (not necessary; better checks will follow in a dedicated nuking PR). - Relation updates on attack now depend on difficulty (makes Easy & Medium nations a bit easier). - On betrayal, every nation in the game previously received a –40 relation penalty toward the betrayer. That was too extreme, so now this only applies only to neighbors. - Nations send fewer alliance requests now; it felt like too many before. - In team games, nations may now reject alliance requests more often (depending on difficulty). - To ensure there are enough non-friendly players to stop the crown with nukes, nations may now reject alliance requests if the other player has too many alliances (on Hard and Impossible difficulty). - Rebalanced nation emoji usage a bit. - Nations may now send an emoji when they get MIRVed. ## 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: iamlewis <lewismmmm@gmail.com>
358 lines
11 KiB
TypeScript
358 lines
11 KiB
TypeScript
import {
|
|
AllPlayers,
|
|
Difficulty,
|
|
Game,
|
|
GameMode,
|
|
Player,
|
|
PlayerType,
|
|
Relation,
|
|
Team,
|
|
Tick,
|
|
} from "../../game/Game";
|
|
import { PseudoRandom } from "../../PseudoRandom";
|
|
import { flattenedEmojiTable } from "../../Util";
|
|
import { EmojiExecution } from "../EmojiExecution";
|
|
|
|
const emojiId = (e: (typeof flattenedEmojiTable)[number]) =>
|
|
flattenedEmojiTable.indexOf(e);
|
|
export const EMOJI_ASSIST_ACCEPT = (["👍", "🤝", "🎯"] as const).map(emojiId);
|
|
export const EMOJI_ASSIST_RELATION_TOO_LOW = (["🥱", "🤦♂️"] as const).map(
|
|
emojiId,
|
|
);
|
|
export const EMOJI_ASSIST_TARGET_ME = (["🥺", "💀"] as const).map(emojiId);
|
|
export const EMOJI_ASSIST_TARGET_ALLY = (["🕊️", "👎"] as const).map(emojiId);
|
|
export const EMOJI_AGGRESSIVE_ATTACK = (["😈"] as const).map(emojiId);
|
|
export const EMOJI_ATTACK = (["😡"] as const).map(emojiId);
|
|
export const EMOJI_WARSHIP_RETALIATION = (["⛵"] as const).map(emojiId);
|
|
export const EMOJI_NUKE = (["☢️", "💥"] as const).map(emojiId);
|
|
export const EMOJI_GOT_INSULTED = (["🖕", "😡", "🤡", "😞", "😭"] as const).map(
|
|
emojiId,
|
|
);
|
|
export const EMOJI_LOVE = (["❤️", "😊", "🥰"] as const).map(emojiId);
|
|
export const EMOJI_CONFUSED = (["❓", "🤡"] as const).map(emojiId);
|
|
export const EMOJI_BRAG = (["👑", "🥇", "💪"] as const).map(emojiId);
|
|
export const EMOJI_CHARM_ALLIES = (["🤝", "😇", "💪"] as const).map(emojiId);
|
|
export const EMOJI_CLOWN = (["🤡", "🤦♂️"] as const).map(emojiId);
|
|
export const EMOJI_RAT = (["🐀"] as const).map(emojiId);
|
|
export const EMOJI_OVERWHELMED = (
|
|
["💀", "🆘", "😱", "🥺", "😭", "😞", "🫡", "👋"] as const
|
|
).map(emojiId);
|
|
export const EMOJI_CONGRATULATE = (["👏"] as const).map(emojiId);
|
|
export const EMOJI_SCARED_OF_THREAT = (["🙏", "🥺"] as const).map(emojiId);
|
|
export const EMOJI_BORED = (["🥱"] as const).map(emojiId);
|
|
export const EMOJI_HANDSHAKE = (["🤝"] as const).map(emojiId);
|
|
export const EMOJI_DONATION_OK = (["👍"] as const).map(emojiId);
|
|
export const EMOJI_DONATION_TOO_SMALL = (["❓", "🥱"] as const).map(emojiId);
|
|
|
|
export class NationEmojiBehavior {
|
|
private readonly lastEmojiSent = new Map<Player, Tick>();
|
|
private hasSentWinnerClap = false;
|
|
|
|
constructor(
|
|
private random: PseudoRandom,
|
|
private game: Game,
|
|
private player: Player,
|
|
) {}
|
|
|
|
maybeSendCasualEmoji() {
|
|
this.checkOverwhelmedByAttacks();
|
|
this.checkVerySmallAttack();
|
|
this.congratulateWinner();
|
|
this.brag();
|
|
this.charmAllies();
|
|
this.annoyTraitors();
|
|
this.findRat();
|
|
}
|
|
|
|
private checkOverwhelmedByAttacks(): void {
|
|
if (!this.random.chance(16)) return;
|
|
|
|
const incomingAttacks = this.player.incomingAttacks();
|
|
if (incomingAttacks.length === 0) return;
|
|
|
|
const incomingTroops = incomingAttacks.reduce(
|
|
(sum, attack) => sum + attack.troops(),
|
|
0,
|
|
);
|
|
const ourTroops = this.player.troops();
|
|
|
|
// If incoming troops are at least 3x our troops, we're overwhelmed
|
|
if (incomingTroops >= ourTroops * 3) {
|
|
this.sendEmoji(AllPlayers, EMOJI_OVERWHELMED);
|
|
}
|
|
}
|
|
|
|
private checkVerySmallAttack(): void {
|
|
if (!this.random.chance(8)) return;
|
|
|
|
const incomingAttacks = this.player.incomingAttacks();
|
|
if (incomingAttacks.length === 0) return;
|
|
|
|
const ourTroops = this.player.troops();
|
|
if (ourTroops <= 0) return;
|
|
|
|
// Find attacks from humans that are very small (less than 10% of our troops)
|
|
for (const attack of incomingAttacks) {
|
|
const attacker = attack.attacker();
|
|
if (attacker.type() !== PlayerType.Human) continue;
|
|
|
|
if (attack.troops() < ourTroops * 0.1) {
|
|
this.maybeSendEmoji(
|
|
attacker,
|
|
this.random.chance(2) ? EMOJI_CONFUSED : EMOJI_BORED,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if game is over - send congratulations
|
|
private congratulateWinner(): void {
|
|
if (this.hasSentWinnerClap) return;
|
|
|
|
const percentToWin = this.game.config().percentageTilesOwnedToWin();
|
|
const numTilesWithoutFallout =
|
|
this.game.numLandTiles() - this.game.numTilesWithFallout();
|
|
const isTeamGame =
|
|
this.game.config().gameConfig().gameMode === GameMode.Team;
|
|
|
|
if (isTeamGame) {
|
|
// Team game: all nations congratulate if another team won
|
|
const teamToTiles = new Map<Team, number>();
|
|
for (const player of this.game.players()) {
|
|
const team = player.team();
|
|
if (team === null) continue;
|
|
teamToTiles.set(
|
|
team,
|
|
(teamToTiles.get(team) ?? 0) + player.numTilesOwned(),
|
|
);
|
|
}
|
|
|
|
const sorted = Array.from(teamToTiles.entries()).sort(
|
|
(a, b) => b[1] - a[1],
|
|
);
|
|
if (sorted.length === 0) return;
|
|
|
|
const [winningTeam, winningTiles] = sorted[0];
|
|
const winningPercent = (winningTiles / numTilesWithoutFallout) * 100;
|
|
if (winningPercent < percentToWin) return;
|
|
|
|
// Don't congratulate if it's our own team
|
|
if (winningTeam === this.player.team()) return;
|
|
|
|
this.hasSentWinnerClap = true;
|
|
this.sendEmoji(AllPlayers, EMOJI_CONGRATULATE);
|
|
} else {
|
|
// FFA game: The largest nation congratulates if a human player won
|
|
const sorted = this.game
|
|
.players()
|
|
.sort((a, b) => b.numTilesOwned() - a.numTilesOwned());
|
|
|
|
if (sorted.length === 0) return;
|
|
|
|
const firstPlace = sorted[0];
|
|
|
|
// Check if first place has won (crossed the win threshold)
|
|
const firstPlacePercent =
|
|
(firstPlace.numTilesOwned() / numTilesWithoutFallout) * 100;
|
|
if (firstPlacePercent < percentToWin) return;
|
|
|
|
// Only send if first place is a human
|
|
if (firstPlace.type() !== PlayerType.Human) return;
|
|
|
|
// Only the largest nation sends the congratulation
|
|
const largestNation = this.game
|
|
.players()
|
|
.filter((p) => p.type() === PlayerType.Nation)
|
|
.sort((a, b) => b.numTilesOwned() - a.numTilesOwned())[0];
|
|
if (largestNation !== this.player) return;
|
|
|
|
this.hasSentWinnerClap = true;
|
|
this.sendEmoji(firstPlace, EMOJI_CONGRATULATE);
|
|
}
|
|
}
|
|
|
|
// Brag with our crown
|
|
private brag(): void {
|
|
if (!this.random.chance(300)) return;
|
|
|
|
const sorted = this.game
|
|
.players()
|
|
.sort((a, b) => b.numTilesOwned() - a.numTilesOwned());
|
|
|
|
if (sorted.length === 0 || sorted[0] !== this.player) return;
|
|
|
|
this.sendEmoji(AllPlayers, EMOJI_BRAG);
|
|
}
|
|
|
|
private charmAllies(): void {
|
|
if (!this.random.chance(250)) return;
|
|
|
|
const humanAllies = this.player
|
|
.allies()
|
|
.filter((p) => p.type() === PlayerType.Human);
|
|
if (humanAllies.length === 0) return;
|
|
|
|
const ally = this.random.randElement(humanAllies);
|
|
const emojiList = this.random.chance(3) ? EMOJI_LOVE : EMOJI_CHARM_ALLIES;
|
|
this.sendEmoji(ally, emojiList);
|
|
}
|
|
|
|
private annoyTraitors(): void {
|
|
if (!this.random.chance(40)) return;
|
|
|
|
const traitors = this.game
|
|
.players()
|
|
.filter(
|
|
(p) =>
|
|
p.type() === PlayerType.Human &&
|
|
!p.isFriendly(this.player) &&
|
|
p.isTraitor(),
|
|
);
|
|
|
|
if (traitors.length === 0) return;
|
|
|
|
const traitor = this.random.randElement(traitors);
|
|
this.sendEmoji(traitor, EMOJI_CLOWN);
|
|
}
|
|
|
|
private findRat(): void {
|
|
if (this.game.ticks() < 6000) return; // Ignore first 10 minutes (everybody is small in the early game)
|
|
if (!this.random.chance(10000)) return;
|
|
|
|
const totalLand = this.game.numLandTiles();
|
|
const threshold = totalLand * 0.01; // 1% of land
|
|
|
|
const smallPlayers = this.game
|
|
.players()
|
|
.filter(
|
|
(p) =>
|
|
p.type() === PlayerType.Human &&
|
|
p.numTilesOwned() < threshold &&
|
|
p.numTilesOwned() > 0,
|
|
);
|
|
|
|
if (smallPlayers.length === 0) return;
|
|
|
|
const smallPlayer = this.random.randElement(smallPlayers);
|
|
this.sendEmoji(smallPlayer, EMOJI_RAT);
|
|
}
|
|
|
|
maybeSendEmoji(
|
|
otherPlayer: Player | typeof AllPlayers,
|
|
emojisList: number[],
|
|
) {
|
|
if (!this.shouldSendEmoji(otherPlayer)) return;
|
|
|
|
return this.sendEmoji(otherPlayer, emojisList);
|
|
}
|
|
|
|
maybeSendAttackEmoji(otherPlayer: Player) {
|
|
if (!this.shouldSendEmoji(otherPlayer)) return;
|
|
|
|
// If we have a good relation to the other player, we are probably attacking first (aggressive)
|
|
if (this.player.relation(otherPlayer) >= Relation.Neutral) {
|
|
if (!this.random.chance(2)) return;
|
|
this.sendEmoji(otherPlayer, EMOJI_AGGRESSIVE_ATTACK);
|
|
return;
|
|
}
|
|
|
|
// We are probably retaliating
|
|
if (!this.random.chance(4)) return;
|
|
this.sendEmoji(otherPlayer, EMOJI_ATTACK);
|
|
}
|
|
|
|
sendEmoji(otherPlayer: Player | typeof AllPlayers, emojisList: number[]) {
|
|
if (!this.shouldSendEmoji(otherPlayer, false)) return;
|
|
|
|
this.game.addExecution(
|
|
new EmojiExecution(
|
|
this.player,
|
|
otherPlayer === AllPlayers ? AllPlayers : otherPlayer.id(),
|
|
this.random.randElement(emojisList),
|
|
),
|
|
);
|
|
}
|
|
|
|
private shouldSendEmoji(
|
|
otherPlayer: Player | typeof AllPlayers,
|
|
limitEmojisByTime: boolean = true,
|
|
): boolean {
|
|
if (otherPlayer === AllPlayers) return true;
|
|
if (this.player.type() === PlayerType.Bot) return false;
|
|
if (otherPlayer.type() !== PlayerType.Human) return false;
|
|
|
|
if (limitEmojisByTime) {
|
|
const lastSent = this.lastEmojiSent.get(otherPlayer) ?? -300;
|
|
if (this.game.ticks() - lastSent <= 300) return false;
|
|
this.lastEmojiSent.set(otherPlayer, this.game.ticks());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
export function respondToEmoji(
|
|
game: Game,
|
|
random: PseudoRandom,
|
|
sender: Player,
|
|
recipient: Player | typeof AllPlayers,
|
|
emojiString: string,
|
|
): void {
|
|
if (recipient === AllPlayers || recipient.type() !== PlayerType.Nation) {
|
|
return;
|
|
}
|
|
|
|
if (emojiString === "🖕") {
|
|
recipient.updateRelation(sender, -100);
|
|
game.addExecution(
|
|
new EmojiExecution(
|
|
recipient,
|
|
sender.id(),
|
|
random.randElement(EMOJI_GOT_INSULTED),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (emojiString === "🤡") {
|
|
recipient.updateRelation(sender, -10);
|
|
game.addExecution(
|
|
new EmojiExecution(
|
|
recipient,
|
|
sender.id(),
|
|
random.randElement(EMOJI_CONFUSED),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (["🕊️", "🏳️", "❤️", "🥰", "👏"].includes(emojiString)) {
|
|
if (game.config().gameConfig().difficulty === Difficulty.Easy) {
|
|
recipient.updateRelation(sender, 15);
|
|
}
|
|
game.addExecution(
|
|
new EmojiExecution(
|
|
recipient,
|
|
sender.id(),
|
|
sender.relation(recipient) >= Relation.Neutral
|
|
? random.randElement(EMOJI_LOVE)
|
|
: random.randElement(EMOJI_CONFUSED),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
export function respondToMIRV(
|
|
game: Game,
|
|
random: PseudoRandom,
|
|
mirvTarget: Player,
|
|
) {
|
|
if (!random.chance(8)) return;
|
|
|
|
game.addExecution(
|
|
new EmojiExecution(
|
|
mirvTarget,
|
|
AllPlayers,
|
|
random.randElement(EMOJI_OVERWHELMED),
|
|
),
|
|
);
|
|
}
|