feat(economy): enhance minigame commands to track stats, quest progress, and achievements

This commit is contained in:
2025-10-05 05:35:32 -05:00
parent b10cc89583
commit 0990533f6e
15 changed files with 954 additions and 30 deletions

View File

@@ -0,0 +1,109 @@
import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { prisma } from '../../../core/database/prisma';
import { EmbedBuilder } from 'discord.js';
export const command: CommandMessage = {
name: 'cooldowns',
type: 'message',
aliases: ['cds', 'tiempos', 'cd'],
cooldown: 3,
description: 'Ver todos tus cooldowns activos',
usage: 'cooldowns',
run: async (message, args, client: Amayo) => {
try {
const userId = message.author.id;
const guildId = message.guild!.id;
// Obtener todos los cooldowns activos
const cooldowns = await prisma.actionCooldown.findMany({
where: {
userId,
guildId,
until: { gt: new Date() }
},
orderBy: { until: 'asc' }
});
if (cooldowns.length === 0) {
await message.reply('✅ No tienes cooldowns activos. ¡Puedes realizar cualquier acción!');
return;
}
const embed = new EmbedBuilder()
.setColor(0xFF6B6B)
.setTitle('⏰ Cooldowns Activos')
.setDescription(`${message.author.username}, estos son tus cooldowns:`)
.setThumbnail(message.author.displayAvatarURL({ size: 128 }));
// Emojis por tipo de acción
const actionEmojis: Record<string, string> = {
'mine': '⛏️',
'fish': '🎣',
'fight': '⚔️',
'farm': '🌾',
'craft': '🛠️',
'smelt': '🔥',
'shop': '🛒',
'daily': '🎁',
'consume': '🍖'
};
// Traducción de acciones
const actionNames: Record<string, string> = {
'mine': 'Minar',
'fish': 'Pescar',
'fight': 'Pelear',
'farm': 'Granja',
'craft': 'Craftear',
'smelt': 'Fundir',
'shop': 'Tienda',
'daily': 'Diario',
'consume': 'Consumir'
};
let cooldownText = '';
const now = Date.now();
for (const cd of cooldowns) {
const remainingMs = cd.until.getTime() - now;
const remainingSec = Math.ceil(remainingMs / 1000);
// Formatear tiempo
let timeStr = '';
if (remainingSec >= 3600) {
const hours = Math.floor(remainingSec / 3600);
const mins = Math.floor((remainingSec % 3600) / 60);
timeStr = `${hours}h ${mins}m`;
} else if (remainingSec >= 60) {
const mins = Math.floor(remainingSec / 60);
const secs = remainingSec % 60;
timeStr = `${mins}m ${secs}s`;
} else {
timeStr = `${remainingSec}s`;
}
// Buscar emoji y nombre
const action = cd.key.split(':')[0];
const emoji = actionEmojis[action] || '⏱️';
const actionName = actionNames[action] || cd.key;
cooldownText += `${emoji} **${actionName}**: ${timeStr}\n`;
}
embed.addFields({
name: `📋 Cooldowns (${cooldowns.length})`,
value: cooldownText,
inline: false
});
embed.setFooter({ text: 'Los cooldowns se actualizan en tiempo real' });
embed.setTimestamp();
await message.reply({ embeds: [embed] });
} catch (error) {
console.error('Error en comando cooldowns:', error);
await message.reply('❌ Error al obtener los cooldowns.');
}
}
};

View File

