From c73a4d2abf819f143af55ff1d315951b442d308f Mon Sep 17 00:00:00 2001 From: Hri7566 Date: Tue, 15 Oct 2024 05:01:21 -0400 Subject: [PATCH] Rework behaviors --- build/index.js | 5 +- config/locations.yml | 33 +- config/talkomatic_bots.yml | 3 +- src/api/behavior/bhv.d.ts | 62 ++ src/api/behavior/index.ts | 72 +++ src/api/behavior/objects/burger.ts | 76 +++ src/api/behavior/objects/fish.ts | 39 ++ src/api/behavior/objects/kekklefruit.ts | 14 + src/api/behavior/objects/sand.ts | 11 + src/api/behavior/register.ts | 5 + src/api/cli/commands.ts | 2 + src/api/cli/commands/grow_fruit.ts | 20 + src/api/commands/BehaviorCommand.ts | 46 ++ src/api/commands/groups/general/info.ts | 2 +- src/api/commands/groups/index.ts | 6 +- src/api/commands/groups/inventory/eat.ts | 69 ++- src/api/commands/groups/inventory/yeet.ts | 574 ++++++++++-------- .../groups/{util => pokemon}/pokedex.ts | 0 .../groups/{inventory => pokemon}/pokemon.ts | 0 src/api/index.ts | 6 +- src/api/items/behavior/defaults.ts | 13 - src/api/items/behavior/index.ts | 26 - src/api/items/behavior/items/fish.ts | 41 -- src/api/items/behavior/items/kekklefruit.ts | 16 - src/util/types.d.ts | 31 +- test/api/behavior/index.test.ts | 60 ++ 26 files changed, 810 insertions(+), 422 deletions(-) create mode 100644 src/api/behavior/bhv.d.ts create mode 100644 src/api/behavior/index.ts create mode 100644 src/api/behavior/objects/burger.ts create mode 100644 src/api/behavior/objects/fish.ts create mode 100644 src/api/behavior/objects/kekklefruit.ts create mode 100644 src/api/behavior/objects/sand.ts create mode 100644 src/api/behavior/register.ts create mode 100644 src/api/cli/commands/grow_fruit.ts create mode 100644 src/api/commands/BehaviorCommand.ts rename src/api/commands/groups/{util => pokemon}/pokedex.ts (100%) rename src/api/commands/groups/{inventory => pokemon}/pokemon.ts (100%) delete mode 100644 src/api/items/behavior/defaults.ts delete mode 100644 src/api/items/behavior/index.ts delete mode 100644 src/api/items/behavior/items/fish.ts delete mode 100644 src/api/items/behavior/items/kekklefruit.ts create mode 100644 test/api/behavior/index.test.ts diff --git a/build/index.js b/build/index.js index 38f7485..2732043 100644 --- a/build/index.js +++ b/build/index.js @@ -17450,7 +17450,10 @@ var TalkomaticBot = class extends import_node_events.EventEmitter { async start() { this.logger.info("Starting"); this.client.connect(); - let data = await this.findChannel(this.config.channel.name) || await this.createChannel(this.config.channel.name, "private"); + let data = await this.findChannel(this.config.channel.name) || await this.createChannel( + this.config.channel.name, + this.config.channel.type + ); this.logger.debug(data); if (typeof data !== "undefined") { try { diff --git a/config/locations.yml b/config/locations.yml index 6fe0980..4f46342 100644 --- a/config/locations.yml +++ b/config/locations.yml @@ -1,32 +1,41 @@ - id: pond name: Pond nearby: - - lake - - river - - sea + - lake + - river + - sea + - shop hasSand: true objects: [] - id: lake name: Lake nearby: - - pond - - river - - sea + - pond + - river + - sea hasSand: false objects: [] - id: river name: River nearby: - - pond - - lake - - sea + - pond + - lake + - sea hasSand: false objects: [] - id: sea name: Sea nearby: - - pond - - lake - - river + - pond + - lake + - river hasSand: true objects: [] +- id: shop + name: Shop + nearby: + - pond +- id: bed # /go sleep? + name: Bed + nearby: + - pond diff --git a/config/talkomatic_bots.yml b/config/talkomatic_bots.yml index 627a01a..a7b8b01 100644 --- a/config/talkomatic_bots.yml +++ b/config/talkomatic_bots.yml @@ -1,2 +1,3 @@ - channel: - name: test/fishing + name: test/fishing + type: public diff --git a/src/api/behavior/bhv.d.ts b/src/api/behavior/bhv.d.ts new file mode 100644 index 0000000..6de9953 --- /dev/null +++ b/src/api/behavior/bhv.d.ts @@ -0,0 +1,62 @@ +interface IBehaviorValidResponse { + success: true; + + // shouldRemove: boolean; + // and?: string; + state: State; +} + +interface IBehaviorErrorResponse { + success: false; + err: string; +} + +type TBehaviorResponse = + | IBehaviorValidResponse + | IBehaviorErrorResponse; + +type TBehaviorCallback = ( + context: Context +) => Promise>; +type TBehavior = Record< + string, + TBehaviorCallback +>; +type TBehaviorMap = Map>; //Record>; + +type TBehaviorNamespace = string; +type TBehaviorAction = keyof IBehaviorContextStateDefinitions; +type TBehaviorID = `${TBehaviorNamespace}:${TBehaviorAction}`; + +interface IBehaviorDefinition { + id: string; + bhv: TBehavior; +} + +declare interface IBehaviorContextStateDefinitions + extends Record> { + eat: { + context: { + id: string; + part: IPart; + object: IObject; + user: User; + }; + + state: { + shouldRemove: boolean; + and?: string; + }; + }; + + yeet: { + context: { + part: IPart; + }; + + state: { + shouldRemove: boolean; + text: string; + }; + }; +} diff --git a/src/api/behavior/index.ts b/src/api/behavior/index.ts new file mode 100644 index 0000000..974e6db --- /dev/null +++ b/src/api/behavior/index.ts @@ -0,0 +1,72 @@ +import { logger } from "@server/commands/handler"; + +export const behaviorMap = new Map>(); + +class BehaviorError extends Error { + constructor(...args: string[]) { + super(...args); + } +} + +function splitBehaviorID(id: TBehaviorID) { + const args = id.split(":"); + + if (typeof args[0] == "undefined" || typeof args[1] == "undefined") + throw new BehaviorError( + "Incomplete behavior ID (should have exactly two segments)" + ); + + if (typeof args[0] !== "string" || typeof args[1] !== "string") + throw new BehaviorError( + `Invalid behavior ID (expected: string:string, got: ${typeof args[0]}:${typeof args[1]})` + ); + + if (typeof args[3] !== "undefined") + throw new BehaviorError("Invalid behavior ID: Too many segments"); + + return args; +} + +export function registerBehavior< + K extends keyof IBehaviorContextStateDefinitions, + Context = IBehaviorContextStateDefinitions[K]["context"], + State = IBehaviorContextStateDefinitions[K]["state"] +>(id: TBehaviorID, behavior: TBehaviorCallback) { + try { + const args = splitBehaviorID(id); + const namespace = args[0]; + const action = args[1]; + + if (!behaviorMap.has(namespace)) behaviorMap.set(namespace, {}); + const set = behaviorMap.get(namespace); + if (!set) + throw new BehaviorError("Unable to resolve namespace to value"); + set[action] = behavior as TBehaviorCallback; + } catch (err) { + if (!(err instanceof BehaviorError)) err = "Unknown error"; + throw new BehaviorError("Unable to register behavior: " + err); + } +} + +export async function executeBehavior< + K extends keyof IBehaviorContextStateDefinitions, + Context = IBehaviorContextStateDefinitions[K]["context"], + State = IBehaviorContextStateDefinitions[K]["state"] +>(id: TBehaviorID, context: Context): Promise> { + const args = splitBehaviorID(id); + const namespace = args[0]; + const action = args[1]; + + const set = behaviorMap.get(namespace); + if (!set) throw new BehaviorError("Unable to resolve namespace to value"); + + const callback = set[action]; + + if (!callback) + return { + success: false, + err: "No callback defined for " + id + }; + + return (await callback(context)) as TBehaviorResponse; +} diff --git a/src/api/behavior/objects/burger.ts b/src/api/behavior/objects/burger.ts new file mode 100644 index 0000000..012bed6 --- /dev/null +++ b/src/api/behavior/objects/burger.ts @@ -0,0 +1,76 @@ +import { addBack } from "@server/backs"; +import { CosmicColor } from "@util/CosmicColor"; +import { registerBehavior } from ".."; + +registerBehavior<"eat">("burger:eat", async context => { + const r = Math.random(); + + // 2% + if (r < 0.02) { + const color = new CosmicColor("#E5B73B"); + + addBack(context.id, { + m: "color", + id: context.part.id, + color: color.toHexa() + }); + + return { + success: true, + state: { + shouldRemove: true, + and: `and it made him/her turn ${color + .getName() + .toLowerCase()}.` + } + }; + } else { + return { + success: true, + state: { + shouldRemove: true + } + }; + } +}); + +registerBehavior<"yeet">("burger:yeet", async context => { + const name = context.part.name; + + const tossed = [ + `Friend ${name} tossed the meaty burger. It skipped off the water like a pebble.`, + `Friend ${name} yeeted the slimy burger into the stratosphere. It came around the other side of the planet and hit them in the head.`, + `The hamburger gracefully soared through the air.`, + `In a majestic flight, the burger takes off like a plane and floats away into the horizon.`, + `A film crew and director watch ${name} toss a burger from afar. After a few months, the burger wins an Oscar.`, + `Friend ${name} flings a Junior Baconator into the sky with enough force to create a second moon.`, + `The burger launched like a rocket and reaches outer space. Suddenly, a ship full of Kerbonauts crashes into the patty, diverting it back to the planet.`, + `Friend ${name} tosses the burger. Suddenly, the burger is hit by a flying hot dog.`, + `The burger sprouts wings and floats away.`, + `Friend ${name} tosses a burger ${ + Math.trunc(Math.random() * 1000) / 100 + } inches in front of themselves.`, + `Friend ${name} winds up for a big throw and yeets the burger. After travelling the sky for a few seconds, it enters somebody else's car window.`, + `Friend ${name} tosses the burger. The local weather station reports falling food.`, + `The burger goes through a cloud shaped like ${name}'s head.`, + `After a long 10 minutes, the burger comes down from the ceiling.`, + `Friend ${name} tosses the burger like a frisbee and it lands on a nearby roof.`, + `Friend ${name} carelessly sends the burger into a passing seagull's mouth.`, + `Friend ${name} managed to deliver a burger perfectly into the window of a house.`, + `The burger hits the water and a shark fin suddenly appears.`, + `After ${name} hurls the burger, it lands onto a nearby grill.`, + `Friend ${name} flings a burger patty into the atmosphere.`, + `Friend ${name} throws a boomerang-shaped hamburger. It comes back and hits ${name} on the head.`, + `Friend ${name} tosses the burger.`, + `Friend ${name} tosses the burger into the air and it lands on their face.`, + `Friend ${name} tosses the burger to the other side of a rainbow.` + ]; + + return { + success: true, + state: { + shouldRemove: true, + text: tossed[Math.floor(Math.random() * tossed.length)] + } + }; +}); diff --git a/src/api/behavior/objects/fish.ts b/src/api/behavior/objects/fish.ts new file mode 100644 index 0000000..dcc8884 --- /dev/null +++ b/src/api/behavior/objects/fish.ts @@ -0,0 +1,39 @@ +import { addBack } from "@server/backs"; +import { CosmicColor } from "@util/CosmicColor"; +import { registerBehavior } from ".."; + +registerBehavior<"eat">("fish:eat", async context => { + const r = Math.random(); + + // 50% + if (r < 0.5) { + const color = new CosmicColor( + Math.floor(Math.random() * 255), + Math.floor(Math.random() * 255), + Math.floor(Math.random() * 255) + ); + + addBack(context.id, { + m: "color", + id: context.part.id, + color: color.toHexa() + }); + + return { + success: true, + state: { + shouldRemove: true, + and: `and it made him/her turn ${color + .getName() + .toLowerCase()}.` + } + }; + } else { + return { + success: true, + state: { + shouldRemove: true + } + }; + } +}); diff --git a/src/api/behavior/objects/kekklefruit.ts b/src/api/behavior/objects/kekklefruit.ts new file mode 100644 index 0000000..c98cd20 --- /dev/null +++ b/src/api/behavior/objects/kekklefruit.ts @@ -0,0 +1,14 @@ +import { incrementFishingChance } from "@server/fish/fishers"; +import { registerBehavior } from ".."; + +registerBehavior<"eat">("kekklefruit:eat", async context => { + const test = await incrementFishingChance(context.user.id); + + return { + success: true, + state: { + shouldRemove: true, + and: "and got a temporary fishing boost." + } + }; +}); diff --git a/src/api/behavior/objects/sand.ts b/src/api/behavior/objects/sand.ts new file mode 100644 index 0000000..6752333 --- /dev/null +++ b/src/api/behavior/objects/sand.ts @@ -0,0 +1,11 @@ +import { registerBehavior } from ".."; + +registerBehavior<"yeet">("sand:yeet", async context => { + return { + success: true, + state: { + shouldRemove: false, + text: `No, ${context.part.name}, don't yeet sand.` + } + }; +}); diff --git a/src/api/behavior/register.ts b/src/api/behavior/register.ts new file mode 100644 index 0000000..68dcdc2 --- /dev/null +++ b/src/api/behavior/register.ts @@ -0,0 +1,5 @@ +export function registerBehaviors() { + require("./objects/fish"); + require("./objects/kekklefruit"); + require("./objects/burger"); +} diff --git a/src/api/cli/commands.ts b/src/api/cli/commands.ts index 80a8563..97bc90b 100644 --- a/src/api/cli/commands.ts +++ b/src/api/cli/commands.ts @@ -1,6 +1,7 @@ import type { ReadlineCommand } from "./ReadlineCommand"; import { deltoken } from "./commands/deltoken"; import { gentoken } from "./commands/gentoken"; +import { grow_fruit } from "./commands/grow_fruit"; import { help } from "./commands/help"; import { lstoken } from "./commands/lstoken"; import { stop } from "./commands/stop"; @@ -12,3 +13,4 @@ readlineCommands.push(gentoken); readlineCommands.push(deltoken); readlineCommands.push(lstoken); readlineCommands.push(stop); +readlineCommands.push(grow_fruit); diff --git a/src/api/cli/commands/grow_fruit.ts b/src/api/cli/commands/grow_fruit.ts new file mode 100644 index 0000000..5fbfa5d --- /dev/null +++ b/src/api/cli/commands/grow_fruit.ts @@ -0,0 +1,20 @@ +import { getAllTokens } from "@server/data/token"; +import { ReadlineCommand } from "../ReadlineCommand"; +import { growFruit } from "@server/fish/tree"; + +export const grow_fruit = new ReadlineCommand( + "grow_fruit", + ["grow_fruit", "grow", "grow_kekklefruit"], + "Grow kekklefruit on the tree", + "grow_fruit ", + async line => { + try { + const args = line.split(" "); + const num = parseInt(args[1], 10); + await growFruit(num); + return `grew ${num} kekklefruit`; + } catch (err) { + return "bad"; + } + } +); diff --git a/src/api/commands/BehaviorCommand.ts b/src/api/commands/BehaviorCommand.ts new file mode 100644 index 0000000..e18b525 --- /dev/null +++ b/src/api/commands/BehaviorCommand.ts @@ -0,0 +1,46 @@ +import type { User } from "@prisma/client"; +import Command from "./Command"; +import { behaviorMap, executeBehavior } from "@server/behavior"; +import { logger } from "./handler"; + +export class BehaviorCommand extends Command { + constructor( + public id: string, + public aliases: string[], + public description: string, + public usage: string, + public permissionNode: string, + callback: TCommandCallbackWithSelf, + public visible: boolean = true + ) { + super( + id, + aliases, + description, + usage, + permissionNode, + props => callback(props, this), + visible + ); + } + + public async behave< + K extends keyof IBehaviorContextStateDefinitions, + C = IBehaviorContextStateDefinitions[K]["context"], + S = IBehaviorContextStateDefinitions[K]["state"] + >(context: C, objectId: string, defaultCallback: TBehaviorCallback) { + const key = `${objectId}:${this.id}` as TBehaviorID; + let hasBehavior = false; + const container = behaviorMap.get(objectId); + + if (container) hasBehavior = typeof container[this.id] === "function"; + + if (hasBehavior) { + return await executeBehavior(key, context); + } else { + return (await defaultCallback(context)) as TBehaviorResponse; + } + } +} + +export default BehaviorCommand; diff --git a/src/api/commands/groups/general/info.ts b/src/api/commands/groups/general/info.ts index 22d2ea4..f51ed54 100644 --- a/src/api/commands/groups/general/info.ts +++ b/src/api/commands/groups/general/info.ts @@ -3,7 +3,7 @@ import Command from "@server/commands/Command"; export const info = new Command( "info", ["info"], - "Get your own user ID", + "Show information about the bot", "info", "command.general.info", async ({ id, command, args, prefix, part, user }) => { diff --git a/src/api/commands/groups/index.ts b/src/api/commands/groups/index.ts index 98bb5df..1306d86 100644 --- a/src/api/commands/groups/index.ts +++ b/src/api/commands/groups/index.ts @@ -13,10 +13,10 @@ import { eat } from "./inventory/eat"; import { sack } from "./inventory/sack"; import { reel } from "./fishing/reel"; import { memory } from "./util/mem"; -import { pokemon } from "./inventory/pokemon"; +import { pokemon } from "./pokemon/pokemon"; import { color } from "./general/color"; import { autofish } from "./util/autofish"; -import { pokedex } from "./util/pokedex"; +import { pokedex } from "./pokemon/pokedex"; import { myid } from "./general/myid"; import { yeet } from "./inventory/yeet"; import { tree } from "./fishing/tree"; @@ -63,7 +63,7 @@ commandGroups.push(inventoryGroup); const pokemonGroup: ICommandGroup = { id: "pokemon", displayName: "Pokémon", - commands: [daily, pokedex] + commands: [daily, pokemon, pokedex] }; commandGroups.push(pokemonGroup); diff --git a/src/api/commands/groups/inventory/eat.ts b/src/api/commands/groups/inventory/eat.ts index 7ac4cb4..f1137e0 100644 --- a/src/api/commands/groups/inventory/eat.ts +++ b/src/api/commands/groups/inventory/eat.ts @@ -1,16 +1,16 @@ -import Command from "@server/commands/Command"; +import BehaviorCommand from "@server/commands/BehaviorCommand"; import { logger } from "@server/commands/handler"; import { getInventory, updateInventory } from "@server/data/inventory"; import { findItemByNameFuzzy, removeItem } from "@server/items"; -import { itemBehaviorMap, runBehavior } from "@server/items/behavior"; +// import { itemBehaviorMap, runBehavior } from "@server/items/behavior"; -export const eat = new Command( +export const eat = new BehaviorCommand( "eat", ["eat", "oot"], "Eat literally anything you have (except non-fish animals)", "eat ", "command.inventory.eat", - async props => { + async (props, self) => { const { args, prefix, part, user } = props; // const eating = args[0]; const eating = args.join(" "); @@ -33,18 +33,48 @@ export const eat = new Command( let thingy = foundObject.id; if (foundObject.objtype == "fish") thingy = "fish"; - const bhv = itemBehaviorMap[thingy]; + // const bhv = itemBehaviorMap[thingy]; + // let res; + + // if (bhv) { + // if (!bhv["eat"]) return `You can't eat the ${foundObject.name}.`; + + // res = await runBehavior(thingy, "eat", foundObject, props); + + // // Check if response had an error, and populate our state that way + // if (res.success) shouldRemove = res.state.shouldRemove; + // else shouldRemove = false; + // } else { + // shouldRemove = true; + // } + let res; + let bhvNamespace = foundObject.id; - if (bhv) { - if (!bhv["eat"]) return `You can't eat the ${foundObject.name}.`; - - res = await runBehavior(thingy, "eat", foundObject, props); - shouldRemove = res.shouldRemove; - } else { - shouldRemove = true; + if (foundObject.objtype == "fish") { + bhvNamespace = "fish"; } + res = await self.behave<"eat">( + { id: props.id, part, object: foundObject, user }, + bhvNamespace, + async () => { + shouldRemove = true; + + return { + success: true, + state: { + shouldRemove: true + } + }; + } + ); + + if (!res) throw new Error(`Unable to eat fish: no behavior result`); + + if (res.success) shouldRemove = res.state.shouldRemove; + else shouldRemove = false; + if (shouldRemove) { if (foundObject.objtype == "fish") { removeItem(inventory.fishSack, foundObject); @@ -55,15 +85,22 @@ export const eat = new Command( await updateInventory(inventory); } + if (!res.success) { + return `You broke the bot trying to ${prefix}eat the ${foundObject.name}. Congratulations.`; + } + + const state = + res.state as IBehaviorContextStateDefinitions["eat"]["state"]; + if (foundObject.id == "sand") { - if (res && res.and) { - return `Our friend ${part.name} ate of his/her ${foundObject.name} ${res.and}`; + if (res && res.success && typeof state.and !== "undefined") { + return `Our friend ${part.name} ate of his/her ${foundObject.name} ${state.and}`; } else { return `Our friend ${part.name} ate of his/her ${foundObject.name}.`; } } else { - if (res && res.and) { - return `Our friend ${part.name} ate his/her ${foundObject.name} ${res.and}`; + if (res && res.success && typeof state.and !== "undefined") { + return `Our friend ${part.name} ate his/her ${foundObject.name} ${state.and}`; } else { return `Our friend ${part.name} ate his/her ${foundObject.name}.`; } diff --git a/src/api/commands/groups/inventory/yeet.ts b/src/api/commands/groups/inventory/yeet.ts index 49b9993..953bb31 100644 --- a/src/api/commands/groups/inventory/yeet.ts +++ b/src/api/commands/groups/inventory/yeet.ts @@ -1,23 +1,20 @@ -import { addBack } from "@server/backs"; -import Command from "@server/commands/Command"; +import BehaviorCommand from "@server/commands/BehaviorCommand"; import { logger } from "@server/commands/handler"; import { getInventory, updateInventory } from "@server/data/inventory"; -import prisma from "@server/data/prisma"; import { getUser } from "@server/data/user"; import { getSizeString } from "@server/fish/fish"; -import { fishers, getFishing } from "@server/fish/fishers"; +import { fishers } from "@server/fish/fishers"; import { locations } from "@server/fish/locations"; -import { addItem } from "@server/items"; -import { CosmicColor } from "@util/CosmicColor"; +import { addItem, findItemByNameFuzzy, removeItem } from "@server/items"; -export const yeet = new Command( +export const yeet = new BehaviorCommand( "yeet", ["yeet", "yoot"], "Yeet literally anything you have (except non-fish animals)", "yeet ", "command.inventory.yeet", - async ({ id, command, args, prefix, part, user }) => { - const yeeting = args[0]; + async ({ id, command, args, prefix, part, user }, self) => { + const yeeting = args.join(" "); if (!yeeting) return `What do you want to ${prefix}yeet?`; const inventory = await getInventory(user.inventoryId); @@ -26,273 +23,314 @@ export const yeet = new Command( let foundObject: IObject | undefined; let tryKekGen = false; let i = 0; + let shouldRemove = false; - for (const item of inventory.items as unknown as IItem[]) { - if (!item.name.toLowerCase().includes(yeeting.toLowerCase())) { - i++; - continue; - } - - foundObject = item; - - let shouldRemove = false; - - if (typeof item.count !== "undefined") { - if (item.count > 1) { - shouldRemove = false; - ((inventory.items as TInventoryItems)[i].count as number)--; - } else { - shouldRemove = true; - } - } else { - shouldRemove = true; - } - - if (shouldRemove && item.id !== "sand") - (inventory.items as TInventoryItems).splice(i, 1); - break; - } - - i = 0; - - for (const fish of inventory.fishSack as TFishSack) { - if (!fish.name.toLowerCase().includes(yeeting.toLowerCase())) { - i++; - continue; - } - - foundObject = fish; - - let shouldRemove = false; - - if (typeof fish.count !== "undefined") { - if (fish.count > 1) { - shouldRemove = false; - ((inventory.fishSack as TFishSack)[i].count as number)--; - } else { - shouldRemove = true; - } - } else { - shouldRemove = true; - } - - if (shouldRemove) (inventory.fishSack as TFishSack).splice(i, 1); - break; - } + foundObject = + findItemByNameFuzzy(inventory.items, yeeting) || + findItemByNameFuzzy(inventory.fishSack, yeeting); if (!foundObject) return `You don't have "${yeeting}" to yeet.`; + + let bhvNamespace = foundObject.id; + let output = `Friend ${part.name} tossed his/her ${foundObject.name}.`; + if (foundObject.objtype == "fish") { + bhvNamespace = "fish"; tryKekGen = true; } + const res = await self.behave<"yeet">( + { + part + }, + foundObject.id, + async ctx => { + logger.debug("stuff"); + if (tryKekGen) { + // 15% + if (Math.random() < 0.15) { + const randomFisher = + Object.values(fishers)[ + Math.floor( + Math.random() * + Object.values(fishers).length + ) + ]; + + let person; + + if (!randomFisher) { + person = { + name: "Anonymous" + }; + } else { + person = await getUser(randomFisher.userID); + } + + let target: string; + + if (!person || person?.id == part.id) { + target = "Anonymous"; + } else { + target = person.name; + } + + let handsAdjective = [ + " violent ", + " shaking ", + " angery ", + " two (2) ", + " unknown number of ", + " " + ]; + + let pastTense = [ + "slung", + "foisted", + "launched", + "yeeted", + "expelled", + "fired" + ]; + + let presentTense = [ + "lazily", + "forcefully", + "haphazardly", + "angrily", + "playfully", + "lovingly" + ]; + + let ending = [ + `in the direction of ${target}.`, + `at where ${target} happens to be.`, + `at ${target}.`, + `directly at ${target}'s location in this realm.`, + `at the general vicinity of ${target}.` + ]; + + let itemAdjective = [ + "gooey", + "powdery", + "residual", + "smelly", + "appropriate", + foundObject.name, + foundObject.name + "y", + "greasy", + "uncomfortable", + "delicious", + "wonderful", + "questionable", + "nice", + "gelatinous", + "shampoo", + "fatty", + "warm", + "hot", + "cold", + "dripping", + "fish", + "unknown" + ]; + + let ps = [ + "It missed.", + "It grazed his/her cheek, leaving a small dab of " + + foundObject.name + + ".", + foundObject.objtype == "fish" + ? "Being that it was so " + + getSizeString((foundObject as IFish).size) + + ", I'm sure you can infer how comical the result is!" + : "Being that it was so voluminous, I'm sure you can infer how comical the result is!", + "It smacked right across his/her face.", + "It got hung in his/her shirt and he/she flung it out onto the ground and it was quite a silly scene.", + `It scooted across his/her head before rebounding off onto the ground nearby. The ${ + itemAdjective[ + Math.floor( + Math.random() * itemAdjective.length + ) + ] + } residue was left behind in ${target}'s hair.` + ]; + + return { + success: true, + state: { + shouldRemove: true, + text: `Friend ${part.name}'s ${ + handsAdjective[ + Math.floor( + Math.random() * + handsAdjective.length + ) + ] + } hands grabbed his/her ${ + foundObject.name + } and ${ + pastTense[ + Math.floor( + Math.random() * pastTense.length + ) + ] + } it ${ + presentTense[ + Math.floor( + Math.random() * presentTense.length + ) + ] + } ${ + ending[ + Math.floor( + Math.random() * ending.length + ) + ] + } ${ps[Math.floor(Math.random() * ps.length)]}` + } + }; + } + + if (Math.random() < 0.15) { + let size = + foundObject.objtype == "fish" + ? getSizeString((foundObject as IFish).size) + : "voluminous"; + + let fish = foundObject.name; + let name = part.name; + + const loc = locations.find( + loc => loc.id == inventory.location + ); + if (!loc) + return { + success: true, + state: { + shouldRemove: true, + text: `Friend ${part.name} carelessly hurled their ${foundObject.name} into the void.` + } + }; + + addItem( + locations[locations.indexOf(loc)].objects, + foundObject + ); + + let kekNames = [ + "kek of good fortune", + "lucky kek", + "kek", + "fortunate kek", + "the kekklefruit that was knocked from the tree", + "sandy kekklefruit", + "baby kekklefruit" + ]; + + addItem(locations[locations.indexOf(loc)].objects, { + id: "kekklefruit", + name: kekNames[ + Math.floor(Math.random() * kekNames.length) + ], + objtype: "item", + count: 1, + emoji: "🍍" + }); + + // transcribed from the old code + const yeets = [ + "The " + + size + + " " + + fish + + " thwapped into the kekklefruit tree sending debris flying. A kekklefruit was knocked to the ground.", + "It's lying there next to the tree.", + "It got splattered on the tree.", + "Part of it is stuck to the tree, but it came to rest on the ground nearby.", + "A distressed-looking " + + fish + + " on the ground near the tree.", + "It landed in the grass.", + "It's kinda scuffed up.", + "It's got tree on it. And " + name + "prints.", + "It's " + size + ".", + "It belongs to the tree now.", + "It's by the tree now.", + "It's a " + + size + + " " + + fish + + " previously owned by " + + name + + " if you still want it after that." + ]; + + return { + success: true, + state: { + shouldRemove: true, + text: yeets[ + Math.floor(Math.random() * yeets.length) + ] + } + }; + } + + if (Math.random() < 0.4) { + const yeets = [ + "Tossed " + foundObject.name + " into the water.", + "It looks like somebody tossed it haphazardly into the shallow water. It is not swimming.", + "It's in the shallows trying to swim away...", + user.name + + " tossed this into the shallows where it rests today. I don't think it's moving.", + "I think it's a " + + foundObject.name + + ". A very immobile one.", + " It's resting at the edge of the water where you can /take it." + ]; + + return { + success: true, + state: { + shouldRemove: true, + text: yeets[ + Math.floor(Math.random() * yeets.length) + ] + } + }; + } + } + + return { + success: true, + state: { + shouldRemove: true, + text: output + } + }; + } + ); + + if (res.success == true) { + const state = + res.state as IBehaviorContextStateDefinitions["yeet"]["state"]; + shouldRemove = state.shouldRemove; + output = state.text; + } + + if (shouldRemove) { + if (foundObject.objtype == "fish") { + removeItem(inventory.fishSack, foundObject); + } else if (foundObject.objtype == "item") { + removeItem(inventory.items, foundObject); + } + + await updateInventory(inventory); + } + await updateInventory(inventory); - if (foundObject.id == "sand") { - return `No, ${ - part.name - }, don't yeet ${foundObject.name.toLowerCase()}.`; - } else { - if (Math.random() < 0.15) { - const randomFisher = - Object.values(fishers)[ - Math.floor( - Math.random() * Object.values(fishers).length - ) - ]; - - let person; - - if (!randomFisher) { - person = { - name: "Anonymous" - }; - } else { - person = await getUser(randomFisher.userID); - } - - let target: string; - - if (!person || person?.id == part.id) { - target = "Anonymous"; - } else { - target = person.name; - } - - let handsAdjective = [ - " violent ", - " shaking ", - " angery ", - " two (2) ", - " unknown number of ", - " " - ]; - - let pastTense = [ - "slung", - "foisted", - "launched", - "yeeted", - "expelled", - "fired" - ]; - - let presentTense = [ - "lazily", - "forcefully", - "haphazardly", - "angrily", - "playfully", - "lovingly" - ]; - - let ending = [ - `in the direction of ${target}.`, - `at where ${target} happens to be.`, - `at ${target}.`, - `directly at ${target}'s location in this realm.`, - `at the general vicinity of ${target}.` - ]; - - let itemAdjective = [ - "gooey", - "powdery", - "residual", - "smelly", - "appropriate", - foundObject.name, - foundObject.name + "y", - "greasy", - "uncomfortable", - "delicious", - "wonderful", - "questionable", - "nice", - "gelatinous", - "shampoo", - "fatty", - "warm", - "hot", - "cold", - "dripping", - "fish", - "unknown" - ]; - - let ps = [ - "It missed.", - "It grazed his/her cheek, leaving a small dab of " + - foundObject.name + - ".", - foundObject.objtype == "fish" - ? "Being that it was so " + - getSizeString((foundObject as IFish).size) + - ", I'm sure you can infer how comical the result is!" - : "Being that it was so voluminous, I'm sure you can infer how comical the result is!", - "It smacked right across his/her face.", - "It got hung in his/her shirt and he/she flung it out onto the ground and it was quite a silly scene.", - `It scooted across his/her head before rebounding off onto the ground nearby. The ${ - itemAdjective[ - Math.floor(Math.random() * itemAdjective.length) - ] - } residue was left behind in ${target}'s hair.` - ]; - - return `Friend ${part.name}'s ${ - handsAdjective[ - Math.floor(Math.random() * handsAdjective.length) - ] - } hands grabbed his/her ${foundObject.name} and ${ - pastTense[Math.floor(Math.random() * pastTense.length)] - } it ${ - presentTense[ - Math.floor(Math.random() * presentTense.length) - ] - } ${ending[Math.floor(Math.random() * ending.length)]} ${ - ps[Math.floor(Math.random() * ps.length)] - }`; - } - - if (Math.random() < 0.15) { - let size = - foundObject.objtype == "fish" - ? getSizeString((foundObject as IFish).size) - : "voluminous"; - - let fish = foundObject.name; - let name = part.name; - - const loc = locations.find(loc => loc.id == inventory.location); - if (!loc) - return `Friend ${part.name} carelessly hurled their ${foundObject.name} into the void.`; - - addItem(locations[locations.indexOf(loc)].objects, foundObject); - - let kekNames = [ - "kek of good fortune", - "lucky kek", - "kek", - "fortunate kek", - "the kekklefruit that was knocked from the tree", - "sandy kekklefruit", - "baby kekklefruit" - ]; - - addItem(locations[locations.indexOf(loc)].objects, { - id: "kekklefruit", - name: kekNames[Math.floor(Math.random() * kekNames.length)], - objtype: "item", - count: 1, - emoji: "🍍" - }); - - // transcribed from the old code - let yeets = [ - "The " + - size + - " " + - fish + - " thwapped into the kekklefruit tree sending debris flying. A kekklefruit was knocked to the ground.", - "It's lying there next to the tree.", - "It got splattered on the tree.", - "Part of it is stuck to the tree, but it came to rest on the ground nearby.", - "A distressed-looking " + - fish + - " on the ground near the tree.", - "It landed in the grass.", - "It's kinda scuffed up.", - "It's got tree on it. And " + name + "prints.", - "It's " + size + ".", - "It belongs to the tree now.", - "It's by the tree now.", - "It's a " + - size + - " " + - fish + - " previously owned by " + - name + - " if you still want it after that." - ]; - - return yeets[Math.floor(Math.random() * yeets.length)]; - } - - if (Math.random() < 0.4) { - const yeets = [ - "Tossed " + foundObject.name + " into the water.", - "It looks like somebody tossed it haphazardly into the shallow water. It is not swimming.", - "It's in the shallows trying to swim away...", - user.name + - " tossed this into the shallows where it rests today. I don't think it's moving.", - "I think it's a " + - foundObject.name + - ". A very immobile one.", - " It's resting at the edge of the water where you can /take it." - ]; - - return yeets[Math.floor(Math.random() * yeets.length)]; - } - - return `Friend ${part.name} tossed his/her ${foundObject.name}.`; - } + return output; } ); diff --git a/src/api/commands/groups/util/pokedex.ts b/src/api/commands/groups/pokemon/pokedex.ts similarity index 100% rename from src/api/commands/groups/util/pokedex.ts rename to src/api/commands/groups/pokemon/pokedex.ts diff --git a/src/api/commands/groups/inventory/pokemon.ts b/src/api/commands/groups/pokemon/pokemon.ts similarity index 100% rename from src/api/commands/groups/inventory/pokemon.ts rename to src/api/commands/groups/pokemon/pokemon.ts diff --git a/src/api/index.ts b/src/api/index.ts index b99f178..a9f0189 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -2,12 +2,14 @@ import { startAutorestart } from "@util/autorestart"; import { startFisherTick } from "./fish/fishers"; import { startObjectTimers } from "./fish/locations"; import { initTree } from "./fish/tree"; -import { loadDefaultBehaviors } from "./items/behavior/defaults"; +import { registerBehaviors } from "./behavior/register"; +// import { loadDefaultBehaviors } from "./items/behavior/defaults"; startObjectTimers(); await startFisherTick(); await initTree(); -loadDefaultBehaviors(); +// loadDefaultBehaviors(); +registerBehaviors(); require("./api/server"); require("./cli/readline"); diff --git a/src/api/items/behavior/defaults.ts b/src/api/items/behavior/defaults.ts deleted file mode 100644 index 37cf2bb..0000000 --- a/src/api/items/behavior/defaults.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { addItemBehavior } from "."; -import { fish } from "./items/fish"; -import { kekklefruit } from "./items/kekklefruit"; - -export function loadDefaultBehaviors() { - const list: IBehaviorDefinition[] = [fish, kekklefruit]; - - for (const item of list) { - for (const key of Object.keys(item.bhv)) { - addItemBehavior(item.id, key, item.bhv[key]); - } - } -} diff --git a/src/api/items/behavior/index.ts b/src/api/items/behavior/index.ts deleted file mode 100644 index baf4c3b..0000000 --- a/src/api/items/behavior/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -export const itemBehaviorMap: TBehaviorMap = {}; - -export function addItemBehavior( - itemID: string, - bhvID: string, - bhv: TBehaviorCallback -) { - if (!itemBehaviorMap[itemID]) itemBehaviorMap[itemID] = {}; - itemBehaviorMap[itemID][bhvID] = bhv; -} - -export async function runBehavior( - itemID: string, - bhvID: string, - obj: IObject, - props: IContextProps -): Promise { - const callback = itemBehaviorMap[itemID][bhvID]; - if (!callback) - return { - success: false, - err: "No callback", - shouldRemove: false - }; - return await callback(obj, props); -} diff --git a/src/api/items/behavior/items/fish.ts b/src/api/items/behavior/items/fish.ts deleted file mode 100644 index 0ef7f17..0000000 --- a/src/api/items/behavior/items/fish.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { addBack } from "@server/backs"; -import { CosmicColor } from "@util/CosmicColor"; - -export const fish: IBehaviorDefinition = { - id: "fish", - bhv: { - async eat(obj, props) { - const r = Math.random(); - - const fish = obj as IFish; - - // 50% - if (r < 0.5) { - const color = new CosmicColor( - Math.floor(Math.random() * 255), - Math.floor(Math.random() * 255), - Math.floor(Math.random() * 255) - ); - - addBack(props.id, { - m: "color", - id: props.part.id, - color: color.toHexa() - }); - - return { - success: true, - shouldRemove: true, - and: `and it made him/her turn ${color - .getName() - .toLowerCase()}.` - }; - } else { - return { - success: true, - shouldRemove: true - }; - } - } - } -}; diff --git a/src/api/items/behavior/items/kekklefruit.ts b/src/api/items/behavior/items/kekklefruit.ts deleted file mode 100644 index 3287610..0000000 --- a/src/api/items/behavior/items/kekklefruit.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { incrementFishingChance } from "@server/fish/fishers"; - -export const kekklefruit: IBehaviorDefinition = { - id: "kekklefruit", - bhv: { - async eat(obj, props) { - const test = await incrementFishingChance(props.user.id); - - return { - success: true, - shouldRemove: true, - and: "and got a temporary fishing boost." - }; - } - } -}; diff --git a/src/util/types.d.ts b/src/util/types.d.ts index cc27ceb..49acb7b 100644 --- a/src/util/types.d.ts +++ b/src/util/types.d.ts @@ -8,7 +8,7 @@ interface ICommandResponse { response: string; } -interface IContextProps { +interface ICommandContextProps { id: string; channel: string; command: string; @@ -19,7 +19,14 @@ interface IContextProps { isDM: boolean; } -type TCommandCallback = (props: IContextProps) => Promise; +type TCommandCallback = ( + props: ICommandContextProps +) => Promise; + +type TCommandCallbackWithSelf = ( + props: ICommandContextProps, + self: T +) => Promise; interface ICountComponent { count: number; @@ -139,26 +146,6 @@ interface IGroup { permissions: string[]; } -interface IBehaviorResponse { - success: boolean; - err?: string; - - shouldRemove: boolean; - and?: string; -} - -type TBehaviorCallback = ( - obj: IObject, - props: IContextProps -) => Promise; -type TBehavior = Record; -type TBehaviorMap = Record; - -interface IBehaviorDefinition { - id: string; - bhv: TBehavior; -} - interface IFishingChance { chance: number; t: number; diff --git a/test/api/behavior/index.test.ts b/test/api/behavior/index.test.ts new file mode 100644 index 0000000..43ed1e8 --- /dev/null +++ b/test/api/behavior/index.test.ts @@ -0,0 +1,60 @@ +import { executeBehavior, registerBehavior } from "@server/behavior"; +import { test, expect } from "bun:test"; + +test("Behavior registering works", async () => { + try { + registerBehavior("fish:eat", async ctx => { + return { + success: true, + state: { + shouldRemove: true + } + }; + }); + + registerBehavior("hamburger:yeet", async ctx => { + return { + success: true, + state: { + shouldRemove: false, + and: "the hamburger rolled away" + } + }; + }); + } catch (err) { + expect(err).toBeUndefined(); + } +}); + +test("Behavior execution is correct", async () => { + registerBehavior<"eat", { cooked: boolean }, { shouldRemove: boolean }>( + "hamburger:eat", + async ctx => { + if (ctx.cooked === false) { + return { + success: false, + err: "the hamburgers are not cooked yet" + }; + } else { + return { + success: true, + state: { + shouldRemove: true + } + }; + } + } + ); + + let response = await executeBehavior("hamburger:eat", { + cooked: false + }); + + expect(response.success).toBeFalse(); + + response = await executeBehavior("hamburger:eat", { + cooked: true + }); + + expect(response.success).toBeTrue(); +});