mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-13 16:57:10 +00:00
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.     ## 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:
@@ -240,6 +240,8 @@
|
||||
"dark_mode_desc": "Toggle the site’s 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",
|
||||
|
||||
@@ -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")}"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user