feat: Enhance offer editing command with interactive selection and improved error handling
- Added interactive selection for offers in the `offerEdit` command, allowing users to choose an offer to edit. - Improved permission checks with detailed error messages. - Refactored editor display and components for better readability and user experience. - Updated modal handling for various editing options (base, price, window, limits, metadata) to ensure consistent UI updates. - Enhanced feedback messages for successful updates and cancellations. - Integrated item fetching and formatting for better display of rewards in `pelear`, `pescar`, `plantar`, and `racha` commands. - Improved item display in the shop command with consistent formatting and icons.
This commit is contained in:
@@ -1,6 +1,16 @@
|
|||||||
import { prisma } from '../../../core/database/prisma';
|
import { prisma } from '../../../core/database/prisma';
|
||||||
import type { GameArea } from '@prisma/client';
|
import type { GameArea } from '@prisma/client';
|
||||||
import type { ItemProps } from '../../../game/economy/types';
|
import type { ItemProps } from '../../../game/economy/types';
|
||||||
|
import type {
|
||||||
|
Message,
|
||||||
|
TextBasedChannel,
|
||||||
|
MessageComponentInteraction,
|
||||||
|
StringSelectMenuInteraction,
|
||||||
|
ButtonInteraction,
|
||||||
|
ModalSubmitInteraction
|
||||||
|
} from 'discord.js';
|
||||||
|
import { MessageFlags } from 'discord.js';
|
||||||
|
import { ButtonStyle, ComponentType, TextInputStyle } from 'discord-api-types/v10';
|
||||||
|
|
||||||
export function parseItemProps(json: unknown): ItemProps {
|
export function parseItemProps(json: unknown): ItemProps {
|
||||||
if (!json || typeof json !== 'object') return {};
|
if (!json || typeof json !== 'object') return {};
|
||||||
@@ -99,3 +109,396 @@ export function parseGameArgs(args: string[]): ParsedGameArgs {
|
|||||||
return { levelArg, providedTool, areaOverride };
|
return { levelArg, providedTool, areaOverride };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_ITEM_ICON = '📦';
|
||||||
|
|
||||||
|
export function resolveItemIcon(icon?: string | null, fallback = DEFAULT_ITEM_ICON) {
|
||||||
|
const trimmed = icon?.trim();
|
||||||
|
return trimmed && trimmed.length > 0 ? trimmed : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatItemLabel(
|
||||||
|
item: { key: string; name?: string | null; icon?: string | null },
|
||||||
|
options: { fallbackIcon?: string; bold?: boolean } = {}
|
||||||
|
): string {
|
||||||
|
const fallbackIcon = options.fallbackIcon ?? DEFAULT_ITEM_ICON;
|
||||||
|
const icon = resolveItemIcon(item.icon, fallbackIcon);
|
||||||
|
const label = (item.name ?? '').trim() || item.key;
|
||||||
|
const content = `${icon ? `${icon} ` : ''}${label}`.trim();
|
||||||
|
return options.bold ? `**${content}**` : content;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ItemBasicInfo = { key: string; name: string | null; icon: string | null };
|
||||||
|
|
||||||
|
export async function fetchItemBasics(guildId: string, keys: string[]): Promise<Map<string, ItemBasicInfo>> {
|
||||||
|
const uniqueKeys = Array.from(new Set(keys.filter((key): key is string => Boolean(key && key.trim()))));
|
||||||
|
if (uniqueKeys.length === 0) return new Map();
|
||||||
|
|
||||||
|
const rows = await prisma.economyItem.findMany({
|
||||||
|
where: {
|
||||||
|
key: { in: uniqueKeys },
|
||||||
|
OR: [{ guildId }, { guildId: null }],
|
||||||
|
},
|
||||||
|
orderBy: [{ key: 'asc' }, { guildId: 'desc' }],
|
||||||
|
select: { key: true, name: true, icon: true, guildId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = new Map<string, ItemBasicInfo>();
|
||||||
|
for (const row of rows) {
|
||||||
|
const current = result.get(row.key);
|
||||||
|
if (!current || row.guildId === guildId) {
|
||||||
|
result.set(row.key, { key: row.key, name: row.name, icon: row.icon });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of uniqueKeys) {
|
||||||
|
if (!result.has(key)) {
|
||||||
|
result.set(key, { key, name: null, icon: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeyPickerOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
keywords?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeyPickerConfig<T> {
|
||||||
|
entries: T[];
|
||||||
|
getOption: (entry: T) => KeyPickerOption;
|
||||||
|
title: string;
|
||||||
|
customIdPrefix: string;
|
||||||
|
emptyText: string;
|
||||||
|
placeholder?: string;
|
||||||
|
filterHint?: string;
|
||||||
|
accentColor?: number;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeyPickerResult<T> {
|
||||||
|
entry: T | null;
|
||||||
|
panelMessage: Message | null;
|
||||||
|
reason: 'selected' | 'empty' | 'cancelled' | 'timeout';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function promptKeySelection<T>(
|
||||||
|
message: Message,
|
||||||
|
config: KeyPickerConfig<T>
|
||||||
|
): Promise<KeyPickerResult<T>> {
|
||||||
|
const channel = message.channel as TextBasedChannel & { send: Function };
|
||||||
|
const userId = config.userId ?? message.author?.id ?? message.member?.user.id;
|
||||||
|
|
||||||
|
const baseOptions = config.entries.map((entry) => {
|
||||||
|
const option = config.getOption(entry);
|
||||||
|
const searchText = [option.label, option.description, option.value, ...(option.keywords ?? [])]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
.toLowerCase();
|
||||||
|
return { entry, option, searchText };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (baseOptions.length === 0) {
|
||||||
|
const emptyPanel = {
|
||||||
|
type: 17,
|
||||||
|
accent_color: 0xFFA500,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: config.emptyText,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
await (channel.send as any)({
|
||||||
|
content: null,
|
||||||
|
flags: 32768,
|
||||||
|
reply: { messageReference: message.id },
|
||||||
|
components: [emptyPanel],
|
||||||
|
});
|
||||||
|
return { entry: null, panelMessage: null, reason: 'empty' };
|
||||||
|
}
|
||||||
|
|
||||||
|
let filter = '';
|
||||||
|
let page = 0;
|
||||||
|
const pageSize = 25;
|
||||||
|
const accentColor = config.accentColor ?? 0x5865F2;
|
||||||
|
const placeholder = config.placeholder ?? 'Selecciona una opción…';
|
||||||
|
|
||||||
|
const buildComponents = () => {
|
||||||
|
const normalizedFilter = filter.trim().toLowerCase();
|
||||||
|
const filtered = normalizedFilter
|
||||||
|
? baseOptions.filter((item) => item.searchText.includes(normalizedFilter))
|
||||||
|
: baseOptions;
|
||||||
|
const totalFiltered = filtered.length;
|
||||||
|
const totalPages = Math.max(1, Math.ceil(totalFiltered / pageSize));
|
||||||
|
const safePage = Math.min(Math.max(0, page), totalPages - 1);
|
||||||
|
if (safePage !== page) page = safePage;
|
||||||
|
const start = safePage * pageSize;
|
||||||
|
const slice = filtered.slice(start, start + pageSize);
|
||||||
|
|
||||||
|
const pageLabel = `Página ${totalFiltered === 0 ? 0 : safePage + 1}/${totalPages}`;
|
||||||
|
const statsLine = `Total: **${baseOptions.length}** • Coincidencias: **${totalFiltered}**\n${pageLabel}`;
|
||||||
|
const filterLine = filter ? `\nFiltro activo: \`${filter}\`` : '';
|
||||||
|
const hintLine = config.filterHint ? `\n${config.filterHint}` : '';
|
||||||
|
|
||||||
|
const display = {
|
||||||
|
type: 17,
|
||||||
|
accent_color: accentColor,
|
||||||
|
components: [
|
||||||
|
{ type: 10, content: `# ${config.title}` },
|
||||||
|
{ type: 14, divider: true },
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: `${statsLine}${filterLine}${hintLine}`,
|
||||||
|
},
|
||||||
|
{ type: 14, divider: true },
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: totalFiltered === 0
|
||||||
|
? 'No hay resultados para el filtro actual. Ajusta el filtro o limpia la búsqueda.'
|
||||||
|
: 'Selecciona una opción del menú desplegable para continuar.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let options = slice.map(({ option }) => ({
|
||||||
|
label: option.label.slice(0, 100),
|
||||||
|
value: option.value,
|
||||||
|
description: option.description?.slice(0, 100),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const selectDisabled = options.length === 0;
|
||||||
|
if (selectDisabled) {
|
||||||
|
options = [
|
||||||
|
{
|
||||||
|
label: 'Sin resultados',
|
||||||
|
value: `${config.customIdPrefix}_empty`,
|
||||||
|
description: 'Ajusta el filtro para ver opciones.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectRow = {
|
||||||
|
type: 1,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
custom_id: `${config.customIdPrefix}_select`,
|
||||||
|
placeholder,
|
||||||
|
options,
|
||||||
|
disabled: selectDisabled,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const navRow = {
|
||||||
|
type: 1,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: '◀️',
|
||||||
|
custom_id: `${config.customIdPrefix}_prev`,
|
||||||
|
disabled: safePage <= 0 || totalFiltered === 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: '▶️',
|
||||||
|
custom_id: `${config.customIdPrefix}_next`,
|
||||||
|
disabled: safePage >= totalPages - 1 || totalFiltered === 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
style: ButtonStyle.Primary,
|
||||||
|
label: '🔎 Filtro',
|
||||||
|
custom_id: `${config.customIdPrefix}_filter`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
style: ButtonStyle.Secondary,
|
||||||
|
label: 'Limpiar',
|
||||||
|
custom_id: `${config.customIdPrefix}_clear`,
|
||||||
|
disabled: filter.length === 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
style: ButtonStyle.Danger,
|
||||||
|
label: 'Cancelar',
|
||||||
|
custom_id: `${config.customIdPrefix}_cancel`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return [display, selectRow, navRow];
|
||||||
|
};
|
||||||
|
|
||||||
|
const panelMessage: Message = await (channel.send as any)({
|
||||||
|
content: null,
|
||||||
|
flags: 32768,
|
||||||
|
reply: { messageReference: message.id },
|
||||||
|
components: buildComponents(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let resolved = false;
|
||||||
|
|
||||||
|
const result = await new Promise<KeyPickerResult<T>>((resolve) => {
|
||||||
|
const finish = (entry: T | null, reason: 'selected' | 'cancelled' | 'timeout') => {
|
||||||
|
if (resolved) return;
|
||||||
|
resolved = true;
|
||||||
|
resolve({ entry, panelMessage, reason });
|
||||||
|
};
|
||||||
|
|
||||||
|
const collector = panelMessage.createMessageComponentCollector({
|
||||||
|
time: 5 * 60_000,
|
||||||
|
filter: (i: MessageComponentInteraction) => i.user.id === userId && i.customId.startsWith(config.customIdPrefix),
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on('collect', async (interaction: MessageComponentInteraction) => {
|
||||||
|
try {
|
||||||
|
if (interaction.customId === `${config.customIdPrefix}_select` && interaction.isStringSelectMenu()) {
|
||||||
|
const select = interaction as StringSelectMenuInteraction;
|
||||||
|
const value = select.values?.[0];
|
||||||
|
const selected = baseOptions.find((opt) => opt.option.value === value);
|
||||||
|
if (!selected) {
|
||||||
|
await select.reply({ content: '❌ Opción no válida.', flags: MessageFlags.Ephemeral });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await select.update({
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 17,
|
||||||
|
accent_color: accentColor,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: `⏳ Cargando **${selected.option.label}**…`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
collector.stop('selected');
|
||||||
|
finish(selected.entry, 'selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interaction.customId === `${config.customIdPrefix}_prev` && interaction.isButton()) {
|
||||||
|
if (page > 0) page -= 1;
|
||||||
|
await interaction.update({ components: buildComponents() });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interaction.customId === `${config.customIdPrefix}_next` && interaction.isButton()) {
|
||||||
|
page += 1;
|
||||||
|
await interaction.update({ components: buildComponents() });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interaction.customId === `${config.customIdPrefix}_clear` && interaction.isButton()) {
|
||||||
|
filter = '';
|
||||||
|
page = 0;
|
||||||
|
await interaction.update({ components: buildComponents() });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interaction.customId === `${config.customIdPrefix}_cancel` && interaction.isButton()) {
|
||||||
|
await interaction.update({
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 17,
|
||||||
|
accent_color: 0xFF0000,
|
||||||
|
components: [
|
||||||
|
{ type: 10, content: '❌ Selección cancelada.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
collector.stop('cancelled');
|
||||||
|
finish(null, 'cancelled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interaction.customId === `${config.customIdPrefix}_filter` && interaction.isButton()) {
|
||||||
|
const modal = {
|
||||||
|
title: 'Filtrar lista',
|
||||||
|
customId: `${config.customIdPrefix}_filter_modal`,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: ComponentType.Label,
|
||||||
|
label: 'Texto a buscar',
|
||||||
|
component: {
|
||||||
|
type: ComponentType.TextInput,
|
||||||
|
customId: 'query',
|
||||||
|
style: TextInputStyle.Short,
|
||||||
|
required: false,
|
||||||
|
value: filter,
|
||||||
|
placeholder: 'Nombre, key, categoría…',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
await (interaction as ButtonInteraction).showModal(modal);
|
||||||
|
let submitted: ModalSubmitInteraction | undefined;
|
||||||
|
try {
|
||||||
|
submitted = await interaction.awaitModalSubmit({
|
||||||
|
time: 120_000,
|
||||||
|
filter: (sub) => sub.user.id === userId && sub.customId === `${config.customIdPrefix}_filter_modal`,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const value = submitted.components.getTextInputValue('query')?.trim() ?? '';
|
||||||
|
filter = value;
|
||||||
|
page = 0;
|
||||||
|
await submitted.deferUpdate();
|
||||||
|
await panelMessage.edit({ components: buildComponents() });
|
||||||
|
} catch {
|
||||||
|
// ignore errors updating filter
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!interaction.deferred && !interaction.replied) {
|
||||||
|
await interaction.reply({ content: '❌ Error procesando la selección.', flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
collector.on('end', async (_collected, reason) => {
|
||||||
|
if (resolved) return;
|
||||||
|
resolved = true;
|
||||||
|
const expiredPanel = {
|
||||||
|
type: 17,
|
||||||
|
accent_color: 0xFFA500,
|
||||||
|
components: [
|
||||||
|
{ type: 10, content: '⏰ Selección expirada.' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await panelMessage.edit({ components: [expiredPanel] });
|
||||||
|
} catch {}
|
||||||
|
const mappedReason: 'selected' | 'cancelled' | 'timeout' = reason === 'cancelled' ? 'cancelled' : 'timeout';
|
||||||
|
resolve({ entry: null, panelMessage, reason: mappedReason });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendDisplayReply(message: Message, display: any, extraComponents: any[] = []) {
|
||||||
|
const channel = message.channel as TextBasedChannel & { send: Function };
|
||||||
|
return (channel.send as any)({
|
||||||
|
content: null,
|
||||||
|
flags: 32768,
|
||||||
|
reply: { messageReference: message.id },
|
||||||
|
components: [display, ...extraComponents],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
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 { openChestByKey } from '../../../game/economy/service';
|
import { openChestByKey } from '../../../game/economy/service';
|
||||||
|
import { prisma } from '../../../core/database/prisma';
|
||||||
|
import { fetchItemBasics, formatItemLabel } from './_helpers';
|
||||||
|
import type { ItemBasicInfo } from './_helpers';
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'abrir',
|
name: 'abrir',
|
||||||
@@ -12,10 +15,50 @@ export const command: CommandMessage = {
|
|||||||
run: async (message, args, _client: Amayo) => {
|
run: async (message, args, _client: Amayo) => {
|
||||||
const itemKey = args[0]?.trim();
|
const itemKey = args[0]?.trim();
|
||||||
if (!itemKey) { await message.reply('Uso: `!abrir <itemKey>`'); return; }
|
if (!itemKey) { await message.reply('Uso: `!abrir <itemKey>`'); return; }
|
||||||
|
const userId = message.author.id;
|
||||||
|
const guildId = message.guild!.id;
|
||||||
try {
|
try {
|
||||||
const res = await openChestByKey(message.author.id, message.guild!.id, itemKey);
|
const res = await openChestByKey(userId, guildId, itemKey);
|
||||||
|
|
||||||
|
const keyRewards = res.itemsToAdd
|
||||||
|
.map((it) => it.itemKey)
|
||||||
|
.filter((key): key is string => typeof key === 'string' && key.trim().length > 0);
|
||||||
|
|
||||||
|
const basicsKeys = [itemKey, ...keyRewards].filter((key): key is string => typeof key === 'string' && key.trim().length > 0);
|
||||||
|
const infoMap = basicsKeys.length > 0 ? await fetchItemBasics(guildId, basicsKeys) : new Map<string, ItemBasicInfo>();
|
||||||
|
|
||||||
|
const idRewards = res.itemsToAdd
|
||||||
|
.filter((it) => !it.itemKey && it.itemId)
|
||||||
|
.map((it) => it.itemId!)
|
||||||
|
.filter((id, idx, arr) => arr.indexOf(id) === idx);
|
||||||
|
const itemsById = new Map<string, ItemBasicInfo>();
|
||||||
|
if (idRewards.length) {
|
||||||
|
const rows = await prisma.economyItem.findMany({
|
||||||
|
where: { id: { in: idRewards } },
|
||||||
|
select: { id: true, key: true, name: true, icon: true },
|
||||||
|
});
|
||||||
|
for (const row of rows) {
|
||||||
|
const info = { key: row.key, name: row.name, icon: row.icon };
|
||||||
|
itemsById.set(row.id, info);
|
||||||
|
if (!infoMap.has(row.key)) infoMap.set(row.key, info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chestLabel = formatItemLabel(infoMap.get(itemKey) ?? { key: itemKey, name: null, icon: null }, { bold: true });
|
||||||
const coins = res.coinsDelta ? `🪙 +${res.coinsDelta}` : '';
|
const coins = res.coinsDelta ? `🪙 +${res.coinsDelta}` : '';
|
||||||
const items = res.itemsToAdd.length ? res.itemsToAdd.map(i => `${i.itemKey ?? i.itemId} x${i.qty}`).join(' · ') : '';
|
const items = res.itemsToAdd.length
|
||||||
|
? res.itemsToAdd.map((i) => {
|
||||||
|
const info = i.itemKey
|
||||||
|
? infoMap.get(i.itemKey)
|
||||||
|
: i.itemId
|
||||||
|
? itemsById.get(i.itemId)
|
||||||
|
: null;
|
||||||
|
const label = info
|
||||||
|
? formatItemLabel(info)
|
||||||
|
: formatItemLabel({ key: i.itemKey ?? (i.itemId ?? 'item'), name: null, icon: null });
|
||||||
|
return `${label} x${i.qty}`;
|
||||||
|
}).join(' · ')
|
||||||
|
: '';
|
||||||
let rolesGiven: string[] = [];
|
let rolesGiven: string[] = [];
|
||||||
let rolesFailed: string[] = [];
|
let rolesFailed: string[] = [];
|
||||||
if (res.rolesToGrant.length && message.member) {
|
if (res.rolesToGrant.length && message.member) {
|
||||||
@@ -24,7 +67,7 @@ export const command: CommandMessage = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const lines = [
|
const lines = [
|
||||||
`🎁 Abriste ${itemKey}${res.consumed ? ' (consumido 1)' : ''}`,
|
`🎁 Abriste ${chestLabel}${res.consumed ? ' (consumido 1)' : ''}`,
|
||||||
coins && `Monedas: ${coins}`,
|
coins && `Monedas: ${coins}`,
|
||||||
items && `Ítems: ${items}`,
|
items && `Ítems: ${items}`,
|
||||||
rolesGiven.length ? `Roles otorgados: ${rolesGiven.map(id=>`<@&${id}>`).join(', ')}` : '',
|
rolesGiven.length ? `Roles otorgados: ${rolesGiven.map(id=>`<@&${id}>`).join(', ')}` : '',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { hasManageGuildOrStaff } from '../../../core/lib/permissions';
|
|||||||
import { prisma } from '../../../core/database/prisma';
|
import { prisma } from '../../../core/database/prisma';
|
||||||
import { Message, MessageComponentInteraction, MessageFlags, ButtonInteraction, TextBasedChannel } from 'discord.js';
|
import { Message, MessageComponentInteraction, MessageFlags, ButtonInteraction, TextBasedChannel } from 'discord.js';
|
||||||
import { ComponentType, TextInputStyle, ButtonStyle } from 'discord-api-types/v10';
|
import { ComponentType, TextInputStyle, ButtonStyle } from 'discord-api-types/v10';
|
||||||
|
import { promptKeySelection } from './_helpers';
|
||||||
|
|
||||||
interface AreaState {
|
interface AreaState {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -74,7 +75,7 @@ export const command: CommandMessage = {
|
|||||||
aliases: ['editar-area','areaedit'],
|
aliases: ['editar-area','areaedit'],
|
||||||
cooldown: 10,
|
cooldown: 10,
|
||||||
description: 'Edita una GameArea de este servidor con un editor interactivo.',
|
description: 'Edita una GameArea de este servidor con un editor interactivo.',
|
||||||
usage: 'area-editar <key-única>',
|
usage: 'area-editar',
|
||||||
run: async (message, args, _client: Amayo) => {
|
run: async (message, args, _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, prisma);
|
const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, prisma);
|
||||||
@@ -95,50 +96,35 @@ export const command: CommandMessage = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = args[0]?.trim();
|
|
||||||
if (!key) {
|
|
||||||
await (channel.send as any)({
|
|
||||||
content: null,
|
|
||||||
flags: 32768,
|
|
||||||
components: [{
|
|
||||||
type: 17,
|
|
||||||
accent_color: 0xFFA500,
|
|
||||||
components: [{
|
|
||||||
type: 10,
|
|
||||||
content: '⚠️ **Uso Incorrecto**\n└ Uso: `!area-editar <key-única>`'
|
|
||||||
}]
|
|
||||||
}],
|
|
||||||
reply: { messageReference: message.id }
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const guildId = message.guild!.id;
|
const guildId = message.guild!.id;
|
||||||
const area = await prisma.gameArea.findFirst({ where: { key, guildId } });
|
const areas = await prisma.gameArea.findMany({ where: { guildId }, orderBy: [{ key: 'asc' }] });
|
||||||
if (!area) {
|
const selection = await promptKeySelection(message, {
|
||||||
await (channel.send as any)({
|
entries: areas,
|
||||||
content: null,
|
customIdPrefix: 'area_edit',
|
||||||
flags: 32768,
|
title: 'Selecciona un área para editar',
|
||||||
components: [{
|
emptyText: '⚠️ **No hay áreas configuradas.** Usa `!area-crear` para crear una nueva.',
|
||||||
type: 17,
|
placeholder: 'Elige un área…',
|
||||||
accent_color: 0xFF0000,
|
filterHint: 'Puedes filtrar por nombre, key o tipo.',
|
||||||
components: [{
|
getOption: (area) => ({
|
||||||
type: 10,
|
value: area.id,
|
||||||
content: '❌ **Área No Encontrada**\n└ No existe un área con esa key en este servidor.'
|
label: `${area.name ?? area.key} (${area.type})`,
|
||||||
}]
|
description: area.key,
|
||||||
}],
|
keywords: [area.key, area.name ?? '', area.type ?? ''],
|
||||||
reply: { messageReference: message.id }
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!selection.entry || !selection.panelMessage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state: AreaState = { key, name: area.name, type: area.type, config: area.config ?? {}, metadata: area.metadata ?? {} };
|
const area = selection.entry;
|
||||||
|
const state: AreaState = { key: area.key, name: area.name, type: area.type, config: area.config ?? {}, metadata: area.metadata ?? {} };
|
||||||
|
|
||||||
const editorMsg = await (channel.send as any)({
|
const editorMsg = selection.panelMessage;
|
||||||
|
await editorMsg.edit({
|
||||||
content: null,
|
content: null,
|
||||||
flags: 32768,
|
flags: 32768,
|
||||||
components: buildEditorComponents(state, true),
|
components: buildEditorComponents(state, true),
|
||||||
reply: { messageReference: message.id }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i)=> i.user.id === message.author.id });
|
const collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i)=> i.user.id === message.author.id });
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 { useConsumableByKey } from '../../../game/consumables/service';
|
import { useConsumableByKey } from '../../../game/consumables/service';
|
||||||
|
import { fetchItemBasics, formatItemLabel } from './_helpers';
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'comer',
|
name: 'comer',
|
||||||
@@ -12,11 +13,19 @@ export const command: CommandMessage = {
|
|||||||
run: async (message, args, _client: Amayo) => {
|
run: async (message, args, _client: Amayo) => {
|
||||||
const itemKey = args[0]?.trim();
|
const itemKey = args[0]?.trim();
|
||||||
if (!itemKey) { await message.reply('Uso: `!comer <itemKey>`'); return; }
|
if (!itemKey) { await message.reply('Uso: `!comer <itemKey>`'); return; }
|
||||||
|
const guildId = message.guild!.id;
|
||||||
|
const userId = message.author.id;
|
||||||
|
let itemInfo: { key: string; name: string | null; icon: string | null } = { key: itemKey, name: null, icon: null };
|
||||||
try {
|
try {
|
||||||
const res = await useConsumableByKey(message.author.id, message.guild!.id, itemKey);
|
const basics = await fetchItemBasics(guildId, [itemKey]);
|
||||||
await message.reply(`🍽️ Usaste ${itemKey}. Curado: +${res.healed} HP.`);
|
itemInfo = basics.get(itemKey) ?? itemInfo;
|
||||||
|
|
||||||
|
const res = await useConsumableByKey(userId, guildId, itemKey);
|
||||||
|
const label = formatItemLabel(itemInfo, { bold: true });
|
||||||
|
await message.reply(`🍽️ Usaste ${label}. Curado: +${res.healed} HP.`);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
await message.reply(`❌ No se pudo usar ${itemKey}: ${e?.message ?? e}`);
|
const label = formatItemLabel(itemInfo, { bold: true });
|
||||||
|
await message.reply(`❌ No se pudo usar ${label}: ${e?.message ?? e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 { buyFromOffer } from '../../../game/economy/service';
|
import { buyFromOffer } from '../../../game/economy/service';
|
||||||
|
import { formatItemLabel } from './_helpers';
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'comprar',
|
name: 'comprar',
|
||||||
@@ -15,7 +16,8 @@ export const command: CommandMessage = {
|
|||||||
if (!offerId) { await message.reply('Uso: `!comprar <offerId> [qty]`'); return; }
|
if (!offerId) { await message.reply('Uso: `!comprar <offerId> [qty]`'); return; }
|
||||||
try {
|
try {
|
||||||
const res = await buyFromOffer(message.author.id, message.guild!.id, offerId, qty);
|
const res = await buyFromOffer(message.author.id, message.guild!.id, offerId, qty);
|
||||||
await message.reply(`🛒 Comprado: ${res.item.key} x${res.qty}`);
|
const label = formatItemLabel(res.item, { bold: true });
|
||||||
|
await message.reply(`🛒 Comprado: ${label} x${res.qty}`);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
await message.reply(`❌ No se pudo comprar: ${e?.message ?? e}`);
|
await message.reply(`❌ No se pudo comprar: ${e?.message ?? e}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 { craftByProductKey } from '../../../game/economy/service';
|
import { craftByProductKey } from '../../../game/economy/service';
|
||||||
|
import { fetchItemBasics, formatItemLabel } from './_helpers';
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'craftear',
|
name: 'craftear',
|
||||||
@@ -14,21 +15,33 @@ export const command: CommandMessage = {
|
|||||||
const times = Math.max(1, parseInt(args[1] || '1', 10) || 1);
|
const times = Math.max(1, parseInt(args[1] || '1', 10) || 1);
|
||||||
if (!productKey) { await message.reply('Uso: `!craftear <productKey> [veces]`'); return; }
|
if (!productKey) { await message.reply('Uso: `!craftear <productKey> [veces]`'); return; }
|
||||||
|
|
||||||
|
const guildId = message.guild!.id;
|
||||||
|
const userId = message.author.id;
|
||||||
|
let itemInfo: { key: string; name: string | null; icon: string | null } = { key: productKey, name: null, icon: null };
|
||||||
|
try {
|
||||||
|
const basics = await fetchItemBasics(guildId, [productKey]);
|
||||||
|
itemInfo = basics.get(productKey) ?? itemInfo;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('No se pudo resolver info de item para craftear', err);
|
||||||
|
}
|
||||||
|
|
||||||
let crafted = 0;
|
let crafted = 0;
|
||||||
let lastError: any = null;
|
let lastError: any = null;
|
||||||
for (let i = 0; i < times; i++) {
|
for (let i = 0; i < times; i++) {
|
||||||
try {
|
try {
|
||||||
const res = await craftByProductKey(message.author.id, message.guild!.id, productKey);
|
const res = await craftByProductKey(userId, guildId, productKey);
|
||||||
crafted += res.added;
|
crafted += res.added;
|
||||||
|
itemInfo = { key: res.product.key, name: res.product.name, icon: res.product.icon };
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
lastError = e; break;
|
lastError = e; break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const label = formatItemLabel(itemInfo, { bold: true });
|
||||||
if (crafted > 0) {
|
if (crafted > 0) {
|
||||||
await message.reply(`🛠️ Crafteado ${productKey} x${crafted * 1}.`);
|
await message.reply(`🛠️ Crafteado ${label} x${crafted}.`);
|
||||||
} else {
|
} else {
|
||||||
await message.reply(`❌ No se pudo craftear: ${lastError?.message ?? 'revise ingredientes/receta'}`);
|
await message.reply(`❌ No se pudo craftear ${label}: ${lastError?.message ?? 'revise ingredientes/receta'}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 { applyMutationToInventory } from '../../../game/mutations/service';
|
import { applyMutationToInventory } from '../../../game/mutations/service';
|
||||||
|
import { fetchItemBasics, formatItemLabel } from './_helpers';
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'encantar',
|
name: 'encantar',
|
||||||
@@ -13,11 +14,19 @@ export const command: CommandMessage = {
|
|||||||
const itemKey = args[0]?.trim();
|
const itemKey = args[0]?.trim();
|
||||||
const mutationKey = args[1]?.trim();
|
const mutationKey = args[1]?.trim();
|
||||||
if (!itemKey || !mutationKey) { await message.reply('Uso: `!encantar <itemKey> <mutationKey>`'); return; }
|
if (!itemKey || !mutationKey) { await message.reply('Uso: `!encantar <itemKey> <mutationKey>`'); return; }
|
||||||
|
const guildId = message.guild!.id;
|
||||||
|
const userId = message.author.id;
|
||||||
|
let itemInfo: { key: string; name: string | null; icon: string | null } = { key: itemKey, name: null, icon: null };
|
||||||
try {
|
try {
|
||||||
await applyMutationToInventory(message.author.id, message.guild!.id, itemKey, mutationKey);
|
const basics = await fetchItemBasics(guildId, [itemKey]);
|
||||||
await message.reply(`✨ Aplicada mutación ${mutationKey} a ${itemKey}.`);
|
itemInfo = basics.get(itemKey) ?? itemInfo;
|
||||||
|
|
||||||
|
await applyMutationToInventory(userId, guildId, itemKey, mutationKey);
|
||||||
|
const label = formatItemLabel(itemInfo, { bold: true });
|
||||||
|
await message.reply(`✨ Aplicada mutación \`${mutationKey}\` a ${label}.`);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
await message.reply(`❌ No se pudo encantar: ${e?.message ?? e}`);
|
const label = formatItemLabel(itemInfo, { bold: true });
|
||||||
|
await message.reply(`❌ No se pudo encantar ${label}: ${e?.message ?? e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { CommandMessage } from '../../../core/types/commands';
|
|||||||
import type Amayo from '../../../core/client';
|
import type Amayo from '../../../core/client';
|
||||||
import { setEquipmentSlot } from '../../../game/combat/equipmentService';
|
import { setEquipmentSlot } from '../../../game/combat/equipmentService';
|
||||||
import { prisma } from '../../../core/database/prisma';
|
import { prisma } from '../../../core/database/prisma';
|
||||||
|
import { formatItemLabel } from './_helpers';
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'equipar',
|
name: 'equipar',
|
||||||
@@ -22,11 +23,12 @@ export const command: CommandMessage = {
|
|||||||
|
|
||||||
const item = await prisma.economyItem.findFirst({ where: { key: itemKey, OR: [{ guildId }, { guildId: null }] }, orderBy: [{ guildId: 'desc' }] });
|
const item = await prisma.economyItem.findFirst({ where: { key: itemKey, OR: [{ guildId }, { guildId: null }] }, orderBy: [{ guildId: 'desc' }] });
|
||||||
if (!item) { await message.reply('❌ Item no encontrado.'); return; }
|
if (!item) { await message.reply('❌ Item no encontrado.'); return; }
|
||||||
|
const label = formatItemLabel(item, { bold: true });
|
||||||
const inv = await prisma.inventoryEntry.findUnique({ where: { userId_guildId_itemId: { userId, guildId, itemId: item.id } } });
|
const inv = await prisma.inventoryEntry.findUnique({ where: { userId_guildId_itemId: { userId, guildId, itemId: item.id } } });
|
||||||
if (!inv || inv.quantity <= 0) { await message.reply('❌ No tienes este item en tu inventario.'); return; }
|
if (!inv || inv.quantity <= 0) { await message.reply(`❌ No tienes ${label} en tu inventario.`); return; }
|
||||||
|
|
||||||
await setEquipmentSlot(userId, guildId, slot, item.id);
|
await setEquipmentSlot(userId, guildId, slot, item.id);
|
||||||
await message.reply(`🧰 Equipado en ${slot}: ${item.key}`);
|
await message.reply(`🧰 Equipado en ${slot}: ${label}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { prisma } from '../../../core/database/prisma';
|
|||||||
import { getOrCreateWallet } from '../../../game/economy/service';
|
import { getOrCreateWallet } from '../../../game/economy/service';
|
||||||
import { getEquipment, getEffectiveStats } from '../../../game/combat/equipmentService';
|
import { getEquipment, getEffectiveStats } from '../../../game/combat/equipmentService';
|
||||||
import type { ItemProps } from '../../../game/economy/types';
|
import type { ItemProps } from '../../../game/economy/types';
|
||||||
|
import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2';
|
||||||
|
import { sendDisplayReply, formatItemLabel } from './_helpers';
|
||||||
|
|
||||||
const PAGE_SIZE = 15;
|
const PAGE_SIZE = 15;
|
||||||
|
|
||||||
@@ -28,6 +30,8 @@ function fmtStats(props: ItemProps) {
|
|||||||
return parts.length ? ` (${parts.join(' ')})` : '';
|
return parts.length ? ` (${parts.join(' ')})` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const INVENTORY_ACCENT = 0xFEE75C;
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'inventario',
|
name: 'inventario',
|
||||||
type: 'message',
|
type: 'message',
|
||||||
@@ -60,15 +64,23 @@ export const command: CommandMessage = {
|
|||||||
const tool = fmtTool(props);
|
const tool = fmtTool(props);
|
||||||
const st = fmtStats(props);
|
const st = fmtStats(props);
|
||||||
const tags = (itemRow.tags || []).join(', ');
|
const tags = (itemRow.tags || []).join(', ');
|
||||||
await message.reply([
|
const detailLines = [
|
||||||
`📦 ${itemRow.name || itemRow.key} — x${qty}`,
|
`**Cantidad:** x${qty}`,
|
||||||
`Key: ${itemRow.key}`,
|
`**Key:** \`${itemRow.key}\``,
|
||||||
itemRow.category ? `Categoría: ${itemRow.category}` : '',
|
itemRow.category ? `**Categoría:** ${itemRow.category}` : '',
|
||||||
tags ? `Tags: ${tags}` : '',
|
tags ? `**Tags:** ${tags}` : '',
|
||||||
tool ? `Herramienta: ${tool}` : '',
|
tool ? `**Herramienta:** ${tool}` : '',
|
||||||
st ? `Bonos: ${st}` : '',
|
st ? `**Bonos:** ${st}` : '',
|
||||||
props.craftingOnly ? 'Solo crafteo' : '',
|
props.craftingOnly ? '⚠️ Solo crafteo' : '',
|
||||||
].filter(Boolean).join('\n'));
|
].filter(Boolean).join('\n');
|
||||||
|
|
||||||
|
const display = buildDisplay(INVENTORY_ACCENT, [
|
||||||
|
textBlock(`# ${formatItemLabel(itemRow, { bold: true })}`),
|
||||||
|
dividerBlock(),
|
||||||
|
textBlock(detailLines || '*Sin información adicional.*'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await sendDisplayReply(message, display);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,37 +100,56 @@ export const command: CommandMessage = {
|
|||||||
.sort((a, b) => (b.quantity - a.quantity) || a.item.key.localeCompare(b.item.key))
|
.sort((a, b) => (b.quantity - a.quantity) || a.item.key.localeCompare(b.item.key))
|
||||||
.slice(start, start + PAGE_SIZE);
|
.slice(start, start + PAGE_SIZE);
|
||||||
|
|
||||||
const lines: string[] = [];
|
|
||||||
// header con saldo y equipo
|
|
||||||
lines.push(`💰 Monedas: ${wallet.coins}`);
|
|
||||||
const gear: string[] = [];
|
const gear: string[] = [];
|
||||||
if (weapon) gear.push(`🗡️ ${weapon.key}`);
|
if (weapon) gear.push(`🗡️ ${formatItemLabel(weapon, { fallbackIcon: '' })}`);
|
||||||
if (armor) gear.push(`🛡️ ${armor.key}`);
|
if (armor) gear.push(`🛡️ ${formatItemLabel(armor, { fallbackIcon: '' })}`);
|
||||||
if (cape) gear.push(`🧥 ${cape.key}`);
|
if (cape) gear.push(`🧥 ${formatItemLabel(cape, { fallbackIcon: '' })}`);
|
||||||
if (gear.length) lines.push(`🧰 Equipo: ${gear.join(' · ')}`);
|
const headerLines = [
|
||||||
lines.push(`❤️ HP: ${stats.hp}/${stats.maxHp} · ⚔️ ATK: ${stats.damage} · 🛡️ DEF: ${stats.defense}`);
|
`💰 Monedas: **${wallet.coins}**`,
|
||||||
|
gear.length ? `🧰 Equipo: ${gear.join(' · ')}` : '',
|
||||||
|
`❤️ HP: ${stats.hp}/${stats.maxHp} · ⚔️ ATK: ${stats.damage} · 🛡️ DEF: ${stats.defense}`,
|
||||||
|
filter ? `🔍 Filtro: ${filter}` : '',
|
||||||
|
].filter(Boolean).join('\n');
|
||||||
|
|
||||||
|
const blocks = [
|
||||||
|
textBlock('# 📦 Inventario'),
|
||||||
|
dividerBlock(),
|
||||||
|
textBlock(headerLines),
|
||||||
|
];
|
||||||
|
|
||||||
if (!pageItems.length) {
|
if (!pageItems.length) {
|
||||||
lines.push(filter ? `No hay ítems que coincidan con "${filter}".` : 'No tienes ítems en tu inventario.');
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
await message.reply(lines.join('\n'));
|
blocks.push(textBlock(filter ? `No hay ítems que coincidan con "${filter}".` : 'No tienes ítems en tu inventario.'));
|
||||||
|
const display = buildDisplay(INVENTORY_ACCENT, blocks);
|
||||||
|
await sendDisplayReply(message, display);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push(`\n📦 Inventario (página ${page}/${totalPages}${filter ? `, filtro: ${filter}` : ''})`);
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
|
blocks.push(textBlock(`📦 Inventario (página ${page}/${totalPages}${filter ? `, filtro: ${filter}` : ''})`));
|
||||||
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
|
|
||||||
for (const e of pageItems) {
|
pageItems.forEach((entry, index) => {
|
||||||
const p = parseItemProps(e.item.props);
|
const props = parseItemProps(entry.item.props);
|
||||||
const tool = fmtTool(p);
|
const tool = fmtTool(props);
|
||||||
const st = fmtStats(p);
|
const st = fmtStats(props);
|
||||||
const name = e.item.name || e.item.key;
|
const label = formatItemLabel(entry.item);
|
||||||
lines.push(`• ${name} — x${e.quantity}${tool ? ` ${tool}` : ''}${st}`);
|
blocks.push(textBlock(`• ${label} — x${entry.quantity}${tool ? ` ${tool}` : ''}${st}`));
|
||||||
|
if (index < pageItems.length - 1) {
|
||||||
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (totalPages > 1) {
|
if (totalPages > 1) {
|
||||||
lines.push(`\nUsa: \`!inv ${filter ? `${page+1} ${filter}` : page+1}\` para la siguiente página.`);
|
const nextPage = Math.min(page + 1, totalPages);
|
||||||
|
const nextCommand = filter ? `!inv ${nextPage} ${filter}` : `!inv ${nextPage}`;
|
||||||
|
const backtick = '`';
|
||||||
|
blocks.push(dividerBlock({ divider: false, spacing: 2 }));
|
||||||
|
blocks.push(textBlock(`💡 Usa ${backtick}${nextCommand}${backtick} para la siguiente página.`));
|
||||||
}
|
}
|
||||||
|
|
||||||
await message.reply(lines.join('\n'));
|
const display = buildDisplay(INVENTORY_ACCENT, blocks);
|
||||||
|
await sendDisplayReply(message, display);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { CommandMessage } from '../../../core/types/commands';
|
|||||||
import { hasManageGuildOrStaff } from '../../../core/lib/permissions';
|
import { hasManageGuildOrStaff } from '../../../core/lib/permissions';
|
||||||
import logger from '../../../core/lib/logger';
|
import logger from '../../../core/lib/logger';
|
||||||
import type Amayo from '../../../core/client';
|
import type Amayo from '../../../core/client';
|
||||||
|
import { promptKeySelection, resolveItemIcon } from './_helpers';
|
||||||
|
|
||||||
interface ItemEditorState {
|
interface ItemEditorState {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -24,8 +25,8 @@ export const command: CommandMessage = {
|
|||||||
cooldown: 10,
|
cooldown: 10,
|
||||||
description: 'Edita un EconomyItem existente del servidor con un pequeño editor interactivo.',
|
description: 'Edita un EconomyItem existente del servidor con un pequeño editor interactivo.',
|
||||||
category: 'Economía',
|
category: 'Economía',
|
||||||
usage: 'item-editar <key-única>',
|
usage: 'item-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) {
|
||||||
@@ -45,53 +46,43 @@ export const command: CommandMessage = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = args[0]?.trim();
|
|
||||||
if (!key) {
|
|
||||||
await (channel.send as any)({
|
|
||||||
content: null,
|
|
||||||
flags: 32768,
|
|
||||||
components: [{
|
|
||||||
type: 17,
|
|
||||||
accent_color: 0xFFA500,
|
|
||||||
components: [{
|
|
||||||
type: 10,
|
|
||||||
content: '⚠️ **Uso Incorrecto**\n└ Uso: `!item-editar <key-única>`'
|
|
||||||
}]
|
|
||||||
}],
|
|
||||||
reply: { messageReference: message.id }
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const guildId = message.guild!.id;
|
const guildId = message.guild!.id;
|
||||||
|
const items = await client.prisma.economyItem.findMany({ where: { guildId }, orderBy: [{ key: 'asc' }] });
|
||||||
const existing = await client.prisma.economyItem.findFirst({ where: { key, guildId } });
|
const selection = await promptKeySelection(message, {
|
||||||
if (!existing) {
|
entries: items,
|
||||||
await (channel.send as any)({
|
customIdPrefix: 'item_edit',
|
||||||
content: null,
|
title: 'Selecciona un ítem para editar',
|
||||||
flags: 32768,
|
emptyText: '⚠️ **No hay ítems locales configurados.** Usa `!item-crear` primero.',
|
||||||
components: [{
|
placeholder: 'Elige un ítem…',
|
||||||
type: 17,
|
filterHint: 'Filtra por nombre, key, categoría o tag.',
|
||||||
accent_color: 0xFF0000,
|
getOption: (item) => {
|
||||||
components: [{
|
const icon = resolveItemIcon(item.icon);
|
||||||
type: 10,
|
const label = `${icon} ${(item.name ?? item.key)}`.trim();
|
||||||
content: '❌ **Item No Encontrado**\n└ No existe un item con esa key en este servidor.'
|
const tags = Array.isArray(item.tags) ? item.tags : [];
|
||||||
}]
|
return {
|
||||||
}],
|
value: item.id,
|
||||||
reply: { messageReference: message.id }
|
label: label.slice(0, 100),
|
||||||
|
description: item.key,
|
||||||
|
keywords: [item.key, item.name ?? '', item.category ?? '', ...tags],
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!selection.entry || !selection.panelMessage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existing = selection.entry;
|
||||||
|
|
||||||
const state: ItemEditorState = {
|
const state: ItemEditorState = {
|
||||||
key,
|
key: existing.key,
|
||||||
name: existing.name,
|
name: existing.name,
|
||||||
description: existing.description || undefined,
|
description: existing.description || undefined,
|
||||||
category: existing.category || undefined,
|
category: existing.category || undefined,
|
||||||
icon: existing.icon || undefined,
|
icon: existing.icon || undefined,
|
||||||
stackable: existing.stackable ?? true,
|
stackable: existing.stackable ?? true,
|
||||||
maxPerInventory: existing.maxPerInventory || null,
|
maxPerInventory: existing.maxPerInventory ?? null,
|
||||||
tags: existing.tags || [],
|
tags: Array.isArray(existing.tags) ? existing.tags : [],
|
||||||
props: existing.props || {},
|
props: existing.props || {},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,7 +105,7 @@ export const command: CommandMessage = {
|
|||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: `# 🛠️ Editando Item: \`${key}\``
|
content: `# 🛠️ Editando Item: \`${state.key}\``
|
||||||
},
|
},
|
||||||
{ type: 14, divider: true },
|
{ type: 14, divider: true },
|
||||||
{
|
{
|
||||||
@@ -149,11 +140,11 @@ export const command: CommandMessage = {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const editorMsg = await (channel.send as any)({
|
const editorMsg = selection.panelMessage;
|
||||||
|
await editorMsg.edit({
|
||||||
content: null,
|
content: null,
|
||||||
flags: 32768,
|
flags: 32768,
|
||||||
components: buildEditorComponents(),
|
components: buildEditorComponents(),
|
||||||
reply: { messageReference: message.id }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const collector = editorMsg.createMessageComponentCollector({ time: 30 * 60_000, filter: (i) => i.user.id === message.author.id });
|
const collector = editorMsg.createMessageComponentCollector({ time: 30 * 60_000, filter: (i) => i.user.id === message.author.id });
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
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 { runMinigame } from '../../../game/minigames/service';
|
import { runMinigame } from '../../../game/minigames/service';
|
||||||
import { getDefaultLevel, findBestToolKey, parseGameArgs, resolveGuildAreaWithFallback, resolveAreaByType } from './_helpers';
|
import { getDefaultLevel, findBestToolKey, parseGameArgs, resolveGuildAreaWithFallback, resolveAreaByType, sendDisplayReply, fetchItemBasics, formatItemLabel } from './_helpers';
|
||||||
import { updateStats } from '../../../game/stats/service';
|
import { updateStats } from '../../../game/stats/service';
|
||||||
import { updateQuestProgress } from '../../../game/quests/service';
|
import { updateQuestProgress } from '../../../game/quests/service';
|
||||||
import { checkAchievements } from '../../../game/achievements/service';
|
import { checkAchievements } from '../../../game/achievements/service';
|
||||||
|
import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2';
|
||||||
|
|
||||||
|
const MINING_ACCENT = 0xC27C0E;
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'mina',
|
name: 'mina',
|
||||||
@@ -42,6 +45,12 @@ export const command: CommandMessage = {
|
|||||||
try {
|
try {
|
||||||
const result = await runMinigame(userId, guildId, area.key, level, { toolKey: toolKey ?? undefined });
|
const result = await runMinigame(userId, guildId, area.key, level, { toolKey: toolKey ?? undefined });
|
||||||
|
|
||||||
|
const rewardKeys = result.rewards
|
||||||
|
.filter((r): r is { type: 'item'; itemKey: string; qty?: number } => r.type === 'item' && Boolean(r.itemKey))
|
||||||
|
.map((r) => r.itemKey!);
|
||||||
|
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||||
|
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||||
|
|
||||||
// Actualizar stats
|
// Actualizar stats
|
||||||
await updateStats(userId, guildId, { minesCompleted: 1 });
|
await updateStats(userId, guildId, { minesCompleted: 1 });
|
||||||
|
|
||||||
@@ -51,25 +60,44 @@ export const command: CommandMessage = {
|
|||||||
// Verificar logros
|
// Verificar logros
|
||||||
const newAchievements = await checkAchievements(userId, guildId, 'mine_count');
|
const newAchievements = await checkAchievements(userId, guildId, 'mine_count');
|
||||||
|
|
||||||
const rewards = result.rewards.map(r => r.type === 'coins' ? `🪙 +${r.amount}` : `📦 ${r.itemKey} x${r.qty}`).join(' · ') || '—';
|
const rewardLines = result.rewards.length
|
||||||
const mobs = result.mobs.length ? result.mobs.join(', ') : '—';
|
? result.rewards.map((r) => {
|
||||||
const toolInfo = result.tool?.key ? `🔧 ${result.tool.key}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta} dur.)`}` : '—';
|
if (r.type === 'coins') return `• 🪙 +${r.amount}`;
|
||||||
|
const info = rewardItems.get(r.itemKey!);
|
||||||
|
const label = formatItemLabel(info ?? { key: r.itemKey!, name: null, icon: null });
|
||||||
|
return `• ${label} x${r.qty ?? 1}`;
|
||||||
|
}).join('\n')
|
||||||
|
: '• —';
|
||||||
|
const mobsLines = result.mobs.length
|
||||||
|
? result.mobs.map(m => `• ${m}`).join('\n')
|
||||||
|
: '• —';
|
||||||
|
const toolInfo = result.tool?.key
|
||||||
|
? `${formatItemLabel(rewardItems.get(result.tool.key) ?? { key: result.tool.key, name: null, icon: null }, { fallbackIcon: '🔧' })}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta ?? 0} dur.)`}`
|
||||||
|
: '—';
|
||||||
|
|
||||||
let response = globalNotice ? `${globalNotice}\n\n` : '';
|
const blocks = [textBlock('# ⛏️ Mina')];
|
||||||
response += `⛏️ Mina (nivel ${level})
|
|
||||||
Recompensas: ${rewards}
|
if (globalNotice) {
|
||||||
Mobs: ${mobs}
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
Herramienta: ${toolInfo}`;
|
blocks.push(textBlock(globalNotice));
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks.push(dividerBlock());
|
||||||
|
const areaScope = source === 'global' ? '🌐 Configuración global' : '📍 Configuración local';
|
||||||
|
blocks.push(textBlock(`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`));
|
||||||
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
|
blocks.push(textBlock(`**Recompensas**\n${rewardLines}`));
|
||||||
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
|
blocks.push(textBlock(`**Mobs**\n${mobsLines}`));
|
||||||
|
|
||||||
// Notificar logros desbloqueados
|
|
||||||
if (newAchievements.length > 0) {
|
if (newAchievements.length > 0) {
|
||||||
response += `\n\n🏆 ¡Logro desbloqueado!`;
|
blocks.push(dividerBlock({ divider: false, spacing: 2 }));
|
||||||
for (const ach of newAchievements) {
|
const achLines = newAchievements.map(ach => `✨ **${ach.name}** — ${ach.description}`).join('\n');
|
||||||
response += `\n✨ **${ach.name}** - ${ach.description}`;
|
blocks.push(textBlock(`🏆 ¡Logro desbloqueado!\n${achLines}`));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await message.reply(response);
|
const display = buildDisplay(MINING_ACCENT, blocks);
|
||||||
|
await sendDisplayReply(message, display);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
await message.reply(`❌ No se pudo minar: ${e?.message ?? e}`);
|
await message.reply(`❌ No se pudo minar: ${e?.message ?? e}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { CommandMessage } from '../../../core/types/commands';
|
|||||||
import type Amayo from '../../../core/client';
|
import type Amayo from '../../../core/client';
|
||||||
import { claimQuestReward, getPlayerQuests } from '../../../game/quests/service';
|
import { claimQuestReward, getPlayerQuests } from '../../../game/quests/service';
|
||||||
import { EmbedBuilder } from 'discord.js';
|
import { EmbedBuilder } from 'discord.js';
|
||||||
|
import { fetchItemBasics, formatItemLabel } from './_helpers';
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'mision-reclamar',
|
name: 'mision-reclamar',
|
||||||
@@ -41,6 +42,21 @@ export const command: CommandMessage = {
|
|||||||
// Reclamar recompensa
|
// Reclamar recompensa
|
||||||
const { quest, rewards } = await claimQuestReward(userId, guildId, selected.quest.id);
|
const { quest, rewards } = await claimQuestReward(userId, guildId, selected.quest.id);
|
||||||
|
|
||||||
|
const rewardData = (quest.rewards as any) ?? {};
|
||||||
|
const formattedRewards: string[] = [];
|
||||||
|
if (rewardData.coins) formattedRewards.push(`💰 **${rewardData.coins.toLocaleString()}** monedas`);
|
||||||
|
if (rewardData.items && Array.isArray(rewardData.items) && rewardData.items.length) {
|
||||||
|
const basics = await fetchItemBasics(guildId, rewardData.items.map((item: any) => item.key));
|
||||||
|
for (const item of rewardData.items) {
|
||||||
|
const info = basics.get(item.key) ?? { key: item.key, name: null, icon: null };
|
||||||
|
const label = formatItemLabel(info, { bold: true });
|
||||||
|
formattedRewards.push(`${label} ×${item.quantity}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rewardData.xp) formattedRewards.push(`⭐ **${rewardData.xp}** XP`);
|
||||||
|
if (rewardData.title) formattedRewards.push(`🏆 Título: **${rewardData.title}**`);
|
||||||
|
const rewardsDisplay = formattedRewards.length > 0 ? formattedRewards : rewards;
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0x00FF00)
|
.setColor(0x00FF00)
|
||||||
.setTitle('🎉 ¡Misión Completada!')
|
.setTitle('🎉 ¡Misión Completada!')
|
||||||
@@ -48,10 +64,10 @@ export const command: CommandMessage = {
|
|||||||
.setThumbnail(message.author.displayAvatarURL({ size: 128 }));
|
.setThumbnail(message.author.displayAvatarURL({ size: 128 }));
|
||||||
|
|
||||||
// Mostrar recompensas
|
// Mostrar recompensas
|
||||||
if (rewards.length > 0) {
|
if (rewardsDisplay.length > 0) {
|
||||||
embed.addFields({
|
embed.addFields({
|
||||||
name: '🎁 Recompensas Recibidas',
|
name: '🎁 Recompensas Recibidas',
|
||||||
value: rewards.join('\n'),
|
value: rewardsDisplay.join('\n'),
|
||||||
inline: false
|
inline: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { CommandMessage } from '../../../core/types/commands';
|
|||||||
import { hasManageGuildOrStaff } from '../../../core/lib/permissions';
|
import { hasManageGuildOrStaff } from '../../../core/lib/permissions';
|
||||||
import logger from '../../../core/lib/logger';
|
import logger from '../../../core/lib/logger';
|
||||||
import type Amayo from '../../../core/client';
|
import type Amayo from '../../../core/client';
|
||||||
|
import { promptKeySelection } from './_helpers';
|
||||||
|
|
||||||
interface MobEditorState {
|
interface MobEditorState {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -64,112 +65,60 @@ export const command: CommandMessage = {
|
|||||||
cooldown: 10,
|
cooldown: 10,
|
||||||
description: 'Edita un Mob (enemigo) de este servidor con editor interactivo.',
|
description: 'Edita un Mob (enemigo) de este servidor con editor interactivo.',
|
||||||
category: 'Minijuegos',
|
category: 'Minijuegos',
|
||||||
usage: 'mob-editar <key-única>',
|
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 allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma);
|
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) {
|
||||||
const key = args[0]?.trim();
|
await (channel.send as any)({
|
||||||
if (!key) { await message.reply('Uso: `!mob-editar <key-única>`'); return; }
|
content: null,
|
||||||
const guildId = message.guild!.id;
|
flags: 32768,
|
||||||
|
components: [{
|
||||||
|
type: 17,
|
||||||
|
accent_color: 0xFF0000,
|
||||||
|
components: [{
|
||||||
|
type: 10,
|
||||||
|
content: '❌ **Error de Permisos**\n└ No tienes permisos de ManageGuild ni rol de staff.'
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
reply: { messageReference: message.id }
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const mob = await client.prisma.mob.findFirst({ where: { key, guildId } });
|
const guildId = message.guild!.id;
|
||||||
if (!mob) { await message.reply('❌ No existe un mob con esa key en este servidor.'); return; }
|
const mobs = await client.prisma.mob.findMany({ where: { guildId }, orderBy: [{ key: 'asc' }] });
|
||||||
|
const selection = await promptKeySelection(message, {
|
||||||
|
entries: mobs,
|
||||||
|
customIdPrefix: 'mob_edit',
|
||||||
|
title: 'Selecciona un mob para editar',
|
||||||
|
emptyText: '⚠️ **No hay mobs configurados.** Usa `!mob-crear` primero.',
|
||||||
|
placeholder: 'Elige un mob…',
|
||||||
|
filterHint: 'Filtra por nombre, key o categoría.',
|
||||||
|
getOption: (mob) => ({
|
||||||
|
value: mob.id,
|
||||||
|
label: mob.name ?? mob.key,
|
||||||
|
description: [mob.category ?? 'Sin categoría', mob.key].filter(Boolean).join(' • '),
|
||||||
|
keywords: [mob.key, mob.name ?? '', mob.category ?? ''],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!selection.entry || !selection.panelMessage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mob = selection.entry;
|
||||||
|
|
||||||
const state: MobEditorState = {
|
const state: MobEditorState = {
|
||||||
key,
|
key: mob.key,
|
||||||
name: mob.name,
|
name: mob.name,
|
||||||
category: mob.category ?? undefined,
|
category: mob.category ?? undefined,
|
||||||
stats: mob.stats ?? {},
|
stats: mob.stats ?? {},
|
||||||
drops: mob.drops ?? {},
|
drops: mob.drops ?? {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const channel = message.channel as TextBasedChannel & { send: Function };
|
const buildEditorComponents = () => [
|
||||||
const editorMsg = await channel.send({
|
createMobDisplay(state, true),
|
||||||
content: `👾 Editor de Mob (editar): \`${key}\``,
|
|
||||||
components: [ { type: 1, components: [
|
|
||||||
{ type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'mb_base' },
|
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Stats (JSON)', custom_id: 'mb_stats' },
|
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Drops (JSON)', custom_id: 'mb_drops' },
|
|
||||||
{ type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'mb_save' },
|
|
||||||
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'mb_cancel' },
|
|
||||||
] } ],
|
|
||||||
});
|
|
||||||
|
|
||||||
const collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i)=> i.user.id === message.author.id });
|
|
||||||
collector.on('collect', async (i: MessageComponentInteraction) => {
|
|
||||||
try {
|
|
||||||
if (!i.isButton()) return;
|
|
||||||
if (i.customId === 'mb_cancel') {
|
|
||||||
await i.deferUpdate();
|
|
||||||
await editorMsg.edit({
|
|
||||||
flags: 32768,
|
|
||||||
components: [{
|
|
||||||
type: 17,
|
|
||||||
accent_color: 0xFF0000,
|
|
||||||
components: [{
|
|
||||||
type: 9,
|
|
||||||
components: [{
|
|
||||||
type: 10,
|
|
||||||
content: '**❌ Editor cancelado.**'
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
collector.stop('cancel');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (i.customId === 'mb_base') { await showBaseModal(i as ButtonInteraction, state, editorMsg, true); return; }
|
|
||||||
if (i.customId === 'mb_stats') { await showJsonModal(i as ButtonInteraction, state, 'stats', 'Stats del Mob (JSON)', editorMsg, true); return; }
|
|
||||||
if (i.customId === 'mb_drops') { await showJsonModal(i as ButtonInteraction, state, 'drops', 'Drops del Mob (JSON)', editorMsg, true); return; }
|
|
||||||
if (i.customId === 'mb_save') {
|
|
||||||
if (!state.name) { await i.reply({ content: '❌ Falta el nombre del mob.', flags: MessageFlags.Ephemeral }); return; }
|
|
||||||
await client.prisma.mob.update({ where: { id: mob.id }, data: { name: state.name!, category: state.category ?? null, stats: state.stats ?? {}, drops: state.drops ?? {} } });
|
|
||||||
await i.reply({ content: '✅ Mob actualizado!', flags: MessageFlags.Ephemeral });
|
|
||||||
await editorMsg.edit({
|
|
||||||
flags: 32768,
|
|
||||||
components: [{
|
|
||||||
type: 17,
|
|
||||||
accent_color: 0x00FF00,
|
|
||||||
components: [{
|
|
||||||
type: 9,
|
|
||||||
components: [{
|
|
||||||
type: 10,
|
|
||||||
content: `**✅ Mob \`${state.key}\` actualizado exitosamente.**`
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
collector.stop('saved');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error({err}, 'mob-editar');
|
|
||||||
if (!i.deferred && !i.replied) await i.reply({ content: '❌ Error procesando la acción.', flags: MessageFlags.Ephemeral });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
collector.on('end', async (_c,r)=> { if (r==='time') { try { await editorMsg.edit({ content:'⏰ Editor expirado.', components: [] }); } catch {} } });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
async function showBaseModal(i: ButtonInteraction, state: MobEditorState, editorMsg: Message, editing: boolean) {
|
|
||||||
const modal = { title: 'Base del Mob', customId: 'mb_base_modal', components: [
|
|
||||||
{ type: ComponentType.Label, label: 'Nombre', component: { type: ComponentType.TextInput, customId: 'name', style: TextInputStyle.Short, required: true, value: state.name ?? '' } },
|
|
||||||
{ type: ComponentType.Label, label: 'Categoría (opcional)', component: { type: ComponentType.TextInput, customId: 'category', style: TextInputStyle.Short, required: false, value: state.category ?? '' } },
|
|
||||||
] } as const;
|
|
||||||
await i.showModal(modal);
|
|
||||||
try {
|
|
||||||
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
|
||||||
state.name = sub.components.getTextInputValue('name').trim();
|
|
||||||
const cat = sub.components.getTextInputValue('category')?.trim();
|
|
||||||
state.category = cat || undefined;
|
|
||||||
await sub.reply({ content: '✅ Base actualizada.', flags: MessageFlags.Ephemeral });
|
|
||||||
|
|
||||||
// Refresh display
|
|
||||||
const newDisplay = createMobDisplay(state, editing);
|
|
||||||
await editorMsg.edit({
|
|
||||||
flags: 32768,
|
|
||||||
components: [
|
|
||||||
newDisplay,
|
|
||||||
{
|
{
|
||||||
type: 1,
|
type: 1,
|
||||||
components: [
|
components: [
|
||||||
@@ -180,12 +129,114 @@ async function showBaseModal(i: ButtonInteraction, state: MobEditorState, editor
|
|||||||
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'mb_cancel' },
|
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'mb_cancel' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
];
|
||||||
|
|
||||||
|
const editorMsg = selection.panelMessage;
|
||||||
|
await editorMsg.edit({
|
||||||
|
content: null,
|
||||||
|
flags: 32768,
|
||||||
|
components: buildEditorComponents(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const collector = editorMsg.createMessageComponentCollector({ time: 30 * 60_000, filter: (i) => i.user.id === message.author.id });
|
||||||
|
collector.on('collect', async (i: MessageComponentInteraction) => {
|
||||||
|
try {
|
||||||
|
if (!i.isButton()) return;
|
||||||
|
switch (i.customId) {
|
||||||
|
case 'mb_cancel':
|
||||||
|
await i.deferUpdate();
|
||||||
|
await editorMsg.edit({
|
||||||
|
content: null,
|
||||||
|
flags: 32768,
|
||||||
|
components: [{
|
||||||
|
type: 17,
|
||||||
|
accent_color: 0xFF0000,
|
||||||
|
components: [{
|
||||||
|
type: 10,
|
||||||
|
content: '**❌ Editor cancelado.**'
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
collector.stop('cancel');
|
||||||
|
return;
|
||||||
|
case 'mb_base':
|
||||||
|
await showBaseModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents);
|
||||||
|
return;
|
||||||
|
case 'mb_stats':
|
||||||
|
await showJsonModal(i as ButtonInteraction, state, 'stats', 'Stats del Mob (JSON)', editorMsg, buildEditorComponents);
|
||||||
|
return;
|
||||||
|
case 'mb_drops':
|
||||||
|
await showJsonModal(i as ButtonInteraction, state, 'drops', 'Drops del Mob (JSON)', editorMsg, buildEditorComponents);
|
||||||
|
return;
|
||||||
|
case 'mb_save':
|
||||||
|
if (!state.name) {
|
||||||
|
await i.reply({ content: '❌ Falta el nombre del mob.', flags: MessageFlags.Ephemeral });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await client.prisma.mob.update({ where: { id: mob.id }, data: { name: state.name!, category: state.category ?? null, stats: state.stats ?? {}, drops: state.drops ?? {} } });
|
||||||
|
await i.reply({ content: '✅ Mob actualizado!', flags: MessageFlags.Ephemeral });
|
||||||
|
await editorMsg.edit({
|
||||||
|
content: null,
|
||||||
|
flags: 32768,
|
||||||
|
components: [{
|
||||||
|
type: 17,
|
||||||
|
accent_color: 0x00FF00,
|
||||||
|
components: [{
|
||||||
|
type: 10,
|
||||||
|
content: `**✅ Mob \`${state.key}\` actualizado exitosamente.**`
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
collector.stop('saved');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err }, 'mob-editar');
|
||||||
|
if (!i.deferred && !i.replied) await i.reply({ content: '❌ Error procesando la acción.', flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
collector.on('end', async (_c, reason) => {
|
||||||
|
if (reason === 'time') {
|
||||||
|
try {
|
||||||
|
await editorMsg.edit({
|
||||||
|
content: null,
|
||||||
|
flags: 32768,
|
||||||
|
components: [{
|
||||||
|
type: 17,
|
||||||
|
accent_color: 0xFFA500,
|
||||||
|
components: [{
|
||||||
|
type: 10,
|
||||||
|
content: '**⏰ Editor expirado.**'
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function showBaseModal(i: ButtonInteraction, state: MobEditorState, editorMsg: Message, buildComponents: () => any[]) {
|
||||||
|
const modal = { title: 'Base del Mob', customId: 'mb_base_modal', components: [
|
||||||
|
{ type: ComponentType.Label, label: 'Nombre', component: { type: ComponentType.TextInput, customId: 'name', style: TextInputStyle.Short, required: true, value: state.name ?? '' } },
|
||||||
|
{ type: ComponentType.Label, label: 'Categoría (opcional)', component: { type: ComponentType.TextInput, customId: 'category', style: TextInputStyle.Short, required: false, value: state.category ?? '' } },
|
||||||
|
] } as const;
|
||||||
|
await i.showModal(modal);
|
||||||
|
try {
|
||||||
|
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
||||||
|
state.name = sub.components.getTextInputValue('name').trim();
|
||||||
|
const cat = sub.components.getTextInputValue('category')?.trim();
|
||||||
|
state.category = cat || undefined;
|
||||||
|
await sub.deferUpdate();
|
||||||
|
await editorMsg.edit({
|
||||||
|
content: null,
|
||||||
|
flags: 32768,
|
||||||
|
components: buildComponents()
|
||||||
});
|
});
|
||||||
} 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, buildComponents: () => any[]) {
|
||||||
const current = JSON.stringify(state[field] ?? {});
|
const current = JSON.stringify(state[field] ?? {});
|
||||||
const modal = { title, customId: `mb_json_${field}`, components: [
|
const modal = { title, customId: `mb_json_${field}`, components: [
|
||||||
{ type: ComponentType.Label, label: 'JSON', component: { type: ComponentType.TextInput, customId: 'json', style: TextInputStyle.Paragraph, required: false, value: current.slice(0,4000) } },
|
{ type: ComponentType.Label, label: 'JSON', component: { type: ComponentType.TextInput, customId: 'json', style: TextInputStyle.Paragraph, required: false, value: current.slice(0,4000) } },
|
||||||
@@ -197,33 +248,19 @@ async function showJsonModal(i: ButtonInteraction, state: MobEditorState, field:
|
|||||||
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.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 {
|
||||||
state[field] = {};
|
state[field] = {};
|
||||||
await sub.reply({ content: 'ℹ️ Limpio.', flags: MessageFlags.Ephemeral });
|
await sub.deferUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh display
|
|
||||||
const newDisplay = createMobDisplay(state, editing);
|
|
||||||
await editorMsg.edit({
|
await editorMsg.edit({
|
||||||
|
content: null,
|
||||||
flags: 32768,
|
flags: 32768,
|
||||||
components: [
|
components: buildComponents()
|
||||||
newDisplay,
|
|
||||||
{
|
|
||||||
type: 1,
|
|
||||||
components: [
|
|
||||||
{ type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'mb_base' },
|
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Stats (JSON)', custom_id: 'mb_stats' },
|
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Drops (JSON)', custom_id: 'mb_drops' },
|
|
||||||
{ type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'mb_save' },
|
|
||||||
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'mb_cancel' },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ 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';
|
||||||
import { Message, MessageComponentInteraction, MessageFlags, ButtonInteraction } from 'discord.js';
|
import { Message, MessageComponentInteraction, MessageFlags, ButtonInteraction, TextBasedChannel } from 'discord.js';
|
||||||
import { ComponentType, TextInputStyle, ButtonStyle } from 'discord-api-types/v10';
|
import { ComponentType, TextInputStyle, ButtonStyle } from 'discord-api-types/v10';
|
||||||
|
import { promptKeySelection, resolveItemIcon } from './_helpers';
|
||||||
|
|
||||||
interface OfferState {
|
interface OfferState {
|
||||||
offerId: string;
|
offerId: string;
|
||||||
@@ -23,23 +24,64 @@ export const command: CommandMessage = {
|
|||||||
aliases: ['editar-oferta','offeredit'],
|
aliases: ['editar-oferta','offeredit'],
|
||||||
cooldown: 10,
|
cooldown: 10,
|
||||||
description: 'Edita una ShopOffer por ID con editor interactivo (price/ventanas/stock/limit).',
|
description: 'Edita una ShopOffer por ID con editor interactivo (price/ventanas/stock/limit).',
|
||||||
usage: 'offer-editar <offerId>',
|
usage: 'offer-editar',
|
||||||
run: async (message, args, _client: Amayo) => {
|
run: async (message, _args, _client: Amayo) => {
|
||||||
|
const channel = message.channel as TextBasedChannel & { send: Function };
|
||||||
const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, prisma);
|
const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, prisma);
|
||||||
if (!allowed) { await message.reply('❌ No tienes permisos de ManageGuild ni rol de staff.'); return; }
|
if (!allowed) {
|
||||||
|
await (channel.send as any)({
|
||||||
const offerId = args[0]?.trim();
|
content: null,
|
||||||
if (!offerId) { await message.reply('Uso: `!offer-editar <offerId>`'); return; }
|
flags: 32768,
|
||||||
|
components: [{
|
||||||
|
type: 17,
|
||||||
|
accent_color: 0xFF0000,
|
||||||
|
components: [{
|
||||||
|
type: 10,
|
||||||
|
content: '❌ **Error de Permisos**\n└ No tienes permisos de ManageGuild ni rol de staff.'
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
reply: { messageReference: message.id }
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const guildId = message.guild!.id;
|
const guildId = message.guild!.id;
|
||||||
const offer = await prisma.shopOffer.findUnique({ where: { id: offerId } });
|
const offers = await prisma.shopOffer.findMany({
|
||||||
if (!offer || offer.guildId !== guildId) { await message.reply('❌ Oferta no encontrada para este servidor.'); return; }
|
where: { guildId },
|
||||||
|
orderBy: [{ updatedAt: 'desc' }],
|
||||||
|
include: { item: true },
|
||||||
|
});
|
||||||
|
|
||||||
const item = await prisma.economyItem.findUnique({ where: { id: offer.itemId } });
|
const selection = await promptKeySelection(message, {
|
||||||
|
entries: offers,
|
||||||
|
customIdPrefix: 'offer_edit',
|
||||||
|
title: 'Selecciona una oferta para editar',
|
||||||
|
emptyText: '⚠️ **No hay ofertas configuradas.** Usa `!offer-crear` primero.',
|
||||||
|
placeholder: 'Elige una oferta…',
|
||||||
|
filterHint: 'Filtra por item, key o estado.',
|
||||||
|
getOption: (offer) => {
|
||||||
|
const icon = resolveItemIcon(offer.item?.icon);
|
||||||
|
const itemName = offer.item?.name ?? offer.item?.key ?? 'Item sin nombre';
|
||||||
|
const status = offer.enabled ? 'Activa' : 'Inactiva';
|
||||||
|
const label = `${icon} ${itemName}`.trim();
|
||||||
|
return {
|
||||||
|
value: offer.id,
|
||||||
|
label: label.slice(0, 100),
|
||||||
|
description: `${status} • ${offer.id.slice(0, 14)}…`,
|
||||||
|
keywords: [offer.id, itemName, offer.item?.key ?? '', status],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!selection.entry || !selection.panelMessage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offer = selection.entry;
|
||||||
|
|
||||||
const state: OfferState = {
|
const state: OfferState = {
|
||||||
offerId,
|
offerId: offer.id,
|
||||||
itemKey: item?.key,
|
itemKey: offer.item?.key,
|
||||||
enabled: offer.enabled,
|
enabled: offer.enabled,
|
||||||
price: offer.price ?? {},
|
price: offer.price ?? {},
|
||||||
startAt: offer.startAt ? new Date(offer.startAt).toISOString() : '',
|
startAt: offer.startAt ? new Date(offer.startAt).toISOString() : '',
|
||||||
@@ -49,38 +91,103 @@ export const command: CommandMessage = {
|
|||||||
metadata: offer.metadata ?? {},
|
metadata: offer.metadata ?? {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const editorMsg = await (message.channel as any).send({
|
const buildEditorDisplay = () => {
|
||||||
content: `🛒 Editor de Oferta (editar): ${offerId}`,
|
const status = state.enabled ? '✅ Activa' : '⛔ Inactiva';
|
||||||
|
const priceInfo = state.price && Object.keys(state.price ?? {}).length ? 'Configurado' : 'Sin configurar';
|
||||||
|
const windowInfo = state.startAt || state.endAt ? `${state.startAt || '—'} → ${state.endAt || '—'}` : 'Sin ventana';
|
||||||
|
const limitsInfo = `Usuario: ${state.perUserLimit ?? '∞'} • Stock: ${state.stock ?? '∞'}`;
|
||||||
|
const metaInfo = state.metadata && Object.keys(state.metadata ?? {}).length ? `${Object.keys(state.metadata!).length} campos` : 'Vacío';
|
||||||
|
return {
|
||||||
|
type: 17,
|
||||||
|
accent_color: state.enabled ? 0x00D9FF : 0x666666,
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: `# 🛒 Editando Oferta: \`${state.offerId}\`\n**Item:** ${state.itemKey ?? '*Sin asignar*'}\n**Estado:** ${status}\n**Precio:** ${priceInfo}\n**Ventana:** ${windowInfo}\n**Límites:** ${limitsInfo}\n**Meta:** ${metaInfo}`
|
||||||
|
},
|
||||||
|
{ type: 14, divider: true },
|
||||||
|
{
|
||||||
|
type: 10,
|
||||||
|
content: '**📋 Pasos rápidos:**\n• Base: item y estado\n• Precio: JSON de coste\n• Ventana: fechas inicio/fin\n• Límites: stock y por usuario\n• Meta: datos adicionales'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildEditorComponents = () => [
|
||||||
|
buildEditorDisplay(),
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
components: [
|
components: [
|
||||||
{ type: 1, components: [
|
|
||||||
{ type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'of_base' },
|
{ type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'of_base' },
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Precio (JSON)', custom_id: 'of_price' },
|
{ type: 2, style: ButtonStyle.Secondary, label: 'Precio (JSON)', custom_id: 'of_price' },
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'of_window' },
|
{ type: 2, style: ButtonStyle.Secondary, label: 'Ventana', custom_id: 'of_window' },
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Límites', custom_id: 'of_limits' },
|
{ type: 2, style: ButtonStyle.Secondary, label: 'Límites', custom_id: 'of_limits' },
|
||||||
{ type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'of_meta' },
|
{ type: 2, style: ButtonStyle.Secondary, label: 'Meta (JSON)', custom_id: 'of_meta' },
|
||||||
] },
|
]
|
||||||
{ type: 1, components: [
|
},
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
components: [
|
||||||
{ type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'of_save' },
|
{ type: 2, style: ButtonStyle.Success, label: 'Guardar', custom_id: 'of_save' },
|
||||||
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'of_cancel' },
|
{ type: 2, style: ButtonStyle.Danger, label: 'Cancelar', custom_id: 'of_cancel' },
|
||||||
] },
|
]
|
||||||
],
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const editorMsg = selection.panelMessage;
|
||||||
|
await editorMsg.edit({
|
||||||
|
content: null,
|
||||||
|
flags: 32768,
|
||||||
|
components: buildEditorComponents(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const collector = editorMsg.createMessageComponentCollector({ time: 30*60_000, filter: (i: MessageComponentInteraction)=> i.user.id === message.author.id });
|
const collector = editorMsg.createMessageComponentCollector({ time: 30 * 60_000, filter: (i: MessageComponentInteraction) => i.user.id === message.author.id });
|
||||||
collector.on('collect', async (i: MessageComponentInteraction) => {
|
collector.on('collect', async (i: MessageComponentInteraction) => {
|
||||||
try {
|
try {
|
||||||
if (!i.isButton()) return;
|
if (!i.isButton()) return;
|
||||||
switch (i.customId) {
|
switch (i.customId) {
|
||||||
case 'of_cancel': await i.deferUpdate(); await editorMsg.edit({ content: '❌ Editor de Oferta cancelado.', components: [] }); collector.stop('cancel'); return;
|
case 'of_cancel':
|
||||||
case 'of_base': await showBaseModal(i as ButtonInteraction, state); return;
|
await i.deferUpdate();
|
||||||
case 'of_price': await showJsonModal(i as ButtonInteraction, state, 'price', 'Precio'); return;
|
await editorMsg.edit({
|
||||||
case 'of_window': await showWindowModal(i as ButtonInteraction, state); return;
|
content: null,
|
||||||
case 'of_limits': await showLimitsModal(i as ButtonInteraction, state); return;
|
flags: 32768,
|
||||||
case 'of_meta': await showJsonModal(i as ButtonInteraction, state, 'metadata', 'Meta'); return;
|
components: [{
|
||||||
|
type: 17,
|
||||||
|
accent_color: 0xFF0000,
|
||||||
|
components: [{
|
||||||
|
type: 10,
|
||||||
|
content: '**❌ Editor de Oferta cancelado.**'
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
collector.stop('cancel');
|
||||||
|
return;
|
||||||
|
case 'of_base':
|
||||||
|
await showBaseModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents);
|
||||||
|
return;
|
||||||
|
case 'of_price':
|
||||||
|
await showJsonModal(i as ButtonInteraction, state, 'price', 'Precio', editorMsg, buildEditorComponents);
|
||||||
|
return;
|
||||||
|
case 'of_window':
|
||||||
|
await showWindowModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents);
|
||||||
|
return;
|
||||||
|
case 'of_limits':
|
||||||
|
await showLimitsModal(i as ButtonInteraction, state, editorMsg, buildEditorComponents);
|
||||||
|
return;
|
||||||
|
case 'of_meta':
|
||||||
|
await showJsonModal(i as ButtonInteraction, state, 'metadata', 'Meta', editorMsg, buildEditorComponents);
|
||||||
|
return;
|
||||||
case 'of_save':
|
case 'of_save':
|
||||||
if (!state.itemKey) { await i.reply({ content: '❌ Falta itemKey en Base.', flags: MessageFlags.Ephemeral }); return; }
|
if (!state.itemKey) {
|
||||||
|
await i.reply({ content: '❌ Falta itemKey en Base.', flags: MessageFlags.Ephemeral });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const it = await prisma.economyItem.findFirst({ where: { key: state.itemKey, OR: [{ guildId }, { guildId: null }] }, orderBy: [{ guildId: 'desc' }] });
|
const it = await prisma.economyItem.findFirst({ where: { key: state.itemKey, OR: [{ guildId }, { guildId: null }] }, orderBy: [{ guildId: 'desc' }] });
|
||||||
if (!it) { await i.reply({ content: '❌ Item no encontrado por key.', flags: MessageFlags.Ephemeral }); return; }
|
if (!it) {
|
||||||
|
await i.reply({ content: '❌ Item no encontrado por key.', flags: MessageFlags.Ephemeral });
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await prisma.shopOffer.update({
|
await prisma.shopOffer.update({
|
||||||
where: { id: state.offerId },
|
where: { id: state.offerId },
|
||||||
@@ -96,7 +203,18 @@ export const command: CommandMessage = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
await i.reply({ content: '✅ Oferta actualizada.', flags: MessageFlags.Ephemeral });
|
await i.reply({ content: '✅ Oferta actualizada.', flags: MessageFlags.Ephemeral });
|
||||||
await editorMsg.edit({ content: `✅ Oferta ${state.offerId} actualizada.`, components: [] });
|
await editorMsg.edit({
|
||||||
|
content: null,
|
||||||
|
flags: 32768,
|
||||||
|
components: [{
|
||||||
|
type: 17,
|
||||||
|
accent_color: 0x00FF00,
|
||||||
|
components: [{
|
||||||
|
type: 10,
|
||||||
|
content: `✅ **Oferta \`${state.offerId}\` actualizada.**`
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
});
|
||||||
collector.stop('saved');
|
collector.stop('saved');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
await i.reply({ content: `❌ Error al guardar: ${err?.message ?? err}`, flags: MessageFlags.Ephemeral });
|
await i.reply({ content: `❌ Error al guardar: ${err?.message ?? err}`, flags: MessageFlags.Ephemeral });
|
||||||
@@ -107,42 +225,115 @@ export const command: CommandMessage = {
|
|||||||
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: any,r: string)=> { if (r==='time') { try { await editorMsg.edit({ content:'⏰ Editor expirado.', components: [] }); } catch {} } });
|
|
||||||
|
collector.on('end', async (_c: any, reason: string) => {
|
||||||
|
if (reason === 'time') {
|
||||||
|
try {
|
||||||
|
await editorMsg.edit({
|
||||||
|
content: null,
|
||||||
|
flags: 32768,
|
||||||
|
components: [{
|
||||||
|
type: 17,
|
||||||
|
accent_color: 0xFFA500,
|
||||||
|
components: [{
|
||||||
|
type: 10,
|
||||||
|
content: '**⏰ Editor expirado.**'
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function showBaseModal(i: ButtonInteraction, state: OfferState) {
|
async function showBaseModal(i: ButtonInteraction, state: OfferState, editorMsg: Message, buildComponents: () => any[]) {
|
||||||
const modal = { title: 'Base de Oferta', customId: 'of_base_modal', components: [
|
const modal = { title: 'Base de Oferta', customId: 'of_base_modal', components: [
|
||||||
{ type: ComponentType.Label, label: 'Item Key', component: { type: ComponentType.TextInput, customId: 'itemKey', style: TextInputStyle.Short, required: true, value: state.itemKey ?? '' } },
|
{ type: ComponentType.Label, label: 'Item Key', component: { type: ComponentType.TextInput, customId: 'itemKey', style: TextInputStyle.Short, required: true, value: state.itemKey ?? '' } },
|
||||||
{ type: ComponentType.Label, label: 'Habilitada? (true/false)', component: { type: ComponentType.TextInput, customId: 'enabled', style: TextInputStyle.Short, required: false, value: String(state.enabled ?? true) } },
|
{ type: ComponentType.Label, label: 'Habilitada? (true/false)', component: { type: ComponentType.TextInput, customId: 'enabled', style: TextInputStyle.Short, required: false, value: String(state.enabled ?? true) } },
|
||||||
] } as const;
|
] } as const;
|
||||||
await i.showModal(modal);
|
await i.showModal(modal);
|
||||||
try { const sub = await i.awaitModalSubmit({ time: 300_000 }); state.itemKey = sub.components.getTextInputValue('itemKey').trim(); const en = sub.components.getTextInputValue('enabled').trim(); state.enabled = en ? (en.toLowerCase() !== 'false') : (state.enabled ?? true); await sub.reply({ content: '✅ Base actualizada.', flags: MessageFlags.Ephemeral }); } catch {}
|
try {
|
||||||
|
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
||||||
|
state.itemKey = sub.components.getTextInputValue('itemKey').trim();
|
||||||
|
const en = sub.components.getTextInputValue('enabled').trim();
|
||||||
|
state.enabled = en ? (en.toLowerCase() !== 'false') : (state.enabled ?? true);
|
||||||
|
await sub.deferUpdate();
|
||||||
|
await editorMsg.edit({
|
||||||
|
content: null,
|
||||||
|
flags: 32768,
|
||||||
|
components: buildComponents()
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showJsonModal(i: ButtonInteraction, state: OfferState, field: 'price'|'metadata', title: string) {
|
async function showJsonModal(i: ButtonInteraction, state: OfferState, field: 'price'|'metadata', title: string, editorMsg: Message, buildComponents: () => any[]) {
|
||||||
const current = JSON.stringify(state[field] ?? {});
|
const current = JSON.stringify(state[field] ?? {});
|
||||||
const modal = { title, customId: `of_json_${field}`, components: [
|
const modal = { title, customId: `of_json_${field}`, components: [
|
||||||
{ type: ComponentType.Label, label: 'JSON', component: { type: ComponentType.TextInput, customId: 'json', style: TextInputStyle.Paragraph, required: false, value: current.slice(0,4000) } },
|
{ type: ComponentType.Label, label: 'JSON', component: { type: ComponentType.TextInput, customId: 'json', style: TextInputStyle.Paragraph, required: false, value: current.slice(0,4000) } },
|
||||||
] } as const;
|
] } as const;
|
||||||
await i.showModal(modal);
|
await i.showModal(modal);
|
||||||
try { const sub = await i.awaitModalSubmit({ time: 300_000 }); const raw = sub.components.getTextInputValue('json'); if (raw) { try { state[field] = JSON.parse(raw); await sub.reply({ content: '✅ Guardado.', flags: MessageFlags.Ephemeral }); } catch { await sub.reply({ content: '❌ JSON inválido.', flags: MessageFlags.Ephemeral }); } } else { state[field] = {}; await sub.reply({ content: 'ℹ️ Limpio.', flags: MessageFlags.Ephemeral }); } } catch {}
|
try {
|
||||||
|
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
||||||
|
const raw = sub.components.getTextInputValue('json');
|
||||||
|
if (raw) {
|
||||||
|
try {
|
||||||
|
state[field] = JSON.parse(raw);
|
||||||
|
await sub.deferUpdate();
|
||||||
|
} catch {
|
||||||
|
await sub.reply({ content: '❌ JSON inválido.', flags: MessageFlags.Ephemeral });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state[field] = {};
|
||||||
|
await sub.deferUpdate();
|
||||||
|
}
|
||||||
|
await editorMsg.edit({
|
||||||
|
content: null,
|
||||||
|
flags: 32768,
|
||||||
|
components: buildComponents()
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showWindowModal(i: ButtonInteraction, state: OfferState) {
|
async function showWindowModal(i: ButtonInteraction, state: OfferState, editorMsg: Message, buildComponents: () => any[]) {
|
||||||
const modal = { title: 'Ventana', customId: 'of_window_modal', components: [
|
const modal = { title: 'Ventana', customId: 'of_window_modal', components: [
|
||||||
{ type: ComponentType.Label, label: 'Inicio (ISO opcional)', component: { type: ComponentType.TextInput, customId: 'start', style: TextInputStyle.Short, required: false, value: state.startAt ?? '' } },
|
{ type: ComponentType.Label, label: 'Inicio (ISO opcional)', component: { type: ComponentType.TextInput, customId: 'start', style: TextInputStyle.Short, required: false, value: state.startAt ?? '' } },
|
||||||
{ type: ComponentType.Label, label: 'Fin (ISO opcional)', component: { type: ComponentType.TextInput, customId: 'end', style: TextInputStyle.Short, required: false, value: state.endAt ?? '' } },
|
{ type: ComponentType.Label, label: 'Fin (ISO opcional)', component: { type: ComponentType.TextInput, customId: 'end', style: TextInputStyle.Short, required: false, value: state.endAt ?? '' } },
|
||||||
] } as const;
|
] } as const;
|
||||||
await i.showModal(modal);
|
await i.showModal(modal);
|
||||||
try { const sub = await i.awaitModalSubmit({ time: 300_000 }); const s = sub.components.getTextInputValue('start').trim(); const e = sub.components.getTextInputValue('end').trim(); state.startAt = s || ''; state.endAt = e || ''; await sub.reply({ content: '✅ Ventana actualizada.', flags: MessageFlags.Ephemeral }); } catch {}
|
try {
|
||||||
|
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
||||||
|
const s = sub.components.getTextInputValue('start').trim();
|
||||||
|
const e = sub.components.getTextInputValue('end').trim();
|
||||||
|
state.startAt = s || '';
|
||||||
|
state.endAt = e || '';
|
||||||
|
await sub.deferUpdate();
|
||||||
|
await editorMsg.edit({
|
||||||
|
content: null,
|
||||||
|
flags: 32768,
|
||||||
|
components: buildComponents()
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showLimitsModal(i: ButtonInteraction, state: OfferState) {
|
async function showLimitsModal(i: ButtonInteraction, state: OfferState, editorMsg: Message, buildComponents: () => any[]) {
|
||||||
const modal = { title: 'Límites', customId: 'of_limits_modal', components: [
|
const modal = { title: 'Límites', customId: 'of_limits_modal', components: [
|
||||||
{ type: ComponentType.Label, label: 'Límite por usuario (vacío = sin límite)', component: { type: ComponentType.TextInput, customId: 'limit', style: TextInputStyle.Short, required: false, value: state.perUserLimit != null ? String(state.perUserLimit) : '' } },
|
{ type: ComponentType.Label, label: 'Límite por usuario (vacío = sin límite)', component: { type: ComponentType.TextInput, customId: 'limit', style: TextInputStyle.Short, required: false, value: state.perUserLimit != null ? String(state.perUserLimit) : '' } },
|
||||||
{ type: ComponentType.Label, label: 'Stock global (vacío = ilimitado)', component: { type: ComponentType.TextInput, customId: 'stock', style: TextInputStyle.Short, required: false, value: state.stock != null ? String(state.stock) : '' } },
|
{ type: ComponentType.Label, label: 'Stock global (vacío = ilimitado)', component: { type: ComponentType.TextInput, customId: 'stock', style: TextInputStyle.Short, required: false, value: state.stock != null ? String(state.stock) : '' } },
|
||||||
] } as const;
|
] } as const;
|
||||||
await i.showModal(modal);
|
await i.showModal(modal);
|
||||||
try { const sub = await i.awaitModalSubmit({ time: 300_000 }); const lim = sub.components.getTextInputValue('limit').trim(); const st = sub.components.getTextInputValue('stock').trim(); state.perUserLimit = lim ? Math.max(0, parseInt(lim,10)||0) : null; state.stock = st ? Math.max(0, parseInt(st,10)||0) : null; await sub.reply({ content: '✅ Límites actualizados.', flags: MessageFlags.Ephemeral }); } catch {}
|
try {
|
||||||
|
const sub = await i.awaitModalSubmit({ time: 300_000 });
|
||||||
|
const lim = sub.components.getTextInputValue('limit').trim();
|
||||||
|
const st = sub.components.getTextInputValue('stock').trim();
|
||||||
|
state.perUserLimit = lim ? Math.max(0, parseInt(lim, 10) || 0) : null;
|
||||||
|
state.stock = st ? Math.max(0, parseInt(st, 10) || 0) : null;
|
||||||
|
await sub.deferUpdate();
|
||||||
|
await editorMsg.edit({
|
||||||
|
content: null,
|
||||||
|
flags: 32768,
|
||||||
|
components: buildComponents()
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
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 { runMinigame } from '../../../game/minigames/service';
|
import { runMinigame } from '../../../game/minigames/service';
|
||||||
import { getDefaultLevel, findBestToolKey, parseGameArgs, resolveGuildAreaWithFallback, resolveAreaByType } from './_helpers';
|
import { getDefaultLevel, findBestToolKey, parseGameArgs, resolveGuildAreaWithFallback, resolveAreaByType, sendDisplayReply, fetchItemBasics, formatItemLabel } from './_helpers';
|
||||||
import { updateStats } from '../../../game/stats/service';
|
import { updateStats } from '../../../game/stats/service';
|
||||||
import { updateQuestProgress } from '../../../game/quests/service';
|
import { updateQuestProgress } from '../../../game/quests/service';
|
||||||
import { checkAchievements } from '../../../game/achievements/service';
|
import { checkAchievements } from '../../../game/achievements/service';
|
||||||
|
import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2';
|
||||||
|
|
||||||
|
const FIGHT_ACCENT = 0x992D22;
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'pelear',
|
name: 'pelear',
|
||||||
@@ -42,6 +45,12 @@ export const command: CommandMessage = {
|
|||||||
try {
|
try {
|
||||||
const result = await runMinigame(userId, guildId, area.key, level, { toolKey: toolKey ?? undefined });
|
const result = await runMinigame(userId, guildId, area.key, level, { toolKey: toolKey ?? undefined });
|
||||||
|
|
||||||
|
const rewardKeys = result.rewards
|
||||||
|
.filter((r): r is { type: 'item'; itemKey: string; qty?: number } => r.type === 'item' && Boolean(r.itemKey))
|
||||||
|
.map((r) => r.itemKey!);
|
||||||
|
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||||
|
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||||
|
|
||||||
// Actualizar stats y misiones
|
// Actualizar stats y misiones
|
||||||
await updateStats(userId, guildId, { fightsCompleted: 1 });
|
await updateStats(userId, guildId, { fightsCompleted: 1 });
|
||||||
await updateQuestProgress(userId, guildId, 'fight_count', 1);
|
await updateQuestProgress(userId, guildId, 'fight_count', 1);
|
||||||
@@ -55,21 +64,44 @@ export const command: CommandMessage = {
|
|||||||
|
|
||||||
const newAchievements = await checkAchievements(userId, guildId, 'fight_count');
|
const newAchievements = await checkAchievements(userId, guildId, 'fight_count');
|
||||||
|
|
||||||
const rewards = result.rewards.map(r => r.type === 'coins' ? `🪙 +${r.amount}` : `🎁 ${r.itemKey} x${r.qty}`).join(' · ') || '—';
|
const rewardLines = result.rewards.length
|
||||||
const mobs = result.mobs.length ? result.mobs.join(', ') : '—';
|
? result.rewards.map((r) => {
|
||||||
const toolInfo = result.tool?.key ? `🗡️ ${result.tool.key}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta} dur.)`}` : '—';
|
if (r.type === 'coins') return `• 🪙 +${r.amount}`;
|
||||||
|
const info = rewardItems.get(r.itemKey!);
|
||||||
|
const label = formatItemLabel(info ?? { key: r.itemKey!, name: null, icon: null });
|
||||||
|
return `• ${label} x${r.qty ?? 1}`;
|
||||||
|
}).join('\n')
|
||||||
|
: '• —';
|
||||||
|
const mobsLines = result.mobs.length
|
||||||
|
? result.mobs.map(m => `• ${m}`).join('\n')
|
||||||
|
: '• —';
|
||||||
|
const toolInfo = result.tool?.key
|
||||||
|
? `${formatItemLabel(rewardItems.get(result.tool.key) ?? { key: result.tool.key, name: null, icon: null }, { fallbackIcon: '🗡️' })}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta ?? 0} dur.)`}`
|
||||||
|
: '—';
|
||||||
|
|
||||||
let response = globalNotice ? `${globalNotice}\n\n` : '';
|
const blocks = [textBlock('# ⚔️ Arena')];
|
||||||
response += `⚔️ Arena (nivel ${level})\nRecompensas: ${rewards}\nEnemigos: ${mobs}\nArma: ${toolInfo}`;
|
|
||||||
|
if (globalNotice) {
|
||||||
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
|
blocks.push(textBlock(globalNotice));
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks.push(dividerBlock());
|
||||||
|
const areaScope = source === 'global' ? '🌐 Configuración global' : '📍 Configuración local';
|
||||||
|
blocks.push(textBlock(`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Arma:** ${toolInfo}`));
|
||||||
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
|
blocks.push(textBlock(`**Recompensas**\n${rewardLines}`));
|
||||||
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
|
blocks.push(textBlock(`**Enemigos**\n${mobsLines}`));
|
||||||
|
|
||||||
if (newAchievements.length > 0) {
|
if (newAchievements.length > 0) {
|
||||||
response += `\n\n🏆 ¡Logro desbloqueado!`;
|
blocks.push(dividerBlock({ divider: false, spacing: 2 }));
|
||||||
for (const ach of newAchievements) {
|
const achLines = newAchievements.map(ach => `✨ **${ach.name}** — ${ach.description}`).join('\n');
|
||||||
response += `\n✨ **${ach.name}** - ${ach.description}`;
|
blocks.push(textBlock(`🏆 ¡Logro desbloqueado!\n${achLines}`));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await message.reply(response);
|
const display = buildDisplay(FIGHT_ACCENT, blocks);
|
||||||
|
await sendDisplayReply(message, display);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
await message.reply(`❌ No se pudo pelear: ${e?.message ?? e}`);
|
await message.reply(`❌ No se pudo pelear: ${e?.message ?? e}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
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 { runMinigame } from '../../../game/minigames/service';
|
import { runMinigame } from '../../../game/minigames/service';
|
||||||
import { getDefaultLevel, findBestToolKey, parseGameArgs, resolveGuildAreaWithFallback, resolveAreaByType } from './_helpers';
|
import { getDefaultLevel, findBestToolKey, parseGameArgs, resolveGuildAreaWithFallback, resolveAreaByType, sendDisplayReply, fetchItemBasics, formatItemLabel } from './_helpers';
|
||||||
import { updateStats } from '../../../game/stats/service';
|
import { updateStats } from '../../../game/stats/service';
|
||||||
import { updateQuestProgress } from '../../../game/quests/service';
|
import { updateQuestProgress } from '../../../game/quests/service';
|
||||||
import { checkAchievements } from '../../../game/achievements/service';
|
import { checkAchievements } from '../../../game/achievements/service';
|
||||||
|
import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2';
|
||||||
|
|
||||||
|
const FISHING_ACCENT = 0x1ABC9C;
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'pescar',
|
name: 'pescar',
|
||||||
@@ -42,29 +45,55 @@ export const command: CommandMessage = {
|
|||||||
try {
|
try {
|
||||||
const result = await runMinigame(userId, guildId, area.key, level, { toolKey: toolKey ?? undefined });
|
const result = await runMinigame(userId, guildId, area.key, level, { toolKey: toolKey ?? undefined });
|
||||||
|
|
||||||
|
const rewardKeys = result.rewards
|
||||||
|
.filter((r): r is { type: 'item'; itemKey: string; qty?: number } => r.type === 'item' && Boolean(r.itemKey))
|
||||||
|
.map((r) => r.itemKey!);
|
||||||
|
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||||
|
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||||
|
|
||||||
// Actualizar stats y misiones
|
// Actualizar stats y misiones
|
||||||
await updateStats(userId, guildId, { fishingCompleted: 1 });
|
await updateStats(userId, guildId, { fishingCompleted: 1 });
|
||||||
await updateQuestProgress(userId, guildId, 'fish_count', 1);
|
await updateQuestProgress(userId, guildId, 'fish_count', 1);
|
||||||
const newAchievements = await checkAchievements(userId, guildId, 'fish_count');
|
const newAchievements = await checkAchievements(userId, guildId, 'fish_count');
|
||||||
|
|
||||||
const rewards = result.rewards.map(r => r.type === 'coins' ? `🪙 +${r.amount}` : `🐟 ${r.itemKey} x${r.qty}`).join(' · ') || '—';
|
const rewardLines = result.rewards.length
|
||||||
const mobs = result.mobs.length ? result.mobs.join(', ') : '—';
|
? result.rewards.map((r) => {
|
||||||
const toolInfo = result.tool?.key ? `🎣 ${result.tool.key}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta} dur.)`}` : '—';
|
if (r.type === 'coins') return `• 🪙 +${r.amount}`;
|
||||||
|
const info = rewardItems.get(r.itemKey!);
|
||||||
|
const label = formatItemLabel(info ?? { key: r.itemKey!, name: null, icon: null });
|
||||||
|
return `• ${label} x${r.qty ?? 1}`;
|
||||||
|
}).join('\n')
|
||||||
|
: '• —';
|
||||||
|
const mobsLines = result.mobs.length
|
||||||
|
? result.mobs.map(m => `• ${m}`).join('\n')
|
||||||
|
: '• —';
|
||||||
|
const toolInfo = result.tool?.key
|
||||||
|
? `${formatItemLabel(rewardItems.get(result.tool.key) ?? { key: result.tool.key, name: null, icon: null }, { fallbackIcon: '🎣' })}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta ?? 0} dur.)`}`
|
||||||
|
: '—';
|
||||||
|
|
||||||
let response = globalNotice ? `${globalNotice}\n\n` : '';
|
const blocks = [textBlock('# 🎣 Pesca')];
|
||||||
response += `🎣 Pesca (nivel ${level})
|
|
||||||
Recompensas: ${rewards}
|
if (globalNotice) {
|
||||||
Mobs: ${mobs}
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
Herramienta: ${toolInfo}`;
|
blocks.push(textBlock(globalNotice));
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks.push(dividerBlock());
|
||||||
|
const areaScope = source === 'global' ? '🌐 Configuración global' : '📍 Configuración local';
|
||||||
|
blocks.push(textBlock(`**Área:** \`${area.key}\` • ${areaScope}\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`));
|
||||||
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
|
blocks.push(textBlock(`**Recompensas**\n${rewardLines}`));
|
||||||
|
blocks.push(dividerBlock({ divider: false, spacing: 1 }));
|
||||||
|
blocks.push(textBlock(`**Mobs**\n${mobsLines}`));
|
||||||
|
|
||||||
if (newAchievements.length > 0) {
|
if (newAchievements.length > 0) {
|
||||||
response += `\n\n🏆 ¡Logro desbloqueado!`;
|
blocks.push(dividerBlock({ divider: false, spacing: 2 }));
|
||||||
for (const ach of newAchievements) {
|
const achLines = newAchievements.map(ach => `✨ **${ach.name}** — ${ach.description}`).join('\n');
|
||||||
response += `\n✨ **${ach.name}** - ${ach.description}`;
|
blocks.push(textBlock(`🏆 ¡Logro desbloqueado!\n${achLines}`));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await message.reply(response);
|
const display = buildDisplay(FISHING_ACCENT, blocks);
|
||||||
|
await sendDisplayReply(message, display);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
await message.reply(`❌ No se pudo pescar: ${e?.message ?? e}`);
|
await message.reply(`❌ No se pudo pescar: ${e?.message ?? e}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
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 { runMinigame } from '../../../game/minigames/service';
|
import { runMinigame } from '../../../game/minigames/service';
|
||||||
import { resolveArea, getDefaultLevel, findBestToolKey } from './_helpers';
|
import { resolveArea, getDefaultLevel, findBestToolKey, sendDisplayReply, fetchItemBasics, formatItemLabel } from './_helpers';
|
||||||
|
import { buildDisplay, dividerBlock, textBlock } from '../../../core/lib/componentsV2';
|
||||||
|
|
||||||
|
const FARM_ACCENT = 0x2ECC71;
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'plantar',
|
name: 'plantar',
|
||||||
@@ -26,13 +29,40 @@ export const command: CommandMessage = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await runMinigame(userId, guildId, areaKey, level, { toolKey: toolKey ?? undefined });
|
const result = await runMinigame(userId, guildId, areaKey, level, { toolKey: toolKey ?? undefined });
|
||||||
const rewards = result.rewards.map(r => r.type === 'coins' ? `🪙 +${r.amount}` : `🌾 ${r.itemKey} x${r.qty}`).join(' · ') || '—';
|
|
||||||
const mobs = result.mobs.length ? result.mobs.join(', ') : '—';
|
const rewardKeys = result.rewards
|
||||||
const toolInfo = result.tool?.key ? `🪓 ${result.tool.key}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta} dur.)`}` : '—';
|
.filter((r): r is { type: 'item'; itemKey: string; qty?: number } => r.type === 'item' && Boolean(r.itemKey))
|
||||||
await message.reply(`🌱 Campo (nivel ${level})
|
.map((r) => r.itemKey!);
|
||||||
Recompensas: ${rewards}
|
if (result.tool?.key) rewardKeys.push(result.tool.key);
|
||||||
Eventos: ${mobs}
|
const rewardItems = await fetchItemBasics(guildId, rewardKeys);
|
||||||
Herramienta: ${toolInfo}`);
|
|
||||||
|
const rewardLines = result.rewards.length
|
||||||
|
? result.rewards.map((r) => {
|
||||||
|
if (r.type === 'coins') return `• 🪙 +${r.amount}`;
|
||||||
|
const info = rewardItems.get(r.itemKey!);
|
||||||
|
const label = formatItemLabel(info ?? { key: r.itemKey!, name: null, icon: null });
|
||||||
|
return `• ${label} x${r.qty ?? 1}`;
|
||||||
|
}).join('\n')
|
||||||
|
: '• —';
|
||||||
|
const mobsLines = result.mobs.length
|
||||||
|
? result.mobs.map(m => `• ${m}`).join('\n')
|
||||||
|
: '• —';
|
||||||
|
const toolInfo = result.tool?.key
|
||||||
|
? `${formatItemLabel(rewardItems.get(result.tool.key) ?? { key: result.tool.key, name: null, icon: null }, { fallbackIcon: '🪓' })}${result.tool.broken ? ' (rota)' : ` (-${result.tool.durabilityDelta ?? 0} dur.)`}`
|
||||||
|
: '—';
|
||||||
|
|
||||||
|
const blocks = [
|
||||||
|
textBlock('# 🌱 Campo'),
|
||||||
|
dividerBlock(),
|
||||||
|
textBlock(`**Área:** \`${area.key}\`\n**Nivel:** ${level}\n**Herramienta:** ${toolInfo}`),
|
||||||
|
dividerBlock({ divider: false, spacing: 1 }),
|
||||||
|
textBlock(`**Recompensas**\n${rewardLines}`),
|
||||||
|
dividerBlock({ divider: false, spacing: 1 }),
|
||||||
|
textBlock(`**Eventos**\n${mobsLines}`),
|
||||||
|
];
|
||||||
|
|
||||||
|
const display = buildDisplay(FARM_ACCENT, blocks);
|
||||||
|
await sendDisplayReply(message, display);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
await message.reply(`❌ No se pudo plantar: ${e?.message ?? e}`);
|
await message.reply(`❌ No se pudo plantar: ${e?.message ?? e}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getOrCreateWallet } from '../../../game/economy/service';
|
|||||||
import { getEquipment, getEffectiveStats } from '../../../game/combat/equipmentService';
|
import { getEquipment, getEffectiveStats } from '../../../game/combat/equipmentService';
|
||||||
import { getPlayerStatsFormatted } from '../../../game/stats/service';
|
import { getPlayerStatsFormatted } from '../../../game/stats/service';
|
||||||
import type { TextBasedChannel } from 'discord.js';
|
import type { TextBasedChannel } from 'discord.js';
|
||||||
|
import { formatItemLabel } from './_helpers';
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'player',
|
name: 'player',
|
||||||
@@ -49,6 +50,16 @@ export const command: CommandMessage = {
|
|||||||
take: 3,
|
take: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const weaponLine = weapon
|
||||||
|
? `⚔️ Arma: ${formatItemLabel(weapon, { fallbackIcon: '🗡️', bold: true })}`
|
||||||
|
: '⚔️ Arma: *Ninguna*';
|
||||||
|
const armorLine = armor
|
||||||
|
? `🛡️ Armadura: ${formatItemLabel(armor, { fallbackIcon: '🛡️', bold: true })}`
|
||||||
|
: '🛡️ Armadura: *Ninguna*';
|
||||||
|
const capeLine = cape
|
||||||
|
? `🧥 Capa: ${formatItemLabel(cape, { fallbackIcon: '🧥', bold: true })}`
|
||||||
|
: '🧥 Capa: *Ninguna*';
|
||||||
|
|
||||||
// Crear DisplayComponent
|
// Crear DisplayComponent
|
||||||
const display = {
|
const display = {
|
||||||
type: 17,
|
type: 17,
|
||||||
@@ -71,9 +82,9 @@ export const command: CommandMessage = {
|
|||||||
{
|
{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: `**⚔️ EQUIPO**\n` +
|
content: `**⚔️ EQUIPO**\n` +
|
||||||
(weapon ? `🗡️ Arma: **${weapon.name || weapon.key}**\n` : '🗡️ Arma: *Ninguna*\n') +
|
`${weaponLine}\n` +
|
||||||
(armor ? `🛡️ Armadura: **${armor.name || armor.key}**\n` : '🛡️ Armadura: *Ninguna*\n') +
|
`${armorLine}\n` +
|
||||||
(cape ? `🧥 Capa: **${cape.name || cape.key}**` : '🧥 Capa: *Ninguna*')
|
`${capeLine}`
|
||||||
},
|
},
|
||||||
{ type: 14, divider: true },
|
{ type: 14, divider: true },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { CommandMessage } from '../../../core/types/commands';
|
|||||||
import type Amayo from '../../../core/client';
|
import type Amayo from '../../../core/client';
|
||||||
import { getStreakInfo, updateStreak } from '../../../game/streaks/service';
|
import { getStreakInfo, updateStreak } from '../../../game/streaks/service';
|
||||||
import type { TextBasedChannel } from 'discord.js';
|
import type { TextBasedChannel } from 'discord.js';
|
||||||
|
import { fetchItemBasics, formatItemLabel } from './_helpers';
|
||||||
|
|
||||||
export const command: CommandMessage = {
|
export const command: CommandMessage = {
|
||||||
name: 'racha',
|
name: 'racha',
|
||||||
@@ -62,9 +63,12 @@ export const command: CommandMessage = {
|
|||||||
if (rewards) {
|
if (rewards) {
|
||||||
let rewardsText = '**🎁 RECOMPENSA DEL DÍA**\n';
|
let rewardsText = '**🎁 RECOMPENSA DEL DÍA**\n';
|
||||||
if (rewards.coins) rewardsText += `💰 **${rewards.coins.toLocaleString()}** monedas\n`;
|
if (rewards.coins) rewardsText += `💰 **${rewards.coins.toLocaleString()}** monedas\n`;
|
||||||
if (rewards.items) {
|
if (rewards.items && rewards.items.length) {
|
||||||
|
const basics = await fetchItemBasics(guildId, rewards.items.map((item) => item.key));
|
||||||
rewards.items.forEach(item => {
|
rewards.items.forEach(item => {
|
||||||
rewardsText += `📦 **${item.quantity}x** ${item.key}\n`;
|
const info = basics.get(item.key) ?? { key: item.key, name: null, icon: null };
|
||||||
|
const label = formatItemLabel(info, { bold: true });
|
||||||
|
rewardsText += `${label} ×${item.quantity}\n`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { prisma } from '../../../core/database/prisma';
|
|||||||
import { getOrCreateWallet, buyFromOffer } from '../../../game/economy/service';
|
import { getOrCreateWallet, buyFromOffer } from '../../../game/economy/service';
|
||||||
import type { DisplayComponentContainer } from '../../../core/types/displayComponents';
|
import type { DisplayComponentContainer } from '../../../core/types/displayComponents';
|
||||||
import type { ItemProps } from '../../../game/economy/types';
|
import type { ItemProps } from '../../../game/economy/types';
|
||||||
|
import { formatItemLabel, resolveItemIcon } from './_helpers';
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 5;
|
const ITEMS_PER_PAGE = 5;
|
||||||
|
|
||||||
@@ -198,7 +199,7 @@ async function buildShopPanel(
|
|||||||
if (selectedOffer) {
|
if (selectedOffer) {
|
||||||
const item = selectedOffer.item;
|
const item = selectedOffer.item;
|
||||||
const props = parseItemProps(item.props);
|
const props = parseItemProps(item.props);
|
||||||
const icon = getItemIcon(props, item.category);
|
const label = formatItemLabel(item, { fallbackIcon: getItemIcon(props, item.category), bold: true });
|
||||||
const price = formatPrice(selectedOffer.price);
|
const price = formatPrice(selectedOffer.price);
|
||||||
|
|
||||||
// Stock info
|
// Stock info
|
||||||
@@ -225,7 +226,7 @@ async function buildShopPanel(
|
|||||||
|
|
||||||
container.components.push({
|
container.components.push({
|
||||||
type: 10,
|
type: 10,
|
||||||
content: `${icon} **${item.name || item.key}**\n\n${item.description || 'Sin descripción'}${statsInfo}\n\n💰 Precio: ${price}${stockInfo}`
|
content: `${label}\n\n${item.description || 'Sin descripción'}${statsInfo}\n\n💰 Precio: ${price}${stockInfo}`
|
||||||
});
|
});
|
||||||
|
|
||||||
container.components.push({
|
container.components.push({
|
||||||
@@ -244,7 +245,7 @@ async function buildShopPanel(
|
|||||||
for (const offer of pageOffers) {
|
for (const offer of pageOffers) {
|
||||||
const item = offer.item;
|
const item = offer.item;
|
||||||
const props = parseItemProps(item.props);
|
const props = parseItemProps(item.props);
|
||||||
const icon = getItemIcon(props, item.category);
|
const label = formatItemLabel(item, { fallbackIcon: getItemIcon(props, item.category), bold: true });
|
||||||
const price = formatPrice(offer.price);
|
const price = formatPrice(offer.price);
|
||||||
const isSelected = selectedOfferId === offer.id;
|
const isSelected = selectedOfferId === offer.id;
|
||||||
|
|
||||||
@@ -255,7 +256,7 @@ async function buildShopPanel(
|
|||||||
type: 9,
|
type: 9,
|
||||||
components: [{
|
components: [{
|
||||||
type: 10,
|
type: 10,
|
||||||
content: `${icon} **${item.name || item.key}**${selectedMark}\n💰 ${price}${stockText}`
|
content: `${label}${selectedMark}\n💰 ${price}${stockText}`
|
||||||
}],
|
}],
|
||||||
accessory: {
|
accessory: {
|
||||||
type: 2,
|
type: 2,
|
||||||
@@ -370,8 +371,9 @@ async function handleButtonInteraction(
|
|||||||
const result = await buyFromOffer(userId, guildId, selectedOfferId, qty);
|
const result = await buyFromOffer(userId, guildId, selectedOfferId, qty);
|
||||||
const wallet = await getOrCreateWallet(userId, guildId);
|
const wallet = await getOrCreateWallet(userId, guildId);
|
||||||
|
|
||||||
|
const purchaseLabel = formatItemLabel(result.item, { fallbackIcon: resolveItemIcon(result.item.icon) });
|
||||||
await interaction.followUp({
|
await interaction.followUp({
|
||||||
content: `✅ **Compra exitosa!**\n🛒 ${result.item.name || result.item.key} x${result.qty}\n💰 Te quedan: ${wallet.coins} monedas`,
|
content: `✅ **Compra exitosa!**\n🛒 ${purchaseLabel} x${result.qty}\n💰 Te quedan: ${wallet.coins} monedas`,
|
||||||
flags: MessageFlags.Ephemeral
|
flags: MessageFlags.Ephemeral
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user