import { SocketClient } from './SocketClient.js'; const INPUT_RATE = 30; export class NetworkClient { constructor(options = {}) { this.socket = new SocketClient(options); this.room = null; this.playerId = null; this.playerName = null; this.joined = false; this.creator = false; this.prev = null; this.prevAt = 0; this.curr = null; this.currAt = 0; this.onStateChange = null; this.onRoomError = null; this._input = null; this._inputTimer = null; this.socket.onConnectionChange = (connected) => { if (connected && this.joined && this.room) { if (this.creator) { this._sendCreate(); } else { this._sendJoin(); } } this.onStateChange?.({ type: 'connection', connected }); }; } connect() { this.socket.connect(); } get connected() { return this.socket.connected; } join(room, { id, name }) { this._enterRoom(room, { id, name }, false); } create(room, { id, name }) { this._enterRoom(room, { id, name }, true); } _enterRoom(room, { id, name }, isCreator) { const changingRoom = this.room && this.room !== room; if (changingRoom) { this.socket.unsubscribe(this._snapshotChannel()); this.socket.unsubscribe(this._inputChannel()); this._sendControl('leave'); } this.room = room; this.playerId = id; this.playerName = name; this.joined = true; this.creator = isCreator; this.prev = null; this.prevAt = 0; this.curr = null; this.currAt = 0; this.socket.subscribe(this._snapshotChannel(), { onEvent: (event, payload, rawBytes) => { if (event === 'snapshot') { this._ingest(payload, rawBytes); } else if (event === 'room-error') { this.onRoomError?.(payload); } }, }); this.socket.subscribe(this._inputChannel()); if (isCreator) { this._sendCreate(); } else { this._sendJoin(); } if (!this._inputTimer) { this._inputTimer = setInterval(() => this._flushInput(), 1000 / INPUT_RATE); } } leave() { if (!this.joined) return; this._sendControl('leave'); this.joined = false; this.creator = false; this.room = null; this.prev = null; this.prevAt = 0; this.curr = null; this.currAt = 0; if (this._inputTimer) { clearInterval(this._inputTimer); this._inputTimer = null; } } abandonRoom() { if (this.room) { this.socket.unsubscribe(this._snapshotChannel()); this.socket.unsubscribe(this._inputChannel()); } this.joined = false; this.creator = false; this.room = null; this.prev = null; this.prevAt = 0; this.curr = null; this.currAt = 0; if (this._inputTimer) { clearInterval(this._inputTimer); this._inputTimer = null; } } start() { this._sendControl('start'); } reset() { this._sendControl('reset'); } sendInput(frame) { this._input = frame; } stopSendingInput() { this._input = null; } _flushInput() { if (!this._input || !this._connected()) return; const { id, keys, aim, fire } = this._input; this.socket.trigger('client-input', this._inputChannel(), { id, keys, aim, fire, }); } _connected() { return this.joined && this.room && this.socket.connected; } _snapshotChannel() { return `game.${this.room}`; } _inputChannel() { return `game.${this.room}.input`; } _sendControl(type) { if (!this._connected()) return; this.socket.trigger(`client-${type}`, this._inputChannel(), { type, id: this.playerId, name: this.playerName, }); } _sendJoin() { if (!this.room || !this.playerId) return; this.socket.trigger('client-join', this._inputChannel(), { type: 'join', id: this.playerId, name: this.playerName, }); } _sendCreate() { if (!this.room || !this.playerId) return; this.socket.trigger('client-create', this._inputChannel(), { type: 'create', id: this.playerId, name: this.playerName, }); } _ingest(payload, rawBytes = 0) { if (this.curr && payload.seq !== undefined && this.curr.seq !== undefined && payload.seq <= this.curr.seq) { return; } this.prev = this.curr; this.prevAt = this.currAt; this.curr = payload; this.curr.bytes = rawBytes; this.currAt = performance.now(); } }