mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 05:23:48 +00:00
first commit
This commit is contained in:
+13
-9
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,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
|
||||
|
||||
@@ -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)"
|
||||
>
|
||||
${getTranslatedPlayerTeamLabel(this.playerTeam)}
|
||||
⦿
|
||||
</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)"
|
||||
>
|
||||
${getTranslatedPlayerTeamLabel(this.playerTeam)}
|
||||
⦿
|
||||
</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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user