fix: resolve durability and combat issues with stackable items

- Updated database schema to set stackable items as non-stackable.
- Implemented migration script to convert existing stackable items to instances with durability.
- Fixed combat logic to ensure players lose if no weapon is equipped.
- Added admin commands for inventory debugging and resetting.
- Enhanced item display to show durability instead of quantity.
- Conducted thorough testing and validation of changes.
This commit is contained in:
2025-10-09 02:47:29 -05:00
parent e5801e49bd
commit 79ece13420
8 changed files with 1353 additions and 3 deletions

View File

@@ -0,0 +1,253 @@
/**
* Script de Migración: Stackable Items → Instanced Items con Durabilidad
*
* Problema:
* - Items de herramientas/armas en DB tienen stackable=true (error de versión antigua)
* - Inventarios tienen quantity>1 sin state.instances con durabilidad
* - Esto causa que reduceToolDurability decremente quantity en lugar de degradar durabilidad
*
* Solución:
* 1. Actualizar EconomyItem: stackable=false para tools/weapons/armor/capes
* 2. Migrar InventoryEntry: convertir quantity a state.instances[] con durabilidad inicializada
*/
import { PrismaClient, Prisma } from "@prisma/client";
const prisma = new PrismaClient();
type ItemProps = {
breakable?: {
enabled?: boolean;
maxDurability?: number;
durabilityPerUse?: number;
};
tool?: { type: string; tier?: number };
damage?: number;
defense?: number;
[k: string]: unknown;
};
type InventoryState = {
instances?: Array<{
durability?: number;
expiresAt?: string;
notes?: string;
mutations?: string[];
}>;
notes?: string;
[k: string]: unknown;
};
async function main() {
console.log("🔧 Iniciando migración de items stackable...\n");
// PASO 1: Actualizar definiciones de items
console.log("📝 PASO 1: Actualizando EconomyItem (stackable → false)...");
const itemUpdateResult = await prisma.$executeRaw`
UPDATE "EconomyItem"
SET "stackable" = false
WHERE "key" LIKE 'tool.%'
OR "key" LIKE 'weapon.%'
OR "key" LIKE 'armor.%'
OR "key" LIKE 'cape.%'
`;
console.log(`${itemUpdateResult} items actualizados\n`);
// PASO 2: Obtener items que ahora son non-stackable
const nonStackableItems = await prisma.economyItem.findMany({
where: {
stackable: false,
OR: [
{ key: { startsWith: "tool." } },
{ key: { startsWith: "weapon." } },
{ key: { startsWith: "armor." } },
{ key: { startsWith: "cape." } },
],
},
});
console.log(
`📦 ${nonStackableItems.length} items non-stackable identificados\n`
);
// PASO 3: Migrar inventarios
console.log("🔄 PASO 2: Migrando inventarios...");
let migratedCount = 0;
let skippedCount = 0;
let errorCount = 0;
for (const item of nonStackableItems) {
const props = (item.props as ItemProps | null) ?? {};
const breakable = props.breakable;
const maxDurability =
breakable?.enabled !== false
? breakable?.maxDurability ?? 100
: undefined;
// Encontrar todas las entradas de inventario de este item con quantity>1 o sin instances
const entries = await prisma.inventoryEntry.findMany({
where: { itemId: item.id },
});
for (const entry of entries) {
try {
const currentState = (entry.state as InventoryState | null) ?? {};
const currentInstances = currentState.instances ?? [];
const currentQuantity = entry.quantity ?? 0;
// Caso 1: quantity>1 pero sin instances (inventario corrupto de versión anterior)
if (currentQuantity > 1 && currentInstances.length === 0) {
console.log(
` 🔧 Migrando: ${item.key} (user=${entry.userId.slice(
0,
8
)}, qty=${currentQuantity})`
);
const newInstances: InventoryState["instances"] = [];
for (let i = 0; i < currentQuantity; i++) {
if (maxDurability && maxDurability > 0) {
newInstances.push({ durability: maxDurability });
} else {
newInstances.push({});
}
}
await prisma.inventoryEntry.update({
where: { id: entry.id },
data: {
state: {
...currentState,
instances: newInstances,
} as unknown as Prisma.InputJsonValue,
quantity: newInstances.length,
},
});
migratedCount++;
}
// Caso 2: Instancia única sin durabilidad inicializada
else if (currentQuantity === 1 && currentInstances.length === 0) {
const newInstance =
maxDurability && maxDurability > 0
? { durability: maxDurability }
: {};
await prisma.inventoryEntry.update({
where: { id: entry.id },
data: {
state: {
...currentState,
instances: [newInstance],
} as unknown as Prisma.InputJsonValue,
quantity: 1,
},
});
migratedCount++;
}
// Caso 3: Ya tiene instances pero sin durabilidad inicializada
else if (currentInstances.length > 0 && maxDurability) {
let needsUpdate = false;
const fixedInstances = currentInstances.map((inst) => {
if (inst.durability == null) {
needsUpdate = true;
return { ...inst, durability: maxDurability };
}
return inst;
});
if (needsUpdate) {
console.log(
` 🔧 Reparando durabilidad: ${
item.key
} (user=${entry.userId.slice(0, 8)}, instances=${
fixedInstances.length
})`
);
await prisma.inventoryEntry.update({
where: { id: entry.id },
data: {
state: {
...currentState,
instances: fixedInstances,
} as unknown as Prisma.InputJsonValue,
quantity: fixedInstances.length,
},
});
migratedCount++;
} else {
skippedCount++;
}
} else {
skippedCount++;
}
} catch (error) {
console.error(` ❌ Error migrando entry ${entry.id}:`, error);
errorCount++;
}
}
}
console.log("\n📊 Resumen de migración:");
console.log(` ✅ Entradas migradas: ${migratedCount}`);
console.log(` ⏭️ Entradas omitidas (ya correctas): ${skippedCount}`);
console.log(` ❌ Errores: ${errorCount}\n`);
// PASO 4: Validación post-migración
console.log("🔍 PASO 3: Validando integridad...");
const inconsistentEntries = await prisma.$queryRaw<
Array<{
id: string;
userId: string;
key: string;
quantity: number;
state: any;
}>
>`
SELECT
ie.id,
ie."userId",
ei.key,
ie.quantity,
ie.state
FROM "InventoryEntry" ie
JOIN "EconomyItem" ei ON ie."itemId" = ei.id
WHERE ei."stackable" = false
AND ie.quantity > 1
AND (
ie.state IS NULL
OR jsonb_array_length(COALESCE((ie.state->>'instances')::jsonb, '[]'::jsonb)) = 0
)
`;
if (inconsistentEntries.length > 0) {
console.log(
`\n⚠ ADVERTENCIA: ${inconsistentEntries.length} entradas inconsistentes detectadas:`
);
inconsistentEntries.forEach((entry) => {
console.log(
` - ${entry.key} (user=${entry.userId.slice(0, 8)}, qty=${
entry.quantity
})`
);
});
console.log(
"\n❗ Ejecuta el comando admin !reset-inventory para estos usuarios\n"
);
} else {
console.log("✅ No se detectaron inconsistencias\n");
}
console.log("🎉 Migración completada exitosamente");
}
main()
.catch((error) => {
console.error("❌ Error fatal durante migración:", error);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});