forked from syntaxbullet/AuroraBot-discord
feat: add /use command for inventory items with effects, implement XP boosts, and enhance scheduler for temporary role removal.
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { inventory, items, users } from "@/db/schema";
|
||||
import { inventory, items, users, userTimers } from "@/db/schema";
|
||||
import { eq, and, sql, count } from "drizzle-orm";
|
||||
import { DrizzleClient } from "@/lib/DrizzleClient";
|
||||
import { economyService } from "@/modules/economy/economy.service";
|
||||
import { levelingService } from "@/modules/leveling/leveling.service";
|
||||
import { config } from "@/lib/config";
|
||||
import { withTransaction } from "@/lib/db";
|
||||
import type { Transaction } from "@/lib/types";
|
||||
import type { Transaction, ItemUsageData } from "@/lib/types";
|
||||
|
||||
export const inventoryService = {
|
||||
addItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => {
|
||||
@@ -130,4 +131,84 @@ export const inventoryService = {
|
||||
where: eq(items.id, itemId),
|
||||
});
|
||||
},
|
||||
|
||||
useItem: async (userId: string, itemId: number, tx?: Transaction) => {
|
||||
return await withTransaction(async (txFn) => {
|
||||
// 1. Check Ownership & Quantity
|
||||
const entry = await txFn.query.inventory.findFirst({
|
||||
where: and(
|
||||
eq(inventory.userId, BigInt(userId)),
|
||||
eq(inventory.itemId, itemId)
|
||||
),
|
||||
with: { item: true }
|
||||
});
|
||||
|
||||
if (!entry || (entry.quantity ?? 0n) < 1n) {
|
||||
throw new Error("You do not own this item.");
|
||||
}
|
||||
|
||||
const item = entry.item;
|
||||
const usageData = item.usageData as ItemUsageData | null;
|
||||
|
||||
if (!usageData || !usageData.effects || usageData.effects.length === 0) {
|
||||
throw new Error("This item cannot be used.");
|
||||
}
|
||||
|
||||
const results: string[] = [];
|
||||
|
||||
// 2. Apply Effects
|
||||
for (const effect of usageData.effects) {
|
||||
switch (effect.type) {
|
||||
case 'ADD_XP':
|
||||
await levelingService.addXp(userId, BigInt(effect.amount), txFn);
|
||||
results.push(`Gained ${effect.amount} XP`);
|
||||
break;
|
||||
case 'ADD_BALANCE':
|
||||
await economyService.modifyUserBalance(userId, BigInt(effect.amount), 'ITEM_USE', `Used ${item.name}`, null, txFn);
|
||||
results.push(`Gained ${effect.amount} 🪙`);
|
||||
break;
|
||||
case 'REPLY_MESSAGE':
|
||||
results.push(effect.message);
|
||||
break;
|
||||
case 'XP_BOOST':
|
||||
const expiresAt = new Date(Date.now() + effect.durationSeconds * 1000);
|
||||
await txFn.insert(userTimers).values({
|
||||
userId: BigInt(userId),
|
||||
type: 'EFFECT',
|
||||
key: 'xp_boost',
|
||||
expiresAt: expiresAt,
|
||||
metadata: { multiplier: effect.multiplier }
|
||||
}).onConflictDoUpdate({
|
||||
target: [userTimers.userId, userTimers.type, userTimers.key],
|
||||
set: { expiresAt: expiresAt, metadata: { multiplier: effect.multiplier } }
|
||||
});
|
||||
results.push(`XP Boost (${effect.multiplier}x) active for ${Math.floor(effect.durationSeconds / 60)}m`);
|
||||
break;
|
||||
case 'TEMP_ROLE':
|
||||
const roleExpiresAt = new Date(Date.now() + effect.durationSeconds * 1000);
|
||||
await txFn.insert(userTimers).values({
|
||||
userId: BigInt(userId),
|
||||
type: 'ACCESS',
|
||||
key: `role_${effect.roleId}`,
|
||||
expiresAt: roleExpiresAt,
|
||||
metadata: { roleId: effect.roleId }
|
||||
}).onConflictDoUpdate({
|
||||
target: [userTimers.userId, userTimers.type, userTimers.key],
|
||||
set: { expiresAt: roleExpiresAt }
|
||||
});
|
||||
// Actual role assignment happens in the Command layer (or here if we had client, but service shouldn't depend on client ideally)
|
||||
// We return a flag to let the interaction handler know it needs to assign a role.
|
||||
results.push(`Temporary Role granted for ${Math.floor(effect.durationSeconds / 60)}m`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Consume
|
||||
if (usageData.consume) {
|
||||
await inventoryService.removeItem(userId, itemId, 1n, txFn);
|
||||
}
|
||||
|
||||
return { success: true, results, usageData };
|
||||
}, tx);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -65,7 +65,21 @@ export const levelingService = {
|
||||
}
|
||||
|
||||
// Calculate random XP
|
||||
const amount = BigInt(Math.floor(Math.random() * (config.leveling.chat.maxXp - config.leveling.chat.minXp + 1)) + config.leveling.chat.minXp);
|
||||
let amount = BigInt(Math.floor(Math.random() * (config.leveling.chat.maxXp - config.leveling.chat.minXp + 1)) + config.leveling.chat.minXp);
|
||||
|
||||
// Check for XP Boost
|
||||
const xpBoost = await txFn.query.userTimers.findFirst({
|
||||
where: and(
|
||||
eq(userTimers.userId, BigInt(id)),
|
||||
eq(userTimers.type, 'EFFECT'),
|
||||
eq(userTimers.key, 'xp_boost')
|
||||
)
|
||||
});
|
||||
|
||||
if (xpBoost && xpBoost.expiresAt > now) {
|
||||
const multiplier = (xpBoost.metadata as any)?.multiplier || 1;
|
||||
amount = BigInt(Math.floor(Number(amount) * multiplier));
|
||||
}
|
||||
|
||||
// Add XP
|
||||
const result = await levelingService.addXp(id, amount, txFn);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { userTimers } from "@/db/schema";
|
||||
import { eq, and, lt } from "drizzle-orm";
|
||||
import { DrizzleClient } from "@/lib/DrizzleClient";
|
||||
import { KyokoClient } from "@/lib/BotClient";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
/**
|
||||
* The Janitor responsible for cleaning up expired ACCESS timers
|
||||
@@ -38,9 +40,30 @@ export const schedulerService = {
|
||||
console.log(`🧹 Janitor: Found ${expiredAccess.length} expired access timers.`);
|
||||
|
||||
for (const timer of expiredAccess) {
|
||||
// TODO: Here we would call Discord API to remove roles/overwrites.
|
||||
const meta = timer.metadata as any;
|
||||
console.log(`🚫 Revoking access for User ${timer.userId}: Key=${timer.key} (Channel: ${meta?.channelId || 'N/A'})`);
|
||||
const userIdStr = timer.userId.toString();
|
||||
|
||||
// Specific Handling for Roles
|
||||
if (timer.key.startsWith('role_')) {
|
||||
try {
|
||||
const roleId = meta?.roleId || timer.key.replace('role_', '');
|
||||
const guildId = env.DISCORD_GUILD_ID;
|
||||
|
||||
if (guildId) {
|
||||
// We try to fetch, if bot is not in guild or lacks perms, it will catch
|
||||
const guild = await KyokoClient.guilds.fetch(guildId);
|
||||
const member = await guild.members.fetch(userIdStr);
|
||||
await member.roles.remove(roleId);
|
||||
console.log(`👋 Removed temporary role ${roleId} from ${member.user.tag}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to remove role for user ${userIdStr}:`, err);
|
||||
// We still delete the timer so we don't loop forever on a left user
|
||||
}
|
||||
} else {
|
||||
console.log(`🚫 Revoking access for User ${timer.userId}: Key=${timer.key} (Channel: ${meta?.channelId || 'N/A'})`);
|
||||
// TODO: Generic channel permission removal if needed
|
||||
}
|
||||
|
||||
// Delete the timer row
|
||||
await DrizzleClient.delete(userTimers)
|
||||
|
||||
Reference in New Issue
Block a user