feat(minigames): implement dual durability system for tools and weapons
- Introduced separation between `tool` (for gathering) and `weaponTool` (for combat) in minigames. - Adjusted durability reduction logic to apply a 50% reduction for weapons used in combat to prevent instant breakage. - Updated `RunResult` type to include `weaponTool` information. - Enhanced `runMinigame` logic to handle weapon degradation during combat scenarios. - Updated user commands to reflect both tool and weapon durability in outputs. - Modified scheduled mob attacks to respect the new durability system. - Added comprehensive documentation on the new dual durability feature and its implications for gameplay balance.
This commit is contained in:
220
README/DOBLE_DURABILIDAD_MINIJUEGOS.md
Normal file
220
README/DOBLE_DURABILIDAD_MINIJUEGOS.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# 🔧⚔️ Doble Degradación de Herramientas en Minijuegos
|
||||
|
||||
**Contexto**: Antes existía confusión: al minar, se mostraba que sólo se usaba la espada, cuando el jugador esperaba ver reflejado el pico usado + la espada degradándose por defenderse.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Cambios Implementados
|
||||
|
||||
### 1. Separación de Herramientas (tool vs weaponTool)
|
||||
- **`tool`**: Herramienta requerida para la actividad (pico para minar, caña para pescar, espada para pelear).
|
||||
- **`weaponTool`**: Arma equipada que se degrada en **combate** (si hubo mobs y el jugador tenía espada equipada).
|
||||
|
||||
**Beneficio**: Ahora minar usa el pico para recolectar minerales **y** la espada equipada para defenderse de mobs, cada una con su propia degradación.
|
||||
|
||||
---
|
||||
|
||||
### 2. Balanceo de Durabilidad en Combate (50%)
|
||||
|
||||
**Problema original**: Las armas caras se rompían al instante tras combate (desgaste completo configurado en `durabilityPerUse`).
|
||||
|
||||
**Solución**:
|
||||
- `reduceToolDurability()` ahora acepta parámetro `usage`:
|
||||
- `"gather"` (default): desgaste completo (actividades de recolección/minería).
|
||||
- `"combat"`: desgaste **reducido al 50%** (arma usada en combate).
|
||||
|
||||
**Implementación**:
|
||||
```typescript
|
||||
async function reduceToolDurability(
|
||||
userId: string,
|
||||
guildId: string,
|
||||
toolKey: string,
|
||||
usage: "gather" | "combat" = "gather"
|
||||
) {
|
||||
let perUse = Math.max(1, breakable.durabilityPerUse ?? 1);
|
||||
if (usage === "combat") {
|
||||
perUse = Math.max(1, Math.ceil(perUse * 0.5)); // Reduce a la mitad, mínimo 1
|
||||
}
|
||||
// ... resto de lógica
|
||||
}
|
||||
```
|
||||
|
||||
**Resultado**: Armas ahora duran el doble en combate, mejorando economía sin eliminar costo operativo.
|
||||
|
||||
---
|
||||
|
||||
### 3. Extensión del Tipo `RunResult`
|
||||
|
||||
Añadido campo opcional `weaponTool` al resultado de minijuegos:
|
||||
|
||||
```typescript
|
||||
export type RunResult = {
|
||||
// ... campos existentes (tool, combat, rewards, etc.)
|
||||
weaponTool?: {
|
||||
key?: string;
|
||||
durabilityDelta?: number;
|
||||
broken?: boolean;
|
||||
remaining?: number;
|
||||
max?: number;
|
||||
brokenInstance?: boolean;
|
||||
instancesRemaining?: number;
|
||||
toolSource?: "equipped";
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Lógica de Degradación en `runMinigame`
|
||||
|
||||
Tras ejecutar combate, si hay mobs y el jugador tiene arma equipada:
|
||||
1. Obtener `weapon` del slot equipment.
|
||||
2. Validar que sea tipo `sword` y **no sea la misma herramienta principal** (evitar doble degradación en pelear).
|
||||
3. Degradarla con `usage: "combat"`.
|
||||
4. Adjuntar info a `weaponTool` en el resultado.
|
||||
|
||||
**Código clave** (en `service.ts`):
|
||||
```typescript
|
||||
if (combatSummary && combatSummary.mobs.length > 0) {
|
||||
const { weapon } = await getEquipment(userId, guildId);
|
||||
if (weapon && weaponProps?.tool?.type === "sword") {
|
||||
const alreadyMain = toolInfo?.key === weapon.key;
|
||||
if (!alreadyMain) {
|
||||
const wt = await reduceToolDurability(userId, guildId, weapon.key, "combat");
|
||||
weaponToolInfo = { ...wt, toolSource: "equipped" };
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Actualización de Comandos (UX)
|
||||
|
||||
**Antes**:
|
||||
```
|
||||
Herramienta: Espada de Hierro (50/150) [⚔️ Equipado]
|
||||
```
|
||||
(El usuario pensaba que se estaba usando solo la espada para minar.)
|
||||
|
||||
**Ahora** (comando `!mina` o `!pescar`):
|
||||
```
|
||||
Pico: Pico Básico (95/100) [🔧 Auto]
|
||||
Arma (defensa): Espada de Hierro (50/150) [⚔️ Equipado]
|
||||
```
|
||||
|
||||
**Comando `!pelear`** (sin cambio visual, pues la espada es la herramienta principal):
|
||||
```
|
||||
Arma: Espada de Hierro (50/150) [⚔️ Equipado]
|
||||
```
|
||||
|
||||
**Implementación**:
|
||||
- En `mina.ts`, `pescar.ts`, `pelear.ts` ahora se lee `result.weaponTool` adicional.
|
||||
- Se construye `weaponInfo` con `formatToolLabel` y se incluye en el bloque de visualización.
|
||||
|
||||
---
|
||||
|
||||
### 6. Ataques Programados (ScheduledMobAttack)
|
||||
|
||||
Actualizado `attacksWorker.ts` para degradar arma equipada con `usage: "combat"` al recibir ataque de mobs.
|
||||
|
||||
**Cambio**:
|
||||
```typescript
|
||||
await reduceToolDurability(job.userId, job.guildId, full.key, "combat");
|
||||
```
|
||||
|
||||
Asegura que ataques programados en background también respeten el balance del 50%.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Resultados
|
||||
|
||||
1. **Claridad**: Jugadores ven explícitamente qué herramienta se usó para recolectar y cuál para combate.
|
||||
2. **Balance económico**: Armas duran el doble en combate, reduciendo costo operativo sin eliminar totalmente el desgaste.
|
||||
3. **Consistencia**: El mismo sistema de doble degradación aplica para ataques programados, minijuegos activos y combate.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Ejemplos de Uso
|
||||
|
||||
### Minar con Pico y Espada Equipada
|
||||
```
|
||||
!mina 2
|
||||
|
||||
Área: mine.cavern • Nivel: 2
|
||||
Pico: Pico Básico (90/100) [-5 usos] [🔧 Auto]
|
||||
Arma (defensa): Espada de Hierro (149/150) [-1 uso] [⚔️ Equipado]
|
||||
|
||||
Recompensas:
|
||||
• 🪙 +50
|
||||
• Mineral de Hierro x3
|
||||
|
||||
Mobs:
|
||||
• slime
|
||||
• goblin
|
||||
|
||||
Combate: ⚔️ 2 mobs → 2 derrotados | 💀 Daño infligido: 45 | 🩹 Daño recibido: 8
|
||||
HP: ❤️❤️❤️❤️🤍 (85/100)
|
||||
```
|
||||
|
||||
### Pescar con Caña y Arma
|
||||
```
|
||||
!pescar 1
|
||||
|
||||
Área: lagoon.shore • Nivel: 1
|
||||
Caña: Caña Básica (77/80) [-3 usos] [🎣 Auto]
|
||||
Arma (defensa): Espada de Hierro (148/150) [-1 uso] [⚔️ Equipado]
|
||||
|
||||
Recompensas:
|
||||
• Pez Común x2
|
||||
• 🪙 +10
|
||||
|
||||
Mobs: —
|
||||
```
|
||||
|
||||
### Pelear (Espada como Tool Principal)
|
||||
```
|
||||
!pelear 1
|
||||
|
||||
Área: fight.arena • Nivel: 1
|
||||
Arma: Espada de Hierro (148/150) [-2 usos] [⚔️ Equipado]
|
||||
|
||||
Recompensas:
|
||||
• 🪙 +25
|
||||
|
||||
Enemigos:
|
||||
• slime
|
||||
|
||||
Combate: ⚔️ 1 mob → 1 derrotado | 💀 Daño infligido: 18 | 🩹 Daño recibido: 3
|
||||
Victoria ✅
|
||||
HP: ❤️❤️❤️❤️❤️ (97/100)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuración Recomendada
|
||||
|
||||
Para ajustar desgaste según dificultad de tu servidor:
|
||||
|
||||
1. **Herramientas de recolección** (picos, cañas):
|
||||
- `durabilityPerUse`: 3-5 (se aplica completo en gather).
|
||||
|
||||
2. **Armas** (espadas):
|
||||
- `durabilityPerUse`: 2-4 (se reduce a 1-2 en combate por factor 0.5).
|
||||
|
||||
3. **Eventos extremos**:
|
||||
- Puedes crear ítems especiales con `durabilityPerUse: 1` para mayor longevidad o eventos sin desgaste (`enabled: false`).
|
||||
|
||||
---
|
||||
|
||||
## 🔮 Próximos Pasos
|
||||
|
||||
- [ ] Extender sistema a herramientas agrícolas (`hoe`, `watering_can`) con `usage: "farming"` y factor ajustable.
|
||||
- [ ] Añadir mutaciones de ítems que reduzcan `durabilityPerUse` (ej: encantamiento "Durabilidad+" reduce desgaste en 25%).
|
||||
- [ ] Implementar `ToolBreakLog` (migración propuesta en `PROPUESTA_MIGRACIONES_RPG.md`) para auditoría completa.
|
||||
|
||||
---
|
||||
|
||||
**Fecha**: Octubre 2025
|
||||
**Autor**: Sistema RPG Integrado v2
|
||||
**Archivo**: `README/DOBLE_DURABILIDAD_MINIJUEGOS.md`
|
||||
@@ -17,7 +17,7 @@
|
||||
"start:prod-optimized": "NODE_ENV=production ENABLE_MEMORY_OPTIMIZER=true NODE_OPTIONS='--max-old-space-size=512 --expose-gc' npx tsx src/main.ts",
|
||||
"tsc": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"seed:minigames": "tsx src/game/minigames/seed.ts",
|
||||
"seed:minigames": "npx tsx src/game/minigames/seed.ts",
|
||||
"start:optimize-relic": "NODE_ENV=production ENABLE_MEMORY_OPTIMIZER=true NEW_RELIC_APP_NAME=amayo NEW_RELIC_LICENSE_KEY=85ef833e676ed6ea726e23b3e373397dFFFFNRAL NODE_OPTIONS='--max-old-space-size=512 --expose-gc' npx tsx --experimental-loader=newrelic/esm-loader.mjs src/main.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
|
||||
@@ -76,6 +76,7 @@ export const command: CommandMessage = {
|
||||
)
|
||||
.map((r) => r.itemKey!);
|
||||
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||
if (result.weaponTool?.key) rewardKeys.push(result.weaponTool.key);
|
||||
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||
|
||||
// Actualizar stats
|
||||
@@ -141,6 +142,27 @@ export const command: CommandMessage = {
|
||||
})
|
||||
: "—";
|
||||
|
||||
const weaponInfo = result.weaponTool?.key
|
||||
? formatToolLabel({
|
||||
key: result.weaponTool.key,
|
||||
displayName: formatItemLabel(
|
||||
rewardItems.get(result.weaponTool.key) ?? {
|
||||
key: result.weaponTool.key,
|
||||
name: null,
|
||||
icon: null,
|
||||
},
|
||||
{ fallbackIcon: "⚔️" }
|
||||
),
|
||||
instancesRemaining: result.weaponTool.instancesRemaining,
|
||||
broken: result.weaponTool.broken,
|
||||
brokenInstance: result.weaponTool.brokenInstance,
|
||||
durabilityDelta: result.weaponTool.durabilityDelta,
|
||||
remaining: result.weaponTool.remaining,
|
||||
max: result.weaponTool.max,
|
||||
source: result.weaponTool.toolSource,
|
||||
})
|
||||
: null;
|
||||
|
||||
const combatSummary = result.combat
|
||||
? combatSummaryRPG({
|
||||
mobs: result.mobs.length,
|
||||
@@ -165,9 +187,12 @@ export const command: CommandMessage = {
|
||||
source === "global"
|
||||
? "🌐 Configuración global"
|
||||
: "📍 Configuración local";
|
||||
const toolsLine = weaponInfo
|
||||
? `**Pico:** ${toolInfo}\n**Arma (defensa):** ${weaponInfo}`
|
||||
: `**Herramienta:** ${toolInfo}`;
|
||||
blocks.push(
|
||||
textBlock(
|
||||
`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`
|
||||
`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n${toolsLine}`
|
||||
)
|
||||
);
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
|
||||
@@ -77,6 +77,7 @@ export const command: CommandMessage = {
|
||||
)
|
||||
.map((r) => r.itemKey!);
|
||||
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||
if (result.weaponTool?.key) rewardKeys.push(result.weaponTool.key);
|
||||
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||
|
||||
// Actualizar stats y misiones
|
||||
|
||||
@@ -76,6 +76,7 @@ export const command: CommandMessage = {
|
||||
)
|
||||
.map((r) => r.itemKey!);
|
||||
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||
if (result.weaponTool?.key) rewardKeys.push(result.weaponTool.key);
|
||||
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||
|
||||
// Actualizar stats y misiones
|
||||
@@ -135,6 +136,27 @@ export const command: CommandMessage = {
|
||||
source: result.tool.toolSource,
|
||||
})
|
||||
: "—";
|
||||
|
||||
const weaponInfo = result.weaponTool?.key
|
||||
? formatToolLabel({
|
||||
key: result.weaponTool.key,
|
||||
displayName: formatItemLabel(
|
||||
rewardItems.get(result.weaponTool.key) ?? {
|
||||
key: result.weaponTool.key,
|
||||
name: null,
|
||||
icon: null,
|
||||
},
|
||||
{ fallbackIcon: "⚔️" }
|
||||
),
|
||||
instancesRemaining: result.weaponTool.instancesRemaining,
|
||||
broken: result.weaponTool.broken,
|
||||
brokenInstance: result.weaponTool.brokenInstance,
|
||||
durabilityDelta: result.weaponTool.durabilityDelta,
|
||||
remaining: result.weaponTool.remaining,
|
||||
max: result.weaponTool.max,
|
||||
source: result.weaponTool.toolSource,
|
||||
})
|
||||
: null;
|
||||
const combatSummary = result.combat
|
||||
? combatSummaryRPG({
|
||||
mobs: result.mobs.length,
|
||||
@@ -159,9 +181,12 @@ export const command: CommandMessage = {
|
||||
source === "global"
|
||||
? "🌐 Configuración global"
|
||||
: "📍 Configuración local";
|
||||
const toolsLine = weaponInfo
|
||||
? `**Caña:** ${toolInfo}\n**Arma (defensa):** ${weaponInfo}`
|
||||
: `**Herramienta:** ${toolInfo}`;
|
||||
blocks.push(
|
||||
textBlock(
|
||||
`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`
|
||||
`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n${toolsLine}`
|
||||
)
|
||||
);
|
||||
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||
|
||||
@@ -1,25 +1,35 @@
|
||||
import { prisma } from '../../core/database/prisma';
|
||||
import { ensurePlayerState, getEquipment, getEffectiveStats, adjustHP } from './equipmentService';
|
||||
import { reduceToolDurability } from '../minigames/service';
|
||||
import { prisma } from "../../core/database/prisma";
|
||||
import {
|
||||
ensurePlayerState,
|
||||
getEquipment,
|
||||
getEffectiveStats,
|
||||
adjustHP,
|
||||
} from "./equipmentService";
|
||||
import { reduceToolDurability } from "../minigames/service";
|
||||
|
||||
function getNumber(v: any, fallback = 0) { return typeof v === 'number' ? v : fallback; }
|
||||
function getNumber(v: any, fallback = 0) {
|
||||
return typeof v === "number" ? v : fallback;
|
||||
}
|
||||
|
||||
export async function processScheduledAttacks(limit = 25) {
|
||||
const now = new Date();
|
||||
const jobs = await prisma.scheduledMobAttack.findMany({
|
||||
where: { status: 'scheduled', scheduleAt: { lte: now } },
|
||||
orderBy: { scheduleAt: 'asc' },
|
||||
where: { status: "scheduled", scheduleAt: { lte: now } },
|
||||
orderBy: { scheduleAt: "asc" },
|
||||
take: limit,
|
||||
});
|
||||
for (const job of jobs) {
|
||||
try {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// marcar processing
|
||||
await tx.scheduledMobAttack.update({ where: { id: job.id }, data: { status: 'processing' } });
|
||||
await tx.scheduledMobAttack.update({
|
||||
where: { id: job.id },
|
||||
data: { status: "processing" },
|
||||
});
|
||||
|
||||
const mob = await tx.mob.findUnique({ where: { id: job.mobId } });
|
||||
if (!mob) throw new Error('Mob inexistente');
|
||||
const stats = mob.stats as any || {};
|
||||
if (!mob) throw new Error("Mob inexistente");
|
||||
const stats = (mob.stats as any) || {};
|
||||
const mobAttack = Math.max(0, getNumber(stats.attack, 5));
|
||||
|
||||
await ensurePlayerState(job.userId, job.guildId);
|
||||
@@ -32,25 +42,49 @@ export async function processScheduledAttacks(limit = 25) {
|
||||
// desgastar arma equipada si existe
|
||||
const { eq, weapon } = await getEquipment(job.userId, job.guildId);
|
||||
if (weapon) {
|
||||
// buscar por key para reducir durabilidad
|
||||
// buscar por key para reducir durabilidad con multiplicador de combate (50%)
|
||||
// weapon tiene id; buscamos para traer key
|
||||
const full = await tx.economyItem.findUnique({ where: { id: weapon.id } });
|
||||
const full = await tx.economyItem.findUnique({
|
||||
where: { id: weapon.id },
|
||||
});
|
||||
if (full) {
|
||||
await reduceToolDurability(job.userId, job.guildId, full.key);
|
||||
await reduceToolDurability(
|
||||
job.userId,
|
||||
job.guildId,
|
||||
full.key,
|
||||
"combat"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// finalizar
|
||||
await tx.scheduledMobAttack.update({ where: { id: job.id }, data: { status: 'done', processedAt: new Date() } });
|
||||
await tx.scheduledMobAttack.update({
|
||||
where: { id: job.id },
|
||||
data: { status: "done", processedAt: new Date() },
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
await prisma.scheduledMobAttack.update({ where: { id: job.id }, data: { status: 'failed', processedAt: new Date(), metadata: { error: String(e) } as any } });
|
||||
await prisma.scheduledMobAttack.update({
|
||||
where: { id: job.id },
|
||||
data: {
|
||||
status: "failed",
|
||||
processedAt: new Date(),
|
||||
metadata: { error: String(e) } as any,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return { processed: jobs.length } as const;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
processScheduledAttacks().then((r) => { console.log('[attacksWorker] processed', r.processed); process.exit(0); }).catch((e) => { console.error(e); process.exit(1); });
|
||||
processScheduledAttacks()
|
||||
.then((r) => {
|
||||
console.log("[attacksWorker] processed", r.processed);
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,134 +1,186 @@
|
||||
import { prisma } from '../../core/database/prisma';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { prisma } from "../../core/database/prisma";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
async function upsertEconomyItem(guildId: string | null, key: string, data: Omit<Prisma.EconomyItemUncheckedCreateInput, 'key' | 'guildId'>) {
|
||||
async function upsertEconomyItem(
|
||||
guildId: string | null,
|
||||
key: string,
|
||||
data: Omit<Prisma.EconomyItemUncheckedCreateInput, "key" | "guildId">
|
||||
) {
|
||||
if (guildId) {
|
||||
return prisma.economyItem.upsert({ where: { guildId_key: { guildId, key } }, update: {}, create: { ...data, key, guildId } });
|
||||
return prisma.economyItem.upsert({
|
||||
where: { guildId_key: { guildId, key } },
|
||||
update: {},
|
||||
create: { ...data, key, guildId },
|
||||
});
|
||||
}
|
||||
const existing = await prisma.economyItem.findFirst({ where: { key, guildId: null } });
|
||||
if (existing) return prisma.economyItem.update({ where: { id: existing.id }, data: {} });
|
||||
const existing = await prisma.economyItem.findFirst({
|
||||
where: { key, guildId: null },
|
||||
});
|
||||
if (existing)
|
||||
return prisma.economyItem.update({ where: { id: existing.id }, data: {} });
|
||||
return prisma.economyItem.create({ data: { ...data, key, guildId: null } });
|
||||
}
|
||||
|
||||
async function upsertGameArea(guildId: string | null, key: string, data: Omit<Prisma.GameAreaUncheckedCreateInput, 'key' | 'guildId'>) {
|
||||
async function upsertGameArea(
|
||||
guildId: string | null,
|
||||
key: string,
|
||||
data: Omit<Prisma.GameAreaUncheckedCreateInput, "key" | "guildId">
|
||||
) {
|
||||
if (guildId) {
|
||||
return prisma.gameArea.upsert({ where: { guildId_key: { guildId, key } }, update: {}, create: { ...data, key, guildId } });
|
||||
return prisma.gameArea.upsert({
|
||||
where: { guildId_key: { guildId, key } },
|
||||
update: {},
|
||||
create: { ...data, key, guildId },
|
||||
});
|
||||
}
|
||||
const existing = await prisma.gameArea.findFirst({ where: { key, guildId: null } });
|
||||
if (existing) return prisma.gameArea.update({ where: { id: existing.id }, data: {} });
|
||||
const existing = await prisma.gameArea.findFirst({
|
||||
where: { key, guildId: null },
|
||||
});
|
||||
if (existing)
|
||||
return prisma.gameArea.update({ where: { id: existing.id }, data: {} });
|
||||
return prisma.gameArea.create({ data: { ...data, key, guildId: null } });
|
||||
}
|
||||
|
||||
async function upsertMob(guildId: string | null, key: string, data: Omit<Prisma.MobUncheckedCreateInput, 'key' | 'guildId'>) {
|
||||
async function upsertMob(
|
||||
guildId: string | null,
|
||||
key: string,
|
||||
data: Omit<Prisma.MobUncheckedCreateInput, "key" | "guildId">
|
||||
) {
|
||||
if (guildId) {
|
||||
return prisma.mob.upsert({ where: { guildId_key: { guildId, key } }, update: { stats: (data as any).stats, drops: (data as any).drops, name: (data as any).name }, create: { ...data, key, guildId } });
|
||||
return prisma.mob.upsert({
|
||||
where: { guildId_key: { guildId, key } },
|
||||
update: {
|
||||
stats: (data as any).stats,
|
||||
drops: (data as any).drops,
|
||||
name: (data as any).name,
|
||||
},
|
||||
create: { ...data, key, guildId },
|
||||
});
|
||||
}
|
||||
const existing = await prisma.mob.findFirst({ where: { key, guildId: null } });
|
||||
if (existing) return prisma.mob.update({ where: { id: existing.id }, data: { stats: (data as any).stats, drops: (data as any).drops, name: (data as any).name } });
|
||||
const existing = await prisma.mob.findFirst({
|
||||
where: { key, guildId: null },
|
||||
});
|
||||
if (existing)
|
||||
return prisma.mob.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
stats: (data as any).stats,
|
||||
drops: (data as any).drops,
|
||||
name: (data as any).name,
|
||||
},
|
||||
});
|
||||
return prisma.mob.create({ data: { ...data, key, guildId: null } });
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const guildId = process.env.TEST_GUILD_ID ?? null; // null => global
|
||||
const guildId = "1316592320954630144"; // null => global
|
||||
|
||||
// Items base: herramientas y minerales
|
||||
const pickKey = 'tool.pickaxe.basic';
|
||||
const rodKey = 'tool.rod.basic';
|
||||
const swordKey = 'weapon.sword.iron';
|
||||
const armorKey = 'armor.leather.basic';
|
||||
const capeKey = 'cape.life.minor';
|
||||
const pickKey = "tool.pickaxe.basic";
|
||||
const rodKey = "tool.rod.basic";
|
||||
const swordKey = "weapon.sword.iron";
|
||||
const armorKey = "armor.leather.basic";
|
||||
const capeKey = "cape.life.minor";
|
||||
|
||||
const ironKey = 'ore.iron';
|
||||
const goldKey = 'ore.gold';
|
||||
const ironIngotKey = 'ingot.iron';
|
||||
const ironKey = "ore.iron";
|
||||
const goldKey = "ore.gold";
|
||||
const ironIngotKey = "ingot.iron";
|
||||
|
||||
const fishCommonKey = 'fish.common';
|
||||
const fishRareKey = 'fish.rare';
|
||||
const fishCommonKey = "fish.common";
|
||||
const fishRareKey = "fish.rare";
|
||||
|
||||
// Herramientas
|
||||
await upsertEconomyItem(guildId, pickKey, {
|
||||
name: 'Pico Básico',
|
||||
name: "Pico Básico",
|
||||
stackable: false,
|
||||
props: {
|
||||
tool: { type: 'pickaxe', tier: 1 },
|
||||
tool: { type: "pickaxe", tier: 1 },
|
||||
breakable: { enabled: true, maxDurability: 100, durabilityPerUse: 5 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
tags: ['tool', 'mine'],
|
||||
tags: ["tool", "mine"],
|
||||
});
|
||||
|
||||
await upsertEconomyItem(guildId, rodKey, {
|
||||
name: 'Caña Básica',
|
||||
name: "Caña Básica",
|
||||
stackable: false,
|
||||
props: {
|
||||
tool: { type: 'rod', tier: 1 },
|
||||
tool: { type: "rod", tier: 1 },
|
||||
breakable: { enabled: true, maxDurability: 80, durabilityPerUse: 3 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
tags: ['tool', 'fish'],
|
||||
tags: ["tool", "fish"],
|
||||
});
|
||||
|
||||
// Arma, armadura y capa
|
||||
await upsertEconomyItem(guildId, swordKey, {
|
||||
name: 'Espada de Hierro',
|
||||
name: "Espada de Hierro",
|
||||
stackable: false,
|
||||
props: { damage: 10, tool: { type: 'sword', tier: 1 }, breakable: { enabled: true, maxDurability: 150, durabilityPerUse: 2 } } as unknown as Prisma.InputJsonValue,
|
||||
tags: ['weapon'],
|
||||
props: {
|
||||
damage: 10,
|
||||
tool: { type: "sword", tier: 1 },
|
||||
breakable: { enabled: true, maxDurability: 150, durabilityPerUse: 2 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
tags: ["weapon"],
|
||||
});
|
||||
|
||||
await upsertEconomyItem(guildId, armorKey, {
|
||||
name: 'Armadura de Cuero',
|
||||
name: "Armadura de Cuero",
|
||||
stackable: false,
|
||||
props: { defense: 3 } as unknown as Prisma.InputJsonValue,
|
||||
tags: ['armor'],
|
||||
tags: ["armor"],
|
||||
});
|
||||
|
||||
await upsertEconomyItem(guildId, capeKey, {
|
||||
name: 'Capa de Vida Menor',
|
||||
name: "Capa de Vida Menor",
|
||||
stackable: false,
|
||||
props: { maxHpBonus: 20 } as unknown as Prisma.InputJsonValue,
|
||||
tags: ['cape'],
|
||||
tags: ["cape"],
|
||||
});
|
||||
|
||||
// Materiales
|
||||
await upsertEconomyItem(guildId, ironKey, {
|
||||
name: 'Mineral de Hierro',
|
||||
name: "Mineral de Hierro",
|
||||
stackable: true,
|
||||
props: { craftingOnly: true } as unknown as Prisma.InputJsonValue,
|
||||
tags: ['ore', 'common'],
|
||||
tags: ["ore", "common"],
|
||||
});
|
||||
|
||||
await upsertEconomyItem(guildId, goldKey, {
|
||||
name: 'Mineral de Oro',
|
||||
name: "Mineral de Oro",
|
||||
stackable: true,
|
||||
props: { craftingOnly: true } as unknown as Prisma.InputJsonValue,
|
||||
tags: ['ore', 'rare'],
|
||||
tags: ["ore", "rare"],
|
||||
});
|
||||
|
||||
await upsertEconomyItem(guildId, ironIngotKey, {
|
||||
name: 'Lingote de Hierro',
|
||||
name: "Lingote de Hierro",
|
||||
stackable: true,
|
||||
props: {} as unknown as Prisma.InputJsonValue,
|
||||
tags: ['ingot', 'metal'],
|
||||
tags: ["ingot", "metal"],
|
||||
});
|
||||
|
||||
// Comida (pesca) que cura con cooldown
|
||||
await upsertEconomyItem(guildId, fishCommonKey, {
|
||||
name: 'Pez Común',
|
||||
name: "Pez Común",
|
||||
stackable: true,
|
||||
props: { food: { healHp: 10, cooldownSeconds: 30 } } as unknown as Prisma.InputJsonValue,
|
||||
tags: ['fish', 'food', 'common'],
|
||||
props: {
|
||||
food: { healHp: 10, cooldownSeconds: 30 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
tags: ["fish", "food", "common"],
|
||||
});
|
||||
|
||||
await upsertEconomyItem(guildId, fishRareKey, {
|
||||
name: 'Pez Raro',
|
||||
name: "Pez Raro",
|
||||
stackable: true,
|
||||
props: { food: { healHp: 20, healPercent: 5, cooldownSeconds: 45 } } as unknown as Prisma.InputJsonValue,
|
||||
tags: ['fish', 'food', 'rare'],
|
||||
props: {
|
||||
food: { healHp: 20, healPercent: 5, cooldownSeconds: 45 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
tags: ["fish", "food", "rare"],
|
||||
});
|
||||
|
||||
// Área de mina con niveles
|
||||
const mineArea = await upsertGameArea(guildId, 'mine.cavern', {
|
||||
name: 'Mina: Caverna',
|
||||
type: 'MINE',
|
||||
const mineArea = await upsertGameArea(guildId, "mine.cavern", {
|
||||
name: "Mina: Caverna",
|
||||
type: "MINE",
|
||||
config: { cooldownSeconds: 10 } as unknown as Prisma.InputJsonValue,
|
||||
});
|
||||
|
||||
@@ -138,9 +190,24 @@ async function main() {
|
||||
create: {
|
||||
areaId: mineArea.id,
|
||||
level: 1,
|
||||
requirements: { tool: { required: true, toolType: 'pickaxe', minTier: 1 } } as unknown as Prisma.InputJsonValue,
|
||||
rewards: { draws: 2, table: [ { type: 'item', itemKey: ironKey, qty: 2, weight: 70 }, { type: 'item', itemKey: ironKey, qty: 3, weight: 20 }, { type: 'item', itemKey: goldKey, qty: 1, weight: 10 } ] } as unknown as Prisma.InputJsonValue,
|
||||
mobs: { draws: 1, table: [ { mobKey: 'bat', weight: 20 }, { mobKey: 'slime', weight: 10 } ] } as unknown as Prisma.InputJsonValue,
|
||||
requirements: {
|
||||
tool: { required: true, toolType: "pickaxe", minTier: 1 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
rewards: {
|
||||
draws: 2,
|
||||
table: [
|
||||
{ type: "item", itemKey: ironKey, qty: 2, weight: 70 },
|
||||
{ type: "item", itemKey: ironKey, qty: 3, weight: 20 },
|
||||
{ type: "item", itemKey: goldKey, qty: 1, weight: 10 },
|
||||
],
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
mobs: {
|
||||
draws: 1,
|
||||
table: [
|
||||
{ mobKey: "bat", weight: 20 },
|
||||
{ mobKey: "slime", weight: 10 },
|
||||
],
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -150,16 +217,32 @@ async function main() {
|
||||
create: {
|
||||
areaId: mineArea.id,
|
||||
level: 2,
|
||||
requirements: { tool: { required: true, toolType: 'pickaxe', minTier: 2 } } as unknown as Prisma.InputJsonValue,
|
||||
rewards: { draws: 3, table: [ { type: 'item', itemKey: ironKey, qty: 3, weight: 60 }, { type: 'item', itemKey: goldKey, qty: 1, weight: 30 }, { type: 'coins', amount: 50, weight: 10 } ] } as unknown as Prisma.InputJsonValue,
|
||||
mobs: { draws: 2, table: [ { mobKey: 'bat', weight: 20 }, { mobKey: 'slime', weight: 20 }, { mobKey: 'goblin', weight: 10 } ] } as unknown as Prisma.InputJsonValue,
|
||||
requirements: {
|
||||
tool: { required: true, toolType: "pickaxe", minTier: 2 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
rewards: {
|
||||
draws: 3,
|
||||
table: [
|
||||
{ type: "item", itemKey: ironKey, qty: 3, weight: 60 },
|
||||
{ type: "item", itemKey: goldKey, qty: 1, weight: 30 },
|
||||
{ type: "coins", amount: 50, weight: 10 },
|
||||
],
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
mobs: {
|
||||
draws: 2,
|
||||
table: [
|
||||
{ mobKey: "bat", weight: 20 },
|
||||
{ mobKey: "slime", weight: 20 },
|
||||
{ mobKey: "goblin", weight: 10 },
|
||||
],
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
// Área de laguna (pesca)
|
||||
const lagoon = await upsertGameArea(guildId, 'lagoon.shore', {
|
||||
name: 'Laguna: Orilla',
|
||||
type: 'LAGOON',
|
||||
const lagoon = await upsertGameArea(guildId, "lagoon.shore", {
|
||||
name: "Laguna: Orilla",
|
||||
type: "LAGOON",
|
||||
config: { cooldownSeconds: 12 } as unknown as Prisma.InputJsonValue,
|
||||
});
|
||||
|
||||
@@ -169,16 +252,25 @@ async function main() {
|
||||
create: {
|
||||
areaId: lagoon.id,
|
||||
level: 1,
|
||||
requirements: { tool: { required: true, toolType: 'rod', minTier: 1 } } as unknown as Prisma.InputJsonValue,
|
||||
rewards: { draws: 2, table: [ { type: 'item', itemKey: fishCommonKey, qty: 1, weight: 70 }, { type: 'item', itemKey: fishRareKey, qty: 1, weight: 10 }, { type: 'coins', amount: 10, weight: 20 } ] } as unknown as Prisma.InputJsonValue,
|
||||
requirements: {
|
||||
tool: { required: true, toolType: "rod", minTier: 1 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
rewards: {
|
||||
draws: 2,
|
||||
table: [
|
||||
{ type: "item", itemKey: fishCommonKey, qty: 1, weight: 70 },
|
||||
{ type: "item", itemKey: fishRareKey, qty: 1, weight: 10 },
|
||||
{ type: "coins", amount: 10, weight: 20 },
|
||||
],
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
mobs: { draws: 0, table: [] } as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
// Área de pelea (arena)
|
||||
const arena = await upsertGameArea(guildId, 'fight.arena', {
|
||||
name: 'Arena de Combate',
|
||||
type: 'FIGHT',
|
||||
const arena = await upsertGameArea(guildId, "fight.arena", {
|
||||
name: "Arena de Combate",
|
||||
type: "FIGHT",
|
||||
config: { cooldownSeconds: 15 } as unknown as Prisma.InputJsonValue,
|
||||
});
|
||||
|
||||
@@ -188,36 +280,395 @@ async function main() {
|
||||
create: {
|
||||
areaId: arena.id,
|
||||
level: 1,
|
||||
requirements: { tool: { required: true, toolType: 'sword', minTier: 1, allowedKeys: [swordKey] } } as unknown as Prisma.InputJsonValue,
|
||||
rewards: { draws: 1, table: [ { type: 'coins', amount: 25, weight: 100 } ] } as unknown as Prisma.InputJsonValue,
|
||||
mobs: { draws: 1, table: [ { mobKey: 'slime', weight: 50 }, { mobKey: 'goblin', weight: 50 } ] } as unknown as Prisma.InputJsonValue,
|
||||
requirements: {
|
||||
tool: {
|
||||
required: true,
|
||||
toolType: "sword",
|
||||
minTier: 1,
|
||||
allowedKeys: [swordKey],
|
||||
},
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
rewards: {
|
||||
draws: 1,
|
||||
table: [{ type: "coins", amount: 25, weight: 100 }],
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
mobs: {
|
||||
draws: 1,
|
||||
table: [
|
||||
{ mobKey: "slime", weight: 50 },
|
||||
{ mobKey: "goblin", weight: 50 },
|
||||
],
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
// Mobs básicos
|
||||
const mobs = [
|
||||
{ key: 'bat', name: 'Murciélago', stats: { attack: 4 } },
|
||||
{ key: 'slime', name: 'Slime', stats: { attack: 6 } },
|
||||
{ key: 'goblin', name: 'Duende', stats: { attack: 8 } },
|
||||
{ key: "bat", name: "Murciélago", stats: { attack: 4 } },
|
||||
{ key: "slime", name: "Slime", stats: { attack: 6 } },
|
||||
{ key: "goblin", name: "Duende", stats: { attack: 8 } },
|
||||
];
|
||||
for (const m of mobs) {
|
||||
await upsertMob(guildId, m.key, { name: m.name, stats: m.stats as unknown as Prisma.InputJsonValue, drops: Prisma.DbNull });
|
||||
await upsertMob(guildId, m.key, {
|
||||
name: m.name,
|
||||
stats: m.stats as unknown as Prisma.InputJsonValue,
|
||||
drops: Prisma.DbNull,
|
||||
});
|
||||
}
|
||||
|
||||
// Programar un par de ataques de mobs (demostración)
|
||||
const targetUser = process.env.TEST_USER_ID;
|
||||
const targetUser = "327207082203938818";
|
||||
if (targetUser) {
|
||||
const slime = await prisma.mob.findFirst({ where: { key: 'slime', OR: [{ guildId }, { guildId: null }] }, orderBy: [{ guildId: 'desc' }] });
|
||||
const slime = await prisma.mob.findFirst({
|
||||
where: { key: "slime", OR: [{ guildId }, { guildId: null }] },
|
||||
orderBy: [{ guildId: "desc" }],
|
||||
});
|
||||
if (slime) {
|
||||
const now = Date.now();
|
||||
await prisma.scheduledMobAttack.createMany({ data: [
|
||||
{ userId: targetUser, guildId: (guildId ?? 'global'), mobId: slime.id, scheduleAt: new Date(now + 5_000) },
|
||||
{ userId: targetUser, guildId: (guildId ?? 'global'), mobId: slime.id, scheduleAt: new Date(now + 15_000) },
|
||||
] });
|
||||
await prisma.scheduledMobAttack.createMany({
|
||||
data: [
|
||||
{
|
||||
userId: targetUser,
|
||||
guildId: guildId ?? "global",
|
||||
mobId: slime.id,
|
||||
scheduleAt: new Date(now + 5_000),
|
||||
},
|
||||
{
|
||||
userId: targetUser,
|
||||
guildId: guildId ?? "global",
|
||||
mobId: slime.id,
|
||||
scheduleAt: new Date(now + 15_000),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[seed:minigames] done');
|
||||
// ---------------------------------------------------------------------------
|
||||
// NUEVO CONTENIDO PARA PROBAR SISTEMAS AVANZADOS (tiers, riskFactor, fatiga)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Herramientas / equipo Tier 2
|
||||
const pickKeyT2 = "tool.pickaxe.iron";
|
||||
const rodKeyT2 = "tool.rod.oak";
|
||||
const swordKeyT2 = "weapon.sword.steel";
|
||||
const armorKeyT2 = "armor.chain.basic";
|
||||
const capeKeyT2 = "cape.life.moderate";
|
||||
|
||||
await upsertEconomyItem(guildId, pickKeyT2, {
|
||||
name: "Pico de Hierro",
|
||||
stackable: false,
|
||||
props: {
|
||||
tool: { type: "pickaxe", tier: 2 },
|
||||
breakable: { enabled: true, maxDurability: 180, durabilityPerUse: 4 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
tags: ["tool", "mine", "tier2"],
|
||||
});
|
||||
|
||||
await upsertEconomyItem(guildId, rodKeyT2, {
|
||||
name: "Caña Robusta",
|
||||
stackable: false,
|
||||
props: {
|
||||
tool: { type: "rod", tier: 2 },
|
||||
breakable: { enabled: true, maxDurability: 140, durabilityPerUse: 3 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
tags: ["tool", "fish", "tier2"],
|
||||
});
|
||||
|
||||
await upsertEconomyItem(guildId, swordKeyT2, {
|
||||
name: "Espada de Acero",
|
||||
stackable: false,
|
||||
props: {
|
||||
damage: 18,
|
||||
tool: { type: "sword", tier: 2 },
|
||||
breakable: { enabled: true, maxDurability: 220, durabilityPerUse: 2 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
tags: ["weapon", "tier2"],
|
||||
});
|
||||
|
||||
await upsertEconomyItem(guildId, armorKeyT2, {
|
||||
name: "Armadura de Cota de Malla",
|
||||
stackable: false,
|
||||
props: { defense: 6 } as unknown as Prisma.InputJsonValue,
|
||||
tags: ["armor", "tier2"],
|
||||
});
|
||||
|
||||
await upsertEconomyItem(guildId, capeKeyT2, {
|
||||
name: "Capa de Vida Moderada",
|
||||
stackable: false,
|
||||
props: { maxHpBonus: 40 } as unknown as Prisma.InputJsonValue,
|
||||
tags: ["cape", "tier2"],
|
||||
});
|
||||
|
||||
// Consumibles / pruebas de curación y limpieza de efectos
|
||||
const bigFoodKey = "food.meat.large";
|
||||
const fatigueClearPotionKey = "potion.fatigue.clear";
|
||||
|
||||
await upsertEconomyItem(guildId, bigFoodKey, {
|
||||
name: "Carne Asada Grande",
|
||||
stackable: true,
|
||||
props: {
|
||||
food: { healHp: 40, healPercent: 10, cooldownSeconds: 60 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
tags: ["food", "healing"],
|
||||
});
|
||||
|
||||
await upsertEconomyItem(guildId, fatigueClearPotionKey, {
|
||||
name: "Poción Energética",
|
||||
stackable: true,
|
||||
props: {
|
||||
potion: { removeEffects: ["FATIGUE"], cooldownSeconds: 90 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
tags: ["potion", "utility"],
|
||||
});
|
||||
|
||||
// ÁREA NUEVA: Mina de Fisura (más riesgo => probar penalización muerte)
|
||||
const riftMine = await upsertGameArea(guildId, "mine.rift", {
|
||||
name: "Mina: Fisura Cristalina",
|
||||
type: "MINE",
|
||||
config: { cooldownSeconds: 14 } as unknown as Prisma.InputJsonValue,
|
||||
metadata: { riskFactor: 1.6 } as unknown as Prisma.InputJsonValue,
|
||||
});
|
||||
|
||||
await prisma.gameAreaLevel.upsert({
|
||||
where: { areaId_level: { areaId: riftMine.id, level: 1 } },
|
||||
update: {},
|
||||
create: {
|
||||
areaId: riftMine.id,
|
||||
level: 1,
|
||||
requirements: {
|
||||
tool: { required: true, toolType: "pickaxe", minTier: 2 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
rewards: {
|
||||
draws: 3,
|
||||
table: [
|
||||
{ type: "item", itemKey: ironKey, qty: 4, weight: 55 },
|
||||
{ type: "item", itemKey: goldKey, qty: 2, weight: 20 },
|
||||
{ type: "coins", amount: 60, weight: 25 },
|
||||
],
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
mobs: {
|
||||
draws: 2,
|
||||
table: [
|
||||
{ mobKey: "goblin", weight: 25 },
|
||||
{ mobKey: "orc", weight: 15 },
|
||||
],
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
metadata: { suggestedHp: 120 } as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
// Extensión de la mina existente: nivel 3
|
||||
await prisma.gameAreaLevel.upsert({
|
||||
where: { areaId_level: { areaId: mineArea.id, level: 3 } },
|
||||
update: {},
|
||||
create: {
|
||||
areaId: mineArea.id,
|
||||
level: 3,
|
||||
requirements: {
|
||||
tool: { required: true, toolType: "pickaxe", minTier: 2 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
rewards: {
|
||||
draws: 4,
|
||||
table: [
|
||||
{ type: "item", itemKey: ironKey, qty: 4, weight: 50 },
|
||||
{ type: "item", itemKey: goldKey, qty: 2, weight: 25 },
|
||||
{ type: "coins", amount: 80, weight: 15 },
|
||||
{ type: "coins", amount: 120, weight: 10 },
|
||||
],
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
mobs: {
|
||||
draws: 2,
|
||||
table: [
|
||||
{ mobKey: "slime", weight: 20 },
|
||||
{ mobKey: "goblin", weight: 20 },
|
||||
{ mobKey: "orc", weight: 10 },
|
||||
],
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
// Laguna nivel 2
|
||||
await prisma.gameAreaLevel.upsert({
|
||||
where: { areaId_level: { areaId: lagoon.id, level: 2 } },
|
||||
update: {},
|
||||
create: {
|
||||
areaId: lagoon.id,
|
||||
level: 2,
|
||||
requirements: {
|
||||
tool: { required: true, toolType: "rod", minTier: 2 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
rewards: {
|
||||
draws: 3,
|
||||
table: [
|
||||
{ type: "item", itemKey: fishCommonKey, qty: 2, weight: 60 },
|
||||
{ type: "item", itemKey: fishRareKey, qty: 1, weight: 20 },
|
||||
{ type: "coins", amount: 30, weight: 20 },
|
||||
],
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
mobs: { draws: 0, table: [] } as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
// Arena existente: nivel 2
|
||||
await prisma.gameAreaLevel.upsert({
|
||||
where: { areaId_level: { areaId: arena.id, level: 2 } },
|
||||
update: {},
|
||||
create: {
|
||||
areaId: arena.id,
|
||||
level: 2,
|
||||
requirements: {
|
||||
tool: { required: true, toolType: "sword", minTier: 2 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
rewards: {
|
||||
draws: 1,
|
||||
table: [
|
||||
{ type: "coins", amount: 60, weight: 70 },
|
||||
{ type: "coins", amount: 90, weight: 30 },
|
||||
],
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
mobs: {
|
||||
draws: 1,
|
||||
table: [
|
||||
{ mobKey: "goblin", weight: 40 },
|
||||
{ mobKey: "orc", weight: 40 },
|
||||
{ mobKey: "troll", weight: 20 },
|
||||
],
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
// Arena élite separada para probar riskFactor de muerte
|
||||
const eliteArena = await upsertGameArea(guildId, "fight.arena.elite", {
|
||||
name: "Arena de Combate Élite",
|
||||
type: "FIGHT",
|
||||
config: { cooldownSeconds: 25 } as unknown as Prisma.InputJsonValue,
|
||||
metadata: { riskFactor: 1.4 } as unknown as Prisma.InputJsonValue,
|
||||
});
|
||||
|
||||
await prisma.gameAreaLevel.upsert({
|
||||
where: { areaId_level: { areaId: eliteArena.id, level: 1 } },
|
||||
update: {},
|
||||
create: {
|
||||
areaId: eliteArena.id,
|
||||
level: 1,
|
||||
requirements: {
|
||||
tool: { required: true, toolType: "sword", minTier: 2 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
rewards: {
|
||||
draws: 1,
|
||||
table: [
|
||||
{ type: "coins", amount: 120, weight: 70 },
|
||||
{ type: "coins", amount: 180, weight: 30 },
|
||||
],
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
mobs: {
|
||||
draws: 1,
|
||||
table: [
|
||||
{ mobKey: "orc", weight: 40 },
|
||||
{ mobKey: "troll", weight: 35 },
|
||||
{ mobKey: "dragonling", weight: 25 },
|
||||
],
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
metadata: { suggestedHp: 150 } as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
// Nuevos mobs avanzados
|
||||
const extraMobs = [
|
||||
{ key: "orc", name: "Orco", stats: { attack: 12, defense: 2 } },
|
||||
{ key: "troll", name: "Trol", stats: { attack: 20, defense: 4 } },
|
||||
{
|
||||
key: "dragonling",
|
||||
name: "Dragoncito",
|
||||
stats: { attack: 35, defense: 6 },
|
||||
},
|
||||
];
|
||||
for (const m of extraMobs) {
|
||||
await upsertMob(guildId, m.key, {
|
||||
name: m.name,
|
||||
stats: m.stats as unknown as Prisma.InputJsonValue,
|
||||
drops: Prisma.DbNull,
|
||||
});
|
||||
}
|
||||
|
||||
// Programar ataques extra de mobs nuevos para pruebas (si existe user objetivo)
|
||||
if (targetUser) {
|
||||
const orc = await prisma.mob.findFirst({
|
||||
where: { key: "orc", OR: [{ guildId }, { guildId: null }] },
|
||||
orderBy: [{ guildId: "desc" }],
|
||||
});
|
||||
const dragon = await prisma.mob.findFirst({
|
||||
where: { key: "dragonling", OR: [{ guildId }, { guildId: null }] },
|
||||
orderBy: [{ guildId: "desc" }],
|
||||
});
|
||||
const now = Date.now();
|
||||
const extraAttacks: Prisma.ScheduledMobAttackCreateManyInput[] = [];
|
||||
if (orc) {
|
||||
extraAttacks.push({
|
||||
userId: targetUser,
|
||||
guildId: guildId ?? "global",
|
||||
mobId: orc.id,
|
||||
scheduleAt: new Date(now + 25_000),
|
||||
});
|
||||
}
|
||||
if (dragon) {
|
||||
extraAttacks.push({
|
||||
userId: targetUser,
|
||||
guildId: guildId ?? "global",
|
||||
mobId: dragon.id,
|
||||
scheduleAt: new Date(now + 40_000),
|
||||
});
|
||||
}
|
||||
if (extraAttacks.length) {
|
||||
await prisma.scheduledMobAttack.createMany({ data: extraAttacks });
|
||||
}
|
||||
}
|
||||
|
||||
// Insertar un efecto FATIGUE de prueba (15% por 30 min) para validar penalización de monedas y reducción de stats
|
||||
if (targetUser) {
|
||||
const expires = new Date(Date.now() + 30 * 60 * 1000);
|
||||
await prisma.playerStatusEffect.upsert({
|
||||
where: {
|
||||
userId_guildId_type: {
|
||||
userId: targetUser,
|
||||
guildId: guildId ?? "global",
|
||||
type: "FATIGUE",
|
||||
},
|
||||
},
|
||||
update: { magnitude: 0.15, expiresAt: expires },
|
||||
create: {
|
||||
userId: targetUser,
|
||||
guildId: guildId ?? "global",
|
||||
type: "FATIGUE",
|
||||
magnitude: 0.15,
|
||||
expiresAt: expires,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Asegurar PlayerState base para el usuario de prueba
|
||||
if (targetUser) {
|
||||
await prisma.playerState.upsert({
|
||||
where: {
|
||||
userId_guildId: { userId: targetUser, guildId: guildId ?? "global" },
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
userId: targetUser,
|
||||
guildId: guildId ?? "global",
|
||||
hp: 100,
|
||||
maxHp: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("[seed:minigames] done");
|
||||
}
|
||||
|
||||
main().then(() => process.exit(0)).catch((e) => { console.error(e); process.exit(1); });
|
||||
main()
|
||||
.then(() => process.exit(0))
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
getEffectiveStats,
|
||||
adjustHP,
|
||||
ensurePlayerState,
|
||||
getEquipment,
|
||||
} from "../combat/equipmentService"; // 🟩 local authoritative
|
||||
import { logToolBreak } from "../lib/toolBreakLog";
|
||||
import { updateStats } from "../stats/service"; // 🟩 local authoritative
|
||||
@@ -269,7 +270,8 @@ async function sampleMobs(mobs?: MobsTable): Promise<string[]> {
|
||||
async function reduceToolDurability(
|
||||
userId: string,
|
||||
guildId: string,
|
||||
toolKey: string
|
||||
toolKey: string,
|
||||
usage: "gather" | "combat" = "gather"
|
||||
) {
|
||||
const { item, entry } = await getInventoryEntry(userId, guildId, toolKey);
|
||||
if (!entry)
|
||||
@@ -298,6 +300,11 @@ async function reduceToolDurability(
|
||||
// Valores base
|
||||
const maxConfigured = Math.max(1, breakable.maxDurability ?? 1);
|
||||
let perUse = Math.max(1, breakable.durabilityPerUse ?? 1);
|
||||
// Ajuste: en combate degradamos menos para evitar roturas instantáneas de armas caras
|
||||
if (usage === "combat") {
|
||||
// Reducimos a la mitad (redondeo hacia arriba mínimo 1)
|
||||
perUse = Math.max(1, Math.ceil(perUse * 0.5));
|
||||
}
|
||||
|
||||
// Protección: si perUse > maxDurability asumimos configuración errónea y lo reducimos a 1
|
||||
// (en lugar de romper inmediatamente el ítem). Si quieres que se rompa de un uso, define maxDurability igual a 1.
|
||||
@@ -758,10 +765,49 @@ export async function runMinigame(
|
||||
}
|
||||
|
||||
// Registrar la ejecución
|
||||
let weaponToolInfo: RunResult["weaponTool"] | undefined;
|
||||
// Si hubo combate y el jugador tenía un arma equipada distinta de la herramienta de recolección, degradarla.
|
||||
if (combatSummary && combatSummary.mobs.length > 0) {
|
||||
try {
|
||||
const { weapon } = await getEquipment(userId, guildId);
|
||||
if (weapon) {
|
||||
const weaponProps = parseItemProps(weapon.props);
|
||||
if (weaponProps?.tool?.type === "sword") {
|
||||
// Evitar degradar dos veces si la herramienta principal ya era la espada usada para recoger (no aplica en mina/pesca normalmente)
|
||||
const alreadyMain = toolInfo?.key === weapon.key;
|
||||
if (!alreadyMain) {
|
||||
const wt = await reduceToolDurability(
|
||||
userId,
|
||||
guildId,
|
||||
weapon.key,
|
||||
"combat"
|
||||
);
|
||||
weaponToolInfo = {
|
||||
key: weapon.key,
|
||||
durabilityDelta: wt.delta,
|
||||
broken: wt.broken,
|
||||
remaining: wt.remaining,
|
||||
max: wt.max,
|
||||
brokenInstance: wt.brokenInstance,
|
||||
instancesRemaining: wt.instancesRemaining,
|
||||
toolSource: "equipped",
|
||||
};
|
||||
} else {
|
||||
// Si la espada era también la herramienta (pelear) ya se degradó en la fase de tool principal
|
||||
weaponToolInfo = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// silencioso
|
||||
}
|
||||
}
|
||||
|
||||
const resultJson: Prisma.InputJsonValue = {
|
||||
rewards: delivered,
|
||||
mobs: mobsSpawned,
|
||||
tool: toolInfo,
|
||||
weaponTool: weaponToolInfo,
|
||||
combat: combatSummary,
|
||||
rewardModifiers,
|
||||
notes: "auto",
|
||||
@@ -810,6 +856,7 @@ export async function runMinigame(
|
||||
rewards: delivered,
|
||||
mobs: mobsSpawned,
|
||||
tool: toolInfo,
|
||||
weaponTool: weaponToolInfo,
|
||||
combat: combatSummary,
|
||||
rewardModifiers,
|
||||
};
|
||||
|
||||
@@ -61,6 +61,17 @@ export type RunResult = {
|
||||
instancesRemaining?: number; // instancias que quedan después del uso
|
||||
toolSource?: "provided" | "equipped" | "auto"; // origen de la selección
|
||||
};
|
||||
// Nueva: arma usada en combate (se degrada con un multiplicador menor para evitar roturas instantáneas)
|
||||
weaponTool?: {
|
||||
key?: string;
|
||||
durabilityDelta?: number;
|
||||
broken?: boolean;
|
||||
remaining?: number;
|
||||
max?: number;
|
||||
brokenInstance?: boolean;
|
||||
instancesRemaining?: number;
|
||||
toolSource?: "equipped"; // siempre proviene del slot de arma
|
||||
};
|
||||
combat?: CombatSummary; // resumen de combate si hubo mobs y se procesó
|
||||
// Modificadores aplicados a las recompensas (ej: penalización por FATIGUE sobre monedas)
|
||||
rewardModifiers?: {
|
||||
|
||||
Reference in New Issue
Block a user