@@ -0,0 +1,117 @@
import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { getPlayerAchievements, getAchievementStats, createProgressBar } from '../../../game/achievements/service';
import { EmbedBuilder } from 'discord.js';
export const command: CommandMessage = {
name: 'logros',
type: 'message',
aliases: ['achievements', 'logro', 'achievement'],
cooldown: 5,
description: 'Ver tus logros desbloqueados y progreso',
usage: 'logros [@usuario]',
run: async (message, args, client: Amayo) => {
try {
const guildId = message.guild!.id;
const targetUser = message.mentions.users.first() || message.author;
const userId = targetUser.id;
// Obtener logros del jugador
const { unlocked, inProgress } = await getPlayerAchievements(userId, guildId);
const achievementStats = await getAchievementStats(userId, guildId);
const embed = new EmbedBuilder()
.setColor(0xFFD700)
.setTitle(`🏆 Logros de ${targetUser.username}`)
.setThumbnail(targetUser.displayAvatarURL({ size: 128 }))
.setDescription(
`**${achievementStats.unlocked}/${achievementStats.total}** logros desbloqueados ` +
`(${achievementStats.percentage}%)\n` +
`⭐ **${achievementStats.points}** puntos totales`
);
// Logros desbloqueados recientes (últimos 5)
if (unlocked.length > 0) {
const recentUnlocked = unlocked.slice(0, 5);
let unlockedText = '';
for (const pa of recentUnlocked) {
const icon = pa.achievement.icon || '🏆';
const points = pa.achievement.points || 10;
unlockedText += `${icon} **${pa.achievement.name}** (+${points} pts)\n`;
unlockedText += `${pa.achievement.description}\n`;
}
embed.addFields({
name: `✅ Desbloqueados Recientes (${unlocked.length})`,
value: unlockedText || 'Ninguno aún',
inline: false
});
}
// Logros en progreso (top 5)
if (inProgress.length > 0) {
const topInProgress = inProgress.slice(0, 5);
let progressText = '';
for (const pa of topInProgress) {
const icon = pa.achievement.icon || '🔒';
const req = pa.achievement.requirements as any;
const progress = pa.progress;
const required = req.value;
const bar = createProgressBar(progress, required, 8);
progressText += `${icon} **${pa.achievement.name}**\n`;
progressText += `${bar} (${progress}/${required})\n`;
}
embed.addFields({
name: `📈 En Progreso (${inProgress.length})`,
value: progressText,
inline: false
});
}
// Categorías
const categories = ['mining', 'fishing', 'combat', 'economy', 'exploration'];
const categoryEmojis: Record<string, string> = {
mining: '⛏️',
fishing: '🎣',
combat: '⚔️',
economy: '💰',
exploration: '🗺️'
};
let categoryText = '';
for (const cat of categories) {
const count = unlocked.filter(pa => pa.achievement.category === cat).length;
if (count > 0) {
categoryText += `${categoryEmojis[cat]} ${count} `;
}
}
if (categoryText) {
embed.addFields({
name: '📊 Por Categoría',
value: categoryText,
inline: false
});
}
if (unlocked.length === 0 && inProgress.length === 0) {
embed.setDescription(
'Aún no has desbloqueado ningún logro.\n' +
'¡Empieza a jugar para obtener logros y puntos!'
);
}
embed.setFooter({ text: 'Los logros se desbloquean automáticamente al cumplir requisitos' });
embed.setTimestamp();
await message.reply({ embeds: [embed] });
} catch (error) {
console.error('Error en comando logros:', error);
await message.reply('❌ Error al obtener los logros.');
}
}
};

View File

