mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:30:45 +00:00
The clown is gone! 🤡 Nations send much better emojis now (#2696)
## Description: Previously, nations just spammed these two rather toxic emojis: 🤡😡 They now send fewer emojis while attacking, and the clown emoji is reserved for special cases. They got the ability to send emojis in much more cases: - Human didn't donate enough for relation update - Human did donate an ok amount - Human did donate a lot - Responding to emojis that they get sent from a human - Nuke sent - MIRV sent - Retaliation warship sent - Traitor tries to ally - Threat asks for / accepts an alliance request - Disliked human tries to ally - Friendly human tries to ally - They are getting attacked by very much troops - They are getting attacked by very little troops - Congratulating the winner - Bragging with their crown - Charming their allies - Clown-Emoting traitors - Easteregg: Sending a rat emoji to very small humans ## 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>
This commit is contained in:
@@ -6,14 +6,23 @@ import {
|
||||
Player,
|
||||
PlayerID,
|
||||
} from "../game/Game";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { assertNever, toInt } from "../Util";
|
||||
import { EmojiExecution } from "./EmojiExecution";
|
||||
import {
|
||||
EMOJI_DONATION_OK,
|
||||
EMOJI_DONATION_TOO_SMALL,
|
||||
EMOJI_LOVE,
|
||||
} from "./nation/NationEmojiBehavior";
|
||||
|
||||
export class DonateGoldExecution implements Execution {
|
||||
private recipient: Player;
|
||||
private gold: Gold;
|
||||
|
||||
private mg: Game;
|
||||
private random: PseudoRandom;
|
||||
|
||||
private active = true;
|
||||
private gold: Gold;
|
||||
|
||||
constructor(
|
||||
private sender: Player,
|
||||
@@ -25,6 +34,7 @@ export class DonateGoldExecution implements Execution {
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
this.random = new PseudoRandom(mg.ticks());
|
||||
|
||||
if (!mg.hasPlayer(this.recipientID)) {
|
||||
console.warn(
|
||||
@@ -49,6 +59,22 @@ export class DonateGoldExecution implements Execution {
|
||||
if (relationUpdate > 0) {
|
||||
this.recipient.updateRelation(this.sender, relationUpdate);
|
||||
}
|
||||
|
||||
// Select emoji based on donation value
|
||||
const emoji =
|
||||
relationUpdate >= 50
|
||||
? EMOJI_LOVE
|
||||
: relationUpdate > 0
|
||||
? EMOJI_DONATION_OK
|
||||
: EMOJI_DONATION_TOO_SMALL;
|
||||
|
||||
this.mg.addExecution(
|
||||
new EmojiExecution(
|
||||
this.recipient,
|
||||
this.sender.id(),
|
||||
this.random.randElement(emoji),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
`cannot send gold from ${this.sender.name()} to ${this.recipient.name()}`,
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { Difficulty, Execution, Game, Player, PlayerID } from "../game/Game";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { assertNever } from "../Util";
|
||||
import { EmojiExecution } from "./EmojiExecution";
|
||||
import {
|
||||
EMOJI_DONATION_TOO_SMALL,
|
||||
EMOJI_LOVE,
|
||||
} from "./nation/NationEmojiBehavior";
|
||||
|
||||
export class DonateTroopsExecution implements Execution {
|
||||
private recipient: Player;
|
||||
|
||||
private random: PseudoRandom;
|
||||
private mg: Game;
|
||||
|
||||
@@ -47,6 +53,16 @@ export class DonateTroopsExecution implements Execution {
|
||||
if (this.troops >= minTroops) {
|
||||
this.recipient.updateRelation(this.sender, 50);
|
||||
}
|
||||
|
||||
this.mg.addExecution(
|
||||
new EmojiExecution(
|
||||
this.recipient,
|
||||
this.sender.id(),
|
||||
this.random.randElement(
|
||||
this.troops >= minTroops ? EMOJI_LOVE : EMOJI_DONATION_TOO_SMALL,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
`cannot send troops from ${this.sender} to ${this.recipient}`,
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import {
|
||||
AllPlayers,
|
||||
Execution,
|
||||
Game,
|
||||
Player,
|
||||
PlayerID,
|
||||
PlayerType,
|
||||
} from "../game/Game";
|
||||
import { AllPlayers, Execution, Game, Player, PlayerID } from "../game/Game";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { flattenedEmojiTable } from "../Util";
|
||||
import { respondToEmoji } from "./nation/NationEmojiBehavior";
|
||||
|
||||
export class EmojiExecution implements Execution {
|
||||
private recipient: Player | typeof AllPlayers;
|
||||
|
||||
private mg: Game;
|
||||
private random: PseudoRandom;
|
||||
|
||||
private active = true;
|
||||
|
||||
constructor(
|
||||
@@ -20,6 +18,9 @@ export class EmojiExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
this.random = new PseudoRandom(mg.ticks());
|
||||
|
||||
if (this.recipientID !== AllPlayers && !mg.hasPlayer(this.recipientID)) {
|
||||
console.warn(`EmojiExecution: recipient ${this.recipientID} not found`);
|
||||
this.active = false;
|
||||
@@ -40,13 +41,13 @@ export class EmojiExecution implements Execution {
|
||||
);
|
||||
} else if (this.requestor.canSendEmoji(this.recipient)) {
|
||||
this.requestor.sendEmoji(this.recipient, emojiString);
|
||||
if (
|
||||
emojiString === "🖕" &&
|
||||
this.recipient !== AllPlayers &&
|
||||
this.recipient.type() === PlayerType.Nation
|
||||
) {
|
||||
this.recipient.updateRelation(this.requestor, -100);
|
||||
}
|
||||
respondToEmoji(
|
||||
this.mg,
|
||||
this.random,
|
||||
this.requestor,
|
||||
this.recipient,
|
||||
emojiString,
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
`cannot send emoji from ${this.requestor} to ${this.recipient}`,
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from "../Util";
|
||||
import { ConstructionExecution } from "./ConstructionExecution";
|
||||
import { NationAllianceBehavior } from "./nation/NationAllianceBehavior";
|
||||
import { NationEmojiBehavior } from "./nation/NationEmojiBehavior";
|
||||
import { EMOJI_NUKE, NationEmojiBehavior } from "./nation/NationEmojiBehavior";
|
||||
import { NationMIRVBehavior } from "./nation/NationMIRVBehavior";
|
||||
import { NationWarshipBehavior } from "./nation/NationWarshipBehavior";
|
||||
import { structureSpawnTileValue } from "./nation/structureSpawnTileValue";
|
||||
@@ -136,6 +136,7 @@ export class NationExecution implements Execution {
|
||||
}
|
||||
|
||||
if (
|
||||
this.emojiBehavior === null ||
|
||||
this.mirvBehavior === null ||
|
||||
this.attackBehavior === null ||
|
||||
this.allianceBehavior === null ||
|
||||
@@ -157,11 +158,13 @@ export class NationExecution implements Execution {
|
||||
this.random,
|
||||
this.mg,
|
||||
this.player,
|
||||
this.emojiBehavior,
|
||||
);
|
||||
this.warshipBehavior = new NationWarshipBehavior(
|
||||
this.random,
|
||||
this.mg,
|
||||
this.player,
|
||||
this.emojiBehavior,
|
||||
);
|
||||
this.attackBehavior = new AiAttackBehavior(
|
||||
this.random,
|
||||
@@ -179,6 +182,7 @@ export class NationExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emojiBehavior.maybeSendCasualEmoji();
|
||||
this.updateRelationsFromEmbargos();
|
||||
this.allianceBehavior.handleAllianceRequests();
|
||||
this.allianceBehavior.handleAllianceExtensionRequests();
|
||||
@@ -676,7 +680,7 @@ export class NationExecution implements Execution {
|
||||
const tick = this.mg.ticks();
|
||||
this.lastNukeSent.push([tick, tile]);
|
||||
this.mg.addExecution(new NukeExecution(nukeType, this.player, tile));
|
||||
this.emojiBehavior.maybeSendHeckleEmoji(targetPlayer);
|
||||
this.emojiBehavior.maybeSendEmoji(targetPlayer, EMOJI_NUKE);
|
||||
}
|
||||
|
||||
private randTerritoryTileArray(numTiles: number): TileRef[] {
|
||||
|
||||
@@ -9,17 +9,25 @@ import { PseudoRandom } from "../../PseudoRandom";
|
||||
import { assertNever } from "../../Util";
|
||||
import { AllianceExtensionExecution } from "../alliance/AllianceExtensionExecution";
|
||||
import { AllianceRequestExecution } from "../alliance/AllianceRequestExecution";
|
||||
import {
|
||||
EMOJI_CONFUSED,
|
||||
EMOJI_HANDSHAKE,
|
||||
EMOJI_LOVE,
|
||||
EMOJI_SCARED_OF_THREAT,
|
||||
NationEmojiBehavior,
|
||||
} from "./NationEmojiBehavior";
|
||||
|
||||
export class NationAllianceBehavior {
|
||||
constructor(
|
||||
private random: PseudoRandom,
|
||||
private game: Game,
|
||||
private player: Player,
|
||||
private emojiBehavior: NationEmojiBehavior,
|
||||
) {}
|
||||
|
||||
handleAllianceRequests() {
|
||||
for (const req of this.player.incomingAllianceRequests()) {
|
||||
if (this.getAllianceRequestDecision(req.requestor())) {
|
||||
if (this.getAllianceDecision(req.requestor(), true)) {
|
||||
req.accept();
|
||||
} else {
|
||||
req.reject();
|
||||
@@ -34,7 +42,7 @@ export class NationAllianceBehavior {
|
||||
if (!alliance.onlyOneAgreedToExtend()) continue;
|
||||
|
||||
const human = alliance.other(this.player);
|
||||
if (!this.getAllianceRequestDecision(human)) continue;
|
||||
if (!this.getAllianceDecision(human, true)) continue;
|
||||
|
||||
this.game.addExecution(
|
||||
new AllianceExtensionExecution(this.player, human.id()),
|
||||
@@ -54,7 +62,7 @@ export class NationAllianceBehavior {
|
||||
this.random.chance(20) &&
|
||||
isAcceptablePlayerType(enemy) &&
|
||||
this.player.canSendAllianceRequest(enemy) &&
|
||||
this.getAllianceRequestDecision(enemy)
|
||||
this.getAllianceDecision(enemy, false)
|
||||
) {
|
||||
this.game.addExecution(
|
||||
new AllianceRequestExecution(this.player, enemy.id()),
|
||||
@@ -63,27 +71,45 @@ export class NationAllianceBehavior {
|
||||
}
|
||||
}
|
||||
|
||||
private getAllianceRequestDecision(otherPlayer: Player): boolean {
|
||||
private getAllianceDecision(
|
||||
otherPlayer: Player,
|
||||
isResponse: boolean,
|
||||
): boolean {
|
||||
// Easy (dumb) nations sometimes get confused and accept/reject randomly (Just like dumb humans do)
|
||||
if (this.isConfused()) {
|
||||
return this.random.chance(2);
|
||||
}
|
||||
// Nearly always reject traitors
|
||||
if (otherPlayer.isTraitor() && this.random.nextInt(0, 100) >= 10) {
|
||||
if (isResponse && this.random.chance(3)) {
|
||||
this.emojiBehavior.sendEmoji(otherPlayer, EMOJI_CONFUSED);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Before caring about the relation, first check if the otherPlayer is a threat
|
||||
// Easy (dumb) nations are blinded by hatred, they don't care about threats, they care about the relation
|
||||
// Impossible (smart) nations on the other hand are analyzing the facts
|
||||
if (this.isAlliancePartnerThreat(otherPlayer)) {
|
||||
if (!isResponse && this.random.chance(3)) {
|
||||
this.emojiBehavior.sendEmoji(otherPlayer, EMOJI_SCARED_OF_THREAT);
|
||||
}
|
||||
if (isResponse && this.random.chance(3)) {
|
||||
this.emojiBehavior.sendEmoji(otherPlayer, EMOJI_LOVE);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Reject if relation is bad
|
||||
if (this.player.relation(otherPlayer) < Relation.Neutral) {
|
||||
if (isResponse && this.random.chance(3)) {
|
||||
this.emojiBehavior.sendEmoji(otherPlayer, EMOJI_CONFUSED);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Maybe accept if relation is friendly
|
||||
if (this.isAlliancePartnerFriendly(otherPlayer)) {
|
||||
if (this.random.chance(3)) {
|
||||
this.emojiBehavior.sendEmoji(otherPlayer, EMOJI_HANDSHAKE);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Reject if we already have some alliances, we don't want to ally with the entire map
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { Game, Player, PlayerType, Tick } from "../../game/Game";
|
||||
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";
|
||||
@@ -13,10 +23,32 @@ export const EMOJI_ASSIST_RELATION_TOO_LOW = (["🥱", "🤦♂️"] as const
|
||||
);
|
||||
export const EMOJI_ASSIST_TARGET_ME = (["🥺", "💀"] as const).map(emojiId);
|
||||
export const EMOJI_ASSIST_TARGET_ALLY = (["🕊️", "👎"] as const).map(emojiId);
|
||||
export const EMOJI_HECKLE = (["🤡", "😡"] 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,
|
||||
@@ -24,28 +56,286 @@ export class NationEmojiBehavior {
|
||||
private player: Player,
|
||||
) {}
|
||||
|
||||
sendEmoji(player: Player, emojisList: number[]) {
|
||||
if (player.type() !== PlayerType.Human) return;
|
||||
maybeSendCasualEmoji() {
|
||||
this.checkOverwhelmedByAttacks();
|
||||
this.checkVerySmallAttack();
|
||||
this.congratulateWinner();
|
||||
this.brag();
|
||||
this.charmAllies();
|
||||
this.annoyTraitors();
|
||||
this.findRat();
|
||||
}
|
||||
|
||||
private checkOverwhelmedByAttacks(): void {
|
||||
if (!this.random.chance(4)) 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(4)) 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(100)) 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.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,
|
||||
player.id(),
|
||||
otherPlayer === AllPlayers ? AllPlayers : otherPlayer.id(),
|
||||
this.random.randElement(emojisList),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
maybeSendHeckleEmoji(enemy: Player) {
|
||||
if (this.player.type() === PlayerType.Bot) return;
|
||||
if (enemy.type() !== PlayerType.Human) return;
|
||||
const lastSent = this.lastEmojiSent.get(enemy) ?? -300;
|
||||
if (this.game.ticks() - lastSent <= 300) return;
|
||||
this.lastEmojiSent.set(enemy, this.game.ticks());
|
||||
this.game.addExecution(
|
||||
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(
|
||||
this.player,
|
||||
enemy.id(),
|
||||
this.random.randElement(EMOJI_HECKLE),
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
AllPlayers,
|
||||
Difficulty,
|
||||
Game,
|
||||
Gold,
|
||||
@@ -11,7 +12,7 @@ import { PseudoRandom } from "../../PseudoRandom";
|
||||
import { assertNever } from "../../Util";
|
||||
import { MirvExecution } from "../MIRVExecution";
|
||||
import { calculateTerritoryCenter } from "../Util";
|
||||
import { NationEmojiBehavior } from "./NationEmojiBehavior";
|
||||
import { EMOJI_NUKE, NationEmojiBehavior } from "./NationEmojiBehavior";
|
||||
|
||||
export class NationMIRVBehavior {
|
||||
constructor(
|
||||
@@ -251,11 +252,12 @@ export class NationMIRVBehavior {
|
||||
private maybeSendMIRV(enemy: Player): void {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
|
||||
this.emojiBehavior.maybeSendHeckleEmoji(enemy);
|
||||
this.emojiBehavior.maybeSendAttackEmoji(enemy);
|
||||
|
||||
const centerTile = this.calculateTerritoryCenter(enemy);
|
||||
if (centerTile && this.player.canBuild(UnitType.MIRV, centerTile)) {
|
||||
this.game.addExecution(new MirvExecution(this.player, centerTile));
|
||||
this.emojiBehavior.sendEmoji(AllPlayers, EMOJI_NUKE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
AllPlayers,
|
||||
Difficulty,
|
||||
Game,
|
||||
Gold,
|
||||
@@ -10,6 +11,10 @@ import {
|
||||
import { TileRef } from "../../game/GameMap";
|
||||
import { PseudoRandom } from "../../PseudoRandom";
|
||||
import { ConstructionExecution } from "../ConstructionExecution";
|
||||
import {
|
||||
EMOJI_WARSHIP_RETALIATION,
|
||||
NationEmojiBehavior,
|
||||
} from "./NationEmojiBehavior";
|
||||
|
||||
export class NationWarshipBehavior {
|
||||
// Track our transport ships we currently own
|
||||
@@ -21,6 +26,7 @@ export class NationWarshipBehavior {
|
||||
private random: PseudoRandom,
|
||||
private game: Game,
|
||||
private player: Player,
|
||||
private emojiBehavior: NationEmojiBehavior,
|
||||
) {}
|
||||
|
||||
trackShipsAndRetaliate(): void {
|
||||
@@ -39,8 +45,8 @@ export class NationWarshipBehavior {
|
||||
for (const ship of Array.from(this.trackedTransportShips)) {
|
||||
if (!ship.isActive()) {
|
||||
// Distinguish between arrival/retreat and enemy destruction
|
||||
if (ship.wasDestroyedByEnemy()) {
|
||||
this.maybeRetaliateWithWarship(ship.tile());
|
||||
if (ship.wasDestroyedByEnemy() && ship.destroyer() !== undefined) {
|
||||
this.maybeRetaliateWithWarship(ship.tile(), ship.destroyer()!);
|
||||
}
|
||||
this.trackedTransportShips.delete(ship);
|
||||
}
|
||||
@@ -62,13 +68,13 @@ export class NationWarshipBehavior {
|
||||
}
|
||||
if (ship.owner().id() !== this.player.id()) {
|
||||
// Ship was ours and is now owned by someone else -> captured
|
||||
this.maybeRetaliateWithWarship(ship.tile());
|
||||
this.maybeRetaliateWithWarship(ship.tile(), ship.owner());
|
||||
this.trackedTradeShips.delete(ship);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private maybeRetaliateWithWarship(tile: TileRef): void {
|
||||
private maybeRetaliateWithWarship(tile: TileRef, enemy: Player): void {
|
||||
const { difficulty } = this.game.config().gameConfig();
|
||||
// In Easy never retaliate. In Medium retaliate with 15% chance. Hard with 50%, Impossible with 80%.
|
||||
if (
|
||||
@@ -83,6 +89,7 @@ export class NationWarshipBehavior {
|
||||
this.game.addExecution(
|
||||
new ConstructionExecution(this.player, UnitType.Warship, tile),
|
||||
);
|
||||
this.emojiBehavior.maybeSendEmoji(enemy, EMOJI_WARSHIP_RETALIATION);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,6 +263,7 @@ export class NationWarshipBehavior {
|
||||
target.warship.tile(),
|
||||
),
|
||||
);
|
||||
this.emojiBehavior.sendEmoji(AllPlayers, EMOJI_WARSHIP_RETALIATION);
|
||||
}
|
||||
|
||||
private cost(type: UnitType): Gold {
|
||||
|
||||
@@ -478,6 +478,11 @@ export class AiAttackBehavior {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.isPlayer() && this.player.type() === PlayerType.Nation) {
|
||||
if (this.emojiBehavior === undefined) throw new Error("not initialized");
|
||||
this.emojiBehavior.maybeSendAttackEmoji(target);
|
||||
}
|
||||
|
||||
this.game.addExecution(
|
||||
new AttackExecution(
|
||||
troops,
|
||||
@@ -485,11 +490,6 @@ export class AiAttackBehavior {
|
||||
target.isPlayer() ? target.id() : this.game.terraNullius().id(),
|
||||
),
|
||||
);
|
||||
|
||||
if (target.isPlayer() && this.player.type() === PlayerType.Nation) {
|
||||
if (this.emojiBehavior === undefined) throw new Error("not initialized");
|
||||
this.emojiBehavior.maybeSendHeckleEmoji(target);
|
||||
}
|
||||
}
|
||||
|
||||
private sendBoatAttack(target: Player) {
|
||||
@@ -515,6 +515,11 @@ export class AiAttackBehavior {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.isPlayer() && this.player.type() === PlayerType.Nation) {
|
||||
if (this.emojiBehavior === undefined) throw new Error("not initialized");
|
||||
this.emojiBehavior.maybeSendAttackEmoji(target);
|
||||
}
|
||||
|
||||
this.game.addExecution(
|
||||
new TransportShipExecution(
|
||||
this.player,
|
||||
@@ -524,11 +529,6 @@ export class AiAttackBehavior {
|
||||
null,
|
||||
),
|
||||
);
|
||||
|
||||
if (target.isPlayer() && this.player.type() === PlayerType.Nation) {
|
||||
if (this.emojiBehavior === undefined) throw new Error("not initialized");
|
||||
this.emojiBehavior.maybeSendHeckleEmoji(target);
|
||||
}
|
||||
}
|
||||
|
||||
private calculateBotAttackTroops(target: Player, maxTroops: number): number {
|
||||
|
||||
@@ -459,6 +459,7 @@ export interface Unit {
|
||||
hasTrainStation(): boolean;
|
||||
setTrainStation(trainStation: boolean): void;
|
||||
wasDestroyedByEnemy(): boolean;
|
||||
destroyer(): Player | undefined;
|
||||
|
||||
// Train
|
||||
trainType(): TrainType | undefined;
|
||||
|
||||
@@ -25,6 +25,7 @@ export class UnitImpl implements Unit {
|
||||
private _targetedBySAM = false;
|
||||
private _reachedTarget = false;
|
||||
private _wasDestroyedByEnemy: boolean = false;
|
||||
private _destroyer: Player | undefined = undefined;
|
||||
private _lastSetSafeFromPirates: number; // Only for trade ships
|
||||
private _underConstruction: boolean = false;
|
||||
private _lastOwner: PlayerImpl | null = null;
|
||||
@@ -258,6 +259,7 @@ export class UnitImpl implements Unit {
|
||||
|
||||
// Record whether this unit was destroyed by an enemy (vs. arrived / retreated)
|
||||
this._wasDestroyedByEnemy = destroyer !== undefined;
|
||||
this._destroyer = destroyer ?? undefined;
|
||||
|
||||
this._owner._units = this._owner._units.filter((b) => b !== this);
|
||||
this._active = false;
|
||||
@@ -302,6 +304,10 @@ export class UnitImpl implements Unit {
|
||||
return this._wasDestroyedByEnemy;
|
||||
}
|
||||
|
||||
destroyer(): Player | undefined {
|
||||
return this._destroyer;
|
||||
}
|
||||
|
||||
retreating(): boolean {
|
||||
return this._retreating;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NationAllianceBehavior } from "../src/core/execution/nation/NationAllianceBehavior";
|
||||
import { NationEmojiBehavior } from "../src/core/execution/nation/NationEmojiBehavior";
|
||||
import {
|
||||
AllianceRequest,
|
||||
Game,
|
||||
@@ -44,7 +45,12 @@ describe("AllianceBehavior.handleAllianceRequests", () => {
|
||||
// Use a fixed random seed for deterministic behavior
|
||||
const random = new PseudoRandom(46);
|
||||
|
||||
allianceBehavior = new NationAllianceBehavior(random, game, player);
|
||||
allianceBehavior = new NationAllianceBehavior(
|
||||
random,
|
||||
game,
|
||||
player,
|
||||
new NationEmojiBehavior(random, game, player),
|
||||
);
|
||||
});
|
||||
|
||||
function setupAllianceRequest({
|
||||
@@ -164,6 +170,7 @@ describe("AllianceBehavior.handleAllianceExtensionRequests", () => {
|
||||
mockRandom,
|
||||
mockGame,
|
||||
mockPlayer,
|
||||
new NationEmojiBehavior(mockRandom, mockGame, mockPlayer),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user