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.
This commit is contained in:
@@ -1,19 +1,25 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { hasManageGuildOrStaff } from '../../../core/lib/permissions';
|
||||
import { prisma } from '../../../core/database/prisma';
|
||||
import type { CommandMessage } from "../../../core/types/commands";
|
||||
import type Amayo from "../../../core/client";
|
||||
import { hasManageGuildOrStaff } from "../../../core/lib/permissions";
|
||||
import { prisma } from "../../../core/database/prisma";
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'mob-eliminar',
|
||||
type: 'message',
|
||||
aliases: ['eliminar-mob', 'mob-delete'],
|
||||
name: "mob-eliminar",
|
||||
type: "message",
|
||||
aliases: ["eliminar-mob", "mob-delete"],
|
||||
cooldown: 5,
|
||||
description: 'Eliminar un mob del servidor',
|
||||
usage: 'mob-eliminar <key>',
|
||||
description: "Eliminar un mob del servidor",
|
||||
usage: "mob-eliminar <key>",
|
||||
run: async (message, args, client: Amayo) => {
|
||||
const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, prisma);
|
||||
const allowed = await hasManageGuildOrStaff(
|
||||
message.member,
|
||||
message.guild!.id,
|
||||
prisma
|
||||
);
|
||||
if (!allowed) {
|
||||
await message.reply('❌ No tienes permisos de ManageGuild ni rol de staff.');
|
||||
await message.reply(
|
||||
"❌ No tienes permisos de ManageGuild ni rol de staff."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,23 +27,21 @@ export const command: CommandMessage = {
|
||||
const key = args[0]?.trim();
|
||||
|
||||
if (!key) {
|
||||
await message.reply('Uso: \`!mob-eliminar <key>\`\nEjemplo: \`!mob-eliminar mob.goblin\`');
|
||||
await message.reply(
|
||||
"Uso: `!mob-eliminar <key>`\nEjemplo: `!mob-eliminar mob.goblin`"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const mob = await prisma.mob.findFirst({
|
||||
where: { key, guildId }
|
||||
});
|
||||
|
||||
if (!mob) {
|
||||
await message.reply(`❌ No se encontró el mob local con key ${key} en este servidor.`);
|
||||
// Use admin.deleteMob to centralize logic
|
||||
const { deleteMob } = await import("../../../game/mobs/admin.js");
|
||||
const deleted = await deleteMob(key);
|
||||
if (!deleted) {
|
||||
await message.reply(
|
||||
`❌ No se encontró el mob local con key ${key} en este servidor.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.mob.delete({
|
||||
where: { id: mob.id }
|
||||
});
|
||||
|
||||
await message.reply(`✅ Mob ${key} eliminado exitosamente.`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,75 +1,78 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { prisma } from '../../../core/database/prisma';
|
||||
import { ComponentType, ButtonStyle } from 'discord-api-types/v10';
|
||||
import type { MessageComponentInteraction, TextBasedChannel } from 'discord.js';
|
||||
import type { CommandMessage } from "../../../core/types/commands";
|
||||
import type Amayo from "../../../core/client";
|
||||
import { prisma } from "../../../core/database/prisma";
|
||||
import { ComponentType, ButtonStyle } from "discord-api-types/v10";
|
||||
import type { MessageComponentInteraction, TextBasedChannel } from "discord.js";
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'mobs-lista',
|
||||
type: 'message',
|
||||
aliases: ['lista-mobs', 'mobs-list'],
|
||||
name: "mobs-lista",
|
||||
type: "message",
|
||||
aliases: ["lista-mobs", "mobs-list"],
|
||||
cooldown: 5,
|
||||
description: 'Ver lista de todos los mobs del servidor',
|
||||
usage: 'mobs-lista [pagina]',
|
||||
description: "Ver lista de todos los mobs del servidor",
|
||||
usage: "mobs-lista [pagina]",
|
||||
run: async (message, args, client: Amayo) => {
|
||||
const guildId = message.guild!.id;
|
||||
const page = parseInt(args[0]) || 1;
|
||||
const perPage = 6;
|
||||
|
||||
const total = await prisma.mob.count({
|
||||
where: { OR: [{ guildId }, { guildId: null }] }
|
||||
});
|
||||
|
||||
const mobs = await prisma.mob.findMany({
|
||||
where: { OR: [{ guildId }, { guildId: null }] },
|
||||
orderBy: [{ key: 'asc' }],
|
||||
skip: (page - 1) * perPage,
|
||||
take: perPage
|
||||
});
|
||||
|
||||
if (mobs.length === 0) {
|
||||
await message.reply('No hay mobs configurados en este servidor.');
|
||||
// Use admin list (including built-ins and DB rows)
|
||||
const { listMobsWithRows } = await import("../../../game/mobs/admin.js");
|
||||
const all = await listMobsWithRows();
|
||||
if (!all || all.length === 0) {
|
||||
await message.reply("No hay mobs configurados en este servidor.");
|
||||
return;
|
||||
}
|
||||
|
||||
const total = all.length;
|
||||
const totalPages = Math.ceil(total / perPage);
|
||||
const pageItems = all.slice(
|
||||
(page - 1) * perPage,
|
||||
(page - 1) * perPage + perPage
|
||||
);
|
||||
|
||||
const display = {
|
||||
type: 17,
|
||||
accent_color: 0xFF0000,
|
||||
accent_color: 0xff0000,
|
||||
components: [
|
||||
{
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**👾 Lista de Mobs**\nPágina ${page}/${totalPages} • Total: ${total}`
|
||||
}]
|
||||
components: [
|
||||
{
|
||||
type: 10,
|
||||
content: `**👾 Lista de Mobs**\nPágina ${page}/${totalPages} • Total: ${total}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: 14, divider: true },
|
||||
...mobs.map(mob => {
|
||||
const stats = mob.stats as any || {};
|
||||
...pageItems.map((entry) => {
|
||||
const mob = entry.def;
|
||||
const stats = (mob.base as any) || {};
|
||||
return {
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**${mob.name || mob.key}**\n` +
|
||||
`└ Key: \`${mob.key}\`\n` +
|
||||
`└ ATK: ${stats.attack || 0} | HP: ${stats.hp || 0}\n` +
|
||||
`└ ${mob.guildId === guildId ? '📍 Local' : '🌐 Global'}`
|
||||
}]
|
||||
components: [
|
||||
{
|
||||
type: 10,
|
||||
content:
|
||||
`**${mob.name || mob.key}**\n` +
|
||||
`└ Key: \`${mob.key}\`\n` +
|
||||
`└ ATK: ${stats.attack || 0} | HP: ${stats.hp || 0}\n` +
|
||||
`└ ${entry.guildId === guildId ? "📍 Local" : "🌐 Global"}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
})
|
||||
]
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
const buttons: any[] = [];
|
||||
|
||||
|
||||
if (page > 1) {
|
||||
buttons.push({
|
||||
type: ComponentType.Button,
|
||||
style: ButtonStyle.Secondary,
|
||||
label: '◀ Anterior',
|
||||
custom_id: `mobs_prev_${page}`
|
||||
label: "◀ Anterior",
|
||||
custom_id: `mobs_prev_${page}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,8 +80,8 @@ export const command: CommandMessage = {
|
||||
buttons.push({
|
||||
type: ComponentType.Button,
|
||||
style: ButtonStyle.Secondary,
|
||||
label: 'Siguiente ▶',
|
||||
custom_id: `mobs_next_${page}`
|
||||
label: "Siguiente ▶",
|
||||
custom_id: `mobs_next_${page}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,34 +90,38 @@ export const command: CommandMessage = {
|
||||
flags: 32768,
|
||||
components: [
|
||||
display,
|
||||
...(buttons.length > 0 ? [{
|
||||
type: ComponentType.ActionRow,
|
||||
components: buttons
|
||||
}] : [])
|
||||
]
|
||||
...(buttons.length > 0
|
||||
? [
|
||||
{
|
||||
type: ComponentType.ActionRow,
|
||||
components: buttons,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
|
||||
const collector = msg.createMessageComponentCollector({
|
||||
time: 5 * 60_000,
|
||||
filter: (i) => i.user.id === message.author.id
|
||||
filter: (i) => i.user.id === message.author.id,
|
||||
});
|
||||
|
||||
collector.on('collect', async (i: MessageComponentInteraction) => {
|
||||
collector.on("collect", async (i: MessageComponentInteraction) => {
|
||||
if (!i.isButton()) return;
|
||||
|
||||
if (i.customId.startsWith('mobs_prev_')) {
|
||||
const currentPage = parseInt(i.customId.split('_')[2]);
|
||||
if (i.customId.startsWith("mobs_prev_")) {
|
||||
const currentPage = parseInt(i.customId.split("_")[2]);
|
||||
await i.deferUpdate();
|
||||
args[0] = String(currentPage - 1);
|
||||
await command.run!(message, args, client);
|
||||
collector.stop();
|
||||
} else if (i.customId.startsWith('mobs_next_')) {
|
||||
const currentPage = parseInt(i.customId.split('_')[2]);
|
||||
} else if (i.customId.startsWith("mobs_next_")) {
|
||||
const currentPage = parseInt(i.customId.split("_")[2]);
|
||||
await i.deferUpdate();
|
||||
args[0] = String(currentPage + 1);
|
||||
await command.run!(message, args, client);
|
||||
collector.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -271,6 +271,8 @@ export interface KeyPickerResult<T> {
|
||||
entry: T | null;
|
||||
panelMessage: Message | null;
|
||||
reason: "selected" | "empty" | "cancelled" | "timeout";
|
||||
// When present, the raw value selected from the select menu (may be id or key)
|
||||
selectedValue?: string;
|
||||
}
|
||||
|
||||
export async function promptKeySelection<T>(
|
||||
@@ -444,11 +446,12 @@ export async function promptKeySelection<T>(
|
||||
const result = await new Promise<KeyPickerResult<T>>((resolve) => {
|
||||
const finish = (
|
||||
entry: T | null,
|
||||
reason: "selected" | "cancelled" | "timeout"
|
||||
reason: "selected" | "cancelled" | "timeout",
|
||||
selectedValue?: string
|
||||
) => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
resolve({ entry, panelMessage, reason });
|
||||
resolve({ entry, panelMessage, reason, selectedValue });
|
||||
};
|
||||
|
||||
const collector = panelMessage.createMessageComponentCollector({
|
||||
@@ -501,7 +504,7 @@ export async function promptKeySelection<T>(
|
||||
}
|
||||
}
|
||||
|
||||
finish(selected.entry, "selected");
|
||||
finish(selected.entry, "selected", value);
|
||||
collector.stop("selected");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -16,10 +16,7 @@ import { sendDisplayReply, formatItemLabel } from "./_helpers";
|
||||
|
||||
const PAGE_SIZE = 15;
|
||||
|
||||
function parseItemProps(json: unknown): ItemProps {
|
||||
if (!json || typeof json !== "object") return {};
|
||||
return json as ItemProps;
|
||||
}
|
||||
import { parseItemProps } from "../../../game/core/utils";
|
||||
|
||||
function fmtTool(props: ItemProps) {
|
||||
const t = props.tool;
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
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';
|
||||
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;
|
||||
@@ -14,151 +24,321 @@ interface MobEditorState {
|
||||
}
|
||||
|
||||
function createMobDisplay(state: MobEditorState, editing: boolean = false) {
|
||||
const title = editing ? 'Editando Mob' : 'Creando Mob';
|
||||
const title = editing ? "Editando Mob" : "Creando Mob";
|
||||
const stats = state.stats || {};
|
||||
return {
|
||||
type: 17,
|
||||
accent_color: 0xFF0000,
|
||||
accent_color: 0xff0000,
|
||||
components: [
|
||||
{
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `👹 **${title}: \`${state.key}\`**`
|
||||
}]
|
||||
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`
|
||||
}]
|
||||
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`
|
||||
}]
|
||||
}
|
||||
]
|
||||
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'],
|
||||
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>',
|
||||
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 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; }
|
||||
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 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' },
|
||||
] } ],
|
||||
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) => {
|
||||
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') {
|
||||
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.**'
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
components: [
|
||||
{
|
||||
type: 17,
|
||||
accent_color: 0xff0000,
|
||||
components: [
|
||||
{
|
||||
type: 9,
|
||||
components: [
|
||||
{
|
||||
type: 10,
|
||||
content: "**❌ Editor cancelado.**",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
collector.stop('cancel');
|
||||
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; }
|
||||
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!', flags: MessageFlags.Ephemeral });
|
||||
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.**`
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
components: [
|
||||
{
|
||||
type: 17,
|
||||
accent_color: 0x00ff00,
|
||||
components: [
|
||||
{
|
||||
type: 9,
|
||||
components: [
|
||||
{
|
||||
type: 10,
|
||||
content: `**✅ Mob \`${state.key}\` creado exitosamente.**`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
collector.stop('saved');
|
||||
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 });
|
||||
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 {}
|
||||
}
|
||||
});
|
||||
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;
|
||||
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.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 });
|
||||
|
||||
await sub.reply({
|
||||
content: "✅ Base actualizada.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
|
||||
// Refresh display
|
||||
const newDisplay = createMobDisplay(state, editing);
|
||||
await editorMsg.edit({
|
||||
@@ -168,40 +348,92 @@ async function showBaseModal(i: ButtonInteraction, state: MobEditorState, editor
|
||||
{
|
||||
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' },
|
||||
]
|
||||
}
|
||||
]
|
||||
{
|
||||
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) {
|
||||
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;
|
||||
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');
|
||||
const raw = sub.components.getTextInputValue("json");
|
||||
if (raw) {
|
||||
try {
|
||||
state[field] = JSON.parse(raw);
|
||||
await sub.reply({ content: '✅ Guardado.', flags: MessageFlags.Ephemeral });
|
||||
await sub.reply({
|
||||
content: "✅ Guardado.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
} catch {
|
||||
await sub.reply({ content: '❌ JSON inválido.', flags: MessageFlags.Ephemeral });
|
||||
await sub.reply({
|
||||
content: "❌ JSON inválido.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
state[field] = {};
|
||||
await sub.reply({ content: 'ℹ️ Limpio.', flags: MessageFlags.Ephemeral });
|
||||
await sub.reply({ content: "ℹ️ Limpio.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
|
||||
// Refresh display
|
||||
const newDisplay = createMobDisplay(state, editing);
|
||||
await editorMsg.edit({
|
||||
@@ -211,14 +443,39 @@ async function showJsonModal(i: ButtonInteraction, state: MobEditorState, field:
|
||||
{
|
||||
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' },
|
||||
]
|
||||
}
|
||||
]
|
||||
{
|
||||
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 {}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
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';
|
||||
import { promptKeySelection } from './_helpers';
|
||||
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";
|
||||
import { promptKeySelection } from "./_helpers";
|
||||
|
||||
interface MobEditorState {
|
||||
key: string;
|
||||
@@ -14,84 +24,103 @@ interface MobEditorState {
|
||||
drops?: any;
|
||||
}
|
||||
function createMobDisplay(state: MobEditorState, editing: boolean = false) {
|
||||
const title = editing ? 'Editando Mob' : 'Creando Mob';
|
||||
const title = editing ? "Editando Mob" : "Creando Mob";
|
||||
const stats = state.stats || {};
|
||||
return {
|
||||
type: 17,
|
||||
accent_color: 0xFF0000,
|
||||
accent_color: 0xff0000,
|
||||
components: [
|
||||
{ type: 10, content: `# 👹 ${title}: \`${state.key}\`` },
|
||||
{ type: 14, divider: true },
|
||||
{
|
||||
type: 10,
|
||||
content: [
|
||||
'**📋 Estado Actual:**',
|
||||
`**Nombre:** ${state.name || '❌ No configurado'}`,
|
||||
`**Categoría:** ${state.category || 'Sin categoría'}`,
|
||||
"**📋 Estado Actual:**",
|
||||
`**Nombre:** ${state.name || "❌ No configurado"}`,
|
||||
`**Categoría:** ${state.category || "Sin categoría"}`,
|
||||
`**Attack:** ${stats.attack || 0}`,
|
||||
`**HP:** ${stats.hp || 0}`,
|
||||
`**Defense:** ${stats.defense || 0}`,
|
||||
`**Drops:** ${Object.keys(state.drops || {}).length} items`,
|
||||
].join('\n'),
|
||||
].join("\n"),
|
||||
},
|
||||
{ type: 14, divider: true },
|
||||
{
|
||||
type: 10,
|
||||
content: [
|
||||
'**🎮 Instrucciones:**',
|
||||
'• **Base**: Nombre y categoría',
|
||||
'• **Stats (JSON)**: Estadísticas del mob',
|
||||
'• **Drops (JSON)**: Items que dropea',
|
||||
'• **Guardar**: Confirma los cambios',
|
||||
'• **Cancelar**: Descarta los cambios',
|
||||
].join('\n'),
|
||||
"**🎮 Instrucciones:**",
|
||||
"• **Base**: Nombre y categoría",
|
||||
"• **Stats (JSON)**: Estadísticas del mob",
|
||||
"• **Drops (JSON)**: Items que dropea",
|
||||
"• **Guardar**: Confirma los cambios",
|
||||
"• **Cancelar**: Descarta los cambios",
|
||||
].join("\n"),
|
||||
},
|
||||
]
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'mob-editar',
|
||||
type: 'message',
|
||||
aliases: ['editar-mob','mobedit'],
|
||||
name: "mob-editar",
|
||||
type: "message",
|
||||
aliases: ["editar-mob", "mobedit"],
|
||||
cooldown: 10,
|
||||
description: 'Edita un Mob (enemigo) de este servidor con editor interactivo.',
|
||||
category: 'Minijuegos',
|
||||
usage: 'mob-editar',
|
||||
description:
|
||||
"Edita un Mob (enemigo) de este servidor con editor interactivo.",
|
||||
category: "Minijuegos",
|
||||
usage: "mob-editar",
|
||||
run: async (message: Message, _args: string[], client: Amayo) => {
|
||||
const channel = message.channel as TextBasedChannel & { send: Function };
|
||||
const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma);
|
||||
const allowed = await hasManageGuildOrStaff(
|
||||
message.member,
|
||||
message.guild!.id,
|
||||
client.prisma
|
||||
);
|
||||
if (!allowed) {
|
||||
await (channel.send as any)({
|
||||
content: null,
|
||||
flags: 32768,
|
||||
components: [{
|
||||
type: 17,
|
||||
accent_color: 0xFF0000,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: '❌ **Error de Permisos**\n└ No tienes permisos de ManageGuild ni rol de staff.'
|
||||
}]
|
||||
}],
|
||||
reply: { messageReference: message.id }
|
||||
components: [
|
||||
{
|
||||
type: 17,
|
||||
accent_color: 0xff0000,
|
||||
components: [
|
||||
{
|
||||
type: 10,
|
||||
content:
|
||||
"❌ **Error de Permisos**\n└ No tienes permisos de ManageGuild ni rol de staff.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
reply: { messageReference: message.id },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const guildId = message.guild!.id;
|
||||
const mobs = await client.prisma.mob.findMany({ where: { guildId }, orderBy: [{ key: 'asc' }] });
|
||||
const { listMobsWithRows } = await import("../../../game/mobs/admin.js");
|
||||
const all = await listMobsWithRows();
|
||||
// Keep behaviour: only guild-local mobs editable here
|
||||
const localEntries = all.filter((e: any) => e.guildId === guildId);
|
||||
const selection = await promptKeySelection(message, {
|
||||
entries: mobs,
|
||||
customIdPrefix: 'mob_edit',
|
||||
title: 'Selecciona un mob para editar',
|
||||
emptyText: '⚠️ **No hay mobs configurados.** Usa `!mob-crear` primero.',
|
||||
placeholder: 'Elige un mob…',
|
||||
filterHint: 'Filtra por nombre, key o categoría.',
|
||||
getOption: (mob) => ({
|
||||
value: mob.id,
|
||||
label: mob.name ?? mob.key,
|
||||
description: [mob.category ?? 'Sin categoría', mob.key].filter(Boolean).join(' • '),
|
||||
keywords: [mob.key, mob.name ?? '', mob.category ?? ''],
|
||||
entries: localEntries,
|
||||
customIdPrefix: "mob_edit",
|
||||
title: "Selecciona un mob para editar",
|
||||
emptyText: "⚠️ **No hay mobs configurados.** Usa `!mob-crear` primero.",
|
||||
placeholder: "Elige un mob…",
|
||||
filterHint: "Filtra por nombre, key o categoría.",
|
||||
getOption: (entry: any) => ({
|
||||
value: entry.id ?? entry.def.key,
|
||||
label: entry.def.name ?? entry.def.key,
|
||||
description: [entry.def?.category ?? "Sin categoría", entry.def.key]
|
||||
.filter(Boolean)
|
||||
.join(" • "),
|
||||
keywords: [
|
||||
entry.def.key,
|
||||
entry.def.name ?? "",
|
||||
entry.def?.category ?? "",
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -99,14 +128,23 @@ export const command: CommandMessage = {
|
||||
return;
|
||||
}
|
||||
|
||||
const mob = selection.entry;
|
||||
const entry = selection.entry as any;
|
||||
if (!entry) return;
|
||||
|
||||
// If entry has an id (DB row), fetch the full row to get stats/drops stored in DB.
|
||||
let dbRow: any = null;
|
||||
if (entry.id) {
|
||||
try {
|
||||
dbRow = await client.prisma.mob.findUnique({ where: { id: entry.id } });
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const state: MobEditorState = {
|
||||
key: mob.key,
|
||||
name: mob.name,
|
||||
category: mob.category ?? undefined,
|
||||
stats: mob.stats ?? {},
|
||||
drops: mob.drops ?? {},
|
||||
key: entry.def.key,
|
||||
name: (dbRow && dbRow.name) ?? entry.def.name,
|
||||
category: (dbRow && dbRow.category) ?? entry.def?.category ?? undefined,
|
||||
stats: (dbRow && dbRow.stats) ?? entry.def?.base ?? {},
|
||||
drops: (dbRow && dbRow.drops) ?? entry.def?.drops ?? {},
|
||||
};
|
||||
|
||||
const buildEditorComponents = () => [
|
||||
@@ -114,13 +152,38 @@ export const command: CommandMessage = {
|
||||
{
|
||||
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' },
|
||||
]
|
||||
}
|
||||
{
|
||||
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 editorMsg = selection.panelMessage;
|
||||
@@ -130,77 +193,142 @@ export const command: CommandMessage = {
|
||||
components: buildEditorComponents(),
|
||||
});
|
||||
|
||||
const collector = editorMsg.createMessageComponentCollector({ time: 30 * 60_000, filter: (i) => i.user.id === message.author.id });
|
||||
collector.on('collect', async (i: MessageComponentInteraction) => {
|
||||
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;
|
||||
switch (i.customId) {
|
||||
case 'mb_cancel':
|
||||
case "mb_cancel":
|
||||
await i.deferUpdate();
|
||||
await editorMsg.edit({
|
||||
content: null,
|
||||
flags: 32768,
|
||||
components: [{
|
||||
type: 17,
|
||||
accent_color: 0xFF0000,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: '**❌ Editor cancelado.**'
|
||||
}]
|
||||
}]
|
||||
components: [
|
||||
{
|
||||
type: 17,
|
||||
accent_color: 0xff0000,
|
||||
components: [
|
||||
{
|
||||
type: 10,
|
||||
content: "**❌ Editor cancelado.**",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
collector.stop('cancel');
|
||||
collector.stop("cancel");
|
||||
return;
|
||||
case 'mb_base':
|
||||
await showBaseModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents);
|
||||
case "mb_base":
|
||||
await showBaseModal(
|
||||
i as ButtonInteraction,
|
||||
state,
|
||||
editorMsg,
|
||||
buildEditorComponents
|
||||
);
|
||||
return;
|
||||
case 'mb_stats':
|
||||
await showJsonModal(i as ButtonInteraction, state, 'stats', 'Stats del Mob (JSON)', editorMsg, buildEditorComponents);
|
||||
case "mb_stats":
|
||||
await showJsonModal(
|
||||
i as ButtonInteraction,
|
||||
state,
|
||||
"stats",
|
||||
"Stats del Mob (JSON)",
|
||||
editorMsg,
|
||||
buildEditorComponents
|
||||
);
|
||||
return;
|
||||
case 'mb_drops':
|
||||
await showJsonModal(i as ButtonInteraction, state, 'drops', 'Drops del Mob (JSON)', editorMsg, buildEditorComponents);
|
||||
case "mb_drops":
|
||||
await showJsonModal(
|
||||
i as ButtonInteraction,
|
||||
state,
|
||||
"drops",
|
||||
"Drops del Mob (JSON)",
|
||||
editorMsg,
|
||||
buildEditorComponents
|
||||
);
|
||||
return;
|
||||
case 'mb_save':
|
||||
case "mb_save":
|
||||
if (!state.name) {
|
||||
await i.reply({ content: '❌ Falta el nombre del mob.', flags: MessageFlags.Ephemeral });
|
||||
await i.reply({
|
||||
content: "❌ Falta el nombre del mob.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await client.prisma.mob.update({ where: { id: mob.id }, data: { name: state.name!, category: state.category ?? null, stats: state.stats ?? {}, drops: state.drops ?? {} } });
|
||||
await i.reply({ content: '✅ Mob actualizado!', flags: MessageFlags.Ephemeral });
|
||||
try {
|
||||
const { createOrUpdateMob } = await import(
|
||||
"../../../game/mobs/admin.js"
|
||||
);
|
||||
// Provide guildId so admin can scope or return db row
|
||||
await createOrUpdateMob({ ...(state as any), guildId });
|
||||
await i.reply({
|
||||
content: "✅ Mob actualizado!",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
} catch (e) {
|
||||
// fallback to direct update
|
||||
await client.prisma.mob.update({
|
||||
where: { id: entry.id },
|
||||
data: {
|
||||
name: state.name!,
|
||||
category: state.category ?? null,
|
||||
stats: state.stats ?? {},
|
||||
drops: state.drops ?? {},
|
||||
},
|
||||
});
|
||||
await i.reply({
|
||||
content: "✅ Mob actualizado (fallback)!",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
await editorMsg.edit({
|
||||
content: null,
|
||||
flags: 32768,
|
||||
components: [{
|
||||
type: 17,
|
||||
accent_color: 0x00FF00,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**✅ Mob \`${state.key}\` actualizado exitosamente.**`
|
||||
}]
|
||||
}]
|
||||
components: [
|
||||
{
|
||||
type: 17,
|
||||
accent_color: 0x00ff00,
|
||||
components: [
|
||||
{
|
||||
type: 10,
|
||||
content: `**✅ Mob \`${state.key}\` actualizado exitosamente.**`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
collector.stop('saved');
|
||||
collector.stop("saved");
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({ err }, 'mob-editar');
|
||||
if (!i.deferred && !i.replied) await i.reply({ content: '❌ Error procesando la acción.', flags: MessageFlags.Ephemeral });
|
||||
logger.error({ err }, "mob-editar");
|
||||
if (!i.deferred && !i.replied)
|
||||
await i.reply({
|
||||
content: "❌ Error procesando la acción.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
});
|
||||
collector.on('end', async (_c, reason) => {
|
||||
if (reason === 'time') {
|
||||
collector.on("end", async (_c, reason) => {
|
||||
if (reason === "time") {
|
||||
try {
|
||||
await editorMsg.edit({
|
||||
content: null,
|
||||
flags: 32768,
|
||||
components: [{
|
||||
type: 17,
|
||||
accent_color: 0xFFA500,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: '**⏰ Editor expirado.**'
|
||||
}]
|
||||
}]
|
||||
components: [
|
||||
{
|
||||
type: 17,
|
||||
accent_color: 0xffa500,
|
||||
components: [
|
||||
{
|
||||
type: 10,
|
||||
content: "**⏰ Editor expirado.**",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
@@ -208,41 +336,94 @@ export const command: CommandMessage = {
|
||||
},
|
||||
};
|
||||
|
||||
async function showBaseModal(i: ButtonInteraction, state: MobEditorState, editorMsg: Message, buildComponents: () => any[]) {
|
||||
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;
|
||||
async function showBaseModal(
|
||||
i: ButtonInteraction,
|
||||
state: MobEditorState,
|
||||
editorMsg: Message,
|
||||
buildComponents: () => any[]
|
||||
) {
|
||||
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.name = sub.components.getTextInputValue("name").trim();
|
||||
const cat = sub.components.getTextInputValue("category")?.trim();
|
||||
state.category = cat || undefined;
|
||||
await sub.deferUpdate();
|
||||
await editorMsg.edit({
|
||||
content: null,
|
||||
flags: 32768,
|
||||
components: buildComponents()
|
||||
components: buildComponents(),
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function showJsonModal(i: ButtonInteraction, state: MobEditorState, field: 'stats'|'drops', title: string, editorMsg: Message, buildComponents: () => any[]) {
|
||||
async function showJsonModal(
|
||||
i: ButtonInteraction,
|
||||
state: MobEditorState,
|
||||
field: "stats" | "drops",
|
||||
title: string,
|
||||
editorMsg: Message,
|
||||
buildComponents: () => any[]
|
||||
) {
|
||||
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;
|
||||
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');
|
||||
const raw = sub.components.getTextInputValue("json");
|
||||
if (raw) {
|
||||
try {
|
||||
state[field] = JSON.parse(raw);
|
||||
await sub.deferUpdate();
|
||||
} catch {
|
||||
await sub.reply({ content: '❌ JSON inválido.', flags: MessageFlags.Ephemeral });
|
||||
await sub.reply({
|
||||
content: "❌ JSON inválido.",
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -252,7 +433,7 @@ async function showJsonModal(i: ButtonInteraction, state: MobEditorState, field:
|
||||
await editorMsg.edit({
|
||||
content: null,
|
||||
flags: 32768,
|
||||
components: buildComponents()
|
||||
components: buildComponents(),
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -36,10 +36,7 @@ function buildEmoji(
|
||||
return { id, name, animated };
|
||||
}
|
||||
|
||||
function parseItemProps(json: unknown): ItemProps {
|
||||
if (!json || typeof json !== "object") return {};
|
||||
return json as ItemProps;
|
||||
}
|
||||
import { parseItemProps } from "../../../game/core/utils";
|
||||
|
||||
function formatPrice(price: any): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
Reference in New Issue
Block a user