diff --git a/src/commands/messages/admin/logroEliminar.ts b/src/commands/messages/admin/logroEliminar.ts new file mode 100644 index 0000000..708160f --- /dev/null +++ b/src/commands/messages/admin/logroEliminar.ts @@ -0,0 +1,61 @@ +import type { CommandMessage } from '../../../core/types/commands'; +import type Amayo from '../../../core/client'; +import { hasManageGuildOrStaff } from '../../../core/lib/permissions'; +import { prisma } from '../../../core/database/prisma'; + +export const command: CommandMessage = { + name: 'logro-eliminar', + type: 'message', + aliases: ['eliminar-logro', 'achievement-delete'], + cooldown: 5, + description: 'Eliminar un logro del servidor', + usage: 'logro-eliminar ', + run: async (message, args, client: Amayo) => { + const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, prisma); + if (!allowed) { + await message.reply('❌ No tienes permisos de ManageGuild ni rol de staff.'); + return; + } + + const guildId = message.guild!.id; + const key = args[0]?.trim(); + + if (!key) { + await message.reply('Uso: `!logro-eliminar `\nEjemplo: `!logro-eliminar test_achievement`'); + return; + } + + const achievement = await prisma.achievement.findFirst({ + where: { key, guildId } + }); + + if (!achievement) { + await message.reply(`❌ No se encontró el logro local con key \`${key}\` en este servidor.\n` + + `💡 Solo puedes eliminar logros locales del servidor, no globales.`); + return; + } + + // Contar cuántos jugadores lo han desbloqueado + const unlockedCount = await prisma.playerAchievement.count({ + where: { + achievementId: achievement.id, + unlockedAt: { not: null } + } + }); + + // Eliminar progreso de jugadores primero + await prisma.playerAchievement.deleteMany({ + where: { achievementId: achievement.id } + }); + + // Eliminar el logro + await prisma.achievement.delete({ + where: { id: achievement.id } + }); + + await message.reply( + `✅ Logro \`${key}\` eliminado exitosamente.\n` + + `${unlockedCount > 0 ? `⚠️ Se eliminó el progreso de ${unlockedCount} jugador(es).` : ''}` + ); + } +}; diff --git a/src/commands/messages/admin/logroVer.ts b/src/commands/messages/admin/logroVer.ts new file mode 100644 index 0000000..92d45e9 --- /dev/null +++ b/src/commands/messages/admin/logroVer.ts @@ -0,0 +1,122 @@ +import type { CommandMessage } from '../../../core/types/commands'; +import type Amayo from '../../../core/client'; +import { prisma } from '../../../core/database/prisma'; +import { ComponentType, ButtonStyle } from 'discord-api-types/v10'; + +export const command: CommandMessage = { + name: 'logro-ver', + type: 'message', + aliases: ['ver-logro', 'achievement-view'], + cooldown: 3, + description: 'Ver detalles de un logro específico', + usage: 'logro-ver ', + run: async (message, args, client: Amayo) => { + const guildId = message.guild!.id; + const key = args[0]?.trim(); + + if (!key) { + await message.reply('Uso: `!logro-ver `\nEjemplo: `!logro-ver first_mine`'); + return; + } + + const achievement = await prisma.achievement.findFirst({ + where: { + key, + OR: [{ guildId }, { guildId: null }] + }, + include: { + unlocked: { + take: 10, + orderBy: { unlockedAt: 'desc' }, + where: { unlockedAt: { not: null } } + } + } + }); + + if (!achievement) { + await message.reply(`❌ No se encontró el logro con key \`${key}\``); + return; + } + + const unlockedCount = await prisma.playerAchievement.count({ + where: { + achievementId: achievement.id, + guildId, + unlockedAt: { not: null } + } + }); + + const req = achievement.requirements as any; + const rew = achievement.rewards as any; + + const display = { + type: 17, + accent_color: 0xFFD700, + components: [ + { + type: 9, + components: [ + { + type: 10, + content: `${achievement.icon || '🏆'} **${achievement.name}**` + } + ] + }, + { type: 14, divider: true }, + { + type: 9, + components: [ + { + type: 10, + content: `**Descripción:** ${achievement.description}\n` + + `**Key:** \`${achievement.key}\`\n` + + `**Categoría:** ${achievement.category}\n` + + `**Puntos:** ${achievement.points} pts\n` + + `**Visibilidad:** ${achievement.hidden ? '🔒 Oculto' : '👁️ Visible'}\n` + + `**Ámbito:** ${achievement.guildId ? '📍 Local del servidor' : '🌐 Global'}\n` + + `**Desbloqueados:** ${unlockedCount} jugadores` + } + ] + }, + { type: 14, divider: true }, + { + type: 9, + components: [ + { + type: 10, + content: `**📋 Requisitos:**\n\`\`\`json\n${JSON.stringify(req, null, 2)}\n\`\`\`` + } + ] + }, + { type: 14, divider: true }, + { + type: 9, + components: [ + { + type: 10, + content: `**🎁 Recompensas:**\n\`\`\`json\n${JSON.stringify(rew, null, 2)}\n\`\`\`` + } + ] + } + ] + }; + + if (achievement.unlocked.length > 0) { + display.components.push({ type: 14, divider: true }); + display.components.push({ + type: 9, + components: [ + { + type: 10, + content: `**🏆 Últimos Desbloqueados:**\n` + + achievement.unlocked.slice(0, 5).map(pa => + `• <@${pa.userId}> - ${pa.unlockedAt ? new Date(pa.unlockedAt).toLocaleDateString() : 'N/A'}` + ).join('\n') + } + ] + }); + } + + await message.reply({ display } as any); + } +}; diff --git a/src/commands/messages/admin/logrosLista.ts b/src/commands/messages/admin/logrosLista.ts new file mode 100644 index 0000000..897111c --- /dev/null +++ b/src/commands/messages/admin/logrosLista.ts @@ -0,0 +1,135 @@ +import type { CommandMessage } from '../../../core/types/commands'; +import type Amayo from '../../../core/client'; +import { hasManageGuildOrStaff } from '../../../core/lib/permissions'; +import { prisma } from '../../../core/database/prisma'; +import { ComponentType, ButtonStyle } from 'discord-api-types/v10'; +import type { MessageComponentInteraction, TextBasedChannel } from 'discord.js'; + +export const command: CommandMessage = { + name: 'logros-lista', + type: 'message', + aliases: ['lista-logros', 'achievements-list'], + cooldown: 5, + description: 'Ver lista de todos los logros del servidor', + usage: 'logros-lista [pagina]', + run: async (message, args, client: Amayo) => { + const guildId = message.guild!.id; + const page = parseInt(args[0]) || 1; + const perPage = 5; + + const total = await prisma.achievement.count({ + where: { OR: [{ guildId }, { guildId: null }] } + }); + + const achievements = await prisma.achievement.findMany({ + where: { OR: [{ guildId }, { guildId: null }] }, + orderBy: [{ category: 'asc' }, { points: 'desc' }], + skip: (page - 1) * perPage, + take: perPage + }); + + if (achievements.length === 0) { + await message.reply('No hay logros configurados en este servidor.'); + return; + } + + const totalPages = Math.ceil(total / perPage); + + const display = { + type: 17, + accent_color: 0xFFD700, + components: [ + { + type: 9, + components: [ + { + type: 10, + content: `**🏆 Lista de Logros**\nPágina ${page}/${totalPages} • Total: ${total}` + } + ] + }, + { type: 14, divider: true }, + ...achievements.map(ach => ({ + type: 9, + components: [ + { + type: 10, + content: `${ach.icon || '🏆'} **${ach.name}** (${ach.points} pts)\n` + + `└ Key: \`${ach.key}\`\n` + + `└ Categoría: ${ach.category}\n` + + `└ ${ach.description}\n` + + `└ ${ach.hidden ? '🔒 Oculto' : '👁️ Visible'}` + + (ach.guildId === guildId ? ' • 📍 Local' : ' • 🌐 Global') + } + ] + })) + ] + }; + + const buttons: any[] = []; + + if (page > 1) { + buttons.push({ + type: ComponentType.Button, + style: ButtonStyle.Secondary, + label: '◀ Anterior', + custom_id: `ach_list_prev_${page}` + }); + } + + if (page < totalPages) { + buttons.push({ + type: ComponentType.Button, + style: ButtonStyle.Secondary, + label: 'Siguiente ▶', + custom_id: `ach_list_next_${page}` + }); + } + + buttons.push({ + type: ComponentType.Button, + style: ButtonStyle.Primary, + label: 'Ver Detalle', + custom_id: 'ach_view_detail' + }); + + const channel = message.channel as TextBasedChannel & { send: Function }; + const msg = await (channel.send as any)({ + display, + components: buttons.length > 0 ? [{ + type: ComponentType.ActionRow, + components: buttons + }] : [] + }); + + const collector = msg.createMessageComponentCollector({ + time: 5 * 60_000, + filter: (i) => i.user.id === message.author.id + }); + + collector.on('collect', async (i: MessageComponentInteraction) => { + if (!i.isButton()) return; + + if (i.customId.startsWith('ach_list_prev_')) { + const currentPage = parseInt(i.customId.split('_')[3]); + await i.deferUpdate(); + // Re-ejecutar comando con página anterior + args[0] = String(currentPage - 1); + await command.run!(message, args, client); + collector.stop(); + } else if (i.customId.startsWith('ach_list_next_')) { + const currentPage = parseInt(i.customId.split('_')[3]); + await i.deferUpdate(); + // Re-ejecutar comando con página siguiente + args[0] = String(currentPage + 1); + await command.run!(message, args, client); + collector.stop(); + } else if (i.customId === 'ach_view_detail') { + await i.reply({ + content: '💡 Usa `!logro-ver ` para ver detalles de un logro específico.', + flags: 64 + }); + } + }); + } +}; diff --git a/src/commands/messages/admin/misionEliminar.ts b/src/commands/messages/admin/misionEliminar.ts new file mode 100644 index 0000000..7a0102e --- /dev/null +++ b/src/commands/messages/admin/misionEliminar.ts @@ -0,0 +1,58 @@ +import type { CommandMessage } from '../../../core/types/commands'; +import type Amayo from '../../../core/client'; +import { hasManageGuildOrStaff } from '../../../core/lib/permissions'; +import { prisma } from '../../../core/database/prisma'; + +export const command: CommandMessage = { + name: 'mision-eliminar', + type: 'message', + aliases: ['eliminar-mision', 'quest-delete'], + cooldown: 5, + description: 'Eliminar una misión del servidor', + usage: 'mision-eliminar ', + run: async (message, args, client: Amayo) => { + const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, prisma); + if (!allowed) { + await message.reply('❌ No tienes permisos de ManageGuild ni rol de staff.'); + return; + } + + const guildId = message.guild!.id; + const key = args[0]?.trim(); + + if (!key) { + await message.reply('Uso: `!mision-eliminar `\nEjemplo: `!mision-eliminar daily_mine`'); + return; + } + + const quest = await prisma.quest.findFirst({ + where: { key, guildId } + }); + + if (!quest) { + await message.reply(`❌ No se encontró la misión local con key \`${key}\` en este servidor.\n` + + `💡 Solo puedes eliminar misiones locales del servidor, no globales.`); + return; + } + + // Contar cuántos progresos existen + const progressCount = await prisma.questProgress.count({ + where: { questId: quest.id } + }); + + // Eliminar progreso de jugadores primero + await prisma.questProgress.deleteMany({ + where: { questId: quest.id } + }); + + // Eliminar la misión + await prisma.quest.delete({ + where: { id: quest.id } + }); + + await message.reply( + `✅ Misión \`${key}\` eliminada exitosamente.\n` + + `${progressCount > 0 ? `⚠️ Se eliminó el progreso de ${progressCount} registro(s).` : ''}` + ); + } +}; diff --git a/src/commands/messages/admin/misionVer.ts b/src/commands/messages/admin/misionVer.ts new file mode 100644 index 0000000..832c646 --- /dev/null +++ b/src/commands/messages/admin/misionVer.ts @@ -0,0 +1,155 @@ +import type { CommandMessage } from '../../../core/types/commands'; +import type Amayo from '../../../core/client'; +import { prisma } from '../../../core/database/prisma'; + +export const command: CommandMessage = { + name: 'mision-ver', + type: 'message', + aliases: ['ver-mision', 'quest-view'], + cooldown: 3, + description: 'Ver detalles de una misión específica', + usage: 'mision-ver ', + run: async (message, args, client: Amayo) => { + const guildId = message.guild!.id; + const key = args[0]?.trim(); + + if (!key) { + await message.reply('Uso: `!mision-ver `\nEjemplo: `!mision-ver daily_mine`'); + return; + } + + const quest = await prisma.quest.findFirst({ + where: { + key, + OR: [{ guildId }, { guildId: null }] + }, + include: { + progress: { + take: 10, + orderBy: { completedAt: 'desc' }, + where: { completed: true } + } + } + }); + + if (!quest) { + await message.reply(`❌ No se encontró la misión con key \`${key}\``); + return; + } + + const completedCount = await prisma.questProgress.count({ + where: { + questId: quest.id, + guildId, + completed: true + } + }); + + const claimedCount = await prisma.questProgress.count({ + where: { + questId: quest.id, + guildId, + claimed: true + } + }); + + const req = quest.requirements as any; + const rew = quest.rewards as any; + + const typeEmojis: Record = { + daily: '📅 Diaria', + weekly: '📆 Semanal', + permanent: '♾️ Permanente', + event: '🎉 Evento' + }; + + const display = { + type: 17, + accent_color: 0x5865F2, + components: [ + { + type: 9, + components: [ + { + type: 10, + content: `${quest.icon || '📋'} **${quest.name}**` + } + ] + }, + { type: 14, divider: true }, + { + type: 9, + components: [ + { + type: 10, + content: `**Descripción:** ${quest.description}\n` + + `**Key:** \`${quest.key}\`\n` + + `**Tipo:** ${typeEmojis[quest.type] || quest.type}\n` + + `**Categoría:** ${quest.category}\n` + + `**Estado:** ${quest.active ? '✅ Activa' : '❌ Inactiva'}\n` + + `**Repetible:** ${quest.repeatable ? '🔄 Sí' : '1️⃣ No'}\n` + + `**Ámbito:** ${quest.guildId ? '📍 Local del servidor' : '🌐 Global'}\n` + + `**Completadas:** ${completedCount} veces\n` + + `**Recompensas reclamadas:** ${claimedCount} veces` + } + ] + } + ] + }; + + if (quest.startAt || quest.endAt) { + display.components.push({ type: 14, divider: true }); + display.components.push({ + type: 9, + components: [ + { + type: 10, + content: `**⏰ Disponibilidad:**\n` + + (quest.startAt ? `Inicio: ${new Date(quest.startAt).toLocaleString()}\n` : '') + + (quest.endAt ? `Fin: ${new Date(quest.endAt).toLocaleString()}` : 'Sin fecha de fin') + } + ] + }); + } + + display.components.push({ type: 14, divider: true }); + display.components.push({ + type: 9, + components: [ + { + type: 10, + content: `**📋 Requisitos:**\n\`\`\`json\n${JSON.stringify(req, null, 2)}\n\`\`\`` + } + ] + }); + + display.components.push({ type: 14, divider: true }); + display.components.push({ + type: 9, + components: [ + { + type: 10, + content: `**🎁 Recompensas:**\n\`\`\`json\n${JSON.stringify(rew, null, 2)}\n\`\`\`` + } + ] + }); + + if (quest.progress.length > 0) { + display.components.push({ type: 14, divider: true }); + display.components.push({ + type: 9, + components: [ + { + type: 10, + content: `**✅ Últimas Completaciones:**\n` + + quest.progress.slice(0, 5).map(qp => + `• <@${qp.userId}> - ${qp.completedAt ? new Date(qp.completedAt).toLocaleDateString() : 'N/A'} ${qp.claimed ? '🎁' : ''}` + ).join('\n') + } + ] + }); + } + + await message.reply({ display } as any); + } +}; diff --git a/src/commands/messages/admin/misionesLista.ts b/src/commands/messages/admin/misionesLista.ts new file mode 100644 index 0000000..7b6d3eb --- /dev/null +++ b/src/commands/messages/admin/misionesLista.ts @@ -0,0 +1,140 @@ +import type { CommandMessage } from '../../../core/types/commands'; +import type Amayo from '../../../core/client'; +import { prisma } from '../../../core/database/prisma'; +import { ComponentType, ButtonStyle } from 'discord-api-types/v10'; +import type { MessageComponentInteraction, TextBasedChannel } from 'discord.js'; + +export const command: CommandMessage = { + name: 'misiones-lista', + type: 'message', + aliases: ['lista-misiones', 'quests-list'], + cooldown: 5, + description: 'Ver lista de todas las misiones del servidor', + usage: 'misiones-lista [pagina]', + run: async (message, args, client: Amayo) => { + const guildId = message.guild!.id; + const page = parseInt(args[0]) || 1; + const perPage = 5; + + const total = await prisma.quest.count({ + where: { OR: [{ guildId }, { guildId: null }] } + }); + + const quests = await prisma.quest.findMany({ + where: { OR: [{ guildId }, { guildId: null }] }, + orderBy: [{ type: 'asc' }, { category: 'asc' }], + skip: (page - 1) * perPage, + take: perPage + }); + + if (quests.length === 0) { + await message.reply('No hay misiones configuradas en este servidor.'); + return; + } + + const totalPages = Math.ceil(total / perPage); + + const typeEmojis: Record = { + daily: '📅', + weekly: '📆', + permanent: '♾️', + event: '🎉' + }; + + const display = { + type: 17, + accent_color: 0x5865F2, + components: [ + { + type: 9, + components: [ + { + type: 10, + content: `**📜 Lista de Misiones**\nPágina ${page}/${totalPages} • Total: ${total}` + } + ] + }, + { type: 14, divider: true }, + ...quests.map(quest => ({ + type: 9, + components: [ + { + type: 10, + content: `${quest.icon || '📋'} **${quest.name}** ${typeEmojis[quest.type] || '📋'}\n` + + `└ Key: \`${quest.key}\`\n` + + `└ Tipo: ${quest.type} • Categoría: ${quest.category}\n` + + `└ ${quest.description}\n` + + `└ ${quest.active ? '✅ Activa' : '❌ Inactiva'} • ` + + `${quest.repeatable ? '🔄 Repetible' : '1️⃣ Una vez'}` + + (quest.guildId === guildId ? ' • 📍 Local' : ' • 🌐 Global') + } + ] + })) + ] + }; + + const buttons: any[] = []; + + if (page > 1) { + buttons.push({ + type: ComponentType.Button, + style: ButtonStyle.Secondary, + label: '◀ Anterior', + custom_id: `quest_list_prev_${page}` + }); + } + + if (page < totalPages) { + buttons.push({ + type: ComponentType.Button, + style: ButtonStyle.Secondary, + label: 'Siguiente ▶', + custom_id: `quest_list_next_${page}` + }); + } + + buttons.push({ + type: ComponentType.Button, + style: ButtonStyle.Primary, + label: 'Ver Detalle', + custom_id: 'quest_view_detail' + }); + + const channel = message.channel as TextBasedChannel & { send: Function }; + const msg = await (channel.send as any)({ + display, + components: buttons.length > 0 ? [{ + type: ComponentType.ActionRow, + components: buttons + }] : [] + }); + + const collector = msg.createMessageComponentCollector({ + time: 5 * 60_000, + filter: (i) => i.user.id === message.author.id + }); + + collector.on('collect', async (i: MessageComponentInteraction) => { + if (!i.isButton()) return; + + if (i.customId.startsWith('quest_list_prev_')) { + const currentPage = parseInt(i.customId.split('_')[3]); + await i.deferUpdate(); + args[0] = String(currentPage - 1); + await command.run!(message, args, client); + collector.stop(); + } else if (i.customId.startsWith('quest_list_next_')) { + const currentPage = parseInt(i.customId.split('_')[3]); + await i.deferUpdate(); + args[0] = String(currentPage + 1); + await command.run!(message, args, client); + collector.stop(); + } else if (i.customId === 'quest_view_detail') { + await i.reply({ + content: '💡 Usa `!mision-ver ` para ver detalles de una misión específica.', + flags: 64 + }); + } + }); + } +}; diff --git a/src/commands/messages/game/player.ts b/src/commands/messages/game/player.ts index 7283e06..cdad3b6 100644 --- a/src/commands/messages/game/player.ts +++ b/src/commands/messages/game/player.ts @@ -3,30 +3,16 @@ import type Amayo from '../../../core/client'; import { prisma } from '../../../core/database/prisma'; import { getOrCreateWallet } from '../../../game/economy/service'; import { getEquipment, getEffectiveStats } from '../../../game/combat/equipmentService'; -import type { ItemProps } from '../../../game/economy/types'; - -function parseItemProps(json: unknown): ItemProps { - if (!json || typeof json !== 'object') return {}; - return json as ItemProps; -} - -function fmtTool(props: ItemProps) { - const t = props.tool; - if (!t) return ''; - const icon = t.type === 'pickaxe' ? '⛏️' : t.type === 'rod' ? '🎣' : t.type === 'sword' ? '🗡️' : t.type === 'bow' ? '🏹' : t.type === 'halberd' ? '⚔️' : t.type === 'net' ? '🕸️' : '🔧'; - const tier = t.tier != null ? ` T${t.tier}` : ''; - return `${icon}${tier}`; -} +import { getPlayerStatsFormatted } from '../../../game/stats/service'; export const command: CommandMessage = { name: 'player', type: 'message', aliases: ['perfil', 'profile', 'yo', 'me'], cooldown: 5, - description: 'Muestra toda tu información de jugador: stats, equipo, progreso y últimas actividades.', + description: 'Muestra toda tu información de jugador con vista visual mejorada', usage: 'player [@usuario]', run: async (message, args, _client: Amayo) => { - // Permitir ver perfil de otros usuarios mencionándolos const targetUser = message.mentions.users.first() || message.author; const userId = targetUser.id; const guildId = message.guild!.id; @@ -35,7 +21,7 @@ export const command: CommandMessage = { const wallet = await getOrCreateWallet(userId, guildId); const { eq, weapon, armor, cape } = await getEquipment(userId, guildId); const stats = await getEffectiveStats(userId, guildId); - const playerState = await prisma.playerState.findUnique({ where: { userId_guildId: { userId, guildId } } }); + const playerStats = await getPlayerStatsFormatted(userId, guildId); // Progreso por áreas const progress = await prisma.playerProgress.findMany({ @@ -45,171 +31,141 @@ export const command: CommandMessage = { take: 5, }); - // Últimas actividades de minijuegos - const recentRuns = await prisma.minigameRun.findMany({ - where: { userId, guildId }, - include: { area: true }, - orderBy: { startedAt: 'desc' }, - take: 5, - }); - - // Conteo de items en inventario + // Inventario const inventoryCount = await prisma.inventoryEntry.count({ where: { userId, guildId, quantity: { gt: 0 } }, }); - // Total de items (cantidad sumada) const inventorySum = await prisma.inventoryEntry.aggregate({ where: { userId, guildId }, _sum: { quantity: true }, }); - // Compras totales - const purchaseCount = await prisma.shopPurchase.count({ - where: { userId, guildId }, - }); - // Cooldowns activos const activeCooldowns = await prisma.actionCooldown.findMany({ where: { userId, guildId, until: { gt: new Date() } }, orderBy: { until: 'asc' }, - take: 5, + take: 3, }); - // Construir el mensaje - const lines: string[] = []; + // Crear DisplayComponent + const display = { + type: 17, + accent_color: 0x5865F2, + components: [ + // Header + { + type: 9, + components: [ + { + type: 10, + content: `👤 **${targetUser.username}**\n${targetUser.bot ? '🤖 Bot' : '👨 Usuario'}` + } + ] + }, + { type: 14, divider: true }, + // Stats Básicos + { + type: 9, + components: [ + { + type: 10, + content: `**📊 ESTADÍSTICAS**\n` + + `❤️ HP: **${stats.hp}/${stats.maxHp}**\n` + + `⚔️ ATK: **${stats.damage}**\n` + + `🛡️ DEF: **${stats.defense}**\n` + + `💰 Monedas: **${wallet.coins.toLocaleString()}**` + } + ] + }, + { type: 14, divider: true }, + // Equipo + { + type: 9, + components: [ + { + type: 10, + content: `**⚔️ EQUIPO**\n` + + (weapon ? `🗡️ Arma: **${weapon.name || weapon.key}**\n` : '🗡️ Arma: *Ninguna*\n') + + (armor ? `🛡️ Armadura: **${armor.name || armor.key}**\n` : '🛡️ Armadura: *Ninguna*\n') + + (cape ? `🧥 Capa: **${cape.name || cape.key}**` : '🧥 Capa: *Ninguna*') + } + ] + }, + { type: 14, divider: true }, + // Inventario + { + type: 9, + components: [ + { + type: 10, + content: `**🎒 INVENTARIO**\n` + + `📦 Items únicos: **${inventoryCount}**\n` + + `🔢 Total items: **${inventorySum._sum.quantity ?? 0}**` + } + ] + } + ] + }; - // Header - lines.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━`); - lines.push(`👤 **Perfil de ${targetUser.username}**`); - lines.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`); - - // 📊 Stats Básicos - lines.push(`📊 **ESTADÍSTICAS**`); - lines.push(`❤️ HP: ${stats.hp}/${stats.maxHp}`); - lines.push(`⚔️ ATK: ${stats.damage}`); - lines.push(`🛡️ DEF: ${stats.defense}`); - lines.push(`💰 Monedas: ${wallet.coins}`); - lines.push(``); - - // 🎒 Inventario - lines.push(`🎒 **INVENTARIO**`); - lines.push(`📦 Items únicos: ${inventoryCount}`); - lines.push(`🔢 Total items: ${inventorySum._sum.quantity ?? 0}`); - lines.push(`🛒 Compras totales: ${purchaseCount}`); - lines.push(``); - - // 🧰 Equipamiento - lines.push(`🧰 **EQUIPAMIENTO**`); - if (weapon) { - const wProps = parseItemProps(weapon.props); - const wTool = fmtTool(wProps); - const wDmg = wProps.damage ? ` +${wProps.damage} ATK` : ''; - lines.push(`🗡️ Arma: ${weapon.name || weapon.key}${wTool ? ` ${wTool}` : ''}${wDmg}`); - } else { - lines.push(`🗡️ Arma: -`); + // Añadir stats de actividades si existen + if (playerStats.activities) { + const activitiesText = Object.entries(playerStats.activities) + .filter(([_, value]) => value > 0) + .map(([key, value]) => `${key}: **${value}**`) + .join('\n'); + + if (activitiesText) { + display.components.push({ type: 14, divider: true }); + display.components.push({ + type: 9, + components: [ + { + type: 10, + content: `**🎮 ACTIVIDADES**\n${activitiesText}` + } + ] + }); + } } - if (armor) { - const aProps = parseItemProps(armor.props); - const aDef = aProps.defense ? ` +${aProps.defense} DEF` : ''; - lines.push(`🛡️ Armadura: ${armor.name || armor.key}${aDef}`); - } else { - lines.push(`🛡️ Armadura: -`); - } - - if (cape) { - const cProps = parseItemProps(cape.props); - const cHp = cProps.maxHpBonus ? ` +${cProps.maxHpBonus} HP` : ''; - lines.push(`🧥 Capa: ${cape.name || cape.key}${cHp}`); - } else { - lines.push(`🧥 Capa: -`); - } - lines.push(``); - - // 🗺️ Progreso por Áreas + // Añadir progreso por áreas if (progress.length > 0) { - lines.push(`🗺️ **PROGRESO EN ÁREAS**`); - for (const p of progress) { - const areaIcon = p.area.type === 'MINE' ? '⛏️' : p.area.type === 'LAGOON' ? '🎣' : p.area.type === 'FIGHT' ? '⚔️' : p.area.type === 'FARM' ? '🌾' : '🗺️'; - lines.push(`${areaIcon} ${p.area.name}: Nivel ${p.highestLevel}`); - } - lines.push(``); + display.components.push({ type: 14, divider: true }); + display.components.push({ + type: 9, + components: [ + { + type: 10, + content: `**🗺️ PROGRESO EN ÁREAS**\n` + + progress.map(p => `• ${p.area.name || p.area.key}: Nivel **${p.highestLevel}**`).join('\n') + } + ] + }); } - // 📜 Últimas Actividades - if (recentRuns.length > 0) { - lines.push(`📜 **ÚLTIMAS ACTIVIDADES**`); - for (const run of recentRuns.slice(0, 3)) { - const result = run.result as any; - const areaIcon = run.area.type === 'MINE' ? '⛏️' : run.area.type === 'LAGOON' ? '🎣' : run.area.type === 'FIGHT' ? '⚔️' : run.area.type === 'FARM' ? '🌾' : '🗺️'; - const timestamp = run.startedAt; - const relativeTime = getRelativeTime(timestamp); - const rewardsCount = result.rewards?.length ?? 0; - lines.push(`${areaIcon} ${run.area.name} (Nv.${run.level}) - ${rewardsCount} recompensas - ${relativeTime}`); - } - lines.push(``); - } - - // ⏱️ Cooldowns Activos + // Añadir cooldowns activos if (activeCooldowns.length > 0) { - lines.push(`⏱️ **COOLDOWNS ACTIVOS**`); - for (const cd of activeCooldowns) { - const remaining = Math.max(0, Math.ceil((cd.until.getTime() - Date.now()) / 1000)); - const cdName = formatCooldownKey(cd.key); - lines.push(`⏳ ${cdName}: ${formatDuration(remaining)}`); - } - lines.push(``); + const now = Date.now(); + const cooldownsText = activeCooldowns.map(cd => { + const remaining = Math.ceil((cd.until.getTime() - now) / 1000); + const mins = Math.floor(remaining / 60); + const secs = remaining % 60; + return `• ${cd.key}: **${mins}m ${secs}s**`; + }).join('\n'); + + display.components.push({ type: 14, divider: true }); + display.components.push({ + type: 9, + components: [ + { + type: 10, + content: `**⏰ COOLDOWNS ACTIVOS**\n${cooldownsText}` + } + ] + }); } - // Stats adicionales del PlayerState - if (playerState?.stats) { - const additionalStats = playerState.stats as any; - if (Object.keys(additionalStats).length > 0) { - lines.push(`🎯 **STATS ADICIONALES**`); - if (additionalStats.attack != null) lines.push(`⚔️ Ataque Base: ${additionalStats.attack}`); - if (additionalStats.defense != null) lines.push(`🛡️ Defensa Base: ${additionalStats.defense}`); - if (additionalStats.strength != null) lines.push(`💪 Fuerza: ${additionalStats.strength}`); - if (additionalStats.luck != null) lines.push(`🍀 Suerte: ${additionalStats.luck}`); - lines.push(``); - } - } - - // Footer - lines.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━`); - lines.push(`Usa \`!inventario\` para ver tus items completos`); - - await message.reply(lines.join('\n')); - }, -}; - -// Helpers -function getRelativeTime(date: Date): string { - const seconds = Math.floor((Date.now() - date.getTime()) / 1000); - if (seconds < 60) return 'Hace un momento'; - if (seconds < 3600) return `Hace ${Math.floor(seconds / 60)}m`; - if (seconds < 86400) return `Hace ${Math.floor(seconds / 3600)}h`; - if (seconds < 604800) return `Hace ${Math.floor(seconds / 86400)}d`; - return date.toLocaleDateString(); -} - -function formatDuration(seconds: number): string { - if (seconds < 60) return `${seconds}s`; - if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`; - const hours = Math.floor(seconds / 3600); - const mins = Math.floor((seconds % 3600) / 60); - return `${hours}h ${mins}m`; -} - -function formatCooldownKey(key: string): string { - // Convertir keys como "minigame:mine.starter" a "Mina" - if (key.startsWith('minigame:')) { - const areaKey = key.split(':')[1]; - if (areaKey?.includes('mine')) return 'Mina'; - if (areaKey?.includes('lagoon')) return 'Laguna'; - if (areaKey?.includes('fight')) return 'Pelea'; - if (areaKey?.includes('farm')) return 'Granja'; - return areaKey || key; + await message.reply({ display } as any); } - return key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); -} +}; diff --git a/src/commands/messages/game/player.ts.backup b/src/commands/messages/game/player.ts.backup new file mode 100644 index 0000000..7283e06 --- /dev/null +++ b/src/commands/messages/game/player.ts.backup @@ -0,0 +1,215 @@ +import type { CommandMessage } from '../../../core/types/commands'; +import type Amayo from '../../../core/client'; +import { prisma } from '../../../core/database/prisma'; +import { getOrCreateWallet } from '../../../game/economy/service'; +import { getEquipment, getEffectiveStats } from '../../../game/combat/equipmentService'; +import type { ItemProps } from '../../../game/economy/types'; + +function parseItemProps(json: unknown): ItemProps { + if (!json || typeof json !== 'object') return {}; + return json as ItemProps; +} + +function fmtTool(props: ItemProps) { + const t = props.tool; + if (!t) return ''; + const icon = t.type === 'pickaxe' ? '⛏️' : t.type === 'rod' ? '🎣' : t.type === 'sword' ? '🗡️' : t.type === 'bow' ? '🏹' : t.type === 'halberd' ? '⚔️' : t.type === 'net' ? '🕸️' : '🔧'; + const tier = t.tier != null ? ` T${t.tier}` : ''; + return `${icon}${tier}`; +} + +export const command: CommandMessage = { + name: 'player', + type: 'message', + aliases: ['perfil', 'profile', 'yo', 'me'], + cooldown: 5, + description: 'Muestra toda tu información de jugador: stats, equipo, progreso y últimas actividades.', + usage: 'player [@usuario]', + run: async (message, args, _client: Amayo) => { + // Permitir ver perfil de otros usuarios mencionándolos + const targetUser = message.mentions.users.first() || message.author; + const userId = targetUser.id; + const guildId = message.guild!.id; + + // Obtener datos del jugador + const wallet = await getOrCreateWallet(userId, guildId); + const { eq, weapon, armor, cape } = await getEquipment(userId, guildId); + const stats = await getEffectiveStats(userId, guildId); + const playerState = await prisma.playerState.findUnique({ where: { userId_guildId: { userId, guildId } } }); + + // Progreso por áreas + const progress = await prisma.playerProgress.findMany({ + where: { userId, guildId }, + include: { area: true }, + orderBy: { updatedAt: 'desc' }, + take: 5, + }); + + // Últimas actividades de minijuegos + const recentRuns = await prisma.minigameRun.findMany({ + where: { userId, guildId }, + include: { area: true }, + orderBy: { startedAt: 'desc' }, + take: 5, + }); + + // Conteo de items en inventario + const inventoryCount = await prisma.inventoryEntry.count({ + where: { userId, guildId, quantity: { gt: 0 } }, + }); + + // Total de items (cantidad sumada) + const inventorySum = await prisma.inventoryEntry.aggregate({ + where: { userId, guildId }, + _sum: { quantity: true }, + }); + + // Compras totales + const purchaseCount = await prisma.shopPurchase.count({ + where: { userId, guildId }, + }); + + // Cooldowns activos + const activeCooldowns = await prisma.actionCooldown.findMany({ + where: { userId, guildId, until: { gt: new Date() } }, + orderBy: { until: 'asc' }, + take: 5, + }); + + // Construir el mensaje + const lines: string[] = []; + + // Header + lines.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━`); + lines.push(`👤 **Perfil de ${targetUser.username}**`); + lines.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`); + + // 📊 Stats Básicos + lines.push(`📊 **ESTADÍSTICAS**`); + lines.push(`❤️ HP: ${stats.hp}/${stats.maxHp}`); + lines.push(`⚔️ ATK: ${stats.damage}`); + lines.push(`🛡️ DEF: ${stats.defense}`); + lines.push(`💰 Monedas: ${wallet.coins}`); + lines.push(``); + + // 🎒 Inventario + lines.push(`🎒 **INVENTARIO**`); + lines.push(`📦 Items únicos: ${inventoryCount}`); + lines.push(`🔢 Total items: ${inventorySum._sum.quantity ?? 0}`); + lines.push(`🛒 Compras totales: ${purchaseCount}`); + lines.push(``); + + // 🧰 Equipamiento + lines.push(`🧰 **EQUIPAMIENTO**`); + if (weapon) { + const wProps = parseItemProps(weapon.props); + const wTool = fmtTool(wProps); + const wDmg = wProps.damage ? ` +${wProps.damage} ATK` : ''; + lines.push(`🗡️ Arma: ${weapon.name || weapon.key}${wTool ? ` ${wTool}` : ''}${wDmg}`); + } else { + lines.push(`🗡️ Arma: -`); + } + + if (armor) { + const aProps = parseItemProps(armor.props); + const aDef = aProps.defense ? ` +${aProps.defense} DEF` : ''; + lines.push(`🛡️ Armadura: ${armor.name || armor.key}${aDef}`); + } else { + lines.push(`🛡️ Armadura: -`); + } + + if (cape) { + const cProps = parseItemProps(cape.props); + const cHp = cProps.maxHpBonus ? ` +${cProps.maxHpBonus} HP` : ''; + lines.push(`🧥 Capa: ${cape.name || cape.key}${cHp}`); + } else { + lines.push(`🧥 Capa: -`); + } + lines.push(``); + + // 🗺️ Progreso por Áreas + if (progress.length > 0) { + lines.push(`🗺️ **PROGRESO EN ÁREAS**`); + for (const p of progress) { + const areaIcon = p.area.type === 'MINE' ? '⛏️' : p.area.type === 'LAGOON' ? '🎣' : p.area.type === 'FIGHT' ? '⚔️' : p.area.type === 'FARM' ? '🌾' : '🗺️'; + lines.push(`${areaIcon} ${p.area.name}: Nivel ${p.highestLevel}`); + } + lines.push(``); + } + + // 📜 Últimas Actividades + if (recentRuns.length > 0) { + lines.push(`📜 **ÚLTIMAS ACTIVIDADES**`); + for (const run of recentRuns.slice(0, 3)) { + const result = run.result as any; + const areaIcon = run.area.type === 'MINE' ? '⛏️' : run.area.type === 'LAGOON' ? '🎣' : run.area.type === 'FIGHT' ? '⚔️' : run.area.type === 'FARM' ? '🌾' : '🗺️'; + const timestamp = run.startedAt; + const relativeTime = getRelativeTime(timestamp); + const rewardsCount = result.rewards?.length ?? 0; + lines.push(`${areaIcon} ${run.area.name} (Nv.${run.level}) - ${rewardsCount} recompensas - ${relativeTime}`); + } + lines.push(``); + } + + // ⏱️ Cooldowns Activos + if (activeCooldowns.length > 0) { + lines.push(`⏱️ **COOLDOWNS ACTIVOS**`); + for (const cd of activeCooldowns) { + const remaining = Math.max(0, Math.ceil((cd.until.getTime() - Date.now()) / 1000)); + const cdName = formatCooldownKey(cd.key); + lines.push(`⏳ ${cdName}: ${formatDuration(remaining)}`); + } + lines.push(``); + } + + // Stats adicionales del PlayerState + if (playerState?.stats) { + const additionalStats = playerState.stats as any; + if (Object.keys(additionalStats).length > 0) { + lines.push(`🎯 **STATS ADICIONALES**`); + if (additionalStats.attack != null) lines.push(`⚔️ Ataque Base: ${additionalStats.attack}`); + if (additionalStats.defense != null) lines.push(`🛡️ Defensa Base: ${additionalStats.defense}`); + if (additionalStats.strength != null) lines.push(`💪 Fuerza: ${additionalStats.strength}`); + if (additionalStats.luck != null) lines.push(`🍀 Suerte: ${additionalStats.luck}`); + lines.push(``); + } + } + + // Footer + lines.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━`); + lines.push(`Usa \`!inventario\` para ver tus items completos`); + + await message.reply(lines.join('\n')); + }, +}; + +// Helpers +function getRelativeTime(date: Date): string { + const seconds = Math.floor((Date.now() - date.getTime()) / 1000); + if (seconds < 60) return 'Hace un momento'; + if (seconds < 3600) return `Hace ${Math.floor(seconds / 60)}m`; + if (seconds < 86400) return `Hace ${Math.floor(seconds / 3600)}h`; + if (seconds < 604800) return `Hace ${Math.floor(seconds / 86400)}d`; + return date.toLocaleDateString(); +} + +function formatDuration(seconds: number): string { + if (seconds < 60) return `${seconds}s`; + if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`; + const hours = Math.floor(seconds / 3600); + const mins = Math.floor((seconds % 3600) / 60); + return `${hours}h ${mins}m`; +} + +function formatCooldownKey(key: string): string { + // Convertir keys como "minigame:mine.starter" a "Mina" + if (key.startsWith('minigame:')) { + const areaKey = key.split(':')[1]; + if (areaKey?.includes('mine')) return 'Mina'; + if (areaKey?.includes('lagoon')) return 'Laguna'; + if (areaKey?.includes('fight')) return 'Pelea'; + if (areaKey?.includes('farm')) return 'Granja'; + return areaKey || key; + } + return key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); +} diff --git a/src/game/quests/service.ts b/src/game/quests/service.ts index cdb6f44..1d06315 100644 --- a/src/game/quests/service.ts +++ b/src/game/quests/service.ts @@ -205,8 +205,9 @@ export async function generateDailyQuests(guildId: string) { } }); - // Templates de misiones diarias + // Templates de misiones diarias expandidas const dailyTemplates = [ + // Minería { key: 'daily_mine', name: 'Minero Diario', @@ -215,6 +216,15 @@ export async function generateDailyQuests(guildId: string) { requirements: { type: 'mine_count', count: 10 }, rewards: { coins: 500 } }, + { + key: 'daily_mine_hard', + name: 'Minero Dedicado', + description: 'Mina 20 veces', + category: 'mining', + requirements: { type: 'mine_count', count: 20 }, + rewards: { coins: 1200 } + }, + // Pesca { key: 'daily_fish', name: 'Pescador Diario', @@ -223,6 +233,15 @@ export async function generateDailyQuests(guildId: string) { requirements: { type: 'fish_count', count: 8 }, rewards: { coins: 400 } }, + { + key: 'daily_fish_hard', + name: 'Pescador Experto', + description: 'Pesca 15 veces', + category: 'fishing', + requirements: { type: 'fish_count', count: 15 }, + rewards: { coins: 900 } + }, + // Combate { key: 'daily_fight', name: 'Guerrero Diario', @@ -231,6 +250,15 @@ export async function generateDailyQuests(guildId: string) { requirements: { type: 'fight_count', count: 5 }, rewards: { coins: 600 } }, + { + key: 'daily_mob_slayer', + name: 'Cazador de Monstruos', + description: 'Derrota 10 mobs', + category: 'combat', + requirements: { type: 'mob_defeat_count', count: 10 }, + rewards: { coins: 800 } + }, + // Crafteo { key: 'daily_craft', name: 'Artesano Diario', @@ -238,6 +266,73 @@ export async function generateDailyQuests(guildId: string) { category: 'crafting', requirements: { type: 'craft_count', count: 3 }, rewards: { coins: 300 } + }, + { + key: 'daily_craft_hard', + name: 'Maestro Artesano', + description: 'Craftea 10 items', + category: 'crafting', + requirements: { type: 'craft_count', count: 10 }, + rewards: { coins: 1000 } + }, + // Economía + { + key: 'daily_coins', + name: 'Acumulador', + description: 'Gana 5000 monedas', + category: 'economy', + requirements: { type: 'coins_earned', count: 5000 }, + rewards: { coins: 1000 } + }, + { + key: 'daily_purchase', + name: 'Comprador', + description: 'Compra 3 items en la tienda', + category: 'economy', + requirements: { type: 'items_purchased', count: 3 }, + rewards: { coins: 500 } + }, + // Items + { + key: 'daily_consume', + name: 'Consumidor', + description: 'Consume 5 items', + category: 'items', + requirements: { type: 'items_consumed', count: 5 }, + rewards: { coins: 300 } + }, + { + key: 'daily_equip', + name: 'Equipador', + description: 'Equipa 3 items diferentes', + category: 'items', + requirements: { type: 'items_equipped', count: 3 }, + rewards: { coins: 400 } + }, + // Fundición + { + key: 'daily_smelt', + name: 'Fundidor', + description: 'Funde 5 items', + category: 'smelting', + requirements: { type: 'items_smelted', count: 5 }, + rewards: { coins: 700 } + }, + // Combinadas + { + key: 'daily_variety', + name: 'Multitarea', + description: 'Mina, pesca y pelea 3 veces cada uno', + category: 'variety', + requirements: { + type: 'variety', + conditions: [ + { type: 'mine_count', count: 3 }, + { type: 'fish_count', count: 3 }, + { type: 'fight_count', count: 3 } + ] + }, + rewards: { coins: 1500 } } ];