diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index ff7f393d4..44b822145 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -1,240 +1,305 @@ import { Executor } from "../core/execution/ExecutionManager"; -import { Cell, Game, PlayerID, GameMapType, Difficulty, GameType } from "../core/game/Game"; +import { + Cell, + Game, + PlayerID, + GameMapType, + Difficulty, + GameType, +} from "../core/game/Game"; import { EventBus } from "../core/EventBus"; import { createRenderer, GameRenderer } from "./graphics/GameRenderer"; -import { InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent } from "./InputHandler" -import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientMessageSchema, GameConfig, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn } from "../core/Schemas"; -import { loadTerrainFromFile, loadTerrainMap } from "../core/game/TerrainMapLoader"; -import { SendAttackIntentEvent, SendSpawnIntentEvent, Transport } from "./Transport"; +import { + InputHandler, + MouseUpEvent, + ZoomEvent, + DragEvent, + MouseDownEvent, +} from "./InputHandler"; +import { + ClientID, + ClientIntentMessageSchema, + ClientJoinMessageSchema, + ClientMessageSchema, + GameConfig, + GameID, + Intent, + ServerMessage, + ServerMessageSchema, + ServerSyncMessage, + Turn, +} from "../core/Schemas"; +import { + loadTerrainFromFile, + loadTerrainMap, +} from "../core/game/TerrainMapLoader"; +import { + SendAttackIntentEvent, + SendSpawnIntentEvent, + Transport, +} from "./Transport"; import { createCanvas } from "./Utils"; -import { MessageType } from '../core/game/Game'; +import { MessageType } from "../core/game/Game"; import { DisplayMessageUpdate, ErrorUpdate } from "../core/game/GameUpdates"; import { WorkerClient } from "../core/worker/WorkerClient"; import { consolex, initRemoteSender } from "../core/Consolex"; import { getConfig, getServerConfig } from "../core/configuration/Config"; import { GameView, PlayerView } from "../core/game/GameView"; -import { GameUpdateViewData } from '../core/game/GameUpdates'; +import { GameUpdateViewData } from "../core/game/GameUpdates"; export interface LobbyConfig { - playerName: () => string - clientID: ClientID, - playerID: PlayerID, - persistentID: string, - gameType: GameType - gameID: GameID, - map: GameMapType | null - difficulty: Difficulty | null + playerName: () => string; + clientID: ClientID; + playerID: PlayerID; + persistentID: string; + gameType: GameType; + gameID: GameID; + map: GameMapType | null; + difficulty: Difficulty | null; } -export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => void { - const eventBus = new EventBus() - initRemoteSender(eventBus) +export function joinLobby( + lobbyConfig: LobbyConfig, + onjoin: () => void, +): () => void { + const eventBus = new EventBus(); + initRemoteSender(eventBus); - consolex.log(`joinging lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}, persistentID: ${lobbyConfig.persistentID}`) + consolex.log( + `joinging lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}, persistentID: ${lobbyConfig.persistentID}`, + ); - const serverConfig = getServerConfig() + const serverConfig = getServerConfig(); - let gameConfig: GameConfig = null - if (lobbyConfig.gameType == GameType.Singleplayer) { - gameConfig = { - gameType: GameType.Singleplayer, - gameMap: lobbyConfig.map, - difficulty: lobbyConfig.difficulty, - } - } - - const transport = new Transport( - lobbyConfig, - gameConfig, - eventBus, - serverConfig, - ) - - const onconnect = () => { - consolex.log(`Joined game lobby ${lobbyConfig.gameID}`); - transport.joinGame(0) + let gameConfig: GameConfig = null; + if (lobbyConfig.gameType == GameType.Singleplayer) { + gameConfig = { + gameType: GameType.Singleplayer, + gameMap: lobbyConfig.map, + difficulty: lobbyConfig.difficulty, }; - const onmessage = (message: ServerMessage) => { - if (message.type == "start") { - consolex.log('lobby: game started') - onjoin() - createClientGame(lobbyConfig, message.config, eventBus, transport).then(r => r.start()) - }; - } - transport.connect(onconnect, onmessage) - return () => { - consolex.log('leaving game') - transport.leaveGame() + } + + const transport = new Transport( + lobbyConfig, + gameConfig, + eventBus, + serverConfig, + ); + + const onconnect = () => { + consolex.log(`Joined game lobby ${lobbyConfig.gameID}`); + transport.joinGame(0); + }; + const onmessage = (message: ServerMessage) => { + if (message.type == "start") { + consolex.log("lobby: game started"); + onjoin(); + createClientGame(lobbyConfig, message.config, eventBus, transport).then( + (r) => r.start(), + ); } + }; + transport.connect(onconnect, onmessage); + return () => { + consolex.log("leaving game"); + transport.leaveGame(); + }; } +export async function createClientGame( + lobbyConfig: LobbyConfig, + gameConfig: GameConfig, + eventBus: EventBus, + transport: Transport, +): Promise { + const config = getConfig(gameConfig); -export async function createClientGame(lobbyConfig: LobbyConfig, gameConfig: GameConfig, eventBus: EventBus, transport: Transport): Promise { - const config = getConfig(gameConfig) + const gameMap = await loadTerrainMap(gameConfig.gameMap); + const worker = new WorkerClient(lobbyConfig.gameID, gameConfig); + await worker.initialize(); + const gameView = new GameView( + worker, + config, + gameMap.gameMap, + lobbyConfig.clientID, + ); - const gameMap = await loadTerrainMap(gameConfig.gameMap); - const worker = new WorkerClient(lobbyConfig.gameID, gameConfig) - await worker.initialize() - const gameView = new GameView(worker, config, gameMap.gameMap, lobbyConfig.clientID) + consolex.log("going to init path finder"); + consolex.log("inited path finder"); + const canvas = createCanvas(); + let gameRenderer = createRenderer( + canvas, + gameView, + eventBus, + lobbyConfig.clientID, + ); + consolex.log( + `creating private game got difficulty: ${gameConfig.difficulty}`, + ); - consolex.log('going to init path finder') - consolex.log('inited path finder') - const canvas = createCanvas() - let gameRenderer = createRenderer(canvas, gameView, eventBus, lobbyConfig.clientID) - - - consolex.log(`creating private game got difficulty: ${gameConfig.difficulty}`) - - return new ClientGameRunner( - lobbyConfig.clientID, - eventBus, - gameRenderer, - new InputHandler(canvas, eventBus), - transport, - worker, - gameView - ) + return new ClientGameRunner( + lobbyConfig.clientID, + eventBus, + gameRenderer, + new InputHandler(canvas, eventBus), + transport, + worker, + gameView, + ); } export class ClientGameRunner { - private myPlayer: PlayerView - private isActive = false + private myPlayer: PlayerView; + private isActive = false; - private turnsSeen = 0 - private hasJoined = false + private turnsSeen = 0; + private hasJoined = false; - constructor( - private clientID: ClientID, - private eventBus: EventBus, - private renderer: GameRenderer, - private input: InputHandler, - private transport: Transport, - private worker: WorkerClient, - private gameView: GameView - ) { } + constructor( + private clientID: ClientID, + private eventBus: EventBus, + private renderer: GameRenderer, + private input: InputHandler, + private transport: Transport, + private worker: WorkerClient, + private gameView: GameView, + ) {} - public start() { - consolex.log('starting client game') - this.isActive = true - this.eventBus.on(MouseUpEvent, (e) => this.inputEvent(e)) + public start() { + consolex.log("starting client game"); + this.isActive = true; + this.eventBus.on(MouseUpEvent, (e) => this.inputEvent(e)); - this.renderer.initialize() - this.input.initialize() - this.worker.start((gu: GameUpdateViewData | ErrorUpdate) => { - if ('errMsg' in gu) { - showErrorModal(gu.errMsg, gu.stack, this.clientID) - return - } - this.gameView.update(gu) - this.renderer.tick() - }) - const worker = this.worker - const keepWorkerAlive = () => { - worker.sendHeartbeat - requestAnimationFrame(keepWorkerAlive) + this.renderer.initialize(); + this.input.initialize(); + this.worker.start((gu: GameUpdateViewData | ErrorUpdate) => { + if ("errMsg" in gu) { + showErrorModal(gu.errMsg, gu.stack, this.clientID); + return; + } + this.gameView.update(gu); + this.renderer.tick(); + }); + const worker = this.worker; + const keepWorkerAlive = () => { + worker.sendHeartbeat; + requestAnimationFrame(keepWorkerAlive); + }; + requestAnimationFrame(keepWorkerAlive); + + const onconnect = () => { + consolex.log("Connected to game server!"); + this.transport.joinGame(this.turnsSeen); + }; + const onmessage = (message: ServerMessage) => { + if (message.type == "start") { + this.hasJoined = true; + consolex.log("starting game!"); + for (const turn of message.turns) { + if (turn.turnNumber < this.turnsSeen) { + continue; + } + this.worker.sendTurn(turn); + this.turnsSeen++; } - requestAnimationFrame(keepWorkerAlive) + } + if (message.type == "turn") { + if (!this.hasJoined) { + this.transport.joinGame(0); + return; + } + if (this.turnsSeen != message.turn.turnNumber) { + consolex.error( + `got wrong turn have turns ${this.turnsSeen}, received turn ${message.turn.turnNumber}`, + ); + } else { + this.worker.sendTurn(message.turn); + this.turnsSeen++; + } + } + }; + this.transport.connect(onconnect, onmessage); + } - const onconnect = () => { - consolex.log('Connected to game server!'); - this.transport.joinGame(this.turnsSeen) - }; - const onmessage = (message: ServerMessage) => { - if (message.type == "start") { - this.hasJoined = true - consolex.log("starting game!") - for (const turn of message.turns) { - if (turn.turnNumber < this.turnsSeen) { - continue - } - this.worker.sendTurn(turn) - this.turnsSeen++ - } - } - if (message.type == "turn") { - if (!this.hasJoined) { - this.transport.joinGame(0) - return - } - if (this.turnsSeen != message.turn.turnNumber) { - consolex.error(`got wrong turn have turns ${this.turnsSeen}, received turn ${message.turn.turnNumber}`) - } else { - this.worker.sendTurn(message.turn) - this.turnsSeen++ - } - } - }; - this.transport.connect(onconnect, onmessage) + public stop() { + this.worker.cleanup(); + this.isActive = false; + this.transport.leaveGame(); + } + + private inputEvent(event: MouseUpEvent) { + if (!this.isActive) { + return; } - - public stop() { - this.worker.cleanup() - this.isActive = false - this.transport.leaveGame() + const cell = this.renderer.transformHandler.screenToWorldCoordinates( + event.x, + event.y, + ); + if (!this.gameView.isValidCoord(cell.x, cell.y)) { + return; } - - private inputEvent(event: MouseUpEvent) { - if (!this.isActive) { - return - } - const cell = this.renderer.transformHandler.screenToWorldCoordinates(event.x, event.y) - if (!this.gameView.isValidCoord(cell.x, cell.y)) { - return - } - consolex.log(`clicked cell ${cell}`) - const tile = this.gameView.ref(cell.x, cell.y) - if (this.gameView.isLand(tile) && !this.gameView.hasOwner(tile) && this.gameView.inSpawnPhase()) { - this.eventBus.emit(new SendSpawnIntentEvent(cell)) - return - } - if (this.gameView.inSpawnPhase()) { - return - } - if (this.myPlayer == null) { - this.myPlayer = this.gameView.playerByClientID(this.clientID) - if (this.myPlayer == null) { - return - } - } - this.myPlayer.actions(tile).then(actions => { - console.log(`got actions: ${JSON.stringify(actions)}`) - if (actions.canAttack) { - this.eventBus.emit( - new SendAttackIntentEvent( - this.gameView.owner(tile).id(), - this.myPlayer.troops() * this.renderer.uiState.attackRatio - ) - ) - } - }) + consolex.log(`clicked cell ${cell}`); + const tile = this.gameView.ref(cell.x, cell.y); + if ( + this.gameView.isLand(tile) && + !this.gameView.hasOwner(tile) && + this.gameView.inSpawnPhase() + ) { + this.eventBus.emit(new SendSpawnIntentEvent(cell)); + return; } + if (this.gameView.inSpawnPhase()) { + return; + } + if (this.myPlayer == null) { + this.myPlayer = this.gameView.playerByClientID(this.clientID); + if (this.myPlayer == null) { + return; + } + } + this.myPlayer.actions(tile).then((actions) => { + console.log(`got actions: ${JSON.stringify(actions)}`); + if (actions.canAttack) { + this.eventBus.emit( + new SendAttackIntentEvent( + this.gameView.owner(tile).id(), + this.myPlayer.troops() * this.renderer.uiState.attackRatio, + ), + ); + } + }); + } } function showErrorModal(errMsg: string, stack: string, clientID: ClientID) { - const errorText = `Error: ${errMsg}\nStack: ${stack}`; - consolex.error(errorText); + const errorText = `Error: ${errMsg}\nStack: ${stack}`; + consolex.error(errorText); - const modal = document.createElement('div'); - const content = `Game crashed! client id: ${clientID}\nPlease paste the following in your bug report in Discord:\n${errorText}`; + const modal = document.createElement("div"); + const content = `Game crashed! client id: ${clientID}\nPlease paste the following in your bug report in Discord:\n${errorText}`; - // Create elements - const pre = document.createElement('pre'); - pre.textContent = content; + // Create elements + const pre = document.createElement("pre"); + pre.textContent = content; - const button = document.createElement('button'); - button.textContent = 'Copy to clipboard'; - button.style.cssText = 'padding: 8px 16px; margin-top: 10px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;'; - button.addEventListener('click', () => { - navigator.clipboard.writeText(content) - .then(() => button.textContent = 'Copied!') - .catch(() => button.textContent = 'Failed to copy'); - }); + const button = document.createElement("button"); + button.textContent = "Copy to clipboard"; + button.style.cssText = + "padding: 8px 16px; margin-top: 10px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;"; + button.addEventListener("click", () => { + navigator.clipboard + .writeText(content) + .then(() => (button.textContent = "Copied!")) + .catch(() => (button.textContent = "Failed to copy")); + }); - // Add to modal - modal.style.cssText = 'position:fixed; padding:20px; background:white; border:1px solid black; top:50%; left:50%; transform:translate(-50%,-50%); z-index:9999;'; - modal.appendChild(pre); - modal.appendChild(button); + // Add to modal + modal.style.cssText = + "position:fixed; padding:20px; background:white; border:1px solid black; top:50%; left:50%; transform:translate(-50%,-50%); z-index:9999;"; + modal.appendChild(pre); + modal.appendChild(button); - document.body.appendChild(modal); -} \ No newline at end of file + document.body.appendChild(modal); +} diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index c5fd65104..8233cd584 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -1,19 +1,19 @@ -import { LitElement, html, css } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; -import { Difficulty, GameMapType, GameType } from '../core/game/Game'; -import { Lobby } from '../core/Schemas'; -import { consolex } from '../core/Consolex'; +import { LitElement, html, css } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { Difficulty, GameMapType, GameType } from "../core/game/Game"; +import { Lobby } from "../core/Schemas"; +import { consolex } from "../core/Consolex"; -@customElement('host-lobby-modal') +@customElement("host-lobby-modal") export class HostLobbyModal extends LitElement { @state() private isModalOpen = false; @state() private selectedMap: GameMapType = GameMapType.World; @state() private selectedDiffculty: Difficulty = Difficulty.Medium; - @state() private lobbyId = ''; + @state() private lobbyId = ""; @state() private copySuccess = false; - @state() private players: string[] = [] + @state() private players: string[] = []; - private playersInterval = null + private playersInterval = null; static styles = css` .modal-overlay { @@ -101,113 +101,154 @@ export class HostLobbyModal extends LitElement { render() { return html` -