A Typescript server emulator for Box Critters, a defunct virtual world.
1import { Server, Socket } from "https://deno.land/x/socket_io@0.2.0/mod.ts";
2import z from "zod";
3
4import * as world from "@/constants/world.ts";
5import * as utils from "@/utils.ts";
6import * as types from "@/types.ts";
7
8import parties from "@/constants/parties.json" with { type: "json" };
9
10export function listen(
11 _io: Server,
12 socket: Socket,
13 ctx: types.SocketHandlerContext,
14) {
15 socket.on("joinRoom", (roomId: string) => {
16 if (!ctx.localPlayer || !ctx.localCrumb) return;
17
18 if (
19 z.object({
20 roomId: z.enum(Object.keys(world.rooms) as [string, ...string[]]),
21 }).safeParse({ roomId: roomId }).success == false
22 ) return;
23
24 const _room = (world.rooms[roomId] || { default: null }).default;
25 if (!_room) return;
26
27 socket.leave(ctx.localCrumb._roomId);
28 socket.broadcast.in(ctx.localCrumb._roomId).emit("R", ctx.localCrumb);
29
30 const modEnabled = (ctx.localPlayer._mods || []).includes("roomExits");
31 //@ts-ignore: Index type is correct
32 const correctExit = world.roomExits[ctx.localCrumb._roomId + "->" + roomId];
33 if (modEnabled && correctExit) {
34 ctx.localPlayer.x = correctExit.x;
35 ctx.localPlayer.y = correctExit.y;
36 ctx.localPlayer.rotation = correctExit.r;
37 }
38
39 if (!modEnabled || !correctExit) {
40 ctx.localPlayer.x = _room.startX;
41 ctx.localPlayer.y = _room.startY;
42 ctx.localPlayer.rotation = _room.startR | 180;
43 }
44
45 ctx.localCrumb = utils.makeCrumb(ctx.localPlayer, roomId);
46 world.players[ctx.localPlayer.playerId] = ctx.localCrumb;
47
48 console.log("> " + ctx.localPlayer.nickname + ' joined "' + roomId + '"!');
49 socket.join(roomId);
50
51 let playerCrumbs = Object.values(world.players).filter((crumb) =>
52 crumb._roomId == roomId
53 );
54 if (world.npcs[roomId]) {
55 playerCrumbs = [
56 ...playerCrumbs,
57 ...world.npcs[roomId],
58 ];
59 }
60 socket.emit("joinRoom", {
61 name: _room.name,
62 roomId: roomId,
63 playerCrumbs: playerCrumbs,
64 });
65
66 socket.broadcast.in(ctx.localCrumb._roomId).emit("A", ctx.localCrumb);
67 });
68
69 socket.on("switchParty", (partyId: string) => {
70 if (!ctx.localPlayer || !ctx.localCrumb) return;
71
72 if (
73 z.object({
74 partyId: z.enum(Object.keys(parties) as [string, ...string[]]),
75 }).strict().safeParse({ partyId: partyId }).success == false
76 ) return;
77
78 ctx.localPlayer._partyId = partyId;
79 socket.emit("switchParty");
80 });
81
82 socket.on("trigger", async () => {
83 if (!ctx.localPlayer || !ctx.localCrumb) return;
84
85 const activatedTrigger = await utils.getTrigger(
86 ctx.localPlayer,
87 ctx.localCrumb._roomId,
88 ctx.localPlayer._partyId,
89 );
90 if (!activatedTrigger) return;
91
92 if (activatedTrigger.hasItems) {
93 for (const item of activatedTrigger.hasItems) {
94 if (!ctx.localPlayer.inventory.includes(item)) return;
95 }
96 }
97
98 if (activatedTrigger.grantItem) {
99 let items = activatedTrigger.grantItem;
100 if (typeof items == "string") items = [items];
101
102 for (const item of items) {
103 if (!ctx.localPlayer.inventory.includes(item)) {
104 socket.emit("addItem", { itemId: item, showGUI: true });
105 ctx.localPlayer.inventory.push(item);
106 }
107 }
108 }
109
110 if (activatedTrigger.addEgg) {
111 const egg = activatedTrigger.addEgg;
112 socket.emit("addEgg", egg);
113 ctx.localPlayer.eggs.push(egg);
114 }
115 });
116}