leaderboard

This commit is contained in:
evanpelle
2026-05-12 09:50:29 -07:00
parent aeb8d60224
commit ee0c995a9c
9 changed files with 483 additions and 573 deletions
+10 -6
View File
@@ -327,11 +327,18 @@
<build-menu></build-menu> <build-menu></build-menu>
<win-modal></win-modal> <win-modal></win-modal>
<game-starting-modal></game-starting-modal> <game-starting-modal></game-starting-modal>
<!-- Top HUD: mirrors bottom HUD grid layout -->
<div <div
class="flex flex-col items-end fixed top-0 right-0 min-[1200px]:top-4 min-[1200px]:right-4 z-1000 gap-2" class="fixed top-0 left-0 w-full z-[1000] flex flex-col pointer-events-none lg:grid lg:grid-cols-[1fr_500px_1fr] lg:items-start min-[1200px]:px-4"
style="
padding-top: env(safe-area-inset-top);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
"
> >
<game-right-sidebar></game-right-sidebar> <div class="lg:col-start-3 flex flex-col items-end pointer-events-auto">
<replay-panel></replay-panel> <game-menu></game-menu>
</div>
</div> </div>
<settings-modal></settings-modal> <settings-modal></settings-modal>
<player-panel></player-panel> <player-panel></player-panel>
@@ -342,11 +349,8 @@
<alert-frame></alert-frame> <alert-frame></alert-frame>
<chat-modal></chat-modal> <chat-modal></chat-modal>
<multi-tab-modal></multi-tab-modal> <multi-tab-modal></multi-tab-modal>
<game-left-sidebar></game-left-sidebar>
<performance-overlay></performance-overlay> <performance-overlay></performance-overlay>
<player-info-overlay></player-info-overlay> <player-info-overlay></player-info-overlay>
<leader-board></leader-board>
<team-stats></team-stats>
<heads-up-message></heads-up-message> <heads-up-message></heads-up-message>
<!-- Scripts --> <!-- Scripts -->
+7 -47
View File
@@ -18,13 +18,11 @@ import { DynamicUILayer } from "./layers/DynamicUILayer";
import { EmojiTable } from "./layers/EmojiTable"; import { EmojiTable } from "./layers/EmojiTable";
import { EventsDisplay } from "./layers/EventsDisplay"; import { EventsDisplay } from "./layers/EventsDisplay";
import { FxLayer } from "./layers/FxLayer"; import { FxLayer } from "./layers/FxLayer";
import { GameLeftSidebar } from "./layers/GameLeftSidebar"; import { GameMenu } from "./layers/GameMenu";
import { GameRightSidebar } from "./layers/GameRightSidebar";
import { HeadsUpMessage } from "./layers/HeadsUpMessage"; import { HeadsUpMessage } from "./layers/HeadsUpMessage";
import { ImmunityTimer } from "./layers/ImmunityTimer"; import { ImmunityTimer } from "./layers/ImmunityTimer";
import { InGamePromo } from "./layers/InGamePromo"; import { InGamePromo } from "./layers/InGamePromo";
import { Layer } from "./layers/Layer"; import { Layer } from "./layers/Layer";
import { Leaderboard } from "./layers/Leaderboard";
import { MainRadialMenu } from "./layers/MainRadialMenu"; import { MainRadialMenu } from "./layers/MainRadialMenu";
import { MultiTabModal } from "./layers/MultiTabModal"; import { MultiTabModal } from "./layers/MultiTabModal";
import { NameLayer } from "./layers/NameLayer"; import { NameLayer } from "./layers/NameLayer";
@@ -33,13 +31,11 @@ import { PerformanceOverlay } from "./layers/PerformanceOverlay";
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay"; import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
import { PlayerPanel } from "./layers/PlayerPanel"; import { PlayerPanel } from "./layers/PlayerPanel";
import { RailroadLayer } from "./layers/RailroadLayer"; import { RailroadLayer } from "./layers/RailroadLayer";
import { ReplayPanel } from "./layers/ReplayPanel";
import { SAMRadiusLayer } from "./layers/SAMRadiusLayer"; import { SAMRadiusLayer } from "./layers/SAMRadiusLayer";
import { SettingsModal } from "./layers/SettingsModal"; import { SettingsModal } from "./layers/SettingsModal";
import { SpawnTimer } from "./layers/SpawnTimer"; import { SpawnTimer } from "./layers/SpawnTimer";
import { StructureIconsLayer } from "./layers/StructureIconsLayer"; import { StructureIconsLayer } from "./layers/StructureIconsLayer";
import { StructureLayer } from "./layers/StructureLayer"; import { StructureLayer } from "./layers/StructureLayer";
import { TeamStats } from "./layers/TeamStats";
import { TerrainLayer } from "./layers/TerrainLayer"; import { TerrainLayer } from "./layers/TerrainLayer";
import { TerritoryLayer } from "./layers/TerritoryLayer"; import { TerritoryLayer } from "./layers/TerritoryLayer";
import { UILayer } from "./layers/UILayer"; import { UILayer } from "./layers/UILayer";
@@ -88,28 +84,12 @@ export function createRenderer(
buildMenu.uiState = uiState; buildMenu.uiState = uiState;
buildMenu.transformHandler = transformHandler; buildMenu.transformHandler = transformHandler;
const leaderboard = document.querySelector("leader-board") as Leaderboard; const gameMenu = document.querySelector("game-menu") as GameMenu;
if (!leaderboard || !(leaderboard instanceof Leaderboard)) { if (!gameMenu || !(gameMenu instanceof GameMenu)) {
console.error("LeaderBoard element not found in the DOM"); console.error("GameMenu element not found in the DOM");
} }
leaderboard.eventBus = eventBus; gameMenu.game = game;
leaderboard.game = game; gameMenu.eventBus = eventBus;
const gameLeftSidebar = document.querySelector(
"game-left-sidebar",
) as GameLeftSidebar;
if (!gameLeftSidebar || !(gameLeftSidebar instanceof GameLeftSidebar)) {
console.error("GameLeftSidebar element not found in the DOM");
}
gameLeftSidebar.game = game;
gameLeftSidebar.eventBus = eventBus;
const teamStats = document.querySelector("team-stats") as TeamStats;
if (!teamStats || !(teamStats instanceof TeamStats)) {
console.error("TeamStats element not found in the DOM");
}
teamStats.eventBus = eventBus;
teamStats.game = game;
const controlPanel = document.querySelector("control-panel") as ControlPanel; const controlPanel = document.querySelector("control-panel") as ControlPanel;
if (!(controlPanel instanceof ControlPanel)) { if (!(controlPanel instanceof ControlPanel)) {
@@ -163,22 +143,6 @@ export function createRenderer(
winModal.eventBus = eventBus; winModal.eventBus = eventBus;
winModal.game = game; winModal.game = game;
const replayPanel = document.querySelector("replay-panel") as ReplayPanel;
if (!(replayPanel instanceof ReplayPanel)) {
console.error("replay panel not found");
}
replayPanel.eventBus = eventBus;
replayPanel.game = game;
const gameRightSidebar = document.querySelector(
"game-right-sidebar",
) as GameRightSidebar;
if (!(gameRightSidebar instanceof GameRightSidebar)) {
console.error("Game Right bar not found");
}
gameRightSidebar.game = game;
gameRightSidebar.eventBus = eventBus;
const settingsModal = document.querySelector( const settingsModal = document.querySelector(
"settings-modal", "settings-modal",
) as SettingsModal; ) as SettingsModal;
@@ -304,16 +268,12 @@ export function createRenderer(
), ),
spawnTimer, spawnTimer,
immunityTimer, immunityTimer,
leaderboard, gameMenu,
gameLeftSidebar,
unitDisplay, unitDisplay,
gameRightSidebar,
controlPanel, controlPanel,
playerInfo, playerInfo,
winModal, winModal,
replayPanel,
settingsModal, settingsModal,
teamStats,
playerPanel, playerPanel,
headsUpMessage, headsUpMessage,
multiTabModal, multiTabModal,
@@ -1,205 +0,0 @@
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 { ImmunityBarVisibleEvent } from "./ImmunityTimer";
import { Layer } from "./Layer";
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 Layer {
@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>
`;
}
}
+415
View File
@@ -0,0 +1,415 @@
import { Colord } from "colord";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus } from "../../../core/EventBus";
import { GameMode, GameType, Team } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { crazyGamesSDK } from "../../CrazyGamesSDK";
import { Platform } from "../../Platform";
import { PauseGameIntentEvent, SendWinnerEvent } from "../../Transport";
import { getTranslatedPlayerTeamLabel, translateText } from "../../Utils";
import { ImmunityBarVisibleEvent } from "./ImmunityTimer";
import { Layer } from "./Layer";
import { Leaderboard } from "./Leaderboard";
import { ReplayPanel, ShowReplayPanelEvent } from "./ReplayPanel";
import { ShowSettingsModalEvent } from "./SettingsModal";
import { SpawnBarVisibleEvent } from "./SpawnTimer";
import { TeamStats } from "./TeamStats";
const exitIcon = assetUrl("images/ExitIconWhite.svg");
const FastForwardIconSolid = assetUrl("images/FastForwardIconSolidWhite.svg");
const leaderboardRegularIcon = assetUrl(
"images/LeaderboardIconRegularWhite.svg",
);
const leaderboardSolidIcon = assetUrl("images/LeaderboardIconSolidWhite.svg");
const pauseIcon = assetUrl("images/PauseIconWhite.svg");
const playIcon = assetUrl("images/PlayIconWhite.svg");
const settingsIcon = assetUrl("images/SettingIconWhite.svg");
const teamRegularIcon = assetUrl("images/TeamIconRegularWhite.svg");
const teamSolidIcon = assetUrl("images/TeamIconSolidWhite.svg");
@customElement("game-menu")
export class GameMenu extends LitElement implements Layer {
@property({ attribute: false }) game: GameView;
public eventBus: EventBus;
@state()
private _isSinglePlayer: boolean = false;
@state()
private _isReplayVisible: boolean = false;
@state()
private _isVisible: boolean = true;
@state()
private isPaused: boolean = false;
@state()
private timer: number = 0;
@state()
private isLeaderboardShow = false;
@state()
private isTeamLeaderboardShow = false;
@state()
private isPlayerTeamLabelVisible = false;
@state()
private playerTeam: Team | null = null;
private get leaderboard(): Leaderboard | null {
const el = this.querySelector("leader-board");
return el instanceof Leaderboard ? el : null;
}
private get teamStats(): TeamStats | null {
const el = this.querySelector("team-stats");
return el instanceof TeamStats ? el : null;
}
private get replayPanel(): ReplayPanel | null {
const el = this.querySelector("replay-panel");
return el instanceof ReplayPanel ? el : null;
}
private playerColor: Colord = new Colord("#FFFFFF");
private hasWinner = false;
private isLobbyCreator = false;
private spawnBarVisible = false;
private immunityBarVisible = false;
private _shownOnInit = false;
createRenderRoot() {
return this;
}
init() {
this._isSinglePlayer =
this.game?.config()?.gameConfig()?.gameType === GameType.Singleplayer ||
this.game.config().isReplay();
this._isVisible = true;
this.eventBus.on(SpawnBarVisibleEvent, (e) => {
this.spawnBarVisible = e.visible;
this.updateParentOffset();
});
this.eventBus.on(ImmunityBarVisibleEvent, (e) => {
this.immunityBarVisible = e.visible;
this.updateParentOffset();
});
this.eventBus.on(SendWinnerEvent, () => {
this.hasWinner = true;
this.requestUpdate();
});
if (this.isTeamGame) {
this.isPlayerTeamLabelVisible = true;
}
if (Platform.isDesktopWidth) {
this._shownOnInit = true;
}
this.requestUpdate();
}
getTickIntervalMs() {
return 250;
}
tick() {
// Check if the player is the lobby creator
if (!this.isLobbyCreator && this.game.myPlayer()?.isLobbyCreator()) {
this.isLobbyCreator = true;
this.requestUpdate();
}
// Team color
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();
}
// Timer logic
const maxTimerValue = this.game.config().gameConfig().maxTimerValue;
const spawnPhaseTurns = this.game.config().numSpawnPhaseTurns();
const ticks = this.game.ticks();
const gameTicks = Math.max(0, ticks - spawnPhaseTurns);
const elapsedSeconds = Math.floor(gameTicks / 10); // 10 ticks per second
const hasMaxTimer = maxTimerValue !== null && maxTimerValue !== undefined;
if (this.game.inSpawnPhase()) {
this.timer = hasMaxTimer ? maxTimerValue * 60 : 0;
return;
}
if (this.hasWinner) {
return;
}
if (hasMaxTimer) {
this.timer = Math.max(0, maxTimerValue * 60 - elapsedSeconds);
} else {
this.timer = elapsedSeconds;
}
this.leaderboard?.tick();
this.teamStats?.tick();
this.replayPanel?.tick?.();
}
private updateParentOffset(): void {
const offset =
(this.spawnBarVisible ? 7 : 0) + (this.immunityBarVisible ? 7 : 0);
const parent = this.parentElement as HTMLElement;
if (parent) {
parent.style.marginTop = `${offset}px`;
}
}
private secondsToHms = (d: number): string => {
const pad = (n: number) => (n < 10 ? `0${n}` : n);
const h = Math.floor(d / 3600);
const m = Math.floor((d % 3600) / 60);
const s = Math.floor((d % 3600) % 60);
if (h !== 0) {
return `${pad(h)}:${pad(m)}:${pad(s)}`;
} else {
return `${pad(m)}:${pad(s)}`;
}
};
private toggleReplayPanel(): void {
this._isReplayVisible = !this._isReplayVisible;
this.eventBus.emit(
new ShowReplayPanelEvent(this._isReplayVisible, this._isSinglePlayer),
);
}
private onPauseButtonClick() {
this.isPaused = !this.isPaused;
if (this.isPaused) {
crazyGamesSDK.gameplayStop();
} else {
crazyGamesSDK.gameplayStart();
}
this.eventBus.emit(new PauseGameIntentEvent(this.isPaused));
}
private async onExitButtonClick() {
const isAlive = this.game.myPlayer()?.isAlive();
if (isAlive) {
const isConfirmed = confirm(
translateText("help_modal.exit_confirmation"),
);
if (!isConfirmed) return;
}
await crazyGamesSDK.requestMidgameAd();
await crazyGamesSDK.gameplayStop();
window.location.href = "/";
}
private onSettingsButtonClick() {
this.eventBus.emit(
new ShowSettingsModalEvent(true, this._isSinglePlayer, this.isPaused),
);
}
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() {
if (this.game === undefined) return html``;
const timerColor =
this.game.config().gameConfig().maxTimerValue !== undefined &&
this.timer < 60
? "text-red-400"
: "";
return html`
<div class="relative">
<aside
class=${`flex flex-row items-center gap-3 py-2 px-3 bg-gray-800/92 backdrop-blur-sm shadow-xs rounded-bl-lg min-[1200px]:rounded-lg transition-transform duration-300 ease-out transform text-white ${
this._isVisible ? "translate-x-0" : "translate-x-full"
}`}
@contextmenu=${(e: Event) => e.preventDefault()}
>
<!-- Leaderboard button -->
<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>
<!-- Team leaderboard button -->
${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}
<!-- In-game time -->
<div class=${timerColor}>${this.secondsToHms(this.timer)}</div>
<!-- Replay/pause buttons -->
${this.maybeRenderReplayButtons()}
<div class="cursor-pointer" @click=${this.onSettingsButtonClick}>
<img src=${settingsIcon} alt="settings" width="20" height="20" />
</div>
<div class="cursor-pointer" @click=${this.onExitButtonClick}>
<img src=${exitIcon} alt="exit" width="20" height="20" />
</div>
</aside>
<div class="absolute right-0 top-full flex flex-col items-end w-96">
<replay-panel
.game=${this.game}
.eventBus=${this.eventBus}
></replay-panel>
${this.isPlayerTeamLabelVisible
? html`
<div
class="flex items-center text-white px-3 text-sm"
@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=${`flex flex-wrap justify-end overflow-x-auto min-w-0 w-full ${this.isLeaderboardShow && this.isTeamLeaderboardShow ? "gap-2" : ""}`}
>
<leader-board
.game=${this.game}
.eventBus=${this.eventBus}
.visible=${this.isLeaderboardShow}
></leader-board>
<team-stats
.game=${this.game}
.eventBus=${this.eventBus}
.visible=${this.isTeamLeaderboardShow && this.isTeamGame}
></team-stats>
</div>
</div>
</div>
`;
}
maybeRenderReplayButtons() {
const isReplayOrSingleplayer =
this._isSinglePlayer || this.game?.config()?.isReplay();
const showPauseButton = isReplayOrSingleplayer || this.isLobbyCreator;
return html`
${isReplayOrSingleplayer
? html`
<div class="cursor-pointer" @click=${this.toggleReplayPanel}>
<img
src=${FastForwardIconSolid}
alt="replay"
width="20"
height="20"
/>
</div>
`
: ""}
${showPauseButton
? html`
<div class="cursor-pointer" @click=${this.onPauseButtonClick}>
<img
src=${this.isPaused ? playIcon : pauseIcon}
alt="play/pause"
width="20"
height="20"
/>
</div>
`
: ""}
`;
}
}
@@ -1,294 +0,0 @@
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus } from "../../../core/EventBus";
import { GameType } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { crazyGamesSDK } from "../../CrazyGamesSDK";
import { TogglePauseIntentEvent } from "../../InputHandler";
import { PauseGameIntentEvent, SendWinnerEvent } from "../../Transport";
import { translateText } from "../../Utils";
import { ImmunityBarVisibleEvent } from "./ImmunityTimer";
import { Layer } from "./Layer";
import { ShowReplayPanelEvent } from "./ReplayPanel";
import { ShowSettingsModalEvent } from "./SettingsModal";
import { SpawnBarVisibleEvent } from "./SpawnTimer";
const exitIcon = assetUrl("images/ExitIconWhite.svg");
const FastForwardIconSolid = assetUrl("images/FastForwardIconSolidWhite.svg");
const pauseIcon = assetUrl("images/PauseIconWhite.svg");
const playIcon = assetUrl("images/PlayIconWhite.svg");
const settingsIcon = assetUrl("images/SettingIconWhite.svg");
const fullscreenIcon = assetUrl("images/FullscreenIconWhite.svg");
const exitFullscreenIcon = assetUrl("images/ExitFullscreenIconWhite.svg");
@customElement("game-right-sidebar")
export class GameRightSidebar extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
@state()
private _isSinglePlayer: boolean = false;
@state()
private _isReplayVisible: boolean = false;
@state()
private _isVisible: boolean = true;
@state()
private isPaused: boolean = false;
@state()
private isFullscreen: boolean = false;
@state()
private timer: number = 0;
private hasWinner = false;
private isLobbyCreator = false;
private spawnBarVisible = false;
private immunityBarVisible = false;
createRenderRoot() {
return this;
}
init() {
this._isSinglePlayer =
this.game?.config()?.gameConfig()?.gameType === GameType.Singleplayer ||
this.game.config().isReplay();
this._isVisible = true;
this.game.inSpawnPhase();
this.eventBus.on(SpawnBarVisibleEvent, (e) => {
this.spawnBarVisible = e.visible;
this.updateParentOffset();
});
this.eventBus.on(ImmunityBarVisibleEvent, (e) => {
this.immunityBarVisible = e.visible;
this.updateParentOffset();
});
this.eventBus.on(SendWinnerEvent, () => {
this.hasWinner = true;
this.requestUpdate();
});
this.eventBus.on(TogglePauseIntentEvent, () => {
const isReplayOrSingleplayer =
this._isSinglePlayer || this.game?.config()?.isReplay();
if (isReplayOrSingleplayer || this.isLobbyCreator) {
this.onPauseButtonClick();
}
});
this.requestUpdate();
}
private onFullscreenChange = () => {
this.isFullscreen = !!document.fullscreenElement;
};
connectedCallback() {
super.connectedCallback();
document.addEventListener("fullscreenchange", this.onFullscreenChange);
this.onFullscreenChange();
}
disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener("fullscreenchange", this.onFullscreenChange);
}
getTickIntervalMs() {
return 250;
}
tick() {
// Timer logic
// Check if the player is the lobby creator
if (!this.isLobbyCreator && this.game.myPlayer()?.isLobbyCreator()) {
this.isLobbyCreator = true;
this.requestUpdate();
}
const maxTimerValue = this.game.config().gameConfig().maxTimerValue;
const spawnPhaseTurns = this.game.config().numSpawnPhaseTurns();
const ticks = this.game.ticks();
const gameTicks = Math.max(0, ticks - spawnPhaseTurns);
const elapsedSeconds = Math.floor(gameTicks / 10); // 10 ticks per second
if (this.game.inSpawnPhase()) {
this.timer =
maxTimerValue !== null && maxTimerValue !== undefined
? maxTimerValue * 60
: 0;
return;
}
if (this.hasWinner) {
return;
}
if (maxTimerValue !== null && maxTimerValue !== undefined) {
this.timer = Math.max(0, maxTimerValue * 60 - elapsedSeconds);
} else {
this.timer = elapsedSeconds;
}
}
private updateParentOffset(): void {
const offset =
(this.spawnBarVisible ? 7 : 0) + (this.immunityBarVisible ? 7 : 0);
const parent = this.parentElement as HTMLElement;
if (parent) {
parent.style.marginTop = `${offset}px`;
}
}
private secondsToHms = (d: number): string => {
const pad = (n: number) => (n < 10 ? `0${n}` : n);
const h = Math.floor(d / 3600);
const m = Math.floor((d % 3600) / 60);
const s = Math.floor((d % 3600) % 60);
if (h !== 0) {
return `${pad(h)}:${pad(m)}:${pad(s)}`;
} else {
return `${pad(m)}:${pad(s)}`;
}
};
private toggleReplayPanel(): void {
this._isReplayVisible = !this._isReplayVisible;
this.eventBus.emit(
new ShowReplayPanelEvent(this._isReplayVisible, this._isSinglePlayer),
);
}
private onPauseButtonClick() {
this.isPaused = !this.isPaused;
if (this.isPaused) {
crazyGamesSDK.gameplayStop();
} else {
crazyGamesSDK.gameplayStart();
}
this.eventBus.emit(new PauseGameIntentEvent(this.isPaused));
}
private async onExitButtonClick() {
const isAlive = this.game.myPlayer()?.isAlive();
if (isAlive) {
const isConfirmed = confirm(
translateText("help_modal.exit_confirmation"),
);
if (!isConfirmed) return;
}
await crazyGamesSDK.requestMidgameAd();
await crazyGamesSDK.gameplayStop();
// redirect to the home page
window.location.href = "/";
}
private onSettingsButtonClick() {
this.eventBus.emit(
new ShowSettingsModalEvent(true, this._isSinglePlayer, this.isPaused),
);
}
private onFullscreenButtonClick() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch((err) => {
console.warn("Failed to enter fullscreen:", err);
});
} else {
document.exitFullscreen().catch((err) => {
console.warn("Failed to exit fullscreen:", err);
});
}
}
render() {
if (this.game === undefined) return html``;
const timerColor =
this.game.config().gameConfig().maxTimerValue !== undefined &&
this.game.config().gameConfig().maxTimerValue !== null &&
this.timer < 60
? "text-red-400"
: "";
return html`
<aside
class=${`w-fit flex flex-row items-center gap-3 py-2 px-3 bg-gray-800/92 backdrop-blur-sm shadow-xs min-[1200px]:rounded-lg rounded-bl-lg transition-transform duration-300 ease-out transform text-white ${
this._isVisible ? "translate-x-0" : "translate-x-full"
}`}
@contextmenu=${(e: Event) => e.preventDefault()}
>
<!-- In-game time -->
<div class=${timerColor}>${this.secondsToHms(this.timer)}</div>
<!-- Buttons -->
${this.maybeRenderReplayButtons()}
<div class="cursor-pointer" @click=${this.onSettingsButtonClick}>
<img src=${settingsIcon} alt="settings" width="20" height="20" />
</div>
${document.fullscreenEnabled
? html`<div
class="cursor-pointer"
@click=${this.onFullscreenButtonClick}
>
<img
src=${this.isFullscreen ? exitFullscreenIcon : fullscreenIcon}
alt=${this.isFullscreen
? translateText("fullscreen.exit")
: translateText("fullscreen.enter")}
width="20"
height="20"
/>
</div>`
: ""}
<div class="cursor-pointer" @click=${this.onExitButtonClick}>
<img src=${exitIcon} alt="exit" width="20" height="20" />
</div>
</aside>
`;
}
maybeRenderReplayButtons() {
const isReplayOrSingleplayer =
this._isSinglePlayer || this.game?.config()?.isReplay();
const showPauseButton = isReplayOrSingleplayer || this.isLobbyCreator;
return html`
${isReplayOrSingleplayer
? html`
<div class="cursor-pointer" @click=${this.toggleReplayPanel}>
<img
src=${FastForwardIconSolid}
alt="replay"
width="20"
height="20"
/>
</div>
`
: ""}
${showPauseButton
? html`
<div class="cursor-pointer" @click=${this.onPauseButtonClick}>
<img
src=${this.isPaused ? playIcon : pauseIcon}
alt="play/pause"
width="20"
height="20"
/>
</div>
`
: ""}
`;
}
}
+34 -13
View File
@@ -2,12 +2,17 @@ import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { repeat } from "lit/directives/repeat.js"; import { repeat } from "lit/directives/repeat.js";
import { renderTroops, translateText } from "../../../client/Utils"; import { renderTroops, translateText } from "../../../client/Utils";
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus } from "../../../core/EventBus"; import { EventBus } from "../../../core/EventBus";
import { GameView, PlayerView } from "../../../core/game/GameView"; import { GameView, PlayerView } from "../../../core/game/GameView";
import { formatPercentage, renderNumber } from "../../Utils"; import { formatPercentage, renderNumber } from "../../Utils";
import { GoToPlayerEvent } from "../TransformHandler"; import { GoToPlayerEvent } from "../TransformHandler";
import { Layer } from "./Layer"; import { Layer } from "./Layer";
const profileIcon = assetUrl("images/ProfileIcon.svg");
const goldIcon = assetUrl("images/GoldCoinIcon.svg");
const troopIcon = assetUrl("images/TroopIconWhite.svg");
interface Entry { interface Entry {
name: string; name: string;
position: number; position: number;
@@ -21,8 +26,8 @@ interface Entry {
@customElement("leader-board") @customElement("leader-board")
export class Leaderboard extends LitElement implements Layer { export class Leaderboard extends LitElement implements Layer {
public game: GameView | null = null; @property({ attribute: false }) game: GameView | null = null;
public eventBus: EventBus | null = null; @property({ attribute: false }) eventBus: EventBus | null = null;
players: Entry[] = []; players: Entry[] = [];
@@ -41,8 +46,8 @@ export class Leaderboard extends LitElement implements Layer {
init() {} init() {}
willUpdate(changed: Map<string, unknown>) { willUpdate(_changed: Map<string, unknown>) {
if (changed.has("visible") && this.visible) { if (this.visible && this.game !== null) {
this.updateLeaderboard(); this.updateLeaderboard();
} }
} }
@@ -177,22 +182,28 @@ export class Leaderboard extends LitElement implements Layer {
> >
<div <div
class="grid bg-gray-800/85 w-full text-xs md:text-xs lg:text-sm rounded-lg overflow-hidden" class="grid bg-gray-800/85 w-full text-xs md:text-xs lg:text-sm rounded-lg overflow-hidden"
style="grid-template-columns: minmax(24px, 30px) minmax(60px, 100px) minmax(45px, 70px) minmax(40px, 55px) minmax(55px, 105px);" style="grid-template-columns: minmax(24px, 30px) minmax(60px, 100px) minmax(45px, 70px) minmax(35px, 50px) minmax(45px, 65px);"
> >
<div class="contents font-bold bg-gray-700/60"> <div class="contents font-bold bg-gray-700/60">
<div class="py-1 md:py-2 text-center border-b border-slate-500"> <div class="py-1 md:py-2 text-center border-b border-slate-500">
# #
</div> </div>
<div <div
class="py-1 md:py-2 text-center border-b border-slate-500 truncate" class="py-1 md:py-2 flex items-center justify-center border-b border-slate-500"
title=${translateText("leaderboard.player")}
> >
${translateText("leaderboard.player")} <img
src=${profileIcon}
alt=${translateText("leaderboard.player")}
class="w-4 h-4"
/>
</div> </div>
<div <div
class="py-1 md:py-2 text-center border-b border-slate-500 cursor-pointer whitespace-nowrap truncate" class="py-1 md:py-2 flex items-center justify-center gap-0.5 border-b border-slate-500 cursor-pointer whitespace-nowrap"
title=${translateText("leaderboard.owned")}
@click=${() => this.setSort("tiles")} @click=${() => this.setSort("tiles")}
> >
${translateText("leaderboard.owned")} <span class="text-base leading-none">🌐</span>
${this._sortKey === "tiles" ${this._sortKey === "tiles"
? this._sortOrder === "asc" ? this._sortOrder === "asc"
? "⬆️" ? "⬆️"
@@ -200,10 +211,15 @@ export class Leaderboard extends LitElement implements Layer {
: ""} : ""}
</div> </div>
<div <div
class="py-1 md:py-2 text-center border-b border-slate-500 cursor-pointer whitespace-nowrap truncate" class="py-1 md:py-2 flex items-center justify-center gap-0.5 border-b border-slate-500 cursor-pointer whitespace-nowrap"
title=${translateText("leaderboard.gold")}
@click=${() => this.setSort("gold")} @click=${() => this.setSort("gold")}
> >
${translateText("leaderboard.gold")} <img
src=${goldIcon}
alt=${translateText("leaderboard.gold")}
class="w-4 h-4"
/>
${this._sortKey === "gold" ${this._sortKey === "gold"
? this._sortOrder === "asc" ? this._sortOrder === "asc"
? "⬆️" ? "⬆️"
@@ -211,10 +227,15 @@ export class Leaderboard extends LitElement implements Layer {
: ""} : ""}
</div> </div>
<div <div
class="py-1 md:py-2 text-center border-b border-slate-500 cursor-pointer whitespace-nowrap truncate" class="py-1 md:py-2 flex items-center justify-center gap-0.5 border-b border-slate-500 cursor-pointer whitespace-nowrap"
title=${translateText("leaderboard.maxtroops")}
@click=${() => this.setSort("maxtroops")} @click=${() => this.setSort("maxtroops")}
> >
${translateText("leaderboard.maxtroops")} <img
src=${troopIcon}
alt=${translateText("leaderboard.maxtroops")}
class="w-4 h-4"
/>
${this._sortKey === "maxtroops" ${this._sortKey === "maxtroops"
? this._sortOrder === "asc" ? this._sortOrder === "asc"
? "⬆️" ? "⬆️"
@@ -498,13 +498,13 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
return html` return html`
<div <div
class="fixed top-0 left-0 right-0 sm:left-1/2 sm:right-auto sm:-translate-x-1/2 z-[1001]" class="fixed top-0 left-0 right-0 sm:right-auto lg:left-1/2 lg:-translate-x-1/2 z-[1001]"
style="margin-top: ${this.barOffset}px;" style="margin-top: ${this.barOffset}px;"
@click=${() => this.hide()} @click=${() => this.hide()}
@contextmenu=${(e: MouseEvent) => e.preventDefault()} @contextmenu=${(e: MouseEvent) => e.preventDefault()}
> >
<div <div
class="bg-gray-800/92 backdrop-blur-sm shadow-xs min-[1200px]:rounded-lg sm:rounded-b-lg shadow-lg text-white text-lg lg:text-base w-full sm:w-[500px] overflow-hidden ${containerClasses}" class="bg-gray-800/92 backdrop-blur-sm shadow-xs rounded-b-lg min-[1200px]:rounded-lg shadow-lg text-white text-lg lg:text-base w-full sm:w-[500px] overflow-hidden ${containerClasses}"
> >
${this.player !== null ? this.renderPlayerInfo(this.player) : ""} ${this.player !== null ? this.renderPlayerInfo(this.player) : ""}
${this.unit !== null ? this.renderUnitInfo(this.unit) : ""} ${this.unit !== null ? this.renderUnitInfo(this.unit) : ""}
+13 -4
View File
@@ -19,8 +19,8 @@ export class ShowReplayPanelEvent {
@customElement("replay-panel") @customElement("replay-panel")
export class ReplayPanel extends LitElement implements Layer { export class ReplayPanel extends LitElement implements Layer {
public game: GameView | undefined; @property({ attribute: false }) game: GameView | undefined;
public eventBus: EventBus | undefined; @property({ attribute: false }) eventBus: EventBus | undefined;
@property({ type: Boolean }) @property({ type: Boolean })
visible: boolean = false; visible: boolean = false;
@@ -31,12 +31,21 @@ export class ReplayPanel extends LitElement implements Layer {
@property({ type: Boolean }) @property({ type: Boolean })
isSingleplayer = false; isSingleplayer = false;
private _eventBusSubscribed = false;
createRenderRoot() { createRenderRoot() {
return this; // Enable Tailwind CSS return this; // Enable Tailwind CSS
} }
init() { init() {}
if (this.eventBus) {
willUpdate(changed: Map<string, unknown>) {
if (
!this._eventBusSubscribed &&
this.eventBus &&
(changed.has("eventBus") || changed.size === 0)
) {
this._eventBusSubscribed = true;
this.eventBus.on(ShowReplayPanelEvent, (event: ShowReplayPanelEvent) => { this.eventBus.on(ShowReplayPanelEvent, (event: ShowReplayPanelEvent) => {
this.visible = event.visible; this.visible = event.visible;
this.isSingleplayer = event.isSingleplayer; this.isSingleplayer = event.isSingleplayer;
+2 -2
View File
@@ -27,8 +27,8 @@ interface TeamEntry {
@customElement("team-stats") @customElement("team-stats")
export class TeamStats extends LitElement implements Layer { export class TeamStats extends LitElement implements Layer {
public game: GameView; @property({ attribute: false }) game!: GameView;
public eventBus: EventBus; @property({ attribute: false }) eventBus!: EventBus;
@property({ type: Boolean }) visible = false; @property({ type: Boolean }) visible = false;
teams: TeamEntry[] = []; teams: TeamEntry[] = [];