From a24710a9f5cc1d49e5c07205aef2fa288fe9f5ec Mon Sep 17 00:00:00 2001 From: Rj Manhas <117674421+RjManhas@users.noreply.github.com> Date: Tue, 11 Nov 2025 14:39:01 -0700 Subject: [PATCH] A timer and icon flashing for betryal debuff (#2430) Resolves #2096 ## Description: (TO CLEARIFY THEIRS NO GRACE PERIOD ADDED, AS THAT ISSUE THAT WOULD OF NEEDED IT WAS FIXED BEFORE ON ITS OWN) Shows the amount left in the UI for the player who trigged it image also the betryal icon, after 15 seconds starts a slow flash, then after 10 seconds it speeds up, and then at 5 seconds it quickly flashs. this was a nice way to show the time left without adding any new ui componets. video link 36 seconds (https://streamable.com/cwzxch) ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] 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: notifxy (1379678982676676639) --- resources/lang/en.json | 3 +- src/client/graphics/layers/EventsDisplay.ts | 52 ++++++++++++++++++- src/client/graphics/layers/NameLayer.ts | 57 ++++++++++++++++++--- 3 files changed, 103 insertions(+), 9 deletions(-) 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();