feat: Ongeki support (#1013)

* feat: initial ongeki support

* feat: initial ongeki seeds

* chore: link to a basic score exporter

* fix: unhardcode the colSpan of score dropdowns

Fixes ongeki and doesn't break chunithm

* fix: make bellCount optional to match judgements and damage

* docs: add a page for Ongeki

* feat: ongeki seeder

* feat: implement isHot

Also restore Arcaea to test.conf.json5 (it was a mistake)

* fix: add isHot to the seeder

* fix: proper handling of re:masters

Also change non-full-bell from an empty string to "NONE", for clarity

* docs: document the re:master fix

* feat: update rg-stats to 0.5.5

enables ongeki calcs

* chore: lockfile

* fix: get rid of scales as they're unused

* fix: update name->NAME

* minrelevantvalue should be one of the values?

* fix: add property that must be there?

* fix: jesus christ the tests are loud

* better docs on mrv

* fix: remove platdelta, make platscore optional

* fix: grade B tests oh my god

* fix: rating check

* fix: pb merge check

* fix: pb merge check #2

* fix: use nowrap in all lamp cells

* fix: tidy up platcell

* fix: all of these MUST be part of scoreID

* fix: remove unused code

* feat: remove note counts & isHot

* chore: bump rg-stats

---------

Co-authored-by: zkldi <20380519+zkldi@users.noreply.github.com>
This commit is contained in:
nairobi
2024-05-08 18:00:46 +01:00
committed by GitHub
co-authored by zkldi
parent 6e87ab28de
commit 4a80b1f516
39 changed files with 82721 additions and 51 deletions
+1 -1
View File
@@ -67,7 +67,7 @@
"react-router-bootstrap": "^0.25.0",
"react-router-dom": "5.1.2",
"react-select": "^5.6.1",
"rg-stats": "0.5.4",
"rg-stats": "0.5.6",
"sync-fetch": "^0.3.1",
"tachi-common": "workspace:../common",
"vite-plugin-html": "^3.2.0"
@@ -341,7 +341,22 @@ function ImportInfoDisplayer({ game }: { game: Game }) {
key="SL-ITG"
/>
);
} // else if (game === "jubeat") {
} else if (game === 'ongeki') {
Content.unshift(
<ImportInfoCard
name="ARTEMiS Exporter"
href="ongeki-artemis-exporter"
desc="Export your scores from an ARTEMiS instance."
moreInfo={
<>
This is a script that exports scores from an ARTEMiS instance. <br />
Note: You will need direct access to the server instance.
</>
}
key="ARTEMiS Exporter"
/>
);
}// else if (game === "jubeat") {
// Content.unshift(
// <ImportTypeInfoCard key="api/cg-dev-jubeat" importType="api/cg-dev-jubeat" />,
// <ImportTypeInfoCard key="api/cg-prod-jubeat" importType="api/cg-prod-jubeat" />
@@ -0,0 +1,27 @@
import useSetSubheader from "components/layout/header/useSetSubheader";
import Divider from "components/util/Divider";
import ExternalLink from "components/util/ExternalLink";
import { TachiConfig } from "lib/config";
import React from "react";
export default function OngekiArtemisExport() {
useSetSubheader(["Import Scores", "O.N.G.E.K.I. ARTEMiS Exporter"]);
return (
<div>
<h1 className="text-center mb-4">What is the ARTEMiS Exporter?</h1>
<div>
The Artemis Exporter is a script that will export your O.N.G.E.K.I. scores from an
ARTEMiS instance to a BATCH-MANUAL JSON for import to {TachiConfig.NAME}. You will
need direct access to the server instance.
</div>
<Divider />
<h1 className="text-center my-4">Setup Instructions</h1>
Instructions are available on{" "}
<ExternalLink href="https://gist.github.com/nyairobi/ffdf9e674f31987b1ffbd38d31b55f6c">
the GitHub gist
</ExternalLink>
.
</div>
);
}
@@ -1,5 +1,5 @@
import { APIFetchV1 } from "util/api";
import { Reverse } from "util/misc";
import { Reverse, UppercaseFirst } from "util/misc";
import DistributionTable from "components/game/folder/FolderDistributionTable";
import Card from "components/layout/page/Card";
import DebounceSearch from "components/util/DebounceSearch";
@@ -14,7 +14,13 @@ import useUGPTBase from "components/util/useUGPTBase";
import { UserContext } from "context/UserContext";
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
import React, { useContext, useMemo, useState } from "react";
import { FolderDocument, GetGPTString, GetGamePTConfig, GetScoreMetricConf } from "tachi-common";
import {
FolderDocument,
GetGPTString,
GetGamePTConfig,
GetScoreMetricConf,
GetScoreMetrics,
} from "tachi-common";
import { ConfEnumScoreMetric } from "tachi-common/types/metrics";
import { FolderStatsInfo, UGPTFolderSearch } from "types/api-returns";
import { UGPT } from "types/react";
@@ -130,12 +136,12 @@ export function FolderInfoComponent({
<div className="row text-center">
<div className="col-12">
<div className="btn-group">
<SelectButton value={metric} setValue={setMetric} id="grade">
<Icon type="sort-alpha-up" /> Grades
</SelectButton>
<SelectButton value={metric} setValue={setMetric} id="lamp">
<Icon type="lightbulb" /> Lamps
</SelectButton>
{GetScoreMetrics(gptConfig, "ENUM").map((e) => (
<SelectButton value={metric} setValue={setMetric} id={e}>
{/* @ts-expect-error this access is legal zzz */}
<Icon type={gptImpl.enumIcons[e]} /> {UppercaseFirst(e)}s
</SelectButton>
))}
</div>
<Divider />
{dataset}
+5
View File
@@ -29,6 +29,7 @@ import ChunithmSiteImportPage from "app/pages/dashboard/import/ChunithmSiteImpor
import SaekawaPage from "app/pages/dashboard/import/SaekawaPage";
import AquaArtemisExport from "app/pages/dashboard/import/AquaArtemisExportPage";
import ChunithmMYTExport from "app/pages/dashboard/import/ChunithmMYTExportPage";
import OngekiArtemisExport from "app/pages/dashboard/import/OngekiArtemisExportPage";
export default function ImportRoutes() {
const { user } = useContext(UserContext);
@@ -224,6 +225,10 @@ export default function ImportRoutes() {
<Route exact path="/import/kt-maimaidx-site-importer">
<MaimaiDXSiteImportPage />
</Route>
<Route exact path="/import/ongeki-artemis-exporter">
<OngekiArtemisExport />
</Route>
</>
)}
</Route>
@@ -28,6 +28,7 @@ const GPT_UTILS: Record<GPTString, Array<GPTUtility>> = {
"wacca:Single": [],
"maimaidx:Single": [],
"maimai:Single": [],
"ongeki:Single": [],
};
export function GetGPTUtils(game: Game, playtype: Playtype) {
@@ -15,6 +15,7 @@ export default function BMSOrPMSLampCell({
<td
style={{
backgroundColor: ChangeOpacity(GetEnumColour(score, "lamp"), 0.2),
whiteSpace: "nowrap",
}}
>
<strong>{score.scoreData.lamp}</strong>
@@ -3,7 +3,7 @@ import { ChangeOpacity } from "util/color-opacity";
import { IsNotNullish } from "util/misc";
import { GetEnumColour } from "lib/game-implementations";
import React from "react";
import { ChartDocument, GetGPTString, PBScoreDocument, ScoreDocument } from "tachi-common";
import { ChartDocument, PBScoreDocument, ScoreDocument } from "tachi-common";
export default function IIDXLampCell({
sc,
@@ -62,6 +62,7 @@ export default function IIDXLampCell({
<td
style={{
backgroundColor: ChangeOpacity(GetEnumColour(sc, "lamp"), 0.2),
whiteSpace: "nowrap",
}}
>
<strong>{sc.scoreData.lamp}</strong>
@@ -6,6 +6,7 @@ export default function LampCell({ lamp, colour }: { lamp: string; colour: strin
<td
style={{
backgroundColor: ChangeOpacity(colour, 0.2),
whiteSpace: "nowrap",
}}
>
<strong>{lamp}</strong>
@@ -0,0 +1,42 @@
import { IsNullish } from "util/misc";
import React from "react";
import { COLOUR_SET, PBScoreDocument, ScoreDocument } from "tachi-common";
export default function OngekiJudgementCell({
score,
}: {
score: ScoreDocument<"ongeki:Single"> | PBScoreDocument<"ongeki:Single">;
}) {
const judgements = score.scoreData.judgements;
if (
IsNullish(judgements.miss) ||
IsNullish(judgements.hit) ||
IsNullish(judgements.break) ||
IsNullish(judgements.cbreak)
) {
return <td>No Data.</td>;
}
return (
<td>
<strong>
<div>
<span style={{ color: COLOUR_SET.gold }}>{judgements.cbreak}</span>-
<span style={{ color: COLOUR_SET.orange }}>{judgements.break}</span>-
<span style={{ color: COLOUR_SET.vibrantBlue }}>{judgements.hit}</span>-
<span style={{ color: COLOUR_SET.gray }}>{judgements.miss}</span>
</div>
<div>
<span style={{ color: COLOUR_SET.vibrantYellow }}>
{score.scoreData.optional.bellCount ?? "?"}/
{score.scoreData.optional.totalBellCount ?? "?"}
</span>
<span style={{ color: COLOUR_SET.red, marginLeft: "0.5em" }}>
{score.scoreData.optional.damage ?? "?"}
</span>
</div>
</strong>
</td>
);
}
@@ -0,0 +1,38 @@
import { ChangeOpacity } from "util/color-opacity";
import React from "react";
export default function OngekiLampCell({
lamp1,
lamp2,
colour,
}: {
lamp1: string;
lamp2: string;
colour: string;
}) {
let content = <div>{lamp1}</div>;
if (lamp2 !== "NONE") {
if (lamp1 === "CLEAR") {
content = <div>{lamp2}</div>;
} else {
content = (
<span>
<div>{lamp1}</div>
<div>{lamp2}</div>
</span>
);
}
}
return (
<td
style={{
backgroundColor: ChangeOpacity(colour, 0.2),
whiteSpace: "nowrap",
}}
>
<strong>{content}</strong>
</td>
);
}
@@ -16,6 +16,7 @@ export default function PopnLampCell({
GPT_CLIENT_IMPLEMENTATIONS["popn:9B"].enumColours.lamp[score.scoreData.lamp],
0.2
),
whiteSpace: "nowrap",
}}
>
<strong>{score.scoreData.lamp}</strong>
@@ -15,6 +15,7 @@ export default function SDVXLampCell({
<td
style={{
backgroundColor: ChangeOpacity(GetEnumColour(score, "lamp"), 0.2),
whiteSpace: "nowrap",
}}
>
<strong>{score.scoreData.lamp === "FAILED" ? "PLAYED" : score.scoreData.lamp}</strong>
@@ -4,10 +4,11 @@ import TimestampCell from "components/tables/cells/TimestampCell";
import ScoreCoreCells from "components/tables/game-core-cells/ScoreCoreCells";
import useScoreRatingAlg from "components/util/useScoreRatingAlg";
import React, { useContext, useEffect, useState } from "react";
import { ChartDocument, PBScoreDocument, ScoreDocument } from "tachi-common";
import { ChartDocument, GetGPTString, PBScoreDocument, ScoreDocument } from "tachi-common";
import { UGPTChartPBComposition } from "types/api-returns";
import { SetState } from "types/react";
import { UserContext } from "context/UserContext";
import { GPT_CLIENT_IMPLEMENTATIONS } from "lib/game-implementations";
import CommentContainer from "./CommentContainer";
import JudgementTable from "./JudgementTable";
import PBNote from "./PBNote";
@@ -22,13 +23,14 @@ export function ScoreInfo({
chart: ChartDocument;
}) {
const rating = useScoreRatingAlg(score.game, score.playtype);
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[GetGPTString(score.game, chart.playtype)];
return (
<div className="col-12">
<table className="table">
<thead>
<tr>
<td colSpan={3}>Score Info</td>
<td colSpan={gptImpl.scoreHeaders.length}>Score Info</td>
<td>{UppercaseFirst(rating)}</td>
<td>Timestamp</td>
</tr>
+112 -5
View File
@@ -1,5 +1,6 @@
import { NumericSOV } from "util/sorts";
import { ChangeOpacity } from "util/color-opacity";
import { FormatMillions } from "util/misc";
import { COLOUR_SET, GPTString, GetGPTString, PBScoreDocument, ScoreDocument } from "tachi-common";
import CHUNITHMJudgementCell from "components/tables/cells/CHUNITHMJudgementCell";
import ITGJudgementCell from "components/tables/cells/ITGJudgementCell";
@@ -16,7 +17,9 @@ import PopnLampCell from "components/tables/cells/PopnLampCell";
import RatingCell from "components/tables/cells/RatingCell";
import ScoreCell from "components/tables/cells/ScoreCell";
import WaccaJudgementCell from "components/tables/cells/WACCAJudgementCell";
import OngekiJudgementCell from "components/tables/cells/OngekiJudgementCell";
import React from "react";
import OngekiLampCell from "components/tables/cells/OngekiLampCell";
import { CreateRatingSys, bgc } from "./games/_util";
import { BMS_14K_IMPL, BMS_7K_IMPL, PMS_IMPL } from "./games/bms-pms";
import { IIDX_DP_IMPL, IIDX_SP_IMPL } from "./games/iidx";
@@ -111,7 +114,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
["Judgements", "Hits", NumericSOV((x) => x.scoreData.score)],
["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)],
],
scoreCoreCells: ({ sc, chart }) => (
scoreCoreCells: ({ sc }) => (
<>
<MillionsScoreCell
score={sc.scoreData.score}
@@ -173,7 +176,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
["Judgements", "Hits", NumericSOV((x) => x?.scoreData.musicRate ?? -Infinity)],
["Lamp", "Lamp", NumericSOV((x) => x?.scoreData.enumIndexes.lamp ?? -Infinity)],
],
scoreCoreCells: ({ sc, chart }) => (
scoreCoreCells: ({ sc }) => (
<>
<JubeatScoreCell sc={sc} />
<JubeatJudgementCell score={sc} />
@@ -458,7 +461,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
["Near - Miss", "Nr. Ms.", NumericSOV((x) => x?.scoreData.score)],
["Lamp", "Lamp", NumericSOV((x) => x?.scoreData.enumIndexes.lamp)],
],
scoreCoreCells: ({ sc, chart }) => (
scoreCoreCells: ({ sc }) => (
<>
<MillionsScoreCell
score={sc.scoreData.score}
@@ -664,7 +667,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
"How fast are the streams in this chart?",
(c) => c.data.streamBPM,
(c) => c.data.streamBPM?.toString(),
(c) => undefined,
() => undefined,
(s) => [
s.scoreData.lamp === "FAILED"
? `Failed ${s.scoreData.survivedPercent.toFixed(2)}%`
@@ -678,7 +681,7 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
["Judgements", "Hits", NumericSOV((x) => x.scoreData.scorePercent)],
["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)],
],
scoreCoreCells: ({ sc, chart }) => (
scoreCoreCells: ({ sc }) => (
<>
<ScoreCell
colour={GetEnumColour(sc, "grade")}
@@ -717,6 +720,110 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
</>
),
},
"ongeki:Single": {
enumIcons: {
grade: "sort-alpha-up",
noteLamp: "lightbulb",
bellLamp: "lightbulb",
},
classColours: {
colour: {
BLUE: bgc("var(--bs-info)", "var(--bs-light)"),
GREEN: bgc("green", "var(--bs-light)"),
ORANGE: bgc("orange", "var(--bs-dark)"),
RED: bgc("red", "var(--bs-light)"),
PURPLE: bgc("purple", "var(--bs-light)"),
COPPER: bgc("sienna", "var(--bs-light)"),
SILVER: bgc("gray", "var(--bs-light)"),
GOLD: bgc("var(--bs-warning)", "var(--bs-dark)"),
PLATINUM: bgc("silver", "var(--bs-dark)"),
RAINBOW: {
background:
"linear-gradient(-45deg, #f0788a, #f48fb1, #9174c2, #79bcf2, #70a173, #f7ff99, #faca7d, #ff9d80, #f0788a)",
color: "var(--bs-dark)",
},
},
},
enumColours: {
grade: {
D: COLOUR_SET.red,
C: COLOUR_SET.purple,
B: COLOUR_SET.paleBlue,
BB: COLOUR_SET.blue,
BBB: COLOUR_SET.vibrantBlue,
A: COLOUR_SET.paleGreen,
AA: COLOUR_SET.green,
AAA: COLOUR_SET.vibrantGreen,
S: COLOUR_SET.orange,
SS: COLOUR_SET.vibrantYellow,
SSS: COLOUR_SET.teal,
"SSS+": COLOUR_SET.white,
},
noteLamp: {
LOSS: COLOUR_SET.red,
CLEAR: COLOUR_SET.paleGreen,
"FULL COMBO": COLOUR_SET.paleBlue,
"ALL BREAK": COLOUR_SET.gold,
},
bellLamp: {
NONE: COLOUR_SET.red,
"FULL BELL": COLOUR_SET.gold,
},
},
difficultyColours: {
BASIC: COLOUR_SET.blue,
ADVANCED: COLOUR_SET.orange,
EXPERT: COLOUR_SET.red,
MASTER: COLOUR_SET.purple,
LUNATIC: COLOUR_SET.vibrantRed,
},
ratingSystems: [],
scoreHeaders: [
[
"Score",
"Score",
NumericSOV(
(x) => x.scoreData.score * 10000 + (x.scoreData.optional.platScore ?? 0)
),
],
["Judgements", "Hits", NumericSOV((x) => x.scoreData.score)],
[
"Lamp",
"Lamp",
NumericSOV(
(x) =>
(x.scoreData.enumIndexes.noteLamp << 8) + x.scoreData.enumIndexes.bellLamp
),
],
],
scoreCoreCells: ({ sc, chart }) => (
<>
<td
style={{
backgroundColor: ChangeOpacity(GetEnumColour(sc, "grade"), 0.2),
}}
>
<strong>{sc.scoreData.grade}</strong>
<br />
{FormatMillions(sc.scoreData.score)}
{typeof sc.scoreData.optional.platScore === "number" &&
(chart.difficulty === "MASTER" || chart.difficulty === "LUNATIC") && (
<>
<br />
[Plat: {sc.scoreData.optional.platScore}]
</>
)}
</td>
<OngekiJudgementCell score={sc} />
<OngekiLampCell
lamp1={sc.scoreData.noteLamp}
lamp2={sc.scoreData.bellLamp}
colour={GetEnumColour(sc, "noteLamp")}
/>
</>
),
ratingCell: ({ sc, rating }) => <RatingCell score={sc} rating={rating} />,
},
"arcaea:Touch": ARCAEA_TOUCH_IMPL,
"gitadora:Dora": GITADORA_DORA_IMPL,
"gitadora:Gita": GITADORA_GITA_IMPL,
-23
View File
@@ -1,23 +0,0 @@
import { Game } from "tachi-common";
const scales: Record<Game, number> = {
iidx: 2,
bms: 2,
pms: 2,
chunithm: 5,
gitadora: 1,
maimaidx: 1,
maimai: 1,
museca: 5,
sdvx: 5,
usc: 5,
wacca: 5,
popn: 4,
jubeat: 5,
itg: 5,
arcaea: 1,
};
export function GetGradeChartExpScale(game: Game) {
return scales[game];
}
+3
View File
@@ -10,6 +10,7 @@ import { JUBEAT_CONF, JUBEAT_SINGLE_CONF } from "./game-support/jubeat";
import { MAIMAI_CONF, MAIMAI_SINGLE_CONF } from "./game-support/maimai";
import { MAIMAI_DX_CONF, MAIMAI_DX_SINGLE_CONF } from "./game-support/maimai-dx";
import { MUSECA_CONF, MUSECA_SINGLE_CONF } from "./game-support/museca";
import { ONGEKI_CONF, ONGEKI_SINGLE_CONF } from "./game-support/ongeki";
import { PMS_CONF, PMS_CONTROLLER_CONF, PMS_KEYBOARD_CONF } from "./game-support/pms";
import { POPN_9B_CONF, POPN_CONF } from "./game-support/popn";
import { SDVX_CONF, SDVX_SINGLE_CONF } from "./game-support/sdvx";
@@ -48,6 +49,7 @@ export const GAME_CONFIGS = {
pms: PMS_CONF,
itg: ITG_CONF,
arcaea: ARCAEA_CONF,
ongeki: ONGEKI_CONF,
} as const satisfies Record<string, INTERNAL_GAME_CONFIG>;
/**
@@ -95,6 +97,7 @@ export const GAME_PT_CONFIGS = {
"usc:Keyboard": USC_KEYBOARD_CONF,
"itg:Stamina": ITG_STAMINA_CONF,
"arcaea:Touch": ARCAEA_TOUCH_CONF,
"ongeki:Single": ONGEKI_SINGLE_CONF,
} as const satisfies Record<GPTString, INTERNAL_GAME_PT_CONFIG>;
/**
+155
View File
@@ -0,0 +1,155 @@
import { FAST_SLOW_MAXCOMBO } from "./_common";
import { FmtNum } from "../../utils/util";
import { ClassValue } 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 ONGEKI_CONF = {
name: "O.N.G.E.K.I.",
playtypes: ["Single"],
songData: z.strictObject({
genre: z.string(),
}),
} as const satisfies INTERNAL_GAME_CONFIG;
export const OngekiColours = [
ClassValue("BLUE", "青", "Blue: 0 - 1.99 Rating"),
ClassValue("GREEN", "緑", "Green: 2 - 3.99 Rating"),
ClassValue("ORANGE", "橙", "Orange: 4 - 6.99 Rating"),
ClassValue("RED", "赤", "Red: 7 - 9.99 Rating"),
ClassValue("PURPLE", "紫", "Purple: 10 - 11.99 Rating"),
ClassValue("COPPER", "銅", "Copper: 12 - 12.99 Rating"),
ClassValue("SILVER", "銀", "Silver: 13 - 13.99 Rating"),
ClassValue("GOLD", "金", "Gold: 14.00 - 14.49 Rating"),
ClassValue("PLATINUM", "鉑", "Platinum: 14.50 - 14.99 Rating"),
ClassValue("RAINBOW", "虹", "Rainbow: >=15 Rating"),
];
export const ONGEKI_SINGLE_CONF = {
providedMetrics: {
score: {
type: "INTEGER",
validate: p.isBetween(0, 1_010_000),
formatter: FmtNum,
description:
"Known in-game as 'Technical Score'. It ranges between 0 and 1,010,000, where notes are worth 950,000, and bells 60,000.",
},
noteLamp: {
type: "ENUM",
values: ["LOSS", "CLEAR", "FULL COMBO", "ALL BREAK"],
minimumRelevantValue: "CLEAR",
description: "The first lamp. A clear is either a draw or a win.",
},
bellLamp: {
type: "ENUM",
values: ["NONE", "FULL BELL"],
minimumRelevantValue: "NONE",
description:
"The second lamp that tracks whether all bells in the chart have been collected.",
},
},
derivedMetrics: {
grade: {
type: "ENUM",
values: ["D", "C", "B", "BB", "BBB", "A", "AA", "AAA", "S", "SS", "SSS", "SSS+"],
minimumRelevantValue: "A",
description: "The grade this score was.",
},
},
defaultMetric: "score",
preferredDefaultEnum: "grade",
optionalMetrics: {
...FAST_SLOW_MAXCOMBO,
damage: {
type: "INTEGER",
chartDependentMax: true,
formatter: FmtNum,
description: "The number of damage ticks received.",
partOfScoreID: true,
},
bellCount: {
type: "INTEGER",
chartDependentMax: true,
formatter: FmtNum,
description: "The number of bells collected.",
partOfScoreID: true,
},
totalBellCount: {
type: "INTEGER",
chartDependentMax: true,
formatter: FmtNum,
description: "The total number of bells.",
},
platScore: {
type: "INTEGER",
chartDependentMax: true,
formatter: FmtNum,
description: "The Platinum Score value. Only exists in MASTER and LUNATIC charts.",
partOfScoreID: true,
},
},
scoreRatingAlgs: {
rating: {
description:
"The rating value of this score. This is identical to the system used in game.",
},
},
sessionRatingAlgs: {
naiveRating: { description: "The average of your best 10 ratings this session." },
},
profileRatingAlgs: {
naiveRating: {
description: "The average of your best 45 scores.",
},
},
defaultScoreRatingAlg: "rating",
defaultSessionRatingAlg: "naiveRating",
defaultProfileRatingAlg: "naiveRating",
difficulties: {
type: "FIXED",
order: ["BASIC", "ADVANCED", "EXPERT", "MASTER", "LUNATIC"],
shorthand: {
BASIC: "BAS",
ADVANCED: "ADV",
EXPERT: "EXP",
MASTER: "MAS",
LUNATIC: "LUN",
},
default: "MASTER",
},
classes: {
colour: {
type: "DERIVED",
values: OngekiColours,
},
},
orderedJudgements: ["cbreak", "break", "hit", "miss"],
versions: {
brightMemory2: "bright MEMORY Act.II",
brightMemory2Omni: "bright MEMORY Act.II Omnimix",
brightMemory3: "bright MEMORY Act.III",
brightMemory3Omni: "bright MEMORY Act.III Omnimix",
},
chartData: z.strictObject({
displayVersion: z.string(),
isUnranked: z.boolean().optional(),
isReMaster: z.boolean().optional(),
}),
preferences: z.strictObject({}),
scoreMeta: z.strictObject({}),
supportedMatchTypes: ["songTitle", "tachiSongID"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
+40
View File
@@ -556,3 +556,43 @@ export enum ARCAEA_COURSE_BANNERS {
PHASE_10 = 9,
PHASE_11 = 10,
}
export enum ONGEKI_NOTE_LAMPS {
LOSS = 0,
CLEAR = 1,
FULL_COMBO = 2,
ALL_BREAK = 3,
}
export enum ONGEKI_BELL_LAMPS {
NONE = 0,
FULL_BELL = 1,
}
export enum ONGEKI_GRADES {
D = 0,
C = 1,
B = 2,
BB = 3,
BBB = 4,
A = 5,
AA = 6,
AAA = 7,
S = 8,
SS = 9,
SSS = 10,
SSS_PLUS = 11,
}
export enum ONGEKI_COLOURS {
BLUE = 0,
GREEN = 1,
ORANGE = 2,
RED = 3,
PURPLE = 4,
COPPER = 5,
SILVER = 6,
GOLD = 7,
PLATINUM = 8,
RAINBOW = 9,
}
+15 -1
View File
@@ -1,4 +1,3 @@
import type { integer } from "../types";
import type { GPTStrings } from "../types/game-config";
import type { GetEnumValue } from "../types/metrics";
@@ -207,3 +206,18 @@ export const ARCAEA_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"arcaea:Touch
EX: 9_800_000,
"EX+": 9_900_000,
});
export const ONGEKI_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"ongeki:Single", "grade">>({
D: 0,
C: 500_000,
B: 700_000,
BB: 750_000,
BBB: 800_000,
A: 850_000,
AA: 900_000,
AAA: 940_000,
S: 970_000,
SS: 990_000,
SSS: 1_000_000,
"SSS+": 1_007_500,
});
+4
View File
@@ -82,6 +82,10 @@ export interface ConfEnumScoreMetric<V extends string> {
type: "ENUM";
values: ReadonlyArray<V>;
/**
* The minimum value that users should care about. Used to prevent UI-issues like saying
* "You got 5 new fails/D ranks today!" etc.
*/
minimumRelevantValue: V;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+212
View File
@@ -3932,6 +3932,218 @@
"tableID": "museca-Single-1.5-levels",
"title": "MÚSECA (1 + 1/2)"
},
{
"default": false,
"description": "Levels for O.N.G.E.K.I. in bright MEMORY Act.2 Omnimix.",
"folders": [
"F9d846efcecb94b8379983e966007b5ebb19a060840dabfd959b3325b5ca871c2",
"F272a16ebce16a4e751a0b6baf081be9dc8bb3c3e006edb55f6865f7ec158d9f7",
"Fa37c407703c21ebf12ce798182e2a664954d30dee1c9eb8e3e6c4ea2e7bef731",
"F13740536865b6c573ee1585270a10093936cad43e35ee8e0e6fec74c92a67ec9",
"Fb8d0753c792ab6c654f1d00304ed5ec44f883b6f24e2b4b0f1d5643e8a134afd",
"F2071665eb9e5ae130766d4a1ea52e54d8429820fca1a53b631f504d2f2c21f3d",
"F0f49415e52dfc6a8a7f26c6f46640fcbf9c7234e0fc90c79063c921ab18df7d4",
"Ffaeb5a16a8aa1977a61aac60e5b162e94436f99848d687939bae7debf69bb5ff",
"F7bef76d81570df63374a69330f2b36802ab1ae64c69aa2343c2062017bd6b2e1",
"Fa57648770688d3e7dbfcae723fbb81b1992c8b5aa858ccc304075b39efe33faf",
"Fc4deabf9610a80f9a9c5761ab37464c817f98a8b787d5e2d21690ef56cfbce34",
"Fad38dc6d28b0963e8937e1902159ca56a0fb0391b63b10b62b0cc59d1027be39",
"Fff292ed9235a780a3ca7587060a77e546f5094bd14643ad7c7e6e057fb3ee4e2",
"F35d2721aeffcd77d5350bc62601e4eeaa154cfe89ca1aa47b86f22f6da999503",
"F9ebe2cd32f49220392c2605c01d90b02597a56c1673fddaea32036b0798b3283",
"F8856f8ceb286ccdd6226e8e439553e6e2488a7777bdb252392dbca9889e94da2",
"F4d40825a38bae2fc95457d90378a60f08ee56a5db94c5f3799ee71ae75e1404a",
"Fdf848c21a5e32cc6db1a56f338bc665b5d9a8fc8182cda51141f88d8ec4dba6b",
"Fb4d6948fb690aa0a8572b9e9db05d8c9755778c4f28d7cdb2393b1875e2d946e",
"F5ab75ac4a2433e6afaf65462e1b36e4588fd13e1f4238b3418eb62ce00061b3d",
"F81f69854d7965c855719ec99bfa16f6b86fc53ce1deddd9b871ea9c233778a47",
"F1129428c65176fd73f322afe157f9a62e7d88df7c409ae8e047962af6225dc44",
"F45b9bb20da3100ddd68cf0e9744e7c343900a68509763c2e9cb0a937cf94db26",
"F8ac34ecb186a405c0b70968d276687b72e23e85c43ea5fe80eadfc1e3649fc46",
"F76f935699b091e1de36f52b8572ab882a3c339def09e8e1f8fdae1166fa43cd9"
],
"game": "ongeki",
"inactive": false,
"playtype": "Single",
"tableID": "ongeki-Single-brightMemory2Omni-levels",
"title": "O.N.G.E.K.I. (bright MEMORY Act.2 Omnimix)"
},
{
"default": false,
"description": "Difficulties for O.N.G.E.K.I. in bright MEMORY Act.2 Omnimix.",
"folders": [
"Fa6dfd0428218be473c5d1f305cd333cce405d77c9e8e6df7edb9775c2cbc2c05",
"F36fcfc0fcef6d056963e01643ded484e491bc95c04b10addfaece815d67347cd",
"F7e323d1a7496ca5adcec5c501fe9b5ff7bceec3da3cc7a137d70cdbc9abf80d9",
"Fc9a0662ab06e11e68f4700446f9ab0c7ed9a01442b0acaf812114234461e3ea9",
"F6c7768ca1c35243b0fd34b0db875e6f1c5b890c5b1f86be888d28ce91bce92eb",
"Fe38407fda4f0e77274d6ae67614e51a3a44202a600f0749b3ccb2745da241928"
],
"game": "ongeki",
"inactive": false,
"playtype": "Single",
"tableID": "ongeki-Single-brightMemory2Omni-difficulties",
"title": "O.N.G.E.K.I. (bright MEMORY Act.2 Omnimix) (Difficulties)"
},
{
"default": true,
"description": "Levels for O.N.G.E.K.I. in bright MEMORY Act.2.",
"folders": [
"Ffd9cf2a106b24860953efb32318714ff2c221a74b79fd57e5182a389f44cb1dc",
"Fb1bbc8ce3fe6208ab25c385398a2e27fad6c6f5fa040d621ed066ce83633bbed",
"F0e20686c106aab0a897f52cd3eaf3a5139ef03ec1dedb9eb3282574be03969c8",
"F51a06f5da5e38a853c37126de751551954f6f931ca82e747663f86449b0ff205",
"F583db98ee120691cdf78e20117cf94aa86a3fa657326cff4a37a38bfdcbe651b",
"Ff098d916c6b9a5302cd884c5f7f95902fdd032a5d5c0659f46bf0a740133b580",
"Ff170ebb1b4e6435bd2ea60de9e0eb3ec115ae799ec4d0dc0fc0cfdb4bb784864",
"Fb74568edd41f9fa68307baef37d7e952286b6727c5c0539dd2b0c652b8542682",
"Fe17b5de32044eb9075c5a828afcd74f646e41ee1a46e04fbdf2bd4a23709ebb5",
"F406cce9133fb1693c6bd9cc8285b142cbba8178be681b57eed40670f7e7fd396",
"F71d69895bfd9a6fab8e0a6a067be68dc7e5c6b1d370a415d8380a4549bc5e25a",
"Fcedc5b2197356f5059b68811257932212753fa3db373cdb130c0e858eca2d75c",
"Fac27c3384aba808c30ea1e5255c31fd4438130ac3ac59f053c36f3b4e949ed27",
"Faaa32dc865025f5d188905c008d48c4a79e8c0ad7dad6284542d51923005520b",
"Fae137e8a9092a18bd058a831f2f02febc752ddedea54b8b24750d74115d7e654",
"F20fb32ddaec6609c127e1ffabdaa28a36707f50fc52636463beeba9e733ce771",
"F7da3a8dbba05d8bfc2963e23ee890f6f3276ce77f0bcf39ed821d3d6134edf2a",
"Fb71c7dc0fa7cc24714575a03770c3506dca63e7a46f56afe78acb6b10482500a",
"F572f54b15e0742ff686b237a055ade44fcafc565f1a728d4741d51a1d70a0885",
"Faef2411d9d27e6893ba721b3e437b2a5e69cad70eae5d87194f618701230b139",
"Fab85a25c2f20412d242995d441d840323442d989256c6b8c97e4a0d8c03821d2",
"F1c5ccbba8da3bde18b6c9b2c345949bae4ca9aba14adc21ec5dd4122f7d8e3bf",
"Fc726778bfc50de7e77b246d86b25d10d48025c2eb0d8de7b0f77df5a9a114502",
"Faaf526ea4c67d1456a091d19ffd7c9fb1a504da98dd33f4fa4687d37aa64c407",
"F290de9ba414d8ed6d8ea5889fcc981d1845e8edb78ce16ee203e51b5698eb15d"
],
"game": "ongeki",
"inactive": false,
"playtype": "Single",
"tableID": "ongeki-Single-brightMemory2-levels",
"title": "O.N.G.E.K.I. (bright MEMORY Act.2)"
},
{
"default": false,
"description": "Difficulties for O.N.G.E.K.I. in bright MEMORY Act.2.",
"folders": [
"Fc6eb78670ec730d94de7e2b422f9db8d5c8e096e225a0769efc9818cdbf76e9c",
"Fbcb6bb96b104a5229321b0fd292418f20e6881c5a9304e610dff985e17231bfa",
"Ff12aac666f9f280d4a60025984c64609f671d992e13c9009d90a4ff8bde0662f",
"F0c4c33417aae13c49ff36e548d4d2b8a481718491a3272377465800de046af02",
"Fe5cfcd9c118e8eca4b3397019947e0287cfe09f296d381e0528af02724ac60fe",
"F8c55bc4cbe3654d5a5a38073b7c4cc40966a5c33fc8d4087ccd7e34ad3ca10da"
],
"game": "ongeki",
"inactive": false,
"playtype": "Single",
"tableID": "ongeki-Single-brightMemory2-difficulties",
"title": "O.N.G.E.K.I. (bright MEMORY Act.2) (Difficulties)"
},
{
"default": false,
"description": "Levels for O.N.G.E.K.I. in bright MEMORY Act.3 Omnimix.",
"folders": [
"F276de07bc9ce42088c76a4c65dcf962241c5b509533cb035558f9369565ab938",
"Fdb1ec28d31c8618ee2b3dd739e3140e9e3f7e2095f316b691c66af6a876a72c5",
"F5ab7e27e7f8b8812df8819acb5332b3a0c326f9b958d75bbf096cff7d1d5ceb3",
"F0ecbb82f7553a35e9e5289637f37c5be1d3dc6afc92c9a329ddce1758f040060",
"F57cd134859f56236e8bf8546ff0b785dfbe62e1b812f9d314135a3154ff7fb03",
"Ff9d5dd4f1d9b5de66b11bffaf8ff3cae8f03db1a7c6555113a57cae957f870cb",
"Fde0e368cd05bfffc1ad239e229de4e8ae7bdcce74723590ffff149f89758d920",
"Faf59d0fe492d2272d0d3a67e9853e1d1c36bcad7e3e9bf565856980a66ec65df",
"F912cb7870f729765b2714e3f0ae7d7f3b45b4d37fa871deb98f135906a263f26",
"Ff47f3a0a4c8d41b28f33ec39ef2f2a0f0ee92ef022607629dc77b68da5245e9f",
"F910f2ee1b8f1710cf39032a65c4f9abefe243612b63afdb1342e372362fb89e8",
"F4062d8cceb274e9d167b4f51e396219f229b9cf459ded3779a354929aa0d6e32",
"F082e8b4dc8831a1fd5c1819d88125d8460695130f59616188fa13dbc45cf4263",
"F10663b393c3337602e8064f924c3f9ec8ae4db5309aa772492bcbb7d8b690adf",
"F0a6fb91e9726baaa8b37995f5bb50613d08674017117fd86732a2743ec9cdba2",
"F640450d271c7477fdeb75f80b4091debfcb4543c623f380f5561a17f9aaf74d9",
"F14881345781cc7f04e92dc26b28c6e888e1ce0dc35dc60de79aea41d384fff3e",
"F22918c2040d224a851cf20ad6d20f6b01f8f882b0bb7e5e6a924951902ae139c",
"Ff2f03e6934cb5304eb6692341b0fcf479f6234c3eac2d20e2d3c14dec226fab0",
"Ff1d306ffa2d62ba67e000e8acfdc2ab745fe79edee27f4b8207342cea022cc9d",
"F48746c903310d676745370e1de478a1acfdf431769af3268e2495d1089f31ce0",
"Fa5ff25ddff12ecece13aa3b7995b0daeb9c9af3e6c2a484eb15a02e4a977096a",
"F81d2d29ad4378b2c0018e7edf60597913bd8fa0120a2b9bcdeff07a73e83be61",
"F4a8669b7afd6fc7de1d901495c0f5ec8d59038d60020466d8dbf8c962fbea019",
"Fb25a317f41d1e3125750a24d489b3949165adc7fc6a0462bcbf0d79e5314ed52"
],
"game": "ongeki",
"inactive": false,
"playtype": "Single",
"tableID": "ongeki-Single-brightMemory3Omni-levels",
"title": "O.N.G.E.K.I. (bright MEMORY Act.3 Omnimix)"
},
{
"default": false,
"description": "Difficulties for O.N.G.E.K.I. in bright MEMORY Act.3 Omnimix.",
"folders": [
"F8a5a5d7424c21898a445961d185812c95e53f668863aed7a1e6e693e76ff7c11",
"F5abeae4a2952c9d0d4df7ae03f71634fcb1b02076841c35a5820aa88f8d7551f",
"F2454301003bbd29013d4626a05a16680a38d9d54a8dd265b5b3dfceb7a994c51",
"F86c6409ff8b9ace572714e3361d456f346708b45eaf1652fd5d86e81b2111989",
"F32de08ef7420094b8b1c9719858afb6d5176bc97f17b60827c11ce2a3a5af83b",
"F20d1e739fd77625944c1b7985e9f06a8e81baa3d9b29cbcdf3b2eb3689d79189"
],
"game": "ongeki",
"inactive": false,
"playtype": "Single",
"tableID": "ongeki-Single-brightMemory3Omni-difficulties",
"title": "O.N.G.E.K.I. (bright MEMORY Act.3 Omnimix) (Difficulties)"
},
{
"default": false,
"description": "Levels for O.N.G.E.K.I. in bright MEMORY Act.3.",
"folders": [
"Fad5e5eb714f0e8d039f533564c92cb95990ca1f5e864ff8cb4766032283e2de2",
"F3cddb7852bcd011e080ecbf8636d9b00240d2f4d9c1859933fefc949e2776b0e",
"F397987bca64f279caecdebbb8a2a61673d2a93b39463687ebe6423c05260712f",
"Fd3889e704d3acfaf6bb2ae6a742a313484ee4b3ecb62dd762448ef6b1871b7ed",
"F0efbcbb1855f01aa38df74843fcccfdd4ddcd25a761ce3deaf855ce5d091dd2f",
"F90ff22aeff14e65c5fe05d10cd195e781a54e491b46b9c5cc800b2da610f1017",
"Fe0f86791caa1f387010c773ecabc1d9f5a9089529605d6c78dfef8f5a6a9de8f",
"Fbc73d737f1a345fac411dcd40a3d1a8cef44d4a58c6a9c6cf906b728bb3e5fbb",
"F68790dcd063be0e97a4be28792f0df2b18404e5766e96262b4ad6602c2906d1f",
"F32051d649f84cab8153ccbf0d470e59d8db3ddf597e0626079503a40474c4590",
"Fbb86790e40e74657fdb8c09fc4bc0734d2ff1324d0947fac7c0765b475282e2e",
"Fa0e81b5ea91c4d7dfe5cea5737219d3c0f951c2d946244af2f5a0a8a8364f6b9",
"F5b62e48240db08753dd1231943d9b74153f36a9efb5654c6eec3e769a51cf764",
"Faa63073e3a3316175b6f87aa8eb6ac87f54284aa19d10671479811adf1776d56",
"Fbec6b88a363b3f928a1eafe16c3ad2328b31c98bb8187b6736dba6e6db985806",
"F60644ce31e027d968a123f6fe62e3af7c5de74049703dbb04429417a54cbc60f",
"F93b8b04bced6a044e569b2a76c3aaec19ea1e3cbba5d30a5f1d7e4bda76f727e",
"F305c511b904f21ff3741127b35fbd6f90e27b7cbeda76445423be58609671aad",
"Feaec120e8f6f1814a3f7500d7d0658310a6facf0ebafde55aadee9dbe530e6c8",
"F7d37163f40263b6fb60f41632b13fbdb472d0814f683e8ef8430ccdaf2a8b8cf",
"F6bef8e774b33e452a515158e5d830dba5a29a535a969b3b3a20a105c79b51646",
"Fab36c11cc2fc91225bddc1eb2608fdcd0d229975d714045e6da22453cfbcce90",
"Fc4be7c884f942a7233b31261828f155290f86d3f7d9e333964004ffd7d6cae22",
"F611bba57563b06bf96e3a110a7ad542817ebb5b46049c6a7ef29c4fe3c61259b",
"F798dd21ed158a1bbd9113e1fe46672e708e233281ac77596be4d6431a5cd5005"
],
"game": "ongeki",
"inactive": false,
"playtype": "Single",
"tableID": "ongeki-Single-brightMemory3-levels",
"title": "O.N.G.E.K.I. (bright MEMORY Act.3)"
},
{
"default": false,
"description": "Difficulties for O.N.G.E.K.I. in bright MEMORY Act.3.",
"folders": [
"F73070f7c2edc7e037775d9965a151dfb6d55df53a68211df7805784d063427b6",
"F21adc25f54a6ce919f75a18f6575569b0869a4762f776f9a844945222c1b8ecd",
"F222a2f1516fd944711e27feb6de3c2502cc21324a1cc5d1af6c6a562d7bf78bf",
"Fc783b12bc69fe0f7ee6ccecfa14a329e25aa8e7274c8696ee9d9f9dc61f11992",
"F095c535be64825c713bebc26d34a0ac8449d487bee1fed3f7df37c16f30b37e4",
"Fddd5da8078803f869db6dc4e92220c3291a1163f25f375d32525160aed09dc8e"
],
"game": "ongeki",
"inactive": false,
"playtype": "Single",
"tableID": "ongeki-Single-brightMemory3-difficulties",
"title": "O.N.G.E.K.I. (bright MEMORY Act.3) (Difficulties)"
},
{
"default": true,
"description": "The Insane PMS Table. This table starts around pop'n level 46.",
+2 -1
View File
@@ -34,6 +34,7 @@
"monk": "^7.3.4",
"node-fetch": "2.6.7",
"prudence": "0.10.0",
"cheerio": "^1.0.0-rc.12",
"tachi-common": "workspace:../../common"
}
}
}
@@ -0,0 +1,176 @@
import { Command } from "commander";
import { CreateChartID, ReadCollection, WriteCollection } from "../../util";
import { ChartDocument, Difficulties, SongDocument } from "tachi-common";
import * as cheerio from "cheerio";
const SXGA_DATA_URL = "https://ongeki.sxga.jp/assets/json/music/music.json".replace("x", "e");
const CURRENT_VERSION = "brightMemory3";
const CURRENT_VERSION_PRETTY = "bright MEMORY Act.3";
type OngekiChart = ChartDocument<"ongeki:Single">;
type OngekiSong = SongDocument<"ongeki">;
interface SxgaEntry {
date: string;
title: string;
artist: string;
category: string;
lunatic: "" | "1";
}
// Wikiwiki really sucks
const unnormalizeTitle = (title: string): string =>
title
.replace("...", "…")
.replace(/:/gu, ":")
.replace(/\(/gu, "(")
.replace(/\)/gu, ")")
.replace(/!/gu, "!")
.replace(/\?/gu, "?")
.replace(/~/gu, "~");
const scrapeWikiwiki = async (
charts: OngekiChart[],
songID: number,
title: string,
lunatic: boolean,
unnormalized = false
) => {
const res = await fetch(`https://wikiwiki.jp/gameongeki/${title.replace(/ /gu, "%20")}`);
const $ = cheerio.load(await res.text());
const tables = $("#content tbody").toArray();
if (tables.length < 2) {
if (unnormalized) {
throw new Error(`${title}: Invalid wikipage: expected 2 tables, got ${tables.length}`);
} else {
return scrapeWikiwiki(charts, songID, unnormalizeTitle(title), lunatic, true);
}
}
const diffTable = tables![1];
const diffRows = $(diffTable).find("tr").toArray();
const diffsToParse: Difficulties["ongeki:Single"][] = lunatic
? ["LUNATIC"]
: ["BASIC", "ADVANCED", "EXPERT", "MASTER"];
for (const diff of diffsToParse) {
const row = diffRows.find((row) => $(row.firstChild!).html() === diff);
const level = $(row?.children[1]).html();
const noteCountRaw = $(row?.children[2]).html();
const bellCountRaw = $(row?.children[3]).html();
const internalLevelRaw = $(row?.children[4]).html();
let noteCount: number | undefined;
let bellCount: number | undefined;
let internalLevel: number;
if (!level) {
throw new Error(`${title} ${diff}: Invalid wikipage: ${diff} has no level`);
}
if (!noteCountRaw) {
if (diff === "MASTER" || diff === "LUNATIC") {
// This value is necessary for platinum delta calculation
throw new Error(`${title} ${diff}: unknown note count`);
} else {
console.log(`warn: ${title} ${diff}: unknown note count`);
}
} else {
noteCount = parseInt(noteCountRaw, 10);
}
if (!bellCountRaw) {
console.log(`warn: ${title} ${diff}: unknown bell count`);
} else {
bellCount = parseInt(bellCountRaw, 10);
}
if (!internalLevelRaw) {
console.log(`warn: ${title} ${diff}: unknown internal level (will be deduced)`);
internalLevel = parseInt(level, 10);
if (level.endsWith("+")) {
internalLevel += 0.7;
}
} else {
internalLevel = parseFloat(internalLevelRaw);
}
const newChart: OngekiChart = {
difficulty: diff,
chartID: CreateChartID(),
songID,
level,
levelNum: internalLevel,
isPrimary: true,
playtype: "Single",
data: {
displayVersion: CURRENT_VERSION_PRETTY,
totalBellCount: bellCount,
totalNoteCount: noteCount,
isHot: true,
},
versions: [CURRENT_VERSION, `${CURRENT_VERSION}Omni`],
};
charts.push(newChart);
}
};
const main = async ({ date }) => {
const charts: OngekiChart[] = ReadCollection("charts-ongeki.json");
const songs: OngekiSong[] = ReadCollection("songs-ongeki.json");
const sxgaEntriesRaw = await fetch(SXGA_DATA_URL);
const sxgaEntries: SxgaEntry[] = (await sxgaEntriesRaw.json()).filter(
(o: SxgaEntry) => o.date >= date
);
for (const entry of sxgaEntries) {
const queriedDiff = entry.lunatic ? "LUNATIC" : "MASTER";
const song = songs.find((song) => song.title === entry.title);
console.log(`${entry.title} ${queriedDiff}`);
if (song) {
const chart = charts.find(
(chart) => chart.songID === song.id && chart.difficulty === queriedDiff
);
if (chart) {
console.log(`\tskipping`);
} else {
console.log(`\t\x1b[34mnew chart\x1b[0m`);
scrapeWikiwiki(charts, song.id, entry.title, entry.lunatic ? true : false);
}
} else {
console.log(`\t\x1b[31mnew song\x1b[0m`);
songs.push({
title: entry.title,
artist: entry.artist,
id: songs[songs.length - 1]!.id + 1,
searchTerms: [],
altTitles: [],
data: {
genre: entry.category,
},
});
scrapeWikiwiki(
charts,
songs[songs.length - 1]!.id,
entry.title,
entry.lunatic ? true : false
);
}
console.log("");
}
WriteCollection("songs-ongeki.json", songs);
WriteCollection("charts-ongeki.json", charts);
};
new Command()
.name("Fetch Ongeki")
.description("Fetch new Ongeki charts.")
.requiredOption("--date <date>", "Start date as YYYYMMDD, e.g. 20231228")
.action(main)
.parse();
@@ -0,0 +1,95 @@
# O.N.G.E.K.I. Support
This game has the internal GPTString of `ongeki:Single`.
!!! 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 | Known in-game as 'Technical Score'. It ranges between 0 and 1,010,000, where notes are worth 950,000, and bells 60,000. |
| `noteLamp` | "LOSS", "CLEAR", "FULL COMBO", "ALL BREAK" | The first lamp. A clear is either a draw or a win. |
| `bellLamp` | "FULL BELL" | The second lamp that tracks whether all bells in the chart have been collected. |
### Derived Metrics
| Metric Name | Type | Description |
| :: | :: | :: |
| `grade` | "D", "C", "B", "BB", "BBB", "A", "AA", "AAA", "S", "SS", "SSS", "SSS+" | The grade this score was. |
### Optional Metrics
| Metric Name | Type | Description |
| :: | :: | :: |
| `fast` | Integer | The number of mistakes in this score that were a result of hitting early. |
| `slow` | Integer | The number of mistakes in this score that were a result of hitting late. |
| `maxCombo` | Integer | The largest combo in this score. |
| `bellCount` | Integer | The numer of bells collected. |
| `damage` | Integer | The number of damage ticks received. |
| `platScore` | Integer | The Platinum Score value. Only exists in MASTER and LUNATIC charts. |
## Judgements
The folowing judgements are defined:
- `cbreak`
- `break`
- `hit`
- `miss`
Note: `pbreak`s are not tracked separately but can be deduced from `platScore`.
## Rating Algorithms
### Score Rating Algorithms
| Name | Description |
| :: | :: |
| `rating` | The rating value of this score. This is identical to the system used in game. |
### Session Rating Algorithms
| Name | Description |
| :: | :: |
| `naiveRating` | The average of your best 10 ratings this session. |
### Profile Rating Algorithms
| Name | Description |
| :: | :: |
| `naiveRating` | The average of your best 45 ratings. |
## Difficulties
- `BASIC`
- `ADVANCED`
- `EXPERT`
- `MASTER`
- `LUNATIC`
Note: since bright MEMORY Act.3, "crazy" charts and "Re:MASTER" charts are in separate folders, but both ought to be tracked as LUNATIC.
## Classes
| Name | Type | Values |
| :: | :: | :: |
| `colour` | DERIVED | BLUE, GREEN, ORANGE, RED, PURPLE, COPPER, SILVER, GOLD, PLATINUM, RAINBOW |
## Versions
| ID | Pretty Name |
| :: | :: |
| `brightMemory2` | bright MEMORY Act.2 |
| `brightMemory3` | bright MEMORY Act.3 |
## Supported Match Types
- `songTitle`
- `tachiSongID`
+1
View File
@@ -71,6 +71,7 @@ nav:
- "game-support/games/pms-Controller.md"
- "game-support/games/pms-Keyboard.md"
- "game-support/games/itg-Stamina.md"
- "game-support/games/ongeki-Single.md"
- Wiki:
- "wiki/index.md"
+93 -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.4
version: 0.5.4
specifier: 0.5.6
version: 0.5.6
sync-fetch:
specifier: ^0.3.1
version: 0.3.1
@@ -386,6 +386,9 @@ importers:
chalk:
specifier: '4'
version: 4.1.2
cheerio:
specifier: ^1.0.0-rc.12
version: 1.0.0-rc.12
commander:
specifier: ^8.2.0
version: 8.3.0
@@ -656,8 +659,8 @@ importers:
specifier: 3.1.2
version: 3.1.2
rg-stats:
specifier: 0.5.4
version: 0.5.4
specifier: 0.5.6
version: 0.5.6
rimraf:
specifier: 3.0.2
version: 3.0.2
@@ -4649,6 +4652,30 @@ packages:
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
dev: false
/cheerio-select@2.1.0:
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
dependencies:
boolbase: 1.0.0
css-select: 5.1.0
css-what: 6.1.0
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.1.0
dev: false
/cheerio@1.0.0-rc.12:
resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==}
engines: {node: '>= 6'}
dependencies:
cheerio-select: 2.1.0
dom-serializer: 2.0.0
domhandler: 5.0.3
domutils: 3.1.0
htmlparser2: 8.0.2
parse5: 7.1.2
parse5-htmlparser2-tree-adapter: 7.0.0
dev: false
/chokidar@3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
engines: {node: '>= 8.10.0'}
@@ -5031,6 +5058,16 @@ packages:
nth-check: 2.1.1
dev: false
/css-select@5.1.0:
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
dependencies:
boolbase: 1.0.0
css-what: 6.1.0
domhandler: 5.0.3
domutils: 3.1.0
nth-check: 2.1.1
dev: false
/css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
@@ -5377,6 +5414,14 @@ packages:
entities: 2.2.0
dev: false
/dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
dev: false
/domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
dev: false
@@ -5388,6 +5433,13 @@ packages:
domelementtype: 2.3.0
dev: false
/domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dependencies:
domelementtype: 2.3.0
dev: false
/domutils@2.8.0:
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
dependencies:
@@ -5396,6 +5448,14 @@ packages:
domhandler: 4.3.1
dev: false
/domutils@3.1.0:
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
dev: false
/dot-case@3.0.4:
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
dependencies:
@@ -5523,6 +5583,11 @@ packages:
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
dev: false
/entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
dev: false
/env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
@@ -7047,6 +7112,15 @@ packages:
terser: 5.16.1
dev: false
/htmlparser2@8.0.2:
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.1.0
entities: 4.5.0
dev: false
/http-cache-semantics@4.1.1:
resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
dev: true
@@ -9215,6 +9289,19 @@ packages:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
/parse5-htmlparser2-tree-adapter@7.0.0:
resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==}
dependencies:
domhandler: 5.0.3
parse5: 7.1.2
dev: false
/parse5@7.1.2:
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
dependencies:
entities: 4.5.0
dev: false
/parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
@@ -10084,8 +10171,8 @@ packages:
resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
dev: false
/rg-stats@0.5.4:
resolution: {integrity: sha512-eatjAvoh7sXH1lvRhUlPLmSMkr1qUwHmZKroZp3HsLDafgxyqQB/UklkXWp7/+7X35CB/tPr/psMDuiT1ybZ6w==}
/rg-stats@0.5.6:
resolution: {integrity: sha512-4YG942ztdaXd27KUABxiQAA3aJuLvLcvjdLfoxKqn89P7967bfyG5Z5OWaAg9Sr8pvhsABA4bHZuBAKtSW90xA==}
dev: false
/rimraf@2.4.5:
+1 -1
View File
@@ -5,7 +5,7 @@ test-env:
- MONGO_URL=127.0.0.1
- REDIS_URL=127.0.0.1
- TCHIS_CONF_LOCATION=test.conf.json5
- LOG_LEVEL=verbose
- LOG_LEVEL=info
# On a local machine, these tests run in under 2-3 minutes.
# On github actions, these tests can take an inordinate amount of time.
# The timeout is set to 10 minutes, which is ridiculous.
+1 -1
View File
@@ -93,7 +93,7 @@
"prudence": "0.10.0",
"rate-limit-redis": "2.1.0",
"redis": "3.1.2",
"rg-stats": "0.5.4",
"rg-stats": "0.5.6",
"rimraf": "3.0.2",
"safe-json-stringify": "1.2.0",
"semver": "^7.3.7",
@@ -8,6 +8,7 @@ import { JUBEAT_IMPL } from "./games/jubeat";
import { MAIMAI_IMPL } from "./games/maimai";
import { MAIMAIDX_IMPL } from "./games/maimaidx";
import { MUSECA_IMPL } from "./games/museca";
import { ONGEKI_IMPL } from "./games/ongeki";
import { POPN_9B_IMPL } from "./games/popn";
import { SDVX_IMPL } from "./games/sdvx";
import { USC_CONTROLLER_IMPL, USC_KEYBOARD_IMPL } from "./games/usc";
@@ -43,4 +44,5 @@ export const GPT_SERVER_IMPLEMENTATIONS: GPTImplementations = {
"usc:Keyboard": USC_KEYBOARD_IMPL,
"sdvx:Single": SDVX_IMPL,
"arcaea:Touch": ARCAEA_IMPL,
"ongeki:Single": ONGEKI_IMPL,
};
@@ -0,0 +1,182 @@
import { ONGEKI_IMPL } from "./ongeki";
import db from "external/mongo/db";
import CreateLogCtx from "lib/logger/logger";
import { CreatePBDoc } from "lib/score-import/framework/pb/create-pb-doc";
import { ONGEKI_BELL_LAMPS, ONGEKI_GRADES, ONGEKI_NOTE_LAMPS } from "tachi-common";
import t from "tap";
import { dmf, mkMockPB, mkMockScore } from "test-utils/misc";
import ResetDBState from "test-utils/resets";
import { TestingOngekiChart } from "test-utils/test-data";
import type { ProvidedMetrics, ScoreData } from "tachi-common";
const baseMetrics: ProvidedMetrics["ongeki:Single"] = {
noteLamp: "CLEAR",
bellLamp: "FULL BELL",
score: 1_001_500,
};
const scoreData: ScoreData<"ongeki:Single"> = {
noteLamp: "CLEAR",
bellLamp: "FULL BELL",
score: 1_001_500,
grade: "SSS",
judgements: {},
optional: { enumIndexes: {}, bellCount: 100, platScore: 1000 },
enumIndexes: {
grade: ONGEKI_GRADES.SSS,
noteLamp: ONGEKI_NOTE_LAMPS.CLEAR,
bellLamp: ONGEKI_BELL_LAMPS.FULL_BELL,
},
};
const mockScore = mkMockScore("ongeki", "Single", TestingOngekiChart, scoreData);
const mockPB = mkMockPB("ongeki", "Single", TestingOngekiChart, scoreData);
const logger = CreateLogCtx(__filename);
t.test("ONGEKI Implementation", (t) => {
t.test("Grade Deriver", (t) => {
const f = (score: number, expected: any) =>
t.equal(
ONGEKI_IMPL.derivers.grade(dmf(baseMetrics, { score }), TestingOngekiChart),
expected,
`A score of ${score.toLocaleString()} should result in grade=${expected}.`
);
f(0, "D");
f(500_000, "C");
f(700_000, "B");
f(750_000, "BB");
f(800_000, "BBB");
f(850_000, "A");
f(900_000, "AA");
f(940_000, "AAA");
f(970_000, "S");
f(990_000, "SS");
f(1_000_000, "SSS");
f(1_007_500, "SSS+");
f(1_010_000, "SSS+");
t.end();
});
t.test("Rating Calc", (t) => {
t.equal(
ONGEKI_IMPL.scoreCalcs.rating(scoreData, TestingOngekiChart),
12.1,
"Basic rating check"
);
t.end();
});
t.todo("Session Calcs");
t.todo("Profile Calcs");
t.test("Colour Deriver", (t) => {
const f = (v: number | null, expected: any) =>
t.equal(
ONGEKI_IMPL.classDerivers.colour({ naiveRating: v }),
expected,
`A naiveRating of ${v} should result in ${expected}.`
);
f(null, null);
f(0, "BLUE");
f(2, "GREEN");
f(4, "ORANGE");
f(7, "RED");
f(10, "PURPLE");
f(12, "COPPER");
f(13, "SILVER");
f(14, "GOLD");
f(14.5, "PLATINUM");
f(15, "RAINBOW");
t.end();
});
t.test("Goal Formatters", (t) => {
t.test("Criteria", (t) => {
t.equal(
ONGEKI_IMPL.goalCriteriaFormatters.score(1_008_182),
"Get a score of 1,008,182 on"
);
t.end();
});
t.test("Progress", (t) => {
const f = (
k: keyof typeof ONGEKI_IMPL.goalProgressFormatters,
modifant: Partial<ScoreData<"ongeki:Single">>,
goalValue: any,
expected: any
) =>
t.equal(
ONGEKI_IMPL.goalProgressFormatters[k](
dmf(mockPB, {
scoreData: modifant,
}),
goalValue
),
expected
);
f("grade", { grade: "S", score: 987_342 }, ONGEKI_GRADES.S, "SS-2.7K");
f("score", { score: 982_123 }, 1_000_000, "982,123");
f("noteLamp", { noteLamp: "CLEAR" }, ONGEKI_NOTE_LAMPS.CLEAR, "CLEAR");
t.end();
});
t.test("Out Of", (t) => {
t.equal(ONGEKI_IMPL.goalOutOfFormatters.score(1_001_003), "1,001,003");
t.equal(ONGEKI_IMPL.goalOutOfFormatters.score(983_132), "983,132");
t.end();
});
t.end();
});
t.test("PB Merging", (t) => {
t.beforeEach(ResetDBState);
t.test("Should join best lamp", async (t) => {
await db.scores.insert(mockScore);
await db.scores.insert(
dmf(mockScore, {
scoreID: "bestLamp",
scoreData: {
score: 0,
noteLamp: "FULL COMBO",
enumIndexes: { noteLamp: ONGEKI_NOTE_LAMPS.FULL_COMBO },
},
})
);
t.hasStrict(await CreatePBDoc("ongeki:Single", 1, TestingOngekiChart, logger), {
composedFrom: [
{ name: "Best Score" },
{ name: "Best Note Lamp", scoreID: "bestLamp" },
],
scoreData: {
score: mockScore.scoreData.score,
noteLamp: "FULL COMBO",
bellLamp: "FULL BELL",
enumIndexes: {
noteLamp: ONGEKI_NOTE_LAMPS.FULL_COMBO,
bellLamp: ONGEKI_BELL_LAMPS.FULL_BELL,
},
},
});
t.end();
});
t.end();
});
t.end();
});
@@ -0,0 +1,145 @@
/* eslint-disable no-confusing-arrow */
import { GoalFmtScore, GoalOutOfFmtScore, GradeGoalFormatter } from "./_common";
import { CreatePBMergeFor } from "game-implementations/utils/pb-merge";
import { ProfileAvgBestN } from "game-implementations/utils/profile-calc";
import { SessionAvgBest10For } from "game-implementations/utils/session-calc";
import { ONGEKIRating } from "rg-stats";
import { ONGEKI_GBOUNDARIES, FmtNum, GetGrade } from "tachi-common";
import { IsNullish } from "utils/misc";
import type { GPTServerImplementation } from "game-implementations/types";
import type { Difficulties } from "tachi-common";
export const OngekiPlatDiff = (diff: Difficulties["ongeki:Single"]) =>
diff === "MASTER" || diff === "LUNATIC";
export const ONGEKI_IMPL: GPTServerImplementation<"ongeki:Single"> = {
chartSpecificValidators: {
platScore: (platScore, chart) => {
if (!OngekiPlatDiff(chart.difficulty)) {
// We don't care about other difficulties
return true;
}
if (platScore < 0) {
return `Platinum Score must be non-negative. Got ${platScore}`;
}
return true;
},
bellCount: (bellCount) => {
if (bellCount < 0) {
return `Bell Count must be non-negative. Got ${bellCount}`;
}
return true;
},
totalBellCount: (bellCount) => {
if (bellCount < 0) {
return `Total bell Count must be non-negative. Got ${bellCount}`;
}
return true;
},
damage: (damage) => {
if (damage < 0) {
return `Damage must be non-negative. Got ${damage}`;
}
return true;
},
},
derivers: {
grade: ({ score }) => GetGrade(ONGEKI_GBOUNDARIES, score),
},
scoreCalcs: {
rating: (scoreData, chart) =>
chart.data.isUnranked || chart.levelNum === 0.0
? 0
: ONGEKIRating.calculate(scoreData.score, chart.levelNum),
},
sessionCalcs: { naiveRating: SessionAvgBest10For("rating") },
profileCalcs: {
naiveRating: ProfileAvgBestN("rating", 45),
},
classDerivers: {
colour: (ratings) => {
const rating = ratings.naiveRating;
if (IsNullish(rating)) {
return null;
}
if (rating >= 15) {
return "RAINBOW";
} else if (rating >= 14.5) {
return "PLATINUM";
} else if (rating >= 14) {
return "GOLD";
} else if (rating >= 13) {
return "SILVER";
} else if (rating >= 12) {
return "COPPER";
} else if (rating >= 10) {
return "PURPLE";
} else if (rating >= 7) {
return "RED";
} else if (rating >= 4) {
return "ORANGE";
} else if (rating >= 2) {
return "GREEN";
}
return "BLUE";
},
},
goalCriteriaFormatters: {
score: GoalFmtScore,
},
goalProgressFormatters: {
grade: (pb, gradeIndex) =>
GradeGoalFormatter(
ONGEKI_GBOUNDARIES,
pb.scoreData.grade,
pb.scoreData.score,
ONGEKI_GBOUNDARIES[gradeIndex]?.name ?? "D"
),
noteLamp: (pb) => pb.scoreData.noteLamp,
bellLamp: (pb) => pb.scoreData.bellLamp,
score: (pb) => FmtNum(pb.scoreData.score),
},
goalOutOfFormatters: {
score: GoalOutOfFmtScore,
},
pbMergeFunctions: [
CreatePBMergeFor("largest", "optional.platScore", "Best Platinum Score", (base, score) => {
base.scoreData.optional.platScore = score.scoreData.optional.platScore;
}),
CreatePBMergeFor("largest", "enumIndexes.noteLamp", "Best Note Lamp", (base, score) => {
base.scoreData.noteLamp = score.scoreData.noteLamp;
}),
CreatePBMergeFor("largest", "enumIndexes.bellLamp", "Best Bell Lamp", (base, score) => {
base.scoreData.bellLamp = score.scoreData.bellLamp;
}),
],
defaultMergeRefName: "Best Score",
scoreValidators: [
(s) => {
let { hit, miss } = s.scoreData.judgements;
hit ??= 0;
miss ??= 0;
if (s.scoreData.noteLamp === "ALL BREAK") {
if (hit + miss > 0) {
return "Cannot have an ALL BREAK if not all hits were justice or better.";
}
}
if (s.scoreData.noteLamp === "FULL COMBO") {
if (miss > 0) {
return "Cannot have a FULL COMBO if the score has misses.";
}
}
},
],
};
@@ -35,6 +35,7 @@ function perToScore(goal: GoalDocument) {
return v * 10_000_000;
case "wacca":
case "chunithm":
case "ongeki":
case "museca":
return v * 1_000_000;
case "popn":
@@ -82,6 +82,7 @@ const scoreMovers: ScoreMovers = {
"pms:Controller": BMS_MV,
"pms:Keyboard": BMS_MV,
"chunithm:Single": NEUTRAL_MV,
"ongeki:Single": NEUTRAL_MV,
"usc:Controller": NEUTRAL_MV,
"usc:Keyboard": NEUTRAL_MV,
"sdvx:Single": NEUTRAL_MV,
+14
View File
@@ -1268,3 +1268,17 @@ export const TestingArcaeaSheriruthFTR: ChartDocument<"arcaea:Touch"> = {
songID: 19,
versions: ["mobile"],
};
export const TestingOngekiChart: ChartDocument<"ongeki:Single"> = {
chartID: "213796bdb6150f80ba6412ce69df1249e16c0cb0",
data: {
displayVersion: "bright MEMORY Act.3",
},
difficulty: "MASTER",
isPrimary: true,
level: "10",
levelNum: 10.5,
playtype: "Single",
songID: 19,
versions: ["brightMemory3", "brightMemory3Omni"],
};
+1
View File
@@ -50,6 +50,7 @@
"maimai",
"maimaidx",
"arcaea",
"ongeki",
],
IMPORT_TYPES: [
"file/eamusement-iidx-csv",