Skip to content

Commit 300a1e7

Browse files
authored
[chunithm] split lamp into combo lamp and clear lamp (#1269)
* [chunithm] split lamp into combo lamp and clear lamp * ignore nonsense lint * fix myt converter tests * fix chunitachi batch manual * mfw edited wrong file * fix(migration): only migrate lamp-related showcases * remove chart lamp showcases * remove unused imports * fix: limit re-pb to chunithm scores, migrate chart lamp showcases to comboLamp * fix migration of comboLamp progress * fix: updateMany with multi: true * fix: when updating references, ensure only known IDs are in * fix(client): make red stand out more * fix(client): lamp sorting * feat(server): validate clear and combo lamps based on judgements * fix(server): validate combo lamps based on score boundary * fix: reword validation errors to stay consistent * fix: correct explanation * fix: how was I this stupid? * fix: update blacklisted scores too * fix: bulk write all the updates * fix: bulkWrite harder * fix: i probably need sleep * all of game-settings to bulkWrite * fix folder lamp goal nonsense * maybe fix colors * fix: name PB merges * chore: properly test PB merge * mfw reversed * fix(SessionRaiseBreakdown): lamp name * rename to noteLamp * why is eslint even complaining here? * rename the enums * fix tests * tweak colors of lamps
1 parent e8dc93b commit 300a1e7

File tree

13 files changed

+857
-95
lines changed

13 files changed

+857
-95
lines changed

client/src/components/sessions/SessionRaiseBreakdown.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export default function SessionRaiseBreakdown({
4949
}) {
5050
const game = sessionData.session.game;
5151
const playtype = sessionData.session.playtype;
52-
const lampName = game === "ongeki" ? "noteLamp" : "lamp";
52+
const lampName = game === "ongeki" || game === "chunithm" ? "noteLamp" : "lamp";
5353

5454
const { user } = useContext(UserContext);
5555

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ChangeOpacity } from "util/color-opacity";
2+
import React from "react";
3+
import { GetEnumValue } from "tachi-common/types/metrics";
4+
5+
export default function CHUNITHMLampCell({
6+
noteLamp,
7+
clearLamp,
8+
noteLampColour,
9+
clearLampColour,
10+
}: {
11+
clearLamp: GetEnumValue<"chunithm:Single", "clearLamp">;
12+
noteLamp: GetEnumValue<"chunithm:Single", "noteLamp">;
13+
noteLampColour: string;
14+
clearLampColour: string;
15+
}) {
16+
let content = <div>{clearLamp}</div>;
17+
let background = ChangeOpacity(clearLampColour, 0.2);
18+
19+
if (noteLamp !== "NONE") {
20+
background = ChangeOpacity(noteLampColour, 0.2);
21+
22+
if (clearLamp === "CLEAR") {
23+
content = <div>{noteLamp}</div>;
24+
} else {
25+
const clearLampLow = ChangeOpacity(clearLampColour, 0.2);
26+
const noteLampLow = ChangeOpacity(noteLampColour, 0.2);
27+
28+
background = `linear-gradient(-45deg, ${clearLampLow} 0%, ${clearLampLow} 12%, ${noteLampLow} 12%, ${noteLampLow} 100%)`;
29+
30+
content = (
31+
<span>
32+
<div>{noteLamp}</div>
33+
<div>{clearLamp}</div>
34+
</span>
35+
);
36+
}
37+
}
38+
39+
return (
40+
<td
41+
style={{
42+
background,
43+
whiteSpace: "nowrap",
44+
}}
45+
>
46+
<strong>{content}</strong>
47+
</td>
48+
);
49+
}

client/src/lib/game-implementations.tsx

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import OngekiJudgementCell from "components/tables/cells/OngekiJudgementCell";
2020
import React from "react";
2121
import OngekiLampCell from "components/tables/cells/OngekiLampCell";
2222
import OngekiPlatinumCell from "components/tables/cells/OngekiPlatinumCell";
23+
import CHUNITHMLampCell from "components/tables/cells/CHUNITHMLampCell";
2324
import { bgc, RAINBOW_EX_GRADIENT, RAINBOW_GRADIENT } from "./games/_util";
2425
import { BMS_14K_IMPL, BMS_7K_IMPL, PMS_IMPL } from "./games/bms-pms";
2526
import { IIDX_DP_IMPL, IIDX_SP_IMPL } from "./games/iidx";
@@ -45,7 +46,11 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
4546
"ddr:DP": DDR_DP_IMPL,
4647
"chunithm:Single": {
4748
sessionImportantScoreCount: 50,
48-
enumIcons: defaultEnumIcons,
49+
enumIcons: {
50+
grade: "sort-alpha-up",
51+
clearLamp: "lightbulb",
52+
noteLamp: "lightbulb",
53+
},
4954
classColours: {
5055
colour: {
5156
BLUE: bgc("var(--bs-info)", "var(--bs-light)"),
@@ -99,10 +104,17 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
99104
SSS: COLOUR_SET.teal,
100105
"SSS+": COLOUR_SET.white,
101106
},
102-
lamp: {
103-
FAILED: COLOUR_SET.red,
104-
CLEAR: COLOUR_SET.paleGreen,
105-
"FULL COMBO": COLOUR_SET.paleBlue,
107+
clearLamp: {
108+
FAILED: COLOUR_SET.vibrantRed,
109+
CLEAR: COLOUR_SET.green,
110+
HARD: "#E12AFB",
111+
BRAVE: "#A21CAF",
112+
ABSOLUTE: "#701A75",
113+
CATASTROPHY: "#4A044E",
114+
},
115+
noteLamp: {
116+
NONE: COLOUR_SET.gray,
117+
"FULL COMBO": COLOUR_SET.darkGreen,
106118
"ALL JUSTICE": COLOUR_SET.gold,
107119
"ALL JUSTICE CRITICAL": COLOUR_SET.white,
108120
},
@@ -118,7 +130,14 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
118130
scoreHeaders: [
119131
["Score", "Score", NumericSOV((x) => x.scoreData.score)],
120132
["Judgements", "Hits", NumericSOV((x) => x.scoreData.score)],
121-
["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)],
133+
[
134+
"Lamp",
135+
"Lamp",
136+
NumericSOV(
137+
(x) =>
138+
(x.scoreData.enumIndexes.noteLamp << 3) + x.scoreData.enumIndexes.clearLamp
139+
),
140+
],
122141
],
123142
scoreCoreCells: ({ sc }) => (
124143
<>
@@ -128,7 +147,12 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
128147
colour={GetEnumColour(sc, "grade")}
129148
/>
130149
<CHUNITHMJudgementCell score={sc} />
131-
<LampCell lamp={sc.scoreData.lamp} colour={GetEnumColour(sc, "lamp")} />
150+
<CHUNITHMLampCell
151+
clearLamp={sc.scoreData.clearLamp}
152+
noteLamp={sc.scoreData.noteLamp}
153+
clearLampColour={GetEnumColour(sc, "clearLamp")}
154+
noteLampColour={GetEnumColour(sc, "noteLamp")}
155+
/>
132156
</>
133157
),
134158
ratingCell: ({ sc, rating }) => <RatingCell score={sc} rating={rating} />,

common/src/config/game-support/chunithm.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,15 @@ export const CHUNITHM_SINGLE_CONF = {
5151
formatter: FmtNum,
5252
description: "The score value. This is between 0 and 1.01 million.",
5353
},
54-
lamp: {
54+
noteLamp: {
5555
type: "ENUM",
56-
values: ["FAILED", "CLEAR", "FULL COMBO", "ALL JUSTICE", "ALL JUSTICE CRITICAL"],
56+
values: ["NONE", "FULL COMBO", "ALL JUSTICE", "ALL JUSTICE CRITICAL"],
57+
minimumRelevantValue: "FULL COMBO",
58+
description: "The type of combo this was.",
59+
},
60+
clearLamp: {
61+
type: "ENUM",
62+
values: ["FAILED", "CLEAR", "HARD", "BRAVE", "ABSOLUTE", "CATASTROPHY"],
5763
minimumRelevantValue: "CLEAR",
5864
description: "The type of clear this was.",
5965
},

common/src/constants/game.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,20 @@ export enum IIDX_LAMPS {
99
FULL_COMBO = 7,
1010
}
1111

12-
export enum CHUNITHM_LAMPS {
12+
export enum CHUNITHM_CLEAR_LAMPS {
1313
FAILED = 0,
1414
CLEAR = 1,
15-
FULL_COMBO = 2,
16-
ALL_JUSTICE = 3,
17-
ALL_JUSTICE_CRITICAL = 4,
15+
HARD = 2,
16+
BRAVE = 3,
17+
ABSOLUTE = 4,
18+
CATASTROPHY = 5,
19+
}
20+
21+
export enum CHUNITHM_NOTE_LAMPS {
22+
NONE = 0,
23+
FULL_COMBO = 1,
24+
ALL_JUSTICE = 2,
25+
ALL_JUSTICE_CRITICAL = 3,
1826
}
1927

2028
export enum SDVX_LAMPS {

server/src/game-implementations/games/chunithm.test.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,30 @@ import { CHUNITHM_IMPL } from "./chunithm";
22
import db from "external/mongo/db";
33
import CreateLogCtx from "lib/logger/logger";
44
import { CreatePBDoc } from "lib/score-import/framework/pb/create-pb-doc";
5-
import { CHUNITHM_GRADES, CHUNITHM_LAMPS } from "tachi-common";
5+
import { CHUNITHM_GRADES, CHUNITHM_NOTE_LAMPS, CHUNITHM_CLEAR_LAMPS } from "tachi-common";
66
import t from "tap";
77
import { dmf, mkMockPB, mkMockScore } from "test-utils/misc";
88
import ResetDBState from "test-utils/resets";
99
import { CHUNITHMBBKKChart, TestingChunithmScorePB } from "test-utils/test-data";
1010
import type { ProvidedMetrics, ScoreData } from "tachi-common";
1111

1212
const baseMetrics: ProvidedMetrics["chunithm:Single"] = {
13-
lamp: "CLEAR",
13+
clearLamp: "CLEAR",
14+
noteLamp: "NONE",
1415
score: 1_003_000,
1516
};
1617

1718
const scoreData: ScoreData<"chunithm:Single"> = {
18-
lamp: "CLEAR",
19+
clearLamp: "CLEAR",
20+
noteLamp: "NONE",
1921
score: 1_003_000,
2022
grade: "SS",
2123
judgements: {},
2224
optional: { enumIndexes: {} },
2325
enumIndexes: {
2426
grade: CHUNITHM_GRADES.SS,
25-
lamp: CHUNITHM_LAMPS.CLEAR,
27+
clearLamp: CHUNITHM_CLEAR_LAMPS.CLEAR,
28+
noteLamp: CHUNITHM_NOTE_LAMPS.NONE,
2629
},
2730
};
2831

@@ -165,7 +168,8 @@ t.test("CHUNITHM Implementation", (t) => {
165168

166169
f("grade", { grade: "S+", score: 997_342 }, CHUNITHM_GRADES.SS, "SS-2.7K");
167170
f("score", { score: 982_123 }, 1_000_000, "982,123");
168-
f("lamp", { lamp: "CLEAR" }, CHUNITHM_LAMPS.CLEAR, "CLEAR");
171+
f("clearLamp", { clearLamp: "CLEAR" }, CHUNITHM_CLEAR_LAMPS.CLEAR, "CLEAR");
172+
f("noteLamp", { noteLamp: "FULL COMBO" }, CHUNITHM_NOTE_LAMPS.FULL_COMBO, "FULL COMBO");
169173

170174
t.end();
171175
});
@@ -187,21 +191,39 @@ t.test("CHUNITHM Implementation", (t) => {
187191
await db.scores.insert(mockScore);
188192
await db.scores.insert(
189193
dmf(mockScore, {
190-
scoreID: "bestLamp",
194+
scoreID: "bestNoteLamp",
191195
scoreData: {
192196
score: 0,
193-
lamp: "FULL COMBO",
194-
enumIndexes: { lamp: CHUNITHM_LAMPS.FULL_COMBO },
197+
noteLamp: "FULL COMBO",
198+
enumIndexes: { noteLamp: CHUNITHM_NOTE_LAMPS.FULL_COMBO },
199+
},
200+
})
201+
);
202+
await db.scores.insert(
203+
dmf(mockScore, {
204+
scoreID: "bestClearLamp",
205+
scoreData: {
206+
score: 0,
207+
clearLamp: "ABSOLUTE",
208+
enumIndexes: { clearLamp: CHUNITHM_CLEAR_LAMPS.ABSOLUTE },
195209
},
196210
})
197211
);
198212

199213
t.hasStrict(await CreatePBDoc("chunithm:Single", 1, CHUNITHMBBKKChart, logger), {
200-
composedFrom: [{ name: "Best Score" }, { name: "Best Lamp", scoreID: "bestLamp" }],
214+
composedFrom: [
215+
{ name: "Best Score" },
216+
{ name: "Best Note Lamp", scoreID: "bestNoteLamp" },
217+
{ name: "Best Clear Lamp", scoreID: "bestClearLamp" },
218+
],
201219
scoreData: {
202220
score: mockScore.scoreData.score,
203-
lamp: "FULL COMBO",
204-
enumIndexes: { lamp: CHUNITHM_LAMPS.FULL_COMBO },
221+
clearLamp: "ABSOLUTE",
222+
noteLamp: "FULL COMBO",
223+
enumIndexes: {
224+
clearLamp: CHUNITHM_CLEAR_LAMPS.ABSOLUTE,
225+
noteLamp: CHUNITHM_NOTE_LAMPS.FULL_COMBO,
226+
},
205227
},
206228
});
207229

server/src/game-implementations/games/chunithm.ts

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,28 +71,45 @@ export const CHUNITHM_IMPL: GPTServerImplementation<"chunithm:Single"> = {
7171
pb.scoreData.score,
7272
CHUNITHM_GBOUNDARIES[gradeIndex]!.name
7373
),
74-
75-
lamp: (pb) => pb.scoreData.lamp,
74+
noteLamp: (pb) => pb.scoreData.noteLamp,
75+
clearLamp: (pb) => pb.scoreData.clearLamp,
7676
score: (pb) => FmtNum(pb.scoreData.score),
7777
},
7878
goalOutOfFormatters: {
7979
score: GoalOutOfFmtScore,
8080
},
8181
pbMergeFunctions: [
82-
CreatePBMergeFor("largest", "enumIndexes.lamp", "Best Lamp", (base, score) => {
83-
base.scoreData.lamp = score.scoreData.lamp;
82+
CreatePBMergeFor("largest", "enumIndexes.noteLamp", "Best Note Lamp", (base, score) => {
83+
base.scoreData.noteLamp = score.scoreData.noteLamp;
84+
}),
85+
CreatePBMergeFor("largest", "enumIndexes.clearLamp", "Best Clear Lamp", (base, score) => {
86+
base.scoreData.clearLamp = score.scoreData.clearLamp;
8487
}),
8588
],
8689
defaultMergeRefName: "Best Score",
8790
scoreValidators: [
8891
(s) => {
89-
if (s.scoreData.lamp === "ALL JUSTICE CRITICAL" && s.scoreData.score !== 1_010_000) {
92+
if (
93+
s.scoreData.noteLamp === "ALL JUSTICE CRITICAL" &&
94+
s.scoreData.score !== 1_010_000
95+
) {
9096
return "An ALL JUSTICE CRITICAL must have a score of 1.01 million.";
9197
}
9298

93-
if (s.scoreData.lamp !== "ALL JUSTICE CRITICAL" && s.scoreData.score === 1_010_000) {
99+
if (
100+
s.scoreData.noteLamp !== "ALL JUSTICE CRITICAL" &&
101+
s.scoreData.score === 1_010_000
102+
) {
94103
return "A score of 1.01 million must have a lamp of ALL JUSTICE CRITICAL.";
95104
}
105+
106+
if (s.scoreData.noteLamp === "ALL JUSTICE" && s.scoreData.score < 1_000_000) {
107+
return `A score of ${s.scoreData.score} cannot be an ALL JUSTICE.`;
108+
}
109+
110+
if (s.scoreData.noteLamp === "FULL COMBO" && s.scoreData.score < 500_000) {
111+
return `A score of ${s.scoreData.score} cannot be a FULL COMBO.`;
112+
}
96113
},
97114
(s) => {
98115
let { attack, justice, miss } = s.scoreData.judgements;
@@ -101,23 +118,73 @@ export const CHUNITHM_IMPL: GPTServerImplementation<"chunithm:Single"> = {
101118
attack ??= 0;
102119
miss ??= 0;
103120

104-
if (s.scoreData.lamp === "ALL JUSTICE CRITICAL") {
121+
if (s.scoreData.noteLamp === "ALL JUSTICE CRITICAL") {
105122
if (attack + justice + miss > 0) {
106123
return "Cannot have an ALL JUSTICE CRITICAL with any non-jcrit judgements.";
107124
}
108125
}
109126

110-
if (s.scoreData.lamp === "ALL JUSTICE") {
127+
if (s.scoreData.noteLamp === "ALL JUSTICE") {
111128
if (attack + miss > 0) {
112129
return "Cannot have an ALL JUSTICE if not all hits were justice or better.";
113130
}
114131
}
115132

116-
if (s.scoreData.lamp === "FULL COMBO") {
133+
if (s.scoreData.noteLamp === "FULL COMBO") {
117134
if (miss > 0) {
118135
return "Cannot have a FULL COMBO if the score has misses.";
119136
}
120137
}
121138
},
139+
(s) => {
140+
const { maxCombo } = s.scoreData.optional;
141+
const { attack, jcrit, justice, miss } = s.scoreData.judgements;
142+
143+
if (
144+
IsNullish(maxCombo) ||
145+
IsNullish(attack) ||
146+
IsNullish(jcrit) ||
147+
IsNullish(justice) ||
148+
IsNullish(miss)
149+
) {
150+
return;
151+
}
152+
153+
if (s.scoreData.noteLamp !== "NONE" && jcrit + justice + attack + miss !== maxCombo) {
154+
const article = s.scoreData.noteLamp === "FULL COMBO" ? "a" : "an";
155+
156+
return `Cannot have ${article} ${s.scoreData.noteLamp} if maxCombo is not equal to the sum of judgements.`;
157+
}
158+
},
159+
(s) => {
160+
const { attack, justice, miss } = s.scoreData.judgements;
161+
162+
// Assume the clear lamp is correct if judgements aren't provided.
163+
if (IsNullish(attack) || IsNullish(justice) || IsNullish(miss)) {
164+
return;
165+
}
166+
167+
if (s.scoreData.clearLamp === "CATASTROPHY" && justice + attack + miss >= 10) {
168+
return "Cannot have a CATASTROPHY clear with 10 or more non-jcrit judgements.";
169+
}
170+
171+
if (s.scoreData.clearLamp === "ABSOLUTE" && justice + attack + miss >= 50) {
172+
return "Cannot have an ABSOLUTE clear with 50 or more non-jcrit judgements.";
173+
}
174+
175+
if (s.scoreData.clearLamp === "BRAVE" && justice + attack + miss >= 150) {
176+
return "Cannot have a BRAVE clear with 150 or more non-jcrit judgements.";
177+
}
178+
179+
// The condition for a HARD clear varies based on the skill used:
180+
// - JUDGE: 20 misses
181+
// - JUDGE+: 10 misses
182+
// - EMBLEM: 300 justices or below
183+
// Since we do not have information about the skill used, we simply validate that a
184+
// hard clear is not completely impossible, i.e. more than 20 misses and more than 300 justices.
185+
if (s.scoreData.clearLamp === "HARD" && justice + attack + miss >= 300 && miss >= 20) {
186+
return "Cannot have a HARD clear with 300 or more non-jcrit judgements, and over 20 misses.";
187+
}
188+
},
122189
],
123190
};

0 commit comments

Comments
 (0)