From e2fd4661a6686ab76f2efbd7e3577f434dbf282b Mon Sep 17 00:00:00 2001 From: Blon Td Date: Tue, 7 Jul 2026 20:41:40 -0400 Subject: [PATCH 1/2] Group event notifications and inbound nukes in quick succession --- resources/lang/en.json | 8 ++ src/client/hud/layers/EventsDisplay.ts | 103 ++++++++++++++++++------- src/core/game/UnitImpl.ts | 24 +++++- 3 files changed, 106 insertions(+), 29 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index eb1208cb4f..5727a26d99 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -460,6 +460,7 @@ "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_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", @@ -470,7 +471,9 @@ "duration_seconds_plural": "{seconds} seconds", "focus": "Focus", "hydrogen_bomb_detonated": "{name} - hydrogen bomb detonated", + "hydrogen_bomb_inbound_plural": "{name} - {count} hydrogen bombs inbound", "ignore": "Ignore", + "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}", "no_boats_available": "No boats available, max {max}", @@ -486,7 +489,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" }, diff --git a/src/client/hud/layers/EventsDisplay.ts b/src/client/hud/layers/EventsDisplay.ts index 042469ff24..4106a0aee2 100644 --- a/src/client/hud/layers/EventsDisplay.ts +++ b/src/client/hud/layers/EventsDisplay.ts @@ -32,6 +32,22 @@ import { translateText, } from "../../Utils"; +const pluralizeUnit = (u: string) => u === "City" ? "Cities" : u === "Factory" ? "Factories" : u + "s"; + +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_plural", + "events_display.hydrogen_bomb_inbound_plural", + "events_display.mirv_inbound_plural", +]; + interface GameEvent { description: string; unsafeDescription?: boolean; @@ -41,6 +57,10 @@ interface GameEvent { onDelete?: () => void; focusID?: number; unitView?: UnitView; + groupKey?: string; + count?: number; + unitType?: string; + targetPlayerName?: string; } const TIER_1_TYPES: ReadonlySet = new Set([ @@ -242,40 +262,70 @@ 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 = pluralizeUnit(existing.unitType ?? ""); + 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 }); + } + this.requestUpdate(); + return; + } + } this.events = [...this.events, event]; this.requestUpdate(); } onDisplayMessageEvent(event: DisplayMessageUpdate) { const myPlayer = this.game.myPlayer(); - 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") { - return; - } - - let description: string = event.message; - if (event.message.startsWith("events_display.")) { - description = translateText(event.message, event.params ?? {}); + if (event.playerID !== null && (!myPlayer || myPlayer.smallID() !== event.playerID)) return; + if (event.message === "events_display.received_gold_from_captured_ship") return; + + const description = event.message.startsWith("events_display.") + ? translateText(event.message, event.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}`; } - 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, }); } @@ -530,20 +580,19 @@ 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 unitView = this.game.unit(event.unitID); - + const parsed = parseInboundNuke(event.message); this.addEvent({ description: event.message, 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, }); } diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 1daabb9a3b..e70c5726ec 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -3,6 +3,7 @@ import { AllUnitParams, MessageType, Player, + Structures, Tick, TrainType, TrajectoryTile, @@ -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; @@ -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; } From faac6665c224989ea7350f7ff4b37cf659c96dbd Mon Sep 17 00:00:00 2001 From: Blon Td Date: Wed, 8 Jul 2026 18:21:04 -0400 Subject: [PATCH 2/2] fix: translate unit type parameters and singular inbound nukes --- resources/lang/en.json | 16 ++++++++ src/client/hud/layers/EventsDisplay.ts | 51 ++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index 5727a26d99..b802bd9df1 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -460,6 +460,7 @@ "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}", @@ -471,8 +472,10 @@ "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}", @@ -1413,6 +1416,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", diff --git a/src/client/hud/layers/EventsDisplay.ts b/src/client/hud/layers/EventsDisplay.ts index 4106a0aee2..eaf8e0a0fb 100644 --- a/src/client/hud/layers/EventsDisplay.ts +++ b/src/client/hud/layers/EventsDisplay.ts @@ -32,7 +32,25 @@ import { translateText, } from "../../Utils"; -const pluralizeUnit = (u: string) => u === "City" ? "Cities" : u === "Factory" ? "Factories" : u + "s"; +const UNIT_TRANSLATION_KEYS: Record = { + "City": "unit_type.city", + "Port": "unit_type.port", + "Defense Post": "unit_type.defense_post", + "SAM Launcher": "unit_type.sam_launcher", + "Missile Silo": "unit_type.missile_silo", + "Factory": "unit_type.factory", + "Warship": "unit_type.warship", + "Transport": "unit_type.boat", + "Atom Bomb": "unit_type.atom_bomb", + "Hydrogen Bomb": "unit_type.hydrogen_bomb", + "MIRV": "unit_type.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" }; @@ -43,9 +61,23 @@ function parseInboundNuke(m: string): { player: string; nukeType: string } | nul // 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 { @@ -272,7 +304,7 @@ export class EventsDisplay extends LitElement implements Controller { if (event.unitView) existing.unitView = event.unitView; if (event.focusID) existing.focusID = event.focusID; - const u = pluralizeUnit(existing.unitType ?? ""); + const u = getTranslatedUnitName(existing.unitType ?? "", true); const n = existing.targetPlayerName ?? ""; const c = existing.count; @@ -299,8 +331,13 @@ export class EventsDisplay extends LitElement implements Controller { if (event.playerID !== null && (!myPlayer || myPlayer.smallID() !== event.playerID)) return; 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); + } + const description = event.message.startsWith("events_display.") - ? translateText(event.message, event.params ?? {}) + ? translateText(event.message, params) : event.message; let groupKey: string | undefined; @@ -583,8 +620,14 @@ export class EventsDisplay extends LitElement implements Controller { 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 }); + } + this.addEvent({ - description: event.message, + description, type: event.messageType, unsafeDescription: false, highlight: true,