Skip to content

Commit

Permalink
Add top feeder to whale mood and fish fed leaderboard
Browse files Browse the repository at this point in the history
  • Loading branch information
ttbowen committed Oct 19, 2024
1 parent 9883bc2 commit 9a5492a
Show file tree
Hide file tree
Showing 8 changed files with 352 additions and 59 deletions.
1 change: 1 addition & 0 deletions packages/mrwhale-discord/src/commands/level/leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default class extends DiscordCommand {
{ name: "Chests opened", value: "chestsopened" },
{ name: "Exp", value: "exp" },
{ name: "Fish caught", value: "fishcaught" },
{ name: "Fish fed", value: "fishfed" },
{ name: "Gems", value: "gems" }
)
);
Expand Down
13 changes: 11 additions & 2 deletions packages/mrwhale-discord/src/commands/utility/whalemood.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DiscordCommand } from "../../client/command/discord-command";
import { EMBED_COLOR } from "../../constants";
import {
getFavoriteFish,
getTopFishFeeder,
getTotalFishFedByUserInGuild,
getTotalFishFedInGuild,
} from "../../database/services/fish-fed";
Expand All @@ -32,7 +33,7 @@ export default class extends DiscordCommand {
message.author.id,
message.guildId
);
return message.reply({ embeds: [embed] });
return message.reply({ embeds: [embed], allowedMentions: { users: [] } });
}

async slashCommandAction(
Expand All @@ -42,7 +43,10 @@ export default class extends DiscordCommand {
interaction.member.user.id,
interaction.guildId
);
return interaction.reply({ embeds: [embed] });
return interaction.reply({
embeds: [embed],
allowedMentions: { users: [] },
});
}

