Files
amayo/scripts/migrateStackableToInstanced.ts
shni 79ece13420 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.
2025-10-09 02:47:29 -05:00

254 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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();
});