feat(economy): add commands for opening chests, consuming items, purchasing from shops, crafting, enchanting, equipping items, smelting, and claiming smelting jobs

This commit is contained in:
2025-10-05 02:08:59 -05:00
parent dcc8e20840
commit a0da2ffa98
13 changed files with 352 additions and 14 deletions

View File

@@ -0,0 +1,39 @@
import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { openChestByKey } from '../../../game/economy/service';
export const command: CommandMessage = {
name: 'abrir',
type: 'message',
aliases: ['open'],
cooldown: 3,
description: 'Abre un cofre (item) por key y recibe sus recompensas/roles.',
usage: 'abrir <itemKey>',
run: async (message, args, _client: Amayo) => {
const itemKey = args[0]?.trim();
if (!itemKey) { await message.reply('Uso: `!abrir <itemKey>`'); return; }
try {
const res = await openChestByKey(message.author.id, message.guild!.id, itemKey);
const coins = res.coinsDelta ? `🪙 +${res.coinsDelta}` : '';
const items = res.itemsToAdd.length ? res.itemsToAdd.map(i => `${i.itemKey ?? i.itemId} x${i.qty}`).join(' · ') : '';
let rolesGiven: string[] = [];
let rolesFailed: string[] = [];
if (res.rolesToGrant.length && message.member) {
for (const r of res.rolesToGrant) {
try { await message.member.roles.add(r); rolesGiven.push(r); } catch { rolesFailed.push(r); }
}
}
const lines = [
`🎁 Abriste ${itemKey}${res.consumed ? ' (consumido 1)' : ''}`,
coins && `Monedas: ${coins}`,
items && `Ítems: ${items}`,
rolesGiven.length ? `Roles otorgados: ${rolesGiven.map(id=>`<@&${id}>`).join(', ')}` : '',
rolesFailed.length ? `Roles fallidos: ${rolesFailed.join(', ')}` : '',
].filter(Boolean);
await message.reply(lines.join('\n'));
} catch (e: any) {
await message.reply(`❌ No se pudo abrir ${itemKey}: ${e?.message ?? e}`);
}
}
};

View File

@@ -0,0 +1,23 @@
import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { useConsumableByKey } from '../../../game/consumables/service';
export const command: CommandMessage = {
name: 'comer',
type: 'message',
aliases: ['usar-comida','usar'],
cooldown: 3,
description: 'Usa un ítem consumible (comida/poción) para curarte. Respeta cooldowns.',
usage: 'comer <itemKey>',
run: async (message, args, _client: Amayo) => {
const itemKey = args[0]?.trim();
if (!itemKey) { await message.reply('Uso: `!comer <itemKey>`'); return; }
try {
const res = await useConsumableByKey(message.author.id, message.guild!.id, itemKey);
await message.reply(`🍽️ Usaste ${itemKey}. Curado: +${res.healed} HP.`);
} catch (e: any) {
await message.reply(`❌ No se pudo usar ${itemKey}: ${e?.message ?? e}`);
}
}
};

View File

@@ -0,0 +1,24 @@
import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { buyFromOffer } from '../../../game/economy/service';
export const command: CommandMessage = {
name: 'comprar',
type: 'message',
aliases: ['buy'],
cooldown: 3,
description: 'Compra una oferta de la tienda por su ID. Respeta límites y stock.',
usage: 'comprar <offerId> [qty] (ej: comprar off_123 2)',
run: async (message, args, _client: Amayo) => {
const offerId = args[0]?.trim();
const qty = Math.max(1, parseInt(args[1] || '1', 10) || 1);
if (!offerId) { await message.reply('Uso: `!comprar <offerId> [qty]`'); return; }
try {
const res = await buyFromOffer(message.author.id, message.guild!.id, offerId, qty);
await message.reply(`🛒 Comprado: ${res.item.key} x${res.qty}`);
} catch (e: any) {
await message.reply(`❌ No se pudo comprar: ${e?.message ?? e}`);
}
}
};

View File

@@ -0,0 +1,35 @@
import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { craftByProductKey } from '../../../game/economy/service';
export const command: CommandMessage = {
name: 'craftear',
type: 'message',
aliases: ['craft'],
cooldown: 3,
description: 'Craftea un ítem por su productKey, consumiendo ingredientes según la receta.',
usage: 'craftear <productKey> [veces] (ej: craftear ingot.iron 3)',
run: async (message, args, _client: Amayo) => {
const productKey = args[0]?.trim();
const times = Math.max(1, parseInt(args[1] || '1', 10) || 1);
if (!productKey) { await message.reply('Uso: `!craftear <productKey> [veces]`'); return; }
let crafted = 0;
let lastError: any = null;
for (let i = 0; i < times; i++) {
try {
const res = await craftByProductKey(message.author.id, message.guild!.id, productKey);
crafted += res.added;
} catch (e: any) {
lastError = e; break;
}
}
if (crafted > 0) {
await message.reply(`🛠️ Crafteado ${productKey} x${crafted * 1}.`);
} else {
await message.reply(`❌ No se pudo craftear: ${lastError?.message ?? 'revise ingredientes/receta'}`);
}
}
};

View File

@@ -0,0 +1,24 @@
import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { applyMutationToInventory } from '../../../game/mutations/service';
export const command: CommandMessage = {
name: 'encantar',
type: 'message',
aliases: ['mutar','enchant'],
cooldown: 3,
description: 'Aplica una mutación/encantamiento a un ítem por su itemKey y mutationKey, respetando mutationPolicy.',
usage: 'encantar <itemKey> <mutationKey>',
run: async (message, args, _client: Amayo) => {
const itemKey = args[0]?.trim();
const mutationKey = args[1]?.trim();
if (!itemKey || !mutationKey) { await message.reply('Uso: `!encantar <itemKey> <mutationKey>`'); return; }
try {
await applyMutationToInventory(message.author.id, message.guild!.id, itemKey, mutationKey);
await message.reply(`✨ Aplicada mutación ${mutationKey} a ${itemKey}.`);
} catch (e: any) {
await message.reply(`❌ No se pudo encantar: ${e?.message ?? e}`);
}
}
};

