item transfer, item to buy other items, item income, unbuyable items

This commit is contained in:
stjet
2024-09-13 18:17:44 +00:00
parent 3145f52df7
commit c2d1cd2af1
18 changed files with 407 additions and 56 deletions

View File

@@ -30,4 +30,3 @@ const data: CommandData = {
export default data;

View File

@@ -2,7 +2,7 @@ import type { ChatInputCommandInteraction } from "discord.js";
import type { CommandData } from "./index";
import type { StoreItem, User } from "../db";
import { get_item, add_item_to_user, sub_balance } from "../db";
import { get_item, add_item_to_user, sub_item_to_user, sub_balance } from "../db";
import { BotError } from "./common/error";
import { item_name_autocomplete } from "./common/autocompletes";
import { has_role } from "../util";
@@ -15,15 +15,24 @@ async function run(interaction: ChatInputCommandInteraction, user: User) {
if (quantity <= 0) throw new BotError("Can't buy 0 or less of an item. Nice try");
const item = await get_item(name);
if (!item) throw new BotError("Item does not exist");
if (!item.price) throw new BotError("Item is not buyable");
if (item.roles_required.length > 0) {
for (const role_id of item.roles_required) {
if (!has_role(interaction, role_id)) throw new BotError("Missing one of the required roles to buy this item");
}
}
if (item.items) {
if (!item.items.every((itemm) => user.items[itemm[0]] >= itemm[1])) throw new BotError("Don't have enough of one of the items");
}
const total_cost = item.price * quantity;
if (!(await sub_balance(user.user, total_cost))) throw new BotError("Not enough balance to buy this item");
if (item.items) {
for (const itemm of item.items) {
await sub_item_to_user(user.user, itemm[0], itemm[1] * quantity);
}
}
await add_item_to_user(user.user, item.name, quantity);
return await interaction.editReply(`Bought ${quantity} of \`${name}\` for ${total_cost} ${ total_cost === 1 ? config.currency : config.currency_plural }`);
return await interaction.editReply(`Bought ${quantity} of \`${name}\` for ${total_cost} ${ total_cost === 1 ? config.currency : config.currency_plural }${ item.items ? " and other items" : "" }`);
}
const data: CommandData = {

16
commands/common/common.ts Normal file
View File

@@ -0,0 +1,16 @@
import { get_item } from "../../db";
//if return string, that is error message
export async function items_string_to_items(items_string: string): Promise<[string, number][] | string> {
let items = [];
for (const item of items_string.split("|")) {
const parts = item.split(",");
if (parts.length !== 2) return "Incorrect items format, must be `name,quantity|name,quantity`, etc";
const quantity = Number(parts[1]);
if (isNaN(quantity) || Math.floor(quantity) !== quantity) return "Item quantity was not an integer";
if (!await get_item(parts[0])) return `Item \`${parts[0].replaceAll("`", "\\`")}\` does not exist`;
items.push([parts[0], quantity]);
}
return items;
}

View File

@@ -1,33 +1,39 @@
//also: edit_item
import type { ChatInputCommandInteraction } from "discord.js";
import type { CommandData } from "./index";
import type { Items, StoreItem, User } from "../db";
import { create_item, get_item } from "../db";
import { BotError } from "./common/error";
import { items_string_to_items } from "./common/common";
async function run(interaction: ChatInputCommandInteraction) {
await interaction.deferReply();
const options = interaction.options;
const name: string = (await options.get("name")).value as string;
const price: number = (await options.get("price")).value as number;
const price = (await options.get("price"))?.value as (number | undefined);
const description: string = (await options.get("description")).value as string;
const usable: boolean = ((await options.get("usable"))?.value ?? true) as boolean;
if (name.includes("`")) throw new BotError("Item name cannot include the ` character"); //don't want to escape shit
const items_string = (await options.get("items"))?.value as (string | undefined);
if (name.includes("`") || name.includes(",") || name.includes("|")) throw new BotError("Item name cannot include the following characters:`|,"); //don't want to escape shit
//to add multiple roles, people will have to use /edit_item, I guess? augh
const required_role = (await options.get("required_role"))?.role;
if (price < 0) throw new BotError("Price cannot be negative");
if (price <= 0) throw new BotError("Price cannot be zero or negative"); //undefined < 0 is false btw
//name and description char limits (based on discord embed field name/value limits)
if (name.length > 200) throw new BotError("Item name cannot be more than 256 characters"); //true limit is 256 (still might error if currency name is more than like 50 characters, or price is absurdly huge)
if (description.length > 900) throw new BotError("Item description cannot be more than 1024 characters"); //true limit is 1024 but we want some margin for other info
if (await get_item(name)) throw new BotError("Item with that name already exists. Use a different name, or /edit_item or /delete_item");
let items;
if (items_string) {
items = await items_string_to_items(items_string);
if (typeof items === "string") throw new BotError(items);
}
const store_item: StoreItem = {
name,
price,
description,
roles_required: required_role ? [required_role.id] : [],
usable,
items,
};
await create_item(store_item);
return await interaction.editReply("Item created");
@@ -35,7 +41,7 @@ async function run(interaction: ChatInputCommandInteraction) {
const data: CommandData = {
name: "create_item",
description: "Create item",
description: "Create item, cannot be made unbuyable after creation",
registered_only: false,
ephemeral: false,
admin_only: true,

View File

@@ -13,12 +13,12 @@ async function run(interaction: ChatInputCommandInteraction) {
const delete_existing_roles: boolean = (await options.get("delete_existing_roles")).value as boolean;
const item = await get_item(name);
if (!item) throw new BotError("No item of that name exists");
const price: number = ((await options.get("price"))?.value ?? item.price) as number;
const price = ((await options.get("price"))?.value ?? item.price) as (number | undefined);
const description: string = ((await options.get("description"))?.value ?? item.description) as string;
const usable: boolean = ((await options.get("usable"))?.value ?? item.usable) as boolean;
//to add multiple roles, people will have to use /edit_item, I guess? augh
const required_role = (await options.get("required_role"))?.role;
if (price < 0) throw new BotError("Price cannot be negative");
if (price <= 0) throw new BotError("Price cannot be zero or negative");
//name and description char limits (based on discord embed field name/value limits)
if (description.length > 900) throw new BotError("Item description cannot be more than 1024 characters"); //true limit is 1024 but we want some margin for other info
const existing = delete_existing_roles ? [] : item.roles_required;

View File

@@ -4,8 +4,7 @@ import { EmbedBuilder } from "discord.js";
import type { User } from "../db";
import { get_user } from "../db";
import { BotError } from "./common/error";
import config from "../config.json";
import { has_role } from "../util";
import { is_admin } from "../util";
import say from "./say";
import roll from "./roll";
@@ -21,6 +20,8 @@ import buy from "./buy";
import use_item from "./use_item";
import delete_item from "./delete_item";
import edit_item from "./edit_item";
import role_income from "./role_income";
import transfer_item from "./transfer_item";
export interface CommandData {
name: string;
@@ -32,11 +33,7 @@ export interface CommandData {
autocomplete?: (interaction: AutocompleteInteraction) => Promise<any>;
};
const commands: CommandData[] = [say, roll, register_user, bal, change_bal, transfer, items, create_item, store, change_item_balance, buy, use_item, delete_item, edit_item];
function is_admin(interaction: ChatInputCommandInteraction): boolean {
return has_role(interaction, config.admin_role);
}
const commands: CommandData[] = [say, roll, register_user, bal, change_bal, transfer, items, create_item, store, change_item_balance, buy, use_item, delete_item, edit_item, role_income, transfer_item];
async function run(interaction: ChatInputCommandInteraction, found: CommandData, name: string) {
//help command is "auto-generated"

View File

@@ -1,10 +1,11 @@
import type { ChatInputCommandInteraction } from "discord.js";
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
import { EmbedBuilder } from "discord.js";
import type { CommandData } from "./index";
import type { Items, User } from "../db";
import { get_user } from "../db";
import { BotError } from "./common/error";
import { gen_action_row } from "../util";
async function run(interaction: ChatInputCommandInteraction, user: User) {
function gen_items_embed(items_target, items: Items, page: number, pages: number) {
@@ -27,23 +28,6 @@ async function run(interaction: ChatInputCommandInteraction, user: User) {
}
return items_embed;
}
function gen_action_row(page: number, pages: number) {
let action_row = new ActionRowBuilder<ButtonBuilder>();
let action_prev = new ButtonBuilder()
.setCustomId(String(page - 1))
.setLabel("Prev")
.setEmoji("⬅️")
.setStyle(ButtonStyle.Primary)
.setDisabled(page - 1 === 0);
let action_next = new ButtonBuilder()
.setCustomId(String(page + 1))
.setLabel("Next")
.setEmoji("➡️")
.setStyle(ButtonStyle.Primary)
.setDisabled(page + 1 > pages);
action_row.addComponents(action_prev, action_next);
return action_row;
}
const options = interaction.options;
const target = (await options.get("target"))?.user;
let items_target = target ?? interaction.user;

94
commands/role_income.ts Normal file
View File

@@ -0,0 +1,94 @@
import type { ChatInputCommandInteraction } from "discord.js";
import { EmbedBuilder } from "discord.js";
import type { CommandData } from "./index";
import { BotError } from "./common/error";
import type { RoleIncome } from "../db";
import { create_role_income, delete_role_income, get_all_role_income, get_item } from "../db";
import { gen_action_row, is_admin } from "../util";
import { items_string_to_items } from "./common/common";
import config from "../config.json";
//subcommands: list, create, delete
async function run(interaction: ChatInputCommandInteraction) {
await interaction.deferReply();
const options = interaction.options;
const subcommand = options.getSubcommand();
if (subcommand === "list") {
const role_incomes = await get_all_role_income();
function gen_role_incomes_embed(role_incomes: RoleIncome[], page: number, pages: number) {
let role_income_embed = new EmbedBuilder();
role_income_embed.setTitle(`Role Incomes (Page ${page}/${pages})`);
if (Object.keys(role_incomes).length === 0) {
role_income_embed.setDescription("No role incomes");
} else {
role_income_embed.addFields(
role_incomes
.slice((page - 1) * 10, page * 10)
.map(
(role_income) =>
({
name: `${role_income.income} ${config.currency} every ${role_income.hours} hour(s)`,
value: `<@&${role_income.role}>${ role_income.items ? " (also gives " + role_income.items.map((item) => item[1] + " of `" + item[0] + "`").join(" + ") + ")" : "" }`,
})
)
);
}
return role_income_embed;
}
const pages: number = Math.ceil(role_incomes.length / 10) || 1; //min of 1
let page = 1;
const dresp = await interaction.editReply({
embeds: [ gen_role_incomes_embed(role_incomes, page, pages) ],
components: [ gen_action_row(page, pages) ],
});
while (true) {
try {
let dresp_bin = await dresp.awaitMessageComponent({ filter: (bin) => bin.user.id === interaction.user.id, time: 60000 }); //bin = button interaction
page = Number(dresp_bin.customId);
await dresp_bin.update({
embeds: [ gen_role_incomes_embed(role_incomes, page, pages) ],
components: [ gen_action_row(page, pages) ],
});
} catch (_) {
//errors when people stop pressing the button
return;
}
}
return await interaction.editReply("```json\n"+JSON.stringify(role_incomes)+"\n```");
} else if (is_admin(interaction)) {
const role_id: string = (await options.get("role")).role.id;
if (subcommand === "create") {
//hour, income
const hours: number = (await options.get("hours")).value as number;
//can be negative or zero
const income: number = (await options.get("income")).value as number;
const items_string = (await options.get("items"))?.value as (string | undefined);
let items;
if (items_string) {
items = await items_string_to_items(items_string);
if (typeof items === "string") throw new BotError(items);
}
await create_role_income(role_id, hours, income, items);
return await interaction.editReply("Created role income");
} else if (subcommand === "delete") {
await delete_role_income(role_id);
return await interaction.editReply("Deleted role income");
}
} else {
throw new BotError("Admin permission needed to run that command");
}
}
const data: CommandData = {
name: "role_income",
description: "View, create, or delete role incomes",
registered_only: false,
ephemeral: false,
admin_only: false,
run,
};
export default data;

View File

@@ -22,8 +22,8 @@ async function run(interaction: ChatInputCommandInteraction) {
.map(
(store_item: StoreItem) =>
({
name: `${store_item.name} (${store_item.price} ${ store_item.price === 1 ? config.currency : config.currency_plural })`,
value: `${store_item.description}\nUsable: ${store_item.usable}${ store_item.roles_required.length === 0 ? "" : `\nRoles required: ${store_item.roles_required.map((role_id) => `<@&${role_id}>`).join("")}` }`,
name: `${store_item.name} (${ !store_item.price ? "unbuyable" : `${store_item.price} ${ store_item.price === 1 ? config.currency : config.currency_plural }` })`,
value: `${store_item.description}\nUsable: ${store_item.usable}${ store_item.roles_required.length === 0 ? "" : `\nRoles required: ${store_item.roles_required.map((role_id) => `<@&${role_id}>`).join("")}` }${ store_item.items ? "\nItems used: " + store_item.items.map((item) => `${item[1]} of \`${item[0]}\``).join(" + ") : "" }`,
})
)
);

35
commands/transfer_item.ts Normal file
View File

@@ -0,0 +1,35 @@
import type { ChatInputCommandInteraction } from "discord.js";
import type { CommandData } from "./index";
import type { StoreItem, User } from "../db";
import { get_item, get_user, add_item_to_user, sub_item_to_user } from "../db";
import { BotError } from "./common/error";
import { item_name_autocomplete } from "./common/autocompletes";
async function run(interaction: ChatInputCommandInteraction, user: User) {
const options = interaction.options;
const name: string = (await options.get("name")).value as string;
const target_id: string = (await options.get("target")).user.id;
const quantity: number = (await options.get("quantity")).value as number;
if (quantity <= 0) throw new BotError("Can't transfer 0 or less of an item");
let trans_user = await get_user(target_id);
if (!trans_user) throw new BotError("Target is not registered");
const item = await get_item(name);
if (!item) throw new BotError("Item does not exist");
if (!(await sub_item_to_user(user.user, name, quantity))) throw new BotError("You did not have enough of that item to transfer");
await add_item_to_user(target_id, item.name, quantity);
return await interaction.editReply(`Transferred ${quantity} of \`${name}\` to <@${target_id}>`);
}
const data: CommandData = {
name: "transfer_item",
description: "Give a(n) item(s) to another user",
registered_only: true,
ephemeral: false,
admin_only: false,
run,
autocomplete: item_name_autocomplete, //autocompletes for the "name" option
};
export default data;

View File

@@ -30,5 +30,3 @@ const data: CommandData = {
export default data;