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[] = [];
|
||||
|
||||
@@ -5,11 +5,7 @@ import {
|
||||
} from "./statusEffectsService";
|
||||
import type { ItemProps } from "../economy/types";
|
||||
import { ensureUserAndGuildExist } from "../core/userService";
|
||||
|
||||
function parseItemProps(json: unknown): ItemProps {
|
||||
if (!json || typeof json !== "object") return {};
|
||||
return json as ItemProps;
|
||||
}
|
||||
import { parseItemProps } from "../core/utils";
|
||||
|
||||
export async function ensurePlayerState(userId: string, guildId: string) {
|
||||
// Asegurar que User y Guild existan antes de crear/buscar state
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
import { prisma } from '../../core/database/prisma';
|
||||
import { assertNotOnCooldown, setCooldown } from '../cooldowns/service';
|
||||
import { findItemByKey, consumeItemByKey } from '../economy/service';
|
||||
import type { ItemProps } from '../economy/types';
|
||||
import { getEffectiveStats, adjustHP } from '../combat/equipmentService';
|
||||
import { prisma } from "../../core/database/prisma";
|
||||
import { assertNotOnCooldown, setCooldown } from "../cooldowns/service";
|
||||
import { findItemByKey, consumeItemByKey } from "../economy/service";
|
||||
import type { ItemProps } from "../economy/types";
|
||||
import { getEffectiveStats, adjustHP } from "../combat/equipmentService";
|
||||
import { parseItemProps } from "../core/utils";
|
||||
import { getCooldownKeyForFood, calculateHealingFromFood } from "./utils";
|
||||
|
||||
function parseItemProps(json: unknown): ItemProps {
|
||||
if (!json || typeof json !== 'object') return {};
|
||||
return json as ItemProps;
|
||||
}
|
||||
|
||||
export async function useConsumableByKey(userId: string, guildId: string, itemKey: string) {
|
||||
export async function useConsumableByKey(
|
||||
userId: string,
|
||||
guildId: string,
|
||||
itemKey: string
|
||||
) {
|
||||
const item = await findItemByKey(guildId, itemKey);
|
||||
if (!item) throw new Error('Ítem no encontrado');
|
||||
if (!item) throw new Error("Ítem no encontrado");
|
||||
const props = parseItemProps(item.props);
|
||||
const food = props.food;
|
||||
if (!food) throw new Error('Este ítem no es consumible');
|
||||
if (!food) throw new Error("Este ítem no es consumible");
|
||||
|
||||
const cdKey = food.cooldownKey ?? `food:${item.key}`;
|
||||
const cdKey = getCooldownKeyForFood(item.key, food);
|
||||
await assertNotOnCooldown(userId, guildId, cdKey);
|
||||
|
||||
// Calcular sanación
|
||||
const stats = await getEffectiveStats(userId, guildId);
|
||||
const flat = Math.max(0, food.healHp ?? 0);
|
||||
const perc = Math.max(0, food.healPercent ?? 0);
|
||||
const byPerc = Math.floor((perc / 100) * stats.maxHp);
|
||||
const heal = Math.max(1, flat + byPerc);
|
||||
const heal = calculateHealingFromFood(food, stats.maxHp);
|
||||
|
||||
// Consumir el ítem
|
||||
const { consumed } = await consumeItemByKey(userId, guildId, item.key, 1);
|
||||
if (consumed <= 0) throw new Error('No tienes este ítem');
|
||||
if (consumed <= 0) throw new Error("No tienes este ítem");
|
||||
|
||||
// Aplicar curación
|
||||
await adjustHP(userId, guildId, heal);
|
||||
@@ -40,4 +38,3 @@ export async function useConsumableByKey(userId: string, guildId: string, itemKe
|
||||
|
||||
return { healed: heal } as const;
|
||||
}
|
||||
|
||||
|
||||
16
src/game/consumables/utils.ts
Normal file
16
src/game/consumables/utils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export function getCooldownKeyForFood(
|
||||
itemKey: string,
|
||||
foodProps: any | undefined
|
||||
) {
|
||||
return foodProps?.cooldownKey ?? `food:${itemKey}`;
|
||||
}
|
||||
|
||||
export function calculateHealingFromFood(
|
||||
foodProps: any | undefined,
|
||||
maxHp: number
|
||||
) {
|
||||
const flat = Math.max(0, (foodProps?.healHp ?? 0) as number);
|
||||
const perc = Math.max(0, (foodProps?.healPercent ?? 0) as number);
|
||||
const byPerc = Math.floor((perc / 100) * maxHp);
|
||||
return Math.max(1, flat + byPerc);
|
||||
}
|
||||
55
src/game/core/utils.ts
Normal file
55
src/game/core/utils.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { prisma } from "../../core/database/prisma";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
export function now(): Date {
|
||||
return new Date();
|
||||
}
|
||||
|
||||
export function isWithin(
|
||||
date: Date,
|
||||
from?: Date | null,
|
||||
to?: Date | null
|
||||
): boolean {
|
||||
if (from && date < from) return false;
|
||||
if (to && date > to) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function ensureArray<T>(v: T[] | undefined | null): T[] {
|
||||
return Array.isArray(v) ? v : [];
|
||||
}
|
||||
|
||||
export function parseItemProps(json: unknown): any {
|
||||
if (!json || typeof json !== "object") return {};
|
||||
return json as any;
|
||||
}
|
||||
|
||||
export function parseState(json: unknown): any {
|
||||
if (!json || typeof json !== "object") return {};
|
||||
return json as any;
|
||||
}
|
||||
|
||||
export async function updateInventoryEntryState(
|
||||
userId: string,
|
||||
guildId: string,
|
||||
itemId: string,
|
||||
state: any
|
||||
) {
|
||||
const quantity =
|
||||
state.instances && Array.isArray(state.instances)
|
||||
? state.instances.length
|
||||
: 0;
|
||||
return prisma.inventoryEntry.update({
|
||||
where: { userId_guildId_itemId: { userId, guildId, itemId } },
|
||||
data: { state: state as unknown as Prisma.InputJsonValue, quantity },
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
now,
|
||||
isWithin,
|
||||
ensureArray,
|
||||
parseItemProps,
|
||||
parseState,
|
||||
updateInventoryEntryState,
|
||||
};
|
||||
@@ -7,17 +7,17 @@ import type {
|
||||
} from "./types";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import { ensureUserAndGuildExist } from "../core/userService";
|
||||
import coreUtils from "../core/utils";
|
||||
|
||||
// Utilidades de tiempo
|
||||
function now(): Date {
|
||||
return new Date();
|
||||
}
|
||||
|
||||
function isWithin(date: Date, from?: Date | null, to?: Date | null): boolean {
|
||||
if (from && date < from) return false;
|
||||
if (to && date > to) return false;
|
||||
return true;
|
||||
}
|
||||
// Reusar utilidades centrales desde game core
|
||||
const {
|
||||
now,
|
||||
isWithin,
|
||||
ensureArray,
|
||||
parseItemProps,
|
||||
parseState,
|
||||
updateInventoryEntryState,
|
||||
} = coreUtils as any;
|
||||
|
||||
// Resuelve un EconomyItem por key con alcance de guild o global
|
||||
export async function findItemByKey(guildId: string, key: string) {
|
||||
@@ -89,15 +89,8 @@ export async function getInventoryEntry(
|
||||
return { item, entry } as const;
|
||||
}
|
||||
|
||||
function parseItemProps(json: unknown): ItemProps {
|
||||
if (!json || typeof json !== "object") return {};
|
||||
return json as ItemProps;
|
||||
}
|
||||
|
||||
function parseState(json: unknown): InventoryState {
|
||||
if (!json || typeof json !== "object") return {};
|
||||
return json as InventoryState;
|
||||
}
|
||||
// utilidades parseItemProps, parseState, ensureArray y updateInventoryEntryState
|
||||
// provistas por coreUtils importado arriba
|
||||
|
||||
function checkUsableWindow(item: {
|
||||
usableFrom: Date | null;
|
||||
@@ -156,7 +149,7 @@ export async function addItemByKey(
|
||||
} else {
|
||||
// No apilable: usar state.instances
|
||||
const state = parseState(entry.state);
|
||||
state.instances ??= [];
|
||||
state.instances = ensureArray(state.instances);
|
||||
const canAdd = Math.max(
|
||||
0,
|
||||
Math.min(qty, Math.max(0, max - state.instances.length))
|
||||
@@ -173,13 +166,12 @@ export async function addItemByKey(
|
||||
state.instances.push({});
|
||||
}
|
||||
}
|
||||
const updated = await prisma.inventoryEntry.update({
|
||||
where: { userId_guildId_itemId: { userId, guildId, itemId: item.id } },
|
||||
data: {
|
||||
state: state as unknown as Prisma.InputJsonValue,
|
||||
quantity: state.instances.length,
|
||||
},
|
||||
});
|
||||
const updated = await updateInventoryEntryState(
|
||||
userId,
|
||||
guildId,
|
||||
item.id,
|
||||
state
|
||||
);
|
||||
return { added: canAdd, entry: updated } as const;
|
||||
}
|
||||
}
|
||||
@@ -203,18 +195,16 @@ export async function consumeItemByKey(
|
||||
return { consumed, entry: updated } as const;
|
||||
} else {
|
||||
const state = parseState(entry.state);
|
||||
const instances = state.instances ?? [];
|
||||
const consumed = Math.min(qty, instances.length);
|
||||
state.instances = ensureArray(state.instances);
|
||||
const consumed = Math.min(qty, state.instances.length);
|
||||
if (consumed === 0) return { consumed: 0 } as const;
|
||||
instances.splice(0, consumed);
|
||||
const newState: InventoryState = { ...state, instances };
|
||||
const updated = await prisma.inventoryEntry.update({
|
||||
where: { userId_guildId_itemId: { userId, guildId, itemId: item.id } },
|
||||
data: {
|
||||
state: newState as unknown as Prisma.InputJsonValue,
|
||||
quantity: instances.length,
|
||||
},
|
||||
});
|
||||
state.instances.splice(0, consumed);
|
||||
const updated = await updateInventoryEntryState(
|
||||
userId,
|
||||
guildId,
|
||||
item.id,
|
||||
state
|
||||
);
|
||||
return { consumed, entry: updated } as const;
|
||||
}
|
||||
}
|
||||
@@ -232,7 +222,7 @@ export async function openChestByKey(
|
||||
const props = parseItemProps(item.props);
|
||||
const chest = props.chest ?? {};
|
||||
if (!chest.enabled) throw new Error("Este ítem no se puede abrir");
|
||||
const rewards = Array.isArray(chest.rewards) ? chest.rewards : [];
|
||||
const rewards: any[] = Array.isArray(chest.rewards) ? chest.rewards : [];
|
||||
const mode = chest.randomMode || "all";
|
||||
const result: OpenChestResult = {
|
||||
coinsDelta: 0,
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { logToolBreak } from "../lib/toolBreakLog";
|
||||
import { updateStats } from "../stats/service"; // 🟩 local authoritative
|
||||
import type { ItemProps, InventoryState } from "../economy/types";
|
||||
import { parseItemProps, parseState as parseInvState } from "../core/utils";
|
||||
import type {
|
||||
LevelRequirements,
|
||||
RunMinigameOptions,
|
||||
@@ -120,15 +121,7 @@ async function ensureAreaAndLevel(
|
||||
return { area, lvl } as const;
|
||||
}
|
||||
|
||||
function parseItemProps(json: unknown): ItemProps {
|
||||
if (!json || typeof json !== "object") return {};
|
||||
return json as ItemProps;
|
||||
}
|
||||
|
||||
function parseInvState(json: unknown): InventoryState {
|
||||
if (!json || typeof json !== "object") return {};
|
||||
return json as InventoryState;
|
||||
}
|
||||
// parseItemProps y parseInvState son importados desde ../core/utils para centralizar parsing
|
||||
|
||||
async function validateRequirements(
|
||||
userId: string,
|
||||
@@ -280,6 +273,26 @@ async function sampleMobs(mobs?: MobsTable): Promise<string[]> {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Devuelve instancias de mobs escaladas por nivel (usa getMobInstance de mobData)
|
||||
import { getMobInstance } from "../mobs/mobData";
|
||||
|
||||
async function sampleMobInstances(
|
||||
mobs?: MobsTable,
|
||||
areaLevel = 1
|
||||
): Promise<ReturnType<typeof getMobInstance>[]> {
|
||||
const out: ReturnType<typeof getMobInstance>[] = [];
|
||||
if (!mobs || !Array.isArray(mobs.table) || mobs.table.length === 0)
|
||||
return out;
|
||||
const draws = Math.max(0, mobs.draws ?? 0);
|
||||
for (let i = 0; i < draws; i++) {
|
||||
const pick = pickWeighted(mobs.table);
|
||||
if (!pick) continue;
|
||||
const inst = getMobInstance(pick.mobKey, areaLevel);
|
||||
if (inst) out.push(inst);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function reduceToolDurability(
|
||||
userId: string,
|
||||
guildId: string,
|
||||
@@ -445,7 +458,7 @@ export async function runMinigame(
|
||||
guildId,
|
||||
rewards
|
||||
);
|
||||
const mobsSpawned = await sampleMobs(mobs);
|
||||
const mobsSpawned = await sampleMobInstances(mobs, level);
|
||||
|
||||
// Reducir durabilidad de herramienta si se usó
|
||||
let toolInfo: RunResult["tool"] | undefined;
|
||||
@@ -478,8 +491,8 @@ export async function runMinigame(
|
||||
|
||||
if (!hasWeapon) {
|
||||
// Registrar derrota simple contra la lista de mobs (no se derrotan mobs).
|
||||
const mobLogs: CombatSummary["mobs"] = mobsSpawned.map((mk) => ({
|
||||
mobKey: mk,
|
||||
const mobLogs: CombatSummary["mobs"] = mobsSpawned.map((m) => ({
|
||||
mobKey: m?.key ?? "unknown",
|
||||
maxHp: 0,
|
||||
defeated: false,
|
||||
totalDamageDealt: 0,
|
||||
@@ -596,16 +609,20 @@ export async function runMinigame(
|
||||
const factor = 0.8 + Math.random() * 0.4; // 0.8 - 1.2
|
||||
return base * factor;
|
||||
};
|
||||
for (const mobKey of mobsSpawned) {
|
||||
for (const mob of mobsSpawned) {
|
||||
if (currentHp <= 0) break; // jugador derrotado antes de iniciar este mob
|
||||
// Stats simples del mob (placeholder mejorable con tabla real)
|
||||
const mobBaseHp = 10 + Math.floor(Math.random() * 6); // 10-15
|
||||
// Stats simples del mob (usamos la instancia escalada)
|
||||
const mobBaseHp = Math.max(1, Math.floor(mob?.scaled?.hp ?? 10));
|
||||
let mobHp = mobBaseHp;
|
||||
const rounds: any[] = [];
|
||||
let round = 1;
|
||||
let mobDamageDealt = 0; // daño que jugador hace a este mob
|
||||
let mobDamageTakenFromMob = 0; // daño que jugador recibe de este mob
|
||||
while (mobHp > 0 && currentHp > 0 && round <= 12) {
|
||||
while (
|
||||
mobHp > 0 &&
|
||||
currentHp > 0 &&
|
||||
round <= (mob?.behavior?.maxRounds ?? 12)
|
||||
) {
|
||||
// Daño jugador -> mob
|
||||
const playerRaw = variance(eff.damage || 1) + 1; // asegurar >=1
|
||||
const playerDamage = Math.max(1, Math.round(playerRaw));
|
||||
@@ -627,7 +644,7 @@ export async function runMinigame(
|
||||
}
|
||||
}
|
||||
rounds.push({
|
||||
mobKey,
|
||||
mobKey: mob?.key ?? "unknown",
|
||||
round,
|
||||
playerDamageDealt: playerDamage,
|
||||
playerDamageTaken: playerTaken,
|
||||
@@ -642,7 +659,7 @@ export async function runMinigame(
|
||||
round++;
|
||||
}
|
||||
mobLogs.push({
|
||||
mobKey,
|
||||
mobKey: mob?.key ?? "unknown",
|
||||
maxHp: mobBaseHp,
|
||||
defeated: mobHp <= 0,
|
||||
totalDamageDealt: mobDamageDealt,
|
||||
@@ -830,7 +847,7 @@ export async function runMinigame(
|
||||
|
||||
const resultJson: Prisma.InputJsonValue = {
|
||||
rewards: delivered,
|
||||
mobs: mobsSpawned,
|
||||
mobs: mobsSpawned.map((m) => m?.key ?? "unknown"),
|
||||
tool: toolInfo,
|
||||
weaponTool: weaponToolInfo,
|
||||
combat: combatSummary,
|
||||
@@ -879,7 +896,7 @@ export async function runMinigame(
|
||||
return {
|
||||
success: true,
|
||||
rewards: delivered,
|
||||
mobs: mobsSpawned,
|
||||
mobs: mobsSpawned.map((m) => m?.key ?? "unknown"),
|
||||
tool: toolInfo,
|
||||
weaponTool: weaponToolInfo,
|
||||
combat: combatSummary,
|
||||
|
||||
16
src/game/mobs/README.md
Normal file
16
src/game/mobs/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Mobs module
|
||||
|
||||
## Propósito
|
||||
|
||||
- Contener definiciones de mobs (plantillas) y helpers para obtener instancias escaladas por nivel.
|
||||
|
||||
## Convenciones
|
||||
|
||||
- `MOB_DEFINITIONS` contiene objetos `BaseMobDefinition` con configuración declarativa.
|
||||
- Usar `getMobInstance(key, areaLevel)` para obtener una instancia lista para combate.
|
||||
- Evitar lógica de combate en este archivo; este módulo solo expone datos y transformaciones determinísticas.
|
||||
|
||||
## Futuro
|
||||
|
||||
- Migrar `MOB_DEFINITIONS` a la base de datos o AppWrite y añadir cache si se requiere edición en runtime.
|
||||
- Añadir validadores y tests para las definiciones.
|
||||
236
src/game/mobs/admin.ts
Normal file
236
src/game/mobs/admin.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import { prisma } from "../../core/database/prisma";
|
||||
import { z } from "zod";
|
||||
import { BaseMobDefinition, MOB_DEFINITIONS, findMobDef } from "./mobData";
|
||||
|
||||
const BaseMobDefinitionSchema = z.object({
|
||||
key: z.string(),
|
||||
name: z.string(),
|
||||
tier: z.number().int().nonnegative(),
|
||||
base: z.object({
|
||||
hp: z.number(),
|
||||
attack: z.number(),
|
||||
defense: z.number().optional(),
|
||||
}),
|
||||
scaling: z
|
||||
.object({
|
||||
hpPerLevel: z.number().optional(),
|
||||
attackPerLevel: z.number().optional(),
|
||||
defensePerLevel: z.number().optional(),
|
||||
hpMultiplierPerTier: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
rewardMods: z
|
||||
.object({
|
||||
coinMultiplier: z.number().optional(),
|
||||
extraDropChance: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
behavior: z
|
||||
.object({
|
||||
maxRounds: z.number().optional(),
|
||||
aggressive: z.boolean().optional(),
|
||||
critChance: z.number().optional(),
|
||||
critMultiplier: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
type MobInput = z.infer<typeof BaseMobDefinitionSchema>;
|
||||
|
||||
export type CreateOrUpdateResult = {
|
||||
def: BaseMobDefinition;
|
||||
row?: any;
|
||||
};
|
||||
|
||||
function prismaMobAvailable(): boolean {
|
||||
const anyPrisma: any = prisma as any;
|
||||
if (!process.env.XATA_DB) return false;
|
||||
return !!(
|
||||
anyPrisma &&
|
||||
anyPrisma.mob &&
|
||||
typeof anyPrisma.mob.create === "function"
|
||||
);
|
||||
}
|
||||
|
||||
export async function listMobs(): Promise<BaseMobDefinition[]> {
|
||||
const rows = await listMobsWithRows();
|
||||
return rows.map((r) => r.def);
|
||||
}
|
||||
|
||||
export type MobWithRow = {
|
||||
def: BaseMobDefinition;
|
||||
id?: string | null;
|
||||
guildId?: string | null;
|
||||
isDb?: boolean;
|
||||
};
|
||||
|
||||
export async function listMobsWithRows(): Promise<MobWithRow[]> {
|
||||
const map: Record<string, MobWithRow> = {};
|
||||
// Start with built-ins
|
||||
for (const d of MOB_DEFINITIONS) {
|
||||
map[d.key] = { def: d, id: null, guildId: null, isDb: false };
|
||||
}
|
||||
|
||||
if (!prismaMobAvailable()) {
|
||||
return Object.values(map);
|
||||
}
|
||||
|
||||
try {
|
||||
const anyPrisma: any = prisma as any;
|
||||
const rows = await anyPrisma.mob.findMany();
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(`listMobsWithRows: DB returned ${rows.length} rows`);
|
||||
for (const r of rows) {
|
||||
const cfg =
|
||||
r.metadata ??
|
||||
r.stats ??
|
||||
r.drops ??
|
||||
r.config ??
|
||||
r.definition ??
|
||||
r.data ??
|
||||
null;
|
||||
if (!cfg || typeof cfg !== "object") continue;
|
||||
try {
|
||||
const parsed = BaseMobDefinitionSchema.parse(cfg as any);
|
||||
map[parsed.key] = {
|
||||
def: parsed,
|
||||
id: r.id ?? null,
|
||||
guildId: r.guildId ?? null,
|
||||
isDb: true,
|
||||
};
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
"Skipping invalid mob row id=",
|
||||
r.id,
|
||||
(e as any)?.errors ?? e
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("listMobsWithRows: DB read failed:", (e as any)?.message ?? e);
|
||||
}
|
||||
|
||||
return Object.values(map).sort((a, b) => a.def.key.localeCompare(b.def.key));
|
||||
}
|
||||
|
||||
export async function getMob(key: string): Promise<BaseMobDefinition | null> {
|
||||
// Check DB first
|
||||
if (prismaMobAvailable()) {
|
||||
try {
|
||||
const anyPrisma: any = prisma as any;
|
||||
const row = await anyPrisma.mob.findFirst({ where: { key } });
|
||||
if (row) {
|
||||
const cfg =
|
||||
row.metadata ??
|
||||
row.stats ??
|
||||
row.drops ??
|
||||
row.config ??
|
||||
row.definition ??
|
||||
row.data ??
|
||||
null;
|
||||
if (cfg) {
|
||||
try {
|
||||
return BaseMobDefinitionSchema.parse(cfg as any);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore DB issues
|
||||
}
|
||||
}
|
||||
// Fallback to built-ins
|
||||
return findMobDef(key);
|
||||
}
|
||||
|
||||
export async function createOrUpdateMob(
|
||||
input: MobInput & { guildId?: string; category?: string }
|
||||
): Promise<CreateOrUpdateResult> {
|
||||
const parsed = BaseMobDefinitionSchema.parse(input);
|
||||
let row: any | undefined;
|
||||
if (prismaMobAvailable()) {
|
||||
try {
|
||||
const anyPrisma: any = prisma as any;
|
||||
const where: any = { key: parsed.key };
|
||||
if (input.guildId) where.guildId = input.guildId;
|
||||
const existing = await anyPrisma.mob.findFirst({ where });
|
||||
if (existing) {
|
||||
row = await anyPrisma.mob.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
name: parsed.name,
|
||||
category: (input as any).category ?? null,
|
||||
metadata: parsed,
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(
|
||||
`createOrUpdateMob: updated mob id=${row.id} key=${parsed.key}`
|
||||
);
|
||||
} else {
|
||||
row = await anyPrisma.mob.create({
|
||||
data: {
|
||||
key: parsed.key,
|
||||
name: parsed.name,
|
||||
category: (input as any).category ?? null,
|
||||
guildId: input.guildId ?? null,
|
||||
metadata: parsed,
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(
|
||||
`createOrUpdateMob: created mob id=${row.id} key=${parsed.key}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// if DB fails, fallthrough to return parsed but do not throw
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
"createOrUpdateMob: DB save failed:",
|
||||
(e as any)?.message ?? e
|
||||
);
|
||||
}
|
||||
}
|
||||
return { def: parsed, row };
|
||||
}
|
||||
|
||||
export async function deleteMob(key: string): Promise<boolean> {
|
||||
if (prismaMobAvailable()) {
|
||||
try {
|
||||
const anyPrisma: any = prisma as any;
|
||||
const existing = await anyPrisma.mob.findFirst({ where: { key } });
|
||||
if (existing) {
|
||||
await anyPrisma.mob.delete({ where: { id: existing.id } });
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(`deleteMob: deleted mob id=${existing.id} key=${key}`);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("deleteMob: DB delete failed:", (e as any)?.message ?? e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// If no DB or not found, attempt to delete from in-memory builtins (no-op)
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function ensureMobRepoUpToDate() {
|
||||
// helper to tell mobData to refresh caches — import dynamically to avoid cycles
|
||||
try {
|
||||
const mod = await import("./mobData.js");
|
||||
if (typeof mod.refreshMobDefinitionsFromDb === "function") {
|
||||
await mod.refreshMobDefinitionsFromDb();
|
||||
}
|
||||
if (typeof mod.validateAllMobDefs === "function") {
|
||||
mod.validateAllMobDefs();
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
@@ -53,10 +53,6 @@ export const MOB_DEFINITIONS: BaseMobDefinition[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export function findMobDef(key: string) {
|
||||
return MOB_DEFINITIONS.find((m) => m.key === key) || null;
|
||||
}
|
||||
|
||||
export function computeMobStats(def: BaseMobDefinition, areaLevel: number) {
|
||||
const lvl = Math.max(1, areaLevel);
|
||||
const s = def.scaling || {};
|
||||
@@ -70,3 +66,206 @@ export function computeMobStats(def: BaseMobDefinition, areaLevel: number) {
|
||||
).toFixed(2);
|
||||
return { hp, attack: atk, defense: defVal };
|
||||
}
|
||||
|
||||
/**
|
||||
* MobInstance: representación de una entidad mob lista para usarse en combate.
|
||||
* - incluye stats escaladas por nivel de área (hp, attack, defense)
|
||||
* - preserva la definición base para referencias (name, tier, tags, behavior)
|
||||
*/
|
||||
export interface MobInstance {
|
||||
key: string;
|
||||
name: string;
|
||||
tier: number;
|
||||
base: BaseMobDefinition["base"];
|
||||
scaled: { hp: number; attack: number; defense: number };
|
||||
tags?: string[];
|
||||
rewardMods?: BaseMobDefinition["rewardMods"];
|
||||
behavior?: BaseMobDefinition["behavior"];
|
||||
}
|
||||
|
||||
/**
|
||||
* getMobInstance: devuelve una instancia de mob con stats calculadas.
|
||||
* Si la definición no existe, devuelve null.
|
||||
*/
|
||||
export function getMobInstance(
|
||||
key: string,
|
||||
areaLevel: number
|
||||
): MobInstance | null {
|
||||
const def = findMobDef(key);
|
||||
if (!def) return null;
|
||||
const scaled = computeMobStats(def, areaLevel);
|
||||
return {
|
||||
key: def.key,
|
||||
name: def.name,
|
||||
tier: def.tier,
|
||||
base: def.base,
|
||||
scaled,
|
||||
tags: def.tags,
|
||||
rewardMods: def.rewardMods,
|
||||
behavior: def.behavior,
|
||||
};
|
||||
}
|
||||
|
||||
export function listMobKeys(): string[] {
|
||||
return MOB_DEFINITIONS.map((m) => m.key);
|
||||
}
|
||||
|
||||
// --- DB-backed optional loader + simple validation ---
|
||||
import { prisma } from "../../core/database/prisma";
|
||||
import { z } from "zod";
|
||||
|
||||
const BaseMobDefinitionSchema = z.object({
|
||||
key: z.string(),
|
||||
name: z.string(),
|
||||
tier: z.number().int().nonnegative(),
|
||||
base: z.object({
|
||||
hp: z.number(),
|
||||
attack: z.number(),
|
||||
defense: z.number().optional(),
|
||||
}),
|
||||
scaling: z
|
||||
.object({
|
||||
hpPerLevel: z.number().optional(),
|
||||
attackPerLevel: z.number().optional(),
|
||||
defensePerLevel: z.number().optional(),
|
||||
hpMultiplierPerTier: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
rewardMods: z
|
||||
.object({
|
||||
coinMultiplier: z.number().optional(),
|
||||
extraDropChance: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
behavior: z
|
||||
.object({
|
||||
maxRounds: z.number().optional(),
|
||||
aggressive: z.boolean().optional(),
|
||||
critChance: z.number().optional(),
|
||||
critMultiplier: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
// Cache for DB-loaded definitions (key -> def)
|
||||
const dbMobDefs: Record<string, BaseMobDefinition> = {};
|
||||
|
||||
/**
|
||||
* Try to refresh mob definitions from the database. This is optional and
|
||||
* fails silently if the Prisma model/table doesn't exist or an error occurs.
|
||||
* Call this during server startup to load editable mobs.
|
||||
*/
|
||||
export async function refreshMobDefinitionsFromDb() {
|
||||
try {
|
||||
// If no DB configured, skip
|
||||
if (!process.env.XATA_DB) return;
|
||||
const anyPrisma: any = prisma as any;
|
||||
if (!anyPrisma.mob || typeof anyPrisma.mob.findMany !== "function") {
|
||||
// Prisma model `mob` not present — skip quietly
|
||||
return;
|
||||
}
|
||||
const rows = await anyPrisma.mob.findMany();
|
||||
// rows expected to contain a JSON/config column (we try `config` or `definition`)
|
||||
const BaseMobDefinitionSchema = z.object({
|
||||
key: z.string(),
|
||||
name: z.string(),
|
||||
tier: z.number().int().nonnegative(),
|
||||
base: z.object({
|
||||
hp: z.number(),
|
||||
attack: z.number(),
|
||||
defense: z.number().optional(),
|
||||
}),
|
||||
scaling: z
|
||||
.object({
|
||||
hpPerLevel: z.number().optional(),
|
||||
attackPerLevel: z.number().optional(),
|
||||
defensePerLevel: z.number().optional(),
|
||||
hpMultiplierPerTier: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
rewardMods: z
|
||||
.object({
|
||||
coinMultiplier: z.number().optional(),
|
||||
extraDropChance: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
behavior: z
|
||||
.object({
|
||||
maxRounds: z.number().optional(),
|
||||
aggressive: z.boolean().optional(),
|
||||
critChance: z.number().optional(),
|
||||
critMultiplier: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
for (const r of rows) {
|
||||
// Prisma model Mob stores arbitrary data in `metadata`, but some projects
|
||||
// may place structured stats in `stats` or `drops`. Try those fields.
|
||||
const cfg =
|
||||
r.metadata ??
|
||||
r.stats ??
|
||||
r.drops ??
|
||||
r.config ??
|
||||
r.definition ??
|
||||
r.data ??
|
||||
null;
|
||||
if (!cfg || typeof cfg !== "object") continue;
|
||||
try {
|
||||
const parsed = BaseMobDefinitionSchema.parse(cfg as any);
|
||||
dbMobDefs[parsed.key] = parsed as BaseMobDefinition;
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
"Invalid mob definition in DB for row id=",
|
||||
r.id,
|
||||
(e as any)?.message ?? e
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// silently ignore DB issues — keep in-memory definitions as source of truth
|
||||
// but log to console for debugging
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
"refreshMobDefinitionsFromDb: could not load mobs from DB:",
|
||||
(err && (err as Error).message) || err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find mob definition checking DB-loaded defs first, then built-in definitions.
|
||||
*/
|
||||
export function findMobDef(key: string) {
|
||||
if (dbMobDefs[key]) return dbMobDefs[key];
|
||||
return MOB_DEFINITIONS.find((m) => m.key === key) || null;
|
||||
}
|
||||
|
||||
export function validateAllMobDefs() {
|
||||
const bad: string[] = [];
|
||||
for (const m of MOB_DEFINITIONS) {
|
||||
const r = BaseMobDefinitionSchema.safeParse(m);
|
||||
if (!r.success) bad.push(m.key ?? "<unknown>");
|
||||
}
|
||||
for (const k of Object.keys(dbMobDefs)) {
|
||||
const r = BaseMobDefinitionSchema.safeParse(dbMobDefs[k]);
|
||||
if (!r.success) bad.push(k);
|
||||
}
|
||||
if (bad.length) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("validateAllMobDefs: invalid mob defs:", bad);
|
||||
}
|
||||
return bad.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize mob repository: attempt to refresh from DB and validate definitions.
|
||||
* Call this on server start (optional).
|
||||
*/
|
||||
export async function initializeMobRepository() {
|
||||
await refreshMobDefinitionsFromDb();
|
||||
validateAllMobDefs();
|
||||
}
|
||||
|
||||
@@ -1,32 +1,38 @@
|
||||
import { prisma } from '../../core/database/prisma';
|
||||
import type { ItemProps } from '../economy/types';
|
||||
import { findItemByKey, getInventoryEntry } from '../economy/service';
|
||||
|
||||
function parseItemProps(json: unknown): ItemProps {
|
||||
if (!json || typeof json !== 'object') return {};
|
||||
return json as ItemProps;
|
||||
}
|
||||
import { prisma } from "../../core/database/prisma";
|
||||
import type { ItemProps } from "../economy/types";
|
||||
import { findItemByKey, getInventoryEntry } from "../economy/service";
|
||||
import { parseItemProps } from "../core/utils";
|
||||
|
||||
export async function findMutationByKey(guildId: string, key: string) {
|
||||
return prisma.itemMutation.findFirst({
|
||||
where: { key, OR: [{ guildId }, { guildId: null }] },
|
||||
orderBy: [{ guildId: 'desc' }],
|
||||
orderBy: [{ guildId: "desc" }],
|
||||
});
|
||||
}
|
||||
|
||||
export async function applyMutationToInventory(userId: string, guildId: string, itemKey: string, mutationKey: string) {
|
||||
const { item, entry } = await getInventoryEntry(userId, guildId, itemKey, { createIfMissing: true });
|
||||
if (!entry) throw new Error('Inventario inexistente');
|
||||
export async function applyMutationToInventory(
|
||||
userId: string,
|
||||
guildId: string,
|
||||
itemKey: string,
|
||||
mutationKey: string
|
||||
) {
|
||||
const { item, entry } = await getInventoryEntry(userId, guildId, itemKey, {
|
||||
createIfMissing: true,
|
||||
});
|
||||
if (!entry) throw new Error("Inventario inexistente");
|
||||
|
||||
const props = parseItemProps(item.props);
|
||||
const policy = props.mutationPolicy;
|
||||
if (policy?.deniedKeys?.includes(mutationKey)) throw new Error('Mutación denegada');
|
||||
if (policy?.allowedKeys && !policy.allowedKeys.includes(mutationKey)) throw new Error('Mutación no permitida');
|
||||
if (policy?.deniedKeys?.includes(mutationKey))
|
||||
throw new Error("Mutación denegada");
|
||||
if (policy?.allowedKeys && !policy.allowedKeys.includes(mutationKey))
|
||||
throw new Error("Mutación no permitida");
|
||||
|
||||
const mutation = await findMutationByKey(guildId, mutationKey);
|
||||
if (!mutation) throw new Error('Mutación no encontrada');
|
||||
if (!mutation) throw new Error("Mutación no encontrada");
|
||||
|
||||
await prisma.inventoryItemMutation.create({ data: { inventoryId: entry.id, mutationId: mutation.id } });
|
||||
await prisma.inventoryItemMutation.create({
|
||||
data: { inventoryId: entry.id, mutationId: mutation.id },
|
||||
});
|
||||
return { ok: true } as const;
|
||||
}
|
||||
|
||||
|
||||
@@ -212,6 +212,15 @@ async function bootstrap() {
|
||||
logger.error({ err: e }, "Error cargando eventos");
|
||||
}
|
||||
|
||||
// Inicializar repositorio de mobs (intenta cargar mobs desde DB si existe)
|
||||
try {
|
||||
// import dinamico para evitar ciclos en startup
|
||||
const { initializeMobRepository } = await import("./game/mobs/mobData.js");
|
||||
await initializeMobRepository();
|
||||
} catch (e) {
|
||||
logger.warn({ err: e }, "No se pudo inicializar el repositorio de mobs");
|
||||
}
|
||||
|
||||
// Registrar comandos en segundo plano con reintentos; no bloquea el arranque del bot
|
||||
withRetry("Registrar slash commands", async () => {
|
||||
await registeringCommands();
|
||||
|
||||
Reference in New Issue
Block a user