Compare commits

..

10 Commits

74 changed files with 855 additions and 109 deletions

0
.env.template Normal file → Executable file
View File

0
.gitignore vendored Normal file → Executable file
View File

0
.prettierrc Normal file → Executable file
View File

0
README.md Normal file → Executable file
View File

BIN
bun.lockb

Binary file not shown.

0
bunfig.toml Normal file → Executable file
View File

0
config/ascii.txt Normal file → Executable file
View File

0
config/ascii.yml Normal file → Executable file
View File

0
config/balance.yml Normal file → Executable file
View File

10
config/mpp_net_channels.yml Normal file → Executable file
View File

@ -1,7 +1,11 @@
desiredUser:
name: "🟇 𝙎𝙪𝙥𝙚𝙧 Cosmic"
#name: "🟇 𝙎𝙪𝙥𝙚𝙧 Cosmic"
name: "🟇 Cosmic ⚝ *help"
color: "#1d0054"
agents:
#wss://smnmpp.hri7566.info:8443:
# - id: "lobby"
wss://mppclone.com:
- id: "✧𝓓𝓔𝓥 𝓡𝓸𝓸𝓶✧"
- id: "test/awkward"
#- id: "✧𝓓𝓔𝓥 𝓡𝓸𝓸𝓶✧"
#- id: "Hree7566"
- id: "{midi-test}"

2
config/prefixes.yml Normal file → Executable file
View File

@ -3,3 +3,5 @@ prefixes:
spaced: false
- id: cosmic
spaced: true
- id: c
spaced: false

View File

@ -4,6 +4,7 @@ NONE:
- cosmic.commandGroup.general
- cosmic.command.inventory
- cosmic.command.balance
- cosmic.command.bake
- cosmic.command.magic8ball
- cosmic.command.color
- cosmic.command.id

6
config/services.yml Normal file → Executable file
View File

@ -1,5 +1,5 @@
debug: false
enableConsole: false
debug: true
enableConsole: true
enableMPP: true
enableDiscord: true
enableDiscord: false
enableSwitchChat: false

0
config/switchchat.yml Normal file → Executable file
View File

50
package.json Normal file → Executable file
View File

@ -1,27 +1,27 @@
{
"name": "supercosmic",
"version": "0.1.0",
"module": "src/index.ts",
"type": "module",
"devDependencies": {
"bun-types": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"@prisma/client": "^5.5.2",
"@t3-oss/env-core": "^0.7.1",
"discord.js": "^14.14.1",
"dotenv": "^16.3.1",
"hyperimport": "^0.1.0",
"mathjs": "^11.11.2",
"mpp-client-net": "^1.1.3",
"mpp-client-xt": "^1.3.1",
"prisma": "^5.4.2",
"switchchat": "^3.2.1",
"typescript": "^5.3.2",
"yaml": "^2.3.3",
"zod": "^3.22.4"
}
"name": "supercosmic",
"version": "0.1.0",
"module": "src/index.ts",
"type": "module",
"devDependencies": {
"bun-types": "^1.0.16"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"@prisma/client": "^5.5.2",
"@t3-oss/env-core": "^0.7.1",
"discord.js": "^14.14.1",
"dotenv": "^16.3.1",
"hyperimport": "^0.1.0",
"mathjs": "^11.11.2",
"mpp-client-net": "^1.1.3",
"mpp-client-xt": "^1.3.1",
"prisma": "^5.4.2",
"switchchat": "^3.2.1",
"typescript": "^5.3.2",
"yaml": "^2.3.3",
"zod": "^3.22.4"
}
}

0
pm2.config.cjs Normal file → Executable file
View File

5
prisma/schema.prisma Normal file → Executable file
View File

@ -34,3 +34,8 @@ enum Role {
ADMINISTRATOR
OWNER
}
model KeyValueStore {
id Int @id @unique @default(1)
data Json @default("{}")
}

0
src/commands/Command.ts Normal file → Executable file
View File

0
src/commands/CommandGroup.ts Normal file → Executable file
View File

11
src/commands/CommandHandler.ts Normal file → Executable file
View File

