Files
discord-rpg-concept/bot/modules/quest/quest.view.ts

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 }
};
}