Show a red alert frame when the player is betrayed (#1195)

## Description:

With the alert frame, we're notified and can view the event using a
“Focus” button that moves the map to the traitor, or dismiss it with a
“Dismiss” button. The alert auto-dismisses after a few seconds.

**Because this feature provides an advantage, the trade-off is having to
wait a full 10 seconds or manually clicking “Dismiss” to close it.**

- Show a red alert frame animation when the player is betrayed.
- ~~The alert frame can be dismissed manually.~~
- The alert frame is automatically dismissed after ~~10~~ 3 seconds.
- If the user doesn’t want to see it at all, they can disable it in the
settings.

![Capture d’écran 2025-06-16 à 20 09
51](https://github.com/user-attachments/assets/2897ae16-85d6-4201-ac5b-e60b0cb18add)
![Capture d’écran 2025-06-16 à 20 10
24](https://github.com/user-attachments/assets/088edf08-20fe-4dae-8c00-cbcd24004bd0)
![Capture d’écran 2025-06-16 à 20 10
50](https://github.com/user-attachments/assets/894296aa-c753-429f-8c5d-edd881deec48)
![Capture d’écran 2025-06-16 à 20 11
14](https://github.com/user-attachments/assets/29503958-0042-4614-8db2-2663cadee441)

## 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
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:

This is my first PR let me know if anything needs improvement!

devalnor
This commit is contained in:
maxime.io
2025-06-23 21:15:30 +02:00
committed by GitHub
parent b48770faf0
commit 629ce68604
9 changed files with 193 additions and 2 deletions
+2
View File
@@ -240,6 +240,8 @@
"dark_mode_desc": "Toggle the sites appearance between light and dark themes",
"emojis_label": "😊 Emojis",
"emojis_desc": "Toggle whether emojis are shown in game",
"alert_frame_label": "🚨 Alert Frame",
"alert_frame_desc": "Toggle the alert frame. When enabled, the frame will be displayed when you are betrayed.",
"special_effects_label": "💥 Special effects",
"special_effects_desc": "Toggle special effects. Deactivate to improve performances",
"anonymous_names_label": "🥷 Hidden Names",
+18
View File
@@ -102,6 +102,15 @@ export class UserSettingModal extends LitElement {
console.log("🤡 Emojis:", enabled ? "ON" : "OFF");
}
private toggleAlertFrame(e: CustomEvent<{ checked: boolean }>) {
const enabled = e.detail?.checked;
if (typeof enabled !== "boolean") return;
this.userSettings.set("settings.alertFrame", enabled);
console.log("🚨 Alert frame:", enabled ? "ON" : "OFF");
}
private toggleFxLayer(e: CustomEvent<{ checked: boolean }>) {
const enabled = e.detail?.checked;
if (typeof enabled !== "boolean") return;
@@ -244,6 +253,15 @@ export class UserSettingModal extends LitElement {
@change=${this.toggleEmojis}
></setting-toggle>
<!-- 🚨 Alert frame -->
<setting-toggle
label="${translateText("user_setting.alert_frame_label")}"
description="${translateText("user_setting.alert_frame_desc")}"
id="alert-frame-toggle"
.checked=${this.userSettings.alertFrame()}
@change=${this.toggleAlertFrame}
></setting-toggle>
<!-- 💥 Special effects -->
<setting-toggle
label="${translateText("user_setting.special_effects_label")}"
+8
View File
@@ -5,6 +5,7 @@ import { GameStartingModal } from "../GameStartingModal";
import { RefreshGraphicsEvent as RedrawGraphicsEvent } from "../InputHandler";
import { TransformHandler } from "./TransformHandler";
import { UIState } from "./UIState";
import { AlertFrame } from "./layers/AlertFrame";
import { BuildMenu } from "./layers/BuildMenu";
import { ChatDisplay } from "./layers/ChatDisplay";
import { ChatModal } from "./layers/ChatModal";
@@ -215,6 +216,12 @@ export function createRenderer(
}
gutterAdModal.eventBus = eventBus;
const alertFrame = document.querySelector("alert-frame") as AlertFrame;
if (!(alertFrame instanceof AlertFrame)) {
console.error("alert frame not found");
}
alertFrame.game = game;
const layers: Layer[] = [
new TerrainLayer(game, transformHandler),
new TerritoryLayer(game, eventBus, transformHandler, userSettings),
@@ -252,6 +259,7 @@ export function createRenderer(
multiTabModal,
spawnAd,
gutterAdModal,
alertFrame,
];
return new GameRenderer(
+132
View File
@@ -0,0 +1,132 @@
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import {
BrokeAllianceUpdate,
GameUpdateType,
} from "../../../core/game/GameUpdates";
import { GameView } from "../../../core/game/GameView";
import { UserSettings } from "../../../core/game/UserSettings";
import { Layer } from "./Layer";
// Parameters for the alert animation
const ALERT_SPEED = 1.6;
const ALERT_COUNT = 2;
@customElement("alert-frame")
export class AlertFrame extends LitElement implements Layer {
public game: GameView;
private userSettings: UserSettings = new UserSettings();
@state()
private isActive = false;
private animationTimeout: number | null = null;
static styles = css`
.alert-border {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
border: 17px solid #ee0000;
box-sizing: border-box;
z-index: 40;
opacity: 0;
}
.alert-border.animate {
animation: alertBlink ${ALERT_SPEED}s ease-in-out ${ALERT_COUNT};
}
@keyframes alertBlink {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
`;
constructor() {
super();
if (!document.querySelector("style[data-alert-frame]")) {
const styleEl = document.createElement("style");
styleEl.setAttribute("data-alert-frame", "");
styleEl.textContent = AlertFrame.styles.cssText;
document.head.appendChild(styleEl);
}
}
createRenderRoot() {
return this;
}
init() {
// Listen for BrokeAllianceUpdate events directly from game updates
}
tick() {
if (!this.game) {
return; // Game not initialized yet
}
// Check for BrokeAllianceUpdate events
this.game
.updatesSinceLastTick()
?.[GameUpdateType.BrokeAlliance]?.forEach((update) => {
this.onBrokeAllianceUpdate(update as BrokeAllianceUpdate);
});
}
// The alert frame is not affected by the camera transform
shouldTransform(): boolean {
return false;
}
private onBrokeAllianceUpdate(update: BrokeAllianceUpdate) {
const myPlayer = this.game.myPlayer();
if (!myPlayer) return;
const betrayed = this.game.playerBySmallID(update.betrayedID);
// Only trigger alert if the current player is the betrayed one
if (betrayed === myPlayer) {
this.activateAlert();
}
}
private activateAlert() {
if (this.userSettings.alertFrame()) {
this.isActive = true;
this.requestUpdate();
}
}
public dismissAlert() {
this.isActive = false;
if (this.animationTimeout) {
clearTimeout(this.animationTimeout);
this.animationTimeout = null;
}
this.requestUpdate();
}
render() {
if (!this.isActive) {
return html``;
}
return html`
<div
class="alert-border animate"
@animationend=${() => this.dismissAlert()}
></div>
`;
}
}
@@ -45,6 +45,7 @@ import {
GoToUnitEvent,
} from "./Leaderboard";
import { UserSettings } from "../../../core/game/UserSettings";
import { getMessageTypeClasses, translateText } from "../../Utils";
interface GameEvent {
@@ -72,6 +73,7 @@ export class EventsDisplay extends LitElement implements Layer {
public eventBus: EventBus;
public game: GameView;
private userSettings: UserSettings = new UserSettings();
private active: boolean = false;
private events: GameEvent[] = [];
@state() private incomingAttacks: AttackUpdate[] = [];
@@ -435,12 +437,21 @@ export class EventsDisplay extends LitElement implements Layer {
focusID: update.betrayedID,
});
} else if (betrayed === myPlayer) {
const buttons = [
{
text: "Focus",
className: "btn-gray",
action: () => this.eventBus.emit(new GoToPlayerEvent(traitor)),
preventClose: true,
},
];
this.addEvent({
description: `${traitor.name()} broke their alliance with you`,
type: MessageType.ALLIANCE_BROKEN,
highlight: true,
createdAt: this.game.ticks(),
focusID: update.traitorID,
buttons,
});
}
}
+10
View File
@@ -100,6 +100,11 @@ export class OptionsMenu extends LitElement implements Layer {
this.requestUpdate();
}
private onToggleAlertFrameButtonClick() {
this.userSettings.toggleAlertFrame();
this.requestUpdate();
}
private onToggleSpecialEffectsButtonClick() {
this.userSettings.toggleFxLayer();
this.requestUpdate();
@@ -207,6 +212,11 @@ export class OptionsMenu extends LitElement implements Layer {
title: "Toggle Emojis",
children: "🙂: " + (this.userSettings.emojis() ? "On" : "Off"),
})}
${button({
onClick: this.onToggleAlertFrameButtonClick,
title: "Toggle Alert frame",
children: "🚨: " + (this.userSettings.alertFrame() ? "On" : "Off"),
})}
${button({
onClick: this.onToggleSpecialEffectsButtonClick,
title: "Toggle Special effects",
+2 -2
View File
@@ -4,6 +4,7 @@ import { Theme } from "../../../core/configuration/Config";
import { Tick, UnitType } from "../../../core/game/Game";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView, UnitView } from "../../../core/game/GameView";
import { UserSettings } from "../../../core/game/UserSettings";
import { UnitSelectionEvent } from "../../InputHandler";
import { ProgressBar } from "../ProgressBar";
import { TransformHandler } from "../TransformHandler";
@@ -28,8 +29,8 @@ const PROGRESSBAR_HEIGHT = 3; // Height of a bar
export class UILayer implements Layer {
private canvas: HTMLCanvasElement;
private context: CanvasRenderingContext2D | null;
private theme: Theme | null = null;
private userSettings: UserSettings = new UserSettings();
private selectionAnimTime = 0;
private allProgressBars: Map<
number,
@@ -95,7 +96,6 @@ export class UILayer implements Layer {
if (unitView === undefined) return;
this.onUnitEvent(unitView);
});
this.updateProgressBars();
}
+1
View File
@@ -368,6 +368,7 @@
<player-panel></player-panel>
<help-modal></help-modal>
<dark-mode-button></dark-mode-button>
<alert-frame></alert-frame>
<chat-modal></chat-modal>
<user-setting></user-setting>
<multi-tab-modal></multi-tab-modal>
+9
View File
@@ -17,6 +17,11 @@ export class UserSettings {
emojis() {
return this.get("settings.emojis", true);
}
alertFrame() {
return this.get("settings.alertFrame", true);
}
anonymousNames() {
return this.get("settings.anonymousNames", false);
}
@@ -55,6 +60,10 @@ export class UserSettings {
this.set("settings.emojis", !this.emojis());
}
toggleAlertFrame() {
this.set("settings.alertFrame", !this.alertFrame());
}
toggleRandomName() {
this.set("settings.anonymousNames", !this.anonymousNames());
}