Implement tiled multiplayer city simulation and scaling stages
This commit is contained in:
1 parent
3d4b29fedf
commit
3b1fcd996a
53 files changed
+7354
-13
No files matched your search
@@ -0,0 +1,68 @@
|
||||
import { create, fromBinary, toBinary } from '@bufbuild/protobuf';
|
||||
import { ClientMessageSchema, ServerMessageSchema, type ClientMessage, type Welcome, type Clock, type VoteState } from './generated/city_pb';
|
||||
import type { Bounds } from '../src/city/format';
|
||||
import { EntityState } from './entities';
|
||||
|
||||
export class RoomConnection {
|
||||
readonly entities = new EntityState();
|
||||
private socket: WebSocket;
|
||||
private ping: ReturnType<typeof setInterval>;
|
||||
private closed = false;
|
||||
private pendingBounds: Bounds | null = null;
|
||||
private welcomed = false;
|
||||
private lod = 0;
|
||||
private retry:ReturnType<typeof setTimeout> | undefined;
|
||||
constructor(room: string, nickname: string, private events: {
|
||||
welcome: (welcome: Welcome) => void; status: (text: string) => void;
|
||||
clock: (clock: Clock) => void; vote: (vote: VoteState) => void;
|
||||
}) {
|
||||
const url = new URL(`/api/rooms/${encodeURIComponent(room)}/ws`, location.href);
|
||||
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
this.socket = new WebSocket(url); this.socket.binaryType = 'arraybuffer';
|
||||
this.socket.onopen = () => this.send({ case: 'join', value: { room, nickname, clientVersion: 1 } });
|
||||
this.socket.onmessage = ev => {
|
||||
try {
|
||||
const message = fromBinary(ServerMessageSchema, new Uint8Array(ev.data));
|
||||
const b = message.body;
|
||||
switch (b.case) {
|
||||
case 'welcome':
|
||||
if (b.value.protocolVersion !== 1) throw new Error('Несовместимая версия сервера');
|
||||
this.welcomed = true; events.welcome(b.value);
|
||||
if (this.pendingBounds) this.subscribe(this.pendingBounds);
|
||||
break;
|
||||
case 'snapshot': this.entities.snapshot(b.value); break;
|
||||
case 'delta': this.entities.delta(b.value); break;
|
||||
case 'error':
|
||||
if (b.value.code === 'ENTITY_LIMIT' && this.pendingBounds && this.lod < 10) {
|
||||
this.lod++; this.subscribe(this.pendingBounds); events.status(`Плотная область: отображается 1 из ${2 ** this.lod} жителей`);
|
||||
} else if (b.value.code==='TRAFFIC_LIMIT' && this.pendingBounds) {
|
||||
const bounds=this.pendingBounds;clearTimeout(this.retry);
|
||||
this.retry=setTimeout(()=>{if (this.pendingBounds===bounds) this.subscribe(bounds);},1000);
|
||||
events.status('Подождите, обновляем область…');
|
||||
} else events.status(b.value.code === 'VOTE_LIMIT' ? 'Дождитесь завершения голосования или 30-секундного интервала между предложениями' : `${b.value.code}: ${b.value.message}`);
|
||||
break;
|
||||
case 'clock': events.clock(b.value); break;
|
||||
case 'voteState': events.vote(b.value); break;
|
||||
}
|
||||
} catch (error) { events.status((error as Error).message); this.destroy(); }
|
||||
};
|
||||
this.socket.onclose = () => { clearInterval(this.ping); this.entities.clear(); if (!this.closed) events.status('Соединение закрыто. Вернитесь в список комнат, чтобы войти снова.'); };
|
||||
this.socket.onerror = () => events.status('Не удалось подключиться к серверу');
|
||||
this.ping = setInterval(() => this.send({ case: 'ping', value: { nonce: 1 } }), 20_000);
|
||||
}
|
||||
private send(body: any) {
|
||||
if (this.socket.readyState === WebSocket.OPEN) this.socket.send(toBinary(ClientMessageSchema, create(ClientMessageSchema, { body })));
|
||||
}
|
||||
subscribe(bounds: Bounds) {
|
||||
clearTimeout(this.retry);
|
||||
this.pendingBounds = bounds;
|
||||
if (!this.welcomed) return;
|
||||
this.send({ case: 'subscribe', value: { rect: {
|
||||
minX: Math.round(bounds.minX * 100), minY: Math.round(bounds.minY * 100),
|
||||
maxX: Math.round(bounds.maxX * 100), maxY: Math.round(bounds.maxY * 100),
|
||||
}, lod: this.lod } });
|
||||
}
|
||||
propose(speed: number) { this.send({ case: 'propose', value: { speed } }); }
|
||||
vote(yes: boolean) { this.send({ case: 'vote', value: { yes } }); }
|
||||
destroy() { this.closed = true; clearTimeout(this.retry); clearInterval(this.ping); this.socket.close(); this.entities.clear(); }
|
||||
}
|
||||
Reference in new issue
Block a user