This commit is contained in:
zk
2026-05-11 23:39:26 +01:00
parent 42a0814f75
commit d82326aace
40 changed files with 244 additions and 141 deletions
@@ -36,7 +36,7 @@ import { useQuery } from "react-query";
import { Link, Route, Switch } from "react-router-dom";
import {
type ChartDocument,
FormatDifficulty,
FormatDifficultyLong,
GameToGameGroup,
GetGameGroupConfig,
type integer,
@@ -54,7 +54,7 @@ export default function GPTChartPage({
song: SongDocument;
} & GamePT) {
const formatSongTitle = `${song.artist} - ${song.title}`;
const formatDiff = chart ? FormatDifficulty(chart) : "Loading...";
const formatDiff = chart ? FormatDifficultyLong(chart) : "Loading...";
useSetSubheader(
[
@@ -54,7 +54,9 @@ export default function EvolutionMilestoneFeedRow({
</>
) : chart ? (
<>
<span className="fst-italic text-body-secondary">Unknown chart</span>
<span className="fst-italic text-body-secondary">
Unknown chart
</span>
<span className="text-body-secondary fw-normal ms-2 small text-nowrap">
({FormatDifficultyShort(chart)})
</span>
@@ -14,17 +14,12 @@ function distributionRowChrome(enumColour: string | undefined): {
fillStrong: string | undefined;
trackBg: string;
} {
const fallbackTrack =
"color-mix(in srgb, var(--bs-secondary) 14%, transparent)";
const fallbackFillStrong =
"color-mix(in srgb, var(--bs-secondary) 42%, transparent)";
const fallbackTrack = "color-mix(in srgb, var(--bs-secondary) 14%, transparent)";
const fallbackFillStrong = "color-mix(in srgb, var(--bs-secondary) 42%, transparent)";
const borderLeftColor = enumColour ?? "var(--bs-secondary)";
if (
enumColour &&
(enumColour.startsWith("#") || enumColour.startsWith("rgb"))
) {
if (enumColour && (enumColour.startsWith("#") || enumColour.startsWith("rgb"))) {
try {
return {
borderLeftColor: enumColour,
@@ -97,8 +92,7 @@ export default function FolderEnumDistributionBreakdown({
const bucket = stats.stats[enumMetric] ?? {};
const minRelIx = conf.values.indexOf(conf.minimumRelevantValue);
const valueIndexFloor =
clipToMinimumRelevance && minRelIx !== -1 ? minRelIx : 0;
const valueIndexFloor = clipToMinimumRelevance && minRelIx !== -1 ? minRelIx : 0;
if (total === 0) {
return <small className="text-body-secondary">No charts in this folder.</small>;
@@ -115,13 +109,10 @@ export default function FolderEnumDistributionBreakdown({
const pct = total > 0 ? (100 * count) / total : 0;
const chipFill = colours?.[label];
const chrome = distributionRowChrome(chipFill);
const pctWidth =
pct > 0 ? `${ToFixedFloor(Math.min(100, Math.max(0, pct)), 3)}%` : "0%";
const pctWidth = pct > 0 ? `${ToFixedFloor(Math.min(100, Math.max(0, pct)), 3)}%` : "0%";
const clickableRow =
Boolean(onEnumBreakdownRowClick && isLg && count > 0);
const rowHoverChromeClass =
clickableRow ? breakdownStyles.enumRowInteractive : "";
const clickableRow = Boolean(onEnumBreakdownRowClick && isLg && count > 0);
const rowHoverChromeClass = clickableRow ? breakdownStyles.enumRowInteractive : "";
const rowShellStyle = {
backgroundColor: chrome.trackBg,
@@ -201,9 +192,19 @@ export default function FolderEnumDistributionBreakdown({
const remainderPct = total > 0 ? (100 * remainder) / total : 0;
return (
<div className={selected ? "border border-primary border-opacity-75 pb-3 pe-2 pt-2 ps-2 rounded-4 shadow-sm bg-primary bg-opacity-10" : undefined}>
<div
className={
selected
? "border border-primary border-opacity-75 pb-3 pe-2 pt-2 ps-2 rounded-4 shadow-sm bg-primary bg-opacity-10"
: undefined
}
>
<div
className={suppressTopRule ? undefined : "mt-3 border-secondary border-opacity-25 border-top pt-3"}
className={
suppressTopRule
? undefined
: "mt-3 border-secondary border-opacity-25 border-top pt-3"
}
>
<div className="align-items-center d-flex flex-wrap gap-2 justify-content-between mb-2">
{onActivate ? (
@@ -65,8 +65,7 @@ export default function FolderEnumProgressBar({
const bucket = stats.stats[enumMetric] ?? {};
const minRelIx = conf.values.indexOf(conf.minimumRelevantValue);
const valueIndexFloor =
clipToMinimumRelevance && minRelIx !== -1 ? minRelIx : 0;
const valueIndexFloor = clipToMinimumRelevance && minRelIx !== -1 ? minRelIx : 0;
const segments: Array<{ count: number; label: string; rawFill: string }> = [];
let filled = 0;
@@ -136,18 +135,18 @@ export default function FolderEnumProgressBar({
</>
}
>
<div
className={`h-100 ${barStyles.folderEnumBarSegment} ${animateSegments ? barStyles.folderEnumBarSegmentAnimated : ""}`}
style={{
...(animateSegments
? { animationDelay: `${0.05 + segIndex * 0.055}s` }
: {}),
backgroundColor: fill,
flex: "none",
minWidth: seg.count ? "4px" : 0,
width: `${pct}%`,
}}
>
<div
className={`h-100 ${barStyles.folderEnumBarSegment} ${animateSegments ? barStyles.folderEnumBarSegmentAnimated : ""}`}
style={{
...(animateSegments
? { animationDelay: `${0.05 + segIndex * 0.055}s` }
: {}),
backgroundColor: fill,
flex: "none",
minWidth: seg.count ? "4px" : 0,
width: `${pct}%`,
}}
>
{showInlinePct && (
<span className={barStyles.folderEnumBarLabel}>
{inlinePctText}
@@ -37,7 +37,15 @@ import React, {
useState,
} from "react";
import { Col, Collapse, Form, Row } from "react-bootstrap";
import { Link, Redirect, Route, Switch, useHistory, useLocation, useParams } from "react-router-dom";
import {
Link,
Redirect,
Route,
Switch,
useHistory,
useLocation,
useParams,
} from "react-router-dom";
import {
COLOUR_SET,
EnumIndexToValue,
@@ -107,7 +115,9 @@ export default function SpecificFolderPage({ reqUser, game }: Props) {
queueMicrotask(() => {
const anchor = document.getElementById(FOLDER_FOLDER_TABLE_SCROLL_INTO_VIEW_ID);
if (!anchor) return;
if (!anchor) {
return;
}
const y =
anchor.getBoundingClientRect().top +
@@ -4,10 +4,7 @@ import { GPT_CLIENT_IMPLEMENTATIONS } from "#lib/game-implementations";
import { APIFetchV1 } from "#util/api";
import React, { useContext, useMemo } from "react";
import { Link } from "react-router-dom";
import {
GetGameConfig,
type TableDocument,
} from "tachi-common";
import { GetGameConfig, type TableDocument } from "tachi-common";
import FolderEnumProgressBar from "./FolderEnumProgressBar";
import folderTableStyles from "./FolderTablePage.module.scss";
@@ -59,10 +59,7 @@ export default function TableFolderViewer({
const gameConfig = useMemo(() => GetGameConfig(game), [game]);
const evolutionScope = useMemo(
() => ({ kind: "table" as const, table }),
[table],
);
const evolutionScope = useMemo(() => ({ kind: "table" as const, table }), [table]);
if (error) {
return <ApiError error={error} />;
@@ -1,11 +1,9 @@
import type {
FolderStatsInfo,
TableEvolutionEventAPI,
} from "#types/api-returns";
import type { FolderStatsInfo, TableEvolutionEventAPI } from "#types/api-returns";
import {
GetScoreMetricConf,
type FolderDocument,
type GameConfig,
GetScoreMetricConf,
type integer,
type TableDocument,
type UserDocument,
@@ -38,7 +38,7 @@ import { Redirect, Route, Switch, useParams } from "react-router-dom";
import {
type ChartDocument,
COLOUR_SET,
FormatDifficulty,
FormatDifficultyLong,
type GamesForGroup,
GameToGameGroup,
GetGameConfig,
@@ -472,7 +472,7 @@ function DifficultyButton({
: undefined,
}}
>
{FormatDifficulty(chart)}
{FormatDifficultyLong(chart)}
{chart.isPrimary ? (
""
) : (
@@ -69,7 +69,10 @@ export default function FolderInfoHeader({
onEnumBreakdownRowClick={
onBreakdownEnumValueClick
? (enumValueLabel: string) => {
onBreakdownEnumValueClick(metric, enumValueLabel);
onBreakdownEnumValueClick(
metric,
enumValueLabel,
);
}
: undefined
}
@@ -125,11 +125,14 @@ function FolderMinimapMain({
// Switchboard tabs / unmount can leave portalled Bootstrap tooltips in <body> while React
// state still has show=true; drop those nodes so nothing sticks.
useLayoutEffect(() => () => {
useLayoutEffect(
() => () => {
document.querySelectorAll(".tooltip-folder-minimap").forEach((el) => {
el.remove();
});
}, [enumMetric]);
},
[enumMetric],
);
return (
<div className="row">
@@ -1,6 +1,11 @@
import { DisplayLevelNum } from "#components/tables/cells/DifficultyCell";
import React from "react";
import { type ChartDocument, FormatDifficulty, type SongDocument, type V3Game } from "tachi-common";
import {
type ChartDocument,
FormatDifficultyLong,
type SongDocument,
type V3Game,
} from "tachi-common";
export default function DefaultSongChartInfoFormat({
song,
@@ -18,7 +23,7 @@ export default function DefaultSongChartInfoFormat({
</h4>
{chart && (
<>
<h5>({FormatDifficulty(chart)})</h5>
<h5>({FormatDifficultyLong(chart)})</h5>
<h6>
<DisplayLevelNum
game={game}
@@ -1,6 +1,11 @@
import { FormatTables } from "#util/misc";
import React from "react";
import { type ChartDocument, FormatDifficulty, type SongDocument, type V3Game } from "tachi-common";
import {
type ChartDocument,
FormatDifficultyLong,
type SongDocument,
type V3Game,
} from "tachi-common";
export default function IIDXStyleSongChartInfoFormat({
song,
@@ -26,5 +31,5 @@ function LevelText(chart: ChartDocument) {
const hasLevel = Object.keys(chart.data.tableFolders).length > 0;
return hasLevel ? FormatTables(chart.data.tableFolders) : "No Level";
}
return FormatDifficulty(chart);
return FormatDifficultyLong(chart);
}
@@ -6,7 +6,7 @@ import { ChangeOpacity } from "#util/color-opacity";
import React from "react";
import {
type ChartDocument,
FormatDifficulty,
FormatDifficultyLong,
FormatDifficultyShort,
type V3Game,
} from "tachi-common";
@@ -59,7 +59,9 @@ export default function DifficultyCell({
? DIFFICULTY_CELL_WIDTH_PX + 16
: DIFFICULTY_CELL_WIDTH_PX;
const nativeTitleTooltip = [FormatDifficulty(chart), chart.level].filter(Boolean).join(" — ");
const nativeTitleTooltip = [FormatDifficultyLong(chart), chart.level]
.filter(Boolean)
.join(" — ");
return (
<td
@@ -77,7 +79,7 @@ export default function DifficultyCell({
{!alwaysShort && (
<div className="d-none d-lg-block">
<span className={truncLineCls} style={{ minWidth: 0 }}>
{FormatDifficulty(chart)}
{FormatDifficultyLong(chart)}
</span>
</div>
)}
@@ -12,11 +12,11 @@ import Form from "react-bootstrap/Form";
import InputGroup from "react-bootstrap/InputGroup";
import { type integer } from "tachi-common";
import tachitableStyles from "./TachiTable.module.scss";
import FilterDirectivesIndicator from "./FilterDirectivesIndicator";
import NoDataWrapper from "./NoDataWrapper";
import PageSelector from "./PageSelector";
import SortableTH from "./SortableTH";
import tachitableStyles from "./TachiTable.module.scss";
const DEFAULT_TACHI_PAGE_LEN_OPTIONS: integer[] = [10, 25, 50, 100, 1000];
@@ -105,6 +105,11 @@ export default function TachiTable<D>({
defaultReverseSort?: boolean;
defaultSortMode?: string;
entryName: string;
/**
* When `nonce` changes (desktop breakpoints only), replaces the filter text — e.g.
* syncing from folder breakdown → table filter.
*/
externalSearchPreset?: { nonce: number; search: string } | null;
headers: Header<D>[];
noBottomDisplayPager?: boolean;
noTopDisplayStr?: boolean;
@@ -114,11 +119,6 @@ export default function TachiTable<D>({
/** Stable keys for tbody rows (e.g. chartID) so filtering reconciles instead of re-mounting. */
rowKey?: (row: D) => Key;
searchFunctions?: SearchFunctions<D>;
/**
* When `nonce` changes (desktop breakpoints only), replaces the filter text — e.g.
* syncing from folder breakdown → table filter.
*/
externalSearchPreset?: { nonce: number; search: string } | null;
}) {
const [search, setSearch] = useState("");
const lastExternalNonce = useRef<number | undefined>(undefined);
@@ -26,16 +26,15 @@ import { CreateRankingHeader } from "../headers/RankingHeader";
export type FolderEnumBreakdownTablePreset = {
metricKey: string;
valueLabel: string;
nonce: number;
valueLabel: string;
};
/** Scroll target used when jumping from folder breakdown chips to this table (desktop-only flow). */
export const FOLDER_FOLDER_TABLE_SCROLL_INTO_VIEW_ID =
"folder-folder-table-scroll-anchor";
export const FOLDER_FOLDER_TABLE_SCROLL_INTO_VIEW_ID = "folder-folder-table-scroll-anchor";
function formatFolderEnumTableFilter(metricKey: string, valueLabel: string): string {
const escaped = valueLabel.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
const escaped = valueLabel.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"');
return `${metricKey}=="${escaped}"`;
}
@@ -45,8 +44,8 @@ export default function FolderTable({
folderBreakdownEnumTablePreset = null,
}: {
dataset: FolderDataset;
game: V3Game;
folderBreakdownEnumTablePreset?: FolderEnumBreakdownTablePreset | null;
game: V3Game;
}) {
const defaultRating = useScoreRatingAlg(game);
@@ -12,7 +12,7 @@ import React, { type ChangeEventHandler, useContext, useEffect, useState } from
import { Button, Form, Modal } from "react-bootstrap";
import {
type FolderDocument,
FormatDifficulty,
FormatDifficultyLong,
GetGameConfig,
GetScoreMetricConf,
GetScoreMetrics,
@@ -177,7 +177,7 @@ function UGPTStatInnerSearchyBit({ game, onCreate, setShow }: GamePT & Props) {
data.push({
chartID: chart.chartID,
name: `${song!.title} ${FormatDifficulty(chart)}`,
name: `${song!.title} ${FormatDifficultyLong(chart)}`,
});
}
+1 -1
View File
@@ -165,7 +165,7 @@ export interface UGPTFolderEvolutionReturns {
songs: SongDocument<GameGroup>[];
}
export type UGPTEvolutionReplayReturns = UGPTTableEvolutionReturns | UGPTFolderEvolutionReturns;
export type UGPTEvolutionReplayReturns = UGPTFolderEvolutionReturns | UGPTTableEvolutionReturns;
export interface UGPTFolderReturns<GPT extends V3Game = V3Game> {
folder: FolderDocument;
+1 -1
View File
@@ -4,7 +4,7 @@ function pathnameOnlyForRouteMatch(str: string): string {
try {
return new URL(str).pathname;
} catch {
return str.split(/[?#]/)[0];
return str.split(/[?#]/u)[0];
}
}
@@ -102,12 +102,13 @@ export const GAME_ARCAEA_CONF = {
difficulties: {
type: "FIXED",
order: ["Past", "Present", "Future", "Beyond"],
format: {
formatShort: {
Past: "PST",
Present: "PRS",
Future: "FTR",
Beyond: "BYD",
},
formatLong: {},
default: "Future",
},
@@ -353,7 +353,8 @@ export const GAME_BMS_7K_CONF = {
// "CHART" isn't a very creative name, but this is in a similar vein to how
// games with no playtypes use the value "Single".
order: ["CHART"],
format: {},
formatShort: {},
formatLong: {},
default: "CHART",
},
@@ -137,13 +137,14 @@ export const GAME_CHUNITHM_CONF = {
difficulties: {
type: "CHUGEKIMAI_STYLE",
order: ["BASIC", "ADVANCED", "EXPERT", "MASTER", "ULTIMA"],
format: {
formatShort: {
BASIC: "B",
ADVANCED: "A",
EXPERT: "E",
MASTER: "M",
ULTIMA: "U",
},
formatLong: {},
default: "MASTER",
},
@@ -176,13 +176,14 @@ export const GAME_DDR_SP_CONF = {
difficulties: {
type: "FIXED",
order: DDR_DIFFICULTIES,
format: {
formatShort: {
BEGINNER: "BEG",
BASIC: "BAS",
DIFFICULT: "DIF",
EXPERT: "EXP",
CHALLENGE: "CHA",
},
formatLong: {},
default: "EXPERT",
},
@@ -96,7 +96,7 @@ export const GAME_GITADORA_GITA_CONF = {
"BASS EXTREME",
"BASS MASTER",
],
format: {
formatShort: {
BASIC: "G-BSC",
ADVANCED: "G-ADV",
EXTREME: "G-EXT",
@@ -106,6 +106,7 @@ export const GAME_GITADORA_GITA_CONF = {
"BASS EXTREME": "B-EXT",
"BASS MASTER": "B-MAS",
},
formatLong: {},
default: "EXTREME",
},
@@ -141,12 +142,13 @@ export const GAME_GITADORA_DORA_CONF = {
difficulties: {
type: "FIXED",
order: ["BASIC", "ADVANCED", "EXTREME", "MASTER"],
format: {
formatShort: {
BASIC: "D-BSC",
ADVANCED: "D-ADV",
EXTREME: "D-EXT",
MASTER: "D-MAS",
},
formatLong: {},
default: "EXTREME",
},
} as const satisfies INTERNAL_GAME_CONFIG;
@@ -237,7 +237,7 @@ export const GAME_IIDX_SP_CONF = {
"Kiraku ANOTHER",
"Kiraku LEGGENDARIA",
],
format: {
formatShort: {
NORMAL: "SPN",
HYPER: "SPH",
ANOTHER: "SPA",
@@ -255,6 +255,26 @@ export const GAME_IIDX_SP_CONF = {
"Kiraku ANOTHER": "SPA (Kr.)",
"Kiraku LEGGENDARIA": "SPL (Kr.)",
},
formatLong: {
NORMAL: "SP NORMAL",
HYPER: "SP HYPER",
ANOTHER: "SP ANOTHER",
LEGGENDARIA: "SP LEGGENDARIA",
// deliberately short
"All Scratch NORMAL": "SPN (Scr.)",
"All Scratch HYPER": "SPH (Scr.)",
"All Scratch ANOTHER": "SPA (Scr.)",
"All Scratch LEGGENDARIA": "SPL (Scr.)",
"Kichiku NORMAL": "SPN (Kc.)",
"Kichiku HYPER": "SPH (Kc.)",
"Kichiku ANOTHER": "SPA (Kc.)",
"Kichiku LEGGENDARIA": "SPL (Kc.)",
"Kiraku NORMAL": "SPN (Kr.)",
"Kiraku HYPER": "SPH (Kr.)",
"Kiraku ANOTHER": "SPA (Kr.)",
"Kiraku LEGGENDARIA": "SPL (Kr.)",
},
default: "ANOTHER",
},
@@ -371,7 +391,7 @@ export const GAME_IIDX_DP_CONF = {
"Kiraku ANOTHER",
"Kiraku LEGGENDARIA",
],
format: {
formatShort: {
NORMAL: "DPN",
HYPER: "DPH",
ANOTHER: "DPA",
@@ -389,6 +409,26 @@ export const GAME_IIDX_DP_CONF = {
"Kiraku ANOTHER": "DPA (Kr.)",
"Kiraku LEGGENDARIA": "DPL (Kr.)",
},
formatLong: {
NORMAL: "DP NORMAL",
HYPER: "DP HYPER",
ANOTHER: "DP ANOTHER",
LEGGENDARIA: "DP LEGGENDARIA",
// deliberately short
"All Scratch NORMAL": "DPN (Scr.)",
"All Scratch HYPER": "DPH (Scr.)",
"All Scratch ANOTHER": "DPA (Scr.)",
"All Scratch LEGGENDARIA": "DPL (Scr.)",
"Kichiku NORMAL": "DPN (Kc.)",
"Kichiku HYPER": "DPH (Kc.)",
"Kichiku ANOTHER": "DPA (Kc.)",
"Kichiku LEGGENDARIA": "DPL (Kc.)",
"Kiraku NORMAL": "DPN (Kr.)",
"Kiraku HYPER": "DPH (Kr.)",
"Kiraku ANOTHER": "DPA (Kr.)",
"Kiraku LEGGENDARIA": "DPL (Kr.)",
},
default: "ANOTHER",
},
} as const satisfies INTERNAL_GAME_CONFIG;
@@ -99,7 +99,7 @@ export const GAME_JUBEAT_SINGLE_CONF = {
difficulties: {
type: "FIXED",
order: ["BSC", "ADV", "EXT", "HARD BSC", "HARD ADV", "HARD EXT"],
format: {
formatShort: {
BSC: "BSC",
ADV: "ADV",
EXT: "EXT",
@@ -107,6 +107,7 @@ export const GAME_JUBEAT_SINGLE_CONF = {
"HARD ADV": "H. ADV",
"HARD EXT": "H. EXT",
},
formatLong: {},
default: "EXT",
},
@@ -188,7 +188,7 @@ export const GAME_MAIMAI_DX_CONF = {
"DX Master",
"DX Re:Master",
],
format: {
formatShort: {
Basic: "BAS",
Advanced: "ADV",
Expert: "EXP",
@@ -200,6 +200,7 @@ export const GAME_MAIMAI_DX_CONF = {
"DX Master": "DX MAS",
"DX Re:Master": "DX Re:MAS",
},
formatLong: {},
default: "Master",
},
@@ -128,7 +128,7 @@ export const GAME_MAIMAI_CONF = {
difficulties: {
type: "CHUGEKIMAI_STYLE",
order: ["Easy", "Basic", "Advanced", "Expert", "Master", "Re:Master"],
format: {
formatShort: {
Easy: "ESY",
Basic: "BAS",
Advanced: "ADV",
@@ -136,6 +136,7 @@ export const GAME_MAIMAI_CONF = {
Master: "MAS",
"Re:Master": "Re:MAS",
},
formatLong: {},
default: "Master",
},
@@ -86,7 +86,8 @@ export const GAME_MUSECA_CONF = {
difficulties: {
type: "FIXED",
order: ["Green", "Yellow", "Red"],
format: { Green: "G", Yellow: "Y", Red: "R" },
formatShort: { Green: "G", Yellow: "Y", Red: "R" },
formatLong: {},
default: "Red",
},
@@ -185,13 +185,14 @@ export const GAME_ONGEKI_CONF = {
difficulties: {
type: "FIXED",
order: ["BASIC", "ADVANCED", "EXPERT", "MASTER", "LUNATIC"],
format: {
formatShort: {
BASIC: "BAS",
ADVANCED: "ADV",
EXPERT: "EXP",
MASTER: "MAS",
LUNATIC: "LUN",
},
formatLong: {},
default: "MASTER",
},
@@ -127,7 +127,8 @@ export const GAME_PMS_CONTROLLER_CONF = {
difficulties: {
type: "FIXED",
order: ["CHART"],
format: {},
formatShort: {},
formatLong: {},
default: "CHART",
},
@@ -120,12 +120,13 @@ export const GAME_POPN_CONF = {
difficulties: {
type: "FIXED",
order: ["Easy", "Normal", "Hyper", "EX"],
format: {
formatShort: {
Easy: "E",
Normal: "N",
Hyper: "H",
EX: "EX",
},
formatLong: {},
default: "EX",
},
@@ -160,7 +160,8 @@ export const GAME_SDVX_CONF = {
difficulties: {
type: "FIXED",
order: ["NOV", "ADV", "EXH", "INF", "GRV", "HVN", "VVD", "XCD", "MXM", "ULT"],
format: {}, // they're all already short enough
formatShort: {}, // they're all already short enough
formatLong: {},
default: "EXH",
},
@@ -91,7 +91,8 @@ export const GAME_USC_CONTROLLER_CONF = {
order: ["NOV", "ADV", "EXH", "INF"],
// deliberately not ported to v3 - formatting of charts as
// "CON EXH" etc.
format: {},
formatShort: {},
formatLong: {},
default: "EXH",
},
@@ -115,12 +115,13 @@ export const GAME_WACCA_CONF = {
difficulties: {
type: "FIXED",
order: ["NORMAL", "HARD", "EXPERT", "INFERNO"],
format: {
formatShort: {
NORMAL: "NRM",
HARD: "HRD",
EXPERT: "EXP",
INFERNO: "INF",
},
formatLong: {},
default: "EXPERT",
},
@@ -130,9 +130,16 @@ export interface FixedDifficulties<Difficulty extends string> {
order: ReadonlyArray<Difficulty>;
/**
* How should we format these difficulty names?
* How should we format these difficulty names for short usage?
* if not specified, will use the difficulty name as-is
*/
format: Partial<Record<Difficulty, string>>;
formatShort: Partial<Record<Difficulty, string>>;
/**
* How should we format these difficulty names for long, full usage?
* if not specified, will use the difficulty name as-is
*/
formatLong: Partial<Record<Difficulty, string>>;
default: Difficulty;
}
@@ -161,7 +168,13 @@ export interface ChuGekiMaiDifficulties<TDifficulty extends string> {
* Dynamic ones (i.e. ones not in the Order set) are not formatted - they are printed
* exactly as-is.
*/
format: Partial<Record<TDifficulty, string>>;
formatShort: Partial<Record<TDifficulty, string>>;
/**
* How should we format these difficulty names for long, full usage?
* if not specified, will use the difficulty name as-is
*/
formatLong: Partial<Record<TDifficulty, string>>;
default: TDifficulty;
}
+20 -9
View File
@@ -45,12 +45,12 @@ export function FormatInt(v: number): string {
return Math.floor(v).toFixed(0);
}
export function FormatDifficulty(chart: ChartDocument): string {
const gameGroup = GameToGameGroup(chart.game);
if (gameGroup === "ongeki" || gameGroup === "chunithm" || gameGroup === "maimaidx") {
return `${chart.difficulty} ${chart.level}`.trim();
}
return FormatDifficultyShort(chart);
/**
* Formats a chart's difficulty into a longer variant. This handles a lot of
* game-specific strange edge cases.
*/
export function FormatDifficultyLong(chart: ChartDocument): string {
return FormatDifficultyInternal(chart, false);
}
/**
@@ -58,6 +58,10 @@ export function FormatDifficulty(chart: ChartDocument): string {
* game-specific strange edge cases.
*/
export function FormatDifficultyShort(chart: ChartDocument): string {
return FormatDifficultyInternal(chart, true);
}
function FormatDifficultyInternal(chart: ChartDocument, short: boolean): string {
const gameGroup = GameToGameGroup(chart.game);
const gameConfig = GetGameConfig(chart.game);
@@ -84,7 +88,14 @@ export function FormatDifficultyShort(chart: ChartDocument): string {
gameConfig.difficulties.type === "FIXED" ||
gameConfig.difficulties.type === "CHUGEKIMAI_STYLE"
) {
const diff = gameConfig.difficulties.format[chart.difficulty] ?? chart.difficulty;
let diff;
if (short) {
diff = gameConfig.difficulties.formatShort[chart.difficulty];
} else {
diff = gameConfig.difficulties.formatLong[chart.difficulty];
}
diff ??= chart.difficulty;
return `${diff} ${chart.level}`.trim();
}
@@ -97,7 +108,7 @@ export function FormatDifficultyShort(chart: ChartDocument): string {
* Formats a chart's difficulty for searching, such as forwarding this query to youtube.
*/
export function FormatDifficultySearch(chart: ChartDocument): string | null {
return FormatDifficulty(chart);
return FormatDifficultyLong(chart);
}
export function FormatGame(game: V3Game): string {
@@ -189,7 +200,7 @@ export function FormatChart(chart: ChartDocument): string {
if (gameConfig.difficulties.type === "DYNAMIC") {
diff = chart.difficulty;
} else {
diff = gameConfig.difficulties.format[chart.difficulty] ?? chart.difficulty;
diff = gameConfig.difficulties.formatShort[chart.difficulty] ?? chart.difficulty;
}
// return the most recent version this chart appeared in if it
@@ -1,21 +1,21 @@
import type { ConfEnumScoreMetric } from "tachi-common/types/metrics";
import { SELECT_CHART, ToChartDocument } from "#lib/db-formats/chart";
import { GetSongsByIDs } from "#lib/db-formats/song";
import DB from "#services/pg/db";
import { GetScoresForTableEvolution } from "#utils/queries/scores";
import {
type ChartDocument,
type FolderDocument,
GetGameConfig,
GetScoreEnumConfs,
type integer,
type ChartDocument,
type FolderDocument,
type ScoreDocument,
type SongDocument,
type TableDocument,
type V3Game,
} from "tachi-common";
import type { ConfEnumScoreMetric } from "tachi-common/types/metrics";
import { GetFolderChartIDs } from "./folders";
/** One strict improvement on a `(chart × metric)` enum index within table evolution semantics. */
@@ -29,7 +29,9 @@ export interface TableEvolutionEvent {
value: string;
}
export async function GetTableEvolutionFolderChartMembership(folders: Array<FolderDocument>): Promise<{
export async function GetTableEvolutionFolderChartMembership(
folders: Array<FolderDocument>,
): Promise<{
distinctChartIDs: Array<string>;
folderChartIDs: Record<string, Array<string>>;
}> {
@@ -77,12 +79,17 @@ export function ComputeTableEvolutionEvents(
let currentChartID: string | undefined;
const prevMaxForMetric: Record<string, integer> = {};
const readEnumIdx = (scoreData: ScoreDocument["scoreData"], metric: string): integer | undefined => {
const readEnumIdx = (
scoreData: ScoreDocument["scoreData"],
metric: string,
): integer | undefined => {
const fromRoot = scoreData.enumIndexes?.[metric as keyof typeof scoreData.enumIndexes] as
| integer
| undefined;
const fromOptional =
scoreData.optional?.enumIndexes?.[metric as keyof typeof scoreData.optional.enumIndexes];
scoreData.optional?.enumIndexes?.[
metric as keyof typeof scoreData.optional.enumIndexes
];
const coerced = typeof fromOptional === "number" ? fromOptional : undefined;
@@ -160,7 +167,8 @@ export async function LoadTableEvolutionPayload(
}> {
const enumConfs = GetScoreEnumConfs(GetGameConfig(game));
const { distinctChartIDs, folderChartIDs } = await GetTableEvolutionFolderChartMembership(folders);
const { distinctChartIDs, folderChartIDs } =
await GetTableEvolutionFolderChartMembership(folders);
const scores = await GetScoresForTableEvolution(userID, game, distinctChartIDs);
const events = ComputeTableEvolutionEvents(scores, enumConfs);
@@ -183,7 +191,9 @@ export async function LoadFolderEvolutionPayload(
}> {
const enumConfs = GetScoreEnumConfs(GetGameConfig(game));
const { distinctChartIDs, folderChartIDs } = await GetTableEvolutionFolderChartMembership([folder]);
const { distinctChartIDs, folderChartIDs } = await GetTableEvolutionFolderChartMembership([
folder,
]);
const scores = await GetScoresForTableEvolution(userID, game, distinctChartIDs);
const events = ComputeTableEvolutionEvents(scores, enumConfs);
@@ -257,17 +257,14 @@ describe("GET /api/v1/users/:userID/games/:game/folders/:folderSlug/evolution",
const uid = `${Date.now()}_${Math.floor(Math.random() * 100_000)}`;
async function insScore(scoreId: string, lamp: string, iso: string) {
const { data, derived, judgements } = mongoScoreDataToPg(
"iidx-sp",
{
score: 1400,
lamp,
percent: 93,
optional: {},
judgements: { pgreat: 1, great: 0 },
grade: "AAA",
} as ScoreData<"iidx-sp">,
);
const { data, derived, judgements } = mongoScoreDataToPg("iidx-sp", {
score: 1400,
lamp,
percent: 93,
optional: {},
judgements: { pgreat: 1, great: 0 },
grade: "AAA",
} as ScoreData<"iidx-sp">);
await DB.insertInto("score")
.values({
@@ -138,17 +138,14 @@ describe("GET /api/v1/users/:userID/games/:game/tables/:tableID", () => {
const uid = `${Date.now()}_${Math.floor(Math.random() * 100_000)}`;
async function insScore(scoreId: string, lamp: string, iso: string) {
const { data, derived, judgements } = mongoScoreDataToPg(
"iidx-sp",
{
score: 1400,
lamp,
percent: 93,
optional: {},
judgements: { pgreat: 1, great: 0 },
grade: "AAA",
} as ScoreData<"iidx-sp">,
);
const { data, derived, judgements } = mongoScoreDataToPg("iidx-sp", {
score: 1400,
lamp,
percent: 93,
optional: {},
judgements: { pgreat: 1, great: 0 },
grade: "AAA",
} as ScoreData<"iidx-sp">);
await DB.insertInto("score")
.values({
@@ -190,9 +187,7 @@ describe("GET /api/v1/users/:userID/games/:game/tables/:tableID", () => {
expect(lampEvents[0].value).toBe("EASY CLEAR");
expect(lampEvents[1].value).toBe("HARD CLEAR");
expect(res.body.body.folderChartIDs["ugtfolder"]).toContain(
TestingIIDXSPScorePB.chartID,
);
expect(res.body.body.folderChartIDs.ugtfolder).toContain(TestingIIDXSPScorePB.chartID);
});
it("returns 404 when the table does not exist", async () => {