diff --git a/packages/multiplayer-client/package.json b/packages/multiplayer-client/package.json new file mode 100644 index 00000000..f98108f1 --- /dev/null +++ b/packages/multiplayer-client/package.json @@ -0,0 +1,35 @@ +{ + "name": "@webgamekit/multiplayer-client", + "version": "0.0.1", + "type": "module", + "files": [ + "dist" + ], + "main": "./dist/index.umd.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.umd.cjs", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "vite build && tsc", + "prepare": "pnpm run build", + "prepublishOnly": "pnpm run build" + }, + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "socket.io-client": "^4.0.0" + }, + "devDependencies": { + "socket.io-client": "^4.8.1", + "typescript": "^5.0.0", + "vite": "^5.0.0", + "vitest": "^4.0.18" + } +} diff --git a/packages/multiplayer-client/src/connection.ts b/packages/multiplayer-client/src/connection.ts new file mode 100644 index 00000000..fd299c31 --- /dev/null +++ b/packages/multiplayer-client/src/connection.ts @@ -0,0 +1,33 @@ +import { io } from 'socket.io-client' +import type { MultiplayerClientConfig, MultiplayerClientSession } from './types' + +/** + * Connect to a Socket.IO multiplayer server. + * @param url - WebSocket server URL (e.g. "http://localhost:3000") + * @param config - Optional configuration overrides + * @returns A MultiplayerClientSession with the socket and a destroy function + */ +export const multiplayerClientCreate = ( + url: string, + config?: MultiplayerClientConfig +): MultiplayerClientSession & { _config: Required } => { + const socket = io(url) + const defaultThrottleMs = 30 + const resolvedConfig: Required = { + throttleMs: config?.throttleMs ?? defaultThrottleMs + } + + const destroy = () => { + socket.disconnect() + } + + return { socket, destroy, _config: resolvedConfig } +} + +/** + * Disconnect and clean up a multiplayer client session. + * @param session - The session returned by multiplayerClientCreate + */ +export const multiplayerClientDestroy = (session: MultiplayerClientSession): void => { + session.destroy() +} diff --git a/packages/multiplayer-client/src/data.ts b/packages/multiplayer-client/src/data.ts new file mode 100644 index 00000000..179043d3 --- /dev/null +++ b/packages/multiplayer-client/src/data.ts @@ -0,0 +1,31 @@ +import type { MultiplayerClientSession } from './types' + +/** + * Send typed data to all connected players via a named channel. + * @param session - The active multiplayer client session + * @param channel - Event name identifying the data channel (e.g. "coin:collected") + * @param payload - Serializable data to broadcast + */ +export const multiplayerClientSendData = ( + session: MultiplayerClientSession, + channel: string, + payload: T +): void => { + session.socket.emit(channel, payload) +} + +/** + * Subscribe to typed data arriving on a named channel from the server. + * @param session - The active multiplayer client session + * @param channel - Event name to listen on + * @param callback - Called with the received payload + * @returns Unsubscribe function + */ +export const multiplayerClientOnData = ( + session: MultiplayerClientSession, + channel: string, + callback: (payload: T) => void +): (() => void) => { + session.socket.on(channel, callback) + return () => session.socket.off(channel, callback) +} diff --git a/packages/multiplayer-client/src/index.test.ts b/packages/multiplayer-client/src/index.test.ts new file mode 100644 index 00000000..a2fde9e1 --- /dev/null +++ b/packages/multiplayer-client/src/index.test.ts @@ -0,0 +1,203 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import type { PlayerState } from './types' + +// ── Mock socket.io-client ────────────────────────────────────────────────── +const mockListeners = new Map void)[]>() +const mockEmitted: Array<[string, unknown]> = [] + +const mockSocket = { + connected: false, + on: vi.fn((event: string, callback: (...args: unknown[]) => void) => { + if (!mockListeners.has(event)) mockListeners.set(event, []) + mockListeners.get(event)!.push(callback) + }), + off: vi.fn(), + emit: vi.fn((event: string, data: unknown) => { + mockEmitted.push([event, data]) + }), + disconnect: vi.fn() +} + +const emit = (event: string, ...args: unknown[]) => + mockListeners.get(event)?.forEach((callback) => callback(...args)) + +vi.mock('socket.io-client', () => ({ io: vi.fn(() => mockSocket) })) + +// ── Import after mock ────────────────────────────────────────────────────── +import { + multiplayerClientCreate, + multiplayerClientDestroy, + multiplayerClientSendPosition, + multiplayerClientOnPlayers, + multiplayerClientSendData, + multiplayerClientOnData +} from './index' + +describe('multiplayerClientCreate', () => { + beforeEach(() => { + vi.clearAllMocks() + mockListeners.clear() + mockEmitted.length = 0 + }) + + it('returns a session with a socket and destroy function', () => { + const session = multiplayerClientCreate('http://localhost:3000') + expect(session).toHaveProperty('socket') + expect(session).toHaveProperty('destroy') + expect(typeof session.destroy).toBe('function') + }) + + it('connects to the provided URL', async () => { + const { io } = await import('socket.io-client') + multiplayerClientCreate('http://example.com:4000') + expect(io).toHaveBeenCalledWith('http://example.com:4000') + }) +}) + +describe('multiplayerClientDestroy', () => { + beforeEach(() => { + vi.clearAllMocks() + mockListeners.clear() + mockEmitted.length = 0 + }) + + it('calls disconnect on the socket', () => { + const session = multiplayerClientCreate('http://localhost:3000') + multiplayerClientDestroy(session) + expect(mockSocket.disconnect).toHaveBeenCalled() + }) + + it('session.destroy() also disconnects', () => { + const session = multiplayerClientCreate('http://localhost:3000') + session.destroy() + expect(mockSocket.disconnect).toHaveBeenCalled() + }) +}) + +describe('multiplayerClientSendPosition', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.clearAllMocks() + mockListeners.clear() + mockEmitted.length = 0 + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('emits user:updated with position and rotation', () => { + const session = multiplayerClientCreate('http://localhost:3000') + const position = { x: 1, y: 2, z: 3 } + const rotation = { x: 0, y: 0.5, z: 0 } + multiplayerClientSendPosition(session, position, rotation) + vi.advanceTimersByTime(30) + expect(mockSocket.emit).toHaveBeenCalledWith('user:updated', { position, rotation }) + }) + + it('throttles rapid calls — only one emission per interval', () => { + const session = multiplayerClientCreate('http://localhost:3000') + const rotation = { x: 0, y: 0, z: 0 } + multiplayerClientSendPosition(session, { x: 1, y: 0, z: 0 }, rotation) + multiplayerClientSendPosition(session, { x: 2, y: 0, z: 0 }, rotation) + multiplayerClientSendPosition(session, { x: 3, y: 0, z: 0 }, rotation) + vi.advanceTimersByTime(30) + const calls = mockEmitted.filter(([e]) => e === 'user:updated') + expect(calls.length).toBe(1) + }) + + it('respects custom throttleMs from config', () => { + const session = multiplayerClientCreate('http://localhost:3000', { throttleMs: 100 }) + const position = { x: 1, y: 0, z: 0 } + const rotation = { x: 0, y: 0, z: 0 } + multiplayerClientSendPosition(session, position, rotation) + vi.advanceTimersByTime(30) + expect(mockSocket.emit).not.toHaveBeenCalledWith('user:updated', expect.anything()) + vi.advanceTimersByTime(70) + expect(mockSocket.emit).toHaveBeenCalledWith( + 'user:updated', + expect.objectContaining({ position }) + ) + }) +}) + +describe('multiplayerClientOnPlayers', () => { + beforeEach(() => { + vi.clearAllMocks() + mockListeners.clear() + mockEmitted.length = 0 + }) + + it('calls callback when user:list is received', () => { + const session = multiplayerClientCreate('http://localhost:3000') + const callback = vi.fn() + const players: PlayerState[] = [ + { id: 'abc', name: 'Alice', position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 } } + ] + multiplayerClientOnPlayers(session, callback) + emit('user:list', players) + expect(callback).toHaveBeenCalledWith(players) + }) + + it('returns an unsubscribe function', () => { + const session = multiplayerClientCreate('http://localhost:3000') + const unsubscribe = multiplayerClientOnPlayers(session, vi.fn()) + unsubscribe() + expect(mockSocket.off).toHaveBeenCalledWith('user:list', expect.any(Function)) + }) +}) + +describe('multiplayerClientSendData', () => { + beforeEach(() => { + vi.clearAllMocks() + mockListeners.clear() + mockEmitted.length = 0 + }) + + it('emits typed data on the given channel', () => { + const session = multiplayerClientCreate('http://localhost:3000') + multiplayerClientSendData(session, 'coin:collected', { coinId: 'c1', playerId: 'p1' }) + expect(mockSocket.emit).toHaveBeenCalledWith('coin:collected', { coinId: 'c1', playerId: 'p1' }) + }) + + it('works with any serializable payload type', () => { + const session = multiplayerClientCreate('http://localhost:3000') + multiplayerClientSendData(session, 'game:event', { type: 'levelUp', level: 5 }) + expect(mockSocket.emit).toHaveBeenCalledWith('game:event', { type: 'levelUp', level: 5 }) + }) +}) + +describe('multiplayerClientOnData', () => { + beforeEach(() => { + vi.clearAllMocks() + mockListeners.clear() + mockEmitted.length = 0 + }) + + it('calls callback with typed payload when server sends data', () => { + const session = multiplayerClientCreate('http://localhost:3000') + const callback = vi.fn() + multiplayerClientOnData<{ coinId: string }>(session, 'coin:collected', callback) + emit('coin:collected', { coinId: 'c1' }) + expect(callback).toHaveBeenCalledWith({ coinId: 'c1' }) + }) + + it('returns an unsubscribe function', () => { + const session = multiplayerClientCreate('http://localhost:3000') + const unsubscribe = multiplayerClientOnData(session, 'coin:collected', vi.fn()) + unsubscribe() + expect(mockSocket.off).toHaveBeenCalledWith('coin:collected', expect.any(Function)) + }) + + it('supports multiple independent channels', () => { + const session = multiplayerClientCreate('http://localhost:3000') + const onCoin = vi.fn() + const onChat = vi.fn() + multiplayerClientOnData(session, 'coin:collected', onCoin) + multiplayerClientOnData(session, 'chat:message', onChat) + emit('coin:collected', { coinId: 'c2' }) + emit('chat:message', { text: 'hello' }) + expect(onCoin).toHaveBeenCalledWith({ coinId: 'c2' }) + expect(onChat).toHaveBeenCalledWith({ text: 'hello' }) + }) +}) diff --git a/packages/multiplayer-client/src/index.ts b/packages/multiplayer-client/src/index.ts new file mode 100644 index 00000000..9b692cb5 --- /dev/null +++ b/packages/multiplayer-client/src/index.ts @@ -0,0 +1,11 @@ +export type { + PlayerPosition, + PlayerRotation, + PlayerState, + MultiplayerClientConfig, + MultiplayerClientSession +} from './types' + +export { multiplayerClientCreate, multiplayerClientDestroy } from './connection' +export { multiplayerClientSendPosition, multiplayerClientOnPlayers } from './players' +export { multiplayerClientSendData, multiplayerClientOnData } from './data' diff --git a/packages/multiplayer-client/src/players.ts b/packages/multiplayer-client/src/players.ts new file mode 100644 index 00000000..05061292 --- /dev/null +++ b/packages/multiplayer-client/src/players.ts @@ -0,0 +1,46 @@ +import type { PlayerPosition, PlayerRotation, PlayerState, MultiplayerClientSession } from './types' + +type InternalSession = MultiplayerClientSession & { _config: { throttleMs: number } } + +const pendingTimers = new WeakMap>() + +/** + * Broadcast the local player's position and rotation to the server, throttled. + * Only one emission is sent per throttle window — always the most recent values. + * @param session - The active multiplayer client session + * @param position - Current player position {x, y, z} + * @param rotation - Current player rotation {x, y, z} + */ +export const multiplayerClientSendPosition = ( + session: MultiplayerClientSession, + position: PlayerPosition, + rotation: PlayerRotation +): void => { + const internal = session as InternalSession + const defaultThrottleMs = 30 + const throttleMs = internal._config?.throttleMs ?? defaultThrottleMs + const existing = pendingTimers.get(session) + if (existing !== undefined) clearTimeout(existing) + + pendingTimers.set( + session, + setTimeout(() => { + pendingTimers.delete(session) + session.socket.emit('user:updated', { position, rotation }) + }, throttleMs) + ) +} + +/** + * Subscribe to player list updates from the server. + * @param session - The active multiplayer client session + * @param callback - Called with the full list of connected players on each update + * @returns Unsubscribe function + */ +export const multiplayerClientOnPlayers = ( + session: MultiplayerClientSession, + callback: (players: PlayerState[]) => void +): (() => void) => { + session.socket.on('user:list', callback) + return () => session.socket.off('user:list', callback) +} diff --git a/packages/multiplayer-client/src/types.ts b/packages/multiplayer-client/src/types.ts new file mode 100644 index 00000000..41e952b3 --- /dev/null +++ b/packages/multiplayer-client/src/types.ts @@ -0,0 +1,30 @@ +export interface PlayerPosition { + x: number + y: number + z: number +} + +export interface PlayerRotation { + x: number + y: number + z: number +} + +export interface PlayerState { + id: string + name: string + position: PlayerPosition + rotation: PlayerRotation +} + +export interface MultiplayerClientConfig { + /** Minimum ms between position broadcasts (default: 30) */ + throttleMs?: number +} + +export interface MultiplayerClientSession { + /** The underlying socket.io Socket */ + socket: import('socket.io-client').Socket + /** Disconnect and cancel pending broadcasts */ + destroy: () => void +} diff --git a/packages/multiplayer-client/tsconfig.json b/packages/multiplayer-client/tsconfig.json new file mode 100644 index 00000000..f3de4d67 --- /dev/null +++ b/packages/multiplayer-client/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"], + "exclude": ["node_modules", "**/*.test.ts", "**/*.spec.ts"] +} diff --git a/packages/multiplayer-client/vite.config.ts b/packages/multiplayer-client/vite.config.ts new file mode 100644 index 00000000..3b80293c --- /dev/null +++ b/packages/multiplayer-client/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite' +import { resolve } from 'path' + +export default defineConfig({ + build: { + lib: { + entry: resolve(import.meta.dirname, 'src/index.ts'), + name: 'WebGameToolkitMultiplayer', + fileName: 'index' + }, + rollupOptions: { + external: ['socket.io-client'], + output: { + globals: { + 'socket.io-client': 'io' + } + } + } + } +}) diff --git a/packages/multiplayer-server/package.json b/packages/multiplayer-server/package.json new file mode 100644 index 00000000..744551b0 --- /dev/null +++ b/packages/multiplayer-server/package.json @@ -0,0 +1,36 @@ +{ + "name": "@webgamekit/multiplayer-server", + "version": "0.0.1", + "type": "module", + "files": [ + "dist" + ], + "main": "./dist/index.umd.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.umd.cjs", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "vite build && tsc", + "prepare": "pnpm run build", + "prepublishOnly": "pnpm run build" + }, + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "socket.io": "^4.0.0" + }, + "devDependencies": { + "socket.io": "^4.8.0", + "@types/node": "^22.0.0", + "typescript": "^5.0.0", + "vite": "^5.0.0", + "vitest": "^4.0.18" + } +} diff --git a/packages/multiplayer-server/src/index.test.ts b/packages/multiplayer-server/src/index.test.ts new file mode 100644 index 00000000..9cda1b5b --- /dev/null +++ b/packages/multiplayer-server/src/index.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import type { PlayerState } from './types' +import { multiplayerServerCreate } from './index' + +// ── Mock Socket.IO ───────────────────────────────────────────────────────── +type Handler = (...args: unknown[]) => void + +const makeSocket = (id: string) => { + const handlers = new Map() + return { + id, + emit: vi.fn(), + on: vi.fn((event: string, handler: Handler) => { + handlers.set(event, handler) + }), + trigger: (event: string, ...args: unknown[]) => { + handlers.get(event)?.(...args) + } + } +} + +const makeIo = () => { + const connectionHandlers: Handler[] = [] + return { + emit: vi.fn(), + on: vi.fn((event: string, handler: Handler) => { + if (event === 'connection') connectionHandlers.push(handler) + }), + off: vi.fn(), + connect: (socket: ReturnType) => { + connectionHandlers.forEach((h) => h(socket)) + } + } +} + +describe('multiplayerServerCreate', () => { + let io: ReturnType + + beforeEach(() => { + io = makeIo() + vi.clearAllMocks() + }) + + it('registers a connection listener on the server', () => { + multiplayerServerCreate(io as never) + expect(io.on).toHaveBeenCalledWith('connection', expect.any(Function)) + }) + + it('broadcasts user:list when a player connects', () => { + multiplayerServerCreate(io as never) + const socket = makeSocket('player-1') + io.connect(socket) + expect(io.emit).toHaveBeenCalledWith( + 'user:list', + expect.arrayContaining([expect.objectContaining({ id: 'player-1' })]) + ) + }) + + it('updates player state and re-broadcasts on user:updated', () => { + multiplayerServerCreate(io as never) + const socket = makeSocket('player-1') + io.connect(socket) + + const position = { x: 5, y: 0, z: 3 } + const rotation = { x: 0, y: 1, z: 0 } + socket.trigger('user:updated', { position, rotation }) + + const lastCall = (io.emit as ReturnType).mock.calls.at(-1) + const players = lastCall?.[1] as PlayerState[] + expect(players[0].position).toEqual(position) + expect(players[0].rotation).toEqual(rotation) + }) + + it('removes player and broadcasts on disconnect', () => { + multiplayerServerCreate(io as never) + const socket = makeSocket('player-1') + io.connect(socket) + socket.trigger('disconnect') + + const lastCall = (io.emit as ReturnType).mock.calls.at(-1) + expect(lastCall?.[1]).toEqual([]) + }) + + it('tracks multiple concurrent players', () => { + multiplayerServerCreate(io as never) + const s1 = makeSocket('p1') + const s2 = makeSocket('p2') + io.connect(s1) + io.connect(s2) + + const lastCall = (io.emit as ReturnType).mock.calls.at(-1) + const players = lastCall?.[1] as PlayerState[] + expect(players).toHaveLength(2) + }) + + it('calls onConnect config and sends initial data to the new socket', () => { + const onConnect = vi.fn(() => ({ 'coin:list': [{ id: 'c1' }] })) + multiplayerServerCreate(io as never, { onConnect }) + const socket = makeSocket('player-1') + io.connect(socket) + + expect(onConnect).toHaveBeenCalledWith('player-1') + expect(socket.emit).toHaveBeenCalledWith('coin:list', [{ id: 'c1' }]) + }) + + it('cleanup removes the connection listener', () => { + const { cleanup } = multiplayerServerCreate(io as never) + cleanup() + expect(io.off).toHaveBeenCalledWith('connection', expect.any(Function)) + }) +}) diff --git a/packages/multiplayer-server/src/index.ts b/packages/multiplayer-server/src/index.ts new file mode 100644 index 00000000..bdfef94a --- /dev/null +++ b/packages/multiplayer-server/src/index.ts @@ -0,0 +1,2 @@ +export type { PlayerPosition, PlayerRotation, PlayerState, MultiplayerServerConfig } from './types' +export { multiplayerServerCreate } from './server' diff --git a/packages/multiplayer-server/src/server.ts b/packages/multiplayer-server/src/server.ts new file mode 100644 index 00000000..6d2df245 --- /dev/null +++ b/packages/multiplayer-server/src/server.ts @@ -0,0 +1,86 @@ +import type { Server, Socket } from 'socket.io' +import type { PlayerState, PlayerRotation, PlayerPosition, MultiplayerServerConfig } from './types' + +type PositionPayload = { position: PlayerPosition; rotation: PlayerRotation; name?: string } +type PlayerRegistry = Readonly> + +const broadcastPlayerList = (io: Server, players: PlayerRegistry): void => { + io.emit('user:list', Object.values(players)) +} + +const registerPlayerHandlers = ( + socket: Socket, + io: Server, + getPlayers: () => PlayerRegistry, + setPlayers: (p: PlayerRegistry) => void, + config: MultiplayerServerConfig +): void => { + const initialState: PlayerState = { + id: socket.id, + name: socket.id, + position: { x: 0, y: 0, z: 0 }, + rotation: { x: 0, y: 0, z: 0 } + } + setPlayers({ ...getPlayers(), [socket.id]: initialState }) + + if (config.onConnect) { + const initialData = config.onConnect(socket.id) + Object.entries(initialData).forEach(([channel, payload]) => { + socket.emit(channel, payload) + }) + } + + broadcastPlayerList(io, getPlayers()) + + socket.on('user:updated', (payload: PositionPayload) => { + const current = getPlayers()[socket.id] ?? initialState + setPlayers({ + ...getPlayers(), + [socket.id]: { + ...current, + name: payload.name ?? current.name, + position: payload.position, + rotation: payload.rotation + } + }) + io.emit('user:list', Object.values(getPlayers())) + }) + + socket.on('disconnect', () => { + const { [socket.id]: _removed, ...rest } = getPlayers() + setPlayers(rest) + broadcastPlayerList(io, getPlayers()) + }) +} + +/** + * Attach multiplayer session handling to a Socket.IO server instance. + * Manages player registry, position relay, and generic data forwarding. + * @param io - A Socket.IO Server instance + * @param config - Optional server configuration + * @returns cleanup function that removes the connection listener + */ +export const multiplayerServerCreate = ( + io: Server, + config: MultiplayerServerConfig = {} +): { cleanup: () => void } => { + let players: PlayerRegistry = {} + + const getPlayers = () => players + const setPlayers = (p: PlayerRegistry) => { + players = p + } + + const onConnection = (socket: Socket) => { + registerPlayerHandlers(socket, io, getPlayers, setPlayers, config) + } + + io.on('connection', onConnection) + + return { + cleanup: () => { + io.off('connection', onConnection) + players = {} + } + } +} diff --git a/packages/multiplayer-server/src/types.ts b/packages/multiplayer-server/src/types.ts new file mode 100644 index 00000000..36e6836f --- /dev/null +++ b/packages/multiplayer-server/src/types.ts @@ -0,0 +1,23 @@ +export interface PlayerPosition { + x: number + y: number + z: number +} + +export interface PlayerRotation { + x: number + y: number + z: number +} + +export interface PlayerState { + id: string + name: string + position: PlayerPosition + rotation: PlayerRotation +} + +export interface MultiplayerServerConfig { + /** Called when a player connects, returns initial data to send them */ + onConnect?: (socketId: string) => Record +} diff --git a/packages/multiplayer-server/tsconfig.json b/packages/multiplayer-server/tsconfig.json new file mode 100644 index 00000000..7c9f9866 --- /dev/null +++ b/packages/multiplayer-server/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext"], + "moduleResolution": "Node", + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"], + "exclude": ["node_modules", "**/*.test.ts"] +} diff --git a/packages/multiplayer-server/vite.config.ts b/packages/multiplayer-server/vite.config.ts new file mode 100644 index 00000000..bead86e5 --- /dev/null +++ b/packages/multiplayer-server/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import { resolve } from 'path' + +export default defineConfig({ + build: { + lib: { + entry: resolve(import.meta.dirname, 'src/index.ts'), + name: 'WebGameToolkitMultiplayerServer', + fileName: 'index' + }, + rollupOptions: { + external: ['socket.io'], + output: { globals: { 'socket.io': 'io' } } + } + } +}) diff --git a/src/views/Experiments/MultiplayerClient/MultiplayerClient.vue b/src/views/Experiments/MultiplayerClient/MultiplayerClient.vue new file mode 100644 index 00000000..83afe4df --- /dev/null +++ b/src/views/Experiments/MultiplayerClient/MultiplayerClient.vue @@ -0,0 +1,302 @@ + + + + +