diff --git a/resources/lang/en.json b/resources/lang/en.json index aac1034c2..9e3d78251 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -580,7 +580,8 @@ "alliance_renewed": "Your alliance with {name} has been renewed", "wants_to_renew_alliance": "{name} wants to renew your alliance", "ignore": "Ignore", - "unit_voluntarily_deleted": "Unit voluntarily deleted" + "unit_voluntarily_deleted": "Unit voluntarily deleted", + "betrayal_debuff_ends": "{time} seconds left until betrayal debuff ends" }, "unit_info_modal": { "structure_info": "Structure Info", diff --git a/src/client/graphics/layers/EventsDisplay.ts b/src/client/graphics/layers/EventsDisplay.ts index 1141643ef..803f81e3f 100644 --- a/src/client/graphics/layers/EventsDisplay.ts +++ b/src/client/graphics/layers/EventsDisplay.ts @@ -877,6 +877,30 @@ export class EventsDisplay extends LitElement implements Layer { `; } + private renderBetrayalDebuffTimer() { + const myPlayer = this.game.myPlayer(); + if (!myPlayer || !myPlayer.isTraitor()) { + return html``; + } + + const remainingTicks = myPlayer.getTraitorRemainingTicks(); + const remainingSeconds = Math.ceil(remainingTicks / 10); + + if (remainingSeconds <= 0) { + return html``; + } + + return html` + ${this.renderButton({ + content: html`${translateText("events_display.betrayal_debuff_ends", { + time: remainingSeconds, + })}`, + className: "text-left text-yellow-400", + translate: false, + })} + `; + } + render() { if (!this.active || !this._isVisible) { return html``; @@ -1081,6 +1105,24 @@ export class EventsDisplay extends LitElement implements Layer { ` : ""} + + ${(() => { + const myPlayer = this.game.myPlayer(); + return ( + myPlayer && + myPlayer.isTraitor() && + myPlayer.getTraitorRemainingTicks() > 0 + ); + })() + ? html` + + + ${this.renderBetrayalDebuffTimer()} + + + ` + : ""} + ${this.outgoingAttacks.length > 0 ? html` @@ -1119,7 +1161,15 @@ export class EventsDisplay extends LitElement implements Layer { this.incomingAttacks.length === 0 && this.outgoingAttacks.length === 0 && this.outgoingLandAttacks.length === 0 && - this.outgoingBoats.length === 0 + this.outgoingBoats.length === 0 && + !(() => { + const myPlayer = this.game.myPlayer(); + return ( + myPlayer && + myPlayer.isTraitor() && + myPlayer.getTraitorRemainingTicks() > 0 + ); + })() ? html` this.onAlternateViewChange(e)); } @@ -410,16 +425,44 @@ export class NameLayer implements Layer { } // Traitor icon - const existingTraitor = iconsDiv.querySelector('[data-icon="traitor"]'); + let existingTraitor = iconsDiv.querySelector('[data-icon="traitor"]'); if (render.player.isTraitor()) { + const remainingTicks = render.player.getTraitorRemainingTicks(); + // Use precise seconds (not rounded) for smoother transitions, rounded to 0.5s intervals + const remainingSeconds = Math.round((remainingTicks / 10) * 2) / 2; + if (!existingTraitor) { - iconsDiv.appendChild( - this.createIconElement( - this.traitorIconImage.src, - iconSize, - "traitor", - ), + existingTraitor = this.createIconElement( + this.traitorIconImage.src, + iconSize, + "traitor", ); + iconsDiv.appendChild(existingTraitor); + } + + // Apply flashing animation - smooth speed increase starting at 15s + if (existingTraitor instanceof HTMLImageElement) { + if (remainingSeconds <= 15) { + // Smooth transition: starts at 1s at 15 seconds, decreases to 0.2s at 0 seconds + // Using cubic ease-out for slower, more gradual acceleration + const clampedSeconds = Math.max(0, Math.min(15, remainingSeconds)); + const normalizedTime = clampedSeconds / 15; // 0 to 1 (1 = 15s remaining, 0 = 0s remaining) + + // Cubic ease-out: slower acceleration, smoother transition + const easedProgress = 1 - Math.pow(1 - normalizedTime, 3); + + const maxDuration = 1.0; // Slow flash at 15 seconds + const minDuration = 0.2; // Fast flash at 0 seconds + const duration = + minDuration + (maxDuration - minDuration) * easedProgress; + const animationDuration = `${duration.toFixed(2)}s`; + + existingTraitor.style.animation = `traitorFlash ${animationDuration} infinite`; + existingTraitor.style.animationTimingFunction = "ease-in-out"; + } else { + // Don't flash if more than 15 seconds remaining + existingTraitor.style.animation = "none"; + } } } else if (existingTraitor) { existingTraitor.remove();