first commit

This commit is contained in:
Ryan Barlow
2026-03-21 23:13:28 +00:00
parent bf09b9c9be
commit b2a433084e
13 changed files with 716 additions and 95 deletions
+13 -9
View File
@@ -267,39 +267,42 @@
<!-- Game components -->
<div id="app"></div>
<!-- Bottom HUD: <sm=column, sm..lg=2col (HUD left | events right), lg+=3col grid centered -->
<div
class="fixed bottom-0 left-0 w-full z-[200] flex flex-col pointer-events-none sm:flex-row sm:items-end lg:grid lg:grid-cols-[1fr_500px_1fr] lg:items-end min-[1200px]:px-4"
class="fixed bottom-0 left-0 w-full z-[200] flex flex-col pointer-events-none sm:contents"
style="
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
"
>
<!-- HUD: <sm contents (children join outer flex), sm+ flex-col 460px, lg+ col-2 -->
<!-- Bottom HUD: contents on mobile, fixed on desktop -->
<div
class="contents sm:flex sm:flex-col sm:pointer-events-none w-full sm:w-[500px] lg:col-start-2 sm:z-10"
class="contents sm:flex sm:fixed sm:bottom-0 sm:left-[calc(50%-250px)] sm:z-[200] sm:flex-col sm:pointer-events-none sm:w-[500px] min-[1200px]:mb-4"
data-draggable="bottom-hud"
style="padding-bottom: env(safe-area-inset-bottom)"
>
<attacks-display
class="w-full pointer-events-auto order-1 sm:order-none"
></attacks-display>
<div
class="pointer-events-auto bg-gray-800/92 backdrop-blur-sm sm:rounded-tr-lg lg:rounded-t-lg min-[1200px]:rounded-lg shadow-lg order-3 sm:order-none"
class="relative pointer-events-auto bg-gray-800/92 backdrop-blur-sm sm:rounded-tr-lg lg:rounded-t-lg min-[1200px]:rounded-lg shadow-lg order-3 sm:order-none"
>
<control-panel class="w-full"></control-panel>
<unit-display class="hidden lg:block w-full"></unit-display>
</div>
</div>
<!-- events+chat: <sm between attacks and control (order-2), sm+ right side, lg+ col-3 -->
<!-- Events/Chat: contents on mobile, fixed on desktop -->
<div
class="flex flex-col pointer-events-none items-end order-2 sm:order-none sm:flex-1 lg:col-start-3 lg:self-end lg:justify-end min-[1200px]:mr-4"
class="contents sm:flex sm:fixed sm:bottom-0 sm:right-0 sm:z-[200] sm:flex-col sm:pointer-events-none sm:items-end min-[1200px]:right-4 min-[1200px]:mb-4"
data-draggable="events-chat"
style="padding-bottom: env(safe-area-inset-bottom)"
>
<chat-display
class="w-full sm:w-auto pointer-events-auto"
class="w-full sm:w-auto pointer-events-auto order-2 sm:order-none"
></chat-display>
<events-display
class="w-full sm:w-auto pointer-events-auto"
class="w-full sm:w-auto pointer-events-auto order-2 sm:order-none"
></events-display>
</div>
</div>
@@ -311,6 +314,7 @@
<game-starting-modal></game-starting-modal>
<div
class="flex flex-col items-end fixed top-0 right-0 min-[1200px]:top-4 min-[1200px]:right-4 z-1000 gap-2"
data-draggable="right-sidebar"
>
<game-right-sidebar></game-right-sidebar>
<replay-panel></replay-panel>
+5
View File
@@ -1008,5 +1008,10 @@
"description": "(ALPHA)",
"login_required": "Login to play ranked!",
"must_login": "You must be logged in to play ranked matchmaking."
},
"draggable_panel": {
"unlock_to_move": "Unlock to move",
"lock_position": "Lock position",
"reset_position": "Reset position"
}
}
+346
View File
@@ -0,0 +1,346 @@
import { DraggableManager } from "./DraggableManager";
/**
* Reusable drag controller that can make any HTMLElement draggable.
* Persists position and lock state to localStorage.
* Registers with DraggableManager for collision detection.
*/
export class DraggableController {
private _locked = false;
private _offsetX = 0;
private _offsetY = 0;
private _dragging = false;
private _pointerId: number | null = null;
private _startMouseX = 0;
private _startMouseY = 0;
private _startOffsetX = 0;
private _startOffsetY = 0;
private _committed = false;
private _naturalRect: DOMRect | null = null;
private _obstacleRects: DOMRect[] = [];
private _isContentsDisplay = false;
private _savedZIndex = "";
private static readonly DRAG_THRESHOLD = 4;
private readonly storageKey: string;
private readonly el: HTMLElement;
private _onMoved: (() => void) | null = null;
private _onResize: (() => void) | null = null;
private onPointerDown = (e: PointerEvent) => this.handlePointerDown(e);
private onPointerMove = (e: PointerEvent) => this.handlePointerMove(e);
private onPointerUp = () => this.handlePointerUp();
constructor(el: HTMLElement, storageKey: string) {
this.el = el;
this.storageKey = `draggable.${storageKey}`;
this.load();
}
set onMoved(cb: (() => void) | null) {
this._onMoved = cb;
}
set onResize(cb: (() => void) | null) {
this._onResize = cb;
}
/** Called by DraggableManager when the element resizes. */
notifyResize(): void {
this._onResize?.();
}
/** True when the panel's center is in the right half of the viewport. */
isOnRightSide(): boolean {
const rect = this.el.getBoundingClientRect();
return (rect.left + rect.right) / 2 > window.innerWidth / 2;
}
get locked(): boolean {
return this._locked;
}
set locked(v: boolean) {
this._locked = v;
this.save();
}
getElement(): HTMLElement {
return this.el;
}
/** Apply the current offset as a CSS transform on the element. */
applyTransform(): void {
// transform has no effect on display:contents elements
if (this._isContentsDisplay) return;
if (this._offsetX === 0 && this._offsetY === 0) {
this.el.style.transform = "";
} else {
this.el.style.transform = `translate(${this._offsetX}px, ${this._offsetY}px)`;
}
}
/** Start listening for drag events. */
attach(): void {
this._isContentsDisplay = getComputedStyle(this.el).display === "contents";
this.el.addEventListener("pointerdown", this.onPointerDown);
this.applyTransform();
DraggableManager.instance.register(this);
// Defer clamp until the element has its final layout
requestAnimationFrame(() => this.clampToViewport());
}
/** Stop listening for drag events and clear the inline transform. */
detach(): void {
DraggableManager.instance.unregister(this);
this.el.removeEventListener("pointerdown", this.onPointerDown);
this.el.removeEventListener("pointermove", this.onPointerMove);
this.el.removeEventListener("pointerup", this.onPointerUp);
this.el.removeEventListener("lostpointercapture", this.onPointerUp);
this.el.style.transform = "";
}
resetPosition(): void {
this._offsetX = 0;
this._offsetY = 0;
this.applyTransform();
this.save();
}
/** Public entry point for the manager to re-clamp after window resize. */
clampAndApply(): void {
this.clampToViewport();
}
/** Push this panel away from any overlapping panels, then clamp to viewport. */
resolveOverlaps(): void {
if (this._dragging) return;
const rect = this.el.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0) return;
const nr = new DOMRect(
rect.x - this._offsetX,
rect.y - this._offsetY,
rect.width,
rect.height,
);
this._obstacleRects = DraggableManager.instance.snapshotObstacles(this);
const prevX = this._offsetX;
const prevY = this._offsetY;
this.resolveCollisions(nr);
const vw = window.innerWidth;
const vh = window.innerHeight;
this._offsetX = Math.max(-nr.left, Math.min(vw - nr.right, this._offsetX));
this._offsetY = Math.max(-nr.top, Math.min(vh - nr.bottom, this._offsetY));
// If overlaps remain (obstacle at viewport edge), revert
if (this.hasAnyOverlap(nr)) {
this._offsetX = prevX;
this._offsetY = prevY;
}
if (this._offsetX !== prevX || this._offsetY !== prevY) {
this.applyTransform();
this.save();
}
}
private handlePointerDown(e: PointerEvent): void {
if (this._locked) return;
const target = e.target as HTMLElement;
if (target.closest("button, input, select, textarea, a, [data-no-drag]")) {
return;
}
e.preventDefault();
this.el.setPointerCapture(e.pointerId);
this._pointerId = e.pointerId;
this._dragging = true;
this._committed = false;
this._startMouseX = e.clientX;
this._startMouseY = e.clientY;
this._startOffsetX = this._offsetX;
this._startOffsetY = this._offsetY;
this.el.addEventListener("pointermove", this.onPointerMove);
this.el.addEventListener("pointerup", this.onPointerUp);
this.el.addEventListener("lostpointercapture", this.onPointerUp);
}
private handlePointerMove(e: PointerEvent): void {
if (!this._dragging) return;
// Require minimum movement before committing to a drag
if (!this._committed) {
const dx = e.clientX - this._startMouseX;
const dy = e.clientY - this._startMouseY;
if (
dx * dx + dy * dy <
DraggableController.DRAG_THRESHOLD * DraggableController.DRAG_THRESHOLD
) {
return;
}
this._committed = true;
// Cache the element's natural position (without transform offset)
const rect = this.el.getBoundingClientRect();
this._naturalRect = new DOMRect(
rect.x - this._offsetX,
rect.y - this._offsetY,
rect.width,
rect.height,
);
// Snapshot obstacle rects and boost z-index for the drag
this._obstacleRects = DraggableManager.instance.snapshotObstacles(this);
this._savedZIndex = this.el.style.zIndex;
this.el.style.zIndex = "10000";
}
if (!this._naturalRect) return;
const nr = this._naturalRect;
const prevX = this._offsetX;
const prevY = this._offsetY;
this._offsetX = this._startOffsetX + (e.clientX - this._startMouseX);
this._offsetY = this._startOffsetY + (e.clientY - this._startMouseY);
const vw = window.innerWidth;
const vh = window.innerHeight;
// Viewport clamp → collision resolution → re-clamp
this._offsetX = Math.max(-nr.left, Math.min(vw - nr.right, this._offsetX));
this._offsetY = Math.max(-nr.top, Math.min(vh - nr.bottom, this._offsetY));
this.resolveCollisions(nr);
this._offsetX = Math.max(-nr.left, Math.min(vw - nr.right, this._offsetX));
this._offsetY = Math.max(-nr.top, Math.min(vh - nr.bottom, this._offsetY));
// If overlaps remain (obstacle at viewport edge), revert
if (this.hasAnyOverlap(nr)) {
this._offsetX = prevX;
this._offsetY = prevY;
}
this.applyTransform();
}
/**
* AABB collision: for each obstacle, if the candidate rect overlaps,
* push out on the axis with the smallest penetration (slide on the other).
*/
private resolveCollisions(nr: DOMRect): void {
for (const obs of this._obstacleRects) {
const cl = nr.left + this._offsetX;
const ct = nr.top + this._offsetY;
const cr = nr.right + this._offsetX;
const cb = nr.bottom + this._offsetY;
if (
cr <= obs.left ||
cl >= obs.right ||
cb <= obs.top ||
ct >= obs.bottom
) {
continue;
}
const overlapLeft = cr - obs.left;
const overlapRight = obs.right - cl;
const overlapTop = cb - obs.top;
const overlapBottom = obs.bottom - ct;
if (
Math.min(overlapLeft, overlapRight) <
Math.min(overlapTop, overlapBottom)
) {
this._offsetX +=
overlapLeft < overlapRight ? -overlapLeft : overlapRight;
} else {
this._offsetY +=
overlapTop < overlapBottom ? -overlapTop : overlapBottom;
}
}
}
private hasAnyOverlap(nr: DOMRect): boolean {
const cl = nr.left + this._offsetX;
const ct = nr.top + this._offsetY;
const cr = nr.right + this._offsetX;
const cb = nr.bottom + this._offsetY;
for (const obs of this._obstacleRects) {
if (cr > obs.left && cl < obs.right && cb > obs.top && ct < obs.bottom) {
return true;
}
}
return false;
}
private handlePointerUp(): void {
if (!this._dragging) return;
const didMove = this._committed;
this._dragging = false;
this._committed = false;
if (didMove) {
this.el.style.zIndex = this._savedZIndex;
}
if (this._pointerId !== null) {
try {
this.el.releasePointerCapture(this._pointerId);
} catch {
// already released
}
this._pointerId = null;
}
this.el.removeEventListener("pointermove", this.onPointerMove);
this.el.removeEventListener("pointerup", this.onPointerUp);
this.el.removeEventListener("lostpointercapture", this.onPointerUp);
if (didMove) {
this.save();
this._onMoved?.();
}
}
/** Clamp restored offsets so the element stays fully within the viewport. */
private clampToViewport(): void {
if (this._offsetX === 0 && this._offsetY === 0) return;
const rect = this.el.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
const prevX = this._offsetX;
const prevY = this._offsetY;
if (rect.left < 0) this._offsetX -= rect.left;
else if (rect.right > vw) this._offsetX -= rect.right - vw;
if (rect.top < 0) this._offsetY -= rect.top;
else if (rect.bottom > vh) this._offsetY -= rect.bottom - vh;
if (this._offsetX !== prevX || this._offsetY !== prevY) {
this.applyTransform();
this.save();
}
}
private save(): void {
const data = {
locked: this._locked,
x: this._offsetX,
y: this._offsetY,
};
localStorage.setItem(this.storageKey, JSON.stringify(data));
}
private load(): void {
try {
const raw = localStorage.getItem(this.storageKey);
if (raw) {
const data = JSON.parse(raw);
this._locked = data.locked ?? false;
this._offsetX = data.x ?? 0;
this._offsetY = data.y ?? 0;
}
} catch {
localStorage.removeItem(this.storageKey);
}
}
}
+53
View File
@@ -0,0 +1,53 @@
import { DraggableController } from "./DraggableController";
const GAP = 4;
export class DraggableManager {
private static _instance: DraggableManager | null = null;
private controllers = new Set<DraggableController>();
private resizeObserver = new ResizeObserver(() => this.onPanelResize());
private _resizeFrame = 0;
static get instance(): DraggableManager {
this._instance ??= new DraggableManager();
return this._instance;
}
register(ctrl: DraggableController): void {
this.controllers.add(ctrl);
this.resizeObserver.observe(ctrl.getElement());
}
unregister(ctrl: DraggableController): void {
this.controllers.delete(ctrl);
this.resizeObserver.unobserve(ctrl.getElement());
}
private onPanelResize(): void {
cancelAnimationFrame(this._resizeFrame);
this._resizeFrame = requestAnimationFrame(() => {
for (const ctrl of this.controllers) {
ctrl.resolveOverlaps();
ctrl.notifyResize();
}
});
}
snapshotObstacles(exclude: DraggableController): DOMRect[] {
const rects: DOMRect[] = [];
const g = GAP / 2;
for (const ctrl of this.controllers) {
if (ctrl === exclude) continue;
const r = ctrl.getElement().getBoundingClientRect();
if (r.width === 0 && r.height === 0) continue;
rects.push(new DOMRect(r.x - g, r.y - g, r.width + GAP, r.height + GAP));
}
return rects;
}
reclampAll(): void {
for (const ctrl of this.controllers) {
ctrl.clampAndApply();
}
}
}
+5 -1
View File
@@ -3,6 +3,7 @@ import { GameView } from "../../core/game/GameView";
import { UserSettings } from "../../core/game/UserSettings";
import { GameStartingModal } from "../GameStartingModal";
import { RefreshGraphicsEvent as RedrawGraphicsEvent } from "../InputHandler";
import { DraggableManager } from "./DraggableManager";
import { FrameProfiler } from "./FrameProfiler";
import { TransformHandler } from "./TransformHandler";
import { UIState } from "./UIState";
@@ -359,7 +360,10 @@ export class GameRenderer {
document.body.appendChild(this.canvas);
}
window.addEventListener("resize", () => this.resizeCanvas());
window.addEventListener("resize", () => {
this.resizeCanvas();
DraggableManager.instance.reclampAll();
});
this.resizeCanvas();
//show whole map on startup
+7 -2
View File
@@ -7,6 +7,7 @@ import { ClientID } from "../../../core/Schemas";
import { AttackRatioEvent } from "../../InputHandler";
import { renderNumber, renderTroops } from "../../Utils";
import { UIState } from "../UIState";
import "./DraggablePanel";
import { Layer } from "./Layer";
import goldCoinIcon from "/images/GoldCoinIcon.svg?url";
import soldierIcon from "/images/SoldierIcon.svg?url";
@@ -383,11 +384,15 @@ export class ControlPanel extends LitElement implements Layer {
render() {
return html`
<div
class="relative pointer-events-auto ${this._isVisible
? "relative w-full text-sm px-2 py-1"
class="pointer-events-auto ${this._isVisible
? "w-full text-sm px-2 py-1"
: "hidden"}"
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
>
<draggable-panel
key="bottom-hud"
class="hidden sm:contents"
></draggable-panel>
<div class="lg:hidden">${this.renderMobile()}</div>
<div class="hidden lg:block">${this.renderDesktop()}</div>
</div>
@@ -0,0 +1,168 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { translateText } from "../../Utils";
import { DraggableController } from "../DraggableController";
/**
* Non-wrapper element: place inside a panel and it renders a lock/reset
* toolbar on the right side. Drag behaviour is applied to the nearest
* ancestor with a matching `data-draggable` attribute.
*
* The toolbar is always visible so it contributes to the panel's bounding
* rect for collision detection.
*/
@customElement("draggable-panel")
export class DraggablePanel extends LitElement {
@property({ type: String }) key = "panel";
@property({ type: Boolean }) visible = true;
@state() private _locked = false;
private ctrl: DraggableController | null = null;
private _observer: MutationObserver | null = null;
createRenderRoot() {
return this;
}
connectedCallback(): void {
super.connectedCallback();
if (document.body.classList.contains("in-game")) {
this.initController();
} else {
this._observer = new MutationObserver(() => {
if (document.body.classList.contains("in-game")) {
this.initController();
this._observer?.disconnect();
this._observer = null;
}
});
this._observer.observe(document.body, {
attributes: true,
attributeFilter: ["class"],
});
}
}
disconnectedCallback(): void {
super.disconnectedCallback();
this.ctrl?.detach();
this._observer?.disconnect();
}
private initController(): void {
if (this.ctrl) return;
const target = this.closest(`[data-draggable="${this.key}"]`);
if (!target) {
console.error(
`draggable-panel: no ancestor [data-draggable="${this.key}"] found`,
);
return;
}
this.ctrl = new DraggableController(target as HTMLElement, this.key);
this._locked = this.ctrl.locked;
this.ctrl.onMoved = () => this.requestUpdate();
this.ctrl.onResize = () => this.requestUpdate();
this.ctrl.attach();
this.requestUpdate();
}
private toggleLock(): void {
if (!this.ctrl) return;
this._locked = !this._locked;
this.ctrl.locked = this._locked;
this.requestUpdate();
}
private resetPosition(): void {
this.ctrl?.resetPosition();
this.requestUpdate();
}
render() {
if (!this.ctrl || !this.visible) return nothing;
if (this.ctrl.getElement().getBoundingClientRect().height < 10)
return nothing;
const right = !this.ctrl.isOnRightSide();
return html`
<div
class="flex items-center absolute top-1/2 -translate-y-1/2 z-[12]
pointer-events-auto rounded-md bg-gray-800/95 backdrop-blur-sm
border border-white/15 px-0.5 py-1
${this._locked ? "opacity-40 hover:opacity-100" : ""}
${right
? "right-0 translate-x-full rounded-l-none border-l-0"
: "left-0 -translate-x-full rounded-r-none border-r-0"}"
>
${this._locked
? nothing
: html`<button
class="flex items-center justify-center size-4
text-gray-400 hover:text-white cursor-pointer transition-colors"
@pointerdown=${(e: Event) => e.stopPropagation()}
@click=${(e: Event) => {
e.stopPropagation();
this.resetPosition();
}}
title=${translateText("draggable_panel.reset_position")}
>
${DraggablePanel.resetSvg}
</button>`}
<button
class="flex items-center justify-center size-4
${this._locked
? "text-gray-400"
: "text-yellow-400"} hover:text-white cursor-pointer transition-colors"
@pointerdown=${(e: Event) => e.stopPropagation()}
@click=${(e: Event) => {
e.stopPropagation();
this.toggleLock();
}}
title=${this._locked
? translateText("draggable_panel.unlock_to_move")
: translateText("draggable_panel.lock_position")}
>
${this._locked ? DraggablePanel.lockSvg : DraggablePanel.unlockSvg}
</button>
</div>
`;
}
private static lockSvg = html`<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-3"
>
<path
fill-rule="evenodd"
d="M12 1.5a5.25 5.25 0 0 0-5.25 5.25v3a3 3 0 0 0-3 3v6.75a3 3 0 0 0 3 3h10.5a3 3 0 0 0 3-3v-6.75a3 3 0 0 0-3-3v-3A5.25 5.25 0 0 0 12 1.5Zm3.75 8.25v-3a3.75 3.75 0 1 0-7.5 0v3h7.5Z"
clip-rule="evenodd"
/>
</svg>`;
private static unlockSvg = html`<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-3.5"
>
<path
d="M18 1.5c2.9 0 5.25 2.35 5.25 5.25v3.75a.75.75 0 0 1-1.5 0V6.75a3.75 3.75 0 1 0-7.5 0v3h1.5a3 3 0 0 1 3 3v6.75a3 3 0 0 1-3 3H5.25a3 3 0 0 1-3-3v-6.75a3 3 0 0 1 3-3h7.5v-3A5.25 5.25 0 0 1 18 1.5Z"
/>
</svg>`;
private static resetSvg = html`<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-3.5"
>
<path
fill-rule="evenodd"
d="M4.755 10.059a7.5 7.5 0 0 1 12.548-3.364l1.903 1.903H14.25a.75.75 0 0 0 0 1.5h6a.75.75 0 0 0 .75-.75v-6a.75.75 0 0 0-1.5 0v4.956l-1.903-1.903A9 9 0 0 0 3.306 9.67a.75.75 0 1 0 1.45.388Zm14.49 3.882a7.5 7.5 0 0 1-12.548 3.364l-1.903-1.903H9.75a.75.75 0 0 0 0-1.5h-6a.75.75 0 0 0-.75.75v6a.75.75 0 0 0 1.5 0v-4.956l1.903 1.903A9 9 0 0 0 20.694 14.33a.75.75 0 1 0-1.45-.388Z"
clip-rule="evenodd"
/>
</svg>`;
}
@@ -37,6 +37,7 @@ import { GoToPlayerEvent, GoToUnitEvent } from "./Leaderboard";
import { getMessageTypeClasses, translateText } from "../../Utils";
import { UIState } from "../UIState";
import "./DraggablePanel";
import allianceIcon from "/images/AllianceIconWhite.svg?url";
import chatIcon from "/images/ChatIconWhite.svg?url";
import donateGoldIcon from "/images/DonateGoldIconWhite.svg?url";
@@ -790,6 +791,10 @@ export class EventsDisplay extends LitElement implements Layer {
});
return html`
<draggable-panel
key="events-chat"
class="hidden sm:contents"
></draggable-panel>
${styles}
<!-- Events Toggle (when hidden) -->
${this._hidden
+89 -79
View File
@@ -6,6 +6,7 @@ import { GameMode, Team } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { Platform } from "../../Platform";
import { getTranslatedPlayerTeamLabel, translateText } from "../../Utils";
import "./DraggablePanel";
import { ImmunityBarVisibleEvent } from "./ImmunityTimer";
import { Layer } from "./Layer";
import { SpawnBarVisibleEvent } from "./SpawnTimer";
@@ -101,95 +102,104 @@ export class GameLeftSidebar extends LitElement implements Layer {
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"
<div
class=${`relative fixed top-0 min-[1200px]:top-4 left-0 min-[1200px]:left-4 z-900 ${
this.isVisible ? "" : "hidden"
}`}
style="margin-top: ${this.barOffset}px;"
data-draggable="left-sidebar"
>
<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"
/>
<draggable-panel
key="left-sidebar"
class="hidden sm:contents"
></draggable-panel>
<aside
class=${`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" : ""}`}
>
<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}
</div>
${this.isTeamGame
${this.isPlayerTeamLabelVisible
? 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();
}
}}
class="flex items-center w-full text-white mt-2"
@contextmenu=${(e: Event) => e.preventDefault()}
>
<img
src=${this.isTeamLeaderboardShow
? teamSolidIcon
: teamRegularIcon}
alt=${translateText(
"help_modal.icon_alt_team_leaderboard",
) || "Team Leaderboard Icon"}
width="20"
height="20"
/>
${translateText("help_modal.ui_your_team")}
<span
style="--color: ${this.playerColor.toRgbString()}"
class="text-(--color)"
>
&nbsp;${getTranslatedPlayerTeamLabel(this.playerTeam)}
&#10687;
</span>
</div>
`
: 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>
<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>
</div>
`;
}
}
@@ -7,6 +7,7 @@ import { crazyGamesSDK } from "../../CrazyGamesSDK";
import { TogglePauseIntentEvent } from "../../InputHandler";
import { PauseGameIntentEvent, SendWinnerEvent } from "../../Transport";
import { translateText } from "../../Utils";
import "./DraggablePanel";
import { ImmunityBarVisibleEvent } from "./ImmunityTimer";
import { Layer } from "./Layer";
import { ShowReplayPanelEvent } from "./ReplayPanel";
@@ -184,11 +185,16 @@ export class GameRightSidebar extends LitElement implements Layer {
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"
class=${`relative 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 text-white ${
this._isVisible ? "" : "hidden"
}`}
@contextmenu=${(e: Event) => e.preventDefault()}
>
<draggable-panel
key="right-sidebar"
class="hidden sm:contents"
.visible=${this._isVisible}
></draggable-panel>
<!-- In-game time -->
<div class=${timerColor}>${this.secondsToHms(this.timer)}</div>
+8 -1
View File
@@ -1,6 +1,7 @@
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import { GameView } from "../../../core/game/GameView";
import "./DraggablePanel";
import { Layer } from "./Layer";
const AD_TYPE = "standard_iab_left1";
@@ -140,7 +141,13 @@ export class InGamePromo extends LitElement implements Layer {
id="${AD_CONTAINER_ID}"
class="fixed left-0 z-[100] pointer-events-auto"
style="bottom: -0.7cm"
></div>
data-draggable="ad-promo"
>
<draggable-panel
key="ad-promo"
class="hidden sm:contents"
></draggable-panel>
</div>
`;
}
}
@@ -27,6 +27,7 @@ import {
} from "../../Utils";
import { getFirstPlacePlayer, getPlayerIcons } from "../PlayerIcons";
import { TransformHandler } from "../TransformHandler";
import "./DraggablePanel";
import { ImmunityBarVisibleEvent } from "./ImmunityTimer";
import { Layer } from "./Layer";
import { CloseRadialMenuEvent } from "./RadialMenu";
@@ -518,11 +519,17 @@ 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:left-[calc(50%-250px)] sm:right-auto z-[1001]"
style="margin-top: ${this.barOffset}px;"
@click=${() => this.hide()}
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
data-draggable="player-info"
>
<draggable-panel
key="player-info"
class="hidden sm:contents"
.visible=${this._isInfoVisible}
></draggable-panel>
<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}"
>
@@ -225,6 +225,7 @@ export class UnitDisplay extends LitElement implements Layer {
return html`
<div
class="flex flex-col items-center relative"
data-no-drag
@mouseenter=${() => {
this._hoveredUnit = unitType;
this.requestUpdate();