DDR Implementation (#1132)

* feat: DDR Implementation

* style: lint fixes

* chore: update rg-stats

* feat: ddr konaste songs and charts update
This commit is contained in:
Gyoo
2024-08-09 19:08:49 +01:00
committed by GitHub
parent 65a79eac2b
commit 4254e29917
32 changed files with 165652 additions and 46 deletions
+1 -1
View File
@@ -12,4 +12,4 @@ TACHI_NAME="Tachi Local Dev"
# turn this off if you're on linux for actually good performance
# this needs to be on for windows because lol lmao
FORCE_FS_POLLING="true"
FORCE_FS_POLLING="true"
+2 -2
View File
@@ -68,7 +68,7 @@
"react-router-bootstrap": "^0.25.0",
"react-router-dom": "5.1.2",
"react-select": "^5.6.1",
"rg-stats": "0.5.6",
"rg-stats": "0.5.8",
"sync-fetch": "^0.3.1",
"tachi-common": "workspace:../common",
"vite-plugin-html": "^3.2.0"
@@ -91,4 +91,4 @@
"node": "20",
"pnpm": "8"
}
}
}
@@ -397,6 +397,7 @@ function InputAlert({ game }: { game: Game }) {
case "popn":
case "wacca":
case "ongeki":
case "ddr":
return (
<>
<strong>Scores must be achieved on an arcade-size controller!</strong>
@@ -29,6 +29,8 @@ const GPT_UTILS: Record<GPTString, Array<GPTUtility>> = {
"maimaidx:Single": [],
"maimai:Single": [],
"ongeki:Single": [],
"ddr:SP": [],
"ddr:DP": [],
};
export function GetGPTUtils(game: Game, playtype: Playtype) {
@@ -0,0 +1,28 @@
import { ChangeOpacity } from "util/color-opacity";
import React from "react";
import { integer } from "tachi-common";
export default function DDRScoreCell({
score,
colour,
grade,
scoreRenderFn,
}: {
score?: integer;
grade: string;
colour: string;
showScore?: boolean;
scoreRenderFn?: (s: number) => string;
}) {
return (
<td
style={{
backgroundColor: ChangeOpacity(colour, 0.2),
}}
>
<strong>{grade}</strong>
<br />
{score !== undefined && <>{scoreRenderFn ? scoreRenderFn(score) : score}</>}
</td>
);
}
+3
View File
@@ -27,6 +27,7 @@ import { GPTClientImplementation } from "./types";
import { SDVX_IMPL, USC_IMPL } from "./games/sdvx-usc";
import { GITADORA_DORA_IMPL, GITADORA_GITA_IMPL } from "./games/gitadora";
import { ARCAEA_TOUCH_IMPL } from "./games/arcaea";
import { DDR_DP_IMPL, DDR_SP_IMPL } from "./games/ddr";
type GPTClientImplementations = {
[GPT in GPTString]: GPTClientImplementation<GPT>;
@@ -40,6 +41,8 @@ const defaultEnumIcons = {
export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
"iidx:SP": IIDX_SP_IMPL,
"iidx:DP": IIDX_DP_IMPL,
"ddr:SP": DDR_SP_IMPL,
"ddr:DP": DDR_DP_IMPL,
"chunithm:Single": {
sessionImportantScoreCount: 30,
enumIcons: defaultEnumIcons,
+154
View File
@@ -0,0 +1,154 @@
import { NumericSOV } from "util/sorts";
import { GPTClientImplementation } from "lib/types";
import { COLOUR_SET, GPTStrings } from "tachi-common";
import ScoreCell from "components/tables/cells/ScoreCell";
import { GetEnumColour } from "lib/game-implementations";
import React from "react";
import RatingCell from "components/tables/cells/RatingCell";
import LampCell from "../../components/tables/cells/LampCell";
import DDRScoreCell from "../../components/tables/cells/DDRScoreCell";
import { bg, bgc } from "./_util";
const DDR_ENUM_COLOURS: GPTClientImplementation<GPTStrings["ddr"]>["enumColours"] = {
grade: {
E: COLOUR_SET.gray,
D: COLOUR_SET.paleBlue,
"D+": COLOUR_SET.paleBlue,
"C-": COLOUR_SET.purple,
C: COLOUR_SET.purple,
"C+": COLOUR_SET.purple,
"B-": COLOUR_SET.blue,
B: COLOUR_SET.blue,
"B+": COLOUR_SET.blue,
"A-": COLOUR_SET.gold,
A: COLOUR_SET.gold,
"A+": COLOUR_SET.gold,
"AA-": COLOUR_SET.gold,
AA: COLOUR_SET.gold,
"AA+": COLOUR_SET.gold,
AAA: COLOUR_SET.teal,
},
lamp: {
FAILED: COLOUR_SET.maroon,
ASSIST: COLOUR_SET.purple,
CLEAR: COLOUR_SET.blue,
"FULL COMBO": COLOUR_SET.vibrantBlue,
"GREAT FULL COMBO": COLOUR_SET.vibrantGreen,
"PERFECT FULL COMBO": COLOUR_SET.gold,
"MARVELOUS FULL COMBO": COLOUR_SET.pink,
LIFE4: COLOUR_SET.vibrantRed,
},
};
const DDR_DIFF_COLOURS: GPTClientImplementation<GPTStrings["ddr"]>["difficultyColours"] = {
BEGINNER: COLOUR_SET.blue,
BASIC: COLOUR_SET.paleGreen,
DIFFICULT: COLOUR_SET.red,
EXPERT: COLOUR_SET.vibrantYellow,
CHALLENGE: COLOUR_SET.purple,
};
const DDR_HEADERS: GPTClientImplementation<"ddr:SP" | "ddr:DP">["scoreHeaders"] = [
["Score", "Score", NumericSOV((x) => x.scoreData.score)],
["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)],
];
const DDR_COLOURS: GPTClientImplementation<"ddr:SP" | "ddr:DP">["classColours"] = {
flare: {
NONE: bg("gray"),
"NONE+": bg("gray"),
"NONE++": bg("gray"),
"NONE+++": bg("gray"),
MERCURY: bg("blue"),
"MERCURY+": bg("blue"),
"MERCURY++": bg("blue"),
"MERCURY+++": bg("blue"),
VENUS: bgc("yellow", "black"),
"VENUS+": bgc("yellow", "black"),
"VENUS++": bgc("yellow", "black"),
"VENUS+++": bgc("yellow", "black"),
EARTH: bg("forestgreen"),
"EARTH+": bg("forestgreen"),
"EARTH++": bg("forestgreen"),
"EARTH+++": bg("forestgreen"),
MARS: bg("red"),
"MARS+": bg("red"),
"MARS++": bg("red"),
"MARS+++": bg("red"),
JUPITER: bg("darkgreen"),
"JUPITER+": bg("darkgreen"),
"JUPITER++": bg("darkgreen"),
"JUPITER+++": bg("darkgreen"),
SATURN: bg("purple"),
"SATURN+": bg("purple"),
"SATURN++": bg("purple"),
"SATURN+++": bg("purple"),
URANUS: bgc("powderblue", "black"),
"URANUS+": bgc("powderblue", "black"),
"URANUS++": bgc("powderblue", "black"),
"URANUS+++": bgc("powderblue", "black"),
NEPTUNE: bg("darkslateblue"),
"NEPTUNE+": bg("darkslateblue"),
"NEPTUNE++": bg("darkslateblue"),
"NEPTUNE+++": bg("darkslateblue"),
SUN: bg("orange"),
"SUN+": bg("orange"),
"SUN++": bg("orange"),
"SUN+++": bg("orange"),
WORLD: bg("black"),
},
};
const DDRCoreCells: GPTClientImplementation<GPTStrings["ddr"]>["scoreCoreCells"] = ({
sc,
chart,
}) => (
<>
<DDRScoreCell
colour={GetEnumColour(sc, "grade")}
grade={sc.scoreData.grade}
score={sc.scoreData.score}
/>
<LampCell lamp={sc.scoreData.lamp} colour={GetEnumColour(sc, "lamp")} />
</>
);
const DDRRatingCell: GPTClientImplementation<GPTStrings["ddr"]>["ratingCell"] = ({
sc,
chart,
rating,
}) => (
<>
<RatingCell rating={rating} score={sc} />
</>
);
export const DDR_SP_IMPL: GPTClientImplementation<"ddr:SP"> = {
sessionImportantScoreCount: 20,
difficultyColours: DDR_DIFF_COLOURS,
enumColours: DDR_ENUM_COLOURS,
enumIcons: {
grade: "sort-alpha-up",
lamp: "lightbulb",
},
ratingSystems: [],
scoreHeaders: DDR_HEADERS,
classColours: DDR_COLOURS,
scoreCoreCells: DDRCoreCells,
ratingCell: DDRRatingCell,
};
export const DDR_DP_IMPL: GPTClientImplementation<"ddr:DP"> = {
sessionImportantScoreCount: 20,
difficultyColours: DDR_DIFF_COLOURS,
enumColours: DDR_ENUM_COLOURS,
enumIcons: {
grade: "sort-alpha-up",
lamp: "lightbulb",
},
ratingSystems: [],
scoreHeaders: DDR_HEADERS,
classColours: DDR_COLOURS,
scoreCoreCells: DDRCoreCells,
ratingCell: DDRRatingCell,
};
+4
View File
@@ -3,6 +3,7 @@
import { ARCAEA_CONF, ARCAEA_TOUCH_CONF } from "./game-support/arcaea";
import { BMS_14K_CONF, BMS_7K_CONF, BMS_CONF } from "./game-support/bms";
import { CHUNITHM_CONF, CHUNITHM_SINGLE_CONF } from "./game-support/chunithm";
import { DDR_CONF, DDR_DP_CONF, DDR_SP_CONF } from "./game-support/ddr";
import { GITADORA_CONF, GITADORA_DORA_CONF, GITADORA_GITA_CONF } from "./game-support/gitadora";
import { IIDX_CONF, IIDX_DP_CONF, IIDX_SP_CONF } from "./game-support/iidx";
import { ITG_CONF, ITG_STAMINA_CONF } from "./game-support/itg";
@@ -50,6 +51,7 @@ export const GAME_CONFIGS = {
itg: ITG_CONF,
arcaea: ARCAEA_CONF,
ongeki: ONGEKI_CONF,
ddr: DDR_CONF,
} as const satisfies Record<string, INTERNAL_GAME_CONFIG>;
/**
@@ -98,6 +100,8 @@ export const GAME_PT_CONFIGS = {
"itg:Stamina": ITG_STAMINA_CONF,
"arcaea:Touch": ARCAEA_TOUCH_CONF,
"ongeki:Single": ONGEKI_SINGLE_CONF,
"ddr:SP": DDR_SP_CONF,
"ddr:DP": DDR_DP_CONF,
} as const satisfies Record<GPTString, INTERNAL_GAME_PT_CONFIG>;
/**
+195
View File
@@ -0,0 +1,195 @@
import { IIDXDans } from "./iidx";
import { FmtNum, FmtPercent, FmtScoreNoCommas } from "../../utils/util";
import { ClassValue, zodNonNegativeInt } from "../config-utils";
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
export const DDR_FLARE_CATEGORIES = z.enum(["CLASSIC", "WHITE", "GOLD", "NONE"]);
export const DDR_DIFFICULTIES = ["BEGINNER", "BASIC", "DIFFICULT", "EXPERT", "CHALLENGE"];
export const DDRFlare = [
ClassValue("NONE", "NONE"),
ClassValue("NONE+", "NONE+"),
ClassValue("NONE++", "NONE++"),
ClassValue("NONE+++", "NONE+++"),
ClassValue("MERCURY", "水星 / MERCURY"),
ClassValue("MERCURY+", "水星 / MERCURY+"),
ClassValue("MERCURY++", "水星 / MERCURY++"),
ClassValue("MERCURY+++", "水星 / MERCURY+++"),
ClassValue("VENUS", "金星 / VENUS"),
ClassValue("VENUS+", "金星 / VENUS+"),
ClassValue("VENUS++", "金星 / VENUS++"),
ClassValue("VENUS+++", "金星 / VENUS+++"),
ClassValue("EARTH", "地球 / EARTH"),
ClassValue("EARTH+", "地球 / EARTH+"),
ClassValue("EARTH++", "地球 / EARTH++"),
ClassValue("EARTH+++", "地球 / EARTH+++"),
ClassValue("MARS", "火星 / MARS"),
ClassValue("MARS+", "火星 / MARS+"),
ClassValue("MARS++", "火星 / MARS++"),
ClassValue("MARS+++", "火星 / MARS+++"),
ClassValue("JUPITER", "木星 / JUPITER"),
ClassValue("JUPITER+", "木星 / JUPITER+"),
ClassValue("JUPITER++", "木星 / JUPITER++"),
ClassValue("JUPITER+++", "木星 / JUPITER+++"),
ClassValue("SATURN", "土星 / SATURN"),
ClassValue("SATURN+", "土星 / SATURN+"),
ClassValue("SATURN++", "土星 / SATURN++"),
ClassValue("SATURN+++", "土星 / SATURN+++"),
ClassValue("URANUS", "天王星 / URANUS"),
ClassValue("URANUS+", "天王星 / URANUS+"),
ClassValue("URANUS++", "天王星 / URANUS++"),
ClassValue("URANUS+++", "天王星 / URANUS+++"),
ClassValue("NEPTUNE", "海王星 / NEPTUNE"),
ClassValue("NEPTUNE+", "海王星 / NEPTUNE+"),
ClassValue("NEPTUNE++", "海王星 / NEPTUNE++"),
ClassValue("NEPTUNE+++", "海王星 / NEPTUNE+++"),
ClassValue("SUN", "太陽 / SUN"),
ClassValue("SUN+", "太陽 / SUN+"),
ClassValue("SUN++", "太陽 / SUN++"),
ClassValue("SUN+++", "太陽 / SUN+++"),
ClassValue("WORLD", "世界 / WORLD"),
];
export const DDR_CONF = {
name: "DDR",
playtypes: ["SP", "DP"],
songData: z.strictObject({
inGameID: zodNonNegativeInt,
flareCategory: DDR_FLARE_CATEGORIES,
}),
} as const satisfies INTERNAL_GAME_CONFIG;
export const DDR_SP_CONF = {
providedMetrics: {
score: {
type: "INTEGER",
validate: p.isBetween(0, 1_000_000),
formatter: FmtNum,
description: "The score value. This is between 0 and 1 million.",
},
lamp: {
type: "ENUM",
values: [
"FAILED",
"ASSIST",
"CLEAR",
"FULL COMBO",
"GREAT FULL COMBO",
"PERFECT FULL COMBO",
"MARVELOUS FULL COMBO",
"LIFE4",
],
minimumRelevantValue: "CLEAR",
description: "The type of clear this user got.",
},
},
derivedMetrics: {
grade: {
type: "ENUM",
values: [
"E",
"D",
"D+",
"C-",
"C",
"C+",
"B-",
"B",
"B+",
"A-",
"A",
"A+",
"AA-",
"AA",
"AA+",
"AAA",
],
minimumRelevantValue: "D",
description:
"The grade this score was. Note that grades are capped at F if this was a fail.",
},
},
optionalMetrics: {
flare: {
type: "ENUM",
values: ["0", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "EX"],
minimumRelevantValue: "0",
description: "The Flare rank. If no Flare is provided, Flare 0 is chosen by default.",
},
},
defaultMetric: "score",
preferredDefaultEnum: "lamp",
scoreRatingAlgs: {
flareSkill: {
description: "Flare Skill as it's implemented in DDR World.",
formatter: FmtScoreNoCommas,
},
},
sessionRatingAlgs: {
flareSkill: {
description: "Average of your 10 best Flare Points this session",
formatter: FmtScoreNoCommas,
},
},
profileRatingAlgs: {
flareSkill: {
description:
"Flare Skill as it's implemented in DDR World, taking 30 best flare points from 3 different categories: CLASSIC (DDR 1st~X3 vs 2ndMIX), WHITE (DDR(2013)~DDR A), GOLD (DDR A20~WORLD).",
formatter: FmtScoreNoCommas,
},
},
defaultScoreRatingAlg: "flareSkill",
defaultProfileRatingAlg: "flareSkill",
defaultSessionRatingAlg: "flareSkill",
difficulties: {
type: "FIXED",
order: DDR_DIFFICULTIES,
shorthand: {
BEGINNER: "BEG",
BASIC: "BAS",
DIFFICULT: "DIF",
EXPERT: "EXP",
CHALLENGE: "CHA",
},
default: "EXPERT",
},
classes: {
flare: {
type: "DERIVED",
values: DDRFlare,
},
},
orderedJudgements: ["MARVELOUS", "PERFECT", "GREAT", "GOOD", "MISS", "OK"],
versions: {
a3: "A3",
konaste: "Konaste",
},
chartData: z.strictObject({
inGameID: zodNonNegativeInt,
}),
preferences: z.strictObject({}),
scoreMeta: z.strictObject({}),
supportedMatchTypes: ["inGameID", "songTitle", "tachiSongID"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
export const DDR_DP_CONF = {
...DDR_SP_CONF,
} as const satisfies INTERNAL_GAME_PT_CONFIG;
+20
View File
@@ -221,3 +221,23 @@ export const ONGEKI_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"ongeki:Singl
SSS: 1_000_000,
"SSS+": 1_007_500,
});
export const DDR_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<GPTStrings["ddr"], "grade">>({
AAA: 990000,
"AA+": 950000,
AA: 900000,
"AA-": 890000,
"A+": 850000,
A: 800000,
"A-": 790000,
"B+": 750000,
B: 700000,
"B-": 690000,
"C+": 650000,
C: 600000,
"C-": 590000,
"D+": 550000,
D: 0,
// cannot be achieved. This can only be attained if the player failed the chart.
E: -Infinity,
});
+1
View File
@@ -131,6 +131,7 @@ export function FormatDifficultySearch(chart: ChartDocument, game: Game): string
case "arcaea":
case "ongeki":
case "chunithm":
case "ddr":
case "usc":
return chart.difficulty;
case "iidx":
File diff suppressed because it is too large Load Diff
+646
View File
@@ -12337,6 +12337,652 @@
"title": "ULTIMA (SUN)",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "1",
"table": "Level "
}
}
},
"folderID": "F0c09b0447497e6ce7016e593f26fb860526c47705e4823fe8e31079b8dc20814",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 1",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "10",
"table": "Level "
}
}
},
"folderID": "F0c9a877c6bedb79a8a49edfc353b1051f85752b57c91a2ae6d12bed0a3196417",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 10",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "11",
"table": "Level "
}
}
},
"folderID": "F520973c7d85f0d197862a897b1a81fc931080e185f6b098be2028ba22c492fc7",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 11",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "12",
"table": "Level "
}
}
},
"folderID": "Fc5acf2723ed0ae888f129be0b77d5414a6fab3420eb5e02d5eb3c08a3fa49741",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 12",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "13",
"table": "Level "
}
}
},
"folderID": "F80d3a7b581735e1b0c8f1669937fad455ec66b34f3c55457278f4cfd36b1c115",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 13",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "14",
"table": "Level "
}
}
},
"folderID": "Ff975e532c7589db66cc619384cf45ee1d9a14d775be4efd2c9253ba8630b13ed",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 14",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "15",
"table": "Level "
}
}
},
"folderID": "Fcdd639a6c30acc4703b98ba6ee87613229bfe726c0f5a3438ce913b52c68e537",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 15",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "16",
"table": "Level "
}
}
},
"folderID": "F408f16670afeff015eb0d8f1c149013a3cb2b29bd474bc676e5993ca25b312cd",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 16",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "17",
"table": "Level "
}
}
},
"folderID": "Fdde615d765569b32535711ef67fd85dde59c31999dda1f48176529f03b6e5de3",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 17",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "18",
"table": "Level "
}
}
},
"folderID": "F9934345ea33451bd21be4151a5ddc130aa735960e8bf9ff66a060e42819f3888",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 18",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "19",
"table": "Level "
}
}
},
"folderID": "Fc6d420869984a20cc8954cff43df46c49b0e2c06998b94e3226655bf13c75d3c",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 19",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "2",
"table": "Level "
}
}
},
"folderID": "F1e62ed269483936c348678b307c649bf674e0d3a3735d9635ed43dae79caa7c0",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 2",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "3",
"table": "Level "
}
}
},
"folderID": "F848646e7a6689964ef7ca99672e32d41560dd4e50b49a2247e800fbff1958323",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 3",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "4",
"table": "Level "
}
}
},
"folderID": "F83239573a4f29a9ac847425afba58cf41fc43bc74d415daf5826611936b59da2",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 4",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "5",
"table": "Level "
}
}
},
"folderID": "F37df248ab2f58f732f24940293c5e91c4aaf5e64344d5fb1cd192a9e4b0d8d45",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 5",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "6",
"table": "Level "
}
}
},
"folderID": "F1e794bc73e08cd2843130b7a9df76877941b3a4d2789ec76c2c3bab878c9f43d",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 6",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "7",
"table": "Level "
}
}
},
"folderID": "Fb59f8945c3d7b0eaa113c9899e9ae55d5a7ad7e157d374c0d9a74235287d4794",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 7",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "8",
"table": "Level "
}
}
},
"folderID": "F2463f6bc988f960f2b6e822187805a35b7c66412e9cfecf2ba0f81c2a9aacebc",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 8",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "9",
"table": "Level "
}
}
},
"folderID": "F5efd26d77d73dc52c934ff7e97b554de63e20ead805525069bc4993cf4d50ffb",
"game": "ddr",
"inactive": false,
"playtype": "DP",
"searchTerms": [],
"title": "Level 9",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "1",
"table": "Level "
}
}
},
"folderID": "F298d0411534d7b38b7cbca14a43a1e4de71f30d73a5bdbb7f677b30fcc6e6cc2",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 1",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "10",
"table": "Level "
}
}
},
"folderID": "Ff99e411eb0e3dbaa2dab8e52dda4ef35ccc327c788140c10c2d2a286c55cdd46",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 10",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "11",
"table": "Level "
}
}
},
"folderID": "F5437f25765ead737606c7fd9042163a0063532c125fe17d9e77d113b7d1ac7ef",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 11",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "12",
"table": "Level "
}
}
},
"folderID": "F9ad83b2f07753c7fbebee715cbaddd0e444bf6c808e4b26fdb8fcd42a46bab92",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 12",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "13",
"table": "Level "
}
}
},
"folderID": "Fe80638cfba7c94e1dbf7c6a8e094ee1b771f886f9b083b5d9ed6d89fbe24290d",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 13",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "14",
"table": "Level "
}
}
},
"folderID": "F5a72b2268e14987eec04b9bd471740109354a683151443469ad44cada067fb12",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 14",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "15",
"table": "Level "
}
}
},
"folderID": "F8a04baa09fb2466d8f5c45d3223a4fd75dd2cff80b09dd3db87d54bee98ee49c",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 15",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "16",
"table": "Level "
}
}
},
"folderID": "F50bad5092469b33ef153acb28aa545a7f5cd0b6ff699ef5e6145667f4377508d",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 16",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "17",
"table": "Level "
}
}
},
"folderID": "F0d6274231e5060bb8f1b4f72f3e0b132825629bbc9a193b3fe8781aa10c628e3",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 17",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "18",
"table": "Level "
}
}
},
"folderID": "Ffb7d33d91fc0ab5c613dd200a51d595fca03d9252f5c10d0a55952c769aeae66",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 18",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "19",
"table": "Level "
}
}
},
"folderID": "F210ff4a4b62852c199717f07532d74371db48ac8922fec0496b8f97813d8c1c7",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 19",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "2",
"table": "Level "
}
}
},
"folderID": "F2f3c320dcb69a9810f88f1bef0ec357b6f25d1200dab5daea6c7d88b4021f0ea",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 2",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "3",
"table": "Level "
}
}
},
"folderID": "F0f6e04147a238bb365fa78d8b45d53f8fb7af3b2b0084c0e7630512c6fb28f94",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 3",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "4",
"table": "Level "
}
}
},
"folderID": "F374c59150d51b54d8096c9873095e4e425d615f35ed88bb03ff1f8c939dcf345",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 4",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "5",
"table": "Level "
}
}
},
"folderID": "F319b480a6607d528b8a630d5a08904c01b5d1357e5c80a23bab5e03cb2d8d6fa",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 5",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "6",
"table": "Level "
}
}
},
"folderID": "F8f4111b72204384257bfd6acd615dd50e28414348efc773a28073fed4165c71f",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 6",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "7",
"table": "Level "
}
}
},
"folderID": "F20ee236c6cb25263eac9af9c9d873b741499778995e3f6ca97a688410ee948b2",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 7",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "8",
"table": "Level "
}
}
},
"folderID": "F35e045b4f2b46bf783ff17ab67e2e834f36561cc7eb7ffeeba2a69e5538bcb01",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 8",
"type": "charts"
},
{
"data": {
"data¬tableFolders": {
"~elemMatch": {
"level": "9",
"table": "Level "
}
}
},
"folderID": "F5379a63559ac7925560c02e98fc8a32c4a8210c989203cb8deaac01120c017b0",
"game": "ddr",
"inactive": false,
"playtype": "SP",
"searchTerms": [],
"title": "Level 9",
"type": "charts"
},
{
"data": {
"levelNum": {
File diff suppressed because it is too large Load Diff
+60
View File
@@ -1313,6 +1313,66 @@
"tableID": "chunithm-Single-sun-difficulties",
"title": "CHUNITHM (SUN) (Difficulties)"
},
{
"default": true,
"description": "All songs in DDR A3",
"folders": [
"F0c09b0447497e6ce7016e593f26fb860526c47705e4823fe8e31079b8dc20814",
"F1e62ed269483936c348678b307c649bf674e0d3a3735d9635ed43dae79caa7c0",
"F848646e7a6689964ef7ca99672e32d41560dd4e50b49a2247e800fbff1958323",
"F83239573a4f29a9ac847425afba58cf41fc43bc74d415daf5826611936b59da2",
"F37df248ab2f58f732f24940293c5e91c4aaf5e64344d5fb1cd192a9e4b0d8d45",
"F1e794bc73e08cd2843130b7a9df76877941b3a4d2789ec76c2c3bab878c9f43d",
"Fb59f8945c3d7b0eaa113c9899e9ae55d5a7ad7e157d374c0d9a74235287d4794",
"F2463f6bc988f960f2b6e822187805a35b7c66412e9cfecf2ba0f81c2a9aacebc",
"F5efd26d77d73dc52c934ff7e97b554de63e20ead805525069bc4993cf4d50ffb",
"F0c9a877c6bedb79a8a49edfc353b1051f85752b57c91a2ae6d12bed0a3196417",
"F520973c7d85f0d197862a897b1a81fc931080e185f6b098be2028ba22c492fc7",
"Fc5acf2723ed0ae888f129be0b77d5414a6fab3420eb5e02d5eb3c08a3fa49741",
"F80d3a7b581735e1b0c8f1669937fad455ec66b34f3c55457278f4cfd36b1c115",
"Ff975e532c7589db66cc619384cf45ee1d9a14d775be4efd2c9253ba8630b13ed",
"Fcdd639a6c30acc4703b98ba6ee87613229bfe726c0f5a3438ce913b52c68e537",
"F408f16670afeff015eb0d8f1c149013a3cb2b29bd474bc676e5993ca25b312cd",
"Fdde615d765569b32535711ef67fd85dde59c31999dda1f48176529f03b6e5de3",
"F9934345ea33451bd21be4151a5ddc130aa735960e8bf9ff66a060e42819f3888",
"Fc6d420869984a20cc8954cff43df46c49b0e2c06998b94e3226655bf13c75d3c"
],
"game": "ddr",
"inactive": false,
"playtype": "DP",
"tableID": "ddr-DP-a3",
"title": "DDR A3"
},
{
"default": true,
"description": "All songs in DDR A3",
"folders": [
"F298d0411534d7b38b7cbca14a43a1e4de71f30d73a5bdbb7f677b30fcc6e6cc2",
"F2f3c320dcb69a9810f88f1bef0ec357b6f25d1200dab5daea6c7d88b4021f0ea",
"F0f6e04147a238bb365fa78d8b45d53f8fb7af3b2b0084c0e7630512c6fb28f94",
"F374c59150d51b54d8096c9873095e4e425d615f35ed88bb03ff1f8c939dcf345",
"F319b480a6607d528b8a630d5a08904c01b5d1357e5c80a23bab5e03cb2d8d6fa",
"F8f4111b72204384257bfd6acd615dd50e28414348efc773a28073fed4165c71f",
"F20ee236c6cb25263eac9af9c9d873b741499778995e3f6ca97a688410ee948b2",
"F35e045b4f2b46bf783ff17ab67e2e834f36561cc7eb7ffeeba2a69e5538bcb01",
"F5379a63559ac7925560c02e98fc8a32c4a8210c989203cb8deaac01120c017b0",
"Ff99e411eb0e3dbaa2dab8e52dda4ef35ccc327c788140c10c2d2a286c55cdd46",
"F5437f25765ead737606c7fd9042163a0063532c125fe17d9e77d113b7d1ac7ef",
"F9ad83b2f07753c7fbebee715cbaddd0e444bf6c808e4b26fdb8fcd42a46bab92",
"Fe80638cfba7c94e1dbf7c6a8e094ee1b771f886f9b083b5d9ed6d89fbe24290d",
"F5a72b2268e14987eec04b9bd471740109354a683151443469ad44cada067fb12",
"F8a04baa09fb2466d8f5c45d3223a4fd75dd2cff80b09dd3db87d54bee98ee49c",
"F50bad5092469b33ef153acb28aa545a7f5cd0b6ff699ef5e6145667f4377508d",
"F0d6274231e5060bb8f1b4f72f3e0b132825629bbc9a193b3fe8781aa10c628e3",
"Ffb7d33d91fc0ab5c613dd200a51d595fca03d9252f5c10d0a55952c769aeae66",
"F210ff4a4b62852c199717f07532d74371db48ac8922fec0496b8f97813d8c1c7"
],
"game": "ddr",
"inactive": false,
"playtype": "SP",
"tableID": "ddr-SP-a3",
"title": "DDR A3"
},
{
"default": true,
"description": "Level Folders for GITADORA Konaste.",
@@ -1,13 +1,12 @@
const { MutateCollection, CreateFolderID } = require("../util");
// Change these for whatever table you are adding.
const GAME = "pms";
const PLAYTYPES = ["Controller", "Keyboard"];
const PREFIX = "PLv";
const TITLE = "Normal PMS Database";
const SHORTTITLE = "normalpmsdb"; // this is used in the tableID
const DESCRIPTION =
"The Normal PMS Database. This contains almost every PMS file ever created, lv1-45.";
const GAME = "ddr";
const PLAYTYPES = ["SP", "DP"];
const PREFIX = "Level ";
const TITLE = "DDR A3";
const SHORTTITLE = "a3"; // this is used in the tableID
const DESCRIPTION = "All songs in DDR A3";
const LEVELS = [
"1",
"2",
@@ -28,34 +27,6 @@ const LEVELS = [
"17",
"18",
"19",
"20",
"21",
"22",
"23",
"24",
"25",
"26",
"27",
"28",
"29",
"30",
"31",
"32",
"33",
"34",
"35",
"36",
"37",
"38",
"39",
"40",
"41",
"42",
"43",
"44",
"45",
"45+",
"?",
];
const ptFolders = {};
@@ -0,0 +1,30 @@
# Import DDR Gamedata
1) Generate gamedata under the following XML format :
```xml
<?xml version="1.0" encoding="UTF-8"?>
<mdb>
<music>
<mcode __type="u32">999</mcode>
<basename>randomsong</basename>
<title>This song doesn't exist</title>
<title_yomi>thissongdoesntexist</title_yomi>
<artist>Some Artist</artist>
<bpmmax __type="u16">120</bpmmax>
<series __type="u8">2</series>
<bemaniflag __type="u32">1</bemaniflag>
<limited_cha __type="u8">1</limited_cha>
<diffLv __type="u8" __count="10">3 5 6 9 0 0 6 8 8 0</diffLv>
</music>
<music>
...
</music>
...
</mdb>
```
File must be stored in the same folder as the script with the name `musicdb.xml`
2) At the last line of the script, eventually change the version if necessary. Current supported versions are `a3` and `konaste` (which are very similar since konaste is based on a3)
3) Run `ts-node parse-gameData-xml.ts`
@@ -0,0 +1,37 @@
import fs from "fs";
import {Music, parseGameData} from "./parse-gameData";
interface JsonMusic {
mcode: number,
basename: string,
title: string,
title_sort: string,
artist: string,
bpmmax: number,
bpmmin: number,
level_str: string,
editable: boolean,
seriesid: number,
ac_seriesid: number,
limited_cha: number,
bemaniflag: number,
}
function jsonMusicToMusic(jsonMusic: JsonMusic): Music {
return {
mcode: jsonMusic.mcode,
basename: jsonMusic.basename,
title: jsonMusic.title,
title_yomi: jsonMusic.title_sort,
artist: jsonMusic.artist,
bpmmax: jsonMusic.bpmmax,
series: jsonMusic.ac_seriesid,
bemaniflag: jsonMusic.bemaniflag,
limited_cha: jsonMusic.limited_cha,
diffLv: jsonMusic.level_str,
}
}
const gameData = JSON.parse(fs.readFileSync("music.json", "utf-8")).map(jsonMusicToMusic);
parseGameData("konaste", gameData);
@@ -0,0 +1,9 @@
import {XMLParser} from "fast-xml-parser";
import fs from "fs";
import {Music, parseGameData} from "./parse-gameData";
const parser = new XMLParser();
const gameData: Music[] = parser.parse(fs.readFileSync("musicdb.xml")).mdb.music;
parseGameData("a3", gameData);
@@ -0,0 +1,147 @@
import { CreateChartID, MutateCollection, ReadCollection, WriteCollection } from "../../util";
import {
ChartDocument,
Difficulties,
type GameConfig,
GetGameConfig,
Playtypes,
SongDocument,
Versions,
} from "tachi-common";
import { DDR_FLARE_CATEGORIES } from "tachi-common/config/game-support/ddr";
const gameConfig = GetGameConfig("ddr") as GameConfig<"ddr">;
export interface Music {
mcode: number;
basename: string;
title: string;
title_yomi: string;
artist: string;
bpmmax: number;
series: number;
bemaniflag: number;
limited_cha: number;
diffLv: string;
}
const DIFFICULTIES: Array<Difficulties["ddr:SP" | "ddr:DP"]> = [
"BEGINNER",
"BASIC",
"DIFFICULT",
"EXPERT",
"CHALLENGE",
];
function seriesToFlareCategory(series: number) {
if (!series) {
return DDR_FLARE_CATEGORIES.enum.NONE;
}
// Before X3 vs 2nd MIX
if (series <= 13) {
return DDR_FLARE_CATEGORIES.enum.CLASSIC;
}
// Between 2013 and A
if (series > 13 && series <= 17) {
return DDR_FLARE_CATEGORIES.enum.WHITE;
}
// Between A20 and WORLD
if (series > 17 && series <= 20) {
return DDR_FLARE_CATEGORIES.enum.GOLD;
}
return DDR_FLARE_CATEGORIES.enum.NONE;
}
function buildSong(music: Music): SongDocument<"ddr"> {
return {
artist: `${music.artist}`,
title: `${music.title}`,
id: music.mcode,
data: {
inGameID: music.mcode,
flareCategory: seriesToFlareCategory(music.series),
},
altTitles: [],
searchTerms: [],
};
}
function buildChart(
music: Music,
playtype: Playtypes["ddr"],
difficulty: Difficulties["ddr:SP" | "ddr:DP"],
version: Versions["ddr:SP" | "ddr:DP"]
): ChartDocument<"ddr:SP" | "ddr:DP"> {
const splitDiff = music.diffLv.split(" ");
const diffIndex = (playtype === "SP" ? 0 : 5) + DIFFICULTIES.indexOf(difficulty);
return {
chartID: CreateChartID(),
songID: music.mcode,
difficulty: difficulty,
isPrimary: true,
level: splitDiff[diffIndex]!,
levelNum: parseInt(splitDiff[diffIndex]!, 10),
playtype: playtype,
versions: [version],
data: {
inGameID: music.mcode,
},
};
}
export function parseGameData(version: Versions["ddr:SP" | "ddr:DP"], gameData: Music[]) {
const songs = ReadCollection("songs-ddr.json");
const existingChartDocs = ReadCollection("charts-ddr.json");
const existingCharts = new Map<string, ChartDocument<"ddr:SP" | "ddr:DP">>();
for (const chart of existingChartDocs) {
existingCharts.set(`${chart.data.inGameID}-${chart.difficulty}-${chart.playtype}`, chart);
}
const newSongs: SongDocument<"ddr">[] = [];
const newCharts: ChartDocument<"ddr:SP" | "ddr:DP">[] = [];
for (const music of gameData) {
const song = songs.find((s: SongDocument<"ddr">) => s.data.inGameID === music.mcode);
if (!song) {
const newSong = buildSong(music);
console.log(
`New song detected: ${newSong.artist} - ${newSong.title} (inGameId ${newSong.id})`
);
newSongs.push(buildSong(music));
}
const splitDiff = music.diffLv.split(" ");
for (let i = 0; i < 10; i++) {
if (parseInt(splitDiff[i]!, 10) > 0) {
const playtype = gameConfig.playtypes.at(Math.floor(i / 5))!;
const difficulty = DIFFICULTIES.at(i % 5)!;
const exists = existingCharts.get(`${music.mcode}-${difficulty}-${playtype}`);
if (exists) {
if (!exists.versions.includes(version)) {
console.log(
`Adding version ${version} to chart ${exists.playtype} ${exists.difficulty} of song ${song.artist} - ${song.title} (inGameId ${song.id})`
);
exists.versions.push(version);
}
exists.level = splitDiff[i]!;
exists.levelNum = parseInt(splitDiff[i]!, 10);
} else {
const newChart = buildChart(music, playtype, difficulty, version);
console.log(
`New chart detected: ${newChart.playtype} ${newChart.difficulty} for song ${music.artist} - ${music.title} (inGameId ${music.mcode})`
);
newCharts.push(newChart);
}
}
}
}
MutateCollection("songs-ddr.json", (songs: Array<SongDocument<"ddr">>) => [
...songs,
...newSongs,
]);
// overwrite this collection instead of mutating it
// we already know the existing chart docs and might have mutated them to
// declare the new versions, or update chart constants.
WriteCollection("charts-ddr.json", [...existingChartDocs, ...newCharts]);
}
+86
View File
@@ -0,0 +1,86 @@
# DDR (DP) Support
This game has the internal GPTString of `ddr:DP`.
!!! note
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
For more information on what metrics are and how they work, see [TODO]!
### Provided Metrics
| Metric Name | Type | Description |
| :: | :: | :: |
| `score` | Integer | The score value. This is between 0 and 1 million. |
| `lamp` | "FAILED", "ASSIST", "CLEAR", "FULL COMBO", "GREAT FULL COMBO", "PERFECT FULL COMBO", "MARVELOUS FULL COMBO", "LIFE4" | The type of clear this user got. |
### Derived Metrics
| Metric Name | Type | Description |
| :: | :: | :: |
| `grade` | "E", "D", "D+", "C-", "C", "C+", "B-", "B", "B+", "A-", "A", "A+", "AA-", "AA", "AA+", "AAA" | The grade this score was. Note that grades are capped at F if this was a fail. |
| `percent` | Decimal | The % value this score was worth. This is a number between 0 and 100. |
### Optional Metrics
| Metric Name | Type | Description |
| :: | :: | :: |
## Judgements
The folowing judgements are defined:
- `MARVELOUS`
- `PERFECT`
- `GREAT`
- `GOOD`
- `MISS`
- `OK`
## Rating Algorithms
### Score Rating Algorithms
| Name | Description |
| :: | :: |
| `flareSkill` | Flare Skill as it's implemented in DDR World. |
### Session Rating Algorithms
| Name | Description |
| :: | :: |
| `flareSkill` | Average of your 10 best Flare Points this session |
### Profile Rating Algorithms
| Name | Description |
| :: | :: |
| `flareSkill` | Flare Skill as it's implemented in DDR World, taking 30 best flare points from 3 different categories: CLASSIC (DDR 1st~X3 vs 2ndMIX), WHITE (DDR(2013)~DDR A), GOLD (DDR A20~WORLD). |
## Difficulties
- `BEGINNER`
- `BASIC`
- `DIFFICULT`
- `EXPERT`
- `CHALLENGE`
## Classes
| Name | Type | Values |
| :: | :: | :: |
## Versions
| ID | Pretty Name |
| :: | :: |
| `a3` | A3 |
| `konaste` | Konaste |
## Supported Match Types
- `inGameID`
- `songTitle`
- `tachiSongID`
+86
View File
@@ -0,0 +1,86 @@
# DDR (SP) Support
This game has the internal GPTString of `ddr:SP`.
!!! note
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
For more information on what metrics are and how they work, see [TODO]!
### Provided Metrics
| Metric Name | Type | Description |
| :: | :: | :: |
| `score` | Integer | The score value. This is between 0 and 1 million. |
| `lamp` | "FAILED", "ASSIST", "CLEAR", "FULL COMBO", "GREAT FULL COMBO", "PERFECT FULL COMBO", "MARVELOUS FULL COMBO", "LIFE4" | The type of clear this user got. |
### Derived Metrics
| Metric Name | Type | Description |
| :: | :: | :: |
| `grade` | "E", "D", "D+", "C-", "C", "C+", "B-", "B", "B+", "A-", "A", "A+", "AA-", "AA", "AA+", "AAA" | The grade this score was. Note that grades are capped at F if this was a fail. |
| `percent` | Decimal | The % value this score was worth. This is a number between 0 and 100. |
### Optional Metrics
| Metric Name | Type | Description |
| :: | :: | :: |
## Judgements
The folowing judgements are defined:
- `MARVELOUS`
- `PERFECT`
- `GREAT`
- `GOOD`
- `MISS`
- `OK`
## Rating Algorithms
### Score Rating Algorithms
| Name | Description |
| :: | :: |
| `flareSkill` | Flare Skill as it's implemented in DDR World. |
### Session Rating Algorithms
| Name | Description |
| :: | :: |
| `flareSkill` | Average of your 10 best Flare Points this session |
### Profile Rating Algorithms
| Name | Description |
| :: | :: |
| `flareSkill` | Flare Skill as it's implemented in DDR World, taking 30 best flare points from 3 different categories: CLASSIC (DDR 1st~X3 vs 2ndMIX), WHITE (DDR(2013)~DDR A), GOLD (DDR A20~WORLD). |
## Difficulties
- `BEGINNER`
- `BASIC`
- `DIFFICULT`
- `EXPERT`
- `CHALLENGE`
## Classes
| Name | Type | Values |
| :: | :: | :: |
## Versions
| ID | Pretty Name |
| :: | :: |
| `a3` | A3 |
| `konaste` | Konaste |
## Supported Match Types
- `inGameID`
- `songTitle`
- `tachiSongID`
+6 -6
View File
@@ -295,8 +295,8 @@ importers:
specifier: ^5.6.1
version: 5.6.1(@babel/core@7.1.0)(@types/react@17.0.13)(react-dom@17.0.2)(react@17.0.2)
rg-stats:
specifier: 0.5.6
version: 0.5.6
specifier: 0.5.8
version: 0.5.8
sync-fetch:
specifier: ^0.3.1
version: 0.3.1
@@ -662,8 +662,8 @@ importers:
specifier: 3.1.2
version: 3.1.2
rg-stats:
specifier: 0.5.6
version: 0.5.6
specifier: 0.5.8
version: 0.5.8
rimraf:
specifier: 3.0.2
version: 3.0.2
@@ -10162,8 +10162,8 @@ packages:
resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
dev: false
/rg-stats@0.5.6:
resolution: {integrity: sha512-4YG942ztdaXd27KUABxiQAA3aJuLvLcvjdLfoxKqn89P7967bfyG5Z5OWaAg9Sr8pvhsABA4bHZuBAKtSW90xA==}
/rg-stats@0.5.8:
resolution: {integrity: sha512-AqhksXLcwACA3Af1XQkJhVSeQtFlu6NggmTqxXvw7BLDiaOsTJDZn4+gnk4XlJtf+7U0nlrMKUFM8ZlHCNFjXA==}
dev: false
/rimraf@2.4.5:
+1
View File
@@ -50,6 +50,7 @@
"maimai",
"maimaidx",
"itg",
"ddr"
],
IMPORT_TYPES: [
"file/eamusement-iidx-csv",
+2 -2
View File
@@ -100,7 +100,7 @@
"prudence": "0.10.0",
"rate-limit-redis": "2.1.0",
"redis": "3.1.2",
"rg-stats": "0.5.6",
"rg-stats": "0.5.8",
"rimraf": "3.0.2",
"safe-json-stringify": "1.2.0",
"semver": "^7.3.7",
@@ -129,4 +129,4 @@
"node": "20",
"pnpm": "8"
}
}
}
@@ -1,6 +1,7 @@
import { ARCAEA_IMPL } from "./games/arcaea";
import { BMS_14K_IMPL, BMS_7K_IMPL, PMS_CONTROLLER_IMPL, PMS_KEYBOARD_IMPL } from "./games/bms-pms";
import { CHUNITHM_IMPL } from "./games/chunithm";
import { DDR_IMPL } from "./games/ddr";
import { GITADORA_DORA_IMPL, GITADORA_GITA_IMPL } from "./games/gitadora";
import { IIDX_DP_IMPL, IIDX_SP_IMPL } from "./games/iidx";
import { ITG_STAMINA_IMPL } from "./games/itg";
@@ -45,4 +46,6 @@ export const GPT_SERVER_IMPLEMENTATIONS: GPTImplementations = {
"sdvx:Single": SDVX_IMPL,
"arcaea:Touch": ARCAEA_IMPL,
"ongeki:Single": ONGEKI_IMPL,
"ddr:SP": DDR_IMPL,
"ddr:DP": DDR_IMPL,
};
@@ -0,0 +1,197 @@
import { DDR_IMPL } from "./ddr";
import { IIDX_DP_IMPL, IIDX_SP_IMPL } from "./iidx";
import { TestSnapshot } from "../../test-utils/single-process-snapshot";
import { DDR_GRADES, DDR_LAMPS } from "tachi-common";
import t from "tap";
import { dmf, mkFakePBDDRSP } from "test-utils/misc";
import { Testing511SPA, TestingDDRSP, TestingDDRSPScore } from "test-utils/test-data";
import type { ChartDocumentData, ProvidedMetrics, ScoreData } from "tachi-common";
const baseMetrics: ProvidedMetrics["ddr:DP" | "ddr:SP"] = {
lamp: "CLEAR",
score: 750_000,
};
t.test("DDR Implementation", (t) => {
t.test("Derivers", (t) => {
const impl = DDR_IMPL;
t.test("Grade", (t) => {
const f = (score: number, expected: any) =>
t.equal(
impl.derivers.grade(dmf(baseMetrics, { score }), TestingDDRSP as any),
expected,
`A score of ${score} should result in grade=${expected}.`
);
f(990500, "AAA");
f(950500, "AA+");
f(900500, "AA");
f(890500, "AA-");
f(850500, "A+");
f(800500, "A");
f(790500, "A-");
f(750500, "B+");
f(700500, "B");
f(690500, "B-");
f(650500, "C+");
f(600500, "C");
f(590500, "C-");
f(550500, "D+");
f(500, "D");
t.end();
});
t.end();
});
t.test("Class Derivers", (t) => {
const impl = DDR_IMPL;
t.test("Flare", (t) => {
const f = (ratings: number, expected: any) =>
t.equal(
impl.classDerivers.flare({ flareSkill: ratings }),
expected,
`A flare skill of ${ratings} should result in grade=${expected}.`
);
f(1, "NONE");
f(501, "NONE+");
f(1001, "NONE++");
f(1501, "NONE+++");
f(2001, "MERCURY");
f(3001, "MERCURY+");
f(4001, "MERCURY++");
f(5001, "MERCURY+++");
f(6001, "VENUS");
f(7001, "VENUS+");
f(8001, "VENUS++");
f(9001, "VENUS+++");
f(10001, "EARTH");
f(11501, "EARTH+");
f(13001, "EARTH++");
f(14501, "EARTH+++");
f(16001, "MARS");
f(18001, "MARS+");
f(20001, "MARS++");
f(22001, "MARS+++");
f(24001, "JUPITER");
f(26501, "JUPITER+");
f(29001, "JUPITER++");
f(31501, "JUPITER+++");
f(34001, "SATURN");
f(36751, "SATURN+");
f(39501, "SATURN++");
f(42251, "SATURN+++");
f(45001, "URANUS");
f(48751, "URANUS+");
f(52501, "URANUS++");
f(56251, "URANUS+++");
f(60001, "NEPTUNE");
f(63751, "NEPTUNE+");
f(67501, "NEPTUNE++");
f(71251, "NEPTUNE+++");
f(75001, "SUN");
f(78751, "SUN+");
f(82501, "SUN++");
f(86251, "SUN+++");
f(90001, "WORLD");
t.end();
});
t.end();
});
t.test("CalculatedData", (t) => {
const impl = DDR_IMPL;
t.test("Flare Skill", (t) => {
const f = (
scoreData: Partial<ScoreData<"ddr:DP" | "ddr:SP">>,
chartData: Partial<ChartDocumentData["ddr:DP" | "ddr:SP"]>,
expected: any,
msg: string
) =>
t.equal(
impl.scoreCalcs.flareSkill(
dmf(TestingDDRSPScore.scoreData, scoreData),
dmf(TestingDDRSP, { data: chartData as any }) as any
),
expected,
msg
);
f(
{ grade: "AA", lamp: "CLEAR", optional: { flare: "II", enumIndexes: {} } },
{},
257,
"Level 6 song with Flare II should give 257 flare skill"
);
f({ grade: "E", lamp: "FAILED" }, {}, 0, "Song failed: flare skill should be 0");
t.end();
});
t.end();
});
t.test("Goal Formatters", (t) => {
const impl = DDR_IMPL;
t.test("Criteria", (t) => {
t.equal(impl.goalCriteriaFormatters.score(123456), "Get a score of 123,456 on");
t.equal(impl.goalCriteriaFormatters.score(0), "Get a score of 0 on");
t.end();
});
t.test("Progress", (t) => {
const f = (
k: keyof typeof impl.goalProgressFormatters,
modifant: Partial<ScoreData>,
goalValue: any,
expected: any
) =>
t.equal(
impl.goalProgressFormatters[k](
mkFakePBDDRSP({
// @ts-expect-error lol deepmerge types
scoreData: modifant,
}),
goalValue
),
expected
);
f("score", { score: 123_456 }, 1_000_000, "123,456");
f("score", { score: 0 }, 1_000_000, "0");
f("grade", { score: 955000, grade: "AA+" }, DDR_GRADES.AAA, "AAA-35,000");
f("grade", { score: 995000, grade: "AAA" }, DDR_GRADES.AAA, "AAA+5,000");
f("lamp", { lamp: "CLEAR", optional: {} as any }, DDR_LAMPS.FULL_COMBO, "CLEAR");
f(
"lamp",
{ lamp: "FULL COMBO", optional: {} as any },
DDR_LAMPS.FULL_COMBO,
"FULL COMBO"
);
t.end();
});
t.test("OutOf", (t) => {
t.equal(impl.goalOutOfFormatters.score(123456), "123,456");
t.end();
});
t.end();
});
t.end();
});
@@ -0,0 +1,342 @@
import {
GoalFmtPercent,
GoalFmtScore,
GoalOutOfFmtPercent,
GoalOutOfFmtScore,
GradeGoalFormatter,
} from "./_common";
import db from "../../external/mongo/db";
import { IsNullish } from "../../utils/misc";
import { CreatePBMergeFor } from "../utils/pb-merge";
import { SessionAvgBest10For } from "../utils/session-calc";
import { DDRFlare } from "rg-stats";
import {
DDR_GBOUNDARIES,
FmtNum,
type Game,
GetGrade,
GetSpecificGPTConfig,
type integer,
type Playtype,
} from "tachi-common";
import type {
GPTGoalFormatters,
GPTGoalProgressFormatters,
GPTServerImplementation,
ScoreValidator,
} from "game-implementations/types";
import type { PBScoreDocument, SongDocument } from "tachi-common";
interface PBScoreDocumentWithSong extends PBScoreDocument<"ddr:DP" | "ddr:SP"> {
song: SongDocument<"ddr">;
top?: number;
}
const FLARE_0_POINTS = [
145, 155, 170, 185, 205, 230, 255, 290, 335, 400, 465, 510, 545, 575, 600, 620, 635, 650, 665,
];
const DDR_GOAL_FMT: GPTGoalFormatters<"ddr:DP" | "ddr:SP"> = {
score: GoalFmtScore,
};
const DDR_GOAL_OO_FMT: GPTGoalFormatters<"ddr:DP" | "ddr:SP"> = {
score: GoalOutOfFmtScore,
};
const DDR_GOAL_PG_FMT: GPTGoalProgressFormatters<"ddr:DP" | "ddr:SP"> = {
score: (pb) => FmtNum(pb.scoreData.score),
lamp: (pb) => {
return pb.scoreData.lamp;
},
grade: (pb, gradeIndex) =>
GradeGoalFormatter(
DDR_GBOUNDARIES,
pb.scoreData.grade,
pb.scoreData.score,
DDR_GBOUNDARIES[gradeIndex]!.name,
(delta) => {
return FmtNum(delta);
}
),
};
export const DDR_SCORE_VALIDATORS: Array<ScoreValidator<"ddr:DP" | "ddr:SP">> = [
(s) => {
const { MARVELOUS, PERFECT, GREAT, GOOD, OK, MISS } = s.scoreData.judgements;
if (
IsNullish(MARVELOUS) ||
IsNullish(PERFECT) ||
IsNullish(GREAT) ||
IsNullish(GOOD) ||
IsNullish(OK) ||
IsNullish(MISS)
) {
return;
}
const stepScore = 1_000_000 / (MARVELOUS + PERFECT + GREAT + GOOD + OK + MISS);
const calculatedScore =
Math.floor(
(stepScore * (MARVELOUS + OK) +
(stepScore - 10) * PERFECT +
((stepScore * 3) / 5 - 10) * GREAT +
(stepScore / 5 - 10) * GOOD) /
10
) * 10;
if (calculatedScore !== s.scoreData.score) {
return `Expected calculated score from judgements of ${calculatedScore} to equal score of ${s.scoreData.score}.`;
}
},
(s) => {
const { MARVELOUS, PERFECT, GREAT, GOOD, MISS } = s.scoreData.judgements;
if (
IsNullish(MARVELOUS) ||
IsNullish(PERFECT) ||
IsNullish(GREAT) ||
IsNullish(GOOD) ||
IsNullish(MISS)
) {
return;
}
switch (s.scoreData.lamp) {
case "FULL COMBO": {
if (MISS > 0) {
return `Cannot have a FULL COMBO with more than 0 MISS`;
}
break;
}
case "GREAT FULL COMBO": {
if (MISS > 0 || GOOD > 0) {
return `Cannot have a GREAT FULL COMBO with more than 0 MISS and GOOD`;
}
break;
}
case "PERFECT FULL COMBO": {
if (MISS > 0 || GOOD > 0 || GREAT > 0) {
return `Cannot have a PERFECT FULL COMBO with more than 0 MISS, GOOD and GREAT`;
}
break;
}
case "MARVELOUS FULL COMBO": {
if (MISS > 0 || GOOD > 0 || GREAT > 0 || PERFECT > 0) {
return `Cannot have a MARVELOUS FULL COMBO with anyhing else than MARVELOUS judgements`;
}
break;
}
default:
}
},
];
export const DDR_IMPL: GPTServerImplementation<"ddr:DP" | "ddr:SP"> = {
chartSpecificValidators: {},
classDerivers: {
flare: (ratings) => {
const flarePoints = ratings.flareSkill;
if (IsNullish(flarePoints)) {
return null;
}
switch (true) {
case flarePoints < 500:
return "NONE";
case flarePoints < 1000:
return "NONE+";
case flarePoints < 1500:
return "NONE++";
case flarePoints < 2000:
return "NONE+++";
case flarePoints < 3000:
return "MERCURY";
case flarePoints < 4000:
return "MERCURY+";
case flarePoints < 5000:
return "MERCURY++";
case flarePoints < 6000:
return "MERCURY+++";
case flarePoints < 7000:
return "VENUS";
case flarePoints < 8000:
return "VENUS+";
case flarePoints < 9000:
return "VENUS++";
case flarePoints < 10000:
return "VENUS+++";
case flarePoints < 11500:
return "EARTH";
case flarePoints < 13000:
return "EARTH+";
case flarePoints < 14500:
return "EARTH++";
case flarePoints < 16000:
return "EARTH+++";
case flarePoints < 18000:
return "MARS";
case flarePoints < 20000:
return "MARS+";
case flarePoints < 22000:
return "MARS++";
case flarePoints < 24000:
return "MARS+++";
case flarePoints < 26500:
return "JUPITER";
case flarePoints < 29000:
return "JUPITER+";
case flarePoints < 31500:
return "JUPITER++";
case flarePoints < 34000:
return "JUPITER+++";
case flarePoints < 36750:
return "SATURN";
case flarePoints < 39500:
return "SATURN+";
case flarePoints < 42250:
return "SATURN++";
case flarePoints < 45000:
return "SATURN+++";
case flarePoints < 48750:
return "URANUS";
case flarePoints < 52500:
return "URANUS+";
case flarePoints < 56250:
return "URANUS++";
case flarePoints < 60000:
return "URANUS+++";
case flarePoints < 63750:
return "NEPTUNE";
case flarePoints < 67500:
return "NEPTUNE+";
case flarePoints < 71250:
return "NEPTUNE++";
case flarePoints < 75000:
return "NEPTUNE+++";
case flarePoints < 78750:
return "SUN";
case flarePoints < 82500:
return "SUN+";
case flarePoints < 86250:
return "SUN++";
case flarePoints < 90000:
return "SUN+++";
}
return "WORLD";
},
},
defaultMergeRefName: "Best Score",
derivers: {
grade: ({ score, lamp }) => {
if (lamp === "FAILED") {
return "E";
}
return GetGrade(DDR_GBOUNDARIES, score);
},
},
goalCriteriaFormatters: DDR_GOAL_FMT,
goalProgressFormatters: DDR_GOAL_PG_FMT,
goalOutOfFormatters: DDR_GOAL_OO_FMT,
pbMergeFunctions: [
CreatePBMergeFor("largest", "enumIndexes.lamp", "Best Lamp", (base, score) => {
base.scoreData.lamp = score.scoreData.lamp;
}),
CreatePBMergeFor("largest", "score", "Best Score", (base, score) => {
base.scoreData.score = score.scoreData.score;
base.scoreData.grade = score.scoreData.grade;
}),
],
profileCalcs: {
flareSkill: async (game: Game, playtype: Playtype, userID: integer) => {
const sc: Array<PBScoreDocumentWithSong> = await db["personal-bests"].aggregate([
{
$match: {
userID,
game,
playtype,
isPrimary: true,
[`calculatedData.flareSkill`]: { $type: "number" },
},
},
{
$lookup: {
from: "songs-ddr",
localField: "songID",
foreignField: "id",
as: "song",
},
},
{
$unwind: {
path: "$song",
},
},
{
$sort: {
[`calculatedData.flareSkill`]: -1,
},
},
]);
if (sc.length === 0) {
return null;
}
let classicIndex = 0;
let goldIndex = 0;
let whiteIndex = 0;
for (const score of sc) {
if (score.song.data.flareCategory === "CLASSIC") {
score.top = classicIndex++;
} else if (score.song.data.flareCategory === "WHITE") {
score.top = whiteIndex++;
} else if (score.song.data.flareCategory === "GOLD") {
score.top = goldIndex++;
} else {
score.top = 99; // Score will be filtered out
}
}
return sc
.filter((score: PBScoreDocumentWithSong) => score.top! < 30)
.reduce(
(a: number, e: PBScoreDocumentWithSong) => a + e.calculatedData.flareSkill!,
0
);
},
},
scoreCalcs: {
flareSkill: (scoreData, chart) => {
// No flare if the song is failed
if (scoreData.lamp === "FAILED") {
return 0;
}
const flareLevel = scoreData.optional.flare
? GetSpecificGPTConfig("ddr:SP").optionalMetrics.flare.values.indexOf(
scoreData.optional.flare
)
: 0;
return DDRFlare.calculate(chart.levelNum, flareLevel);
},
},
scoreValidators: DDR_SCORE_VALIDATORS,
sessionCalcs: {
flareSkill: SessionAvgBest10For("flareSkill"),
},
};
@@ -36,6 +36,7 @@ function perToScore(goal: GoalDocument) {
case "wacca":
case "chunithm":
case "ongeki":
case "ddr":
case "museca":
return v * 1_000_000;
case "popn":
@@ -88,6 +88,8 @@ const scoreMovers: ScoreMovers = {
"sdvx:Single": NEUTRAL_MV,
"wacca:Single": NEUTRAL_MV,
"museca:Single": NEUTRAL_MV,
"ddr:SP": NEUTRAL_MV,
"ddr:DP": NEUTRAL_MV,
"maimai:Single": (old) => ({
percent: old.percent,
judgements: old.judgements,
+5
View File
@@ -5,6 +5,7 @@ import {
FakeOtherUser,
HC511Goal,
HC511UserGoal,
TestingDDRSPScorePB,
TestingIIDXSPScore,
TestingIIDXSPScorePB,
TestingJubeatPB,
@@ -101,6 +102,10 @@ export function mkFakePBIIDXSP(modifant: DeepPartial<PBScoreDocument<"iidx:SP">>
return dmf(TestingIIDXSPScorePB, modifant);
}
export function mkFakePBDDRSP(modifant: DeepPartial<PBScoreDocument<"ddr:SP">> = {}) {
return dmf(TestingDDRSPScorePB, modifant);
}
export function mkFakePBJubeat(modifant: DeepPartial<PBScoreDocument<"jubeat:Single">> = {}) {
return dmf(TestingJubeatPB, modifant);
}
+97
View File
@@ -101,6 +101,44 @@ export const TestingIIDXSPScorePB: PBScoreDocument<"iidx:SP"> = {
timeAchieved: 10000,
};
export const TestingDDRSPScorePB: PBScoreDocument<"ddr:SP"> = {
calculatedData: {
flareSkill: 545,
},
chartID: "f14bc72ad8336625ce64fc42571192e1ec168113",
composedFrom: [
{
name: "Best Score",
scoreID: "Te065000f08b49458f8b0aa3eabf0b857b79c562d7cf9eb34f6dabd7a1c3c3fa6",
},
],
game: "ddr",
highlight: false,
isPrimary: true,
playtype: "SP",
rankingData: {
rank: 1,
outOf: 1,
rivalRank: null,
},
scoreData: {
score: 914400,
lamp: "CLEAR",
judgements: {},
optional: {
enumIndexes: {},
},
grade: "AA",
enumIndexes: {
lamp: 2,
grade: 13,
},
},
songID: 37691,
timeAchieved: null,
userID: 1,
};
export const TestingJubeatPB: PBScoreDocument<"jubeat:Single"> = {
chartID: "b90a319f18d1a746b330b8f4cd6f74874f664421",
userID: 1,
@@ -172,6 +210,39 @@ export const TestingIIDXSPScore: ScoreDocument<"iidx:SP"> = {
importType: "ir/direct-manual",
};
export const TestingDDRSPScore: ScoreDocument<"ddr:SP"> = {
calculatedData: {
flareSkill: 545,
},
chartID: "2fc2e0cfdda42addb7840b58be1df1f545310d66",
comment: null,
game: "ddr",
highlight: false,
importType: "file/batch-manual",
isPrimary: true,
playtype: "SP",
scoreData: {
score: 914400,
lamp: "CLEAR",
judgements: {},
optional: {
enumIndexes: {},
},
grade: "AA",
enumIndexes: {
lamp: 2,
grade: 13,
},
},
scoreID: "Te065000f08b49458f8b0aa3eabf0b857b79c562d7cf9eb34f6dabd7a1c3c3fa6",
scoreMeta: {},
service: "History (BATCH-MANUAL)",
songID: 10,
timeAchieved: null,
timeAdded: 1722084133807,
userID: 1,
};
export const TestingSDVXScore: ScoreDocument<"sdvx:Single"> = {
service: "foo (DIRECT-MANUAL)",
game: "sdvx",
@@ -475,6 +546,32 @@ export const Testing511Song: SongDocument<"iidx"> = {
},
};
export const TestingDDRSP: ChartDocument<"ddr:SP"> = {
chartID: "2fc2e0cfdda42addb7840b58be1df1f545310d66",
data: {
inGameID: 10,
},
difficulty: "BASIC",
isPrimary: true,
level: "6",
levelNum: 6,
playtype: "SP",
songID: 10,
versions: ["a3"],
};
export const TestingDDRSong: SongDocument<"ddr"> = {
altTitles: [],
artist: "UZI-LAY",
data: {
flareCategory: "CLASSIC",
inGameID: 10,
},
id: 10,
searchTerms: [],
title: "PUT YOUR FAITH IN ME",
};
export const TestingAlbidaADV: ChartDocument<"sdvx:Single"> = {
chartID: "5088a4d0e1ee9d0cc2f625934306e45b1a60699b",
difficulty: "ADV",