@@ -2,6 +2,9 @@ import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { runMinigame } from '../../../game/minigames/service';
import { resolveArea, getDefaultLevel, findBestToolKey } from './_helpers';
import { updateStats } from '../../../game/stats/service';
import { updateQuestProgress } from '../../../game/quests/service';
import { checkAchievements } from '../../../game/achievements/service';
export const command: CommandMessage = {
name: 'mina',
@@ -26,13 +29,34 @@ export const command: CommandMessage = {
try {
const result = await runMinigame(userId, guildId, areaKey, level, { toolKey: toolKey ?? undefined });
// Actualizar stats
await updateStats(userId, guildId, { minesCompleted: 1 });
// Actualizar progreso de misiones
await updateQuestProgress(userId, guildId, 'mine_count', 1);
// Verificar logros
const newAchievements = await checkAchievements(userId, guildId, 'mine_count');
const rewards = result.rewards.map(r => r.type === 'coins' ? `🪙 +${r.amount}` : `📦 ${r.itemKey} x${r.qty}`).join(' · ') || '—';
const mobs = result.mobs.length ? result.mobs.join(', ') : '—';
const toolInfo = result.tool?.key ? `🔧 ${result.tool.key}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta} dur.)`}` : '—';
await message.reply(`⛏️ Mina (nivel ${level})
let response = `⛏️ Mina (nivel ${level})
Recompensas: ${rewards}
Mobs: ${mobs}
Herramienta: ${toolInfo}`);
Herramienta: ${toolInfo}`;
// Notificar logros desbloqueados
if (newAchievements.length > 0) {
response += `\n\n🏆 ¡Logro desbloqueado!`;
for (const ach of newAchievements) {
response += `\n✨ **${ach.name}** - ${ach.description}`;
}
}
await message.reply(response);
} catch (e: any) {
await message.reply(`❌ No se pudo minar: ${e?.message ?? e}`);
}

View File

@@ -0,0 +1,73 @@
import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { claimQuestReward, getPlayerQuests } from '../../../game/quests/service';
import { EmbedBuilder } from 'discord.js';
export const command: CommandMessage = {
name: 'mision-reclamar',
type: 'message',
aliases: ['claim-quest', 'reclamar-mision'],
cooldown: 3,
description: 'Reclamar recompensa de misión completada',
usage: 'mision-reclamar <numero>',
run: async (message, args, client: Amayo) => {
try {
const userId = message.author.id;
const guildId = message.guild!.id;
if (!args[0]) {
await message.reply(`❌ Uso: \`!mision-reclamar <numero>\`\nEjemplo: \`!mision-reclamar 1\``);
return;
}
// Obtener misiones completadas
const quests = await getPlayerQuests(userId, guildId);
const allQuests = [...quests.daily, ...quests.weekly, ...quests.permanent, ...quests.event];
const claimable = allQuests.filter(q => q.canClaim);
if (claimable.length === 0) {
await message.reply('❌ No tienes misiones listas para reclamar. Completa misiones primero usando los comandos del bot.');
return;
}
const index = parseInt(args[0]) - 1;
if (isNaN(index) || index < 0 || index >= claimable.length) {
await message.reply(`❌ Número de misión inválido. Elige un número entre 1 y ${claimable.length}.`);
return;
}
const selected = claimable[index];
// Reclamar recompensa
const { quest, rewards } = await claimQuestReward(userId, guildId, selected.quest.id);
const embed = new EmbedBuilder()
.setColor(0x00FF00)
.setTitle('🎉 ¡Misión Completada!')
.setDescription(`Has reclamado las recompensas de **${quest.name}**`)
.setThumbnail(message.author.displayAvatarURL({ size: 128 }));
// Mostrar recompensas
if (rewards.length > 0) {
embed.addFields({
name: '🎁 Recompensas Recibidas',
value: rewards.join('\n'),
inline: false
});
}
// Info de la misión
embed.addFields(
{ name: '📜 Misión', value: quest.description, inline: false }
);
embed.setFooter({ text: `Usa !misiones para ver más misiones` });
embed.setTimestamp();
await message.reply({ embeds: [embed] });
} catch (error: any) {
console.error('Error en comando mision-reclamar:', error);
await message.reply(`${error.message || 'Error al reclamar la misión.'}`);
}
}
};

View File

