import { LitElement, html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { EventBus } from "../../../core/EventBus"; import { Gold } from "../../../core/game/Game"; import { GameView } from "../../../core/game/GameView"; import { ClientID } from "../../../core/Schemas"; import { AttackRatioEvent } from "../../InputHandler"; import { renderNumber, renderTroops } from "../../Utils"; import { UIState } from "../UIState"; import { Layer } from "./Layer"; import goldCoinIcon from "/images/GoldCoinIcon.svg?url"; import soldierIcon from "/images/SoldierIcon.svg?url"; import swordIcon from "/images/SwordIcon.svg?url"; @customElement("control-panel") export class ControlPanel extends LitElement implements Layer { public game: GameView; public clientID: ClientID; public eventBus: EventBus; public uiState: UIState; @state() private attackRatio: number = 0.2; @state() private _maxTroops: number; @state() private troopRate: number; @state() private _troops: number; @state() private _isVisible = false; @state() private _gold: Gold; @state() private _attackingTroops: number = 0; @state() private _touchDragging = false; private _troopRateIsIncreasing: boolean = true; private _lastTroopIncreaseRate: number; getTickIntervalMs() { return 100; } init() { this.attackRatio = Number( localStorage.getItem("settings.attackRatio") ?? "0.2", ); this.uiState.attackRatio = this.attackRatio; this.eventBus.on(AttackRatioEvent, (event) => { let newAttackRatio = this.attackRatio + event.attackRatio / 100; if (newAttackRatio < 0.01) { newAttackRatio = 0.01; } if (newAttackRatio > 1) { newAttackRatio = 1; } if (newAttackRatio === 0.11 && this.attackRatio === 0.01) { // If we're changing the ratio from 1%, then set it to 10% instead of 11% to keep a consistency newAttackRatio = 0.1; } this.attackRatio = newAttackRatio; this.onAttackRatioChange(this.attackRatio); }); } tick() { if (!this._isVisible && !this.game.inSpawnPhase()) { this.setVisibile(true); } const player = this.game.myPlayer(); if (player === null || !player.isAlive()) { this.setVisibile(false); return; } this.updateTroopIncrease(); this._maxTroops = this.game.config().maxTroops(player); this._gold = player.gold(); this._troops = player.troops(); this._attackingTroops = player .outgoingAttacks() .map((a) => a.troops) .reduce((a, b) => a + b, 0); this.troopRate = this.game.config().troopIncreaseRate(player) * 10; this.requestUpdate(); } private updateTroopIncrease() { const player = this.game?.myPlayer(); if (player === null) return; const troopIncreaseRate = this.game.config().troopIncreaseRate(player); this._troopRateIsIncreasing = troopIncreaseRate >= this._lastTroopIncreaseRate; this._lastTroopIncreaseRate = troopIncreaseRate; } onAttackRatioChange(newRatio: number) { this.uiState.attackRatio = newRatio; } renderLayer(context: CanvasRenderingContext2D) { // Render any necessary canvas elements } shouldTransform(): boolean { return false; } setVisibile(visible: boolean) { this._isVisible = visible; this.requestUpdate(); } private _outsideTouchHandler: ((ev: Event) => void) | null = null; private handleAttackTouchStart(e: TouchEvent) { e.preventDefault(); e.stopPropagation(); if (this._touchDragging) { this.closeAttackBar(); return; } this._touchDragging = true; setTimeout(() => { this._outsideTouchHandler = () => { this.closeAttackBar(); }; document.addEventListener("touchstart", this._outsideTouchHandler); }, 0); } private closeAttackBar() { this._touchDragging = false; if (this._outsideTouchHandler) { document.removeEventListener("touchstart", this._outsideTouchHandler); this._outsideTouchHandler = null; } } private handleBarTouch(e: TouchEvent) { e.preventDefault(); e.stopPropagation(); this.setRatioFromTouch(e.touches[0]); const onMove = (ev: TouchEvent) => { ev.preventDefault(); this.setRatioFromTouch(ev.touches[0]); }; const onEnd = () => { document.removeEventListener("touchmove", onMove); document.removeEventListener("touchend", onEnd); }; document.addEventListener("touchmove", onMove, { passive: false }); document.addEventListener("touchend", onEnd); } private setRatioFromTouch(touch: Touch) { const barEl = this.querySelector(".attack-drag-bar"); if (!barEl) return; const rect = barEl.getBoundingClientRect(); const ratio = (rect.bottom - touch.clientY) / (rect.bottom - rect.top); this.attackRatio = Math.round(Math.max(1, Math.min(100, ratio * 100))) / 100; this.onAttackRatioChange(this.attackRatio); } private handleRatioSliderInput(e: Event) { const value = Number((e.target as HTMLInputElement).value); this.attackRatio = value / 100; this.onAttackRatioChange(this.attackRatio); } private renderTroopBar() { const base = Math.max(this._maxTroops, 1); const greenPercentRaw = (this._troops / base) * 100; const orangePercentRaw = (this._attackingTroops / base) * 100; const greenPercent = Math.max(0, Math.min(100, greenPercentRaw)); const orangePercent = Math.max( 0, Math.min(100 - greenPercent, orangePercentRaw), ); return html`
${greenPercent > 0 ? html`
` : ""} ${orangePercent > 0 ? html`
` : ""}
${renderTroops(this._troops)} ${renderTroops(this._maxTroops)}
+${renderTroops(this.troopRate)}/s
`; } render() { return html`
e.preventDefault()} >
${renderNumber(this._gold)}
${this.renderTroopBar()}
this.handleAttackTouchStart(e)} >
${(this.attackRatio * 100).toFixed(0)}%
(${renderTroops( (this.game?.myPlayer()?.troops() ?? 0) * this.attackRatio, )})
${this._touchDragging ? html`
this.handleBarTouch(e)} > ${(this.attackRatio * 100).toFixed(0)}%
` : ""}
`; } createRenderRoot() { return this; // Disable shadow DOM to allow Tailwind styles } }