feat: add crafting recipe functionality to item creation and editing commands
This commit is contained in:
490
Mas Ejemplos.md
490
Mas Ejemplos.md
@@ -1,6 +1,133 @@
|
|||||||
# Guía rápida para el staff: crear y ajustar contenido desde Discord
|
# 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 <key>`
|
||||||
|
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 <key>` borra la versión local (solicita confirmación).
|
||||||
|
- `!items-lista` y `!item-ver <key>` sirven para revisar lo que ya existe.
|
||||||
|
|
||||||
|
> 💡 Tip: si solo quieres revisar las propiedades de un ítem, usa `!item-ver <key>`; 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 <key>`.
|
||||||
|
|
||||||
|
- **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 <productKey>`.
|
||||||
|
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.
|
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 |
|
| `vampire_core` | Vampirismo | +10 damage, lifesteal | Armas |
|
||||||
| `thorns_enchant` | Espinas | refleja daño | Armaduras |
|
| `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`.
|
- 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.
|
- 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.
|
3. Si los tiene, los descuenta del inventario y entrega el producto inmediatamente.
|
||||||
4. Las estadísticas del jugador se actualizan (`itemsCrafted`).
|
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
|
#### 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
|
- Stackable: true,999
|
||||||
- Props: `{"craftingOnly": true}`
|
- Props: `{"craftingOnly": true}`
|
||||||
|
|
||||||
2. **Producto final**:
|
2. **Producto final con receta**:
|
||||||
```
|
```
|
||||||
!item-crear iron_sword
|
!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**
|
Antes de guardar el ítem, pulsa el botón **Receta** en el editor. Aparecerá un modal como este:
|
||||||
- **Product**: `iron_sword`
|
|
||||||
- **Product Quantity**: 1
|
|
||||||
- **Ingredientes**:
|
|
||||||
- `iron_ingot`: 3
|
|
||||||
- `wood_plank`: 1
|
|
||||||
|
|
||||||
El equipo dev ejecutará:
|
```
|
||||||
```typescript
|
┌─────────────────────────────────────────┐
|
||||||
await prisma.itemRecipe.create({
|
│ 📝 Receta de Crafteo │
|
||||||
data: {
|
├─────────────────────────────────────────┤
|
||||||
productItemId: ironSwordItem.id,
|
│ Habilitar receta? (true/false) │
|
||||||
productQuantity: 1,
|
│ [ true ] │
|
||||||
ingredients: {
|
├─────────────────────────────────────────┤
|
||||||
create: [
|
│ Cantidad que produce │
|
||||||
{ itemId: ironIngotItem.id, quantity: 3 },
|
│ [ 1 ] │
|
||||||
{ itemId: woodPlankItem.id, quantity: 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.
|
1. Asegúrate de tener los ingredientes en tu inventario.
|
||||||
2. Ejecuta:
|
2. Ejecuta:
|
||||||
@@ -273,6 +486,133 @@ await prisma.itemRecipe.create({
|
|||||||
- ✅ Si tienes todo: "✨ Crafteaste **Espada de Hierro** x1"
|
- ✅ Si tienes todo: "✨ Crafteaste **Espada de Hierro** x1"
|
||||||
- ❌ Si falta algo: "No tienes suficientes ingredientes: necesitas 3 iron_ingot, 1 wood_plank"
|
- ❌ 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
|
### Categorías de recetas sugeridas
|
||||||
|
|
||||||
#### 🛠️ Herramientas
|
#### 🛠️ Herramientas
|
||||||
@@ -398,6 +738,63 @@ Nivel 5 (Legendario)
|
|||||||
| Cristal de Lava | `lava_crystal` | Área volcánica | Crafteo legendario |
|
| Cristal de Lava | `lava_crystal` | Área volcánica | Crafteo legendario |
|
||||||
| Mythril | `mythril_ingot` | Fundir mythril_ore | Armas/armor tier 4 |
|
| 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
|
### Cadenas de crafteo complejas
|
||||||
|
|
||||||
**Ejemplo: Espada Legendaria**
|
**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.
|
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
|
## Mutaciones y encantamientos
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ interface ItemEditorState {
|
|||||||
maxPerInventory?: number | null;
|
maxPerInventory?: number | null;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
props?: any;
|
props?: any;
|
||||||
|
// Nueva propiedad para receta de crafteo
|
||||||
|
recipe?: {
|
||||||
|
enabled: boolean;
|
||||||
|
ingredients: Array<{ itemKey: string; quantity: number }>;
|
||||||
|
productQuantity: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
@@ -89,6 +95,11 @@ export const command: CommandMessage = {
|
|||||||
stackable: true,
|
stackable: true,
|
||||||
maxPerInventory: null,
|
maxPerInventory: null,
|
||||||
props: {},
|
props: {},
|
||||||
|
recipe: {
|
||||||
|
enabled: false,
|
||||||
|
ingredients: [],
|
||||||
|
productQuantity: 1
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildEditorDisplay = () => {
|
const buildEditorDisplay = () => {
|
||||||
@@ -103,6 +114,9 @@ export const command: CommandMessage = {
|
|||||||
|
|
||||||
const tagsInfo = `**Tags:** ${state.tags.length > 0 ? state.tags.join(', ') : '*Ninguno*'}`;
|
const tagsInfo = `**Tags:** ${state.tags.length > 0 ? state.tags.join(', ') : '*Ninguno*'}`;
|
||||||
const propsJson = JSON.stringify(state.props ?? {}, null, 2);
|
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 {
|
return {
|
||||||
type: 17,
|
type: 17,
|
||||||
@@ -123,6 +137,11 @@ export const command: CommandMessage = {
|
|||||||
content: tagsInfo
|
content: tagsInfo
|
||||||
},
|
},
|
||||||
{ type: 14, divider: true },
|
{ type: 14, divider: true },
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: recipeInfo
|
||||||
|
},
|
||||||
|
{ type: 14, divider: true },
|
||||||
{
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: `**Props (JSON):**\n\`\`\`json\n${propsJson}\n\`\`\``
|
content: `**Props (JSON):**\n\`\`\`json\n${propsJson}\n\`\`\``
|
||||||
@@ -138,7 +157,13 @@ export const command: CommandMessage = {
|
|||||||
components: [
|
components: [
|
||||||
{ type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'it_base' },
|
{ 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: '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: 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.Success, label: 'Guardar', custom_id: 'it_save' },
|
||||||
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'it_cancel' },
|
{ 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);
|
await showTagsModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (i.customId === 'it_recipe') {
|
||||||
|
await showRecipeModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents, client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (i.customId === 'it_props') {
|
if (i.customId === 'it_props') {
|
||||||
await showPropsModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents);
|
await showPropsModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents);
|
||||||
return;
|
return;
|
||||||
@@ -192,8 +221,9 @@ export const command: CommandMessage = {
|
|||||||
await i.reply({ content: '❌ Falta el nombre del item (configura en Base).', flags: MessageFlags.Ephemeral });
|
await i.reply({ content: '❌ Falta el nombre del item (configura en Base).', flags: MessageFlags.Ephemeral });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Guardar
|
|
||||||
await client.prisma.economyItem.create({
|
// Guardar item
|
||||||
|
const createdItem = await client.prisma.economyItem.create({
|
||||||
data: {
|
data: {
|
||||||
guildId,
|
guildId,
|
||||||
key: state.key,
|
key: state.key,
|
||||||
@@ -207,6 +237,44 @@ export const command: CommandMessage = {
|
|||||||
props: state.props ?? {},
|
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 i.reply({ content: '✅ Item guardado!', flags: MessageFlags.Ephemeral });
|
||||||
await editorMsg.edit({
|
await editorMsg.edit({
|
||||||
content: null,
|
content: null,
|
||||||
@@ -364,3 +432,84 @@ async function showPropsModal(i: ButtonInteraction, state: ItemEditorState, edit
|
|||||||
}
|
}
|
||||||
} catch {}
|
} 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 {}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ interface ItemEditorState {
|
|||||||
maxPerInventory?: number | null;
|
maxPerInventory?: number | null;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
props?: any;
|
props?: any;
|
||||||
|
// Nueva propiedad para receta de crafteo
|
||||||
|
recipe?: {
|
||||||
|
enabled: boolean;
|
||||||
|
ingredients: Array<{ itemKey: string; quantity: number }>;
|
||||||
|
productQuantity: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
@@ -74,6 +80,20 @@ export const command: CommandMessage = {
|
|||||||
|
|
||||||
const existing = selection.entry;
|
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 = {
|
const state: ItemEditorState = {
|
||||||
key: existing.key,
|
key: existing.key,
|
||||||
name: existing.name,
|
name: existing.name,
|
||||||
@@ -84,6 +104,18 @@ export const command: CommandMessage = {
|
|||||||
maxPerInventory: existing.maxPerInventory ?? null,
|
maxPerInventory: existing.maxPerInventory ?? null,
|
||||||
tags: Array.isArray(existing.tags) ? existing.tags : [],
|
tags: Array.isArray(existing.tags) ? existing.tags : [],
|
||||||
props: existing.props || {},
|
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 = () => {
|
const buildEditorDisplay = () => {
|
||||||
@@ -98,6 +130,9 @@ export const command: CommandMessage = {
|
|||||||
|
|
||||||
const tagsInfo = `**Tags:** ${state.tags.length > 0 ? state.tags.join(', ') : '*Ninguno*'}`;
|
const tagsInfo = `**Tags:** ${state.tags.length > 0 ? state.tags.join(', ') : '*Ninguno*'}`;
|
||||||
const propsJson = JSON.stringify(state.props ?? {}, null, 2);
|
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 {
|
return {
|
||||||
type: 17,
|
type: 17,
|
||||||
@@ -118,6 +153,11 @@ export const command: CommandMessage = {
|
|||||||
content: tagsInfo
|
content: tagsInfo
|
||||||
},
|
},
|
||||||
{ type: 14, divider: true },
|
{ type: 14, divider: true },
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: recipeInfo
|
||||||
|
},
|
||||||
|
{ type: 14, divider: true },
|
||||||
{
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: `**Props (JSON):**\n\`\`\`json\n${propsJson}\n\`\`\``
|
content: `**Props (JSON):**\n\`\`\`json\n${propsJson}\n\`\`\``
|
||||||
@@ -133,7 +173,13 @@ export const command: CommandMessage = {
|
|||||||
components: [
|
components: [
|
||||||
{ type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'it_base' },
|
{ 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: '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: 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.Success, label: 'Guardar', custom_id: 'it_save' },
|
||||||
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'it_cancel' },
|
{ 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);
|
await showTagsModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (i.customId === 'it_recipe') {
|
||||||
|
await showRecipeModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents, client, guildId, existing.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (i.customId === 'it_props') {
|
if (i.customId === 'it_props') {
|
||||||
await showPropsModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents);
|
await showPropsModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents);
|
||||||
return;
|
return;
|
||||||
@@ -201,6 +251,76 @@ export const command: CommandMessage = {
|
|||||||
props: state.props ?? {},
|
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 i.reply({ content: '✅ Item actualizado!', flags: MessageFlags.Ephemeral });
|
||||||
await editorMsg.edit({
|
await editorMsg.edit({
|
||||||
content: null,
|
content: null,
|
||||||
@@ -358,3 +478,84 @@ async function showPropsModal(i: ButtonInteraction, state: ItemEditorState, edit
|
|||||||
}
|
}
|
||||||
} catch {}
|
} 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 {}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user