@@ -0,0 +1,157 @@
import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { getPlayerQuests, claimQuestReward } from '../../../game/quests/service';
import { createProgressBar } from '../../../game/achievements/service';
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
export const command: CommandMessage = {
name: 'misiones',
type: 'message',
aliases: ['quests', 'mision', 'quest'],
cooldown: 5,
description: 'Ver misiones disponibles y tu progreso',
usage: 'misiones [categoria]',
run: async (message, args, client: Amayo) => {
try {
const userId = message.author.id;
const guildId = message.guild!.id;
// Obtener misiones con progreso
const quests = await getPlayerQuests(userId, guildId);
const embed = new EmbedBuilder()
.setColor(0x5865F2)
.setTitle('📜 Misiones Disponibles')
.setDescription(`${message.author.username}, aquí están tus misiones:`)
.setThumbnail(message.author.displayAvatarURL({ size: 128 }));
// Emojis por categoría
const categoryEmojis: Record<string, string> = {
mining: '⛏️',
fishing: '🎣',
combat: '⚔️',
economy: '💰',
exploration: '🗺️',
crafting: '🛠️'
};
// Función para formatear una lista de misiones
const formatQuests = (questList: any[], type: string) => {
if (questList.length === 0) return null;
let text = '';
for (const { quest, progress, canClaim, percentage } of questList) {
const icon = categoryEmojis[quest.category] || '📋';
const req = quest.requirements as any;
const currentProgress = progress?.progress || 0;
const required = req.count;
// Estado
let status = '';
if (canClaim) {
status = '✅ ¡Listo para reclamar!';
} else if (progress?.completed) {
status = '🎁 Completada';
} else {
const bar = createProgressBar(currentProgress, required, 8);
status = `${bar}`;
}
text += `${icon} **${quest.name}**\n`;
text += `${quest.description}\n`;
text += `${status}\n`;
// Recompensas
const rewards = quest.rewards as any;
let rewardStr = '';
if (rewards.coins) rewardStr += `💰 ${rewards.coins} `;
if (rewards.items && rewards.items.length > 0) {
rewardStr += `📦 ${rewards.items.length} items `;
}
if (rewardStr) {
text += `└ Recompensa: ${rewardStr}\n`;
}
text += '\n';
}
return text;
};
// Misiones diarias
if (quests.daily.length > 0) {
const dailyText = formatQuests(quests.daily, 'daily');
if (dailyText) {
embed.addFields({
name: '📅 Misiones Diarias',
value: dailyText,
inline: false
});
}
}
// Misiones semanales
if (quests.weekly.length > 0) {
const weeklyText = formatQuests(quests.weekly, 'weekly');
if (weeklyText) {
embed.addFields({
name: '📆 Misiones Semanales',
value: weeklyText,
inline: false
});
}
}
// Misiones permanentes
if (quests.permanent.length > 0) {
const permanentText = formatQuests(quests.permanent.slice(0, 3), 'permanent');
if (permanentText) {
embed.addFields({
name: '♾️ Misiones Permanentes',
value: permanentText,
inline: false
});
}
}
// Misiones de evento
if (quests.event.length > 0) {
const eventText = formatQuests(quests.event, 'event');
if (eventText) {
embed.addFields({
name: '🎉 Misiones de Evento',
value: eventText,
inline: false
});
}
}
// Verificar si hay misiones para reclamar
const canClaim = [...quests.daily, ...quests.weekly, ...quests.permanent, ...quests.event]
.filter(q => q.canClaim);
if (canClaim.length > 0) {
embed.addFields({
name: '🎁 ¡Misiones Listas!',
value: `Tienes **${canClaim.length}** misiones listas para reclamar.\nUsa \`!mision-reclamar <id>\` para reclamar recompensas.`,
inline: false
});
}
if (quests.daily.length === 0 && quests.weekly.length === 0 && quests.permanent.length === 0) {
embed.setDescription(
'No hay misiones disponibles en este momento.\n' +
'Las misiones diarias se generan automáticamente cada día.'
);
}
embed.setFooter({ text: 'Completa misiones para ganar recompensas' });
embed.setTimestamp();
await message.reply({ embeds: [embed] });
} catch (error) {
console.error('Error en comando misiones:', error);
await message.reply('❌ Error al obtener las misiones.');
}
}
};

View File

