docs: add JSDoc to service public methods
One-line JSDoc on 82 methods across 11 service files for quick scanning without reading full implementations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,10 +6,12 @@ import { withTransaction } from "@/lib/db";
|
|||||||
import type { Transaction } from "@shared/lib/types";
|
import type { Transaction } from "@shared/lib/types";
|
||||||
|
|
||||||
export const classService = {
|
export const classService = {
|
||||||
|
/** Retrieve all available classes. */
|
||||||
getAllClasses: async () => {
|
getAllClasses: async () => {
|
||||||
return await DrizzleClient.query.classes.findMany();
|
return await DrizzleClient.query.classes.findMany();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Assign a class to a user; throws if the class does not exist. */
|
||||||
assignClass: async (userId: string, classId: bigint, tx?: Transaction) => {
|
assignClass: async (userId: string, classId: bigint, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
const cls = await txFn.query.classes.findFirst({
|
const cls = await txFn.query.classes.findFirst({
|
||||||
@@ -26,12 +28,14 @@ export const classService = {
|
|||||||
return user;
|
return user;
|
||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
/** Get the current balance for a class, returning 0 if not found. */
|
||||||
getClassBalance: async (classId: bigint) => {
|
getClassBalance: async (classId: bigint) => {
|
||||||
const cls = await DrizzleClient.query.classes.findFirst({
|
const cls = await DrizzleClient.query.classes.findFirst({
|
||||||
where: eq(classes.id, classId),
|
where: eq(classes.id, classId),
|
||||||
});
|
});
|
||||||
return cls?.balance || 0n;
|
return cls?.balance || 0n;
|
||||||
},
|
},
|
||||||
|
/** Adjust a class balance by the given amount; throws if funds are insufficient for a deduction. */
|
||||||
modifyClassBalance: async (classId: bigint, amount: bigint, tx?: Transaction) => {
|
modifyClassBalance: async (classId: bigint, amount: bigint, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
const cls = await txFn.query.classes.findFirst({
|
const cls = await txFn.query.classes.findFirst({
|
||||||
@@ -55,6 +59,7 @@ export const classService = {
|
|||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Update a class record with partial data. */
|
||||||
updateClass: async (id: bigint, data: Partial<typeof classes.$inferInsert>, tx?: Transaction) => {
|
updateClass: async (id: bigint, data: Partial<typeof classes.$inferInsert>, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
const [updatedClass] = await txFn.update(classes)
|
const [updatedClass] = await txFn.update(classes)
|
||||||
@@ -65,6 +70,7 @@ export const classService = {
|
|||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Create a new class record. */
|
||||||
createClass: async (data: typeof classes.$inferInsert, tx?: Transaction) => {
|
createClass: async (data: typeof classes.$inferInsert, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
const [newClass] = await txFn.insert(classes)
|
const [newClass] = await txFn.insert(classes)
|
||||||
@@ -74,6 +80,7 @@ export const classService = {
|
|||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Delete a class by ID. */
|
||||||
deleteClass: async (id: bigint, tx?: Transaction) => {
|
deleteClass: async (id: bigint, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
await txFn.delete(classes).where(eq(classes.id, id));
|
await txFn.delete(classes).where(eq(classes.id, id));
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { UserError } from "@shared/lib/errors";
|
|||||||
import { TimerKey, TimerType, TransactionType } from "@shared/lib/constants";
|
import { TimerKey, TimerType, TransactionType } from "@shared/lib/constants";
|
||||||
|
|
||||||
export const economyService = {
|
export const economyService = {
|
||||||
|
/** Transfer currency between two users; validates sufficient balance and creates transaction records for both sides. */
|
||||||
transfer: async (fromUserId: string, toUserId: string, amount: bigint, tx?: Transaction) => {
|
transfer: async (fromUserId: string, toUserId: string, amount: bigint, tx?: Transaction) => {
|
||||||
if (amount <= 0n) {
|
if (amount <= 0n) {
|
||||||
throw new UserError("Amount must be positive");
|
throw new UserError("Amount must be positive");
|
||||||
@@ -69,6 +70,7 @@ export const economyService = {
|
|||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Claim the daily reward, applying streak bonuses and weekly bonuses; enforces a UTC-midnight cooldown. */
|
||||||
claimDaily: async (userId: string, tx?: Transaction) => {
|
claimDaily: async (userId: string, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -164,6 +166,7 @@ export const economyService = {
|
|||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Adjust a user's balance by the given amount and log a transaction; throws if deduction exceeds current balance. */
|
||||||
modifyUserBalance: async (id: string, amount: bigint, type: string, description: string, relatedUserId?: string | null, tx?: Transaction) => {
|
modifyUserBalance: async (id: string, amount: bigint, type: string, description: string, relatedUserId?: string | null, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
if (amount < 0n) {
|
if (amount < 0n) {
|
||||||
|
|||||||
@@ -213,12 +213,20 @@ async function clearCaches() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const lootdropService = {
|
export const lootdropService = {
|
||||||
|
/** Delete expired lootdrops from the database; optionally includes already-claimed ones. */
|
||||||
cleanupExpiredLootdrops,
|
cleanupExpiredLootdrops,
|
||||||
|
/** Record a message in a channel and return whether a lootdrop should spawn based on activity and RNG. */
|
||||||
trackActivity,
|
trackActivity,
|
||||||
|
/** Calculate a random lootdrop reward amount and currency, with optional overrides. */
|
||||||
calculateReward,
|
calculateReward,
|
||||||
|
/** Save a spawned lootdrop to the database with a 10-minute expiration. */
|
||||||
persistLootdrop,
|
persistLootdrop,
|
||||||
|
/** Remove a lootdrop by message ID and return its channel ID for Discord cleanup. */
|
||||||
removeLootdrop,
|
removeLootdrop,
|
||||||
|
/** Atomically claim a lootdrop for a user; credits reward to their balance. */
|
||||||
tryClaim,
|
tryClaim,
|
||||||
|
/** Get current lootdrop system state including the most active channel and spawn config. */
|
||||||
getLootdropState,
|
getLootdropState,
|
||||||
|
/** Clear all in-memory activity tracking and cooldown caches. */
|
||||||
clearCaches,
|
clearCaches,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface FeatureFlagContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const featureFlagsService = {
|
export const featureFlagsService = {
|
||||||
|
/** Check whether a feature flag is enabled globally. */
|
||||||
isFlagEnabled: async (flagName: string): Promise<boolean> => {
|
isFlagEnabled: async (flagName: string): Promise<boolean> => {
|
||||||
const flag = await DrizzleClient.query.featureFlags.findFirst({
|
const flag = await DrizzleClient.query.featureFlags.findFirst({
|
||||||
where: eq(featureFlags.name, flagName),
|
where: eq(featureFlags.name, flagName),
|
||||||
@@ -17,6 +18,7 @@ export const featureFlagsService = {
|
|||||||
return flag?.enabled ?? false;
|
return flag?.enabled ?? false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Check if a guild/user/role has access to a feature flag; returns false if flag is disabled. */
|
||||||
hasAccess: async (
|
hasAccess: async (
|
||||||
flagName: string,
|
flagName: string,
|
||||||
context: FeatureFlagContext
|
context: FeatureFlagContext
|
||||||
@@ -51,6 +53,7 @@ export const featureFlagsService = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Create a new feature flag, disabled by default. */
|
||||||
createFlag: async (name: string, description?: string) => {
|
createFlag: async (name: string, description?: string) => {
|
||||||
const [flag] = await DrizzleClient.insert(featureFlags).values({
|
const [flag] = await DrizzleClient.insert(featureFlags).values({
|
||||||
name,
|
name,
|
||||||
@@ -60,6 +63,7 @@ export const featureFlagsService = {
|
|||||||
return flag;
|
return flag;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Enable or disable a feature flag by name; throws if the flag does not exist. */
|
||||||
setFlagEnabled: async (name: string, enabled: boolean) => {
|
setFlagEnabled: async (name: string, enabled: boolean) => {
|
||||||
const [flag] = await DrizzleClient.update(featureFlags)
|
const [flag] = await DrizzleClient.update(featureFlags)
|
||||||
.set({ enabled, updatedAt: new Date() })
|
.set({ enabled, updatedAt: new Date() })
|
||||||
@@ -72,6 +76,7 @@ export const featureFlagsService = {
|
|||||||
return flag;
|
return flag;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Grant a guild, user, or role access to a feature flag. */
|
||||||
grantAccess: async (
|
grantAccess: async (
|
||||||
flagName: string,
|
flagName: string,
|
||||||
access: { guildId?: string; userId?: string; roleId?: string }
|
access: { guildId?: string; userId?: string; roleId?: string }
|
||||||
@@ -90,6 +95,7 @@ export const featureFlagsService = {
|
|||||||
return accessRecord;
|
return accessRecord;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Revoke a specific access record by ID; throws if not found. */
|
||||||
revokeAccess: async (accessId: number) => {
|
revokeAccess: async (accessId: number) => {
|
||||||
const [access] = await DrizzleClient.delete(featureFlagAccess)
|
const [access] = await DrizzleClient.delete(featureFlagAccess)
|
||||||
.where(eq(featureFlagAccess.id, accessId))
|
.where(eq(featureFlagAccess.id, accessId))
|
||||||
@@ -101,18 +107,21 @@ export const featureFlagsService = {
|
|||||||
return access;
|
return access;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Retrieve a single feature flag by name. */
|
||||||
getFlag: async (name: string) => {
|
getFlag: async (name: string) => {
|
||||||
return await DrizzleClient.query.featureFlags.findFirst({
|
return await DrizzleClient.query.featureFlags.findFirst({
|
||||||
where: eq(featureFlags.name, name),
|
where: eq(featureFlags.name, name),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** List all feature flags, ordered by name. */
|
||||||
listFlags: async () => {
|
listFlags: async () => {
|
||||||
return await DrizzleClient.query.featureFlags.findMany({
|
return await DrizzleClient.query.featureFlags.findMany({
|
||||||
orderBy: (flags, { asc }) => [asc(flags.name)],
|
orderBy: (flags, { asc }) => [asc(flags.name)],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** List all access records for a given feature flag. */
|
||||||
listAccess: async (flagName: string) => {
|
listAccess: async (flagName: string) => {
|
||||||
const flag = await DrizzleClient.query.featureFlags.findFirst({
|
const flag = await DrizzleClient.query.featureFlags.findFirst({
|
||||||
where: eq(featureFlags.name, flagName),
|
where: eq(featureFlags.name, flagName),
|
||||||
@@ -125,6 +134,7 @@ export const featureFlagsService = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Delete a feature flag and its associated access records; throws if not found. */
|
||||||
deleteFlag: async (name: string) => {
|
deleteFlag: async (name: string) => {
|
||||||
const [flag] = await DrizzleClient.delete(featureFlags)
|
const [flag] = await DrizzleClient.delete(featureFlags)
|
||||||
.where(eq(featureFlags.name, name))
|
.where(eq(featureFlags.name, name))
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ let cacheTimestamp = 0;
|
|||||||
const CACHE_TTL_MS = 30000;
|
const CACHE_TTL_MS = 30000;
|
||||||
|
|
||||||
export const gameSettingsService = {
|
export const gameSettingsService = {
|
||||||
|
/** Retrieve game settings, using a 30-second TTL cache by default. */
|
||||||
getSettings: async (useCache = true): Promise<GameSettingsData | null> => {
|
getSettings: async (useCache = true): Promise<GameSettingsData | null> => {
|
||||||
if (useCache && cachedSettings && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
|
if (useCache && cachedSettings && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
|
||||||
return cachedSettings;
|
return cachedSettings;
|
||||||
@@ -56,6 +57,7 @@ export const gameSettingsService = {
|
|||||||
return cachedSettings;
|
return cachedSettings;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Create or update game settings, merging with existing values and invalidating cache. */
|
||||||
upsertSettings: async (data: Partial<GameSettingsData>) => {
|
upsertSettings: async (data: Partial<GameSettingsData>) => {
|
||||||
const existing = await gameSettingsService.getSettings(false);
|
const existing = await gameSettingsService.getSettings(false);
|
||||||
|
|
||||||
@@ -86,6 +88,7 @@ export const gameSettingsService = {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Update a single configuration section (e.g., "leveling", "economy") and invalidate cache. */
|
||||||
updateSection: async <K extends keyof GameSettingsData>(
|
updateSection: async <K extends keyof GameSettingsData>(
|
||||||
section: K,
|
section: K,
|
||||||
value: GameSettingsData[K]
|
value: GameSettingsData[K]
|
||||||
@@ -102,6 +105,7 @@ export const gameSettingsService = {
|
|||||||
gameSettingsService.invalidateCache();
|
gameSettingsService.invalidateCache();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Enable or disable a specific command in the game settings. */
|
||||||
toggleCommand: async (commandName: string, enabled: boolean) => {
|
toggleCommand: async (commandName: string, enabled: boolean) => {
|
||||||
const settings = await gameSettingsService.getSettings(false);
|
const settings = await gameSettingsService.getSettings(false);
|
||||||
|
|
||||||
@@ -117,11 +121,13 @@ export const gameSettingsService = {
|
|||||||
await gameSettingsService.updateSection("commands", commands);
|
await gameSettingsService.updateSection("commands", commands);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Invalidate the in-memory settings cache, forcing a fresh DB read on next access. */
|
||||||
invalidateCache: () => {
|
invalidateCache: () => {
|
||||||
cachedSettings = null;
|
cachedSettings = null;
|
||||||
cacheTimestamp = 0;
|
cacheTimestamp = 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Return default leveling configuration values. */
|
||||||
getDefaultLeveling: (): LevelingConfig => ({
|
getDefaultLeveling: (): LevelingConfig => ({
|
||||||
base: 100,
|
base: 100,
|
||||||
exponent: 1.5,
|
exponent: 1.5,
|
||||||
@@ -132,6 +138,7 @@ export const gameSettingsService = {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/** Return default economy configuration values. */
|
||||||
getDefaultEconomy: (): EconomyConfig => ({
|
getDefaultEconomy: (): EconomyConfig => ({
|
||||||
daily: {
|
daily: {
|
||||||
amount: "100",
|
amount: "100",
|
||||||
@@ -149,11 +156,13 @@ export const gameSettingsService = {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/** Return default inventory configuration values. */
|
||||||
getDefaultInventory: (): InventoryConfig => ({
|
getDefaultInventory: (): InventoryConfig => ({
|
||||||
maxStackSize: "99",
|
maxStackSize: "99",
|
||||||
maxSlots: 20,
|
maxSlots: 20,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/** Return default lootdrop configuration values. */
|
||||||
getDefaultLootdrop: (): LootdropConfig => ({
|
getDefaultLootdrop: (): LootdropConfig => ({
|
||||||
activityWindowMs: 300000,
|
activityWindowMs: 300000,
|
||||||
minMessages: 5,
|
minMessages: 5,
|
||||||
@@ -166,6 +175,7 @@ export const gameSettingsService = {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/** Return default trivia configuration values. */
|
||||||
getDefaultTrivia: (): TriviaConfig => ({
|
getDefaultTrivia: (): TriviaConfig => ({
|
||||||
entryFee: "50",
|
entryFee: "50",
|
||||||
rewardMultiplier: 1.8,
|
rewardMultiplier: 1.8,
|
||||||
@@ -175,6 +185,7 @@ export const gameSettingsService = {
|
|||||||
difficulty: "random",
|
difficulty: "random",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/** Return default moderation configuration values. */
|
||||||
getDefaultModeration: (): ModerationConfig => ({
|
getDefaultModeration: (): ModerationConfig => ({
|
||||||
prune: {
|
prune: {
|
||||||
maxAmount: 100,
|
maxAmount: 100,
|
||||||
@@ -184,10 +195,12 @@ export const gameSettingsService = {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/** Return default quest configuration values. */
|
||||||
getDefaultQuest: (): QuestConfig => ({
|
getDefaultQuest: (): QuestConfig => ({
|
||||||
maxActiveQuests: 3,
|
maxActiveQuests: 3,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
/** Return the complete set of default game settings across all sections. */
|
||||||
getDefaults: (): GameSettingsData => ({
|
getDefaults: (): GameSettingsData => ({
|
||||||
leveling: gameSettingsService.getDefaultLeveling(),
|
leveling: gameSettingsService.getDefaultLeveling(),
|
||||||
economy: gameSettingsService.getDefaultEconomy(),
|
economy: gameSettingsService.getDefaultEconomy(),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface GuildSettingsData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const guildSettingsService = {
|
export const guildSettingsService = {
|
||||||
|
/** Retrieve guild settings by guild ID, or null if none exist. */
|
||||||
getSettings: async (guildId: string): Promise<GuildSettingsData | null> => {
|
getSettings: async (guildId: string): Promise<GuildSettingsData | null> => {
|
||||||
const settings = await DrizzleClient.query.guildSettings.findFirst({
|
const settings = await DrizzleClient.query.guildSettings.findFirst({
|
||||||
where: eq(guildSettings.guildId, BigInt(guildId)),
|
where: eq(guildSettings.guildId, BigInt(guildId)),
|
||||||
@@ -44,6 +45,7 @@ export const guildSettingsService = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Create or fully replace guild settings via upsert. */
|
||||||
upsertSettings: async (data: Partial<GuildSettingsData> & { guildId: string }) => {
|
upsertSettings: async (data: Partial<GuildSettingsData> & { guildId: string }) => {
|
||||||
const values: typeof guildSettings.$inferInsert = {
|
const values: typeof guildSettings.$inferInsert = {
|
||||||
guildId: BigInt(data.guildId),
|
guildId: BigInt(data.guildId),
|
||||||
@@ -73,6 +75,7 @@ export const guildSettingsService = {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Update a single guild setting by key name; throws if the key is unknown or settings do not exist. */
|
||||||
updateSetting: async (
|
updateSetting: async (
|
||||||
guildId: string,
|
guildId: string,
|
||||||
key: string,
|
key: string,
|
||||||
@@ -128,6 +131,7 @@ export const guildSettingsService = {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Delete all settings for a guild. */
|
||||||
deleteSettings: async (guildId: string) => {
|
deleteSettings: async (guildId: string) => {
|
||||||
const [result] = await DrizzleClient.delete(guildSettings)
|
const [result] = await DrizzleClient.delete(guildSettings)
|
||||||
.where(eq(guildSettings.guildId, BigInt(guildId)))
|
.where(eq(guildSettings.guildId, BigInt(guildId)))
|
||||||
@@ -136,6 +140,7 @@ export const guildSettingsService = {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Add a color role to the guild's allowed list; no-ops if already present. */
|
||||||
addColorRole: async (guildId: string, roleId: string) => {
|
addColorRole: async (guildId: string, roleId: string) => {
|
||||||
const settings = await guildSettingsService.getSettings(guildId);
|
const settings = await guildSettingsService.getSettings(guildId);
|
||||||
const colorRoleIds = settings?.colorRoleIds ?? [];
|
const colorRoleIds = settings?.colorRoleIds ?? [];
|
||||||
@@ -148,6 +153,7 @@ export const guildSettingsService = {
|
|||||||
return await guildSettingsService.upsertSettings({ guildId, colorRoleIds });
|
return await guildSettingsService.upsertSettings({ guildId, colorRoleIds });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Remove a color role from the guild's allowed list. */
|
||||||
removeColorRole: async (guildId: string, roleId: string) => {
|
removeColorRole: async (guildId: string, roleId: string) => {
|
||||||
const settings = await guildSettingsService.getSettings(guildId);
|
const settings = await guildSettingsService.getSettings(guildId);
|
||||||
if (!settings) return null;
|
if (!settings) return null;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { TransactionType } from "@shared/lib/constants";
|
|||||||
|
|
||||||
|
|
||||||
export const inventoryService = {
|
export const inventoryService = {
|
||||||
|
/** Add items to a user's inventory; enforces max stack size and max slot limits. */
|
||||||
addItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => {
|
addItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
// Check if item exists in inventory
|
// Check if item exists in inventory
|
||||||
@@ -74,6 +75,7 @@ export const inventoryService = {
|
|||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Remove items from a user's inventory; deletes the entry if quantity reaches zero. */
|
||||||
removeItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => {
|
removeItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
const existing = await txFn.query.inventory.findFirst({
|
const existing = await txFn.query.inventory.findFirst({
|
||||||
@@ -110,6 +112,7 @@ export const inventoryService = {
|
|||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Retrieve all inventory entries for a user, including item details. */
|
||||||
getInventory: async (userId: string) => {
|
getInventory: async (userId: string) => {
|
||||||
return await DrizzleClient.query.inventory.findMany({
|
return await DrizzleClient.query.inventory.findMany({
|
||||||
where: eq(inventory.userId, BigInt(userId)),
|
where: eq(inventory.userId, BigInt(userId)),
|
||||||
@@ -119,6 +122,7 @@ export const inventoryService = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Purchase an item from the shop; deducts balance and adds to inventory atomically. */
|
||||||
buyItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => {
|
buyItem: async (userId: string, itemId: number, quantity: bigint = 1n, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
const item = await txFn.query.items.findFirst({
|
const item = await txFn.query.items.findFirst({
|
||||||
@@ -139,12 +143,14 @@ export const inventoryService = {
|
|||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Fetch a single item definition by ID. */
|
||||||
getItem: async (itemId: number) => {
|
getItem: async (itemId: number) => {
|
||||||
return await DrizzleClient.query.items.findFirst({
|
return await DrizzleClient.query.items.findFirst({
|
||||||
where: eq(items.id, itemId),
|
where: eq(items.id, itemId),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Use a consumable item, applying its effects and consuming it if configured. */
|
||||||
useItem: async (userId: string, itemId: number, tx?: Transaction) => {
|
useItem: async (userId: string, itemId: number, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
// 1. Check Ownership & Quantity
|
// 1. Check Ownership & Quantity
|
||||||
@@ -189,6 +195,7 @@ export const inventoryService = {
|
|||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Search a user's usable inventory items by name for autocomplete suggestions. */
|
||||||
getAutocompleteItems: async (userId: string, query: string) => {
|
getAutocompleteItems: async (userId: string, query: string) => {
|
||||||
const entries = await DrizzleClient.select({
|
const entries = await DrizzleClient.select({
|
||||||
quantity: inventory.quantity,
|
quantity: inventory.quantity,
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ import { TimerKey, TimerType } from "@shared/lib/constants";
|
|||||||
import { UserError } from "@shared/lib/errors";
|
import { UserError } from "@shared/lib/errors";
|
||||||
|
|
||||||
export const levelingService = {
|
export const levelingService = {
|
||||||
// Calculate total XP required to REACH a specific level (Cumulative)
|
/** Calculate the cumulative XP required to reach a specific level. */
|
||||||
// Level 1 = 0 XP
|
|
||||||
// Level 2 = Base * (1^Exp)
|
|
||||||
// Level 3 = Level 2 + Base * (2^Exp)
|
|
||||||
// ...
|
|
||||||
getXpToReachLevel: (level: number) => {
|
getXpToReachLevel: (level: number) => {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
for (let l = 1; l < level; l++) {
|
for (let l = 1; l < level; l++) {
|
||||||
@@ -21,7 +17,7 @@ export const levelingService = {
|
|||||||
return total;
|
return total;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Calculate level from Total XP
|
/** Derive a user's level from their total accumulated XP. */
|
||||||
getLevelFromXp: (totalXp: bigint) => {
|
getLevelFromXp: (totalXp: bigint) => {
|
||||||
let level = 1;
|
let level = 1;
|
||||||
let xp = Number(totalXp);
|
let xp = Number(totalXp);
|
||||||
@@ -37,13 +33,12 @@ export const levelingService = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get XP needed to complete the current level (for calculating next level threshold in isolation)
|
/** Get the XP needed to advance from the given level to the next. */
|
||||||
// Used internally or for display
|
|
||||||
getXpForNextLevel: (currentLevel: number) => {
|
getXpForNextLevel: (currentLevel: number) => {
|
||||||
return Math.floor(config.leveling.base * Math.pow(currentLevel, config.leveling.exponent));
|
return Math.floor(config.leveling.base * Math.pow(currentLevel, config.leveling.exponent));
|
||||||
},
|
},
|
||||||
|
|
||||||
// Cumulative XP addition
|
/** Add XP to a user, recalculating their level and emitting a quest event. */
|
||||||
addXp: async (id: string, amount: bigint, tx?: Transaction) => {
|
addXp: async (id: string, amount: bigint, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
// Get current state
|
// Get current state
|
||||||
@@ -77,7 +72,7 @@ export const levelingService = {
|
|||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Handle chat XP with cooldowns
|
/** Award random chat XP if the user is not on cooldown; applies active XP boost multipliers. */
|
||||||
processChatXp: async (id: string, tx?: Transaction) => {
|
processChatXp: async (id: string, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
// check if an xp cooldown is in place
|
// check if an xp cooldown is in place
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { TransactionType } from "@shared/lib/constants";
|
|||||||
import { systemEvents, EVENTS } from "@shared/lib/events";
|
import { systemEvents, EVENTS } from "@shared/lib/events";
|
||||||
|
|
||||||
export const questService = {
|
export const questService = {
|
||||||
|
/** Assign a quest to a user; enforces the maximum active quest limit. */
|
||||||
assignQuest: async (userId: string, questId: number, tx?: Transaction) => {
|
assignQuest: async (userId: string, questId: number, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
// Check active quest limit
|
// Check active quest limit
|
||||||
@@ -40,6 +41,7 @@ export const questService = {
|
|||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Set the progress value for a user's active quest. */
|
||||||
updateProgress: async (userId: string, questId: number, progress: number, tx?: Transaction) => {
|
updateProgress: async (userId: string, questId: number, progress: number, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
return await txFn.update(userQuests)
|
return await txFn.update(userQuests)
|
||||||
@@ -52,6 +54,7 @@ export const questService = {
|
|||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Process a domain event against active quests, incrementing progress and auto-completing if target is met. */
|
||||||
handleEvent: async (userId: string, eventName: string, weight: number = 1, tx?: Transaction) => {
|
handleEvent: async (userId: string, eventName: string, weight: number = 1, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
// 1. Fetch active user quests for this event
|
// 1. Fetch active user quests for this event
|
||||||
@@ -86,6 +89,7 @@ export const questService = {
|
|||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Mark a quest as completed and distribute its XP and balance rewards. */
|
||||||
completeQuest: async (userId: string, questId: number, tx?: Transaction) => {
|
completeQuest: async (userId: string, questId: number, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
const userQuest = await txFn.query.userQuests.findFirst({
|
const userQuest = await txFn.query.userQuests.findFirst({
|
||||||
@@ -137,6 +141,7 @@ export const questService = {
|
|||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Get all quests assigned to a user, including quest details. */
|
||||||
getUserQuests: async (userId: string) => {
|
getUserQuests: async (userId: string) => {
|
||||||
return await DrizzleClient.query.userQuests.findMany({
|
return await DrizzleClient.query.userQuests.findMany({
|
||||||
where: eq(userQuests.userId, BigInt(userId)),
|
where: eq(userQuests.userId, BigInt(userId)),
|
||||||
@@ -146,6 +151,7 @@ export const questService = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Get quests not yet assigned to a user. */
|
||||||
async getAvailableQuests(userId: string) {
|
async getAvailableQuests(userId: string) {
|
||||||
const userQuestIds = (await DrizzleClient.query.userQuests.findMany({
|
const userQuestIds = (await DrizzleClient.query.userQuests.findMany({
|
||||||
where: eq(userQuests.userId, BigInt(userId)),
|
where: eq(userQuests.userId, BigInt(userId)),
|
||||||
@@ -161,6 +167,7 @@ export const questService = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Create a new quest definition with trigger event, requirements, and rewards. */
|
||||||
async createQuest(data: {
|
async createQuest(data: {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -181,12 +188,14 @@ export const questService = {
|
|||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** List all quest definitions, ordered by ID. */
|
||||||
async getAllQuests() {
|
async getAllQuests() {
|
||||||
return await DrizzleClient.query.quests.findMany({
|
return await DrizzleClient.query.quests.findMany({
|
||||||
orderBy: (quests, { asc }) => [asc(quests.id)],
|
orderBy: (quests, { asc }) => [asc(quests.id)],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Delete a quest definition by ID. */
|
||||||
async deleteQuest(id: number, tx?: Transaction) {
|
async deleteQuest(id: number, tx?: Transaction) {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
return await txFn.delete(quests)
|
return await txFn.delete(quests)
|
||||||
@@ -195,6 +204,7 @@ export const questService = {
|
|||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Update a quest definition with partial data. */
|
||||||
async updateQuest(id: number, data: {
|
async updateQuest(id: number, data: {
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|||||||
@@ -101,10 +101,12 @@ export const tradeService = {
|
|||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Get an active trade session by thread ID. */
|
||||||
getSession: (threadId: string): TradeSession | undefined => {
|
getSession: (threadId: string): TradeSession | undefined => {
|
||||||
return sessions.get(threadId);
|
return sessions.get(threadId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Remove a trade session from memory. */
|
||||||
endSession: (threadId: string) => {
|
endSession: (threadId: string) => {
|
||||||
sessions.delete(threadId);
|
sessions.delete(threadId);
|
||||||
},
|
},
|
||||||
@@ -126,6 +128,7 @@ export const tradeService = {
|
|||||||
session.lastInteraction = Date.now();
|
session.lastInteraction = Date.now();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Add an item to a participant's trade offer; unlocks both sides when the offer changes. */
|
||||||
addItem: (threadId: string, userId: string, item: { id: number, name: string }, quantity: bigint) => {
|
addItem: (threadId: string, userId: string, item: { id: number, name: string }, quantity: bigint) => {
|
||||||
const session = tradeService.getSession(threadId);
|
const session = tradeService.getSession(threadId);
|
||||||
if (!session) throw new UserError("Session not found");
|
if (!session) throw new UserError("Session not found");
|
||||||
@@ -145,6 +148,7 @@ export const tradeService = {
|
|||||||
session.lastInteraction = Date.now();
|
session.lastInteraction = Date.now();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Remove an item from a participant's trade offer; unlocks both sides. */
|
||||||
removeItem: (threadId: string, userId: string, itemId: number) => {
|
removeItem: (threadId: string, userId: string, itemId: number) => {
|
||||||
const session = tradeService.getSession(threadId);
|
const session = tradeService.getSession(threadId);
|
||||||
if (!session) throw new UserError("Session not found");
|
if (!session) throw new UserError("Session not found");
|
||||||
@@ -158,6 +162,7 @@ export const tradeService = {
|
|||||||
session.lastInteraction = Date.now();
|
session.lastInteraction = Date.now();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Toggle a participant's lock status on their offer; returns the new lock state. */
|
||||||
toggleLock: (threadId: string, userId: string): boolean => {
|
toggleLock: (threadId: string, userId: string): boolean => {
|
||||||
const session = tradeService.getSession(threadId);
|
const session = tradeService.getSession(threadId);
|
||||||
if (!session) throw new UserError("Session not found");
|
if (!session) throw new UserError("Session not found");
|
||||||
@@ -199,6 +204,7 @@ export const tradeService = {
|
|||||||
tradeService.endSession(threadId);
|
tradeService.endSession(threadId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Clear all active trade sessions from memory. */
|
||||||
clearSessions: () => {
|
clearSessions: () => {
|
||||||
sessions.clear();
|
sessions.clear();
|
||||||
console.log("[TradeService] All active trade sessions cleared.");
|
console.log("[TradeService] All active trade sessions cleared.");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { withTransaction } from "@/lib/db";
|
|||||||
import type { Transaction } from "@shared/lib/types";
|
import type { Transaction } from "@shared/lib/types";
|
||||||
|
|
||||||
export const userService = {
|
export const userService = {
|
||||||
|
/** Fetch a user by Discord ID, including their class relation. */
|
||||||
getUserById: async (id: string) => {
|
getUserById: async (id: string) => {
|
||||||
const user = await DrizzleClient.query.users.findFirst({
|
const user = await DrizzleClient.query.users.findFirst({
|
||||||
where: eq(users.id, BigInt(id)),
|
where: eq(users.id, BigInt(id)),
|
||||||
@@ -12,10 +13,12 @@ export const userService = {
|
|||||||
});
|
});
|
||||||
return user;
|
return user;
|
||||||
},
|
},
|
||||||
|
/** Fetch a user by their username. */
|
||||||
getUserByUsername: async (username: string) => {
|
getUserByUsername: async (username: string) => {
|
||||||
const user = await DrizzleClient.query.users.findFirst({ where: eq(users.username, username) });
|
const user = await DrizzleClient.query.users.findFirst({ where: eq(users.username, username) });
|
||||||
return user;
|
return user;
|
||||||
},
|
},
|
||||||
|
/** Fetch a user by ID, creating a new record if one does not exist. */
|
||||||
getOrCreateUser: async (id: string, username: string, tx?: Transaction) => {
|
getOrCreateUser: async (id: string, username: string, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
let user = await txFn.query.users.findFirst({
|
let user = await txFn.query.users.findFirst({
|
||||||
@@ -38,6 +41,7 @@ export const userService = {
|
|||||||
return user;
|
return user;
|
||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
/** Get the class assigned to a user, or undefined if none. */
|
||||||
getUserClass: async (id: string) => {
|
getUserClass: async (id: string) => {
|
||||||
const user = await DrizzleClient.query.users.findFirst({
|
const user = await DrizzleClient.query.users.findFirst({
|
||||||
where: eq(users.id, BigInt(id)),
|
where: eq(users.id, BigInt(id)),
|
||||||
@@ -45,6 +49,7 @@ export const userService = {
|
|||||||
});
|
});
|
||||||
return user?.class;
|
return user?.class;
|
||||||
},
|
},
|
||||||
|
/** Create a new user with an optional initial class assignment. */
|
||||||
createUser: async (id: string | bigint, username: string, classId?: bigint, tx?: Transaction) => {
|
createUser: async (id: string | bigint, username: string, classId?: bigint, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
const [user] = await txFn.insert(users).values({
|
const [user] = await txFn.insert(users).values({
|
||||||
@@ -55,6 +60,7 @@ export const userService = {
|
|||||||
return user;
|
return user;
|
||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
/** Update a user record with partial data. */
|
||||||
updateUser: async (id: string, data: Partial<typeof users.$inferInsert>, tx?: Transaction) => {
|
updateUser: async (id: string, data: Partial<typeof users.$inferInsert>, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
const [user] = await txFn.update(users)
|
const [user] = await txFn.update(users)
|
||||||
@@ -64,6 +70,7 @@ export const userService = {
|
|||||||
return user;
|
return user;
|
||||||
}, tx);
|
}, tx);
|
||||||
},
|
},
|
||||||
|
/** Delete a user by ID. */
|
||||||
deleteUser: async (id: string, tx?: Transaction) => {
|
deleteUser: async (id: string, tx?: Transaction) => {
|
||||||
return await withTransaction(async (txFn) => {
|
return await withTransaction(async (txFn) => {
|
||||||
await txFn.delete(users).where(eq(users.id, BigInt(id)));
|
await txFn.delete(users).where(eq(users.id, BigInt(id)));
|
||||||
|
|||||||
Reference in New Issue
Block a user