Skip to content

Commit

Permalink
[feat-500] show beatleader leaderboards for maps/songs
Browse files Browse the repository at this point in the history
  • Loading branch information
silentrald committed Nov 2, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent e20330e commit 8835292
Showing 8 changed files with 702 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Tippy from "@tippyjs/react";
import { formatAccuracy, formatInt, formatPercentile, formatPp, formatRank } from "renderer/helpers/leaderboard";
import { BeatleaderPlayerInfo, BeatleaderScoreStats } from "renderer/services/third-parties/beatleader.service"
import { BeatleaderChip } from "./chip.component";
import { useState } from "react";
import { BeatleaderChip } from "./chip.component";
import { BsmButton } from "renderer/components/shared/bsm-button.component";

type Props = {
@@ -13,30 +14,23 @@ export function BeatleaderStatsSection({
}: Readonly<Props>) {
const [showHidden, setShowHidden] = useState(false);

const intFormatter = new Intl.NumberFormat("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 0 })
const numberFormatter = new Intl.NumberFormat("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const formatPp = (pp: number) => `${numberFormatter.format(pp)}pp`;
const formatAccuracy = (accuracy: number) => `${(Math.round(accuracy * 1000) / 100).toFixed(2)}%`;
const formatPercentile = (percentile: number) => `Top ${(Math.round(percentile * 1000) / 100).toFixed(2)}% of players`;
const formatRank = (rank: number) => `#${numberFormatter.format(rank)}`;

const topChips: ChipInfo[] = [{
resource: "total play count",
key: "totalPlayCount",
formatter: intFormatter.format,
formatter: formatInt,
}, {
resource: "total score",
key: "totalScore",
formatter: intFormatter.format,
formatter: formatInt,
hidden: true,
}, {
resource: "ranked play count",
key: "rankedPlayCount",
formatter: intFormatter.format,
formatter: formatInt,
}, {
resource: "total ranked score",
key: "totalRankedScore",
formatter: intFormatter.format,
formatter: formatInt,
hidden: true,
}, {
resource: "top PP",
@@ -65,7 +59,7 @@ export function BeatleaderStatsSection({
}, {
resource: "average ranked acc",
key: "averageRankedAccuracy",
formatter: formatRank,
formatter: formatAccuracy,
}, {
resource: "weighted ranked acc",
key: "averageWeightedRankedAccuracy",
@@ -84,7 +78,7 @@ export function BeatleaderStatsSection({
}, {
resource: "peak rank",
key: "peakRank",
formatter: intFormatter.format,
formatter: formatInt,
hidden: true,
}, {
resource: "average rank",
@@ -105,19 +99,19 @@ export function BeatleaderStatsSection({
const bottomChips: ChipInfo[] = [{
resource: "SS+",
key: "sspPlays",
formatter: intFormatter.format,
formatter: formatInt,
}, {
resource: "SS",
key: "ssPlays",
formatter: intFormatter.format,
formatter: formatInt,
}, {
resource: "S+",
key: "spPlays",
formatter: intFormatter.format,
formatter: formatInt,
}, {
resource: "S",
key: "sPlays",
formatter: intFormatter.format,
formatter: formatInt,
}];

return <div className="w-full h-fit flex gap-x-4 rounded-md p-4 bg-light-main-color-2 dark:bg-main-color-2">
Original file line number Diff line number Diff line change
@@ -24,6 +24,9 @@ import { MapItem } from "./map-item.component";
import { isLocalMapFitMapFilter } from "./filter-panel.component";
import { MapItemComponentPropsMapper } from "shared/mappers/map/map-item-component-props.mapper";
import { noop } from "shared/helpers/function.helpers";
import { LeaderboardModal, LeaderboardType } from "renderer/components/modal/modal-types/leaderboard.component";
import { ModalService } from "renderer/services/modale.service";
import { SongDetailDiffCharactertistic, SongDiffName } from "shared/models/maps";

type Props = {
version: BSVersion;
@@ -43,6 +46,7 @@ export type LocalMapsListPanelRef = {
export const LocalMapsListPanel = forwardRef<LocalMapsListPanelRef, Props>(({ version, className, filter, search, linkedState, isActive }, forwardRef) => {
const mapsManager = useService(MapsManagerService);
const mapsDownloader = useService(MapsDownloaderService);
const modals = useService(ModalService);

const t = useTranslation();

@@ -175,6 +179,21 @@ export const LocalMapsListPanel = forwardRef<LocalMapsListPanelRef, Props>(({ ve
});
}

const handleShowLeaderboard = (
leaderboard: LeaderboardType,
map: BsmLocalMap,
difficulty?: {
characteristic: SongDetailDiffCharactertistic;
name: SongDiffName;
}
) => {
modals.openModal(LeaderboardModal, { data: {
leaderboard,
map,
difficulty
}});
}

const renderMap = useCallback((renderableMap: RenderableMap) => {
const { map } = renderableMap;
return (
@@ -198,6 +217,7 @@ export const LocalMapsListPanel = forwardRef<LocalMapsListPanelRef, Props>(({ ve
createdAt={map.songDetails?.uploadedAt}
onDelete={handleDelete}
onSelected={onMapSelected}
onShowLeaderboard={handleShowLeaderboard}
callBackParam={map}
/>
);
Original file line number Diff line number Diff line change
@@ -34,6 +34,7 @@ import { sToMs } from "shared/helpers/time.helpers";
import formatDuration from "format-duration";
import { NpsIcon } from "renderer/components/svgs/icons/nps-icon.component";
import { SpeedIcon } from "renderer/components/svgs/icons/speed-icon.component";
import { LeaderboardType } from "renderer/components/modal/modal-types/leaderboard.component";

export type MapItemComponentProps<T = unknown> = {
hash: string;
@@ -66,9 +67,17 @@ export type MapItemComponentProps<T = unknown> = {
onCancelDownload?: (param: T) => void;
onDoubleClick?: (param: T) => void;
onHighlightedDiffsChange?: (diffs: BPListDifficulty[]) => void;
onShowLeaderboard?: (
leaderboard: LeaderboardType,
param: T,
difficulty?: {
characteristic: SongDetailDiffCharactertistic;
name: SongDiffName;
}
) => void;
};

export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, highlightedDiffs, ranked, blRanked, bpm, duration, likes, createdAt, selected, selected$, downloading, showOwned, isOwned$, canOpenMapDetails, canOpenAuthorDetails, callBackParam, onDelete, onDownload, onSelected, onCancelDownload, onDoubleClick, onHighlightedDiffsChange }: MapItemComponentProps<T>) {
export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, highlightedDiffs, ranked, blRanked, bpm, duration, likes, createdAt, selected, selected$, downloading, showOwned, isOwned$, canOpenMapDetails, canOpenAuthorDetails, callBackParam, onDelete, onDownload, onSelected, onCancelDownload, onDoubleClick, onHighlightedDiffsChange, onShowLeaderboard }: MapItemComponentProps<T>) {
const linkOpener = useService(LinkOpenerService);
const audioPlayer = useService(AudioPlayerService);

@@ -211,7 +220,18 @@ export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor,
{Array.from(diffs.entries()).map(([charac, diffSet]) => (
<ol key={charac} className="flex flex-col w-full gap-1">
{diffSet.map(({ name, libelle, stars, nps, njs }) => (
<li key={`${name}${libelle}${stars}`} className="w-full h-[1.15rem] flex items-center gap-1">
<li
key={`${name}${libelle}${stars}`}
className="w-full h-[1.15rem] flex items-center gap-1"
onClick={event => {
event.stopPropagation();
onShowLeaderboard?.(
LeaderboardType.None,
callBackParam,
{ characteristic: charac, name }
)
}}
>
{onHighlightedDiffsChange && (
<Tippy content={t("maps.map-item.hightlight-difficulty")} placement="top" theme="default">
<div className="h-full aspect-square">
@@ -322,16 +342,36 @@ export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor,
)}
</div>
<motion.div className="w-full h-5 pb-1 pr-7 flex items-center gap-1" onHoverStart={bottomBarHoverStart} onHoverEnd={bottomBarHoverEnd}>
{ranked && (
<div className="text-yellow-300 bg-current rounded-full px-1 h-full flex items-center justify-center">
<span className="uppercase text-xs font-bold tracking-wide brightness-[.25]">{t("maps.map-specificities.ranked")}</span>
</div>
)}
{blRanked && (
<div className="bg-pink-400 bg-current rounded-full px-1 h-full flex items-center justify-center">
<span className="uppercase text-xs font-bold tracking-wide brightness-[.25]">{t("maps.map-specificities.ranked")}</span>
</div>
)}
{!ranked && !blRanked &&
<MapRankCapsule
text="unranked"
className="bg-gray-400"
onClick={() => onShowLeaderboard?.(
LeaderboardType.None,
callBackParam
)}
/>
}
{ranked &&
<MapRankCapsule
text={t("maps.map-specificities.ranked")}
className="text-yellow-300"
onClick={() => onShowLeaderboard?.(
LeaderboardType.ScoreSaber,
callBackParam
)}
/>
}
{blRanked &&
<MapRankCapsule
text={t("maps.map-specificities.ranked")}
className="bg-pink-400"
onClick={() => onShowLeaderboard?.(
LeaderboardType.Beatleader,
callBackParam
)}
/>
}
<div className="h-full grow flex items-start content-start">{renderDiffPreview()}</div>
</motion.div>
</div>
@@ -429,4 +469,28 @@ export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor,
);
};

function MapRankCapsule({
text,
className,
onClick,
}: Readonly<{
text: string;
className: string;
onClick: () => void;
}>) {
return <div
className={`bg-current rounded-full px-1 h-full flex items-center justify-center ${className ?? ""}`}
role="button"
tabIndex={0}
onClick={event => {
event.stopPropagation();
onClick();
}}
>
<span className="uppercase text-xs font-bold tracking-wide brightness-[.25]">
{text}
</span>
</div>
}

export const MapItem = typedMemo(MapItemComponent, equal);
Loading

0 comments on commit 8835292

Please sign in to comment.