v1.2
- feat: alpha hamster critter variant - style: format client JS - feat: improved login validation - style: format & update footers - fix: mods array not being populated by JWT contents - style: restructure socket.io server into multiple files - style: remove unused socket.io types
This commit is contained in:
parent
9b12e23dbf
commit
1b98772fe6
17 changed files with 7509 additions and 625 deletions
15
Dockerfile
15
Dockerfile
|
|
@ -1,15 +0,0 @@
|
||||||
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"]
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
|
"@/": "./src/",
|
||||||
"@std/fs": "jsr:@std/fs@^1.0.14"
|
"@std/fs": "jsr:@std/fs@^1.0.14"
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"start": "deno run --watch --allow-net --allow-read --allow-env --env-file=.env --allow-write src/main.ts",
|
"start": "deno run --allow-net --allow-read --allow-env --env-file=.env --allow-write src/main.ts",
|
||||||
|
"dev": "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"
|
"build": "deno compile --allow-net --allow-read --allow-env --env-file=.env.build --allow-write --output Localbox src/main.ts"
|
||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
|
|
||||||
6530
src/constants/items.json
Normal file
6530
src/constants/items.json
Normal file
File diff suppressed because it is too large
Load diff
140
src/constants/items.ts
Normal file
140
src/constants/items.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
};
|
||||||
50
src/constants/parties.json
Normal file
50
src/constants/parties.json
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/constants/world.ts
Normal file
91
src/constants/world.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
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": "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
546
src/io.ts
546
src/io.ts
|
|
@ -1,546 +0,0 @@
|
||||||
// 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) => {
|
|
||||||
let localPlayer: LocalPlayer;
|
|
||||||
|
|
||||||
/** 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 result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return target[property];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handler = {
|
|
||||||
set(target: any, property: string, value: any) {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
target[property] = new Proxy(value, createArrayHandler(property));
|
|
||||||
onPropertyChange(property, target[property]);
|
|
||||||
} else {
|
|
||||||
target[property] = value;
|
|
||||||
onPropertyChange(property, value);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
get(target: any, property: string) {
|
|
||||||
if (Array.isArray(target[property])) {
|
|
||||||
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 = {
|
|
||||||
playerId: sub.playerId,
|
|
||||||
nickname: sub.nickname,
|
|
||||||
critterId: sub.critterId,
|
|
||||||
ignore: [],
|
|
||||||
friends: [],
|
|
||||||
inventory: [],
|
|
||||||
gear: [],
|
|
||||||
eggs: [],
|
|
||||||
coins: 150,
|
|
||||||
isMember: false,
|
|
||||||
isGuest: false,
|
|
||||||
isTeam: false,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
rotation: 0,
|
|
||||||
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.join(world.spawnRoom);
|
|
||||||
|
|
||||||
world.players[localPlayer.playerId] = localCrumb;
|
|
||||||
socket.emit("login", {
|
|
||||||
player: localPlayer,
|
|
||||||
spawnRoom: 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);
|
|
||||||
|
|
||||||
localPlayer.x = x;
|
|
||||||
localPlayer.y = y;
|
|
||||||
localPlayer.rotation = newDirection;
|
|
||||||
|
|
||||||
localCrumb.x = x;
|
|
||||||
localCrumb.y = y;
|
|
||||||
localCrumb.r = newDirection;
|
|
||||||
|
|
||||||
io.in(localCrumb._roomId).volatile.emit("X", {
|
|
||||||
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(() => {
|
|
||||||
if (localCrumb.m != text) return;
|
|
||||||
localCrumb.m = "";
|
|
||||||
}, 5e3);
|
|
||||||
});
|
|
||||||
|
|
||||||
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(() => {
|
|
||||||
if (localCrumb.e != emote) return;
|
|
||||||
localCrumb.e = "";
|
|
||||||
}, 5e3);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ? 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") {
|
|
||||||
localPlayer.x = 216;
|
|
||||||
localPlayer.y = 118;
|
|
||||||
|
|
||||||
localCrumb.x = 216;
|
|
||||||
localCrumb.y = 118;
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("getShop", () => {
|
|
||||||
const _shopItems = items.shop as unknown as ShopData;
|
|
||||||
socket.emit("getShop", {
|
|
||||||
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.emit("buyItem", { itemId: itemId });
|
|
||||||
socket.emit("updateCoins", { balance: localPlayer.coins });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("trigger", async () => {
|
|
||||||
const activatedTrigger = await utils.getTrigger(
|
|
||||||
localPlayer,
|
|
||||||
localCrumb._roomId,
|
|
||||||
localPlayer._partyId,
|
|
||||||
);
|
|
||||||
if (!activatedTrigger) return;
|
|
||||||
|
|
||||||
if (activatedTrigger.hasItems) {
|
|
||||||
for (const item of activatedTrigger.hasItems) {
|
|
||||||
if (!localPlayer.inventory.includes(item)) return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activatedTrigger.grantItem) {
|
|
||||||
let items = activatedTrigger.grantItem;
|
|
||||||
if (typeof items == "string") items = [items];
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
if (!localPlayer.inventory.includes(item)) {
|
|
||||||
socket.emit("addItem", { itemId: item, showGUI: true });
|
|
||||||
localPlayer.inventory.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activatedTrigger.addEgg) {
|
|
||||||
const egg = activatedTrigger.addEgg;
|
|
||||||
socket.emit("addEgg", egg);
|
|
||||||
localPlayer.eggs.push(egg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
localPlayer.coins += 10;
|
|
||||||
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");
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("beep", () => socket.emit("beep"));
|
|
||||||
|
|
||||||
socket.on("disconnect", (reason) => {
|
|
||||||
if (reason == "server namespace disconnect") return;
|
|
||||||
|
|
||||||
if (localPlayer && localCrumb) {
|
|
||||||
io.in(localCrumb._roomId).emit("R", localCrumb);
|
|
||||||
delete world.players[localPlayer.playerId];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
39
src/main.ts
39
src/main.ts
|
|
@ -8,12 +8,12 @@ import {
|
||||||
} from "https://deno.land/std@0.224.0/path/mod.ts";
|
} from "https://deno.land/std@0.224.0/path/mod.ts";
|
||||||
import { exists } from "jsr:@std/fs/exists";
|
import { exists } from "jsr:@std/fs/exists";
|
||||||
|
|
||||||
import { io } from "./io.ts";
|
import { io } from "@/socket/index.ts";
|
||||||
import * as world from "../constants/world.ts";
|
import * as world from "@/constants/world.ts";
|
||||||
import { getAccount } from "./utils.ts";
|
import { getAccount } from "@/utils.ts";
|
||||||
import * as schemas from "./schema.ts";
|
import * as schemas from "@/schema.ts";
|
||||||
import * as utils from "./utils.ts";
|
import * as utils from "@/utils.ts";
|
||||||
import parties from "../constants/parties.json" with { type: "json" };
|
import parties from "@/constants/parties.json" with { type: "json" };
|
||||||
import { extname } from "https://deno.land/std@0.212.0/path/extname.ts";
|
import { extname } from "https://deno.land/std@0.212.0/path/extname.ts";
|
||||||
import { parseArgs } from "jsr:@std/cli/parse-args";
|
import { parseArgs } from "jsr:@std/cli/parse-args";
|
||||||
|
|
||||||
|
|
@ -33,6 +33,7 @@ if (!EXECUTABLE) {
|
||||||
async function serveStatic(req: Request): Promise<Response> {
|
async function serveStatic(req: Request): Promise<Response> {
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
let pathname = url.pathname;
|
let pathname = url.pathname;
|
||||||
|
pathname = decodeURIComponent(pathname);
|
||||||
pathname = pathname.endsWith("/") ? pathname + "index.html" : pathname;
|
pathname = pathname.endsWith("/") ? pathname + "index.html" : pathname;
|
||||||
|
|
||||||
const fsPath = normalize(join(PUBLIC_DIR, pathname));
|
const fsPath = normalize(join(PUBLIC_DIR, pathname));
|
||||||
|
|
@ -139,13 +140,17 @@ async function handler(
|
||||||
let partyId = url.searchParams.get("partyId") || "default";
|
let partyId = url.searchParams.get("partyId") || "default";
|
||||||
const debug = url.searchParams.has("debug");
|
const debug = url.searchParams.has("debug");
|
||||||
|
|
||||||
if ([
|
if (
|
||||||
"today2019",
|
[
|
||||||
"today2020",
|
"today2019",
|
||||||
"today2021"
|
"today2020",
|
||||||
].includes(partyId)) {
|
"today2021",
|
||||||
partyId = utils.getCurrentEvent(parseInt(partyId.replace('today', '')))
|
].includes(partyId)
|
||||||
};
|
) {
|
||||||
|
partyId = utils.getCurrentEvent(
|
||||||
|
parseInt(partyId.replace("today", "")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!Object.keys(parties).includes(partyId)) {
|
if (!Object.keys(parties).includes(partyId)) {
|
||||||
return Response.json({
|
return Response.json({
|
||||||
|
|
@ -219,14 +224,14 @@ async function handler(
|
||||||
const args = parseArgs(Deno.args, {
|
const args = parseArgs(Deno.args, {
|
||||||
string: ["port"],
|
string: ["port"],
|
||||||
default: {
|
default: {
|
||||||
port: "3257"
|
port: "3257",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isNaN(Number(args.port))) {
|
if (isNaN(Number(args.port))) {
|
||||||
console.log('Port provided is not valid.')
|
console.log("Port provided is not valid.");
|
||||||
Deno.exit();
|
Deno.exit();
|
||||||
};
|
}
|
||||||
|
|
||||||
//@ts-ignore: Type issues occuring from upgrading websocket requests to Socket.io
|
//@ts-ignore: Type issues occuring from upgrading websocket requests to Socket.io
|
||||||
await serve(handler, { port: args.port });
|
await serve(handler, { port: args.port });
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import parties from "../constants/parties.json" with { type: "json" };
|
import parties from "@/constants/parties.json" with { type: "json" };
|
||||||
|
|
||||||
/*
|
/*
|
||||||
LOGIN API
|
LOGIN API
|
||||||
*/
|
*/
|
||||||
export const login = z.object({
|
export const login = z.object({
|
||||||
nickname: z.string().nonempty().max(25),
|
nickname: z.string()
|
||||||
|
.transform((s) => s.trim())
|
||||||
|
.pipe(
|
||||||
|
z.string()
|
||||||
|
.min(3, "The nickname must be at least 3 characters long.")
|
||||||
|
.max(25, "The nickname must be less than 25 characters long."),
|
||||||
|
),
|
||||||
critterId: z.enum([
|
critterId: z.enum([
|
||||||
"hamster",
|
"hamster",
|
||||||
"beaver",
|
"beaver",
|
||||||
|
|
@ -18,12 +24,13 @@ export const login = z.object({
|
||||||
"snowgirl",
|
"snowgirl",
|
||||||
"snow_patrol",
|
"snow_patrol",
|
||||||
"snowgrandma",
|
"snowgrandma",
|
||||||
|
"alpha_hamster",
|
||||||
]).default("hamster"),
|
]).default("hamster"),
|
||||||
partyId: z.enum([
|
partyId: z.enum([
|
||||||
...Object.keys(parties) as [string, ...string[]],
|
...Object.keys(parties) as [string, ...string[]],
|
||||||
"today2019",
|
"today2019",
|
||||||
"today2020",
|
"today2020",
|
||||||
"today2021"
|
"today2021",
|
||||||
]).default("default"),
|
]).default("default"),
|
||||||
persistent: z.boolean().default(false),
|
persistent: z.boolean().default(false),
|
||||||
mods: z.array(z.enum(["roomExits"])).default([]),
|
mods: z.array(z.enum(["roomExits"])).default([]),
|
||||||
|
|
|
||||||
155
src/socket/handlers/connection.ts
Normal file
155
src/socket/handlers/connection.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
// deno-lint-ignore-file no-explicit-any
|
||||||
|
import { Server, Socket } 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 utils from "@/utils.ts";
|
||||||
|
import * as types from "@/types.ts";
|
||||||
|
|
||||||
|
export function listen(
|
||||||
|
io: Server,
|
||||||
|
socket: Socket,
|
||||||
|
ctx: types.SocketHandlerContext,
|
||||||
|
) {
|
||||||
|
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(ctx.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 result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return target[property];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handler = {
|
||||||
|
set(target: any, property: string, value: any) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
target[property] = new Proxy(value, createArrayHandler(property));
|
||||||
|
onPropertyChange(property, target[property]);
|
||||||
|
} else {
|
||||||
|
target[property] = value;
|
||||||
|
onPropertyChange(property, value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
get(target: any, property: string) {
|
||||||
|
if (Array.isArray(target[property])) {
|
||||||
|
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: types.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) {
|
||||||
|
ctx.localPlayer = {
|
||||||
|
playerId: sub.playerId,
|
||||||
|
nickname: sub.nickname,
|
||||||
|
critterId: sub.critterId,
|
||||||
|
ignore: [],
|
||||||
|
friends: [],
|
||||||
|
inventory: [],
|
||||||
|
gear: [],
|
||||||
|
eggs: [],
|
||||||
|
coins: 150,
|
||||||
|
isMember: false,
|
||||||
|
isGuest: false,
|
||||||
|
isTeam: false,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
rotation: 0,
|
||||||
|
mutes: [],
|
||||||
|
|
||||||
|
_partyId: sub.partyId, // This key is replaced down the line anyway
|
||||||
|
_mods: sub.mods,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sub.persistent) {
|
||||||
|
utils.createAccount(ctx.localPlayer);
|
||||||
|
ctx.localPlayer = new Proxy<types.LocalPlayer>(
|
||||||
|
utils.expandAccount(ctx.localPlayer),
|
||||||
|
handler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
persistentAccount.individual.critterId = sub.critterId || "hamster";
|
||||||
|
persistentAccount.individual._partyId = sub.partyId || "default";
|
||||||
|
persistentAccount.individual._mods = sub.mods || [];
|
||||||
|
|
||||||
|
ctx.localPlayer = new Proxy<types.LocalPlayer>(
|
||||||
|
utils.expandAccount(persistentAccount.individual),
|
||||||
|
handler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.localPlayer._partyId = socket.handshake.query.get("partyId") ||
|
||||||
|
"default";
|
||||||
|
world.queue.splice(world.queue.indexOf(ctx.localPlayer.nickname), 1);
|
||||||
|
|
||||||
|
ctx.localCrumb = utils.makeCrumb(ctx.localPlayer, world.spawnRoom);
|
||||||
|
socket.join(world.spawnRoom);
|
||||||
|
|
||||||
|
world.players[ctx.localPlayer.playerId] = ctx.localCrumb;
|
||||||
|
socket.emit("login", {
|
||||||
|
player: ctx.localPlayer,
|
||||||
|
spawnRoom: world.spawnRoom,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("beep", () => socket.emit("beep"));
|
||||||
|
|
||||||
|
socket.on("disconnect", (reason) => {
|
||||||
|
if (reason == "server namespace disconnect") return;
|
||||||
|
|
||||||
|
if (ctx.localPlayer && ctx.localCrumb) {
|
||||||
|
io.in(ctx.localCrumb._roomId).emit("R", ctx.localCrumb);
|
||||||
|
delete world.players[ctx.localPlayer.playerId];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
62
src/socket/handlers/economy.ts
Normal file
62
src/socket/handlers/economy.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { Server, Socket } from "https://deno.land/x/socket_io@0.2.0/mod.ts";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import * as items from "@/constants/items.ts";
|
||||||
|
import * as types from "@/types.ts";
|
||||||
|
|
||||||
|
export function listen(
|
||||||
|
_io: Server,
|
||||||
|
socket: Socket,
|
||||||
|
ctx: types.SocketHandlerContext,
|
||||||
|
) {
|
||||||
|
socket.on("getShop", () => {
|
||||||
|
const _shopItems = items.shop as unknown as types.ShopData;
|
||||||
|
socket.emit("getShop", {
|
||||||
|
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 (!ctx.localPlayer || !ctx.localCrumb) return;
|
||||||
|
|
||||||
|
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 (
|
||||||
|
ctx.localPlayer.coins >= target.cost &&
|
||||||
|
!ctx.localPlayer.inventory.includes(itemId)
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
"[+] Bought item: " + itemId + " for " + target.cost + " coins",
|
||||||
|
);
|
||||||
|
ctx.localPlayer.coins -= target.cost;
|
||||||
|
ctx.localPlayer.inventory.push(itemId);
|
||||||
|
|
||||||
|
socket.emit("buyItem", { itemId: itemId });
|
||||||
|
socket.emit("updateCoins", { balance: ctx.localPlayer.coins });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
73
src/socket/handlers/player.ts
Normal file
73
src/socket/handlers/player.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { Server, Socket } 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 utils from "@/utils.ts";
|
||||||
|
import * as types from "@/types.ts";
|
||||||
|
|
||||||
|
export function listen(
|
||||||
|
io: Server,
|
||||||
|
socket: Socket,
|
||||||
|
ctx: types.SocketHandlerContext,
|
||||||
|
) {
|
||||||
|
socket.on("moveTo", (x: number, y: number) => {
|
||||||
|
if (!ctx.localPlayer || !ctx.localCrumb) return;
|
||||||
|
|
||||||
|
const roomData =
|
||||||
|
world.rooms[ctx.localCrumb._roomId][ctx.localPlayer._partyId] ||
|
||||||
|
world.rooms[ctx.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(
|
||||||
|
ctx.localPlayer.x,
|
||||||
|
ctx.localPlayer.y,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.localPlayer.x = x;
|
||||||
|
ctx.localPlayer.y = y;
|
||||||
|
ctx.localPlayer.rotation = newDirection;
|
||||||
|
|
||||||
|
ctx.localCrumb.x = x;
|
||||||
|
ctx.localCrumb.y = y;
|
||||||
|
ctx.localCrumb.r = newDirection;
|
||||||
|
|
||||||
|
io.in(ctx.localCrumb._roomId).volatile.emit("X", {
|
||||||
|
i: ctx.localPlayer.playerId,
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
r: newDirection,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("updateGear", (gear: Array<string>) => {
|
||||||
|
if (!ctx.localPlayer || !ctx.localCrumb) return;
|
||||||
|
|
||||||
|
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 (ctx.localPlayer.inventory.includes(itemId)) {
|
||||||
|
_gear.push(itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.localPlayer.gear = _gear;
|
||||||
|
|
||||||
|
io.in(ctx.localCrumb._roomId).emit("G", {
|
||||||
|
i: ctx.localPlayer.playerId,
|
||||||
|
g: ctx.localPlayer.gear,
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.emit("updateGear", ctx.localPlayer.gear);
|
||||||
|
});
|
||||||
|
}
|
||||||
219
src/socket/handlers/social.ts
Normal file
219
src/socket/handlers/social.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
// deno-lint-ignore-file no-explicit-any
|
||||||
|
import { Server, Socket } 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 "@/utils.ts";
|
||||||
|
import * as types from "@/types.ts";
|
||||||
|
|
||||||
|
import itemsJSON from "@/constants/items.json" with { type: "json" };
|
||||||
|
|
||||||
|
export function listen(
|
||||||
|
io: Server,
|
||||||
|
socket: Socket,
|
||||||
|
ctx: types.SocketHandlerContext,
|
||||||
|
) {
|
||||||
|
socket.on("message", (text: string) => {
|
||||||
|
if (!ctx.localPlayer || !ctx.localCrumb) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
z.object({
|
||||||
|
text: z.string().nonempty(),
|
||||||
|
}).safeParse({ text: text }).success == false
|
||||||
|
) return;
|
||||||
|
|
||||||
|
console.log(`> ${ctx.localPlayer.nickname} sent message:`, text);
|
||||||
|
ctx.localCrumb.m = text;
|
||||||
|
|
||||||
|
socket.broadcast.in(ctx.localCrumb._roomId).emit("M", {
|
||||||
|
i: ctx.localPlayer.playerId,
|
||||||
|
m: text,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (ctx.localCrumb!.m != text) return;
|
||||||
|
ctx.localCrumb!.m = "";
|
||||||
|
}, 5e3);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("emote", (emote: string) => {
|
||||||
|
if (!ctx.localPlayer || !ctx.localCrumb) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
z.object({
|
||||||
|
emote: z.string().nonempty(), // TODO: make this an enum
|
||||||
|
}).safeParse({ emote: emote }).success == false
|
||||||
|
) return;
|
||||||
|
|
||||||
|
console.log(`> ${ctx.localPlayer.nickname} sent emote:`, emote);
|
||||||
|
ctx.localCrumb.e = emote;
|
||||||
|
|
||||||
|
socket.broadcast.in(ctx.localCrumb._roomId).emit("E", {
|
||||||
|
i: ctx.localPlayer.playerId,
|
||||||
|
e: emote,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (ctx.localCrumb!.e != emote) return;
|
||||||
|
ctx.localCrumb!.e = "";
|
||||||
|
}, 5e3);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ? Options is specified just because sometimes it is sent, but its always an empty string
|
||||||
|
socket.on("code", (code: string, _options?: string) => {
|
||||||
|
if (!ctx.localPlayer || !ctx.localCrumb) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
z.object({
|
||||||
|
command: z.enum([
|
||||||
|
"pop",
|
||||||
|
"freeitem",
|
||||||
|
"tbt",
|
||||||
|
"darkmode",
|
||||||
|
"spydar",
|
||||||
|
"allitems",
|
||||||
|
]),
|
||||||
|
}).safeParse({
|
||||||
|
command: code,
|
||||||
|
}).success == false
|
||||||
|
) return;
|
||||||
|
|
||||||
|
console.log(`> ${ctx.localPlayer.nickname} sent code:`, code);
|
||||||
|
|
||||||
|
const addItem = function (id: string, showGUI: boolean = false) {
|
||||||
|
if (!ctx.localPlayer!.inventory.includes(id)) {
|
||||||
|
socket.emit("addItem", { itemId: id, showGUI: showGUI });
|
||||||
|
ctx.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(
|
||||||
|
ctx.localPlayer,
|
||||||
|
items.throwback,
|
||||||
|
);
|
||||||
|
if (_throwbackItem) addItem(_throwbackItem, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "darkmode": {
|
||||||
|
addItem("3d_black", true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "spydar": {
|
||||||
|
ctx.localPlayer.gear = [
|
||||||
|
"sun_orange",
|
||||||
|
"super_mask_black",
|
||||||
|
"toque_blue",
|
||||||
|
"dracula_cloak",
|
||||||
|
"headphones_black",
|
||||||
|
"hoodie_black",
|
||||||
|
];
|
||||||
|
|
||||||
|
if (ctx.localCrumb._roomId == "tavern") {
|
||||||
|
ctx.localPlayer.x = 216;
|
||||||
|
ctx.localPlayer.y = 118;
|
||||||
|
|
||||||
|
ctx.localCrumb.x = 216;
|
||||||
|
ctx.localCrumb.y = 118;
|
||||||
|
|
||||||
|
io.in(ctx.localCrumb._roomId).volatile.emit("X", {
|
||||||
|
i: ctx.localPlayer.playerId,
|
||||||
|
x: 216,
|
||||||
|
y: 118,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
io.in(ctx.localCrumb._roomId).emit("G", {
|
||||||
|
i: ctx.localPlayer.playerId,
|
||||||
|
g: ctx.localPlayer.gear,
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.emit("updateGear", ctx.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[ctx.localPlayer._partyId] || {})[code];
|
||||||
|
if (eventItem) addItem(eventItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("addIgnore", (playerId: string) => {
|
||||||
|
if (!ctx.localPlayer || !ctx.localCrumb) return;
|
||||||
|
|
||||||
|
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) &&
|
||||||
|
!ctx.localPlayer.ignore.includes(playerId)
|
||||||
|
) {
|
||||||
|
ctx.localPlayer.ignore.push(playerId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("attack", (playerId: string) => {
|
||||||
|
if (!ctx.localPlayer || !ctx.localCrumb) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
z.object({
|
||||||
|
playerId: z.enum(Object.keys(world.players) as any),
|
||||||
|
}).strict().safeParse({ playerId: playerId }).success == false
|
||||||
|
) return;
|
||||||
|
|
||||||
|
if (!ctx.localPlayer.gear.includes("bb_beebee")) return;
|
||||||
|
const monster = Object.values(world.players).find((player) =>
|
||||||
|
player.i == playerId && player.c == "huggable"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (monster) {
|
||||||
|
io.in(ctx.localCrumb._roomId).emit("R", monster);
|
||||||
|
|
||||||
|
ctx.localPlayer.coins += 10;
|
||||||
|
socket.emit("updateCoins", { balance: ctx.localPlayer.coins });
|
||||||
|
|
||||||
|
delete world.players[playerId];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
116
src/socket/handlers/world.ts
Normal file
116
src/socket/handlers/world.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { Server, Socket } 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 utils from "@/utils.ts";
|
||||||
|
import * as types from "@/types.ts";
|
||||||
|
|
||||||
|
import parties from "@/constants/parties.json" with { type: "json" };
|
||||||
|
|
||||||
|
export function listen(
|
||||||
|
_io: Server,
|
||||||
|
socket: Socket,
|
||||||
|
ctx: types.SocketHandlerContext,
|
||||||
|
) {
|
||||||
|
socket.on("joinRoom", (roomId: string) => {
|
||||||
|
if (!ctx.localPlayer || !ctx.localCrumb) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
z.object({
|
||||||
|
roomId: z.enum(Object.keys(world.rooms) as [string, ...string[]]),
|
||||||
|
}).safeParse({ roomId: roomId }).success == false
|
||||||
|
) return;
|
||||||
|
|
||||||
|
const _room = (world.rooms[roomId] || { default: null }).default;
|
||||||
|
if (!_room) return;
|
||||||
|
|
||||||
|
socket.leave(ctx.localCrumb._roomId);
|
||||||
|
socket.broadcast.in(ctx.localCrumb._roomId).emit("R", ctx.localCrumb);
|
||||||
|
|
||||||
|
const modEnabled = (ctx.localPlayer._mods || []).includes("roomExits");
|
||||||
|
//@ts-ignore: Index type is correct
|
||||||
|
const correctExit = world.roomExits[ctx.localCrumb._roomId + "->" + roomId];
|
||||||
|
if (modEnabled && correctExit) {
|
||||||
|
ctx.localPlayer.x = correctExit.x;
|
||||||
|
ctx.localPlayer.y = correctExit.y;
|
||||||
|
ctx.localPlayer.rotation = correctExit.r;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!modEnabled || !correctExit) {
|
||||||
|
ctx.localPlayer.x = _room.startX;
|
||||||
|
ctx.localPlayer.y = _room.startY;
|
||||||
|
ctx.localPlayer.rotation = _room.startR | 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.localCrumb = utils.makeCrumb(ctx.localPlayer, roomId);
|
||||||
|
world.players[ctx.localPlayer.playerId] = ctx.localCrumb;
|
||||||
|
|
||||||
|
console.log("> " + ctx.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(ctx.localCrumb._roomId).emit("A", ctx.localCrumb);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("switchParty", (partyId: string) => {
|
||||||
|
if (!ctx.localPlayer || !ctx.localCrumb) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
z.object({
|
||||||
|
partyId: z.enum(Object.keys(parties) as [string, ...string[]]),
|
||||||
|
}).strict().safeParse({ partyId: partyId }).success == false
|
||||||
|
) return;
|
||||||
|
|
||||||
|
ctx.localPlayer._partyId = partyId;
|
||||||
|
socket.emit("switchParty");
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("trigger", async () => {
|
||||||
|
if (!ctx.localPlayer || !ctx.localCrumb) return;
|
||||||
|
|
||||||
|
const activatedTrigger = await utils.getTrigger(
|
||||||
|
ctx.localPlayer,
|
||||||
|
ctx.localCrumb._roomId,
|
||||||
|
ctx.localPlayer._partyId,
|
||||||
|
);
|
||||||
|
if (!activatedTrigger) return;
|
||||||
|
|
||||||
|
if (activatedTrigger.hasItems) {
|
||||||
|
for (const item of activatedTrigger.hasItems) {
|
||||||
|
if (!ctx.localPlayer.inventory.includes(item)) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activatedTrigger.grantItem) {
|
||||||
|
let items = activatedTrigger.grantItem;
|
||||||
|
if (typeof items == "string") items = [items];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
if (!ctx.localPlayer.inventory.includes(item)) {
|
||||||
|
socket.emit("addItem", { itemId: item, showGUI: true });
|
||||||
|
ctx.localPlayer.inventory.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activatedTrigger.addEgg) {
|
||||||
|
const egg = activatedTrigger.addEgg;
|
||||||
|
socket.emit("addEgg", egg);
|
||||||
|
ctx.localPlayer.eggs.push(egg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
22
src/socket/index.ts
Normal file
22
src/socket/index.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Server } from "https://deno.land/x/socket_io@0.2.0/mod.ts";
|
||||||
|
|
||||||
|
import * as connection from "./handlers/connection.ts";
|
||||||
|
import * as player from "./handlers/player.ts";
|
||||||
|
import * as world from "./handlers/world.ts";
|
||||||
|
import * as social from "./handlers/social.ts";
|
||||||
|
import * as economy from "./handlers/economy.ts";
|
||||||
|
|
||||||
|
export const io = new Server();
|
||||||
|
|
||||||
|
io.on("connection", (socket) => {
|
||||||
|
const context = {
|
||||||
|
localPlayer: null,
|
||||||
|
localCrumb: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
connection.listen(io, socket, context);
|
||||||
|
player.listen(io, socket, context);
|
||||||
|
world.listen(io, socket, context);
|
||||||
|
social.listen(io, socket, context);
|
||||||
|
economy.listen(io, socket, context);
|
||||||
|
});
|
||||||
41
src/types.ts
41
src/types.ts
|
|
@ -96,45 +96,14 @@ export type ShopData = {
|
||||||
collection: Array<{ 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 = {
|
export type PartySchedule = {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
start: string | null;
|
start: string | null;
|
||||||
end: string | null;
|
end: string | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SocketHandlerContext = {
|
||||||
|
localPlayer: null | LocalPlayer;
|
||||||
|
localCrumb: null | PlayerCrumb;
|
||||||
|
};
|
||||||
|
|
|
||||||
18
src/utils.ts
18
src/utils.ts
|
|
@ -6,9 +6,9 @@ import {
|
||||||
} from "https://deno.land/std@0.224.0/path/mod.ts";
|
} from "https://deno.land/std@0.224.0/path/mod.ts";
|
||||||
import { jwtVerify, SignJWT } from "npm:jose@5.9.6";
|
import { jwtVerify, SignJWT } from "npm:jose@5.9.6";
|
||||||
|
|
||||||
import { rooms, spawnRoom } from "../constants/world.ts";
|
import { rooms, spawnRoom } from "@/constants/world.ts";
|
||||||
import { LocalPlayer, PlayerCrumb, Room } from "./types.ts";
|
import { LocalPlayer, PlayerCrumb, Room } from "@/types.ts";
|
||||||
import parties from "../constants/parties.json" with { type: "json" };
|
import parties from "@/constants/parties.json" with { type: "json" };
|
||||||
|
|
||||||
const EXECUTABLE = Deno.env.get("EXECUTABLE") == "true";
|
const EXECUTABLE = Deno.env.get("EXECUTABLE") == "true";
|
||||||
const BASE_DIR = EXECUTABLE
|
const BASE_DIR = EXECUTABLE
|
||||||
|
|
@ -183,7 +183,7 @@ export async function updateAccount(
|
||||||
property: string,
|
property: string,
|
||||||
value: unknown,
|
value: unknown,
|
||||||
) {
|
) {
|
||||||
if (["x", "y", "rotation", "_partyId"].includes(property)) return;
|
if (["x", "y", "rotation", "_partyId", "_mods"].includes(property)) return;
|
||||||
const accounts = await getAccount(nickname);
|
const accounts = await getAccount(nickname);
|
||||||
|
|
||||||
accounts.individual[property] = value;
|
accounts.individual[property] = value;
|
||||||
|
|
@ -245,12 +245,16 @@ export function getCurrentEvent(year: number): string {
|
||||||
const originalStart = new Date(start);
|
const originalStart = new Date(start);
|
||||||
const originalEnd = new Date(end);
|
const originalEnd = new Date(end);
|
||||||
|
|
||||||
const adjustedStart = new Date(year, originalStart.getMonth(), originalStart.getDate());
|
const adjustedStart = new Date(
|
||||||
|
year,
|
||||||
|
originalStart.getMonth(),
|
||||||
|
originalStart.getDate(),
|
||||||
|
);
|
||||||
const adjustedEnd = new Date(
|
const adjustedEnd = new Date(
|
||||||
// Handle roll over
|
// Handle roll over
|
||||||
originalEnd.getFullYear() > originalStart.getFullYear() ? year + 1 : year,
|
originalEnd.getFullYear() > originalStart.getFullYear() ? year + 1 : year,
|
||||||
originalEnd.getMonth(),
|
originalEnd.getMonth(),
|
||||||
originalEnd.getDate()
|
originalEnd.getDate(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (testDate >= adjustedStart && testDate <= adjustedEnd) {
|
if (testDate >= adjustedStart && testDate <= adjustedEnd) {
|
||||||
|
|
@ -259,4 +263,4 @@ export function getCurrentEvent(year: number): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
return "default";
|
return "default";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue