feat: Add scripts for mob dependency management and server setup

- Implemented `findMobDependencies.ts` to identify foreign key constraints referencing the Mob table and log dependent rows.
- Created `fullServerSetup.ts` for idempotent server setup, including economy items, item recipes, game areas, mobs, and optional demo mob attacks.
- Developed `removeInvalidMobsWithDeps.ts` to delete invalid mobs and their dependencies, backing up affected scheduled mob attacks.
- Added unit tests in `testMobUnit.ts` and `mob.test.ts` for mob functionality, including stats computation and instance retrieval.
- Introduced reward modification tests in `testRewardMods.ts` and `rewardMods.unit.ts` to validate drop selection and coin multiplier behavior.
- Enhanced command handling for mob deletion in `mobDelete.ts` and setup examples in `setup.ts`, ensuring proper permissions and feedback.
- Created utility functions in `testHelpers.ts` for deterministic drop selection from mob definitions.
This commit is contained in:
Shni
2025-10-14 14:58:38 -05:00
parent f36fa24e46
commit 852b1d02a2
24 changed files with 2158 additions and 177 deletions

View File

@@ -1,6 +1,5 @@
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";

View File

@@ -1,9 +1,19 @@
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 { Message, MessageComponentInteraction, MessageFlags, ButtonInteraction, TextBasedChannel } from 'discord.js';
import { ComponentType, TextInputStyle, ButtonStyle } from 'discord-api-types/v10';
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 {
Message,
MessageComponentInteraction,
MessageFlags,
ButtonInteraction,
TextBasedChannel,
} from "discord.js";
import {
ComponentType,
TextInputStyle,
ButtonStyle,
} from "discord-api-types/v10";
interface AreaState {
key: string;
@@ -14,43 +24,43 @@ interface AreaState {
}
function buildAreaDisplay(state: AreaState, editing: boolean = false) {
const title = editing ? 'Editando Área' : 'Creando Área';
const title = editing ? "Editando Área" : "Creando Área";
const statusText = [
'**📋 Estado Actual:**',
`**Nombre:** ${state.name || '❌ No configurado'}`,
`**Tipo:** ${state.type || '❌ No configurado'}`,
"**📋 Estado Actual:**",
`**Nombre:** ${state.name || "❌ No configurado"}`,
`**Tipo:** ${state.type || "❌ No configurado"}`,
`**Config:** ${Object.keys(state.config || {}).length} campos`,
`**Metadata:** ${Object.keys(state.metadata || {}).length} campos`
].join('\n');
`**Metadata:** ${Object.keys(state.metadata || {}).length} campos`,
].join("\n");
const instructionsText = [
'**🎮 Instrucciones:**',
'• **Base**: Configura nombre y tipo',
'• **Config (JSON)**: Configuración técnica',
'• **Meta (JSON)**: Metadatos adicionales',
'• **Guardar**: Confirma los cambios',
'• **Cancelar**: Descarta los cambios'
].join('\n');
"**🎮 Instrucciones:**",
"• **Base**: Configura nombre y tipo",
"• **Config (JSON)**: Configuración técnica",
"• **Meta (JSON)**: Metadatos adicionales",
"• **Guardar**: Confirma los cambios",
"• **Cancelar**: Descarta los cambios",
].join("\n");
return {
type: 17,
accent_color: 0x00FF00,
accent_color: 0x00ff00,
components: [
{
type: 10,
content: `# 🗺️ ${title}: \`${state.key}\``
content: `# 🗺️ ${title}: \`${state.key}\``,
},
{ type: 14, divider: true },
{
type: 10,
content: statusText
content: statusText,
},
{ type: 14, divider: true },
{
type: 10,
content: instructionsText
}
]
content: instructionsText,
},
],
};
}
@@ -59,38 +69,73 @@ const buildEditorComponents = (state: AreaState, editing: boolean = false) => [
{
type: 1,
components: [
{ type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'ga_base' },
{ type: 2, style: ButtonStyle.Secondary, label: 'Config (JSON)', custom_id: 'ga_config' },
{ type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'ga_meta' },
{ type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'ga_save' },
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'ga_cancel' },
]
}
{
type: 2,
style: ButtonStyle.Primary,
label: "Base",
custom_id: "ga_base",
},
{
type: 2,
style: ButtonStyle.Secondary,
label: "Config (JSON)",
custom_id: "ga_config",
},
{
type: 2,
style: ButtonStyle.Secondary,
label: "Meta (JSON)",
custom_id: "ga_meta",
},
{
type: 2,
style: ButtonStyle.Success,
label: "Guardar",
custom_id: "ga_save",
},
{
type: 2,
style: ButtonStyle.Danger,
label: "Cancelar",
custom_id: "ga_cancel",
},
],
},
];
export const command: CommandMessage = {
name: 'area-crear',
type: 'message',
aliases: ['crear-area','areacreate'],
name: "area-crear",
type: "message",
aliases: ["crear-area", "areacreate"],
cooldown: 10,
description: 'Crea una GameArea (mina/laguna/arena/farm) para este servidor con editor.',
usage: 'area-crear <key-única>',
description:
"Crea una GameArea (mina/laguna/arena/farm) para este servidor con editor.",
usage: "area-crear <key-única>",
run: async (message, args, _client: Amayo) => {
const channel = message.channel as TextBasedChannel & { send: Function };
const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, prisma);
const allowed = await hasManageGuildOrStaff(
message.member,
message.guild!.id,
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;
}
@@ -100,15 +145,20 @@ export const command: CommandMessage = {
await (channel.send as any)({
content: null,
flags: 32768,
components: [{
type: 17,
accent_color: 0xFFA500,
components: [{
type: 10,
content: '⚠️ **Uso Incorrecto**\n└ Uso: `!area-crear <key-única>`'
}]
}],
reply: { messageReference: message.id }
components: [
{
type: 17,
accent_color: 0xffa500,
components: [
{
type: 10,
content:
"⚠️ **Uso Incorrecto**\n└ Uso: `!area-crear <key-única>`",
},
],
},
],
reply: { messageReference: message.id },
});
return;
}
@@ -119,15 +169,20 @@ export const command: CommandMessage = {
await (channel.send as any)({
content: null,
flags: 32768,
components: [{
type: 17,
accent_color: 0xFF0000,
components: [{
type: 10,
content: '❌ **Área Ya Existe**\n└ Ya existe un área con esa key en este servidor.'
}]
}],
reply: { messageReference: message.id }
components: [
{
type: 17,
accent_color: 0xff0000,
components: [
{
type: 10,
content:
"❌ **Área Ya Existe**\n└ Ya existe un área con esa key en este servidor.",
},
],
},
],
reply: { messageReference: message.id },
});
return;
}
@@ -138,133 +193,271 @@ export const command: CommandMessage = {
content: null,
flags: 32768,
components: buildEditorComponents(state, false),
reply: { messageReference: message.id }
reply: { messageReference: message.id },
});
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 'ga_cancel':
case "ga_cancel":
await i.deferUpdate();
await editorMsg.edit({
content: null,
flags: 32768,
components: [{
type: 17,
accent_color: 0xFF0000,
components: [{
type: 10,
content: '**❌ Editor de Área cancelado.**'
}]
}]
components: [
{
type: 17,
accent_color: 0xff0000,
components: [
{
type: 10,
content: "**❌ Editor de Área cancelado.**",
},
],
},
],
});
collector.stop('cancel');
collector.stop("cancel");
return;
case 'ga_base':
await showBaseModal(i as ButtonInteraction, state, editorMsg, false);
case "ga_base":
await showBaseModal(
i as ButtonInteraction,
state,
editorMsg,
false
);
return;
case 'ga_config':
await showJsonModal(i as ButtonInteraction, state, 'config', 'Config del Área', editorMsg, false);
case "ga_config":
await showJsonModal(
i as ButtonInteraction,
state,
"config",
"Config del Área",
editorMsg,
false
);
return;
case 'ga_meta':
await showJsonModal(i as ButtonInteraction, state, 'metadata', 'Meta del Área', editorMsg, false);
case "ga_meta":
await showJsonModal(
i as ButtonInteraction,
state,
"metadata",
"Meta del Área",
editorMsg,
false
);
return;
case 'ga_save':
if (!state.name || !state.type) { await i.reply({ content: '❌ Completa Base (nombre/tipo).', flags: MessageFlags.Ephemeral }); return; }
await prisma.gameArea.create({ data: { guildId, key: state.key, name: state.name!, type: state.type!, config: state.config ?? {}, metadata: state.metadata ?? {} } });
await i.reply({ content: '✅ Área guardada.', flags: MessageFlags.Ephemeral });
case "ga_save":
if (!state.name || !state.type) {
await i.reply({
content: "❌ Completa Base (nombre/tipo).",
flags: MessageFlags.Ephemeral,
});
return;
}
await prisma.gameArea.create({
data: {
guildId,
key: state.key,
name: state.name!,
type: state.type!,
config: state.config ?? {},
metadata: state.metadata ?? {},
},
});
await i.reply({
content: "✅ Área guardada.",
flags: MessageFlags.Ephemeral,
});
await editorMsg.edit({
content: null,
flags: 32768,
components: [{
type: 17,
accent_color: 0x00FF00,
components: [{
type: 10,
content: `**✅ Área \`${state.key}\` creada exitosamente.**`
}]
}]
components: [
{
type: 17,
accent_color: 0x00ff00,
components: [
{
type: 10,
content: `**✅ Área \`${state.key}\` creada exitosamente.**`,
},
],
},
],
});
collector.stop('saved');
collector.stop("saved");
return;
}
} catch (e) {
if (!i.deferred && !i.replied) await i.reply({ content: '❌ Error procesando la acción.', flags: MessageFlags.Ephemeral });
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') {
collector.on("end", async (_c, r) => {
if (r === "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 {}
}
});
}
},
};
async function showBaseModal(i: ButtonInteraction, state: AreaState, editorMsg: Message, editing: boolean) {
const modal = { title: 'Base del Área', customId: 'ga_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: 'Tipo (MINE/LAGOON/FIGHT/FARM)', component: { type: ComponentType.TextInput, customId: 'type', style: TextInputStyle.Short, required: true, value: state.type ?? '' } },
] } as const;
async function showBaseModal(
i: ButtonInteraction,
state: AreaState,
editorMsg: Message,
editing: boolean
) {
const modal = {
title: "Base del Área",
customId: "ga_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: "Tipo (MINE/LAGOON/FIGHT/FARM)",
component: {
type: ComponentType.TextInput,
customId: "type",
style: TextInputStyle.Short,
required: true,
value: state.type ?? "",
},
},
{
type: ComponentType.Label,
label: "Imagen de referencia (URL, opcional)",
component: {
type: ComponentType.TextInput,
customId: "referenceImage",
style: TextInputStyle.Short,
required: false,
value:
(state.metadata &&
(state.metadata.referenceImage ||
state.metadata.image ||
state.metadata.previewImage)) ??
"",
},
},
],
} as const;
await i.showModal(modal);
try {
const sub = await i.awaitModalSubmit({ time: 300_000 });
state.name = sub.components.getTextInputValue('name').trim();
state.type = sub.components.getTextInputValue('type').trim().toUpperCase();
await sub.reply({ content: '✅ Base actualizada.', flags: MessageFlags.Ephemeral });
state.name = sub.components.getTextInputValue("name").trim();
state.type = sub.components.getTextInputValue("type").trim().toUpperCase();
try {
const ref = sub.components.getTextInputValue("referenceImage")?.trim();
if (ref && ref.length > 0) {
state.metadata = state.metadata || {};
// store as referenceImage for consumers; renderer looks at previewImage/image/referenceImage
(state.metadata as any).referenceImage = ref;
}
} catch {}
await sub.reply({
content: "✅ Base actualizada.",
flags: MessageFlags.Ephemeral,
});
// Actualizar display
await editorMsg.edit({
content: null,
flags: 32768,
components: buildEditorComponents(state, editing)
components: buildEditorComponents(state, editing),
});
} catch {}
}
async function showJsonModal(i: ButtonInteraction, state: AreaState, field: 'config'|'metadata', title: string, editorMsg: Message, editing: boolean) {
async function showJsonModal(
i: ButtonInteraction,
state: AreaState,
field: "config" | "metadata",
title: string,
editorMsg: Message,
editing: boolean
) {
const current = JSON.stringify(state[field] ?? {});
const modal = { title, customId: `ga_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: `ga_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 });
}
// Actualizar display
await editorMsg.edit({
content: null,
flags: 32768,
components: buildEditorComponents(state, editing)
components: buildEditorComponents(state, editing),
});
} catch {}
}

