diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 diff --git a/__mocks__/jose.js b/__mocks__/jose.js new file mode 100644 index 0000000000..81f36f295b --- /dev/null +++ b/__mocks__/jose.js @@ -0,0 +1,8 @@ +// Minimal mock of jose for tests +export const base64url = { + encode: (input) => Buffer.from(String(input)).toString("base64url"), + decode: (input) => Buffer.from(String(input), "base64url").toString("utf8"), +}; +export const jwtVerify = async () => ({ payload: {}, protectedHeader: {} }); +export const decodeJwt = () => ({ sub: "test" }); +export const JWK = {}; diff --git a/eslint.config.js b/eslint.config.js index 3168893d5b..4d3c5217ac 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -25,6 +25,7 @@ export default [ projectService: { allowDefaultProject: [ "__mocks__/fileMock.js", + "__mocks__/jose.js", "eslint.config.js", "scripts/sync-assets.mjs", ], diff --git a/resources/lang/en.json b/resources/lang/en.json index 290353f6a6..911ee80f5c 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -612,6 +612,12 @@ "stop_trading": "Stop trading with [P1]!" } }, + "lobby_chat": { + "title": "Lobby Chat", + "placeholder": "Type a message...", + "enable": "Enable Lobby Chat", + "send": "Send" + }, "build_menu": { "desc": { "atom_bomb": "Small explosion", diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 995decabe7..ebe94c47d2 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -7,6 +7,7 @@ import { GameStartInfo, PlayerCosmeticRefs, PlayerRecord, + ServerLobbyChatMessage, ServerMessage, } from "../core/Schemas"; import { createPartialGameRecord, replacer } from "../core/Util"; @@ -64,6 +65,18 @@ export interface LobbyConfig { gameRecord?: GameRecord; } +function isValidLobbyChatMessage( + message: ServerMessage, +): message is ServerLobbyChatMessage { + if (message.type !== "lobby_chat") return false; + const candidate = message as Record; + return ( + typeof candidate.username === "string" && + typeof candidate.isHost === "boolean" && + typeof candidate.text === "string" + ); +} + export function joinLobby( eventBus: EventBus, lobbyConfig: LobbyConfig, @@ -74,6 +87,12 @@ export function joinLobby( `joining lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}`, ); + window.__eventBus = eventBus; + window.__username = lobbyConfig.playerName; + document.dispatchEvent( + new CustomEvent("event-bus:ready", { bubbles: true, composed: true }), + ); + const userSettings: UserSettings = new UserSettings(); startGame(lobbyConfig.gameID, lobbyConfig.gameStartInfo?.config ?? {}); @@ -163,6 +182,23 @@ export function joinLobby( ); } } + if (message.type === "lobby_chat") { + if (!isValidLobbyChatMessage(message)) { + console.error("Malformed lobby_chat message:", message); + return; + } + document.dispatchEvent( + new CustomEvent("lobby-chat:message", { + detail: { + username: message.username, + isHost: message.isHost, + text: message.text, + }, + bubbles: true, + composed: true, + }), + ); + } }; transport.connect(onconnect, onmessage); return () => { diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 8d87feddf8..1bd3233f03 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -27,6 +27,7 @@ import "./components/baseComponents/Modal"; import { BaseModal } from "./components/BaseModal"; import "./components/Difficulties"; import "./components/FluentSlider"; +import "./components/LobbyChatPanel"; import "./components/LobbyTeamView"; import "./components/Maps"; import { crazyGamesSDK } from "./CrazyGamesSDK"; @@ -58,6 +59,7 @@ export class HostLobbyModal extends BaseModal { @state() private instantBuild: boolean = false; @state() private randomSpawn: boolean = false; @state() private compactMap: boolean = false; + @state() private chatEnabled: boolean = false; @state() private lobbyId = ""; @state() private copySuccess = false; @state() private clients: ClientInfo[] = []; @@ -559,6 +561,11 @@ export class HostLobbyModal extends BaseModal { this.compactMap, this.handleCompactMapChange, )} + ${this.renderOptionToggle( + "lobby_chat.enable", + this.chatEnabled, + this.handleChatEnabledChange, + )}
this.kickPlayer(clientID)} > + + ${this.chatEnabled + ? html` +
+
+ ${translateText("lobby_chat.title")} +
+ +
+ ` + : ""}
@@ -1027,6 +1047,11 @@ export class HostLobbyModal extends BaseModal { this.putGameConfig(); }; + private handleChatEnabledChange = (val: boolean) => { + this.chatEnabled = val; + this.putGameConfig(); + }; + private handleDonateTroopsChange = (val: boolean) => { this.donateTroops = val; this.putGameConfig(); @@ -1108,6 +1133,7 @@ export class HostLobbyModal extends BaseModal { : { disableNations: this.disableNations, }), + chatEnabled: this.chatEnabled, maxTimerValue: this.maxTimer === true ? this.maxTimerValue : undefined, } satisfies Partial, diff --git a/src/client/JoinPrivateLobbyModal.ts b/src/client/JoinPrivateLobbyModal.ts index f6531472f2..50445fdbf5 100644 --- a/src/client/JoinPrivateLobbyModal.ts +++ b/src/client/JoinPrivateLobbyModal.ts @@ -15,6 +15,7 @@ import { getApiBase } from "./Api"; import { JoinLobbyEvent } from "./Main"; import { BaseModal } from "./components/BaseModal"; import "./components/Difficulties"; +import "./components/LobbyChatPanel"; import "./components/LobbyTeamView"; @customElement("join-private-lobby-modal") export class JoinPrivateLobbyModal extends BaseModal { @@ -27,6 +28,7 @@ export class JoinPrivateLobbyModal extends BaseModal { @state() private lobbyIdVisible: boolean = true; @state() private copySuccess: boolean = false; @state() private currentLobbyId: string = ""; + @state() private chatEnabled: boolean = false; private playersInterval: NodeJS.Timeout | null = null; private userSettings: UserSettings = new UserSettings(); @@ -201,6 +203,19 @@ export class JoinPrivateLobbyModal extends BaseModal { .lobbyCreatorClientID=${this.lobbyCreatorClientID} .teamCount=${this.gameConfig?.playerTeams ?? 2} > + + ${this.chatEnabled + ? html` +
+
+ ${translateText("lobby_chat.title")} +
+ +
+ ` + : ""} ` : ""} @@ -611,6 +626,7 @@ export class JoinPrivateLobbyModal extends BaseModal { this.players = data.clients ?? []; if (data.gameConfig) { this.gameConfig = data.gameConfig; + this.chatEnabled = data.gameConfig.chatEnabled ?? false; } }) .catch((error) => { diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index c869c5877d..10f49ebe31 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -884,6 +884,7 @@ export class SinglePlayerModal extends BaseModal { : GameMapSize.Normal, gameType: GameType.Singleplayer, gameMode: this.gameMode, + chatEnabled: false, playerTeams: this.teamCount, difficulty: this.selectedDifficulty, maxTimerValue: finalMaxTimerValue, diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 1f35131a48..53a4698c5b 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -15,6 +15,7 @@ import { ClientHashMessage, ClientIntentMessage, ClientJoinMessage, + ClientLobbyChatMessage, ClientMessage, ClientPingMessage, ClientRejoinMessage, @@ -176,6 +177,11 @@ export class SendKickPlayerIntentEvent implements GameEvent { constructor(public readonly target: string) {} } +// New event: send a basic lobby chat message +export class SendLobbyChatEvent implements GameEvent { + constructor(public readonly text: string) {} +} + export class SendUpdateGameConfigIntentEvent implements GameEvent { constructor(public readonly config: Partial) {} } @@ -265,6 +271,7 @@ export class Transport { this.eventBus.on(SendKickPlayerIntentEvent, (e) => this.onSendKickPlayerIntent(e), ); + this.eventBus.on(SendLobbyChatEvent, (e) => this.onSendLobbyChat(e)); this.eventBus.on(SendUpdateGameConfigIntentEvent, (e) => this.onSendUpdateGameConfigIntent(e), @@ -668,6 +675,14 @@ export class Transport { }); } + private onSendLobbyChat(event: SendLobbyChatEvent) { + this.sendMsg({ + type: "lobby_chat", + text: event.text, + clientID: this.lobbyConfig.clientID, + } satisfies ClientLobbyChatMessage); + } + private onSendUpdateGameConfigIntent(event: SendUpdateGameConfigIntentEvent) { this.sendIntent({ type: "update_game_config", diff --git a/src/client/components/LobbyChatPanel.ts b/src/client/components/LobbyChatPanel.ts new file mode 100644 index 0000000000..e4cf8da245 --- /dev/null +++ b/src/client/components/LobbyChatPanel.ts @@ -0,0 +1,272 @@ +import { LitElement, html } from "lit"; +import { customElement, state } from "lit/decorators.js"; +import { translateText } from "../../client/Utils"; +import { EventBus } from "../../core/EventBus"; +import { SendLobbyChatEvent } from "../Transport"; + +interface ChatMessage { + username: string; + isHost: boolean; + text: string; +} + +type LobbyChatMessageEvent = CustomEvent<{ + username: string; + isHost: boolean; + text: string; +}>; + +type EventBusReadyEvent = CustomEvent; + +@customElement("lobby-chat-panel") +export class LobbyChatPanel extends LitElement { + @state() private messages: ChatMessage[] = []; + @state() private inputText: string = ""; + + private bus: EventBus | null = null; + private username: string | null = null; + + connectedCallback(): void { + super.connectedCallback(); + document.addEventListener( + "lobby-chat:message", + this.onIncoming as EventListener, + ); + const globalBus = window.__eventBus; + if (globalBus) { + this.bus = globalBus; + } + this.username = window.__username ?? null; + document.addEventListener( + "event-bus:ready", + this.onBusReady as EventListener, + ); + } + + disconnectedCallback(): void { + document.removeEventListener( + "lobby-chat:message", + this.onIncoming as EventListener, + ); + document.removeEventListener( + "event-bus:ready", + this.onBusReady as EventListener, + ); + super.disconnectedCallback(); + } + + setEventBus(bus: EventBus) { + this.bus = bus; + } + + private onIncoming = async (e: LobbyChatMessageEvent) => { + const { username, isHost, text } = e.detail; + this.messages = [...this.messages, { username, isHost, text }]; + await this.updateComplete; + const container = this.renderRoot.querySelector( + ".lcp-messages", + ) as HTMLElement | null; + if (container) container.scrollTop = container.scrollHeight; + }; + + private onBusReady = (_e: EventBusReadyEvent) => { + const globalBus = window.__eventBus; + if (globalBus) { + this.bus = globalBus; + } + this.username ??= window.__username ?? null; + }; + + private get canSend(): boolean { + return this.bus !== null || window.__eventBus !== undefined; + } + + private sendMessage() { + const text = this.inputText.trim(); + if (!text) return; + + // Try to get the bus from global if not already set + if (!this.bus) { + const globalBus = window.__eventBus; + if (globalBus) { + this.bus = globalBus; + } + } + + // If still no bus, don't clear input - user can retry + if (!this.bus) { + return; + } + + const capped = text.slice(0, 300); + this.bus.emit(new SendLobbyChatEvent(capped)); + this.inputText = ""; + } + + render() { + return html` +
+
+ ${this.messages.map((m) => { + const displayName = m.isHost ? `${m.username} (Host)` : m.username; + const isLocal = + this.username !== null && m.username === this.username; + const msgClass = isLocal + ? "lcp-msg lcp-msg--local" + : "lcp-msg lcp-msg--remote"; + return html`
+ ${displayName}: ${m.text} +
`; + })} +
+
+ + (this.inputText = (e.target as HTMLInputElement).value)} + @keydown=${(e: KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + this.sendMessage(); + } + }} + placeholder=${translateText("lobby_chat.placeholder")} + /> + +
+
+ `; + } + + createRenderRoot() { + return this; // use light DOM for existing styles + } +} + +if (!document.head.querySelector("#lcp-styles")) { + const style = document.createElement("style"); + style.id = "lcp-styles"; + style.textContent = ` + .lcp-container { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 240px; + width: 100%; + } + .lcp-messages { + overflow-y: auto; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 8px; + height: 150px; + min-height: 120px; + background: rgba(0, 0, 0, 0.5); + color: #ddd; + display: flex; + flex-direction: column; + gap: 6px; + -webkit-overflow-scrolling: touch; + } + .lcp-msg { + font-size: 0.85rem; + padding: 8px 12px; + border-radius: 12px; + background: rgba(0, 0, 0, 0.6); + max-width: 85%; + word-wrap: break-word; + overflow-wrap: break-word; + } + .lcp-msg--local { + align-self: flex-end; + text-align: right; + background: rgba(36, 59, 85, 0.7); + } + .lcp-msg--remote { + align-self: flex-start; + text-align: left; + background: rgba(0, 0, 0, 0.6); + } + .lcp-sender { + color: #9ae6b4; + margin-right: 4px; + font-weight: 500; + } + .lcp-input-row { + display: flex; + gap: 8px; + flex-wrap: nowrap; + } + .lcp-input { + flex: 1; + min-width: 0; + border-radius: 8px; + padding: 10px 12px; + font-size: 16px; + color: #000; + background: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(255, 255, 255, 0.2); + -webkit-appearance: none; + appearance: none; + } + .lcp-input:focus { + outline: none; + border-color: rgba(59, 130, 246, 0.5); + background: #fff; + } + .lcp-send { + border-radius: 8px; + padding: 10px 16px; + font-size: 14px; + font-weight: 600; + background: rgba(59, 130, 246, 0.8); + color: #fff; + border: none; + cursor: pointer; + white-space: nowrap; + min-width: 60px; + transition: background 0.2s; + -webkit-tap-highlight-color: transparent; + } + .lcp-send:hover { + background: rgba(59, 130, 246, 1); + } + .lcp-send:active { + background: rgba(37, 99, 235, 1); + transform: scale(0.98); + } + @media (max-width: 640px) { + .lcp-container { + max-height: 200px; + } + .lcp-messages { + height: 120px; + min-height: 100px; + padding: 6px; + } + .lcp-msg { + font-size: 0.8rem; + padding: 6px 10px; + max-width: 90%; + } + .lcp-input { + padding: 8px 10px; + } + .lcp-send { + padding: 8px 12px; + min-width: 50px; + } + } +`; + document.head.appendChild(style); +} diff --git a/src/client/vite-env.d.ts b/src/client/vite-env.d.ts index 83679d71c2..4a18d83924 100644 --- a/src/client/vite-env.d.ts +++ b/src/client/vite-env.d.ts @@ -40,6 +40,14 @@ declare module "*.webp" { export default webpContent; } +declare global { + interface Window { + __eventBus?: import("../core/EventBus").EventBus; + __username?: string; + } +} + +export {}; declare module "*.svg?url" { const svgUrl: string; export default svgUrl; diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 15927fa564..de7ab19c24 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -96,15 +96,16 @@ export type ClientMessage = | ClientJoinMessage | ClientRejoinMessage | ClientLogMessage - | ClientHashMessage; - + | ClientHashMessage + | ClientLobbyChatMessage; export type ServerMessage = | ServerTurnMessage | ServerStartGameMessage | ServerPingMessage | ServerDesyncMessage | ServerPrestartMessage - | ServerErrorMessage; + | ServerErrorMessage + | ServerLobbyChatMessage; export type ServerTurnMessage = z.infer; export type ServerStartGameMessage = z.infer< @@ -121,6 +122,8 @@ export type ClientJoinMessage = z.infer; export type ClientRejoinMessage = z.infer; export type ClientLogMessage = z.infer; export type ClientHashMessage = z.infer; +export type ClientLobbyChatMessage = z.infer; +export type ServerLobbyChatMessage = z.infer; export type AllPlayersStats = z.infer; export type Player = z.infer; @@ -178,6 +181,8 @@ export const GameConfigSchema = z.object({ }) .optional(), disableNations: z.boolean(), + // New: Enable in-lobby chat for private games + chatEnabled: z.boolean().default(false), bots: z.number().int().min(0).max(400), infiniteGold: z.boolean(), infiniteTroops: z.boolean(), @@ -520,6 +525,13 @@ export const ServerErrorSchema = z.object({ message: z.string().optional(), }); +export const ServerLobbyChatSchema = z.object({ + type: z.literal("lobby_chat"), + username: z.string(), + isHost: z.boolean(), + text: SafeString.max(300), +}); + export const ServerMessageSchema = z.discriminatedUnion("type", [ ServerTurnMessageSchema, ServerPrestartMessageSchema, @@ -527,6 +539,7 @@ export const ServerMessageSchema = z.discriminatedUnion("type", [ ServerPingMessageSchema, ServerDesyncSchema, ServerErrorSchema, + ServerLobbyChatSchema, ]); // @@ -545,6 +558,12 @@ export const ClientHashSchema = z.object({ turnNumber: z.number(), }); +export const ClientLobbyChatSchema = z.object({ + type: z.literal("lobby_chat"), + clientID: ID, + text: SafeString.max(300), +}); + export const ClientLogMessageSchema = z.object({ type: z.literal("log"), severity: z.enum(LogSeverity), @@ -588,6 +607,7 @@ export const ClientMessageSchema = z.discriminatedUnion("type", [ ClientRejoinMessageSchema, ClientLogMessageSchema, ClientHashSchema, + ClientLobbyChatSchema, ]); // diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts index d2588ae00a..65dfb1dcac 100644 --- a/src/server/GameManager.ts +++ b/src/server/GameManager.ts @@ -65,6 +65,7 @@ export class GameManager { gameType: GameType.Private, gameMapSize: GameMapSize.Normal, difficulty: Difficulty.Medium, + chatEnabled: false, disableNations: false, infiniteGold: false, infiniteTroops: false, diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 0682539203..5b5e31fc96 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -121,6 +121,12 @@ export class GameServer { if (gameConfig.randomSpawn !== undefined) { this.gameConfig.randomSpawn = gameConfig.randomSpawn; } + if (gameConfig.chatEnabled !== undefined) { + // Enforce: public lobbies cannot enable chat + this.gameConfig.chatEnabled = this.isPublic() + ? false + : gameConfig.chatEnabled; + } if (gameConfig.spawnImmunityDuration !== undefined) { this.gameConfig.spawnImmunityDuration = gameConfig.spawnImmunityDuration; } @@ -441,6 +447,23 @@ export class GameServer { } break; } + case "lobby_chat": { + if (this.phase() !== GamePhase.Lobby) { + return; + } + if (!this.gameConfig.chatEnabled) { + return; + } + const isHost = client.clientID === this.lobbyCreatorID; + const payload = JSON.stringify({ + type: "lobby_chat", + username: client.username, + isHost, + text: clientMsg.text, + }); + this.activeClients.forEach((c) => c.ws.send(payload)); + break; + } case "ping": { this.lastPingUpdate = Date.now(); client.lastPing = Date.now(); diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index 35a218a47a..d10c25faea 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -124,6 +124,7 @@ export class MapPlaylist { playerTeams === HumansVsNations ? Difficulty.Impossible : Difficulty.Easy, + chatEnabled: false, infiniteGold: false, infiniteTroops: false, maxTimerValue: undefined, diff --git a/src/types/assets.d.ts b/src/types/assets.d.ts new file mode 100644 index 0000000000..22c524ecd6 --- /dev/null +++ b/src/types/assets.d.ts @@ -0,0 +1,55 @@ +declare module "*.png" { + const content: string; + export default content; +} +declare module "*.jpg" { + const value: string; + export default value; +} + +declare module "*.webp" { + const value: string; + export default value; +} + +declare module "*.jpeg" { + const value: string; + export default value; +} +declare module "*.svg" { + const value: string; + export default value; +} +declare module "*.bin" { + const value: string; + export default value; +} +declare module "*.md" { + const value: string; + export default value; +} +declare module "*.txt" { + const value: string; + export default value; +} +declare module "*.html" { + const content: string; + export default content; +} +declare module "*.xml" { + const value: string; + export default value; +} + +declare module "*.mp3" { + const value: string; + export default value; +} +declare module "*.wav" { + const value: string; + export default value; +} +declare module "*.ogg" { + const value: string; + export default value; +} diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 0000000000..a048bcc95f --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1,10 @@ +import type { EventBus } from "../core/EventBus"; + +declare global { + interface Window { + __eventBus?: EventBus; + __username?: string; + } +} + +export {}; diff --git a/tests/GameInfoRanking.test.ts b/tests/GameInfoRanking.test.ts index 7955fb8073..b6397ed72a 100644 --- a/tests/GameInfoRanking.test.ts +++ b/tests/GameInfoRanking.test.ts @@ -27,6 +27,7 @@ describe("Ranking class", () => { gameMapSize: GameMapSize.Normal, disableNations: true, bots: 0, + chatEnabled: false, infiniteGold: false, infiniteTroops: false, instantBuild: false, diff --git a/tests/LobbyChatPanel.test.ts b/tests/LobbyChatPanel.test.ts new file mode 100644 index 0000000000..526c4743a2 --- /dev/null +++ b/tests/LobbyChatPanel.test.ts @@ -0,0 +1,35 @@ +import fs from "fs"; +import path from "path"; + +// Temporary minimal tests for Lobby Chat while Lit component tests are deferred +// due to ESM transform issues. Ensures suite is non-empty and i18n keys exist. + +describe("LobbyChat i18n keys", () => { + const langPath = path.resolve( + __dirname, + "..", + "resources", + "lang", + "en.json", + ); + const raw = fs.readFileSync(langPath, "utf-8"); + const json = JSON.parse(raw); + + test("has lobby_chat.title", () => { + expect(json.lobby_chat?.title).toBeTruthy(); + }); + + test("has lobby_chat.placeholder", () => { + expect(json.lobby_chat?.placeholder).toBeTruthy(); + }); + + test("has lobby_chat.enable", () => { + expect(json.lobby_chat?.enable).toBeTruthy(); + }); + + test("has lobby_chat.send", () => { + expect(json.lobby_chat?.send).toBeTruthy(); + }); +}); + +// Replace with component behavioral tests (alignment, event emission) once ESM config fixed. diff --git a/tests/LobbyChatSchemas.test.ts b/tests/LobbyChatSchemas.test.ts new file mode 100644 index 0000000000..84d79ba4d2 --- /dev/null +++ b/tests/LobbyChatSchemas.test.ts @@ -0,0 +1,64 @@ +import { + ClientLobbyChatSchema, + GameConfigSchema, + ServerLobbyChatSchema, +} from "../src/core/Schemas"; + +describe("Lobby Chat Schemas", () => { + test("GameConfigSchema applies default chatEnabled false", () => { + const cfg = GameConfigSchema.parse({ + gameMap: "World", + difficulty: "Medium", + donateGold: false, + donateTroops: false, + gameType: "Private", + gameMode: "Free For All", + gameMapSize: "Normal", + disableNations: false, + bots: 0, + infiniteGold: false, + infiniteTroops: false, + instantBuild: false, + randomSpawn: false, + } as any); + expect(cfg.chatEnabled).toBe(false); + }); + + test("ClientLobbyChatSchema valid message", () => { + const msg = ClientLobbyChatSchema.parse({ + type: "lobby_chat", + clientID: "ABCDEFGH", + text: "Hello everyone", + }); + expect(msg.text).toBe("Hello everyone"); + }); + + test("ClientLobbyChatSchema rejects long text", () => { + const longText = "A".repeat(301); + const result = ClientLobbyChatSchema.safeParse({ + type: "lobby_chat", + clientID: "ABCDEFGH", + text: longText, + }); + expect(result.success).toBe(false); + }); + + test("ServerLobbyChatSchema valid message", () => { + const msg = ServerLobbyChatSchema.parse({ + type: "lobby_chat", + username: "TestUser", + isHost: true, + text: "Hi host", + }); + expect(msg.username).toBe("TestUser"); + expect(msg.isHost).toBe(true); + }); + + test("ServerLobbyChatSchema rejects missing fields", () => { + const result = ServerLobbyChatSchema.safeParse({ + type: "lobby_chat", + text: "Hi host", + }); + expect(result.success).toBe(false); + }); +}); diff --git a/tests/core/pathfinding/utils.ts b/tests/core/pathfinding/utils.ts index b8f19be040..1c24624b45 100644 --- a/tests/core/pathfinding/utils.ts +++ b/tests/core/pathfinding/utils.ts @@ -123,6 +123,7 @@ export async function gameFromString(mapRows: string[]): Promise { instantBuild: false, disableNavMesh: false, randomSpawn: false, + chatEnabled: false, }; const config = new TestConfig( serverConfig, diff --git a/tests/pathfinding/utils.ts b/tests/pathfinding/utils.ts index c32cd5f1b4..741b67dfca 100644 --- a/tests/pathfinding/utils.ts +++ b/tests/pathfinding/utils.ts @@ -221,6 +221,7 @@ export async function setupFromPath( infiniteTroops: false, instantBuild: false, randomSpawn: false, + chatEnabled: false, ...gameConfig, }, new UserSettings(), diff --git a/tests/util/Setup.ts b/tests/util/Setup.ts index e9b2722ee6..0b549b9e1c 100644 --- a/tests/util/Setup.ts +++ b/tests/util/Setup.ts @@ -61,6 +61,7 @@ export async function setup( gameMode: GameMode.FFA, gameType: GameType.Singleplayer, difficulty: Difficulty.Medium, + chatEnabled: false, disableNations: false, donateGold: false, donateTroops: false, diff --git a/tsconfig.json b/tsconfig.json index a4c5f7cc38..e1a53c1e6d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,11 +19,11 @@ "alwaysStrict": true, "esModuleInterop": true, "experimentalDecorators": true, + "skipLibCheck": true, "resolveJsonModule": true, "strictNullChecks": true, "useDefineForClassFields": false, "strictPropertyInitialization": false, - "skipLibCheck": true, "types": ["vitest/globals", "node"] }, "include": [