diff --git a/client/src/components/tables/cells/DDRScoreCell.tsx b/client/src/components/tables/cells/DDRScoreCell.tsx
index 39d4a931f..59954a748 100644
--- a/client/src/components/tables/cells/DDRScoreCell.tsx
+++ b/client/src/components/tables/cells/DDRScoreCell.tsx
@@ -6,12 +6,14 @@ export default function DDRScoreCell({
score,
colour,
grade,
+ exScore,
scoreRenderFn,
}: {
score?: integer;
grade: string;
colour: string;
showScore?: boolean;
+ exScore?: integer;
scoreRenderFn?: (s: number) => string;
}) {
return (
@@ -23,6 +25,12 @@ export default function DDRScoreCell({
{grade}
{score !== undefined && <>{scoreRenderFn ? scoreRenderFn(score) : score}>}
+ {typeof exScore === "number" && (
+ <>
+
+ [EX: {exScore}]
+ >
+ )}
);
}
diff --git a/common/src/config/game-support/ddr.ts b/common/src/config/game-support/ddr.ts
index f46436d4d..c773c0d8b 100644
--- a/common/src/config/game-support/ddr.ts
+++ b/common/src/config/game-support/ddr.ts
@@ -1,5 +1,5 @@
-import { IIDXDans } from "./iidx";
-import { FmtNum, FmtPercent, FmtScoreNoCommas } from "../../utils/util";
+import { FAST_SLOW_MAXCOMBO } from "./_common";
+import { FmtNum, FmtScoreNoCommas } from "../../utils/util";
import { ClassValue, zodNonNegativeInt } from "../config-utils";
import { p } from "prudence";
import { z } from "zod";
@@ -122,6 +122,19 @@ export const DDR_SP_CONF = {
minimumRelevantValue: "0",
description: "The Flare rank. If no Flare is provided, Flare 0 is chosen by default.",
},
+ exScore: {
+ type: "INTEGER",
+ formatter: FmtNum,
+ validate: p.isPositiveInteger,
+
+ // We want to track the best EXScore a user gets, but it is an optional
+ // metric.
+ partOfScoreID: true,
+
+ description:
+ "The EXScore value. Marvelous and O.K. judgements are worth 3 points, Perfect judgements are worth 2 points, Great judgements are worth 1 point, and Good and lower judgements are not worth any points.",
+ },
+ ...FAST_SLOW_MAXCOMBO,
},
defaultMetric: "score",
diff --git a/server/src/game-implementations/games/ddr.ts b/server/src/game-implementations/games/ddr.ts
index 96b0e3634..5a89c557e 100644
--- a/server/src/game-implementations/games/ddr.ts
+++ b/server/src/game-implementations/games/ddr.ts
@@ -141,6 +141,25 @@ export const DDR_SCORE_VALIDATORS: Array> =
default:
}
},
+ (s) => {
+ const { MARVELOUS, PERFECT, GREAT, OK } = s.scoreData.judgements;
+
+ if (
+ IsNullish(MARVELOUS) ||
+ IsNullish(PERFECT) ||
+ IsNullish(GREAT) ||
+ IsNullish(OK) ||
+ IsNullish(s.scoreData.optional.exScore)
+ ) {
+ return;
+ }
+
+ const calculatedExScore = MARVELOUS * 3 + OK * 3 + PERFECT * 2 + GREAT;
+
+ if (calculatedExScore !== s.scoreData.optional.exScore) {
+ return `EXScore expected to be ${calculatedExScore} instead of ${s.scoreData.optional.exScore}`;
+ }
+ },
];
export const DDR_IMPL: GPTServerImplementation<"ddr:DP" | "ddr:SP"> = {
@@ -260,6 +279,9 @@ export const DDR_IMPL: GPTServerImplementation<"ddr:DP" | "ddr:SP"> = {
base.scoreData.score = score.scoreData.score;
base.scoreData.grade = score.scoreData.grade;
}),
+ CreatePBMergeFor("largest", "optional.exScore", "Best EX Score", (base, score) => {
+ base.scoreData.optional.exScore = score.scoreData.optional.exScore;
+ }),
],
profileCalcs: {
flareSkill: async (game: Game, playtype: Playtype, userID: integer) => {