A Typescript server emulator for Box Critters, a defunct virtual world.
1import { Image } from 'https://deno.land/x/imagescript@1.3.0/mod.ts'; 2import chalk from "https://deno.land/x/chalk_deno@v4.1.1-deno/source/index.js" 3import { join } from "https://deno.land/std@0.224.0/path/mod.ts"; 4 5import { rooms, spawnRoom } from "./constants/world.ts"; 6import { Room, LocalPlayer, PlayerCrumb } from "./types.ts"; 7 8/** Condenses the local player variable into data that is sufficient enough for other clients */ 9export function makeCrumb(player: LocalPlayer, roomId: string): PlayerCrumb { 10 return { 11 i: player.playerId, 12 n: player.nickname, 13 c: player.critterId, 14 x: player.x, 15 y: player.y, 16 r: player.rotation, 17 g: player.gear, 18 19 // message & emote 20 m: "", 21 e: "", 22 23 _roomId: roomId 24 } 25} 26 27// TODO: use the correct triggers for the active party 28export async function getTrigger(player: LocalPlayer, roomId: string, partyId: string) { 29 const room = rooms[roomId][partyId]; 30 if (!room) { 31 console.log(chalk.red(`[!] Cannot find room: "${roomId}@${partyId}"!`)); 32 return; 33 } 34 35 try { 36 //@ts-ignore: Deno lint 37 const treasureBuffer = await Deno.readFile(room.media.treasure?.replace('..','public')); 38 const treasure = await Image.decode(treasureBuffer); 39 if (!treasure) throw new Error('Missing map server for room "' + roomId + '"!'); 40 41 const pixel = treasure.getPixelAt(player.x, player.y); 42 const r = (pixel >> 24) & 0xFF, g = (pixel >> 16) & 0xFF, b = (pixel >> 8) & 0xFF; 43 const hexCode = ((r << 16) | (g << 8) | b).toString(16).padStart(6, '0'); 44 45 const trigger = room.triggers.find((trigger) => trigger.hex == hexCode); 46 if (trigger) { 47 return trigger.server; 48 } else { 49 return null; 50 } 51 } catch(e) { 52 console.warn(chalk.red('[!] Caught error while checking for activated trigger.'), e); 53 } 54} 55 56export function getNewCodeItem(player: LocalPlayer, items: Array<string>) { 57 const itemsSet = new Set(player.inventory); 58 const available = items.filter(item => !itemsSet.has(item)); 59 return available.length === 0 ? null : available[Math.floor(Math.random() * available.length)]; 60} 61 62/** 63 * Indexes the /media/rooms directory for all versions of all rooms 64 * @returns All versions of every room 65 */ 66export async function indexRoomData() { 67 const _roomData: Record<string, Record<string, Room>> = {}; 68 69 const basePath = join(Deno.cwd(), 'public', 'media', 'rooms'); 70 const _rooms = Deno.readDir(basePath); 71 72 for await (const room of _rooms) { 73 if (room.isDirectory) { 74 _roomData[room.name] = {}; 75 const roomPath = join(basePath, room.name); 76 const versions = Deno.readDir(roomPath); 77 for await (const version of versions) { 78 if (version.isDirectory) { 79 const versionPath = join(roomPath, version.name, 'data.json'); 80 try { 81 const data = await Deno.readTextFile(versionPath); 82 _roomData[room.name][version.name] = JSON.parse(data); 83 } catch(_) { 84 console.log(chalk.red('[!] "%s@%s" is missing a data.json file'), room.name, version.name); 85 }; 86 } 87 } 88 } 89 } 90 91 return _roomData 92} 93 94export async function getAccount(nickname?: string) { 95 let accounts = []; 96 try { 97 const data = await Deno.readTextFile('accounts.json'); 98 accounts = JSON.parse(data); 99 } catch (error) { 100 if (error instanceof Deno.errors.NotFound) { 101 console.log(chalk.gray('Persistent login JSON is missing, using blank JSON array..')); 102 accounts = []; 103 } else { 104 console.log(chalk.red('[!] Failure to fetch persistent login data with nickname: '), nickname); 105 throw error; 106 }; 107 } 108 109 if (nickname) { 110 const existingAccount = accounts.find((player: { nickname: string }) => player.nickname == nickname); 111 if (existingAccount) { 112 return { 113 all: accounts, 114 individual: existingAccount, 115 } 116 } else { 117 return { 118 all: accounts, 119 individual: null 120 } 121 } 122 } else { 123 return accounts; 124 } 125} 126 127export async function updateAccount(nickname: string, property: string, value: unknown) { 128 if (["x", "y", "rotation", "_partyId"].includes(property)) return; 129 const accounts = await getAccount(nickname); 130 131 accounts.individual[property] = value; 132 await Deno.writeTextFile('accounts.json', JSON.stringify(accounts.all, null, 2)); 133} 134 135export function trimAccount(player: LocalPlayer) { 136 for (const key of [ 137 "critterId", 138 "x", 139 "y", 140 "rotation", 141 "_partyId", 142 "_mods" 143 ]) { 144 delete player[key]; 145 } 146 return player; 147} 148 149export function expandAccount(player: LocalPlayer) { 150 const defaultPos = rooms[spawnRoom].default; 151 player.x = defaultPos.startX; 152 player.y = defaultPos.startY; 153 player.rotation = defaultPos.startR; 154 return player; 155} 156 157export function getDirection(x: number, y: number, targetX: number, targetY: number) { 158 const a = Math.floor((180 * Math.atan2(targetX - x, y - targetY)) / Math.PI); 159 return a < 0 ? a + 360 : a; 160} 161 162export async function createAccount(player: LocalPlayer) { 163 const accounts = await getAccount(); 164 accounts.push(trimAccount(player)); 165 166 await Deno.writeTextFile('accounts.json', JSON.stringify(accounts, null, 2)); 167}