feat: maimai support (#925)

Co-authored-by: Olivia Simmons <38598881+justOkapi@users.noreply.github.com>
Co-authored-by: tachi-seeds <107944937+tachi-seeds@users.noreply.github.com>
Co-authored-by: beerpiss <lacvtg.a1.2023@gmail.com>
This commit is contained in:
zkldi
2023-08-03 19:33:23 +01:00
committed by GitHub
co-authored by Olivia Simmons tachi-seeds beerpiss
parent 2a9fa8e313
commit 14f8f79a15
31 changed files with 65169 additions and 73 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

+3
View File
@@ -2,6 +2,9 @@
This is the main monorepo for Tachi.
![](.github/img/tachi.png)
![](.github/img/tachi-2.png)
## What is Tachi?
Tachi is a modern, powerful, modular Rhythm Game Score Tracker.
@@ -1,4 +1,3 @@
import { ToAPIURL } from "util/api";
import { RFA } from "util/misc";
import { heySplashes } from "util/splashes";
import Divider from "components/util/Divider";
@@ -0,0 +1,31 @@
import { IsNullish } from "util/misc";
import React from "react";
import { COLOUR_SET, PBScoreDocument, ScoreDocument } from "tachi-common";
export default function MaimaiJudgementCell({
score,
}: {
score: ScoreDocument<"maimai:Single"> | PBScoreDocument<"maimai:Single">;
}) {
const judgements = score.scoreData.judgements;
if (
IsNullish(judgements.miss) ||
IsNullish(judgements.great) ||
IsNullish(judgements.good) ||
IsNullish(judgements.perfect)
) {
return <td>No Data.</td>;
}
return (
<td>
<strong>
<span style={{ color: COLOUR_SET.vibrantYellow }}>{judgements.perfect}</span>-
<span style={{ color: COLOUR_SET.pink }}>{judgements.great}</span>-
<span style={{ color: COLOUR_SET.green }}>{judgements.good}</span>-
<span style={{ color: COLOUR_SET.gray }}>{judgements.miss}</span>
</strong>
</td>
);
}
+101
View File
@@ -7,6 +7,7 @@ import JubeatJudgementCell from "components/tables/cells/JubeatJudgementCell";
import JubeatScoreCell from "components/tables/cells/JubeatScoreCell";
import JubilityCell from "components/tables/cells/JubilityCell";
import LampCell from "components/tables/cells/LampCell";
import MaimaiJudgementCell from "components/tables/cells/MaimaiJudgementCell";
import MaimaiDXJudgementCell from "components/tables/cells/MaimaiDXJudgementCell";
import MillionsScoreCell from "components/tables/cells/MillionsScoreCell";
import MusecaJudgementCell from "components/tables/cells/MusecaJudgementCell";
@@ -160,6 +161,106 @@ export const GPT_CLIENT_IMPLEMENTATIONS: GPTClientImplementations = {
),
ratingCell: ({ sc }) => <JubilityCell score={sc} />,
},
"maimai:Single": {
enumIcons: defaultEnumIcons,
enumColours: {
lamp: {
FAILED: COLOUR_SET.red,
CLEAR: COLOUR_SET.green,
"FULL COMBO": COLOUR_SET.blue,
"ALL PERFECT": COLOUR_SET.gold,
"ALL PERFECT+": COLOUR_SET.teal,
},
grade: {
F: COLOUR_SET.gray,
E: COLOUR_SET.gray,
D: COLOUR_SET.gray,
C: COLOUR_SET.red,
B: COLOUR_SET.maroon,
A: COLOUR_SET.green,
AA: COLOUR_SET.paleBlue,
AAA: COLOUR_SET.blue,
S: COLOUR_SET.gold,
"S+": COLOUR_SET.vibrantYellow,
SS: COLOUR_SET.paleOrange,
"SS+": COLOUR_SET.orange,
SSS: COLOUR_SET.teal,
"SSS+": COLOUR_SET.white,
},
},
classColours: {
dan: {
DAN_1: "warning",
DAN_2: "warning",
DAN_3: "warning",
DAN_4: "warning",
DAN_5: "warning",
DAN_6: "warning",
DAN_7: "warning",
DAN_8: "warning",
DAN_9: "warning",
DAN_10: "warning",
KAIDEN: "warning",
SHINDAN_1: bg("purple"),
SHINDAN_2: bg("purple"),
SHINDAN_3: bg("purple"),
SHINDAN_4: bg("purple"),
SHINDAN_5: bg("purple"),
SHINDAN_6: bg("purple"),
SHINDAN_7: bg("purple"),
SHINDAN_8: bg("purple"),
SHINDAN_9: bg("purple"),
SHINDAN_10: bg("purple"),
SHINKAIDEN: bg("purple"),
},
colour: {
WHITE: bgc("white", "black"),
BLUE: bgc("cyan", "black"),
GREEN: bg("green"),
YELLOW: bgc("yellow", "black"),
RED: bg("red"),
PURPLE: bg("purple"),
BRONZE: bg("brown"),
SILVER: bg("gray"),
GOLD: "warning",
RAINBOW: {
background:
"linear-gradient(-45deg, #f0788a, #f48fb1, #9174c2, #79bcf2, #70a173, #f7ff99, #faca7d, #ff9d80, #f0788a)",
color: "black",
},
},
},
difficultyColours: {
Easy: COLOUR_SET.blue,
Basic: COLOUR_SET.green,
Advanced: COLOUR_SET.orange,
Expert: COLOUR_SET.red,
Master: COLOUR_SET.purple,
"Re:Master": COLOUR_SET.white,
},
ratingSystems: [],
scoreHeaders: [
["Percent", "%", NumericSOV((x) => x?.scoreData.percent)],
["Judgements", "Hits", NumericSOV((x) => x?.scoreData.percent)],
["Lamp", "Lamp", NumericSOV((x) => x?.scoreData.enumIndexes.lamp)],
],
scoreCoreCells: ({ sc }) => (
<>
<ScoreCell
colour={GetEnumColour(sc, "grade")}
grade={sc.scoreData.grade}
percent={sc.scoreData.percent}
/>
<MaimaiJudgementCell score={sc} />
<LampCell lamp={sc.scoreData.lamp} colour={GetEnumColour(sc, "lamp")} />
</>
),
ratingCell: ({ sc, rating }) => <RatingCell score={sc} rating={rating} />,
},
"maimaidx:Single": {
enumIcons: defaultEnumIcons,
enumColours: {
+3
View File
@@ -6,6 +6,7 @@ import { GITADORA_CONF, GITADORA_DORA_CONF, GITADORA_GITA_CONF } from "./game-su
import { IIDX_CONF, IIDX_DP_CONF, IIDX_SP_CONF } from "./game-support/iidx";
import { ITG_CONF, ITG_STAMINA_CONF } from "./game-support/itg";
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 { PMS_CONF, PMS_CONTROLLER_CONF, PMS_KEYBOARD_CONF } from "./game-support/pms";
@@ -37,6 +38,7 @@ export const GAME_CONFIGS = {
bms: BMS_CONF,
gitadora: GITADORA_CONF,
jubeat: JUBEAT_CONF,
maimai: MAIMAI_CONF,
maimaidx: MAIMAI_DX_CONF,
popn: POPN_CONF,
sdvx: SDVX_CONF,
@@ -83,6 +85,7 @@ export const GAME_PT_CONFIGS = {
"wacca:Single": WACCA_SINGLE_CONF,
"jubeat:Single": JUBEAT_SINGLE_CONF,
"popn:9B": POPN_9B_CONF,
"maimai:Single": MAIMAI_SINGLE_CONF,
"maimaidx:Single": MAIMAI_DX_SINGLE_CONF,
"pms:Controller": PMS_CONTROLLER_CONF,
"pms:Keyboard": PMS_KEYBOARD_CONF,
+168
View File
@@ -0,0 +1,168 @@
import { FAST_SLOW_MAXCOMBO } from "./_common";
import { FmtPercent } from "../../utils/util";
import { ClassValue, ToDecimalPlaces } from "../config-utils";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
export const MAIMAI_CONF = {
name: "maimai",
playtypes: ["Single"],
songData: z.strictObject({
titleJP: z.string(),
artistJP: z.string(),
displayVersion: z.string(),
}),
} as const satisfies INTERNAL_GAME_CONFIG;
const MaimaiDans = [
ClassValue("DAN_1", "初段", "1st Dan"),
ClassValue("DAN_2", "二段", "2nd Dan"),
ClassValue("DAN_3", "三段", "3rd Dan"),
ClassValue("DAN_4", "四段", "4th Dan"),
ClassValue("DAN_5", "五段", "5th Dan"),
ClassValue("DAN_6", "六段", "6th Dan"),
ClassValue("DAN_7", "七段", "7th Dan"),
ClassValue("DAN_8", "八段", "8th Dan"),
ClassValue("DAN_9", "九段", "9th Dan"),
ClassValue("DAN_10", "十段", "10th Dan"),
ClassValue("KAIDEN", "皆伝", "Kaiden"),
ClassValue("SHINDAN_1", "真初段", "Shinshodan"),
ClassValue("SHINDAN_2", "真二段", "2nd Shindan"),
ClassValue("SHINDAN_3", "真三段", "3rd Shindan"),
ClassValue("SHINDAN_4", "真四段", "4th Shindan"),
ClassValue("SHINDAN_5", "真五段", "5th Shindan"),
ClassValue("SHINDAN_6", "真六段", "6th Shindan"),
ClassValue("SHINDAN_7", "真七段", "7th Shindan"),
ClassValue("SHINDAN_8", "真八段", "8th Shindan"),
ClassValue("SHINDAN_9", "真九段", "9th Shindan"),
ClassValue("SHINDAN_10", "真十段", "10th Shindan"),
ClassValue("SHINKAIDEN", "真皆伝", "Shinkaiden"),
];
const MaimaiColours = [
ClassValue("WHITE", "White"),
ClassValue("BLUE", "Blue"),
ClassValue("GREEN", "Green"),
ClassValue("YELLOW", "Yellow"),
ClassValue("RED", "Red"),
ClassValue("PURPLE", "Purple"),
ClassValue("BRONZE", "Bronze"),
ClassValue("SILVER", "Silver"),
ClassValue("GOLD", "Gold"),
ClassValue("RAINBOW", "Rainbow"),
];
export const MAIMAI_SINGLE_CONF = {
providedMetrics: {
percent: {
type: "DECIMAL",
chartDependentMax: true,
formatter: FmtPercent,
description:
"The percent this score was worth. Sometimes called 'rate' in game. This is upper-bounded by how many BREAK notes the chart has.",
},
lamp: {
type: "ENUM",
values: ["FAILED", "CLEAR", "FULL COMBO", "ALL PERFECT", "ALL PERFECT+"],
minimumRelevantValue: "CLEAR",
description: "The type of clear this score was.",
},
},
derivedMetrics: {
grade: {
type: "ENUM",
values: [
"F",
"E",
"D",
"C",
"B",
"A",
"AA",
"AAA",
"S",
"S+",
"SS",
"SS+",
"SSS",
"SSS+",
],
minimumRelevantValue: "A",
description: "The grade this score was.",
},
},
defaultMetric: "percent",
preferredDefaultEnum: "grade",
optionalMetrics: FAST_SLOW_MAXCOMBO,
scoreRatingAlgs: {
rate: { description: "Rating as it's implemented in game.", formatter: ToDecimalPlaces(2) },
},
sessionRatingAlgs: {
rate: {
description: "The average of your best 10 ratings this session.",
formatter: ToDecimalPlaces(2),
},
},
profileRatingAlgs: {
naiveRate: {
description:
"An average of your 30 best ratings. This is different from the rating in-game, as that is song-based and takes into account recent scores.",
formatter: ToDecimalPlaces(2),
},
},
defaultScoreRatingAlg: "rate",
defaultSessionRatingAlg: "rate",
defaultProfileRatingAlg: "naiveRate",
// Similar to CHUNITHM, this game has a dynamic set of difficulties,
// with a song able to have as many UTAGE difficulties as it likes.
// This will also not be implemented for the same reasons as CHUNITHM.
// Sorry!
difficulties: {
type: "FIXED",
order: ["Easy", "Basic", "Advanced", "Expert", "Master", "Re:Master"],
shorthand: {
Easy: "ESY",
Basic: "BAS",
Advanced: "ADV",
Expert: "EXP",
Master: "MAS",
"Re:Master": "Re:MAS",
},
default: "Master",
},
classes: {
colour: {
type: "DERIVED",
values: MaimaiColours,
},
dan: {
type: "PROVIDED",
values: MaimaiDans,
},
},
orderedJudgements: ["perfect", "great", "good", "miss"],
versions: {
finale: "FiNALE",
},
chartData: z.strictObject({
inGameID: z.number(),
inGameStrID: z.string(),
maxPercent: z.number(),
}),
preferences: z.strictObject({}),
scoreMeta: z.strictObject({}),
supportedMatchTypes: ["songTitle", "tachiSongID", "inGameID"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
+8
View File
@@ -42,6 +42,14 @@ export enum DDR_LAMPS {
MARVELOUS_FULL_COMBO = 6,
}
export enum MAIMAI_LAMPS {
FAILED = 0,
CLEAR = 1,
FULL_COMBO = 2,
ALL_PERFECT = 3,
ALL_PERFECT_PLUS = 4,
}
export enum MAIMAIDX_LAMPS {
FAILED = 0,
CLEAR = 1,
+26 -4
View File
@@ -132,12 +132,34 @@ export const MUSECA_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"museca:Singl
傑G: 1_000_000,
});
/**
* @note - The SSS+ rank boundary is chart-dependent - it is the maximum percent of a chart.
* 104 is just the upper limit of the maximum percent.
*/
export const MAIMAI_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"maimai:Single", "grade">>({
F: 0,
E: 10,
D: 20,
C: 40,
B: 60,
A: 80,
AA: 90,
AAA: 94,
S: 97,
"S+": 98,
SS: 99,
"SS+": 99.5,
SSS: 100,
// The actual SSS+ boundary is chart-dependent!
"SSS+": 104,
});
export const MAIMAIDX_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"maimaidx:Single", "grade">>({
D: 0,
C: 10,
B: 20,
BB: 40,
BBB: 60,
C: 50,
B: 60,
BB: 70,
BBB: 75,
A: 80,
AA: 90,
AAA: 94,
File diff suppressed because it is too large Load Diff
+299
View File
@@ -28110,6 +28110,305 @@
"title": "Level 1 (FESTiVAL PLUS)",
"type": "charts"
},
{
"data": {
"level": "1",
"versions": "finale"
},
"folderID": "F1a0c0e32b477738519b1a69e167b4c6f87fa5ffd704b2a47759b414a7da4b8c4",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 1 (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "10",
"versions": "finale"
},
"folderID": "F15a62cbb7b4af62198f1048d315d4f784d0f9943c967ebee2e8207b376513668",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 10 (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "10+",
"versions": "finale"
},
"folderID": "Fb26affb9911425f64b06ee78ae717f52ca2b19d06ffd26a7c1cbe90de8cea7e0",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 10+ (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "11",
"versions": "finale"
},
"folderID": "F5b7589434609a8a5117dbc71c421e9c6b8588926cf0ca01eec1cbb8f2792aa49",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 11 (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "11+",
"versions": "finale"
},
"folderID": "F77b0b508a335d4ad32c351690364f6ec75615499660c033ce33103b489802cf8",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 11+ (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "12",
"versions": "finale"
},
"folderID": "Fae9a5d9ce589b0286a41cebf212aafba4d209e22ff8d6874717c10b743f45a36",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 12 (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "12+",
"versions": "finale"
},
"folderID": "F6467544964f67cadfbb688470493baece6a97cdda1d0108949a807436ffe9f72",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 12+ (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "13",
"versions": "finale"
},
"folderID": "F5fd3fe0b33f6d9473c16f970a4a4a7b775c1ef833ad346d230cc76ec105cb6bd",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 13 (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "13+",
"versions": "finale"
},
"folderID": "F6b5f9be654a31e6eb13f4f5d4fbb252392d02b0dbdd163e736f37dcd3a37ba77",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 13+ (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "14",
"versions": "finale"
},
"folderID": "F506c71630d7208d67522261012fdc3ab253ddaa3b5c9ba4e31a5c6fe88a7ffb9",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 14 (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "2",
"versions": "finale"
},
"folderID": "Fae54b2e40ea8e8a3cab7f007a516f6c4dbbb0eda8a6d211d02e1b8cce5cf1ff9",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 2 (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "3",
"versions": "finale"
},
"folderID": "Fc0ffd85c2959bd4f39703ca5df640bc55ef375de5c5633c2724ee9b3b22cd86d",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 3 (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "4",
"versions": "finale"
},
"folderID": "F4e7ac5d666f05d39cbb4d1a6ea8061241a904685f2e888b3e97388de6bc70e9a",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 4 (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "5",
"versions": "finale"
},
"folderID": "F6f79ae4e23953dbff6bfd3709360805b2652b863dedf29c7a7880157e2bc36d1",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 5 (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "5+",
"versions": "finale"
},
"folderID": "Fe789cf5db5b4153582c557a99c1b3af74dbf8aaa90afc5cb2d9faa05ec60cf03",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 5+ (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "6",
"versions": "finale"
},
"folderID": "F1c076b827e34f5f3c03c8ee2626773c1bed614687e605acf866d3f2acd2a8bb2",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 6 (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "6+",
"versions": "finale"
},
"folderID": "F017c78011efb1784e53b2c659fcaf089daede5ec2e39a26d55a3ddc53c4c5b58",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 6+ (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "7",
"versions": "finale"
},
"folderID": "Fee6ff8ec22ed89de6c619a5e0584a19f2e80eefc8ceddb15ab99a99ed6b17dab",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 7 (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "7+",
"versions": "finale"
},
"folderID": "Fbec2ce2959e8a23ae39cbba3396136617c289836e50b9a9f635ded49fe546736",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 7+ (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "8",
"versions": "finale"
},
"folderID": "Fb2baf0a296fac30b8b2270f5ca3e7c759c403e2d8c110ec424e05727c6c33d18",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 8 (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "8+",
"versions": "finale"
},
"folderID": "F85df71c7b200899ec6759b2dfee5446c7da0a0af49524b3b261c4aac34f18e09",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 8+ (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "9",
"versions": "finale"
},
"folderID": "F5534b37f2e158f7ad946f7d38b61c085b3fc6b2ddaf2cc57ae0a2693e12b803e",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 9 (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "9+",
"versions": "finale"
},
"folderID": "F8fd9946d8a24ba19f98736bded20ad6c173063a5b4a1322c004a8775851cd5cb",
"game": "maimai",
"inactive": false,
"playtype": "Single",
"searchTerms": [],
"title": "Level 9+ (FiNALE)",
"type": "charts"
},
{
"data": {
"level": "1",
File diff suppressed because it is too large Load Diff
+35 -1
View File
@@ -3155,6 +3155,40 @@
"tableID": "maimaidx-Single-festivalplus-levels",
"title": "maimai DX (FESTiVAL PLUS)"
},
{
"default": true,
"description": "Levels for maimai FiNALE.",
"folders": [
"F1a0c0e32b477738519b1a69e167b4c6f87fa5ffd704b2a47759b414a7da4b8c4",
"Fae54b2e40ea8e8a3cab7f007a516f6c4dbbb0eda8a6d211d02e1b8cce5cf1ff9",
"Fc0ffd85c2959bd4f39703ca5df640bc55ef375de5c5633c2724ee9b3b22cd86d",
"F4e7ac5d666f05d39cbb4d1a6ea8061241a904685f2e888b3e97388de6bc70e9a",
"F6f79ae4e23953dbff6bfd3709360805b2652b863dedf29c7a7880157e2bc36d1",
"Fe789cf5db5b4153582c557a99c1b3af74dbf8aaa90afc5cb2d9faa05ec60cf03",
"F1c076b827e34f5f3c03c8ee2626773c1bed614687e605acf866d3f2acd2a8bb2",
"F017c78011efb1784e53b2c659fcaf089daede5ec2e39a26d55a3ddc53c4c5b58",
"Fee6ff8ec22ed89de6c619a5e0584a19f2e80eefc8ceddb15ab99a99ed6b17dab",
"Fbec2ce2959e8a23ae39cbba3396136617c289836e50b9a9f635ded49fe546736",
"Fb2baf0a296fac30b8b2270f5ca3e7c759c403e2d8c110ec424e05727c6c33d18",
"F85df71c7b200899ec6759b2dfee5446c7da0a0af49524b3b261c4aac34f18e09",
"F5534b37f2e158f7ad946f7d38b61c085b3fc6b2ddaf2cc57ae0a2693e12b803e",
"F8fd9946d8a24ba19f98736bded20ad6c173063a5b4a1322c004a8775851cd5cb",
"F15a62cbb7b4af62198f1048d315d4f784d0f9943c967ebee2e8207b376513668",
"Fb26affb9911425f64b06ee78ae717f52ca2b19d06ffd26a7c1cbe90de8cea7e0",
"F5b7589434609a8a5117dbc71c421e9c6b8588926cf0ca01eec1cbb8f2792aa49",
"F77b0b508a335d4ad32c351690364f6ec75615499660c033ce33103b489802cf8",
"Fae9a5d9ce589b0286a41cebf212aafba4d209e22ff8d6874717c10b743f45a36",
"F6467544964f67cadfbb688470493baece6a97cdda1d0108949a807436ffe9f72",
"F5fd3fe0b33f6d9473c16f970a4a4a7b775c1ef833ad346d230cc76ec105cb6bd",
"F6b5f9be654a31e6eb13f4f5d4fbb252392d02b0dbdd163e736f37dcd3a37ba77",
"F506c71630d7208d67522261012fdc3ab253ddaa3b5c9ba4e31a5c6fe88a7ffb9"
],
"game": "maimai",
"inactive": false,
"playtype": "Single",
"tableID": "maimai-Single-finale-levels",
"title": "maimai (FiNALE)"
},
{
"default": false,
"description": "Levels for maimai DX in FESTiVAL.",
@@ -4053,4 +4087,4 @@
"tableID": "wacca-Single-reverse-levels",
"title": "WACCA Reverse Levels"
}
]
]
@@ -1,10 +1,5 @@
const logger = require("../logger");
const {
MutateCollection,
CreateFolderID,
ReadCollection,
WriteCollection,
} = require("../util");
const { MutateCollection, CreateFolderID, ReadCollection, WriteCollection } = require("../util");
const translateMap = new Map();
@@ -14,11 +9,7 @@ MutateCollection("folders.json", (folders) => {
logger.info("Updating Folders.");
for (const folder of folders) {
const newFolderID = CreateFolderID(
folder.data,
folder.game,
folder.playtype
);
const newFolderID = CreateFolderID(folder.data, folder.game, folder.playtype);
translateMap.set(folder.folderID, newFolderID);
@@ -37,9 +28,7 @@ try {
const newFolderID = translateMap.get(e);
if (!newFolderID) {
throw new Error(
`${e} doesn't exist as a folderID? Can't autofix.`
);
throw new Error(`${e} doesn't exist as a folderID? Can't autofix.`);
}
return newFolderID;
@@ -51,9 +40,6 @@ try {
return tables;
});
} catch (err) {
logger.error(
"Failed to update tables.json, reverting all auto-folder fixes.",
{ err }
);
logger.error("Failed to update tables.json, reverting all auto-folder fixes.", { err });
WriteCollection("folders.json", origFolders);
}
@@ -16,10 +16,10 @@ MutateCollection("charts-iidx.json", (charts) => {
chart.data["2dxtraSet"] === null &&
chart.difficulty !== "LEGGENDARIA" &&
chart.difficulty !== "BEGINNER"
) {
chart.versions.push("inf");
}
) {
chart.versions.push("inf");
}
}
return charts;
});
return charts;
});
@@ -17,38 +17,41 @@ const TIERS = {
text: "16F",
value: 16.0,
},
...(Object.fromEntries(
...Object.fromEntries(
[
"16弱(16EとF) 地力 記号・A〜Z",
"16弱(16EとF) 地力 あ〜わ",
"16弱(16EとF)鍵盤・つまみ",
].map(key => [
key, {
].map((key) => [
key,
{
text: "16E",
value: 16.1,
},
])
)),
...(Object.fromEntries(
),
...Object.fromEntries(
[
"16中(16CとD)地力 記号・A〜Z",
"16中(16CとD)地力 あ〜わ",
"16中(16CとD)鍵盤・つまみ",
].map(key => [
key, {
].map((key) => [
key,
{
text: "16C/D",
value: 16.3,
},
])
)),
...(Object.fromEntries(
["16強(16AとB) 地力", "16強(16AとB) 鍵盤・つまみ"].map(key => [
key, {
),
...Object.fromEntries(
["16強(16AとB) 地力", "16強(16AとB) 鍵盤・つまみ"].map((key) => [
key,
{
text: "16A/B",
value: 16.5,
},
])
)),
),
"16強+(16A以上S未満)": {
text: "16A+",
value: 16.7,
@@ -238,7 +241,7 @@ const MANUAL_TITLE_MAP = {
"VALKYRIE ASAULT": "VALKYRIE ASSAULT",
SuperMiracleEmsemble: "SuperMiracleEnsemble",
"仔羊のナヴァラン・クリシェを添えて": "~仔羊のナヴァラン・クリシェを添えて~",
"あいあむなんばーわんぱとらちゃん様": "あいあむなんばーわんパトラちゃん様", // hirigana/katakana
あいあむなんばーわんぱとらちゃん様: "あいあむなんばーわんパトラちゃん様", // hirigana/katakana
// 17s
"Emperors divide": "Emperor's Divide",
@@ -441,7 +444,9 @@ function addTiers(levelNum, csvData, headerRow, leftOffset, simple) {
const [_, title, difficulty] = chartString.match(/^(.*?)(?:\[([A-Z]{3})\])?$/);
if (
difficulty &&
!["NOV", "ADV", "EXH", "MXM", "INF", "GRV", "HVN", "VVD", "XCD"].includes(difficulty)
!["NOV", "ADV", "EXH", "MXM", "INF", "GRV", "HVN", "VVD", "XCD"].includes(
difficulty
)
) {
console.log(`Unknown difficulty ${difficulty} for ${title}.`);
}
@@ -201,7 +201,7 @@ for (const tableFile of options.charts) {
// So it doesn't give VF. (Consider changing this.)
levelNum: 0,
playtype,
songID,
tierlistInfo: {},
versions: {},
@@ -0,0 +1,88 @@
# maimai DX Support
This game has the internal GPTString of `maimai: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 |
| :: | :: | :: |
| `percent` | Decimal | The percent this score was worth. Sometimes called 'rate' in game. This is between 0 and 104 (upper bound, each chart has its own maximum percent) |
| `lamp` | "FAILED", "CLEAR", "FULL COMBO", "ALL PERFECT", "ALL PERFECT+" | The type of clear this score was. |
### Derived Metrics
| Metric Name | Type | Description |
| :: | :: | :: |
| `grade` | "D", "C", "B", "A", "AA", "AAA", "S", "S+", "SS", "SS+", "SSS", "SSS+" | The grade this score was. |
### Optional Metrics
| Metric Name | Type | Description |
| :: | :: | :: |
| `fast` | Integer | The amount of mistakes in this score that were a result of hitting early. |
| `slow` | Integer | The amount of mistakes in this score that were a result of hitting late. |
| `maxCombo` | Integer | The largest combo in this score. |
## Judgements
The folowing judgements are defined:
- `perfect`
- `great`
- `good`
- `miss`
## Rating Algorithms
### Score Rating Algorithms
| Name | Description |
| :: | :: |
| `rate` | Rating as it's implemented in game. |
### Session Rating Algorithms
| Name | Description |
| :: | :: |
| `rate` | The average of your best 10 ratings this session. |
### Profile Rating Algorithms
The default rating algorithm is `naiveRate`.
| Name | Description |
| :: | :: |
| `naiveRate` | An average of your 30 best ratings. This is different from the rating in-game, as that is song-based and takes into account recent scores. |
## Difficulties
- `Basic`
- `Advanced`
- `Expert`
- `Master`
- `Re:Master`
## Classes
| Name | Type | Values |
| :: | :: | :: |
| `colour` | DERIVED | WHITE, BLUE, GREEN, YELLOW, RED, PURPLE, BRONZE, SILVER, GOLD, RAINBOW
| `dan` | PROVIDED | DAN_1, DAN_2, DAN_3, DAN_4, DAN_5, DAN_6, DAN_7, DAN_8, DAN_9, DAN_10, KAIDEN, SHINDAN_1, SHINDAN_2, SHINDAN_3, SHINDAN_4, SHINDAN_5, SHINDAN_6, SHINDAN_7, SHINDAN_8, SHINDAN_9, SHINDAN_10, SHINKAIDEN
## Versions
| ID | Pretty Name |
| :: | :: |
| `finale` | FiNALE |
## Supported Match Types
- `songTitle`
- `tachiSongID`
+25 -24
View File
@@ -2353,48 +2353,48 @@ packages:
'@jridgewell/resolve-uri': 3.1.0
'@jridgewell/sourcemap-codec': 1.4.14
/@msgpackr-extract/msgpackr-extract-darwin-arm64/2.0.2:
resolution: {integrity: sha512-FMX5i7a+ojIguHpWbzh5MCsCouJkwf4z4ejdUY/fsgB9Vkdak4ZnoIEskOyOUMMB4lctiZFGszFQJXUeFL8tRg==}
/@msgpackr-extract/msgpackr-extract-darwin-arm64/2.2.0:
resolution: {integrity: sha512-Z9LFPzfoJi4mflGWV+rv7o7ZbMU5oAU9VmzCgL240KnqDW65Y2HFCT3MW06/ITJSnbVLacmcEJA8phywK7JinQ==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@msgpackr-extract/msgpackr-extract-darwin-x64/2.0.2:
resolution: {integrity: sha512-DznYtF3lHuZDSRaIOYeif4JgO0NtO2Xf8DsngAugMx/bUdTFbg86jDTmkVJBNmV+cxszz6OjGvinnS8AbJ342g==}
/@msgpackr-extract/msgpackr-extract-darwin-x64/2.2.0:
resolution: {integrity: sha512-vq0tT8sjZsy4JdSqmadWVw6f66UXqUCabLmUVHZwUFzMgtgoIIQjT4VVRHKvlof3P/dMCkbMJ5hB1oJ9OWHaaw==}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@msgpackr-extract/msgpackr-extract-linux-arm/2.0.2:
resolution: {integrity: sha512-Gy9+c3Wj+rUlD3YvCZTi92gs+cRX7ZQogtwq0IhRenloTTlsbpezNgk6OCkt59V4ATEWSic9rbU92H/l7XsRvA==}
/@msgpackr-extract/msgpackr-extract-linux-arm/2.2.0:
resolution: {integrity: sha512-SaJ3Qq4lX9Syd2xEo9u3qPxi/OB+5JO/ngJKK97XDpa1C587H9EWYO6KD8995DAjSinWvdHKRrCOXVUC5fvGOg==}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@msgpackr-extract/msgpackr-extract-linux-arm64/2.0.2:
resolution: {integrity: sha512-b0jMEo566YdM2K+BurSed7bswjo3a6bcdw5ETqoIfSuxKuRLPfAiOjVbZyZBgx3J/TAM/QrvEQ/VN89A0ZAxSg==}
/@msgpackr-extract/msgpackr-extract-linux-arm64/2.2.0:
resolution: {integrity: sha512-hlxxLdRmPyq16QCutUtP8Tm6RDWcyaLsRssaHROatgnkOxdleMTgetf9JsdncL8vLh7FVy/RN9i3XR5dnb9cRA==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@msgpackr-extract/msgpackr-extract-linux-x64/2.0.2:
resolution: {integrity: sha512-zrBHaePwcv4cQXxzYgNj0+A8I1uVN97E7/3LmkRocYZ+rMwUsnPpp4RuTAHSRoKlTQV3nSdCQW4Qdt4MXw/iHw==}
/@msgpackr-extract/msgpackr-extract-linux-x64/2.2.0:
resolution: {integrity: sha512-94y5PJrSOqUNcFKmOl7z319FelCLAE0rz/jPCWS+UtdMZvpa4jrQd+cJPQCLp2Fes1yAW/YUQj/Di6YVT3c3Iw==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@msgpackr-extract/msgpackr-extract-win32-x64/2.0.2:
resolution: {integrity: sha512-fpnI00dt+yO1cKx9qBXelKhPBdEgvc8ZPav1+0r09j0woYQU2N79w/jcGawSY5UGlgQ3vjaJsFHnGbGvvqdLzg==}
/@msgpackr-extract/msgpackr-extract-win32-x64/2.2.0:
resolution: {integrity: sha512-XrC0JzsqQSvOyM3t04FMLO6z5gCuhPE6k4FXuLK5xf52ZbdvcFe1yBmo7meCew9B8G2f0T9iu9t3kfTYRYROgA==}
cpu: [x64]
os: [win32]
requiresBuild: true
@@ -8019,25 +8019,26 @@ packages:
/ms/2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
/msgpackr-extract/2.0.2:
resolution: {integrity: sha512-coskCeJG2KDny23zWeu+6tNy7BLnAiOGgiwzlgdm4oeSsTpqEJJPguHIuKZcCdB7tzhZbXNYSg6jZAXkZErkJA==}
/msgpackr-extract/2.2.0:
resolution: {integrity: sha512-0YcvWSv7ZOGl9Od6Y5iJ3XnPww8O7WLcpYMDwX+PAA/uXLDtyw94PJv9GLQV/nnp3cWlDhMoyKZIQLrx33sWog==}
hasBin: true
requiresBuild: true
dependencies:
node-gyp-build-optional-packages: 5.0.2
node-gyp-build-optional-packages: 5.0.3
optionalDependencies:
'@msgpackr-extract/msgpackr-extract-darwin-arm64': 2.0.2
'@msgpackr-extract/msgpackr-extract-darwin-x64': 2.0.2
'@msgpackr-extract/msgpackr-extract-linux-arm': 2.0.2
'@msgpackr-extract/msgpackr-extract-linux-arm64': 2.0.2
'@msgpackr-extract/msgpackr-extract-linux-x64': 2.0.2
'@msgpackr-extract/msgpackr-extract-win32-x64': 2.0.2
'@msgpackr-extract/msgpackr-extract-darwin-arm64': 2.2.0
'@msgpackr-extract/msgpackr-extract-darwin-x64': 2.2.0
'@msgpackr-extract/msgpackr-extract-linux-arm': 2.2.0
'@msgpackr-extract/msgpackr-extract-linux-arm64': 2.2.0
'@msgpackr-extract/msgpackr-extract-linux-x64': 2.2.0
'@msgpackr-extract/msgpackr-extract-win32-x64': 2.2.0
dev: false
optional: true
/msgpackr/1.6.1:
resolution: {integrity: sha512-Je+xBEfdjtvA4bKaOv8iRhjC8qX2oJwpYH4f7JrG4uMVJVmnmkAT4pjKdbztKprGj3iwjcxPzb5umVZ02Qq3tA==}
optionalDependencies:
msgpackr-extract: 2.0.2
msgpackr-extract: 2.2.0
dev: false
/multer/1.4.4:
@@ -8159,8 +8160,8 @@ packages:
whatwg-url: 5.0.0
dev: false
/node-gyp-build-optional-packages/5.0.2:
resolution: {integrity: sha512-PiN4NWmlQPqvbEFcH/omQsswWQbe5Z9YK/zdB23irp5j2XibaA2IrGvpSWmVVG4qMZdmPdwPctSy4a86rOMn6g==}
/node-gyp-build-optional-packages/5.0.3:
resolution: {integrity: sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==}
hasBin: true
dev: false
optional: true
+1 -1
View File
@@ -7,7 +7,7 @@ test-env:
- TCHIS_CONF_LOCATION=test.conf.json5
# 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 16 minutes, which is ridiculous.
# The timeout is set to 10 minutes, which is ridiculous.
# Doesn't matter for real machines.
timeout: 600
ts: true
+1
View File
@@ -45,6 +45,7 @@
"jubeat",
"pms",
"gitadora",
"maimai",
"maimaidx",
"itg",
],
@@ -4,6 +4,7 @@ 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";
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 { POPN_9B_IMPL } from "./games/popn";
@@ -33,6 +34,7 @@ export const GPT_SERVER_IMPLEMENTATIONS: GPTImplementations = {
"gitadora:Gita": GITADORA_GITA_IMPL,
"itg:Stamina": ITG_STAMINA_IMPL,
"jubeat:Single": JUBEAT_IMPL,
"maimai:Single": MAIMAI_IMPL,
"maimaidx:Single": MAIMAIDX_IMPL,
"museca:Single": MUSECA_IMPL,
"popn:9B": POPN_9B_IMPL,
@@ -0,0 +1,201 @@
import { MAIMAI_IMPL } from "./maimai";
import db from "external/mongo/db";
import CreateLogCtx from "lib/logger/logger";
import { CreatePBDoc } from "lib/score-import/framework/pb/create-pb-doc";
import { MAIMAI_SINGLE_CONF } from "tachi-common/config/game-support/maimai";
import t from "tap";
import { mkMockScore, mkMockPB, dmf } from "test-utils/misc";
import ResetDBState from "test-utils/resets";
import { TestSnapshot } from "test-utils/single-process-snapshot";
import { TestingMaimaiChart } from "test-utils/test-data";
import type { ProvidedMetrics, ScoreData, integer } from "tachi-common";
const logger = CreateLogCtx(__filename);
const baseMetrics: ProvidedMetrics["maimai:Single"] = {
lamp: "CLEAR",
percent: 97.01,
};
const GRADES = MAIMAI_SINGLE_CONF.derivedMetrics.grade.values;
const LAMPS = MAIMAI_SINGLE_CONF.providedMetrics.lamp.values;
const scoreData: ScoreData<"maimai:Single"> = {
lamp: "CLEAR",
percent: 97.01,
grade: "S",
judgements: {},
optional: { enumIndexes: {} },
enumIndexes: {
grade: GRADES.indexOf("S"),
lamp: LAMPS.indexOf("CLEAR"),
},
};
const mockScore = mkMockScore("maimai", "Single", TestingMaimaiChart, scoreData);
const mockPB = mkMockPB("maimai", "Single", TestingMaimaiChart, scoreData);
t.test("maimai Implementation", (t) => {
t.test("Percent Validator", (t) => {
t.equal(MAIMAI_IMPL.chartSpecificValidators.percent(100.78, TestingMaimaiChart), true);
t.equal(MAIMAI_IMPL.chartSpecificValidators.percent(0, TestingMaimaiChart), true);
TestSnapshot(
t,
MAIMAI_IMPL.chartSpecificValidators.percent(-1, TestingMaimaiChart),
`Maimai Percent Validator: negative`
);
TestSnapshot(
t,
MAIMAI_IMPL.chartSpecificValidators.percent(101, TestingMaimaiChart),
`Maimai Percent Validator: excessive`
);
t.end();
});
t.test("Grade Deriver", (t) => {
const f = (percent: number, expected: string) =>
t.equal(
MAIMAI_IMPL.derivers.grade(dmf(baseMetrics, { percent }), TestingMaimaiChart),
expected,
`A percent of ${percent} should result in grade=${expected}.`
);
f(0, "F");
f(10, "E");
f(20, "D");
f(40, "C");
f(60, "B");
f(80, "A");
f(90, "AA");
f(94, "AAA");
f(97, "S");
f(98, "S+");
f(99, "SS");
f(99.5, "SS+");
f(100, "SSS");
f(100.78, "SSS+");
t.end();
});
t.test("Score Calcs", (t) => {
t.equal(
MAIMAI_IMPL.scoreCalcs.rate(scoreData, TestingMaimaiChart),
14.85,
"Basic rate check"
);
t.end();
});
t.todo("Session Calcs");
t.todo("Profile Calcs");
t.test("Colour Deriver", (t) => {
const f = (v: number | null, expected: string | null) =>
t.equal(
MAIMAI_IMPL.classDerivers.colour({ naiveRate: v }),
expected,
`A rate of ${v} should result in ${expected}.`
);
f(null, null);
f(0, "WHITE");
f(15, "RAINBOW");
f(14.5, "GOLD");
f(14, "SILVER");
f(13, "BRONZE");
f(12, "PURPLE");
f(10, "RED");
f(7, "YELLOW");
f(4, "GREEN");
f(2, "BLUE");
t.end();
});
t.test("Goal Formatters", (t) => {
t.test("Criteria", (t) => {
t.equal(MAIMAI_IMPL.goalCriteriaFormatters.percent(93.14), "Get 93.14% on");
t.end();
});
t.test("Progress", (t) => {
const f = (
k: keyof typeof MAIMAI_IMPL.goalProgressFormatters,
modifant: Partial<ScoreData<"maimai:Single">>,
goalValue: integer,
expected: string
) =>
t.equal(
MAIMAI_IMPL.goalProgressFormatters[k](
dmf(mockPB, {
scoreData: modifant,
}),
goalValue
),
expected
);
f("grade", { grade: "S", percent: 97.5 }, GRADES.indexOf("SS"), "(S+)-0.50%");
f("grade", { grade: "S", percent: 97.5 }, GRADES.indexOf("SSS+"), "(S+)-0.50%");
f("grade", { grade: "SSS", percent: 100.2 }, GRADES.indexOf("SSS+"), "SSS+0.20%");
f("grade", { grade: "SSS+", percent: 100.78 }, GRADES.indexOf("SSS+"), "SSS+");
f("percent", { percent: 98.23 }, 1_000_000, "98.23%");
f("lamp", { lamp: "CLEAR" }, LAMPS.indexOf("CLEAR"), "CLEAR");
t.end();
});
t.test("Out Of", (t) => {
t.equal(MAIMAI_IMPL.goalOutOfFormatters.percent(99.11), "99.11%");
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: {
percent: 0,
lamp: "FULL COMBO",
enumIndexes: { lamp: LAMPS.indexOf("FULL COMBO") },
},
})
);
t.hasStrict(await CreatePBDoc("maimai:Single", 1, TestingMaimaiChart, logger), {
composedFrom: [
{ name: "Best Percent" },
{ name: "Best Lamp", scoreID: "bestLamp" },
],
scoreData: {
percent: mockScore.scoreData.percent,
lamp: "FULL COMBO",
enumIndexes: { lamp: LAMPS.indexOf("FULL COMBO") },
},
});
t.end();
});
t.end();
});
t.end();
});
@@ -0,0 +1,155 @@
import { GoalFmtPercent, GoalOutOfFmtPercent, 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 { MaimaiRate } from "rg-stats";
import { GetGrade, MAIMAI_GBOUNDARIES } from "tachi-common";
import { IsNullish } from "utils/misc";
import type { GPTServerImplementation } from "game-implementations/types";
export const MAIMAI_IMPL: GPTServerImplementation<"maimai:Single"> = {
chartSpecificValidators: {
percent: (percent, chart) => {
if (percent < 0) {
return "Percent cannot be negative.";
}
if (percent > chart.data.maxPercent) {
return `Percent cannot be greater than ${chart.data.maxPercent} for this chart.`;
}
return true;
},
},
derivers: {
grade: ({ percent }, chart) => {
if (percent === chart.data.maxPercent) {
return "SSS+";
}
return GetGrade(MAIMAI_GBOUNDARIES, percent);
},
},
scoreCalcs: {
rate: (scoreData, chart) =>
MaimaiRate.calculate(scoreData.percent, chart.data.maxPercent, chart.levelNum),
},
sessionCalcs: { rate: SessionAvgBest10For("rate") },
profileCalcs: {
naiveRate: ProfileAvgBestN("rate", 30),
},
classDerivers: {
colour: (ratings) => {
const rate = ratings.naiveRate;
if (IsNullish(rate)) {
return null;
}
if (rate >= 15) {
return "RAINBOW";
} else if (rate >= 14.5) {
return "GOLD";
} else if (rate >= 14) {
return "SILVER";
} else if (rate >= 13) {
return "BRONZE";
} else if (rate >= 12) {
return "PURPLE";
} else if (rate >= 10) {
return "RED";
} else if (rate >= 7) {
return "YELLOW";
} else if (rate >= 4) {
return "GREEN";
} else if (rate >= 2) {
return "BLUE";
}
return "WHITE";
},
},
goalCriteriaFormatters: {
percent: GoalFmtPercent,
},
goalProgressFormatters: {
percent: (pb) => `${pb.scoreData.percent.toFixed(2)}%`,
lamp: (pb) => pb.scoreData.lamp,
grade: (pb, gradeIndex) => {
if (pb.scoreData.grade === "SSS+") {
return "SSS+";
}
// gradeIndex is guaranteed to be a valid rank
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const goalGrade = MAIMAI_GBOUNDARIES[gradeIndex]!.name;
// Grade SSS+ is chart-dependent, and it isn't possible to get the
// max score/max percent from only the percent.
//
// As such, if the goal is to SSS+, we have to return the current rank+delta.
if (goalGrade === "SSS+" && pb.scoreData.grade === "SSS") {
const boundary =
MAIMAI_GBOUNDARIES.find((c) => c.name === "SSS")?.lowerBound ?? 100;
const delta = pb.scoreData.percent - boundary;
return `SSS+${delta.toFixed(2)}%`;
}
return GradeGoalFormatter(
MAIMAI_GBOUNDARIES,
pb.scoreData.grade,
pb.scoreData.percent,
goalGrade,
(v) => `${v.toFixed(2)}%`
);
},
},
goalOutOfFormatters: {
percent: GoalOutOfFmtPercent,
},
pbMergeFunctions: [
CreatePBMergeFor("largest", "enumIndexes.lamp", "Best Lamp", (base, score) => {
base.scoreData.lamp = score.scoreData.lamp;
}),
],
defaultMergeRefName: "Best Percent",
scoreValidators: [
(s) => {
if (s.scoreData.percent > 104) {
return "Score cannot be greater than 104%.";
}
},
(s) => {
if (s.scoreData.lamp === "ALL PERFECT+" && !(s.scoreData.grade === "SSS+")) {
return "Cannot have an ALL PERFECT+ without grade SSS+.";
}
if (s.scoreData.grade === "SSS+" && !(s.scoreData.lamp === "ALL PERFECT+")) {
return "Cannot have grade SSS+ without an ALL PERFECT+";
}
},
(s) => {
let { great, good, miss } = s.scoreData.judgements;
great ??= 0;
good ??= 0;
miss ??= 0;
if (s.scoreData.lamp === "ALL PERFECT") {
// `great`, `good` and `miss` are all coalesced to 0, so they're all
// numbers, even if eslint doesn't think so.
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
if (great + good + miss > 0) {
return "Cannot have an ALL PERFECT with any non-perfect judgements.";
}
}
if (s.scoreData.lamp === "FULL COMBO") {
if (miss > 0) {
return "Cannot have a FULL COMBO if the score has misses.";
}
}
},
],
};
@@ -44,10 +44,10 @@ t.test("Maimai DX Implementation", (t) => {
);
f(0, "D");
f(10, "C");
f(20, "B");
f(40, "BB");
f(60, "BBB");
f(50, "C");
f(60, "B");
f(70, "BB");
f(75, "BBB");
f(80, "A");
f(90, "AA");
f(94, "AAA");
@@ -43,6 +43,7 @@ function perToScore(goal: GoalDocument) {
case "bms":
case "gitadora":
case "jubeat":
case "maimai":
case "maimaidx":
case "pms":
case "itg":
@@ -87,6 +87,12 @@ const scoreMovers: ScoreMovers = {
"sdvx:Single": NEUTRAL_MV,
"wacca:Single": NEUTRAL_MV,
"museca:Single": NEUTRAL_MV,
"maimai:Single": (old) => ({
percent: old.percent,
judgements: old.judgements,
lamp: old.lamp as any,
optional: old.hitMeta,
}),
"maimaidx:Single": (old) => ({
percent: old.percent,
judgements: old.judgements,
@@ -48,6 +48,8 @@
"EX Score Validator Controller: excessive": "EX Score cannot be greater than 4512 for this chart.",
"EX Score Validator Keyboard: negative": "EX Score cannot be negative.",
"EX Score Validator Keyboard: excessive": "EX Score cannot be greater than 4512 for this chart.",
"Maimai Percent Validator: negative": "Percent cannot be negative.",
"Maimai Percent Validator: excessive": "Percent cannot be greater than 100.78 for this chart.",
"WACCA IMPL Validators: Should disallow ALL MARVELOUS with non-perfect score.": [
"ALL MARVELOUS scores must have a perfect score. Got 999999 instead."
],
+16
View File
@@ -369,6 +369,22 @@ export const TestingGitadoraChart: ChartDocument<"gitadora:Dora" | "gitadora:Git
versions: ["konaste"],
};
export const TestingMaimaiChart: ChartDocument<"maimai:Single"> = {
chartID: "6e087e5f58519a2697374b9bff776a004046e952",
data: {
inGameID: 834,
inGameStrID: "pandora_paradoxxx",
maxPercent: 100.78,
},
difficulty: "Master",
isPrimary: true,
level: "13+",
levelNum: 13.9,
playtype: "Single",
songID: 834,
versions: ["finale"],
};
export const TestingMaimaiDXChart: ChartDocument<"maimaidx:Single"> = {
chartID: "fbde73d543f93cbad9ec67052a7165700c5c8df6",
data: {
+1
View File
@@ -47,6 +47,7 @@
"popn",
"jubeat",
"pms",
"maimai",
"maimaidx",
],
IMPORT_TYPES: [