lab13-sdk
Version:
JS13K Online communication protocol SDK for multiplayer games.
176 lines (175 loc) • 6.34 kB
JavaScript
//#region src/index.ts
const generateId = () => {
const t = Date.now().toString(36);
const r = Math.random().toString(36).slice(2, 8);
return (t + r).slice(-16);
};
const Lab13Client = (socket, options) => {
const { bot = false } = options || {};
let playerId = null;
const clientIds = /* @__PURE__ */ new Set();
const playerIds = /* @__PURE__ */ new Set();
const botIds = /* @__PURE__ */ new Set();
const clientType = bot ? "bot" : "player";
let state = {};
const tombstoneEntities = /* @__PURE__ */ new Set();
function deepMerge(target, source, isEntityCollection = false) {
const result = { ...target };
for (const key in source) {
if (tombstoneEntities.has(key) && isEntityCollection) continue;
if (source[key] === null) {
delete result[key];
if (isEntityCollection) tombstoneEntities.add(key);
} else if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) result[key] = deepMerge(result[key] || {}, source[key], key.startsWith("_"));
else result[key] = source[key];
}
return result;
}
socket.addEventListener("message", (event) => {
const msg = event.data.toString();
console.log("got message", msg.slice(0, 1));
switch (msg[0]) {
case "@":
playerId = msg.slice(1);
if (bot) api.sendToAll(`b${playerId}`);
socket.dispatchEvent(new CustomEvent("player-id-updated", { detail: playerId }));
break;
case "b":
const botId = msg.slice(1);
clientIds.add(botId);
botIds.add(botId);
playerIds.delete(botId);
if (state._players && state._players[botId]) delete state._players[botId];
console.log("got bot");
socket.dispatchEvent(new CustomEvent("client-ids-updated", { detail: Array.from(clientIds) }));
socket.dispatchEvent(new CustomEvent("player-ids-updated", { detail: Array.from(playerIds) }));
socket.dispatchEvent(new CustomEvent("bot-ids-updated", { detail: Array.from(botIds) }));
break;
case "+":
const newClientId = msg.slice(1);
clientIds.add(newClientId);
botIds.delete(newClientId);
playerIds.add(newClientId);
if (!state._players) state._players = {};
state._players[newClientId] = { id: newClientId };
socket.dispatchEvent(new CustomEvent("client-connected", { detail: newClientId }));
socket.dispatchEvent(new CustomEvent("client-ids-updated", { detail: Array.from(clientIds) }));
socket.dispatchEvent(new CustomEvent("player-ids-updated", { detail: Array.from(playerIds) }));
socket.dispatchEvent(new CustomEvent("bot-ids-updated", { detail: Array.from(botIds) }));
break;
case "-":
const disconnectedClientId = msg.slice(1);
clientIds.delete(disconnectedClientId);
playerIds.delete(disconnectedClientId);
botIds.delete(disconnectedClientId);
if (state._players && state._players[disconnectedClientId]) {
delete state._players[disconnectedClientId];
tombstoneEntities.add(disconnectedClientId);
}
socket.dispatchEvent(new CustomEvent("client-disconnected", { detail: disconnectedClientId }));
socket.dispatchEvent(new CustomEvent("client-ids-updated", { detail: Array.from(clientIds) }));
socket.dispatchEvent(new CustomEvent("player-ids-updated", { detail: Array.from(playerIds) }));
socket.dispatchEvent(new CustomEvent("bot-ids-updated", { detail: Array.from(botIds) }));
break;
case "?":
{
const subCommand = msg[1];
switch (subCommand) {
case "i":
const replyPlayerId = msg.slice(2);
if (playerId) api.sendToPlayer(replyPlayerId, `.i${playerId}${bot ? "|b" : ""}`);
break;
case "s":
const replyId = msg.slice(2);
api.sendToPlayer(replyId, `.s${JSON.stringify(state)}`);
break;
default: break;
}
}
break;
case ".":
{
const subCommand = msg[1];
switch (subCommand) {
case "i":
const [newClientId$1, isBot] = msg.slice(2).split("|");
clientIds.add(newClientId$1);
if (isBot) {
botIds.add(newClientId$1);
playerIds.delete(newClientId$1);
if (state._players && state._players[newClientId$1]) delete state._players[newClientId$1];
} else {
playerIds.add(newClientId$1);
botIds.delete(newClientId$1);
if (!state._players) state._players = {};
if (!state._players[newClientId$1]) state._players[newClientId$1] = { id: newClientId$1 };
}
socket.dispatchEvent(new CustomEvent("client-ids-updated", { detail: Array.from(clientIds) }));
socket.dispatchEvent(new CustomEvent("player-ids-updated", { detail: Array.from(playerIds) }));
socket.dispatchEvent(new CustomEvent("bot-ids-updated", { detail: Array.from(botIds) }));
break;
case "s":
try {
state = JSON.parse(msg.slice(2));
socket.dispatchEvent(new CustomEvent("state-updated", { detail: { state } }));
} catch (e) {
console.error("Failed to parse state data:", e);
}
break;
default: break;
}
}
break;
case "d":
try {
const delta = JSON.parse(msg.slice(1));
state = deepMerge(state, delta);
socket.dispatchEvent(new CustomEvent("state-updated", { detail: {
state,
delta
} }));
} catch (e) {
console.error("Failed to parse delta:", e);
}
break;
default: break;
}
});
const api = {
queryPlayerIds: () => {
if (playerId) socket.send(`?i${playerId}`);
},
on(event, callback) {
socket.addEventListener(event, callback);
},
off(event, callback) {
socket.removeEventListener(event, callback);
},
playerId: () => playerId,
clientIds: () => Array.from(clientIds),
playerIds: () => Array.from(playerIds),
botIds: () => Array.from(botIds),
clientType: () => clientType,
sendToPlayer: (playerId$1, message) => {
socket.send(`@${playerId$1}|${message}`);
},
sendToAll: (message) => {
socket.send(`${message}`);
},
state: () => state,
mutateState: (delta) => {
state = deepMerge(state, delta);
socket.send(`d${JSON.stringify(delta)}`);
socket.dispatchEvent(new CustomEvent("state-updated", { detail: {
state,
delta
} }));
},
generateId
};
return api;
};
if (typeof window !== "undefined") window.Lab13Client = Lab13Client;
//#endregion
export { Lab13Client, generateId };
//# sourceMappingURL=index.js.map