- 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.
254 lines
7.4 KiB
TypeScript
254 lines
7.4 KiB
TypeScript
/**
|
||
* 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();
|
||
});
|