Rework behaviors

This commit is contained in:
Hri7566 2024-10-15 05:01:21 -04:00
parent f6ca90329c
commit c73a4d2abf
26 changed files with 810 additions and 422 deletions

View File

@ -17450,7 +17450,10 @@ var TalkomaticBot = class extends import_node_events.EventEmitter {
async start() { async start() {
this.logger.info("Starting"); this.logger.info("Starting");
this.client.connect(); 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); this.logger.debug(data);
if (typeof data !== "undefined") { if (typeof data !== "undefined") {
try { try {

View File

@ -1,32 +1,41 @@
- id: pond - id: pond
name: Pond name: Pond
nearby: nearby:
- lake - lake
- river - river
- sea - sea
- shop
hasSand: true hasSand: true
objects: [] objects: []
- id: lake - id: lake
name: Lake name: Lake
nearby: nearby:
- pond - pond
- river - river
- sea - sea
hasSand: false hasSand: false
objects: [] objects: []
- id: river - id: river
name: River name: River
nearby: nearby:
- pond - pond
- lake - lake
- sea - sea
hasSand: false hasSand: false
objects: [] objects: []
- id: sea - id: sea
name: Sea name: Sea
nearby: nearby:
- pond - pond
- lake - lake
- river - river
hasSand: true hasSand: true
objects: [] objects: []
- id: shop
name: Shop
nearby:
- pond
- id: bed # /go sleep?
name: Bed
nearby:
- pond

View File

@ -1,2 +1,3 @@
- channel: - channel:
name: test/fishing name: test/fishing
type: public

62
src/api/behavior/bhv.d.ts vendored Normal file
View File

@ -0,0 +1,62 @@
interface IBehaviorValidResponse<State> {
success: true;
// shouldRemove: boolean;
// and?: string;
state: State;
}
interface IBehaviorErrorResponse {
success: false;
err: string;
}
type TBehaviorResponse<State> =
| IBehaviorValidResponse<State>
| IBehaviorErrorResponse;
type TBehaviorCallback<Context, State> = (
context: Context
) => Promise<TBehaviorResponse<State>>;
type TBehavior<Context, State> = Record<
string,
TBehaviorCallback<Context, State>
>;
type TBehaviorMap<Context, State> = Map<string, TBehavior<Context, State>>; //Record<string, TBehavior<C>>;
type TBehaviorNamespace = string;
type TBehaviorAction = keyof IBehaviorContextStateDefinitions;
type TBehaviorID = `${TBehaviorNamespace}:${TBehaviorAction}`;
interface IBehaviorDefinition<Context> {
id: string;
bhv: TBehavior<Context>;
}
declare interface IBehaviorContextStateDefinitions
extends Record<Record<{ context: unknown; state: unknown }>> {
eat: {
context: {
id: string;
part: IPart;
object: IObject;
user: User;
};
state: {
shouldRemove: boolean;
and?: string;
};
};
yeet: {
context: {
part: IPart;
};
state: {
shouldRemove: boolean;
text: string;
};
};
}

72
src/api/behavior/index.ts Normal file
View File

@ -0,0 +1,72 @@
import { logger } from "@server/commands/handler";
export const behaviorMap = new Map<string, TBehavior<unknown, unknown>>();
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<Context, State>) {
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<unknown, unknown>;
} 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<TBehaviorResponse<State>> {
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<State>;
}

View File

@ -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)]
}
};
});

View File

@ -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
}
};
}
});

View File

@ -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."
}
};
});

View File

