From f9a8ee71e19c4fff5a0b1e4571118221266dd139 Mon Sep 17 00:00:00 2001 From: Rj Manhas Date: Tue, 11 Nov 2025 11:06:20 -0700 Subject: [PATCH] feat: for attacker, adds boat countdown --- src/client/graphics/layers/EventsDisplay.ts | 116 +++++++++++++++++--- 1 file changed, 98 insertions(+), 18 deletions(-) diff --git a/src/client/graphics/layers/EventsDisplay.ts b/src/client/graphics/layers/EventsDisplay.ts index 0d5725e7c..4f4b38c25 100644 --- a/src/client/graphics/layers/EventsDisplay.ts +++ b/src/client/graphics/layers/EventsDisplay.ts @@ -133,6 +133,52 @@ export class EventsDisplay extends LitElement implements Layer { `; } + /** + * Calculate time remaining in seconds for a boat to reach its target. + * Boats move 1 tile per tick (10 ticks per second). + */ + private getBoatTimeRemaining(boat: UnitView): number | null { + const targetTile = boat.targetTile(); + if (!targetTile || boat.reachedTarget() || !boat.isActive()) { + return null; + } + const distance = this.game.manhattanDist(boat.tile(), targetTile); + return Math.ceil(distance / 10); + } + + /** + * Find the boat unit associated with an attack. + * Matches boats by owner, target territory, and optionally troop count. + */ + private findBoatForAttack( + attackerID: number, + targetID: number, + troops: number, + ): UnitView | null { + const attacker = this.game.playerBySmallID(attackerID) as PlayerView; + const target = this.game.playerBySmallID(targetID) as PlayerView; + if (!attacker || !target) return null; + + const boats = this.game + .units(UnitType.TransportShip) + .filter((u) => u.owner().smallID() === attackerID && u.isActive()); + + for (const boat of boats) { + const targetTile = boat.targetTile(); + if (!targetTile) continue; + + const targetOwnerSmallID = this.game.ownerID(targetTile); + if (targetOwnerSmallID === target.smallID()) { + // Prefer exact troop match, but accept any boat heading to target if no match + if (troops === 0 || Math.abs(boat.troops() - troops) < 10) { + return boat; + } + } + } + + return null; + } + private renderToggleButton(src: string, category: MessageCategory) { // Adding the literal for the default size ensures tailwind will generate the class const toggleButtonSizeMap = { default: "h-5" }; @@ -727,9 +773,9 @@ export class EventsDisplay extends LitElement implements Layer { // Format the message for NAVAL_INVASION_INBOUND to display formatted troop count let description = event.message; if (event.messageType === MessageType.NAVAL_INVASION_INBOUND && unitView) { - // Replace raw troop count with formatted version - // Message format: "Boat: {troops} {attackerName}" - const formattedTroops = renderTroops(unitView.troops()); + const actualTroops = Math.round(Math.round(unitView.troops()) / 10); + const cleanTroopsValue = actualTroops * 10; + const formattedTroops = renderTroops(cleanTroopsValue); description = event.message.replace( /Boat: \d+/, `Boat: ${formattedTroops}`, @@ -800,15 +846,30 @@ export class EventsDisplay extends LitElement implements Layer { } } + /** + * Render troops as a whole number (no decimals) for incoming attacks. + */ + private renderTroopsWholeNumber(troops: number): string { + const actualTroops = Math.round(Math.round(troops) / 10); + + if (actualTroops >= 1_000_000) { + return Math.floor(actualTroops / 1_000_000) + "M"; + } + if (actualTroops >= 1_000) { + return Math.floor(actualTroops / 1_000) + "K"; + } + return actualTroops.toString(); + } + private renderIncomingAttacks() { return html` ${this.incomingAttacks.length > 0 ? html` - ${this.incomingAttacks.map( - (attack) => html` + ${this.incomingAttacks.map((attack) => { + return html` ${this.renderButton({ content: html` - ${renderTroops(attack.troops)} + ${this.renderTroopsWholeNumber(attack.troops)} ${( this.game.playerBySmallID(attack.attackerID) as PlayerView )?.name()} @@ -820,8 +881,8 @@ export class EventsDisplay extends LitElement implements Layer { className: "text-left text-red-400", translate: false, })} - `, - )} + `; + })} ` : ""} `; @@ -832,12 +893,26 @@ export class EventsDisplay extends LitElement implements Layer { ${this.outgoingAttacks.length > 0 ? html`
- ${this.outgoingAttacks.map( - (attack) => html` + ${this.outgoingAttacks.map((attack) => { + const myPlayer = this.game.myPlayer(); + const boat = myPlayer + ? this.findBoatForAttack( + myPlayer.smallID(), + attack.targetID, + attack.troops, + ) + : null; + const timeRemaining = + boat && !attack.retreating + ? this.getBoatTimeRemaining(boat) + : null; + + return html`
${this.renderButton({ content: html` - ${renderTroops(attack.troops)} + ${renderTroops(Math.round(attack.troops))} + ${timeRemaining !== null ? ` ${timeRemaining}s` : ""} ${( this.game.playerBySmallID( attack.targetID, @@ -861,8 +936,8 @@ export class EventsDisplay extends LitElement implements Layer { )}...)`}
- `, - )} + `; + })}
` : ""} @@ -910,12 +985,17 @@ export class EventsDisplay extends LitElement implements Layer { ${this.outgoingBoats.length > 0 ? html`
- ${this.outgoingBoats.map( - (boat) => html` + ${this.outgoingBoats.map((boat) => { + const timeRemaining = !boat.retreating() + ? this.getBoatTimeRemaining(boat) + : null; + + return html`
${this.renderButton({ content: html`${translateText("events_display.boat")}: - ${renderTroops(boat.troops())}`, + ${renderTroops(boat.troops())} + ${timeRemaining !== null ? ` ${timeRemaining}s` : ""}`, onClick: () => this.emitGoToUnitEvent(boat), className: "text-left text-blue-400", translate: false, @@ -933,8 +1013,8 @@ export class EventsDisplay extends LitElement implements Layer { )}...)`}
- `, - )} + `; + })}
` : ""}