Files
amayo/src/commands/messages/game/mobCreate.ts
Shni f36fa24e46 Refactor item property parsing and centralize utility functions
- Moved `parseItemProps` function to `core/utils.ts` for reuse across modules.
- Updated various services to import and utilize the centralized `parseItemProps`.
- Introduced new utility functions for handling consumable cooldowns and healing calculations.
- Enhanced mob management with a new repository system, allowing for dynamic loading and validation of mob definitions from the database.
- Added admin functions for creating, updating, listing, and deleting mobs, with validation using Zod.
- Implemented tests for mob management functionalities.
- Improved error handling and logging throughout the mob and consumable services.
2025-10-14 12:57:53 -05:00

482 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
Message,
MessageFlags,
MessageComponentInteraction,
ButtonInteraction,
TextBasedChannel,
} from "discord.js";
import {
ComponentType,
TextInputStyle,
ButtonStyle,
} from "discord-api-types/v10";
import type { CommandMessage } from "../../../core/types/commands";
import { hasManageGuildOrStaff } from "../../../core/lib/permissions";
import logger from "../../../core/lib/logger";
import type Amayo from "../../../core/client";
interface MobEditorState {
key: string;
name?: string;
category?: string;
stats?: any; // JSON libre, ej: { attack, hp, defense }
drops?: any; // JSON libre, tabla de recompensas
}
function createMobDisplay(state: MobEditorState, editing: boolean = false) {
const title = editing ? "Editando Mob" : "Creando Mob";
const stats = state.stats || {};
return {
type: 17,
accent_color: 0xff0000,
components: [
{
type: 9,
components: [
{
type: 10,
content: `👹 **${title}: \`${state.key}\`**`,
},
],
},
{ type: 14, divider: true },
{
type: 9,
components: [
{
type: 10,
content:
`**📋 Estado Actual:**\n` +
`**Nombre:** ${state.name || "❌ No configurado"}\n` +
`**Categoría:** ${state.category || "Sin categoría"}\n` +
`**Attack:** ${stats.attack || 0}\n` +
`**HP:** ${stats.hp || 0}\n` +
`**Defense:** ${stats.defense || 0}\n` +
`**Drops:** ${Object.keys(state.drops || {}).length} items`,
},
],
},
{ type: 14, divider: true },
{
type: 9,
components: [
{
type: 10,
content:
`**🎮 Instrucciones:**\n` +
`• **Base**: Nombre y categoría\n` +
`• **Stats (JSON)**: Estadísticas del mob\n` +
`• **Drops (JSON)**: Items que dropea\n` +
`• **Guardar**: Confirma los cambios\n` +
`• **Cancelar**: Descarta los cambios`,
},
],
},
],
};
}
export const command: CommandMessage = {
name: "mob-crear",
type: "message",
aliases: ["crear-mob", "mobcreate"],
cooldown: 10,
description:
"Crea un Mob (enemigo) para este servidor con editor interactivo.",
category: "Minijuegos",
usage: "mob-crear <key-única>",
run: async (message: Message, args: string[], client: Amayo) => {
const allowed = await hasManageGuildOrStaff(
message.member,
message.guild!.id,
client.prisma
);
if (!allowed) {
await message.reply(
"❌ No tienes permisos de ManageGuild ni rol de staff."
);
return;
}
const key = args[0]?.trim();
if (!key) {
await message.reply("Uso: `!mob-crear <key-única>`");
return;
}
const guildId = message.guild!.id;
const exists = await client.prisma.mob.findFirst({
where: { key, guildId },
});
if (exists) {
await message.reply("❌ Ya existe un mob con esa key.");
return;
}
const state: MobEditorState = { key, stats: { attack: 5 }, drops: {} };
const channel = message.channel as TextBasedChannel & { send: Function };
const editorMsg = await channel.send({
content: `👾 Editor de Mob: \`${key}\``,
components: [
{
type: 1,
components: [
{
type: 2,
style: ButtonStyle.Primary,
label: "Base",
custom_id: "mb_base",
},
{
type: 2,
style: ButtonStyle.Secondary,
label: "Stats (JSON)",
custom_id: "mb_stats",
},
{
type: 2,
style: ButtonStyle.Secondary,
label: "Drops (JSON)",
custom_id: "mb_drops",
},
{
type: 2,
style: ButtonStyle.Success,
label: "Guardar",
custom_id: "mb_save",
},
{
type: 2,
style: ButtonStyle.Danger,
label: "Cancelar",
custom_id: "mb_cancel",
},
],
},
],
});
const collector = editorMsg.createMessageComponentCollector({
time: 30 * 60_000,
filter: (i) => i.user.id === message.author.id,
});
collector.on("collect", async (i: MessageComponentInteraction) => {
try {
if (!i.isButton()) return;
if (i.customId === "mb_cancel") {
await i.deferUpdate();
await editorMsg.edit({
flags: 32768,
components: [
{
type: 17,
accent_color: 0xff0000,
components: [
{
type: 9,
components: [
{
type: 10,
content: "**❌ Editor cancelado.**",
},
],
},
],
},
],
});
collector.stop("cancel");
return;
}
if (i.customId === "mb_base") {
await showBaseModal(i as ButtonInteraction, state, editorMsg, false);
return;
}
if (i.customId === "mb_stats") {
await showJsonModal(
i as ButtonInteraction,
state,
"stats",
"Stats del Mob (JSON)",
editorMsg,
false
);
return;
}
if (i.customId === "mb_drops") {
await showJsonModal(
i as ButtonInteraction,
state,
"drops",
"Drops del Mob (JSON)",
editorMsg,
false
);
return;
}
if (i.customId === "mb_save") {
if (!state.name) {
await i.reply({
content: "❌ Falta el nombre del mob.",
flags: MessageFlags.Ephemeral,
});
return;
}
// Use centralized admin createOrUpdate to persist mob (returns row when possible)
try {
const { createOrUpdateMob } = await import(
"../../../game/mobs/admin.js"
);
await createOrUpdateMob({ ...(state as any), guildId });
await i.reply({
content: "✅ Mob guardado!",
flags: MessageFlags.Ephemeral,
});
} catch (e) {
// fallback to direct Prisma if admin module not available
await client.prisma.mob.create({
data: {
guildId,
key: state.key,
name: state.name!,
category: state.category ?? null,
stats: state.stats ?? {},
drops: state.drops ?? {},
},
});
await i.reply({
content: "✅ Mob guardado (fallback)!",
flags: MessageFlags.Ephemeral,
});
}
await editorMsg.edit({
flags: 32768,
components: [
{
type: 17,
accent_color: 0x00ff00,
components: [
{
type: 9,
components: [
{
type: 10,
content: `**✅ Mob \`${state.key}\` creado exitosamente.**`,
},
],
},
],
},
],
});
collector.stop("saved");
return;
}
} catch (err) {
logger.error({ err }, "mob-crear");
if (!i.deferred && !i.replied)
await i.reply({
content: "❌ Error procesando la acción.",
flags: MessageFlags.Ephemeral,
});
}
});
collector.on("end", async (_c, r) => {
if (r === "time") {
try {
await editorMsg.edit({
content: "⏰ Editor expirado.",
components: [],
});
} catch {}
}
});
},
};
async function showBaseModal(
i: ButtonInteraction,
state: MobEditorState,
editorMsg: Message,
editing: boolean
) {
const modal = {
title: "Base del Mob",
customId: "mb_base_modal",
components: [
{
type: ComponentType.Label,
label: "Nombre",
component: {
type: ComponentType.TextInput,
customId: "name",
style: TextInputStyle.Short,
required: true,
value: state.name ?? "",
},
},
{
type: ComponentType.Label,
label: "Categoría (opcional)",
component: {
type: ComponentType.TextInput,
customId: "category",
style: TextInputStyle.Short,
required: false,
value: state.category ?? "",
},
},
],
} as const;
await i.showModal(modal);
try {
const sub = await i.awaitModalSubmit({ time: 300_000 });
state.name = sub.components.getTextInputValue("name").trim();
const cat = sub.components.getTextInputValue("category")?.trim();
state.category = cat || undefined;
await sub.reply({
content: "✅ Base actualizada.",
flags: MessageFlags.Ephemeral,
});
// Refresh display
const newDisplay = createMobDisplay(state, editing);
await editorMsg.edit({
flags: 32768,
components: [
newDisplay,
{
type: 1,
components: [
{
type: 2,
style: ButtonStyle.Primary,
label: "Base",
custom_id: "mb_base",
},
{
type: 2,
style: ButtonStyle.Secondary,
label: "Stats (JSON)",
custom_id: "mb_stats",
},
{
type: 2,
style: ButtonStyle.Secondary,
label: "Drops (JSON)",
custom_id: "mb_drops",
},
{
type: 2,
style: ButtonStyle.Success,
label: "Guardar",
custom_id: "mb_save",
},
{
type: 2,
style: ButtonStyle.Danger,
label: "Cancelar",
custom_id: "mb_cancel",
},
],
},
],
});
} catch {}
}
async function showJsonModal(
i: ButtonInteraction,
state: MobEditorState,
field: "stats" | "drops",
title: string,
editorMsg: Message,
editing: boolean
) {
const current = JSON.stringify(state[field] ?? {});
const modal = {
title,
customId: `mb_json_${field}`,
components: [
{
type: ComponentType.Label,
label: "JSON",
component: {
type: ComponentType.TextInput,
customId: "json",
style: TextInputStyle.Paragraph,
required: false,
value: current.slice(0, 4000),
},
},
],
} as const;
await i.showModal(modal);
try {
const sub = await i.awaitModalSubmit({ time: 300_000 });
const raw = sub.components.getTextInputValue("json");
if (raw) {
try {
state[field] = JSON.parse(raw);
await sub.reply({
content: "✅ Guardado.",
flags: MessageFlags.Ephemeral,
});
} catch {
await sub.reply({
content: "❌ JSON inválido.",
flags: MessageFlags.Ephemeral,
});
return;
}
} else {
state[field] = {};
await sub.reply({ content: " Limpio.", flags: MessageFlags.Ephemeral });
}
// Refresh display
const newDisplay = createMobDisplay(state, editing);
await editorMsg.edit({
flags: 32768,
components: [
newDisplay,
{
type: 1,
components: [
{
type: 2,
style: ButtonStyle.Primary,
label: "Base",
custom_id: "mb_base",
},
{
type: 2,
style: ButtonStyle.Secondary,
label: "Stats (JSON)",
custom_id: "mb_stats",
},
{
type: 2,
style: ButtonStyle.Secondary,
label: "Drops (JSON)",
custom_id: "mb_drops",
},
{
type: 2,
style: ButtonStyle.Success,
label: "Guardar",
custom_id: "mb_save",
},
{
type: 2,
style: ButtonStyle.Danger,
label: "Cancelar",
custom_id: "mb_cancel",
},
],
},
],
});
} catch {}
}