Command to allow the user to buy more effects (#424)
All checks were successful
Deploy To Stage / build (push) Successful in 54s
Deploy To Stage / deploy (push) Successful in 15s

# Description

- Create a command to generate an embed for the user to be able to buy more effects
- This embed will contain the details about the effect as well as 2 buttons; Confirm and Cancel
- The confirm button will call the button event to:
    - Remove the currency from the user
    - Give the user the effect to their inventory
- The cancel button will just disable the buttons, so the user can't accidentally use it if they don't want to.

#381

## Type of change

Please delete options that are not relevant.

- [x] New feature (non-breaking change which adds functionality)

# How Has This Been Tested?

- Have created unit tests and tested locally

# Checklist

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that provde my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules

Reviewed-on: #424
Reviewed-by: VylpesTester <tester@vylpes.com>
Co-authored-by: Ethan Lane <ethan@vylpes.com>
Co-committed-by: Ethan Lane <ethan@vylpes.com>
This commit is contained in:
Ethan Lane 2025-03-26 18:30:16 +00:00 committed by Vylpes
parent c6b458199c
commit 5089ad6ab6
20 changed files with 942 additions and 97 deletions

View file

@ -3,6 +3,7 @@ import { ButtonEvent } from "../type/buttonEvent";
import List from "./Effects/List";
import Use from "./Effects/Use";
import AppLogger from "../client/appLogger";
import Buy from "./Effects/Buy";
export default class Effects extends ButtonEvent {
public override async execute(interaction: ButtonInteraction) {
@ -15,6 +16,9 @@ export default class Effects extends ButtonEvent {
case "use":
await Use.Execute(interaction);
break;
case "buy":
await Buy.Execute(interaction);
break;
default:
AppLogger.LogError("Buttons/Effects", `Unknown action, ${action}`);
}

View file

@ -0,0 +1,120 @@
import {ButtonInteraction} from "discord.js";
import AppLogger from "../../client/appLogger";
import EffectHelper from "../../helpers/EffectHelper";
import EmbedColours from "../../constants/EmbedColours";
import User from "../../database/entities/app/User";
import {EffectDetails} from "../../constants/EffectDetails";
export default class Buy {
public static async Execute(interaction: ButtonInteraction) {
const subaction = interaction.customId.split(" ")[2];
switch (subaction) {
case "confirm":
await this.Confirm(interaction);
break;
case "cancel":
await this.Cancel(interaction);
break;
default:
AppLogger.LogError("Buy", `Unknown subaction, effects ${subaction}`);
}
}
private static async Confirm(interaction: ButtonInteraction) {
const id = interaction.customId.split(" ")[3];
const quantity = interaction.customId.split(" ")[4];
if (!id || !quantity) {
AppLogger.LogError("Buy Confirm", "Not enough parameters");
return;
}
const effectDetail = EffectDetails.get(id);
if (!effectDetail) {
AppLogger.LogError("Buy Confirm", "Effect detail not found!");
return;
}
const quantityNumber = Number(quantity);
if (!quantityNumber || quantityNumber < 1) {
AppLogger.LogError("Buy Confirm", "Invalid number");
return;
}
const totalCost = effectDetail.cost * quantityNumber;
const user = await User.FetchOneById(User, interaction.user.id);
if (!user) {
AppLogger.LogError("Buy Confirm", "Unable to find user");
return;
}
if (user.Currency < totalCost) {
interaction.reply(`You don't have enough currency to buy this! You have \`${user.Currency} Currency\` and need \`${totalCost} Currency\`!`);
return;
}
user.RemoveCurrency(totalCost);
await user.Save(User, user);
await EffectHelper.AddEffectToUserInventory(interaction.user.id, id, quantityNumber);
const generatedEmbed = await EffectHelper.GenerateEffectBuyEmbed(interaction.user.id, id, quantityNumber, true);
if (typeof generatedEmbed == "string") {
await interaction.reply(generatedEmbed);
return;
}
generatedEmbed.embed.setColor(EmbedColours.Success);
generatedEmbed.embed.setFooter({ text: "Purchased" });
await interaction.update({
embeds: [ generatedEmbed.embed ],
components: [ generatedEmbed.row ],
});
}
private static async Cancel(interaction: ButtonInteraction) {
const id = interaction.customId.split(" ")[3];
const quantity = interaction.customId.split(" ")[4];
if (!id || !quantity) {
AppLogger.LogError("Buy Cancel", "Not enough parameters");
return;
}
const effectDetail = EffectDetails.get(id);
if (!effectDetail) {
AppLogger.LogError("Buy Cancel", "Effect detail not found!");
return;
}
const quantityNumber = Number(quantity);
if (!quantityNumber || quantityNumber < 1) {
AppLogger.LogError("Buy Cancel", "Invalid number");
return;
}
const generatedEmbed = await EffectHelper.GenerateEffectBuyEmbed(interaction.user.id, id, quantityNumber, true);
if (typeof generatedEmbed == "string") {
await interaction.reply(generatedEmbed);
return;
}
generatedEmbed.embed.setColor(EmbedColours.Error);
generatedEmbed.embed.setFooter({ text: "Cancelled" });
await interaction.update({
embeds: [ generatedEmbed.embed ],
components: [ generatedEmbed.row ],
});
}
}

View file

@ -11,7 +11,7 @@ export default async function List(interaction: ButtonInteraction) {
return;
}
const result = await EffectHelper.GenerateEffectEmbed(interaction.user.id, page);
const result = await EffectHelper.GenerateEffectListEmbed(interaction.user.id, page);
await interaction.update({
embeds: [ result.embed ],

View file

@ -1,10 +1,10 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, SlashCommandBuilder } from "discord.js";
import { CommandInteraction, SlashCommandBuilder } from "discord.js";
import { Command } from "../type/command";
import EffectHelper from "../helpers/EffectHelper";
import { EffectDetails } from "../constants/EffectDetails";
import TimeLengthInput from "../helpers/TimeLengthInput";
import EmbedColours from "../constants/EmbedColours";
import { EffectChoices } from "../constants/EffectDetails";
import AppLogger from "../client/appLogger";
import List from "./effects/List";
import Use from "./effects/Use";
import Buy from "./effects/Buy";
export default class Effects extends Command {
constructor() {
@ -27,9 +27,19 @@ export default class Effects extends Command {
.setName("id")
.setDescription("The effect id to use")
.setRequired(true)
.setChoices([
{ name: "Unclaimed Chance Up", value: "unclaimed" },
])));
.setChoices(EffectChoices)))
.addSubcommand(x => x
.setName("buy")
.setDescription("Buy more effects")
.addStringOption(y => y
.setName("id")
.setDescription("The effect id to buy")
.setRequired(true)
.setChoices(EffectChoices))
.addNumberOption(y => y
.setName("quantity")
.setDescription("The amount to buy")
.setMinValue(1)));
}
public override async execute(interaction: CommandInteraction) {
@ -39,80 +49,16 @@ export default class Effects extends Command {
switch (subcommand) {
case "list":
await this.List(interaction);
await List(interaction);
break;
case "use":
await this.Use(interaction);
await Use(interaction);
break;
case "buy":
await Buy(interaction);
break;
default:
AppLogger.LogError("Commands/Effects", `Invalid subcommand: ${subcommand}`);
}
}
private async List(interaction: CommandInteraction) {
const pageOption = interaction.options.get("page");
const page = !isNaN(Number(pageOption?.value)) ? Number(pageOption?.value) : 1;
const result = await EffectHelper.GenerateEffectEmbed(interaction.user.id, page);
await interaction.reply({
embeds: [ result.embed ],
components: [ result.row ],
});
}
private async Use(interaction: CommandInteraction) {
const id = interaction.options.get("id", true).value!.toString();
const effectDetail = EffectDetails.get(id);
if (!effectDetail) {
AppLogger.LogWarn("Commands/Effects", `Unable to find effect details for ${id}`);
await interaction.reply("Unable to find effect!");
return;
}
const canUseEffect = await EffectHelper.CanUseEffect(interaction.user.id, id);
if (!canUseEffect) {
await interaction.reply("Unable to use effect! Please make sure you have it in your inventory and is not on cooldown");
return;
}
const timeLengthInput = TimeLengthInput.ConvertFromMilliseconds(effectDetail.duration);
const embed = new EmbedBuilder()
.setTitle("Effect Confirmation")
.setDescription("Would you like to use this effect?")
.setColor(EmbedColours.Ok)
.addFields([
{
name: "Effect",
value: effectDetail.friendlyName,
inline: true,
},
{
name: "Length",
value: timeLengthInput.GetLengthShort(),
inline: true,
},
]);
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents([
new ButtonBuilder()
.setLabel("Confirm")
.setCustomId(`effects use confirm ${effectDetail.id}`)
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setLabel("Cancel")
.setCustomId(`effects use cancel ${effectDetail.id}`)
.setStyle(ButtonStyle.Danger),
]);
await interaction.reply({
embeds: [ embed ],
components: [ row ],
});
}
}

View file

@ -0,0 +1,22 @@
import { CommandInteraction } from "discord.js";
import EffectHelper from "../../helpers/EffectHelper";
export default async function Buy(interaction: CommandInteraction) {
const id = interaction.options.get("id", true).value!;
const quantity = interaction.options.get("quantity")?.value ?? 1;
const idValue = id.toString();
const quantityValue = Number(quantity);
const result = await EffectHelper.GenerateEffectBuyEmbed(interaction.user.id, idValue, quantityValue, false);
if (typeof result == "string") {
await interaction.reply(result);
return;
}
await interaction.reply({
embeds: [ result.embed ],
components: [ result.row ],
});
}

View file

@ -0,0 +1,15 @@
import { CommandInteraction } from "discord.js";
import EffectHelper from "../../helpers/EffectHelper";
export default async function List(interaction: CommandInteraction) {
const pageOption = interaction.options.get("page");
const page = !isNaN(Number(pageOption?.value)) ? Number(pageOption?.value) : 1;
const result = await EffectHelper.GenerateEffectListEmbed(interaction.user.id, page);
await interaction.reply({
embeds: [ result.embed ],
components: [ result.row ],
});
}

View file

@ -0,0 +1,62 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder } from "discord.js";
import { EffectDetails } from "../../constants/EffectDetails";
import AppLogger from "../../client/appLogger";
import EffectHelper from "../../helpers/EffectHelper";
import TimeLengthInput from "../../helpers/TimeLengthInput";
import EmbedColours from "../../constants/EmbedColours";
export default async function Use(interaction: CommandInteraction) {
const id = interaction.options.get("id", true).value!.toString();
const effectDetail = EffectDetails.get(id);
if (!effectDetail) {
AppLogger.LogWarn("Commands/Effects", `Unable to find effect details for ${id}`);
await interaction.reply("Unable to find effect!");
return;
}
const canUseEffect = await EffectHelper.CanUseEffect(interaction.user.id, id);
if (!canUseEffect) {
await interaction.reply("Unable to use effect! Please make sure you have it in your inventory and is not on cooldown");
return;
}
const timeLengthInput = TimeLengthInput.ConvertFromMilliseconds(effectDetail.duration);
const embed = new EmbedBuilder()
.setTitle("Effect Confirmation")
.setDescription("Would you like to use this effect?")
.setColor(EmbedColours.Ok)
.addFields([
{
name: "Effect",
value: effectDetail.friendlyName,
inline: true,
},
{
name: "Length",
value: timeLengthInput.GetLengthShort(),
inline: true,
},
]);
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents([
new ButtonBuilder()
.setLabel("Confirm")
.setCustomId(`effects use confirm ${effectDetail.id}`)
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setLabel("Cancel")
.setCustomId(`effects use cancel ${effectDetail.id}`)
.setStyle(ButtonStyle.Danger),
]);
await interaction.reply({
embeds: [ embed ],
components: [ row ],
});
}

View file

@ -17,3 +17,7 @@ class EffectDetail {
export const EffectDetails = new Map<string, EffectDetail>([
[ "unclaimed", new EffectDetail("unclaimed", "Unclaimed Chance Up", 10 * 60 * 1000, 100, 3 * 60 * 60 * 1000) ],
]);
export const EffectChoices = [
{ name: "Unclaimed Chance Up", value: "unclaimed" },
];

View file

@ -2,6 +2,9 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "disc
import UserEffect from "../database/entities/app/UserEffect";
import EmbedColours from "../constants/EmbedColours";
import { EffectDetails } from "../constants/EffectDetails";
import User from "../database/entities/app/User";
import CardConstants from "../constants/CardConstants";
import AppLogger from "../client/appLogger";
export default class EffectHelper {
public static async AddEffectToUserInventory(userId: string, name: string, quantity: number = 1) {
@ -66,7 +69,7 @@ export default class EffectHelper {
return true;
}
public static async GenerateEffectEmbed(userId: string, page: number): Promise<{
public static async GenerateEffectListEmbed(userId: string, page: number): Promise<{
embed: EmbedBuilder,
row: ActionRowBuilder<ButtonBuilder>,
}> {
@ -126,4 +129,66 @@ export default class EffectHelper {
row,
};
}
public static async GenerateEffectBuyEmbed(userId: string, id: string, quantity: number, disabled: boolean): Promise<{
embed: EmbedBuilder,
row: ActionRowBuilder<ButtonBuilder>,
} | string> {
const effectDetail = EffectDetails.get(id);
if (!effectDetail) {
return "Effect detail not found!";
}
const totalCost = effectDetail.cost * quantity;
let user = await User.FetchOneById(User, userId);
if (!user) {
user = new User(userId, CardConstants.StartingCurrency);
await user.Save(User, user);
AppLogger.LogInfo("EffectHelper", `Created initial user entity for : ${userId}`);
}
if (user.Currency < totalCost) {
return `You don't have enough currency to buy this! You have \`${user.Currency} Currency\` and need \`${totalCost} Currency\`!`;
}
const embed = new EmbedBuilder()
.setTitle("Buy Effect")
.setDescription(effectDetail.friendlyName)
.setColor(EmbedColours.Ok)
.addFields([
{
name: "Cost",
value: `${totalCost}`,
inline: true,
},
{
name: "Quantity",
value: `${quantity}`,
inline: true,
},
]);
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents([
new ButtonBuilder()
.setCustomId(`effects buy confirm ${id} ${quantity}`)
.setLabel("Confirm")
.setStyle(ButtonStyle.Success)
.setDisabled(disabled),
new ButtonBuilder()
.setCustomId(`effects buy cancel ${id} ${quantity}`)
.setLabel("Cancel")
.setStyle(ButtonStyle.Danger)
.setDisabled(disabled),
]);
return {
embed,
row,
}
}
}