From ab49a7d3c537dbcaaca91e0d3a4df656e5702259 Mon Sep 17 00:00:00 2001 From: shni Date: Mon, 6 Oct 2025 11:47:22 -0500 Subject: [PATCH] feat: add crafting recipe functionality to item creation and editing commands --- Mas Ejemplos.md | 490 +++++++++++++++++++++-- src/commands/messages/game/itemCreate.ts | 153 ++++++- src/commands/messages/game/itemEdit.ts | 201 ++++++++++ 3 files changed, 815 insertions(+), 29 deletions(-) diff --git a/Mas Ejemplos.md b/Mas Ejemplos.md index 42581ec..15d1c1a 100644 --- a/Mas Ejemplos.md +++ b/Mas Ejemplos.md @@ -1,6 +1,133 @@ # Guía rápida para el staff: crear y ajustar contenido desde Discord -Este documento reúne## Mutaciones y encantamientos +Este documento reúne ejemplos prácticos y flujos de trabajo completos **para el equipo de staff**. Todo lo que ves aquí se realiza directamente con los comandos del bot (prefijo `!`), sin tocar código ni ejecutar scripts. Los comandos viven en `src/commands/messages/admin` y `src/commands/messages/game`, pero no necesitas abrir esos archivos: la idea es que puedas hacerlo todo desde Discord siguiendo estos pasos. + +--- + +## Tabla de contenidos + +1. [Antes de empezar](#antes-de-empezar) +2. [Items: creación, edición y revisión](#items-creación-edición-y-revisión) +3. [Crafteos y materiales](#crafteos-y-materiales) ⭐ **¡Ahora con editor integrado!** +4. [Fundición y refinado](#fundición-y-refinado) +5. [Mutaciones y encantamientos](#mutaciones-y-encantamientos) +6. [Mobs: enemigos y NPCs](#mobs-enemigos-y-npcs) +7. [Áreas y niveles](#áreas-y-niveles) +8. [Misiones (Quests)](#misiones-quests) +9. [Logros (Achievements)](#logros-achievements) +10. [Workflows completos de ejemplo](#workflows-completos-de-ejemplo) + +--- + +## 🎯 Flujo rápido: Crear un ítem con receta de crafteo + +**Nuevo proceso (2025)** - Todo desde Discord, sin código: + +``` +1. !item-crear iron_ingot → Crear ingrediente 1 +2. !item-crear wood_plank → Crear ingrediente 2 +3. !item-crear iron_sword → Crear producto + ├─ Pulsar "Base" → Nombre, descripción, etc. + ├─ Pulsar "Props" → Agregar {"craftable": {"enabled": true}} + ├─ Pulsar "Receta" ⭐ NUEVO → Escribir: iron_ingot:3, wood_plank:1 + └─ Pulsar "Guardar" → ¡Listo! Receta activa +4. !craftear iron_sword → Los jugadores pueden craftear +``` + +**Antes (2024)**: Había que pedirle al equipo dev que ejecutara scripts de Prisma 🚫 + +--- + +## Antes de empezar + +> ⭐ **¡NUEVO!** Ahora puedes crear y editar recetas de crafteo directamente desde Discord sin necesidad del equipo dev. Usa el botón **Receta** en los comandos `!item-crear` e `!item-editar`. Ver [sección de Crafteos](#crear-nuevas-recetas-de-crafteo-directo-desde-discord) para más detalles. + +- Asegúrate de tener el permiso `Manage Guild` o el rol de staff configurado; varios comandos lo revisan con `hasManageGuildOrStaff`. +- Siempre usa claves (`key`) en minúsculas y sin espacios. Son únicas por servidor y no se pueden repetir. +- Todos los editores funcionan con botones + modales. Si cierras la ventana o pasa más de 30 min sin responder, el editor caduca y debes reabrirlo. +- Cuando un modal pida JSON, puedes copiar los ejemplos de esta guía y ajustarlos. Si el JSON no es válido, el bot te avisará y no guardará los cambios. + +--- + +## Items: creación, edición y revisión + +### Crear un ítem nuevo — `!item-crear ` +1. Escribe `!item-crear piedra_mistica` (usa la key que necesites). +2. Pulsa **Base** y completa: + - **Nombre** y **Descripción**: lo que verán los jugadores. + - **Categoría** (opcional) para agrupar en listados (`weapon`, `material`, `consumible`, etc.). + - **Icon URL** si tienes una imagen. + - **Stackable y Máx inventario** en formato `true,10`. Ejemplos: `true,64`, `false,1`, o deja vacío para infinito. +3. Pulsa **Tags** y agrega etiquetas separadas por coma (`rare, evento`); sirven para filtrar en `!items-lista`. +4. Pulsa **Props (JSON)** y pega solo lo que necesites. Ejemplo rápido para una herramienta que también cura al uso: + +```json +{ + "tool": { "type": "pickaxe", "tier": 2 }, + "breakable": { "enabled": true, "maxDurability": 120 }, + "food": { "healHp": 25, "cooldownSeconds": 180 } +} +``` + +5. Pulsa **Receta** (⭐ nuevo) si quieres que el ítem sea crafteable. Ver [sección de Crafteos](#crear-nuevas-recetas-de-crafteo-directo-desde-discord) para más detalles. +6. Cuando todo esté listo, pulsa **Guardar**. El bot confirmará con "✅ Item creado". + +### Editar, listar y borrar + +- `!item-editar` abre el mismo editor, pero cargando un ítem existente. +- `!item-eliminar ` borra la versión local (solicita confirmación). +- `!items-lista` y `!item-ver ` sirven para revisar lo que ya existe. + +> 💡 Tip: si solo quieres revisar las propiedades de un ítem, usa `!item-ver `; mostrará los `props` formateados en JSON. + +### Preparar ítems especiales + +- **Consumibles**: en Props agrega + + ```json + "food": { + "healHp": 40, + "healPercent": 10, + "cooldownKey": "food:pocion_epica", + "cooldownSeconds": 120 + } + ``` + + Luego prueba con `!comer pocion_epica` (usa la key real) para ver el mensaje de curación y el cooldown. + +- **Cofres**: añade + + ```json + "chest": { + "enabled": true, + "consumeOnOpen": true, + "rewards": [ + { "type": "coins", "amount": 500 }, + { "type": "item", "itemKey": "token_evento", "qty": 3 } + ] + } + ``` + + Después abre el cofre con `!abrir `. + +- **Armas/armaduras**: usa `damage`, `defense` o `maxHpBonus`. Si quieres limitar mutaciones, agrega `mutationPolicy` (ver sección más abajo). + +--- + +## Crafteos y materiales + +El crafteo permite combinar materiales para crear ítems más valiosos. A diferencia de la fundición, el crafteo es instantáneo y no requiere tiempo de espera. + +### Cómo funciona el crafteo + +1. El jugador ejecuta `!craftear `. +2. El bot verifica que tenga todos los ingredientes. +3. Si los tiene, los descuenta del inventario y entrega el producto inmediatamente. +4. Las estadísticas del jugador se actualizan (`itemsCrafted`). + +--- + +## Mutaciones y encantamientos Las mutaciones permiten mejorar ítems agregándoles bonificaciones especiales. Son consumibles permanentes que se aplican a un ítem específico. @@ -111,11 +238,11 @@ Decide qué mutaciones puede recibir cada ítem editando sus **Props**: | `vampire_core` | Vampirismo | +10 damage, lifesteal | Armas | | `thorns_enchant` | Espinas | refleja daño | Armaduras | -> 💡 **Tip**: las mutaciones con efectos custom (como `fortune_enchant` que aumenta drops) requieren lógica adicional en el código. Consulta con el equipo dev antes de anunciarlas.ados **para el equipo de staff**. Todo lo que ves aquí se realiza directamente con los comandos del bot (prefijo `!`), sin tocar código ni ejecutar scripts. Los comandos viven en `src/commands/messages/admin` y `src/commands/messages/game`, pero no necesitas abrir esos archivos: la idea es que puedas hacerlo todo desde Discord siguiendo estos pasos. +> 💡 **Tip**: las mutaciones con efectos custom (como `fortune_enchant` que aumenta drops) requieren lógica adicional en el código. Consulta con el equipo dev antes de anunciarlas. --- -## Antes de empezar +## Mobs: enemigos y NPCs - Asegúrate de tener el permiso `Manage Guild` o el rol de staff configurado; varios comandos lo revisan con `hasManageGuildOrStaff`. - Siempre usa claves (`key`) en minúsculas y sin espacios. Son únicas por servidor y no se pueden repetir. @@ -199,7 +326,9 @@ El crafteo permite combinar materiales para crear ítems más valiosos. A difere 3. Si los tiene, los descuenta del inventario y entrega el producto inmediatamente. 4. Las estadísticas del jugador se actualizan (`itemsCrafted`). -### Crear nuevas recetas de crafteo +### Crear nuevas recetas de crafteo (¡directo desde Discord!) + +Ya **NO necesitas al equipo dev** para crear recetas. Ahora puedes configurarlas directamente al crear o editar un ítem. #### Paso 1: Crear todos los ítems involucrados @@ -220,7 +349,7 @@ El crafteo permite combinar materiales para crear ítems más valiosos. A difere - Stackable: true,999 - Props: `{"craftingOnly": true}` -2. **Producto final**: +2. **Producto final con receta**: ``` !item-crear iron_sword ``` @@ -237,32 +366,116 @@ El crafteo permite combinar materiales para crear ítems más valiosos. A difere } ``` -#### Paso 2: Enviar receta al equipo dev +#### Paso 2: Configurar la receta (¡NUEVO!) -**Receta: Espada de Hierro** -- **Product**: `iron_sword` -- **Product Quantity**: 1 -- **Ingredientes**: - - `iron_ingot`: 3 - - `wood_plank`: 1 +Antes de guardar el ítem, pulsa el botón **Receta** en el editor. Aparecerá un modal como este: -El equipo dev ejecutará: -```typescript -await prisma.itemRecipe.create({ - data: { - productItemId: ironSwordItem.id, - productQuantity: 1, - ingredients: { - create: [ - { itemId: ironIngotItem.id, quantity: 3 }, - { itemId: woodPlankItem.id, quantity: 1 } - ] - } - } -}); +``` +┌─────────────────────────────────────────┐ +│ 📝 Receta de Crafteo │ +├─────────────────────────────────────────┤ +│ Habilitar receta? (true/false) │ +│ [ true ] │ +├─────────────────────────────────────────┤ +│ Cantidad que produce │ +│ [ 1 ] │ +├─────────────────────────────────────────┤ +│ Ingredientes (itemKey:qty, ...) │ +│ ┌───────────────────────────────────┐ │ +│ │ iron_ingot:3, wood_plank:1 │ │ +│ │ │ │ +│ └───────────────────────────────────┘ │ +│ │ +│ [Enviar] [Cancelar] │ +└─────────────────────────────────────────┘ ``` -#### Paso 3: Probar la receta +**Campos del modal:** + +1. **Habilitar receta?**: escribe `true` para activar, `false` para desactivar +2. **Cantidad que produce**: cuántas unidades del producto se crean (ej. `1` espada, `3` lingotes, `10` flechas) +3. **Ingredientes**: lista separada por comas en formato `itemKey:cantidad` + +El formato de ingredientes es: `itemKey:cantidad, itemKey:cantidad, ...` + +**Ejemplos válidos:** +- `iron_ingot:3, wood_plank:1` → necesita 3 lingotes y 1 tablón +- `leather:8, string:2` → necesita 8 cueros y 2 cuerdas +- `ruby:1, gold_ingot:5, magic_dust:2` → necesita 1 rubí, 5 lingotes de oro y 2 polvos mágicos + +El bot automáticamente: +- ✅ Valida que las claves (`itemKey`) existan en tu servidor +- ✅ Convierte las claves a IDs de base de datos +- ✅ Guarda la receta junto con el ítem +- ❌ Rechaza ingredientes que no existen con mensaje de error claro + +Finalmente pulsa **Guardar** y listo. ¡La receta ya está activa! + +#### 📋 Ejemplo JSON completo de un ítem con receta + +Después de guardar, el ítem quedará estructurado así en la base de datos: + +**EconomyItem (iron_sword)**: +```json +{ + "id": "uuid-123", + "key": "iron_sword", + "guildId": "guild-456", + "name": "Espada de Hierro", + "description": "Espada básica de hierro forjado", + "stackable": false, + "maxInventory": 1, + "props": { + "craftable": {"enabled": true}, + "tool": {"type": "sword", "tier": 2}, + "damage": 15, + "breakable": {"enabled": true, "maxDurability": 200} + }, + "tags": ["weapon", "tier2"], + "itemRecipe": { + "id": "recipe-789", + "productItemId": "uuid-123", + "productQuantity": 1, + "ingredients": [ + { + "id": "ing-001", + "recipeId": "recipe-789", + "itemId": "uuid-iron-ingot", + "quantity": 3 + }, + { + "id": "ing-002", + "recipeId": "recipe-789", + "itemId": "uuid-wood-plank", + "quantity": 1 + } + ] + } +} +``` + +> 💡 **Nota**: No necesitas escribir este JSON manualmente. El editor lo crea automáticamente cuando pulsas "Receta" y guardas. + +#### Paso 3: Editar recetas existentes + +Si ya creaste un ítem y quieres agregar/modificar su receta: + +``` +!item-editar iron_sword +``` + +Pulsa **Receta** y edita: +- Para **agregar** una receta nueva: pon `true` y escribe los ingredientes +- Para **modificar** ingredientes: cambia el texto (ej. `iron_ingot:5, wood_plank:2`) +- Para **deshabilitar** la receta: pon `false` en "Habilitar receta?" +- Para **eliminar** completamente: pon `false` y deja ingredientes vacío + +Cuando guardas, el bot: +- 🔄 Actualiza la receta si cambió +- ➕ Crea la receta si es nueva +- 🗑️ Elimina la receta si la deshabilitaste + +#### Paso 4: Probar la receta 1. Asegúrate de tener los ingredientes en tu inventario. 2. Ejecuta: @@ -273,6 +486,133 @@ await prisma.itemRecipe.create({ - ✅ Si tienes todo: "✨ Crafteaste **Espada de Hierro** x1" - ❌ Si falta algo: "No tienes suficientes ingredientes: necesitas 3 iron_ingot, 1 wood_plank" +### 📦 Ejemplos de Props JSON para diferentes tipos de crafteo + +#### Arma crafteable con durabilidad +```json +{ + "craftable": {"enabled": true}, + "tool": {"type": "sword", "tier": 2}, + "damage": 15, + "breakable": { + "enabled": true, + "maxDurability": 200, + "repairItem": "iron_ingot", + "repairAmount": 20 + } +} +``` +**Receta sugerida**: `iron_ingot:3, wood_plank:1` + +--- + +#### Armadura crafteable con bonificaciones +```json +{ + "craftable": {"enabled": true}, + "wearable": { + "slot": "chest", + "visual": "https://example.com/iron_chestplate.png" + }, + "defense": 12, + "maxHpBonus": 20, + "breakable": { + "enabled": true, + "maxDurability": 300 + } +} +``` +**Receta sugerida**: `iron_ingot:8, leather:2` + +--- + +#### Consumible crafteable (pociones) +```json +{ + "craftable": {"enabled": true}, + "food": { + "healHp": 50, + "healPercent": 0, + "cooldownKey": "potion:health", + "cooldownSeconds": 60 + }, + "stackable": true, + "maxInventory": 10 +} +``` +**Receta sugerida**: `red_herb:2, water_bottle:1, magic_dust:1` + +--- + +#### Material de crafteo (produce múltiples unidades) +```json +{ + "craftable": {"enabled": true}, + "craftingOnly": true, + "description": "Material refinado usado en crafteo avanzado", + "stackable": true, + "maxInventory": 999 +} +``` +**Receta sugerida**: `iron_ore:2, coal:1` → **Produce 3 unidades** (configurar en "Cantidad que produce": `3`) + +--- + +#### Herramienta con efectos especiales +```json +{ + "craftable": {"enabled": true}, + "tool": { + "type": "pickaxe", + "tier": 3, + "efficiency": 1.5, + "fortune": true + }, + "breakable": { + "enabled": true, + "maxDurability": 500, + "unbreaking": 2 + } +} +``` +**Receta sugerida**: `steel_ingot:3, diamond:2, enchanted_core:1` + +--- + +#### Ítem decorativo/coleccionable +```json +{ + "craftable": {"enabled": true}, + "collectible": true, + "rarity": "legendary", + "tradeable": false, + "description": "Trofeo único obtenido al craftear materiales legendarios", + "stackable": false, + "maxInventory": 1 +} +``` +**Receta sugerida**: `mythril_ingot:10, dragon_scale:5, phoenix_feather:3` + +--- + +#### Cofre crafteable con recompensas +```json +{ + "craftable": {"enabled": true}, + "chest": { + "enabled": true, + "consumeOnOpen": true, + "rewards": [ + {"type": "coins", "amount": 1000}, + {"type": "item", "itemKey": "rare_gem", "qty": 2} + ] + } +} +``` +**Receta sugerida**: `wood_plank:8, iron_ingot:2, gold_ingot:1` + +--- + ### Categorías de recetas sugeridas #### 🛠️ Herramientas @@ -398,6 +738,63 @@ Nivel 5 (Legendario) | Cristal de Lava | `lava_crystal` | Área volcánica | Crafteo legendario | | Mythril | `mythril_ingot` | Fundir mythril_ore | Armas/armor tier 4 | +### Verificar y solucionar problemas con recetas + +#### Ver información de una receta + +Para verificar si un ítem tiene receta activa, usa: + +``` +!item-ver iron_sword +``` + +En el editor aparecerá: +- **Receta**: `Habilitada (3 ingredientes → 1 unidades)` ← tiene receta activa +- **Receta**: `Deshabilitada` ← no tiene receta o está desactivada + +#### Errores comunes y soluciones + +**Error**: "No se encontró el ítem `xxx_ingot` en este servidor" +- **Causa**: La clave (`itemKey`) del ingrediente no existe o tiene un typo +- **Solución**: Verifica con `!items-lista` que todos los ingredientes existan. Crea los que falten con `!item-crear` + +**Error**: "No tienes suficientes ingredientes" +- **Causa**: El jugador no tiene todos los materiales en su inventario +- **Solución**: Usa `!inventario` para verificar qué falta. Añade ítems con `!give @usuario itemKey cantidad` + +**Problema**: La receta no se guarda +- **Causa**: El prop `craftable.enabled` está en `false` o falta +- **Solución**: En Props (JSON) asegúrate de tener: `"craftable": {"enabled": true}` + +**Problema**: La receta desapareció después de editar el ítem +- **Causa**: No marcaste "Habilitar receta?" como `true` al editar +- **Solución**: Vuelve a `!item-editar`, pulsa **Receta**, pon `true` y reingresa los ingredientes + +#### Workflow de debug para recetas + +1. **Verificar que el producto existe**: + ``` + !item-ver iron_sword + ``` + Debe aparecer en la lista y tener `craftable.enabled: true` en props. + +2. **Verificar que todos los ingredientes existen**: + ``` + !items-lista + ``` + Busca `iron_ingot` y `wood_plank` en la lista. + +3. **Probar con admin**: + - Añádete los ingredientes: `!give @tuUsuario iron_ingot 10` + - Intenta craftear: `!craftear iron_sword` + - Si funciona: la receta está bien configurada + - Si falla: revisa los errores del bot + +4. **Verificar la base de datos (solo si todo lo anterior falló)**: + - Pide al equipo dev que revise la tabla `ItemRecipe` + - Debe haber un registro con `productItemId` apuntando al ítem correcto + - Los `RecipeIngredient` deben tener `itemId` y `quantity` correctos + ### Cadenas de crafteo complejas **Ejemplo: Espada Legendaria** @@ -438,6 +835,45 @@ Para materiales que **solo** sirven para craftear y no tienen uso directo: Esto ayuda a los jugadores a entender que deben combinarlo con otros ítems para obtener valor. +### 💡 Tips y mejores prácticas para recetas + +#### Organización de recetas + +- **Nombra consistentemente**: Usa sufijos como `_ingot`, `_ore`, `_plank` para que sean fáciles de identificar +- **Agrupa por tier**: Crea materiales tier 1, tier 2, tier 3... para facilitar progresión +- **Documenta ingredientes raros**: Si una receta usa ítems de eventos o bosses, menciónalo en la descripción del producto + +#### Balance de economía + +- **Recetas básicas**: 2-3 ingredientes, cantidades bajas (< 5 unidades) +- **Recetas intermedias**: 3-5 ingredientes, algunas de tier anterior +- **Recetas avanzadas**: 5+ ingredientes, incluyen materiales raros y crafteos previos +- **Recetas legendarias**: Cadenas complejas que requieren múltiples pasos de fundición y crafteo + +#### Productividad del staff + +- **Crea plantillas**: Guarda en un documento los props JSON comunes para copiar/pegar rápidamente +- **Batch creation**: Crea todos los ingredientes primero, luego todos los productos +- **Usa el mismo editor**: No cierres el editor entre ítems similares, solo cambia la key y ajusta valores +- **Prueba inmediatamente**: Después de crear una receta, añádete los ingredientes y prueba `!craftear` para validar + +#### Errores a evitar + +- ❌ **No crear los ingredientes primero**: Si creas el producto con receta pero los ingredientes no existen, la receta fallará +- ❌ **Typos en itemKeys**: `iron_ingott` vs `iron_ingot` - el bot no encontrará el ítem +- ❌ **Olvidar craftable.enabled**: Si el prop no está en `true`, la receta no funcionará aunque esté guardada +- ❌ **Cantidades desbalanceadas**: 100 unidades de un material común no debe producir 1 ítem legendario + +#### Ejemplos de recetas balanceadas + +| Tier | Ingredientes Típicos | Output Typical | Ejemplo | +| --- | --- | --- | --- | +| 1 (Común) | 2-3 materiales básicos x2-5 | 1-5 unidades | 3 wood + 2 stone → 1 basic_axe | +| 2 (Poco común) | 3-4 materiales, algunos refinados x3-10 | 1-3 unidades | 5 iron_ingot + 2 leather → 1 iron_sword | +| 3 (Raro) | 4-5 materiales, crafteos tier 2 x5-15 | 1-2 unidades | 8 steel_ingot + 3 ruby + 1 iron_sword → 1 steel_sword | +| 4 (Épico) | 5-7 materiales, incluye raros x10-30 | 1 unidad | 10 mythril + 5 magic_dust + 3 dragon_scale → 1 mythril_sword | +| 5 (Legendario) | 6+ materiales, cadenas complejas x20+ | 1 unidad | 15 divine_ore + 10 phoenix_feather + 1 mythril_sword → 1 godslayer | + --- ## Mutaciones y encantamientos diff --git a/src/commands/messages/game/itemCreate.ts b/src/commands/messages/game/itemCreate.ts index 4c1ba41..43aaf61 100644 --- a/src/commands/messages/game/itemCreate.ts +++ b/src/commands/messages/game/itemCreate.ts @@ -15,6 +15,12 @@ interface ItemEditorState { maxPerInventory?: number | null; tags: string[]; props?: any; + // Nueva propiedad para receta de crafteo + recipe?: { + enabled: boolean; + ingredients: Array<{ itemKey: string; quantity: number }>; + productQuantity: number; + }; } export const command: CommandMessage = { @@ -89,6 +95,11 @@ export const command: CommandMessage = { stackable: true, maxPerInventory: null, props: {}, + recipe: { + enabled: false, + ingredients: [], + productQuantity: 1 + } }; const buildEditorDisplay = () => { @@ -103,6 +114,9 @@ export const command: CommandMessage = { const tagsInfo = `**Tags:** ${state.tags.length > 0 ? state.tags.join(', ') : '*Ninguno*'}`; const propsJson = JSON.stringify(state.props ?? {}, null, 2); + const recipeInfo = state.recipe?.enabled + ? `**Receta:** Habilitada (${state.recipe.ingredients.length} ingredientes → ${state.recipe.productQuantity} unidades)` + : `**Receta:** Deshabilitada`; return { type: 17, @@ -123,6 +137,11 @@ export const command: CommandMessage = { content: tagsInfo }, { type: 14, divider: true }, + { + type: 10, + content: recipeInfo + }, + { type: 14, divider: true }, { type: 10, content: `**Props (JSON):**\n\`\`\`json\n${propsJson}\n\`\`\`` @@ -138,7 +157,13 @@ export const command: CommandMessage = { 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: 'Receta', custom_id: 'it_recipe' }, { type: 2, style: ButtonStyle.Secondary, label: 'Props (JSON)', custom_id: 'it_props' }, + ] + }, + { + type: 1, + components: [ { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'it_save' }, { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'it_cancel' }, ] @@ -182,6 +207,10 @@ export const command: CommandMessage = { await showTagsModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents); return; } + if (i.customId === 'it_recipe') { + await showRecipeModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents, client); + return; + } if (i.customId === 'it_props') { await showPropsModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents); return; @@ -192,8 +221,9 @@ export const command: CommandMessage = { await i.reply({ content: '❌ Falta el nombre del item (configura en Base).', flags: MessageFlags.Ephemeral }); return; } - // Guardar - await client.prisma.economyItem.create({ + + // Guardar item + const createdItem = await client.prisma.economyItem.create({ data: { guildId, key: state.key, @@ -207,6 +237,44 @@ export const command: CommandMessage = { props: state.props ?? {}, }, }); + + // Guardar receta si está habilitada + if (state.recipe?.enabled && state.recipe.ingredients.length > 0) { + try { + // Resolver itemIds de los ingredientes + const ingredientsData: Array<{ itemId: string; quantity: number }> = []; + for (const ing of state.recipe.ingredients) { + const item = await client.prisma.economyItem.findFirst({ + where: { + key: ing.itemKey, + OR: [{ guildId }, { guildId: null }] + }, + orderBy: [{ guildId: 'desc' }] + }); + if (!item) { + throw new Error(`Ingrediente no encontrado: ${ing.itemKey}`); + } + ingredientsData.push({ + itemId: item.id, + quantity: ing.quantity + }); + } + + // Crear la receta + await client.prisma.itemRecipe.create({ + data: { + productItemId: createdItem.id, + productQuantity: state.recipe.productQuantity, + ingredients: { + create: ingredientsData + } + } + }); + } catch (err: any) { + logger.warn({ err }, 'Error creando receta para item'); + await i.followUp({ content: `⚠️ Item creado pero falló la receta: ${err.message}`, flags: MessageFlags.Ephemeral }); + } + } await i.reply({ content: '✅ Item guardado!', flags: MessageFlags.Ephemeral }); await editorMsg.edit({ content: null, @@ -364,3 +432,84 @@ async function showPropsModal(i: ButtonInteraction, state: ItemEditorState, edit } } catch {} } + +async function showRecipeModal(i: ButtonInteraction, state: ItemEditorState, editorMsg: any, buildComponents: () => any[], client: Amayo) { + const currentRecipe = state.recipe || { enabled: false, ingredients: [], productQuantity: 1 }; + const ingredientsStr = currentRecipe.ingredients.map(ing => `${ing.itemKey}:${ing.quantity}`).join(', '); + + const modal = { + title: 'Receta de Crafteo', + customId: 'it_recipe_modal', + components: [ + { + type: ComponentType.Label, + label: 'Habilitar receta? (true/false)', + component: { + type: ComponentType.TextInput, + customId: 'enabled', + style: TextInputStyle.Short, + required: false, + value: String(currentRecipe.enabled), + placeholder: 'true o false' + } + }, + { + type: ComponentType.Label, + label: 'Cantidad que produce', + component: { + type: ComponentType.TextInput, + customId: 'quantity', + style: TextInputStyle.Short, + required: false, + value: String(currentRecipe.productQuantity), + placeholder: '1' + } + }, + { + type: ComponentType.Label, + label: 'Ingredientes (itemKey:qty, ...)', + component: { + type: ComponentType.TextInput, + customId: 'ingredients', + style: TextInputStyle.Paragraph, + required: false, + value: ingredientsStr, + placeholder: 'iron_ingot:3, wood_plank:1' + } + }, + ], + } as const; + + await i.showModal(modal); + try { + const sub = await i.awaitModalSubmit({ time: 300_000 }); + const enabledStr = sub.components.getTextInputValue('enabled').trim().toLowerCase(); + const quantityStr = sub.components.getTextInputValue('quantity').trim(); + const ingredientsInput = sub.components.getTextInputValue('ingredients').trim(); + + const enabled = enabledStr === 'true'; + const productQuantity = parseInt(quantityStr, 10) || 1; + + // Parsear ingredientes + const ingredients: Array<{ itemKey: string; quantity: number }> = []; + if (ingredientsInput && enabled) { + const parts = ingredientsInput.split(',').map(p => p.trim()).filter(Boolean); + for (const part of parts) { + const [itemKey, qtyStr] = part.split(':').map(s => s.trim()); + const qty = parseInt(qtyStr, 10); + if (itemKey && qty > 0) { + ingredients.push({ itemKey, quantity: qty }); + } + } + } + + state.recipe = { enabled, ingredients, productQuantity }; + + await sub.deferUpdate(); + await editorMsg.edit({ + content: null, + flags: 32768, + components: buildComponents() + }); + } catch {} +} diff --git a/src/commands/messages/game/itemEdit.ts b/src/commands/messages/game/itemEdit.ts index 6b8217a..33de07f 100644 --- a/src/commands/messages/game/itemEdit.ts +++ b/src/commands/messages/game/itemEdit.ts @@ -16,6 +16,12 @@ interface ItemEditorState { maxPerInventory?: number | null; tags: string[]; props?: any; + // Nueva propiedad para receta de crafteo + recipe?: { + enabled: boolean; + ingredients: Array<{ itemKey: string; quantity: number }>; + productQuantity: number; + }; } export const command: CommandMessage = { @@ -74,6 +80,20 @@ export const command: CommandMessage = { const existing = selection.entry; + // Cargar receta si existe + let existingRecipe: { + ingredients: Array<{ item: { key: string }; quantity: number }>; + productQuantity: number; + } | null = null; + try { + existingRecipe = await client.prisma.itemRecipe.findUnique({ + where: { productItemId: existing.id }, + include: { ingredients: { include: { item: true } } } + }); + } catch (e) { + logger.warn({ err: e }, 'Error cargando receta existente'); + } + const state: ItemEditorState = { key: existing.key, name: existing.name, @@ -84,6 +104,18 @@ export const command: CommandMessage = { maxPerInventory: existing.maxPerInventory ?? null, tags: Array.isArray(existing.tags) ? existing.tags : [], props: existing.props || {}, + recipe: existingRecipe ? { + enabled: true, + ingredients: existingRecipe.ingredients.map(ing => ({ + itemKey: ing.item.key, + quantity: ing.quantity + })), + productQuantity: existingRecipe.productQuantity + } : { + enabled: false, + ingredients: [], + productQuantity: 1 + } }; const buildEditorDisplay = () => { @@ -98,6 +130,9 @@ export const command: CommandMessage = { const tagsInfo = `**Tags:** ${state.tags.length > 0 ? state.tags.join(', ') : '*Ninguno*'}`; const propsJson = JSON.stringify(state.props ?? {}, null, 2); + const recipeInfo = state.recipe?.enabled + ? `**Receta:** Habilitada (${state.recipe.ingredients.length} ingredientes → ${state.recipe.productQuantity} unidades)` + : `**Receta:** Deshabilitada`; return { type: 17, @@ -118,6 +153,11 @@ export const command: CommandMessage = { content: tagsInfo }, { type: 14, divider: true }, + { + type: 10, + content: recipeInfo + }, + { type: 14, divider: true }, { type: 10, content: `**Props (JSON):**\n\`\`\`json\n${propsJson}\n\`\`\`` @@ -133,7 +173,13 @@ export const command: CommandMessage = { 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: 'Receta', custom_id: 'it_recipe' }, { type: 2, style: ButtonStyle.Secondary, label: 'Props (JSON)', custom_id: 'it_props' }, + ] + }, + { + type: 1, + components: [ { type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'it_save' }, { type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'it_cancel' }, ] @@ -177,6 +223,10 @@ export const command: CommandMessage = { await showTagsModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents); return; } + if (i.customId === 'it_recipe') { + await showRecipeModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents, client, guildId, existing.id); + return; + } if (i.customId === 'it_props') { await showPropsModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents); return; @@ -201,6 +251,76 @@ export const command: CommandMessage = { props: state.props ?? {}, }, }); + + // Actualizar/crear/eliminar receta + try { + const existingRecipeCheck = await client.prisma.itemRecipe.findUnique({ + where: { productItemId: existing.id }, + include: { ingredients: true } + }); + + if (state.recipe?.enabled && state.recipe.ingredients.length > 0) { + // Resolver itemIds de los ingredientes + const ingredientsData: Array<{ itemId: string; quantity: number }> = []; + for (const ing of state.recipe.ingredients) { + const item = await client.prisma.economyItem.findFirst({ + where: { + key: ing.itemKey, + OR: [{ guildId }, { guildId: null }] + }, + orderBy: [{ guildId: 'desc' }] + }); + if (!item) { + throw new Error(`Ingrediente no encontrado: ${ing.itemKey}`); + } + ingredientsData.push({ + itemId: item.id, + quantity: ing.quantity + }); + } + + if (existingRecipeCheck) { + // Actualizar receta existente + // Primero eliminar ingredientes viejos + await client.prisma.recipeIngredient.deleteMany({ + where: { recipeId: existingRecipeCheck.id } + }); + // Luego actualizar la receta con los nuevos ingredientes + await client.prisma.itemRecipe.update({ + where: { id: existingRecipeCheck.id }, + data: { + productQuantity: state.recipe.productQuantity, + ingredients: { + create: ingredientsData + } + } + }); + } else { + // Crear nueva receta + await client.prisma.itemRecipe.create({ + data: { + productItemId: existing.id, + productQuantity: state.recipe.productQuantity, + ingredients: { + create: ingredientsData + } + } + }); + } + } else if (existingRecipeCheck && !state.recipe?.enabled) { + // Eliminar receta si está deshabilitada + await client.prisma.recipeIngredient.deleteMany({ + where: { recipeId: existingRecipeCheck.id } + }); + await client.prisma.itemRecipe.delete({ + where: { id: existingRecipeCheck.id } + }); + } + } catch (err: any) { + logger.warn({ err }, 'Error actualizando receta'); + await i.followUp({ content: `⚠️ Item actualizado pero falló la receta: ${err.message}`, flags: MessageFlags.Ephemeral }); + } + await i.reply({ content: '✅ Item actualizado!', flags: MessageFlags.Ephemeral }); await editorMsg.edit({ content: null, @@ -358,3 +478,84 @@ async function showPropsModal(i: ButtonInteraction, state: ItemEditorState, edit } } catch {} } + +async function showRecipeModal(i: ButtonInteraction, state: ItemEditorState, editorMsg: Message, buildComponents: () => any[], client: Amayo, guildId: string, itemId: string) { + const currentRecipe = state.recipe || { enabled: false, ingredients: [], productQuantity: 1 }; + const ingredientsStr = currentRecipe.ingredients.map(ing => `${ing.itemKey}:${ing.quantity}`).join(', '); + + const modal = { + title: 'Receta de Crafteo', + customId: 'it_recipe_modal', + components: [ + { + type: ComponentType.Label, + label: 'Habilitar receta? (true/false)', + component: { + type: ComponentType.TextInput, + customId: 'enabled', + style: TextInputStyle.Short, + required: false, + value: String(currentRecipe.enabled), + placeholder: 'true o false' + } + }, + { + type: ComponentType.Label, + label: 'Cantidad que produce', + component: { + type: ComponentType.TextInput, + customId: 'quantity', + style: TextInputStyle.Short, + required: false, + value: String(currentRecipe.productQuantity), + placeholder: '1' + } + }, + { + type: ComponentType.Label, + label: 'Ingredientes (itemKey:qty, ...)', + component: { + type: ComponentType.TextInput, + customId: 'ingredients', + style: TextInputStyle.Paragraph, + required: false, + value: ingredientsStr, + placeholder: 'iron_ingot:3, wood_plank:1' + } + }, + ], + } as const; + + await i.showModal(modal); + try { + const sub = await i.awaitModalSubmit({ time: 300_000 }); + const enabledStr = sub.components.getTextInputValue('enabled').trim().toLowerCase(); + const quantityStr = sub.components.getTextInputValue('quantity').trim(); + const ingredientsInput = sub.components.getTextInputValue('ingredients').trim(); + + const enabled = enabledStr === 'true'; + const productQuantity = parseInt(quantityStr, 10) || 1; + + // Parsear ingredientes + const ingredients: Array<{ itemKey: string; quantity: number }> = []; + if (ingredientsInput && enabled) { + const parts = ingredientsInput.split(',').map(p => p.trim()).filter(Boolean); + for (const part of parts) { + const [itemKey, qtyStr] = part.split(':').map(s => s.trim()); + const qty = parseInt(qtyStr, 10); + if (itemKey && qty > 0) { + ingredients.push({ itemKey, quantity: qty }); + } + } + } + + state.recipe = { enabled, ingredients, productQuantity }; + + await sub.deferUpdate(); + await editorMsg.edit({ + content: null, + flags: 32768, + components: buildComponents() + }); + } catch {} +}