Files
OpenFrontIO/src/client/graphics/layers/AlertFrame.ts
T
maxime.io 629ce68604 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
2025-06-23 19:15:30 +00:00

133 lines
2.9 KiB
TypeScript

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>
`;
}
}