migrate
This commit is contained in:
17
src/boredBot/commands.ts
Normal file
17
src/boredBot/commands.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { SlashCommandBuilder } from "discord.js";
|
||||
import { Command } from "../discordBot/types";
|
||||
|
||||
export const boredBotCommands = [
|
||||
new SlashCommandBuilder()
|
||||
.setName(Command.Roll)
|
||||
.setDescription("Roll dice (e.g. 'd20', '6d12-4', '2d8 + 1d6+4')")
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName("input")
|
||||
.setDescription("The dice you want to roll")
|
||||
.setRequired(true),
|
||||
),
|
||||
new SlashCommandBuilder()
|
||||
.setName(Command.Help)
|
||||
.setDescription("Displays a list of available commands."),
|
||||
];
|
123
src/boredBot/index.ts
Normal file
123
src/boredBot/index.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import type { SlashCommandOptionsOnlyBuilder } from "discord.js";
|
||||
import { DiceRoller } from "../diceRoller";
|
||||
import type { DiceParseResult } from "../diceRoller/types";
|
||||
import { parseDiceUserInput } from "../diceRoller/utils/parseDiceUserInput";
|
||||
import { DiscordBot } from "../discordBot";
|
||||
import { Command, MessageContent } from "../discordBot/types";
|
||||
|
||||
export class BoredBot extends DiscordBot {
|
||||
private static instance: BoredBot;
|
||||
|
||||
private constructor(
|
||||
token: string,
|
||||
applicationId: string,
|
||||
commands: SlashCommandOptionsOnlyBuilder[],
|
||||
) {
|
||||
super(token, applicationId, commands);
|
||||
this.useCommand();
|
||||
}
|
||||
|
||||
public static async getInstance(
|
||||
token: string,
|
||||
applicationId: string,
|
||||
commands: SlashCommandOptionsOnlyBuilder[],
|
||||
): Promise<BoredBot> {
|
||||
if (!BoredBot.instance) {
|
||||
BoredBot.instance = new BoredBot(token, applicationId, commands);
|
||||
await BoredBot.instance.initialize("Bored Bot");
|
||||
}
|
||||
return BoredBot.instance;
|
||||
}
|
||||
|
||||
private async useCommand() {
|
||||
this.client.on("interactionCreate", async (interaction) => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
const command = interaction.commandName as Command;
|
||||
const userName = interaction.user.displayName;
|
||||
const userInput = interaction.options.getString(MessageContent.Input);
|
||||
|
||||
if (!userInput) throw new Error("Expected input to be defined");
|
||||
|
||||
console.log("got command");
|
||||
|
||||
switch (command) {
|
||||
case Command.Roll: {
|
||||
await interaction.reply(
|
||||
await this.handleRollCommand(userInput, userName),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async handleRollCommand(
|
||||
inputStringFromUser: string,
|
||||
username: string,
|
||||
): Promise<string> {
|
||||
let parsedInputResult: DiceParseResult = { dices: [], mod: 0 };
|
||||
|
||||
try {
|
||||
parsedInputResult = parseDiceUserInput(inputStringFromUser);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) return error.message;
|
||||
}
|
||||
|
||||
const roller = new DiceRoller(username);
|
||||
const resultsMessages: string[] = [];
|
||||
|
||||
if (parsedInputResult.dices.length === 1) {
|
||||
const singleDie = parsedInputResult.dices[0];
|
||||
const { rollResult, message } = await roller.roll(singleDie);
|
||||
|
||||
const mod = parsedInputResult.mod;
|
||||
const finalResult = rollResult + mod;
|
||||
|
||||
let singleLine = `(${singleDie.toString()}) => ${rollResult}`;
|
||||
if (mod !== 0) {
|
||||
const sign = mod > 0 ? "+" : "-";
|
||||
singleLine += ` ${sign}${Math.abs(mod)} = ${finalResult}`;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
singleLine += `\n${message}`;
|
||||
}
|
||||
|
||||
resultsMessages.push(singleLine);
|
||||
} else {
|
||||
resultsMessages.push(
|
||||
`You rolled ${parsedInputResult.dices.length} dice!`,
|
||||
);
|
||||
|
||||
let sum = 0;
|
||||
let rollCount = 1;
|
||||
|
||||
for (const dieInput of parsedInputResult.dices) {
|
||||
const { rollResult, message } = await roller.roll(dieInput);
|
||||
sum += rollResult;
|
||||
|
||||
const prefix = `Roll #${rollCount}: `;
|
||||
const suffix = message ? ` **${message}**` : "";
|
||||
|
||||
resultsMessages.push(
|
||||
`${prefix}(d${dieInput.toString()}) => ${rollResult}${suffix}`,
|
||||
);
|
||||
rollCount++;
|
||||
}
|
||||
|
||||
const mod = parsedInputResult.mod;
|
||||
const finalResult = sum + mod;
|
||||
if (mod !== 0) {
|
||||
const sign = mod > 0 ? "+" : "-";
|
||||
|
||||
resultsMessages.push(
|
||||
`Result: ${sum} ${sign} ${Math.abs(mod)} = ${finalResult}`,
|
||||
);
|
||||
} else {
|
||||
resultsMessages.push(`**Final result: ${sum}**`);
|
||||
}
|
||||
}
|
||||
|
||||
return resultsMessages.join("\n");
|
||||
}
|
||||
}
|
53
src/diceRoller/index.ts
Normal file
53
src/diceRoller/index.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { Critical, Dice } from "./types";
|
||||
|
||||
export class DiceRoller {
|
||||
private username: string;
|
||||
constructor(user: string) {
|
||||
this.username = user;
|
||||
}
|
||||
|
||||
public async roll(
|
||||
dice: (typeof Dice)[keyof typeof Dice],
|
||||
): Promise<{ rollResult: number; message?: string }> {
|
||||
if (!Object.values(Dice).includes(dice))
|
||||
throw new Error(`Invalid dice type: ${dice}`);
|
||||
|
||||
const { outcome: result, crit } = this.getSingleDiceRollOutcome(dice);
|
||||
|
||||
if (crit.failure || crit.success) {
|
||||
return {
|
||||
rollResult: result,
|
||||
message: crit.success
|
||||
? await this.handleCritical(Critical.Success)
|
||||
: await this.handleCritical(Critical.Fail),
|
||||
};
|
||||
}
|
||||
return { rollResult: result };
|
||||
}
|
||||
|
||||
private getSingleDiceRollOutcome(dice: (typeof Dice)[keyof typeof Dice]): {
|
||||
outcome: number;
|
||||
crit: { failure: boolean; success: boolean };
|
||||
} {
|
||||
const roll = Math.floor(Math.random() * dice) + 1;
|
||||
|
||||
switch (dice) {
|
||||
case Dice.Twenty:
|
||||
return {
|
||||
outcome: roll,
|
||||
crit: { failure: roll === 1, success: roll === 20 },
|
||||
};
|
||||
default:
|
||||
return { outcome: roll, crit: { failure: false, success: false } };
|
||||
}
|
||||
}
|
||||
|
||||
private async handleCritical(critical: Critical): Promise<string> {
|
||||
switch (critical) {
|
||||
case Critical.Fail:
|
||||
return "Critical FAIL!";
|
||||
case Critical.Success:
|
||||
return "Critical SUCCESS!";
|
||||
}
|
||||
}
|
||||
}
|
18
src/diceRoller/types.ts
Normal file
18
src/diceRoller/types.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export const Dice = {
|
||||
Four: 4,
|
||||
Six: 6,
|
||||
Ten: 10,
|
||||
Twelve: 12,
|
||||
Twenty: 20,
|
||||
Hundrer: 100,
|
||||
};
|
||||
|
||||
export enum Critical {
|
||||
Fail = "fail",
|
||||
Success = "success",
|
||||
}
|
||||
|
||||
export type DiceParseResult = {
|
||||
dices: number[];
|
||||
mod: number;
|
||||
};
|
71
src/diceRoller/utils/handleRollCommand.ts
Normal file
71
src/diceRoller/utils/handleRollCommand.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { DiceRoller } from "../discord/dice";
|
||||
import { parseDiceUserInput } from "./parseDiceUserInput";
|
||||
import type { DiceParseResult } from "../types";
|
||||
|
||||
export async function handleRollCommand(
|
||||
inputStringFromUser: string,
|
||||
username: string,
|
||||
): Promise<string> {
|
||||
let parsedInputResult: DiceParseResult = { dices: [], mod: 0 };
|
||||
|
||||
try {
|
||||
parsedInputResult = parseDiceUserInput(inputStringFromUser);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) return error.message;
|
||||
}
|
||||
|
||||
const roller = new DiceRoller(username);
|
||||
const resultsMessages: string[] = [];
|
||||
|
||||
if (parsedInputResult.dices.length === 1) {
|
||||
const singleDie = parsedInputResult.dices[0];
|
||||
const { rollResult, message } = await roller.roll(singleDie);
|
||||
|
||||
const mod = parsedInputResult.mod;
|
||||
const finalResult = rollResult + mod;
|
||||
|
||||
let singleLine = `(${singleDie.toString()}) => ${rollResult}`;
|
||||
if (mod !== 0) {
|
||||
const sign = mod > 0 ? "+" : "-";
|
||||
singleLine += ` ${sign}${Math.abs(mod)} = ${finalResult}`;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
singleLine += `\n${message}`;
|
||||
}
|
||||
|
||||
resultsMessages.push(singleLine);
|
||||
} else {
|
||||
resultsMessages.push(`You rolled ${parsedInputResult.dices.length} dice!`);
|
||||
|
||||
let sum = 0;
|
||||
let rollCount = 1;
|
||||
|
||||
for (const dieInput of parsedInputResult.dices) {
|
||||
const { rollResult, message } = await roller.roll(dieInput);
|
||||
sum += rollResult;
|
||||
|
||||
const prefix = `Roll #${rollCount}: `;
|
||||
const suffix = message ? ` **${message}**` : "";
|
||||
|
||||
resultsMessages.push(
|
||||
`${prefix}(d${dieInput.toString()}) => ${rollResult}${suffix}`,
|
||||
);
|
||||
rollCount++;
|
||||
}
|
||||
|
||||
const mod = parsedInputResult.mod;
|
||||
const finalResult = sum + mod;
|
||||
if (mod !== 0) {
|
||||
const sign = mod > 0 ? "+" : "-";
|
||||
|
||||
resultsMessages.push(
|
||||
`Result: ${sum} ${sign} ${Math.abs(mod)} = ${finalResult}`,
|
||||
);
|
||||
} else {
|
||||
resultsMessages.push(`**Final result: ${sum}**`);
|
||||
}
|
||||
}
|
||||
|
||||
return resultsMessages.join("\n");
|
||||
}
|
69
src/diceRoller/utils/parseDiceUserInput.ts
Normal file
69
src/diceRoller/utils/parseDiceUserInput.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { Dice } from "../types";
|
||||
|
||||
const ALLOWED_DICE_SIDES = new Set(Object.values(Dice));
|
||||
|
||||
export function parseDiceUserInput(input: string): DiceParseResult {
|
||||
if (typeof input !== "string" || !input.trim()) {
|
||||
throw new Error("Input must be a non-empty string.");
|
||||
}
|
||||
|
||||
let clean = input.toLowerCase().trim();
|
||||
clean = clean.replace(/\b(roll|dice|die)\b/g, "");
|
||||
clean = clean.replace(/\s+/g, "");
|
||||
|
||||
if (/^\d+$/.test(clean)) {
|
||||
const sides = Number.parseInt(clean, 10);
|
||||
if (!ALLOWED_DICE_SIDES.has(sides)) {
|
||||
throw new Error(
|
||||
`Allowed dice sides are ${[...ALLOWED_DICE_SIDES].join(", ")}. Received: ${sides}`,
|
||||
);
|
||||
}
|
||||
return { dices: [sides], mod: 0 };
|
||||
}
|
||||
|
||||
const tokenRegex = /(\d*d\d+|[+\-]\d+)/g;
|
||||
const tokens = clean.match(tokenRegex);
|
||||
|
||||
if (!tokens) {
|
||||
throw new Error(
|
||||
"Unable to parse any dice or modifiers. Examples: '3d6+5', 'd20-2', '2d8+1d6+4'.",
|
||||
);
|
||||
}
|
||||
|
||||
const dices: number[] = [];
|
||||
let mod = 0;
|
||||
|
||||
for (const token of tokens) {
|
||||
if (token.includes("d")) {
|
||||
const [countPart, sidesPart] = token.split("d");
|
||||
|
||||
let diceCount = countPart ? Number.parseInt(countPart, 10) : 1;
|
||||
if (Number.isNaN(diceCount) || diceCount < 1) {
|
||||
diceCount = 1;
|
||||
}
|
||||
|
||||
const sides = Number.parseInt(sidesPart, 10);
|
||||
if (Number.isNaN(sides) || sides <= 0) {
|
||||
throw new Error(`Invalid number of sides: "${sidesPart}"`);
|
||||
}
|
||||
|
||||
if (!ALLOWED_DICE_SIDES.has(sides)) {
|
||||
throw new Error(
|
||||
`Allowed dice sides are ${[...ALLOWED_DICE_SIDES].join(", ")}. Received: ${sides}`,
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < diceCount; i++) {
|
||||
dices.push(sides);
|
||||
}
|
||||
} else {
|
||||
const parsedMod = Number.parseInt(token, 10);
|
||||
if (Number.isNaN(parsedMod)) {
|
||||
throw new Error(`Invalid modifier: "${token}"`);
|
||||
}
|
||||
mod += parsedMod;
|
||||
}
|
||||
}
|
||||
|
||||
return { dices, mod };
|
||||
}
|
65
src/discordBot/index.ts
Normal file
65
src/discordBot/index.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import {
|
||||
Client,
|
||||
GatewayIntentBits,
|
||||
REST,
|
||||
Routes,
|
||||
type SlashCommandOptionsOnlyBuilder,
|
||||
} from "discord.js";
|
||||
|
||||
export abstract class DiscordBot {
|
||||
private token: string;
|
||||
private applicationId: string;
|
||||
protected client: Client;
|
||||
private commands: SlashCommandOptionsOnlyBuilder[];
|
||||
|
||||
protected constructor(
|
||||
token: string,
|
||||
applicationId: string,
|
||||
commands: SlashCommandOptionsOnlyBuilder[],
|
||||
) {
|
||||
this.client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
],
|
||||
});
|
||||
this.token = token;
|
||||
this.applicationId = applicationId;
|
||||
this.commands = commands;
|
||||
}
|
||||
|
||||
private async login(): Promise<void> {
|
||||
if (!this.token) {
|
||||
throw new Error("No token provided when attempting to log in bot");
|
||||
}
|
||||
try {
|
||||
await this.client.login(this.token);
|
||||
} catch (error) {
|
||||
throw new Error(`Bot failed to log in: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async registerCommands(): Promise<void> {
|
||||
try {
|
||||
await new REST({ version: "10" })
|
||||
.setToken(this.token)
|
||||
.put(Routes.applicationCommands(this.applicationId), {
|
||||
body: this.commands,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to register slash commands: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
protected async initialize(botName: string): Promise<void> {
|
||||
console.log(`Initializing bot '${botName}'...`);
|
||||
try {
|
||||
await this.login();
|
||||
await this.registerCommands();
|
||||
console.log(`${botName} is ready!`);
|
||||
} catch (error) {
|
||||
console.log("Initialization error:", error);
|
||||
}
|
||||
}
|
||||
}
|
8
src/discordBot/types.ts
Normal file
8
src/discordBot/types.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export enum Command {
|
||||
Roll = "roll",
|
||||
Help = "help",
|
||||
}
|
||||
|
||||
export enum MessageContent {
|
||||
Input = "input",
|
||||
}
|
10
src/index.ts
Normal file
10
src/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
// import { Elysia } from "elysia";
|
||||
import { BoredBot } from "./boredBot";
|
||||
import { boredBotCommands } from "./boredBot/commands";
|
||||
|
||||
// new Elysia().get("/", () => "Hello Elysia").listen(3000);
|
||||
if (!process.env.DISCORD_BOT_TOKEN || !process.env.APPLICATION_ID) {
|
||||
throw new Error("DISCORD_BOT_TOKEN and APPLICATION_ID must be defined in the environment variables.");
|
||||
}
|
||||
|
||||
await BoredBot.getInstance(process.env.DISCORD_BOT_TOKEN, process.env.APPLICATION_ID, boredBotCommands);
|
Reference in New Issue
Block a user