diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts index 4e2955700..9bd4a4da2 100644 --- a/src/client/graphics/layers/ControlPanel.ts +++ b/src/client/graphics/layers/ControlPanel.ts @@ -10,10 +10,10 @@ import { SendSetTargetTroopRatioEvent } from '../../Transport'; @customElement('control-panel') export class ControlPanel extends LitElement implements Layer { - private game: Game - public clientID: ClientID - public eventBus: EventBus - public uiState: UIState + private game: Game; + public clientID: ClientID; + public eventBus: EventBus; + public uiState: UIState; @state() private attackRatio: number = .2; @@ -21,6 +21,9 @@ export class ControlPanel extends LitElement implements Layer { @state() private targetTroopRatio = 1; + @state() + private currentTroopRatio = 1; + @state() private _population: number; @@ -43,40 +46,42 @@ export class ControlPanel extends LitElement implements Layer { private _manpower: number = 0; @state() - private _gold: number - + private _gold: number; @state() - private _goldPerSecond: number + private _goldPerSecond: number; init(game: Game) { this.game = game; - this.attackRatio = .20 - this.uiState.attackRatio = this.attackRatio + this.attackRatio = .20; + this.uiState.attackRatio = this.attackRatio; + this.currentTroopRatio = this.targetTroopRatio; } tick() { - // Update game state based on numTroops value if needed if (!this._isVisible && !this.game.inSpawnPhase()) { - this.toggleVisibility(); + this.setVisibile(true); } - const player = this.game.playerByClientID(this.clientID) - if (player == null) { - this._isVisible = false - return + 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._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(); } onAttackRatioChange(newRatio: number) { - this.uiState.attackRatio = newRatio + this.uiState.attackRatio = newRatio; } renderLayer(context: CanvasRenderingContext2D) { @@ -87,32 +92,29 @@ export class ControlPanel extends LitElement implements Layer { return false; } - toggleVisibility() { - this._isVisible = !this._isVisible; + setVisibile(visible: boolean) { + this._isVisible = visible; this.requestUpdate(); } targetTroops(): number { - return this._manpower * this.targetTroopRatio + return this._manpower * this.targetTroopRatio; } onTroopChange(newRatio: number) { - this.eventBus.emit(new SendSetTargetTroopRatioEvent(newRatio)) + this.eventBus.emit(new SendSetTargetTroopRatioEvent(newRatio)); } delta(): number { - const d = this._population - this.targetTroops() - // if (Math.abs(d) < this._manpower / 200) { - // return 0 - // } - return d + const d = this._population - this.targetTroops(); + return d; } - static styles = css` :host { display: block; } + .control-panel { position: fixed; bottom: 10px; @@ -126,13 +128,62 @@ export class ControlPanel extends LitElement implements Layer { backdrop-filter: blur(5px); transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out; } + .hidden { opacity: 0; visibility: hidden; } + .slider-container { + position: relative; margin-bottom: 15px; + height: 48px; } + + .slider-track { + position: absolute; + width: 100%; + height: 8px; + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; + top: 20px; + } + + .slider-fill { + position: absolute; + height: 8px; + background: rgba(0, 150, 255, 0.6); + border-radius: 4px; + top: 20px; + transition: width 0.3s ease-out; + } + + .slider-thumb { + position: absolute; + width: 16px; + height: 16px; + background: white; + border: 2px solid rgb(0, 150, 255); + border-radius: 50%; + top: 16px; + transform: translateX(-50%); + cursor: pointer; + transition: transform 0.1s ease; + } + + .slider-thumb:hover { + transform: translateX(-50%) scale(1.1); + } + + input[type="range"] { + position: absolute; + width: 100%; + top: 12px; + margin: 0; + opacity: 0; + cursor: pointer; + } + .control-panel-info { color: white; margin-bottom: 15px; @@ -140,26 +191,77 @@ export class ControlPanel extends LitElement implements Layer { background-color: rgba(0, 0, 0, 0.3); border-radius: 5px; } + .info-row { display: flex; justify-content: space-between; margin-bottom: 5px; } + .info-label { font-weight: bold; } + label { display: block; color: white; margin-bottom: 5px; } - input[type="range"] { - width: 100%; - } + .slider-value { color: white; text-align: right; } + + .attack-slider { + position: relative; + margin-bottom: 15px; + height: 48px; + } + + .attack-slider .slider-track { + position: absolute; + width: 100%; + height: 8px; + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; + top: 20px; + } + + .attack-slider .slider-fill { + position: absolute; + height: 8px; + background: rgba(255, 0, 0, 0.6); + border-radius: 4px; + top: 20px; + transition: width 0.3s ease-out; + } + + .attack-slider .slider-thumb { + position: absolute; + width: 16px; + height: 16px; + background: white; + border: 2px solid rgb(255, 0, 0); + border-radius: 50%; + top: 16px; + transform: translateX(-50%); + cursor: pointer; + transition: transform 0.1s ease; + } + + .attack-slider .slider-thumb:hover { + transform: translateX(-50%) scale(1.1); + } + + .attack-slider input[type="range"] { + position: absolute; + width: 100%; + top: 12px; + margin: 0; + opacity: 0; + cursor: pointer; + } `; render() { @@ -183,21 +285,39 @@ export class ControlPanel extends LitElement implements Layer { ${renderNumber(this._goldPerSecond)} +
- `; diff --git a/src/client/graphics/layers/radial/BuildMenu.ts b/src/client/graphics/layers/radial/BuildMenu.ts index 7d06b6483..6d42f920b 100644 --- a/src/client/graphics/layers/radial/BuildMenu.ts +++ b/src/client/graphics/layers/radial/BuildMenu.ts @@ -1,22 +1,20 @@ import { LitElement, html, css } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { EventBus } from '../../../../core/EventBus'; -import { Cell, Game, Player } from '../../../../core/game/Game'; +import { Cell, Game, Item, Items, Player } from '../../../../core/game/Game'; import { SendNukeIntentEvent } from '../../../Transport'; import nukeIcon from '../../../../../resources/images/NukeIconWhite.svg'; import goldCoinIcon from '../../../../../resources/images/GoldCoinIcon.svg'; import { renderNumber } from '../../Utils'; interface BuildItem { - id: string; - name: string; + item: Item icon: string; - cost: number; } const buildTable: BuildItem[][] = [ [ - { id: 'nuke', name: 'Nuke', icon: nukeIcon, cost: 1_000_000 }, + { item: Items.Nuke, icon: nukeIcon }, // { id: 'battleship', name: 'Battleship', icon: '🚢', cost: 500, buildTime: 20 } ] ]; @@ -24,11 +22,10 @@ const buildTable: BuildItem[][] = [ @customElement('build-menu') export class BuildMenu extends LitElement { public game: Game; - public eventBus: EventBus + public eventBus: EventBus; - - private myPlayer: Player - private clickedCell: Cell + private myPlayer: Player; + private clickedCell: Cell; static styles = css` :host { @@ -72,15 +69,27 @@ export class BuildMenu extends LitElement { margin: 8px; padding: 10px; } - .build-button:hover { + .build-button:not(:disabled):hover { background-color: #3A3A3A; transform: scale(1.05); border-color: #666; } - .build-button:active { + .build-button:not(:disabled):active { background-color: #4A4A4A; transform: scale(0.95); } + .build-button:disabled { + background-color: #1A1A1A; + border-color: #333; + cursor: not-allowed; + opacity: 0.7; + } + .build-button:disabled img { + opacity: 0.5; + } + .build-button:disabled .build-cost { + color: #FF4444; + } .build-icon { font-size: 40px; margin-bottom: 5px; @@ -128,6 +137,10 @@ export class BuildMenu extends LitElement { @state() private _hidden = true; + private canAfford(item: BuildItem): boolean { + return this.myPlayer && this.myPlayer.gold() >= item.item.cost; + } + public onBuildSelected: (item: BuildItem) => void = () => { this.eventBus.emit(new SendNukeIntentEvent(this.myPlayer, this.clickedCell, null)) this.hideMenu() @@ -139,11 +152,16 @@ export class BuildMenu extends LitElement { ${buildTable.map(row => html`