Files
OpenFrontIO/src/core/game/Game.ts
T
Arkadiusz Sygulski ab5b044362 Fix train was destroyed message spam (#2774)
## Description:

Trains are made of a primary unit (`TrainExecution.train`) followed by 6
cars (`TrainExecution.cars`). Currently when any of the units is
destroyed, a message "Your Train was destroyed" is shown. In worst case
scenario, when all cars are blasted by a nuke, 7 messages are displayed
to the user, but the train continues.

Since the actual logic is unaffected as long as the primary unit stays
alive, displaying messages is confusing. This PR fixes it. The message
is only displayed when the primary unit is destroyed. The following cars
are only there for fancy visuals.



## Screencast

##### Current - multiple messages for single train


https://github.com/user-attachments/assets/3df04c71-d899-4f68-af83-36c9d0ffa730

First nuke was a test.
Second nuke destroyed the entire train, prompting 7 messages.
Third nuke destroyed one full train and then some. Prompting many too
many messages.

##### Fixed - one message, only if train was fully destroyed


https://github.com/user-attachments/assets/1f3840a7-6c62-487d-af3a-82de39dad9e8

First train was only partially destroyed, no message was shown, delivery
mission completed A+.
Following 2 trains had the front engine destroyed and therefore were
promptly decommissioned.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [ ] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [ ] 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:

moleole
2026-01-02 18:53:24 -08:00

901 lines
22 KiB
TypeScript

import { Config } from "../configuration/Config";
import { AllPlayersStats, ClientID } from "../Schemas";
import { getClanTag } from "../Util";
import { GameMap, TileRef } from "./GameMap";
import {
GameUpdate,
GameUpdateType,
PlayerUpdate,
UnitUpdate,
} from "./GameUpdates";
import { RailNetwork } from "./RailNetwork";
import { Stats } from "./Stats";
import { UnitPredicate } from "./UnitGrid";
function isEnumValue<T extends Record<string, string | number>>(
enumObj: T,
value: unknown,
): value is T[keyof T] {
return Object.values(enumObj).includes(value as T[keyof T]);
}
export type PlayerID = string;
export type Tick = number;
export type Gold = bigint;
export const AllPlayers = "AllPlayers" as const;
// export type GameUpdates = Record<GameUpdateType, GameUpdate[]>;
// Create a type that maps GameUpdateType to its corresponding update type
type UpdateTypeMap<T extends GameUpdateType> = Extract<GameUpdate, { type: T }>;
// Then use it to create the record type
export type GameUpdates = {
[K in GameUpdateType]: UpdateTypeMap<K>[];
};
export interface MapPos {
x: number;
y: number;
}
export enum Difficulty {
Easy = "Easy",
Medium = "Medium",
Hard = "Hard",
Impossible = "Impossible",
}
export const isDifficulty = (value: unknown): value is Difficulty =>
isEnumValue(Difficulty, value);
export type Team = string;
export const Duos = "Duos" as const;
export const Trios = "Trios" as const;
export const Quads = "Quads" as const;
export const HumansVsNations = "Humans Vs Nations" as const;
export const ColoredTeams: Record<string, Team> = {
Red: "Red",
Blue: "Blue",
Teal: "Teal",
Purple: "Purple",
Yellow: "Yellow",
Orange: "Orange",
Green: "Green",
Bot: "Bot",
Humans: "Humans",
Nations: "Nations",
} as const;
export enum GameMapType {
World = "World",
GiantWorldMap = "Giant World Map",
Europe = "Europe",
EuropeClassic = "Europe Classic",
Mena = "Mena",
NorthAmerica = "North America",
SouthAmerica = "South America",
Oceania = "Oceania",
BlackSea = "Black Sea",
Africa = "Africa",
Pangaea = "Pangaea",
Asia = "Asia",
Mars = "Mars",
Britannia = "Britannia",
GatewayToTheAtlantic = "Gateway to the Atlantic",
Australia = "Australia",
Iceland = "Iceland",
EastAsia = "East Asia",
BetweenTwoSeas = "Between Two Seas",
FaroeIslands = "Faroe Islands",
DeglaciatedAntarctica = "Deglaciated Antarctica",
FalklandIslands = "Falkland Islands",
Baikal = "Baikal",
Halkidiki = "Halkidiki",
StraitOfGibraltar = "Strait of Gibraltar",
Italia = "Italia",
Japan = "Japan",
Pluto = "Pluto",
Montreal = "Montreal",
NewYorkCity = "New York City",
Achiran = "Achiran",
BaikalNukeWars = "Baikal (Nuke Wars)",
FourIslands = "Four Islands",
Svalmel = "Svalmel",
GulfOfStLawrence = "Gulf of St. Lawrence",
Lisbon = "Lisbon",
Manicouagan = "Manicouagan",
Lemnos = "Lemnos",
TwoLakes = "Two Lakes",
StraitOfHormuz = "Strait of Hormuz",
Surrounded = "Surrounded",
}
export type GameMapName = keyof typeof GameMapType;
export const mapCategories: Record<string, GameMapType[]> = {
continental: [
GameMapType.World,
GameMapType.GiantWorldMap,
GameMapType.NorthAmerica,
GameMapType.SouthAmerica,
GameMapType.Europe,
GameMapType.EuropeClassic,
GameMapType.Asia,
GameMapType.Africa,
GameMapType.Oceania,
],
regional: [
GameMapType.BlackSea,
GameMapType.Britannia,
GameMapType.GatewayToTheAtlantic,
GameMapType.BetweenTwoSeas,
GameMapType.Iceland,
GameMapType.EastAsia,
GameMapType.Mena,
GameMapType.Australia,
GameMapType.FaroeIslands,
GameMapType.FalklandIslands,
GameMapType.Baikal,
GameMapType.Halkidiki,
GameMapType.StraitOfGibraltar,
GameMapType.Italia,
GameMapType.Japan,
GameMapType.Montreal,
GameMapType.GulfOfStLawrence,
GameMapType.Lisbon,
GameMapType.NewYorkCity,
GameMapType.Manicouagan,
GameMapType.Lemnos,
GameMapType.TwoLakes,
GameMapType.StraitOfHormuz,
],
fantasy: [
GameMapType.Pangaea,
GameMapType.Pluto,
GameMapType.Mars,
GameMapType.DeglaciatedAntarctica,
GameMapType.Achiran,
GameMapType.BaikalNukeWars,
GameMapType.FourIslands,
GameMapType.Svalmel,
GameMapType.Surrounded,
],
};
export enum GameType {
Singleplayer = "Singleplayer",
Public = "Public",
Private = "Private",
}
export const isGameType = (value: unknown): value is GameType =>
isEnumValue(GameType, value);
export enum GameMode {
FFA = "Free For All",
Team = "Team",
}
export const isGameMode = (value: unknown): value is GameMode =>
isEnumValue(GameMode, value);
export enum GameMapSize {
Compact = "Compact",
Normal = "Normal",
}
export interface UnitInfo {
cost: (game: Game, player: Player) => Gold;
// Determines if its owner changes when its tile is conquered.
territoryBound: boolean;
maxHealth?: number;
damage?: number;
constructionDuration?: number;
upgradable?: boolean;
canBuildTrainStation?: boolean;
experimental?: boolean;
}
export enum UnitType {
TransportShip = "Transport",
Warship = "Warship",
Shell = "Shell",
SAMMissile = "SAMMissile",
Port = "Port",
AtomBomb = "Atom Bomb",
HydrogenBomb = "Hydrogen Bomb",
TradeShip = "Trade Ship",
MissileSilo = "Missile Silo",
DefensePost = "Defense Post",
SAMLauncher = "SAM Launcher",
City = "City",
MIRV = "MIRV",
MIRVWarhead = "MIRV Warhead",
Train = "Train",
Factory = "Factory",
}
export enum TrainType {
Engine = "Engine",
TailEngine = "TailEngine",
Carriage = "Carriage",
}
const _structureTypes: ReadonlySet<UnitType> = new Set([
UnitType.City,
UnitType.DefensePost,
UnitType.SAMLauncher,
UnitType.MissileSilo,
UnitType.Port,
UnitType.Factory,
]);
export function isStructureType(type: UnitType): boolean {
return _structureTypes.has(type);
}
export interface OwnerComp {
owner: Player;
}
export type TrajectoryTile = {
tile: TileRef;
targetable: boolean;
};
export interface UnitParamsMap {
[UnitType.TransportShip]: {
troops?: number;
destination?: TileRef;
};
[UnitType.Warship]: {
patrolTile: TileRef;
};
[UnitType.Shell]: Record<string, never>;
[UnitType.SAMMissile]: Record<string, never>;
[UnitType.Port]: Record<string, never>;
[UnitType.AtomBomb]: {
targetTile?: number;
trajectory: TrajectoryTile[];
};
[UnitType.HydrogenBomb]: {
targetTile?: number;
trajectory: TrajectoryTile[];
};
[UnitType.TradeShip]: {
targetUnit: Unit;
lastSetSafeFromPirates?: number;
};
[UnitType.Train]: {
trainType: TrainType;
targetUnit?: Unit;
loaded?: boolean;
};
[UnitType.Factory]: Record<string, never>;
[UnitType.MissileSilo]: Record<string, never>;
[UnitType.DefensePost]: Record<string, never>;
[UnitType.SAMLauncher]: Record<string, never>;
[UnitType.City]: Record<string, never>;
[UnitType.MIRV]: {
targetTile?: number;
};
[UnitType.MIRVWarhead]: {
targetTile?: number;
};
}
// Type helper to get params type for a specific unit type
export type UnitParams<T extends UnitType> = UnitParamsMap[T];
export type AllUnitParams = UnitParamsMap[keyof UnitParamsMap];
export const nukeTypes = [
UnitType.AtomBomb,
UnitType.HydrogenBomb,
UnitType.MIRVWarhead,
UnitType.MIRV,
] as UnitType[];
export enum Relation {
Hostile = 0,
Distrustful = 1,
Neutral = 2,
Friendly = 3,
}
export class Nation {
constructor(
public readonly spawnCell: Cell | undefined,
public readonly playerInfo: PlayerInfo,
) {}
}
export class Cell {
public index: number;
private strRepr: string;
constructor(
public readonly x: number,
public readonly y: number,
) {
this.strRepr = `Cell[${this.x},${this.y}]`;
}
pos(): MapPos {
return {
x: this.x,
y: this.y,
};
}
toString(): string {
return this.strRepr;
}
}
export enum TerrainType {
Plains,
Highland,
Mountain,
Lake,
Ocean,
}
export enum PlayerType {
Bot = "BOT",
Human = "HUMAN",
Nation = "NATION",
}
export interface Execution {
isActive(): boolean;
activeDuringSpawnPhase(): boolean;
init(mg: Game, ticks: number): void;
tick(ticks: number): void;
}
export interface Attack {
id(): string;
retreating(): boolean;
retreated(): boolean;
orderRetreat(): void;
executeRetreat(): void;
target(): Player | TerraNullius;
attacker(): Player;
troops(): number;
setTroops(troops: number): void;
isActive(): boolean;
delete(): void;
// The tile the attack originated from, mostly used for boat attacks.
sourceTile(): TileRef | null;
addBorderTile(tile: TileRef): void;
removeBorderTile(tile: TileRef): void;
clearBorder(): void;
borderSize(): number;
averagePosition(): Cell | null;
}
export interface AllianceRequest {
accept(): void;
reject(): void;
requestor(): Player;
recipient(): Player;
createdAt(): Tick;
status(): "pending" | "accepted" | "rejected";
}
export interface Alliance {
requestor(): Player;
recipient(): Player;
createdAt(): Tick;
expiresAt(): Tick;
other(player: Player): Player;
}
export interface MutableAlliance extends Alliance {
expire(): void;
other(player: Player): Player;
bothAgreedToExtend(): boolean;
addExtensionRequest(player: Player): void;
id(): number;
extend(): void;
onlyOneAgreedToExtend(): boolean;
}
export class PlayerInfo {
public readonly clan: string | null;
constructor(
public readonly name: string,
public readonly playerType: PlayerType,
// null if bot.
public readonly clientID: ClientID | null,
// TODO: make player id the small id
public readonly id: PlayerID,
public readonly isLobbyCreator: boolean = false,
) {
this.clan = getClanTag(name);
}
}
export function isUnit(unit: unknown): unit is Unit {
return (
unit &&
typeof unit === "object" &&
"isUnit" in unit &&
typeof unit.isUnit === "function" &&
unit.isUnit()
);
}
export interface Unit {
isUnit(): this is Unit;
// Common properties.
id(): number;
type(): UnitType;
owner(): Player;
info(): UnitInfo;
isMarkedForDeletion(): boolean;
markForDeletion(): void;
isOverdueDeletion(): boolean;
delete(displayMessage?: boolean, destroyer?: Player): void;
tile(): TileRef;
lastTile(): TileRef;
move(tile: TileRef): void;
isActive(): boolean;
setOwner(owner: Player): void;
touch(): void;
hash(): number;
toUpdate(): UnitUpdate;
hasTrainStation(): boolean;
setTrainStation(trainStation: boolean): void;
wasDestroyedByEnemy(): boolean;
destroyer(): Player | undefined;
// Train
trainType(): TrainType | undefined;
isLoaded(): boolean | undefined;
setLoaded(loaded: boolean): void;
// Targeting
setTargetTile(cell: TileRef | undefined): void;
targetTile(): TileRef | undefined;
setTrajectoryIndex(i: number): void;
trajectoryIndex(): number;
trajectory(): TrajectoryTile[];
setTargetUnit(unit: Unit | undefined): void;
targetUnit(): Unit | undefined;
setTargetedBySAM(targeted: boolean): void;
targetedBySAM(): boolean;
setReachedTarget(): void;
reachedTarget(): boolean;
isTargetable(): boolean;
setTargetable(targetable: boolean): void;
// Health
hasHealth(): boolean;
retreating(): boolean;
orderBoatRetreat(): void;
health(): number;
modifyHealth(delta: number, attacker?: Player): void;
// Troops
setTroops(troops: number): void;
troops(): number;
// --- UNIT SPECIFIC ---
// SAMs & Missile Silos
launch(): void;
reloadMissile(): void;
isInCooldown(): boolean;
missileTimerQueue(): number[];
// Trade Ships
setSafeFromPirates(): void; // Only for trade ships
isSafeFromPirates(): boolean; // Only for trade ships
// Construction phase on structures
isUnderConstruction(): boolean;
setUnderConstruction(underConstruction: boolean): void;
// Upgradable Structures
level(): number;
increaseLevel(): void;
decreaseLevel(destroyer?: Player): void;
// Warships
setPatrolTile(tile: TileRef): void;
patrolTile(): TileRef | undefined;
}
export interface TerraNullius {
isPlayer(): false;
id(): null;
clientID(): ClientID;
smallID(): number;
}
export interface Embargo {
createdAt: Tick;
isTemporary: boolean;
target: Player;
}
export interface Player {
// Basic Info
smallID(): number;
info(): PlayerInfo;
name(): string;
displayName(): string;
clientID(): ClientID | null;
id(): PlayerID;
type(): PlayerType;
isPlayer(): this is Player;
toString(): string;
isLobbyCreator(): boolean;
// State & Properties
isAlive(): boolean;
isTraitor(): boolean;
markTraitor(): void;
largestClusterBoundingBox: { min: Cell; max: Cell } | null;
lastTileChange(): Tick;
isDisconnected(): boolean;
markDisconnected(isDisconnected: boolean): void;
hasSpawned(): boolean;
setSpawnTile(spawnTile: TileRef): void;
spawnTile(): TileRef | undefined;
// Territory
tiles(): ReadonlySet<TileRef>;
borderTiles(): ReadonlySet<TileRef>;
numTilesOwned(): number;
conquer(tile: TileRef): void;
relinquish(tile: TileRef): void;
// Resources & Troops
gold(): Gold;
addGold(toAdd: Gold, tile?: TileRef): void;
removeGold(toRemove: Gold): Gold;
troops(): number;
setTroops(troops: number): void;
addTroops(troops: number): void;
removeTroops(troops: number): number;
// Units
units(...types: UnitType[]): Unit[];
unitCount(type: UnitType): number;
unitsConstructed(type: UnitType): number;
unitsOwned(type: UnitType): number;
buildableUnits(tile: TileRef | null): BuildableUnit[];
canBuild(type: UnitType, targetTile: TileRef): TileRef | false;
buildUnit<T extends UnitType>(
type: T,
spawnTile: TileRef,
params: UnitParams<T>,
): Unit;
// Returns the existing unit that can be upgraded,
// or false if it cannot be upgraded.
// New units of the same type can upgrade existing units.
// e.g. if a place a new city here, can it upgrade an existing city?
findUnitToUpgrade(type: UnitType, targetTile: TileRef): Unit | false;
canUpgradeUnit(unit: Unit): boolean;
upgradeUnit(unit: Unit): void;
captureUnit(unit: Unit): void;
// Relations & Diplomacy
neighbors(): (Player | TerraNullius)[];
sharesBorderWith(other: Player | TerraNullius): boolean;
relation(other: Player): Relation;
allRelationsSorted(): { player: Player; relation: Relation }[];
updateRelation(other: Player, delta: number): void;
decayRelations(): void;
isOnSameTeam(other: Player): boolean;
// Either allied or on same team.
isFriendly(other: Player, treatAFKFriendly?: boolean): boolean;
team(): Team | null;
clan(): string | null;
incomingAllianceRequests(): AllianceRequest[];
outgoingAllianceRequests(): AllianceRequest[];
alliances(): MutableAlliance[];
expiredAlliances(): Alliance[];
allies(): Player[];
isAlliedWith(other: Player): boolean;
allianceWith(other: Player): MutableAlliance | null;
canSendAllianceRequest(other: Player): boolean;
breakAlliance(alliance: Alliance): void;
createAllianceRequest(recipient: Player): AllianceRequest | null;
betrayals(): number;
// Targeting
canTarget(other: Player): boolean;
target(other: Player): void;
targets(): Player[];
transitiveTargets(): Player[];
// Communication
canSendEmoji(recipient: Player | typeof AllPlayers): boolean;
outgoingEmojis(): EmojiMessage[];
sendEmoji(recipient: Player | typeof AllPlayers, emoji: string): void;
// Donation
canDonateGold(recipient: Player): boolean;
canDonateTroops(recipient: Player): boolean;
donateTroops(recipient: Player, troops: number): boolean;
donateGold(recipient: Player, gold: Gold): boolean;
canDeleteUnit(): boolean;
recordDeleteUnit(): void;
canEmbargoAll(): boolean;
recordEmbargoAll(): void;
// Embargo
hasEmbargoAgainst(other: Player): boolean;
tradingPartners(): Player[];
addEmbargo(other: Player, isTemporary: boolean): void;
getEmbargoes(): Embargo[];
stopEmbargo(other: Player): void;
endTemporaryEmbargo(other: Player): void;
canTrade(other: Player): boolean;
// Attacking.
canAttack(tile: TileRef): boolean;
createAttack(
target: Player | TerraNullius,
troops: number,
sourceTile: TileRef | null,
border: Set<number>,
): Attack;
outgoingAttacks(): Attack[];
incomingAttacks(): Attack[];
orderRetreat(attackID: string): void;
executeRetreat(attackID: string): void;
// Misc
toUpdate(): PlayerUpdate;
playerProfile(): PlayerProfile;
// WARNING: this operation is expensive.
bestTransportShipSpawn(tile: TileRef): TileRef | false;
}
export interface Game extends GameMap {
// Map & Dimensions
isOnMap(cell: Cell): boolean;
width(): number;
height(): number;
map(): GameMap;
miniMap(): GameMap;
forEachTile(fn: (tile: TileRef) => void): void;
// Zero-allocation neighbor iteration (cardinal only) to avoid creating arrays
forEachNeighbor(tile: TileRef, callback: (neighbor: TileRef) => void): void;
// Zero-allocation neighbor iteration for performance-critical cluster calculation
// Alternative to neighborsWithDiag() that returns arrays
// Avoids creating intermediate arrays and uses a callback for better performance
forEachNeighborWithDiag(
tile: TileRef,
callback: (neighbor: TileRef) => void,
): void;
// Player Management
player(id: PlayerID): Player;
players(): Player[];
allPlayers(): Player[];
playerByClientID(id: ClientID): Player | null;
playerBySmallID(id: number): Player | TerraNullius;
hasPlayer(id: PlayerID): boolean;
addPlayer(playerInfo: PlayerInfo): Player;
terraNullius(): TerraNullius;
owner(ref: TileRef): Player | TerraNullius;
teams(): Team[];
// Alliances
alliances(): MutableAlliance[];
expireAlliance(alliance: Alliance): void;
// Game State
ticks(): Tick;
inSpawnPhase(): boolean;
executeNextTick(): GameUpdates;
setWinner(winner: Player | Team, allPlayersStats: AllPlayersStats): void;
config(): Config;
isPaused(): boolean;
setPaused(paused: boolean): void;
// Units
units(...types: UnitType[]): Unit[];
unitCount(type: UnitType): number;
unitInfo(type: UnitType): UnitInfo;
hasUnitNearby(
tile: TileRef,
searchRange: number,
type: UnitType,
playerId?: PlayerID,
includeUnderConstruction?: boolean,
): boolean;
nearbyUnits(
tile: TileRef,
searchRange: number,
types: UnitType | UnitType[],
predicate?: UnitPredicate,
includeUnderConstruction?: boolean,
): Array<{ unit: Unit; distSquared: number }>;
addExecution(...exec: Execution[]): void;
displayMessage(
message: string,
type: MessageType,
playerID: PlayerID | null,
goldAmount?: bigint,
params?: Record<string, string | number>,
): void;
displayIncomingUnit(
unitID: number,
message: string,
type: MessageType,
playerID: PlayerID | null,
): void;
displayChat(
message: string,
category: string,
target: PlayerID | undefined,
playerID: PlayerID | null,
isFrom: boolean,
recipient: string,
): void;
// Nations
nations(): Nation[];
numTilesWithFallout(): number;
stats(): Stats;
addUpdate(update: GameUpdate): void;
railNetwork(): RailNetwork;
conquerPlayer(conqueror: Player, conquered: Player): void;
}
export interface PlayerActions {
canAttack: boolean;
buildableUnits: BuildableUnit[];
canSendEmojiAllPlayers: boolean;
canEmbargoAll?: boolean;
interaction?: PlayerInteraction;
}
export interface BuildableUnit {
canBuild: TileRef | false;
// unit id of the existing unit that can be upgraded, or false if it cannot be upgraded.
canUpgrade: number | false;
type: UnitType;
cost: Gold;
}
export interface PlayerProfile {
relations: Record<number, Relation>;
alliances: number[];
}
export interface PlayerBorderTiles {
borderTiles: ReadonlySet<TileRef>;
}
export interface PlayerInteraction {
sharedBorder: boolean;
canSendEmoji: boolean;
canSendAllianceRequest: boolean;
canBreakAlliance: boolean;
canTarget: boolean;
canDonateGold: boolean;
canDonateTroops: boolean;
canEmbargo: boolean;
allianceExpiresAt?: Tick;
}
export interface EmojiMessage {
message: string;
senderID: number;
recipientID: number | typeof AllPlayers;
createdAt: Tick;
}
export enum MessageType {
ATTACK_FAILED,
ATTACK_CANCELLED,
ATTACK_REQUEST,
CONQUERED_PLAYER,
MIRV_INBOUND,
NUKE_INBOUND,
HYDROGEN_BOMB_INBOUND,
NAVAL_INVASION_INBOUND,
SAM_MISS,
SAM_HIT,
CAPTURED_ENEMY_UNIT,
UNIT_CAPTURED_BY_ENEMY,
UNIT_DESTROYED,
ALLIANCE_ACCEPTED,
ALLIANCE_REJECTED,
ALLIANCE_REQUEST,
ALLIANCE_BROKEN,
ALLIANCE_EXPIRED,
SENT_GOLD_TO_PLAYER,
RECEIVED_GOLD_FROM_PLAYER,
RECEIVED_GOLD_FROM_TRADE,
SENT_TROOPS_TO_PLAYER,
RECEIVED_TROOPS_FROM_PLAYER,
CHAT,
RENEW_ALLIANCE,
}
// Message categories used for filtering events in the EventsDisplay
export enum MessageCategory {
ATTACK = "ATTACK",
NUKE = "NUKE",
ALLIANCE = "ALLIANCE",
TRADE = "TRADE",
CHAT = "CHAT",
}
// Ensures that all message types are included in a category
export const MESSAGE_TYPE_CATEGORIES: Record<MessageType, MessageCategory> = {
[MessageType.ATTACK_FAILED]: MessageCategory.ATTACK,
[MessageType.ATTACK_CANCELLED]: MessageCategory.ATTACK,
[MessageType.ATTACK_REQUEST]: MessageCategory.ATTACK,
[MessageType.CONQUERED_PLAYER]: MessageCategory.ATTACK,
[MessageType.MIRV_INBOUND]: MessageCategory.NUKE,
[MessageType.NUKE_INBOUND]: MessageCategory.NUKE,
[MessageType.HYDROGEN_BOMB_INBOUND]: MessageCategory.NUKE,
[MessageType.NAVAL_INVASION_INBOUND]: MessageCategory.ATTACK,
[MessageType.SAM_MISS]: MessageCategory.ATTACK,
[MessageType.SAM_HIT]: MessageCategory.ATTACK,
[MessageType.CAPTURED_ENEMY_UNIT]: MessageCategory.ATTACK,
[MessageType.UNIT_CAPTURED_BY_ENEMY]: MessageCategory.ATTACK,
[MessageType.UNIT_DESTROYED]: MessageCategory.ATTACK,
[MessageType.ALLIANCE_ACCEPTED]: MessageCategory.ALLIANCE,
[MessageType.ALLIANCE_REJECTED]: MessageCategory.ALLIANCE,
[MessageType.ALLIANCE_REQUEST]: MessageCategory.ALLIANCE,
[MessageType.ALLIANCE_BROKEN]: MessageCategory.ALLIANCE,
[MessageType.ALLIANCE_EXPIRED]: MessageCategory.ALLIANCE,
[MessageType.RENEW_ALLIANCE]: MessageCategory.ALLIANCE,
[MessageType.SENT_GOLD_TO_PLAYER]: MessageCategory.TRADE,
[MessageType.RECEIVED_GOLD_FROM_PLAYER]: MessageCategory.TRADE,
[MessageType.RECEIVED_GOLD_FROM_TRADE]: MessageCategory.TRADE,
[MessageType.SENT_TROOPS_TO_PLAYER]: MessageCategory.TRADE,
[MessageType.RECEIVED_TROOPS_FROM_PLAYER]: MessageCategory.TRADE,
[MessageType.CHAT]: MessageCategory.CHAT,
} as const;
/**
* Get the category of a message type
*/
export function getMessageCategory(messageType: MessageType): MessageCategory {
return MESSAGE_TYPE_CATEGORIES[messageType];
}
export interface NameViewData {
x: number;
y: number;
size: number;
}