View File

@@ -0,0 +1,178 @@
import {
Message,
MessageFlags,
MessageComponentInteraction,
ButtonInteraction,
TextBasedChannel,
} from "discord.js";
import { ButtonStyle, ComponentType } 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";
export const command: CommandMessage = {
name: "mob-eliminar",
type: "message",
aliases: ["eliminar-mob", "mobdelete"],
cooldown: 10,
description: "Elimina un mob del servidor (requiere permisos de staff)",
category: "Minijuegos",
usage: "mob-eliminar",
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
);
if (!allowed) {
await channel.send({
content: undefined,
flags: 32768,
components: [
{
type: 17,
accent_color: 0xff0000,
components: [
{
type: 10,
content:
"❌ No tienes permisos de ManageGuild ni rol de staff.",
},
],
},
],
});
return;
}
const guildId = message.guild!.id;
try {
const { listMobsWithRows } = await import("../../../game/mobs/admin.js");
const all = await listMobsWithRows();
const localEntries = all.filter(
(e: any) => e.guildId === guildId && e.id
);
const selection = await promptKeySelection(message, {
entries: localEntries,
customIdPrefix: "mob_delete",
title: "Selecciona un mob para eliminar",
emptyText: "⚠️ No hay mobs locales configurados.",
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(" • "),
}),
});
if (!selection.entry) return;
const entry = selection.entry as any;
// confirm
const confirmMsg = await channel.send({
content: `¿Eliminar mob \`${
entry.def.name || entry.def.key
}\`? Esta acción es irreversible.`,
components: [
{
type: 1,
components: [
{
type: 2,
style: ButtonStyle.Danger,
label: "Confirmar",
custom_id: "confirm_delete",
},
{
type: 2,
style: ButtonStyle.Secondary,
label: "Cancelar",
custom_id: "cancel_delete",
},
],
},
],
});
const collector = confirmMsg.createMessageComponentCollector({
time: 60_000,
filter: (i) => i.user.id === message.author.id,
});
collector.on("collect", async (i: MessageComponentInteraction) => {
try {
if (!i.isButton()) return;
if (i.customId === "cancel_delete") {
await i.update({ content: "❌ Cancelado.", components: [] });
collector.stop("cancel");
return;
}
if (i.customId === "confirm_delete") {
await i.deferUpdate();
try {
const { deleteMob } = await import("../../../game/mobs/admin.js");
const ok = await deleteMob(entry.def.key);
if (ok) {
await i.followUp({
content: "✅ Mob eliminado.",
flags: MessageFlags.Ephemeral,
});
try {
await confirmMsg.edit({
content: "✅ Eliminado.",
components: [],
});
} catch {}
} else {
// fallback to direct Prisma delete by id
await client.prisma.mob.delete({ where: { id: entry.id } });
await i.followUp({
content: "✅ Mob eliminado (fallback).",
flags: MessageFlags.Ephemeral,
});
try {
await confirmMsg.edit({
content: "✅ Eliminado (fallback).",
components: [],
});
} catch {}
}
} catch (e: any) {
// If FK prevents deletion, inform user and suggest running cleanup script
const msg = (e && e.message) || String(e);
await i.followUp({
content: `❌ No se pudo eliminar: ${msg}`,
flags: MessageFlags.Ephemeral,
});
}
collector.stop("done");
return;
}
} catch (err) {
logger.error({ err }, "mob-eliminar");
}
});
collector.on("end", async (_c, reason) => {
if (reason === "time") {
try {
await confirmMsg.edit({
content: "⏰ Confirmación expirada.",
components: [],
});
} catch {}
}
});
} catch (e) {
logger.error({ e }, "mob-eliminar");
await channel.send({
content: "❌ Error al intentar eliminar mob.",
flags: 32768,
});
}
},
};

