Files
OpenFrontIO/src/client/graphics/layers/GameLeftSidebar.ts
T
evanpelle bac29448c2 rename Layer → Controller; drop canvas2D-era interface hooks
The Layer interface dates to the canvas2D era when each entry drew to
the shared 2D context via renderLayer(ctx). With canvas2D gone, nothing
draws there and the renderLayer hook is dead. Rename the interface
("main-thread analog of the worker's Execution") and trim it:

  interface Controller {
    init?: () => void;
    tick?: () => void;
    getTickIntervalMs?: () => number;
  }

renderLayer / shouldTransform / redraw are gone.

Sweep across 28 files: from "./Layer" → "./Controller", implements
Layer → implements Controller, Layer[] / Map<Layer,…> →
Controller[] / Map<Controller,…>. Delete the no-op renderLayer +
shouldTransform method bodies that every layer had inherited.

GameRenderer drops the RedrawGraphicsEvent listener + redraw() fanout
(nothing implements redraw anymore) and the now-unused eventBus
constructor field.

One real case: AttackingTroopsOverlay.renderLayer wasn't a no-op — it
updates DOM label transforms each frame so labels track the WebGL
camera during pan/zoom. Rename to private updateLabelDOM() and start
a self-driven RAF in init() so the per-frame updates keep running.

Class names ending in "Layer" (UILayer, StructureIconsLayer, NameLayer,
etc.) intentionally left as-is — those are separate identifiers and
the class-rename / file-move is a follow-up.

407 tests pass.
2026-05-16 22:35:14 -07:00

206 lines
7.0 KiB
TypeScript

