diff --git a/TODO.txt b/TODO.txt index a65beec78..c41621573 100644 --- a/TODO.txt +++ b/TODO.txt @@ -164,7 +164,7 @@ * Make fake humans spawn by their country DONE 10/9/2024 * UI: leader board DONE 10/12/2024 * single player mode DONE 10/12/2024 -* single player select map +* single player select map DONE 10/12/2024 * implement private game * private game can select map * optimize sendBoat function diff --git a/src/client/ClientGame.ts b/src/client/ClientGame.ts index 841fbd801..2495f89c2 100644 --- a/src/client/ClientGame.ts +++ b/src/client/ClientGame.ts @@ -1,39 +1,52 @@ import {Executor} from "../core/execution/ExecutionManager"; -import {Cell, MutableGame, PlayerEvent, PlayerID, MutablePlayer, TileEvent, Player, Game, BoatEvent, Tile, PlayerType} from "../core/game/Game"; +import {Cell, MutableGame, PlayerEvent, PlayerID, MutablePlayer, TileEvent, Player, Game, BoatEvent, Tile, PlayerType, GameMap} from "../core/game/Game"; import {createGame} from "../core/game/GameImpl"; import {EventBus} from "../core/EventBus"; -import {Config} from "../core/configuration/Config"; +import {Config, getConfig} from "../core/configuration/Config"; import {createRenderer, GameRenderer} from "./graphics/GameRenderer"; import {InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent} from "./InputHandler" import {ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, ClientMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn} from "../core/Schemas"; -import {TerrainMap} from "../core/game/TerrainMapLoader"; +import {loadTerrainMap, TerrainMap} from "../core/game/TerrainMapLoader"; import {and, bfs, dist, manhattanDist} from "../core/Util"; -import {TerrainLayer} from "./graphics/layers/TerrainLayer"; import {WinCheckExecution} from "../core/execution/WinCheckExecution"; import {SendAttackIntentEvent, SendSpawnIntentEvent, Transport} from "./Transport"; import {createCanvas} from "./graphics/Utils"; import {DisplayMessageEvent, MessageType} from "./graphics/layers/EventsDisplay"; +import {v4 as uuidv4} from 'uuid'; -export function createClientGame(isLocal: boolean, playerName: () => string, clientID: ClientID, playerID: PlayerID, ip: string | null, gameID: GameID, config: Config, terrainMap: TerrainMap): ClientGame { +export interface GameConfig { + isLocal: boolean + playerName: () => string + gameID: GameID + ip: string | null + map: GameMap +} + +export async function createClientGame(gameConfig: GameConfig): Promise { let eventBus = new EventBus() + const config = getConfig() + + const clientID = uuidv4() + const playerID = uuidv4() + + const terrainMap = await loadTerrainMap(gameConfig.map) let game = createGame(terrainMap, eventBus, config) const canvas = createCanvas() let gameRenderer = createRenderer(canvas, game, eventBus, clientID) - const transport = new Transport(isLocal, eventBus, gameID, clientID, playerID, config, playerName) + const transport = new Transport(gameConfig.isLocal, eventBus, gameConfig.gameID, clientID, playerID, config, gameConfig.playerName) return new ClientGame( clientID, - ip, - gameID, + gameConfig.ip, eventBus, game, gameRenderer, new InputHandler(canvas, eventBus), - new Executor(game, gameID), + new Executor(game, gameConfig.gameID), transport, ) } @@ -52,7 +65,6 @@ export class ClientGame { constructor( private id: ClientID, private clientIP: string | null, - private gameID: GameID, private eventBus: EventBus, private gs: Game, private renderer: GameRenderer, diff --git a/src/client/Main.ts b/src/client/Main.ts index 18198a4c1..693731a4a 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -1,10 +1,6 @@ -import {Config, getConfig} from "../core/configuration/Config"; -import {GameID, Lobby, ServerMessage, ServerMessageSchema} from "../core/Schemas"; -import {loadTerrainMap, TerrainMap} from "../core/game/TerrainMapLoader"; import {ClientGame, createClientGame} from "./ClientGame"; import backgroundImage from '../../resources/images/TerrainMapFrontPage.png'; import favicon from '../../resources/images/Favicon.png'; -import {v4 as uuidv4} from 'uuid'; import './PublicLobby'; import './UsernameInput'; @@ -13,19 +9,17 @@ import './UsernameInput'; import './styles.css'; import {UsernameInput} from "./UsernameInput"; import {SinglePlayerModal} from "./SinglePlayerModal"; +import {GameMap} from "../core/game/Game"; const usernameKey: string = 'username'; class Client { - private terrainMap: Promise private game: ClientGame private ip: Promise = null - private config: Config - private usernameInput: UsernameInput | null = null; @@ -38,9 +32,7 @@ class Client { console.warn('Username input element not found'); } - this.config = getConfig() setFavicon() - this.terrainMap = loadTerrainMap() this.ip = getClientIP() document.addEventListener('join-lobby', this.handleJoinLobby.bind(this)); document.addEventListener('leave-lobby', this.handleLeaveLobby.bind(this)); @@ -61,23 +53,19 @@ class Client { private async handleJoinLobby(event: CustomEvent) { const lobby = event.detail.lobby console.log(`joining lobby ${lobby.id}`) - const [terrainMap, clientIP] = await Promise.all([ - this.terrainMap, - this.ip - ]); + const clientIP = await this.ip console.log(`got ip ${clientIP}`) if (this.game != null) { this.game.stop() } - this.game = createClientGame( - event.detail.singlePlayer, - (): string => {return this.usernameInput.getCurrentUsername()}, - uuidv4(), - uuidv4(), - clientIP, - lobby.id, - this.config, - terrainMap + this.game = await createClientGame( + { + isLocal: event.detail.singlePlayer, + playerName: (): string => this.usernameInput.getCurrentUsername(), + gameID: lobby.id, + ip: clientIP, + map: event.detail.map, + } ); this.game.join(); const g = this.game; diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts index 23d0fe76b..f812e796f 100644 --- a/src/client/PublicLobby.ts +++ b/src/client/PublicLobby.ts @@ -1,6 +1,7 @@ import {LitElement, html, css} from 'lit'; import {customElement, state} from 'lit/decorators.js'; import {Lobby} from "../core/Schemas"; +import {GameMap} from '../core/game/Game'; @customElement('public-lobby') export class PublicLobby extends LitElement { @@ -108,7 +109,11 @@ export class PublicLobby extends LitElement { if (this.currLobby == null) { this.currLobby = lobby this.dispatchEvent(new CustomEvent('join-lobby', { - detail: {lobby: lobby, singlePlayer: false}, + detail: { + lobby: lobby, + singlePlayer: false, + map: GameMap.World, + }, bubbles: true, composed: true })); diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index cc2c7fbb8..a1ed2cc28 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -1,64 +1,72 @@ import {LitElement, html, css} from 'lit'; import {customElement, property, state} from 'lit/decorators.js'; - +import {GameMap} from '../core/game/Game'; @customElement('single-player-modal') export class SinglePlayerModal extends LitElement { @state() private isModalOpen = false; + @state() private selectedMap: GameMap = GameMap.World; static styles = css` -.modal-overlay { - display: none; - position: fixed; - z-index: 1000; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); -} + .modal-overlay { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + } -.modal-content { - background-color: white; - margin: 15% auto; - padding: 20px; - border-radius: 8px; - width: 80%; - max-width: 500px; - text-align: center; /* Center the content inside the modal */ -} + .modal-content { + background-color: white; + margin: 15% auto; + padding: 20px; + border-radius: 8px; + width: 80%; + max-width: 500px; + text-align: center; + } -.close { - color: #aaa; - float: right; - font-size: 28px; - font-weight: bold; - cursor: pointer; -} + .close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + cursor: pointer; + } -.close:hover, -.close:focus { - color: black; - text-decoration: none; - cursor: pointer; -} + .close:hover, + .close:focus { + color: black; + text-decoration: none; + cursor: pointer; + } -button { - padding: 10px 20px; - font-size: 16px; - cursor: pointer; - background-color: #007bff; /* Changed to blue */ - color: white; - border: none; - border-radius: 4px; - transition: background-color 0.3s; - display: inline-block; /* Ensures the button takes only necessary width */ - margin-top: 20px; /* Adds some space above the button */ -} + button { + padding: 10px 20px; + font-size: 16px; + cursor: pointer; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + transition: background-color 0.3s; + display: inline-block; + margin-top: 20px; + } -button:hover { - background-color: #0056b3; /* Darker blue for hover state */ -} + button:hover { + background-color: #0056b3; + } + + select { + padding: 8px; + font-size: 16px; + margin-top: 10px; + width: 200px; + } `; render() { @@ -67,6 +75,18 @@ button:hover { @@ -81,14 +101,19 @@ button:hover { this.isModalOpen = false; } + private handleMapChange(e: Event) { + this.selectedMap = Number((e.target as HTMLSelectElement).value) as GameMap; + } + private startGame() { - console.log('Starting single player game...'); + console.log(`Starting single player game with map: ${GameMap[this.selectedMap]}`); this.dispatchEvent(new CustomEvent('join-lobby', { detail: { singlePlayer: true, lobby: { id: "LOCAL", - } + }, + map: this.selectedMap, }, bubbles: true, composed: true diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 3d9a3724c..0fd090902 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -11,6 +11,11 @@ export type Tick = number export const AllPlayers = "AllPlayers" as const; +export enum GameMap { + World, + Europe +} + export class Nation { constructor( public readonly name: string, diff --git a/src/core/game/TerrainMapLoader.ts b/src/core/game/TerrainMapLoader.ts index 4cafa1806..0472718aa 100644 --- a/src/core/game/TerrainMapLoader.ts +++ b/src/core/game/TerrainMapLoader.ts @@ -1,6 +1,13 @@ -import {Cell, TerrainType} from './Game'; -import binAsString from "!!binary-loader!../../../resources/maps/Europe.bin"; -import worldMapInfo from "../../../resources/maps/Europe.json" +import {Cell, GameMap, TerrainType} from './Game'; +import europeBin from "!!binary-loader!../../../resources/maps/Europe.bin"; +import europeInfo from "../../../resources/maps/Europe.json" + +import worldBin from "!!binary-loader!../../../resources/maps/WorldMap.bin"; +import worldInfo from "../../../resources/maps/WorldMap.json" + +const maps = new Map() + .set(GameMap.World, {bin: worldBin, info: worldInfo}) + .set(GameMap.Europe, {bin: europeBin, info: europeInfo}); export interface NationMap { name: string; @@ -44,10 +51,13 @@ export class Terrain { constructor(public type: TerrainType) { } } -export async function loadTerrainMap(): Promise { +export async function loadTerrainMap(map: GameMap): Promise { + + const mapData = maps.get(map) + // Simulate an asynchronous file load const fileData = await new Promise((resolve) => { - setTimeout(() => resolve(binAsString), 100); + setTimeout(() => resolve(mapData.bin), 100); }); console.log(`Loaded data length: ${fileData.length} bytes`); @@ -103,7 +113,7 @@ export async function loadTerrainMap(): Promise { } } - return new TerrainMap(terrain, numLand, worldMapInfo); + return new TerrainMap(terrain, numLand, mapData.info); } function logBinaryAsAscii(data: string, length: number = 8) {