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:
253
scripts/migrateStackableToInstanced.ts
Normal file
253
scripts/migrateStackableToInstanced.ts
Normal 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();
|
||||
});
|
||||
Reference in New Issue
Block a user