mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-01 19:38:13 +03:00
i forgot
This commit is contained in:
@@ -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(
|
||||
[
|
||||
|
||||
+3
-1
@@ -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>
|
||||
|
||||
+19
-18
@@ -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 ? (
|
||||
|
||||
+13
-14
@@ -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}
|
||||
|
||||
+12
-2
@@ -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 +
|
||||
|
||||
+1
-4
@@ -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";
|
||||
|
||||
+1
-4
@@ -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} />;
|
||||
|
||||
+3
-5
@@ -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)}`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
+8
-11
@@ -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({
|
||||
|
||||
+9
-14
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user