make control panel mobile friendly

This commit is contained in:
Evan
2025-01-29 14:37:25 -08:00
parent a103d721e4
commit ebe50b30e1
3 changed files with 292 additions and 255 deletions
+169 -140
View File
@@ -1,168 +1,197 @@
import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { Layer } from './Layer';
import { Game } from '../../../core/game/Game';
import { ClientID } from '../../../core/Schemas';
import { renderNumber, renderTroops } from '../../Utils';
import { EventBus } from '../../../core/EventBus';
import { UIState } from '../UIState';
import { SendSetTargetTroopRatioEvent } from '../../Transport';
import { GameView } from '../../../core/game/GameView';
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { Layer } from "./Layer";
import { Game } from "../../../core/game/Game";
import { ClientID } from "../../../core/Schemas";
import { renderNumber, renderTroops } from "../../Utils";
import { EventBus } from "../../../core/EventBus";
import { UIState } from "../UIState";
import { SendSetTargetTroopRatioEvent } from "../../Transport";
import { GameView } from "../../../core/game/GameView";
@customElement('control-panel')
@customElement("control-panel")
export class ControlPanel extends LitElement implements Layer {
public game: GameView;
public clientID: ClientID;
public eventBus: EventBus;
public uiState: UIState;
public game: GameView;
public clientID: ClientID;
public eventBus: EventBus;
public uiState: UIState;
@state()
private attackRatio: number = .2;
@state()
private attackRatio: number = 0.2;
@state()
private targetTroopRatio = 1;
@state()
private targetTroopRatio = 1;
@state()
private currentTroopRatio = 1;
@state()
private currentTroopRatio = 1;
@state()
private _population: number;
@state()
private _population: number;
@state()
private _maxPopulation: number;
@state()
private _maxPopulation: number;
@state()
private popRate: number;
@state()
private popRate: number;
@state()
private _troops: number;
@state()
private _troops: number;
@state()
private _workers: number;
@state()
private _workers: number;
@state()
private _isVisible = false;
@state()
private _isVisible = false;
@state()
private _manpower: number = 0;
@state()
private _manpower: number = 0;
@state()
private _gold: number;
@state()
private _gold: number;
@state()
private _goldPerSecond: number;
@state()
private _goldPerSecond: number;
init() {
this.attackRatio = .20;
this.uiState.attackRatio = this.attackRatio;
this.currentTroopRatio = this.targetTroopRatio;
init() {
this.attackRatio = 0.2;
this.uiState.attackRatio = this.attackRatio;
this.currentTroopRatio = this.targetTroopRatio;
}
tick() {
if (!this._isVisible && !this.game.inSpawnPhase()) {
this.setVisibile(true);
}
tick() {
if (!this._isVisible && !this.game.inSpawnPhase()) {
this.setVisibile(true);
}
const player = this.game.playerByClientID(this.clientID);
if (player == null || !player.isAlive()) {
this.setVisibile(false);
return;
}
this._population = player.population();
this._maxPopulation = this.game.config().maxPopulation(player);
this._gold = player.gold();
this._troops = player.troops();
this._workers = player.workers();
this.popRate = this.game.config().populationIncreaseRate(player) * 10;
this._goldPerSecond = this.game.config().goldAdditionRate(player) * 10;
this.currentTroopRatio = player.troops() / player.population();
const player = this.game.playerByClientID(this.clientID);
if (player == null || !player.isAlive()) {
this.setVisibile(false);
return;
}
onAttackRatioChange(newRatio: number) {
this.uiState.attackRatio = newRatio;
}
this._population = player.population();
this._maxPopulation = this.game.config().maxPopulation(player);
this._gold = player.gold();
this._troops = player.troops();
this._workers = player.workers();
this.popRate = this.game.config().populationIncreaseRate(player) * 10;
this._goldPerSecond = this.game.config().goldAdditionRate(player) * 10;
renderLayer(context: CanvasRenderingContext2D) {
// Render any necessary canvas elements
}
this.currentTroopRatio = player.troops() / player.population();
}
shouldTransform(): boolean {
return false;
}
onAttackRatioChange(newRatio: number) {
this.uiState.attackRatio = newRatio;
}
setVisibile(visible: boolean) {
this._isVisible = visible;
this.requestUpdate();
}
renderLayer(context: CanvasRenderingContext2D) {
// Render any necessary canvas elements
}
targetTroops(): number {
return this._manpower * this.targetTroopRatio;
}
shouldTransform(): boolean {
return false;
}
onTroopChange(newRatio: number) {
this.eventBus.emit(new SendSetTargetTroopRatioEvent(newRatio));
}
setVisibile(visible: boolean) {
this._isVisible = visible;
this.requestUpdate();
}
delta(): number {
const d = this._population - this.targetTroops();
return d;
}
targetTroops(): number {
return this._manpower * this.targetTroopRatio;
}
render() {
return html`
<div class="${this._isVisible ? 'fixed bottom-3 left-3 z-50 bg-gray-800/70 p-4 shadow-lg rounded-lg w-72 backdrop-blur' : 'hidden'}">
<div class="bg-black/30 text-white mb-4 p-2 rounded">
<div class="flex justify-between mb-1">
<span class="font-bold">Pop:</span>
<span>${renderTroops(this._population)} / ${renderTroops(this._maxPopulation)} (+${renderTroops(this.popRate)})</span>
</div>
<div class="flex justify-between">
<span class="font-bold">Gold:</span>
<span>${renderNumber(this._gold)} (+${renderNumber(this._goldPerSecond)})</span>
</div>
</div>
<div class="relative mb-4 h-12">
<label class="block text-white mb-1">Troops: ${renderTroops(this._troops)} | Workers: ${renderTroops(this._workers)}</label>
<div class="absolute w-full h-2 bg-white/20 rounded top-6"></div>
<div class="absolute h-2 bg-blue-500/60 rounded top-6 transition-all duration-300" style="width: ${this.currentTroopRatio * 100}%"></div>
<div class="absolute w-4 h-4 bg-white border-2 border-blue-500 rounded-full top-5 -ml-2 cursor-pointer hover:scale-110 transition-transform" style="left: ${this.targetTroopRatio * 100}%"></div>
<input
type="range"
min="1"
max="100"
.value=${this.targetTroopRatio * 100}
@input=${(e: Event) => {
this.targetTroopRatio = parseInt((e.target as HTMLInputElement).value) / 100;
this.onTroopChange(this.targetTroopRatio);
onTroopChange(newRatio: number) {
this.eventBus.emit(new SendSetTargetTroopRatioEvent(newRatio));
}
delta(): number {
const d = this._population - this.targetTroops();
return d;
}
render() {
return html`
<div
class="${this._isVisible
? "fixed bottom-0 w-full landscape:w-1/3 landscape:lg:w-72 lg:bottom-3 left-0 lg:left-3 z-50 bg-gray-800/70 p-2 pr-3 lg:p-4 shadow-lg rounded-lg backdrop-blur"
: "hidden"}"
>
<div class="hidden lg:block bg-black/30 text-white mb-4 p-2 rounded">
<div class="flex justify-between mb-1">
<span class="font-bold">Pop:</span>
<span
>${renderTroops(this._population)} /
${renderTroops(this._maxPopulation)}
(+${renderTroops(this.popRate)})</span
>
</div>
<div class="flex justify-between">
<span class="font-bold">Gold:</span>
<span
>${renderNumber(this._gold)}
(+${renderNumber(this._goldPerSecond)})</span
>
</div>
</div>
<div class="relative mb-4 lg:mb-4 h-6 lg:h-6">
<label class="block text-white mb-1"
>Troops: ${renderTroops(this._troops)} | Workers:
${renderTroops(this._workers)}</label
>
<div
class="absolute h-2 bg-blue-500/60 rounded top-6 transition-all duration-300"
style="width: ${this.currentTroopRatio * 100}%"
></div>
<div
class="absolute w-4 h-4 bg-white border-2 border-blue-500 rounded-full top-5 -ml-2 cursor-pointer hover:scale-110 transition-transform"
style="left: ${this.targetTroopRatio * 100}%"
></div>
<input
type="range"
min="1"
max="100"
.value=${this.targetTroopRatio * 100}
@input=${(e: Event) => {
this.targetTroopRatio =
parseInt((e.target as HTMLInputElement).value) / 100;
this.onTroopChange(this.targetTroopRatio);
}}
class="absolute w-full top-3 m-0 opacity-0 cursor-pointer"
>
</div>
<div class="relative mb-4 h-12">
<label class="block text-white mb-1">Attack Ratio: ${(this.attackRatio * 100).toFixed(0)}%</label>
<div class="absolute w-full h-2 bg-white/20 rounded top-6"></div>
<div class="absolute h-2 bg-red-500/60 rounded top-6 transition-all duration-300" style="width: ${this.attackRatio * 100}%"></div>
<div class="absolute w-4 h-4 bg-white border-2 border-red-500 rounded-full top-5 -ml-2 cursor-pointer hover:scale-110 transition-transform" style="left: ${this.attackRatio * 100}%"></div>
<input
type="range"
min="1"
max="100"
.value=${this.attackRatio * 100}
@input=${(e: Event) => {
this.attackRatio = parseInt((e.target as HTMLInputElement).value) / 100;
this.onAttackRatioChange(this.attackRatio);
class="absolute w-full top-3 m-0 opacity-0 cursor-pointer"
/>
</div>
<div class="relative mb:0 lg:mb-4 h-10 lg:h-12">
<label class="block text-white mb-1"
>Attack Ratio: ${(this.attackRatio * 100).toFixed(0)}%</label
>
<div class="absolute w-full h-2 bg-white/20 rounded top-6"></div>
<div
class="absolute h-2 bg-red-500/60 rounded top-6 transition-all duration-300"
style="width: ${this.attackRatio * 100}%"
></div>
<div
class="absolute w-4 h-4 bg-white border-2 border-red-500 rounded-full top-5 -ml-2 cursor-pointer hover:scale-110 transition-transform"
style="left: ${this.attackRatio * 100}%"
></div>
<input
type="range"
min="1"
max="100"
.value=${this.attackRatio * 100}
@input=${(e: Event) => {
this.attackRatio =
parseInt((e.target as HTMLInputElement).value) / 100;
this.onAttackRatioChange(this.attackRatio);
}}
class="absolute w-full top-3 m-0 opacity-0 cursor-pointer"
>
</div>
</div>
`;
}
createRenderRoot() {
return this; // Disable shadow DOM to allow Tailwind styles
}
}
class="absolute w-full top-3 m-0 opacity-0 cursor-pointer"
/>
</div>
</div>
`;
}
createRenderRoot() {
return this; // Disable shadow DOM to allow Tailwind styles
}
}
+89 -79
View File
@@ -1,101 +1,111 @@
import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { EventBus } from '../../../core/EventBus';
import { PauseGameEvent } from '../../Transport';
import { GameType } from '../../../core/game/Game';
import { GameView } from '../../../core/game/GameView';
import { Layer } from './Layer';
import { GameUpdateType } from '../../../core/game/GameUpdates';
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { EventBus } from "../../../core/EventBus";
import { PauseGameEvent } from "../../Transport";
import { GameType } from "../../../core/game/Game";
import { GameView } from "../../../core/game/GameView";
import { Layer } from "./Layer";
import { GameUpdateType } from "../../../core/game/GameUpdates";
@customElement('options-menu')
@customElement("options-menu")
export class OptionsMenu extends LitElement implements Layer {
public game: GameView;
public eventBus: EventBus;
public game: GameView;
public eventBus: EventBus;
@state()
private showPauseButton: boolean = true;
@state()
private showPauseButton: boolean = true;
@state()
private isPaused: boolean = false;
@state()
private isPaused: boolean = false;
@state()
private timer: number = 0;
@state()
private timer: number = 0;
private isVisible = false;
private isVisible = false;
private hasWinner = false;
private hasWinner = false;
private onExitButtonClick() {
window.location.reload();
private onExitButtonClick() {
window.location.reload();
}
createRenderRoot() {
return this;
}
private onPauseButtonClick() {
this.isPaused = !this.isPaused;
this.eventBus.emit(new PauseGameEvent(this.isPaused));
}
init() {
console.log("init called from OptionsMenu");
this.showPauseButton =
this.game.config().gameConfig().gameType == GameType.Singleplayer;
this.isVisible = true;
this.requestUpdate();
}
tick() {
this.hasWinner =
this.hasWinner ||
this.game.updatesSinceLastTick()[GameUpdateType.WinUpdate].length > 0;
if (this.game.inSpawnPhase()) {
this.timer = 0;
} else if (!this.hasWinner && this.game.ticks() % 10 == 0) {
this.timer++;
}
this.isVisible = true;
this.requestUpdate();
}
createRenderRoot() {
return this;
render() {
if (!this.isVisible) {
return html``;
}
private onPauseButtonClick() {
this.isPaused = !this.isPaused;
this.eventBus.emit(new PauseGameEvent(this.isPaused));
}
init() {
console.log('init called from OptionsMenu');
this.showPauseButton = this.game.config().gameConfig().gameType == GameType.Singleplayer;
this.isVisible = true;
this.requestUpdate();
}
tick() {
this.hasWinner = this.hasWinner || this.game.updatesSinceLastTick()[GameUpdateType.WinUpdate].length > 0;
if (this.game.inSpawnPhase()) {
this.timer = 0;
} else if (!this.hasWinner && this.game.ticks() % 10 == 0) {
this.timer++;
}
this.isVisible = true;
this.requestUpdate();
}
render() {
if (!this.isVisible) {
return html``;
}
return html`
<div class="fixed top-0 md:top-4 right-0 md:right-4 z-50 pointer-events-auto">
<div class="bg-opacity-60 bg-gray-900 p-1 md:p-2 rounded-lg backdrop-blur-md">
<div class="flex items-center gap-1 md:gap-2">
<button
class="${!this.showPauseButton ? 'hidden' : ''}
w-6 h-6 md:w-10 md:h-10 flex items-center justify-center
return html`
<div
class="fixed top-0 lg:top-4 right-0 lg:right-4 z-50 pointer-events-auto"
>
<div
class="bg-opacity-60 bg-gray-900 p-1 lg:p-2 rounded-lg backdrop-blur-md"
>
<div class="flex items-center gap-1 lg:gap-2">
<button
class="${!this.showPauseButton ? "hidden" : ""}
w-6 h-6 lg:w-10 lg:h-10 flex items-center justify-center
bg-opacity-70 bg-gray-700 text-opacity-90 text-white
border-none rounded cursor-pointer
hover:bg-opacity-60 hover:bg-gray-600
transition-colors duration-200
text-sm md:text-xl"
@click=${this.onPauseButtonClick}
aria-label="${this.isPaused ? 'Resume game' : 'Pause game'}"
>
${this.isPaused ? '▶' : '⏸'}
</button>
<div class="w-14 h-6 md:w-20 md:h-10 flex items-center justify-center
text-sm lg:text-xl"
@click=${this.onPauseButtonClick}
aria-label="${this.isPaused ? "Resume game" : "Pause game"}"
>
${this.isPaused ? "▶" : "⏸"}
</button>
<div
class="w-14 h-6 lg:w-20 lg:h-10 flex items-center justify-center
bg-opacity-50 bg-gray-700 text-opacity-90 text-white
rounded text-sm md:text-xl">
${this.timer}
</div>
<button
class="w-6 h-6 md:w-10 md:h-10 flex items-center justify-center
rounded text-sm lg:text-xl"
>
${this.timer}
</div>
<button
class="w-6 h-6 lg:w-10 lg:h-10 flex items-center justify-center
bg-opacity-70 bg-gray-700 text-opacity-90 text-white
border-none rounded cursor-pointer
hover:bg-opacity-60 hover:bg-gray-600
transition-colors duration-200
text-sm md:text-xl"
@click=${this.onExitButtonClick}
aria-label="Exit game"
>×</button>
</div>
</div>
text-sm lg:text-xl"
@click=${this.onExitButtonClick}
aria-label="Exit game"
>
×
</button>
</div>
</div>
</div>
`;
}
}
}
}
+34 -36
View File
@@ -4,49 +4,47 @@ import { ServerConfig } from "./Config";
import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig";
export class DevServerConfig extends DefaultServerConfig {
gameCreationRate(): number {
return 10 * 1000
}
lobbyLifetime(): number {
return 10 * 1000
}
gameCreationRate(): number {
return 10 * 1000;
}
lobbyLifetime(): number {
return 10 * 1000;
}
}
export class DevConfig extends DefaultConfig {
constructor(sc: ServerConfig, gc: GameConfig) {
super(sc, gc);
}
constructor(sc: ServerConfig, gc: GameConfig) {
super(sc, gc);
}
numSpawnPhaseTurns(): number {
return this.gameConfig().gameType == GameType.Singleplayer ? 20 : 100;
// return 100
}
numSpawnPhaseTurns(): number {
return this.gameConfig().gameType == GameType.Singleplayer ? 40 : 100
// return 100
}
unitInfo(type: UnitType): UnitInfo {
const info = super.unitInfo(type);
const oldCost = info.cost;
info.cost = (p: Player) => oldCost(p) / 1000000000;
return info;
}
unitInfo(type: UnitType): UnitInfo {
const info = super.unitInfo(type)
const oldCost = info.cost
info.cost = (p: Player) => oldCost(p) / 1000000000
return info
}
// percentageTilesOwnedToWin(): number {
// return 1
// }
// percentageTilesOwnedToWin(): number {
// return 1
// }
// populationIncreaseRate(player: Player): number {
// return this.maxPopulation(player)
// }
// populationIncreaseRate(player: Player): number {
// return this.maxPopulation(player)
// }
// boatMaxDistance(): number {
// return 5000
// }
// numBots(): number {
// return 0
// }
// spawnNPCs(): boolean {
// return false
// }
// boatMaxDistance(): number {
// return 5000
// }
// numBots(): number {
// return 0
// }
// spawnNPCs(): boolean {
// return false
// }
}