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 497ef1595..99b28b1f1 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";
@@ -123,6 +123,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 5a14c1cad..174e9c90b 100644
--- a/server/src/game-implementations/games/ddr.ts
+++ b/server/src/game-implementations/games/ddr.ts
@@ -136,6 +136,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"> = {
@@ -255,6 +274,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) => {