diff --git a/resources/images/OpenFrontLogo.png b/resources/images/OpenFrontLogo.png new file mode 100644 index 000000000..b19a1dd16 Binary files /dev/null and b/resources/images/OpenFrontLogo.png differ diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts index 1d497918b..57fe25289 100644 --- a/src/client/PublicLobby.ts +++ b/src/client/PublicLobby.ts @@ -20,7 +20,7 @@ export class PublicLobby extends LitElement { this.fetchAndUpdateLobbies(); this.lobbiesInterval = window.setInterval( () => this.fetchAndUpdateLobbies(), - 1000, + 1000 ); } @@ -60,6 +60,11 @@ export class PublicLobby extends LitElement { const lobby = this.lobbies[0]; const timeRemaining = Math.max(0, Math.floor(lobby.msUntilStart / 1000)); + // Format time to show minutes and seconds + const minutes = Math.floor(timeRemaining / 60); + const seconds = timeRemaining % 60; + const timeDisplay = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`; + return html` `; @@ -93,7 +111,7 @@ export class PublicLobby extends LitElement { }, bubbles: true, composed: true, - }), + }) ); } else { this.dispatchEvent( @@ -101,7 +119,7 @@ export class PublicLobby extends LitElement { detail: { lobby: this.currLobby }, bubbles: true, composed: true, - }), + }) ); this.currLobby = null; } diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts index 596ece856..7e083e183 100644 --- a/src/client/graphics/layers/ControlPanel.ts +++ b/src/client/graphics/layers/ControlPanel.ts @@ -135,62 +135,80 @@ export class ControlPanel extends LitElement implements Layer { -
+
-
-
- { - this.targetTroopRatio = - parseInt((e.target as HTMLInputElement).value) / 100; - this.onTroopChange(this.targetTroopRatio); - }} - class="absolute w-full top-3 m-0 opacity-0 cursor-pointer" - /> +
+ +
+ +
+ + { + this.targetTroopRatio = + parseInt((e.target as HTMLInputElement).value) / 100; + this.onTroopChange(this.targetTroopRatio); + }} + class="absolute left-0 right-0 top-2 m-0 h-4 opacity-0 cursor-pointer" + /> + +
+
-
+
-
-
-
- { - this.attackRatio = - parseInt((e.target as HTMLInputElement).value) / 100; - this.onAttackRatioChange(this.attackRatio); - }} - class="absolute w-full top-3 m-0 opacity-0 cursor-pointer" - /> +
+ +
+ +
+ + { + this.attackRatio = + parseInt((e.target as HTMLInputElement).value) / 100; + this.onAttackRatioChange(this.attackRatio); + }} + class="absolute left-0 right-0 top-2 m-0 h-4 opacity-0 cursor-pointer" + /> + +
+
`; } + createRenderRoot() { return this; // Disable shadow DOM to allow Tailwind styles } diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts index 45ff67ed6..2161ff4be 100644 --- a/src/client/graphics/layers/NameLayer.ts +++ b/src/client/graphics/layers/NameLayer.ts @@ -25,7 +25,7 @@ class RenderInfo { public lastRenderCalc: number, public location: Cell, public fontSize: number, - public element: HTMLElement, + public element: HTMLElement ) {} } @@ -49,7 +49,7 @@ export class NameLayer implements Layer { private game: GameView, private theme: Theme, private transformHandler: TransformHandler, - private clientID: ClientID, + private clientID: ClientID ) { this.traitorIconImage = new Image(); this.traitorIconImage.src = traitorIcon; @@ -100,13 +100,7 @@ export class NameLayer implements Layer { if (!this.seenPlayers.has(player)) { this.seenPlayers.add(player); this.renders.push( - new RenderInfo( - player, - 0, - null, - 0, - this.createPlayerElement(player), - ), + new RenderInfo(player, 0, null, 0, this.createPlayerElement(player)) ); } } @@ -115,11 +109,11 @@ export class NameLayer implements Layer { public renderLayer(mainContex: CanvasRenderingContext2D) { const screenPosOld = this.transformHandler.worldToScreenCoordinates( - new Cell(0, 0), + new Cell(0, 0) ); const screenPos = new Cell( screenPosOld.x - window.innerWidth / 2, - screenPosOld.y - window.innerHeight / 2, + screenPosOld.y - window.innerHeight / 2 ); this.container.style.transform = `translate(${screenPos.x}px, ${screenPos.y}px) scale(${this.transformHandler.scale})`; @@ -136,7 +130,7 @@ export class NameLayer implements Layer { 0, 0, mainContex.canvas.width, - mainContex.canvas.height, + mainContex.canvas.height ); } @@ -191,7 +185,7 @@ export class NameLayer implements Layer { const oldLocation = render.location; render.location = new Cell( render.player.nameLocation().x, - render.player.nameLocation().y, + render.player.nameLocation().y ); // Calculate base size and scale @@ -230,7 +224,7 @@ export class NameLayer implements Layer { if (render.player === this.firstPlace) { if (!existingCrown) { iconsDiv.appendChild( - this.createIconElement(this.crownIconImage.src, iconSize, "crown"), + this.createIconElement(this.crownIconImage.src, iconSize, "crown") ); } } else if (existingCrown) { @@ -242,11 +236,7 @@ export class NameLayer implements Layer { if (render.player.isTraitor()) { if (!existingTraitor) { iconsDiv.appendChild( - this.createIconElement( - this.traitorIconImage.src, - iconSize, - "traitor", - ), + this.createIconElement(this.traitorIconImage.src, iconSize, "traitor") ); } } else if (existingTraitor) { @@ -261,8 +251,8 @@ export class NameLayer implements Layer { this.createIconElement( this.allianceIconImage.src, iconSize, - "alliance", - ), + "alliance" + ) ); } } else if (existingAlliance) { @@ -277,7 +267,7 @@ export class NameLayer implements Layer { ) { if (!existingTarget) { iconsDiv.appendChild( - this.createIconElement(this.targetIconImage.src, iconSize, "target"), + this.createIconElement(this.targetIconImage.src, iconSize, "target") ); } } else if (existingTarget) { @@ -291,7 +281,7 @@ export class NameLayer implements Layer { .filter( (emoji) => emoji.recipientID == AllPlayers || - emoji.recipientID == myPlayer?.smallID(), + emoji.recipientID == myPlayer?.smallID() ); if (emojis.length > 0) { @@ -324,7 +314,7 @@ export class NameLayer implements Layer { private createIconElement( src: string, size: number, - id: string, + id: string ): HTMLImageElement { const icon = document.createElement("img"); icon.src = src; diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index 2afc300bd..b6eef957d 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -11,6 +11,7 @@ import { manhattanDistFN, TileRef, } from "../../../core/game/GameMap"; +import { GameUpdateType } from "../../../core/game/GameUpdates"; enum Relationship { Self, @@ -48,9 +49,9 @@ export class UnitLayer implements Layer { if (this.myPlayer == null) { this.myPlayer = this.game.playerByClientID(this.clientID); } - for (const unit of this.game.units()) { - if (unit.wasUpdated()) this.onUnitEvent(unit); - } + this.game.updatesSinceLastTick()?.[GameUpdateType.Unit]?.forEach((unit) => { + this.onUnitEvent(this.game.unit(unit.id)); + }); } init() { @@ -79,9 +80,11 @@ export class UnitLayer implements Layer { this.canvas.width = this.game.width(); this.canvas.height = this.game.height(); - for (const unit of this.game.units()) { - this.onUnitEvent(unit); - } + this.game + ?.updatesSinceLastTick() + ?.[GameUpdateType.Unit]?.forEach((unit) => { + this.onUnitEvent(this.game.unit(unit.id)); + }); } private relationship(unit: UnitView): Relationship { diff --git a/src/client/index.html b/src/client/index.html index 054cfab15..dd76de985 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -74,6 +74,11 @@ gtag("js", new Date()); gtag("config", "AW-16702609763"); + -
- OpenFront.io -

+
+ OpenFront.io +
+
- (v0.15.0) -

+ v0.15.0 +
-
- -
+
+ +
diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 9abd1f3e4..1c402ccf9 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -85,6 +85,7 @@ export interface Lobby { id: string; msUntilStart?: number; numClients?: number; + gameConfig?: GameConfig; } const GameConfigSchema = z.object({ @@ -96,7 +97,7 @@ const GameConfigSchema = z.object({ const SafeString = z .string() // Remove common dangerous characters and patterns - .regex(/^[a-zA-Z0-9\s.,!?@#$%&*()-_+=[\]{}|;:"'\/]+$/) + .regex(/^[a-zA-Z0-9\s.,!?@#$%&*()-_+=\[\]{}|;:"'\/]+$/) // Reasonable max length to prevent DOS .max(1000); @@ -106,7 +107,7 @@ const EmojiSchema = z.string().refine( }, { message: "Must contain at least one emoji character", - }, + } ); const ID = z .string() diff --git a/src/core/Util.ts b/src/core/Util.ts index 109c9d770..a31bdb7f5 100644 --- a/src/core/Util.ts +++ b/src/core/Util.ts @@ -16,7 +16,7 @@ import { andFN, GameMap, manhattanDistFN, TileRef } from "./game/GameMap"; export function manhattanDistWrapped( c1: Cell, c2: Cell, - width: number, + width: number ): number { // Calculate x distance let dx = Math.abs(c1.x - c2.x); @@ -36,7 +36,7 @@ export function within(value: number, min: number, max: number): number { export function distSort( gm: GameMap, - target: TileRef, + target: TileRef ): (a: TileRef, b: TileRef) => number { return (a: TileRef, b: TileRef) => { return gm.manhattanDist(a, target) - gm.manhattanDist(b, target); @@ -45,7 +45,7 @@ export function distSort( export function distSortUnit( gm: GameMap, - target: Unit | TileRef, + target: Unit | TileRef ): (a: Unit, b: Unit) => number { const targetRef = typeof target === "number" ? target : target.tile(); @@ -61,7 +61,7 @@ export function distSortUnit( export function sourceDstOceanShore( gm: Game, src: Player, - tile: TileRef, + tile: TileRef ): [TileRef | null, TileRef | null] { const dst = gm.owner(tile); let srcTile = closestOceanShoreFromPlayer(gm, src, tile); @@ -88,10 +88,10 @@ export function targetTransportTile(gm: Game, tile: TileRef): TileRef | null { export function closestOceanShoreFromPlayer( gm: GameMap, player: Player, - target: TileRef, + target: TileRef ): TileRef | null { const shoreTiles = Array.from(player.borderTiles()).filter((t) => - gm.isOceanShore(t), + gm.isOceanShore(t) ); if (shoreTiles.length == 0) { return null; @@ -101,12 +101,12 @@ export function closestOceanShoreFromPlayer( const closestDistance = manhattanDistWrapped( gm.cell(target), gm.cell(closest), - gm.width(), + gm.width() ); const currentDistance = manhattanDistWrapped( gm.cell(target), gm.cell(current), - gm.width(), + gm.width() ); return currentDistance < closestDistance ? current : closest; }); @@ -115,13 +115,13 @@ export function closestOceanShoreFromPlayer( function closestOceanShoreTN( gm: GameMap, tile: TileRef, - searchDist: number, + searchDist: number ): TileRef { const tn = Array.from( gm.bfs( tile, - andFN((_, t) => !gm.hasOwner(t), manhattanDistFN(tile, searchDist)), - ), + andFN((_, t) => !gm.hasOwner(t), manhattanDistFN(tile, searchDist)) + ) ) .filter((t) => gm.isOceanShore(t)) .sort((a, b) => gm.manhattanDist(tile, a) - gm.manhattanDist(tile, b)); @@ -143,7 +143,7 @@ export function simpleHash(str: string): number { export function calculateBoundingBox( gm: GameMap, - borderTiles: ReadonlySet, + borderTiles: ReadonlySet ): { min: Cell; max: Cell } { let minX = Infinity, minY = Infinity, @@ -163,18 +163,18 @@ export function calculateBoundingBox( export function calculateBoundingBoxCenter( gm: GameMap, - borderTiles: ReadonlySet, + borderTiles: ReadonlySet ): Cell { const { min, max } = calculateBoundingBox(gm, borderTiles); return new Cell( min.x + Math.floor((max.x - min.x) / 2), - min.y + Math.floor((max.y - min.y) / 2), + min.y + Math.floor((max.y - min.y) / 2) ); } export function inscribed( outer: { min: Cell; max: Cell }, - inner: { min: Cell; max: Cell }, + inner: { min: Cell; max: Cell } ): boolean { return ( outer.min.x <= inner.min.x && @@ -208,7 +208,7 @@ export function getMode(list: Set): number { export function sanitize(name: string): string { return Array.from(name) .join("") - .replace(/[^\p{L}\p{N}\s\p{Emoji}\p{Emoji_Component}]/gu, ""); + .replace(/[^\p{L}\p{N}\s\p{Emoji}\p{Emoji_Component}\[\]]/gu, ""); } export function processName(name: string): string { @@ -238,7 +238,7 @@ export function processName(name: string): string { // Add CSS for the emoji images const withEmojiStyles = styledHTML.replace( / this.mg.manhattanDist(u.tile(), this.warship.tile()) < 100 + (u) => this.mg.manhattanDist(u.tile(), this.warship.tile()) < 130 ) .filter((u) => u.owner() != this.warship.owner()) .filter((u) => u != this.warship) diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index bde421c9e..ff1416c7f 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -333,10 +333,10 @@ export class GameView implements GameMap { } units(...types: UnitType[]): UnitView[] { if (types.length == 0) { - return Array.from(this._units.values()); + return Array.from(this._units.values()).filter((u) => u.isActive()); } - return Array.from(this._units.values()).filter((u) => - types.includes(u.type()) + return Array.from(this._units.values()).filter( + (u) => u.isActive() && types.includes(u.type()) ); } unit(id: number): UnitView { diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 2407dbef1..921008f5c 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -569,6 +569,10 @@ export class PlayerImpl implements Player { } switch (unitType) { case UnitType.MIRV: + if (!this.mg.hasOwner(targetTile)) { + return false; + } + return this.nukeSpawn(targetTile); case UnitType.AtomBomb: case UnitType.HydrogenBomb: return this.nukeSpawn(targetTile); diff --git a/src/core/validations/username.ts b/src/core/validations/username.ts index 069d6cd86..531defe68 100644 --- a/src/core/validations/username.ts +++ b/src/core/validations/username.ts @@ -13,7 +13,7 @@ const matcher = new RegExpMatcher({ export const MIN_USERNAME_LENGTH = 3; export const MAX_USERNAME_LENGTH = 20; -const validPattern = /^[a-zA-Z0-9_ ]+$/; +const validPattern = /^[a-zA-Z0-9_\[\] ]+$/; const shadowNames = [ "NicePeopleOnly", diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts index 2cfcf4cf1..b4ff3f7c0 100644 --- a/src/server/GameManager.ts +++ b/src/server/GameManager.ts @@ -5,12 +5,15 @@ import { Client } from "./Client"; import { GamePhase, GameServer } from "./GameServer"; import { Difficulty, GameMapType, GameType } from "../core/game/Game"; import { generateID } from "../core/Util"; +import { PseudoRandom } from "../core/PseudoRandom"; export class GameManager { private lastNewLobby: number = 0; private games: GameServer[] = []; + private random = new PseudoRandom(123); + constructor(private config: ServerConfig) {} public game(id: GameID): GameServer | null { @@ -46,7 +49,7 @@ export class GameManager { gameMap: GameMapType.World, gameType: GameType.Private, difficulty: Difficulty.Medium, - }), + }) ); return id; } @@ -54,7 +57,7 @@ export class GameManager { hasActiveGame(gameID: GameID): boolean { const game = this.games .filter( - (g) => g.phase() == GamePhase.Lobby || g.phase() == GamePhase.Active, + (g) => g.phase() == GamePhase.Lobby || g.phase() == GamePhase.Active ) .find((g) => g.id == gameID); return game != null; @@ -81,10 +84,10 @@ export class GameManager { this.lastNewLobby = now; lobbies.push( new GameServer(generateID(), now, true, this.config, { - gameMap: GameMapType.World, + gameMap: this.random.randElement(Object.values(GameMapType)), gameType: GameType.Public, difficulty: Difficulty.Medium, - }), + }) ); } diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 03af445ee..df12a0013 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -47,7 +47,7 @@ export class GameServer { public readonly createdAt: number, public readonly isPublic: boolean, private config: ServerConfig, - private gameConfig: GameConfig, + public gameConfig: GameConfig, ) {} public updateGameConfig(gameConfig: GameConfig): void { diff --git a/src/server/Server.ts b/src/server/Server.ts index 583a5b60e..baa8030fc 100644 --- a/src/server/Server.ts +++ b/src/server/Server.ts @@ -195,6 +195,7 @@ function updateLobbies() { id: g.id, msUntilStart: g.startTime() - Date.now(), numClients: g.numClients(), + gameConfig: g.gameConfig, })) .sort((a, b) => a.msUntilStart - b.msUntilStart), });