import { Colord } from "colord";
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus } from "../../../core/EventBus";
import { GameMode, Team } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { Platform } from "../../Platform";
import { getTranslatedPlayerTeamLabel, translateText } from "../../Utils";
import { Controller } from "./Controller";
import { ImmunityBarVisibleEvent } from "./ImmunityTimer";
import { SpawnBarVisibleEvent } from "./SpawnTimer";
const leaderboardRegularIcon = assetUrl(
"images/LeaderboardIconRegularWhite.svg",
);
const leaderboardSolidIcon = assetUrl("images/LeaderboardIconSolidWhite.svg");
const teamRegularIcon = assetUrl("images/TeamIconRegularWhite.svg");
const teamSolidIcon = assetUrl("images/TeamIconSolidWhite.svg");
@customElement("game-left-sidebar")
export class GameLeftSidebar extends LitElement implements Controller {
@state()
private isLeaderboardShow = false;
@state()
private isTeamLeaderboardShow = false;
@state()
private isVisible = false;
@state()
private isPlayerTeamLabelVisible = false;
@state()
private playerTeam: Team | null = null;
@state()
private spawnBarVisible = false;
@state()
private immunityBarVisible = false;
private playerColor: Colord = new Colord("#FFFFFF");
public game: GameView;
public eventBus: EventBus;
private _shownOnInit = false;
createRenderRoot() {
return this;
}
init() {
this.isVisible = true;
this.eventBus.on(SpawnBarVisibleEvent, (e) => {
this.spawnBarVisible = e.visible;
});
this.eventBus.on(ImmunityBarVisibleEvent, (e) => {
this.immunityBarVisible = e.visible;
});
if (this.isTeamGame) {
this.isPlayerTeamLabelVisible = true;
}
// Make it visible by default on large screens
if (Platform.isDesktopWidth) {
// lg breakpoint
this._shownOnInit = true;
}
this.requestUpdate();
}
tick() {
if (!this.playerTeam && this.game.myPlayer()?.team()) {
this.playerTeam = this.game.myPlayer()!.team();
if (this.playerTeam) {
this.playerColor = this.game
.config()
.theme()
.teamColor(this.playerTeam);
this.requestUpdate();
}
}
if (this._shownOnInit && !this.game.inSpawnPhase()) {
this._shownOnInit = false;
this.isLeaderboardShow = true;
this.requestUpdate();
}
if (!this.game.inSpawnPhase() && this.isPlayerTeamLabelVisible) {
this.isPlayerTeamLabelVisible = false;
this.requestUpdate();
}
}
private get barOffset(): number {
return (this.spawnBarVisible ? 7 : 0) + (this.immunityBarVisible ? 7 : 0);
}
private toggleLeaderboard(): void {
this.isLeaderboardShow = !this.isLeaderboardShow;
}
private toggleTeamLeaderboard(): void {
this.isTeamLeaderboardShow = !this.isTeamLeaderboardShow;
}
private get isTeamGame(): boolean {
return this.game?.config().gameConfig().gameMode === GameMode.Team;
}
render() {
return html`
<aside
class=${`fixed top-0 min-[1200px]:top-4 left-0 min-[1200px]:left-4 z-900 flex flex-col max-h-[calc(100vh-80px)] overflow-y-auto p-2 bg-gray-800/92 backdrop-blur-sm shadow-xs min-[1200px]:rounded-lg rounded-br-lg ${this.isLeaderboardShow || this.isTeamLeaderboardShow ? "max-[400px]:w-full max-[400px]:rounded-none" : ""} transition-all duration-300 ease-out transform ${
this.isVisible ? "translate-x-0" : "hidden"
}`}
style="margin-top: ${this.barOffset}px;"
>
<div class="flex items-center gap-4 xl:gap-6 text-white">
<div
class="cursor-pointer p-0.5 bg-gray-700/50 hover:bg-gray-600 border rounded-md border-slate-500 transition-colors"
@click=${this.toggleLeaderboard}
role="button"
tabindex="0"
@keydown=${(e: KeyboardEvent) => {
if (e.key === "Enter" || e.key === " " || e.code === "Space") {
e.preventDefault();
this.toggleLeaderboard();
}
}}
>
<img
src=${this.isLeaderboardShow
? leaderboardSolidIcon
: leaderboardRegularIcon}
alt=${translateText("help_modal.icon_alt_player_leaderboard") ||
"Player Leaderboard Icon"}
width="20"
height="20"
/>
</div>
${this.isTeamGame
? html`
<div
class="cursor-pointer p-0.5 bg-gray-700/50 hover:bg-gray-600 border rounded-md border-slate-500 transition-colors"
@click=${this.toggleTeamLeaderboard}
role="button"
tabindex="0"
@keydown=${(e: KeyboardEvent) => {
if (
e.key === "Enter" ||
e.key === " " ||
e.code === "Space"
) {
e.preventDefault();
this.toggleTeamLeaderboard();
}
}}
>
<img
src=${this.isTeamLeaderboardShow
? teamSolidIcon
: teamRegularIcon}
alt=${translateText(
"help_modal.icon_alt_team_leaderboard",
) || "Team Leaderboard Icon"}
width="20"
height="20"
/>
</div>
`
: null}
${this.isLeaderboardShow || this.isTeamLeaderboardShow
? html`<span
class="ml-auto text-[10px] text-slate-500 select-all leading-none self-start"
title=${translateText("help_modal.game_id_tooltip")}
>${this.game?.gameID() ?? ""}</span
>`
: null}
</div>
${this.isPlayerTeamLabelVisible
? html`
<div
class="flex items-center w-full text-white mt-2"
@contextmenu=${(e: Event) => e.preventDefault()}
>
${translateText("help_modal.ui_your_team")}
<span
style="--color: ${this.playerColor.toRgbString()}"
class="text-(--color)"
>
&nbsp;${getTranslatedPlayerTeamLabel(this.playerTeam)}
&#10687;
</span>
</div>
`
: null}
<div
class=${`block lg:flex flex-wrap overflow-x-auto min-w-0 w-full ${this.isLeaderboardShow && this.isTeamLeaderboardShow ? "gap-2" : ""}`}
>
<leader-board .visible=${this.isLeaderboardShow}></leader-board>
<team-stats
class="flex-1"
.visible=${this.isTeamLeaderboardShow && this.isTeamGame}
></team-stats>
</div>
<slot></slot>
</aside>
`;
}
}