diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 1ba1ff910..2297bd0f9 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -3,7 +3,6 @@ import { NameLayer } from "./layers/NameLayer"; import { TerrainLayer } from "./layers/TerrainLayer"; import { TerritoryLayer } from "./layers/TerritoryLayer"; import { ClientID } from "../../core/Schemas"; -import { UILayer } from "./layers/UILayer"; import { EventBus } from "../../core/EventBus"; import { TransformHandler } from "./TransformHandler"; import { Layer } from "./layers/Layer"; @@ -20,6 +19,8 @@ import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay"; import { consolex } from "../../core/Consolex"; import { RefreshGraphicsEvent as RedrawGraphicsEvent } from "../InputHandler"; import { GameView } from "../../core/game/GameView"; +import { WinModal } from "./layers/WinModal"; +import { SpawnTimer } from "./layers/SpawnTimer"; export function createRenderer(canvas: HTMLCanvasElement, game: GameView, eventBus: EventBus, clientID: ClientID): GameRenderer { @@ -75,18 +76,27 @@ export function createRenderer(canvas: HTMLCanvasElement, game: GameView, eventB playerInfo.game = game + const winModel = document.querySelector('win-modal') as WinModal + if (!(playerInfo instanceof WinModal)) { + console.error('win modal not found') + } + winModel.clientID = clientID + winModel.game = game + + const layers: Layer[] = [ new TerrainLayer(game), new TerritoryLayer(game, eventBus), new StructureLayer(game, eventBus), new UnitLayer(game, eventBus, clientID), new NameLayer(game, game.config().theme(), transformHandler, clientID), - new UILayer(eventBus, game, clientID, transformHandler), eventsDisplay, new RadialMenu(eventBus, game, transformHandler, clientID, emojiTable as EmojiTable, buildMenu, uiState), + new SpawnTimer(game, transformHandler), leaderboard, controlPanel, - playerInfo + playerInfo, + winModel ] return new GameRenderer(game, eventBus, canvas, transformHandler, uiState, layers) diff --git a/src/client/graphics/layers/SpawnTimer.ts b/src/client/graphics/layers/SpawnTimer.ts new file mode 100644 index 000000000..05a1ad451 --- /dev/null +++ b/src/client/graphics/layers/SpawnTimer.ts @@ -0,0 +1,34 @@ +import { GameView } from '../../../core/game/GameView'; +import { TransformHandler } from '../TransformHandler'; +import { Layer } from './Layer'; + +export class SpawnTimer implements Layer { + + constructor(private game: GameView, private transformHandler: TransformHandler) { } + + init() { + } + tick() { + } + shouldTransform(): boolean { + return false + } + + renderLayer(context: CanvasRenderingContext2D) { + if (!this.game.inSpawnPhase()) { + return + } + + const barHeight = 15; + const barBackgroundWidth = this.transformHandler.width(); + + const ratio = this.game.ticks() / this.game.config().numSpawnPhaseTurns() + + // Draw bar background + context.fillStyle = 'rgba(0, 0, 0, 0.5)'; + context.fillRect(0, 0, barBackgroundWidth, barHeight); + + context.fillStyle = 'rgba(0, 128, 255, 0.7)'; + context.fillRect(0, 0, barBackgroundWidth * ratio, barHeight); + } +} diff --git a/src/client/graphics/layers/UILayer.ts b/src/client/graphics/layers/UILayer.ts deleted file mode 100644 index 04592cca9..000000000 --- a/src/client/graphics/layers/UILayer.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { EventBus } from "../../../core/EventBus"; -import { WinEvent } from "../../../core/execution/WinCheckExecution"; -import { Player } from "../../../core/game/Game"; -import { ClientID } from "../../../core/Schemas"; -import { Layer } from "./Layer"; -import { TransformHandler } from "../TransformHandler"; -import { consolex } from "../../../core/Consolex"; -import { GameView } from "../../../core/game/GameView"; - -interface MenuOption { - label: string; - action: () => void; -} - -export class UILayer implements Layer { - private exitButton: HTMLButtonElement; - private winModal: HTMLElement | null = null; - - private customMenu = document.getElementById('customMenu'); - - - constructor( - private eventBus: EventBus, - private game: GameView, - private clientID: ClientID, - private transformHandler: TransformHandler - ) { - - } - - renderLayer(context: CanvasRenderingContext2D) { - if (!this.game.inSpawnPhase()) { - return - } - - const barHeight = 15; - const barBackgroundWidth = this.transformHandler.width(); - - const ratio = this.game.ticks() / this.game.config().numSpawnPhaseTurns() - - // Draw bar background - context.fillStyle = 'rgba(0, 0, 0, 0.5)'; - context.fillRect(0, 0, barBackgroundWidth, barHeight); - - context.fillStyle = 'rgba(0, 128, 255, 0.7)'; - context.fillRect(0, 0, barBackgroundWidth * ratio, barHeight); - } - - shouldTransform(): boolean { - return false - } - - tick() { - } - - init() { - this.createWinModal() - this.initRightClickMenu() - this.eventBus.on(WinEvent, (e) => this.onWinEvent(e)) - } - - initRightClickMenu() { - if (!this.customMenu) { - consolex.error('Custom menu not found'); - return; - } - - document.addEventListener('click', () => { - this.customMenu!.style.display = 'none'; - }); - - const menuItems = this.customMenu.querySelectorAll('li'); - menuItems.forEach(item => { - item.addEventListener('click', () => { - alert(`You clicked: ${item.textContent}`); - this.customMenu!.style.display = 'none'; - }); - }); - } - - createWinModal() { - consolex.log("Creating win modal"); - this.winModal = document.createElement('div'); - this.winModal.style.cssText = ` - display: none; - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background-color: white; - padding: 20px; - border: 2px solid black; - border-radius: 10px; - z-index: 2000; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - `; - - const content = document.createElement('div'); - - const title = document.createElement('h2'); - title.textContent = 'Game Over'; - title.id = 'winTitle'; - title.style.marginTop = '0'; - - const message = document.createElement('p'); - message.id = 'winMessage'; - - const buttonContainer = document.createElement('div'); - buttonContainer.style.display = 'flex'; - buttonContainer.style.justifyContent = 'space-between'; - buttonContainer.style.marginTop = '20px'; - - const exitButton = document.createElement('button'); - exitButton.textContent = 'Exit Game'; - exitButton.onclick = () => this.exitGame(); - this.styleButton(exitButton); - - const continueButton = document.createElement('button'); - continueButton.textContent = 'Keep Playing'; - continueButton.onclick = () => this.closeWinModal(); - this.styleButton(continueButton); - - buttonContainer.appendChild(exitButton); - buttonContainer.appendChild(continueButton); - - content.appendChild(title); - content.appendChild(message); - content.appendChild(buttonContainer); - - this.winModal.appendChild(content); - document.body.appendChild(this.winModal); - - consolex.log("Win modal appended to body"); - } - - styleButton(button: HTMLButtonElement) { - button.style.cssText = ` - padding: 10px 20px; - font-size: 16px; - cursor: pointer; - background-color: #4A90E2; - color: white; - border: none; - border-radius: 5px; - transition: background-color 0.3s; - `; - button.onmouseover = () => button.style.backgroundColor = '#3A7BCE'; - button.onmouseout = () => button.style.backgroundColor = '#4A90E2'; - } - - - onWinEvent(event: WinEvent) { - consolex.log(`${event.winner.name()} won the game!!}`) - this.showWinModal(event.winner) - } - - showWinModal(winner: Player) { - if (this.winModal) { - const message = this.winModal.querySelector('#winMessage'); - if (message) { - message.textContent = `${winner.name()} won the game!`; - } - const title = this.winModal.querySelector('#winTitle') - if (winner.clientID() == this.clientID) { - title.textContent = 'You Won!!!' - } else { - title.textContent = 'You Lost!!!' - } - this.winModal.style.display = 'block'; - } - } - - closeWinModal() { - if (this.winModal) { - this.winModal.style.display = 'none'; - } - } - - exitGame() { - this.closeWinModal(); - window.location.reload(); - } - -} \ No newline at end of file diff --git a/src/client/graphics/layers/WinModal.ts b/src/client/graphics/layers/WinModal.ts new file mode 100644 index 000000000..04a2e9f8e --- /dev/null +++ b/src/client/graphics/layers/WinModal.ts @@ -0,0 +1,167 @@ +import { LitElement, html, css } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { Player } from '../../../core/game/Game'; +import { ClientID } from '../../../core/Schemas'; +import { GameView, PlayerView } from '../../../core/game/GameView'; +import { Layer } from './Layer'; +import { GameUpdateType } from '../../../core/game/GameUpdates'; + +@customElement('win-modal') +export class WinModal extends LitElement implements Layer { + public clientID: ClientID + public game: GameView + private winner: PlayerView + + @state() + isVisible = false + + static styles = css` + :host { + display: block; + } + + .modal { + display: none; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: rgba(30, 30, 30, 0.7); + padding: 25px; + border-radius: 10px; + z-index: 9999; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + backdrop-filter: blur(5px); + color: white; + width: 300px; + transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out; + } + + .modal.visible { + display: block; + animation: fadeIn 0.3s ease-out; + } + + @keyframes fadeIn { + from { + opacity: 0; + transform: translate(-50%, -48%); + } + to { + opacity: 1; + transform: translate(-50%, -50%); + } + } + + h2 { + margin: 0 0 15px 0; + font-size: 24px; + text-align: center; + color: white; + } + + p { + margin: 0 0 20px 0; + text-align: center; + background-color: rgba(0, 0, 0, 0.3); + padding: 10px; + border-radius: 5px; + } + + .button-container { + display: flex; + justify-content: space-between; + gap: 10px; + } + + button { + flex: 1; + padding: 12px; + font-size: 16px; + cursor: pointer; + background: rgba(0, 150, 255, 0.6); + color: white; + border: none; + border-radius: 5px; + transition: background-color 0.2s ease, transform 0.1s ease; + } + + button:hover { + background: rgba(0, 150, 255, 0.8); + transform: translateY(-1px); + } + + button:active { + transform: translateY(1px); + } + + @media (max-width: 768px) { + .modal { + width: 90%; + max-width: 300px; + padding: 20px; + } + + h2 { + font-size: 20px; + } + + button { + padding: 10px; + font-size: 14px; + } + } + `; + + render() { + if (!this.winner) return null; + const isWinner = this.winner.clientID() === this.clientID; + const title = isWinner ? 'You Won!!!' : 'You Lost!!!'; + const message = `${this.winner.name()} won the game!`; + + return html` + + `; + } + + show(winner: PlayerView) { + this.winner = winner; + this.isVisible = true; + this.requestUpdate(); + } + + hide() { + this.isVisible = false; + this.requestUpdate(); + } + + private _handleExit() { + this.hide(); + window.location.reload(); + } + + private _handleContinue() { + this.hide(); + } + + init() { } + + tick() { + this.game.updatesSinceLastTick()[GameUpdateType.WinUpdate] + .forEach(wu => this.show(this.game.playerBySmallID(wu.winnerID) as PlayerView)) + } + + renderLayer(context: CanvasRenderingContext2D) { + } + + shouldTransform(): boolean { + return false + } +} \ No newline at end of file diff --git a/src/client/index.html b/src/client/index.html index 79b6b7083..a8350270f 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -117,19 +117,28 @@ - + + + + + + + + + + - - - - - + + + + + + + + \ No newline at end of file diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index bf3155cd2..9d04ff48a 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -145,7 +145,6 @@ export class GameRunner { ), alliances: player.alliances().map(a => a.other(player).smallID()) }; - console.log(`got relations: ${JSON.stringify(rel)}`) return rel } diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index ca15edb80..3426f516d 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -171,7 +171,7 @@ export class DefaultConfig implements Config { return 600 * 10 } percentageTilesOwnedToWin(): number { - return 95 + return 80 } boatMaxNumber(): number { return 3 diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index d1a09e5f6..803d3b6a0 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -30,6 +30,10 @@ export class DevConfig extends DefaultConfig { return info } + // percentageTilesOwnedToWin(): number { + // return 1 + // } + // populationIncreaseRate(player: Player): number { // return this.maxPopulation(player) // } @@ -38,15 +42,15 @@ export class DevConfig extends DefaultConfig { // tradeShipSpawnRate(): number { // return 10 // } - // boatMaxDistance(): number { - // return 5000 - // } + boatMaxDistance(): number { + return 5000 + } - // numBots(): number { - // return 0 - // } - // spawnNPCs(): boolean { - // return false - // } + numBots(): number { + return 0 + } + spawnNPCs(): boolean { + return false + } } diff --git a/src/core/execution/WinCheckExecution.ts b/src/core/execution/WinCheckExecution.ts index 2de53cece..cbadfbcc5 100644 --- a/src/core/execution/WinCheckExecution.ts +++ b/src/core/execution/WinCheckExecution.ts @@ -27,8 +27,13 @@ export class WinCheckExecution implements Execution { return } const max = sorted[0] - if (max.numTilesOwned() / this.mg.map().numLandTiles() * 100 > this.mg.config().percentageTilesOwnedToWin()) { + const numTilesWithoutFallout = this.mg.numLandTiles() - this.mg.numTilesWithFallout() + if (this.mg.ticks() % 10 == 0) { + console.log(`player: ${max.name()} owns ${max.numTilesOwned()} tiles, ${numTilesWithoutFallout}`) + } + if (max.numTilesOwned() / numTilesWithoutFallout * 100 > this.mg.config().percentageTilesOwnedToWin()) { this.mg.setWinner(max) + console.log(`${max.name()} has won the game`) this.active = false } } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index d7d5f0504..e50feb061 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -325,6 +325,8 @@ export interface Game extends GameMap { // Nations nations(): Nation[] + + numTilesWithFallout(): number } export interface PlayerActions { diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 559da1b58..266d595be 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -43,6 +43,8 @@ export class GameImpl implements Game { private updates: GameUpdates = createGameUpdatesMap() + private _numTilesWithFallout = 0 + constructor( private _map: GameMap, private miniGameMap: GameMap, @@ -59,6 +61,11 @@ export class GameImpl implements Game { n.strength )) } + + numTilesWithFallout(): number { + return this._numTilesWithFallout + } + owner(ref: TileRef): Player | TerraNullius { return this.playerBySmallID(this.ownerID(ref)) } @@ -79,7 +86,6 @@ export class GameImpl implements Game { (this.updates[update.type] as any[]).push(update); } - nextUnitID(): number { const old = this._nextUnitID this._nextUnitID++ @@ -90,6 +96,10 @@ export class GameImpl implements Game { if (value && this.hasOwner(tile)) { throw Error(`cannot set fallout, tile ${tile} has owner`) } + if (this._map.hasFallout(tile)) { + return + } + this._numTilesWithFallout++ this._map.setFallout(tile, value) this.addUpdate({ type: GameUpdateType.Tile, @@ -331,7 +341,10 @@ export class GameImpl implements Game { owner._tiles.add(tile) owner._lastTileChange = this._ticks this.updateBorders(tile) - this._map.setFallout(tile, false) + if (this._map.hasFallout(tile)) { + this._numTilesWithFallout-- + this._map.setFallout(tile, false) + } this.addUpdate({ type: GameUpdateType.Tile, update: this.toTileUpdate(tile)