diff --git a/TODO.txt b/TODO.txt index 3b3fc016d..559617ff1 100644 --- a/TODO.txt +++ b/TODO.txt @@ -47,8 +47,8 @@ * improve menu (keep highlighted when click, allow deselect lobby) DONE 8/25/2024 * give time to (re) spawn at start of game DONE 8/25/2024 * show bar for long to respawn DONE 8/26/2024 -* store & delay tile updates for lag compensation -* BUG: error if don't spawn and then click after spawn mode +* store & delay tile updates for lag compensation DONE 8/26/2024 +* BUG: error if don't spawn and then click after spawn mode DONE 8/26/2024 * BUG: change player name after join lobby * REFACTOR: use new priority queue * BUG: players attack each other same time creates islands diff --git a/src/client/Client.ts b/src/client/Client.ts index 183d3d22e..abd9c5a0a 100644 --- a/src/client/Client.ts +++ b/src/client/Client.ts @@ -36,6 +36,12 @@ class Client { setFavicon() this.terrainMap = loadTerrainMap() this.startLobbyPolling() + setupUsernameCallback((username) => { + console.log('Username updated:', username); + if (this.game != null) { + this.game.playerName = username + } + }); } private startLobbyPolling(): void { @@ -118,6 +124,11 @@ class Client { g.stop(); }); } + + + + + } function getUsername(): string { @@ -129,6 +140,21 @@ function getUsername(): string { return 'Anon'; // Return 'Anon' if the input element is not found } +function setupUsernameCallback(callback: (username: string) => void): void { + const usernameInput = document.getElementById('username') as HTMLInputElement | null; + if (usernameInput) { + usernameInput.addEventListener('input', () => { + const username = getUsername(); + callback(username); + }); + } else { + console.error('Username input element not found'); + } +} + + + + // Initialize the client when the DOM is loaded document.addEventListener('DOMContentLoaded', () => { new Client().initialize(); diff --git a/src/client/ClientGame.ts b/src/client/ClientGame.ts index 3b6b21557..ed2fc059a 100644 --- a/src/client/ClientGame.ts +++ b/src/client/ClientGame.ts @@ -1,4 +1,4 @@ -import {Executor} from "../core/execution/Executor"; +import {Executor} from "../core/execution/ExecutionManager"; import {Cell, MutableGame, PlayerEvent, PlayerID, MutablePlayer, TileEvent, Player, Game, BoatEvent, Tile} from "../core/Game"; import {createGame} from "../core/GameImpl"; import {EventBus} from "../core/EventBus"; @@ -46,7 +46,7 @@ export class ClientGame { private isProcessingTurn = false constructor( - private playerName: string, + public playerName: string, private id: ClientID, private gameID: GameID, private eventBus: EventBus, @@ -86,6 +86,13 @@ export class ClientGame { if (!this.isActive) { this.start() } + this.sendIntent( + { + type: "updateName", + name: this.playerName, + clientID: this.id + } + ) } if (message.type == "turn") { this.addTurn(message.turn) @@ -163,7 +170,7 @@ export class ClientGame { private playerEvent(event: PlayerEvent) { console.log('received new player event!') - if (event.player.info().clientID == this.id) { + if (event.player.clientID() == this.id) { console.log('setting name') this.myPlayer = event.player } @@ -190,6 +197,9 @@ export class ClientGame { if (this.gs.inSpawnPhase()) { return } + if (this.myPlayer == null) { + return + } const owner = tile.owner() const targetID = owner.isPlayer() ? owner.id() : null; diff --git a/src/client/graphics/NameBoxCalculator.ts b/src/client/graphics/NameBoxCalculator.ts index dc0e37acc..361d27647 100644 --- a/src/client/graphics/NameBoxCalculator.ts +++ b/src/client/graphics/NameBoxCalculator.ts @@ -32,7 +32,7 @@ export function placeName(game: Game, player: Player): [position: Cell, fontSize Math.floor(largestRectangle.y + largestRectangle.height / 2 + boundingBox.min.y), ) - const fontSize = calculateFontSize(largestRectangle, player.info().name); + const fontSize = calculateFontSize(largestRectangle, player.name()); return [center, fontSize] } diff --git a/src/client/graphics/NameRenderer.ts b/src/client/graphics/NameRenderer.ts index 2a484251e..271aaf145 100644 --- a/src/client/graphics/NameRenderer.ts +++ b/src/client/graphics/NameRenderer.ts @@ -84,7 +84,7 @@ export class NameRenderer { isVisible(render: RenderInfo, min: Cell, max: Cell): boolean { const ratio = (max.x - min.x) / Math.max(20, (render.boundingBox.max.x - render.boundingBox.min.x)) - if (render.player.info().isBot) { + if (render.player.isBot()) { if (ratio > 25) { return false } @@ -121,7 +121,7 @@ export class NameRenderer { context.textAlign = 'center'; context.textBaseline = 'middle'; - context.fillText(render.player.info().name, nameCenterX, nameCenterY - render.fontSize / 2); + context.fillText(render.player.name(), nameCenterX, nameCenterY - render.fontSize / 2); context.font = `bold ${render.fontSize}px ${this.theme.font()}`; let troopsStr: string = "" let troops = render.player.troops() / 10 diff --git a/src/core/Game.ts b/src/core/Game.ts index 817e4945d..ca3131c72 100644 --- a/src/core/Game.ts +++ b/src/core/Game.ts @@ -82,8 +82,10 @@ export interface TerraNullius { } export interface Player { - info(): PlayerInfo + name(): string + clientID(): ClientID id(): PlayerID + isBot(): boolean troops(): number boats(): Boat[] ownsTile(cell: Cell): boolean @@ -99,6 +101,7 @@ export interface Player { } export interface MutablePlayer extends Player { + setName(name: string): void setTroops(troops: number): void addTroops(troops: number): void removeTroops(troops: number): void diff --git a/src/core/GameImpl.ts b/src/core/GameImpl.ts index a6dcae1e3..1df4629dd 100644 --- a/src/core/GameImpl.ts +++ b/src/core/GameImpl.ts @@ -1,6 +1,7 @@ import {Config} from "./configuration/Config"; import {EventBus} from "./EventBus"; import {Cell, Execution, MutableGame, Game, MutablePlayer, PlayerEvent, PlayerID, PlayerInfo, Player, TerraNullius, Tile, TileEvent, Boat, MutableBoat, BoatEvent} from "./Game"; +import {ClientID} from "./Schemas"; import {Terrain, TerrainMap, TerrainType} from "./TerrainMapLoader"; import {simpleHash} from "./Util"; @@ -123,7 +124,30 @@ export class PlayerImpl implements MutablePlayer { public _boats: BoatImpl[] = [] public _tiles: Map = new Map() - constructor(private gs: GameImpl, public readonly playerInfo: PlayerInfo, private _troops) { + private _name: string + + constructor(private gs: GameImpl, private readonly playerInfo: PlayerInfo, private _troops) { + this._name = playerInfo.name + } + + name(): string { + return this._name + } + + clientID(): ClientID { + return this.playerInfo.clientID + } + + id(): PlayerID { + return this.playerInfo.id + } + + isBot(): boolean { + return this.playerInfo.isBot + } + + setName(name: string) { + } addBoat(troops: number, tile: Tile, target: Player | TerraNullius): BoatImpl { @@ -190,7 +214,6 @@ export class PlayerImpl implements MutablePlayer { this.gs.relinquish(tile) } info(): PlayerInfo {return this.playerInfo} - id(): PlayerID {return this.playerInfo.id} troops(): number {return this._troops} isAlive(): boolean {return this._tiles.size > 0} gameState(): MutableGame {return this.gs} diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index e8e32d236..5fca9197c 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -3,11 +3,13 @@ import {z} from 'zod'; export type GameID = string export type ClientID = string -export type Intent = SpawnIntent | AttackIntent | BoatAttackIntent +export type Intent = SpawnIntent | AttackIntent | BoatAttackIntent | UpdateNameIntent export type AttackIntent = z.infer export type SpawnIntent = z.infer export type BoatAttackIntent = z.infer +export type UpdateNameIntent = z.infer + export type Turn = z.infer @@ -31,7 +33,7 @@ export interface Lobby { // Zod schemas const BaseIntentSchema = z.object({ - type: z.enum(['attack', 'spawn', 'boat']), + type: z.enum(['attack', 'spawn', 'boat', 'name']), clientID: z.string(), }); @@ -62,7 +64,12 @@ export const BoatAttackIntentSchema = BaseIntentSchema.extend({ y: z.number(), }) -const IntentSchema = z.union([AttackIntentSchema, SpawnIntentSchema, BoatAttackIntentSchema]); +export const UpdateNameIntentSchema = BaseIntentSchema.extend({ + type: z.literal('updateName'), + name: z.string(), +}) + +const IntentSchema = z.union([AttackIntentSchema, SpawnIntentSchema, BoatAttackIntentSchema, UpdateNameIntentSchema]); const TurnSchema = z.object({ turnNumber: z.number(), diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 279283c84..ac770b67d 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -56,7 +56,7 @@ export class DefaultPlayerConfig implements PlayerConfig { } attackAmount(attacker: Player, defender: Player | TerraNullius) { - if (attacker.info().isBot) { + if (attacker.isBot()) { return attacker.troops() / 20 } else { return attacker.troops() / 5 diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index 7cca37118..75fabccc4 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -7,10 +7,10 @@ export const devConfig = new class extends DefaultConfig { return 40 } gameCreationRate(): number { - return 3 * 1000 + return 10 * 1000 } lobbyLifetime(): number { - return 3 * 1000 + return 10 * 1000 } turnIntervalMs(): number { return 100 diff --git a/src/core/execution/Executor.ts b/src/core/execution/ExecutionManager.ts similarity index 88% rename from src/core/execution/Executor.ts rename to src/core/execution/ExecutionManager.ts index 3461ca79b..732f44410 100644 --- a/src/core/execution/Executor.ts +++ b/src/core/execution/ExecutionManager.ts @@ -6,6 +6,7 @@ import {SpawnExecution} from "./SpawnExecution"; import {BotSpawner} from "./BotSpawner"; import {BoatAttackExecution} from "./BoatAttackExecution"; import {PseudoRandom} from "../PseudoRandom"; +import {UpdateNameExecution} from "./UpdateNameExecution"; export class Executor { @@ -40,6 +41,11 @@ export class Executor { new Cell(intent.x, intent.y), intent.troops ) + } else if (intent.type == "updateName") { + return new UpdateNameExecution( + intent.name, + intent.clientID + ) } else { throw new Error(`intent type ${intent} not found`) } diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts index 50a0dbc6f..aca172119 100644 --- a/src/core/execution/SpawnExecution.ts +++ b/src/core/execution/SpawnExecution.ts @@ -28,7 +28,7 @@ export class SpawnExecution implements Execution { return } - const existing = this.mg.players().find(p => p.info().clientID != null && p.info().clientID == this.playerInfo.clientID) + const existing = this.mg.players().find(p => p.clientID() != null && p.clientID() == this.playerInfo.clientID) if (existing) { existing.tiles().forEach(t => existing.relinquish(t)) getSpawnCells(this.mg, this.cell).forEach(c => { @@ -42,7 +42,7 @@ export class SpawnExecution implements Execution { player.conquer(this.mg.tile(c)) }) this.mg.addExecution(new PlayerExecution(player.id())) - if (player.info().isBot) { + if (player.isBot()) { this.mg.addExecution(new BotExecution(player)) } this.active = false diff --git a/src/core/execution/UpdateNameExecution.ts b/src/core/execution/UpdateNameExecution.ts new file mode 100644 index 000000000..bc87ab20b --- /dev/null +++ b/src/core/execution/UpdateNameExecution.ts @@ -0,0 +1,36 @@ +import {Config, PlayerConfig} from "../configuration/Config" +import {Execution, MutableGame, MutablePlayer, PlayerID} from "../Game" +import {ClientID} from "../Schemas" + +export class UpdateNameExecution implements Execution { + + private active = true + private mg: MutableGame + + constructor(private newName: string, private clientID: ClientID) { + } + + init(mg: MutableGame, ticks: number) { + this.mg = mg + } + + tick(ticks: number) { + const player = this.mg.players().find(p => p.clientID() == this.clientID) + if (player == null) { + return + } + player.setName(this.newName) + this.active = false + } + + owner(): MutablePlayer { + return null + } + + isActive(): boolean { + return this.active + } + activeDuringSpawnPhase(): boolean { + return true + } +} \ No newline at end of file