From ed80eb2584a9584aaa5b1ea3ba8e93a6891dc6bf Mon Sep 17 00:00:00 2001 From: shni Date: Sun, 5 Oct 2025 06:09:53 -0500 Subject: [PATCH] feat(economy): add commands for managing areas and items with interactive displays --- src/commands/messages/admin/areaEliminar.ts | 56 +++++ src/commands/messages/admin/areasLista.ts | 113 ++++++++++ src/commands/messages/admin/itemEliminar.ts | 64 ++++++ src/commands/messages/admin/itemVer.ts | 104 +++++++++ src/commands/messages/admin/itemsLista.ts | 128 +++++++++++ src/commands/messages/admin/logroVer.ts | 3 +- src/commands/messages/admin/misionVer.ts | 3 +- src/commands/messages/admin/mobEliminar.ts | 43 ++++ src/commands/messages/admin/mobsLista.ts | 117 ++++++++++ src/commands/messages/game/itemEdit.ts | 238 +++++++++++++++----- src/commands/messages/game/player.ts | 7 +- 11 files changed, 820 insertions(+), 56 deletions(-) create mode 100644 src/commands/messages/admin/areaEliminar.ts create mode 100644 src/commands/messages/admin/areasLista.ts create mode 100644 src/commands/messages/admin/itemEliminar.ts create mode 100644 src/commands/messages/admin/itemVer.ts create mode 100644 src/commands/messages/admin/itemsLista.ts create mode 100644 src/commands/messages/admin/mobEliminar.ts create mode 100644 src/commands/messages/admin/mobsLista.ts diff --git a/src/commands/messages/admin/areaEliminar.ts b/src/commands/messages/admin/areaEliminar.ts new file mode 100644 index 0000000..9eb283a --- /dev/null +++ b/src/commands/messages/admin/areaEliminar.ts @@ -0,0 +1,56 @@ +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: 'area-eliminar', + type: 'message', + aliases: ['eliminar-area', 'area-delete'], + cooldown: 5, + description: 'Eliminar un área del servidor', + usage: 'area-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: \`!area-eliminar \`\nEjemplo: \`!area-eliminar mine.cavern\`'); + return; + } + + const area = await prisma.gameArea.findFirst({ + where: { key, guildId } + }); + + if (!area) { + await message.reply(`❌ No se encontró el área local con key ${key} en este servidor.`); + return; + } + + const levelsCount = await prisma.gameAreaLevel.count({ + where: { areaId: area.id } + }); + + if (levelsCount > 0) { + await prisma.gameAreaLevel.deleteMany({ + where: { areaId: area.id } + }); + } + + await prisma.gameArea.delete({ + where: { id: area.id } + }); + + await message.reply( + `✅ Área ${key} eliminada exitosamente.\n` + + `${levelsCount > 0 ? `⚠️ Se eliminaron ${levelsCount} nivel(es) asociado(s).` : ''}` + ); + } +}; diff --git a/src/commands/messages/admin/areasLista.ts b/src/commands/messages/admin/areasLista.ts new file mode 100644 index 0000000..024bca4 --- /dev/null +++ b/src/commands/messages/admin/areasLista.ts @@ -0,0 +1,113 @@ +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: 'areas-lista', + type: 'message', + aliases: ['lista-areas', 'areas-list'], + cooldown: 5, + description: 'Ver lista de todas las áreas del servidor', + usage: 'areas-lista [pagina]', + run: async (message, args, client: Amayo) => { + const guildId = message.guild!.id; + const page = parseInt(args[0]) || 1; + const perPage = 6; + + const total = await prisma.gameArea.count({ + where: { OR: [{ guildId }, { guildId: null }] } + }); + + const areas = await prisma.gameArea.findMany({ + where: { OR: [{ guildId }, { guildId: null }] }, + orderBy: [{ key: 'asc' }], + skip: (page - 1) * perPage, + take: perPage + }); + + if (areas.length === 0) { + await message.reply('No hay áreas configuradas en este servidor.'); + return; + } + + const totalPages = Math.ceil(total / perPage); + + const display = { + type: 17, + accent_color: 0x00FF00, + components: [ + { + type: 9, + components: [{ + type: 10, + content: `**🗺️ Lista de Áreas**\nPágina ${page}/${totalPages} • Total: ${total}` + }] + }, + { type: 14, divider: true }, + ...areas.map(area => ({ + type: 9, + components: [{ + type: 10, + content: `**${area.name || area.key}**\n` + + `└ Key: \`${area.key}\`\n` + + `└ ${area.guildId === guildId ? '📍 Local' : '🌐 Global'}` + }] + })) + ] + }; + + const buttons: any[] = []; + + if (page > 1) { + buttons.push({ + type: ComponentType.Button, + style: ButtonStyle.Secondary, + label: '◀ Anterior', + custom_id: `areas_prev_${page}` + }); + } + + if (page < totalPages) { + buttons.push({ + type: ComponentType.Button, + style: ButtonStyle.Secondary, + label: 'Siguiente ▶', + custom_id: `areas_next_${page}` + }); + } + + 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('areas_prev_')) { + const currentPage = parseInt(i.customId.split('_')[2]); + await i.deferUpdate(); + args[0] = String(currentPage - 1); + await command.run!(message, args, client); + collector.stop(); + } else if (i.customId.startsWith('areas_next_')) { + const currentPage = parseInt(i.customId.split('_')[2]); + await i.deferUpdate(); + args[0] = String(currentPage + 1); + await command.run!(message, args, client); + collector.stop(); + } + }); + } +}; diff --git a/src/commands/messages/admin/itemEliminar.ts b/src/commands/messages/admin/itemEliminar.ts new file mode 100644 index 0000000..925364c --- /dev/null +++ b/src/commands/messages/admin/itemEliminar.ts @@ -0,0 +1,64 @@ +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: 'item-eliminar', + type: 'message', + aliases: ['eliminar-item', 'item-delete'], + cooldown: 5, + description: 'Eliminar un item del servidor', + usage: 'item-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: `!item-eliminar `\nEjemplo: `!item-eliminar tool.pickaxe.iron`'); + return; + } + + const item = await prisma.economyItem.findFirst({ + where: { key, guildId } + }); + + if (!item) { + await message.reply(`❌ No se encontró el item local con key \`${key}\` en este servidor.\n` + + `💡 Solo puedes eliminar items locales del servidor, no globales.`); + return; + } + + // Verificar si está en uso + const inInventory = await prisma.inventoryEntry.count({ + where: { itemId: item.id, quantity: { gt: 0 } } + }); + + const inOffers = await prisma.shopOffer.count({ + where: { itemId: item.id } + }); + + if (inInventory > 0 || inOffers > 0) { + await message.reply( + `⚠️ **Advertencia:** Este item está en uso:\n` + + `${inInventory > 0 ? `• En ${inInventory} inventario(s)\n` : ''}` + + `${inOffers > 0 ? `• En ${inOffers} oferta(s) de tienda\n` : ''}` + + `¿Estás seguro? Usa \`!item-eliminar-forzar ${key}\` para confirmar.` + ); + return; + } + + // Eliminar el item + await prisma.economyItem.delete({ + where: { id: item.id } + }); + + await message.reply(`✅ Item \`${key}\` eliminado exitosamente.`); + } +}; diff --git a/src/commands/messages/admin/itemVer.ts b/src/commands/messages/admin/itemVer.ts new file mode 100644 index 0000000..421dd31 --- /dev/null +++ b/src/commands/messages/admin/itemVer.ts @@ -0,0 +1,104 @@ +import type { CommandMessage } from '../../../core/types/commands'; +import type Amayo from '../../../core/client'; +import { prisma } from '../../../core/database/prisma'; +import type { TextBasedChannel } from 'discord.js'; + +export const command: CommandMessage = { + name: 'item-ver', + type: 'message', + aliases: ['ver-item', 'item-view'], + cooldown: 3, + description: 'Ver detalles de un item específico', + usage: 'item-ver ', + run: async (message, args, client: Amayo) => { + const guildId = message.guild!.id; + const key = args[0]?.trim(); + + if (!key) { + await message.reply('Uso: `!item-ver `\nEjemplo: `!item-ver tool.pickaxe.iron`'); + return; + } + + const item = await prisma.economyItem.findFirst({ + where: { + key, + OR: [{ guildId }, { guildId: null }] + } + }); + + if (!item) { + await message.reply(`❌ No se encontró el item con key \`${key}\``); + return; + } + + const props = item.props as any || {}; + const tags = item.tags || []; + + const display = { + type: 17, + accent_color: 0x00D9FF, + components: [ + { + type: 9, + components: [{ + type: 10, + content: `**🛠️ ${item.name || item.key}**` + }] + }, + { type: 14, divider: true }, + { + type: 9, + components: [{ + type: 10, + content: `**Key:** \`${item.key}\`\n` + + `**Nombre:** ${item.name || '*Sin nombre*'}\n` + + `**Descripción:** ${item.description || '*Sin descripción*'}\n` + + `**Categoría:** ${item.category || '*Sin categoría*'}\n` + + `**Stackable:** ${item.stackable ? 'Sí' : 'No'}\n` + + `**Máx. Inventario:** ${item.maxPerInventory || 'Ilimitado'}\n` + + `**Ámbito:** ${item.guildId ? '📍 Local del servidor' : '🌐 Global'}` + }] + } + ] + }; + + if (tags.length > 0) { + display.components.push({ type: 14, divider: true }); + display.components.push({ + type: 9, + components: [{ + type: 10, + content: `**Tags:** ${tags.join(', ')}` + }] + }); + } + + if (item.icon) { + display.components.push({ type: 14, divider: true }); + display.components.push({ + type: 9, + components: [{ + type: 10, + content: `**Icon URL:** ${item.icon}` + }] + }); + } + + if (Object.keys(props).length > 0) { + display.components.push({ type: 14, divider: true }); + display.components.push({ + type: 9, + components: [{ + type: 10, + content: `**Props (JSON):**\n\`\`\`json\n${JSON.stringify(props, null, 2)}\n\`\`\`` + }] + }); + } + + const channel = message.channel as TextBasedChannel & { send: Function }; + await (channel.send as any)({ + display, + reply: { messageReference: message.id } + }); + } +}; diff --git a/src/commands/messages/admin/itemsLista.ts b/src/commands/messages/admin/itemsLista.ts new file mode 100644 index 0000000..f290755 --- /dev/null +++ b/src/commands/messages/admin/itemsLista.ts @@ -0,0 +1,128 @@ +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: 'items-lista', + type: 'message', + aliases: ['lista-items', 'items-list'], + cooldown: 5, + description: 'Ver lista de todos los items del servidor', + usage: 'items-lista [pagina]', + run: async (message, args, client: Amayo) => { + const guildId = message.guild!.id; + const page = parseInt(args[0]) || 1; + const perPage = 8; + + const total = await prisma.economyItem.count({ + where: { OR: [{ guildId }, { guildId: null }] } + }); + + const items = await prisma.economyItem.findMany({ + where: { OR: [{ guildId }, { guildId: null }] }, + orderBy: [{ category: 'asc' }, { name: 'asc' }], + skip: (page - 1) * perPage, + take: perPage + }); + + if (items.length === 0) { + await message.reply('No hay items configurados en este servidor.'); + return; + } + + const totalPages = Math.ceil(total / perPage); + + const display = { + type: 17, + accent_color: 0x00D9FF, + components: [ + { + type: 9, + components: [{ + type: 10, + content: `**🛠️ Lista de Items**\nPágina ${page}/${totalPages} • Total: ${total}` + }] + }, + { type: 14, divider: true }, + ...items.map(item => ({ + type: 9, + components: [{ + type: 10, + content: `**${item.name || item.key}**\n` + + `└ Key: \`${item.key}\`\n` + + `└ Categoría: ${item.category || '*Sin categoría*'}\n` + + `└ ${item.stackable ? '📚 Apilable' : '🔒 No apilable'}` + + (item.maxPerInventory ? ` (Máx: ${item.maxPerInventory})` : '') + + (item.guildId === guildId ? ' • 📍 Local' : ' • 🌐 Global') + }] + })) + ] + }; + + const buttons: any[] = []; + + if (page > 1) { + buttons.push({ + type: ComponentType.Button, + style: ButtonStyle.Secondary, + label: '◀ Anterior', + custom_id: `items_prev_${page}` + }); + } + + if (page < totalPages) { + buttons.push({ + type: ComponentType.Button, + style: ButtonStyle.Secondary, + label: 'Siguiente ▶', + custom_id: `items_next_${page}` + }); + } + + buttons.push({ + type: ComponentType.Button, + style: ButtonStyle.Primary, + label: 'Ver Detalle', + custom_id: 'items_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('items_prev_')) { + const currentPage = parseInt(i.customId.split('_')[2]); + await i.deferUpdate(); + args[0] = String(currentPage - 1); + await command.run!(message, args, client); + collector.stop(); + } else if (i.customId.startsWith('items_next_')) { + const currentPage = parseInt(i.customId.split('_')[2]); + await i.deferUpdate(); + args[0] = String(currentPage + 1); + await command.run!(message, args, client); + collector.stop(); + } else if (i.customId === 'items_detail') { + await i.reply({ + content: '💡 Usa `!item-ver ` para ver detalles de un item específico.', + flags: 64 + }); + } + }); + } +}; diff --git a/src/commands/messages/admin/logroVer.ts b/src/commands/messages/admin/logroVer.ts index 92d45e9..85f9d2f 100644 --- a/src/commands/messages/admin/logroVer.ts +++ b/src/commands/messages/admin/logroVer.ts @@ -1,6 +1,7 @@ import type { CommandMessage } from '../../../core/types/commands'; import type Amayo from '../../../core/client'; import { prisma } from '../../../core/database/prisma'; +import type { TextBasedChannel } from 'discord.js'; import { ComponentType, ButtonStyle } from 'discord-api-types/v10'; export const command: CommandMessage = { @@ -117,6 +118,6 @@ export const command: CommandMessage = { }); } - await message.reply({ display } as any); + const channel = message.channel as TextBasedChannel & { send: Function }; await (channel.send as any)({ display, reply: { messageReference: message.id } }); } }; diff --git a/src/commands/messages/admin/misionVer.ts b/src/commands/messages/admin/misionVer.ts index 832c646..809a78b 100644 --- a/src/commands/messages/admin/misionVer.ts +++ b/src/commands/messages/admin/misionVer.ts @@ -1,6 +1,7 @@ import type { CommandMessage } from '../../../core/types/commands'; import type Amayo from '../../../core/client'; import { prisma } from '../../../core/database/prisma'; +import type { TextBasedChannel } from 'discord.js'; export const command: CommandMessage = { name: 'mision-ver', @@ -150,6 +151,6 @@ export const command: CommandMessage = { }); } - await message.reply({ display } as any); + const channel = message.channel as TextBasedChannel & { send: Function }; await (channel.send as any)({ display, reply: { messageReference: message.id } }); } }; diff --git a/src/commands/messages/admin/mobEliminar.ts b/src/commands/messages/admin/mobEliminar.ts new file mode 100644 index 0000000..3bfb3c7 --- /dev/null +++ b/src/commands/messages/admin/mobEliminar.ts @@ -0,0 +1,43 @@ +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: 'mob-eliminar', + type: 'message', + aliases: ['eliminar-mob', 'mob-delete'], + cooldown: 5, + description: 'Eliminar un mob del servidor', + usage: 'mob-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: \`!mob-eliminar \`\nEjemplo: \`!mob-eliminar mob.goblin\`'); + return; + } + + const mob = await prisma.mob.findFirst({ + where: { key, guildId } + }); + + if (!mob) { + await message.reply(`❌ No se encontró el mob local con key ${key} en este servidor.`); + return; + } + + await prisma.mob.delete({ + where: { id: mob.id } + }); + + await message.reply(`✅ Mob ${key} eliminado exitosamente.`); + } +}; diff --git a/src/commands/messages/admin/mobsLista.ts b/src/commands/messages/admin/mobsLista.ts new file mode 100644 index 0000000..d0f94bf --- /dev/null +++ b/src/commands/messages/admin/mobsLista.ts @@ -0,0 +1,117 @@ +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: 'mobs-lista', + type: 'message', + aliases: ['lista-mobs', 'mobs-list'], + cooldown: 5, + description: 'Ver lista de todos los mobs del servidor', + usage: 'mobs-lista [pagina]', + run: async (message, args, client: Amayo) => { + const guildId = message.guild!.id; + const page = parseInt(args[0]) || 1; + const perPage = 6; + + const total = await prisma.mob.count({ + where: { OR: [{ guildId }, { guildId: null }] } + }); + + const mobs = await prisma.mob.findMany({ + where: { OR: [{ guildId }, { guildId: null }] }, + orderBy: [{ key: 'asc' }], + skip: (page - 1) * perPage, + take: perPage + }); + + if (mobs.length === 0) { + await message.reply('No hay mobs configurados en este servidor.'); + return; + } + + const totalPages = Math.ceil(total / perPage); + + const display = { + type: 17, + accent_color: 0xFF0000, + components: [ + { + type: 9, + components: [{ + type: 10, + content: `**👾 Lista de Mobs**\nPágina ${page}/${totalPages} • Total: ${total}` + }] + }, + { type: 14, divider: true }, + ...mobs.map(mob => { + const stats = mob.stats as any || {}; + return { + type: 9, + components: [{ + type: 10, + content: `**${mob.name || mob.key}**\n` + + `└ Key: \`${mob.key}\`\n` + + `└ ATK: ${stats.attack || 0} | HP: ${stats.hp || 0}\n` + + `└ ${mob.guildId === guildId ? '📍 Local' : '🌐 Global'}` + }] + }; + }) + ] + }; + + const buttons: any[] = []; + + if (page > 1) { + buttons.push({ + type: ComponentType.Button, + style: ButtonStyle.Secondary, + label: '◀ Anterior', + custom_id: `mobs_prev_${page}` + }); + } + + if (page < totalPages) { + buttons.push({ + type: ComponentType.Button, + style: ButtonStyle.Secondary, + label: 'Siguiente ▶', + custom_id: `mobs_next_${page}` + }); + } + + 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('mobs_prev_')) { + const currentPage = parseInt(i.customId.split('_')[2]); + await i.deferUpdate(); + args[0] = String(currentPage - 1); + await command.run!(message, args, client); + collector.stop(); + } else if (i.customId.startsWith('mobs_next_')) { + const currentPage = parseInt(i.customId.split('_')[2]); + await i.deferUpdate(); + args[0] = String(currentPage + 1); + await command.run!(message, args, client); + collector.stop(); + } + }); + } +}; diff --git a/src/commands/messages/game/itemEdit.ts b/src/commands/messages/game/itemEdit.ts index 2d17fb8..db8c920 100644 --- a/src/commands/messages/game/itemEdit.ts +++ b/src/commands/messages/game/itemEdit.ts @@ -20,43 +20,98 @@ interface ItemEditorState { export const command: CommandMessage = { name: 'item-editar', type: 'message', - aliases: ['editar-item','itemedit'], + aliases: ['crear-item','itemcreate'], cooldown: 10, - description: 'Edita un EconomyItem de este servidor con un editor interactivo.', + description: 'Crea un EconomyItem para este servidor con un pequeño editor interactivo.', category: 'Economía', usage: 'item-editar ', run: async (message: Message, args: string[], client: Amayo) => { const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma); - if (!allowed) { await message.reply('❌ No tienes permisos de ManageGuild ni rol de staff.'); return; } + if (!allowed) { + await message.reply('❌ No tienes permisos de ManageGuild ni rol de staff.'); + return; + } + const key = args[0]?.trim(); - if (!key) { await message.reply('Uso: `!item-editar `'); return; } + if (!key) { + await message.reply('Uso: `!item-editar `'); + return; + } + const guildId = message.guild!.id; - const item = await client.prisma.economyItem.findFirst({ where: { key, guildId } }); - if (!item) { await message.reply('❌ No existe un item con esa key en este servidor.'); return; } + const exists = await client.prisma.economyItem.findFirst({ where: { key, guildId } }); + if (exists) { + await message.reply('❌ Ya existe un item con esa key en este servidor.'); + return; + } const state: ItemEditorState = { key, - name: item.name, - description: item.description ?? undefined, - category: item.category ?? undefined, - icon: item.icon ?? undefined, - stackable: item.stackable ?? true, - maxPerInventory: item.maxPerInventory ?? null, - tags: item.tags ?? [], - props: item.props ?? {}, + tags: [], + stackable: true, + maxPerInventory: null, + props: {}, }; + // Función para crear display + const createDisplay = () => ({ + display: { + type: 17, + accent_color: 0x00D9FF, + components: [ + { + type: 9, + components: [{ + type: 10, + content: `**🛠️ Editando Item: \`${key}\`**` + }] + }, + { type: 14, divider: true }, + { + type: 9, + components: [{ + type: 10, + content: `**Nombre:** ${state.name || '*Sin definir*'}\n` + + `**Descripción:** ${state.description || '*Sin definir*'}\n` + + `**Categoría:** ${state.category || '*Sin definir*'}\n` + + `**Icon URL:** ${state.icon || '*Sin definir*'}\n` + + `**Stackable:** ${state.stackable ? 'Sí' : 'No'}\n` + + `**Máx. Inventario:** ${state.maxPerInventory || 'Ilimitado'}` + }] + }, + { type: 14, divider: true }, + { + type: 9, + components: [{ + type: 10, + content: `**Tags:** ${state.tags.length > 0 ? state.tags.join(', ') : '*Ninguno*'}` + }] + }, + { type: 14, divider: true }, + { + type: 9, + components: [{ + type: 10, + content: `**Props (JSON):**\n\`\`\`json\n${JSON.stringify(state.props, null, 2)}\n\`\`\`` + }] + } + ] + } + }); + const channel = message.channel as TextBasedChannel & { send: Function }; const editorMsg = await channel.send({ - content: `🛠️ Editor de Item (editar): \`${key}\``, - components: [ { type: 1, components: [ - { type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'it_base' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Tags', custom_id: 'it_tags' }, - { type: 2, style: ButtonStyle.Secondary, label: 'Props (JSON)', custom_id: 'it_props' }, - { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'it_save' }, - { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'it_cancel' }, - ] } ], + ...createDisplay(), + components: [ + { type: 1, components: [ + { type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'it_base' }, + { type: 2, style: ButtonStyle.Secondary, label: 'Tags', custom_id: 'it_tags' }, + { type: 2, style: ButtonStyle.Secondary, label: 'Props (JSON)', custom_id: 'it_props' }, + { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'it_save' }, + { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'it_cancel' }, + ]}, + ], }); const collector = editorMsg.createMessageComponentCollector({ time: 30 * 60_000, filter: (i) => i.user.id === message.author.id }); @@ -64,15 +119,35 @@ export const command: CommandMessage = { collector.on('collect', async (i: MessageComponentInteraction) => { try { if (!i.isButton()) return; - if (i.customId === 'it_cancel') { await i.deferUpdate(); await editorMsg.edit({ content: '❌ Editor cancelado.', components: [] }); collector.stop('cancel'); return; } - if (i.customId === 'it_base') { await showBaseModal(i as ButtonInteraction, state); return; } - if (i.customId === 'it_tags') { await showTagsModal(i as ButtonInteraction, state); return; } - if (i.customId === 'it_props') { await showPropsModal(i as ButtonInteraction, state); return; } + if (i.customId === 'it_cancel') { + await i.deferUpdate(); + await editorMsg.edit({ content: '❌ Editor cancelado.', components: [], display: undefined }); + collector.stop('cancel'); + return; + } + if (i.customId === 'it_base') { + await showBaseModal(i as ButtonInteraction, state, editorMsg, createDisplay); + return; + } + if (i.customId === 'it_tags') { + await showTagsModal(i as ButtonInteraction, state, editorMsg, createDisplay); + return; + } + if (i.customId === 'it_props') { + await showPropsModal(i as ButtonInteraction, state, editorMsg, createDisplay); + return; + } if (i.customId === 'it_save') { - if (!state.name) { await i.reply({ content: '❌ Falta el nombre del item.', flags: MessageFlags.Ephemeral }); return; } - await client.prisma.economyItem.update({ - where: { id: item.id }, + // Validar + if (!state.name) { + await i.reply({ content: '❌ Falta el nombre del item (configura en Base).', flags: MessageFlags.Ephemeral }); + return; + } + // Guardar + await client.prisma.economyItem.create({ data: { + guildId, + key: state.key, name: state.name!, description: state.description, category: state.category, @@ -83,8 +158,8 @@ export const command: CommandMessage = { props: state.props ?? {}, }, }); - await i.reply({ content: '✅ Item actualizado!', flags: MessageFlags.Ephemeral }); - await editorMsg.edit({ content: `✅ Item \`${state.key}\` actualizado.`, components: [] }); + await i.reply({ content: '✅ Item guardado!', flags: MessageFlags.Ephemeral }); + await editorMsg.edit({ content: `✅ Item \`${state.key}\` creado.`, components: [], display: undefined }); collector.stop('saved'); return; } @@ -94,50 +169,107 @@ export const command: CommandMessage = { } }); - collector.on('end', async (_c, r) => { if (r === 'time') { try { await editorMsg.edit({ content: '⏰ Editor expirado.', components: [] }); } catch {} } }); + collector.on('end', async (_c, r) => { + if (r === 'time') { + try { await editorMsg.edit({ content: '⏰ Editor expirado.', components: [], display: undefined }); } catch {} + } + }); }, }; -async function showBaseModal(i: ButtonInteraction, state: ItemEditorState) { +async function showBaseModal(i: ButtonInteraction, state: ItemEditorState, editorMsg: any, createDisplay: Function) { const modal = { - title: 'Configuración base del Item', customId: 'it_base_modal', components: [ + title: 'Configuración base del Item', + customId: 'it_base_modal', + components: [ { type: ComponentType.Label, label: 'Nombre', component: { type: ComponentType.TextInput, customId: 'name', style: TextInputStyle.Short, required: true, value: state.name ?? '' } }, { type: ComponentType.Label, label: 'Descripción', component: { type: ComponentType.TextInput, customId: 'desc', style: TextInputStyle.Paragraph, required: false, value: state.description ?? '' } }, { type: ComponentType.Label, label: 'Categoría', component: { type: ComponentType.TextInput, customId: 'cat', style: TextInputStyle.Short, required: false, value: state.category ?? '' } }, { type: ComponentType.Label, label: 'Icon URL', component: { type: ComponentType.TextInput, customId: 'icon', style: TextInputStyle.Short, required: false, value: state.icon ?? '' } }, { type: ComponentType.Label, label: 'Stackable y Máx inventario', component: { type: ComponentType.TextInput, customId: 'stack_max', style: TextInputStyle.Short, required: false, placeholder: 'true,10', value: state.stackable !== undefined ? `${state.stackable},${state.maxPerInventory ?? ''}` : '' } }, - ], } as const; + ], + } as const; + await i.showModal(modal); try { const sub = await i.awaitModalSubmit({ time: 300_000 }); - state.name = sub.components.getTextInputValue('name').trim(); - state.description = sub.components.getTextInputValue('desc').trim() || undefined; - state.category = sub.components.getTextInputValue('cat').trim() || undefined; - state.icon = sub.components.getTextInputValue('icon').trim() || undefined; + const name = sub.components.getTextInputValue('name').trim(); + const desc = sub.components.getTextInputValue('desc').trim(); + const cat = sub.components.getTextInputValue('cat').trim(); + const icon = sub.components.getTextInputValue('icon').trim(); const stackMax = sub.components.getTextInputValue('stack_max').trim(); - if (stackMax) { const [s,m] = stackMax.split(','); state.stackable = String(s).toLowerCase() !== 'false'; const mv = m?.trim(); state.maxPerInventory = mv ? Math.max(0, parseInt(mv,10)||0) : null; } - await sub.reply({ content: '✅ Base actualizada.', flags: MessageFlags.Ephemeral }); + + state.name = name; + state.description = desc || undefined; + state.category = cat || undefined; + state.icon = icon || undefined; + + if (stackMax) { + const [s, m] = stackMax.split(','); + state.stackable = String(s).toLowerCase() !== 'false'; + const mv = m?.trim(); + state.maxPerInventory = mv ? Math.max(0, parseInt(mv, 10) || 0) : null; + } + + await sub.deferUpdate(); + await editorMsg.edit(createDisplay()); } catch {} } -async function showTagsModal(i: ButtonInteraction, state: ItemEditorState) { - const modal = { title: 'Tags del Item (separados por coma)', customId: 'it_tags_modal', components: [ - { type: ComponentType.Label, label: 'Tags', component: { type: ComponentType.TextInput, customId: 'tags', style: TextInputStyle.Paragraph, required: false, value: state.tags.join(', ') } }, - ], } as const; +async function showTagsModal(i: ButtonInteraction, state: ItemEditorState, editorMsg: any, createDisplay: Function) { + const modal = { + title: 'Tags del Item (separados por coma)', + customId: 'it_tags_modal', + components: [ + { type: ComponentType.Label, label: 'Tags', component: { type: ComponentType.TextInput, customId: 'tags', style: TextInputStyle.Paragraph, required: false, value: state.tags.join(', ') } }, + ], + } as const; await i.showModal(modal); - try { const sub = await i.awaitModalSubmit({ time: 300_000 }); const tags = sub.components.getTextInputValue('tags'); state.tags = tags ? tags.split(',').map(t=>t.trim()).filter(Boolean) : []; await sub.reply({ content: '✅ Tags actualizados.', flags: MessageFlags.Ephemeral }); } catch {} + try { + const sub = await i.awaitModalSubmit({ time: 300_000 }); + const tags = sub.components.getTextInputValue('tags'); + state.tags = tags ? tags.split(',').map((t) => t.trim()).filter(Boolean) : []; + await sub.deferUpdate(); + await editorMsg.edit(createDisplay()); + } catch {} } -async function showPropsModal(i: ButtonInteraction, state: ItemEditorState) { - const template = state.props && Object.keys(state.props).length ? JSON.stringify(state.props) : JSON.stringify({}); - const modal = { title: 'Props (JSON) del Item', customId: 'it_props_modal', components: [ - { type: ComponentType.Label, label: 'JSON', component: { type: ComponentType.TextInput, customId: 'props', style: TextInputStyle.Paragraph, required: false, value: template.slice(0,4000) } }, - ], } as const; +async function showPropsModal(i: ButtonInteraction, state: ItemEditorState, editorMsg: any, createDisplay: Function) { + const template = state.props && Object.keys(state.props).length ? JSON.stringify(state.props) : JSON.stringify({ + tool: undefined, + breakable: undefined, + chest: undefined, + eventCurrency: undefined, + passiveEffects: [], + mutationPolicy: undefined, + craftingOnly: false, + food: undefined, + damage: undefined, + defense: undefined, + maxHpBonus: undefined, + }); + const modal = { + title: 'Props (JSON) del Item', + customId: 'it_props_modal', + components: [ + { type: ComponentType.Label, label: 'JSON', component: { type: ComponentType.TextInput, customId: 'props', style: TextInputStyle.Paragraph, required: false, value: template.slice(0,4000) } }, + ], + } as const; await i.showModal(modal); try { const sub = await i.awaitModalSubmit({ time: 300_000 }); const raw = sub.components.getTextInputValue('props'); - if (raw) { try { state.props = JSON.parse(raw); await sub.reply({ content: '✅ Props guardados.', flags: MessageFlags.Ephemeral }); } catch { await sub.reply({ content: '❌ JSON inválido.', flags: MessageFlags.Ephemeral }); } } - else { state.props = {}; await sub.reply({ content: 'ℹ️ Props limpiados.', flags: MessageFlags.Ephemeral }); } + if (raw) { + try { + const parsed = JSON.parse(raw); + state.props = parsed; + await sub.deferUpdate(); await editorMsg.edit(createDisplay()); + } catch (e) { + await sub.reply({ content: '❌ JSON inválido.', flags: MessageFlags.Ephemeral }); + } + } else { + state.props = {}; + await sub.reply({ content: 'ℹ️ Props limpiados.', flags: MessageFlags.Ephemeral }); + } } catch {} } diff --git a/src/commands/messages/game/player.ts b/src/commands/messages/game/player.ts index cdad3b6..63ce151 100644 --- a/src/commands/messages/game/player.ts +++ b/src/commands/messages/game/player.ts @@ -4,6 +4,7 @@ import { prisma } from '../../../core/database/prisma'; import { getOrCreateWallet } from '../../../game/economy/service'; import { getEquipment, getEffectiveStats } from '../../../game/combat/equipmentService'; import { getPlayerStatsFormatted } from '../../../game/stats/service'; +import type { TextBasedChannel } from 'discord.js'; export const command: CommandMessage = { name: 'player', @@ -166,6 +167,10 @@ export const command: CommandMessage = { }); } - await message.reply({ display } as any); + const channel = message.channel as TextBasedChannel & { send: Function }; + await (channel.send as any)({ + display, + reply: { messageReference: message.id } + }); } };