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:
FloPinguin
2025-12-26 22:07:31 +01:00
committed by GitHub
parent e45839fbc2
commit 5d52f73278
12 changed files with 441 additions and 54 deletions
+27 -1
View File
@@ -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}`,
+16 -15
View File
@@ -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}`,
+6 -2
View File
@@ -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
+305 -15
View File
@@ -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 {
+10 -10
View File
@@ -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 {
+1
View File
@@ -459,6 +459,7 @@ export interface Unit {
hasTrainStation(): boolean;
setTrainStation(trainStation: boolean): void;
wasDestroyedByEnemy(): boolean;
destroyer(): Player | undefined;
// Train
trainType(): TrainType | undefined;
+6
View File
@@ -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;
}
+8 -1
View File
@@ -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),
);
});