View File

@@ -0,0 +1,32 @@
import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { setEquipmentSlot } from '../../../game/combat/equipmentService';
import { prisma } from '../../../core/database/prisma';
export const command: CommandMessage = {
name: 'equipar',
type: 'message',
aliases: ['equip'],
cooldown: 3,
description: 'Equipa un item en un slot (weapon|armor|cape) por su key, si lo tienes en inventario.',
usage: 'equipar <weapon|armor|cape> <itemKey>',
run: async (message, args, _client: Amayo) => {
const slot = (args[0]?.trim()?.toLowerCase() as 'weapon'|'armor'|'cape'|undefined);
const itemKey = args[1]?.trim();
if (!slot || !['weapon','armor','cape'].includes(slot) || !itemKey) {
await message.reply('Uso: `!equipar <weapon|armor|cape> <itemKey>`');
return;
}
const guildId = message.guild!.id;
const userId = message.author.id;
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; }
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; }
await setEquipmentSlot(userId, guildId, slot, item.id);
await message.reply(`🧰 Equipado en ${slot}: ${item.key}`);
}
};

View File

@@ -0,0 +1,40 @@
import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { createSmeltJob } from '../../../game/smelting/service';
export const command: CommandMessage = {
name: 'fundir',
type: 'message',
aliases: ['smelt'],
cooldown: 5,
description: 'Crea un job de fundición: descuenta insumos y estará listo tras el tiempo indicado.',
usage: 'fundir <outputKey> <outputQty> <segundos> <inputKey1>:<qty> [inputKey2>:<qty> ...]'
+ '\nEj: fundir ingot.iron 1 60 ore.iron:3',
run: async (message, args, _client: Amayo) => {
const [outputKey, qtyStr, secsStr, ...rest] = args;
if (!outputKey || !qtyStr || !secsStr || rest.length === 0) {
await message.reply('Uso: `!fundir <outputKey> <outputQty> <segundos> <inputKey1>:<qty> [...]`');
return;
}
const outputQty = parseInt(qtyStr, 10);
const seconds = parseInt(secsStr, 10);
if (!Number.isFinite(outputQty) || outputQty <= 0 || !Number.isFinite(seconds) || seconds <= 0) {
await message.reply('❌ Cantidades/segundos inválidos.');
return;
}
const inputs = rest.map((tok) => {
const [k, q] = tok.split(':');
return { itemKey: (k || '').trim(), qty: Math.max(1, parseInt((q || '1'), 10) || 1) };
}).filter(x => x.itemKey);
if (!inputs.length) { await message.reply('❌ Debes especificar al menos un insumo como key:qty'); return; }
try {
const res = await createSmeltJob(message.author.id, message.guild!.id, inputs, outputKey, outputQty, seconds);
const when = new Date(res.readyAt).toLocaleTimeString('es-ES', { hour12: false });
await message.reply(`🔥 Fundición creada (job: ${res.jobId}). Estará lista a las ${when}.`);
} catch (e: any) {
await message.reply(`❌ No se pudo crear la fundición: ${e?.message ?? e}`);
}
}
};

View File

@@ -0,0 +1,28 @@
import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { claimSmeltJob, claimNextReadyJob } from '../../../game/smelting/service';
export const command: CommandMessage = {
name: 'fundir-reclamar',
type: 'message',
aliases: ['smelt-claim','reclamar-fundicion'],
cooldown: 3,
description: 'Reclama una fundición lista por jobId o la más antigua lista si no especificas id.',
usage: 'fundir-reclamar [jobId]'
+ '\nSin argumentos intenta reclamar la más antigua lista.',
run: async (message, args, _client: Amayo) => {
const jobId = args[0]?.trim();
try {
if (jobId) {
await claimSmeltJob(message.author.id, message.guild!.id, jobId);
await message.reply(`✅ Fundición reclamada (job ${jobId}).`);
} else {
const res = await claimNextReadyJob(message.author.id, message.guild!.id);
await message.reply(`✅ Fundición reclamada (job ${res.jobId}).`);
}
} catch (e: any) {
await message.reply(`❌ No se pudo reclamar: ${e?.message ?? e}`);
}
}
};

View File

@@ -41,7 +41,6 @@ export const command: CommandMessage = {
const editorMsg = await message.channel.send({
content: `👾 Editor de Mob (editar): \`${key}\``,
flags: MessageFlags.IsComponentsV2,
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' },
@@ -98,4 +97,3 @@ async function showJsonModal(i: ButtonInteraction, state: MobEditorState, field:
else { state[field] = field==='stats' ? { attack: 5 } : {}; await sub.reply({ content: ' Limpio.', flags: MessageFlags.Ephemeral }); }
} catch {}
}

View File

@@ -0,0 +1,17 @@
import type { CommandMessage } from '../../../core/types/commands';
import type Amayo from '../../../core/client';
import { getOrCreateWallet } from '../../../game/economy/service';
export const command: CommandMessage = {
name: 'monedas',
type: 'message',
aliases: ['coins','saldo'],
cooldown: 2,
description: 'Muestra tu saldo de monedas en este servidor.',
usage: 'monedas',
run: async (message, _args, _client: Amayo) => {
const wallet = await getOrCreateWallet(message.author.id, message.guild!.id);
await message.reply(`💰 Monedas: ${wallet.coins}`);
}
};