From d666a398ecc528f2c214e37245ac6a148dff4c91 Mon Sep 17 00:00:00 2001 From: Evan Date: Sun, 3 Nov 2024 20:29:53 -0800 Subject: [PATCH 1/3] control panel dissappears when player dies --- src/client/graphics/layers/ControlPanel.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts index 4e2955700..855c33669 100644 --- a/src/client/graphics/layers/ControlPanel.ts +++ b/src/client/graphics/layers/ControlPanel.ts @@ -58,12 +58,12 @@ export class ControlPanel extends LitElement implements Layer { 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 + if (player == null || !player.isAlive()) { + this.setVisibile(false) return } this._population = player.population() @@ -87,11 +87,12 @@ 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 } From 7b1bab1e0fd809bfe2ce624eff7468331249cbe1 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 4 Nov 2024 19:17:12 -0800 Subject: [PATCH 2/3] troop/worker slider shows current ratio --- src/client/graphics/layers/ControlPanel.ts | 211 ++++++++++++++++----- src/core/configuration/DefaultConfig.ts | 2 +- src/core/configuration/DevConfig.ts | 2 +- 3 files changed, 167 insertions(+), 48 deletions(-) diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts index 855c33669..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.setVisibile(true) + this.setVisibile(true); } - const player = this.game.playerByClientID(this.clientID) + const player = this.game.playerByClientID(this.clientID); if (player == null || !player.isAlive()) { - this.setVisibile(false) - return + 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) { @@ -88,32 +93,28 @@ export class ControlPanel extends LitElement implements Layer { } setVisibile(visible: boolean) { - this._isVisible = visible + 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; @@ -127,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; @@ -141,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() { @@ -184,21 +285,39 @@ export class ControlPanel extends LitElement implements Layer { ${renderNumber(this._goldPerSecond)} +
- - { - this.targetTroopRatio = parseInt((e.target as HTMLInputElement).value) / 10; + +
+
+
+ { + this.targetTroopRatio = parseInt((e.target as HTMLInputElement).value) / 100; this.onTroopChange(this.targetTroopRatio); - }}> + }} + >
-
- - { - this.attackRatio = parseInt((e.target as HTMLInputElement).value) / 10; + +
+ +
+
+
+ { + this.attackRatio = parseInt((e.target as HTMLInputElement).value) / 100; this.onAttackRatioChange(this.attackRatio); - }}> + }} + >
`; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index ceef2e7d4..332e4a534 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -163,7 +163,7 @@ export class DefaultConfig implements Config { } troopAdjustmentRate(player: Player): number { - const maxDiff = this.maxPopulation(player) / 300 + const maxDiff = this.maxPopulation(player) / 1000 const target = player.population() * player.targetTroopRatio() const diff = target - player.troops() if (Math.abs(diff) < maxDiff) { diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index 251d1178e..133237fdc 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -6,7 +6,7 @@ export const devConfig = new class extends DefaultConfig { return 95 } numSpawnPhaseTurns(): number { - return 40 + return 20 } gameCreationRate(): number { return 20 * 1000 From 1654f73f43d947d8c775d580026cf81587943eec Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 4 Nov 2024 19:45:31 -0800 Subject: [PATCH 3/3] require gold to buy items --- .../graphics/layers/radial/BuildMenu.ts | 52 +++++++++++++------ src/core/execution/AttackExecution.ts | 1 - src/core/execution/NukeExecution.ts | 9 +++- src/core/game/Game.ts | 8 +++ 4 files changed, 51 insertions(+), 19 deletions(-) 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`
${row.map(item => html` - @@ -160,8 +178,8 @@ export class BuildMenu extends LitElement { } showMenu(player: Player, clickedCell: Cell) { - this.myPlayer = player - this.clickedCell = clickedCell + this.myPlayer = player; + this.clickedCell = clickedCell; this._hidden = false; this.requestUpdate(); } diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index 7608b7732..a1f059f34 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -2,7 +2,6 @@ import { PriorityQueue } from "@datastructures-js/priority-queue"; import { Cell, Execution, MutableGame, MutablePlayer, Player, PlayerID, TerrainType, TerraNullius, Tile } from "../game/Game"; import { PseudoRandom } from "../PseudoRandom"; import { manhattanDist } from "../Util"; -import { Terrain } from "../game/TerrainMapLoader"; export class AttackExecution implements Execution { private breakAlliance = false diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index d135a4a33..7b004159a 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -1,4 +1,4 @@ -import { Cell, Execution, MutableGame, MutablePlayer, PlayerID, Tile } from "../game/Game"; +import { Cell, Execution, Items, MutableGame, MutablePlayer, PlayerID, Tile } from "../game/Game"; import { PseudoRandom } from "../PseudoRandom"; import { bfs, dist, euclideanDist, manhattanDist } from "../Util"; @@ -26,6 +26,13 @@ export class NukeExecution implements Execution { } tick(ticks: number): void { + if (this.sender.gold() < Items.Nuke.cost) { + console.warn(`player ${this.sender} insufficient gold for nuke`) + this.active = false + return + } + this.sender.removeGold(Items.Nuke.cost) + const rand = new PseudoRandom(this.mg.ticks()) const tile = this.mg.tile(this.cell) const toDestroy = bfs(tile, (n: Tile) => { diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 47b7759e1..ddfae6680 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -25,6 +25,14 @@ export enum GameMap { Mena } +export class Item { + constructor(public readonly name: string, public readonly cost: Gold) { } +} + +export const Items = { + Nuke: new Item("Nuke", 1_000_000), +} as const; + export class Nation { constructor( public readonly name: string,