can select map private game

This commit is contained in:
evanpelle
2024-10-12 20:54:18 -07:00
parent 3a2cd4e326
commit 5da1c2f620
7 changed files with 135 additions and 90 deletions
+1 -1
View File
@@ -164,7 +164,7 @@
* Make fake humans spawn by their country DONE 10/9/2024 * Make fake humans spawn by their country DONE 10/9/2024
* UI: leader board DONE 10/12/2024 * UI: leader board DONE 10/12/2024
* single player mode 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 * implement private game
* private game can select map * private game can select map
* optimize sendBoat function * optimize sendBoat function
+22 -10
View File
@@ -1,39 +1,52 @@
import {Executor} from "../core/execution/ExecutionManager"; 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 {createGame} from "../core/game/GameImpl";
import {EventBus} from "../core/EventBus"; 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 {createRenderer, GameRenderer} from "./graphics/GameRenderer";
import {InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent} from "./InputHandler" import {InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent} from "./InputHandler"
import {ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, ClientMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn} from "../core/Schemas"; 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 {and, bfs, dist, manhattanDist} from "../core/Util";
import {TerrainLayer} from "./graphics/layers/TerrainLayer";
import {WinCheckExecution} from "../core/execution/WinCheckExecution"; import {WinCheckExecution} from "../core/execution/WinCheckExecution";
import {SendAttackIntentEvent, SendSpawnIntentEvent, Transport} from "./Transport"; import {SendAttackIntentEvent, SendSpawnIntentEvent, Transport} from "./Transport";
import {createCanvas} from "./graphics/Utils"; import {createCanvas} from "./graphics/Utils";
import {DisplayMessageEvent, MessageType} from "./graphics/layers/EventsDisplay"; 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<ClientGame> {
let eventBus = new EventBus() 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) let game = createGame(terrainMap, eventBus, config)
const canvas = createCanvas() const canvas = createCanvas()
let gameRenderer = createRenderer(canvas, game, eventBus, clientID) 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( return new ClientGame(
clientID, clientID,
ip, gameConfig.ip,
gameID,
eventBus, eventBus,
game, game,
gameRenderer, gameRenderer,
new InputHandler(canvas, eventBus), new InputHandler(canvas, eventBus),
new Executor(game, gameID), new Executor(game, gameConfig.gameID),
transport, transport,
) )
} }
@@ -52,7 +65,6 @@ export class ClientGame {
constructor( constructor(
private id: ClientID, private id: ClientID,
private clientIP: string | null, private clientIP: string | null,
private gameID: GameID,
private eventBus: EventBus, private eventBus: EventBus,
private gs: Game, private gs: Game,
private renderer: GameRenderer, private renderer: GameRenderer,
+10 -22
View File
@@ -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 {ClientGame, createClientGame} from "./ClientGame";
import backgroundImage from '../../resources/images/TerrainMapFrontPage.png'; import backgroundImage from '../../resources/images/TerrainMapFrontPage.png';
import favicon from '../../resources/images/Favicon.png'; import favicon from '../../resources/images/Favicon.png';
import {v4 as uuidv4} from 'uuid';
import './PublicLobby'; import './PublicLobby';
import './UsernameInput'; import './UsernameInput';
@@ -13,19 +9,17 @@ import './UsernameInput';
import './styles.css'; import './styles.css';
import {UsernameInput} from "./UsernameInput"; import {UsernameInput} from "./UsernameInput";
import {SinglePlayerModal} from "./SinglePlayerModal"; import {SinglePlayerModal} from "./SinglePlayerModal";
import {GameMap} from "../core/game/Game";
const usernameKey: string = 'username'; const usernameKey: string = 'username';
class Client { class Client {
private terrainMap: Promise<TerrainMap>
private game: ClientGame private game: ClientGame
private ip: Promise<string | null> = null private ip: Promise<string | null> = null
private config: Config
private usernameInput: UsernameInput | null = null; private usernameInput: UsernameInput | null = null;
@@ -38,9 +32,7 @@ class Client {
console.warn('Username input element not found'); console.warn('Username input element not found');
} }
this.config = getConfig()
setFavicon() setFavicon()
this.terrainMap = loadTerrainMap()
this.ip = getClientIP() this.ip = getClientIP()
document.addEventListener('join-lobby', this.handleJoinLobby.bind(this)); document.addEventListener('join-lobby', this.handleJoinLobby.bind(this));
document.addEventListener('leave-lobby', this.handleLeaveLobby.bind(this)); document.addEventListener('leave-lobby', this.handleLeaveLobby.bind(this));
@@ -61,23 +53,19 @@ class Client {
private async handleJoinLobby(event: CustomEvent) { private async handleJoinLobby(event: CustomEvent) {
const lobby = event.detail.lobby const lobby = event.detail.lobby
console.log(`joining lobby ${lobby.id}`) console.log(`joining lobby ${lobby.id}`)
const [terrainMap, clientIP] = await Promise.all([ const clientIP = await this.ip
this.terrainMap,
this.ip
]);
console.log(`got ip ${clientIP}`) console.log(`got ip ${clientIP}`)
if (this.game != null) { if (this.game != null) {
this.game.stop() this.game.stop()
} }
this.game = createClientGame( this.game = await createClientGame(
event.detail.singlePlayer, {
(): string => {return this.usernameInput.getCurrentUsername()}, isLocal: event.detail.singlePlayer,
uuidv4(), playerName: (): string => this.usernameInput.getCurrentUsername(),
uuidv4(), gameID: lobby.id,
clientIP, ip: clientIP,
lobby.id, map: event.detail.map,
this.config, }
terrainMap
); );
this.game.join(); this.game.join();
const g = this.game; const g = this.game;
+6 -1
View File
@@ -1,6 +1,7 @@
import {LitElement, html, css} from 'lit'; import {LitElement, html, css} from 'lit';
import {customElement, state} from 'lit/decorators.js'; import {customElement, state} from 'lit/decorators.js';
import {Lobby} from "../core/Schemas"; import {Lobby} from "../core/Schemas";
import {GameMap} from '../core/game/Game';
@customElement('public-lobby') @customElement('public-lobby')
export class PublicLobby extends LitElement { export class PublicLobby extends LitElement {
@@ -108,7 +109,11 @@ export class PublicLobby extends LitElement {
if (this.currLobby == null) { if (this.currLobby == null) {
this.currLobby = lobby this.currLobby = lobby
this.dispatchEvent(new CustomEvent('join-lobby', { this.dispatchEvent(new CustomEvent('join-lobby', {
detail: {lobby: lobby, singlePlayer: false}, detail: {
lobby: lobby,
singlePlayer: false,
map: GameMap.World,
},
bubbles: true, bubbles: true,
composed: true composed: true
})); }));
+75 -50
View File
@@ -1,64 +1,72 @@
import {LitElement, html, css} from 'lit'; import {LitElement, html, css} from 'lit';
import {customElement, property, state} from 'lit/decorators.js'; import {customElement, property, state} from 'lit/decorators.js';
import {GameMap} from '../core/game/Game';
@customElement('single-player-modal') @customElement('single-player-modal')
export class SinglePlayerModal extends LitElement { export class SinglePlayerModal extends LitElement {
@state() private isModalOpen = false; @state() private isModalOpen = false;
@state() private selectedMap: GameMap = GameMap.World;
static styles = css` static styles = css`
.modal-overlay { .modal-overlay {
display: none; display: none;
position: fixed; position: fixed;
z-index: 1000; z-index: 1000;
left: 0; left: 0;
top: 0; top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
} }
.modal-content { .modal-content {
background-color: white; background-color: white;
margin: 15% auto; margin: 15% auto;
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
width: 80%; width: 80%;
max-width: 500px; max-width: 500px;
text-align: center; /* Center the content inside the modal */ text-align: center;
} }
.close { .close {
color: #aaa; color: #aaa;
float: right; float: right;
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
} }
.close:hover, .close:hover,
.close:focus { .close:focus {
color: black; color: black;
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
} }
button { button {
padding: 10px 20px; padding: 10px 20px;
font-size: 16px; font-size: 16px;
cursor: pointer; cursor: pointer;
background-color: #007bff; /* Changed to blue */ background-color: #007bff;
color: white; color: white;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
transition: background-color 0.3s; transition: background-color 0.3s;
display: inline-block; /* Ensures the button takes only necessary width */ display: inline-block;
margin-top: 20px; /* Adds some space above the button */ margin-top: 20px;
} }
button:hover { button:hover {
background-color: #0056b3; /* Darker blue for hover state */ background-color: #0056b3;
} }
select {
padding: 8px;
font-size: 16px;
margin-top: 10px;
width: 200px;
}
`; `;
render() { render() {
@@ -67,6 +75,18 @@ button:hover {
<div class="modal-content"> <div class="modal-content">
<span class="close" @click=${this.close}>&times;</span> <span class="close" @click=${this.close}>&times;</span>
<h2>Start Single Player Game</h2> <h2>Start Single Player Game</h2>
<div>
<label for="map-select">Map: </label>
<select id="map-select" @change=${this.handleMapChange}>
${Object.entries(GameMap)
.filter(([key]) => isNaN(Number(key)))
.map(([key, value]) => html`
<option value=${value} ?selected=${this.selectedMap === value}>
${key}
</option>
`)}
</select>
</div>
<button @click=${this.startGame}>Start Game</button> <button @click=${this.startGame}>Start Game</button>
</div> </div>
</div> </div>
@@ -81,14 +101,19 @@ button:hover {
this.isModalOpen = false; this.isModalOpen = false;
} }
private handleMapChange(e: Event) {
this.selectedMap = Number((e.target as HTMLSelectElement).value) as GameMap;
}
private startGame() { 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', { this.dispatchEvent(new CustomEvent('join-lobby', {
detail: { detail: {
singlePlayer: true, singlePlayer: true,
lobby: { lobby: {
id: "LOCAL", id: "LOCAL",
} },
map: this.selectedMap,
}, },
bubbles: true, bubbles: true,
composed: true composed: true
+5
View File
@@ -11,6 +11,11 @@ export type Tick = number
export const AllPlayers = "AllPlayers" as const; export const AllPlayers = "AllPlayers" as const;
export enum GameMap {
World,
Europe
}
export class Nation { export class Nation {
constructor( constructor(
public readonly name: string, public readonly name: string,
+16 -6
View File
@@ -1,6 +1,13 @@
import {Cell, TerrainType} from './Game'; import {Cell, GameMap, TerrainType} from './Game';
import binAsString from "!!binary-loader!../../../resources/maps/Europe.bin"; import europeBin from "!!binary-loader!../../../resources/maps/Europe.bin";
import worldMapInfo from "../../../resources/maps/Europe.json" 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 { export interface NationMap {
name: string; name: string;
@@ -44,10 +51,13 @@ export class Terrain {
constructor(public type: TerrainType) { } constructor(public type: TerrainType) { }
} }
export async function loadTerrainMap(): Promise<TerrainMap> { export async function loadTerrainMap(map: GameMap): Promise<TerrainMap> {
const mapData = maps.get(map)
// Simulate an asynchronous file load // Simulate an asynchronous file load
const fileData = await new Promise<string>((resolve) => { const fileData = await new Promise<string>((resolve) => {
setTimeout(() => resolve(binAsString), 100); setTimeout(() => resolve(mapData.bin), 100);
}); });
console.log(`Loaded data length: ${fileData.length} bytes`); console.log(`Loaded data length: ${fileData.length} bytes`);
@@ -103,7 +113,7 @@ export async function loadTerrainMap(): Promise<TerrainMap> {
} }
} }
return new TerrainMap(terrain, numLand, worldMapInfo); return new TerrainMap(terrain, numLand, mapData.info);
} }
function logBinaryAsAscii(data: string, length: number = 8) { function logBinaryAsAscii(data: string, length: number = 8) {