A Typescript server emulator for Box Critters, a defunct virtual world.

v1.1

- minor: add checks for public dir & env vars
- feat: `deno compile` support
- fix: use .default ver of room as backup with moveTo event to prevent server crashes
- style: restructure all code into a /src/ folder
- style: deno format
- style: update README and package.json
- feat: clean up dependencies & use Deno.serve instead of Hono
- feat: "Today in 2019/2020/2021" party switcher
- minor: --port CLI config
- fix: API responding parties in incorrect format

Index 9b12e23d d87f5628

+2
.env.build
···
···
+
EXECUTABLE=true
+
JWT_TOKEN="boxcritters"
+1
.env.example
···
JWT_TOKEN=
···
+
EXECUTABLE=
JWT_TOKEN=
+2
.gitignore
···
Thumbs.db
accounts.json
/node_modules
···
Thumbs.db
accounts.json
/node_modules
+
Localbox
+
/public
+15
Dockerfile
···
···
+
FROM denoland/deno:2.1.5
+
+
WORKDIR /app
+
+
COPY deno.json .
+
+
RUN deno install
+
+
COPY . .
+
RUN deno cache main.ts
+
+
ARG PORT=3257
+
EXPOSE $PORT
+
+
CMD ["task", "start"]
+16 -9
Events.md
···
# Event Recreation Progress
-
A checklist for each party of which rooms are finished. Not all parties will be possible due to them not being archived sadly. This list ignores tedious animations which can be done at a later date.
## Default (no party)
···
## Battle Bears
-
This event had no significant changes. The Crash Site room became a default room, and is classified as that.
### Christmas
···
- [x] Tavern
- [x] Port
- [x] Shack
-
- [x] **Solar System** *(The background and foreground that were archived are corrupted)*
### Halloween
···
### Club Penguin Celebration
-
- [x] Port (practically the same as `Halloween 2020`'s Port, just with a party hat on the outside of the `Shack`)
- [x] Shack
### CritterCon
···
### New Years
-
- [ ] Port
-
*The wiki only includes a screenshot of the Port, and no other rooms.*
### 2nd Anniversary
···
- [x] Port
- [x] Shack
- [x] Jungle
-
- [x] **Box Realm (work in progress)** *(the background is the only file found so far)*
### Easter
···
### Club Penguin Celebration
-
- [x] Port (practically the same as `Halloween 2021`'s Port, just with a party hat on the outside of the `Shack`)
- [x] Shack
### CritterCon
···
- [ ] Cellar
- [ ] Port
- [ ] Jungle
-
- [ ] Shack
···
# Event Recreation Progress
+
A checklist for each party of which rooms are finished. Not all parties will be
+
possible due to them not being archived sadly. This list ignores tedious
+
animations which can be done at a later date.
## Default (no party)
···
## Battle Bears
+
This event had no significant changes. The Crash Site room became a default
+
room, and is classified as that.
### Christmas
···
- [x] Tavern
- [x] Port
- [x] Shack
+
- [x] **Solar System** _(The background and foreground that were archived are
+
corrupted)_
### Halloween
···
### Club Penguin Celebration
+
- [x] Port (practically the same as `Halloween 2020`'s Port, just with a party
+
hat on the outside of the `Shack`)
- [x] Shack
### CritterCon
···
### New Years
+
- [ ] Port _The wiki only includes a screenshot of the Port, and no other
+
rooms._
### 2nd Anniversary
···
- [x] Port
- [x] Shack
- [x] Jungle
+
- [x] **Box Realm (work in progress)** _(the background is the only file found
+
so far)_
### Easter
···
### Club Penguin Celebration
+
- [x] Port (practically the same as `Halloween 2021`'s Port, just with a party
+
hat on the outside of the `Shack`)
- [x] Shack
### CritterCon
···
- [ ] Cellar
- [ ] Port
- [ ] Jungle
+
- [ ] Shack
+21
LICENSE
···
···
+
MIT License
+
+
Copyright (c) 2025 Index
+
+
Permission is hereby granted, free of charge, to any person obtaining a copy
+
of this software and associated documentation files (the "Software"), to deal
+
in the Software without restriction, including without limitation the rights
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+
copies of the Software, and to permit persons to whom the Software is
+
furnished to do so, subject to the following conditions:
+
+
The above copyright notice and this permission notice shall be included in all
+
copies or substantial portions of the Software.
+
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+
SOFTWARE.
+89 -13
README.md
···
-
# Box Critters Localbox
-
Reopening the dusty box of the world of Box Critters! This is a Typescript server emulator using Deno.
## Party Switcher
-
A custom party switcher has been implemented, you can change the party on the log-in page, or using the `/party [ID]` command in-game. For a breakdown of party room recreation progress, go [here](Events.md).
## Development
> Installation
```bash
deno install
```
> Serving
```bash
deno run start
> Listening on http://localhost:3257/
```
-
## APIs
-
The game has 4 APIs:
-
### (GET) `/api/server/players`
-
This API returns information on the player(s) in-game, if any.
-
### (GET) `/api/server/rooms`
-
This API returns almost-identical information as the `/api/client/rooms` API, however it returns information on all hashes of all rooms with no required party ID URL parameter.
-
### (POST) `/api/client/login`
-
This API takes in all the information provided by the user on log-in and generates a JWT for that session.
-
### (GET) `/api/client/rooms?partyId=`
-
This API returns information on the parties the game supports, and depending on the party ID provided in the URL, information for each room that party changes in some way. If the party does not change the room in any way, the default version of that room will be returned. All the data is gathered by the server reading the `/public/media/rooms/` directory of the game - and cached for future requests.
···
+
# Localbox
+
+
Reopen the dusty box of the world of Box Critters! This repository features a
+
Typescript server emulator, **built using Deno**. The version of the game the
+
server is built around is **client version 161**.
+
+
This repository appears empty because it is a fork of my private repository so
+
that the assets didn't get included in the public version of the repository.
+
+
_project start: late November 2024_
+
+
## Assets
+
+
The assets for the game are not included in this repository, since I've been
+
told that Rocketsnail has taken down GitHub repositories for hosting the assets.
+
Though, I find it ironic since the modding community has been allowed to freely
+
host asset archives [here](https://github.com/boxcrittersmods/BCArchive) for 5
+
years at this point. As far as I can see, Localbox has the most comprehensive
+
archive of Box Critters assets, compiling several sources & custom spritesheet
+
JSON for spritesheet mis-matches.
+
+
### Archive Statistics
+
+
Below is a break down of the archive compilation, to be consider archived the
+
spritesheet has to be archived (spritesheet JSON is not taken into
+
consideration, as it is pretty easy to custom make). To get the assets, feel
+
free message me on Discord: @index.lua
+
+
> _Note:_ I do plan on manually cropping some rooms to make custom spritesheets,
+
> but when I do those will be marked as custom and not legit.
+
+
- **6/7** full-time rooms archived
+
- Full-time rooms are rooms that were always available, no matter the party.
+
- Missing: [_Jungle_](https://box-critters.fandom.com/wiki/Jungle)
+
+
- **6/10** party-exclusive rooms archived
+
- Party-exclusive rooms are rooms that were only available during specific
+
parties.
+
- Missing:
+
[_Holiday Cliff_](https://box-critters.fandom.com/wiki/Holiday_Cliff),
+
[_Holiday Forest_](https://box-critters.fandom.com/wiki/Holiday_Forest),
+
[_CritterCon Hall_](https://box-critters.fandom.com/wiki/Critter_Con_Hall),
+
[_Box Realm_](https://box-critters.fandom.com/wiki/Box_Realm)
+
- **465/590** released items archived
+
+
- **12/12** critters archived
+
+
- **2/2** mini-games archived
+
- This count excludes _Critter Ball_, because that requires an entirely
+
separate backend. If you want to relive _Critter Ball_, check out
+
[FarawayDrip30's Critter Ball server](https://farawaydrip30.itch.io/critterball-server).
## Party Switcher
+
A custom party switcher has been implemented, you can change the party on the
+
log-in page, or using the `/party [ID]` command in-game. For a breakdown of
+
party room recreation progress, go [here](Events.md).
## Development
+
### CLI
+
> Installation
+
```bash
deno install
```
> Serving
+
```bash
deno run start
> Listening on http://localhost:3257/
```
+
> Building to an executable (shorthand invocation of `deno compile` using
+
> already-set flags & config)
+
```bash
+
deno run build
+
> If the command is a success, a "Localbox" executable will appear in the project directory.
+
```
+
### APIs
+
The game has 4 APIs for debugging or for use by the game client:
+
- (GET) `/api/server/players`
+
- This API returns information on the player(s) in-game, if any.
+
- (GET) `/api/server/rooms`
+
- This API returns almost-identical information as the `/api/client/rooms`
+
API, however it returns information on all hashes of all rooms with no
+
required party ID URL parameter.
+
- (POST) `/api/client/login`
+
- This API takes in all the information provided by the user on log-in and
+
generates a JWT for that session.
+
- (GET) `/api/client/rooms?partyId=`
+
- This API returns information on the parties the game supports, and depending
+
on the party ID provided in the URL, information for each room that party
+
changes in some way. If the party does not change the room in any way, the
+
default version of that room will be returned. All the data is gathered by
+
the server reading the `/public/media/rooms/` directory of the game - and
+
cached for future requests.
+
## Contributors
+
- [jonastisell](https://github.com/jonastisell) - spritesheet extraction help &
+
moral support
+
- [Boo0](https://github.com/Boo6447) - provided archived assets of party room
+
versions & moral support
+
- [@boxcrittersmods/BCArchive](https://github.com/boxcrittersmods/BCArchive) -
+
provided a lot of archived assets of early rooms
-10
chalk.d.ts
···
-
/*
-
Typescript doesn't seem to recognize the functions of the chalk_deno module, so some are specified here to avoid type warnings.
-
*/
-
-
declare module "https://deno.land/x/chalk_deno@v4.1.1-deno/source/index.js" {
-
export function red(text: string): string;
-
export function green(text: string): string;
-
export function blue(text: string): string;
-
export function gray(text: string): string;
-
}
···
+133 -133
constants/items.ts
···
export const shop = {
-
lastItem: { itemId: "beard2_brown", cost: 20 },
-
freeItem: { itemId: "goggles_pink", cost: 0},
-
nextItem: { itemId: "scout_uniform", cost: 0},
-
collection: [
-
{ itemId: "santa_hat", cost: 100 }
-
]
-
}
export const codes = {
-
rocketsnail: ["viking", "rocket_red"],
-
andybulletin: "propeller",
-
cute: "toque_pink",
-
oommgames: "space_red",
-
boxcritters3d: ["3d_white", "3d_black"],
-
goodnight: "sleeping",
-
madeincanada: "toque_white",
-
thekeeper: "party_green",
-
bunnyhug: "hoodie_blue",
-
greenplumber: "ballcap_green",
-
duckhunter: "float_pink",
-
piratepack: "pirate_patch",
-
pickle: "pickle",
-
oscarproductions: "guitar_blue",
-
livestream: "headphones_black",
-
creative: "headphones_black",
-
fun: "propeller_pink",
-
marco: "keytar_red",
-
snowball: "hoodie_white",
-
critbits: "ballcap_pink",
-
esporte: "kit_yellow",
-
squeeze: "hoodie_orange",
-
imagination: "box_brown",
-
sparkle: "propeller_silver",
-
adventure: "snorkel_blue",
-
tamago: "sun_square",
-
redcross: "australian_hat",
-
discordcritterspt: "headphones_green",
-
scarletraven: "wings_black",
-
boxcritterswiki: "paperhat_colour",
-
wikicritters: "brain_green",
-
boxcrittersguild: "cardboard_sword_silver",
-
staysafe: "doctor_mask_blue",
-
glitter: "pirate_capt_pink"
-
}
export const throwback = [
-
"goggles_black",
-
"pot",
-
"sombrero_yellow",
-
"cone",
-
"toque_purple",
-
"sun_orange",
-
"pirate_capt_black",
-
"lifejacket_red",
-
"ballcap_black",
-
"super_cape_red",
-
"tshirt_white",
-
"lei_red",
-
"ballcap_blue",
-
"viking_blue",
-
"overalls_orange",
-
"bunny_blue",
-
"messenger_brown",
-
"plaid_black",
-
"tinfoil_hat",
-
"monk",
-
"pirate_hat_black",
-
"hawaii_orange",
-
"space_black",
-
"traffic_cone",
-
"grass_yellow",
-
"ushanka",
-
"bandana_purple",
-
"ski_suit_blue",
-
"hotdog",
-
"space_blue",
-
"sweater_orange",
-
"tactical_headset",
-
"ballcap_yellow",
-
"ringmaster_suit_red",
-
"flight_helmet_red",
-
"rainhat_yellow",
-
"raincoat_yellow",
-
"ballcap_red",
-
"ballerina_pink",
-
"hawaii_blue",
-
"stripe_red_white",
-
"grass_green",
-
"plaid_blue",
-
"toque_orange",
-
"school_pack_orange",
-
"pirate_crew_blue",
-
"ringmaster_hat_black",
-
"super_mask_black",
-
"pirate_hat_pink",
-
"rainhat_red",
-
"tophat_black",
-
"scarf_red",
-
"skeleton_body",
-
"dracula_cloak",
-
"pumpkin",
-
"hotdog",
-
"beard3_black",
-
"scarf_purple",
-
"puffy_red",
-
"blockhead",
-
"hoodie_purple",
-
"winter_dress_red",
-
"bulb_blue",
-
"tacky_red",
-
"pirate_bandana_red"
-
]
export const eventCodes = {
-
christmas2019: {
-
jinglebells: "elf_hat_green",
-
winter: "winter_dress",
-
joy: "bulb_yellow",
-
rudolph: "reindeer_head",
-
elf: "elf_suit_green",
-
cheer: "bulb_green",
-
glow: "bulb_red",
-
rednose: "reindeer_body",
-
blizzard: "wizard_blizzard",
-
family: "ornament_red",
-
shine: "angel_halo",
-
peace: "angel_wings",
-
jolly: "santa_beard",
-
merry: "santa_hat",
-
christmas: "santa_suit",
-
boxingday: "onsie_plaid_red",
-
warm: "sleeping_red",
-
tacky: "tacky_green",
-
snow: "goggles_white",
-
beautiful: "winter_dress_red",
-
newyear: "tuxedo_black",
-
"2020": "tophat_black"
-
}
-
}
···
export const shop = {
+
lastItem: { itemId: "beard2_brown", cost: 20 },
+
freeItem: { itemId: "goggles_pink", cost: 0 },
+
nextItem: { itemId: "scout_uniform", cost: 0 },
+
collection: [
+
{ itemId: "santa_hat", cost: 100 },
+
],
+
};
export const codes = {
+
rocketsnail: ["viking", "rocket_red"],
+
andybulletin: "propeller",
+
cute: "toque_pink",
+
oommgames: "space_red",
+
boxcritters3d: ["3d_white", "3d_black"],
+
goodnight: "sleeping",
+
madeincanada: "toque_white",
+
thekeeper: "party_green",
+
bunnyhug: "hoodie_blue",
+
greenplumber: "ballcap_green",
+
duckhunter: "float_pink",
+
piratepack: "pirate_patch",
+
pickle: "pickle",
+
oscarproductions: "guitar_blue",
+
livestream: "headphones_black",
+
creative: "headphones_black",
+
fun: "propeller_pink",
+
marco: "keytar_red",
+
snowball: "hoodie_white",
+
critbits: "ballcap_pink",
+
esporte: "kit_yellow",
+
squeeze: "hoodie_orange",
+
imagination: "box_brown",
+
sparkle: "propeller_silver",
+
adventure: "snorkel_blue",
+
tamago: "sun_square",
+
redcross: "australian_hat",
+
discordcritterspt: "headphones_green",
+
scarletraven: "wings_black",
+
boxcritterswiki: "paperhat_colour",
+
wikicritters: "brain_green",
+
boxcrittersguild: "cardboard_sword_silver",
+
staysafe: "doctor_mask_blue",
+
glitter: "pirate_capt_pink",
+
};
export const throwback = [
+
"goggles_black",
+
"pot",
+
"sombrero_yellow",
+
"cone",
+
"toque_purple",
+
"sun_orange",
+
"pirate_capt_black",
+
"lifejacket_red",
+
"ballcap_black",
+
"super_cape_red",
+
"tshirt_white",
+
"lei_red",
+
"ballcap_blue",
+
"viking_blue",
+
"overalls_orange",
+
"bunny_blue",
+
"messenger_brown",
+
"plaid_black",
+
"tinfoil_hat",
+
"monk",
+
"pirate_hat_black",
+
"hawaii_orange",
+
"space_black",
+
"traffic_cone",
+
"grass_yellow",
+
"ushanka",
+
"bandana_purple",
+
"ski_suit_blue",
+
"hotdog",
+
"space_blue",
+
"sweater_orange",
+
"tactical_headset",
+
"ballcap_yellow",
+
"ringmaster_suit_red",
+
"flight_helmet_red",
+
"rainhat_yellow",
+
"raincoat_yellow",
+
"ballcap_red",
+
"ballerina_pink",
+
"hawaii_blue",
+
"stripe_red_white",
+
"grass_green",
+
"plaid_blue",
+
"toque_orange",
+
"school_pack_orange",
+
"pirate_crew_blue",
+
"ringmaster_hat_black",
+
"super_mask_black",
+
"pirate_hat_pink",
+
"rainhat_red",
+
"tophat_black",
+
"scarf_red",
+
"skeleton_body",
+
"dracula_cloak",
+
"pumpkin",
+
"hotdog",
+
"beard3_black",
+
"scarf_purple",
+
"puffy_red",
+
"blockhead",
+
"hoodie_purple",
+
"winter_dress_red",
+
"bulb_blue",
+
"tacky_red",
+
"pirate_bandana_red",
+
];
export const eventCodes = {
+
christmas2019: {
+
jinglebells: "elf_hat_green",
+
winter: "winter_dress",
+
joy: "bulb_yellow",
+
rudolph: "reindeer_head",
+
elf: "elf_suit_green",
+
cheer: "bulb_green",
+
glow: "bulb_red",
+
rednose: "reindeer_body",
+
blizzard: "wizard_blizzard",
+
family: "ornament_red",
+
shine: "angel_halo",
+
peace: "angel_wings",
+
jolly: "santa_beard",
+
merry: "santa_hat",
+
christmas: "santa_suit",
+
boxingday: "onsie_plaid_red",
+
warm: "sleeping_red",
+
tacky: "tacky_green",
+
snow: "goggles_white",
+
beautiful: "winter_dress_red",
+
newyear: "tuxedo_black",
+
"2020": "tophat_black",
+
},
+
};
+14
constants/parties-old.json
···
···
+
[
+
"default",
+
"easter2019",
+
"christmas2019",
+
"lucky2020",
+
"easter2020",
+
"space",
+
"grad2020",
+
"summer2020",
+
"halloween2019",
+
"halloween2020",
+
"fools2021",
+
"halloween2021"
+
]
+50 -14
constants/parties.json
···
-
[
-
"default",
-
"easter2019",
-
"christmas2019",
-
"lucky2020",
-
"easter2020",
-
"space",
-
"grad2020",
-
"summer2020",
-
"halloween2019",
-
"halloween2020",
-
"fools2021",
-
"halloween2021"
-
]
···
+
{
+
"default": {
+
"start": null,
+
"end": null
+
},
+
"easter2019": {
+
"start": "04-18-2019",
+
"end": "04-28-2019"
+
},
+
"halloween2019": {
+
"start": "10-20-2019",
+
"end": "11-06-2019"
+
},
+
"christmas2019": {
+
"start": "12-05-2019",
+
"end": "01-05-2020"
+
},
+
"lucky2020": {
+
"start": "03-11-2020",
+
"end": "04-01-2020"
+
},
+
"easter2020": {
+
"start": "04-10-2020",
+
"end": "04-19-2020"
+
},
+
"grad2020": {
+
"start": "06-03-2020",
+
"end": "06-20-2020"
+
},
+
"summer2020": {
+
"start": "07-15-2020",
+
"end": "08-16-2020"
+
},
+
"space": {
+
"start": "08-16-2020",
+
"end": "09-04-2020"
+
},
+
"halloween2020": {
+
"start": "10-07-2020",
+
"end": "11-08-2020"
+
},
+
"christmas2020": {
+
"start": "12-02-2020",
+
"end": "01-01-2021"
+
},
+
"fools2021": {
+
"start": "03-30-2021",
+
"end": "04-02-2021"
+
}
+
}
+82 -81
constants/world.ts
···
-
import { PlayerCrumb, Room } from "../types.ts"
-
import { indexRoomData } from "../utils.ts";
-
export const rooms: Record<string, Record<string, Room>> = await indexRoomData()
export const spawnRoom = "tavern";
export const players: Record<string, PlayerCrumb> = {
-
"0": {
-
"i": "0",
-
"n": "Huggable",
-
"c": "huggable",
-
"x": 1670,
-
"y": 323,
-
"r": 180,
-
"g": [],
-
"m": "",
-
"e": "",
-
"_roomId": "crash_site"
-
}
-
}
-
export const queue: Array<string> = []
export const roomExits = {
-
"cellar->tavern": { x: 360, y: 410, r: 0 },
-
"crash_site->cellar": { x: 615, y: 400, r: 0 },
-
"shack->port": { x: 550, y: 235, r: 0 },
-
"jungle->port": { x: 650, y: 230, r: 0 },
-
"snowman_village->tavern": { x: 563, y: 368, r: 0 }
-
}
// deno-lint-ignore no-explicit-any
export const npcs: { [key: string]: any } = {
-
snowman_village: [
-
{
-
"i": "NPC0",
-
"n": "Snow Girl",
-
"c": "snowgirl",
-
"x": 1289,
-
"y": 228,
-
"r": 180,
-
"g": [],
-
"m": "",
-
"e": ""
-
},
-
{
-
"i": "NPC1",
-
"n": "Snow Patrol",
-
"c": "snow_patrol",
-
"x": 1644,
-
"y": 221,
-
"r": 180,
-
"g": [],
-
"m": "",
-
"e": ""
-
},
-
{
-
"i": "NPC2",
-
"n": "Snow Greeter",
-
"c": "snow_greeter",
-
"x": 443,
-
"y": 317,
-
"r": 180,
-
"g": [],
-
"m": "",
-
"e": ""
-
},
-
{
-
"i": "NPC3",
-
"n": "Snow Grandma",
-
"c": "snowgrandma",
-
"x": 1938,
-
"y": 251,
-
"r": 180,
-
"g": [],
-
"m": "",
-
"e": ""
-
},
-
{
-
"i": "NPC4",
-
"n": "Snow Keeper",
-
"c": "snowkeeper",
-
"x": 893,
-
"y": 216,
-
"r": 180,
-
"g": [],
-
"m": "",
-
"e": ""
-
}
-
]
-
}
···
+
import { PlayerCrumb, Room } from "../src/types.ts";
+
import { indexRoomData } from "../src/utils.ts";
+
export const rooms: Record<string, Record<string, Room>> =
+
await indexRoomData();
export const spawnRoom = "tavern";
export const players: Record<string, PlayerCrumb> = {
+
"0": {
+
"i": "0",
+
"n": "Huggable",
+
"c": "huggable",
+
"x": 1670,
+
"y": 323,
+
"r": 180,
+
"g": [],
+
"m": "",
+
"e": "",
+
"_roomId": "crash_site",
+
},
+
};
+
export const queue: Array<string> = [];
export const roomExits = {
+
"cellar->tavern": { x: 360, y: 410, r: 0 },
+
"crash_site->cellar": { x: 615, y: 400, r: 0 },
+
"shack->port": { x: 550, y: 235, r: 0 },
+
"jungle->port": { x: 650, y: 230, r: 0 },
+
"snowman_village->tavern": { x: 563, y: 368, r: 0 },
+
};
// deno-lint-ignore no-explicit-any
export const npcs: { [key: string]: any } = {
+
snowman_village: [
+
{
+
"i": "NPC0",
+
"n": "Snow Girl",
+
"c": "snowgirl",
+
"x": 1289,
+
"y": 228,
+
"r": 180,
+
"g": [],
+
"m": "",
+
"e": "",
+
},
+
{
+
"i": "NPC1",
+
"n": "Snow Patrol",
+
"c": "snow_patrol",
+
"x": 1644,
+
"y": 221,
+
"r": 180,
+
"g": [],
+
"m": "",
+
"e": "",
+
},
+
{
+
"i": "NPC2",
+
"n": "Snow Greeter",
+
"c": "snow_greeter",
+
"x": 443,
+
"y": 317,
+
"r": 180,
+
"g": [],
+
"m": "",
+
"e": "",
+
},
+
{
+
"i": "NPC3",
+
"n": "Snow Grandma",
+
"c": "snowgrandma",
+
"x": 1938,
+
"y": 251,
+
"r": 180,
+
"g": [],
+
"m": "",
+
"e": "",
+
},
+
{
+
"i": "NPC4",
+
"n": "Snow Keeper",
+
"c": "snowkeeper",
+
"x": 893,
+
"y": 216,
+
"r": 180,
+
"g": [],
+
"m": "",
+
"e": "",
+
},
+
],
+
};
+3 -2
deno.json
···
{
"imports": {
-
"hono": "jsr:@hono/hono@^4.6.11"
},
"tasks": {
-
"start": "deno run --watch --allow-net --allow-read --allow-env --env-file=.env --allow-write main.ts"
},
"compilerOptions": {
"jsx": "precompile",
···
{
"imports": {
+
"@std/fs": "jsr:@std/fs@^1.0.14"
},
"tasks": {
+
"start": "deno run --watch --allow-net --allow-read --allow-env --env-file=.env --allow-write src/main.ts",
+
"build": "deno compile --allow-net --allow-read --allow-env --env-file=.env.build --allow-write --output Localbox src/main.ts"
},
"compilerOptions": {
"jsx": "precompile",
+47 -131
deno.lock
···
{
"version": "4",
"specifiers": {
-
"jsr:@hono/hono@^4.6.11": "4.6.11",
-
"jsr:@std/encoding@*": "1.0.5",
"npm:fs@^0.0.1-security": "0.0.1-security",
"npm:http@^0.0.1-security": "0.0.1-security",
"npm:nodemon@^3.1.7": "3.1.9",
"npm:path@~0.12.7": "0.12.7",
"npm:socket.io@^4.8.1": "4.8.1",
"npm:zod@^3.24.1": "3.24.2"
},
"jsr": {
-
"@hono/hono@4.6.11": {
-
"integrity": "07399d911f09e94b7dc1e0e0a0577d35fe66578af20163d513958364c4e9e702"
},
-
"@std/encoding@1.0.5": {
-
"integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04"
}
},
"npm": {
···
"@types/node@22.12.0": {
"integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==",
"dependencies": [
-
"undici-types"
]
},
-
"@types/node@22.13.8": {
-
"integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==",
"dependencies": [
-
"undici-types"
]
},
"accepts@1.3.8": {
···
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
"dependencies": [
"@types/cors",
-
"@types/node@22.13.8",
"accepts",
"base64id",
"cookie",
···
},
"is-number@7.0.0": {
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
"mime-db@1.52.0": {
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
···
"undici-types@6.20.0": {
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
},
"util@0.10.4": {
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
"dependencies": [
···
"zod@3.24.2": {
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="
}
-
},
-
"redirects": {
-
"https://deno.land/std/path/mod.ts": "https://deno.land/std@0.224.0/path/mod.ts"
},
"remote": {
"https://deno.land/std@0.150.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
···
"https://deno.land/std@0.162.0/async/pool.ts": "ef9eb97b388543acbf0ac32647121e4dbe629236899586c4d4311a8770fbb239",
"https://deno.land/std@0.162.0/async/tee.ts": "9af3a3e7612af75861308b52249e167f5ebc3dcfc8a1a4d45462d96606ee2b70",
"https://deno.land/std@0.162.0/http/server.ts": "e99c1bee8a3f6571ee4cdeb2966efad465b8f6fe62bec1bdb59c1f007cc4d155",
-
"https://deno.land/std@0.177.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
-
"https://deno.land/std@0.177.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3",
-
"https://deno.land/std@0.177.0/bytes/index_of_needle.ts": "65c939607df609374c4415598fa4dad04a2f14c4d98cd15775216f0aaf597f24",
-
"https://deno.land/std@0.177.0/encoding/base64.ts": "7de04c2f8aeeb41453b09b186480be90f2ff357613b988e99fabb91d2eeceba1",
-
"https://deno.land/std@0.177.0/encoding/base64url.ts": "3f1178f6446834457b16bfde8b559c1cd3481727fe384d3385e4a9995dc2d851",
-
"https://deno.land/std@0.177.0/node/_core.ts": "9a58c0ef98ee77e9b8fcc405511d1b37a003a705eb6a9b6e95f75434d8009adc",
-
"https://deno.land/std@0.177.0/node/_utils.ts": "7fd55872a0cf9275e3c080a60e2fa6d45b8de9e956ebcde9053e72a344185884",
-
"https://deno.land/std@0.177.0/node/buffer.ts": "85617be2063eccaf177dbb84c7580d1e32023724ed14bd9df4e453b152a26167",
-
"https://deno.land/std@0.177.0/node/internal/buffer.mjs": "e92303a3cc6d9aaabcd270a937ad9319825d9ba08cb332650944df4562029b27",
-
"https://deno.land/std@0.177.0/node/internal/crypto/_keys.ts": "8f3c3b5a141aa0331a53c205e9338655f1b3b307a08085fd6ff6dda6f7c4190b",
-
"https://deno.land/std@0.177.0/node/internal/crypto/constants.ts": "544d605703053218499b08214f2e25cf4310651d535b7ab995891c4b7a217693",
-
"https://deno.land/std@0.177.0/node/internal/error_codes.ts": "8495e33f448a484518d76fa3d41d34fc20fe03c14b30130ad8e936b0035d4b8b",
-
"https://deno.land/std@0.177.0/node/internal/errors.ts": "1c699b8a3cb93174f697a348c004b1c6d576b66688eac8a48ebb78e65c720aae",
-
"https://deno.land/std@0.177.0/node/internal/hide_stack_frames.ts": "9dd1bad0a6e62a1042ce3a51eb1b1ecee2f246907bff44835f86e8f021de679a",
-
"https://deno.land/std@0.177.0/node/internal/normalize_encoding.mjs": "fd1d9df61c44d7196432f6e8244621468715131d18cc79cd299fc78ac549f707",
-
"https://deno.land/std@0.177.0/node/internal/primordials.mjs": "a72d86b5aa55d3d50b8e916b6a59b7cc0dc5a31da8937114b4a113ad5aa08c74",
-
"https://deno.land/std@0.177.0/node/internal/util.mjs": "f7fe2e1ca5e66f550ad0856b9f5ee4d666f0c071fe212ea7fc7f37cfa81f97a5",
-
"https://deno.land/std@0.177.0/node/internal/util/inspect.mjs": "11d7c9cab514b8e485acc3978c74b837263ff9c08ae4537fa18ad56bae633259",
-
"https://deno.land/std@0.177.0/node/internal/util/types.ts": "0e587b44ec5e017cf228589fc5ce9983b75beece6c39409c34170cfad49d6417",
-
"https://deno.land/std@0.177.0/node/internal/validators.mjs": "e02f2b02dd072a5d623970292588d541204dc82207b4c58985d933a5f4b382e6",
-
"https://deno.land/std@0.177.0/node/internal_binding/_libuv_winerror.ts": "30c9569603d4b97a1f1a034d88a3f74800d5ea1f12fcc3d225c9899d4e1a518b",
-
"https://deno.land/std@0.177.0/node/internal_binding/_node.ts": "cb2389b0eab121df99853eb6a5e3a684e4537e065fb8bf2cca0cbf219ce4e32e",
-
"https://deno.land/std@0.177.0/node/internal_binding/_utils.ts": "7c58a2fbb031a204dee9583ba211cf9c67922112fe77e7f0b3226112469e9fe1",
-
"https://deno.land/std@0.177.0/node/internal_binding/_winerror.ts": "3e8cfdfe22e89f13d2b28529bab35155e6b1730c0221ec5a6fc7077dc037be13",
-
"https://deno.land/std@0.177.0/node/internal_binding/buffer.ts": "31729e0537921d6c730ad0afea44a7e8a0a1044d070ade8368226cb6f7390c8b",
-
"https://deno.land/std@0.177.0/node/internal_binding/constants.ts": "21ff9d1ee71d0a2086541083a7711842fc6ae25e264dbf45c73815aadce06f4c",
-
"https://deno.land/std@0.177.0/node/internal_binding/string_decoder.ts": "54c3c1cbd5a9254881be58bf22637965dc69535483014dab60487e299cb95445",
-
"https://deno.land/std@0.177.0/node/internal_binding/types.ts": "2187595a58d2cf0134f4db6cc2a12bf777f452f52b15b6c3aed73fa072aa5fc3",
-
"https://deno.land/std@0.177.0/node/internal_binding/util.ts": "808ff3b92740284184ab824adfc420e75398c88c8bccf5111f0c24ac18c48f10",
-
"https://deno.land/std@0.177.0/node/internal_binding/uv.ts": "eb0048e30af4db407fb3f95563e30d70efd6187051c033713b0a5b768593a3a3",
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
"https://deno.land/std@0.224.0/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8",
"https://deno.land/std@0.224.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2",
"https://deno.land/std@0.224.0/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c",
···
"https://deno.land/std@0.224.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972",
"https://deno.land/std@0.224.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e",
"https://deno.land/std@0.224.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c",
-
"https://deno.land/std@0.94.0/node/tty.ts": "9fa7f7b461759774b4eeab00334ac5d25b69bf0de003c02814be01e65150da79",
-
"https://deno.land/x/chalk_deno@v4.1.1-deno/source/ansi-styles/index.js": "7cc96ab93d1c9cfc0746e9dffb40be872e42ee242906f48e68df0d2c9669f737",
-
"https://deno.land/x/chalk_deno@v4.1.1-deno/source/has-flag/index.js": "aed21e4eba656057e7b8c6024305f5354d2ebee2adc857a1d8cd5207923de7e5",
-
"https://deno.land/x/chalk_deno@v4.1.1-deno/source/index.js": "6339123f32f7eb4b17c5c9c926ecdf3dbc353fd4fda7811ad2d3c1d4b98a7420",
-
"https://deno.land/x/chalk_deno@v4.1.1-deno/source/supports-color/index.js": "4d7f2d216b6ac9013d9ec7e004de21f5a7d00bf2be4075bab2d82638d0d41a86",
-
"https://deno.land/x/chalk_deno@v4.1.1-deno/source/templates.js": "f2e12be18cb84710e341e5499528280278052909fa74a12cefc9e2cc26a597ac",
-
"https://deno.land/x/chalk_deno@v4.1.1-deno/source/util.js": "cd08297ec411dcee91826ad01a00d3427235d4548ba605a59e64f0da83af8306",
-
"https://deno.land/x/hono@v3.0.0/adapter/deno/serve-static.ts": "d1c21498ced39849fa0bb23b372bf5d30677916fdbc875902735700ca1e789e3",
-
"https://deno.land/x/hono@v3.0.0/client/client.ts": "c720020a167139dfe8d6af7b91c0c5db38186722b380fb0bb251bae685a2103e",
-
"https://deno.land/x/hono@v3.0.0/client/index.ts": "7ad089b121f2613a0eaedd3d8aa8307a48bf3bf48f6c38281d6e73ee35f0d7ea",
-
"https://deno.land/x/hono@v3.0.0/client/types.ts": "47b83ab3dea4d6ef60a7c5ed11bfdb5113df7ded05403869e029f5317e44c827",
-
"https://deno.land/x/hono@v3.0.0/client/utils.ts": "781ec703f3e685cf02a201cfa717be111820ef89bebd7cb5968c6823310aebaa",
-
"https://deno.land/x/hono@v3.0.0/compose.ts": "9bc97f737857da061b2294eb9812b7e0ca773acc2e913c11bf93d5732c0469f0",
-
"https://deno.land/x/hono@v3.0.0/context.ts": "b053da7ec49c7a32f10572f951ae68347260ffab62fcce76cbc2ce544fe44d23",
-
"https://deno.land/x/hono@v3.0.0/hono.ts": "4929cefc738a4bd5efea2371f0c5338d27e9de91045b1c223ab98eda5a0364d5",
-
"https://deno.land/x/hono@v3.0.0/http-exception.ts": "e74a5e8504ecf795e9babdc2bab9d4052e1806a7396efc89f8e1ed83e813e3be",
-
"https://deno.land/x/hono@v3.0.0/middleware.ts": "d2c886e2f63b91b17f05a11f598ca101912c8fbfd66a25aef345b934ccfcd603",
-
"https://deno.land/x/hono@v3.0.0/middleware/basic-auth/index.ts": "0664ddf00c9f08a5109f92e93264678de55e90c341955c746bafa8f99ecab1eb",
-
"https://deno.land/x/hono@v3.0.0/middleware/bearer-auth/index.ts": "11d4ead9b57f5bcb2b6b4bf27076871f15da0e1e8828b2b79d90c15423357b47",
-
"https://deno.land/x/hono@v3.0.0/middleware/cache/index.ts": "2e86e089ebce01611ab5e231a6fbf7953ebabee3ab68362fcacea752ef48a0ab",
-
"https://deno.land/x/hono@v3.0.0/middleware/compress/index.ts": "0b8ddbd70688361d5ef4e9418afa28affe2500a7f41a231a87873dad26ef5548",
-
"https://deno.land/x/hono@v3.0.0/middleware/cors/index.ts": "10a743dcc793204835a5299070e2afaabb7e81cb742262faa9f1181f5e5d65ac",
-
"https://deno.land/x/hono@v3.0.0/middleware/etag/index.ts": "e679c30ddd2600087521a7c1dac3798c2319c09d203bb627b54357b984fc72e7",
-
"https://deno.land/x/hono@v3.0.0/middleware/html/index.ts": "a5028d8170dcc030d003749e743213e6532ff65798b741b81220207abc9af82d",
-
"https://deno.land/x/hono@v3.0.0/middleware/jsx/index.ts": "1925e4bf01ef1252b9bda6b0c4f79520b138ee319c41df3218cb3bf10c0ed248",
-
"https://deno.land/x/hono@v3.0.0/middleware/jwt/index.ts": "4af4649d9ae8ff2e767e53692d1974c956f523ab47a2560b869083cb493441ca",
-
"https://deno.land/x/hono@v3.0.0/middleware/logger/index.ts": "281b0fe431183a5d7b8d576645370efbd2737aeefaac7dc989d1c90dc03c52c0",
-
"https://deno.land/x/hono@v3.0.0/middleware/powered-by/index.ts": "7ec561885ac0410786f78aeb9789ed7869edb2d43c615cbc3d14f21baee87359",
-
"https://deno.land/x/hono@v3.0.0/middleware/pretty-json/index.ts": "f4a4b2fa2ecb73e23da6f0ef716fe7d6a7f05c69f64dc7f89fc68cbcb204a87f",
-
"https://deno.land/x/hono@v3.0.0/mod.ts": "e771d1c9f711b78f7540134e44a1ceda308fe0af58cb4c83ce7434c56c224670",
-
"https://deno.land/x/hono@v3.0.0/request.ts": "e3b38e76f7d13596266e644c8598d324c56ae73af263c3bf029f6ffeafdb221b",
-
"https://deno.land/x/hono@v3.0.0/router.ts": "21448bc2e6019574c10fae11237da4367037fa107e68bf3d049cd2fd0efd2adb",
-
"https://deno.land/x/hono@v3.0.0/router/reg-exp-router/index.ts": "52755829213941756159b7a963097bafde5cc4fc22b13b1c7c9184dc0512d1db",
-
"https://deno.land/x/hono@v3.0.0/router/reg-exp-router/node.ts": "8006b5bccb83d9fc98e0562a5545f6dd0be639ce445b089a6171c9c617aa8693",
-
"https://deno.land/x/hono@v3.0.0/router/reg-exp-router/router.ts": "35a405c855cf6d10c350a651169c2c728fd19aaf3d3a0c416e5a06752914e6bd",
-
"https://deno.land/x/hono@v3.0.0/router/reg-exp-router/trie.ts": "567493b301c44174f0895aedb8d055bbecf88f8a25626fa8ca61333bbd0c882c",
-
"https://deno.land/x/hono@v3.0.0/router/smart-router/index.ts": "74f9b4fe15ea535900f2b9b048581915f12fe94e531dd2b0032f5610e61c3bef",
-
"https://deno.land/x/hono@v3.0.0/router/smart-router/router.ts": "1d54f5c87875d856ed5fc2d22a100e1ff31debe3e9d8e9b1cc18d8e5706239f2",
-
"https://deno.land/x/hono@v3.0.0/router/trie-router/index.ts": "3eb75e7f71ba81801631b30de6b1f5cefb2c7239c03797e2b2cbab5085911b41",
-
"https://deno.land/x/hono@v3.0.0/router/trie-router/node.ts": "ca5b6a1ce6b6dc01003809bfa9cb93a323b37b27feee14c64655015deff7d2a9",
-
"https://deno.land/x/hono@v3.0.0/router/trie-router/router.ts": "0a969528a0c1680b552b20f0ca90e484e968ac279be9d5fd952b61a804d680e7",
-
"https://deno.land/x/hono@v3.0.0/types.ts": "a96ee9693b0de61b42016b80d3cbd958fabb795d8e4becb455845d70f66e7c87",
-
"https://deno.land/x/hono@v3.0.0/utils/body.ts": "b6b5ed679122968a74845df4c5454c677f09adc4f3466d822f3b1397884e540e",
-
"https://deno.land/x/hono@v3.0.0/utils/buffer.ts": "d28ab08d2571e890ec2ad7ce4c0318a503094f8403eac3d5eb18a8e5c23b29b2",
-
"https://deno.land/x/hono@v3.0.0/utils/cookie.ts": "545872bd7af3b455c24fd386ecbccfd161e7d4a0038d6b09b1bb22723602f90a",
-
"https://deno.land/x/hono@v3.0.0/utils/crypto.ts": "bda0e141bbe46d3a4a20f8fbcb6380d473b617123d9fdfa93e4499410b537acc",
-
"https://deno.land/x/hono@v3.0.0/utils/encode.ts": "b628be2de7ab48cc806b1e5b93b3baf6886b7656d6d65dfa80f4fcd9c0f63a5f",
-
"https://deno.land/x/hono@v3.0.0/utils/filepath.ts": "5f708bb6a2f0b8e83a3333868ac86abdfba15e52b46acb5e13018fa2138f0826",
-
"https://deno.land/x/hono@v3.0.0/utils/html.ts": "636c4a04eaea1c52c14a37cd28d83cba66b293d1e31420a4475e951268901ae4",
-
"https://deno.land/x/hono@v3.0.0/utils/http-status.ts": "2d6003e352c1fe918db663fa4bd2b20bf0b9d4e1699ba5e163f317f00b29d938",
-
"https://deno.land/x/hono@v3.0.0/utils/jwt/index.ts": "5e4b82a42eb3603351dfce726cd781ca41cb57437395409d227131aec348d2d5",
-
"https://deno.land/x/hono@v3.0.0/utils/jwt/jwt.ts": "4fefadfb914ad88282949d0cce2be16bdbd0b0614d15ecbb340c4fc47a9929b9",
-
"https://deno.land/x/hono@v3.0.0/utils/jwt/types.ts": "715514fe59f0c37048b2940528bd4f4ec5795e04f0bc1d6d7e33f8d8bb1fe9de",
-
"https://deno.land/x/hono@v3.0.0/utils/mime.ts": "e17bdbac85b97c3d223c48874c2abe867e0720461e9f7e0c340141c080b9c6d6",
-
"https://deno.land/x/hono@v3.0.0/utils/types.ts": "173dedfe018b447cc6b067d2b6968c1f1dccba67ad50526d356b79e0465a5753",
-
"https://deno.land/x/hono@v3.0.0/utils/url.ts": "2cf0f38d976761296ccaf2622b6999b7c0c1d3a1f63f066a6f832288ea7c28d9",
-
"https://deno.land/x/hono@v3.0.0/validator/index.ts": "3dc2c9418dee74333ea8b98642ce112b6d449042bbcefe1e835047fcf2458170",
-
"https://deno.land/x/hono@v3.0.0/validator/validator.ts": "9b2c9983b6b8a31cbc1f687096e232fb159004c15421e39cb050da2ac0f78c95",
-
"https://deno.land/x/hono@v4.3.11/adapter/deno/serve-static.ts": "db226d30f08f1a8bb77653ead42a911357b2f8710d653e43c01eccebb424b295",
-
"https://deno.land/x/hono@v4.3.11/compose.ts": "37d6e33b7db80e4c43a0629b12fd3a1e3406e7d9e62a4bfad4b30426ea7ae4f1",
-
"https://deno.land/x/hono@v4.3.11/context.ts": "facfd749d823a645039571d66d9d228f5ae6836818b65d3b6c4c6891adfe071e",
-
"https://deno.land/x/hono@v4.3.11/hono-base.ts": "fd7e9c1bba1e13119e95158270011784da3a7c3014c149ba0700e700f840ae0d",
-
"https://deno.land/x/hono@v4.3.11/hono.ts": "23edd0140bf0bd5a68c14ae96e5856a5cec6b844277e853b91025e91ea74f416",
-
"https://deno.land/x/hono@v4.3.11/http-exception.ts": "f5dd375e61aa4b764eb9b99dd45a7160f8317fd36d3f79ae22585b9a5e8ad7c5",
-
"https://deno.land/x/hono@v4.3.11/middleware/serve-static/index.ts": "14b760bbbc4478cc3a7fb9728730bc6300581c890365b7101b80c16e70e4b21e",
-
"https://deno.land/x/hono@v4.3.11/request.ts": "7b08602858e642d1626c3106c0bedc2aa8d97e30691a079351d9acef7c5955e6",
-
"https://deno.land/x/hono@v4.3.11/router.ts": "880316f561918fc197481755aac2165fdbe2f530b925c5357a9f98d6e2cc85c7",
-
"https://deno.land/x/hono@v4.3.11/router/reg-exp-router/index.ts": "52755829213941756159b7a963097bafde5cc4fc22b13b1c7c9184dc0512d1db",
-
"https://deno.land/x/hono@v4.3.11/router/reg-exp-router/node.ts": "7efaa6f4301efc2aad0519c84973061be8555da02e5868409293a1fd98536aaf",
-
"https://deno.land/x/hono@v4.3.11/router/reg-exp-router/router.ts": "632f2fa426b3e45a66aeed03f7205dad6d13e8081bed6f8d1d987b6cad8fb455",
-
"https://deno.land/x/hono@v4.3.11/router/reg-exp-router/trie.ts": "852ce7207e6701e47fa30889a0d2b8bfcd56d8862c97e7bc9831e0a64bd8835f",
-
"https://deno.land/x/hono@v4.3.11/router/smart-router/index.ts": "74f9b4fe15ea535900f2b9b048581915f12fe94e531dd2b0032f5610e61c3bef",
-
"https://deno.land/x/hono@v4.3.11/router/smart-router/router.ts": "dc22a8505a0f345476f07dca3054c0c50a64d7b81c9af5a904476490dfd5cbb4",
-
"https://deno.land/x/hono@v4.3.11/router/trie-router/index.ts": "3eb75e7f71ba81801631b30de6b1f5cefb2c7239c03797e2b2cbab5085911b41",
-
"https://deno.land/x/hono@v4.3.11/router/trie-router/node.ts": "d3e00e8f1ba7fb26896459d5bba882356891a07793387c4655d1864c519a91de",
-
"https://deno.land/x/hono@v4.3.11/router/trie-router/router.ts": "54ced78d35676302c8fcdda4204f7bdf5a7cc907fbf9967c75674b1e394f830d",
-
"https://deno.land/x/hono@v4.3.11/types.ts": "b561c3ee846121b33c2d81331246cdedf7781636ed72dad7406677105b4275de",
-
"https://deno.land/x/hono@v4.3.11/utils/body.ts": "774cb319dfbe886a9d39f12c43dea15a39f9d01e45de0323167cdd5d0aad14d4",
-
"https://deno.land/x/hono@v4.3.11/utils/filepath.ts": "a83e5fe87396bb291a6c5c28e13356fcbea0b5547bad2c3ba9660100ff964000",
-
"https://deno.land/x/hono@v4.3.11/utils/html.ts": "6ea4f6bf41587a51607dff7a6d2865ef4d5001e4203b07e5c8a45b63a098e871",
-
"https://deno.land/x/hono@v4.3.11/utils/http-status.ts": "f5b820f2793e45209f34deddf147b23e3133a89eb4c57dc643759a504706636b",
-
"https://deno.land/x/hono@v4.3.11/utils/mime.ts": "d1fc2c047191ccb01d736c6acf90df731324536298181dba0ecc2259e5f7d661",
-
"https://deno.land/x/hono@v4.3.11/utils/types.ts": "050bfa9dc6d0cc4b7c5069944a8bd60066c2f9f95ee69833623ad104f11f92bf",
-
"https://deno.land/x/hono@v4.3.11/utils/url.ts": "855169632c61d03703bd08cafb27664ba3fdb352892f01687d5cce8fd49e3cb1",
"https://deno.land/x/imagescript@1.3.0/ImageScript.js": "cf90773c966031edd781ed176c598f7ed495e7694cd9b86c986d2d97f783cca0",
"https://deno.land/x/imagescript@1.3.0/mod.ts": "18a6cb83c55e690c873505f6fe867364c678afb64934fe7aef593a6b92f79995",
"https://deno.land/x/imagescript@1.3.0/png/src/crc.mjs": "5cf50de181d61dd00e66a240d811018ba5070afa8bba302f393604404604de84",
···
"https://deno.land/x/socket_io@0.2.0/packages/socket.io/mod.ts": "dfd465bdcf23161af0c4d79fb8fc8912418c46a20d15e8b314cec6d9fb508196",
"https://deno.land/x/socket_io@0.2.0/test_deps.ts": "1f9dfa07a1e806ccddc9fa5f7255338d9dff67c40d7e83795f4f0f7bd710bde9",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/backoff.ts": "33e4a6e245f8743fbae0ce583993a671a3ac2ecee433a3e7f0bd77b5dd541d84",
-
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/command.ts": "802df3a1f49f6c49fe3e8fcf13fd0cc360b8a02369de0310a72d7f0c8e4ceaab",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/connection.ts": "c31d2e0cb360bc641e7286f1d53cf58790fbcda025c06887f84a821f39d0fdff",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/errors.ts": "bc8f7091cb9f36cdd31229660e0139350b02c26851e3ac69d592c066745feb27",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/executor.ts": "03e5f43df4e0c9c62b0e1be778811d45b6a1966ddf406e21ed5a227af70b7183",
···
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/protocol/command.ts": "b1efd3b62fe5d1230e6d96b5c65ba7de1592a1eda2cc927161e5997a15f404ac",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/protocol/mod.ts": "f2601df31d8adc71785b5d19f6a7e43dfce94adbb6735c4dafc1fb129169d11a",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/protocol/reply.ts": "beac2061b03190bada179aef1a5d92b47a5104d9835e8c7468a55c24812ae9e4",
-
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/protocol/types.ts": "40b0a568cb7fd4dc9107997062584d24e5c6ffa1f21acb6410aa19c92f89e9e1",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/pubsub.ts": "324b87dae0700e4cb350780ce3ae5bc02780f79f3de35e01366b894668b016c6",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/redis.ts": "a5c2cf8c72e7c92c9c8c6911f98227062649f6cba966938428c5414200f3aa54",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/stream.ts": "f116d73cfe04590ff9fa8a3c08be8ff85219d902ef2f6929b8c1e88d92a07810",
···
},
"workspace": {
"dependencies": [
-
"jsr:@hono/hono@^4.6.11"
],
"packageJson": {
"dependencies": [
"npm:fs@^0.0.1-security",
"npm:http@^0.0.1-security",
"npm:nodemon@^3.1.7",
"npm:path@~0.12.7",
"npm:socket.io@^4.8.1",
···
{
"version": "4",
"specifiers": {
+
"jsr:@std/cli@*": "1.0.15",
+
"jsr:@std/fs@*": "1.0.15",
+
"jsr:@std/fs@^1.0.14": "1.0.15",
+
"jsr:@std/path@^1.0.8": "1.0.8",
"npm:fs@^0.0.1-security": "0.0.1-security",
"npm:http@^0.0.1-security": "0.0.1-security",
+
"npm:jose@5.9.6": "5.9.6",
"npm:nodemon@^3.1.7": "3.1.9",
"npm:path@~0.12.7": "0.12.7",
"npm:socket.io@^4.8.1": "4.8.1",
"npm:zod@^3.24.1": "3.24.2"
},
"jsr": {
+
"@std/cli@1.0.15": {
+
"integrity": "e79ba3272ec710ca44d8342a7688e6288b0b88802703f3264184b52893d5e93f"
+
},
+
"@std/fs@1.0.15": {
+
"integrity": "c083fb479889d6440d768e498195c3fc499d426fbf9a6592f98f53884d1d3f41",
+
"dependencies": [
+
"jsr:@std/path"
+
]
},
+
"@std/path@1.0.8": {
+
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
}
},
"npm": {
···
"@types/node@22.12.0": {
"integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==",
"dependencies": [
+
"undici-types@6.20.0"
]
},
+
"@types/node@22.14.0": {
+
"integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==",
"dependencies": [
+
"undici-types@6.21.0"
]
},
"accepts@1.3.8": {
···
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
"dependencies": [
"@types/cors",
+
"@types/node@22.14.0",
"accepts",
"base64id",
"cookie",
···
},
"is-number@7.0.0": {
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
+
},
+
"jose@5.9.6": {
+
"integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="
},
"mime-db@1.52.0": {
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
···
"undici-types@6.20.0": {
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
},
+
"undici-types@6.21.0": {
+
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
+
},
"util@0.10.4": {
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
"dependencies": [
···
"zod@3.24.2": {
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="
}
},
"remote": {
"https://deno.land/std@0.150.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
···
"https://deno.land/std@0.162.0/async/pool.ts": "ef9eb97b388543acbf0ac32647121e4dbe629236899586c4d4311a8770fbb239",
"https://deno.land/std@0.162.0/async/tee.ts": "9af3a3e7612af75861308b52249e167f5ebc3dcfc8a1a4d45462d96606ee2b70",
"https://deno.land/std@0.162.0/http/server.ts": "e99c1bee8a3f6571ee4cdeb2966efad465b8f6fe62bec1bdb59c1f007cc4d155",
+
"https://deno.land/std@0.212.0/path/_common/assert_path.ts": "2ca275f36ac1788b2acb60fb2b79cb06027198bc2ba6fb7e163efaedde98c297",
+
"https://deno.land/std@0.212.0/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c",
+
"https://deno.land/std@0.212.0/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15",
+
"https://deno.land/std@0.212.0/path/extname.ts": "593303db8ae8c865cbd9ceec6e55d4b9ac5410c1e276bfd3131916591b954441",
+
"https://deno.land/std@0.212.0/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d",
+
"https://deno.land/std@0.212.0/path/posix/extname.ts": "8d36ae0082063c5e1191639699e6f77d3acf501600a3d87b74943f0ae5327427",
+
"https://deno.land/std@0.212.0/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808",
+
"https://deno.land/std@0.212.0/path/windows/extname.ts": "165a61b00d781257fda1e9606a48c78b06815385e7d703232548dbfc95346bef",
"https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
"https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
+
"https://deno.land/std@0.224.0/media_types/_db.ts": "19563a2491cd81b53b9c1c6ffd1a9145c355042d4a854c52f6e1424f73ff3923",
+
"https://deno.land/std@0.224.0/media_types/_util.ts": "e0b8da0c7d8ad2015cf27ac16ddf0809ac984b2f3ec79f7fa4206659d4f10deb",
+
"https://deno.land/std@0.224.0/media_types/content_type.ts": "ed3f2e1f243b418ad3f441edc95fd92efbadb0f9bde36219c7564c67f9639513",
+
"https://deno.land/std@0.224.0/media_types/extension.ts": "ec91e1818864cb84f8053ecafb270eaca702412c15c2086929ae34132e11c56a",
+
"https://deno.land/std@0.224.0/media_types/extensions_by_type.ts": "9db10797e09421815688c8f7a2fbfd5dcb040fa5c488278f1b9e04359369bd0b",
+
"https://deno.land/std@0.224.0/media_types/format_media_type.ts": "ffef4718afa2489530cb94021bb865a466eb02037609f7e82899c017959d288a",
+
"https://deno.land/std@0.224.0/media_types/get_charset.ts": "277ebfceb205bd34e616fe6764ef03fb277b77f040706272bea8680806ae3f11",
+
"https://deno.land/std@0.224.0/media_types/mod.ts": "c8acfa43ce3993e99f4d8aa60fb828a4eee3ab6920aaeb90f6a3d63f6f4f3435",
+
"https://deno.land/std@0.224.0/media_types/parse_media_type.ts": "487f000a38c230ccbac25420a50f600862e06796d0eee19d19631b9e84ee9654",
+
"https://deno.land/std@0.224.0/media_types/type_by_extension.ts": "bf4e3f5d6b58b624d5daa01cbb8b1e86d9939940a77e7c26e796a075b60ec73b",
+
"https://deno.land/std@0.224.0/media_types/vendor/mime-db.v1.52.0.ts": "0218d2c7d900e8cd6fa4a866e0c387712af4af9a1bae55d6b2546c73d273a1e6",
"https://deno.land/std@0.224.0/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8",
"https://deno.land/std@0.224.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2",
"https://deno.land/std@0.224.0/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c",
···
"https://deno.land/std@0.224.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972",
"https://deno.land/std@0.224.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e",
"https://deno.land/std@0.224.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c",
"https://deno.land/x/imagescript@1.3.0/ImageScript.js": "cf90773c966031edd781ed176c598f7ed495e7694cd9b86c986d2d97f783cca0",
"https://deno.land/x/imagescript@1.3.0/mod.ts": "18a6cb83c55e690c873505f6fe867364c678afb64934fe7aef593a6b92f79995",
"https://deno.land/x/imagescript@1.3.0/png/src/crc.mjs": "5cf50de181d61dd00e66a240d811018ba5070afa8bba302f393604404604de84",
···
"https://deno.land/x/socket_io@0.2.0/packages/socket.io/mod.ts": "dfd465bdcf23161af0c4d79fb8fc8912418c46a20d15e8b314cec6d9fb508196",
"https://deno.land/x/socket_io@0.2.0/test_deps.ts": "1f9dfa07a1e806ccddc9fa5f7255338d9dff67c40d7e83795f4f0f7bd710bde9",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/backoff.ts": "33e4a6e245f8743fbae0ce583993a671a3ac2ecee433a3e7f0bd77b5dd541d84",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/connection.ts": "c31d2e0cb360bc641e7286f1d53cf58790fbcda025c06887f84a821f39d0fdff",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/errors.ts": "bc8f7091cb9f36cdd31229660e0139350b02c26851e3ac69d592c066745feb27",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/executor.ts": "03e5f43df4e0c9c62b0e1be778811d45b6a1966ddf406e21ed5a227af70b7183",
···
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/protocol/command.ts": "b1efd3b62fe5d1230e6d96b5c65ba7de1592a1eda2cc927161e5997a15f404ac",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/protocol/mod.ts": "f2601df31d8adc71785b5d19f6a7e43dfce94adbb6735c4dafc1fb129169d11a",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/protocol/reply.ts": "beac2061b03190bada179aef1a5d92b47a5104d9835e8c7468a55c24812ae9e4",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/pubsub.ts": "324b87dae0700e4cb350780ce3ae5bc02780f79f3de35e01366b894668b016c6",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/redis.ts": "a5c2cf8c72e7c92c9c8c6911f98227062649f6cba966938428c5414200f3aa54",
"https://deno.land/x/socket_io@0.2.0/vendor/deno.land/x/redis@v0.27.1/stream.ts": "f116d73cfe04590ff9fa8a3c08be8ff85219d902ef2f6929b8c1e88d92a07810",
···
},
"workspace": {
"dependencies": [
+
"jsr:@std/fs@^1.0.14"
],
"packageJson": {
"dependencies": [
"npm:fs@^0.0.1-security",
"npm:http@^0.0.1-security",
+
"npm:jose@5.9.6",
"npm:nodemon@^3.1.7",
"npm:path@~0.12.7",
"npm:socket.io@^4.8.1",
+201 -130
io.ts src/io.ts
···
// deno-lint-ignore-file no-explicit-any
import { Server } from "https://deno.land/x/socket_io@0.2.0/mod.ts";
-
import { decode } from 'hono/jwt';
-
import chalk from "https://deno.land/x/chalk_deno@v4.1.1-deno/source/index.js";
import { z } from "zod";
-
import * as world from "./constants/world.ts";
-
import * as items from "./constants/items.ts";
-
import * as utils from "./utils.ts";
-
import { LocalPlayer, PlayerCrumb, ShopData, CritterId } from "./types.ts";
-
import parties from "./constants/parties.json" with { type: "json" };
-
import itemsJSON from "./public/base/items.json" with { type: "json" };
export const io = new Server();
io.on("connection", (socket) => {
···
/** Condensed player data that is sufficient enough for other clients */
let localCrumb: PlayerCrumb;
-
// TODO: implement checking PlayFab API with ticket
socket.once("login", async (ticket: string) => {
-
if (z.object({
-
ticket: z.string()
-
}).safeParse({ ticket: ticket }).success == false) return;
let playerData;
try {
-
playerData = decode(ticket);
-
} catch(_e) {
socket.disconnect(true);
-
return
}
-
// TODO: make this just an inline function instead of having an onPropertyChange function, I'm just really lazy right now lol -index
function onPropertyChange(property: string, value: any) {
utils.updateAccount(localPlayer.nickname, property, value);
···
const createArrayHandler = (propertyName: string) => ({
get(target: any, property: string) {
-
if (typeof target[property] === 'function') {
return function (...args: any[]) {
const result = target[property].apply(target, args);
onPropertyChange(propertyName, target);
···
};
}
return target[property];
-
}
});
const handler = {
···
return new Proxy(target[property], createArrayHandler(property));
}
return target[property];
-
}
};
-
const payload = playerData.payload;
-
const sub = payload.sub as {
-
playerId: string,
-
nickname: string,
-
critterId: CritterId,
-
partyId: string,
-
persistent: boolean,
-
mods: Array<string>
};
const persistentAccount = await utils.getAccount(sub.nickname);
if (!sub.persistent || persistentAccount.individual == null) {
localPlayer = {
···
mutes: [],
_partyId: sub.partyId, // This key is replaced down the line anyway
-
_mods: []
};
if (sub.persistent) {
utils.createAccount(localPlayer);
-
localPlayer = new Proxy<LocalPlayer>(utils.expandAccount(localPlayer), handler);
-
};
} else {
persistentAccount.individual.critterId = sub.critterId || "hamster";
persistentAccount.individual._partyId = sub.partyId || "default";
persistentAccount.individual._mods = sub.mods || [];
-
localPlayer = new Proxy<LocalPlayer>(utils.expandAccount(persistentAccount.individual), handler);
}
-
localPlayer._partyId = socket.handshake.query.get('partyId') || 'default';
world.queue.splice(world.queue.indexOf(localPlayer.nickname), 1);
localCrumb = utils.makeCrumb(localPlayer, world.spawnRoom);
···
});
socket.on("joinRoom", (roomId: string) => {
-
if (z.object({
-
roomId: z.enum(Object.keys(world.rooms) as any)
-
}).safeParse({ roomId: roomId }).success == false) return;
const _room = (world.rooms[roomId] || { default: null }).default;
if (!_room) return;
···
socket.leave(localCrumb._roomId);
socket.broadcast.in(localCrumb._roomId).emit("R", localCrumb);
-
const modEnabled = (localPlayer._mods || []).includes('roomExits');
//@ts-ignore: Index type is correct
-
const correctExit = world.roomExits[localCrumb._roomId + "->" + roomId]
if (modEnabled && correctExit) {
localPlayer.x = correctExit.x;
localPlayer.y = correctExit.y;
localPlayer.rotation = correctExit.r;
-
};
-
if (!modEnabled || !correctExit) {
localPlayer.x = _room.startX;
localPlayer.y = _room.startY;
localPlayer.rotation = _room.startR | 180;
-
};
localCrumb = utils.makeCrumb(localPlayer, roomId);
world.players[localPlayer.playerId] = localCrumb;
-
console.log(chalk.green('> ' + localPlayer.nickname + ' joined "' + roomId + '"!'));
socket.join(roomId);
-
let playerCrumbs = Object.values(world.players).filter((crumb) => crumb._roomId == roomId);
if (world.npcs[roomId]) {
playerCrumbs = [
...playerCrumbs,
-
...world.npcs[roomId]
];
-
};
socket.emit("joinRoom", {
name: _room.name,
roomId: roomId,
-
playerCrumbs: playerCrumbs
});
socket.broadcast.in(localCrumb._roomId).emit("A", localCrumb);
});
socket.on("moveTo", (x: number, y: number) => {
-
const roomData = world.rooms[localCrumb._roomId][localPlayer._partyId];
-
if (z.object({
-
x: z.number().min(0).max(roomData.width),
-
y: z.number().min(0).max(roomData.height)
-
}).safeParse({ x: x, y: y }).success == false) return;
const newDirection = utils.getDirection(localPlayer.x, localPlayer.y, x, y);
···
i: localPlayer.playerId,
x: x,
y: y,
-
r: newDirection
});
});
socket.on("message", (text: string) => {
-
if (z.object({
-
text: z.string().nonempty()
-
}).safeParse({ text: text }).success == false) return;
-
console.log(chalk.gray(`> ${localPlayer.nickname} sent message: "%s"`), text);
localCrumb.m = text;
socket.broadcast.in(localCrumb._roomId).emit("M", {
i: localPlayer.playerId,
-
m: text
});
setTimeout(() => {
···
});
socket.on("emote", (emote: string) => {
-
if (z.object({
-
emote: z.string().nonempty() // TODO: make this an enum
-
}).safeParse({ emote: emote }).success == false) return;
-
console.log(chalk.gray(`> ${localPlayer.nickname} sent emote: %s`), emote);
localCrumb.e = emote;
socket.broadcast.in(localCrumb._roomId).emit("E", {
i: localPlayer.playerId,
-
e: emote
});
setTimeout(() => {
···
// ? Options is specified just because sometimes it is sent, but its always an empty string
socket.on("code", (code: string, _options?: string) => {
-
if (z.object({
-
command: z.enum([
-
"pop",
-
"freeitem",
-
"tbt",
-
"darkmode",
-
"spydar",
-
"allitems"
-
])
-
}).safeParse({
-
command: code
-
}).success == false) return;
-
console.log(chalk.gray(`> ${localPlayer.nickname} sent code: %s`), code);
-
const addItem = function(id: string, showGUI: boolean = false) {
if (!localPlayer.inventory.includes(id)) {
socket.emit("addItem", { itemId: id, showGUI: showGUI });
localPlayer.inventory.push(id);
}
-
}
-
// Misc. Codes
-
switch(code) {
-
case 'pop': {
-
socket.emit("pop", Object.values(world.players).filter((critter) => critter.c != "huggable").length);
break;
}
-
case 'freeitem': {
addItem(items.shop.freeItem.itemId, true);
break;
}
-
case 'tbt': {
-
const _throwbackItem = utils.getNewCodeItem(localPlayer, items.throwback);
if (_throwbackItem) addItem(_throwbackItem, true);
break;
}
-
case 'darkmode': {
addItem("3d_black", true);
break;
}
-
case 'spydar': {
localPlayer.gear = [
"sun_orange",
"super_mask_black",
"toque_blue",
"dracula_cloak",
"headphones_black",
-
"hoodie_black"
];
if (localCrumb._roomId == "tavern") {
···
io.in(localCrumb._roomId).volatile.emit("X", {
i: localPlayer.playerId,
x: 216,
-
y: 118
});
}
io.in(localCrumb._roomId).emit("G", {
i: localPlayer.playerId,
-
g: localPlayer.gear
});
-
socket.emit("updateGear", localPlayer.gear);
break;
}
-
case 'allitems': {
for (const item of itemsJSON) {
addItem(item.itemId, false);
}
break;
}
-
};
// Item Codes
-
const _itemCodes = items.codes as Record<string, string|Array<string>>
const item = _itemCodes[code];
-
if (typeof(item) == "string") {
addItem(item, true);
-
} else if (typeof(item) == "object") {
for (const _ of item) {
addItem(_, true);
}
}
// Event Codes (eg. Christmas 2019)
-
const _eventItemCodes = items.eventCodes as Record<string, Record<string, string>>;
const eventItem = (_eventItemCodes[localPlayer._partyId] || {})[code];
if (eventItem) addItem(eventItem);
});
socket.on("updateGear", (gear: Array<string>) => {
-
if (z.object({
-
gear: z.array(z.string().nonempty()).default([])
-
}).strict().safeParse({ gear: gear }).success == false) return;
const _gear = [];
for (const itemId of gear) {
if (localPlayer.inventory.includes(itemId)) {
-
_gear.push(itemId)
}
}
localPlayer.gear = _gear;
io.in(localCrumb._roomId).emit("G", {
i: localPlayer.playerId,
-
g: localPlayer.gear
});
socket.emit("updateGear", localPlayer.gear);
···
lastItem: _shopItems.lastItem.itemId,
freeItem: _shopItems.freeItem.itemId,
nextItem: _shopItems.nextItem.itemId,
-
collection: _shopItems.collection.map((item) => item.itemId)
-
})
});
socket.on("buyItem", (itemId: string) => {
-
if (z.object({
-
itemId: z.string().nonempty()
-
}).strict().safeParse({ itemId: itemId }).success == false) return;
// ? Free item is excluded from this list because the game just sends the "/freeitem" code
const currentShop = items.shop;
-
const _shopItems = [currentShop.lastItem, currentShop.nextItem, ...currentShop.collection]
-
const target = _shopItems.find((item) => item.itemId == itemId)!;
if (!target) {
-
console.log(chalk.red("> There is no item in this week's shop with itemId: %s"), itemId);
return;
-
};
-
if (localPlayer.coins >= target.cost && !localPlayer.inventory.includes(itemId)) {
-
console.log(chalk.green("[+] Bought item: %s for %d coins"), itemId, target.cost);
localPlayer.coins -= target.cost;
localPlayer.inventory.push(itemId);
···
});
socket.on("trigger", async () => {
-
const activatedTrigger = await utils.getTrigger(localPlayer, localCrumb._roomId, localPlayer._partyId);
if (!activatedTrigger) return;
if (activatedTrigger.hasItems) {
···
if (activatedTrigger.grantItem) {
let items = activatedTrigger.grantItem;
-
if (typeof(items) == 'string') items = [items];
for (const item of items) {
if (!localPlayer.inventory.includes(item)) {
···
});
socket.on("addIgnore", (playerId: string) => {
-
if (z.object({
-
playerId: z.enum(Object.keys(world.players) as any)
-
}).strict().safeParse({ playerId: playerId }).success == false) return;
-
if (Object.keys(world.players).includes(playerId) && !localPlayer.ignore.includes(playerId)) {
localPlayer.ignore.push(playerId);
-
};
});
socket.on("attack", (playerId: string) => {
-
if (z.object({
-
playerId: z.enum(Object.keys(world.players) as any)
-
}).strict().safeParse({ playerId: playerId }).success == false) return;
-
if (!localPlayer.gear.includes('bb_beebee')) return;
-
const monster = Object.values(world.players).find((player) => player.i == playerId && player.c == "huggable");
-
if (monster) {
io.in(localCrumb._roomId).emit("R", monster);
···
socket.emit("updateCoins", { balance: localPlayer.coins });
delete world.players[playerId];
-
};
});
socket.on("switchParty", (partyId: string) => {
-
if (z.object({
-
partyId: z.enum(parties as any)
-
}).strict().safeParse({ partyId: partyId }).success == false) return;
localPlayer._partyId = partyId;
socket.emit("switchParty");
···
if (localPlayer && localCrumb) {
io.in(localCrumb._roomId).emit("R", localCrumb);
delete world.players[localPlayer.playerId];
-
};
});
-
});
···
// deno-lint-ignore-file no-explicit-any
import { Server } from "https://deno.land/x/socket_io@0.2.0/mod.ts";
import { z } from "zod";
+
import * as world from "../constants/world.ts";
+
import * as items from "../constants/items.ts";
+
import * as utils from "../src/utils.ts";
+
import { CritterId, LocalPlayer, PlayerCrumb, ShopData } from "../src/types.ts";
+
import parties from "../constants/parties.json" with { type: "json" };
+
import itemsJSON from "../public/base/items.json" with { type: "json" };
export const io = new Server();
io.on("connection", (socket) => {
···
/** Condensed player data that is sufficient enough for other clients */
let localCrumb: PlayerCrumb;
+
// TODO: implement checking PlayFab API with ticket
socket.once("login", async (ticket: string) => {
+
if (
+
z.object({
+
ticket: z.string(),
+
}).safeParse({ ticket: ticket }).success == false
+
) return;
let playerData;
try {
+
playerData = await utils.verifyJWT(ticket);
+
} catch (_e) {
socket.disconnect(true);
+
return;
}
+
// TODO: make this just an inline function instead of having an onPropertyChange function, I'm just really lazy right now lol -index
function onPropertyChange(property: string, value: any) {
utils.updateAccount(localPlayer.nickname, property, value);
···
const createArrayHandler = (propertyName: string) => ({
get(target: any, property: string) {
+
if (typeof target[property] === "function") {
return function (...args: any[]) {
const result = target[property].apply(target, args);
onPropertyChange(propertyName, target);
···
};
}
return target[property];
+
},
});
const handler = {
···
return new Proxy(target[property], createArrayHandler(property));
}
return target[property];
+
},
+
};
+
+
//@ts-ignore: I will fix the type errors with using a different JWT library eventually
+
const sub = playerData as {
+
playerId: string;
+
nickname: string;
+
critterId: CritterId;
+
partyId: string;
+
persistent: boolean;
+
mods: Array<string>;
};
+
if ([
+
"today2019",
+
"today2020",
+
"today2021"
+
].includes(sub.partyId)) {
+
console.log('target year:', parseInt(sub.partyId.replace('today', '')));
+
sub.partyId = utils.getCurrentEvent(parseInt(sub.partyId.replace('today', '')))
};
+
const persistentAccount = await utils.getAccount(sub.nickname);
if (!sub.persistent || persistentAccount.individual == null) {
localPlayer = {
···
mutes: [],
_partyId: sub.partyId, // This key is replaced down the line anyway
+
_mods: [],
};
if (sub.persistent) {
utils.createAccount(localPlayer);
+
localPlayer = new Proxy<LocalPlayer>(
+
utils.expandAccount(localPlayer),
+
handler,
+
);
+
}
} else {
persistentAccount.individual.critterId = sub.critterId || "hamster";
persistentAccount.individual._partyId = sub.partyId || "default";
persistentAccount.individual._mods = sub.mods || [];
+
localPlayer = new Proxy<LocalPlayer>(
+
utils.expandAccount(persistentAccount.individual),
+
handler,
+
);
}
+
localPlayer._partyId = socket.handshake.query.get("partyId") || "default";
world.queue.splice(world.queue.indexOf(localPlayer.nickname), 1);
localCrumb = utils.makeCrumb(localPlayer, world.spawnRoom);
···
});
socket.on("joinRoom", (roomId: string) => {
+
if (
+
z.object({
+
roomId: z.enum(Object.keys(world.rooms) as any),
+
}).safeParse({ roomId: roomId }).success == false
+
) return;
const _room = (world.rooms[roomId] || { default: null }).default;
if (!_room) return;
···
socket.leave(localCrumb._roomId);
socket.broadcast.in(localCrumb._roomId).emit("R", localCrumb);
+
const modEnabled = (localPlayer._mods || []).includes("roomExits");
//@ts-ignore: Index type is correct
+
const correctExit = world.roomExits[localCrumb._roomId + "->" + roomId];
if (modEnabled && correctExit) {
localPlayer.x = correctExit.x;
localPlayer.y = correctExit.y;
localPlayer.rotation = correctExit.r;
+
}
+
if (!modEnabled || !correctExit) {
localPlayer.x = _room.startX;
localPlayer.y = _room.startY;
localPlayer.rotation = _room.startR | 180;
+
}
localCrumb = utils.makeCrumb(localPlayer, roomId);
world.players[localPlayer.playerId] = localCrumb;
+
console.log("> " + localPlayer.nickname + ' joined "' + roomId + '"!');
socket.join(roomId);
+
let playerCrumbs = Object.values(world.players).filter((crumb) =>
+
crumb._roomId == roomId
+
);
if (world.npcs[roomId]) {
playerCrumbs = [
...playerCrumbs,
+
...world.npcs[roomId],
];
+
}
socket.emit("joinRoom", {
name: _room.name,
roomId: roomId,
+
playerCrumbs: playerCrumbs,
});
socket.broadcast.in(localCrumb._roomId).emit("A", localCrumb);
});
socket.on("moveTo", (x: number, y: number) => {
+
const roomData = world.rooms[localCrumb._roomId][localPlayer._partyId] ||
+
world.rooms[localCrumb._roomId].default;
+
if (
+
z.object({
+
x: z.number().min(0).max(roomData.width),
+
y: z.number().min(0).max(roomData.height),
+
}).safeParse({ x: x, y: y }).success == false
+
) return;
const newDirection = utils.getDirection(localPlayer.x, localPlayer.y, x, y);
···
i: localPlayer.playerId,
x: x,
y: y,
+
r: newDirection,
});
});
socket.on("message", (text: string) => {
+
if (
+
z.object({
+
text: z.string().nonempty(),
+
}).safeParse({ text: text }).success == false
+
) return;
+
console.log(`> ${localPlayer.nickname} sent message:`, text);
localCrumb.m = text;
socket.broadcast.in(localCrumb._roomId).emit("M", {
i: localPlayer.playerId,
+
m: text,
});
setTimeout(() => {
···
});
socket.on("emote", (emote: string) => {
+
if (
+
z.object({
+
emote: z.string().nonempty(), // TODO: make this an enum
+
}).safeParse({ emote: emote }).success == false
+
) return;
+
console.log(`> ${localPlayer.nickname} sent emote:`, emote);
localCrumb.e = emote;
socket.broadcast.in(localCrumb._roomId).emit("E", {
i: localPlayer.playerId,
+
e: emote,
});
setTimeout(() => {
···
// ? Options is specified just because sometimes it is sent, but its always an empty string
socket.on("code", (code: string, _options?: string) => {
+
if (
+
z.object({
+
command: z.enum([
+
"pop",
+
"freeitem",
+
"tbt",
+
"darkmode",
+
"spydar",
+
"allitems",
+
]),
+
}).safeParse({
+
command: code,
+
}).success == false
+
) return;
+
console.log(`> ${localPlayer.nickname} sent code:`, code);
+
const addItem = function (id: string, showGUI: boolean = false) {
if (!localPlayer.inventory.includes(id)) {
socket.emit("addItem", { itemId: id, showGUI: showGUI });
localPlayer.inventory.push(id);
}
+
};
+
// Misc. Codes
+
switch (code) {
+
case "pop": {
+
socket.emit(
+
"pop",
+
Object.values(world.players).filter((critter) =>
+
critter.c != "huggable"
+
).length,
+
);
break;
}
+
case "freeitem": {
addItem(items.shop.freeItem.itemId, true);
break;
}
+
case "tbt": {
+
const _throwbackItem = utils.getNewCodeItem(
+
localPlayer,
+
items.throwback,
+
);
if (_throwbackItem) addItem(_throwbackItem, true);
break;
}
+
case "darkmode": {
addItem("3d_black", true);
break;
}
+
case "spydar": {
localPlayer.gear = [
"sun_orange",
"super_mask_black",
"toque_blue",
"dracula_cloak",
"headphones_black",
+
"hoodie_black",
];
if (localCrumb._roomId == "tavern") {
···
io.in(localCrumb._roomId).volatile.emit("X", {
i: localPlayer.playerId,
x: 216,
+
y: 118,
});
}
io.in(localCrumb._roomId).emit("G", {
i: localPlayer.playerId,
+
g: localPlayer.gear,
});
+
socket.emit("updateGear", localPlayer.gear);
break;
}
+
case "allitems": {
for (const item of itemsJSON) {
addItem(item.itemId, false);
}
break;
}
+
}
// Item Codes
+
const _itemCodes = items.codes as Record<string, string | Array<string>>;
const item = _itemCodes[code];
+
if (typeof item == "string") {
addItem(item, true);
+
} else if (typeof item == "object") {
for (const _ of item) {
addItem(_, true);
}
}
// Event Codes (eg. Christmas 2019)
+
const _eventItemCodes = items.eventCodes as Record<
+
string,
+
Record<string, string>
+
>;
const eventItem = (_eventItemCodes[localPlayer._partyId] || {})[code];
if (eventItem) addItem(eventItem);
});
socket.on("updateGear", (gear: Array<string>) => {
+
if (
+
z.object({
+
gear: z.array(z.string().nonempty()).default([]),
+
}).strict().safeParse({ gear: gear }).success == false
+
) return;
const _gear = [];
for (const itemId of gear) {
if (localPlayer.inventory.includes(itemId)) {
+
_gear.push(itemId);
}
}
localPlayer.gear = _gear;
io.in(localCrumb._roomId).emit("G", {
i: localPlayer.playerId,
+
g: localPlayer.gear,
});
socket.emit("updateGear", localPlayer.gear);
···
lastItem: _shopItems.lastItem.itemId,
freeItem: _shopItems.freeItem.itemId,
nextItem: _shopItems.nextItem.itemId,
+
collection: _shopItems.collection.map((item) => item.itemId),
+
});
});
socket.on("buyItem", (itemId: string) => {
+
if (
+
z.object({
+
itemId: z.string().nonempty(),
+
}).strict().safeParse({ itemId: itemId }).success == false
+
) return;
// ? Free item is excluded from this list because the game just sends the "/freeitem" code
const currentShop = items.shop;
+
const _shopItems = [
+
currentShop.lastItem,
+
currentShop.nextItem,
+
...currentShop.collection,
+
];
+
const target = _shopItems.find((item) => item.itemId == itemId)!;
if (!target) {
+
console.log(
+
"> There is no item in this week's shop with itemId:",
+
itemId,
+
);
return;
+
}
+
if (
+
localPlayer.coins >= target.cost &&
+
!localPlayer.inventory.includes(itemId)
+
) {
+
console.log(
+
"[+] Bought item: " + itemId + " for " + target.cost + " coins",
+
);
localPlayer.coins -= target.cost;
localPlayer.inventory.push(itemId);
···
});
socket.on("trigger", async () => {
+
const activatedTrigger = await utils.getTrigger(
+
localPlayer,
+
localCrumb._roomId,
+
localPlayer._partyId,
+
);
if (!activatedTrigger) return;
if (activatedTrigger.hasItems) {
···
if (activatedTrigger.grantItem) {
let items = activatedTrigger.grantItem;
+
if (typeof items == "string") items = [items];
for (const item of items) {
if (!localPlayer.inventory.includes(item)) {
···
});
socket.on("addIgnore", (playerId: string) => {
+
if (
+
z.object({
+
playerId: z.enum(Object.keys(world.players) as any),
+
}).strict().safeParse({ playerId: playerId }).success == false
+
) return;
+
if (
+
Object.keys(world.players).includes(playerId) &&
+
!localPlayer.ignore.includes(playerId)
+
) {
localPlayer.ignore.push(playerId);
+
}
});
socket.on("attack", (playerId: string) => {
+
if (
+
z.object({
+
playerId: z.enum(Object.keys(world.players) as any),
+
}).strict().safeParse({ playerId: playerId }).success == false
+
) return;
+
+
if (!localPlayer.gear.includes("bb_beebee")) return;
+
const monster = Object.values(world.players).find((player) =>
+
player.i == playerId && player.c == "huggable"
+
);
if (monster) {
io.in(localCrumb._roomId).emit("R", monster);
···
socket.emit("updateCoins", { balance: localPlayer.coins });
delete world.players[playerId];
+
}
});
socket.on("switchParty", (partyId: string) => {
+
if (
+
z.object({
+
partyId: z.enum(Object.keys(parties) as any),
+
}).strict().safeParse({ partyId: partyId }).success == false
+
) return;
localPlayer._partyId = partyId;
socket.emit("switchParty");
···
if (localPlayer && localCrumb) {
io.in(localCrumb._roomId).emit("R", localCrumb);
delete world.players[localPlayer.playerId];
+
}
});
+
});
-144
main.ts
···
-
import { serve } from "https://deno.land/std@0.162.0/http/server.ts";
-
-
import { sign } from 'hono/jwt';
-
import { Hono } from "https://deno.land/x/hono@v3.0.0/mod.ts";
-
import { serveStatic } from "https://deno.land/x/hono@v3.0.0/middleware.ts";
-
import { env } from 'hono/adapter';
-
import { validator } from 'hono/validator';
-
-
import { io } from "./io.ts";
-
import * as world from "./constants/world.ts";
-
import { Room } from "./types.ts";
-
import * as schemas from "./schema.ts";
-
import { getAccount } from "./utils.ts";
-
import parties from "./constants/parties.json" with { type: 'json' };
-
-
const app = new Hono();
-
app.get('/*', serveStatic({ root: './public' }));
-
-
// APIs for debugging and other purposes
-
app.get('/api/server/players', (c) => c.json({ players: world.players }));
-
-
app.get('/api/server/rooms', (c) => c.json(world.rooms));
-
-
app.get('/api/server/persistence', async (c) => {
-
const account = await getAccount();
-
return c.json({
-
success: true,
-
data: account
-
});
-
})
-
-
// APIs for use by the client
-
app.post('/api/client/login', validator('json', async (_value, c) => {
-
try {
-
const body = await c.req.json();
-
const parsed = schemas.login.safeParse(body);
-
if (!parsed.success) {
-
return c.json({
-
success: false,
-
message: "Validation failure",
-
error: parsed.error
-
}, 400);
-
};
-
return parsed.data;
-
} catch(_e) {
-
return c.json({
-
success: false,
-
message: "Bad request"
-
}, 400);
-
}
-
// deno-lint-ignore no-explicit-any
-
}) as any, async (c) => {
-
const body = c.req.valid('json') as {
-
nickname: string,
-
critterId: string,
-
partyId: string,
-
persistent: boolean,
-
mods: Array<string>
-
};
-
-
const _players = Object.values(world.players);
-
if (_players.find((player) => player.n == body.nickname) || world.queue.includes(body.nickname)) {
-
return c.json({
-
success: false,
-
message: "There is already a player with this nickname online."
-
});
-
}
-
-
const JWT_CONTENT = {
-
sub: {
-
playerId: crypto.randomUUID(),
-
...body // ZOD validator is set to make the body strict, so this expansion should be fine
-
},
-
exp: Math.floor(Date.now() / 1000) + 60 * 5 // 5 mins expiry
-
};
-
-
//@ts-ignore: Deno lint
-
const { JWT_TOKEN } = env<{ JWT_TOKEN: string }>(c);
-
const token = await sign(JWT_CONTENT, JWT_TOKEN);
-
-
world.queue.push(body.nickname);
-
return c.json({
-
success: true,
-
playerId: JWT_CONTENT.sub.playerId,
-
token: token
-
});
-
});
-
-
app.get('/api/client/rooms', (c) => {
-
const partyId = c.req.query('partyId') || 'default';
-
if (!parties.includes(partyId)) {
-
return c.json({
-
success: false,
-
message: "Invalid partyId hash provided."
-
});
-
}
-
-
let missing = 0;
-
const roomResponse = Object.keys(world.rooms).reduce((res: Array<Room>, roomId) => {
-
const room = world.rooms[roomId];
-
-
if (room[partyId]) {
-
if (!room[partyId].partyExclusive || room[partyId].partyExclusive.includes(partyId)) {
-
res.push(room[partyId]);
-
} else {
-
missing++;
-
}
-
} else {
-
if (!room.default.partyExclusive || room.default.partyExclusive.includes(partyId)) {
-
res.push(room.default);
-
} else {
-
missing++;
-
}
-
}
-
return res;
-
}, []);
-
-
if (missing == Object.keys(world.rooms).length) {
-
return c.json({
-
success: false,
-
message: "No rooms were fetched while indexxing using the specified partyId hash."
-
});
-
}
-
-
const res = roomResponse.filter((room) => room != null);
-
if (c.req.query('debug')) {
-
const roomNames = res.map((room) => room.name);
-
return c.json({
-
parties: parties,
-
data: roomNames
-
});
-
}
-
-
return c.json({
-
parties: parties,
-
data: res
-
});
-
});
-
-
const handler = io.handler(async (req) => {
-
return await app.fetch(req);
-
});
-
-
await serve(handler, { port: 3257 });
···
+2 -2
package-lock.json
···
{
-
"name": "box-critters-revival",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
-
"name": "box-critters-revival",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
···
{
+
"name": "localbox",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
+
"name": "localbox",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
+12 -10
package.json
···
{
-
"name": "box-critters-revival",
"version": "1.0.0",
-
"description": "",
-
"main": "main.js",
-
"scripts": {
-
"test": "echo \"Error: no test specified\" && exit 1",
-
"start": "nodemon main.js",
-
"dev": "nodemon main.js"
},
-
"author": "",
-
"license": "ISC",
"dependencies": {
"fs": "^0.0.1-security",
"http": "^0.0.1-security",
"nodemon": "^3.1.7",
"path": "^0.12.7",
"socket.io": "^4.8.1",
"zod": "^3.24.1"
-
}
}
···
{
+
"name": "localbox",
"version": "1.0.0",
+
"description": "A Typescript server emulator for Box Critters, a defunct virtual world by RocketSnail games.",
+
"author": {
+
"name": "Index",
+
"url": "https://github.com/indexxing/"
},
+
"license": "MIT",
"dependencies": {
"fs": "^0.0.1-security",
"http": "^0.0.1-security",
+
"jose": "5.9.6",
"nodemon": "^3.1.7",
"path": "^0.12.7",
"socket.io": "^4.8.1",
"zod": "^3.24.1"
+
},
+
"repository": "github:Box-Critters-Localbox/Localbox",
+
"contributors": [
+
"https://github.com/jonastisell"
+
]
}
-25
schema.ts
···
-
import { z } from 'zod';
-
import parties from './constants/parties.json' with { type: 'json' };
-
-
/*
-
LOGIN API
-
*/
-
export const login = z.object({
-
nickname: z.string().nonempty().max(25),
-
critterId: z.enum([
-
"hamster",
-
"beaver",
-
"lizard",
-
"raccoon",
-
"penguin",
-
"snail",
-
"snow_greeter",
-
"snowkeeper",
-
"snowgirl",
-
"snow_patrol",
-
"snowgrandma"
-
]).default("hamster"),
-
partyId: z.enum(parties as [string, ...string[]]).default("default"),
-
persistent: z.boolean().default(false),
-
mods: z.array(z.enum(["roomExits"])).default([])
-
}).strict(); // Strict to disallow extra keys
···
+232
src/main.ts
···
···
+
import { serve } from "https://deno.land/std@0.162.0/http/server.ts";
+
import { contentType } from "https://deno.land/std@0.224.0/media_types/mod.ts";
+
import {
+
dirname,
+
fromFileUrl,
+
join,
+
normalize,
+
} from "https://deno.land/std@0.224.0/path/mod.ts";
+
import { exists } from "jsr:@std/fs/exists";
+
+
import { io } from "./io.ts";
+
import * as world from "../constants/world.ts";
+
import { getAccount } from "./utils.ts";
+
import * as schemas from "./schema.ts";
+
import * as utils from "./utils.ts";
+
import parties from "../constants/parties.json" with { type: "json" };
+
import { extname } from "https://deno.land/std@0.212.0/path/extname.ts";
+
import { parseArgs } from "jsr:@std/cli/parse-args";
+
+
const EXECUTABLE = Deno.env.get("EXECUTABLE") == "true";
+
const BASE_DIR = EXECUTABLE
+
? dirname(Deno.execPath())
+
: dirname(dirname(fromFileUrl(Deno.mainModule)));
+
const PUBLIC_DIR = join(BASE_DIR, "public");
+
+
if (!EXECUTABLE) {
+
if (!await exists("./public") || !await exists(".env")) {
+
console.error("Missing files. Make sure you have `public/` and `.env`");
+
Deno.exit();
+
}
+
}
+
+
async function serveStatic(req: Request): Promise<Response> {
+
const url = new URL(req.url);
+
let pathname = url.pathname;
+
pathname = pathname.endsWith("/") ? pathname + "index.html" : pathname;
+
+
const fsPath = normalize(join(PUBLIC_DIR, pathname));
+
+
// Prevent directory traversal
+
if (!fsPath.startsWith(PUBLIC_DIR)) {
+
return new Response("Forbidden", { status: 403 });
+
}
+
+
try {
+
const file = await Deno.readFile(fsPath);
+
const mime = contentType(extname(fsPath)) || "application/octet-stream";
+
return new Response(file, {
+
headers: { "Content-Type": mime },
+
});
+
} catch {
+
return new Response("Not Found", { status: 404 });
+
}
+
}
+
+
async function handler(
+
req: Request,
+
connInfo: Deno.ServeHandlerInfo,
+
): Promise<Response> {
+
const url = new URL(req.url);
+
const pathname = url.pathname;
+
+
if (req.headers.get("upgrade") === "websocket") {
+
//@ts-ignore: The websocket successfully upgrades
+
return io.handler()(req, connInfo);
+
}
+
+
if (req.method == "POST" && pathname == "/api/client/login") {
+
try {
+
const body = await req.json();
+
const parsed = schemas.login.safeParse(body);
+
if (!parsed.success) {
+
return Response.json({
+
success: false,
+
message: "Validation failure",
+
error: parsed.error,
+
}, { status: 400 });
+
}
+
+
const data = parsed.data;
+
const _players = Object.values(world.players);
+
const nameInUse = _players.find((p) => p.n === data.nickname) ||
+
world.queue.includes(data.nickname);
+
+
if (nameInUse) {
+
return Response.json({
+
success: false,
+
message: "There is already a player with this nickname online.",
+
});
+
}
+
+
const JWT_CONTENT = {
+
playerId: crypto.randomUUID(),
+
...data,
+
};
+
+
const JWT_TOKEN = Deno.env.get("JWT_TOKEN");
+
if (!JWT_TOKEN) {
+
return new Response("JWT_TOKEN not set in env", { status: 500 });
+
}
+
+
const token = await utils.createJWT(JWT_CONTENT);
+
+
world.queue.push(data.nickname);
+
+
return Response.json({
+
success: true,
+
playerId: JWT_CONTENT.playerId,
+
token,
+
});
+
} catch {
+
return Response.json({
+
success: false,
+
message: "Bad request",
+
}, { status: 400 });
+
}
+
}
+
+
if (req.method == "GET") {
+
switch (pathname) {
+
case "/api/server/players": {
+
return Response.json({ players: world.players });
+
}
+
+
case "/api/server/rooms": {
+
return Response.json(world.rooms);
+
}
+
+
case "/api/server/persistence": {
+
const account = await getAccount();
+
return Response.json({
+
success: true,
+
data: account,
+
});
+
}
+
+
case "/api/client/rooms": {
+
const url = new URL(req.url);
+
let partyId = url.searchParams.get("partyId") || "default";
+
const debug = url.searchParams.has("debug");
+
+
if ([
+
"today2019",
+
"today2020",
+
"today2021"
+
].includes(partyId)) {
+
partyId = utils.getCurrentEvent(parseInt(partyId.replace('today', '')))
+
};
+
+
if (!Object.keys(parties).includes(partyId)) {
+
return Response.json({
+
success: false,
+
message: "Invalid partyId hash provided.",
+
});
+
}
+
+
let missing = 0;
+
const roomResponse = Object.keys(world.rooms).reduce(
+
(res, roomId) => {
+
const room = world.rooms[roomId];
+
+
if (room[partyId]) {
+
if (
+
!room[partyId].partyExclusive ||
+
room[partyId]?.partyExclusive?.includes(partyId)
+
) {
+
res.push(room[partyId]);
+
} else {
+
missing++;
+
}
+
} else {
+
if (
+
!room.default.partyExclusive ||
+
room.default.partyExclusive.includes(partyId)
+
) {
+
res.push(room.default);
+
} else {
+
missing++;
+
}
+
}
+
+
return res;
+
},
+
[] as Array<typeof world.rooms[string]["default"]>,
+
);
+
+
if (missing === Object.keys(world.rooms).length) {
+
return Response.json({
+
success: false,
+
message:
+
"No rooms were fetched while indexxing using the specified partyId hash.",
+
});
+
}
+
+
const partyIds = Object.keys(parties);
+
if (debug) {
+
const roomNames = roomResponse.map((room) => room.name);
+
return Response.json({
+
parties: partyIds,
+
data: roomNames,
+
});
+
}
+
+
return Response.json({
+
parties: partyIds,
+
data: roomResponse,
+
});
+
}
+
+
default: {
+
return serveStatic(req);
+
}
+
}
+
}
+
+
return new Response("Not Found", { status: 404 });
+
}
+
+
const args = parseArgs(Deno.args, {
+
string: ["port"],
+
default: {
+
port: "3257"
+
}
+
});
+
+
if (isNaN(Number(args.port))) {
+
console.log('Port provided is not valid.')
+
Deno.exit();
+
};
+
+
//@ts-ignore: Type issues occuring from upgrading websocket requests to Socket.io
+
await serve(handler, { port: args.port });
+30
src/schema.ts
···
···
+
import { z } from "zod";
+
import parties from "../constants/parties.json" with { type: "json" };
+
+
/*
+
LOGIN API
+
*/
+
export const login = z.object({
+
nickname: z.string().nonempty().max(25),
+
critterId: z.enum([
+
"hamster",
+
"beaver",
+
"lizard",
+
"raccoon",
+
"penguin",
+
"snail",
+
"snow_greeter",
+
"snowkeeper",
+
"snowgirl",
+
"snow_patrol",
+
"snowgrandma",
+
]).default("hamster"),
+
partyId: z.enum([
+
...Object.keys(parties) as [string, ...string[]],
+
"today2019",
+
"today2020",
+
"today2021"
+
]).default("default"),
+
persistent: z.boolean().default(false),
+
mods: z.array(z.enum(["roomExits"])).default([]),
+
}).strict(); // Strict to disallow extra keys
+140
src/types.ts
···
···
+
export type CritterId =
+
| "hamster"
+
| "snail"
+
| "lizard"
+
| "beaver"
+
| "raccoon"
+
| "penguin"
+
| "huggable";
+
+
export type Trigger = {
+
hex: string;
+
world?: { joinRoom: string };
+
room?: { hide: Array<string> };
+
server?: {
+
grantItem?: string | Array<string>;
+
hasItems?: Array<string>;
+
joinGame?: string;
+
addEgg?: string;
+
};
+
};
+
+
export type Room = {
+
roomId: string;
+
name: string;
+
width: number;
+
height: number;
+
startX: number;
+
startY: number;
+
startR: number;
+
media: {
+
background: string;
+
foreground?: string;
+
treasure?: string;
+
navMesh: string;
+
music?: string;
+
video?: string;
+
};
+
layout: string;
+
triggers: Array<Trigger>;
+
spriteSheet: string;
+
extra: null;
+
partyExclusive?: Array<string>;
+
};
+
+
export type LocalPlayer = {
+
playerId: string;
+
nickname: string;
+
critterId: CritterId;
+
ignore: Array<string>;
+
friends: Array<string>;
+
inventory: Array<string>;
+
gear: Array<string>;
+
/** Eggs is the term used to describe any object used in a scavenger hunt. Any prop name found in that list will be hidden and replaced with it's "_found" suffix prop counterpart */
+
eggs: Array<string>;
+
coins: number;
+
isMember: boolean | false;
+
isGuest: boolean | false;
+
isTeam: boolean | false;
+
x: number | 440;
+
y: number | 210;
+
rotation: number | 180;
+
mutes: Array<unknown>;
+
+
_partyId: string;
+
_mods: Array<string>;
+
+
// deno-lint-ignore no-explicit-any
+
[key: string]: any;
+
};
+
+
export type PlayerCrumb = {
+
/** Player ID */
+
i: string;
+
/** Player Nickname */
+
n: string;
+
/** Critter (Hamster, Beaver, Lizard, Snail, etc) */
+
c: CritterId;
+
x: number;
+
y: number;
+
r: number;
+
/** Gear (equipped items) */
+
g: Array<string>;
+
+
/** Message */
+
m: string;
+
/** Emote */
+
e: string;
+
+
_roomId: string;
+
};
+
+
export type ShopData = {
+
lastItem: { itemId: string; cost: number };
+
freeItem: { itemId: string; cost: number };
+
nextItem: { itemId: string; cost: number };
+
collection: Array<{ itemId: string; cost: number }>;
+
};
+
+
/*
+
Socket.io
+
*/
+
export interface ServerToClientEvents {
+
login: () => { player: LocalPlayer };
+
updateGear: () => { i: number; g: Array<string> };
+
updateCoins: () => { balance: number };
+
addItem: () => { itemId: string };
+
addEgg: () => string;
+
A: () => PlayerCrumb;
+
R: () => PlayerCrumb;
+
X: () => { i: number; x: number; y: number };
+
M: () => { i: number; m: string };
+
E: () => { i: number; e: string };
+
G: () => { i: number; g: Array<string> };
+
}
+
+
export interface ClientToServerEvents {
+
login: (ticket: string) => void;
+
joinLobby: () => unknown; // Unsure what this is for, I don't think the game had several servers
+
joinRoom: (
+
roomId: string,
+
) => { name: string; roomId: string; playerCrumbs: Array<PlayerCrumb> };
+
message: (text: string) => void;
+
emote: (emote: string) => void;
+
code: (code: string, options?: string) => void;
+
addIgnore: (id: number) => void;
+
addFriend: (id: number) => void;
+
moveTo: (x: number, y: number) => void;
+
updateCritter: () => unknown; // Unsure of what this is, maybe settings?
+
updateGear: (gear: Array<string>) => void;
+
getShop: () => ShopData;
+
buyItem: (itemId: string) => void;
+
trigger: () => void;
+
}
+
+
export type PartySchedule = {
+
[key: string]: {
+
start: string | null;
+
end: string | null;
+
};
+
};
+262
src/utils.ts
···
···
+
import { Image } from "https://deno.land/x/imagescript@1.3.0/mod.ts";
+
import {
+
dirname,
+
fromFileUrl,
+
join,
+
} from "https://deno.land/std@0.224.0/path/mod.ts";
+
import { jwtVerify, SignJWT } from "npm:jose@5.9.6";
+
+
import { rooms, spawnRoom } from "../constants/world.ts";
+
import { LocalPlayer, PlayerCrumb, Room } from "./types.ts";
+
import parties from "../constants/parties.json" with { type: "json" };
+
+
const EXECUTABLE = Deno.env.get("EXECUTABLE") == "true";
+
const BASE_DIR = EXECUTABLE
+
? dirname(Deno.execPath())
+
: dirname(dirname(fromFileUrl(Deno.mainModule)));
+
const PUBLIC_DIR = join(BASE_DIR, "public");
+
+
// deno-lint-ignore no-explicit-any
+
export async function createJWT(payload: any): Promise<string> {
+
const jwt = await new SignJWT(payload)
+
.setProtectedHeader({ alg: "HS256" })
+
.setIssuedAt()
+
.setExpirationTime("1h")
+
.sign(new TextEncoder().encode(Deno.env.get("JWT_TOKEN")));
+
+
return jwt;
+
}
+
+
// deno-lint-ignore no-explicit-any
+
export async function verifyJWT(token: string): Promise<any | null> {
+
try {
+
const { payload } = await jwtVerify(
+
token,
+
new TextEncoder().encode(Deno.env.get("JWT_TOKEN")),
+
);
+
return payload;
+
} catch (_e) {
+
return null;
+
}
+
}
+
+
/** Condenses the local player variable into data that is sufficient enough for other clients */
+
export function makeCrumb(player: LocalPlayer, roomId: string): PlayerCrumb {
+
return {
+
i: player.playerId,
+
n: player.nickname,
+
c: player.critterId,
+
x: player.x,
+
y: player.y,
+
r: player.rotation,
+
g: player.gear,
+
+
// message & emote
+
m: "",
+
e: "",
+
+
_roomId: roomId,
+
};
+
}
+
+
// TODO: use the correct triggers for the active party
+
export async function getTrigger(
+
player: LocalPlayer,
+
roomId: string,
+
partyId: string,
+
) {
+
const room = rooms[roomId][partyId];
+
if (!room) {
+
console.log(`[!] Cannot find room: "${roomId}@${partyId}"!`);
+
return;
+
}
+
+
try {
+
const treasureBuffer = await Deno.readFile(
+
//@ts-ignore: Deno lint
+
room.media.treasure?.replace("..", "public"),
+
);
+
const treasure = await Image.decode(treasureBuffer);
+
if (!treasure) {
+
throw new Error('Missing map server for room "' + roomId + '"!');
+
}
+
+
const pixel = treasure.getPixelAt(player.x, player.y);
+
const r = (pixel >> 24) & 0xFF,
+
g = (pixel >> 16) & 0xFF,
+
b = (pixel >> 8) & 0xFF;
+
const hexCode = ((r << 16) | (g << 8) | b).toString(16).padStart(6, "0");
+
+
const trigger = room.triggers.find((trigger) => trigger.hex == hexCode);
+
if (trigger) {
+
return trigger.server;
+
} else {
+
return null;
+
}
+
} catch (e) {
+
console.warn("[!] Caught error while checking for activated trigger.", e);
+
}
+
}
+
+
export function getNewCodeItem(player: LocalPlayer, items: Array<string>) {
+
const itemsSet = new Set(player.inventory);
+
const available = items.filter((item) => !itemsSet.has(item));
+
return available.length === 0
+
? null
+
: available[Math.floor(Math.random() * available.length)];
+
}
+
+
/**
+
* Indexes the /media/rooms directory for all versions of all rooms
+
* @returns All versions of every room
+
*/
+
export async function indexRoomData() {
+
const _roomData: Record<string, Record<string, Room>> = {};
+
+
const basePath = join(PUBLIC_DIR, "media", "rooms");
+
const _rooms = Deno.readDir(basePath);
+
+
for await (const room of _rooms) {
+
if (room.isDirectory) {
+
_roomData[room.name] = {};
+
const roomPath = join(basePath, room.name);
+
const versions = Deno.readDir(roomPath);
+
for await (const version of versions) {
+
if (version.isDirectory) {
+
const versionPath = join(roomPath, version.name, "data.json");
+
try {
+
const data = await Deno.readTextFile(versionPath);
+
_roomData[room.name][version.name] = JSON.parse(data);
+
} catch (_) {
+
console.log(
+
`[!] "${room.name}@${version.name}" is missing a data.json file`,
+
);
+
}
+
}
+
}
+
}
+
}
+
+
return _roomData;
+
}
+
+
export async function getAccount(nickname?: string) {
+
let accounts = [];
+
try {
+
const data = await Deno.readTextFile("accounts.json");
+
accounts = JSON.parse(data);
+
} catch (error) {
+
if (error instanceof Deno.errors.NotFound) {
+
console.log("Persistent login JSON is missing, using blank JSON array..");
+
accounts = [];
+
} else {
+
console.log(
+
"[!] Failure to fetch persistent login data with nickname: ",
+
nickname,
+
);
+
throw error;
+
}
+
}
+
+
if (nickname) {
+
const existingAccount = accounts.find((player: { nickname: string }) =>
+
player.nickname == nickname
+
);
+
if (existingAccount) {
+
return {
+
all: accounts,
+
individual: existingAccount,
+
};
+
} else {
+
return {
+
all: accounts,
+
individual: null,
+
};
+
}
+
} else {
+
return accounts;
+
}
+
}
+
+
export async function updateAccount(
+
nickname: string,
+
property: string,
+
value: unknown,
+
) {
+
if (["x", "y", "rotation", "_partyId"].includes(property)) return;
+
const accounts = await getAccount(nickname);
+
+
accounts.individual[property] = value;
+
await Deno.writeTextFile(
+
"accounts.json",
+
JSON.stringify(accounts.all, null, 2),
+
);
+
}
+
+
export function trimAccount(player: LocalPlayer) {
+
for (
+
const key of [
+
"critterId",
+
"x",
+
"y",
+
"rotation",
+
"_partyId",
+
"_mods",
+
]
+
) {
+
delete player[key];
+
}
+
return player;
+
}
+
+
export function expandAccount(player: LocalPlayer) {
+
const defaultPos = rooms[spawnRoom].default;
+
player.x = defaultPos.startX;
+
player.y = defaultPos.startY;
+
player.rotation = defaultPos.startR;
+
return player;
+
}
+
+
export function getDirection(
+
x: number,
+
y: number,
+
targetX: number,
+
targetY: number,
+
) {
+
const a = Math.floor((180 * Math.atan2(targetX - x, y - targetY)) / Math.PI);
+
return a < 0 ? a + 360 : a;
+
}
+
+
export async function createAccount(player: LocalPlayer) {
+
const accounts = await getAccount();
+
accounts.push(trimAccount(player));
+
+
await Deno.writeTextFile("accounts.json", JSON.stringify(accounts, null, 2));
+
}
+
+
export function getCurrentEvent(year: number): string {
+
const today = new Date();
+
const testDate = new Date(year, today.getMonth(), today.getDate());
+
+
//@ts-ignore: Types are bugging out here for absolutely no reason
+
for (const [eventId, { start, end }] of Object.entries(parties)) {
+
if (!start || !end) continue;
+
+
const originalStart = new Date(start);
+
const originalEnd = new Date(end);
+
+
const adjustedStart = new Date(year, originalStart.getMonth(), originalStart.getDate());
+
const adjustedEnd = new Date(
+
// Handle roll over
+
originalEnd.getFullYear() > originalStart.getFullYear() ? year + 1 : year,
+
originalEnd.getMonth(),
+
originalEnd.getDate()
+
);
+
+
if (testDate >= adjustedStart && testDate <= adjustedEnd) {
+
return eventId;
+
}
+
}
+
+
return "default";
+
}
-124
types.ts
···
-
export type CritterId = "hamster" | "snail" | "lizard" | "beaver" | "raccoon" | "penguin" | "huggable";
-
-
export type Trigger = {
-
hex: string,
-
world?: { joinRoom: string },
-
room?: { hide: Array<string> },
-
server?: {
-
grantItem?: string | Array<string>,
-
hasItems?: Array<string>,
-
joinGame?: string,
-
addEgg?: string
-
}
-
}
-
-
export type Room = {
-
roomId: string,
-
name: string,
-
width: number,
-
height: number,
-
startX: number,
-
startY: number,
-
startR: number,
-
media: {
-
background: string,
-
foreground?: string,
-
treasure?: string,
-
navMesh: string,
-
music?: string,
-
video?: string,
-
},
-
layout: string,
-
triggers: Array<Trigger>,
-
spriteSheet: string,
-
extra: null,
-
partyExclusive?: Array<string>
-
}
-
-
export type LocalPlayer = {
-
playerId: string,
-
nickname: string,
-
critterId: CritterId,
-
ignore: Array<string>,
-
friends: Array<string>,
-
inventory: Array<string>,
-
gear: Array<string>,
-
/** Eggs is the term used to describe any object used in a scavenger hunt. Any prop name found in that list will be hidden and replaced with it's "_found" suffix prop counterpart */
-
eggs: Array<string>,
-
coins: number,
-
isMember: boolean | false,
-
isGuest: boolean | false,
-
isTeam: boolean | false,
-
x: number | 440,
-
y: number | 210,
-
rotation: number | 180,
-
mutes: Array<unknown>,
-
-
_partyId: string,
-
_mods: Array<string>,
-
-
// deno-lint-ignore no-explicit-any
-
[key: string]: any
-
}
-
-
export type PlayerCrumb = {
-
/** Player ID */
-
i: string,
-
/** Player Nickname */
-
n: string,
-
/** Critter (Hamster, Beaver, Lizard, Snail, etc) */
-
c: CritterId,
-
x: number,
-
y: number,
-
r: number,
-
/** Gear (equipped items) */
-
g: Array<string>,
-
-
/** Message */
-
m: string,
-
/** Emote */
-
e: string,
-
-
_roomId: string
-
}
-
-
export type ShopData = {
-
lastItem: { itemId: string, cost: number },
-
freeItem: { itemId: string, cost: number },
-
nextItem: { itemId: string, cost: number },
-
collection: Array<{ itemId: string, cost: number }>
-
}
-
-
/*
-
Socket.io
-
*/
-
export interface ServerToClientEvents {
-
login: () => {player: LocalPlayer};
-
updateGear: () => { i: number, g: Array<string> };
-
updateCoins: () => { balance: number };
-
addItem: () => { itemId: string };
-
addEgg: () => string;
-
A: () => PlayerCrumb;
-
R: () => PlayerCrumb;
-
X: () => { i: number, x: number, y: number };
-
M: () => { i: number, m: string };
-
E: () => { i: number, e: string };
-
G: () => { i: number, g: Array<string> };
-
}
-
-
export interface ClientToServerEvents {
-
login: (ticket: string) => void;
-
joinLobby: () => unknown; // Unsure what this is for, I don't think the game had several servers
-
joinRoom: (roomId: string) => { name: string, roomId: string, playerCrumbs: Array<PlayerCrumb> };
-
message: (text: string) => void;
-
emote: (emote: string) => void;
-
code: (code: string, options?: string) => void;
-
addIgnore: (id: number) => void;
-
addFriend: (id: number) => void;
-
moveTo: (x: number, y: number) => void;
-
updateCritter: () => unknown; // Unsure of what this is, maybe settings?
-
updateGear: (gear: Array<string>) => void;
-
getShop: () => ShopData;
-
buyItem: (itemId: string) => void;
-
trigger: () => void;
-
}
···
-167
utils.ts
···
-
import { Image } from 'https://deno.land/x/imagescript@1.3.0/mod.ts';
-
import chalk from "https://deno.land/x/chalk_deno@v4.1.1-deno/source/index.js"
-
import { join } from "https://deno.land/std@0.224.0/path/mod.ts";
-
-
import { rooms, spawnRoom } from "./constants/world.ts";
-
import { Room, LocalPlayer, PlayerCrumb } from "./types.ts";
-
-
/** Condenses the local player variable into data that is sufficient enough for other clients */
-
export function makeCrumb(player: LocalPlayer, roomId: string): PlayerCrumb {
-
return {
-
i: player.playerId,
-
n: player.nickname,
-
c: player.critterId,
-
x: player.x,
-
y: player.y,
-
r: player.rotation,
-
g: player.gear,
-
-
// message & emote
-
m: "",
-
e: "",
-
-
_roomId: roomId
-
}
-
}
-
-
// TODO: use the correct triggers for the active party
-
export async function getTrigger(player: LocalPlayer, roomId: string, partyId: string) {
-
const room = rooms[roomId][partyId];
-
if (!room) {
-
console.log(chalk.red(`[!] Cannot find room: "${roomId}@${partyId}"!`));
-
return;
-
}
-
-
try {
-
//@ts-ignore: Deno lint
-
const treasureBuffer = await Deno.readFile(room.media.treasure?.replace('..','public'));
-
const treasure = await Image.decode(treasureBuffer);
-
if (!treasure) throw new Error('Missing map server for room "' + roomId + '"!');
-
-
const pixel = treasure.getPixelAt(player.x, player.y);
-
const r = (pixel >> 24) & 0xFF, g = (pixel >> 16) & 0xFF, b = (pixel >> 8) & 0xFF;
-
const hexCode = ((r << 16) | (g << 8) | b).toString(16).padStart(6, '0');
-
-
const trigger = room.triggers.find((trigger) => trigger.hex == hexCode);
-
if (trigger) {
-
return trigger.server;
-
} else {
-
return null;
-
}
-
} catch(e) {
-
console.warn(chalk.red('[!] Caught error while checking for activated trigger.'), e);
-
}
-
}
-
-
export function getNewCodeItem(player: LocalPlayer, items: Array<string>) {
-
const itemsSet = new Set(player.inventory);
-
const available = items.filter(item => !itemsSet.has(item));
-
return available.length === 0 ? null : available[Math.floor(Math.random() * available.length)];
-
}
-
-
/**
-
* Indexes the /media/rooms directory for all versions of all rooms
-
* @returns All versions of every room
-
*/
-
export async function indexRoomData() {
-
const _roomData: Record<string, Record<string, Room>> = {};
-
-
const basePath = join(Deno.cwd(), 'public', 'media', 'rooms');
-
const _rooms = Deno.readDir(basePath);
-
-
for await (const room of _rooms) {
-
if (room.isDirectory) {
-
_roomData[room.name] = {};
-
const roomPath = join(basePath, room.name);
-
const versions = Deno.readDir(roomPath);
-
for await (const version of versions) {
-
if (version.isDirectory) {
-
const versionPath = join(roomPath, version.name, 'data.json');
-
try {
-
const data = await Deno.readTextFile(versionPath);
-
_roomData[room.name][version.name] = JSON.parse(data);
-
} catch(_) {
-
console.log(chalk.red('[!] "%s@%s" is missing a data.json file'), room.name, version.name);
-
};
-
}
-
}
-
}
-
}
-
-
return _roomData
-
}
-
-
export async function getAccount(nickname?: string) {
-
let accounts = [];
-
try {
-
const data = await Deno.readTextFile('accounts.json');
-
accounts = JSON.parse(data);
-
} catch (error) {
-
if (error instanceof Deno.errors.NotFound) {
-
console.log(chalk.gray('Persistent login JSON is missing, using blank JSON array..'));
-
accounts = [];
-
} else {
-
console.log(chalk.red('[!] Failure to fetch persistent login data with nickname: '), nickname);
-
throw error;
-
};
-
}
-
-
if (nickname) {
-
const existingAccount = accounts.find((player: { nickname: string }) => player.nickname == nickname);
-
if (existingAccount) {
-
return {
-
all: accounts,
-
individual: existingAccount,
-
}
-
} else {
-
return {
-
all: accounts,
-
individual: null
-
}
-
}
-
} else {
-
return accounts;
-
}
-
}
-
-
export async function updateAccount(nickname: string, property: string, value: unknown) {
-
if (["x", "y", "rotation", "_partyId"].includes(property)) return;
-
const accounts = await getAccount(nickname);
-
-
accounts.individual[property] = value;
-
await Deno.writeTextFile('accounts.json', JSON.stringify(accounts.all, null, 2));
-
}
-
-
export function trimAccount(player: LocalPlayer) {
-
for (const key of [
-
"critterId",
-
"x",
-
"y",
-
"rotation",
-
"_partyId",
-
"_mods"
-
]) {
-
delete player[key];
-
}
-
return player;
-
}
-
-
export function expandAccount(player: LocalPlayer) {
-
const defaultPos = rooms[spawnRoom].default;
-
player.x = defaultPos.startX;
-
player.y = defaultPos.startY;
-
player.rotation = defaultPos.startR;
-
return player;
-
}
-
-
export function getDirection(x: number, y: number, targetX: number, targetY: number) {
-
const a = Math.floor((180 * Math.atan2(targetX - x, y - targetY)) / Math.PI);
-
return a < 0 ? a + 360 : a;
-
}
-
-
export async function createAccount(player: LocalPlayer) {
-
const accounts = await getAccount();
-
accounts.push(trimAccount(player));
-
-
await Deno.writeTextFile('accounts.json', JSON.stringify(accounts, null, 2));
-
}
···