feat(economy): enhance player command with improved display components and stats overview
This commit is contained in:
61
src/commands/messages/admin/logroEliminar.ts
Normal file
61
src/commands/messages/admin/logroEliminar.ts
Normal file
@@ -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 <key>',
|
||||||
|
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 <key>`\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).` : ''}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
122
src/commands/messages/admin/logroVer.ts
Normal file
122
src/commands/messages/admin/logroVer.ts
Normal file
@@ -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 <key>',
|
||||||
|
run: async (message, args, client: Amayo) => {
|
||||||
|
const guildId = message.guild!.id;
|
||||||
|
const key = args[0]?.trim();
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
await message.reply('Uso: `!logro-ver <key>`\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);
|
||||||
|
}
|
||||||
|
};
|
||||||
135
src/commands/messages/admin/logrosLista.ts
Normal file
135
src/commands/messages/admin/logrosLista.ts
Normal file
@@ -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 <key>` para ver detalles de un logro específico.',
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
58
src/commands/messages/admin/misionEliminar.ts
Normal file
58
src/commands/messages/admin/misionEliminar.ts
Normal file
@@ -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 <key>',
|
||||||
|
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 <key>`\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).` : ''}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
155
src/commands/messages/admin/misionVer.ts
Normal file
155
src/commands/messages/admin/misionVer.ts
Normal file
@@ -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 <key>',
|
||||||
|
run: async (message, args, client: Amayo) => {
|
||||||
|
const guildId = message.guild!.id;
|
||||||
|
const key = args[0]?.trim();
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
await message.reply('Uso: `!mision-ver <key>`\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<string, string> = {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
140
src/commands/messages/admin/misionesLista.ts
Normal file
140
src/commands/messages/admin/misionesLista.ts
Normal file
@@ -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<string, string> = {
|
||||||
|
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 <key>` para ver detalles de una misión específica.',
|
||||||
|
flags: 64
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -3,30 +3,16 @@ import type Amayo from '../../../core/client';
|
|||||||
import { prisma } from '../../../core/database/prisma';
|
import { prisma } from '../../../core/database/prisma';
|
||||||
import { getOrCreateWallet } from '../../../game/economy/service';
|
import { getOrCreateWallet } from '../../../game/economy/service';
|
||||||
import { getEquipment, getEffectiveStats } from '../../../game/combat/equipmentService';
|
import { getEquipment, getEffectiveStats } from '../../../game/combat/equipmentService';
|
||||||
import type { ItemProps } from '../../../game/economy/types';
|
import { getPlayerStatsFormatted } from '../../../game/stats/service';
|
||||||
|
|
||||||
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 = {
|
export const command: CommandMessage = {
|
||||||
name: 'player',
|
name: 'player',
|
||||||
type: 'message',
|
type: 'message',
|
||||||
aliases: ['perfil', 'profile', 'yo', 'me'],
|
aliases: ['perfil', 'profile', 'yo', 'me'],
|
||||||
cooldown: 5,
|
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]',
|
usage: 'player [@usuario]',
|
||||||
run: async (message, args, _client: Amayo) => {
|
run: async (message, args, _client: Amayo) => {
|
||||||
// Permitir ver perfil de otros usuarios mencionándolos
|
|
||||||
const targetUser = message.mentions.users.first() || message.author;
|
const targetUser = message.mentions.users.first() || message.author;
|
||||||
const userId = targetUser.id;
|
const userId = targetUser.id;
|
||||||
const guildId = message.guild!.id;
|
const guildId = message.guild!.id;
|
||||||
@@ -35,7 +21,7 @@ export const command: CommandMessage = {
|
|||||||
const wallet = await getOrCreateWallet(userId, guildId);
|
const wallet = await getOrCreateWallet(userId, guildId);
|
||||||
const { eq, weapon, armor, cape } = await getEquipment(userId, guildId);
|
const { eq, weapon, armor, cape } = await getEquipment(userId, guildId);
|
||||||
const stats = await getEffectiveStats(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
|
// Progreso por áreas
|
||||||
const progress = await prisma.playerProgress.findMany({
|
const progress = await prisma.playerProgress.findMany({
|
||||||
@@ -45,171 +31,141 @@ export const command: CommandMessage = {
|
|||||||
take: 5,
|
take: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Últimas actividades de minijuegos
|
// Inventario
|
||||||
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({
|
const inventoryCount = await prisma.inventoryEntry.count({
|
||||||
where: { userId, guildId, quantity: { gt: 0 } },
|
where: { userId, guildId, quantity: { gt: 0 } },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Total de items (cantidad sumada)
|
|
||||||
const inventorySum = await prisma.inventoryEntry.aggregate({
|
const inventorySum = await prisma.inventoryEntry.aggregate({
|
||||||
where: { userId, guildId },
|
where: { userId, guildId },
|
||||||
_sum: { quantity: true },
|
_sum: { quantity: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Compras totales
|
|
||||||
const purchaseCount = await prisma.shopPurchase.count({
|
|
||||||
where: { userId, guildId },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cooldowns activos
|
// Cooldowns activos
|
||||||
const activeCooldowns = await prisma.actionCooldown.findMany({
|
const activeCooldowns = await prisma.actionCooldown.findMany({
|
||||||
where: { userId, guildId, until: { gt: new Date() } },
|
where: { userId, guildId, until: { gt: new Date() } },
|
||||||
orderBy: { until: 'asc' },
|
orderBy: { until: 'asc' },
|
||||||
take: 5,
|
take: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Construir el mensaje
|
// Crear DisplayComponent
|
||||||
const lines: string[] = [];
|
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
|
// Añadir stats de actividades si existen
|
||||||
lines.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
if (playerStats.activities) {
|
||||||
lines.push(`👤 **Perfil de ${targetUser.username}**`);
|
const activitiesText = Object.entries(playerStats.activities)
|
||||||
lines.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
.filter(([_, value]) => value > 0)
|
||||||
|
.map(([key, value]) => `${key}: **${value}**`)
|
||||||
// 📊 Stats Básicos
|
.join('\n');
|
||||||
lines.push(`📊 **ESTADÍSTICAS**`);
|
|
||||||
lines.push(`❤️ HP: ${stats.hp}/${stats.maxHp}`);
|
if (activitiesText) {
|
||||||
lines.push(`⚔️ ATK: ${stats.damage}`);
|
display.components.push({ type: 14, divider: true });
|
||||||
lines.push(`🛡️ DEF: ${stats.defense}`);
|
display.components.push({
|
||||||
lines.push(`💰 Monedas: ${wallet.coins}`);
|
type: 9,
|
||||||
lines.push(``);
|
components: [
|
||||||
|
{
|
||||||
// 🎒 Inventario
|
type: 10,
|
||||||
lines.push(`🎒 **INVENTARIO**`);
|
content: `**🎮 ACTIVIDADES**\n${activitiesText}`
|
||||||
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) {
|
// Añadir progreso por áreas
|
||||||
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) {
|
if (progress.length > 0) {
|
||||||
lines.push(`🗺️ **PROGRESO EN ÁREAS**`);
|
display.components.push({ type: 14, divider: true });
|
||||||
for (const p of progress) {
|
display.components.push({
|
||||||
const areaIcon = p.area.type === 'MINE' ? '⛏️' : p.area.type === 'LAGOON' ? '🎣' : p.area.type === 'FIGHT' ? '⚔️' : p.area.type === 'FARM' ? '🌾' : '🗺️';
|
type: 9,
|
||||||
lines.push(`${areaIcon} ${p.area.name}: Nivel ${p.highestLevel}`);
|
components: [
|
||||||
}
|
{
|
||||||
lines.push(``);
|
type: 10,
|
||||||
|
content: `**🗺️ PROGRESO EN ÁREAS**\n` +
|
||||||
|
progress.map(p => `• ${p.area.name || p.area.key}: Nivel **${p.highestLevel}**`).join('\n')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 📜 Últimas Actividades
|
// Añadir cooldowns activos
|
||||||
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) {
|
if (activeCooldowns.length > 0) {
|
||||||
lines.push(`⏱️ **COOLDOWNS ACTIVOS**`);
|
const now = Date.now();
|
||||||
for (const cd of activeCooldowns) {
|
const cooldownsText = activeCooldowns.map(cd => {
|
||||||
const remaining = Math.max(0, Math.ceil((cd.until.getTime() - Date.now()) / 1000));
|
const remaining = Math.ceil((cd.until.getTime() - now) / 1000);
|
||||||
const cdName = formatCooldownKey(cd.key);
|
const mins = Math.floor(remaining / 60);
|
||||||
lines.push(`⏳ ${cdName}: ${formatDuration(remaining)}`);
|
const secs = remaining % 60;
|
||||||
}
|
return `• ${cd.key}: **${mins}m ${secs}s**`;
|
||||||
lines.push(``);
|
}).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
|
await message.reply({ display } as any);
|
||||||
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());
|
};
|
||||||
}
|
|
||||||
|
|||||||
215
src/commands/messages/game/player.ts.backup
Normal file
215
src/commands/messages/game/player.ts.backup
Normal file
@@ -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());
|
||||||
|
}
|
||||||
@@ -205,8 +205,9 @@ export async function generateDailyQuests(guildId: string) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Templates de misiones diarias
|
// Templates de misiones diarias expandidas
|
||||||
const dailyTemplates = [
|
const dailyTemplates = [
|
||||||
|
// Minería
|
||||||
{
|
{
|
||||||
key: 'daily_mine',
|
key: 'daily_mine',
|
||||||
name: 'Minero Diario',
|
name: 'Minero Diario',
|
||||||
@@ -215,6 +216,15 @@ export async function generateDailyQuests(guildId: string) {
|
|||||||
requirements: { type: 'mine_count', count: 10 },
|
requirements: { type: 'mine_count', count: 10 },
|
||||||
rewards: { coins: 500 }
|
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',
|
key: 'daily_fish',
|
||||||
name: 'Pescador Diario',
|
name: 'Pescador Diario',
|
||||||
@@ -223,6 +233,15 @@ export async function generateDailyQuests(guildId: string) {
|
|||||||
requirements: { type: 'fish_count', count: 8 },
|
requirements: { type: 'fish_count', count: 8 },
|
||||||
rewards: { coins: 400 }
|
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',
|
key: 'daily_fight',
|
||||||
name: 'Guerrero Diario',
|
name: 'Guerrero Diario',
|
||||||
@@ -231,6 +250,15 @@ export async function generateDailyQuests(guildId: string) {
|
|||||||
requirements: { type: 'fight_count', count: 5 },
|
requirements: { type: 'fight_count', count: 5 },
|
||||||
rewards: { coins: 600 }
|
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',
|
key: 'daily_craft',
|
||||||
name: 'Artesano Diario',
|
name: 'Artesano Diario',
|
||||||
@@ -238,6 +266,73 @@ export async function generateDailyQuests(guildId: string) {
|
|||||||
category: 'crafting',
|
category: 'crafting',
|
||||||
requirements: { type: 'craft_count', count: 3 },
|
requirements: { type: 'craft_count', count: 3 },
|
||||||
rewards: { coins: 300 }
|
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 }
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user