A Typescript server emulator for Box Critters, a defunct virtual world.
at main 5.7 kB view raw
1// deno-lint-ignore-file no-explicit-any 2import { Server, Socket } from "https://deno.land/x/socket_io@0.2.0/mod.ts"; 3import z from "zod"; 4 5import * as world from "@/constants/world.ts"; 6import * as items from "@/constants/items.ts"; 7import * as utils from "@/utils.ts"; 8import * as types from "@/types.ts"; 9 10import itemsJSON from "@/constants/items.json" with { type: "json" }; 11 12export function listen( 13 io: Server, 14 socket: Socket, 15 ctx: types.SocketHandlerContext, 16) { 17 socket.on("message", (text: string) => { 18 if (!ctx.localPlayer || !ctx.localCrumb) return; 19 20 if ( 21 z.object({ 22 text: z.string().nonempty(), 23 }).safeParse({ text: text }).success == false 24 ) return; 25 26 console.log(`> ${ctx.localPlayer.nickname} sent message:`, text); 27 ctx.localCrumb.m = text; 28 29 socket.broadcast.in(ctx.localCrumb._roomId).emit("M", { 30 i: ctx.localPlayer.playerId, 31 m: text, 32 }); 33 34 setTimeout(() => { 35 if (ctx.localCrumb!.m != text) return; 36 ctx.localCrumb!.m = ""; 37 }, 5e3); 38 }); 39 40 socket.on("emote", (emote: string) => { 41 if (!ctx.localPlayer || !ctx.localCrumb) return; 42 43 if ( 44 z.object({ 45 emote: z.string().nonempty(), // TODO: make this an enum 46 }).safeParse({ emote: emote }).success == false 47 ) return; 48 49 console.log(`> ${ctx.localPlayer.nickname} sent emote:`, emote); 50 ctx.localCrumb.e = emote; 51 52 socket.broadcast.in(ctx.localCrumb._roomId).emit("E", { 53 i: ctx.localPlayer.playerId, 54 e: emote, 55 }); 56 57 setTimeout(() => { 58 if (ctx.localCrumb!.e != emote) return; 59 ctx.localCrumb!.e = ""; 60 }, 5e3); 61 }); 62 63 // ? Options is specified just because sometimes it is sent, but its always an empty string 64 socket.on("code", (code: string, _options?: string) => { 65 if (!ctx.localPlayer || !ctx.localCrumb) return; 66 67 if ( 68 z.object({ 69 command: z.enum([ 70 "pop", 71 "freeitem", 72 "tbt", 73 "darkmode", 74 "spydar", 75 "allitems", 76 ]), 77 }).safeParse({ 78 command: code, 79 }).success == false 80 ) return; 81 82 console.log(`> ${ctx.localPlayer.nickname} sent code:`, code); 83 84 const addItem = function (id: string, showGUI: boolean = false) { 85 if (!ctx.localPlayer!.inventory.includes(id)) { 86 socket.emit("addItem", { itemId: id, showGUI: showGUI }); 87 ctx.localPlayer!.inventory.push(id); 88 } 89 }; 90 91 // Misc. Codes 92 switch (code) { 93 case "pop": { 94 socket.emit( 95 "pop", 96 Object.values(world.players).filter((critter) => 97 critter.c != "huggable" 98 ).length, 99 ); 100 break; 101 } 102 case "freeitem": { 103 addItem(items.shop.freeItem.itemId, true); 104 break; 105 } 106 case "tbt": { 107 const _throwbackItem = utils.getNewCodeItem( 108 ctx.localPlayer, 109 items.throwback, 110 ); 111 if (_throwbackItem) addItem(_throwbackItem, true); 112 break; 113 } 114 case "darkmode": { 115 addItem("3d_black", true); 116 break; 117 } 118 case "spydar": { 119 ctx.localPlayer.gear = [ 120 "sun_orange", 121 "super_mask_black", 122 "toque_blue", 123 "dracula_cloak", 124 "headphones_black", 125 "hoodie_black", 126 ]; 127 128 if (ctx.localCrumb._roomId == "tavern") { 129 ctx.localPlayer.x = 216; 130 ctx.localPlayer.y = 118; 131 132 ctx.localCrumb.x = 216; 133 ctx.localCrumb.y = 118; 134 135 io.in(ctx.localCrumb._roomId).volatile.emit("X", { 136 i: ctx.localPlayer.playerId, 137 x: 216, 138 y: 118, 139 }); 140 } 141 142 io.in(ctx.localCrumb._roomId).emit("G", { 143 i: ctx.localPlayer.playerId, 144 g: ctx.localPlayer.gear, 145 }); 146 147 socket.emit("updateGear", ctx.localPlayer.gear); 148 break; 149 } 150 case "allitems": { 151 for (const item of itemsJSON) { 152 addItem(item.itemId, false); 153 } 154 break; 155 } 156 } 157 158 // Item Codes 159 const _itemCodes = items.codes as Record<string, string | Array<string>>; 160 const item = _itemCodes[code]; 161 162 if (typeof item == "string") { 163 addItem(item, true); 164 } else if (typeof item == "object") { 165 for (const _ of item) { 166 addItem(_, true); 167 } 168 } 169 170 // Event Codes (eg. Christmas 2019) 171 const _eventItemCodes = items.eventCodes as Record< 172 string, 173 Record<string, string> 174 >; 175 const eventItem = (_eventItemCodes[ctx.localPlayer._partyId] || {})[code]; 176 if (eventItem) addItem(eventItem); 177 }); 178 179 socket.on("addIgnore", (playerId: string) => { 180 if (!ctx.localPlayer || !ctx.localCrumb) return; 181 182 if ( 183 z.object({ 184 playerId: z.enum(Object.keys(world.players) as any), 185 }).strict().safeParse({ playerId: playerId }).success == false 186 ) return; 187 188 if ( 189 Object.keys(world.players).includes(playerId) && 190 !ctx.localPlayer.ignore.includes(playerId) 191 ) { 192 ctx.localPlayer.ignore.push(playerId); 193 } 194 }); 195 196 socket.on("attack", (playerId: string) => { 197 if (!ctx.localPlayer || !ctx.localCrumb) return; 198 199 if ( 200 z.object({ 201 playerId: z.enum(Object.keys(world.players) as any), 202 }).strict().safeParse({ playerId: playerId }).success == false 203 ) return; 204 205 if (!ctx.localPlayer.gear.includes("bb_beebee")) return; 206 const monster = Object.values(world.players).find((player) => 207 player.i == playerId && player.c == "huggable" 208 ); 209 210 if (monster) { 211 io.in(ctx.localCrumb._roomId).emit("R", monster); 212 213 ctx.localPlayer.coins += 10; 214 socket.emit("updateCoins", { balance: ctx.localPlayer.coins }); 215 216 delete world.players[playerId]; 217 } 218 }); 219}