Files
zombie-game/resources/js/net/NetworkClient.js
T
2026-09-08 22:33:51 +02:00

201 lines
4.4 KiB
JavaScript

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();
}
}