feat(economy): add commands for managing areas and items with interactive displays
This commit is contained in:
56
src/commands/messages/admin/areaEliminar.ts
Normal file
56
src/commands/messages/admin/areaEliminar.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { hasManageGuildOrStaff } from '../../../core/lib/permissions';
|
||||
import { prisma } from '../../../core/database/prisma';
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'area-eliminar',
|
||||
type: 'message',
|
||||
aliases: ['eliminar-area', 'area-delete'],
|
||||
cooldown: 5,
|
||||
description: 'Eliminar un área del servidor',
|
||||
usage: 'area-eliminar <key>',
|
||||
run: async (message, args, client: Amayo) => {
|
||||
const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, prisma);
|
||||
if (!allowed) {
|
||||
await message.reply('❌ No tienes permisos de ManageGuild ni rol de staff.');
|
||||
return;
|
||||
}
|
||||
|
||||
const guildId = message.guild!.id;
|
||||
const key = args[0]?.trim();
|
||||
|
||||
if (!key) {
|
||||
await message.reply('Uso: \`!area-eliminar <key>\`\nEjemplo: \`!area-eliminar mine.cavern\`');
|
||||
return;
|
||||
}
|
||||
|
||||
const area = await prisma.gameArea.findFirst({
|
||||
where: { key, guildId }
|
||||
});
|
||||
|
||||
if (!area) {
|
||||
await message.reply(`❌ No se encontró el área local con key ${key} en este servidor.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const levelsCount = await prisma.gameAreaLevel.count({
|
||||
where: { areaId: area.id }
|
||||
});
|
||||
|
||||
if (levelsCount > 0) {
|
||||
await prisma.gameAreaLevel.deleteMany({
|
||||
where: { areaId: area.id }
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.gameArea.delete({
|
||||
where: { id: area.id }
|
||||
});
|
||||
|
||||
await message.reply(
|
||||
`✅ Área ${key} eliminada exitosamente.\n` +
|
||||
`${levelsCount > 0 ? `⚠️ Se eliminaron ${levelsCount} nivel(es) asociado(s).` : ''}`
|
||||
);
|
||||
}
|
||||
};
|
||||
113
src/commands/messages/admin/areasLista.ts
Normal file
113
src/commands/messages/admin/areasLista.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { prisma } from '../../../core/database/prisma';
|
||||
import { ComponentType, ButtonStyle } from 'discord-api-types/v10';
|
||||
import type { MessageComponentInteraction, TextBasedChannel } from 'discord.js';
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'areas-lista',
|
||||
type: 'message',
|
||||
aliases: ['lista-areas', 'areas-list'],
|
||||
cooldown: 5,
|
||||
description: 'Ver lista de todas las áreas del servidor',
|
||||
usage: 'areas-lista [pagina]',
|
||||
run: async (message, args, client: Amayo) => {
|
||||
const guildId = message.guild!.id;
|
||||
const page = parseInt(args[0]) || 1;
|
||||
const perPage = 6;
|
||||
|
||||
const total = await prisma.gameArea.count({
|
||||
where: { OR: [{ guildId }, { guildId: null }] }
|
||||
});
|
||||
|
||||
const areas = await prisma.gameArea.findMany({
|
||||
where: { OR: [{ guildId }, { guildId: null }] },
|
||||
orderBy: [{ key: 'asc' }],
|
||||
skip: (page - 1) * perPage,
|
||||
take: perPage
|
||||
});
|
||||
|
||||
if (areas.length === 0) {
|
||||
await message.reply('No hay áreas configuradas en este servidor.');
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(total / perPage);
|
||||
|
||||
const display = {
|
||||
type: 17,
|
||||
accent_color: 0x00FF00,
|
||||
components: [
|
||||
{
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**🗺️ Lista de Áreas**\nPágina ${page}/${totalPages} • Total: ${total}`
|
||||
}]
|
||||
},
|
||||
{ type: 14, divider: true },
|
||||
...areas.map(area => ({
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**${area.name || area.key}**\n` +
|
||||
`└ Key: \`${area.key}\`\n` +
|
||||
`└ ${area.guildId === guildId ? '📍 Local' : '🌐 Global'}`
|
||||
}]
|
||||
}))
|
||||
]
|
||||
};
|
||||
|
||||
const buttons: any[] = [];
|
||||
|
||||
if (page > 1) {
|
||||
buttons.push({
|
||||
type: ComponentType.Button,
|
||||
style: ButtonStyle.Secondary,
|
||||
label: '◀ Anterior',
|
||||
custom_id: `areas_prev_${page}`
|
||||
});
|
||||
}
|
||||
|
||||
if (page < totalPages) {
|
||||
buttons.push({
|
||||
type: ComponentType.Button,
|
||||
style: ButtonStyle.Secondary,
|
||||
label: 'Siguiente ▶',
|
||||
custom_id: `areas_next_${page}`
|
||||
});
|
||||
}
|
||||
|
||||
const channel = message.channel as TextBasedChannel & { send: Function };
|
||||
const msg = await (channel.send as any)({
|
||||
display,
|
||||
components: buttons.length > 0 ? [{
|
||||
type: ComponentType.ActionRow,
|
||||
components: buttons
|
||||
}] : []
|
||||
});
|
||||
|
||||
const collector = msg.createMessageComponentCollector({
|
||||
time: 5 * 60_000,
|
||||
filter: (i) => i.user.id === message.author.id
|
||||
});
|
||||
|
||||
collector.on('collect', async (i: MessageComponentInteraction) => {
|
||||
if (!i.isButton()) return;
|
||||
|
||||
if (i.customId.startsWith('areas_prev_')) {
|
||||
const currentPage = parseInt(i.customId.split('_')[2]);
|
||||
await i.deferUpdate();
|
||||
args[0] = String(currentPage - 1);
|
||||
await command.run!(message, args, client);
|
||||
collector.stop();
|
||||
} else if (i.customId.startsWith('areas_next_')) {
|
||||
const currentPage = parseInt(i.customId.split('_')[2]);
|
||||
await i.deferUpdate();
|
||||
args[0] = String(currentPage + 1);
|
||||
await command.run!(message, args, client);
|
||||
collector.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
64
src/commands/messages/admin/itemEliminar.ts
Normal file
64
src/commands/messages/admin/itemEliminar.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { hasManageGuildOrStaff } from '../../../core/lib/permissions';
|
||||
import { prisma } from '../../../core/database/prisma';
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'item-eliminar',
|
||||
type: 'message',
|
||||
aliases: ['eliminar-item', 'item-delete'],
|
||||
cooldown: 5,
|
||||
description: 'Eliminar un item del servidor',
|
||||
usage: 'item-eliminar <key>',
|
||||
run: async (message, args, client: Amayo) => {
|
||||
const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, prisma);
|
||||
if (!allowed) {
|
||||
await message.reply('❌ No tienes permisos de ManageGuild ni rol de staff.');
|
||||
return;
|
||||
}
|
||||
|
||||
const guildId = message.guild!.id;
|
||||
const key = args[0]?.trim();
|
||||
|
||||
if (!key) {
|
||||
await message.reply('Uso: `!item-eliminar <key>`\nEjemplo: `!item-eliminar tool.pickaxe.iron`');
|
||||
return;
|
||||
}
|
||||
|
||||
const item = await prisma.economyItem.findFirst({
|
||||
where: { key, guildId }
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
await message.reply(`❌ No se encontró el item local con key \`${key}\` en este servidor.\n` +
|
||||
`💡 Solo puedes eliminar items locales del servidor, no globales.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar si está en uso
|
||||
const inInventory = await prisma.inventoryEntry.count({
|
||||
where: { itemId: item.id, quantity: { gt: 0 } }
|
||||
});
|
||||
|
||||
const inOffers = await prisma.shopOffer.count({
|
||||
where: { itemId: item.id }
|
||||
});
|
||||
|
||||
if (inInventory > 0 || inOffers > 0) {
|
||||
await message.reply(
|
||||
`⚠️ **Advertencia:** Este item está en uso:\n` +
|
||||
`${inInventory > 0 ? `• En ${inInventory} inventario(s)\n` : ''}` +
|
||||
`${inOffers > 0 ? `• En ${inOffers} oferta(s) de tienda\n` : ''}` +
|
||||
`¿Estás seguro? Usa \`!item-eliminar-forzar ${key}\` para confirmar.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Eliminar el item
|
||||
await prisma.economyItem.delete({
|
||||
where: { id: item.id }
|
||||
});
|
||||
|
||||
await message.reply(`✅ Item \`${key}\` eliminado exitosamente.`);
|
||||
}
|
||||
};
|
||||
104
src/commands/messages/admin/itemVer.ts
Normal file
104
src/commands/messages/admin/itemVer.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { prisma } from '../../../core/database/prisma';
|
||||
import type { TextBasedChannel } from 'discord.js';
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'item-ver',
|
||||
type: 'message',
|
||||
aliases: ['ver-item', 'item-view'],
|
||||
cooldown: 3,
|
||||
description: 'Ver detalles de un item específico',
|
||||
usage: 'item-ver <key>',
|
||||
run: async (message, args, client: Amayo) => {
|
||||
const guildId = message.guild!.id;
|
||||
const key = args[0]?.trim();
|
||||
|
||||
if (!key) {
|
||||
await message.reply('Uso: `!item-ver <key>`\nEjemplo: `!item-ver tool.pickaxe.iron`');
|
||||
return;
|
||||
}
|
||||
|
||||
const item = await prisma.economyItem.findFirst({
|
||||
where: {
|
||||
key,
|
||||
OR: [{ guildId }, { guildId: null }]
|
||||
}
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
await message.reply(`❌ No se encontró el item con key \`${key}\``);
|
||||
return;
|
||||
}
|
||||
|
||||
const props = item.props as any || {};
|
||||
const tags = item.tags || [];
|
||||
|
||||
const display = {
|
||||
type: 17,
|
||||
accent_color: 0x00D9FF,
|
||||
components: [
|
||||
{
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**🛠️ ${item.name || item.key}**`
|
||||
}]
|
||||
},
|
||||
{ type: 14, divider: true },
|
||||
{
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**Key:** \`${item.key}\`\n` +
|
||||
`**Nombre:** ${item.name || '*Sin nombre*'}\n` +
|
||||
`**Descripción:** ${item.description || '*Sin descripción*'}\n` +
|
||||
`**Categoría:** ${item.category || '*Sin categoría*'}\n` +
|
||||
`**Stackable:** ${item.stackable ? 'Sí' : 'No'}\n` +
|
||||
`**Máx. Inventario:** ${item.maxPerInventory || 'Ilimitado'}\n` +
|
||||
`**Ámbito:** ${item.guildId ? '📍 Local del servidor' : '🌐 Global'}`
|
||||
}]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (tags.length > 0) {
|
||||
display.components.push({ type: 14, divider: true });
|
||||
display.components.push({
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**Tags:** ${tags.join(', ')}`
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
if (item.icon) {
|
||||
display.components.push({ type: 14, divider: true });
|
||||
display.components.push({
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**Icon URL:** ${item.icon}`
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.keys(props).length > 0) {
|
||||
display.components.push({ type: 14, divider: true });
|
||||
display.components.push({
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**Props (JSON):**\n\`\`\`json\n${JSON.stringify(props, null, 2)}\n\`\`\``
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
const channel = message.channel as TextBasedChannel & { send: Function };
|
||||
await (channel.send as any)({
|
||||
display,
|
||||
reply: { messageReference: message.id }
|
||||
});
|
||||
}
|
||||
};
|
||||
128
src/commands/messages/admin/itemsLista.ts
Normal file
128
src/commands/messages/admin/itemsLista.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { prisma } from '../../../core/database/prisma';
|
||||
import { ComponentType, ButtonStyle } from 'discord-api-types/v10';
|
||||
import type { MessageComponentInteraction, TextBasedChannel } from 'discord.js';
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'items-lista',
|
||||
type: 'message',
|
||||
aliases: ['lista-items', 'items-list'],
|
||||
cooldown: 5,
|
||||
description: 'Ver lista de todos los items del servidor',
|
||||
usage: 'items-lista [pagina]',
|
||||
run: async (message, args, client: Amayo) => {
|
||||
const guildId = message.guild!.id;
|
||||
const page = parseInt(args[0]) || 1;
|
||||
const perPage = 8;
|
||||
|
||||
const total = await prisma.economyItem.count({
|
||||
where: { OR: [{ guildId }, { guildId: null }] }
|
||||
});
|
||||
|
||||
const items = await prisma.economyItem.findMany({
|
||||
where: { OR: [{ guildId }, { guildId: null }] },
|
||||
orderBy: [{ category: 'asc' }, { name: 'asc' }],
|
||||
skip: (page - 1) * perPage,
|
||||
take: perPage
|
||||
});
|
||||
|
||||
if (items.length === 0) {
|
||||
await message.reply('No hay items configurados en este servidor.');
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(total / perPage);
|
||||
|
||||
const display = {
|
||||
type: 17,
|
||||
accent_color: 0x00D9FF,
|
||||
components: [
|
||||
{
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**🛠️ Lista de Items**\nPágina ${page}/${totalPages} • Total: ${total}`
|
||||
}]
|
||||
},
|
||||
{ type: 14, divider: true },
|
||||
...items.map(item => ({
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**${item.name || item.key}**\n` +
|
||||
`└ Key: \`${item.key}\`\n` +
|
||||
`└ Categoría: ${item.category || '*Sin categoría*'}\n` +
|
||||
`└ ${item.stackable ? '📚 Apilable' : '🔒 No apilable'}` +
|
||||
(item.maxPerInventory ? ` (Máx: ${item.maxPerInventory})` : '') +
|
||||
(item.guildId === guildId ? ' • 📍 Local' : ' • 🌐 Global')
|
||||
}]
|
||||
}))
|
||||
]
|
||||
};
|
||||
|
||||
const buttons: any[] = [];
|
||||
|
||||
if (page > 1) {
|
||||
buttons.push({
|
||||
type: ComponentType.Button,
|
||||
style: ButtonStyle.Secondary,
|
||||
label: '◀ Anterior',
|
||||
custom_id: `items_prev_${page}`
|
||||
});
|
||||
}
|
||||
|
||||
if (page < totalPages) {
|
||||
buttons.push({
|
||||
type: ComponentType.Button,
|
||||
style: ButtonStyle.Secondary,
|
||||
label: 'Siguiente ▶',
|
||||
custom_id: `items_next_${page}`
|
||||
});
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
type: ComponentType.Button,
|
||||
style: ButtonStyle.Primary,
|
||||
label: 'Ver Detalle',
|
||||
custom_id: 'items_detail'
|
||||
});
|
||||
|
||||
const channel = message.channel as TextBasedChannel & { send: Function };
|
||||
const msg = await (channel.send as any)({
|
||||
display,
|
||||
components: buttons.length > 0 ? [{
|
||||
type: ComponentType.ActionRow,
|
||||
components: buttons
|
||||
}] : []
|
||||
});
|
||||
|
||||
const collector = msg.createMessageComponentCollector({
|
||||
time: 5 * 60_000,
|
||||
filter: (i) => i.user.id === message.author.id
|
||||
});
|
||||
|
||||
collector.on('collect', async (i: MessageComponentInteraction) => {
|
||||
if (!i.isButton()) return;
|
||||
|
||||
if (i.customId.startsWith('items_prev_')) {
|
||||
const currentPage = parseInt(i.customId.split('_')[2]);
|
||||
await i.deferUpdate();
|
||||
args[0] = String(currentPage - 1);
|
||||
await command.run!(message, args, client);
|
||||
collector.stop();
|
||||
} else if (i.customId.startsWith('items_next_')) {
|
||||
const currentPage = parseInt(i.customId.split('_')[2]);
|
||||
await i.deferUpdate();
|
||||
args[0] = String(currentPage + 1);
|
||||
await command.run!(message, args, client);
|
||||
collector.stop();
|
||||
} else if (i.customId === 'items_detail') {
|
||||
await i.reply({
|
||||
content: '💡 Usa `!item-ver <key>` para ver detalles de un item específico.',
|
||||
flags: 64
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { prisma } from '../../../core/database/prisma';
|
||||
import type { TextBasedChannel } from 'discord.js';
|
||||
import { ComponentType, ButtonStyle } from 'discord-api-types/v10';
|
||||
|
||||
export const command: CommandMessage = {
|
||||
@@ -117,6 +118,6 @@ export const command: CommandMessage = {
|
||||
});
|
||||
}
|
||||
|
||||
await message.reply({ display } as any);
|
||||
const channel = message.channel as TextBasedChannel & { send: Function }; await (channel.send as any)({ display, reply: { messageReference: message.id } });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { prisma } from '../../../core/database/prisma';
|
||||
import type { TextBasedChannel } from 'discord.js';
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'mision-ver',
|
||||
@@ -150,6 +151,6 @@ export const command: CommandMessage = {
|
||||
});
|
||||
}
|
||||
|
||||
await message.reply({ display } as any);
|
||||
const channel = message.channel as TextBasedChannel & { send: Function }; await (channel.send as any)({ display, reply: { messageReference: message.id } });
|
||||
}
|
||||
};
|
||||
|
||||
43
src/commands/messages/admin/mobEliminar.ts
Normal file
43
src/commands/messages/admin/mobEliminar.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { hasManageGuildOrStaff } from '../../../core/lib/permissions';
|
||||
import { prisma } from '../../../core/database/prisma';
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'mob-eliminar',
|
||||
type: 'message',
|
||||
aliases: ['eliminar-mob', 'mob-delete'],
|
||||
cooldown: 5,
|
||||
description: 'Eliminar un mob del servidor',
|
||||
usage: 'mob-eliminar <key>',
|
||||
run: async (message, args, client: Amayo) => {
|
||||
const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, prisma);
|
||||
if (!allowed) {
|
||||
await message.reply('❌ No tienes permisos de ManageGuild ni rol de staff.');
|
||||
return;
|
||||
}
|
||||
|
||||
const guildId = message.guild!.id;
|
||||
const key = args[0]?.trim();
|
||||
|
||||
if (!key) {
|
||||
await message.reply('Uso: \`!mob-eliminar <key>\`\nEjemplo: \`!mob-eliminar mob.goblin\`');
|
||||
return;
|
||||
}
|
||||
|
||||
const mob = await prisma.mob.findFirst({
|
||||
where: { key, guildId }
|
||||
});
|
||||
|
||||
if (!mob) {
|
||||
await message.reply(`❌ No se encontró el mob local con key ${key} en este servidor.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.mob.delete({
|
||||
where: { id: mob.id }
|
||||
});
|
||||
|
||||
await message.reply(`✅ Mob ${key} eliminado exitosamente.`);
|
||||
}
|
||||
};
|
||||
117
src/commands/messages/admin/mobsLista.ts
Normal file
117
src/commands/messages/admin/mobsLista.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { CommandMessage } from '../../../core/types/commands';
|
||||
import type Amayo from '../../../core/client';
|
||||
import { prisma } from '../../../core/database/prisma';
|
||||
import { ComponentType, ButtonStyle } from 'discord-api-types/v10';
|
||||
import type { MessageComponentInteraction, TextBasedChannel } from 'discord.js';
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'mobs-lista',
|
||||
type: 'message',
|
||||
aliases: ['lista-mobs', 'mobs-list'],
|
||||
cooldown: 5,
|
||||
description: 'Ver lista de todos los mobs del servidor',
|
||||
usage: 'mobs-lista [pagina]',
|
||||
run: async (message, args, client: Amayo) => {
|
||||
const guildId = message.guild!.id;
|
||||
const page = parseInt(args[0]) || 1;
|
||||
const perPage = 6;
|
||||
|
||||
const total = await prisma.mob.count({
|
||||
where: { OR: [{ guildId }, { guildId: null }] }
|
||||
});
|
||||
|
||||
const mobs = await prisma.mob.findMany({
|
||||
where: { OR: [{ guildId }, { guildId: null }] },
|
||||
orderBy: [{ key: 'asc' }],
|
||||
skip: (page - 1) * perPage,
|
||||
take: perPage
|
||||
});
|
||||
|
||||
if (mobs.length === 0) {
|
||||
await message.reply('No hay mobs configurados en este servidor.');
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(total / perPage);
|
||||
|
||||
const display = {
|
||||
type: 17,
|
||||
accent_color: 0xFF0000,
|
||||
components: [
|
||||
{
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**👾 Lista de Mobs**\nPágina ${page}/${totalPages} • Total: ${total}`
|
||||
}]
|
||||
},
|
||||
{ type: 14, divider: true },
|
||||
...mobs.map(mob => {
|
||||
const stats = mob.stats as any || {};
|
||||
return {
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**${mob.name || mob.key}**\n` +
|
||||
`└ Key: \`${mob.key}\`\n` +
|
||||
`└ ATK: ${stats.attack || 0} | HP: ${stats.hp || 0}\n` +
|
||||
`└ ${mob.guildId === guildId ? '📍 Local' : '🌐 Global'}`
|
||||
}]
|
||||
};
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
const buttons: any[] = [];
|
||||
|
||||
if (page > 1) {
|
||||
buttons.push({
|
||||
type: ComponentType.Button,
|
||||
style: ButtonStyle.Secondary,
|
||||
label: '◀ Anterior',
|
||||
custom_id: `mobs_prev_${page}`
|
||||
});
|
||||
}
|
||||
|
||||
if (page < totalPages) {
|
||||
buttons.push({
|
||||
type: ComponentType.Button,
|
||||
style: ButtonStyle.Secondary,
|
||||
label: 'Siguiente ▶',
|
||||
custom_id: `mobs_next_${page}`
|
||||
});
|
||||
}
|
||||
|
||||
const channel = message.channel as TextBasedChannel & { send: Function };
|
||||
const msg = await (channel.send as any)({
|
||||
display,
|
||||
components: buttons.length > 0 ? [{
|
||||
type: ComponentType.ActionRow,
|
||||
components: buttons
|
||||
}] : []
|
||||
});
|
||||
|
||||
const collector = msg.createMessageComponentCollector({
|
||||
time: 5 * 60_000,
|
||||
filter: (i) => i.user.id === message.author.id
|
||||
});
|
||||
|
||||
collector.on('collect', async (i: MessageComponentInteraction) => {
|
||||
if (!i.isButton()) return;
|
||||
|
||||
if (i.customId.startsWith('mobs_prev_')) {
|
||||
const currentPage = parseInt(i.customId.split('_')[2]);
|
||||
await i.deferUpdate();
|
||||
args[0] = String(currentPage - 1);
|
||||
await command.run!(message, args, client);
|
||||
collector.stop();
|
||||
} else if (i.customId.startsWith('mobs_next_')) {
|
||||
const currentPage = parseInt(i.customId.split('_')[2]);
|
||||
await i.deferUpdate();
|
||||
args[0] = String(currentPage + 1);
|
||||
await command.run!(message, args, client);
|
||||
collector.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -20,43 +20,98 @@ interface ItemEditorState {
|
||||
export const command: CommandMessage = {
|
||||
name: 'item-editar',
|
||||
type: 'message',
|
||||
aliases: ['editar-item','itemedit'],
|
||||
aliases: ['crear-item','itemcreate'],
|
||||
cooldown: 10,
|
||||
description: 'Edita un EconomyItem de este servidor con un editor interactivo.',
|
||||
description: 'Crea un EconomyItem para este servidor con un pequeño editor interactivo.',
|
||||
category: 'Economía',
|
||||
usage: 'item-editar <key-única>',
|
||||
run: async (message: Message, args: string[], client: Amayo) => {
|
||||
const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma);
|
||||
if (!allowed) { await message.reply('❌ No tienes permisos de ManageGuild ni rol de staff.'); return; }
|
||||
if (!allowed) {
|
||||
await message.reply('❌ No tienes permisos de ManageGuild ni rol de staff.');
|
||||
return;
|
||||
}
|
||||
|
||||
const key = args[0]?.trim();
|
||||
if (!key) { await message.reply('Uso: `!item-editar <key-única>`'); return; }
|
||||
if (!key) {
|
||||
await message.reply('Uso: `!item-editar <key-única>`');
|
||||
return;
|
||||
}
|
||||
|
||||
const guildId = message.guild!.id;
|
||||
|
||||
const item = await client.prisma.economyItem.findFirst({ where: { key, guildId } });
|
||||
if (!item) { await message.reply('❌ No existe un item con esa key en este servidor.'); return; }
|
||||
const exists = await client.prisma.economyItem.findFirst({ where: { key, guildId } });
|
||||
if (exists) {
|
||||
await message.reply('❌ Ya existe un item con esa key en este servidor.');
|
||||
return;
|
||||
}
|
||||
|
||||
const state: ItemEditorState = {
|
||||
key,
|
||||
name: item.name,
|
||||
description: item.description ?? undefined,
|
||||
category: item.category ?? undefined,
|
||||
icon: item.icon ?? undefined,
|
||||
stackable: item.stackable ?? true,
|
||||
maxPerInventory: item.maxPerInventory ?? null,
|
||||
tags: item.tags ?? [],
|
||||
props: item.props ?? {},
|
||||
tags: [],
|
||||
stackable: true,
|
||||
maxPerInventory: null,
|
||||
props: {},
|
||||
};
|
||||
|
||||
// Función para crear display
|
||||
const createDisplay = () => ({
|
||||
display: {
|
||||
type: 17,
|
||||
accent_color: 0x00D9FF,
|
||||
components: [
|
||||
{
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**🛠️ Editando Item: \`${key}\`**`
|
||||
}]
|
||||
},
|
||||
{ type: 14, divider: true },
|
||||
{
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**Nombre:** ${state.name || '*Sin definir*'}\n` +
|
||||
`**Descripción:** ${state.description || '*Sin definir*'}\n` +
|
||||
`**Categoría:** ${state.category || '*Sin definir*'}\n` +
|
||||
`**Icon URL:** ${state.icon || '*Sin definir*'}\n` +
|
||||
`**Stackable:** ${state.stackable ? 'Sí' : 'No'}\n` +
|
||||
`**Máx. Inventario:** ${state.maxPerInventory || 'Ilimitado'}`
|
||||
}]
|
||||
},
|
||||
{ type: 14, divider: true },
|
||||
{
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**Tags:** ${state.tags.length > 0 ? state.tags.join(', ') : '*Ninguno*'}`
|
||||
}]
|
||||
},
|
||||
{ type: 14, divider: true },
|
||||
{
|
||||
type: 9,
|
||||
components: [{
|
||||
type: 10,
|
||||
content: `**Props (JSON):**\n\`\`\`json\n${JSON.stringify(state.props, null, 2)}\n\`\`\``
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const channel = message.channel as TextBasedChannel & { send: Function };
|
||||
const editorMsg = await channel.send({
|
||||
content: `🛠️ Editor de Item (editar): \`${key}\``,
|
||||
components: [ { type: 1, components: [
|
||||
{ type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'it_base' },
|
||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Tags', custom_id: 'it_tags' },
|
||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Props (JSON)', custom_id: 'it_props' },
|
||||
{ type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'it_save' },
|
||||
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'it_cancel' },
|
||||
] } ],
|
||||
...createDisplay(),
|
||||
components: [
|
||||
{ type: 1, components: [
|
||||
{ type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'it_base' },
|
||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Tags', custom_id: 'it_tags' },
|
||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Props (JSON)', custom_id: 'it_props' },
|
||||
{ type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'it_save' },
|
||||
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'it_cancel' },
|
||||
]},
|
||||
],
|
||||
});
|
||||
|
||||
const collector = editorMsg.createMessageComponentCollector({ time: 30 * 60_000, filter: (i) => i.user.id === message.author.id });
|
||||
@@ -64,15 +119,35 @@ export const command: CommandMessage = {
|
||||
collector.on('collect', async (i: MessageComponentInteraction) => {
|
||||
try {
|
||||
if (!i.isButton()) return;
|
||||
if (i.customId === 'it_cancel') { await i.deferUpdate(); await editorMsg.edit({ content: '❌ Editor cancelado.', components: [] }); collector.stop('cancel'); return; }
|
||||
if (i.customId === 'it_base') { await showBaseModal(i as ButtonInteraction, state); return; }
|
||||
if (i.customId === 'it_tags') { await showTagsModal(i as ButtonInteraction, state); return; }
|
||||
if (i.customId === 'it_props') { await showPropsModal(i as ButtonInteraction, state); return; }
|
||||
if (i.customId === 'it_cancel') {
|
||||
await i.deferUpdate();
|
||||
await editorMsg.edit({ content: '❌ Editor cancelado.', components: [], display: undefined });
|
||||
collector.stop('cancel');
|
||||
return;
|
||||
}
|
||||
if (i.customId === 'it_base') {
|
||||
await showBaseModal(i as ButtonInteraction, state, editorMsg, createDisplay);
|
||||
return;
|
||||
}
|
||||
if (i.customId === 'it_tags') {
|
||||
await showTagsModal(i as ButtonInteraction, state, editorMsg, createDisplay);
|
||||
return;
|
||||
}
|
||||
if (i.customId === 'it_props') {
|
||||
await showPropsModal(i as ButtonInteraction, state, editorMsg, createDisplay);
|
||||
return;
|
||||
}
|
||||
if (i.customId === 'it_save') {
|
||||
if (!state.name) { await i.reply({ content: '❌ Falta el nombre del item.', flags: MessageFlags.Ephemeral }); return; }
|
||||
await client.prisma.economyItem.update({
|
||||
where: { id: item.id },
|
||||
// Validar
|
||||
if (!state.name) {
|
||||
await i.reply({ content: '❌ Falta el nombre del item (configura en Base).', flags: MessageFlags.Ephemeral });
|
||||
return;
|
||||
}
|
||||
// Guardar
|
||||
await client.prisma.economyItem.create({
|
||||
data: {
|
||||
guildId,
|
||||
key: state.key,
|
||||
name: state.name!,
|
||||
description: state.description,
|
||||
category: state.category,
|
||||
@@ -83,8 +158,8 @@ export const command: CommandMessage = {
|
||||
props: state.props ?? {},
|
||||
},
|
||||
});
|
||||
await i.reply({ content: '✅ Item actualizado!', flags: MessageFlags.Ephemeral });
|
||||
await editorMsg.edit({ content: `✅ Item \`${state.key}\` actualizado.`, components: [] });
|
||||
await i.reply({ content: '✅ Item guardado!', flags: MessageFlags.Ephemeral });
|
||||
await editorMsg.edit({ content: `✅ Item \`${state.key}\` creado.`, components: [], display: undefined });
|
||||
collector.stop('saved');
|
||||
return;
|
||||
}
|
||||
@@ -94,50 +169,107 @@ export const command: CommandMessage = {
|
||||
}
|
||||
});
|
||||
|
||||
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: [], display: undefined }); } catch {}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
async function showBaseModal(i: ButtonInteraction, state: ItemEditorState) {
|
||||
async function showBaseModal(i: ButtonInteraction, state: ItemEditorState, editorMsg: any, createDisplay: Function) {
|
||||
const modal = {
|
||||
title: 'Configuración base del Item', customId: 'it_base_modal', components: [
|
||||
title: 'Configuración base del Item',
|
||||
customId: 'it_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: 'Descripción', component: { type: ComponentType.TextInput, customId: 'desc', style: TextInputStyle.Paragraph, required: false, value: state.description ?? '' } },
|
||||
{ type: ComponentType.Label, label: 'Categoría', component: { type: ComponentType.TextInput, customId: 'cat', style: TextInputStyle.Short, required: false, value: state.category ?? '' } },
|
||||
{ type: ComponentType.Label, label: 'Icon URL', component: { type: ComponentType.TextInput, customId: 'icon', style: TextInputStyle.Short, required: false, value: state.icon ?? '' } },
|
||||
{ type: ComponentType.Label, label: 'Stackable y Máx inventario', component: { type: ComponentType.TextInput, customId: 'stack_max', style: TextInputStyle.Short, required: false, placeholder: 'true,10', value: state.stackable !== undefined ? `${state.stackable},${state.maxPerInventory ?? ''}` : '' } },
|
||||
], } as const;
|
||||
],
|
||||
} as const;
|
||||
|
||||
await i.showModal(modal);
|
||||
try {
|
||||
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
||||
state.name = sub.components.getTextInputValue('name').trim();
|
||||
state.description = sub.components.getTextInputValue('desc').trim() || undefined;
|
||||
state.category = sub.components.getTextInputValue('cat').trim() || undefined;
|
||||
state.icon = sub.components.getTextInputValue('icon').trim() || undefined;
|
||||
const name = sub.components.getTextInputValue('name').trim();
|
||||
const desc = sub.components.getTextInputValue('desc').trim();
|
||||
const cat = sub.components.getTextInputValue('cat').trim();
|
||||
const icon = sub.components.getTextInputValue('icon').trim();
|
||||
const stackMax = sub.components.getTextInputValue('stack_max').trim();
|
||||
if (stackMax) { const [s,m] = stackMax.split(','); state.stackable = String(s).toLowerCase() !== 'false'; const mv = m?.trim(); state.maxPerInventory = mv ? Math.max(0, parseInt(mv,10)||0) : null; }
|
||||
await sub.reply({ content: '✅ Base actualizada.', flags: MessageFlags.Ephemeral });
|
||||
|
||||
state.name = name;
|
||||
state.description = desc || undefined;
|
||||
state.category = cat || undefined;
|
||||
state.icon = icon || undefined;
|
||||
|
||||
if (stackMax) {
|
||||
const [s, m] = stackMax.split(',');
|
||||
state.stackable = String(s).toLowerCase() !== 'false';
|
||||
const mv = m?.trim();
|
||||
state.maxPerInventory = mv ? Math.max(0, parseInt(mv, 10) || 0) : null;
|
||||
}
|
||||
|
||||
await sub.deferUpdate();
|
||||
await editorMsg.edit(createDisplay());
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function showTagsModal(i: ButtonInteraction, state: ItemEditorState) {
|
||||
const modal = { title: 'Tags del Item (separados por coma)', customId: 'it_tags_modal', components: [
|
||||
{ type: ComponentType.Label, label: 'Tags', component: { type: ComponentType.TextInput, customId: 'tags', style: TextInputStyle.Paragraph, required: false, value: state.tags.join(', ') } },
|
||||
], } as const;
|
||||
async function showTagsModal(i: ButtonInteraction, state: ItemEditorState, editorMsg: any, createDisplay: Function) {
|
||||
const modal = {
|
||||
title: 'Tags del Item (separados por coma)',
|
||||
customId: 'it_tags_modal',
|
||||
components: [
|
||||
{ type: ComponentType.Label, label: 'Tags', component: { type: ComponentType.TextInput, customId: 'tags', style: TextInputStyle.Paragraph, required: false, value: state.tags.join(', ') } },
|
||||
],
|
||||
} as const;
|
||||
await i.showModal(modal);
|
||||
try { const sub = await i.awaitModalSubmit({ time: 300_000 }); const tags = sub.components.getTextInputValue('tags'); state.tags = tags ? tags.split(',').map(t=>t.trim()).filter(Boolean) : []; await sub.reply({ content: '✅ Tags actualizados.', flags: MessageFlags.Ephemeral }); } catch {}
|
||||
try {
|
||||
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
||||
const tags = sub.components.getTextInputValue('tags');
|
||||
state.tags = tags ? tags.split(',').map((t) => t.trim()).filter(Boolean) : [];
|
||||
await sub.deferUpdate();
|
||||
await editorMsg.edit(createDisplay());
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function showPropsModal(i: ButtonInteraction, state: ItemEditorState) {
|
||||
const template = state.props && Object.keys(state.props).length ? JSON.stringify(state.props) : JSON.stringify({});
|
||||
const modal = { title: 'Props (JSON) del Item', customId: 'it_props_modal', components: [
|
||||
{ type: ComponentType.Label, label: 'JSON', component: { type: ComponentType.TextInput, customId: 'props', style: TextInputStyle.Paragraph, required: false, value: template.slice(0,4000) } },
|
||||
], } as const;
|
||||
async function showPropsModal(i: ButtonInteraction, state: ItemEditorState, editorMsg: any, createDisplay: Function) {
|
||||
const template = state.props && Object.keys(state.props).length ? JSON.stringify(state.props) : JSON.stringify({
|
||||
tool: undefined,
|
||||
breakable: undefined,
|
||||
chest: undefined,
|
||||
eventCurrency: undefined,
|
||||
passiveEffects: [],
|
||||
mutationPolicy: undefined,
|
||||
craftingOnly: false,
|
||||
food: undefined,
|
||||
damage: undefined,
|
||||
defense: undefined,
|
||||
maxHpBonus: undefined,
|
||||
});
|
||||
const modal = {
|
||||
title: 'Props (JSON) del Item',
|
||||
customId: 'it_props_modal',
|
||||
components: [
|
||||
{ type: ComponentType.Label, label: 'JSON', component: { type: ComponentType.TextInput, customId: 'props', style: TextInputStyle.Paragraph, required: false, value: template.slice(0,4000) } },
|
||||
],
|
||||
} as const;
|
||||
await i.showModal(modal);
|
||||
try {
|
||||
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
||||
const raw = sub.components.getTextInputValue('props');
|
||||
if (raw) { try { state.props = JSON.parse(raw); await sub.reply({ content: '✅ Props guardados.', flags: MessageFlags.Ephemeral }); } catch { await sub.reply({ content: '❌ JSON inválido.', flags: MessageFlags.Ephemeral }); } }
|
||||
else { state.props = {}; await sub.reply({ content: 'ℹ️ Props limpiados.', flags: MessageFlags.Ephemeral }); }
|
||||
if (raw) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
state.props = parsed;
|
||||
await sub.deferUpdate(); await editorMsg.edit(createDisplay());
|
||||
} catch (e) {
|
||||
await sub.reply({ content: '❌ JSON inválido.', flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
} else {
|
||||
state.props = {};
|
||||
await sub.reply({ content: 'ℹ️ Props limpiados.', flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { prisma } from '../../../core/database/prisma';
|
||||
import { getOrCreateWallet } from '../../../game/economy/service';
|
||||
import { getEquipment, getEffectiveStats } from '../../../game/combat/equipmentService';
|
||||
import { getPlayerStatsFormatted } from '../../../game/stats/service';
|
||||
import type { TextBasedChannel } from 'discord.js';
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: 'player',
|
||||
@@ -166,6 +167,10 @@ export const command: CommandMessage = {
|
||||
});
|
||||
}
|
||||
|
||||
await message.reply({ display } as any);
|
||||
const channel = message.channel as TextBasedChannel & { send: Function };
|
||||
await (channel.send as any)({
|
||||
display,
|
||||
reply: { messageReference: message.id }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user