From 7856e1174f852587f7868514bf91cc152b793a17 Mon Sep 17 00:00:00 2001 From: Cody Date: Fri, 5 Jun 2026 02:50:11 -0600 Subject: [PATCH 01/11] feat(fishing): add fishing game mode and related features * Introduce fishing game mode with comprehensive data structures * Add fishing special fish and mythical fish data * Implement fishing rods and hook trails with unlocking logic * Update player stats to include fishing data * Enhance general stats to accommodate fishing statistics * Modify localization files to support fishing commands This change expands the game by introducing a new fishing mode, enhancing player engagement and providing additional gameplay options. --- .../src/commands/fishing/fishing.command.tsx | 86 ++ .../src/commands/fishing/fishing.profile.tsx | 422 +++++++ locales/en-US/default.json | 1 + packages/schemas/src/game/constants.ts | 5 +- .../src/player/gamemodes/general/fishing.ts | 1052 +++++++++++++++++ .../src/player/gamemodes/general/index.ts | 13 + packages/schemas/src/player/stats.ts | 11 +- pnpm-lock.yaml | 2 +- 8 files changed, 1586 insertions(+), 6 deletions(-) create mode 100644 apps/discord-bot/src/commands/fishing/fishing.command.tsx create mode 100644 apps/discord-bot/src/commands/fishing/fishing.profile.tsx create mode 100644 packages/schemas/src/player/gamemodes/general/fishing.ts diff --git a/apps/discord-bot/src/commands/fishing/fishing.command.tsx b/apps/discord-bot/src/commands/fishing/fishing.command.tsx new file mode 100644 index 000000000..24dd3d48f --- /dev/null +++ b/apps/discord-bot/src/commands/fishing/fishing.command.tsx @@ -0,0 +1,86 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +import { + ApiService, + Command, + CommandContext, + PlayerArgument, + paginate, +} from "@statsify/discord"; +import { FishingProfile } from "./fishing.profile.js"; +import { GENERAL_MODES } from "@statsify/schemas"; +import { getBackground, getLogo } from "@statsify/assets"; +import { getTheme } from "#themes"; +import { mapBackground } from "#constants"; +import { render } from "@statsify/rendering"; +import type { FishingPageData } from "./fishing.profile.js"; +import type { Page } from "@statsify/discord"; + +@Command({ description: (t) => t("commands.fishing"), args: [PlayerArgument] }) +export class FishingCommand { + public constructor(private readonly apiService: ApiService) {} + + public async run(context: CommandContext) { + const user = context.getUser(); + const player = await this.apiService.getPlayer( + context.option("player"), + user, + ); + const fishing = player.stats.general.fishing; + + const [logo, skin, badge] = await Promise.all([ + getLogo(user), + this.apiService.getPlayerSkin(player.uuid, user), + this.apiService.getUserBadge(player.uuid), + ]); + + const pages: FishingPageData[] = [ + { id: "overview", label: "Overview" }, + { id: "mythicals", label: "Mythicals" }, + { id: "specialsOne", label: "Special Fish I" }, + { id: "specialsTwo", label: "Special Fish II" }, + { id: "collections", label: "Collections" }, + ]; + + if (fishing.seasonal.hasData) { + pages.push({ id: "seasonal", label: "Seasonal" }); + } + + return paginate( + context, + pages.map( + (pageData, page): Page => ({ + label: pageData.label, + generator: async (t) => { + const background = await getBackground( + ...mapBackground(GENERAL_MODES, GENERAL_MODES.getApiModes()[0]), + ); + + return render( + , + getTheme(user), + ); + }, + }), + ), + ); + } +} diff --git a/apps/discord-bot/src/commands/fishing/fishing.profile.tsx b/apps/discord-bot/src/commands/fishing/fishing.profile.tsx new file mode 100644 index 000000000..134189fe0 --- /dev/null +++ b/apps/discord-bot/src/commands/fishing/fishing.profile.tsx @@ -0,0 +1,422 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +import { + Container, + Footer, + Header, + Multiline, + SidebarItem, + Table, +} from "#components"; +import { FormattedGame } from "@statsify/schemas"; +import { arrayGroup } from "@statsify/util"; +import type { BaseProfileProps } from "#commands/base.hypixel-command"; +import type { + FishingCollectionItem, + FishingSeasonalYear, +} from "@statsify/schemas"; + +export type FishingPage = + | "overview" + | "mythicals" + | "specialsOne" + | "specialsTwo" + | "collections" + | "seasonal"; + +export interface FishingPageData { + id: FishingPage; + label: string; +} + +interface FishingProfileProps extends BaseProfileProps { + page: FishingPage; + pageNumber: number; + pageCount: number; +} + +interface FishingCollectionTableProps { + items: FishingCollectionItem[]; + columns?: number; + compact?: boolean; +} + +const statusColor = (unlocked: boolean) => (unlocked ? "§a" : "§c"); + +const formatId = (id: string) => + id === "N/A" + ? "N/A" + : id + .replace("mainlobby_fishing_", "") + .replace("fishing_rod_", "") + .replaceAll("_", " ") + .replaceAll("-", " ") + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + +const formatPercent = (value: number) => `${(value * 100).toFixed(1)}%`; + +const FishingCollectionTable = ({ + items, + columns = 3, + compact = false, +}: FishingCollectionTableProps) => ( + + {arrayGroup(items, columns).map((row) => ( + + {row.map((item) => ( + + + §l{statusColor(item.unlocked)} + {item.active ? "§n" : ""} + {item.name} + + + {statusColor(item.unlocked)} + {item.unlocked ? "Unlocked" : "Locked"} + {compact + ? "" + : ` §7- ${item.source !== "N/A" ? item.source : item.requirement}`} + + {!compact && item.environment !== "N/A" ? ( + §7Found In: §b{item.environment} + ) : ( + <> + )} + + ))} + + ))} + +); + +const FishingOverview = ({ + t, + player, +}: Pick) => { + const { fishing } = player.stats.general; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const FishingMythicals = ({ + t, + player, +}: Pick) => { + const { fishing } = player.stats.general; + + return ( + + {arrayGroup(fishing.mythicals, 2).map((row) => ( + + {row.map((mythical) => ( + + + + {[ + `§7Rarity: §b${mythical.rarity}`, + `§7Catches: §6${t(mythical.catches)}`, + `§7Share: §6${formatPercent(mythical.percentage)}`, + `§7Max Weight: §6${mythical.maxWeight ? `${t(mythical.maxWeight)}kg` : "N/A"}${mythical.maxed ? " §aMaxed" : ""}`, + ].join("\n")} + + + + ))} + + ))} + + ); +}; + +const FishingSpecials = ({ + player, + page, +}: Pick) => { + const { fishing } = player.stats.general; + const offset = page === "specialsOne" ? 0 : 24; + + return ( + + ); +}; + +const FishingCollections = ({ + player, +}: Pick) => { + const { fishing } = player.stats.general; + + return ( + + + + + + + + + ); +}; + +const seasonalSummary = (year: FishingSeasonalYear) => { + const events = [year.halloween, year.christmas, year.easter, year.summer]; + const environments = events.flatMap((event) => [ + event.water, + event.lava, + event.ice, + ]); + + return { + fish: environments.reduce( + (total, environment) => total + environment.fish, + 0, + ), + junk: environments.reduce( + (total, environment) => total + environment.junk, + 0, + ), + treasure: environments.reduce( + (total, environment) => total + environment.treasure, + 0, + ), + mythical: environments.reduce( + (total, environment) => total + environment.mythical, + 0, + ), + water: events.reduce((total, event) => total + event.water.total, 0), + lava: events.reduce((total, event) => total + event.lava.total, 0), + ice: events.reduce((total, event) => total + event.ice.total, 0), + total: year.total, + }; +}; + +const FishingSeasonal = ({ + t, + player, +}: Pick) => { + const { seasonal } = player.stats.general.fishing; + const years = [ + ["2022", seasonal.year2022], + ["2023", seasonal.year2023], + ["2024", seasonal.year2024], + ["2025", seasonal.year2025], + ["2026", seasonal.year2026], + ] as const; + + return ( + + + {years.map(([year, stats]) => { + const summary = seasonalSummary(stats); + + return ( + + + + + + + + ); + })} + + + {years.map(([year, stats]) => { + const summary = seasonalSummary(stats); + + return ( + + + + + + + ); + })} + + + ); +}; + +export const FishingProfile = ({ + skin, + player, + background, + logo, + user, + badge, + t, + time, + page, + pageNumber, + pageCount, +}: FishingProfileProps) => { + const { fishing } = player.stats.general; + + const sidebar: SidebarItem[] = [ + ["Total Catches", t(fishing.totalCatches), "§b"], + ["Mythicals", t(fishing.mythical), "§6"], + ["Special Fish", `${t(fishing.special)}/48`, "§d"], + ["Rods", `${t(fishing.rods)}/8`, "§a"], + ["Hook Trails", `${t(fishing.hookTrails)}/11`, "§6"], + ["Active Rod", formatId(fishing.activeFishingRod), "§a"], + ["Active Trail", formatId(fishing.activeFishHookTrail), "§6"], + ]; + + const title = + page === "overview" + ? `§l${FormattedGame.FISHING} §fStats` + : page === "mythicals" + ? `§l${FormattedGame.FISHING} §fMythicals` + : page === "specialsOne" + ? `§l${FormattedGame.FISHING} §fSpecial Fish` + : page === "specialsTwo" + ? `§l${FormattedGame.FISHING} §fSpecial Fish` + : page === "collections" + ? `§l${FormattedGame.FISHING} §fCollections` + : `§l${FormattedGame.FISHING} §fSeasonal`; + + return ( + +
+ {page === "overview" ? ( + + ) : page === "mythicals" ? ( + + ) : page === "specialsOne" || page === "specialsTwo" ? ( + + ) : page === "collections" ? ( + + ) : ( + + )} +