feat: Mejorar el flujo de equipamiento de items, añadiendo validaciones y opciones interactivas para seleccionar slots y items del inventario.

This commit is contained in:
Shni
2025-10-14 15:27:52 -05:00
parent 7a080f9b71
commit 8f5beee709
2 changed files with 142 additions and 31 deletions

View File

@@ -70,47 +70,82 @@ export const command: CommandMessage = {
return; return;
} }
// Build select options (max 25) // Determine which slots the user actually has items for (based on item.tags)
const options = inventory const slotsSet = new Set<string>();
.slice(0, 25) for (const inv of inventory) {
.map((inv) => ({ const tags = Array.isArray(inv.item.tags) ? inv.item.tags : [];
label: inv.item.name || inv.item.key, if (tags.includes("weapon")) slotsSet.add("weapon");
value: inv.item.id, if (tags.includes("armor")) slotsSet.add("armor");
description: inv.item.key, if (tags.includes("cape")) slotsSet.add("cape");
})); }
const availableSlots = Array.from(slotsSet);
if (availableSlots.length === 0) {
await message.reply(
"❌ No tienes items equipables en el inventario (weapon/armor/cape)."
);
return;
}
const buildItemOptionsForSlot = (slot: string) =>
inventory
.filter((inv) => {
const tags = Array.isArray(inv.item.tags) ? inv.item.tags : [];
return tags.includes(slot);
})
.slice(0, 25)
.map((inv) => ({
label: inv.item.name || inv.item.key,
value: inv.item.id,
description: inv.item.key,
}));
const slotOptions = availableSlots.map((s) => {
if (s === "weapon")
return {
label: "Weapon (arma)",
value: "weapon",
description: "Equipar como arma",
};
if (s === "armor")
return {
label: "Armor (armadura)",
value: "armor",
description: "Equipar como armadura",
};
return {
label: "Cape (capa)",
value: "cape",
description: "Equipar como capa",
};
});
const slotSelect = { const slotSelect = {
type: ComponentType.StringSelect, type: ComponentType.StringSelect,
custom_id: "equip_slot_select", custom_id: "equip_slot_select",
placeholder: "Selecciona el slot (weapon / armor / cape)", placeholder: "Selecciona el slot",
min_values: 1, min_values: 1,
max_values: 1, max_values: 1,
options: [ options: slotOptions,
{
label: "Weapon (arma)",
value: "weapon",
description: "Equipar como arma",
},
{
label: "Armor (armadura)",
value: "armor",
description: "Equipar como armadura",
},
{
label: "Cape (capa)",
value: "cape",
description: "Equipar como capa",
},
],
} as any; } as any;
// If only one slot available, preselect it and build item options for it.
let initialSelectedSlot: string | null = null;
if (availableSlots.length === 1) initialSelectedSlot = availableSlots[0];
const initialItemOptions = initialSelectedSlot
? buildItemOptionsForSlot(initialSelectedSlot)
: // default to first slot's items so the select is populated
buildItemOptionsForSlot(availableSlots[0]);
const itemSelect = { const itemSelect = {
type: ComponentType.StringSelect, type: ComponentType.StringSelect,
custom_id: "equip_item_select", custom_id: "equip_item_select",
placeholder: "Selecciona el item a equipar", placeholder: initialSelectedSlot
? "Selecciona el item a equipar"
: "Selecciona primero el slot (o usa el slot disponible)",
min_values: 1, min_values: 1,
max_values: 1, max_values: 1,
options, options: initialItemOptions,
} as any; } as any;
const channel = message.channel as TextBasedChannel & { send: Function }; const channel = message.channel as TextBasedChannel & { send: Function };
@@ -130,7 +165,7 @@ export const command: CommandMessage = {
i.user.id === message.author.id, i.user.id === message.author.id,
}); });
let selectedSlot: string | null = null; let selectedSlot: string | null = initialSelectedSlot;
let selectedItemId: string | null = null; let selectedItemId: string | null = null;
collector.on("collect", async (i: MessageComponentInteraction) => { collector.on("collect", async (i: MessageComponentInteraction) => {
@@ -138,11 +173,25 @@ export const command: CommandMessage = {
await i.deferUpdate(); await i.deferUpdate();
if (i.customId === "equip_slot_select") { if (i.customId === "equip_slot_select") {
selectedSlot = (i as StringSelectMenuInteraction).values[0]; selectedSlot = (i as StringSelectMenuInteraction).values[0];
// inform user visually by editing the prompt to show selection // rebuild item select options for the chosen slot
const newItemOptions = buildItemOptionsForSlot(selectedSlot);
const newSlotSelect =
prompt.components[0]?.components?.[0] ?? slotSelect;
const newItemSelect = {
type: ComponentType.StringSelect,
custom_id: "equip_item_select",
placeholder: "Selecciona el item a equipar",
min_values: 1,
max_values: 1,
options: newItemOptions,
} as any;
try { try {
await prompt.edit({ await prompt.edit({
content: `Slot seleccionado: **${selectedSlot}**`, content: `Slot seleccionado: **${selectedSlot}**`,
components: prompt.components, components: [
{ type: ComponentType.ActionRow, components: [newSlotSelect] },
{ type: ComponentType.ActionRow, components: [newItemSelect] },
],
}); });
} catch {} } catch {}
return; return;
@@ -161,6 +210,24 @@ export const command: CommandMessage = {
return; return;
} }
// validate that the selected item belongs to the chosen slot
const chosenItem = await prisma.economyItem.findUnique({
where: { id: selectedItemId },
});
const chosenTags = Array.isArray(chosenItem?.tags)
? chosenItem!.tags
: [];
if (!chosenTags.includes(selectedSlot)) {
try {
await prompt.edit({
content: `❌ Ese ítem no puede equiparse en el slot **${selectedSlot}**.`,
components: [],
});
} catch {}
collector.stop();
return;
}
// perform equip // perform equip
try { try {
await setEquipmentSlot( await setEquipmentSlot(

View File

@@ -66,6 +66,10 @@ export const command: CommandMessage = {
providedTool ?? (await findBestToolKey(userId, guildId, "sword")); providedTool ?? (await findBestToolKey(userId, guildId, "sword"));
try { try {
// Pre-check: si el nivel tiene requirements con herramienta obligatoria
// intentamos validar rápidamente si existe alguna herramienta equipada o en inventario
// usando validateRequirements indirectamente sería duplicar lógica; hacemos una ligera comprobación
// basada en `toolKey` detectado y si findBestToolKey falla, informaremos.
const result = await runMinigame(userId, guildId, area.key, level, { const result = await runMinigame(userId, guildId, area.key, level, {
toolKey: toolKey ?? undefined, toolKey: toolKey ?? undefined,
}); });
@@ -206,6 +210,46 @@ export const command: CommandMessage = {
const display = buildDisplay(FIGHT_ACCENT, blocks); const display = buildDisplay(FIGHT_ACCENT, blocks);
await sendDisplayReply(message, display); await sendDisplayReply(message, display);
} catch (e: any) { } catch (e: any) {
const msg = (e?.message || String(e)).toLowerCase();
// Mapear errores conocidos a mensajes más amigables
if (
msg.includes("area no encontrada") ||
msg.includes("nivel no encontrado")
) {
await message.reply(
"⚠️ El área o nivel especificado no existe para este servidor."
);
return;
}
if (msg.includes("cooldown activo")) {
await message.reply(
"⏳ Estás en cooldown para esta actividad. Intenta más tarde."
);
return;
}
if (
msg.includes("se requiere una herramienta adecuada") ||
msg.includes("no tienes la herramienta") ||
msg.includes("tipo de herramienta incorrecto") ||
msg.includes("tier de herramienta insuficiente")
) {
// Mensaje más específico: si no hay arma equipada y el área requiere saberlo, sugerir equipar o conseguir herramienta
await message.reply(
"⚠️ No tienes una herramienta válida para esta actividad. Equipa una herramienta adecuada (ej: espada) o especifica `toolKey`."
);
return;
}
if (
msg.includes("no puede infligir daño") ||
msg.includes("autoDefeatNoWeapon") ||
msg.includes("auto defeat")
) {
await message.reply(
"⚠️ No tienes un arma equipada o válida para pelear. Equipa un arma para poder infligir daño o usa `pelear <toolKey>` con una herramienta válida."
);
return;
}
// Fallback genérico
await message.reply(`❌ No se pudo pelear: ${e?.message ?? e}`); await message.reply(`❌ No se pudo pelear: ${e?.message ?? e}`);
} }
}, },