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": {
|
||||
"@/": "./src/",
|
||||
"@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",
|
||||
"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"
|
||||
},
|
||||
"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";
|
||||
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 { io } from "@/socket/index.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";
|
||||
|
||||
|
|
@ -33,6 +33,7 @@ if (!EXECUTABLE) {
|
|||
async function serveStatic(req: Request): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
let pathname = url.pathname;
|
||||
pathname = decodeURIComponent(pathname);
|
||||
pathname = pathname.endsWith("/") ? pathname + "index.html" : pathname;
|
||||
|
||||
const fsPath = normalize(join(PUBLIC_DIR, pathname));
|
||||
|
|
@ -139,13 +140,17 @@ async function handler(
|
|||
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 (
|
||||
[
|
||||
"today2019",
|
||||
"today2020",
|
||||
"today2021",
|
||||
].includes(partyId)
|
||||
) {
|
||||
partyId = utils.getCurrentEvent(
|
||||
parseInt(partyId.replace("today", "")),
|
||||
);
|
||||
}
|
||||
|
||||
if (!Object.keys(parties).includes(partyId)) {
|
||||
return Response.json({
|
||||
|
|
@ -219,14 +224,14 @@ async function handler(
|
|||
const args = parseArgs(Deno.args, {
|
||||
string: ["port"],
|
||||
default: {
|
||||
port: "3257"
|
||||
}
|
||||
port: "3257",
|
||||
},
|
||||
});
|
||||
|
||||
if (isNaN(Number(args.port))) {
|
||||
console.log('Port provided is not valid.')
|
||||
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 });
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
import { z } from "zod";
|
||||
import parties from "../constants/parties.json" with { type: "json" };
|
||||
import parties from "@/constants/parties.json" with { type: "json" };
|
||||
|
||||
/*
|
||||
LOGIN API
|
||||
*/
|
||||
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([
|
||||
"hamster",
|
||||
"beaver",
|
||||
|
|
@ -18,12 +24,13 @@ export const login = z.object({
|
|||
"snowgirl",
|
||||
"snow_patrol",
|
||||
"snowgrandma",
|
||||
"alpha_hamster",
|
||||
]).default("hamster"),
|
||||
partyId: z.enum([
|
||||
...Object.keys(parties) as [string, ...string[]],
|
||||
"today2019",
|
||||
"today2020",
|
||||
"today2021"
|
||||
"today2021",
|
||||
]).default("default"),
|
||||
persistent: z.boolean().default(false),
|
||||
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 }>;
|
||||
};
|
||||
|
||||
/*
|
||||
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;
|
||||
};
|
||||
};
|
||||
|
||||
export type SocketHandlerContext = {
|
||||
localPlayer: null | LocalPlayer;
|
||||
localCrumb: null | PlayerCrumb;
|
||||
};
|
||||
|
|
|
|||
16
src/utils.ts
16
src/utils.ts
|
|
@ -6,9 +6,9 @@ import {
|
|||
} 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" };
|
||||
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
|
||||
|
|
@ -183,7 +183,7 @@ export async function updateAccount(
|
|||
property: string,
|
||||
value: unknown,
|
||||
) {
|
||||
if (["x", "y", "rotation", "_partyId"].includes(property)) return;
|
||||
if (["x", "y", "rotation", "_partyId", "_mods"].includes(property)) return;
|
||||
const accounts = await getAccount(nickname);
|
||||
|
||||
accounts.individual[property] = value;
|
||||
|
|
@ -245,12 +245,16 @@ export function getCurrentEvent(year: number): string {
|
|||
const originalStart = new Date(start);
|
||||
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(
|
||||
// Handle roll over
|
||||
originalEnd.getFullYear() > originalStart.getFullYear() ? year + 1 : year,
|
||||
originalEnd.getMonth(),
|
||||
originalEnd.getDate()
|
||||
originalEnd.getDate(),
|
||||
);
|
||||
|
||||
if (testDate >= adjustedStart && testDate <= adjustedEnd) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue