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}