View File

@@ -0,0 +1,159 @@
import { Message, TextBasedChannel } from "discord.js";
import type { CommandMessage } from "../../../core/types/commands";
import { hasManageGuildOrStaff } from "../../../core/lib/permissions";
import fs from "fs";
import path from "path";
import logger from "../../../core/lib/logger";
import type Amayo from "../../../core/client";
// Helper: split text into chunks under Discord message limit (~2000)
function chunkText(text: string, size = 1900) {
const parts: string[] = [];
let i = 0;
while (i < text.length) {
parts.push(text.slice(i, i + size));
i += size;
}
return parts;
}
export const command: CommandMessage = {
name: "setup",
type: "message",
aliases: ["setup-ejemplos", "setup-demo"],
cooldown: 10,
description:
"Publica ejemplos básicos y avanzados para configurar items, mobs y áreas.",
category: "Admin",
usage: "setup [advanced]",
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
);
if (!allowed) {
await channel.send({
content: "❌ No tienes permisos de ManageGuild ni rol de staff.",
});
return;
}
const showAdvanced = args[0] === "advanced" || args.includes("advanced");
const doInit = args[0] === "init" || args.includes("init");
const doInitFull = args[0] === "init-full" || args.includes("init-full");
const initAdvanced = args.includes("advanced") && doInit;
if (doInitFull) {
await channel.send(
"Iniciando FULL setup: creando items, areas, mobs y recetas (modo idempotente). Esto puede tardar unos segundos."
);
try {
const setupMod: any = await import(
"../../../../scripts/fullServerSetup.js"
);
if (typeof setupMod.runFullServerSetup === "function") {
// Use guild id from the current guild context
await setupMod.runFullServerSetup(message.guild!.id);
await channel.send("✅ Full setup completado.");
} else {
await channel.send(
"❌ El módulo de setup completo no exporta runFullServerSetup()."
);
}
} catch (e) {
logger.error({ e }, "setup init-full failed");
await channel.send(
`❌ Error corriendo fullServerSetup: ${
(e && (e as any).message) || e
}`
);
}
return;
}
if (doInit) {
// Run seed logic in-process by importing the seed script module
await channel.send(
"Iniciando setup: creando items, areas, mobs y recetas... Esto puede tardar unos segundos."
);
try {
const seedMod: any = await import("../../../game/minigames/seed.js");
if (typeof seedMod.main === "function") {
await seedMod.main();
await channel.send("✅ Setup inicial completado.");
} else {
// fallback: try executing default export or module itself
if (typeof seedMod === "function") {
await seedMod();
await channel.send("✅ Setup inicial completado (fallback).");
} else {
await channel.send(
"❌ Módulo seed no expone main(). Ejecuta el seed manualmente."
);
}
}
} catch (e) {
logger.error({ e }, "setup init failed");
await channel.send(
`❌ Error corriendo seed: ${(e && (e as any).message) || e}`
);
}
return;
}
try {
const readPath = path.resolve(process.cwd(), "README", "Mas Ejemplos.md");
if (!fs.existsSync(readPath)) {
await channel.send("README/Mas Ejemplos.md no encontrado en el repo.");
return;
}
const raw = fs.readFileSync(readPath, "utf8");
// Extract two sections: "Flujo rápido" and "Items: creación" and the mobs section
// We'll be generous and send large chunks; the README already contains the examples.
const header = "# Guía rápida para el staff";
const basicIndex = raw.indexOf(
"## Flujo rápido: Crear un ítem con receta"
);
const itemsIndex = raw.indexOf("## Items: creación, edición y revisión");
const mobsIndex = raw.indexOf("## Mobs: enemigos y NPCs");
// Fallback: send the whole file (chunked) if parsing fails
if (basicIndex === -1 || itemsIndex === -1) {
const chunks = chunkText(raw);
for (const c of chunks) await channel.send(c);
if (!showAdvanced) return;
// advanced is basically the rest of the README; already sent
return;
}
const basicSection = raw.slice(basicIndex, itemsIndex);
const itemsSection = raw.slice(
itemsIndex,
mobsIndex === -1 ? raw.length : mobsIndex
);
const mobsSection =
mobsIndex === -1 ? "" : raw.slice(mobsIndex, raw.length);
// Send basic & items
for (const chunk of chunkText(basicSection)) await channel.send(chunk);
for (const chunk of chunkText(itemsSection)) await channel.send(chunk);
if (showAdvanced) {
for (const chunk of chunkText(mobsSection)) await channel.send(chunk);
// Also send rest of file
const restIndex = raw.indexOf("\n---\n", mobsIndex);
if (restIndex !== -1) {
const rest = raw.slice(restIndex);
for (const chunk of chunkText(rest)) await channel.send(chunk);
}
} else {
await channel.send(
"Usa `!setup advanced` para publicar la sección avanzada (mobs, crafteos avanzados y workflows)."
);
}
} catch (e) {
logger.error({ e }, "setup command failed");
await channel.send("❌ Error al publicar ejemplos. Revisa logs.");
}
},
};