forked from syntaxbullet/aurorabot
feat: Add web admin page for quest management and refactor Discord bot's quest UI to use new components.
This commit is contained in:
@@ -1,4 +1,13 @@
|
||||
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from "discord.js";
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
ContainerBuilder,
|
||||
TextDisplayBuilder,
|
||||
SeparatorBuilder,
|
||||
SeparatorSpacingSize,
|
||||
MessageFlags
|
||||
} from "discord.js";
|
||||
|
||||
/**
|
||||
* Quest entry with quest details and progress
|
||||
@@ -27,6 +36,13 @@ interface AvailableQuest {
|
||||
requirements: any;
|
||||
}
|
||||
|
||||
// Color palette for containers
|
||||
const COLORS = {
|
||||
ACTIVE: 0x3498db, // Blue - in progress
|
||||
AVAILABLE: 0x2ecc71, // Green - available
|
||||
COMPLETED: 0xf1c40f // Gold - completed
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats quest rewards object into a human-readable string
|
||||
*/
|
||||
@@ -34,14 +50,7 @@ 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";
|
||||
return rewardStr.join(" • ") || "None";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,108 +64,155 @@ function renderProgressBar(current: number, total: number, size: number = 10): s
|
||||
const progressText = "▰".repeat(progress);
|
||||
const emptyText = "▱".repeat(empty);
|
||||
|
||||
return `${progressText}${emptyText} ${Math.round(percentage * 100)}% (${current}/${total})`;
|
||||
return `${progressText}${emptyText} ${Math.round(percentage * 100)}%`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an embed displaying a user's quest log
|
||||
* Creates Components v2 containers for the quest list (active quests only)
|
||||
*/
|
||||
export function getQuestListEmbed(userQuests: QuestEntry[]): EmbedBuilder {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("📜 Quest Log")
|
||||
.setDescription("Your active and completed quests.")
|
||||
.setColor(0x3498db); // Blue
|
||||
export function getQuestListComponents(userQuests: QuestEntry[]): ContainerBuilder[] {
|
||||
// Filter to only show in-progress quests (not completed)
|
||||
const activeQuests = userQuests.filter(entry => entry.completedAt === null);
|
||||
|
||||
if (userQuests.length === 0) {
|
||||
embed.setDescription("You have no active quests. Check available quests!");
|
||||
const container = new ContainerBuilder()
|
||||
.setAccentColor(COLORS.ACTIVE)
|
||||
.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent("# 📜 Quest Log"),
|
||||
new TextDisplayBuilder().setContent("-# Your active quests")
|
||||
);
|
||||
|
||||
if (activeQuests.length === 0) {
|
||||
container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small));
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent("*You have no active quests. Check available quests!*")
|
||||
);
|
||||
return [container];
|
||||
}
|
||||
|
||||
userQuests.forEach(entry => {
|
||||
const status = getQuestStatus(entry.completedAt);
|
||||
activeQuests.forEach((entry) => {
|
||||
container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small));
|
||||
|
||||
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 = renderProgressBar(progress, target);
|
||||
|
||||
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
|
||||
});
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(`**${entry.quest.name}**`),
|
||||
new TextDisplayBuilder().setContent(entry.quest.description || "*No description*"),
|
||||
new TextDisplayBuilder().setContent(`📊 ${progressBar} \`${progress}/${target}\` • 🎁 ${rewardsText}`)
|
||||
);
|
||||
});
|
||||
|
||||
return embed;
|
||||
return [container];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an embed for available quests
|
||||
* Creates Components v2 containers for available quests with inline accept buttons
|
||||
*/
|
||||
export function getAvailableQuestsEmbed(availableQuests: AvailableQuest[]): EmbedBuilder {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("🗺️ Available Quests")
|
||||
.setDescription("Quests you can accept right now.")
|
||||
.setColor(0x2ecc71); // Green
|
||||
export function getAvailableQuestsComponents(availableQuests: AvailableQuest[]): ContainerBuilder[] {
|
||||
const container = new ContainerBuilder()
|
||||
.setAccentColor(COLORS.AVAILABLE)
|
||||
.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent("# 🗺️ Available Quests"),
|
||||
new TextDisplayBuilder().setContent("-# Quests you can accept")
|
||||
);
|
||||
|
||||
if (availableQuests.length === 0) {
|
||||
embed.setDescription("There are no new quests available for you at the moment.");
|
||||
container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small));
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent("*No new quests available at the moment.*")
|
||||
);
|
||||
return [container];
|
||||
}
|
||||
|
||||
availableQuests.forEach(quest => {
|
||||
// Limit to 10 quests (5 action rows max with 2 added for navigation)
|
||||
const questsToShow = availableQuests.slice(0, 10);
|
||||
|
||||
questsToShow.forEach((quest) => {
|
||||
container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small));
|
||||
|
||||
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
|
||||
});
|
||||
container.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(`**${quest.name}**`),
|
||||
new TextDisplayBuilder().setContent(quest.description || "*No description*"),
|
||||
new TextDisplayBuilder().setContent(`🎯 Goal: \`${target}\` • 🎁 ${rewardsText}`)
|
||||
);
|
||||
|
||||
// Add accept button inline within the container
|
||||
container.addActionRowComponents(
|
||||
new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`quest_accept:${quest.id}`)
|
||||
.setLabel("Accept Quest")
|
||||
.setStyle(ButtonStyle.Success)
|
||||
.setEmoji("✅")
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return embed;
|
||||
return [container];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns action rows for the quest view
|
||||
* Returns action rows for navigation only
|
||||
*/
|
||||
export function getQuestActionRows(viewType: 'active' | 'available', availableQuests: AvailableQuest[] = []): ActionRowBuilder<any>[] {
|
||||
const rows: ActionRowBuilder<any>[] = [];
|
||||
|
||||
export function getQuestActionRows(viewType: 'active' | 'available'): ActionRowBuilder<ButtonBuilder>[] {
|
||||
// Navigation row
|
||||
const navRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId("quest_view_active")
|
||||
.setLabel("Active Quests")
|
||||
.setLabel("📜 Active")
|
||||
.setStyle(viewType === 'active' ? ButtonStyle.Primary : ButtonStyle.Secondary)
|
||||
.setDisabled(viewType === 'active'),
|
||||
new ButtonBuilder()
|
||||
.setCustomId("quest_view_available")
|
||||
.setLabel("Available Quests")
|
||||
.setLabel("🗺️ Available")
|
||||
.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;
|
||||
return [navRow];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Components v2 celebratory message for quest completion
|
||||
*/
|
||||
export function getQuestCompletionComponents(quest: any, rewards: { xp: bigint, balance: bigint }): ContainerBuilder[] {
|
||||
const rewardsText = formatQuestRewards({
|
||||
xp: Number(rewards.xp),
|
||||
balance: Number(rewards.balance)
|
||||
});
|
||||
|
||||
const container = new ContainerBuilder()
|
||||
.setAccentColor(COLORS.COMPLETED)
|
||||
.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent("# 🎉 Quest Completed!"),
|
||||
new TextDisplayBuilder().setContent(`Congratulations! You've completed **${quest.name}**`)
|
||||
)
|
||||
.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small))
|
||||
.addTextDisplayComponents(
|
||||
new TextDisplayBuilder().setContent(`📝 ${quest.description || "No description provided."}`),
|
||||
new TextDisplayBuilder().setContent(`🎁 **Rewards Earned:** ${rewardsText}`)
|
||||
);
|
||||
|
||||
return [container];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets MessageFlags and allowedMentions for Components v2 messages
|
||||
*/
|
||||
export function getComponentsV2MessageFlags() {
|
||||
return {
|
||||
flags: MessageFlags.IsComponentsV2,
|
||||
allowedMentions: { parse: [] as const }
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user