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:
@@ -18,6 +18,7 @@
|
|||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"seed:minigames": "npx tsx src/game/minigames/seed.ts",
|
"seed:minigames": "npx tsx src/game/minigames/seed.ts",
|
||||||
|
"test:mobs": "npx tsx scripts/testMobData.ts",
|
||||||
"start:optimize-relic": "NODE_ENV=production ENABLE_MEMORY_OPTIMIZER=true NEW_RELIC_APP_NAME=amayo NEW_RELIC_LICENSE_KEY= NODE_OPTIONS='--max-old-space-size=512 --expose-gc' npx tsx --experimental-loader=newrelic/esm-loader.mjs src/main.ts"
|
"start:optimize-relic": "NODE_ENV=production ENABLE_MEMORY_OPTIMIZER=true NEW_RELIC_APP_NAME=amayo NEW_RELIC_LICENSE_KEY= NODE_OPTIONS='--max-old-space-size=512 --expose-gc' npx tsx --experimental-loader=newrelic/esm-loader.mjs src/main.ts"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
"newrelic": "13.4.0",
|
"newrelic": "13.4.0",
|
||||||
"node-appwrite": "19.1.0",
|
"node-appwrite": "19.1.0",
|
||||||
"pino": "9.13.0",
|
"pino": "9.13.0",
|
||||||
|
"zod": "4.25.1",
|
||||||
"prisma": "6.16.2",
|
"prisma": "6.16.2",
|
||||||
"redis": "5.8.2"
|
"redis": "5.8.2"
|
||||||
},
|
},
|
||||||
|
|||||||
41
scripts/mobAdminTest.ts
Normal file
41
scripts/mobAdminTest.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
createOrUpdateMob,
|
||||||
|
listMobs,
|
||||||
|
getMob,
|
||||||
|
deleteMob,
|
||||||
|
ensureMobRepoUpToDate,
|
||||||
|
} from "../src/game/mobs/admin";
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log("Ensuring repo up-to-date...");
|
||||||
|
await ensureMobRepoUpToDate();
|
||||||
|
|
||||||
|
const testMob = {
|
||||||
|
key: "test.goblin",
|
||||||
|
name: "Goblin Test",
|
||||||
|
tier: 1,
|
||||||
|
base: { hp: 12, attack: 3 },
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
console.log("Creating test mob...");
|
||||||
|
const created = await createOrUpdateMob(testMob);
|
||||||
|
console.log("Created:", created.key);
|
||||||
|
|
||||||
|
console.log("Listing mobs (sample):");
|
||||||
|
const all = await listMobs();
|
||||||
|
console.log(`Total mobs: ${all.length}`);
|
||||||
|
console.log(all.map((m) => m.key).join(", "));
|
||||||
|
|
||||||
|
console.log("Fetching test.mob...");
|
||||||
|
const fetched = await getMob("test.goblin");
|
||||||
|
console.log("Fetched:", !!fetched, fetched ? fetched : "(no data)");
|
||||||
|
|
||||||
|
console.log("Deleting test mob...");
|
||||||
|
const deleted = await deleteMob("test.goblin");
|
||||||
|
console.log("Deleted?", deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
18
scripts/testMobData.ts
Normal file
18
scripts/testMobData.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import {
|
||||||
|
initializeMobRepository,
|
||||||
|
getMobInstance,
|
||||||
|
listMobKeys,
|
||||||
|
} from "../src/game/mobs/mobData";
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log("Initializing mob repository...");
|
||||||
|
await initializeMobRepository();
|
||||||
|
console.log("Available mob keys:", listMobKeys());
|
||||||
|
const inst = getMobInstance("slime.green", 3);
|
||||||
|
console.log("Sample slime.green @ lvl3 ->", inst);
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -1,19 +1,25 @@
|
|||||||
import type { CommandMessage } from '../../../core/types/commands';
|
import type { CommandMessage } from "../../../core/types/commands";
|
||||||
import type Amayo from '../../../core/client';
|
import type Amayo from "../../../core/client";
|
||||||
import { hasManageGuildOrStaff } from '../../../core/lib/permissions';
|
import { hasManageGuildOrStaff } from "../../../core/lib/permissions";
|
||||||
import { prisma } from '../../../core/database/prisma';
|
import { prisma } from "../../../core/database/prisma";
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'mob-eliminar',
|
name: "mob-eliminar",
|
||||||
type: 'message',
|
type: "message",
|
||||||
aliases: ['eliminar-mob', 'mob-delete'],
|
aliases: ["eliminar-mob", "mob-delete"],
|
||||||
cooldown: 5,
|
cooldown: 5,
|
||||||
description: 'Eliminar un mob del servidor',
|
description: "Eliminar un mob del servidor",
|
||||||
usage: 'mob-eliminar <key>',
|
usage: "mob-eliminar <key>",
|
||||||
run: async (message, args, client: Amayo) => {
|
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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,23 +27,21 @@ export const command: CommandMessage = {
|
|||||||
const key = args[0]?.trim();
|
const key = args[0]?.trim();
|
||||||
|
|
||||||
if (!key) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mob = await prisma.mob.findFirst({
|
// Use admin.deleteMob to centralize logic
|
||||||
where: { key, guildId }
|
const { deleteMob } = await import("../../../game/mobs/admin.js");
|
||||||
});
|
const deleted = await deleteMob(key);
|
||||||
|
if (!deleted) {
|
||||||
if (!mob) {
|
await message.reply(
|
||||||
await message.reply(`❌ No se encontró el mob local con key ${key} en este servidor.`);
|
`❌ No se encontró el mob local con key ${key} en este servidor.`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.mob.delete({
|
|
||||||
where: { id: mob.id }
|
|
||||||
});
|
|
||||||
|
|
||||||
await message.reply(`✅ Mob ${key} eliminado exitosamente.`);
|
await message.reply(`✅ Mob ${key} eliminado exitosamente.`);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,65 +1,68 @@
|
|||||||
import type { CommandMessage } from '../../../core/types/commands';
|
import type { CommandMessage } from "../../../core/types/commands";
|
||||||
import type Amayo from '../../../core/client';
|
import type Amayo from "../../../core/client";
|
||||||
import { prisma } from '../../../core/database/prisma';
|
import { prisma } from "../../../core/database/prisma";
|
||||||
import { ComponentType, ButtonStyle } from 'discord-api-types/v10';
|
import { ComponentType, ButtonStyle } from "discord-api-types/v10";
|
||||||
import type { MessageComponentInteraction, TextBasedChannel } from 'discord.js';
|
import type { MessageComponentInteraction, TextBasedChannel } from "discord.js";
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'mobs-lista',
|
name: "mobs-lista",
|
||||||
type: 'message',
|
type: "message",
|
||||||
aliases: ['lista-mobs', 'mobs-list'],
|
aliases: ["lista-mobs", "mobs-list"],
|
||||||
cooldown: 5,
|
cooldown: 5,
|
||||||
description: 'Ver lista de todos los mobs del servidor',
|
description: "Ver lista de todos los mobs del servidor",
|
||||||
usage: 'mobs-lista [pagina]',
|
usage: "mobs-lista [pagina]",
|
||||||
run: async (message, args, client: Amayo) => {
|
run: async (message, args, client: Amayo) => {
|
||||||
const guildId = message.guild!.id;
|
const guildId = message.guild!.id;
|
||||||
const page = parseInt(args[0]) || 1;
|
const page = parseInt(args[0]) || 1;
|
||||||
const perPage = 6;
|
const perPage = 6;
|
||||||
|
|
||||||
const total = await prisma.mob.count({
|
// Use admin list (including built-ins and DB rows)
|
||||||
where: { OR: [{ guildId }, { guildId: null }] }
|
const { listMobsWithRows } = await import("../../../game/mobs/admin.js");
|
||||||
});
|
const all = await listMobsWithRows();
|
||||||
|
if (!all || all.length === 0) {
|
||||||
const mobs = await prisma.mob.findMany({
|
await message.reply("No hay mobs configurados en este servidor.");
|
||||||
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.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const total = all.length;
|
||||||
const totalPages = Math.ceil(total / perPage);
|
const totalPages = Math.ceil(total / perPage);
|
||||||
|
const pageItems = all.slice(
|
||||||
|
(page - 1) * perPage,
|
||||||
|
(page - 1) * perPage + perPage
|
||||||
|
);
|
||||||
|
|
||||||
const display = {
|
const display = {
|
||||||
type: 17,
|
type: 17,
|
||||||
accent_color: 0xFF0000,
|
accent_color: 0xff0000,
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
type: 9,
|
type: 9,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: `**👾 Lista de Mobs**\nPágina ${page}/${totalPages} • Total: ${total}`
|
content: `**👾 Lista de Mobs**\nPágina ${page}/${totalPages} • Total: ${total}`,
|
||||||
}]
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{ type: 14, divider: true },
|
{ type: 14, divider: true },
|
||||||
...mobs.map(mob => {
|
...pageItems.map((entry) => {
|
||||||
const stats = mob.stats as any || {};
|
const mob = entry.def;
|
||||||
|
const stats = (mob.base as any) || {};
|
||||||
return {
|
return {
|
||||||
type: 9,
|
type: 9,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: `**${mob.name || mob.key}**\n` +
|
content:
|
||||||
|
`**${mob.name || mob.key}**\n` +
|
||||||
`└ Key: \`${mob.key}\`\n` +
|
`└ Key: \`${mob.key}\`\n` +
|
||||||
`└ ATK: ${stats.attack || 0} | HP: ${stats.hp || 0}\n` +
|
`└ ATK: ${stats.attack || 0} | HP: ${stats.hp || 0}\n` +
|
||||||
`└ ${mob.guildId === guildId ? '📍 Local' : '🌐 Global'}`
|
`└ ${entry.guildId === guildId ? "📍 Local" : "🌐 Global"}`,
|
||||||
}]
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttons: any[] = [];
|
const buttons: any[] = [];
|
||||||
@@ -68,8 +71,8 @@ export const command: CommandMessage = {
|
|||||||
buttons.push({
|
buttons.push({
|
||||||
type: ComponentType.Button,
|
type: ComponentType.Button,
|
||||||
style: ButtonStyle.Secondary,
|
style: ButtonStyle.Secondary,
|
||||||
label: '◀ Anterior',
|
label: "◀ Anterior",
|
||||||
custom_id: `mobs_prev_${page}`
|
custom_id: `mobs_prev_${page}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,8 +80,8 @@ export const command: CommandMessage = {
|
|||||||
buttons.push({
|
buttons.push({
|
||||||
type: ComponentType.Button,
|
type: ComponentType.Button,
|
||||||
style: ButtonStyle.Secondary,
|
style: ButtonStyle.Secondary,
|
||||||
label: 'Siguiente ▶',
|
label: "Siguiente ▶",
|
||||||
custom_id: `mobs_next_${page}`
|
custom_id: `mobs_next_${page}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,34 +90,38 @@ export const command: CommandMessage = {
|
|||||||
flags: 32768,
|
flags: 32768,
|
||||||
components: [
|
components: [
|
||||||
display,
|
display,
|
||||||
...(buttons.length > 0 ? [{
|
...(buttons.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
type: ComponentType.ActionRow,
|
type: ComponentType.ActionRow,
|
||||||
components: buttons
|
components: buttons,
|
||||||
}] : [])
|
},
|
||||||
]
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const collector = msg.createMessageComponentCollector({
|
const collector = msg.createMessageComponentCollector({
|
||||||
time: 5 * 60_000,
|
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.isButton()) return;
|
||||||
|
|
||||||
if (i.customId.startsWith('mobs_prev_')) {
|
if (i.customId.startsWith("mobs_prev_")) {
|
||||||
const currentPage = parseInt(i.customId.split('_')[2]);
|
const currentPage = parseInt(i.customId.split("_")[2]);
|
||||||
await i.deferUpdate();
|
await i.deferUpdate();
|
||||||
args[0] = String(currentPage - 1);
|
args[0] = String(currentPage - 1);
|
||||||
await command.run!(message, args, client);
|
await command.run!(message, args, client);
|
||||||
collector.stop();
|
collector.stop();
|
||||||
} else if (i.customId.startsWith('mobs_next_')) {
|
} else if (i.customId.startsWith("mobs_next_")) {
|
||||||
const currentPage = parseInt(i.customId.split('_')[2]);
|
const currentPage = parseInt(i.customId.split("_")[2]);
|
||||||
await i.deferUpdate();
|
await i.deferUpdate();
|
||||||
args[0] = String(currentPage + 1);
|
args[0] = String(currentPage + 1);
|
||||||
await command.run!(message, args, client);
|
await command.run!(message, args, client);
|
||||||
collector.stop();
|
collector.stop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -271,6 +271,8 @@ export interface KeyPickerResult<T> {
|
|||||||
entry: T | null;
|
entry: T | null;
|
||||||
panelMessage: Message | null;
|
panelMessage: Message | null;
|
||||||
reason: "selected" | "empty" | "cancelled" | "timeout";
|
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>(
|
export async function promptKeySelection<T>(
|
||||||
@@ -444,11 +446,12 @@ export async function promptKeySelection<T>(
|
|||||||
const result = await new Promise<KeyPickerResult<T>>((resolve) => {
|
const result = await new Promise<KeyPickerResult<T>>((resolve) => {
|
||||||
const finish = (
|
const finish = (
|
||||||
entry: T | null,
|
entry: T | null,
|
||||||
reason: "selected" | "cancelled" | "timeout"
|
reason: "selected" | "cancelled" | "timeout",
|
||||||
|
selectedValue?: string
|
||||||
) => {
|
) => {
|
||||||
if (resolved) return;
|
if (resolved) return;
|
||||||
resolved = true;
|
resolved = true;
|
||||||
resolve({ entry, panelMessage, reason });
|
resolve({ entry, panelMessage, reason, selectedValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
const collector = panelMessage.createMessageComponentCollector({
|
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");
|
collector.stop("selected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ import { sendDisplayReply, formatItemLabel } from "./_helpers";
|
|||||||
|
|
||||||
const PAGE_SIZE = 15;
|
const PAGE_SIZE = 15;
|
||||||
|
|
||||||
function parseItemProps(json: unknown): ItemProps {
|
import { parseItemProps } from "../../../game/core/utils";
|
||||||
if (!json || typeof json !== "object") return {};
|
|
||||||
return json as ItemProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmtTool(props: ItemProps) {
|
function fmtTool(props: ItemProps) {
|
||||||
const t = props.tool;
|
const t = props.tool;
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
import { Message, MessageFlags, MessageComponentInteraction, ButtonInteraction, TextBasedChannel } from 'discord.js';
|
import {
|
||||||
import { ComponentType, TextInputStyle, ButtonStyle } from 'discord-api-types/v10';
|
Message,
|
||||||
import type { CommandMessage } from '../../../core/types/commands';
|
MessageFlags,
|
||||||
import { hasManageGuildOrStaff } from '../../../core/lib/permissions';
|
MessageComponentInteraction,
|
||||||
import logger from '../../../core/lib/logger';
|
ButtonInteraction,
|
||||||
import type Amayo from '../../../core/client';
|
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 {
|
interface MobEditorState {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -14,150 +24,320 @@ interface MobEditorState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createMobDisplay(state: MobEditorState, editing: boolean = false) {
|
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 || {};
|
const stats = state.stats || {};
|
||||||
return {
|
return {
|
||||||
type: 17,
|
type: 17,
|
||||||
accent_color: 0xFF0000,
|
accent_color: 0xff0000,
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
type: 9,
|
type: 9,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: `👹 **${title}: \`${state.key}\`**`
|
content: `👹 **${title}: \`${state.key}\`**`,
|
||||||
}]
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{ type: 14, divider: true },
|
{ type: 14, divider: true },
|
||||||
{
|
{
|
||||||
type: 9,
|
type: 9,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: `**📋 Estado Actual:**\n` +
|
content:
|
||||||
`**Nombre:** ${state.name || '❌ No configurado'}\n` +
|
`**📋 Estado Actual:**\n` +
|
||||||
`**Categoría:** ${state.category || 'Sin categoría'}\n` +
|
`**Nombre:** ${state.name || "❌ No configurado"}\n` +
|
||||||
|
`**Categoría:** ${state.category || "Sin categoría"}\n` +
|
||||||
`**Attack:** ${stats.attack || 0}\n` +
|
`**Attack:** ${stats.attack || 0}\n` +
|
||||||
`**HP:** ${stats.hp || 0}\n` +
|
`**HP:** ${stats.hp || 0}\n` +
|
||||||
`**Defense:** ${stats.defense || 0}\n` +
|
`**Defense:** ${stats.defense || 0}\n` +
|
||||||
`**Drops:** ${Object.keys(state.drops || {}).length} items`
|
`**Drops:** ${Object.keys(state.drops || {}).length} items`,
|
||||||
}]
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{ type: 14, divider: true },
|
{ type: 14, divider: true },
|
||||||
{
|
{
|
||||||
type: 9,
|
type: 9,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: `**🎮 Instrucciones:**\n` +
|
content:
|
||||||
|
`**🎮 Instrucciones:**\n` +
|
||||||
`• **Base**: Nombre y categoría\n` +
|
`• **Base**: Nombre y categoría\n` +
|
||||||
`• **Stats (JSON)**: Estadísticas del mob\n` +
|
`• **Stats (JSON)**: Estadísticas del mob\n` +
|
||||||
`• **Drops (JSON)**: Items que dropea\n` +
|
`• **Drops (JSON)**: Items que dropea\n` +
|
||||||
`• **Guardar**: Confirma los cambios\n` +
|
`• **Guardar**: Confirma los cambios\n` +
|
||||||
`• **Cancelar**: Descarta los cambios`
|
`• **Cancelar**: Descarta los cambios`,
|
||||||
}]
|
},
|
||||||
}
|
],
|
||||||
]
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'mob-crear',
|
name: "mob-crear",
|
||||||
type: 'message',
|
type: "message",
|
||||||
aliases: ['crear-mob','mobcreate'],
|
aliases: ["crear-mob", "mobcreate"],
|
||||||
cooldown: 10,
|
cooldown: 10,
|
||||||
description: 'Crea un Mob (enemigo) para este servidor con editor interactivo.',
|
description:
|
||||||
category: 'Minijuegos',
|
"Crea un Mob (enemigo) para este servidor con editor interactivo.",
|
||||||
usage: 'mob-crear <key-única>',
|
category: "Minijuegos",
|
||||||
|
usage: "mob-crear <key-única>",
|
||||||
run: async (message: Message, args: string[], client: Amayo) => {
|
run: async (message: Message, args: string[], client: Amayo) => {
|
||||||
const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma);
|
const allowed = await hasManageGuildOrStaff(
|
||||||
if (!allowed) { await message.reply('❌ No tienes permisos de ManageGuild ni rol de staff.'); return; }
|
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();
|
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 guildId = message.guild!.id;
|
||||||
const exists = await client.prisma.mob.findFirst({ where: { key, guildId } });
|
const exists = await client.prisma.mob.findFirst({
|
||||||
if (exists) { await message.reply('❌ Ya existe un mob con esa key.'); return; }
|
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 state: MobEditorState = { key, stats: { attack: 5 }, drops: {} };
|
||||||
|
|
||||||
const channel = message.channel as TextBasedChannel & { send: Function };
|
const channel = message.channel as TextBasedChannel & { send: Function };
|
||||||
const editorMsg = await channel.send({
|
const editorMsg = await channel.send({
|
||||||
content: `👾 Editor de Mob: \`${key}\``,
|
content: `👾 Editor de Mob: \`${key}\``,
|
||||||
components: [ { type: 1, components: [
|
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: 1,
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Drops (JSON)', custom_id: 'mb_drops' },
|
components: [
|
||||||
{ 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 collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i)=> i.user.id === message.author.id });
|
const collector = editorMsg.createMessageComponentCollector({
|
||||||
collector.on('collect', async (i: MessageComponentInteraction) => {
|
time: 30 * 60_000,
|
||||||
|
filter: (i) => i.user.id === message.author.id,
|
||||||
|
});
|
||||||
|
collector.on("collect", async (i: MessageComponentInteraction) => {
|
||||||
try {
|
try {
|
||||||
if (!i.isButton()) return;
|
if (!i.isButton()) return;
|
||||||
if (i.customId === 'mb_cancel') {
|
if (i.customId === "mb_cancel") {
|
||||||
await i.deferUpdate();
|
await i.deferUpdate();
|
||||||
await editorMsg.edit({
|
await editorMsg.edit({
|
||||||
flags: 32768,
|
flags: 32768,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 17,
|
type: 17,
|
||||||
accent_color: 0xFF0000,
|
accent_color: 0xff0000,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 9,
|
type: 9,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: '**❌ Editor cancelado.**'
|
content: "**❌ Editor cancelado.**",
|
||||||
}]
|
},
|
||||||
}]
|
],
|
||||||
}]
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
collector.stop('cancel');
|
collector.stop("cancel");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (i.customId === 'mb_base') { await showBaseModal(i as ButtonInteraction, state, editorMsg, false); return; }
|
if (i.customId === "mb_base") {
|
||||||
if (i.customId === 'mb_stats') { await showJsonModal(i as ButtonInteraction, state, 'stats', 'Stats del Mob (JSON)', editorMsg, false); return; }
|
await showBaseModal(i as ButtonInteraction, state, editorMsg, false);
|
||||||
if (i.customId === 'mb_drops') { await showJsonModal(i as ButtonInteraction, state, 'drops', 'Drops del Mob (JSON)', editorMsg, false); return; }
|
return;
|
||||||
if (i.customId === 'mb_save') {
|
}
|
||||||
if (!state.name) { await i.reply({ content: '❌ Falta el nombre del mob.', flags: MessageFlags.Ephemeral }); return; }
|
if (i.customId === "mb_stats") {
|
||||||
await client.prisma.mob.create({ data: { guildId, key: state.key, name: state.name!, category: state.category ?? null, stats: state.stats ?? {}, drops: state.drops ?? {} } });
|
await showJsonModal(
|
||||||
await i.reply({ content: '✅ Mob guardado!', flags: MessageFlags.Ephemeral });
|
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({
|
await editorMsg.edit({
|
||||||
flags: 32768,
|
flags: 32768,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 17,
|
type: 17,
|
||||||
accent_color: 0x00FF00,
|
accent_color: 0x00ff00,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 9,
|
type: 9,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: `**✅ Mob \`${state.key}\` creado exitosamente.**`
|
content: `**✅ Mob \`${state.key}\` creado exitosamente.**`,
|
||||||
}]
|
},
|
||||||
}]
|
],
|
||||||
}]
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
collector.stop('saved');
|
collector.stop("saved");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({err}, 'mob-crear');
|
logger.error({ err }, "mob-crear");
|
||||||
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") {
|
||||||
|
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) {
|
async function showBaseModal(
|
||||||
const modal = { title: 'Base del Mob', customId: 'mb_base_modal', components: [
|
i: ButtonInteraction,
|
||||||
{ type: ComponentType.Label, label: 'Nombre', component: { type: ComponentType.TextInput, customId: 'name', style: TextInputStyle.Short, required: true, value: state.name ?? '' } },
|
state: MobEditorState,
|
||||||
{ type: ComponentType.Label, label: 'Categoría (opcional)', component: { type: ComponentType.TextInput, customId: 'category', style: TextInputStyle.Short, required: false, value: state.category ?? '' } },
|
editorMsg: Message,
|
||||||
] } as const;
|
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);
|
await i.showModal(modal);
|
||||||
try {
|
try {
|
||||||
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
||||||
state.name = sub.components.getTextInputValue('name').trim();
|
state.name = sub.components.getTextInputValue("name").trim();
|
||||||
const cat = sub.components.getTextInputValue('category')?.trim();
|
const cat = sub.components.getTextInputValue("category")?.trim();
|
||||||
state.category = cat || undefined;
|
state.category = cat || undefined;
|
||||||
await sub.reply({ content: '✅ Base actualizada.', flags: MessageFlags.Ephemeral });
|
await sub.reply({
|
||||||
|
content: "✅ Base actualizada.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
|
||||||
// Refresh display
|
// Refresh display
|
||||||
const newDisplay = createMobDisplay(state, editing);
|
const newDisplay = createMobDisplay(state, editing);
|
||||||
@@ -168,38 +348,90 @@ async function showBaseModal(i: ButtonInteraction, state: MobEditorState, editor
|
|||||||
{
|
{
|
||||||
type: 1,
|
type: 1,
|
||||||
components: [
|
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,
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Drops (JSON)', custom_id: 'mb_drops' },
|
style: ButtonStyle.Primary,
|
||||||
{ type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'mb_save' },
|
label: "Base",
|
||||||
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'mb_cancel' },
|
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 {}
|
} 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 current = JSON.stringify(state[field] ?? {});
|
||||||
const modal = { title, customId: `mb_json_${field}`, components: [
|
const modal = {
|
||||||
{ type: ComponentType.Label, label: 'JSON', component: { type: ComponentType.TextInput, customId: 'json', style: TextInputStyle.Paragraph, required: false, value: current.slice(0,4000) } },
|
title,
|
||||||
] } as const;
|
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);
|
await i.showModal(modal);
|
||||||
try {
|
try {
|
||||||
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
||||||
const raw = sub.components.getTextInputValue('json');
|
const raw = sub.components.getTextInputValue("json");
|
||||||
if (raw) {
|
if (raw) {
|
||||||
try {
|
try {
|
||||||
state[field] = JSON.parse(raw);
|
state[field] = JSON.parse(raw);
|
||||||
await sub.reply({ content: '✅ Guardado.', flags: MessageFlags.Ephemeral });
|
await sub.reply({
|
||||||
|
content: "✅ Guardado.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
await sub.reply({ content: '❌ JSON inválido.', flags: MessageFlags.Ephemeral });
|
await sub.reply({
|
||||||
|
content: "❌ JSON inválido.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state[field] = {};
|
state[field] = {};
|
||||||
await sub.reply({ content: 'ℹ️ Limpio.', flags: MessageFlags.Ephemeral });
|
await sub.reply({ content: "ℹ️ Limpio.", flags: MessageFlags.Ephemeral });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh display
|
// Refresh display
|
||||||
@@ -211,14 +443,39 @@ async function showJsonModal(i: ButtonInteraction, state: MobEditorState, field:
|
|||||||
{
|
{
|
||||||
type: 1,
|
type: 1,
|
||||||
components: [
|
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,
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Drops (JSON)', custom_id: 'mb_drops' },
|
style: ButtonStyle.Primary,
|
||||||
{ type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'mb_save' },
|
label: "Base",
|
||||||
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'mb_cancel' },
|
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 {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import { Message, MessageFlags, MessageComponentInteraction, ButtonInteraction, TextBasedChannel } from 'discord.js';
|
import {
|
||||||
import { ComponentType, TextInputStyle, ButtonStyle } from 'discord-api-types/v10';
|
Message,
|
||||||
import type { CommandMessage } from '../../../core/types/commands';
|
MessageFlags,
|
||||||
import { hasManageGuildOrStaff } from '../../../core/lib/permissions';
|
MessageComponentInteraction,
|
||||||
import logger from '../../../core/lib/logger';
|
ButtonInteraction,
|
||||||
import type Amayo from '../../../core/client';
|
TextBasedChannel,
|
||||||
import { promptKeySelection } from './_helpers';
|
} 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 {
|
interface MobEditorState {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -14,84 +24,103 @@ interface MobEditorState {
|
|||||||
drops?: any;
|
drops?: any;
|
||||||
}
|
}
|
||||||
function createMobDisplay(state: MobEditorState, editing: boolean = false) {
|
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 || {};
|
const stats = state.stats || {};
|
||||||
return {
|
return {
|
||||||
type: 17,
|
type: 17,
|
||||||
accent_color: 0xFF0000,
|
accent_color: 0xff0000,
|
||||||
components: [
|
components: [
|
||||||
{ type: 10, content: `# 👹 ${title}: \`${state.key}\`` },
|
{ type: 10, content: `# 👹 ${title}: \`${state.key}\`` },
|
||||||
{ type: 14, divider: true },
|
{ type: 14, divider: true },
|
||||||
{
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: [
|
content: [
|
||||||
'**📋 Estado Actual:**',
|
"**📋 Estado Actual:**",
|
||||||
`**Nombre:** ${state.name || '❌ No configurado'}`,
|
`**Nombre:** ${state.name || "❌ No configurado"}`,
|
||||||
`**Categoría:** ${state.category || 'Sin categoría'}`,
|
`**Categoría:** ${state.category || "Sin categoría"}`,
|
||||||
`**Attack:** ${stats.attack || 0}`,
|
`**Attack:** ${stats.attack || 0}`,
|
||||||
`**HP:** ${stats.hp || 0}`,
|
`**HP:** ${stats.hp || 0}`,
|
||||||
`**Defense:** ${stats.defense || 0}`,
|
`**Defense:** ${stats.defense || 0}`,
|
||||||
`**Drops:** ${Object.keys(state.drops || {}).length} items`,
|
`**Drops:** ${Object.keys(state.drops || {}).length} items`,
|
||||||
].join('\n'),
|
].join("\n"),
|
||||||
},
|
},
|
||||||
{ type: 14, divider: true },
|
{ type: 14, divider: true },
|
||||||
{
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: [
|
content: [
|
||||||
'**🎮 Instrucciones:**',
|
"**🎮 Instrucciones:**",
|
||||||
'• **Base**: Nombre y categoría',
|
"• **Base**: Nombre y categoría",
|
||||||
'• **Stats (JSON)**: Estadísticas del mob',
|
"• **Stats (JSON)**: Estadísticas del mob",
|
||||||
'• **Drops (JSON)**: Items que dropea',
|
"• **Drops (JSON)**: Items que dropea",
|
||||||
'• **Guardar**: Confirma los cambios',
|
"• **Guardar**: Confirma los cambios",
|
||||||
'• **Cancelar**: Descarta los cambios',
|
"• **Cancelar**: Descarta los cambios",
|
||||||
].join('\n'),
|
].join("\n"),
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'mob-editar',
|
name: "mob-editar",
|
||||||
type: 'message',
|
type: "message",
|
||||||
aliases: ['editar-mob','mobedit'],
|
aliases: ["editar-mob", "mobedit"],
|
||||||
cooldown: 10,
|
cooldown: 10,
|
||||||
description: 'Edita un Mob (enemigo) de este servidor con editor interactivo.',
|
description:
|
||||||
category: 'Minijuegos',
|
"Edita un Mob (enemigo) de este servidor con editor interactivo.",
|
||||||
usage: 'mob-editar',
|
category: "Minijuegos",
|
||||||
|
usage: "mob-editar",
|
||||||
run: async (message: Message, _args: string[], client: Amayo) => {
|
run: async (message: Message, _args: string[], client: Amayo) => {
|
||||||
const channel = message.channel as TextBasedChannel & { send: Function };
|
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) {
|
if (!allowed) {
|
||||||
await (channel.send as any)({
|
await (channel.send as any)({
|
||||||
content: null,
|
content: null,
|
||||||
flags: 32768,
|
flags: 32768,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 17,
|
type: 17,
|
||||||
accent_color: 0xFF0000,
|
accent_color: 0xff0000,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: '❌ **Error de Permisos**\n└ No tienes permisos de ManageGuild ni rol de staff.'
|
content:
|
||||||
}]
|
"❌ **Error de Permisos**\n└ No tienes permisos de ManageGuild ni rol de staff.",
|
||||||
}],
|
},
|
||||||
reply: { messageReference: message.id }
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
reply: { messageReference: message.id },
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const guildId = message.guild!.id;
|
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, {
|
const selection = await promptKeySelection(message, {
|
||||||
entries: mobs,
|
entries: localEntries,
|
||||||
customIdPrefix: 'mob_edit',
|
customIdPrefix: "mob_edit",
|
||||||
title: 'Selecciona un mob para editar',
|
title: "Selecciona un mob para editar",
|
||||||
emptyText: '⚠️ **No hay mobs configurados.** Usa `!mob-crear` primero.',
|
emptyText: "⚠️ **No hay mobs configurados.** Usa `!mob-crear` primero.",
|
||||||
placeholder: 'Elige un mob…',
|
placeholder: "Elige un mob…",
|
||||||
filterHint: 'Filtra por nombre, key o categoría.',
|
filterHint: "Filtra por nombre, key o categoría.",
|
||||||
getOption: (mob) => ({
|
getOption: (entry: any) => ({
|
||||||
value: mob.id,
|
value: entry.id ?? entry.def.key,
|
||||||
label: mob.name ?? mob.key,
|
label: entry.def.name ?? entry.def.key,
|
||||||
description: [mob.category ?? 'Sin categoría', mob.key].filter(Boolean).join(' • '),
|
description: [entry.def?.category ?? "Sin categoría", entry.def.key]
|
||||||
keywords: [mob.key, mob.name ?? '', mob.category ?? ''],
|
.filter(Boolean)
|
||||||
|
.join(" • "),
|
||||||
|
keywords: [
|
||||||
|
entry.def.key,
|
||||||
|
entry.def.name ?? "",
|
||||||
|
entry.def?.category ?? "",
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,14 +128,23 @@ export const command: CommandMessage = {
|
|||||||
return;
|
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 = {
|
const state: MobEditorState = {
|
||||||
key: mob.key,
|
key: entry.def.key,
|
||||||
name: mob.name,
|
name: (dbRow && dbRow.name) ?? entry.def.name,
|
||||||
category: mob.category ?? undefined,
|
category: (dbRow && dbRow.category) ?? entry.def?.category ?? undefined,
|
||||||
stats: mob.stats ?? {},
|
stats: (dbRow && dbRow.stats) ?? entry.def?.base ?? {},
|
||||||
drops: mob.drops ?? {},
|
drops: (dbRow && dbRow.drops) ?? entry.def?.drops ?? {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildEditorComponents = () => [
|
const buildEditorComponents = () => [
|
||||||
@@ -114,13 +152,38 @@ export const command: CommandMessage = {
|
|||||||
{
|
{
|
||||||
type: 1,
|
type: 1,
|
||||||
components: [
|
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,
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Drops (JSON)', custom_id: 'mb_drops' },
|
style: ButtonStyle.Primary,
|
||||||
{ type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'mb_save' },
|
label: "Base",
|
||||||
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'mb_cancel' },
|
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;
|
const editorMsg = selection.panelMessage;
|
||||||
@@ -130,77 +193,142 @@ export const command: CommandMessage = {
|
|||||||
components: buildEditorComponents(),
|
components: buildEditorComponents(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const collector = editorMsg.createMessageComponentCollector({ time: 30 * 60_000, filter: (i) => i.user.id === message.author.id });
|
const collector = editorMsg.createMessageComponentCollector({
|
||||||
collector.on('collect', async (i: MessageComponentInteraction) => {
|
time: 30 * 60_000,
|
||||||
|
filter: (i) => i.user.id === message.author.id,
|
||||||
|
});
|
||||||
|
collector.on("collect", async (i: MessageComponentInteraction) => {
|
||||||
try {
|
try {
|
||||||
if (!i.isButton()) return;
|
if (!i.isButton()) return;
|
||||||
switch (i.customId) {
|
switch (i.customId) {
|
||||||
case 'mb_cancel':
|
case "mb_cancel":
|
||||||
await i.deferUpdate();
|
await i.deferUpdate();
|
||||||
await editorMsg.edit({
|
await editorMsg.edit({
|
||||||
content: null,
|
content: null,
|
||||||
flags: 32768,
|
flags: 32768,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 17,
|
type: 17,
|
||||||
accent_color: 0xFF0000,
|
accent_color: 0xff0000,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: '**❌ Editor cancelado.**'
|
content: "**❌ Editor cancelado.**",
|
||||||
}]
|
},
|
||||||
}]
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
collector.stop('cancel');
|
collector.stop("cancel");
|
||||||
return;
|
return;
|
||||||
case 'mb_base':
|
case "mb_base":
|
||||||
await showBaseModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents);
|
await showBaseModal(
|
||||||
|
i as ButtonInteraction,
|
||||||
|
state,
|
||||||
|
editorMsg,
|
||||||
|
buildEditorComponents
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
case 'mb_stats':
|
case "mb_stats":
|
||||||
await showJsonModal(i as ButtonInteraction, state, 'stats', 'Stats del Mob (JSON)', editorMsg, buildEditorComponents);
|
await showJsonModal(
|
||||||
|
i as ButtonInteraction,
|
||||||
|
state,
|
||||||
|
"stats",
|
||||||
|
"Stats del Mob (JSON)",
|
||||||
|
editorMsg,
|
||||||
|
buildEditorComponents
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
case 'mb_drops':
|
case "mb_drops":
|
||||||
await showJsonModal(i as ButtonInteraction, state, 'drops', 'Drops del Mob (JSON)', editorMsg, buildEditorComponents);
|
await showJsonModal(
|
||||||
|
i as ButtonInteraction,
|
||||||
|
state,
|
||||||
|
"drops",
|
||||||
|
"Drops del Mob (JSON)",
|
||||||
|
editorMsg,
|
||||||
|
buildEditorComponents
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
case 'mb_save':
|
case "mb_save":
|
||||||
if (!state.name) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
await client.prisma.mob.update({ where: { id: mob.id }, data: { name: state.name!, category: state.category ?? null, stats: state.stats ?? {}, drops: state.drops ?? {} } });
|
try {
|
||||||
await i.reply({ content: '✅ Mob actualizado!', flags: MessageFlags.Ephemeral });
|
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({
|
await editorMsg.edit({
|
||||||
content: null,
|
content: null,
|
||||||
flags: 32768,
|
flags: 32768,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 17,
|
type: 17,
|
||||||
accent_color: 0x00FF00,
|
accent_color: 0x00ff00,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: `**✅ Mob \`${state.key}\` actualizado exitosamente.**`
|
content: `**✅ Mob \`${state.key}\` actualizado exitosamente.**`,
|
||||||
}]
|
},
|
||||||
}]
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
collector.stop('saved');
|
collector.stop("saved");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ err }, 'mob-editar');
|
logger.error({ err }, "mob-editar");
|
||||||
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, reason) => {
|
collector.on("end", async (_c, reason) => {
|
||||||
if (reason === 'time') {
|
if (reason === "time") {
|
||||||
try {
|
try {
|
||||||
await editorMsg.edit({
|
await editorMsg.edit({
|
||||||
content: null,
|
content: null,
|
||||||
flags: 32768,
|
flags: 32768,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 17,
|
type: 17,
|
||||||
accent_color: 0xFFA500,
|
accent_color: 0xffa500,
|
||||||
components: [{
|
components: [
|
||||||
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: '**⏰ Editor expirado.**'
|
content: "**⏰ Editor expirado.**",
|
||||||
}]
|
},
|
||||||
}]
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -208,41 +336,94 @@ export const command: CommandMessage = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
async function showBaseModal(i: ButtonInteraction, state: MobEditorState, editorMsg: Message, buildComponents: () => any[]) {
|
async function showBaseModal(
|
||||||
const modal = { title: 'Base del Mob', customId: 'mb_base_modal', components: [
|
i: ButtonInteraction,
|
||||||
{ type: ComponentType.Label, label: 'Nombre', component: { type: ComponentType.TextInput, customId: 'name', style: TextInputStyle.Short, required: true, value: state.name ?? '' } },
|
state: MobEditorState,
|
||||||
{ type: ComponentType.Label, label: 'Categoría (opcional)', component: { type: ComponentType.TextInput, customId: 'category', style: TextInputStyle.Short, required: false, value: state.category ?? '' } },
|
editorMsg: Message,
|
||||||
] } as const;
|
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);
|
await i.showModal(modal);
|
||||||
try {
|
try {
|
||||||
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
||||||
state.name = sub.components.getTextInputValue('name').trim();
|
state.name = sub.components.getTextInputValue("name").trim();
|
||||||
const cat = sub.components.getTextInputValue('category')?.trim();
|
const cat = sub.components.getTextInputValue("category")?.trim();
|
||||||
state.category = cat || undefined;
|
state.category = cat || undefined;
|
||||||
await sub.deferUpdate();
|
await sub.deferUpdate();
|
||||||
await editorMsg.edit({
|
await editorMsg.edit({
|
||||||
content: null,
|
content: null,
|
||||||
flags: 32768,
|
flags: 32768,
|
||||||
components: buildComponents()
|
components: buildComponents(),
|
||||||
});
|
});
|
||||||
} catch {}
|
} 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 current = JSON.stringify(state[field] ?? {});
|
||||||
const modal = { title, customId: `mb_json_${field}`, components: [
|
const modal = {
|
||||||
{ type: ComponentType.Label, label: 'JSON', component: { type: ComponentType.TextInput, customId: 'json', style: TextInputStyle.Paragraph, required: false, value: current.slice(0,4000) } },
|
title,
|
||||||
] } as const;
|
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);
|
await i.showModal(modal);
|
||||||
try {
|
try {
|
||||||
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
||||||
const raw = sub.components.getTextInputValue('json');
|
const raw = sub.components.getTextInputValue("json");
|
||||||
if (raw) {
|
if (raw) {
|
||||||
try {
|
try {
|
||||||
state[field] = JSON.parse(raw);
|
state[field] = JSON.parse(raw);
|
||||||
await sub.deferUpdate();
|
await sub.deferUpdate();
|
||||||
} catch {
|
} catch {
|
||||||
await sub.reply({ content: '❌ JSON inválido.', flags: MessageFlags.Ephemeral });
|
await sub.reply({
|
||||||
|
content: "❌ JSON inválido.",
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -252,7 +433,7 @@ async function showJsonModal(i: ButtonInteraction, state: MobEditorState, field:
|
|||||||
await editorMsg.edit({
|
await editorMsg.edit({
|
||||||
content: null,
|
content: null,
|
||||||
flags: 32768,
|
flags: 32768,
|
||||||
components: buildComponents()
|
components: buildComponents(),
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,7 @@ function buildEmoji(
|
|||||||
return { id, name, animated };
|
return { id, name, animated };
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItemProps(json: unknown): ItemProps {
|
import { parseItemProps } from "../../../game/core/utils";
|
||||||
if (!json || typeof json !== "object") return {};
|
|
||||||
return json as ItemProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatPrice(price: any): string {
|
function formatPrice(price: any): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ import {
|
|||||||
} from "./statusEffectsService";
|
} from "./statusEffectsService";
|
||||||
import type { ItemProps } from "../economy/types";
|
import type { ItemProps } from "../economy/types";
|
||||||
import { ensureUserAndGuildExist } from "../core/userService";
|
import { ensureUserAndGuildExist } from "../core/userService";
|
||||||
|
import { parseItemProps } from "../core/utils";
|
||||||
function parseItemProps(json: unknown): ItemProps {
|
|
||||||
if (!json || typeof json !== "object") return {};
|
|
||||||
return json as ItemProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function ensurePlayerState(userId: string, guildId: string) {
|
export async function ensurePlayerState(userId: string, guildId: string) {
|
||||||
// Asegurar que User y Guild existan antes de crear/buscar state
|
// Asegurar que User y Guild existan antes de crear/buscar state
|
||||||
|
|||||||
@@ -1,34 +1,32 @@
|
|||||||
import { prisma } from '../../core/database/prisma';
|
import { prisma } from "../../core/database/prisma";
|
||||||
import { assertNotOnCooldown, setCooldown } from '../cooldowns/service';
|
import { assertNotOnCooldown, setCooldown } from "../cooldowns/service";
|
||||||
import { findItemByKey, consumeItemByKey } from '../economy/service';
|
import { findItemByKey, consumeItemByKey } from "../economy/service";
|
||||||
import type { ItemProps } from '../economy/types';
|
import type { ItemProps } from "../economy/types";
|
||||||
import { getEffectiveStats, adjustHP } from '../combat/equipmentService';
|
import { getEffectiveStats, adjustHP } from "../combat/equipmentService";
|
||||||
|
import { parseItemProps } from "../core/utils";
|
||||||
|
import { getCooldownKeyForFood, calculateHealingFromFood } from "./utils";
|
||||||
|
|
||||||
function parseItemProps(json: unknown): ItemProps {
|
export async function useConsumableByKey(
|
||||||
if (!json || typeof json !== 'object') return {};
|
userId: string,
|
||||||
return json as ItemProps;
|
guildId: string,
|
||||||
}
|
itemKey: string
|
||||||
|
) {
|
||||||
export async function useConsumableByKey(userId: string, guildId: string, itemKey: string) {
|
|
||||||
const item = await findItemByKey(guildId, itemKey);
|
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 props = parseItemProps(item.props);
|
||||||
const food = props.food;
|
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);
|
await assertNotOnCooldown(userId, guildId, cdKey);
|
||||||
|
|
||||||
// Calcular sanación
|
// Calcular sanación
|
||||||
const stats = await getEffectiveStats(userId, guildId);
|
const stats = await getEffectiveStats(userId, guildId);
|
||||||
const flat = Math.max(0, food.healHp ?? 0);
|
const heal = calculateHealingFromFood(food, stats.maxHp);
|
||||||
const perc = Math.max(0, food.healPercent ?? 0);
|
|
||||||
const byPerc = Math.floor((perc / 100) * stats.maxHp);
|
|
||||||
const heal = Math.max(1, flat + byPerc);
|
|
||||||
|
|
||||||
// Consumir el ítem
|
// Consumir el ítem
|
||||||
const { consumed } = await consumeItemByKey(userId, guildId, item.key, 1);
|
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
|
// Aplicar curación
|
||||||
await adjustHP(userId, guildId, heal);
|
await adjustHP(userId, guildId, heal);
|
||||||
@@ -40,4 +38,3 @@ export async function useConsumableByKey(userId: string, guildId: string, itemKe
|
|||||||
|
|
||||||
return { healed: heal } as const;
|
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";
|
} from "./types";
|
||||||
import type { Prisma } from "@prisma/client";
|
import type { Prisma } from "@prisma/client";
|
||||||
import { ensureUserAndGuildExist } from "../core/userService";
|
import { ensureUserAndGuildExist } from "../core/userService";
|
||||||
|
import coreUtils from "../core/utils";
|
||||||
|
|
||||||
// Utilidades de tiempo
|
// Reusar utilidades centrales desde game core
|
||||||
function now(): Date {
|
const {
|
||||||
return new Date();
|
now,
|
||||||
}
|
isWithin,
|
||||||
|
ensureArray,
|
||||||
function isWithin(date: Date, from?: Date | null, to?: Date | null): boolean {
|
parseItemProps,
|
||||||
if (from && date < from) return false;
|
parseState,
|
||||||
if (to && date > to) return false;
|
updateInventoryEntryState,
|
||||||
return true;
|
} = coreUtils as any;
|
||||||
}
|
|
||||||
|
|
||||||
// Resuelve un EconomyItem por key con alcance de guild o global
|
// Resuelve un EconomyItem por key con alcance de guild o global
|
||||||
export async function findItemByKey(guildId: string, key: string) {
|
export async function findItemByKey(guildId: string, key: string) {
|
||||||
@@ -89,15 +89,8 @@ export async function getInventoryEntry(
|
|||||||
return { item, entry } as const;
|
return { item, entry } as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItemProps(json: unknown): ItemProps {
|
// utilidades parseItemProps, parseState, ensureArray y updateInventoryEntryState
|
||||||
if (!json || typeof json !== "object") return {};
|
// provistas por coreUtils importado arriba
|
||||||
return json as ItemProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseState(json: unknown): InventoryState {
|
|
||||||
if (!json || typeof json !== "object") return {};
|
|
||||||
return json as InventoryState;
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkUsableWindow(item: {
|
function checkUsableWindow(item: {
|
||||||
usableFrom: Date | null;
|
usableFrom: Date | null;
|
||||||
@@ -156,7 +149,7 @@ export async function addItemByKey(
|
|||||||
} else {
|
} else {
|
||||||
// No apilable: usar state.instances
|
// No apilable: usar state.instances
|
||||||
const state = parseState(entry.state);
|
const state = parseState(entry.state);
|
||||||
state.instances ??= [];
|
state.instances = ensureArray(state.instances);
|
||||||
const canAdd = Math.max(
|
const canAdd = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(qty, Math.max(0, max - state.instances.length))
|
Math.min(qty, Math.max(0, max - state.instances.length))
|
||||||
@@ -173,13 +166,12 @@ export async function addItemByKey(
|
|||||||
state.instances.push({});
|
state.instances.push({});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const updated = await prisma.inventoryEntry.update({
|
const updated = await updateInventoryEntryState(
|
||||||
where: { userId_guildId_itemId: { userId, guildId, itemId: item.id } },
|
userId,
|
||||||
data: {
|
guildId,
|
||||||
state: state as unknown as Prisma.InputJsonValue,
|
item.id,
|
||||||
quantity: state.instances.length,
|
state
|
||||||
},
|
);
|
||||||
});
|
|
||||||
return { added: canAdd, entry: updated } as const;
|
return { added: canAdd, entry: updated } as const;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,18 +195,16 @@ export async function consumeItemByKey(
|
|||||||
return { consumed, entry: updated } as const;
|
return { consumed, entry: updated } as const;
|
||||||
} else {
|
} else {
|
||||||
const state = parseState(entry.state);
|
const state = parseState(entry.state);
|
||||||
const instances = state.instances ?? [];
|
state.instances = ensureArray(state.instances);
|
||||||
const consumed = Math.min(qty, instances.length);
|
const consumed = Math.min(qty, state.instances.length);
|
||||||
if (consumed === 0) return { consumed: 0 } as const;
|
if (consumed === 0) return { consumed: 0 } as const;
|
||||||
instances.splice(0, consumed);
|
state.instances.splice(0, consumed);
|
||||||
const newState: InventoryState = { ...state, instances };
|
const updated = await updateInventoryEntryState(
|
||||||
const updated = await prisma.inventoryEntry.update({
|
userId,
|
||||||
where: { userId_guildId_itemId: { userId, guildId, itemId: item.id } },
|
guildId,
|
||||||
data: {
|
item.id,
|
||||||
state: newState as unknown as Prisma.InputJsonValue,
|
state
|
||||||
quantity: instances.length,
|
);
|
||||||
},
|
|
||||||
});
|
|
||||||
return { consumed, entry: updated } as const;
|
return { consumed, entry: updated } as const;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,7 +222,7 @@ export async function openChestByKey(
|
|||||||
const props = parseItemProps(item.props);
|
const props = parseItemProps(item.props);
|
||||||
const chest = props.chest ?? {};
|
const chest = props.chest ?? {};
|
||||||
if (!chest.enabled) throw new Error("Este ítem no se puede abrir");
|
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 mode = chest.randomMode || "all";
|
||||||
const result: OpenChestResult = {
|
const result: OpenChestResult = {
|
||||||
coinsDelta: 0,
|
coinsDelta: 0,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import { logToolBreak } from "../lib/toolBreakLog";
|
import { logToolBreak } from "../lib/toolBreakLog";
|
||||||
import { updateStats } from "../stats/service"; // 🟩 local authoritative
|
import { updateStats } from "../stats/service"; // 🟩 local authoritative
|
||||||
import type { ItemProps, InventoryState } from "../economy/types";
|
import type { ItemProps, InventoryState } from "../economy/types";
|
||||||
|
import { parseItemProps, parseState as parseInvState } from "../core/utils";
|
||||||
import type {
|
import type {
|
||||||
LevelRequirements,
|
LevelRequirements,
|
||||||
RunMinigameOptions,
|
RunMinigameOptions,
|
||||||
@@ -120,15 +121,7 @@ async function ensureAreaAndLevel(
|
|||||||
return { area, lvl } as const;
|
return { area, lvl } as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItemProps(json: unknown): ItemProps {
|
// parseItemProps y parseInvState son importados desde ../core/utils para centralizar parsing
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validateRequirements(
|
async function validateRequirements(
|
||||||
userId: string,
|
userId: string,
|
||||||
@@ -280,6 +273,26 @@ async function sampleMobs(mobs?: MobsTable): Promise<string[]> {
|
|||||||
return out;
|
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(
|
async function reduceToolDurability(
|
||||||
userId: string,
|
userId: string,
|
||||||
guildId: string,
|
guildId: string,
|
||||||
@@ -445,7 +458,7 @@ export async function runMinigame(
|
|||||||
guildId,
|
guildId,
|
||||||
rewards
|
rewards
|
||||||
);
|
);
|
||||||
const mobsSpawned = await sampleMobs(mobs);
|
const mobsSpawned = await sampleMobInstances(mobs, level);
|
||||||
|
|
||||||
// Reducir durabilidad de herramienta si se usó
|
// Reducir durabilidad de herramienta si se usó
|
||||||
let toolInfo: RunResult["tool"] | undefined;
|
let toolInfo: RunResult["tool"] | undefined;
|
||||||
@@ -478,8 +491,8 @@ export async function runMinigame(
|
|||||||
|
|
||||||
if (!hasWeapon) {
|
if (!hasWeapon) {
|
||||||
// Registrar derrota simple contra la lista de mobs (no se derrotan mobs).
|
// Registrar derrota simple contra la lista de mobs (no se derrotan mobs).
|
||||||
const mobLogs: CombatSummary["mobs"] = mobsSpawned.map((mk) => ({
|
const mobLogs: CombatSummary["mobs"] = mobsSpawned.map((m) => ({
|
||||||
mobKey: mk,
|
mobKey: m?.key ?? "unknown",
|
||||||
maxHp: 0,
|
maxHp: 0,
|
||||||
defeated: false,
|
defeated: false,
|
||||||
totalDamageDealt: 0,
|
totalDamageDealt: 0,
|
||||||
@@ -596,16 +609,20 @@ export async function runMinigame(
|
|||||||
const factor = 0.8 + Math.random() * 0.4; // 0.8 - 1.2
|
const factor = 0.8 + Math.random() * 0.4; // 0.8 - 1.2
|
||||||
return base * factor;
|
return base * factor;
|
||||||
};
|
};
|
||||||
for (const mobKey of mobsSpawned) {
|
for (const mob of mobsSpawned) {
|
||||||
if (currentHp <= 0) break; // jugador derrotado antes de iniciar este mob
|
if (currentHp <= 0) break; // jugador derrotado antes de iniciar este mob
|
||||||
// Stats simples del mob (placeholder mejorable con tabla real)
|
// Stats simples del mob (usamos la instancia escalada)
|
||||||
const mobBaseHp = 10 + Math.floor(Math.random() * 6); // 10-15
|
const mobBaseHp = Math.max(1, Math.floor(mob?.scaled?.hp ?? 10));
|
||||||
let mobHp = mobBaseHp;
|
let mobHp = mobBaseHp;
|
||||||
const rounds: any[] = [];
|
const rounds: any[] = [];
|
||||||
let round = 1;
|
let round = 1;
|
||||||
let mobDamageDealt = 0; // daño que jugador hace a este mob
|
let mobDamageDealt = 0; // daño que jugador hace a este mob
|
||||||
let mobDamageTakenFromMob = 0; // daño que jugador recibe de 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
|
// Daño jugador -> mob
|
||||||
const playerRaw = variance(eff.damage || 1) + 1; // asegurar >=1
|
const playerRaw = variance(eff.damage || 1) + 1; // asegurar >=1
|
||||||
const playerDamage = Math.max(1, Math.round(playerRaw));
|
const playerDamage = Math.max(1, Math.round(playerRaw));
|
||||||
@@ -627,7 +644,7 @@ export async function runMinigame(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
rounds.push({
|
rounds.push({
|
||||||
mobKey,
|
mobKey: mob?.key ?? "unknown",
|
||||||
round,
|
round,
|
||||||
playerDamageDealt: playerDamage,
|
playerDamageDealt: playerDamage,
|
||||||
playerDamageTaken: playerTaken,
|
playerDamageTaken: playerTaken,
|
||||||
@@ -642,7 +659,7 @@ export async function runMinigame(
|
|||||||
round++;
|
round++;
|
||||||
}
|
}
|
||||||
mobLogs.push({
|
mobLogs.push({
|
||||||
mobKey,
|
mobKey: mob?.key ?? "unknown",
|
||||||
maxHp: mobBaseHp,
|
maxHp: mobBaseHp,
|
||||||
defeated: mobHp <= 0,
|
defeated: mobHp <= 0,
|
||||||
totalDamageDealt: mobDamageDealt,
|
totalDamageDealt: mobDamageDealt,
|
||||||
@@ -830,7 +847,7 @@ export async function runMinigame(
|
|||||||
|
|
||||||
const resultJson: Prisma.InputJsonValue = {
|
const resultJson: Prisma.InputJsonValue = {
|
||||||
rewards: delivered,
|
rewards: delivered,
|
||||||
mobs: mobsSpawned,
|
mobs: mobsSpawned.map((m) => m?.key ?? "unknown"),
|
||||||
tool: toolInfo,
|
tool: toolInfo,
|
||||||
weaponTool: weaponToolInfo,
|
weaponTool: weaponToolInfo,
|
||||||
combat: combatSummary,
|
combat: combatSummary,
|
||||||
@@ -879,7 +896,7 @@ export async function runMinigame(
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
rewards: delivered,
|
rewards: delivered,
|
||||||
mobs: mobsSpawned,
|
mobs: mobsSpawned.map((m) => m?.key ?? "unknown"),
|
||||||
tool: toolInfo,
|
tool: toolInfo,
|
||||||
weaponTool: weaponToolInfo,
|
weaponTool: weaponToolInfo,
|
||||||
combat: combatSummary,
|
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) {
|
export function computeMobStats(def: BaseMobDefinition, areaLevel: number) {
|
||||||
const lvl = Math.max(1, areaLevel);
|
const lvl = Math.max(1, areaLevel);
|
||||||
const s = def.scaling || {};
|
const s = def.scaling || {};
|
||||||
@@ -70,3 +66,206 @@ export function computeMobStats(def: BaseMobDefinition, areaLevel: number) {
|
|||||||
).toFixed(2);
|
).toFixed(2);
|
||||||
return { hp, attack: atk, defense: defVal };
|
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 { prisma } from "../../core/database/prisma";
|
||||||
import type { ItemProps } from '../economy/types';
|
import type { ItemProps } from "../economy/types";
|
||||||
import { findItemByKey, getInventoryEntry } from '../economy/service';
|
import { findItemByKey, getInventoryEntry } from "../economy/service";
|
||||||
|
import { parseItemProps } from "../core/utils";
|
||||||
function parseItemProps(json: unknown): ItemProps {
|
|
||||||
if (!json || typeof json !== 'object') return {};
|
|
||||||
return json as ItemProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function findMutationByKey(guildId: string, key: string) {
|
export async function findMutationByKey(guildId: string, key: string) {
|
||||||
return prisma.itemMutation.findFirst({
|
return prisma.itemMutation.findFirst({
|
||||||
where: { key, OR: [{ guildId }, { guildId: null }] },
|
where: { key, OR: [{ guildId }, { guildId: null }] },
|
||||||
orderBy: [{ guildId: 'desc' }],
|
orderBy: [{ guildId: "desc" }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function applyMutationToInventory(userId: string, guildId: string, itemKey: string, mutationKey: string) {
|
export async function applyMutationToInventory(
|
||||||
const { item, entry } = await getInventoryEntry(userId, guildId, itemKey, { createIfMissing: true });
|
userId: string,
|
||||||
if (!entry) throw new Error('Inventario inexistente');
|
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 props = parseItemProps(item.props);
|
||||||
const policy = props.mutationPolicy;
|
const policy = props.mutationPolicy;
|
||||||
if (policy?.deniedKeys?.includes(mutationKey)) throw new Error('Mutación denegada');
|
if (policy?.deniedKeys?.includes(mutationKey))
|
||||||
if (policy?.allowedKeys && !policy.allowedKeys.includes(mutationKey)) throw new Error('Mutación no permitida');
|
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);
|
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;
|
return { ok: true } as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -212,6 +212,15 @@ async function bootstrap() {
|
|||||||
logger.error({ err: e }, "Error cargando eventos");
|
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
|
// Registrar comandos en segundo plano con reintentos; no bloquea el arranque del bot
|
||||||
withRetry("Registrar slash commands", async () => {
|
withRetry("Registrar slash commands", async () => {
|
||||||
await registeringCommands();
|
await registeringCommands();
|
||||||
|
|||||||
Reference in New Issue
Block a user