mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-28 18:08:15 +03:00
Merge branch 'common-master'
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
js
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"import",
|
||||
"cadence"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:cadence/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"no-inline-comments": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"**/*.test.ts"
|
||||
],
|
||||
"rules": {
|
||||
// Our test files break these rules *all* the time, and there's
|
||||
// no point refactoring. Our tests deliberately play with the
|
||||
// dynamic nature of TS to more accurately test arbitrary input.
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
// we abuse this one for faking data, but we really shouldn't.
|
||||
"@typescript-eslint/consistent-type-assertions": "warn"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/node_modules
|
||||
/.vscode
|
||||
|
||||
.nyc_output
|
||||
coverage
|
||||
|
||||
/js
|
||||
@@ -0,0 +1,10 @@
|
||||
files: "src/**/*.test.ts"
|
||||
test-env: NODE_ENV=test
|
||||
timeout: 120 # might need to be increased
|
||||
ts: true
|
||||
check-coverage: true
|
||||
branches: 80
|
||||
functions: 80
|
||||
lines: 80
|
||||
statements: 80
|
||||
reporter: terse
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 zkldi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Tachi-common
|
||||
|
||||
Common Types (and code) for Tachi. These are shared between the client and the server implementations.
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "tachi-common",
|
||||
"version": "0.15.2",
|
||||
"description": "Common Types & Utilities for Tachi",
|
||||
"main": "js/index.js",
|
||||
"files": [
|
||||
"js/**/*"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "tap",
|
||||
"build": "tsc",
|
||||
"prepublish": "tsc",
|
||||
"lint": "eslint . --ext .ts --fix"
|
||||
},
|
||||
"author": "zkldi",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "17.0.8",
|
||||
"@types/tap": "15.0.3",
|
||||
"eslint": "8",
|
||||
"eslint-plugin-cadence": "0.0.3",
|
||||
"tap": "15.0.9",
|
||||
"ts-node": "10.0.0",
|
||||
"typescript": "4.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/express-serve-static-core": "^4.17.28",
|
||||
"@types/mongodb": "3.6.18",
|
||||
"prudence": "^0.9.8"
|
||||
},
|
||||
"nyc": {
|
||||
"reporter": [
|
||||
"html",
|
||||
"lcov"
|
||||
],
|
||||
"all": true,
|
||||
"include": [
|
||||
"src/*/!(test-utils)/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.test.ts",
|
||||
"src/external/mongo/schemas.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
Generated
+3240
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
import { GetGameConfig, GetGamePTConfig } from "./config";
|
||||
import { allSupportedGames } from "./static-config";
|
||||
import t from "tap";
|
||||
|
||||
t.test("#GetGameConfig", (t) => {
|
||||
t.plan(allSupportedGames.length);
|
||||
|
||||
for (const game of allSupportedGames) {
|
||||
t.equal(GetGameConfig(game).internalName, game);
|
||||
}
|
||||
});
|
||||
|
||||
// i have no idea why this one doesn't need t.end()
|
||||
// and this one does
|
||||
t.test("#GetGamePTConfig", (t) => {
|
||||
for (const game of allSupportedGames) {
|
||||
const gameConfig = GetGameConfig(game);
|
||||
|
||||
for (const playtype of gameConfig.validPlaytypes) {
|
||||
const conf = GetGamePTConfig(game, playtype);
|
||||
|
||||
t.equal(
|
||||
conf.idString,
|
||||
`${game}:${playtype}`,
|
||||
`Should return the right GamePTConfig (${game} ${playtype})`
|
||||
);
|
||||
|
||||
if (conf.gradeBoundaries === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
t.equal(
|
||||
conf.gradeBoundaries.length,
|
||||
conf.grades.length,
|
||||
`Should have the same amount of grades as grade boundaries. (${game} ${playtype})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
t.end();
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,174 @@
|
||||
import type {
|
||||
FileUploadImportTypes,
|
||||
IRImportTypes,
|
||||
APIImportTypes,
|
||||
ImportTypes,
|
||||
Game,
|
||||
IDStrings,
|
||||
GPTSupportedVersions,
|
||||
} from "..";
|
||||
|
||||
export const fileImportTypes: Array<FileUploadImportTypes> = [
|
||||
"file/eamusement-iidx-csv",
|
||||
"file/eamusement-sdvx-csv",
|
||||
"file/batch-manual",
|
||||
"file/solid-state-squad",
|
||||
"file/mer-iidx",
|
||||
"file/pli-iidx-csv",
|
||||
];
|
||||
|
||||
export const irImportTypes: Array<IRImportTypes> = [
|
||||
"ir/direct-manual",
|
||||
"ir/barbatos",
|
||||
"ir/fervidex",
|
||||
"ir/fervidex-static",
|
||||
"ir/beatoraja",
|
||||
"ir/usc",
|
||||
"ir/kshook-sv6c",
|
||||
];
|
||||
|
||||
export const apiImportTypes: Array<APIImportTypes> = [
|
||||
"api/arc-iidx",
|
||||
"api/arc-sdvx",
|
||||
"api/eag-iidx",
|
||||
"api/eag-sdvx",
|
||||
"api/flo-iidx",
|
||||
"api/flo-sdvx",
|
||||
"api/min-sdvx",
|
||||
];
|
||||
|
||||
export const allImportTypes: Array<ImportTypes> = [
|
||||
...fileImportTypes,
|
||||
...irImportTypes,
|
||||
...apiImportTypes,
|
||||
];
|
||||
|
||||
export const allIDStrings: Array<IDStrings> = [
|
||||
"iidx:SP",
|
||||
"bms:14K",
|
||||
"bms:7K",
|
||||
"chunithm:Single",
|
||||
"ddr:DP",
|
||||
"ddr:SP",
|
||||
"gitadora:Dora",
|
||||
"gitadora:Gita",
|
||||
"iidx:DP",
|
||||
"maimai:Single",
|
||||
"museca:Single",
|
||||
"sdvx:Single",
|
||||
"usc:Controller",
|
||||
"usc:Keyboard",
|
||||
"wacca:Single",
|
||||
"pms:Controller",
|
||||
"pms:Keyboard",
|
||||
"jubeat:Single",
|
||||
"itg:Stamina",
|
||||
];
|
||||
|
||||
export const allSupportedGames: Array<Game> = [
|
||||
"iidx",
|
||||
"museca",
|
||||
"maimai",
|
||||
"jubeat",
|
||||
"popn",
|
||||
"sdvx",
|
||||
"ddr",
|
||||
"bms",
|
||||
"chunithm",
|
||||
"gitadora",
|
||||
"usc",
|
||||
"wacca",
|
||||
"pms",
|
||||
"itg",
|
||||
];
|
||||
|
||||
export type Versions = {
|
||||
[I in IDStrings]: {
|
||||
[K in GPTSupportedVersions[I]]: string;
|
||||
};
|
||||
};
|
||||
|
||||
const prettyIIDXVersions: Versions["iidx:SP"] = {
|
||||
20: "tricoro",
|
||||
21: "SPADA",
|
||||
22: "PENDUAL",
|
||||
23: "copula",
|
||||
24: "SINOBUZ",
|
||||
25: "CANNON BALLERS",
|
||||
26: "ROOTAGE",
|
||||
27: "HEROIC VERSE",
|
||||
28: "BISTROVER",
|
||||
29: "CastHour",
|
||||
"10-cs": "10th Style CS",
|
||||
"11-cs": "IIDX RED CS",
|
||||
"12-cs": "HAPPY SKY CS",
|
||||
"13-cs": "DISTORTED CS",
|
||||
"14-cs": "GOLD CS",
|
||||
"15-cs": "DJ TROOPERS CS",
|
||||
"16-cs": "EMPRESS CS",
|
||||
"26-omni": "ROOTAGE Omnimix",
|
||||
"3-cs": "3rd Style CS",
|
||||
"4-cs": "4th Style CS",
|
||||
"5-cs": "5th Style CS",
|
||||
"6-cs": "6th Style CS",
|
||||
"7-cs": "7th Style CS",
|
||||
"8-cs": "8th Style CS",
|
||||
"9-cs": "9th Style CS",
|
||||
"27-omni": "HEROIC VERSE Omnimix",
|
||||
"28-omni": "BISTROVER Omnimix",
|
||||
"27-2dxtra": "HEROIC VERSE 2dxtra",
|
||||
"28-2dxtra": "BISTROVER 2dxtra",
|
||||
bmus: "BEATMANIA US",
|
||||
inf: "INFINITAS",
|
||||
};
|
||||
|
||||
const prettyDDRVersions: Versions["ddr:SP"] = {
|
||||
a20: "A20",
|
||||
};
|
||||
|
||||
const prettyGitadoraVersions: Versions["gitadora:Dora"] = {
|
||||
nextage: "NEX+AGE",
|
||||
};
|
||||
|
||||
export const PrettyVersions: Versions = {
|
||||
"iidx:SP": prettyIIDXVersions,
|
||||
"iidx:DP": prettyIIDXVersions,
|
||||
"popn:9B": { peace: "peace" },
|
||||
"bms:7K": {},
|
||||
"bms:14K": {},
|
||||
"chunithm:Single": {
|
||||
paradiselost: "Paradise Lost",
|
||||
},
|
||||
"ddr:SP": prettyDDRVersions,
|
||||
"ddr:DP": prettyDDRVersions,
|
||||
"gitadora:Gita": prettyGitadoraVersions,
|
||||
"gitadora:Dora": prettyGitadoraVersions,
|
||||
"maimai:Single": {
|
||||
finale: "FiNALE",
|
||||
},
|
||||
"museca:Single": {
|
||||
1.5: "1 + 1/2",
|
||||
"1.5-b": "1 + 1/2 Rev. B",
|
||||
},
|
||||
"sdvx:Single": {
|
||||
booth: "BOOTH",
|
||||
inf: "Infinite Infection",
|
||||
gw: "GRAVITY WARS",
|
||||
heaven: "HEAVENLY HAVEN",
|
||||
vivid: "VIVID WAVE",
|
||||
exceed: "EXCEED GEAR",
|
||||
konaste: "Konaste",
|
||||
},
|
||||
"usc:Keyboard": {},
|
||||
"usc:Controller": {},
|
||||
"wacca:Single": {
|
||||
reverse: "REVERSE",
|
||||
},
|
||||
"jubeat:Single": {
|
||||
festo: "festo",
|
||||
clan: "clan",
|
||||
},
|
||||
"pms:Controller": {},
|
||||
"pms:Keyboard": {},
|
||||
"itg:Stamina": {},
|
||||
};
|
||||
@@ -0,0 +1,172 @@
|
||||
export enum IIDX_LAMPS {
|
||||
NO_PLAY = 0,
|
||||
FAILED = 1,
|
||||
ASSIST_CLEAR = 2,
|
||||
EASY_CLEAR = 3,
|
||||
CLEAR = 4,
|
||||
HARD_CLEAR = 5,
|
||||
EX_HARD_CLEAR = 6,
|
||||
FULL_COMBO = 7,
|
||||
}
|
||||
|
||||
export enum CHUNITHM_LAMPS {
|
||||
FAILED = 0,
|
||||
CLEAR = 1,
|
||||
FULL_COMBO = 2,
|
||||
ALL_JUSTICE = 3,
|
||||
ALL_JUSTICE_CRITICAL = 4,
|
||||
}
|
||||
|
||||
export enum SDVX_LAMPS {
|
||||
FAILED = 0,
|
||||
CLEAR = 1,
|
||||
EXCESSIVE_CLEAR = 2,
|
||||
ULTIMATE_CHAIN = 3,
|
||||
PERFECT_ULTIMATE_CHAIN = 4,
|
||||
}
|
||||
|
||||
export enum MUSECA_LAMPS {
|
||||
FAILED = 0,
|
||||
CLEAR = 1,
|
||||
CONNECT_ALL = 2,
|
||||
PERFECT_CONNECT_ALL = 3,
|
||||
}
|
||||
|
||||
export enum DDR_LAMPS {
|
||||
FAILED = 0,
|
||||
CLEAR = 1,
|
||||
LIFE4 = 2,
|
||||
FULL_COMBO = 3,
|
||||
GREAT_FULL_COMBO = 4,
|
||||
PERFECT_FULL_COMBO = 5,
|
||||
MARVELOUS_FULL_COMBO = 6,
|
||||
}
|
||||
|
||||
export enum MAIMAI_LAMPS {
|
||||
FAILED = 0,
|
||||
CLEAR = 1,
|
||||
FULL_COMBO = 2,
|
||||
ALL_PERFECT = 3,
|
||||
ALL_PERFECT_PLUS = 4,
|
||||
}
|
||||
|
||||
export enum GITADORA_LAMPS {
|
||||
FAILED = 0,
|
||||
CLEAR = 1,
|
||||
FULL_COMBO = 2,
|
||||
EXCELLENT = 3,
|
||||
}
|
||||
|
||||
export enum IIDX_GRADES {
|
||||
F = 0,
|
||||
E = 1,
|
||||
D = 2,
|
||||
C = 3,
|
||||
B = 4,
|
||||
A = 5,
|
||||
AA = 6,
|
||||
AAA = 7,
|
||||
MAX_MINUS = 8,
|
||||
MAX = 9,
|
||||
}
|
||||
|
||||
export enum CHUNITHM_GRADES {
|
||||
D = 0,
|
||||
C = 1,
|
||||
B = 2,
|
||||
BB = 3,
|
||||
BBB = 4,
|
||||
A = 5,
|
||||
AA = 6,
|
||||
AAA = 7,
|
||||
S = 8,
|
||||
SS = 9,
|
||||
SSS = 10,
|
||||
}
|
||||
|
||||
export enum SDVX_GRADES {
|
||||
D = 0,
|
||||
C = 1,
|
||||
B = 2,
|
||||
A = 3,
|
||||
A_PLUS = 4,
|
||||
AA = 5,
|
||||
AA_PLUS = 6,
|
||||
AAA = 7,
|
||||
AAA_PLUS = 8,
|
||||
S = 9,
|
||||
}
|
||||
|
||||
export enum MUSECA_GRADES {
|
||||
没 = 0,
|
||||
拙 = 1,
|
||||
凡 = 2,
|
||||
佳 = 3,
|
||||
良 = 4,
|
||||
優 = 5,
|
||||
秀 = 6,
|
||||
傑 = 7,
|
||||
傑G = 8,
|
||||
}
|
||||
|
||||
/**
|
||||
* Kxxx refers to the starting point of that grade,
|
||||
* since they dont translate to english well.
|
||||
* K950 means the grade that starts at 950K and goes
|
||||
* to 975K.
|
||||
*/
|
||||
export enum MUSECA_GRADES_ASCII {
|
||||
ZERO = 0,
|
||||
K600 = 1,
|
||||
K700 = 2,
|
||||
K800 = 3,
|
||||
K850 = 4,
|
||||
K900 = 5,
|
||||
K950 = 6,
|
||||
K975 = 7,
|
||||
K1000 = 8,
|
||||
}
|
||||
|
||||
export enum DDR_GRADES {
|
||||
D = 0,
|
||||
D_PLUS = 1,
|
||||
C_MINUS = 2,
|
||||
C = 3,
|
||||
C_PLUS = 4,
|
||||
B_MINUS = 5,
|
||||
B = 6,
|
||||
B_PLUS = 7,
|
||||
A_MINUS = 8,
|
||||
A = 9,
|
||||
A_PLUS = 10,
|
||||
AA_MINUS = 11,
|
||||
AA = 12,
|
||||
AA_PLUS = 13,
|
||||
AAA = 14,
|
||||
}
|
||||
|
||||
export enum MAIMAI_GRADES {
|
||||
F = 0,
|
||||
E = 1,
|
||||
D = 2,
|
||||
C = 3,
|
||||
B = 4,
|
||||
A = 5,
|
||||
AA = 6,
|
||||
AAA = 7,
|
||||
S = 8,
|
||||
S_PLUS = 9,
|
||||
SS = 10,
|
||||
SS_PLUS = 11,
|
||||
SSS = 12,
|
||||
SSS_PLUS = 13,
|
||||
}
|
||||
|
||||
export enum GITADORA_GRADES {
|
||||
C = 0,
|
||||
B = 1,
|
||||
A = 2,
|
||||
S = 3,
|
||||
SS = 4,
|
||||
MAX = 5,
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { APIPermissions } from "../types";
|
||||
|
||||
// Some parts of tachi-server and some parts of the schemas refer to this
|
||||
// in order to assert that permissions are correctly assigned.
|
||||
export const ALL_PERMISSIONS: Record<APIPermissions, true> = {
|
||||
customise_profile: true,
|
||||
customise_score: true,
|
||||
customise_session: true,
|
||||
delete_score: true,
|
||||
manage_rivals: true,
|
||||
manage_targets: true,
|
||||
submit_score: true,
|
||||
manage_challenges: true,
|
||||
};
|
||||
@@ -0,0 +1,359 @@
|
||||
// Classes refer to things like dans.
|
||||
// Not the JS construct.
|
||||
|
||||
import type { IDStrings, integer } from "./types";
|
||||
|
||||
export interface ClassInfo {
|
||||
display: string;
|
||||
id: string;
|
||||
mouseover?: string;
|
||||
css?: {
|
||||
backgroundColor: string;
|
||||
color: string;
|
||||
};
|
||||
variant?: "danger" | "info" | "primary" | "secondary" | "success" | "warning";
|
||||
}
|
||||
|
||||
function mouseoverCSS(id: string, d: string, m: string, bg = "black", color = "white") {
|
||||
return { id, display: d, mouseover: m, css: { backgroundColor: bg, color } };
|
||||
}
|
||||
|
||||
function mouseoverVariant(
|
||||
id: string,
|
||||
d: string,
|
||||
m: string,
|
||||
variant: "danger" | "info" | "primary" | "secondary" | "success" | "warning"
|
||||
) {
|
||||
return { id, display: d, mouseover: m, variant };
|
||||
}
|
||||
|
||||
function noMouseoverVariant(
|
||||
id: string,
|
||||
d: string,
|
||||
variant: "danger" | "info" | "primary" | "secondary" | "success" | "warning"
|
||||
) {
|
||||
return { id, display: d, variant };
|
||||
}
|
||||
|
||||
function noMouseoverCSS(id: string, d: string, bg = "#131313", color = "white") {
|
||||
return { id, display: d, css: { backgroundColor: bg, color } };
|
||||
}
|
||||
|
||||
export const IIDXDans: Array<ClassInfo> = [
|
||||
mouseoverCSS("KYU_7", "七級", "7th Kyu", "green"),
|
||||
mouseoverCSS("KYU_6", "六級", "6th Kyu", "green"),
|
||||
mouseoverCSS("KYU_5", "五級", "5th Kyu", "green"),
|
||||
mouseoverCSS("KYU_4", "四級", "4th Kyu", "green"),
|
||||
mouseoverCSS("KYU_3", "三級", "3rd Kyu", "green"),
|
||||
mouseoverCSS("KYU_2", "二級", "2nd Kyu", "green"),
|
||||
mouseoverCSS("KYU_1", "一級", "1st Kyu", "green"),
|
||||
mouseoverVariant("DAN_1", "初段", "1st Dan", "info"),
|
||||
mouseoverVariant("DAN_2", "二段", "2nd Dan", "info"),
|
||||
mouseoverVariant("DAN_3", "三段", "3rd Dan", "info"),
|
||||
mouseoverVariant("DAN_4", "四段", "4th Dan", "info"),
|
||||
mouseoverVariant("DAN_5", "五段", "5th Dan", "info"),
|
||||
mouseoverVariant("DAN_6", "六段", "6th Dan", "info"),
|
||||
mouseoverVariant("DAN_7", "七段", "7th Dan", "info"),
|
||||
mouseoverVariant("DAN_8", "八段", "8th Dan", "info"),
|
||||
mouseoverVariant("DAN_9", "九段", "9th Dan", "danger"),
|
||||
mouseoverVariant("DAN_10", "十段", "10th Dan", "danger"),
|
||||
mouseoverCSS("CHUUDEN", "中伝", "Chuuden", "silver", "black"),
|
||||
mouseoverVariant("KAIDEN", "皆伝", "Kaiden", "warning"),
|
||||
];
|
||||
|
||||
export const GitadoraColours: Array<ClassInfo> = [
|
||||
mouseoverCSS("WHITE", "白", "White", "white", "black"),
|
||||
mouseoverCSS("ORANGE", "橙", "Orange", "orange"),
|
||||
mouseoverCSS("ORANGE_GRD", "橙グラ", "Orange Gradient", "orange"),
|
||||
mouseoverVariant("YELLOW", "黄", "Yellow", "warning"),
|
||||
mouseoverVariant("YELLOW_GRD", "黄グラ", "Yellow Gradient", "warning"),
|
||||
mouseoverCSS("GREEN", "緑", "Green", "green"),
|
||||
mouseoverCSS("GREEN_GRD", "緑グラ", "Green Gradient", "green"),
|
||||
mouseoverVariant("BLUE", "青", "Blue", "info"),
|
||||
mouseoverVariant("BLUE_GRD", "青グラ", "Blue Gradient", "info"),
|
||||
mouseoverCSS("PURPLE", "紫", "Purple", "purple"),
|
||||
mouseoverCSS("PURPLE_GRD", "紫グラ", "Purple Gradient", "purple"),
|
||||
mouseoverVariant("RED", "赤", "Red", "danger"),
|
||||
mouseoverVariant("RED_GRD", "赤グラ", "Red Gradient", "danger"),
|
||||
mouseoverCSS("BRONZE", "銅", "Bronze", "bronze"),
|
||||
mouseoverCSS("SILVER", "銀", "Silver", "silver", "black"),
|
||||
mouseoverCSS("GOLD", "金", "Gold", "gold"),
|
||||
|
||||
// @todo #11 Come up with CSS for Gitadora's rainbow icon.
|
||||
mouseoverCSS("RAINBOW", "虹", "Rainbow", "todo"),
|
||||
];
|
||||
|
||||
export const BMSGenocideDans: Array<ClassInfo> = [
|
||||
mouseoverCSS("NORMAL_1", "☆1", "Normal 1st Dan", "lightblue"),
|
||||
mouseoverCSS("NORMAL_2", "☆2", "Normal 2nd Dan", "lightblue"),
|
||||
mouseoverCSS("NORMAL_3", "☆3", "Normal 3rd Dan", "lightblue"),
|
||||
mouseoverCSS("NORMAL_4", "☆4", "Normal 4th Dan", "lightblue"),
|
||||
mouseoverCSS("NORMAL_5", "☆5", "Normal 5th Dan", "lightblue"),
|
||||
mouseoverCSS("NORMAL_6", "☆6", "Normal 6th Dan", "lightblue"),
|
||||
mouseoverCSS("NORMAL_7", "☆7", "Normal 7th Dan", "lightblue"),
|
||||
mouseoverCSS("NORMAL_8", "☆8", "Normal 8th Dan", "lightblue"),
|
||||
mouseoverCSS("NORMAL_9", "☆9", "Normal 9th Dan", "lightred"),
|
||||
mouseoverCSS("NORMAL_10", "☆10", "Normal 10th Dan", "lightred"),
|
||||
mouseoverVariant("INSANE_1", "★1", "Insane 1st Dan", "info"),
|
||||
mouseoverVariant("INSANE_2", "★2", "Insane 2nd Dan", "info"),
|
||||
mouseoverVariant("INSANE_3", "★3", "Insane 3rd Dan", "info"),
|
||||
mouseoverVariant("INSANE_4", "★4", "Insane 4th Dan", "info"),
|
||||
mouseoverVariant("INSANE_5", "★5", "Insane 5th Dan", "info"),
|
||||
mouseoverVariant("INSANE_6", "★6", "Insane 6th Dan", "info"),
|
||||
mouseoverVariant("INSANE_7", "★7", "Insane 7th Dan", "info"),
|
||||
mouseoverVariant("INSANE_8", "★8", "Insane 8th Dan", "info"),
|
||||
mouseoverCSS("INSANE_9", "★9", "Insane 9th Dan", "red"),
|
||||
mouseoverCSS("INSANE_10", "★10", "Insane 10th Dan", "red"),
|
||||
mouseoverCSS("INSANE_KAIDEN", "★★", "Insane Kaiden", "teal"),
|
||||
mouseoverCSS("OVERJOY", "(^^)", "Overjoy", "purple"),
|
||||
];
|
||||
|
||||
export const BMSStSlDans: Array<ClassInfo> = [
|
||||
mouseoverCSS("SL0", "sl0", "Satellite 0"),
|
||||
mouseoverCSS("SL1", "sl1", "Satellite 1"),
|
||||
mouseoverCSS("SL2", "sl2", "Satellite 2"),
|
||||
mouseoverCSS("SL3", "sl3", "Satellite 3"),
|
||||
mouseoverCSS("SL4", "sl4", "Satellite 4"),
|
||||
mouseoverCSS("SL5", "sl5", "Satellite 5"),
|
||||
mouseoverCSS("SL6", "sl6", "Satellite 6"),
|
||||
mouseoverCSS("SL7", "sl7", "Satellite 7"),
|
||||
mouseoverCSS("SL8", "sl8", "Satellite 8"),
|
||||
mouseoverCSS("SL9", "sl9", "Satellite 9"),
|
||||
mouseoverCSS("SL10", "sl10", "Satellite 10"),
|
||||
mouseoverCSS("SL11", "sl11", "Satellite 11"),
|
||||
mouseoverCSS("SL12", "sl12", "Satellite 12"),
|
||||
mouseoverCSS("ST0", "st0", "Stella 0"),
|
||||
mouseoverCSS("ST1", "st1", "Stella 1"),
|
||||
mouseoverCSS("ST2", "st2", "Stella 2"),
|
||||
mouseoverCSS("ST3", "st3", "Stella 3"),
|
||||
mouseoverCSS("ST4", "st4", "Stella 4"),
|
||||
mouseoverCSS("ST5", "st5", "Stella 5"),
|
||||
mouseoverCSS("ST6", "st6", "Stella 6"),
|
||||
mouseoverCSS("ST7", "st7", "Stella 7"),
|
||||
mouseoverCSS("ST8", "st8", "Stella 8"),
|
||||
mouseoverCSS("ST9", "st9", "Stella 9"),
|
||||
mouseoverCSS("ST10", "st10", "Stella 10"),
|
||||
mouseoverCSS("ST11", "st11", "Stella 11"),
|
||||
];
|
||||
|
||||
export const BMSLNDans: Array<ClassInfo> = [
|
||||
mouseoverCSS("DAN_1", "◆1", "LN 1st Dan", "lightblue"),
|
||||
mouseoverCSS("DAN_2", "◆2", "LN 2nd Dan", "lightblue"),
|
||||
mouseoverCSS("DAN_3", "◆3", "LN 3rd Dan", "lightblue"),
|
||||
mouseoverCSS("DAN_4", "◆4", "LN 4th Dan", "lightblue"),
|
||||
mouseoverCSS("DAN_5", "◆5", "LN 5th Dan", "lightblue"),
|
||||
mouseoverCSS("DAN_6", "◆6", "LN 6th Dan", "lightblue"),
|
||||
mouseoverCSS("DAN_7", "◆7", "LN 7th Dan", "lightblue"),
|
||||
mouseoverCSS("DAN_8", "◆8", "LN 8th Dan", "lightblue"),
|
||||
mouseoverCSS("DAN_9", "◆9", "LN 9th Dan", "lightred"),
|
||||
mouseoverCSS("DAN_10", "◆10", "LN 10th Dan", "lightred"),
|
||||
mouseoverCSS("KAIDEN", "◆◆", "LN Kaiden", "teal"),
|
||||
mouseoverCSS("OVERJOY", "◆(^^)", "LN Overjoy", "purple"),
|
||||
mouseoverCSS("UDON", "◆うどん", "LN Udon", "gold"),
|
||||
];
|
||||
|
||||
export const BMSScratchDans: Array<ClassInfo> = [
|
||||
mouseoverCSS("KYU_7", "七級", "Scratch 7th Kyu", "green"),
|
||||
mouseoverCSS("KYU_6", "六級", "Scratch 6th Kyu", "green"),
|
||||
mouseoverCSS("KYU_5", "五級", "Scratch 5th Kyu", "green"),
|
||||
mouseoverCSS("KYU_4", "四級", "Scratch 4th Kyu", "green"),
|
||||
mouseoverCSS("KYU_3", "三級", "Scratch 3rd Kyu", "green"),
|
||||
mouseoverCSS("KYU_2", "二級", "Scratch 2nd Kyu", "green"),
|
||||
mouseoverCSS("KYU_1", "一級", "Scratch 1st Kyu", "green"),
|
||||
mouseoverVariant("DAN_1", "初段", "Scratch 1st Dan", "info"),
|
||||
mouseoverVariant("DAN_2", "二段", "Scratch 2nd Dan", "info"),
|
||||
mouseoverVariant("DAN_3", "三段", "Scratch 3rd Dan", "info"),
|
||||
mouseoverVariant("DAN_4", "四段", "Scratch 4th Dan", "info"),
|
||||
mouseoverVariant("DAN_5", "五段", "Scratch 5th Dan", "info"),
|
||||
mouseoverVariant("DAN_6", "六段", "Scratch 6th Dan", "info"),
|
||||
mouseoverVariant("DAN_7", "七段", "Scratch 7th Dan", "info"),
|
||||
mouseoverVariant("DAN_8", "八段", "Scratch 8th Dan", "info"),
|
||||
mouseoverVariant("DAN_9", "九段", "Scratch 9th Dan", "danger"),
|
||||
mouseoverVariant("DAN_10", "十段", "Scratch 10th Dan", "danger"),
|
||||
mouseoverVariant("KAIDEN", "皆伝", "Scratch Kaiden", "warning"),
|
||||
];
|
||||
|
||||
export const SDVXDans: Array<ClassInfo> = [
|
||||
mouseoverCSS("DAN_1", "LV.01", "1st Dan"),
|
||||
mouseoverCSS("DAN_2", "LV.02", "2nd Dan"),
|
||||
mouseoverCSS("DAN_3", "LV.03", "3rd Dan"),
|
||||
mouseoverCSS("DAN_4", "LV.04", "4th Dan"),
|
||||
mouseoverCSS("DAN_5", "LV.05", "5th Dan"),
|
||||
mouseoverCSS("DAN_6", "LV.06", "6th Dan"),
|
||||
mouseoverCSS("DAN_7", "LV.07", "7th Dan"),
|
||||
mouseoverCSS("DAN_8", "LV.08", "8th Dan"),
|
||||
mouseoverCSS("DAN_9", "LV.09", "9th Dan"),
|
||||
mouseoverVariant("DAN_10", "LV.10", "10th Dan", "warning"),
|
||||
mouseoverVariant("DAN_11", "LV.11", "11th Dan", "danger"),
|
||||
mouseoverCSS("INF", "LV.INF", "Inf. Dan", "purple", "gold"),
|
||||
];
|
||||
|
||||
export const SDVXVFClasses: Array<ClassInfo> = [
|
||||
noMouseoverCSS("SIENNA_I", "Sienna I"),
|
||||
noMouseoverCSS("SIENNA_II", "Sienna II"),
|
||||
noMouseoverCSS("SIENNA_III", "Sienna III"),
|
||||
noMouseoverCSS("SIENNA_IV", "Sienna IV"),
|
||||
noMouseoverCSS("COBALT_I", "Cobalt I"),
|
||||
noMouseoverCSS("COBALT_II", "Cobalt II"),
|
||||
noMouseoverCSS("COBALT_III", "Cobalt III"),
|
||||
noMouseoverCSS("COBALT_IV", "Cobalt IV"),
|
||||
noMouseoverCSS("DANDELION_I", "Dandelion I"),
|
||||
noMouseoverCSS("DANDELION_II", "Dandelion II"),
|
||||
noMouseoverCSS("DANDELION_III", "Dandelion III"),
|
||||
noMouseoverCSS("DANDELION_IV", "Dandelion IV"),
|
||||
noMouseoverCSS("CYAN_I", "Cyan I"),
|
||||
noMouseoverCSS("CYAN_II", "Cyan II"),
|
||||
noMouseoverCSS("CYAN_III", "Cyan III"),
|
||||
noMouseoverCSS("CYAN_IV", "Cyan IV"),
|
||||
noMouseoverCSS("SCARLET_I", "Scarlet I"),
|
||||
noMouseoverCSS("SCARLET_II", "Scarlet II"),
|
||||
noMouseoverCSS("SCARLET_III", "Scarlet III"),
|
||||
noMouseoverCSS("SCARLET_IV", "Scarlet IV"),
|
||||
noMouseoverCSS("CORAL_I", "Coral I"),
|
||||
noMouseoverCSS("CORAL_II", "Coral II"),
|
||||
noMouseoverCSS("CORAL_III", "Coral III"),
|
||||
noMouseoverCSS("CORAL_IV", "Coral IV"),
|
||||
noMouseoverCSS("ARGENTO_I", "Argento I"),
|
||||
noMouseoverCSS("ARGENTO_II", "Argento II"),
|
||||
noMouseoverCSS("ARGENTO_III", "Argento III"),
|
||||
noMouseoverCSS("ARGENTO_IV", "Argento IV"),
|
||||
noMouseoverCSS("ELDORA_I", "Eldora I"),
|
||||
noMouseoverCSS("ELDORA_II", "Eldora II"),
|
||||
noMouseoverCSS("ELDORA_III", "Eldora III"),
|
||||
noMouseoverCSS("ELDORA_IV", "Eldora IV"),
|
||||
noMouseoverCSS("CRIMSON_I", "Crimson I"),
|
||||
noMouseoverCSS("CRIMSON_II", "Crimson II"),
|
||||
noMouseoverCSS("CRIMSON_III", "Crimson III"),
|
||||
noMouseoverCSS("CRIMSON_IV", "Crimson IV"),
|
||||
noMouseoverCSS("IMPERIAL_I", "Imperial I"),
|
||||
noMouseoverCSS("IMPERIAL_II", "Imperial II"),
|
||||
noMouseoverCSS("IMPERIAL_III", "Imperial III"),
|
||||
noMouseoverCSS("IMPERIAL_IV", "Imperial IV"),
|
||||
];
|
||||
|
||||
export const DDRDans = [
|
||||
mouseoverVariant("DAN_1", "初段", "1st Dan", "info"),
|
||||
mouseoverVariant("DAN_2", "二段", "2nd Dan", "info"),
|
||||
mouseoverVariant("DAN_3", "三段", "3rd Dan", "info"),
|
||||
mouseoverVariant("DAN_4", "四段", "4th Dan", "info"),
|
||||
mouseoverVariant("DAN_5", "五段", "5th Dan", "info"),
|
||||
mouseoverVariant("DAN_6", "六段", "6th Dan", "info"),
|
||||
mouseoverVariant("DAN_7", "七段", "7th Dan", "info"),
|
||||
mouseoverVariant("DAN_8", "八段", "8th Dan", "info"),
|
||||
mouseoverVariant("DAN_9", "九段", "9th Dan", "danger"),
|
||||
mouseoverVariant("DAN_10", "十段", "10th Dan", "danger"),
|
||||
mouseoverVariant("KAIDEN", "皆伝", "Kaiden", "warning"),
|
||||
];
|
||||
|
||||
export const CHUNITHMColours = [
|
||||
mouseoverVariant("BLUE", "青", "Blue", "info"),
|
||||
mouseoverCSS("GREEN", "緑", "Green", "green"),
|
||||
mouseoverCSS("ORANGE", "橙", "Orange", "orange"),
|
||||
mouseoverVariant("RED", "赤", "Red", "danger"),
|
||||
mouseoverCSS("PURPLE", "紫", "Purple", "purple"),
|
||||
mouseoverCSS("COPPER", "銅", "Copper", "bronze"),
|
||||
mouseoverVariant("SILVER", "銀", "Silver", "secondary"),
|
||||
mouseoverVariant("GOLD", "金", "Gold", "warning"),
|
||||
mouseoverCSS("PLATINUM", "鉑", "Platinum", "silver", "black"),
|
||||
mouseoverVariant("RAINBOW", "虹", "Rainbow", "success"),
|
||||
];
|
||||
|
||||
// todo: #6 Add colours to the wacca stage ups.
|
||||
export const WaccaStageUps = [
|
||||
noMouseoverCSS("I", "I"),
|
||||
noMouseoverCSS("II", "II"),
|
||||
noMouseoverCSS("III", "III"),
|
||||
noMouseoverCSS("IV", "IV"),
|
||||
noMouseoverCSS("V", "V"),
|
||||
noMouseoverCSS("VI", "VI"),
|
||||
noMouseoverCSS("VII", "VII"),
|
||||
noMouseoverCSS("VIII", "VIII"),
|
||||
noMouseoverCSS("IX", "IX"),
|
||||
noMouseoverCSS("X", "X"),
|
||||
noMouseoverCSS("XI", "XI"),
|
||||
noMouseoverCSS("XII", "XII"),
|
||||
noMouseoverCSS("XIII", "XIII"),
|
||||
noMouseoverCSS("XIV", "XIV"),
|
||||
];
|
||||
|
||||
export const WaccaColours = [
|
||||
noMouseoverVariant("ASH", "Ash", "secondary"),
|
||||
noMouseoverCSS("NAVY", "Navy", "darkblue"),
|
||||
noMouseoverCSS("YELLOW", "Yellow", "orange"),
|
||||
noMouseoverVariant("RED", "Red", "danger"),
|
||||
noMouseoverCSS("PURPLE", "Purple", "purple"),
|
||||
noMouseoverVariant("BLUE", "Blue", "info"),
|
||||
noMouseoverCSS("SILVER", "Silver", "silver", "black"),
|
||||
noMouseoverVariant("GOLD", "Gold", "warning"),
|
||||
noMouseoverVariant("RAINBOW", "Rainbow", "success"),
|
||||
];
|
||||
|
||||
// what the hell are these names?
|
||||
export const PopnClasses = [
|
||||
mouseoverCSS("KITTY", "にゃんこ", "Kitty", "brown"),
|
||||
mouseoverCSS("STUDENT", "小学生", "Grade School Student", "green"),
|
||||
mouseoverCSS("DELINQUENT", "番長", "Delinquent", "lime", "black"),
|
||||
mouseoverCSS("DETECTIVE", "刑事", "Detective", "purple"),
|
||||
mouseoverVariant("IDOL", "アイドル", "Idol", "danger"),
|
||||
mouseoverCSS("GENERAL", "将軍", "General", "darkgoldenrod"),
|
||||
mouseoverVariant("HERMIT", "仙人", "Hermit", "success"),
|
||||
mouseoverVariant("GOD", "神", "God", "warning"),
|
||||
];
|
||||
|
||||
export const PMSDans = [
|
||||
mouseoverVariant("INSANE_1", "●1", "Insane 1st Dan", "info"),
|
||||
mouseoverVariant("INSANE_2", "●2", "Insane 2nd Dan", "info"),
|
||||
mouseoverVariant("INSANE_3", "●3", "Insane 3rd Dan", "info"),
|
||||
mouseoverVariant("INSANE_4", "●4", "Insane 4th Dan", "info"),
|
||||
mouseoverVariant("INSANE_5", "●5", "Insane 5th Dan", "info"),
|
||||
mouseoverVariant("INSANE_6", "●6", "Insane 6th Dan", "info"),
|
||||
mouseoverVariant("INSANE_7", "●7", "Insane 7th Dan", "info"),
|
||||
mouseoverVariant("INSANE_8", "●8", "Insane 8th Dan", "info"),
|
||||
mouseoverCSS("INSANE_9", "●9", "Insane 9th Dan", "red"),
|
||||
mouseoverCSS("INSANE_10", "●10", "Insane 10th Dan", "red"),
|
||||
mouseoverCSS("INSANE_KAIDEN", "●●", "Insane Kaiden", "teal"),
|
||||
noMouseoverCSS("OVERJOY", "●OJ", "Overjoy", "purple"),
|
||||
mouseoverVariant("UNDEFINED", "●UNDF", "Undefined (Post-Overjoy)", "warning"),
|
||||
];
|
||||
|
||||
export const JubeatColours = [
|
||||
noMouseoverVariant("BLACK", "Black", "secondary"),
|
||||
noMouseoverCSS("YELLOW_GREEN", "Yellow-Green", "yellowgreen", "black"),
|
||||
noMouseoverCSS("GREEN", "Green", "green"),
|
||||
noMouseoverCSS("LIGHT_BLUE", "Light Blue", "cyan", "black"),
|
||||
noMouseoverVariant("BLUE", "Blue", "info"),
|
||||
noMouseoverCSS("VIOLET", "Violet", "violet"),
|
||||
noMouseoverCSS("PURPLE", "Purple", "purple"),
|
||||
noMouseoverCSS("PINK", "Pink", "pink", "black"),
|
||||
noMouseoverCSS("ORANGE", "Orange", "orange"),
|
||||
noMouseoverVariant("GOLD", "Gold", "warning"),
|
||||
];
|
||||
|
||||
export interface GameClassSets {
|
||||
"iidx:SP": "dan";
|
||||
"iidx:DP": "dan";
|
||||
"popn:9B": "class";
|
||||
"sdvx:Single": "dan" | "vfClass";
|
||||
"usc:Keyboard": never;
|
||||
"usc:Controller": never;
|
||||
"ddr:SP": "dan";
|
||||
"ddr:DP": "dan";
|
||||
"maimai:Single": never;
|
||||
"jubeat:Single": "colour";
|
||||
"museca:Single": never;
|
||||
"bms:7K": "genocideDan" | "lnDan" | "scratchDan" | "stslDan";
|
||||
"bms:14K": "genocideDan";
|
||||
"chunithm:Single": "colour";
|
||||
"gitadora:Gita": "colour";
|
||||
"gitadora:Dora": "colour";
|
||||
"wacca:Single": "colour" | "stageUp";
|
||||
"pms:Controller": "dan";
|
||||
"pms:Keyboard": "dan";
|
||||
"itg:Stamina": never;
|
||||
}
|
||||
|
||||
export type AllClassSets = GameClassSets[IDStrings];
|
||||
|
||||
export type GameClasses<I extends IDStrings> = {
|
||||
[K in GameClassSets[I]]: integer;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as tachiCommon from "./index";
|
||||
import t from "tap";
|
||||
|
||||
// we just check that we're exporting stuff properly
|
||||
t.test("Property Checks", (t) => {
|
||||
t.type(tachiCommon.GetGameConfig, "function");
|
||||
t.type(tachiCommon.GetGamePTConfig, "function");
|
||||
t.type(tachiCommon.gameClasses, "object");
|
||||
t.type(tachiCommon.COLOUR_SET, "object");
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
export * from "./types";
|
||||
|
||||
export * from "./config/config";
|
||||
export * as gameClasses from "./game-classes";
|
||||
export * as ESDCore from "./lib/esd";
|
||||
export * as StaticConfig from "./config/static-config";
|
||||
export * from "./utils/util";
|
||||
export * from "./webhooks";
|
||||
export * from "./constants/game";
|
||||
export * from "./constants/permissions";
|
||||
export * as Schemas from "./lib/schemas";
|
||||
@@ -0,0 +1,46 @@
|
||||
import { CalculateESD, ESDCompare, PercentCompare } from "./esd";
|
||||
import { GetGamePTConfig } from "../config/config";
|
||||
import t from "tap";
|
||||
|
||||
function approx(t: Tap.Test, given: number, recieved: number, acc = 0.01) {
|
||||
if (Math.abs(given - recieved) < acc) {
|
||||
return t.pass();
|
||||
}
|
||||
|
||||
return t.fail(`Expected ${given} to be within ${acc} of ${recieved}.`);
|
||||
}
|
||||
|
||||
const gptConfig = GetGamePTConfig("iidx", "SP");
|
||||
|
||||
if (!gptConfig.supportsESD) {
|
||||
throw new Error(`IIDX:SP no longer supports esd, these tests won't work.`);
|
||||
}
|
||||
|
||||
const judgementWindows = gptConfig.judgementWindows;
|
||||
|
||||
t.test("#CalculateESD", (t) => {
|
||||
const jdg = judgementWindows;
|
||||
|
||||
approx(t, CalculateESD(jdg, 0.8), 17.58);
|
||||
approx(t, CalculateESD(jdg, 0), 200);
|
||||
t.throws(() => CalculateESD(jdg, 0, true));
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#ESDCompare", (t) => {
|
||||
approx(t, ESDCompare(16, 14), 6.42);
|
||||
approx(t, ESDCompare(16, 14, 1), ESDCompare(16, 14));
|
||||
approx(t, ESDCompare(0, 200, 1), -68.27);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#PercentCompare", (t) => {
|
||||
const jdg = judgementWindows;
|
||||
|
||||
approx(t, PercentCompare(jdg, 0.99, 0.98), -5.53);
|
||||
approx(t, PercentCompare(jdg, 0.99, 0.98), PercentCompare(jdg, 0.99, 0.98, 1));
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Cumulative Distribution Function
|
||||
* https://en.wikipedia.org/wiki/Cumulative_distribution_function
|
||||
*/
|
||||
function cdf(x: number, mean: number, variance: number) {
|
||||
return 0.5 * (1 + erf((x - mean) / Math.sqrt(2 * variance)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Error Function
|
||||
* https://en.wikipedia.org/wiki/Error_function
|
||||
*/
|
||||
function erf(x: number) {
|
||||
// save the sign of x
|
||||
const sign = x >= 0 ? 1 : -1;
|
||||
|
||||
const absX = Math.abs(x);
|
||||
|
||||
// constants
|
||||
const a1 = 0.254829592;
|
||||
const a2 = -0.284496736;
|
||||
const a3 = 1.421413741;
|
||||
const a4 = -1.453152027;
|
||||
const a5 = 1.061405429;
|
||||
const p = 0.3275911;
|
||||
|
||||
// A&S formula 7.1.26
|
||||
const t = 1.0 / (1.0 + p * absX);
|
||||
const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-absX * absX);
|
||||
|
||||
return sign * y; // erf(-x) = -erf(x);
|
||||
}
|
||||
|
||||
function CDFBetween(lowBound: number, highBound: number, mean: number, variance: number) {
|
||||
// Since the normal distribution is symmetrical, we want to double this, as the cut will only get one tail.
|
||||
return 2 * (cdf(highBound, mean, variance) - cdf(lowBound, mean, variance));
|
||||
}
|
||||
|
||||
// This is a direct port of ESD-JS to typescript. Maybe this should be released as its own module some day.
|
||||
|
||||
const MEAN = 0;
|
||||
|
||||
/**
|
||||
* Gets the percent this score would roughly be, given a standard deviation.
|
||||
* @param judgements - The judgements this game uses.
|
||||
* @param stddev - The standard deviation to estimate the percent of.
|
||||
* @param largestValue - The largest "value" of a judgement in this game. This lets us normalise results.
|
||||
* @returns The percent this std. deviation would produce.
|
||||
*/
|
||||
function StdDeviationToPercent(
|
||||
judgements: Array<ESDJudgementFormat>,
|
||||
stddev: number,
|
||||
largestValue: number
|
||||
) {
|
||||
let lastJudgeMSBorder = 0;
|
||||
let prbSum = 0;
|
||||
|
||||
for (const judge of judgements) {
|
||||
const nVal = CDFBetween(lastJudgeMSBorder, judge.msBorder, MEAN, stddev ** 2) * judge.value;
|
||||
|
||||
lastJudgeMSBorder = judge.msBorder;
|
||||
prbSum = prbSum + nVal;
|
||||
}
|
||||
|
||||
prbSum = prbSum / largestValue;
|
||||
|
||||
return prbSum;
|
||||
}
|
||||
|
||||
const ACCEPTABLE_ERROR = 0.001;
|
||||
const MAX_ITERATIONS = 50;
|
||||
|
||||
export interface ESDJudgementFormat {
|
||||
name: string;
|
||||
msBorder: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given judgements and a percent, estimate the standard deviation needed to get a score
|
||||
* with that percent.
|
||||
* @param judgements - The judgements for this game.
|
||||
* @param percent - The percent to estimate SD needed to get.
|
||||
* @param errOnInaccuracy - Whether or whether not to throw if the estimate is not accurate enough.
|
||||
* @returns
|
||||
*/
|
||||
export function CalculateESD(
|
||||
judgements: Array<ESDJudgementFormat>,
|
||||
percent: number,
|
||||
errOnInaccuracy = false
|
||||
): number {
|
||||
if (percent > 1 || percent < 0 || Number.isNaN(percent)) {
|
||||
throw new Error(
|
||||
"(ESD) Invalid percent. Percent must be between 0 and 1, and also a number."
|
||||
);
|
||||
}
|
||||
|
||||
const largestValue = judgements.slice(0).sort((a, b) => b.value - a.value)[0].value;
|
||||
|
||||
// massive optimisation possible here by using better initial estimates with precalc'd table of values.
|
||||
// until then, it's just kinda slow.
|
||||
let minSD = 0;
|
||||
let maxSD = 200;
|
||||
let estSD = (minSD + maxSD) / 2;
|
||||
|
||||
// So, fundamentally the function that takes SD and returns estimated percent is NOT invertible.
|
||||
// as in, it is very literally not invertible.
|
||||
// if you figure it out, let me know.
|
||||
// until then; we make MAX_ITERATIONS attempts at finding a value within the acceptable range of error.
|
||||
// the defaults for these are 100 for iterations, and 0.001 for error.
|
||||
// for most of what i've tested, this has been fine.
|
||||
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
||||
const estimatedPercent = StdDeviationToPercent(judgements, estSD, largestValue);
|
||||
|
||||
if (Math.abs(estimatedPercent - percent) < ACCEPTABLE_ERROR) {
|
||||
return estSD;
|
||||
}
|
||||
|
||||
if (estimatedPercent < percent) {
|
||||
maxSD = estSD;
|
||||
} else {
|
||||
minSD = estSD;
|
||||
}
|
||||
|
||||
if (estSD === (minSD + maxSD) / 2) {
|
||||
// if it isn't moving, just terminate
|
||||
break;
|
||||
}
|
||||
|
||||
estSD = (minSD + maxSD) / 2;
|
||||
}
|
||||
|
||||
if (errOnInaccuracy) {
|
||||
throw new Error(`(ESD-JS) Did not reach value within MAX_ITERATIONS (${MAX_ITERATIONS})`);
|
||||
}
|
||||
|
||||
return estSD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two ESD values such that 1->2 produces a larger value than 101->102.
|
||||
* @param baseESD - The first ESD to compare.
|
||||
* @param compareESD - The second ESD to compare.
|
||||
* @param cdeg - The degrees of confidence to use. This should be 1.
|
||||
* @returns A number between -100 and 100.
|
||||
*/
|
||||
export function ESDCompare(baseESD: number, compareESD: number, cdeg = 1): number {
|
||||
const CONFIDENCE_DEGREE = cdeg;
|
||||
const BASE_CASE = CDFBetween(-1 * CONFIDENCE_DEGREE, CONFIDENCE_DEGREE, 0, 1) / 2;
|
||||
|
||||
let inv = false;
|
||||
let variance;
|
||||
let bound;
|
||||
|
||||
if (compareESD > baseESD) {
|
||||
inv = true;
|
||||
variance = compareESD ** 2;
|
||||
bound = CONFIDENCE_DEGREE * baseESD;
|
||||
} else {
|
||||
variance = baseESD ** 2;
|
||||
bound = CONFIDENCE_DEGREE * compareESD;
|
||||
}
|
||||
|
||||
const esdc = CDFBetween(-1 * bound, bound, 0, variance) / 2;
|
||||
|
||||
let besdc = BASE_CASE - esdc;
|
||||
|
||||
if (inv) {
|
||||
besdc = besdc * -1;
|
||||
}
|
||||
|
||||
return besdc * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts two percents to ESD, then runs ESDCompare.
|
||||
*/
|
||||
export function PercentCompare(
|
||||
judgements: Array<ESDJudgementFormat>,
|
||||
baseP: number,
|
||||
compareP: number,
|
||||
cdeg = 1
|
||||
): number {
|
||||
const e1 = CalculateESD(judgements, baseP);
|
||||
const e2 = CalculateESD(judgements, compareP);
|
||||
|
||||
return ESDCompare(e1, e2, cdeg);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+1635
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,245 @@
|
||||
import { GetGameConfig, GetGamePTConfig } from "../config/config";
|
||||
import type { ChartDocument, SongDocument, Game, IDStrings, Playtypes } from "..";
|
||||
import type { Grades, integer } from "../types";
|
||||
|
||||
export function FormatInt(v: number): string {
|
||||
return v.toString();
|
||||
}
|
||||
|
||||
export function FormatDifficulty(chart: ChartDocument, game: Game): string {
|
||||
if (game === "bms" || game === "pms") {
|
||||
const bmsChart = chart as ChartDocument<"bms:7K" | "bms:14K">;
|
||||
|
||||
return (
|
||||
bmsChart.data.tableFolders.map((e) => `${e.table}${e.level}`).join(", ") || "Unrated"
|
||||
);
|
||||
}
|
||||
|
||||
if (game === "itg") {
|
||||
const itgChart = chart as ChartDocument<"itg:Stamina">;
|
||||
|
||||
return `${itgChart.data.difficultyTag} ${itgChart.level}`;
|
||||
}
|
||||
|
||||
const gameConfig = GetGameConfig(game);
|
||||
|
||||
if (gameConfig.validPlaytypes.length > 1) {
|
||||
return `${chart.playtype} ${chart.difficulty} ${chart.level}`;
|
||||
}
|
||||
|
||||
return `${chart.difficulty} ${chart.level}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a chart's difficulty into a shorter variant. This handles a lot of
|
||||
* game-specific strange edge cases.
|
||||
*/
|
||||
export function FormatDifficultyShort(chart: ChartDocument, game: Game): string {
|
||||
const gameConfig = GetGameConfig(game);
|
||||
const gptConfig = GetGamePTConfig(game, chart.playtype);
|
||||
|
||||
if (game === "itg") {
|
||||
const itgChart = chart as ChartDocument<"itg:Stamina">;
|
||||
|
||||
return `S${itgChart.data.difficultyTag} ${chart.level}`;
|
||||
}
|
||||
|
||||
const shortDiff = gptConfig.shortDifficulties[chart.difficulty] ?? chart.difficulty;
|
||||
|
||||
if (game === "ddr") {
|
||||
return `${shortDiff}${chart.playtype}`;
|
||||
}
|
||||
|
||||
if (gameConfig.validPlaytypes.length === 1 || game === "gitadora") {
|
||||
return `${shortDiff} ${chart.level}`;
|
||||
}
|
||||
|
||||
if (game === "usc") {
|
||||
return `${chart.playtype === "Controller" ? "CON" : "KB"} ${shortDiff} ${chart.level}`;
|
||||
}
|
||||
|
||||
return `${chart.playtype}${shortDiff} ${chart.level}`;
|
||||
}
|
||||
|
||||
export function FormatGame(game: Game, playtype: Playtypes[Game]): string {
|
||||
const gameConfig = GetGameConfig(game);
|
||||
|
||||
if (gameConfig.validPlaytypes.length === 1) {
|
||||
return gameConfig.name;
|
||||
}
|
||||
|
||||
return `${gameConfig.name} (${playtype})`;
|
||||
}
|
||||
|
||||
export function FormatChart(game: Game, song: SongDocument, chart: ChartDocument): string {
|
||||
if (game === "bms") {
|
||||
const tables = (chart as ChartDocument<"bms:7K" | "bms:14K">).data.tableFolders;
|
||||
|
||||
const bmsSong = song as SongDocument<"bms">;
|
||||
|
||||
let realTitle = bmsSong.title;
|
||||
|
||||
if (bmsSong.data.subtitle) {
|
||||
realTitle = `${realTitle} - ${bmsSong.data.subtitle}`;
|
||||
}
|
||||
|
||||
if (bmsSong.data.genre) {
|
||||
realTitle = `${realTitle} [${bmsSong.data.genre}]`;
|
||||
}
|
||||
|
||||
if (tables.length === 0) {
|
||||
return realTitle;
|
||||
}
|
||||
|
||||
return `${realTitle} (${tables.map((e) => `${e.table}${e.level}`).join(", ")})`;
|
||||
} else if (game === "usc") {
|
||||
const uscChart = chart as ChartDocument<"usc:Controller" | "usc:Keyboard">;
|
||||
|
||||
// If this chart isn't an official, render it differently
|
||||
if (!uscChart.data.isOfficial) {
|
||||
// Same as BMS. turn this into SongTitle (Keyboard MXM normal1, insane2)
|
||||
return `${song.title} (${chart.playtype} ${
|
||||
chart.difficulty
|
||||
} ${uscChart.data.tableFolders.map((e) => `${e.table}${e.level}`).join(", ")})`;
|
||||
} else if (uscChart.data.tableFolders.length !== 0) {
|
||||
// if this chart is an official **AND** is on tables (unlikely), render
|
||||
// it as so:
|
||||
|
||||
// SongTitle (Keyboard MXM 17, normal1, insane2)
|
||||
return `${song.title} (${chart.playtype} ${chart.difficulty} ${
|
||||
chart.level
|
||||
}, ${uscChart.data.tableFolders.map((e) => `${e.table}${e.level}`).join(", ")})`;
|
||||
}
|
||||
|
||||
// otherwise, it's just an official and should be rendered like any other game.
|
||||
} else if (game === "itg") {
|
||||
const itgChart = chart as ChartDocument<"itg:Stamina">;
|
||||
const itgSong = song as SongDocument<"itg">;
|
||||
|
||||
return `${itgSong.title}${itgSong.data.subtitle ? ` ${itgSong.data.subtitle}` : ""} ${
|
||||
itgChart.data.difficultyTag
|
||||
} ${chart.level}`;
|
||||
}
|
||||
|
||||
const gameConfig = GetGameConfig(game);
|
||||
|
||||
let playtypeStr = `${chart.playtype} `;
|
||||
|
||||
if (gameConfig.validPlaytypes.length === 1) {
|
||||
playtypeStr = "";
|
||||
}
|
||||
|
||||
// return the most recent version this chart appeared in if it
|
||||
// is not primary.
|
||||
if (!chart.isPrimary) {
|
||||
return `${song.title} (${playtypeStr}${chart.difficulty} ${chart.level} ${chart.versions[0]})`;
|
||||
}
|
||||
|
||||
return `${song.title} (${playtypeStr}${chart.difficulty} ${chart.level})`;
|
||||
}
|
||||
|
||||
export function AbsoluteGradeDelta<I extends IDStrings = IDStrings>(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
score: number,
|
||||
percent: number,
|
||||
gradeOrIndex: Grades[I] | integer
|
||||
): number {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
if (!gptConfig.gradeBoundaries) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const maxScore = Math.round(score * (100 / percent));
|
||||
|
||||
const gradeIndex =
|
||||
typeof gradeOrIndex === "number" ? gradeOrIndex : gptConfig.grades.indexOf(gradeOrIndex);
|
||||
|
||||
const gradeScore = Math.ceil((gptConfig.gradeBoundaries[gradeIndex] / 100) * maxScore);
|
||||
|
||||
return score - gradeScore;
|
||||
}
|
||||
|
||||
export function RelativeGradeDelta<I extends IDStrings = IDStrings>(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
score: number,
|
||||
percent: number,
|
||||
grade: Grades[I],
|
||||
relativeIndex: integer
|
||||
): { grade: string; delta: number } | null {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const nextGradeIndex = gptConfig.grades.indexOf(grade) + relativeIndex;
|
||||
|
||||
if (nextGradeIndex < 0 || nextGradeIndex >= gptConfig.grades.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
grade: gptConfig.grades[nextGradeIndex],
|
||||
delta: AbsoluteGradeDelta(game, playtype, score, percent, nextGradeIndex),
|
||||
};
|
||||
}
|
||||
|
||||
function WrapGrade(grade: string) {
|
||||
if (grade.endsWith("-") || grade.endsWith("+")) {
|
||||
return `(${grade})`;
|
||||
}
|
||||
|
||||
return grade;
|
||||
}
|
||||
|
||||
export function GenericFormatGradeDelta<I extends IDStrings = IDStrings>(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
score: number,
|
||||
percent: number,
|
||||
grade: Grades[I],
|
||||
formatNumFn: (n: number) => number = (s) => s
|
||||
): {
|
||||
lower: string;
|
||||
upper?: string;
|
||||
closer: "lower" | "upper";
|
||||
} {
|
||||
const upper = RelativeGradeDelta(game, playtype, score, percent, grade, 1);
|
||||
const lower = AbsoluteGradeDelta(game, playtype, score, percent, grade);
|
||||
|
||||
const formatLower = `${WrapGrade(grade)}+${formatNumFn(lower)}`;
|
||||
|
||||
if (!upper) {
|
||||
return {
|
||||
lower: formatLower,
|
||||
closer: "lower",
|
||||
};
|
||||
}
|
||||
|
||||
const formatUpper = `${WrapGrade(upper.grade)}${formatNumFn(upper.delta)}`;
|
||||
|
||||
return {
|
||||
lower: formatLower,
|
||||
upper: formatUpper,
|
||||
closer: upper.delta + lower < 0 ? "lower" : "upper",
|
||||
};
|
||||
}
|
||||
|
||||
export function FormatSieglindeBMS(sgl: number): string {
|
||||
const fixedSgl = sgl.toFixed(2);
|
||||
|
||||
if (sgl < 13) {
|
||||
return `${fixedSgl} (☆${fixedSgl})`;
|
||||
}
|
||||
|
||||
return `${fixedSgl} (🟊${(sgl - 12).toFixed(2)})`;
|
||||
}
|
||||
|
||||
export function FormatSieglindePMS(sgl: number): string {
|
||||
const fixedSgl = sgl.toFixed(2);
|
||||
|
||||
if (sgl < 13) {
|
||||
return `${fixedSgl} (○${fixedSgl})`;
|
||||
}
|
||||
|
||||
return `${fixedSgl} (●${(sgl - 12).toFixed(2)})`;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { Game, Playtypes } from ".";
|
||||
import type { GameClassSets } from "./game-classes";
|
||||
import type { GoalImportStat, IDStrings, integer, MilestoneImportStat } from "./types";
|
||||
|
||||
/**
|
||||
* An event fired when a users class improves.
|
||||
*/
|
||||
export interface WebhookEventClassUpdateV1 {
|
||||
type: "class-update/v1";
|
||||
content: {
|
||||
userID: integer;
|
||||
set: GameClassSets[IDStrings];
|
||||
old: integer | null;
|
||||
new: integer;
|
||||
game: Game;
|
||||
playtype: Playtypes[Game];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* An event fired when a goal is achieved.
|
||||
*/
|
||||
export interface WebhookEventGoalAchievedV1 {
|
||||
type: "goals-achieved/v1";
|
||||
content: {
|
||||
userID: integer;
|
||||
game: Game;
|
||||
goals: Array<{
|
||||
goalID: string;
|
||||
old: GoalImportStat;
|
||||
new: GoalImportStat;
|
||||
playtype: Playtypes[Game];
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* An event fired when a milestone is achieved.
|
||||
*/
|
||||
export interface WebhookEventMilestoneAchievedV1 {
|
||||
type: "milestone-achieved/v1";
|
||||
content: {
|
||||
userID: integer;
|
||||
milestoneID: string;
|
||||
old: MilestoneImportStat;
|
||||
new: MilestoneImportStat;
|
||||
game: Game;
|
||||
playtype: Playtypes[Game];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* An event used for debugging. Contains information about the
|
||||
* registered client and the server.
|
||||
*/
|
||||
export interface WebhookEventStatusV1 {
|
||||
type: "status/v1";
|
||||
content: {
|
||||
clientName: string;
|
||||
clientID: string;
|
||||
serverVersion: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type WebhookEvents =
|
||||
| WebhookEventClassUpdateV1
|
||||
| WebhookEventGoalAchievedV1
|
||||
| WebhookEventMilestoneAchievedV1;
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"module": "commonjs",
|
||||
"lib": [
|
||||
"ES2019"
|
||||
],
|
||||
"declaration": true,
|
||||
"outDir": "./js",
|
||||
"strict": true,
|
||||
"allowJs": false,
|
||||
"esModuleInterop": true,
|
||||
"typeRoots": [
|
||||
"@types"
|
||||
],
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.test.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/legacy/**/*.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user