@ -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.`
}
};
});

View File

@ -0,0 +1,5 @@
export function registerBehaviors() {
require("./objects/fish");
require("./objects/kekklefruit");
require("./objects/burger");
}

View File

@ -1,6 +1,7 @@
import type { ReadlineCommand } from "./ReadlineCommand"; import type { ReadlineCommand } from "./ReadlineCommand";
import { deltoken } from "./commands/deltoken"; import { deltoken } from "./commands/deltoken";
import { gentoken } from "./commands/gentoken"; import { gentoken } from "./commands/gentoken";
import { grow_fruit } from "./commands/grow_fruit";
import { help } from "./commands/help"; import { help } from "./commands/help";
import { lstoken } from "./commands/lstoken"; import { lstoken } from "./commands/lstoken";
import { stop } from "./commands/stop"; import { stop } from "./commands/stop";
@ -12,3 +13,4 @@ readlineCommands.push(gentoken);
readlineCommands.push(deltoken); readlineCommands.push(deltoken);
readlineCommands.push(lstoken); readlineCommands.push(lstoken);
readlineCommands.push(stop); readlineCommands.push(stop);
readlineCommands.push(grow_fruit);

View File

@ -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 <number>",
async line => {
try {
const args = line.split(" ");
const num = parseInt(args[1], 10);
await growFruit(num);
return `grew ${num} kekklefruit`;
} catch (err) {
return "bad";
}
}
);

View File

@ -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<User, BehaviorCommand>,
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<C, S>) {
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<S>;
}
}
}
export default BehaviorCommand;

View File

@ -3,7 +3,7 @@ import Command from "@server/commands/Command";
export const info = new Command( export const info = new Command(
"info", "info",
["info"], ["info"],
"Get your own user ID", "Show information about the bot",
"info", "info",
"command.general.info", "command.general.info",
async ({ id, command, args, prefix, part, user }) => { async ({ id, command, args, prefix, part, user }) => {

View File

@ -13,10 +13,10 @@ import { eat } from "./inventory/eat";
import { sack } from "./inventory/sack"; import { sack } from "./inventory/sack";
import { reel } from "./fishing/reel"; import { reel } from "./fishing/reel";
import { memory } from "./util/mem"; import { memory } from "./util/mem";
import { pokemon } from "./inventory/pokemon"; import { pokemon } from "./pokemon/pokemon";
import { color } from "./general/color"; import { color } from "./general/color";
import { autofish } from "./util/autofish"; import { autofish } from "./util/autofish";
import { pokedex } from "./util/pokedex"; import { pokedex } from "./pokemon/pokedex";
import { myid } from "./general/myid"; import { myid } from "./general/myid";
import { yeet } from "./inventory/yeet"; import { yeet } from "./inventory/yeet";
import { tree } from "./fishing/tree"; import { tree } from "./fishing/tree";
@ -63,7 +63,7 @@ commandGroups.push(inventoryGroup);
const pokemonGroup: ICommandGroup = { const pokemonGroup: ICommandGroup = {
id: "pokemon", id: "pokemon",
displayName: "Pokémon", displayName: "Pokémon",
commands: [daily, pokedex] commands: [daily, pokemon, pokedex]
}; };
commandGroups.push(pokemonGroup); commandGroups.push(pokemonGroup);

View File

@ -1,16 +1,16 @@
import Command from "@server/commands/Command"; import BehaviorCommand from "@server/commands/BehaviorCommand";
import { logger } from "@server/commands/handler"; import { logger } from "@server/commands/handler";
import { getInventory, updateInventory } from "@server/data/inventory"; import { getInventory, updateInventory } from "@server/data/inventory";
import { findItemByNameFuzzy, removeItem } from "@server/items"; 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",
["eat", "oot"], ["eat", "oot"],
"Eat literally anything you have (except non-fish animals)", "Eat literally anything you have (except non-fish animals)",
"eat <something>", "eat <something>",
"command.inventory.eat", "command.inventory.eat",
async props => { async (props, self) => {
const { args, prefix, part, user } = props; const { args, prefix, part, user } = props;
// const eating = args[0]; // const eating = args[0];
const eating = args.join(" "); const eating = args.join(" ");
@ -33,18 +33,48 @@ export const eat = new Command(
let thingy = foundObject.id; let thingy = foundObject.id;
if (foundObject.objtype == "fish") thingy = "fish"; 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 res;
let bhvNamespace = foundObject.id;
if (bhv) { if (foundObject.objtype == "fish") {
if (!bhv["eat"]) return `You can't eat the ${foundObject.name}.`; bhvNamespace = "fish";
res = await runBehavior(thingy, "eat", foundObject, props);
shouldRemove = res.shouldRemove;
} else {
shouldRemove = true;
} }
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 (shouldRemove) {
if (foundObject.objtype == "fish") { if (foundObject.objtype == "fish") {
removeItem(inventory.fishSack, foundObject); removeItem(inventory.fishSack, foundObject);
@ -55,15 +85,22 @@ export const eat = new Command(
await updateInventory(inventory); 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 (foundObject.id == "sand") {
if (res && res.and) { if (res && res.success && typeof state.and !== "undefined") {
return `Our friend ${part.name} ate of his/her ${foundObject.name} ${res.and}`; return `Our friend ${part.name} ate of his/her ${foundObject.name} ${state.and}`;
} else { } else {
return `Our friend ${part.name} ate of his/her ${foundObject.name}.`; return `Our friend ${part.name} ate of his/her ${foundObject.name}.`;
} }
} else { } else {
if (res && res.and) { if (res && res.success && typeof state.and !== "undefined") {
return `Our friend ${part.name} ate his/her ${foundObject.name} ${res.and}`; return `Our friend ${part.name} ate his/her ${foundObject.name} ${state.and}`;
} else { } else {
return `Our friend ${part.name} ate his/her ${foundObject.name}.`; return `Our friend ${part.name} ate his/her ${foundObject.name}.`;
} }

View File

@ -1,23 +1,20 @@
import { addBack } from "@server/backs"; import BehaviorCommand from "@server/commands/BehaviorCommand";
import Command from "@server/commands/Command";
import { logger } from "@server/commands/handler"; import { logger } from "@server/commands/handler";
import { getInventory, updateInventory } from "@server/data/inventory"; import { getInventory, updateInventory } from "@server/data/inventory";
import prisma from "@server/data/prisma";
import { getUser } from "@server/data/user"; import { getUser } from "@server/data/user";
import { getSizeString } from "@server/fish/fish"; 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 { locations } from "@server/fish/locations";
import { addItem } from "@server/items"; import { addItem, findItemByNameFuzzy, removeItem } from "@server/items";
import { CosmicColor } from "@util/CosmicColor";
export const yeet = new Command( export const yeet = new BehaviorCommand(
"yeet", "yeet",
["yeet", "yoot"], ["yeet", "yoot"],
"Yeet literally anything you have (except non-fish animals)", "Yeet literally anything you have (except non-fish animals)",
"yeet <something>", "yeet <something>",
"command.inventory.yeet", "command.inventory.yeet",
async ({ id, command, args, prefix, part, user }) => { async ({ id, command, args, prefix, part, user }, self) => {
const yeeting = args[0]; const yeeting = args.join(" ");
if (!yeeting) return `What do you want to ${prefix}yeet?`; if (!yeeting) return `What do you want to ${prefix}yeet?`;
const inventory = await getInventory(user.inventoryId); const inventory = await getInventory(user.inventoryId);
@ -26,273 +23,314 @@ export const yeet = new Command(
let foundObject: IObject | undefined; let foundObject: IObject | undefined;
let tryKekGen = false; let tryKekGen = false;
let i = 0; let i = 0;
let shouldRemove = false;
for (const item of inventory.items as unknown as IItem[]) { foundObject =
if (!item.name.toLowerCase().includes(yeeting.toLowerCase())) { findItemByNameFuzzy(inventory.items, yeeting) ||
i++; findItemByNameFuzzy(inventory.fishSack, yeeting);
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;
}
if (!foundObject) return `You don't have "${yeeting}" to yeet.`; 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") { if (foundObject.objtype == "fish") {
bhvNamespace = "fish";
tryKekGen = true; 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); await updateInventory(inventory);
if (foundObject.id == "sand") { return output;
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}.`;
}
} }
); );

View File

@ -2,12 +2,14 @@ import { startAutorestart } from "@util/autorestart";
import { startFisherTick } from "./fish/fishers"; import { startFisherTick } from "./fish/fishers";
import { startObjectTimers } from "./fish/locations"; import { startObjectTimers } from "./fish/locations";
import { initTree } from "./fish/tree"; import { initTree } from "./fish/tree";
import { loadDefaultBehaviors } from "./items/behavior/defaults"; import { registerBehaviors } from "./behavior/register";
// import { loadDefaultBehaviors } from "./items/behavior/defaults";
startObjectTimers(); startObjectTimers();
await startFisherTick(); await startFisherTick();
await initTree(); await initTree();
loadDefaultBehaviors(); // loadDefaultBehaviors();
registerBehaviors();
require("./api/server"); require("./api/server");
require("./cli/readline"); require("./cli/readline");

View File

@ -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]);
}
}
}

View File

@ -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<IBehaviorResponse> {
const callback = itemBehaviorMap[itemID][bhvID];
if (!callback)
return {
success: false,
err: "No callback",
shouldRemove: false
};
return await callback(obj, props);
}

View File

@ -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
};
}
}
}
};

View File

@ -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."
};
}
}
};

31
src/util/types.d.ts vendored
View File

@ -8,7 +8,7 @@ interface ICommandResponse {
response: string; response: string;
} }
interface IContextProps { interface ICommandContextProps {
id: string; id: string;
channel: string; channel: string;
command: string; command: string;
@ -19,7 +19,14 @@ interface IContextProps {
isDM: boolean; isDM: boolean;
} }
type TCommandCallback<User> = (props: IContextProps) => Promise<string | void>; type TCommandCallback<User> = (
props: ICommandContextProps
) => Promise<string | void>;
type TCommandCallbackWithSelf<User, T> = (
props: ICommandContextProps,
self: T
) => Promise<string | void>;
interface ICountComponent { interface ICountComponent {
count: number; count: number;
@ -139,26 +146,6 @@ interface IGroup {
permissions: string[]; permissions: string[];
} }
interface IBehaviorResponse {
success: boolean;
err?: string;
shouldRemove: boolean;
and?: string;
}
type TBehaviorCallback = (
obj: IObject,
props: IContextProps
) => Promise<IBehaviorResponse>;
type TBehavior = Record<string, TBehaviorCallback>;
type TBehaviorMap = Record<string, TBehavior>;
interface IBehaviorDefinition {
id: string;
bhv: TBehavior;
}
interface IFishingChance { interface IFishingChance {
chance: number; chance: number;
t: number; t: number;

View File

@ -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();
});