@@ -2,6 +2,9 @@ import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { runMinigame } from '../../../game/minigames/service';
import { resolveArea, getDefaultLevel, findBestToolKey } from './_helpers';
import { updateStats } from '../../../game/stats/service';
import { updateQuestProgress } from '../../../game/quests/service';
import { checkAchievements } from '../../../game/achievements/service';
export const command: CommandMessage = {
name: 'pelear',
@@ -26,13 +29,37 @@ export const command: CommandMessage = {
try {
const result = await runMinigame(userId, guildId, areaKey, level, { toolKey: toolKey ?? undefined });
// Actualizar stats y misiones
await updateStats(userId, guildId, { fightsCompleted: 1 });
await updateQuestProgress(userId, guildId, 'fight_count', 1);
// Contar mobs derrotados
const mobsCount = result.mobs.length;
if (mobsCount > 0) {
await updateStats(userId, guildId, { mobsDefeated: mobsCount });
await updateQuestProgress(userId, guildId, 'mob_defeat_count', mobsCount);
}
const newAchievements = await checkAchievements(userId, guildId, 'fight_count');
const rewards = result.rewards.map(r => r.type === 'coins' ? `🪙 +${r.amount}` : `🎁 ${r.itemKey} x${r.qty}`).join(' · ') || '—';
const mobs = result.mobs.length ? result.mobs.join(', ') : '—';
const toolInfo = result.tool?.key ? `🗡️ ${result.tool.key}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta} dur.)`}` : '—';
await message.reply(`⚔️ Arena (nivel ${level})
let response = `⚔️ Arena (nivel ${level})
Recompensas: ${rewards}
Enemigos: ${mobs}
Arma: ${toolInfo}`);
Arma: ${toolInfo}`;
if (newAchievements.length > 0) {
response += `\n\n🏆 ¡Logro desbloqueado!`;
for (const ach of newAchievements) {
response += `\n✨ **${ach.name}** - ${ach.description}`;
}
}
await message.reply(response);
} catch (e: any) {
await message.reply(`❌ No se pudo pelear: ${e?.message ?? e}`);
}

View File

@@ -2,6 +2,9 @@ import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { runMinigame } from '../../../game/minigames/service';
import { resolveArea, getDefaultLevel, findBestToolKey } from './_helpers';
import { updateStats } from '../../../game/stats/service';
import { updateQuestProgress } from '../../../game/quests/service';
import { checkAchievements } from '../../../game/achievements/service';
export const command: CommandMessage = {
name: 'pescar',
@@ -26,13 +29,29 @@ export const command: CommandMessage = {
try {
const result = await runMinigame(userId, guildId, areaKey, level, { toolKey: toolKey ?? undefined });
// Actualizar stats y misiones
await updateStats(userId, guildId, { fishingCompleted: 1 });
await updateQuestProgress(userId, guildId, 'fish_count', 1);
const newAchievements = await checkAchievements(userId, guildId, 'fish_count');
const rewards = result.rewards.map(r => r.type === 'coins' ? `🪙 +${r.amount}` : `🐟 ${r.itemKey} x${r.qty}`).join(' · ') || '—';
const mobs = result.mobs.length ? result.mobs.join(', ') : '—';
const toolInfo = result.tool?.key ? `🎣 ${result.tool.key}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta} dur.)`}` : '—';
await message.reply(`🎣 Pesca (nivel ${level})
let response = `🎣 Pesca (nivel ${level})
Recompensas: ${rewards}
Mobs: ${mobs}
Herramienta: ${toolInfo}`);
Herramienta: ${toolInfo}`;
if (newAchievements.length > 0) {
response += `\n\n🏆 ¡Logro desbloqueado!`;
for (const ach of newAchievements) {
response += `\n✨ **${ach.name}** - ${ach.description}`;
}
}
await message.reply(response);
} catch (e: any) {
await message.reply(`❌ No se pudo pescar: ${e?.message ?? e}`);
}

View File

@@ -66,7 +66,7 @@ export const command: CommandMessage = {
embed.addFields({ name: '🏆 Récords', value: recordsText || 'Sin datos', inline: true });
}
embed.setFooter({ text: `Usa ${client.prefix}ranking-stats para ver el ranking global` });
embed.setFooter({ text: 'Usa !ranking-stats para ver el ranking global' });
await message.reply({ embeds: [embed] });
} catch (error) {