feat: add trading system with dedicated modules and centralize embed creation for commands

This commit is contained in:
syntaxbullet
2025-12-13 12:43:27 +01:00
parent 5f4efd372f
commit 421bb26ceb
13 changed files with 667 additions and 41 deletions

View File

@@ -2,6 +2,7 @@ import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, EmbedBuilder } from "discord.js";
import { economyService } from "@/modules/economy/economy.service";
import { userService } from "@/modules/user/user.service";
import { createErrorEmbed, createWarningEmbed } from "@lib/embeds";
export const daily = createCommand({
data: new SlashCommandBuilder()
@@ -27,17 +28,12 @@ export const daily = createCommand({
} catch (error: any) {
if (error.message.includes("Daily already claimed")) {
const embed = new EmbedBuilder()
.setTitle("⏳ Cooldown")
.setDescription(error.message)
.setColor("Orange");
await interaction.editReply({ embeds: [embed] });
await interaction.editReply({ embeds: [createWarningEmbed(error.message, "Cooldown")] });
return;
}
console.error(error);
await interaction.editReply({ content: "❌ An error occurred while claiming your daily reward." });
await interaction.editReply({ embeds: [createErrorEmbed("An error occurred while claiming your daily reward.")] });
}
}
});

View File

@@ -3,6 +3,7 @@ import { SlashCommandBuilder, EmbedBuilder } from "discord.js";
import { economyService } from "@/modules/economy/economy.service";
import { userService } from "@/modules/user/user.service";
import { GameConfig } from "@/config/game";
import { createErrorEmbed, createWarningEmbed } from "@lib/embeds";
export const pay = createCommand({
data: new SlashCommandBuilder()
@@ -28,12 +29,12 @@ export const pay = createCommand({
const receiverId = targetUser.id;
if (amount < GameConfig.economy.transfers.minAmount) {
await interaction.editReply({ content: `Amount must be at least ${GameConfig.economy.transfers.minAmount}.` });
await interaction.editReply({ embeds: [createWarningEmbed(`Amount must be at least ${GameConfig.economy.transfers.minAmount}.`)] });
return;
}
if (senderId === receiverId) {
await interaction.editReply({ content: "❌ You cannot pay yourself." });
await interaction.editReply({ embeds: [createWarningEmbed("You cannot pay yourself.")] });
return;
}
@@ -50,11 +51,11 @@ export const pay = createCommand({
} catch (error: any) {
if (error.message.includes("Insufficient funds")) {
await interaction.editReply({ content: "❌ Insufficient funds." });
await interaction.editReply({ embeds: [createWarningEmbed("Insufficient funds.")] });
return;
}
console.error(error);
await interaction.editReply({ content: "❌ Transfer failed." });
await interaction.editReply({ embeds: [createErrorEmbed("Transfer failed.")] });
}
}
});

View File

@@ -13,6 +13,7 @@ import {
import { userService } from "@/modules/user/user.service";
import { inventoryService } from "@/modules/inventory/inventory.service";
import type { items } from "@db/schema";
import { createErrorEmbed, createWarningEmbed } from "@lib/embeds";
export const sell = createCommand({
data: new SlashCommandBuilder()
@@ -36,18 +37,18 @@ export const sell = createCommand({
const targetChannel = (interaction.options.getChannel("channel") as BaseGuildTextChannel) || interaction.channel as BaseGuildTextChannel;
if (!targetChannel || !targetChannel.isSendable()) {
await interaction.editReply({ content: "Target channel is invalid or not sendable." });
await interaction.editReply({ content: "", embeds: [createErrorEmbed("Target channel is invalid or not sendable.")] });
return;
}
const item = await inventoryService.getItem(itemId);
if (!item) {
await interaction.editReply({ content: `Item with ID ${itemId} not found.` });
await interaction.editReply({ content: "", embeds: [createErrorEmbed(`Item with ID ${itemId} not found.`)] });
return;
}
if (!item.price) {
await interaction.editReply({ content: `Item "${item.name}" is not for sale (no price set).` });
await interaction.editReply({ content: "", embeds: [createWarningEmbed(`Item "${item.name}" is not for sale (no price set).`)] });
return;
}
@@ -82,7 +83,7 @@ export const sell = createCommand({
} catch (error) {
console.error("Failed to send sell message:", error);
await interaction.editReply({ content: "Failed to post the item for sale." });
await interaction.editReply({ content: "", embeds: [createErrorEmbed("Failed to post the item for sale.")] });
}
}
});
@@ -95,19 +96,19 @@ async function handleBuyInteraction(interaction: ButtonInteraction, item: typeof
const user = await userService.getUserById(userId);
if (!user) {
await interaction.editReply({ content: "User profile not found." });
await interaction.editReply({ content: "", embeds: [createErrorEmbed("User profile not found.")] });
return;
}
if ((user.balance ?? 0n) < (item.price ?? 0n)) {
await interaction.editReply({ content: `You don't have enough money! You need ${item.price} 🪙.` });
await interaction.editReply({ content: "", embeds: [createWarningEmbed(`You don't have enough money! You need ${item.price} 🪙.`)] });
return;
}
const result = await inventoryService.buyItem(userId, item.id, 1n);
if (!result.success) {
await interaction.editReply({ content: "Transaction failed. Please try again." });
await interaction.editReply({ content: "", embeds: [createErrorEmbed("Transaction failed. Please try again.")] });
return;
}
@@ -115,9 +116,9 @@ async function handleBuyInteraction(interaction: ButtonInteraction, item: typeof
} catch (error) {
console.error("Error processing purchase:", error);
if (interaction.deferred || interaction.replied) {
await interaction.editReply({ content: "An error occurred while processing your purchase." });
await interaction.editReply({ content: "", embeds: [createErrorEmbed("An error occurred while processing your purchase.")] });
} else {
await interaction.reply({ content: "An error occurred while processing your purchase.", ephemeral: true });
await interaction.reply({ embeds: [createErrorEmbed("An error occurred while processing your purchase.")], ephemeral: true });
}
}
}

View File

@@ -0,0 +1,87 @@
import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, EmbedBuilder, ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, ThreadAutoArchiveDuration } from "discord.js";
import { TradeService } from "@/modules/trade/trade.service";
import { updateTradeDashboard } from "@/modules/trade/trade.interaction";
import { createErrorEmbed, createWarningEmbed } from "@lib/embeds";
export const trade = createCommand({
data: new SlashCommandBuilder()
.setName("trade")
.setDescription("Start a trade with another player")
.addUserOption(option =>
option.setName("user")
.setDescription("The user to trade with")
.setRequired(true)
),
execute: async (interaction) => {
const targetUser = interaction.options.getUser("user", true);
if (targetUser.id === interaction.user.id) {
await interaction.reply({ embeds: [createWarningEmbed("You cannot trade with yourself.")], ephemeral: true });
return;
}
if (targetUser.bot) {
await interaction.reply({ embeds: [createWarningEmbed("You cannot trade with bots.")], ephemeral: true });
return;
}
// Create Thread
const channel = interaction.channel;
if (!channel || channel.type === ChannelType.DM) {
await interaction.reply({ embeds: [createErrorEmbed("Cannot start trade in DMs.")], ephemeral: true });
return;
}
// Check if we can create threads
// Assuming permissions are fine.
await interaction.reply({ content: `🔄 Setting up trade with ${targetUser}...` });
const message = await interaction.fetchReply();
let thread;
try {
thread = await message.startThread({
name: `trade-${interaction.user.username}-${targetUser.username}`,
autoArchiveDuration: ThreadAutoArchiveDuration.OneHour,
reason: "Trading Session"
});
} catch (e) {
// Fallback if message threads fail, try channel threads (private preferred)
// But startThread on message is usually easiest.
await interaction.editReply({ content: "", embeds: [createErrorEmbed("Failed to create trade thread. Check permissions.")] });
return;
}
// Setup Session
TradeService.createSession(thread.id,
{ id: interaction.user.id, username: interaction.user.username },
{ id: targetUser.id, username: targetUser.username }
);
// Send Dashboard to Thread
const embed = new EmbedBuilder()
.setTitle("🤝 Trading Session")
.setDescription(`Trade started between ${interaction.user} and ${targetUser}.\nUse the controls below to build your offer.`)
.setColor(0xFFD700)
.addFields(
{ name: interaction.user.username, value: "*Empty Offer*", inline: true },
{ name: targetUser.username, value: "*Empty Offer*", inline: true }
)
.setFooter({ text: "Both parties must click Lock to confirm trade." });
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents(
new ButtonBuilder().setCustomId('trade_add_item').setLabel('Add Item').setStyle(ButtonStyle.Secondary),
new ButtonBuilder().setCustomId('trade_add_money').setLabel('Add Money').setStyle(ButtonStyle.Success),
new ButtonBuilder().setCustomId('trade_remove_item').setLabel('Remove Item').setStyle(ButtonStyle.Secondary),
new ButtonBuilder().setCustomId('trade_lock').setLabel('Lock / Unlock').setStyle(ButtonStyle.Primary),
new ButtonBuilder().setCustomId('trade_cancel').setLabel('Cancel').setStyle(ButtonStyle.Danger),
);
await thread.send({ content: `${interaction.user} ${targetUser} Welcome to your trading session!`, embeds: [embed], components: [row] });
// Update original reply
await interaction.editReply({ content: `✅ Trade opened: <#${thread.id}>` });
}
});