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:
97
src/commands/messages/admin/debugInv.ts
Normal file
97
src/commands/messages/admin/debugInv.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { CommandMessage } from "../../../core/types/commands";
|
||||
import type Amayo from "../../../core/client";
|
||||
import { prisma } from "../../../core/database/prisma";
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: "debug-inv",
|
||||
type: "message",
|
||||
aliases: ["dinv"],
|
||||
cooldown: 0,
|
||||
category: "Admin",
|
||||
description: "Muestra información detallada del inventario para debug.",
|
||||
usage: "debug-inv [@user]",
|
||||
run: async (message, args, _client: Amayo) => {
|
||||
if (message.author.id !== process.env.OWNER_ID) {
|
||||
await message.reply("❌ Solo el owner puede usar este comando.");
|
||||
return;
|
||||
}
|
||||
|
||||
const targetUser = message.mentions.users.first() ?? message.author;
|
||||
const userId = targetUser.id;
|
||||
const guildId = message.guild!.id;
|
||||
|
||||
const entries = await prisma.inventoryEntry.findMany({
|
||||
where: { userId, guildId },
|
||||
include: { item: true },
|
||||
});
|
||||
|
||||
let output = `🔍 **Inventario de <@${userId}>**\n\n`;
|
||||
|
||||
for (const entry of entries) {
|
||||
const item = entry.item;
|
||||
const state = entry.state as any;
|
||||
const instances = state?.instances ?? [];
|
||||
const props = item.props as any;
|
||||
|
||||
output += `**${item.name}** (\`${item.key}\`)\n`;
|
||||
output += `• Stackable: ${item.stackable}\n`;
|
||||
output += `• Quantity: ${entry.quantity}\n`;
|
||||
output += `• Instances: ${instances.length}\n`;
|
||||
|
||||
if (props?.breakable) {
|
||||
output += `• Breakable: enabled=${
|
||||
props.breakable.enabled !== false
|
||||
}, max=${props.breakable.maxDurability}\n`;
|
||||
}
|
||||
|
||||
if (instances.length > 0) {
|
||||
instances.forEach((inst: any, idx: number) => {
|
||||
output += ` └ [${idx}] dur: ${inst.durability ?? "N/A"}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
if (!item.stackable && entry.quantity > 1 && instances.length === 0) {
|
||||
output += `⚠️ **CORRUPTO**: Non-stackable con qty>1 sin instances\n`;
|
||||
}
|
||||
|
||||
output += "\n";
|
||||
}
|
||||
|
||||
// Verificar equipo
|
||||
const equipment = await prisma.playerEquipment.findUnique({
|
||||
where: { userId_guildId: { userId, guildId } },
|
||||
});
|
||||
|
||||
if (equipment) {
|
||||
output += `🧰 **Equipo:**\n`;
|
||||
if (equipment.weaponItemId) {
|
||||
const weapon = await prisma.economyItem.findUnique({
|
||||
where: { id: equipment.weaponItemId },
|
||||
});
|
||||
output += `• Arma: ${weapon?.name ?? "Desconocida"}\n`;
|
||||
} else {
|
||||
output += `• Arma: ❌ NINGUNA\n`;
|
||||
}
|
||||
|
||||
if (equipment.armorItemId) {
|
||||
const armor = await prisma.economyItem.findUnique({
|
||||
where: { id: equipment.armorItemId },
|
||||
});
|
||||
output += `• Armadura: ${armor?.name ?? "Desconocida"}\n`;
|
||||
}
|
||||
|
||||
if (equipment.capeItemId) {
|
||||
const cape = await prisma.economyItem.findUnique({
|
||||
where: { id: equipment.capeItemId },
|
||||
});
|
||||
output += `• Capa: ${cape?.name ?? "Desconocida"}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Dividir en chunks si es muy largo
|
||||
const chunks = output.match(/[\s\S]{1,1900}/g) ?? [output];
|
||||
for (const chunk of chunks) {
|
||||
await message.reply(chunk);
|
||||
}
|
||||
},
|
||||
};
|
||||
185
src/commands/messages/admin/resetInventory.ts
Normal file
185
src/commands/messages/admin/resetInventory.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import type { CommandMessage } from "../../../core/types/commands";
|
||||
import type Amayo from "../../../core/client";
|
||||
import { prisma } from "../../../core/database/prisma";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
type ItemProps = {
|
||||
breakable?: {
|
||||
enabled?: boolean;
|
||||
maxDurability?: number;
|
||||
durabilityPerUse?: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
};
|
||||
|
||||
type InventoryState = {
|
||||
instances?: Array<{
|
||||
durability?: number;
|
||||
expiresAt?: string;
|
||||
notes?: string;
|
||||
mutations?: string[];
|
||||
}>;
|
||||
notes?: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: "reset-inventory",
|
||||
type: "message",
|
||||
aliases: ["resetinv", "fix-stackable"],
|
||||
cooldown: 0,
|
||||
category: "Admin",
|
||||
description:
|
||||
"Resetea el inventario de herramientas/armas de un usuario para migrar de stackable a non-stackable con durabilidad.",
|
||||
usage: "reset-inventory [@user]",
|
||||
run: async (message, args, _client: Amayo) => {
|
||||
// Solo el owner del bot puede ejecutar esto
|
||||
if (message.author.id !== process.env.OWNER_ID) {
|
||||
await message.reply("❌ Solo el owner del bot puede usar este comando.");
|
||||
return;
|
||||
}
|
||||
|
||||
const targetUser = message.mentions.users.first() ?? message.author;
|
||||
const guildId = message.guild!.id;
|
||||
const userId = targetUser.id;
|
||||
|
||||
await message.reply(
|
||||
`🔄 Iniciando reseteo de inventario para <@${userId}>...`
|
||||
);
|
||||
|
||||
try {
|
||||
// Paso 1: Obtener todos los items non-stackable (herramientas/armas/armaduras/capas)
|
||||
const nonStackableItems = await prisma.economyItem.findMany({
|
||||
where: {
|
||||
stackable: false,
|
||||
OR: [
|
||||
{ key: { startsWith: "tool." } },
|
||||
{ key: { startsWith: "weapon." } },
|
||||
{ key: { startsWith: "armor." } },
|
||||
{ key: { startsWith: "cape." } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
let migratedCount = 0;
|
||||
let deletedCount = 0;
|
||||
let recreatedCount = 0;
|
||||
|
||||
for (const item of nonStackableItems) {
|
||||
const entry = await prisma.inventoryEntry.findUnique({
|
||||
where: {
|
||||
userId_guildId_itemId: { userId, guildId, itemId: item.id },
|
||||
},
|
||||
});
|
||||
|
||||
if (!entry) continue;
|
||||
|
||||
const props = (item.props as ItemProps | null) ?? {};
|
||||
const breakable = props.breakable;
|
||||
const maxDurability =
|
||||
breakable?.enabled !== false
|
||||
? breakable?.maxDurability ?? 100
|
||||
: undefined;
|
||||
|
||||
const currentState = (entry.state as InventoryState | null) ?? {};
|
||||
const currentInstances = currentState.instances ?? [];
|
||||
const currentQuantity = entry.quantity ?? 0;
|
||||
|
||||
// Si tiene quantity>1 sin instances, está corrupto
|
||||
if (currentQuantity > 1 && currentInstances.length === 0) {
|
||||
// Opción 1: Migrar (convertir quantity a instances)
|
||||
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++;
|
||||
}
|
||||
// Si tiene quantity=1 pero sin instancia, crear instancia
|
||||
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++;
|
||||
}
|
||||
// Si tiene instances pero sin durabilidad, inicializar
|
||||
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) {
|
||||
await prisma.inventoryEntry.update({
|
||||
where: { id: entry.id },
|
||||
data: {
|
||||
state: {
|
||||
...currentState,
|
||||
instances: fixedInstances,
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
quantity: fixedInstances.length,
|
||||
},
|
||||
});
|
||||
migratedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Paso 2: Actualizar items en DB para asegurar 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.%'
|
||||
`;
|
||||
|
||||
await message.reply(
|
||||
`✅ **Reseteo completado para <@${userId}>**\n` +
|
||||
`• Entradas migradas: ${migratedCount}\n` +
|
||||
`• Items actualizados en DB: ${itemUpdateResult}\n\n` +
|
||||
`El usuario puede volver a usar sus herramientas normalmente.`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error en reset-inventory:", error);
|
||||
await message.reply(
|
||||
`❌ Error durante el reseteo: ${
|
||||
error instanceof Error ? error.message : "Desconocido"
|
||||
}`
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -462,9 +462,12 @@ export async function runMinigame(
|
||||
const eff = await getEffectiveStats(userId, guildId);
|
||||
const playerState = await ensurePlayerState(userId, guildId);
|
||||
const startHp = eff.hp; // HP actual persistente
|
||||
// Regla: si el jugador no tiene arma (damage <=0) no puede infligir daño real y perderá automáticamente contra cualquier mob.
|
||||
// En lugar de simular rondas irreales con daño mínimo artificial, forzamos derrota directa manteniendo coherencia.
|
||||
if (!eff.damage || eff.damage <= 0) {
|
||||
|
||||
// ⚠️ CRÍTICO: Validar que el jugador tenga arma equipada ANTES de iniciar combate
|
||||
// Regla: si el jugador no tiene arma (damage <=0) no puede infligir daño real y perderá automáticamente.
|
||||
const hasWeapon = eff.damage > 0;
|
||||
|
||||
if (!hasWeapon) {
|
||||
// Registrar derrota simple contra la lista de mobs (no se derrotan mobs).
|
||||
const mobLogs: CombatSummary["mobs"] = mobsSpawned.map((mk) => ({
|
||||
mobKey: mk,
|
||||
|
||||
Reference in New Issue
Block a user