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,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);
}
},
};

View 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"
}`
);
}
},
};

View File

@@ -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,