diff --git a/DISPLAYCOMPONENTS_IMPLEMENTATION.md b/DISPLAYCOMPONENTS_IMPLEMENTATION.md new file mode 100644 index 0000000..58f2ea4 --- /dev/null +++ b/DISPLAYCOMPONENTS_IMPLEMENTATION.md @@ -0,0 +1,298 @@ +# 🎨 Implementación de DisplayComponents y Comandos Admin + +## ✅ Nuevos Comandos Administrativos Creados + +### 1. **!logro-crear** (`src/commands/messages/admin/logroCrear.ts`) +Editor interactivo completo para crear logros usando DisplayComponents. + +**Características:** +- ✅ Preview visual con DisplayComponents +- ✅ Modales interactivos para editar cada sección +- ✅ Sections: Base, Requisitos, Recompensas +- ✅ Validación de JSON para requisitos y recompensas +- ✅ Botones de navegación intuitivos +- ✅ Auto-guardado con confirmación + +**Uso:** +``` +!logro-crear +``` + +**Display Components utilizados:** +- Container (type 17) - Contenedor principal con accent_color +- Section (type 9) - Secciones organizadas +- TextDisplay (type 10) - Contenido de texto formateado +- Separator (type 14) - Divisores visuales +- Modales con Label + TextInput para entrada de datos +- TextDisplay en modales para instrucciones + +### 2. **!mision-crear** (`src/commands/messages/admin/misionCrear.ts`) +Editor interactivo completo para crear misiones usando DisplayComponents. + +**Características:** +- ✅ Preview visual con DisplayComponents +- ✅ Modales para Base, Requisitos y Recompensas +- ✅ Soporte para tipos: daily, weekly, permanent, event +- ✅ Validación de JSON +- ✅ Emojis contextuales según tipo de misión + +**Uso:** +``` +!mision-crear +``` + +## 🎨 DisplayComponents - Guía de Uso + +### Tipos de Componentes Implementados + +```typescript +// Container - Contenedor principal (type 17) +{ + type: 17, + accent_color: 0xFFD700, // Color hex + components: [ /* otros componentes */ ] +} + +// Section - Sección con contenido (type 9) +{ + type: 9, + components: [ /* TextDisplay, etc */ ] +} + +// TextDisplay - Texto formateado (type 10) +{ + type: 10, + content: "**Bold** *Italic* `Code` ```json\n{}\n```" +} + +// Separator - Divisor visual (type 14) +{ + type: 14, + divider: true +} + +// Thumbnail - Imagen/thumbnail (type 11) +{ + type: 11, + media: { url: "https://..." } +} +``` + +### Modales con DisplayComponents + +```typescript +const modal = { + title: 'Título del Modal', + customId: 'modal_id', + components: [ + // TextDisplay para instrucciones + { + type: ComponentType.TextDisplay, + content: 'Instrucciones aquí' + }, + // Label con TextInput + { + type: ComponentType.Label, + label: 'Campo a llenar', + component: { + type: ComponentType.TextInput, + customId: 'field_id', + style: TextInputStyle.Short, // o Paragraph + required: true, + value: 'Valor actual', + placeholder: 'Placeholder...' + } + } + ] +} as const; + +await interaction.showModal(modal); +``` + +### Responder a Modal Submits + +```typescript +const submit = await interaction.awaitModalSubmit({ + time: 5 * 60_000 +}).catch(() => null); + +if (!submit) return; + +const value = submit.components.getTextInputValue('field_id'); + +// Actualizar display +await submit.deferUpdate(); +await message.edit({ display: newDisplay }); +``` + +## 📊 Estructura de Display + +Los comandos admin siguen esta estructura: + +``` +┌─────────────────────────────┐ +│ Container (accent_color) │ +├─────────────────────────────┤ +│ Section: Título │ +├─────────────────────────────┤ +│ Separator (divider) │ +├─────────────────────────────┤ +│ Section: Campos Base │ +├─────────────────────────────┤ +│ Separator │ +├─────────────────────────────┤ +│ Section: Requisitos (JSON) │ +├─────────────────────────────┤ +│ Separator │ +├─────────────────────────────┤ +│ Section: Recompensas (JSON) │ +└─────────────────────────────┘ +``` + +## 🔧 Integración con Comandos Existentes + +### Próximos comandos a actualizar con DisplayComponents: + +1. **!inventario** - Lista de items visual con thumbnails +2. **!tienda** - Catálogo visual de items +3. **!player** - Stats del jugador en formato visual +4. **!item-crear** - Mejorar con DisplayComponents +5. **!area-crear** - Mejorar con DisplayComponents +6. **!mob-crear** - Mejorar con DisplayComponents + +### Patrón recomendado para actualizar comandos: + +```typescript +// 1. Crear función de display +function createDisplay(data: any) { + return { + display: { + type: 17, + accent_color: 0xCOLOR, + components: [ + // Secciones aquí + ] + } + }; +} + +// 2. Enviar con botones +const msg = await channel.send({ + ...createDisplay(data), + components: [ + { + type: ComponentType.ActionRow, + components: [ /* botones */ ] + } + ] +}); + +// 3. Collector para interacciones +const collector = msg.createMessageComponentCollector({ ... }); + +// 4. Actualizar display al cambiar datos +await msg.edit(createDisplay(updatedData)); +``` + +## 🎯 Ventajas de DisplayComponents + +### vs. Embeds tradicionales: +- ✅ Más moderno y visual +- ✅ Mejor separación de secciones +- ✅ Dividers nativos +- ✅ Accent color personalizable +- ✅ Mejor para contenido largo +- ✅ Soporte nativo en discord.js dev + +### vs. Texto plano: +- ✅ Muchísimo más visual +- ✅ Organización clara +- ✅ Professional look +- ✅ Mejor UX para el usuario +- ✅ Más información en menos espacio + +## 📝 Notas de Implementación + +### Tipos de Componente (ComponentType): +- `17` - Container +- `9` - Section +- `10` - TextDisplay +- `14` - Separator +- `11` - Thumbnail +- `2` - Button (ActionRow) +- `3` - Select Menu +- `4` - TextInput (en modales) +- `40` - Label (wrapper para inputs en modales) + +### Best Practices: + +1. **Usar accent_color** para dar contexto visual + - Logros: 0xFFD700 (dorado) + - Misiones: 0x5865F2 (azul Discord) + - Errores: 0xFF0000 (rojo) + - Éxito: 0x00FF00 (verde) + +2. **Separators con divider: true** entre secciones importantes + +3. **TextDisplay soporta Markdown**: + - **Bold**, *Italic*, `Code` + - ```json code blocks``` + - Listas, etc. + +4. **Modales siempre usan `as const`** para type safety + +5. **awaitModalSubmit** debe tener timeout y catch + +6. **Siempre hacer deferUpdate()** antes de editar mensaje tras modal + +## 🚀 Testing + +### Comandos a probar: + +```bash +# Crear logro +!logro-crear test_achievement + +# Crear misión +!mision-crear test_quest + +# Verificar que los displays se ven correctamente +# Verificar que los modales funcionan +# Verificar que el guardado funciona +# Verificar que los datos se persisten en DB +``` + +### Verificar en Discord: +1. Los DisplayComponents se renderizan correctamente +2. Los separators dividen las secciones +3. El accent_color se muestra +4. Los botones son clickeables +5. Los modales se abren +6. Los TextDisplay en modales son visibles +7. Los datos se guardan correctamente + +## 📚 Recursos + +- **Ejemplo oficial**: `example.ts.txt` en la raíz del proyecto +- **Tipos**: `src/core/types/displayComponents.ts` +- **Discord.js types**: `node_modules/discord.js/typings/index.d.ts` +- **API Types**: `node_modules/discord-api-types/` + +## ⚠️ Limitaciones Conocidas + +1. DisplayComponents son beta en discord.js +2. No todas las features están documentadas +3. Algunos componentes pueden no funcionar en mobile +4. TextDisplay tiene límite de caracteres (~2000) +5. Containers tienen límite de componentes (~25) + +## 🎯 Próximos Pasos + +1. ✅ Comandos admin para logros y misiones - COMPLETADO +2. ⬜ Actualizar !inventario con DisplayComponents +3. ⬜ Actualizar !tienda con DisplayComponents +4. ⬜ Actualizar !player con DisplayComponents +5. ⬜ Mejorar !item-crear, !area-crear, !mob-crear +6. ⬜ Crear comando !ranking con DisplayComponents +7. ⬜ Crear comando !logros con DisplayComponents mejorado +8. ⬜ Crear comando !misiones con DisplayComponents mejorado diff --git a/src/commands/messages/admin/logroCrear.ts b/src/commands/messages/admin/logroCrear.ts new file mode 100644 index 0000000..199b7ab --- /dev/null +++ b/src/commands/messages/admin/logroCrear.ts @@ -0,0 +1,356 @@ +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, TextInputStyle, ButtonStyle } from 'discord-api-types/v10'; +import type { ButtonInteraction, MessageComponentInteraction, TextBasedChannel } from 'discord.js'; + +interface AchievementState { + key: string; + name?: string; + description?: string; + category?: string; + icon?: string; + requirements?: any; + rewards?: any; + points?: number; + hidden?: boolean; +} + +export const command: CommandMessage = { + name: 'logro-crear', + type: 'message', + aliases: ['crear-logro', 'achievement-create'], + cooldown: 10, + description: 'Crea un logro para el servidor con editor interactivo', + usage: 'logro-crear ', + 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 key = args[0]?.trim(); + if (!key) { + await message.reply('Uso: `!logro-crear `\nEjemplo: `!logro-crear master_fisher`'); + return; + } + + const guildId = message.guild!.id; + const exists = await prisma.achievement.findFirst({ where: { key, guildId } }); + if (exists) { + await message.reply('❌ Ya existe un logro con esa key en este servidor.'); + return; + } + + const state: AchievementState = { + key, + category: 'economy', + points: 10, + hidden: false, + requirements: { type: 'mine_count', value: 1 }, + rewards: { coins: 100 } + }; + + // Crear mensaje con DisplayComponents + const displayMessage = createDisplay(state); + + const channel = message.channel as TextBasedChannel & { send: Function }; + const editorMsg = await channel.send({ + ...displayMessage, + components: [ + { + type: ComponentType.ActionRow, + components: [ + { type: ComponentType.Button, style: ButtonStyle.Primary, label: 'Base', custom_id: 'ach_base' }, + { type: ComponentType.Button, style: ButtonStyle.Secondary, label: 'Requisitos', custom_id: 'ach_req' }, + { type: ComponentType.Button, style: ButtonStyle.Secondary, label: 'Recompensas', custom_id: 'ach_reward' }, + { type: ComponentType.Button, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'ach_save' }, + { type: ComponentType.Button, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'ach_cancel' } + ] + } + ] + }); + + const collector = editorMsg.createMessageComponentCollector({ + time: 30 * 60_000, + filter: (i) => i.user.id === message.author.id + }); + + collector.on('collect', async (i: MessageComponentInteraction) => { + try { + if (!i.isButton()) return; + + switch (i.customId) { + case 'ach_cancel': + await i.deferUpdate(); + await editorMsg.edit({ content: '❌ Creación de logro cancelada.', components: [], display: undefined }); + collector.stop('cancel'); + return; + + case 'ach_base': + await showBaseModal(i as ButtonInteraction, state, editorMsg); + return; + + case 'ach_req': + await showRequirementsModal(i as ButtonInteraction, state, editorMsg); + return; + + case 'ach_reward': + await showRewardsModal(i as ButtonInteraction, state, editorMsg); + return; + + case 'ach_save': + if (!state.name || !state.description) { + await i.reply({ content: '❌ Completa al menos el nombre y descripción.', flags: 64 }); + return; + } + + await prisma.achievement.create({ + data: { + guildId, + key: state.key, + name: state.name!, + description: state.description!, + category: state.category || 'economy', + icon: state.icon, + requirements: state.requirements as any || {}, + rewards: state.rewards as any || {}, + points: state.points || 10, + hidden: state.hidden || false + } + }); + + await i.reply({ content: '✅ Logro creado exitosamente.', flags: 64 }); + await editorMsg.edit({ + content: `✅ Logro \`${state.key}\` creado.`, + components: [], + display: undefined + }); + collector.stop('saved'); + return; + } + } catch (e: any) { + console.error('Error en editor de logros:', e); + if (!i.deferred && !i.replied) { + await i.reply({ content: '❌ Error procesando la acción.', flags: 64 }); + } + } + }); + + collector.on('end', async (_c, r) => { + if (r === 'time') { + try { + await editorMsg.edit({ content: '⏰ Editor expirado.', components: [], display: undefined }); + } catch {} + } + }); + } +}; + +function createDisplay(state: AchievementState) { + return { + display: { + type: 17, // Container + accent_color: 0xFFD700, + components: [ + { + type: 9, // Section + components: [ + { + type: 10, // Text Display + content: `**🏆 Creando Logro: \`${state.key}\`**` + } + ] + }, + { type: 14, divider: true }, // Separator + { + type: 9, + components: [ + { + type: 10, + content: `**Nombre:** ${state.name || '*Sin definir*'}\n**Descripción:** ${state.description || '*Sin definir*'}\n**Categoría:** ${state.category || 'economy'}\n**Icono:** ${state.icon || '🏆'}\n**Puntos:** ${state.points || 10}\n**Oculto:** ${state.hidden ? 'Sí' : 'No'}` + } + ] + }, + { type: 14, divider: true }, + { + type: 9, + components: [ + { + type: 10, + content: `**Requisitos:**\n\`\`\`json\n${JSON.stringify(state.requirements, null, 2)}\n\`\`\`` + } + ] + }, + { type: 14, divider: true }, + { + type: 9, + components: [ + { + type: 10, + content: `**Recompensas:**\n\`\`\`json\n${JSON.stringify(state.rewards, null, 2)}\n\`\`\`` + } + ] + } + ] + } + }; +} + +async function showBaseModal(i: ButtonInteraction, state: AchievementState, editorMsg: any) { + const modal = { + title: 'Información Base del Logro', + customId: 'ach_base_modal', + components: [ + { + type: ComponentType.Label, + label: 'Nombre del logro', + component: { + type: ComponentType.TextInput, + customId: 'name', + style: TextInputStyle.Short, + required: true, + value: state.name || '', + placeholder: 'Ej: Maestro Pescador' + } + }, + { + type: ComponentType.Label, + label: 'Descripción', + component: { + type: ComponentType.TextInput, + customId: 'description', + style: TextInputStyle.Paragraph, + required: true, + value: state.description || '', + placeholder: 'Ej: Pesca 100 veces' + } + }, + { + type: ComponentType.Label, + label: 'Categoría (mining/fishing/combat/economy/crafting)', + component: { + type: ComponentType.TextInput, + customId: 'category', + style: TextInputStyle.Short, + required: false, + value: state.category || 'economy' + } + }, + { + type: ComponentType.Label, + label: 'Icono (emoji)', + component: { + type: ComponentType.TextInput, + customId: 'icon', + style: TextInputStyle.Short, + required: false, + value: state.icon || '🏆' + } + }, + { + type: ComponentType.Label, + label: 'Puntos (número)', + component: { + type: ComponentType.TextInput, + customId: 'points', + style: TextInputStyle.Short, + required: false, + value: String(state.points || 10) + } + } + ] + } as const; + + await i.showModal(modal); + + const submit = await i.awaitModalSubmit({ time: 5 * 60_000 }).catch(() => null); + if (!submit) return; + + state.name = submit.components.getTextInputValue('name'); + state.description = submit.components.getTextInputValue('description'); + state.category = submit.components.getTextInputValue('category') || 'economy'; + state.icon = submit.components.getTextInputValue('icon') || '🏆'; + state.points = parseInt(submit.components.getTextInputValue('points')) || 10; + + await submit.deferUpdate(); + await editorMsg.edit(createDisplay(state)); +} + +async function showRequirementsModal(i: ButtonInteraction, state: AchievementState, editorMsg: any) { + const modal = { + title: 'Requisitos del Logro', + customId: 'ach_req_modal', + components: [ + { + type: ComponentType.TextDisplay, + content: 'Formato JSON con "type" y "value"' + }, + { + type: ComponentType.Label, + label: 'Requisitos (JSON)', + component: { + type: ComponentType.TextInput, + customId: 'requirements', + style: TextInputStyle.Paragraph, + required: true, + value: JSON.stringify(state.requirements, null, 2), + placeholder: '{"type": "mine_count", "value": 100}' + } + } + ] + } as const; + + await i.showModal(modal); + + const submit = await i.awaitModalSubmit({ time: 5 * 60_000 }).catch(() => null); + if (!submit) return; + + try { + state.requirements = JSON.parse(submit.components.getTextInputValue('requirements')); + await submit.deferUpdate(); + await editorMsg.edit(createDisplay(state)); + } catch (e) { + await submit.reply({ content: '❌ JSON inválido en requisitos.', flags: 64 }); + } +} + +async function showRewardsModal(i: ButtonInteraction, state: AchievementState, editorMsg: any) { + const modal = { + title: 'Recompensas del Logro', + customId: 'ach_reward_modal', + components: [ + { + type: ComponentType.TextDisplay, + content: 'Formato JSON con coins, items, etc.' + }, + { + type: ComponentType.Label, + label: 'Recompensas (JSON)', + component: { + type: ComponentType.TextInput, + customId: 'rewards', + style: TextInputStyle.Paragraph, + required: true, + value: JSON.stringify(state.rewards, null, 2), + placeholder: '{"coins": 1000, "items": [{"key": "item.key", "quantity": 1}]}' + } + } + ] + } as const; + + await i.showModal(modal); + + const submit = await i.awaitModalSubmit({ time: 5 * 60_000 }).catch(() => null); + if (!submit) return; + + try { + state.rewards = JSON.parse(submit.components.getTextInputValue('rewards')); + await submit.deferUpdate(); + await editorMsg.edit(createDisplay(state)); + } catch (e) { + await submit.reply({ content: '❌ JSON inválido en recompensas.', flags: 64 }); + } +} diff --git a/src/commands/messages/admin/misionCrear.ts b/src/commands/messages/admin/misionCrear.ts new file mode 100644 index 0000000..513fdae --- /dev/null +++ b/src/commands/messages/admin/misionCrear.ts @@ -0,0 +1,363 @@ +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, TextInputStyle, ButtonStyle } from 'discord-api-types/v10'; +import type { ButtonInteraction, MessageComponentInteraction, TextBasedChannel } from 'discord.js'; + +interface QuestState { + key: string; + name?: string; + description?: string; + category?: string; + type?: string; + icon?: string; + requirements?: any; + rewards?: any; + repeatable?: boolean; +} + +export const command: CommandMessage = { + name: 'mision-crear', + type: 'message', + aliases: ['crear-mision', 'quest-create'], + cooldown: 10, + description: 'Crea una misión para el servidor con editor interactivo', + usage: 'mision-crear ', + 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 key = args[0]?.trim(); + if (!key) { + await message.reply('Uso: `!mision-crear `\nEjemplo: `!mision-crear daily_mine_10`'); + return; + } + + const guildId = message.guild!.id; + const exists = await prisma.quest.findFirst({ where: { key, guildId } }); + if (exists) { + await message.reply('❌ Ya existe una misión con esa key en este servidor.'); + return; + } + + const state: QuestState = { + key, + category: 'mining', + type: 'daily', + repeatable: false, + requirements: { type: 'mine_count', count: 10 }, + rewards: { coins: 500 } + }; + + const displayMessage = createDisplay(state); + + const channel = message.channel as TextBasedChannel & { send: Function }; + const editorMsg = await channel.send({ + ...displayMessage, + components: [ + { + type: ComponentType.ActionRow, + components: [ + { type: ComponentType.Button, style: ButtonStyle.Primary, label: 'Base', custom_id: 'quest_base' }, + { type: ComponentType.Button, style: ButtonStyle.Secondary, label: 'Requisitos', custom_id: 'quest_req' }, + { type: ComponentType.Button, style: ButtonStyle.Secondary, label: 'Recompensas', custom_id: 'quest_reward' }, + { type: ComponentType.Button, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'quest_save' }, + { type: ComponentType.Button, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'quest_cancel' } + ] + } + ] + }); + + const collector = editorMsg.createMessageComponentCollector({ + time: 30 * 60_000, + filter: (i) => i.user.id === message.author.id + }); + + collector.on('collect', async (i: MessageComponentInteraction) => { + try { + if (!i.isButton()) return; + + switch (i.customId) { + case 'quest_cancel': + await i.deferUpdate(); + await editorMsg.edit({ content: '❌ Creación de misión cancelada.', components: [], display: undefined }); + collector.stop('cancel'); + return; + + case 'quest_base': + await showBaseModal(i as ButtonInteraction, state, editorMsg); + return; + + case 'quest_req': + await showRequirementsModal(i as ButtonInteraction, state, editorMsg); + return; + + case 'quest_reward': + await showRewardsModal(i as ButtonInteraction, state, editorMsg); + return; + + case 'quest_save': + if (!state.name || !state.description) { + await i.reply({ content: '❌ Completa al menos el nombre y descripción.', flags: 64 }); + return; + } + + await prisma.quest.create({ + data: { + guildId, + key: state.key, + name: state.name!, + description: state.description!, + category: state.category || 'mining', + type: state.type || 'daily', + icon: state.icon, + requirements: state.requirements as any || {}, + rewards: state.rewards as any || {}, + repeatable: state.repeatable || false, + active: true + } + }); + + await i.reply({ content: '✅ Misión creada exitosamente.', flags: 64 }); + await editorMsg.edit({ + content: `✅ Misión \`${state.key}\` creada.`, + components: [], + display: undefined + }); + collector.stop('saved'); + return; + } + } catch (e: any) { + console.error('Error en editor de misiones:', e); + if (!i.deferred && !i.replied) { + await i.reply({ content: '❌ Error procesando la acción.', flags: 64 }); + } + } + }); + + collector.on('end', async (_c, r) => { + if (r === 'time') { + try { + await editorMsg.edit({ content: '⏰ Editor expirado.', components: [], display: undefined }); + } catch {} + } + }); + } +}; + +function createDisplay(state: QuestState) { + const typeEmojis: Record = { + daily: '📅', + weekly: '📆', + permanent: '♾️', + event: '🎉' + }; + + return { + display: { + type: 17, // Container + accent_color: 0x5865F2, + components: [ + { + type: 9, // Section + components: [ + { + type: 10, // Text Display + content: `**📜 Creando Misión: \`${state.key}\`**` + } + ] + }, + { type: 14, divider: true }, // Separator + { + type: 9, + components: [ + { + type: 10, + content: `**Nombre:** ${state.name || '*Sin definir*'}\n**Descripción:** ${state.description || '*Sin definir*'}\n**Categoría:** ${state.category || 'mining'}\n**Tipo:** ${typeEmojis[state.type || 'daily']} ${state.type || 'daily'}\n**Icono:** ${state.icon || '📋'}\n**Repetible:** ${state.repeatable ? 'Sí' : 'No'}` + } + ] + }, + { type: 14, divider: true }, + { + type: 9, + components: [ + { + type: 10, + content: `**Requisitos:**\n\`\`\`json\n${JSON.stringify(state.requirements, null, 2)}\n\`\`\`` + } + ] + }, + { type: 14, divider: true }, + { + type: 9, + components: [ + { + type: 10, + content: `**Recompensas:**\n\`\`\`json\n${JSON.stringify(state.rewards, null, 2)}\n\`\`\`` + } + ] + } + ] + } + }; +} + +async function showBaseModal(i: ButtonInteraction, state: QuestState, editorMsg: any) { + const modal = { + title: 'Información Base de la Misión', + customId: 'quest_base_modal', + components: [ + { + type: ComponentType.Label, + label: 'Nombre de la misión', + component: { + type: ComponentType.TextInput, + customId: 'name', + style: TextInputStyle.Short, + required: true, + value: state.name || '', + placeholder: 'Ej: Minero Diario' + } + }, + { + type: ComponentType.Label, + label: 'Descripción', + component: { + type: ComponentType.TextInput, + customId: 'description', + style: TextInputStyle.Paragraph, + required: true, + value: state.description || '', + placeholder: 'Ej: Mina 10 veces hoy' + } + }, + { + type: ComponentType.Label, + label: 'Categoría (mining/fishing/combat/economy/crafting)', + component: { + type: ComponentType.TextInput, + customId: 'category', + style: TextInputStyle.Short, + required: false, + value: state.category || 'mining' + } + }, + { + type: ComponentType.Label, + label: 'Tipo (daily/weekly/permanent/event)', + component: { + type: ComponentType.TextInput, + customId: 'type', + style: TextInputStyle.Short, + required: false, + value: state.type || 'daily' + } + }, + { + type: ComponentType.Label, + label: 'Icono (emoji)', + component: { + type: ComponentType.TextInput, + customId: 'icon', + style: TextInputStyle.Short, + required: false, + value: state.icon || '📋' + } + } + ] + } as const; + + await i.showModal(modal); + + const submit = await i.awaitModalSubmit({ time: 5 * 60_000 }).catch(() => null); + if (!submit) return; + + state.name = submit.components.getTextInputValue('name'); + state.description = submit.components.getTextInputValue('description'); + state.category = submit.components.getTextInputValue('category') || 'mining'; + state.type = submit.components.getTextInputValue('type') || 'daily'; + state.icon = submit.components.getTextInputValue('icon') || '📋'; + + await submit.deferUpdate(); + await editorMsg.edit(createDisplay(state)); +} + +async function showRequirementsModal(i: ButtonInteraction, state: QuestState, editorMsg: any) { + const modal = { + title: 'Requisitos de la Misión', + customId: 'quest_req_modal', + components: [ + { + type: ComponentType.TextDisplay, + content: 'Formato JSON con "type" y "count"' + }, + { + type: ComponentType.Label, + label: 'Requisitos (JSON)', + component: { + type: ComponentType.TextInput, + customId: 'requirements', + style: TextInputStyle.Paragraph, + required: true, + value: JSON.stringify(state.requirements, null, 2), + placeholder: '{"type": "mine_count", "count": 10}' + } + } + ] + } as const; + + await i.showModal(modal); + + const submit = await i.awaitModalSubmit({ time: 5 * 60_000 }).catch(() => null); + if (!submit) return; + + try { + state.requirements = JSON.parse(submit.components.getTextInputValue('requirements')); + await submit.deferUpdate(); + await editorMsg.edit(createDisplay(state)); + } catch (e) { + await submit.reply({ content: '❌ JSON inválido en requisitos.', flags: 64 }); + } +} + +async function showRewardsModal(i: ButtonInteraction, state: QuestState, editorMsg: any) { + const modal = { + title: 'Recompensas de la Misión', + customId: 'quest_reward_modal', + components: [ + { + type: ComponentType.TextDisplay, + content: 'Formato JSON con coins, items, xp, etc.' + }, + { + type: ComponentType.Label, + label: 'Recompensas (JSON)', + component: { + type: ComponentType.TextInput, + customId: 'rewards', + style: TextInputStyle.Paragraph, + required: true, + value: JSON.stringify(state.rewards, null, 2), + placeholder: '{"coins": 500, "items": [{"key": "item.key", "quantity": 1}]}' + } + } + ] + } as const; + + await i.showModal(modal); + + const submit = await i.awaitModalSubmit({ time: 5 * 60_000 }).catch(() => null); + if (!submit) return; + + try { + state.rewards = JSON.parse(submit.components.getTextInputValue('rewards')); + await submit.deferUpdate(); + await editorMsg.edit(createDisplay(state)); + } catch (e) { + await submit.reply({ content: '❌ JSON inválido en recompensas.', flags: 64 }); + } +}