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:
@@ -70,8 +70,28 @@ export const command: CommandMessage = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build select options (max 25)
|
||||
const options = inventory
|
||||
// Determine which slots the user actually has items for (based on item.tags)
|
||||
const slotsSet = new Set<string>();
|
||||
for (const inv of inventory) {
|
||||
const tags = Array.isArray(inv.item.tags) ? inv.item.tags : [];
|
||||
if (tags.includes("weapon")) slotsSet.add("weapon");
|
||||
if (tags.includes("armor")) slotsSet.add("armor");
|
||||
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,
|
||||
@@ -79,38 +99,53 @@ export const command: CommandMessage = {
|
||||
description: inv.item.key,
|
||||
}));
|
||||
|
||||
const slotSelect = {
|
||||
type: ComponentType.StringSelect,
|
||||
custom_id: "equip_slot_select",
|
||||
placeholder: "Selecciona el slot (weapon / armor / cape)",
|
||||
min_values: 1,
|
||||
max_values: 1,
|
||||
options: [
|
||||
{
|
||||
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 = {
|
||||
type: ComponentType.StringSelect,
|
||||
custom_id: "equip_slot_select",
|
||||
placeholder: "Selecciona el slot",
|
||||
min_values: 1,
|
||||
max_values: 1,
|
||||
options: slotOptions,
|
||||
} 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 = {
|
||||
type: ComponentType.StringSelect,
|
||||
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,
|
||||
max_values: 1,
|
||||
options,
|
||||
options: initialItemOptions,
|
||||
} as any;
|
||||
|
||||
const channel = message.channel as TextBasedChannel & { send: Function };
|
||||
@@ -130,7 +165,7 @@ export const command: CommandMessage = {
|
||||
i.user.id === message.author.id,
|
||||
});
|
||||
|
||||
let selectedSlot: string | null = null;
|
||||
let selectedSlot: string | null = initialSelectedSlot;
|
||||
let selectedItemId: string | null = null;
|
||||
|
||||
collector.on("collect", async (i: MessageComponentInteraction) => {
|
||||
@@ -138,11 +173,25 @@ export const command: CommandMessage = {
|
||||
await i.deferUpdate();
|
||||
if (i.customId === "equip_slot_select") {
|
||||
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 {
|
||||
await prompt.edit({
|
||||
content: `Slot seleccionado: **${selectedSlot}**`,
|
||||
components: prompt.components,
|
||||
components: [
|
||||
{ type: ComponentType.ActionRow, components: [newSlotSelect] },
|
||||
{ type: ComponentType.ActionRow, components: [newItemSelect] },
|
||||
],
|
||||
});
|
||||
} catch {}
|
||||
return;
|
||||
@@ -161,6 +210,24 @@ export const command: CommandMessage = {
|
||||
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
|
||||
try {
|
||||
await setEquipmentSlot(
|
||||
|
||||
@@ -66,6 +66,10 @@ export const command: CommandMessage = {
|
||||
providedTool ?? (await findBestToolKey(userId, guildId, "sword"));
|
||||
|
||||
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, {
|
||||
toolKey: toolKey ?? undefined,
|
||||
});
|
||||
@@ -206,6 +210,46 @@ export const command: CommandMessage = {
|
||||
const display = buildDisplay(FIGHT_ACCENT, blocks);
|
||||
await sendDisplayReply(message, display);
|
||||
} 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}`);
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user