+ ${translateText("cosmetics.free", {
+ numFree: pack.bonusAmount.toLocaleString(),
+ })}
+
`
+ : nothing}
`;
}
diff --git a/src/client/components/DesktopNavBar.ts b/src/client/components/DesktopNavBar.ts
index 7b149df948..5786c80578 100644
--- a/src/client/components/DesktopNavBar.ts
+++ b/src/client/components/DesktopNavBar.ts
@@ -124,7 +124,7 @@ export class DesktopNavBar extends LitElement {
data-i18n="main.leaderboard"
>
diff --git a/src/client/components/MobileNavBar.ts b/src/client/components/MobileNavBar.ts
index aeaeb67228..b6b682911e 100644
--- a/src/client/components/MobileNavBar.ts
+++ b/src/client/components/MobileNavBar.ts
@@ -115,7 +115,7 @@ export class MobileNavBar extends LitElement {
data-i18n="main.leaderboard"
>
+ ${this.isWilderness || this.isIrradiatedWilderness
+ ? html`
+ ${translateText(
+ this.isIrradiatedWilderness
+ ? "player_info_overlay.irradiated_wilderness_title"
+ : "player_info_overlay.wilderness_title",
+ )}
+
`
+ : ""}
${this.player !== null ? this.renderPlayerInfo(this.player) : ""}
${this.unit !== null ? this.renderUnitInfo(this.unit) : ""}
diff --git a/src/client/graphics/layers/RendererStatusPanel.ts b/src/client/graphics/layers/RendererStatusPanel.ts
new file mode 100644
index 0000000000..437b56a325
--- /dev/null
+++ b/src/client/graphics/layers/RendererStatusPanel.ts
@@ -0,0 +1,445 @@
+import { css, html, LitElement } from "lit";
+import { customElement, property, state } from "lit/decorators.js";
+import {
+ TERRITORY_RENDERER_KEY,
+ USER_SETTINGS_CHANGED_EVENT,
+ UserSettings,
+} from "../../../core/game/UserSettings";
+import { Layer } from "./Layer";
+import {
+ TERRITORY_RENDERER_OPTIONS,
+ TERRITORY_RENDERER_STATUS_EVENT,
+ TerritoryRendererId,
+ TerritoryRendererPreference,
+ TerritoryRendererStatus,
+} from "./TerritoryBackend";
+
+@customElement("renderer-status-panel")
+export class RendererStatusPanel extends LitElement implements Layer {
+ @property({ type: Object })
+ public userSettings!: UserSettings;
+
+ @state()
+ private activeRenderer: TerritoryRendererId | null = null;
+
+ @state()
+ private preference: TerritoryRendererPreference = "auto";
+
+ @state()
+ private failedBackends: TerritoryRendererId[] = [];
+
+ @state()
+ private message: string | null = null;
+
+ @state()
+ private position: { x: number; y: number } | null = null;
+
+ @state()
+ private isDragging = false;
+
+ private dragState: {
+ pointerId: number;
+ offsetX: number;
+ offsetY: number;
+ } | null = null;
+
+ private readonly positionStorageKey = "rendererStatusPanel.position.v1";
+
+ static styles = css`
+ .panel {
+ position: fixed;
+ left: 16px;
+ bottom: 16px;
+ z-index: 9998;
+ width: min(280px, calc(100vw - 32px));
+ box-sizing: border-box;
+ border: 1px solid rgba(255, 255, 255, 0.16);
+ border-radius: 8px;
+ background: rgba(13, 16, 20, 0.86);
+ color: rgba(255, 255, 255, 0.92);
+ font-family:
+ Inter,
+ ui-sans-serif,
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ sans-serif;
+ font-size: 12px;
+ line-height: 1.35;
+ pointer-events: auto;
+ user-select: none;
+ box-shadow: 0 14px 32px rgba(0, 0, 0, 0.24);
+ backdrop-filter: blur(10px);
+ }
+
+ .panel.dragging {
+ opacity: 0.72;
+ }
+
+ .title {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 8px 10px 6px;
+ cursor: grab;
+ touch-action: none;
+ font-weight: 700;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ }
+
+ .titleActions {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+
+ button {
+ border: 1px solid rgba(255, 255, 255, 0.16);
+ border-radius: 6px;
+ background: rgba(255, 255, 255, 0.08);
+ color: rgba(255, 255, 255, 0.9);
+ padding: 4px 7px;
+ font: inherit;
+ cursor: pointer;
+ }
+
+ button:hover:not(:disabled) {
+ background: rgba(255, 255, 255, 0.16);
+ }
+
+ button:disabled {
+ opacity: 0.45;
+ cursor: default;
+ }
+
+ .panel.dragging .title {
+ cursor: grabbing;
+ }
+
+ .body {
+ display: grid;
+ gap: 7px;
+ padding: 8px 10px 10px;
+ }
+
+ .row {
+ display: grid;
+ grid-template-columns: 72px 1fr;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .label {
+ color: rgba(255, 255, 255, 0.62);
+ }
+
+ .value {
+ min-width: 0;
+ color: rgba(255, 255, 255, 0.94);
+ font-weight: 650;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .active {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ }
+
+ .dot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: rgb(67, 214, 142);
+ box-shadow: 0 0 0 3px rgba(67, 214, 142, 0.16);
+ }
+
+ select {
+ width: 100%;
+ min-width: 0;
+ border: 1px solid rgba(255, 255, 255, 0.16);
+ border-radius: 6px;
+ background: rgba(0, 0, 0, 0.38);
+ color: rgba(255, 255, 255, 0.94);
+ padding: 5px 7px;
+ font: inherit;
+ outline: none;
+ }
+
+ .note {
+ color: rgba(255, 255, 255, 0.66);
+ overflow-wrap: anywhere;
+ }
+ `;
+
+ init() {
+ this.preference = this.userSettings.territoryRenderer();
+ this.restorePosition();
+ globalThis.addEventListener(
+ TERRITORY_RENDERER_STATUS_EVENT,
+ this.handleRendererStatus,
+ );
+ globalThis.addEventListener(
+ `${USER_SETTINGS_CHANGED_EVENT}:${TERRITORY_RENDERER_KEY}`,
+ this.handlePreferenceChanged,
+ );
+ this.requestUpdate();
+ }
+
+ disconnectedCallback(): void {
+ super.disconnectedCallback();
+ this.endDrag();
+ globalThis.removeEventListener(
+ TERRITORY_RENDERER_STATUS_EVENT,
+ this.handleRendererStatus,
+ );
+ globalThis.removeEventListener(
+ `${USER_SETTINGS_CHANGED_EVENT}:${TERRITORY_RENDERER_KEY}`,
+ this.handlePreferenceChanged,
+ );
+ }
+
+ private readonly handleRendererStatus = (event: Event) => {
+ const detail = (event as CustomEvent