163 lines
5.2 KiB
TypeScript
163 lines
5.2 KiB
TypeScript
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from "discord.js";
|
|
|
|
/**
|
|
* Quest entry with quest details and progress
|
|
*/
|
|
interface QuestEntry {
|
|
progress: number | null;
|
|
completedAt: Date | null;
|
|
quest: {
|
|
id: number;
|
|
name: string;
|
|
description: string | null;
|
|
triggerEvent: string;
|
|
requirements: any;
|
|
rewards: any;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Available quest interface
|
|
*/
|
|
interface AvailableQuest {
|
|
id: number;
|
|
name: string;
|
|
description: string | null;
|
|
rewards: any;
|
|
requirements: any;
|
|
}
|
|
|
|
/**
|
|
* Formats quest rewards object into a human-readable string
|
|
*/
|
|
function formatQuestRewards(rewards: { xp?: number, balance?: number }): string {
|
|
const rewardStr: string[] = [];
|
|
if (rewards?.xp) rewardStr.push(`${rewards.xp} XP`);
|
|
if (rewards?.balance) rewardStr.push(`${rewards.balance} 🪙`);
|
|
return rewardStr.join(", ");
|
|
}
|
|
|
|
/**
|
|
* Returns the quest status display string
|
|
*/
|
|
function getQuestStatus(completedAt: Date | null): string {
|
|
return completedAt ? "✅ Completed" : "📝 In Progress";
|
|
}
|
|
|
|
/**
|
|
* Renders a simple progress bar
|
|
*/
|
|
function renderProgressBar(current: number, total: number, size: number = 10): string {
|
|
const percentage = Math.min(current / total, 1);
|
|
const progress = Math.round(size * percentage);
|
|
const empty = size - progress;
|
|
|
|
const progressText = "▰".repeat(progress);
|
|
const emptyText = "▱".repeat(empty);
|
|
|
|
return `${progressText}${emptyText} ${Math.round(percentage * 100)}% (${current}/${total})`;
|
|
}
|
|
|
|
/**
|
|
* Creates an embed displaying a user's quest log
|
|
*/
|
|
export function getQuestListEmbed(userQuests: QuestEntry[]): EmbedBuilder {
|
|
const embed = new EmbedBuilder()
|
|
.setTitle("📜 Quest Log")
|
|
.setDescription("Your active and completed quests.")
|
|
.setColor(0x3498db); // Blue
|
|
|
|
if (userQuests.length === 0) {
|
|
embed.setDescription("You have no active quests. Check available quests!");
|
|
}
|
|
|
|
userQuests.forEach(entry => {
|
|
const status = getQuestStatus(entry.completedAt);
|
|
const rewards = entry.quest.rewards as { xp?: number, balance?: number };
|
|
const rewardsText = formatQuestRewards(rewards);
|
|
|
|
const requirements = entry.quest.requirements as { target?: number };
|
|
const target = requirements?.target || 1;
|
|
const progress = entry.progress || 0;
|
|
|
|
const progressBar = entry.completedAt ? "✅ Fully completed" : renderProgressBar(progress, target);
|
|
|
|
embed.addFields({
|
|
name: `${entry.quest.name} (${status})`,
|
|
value: `${entry.quest.description}\n**Rewards:** ${rewardsText}\n**Progress:** ${progressBar}`,
|
|
inline: false
|
|
});
|
|
});
|
|
|
|
return embed;
|
|
}
|
|
|
|
/**
|
|
* Creates an embed for available quests
|
|
*/
|
|
export function getAvailableQuestsEmbed(availableQuests: AvailableQuest[]): EmbedBuilder {
|
|
const embed = new EmbedBuilder()
|
|
.setTitle("🗺️ Available Quests")
|
|
.setDescription("Quests you can accept right now.")
|
|
.setColor(0x2ecc71); // Green
|
|
|
|
if (availableQuests.length === 0) {
|
|
embed.setDescription("There are no new quests available for you at the moment.");
|
|
}
|
|
|
|
availableQuests.forEach(quest => {
|
|
const rewards = quest.rewards as { xp?: number, balance?: number };
|
|
const rewardsText = formatQuestRewards(rewards);
|
|
|
|
const requirements = quest.requirements as { target?: number };
|
|
const target = requirements?.target || 1;
|
|
|
|
embed.addFields({
|
|
name: quest.name,
|
|
value: `${quest.description}\n**Goal:** Reach ${target} for this activity.\n**Rewards:** ${rewardsText}`,
|
|
inline: false
|
|
});
|
|
});
|
|
|
|
return embed;
|
|
}
|
|
|
|
/**
|
|
* Returns action rows for the quest view
|
|
*/
|
|
export function getQuestActionRows(viewType: 'active' | 'available', availableQuests: AvailableQuest[] = []): ActionRowBuilder<any>[] {
|
|
const rows: ActionRowBuilder<any>[] = [];
|
|
|
|
const navRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
new ButtonBuilder()
|
|
.setCustomId("quest_view_active")
|
|
.setLabel("Active Quests")
|
|
.setStyle(viewType === 'active' ? ButtonStyle.Primary : ButtonStyle.Secondary)
|
|
.setDisabled(viewType === 'active'),
|
|
new ButtonBuilder()
|
|
.setCustomId("quest_view_available")
|
|
.setLabel("Available Quests")
|
|
.setStyle(viewType === 'available' ? ButtonStyle.Primary : ButtonStyle.Secondary)
|
|
.setDisabled(viewType === 'available')
|
|
);
|
|
rows.push(navRow);
|
|
|
|
if (viewType === 'available' && availableQuests.length > 0) {
|
|
const selectMenu = new StringSelectMenuBuilder()
|
|
.setCustomId("quest_accept_select")
|
|
.setPlaceholder("Select a quest to accept")
|
|
.addOptions(
|
|
availableQuests.slice(0, 25).map(q =>
|
|
new StringSelectMenuOptionBuilder()
|
|
.setLabel(q.name)
|
|
.setDescription(q.description?.substring(0, 100) || "")
|
|
.setValue(q.id.toString())
|
|
)
|
|
);
|
|
|
|
rows.push(new ActionRowBuilder().addComponents(selectMenu));
|
|
}
|
|
|
|
return rows;
|
|
}
|