Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,8 @@
"alliance_request_sent": "Alliance request sent to {name}.",
"alliance_request_status": "{name} {status} your alliance request",
"atom_bomb_detonated": "{name} - atom bomb detonated",
"atom_bomb_inbound": "{name} - atom bomb inbound",
"atom_bomb_inbound_plural": "{name} - {count} atom bombs inbound",
"attack_cancelled_retreat": "Attack cancelled, {troops} soldiers killed during retreat",
"attack_request": "{name} requests you attack {target}",
"betrayal_debuff_ends": "{time} seconds left until betrayal debuff ends",
Expand All @@ -471,9 +473,14 @@
"duration_seconds_plural": "{seconds} seconds",
"focus": "Focus",
"hydrogen_bomb_detonated": "{name} - hydrogen bomb detonated",
"hydrogen_bomb_inbound": "{name} - hydrogen bomb inbound",
"hydrogen_bomb_inbound_plural": "{name} - {count} hydrogen bombs inbound",
"ignore": "Ignore",
"mirv_inbound": "⚠️⚠️⚠️ {name} - MIRV INBOUND ⚠️⚠️⚠️",
"mirv_inbound_plural": "⚠️⚠️⚠️ {name} - {count} MIRVs INBOUND ⚠️⚠️⚠️",
"mirv_warheads_intercepted": "{count, plural, one {{count} MIRV warhead intercepted} other {{count} MIRV warheads intercepted}}",
"missile_intercepted": "Missile intercepted {unit}",
"missile_intercepted_plural": "{count} missiles intercepted ({unit})",
"no_boats_available": "No boats available, max {max}",
"received_gold_from_captured_ship": "Received {gold} gold from ship captured from {name}",
"received_gold_from_conquest": "Conquered {name}, received {gold} gold",
Expand All @@ -487,7 +494,12 @@
"sent_gold_to_player": "Sent {gold} gold to {name}",
"sent_troops_to_player": "Sent {troops} troops to {name}",
"trade_ship_captured": "Your trade ship was captured by {name}",
"unit_captured": "Captured {unit} from {name}",
"unit_captured_plural": "Captured {count} {unit} from {name}",
"unit_destroyed": "Your {unit} was destroyed",
"unit_destroyed_plural": "{count} {unit} destroyed",
"unit_lost": "Your {unit} was captured by {name}",
"unit_lost_plural": "Lost {count} {unit} to {name}",
"unit_voluntarily_deleted": "Unit voluntarily deleted",
"wants_to_renew_alliance": "{name} wants to renew your alliance"
},
Expand Down Expand Up @@ -1409,6 +1421,19 @@
"sam_launcher": "SAM Launcher",
"warship": "Warship"
},
"unit_type_plural": {
"atom_bomb": "Atom Bombs",
"boat": "Boats",
"city": "Cities",
"defense_post": "Defense Posts",
"factory": "Factories",
"hydrogen_bomb": "Hydrogen Bombs",
"mirv": "MIRVs",
"missile_silo": "Missile Silos",
"port": "Ports",
"sam_launcher": "SAM Launchers",
"warship": "Warships"
},
"user_setting": {
"alert_frame_desc": "Toggle the alert frame. When enabled, the frame will be displayed when you are betrayed or attacked over land.",
"alert_frame_label": "Alert Frame",
Expand Down
174 changes: 154 additions & 20 deletions src/client/hud/layers/EventsDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,59 @@ import {
translateText,
} from "../../Utils";

const UNIT_TRANSLATION_KEYS: Record<string, string> = {
City: "city",
Port: "port",
"Defense Post": "defense_post",
"SAM Launcher": "sam_launcher",
"Missile Silo": "missile_silo",
Factory: "factory",
Warship: "warship",
Transport: "boat",
"Atom Bomb": "atom_bomb",
"Hydrogen Bomb": "hydrogen_bomb",
MIRV: "mirv",
};

const getTranslatedUnitName = (unitType: string, plural: boolean): string => {
const key = UNIT_TRANSLATION_KEYS[unitType];
if (!key) return unitType;
return translateText(plural ? `unit_type_plural.${key}` : `unit_type.${key}`);
};

function parseInboundNuke(
m: string,
): { player: string; nukeType: string } | null {
if (m.endsWith(" - atom bomb inbound"))
return { player: m.slice(0, -20), nukeType: "atom" };
if (m.endsWith(" - hydrogen bomb inbound"))
return { player: m.slice(0, -24), nukeType: "hydrogen" };
if (m.startsWith("⚠️⚠️⚠️ ") && m.endsWith(" - MIRV INBOUND ⚠️⚠️⚠️"))
return { player: m.slice(7, -22), nukeType: "mirv" };
return null;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _unused = [
"events_display.atom_bomb_inbound",
"events_display.atom_bomb_inbound_plural",
"events_display.hydrogen_bomb_inbound",
"events_display.hydrogen_bomb_inbound_plural",
"events_display.mirv_inbound",
"events_display.mirv_inbound_plural",
"unit_type_plural.atom_bomb",
"unit_type_plural.boat",
"unit_type_plural.city",
"unit_type_plural.defense_post",
"unit_type_plural.factory",
"unit_type_plural.hydrogen_bomb",
"unit_type_plural.mirv",
"unit_type_plural.missile_silo",
"unit_type_plural.port",
"unit_type_plural.sam_launcher",
"unit_type_plural.warship",
];

interface GameEvent {
description: string;
unsafeDescription?: boolean;
Expand All @@ -41,6 +94,10 @@ interface GameEvent {
onDelete?: () => void;
focusID?: number;
unitView?: UnitView;
groupKey?: string;
count?: number;
unitType?: string;
targetPlayerName?: string;
}

const TIER_1_TYPES: ReadonlySet<MessageType> = new Set([
Expand Down Expand Up @@ -242,6 +299,57 @@ export class EventsDisplay extends LitElement implements Controller {
}

private addEvent(event: GameEvent) {
if (event.groupKey) {
const existing = this.events.find(
(e) =>
e.groupKey === event.groupKey &&
this.game.ticks() - e.createdAt <= 30,
);
if (existing) {
existing.count = (existing.count ?? 1) + 1;
existing.createdAt = this.game.ticks();
if (event.unitView) existing.unitView = event.unitView;
if (event.focusID) existing.focusID = event.focusID;

const u = getTranslatedUnitName(existing.unitType ?? "", true);
const n = existing.targetPlayerName ?? "";
const c = existing.count;

if (existing.groupKey?.startsWith("destroyed_")) {
existing.description = translateText(
"events_display.unit_destroyed_plural",
{ count: c, unit: u },
);
} else if (existing.groupKey?.startsWith("captured_")) {
existing.description = translateText(
"events_display.unit_captured_plural",
{ count: c, unit: u, name: n },
);
} else if (existing.groupKey?.startsWith("lost_")) {
existing.description = translateText(
"events_display.unit_lost_plural",
{ count: c, unit: u, name: n },
);
} else if (existing.groupKey?.startsWith("inbound_")) {
const k =
"events_display." +
existing.unitType +
(existing.unitType === "mirv" ? "" : "_bomb") +
"_inbound_plural";
existing.description = translateText(k, { count: c, name: n });
} else if (existing.groupKey?.startsWith("intercepted_")) {
existing.description = translateText(
"events_display.missile_intercepted_plural",
{
count: c,
unit: getTranslatedUnitName(existing.unitType ?? "", true),
},
);
}
this.requestUpdate();
return;
}
}
this.events = [...this.events, event];
this.requestUpdate();
}
Expand All @@ -251,31 +359,46 @@ export class EventsDisplay extends LitElement implements Controller {
if (
event.playerID !== null &&
(!myPlayer || myPlayer.smallID() !== event.playerID)
) {
)
return;
}

// Captured trade-ship gold is surfaced as a transient +gold pip in
// control-panel rather than as a scroll-list entry.
if (event.message === "events_display.received_gold_from_captured_ship") {
if (event.message === "events_display.received_gold_from_captured_ship")
return;

const params = { ...event.params };
if (params.unit) {
params.unit = getTranslatedUnitName(String(params.unit), false);
}

let description: string = event.message;
if (event.message.startsWith("events_display.")) {
description = translateText(event.message, event.params ?? {});
const description = event.message.startsWith("events_display.")
? translateText(event.message, params)
: event.message;

let groupKey: string | undefined;
const unitType = String(event.params?.unit ?? "");
const targetPlayerName = String(event.params?.name ?? "");

if (event.message === "events_display.unit_destroyed") {
groupKey = `destroyed_${unitType}`;
} else if (event.message === "events_display.unit_captured") {
groupKey = `captured_${unitType}_${targetPlayerName}`;
} else if (event.message === "events_display.unit_lost") {
groupKey = `lost_${unitType}_${targetPlayerName}`;
} else if (event.message === "events_display.missile_intercepted") {
groupKey = `intercepted_${unitType}`;
}

const unitView =
event.unitID !== undefined ? this.game.unit(event.unitID) : undefined;
this.addEvent({
description: description,
description,
createdAt: this.game.ticks(),
highlight: true,
type: event.messageType,
unsafeDescription: true,
unitView: unitView,
unitView:
event.unitID !== undefined ? this.game.unit(event.unitID) : undefined,
focusID: event.focusPlayerID,
groupKey,
unitType,
targetPlayerName,
});
}

Expand Down Expand Up @@ -530,20 +653,31 @@ export class EventsDisplay extends LitElement implements Controller {

onUnitIncomingEvent(event: UnitIncomingUpdate) {
const myPlayer = this.game.myPlayer();

if (!myPlayer || myPlayer.smallID() !== event.playerID) {
return;
if (!myPlayer || myPlayer.smallID() !== event.playerID) return;

const parsed = parseInboundNuke(event.message);
let description = event.message;
if (parsed) {
const k =
"events_display." +
parsed.nukeType +
(parsed.nukeType === "mirv" ? "" : "_bomb") +
"_inbound";
description = translateText(k, { name: parsed.player });
}

const unitView = this.game.unit(event.unitID);

this.addEvent({
description: event.message,
description,
type: event.messageType,
unsafeDescription: false,
highlight: true,
createdAt: this.game.ticks(),
unitView: unitView,
unitView: this.game.unit(event.unitID),
groupKey: parsed
? `inbound_${parsed.nukeType}_${parsed.player}`
: undefined,
unitType: parsed?.nukeType,
targetPlayerName: parsed?.player,
});
}

Expand Down
24 changes: 22 additions & 2 deletions src/core/game/UnitImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
AllUnitParams,
MessageType,
Player,
Structures,
Tick,
TrainType,
TrajectoryTile,
Expand Down Expand Up @@ -216,6 +217,24 @@ export class UnitImpl implements Unit {
this.mg.stats().unitLose(this._owner, this._type);
break;
}
if (Structures.has(this._type)) {
this.mg.displayMessage(
"events_display.unit_captured",
MessageType.CAPTURED_ENEMY_UNIT,
newOwner.id(),
undefined,
{ unit: this._type, name: this._owner.displayName() },
this.id(),
);
this.mg.displayMessage(
"events_display.unit_lost",
MessageType.UNIT_DESTROYED,
this._owner.id(),
undefined,
{ unit: this._type, name: newOwner.displayName() },
this.id(),
);
}
this._lastOwner = this._owner;
this._lastOwner._units = this._lastOwner._units.filter((u) => u !== this);
this._owner = newOwner;
Expand Down Expand Up @@ -326,11 +345,12 @@ export class UnitImpl implements Unit {
}

private displayMessageOnDeleted(): void {
// Only warships and transport ships are worth notifying about; everything
// Only warships, transport ships, and structures are worth notifying about; everything
// else is either visible on the map or too low-stakes to surface.
if (
this._type !== UnitType.Warship &&
this._type !== UnitType.TransportShip
this._type !== UnitType.TransportShip &&
!Structures.has(this._type)
) {
return;
}
Expand Down
Loading