initial commit
This commit is contained in:
commit
e8c3f9862a
193 files changed
+29795
No files matched your search
@@ -0,0 +1,172 @@
|
||||
import { Zombie } from '../game/Zombie.js';
|
||||
import { MegaTank } from '../game/MegaTank.js';
|
||||
import { Leaper } from '../game/Leaper.js';
|
||||
import { Witch } from '../game/Witch.js';
|
||||
import { MegaWitch } from '../game/MegaWitch.js';
|
||||
import { Boss } from '../game/Boss.js';
|
||||
import { Bullet } from '../game/Bullet.js';
|
||||
import { WitchProjectile } from '../game/WitchProjectile.js';
|
||||
import { Pickup } from '../game/Pickup.js';
|
||||
import { Player } from '../game/Player.js';
|
||||
import { Vector2D } from '../engine/Vector2D.js';
|
||||
|
||||
const CLASSES = {
|
||||
zombie: Zombie,
|
||||
megatank: MegaTank,
|
||||
leaper: Leaper,
|
||||
witch: Witch,
|
||||
megawitch: MegaWitch,
|
||||
boss: Boss,
|
||||
bullet: Bullet,
|
||||
projectile: WitchProjectile,
|
||||
};
|
||||
|
||||
const PLAYER_SIZE = 24;
|
||||
|
||||
export class NetEntities {
|
||||
constructor() {
|
||||
this.target = { pos: new Vector2D(960, 540) };
|
||||
this.views = new Map();
|
||||
this.players = new Map();
|
||||
}
|
||||
|
||||
setTarget(x, y) {
|
||||
this.target.pos.x = x;
|
||||
this.target.pos.y = y;
|
||||
}
|
||||
|
||||
_timeFor(eid, now) {
|
||||
return now + (eid % 17) * 0.21 + (eid % 5) * 0.07;
|
||||
}
|
||||
|
||||
_key(entity) {
|
||||
return `${entity.d}#${entity.eid}`;
|
||||
}
|
||||
|
||||
_createView(entity) {
|
||||
const Ctor = CLASSES[entity.d];
|
||||
if (entity.d === 'pickup') {
|
||||
return new Pickup(entity.x, entity.y, entity.t, { container: null, viewport: {}, life: 10 });
|
||||
}
|
||||
if (entity.d === 'bullet' || entity.d === 'projectile') {
|
||||
return new Ctor(entity.x, entity.y, new Vector2D(1, 0), {});
|
||||
}
|
||||
return new Ctor(entity.x, entity.y, this.target, {});
|
||||
}
|
||||
|
||||
draw(entities, players, renderer, textRenderer, now) {
|
||||
const seen = new Set();
|
||||
let boss = null;
|
||||
|
||||
for (const entity of entities) {
|
||||
const key = this._key(entity);
|
||||
seen.add(key);
|
||||
let view = this.views.get(key);
|
||||
if (!view) {
|
||||
view = this._createView(entity);
|
||||
this.views.set(key, view);
|
||||
}
|
||||
this._stamp(view, entity, now);
|
||||
view.draw(renderer, textRenderer);
|
||||
if (entity.d === 'boss') {
|
||||
boss = view;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, view] of this.views) {
|
||||
if (!seen.has(key)) {
|
||||
this.views.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const snapshot of players) {
|
||||
let view = this.players.get(snapshot.id);
|
||||
if (!view) {
|
||||
view = new Player({
|
||||
input: null,
|
||||
container: null,
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
size: PLAYER_SIZE,
|
||||
});
|
||||
this.players.set(snapshot.id, view);
|
||||
}
|
||||
this._stampPlayer(view, snapshot, now);
|
||||
if (!snapshot.dead) {
|
||||
view.draw(renderer);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, view] of this.players) {
|
||||
if (!players.some((snapshot) => snapshot.id === id)) {
|
||||
this.players.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
return boss;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.views.clear();
|
||||
this.players.clear();
|
||||
}
|
||||
|
||||
_stamp(view, entity, now) {
|
||||
view.pos.x = entity.x;
|
||||
view.pos.y = entity.y;
|
||||
if (entity.s !== undefined) {
|
||||
view.size = entity.s;
|
||||
}
|
||||
if (entity.c) {
|
||||
view.baseColor = [entity.c[0], entity.c[1], entity.c[2], 1];
|
||||
}
|
||||
if (entity.hp !== undefined && entity.mhp !== undefined) {
|
||||
view.health = entity.hp;
|
||||
view.maxHealth = entity.mhp;
|
||||
}
|
||||
view._time = this._timeFor(entity.eid, now);
|
||||
view._flash = 0;
|
||||
|
||||
if (entity.et !== undefined) {
|
||||
view._emergeTotal = entity.et;
|
||||
view._emerge = entity.er ?? 0;
|
||||
}
|
||||
|
||||
switch (entity.d) {
|
||||
case 'leaper':
|
||||
view._dashTimer = entity.dash ? 999 : 0;
|
||||
view._windupTimer = entity.wind ? 999 : 0;
|
||||
if (!view._dashDirection) {
|
||||
view._dashDirection = { x: 1, y: 0 };
|
||||
}
|
||||
break;
|
||||
case 'megawitch':
|
||||
view._novaTimer = (1 - (entity.charge ?? 0)) * view.novaInterval;
|
||||
break;
|
||||
case 'witch':
|
||||
view._cast = 0;
|
||||
view._fireTimer = view.fireInterval * (1 - ((entity.eid * 0.37 + now * 0.11) % 1));
|
||||
break;
|
||||
case 'boss':
|
||||
view._hurt = 0;
|
||||
break;
|
||||
case 'pickup':
|
||||
view._time = Math.max(0, 10 - entity.life);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_stampPlayer(view, snapshot, now) {
|
||||
view.pos.x = snapshot.x;
|
||||
view.pos.y = snapshot.y;
|
||||
const aim = new Vector2D(snapshot.aimX, snapshot.aimY);
|
||||
if (aim.lengthSqrt() > 0.01) {
|
||||
view._aimDirection = aim.normalize();
|
||||
}
|
||||
view._time = now;
|
||||
view._damageCooldown = 0;
|
||||
view.health = snapshot.hp;
|
||||
view.maxHealth = snapshot.mhp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
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) => {
|
||||
if (event === 'snapshot') {
|
||||
this._ingest(payload);
|
||||
} 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, autoAim } = this._input;
|
||||
this.socket.trigger('client-input', this._inputChannel(), {
|
||||
id,
|
||||
keys,
|
||||
aim,
|
||||
fire,
|
||||
autoAim,
|
||||
});
|
||||
}
|
||||
|
||||
_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) {
|
||||
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.currAt = performance.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
const DEFAULT_OPTIONS = {
|
||||
scheme: 'ws',
|
||||
host: '127.0.0.1',
|
||||
port: 8080,
|
||||
key: 'game-local-key',
|
||||
client: 'zombie-client',
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
export class SocketClient {
|
||||
constructor(options = {}) {
|
||||
this.opts = { ...DEFAULT_OPTIONS, ...options };
|
||||
this.socket = null;
|
||||
this.socketId = null;
|
||||
this.connected = false;
|
||||
this.closedByUser = false;
|
||||
this.retries = 0;
|
||||
this.handlers = new Map();
|
||||
this.queue = [];
|
||||
this.onConnectionChange = null;
|
||||
}
|
||||
|
||||
get url() {
|
||||
const { scheme, host, port, key, client, version } = this.opts;
|
||||
return `${scheme}://${host}:${port}/app/${key}?protocol=7&client=${client}&version=${version}&flash=false`;
|
||||
}
|
||||
|
||||
_connect() {
|
||||
if (this.socket) return;
|
||||
this.closedByUser = false;
|
||||
const socket = new WebSocket(this.url);
|
||||
this.socket = socket;
|
||||
|
||||
socket.onopen = () => {
|
||||
this.retries = 0;
|
||||
};
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
let msg;
|
||||
try {
|
||||
msg = JSON.parse(event.data);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
this._handle(msg);
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
this.socket = null;
|
||||
if (this.socketId) {
|
||||
this.connected = false;
|
||||
this.socketId = null;
|
||||
this.onConnectionChange?.(false);
|
||||
}
|
||||
if (this.closedByUser) return;
|
||||
if (!this.handlers.size) return;
|
||||
const delay = Math.min(5000, 300 * 2 ** this.retries);
|
||||
this.retries += 1;
|
||||
setTimeout(() => this._connect(), delay);
|
||||
};
|
||||
|
||||
socket.onerror = () => {};
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.socket) return;
|
||||
this._connect();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.closedByUser = true;
|
||||
this.handlers.clear();
|
||||
this.socket?.close();
|
||||
this.socket = null;
|
||||
this.connected = false;
|
||||
this.socketId = null;
|
||||
}
|
||||
|
||||
_handle(msg) {
|
||||
const { event, channel } = msg;
|
||||
|
||||
if (event === 'pusher:connection_established') {
|
||||
try {
|
||||
this.socketId = JSON.parse(msg.data).socket_id;
|
||||
} catch {
|
||||
this.socketId = null;
|
||||
}
|
||||
this.connected = true;
|
||||
this.onConnectionChange?.(true);
|
||||
for (const channel of this.handlers.keys()) {
|
||||
this._sendSubscribe(channel);
|
||||
}
|
||||
this.queue = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (event === 'pusher_internal:subscription_succeeded') {
|
||||
if (channel) this.handlers.get(channel)?.onSubscribed?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event === 'pusher:ping') {
|
||||
this.send({ event: 'pusher:pong', data: {} });
|
||||
return;
|
||||
}
|
||||
|
||||
if (event === 'pusher:pong') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (channel && this.handlers.has(channel)) {
|
||||
let payload = msg.data;
|
||||
if (typeof payload === 'string') {
|
||||
try {
|
||||
payload = JSON.parse(payload);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.handlers.get(channel)?.onEvent?.(event, payload);
|
||||
}
|
||||
}
|
||||
|
||||
_sendSubscribe(channel) {
|
||||
this.send({ event: 'pusher:subscribe', data: { channel } });
|
||||
}
|
||||
|
||||
subscribe(channel, { onEvent = null, onSubscribed = null } = {}) {
|
||||
this.handlers.set(channel, { onEvent, onSubscribed });
|
||||
if (this.connected) {
|
||||
this._sendSubscribe(channel);
|
||||
} else {
|
||||
this.queue.push({ channel });
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribe(channel) {
|
||||
this.handlers.delete(channel);
|
||||
this.queue = this.queue.filter((entry) => entry.channel !== channel);
|
||||
if (this.connected) {
|
||||
this.send({ event: 'pusher:unsubscribe', data: { channel } });
|
||||
}
|
||||
}
|
||||
|
||||
trigger(event, channel, data) {
|
||||
if (!this.connected) return;
|
||||
this.send({ event, channel, data });
|
||||
}
|
||||
|
||||
send(payload) {
|
||||
if (this.socket?.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(JSON.stringify(payload));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user