@ -46,7 +46,7 @@ export type BaseCommandMessage<T = unknown> = Omit<
export class CommandHandler {
public static commandGroups = new Array<CommandGroup>();
public static prefixes = new Array<Prefix>();
public static prefixes = new Set<Prefix>();
public static logger = new Logger("Command Handler");
@ -172,5 +172,12 @@ export class CommandHandler {
// Add prefixes
for (const prefix of prefixConfig.prefixes) {
CommandHandler.prefixes.push(prefix);
CommandHandler.prefixes.add(prefix);
}
// Store commands for hot reload
declare global {
var commandHandler: any;
}
globalThis.commandHandler ??= CommandHandler;

0
src/commands/Prefix.ts Normal file → Executable file
View File

View File

@ -0,0 +1,15 @@
import { formatBalance } from "../../../economy/Balance";
import { startBaking } from "../../../economy/baking";
import { Command } from "../../Command";
export const bake = new Command(
"bake",
["bake", "b", "startbaking", "bakecake", "oven"],
"bozo's oven",
"bake",
(msg, agent) => {
const agentId = globalThis.serviceLoader.getAgentId(agent);
const message = startBaking(msg.user.id, agentId);
return message;
}
);

2
src/commands/commands/economy/balance.ts Normal file → Executable file
View File

@ -8,6 +8,6 @@ export const balance = new Command(
"balance",
msg => {
const bal = msg.inventory.balance;
return `Balance: ${formatBalance(bal)}`;
return `Your balance: ${formatBalance(bal)}`;
}
);

View File

@ -0,0 +1,23 @@
import { KekklefruitTree } from "../../../economy/kekkle";
import { Command } from "../../Command";
export const grow = new Command(
"grow",
["grow"],
"grow bozo's kekklefruit (forcefully)",
"grow [number]",
async msg => {
let num: number;
if (msg.argv[1]) {
num = parseInt(msg.argv[1]);
if (isNaN(num)) return `Need number bozo`;
} else {
num = 1;
}
await KekklefruitTree.growFruit(num);
return `You grew ${num} fruit.`;
}
);

18
src/commands/commands/economy/inventory.ts Normal file → Executable file
View File

@ -1,4 +1,5 @@
import { Item, StackableItem } from "../../../economy/Item";
import { readItems } from "../../../data/inventory";
import { CakeItem, Item, StackableItem } from "../../../economy/Item";
import { Command } from "../../Command";
export const inventory = new Command(
@ -6,15 +7,18 @@ export const inventory = new Command(
["inventory", "inv"],
"get bozo's inventory",
"inventory",
msg => {
const items = msg.inventory.items as unknown as Item[];
async msg => {
const items = await readItems(msg.p._id);
if (!items) return `Items: (none)`;
console.log(typeof items, items);
const list = items
.map(
i =>
`${i.name}${
(i as StackableItem).count
? " " + (i as StackableItem).count
: ""
`${(i as CakeItem).emoji ? (i as CakeItem).emoji : ""}${i.name}${(i as StackableItem).count
? " " + `(x${(i as StackableItem).count})`
: ""
}`
)
.join(" | ");

View File

@ -0,0 +1,21 @@
import { JsonArray, JsonValue } from "@prisma/client/runtime/library";
import { KekklefruitTree } from "../../../economy/kekkle";
import { Command } from "../../Command";
import { addItem, updateInventory } from "../../../data/inventory";
export const pick = new Command(
"pick",
["pick"],
"bozo will pick fruit off the kekklefruit tree",
"pick",
async msg => {
const fruit = await KekklefruitTree.pickFruit();
if (!fruit)
return `There are not enough fruit on the kekklefruit tree.`;
addItem(msg.p._id, fruit);
return `(insert random boring message about ${fruit.name} here)`;
},
false
);

View File

@ -0,0 +1,13 @@
import { KekklefruitTree } from "../../../economy/kekkle";
import { Command } from "../../Command";
export const tree = new Command(
"tree",
["tree"],
"bozo will get the amount of fruit on the kekklefruit tree",
"tree",
async msg => {
return `There are ${KekklefruitTree.getFruitCount()} kekklefruit on the tree.`;
},
false
);

View File

@ -0,0 +1,50 @@
import { readItems, subtractItem } from "../../../data/inventory";
import { FoodItem } from "../../../economy/Item";
import { TEatBhv, eatBhv } from "../../../economy/eatBhv";
import { Logger } from "../../../util/Logger";
import { Command } from "../../Command";
const logger = new Logger("eat");
export const eat = new Command(
"eat",
["eat"],
"bozo hungy",
"eat",
async (msg, agent) => {
if (!msg.argv[1]) return "What do you want to eat?";
const itemFuzzy = msg.argv[1].toLowerCase();
const items = await readItems(msg.user.id);
if (!items) return "You have nothing to eat.";
let what = items.find(item => {
if (item.name.toLowerCase().includes(itemFuzzy)) return true;
}) as FoodItem | undefined;
if (!what) return `You don't have any "${itemFuzzy}" to eat.`;
if (!what.consumable) return `You can't eat the ${what.name}.`;
if (!what.edible) return `The ${what.name} is not edible.`;
// TODO cause eat behavior
let bhv: TEatBhv | undefined;
if (what.id.includes("cake")) {
// Find regular cake behavior
bhv = eatBhv.get("cake");
} else {
// Find eat bhv for item ID
bhv = eatBhv.get(what.id);
}
if (!bhv) return `You don't know how to eat ${what.name}.`;
const reply = await bhv(msg, agent, what);
if (reply.consumed) {
const worked = await subtractItem(msg.user.id, what);
if (!worked) logger.warn(`Failed to subtract item ${what.name} from user ${msg.user.id}`);
}
return reply.output;
//return `You ate the ${what.name}. (not really)`;
}
);

0
src/commands/commands/fun/magic8ball.ts Normal file → Executable file
View File

0
src/commands/commands/general/about.ts Normal file → Executable file
View File

0
src/commands/commands/general/help.ts Normal file → Executable file
View File

0
src/commands/commands/utility/branch.ts Normal file → Executable file
View File

0
src/commands/commands/utility/color.ts Normal file → Executable file
View File

View File

@ -0,0 +1,16 @@
import Client from "mpp-client-net";
import { Command } from "../../Command";
export const crown = new Command(
"crown",
["crown"],
"get bozo hat",
"crown",
(msg, agent) => {
if (agent.platform !== "mpp") return;
(agent.client as Client).sendArray([{
m: "chown",
id: msg.p.platformId
}]);
}
);

4
src/commands/commands/utility/cursor.ts Normal file → Executable file
View File

@ -1,4 +1,4 @@
import type { MPPAgent } from "../../../services/mpp";
import type { MPPNetAgent } from "../../../services/mppnet";
import { Command } from "../../Command";
export const cursor = new Command(
@ -10,7 +10,7 @@ export const cursor = new Command(
if (agent.platform !== "mpp") return;
if (!msg.argv[1]) return "Specify a mode.";
const cursor = (agent as MPPAgent).cursor;
const cursor = (agent as MPPNetAgent).cursor;
cursor.props.currentAnimation = msg.argv[1];
}
);

View File

@ -0,0 +1,14 @@
import { deleteInventory } from "../../../data/inventory";
import { Command } from "../../Command";
export const delinv = new Command(
"delinv",
["delinv"],
"delete a bozo's inventory",
"delinv [id]",
async (msg) => {
let userId = msg.argv[1] ? msg.argv[1] : msg.p._id;
await deleteInventory(userId);
return `Inventory of \`${userId}\` deleted.`
}
);

0
src/commands/commands/utility/ic.ts Normal file → Executable file
View File

2
src/commands/commands/utility/id.ts Normal file → Executable file
View File

@ -6,7 +6,7 @@ export const id = new Command(
"get your id bozo",
"id",
(msg, agent) => {
if (agent.platform == "mpp") {
if (agent.platform === "mpp") {
return `ID: \`${
(msg.originalMessage as any).p._id
}\` Cosmic ID: \`${msg.p._id}\``;

0
src/commands/commands/utility/math.ts Normal file → Executable file
View File

12
src/commands/commands/utility/memory.ts Normal file → Executable file
View File

@ -6,10 +6,12 @@ export const memory = new Command(
"get the memory bozo",
"memory",
() => {
return `${(process.memoryUsage().heapUsed / 1000 / 1000).toFixed(
2
)} MB used / ${(process.memoryUsage().heapTotal / 1000 / 1000).toFixed(
2
)} MB total`;
const mem = process.memoryUsage();
return `${(mem.heapUsed / 1000 / 1000).toFixed(2)} MB / ${(
mem.heapTotal /
1000 /
1000
).toFixed(2)} MB / ${(mem.rss / 1000 / 1000).toFixed(2)} MB`;
}
);

0
src/commands/commands/utility/msg.ts Normal file → Executable file
View File

4
src/commands/commands/utility/permissions.ts Normal file → Executable file
View File

@ -18,9 +18,7 @@ export const permissions = new Command(
if (msg.argv[1]) role = fuzzyFindRoleByDisplayName(msg.argv[1]);
if (role) {
return `Permissions for role "${
role.displayName
}": ${role.permissions.join(" | ")}`;
return `Permissions for role \`${role.displayName}\`: \`${role.permissions.join("\` | \`")}\``;
} else {
return `No role found.`;
}

0
src/commands/commands/utility/role.ts Normal file → Executable file
View File

0
src/commands/commands/utility/uptime.ts Normal file → Executable file
View File

26
src/commands/index.ts Normal file → Executable file
View File

@ -1,5 +1,4 @@
import { CommandGroup } from "./CommandGroup";
import { CommandHandler } from "./CommandHandler";
import { about } from "./commands/general/about";
import { help } from "./commands/general/help";
import { id } from "./commands/utility/id";
@ -16,20 +15,27 @@ import { uptime } from "./commands/utility/uptime";
import { balance } from "./commands/economy/balance";
import { permissions } from "./commands/utility/permissions";
import { branch } from "./commands/utility/branch";
import { tree } from "./commands/economy/tree";
import { pick } from "./commands/economy/pick";
import { grow } from "./commands/economy/grow";
import { delinv } from "./commands/utility/delinv";
import { eat } from "./commands/fun/eat";
import { bake } from "./commands/economy/bake";
import { crown } from "./commands/utility/crown";
export function loadCommands() {
// cringe
const general = new CommandGroup("general", "⭐ General");
general.addCommands([help, about]);
CommandHandler.addCommandGroup(general);
globalThis.commandHandler.addCommandGroup(general);
const economy = new CommandGroup("economy", "💸 Economy");
economy.addCommands([inventory, balance]);
CommandHandler.addCommandGroup(economy);
economy.addCommands([inventory, balance, tree, pick, grow, bake]);
globalThis.commandHandler.addCommandGroup(economy);
const fun = new CommandGroup("fun", "✨ Fun");
fun.addCommands([magic8ball]);
CommandHandler.addCommandGroup(fun);
fun.addCommands([magic8ball, eat]);
globalThis.commandHandler.addCommandGroup(fun);
const utility = new CommandGroup("utility", "🔨 Utility");
utility.addCommands([
@ -43,7 +49,11 @@ export function loadCommands() {
ic,
uptime,
permissions,
branch
branch,
delinv,
crown
]);
CommandHandler.addCommandGroup(utility);
globalThis.commandHandler.addCommandGroup(utility);
}
export { CommandGroup };

109
src/data/inventory.ts Normal file → Executable file
View File

@ -1,5 +1,7 @@
import { Inventory } from "@prisma/client";
import { prisma } from "./prisma";
import { JsonArray } from "@prisma/client/runtime/library";
import { Item, StackableItem } from "../economy/Item";
export async function createInventory(data: Omit<Inventory, "id">) {
await prisma.inventory.create({
@ -14,13 +16,118 @@ export async function readInventory(userId: Inventory["userId"]) {
return await prisma.inventory.findUnique({ where: { userId: userId } });
}
export async function readItems(userId: Inventory["userId"]) {
const inv = await readInventory(userId);
if (!inv) return null;
console.log("bruh", inv.items, "end bruh");
// prisma why? pick one!!!
if (typeof inv.items !== "string") return inv.items as unknown as Item[];
return JSON.parse(inv.items) as Item[];
}
export function collapseInventory(inventoryData: Item[]) {
let newItems: Item[] = [];
oldLoop:
for (let i of inventoryData) {
let found = false;
newLoop:
for (let j of newItems) {
if (i.id === j.id) {
// Merge
if (
typeof (i as StackableItem).count === "number" &&
typeof (j as StackableItem).count === "number"
) {
(i as StackableItem).count += (j as StackableItem).count;
}
found = true;
break newLoop;
}
}
// Add
if (!found) newItems.push(i);
}
for (let i = 0; i < inventoryData.length; i++) {
if (i <= 0) continue;
if (inventoryData[i].id === inventoryData[i - 1].id) {
if (
typeof (inventoryData[i - 1] as StackableItem).count ===
"number" &&
typeof (inventoryData[i] as StackableItem).count === "number"
) {
(inventoryData[i - 1] as StackableItem).count += (
inventoryData[i] as StackableItem
).count;
inventoryData.splice(i, 1);
i--;
}
}
}
}
export async function updateInventory(data: Omit<Inventory, "id">) {
collapseInventory(data.items as unknown as Item[]);
return await prisma.inventory.update({
where: { userId: data.userId },
data: {}
data: {
balance: data.balance,
items: data.items as JsonArray
}
});
}
export async function deleteInventory(userId: Inventory["userId"]) {
return await prisma.inventory.delete({ where: { userId } });
}
export async function addItem<T extends Item>(
userId: Inventory["userId"],
item: T
) {
let inventory = await readInventory(userId);
if (!inventory) return false;
(inventory.items as unknown as Item[]).push(item);
collapseInventory(inventory.items as unknown as Item[]);
await updateInventory(inventory);
return true;
}
export async function subtractItem<T extends Item>(
userId: Inventory["userId"],
item: T
) {
let inventory = await readInventory(userId);
if (!inventory) return false;
if ((item as unknown as StackableItem).count) {
const it = (inventory.items as unknown as Item[]).find(
it => it.id == item.id
);
if (!it) return false;
(it as StackableItem).count--;
} else {
const it = (inventory.items as unknown as Item[]).find(
it => it.id == item.id
);
if (!it) return false;
(inventory.items as unknown as Item[]).splice(
(inventory.items as unknown as Item[]).indexOf(it),
1
);
}
await updateInventory(inventory);
return true;
}

50
src/data/prisma.ts Normal file → Executable file
View File

@ -1,3 +1,51 @@
import { PrismaClient } from "@prisma/client";
import { JsonObject } from "@prisma/client/runtime/library";
export const prisma = new PrismaClient();
declare global {
var prisma: PrismaClient;
}
globalThis.prisma ??= new PrismaClient();
export const prisma = globalThis.prisma;
export async function set(key: string, value: any) {
const store = await globalThis.prisma.keyValueStore.findUnique({
where: { id: 1 }
});
if (!store) {
// throw new Error("Unable to access key-value store.");
await prisma.keyValueStore.create({
data: {}
});
return set(key, value);
}
const data = store.data as JsonObject;
data[key] = value;
await globalThis.prisma.keyValueStore.update({
where: { id: 1 },
data: { data: data }
});
return;
}
export async function get<T = unknown>(key: string) {
const store = await globalThis.prisma.keyValueStore.findUnique({
where: { id: 1 }
});
if (!store) {
// throw new Error("Unable to access key-value store.");
await globalThis.prisma.keyValueStore.create({
data: {}
});
return get(key);
}
const data = store.data as JsonObject;
return data[key] as T;
}

0
src/data/user.ts Normal file → Executable file
View File

15
src/economy/Balance.ts Normal file → Executable file
View File

@ -1,3 +1,4 @@
import { readInventory, updateInventory } from "../data/inventory";
import { loadConfig } from "../util/config";
export const balanceConfig = loadConfig("config/balance.yml", {
@ -16,3 +17,17 @@ export function formatBalance(
if (after) return `${balance.toFixed(cutoff)}${symbol}`;
else return `${symbol}${balance.toFixed(cutoff)}`;
}
export async function getBalance(id: string) {
const inventory = await readInventory(id);
if (!inventory) return balanceConfig.defaultBalance;
return inventory.balance;
}
export async function setBalance(id: string, balance: number) {
const inventory = await readInventory(id);
if (!inventory) return false;
inventory.balance = balance;
await updateInventory(inventory);
return true;
}

19
src/economy/Item.ts Normal file → Executable file
View File

@ -6,3 +6,22 @@ export interface Item {
export interface StackableItem extends Item {
count: number;
}
export interface ConsumableItem extends Item {
consumable: true;
}
export interface FoodItem extends ConsumableItem {
edible: true;
}
export interface CakeItem extends FoodItem {
emoji: string;
icing: string;
filling: string;
}
export interface ShopItem extends Item {
buyValue: number;
sellValue: number;
}

32
src/economy/baking/cakes.ts Executable file
View File

@ -0,0 +1,32 @@
import { type CakeItem } from "../Item";
export const cakes: CakeItem[] = [
{
id: "cake",
name: "Chocolate Cake",
emoji: "🎂",
icing: "chocolate",
filling: "chocolate",
edible: true,
consumable: true,
},
{
id: "cake",
name: "Vanilla Cake",
emoji: "🎂",
icing: "vanilla",
filling: "vanilla",
edible: true,
consumable: true,
},
{
id: "cake",
name: "Strawberry Cake",
emoji: "🍓",
icing: "strawberry",
filling: "strawberry",
edible: true,
consumable: true,
}
];

93
src/economy/baking/index.ts Executable file
View File

@ -0,0 +1,93 @@
import { User } from "@prisma/client";
import { cakes } from "./cakes";
import { ServiceAgent } from "../../services/ServiceAgent";
import { StackableItem } from "../Item";
import { Logger } from "../../util/Logger";
import { addItem } from "../../data/inventory";
import { readUser } from "../../data/user";
export interface BakingUser {
id: User["id"];
client_id: string;
}
export const bakingUsers = new Array<BakingUser>();
export function getRandomCake() {
return cakes[Math.floor(Math.random() * cakes.length)];
}
export function isBaking(userId: User["id"]) {
return bakingUsers.find(u => u.id === userId) !== undefined;
}
const alreadyBakingAnswers = [
"You are already baking, dummy.",
"Though it seems you don't care, you are already baking a cake, and you can't start baking another one.",
"Baking is an art that requires patience, which you do not have. In other words, you are already baking, dummy.",
"You shouldn't be baking any more cakes than one at a time.",
"You happen to be baking already, and you would need another oven to bake another cake, and there is only one oven in your house.",
"Since you don't seem to get it, baking can not be started again while you are already baking.",
"You are already baking.",
"Baking is something you are already doing.",
"Baking should be something you do once in a while, not constantly.",
"You are currently baking a cake, which means the oven is already in use, so you can't start baking another one."
];
export function startBaking(userId: User["id"], clientId: string) {
// Already baking?
if (isBaking(userId)) return alreadyBakingAnswers[Math.floor(Math.random() * alreadyBakingAnswers.length)];
// Add to baking list
bakingUsers.push({
id: userId,
client_id: clientId
});
return "You started baking a cake.";
}
export function stopBaking(userId: User["id"]) {
if (!isBaking(userId)) return "You are not baking.";
bakingUsers.splice(bakingUsers.findIndex(u => u.id === userId), 1);
return "You stopped baking a cake.";
}
export function getRandomBaker() {
return bakingUsers[Math.floor(Math.random() * bakingUsers.length)];
}
export function getClient(clientId: string) {
const agent = globalThis.serviceLoader.getAgent(0) as ServiceAgent<unknown>;
return agent;
}
const logger = new Logger("baking");
setInterval(async () => {
const r = Math.random();
//logger.debug(`Baking check: ${r}`);
if (r < 0.03) {
// Get a random baker and send them a cake and a message stating they finished baking
const baker = getRandomBaker();
if (!baker) return;
const client = getClient(baker.client_id);
bakingUsers.splice(bakingUsers.findIndex(u => u.id === baker.id), 1);
if (client) {
const cake = getRandomCake();
const user = await readUser(baker.id);
if (!user) return void client.emit("send chat", "Something terrible happened when I tried to give someone a cake");
client.emit("send chat", `@${user.platformId} finished baking and got ${cake.name}${(cake as unknown as StackableItem).count
? " " + `(x${(cake as unknown as StackableItem).count})` : ""}.`);
// Add cake to inventory
await addItem(baker.id, cake);
}
}
}, 1000);

35
src/economy/eatBhv.ts Executable file
View File

@ -0,0 +1,35 @@
import type { CommandMessage } from "../commands/CommandHandler";
import type { ServiceAgent } from "../services/ServiceAgent";
import { formatBalance, getBalance, setBalance } from "./Balance";
import type { CakeItem, Item } from "./Item";
export interface IEatReply {
output?: string;
consumed: boolean;
}
export type TEatBhv = (msg: CommandMessage, agent: ServiceAgent<unknown>, item: Item) => Promise<IEatReply>;
export const eatBhv = new Map<Item["id"], TEatBhv>();
eatBhv.set("kekklefruit", async (msg, agent) => {
return {
output: "kek eat bhv test",
consumed: true
};
});
eatBhv.set("cake", async (msg, agent, item) => {
const cake = item as CakeItem;
const balance = await getBalance(msg.user.id);
if (typeof balance !== "number") return { output: "Somehow, you don't have a wallet to put stuff in, so you can't eat cake.", consumed: false };
const money = Math.floor(Math.random() * 500);
await setBalance(msg.user.id, balance + money);
return {
output: `You ate ${item.name} and your stomach turned it into ${formatBalance(money)}.`,
consumed: true
};
});

15
src/economy/items.ts Executable file
View File

@ -0,0 +1,15 @@
import { Item } from "./Item";
const items = new Map<string, Item>();
export function getItem(key: string) {
return items.get(key);
}
export function setItem(key: string, item: Item) {
return items.set(key, item);
}
export function deleteItem(key: string) {
return items.delete(key);
}

56
src/economy/kekkle.ts Executable file
View File

@ -0,0 +1,56 @@
import { get, set } from "../data/prisma";
import { Logger } from "../util/Logger";
import type { FoodItem, StackableItem } from "./Item";
export class KekklefruitTree {
protected static fruit: number = 0;
public static logger = new Logger("Kekklefruit Tree");
public static async saveFruit() {
return set("kekklefruit-tree", this.fruit);
}
public static async loadFruit() {
let fruit = await get<number>("kekklefruit-tree");
let save = false;
if (!fruit) {
fruit = 0;
save = true;
}
this.fruit = fruit;
if (save) this.saveFruit();
}
public static async growFruit(amount: number = 1) {
this.fruit += amount;
await this.saveFruit();
}
public static getFruitCount() {
return this.fruit;
}
public static async pickFruit() {
if (this.fruit > 0) {
this.fruit--;
await this.saveFruit();
return this.randomFruit();
} else {
return undefined;
}
}
public static randomFruit() {
return {
id: "kekklefruit",
name: "Kekklefruit",
consumable: true,
edible: true,
count: 1
} as FoodItem & StackableItem;
}
}
await KekklefruitTree.loadFruit();

52
src/index.ts Normal file → Executable file
View File

@ -1,12 +1,54 @@
import { loadCommands } from "./commands";
import { loadCommands, type CommandGroup } from "./commands";
import { CommandHandler } from "./commands/CommandHandler";
import { loadRoleConfig } from "./permissions";
import { ServiceLoader } from "./services";
import { ConsoleAgent } from "./services/console";
import { printStartupASCII } from "./util/ascii";
printStartupASCII();
loadRoleConfig();
loadCommands();
ServiceLoader.loadServices();
// Hot reload persistence
declare global {
var loaded: boolean;
var serviceLoader: any;
}
globalThis.loaded ??= false;
globalThis.serviceLoader ??= ServiceLoader;
function load() {
printStartupASCII();
loadRoleConfig();
loadCommands();
globalThis.serviceLoader.loadServices();
globalThis.loaded = true;
}
function reload() {
console.log("Reloading...");
// Reload commands
globalThis.commandHandler.commandGroups = new Array<CommandGroup>();
loadCommands();
// Reload services
globalThis.serviceLoader.unloadServices();
globalThis.serviceLoader.loadServices();
// Set console prompt
globalThis.serviceLoader.agents.forEach(agent => {
if (agent.platform === "console")
(agent as ConsoleAgent).client.prompt();
});
}
// Check for hot reload
if (!globalThis.loaded) {
load();
} else {
console.clear();
console.log("Hot reload triggered");
reload();
}
export function scopedEval(code: string) {
return eval(code);

2
src/permissions/default.ts Normal file → Executable file
View File

@ -9,6 +9,8 @@ export const defaultConfig = {
"cosmic.command.inventory",
"cosmic.command.balance",
"cosmic.command.bake",
"cosmic.command.eat",
"cosmic.command.magic8ball",

15
src/permissions/index.ts Normal file → Executable file
View File

@ -23,9 +23,9 @@ export function handlePermission(node1: string, node2: string) {
// Check nodes in order
for (let i = 0; i < hierarchy1.length; i++) {
if (hierarchy1[i] == hierarchy2[i]) {
if (hierarchy1[i] === hierarchy2[i]) {
// Last node?
if (i == hierarchy1.length - 1 || i == hierarchy2.length - 1) {
if (i === hierarchy1.length - 1 || i === hierarchy2.length - 1) {
return true;
} else {
continue;
@ -33,8 +33,8 @@ export function handlePermission(node1: string, node2: string) {
}
// Wildcard?
if (hierarchy1[i] == "*") return true;
if (hierarchy2[i] == "*") return true;
if (hierarchy1[i] === "*") return true;
if (hierarchy2[i] === "*") return true;
return false;
}
@ -66,7 +66,12 @@ export function hasPermission(role: Role, permission: string) {
return false;
}
export const roles = new Map<Role, TRole>();
declare global {
var roles: Map<Role, TRole>;
}
globalThis.roles ??= new Map<Role, TRole>();
export const roles = globalThis.roles;
export type TRole = {
displayName: string;

0
src/services/ServiceAgent.ts Normal file → Executable file
View File

37
src/services/console/MicroHandler.ts Normal file → Executable file
View File

@ -6,7 +6,8 @@ import { BaseCommandMessage } from "../../commands/CommandHandler";
import { readUser, updateUser } from "../../data/user";
import { CosmicColor } from "../../util/CosmicColor";
import { ServiceAgent } from "../ServiceAgent";
import { MPPAgent } from "../mpp";
import { MPPNetAgent } from "../mppnet";
import { readFileSync } from "fs";
export interface ChatMessage<T = unknown> {
m: "a";
@ -22,7 +23,7 @@ export interface ChatMessage<T = unknown> {
function onChildMessage(msg: ChatMessage) {
const consoleAgent = ServiceLoader.agents.find(
ag => ag.platform == "console"
ag => ag.platform === "console"
) as ConsoleAgent | undefined;
if (!consoleAgent) return;
@ -34,7 +35,7 @@ function onChildMessage(msg: ChatMessage) {
function onConsoleMessage(text: string) {
const consoleAgent = ServiceLoader.agents.find(
ag => ag.platform == "console"
ag => ag.platform === "console"
) as ConsoleAgent | undefined;
if (!consoleAgent) return;
@ -55,7 +56,7 @@ export class MicroHandler {
case "commands":
case "cmds":
default:
return "Microcommands: /help | /js <expr> | /exit | /list | /view <index> | /unview | /admin+ <id> | /admin- <id> | /owner+ <id>";
return "Microcommands: /help | /js <expr> | /exit | /list | /view <index> | /unview | /admin+ <id> | /admin- <id> | /owner+ <id> | /owner- <id>";
break;
case "js":
case "eval":
@ -77,11 +78,10 @@ export class MicroHandler {
for (let i in ServiceLoader.agents) {
const agent2 = ServiceLoader.agents[i];
if (agent2.platform == "mpp") {
if (agent2.platform === "mpp") {
agent.emit(
"log",
`${i} - ${agent2.platform} - ${
(agent2 as MPPAgent).desiredChannel
`${i} - ${agent2.platform} - ${(agent2 as MPPNetAgent).desiredChannel
}`
);
} else {
@ -101,7 +101,7 @@ export class MicroHandler {
let walkie = agent as ConsoleAgent;
let talky = ServiceLoader.agents[index];
if (index == ServiceLoader.agents.indexOf(walkie))
if (index === ServiceLoader.agents.indexOf(walkie))
return "Why would you want to chat with yourself?";
// Remove old listeners
@ -150,11 +150,10 @@ export class MicroHandler {
if (conAg.viewAgent.platform !== "mpp")
return "The view agent is not on MPP.";
const ppl = (conAg.viewAgent as MPPAgent).client.ppl;
const ppl = (conAg.viewAgent as MPPNetAgent).client.ppl;
return `MPP Users: ${Object.values(ppl).map(
p =>
`\n - ${p._id} (user) / ${p.id} (part): ${p.name} (${
p.color
`\n - ${p._id} (user) / ${p.id} (part): ${p.name} (${p.color
}, ${new CosmicColor(p.color).getName()})`
)}`;
break;
@ -206,6 +205,22 @@ export class MicroHandler {
6
)}...] an owner`;
break;
case "owner-":
const userId4 = command.argv
.slice(1, command.argv.length)
.join(" ");
let user4 = await readUser(userId4);
if (!user4) return "No such user.";
user4.role = Role.NONE;
await updateUser(user4);
return `Made user "${user4.name}" [${user4.platformId.substring(
0,
6
)}] a normal user.`;
break;
}
}
}

9
src/services/console/index.ts Normal file → Executable file
View File

@ -32,12 +32,14 @@ export class ConsoleAgent extends ServiceAgent<readline.ReadLine> {
this.started = true;
this.client.setPrompt("> ");
this.client.prompt(true);
(globalThis as any).readline = this.client;
}
public stop() {
if (!this.started) return;
this.started = false;
this.client.setPrompt("");
this.client.close();
delete (globalThis as any).readline;
}
protected bindEventListeners(): void {
@ -65,7 +67,10 @@ export class ConsoleAgent extends ServiceAgent<readline.ReadLine> {
if (text.startsWith("/")) {
out = await MicroHandler.handleMicroCommand(message, this);
} else {
out = await CommandHandler.handleCommand(message, this);
out = await globalThis.commandHandler.handleCommand(
message,
this
);
}
if (out) {

4
src/services/discord/index.ts Normal file → Executable file
View File

@ -56,7 +56,7 @@ export class DiscordAgent extends ServiceAgent<Discord.Client> {
let args = msg.content.split(" ");
const str = await CommandHandler.handleCommand(
const str = await globalThis.commandHandler.handleCommand(
{
m: "command",
a: msg.content,
@ -74,7 +74,7 @@ export class DiscordAgent extends ServiceAgent<Discord.Client> {
);
if (str) {
if (typeof str == "string") {
if (typeof str === "string") {
const channel = await this.client.channels.fetch(
msg.channelId
);

25
src/services/index.ts Normal file → Executable file
View File

@ -1,4 +1,4 @@
import { MPPAgent } from "./mpp";
import { MPPNetAgent } from "./mppnet";
import env from "../util/env";
import { ServiceAgent } from "./ServiceAgent";
import { loadConfig } from "../util/config";
@ -50,18 +50,22 @@ export class ServiceLoader {
this.agents.push(agent);
}
public static getAgent(index: number) {
return this.agents[index];
}
public static loadServices() {
if (config.enableMPP) {
for (const uri of Object.keys(mppConfig.agents)) {
for (const channel of mppConfig.agents[uri]) {
const mppAgent = new MPPAgent(
const mppAgent = new MPPNetAgent(
uri,
channel.id,
channel.overrideName
? {
name: channel.overrideName,
color: mppConfig.desiredUser.color
}
name: channel.overrideName,
color: mppConfig.desiredUser.color
}
: mppConfig.desiredUser,
env.MPPNET_TOKEN,
config.debug
@ -96,4 +100,15 @@ export class ServiceLoader {
this.addAgent(consoleAgent);
}
}
public static unloadServices() {
for (const agent of this.agents) {
agent.stop();
this.agents.splice(this.agents.indexOf(agent), 1);
}
}
public static getAgentId(agent: ServiceAgent<unknown>) {
return this.agents.indexOf(agent);
}
}

View File

@ -1,4 +1,4 @@
import { MPPAgent } from ".";
import { MPPNetAgent } from ".";
interface Vector2 {
x: number;
@ -20,12 +20,22 @@ interface CursorProps {
}
export class Cursor {
public static animations = new Map<string, (cursor: Cursor) => void>();
public visible: boolean = true;
public displayInterval: NodeJS.Timeout;
public updateInterval: NodeJS.Timeout;
public animationLoop = [
"bounce",
"bounce2",
"constrained",
"lemniscate",
"test",
];
public props: CursorProps = {
currentAnimation: "lemniscate",
currentAnimation: this.animationLoop[0],
position: {
x: 50,
y: 50
@ -50,7 +60,7 @@ export class Cursor {
following: ""
};
constructor(public agent: MPPAgent) {
constructor(public agent: MPPNetAgent) {
this.displayInterval = setInterval(() => {
if (
this.props.oldPosition.x !== this.props.position.x ||
@ -66,6 +76,13 @@ export class Cursor {
}
}, 1000 / 20);
let animationIndex = 0;
const animationInterval = setInterval(() => {
animationIndex++;
if (animationIndex >= this.animationLoop.length) animationIndex = 0;
this.props.currentAnimation = this.animationLoop[animationIndex];
}, 10000);
this.updateInterval = setInterval(() => {
switch (this.props.currentAnimation) {
case "bounce":
@ -88,6 +105,11 @@ export class Cursor {
this.props.velocity.y +=
this.props.acceleration.y * this.props.dt;
if (this.props.velocity.x > 50) this.props.velocity.x = 50;
if (this.props.velocity.x < -50) this.props.velocity.x = -50;
if (this.props.velocity.y > 50) this.props.velocity.y = 50;
if (this.props.velocity.y < -50) this.props.velocity.y = -50;
this.props.position.x +=
this.props.velocity.x * this.props.dt;
this.props.position.y +=
@ -135,7 +157,7 @@ export class Cursor {
if (this.props.position.y < 75) {
this.props.acceleration.y =
Math.random() * 100 - 50 - this.props.gravity;
((Math.random() * 50) - 25) - this.props.gravity;
} else {
this.props.acceleration.y = -(Math.random() * 50);
}
@ -239,7 +261,7 @@ export class Cursor {
50;
this.props.position.y =
Math.sin(this.props.angles[0] * (Math.PI / 180) * 3) *
10 +
10 +
50;
break;
@ -254,7 +276,7 @@ export class Cursor {
50;
this.props.position.y =
Math.sin(this.props.angles[0] * (Math.PI / 180) * 2) *
10 +
10 +
50;
break;
@ -296,7 +318,7 @@ export class Cursor {
break;
}
}, 1000 / 60);
}, 1500 / 60);
}
public show() {

View File

@ -5,7 +5,7 @@ import { Cursor } from "./Cursor";
import { ChatMessage } from "../console/MicroHandler";
import { help as helpCommand } from "../../commands/commands/general/help";
export class MPPAgent extends ServiceAgent<Client> {
export class MPPNetAgent extends ServiceAgent<Client> {
public cursor: Cursor;
constructor(
@ -18,6 +18,7 @@ export class MPPAgent extends ServiceAgent<Client> {
const cl = new Client(uri, token);
super("mpp", cl);
this.cursor = new Cursor(this);
this.cursor.show();
}
public start() {
@ -33,11 +34,14 @@ export class MPPAgent extends ServiceAgent<Client> {
this.client.on("hi", msg => {
this.client.setChannel(this.desiredChannel);
});
this.client.on("ch", msg => {
this.fixUser();
});
this.client.on("t", msg => {
this.fixUser();
// this.fixUser();
});
this.client.on("a", async msg => {
@ -61,7 +65,7 @@ export class MPPAgent extends ServiceAgent<Client> {
let args = msg.a.split(" ");
// Run command and get output
const str = await CommandHandler.handleCommand(
const str = await globalThis.commandHandler.handleCommand(
{
m: "command",
a: msg.a,
@ -80,7 +84,7 @@ export class MPPAgent extends ServiceAgent<Client> {
// Send message in chat
if (str) {
if (typeof str == "string") {
if (typeof str === "string") {
if (str.includes("\n")) {
let sp = str.split("\n");

0
src/services/switchchat/index.ts Normal file → Executable file
View File

6
src/util/CosmicColor.ts Normal file → Executable file
View File

@ -80,9 +80,9 @@ export class CosmicColor {
let g = (~~this.g || 0).toString(16);
let b = (~~this.b || 0).toString(16);
if (r.length == 1) r = "0" + r;
if (g.length == 1) g = "0" + g;
if (b.length == 1) b = "0" + b;
if (r.length === 1) r = "0" + r;
if (g.length === 1) g = "0" + g;
if (b.length === 1) b = "0" + b;
return "#" + r + g + b;
}

6
src/util/Logger.ts Normal file → Executable file
View File

@ -17,6 +17,8 @@ export function padNum(
export class Logger {
private static log(method: string, ...args: any[]) {
process.stdout.write(`\x1b[2K\r`);
(console as unknown as Record<string, (..._args: any[]) => any>)[
method
](
@ -24,6 +26,10 @@ export class Logger {
unimportant(this.getHHMMSSMS()),
...args
);
if ((globalThis as any).readline) {
(globalThis as any).readline.prompt();
}
}
public static getHHMMSSMS() {

0
src/util/ascii.ts Normal file → Executable file
View File

8
src/util/config.ts Normal file → Executable file
View File

@ -16,6 +16,7 @@ import { parse as parsePath } from "path/posix";
* @returns Parsed YAML config
*/
export function loadConfig<T>(configPath: string, defaultConfig: T): T {
console.time(`Loading config ${configPath}`);
// Config exists?
if (existsSync(configPath)) {
// Load config
@ -30,12 +31,12 @@ export function loadConfig<T>(configPath: string, defaultConfig: T): T {
obj2: Record<string, unknown>
) {
for (const key of Object.keys(obj2)) {
if (typeof obj[key] == "undefined") {
if (typeof obj[key] === "undefined") {
obj[key] = obj2[key];
changed = true;
}
if (typeof obj[key] == "object" && !Array.isArray(obj[key])) {
if (typeof obj[key] === "object" && !Array.isArray(obj[key])) {
mix(
obj[key] as Record<string, unknown>,
obj2[key] as Record<string, unknown>
@ -45,7 +46,7 @@ export function loadConfig<T>(configPath: string, defaultConfig: T): T {
}
// Apply any missing default values
mix(config, defRecord);
// mix(config, defRecord);
// Save config if modified
if (changed) writeConfig(configPath, config);
@ -54,6 +55,7 @@ export function loadConfig<T>(configPath: string, defaultConfig: T): T {
} else {
// Write default config to disk and use that
writeConfig(configPath, defaultConfig);
return defaultConfig as T;
}
}

0
src/util/env.ts Normal file → Executable file
View File

0
src/util/git.ts Normal file → Executable file
View File

23
test/data/inventory.test.ts Executable file
View File

@ -0,0 +1,23 @@
import { expect, test } from "bun:test";
import { collapseInventory } from "../../src/data/inventory";
import { StackableItem } from "../../src/economy/Item";
test("Collapse inventory", () => {
let sampleData: StackableItem[] = [
{
id: "test_item",
name: "Test Item",
count: 10
},
{
id: "test_item",
name: "Test Item",
count: 15
}
];
collapseInventory(sampleData);
expect(sampleData[0].count).toBe(25);
expect(sampleData[1]).toBe(undefined);
console.log(sampleData);
});

0
tsconfig.json Normal file → Executable file
View File