Merge branch 'common-master'

This commit is contained in:
zkldi
2022-07-04 02:02:58 +01:00
23 changed files with 9921 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules
js
+34
View File
@@ -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"
}
}
]
}
+7
View File
@@ -0,0 +1,7 @@
/node_modules
/.vscode
.nyc_output
coverage
/js
+10
View File
@@ -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
+21
View File
@@ -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.
+3
View File
@@ -0,0 +1,3 @@
# Tachi-common
Common Types (and code) for Tachi. These are shared between the client and the server implementations.
+45
View File
@@ -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"
]
}
}
+3240
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -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
+174
View File
@@ -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": {},
};
+172
View File
@@ -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,
}
+14
View File
@@ -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,
};
+359
View File
@@ -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;
};
+12
View File
@@ -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();
});
+11
View File
@@ -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";
+46
View File
@@ -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();
});
+188
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+245
View File
@@ -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)})`;
}
+68
View File
@@ -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;
+25
View File
@@ -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"
]
}