programming-game
Version:
The client for programming game, an mmorpg that you interact with entirely through code.
213 lines (195 loc) • 5.83 kB
text/typescript
import {
BatchedEvents,
ClientSideUnit,
GameObject,
Intent,
PlayersSeekingParty,
Position,
UniqueItemId,
} from "./types";
import { ItemDefinition, Items } from "./items";
import { version } from "./package.json";
import { BaseClient, OnEvent, OnTickCurrentPlayer } from "./base-client";
export type { OnTickCurrentPlayer } from "./base-client"; // just here for backwards compatibility, can remove later
interface BasicHeartbeat {
time: number;
inArena: boolean;
instanceId: string;
player?: OnTickCurrentPlayer;
playersSeekingParty: PlayersSeekingParty;
units: Record<string, ClientSideUnit>;
items: Record<UniqueItemId | Items, ItemDefinition>;
gameObjects: Record<string, GameObject>;
boundary?: {
xMin: number;
xMax: number;
yMin: number;
yMax: number;
};
}
export type TickHeartbeat = OverworldHeartBeat | ArenaHeartBeat;
interface OverworldHeartBeat extends BasicHeartbeat {
inArena: false;
player: OnTickCurrentPlayer;
}
interface ArenaHeartBeat extends BasicHeartbeat {
inArena: true;
arenaTimeRemaining: number;
player?: OnTickCurrentPlayer;
}
export const distance = (pos1: Position, pos2: Position) => {
return Math.sqrt((pos1.x - pos2.x) ** 2 + (pos1.y - pos2.y) ** 2);
};
export type OnTick = (heartbeat: TickHeartbeat) => Intent | void;
type ConnectProps = {
credentials: {
id: string;
key: string;
};
onTick: OnTick;
onEvent?: OnEvent;
};
class InnerSocket {
private socket: WebSocket;
private closed: boolean = false;
private handlers = new Map<string, Set<(data: any) => void>>();
private credentials: ConnectProps["credentials"];
private reconnecting: boolean = false;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
constructor(credentials: ConnectProps["credentials"]) {
this.credentials = credentials;
this.socket = this.reconnect();
}
private reconnect() {
console.log("reconnect call...");
const socket = new WebSocket(
process.env.NODE_ENV === "development"
? "http://localhost:3001"
: `wss://programming-game.com`
);
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnecting = false;
}
this.reconnecting = true;
socket.addEventListener("open", () => {
this.reconnecting = false;
console.log("connected to server!");
socket.send(
JSON.stringify({
type: "credentials",
value: this.credentials,
})
);
});
const attemptReconnect = () => {
if (this.closed) return;
if (this.reconnecting) return;
this.reconnecting = true;
this.reconnectTimer = setTimeout(() => {
console.log("attempting to reconnect...");
this.socket = this.reconnect();
}, 1000);
};
socket.addEventListener("error", (event) => {
this.reconnecting = false;
console.log("WebSocket error...", Date.now());
attemptReconnect();
});
// @ts-ignore
socket.addEventListener("close", (e, b) => {
console.log("WebSocket closed", e, b);
this.triggerHandlers("close", null);
attemptReconnect();
});
socket.addEventListener("message", (event) => {
try {
const data = JSON.parse(event.data.toString());
switch (data.type) {
case "version": {
return this.triggerHandlers("version", data.value);
}
case "events": {
return this.triggerHandlers("events", data.value);
}
default: {
console.error("Unknown message type:", data.type);
}
}
} catch (e) {
console.log("Error parsing message:", e);
}
});
return socket;
}
private triggerHandlers(event: string, data: any) {
this.handlers.get(event)?.forEach((handler) => {
handler(data);
});
}
emit(event: string, data: any) {
this.socket.send(JSON.stringify({ type: event, value: data }));
}
on(event: "close", callback: () => void): void;
on(event: "version", callback: (version: string) => void): void;
on(event: "events", callback: (events: BatchedEvents) => void): void;
on(event: string, callback: (data: any) => void) {
let set = this.handlers.get(event) || new Set<(data: any) => void>();
if (!this.handlers.has(event)) {
this.handlers.set(event, set);
}
set.add(callback);
}
close() {
this.closed = true;
this.socket.close();
}
}
export const connect = ({
credentials,
onTick,
onEvent,
}: ConnectProps): (() => void) => {
if (typeof WebSocket === "undefined") {
throw new Error(
"No global WebSocket, please upgrade to Node 22, play in the browser, or polyfill w/ a compatible library."
);
}
const socket = new InnerSocket(credentials);
const versionHandler = (serverVersion: string) => {
const clientVersion = version;
if (serverVersion !== clientVersion) {
const lines: string[] = [];
lines.push("There's an updated version of programming-game.");
lines.push(
`Your version: ${clientVersion}, latest version: ${serverVersion}`
);
console.log("\n" + lines.join("\n") + "\n");
} else {
console.log(
`\nYou're on the latest version of programming-game, enjoy!.\n`
);
}
};
socket.on("version", versionHandler);
const baseClient = new BaseClient({
onTick,
onEvent,
setIntent: (intent) => {
socket.emit("setIntent", intent);
},
});
// @ts-ignore
socket.on("error", (error) => {
console.log("socket error", error);
});
socket.on("events", baseClient.eventsHandler.bind(baseClient));
// @ts-ignore
socket.on("close", (error) => {
baseClient.clearState();
});
return () => {
socket.close();
baseClient.clearState();
};
};