feat: add crafting recipe functionality to item creation and editing commands
This commit is contained in:
484
Mas Ejemplos.md
484
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 <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.
|
||||
|
||||
@@ -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] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Paso 3: Probar la receta
|
||||
> 💡 **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
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user