forked from syntaxbullet/AuroraBot-discord
feat: Add lootbox item type with weighted rewards and dedicated UI for item usage results.
This commit is contained in:
@@ -58,7 +58,7 @@ export const use = createCommand({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const embed = getItemUseResultEmbed(result.results);
|
const embed = getItemUseResultEmbed(result.results, result.item);
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [embed] });
|
await interaction.editReply({ embeds: [embed] });
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,18 @@ export type ItemEffect =
|
|||||||
| { type: 'XP_BOOST'; multiplier: number; durationSeconds?: number; durationMinutes?: number; durationHours?: number }
|
| { type: 'XP_BOOST'; multiplier: number; durationSeconds?: number; durationMinutes?: number; durationHours?: number }
|
||||||
| { type: 'TEMP_ROLE'; roleId: string; durationSeconds?: number; durationMinutes?: number; durationHours?: number }
|
| { type: 'TEMP_ROLE'; roleId: string; durationSeconds?: number; durationMinutes?: number; durationHours?: number }
|
||||||
| { type: 'REPLY_MESSAGE'; message: string }
|
| { type: 'REPLY_MESSAGE'; message: string }
|
||||||
| { type: 'COLOR_ROLE'; roleId: string };
|
| { type: 'COLOR_ROLE'; roleId: string }
|
||||||
|
| { type: 'LOOTBOX'; pool: LootTableItem[] };
|
||||||
|
|
||||||
|
export interface LootTableItem {
|
||||||
|
type: 'CURRENCY' | 'ITEM' | 'XP' | 'NOTHING';
|
||||||
|
weight: number;
|
||||||
|
amount?: number; // For CURRENCY, XP
|
||||||
|
itemId?: number; // For ITEM
|
||||||
|
minAmount?: number; // Optional range for CURRENCY/XP
|
||||||
|
maxAmount?: number; // Optional range for CURRENCY/XP
|
||||||
|
message?: string; // Optional custom message for this outcome
|
||||||
|
}
|
||||||
|
|
||||||
export interface ItemUsageData {
|
export interface ItemUsageData {
|
||||||
consume: boolean;
|
consume: boolean;
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import { levelingService } from "@/modules/leveling/leveling.service";
|
|||||||
import { economyService } from "@/modules/economy/economy.service";
|
import { economyService } from "@/modules/economy/economy.service";
|
||||||
import { userTimers } from "@/db/schema";
|
import { userTimers } from "@/db/schema";
|
||||||
import type { EffectHandler } from "./types";
|
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
|
// Helper to extract duration in seconds
|
||||||
const getDuration = (effect: any): number => {
|
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) => {
|
export const handleColorRole: EffectHandler = async (_userId, _effect, _txFn) => {
|
||||||
return "Color Role Equipped";
|
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.";
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import {
|
|||||||
handleReplyMessage,
|
handleReplyMessage,
|
||||||
handleXpBoost,
|
handleXpBoost,
|
||||||
handleTempRole,
|
handleTempRole,
|
||||||
handleColorRole
|
handleColorRole,
|
||||||
|
handleLootbox
|
||||||
} from "./handlers";
|
} from "./handlers";
|
||||||
import type { EffectHandler } from "./types";
|
import type { EffectHandler } from "./types";
|
||||||
|
|
||||||
@@ -14,5 +15,6 @@ export const effectHandlers: Record<string, EffectHandler> = {
|
|||||||
'REPLY_MESSAGE': handleReplyMessage,
|
'REPLY_MESSAGE': handleReplyMessage,
|
||||||
'XP_BOOST': handleXpBoost,
|
'XP_BOOST': handleXpBoost,
|
||||||
'TEMP_ROLE': handleTempRole,
|
'TEMP_ROLE': handleTempRole,
|
||||||
'COLOR_ROLE': handleColorRole
|
'COLOR_ROLE': handleColorRole,
|
||||||
|
'LOOTBOX': handleLootbox
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -159,7 +159,6 @@ export const inventoryService = {
|
|||||||
|
|
||||||
const results: string[] = [];
|
const results: string[] = [];
|
||||||
|
|
||||||
// 2. Apply Effects
|
|
||||||
// 2. Apply Effects
|
// 2. Apply Effects
|
||||||
const { effectHandlers } = await import("./effects/registry");
|
const { effectHandlers } = await import("./effects/registry");
|
||||||
|
|
||||||
@@ -179,7 +178,7 @@ export const inventoryService = {
|
|||||||
await inventoryService.removeItem(userId, itemId, 1n, txFn);
|
await inventoryService.removeItem(userId, itemId, 1n, txFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, results, usageData };
|
return { success: true, results, usageData, item };
|
||||||
}, tx);
|
}, tx);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,11 +30,24 @@ export function getInventoryEmbed(items: InventoryEntry[], username: string): Em
|
|||||||
/**
|
/**
|
||||||
* Creates an embed showing the results of using an item
|
* Creates an embed showing the results of using an item
|
||||||
*/
|
*/
|
||||||
export function getItemUseResultEmbed(results: string[], itemName?: string): EmbedBuilder {
|
export function getItemUseResultEmbed(results: string[], item?: { name: string, iconUrl: string | null, usageData: any }): EmbedBuilder {
|
||||||
const description = results.map(r => `• ${r}`).join("\n");
|
const description = results.map(r => `• ${r}`).join("\n");
|
||||||
|
|
||||||
return new EmbedBuilder()
|
// Check if it was a lootbox
|
||||||
.setTitle("✅ Item Used!")
|
const isLootbox = item?.usageData?.effects?.some((e: any) => e.type === 'LOOTBOX');
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
.setDescription(description)
|
.setDescription(description)
|
||||||
.setColor(0x2ecc71); // Green/Success
|
.setColor(isLootbox ? 0xFFD700 : 0x2ecc71); // Gold for lootbox, Green otherwise
|
||||||
|
|
||||||
|
if (isLootbox && item) {
|
||||||
|
embed.setTitle(`🎁 ${item.name} Opened!`);
|
||||||
|
if (item.iconUrl) {
|
||||||
|
embed.setThumbnail(item.iconUrl);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
embed.setTitle(item ? `✅ Used ${item.name}` : "✅ Item Used!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return embed;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user