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:
39
src/commands/messages/game/abrir.ts
Normal file
39
src/commands/messages/game/abrir.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
23
src/commands/messages/game/comer.ts
Normal file
23
src/commands/messages/game/comer.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
24
src/commands/messages/game/comprar.ts
Normal file
24
src/commands/messages/game/comprar.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
35
src/commands/messages/game/craftear.ts
Normal file
35
src/commands/messages/game/craftear.ts
Normal 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'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
24
src/commands/messages/game/encantar.ts
Normal file
24
src/commands/messages/game/encantar.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
32
src/commands/messages/game/equipar.ts
Normal file
32
src/commands/messages/game/equipar.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
40
src/commands/messages/game/fundir.ts
Normal file
40
src/commands/messages/game/fundir.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
28
src/commands/messages/game/fundirReclamar.ts
Normal file
28
src/commands/messages/game/fundirReclamar.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@@ -41,7 +41,6 @@ export const command: CommandMessage = {
|
|||||||
|
|
||||||
const editorMsg = await message.channel.send({
|
const editorMsg = await message.channel.send({
|
||||||
content: `👾 Editor de Mob (editar): \`${key}\``,
|
content: `👾 Editor de Mob (editar): \`${key}\``,
|
||||||
flags: MessageFlags.IsComponentsV2,
|
|
||||||
components: [ { type: 1, components: [
|
components: [ { type: 1, components: [
|
||||||
{ type: 2, style: ButtonStyle.Primary, label: 'Base', custom_id: 'mb_base' },
|
{ 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: '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 }); }
|
else { state[field] = field==='stats' ? { attack: 5 } : {}; await sub.reply({ content: 'ℹ️ Limpio.', flags: MessageFlags.Ephemeral }); }
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
src/commands/messages/game/monedas.ts
Normal file
17
src/commands/messages/game/monedas.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@@ -4,6 +4,22 @@ import type { ItemProps, InventoryState } from '../economy/types';
|
|||||||
import type { LevelRequirements, RunMinigameOptions, RunResult, RewardsTable, MobsTable } from './types';
|
import type { LevelRequirements, RunMinigameOptions, RunResult, RewardsTable, MobsTable } from './types';
|
||||||
import type { Prisma } from '@prisma/client';
|
import type { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
// Auto-select best tool from inventory by type and constraints
|
||||||
|
async function findBestToolKey(userId: string, guildId: string, toolType: string, opts?: { minTier?: number; allowedKeys?: string[] }) {
|
||||||
|
const entries = await prisma.inventoryEntry.findMany({ where: { userId, guildId, quantity: { gt: 0 } }, include: { item: true } });
|
||||||
|
let best: { key: string; tier: number } | null = null;
|
||||||
|
for (const e of entries) {
|
||||||
|
const props = parseItemProps(e.item.props);
|
||||||
|
const t = props.tool;
|
||||||
|
if (!t || t.type !== toolType) continue;
|
||||||
|
const tier = Math.max(0, t.tier ?? 0);
|
||||||
|
if (opts?.minTier != null && tier < opts.minTier) continue;
|
||||||
|
if (opts?.allowedKeys && opts.allowedKeys.length && !opts.allowedKeys.includes(e.item.key)) continue;
|
||||||
|
if (!best || tier > best.tier) best = { key: e.item.key, tier };
|
||||||
|
}
|
||||||
|
return best?.key ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
function parseJSON<T>(v: unknown): T | null {
|
function parseJSON<T>(v: unknown): T | null {
|
||||||
if (!v || (typeof v !== 'object' && typeof v !== 'string')) return null;
|
if (!v || (typeof v !== 'object' && typeof v !== 'string')) return null;
|
||||||
return v as T;
|
return v as T;
|
||||||
@@ -44,23 +60,30 @@ async function validateRequirements(userId: string, guildId: string, req?: Level
|
|||||||
const toolReq = req.tool;
|
const toolReq = req.tool;
|
||||||
if (!toolReq) return { toolKeyUsed: undefined as string | undefined };
|
if (!toolReq) return { toolKeyUsed: undefined as string | undefined };
|
||||||
|
|
||||||
|
let toolKeyUsed = toolKey;
|
||||||
|
|
||||||
|
// Auto-select tool when required and not provided
|
||||||
|
if (!toolKeyUsed && toolReq.required && toolReq.toolType) {
|
||||||
|
toolKeyUsed = await findBestToolKey(userId, guildId, toolReq.toolType, { minTier: toolReq.minTier, allowedKeys: toolReq.allowedKeys });
|
||||||
|
}
|
||||||
|
|
||||||
// herramienta requerida
|
// herramienta requerida
|
||||||
if (toolReq.required && !toolKey) throw new Error('Se requiere una herramienta');
|
if (toolReq.required && !toolKeyUsed) throw new Error('Se requiere una herramienta adecuada');
|
||||||
if (!toolKey) return { toolKeyUsed: undefined };
|
if (!toolKeyUsed) return { toolKeyUsed: undefined };
|
||||||
|
|
||||||
// verificar herramienta
|
// verificar herramienta
|
||||||
const toolItem = await findItemByKey(guildId, toolKey);
|
const toolItem = await findItemByKey(guildId, toolKeyUsed);
|
||||||
if (!toolItem) throw new Error('Herramienta no encontrada');
|
if (!toolItem) throw new Error('Herramienta no encontrada');
|
||||||
const { entry } = await getInventoryEntry(userId, guildId, toolKey);
|
const { entry } = await getInventoryEntry(userId, guildId, toolKeyUsed);
|
||||||
if (!entry || (entry.quantity ?? 0) <= 0) throw new Error('No tienes la herramienta');
|
if (!entry || (entry.quantity ?? 0) <= 0) throw new Error('No tienes la herramienta');
|
||||||
|
|
||||||
const props = parseItemProps(toolItem.props);
|
const props = parseItemProps(toolItem.props);
|
||||||
const tool = props.tool;
|
const tool = props.tool;
|
||||||
if (toolReq.toolType && tool?.type !== toolReq.toolType) throw new Error('Tipo de herramienta incorrecto');
|
if (toolReq.toolType && tool?.type !== toolReq.toolType) throw new Error('Tipo de herramienta incorrecto');
|
||||||
if (toolReq.minTier != null && (tool?.tier ?? 0) < toolReq.minTier) throw new Error('Tier de herramienta insuficiente');
|
if (toolReq.minTier != null && (tool?.tier ?? 0) < toolReq.minTier) throw new Error('Tier de herramienta insuficiente');
|
||||||
if (toolReq.allowedKeys && !toolReq.allowedKeys.includes(toolKey)) throw new Error('Esta herramienta no es válida para esta área');
|
if (toolReq.allowedKeys && !toolReq.allowedKeys.includes(toolKeyUsed)) throw new Error('Esta herramienta no es válida para esta área');
|
||||||
|
|
||||||
return { toolKeyUsed: toolKey };
|
return { toolKeyUsed };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyRewards(userId: string, guildId: string, rewards?: RewardsTable): Promise<RunResult['rewards']> {
|
async function applyRewards(userId: string, guildId: string, rewards?: RewardsTable): Promise<RunResult['rewards']> {
|
||||||
@@ -206,3 +229,18 @@ export async function runMinigame(userId: string, guildId: string, areaKey: stri
|
|||||||
|
|
||||||
return { success: true, rewards: delivered, mobs: mobsSpawned, tool: toolInfo };
|
return { success: true, rewards: delivered, mobs: mobsSpawned, tool: toolInfo };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convenience wrappers with auto-level (from PlayerProgress) and auto-tool selection inside validateRequirements
|
||||||
|
export async function runMining(userId: string, guildId: string, level?: number, toolKey?: string) {
|
||||||
|
const area = await prisma.gameArea.findFirst({ where: { key: 'mine.cavern', OR: [{ guildId }, { guildId: null }] }, orderBy: [{ guildId: 'desc' }] });
|
||||||
|
if (!area) throw new Error('Área de mina no configurada');
|
||||||
|
const lvl = level ?? (await prisma.playerProgress.findUnique({ where: { userId_guildId_areaId: { userId, guildId, areaId: area.id } } }))?.highestLevel ?? 1;
|
||||||
|
return runMinigame(userId, guildId, 'mine.cavern', Math.max(1, lvl), { toolKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runFishing(userId: string, guildId: string, level?: number, toolKey?: string) {
|
||||||
|
const area = await prisma.gameArea.findFirst({ where: { key: 'lagoon.shore', OR: [{ guildId }, { guildId: null }] }, orderBy: [{ guildId: 'desc' }] });
|
||||||
|
if (!area) throw new Error('Área de laguna no configurada');
|
||||||
|
const lvl = level ?? (await prisma.playerProgress.findUnique({ where: { userId_guildId_areaId: { userId, guildId, areaId: area.id } } }))?.highestLevel ?? 1;
|
||||||
|
return runMinigame(userId, guildId, 'lagoon.shore', Math.max(1, lvl), { toolKey });
|
||||||
|
}
|
||||||
|
|||||||
32
src/game/mutations/service.ts
Normal file
32
src/game/mutations/service.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { prisma } from '../../core/database/prisma';
|
||||||
|
import type { ItemProps } from '../economy/types';
|
||||||
|
import { findItemByKey, getInventoryEntry } from '../economy/service';
|
||||||
|
|
||||||
|
function parseItemProps(json: unknown): ItemProps {
|
||||||
|
if (!json || typeof json !== 'object') return {};
|
||||||
|
return json as ItemProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findMutationByKey(guildId: string, key: string) {
|
||||||
|
return prisma.itemMutation.findFirst({
|
||||||
|
where: { key, OR: [{ guildId }, { guildId: null }] },
|
||||||
|
orderBy: [{ guildId: 'desc' }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyMutationToInventory(userId: string, guildId: string, itemKey: string, mutationKey: string) {
|
||||||
|
const { item, entry } = await getInventoryEntry(userId, guildId, itemKey, { createIfMissing: true });
|
||||||
|
if (!entry) throw new Error('Inventario inexistente');
|
||||||
|
|
||||||
|
const props = parseItemProps(item.props);
|
||||||
|
const policy = props.mutationPolicy;
|
||||||
|
if (policy?.deniedKeys?.includes(mutationKey)) throw new Error('Mutación denegada');
|
||||||
|
if (policy?.allowedKeys && !policy.allowedKeys.includes(mutationKey)) throw new Error('Mutación no permitida');
|
||||||
|
|
||||||
|
const mutation = await findMutationByKey(guildId, mutationKey);
|
||||||
|
if (!mutation) throw new Error('Mutación no encontrada');
|
||||||
|
|
||||||
|
await prisma.inventoryItemMutation.create({ data: { inventoryId: entry.id, mutationId: mutation.id } });
|
||||||
|
return { ok: true } as const;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -13,8 +13,8 @@ export async function createSmeltJob(userId: string, guildId: string, inputs: Sm
|
|||||||
const outItem = await findItemByKey(guildId, outputItemKey);
|
const outItem = await findItemByKey(guildId, outputItemKey);
|
||||||
if (!outItem) throw new Error('Output item no encontrado');
|
if (!outItem) throw new Error('Output item no encontrado');
|
||||||
|
|
||||||
|
let newJobId: string | null = null;
|
||||||
// Validar y descontar inputs
|
// Validar y descontar inputs
|
||||||
// Nota: para simplificar, chequeo y descuento debería ser transaccional; usamos una transacción
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
// Chequeo
|
// Chequeo
|
||||||
for (const i of inputs) {
|
for (const i of inputs) {
|
||||||
@@ -30,7 +30,7 @@ export async function createSmeltJob(userId: string, guildId: string, inputs: Sm
|
|||||||
await tx.inventoryEntry.update({ where: { userId_guildId_itemId: { userId, guildId, itemId: it.id } }, data: { quantity: { decrement: i.qty } } });
|
await tx.inventoryEntry.update({ where: { userId_guildId_itemId: { userId, guildId, itemId: it.id } }, data: { quantity: { decrement: i.qty } } });
|
||||||
}
|
}
|
||||||
// Crear job
|
// Crear job
|
||||||
await tx.smeltJob.create({
|
const created = await tx.smeltJob.create({
|
||||||
data: {
|
data: {
|
||||||
userId,
|
userId,
|
||||||
guildId,
|
guildId,
|
||||||
@@ -40,10 +40,12 @@ export async function createSmeltJob(userId: string, guildId: string, inputs: Sm
|
|||||||
readyAt,
|
readyAt,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
},
|
},
|
||||||
|
select: { id: true },
|
||||||
});
|
});
|
||||||
|
newJobId = created.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { readyAt } as const;
|
return { readyAt, jobId: newJobId! } as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function claimSmeltJob(userId: string, guildId: string, jobId: string) {
|
export async function claimSmeltJob(userId: string, guildId: string, jobId: string) {
|
||||||
@@ -52,13 +54,10 @@ export async function claimSmeltJob(userId: string, guildId: string, jobId: stri
|
|||||||
if (job.status !== 'pending' && job.status !== 'ready') throw new Error('Estado inválido');
|
if (job.status !== 'pending' && job.status !== 'ready') throw new Error('Estado inválido');
|
||||||
if (job.readyAt > new Date()) throw new Error('Aún no está listo');
|
if (job.readyAt > new Date()) throw new Error('Aún no está listo');
|
||||||
|
|
||||||
// Otorgar outputs y marcar claimed
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
await tx.smeltJob.update({ where: { id: job.id }, data: { status: 'claimed' } });
|
await tx.smeltJob.update({ where: { id: job.id }, data: { status: 'claimed' } });
|
||||||
const outItem = await tx.economyItem.findUnique({ where: { id: job.outputItemId } });
|
const outItem = await tx.economyItem.findUnique({ where: { id: job.outputItemId } });
|
||||||
if (outItem) {
|
if (outItem) {
|
||||||
// usamos servicio economy por fuera de la transacción (para evitar nested client); hacemos simple aquí
|
|
||||||
// añadir con tx: replicamos addItem
|
|
||||||
const inv = await tx.inventoryEntry.findUnique({ where: { userId_guildId_itemId: { userId, guildId, itemId: outItem.id } } });
|
const inv = await tx.inventoryEntry.findUnique({ where: { userId_guildId_itemId: { userId, guildId, itemId: outItem.id } } });
|
||||||
if (inv) {
|
if (inv) {
|
||||||
await tx.inventoryEntry.update({ where: { userId_guildId_itemId: { userId, guildId, itemId: outItem.id } }, data: { quantity: { increment: job.outputQty } } });
|
await tx.inventoryEntry.update({ where: { userId_guildId_itemId: { userId, guildId, itemId: outItem.id } }, data: { quantity: { increment: job.outputQty } } });
|
||||||
@@ -71,3 +70,12 @@ export async function claimSmeltJob(userId: string, guildId: string, jobId: stri
|
|||||||
return { ok: true } as const;
|
return { ok: true } as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function claimNextReadyJob(userId: string, guildId: string) {
|
||||||
|
const job = await prisma.smeltJob.findFirst({
|
||||||
|
where: { userId, guildId, status: { in: ['pending', 'ready'] }, readyAt: { lte: new Date() } },
|
||||||
|
orderBy: { readyAt: 'asc' },
|
||||||
|
});
|
||||||
|
if (!job) throw new Error('No hay jobs listos');
|
||||||
|
await claimSmeltJob(userId, guildId, job.id);
|
||||||
|
return { ok: true, jobId: job.id } as const;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user