private async getHungerLevelEmbed(userId: string, guildId: string) {
Expand All @@ -59,6 +63,7 @@ export default class extends DiscordCommand {
);
const totalFishFedByGuild = await getTotalFishFedInGuild(guildId);
const favoriteFish = await getFavoriteFish(guildId);
const topFeeder = await getTopFishFeeder(guildId, userId);
const embed = new EmbedBuilder()
.setColor(EMBED_COLOR)
.addFields([
Expand All @@ -81,6 +86,10 @@ export default class extends DiscordCommand {
value: `${favoriteFish.icon} ${favoriteFish.name}`,
inline: true,
},
{
name: "Top Feeder",
value: topFeeder ? `<@${topFeeder}>` : "No top feeder yet",
},
{
name: "Fish Fed",
value: `${totalFishFedByUser}`,
Expand Down
1 change: 1 addition & 0 deletions packages/mrwhale-discord/src/database/models/fish-fed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface FishFedAttributes {
guildId: string;
fishId: number;
quantity: number;
totalQuantity?: number;
}

export interface FishFedInstance
Expand Down
179 changes: 179 additions & 0 deletions packages/mrwhale-discord/src/database/services/fish-fed-leaderboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { ChatInputCommandInteraction, Message } from "discord.js";
import * as NodeCache from "node-cache";
import * as sequelize from "sequelize";

import { getLevelFromExp } from "@mrwhale-io/core";
import { FishFed, FishFedInstance } from "../models/fish-fed";
import { HIGHSCORE_PAGE_LIMIT } from "../../constants";
import { ScoreResult } from "../../types/scores/score-result";
import { fetchUser } from "./user";
import { MappedScores } from "../../types/scores/mapped-scores";

const leaderboardCache = new NodeCache({ stdTTL: 600 }); // Cache for 10 minutes

/**
* Retrieves the fish fed scores for a specified guild, paginated.
*
* @param messageOrInteraction The Discord message or interaction instance.
* @param page The page number to fetch scores for.
* @returns A Promise that resolves to a ScoreResult containing the scores, total count, pages, and offset.
* @throws An error if the fish fed scores could not be fetched.
*/
export async function getGuildFishFedScores(
messageOrInteraction: Message | ChatInputCommandInteraction,
page: number
): Promise<ScoreResult> {
try {
const guildId = messageOrInteraction.guildId;
const cacheKey = `topFishFedScores:${guildId}:${page}`;
const cachedData = leaderboardCache.get<ScoreResult>(cacheKey);

if (cachedData) {
return cachedData;
}

const totalFishFedScores = await FishFed.count({
where: { guildId },
distinct: true,
col: "userId",
});
const offset = (page - 1) * HIGHSCORE_PAGE_LIMIT;
const totalPages = Math.ceil(totalFishFedScores / HIGHSCORE_PAGE_LIMIT);

const fishFed = await getGuildScores(guildId, offset);
const mappedScores = await mapScoresToUsers(messageOrInteraction, fishFed);

const topScorePage: ScoreResult = {
scores: mappedScores,
total: totalFishFedScores,
pages: totalPages,
offset,
};

leaderboardCache.set(cacheKey, topScorePage);

return topScorePage;
} catch (error) {
throw new Error("Failed to fetch fish fed scores.");
}
}

/**
* Retrieves the global fish fed scores, paginated.
*
* This function fetches and calculates the global fish fed scores across all guilds.
* It supports pagination to navigate through the scores and uses caching to store and
* retrieve the results efficiently. The function first checks for cached data, and if not
* found, it queries the database to fetch the total number of users, calculates the total
* pages, and retrieves the fish fed data for the specified page. It then maps the user
* IDs to user objects and constructs the final scores result.
*
* @param messageOrInteraction The Discord message or interaction instance.
* @param page The page number to fetch scores for.
* @returns A Promise that resolves to a ScoreResult containing the scores, total count, pages, and offset.
* @throws An error if the global fish fed scores could not be fetched.
*/
export async function getGlobalFishFedScores(
messageOrInteraction: Message | ChatInputCommandInteraction,
page: number
): Promise<ScoreResult> {
try {
const cacheKey = `topGlobalFishFedScores:${page}`;
const cachedData = leaderboardCache.get<ScoreResult>(cacheKey);

if (cachedData) {
return cachedData;
}

const totalFishFedScores = await FishFed.count({
distinct: true,
col: "userId",
});
const offset = (page - 1) * HIGHSCORE_PAGE_LIMIT;
const totalPages = Math.ceil(totalFishFedScores / HIGHSCORE_PAGE_LIMIT);

const fishFed = await getGlobalScores(offset);
const mappedScores = await mapScoresToUsers(messageOrInteraction, fishFed);

const topScorePage: ScoreResult = {
scores: mappedScores,
total: totalFishFedScores,
pages: totalPages,
offset,
};

leaderboardCache.set(cacheKey, topScorePage);

return topScorePage;
} catch (error) {
throw new Error("Failed to fetch global fish fed scores.");
}
}

/**
* Retrieves the fish feeding scores for a specific guild, ordered by the total quantity of fish fed in descending order.
*
* @param guildId The Id of the guild for which to retrieve the fish feeding scores.
* @param offset The offset for pagination of the results.
* @returns A promise that resolves to an array of objects containing user Ids and their corresponding total quantity of fish fed.
*/
async function getGuildScores(
guildId: string,
offset: number
): Promise<FishFedInstance[]> {
return await FishFed.findAll({
where: { guildId },
attributes: [
"userId",
[sequelize.fn("SUM", sequelize.col("quantity")), "totalQuantity"],
],
group: ["userId"],
order: [[sequelize.literal("totalQuantity"), "DESC"]],
limit: HIGHSCORE_PAGE_LIMIT,
offset,
raw: true,
});
}

/**
* Retrieves the global scores for the fish-fed leaderboard.
*
* @param offset The offset for pagination.
* @returns A promise that resolves to an array of FishFedInstance objects containing userId and totalQuantity.
*/
async function getGlobalScores(offset: number): Promise<FishFedInstance[]> {
return await FishFed.findAll({
attributes: [
"userId",
[sequelize.fn("SUM", sequelize.col("quantity")), "totalQuantity"],
],
group: ["userId"],
order: [[sequelize.literal("totalQuantity"), "DESC"]],
limit: HIGHSCORE_PAGE_LIMIT,
offset,
raw: true,
});
}

/**
* Maps scores to user objects.
*
* @param messageOrInteraction The Discord message or interaction instance.
* @param scores The scores to map.
* @returns A Promise that resolves to an array of mapped scores.
*/
async function mapScoresToUsers(
messageOrInteraction: Message | ChatInputCommandInteraction,
scores: FishFedInstance[]
): Promise<MappedScores[]> {
const mappedScoresPromises = scores.map(async (score) => {
const user = await fetchUser(messageOrInteraction.client, score.userId);
return {
exp: score.totalQuantity,
level: getLevelFromExp(score.totalQuantity),
user: user ? user : null,
};
});

return Promise.all(mappedScoresPromises);
}
26 changes: 26 additions & 0 deletions packages/mrwhale-discord/src/database/services/fish-fed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,29 @@ export async function getFavoriteFish(

return { name: favouriteFish.name, icon: favouriteFish.icon };
}

/**
* Get the guild user with the highest quantity of fish fed.
* @param guildId The Id of the guild.
* @param userId The Id of the user.
* @returns A promise that resolves to the user with the highest quantity of fish fed, or null if no user is found.
*/
export async function getTopFishFeeder(
guildId: string,
userId: string
): Promise<string> {
const favoriteFishData = await FishFed.findAll({
attributes: [
"userId",
[sequelize.fn("SUM", sequelize.col("quantity")), "totalFed"],
],
where: {
guildId,
userId,
},
order: [[sequelize.literal("totalFed"), "DESC"]],
limit: 1,
});

return favoriteFishData.length > 0 ? favoriteFishData[0].userId : null;
}
28 changes: 28 additions & 0 deletions packages/mrwhale-discord/src/database/services/leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
createChestsOpenedLeaderboardTable,
createExpLeaderboardTable,
createFishCaughtLeaderboardTable,
createFishFedLeaderboardTable,
createGemsLeaderboardTable,
} from "../../util/embed/leaderboard-table-helpers";
import {
Expand All @@ -16,6 +17,10 @@ import {
} from "./fish-caught-leaderboard";
import { getGlobalExpScores, getGuildExpScores } from "./exp-leaderboard";
import { getGlobalGemsScores, getGuildGemsScores } from "./gems-leaderboard";
import {
getGlobalFishFedScores,
getGuildFishFedScores,
} from "./fish-fed-leaderboard";

/**
* Retrieves a leaderboard table embed of the specified type.
Expand Down Expand Up @@ -46,6 +51,9 @@ export async function getLeaderboardTable(
isGlobal
);

case "fishfed":
return getFishFedLeaderboardTable(interactionOrMessage, page, isGlobal);

case "chestsopened":
return getChestsOpenedLeaderboardTable(
interactionOrMessage,
Expand Down Expand Up @@ -98,6 +106,26 @@ async function getFishCaughtLeaderboardTable(
};
}

async function getFishFedLeaderboardTable(
interactionOrMessage: ChatInputCommandInteraction | Message,
page: number,
isGlobal: boolean = false
) {
const fishFedScores = isGlobal
? await getGlobalFishFedScores(interactionOrMessage, page)
: await getGuildFishFedScores(interactionOrMessage, page);

return {
table: await createFishFedLeaderboardTable(
interactionOrMessage,
fishFedScores,
page,
isGlobal
),
pages: fishFedScores.pages,
};
}

async function getChestsOpenedLeaderboardTable(
interactionOrMessage: ChatInputCommandInteraction | Message,
page: number,
Expand Down
Loading

0 comments on commit 9a5492a

Please sign in to comment.