Refactor economy service and types for improved readability and functionality
- Reformatted code for consistent styling and indentation in service.ts - Enhanced item reward structure in types.ts to include probability for chest rewards - Added randomization modes for chest rewards: 'all', 'single', and 'roll-each' - Updated functions in service.ts to handle new reward structures and improve error handling - Ensured better organization and clarity in function parameters and return types
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
import { prisma } from '../../core/database/prisma';
|
||||
import type { ItemProps, InventoryState, Price, OpenChestResult } from './types';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { ensureUserAndGuildExist } from '../core/userService';
|
||||
import { prisma } from "../../core/database/prisma";
|
||||
import type {
|
||||
ItemProps,
|
||||
InventoryState,
|
||||
Price,
|
||||
OpenChestResult,
|
||||
} from "./types";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import { ensureUserAndGuildExist } from "../core/userService";
|
||||
|
||||
// Utilidades de tiempo
|
||||
function now(): Date {
|
||||
@@ -24,7 +29,7 @@ export async function findItemByKey(guildId: string, key: string) {
|
||||
},
|
||||
orderBy: [
|
||||
// preferir coincidencia del servidor
|
||||
{ guildId: 'desc' },
|
||||
{ guildId: "desc" },
|
||||
],
|
||||
});
|
||||
return item;
|
||||
@@ -33,7 +38,7 @@ export async function findItemByKey(guildId: string, key: string) {
|
||||
export async function getOrCreateWallet(userId: string, guildId: string) {
|
||||
// Asegurar que User y Guild existan antes de crear/buscar wallet
|
||||
await ensureUserAndGuildExist(userId, guildId);
|
||||
|
||||
|
||||
return prisma.economyWallet.upsert({
|
||||
where: { userId_guildId: { userId, guildId } },
|
||||
update: {},
|
||||
@@ -41,7 +46,11 @@ export async function getOrCreateWallet(userId: string, guildId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function adjustCoins(userId: string, guildId: string, delta: number) {
|
||||
export async function adjustCoins(
|
||||
userId: string,
|
||||
guildId: string,
|
||||
delta: number
|
||||
) {
|
||||
const wallet = await getOrCreateWallet(userId, guildId);
|
||||
const next = Math.max(0, wallet.coins + delta);
|
||||
return prisma.economyWallet.update({
|
||||
@@ -52,16 +61,28 @@ export async function adjustCoins(userId: string, guildId: string, delta: number
|
||||
|
||||
export type EnsureInventoryOptions = { createIfMissing?: boolean };
|
||||
|
||||
export async function getInventoryEntryByItemId(userId: string, guildId: string, itemId: string, opts?: EnsureInventoryOptions) {
|
||||
export async function getInventoryEntryByItemId(
|
||||
userId: string,
|
||||
guildId: string,
|
||||
itemId: string,
|
||||
opts?: EnsureInventoryOptions
|
||||
) {
|
||||
const existing = await prisma.inventoryEntry.findUnique({
|
||||
where: { userId_guildId_itemId: { userId, guildId, itemId } },
|
||||
});
|
||||
if (existing) return existing;
|
||||
if (!opts?.createIfMissing) return null;
|
||||
return prisma.inventoryEntry.create({ data: { userId, guildId, itemId, quantity: 0 } });
|
||||
return prisma.inventoryEntry.create({
|
||||
data: { userId, guildId, itemId, quantity: 0 },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getInventoryEntry(userId: string, guildId: string, itemKey: string, opts?: EnsureInventoryOptions) {
|
||||
export async function getInventoryEntry(
|
||||
userId: string,
|
||||
guildId: string,
|
||||
itemKey: string,
|
||||
opts?: EnsureInventoryOptions
|
||||
) {
|
||||
const item = await findItemByKey(guildId, itemKey);
|
||||
if (!item) throw new Error(`Item key not found: ${itemKey}`);
|
||||
const entry = await getInventoryEntryByItemId(userId, guildId, item.id, opts);
|
||||
@@ -69,40 +90,57 @@ export async function getInventoryEntry(userId: string, guildId: string, itemKey
|
||||
}
|
||||
|
||||
function parseItemProps(json: unknown): ItemProps {
|
||||
if (!json || typeof json !== 'object') return {};
|
||||
if (!json || typeof json !== "object") return {};
|
||||
return json as ItemProps;
|
||||
}
|
||||
|
||||
function parseState(json: unknown): InventoryState {
|
||||
if (!json || typeof json !== 'object') return {};
|
||||
if (!json || typeof json !== "object") return {};
|
||||
return json as InventoryState;
|
||||
}
|
||||
|
||||
function checkUsableWindow(item: { usableFrom: Date | null; usableTo: Date | null; props: any }) {
|
||||
function checkUsableWindow(item: {
|
||||
usableFrom: Date | null;
|
||||
usableTo: Date | null;
|
||||
props: any;
|
||||
}) {
|
||||
const props = parseItemProps(item.props);
|
||||
const from = props.usableFrom ? new Date(props.usableFrom) : item.usableFrom;
|
||||
const to = props.usableTo ? new Date(props.usableTo) : item.usableTo;
|
||||
if (!isWithin(now(), from ?? null, to ?? null)) {
|
||||
throw new Error('Item no usable por ventana de tiempo');
|
||||
throw new Error("Item no usable por ventana de tiempo");
|
||||
}
|
||||
}
|
||||
|
||||
function checkAvailableWindow(item: { availableFrom: Date | null; availableTo: Date | null; props: any }) {
|
||||
function checkAvailableWindow(item: {
|
||||
availableFrom: Date | null;
|
||||
availableTo: Date | null;
|
||||
props: any;
|
||||
}) {
|
||||
const props = parseItemProps(item.props);
|
||||
const from = props.availableFrom ? new Date(props.availableFrom) : item.availableFrom;
|
||||
const from = props.availableFrom
|
||||
? new Date(props.availableFrom)
|
||||
: item.availableFrom;
|
||||
const to = props.availableTo ? new Date(props.availableTo) : item.availableTo;
|
||||
if (!isWithin(now(), from ?? null, to ?? null)) {
|
||||
throw new Error('Item no disponible para adquirir');
|
||||
throw new Error("Item no disponible para adquirir");
|
||||
}
|
||||
}
|
||||
|
||||
// Agrega cantidad respetando maxPerInventory y stackable
|
||||
export async function addItemByKey(userId: string, guildId: string, itemKey: string, qty: number) {
|
||||
export async function addItemByKey(
|
||||
userId: string,
|
||||
guildId: string,
|
||||
itemKey: string,
|
||||
qty: number
|
||||
) {
|
||||
if (qty <= 0) return { added: 0 } as const;
|
||||
const found = await getInventoryEntry(userId, guildId, itemKey, { createIfMissing: true });
|
||||
const found = await getInventoryEntry(userId, guildId, itemKey, {
|
||||
createIfMissing: true,
|
||||
});
|
||||
const item = found.item;
|
||||
const entry = found.entry;
|
||||
if (!entry) throw new Error('No se pudo crear/obtener inventario');
|
||||
if (!entry) throw new Error("No se pudo crear/obtener inventario");
|
||||
checkAvailableWindow(item);
|
||||
|
||||
const max = item.maxPerInventory ?? Number.MAX_SAFE_INTEGER;
|
||||
@@ -119,17 +157,28 @@ export async function addItemByKey(userId: string, guildId: string, itemKey: str
|
||||
// No apilable: usar state.instances
|
||||
const state = parseState(entry.state);
|
||||
state.instances ??= [];
|
||||
const canAdd = Math.max(0, Math.min(qty, Math.max(0, max - state.instances.length)));
|
||||
const canAdd = Math.max(
|
||||
0,
|
||||
Math.min(qty, Math.max(0, max - state.instances.length))
|
||||
);
|
||||
for (let i = 0; i < canAdd; i++) state.instances.push({});
|
||||
const updated = await prisma.inventoryEntry.update({
|
||||
where: { userId_guildId_itemId: { userId, guildId, itemId: item.id } },
|
||||
data: { state: state as unknown as Prisma.InputJsonValue, quantity: state.instances.length },
|
||||
data: {
|
||||
state: state as unknown as Prisma.InputJsonValue,
|
||||
quantity: state.instances.length,
|
||||
},
|
||||
});
|
||||
return { added: canAdd, entry: updated } as const;
|
||||
}
|
||||
}
|
||||
|
||||
export async function consumeItemByKey(userId: string, guildId: string, itemKey: string, qty: number) {
|
||||
export async function consumeItemByKey(
|
||||
userId: string,
|
||||
guildId: string,
|
||||
itemKey: string,
|
||||
qty: number
|
||||
) {
|
||||
if (qty <= 0) return { consumed: 0 } as const;
|
||||
const { item, entry } = await getInventoryEntry(userId, guildId, itemKey);
|
||||
if (!entry || (entry.quantity ?? 0) <= 0) return { consumed: 0 } as const;
|
||||
@@ -150,35 +199,100 @@ export async function consumeItemByKey(userId: string, guildId: string, itemKey:
|
||||
const newState: InventoryState = { ...state, instances };
|
||||
const updated = await prisma.inventoryEntry.update({
|
||||
where: { userId_guildId_itemId: { userId, guildId, itemId: item.id } },
|
||||
data: { state: newState as unknown as Prisma.InputJsonValue, quantity: instances.length },
|
||||
data: {
|
||||
state: newState as unknown as Prisma.InputJsonValue,
|
||||
quantity: instances.length,
|
||||
},
|
||||
});
|
||||
return { consumed, entry: updated } as const;
|
||||
}
|
||||
}
|
||||
|
||||
export async function openChestByKey(userId: string, guildId: string, itemKey: string): Promise<OpenChestResult> {
|
||||
export async function openChestByKey(
|
||||
userId: string,
|
||||
guildId: string,
|
||||
itemKey: string
|
||||
): Promise<OpenChestResult> {
|
||||
const { item, entry } = await getInventoryEntry(userId, guildId, itemKey);
|
||||
if (!entry || (entry.quantity ?? 0) <= 0) throw new Error('No tienes este cofre');
|
||||
if (!entry || (entry.quantity ?? 0) <= 0)
|
||||
throw new Error("No tienes este cofre");
|
||||
checkUsableWindow(item);
|
||||
|
||||
const props = parseItemProps(item.props);
|
||||
const chest = props.chest ?? {};
|
||||
if (!chest.enabled) throw new Error('Este ítem no se puede abrir');
|
||||
|
||||
if (!chest.enabled) throw new Error("Este ítem no se puede abrir");
|
||||
const rewards = Array.isArray(chest.rewards) ? chest.rewards : [];
|
||||
const result: OpenChestResult = { coinsDelta: 0, itemsToAdd: [], rolesToGrant: [], consumed: false };
|
||||
const mode = chest.randomMode || "all";
|
||||
const result: OpenChestResult = {
|
||||
coinsDelta: 0,
|
||||
itemsToAdd: [],
|
||||
rolesToGrant: [],
|
||||
consumed: false,
|
||||
};
|
||||
|
||||
for (const r of rewards) {
|
||||
if (r.type === 'coins') result.coinsDelta += Math.max(0, r.amount);
|
||||
else if (r.type === 'item') result.itemsToAdd.push({ itemKey: r.itemKey, itemId: r.itemId, qty: r.qty });
|
||||
else if (r.type === 'role') result.rolesToGrant.push(r.roleId);
|
||||
function pickOneWeighted<T extends { probability?: number }>(
|
||||
arr: T[]
|
||||
): T | null {
|
||||
const prepared = arr.map((a) => ({
|
||||
...a,
|
||||
_w: a.probability != null ? Math.max(0, a.probability) : 1,
|
||||
}));
|
||||
const total = prepared.reduce((s, a) => s + a._w, 0);
|
||||
if (total <= 0) return null;
|
||||
let r = Math.random() * total;
|
||||
for (const a of prepared) {
|
||||
r -= a._w;
|
||||
if (r <= 0) return a;
|
||||
}
|
||||
return prepared[prepared.length - 1] ?? null;
|
||||
}
|
||||
|
||||
if (mode === "single") {
|
||||
const one = pickOneWeighted(rewards);
|
||||
if (one) {
|
||||
if (one.type === "coins") result.coinsDelta += Math.max(0, one.amount);
|
||||
else if (one.type === "item")
|
||||
result.itemsToAdd.push({
|
||||
itemKey: one.itemKey,
|
||||
itemId: one.itemId,
|
||||
qty: one.qty,
|
||||
});
|
||||
else if (one.type === "role") result.rolesToGrant.push(one.roleId);
|
||||
}
|
||||
} else {
|
||||
// 'all' y 'roll-each': procesar cada reward con probabilidad (default 100%)
|
||||
for (const r of rewards) {
|
||||
const p = r.probability != null ? Math.max(0, r.probability) : 1; // p en [0,1] recomendado; si usan valores >1 se interpretan como peso
|
||||
// Si p > 1 asumimos error o peso -> para modo 'all' lo tratamos como 1 (100%)
|
||||
const chance = p > 1 ? 1 : p; // normalizado
|
||||
if (Math.random() <= chance) {
|
||||
if (r.type === "coins") result.coinsDelta += Math.max(0, r.amount);
|
||||
else if (r.type === "item")
|
||||
result.itemsToAdd.push({
|
||||
itemKey: r.itemKey,
|
||||
itemId: r.itemId,
|
||||
qty: r.qty,
|
||||
});
|
||||
else if (r.type === "role") result.rolesToGrant.push(r.roleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Roles fijos adicionales en chest.roles
|
||||
if (Array.isArray(chest.roles) && chest.roles.length) {
|
||||
for (const roleId of chest.roles) {
|
||||
if (typeof roleId === "string" && roleId.length > 0)
|
||||
result.rolesToGrant.push(roleId);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.coinsDelta) await adjustCoins(userId, guildId, result.coinsDelta);
|
||||
for (const it of result.itemsToAdd) {
|
||||
if (it.itemKey) await addItemByKey(userId, guildId, it.itemKey, it.qty);
|
||||
else if (it.itemId) {
|
||||
const item = await prisma.economyItem.findUnique({ where: { id: it.itemId } });
|
||||
const item = await prisma.economyItem.findUnique({
|
||||
where: { id: it.itemId },
|
||||
});
|
||||
if (item) await addItemByKey(userId, guildId, item.key, it.qty);
|
||||
}
|
||||
}
|
||||
@@ -191,23 +305,29 @@ export async function openChestByKey(userId: string, guildId: string, itemKey: s
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function craftByProductKey(userId: string, guildId: string, productKey: string) {
|
||||
export async function craftByProductKey(
|
||||
userId: string,
|
||||
guildId: string,
|
||||
productKey: string
|
||||
) {
|
||||
const product = await findItemByKey(guildId, productKey);
|
||||
if (!product) throw new Error(`Producto no encontrado: ${productKey}`);
|
||||
const recipe = await prisma.itemRecipe.findUnique({
|
||||
where: { productItemId: product.id },
|
||||
include: { ingredients: true },
|
||||
});
|
||||
if (!recipe) throw new Error('No existe receta para este ítem');
|
||||
if (!recipe) throw new Error("No existe receta para este ítem");
|
||||
|
||||
// Verificar ingredientes suficientes
|
||||
const shortages: string[] = [];
|
||||
for (const ing of recipe.ingredients) {
|
||||
const inv = await prisma.inventoryEntry.findUnique({ where: { userId_guildId_itemId: { userId, guildId, itemId: ing.itemId } } });
|
||||
const inv = await prisma.inventoryEntry.findUnique({
|
||||
where: { userId_guildId_itemId: { userId, guildId, itemId: ing.itemId } },
|
||||
});
|
||||
const have = inv?.quantity ?? 0;
|
||||
if (have < ing.quantity) shortages.push(ing.itemId);
|
||||
}
|
||||
if (shortages.length) throw new Error('Ingredientes insuficientes');
|
||||
if (shortages.length) throw new Error("Ingredientes insuficientes");
|
||||
|
||||
// Consumir ingredientes
|
||||
for (const ing of recipe.ingredients) {
|
||||
@@ -218,17 +338,29 @@ export async function craftByProductKey(userId: string, guildId: string, product
|
||||
}
|
||||
|
||||
// Agregar producto
|
||||
const add = await addItemByKey(userId, guildId, product.key, recipe.productQuantity);
|
||||
const add = await addItemByKey(
|
||||
userId,
|
||||
guildId,
|
||||
product.key,
|
||||
recipe.productQuantity
|
||||
);
|
||||
return { added: add.added, product } as const;
|
||||
}
|
||||
|
||||
export async function buyFromOffer(userId: string, guildId: string, offerId: string, qty = 1) {
|
||||
if (qty <= 0) throw new Error('Cantidad inválida');
|
||||
export async function buyFromOffer(
|
||||
userId: string,
|
||||
guildId: string,
|
||||
offerId: string,
|
||||
qty = 1
|
||||
) {
|
||||
if (qty <= 0) throw new Error("Cantidad inválida");
|
||||
const offer = await prisma.shopOffer.findUnique({ where: { id: offerId } });
|
||||
if (!offer || offer.guildId !== guildId) throw new Error('Oferta no encontrada');
|
||||
if (!offer.enabled) throw new Error('Oferta deshabilitada');
|
||||
if (!offer || offer.guildId !== guildId)
|
||||
throw new Error("Oferta no encontrada");
|
||||
if (!offer.enabled) throw new Error("Oferta deshabilitada");
|
||||
const nowD = now();
|
||||
if (!isWithin(nowD, offer.startAt ?? null, offer.endAt ?? null)) throw new Error('Oferta fuera de fecha');
|
||||
if (!isWithin(nowD, offer.startAt ?? null, offer.endAt ?? null))
|
||||
throw new Error("Oferta fuera de fecha");
|
||||
|
||||
const price = (offer.price as unknown as Price) ?? {};
|
||||
// Limites
|
||||
@@ -238,19 +370,23 @@ export async function buyFromOffer(userId: string, guildId: string, offerId: str
|
||||
_sum: { qty: true },
|
||||
});
|
||||
const already = count._sum.qty ?? 0;
|
||||
if (already + qty > offer.perUserLimit) throw new Error('Excede el límite por usuario');
|
||||
if (already + qty > offer.perUserLimit)
|
||||
throw new Error("Excede el límite por usuario");
|
||||
}
|
||||
|
||||
if (offer.stock != null) {
|
||||
if (offer.stock < qty) throw new Error('Stock insuficiente');
|
||||
if (offer.stock < qty) throw new Error("Stock insuficiente");
|
||||
}
|
||||
|
||||
// Cobro: coins
|
||||
if (price.coins && price.coins > 0) {
|
||||
const wallet = await getOrCreateWallet(userId, guildId);
|
||||
const total = price.coins * qty;
|
||||
if (wallet.coins < total) throw new Error('Monedas insuficientes');
|
||||
await prisma.economyWallet.update({ where: { userId_guildId: { userId, guildId } }, data: { coins: wallet.coins - total } });
|
||||
if (wallet.coins < total) throw new Error("Monedas insuficientes");
|
||||
await prisma.economyWallet.update({
|
||||
where: { userId_guildId: { userId, guildId } },
|
||||
data: { coins: wallet.coins - total },
|
||||
});
|
||||
}
|
||||
// Cobro: items
|
||||
if (price.items && price.items.length) {
|
||||
@@ -265,9 +401,12 @@ export async function buyFromOffer(userId: string, guildId: string, offerId: str
|
||||
} else if (comp.itemId) {
|
||||
itemId = comp.itemId;
|
||||
}
|
||||
if (!itemId) throw new Error('Item de precio inválido');
|
||||
const inv = await prisma.inventoryEntry.findUnique({ where: { userId_guildId_itemId: { userId, guildId, itemId } } });
|
||||
if ((inv?.quantity ?? 0) < compQty) throw new Error('No tienes suficientes items para pagar');
|
||||
if (!itemId) throw new Error("Item de precio inválido");
|
||||
const inv = await prisma.inventoryEntry.findUnique({
|
||||
where: { userId_guildId_itemId: { userId, guildId, itemId } },
|
||||
});
|
||||
if ((inv?.quantity ?? 0) < compQty)
|
||||
throw new Error("No tienes suficientes items para pagar");
|
||||
}
|
||||
// si todo está ok, descontar
|
||||
for (const comp of price.items) {
|
||||
@@ -281,21 +420,31 @@ export async function buyFromOffer(userId: string, guildId: string, offerId: str
|
||||
itemId = comp.itemId;
|
||||
}
|
||||
if (!itemId) continue;
|
||||
await prisma.inventoryEntry.update({ where: { userId_guildId_itemId: { userId, guildId, itemId } }, data: { quantity: { decrement: compQty } } });
|
||||
await prisma.inventoryEntry.update({
|
||||
where: { userId_guildId_itemId: { userId, guildId, itemId } },
|
||||
data: { quantity: { decrement: compQty } },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Entregar producto
|
||||
const item = await prisma.economyItem.findUnique({ where: { id: offer.itemId } });
|
||||
if (!item) throw new Error('Ítem de oferta no existente');
|
||||
const item = await prisma.economyItem.findUnique({
|
||||
where: { id: offer.itemId },
|
||||
});
|
||||
if (!item) throw new Error("Ítem de oferta no existente");
|
||||
await addItemByKey(userId, guildId, item.key, qty);
|
||||
|
||||
// Registrar compra
|
||||
await prisma.shopPurchase.create({ data: { offerId: offer.id, userId, guildId, qty } });
|
||||
await prisma.shopPurchase.create({
|
||||
data: { offerId: offer.id, userId, guildId, qty },
|
||||
});
|
||||
|
||||
// Reducir stock global
|
||||
if (offer.stock != null) {
|
||||
await prisma.shopOffer.update({ where: { id: offer.id }, data: { stock: offer.stock - qty } });
|
||||
await prisma.shopOffer.update({
|
||||
where: { id: offer.id },
|
||||
data: { stock: offer.stock - qty },
|
||||
});
|
||||
}
|
||||
|
||||
return { ok: true, item, qty } as const;
|
||||
@@ -307,24 +456,35 @@ export async function buyFromOffer(userId: string, guildId: string, offerId: str
|
||||
export async function findMutationByKey(guildId: string, key: string) {
|
||||
return prisma.itemMutation.findFirst({
|
||||
where: { key, OR: [{ guildId }, { guildId: null }] },
|
||||
orderBy: [{ guildId: 'desc' }],
|
||||
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');
|
||||
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");
|
||||
|
||||
// Política de mutaciones
|
||||
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');
|
||||
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');
|
||||
if (!mutation) throw new Error("Mutación no encontrada");
|
||||
|
||||
// Registrar vínculo
|
||||
await prisma.inventoryItemMutation.create({ data: { inventoryId: entry.id, mutationId: mutation.id } });
|
||||
await prisma.inventoryItemMutation.create({
|
||||
data: { inventoryId: entry.id, mutationId: mutation.id },
|
||||
});
|
||||
return { ok: true } as const;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
export type PriceItemComponent = {
|
||||
itemKey?: string; // preferido para lookup
|
||||
itemId?: string; // fallback directo
|
||||
itemId?: string; // fallback directo
|
||||
qty: number;
|
||||
};
|
||||
|
||||
@@ -14,9 +14,15 @@ export type Price = {
|
||||
};
|
||||
|
||||
export type ChestReward =
|
||||
| { type: 'coins'; amount: number }
|
||||
| { type: 'item'; itemKey?: string; itemId?: string; qty: number }
|
||||
| { type: 'role'; roleId: string };
|
||||
| { type: "coins"; amount: number; probability?: number }
|
||||
| {
|
||||
type: "item";
|
||||
itemKey?: string;
|
||||
itemId?: string;
|
||||
qty: number;
|
||||
probability?: number;
|
||||
}
|
||||
| { type: "role"; roleId: string; probability?: number };
|
||||
|
||||
export type PassiveEffect = {
|
||||
key: string; // p.ej. "xpBoost", "defenseUp"
|
||||
@@ -38,8 +44,15 @@ export type CraftableProps = {
|
||||
|
||||
export type ChestProps = {
|
||||
enabled?: boolean;
|
||||
// Recompensas que el bot debe otorgar al "abrir"
|
||||
// Modo de randomización:
|
||||
// 'all' (default): se procesan todas las recompensas y cada una evalúa su probability (si no hay probability, se asume 100%).
|
||||
// 'single': selecciona UNA recompensa aleatoria ponderada por probability (o 1 si falta) y solo otorga esa.
|
||||
// 'roll-each': similar a 'all' pero probability se trata como chance independiente (igual que all; se mantiene por semántica futura).
|
||||
randomMode?: "all" | "single" | "roll-each";
|
||||
// Recompensas configuradas
|
||||
rewards?: ChestReward[];
|
||||
// Roles adicionales fijos (independientes de rewards)
|
||||
roles?: string[];
|
||||
// Si true, consume 1 del inventario al abrir
|
||||
consumeOnOpen?: boolean;
|
||||
};
|
||||
@@ -60,7 +73,7 @@ export type ShopProps = {
|
||||
};
|
||||
|
||||
export type ToolProps = {
|
||||
type: 'pickaxe' | 'rod' | 'sword' | 'bow' | 'halberd' | 'net' | string; // extensible
|
||||
type: "pickaxe" | "rod" | "sword" | "bow" | "halberd" | "net" | string; // extensible
|
||||
tier?: number; // nivel/calidad de la herramienta
|
||||
};
|
||||
|
||||
@@ -76,6 +89,8 @@ export type ItemProps = {
|
||||
breakable?: BreakableProps; // romperse
|
||||
craftable?: CraftableProps; // craftear
|
||||
chest?: ChestProps; // estilo cofre que al usar da roles/ítems/monedas
|
||||
// Si true, este ítem se considera global (guildId = null) y solo el owner del bot puede editarlo
|
||||
global?: boolean;
|
||||
eventCurrency?: EventCurrencyProps; // puede actuar como moneda de evento
|
||||
passiveEffects?: PassiveEffect[]; // efectos por tenerlo
|
||||
mutationPolicy?: MutationPolicy; // reglas para mutaciones extra
|
||||
|
||||
Reference in New Issue
Block a user