feat: Add lootbox item type with weighted rewards and dedicated UI for item usage results.

This commit is contained in:
syntaxbullet
2026-01-05 12:52:34 +01:00
parent 5606fb6e2f
commit 599684cde8
6 changed files with 109 additions and 10 deletions

View File

@@ -2,6 +2,10 @@ import { levelingService } from "@/modules/leveling/leveling.service";
import { economyService } from "@/modules/economy/economy.service";
import { userTimers } from "@/db/schema";
import type { EffectHandler } from "./types";
import type { LootTableItem } from "@/lib/types";
import { inventoryService } from "@/modules/inventory/inventory.service";
import { inventory, items } from "@/db/schema";
// Helper to extract duration in seconds
const getDuration = (effect: any): number => {
@@ -60,3 +64,73 @@ export const handleTempRole: EffectHandler = async (userId, effect, txFn) => {
export const handleColorRole: EffectHandler = async (_userId, _effect, _txFn) => {
return "Color Role Equipped";
};
export const handleLootbox: EffectHandler = async (userId, effect, txFn) => {
const pool = effect.pool as LootTableItem[];
if (!pool || pool.length === 0) return "The box is empty...";
const totalWeight = pool.reduce((sum, item) => sum + item.weight, 0);
let random = Math.random() * totalWeight;
let winner: LootTableItem | null = null;
for (const item of pool) {
if (random < item.weight) {
winner = item;
break;
}
random -= item.weight;
}
if (!winner) return "The box is empty..."; // Should not happen
// Process Winner
if (winner.type === 'NOTHING') {
return winner.message || "You found nothing inside.";
}
if (winner.type === 'CURRENCY') {
let amount = winner.amount || 0;
if (winner.minAmount && winner.maxAmount) {
amount = Math.floor(Math.random() * (winner.maxAmount - winner.minAmount + 1)) + winner.minAmount;
}
if (amount > 0) {
await economyService.modifyUserBalance(userId, BigInt(amount), 'LOOTBOX', 'Lootbox Reward', null, txFn);
return winner.message || `You found ${amount} 🪙!`;
}
}
if (winner.type === 'XP') {
let amount = winner.amount || 0;
if (winner.minAmount && winner.maxAmount) {
amount = Math.floor(Math.random() * (winner.maxAmount - winner.minAmount + 1)) + winner.minAmount;
}
if (amount > 0) {
await levelingService.addXp(userId, BigInt(amount), txFn);
return winner.message || `You gained ${amount} XP!`;
}
}
if (winner.type === 'ITEM') {
if (winner.itemId) {
const quantity = BigInt(winner.amount || 1);
await inventoryService.addItem(userId, winner.itemId, quantity, txFn);
// Try to fetch item name for the message
try {
const item = await txFn.query.items.findFirst({
where: (items, { eq }) => eq(items.id, winner.itemId!)
});
if (item) {
return winner.message || `You found ${quantity > 1 ? quantity + 'x ' : ''}**${item.name}**!`;
}
} catch (e) {
console.error("Failed to fetch item name for lootbox message", e);
}
return winner.message || `You found an item! (ID: ${winner.itemId})`;
}
}
return "You found nothing suitable inside.";
};

View File

@@ -4,7 +4,8 @@ import {
handleReplyMessage,
handleXpBoost,
handleTempRole,
handleColorRole
handleColorRole,
handleLootbox
} from "./handlers";
import type { EffectHandler } from "./types";
@@ -14,5 +15,6 @@ export const effectHandlers: Record<string, EffectHandler> = {
'REPLY_MESSAGE': handleReplyMessage,
'XP_BOOST': handleXpBoost,
'TEMP_ROLE': handleTempRole,
'COLOR_ROLE': handleColorRole
'COLOR_ROLE': handleColorRole,
'LOOTBOX': handleLootbox
};