mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-30 11:12:18 +00:00
6ca81211ea
## Description: The red alert frame for betrayals was added in https://github.com/openfrontio/OpenFrontIO/pull/1195. It also flashes red for incoming land attacks since https://github.com/openfrontio/OpenFrontIO/pull/2358. The same color for betrayals and attacks leads to confusion. And possibly red alert fatigue. But when players find themselves fatigued and want to shut it off for awhile, they can't because the setting doesn't exist in-game. Also, the setting description on the homepage settings didn't yet reflect that the alert frame flashes for attacks too. This PR fixes this by: - making the color for land attacks orange. This is well discernable from red for various colorblindness types, while still looking alarming. - adding the setting to in-game SettingsModal - adding land attack to setting description Reference to comments on it on Dev Discord: https://discord.com/channels/1359946986937258015/1381347989464809664/1441232666065240064 https://discord.com/channels/1359946986937258015/1360078040222142564/1434574256704061523 Orange alert frame on being attacked over land: https://github.com/user-attachments/assets/e0772d62-5b25-4213-a393-dd5af13e8bc9 Settings description change and addition to in-game toggles: <img width="560" height="160" alt="Added to description what was added in PR 2358" src="https://github.com/user-attachments/assets/bc6e2206-b7ac-498d-9009-d2b6e302d3cf" /> <img width="665" height="425" alt="In SettingsModal and with attacks added to description" src="https://github.com/user-attachments/assets/d489830c-e359-4a5f-8eb4-3caa7d0c21b2" /> ## 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: tryout33
487 lines
17 KiB
TypeScript
487 lines
17 KiB
TypeScript
import { html, LitElement } from "lit";
|
||
import { customElement, property, query, state } from "lit/decorators.js";
|
||
import structureIcon from "../../../../resources/images/CityIconWhite.svg";
|
||
import darkModeIcon from "../../../../resources/images/DarkModeIconWhite.svg";
|
||
import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg";
|
||
import exitIcon from "../../../../resources/images/ExitIconWhite.svg";
|
||
import explosionIcon from "../../../../resources/images/ExplosionIconWhite.svg";
|
||
import mouseIcon from "../../../../resources/images/MouseIconWhite.svg";
|
||
import ninjaIcon from "../../../../resources/images/NinjaIconWhite.svg";
|
||
import settingsIcon from "../../../../resources/images/SettingIconWhite.svg";
|
||
import sirenIcon from "../../../../resources/images/SirenIconWhite.svg";
|
||
import treeIcon from "../../../../resources/images/TreeIconWhite.svg";
|
||
import musicIcon from "../../../../resources/images/music.svg";
|
||
import { EventBus } from "../../../core/EventBus";
|
||
import { UserSettings } from "../../../core/game/UserSettings";
|
||
import { AlternateViewEvent, RefreshGraphicsEvent } from "../../InputHandler";
|
||
import { PauseGameEvent } from "../../Transport";
|
||
import { translateText } from "../../Utils";
|
||
import SoundManager from "../../sound/SoundManager";
|
||
import { Layer } from "./Layer";
|
||
|
||
export class ShowSettingsModalEvent {
|
||
constructor(
|
||
public readonly isVisible: boolean = true,
|
||
public readonly shouldPause: boolean = false,
|
||
public readonly isPaused: boolean = false,
|
||
) {}
|
||
}
|
||
|
||
@customElement("settings-modal")
|
||
export class SettingsModal extends LitElement implements Layer {
|
||
public eventBus: EventBus;
|
||
public userSettings: UserSettings;
|
||
|
||
@state()
|
||
private isVisible: boolean = false;
|
||
|
||
@state()
|
||
private alternateView: boolean = false;
|
||
|
||
@query(".modal-overlay")
|
||
private modalOverlay!: HTMLElement;
|
||
|
||
@property({ type: Boolean })
|
||
shouldPause = false;
|
||
|
||
@property({ type: Boolean })
|
||
wasPausedWhenOpened = false;
|
||
|
||
init() {
|
||
SoundManager.setBackgroundMusicVolume(
|
||
this.userSettings.backgroundMusicVolume(),
|
||
);
|
||
SoundManager.setSoundEffectsVolume(this.userSettings.soundEffectsVolume());
|
||
this.eventBus.on(ShowSettingsModalEvent, (event) => {
|
||
this.isVisible = event.isVisible;
|
||
this.shouldPause = event.shouldPause;
|
||
this.wasPausedWhenOpened = event.isPaused;
|
||
this.pauseGame(true);
|
||
});
|
||
}
|
||
|
||
createRenderRoot() {
|
||
return this;
|
||
}
|
||
|
||
connectedCallback() {
|
||
super.connectedCallback();
|
||
window.addEventListener("click", this.handleOutsideClick, true);
|
||
window.addEventListener("keydown", this.handleKeyDown);
|
||
}
|
||
|
||
disconnectedCallback() {
|
||
window.removeEventListener("click", this.handleOutsideClick, true);
|
||
window.removeEventListener("keydown", this.handleKeyDown);
|
||
super.disconnectedCallback();
|
||
}
|
||
|
||
private handleOutsideClick = (event: MouseEvent) => {
|
||
if (
|
||
this.isVisible &&
|
||
this.modalOverlay &&
|
||
event.target === this.modalOverlay
|
||
) {
|
||
this.closeModal();
|
||
}
|
||
};
|
||
|
||
private handleKeyDown = (event: KeyboardEvent) => {
|
||
if (this.isVisible && event.key === "Escape") {
|
||
this.closeModal();
|
||
}
|
||
};
|
||
|
||
public openModal() {
|
||
this.isVisible = true;
|
||
document.body.style.overflow = "hidden";
|
||
this.requestUpdate();
|
||
}
|
||
|
||
public closeModal() {
|
||
this.isVisible = false;
|
||
document.body.style.overflow = "";
|
||
this.requestUpdate();
|
||
this.pauseGame(false);
|
||
}
|
||
|
||
private pauseGame(pause: boolean) {
|
||
if (this.shouldPause && !this.wasPausedWhenOpened)
|
||
this.eventBus.emit(new PauseGameEvent(pause));
|
||
}
|
||
|
||
private onTerrainButtonClick() {
|
||
this.alternateView = !this.alternateView;
|
||
this.eventBus.emit(new AlternateViewEvent(this.alternateView));
|
||
this.requestUpdate();
|
||
}
|
||
|
||
private onToggleEmojisButtonClick() {
|
||
this.userSettings.toggleEmojis();
|
||
this.requestUpdate();
|
||
}
|
||
|
||
private onToggleStructureSpritesButtonClick() {
|
||
this.userSettings.toggleStructureSprites();
|
||
this.requestUpdate();
|
||
}
|
||
|
||
private onToggleSpecialEffectsButtonClick() {
|
||
this.userSettings.toggleFxLayer();
|
||
this.requestUpdate();
|
||
}
|
||
|
||
private onToggleAlertFrameButtonClick() {
|
||
this.userSettings.toggleAlertFrame();
|
||
this.requestUpdate();
|
||
}
|
||
|
||
private onToggleDarkModeButtonClick() {
|
||
this.userSettings.toggleDarkMode();
|
||
this.eventBus.emit(new RefreshGraphicsEvent());
|
||
this.requestUpdate();
|
||
}
|
||
|
||
private onToggleRandomNameModeButtonClick() {
|
||
this.userSettings.toggleRandomName();
|
||
this.requestUpdate();
|
||
}
|
||
|
||
private onToggleLeftClickOpensMenu() {
|
||
this.userSettings.toggleLeftClickOpenMenu();
|
||
this.requestUpdate();
|
||
}
|
||
|
||
private onTogglePerformanceOverlayButtonClick() {
|
||
this.userSettings.togglePerformanceOverlay();
|
||
this.requestUpdate();
|
||
}
|
||
|
||
private onExitButtonClick() {
|
||
// redirect to the home page
|
||
window.location.href = "/";
|
||
}
|
||
|
||
private onVolumeChange(event: Event) {
|
||
const volume = parseFloat((event.target as HTMLInputElement).value) / 100;
|
||
this.userSettings.setBackgroundMusicVolume(volume);
|
||
SoundManager.setBackgroundMusicVolume(volume);
|
||
this.requestUpdate();
|
||
}
|
||
|
||
private onSoundEffectsVolumeChange(event: Event) {
|
||
const volume = parseFloat((event.target as HTMLInputElement).value) / 100;
|
||
this.userSettings.setSoundEffectsVolume(volume);
|
||
SoundManager.setSoundEffectsVolume(volume);
|
||
this.requestUpdate();
|
||
}
|
||
|
||
render() {
|
||
if (!this.isVisible) {
|
||
return null;
|
||
}
|
||
|
||
return html`
|
||
<div
|
||
class="modal-overlay fixed inset-0 bg-black/50 backdrop-blur-sm z-[2000] flex items-center justify-center p-4"
|
||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||
>
|
||
<div
|
||
class="bg-slate-800 border border-slate-600 rounded-lg shadow-xl max-w-md w-full max-h-[80vh] overflow-y-auto"
|
||
>
|
||
<div
|
||
class="flex items-center justify-between p-4 border-b border-slate-600"
|
||
>
|
||
<div class="flex items-center gap-2">
|
||
<img
|
||
src=${settingsIcon}
|
||
alt="settings"
|
||
width="24"
|
||
height="24"
|
||
style="vertical-align: middle;"
|
||
/>
|
||
<h2 class="text-xl font-semibold text-white">
|
||
${translateText("user_setting.tab_basic")}
|
||
</h2>
|
||
</div>
|
||
<button
|
||
class="text-slate-400 hover:text-white text-2xl font-bold leading-none"
|
||
@click=${this.closeModal}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
<div class="p-4 space-y-3">
|
||
<div
|
||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||
>
|
||
<img src=${musicIcon} alt="musicIcon" width="20" height="20" />
|
||
<div class="flex-1">
|
||
<div class="font-medium">
|
||
${translateText("user_setting.background_music_volume")}
|
||
</div>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="100"
|
||
.value=${this.userSettings.backgroundMusicVolume() * 100}
|
||
@input=${this.onVolumeChange}
|
||
class="w-full border border-slate-500 rounded-lg"
|
||
/>
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${Math.round(this.userSettings.backgroundMusicVolume() * 100)}%
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||
>
|
||
<img
|
||
src=${musicIcon}
|
||
alt="soundEffectsIcon"
|
||
width="20"
|
||
height="20"
|
||
/>
|
||
<div class="flex-1">
|
||
<div class="font-medium">
|
||
${translateText("user_setting.sound_effects_volume")}
|
||
</div>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="100"
|
||
.value=${this.userSettings.soundEffectsVolume() * 100}
|
||
@input=${this.onSoundEffectsVolumeChange}
|
||
class="w-full border border-slate-500 rounded-lg"
|
||
/>
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${Math.round(this.userSettings.soundEffectsVolume() * 100)}%
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||
@click="${this.onTerrainButtonClick}"
|
||
>
|
||
<img src=${treeIcon} alt="treeIcon" width="20" height="20" />
|
||
<div class="flex-1">
|
||
<div class="font-medium">
|
||
${translateText("user_setting.toggle_terrain")}
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${translateText("user_setting.toggle_view_desc")}
|
||
</div>
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${this.alternateView
|
||
? translateText("user_setting.on")
|
||
: translateText("user_setting.off")}
|
||
</div>
|
||
</button>
|
||
|
||
<button
|
||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||
@click="${this.onToggleEmojisButtonClick}"
|
||
>
|
||
<img src=${emojiIcon} alt="emojiIcon" width="20" height="20" />
|
||
<div class="flex-1">
|
||
<div class="font-medium">
|
||
${translateText("user_setting.emojis_label")}
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${translateText("user_setting.emojis_desc")}
|
||
</div>
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${this.userSettings.emojis()
|
||
? translateText("user_setting.on")
|
||
: translateText("user_setting.off")}
|
||
</div>
|
||
</button>
|
||
|
||
<button
|
||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||
@click="${this.onToggleDarkModeButtonClick}"
|
||
>
|
||
<img
|
||
src=${darkModeIcon}
|
||
alt="darkModeIcon"
|
||
width="20"
|
||
height="20"
|
||
/>
|
||
<div class="flex-1">
|
||
<div class="font-medium">
|
||
${translateText("user_setting.dark_mode_label")}
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${translateText("user_setting.dark_mode_desc")}
|
||
</div>
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${this.userSettings.darkMode()
|
||
? translateText("user_setting.on")
|
||
: translateText("user_setting.off")}
|
||
</div>
|
||
</button>
|
||
|
||
<button
|
||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||
@click="${this.onToggleSpecialEffectsButtonClick}"
|
||
>
|
||
<img
|
||
src=${explosionIcon}
|
||
alt="specialEffects"
|
||
width="20"
|
||
height="20"
|
||
/>
|
||
<div class="flex-1">
|
||
<div class="font-medium">
|
||
${translateText("user_setting.special_effects_label")}
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${translateText("user_setting.special_effects_desc")}
|
||
</div>
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${this.userSettings.fxLayer()
|
||
? translateText("user_setting.on")
|
||
: translateText("user_setting.off")}
|
||
</div>
|
||
</button>
|
||
|
||
<button
|
||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||
@click="${this.onToggleAlertFrameButtonClick}"
|
||
>
|
||
<img src=${sirenIcon} alt="alertFrame" width="20" height="20" />
|
||
<div class="flex-1">
|
||
<div class="font-medium">
|
||
${translateText("user_setting.alert_frame_label")}
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${translateText("user_setting.alert_frame_desc")}
|
||
</div>
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${this.userSettings.alertFrame()
|
||
? translateText("user_setting.on")
|
||
: translateText("user_setting.off")}
|
||
</div>
|
||
</button>
|
||
|
||
<button
|
||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||
@click="${this.onToggleStructureSpritesButtonClick}"
|
||
>
|
||
<img
|
||
src=${structureIcon}
|
||
alt="structureSprites"
|
||
width="20"
|
||
height="20"
|
||
/>
|
||
<div class="flex-1">
|
||
<div class="font-medium">
|
||
${translateText("user_setting.structure_sprites_label")}
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${translateText("user_setting.structure_sprites_desc")}
|
||
</div>
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${this.userSettings.structureSprites()
|
||
? translateText("user_setting.on")
|
||
: translateText("user_setting.off")}
|
||
</div>
|
||
</button>
|
||
|
||
<button
|
||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||
@click="${this.onToggleRandomNameModeButtonClick}"
|
||
>
|
||
<img src=${ninjaIcon} alt="ninjaIcon" width="20" height="20" />
|
||
<div class="flex-1">
|
||
<div class="font-medium">
|
||
${translateText("user_setting.anonymous_names_label")}
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${translateText("user_setting.anonymous_names_desc")}
|
||
</div>
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${this.userSettings.anonymousNames()
|
||
? translateText("user_setting.on")
|
||
: translateText("user_setting.off")}
|
||
</div>
|
||
</button>
|
||
|
||
<button
|
||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||
@click="${this.onToggleLeftClickOpensMenu}"
|
||
>
|
||
<img src=${mouseIcon} alt="mouseIcon" width="20" height="20" />
|
||
<div class="flex-1">
|
||
<div class="font-medium">
|
||
${translateText("user_setting.left_click_menu")}
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${translateText("user_setting.left_click_desc")}
|
||
</div>
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${this.userSettings.leftClickOpensMenu()
|
||
? translateText("user_setting.on")
|
||
: translateText("user_setting.off")}
|
||
</div>
|
||
</button>
|
||
|
||
<button
|
||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
|
||
@click="${this.onTogglePerformanceOverlayButtonClick}"
|
||
>
|
||
<img
|
||
src=${settingsIcon}
|
||
alt="performanceIcon"
|
||
width="20"
|
||
height="20"
|
||
/>
|
||
<div class="flex-1">
|
||
<div class="font-medium">
|
||
${translateText("user_setting.performance_overlay_label")}
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${translateText("user_setting.performance_overlay_desc")}
|
||
</div>
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${this.userSettings.performanceOverlay()
|
||
? translateText("user_setting.on")
|
||
: translateText("user_setting.off")}
|
||
</div>
|
||
</button>
|
||
|
||
<div class="border-t border-slate-600 pt-3 mt-4">
|
||
<button
|
||
class="flex gap-3 items-center w-full text-left p-3 hover:bg-red-600/20 rounded text-red-400 transition-colors"
|
||
@click="${this.onExitButtonClick}"
|
||
>
|
||
<img src=${exitIcon} alt="exitIcon" width="20" height="20" />
|
||
<div class="flex-1">
|
||
<div class="font-medium">
|
||
${translateText("user_setting.exit_game_label")}
|
||
</div>
|
||
<div class="text-sm text-slate-400">
|
||
${translateText("user_setting.exit_game_info")}
|
||
</div>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|