leaderboard

This commit is contained in:
evanpelle
2026-05-12 09:39:39 -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>
<win-modal></win-modal>
<game-starting-modal></game-starting-modal>
<!-- Top HUD: mirrors bottom HUD grid layout -->
<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>
<replay-panel></replay-panel>
<div class="lg:col-start-3 flex flex-col items-end pointer-events-auto">
<game-menu></game-menu>
</div>
</div>
<settings-modal></settings-modal>
<player-panel></player-panel>
@@ -342,11 +349,8 @@
<alert-frame></alert-frame>
<chat-modal></chat-modal>
<multi-tab-modal></multi-tab-modal>
<game-left-sidebar></game-left-sidebar>
<performance-overlay></performance-overlay>
<player-info-overlay></player-info-overlay>
<leader-board></leader-board>
<team-stats></team-stats>
<heads-up-message></heads-up-message>
<!-- Scripts -->
+7 -47
View File
@@ -18,13 +18,11 @@ import { DynamicUILayer } from "./layers/DynamicUILayer";
import { EmojiTable } from "./layers/EmojiTable";
import { EventsDisplay } from "./layers/EventsDisplay";
import { FxLayer } from "./layers/FxLayer";
import { GameLeftSidebar } from "./layers/GameLeftSidebar";
import { GameRightSidebar } from "./layers/GameRightSidebar";
import { GameMenu } from "./layers/GameMenu";
import { HeadsUpMessage } from "./layers/HeadsUpMessage";
import { ImmunityTimer } from "./layers/ImmunityTimer";
import { InGamePromo } from "./layers/InGamePromo";
import { Layer } from "./layers/Layer";
import { Leaderboard } from "./layers/Leaderboard";
import { MainRadialMenu } from "./layers/MainRadialMenu";
import { MultiTabModal } from "./layers/MultiTabModal";
import { NameLayer } from "./layers/NameLayer";
@@ -33,13 +31,11 @@ import { PerformanceOverlay } from "./layers/PerformanceOverlay";
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
import { PlayerPanel } from "./layers/PlayerPanel";
import { RailroadLayer } from "./layers/RailroadLayer";
import { ReplayPanel } from "./layers/ReplayPanel";
import { SAMRadiusLayer } from "./layers/SAMRadiusLayer";
import { SettingsModal } from "./layers/SettingsModal";
import { SpawnTimer } from "./layers/SpawnTimer";
import { StructureIconsLayer } from "./layers/StructureIconsLayer";
import { StructureLayer } from "./layers/StructureLayer";
import { TeamStats } from "./layers/TeamStats";
import { TerrainLayer } from "./layers/TerrainLayer";
import { TerritoryLayer } from "./layers/TerritoryLayer";
import { UILayer } from "./layers/UILayer";
@@ -88,28 +84,12 @@ export function createRenderer(
buildMenu.uiState = uiState;
buildMenu.transformHandler = transformHandler;
const leaderboard = document.querySelector("leader-board") as Leaderboard;
if (!leaderboard || !(leaderboard instanceof Leaderboard)) {
console.error("LeaderBoard element not found in the DOM");
const gameMenu = document.querySelector("game-menu") as GameMenu;
if (!gameMenu || !(gameMenu instanceof GameMenu)) {
console.error("GameMenu element not found in the DOM");
}
leaderboard.eventBus = eventBus;
leaderboard.game = game;
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;
gameMenu.game = game;
gameMenu.eventBus = eventBus;
const controlPanel = document.querySelector("control-panel") as ControlPanel;
if (!(controlPanel instanceof ControlPanel)) {
@@ -163,22 +143,6 @@ export function createRenderer(
winModal.eventBus = eventBus;
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(
"settings-modal",
) as SettingsModal;
@@ -304,16 +268,12 @@ export function createRenderer(
),
spawnTimer,
immunityTimer,
leaderboard,
gameLeftSidebar,
gameMenu,
unitDisplay,
gameRightSidebar,
controlPanel,
playerInfo,
winModal,
replayPanel,
settingsModal,
teamStats,
playerPanel,
headsUpMessage,
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 { repeat } from "lit/directives/repeat.js";
import { renderTroops, translateText } from "../../../client/Utils";
import { assetUrl } from "../../../core/AssetUrls";
import { EventBus } from "../../../core/EventBus";
import { GameView, PlayerView } from "../../../core/game/GameView";
import { formatPercentage, renderNumber } from "../../Utils";
import { GoToPlayerEvent } from "../TransformHandler";
import { Layer } from "./Layer";
const profileIcon = assetUrl("images/ProfileIcon.svg");
const goldIcon = assetUrl("images/GoldCoinIcon.svg");
const troopIcon = assetUrl("images/TroopIconWhite.svg");
interface Entry {
name: string;
position: number;
@@ -21,8 +26,8 @@ interface Entry {
@customElement("leader-board")
export class Leaderboard extends LitElement implements Layer {
public game: GameView | null = null;
public eventBus: EventBus | null = null;
@property({ attribute: false }) game: GameView | null = null;
@property({ attribute: false }) eventBus: EventBus | null = null;
players: Entry[] = [];
@@ -41,8 +46,8 @@ export class Leaderboard extends LitElement implements Layer {
init() {}
willUpdate(changed: Map<string, unknown>) {
if (changed.has("visible") && this.visible) {
willUpdate(_changed: Map<string, unknown>) {
if (this.visible && this.game !== null) {
this.updateLeaderboard();
}
}
@@ -177,22 +182,28 @@ export class Leaderboard extends LitElement implements Layer {
>
<div
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="py-1 md:py-2 text-center border-b border-slate-500">
#
</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
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")}
>
${translateText("leaderboard.owned")}
<span class="text-base leading-none">🌐</span>
${this._sortKey === "tiles"
? this._sortOrder === "asc"
? "⬆️"
@@ -200,10 +211,15 @@ export class Leaderboard extends LitElement implements Layer {
: ""}
</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")}
>
${translateText("leaderboard.gold")}
<img
src=${goldIcon}
alt=${translateText("leaderboard.gold")}
class="w-4 h-4"
/>
${this._sortKey === "gold"
? this._sortOrder === "asc"
? "⬆️"
@@ -211,10 +227,15 @@ export class Leaderboard extends LitElement implements Layer {
: ""}
</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")}
>
${translateText("leaderboard.maxtroops")}
<img
src=${troopIcon}
alt=${translateText("leaderboard.maxtroops")}
class="w-4 h-4"
/>
${this._sortKey === "maxtroops"
? this._sortOrder === "asc"
? "⬆️"
@@ -498,13 +498,13 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
return html`
<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;"
@click=${() => this.hide()}
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
>
<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.unit !== null ? this.renderUnitInfo(this.unit) : ""}
+13 -4
View File
@@ -19,8 +19,8 @@ export class ShowReplayPanelEvent {
@customElement("replay-panel")
export class ReplayPanel extends LitElement implements Layer {
public game: GameView | undefined;
public eventBus: EventBus | undefined;
@property({ attribute: false }) game: GameView | undefined;
@property({ attribute: false }) eventBus: EventBus | undefined;
@property({ type: Boolean })
visible: boolean = false;
@@ -31,12 +31,21 @@ export class ReplayPanel extends LitElement implements Layer {
@property({ type: Boolean })
isSingleplayer = false;
private _eventBusSubscribed = false;
createRenderRoot() {
return this; // Enable Tailwind CSS
}
init() {
if (this.eventBus) {
init() {}
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.visible = event.visible;
this.isSingleplayer = event.isSingleplayer;
+2 -2
View File
@@ -27,8 +27,8 @@ interface TeamEntry {
@customElement("team-stats")
export class TeamStats extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
@property({ attribute: false }) game!: GameView;
@property({ attribute: false }) eventBus!: EventBus;
@property({ type: Boolean }) visible = false;
teams: TeamEntry[] = [];