219 lines
7.5 KiB
TypeScript
219 lines
7.5 KiB
TypeScript
import {
|
|
ActionRowBuilder,
|
|
ButtonBuilder,
|
|
ButtonStyle,
|
|
ContainerBuilder,
|
|
TextDisplayBuilder,
|
|
SeparatorBuilder,
|
|
SeparatorSpacingSize,
|
|
MessageFlags
|
|
} 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;
|
|
}
|
|
|
|
// 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
|
|
*/
|
|
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(" • ") || "None";
|
|
}
|
|
|
|
/**
|
|
* 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)}%`;
|
|
}
|
|
|
|
/**
|
|
* Creates Components v2 containers for the quest list (active quests only)
|
|
*/
|
|
export function getQuestListComponents(userQuests: QuestEntry[]): ContainerBuilder[] {
|
|
// Filter to only show in-progress quests (not completed)
|
|
const activeQuests = userQuests.filter(entry => entry.completedAt === null);
|
|
|
|
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];
|
|
}
|
|
|
|
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);
|
|
|
|
container.addTextDisplayComponents(
|
|
new TextDisplayBuilder().setContent(`**${entry.quest.name}**`),
|
|
new TextDisplayBuilder().setContent(entry.quest.description || "*No description*"),
|
|
new TextDisplayBuilder().setContent(`📊 ${progressBar} \`${progress}/${target}\` • 🎁 ${rewardsText}`)
|
|
);
|
|
});
|
|
|
|
return [container];
|
|
}
|
|
|
|
/**
|
|
* Creates Components v2 containers for available quests with inline accept buttons
|
|
*/
|
|
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) {
|
|
container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small));
|
|
container.addTextDisplayComponents(
|
|
new TextDisplayBuilder().setContent("*No new quests available at the moment.*")
|
|
);
|
|
return [container];
|
|
}
|
|
|
|
// 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;
|
|
|
|
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 [container];
|
|
}
|
|
|
|
/**
|
|
* Returns action rows for navigation only
|
|
*/
|
|
export function getQuestActionRows(viewType: 'active' | 'available'): ActionRowBuilder<ButtonBuilder>[] {
|
|
// Navigation row
|
|
const navRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
new ButtonBuilder()
|
|
.setCustomId("quest_view_active")
|
|
.setLabel("📜 Active")
|
|
.setStyle(viewType === 'active' ? ButtonStyle.Primary : ButtonStyle.Secondary)
|
|
.setDisabled(viewType === 'active'),
|
|
new ButtonBuilder()
|
|
.setCustomId("quest_view_available")
|
|
.setLabel("🗺️ Available")
|
|
.setStyle(viewType === 'available' ? ButtonStyle.Primary : ButtonStyle.Secondary)
|
|
.setDisabled(viewType === 'available')
|
|
);
|
|
|
|
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 }
|
|
};
|
|
}
|