mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-02 20:08:17 +03:00
throwing in the towel
This commit is contained in:
@@ -27,7 +27,7 @@ const SDVXDans = [
|
||||
ClassValue("INF", "LV.INF", "Inf. Dan"),
|
||||
];
|
||||
|
||||
const SDVXVFClasses = [
|
||||
export const SDVXVFClasses = [
|
||||
ClassValue("SIENNA_I", "Sienna I", "0 - 2.499VF"),
|
||||
ClassValue("SIENNA_II", "Sienna II", "2.5 - 4.999VF"),
|
||||
ClassValue("SIENNA_III", "Sienna III", "5.0 - 7.499VF"),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FAST_SLOW_MAXCOMBO } from "./_common";
|
||||
import { SDVXVFClasses } from "./sdvx";
|
||||
import { ToDecimalPlaces } from "../config-utils";
|
||||
import { z } from "zod";
|
||||
import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals";
|
||||
@@ -73,7 +74,12 @@ export const USC_CONTROLLER_CONF = {
|
||||
default: "EXH",
|
||||
},
|
||||
|
||||
classes: {},
|
||||
classes: {
|
||||
vfClass: {
|
||||
type: "DERIVED",
|
||||
values: SDVXVFClasses,
|
||||
},
|
||||
},
|
||||
|
||||
orderedJudgements: ["critical", "near", "miss"],
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import type {
|
||||
AnyClasses,
|
||||
ChartDocumentData,
|
||||
Classes,
|
||||
DerivedMetrics,
|
||||
Difficulties,
|
||||
ExtractedClasses,
|
||||
GPTString,
|
||||
GPTStringToGame,
|
||||
GPTStringToPlaytype,
|
||||
GPTStrings,
|
||||
Game,
|
||||
Judgements,
|
||||
OptionalMetrics,
|
||||
@@ -42,20 +45,20 @@ export interface ChartFolderLookupDocument {
|
||||
folderID: string;
|
||||
}
|
||||
|
||||
export interface BaseGoalDocument {
|
||||
export interface BaseGoalDocument<GPT extends GPTString> {
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
name: string;
|
||||
goalID: string;
|
||||
criteria: GoalCountCriteria | GoalSingleCriteria;
|
||||
criteria: GoalCountCriteria<GPT> | GoalSingleCriteria<GPT>;
|
||||
}
|
||||
|
||||
interface GoalCriteria {
|
||||
key: "scoreData.gradeIndex" | "scoreData.lampIndex" | "scoreData.percent" | "scoreData.score";
|
||||
interface GoalCriteria<GPT extends GPTString> {
|
||||
key: keyof DerivedMetrics[GPT] | keyof ProvidedMetrics[GPT];
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface GoalSingleCriteria extends GoalCriteria {
|
||||
export interface GoalSingleCriteria<GPT extends GPTString> extends GoalCriteria<GPT> {
|
||||
mode: "single";
|
||||
}
|
||||
|
||||
@@ -63,7 +66,7 @@ export interface GoalSingleCriteria extends GoalCriteria {
|
||||
* Criteria for a score to match this criteria - this is a "count" mode, which means that
|
||||
* atleast N scores have to match this criteria. This is for things like folders.
|
||||
*/
|
||||
export interface GoalCountCriteria extends GoalCriteria {
|
||||
export interface GoalCountCriteria<GPT extends GPTString> extends GoalCriteria<GPT> {
|
||||
mode: "absolute" | "proportion";
|
||||
countNum: number;
|
||||
}
|
||||
@@ -71,7 +74,8 @@ export interface GoalCountCriteria extends GoalCriteria {
|
||||
/**
|
||||
* Goal Document - Single. A goal document that is only for one specific chart.
|
||||
*/
|
||||
export interface GoalDocumentSingle extends BaseGoalDocument {
|
||||
export interface GoalDocumentSingle<GPT extends GPTString = GPTString>
|
||||
extends BaseGoalDocument<GPT> {
|
||||
charts: {
|
||||
type: "single";
|
||||
data: string;
|
||||
@@ -82,7 +86,8 @@ export interface GoalDocumentSingle extends BaseGoalDocument {
|
||||
* Goal Document - Multi. A goal document whos set of charts is the array of
|
||||
* chartIDs inside "charts".
|
||||
*/
|
||||
export interface GoalDocumentMulti extends BaseGoalDocument {
|
||||
export interface GoalDocumentMulti<GPT extends GPTString = GPTString>
|
||||
extends BaseGoalDocument<GPT> {
|
||||
charts: {
|
||||
type: "multi";
|
||||
data: Array<string>;
|
||||
@@ -93,14 +98,18 @@ export interface GoalDocumentMulti extends BaseGoalDocument {
|
||||
* Goal Document - Folder. A goal document whos set of charts is derived from
|
||||
* the folderID inside "charts".
|
||||
*/
|
||||
export interface GoalDocumentFolder extends BaseGoalDocument {
|
||||
export interface GoalDocumentFolder<GPT extends GPTString = GPTString>
|
||||
extends BaseGoalDocument<GPT> {
|
||||
charts: {
|
||||
type: "folder";
|
||||
data: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type GoalDocument = GoalDocumentFolder | GoalDocumentMulti | GoalDocumentSingle;
|
||||
export type GoalDocument<GPT extends GPTString = GPTString> =
|
||||
| GoalDocumentFolder<GPT>
|
||||
| GoalDocumentMulti<GPT>
|
||||
| GoalDocumentSingle<GPT>;
|
||||
|
||||
interface BaseInviteCodeDocument {
|
||||
createdBy: integer;
|
||||
@@ -164,7 +173,7 @@ export interface ImportDocument {
|
||||
timeFinished: number;
|
||||
|
||||
// Contains an array of GPTString, which dictates what (game:playtype)s were involved in this import.
|
||||
GPTString: Array<GPTString>;
|
||||
gptStrings: Array<GPTString>;
|
||||
importID: string;
|
||||
scoreIDs: Array<string>;
|
||||
game: Game;
|
||||
@@ -313,12 +322,24 @@ export interface UserDocument {
|
||||
authLevel: UserAuthLevels;
|
||||
}
|
||||
|
||||
export interface UserGameStats<GPT extends GPTString = GPTString> {
|
||||
export interface SpecificUserGameStats<GPT extends GPTString> {
|
||||
userID: integer;
|
||||
game: GPTStringToGame[GPT];
|
||||
playtype: GPTStringToPlaytype[GPT];
|
||||
ratings: Partial<Record<ProfileRatingAlgorithms[GPT], number | null>>;
|
||||
classes: Partial<Classes[GPT]>;
|
||||
classes: Partial<ExtractedClasses[GPT]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* GPT agnostic stats for a game. This type is easier to work with than the
|
||||
* specificUserGameStats one for general cases.
|
||||
*/
|
||||
export interface UserGameStats {
|
||||
userID: integer;
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
ratings: Partial<Record<ProfileRatingAlgorithms[GPTString], number | null>>;
|
||||
classes: AnyClasses;
|
||||
}
|
||||
|
||||
export interface ChartTierlistInfo {
|
||||
@@ -548,15 +569,19 @@ export interface CGCardInfo {
|
||||
pin: string;
|
||||
}
|
||||
|
||||
interface BMSCourseInner<GPT extends GPTStrings["bms"], Set extends keyof ExtractedClasses[GPT]> {
|
||||
set: Set;
|
||||
playtype: GPTStringToPlaytype[GPT];
|
||||
value: ExtractedClasses[GPT][Set];
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to resolve beatoraja IR courses.
|
||||
*/
|
||||
export interface BMSCourseDocument {
|
||||
export interface BMSCourseDocument
|
||||
extends BMSCourseInner<GPTStrings["bms"], keyof ExtractedClasses[GPTStrings["bms"]]> {
|
||||
title: string;
|
||||
md5sums: string;
|
||||
set: "genocideDan" | "lnDan" | "scratchDan" | "stslDan";
|
||||
playtype: "7K" | "14K";
|
||||
value: integer;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -610,7 +635,7 @@ export interface UGPTSettingsDocument<GPT extends GPTString = GPTString> {
|
||||
}
|
||||
|
||||
export interface UserGameStatsSnapshotDocument<GPT extends GPTString = GPTString>
|
||||
extends UserGameStats<GPT> {
|
||||
extends SpecificUserGameStats<GPT> {
|
||||
rankings: Record<ProfileRatingAlgorithms[GPT], { ranking: integer | null; outOf: integer }>;
|
||||
playcount: integer;
|
||||
timestamp: integer;
|
||||
@@ -662,16 +687,16 @@ export interface ClassDelta {
|
||||
game: Game;
|
||||
set: Classes[GPTString];
|
||||
playtype: Playtype;
|
||||
old: integer | null;
|
||||
new: integer;
|
||||
old: string | null;
|
||||
new: string;
|
||||
}
|
||||
|
||||
export interface ClassAchievementDocument<GPT extends GPTString = GPTString> {
|
||||
game: GPTStringToGame[GPT];
|
||||
playtype: GPTStringToPlaytype[GPT];
|
||||
classSet: Classes[GPT];
|
||||
classOldValue: integer | null;
|
||||
classValue: integer;
|
||||
classOldValue: string | null;
|
||||
classValue: string;
|
||||
timeAchieved: number;
|
||||
userID: integer;
|
||||
}
|
||||
|
||||
@@ -21,13 +21,13 @@ export interface ClassInfo<ClassID extends string> {
|
||||
hoverText?: string;
|
||||
}
|
||||
|
||||
interface BaseClassConfig {
|
||||
interface BaseClassConfig<V extends string> {
|
||||
/**
|
||||
* What are the possible values for this class field?
|
||||
*
|
||||
* @note This should be in ascending order.
|
||||
*/
|
||||
values: Array<ClassInfo<string>>;
|
||||
values: Array<ClassInfo<V>>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,7 +42,7 @@ interface BaseClassConfig {
|
||||
* the user is 3rd dan when they've cleared 5th dan in the past, this value will not go
|
||||
* back down at any point.
|
||||
*/
|
||||
export interface ProvidedClassConfig extends BaseClassConfig {
|
||||
export interface ProvidedClassConfig<V extends string = string> extends BaseClassConfig<V> {
|
||||
type: "PROVIDED";
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export interface ProvidedClassConfig extends BaseClassConfig {
|
||||
* @note "DERIVED" classes are always downgradable, as they are a function of state
|
||||
* and might go down at any time for any reason.
|
||||
*/
|
||||
export interface DerivedClassConfig extends BaseClassConfig {
|
||||
export interface DerivedClassConfig<V extends string = string> extends BaseClassConfig<V> {
|
||||
type: "DERIVED";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { GAME_CONFIGS, GAME_PT_CONFIGS } from "../config/config";
|
||||
import type { integer } from "../types";
|
||||
import type { MatchTypes } from "./batch-manual";
|
||||
import type {
|
||||
ExtractClassValues,
|
||||
@@ -147,12 +146,16 @@ export type ExtractedClasses = {
|
||||
[G in GPTString]: ExtractClassValues<typeof GAME_PT_CONFIGS[G]["classes"]>;
|
||||
};
|
||||
|
||||
export type AnyClasses = {
|
||||
[C in Classes[GPTString]]?: string | null;
|
||||
};
|
||||
|
||||
export type Classes = {
|
||||
[G in GPTString]: keyof typeof GAME_PT_CONFIGS[G]["classes"];
|
||||
};
|
||||
|
||||
export type ClassRecords<G extends GPTString> = {
|
||||
[C in Classes[G]]: integer;
|
||||
export type ClassConfigs = {
|
||||
[G in GPTString]: typeof GAME_PT_CONFIGS[G]["classes"];
|
||||
};
|
||||
|
||||
export type ChartDocumentData = {
|
||||
@@ -300,7 +303,7 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
|
||||
* or they may be provided by score imports, such as "dans", which cannot be
|
||||
* derived from a player's scores or profile ratings.
|
||||
*/
|
||||
classes: Classes[GPT];
|
||||
classes: ClassConfigs[GPT];
|
||||
|
||||
/**
|
||||
* What's the default score rating algorithm for this GPT?
|
||||
|
||||
@@ -9,8 +9,8 @@ export interface WebhookEventClassUpdateV1 {
|
||||
content: {
|
||||
userID: integer;
|
||||
set: Classes[GPTString];
|
||||
old: integer | null;
|
||||
new: integer;
|
||||
old: string | null;
|
||||
new: string;
|
||||
game: Game;
|
||||
playtype: Playtypes[Game];
|
||||
};
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
import type { ClassDeriver, GPTClassDerivers } from "./types";
|
||||
import type { ExtractedClasses, GPTString, UserGameStats } from "tachi-common";
|
||||
|
||||
function VF6ToClass(vf: number): UserGameStats<"sdvx:Single">["classes"]["vfClass"] {
|
||||
// jesus christ man
|
||||
if (vf >= 23) {
|
||||
return "IMPERIAL_IV";
|
||||
} else if (vf >= 22) {
|
||||
return "IMPERIAL_III";
|
||||
} else if (vf >= 21) {
|
||||
return "IMPERIAL_II";
|
||||
} else if (vf >= 20) {
|
||||
return "IMPERIAL_I";
|
||||
} else if (vf >= 19.75) {
|
||||
return "CRIMSON_IV";
|
||||
} else if (vf > 19.5) {
|
||||
return "CRIMSON_III";
|
||||
} else if (vf >= 19.25) {
|
||||
return "CRIMSON_II";
|
||||
} else if (vf >= 19) {
|
||||
return "CRIMSON_I";
|
||||
} else if (vf >= 18.75) {
|
||||
return "ELDORA_IV";
|
||||
} else if (vf >= 18.5) {
|
||||
return "ELDORA_III";
|
||||
} else if (vf >= 18.25) {
|
||||
return "ELDORA_II";
|
||||
} else if (vf >= 18) {
|
||||
return "ELDORA_I";
|
||||
} else if (vf >= 17.75) {
|
||||
return "ARGENTO_IV";
|
||||
} else if (vf >= 17.5) {
|
||||
return "ARGENTO_III";
|
||||
} else if (vf >= 17.25) {
|
||||
return "ARGENTO_II";
|
||||
} else if (vf >= 17) {
|
||||
return "ARGENTO_I";
|
||||
} else if (vf >= 16.75) {
|
||||
return "CORAL_IV";
|
||||
} else if (vf >= 16.5) {
|
||||
return "CORAL_III";
|
||||
} else if (vf >= 16.25) {
|
||||
return "CORAL_II";
|
||||
} else if (vf >= 16) {
|
||||
return "CORAL_I";
|
||||
} else if (vf >= 15.75) {
|
||||
return "SCARLET_IV";
|
||||
} else if (vf >= 15.5) {
|
||||
return "SCARLET_III";
|
||||
} else if (vf >= 15.25) {
|
||||
return "SCARLET_II";
|
||||
} else if (vf >= 15) {
|
||||
return "SCARLET_I";
|
||||
} else if (vf >= 14.75) {
|
||||
return "CYAN_IV";
|
||||
} else if (vf >= 14.5) {
|
||||
return "CYAN_III";
|
||||
} else if (vf >= 14.25) {
|
||||
return "CYAN_II";
|
||||
} else if (vf >= 14) {
|
||||
return "CYAN_I";
|
||||
} else if (vf >= 13.5) {
|
||||
return "DANDELION_IV";
|
||||
} else if (vf >= 13) {
|
||||
return "DANDELION_III";
|
||||
} else if (vf >= 12.5) {
|
||||
return "DANDELION_II";
|
||||
} else if (vf >= 12) {
|
||||
return "DANDELION_I";
|
||||
} else if (vf >= 11.5) {
|
||||
return "COBALT_IV";
|
||||
} else if (vf >= 11) {
|
||||
return "COBALT_III";
|
||||
} else if (vf >= 10.5) {
|
||||
return "COBALT_II";
|
||||
} else if (vf >= 10) {
|
||||
return "COBALT_I";
|
||||
} else if (vf >= 7.5) {
|
||||
return "SIENNA_IV";
|
||||
} else if (vf >= 5) {
|
||||
return "SIENNA_III";
|
||||
} else if (vf >= 2.5) {
|
||||
return "SIENNA_II";
|
||||
}
|
||||
|
||||
return "SIENNA_I";
|
||||
}
|
||||
|
||||
function GitadoraSkillToColour(sk: number) {
|
||||
if (sk >= 8500) {
|
||||
return "RAINBOW";
|
||||
} else if (sk >= 8000) {
|
||||
return "GOLD";
|
||||
} else if (sk >= 7500) {
|
||||
return "SILVER";
|
||||
} else if (sk >= 7000) {
|
||||
return "BRONZE";
|
||||
} else if (sk >= 6500) {
|
||||
return "RED_GRD";
|
||||
} else if (sk >= 6000) {
|
||||
return "RED";
|
||||
} else if (sk >= 5500) {
|
||||
return "PURPLE_GRD";
|
||||
} else if (sk >= 5000) {
|
||||
return "PURPLE";
|
||||
} else if (sk >= 4500) {
|
||||
return "BLUE_GRD";
|
||||
} else if (sk >= 4000) {
|
||||
return "BLUE";
|
||||
} else if (sk >= 3500) {
|
||||
return "GREEN_GRD";
|
||||
} else if (sk >= 3000) {
|
||||
return "GREEN";
|
||||
} else if (sk >= 2500) {
|
||||
return "YELLOW_GRD";
|
||||
} else if (sk >= 2000) {
|
||||
return "YELLOW";
|
||||
} else if (sk >= 1500) {
|
||||
return "ORANGE_GRD";
|
||||
} else if (sk >= 1000) {
|
||||
return "ORANGE";
|
||||
}
|
||||
|
||||
return "WHITE";
|
||||
}
|
||||
|
||||
export const CLASS_DERIVERS: GPTClassDerivers = {
|
||||
"bms:14K": {},
|
||||
"bms:7K": {},
|
||||
"iidx:DP": {},
|
||||
"iidx:SP": {},
|
||||
"itg:Stamina": {},
|
||||
|
||||
"museca:Single": {},
|
||||
"pms:Controller": {},
|
||||
"pms:Keyboard": {},
|
||||
|
||||
"chunithm:Single": {
|
||||
colour: (ratings) => {
|
||||
const rating = ratings.naiveRating;
|
||||
|
||||
if (rating === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rating >= 15) {
|
||||
return "RAINBOW";
|
||||
} else if (rating >= 14.5) {
|
||||
return "PLATINUM";
|
||||
} else if (rating >= 14) {
|
||||
return "GOLD";
|
||||
} else if (rating >= 13) {
|
||||
return "SILVER";
|
||||
} else if (rating >= 12) {
|
||||
return "COPPER";
|
||||
} else if (rating >= 10) {
|
||||
return "PURPLE";
|
||||
} else if (rating >= 7) {
|
||||
return "RED";
|
||||
} else if (rating >= 4) {
|
||||
return "ORANGE";
|
||||
} else if (rating >= 2) {
|
||||
return "GREEN";
|
||||
}
|
||||
|
||||
return "BLUE";
|
||||
},
|
||||
},
|
||||
"gitadora:Dora": {
|
||||
colour: (ratings) => {
|
||||
if (ratings.skill === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return GitadoraSkillToColour(ratings.skill);
|
||||
},
|
||||
},
|
||||
"gitadora:Gita": {
|
||||
colour: (ratings) => {
|
||||
if (ratings.skill === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return GitadoraSkillToColour(ratings.skill);
|
||||
},
|
||||
},
|
||||
|
||||
"jubeat:Single": {
|
||||
colour: (ratings) => {
|
||||
const jubility = ratings.jubility;
|
||||
|
||||
if (jubility === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (jubility >= 9500) {
|
||||
return "GOLD";
|
||||
} else if (jubility >= 8500) {
|
||||
return "ORANGE";
|
||||
} else if (jubility >= 7000) {
|
||||
return "PINK";
|
||||
} else if (jubility >= 5500) {
|
||||
return "PURPLE";
|
||||
} else if (jubility >= 4000) {
|
||||
return "VIOLET";
|
||||
} else if (jubility >= 2500) {
|
||||
return "BLUE";
|
||||
} else if (jubility >= 1500) {
|
||||
return "LIGHT_BLUE";
|
||||
} else if (jubility >= 750) {
|
||||
return "GREEN";
|
||||
} else if (jubility >= 250) {
|
||||
return "YELLOW_GREEN";
|
||||
}
|
||||
|
||||
return "BLACK";
|
||||
},
|
||||
},
|
||||
"maimaidx:Single": {
|
||||
colour: (ratings) => {
|
||||
const rate = ratings.rate;
|
||||
|
||||
if (rate === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rate >= 15000) {
|
||||
return "RAINBOW";
|
||||
} else if (rate >= 14500) {
|
||||
return "PLATINUM";
|
||||
} else if (rate >= 14000) {
|
||||
return "GOLD";
|
||||
} else if (rate >= 13000) {
|
||||
return "SILVER";
|
||||
} else if (rate >= 12000) {
|
||||
return "BRONZE";
|
||||
} else if (rate >= 10000) {
|
||||
return "PURPLE";
|
||||
} else if (rate >= 7000) {
|
||||
return "RED";
|
||||
} else if (rate >= 4000) {
|
||||
return "YELLOW";
|
||||
} else if (rate >= 2000) {
|
||||
return "GREEN";
|
||||
} else if (rate >= 1000) {
|
||||
return "BLUE";
|
||||
}
|
||||
|
||||
return "WHITE";
|
||||
},
|
||||
},
|
||||
|
||||
"popn:9B": {
|
||||
class: (ratings) => {
|
||||
const points = ratings.naiveClassPoints;
|
||||
|
||||
if (points === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (points < 21) {
|
||||
return "KITTY";
|
||||
} else if (points < 34) {
|
||||
return "STUDENT";
|
||||
} else if (points < 46) {
|
||||
return "DELINQUENT";
|
||||
} else if (points < 59) {
|
||||
return "DETECTIVE";
|
||||
} else if (points < 68) {
|
||||
return "IDOL";
|
||||
} else if (points < 79) {
|
||||
return "GENERAL";
|
||||
} else if (points < 91) {
|
||||
return "HERMIT";
|
||||
}
|
||||
|
||||
return "GOD";
|
||||
},
|
||||
},
|
||||
"sdvx:Single": {
|
||||
vfClass: (ratings) => {
|
||||
const vf6 = ratings.VF6;
|
||||
|
||||
if (vf6 === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return VF6ToClass(vf6);
|
||||
},
|
||||
},
|
||||
"usc:Controller": {
|
||||
vfClass: (ratings) => {
|
||||
const vf6 = ratings.VF6;
|
||||
|
||||
if (vf6 === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return VF6ToClass(vf6);
|
||||
},
|
||||
},
|
||||
"usc:Keyboard": {
|
||||
vfClass: (ratings) => {
|
||||
const vf6 = ratings.VF6;
|
||||
|
||||
if (vf6 === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return VF6ToClass(vf6);
|
||||
},
|
||||
},
|
||||
|
||||
"wacca:Single": {
|
||||
colour: (ratings) => {
|
||||
const rate = ratings.rate;
|
||||
|
||||
if (rate === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rate >= 2500) {
|
||||
return "RAINBOW";
|
||||
} else if (rate >= 2200) {
|
||||
return "GOLD";
|
||||
} else if (rate >= 1900) {
|
||||
return "SILVER";
|
||||
} else if (rate >= 1600) {
|
||||
return "BLUE";
|
||||
} else if (rate >= 1300) {
|
||||
return "PURPLE";
|
||||
} else if (rate >= 1000) {
|
||||
return "RED";
|
||||
} else if (rate >= 600) {
|
||||
return "YELLOW";
|
||||
} else if (rate >= 300) {
|
||||
return "NAVY";
|
||||
}
|
||||
|
||||
return "ASH";
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function CalculateDerivedClasses<GPT extends GPTString>(
|
||||
gptString: GPT,
|
||||
profileRatings: UserGameStats<GPT>["ratings"]
|
||||
) {
|
||||
const derivedClasses: Record<string, string> = {};
|
||||
|
||||
for (const [key, fn] of Object.entries(CLASS_DERIVERS[gptString])) {
|
||||
const deriver = fn as ClassDeriver<GPT, any>;
|
||||
|
||||
derivedClasses[key] = deriver(profileRatings);
|
||||
}
|
||||
|
||||
return derivedClasses as Partial<ExtractedClasses[GPT]>;
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
import db from "external/mongo/db";
|
||||
import { GetGPTString } from "tachi-common";
|
||||
import type { GPTProfileCalculators } from "./types";
|
||||
import type {
|
||||
GPTString,
|
||||
Game,
|
||||
PBScoreDocument,
|
||||
Playtype,
|
||||
ScoreRatingAlgorithms,
|
||||
Versions,
|
||||
integer,
|
||||
UserGameStats,
|
||||
} from "tachi-common";
|
||||
|
||||
/**
|
||||
* Curries a function that returns the sum of N best ratings on `key`.
|
||||
*
|
||||
* @param key - What rating value to sum.
|
||||
* @param n - The amount of rating values to pull.
|
||||
* @param returnMean - Optionally, if true, return the sum of these values divided by N.
|
||||
* @param nullIfNotEnoughScores - If true, return null if the total scores this user has is less than N.
|
||||
*
|
||||
* @returns - Number if the user has scores with that rating algorithm, null if they have
|
||||
* no scores with this rating algorithm that are non-null.
|
||||
*/
|
||||
function CalcN<GPT extends GPTString>(
|
||||
key: ScoreRatingAlgorithms[GPT],
|
||||
n: integer,
|
||||
returnMean = false,
|
||||
nullIfNotEnoughScores = false
|
||||
) {
|
||||
return async (game: Game, playtype: Playtype, userID: integer) => {
|
||||
const sc = await db["personal-bests"].find(
|
||||
{
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
isPrimary: true,
|
||||
// @ts-expect-error what? this can't be a symbol.
|
||||
[`calculatedData.${key}`]: { $type: "number" },
|
||||
},
|
||||
{
|
||||
limit: n,
|
||||
// @ts-expect-error what? this can't be a symbol.
|
||||
sort: { [`calculatedData.${key}`]: -1 },
|
||||
}
|
||||
);
|
||||
|
||||
if (sc.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (nullIfNotEnoughScores && sc.length < n) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let result = sc.reduce((a, e) => a + e.calculatedData[key]!, 0);
|
||||
|
||||
if (returnMean) {
|
||||
result = result / n;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
function SumBestN<GPT extends GPTString>(
|
||||
key: ScoreRatingAlgorithms[GPT],
|
||||
n: integer,
|
||||
nullIfNotEnoughScores = false
|
||||
) {
|
||||
return CalcN(key, n, false, nullIfNotEnoughScores);
|
||||
}
|
||||
|
||||
function AvgBestN<GPT extends GPTString>(
|
||||
key: ScoreRatingAlgorithms[GPT],
|
||||
n: integer,
|
||||
nullIfNotEnoughScores = false
|
||||
) {
|
||||
return CalcN(key, n, true, nullIfNotEnoughScores);
|
||||
}
|
||||
|
||||
// Wacca has a funny algorithm for rate involving gitadora-style latest chart bonuses,
|
||||
async function CalculateWACCARate(game: Game, playtype: Playtype, userID: integer) {
|
||||
const hotChartIDs = (
|
||||
await db.charts.wacca.find({ "data.isHot": true }, { projection: { chartID: 1 } })
|
||||
).map((e) => e.chartID);
|
||||
|
||||
const coldChartIDs = (
|
||||
await db.charts.wacca.find({ "data.isHot": false }, { projection: { chartID: 1 } })
|
||||
).map((e) => e.chartID);
|
||||
|
||||
const best15Hot = await db["personal-bests"].find(
|
||||
{
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
chartID: { $in: hotChartIDs },
|
||||
"calculatedData.rate": { $type: "number" },
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
"calculatedData.rate": -1,
|
||||
},
|
||||
limit: 15,
|
||||
projection: {
|
||||
"calculatedData.rate": 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const best35Cold = await db["personal-bests"].find(
|
||||
{
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
chartID: { $in: coldChartIDs },
|
||||
"calculatedData.rate": { $type: "number" },
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
"calculatedData.rate": -1,
|
||||
},
|
||||
limit: 35,
|
||||
projection: {
|
||||
"calculatedData.rate": 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (best15Hot.length + best35Cold.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
best15Hot.reduce((a, e) => a + e.calculatedData.rate!, 0) +
|
||||
best35Cold.reduce((a, e) => a + e.calculatedData.rate!, 0)
|
||||
);
|
||||
}
|
||||
|
||||
// basically the same as WACCA's.
|
||||
async function CalculateMaimaiDXRate(game: Game, playtype: Playtype, userID: integer) {
|
||||
const newChartIDs = (
|
||||
await db.charts.maimaidx.find({ "data.isLatest": true }, { projection: { chartID: 1 } })
|
||||
).map((e) => e.chartID);
|
||||
|
||||
const oldChartIDs = (
|
||||
await db.charts.maimaidx.find({ "data.isLatest": false }, { projection: { chartID: 1 } })
|
||||
).map((e) => e.chartID);
|
||||
|
||||
const best15New = await db["personal-bests"].find(
|
||||
{
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
chartID: { $in: newChartIDs },
|
||||
"calculatedData.rate": { $type: "number" },
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
"calculatedData.rate": -1,
|
||||
},
|
||||
limit: 15,
|
||||
projection: {
|
||||
"calculatedData.rate": 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const best35Old = await db["personal-bests"].find(
|
||||
{
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
chartID: { $in: oldChartIDs },
|
||||
"calculatedData.rate": { $type: "number" },
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
"calculatedData.rate": -1,
|
||||
},
|
||||
limit: 35,
|
||||
projection: {
|
||||
"calculatedData.rate": 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (best15New.length + best35Old.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
best15New.reduce((a, e) => a + e.calculatedData.rate!, 0) +
|
||||
best35Old.reduce((a, e) => a + e.calculatedData.rate!, 0)
|
||||
);
|
||||
}
|
||||
|
||||
async function GetBestRatingOnSongs(
|
||||
songIDs: Array<integer>,
|
||||
userID: integer,
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
ratingProp: "skill",
|
||||
limit: integer
|
||||
): Promise<Array<PBScoreDocument>> {
|
||||
const r: Array<{ doc: PBScoreDocument }> = await db["personal-bests"].aggregate([
|
||||
{
|
||||
$match: {
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
songID: { $in: songIDs },
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
[`calculatedData.${ratingProp}`]: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$songID",
|
||||
doc: { $first: "$$ROOT" },
|
||||
},
|
||||
},
|
||||
|
||||
// for some godforsaken reason you have to sort twice. after a grouping
|
||||
// the sort order becomes nondeterministic
|
||||
{
|
||||
$sort: {
|
||||
[`doc.calculatedData.${ratingProp}`]: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$limit: limit,
|
||||
},
|
||||
]);
|
||||
|
||||
return r.map((e) => e.doc);
|
||||
}
|
||||
|
||||
async function GetBestJubilityOnSongs(
|
||||
songIDs: Array<integer>,
|
||||
userID: integer,
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
limit: integer
|
||||
): Promise<Array<PBScoreDocument>> {
|
||||
const r: Array<{ doc: PBScoreDocument }> = await db["personal-bests"].aggregate([
|
||||
{
|
||||
$match: {
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
songID: { $in: songIDs },
|
||||
},
|
||||
},
|
||||
{
|
||||
// we need to do stuff dependent on the chart difficulty,
|
||||
// so we need the chart difficulty
|
||||
$lookup: {
|
||||
from: "charts-jubeat",
|
||||
localField: "chartID",
|
||||
foreignField: "chartID",
|
||||
as: "chart",
|
||||
},
|
||||
},
|
||||
{
|
||||
// "chart" is an array unless we unwind it.
|
||||
$unwind: {
|
||||
path: "$chart",
|
||||
},
|
||||
},
|
||||
{
|
||||
// sort on jubility (so we get the best score)
|
||||
$sort: {
|
||||
[`calculatedData.jubility`]: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
songID: "$songID",
|
||||
|
||||
// Jubility is unique upon songID + difficulty. However, you
|
||||
// cannot have a PB on both a HARD BSC and a BSC counted for
|
||||
// jubility. This query is awkward. Sorry!
|
||||
difficulty: {
|
||||
$switch: {
|
||||
branches: [
|
||||
{
|
||||
case: { $in: ["$chart.difficulty", ["HARD BSC", "BSC"]] },
|
||||
then: "BSC",
|
||||
},
|
||||
{
|
||||
case: { $in: ["$chart.difficulty", ["HARD ADV", "ADV"]] },
|
||||
then: "ADV",
|
||||
},
|
||||
{
|
||||
case: { $in: ["$chart.difficulty", ["HARD EXT", "EXT"]] },
|
||||
then: "EXT",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
doc: { $first: "$$ROOT" },
|
||||
},
|
||||
},
|
||||
|
||||
// for some godforsaken reason you have to sort twice. after a grouping
|
||||
// the sort order becomes nondeterministic
|
||||
{
|
||||
$sort: {
|
||||
[`doc.calculatedData.jubility`]: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$limit: limit,
|
||||
},
|
||||
]);
|
||||
|
||||
return r.map((e) => e.doc);
|
||||
}
|
||||
|
||||
const CURRENT_JUBEAT_HOT_VERSION: Versions["jubeat:Single"] = "festo";
|
||||
|
||||
async function GetPBsForJubility(userID: integer) {
|
||||
const hotSongs = await db.songs.jubeat.find(
|
||||
{ "data.displayVersion": CURRENT_JUBEAT_HOT_VERSION },
|
||||
{ projection: { id: 1 } }
|
||||
);
|
||||
|
||||
const hotSongIDs = hotSongs.map((e) => e.id);
|
||||
|
||||
const coldSongs = await db.songs.jubeat.find(
|
||||
{ "data.displayVersion": { $ne: CURRENT_JUBEAT_HOT_VERSION } },
|
||||
{ projection: { id: 1 } }
|
||||
);
|
||||
|
||||
const coldSongIDs = coldSongs.map((e) => e.id);
|
||||
|
||||
const [bestHotScores, bestScores] = await Promise.all([
|
||||
GetBestJubilityOnSongs(hotSongIDs, userID, "jubeat", "Single", 30),
|
||||
GetBestJubilityOnSongs(coldSongIDs, userID, "jubeat", "Single", 30),
|
||||
]);
|
||||
|
||||
return { bestHotScores, bestScores };
|
||||
}
|
||||
|
||||
async function CalculateJubility(game: Game, playtype: Playtype, userID: integer): Promise<number> {
|
||||
const { bestHotScores, bestScores } = await GetPBsForJubility(userID);
|
||||
|
||||
let jubility = 0;
|
||||
|
||||
jubility = jubility + bestHotScores.reduce((a, e) => a + (e.calculatedData.jubility ?? 0), 0);
|
||||
jubility = jubility + bestScores.reduce((a, e) => a + (e.calculatedData.jubility ?? 0), 0);
|
||||
|
||||
return jubility;
|
||||
}
|
||||
|
||||
async function CalculateGitadoraSkill(game: Game, playtype: Playtype, userID: integer) {
|
||||
const hotSongIDs = (
|
||||
await db.songs.gitadora.find({ "data.isHot": true }, { projection: { id: 1 } })
|
||||
).map((e) => e.id);
|
||||
|
||||
// get it
|
||||
const coldSongIDs = (
|
||||
await db.songs.gitadora.find({ "data.isHot": false }, { projection: { id: 1 } })
|
||||
).map((e) => e.id);
|
||||
|
||||
const [bestHotScores, bestScores] = await Promise.all([
|
||||
GetBestRatingOnSongs(hotSongIDs, userID, game, playtype, "skill", 25),
|
||||
GetBestRatingOnSongs(coldSongIDs, userID, game, playtype, "skill", 25),
|
||||
]);
|
||||
|
||||
if (bestHotScores.length + bestScores.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let skill = 0;
|
||||
|
||||
skill = skill + bestHotScores.reduce((a, e) => a + e.calculatedData.skill!, 0);
|
||||
skill = skill + bestScores.reduce((a, e) => a + e.calculatedData.skill!, 0);
|
||||
|
||||
return skill;
|
||||
}
|
||||
|
||||
export const PROFILE_CALCULATORS: GPTProfileCalculators = {
|
||||
"iidx:SP": {
|
||||
BPI: AvgBestN("BPI", 20, true),
|
||||
ktLampRating: AvgBestN("ktLampRating", 20),
|
||||
},
|
||||
"iidx:DP": {
|
||||
BPI: AvgBestN("BPI", 20, true),
|
||||
ktLampRating: AvgBestN("ktLampRating", 20),
|
||||
},
|
||||
|
||||
"bms:14K": { sieglinde: AvgBestN("sieglinde", 20) },
|
||||
"bms:7K": { sieglinde: AvgBestN("sieglinde", 20) },
|
||||
"pms:Controller": { sieglinde: AvgBestN("sieglinde", 20) },
|
||||
"pms:Keyboard": { sieglinde: AvgBestN("sieglinde", 20) },
|
||||
|
||||
"gitadora:Dora": { skill: CalculateGitadoraSkill, naiveSkill: SumBestN("skill", 50) },
|
||||
"gitadora:Gita": { skill: CalculateGitadoraSkill, naiveSkill: SumBestN("skill", 50) },
|
||||
|
||||
"chunithm:Single": { naiveRating: AvgBestN("rating", 20) },
|
||||
"itg:Stamina": {
|
||||
// the sum of your 1 best blockrating/fastest32 is basically just
|
||||
// picking your best blockrating/fastest32. neat.
|
||||
highestBlock: SumBestN("blockRating", 1, true),
|
||||
fastest32: SumBestN("fastest32", 1, true),
|
||||
},
|
||||
"jubeat:Single": {
|
||||
jubility: CalculateJubility,
|
||||
naiveJubility: SumBestN("jubility", 60),
|
||||
},
|
||||
"maimaidx:Single": {
|
||||
rate: CalculateMaimaiDXRate,
|
||||
naiveRate: SumBestN("rate", 50),
|
||||
},
|
||||
"museca:Single": {
|
||||
curatorSkill: SumBestN("curatorSkill", 20),
|
||||
},
|
||||
"popn:9B": {
|
||||
naiveClassPoints: SumBestN("classPoints", 20),
|
||||
},
|
||||
"sdvx:Single": {
|
||||
VF6: SumBestN("VF6", 50),
|
||||
},
|
||||
"usc:Controller": {
|
||||
VF6: SumBestN("VF6", 50),
|
||||
},
|
||||
"usc:Keyboard": {
|
||||
VF6: SumBestN("VF6", 50),
|
||||
},
|
||||
"wacca:Single": {
|
||||
rate: CalculateWACCARate,
|
||||
naiveRate: SumBestN("rate", 50),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate profile ratings for this UGPT.
|
||||
*/
|
||||
export async function CalculateProfileRatings(game: Game, playtype: Playtype, userID: integer) {
|
||||
const calculatedData: Record<string, number | null> = {};
|
||||
|
||||
const gptString = GetGPTString(game, playtype);
|
||||
|
||||
// Do this in parallel for performance.
|
||||
await Promise.all(
|
||||
Object.entries(PROFILE_CALCULATORS[gptString]).map(async ([key, fn]) => {
|
||||
calculatedData[key] = await fn(game, playtype, userID);
|
||||
})
|
||||
);
|
||||
|
||||
return calculatedData as UserGameStats["ratings"];
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import type { DryScore } from "../common/types";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
import type {
|
||||
GPTString,
|
||||
ChartDocument,
|
||||
ScoreRatingAlgorithms,
|
||||
SessionRatingAlgorithms,
|
||||
ScoreDocument,
|
||||
ClassConfigs,
|
||||
GPTString,
|
||||
GPTStringToGame,
|
||||
GPTStringToPlaytype,
|
||||
ProfileRatingAlgorithms,
|
||||
ScoreDocument,
|
||||
ScoreRatingAlgorithms,
|
||||
SessionRatingAlgorithms,
|
||||
UserGameStats,
|
||||
integer,
|
||||
} from "tachi-common";
|
||||
import type { DerivedClassConfig, ProvidedClassConfig } from "tachi-common/types/game-config-utils";
|
||||
|
||||
export type ScoreCalculator<GPT extends GPTString> = (
|
||||
dryScore: DryScore<GPT>,
|
||||
@@ -37,5 +42,43 @@ export type GPTSessionCalculators = {
|
||||
};
|
||||
|
||||
export type GPTProfileCalculators = {
|
||||
[G in GPTString]: Record<SessionRatingAlgorithms[G], ScoreCalculator<G>>;
|
||||
[G in GPTString]: Record<ProfileRatingAlgorithms[G], ProfileCalculator<G>>;
|
||||
};
|
||||
|
||||
export type ClassDeriver<GPT extends GPTString, V extends string> = (
|
||||
profileRatings: UserGameStats<GPT>["ratings"]
|
||||
) => V | null;
|
||||
|
||||
// absolutely stupid magic.
|
||||
|
||||
// makes a record of all the classes that are derivable,
|
||||
// and returns a ClassDeriver function for each: i.e.
|
||||
// wacca has StageUP which is not derivable, and colour (which is)
|
||||
// so this type will result in
|
||||
// {
|
||||
// colour: ClassDeriver<"YELLOW" | "asdf" ...>
|
||||
// }
|
||||
type RecordClassDeriver<GPT extends GPTString> = {
|
||||
[C in keyof ClassConfigs[GPT] as ClassConfigs[GPT][C] extends DerivedClassConfig
|
||||
? C
|
||||
: never]: ClassConfigs[GPT][C] extends DerivedClassConfig<infer V>
|
||||
? ClassDeriver<GPT, V>
|
||||
: never;
|
||||
};
|
||||
|
||||
type RecordClassProvider<GPT extends GPTString> = {
|
||||
[C in keyof ClassConfigs[GPT] as ClassConfigs[GPT][C] extends DerivedClassConfig
|
||||
? C
|
||||
: never]: ClassConfigs[GPT][C] extends ProvidedClassConfig<infer V> ? V : never;
|
||||
};
|
||||
|
||||
export type GPTClassDerivers = {
|
||||
[GPT in GPTString]: RecordClassDeriver<GPT>;
|
||||
};
|
||||
|
||||
export type ClassProvider<GPT extends GPTString = GPTString> = (
|
||||
gptString: GPT,
|
||||
userID: integer,
|
||||
ratings: Record<string, number | null>,
|
||||
logger: KtLogger
|
||||
) => Partial<RecordClassProvider<GPT>> | Promise<Partial<RecordClassProvider<GPT>>> | undefined;
|
||||
|
||||
-107
@@ -1,107 +0,0 @@
|
||||
import {
|
||||
CalculateGitadoraColour,
|
||||
GitadoraSkillToColour,
|
||||
CalculateSDVXClass,
|
||||
SDVXVF6ToClass,
|
||||
} from "./builtin-class-handlers";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { GITADORA_COLOURS, SDVX_VF_CLASSES } from "tachi-common";
|
||||
import t from "tap";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#CalculateGitadoraColour", (t) => {
|
||||
t.strictSame(
|
||||
CalculateGitadoraColour("gitadora", "Gita", 1, { skill: 1500 }),
|
||||
{
|
||||
colour: GITADORA_COLOURS.ORANGE_GRADIENT,
|
||||
},
|
||||
"Should wrap the skill colour."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#GitadoraSkillToColour", (t) => {
|
||||
t.equal(GitadoraSkillToColour(0), GITADORA_COLOURS.WHITE);
|
||||
t.equal(GitadoraSkillToColour(999), GITADORA_COLOURS.WHITE);
|
||||
t.equal(GitadoraSkillToColour(1000), GITADORA_COLOURS.ORANGE);
|
||||
t.equal(GitadoraSkillToColour(1500), GITADORA_COLOURS.ORANGE_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(2000), GITADORA_COLOURS.YELLOW);
|
||||
t.equal(GitadoraSkillToColour(2500), GITADORA_COLOURS.YELLOW_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(3000), GITADORA_COLOURS.GREEN);
|
||||
t.equal(GitadoraSkillToColour(3500), GITADORA_COLOURS.GREEN_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(4000), GITADORA_COLOURS.BLUE);
|
||||
t.equal(GitadoraSkillToColour(4500), GITADORA_COLOURS.BLUE_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(5000), GITADORA_COLOURS.PURPLE);
|
||||
t.equal(GitadoraSkillToColour(5500), GITADORA_COLOURS.PURPLE_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(6000), GITADORA_COLOURS.RED);
|
||||
t.equal(GitadoraSkillToColour(6500), GITADORA_COLOURS.RED_GRADIENT);
|
||||
t.equal(GitadoraSkillToColour(7000), GITADORA_COLOURS.BRONZE);
|
||||
t.equal(GitadoraSkillToColour(7500), GITADORA_COLOURS.SILVER);
|
||||
t.equal(GitadoraSkillToColour(8000), GITADORA_COLOURS.GOLD);
|
||||
t.equal(GitadoraSkillToColour(8500), GITADORA_COLOURS.RAINBOW);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#CalculateSDVXClass", (t) => {
|
||||
t.strictSame(CalculateSDVXClass("sdvx", "Single", 1, { VF6: 10 }, logger), {
|
||||
vfClass: SDVX_VF_CLASSES.COBALT_I,
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#SDVXVF6ToClass", (t) => {
|
||||
function f(vf: number) {
|
||||
return SDVXVF6ToClass(vf, logger);
|
||||
}
|
||||
|
||||
t.equal(f(0), SDVX_VF_CLASSES.SIENNA_I);
|
||||
t.equal(f(2.5), SDVX_VF_CLASSES.SIENNA_II);
|
||||
t.equal(f(5), SDVX_VF_CLASSES.SIENNA_III);
|
||||
t.equal(f(7.5), SDVX_VF_CLASSES.SIENNA_IV);
|
||||
t.equal(f(10), SDVX_VF_CLASSES.COBALT_I);
|
||||
t.equal(f(10.5), SDVX_VF_CLASSES.COBALT_II);
|
||||
t.equal(f(11), SDVX_VF_CLASSES.COBALT_III);
|
||||
t.equal(f(11.5), SDVX_VF_CLASSES.COBALT_IV);
|
||||
t.equal(f(12), SDVX_VF_CLASSES.DANDELION_I);
|
||||
t.equal(f(12.5), SDVX_VF_CLASSES.DANDELION_II);
|
||||
t.equal(f(13), SDVX_VF_CLASSES.DANDELION_III);
|
||||
t.equal(f(13.5), SDVX_VF_CLASSES.DANDELION_IV);
|
||||
|
||||
t.equal(f(14), SDVX_VF_CLASSES.CYAN_I);
|
||||
t.equal(f(14.25), SDVX_VF_CLASSES.CYAN_II);
|
||||
t.equal(f(14.5), SDVX_VF_CLASSES.CYAN_III);
|
||||
t.equal(f(14.75), SDVX_VF_CLASSES.CYAN_IV);
|
||||
t.equal(f(15), SDVX_VF_CLASSES.SCARLET_I);
|
||||
t.equal(f(15.25), SDVX_VF_CLASSES.SCARLET_II);
|
||||
t.equal(f(15.5), SDVX_VF_CLASSES.SCARLET_III);
|
||||
t.equal(f(15.75), SDVX_VF_CLASSES.SCARLET_IV);
|
||||
t.equal(f(16), SDVX_VF_CLASSES.CORAL_I);
|
||||
t.equal(f(16.25), SDVX_VF_CLASSES.CORAL_II);
|
||||
t.equal(f(16.5), SDVX_VF_CLASSES.CORAL_III);
|
||||
t.equal(f(16.75), SDVX_VF_CLASSES.CORAL_IV);
|
||||
t.equal(f(17), SDVX_VF_CLASSES.ARGENTO_I);
|
||||
t.equal(f(17.25), SDVX_VF_CLASSES.ARGENTO_II);
|
||||
t.equal(f(17.5), SDVX_VF_CLASSES.ARGENTO_III);
|
||||
t.equal(f(17.75), SDVX_VF_CLASSES.ARGENTO_IV);
|
||||
t.equal(f(18), SDVX_VF_CLASSES.ELDORA_I);
|
||||
t.equal(f(18.25), SDVX_VF_CLASSES.ELDORA_II);
|
||||
t.equal(f(18.5), SDVX_VF_CLASSES.ELDORA_III);
|
||||
t.equal(f(18.75), SDVX_VF_CLASSES.ELDORA_IV);
|
||||
t.equal(f(19), SDVX_VF_CLASSES.CRIMSON_I);
|
||||
t.equal(f(19.25), SDVX_VF_CLASSES.CRIMSON_II);
|
||||
t.equal(f(19.5), SDVX_VF_CLASSES.CRIMSON_III);
|
||||
t.equal(f(19.75), SDVX_VF_CLASSES.CRIMSON_IV);
|
||||
|
||||
t.equal(f(20), SDVX_VF_CLASSES.IMPERIAL_I);
|
||||
t.equal(f(21), SDVX_VF_CLASSES.IMPERIAL_II);
|
||||
t.equal(f(22), SDVX_VF_CLASSES.IMPERIAL_III);
|
||||
t.equal(f(23), SDVX_VF_CLASSES.IMPERIAL_IV);
|
||||
t.equal(f(24), SDVX_VF_CLASSES.IMPERIAL_IV);
|
||||
t.equal(f(1000), SDVX_VF_CLASSES.IMPERIAL_IV);
|
||||
|
||||
t.end();
|
||||
});
|
||||
-298
@@ -1,298 +0,0 @@
|
||||
import {
|
||||
CHUNITHM_COLOURS,
|
||||
GITADORA_COLOURS,
|
||||
JUBEAT_COLOURS,
|
||||
MAIMAIDX_COLOURS,
|
||||
POPN_CLASSES,
|
||||
SDVX_VF_CLASSES,
|
||||
WACCA_COLOURS,
|
||||
} from "tachi-common";
|
||||
import { IsNullish } from "utils/misc";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
import type { Game, integer, Playtype, ScoreCalculatedDataLookup } from "tachi-common";
|
||||
import type { ClassRecords } from "tachi-common/game-classes";
|
||||
|
||||
export function CalculateSDVXClass(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
ratings: Partial<Record<ScoreCalculatedDataLookup["sdvx:Single"], number>>,
|
||||
logger: KtLogger
|
||||
): Partial<ClassRecords<"sdvx:Single">> {
|
||||
if (ratings.VF6 === undefined) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return { vfClass: SDVXVF6ToClass(ratings.VF6, logger) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a user's profile VF6 to a class. There are almost 40 classes, and writing
|
||||
* if statements for all of them is a painful endeavour. This function returns an index
|
||||
* derived from the gaps between classes, to avoid writing out all those statements.
|
||||
* @see https://bemaniwiki.com/index.php?SOUND%20VOLTEX%20VIVID%20WAVE/VOLFORCE
|
||||
*/
|
||||
export function SDVXVF6ToClass(vf: number, logger: KtLogger) {
|
||||
// This is impossible, but a failsafe regardless
|
||||
if (vf >= 24) {
|
||||
logger.warn(`User has excessive VF6 of ${vf}. Defaulting to Imperial IV.`);
|
||||
return SDVX_VF_CLASSES.IMPERIAL_IV;
|
||||
} else if (vf >= 20) {
|
||||
// imperial i -> iv has gaps of 1
|
||||
return SDVX_VF_CLASSES.IMPERIAL_I + Math.floor(vf - 20);
|
||||
} else if (vf >= 14) {
|
||||
// cyan i -> crimson iv has gaps of 0.25
|
||||
return SDVX_VF_CLASSES.CYAN_I + Math.floor(4 * (vf - 14));
|
||||
} else if (vf >= 10) {
|
||||
// cobalt i -> dandelion iv have gaps of 0.5
|
||||
return SDVX_VF_CLASSES.COBALT_I + Math.floor(2 * (vf - 10));
|
||||
}
|
||||
|
||||
return Math.floor(vf / 2.5);
|
||||
}
|
||||
|
||||
export function CalculateGitadoraColour(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
ratings: Record<string, number | null>
|
||||
): Partial<ClassRecords<"gitadora:Dora" | "gitadora:Gita">> {
|
||||
if (IsNullish(ratings.skill)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const colour = GitadoraSkillToColour(ratings.skill);
|
||||
|
||||
return {
|
||||
colour,
|
||||
};
|
||||
}
|
||||
|
||||
export function GitadoraSkillToColour(sk: number) {
|
||||
if (sk >= 8500) {
|
||||
return GITADORA_COLOURS.RAINBOW;
|
||||
} else if (sk >= 8000) {
|
||||
return GITADORA_COLOURS.GOLD;
|
||||
} else if (sk >= 7500) {
|
||||
return GITADORA_COLOURS.SILVER;
|
||||
} else if (sk >= 7000) {
|
||||
return GITADORA_COLOURS.BRONZE;
|
||||
} else if (sk >= 6500) {
|
||||
return GITADORA_COLOURS.RED_GRADIENT;
|
||||
} else if (sk >= 6000) {
|
||||
return GITADORA_COLOURS.RED;
|
||||
} else if (sk >= 5500) {
|
||||
return GITADORA_COLOURS.PURPLE_GRADIENT;
|
||||
} else if (sk >= 5000) {
|
||||
return GITADORA_COLOURS.PURPLE;
|
||||
} else if (sk >= 4500) {
|
||||
return GITADORA_COLOURS.BLUE_GRADIENT;
|
||||
} else if (sk >= 4000) {
|
||||
return GITADORA_COLOURS.BLUE;
|
||||
} else if (sk >= 3500) {
|
||||
return GITADORA_COLOURS.GREEN_GRADIENT;
|
||||
} else if (sk >= 3000) {
|
||||
return GITADORA_COLOURS.GREEN;
|
||||
} else if (sk >= 2500) {
|
||||
return GITADORA_COLOURS.YELLOW_GRADIENT;
|
||||
} else if (sk >= 2000) {
|
||||
return GITADORA_COLOURS.YELLOW;
|
||||
} else if (sk >= 1500) {
|
||||
return GITADORA_COLOURS.ORANGE_GRADIENT;
|
||||
} else if (sk >= 1000) {
|
||||
return GITADORA_COLOURS.ORANGE;
|
||||
}
|
||||
|
||||
return GITADORA_COLOURS.WHITE;
|
||||
}
|
||||
|
||||
export function CalculateWACCAColour(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
ratings: Record<string, number | null>
|
||||
) {
|
||||
if (IsNullish(ratings.rate)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const colour = WACCARateToColour(ratings.rate);
|
||||
|
||||
return { colour };
|
||||
}
|
||||
|
||||
function WACCARateToColour(rate: number) {
|
||||
if (rate >= 2500) {
|
||||
return WACCA_COLOURS.RAINBOW;
|
||||
} else if (rate >= 2200) {
|
||||
return WACCA_COLOURS.GOLD;
|
||||
} else if (rate >= 1900) {
|
||||
return WACCA_COLOURS.SILVER;
|
||||
} else if (rate >= 1600) {
|
||||
return WACCA_COLOURS.BLUE;
|
||||
} else if (rate >= 1300) {
|
||||
return WACCA_COLOURS.PURPLE;
|
||||
} else if (rate >= 1000) {
|
||||
return WACCA_COLOURS.RED;
|
||||
} else if (rate >= 600) {
|
||||
return WACCA_COLOURS.YELLOW;
|
||||
} else if (rate >= 300) {
|
||||
return WACCA_COLOURS.NAVY;
|
||||
}
|
||||
|
||||
return WACCA_COLOURS.ASH;
|
||||
}
|
||||
|
||||
export function CalculatePopnClass(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
ratings: Record<string, number | null>
|
||||
) {
|
||||
if (IsNullish(ratings.naiveClassPoints)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const cls = PopnClassPointsToClass(ratings.naiveClassPoints);
|
||||
|
||||
return { class: cls };
|
||||
}
|
||||
|
||||
function PopnClassPointsToClass(points: number) {
|
||||
if (points < 21) {
|
||||
return POPN_CLASSES.KITTY;
|
||||
} else if (points < 34) {
|
||||
return POPN_CLASSES.GRADE_SCHOOL;
|
||||
} else if (points < 46) {
|
||||
return POPN_CLASSES.DELINQUENT;
|
||||
} else if (points < 59) {
|
||||
return POPN_CLASSES.DETECTIVE;
|
||||
} else if (points < 68) {
|
||||
return POPN_CLASSES.IDOL;
|
||||
} else if (points < 79) {
|
||||
return POPN_CLASSES.GENERAL;
|
||||
} else if (points < 91) {
|
||||
return POPN_CLASSES.HERMIT;
|
||||
}
|
||||
|
||||
return POPN_CLASSES.GOD;
|
||||
}
|
||||
|
||||
export function CalculateChunithmColour(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
ratings: Record<string, number | null>
|
||||
) {
|
||||
if (IsNullish(ratings.naiveRating)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const colour = ChuniRatingToColour(ratings.naiveRating);
|
||||
|
||||
return { colour };
|
||||
}
|
||||
|
||||
function ChuniRatingToColour(rating: number) {
|
||||
if (rating >= 15) {
|
||||
return CHUNITHM_COLOURS.RAINBOW;
|
||||
} else if (rating >= 14.5) {
|
||||
return CHUNITHM_COLOURS.PLATINUM;
|
||||
} else if (rating >= 14) {
|
||||
return CHUNITHM_COLOURS.GOLD;
|
||||
} else if (rating >= 13) {
|
||||
return CHUNITHM_COLOURS.SILVER;
|
||||
} else if (rating >= 12) {
|
||||
return CHUNITHM_COLOURS.BRONZE;
|
||||
} else if (rating >= 10) {
|
||||
return CHUNITHM_COLOURS.PURPLE;
|
||||
} else if (rating >= 7) {
|
||||
return CHUNITHM_COLOURS.RED;
|
||||
} else if (rating >= 4) {
|
||||
return CHUNITHM_COLOURS.ORANGE;
|
||||
} else if (rating >= 2) {
|
||||
return CHUNITHM_COLOURS.GREEN;
|
||||
}
|
||||
|
||||
return CHUNITHM_COLOURS.BLUE;
|
||||
}
|
||||
|
||||
export function CalculateJubeatColour(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
ratings: Record<string, number | null>
|
||||
) {
|
||||
if (IsNullish(ratings.jubility)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const colour = JubilityToColour(ratings.jubility);
|
||||
|
||||
return { colour };
|
||||
}
|
||||
|
||||
function JubilityToColour(jubility: number) {
|
||||
if (jubility >= 9500) {
|
||||
return JUBEAT_COLOURS.GOLD;
|
||||
} else if (jubility >= 8500) {
|
||||
return JUBEAT_COLOURS.ORANGE;
|
||||
} else if (jubility >= 7000) {
|
||||
return JUBEAT_COLOURS.PINK;
|
||||
} else if (jubility >= 5500) {
|
||||
return JUBEAT_COLOURS.PURPLE;
|
||||
} else if (jubility >= 4000) {
|
||||
return JUBEAT_COLOURS.VIOLET;
|
||||
} else if (jubility >= 2500) {
|
||||
return JUBEAT_COLOURS.BLUE;
|
||||
} else if (jubility >= 1500) {
|
||||
return JUBEAT_COLOURS.LIGHT_BLUE;
|
||||
} else if (jubility >= 750) {
|
||||
return JUBEAT_COLOURS.GREEN;
|
||||
} else if (jubility >= 250) {
|
||||
return JUBEAT_COLOURS.YELLOW_GREEN;
|
||||
}
|
||||
|
||||
return JUBEAT_COLOURS.BLACK;
|
||||
}
|
||||
|
||||
export function CalculateMaimaiDXColour(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
ratings: Record<string, number | null>
|
||||
) {
|
||||
if (IsNullish(ratings.rate)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const colour = MaimaiDXRateToColour(ratings.rate);
|
||||
|
||||
return { colour };
|
||||
}
|
||||
|
||||
function MaimaiDXRateToColour(rate: number) {
|
||||
if (rate >= 15000) {
|
||||
return MAIMAIDX_COLOURS.RAINBOW;
|
||||
} else if (rate >= 14500) {
|
||||
return MAIMAIDX_COLOURS.PLATINUM;
|
||||
} else if (rate >= 14000) {
|
||||
return MAIMAIDX_COLOURS.GOLD;
|
||||
} else if (rate >= 13000) {
|
||||
return MAIMAIDX_COLOURS.SILVER;
|
||||
} else if (rate >= 12000) {
|
||||
return MAIMAIDX_COLOURS.BRONZE;
|
||||
} else if (rate >= 10000) {
|
||||
return MAIMAIDX_COLOURS.PURPLE;
|
||||
} else if (rate >= 7000) {
|
||||
return MAIMAIDX_COLOURS.RED;
|
||||
} else if (rate >= 4000) {
|
||||
return MAIMAIDX_COLOURS.YELLOW;
|
||||
} else if (rate >= 2000) {
|
||||
return MAIMAIDX_COLOURS.GREEN;
|
||||
} else if (rate >= 1000) {
|
||||
return MAIMAIDX_COLOURS.BLUE;
|
||||
}
|
||||
|
||||
return MAIMAIDX_COLOURS.WHITE;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ProcessClassDeltas, UpdateUGSClasses } from "./classes";
|
||||
import { ProcessClassDeltas, CalculateUGPTClasses } from "./classes";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { GITADORA_COLOURS } from "tachi-common";
|
||||
import t from "tap";
|
||||
@@ -9,7 +9,7 @@ const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#UpdateUGSClasses", (t) => {
|
||||
t.test("Should produce an empty object by default", async (t) => {
|
||||
const res = await UpdateUGSClasses("iidx", "SP", 1, {}, null, logger);
|
||||
const res = await CalculateUGPTClasses("iidx", "SP", 1, {}, null, logger);
|
||||
|
||||
t.strictSame(res, {});
|
||||
|
||||
@@ -17,7 +17,7 @@ t.test("#UpdateUGSClasses", (t) => {
|
||||
});
|
||||
|
||||
t.test("Should call and merge the ClassHandler", async (t) => {
|
||||
const res = await UpdateUGSClasses("iidx", "SP", 1, {}, () => ({ dan: 2 }), logger);
|
||||
const res = await CalculateUGPTClasses("iidx", "SP", 1, {}, () => ({ dan: 2 }), logger);
|
||||
|
||||
t.strictSame(res, { dan: 2 });
|
||||
|
||||
@@ -25,7 +25,7 @@ t.test("#UpdateUGSClasses", (t) => {
|
||||
});
|
||||
|
||||
t.test("Should call static handlers if there is one", async (t) => {
|
||||
const res = await UpdateUGSClasses(
|
||||
const res = await CalculateUGPTClasses(
|
||||
"gitadora",
|
||||
"Dora",
|
||||
1,
|
||||
|
||||
@@ -1,78 +1,23 @@
|
||||
import {
|
||||
CalculateChunithmColour,
|
||||
CalculateGitadoraColour,
|
||||
CalculateJubeatColour,
|
||||
CalculatePopnClass,
|
||||
CalculateSDVXClass,
|
||||
CalculateWACCAColour,
|
||||
CalculateMaimaiDXColour,
|
||||
} from "./builtin-class-handlers";
|
||||
import { CalculateDerivedClasses } from "../calculated-data/profile-classes";
|
||||
import deepmerge from "deepmerge";
|
||||
import db from "external/mongo/db";
|
||||
import { EmitWebhookEvent } from "lib/webhooks/webhooks";
|
||||
import { GetGamePTConfig } from "tachi-common";
|
||||
import { GetGPTString, GetGamePTConfig } from "tachi-common";
|
||||
import { ReturnClassIfGreater } from "utils/class";
|
||||
import type { ClassHandler, ScoreClasses } from "./types";
|
||||
import type { ClassProvider } from "../calculated-data/types";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
import type {
|
||||
ClassDelta,
|
||||
Game,
|
||||
GPTString,
|
||||
integer,
|
||||
Playtype,
|
||||
Playtypes,
|
||||
UserGameStats,
|
||||
Classes,
|
||||
GPTString,
|
||||
Game,
|
||||
Playtype,
|
||||
UserGameStats,
|
||||
integer,
|
||||
ExtractedClasses,
|
||||
AnyClasses,
|
||||
} from "tachi-common";
|
||||
|
||||
type ClassHandlerMap = {
|
||||
[G in Game]:
|
||||
| {
|
||||
[P in Playtypes[G]]: ClassHandler;
|
||||
}
|
||||
| null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the static class handlers for a game, I.E. ones that are
|
||||
* always meant to be called when new scores are found.
|
||||
*
|
||||
* These can be calculated without any external (i.e. user/api-provided) data.
|
||||
*
|
||||
* A good example of this would be, say, jubeat's colours - the boundaries depend entirely
|
||||
* on your profile skill level, which is known at the time this function is called.
|
||||
*/
|
||||
const STATIC_CLASS_HANDLERS: ClassHandlerMap = {
|
||||
iidx: null,
|
||||
bms: null,
|
||||
chunithm: {
|
||||
Single: CalculateChunithmColour,
|
||||
},
|
||||
gitadora: {
|
||||
Gita: CalculateGitadoraColour,
|
||||
Dora: CalculateGitadoraColour,
|
||||
},
|
||||
maimaidx: {
|
||||
Single: CalculateMaimaiDXColour,
|
||||
},
|
||||
museca: null,
|
||||
popn: {
|
||||
"9B": CalculatePopnClass,
|
||||
},
|
||||
sdvx: {
|
||||
Single: CalculateSDVXClass,
|
||||
},
|
||||
usc: null,
|
||||
wacca: {
|
||||
Single: CalculateWACCAColour,
|
||||
},
|
||||
jubeat: {
|
||||
Single: CalculateJubeatColour,
|
||||
},
|
||||
pms: null,
|
||||
itg: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates a User's Game Stats Classes. This function is rather complex, because the reality is rather complex.
|
||||
*
|
||||
@@ -90,36 +35,28 @@ const STATIC_CLASS_HANDLERS: ClassHandlerMap = {
|
||||
* @param ratings - A users ratings. This is calculated in rating.ts, and passed via update-ugs.ts.
|
||||
* We request this because we need it for things like gitadora's skill divisions - We don't need to calculate our skill
|
||||
* statistic twice if we just request it be passed to us!
|
||||
* @param ClassHandler - The Custom Resolve Function that certain import types may pass to us as a means
|
||||
* for retrieving information about a class. This returns the same thing as this function, and it is merged with the
|
||||
* @param ClassProvider - The Custom Resolve Function that certain import types may pass to us as a means
|
||||
* for providing information about a class. This returns the same thing as this function, and it is merged with the
|
||||
* defaults.
|
||||
*/
|
||||
export async function UpdateUGSClasses(
|
||||
export async function CalculateUGPTClasses(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
ratings: Record<string, number | null>,
|
||||
ClassHandler: ClassHandler | null,
|
||||
ClassProvider: ClassProvider | null,
|
||||
logger: KtLogger
|
||||
): Promise<ScoreClasses> {
|
||||
let classes: ScoreClasses = {};
|
||||
): Promise<ExtractedClasses[GPTString]> {
|
||||
const gptString = GetGPTString(game, playtype);
|
||||
|
||||
// @ts-expect-error This one sucks - I need to look into a better way of representing these types
|
||||
if (STATIC_CLASS_HANDLERS[game]?.[playtype] !== undefined) {
|
||||
// @ts-expect-error see above
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
||||
classes = await STATIC_CLASS_HANDLERS[game][playtype](
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
ratings,
|
||||
logger
|
||||
);
|
||||
}
|
||||
// Derive all classes first.
|
||||
let classes = CalculateDerivedClasses(gptString, ratings);
|
||||
|
||||
if (ClassHandler) {
|
||||
// If this import method is providing us classes, merge those with the
|
||||
// other classes we have.
|
||||
if (ClassProvider) {
|
||||
logger.debug(`Calling custom class handler.`);
|
||||
const customClasses = (await ClassHandler(game, playtype, userID, ratings, logger)) ?? {};
|
||||
const customClasses = (await ClassProvider(gptString, userID, ratings, logger)) ?? {};
|
||||
|
||||
classes = deepmerge(customClasses, classes);
|
||||
}
|
||||
@@ -131,17 +68,24 @@ export async function UpdateUGSClasses(
|
||||
* Calculates the class "deltas" for this users classes.
|
||||
* This is for calculating scenarios where a users class has improved (i.e. they have gone from 9th dan to 10th dan).
|
||||
*
|
||||
* If a class is provided, we don't want to potentially downgrade users. I.e.
|
||||
* if a user clears 7th dan while they're 10th dan, we don't want to downgrade them.
|
||||
*
|
||||
* For derived classes however, downgrading is fine.
|
||||
*
|
||||
* Knowing this information allows us to attach it onto the import, and also emit things on webhooks.
|
||||
* This function emits webhook events and inserts classachieved documents into the DB!**
|
||||
* This function emits webhook events and inserts classachieved documents into the DB!
|
||||
*/
|
||||
export async function ProcessClassDeltas(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
classes: ScoreClasses,
|
||||
classes: AnyClasses,
|
||||
userGameStats: UserGameStats | null,
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
): Promise<Array<ClassDelta>> {
|
||||
const gptString = GetGPTString(game, playtype);
|
||||
|
||||
const deltas: Array<ClassDelta> = [];
|
||||
|
||||
const achievementOps = [];
|
||||
@@ -152,7 +96,7 @@ export async function ProcessClassDeltas(
|
||||
const classSet = s as Classes[GPTString];
|
||||
const classVal = classes[classSet];
|
||||
|
||||
if (classVal === undefined) {
|
||||
if (classVal === undefined || classVal === null) {
|
||||
logger.debug(`Skipped deltaing-class ${classSet}.`);
|
||||
continue;
|
||||
}
|
||||
@@ -160,11 +104,11 @@ export async function ProcessClassDeltas(
|
||||
const classConfig = gptConfig.classes[classSet]!;
|
||||
|
||||
try {
|
||||
const isGreater = ReturnClassIfGreater(classSet, classVal, userGameStats);
|
||||
const isGreater = ReturnClassIfGreater(gptString, classSet, classVal, userGameStats);
|
||||
|
||||
// if this was worse, and this class isn't DERIVED (i.e. it's a dan)
|
||||
// if this was worse, and this class is PROVIDED (i.e. it's a dan)
|
||||
// then don't do anything
|
||||
if (isGreater === false && classConfig.type !== "DERIVED") {
|
||||
if (isGreater === false && classConfig.type === "PROVIDED") {
|
||||
continue;
|
||||
} else {
|
||||
// otherwise, provide this as an update.
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { CalculateRatings } from "./rating";
|
||||
import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import t from "tap";
|
||||
import { mkFakePBIIDXSP, mkFakePBJubeat } from "test-utils/misc";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#CalculateRatings", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should return BPI for IIDX", async (t) => {
|
||||
const res = await CalculateRatings("iidx", "SP", 1, logger);
|
||||
|
||||
t.strictSame(res, { BPI: null, ktLampRating: null }, "Should return BPI as a custom key.");
|
||||
|
||||
const resDP = await CalculateRatings("iidx", "DP", 1, logger);
|
||||
|
||||
t.strictSame(
|
||||
resDP,
|
||||
{ BPI: null, ktLampRating: null },
|
||||
"Should return BPI as a custom key."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return VF6 for SDVX", async (t) => {
|
||||
const res = await CalculateRatings("sdvx", "Single", 1, logger);
|
||||
|
||||
t.strictSame(res, { VF6: null }, "Should return VF6 keys.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return VF6 for USC:Keyboard", async (t) => {
|
||||
const res = await CalculateRatings("usc", "Keyboard", 1, logger);
|
||||
|
||||
t.strictSame(res, { VF6: null }, "Should return VF6 keys.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return VF6 for USC:Controller", async (t) => {
|
||||
const res = await CalculateRatings("usc", "Controller", 1, logger);
|
||||
|
||||
t.strictSame(res, { VF6: null }, "Should return VF6 keys.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return skill for Gitadora", async (t) => {
|
||||
const res = await CalculateRatings("gitadora", "Dora", 1, logger);
|
||||
|
||||
t.strictSame(res, { skill: null, naiveSkill: null }, "Should return skill keys.");
|
||||
|
||||
const res2 = await CalculateRatings("gitadora", "Gita", 1, logger);
|
||||
|
||||
t.strictSame(res2, { skill: null, naiveSkill: null }, "Should return skill keys.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should calculate jubility for Jubeat", async (t) => {
|
||||
await db["personal-bests"].insert(mkFakePBJubeat());
|
||||
|
||||
const res = await CalculateRatings("jubeat", "Single", 1, logger);
|
||||
|
||||
t.strictSame(res, { jubility: 5, naiveJubility: 5 }, "Should return jubility keys.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -1,477 +0,0 @@
|
||||
import db from "external/mongo/db";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
import type {
|
||||
Game,
|
||||
Versions,
|
||||
GPTString,
|
||||
integer,
|
||||
PBScoreDocument,
|
||||
Playtype,
|
||||
UserGameStats,
|
||||
ScoreRatingAlgorithms,
|
||||
} from "tachi-common";
|
||||
|
||||
type CustomCalcNames = ScoreRatingAlgorithms[GPTString];
|
||||
|
||||
/**
|
||||
* Curries a function that returns the sum of N best ratings on `key`.
|
||||
*
|
||||
* @param key - What rating value to sum.
|
||||
* @param n - The amount of rating values to pull.
|
||||
* @param returnMean - Optionally, if true, return the sum of these values divided by N.
|
||||
* @param nullIfNotEnoughScores - If true, return null if the total scores this user has is less than N.
|
||||
*
|
||||
* @returns - Number if the user has scores with that rating algorithm, null if they have
|
||||
* no scores with this rating algorithm that are non-null.
|
||||
*/
|
||||
function LazyCalcN(
|
||||
key: CustomCalcNames,
|
||||
n: integer,
|
||||
returnMean = false,
|
||||
nullIfNotEnoughScores = false
|
||||
) {
|
||||
return async (game: Game, playtype: Playtype, userID: integer) => {
|
||||
const sc = await db["personal-bests"].find(
|
||||
{
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
isPrimary: true,
|
||||
[`calculatedData.${key}`]: { $type: "number" },
|
||||
},
|
||||
{
|
||||
limit: n,
|
||||
sort: { [`calculatedData.${key}`]: -1 },
|
||||
}
|
||||
);
|
||||
|
||||
if (sc.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (nullIfNotEnoughScores && sc.length < n) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let result = sc.reduce((a, e) => a + e.calculatedData[key]!, 0);
|
||||
|
||||
if (returnMean) {
|
||||
result = result / n;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
const LazySumN = (key: CustomCalcNames, n: integer, nullIfNotEnoughScores = false) =>
|
||||
LazyCalcN(key, n, false, nullIfNotEnoughScores);
|
||||
const LazyMeanN = (key: CustomCalcNames, n: integer, nullIfNotEnoughScores = false) =>
|
||||
LazyCalcN(key, n, true, nullIfNotEnoughScores);
|
||||
|
||||
type RatingFunction = (
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
) => Promise<UserGameStats["ratings"]>;
|
||||
|
||||
function GetGPTRatingFunction(game: Game, playtype: Playtype): RatingFunction {
|
||||
switch (`${game}:${playtype}` as GPTString) {
|
||||
case "iidx:SP":
|
||||
case "iidx:DP":
|
||||
return async (g, p, u) => ({
|
||||
BPI: await LazyMeanN("BPI", 20, true)(g, p, u),
|
||||
ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u),
|
||||
});
|
||||
case "sdvx:Single":
|
||||
case "usc:Keyboard":
|
||||
case "usc:Controller":
|
||||
return async (g, p, u) => ({
|
||||
VF6: await LazySumN("VF6", 50)(g, p, u),
|
||||
});
|
||||
case "gitadora:Dora":
|
||||
case "gitadora:Gita":
|
||||
return async (g, p, u, l) => ({
|
||||
skill: await CalculateGitadoraSkill(g, p, u, l),
|
||||
naiveSkill: await LazySumN("skill", 50)(g, p, u),
|
||||
});
|
||||
case "bms:7K":
|
||||
case "bms:14K":
|
||||
case "pms:Keyboard":
|
||||
case "pms:Controller":
|
||||
return async (g, p, u) => ({
|
||||
sieglinde: await LazyMeanN("sieglinde", 20)(g, p, u),
|
||||
});
|
||||
case "chunithm:Single":
|
||||
return async (g, p, u) => ({
|
||||
naiveRating: await LazyMeanN("rating", 20)(g, p, u),
|
||||
});
|
||||
case "popn:9B":
|
||||
return async (g, p, u) => ({
|
||||
naiveClassPoints: await LazyMeanN("classPoints", 20)(g, p, u),
|
||||
});
|
||||
case "museca:Single":
|
||||
return async (g, p, u) => ({
|
||||
curatorSkill: await LazySumN("curatorSkill", 20)(g, p, u),
|
||||
});
|
||||
case "maimaidx:Single":
|
||||
return async (g, p, u, l) => ({
|
||||
naiveRate: await LazySumN("rate", 50)(g, p, u),
|
||||
rate: await CalculateMaimaiDXRate(g, p, u, l),
|
||||
});
|
||||
case "wacca:Single":
|
||||
return async (g, p, u, l) => ({
|
||||
naiveRate: await LazySumN("rate", 50)(g, p, u),
|
||||
rate: await CalculateWACCARate(g, p, u, l),
|
||||
});
|
||||
case "jubeat:Single":
|
||||
return async (g, p, u, l) => ({
|
||||
jubility: await CalculateJubility(g, p, u, l),
|
||||
naiveJubility: await LazySumN("jubility", 60)(g, p, u),
|
||||
});
|
||||
case "itg:Stamina":
|
||||
// HighestX is equivalent to summing 1.
|
||||
return async (g, p, u) => ({
|
||||
highestBlock: await LazySumN("blockRating", 1, true)(g, p, u),
|
||||
highest32: await LazySumN("fastest32", 1, true)(g, p, u),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function CalculateRatings(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
): Promise<Partial<Record<CustomCalcNames, number | null>>> {
|
||||
const RatingFunction = GetGPTRatingFunction(game, playtype);
|
||||
|
||||
return RatingFunction(game, playtype, userID, logger);
|
||||
}
|
||||
|
||||
// Wacca has a funny algorithm for rate involving gitadora-style latest chart bonuses,
|
||||
async function CalculateWACCARate(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
_logger: KtLogger
|
||||
) {
|
||||
const hotChartIDs = (
|
||||
await db.charts.wacca.find({ "data.isHot": true }, { projection: { chartID: 1 } })
|
||||
).map((e) => e.chartID);
|
||||
|
||||
const coldChartIDs = (
|
||||
await db.charts.wacca.find({ "data.isHot": false }, { projection: { chartID: 1 } })
|
||||
).map((e) => e.chartID);
|
||||
|
||||
const best15Hot = await db["personal-bests"].find(
|
||||
{
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
chartID: { $in: hotChartIDs },
|
||||
"calculatedData.rate": { $type: "number" },
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
"calculatedData.rate": -1,
|
||||
},
|
||||
limit: 15,
|
||||
projection: {
|
||||
"calculatedData.rate": 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const best35Cold = await db["personal-bests"].find(
|
||||
{
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
chartID: { $in: coldChartIDs },
|
||||
"calculatedData.rate": { $type: "number" },
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
"calculatedData.rate": -1,
|
||||
},
|
||||
limit: 35,
|
||||
projection: {
|
||||
"calculatedData.rate": 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (best15Hot.length + best35Cold.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
best15Hot.reduce((a, e) => a + e.calculatedData.rate!, 0) +
|
||||
best35Cold.reduce((a, e) => a + e.calculatedData.rate!, 0)
|
||||
);
|
||||
}
|
||||
|
||||
// basically the same as WACCA's.
|
||||
async function CalculateMaimaiDXRate(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
_logger: KtLogger
|
||||
) {
|
||||
const newChartIDs = (
|
||||
await db.charts.maimaidx.find({ "data.isLatest": true }, { projection: { chartID: 1 } })
|
||||
).map((e) => e.chartID);
|
||||
|
||||
const oldChartIDs = (
|
||||
await db.charts.maimaidx.find({ "data.isLatest": false }, { projection: { chartID: 1 } })
|
||||
).map((e) => e.chartID);
|
||||
|
||||
const best15New = await db["personal-bests"].find(
|
||||
{
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
chartID: { $in: newChartIDs },
|
||||
"calculatedData.rate": { $type: "number" },
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
"calculatedData.rate": -1,
|
||||
},
|
||||
limit: 15,
|
||||
projection: {
|
||||
"calculatedData.rate": 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const best35Old = await db["personal-bests"].find(
|
||||
{
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
chartID: { $in: oldChartIDs },
|
||||
"calculatedData.rate": { $type: "number" },
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
"calculatedData.rate": -1,
|
||||
},
|
||||
limit: 35,
|
||||
projection: {
|
||||
"calculatedData.rate": 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (best15New.length + best35Old.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
best15New.reduce((a, e) => a + e.calculatedData.rate!, 0) +
|
||||
best35Old.reduce((a, e) => a + e.calculatedData.rate!, 0)
|
||||
);
|
||||
}
|
||||
|
||||
async function CalculateGitadoraSkill(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
_logger: KtLogger
|
||||
) {
|
||||
const hotSongIDs = (
|
||||
await db.songs.gitadora.find({ "data.isHot": true }, { projection: { id: 1 } })
|
||||
).map((e) => e.id);
|
||||
|
||||
// get it
|
||||
const coldSongIDs = (
|
||||
await db.songs.gitadora.find({ "data.isHot": false }, { projection: { id: 1 } })
|
||||
).map((e) => e.id);
|
||||
|
||||
const [bestHotScores, bestScores] = await Promise.all([
|
||||
GetBestRatingOnSongs(hotSongIDs, userID, game, playtype, "skill", 25),
|
||||
GetBestRatingOnSongs(coldSongIDs, userID, game, playtype, "skill", 25),
|
||||
]);
|
||||
|
||||
if (bestHotScores.length + bestScores.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let skill = 0;
|
||||
|
||||
skill = skill + bestHotScores.reduce((a, e) => a + e.calculatedData.skill!, 0);
|
||||
skill = skill + bestScores.reduce((a, e) => a + e.calculatedData.skill!, 0);
|
||||
|
||||
return skill;
|
||||
}
|
||||
|
||||
export async function GetBestRatingOnSongs(
|
||||
songIDs: Array<integer>,
|
||||
userID: integer,
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
ratingProp: "skill",
|
||||
limit: integer
|
||||
): Promise<Array<PBScoreDocument>> {
|
||||
const r: Array<{ doc: PBScoreDocument }> = await db["personal-bests"].aggregate([
|
||||
{
|
||||
$match: {
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
songID: { $in: songIDs },
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
[`calculatedData.${ratingProp}`]: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$songID",
|
||||
doc: { $first: "$$ROOT" },
|
||||
},
|
||||
},
|
||||
|
||||
// for some godforsaken reason you have to sort twice. after a grouping
|
||||
// the sort order becomes nondeterministic
|
||||
{
|
||||
$sort: {
|
||||
[`doc.calculatedData.${ratingProp}`]: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$limit: limit,
|
||||
},
|
||||
]);
|
||||
|
||||
return r.map((e) => e.doc);
|
||||
}
|
||||
|
||||
async function GetBestJubilityOnSongs(
|
||||
songIDs: Array<integer>,
|
||||
userID: integer,
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
limit: integer
|
||||
): Promise<Array<PBScoreDocument>> {
|
||||
const r: Array<{ doc: PBScoreDocument }> = await db["personal-bests"].aggregate([
|
||||
{
|
||||
$match: {
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
songID: { $in: songIDs },
|
||||
},
|
||||
},
|
||||
{
|
||||
// we need to do stuff dependent on the chart difficulty,
|
||||
// so we need the chart difficulty
|
||||
$lookup: {
|
||||
from: "charts-jubeat",
|
||||
localField: "chartID",
|
||||
foreignField: "chartID",
|
||||
as: "chart",
|
||||
},
|
||||
},
|
||||
{
|
||||
// "chart" is an array unless we unwind it.
|
||||
$unwind: {
|
||||
path: "$chart",
|
||||
},
|
||||
},
|
||||
{
|
||||
// sort on jubility (so we get the best score)
|
||||
$sort: {
|
||||
[`calculatedData.jubility`]: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
songID: "$songID",
|
||||
|
||||
// Jubility is unique upon songID + difficulty. However, you
|
||||
// cannot have a PB on both a HARD BSC and a BSC counted for
|
||||
// jubility. This query is awkward. Sorry!
|
||||
difficulty: {
|
||||
$switch: {
|
||||
branches: [
|
||||
{
|
||||
case: { $in: ["$chart.difficulty", ["HARD BSC", "BSC"]] },
|
||||
then: "BSC",
|
||||
},
|
||||
{
|
||||
case: { $in: ["$chart.difficulty", ["HARD ADV", "ADV"]] },
|
||||
then: "ADV",
|
||||
},
|
||||
{
|
||||
case: { $in: ["$chart.difficulty", ["HARD EXT", "EXT"]] },
|
||||
then: "EXT",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
doc: { $first: "$$ROOT" },
|
||||
},
|
||||
},
|
||||
|
||||
// for some godforsaken reason you have to sort twice. after a grouping
|
||||
// the sort order becomes nondeterministic
|
||||
{
|
||||
$sort: {
|
||||
[`doc.calculatedData.jubility`]: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$limit: limit,
|
||||
},
|
||||
]);
|
||||
|
||||
return r.map((e) => e.doc);
|
||||
}
|
||||
|
||||
const CURRENT_JUBEAT_HOT_VERSION: Versions["jubeat:Single"] = "festo";
|
||||
|
||||
export async function GetPBsForJubility(userID: integer) {
|
||||
const hotSongs = await db.songs.jubeat.find(
|
||||
{ "data.displayVersion": CURRENT_JUBEAT_HOT_VERSION },
|
||||
{ projection: { id: 1 } }
|
||||
);
|
||||
|
||||
const hotSongIDs = hotSongs.map((e) => e.id);
|
||||
|
||||
const coldSongs = await db.songs.jubeat.find(
|
||||
{ "data.displayVersion": { $ne: CURRENT_JUBEAT_HOT_VERSION } },
|
||||
{ projection: { id: 1 } }
|
||||
);
|
||||
|
||||
const coldSongIDs = coldSongs.map((e) => e.id);
|
||||
|
||||
const [bestHotScores, bestScores] = await Promise.all([
|
||||
GetBestJubilityOnSongs(hotSongIDs, userID, "jubeat", "Single", 30),
|
||||
GetBestJubilityOnSongs(coldSongIDs, userID, "jubeat", "Single", 30),
|
||||
]);
|
||||
|
||||
return { bestHotScores, bestScores };
|
||||
}
|
||||
|
||||
async function CalculateJubility(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
_logger: KtLogger
|
||||
): Promise<number> {
|
||||
const { bestHotScores, bestScores } = await GetPBsForJubility(userID);
|
||||
|
||||
let jubility = 0;
|
||||
|
||||
jubility = jubility + bestHotScores.reduce((a, e) => a + (e.calculatedData.jubility ?? 0), 0);
|
||||
jubility = jubility + bestScores.reduce((a, e) => a + (e.calculatedData.jubility ?? 0), 0);
|
||||
|
||||
return jubility;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
import type { Game, GPTString, integer, Playtype } from "tachi-common";
|
||||
import type { ClassRecords } from "tachi-common/game-classes";
|
||||
|
||||
export type ScoreClasses = Partial<ClassRecords<GPTString>>;
|
||||
|
||||
export type ClassHandler = (
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
ratings: Record<string, number | null>,
|
||||
logger: KtLogger
|
||||
) => Promise<ScoreClasses> | ScoreClasses | undefined;
|
||||
@@ -8,14 +8,14 @@ import { CheckAndSetOngoingImportLock, UnsetOngoingImportLock } from "../import-
|
||||
import { ProcessPBs } from "../pb/process-pbs";
|
||||
import { UpdateUsersQuests } from "../quests/quests";
|
||||
import { CreateSessions } from "../sessions/sessions";
|
||||
import { UpdateUsersGamePlaytypeStats } from "../profile-calculated-data/update-ugs";
|
||||
import { UpdateUsersGamePlaytypeStats } from "../ugpt-stats/update-ugpt-stats";
|
||||
import db from "external/mongo/db";
|
||||
import { GetGameConfig } from "tachi-common";
|
||||
import { GetMillisecondsSince } from "utils/misc";
|
||||
import { GetUserWithID } from "utils/user";
|
||||
import type { ConverterFunction, ImportInputParser } from "../../import-types/common/types";
|
||||
import type { ClassProvider } from "../calculated-data/types";
|
||||
import type { ChartIDPlaytypeMap, ScorePlaytypeMap } from "../common/types";
|
||||
import type { ClassHandler } from "../profile-calculated-data/types";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
import type { ScoreImportJob } from "lib/score-import/worker/types";
|
||||
import type {
|
||||
@@ -87,7 +87,7 @@ export default async function ScoreImportMain<D, C>(
|
||||
// We get an iterable from the provided parser function, alongside some context and a converter function.
|
||||
// This iterable does not have to be an array - it's anything that's iterable, like a generator.
|
||||
const parseTimeStart = process.hrtime.bigint();
|
||||
const { iterable, context, game, classHandler } = await InputParser(logger);
|
||||
const { iterable, context, game, classProvider: classProvider } = await InputParser(logger);
|
||||
|
||||
const parseTime = GetMillisecondsSince(parseTimeStart);
|
||||
|
||||
@@ -172,7 +172,7 @@ export default async function ScoreImportMain<D, C>(
|
||||
user,
|
||||
importType,
|
||||
game,
|
||||
classHandler,
|
||||
classProvider,
|
||||
logger,
|
||||
job
|
||||
);
|
||||
@@ -260,7 +260,7 @@ export async function HandlePostImportSteps(
|
||||
user: UserDocument,
|
||||
importType: ImportTypes,
|
||||
game: Game,
|
||||
classHandler: ClassHandler | null,
|
||||
classProvider: ClassProvider | null,
|
||||
logger: KtLogger,
|
||||
job: ScoreImportJob | undefined
|
||||
) {
|
||||
@@ -324,7 +324,7 @@ export async function HandlePostImportSteps(
|
||||
// --- 6. Game Stats ---
|
||||
// This function updates the users "stats" for this game - such as their profile rating or their classes.
|
||||
const ugsTimeStart = process.hrtime.bigint();
|
||||
const classDeltas = await UpdateUsersGameStats(game, playtypes, user.id, classHandler, logger);
|
||||
const classDeltas = await UpdateUsersGameStats(game, playtypes, user.id, classProvider, logger);
|
||||
|
||||
const ugsTime = GetMillisecondsSince(ugsTimeStart);
|
||||
|
||||
@@ -384,20 +384,20 @@ async function UpdateUsersGameStats(
|
||||
game: Game,
|
||||
modifiedPlaytypes: Array<Playtype>,
|
||||
userID: integer,
|
||||
classHandler: ClassHandler | null,
|
||||
classProvider: ClassProvider | null,
|
||||
logger: KtLogger
|
||||
) {
|
||||
const promises = [];
|
||||
|
||||
// Instead of using the provided playtypes, run the classHandler on all
|
||||
// playtypes. This should only happen if a classHandler is provided, and is
|
||||
// Instead of using the provided playtypes, run the classProvider on all
|
||||
// playtypes. This should only happen if a classProvider is provided, and is
|
||||
// a hack fix for things like #480.
|
||||
const allPlaytypes = GetGameConfig(game).playtypes;
|
||||
|
||||
const playtypes = classHandler ? allPlaytypes : modifiedPlaytypes;
|
||||
const playtypes = classProvider ? allPlaytypes : modifiedPlaytypes;
|
||||
|
||||
for (const pt of playtypes) {
|
||||
promises.push(UpdateUsersGamePlaytypeStats(game, pt, userID, classHandler, logger));
|
||||
promises.push(UpdateUsersGamePlaytypeStats(game, pt, userID, classProvider, logger));
|
||||
}
|
||||
|
||||
const r = await Promise.all(promises);
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { UpdateUsersGamePlaytypeStats } from "./update-ugs";
|
||||
import { UpdateUsersGamePlaytypeStats } from "./update-ugpt-stats";
|
||||
import deepmerge from "deepmerge";
|
||||
import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
+14
-11
@@ -1,12 +1,8 @@
|
||||
// "rating" refers to a user's profile statistics, as in their "rating" on a game.
|
||||
// Some games have dedicated methods to calculate statistics like these, other games do not.
|
||||
// That's about all there is to it!
|
||||
|
||||
import { ProcessClassDeltas, UpdateUGSClasses } from "./classes";
|
||||
import { CalculateRatings } from "./rating";
|
||||
import { CalculateProfileRatings } from "../calculated-data/profile";
|
||||
import { CalculateUGPTClasses, ProcessClassDeltas } from "../profile-calculated-data/classes";
|
||||
import db from "external/mongo/db";
|
||||
import { CreateGameSettings } from "lib/game-settings/create-game-settings";
|
||||
import type { ClassHandler } from "./types";
|
||||
import type { ClassProvider } from "../calculated-data/types";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
import type { ClassDelta, Game, integer, Playtype, UserGameStats } from "tachi-common";
|
||||
|
||||
@@ -14,12 +10,12 @@ export async function UpdateUsersGamePlaytypeStats(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
userID: integer,
|
||||
classHandler: ClassHandler | null,
|
||||
classProvider: ClassProvider | null,
|
||||
logger: KtLogger
|
||||
): Promise<Array<ClassDelta>> {
|
||||
logger.debug(`Calculating Ratings...`);
|
||||
|
||||
const ratings = await CalculateRatings(game, playtype, userID, logger);
|
||||
const ratings = await CalculateProfileRatings(game, playtype, userID);
|
||||
|
||||
// Attempt to find a users game stats if one already exists. If one doesn't exist,
|
||||
// this is this players first import for this game!
|
||||
@@ -31,7 +27,14 @@ export async function UpdateUsersGamePlaytypeStats(
|
||||
|
||||
logger.debug(`Calculating UGSClasses...`);
|
||||
|
||||
const classes = await UpdateUGSClasses(game, playtype, userID, ratings, classHandler, logger);
|
||||
const classes = await CalculateUGPTClasses(
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
ratings,
|
||||
classProvider,
|
||||
logger
|
||||
);
|
||||
|
||||
logger.debug(`Finished Calculating UGSClasses`);
|
||||
|
||||
@@ -44,7 +47,7 @@ export async function UpdateUsersGamePlaytypeStats(
|
||||
if (userGameStats) {
|
||||
logger.debug(`Updated player gamestats for ${game} (${playtype})`);
|
||||
|
||||
const updateClasses: Record<string, number> = {};
|
||||
const updateClasses: Record<string, string> = {};
|
||||
|
||||
for (const delta of deltas) {
|
||||
updateClasses[`classes.${delta.set}`] = delta.new;
|
||||
@@ -134,7 +134,7 @@ export function CreateCGParser<T>(cgGame: CGSupportedGames, service: CGServices)
|
||||
},
|
||||
game: CGGameToTachiGame(cgGame),
|
||||
iterable: scores as Array<T>,
|
||||
classHandler: null,
|
||||
classProvider: null,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
+10
-10
@@ -1,4 +1,4 @@
|
||||
import { CreateKaiIIDXClassHandler } from "./class-handler";
|
||||
import { CreateKaiIIDXClassProvider } from "./class-handler";
|
||||
import { KaiTypeToBaseURL } from "../utils";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { IIDX_DANS } from "tachi-common";
|
||||
@@ -8,10 +8,10 @@ import ResetDBState from "test-utils/resets";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#CreateKaiIIDXClassHandler", async (t) => {
|
||||
t.test("#CreateKaiIIDXClassProvider", async (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
const fn = await CreateKaiIIDXClassHandler(
|
||||
const fn = await CreateKaiIIDXClassProvider(
|
||||
"FLO",
|
||||
"token",
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
@@ -46,7 +46,7 @@ t.test("#CreateKaiIIDXClassHandler", async (t) => {
|
||||
});
|
||||
|
||||
t.test("Should return nothing if dan is not a number", async (t) => {
|
||||
const fn = await CreateKaiIIDXClassHandler(
|
||||
const fn = await CreateKaiIIDXClassProvider(
|
||||
"FLO",
|
||||
"token",
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
@@ -74,7 +74,7 @@ t.test("#CreateKaiIIDXClassHandler", async (t) => {
|
||||
});
|
||||
|
||||
t.test("Should return nothing if dan is too great", async (t) => {
|
||||
const fn = await CreateKaiIIDXClassHandler(
|
||||
const fn = await CreateKaiIIDXClassProvider(
|
||||
"FLO",
|
||||
"token",
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
@@ -102,7 +102,7 @@ t.test("#CreateKaiIIDXClassHandler", async (t) => {
|
||||
});
|
||||
|
||||
t.test("Should return nothing if dan is negative", async (t) => {
|
||||
const fn = await CreateKaiIIDXClassHandler(
|
||||
const fn = await CreateKaiIIDXClassProvider(
|
||||
"FLO",
|
||||
"token",
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
@@ -130,7 +130,7 @@ t.test("#CreateKaiIIDXClassHandler", async (t) => {
|
||||
});
|
||||
|
||||
t.test("Should gracefully handle negative API responses", async (t) => {
|
||||
const fn = await CreateKaiIIDXClassHandler(
|
||||
const fn = await CreateKaiIIDXClassProvider(
|
||||
"FLO",
|
||||
"token",
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
@@ -149,7 +149,7 @@ t.test("#CreateKaiIIDXClassHandler", async (t) => {
|
||||
|
||||
t.test("Should call reauthFn if statusCode is 401", async (t) => {
|
||||
let pass = false;
|
||||
const fn = await CreateKaiIIDXClassHandler(
|
||||
const fn = await CreateKaiIIDXClassProvider(
|
||||
"FLO",
|
||||
"token",
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
@@ -168,7 +168,7 @@ t.test("#CreateKaiIIDXClassHandler", async (t) => {
|
||||
});
|
||||
|
||||
t.test("Should ignore null dans", async (t) => {
|
||||
const fn = await CreateKaiIIDXClassHandler(
|
||||
const fn = await CreateKaiIIDXClassProvider(
|
||||
"FLO",
|
||||
"token",
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
@@ -196,7 +196,7 @@ t.test("#CreateKaiIIDXClassHandler", async (t) => {
|
||||
});
|
||||
|
||||
t.test("Should handle invalid playtypes", async (t) => {
|
||||
const fn = await CreateKaiIIDXClassHandler(
|
||||
const fn = await CreateKaiIIDXClassProvider(
|
||||
"FLO",
|
||||
"token",
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
|
||||
@@ -3,14 +3,14 @@ import { IIDX_DANS } from "tachi-common";
|
||||
import nodeFetch from "utils/fetch";
|
||||
import { IsRecord } from "utils/misc";
|
||||
import type { KaiAPIReauthFunction } from "../traverse-api";
|
||||
import type { ClassHandler } from "lib/score-import/framework/profile-calculated-data/types";
|
||||
import type { ClassProvider } from "lib/score-import/framework/profile-calculated-data/types";
|
||||
|
||||
export async function CreateKaiIIDXClassHandler(
|
||||
export async function CreateKaiIIDXClassProvider(
|
||||
kaiType: "EAG" | "FLO",
|
||||
token: string,
|
||||
reauthFn: KaiAPIReauthFunction,
|
||||
fetch = nodeFetch
|
||||
): Promise<ClassHandler> {
|
||||
): Promise<ClassProvider> {
|
||||
let json: unknown;
|
||||
let err: unknown;
|
||||
const baseUrl = KaiTypeToBaseURL(kaiType);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CreateKaiIIDXClassHandler } from "./class-handler";
|
||||
import { CreateKaiIIDXClassProvider } from "./class-handler";
|
||||
import { CreateKaiReauthFunction } from "../reauth";
|
||||
import { TraverseKaiAPI } from "../traverse-api";
|
||||
import { KaiTypeToBaseURL } from "../utils";
|
||||
@@ -30,7 +30,7 @@ export async function ParseKaiIIDX(
|
||||
context: {
|
||||
service,
|
||||
},
|
||||
classHandler: await CreateKaiIIDXClassHandler(service, authDoc.token, reauthFn, fetch),
|
||||
classProvider: await CreateKaiIIDXClassProvider(service, authDoc.token, reauthFn, fetch),
|
||||
game: "iidx",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CreateKaiSDVXClassHandler } from "./class-handler";
|
||||
import { CreateKaiSDVXClassProvider } from "./class-handler";
|
||||
import { KaiTypeToBaseURL } from "../utils";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { SDVX_DANS } from "tachi-common";
|
||||
@@ -8,10 +8,10 @@ import ResetDBState from "test-utils/resets";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#CreateKaiSDVXClassHandler", async (t) => {
|
||||
t.test("#CreateKaiSDVXClassProvider", async (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
const fn = await CreateKaiSDVXClassHandler(
|
||||
const fn = await CreateKaiSDVXClassProvider(
|
||||
"FLO",
|
||||
"token",
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
@@ -45,7 +45,7 @@ t.test("#CreateKaiSDVXClassHandler", async (t) => {
|
||||
});
|
||||
|
||||
t.test("Should return nothing if dan is not a number", async (t) => {
|
||||
const fn = await CreateKaiSDVXClassHandler(
|
||||
const fn = await CreateKaiSDVXClassProvider(
|
||||
"FLO",
|
||||
"token",
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
@@ -72,7 +72,7 @@ t.test("#CreateKaiSDVXClassHandler", async (t) => {
|
||||
});
|
||||
|
||||
t.test("Should return nothing if dan is too great", async (t) => {
|
||||
const fn = await CreateKaiSDVXClassHandler(
|
||||
const fn = await CreateKaiSDVXClassProvider(
|
||||
"FLO",
|
||||
"token",
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
@@ -99,7 +99,7 @@ t.test("#CreateKaiSDVXClassHandler", async (t) => {
|
||||
});
|
||||
|
||||
t.test("Should return nothing if dan is negative", async (t) => {
|
||||
const fn = await CreateKaiSDVXClassHandler(
|
||||
const fn = await CreateKaiSDVXClassProvider(
|
||||
"FLO",
|
||||
"token",
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
@@ -126,7 +126,7 @@ t.test("#CreateKaiSDVXClassHandler", async (t) => {
|
||||
});
|
||||
|
||||
t.test("Should gracefully handle negative API responses", async (t) => {
|
||||
const fn = await CreateKaiSDVXClassHandler(
|
||||
const fn = await CreateKaiSDVXClassProvider(
|
||||
"FLO",
|
||||
"token",
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
@@ -145,7 +145,7 @@ t.test("#CreateKaiSDVXClassHandler", async (t) => {
|
||||
|
||||
t.test("Should call reauthFn if statusCode is 401", async (t) => {
|
||||
let pass = false;
|
||||
const fn = await CreateKaiSDVXClassHandler(
|
||||
const fn = await CreateKaiSDVXClassProvider(
|
||||
"FLO",
|
||||
"token",
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
@@ -164,7 +164,7 @@ t.test("#CreateKaiSDVXClassHandler", async (t) => {
|
||||
});
|
||||
|
||||
t.test("Should ignore null dans", async (t) => {
|
||||
const fn = await CreateKaiSDVXClassHandler(
|
||||
const fn = await CreateKaiSDVXClassProvider(
|
||||
"FLO",
|
||||
"token",
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
|
||||
@@ -3,14 +3,14 @@ import { SDVX_DANS } from "tachi-common";
|
||||
import nodeFetch from "utils/fetch";
|
||||
import { IsRecord } from "utils/misc";
|
||||
import type { KaiAPIReauthFunction } from "../traverse-api";
|
||||
import type { ClassHandler } from "lib/score-import/framework/profile-calculated-data/types";
|
||||
import type { ClassProvider } from "lib/score-import/framework/profile-calculated-data/types";
|
||||
|
||||
export async function CreateKaiSDVXClassHandler(
|
||||
export async function CreateKaiSDVXClassProvider(
|
||||
kaiType: "EAG" | "FLO" | "MIN",
|
||||
token: string,
|
||||
reauthFn: KaiAPIReauthFunction,
|
||||
fetch = nodeFetch
|
||||
): Promise<ClassHandler> {
|
||||
): Promise<ClassProvider> {
|
||||
let json: unknown;
|
||||
let err: unknown;
|
||||
const baseUrl = KaiTypeToBaseURL(kaiType);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CreateKaiSDVXClassHandler } from "./class-handler";
|
||||
import { CreateKaiSDVXClassProvider } from "./class-handler";
|
||||
import { CreateKaiReauthFunction } from "../reauth";
|
||||
import { TraverseKaiAPI } from "../traverse-api";
|
||||
import { KaiTypeToBaseURL } from "../utils";
|
||||
@@ -30,7 +30,7 @@ export async function ParseKaiSDVX(
|
||||
context: {
|
||||
service,
|
||||
},
|
||||
classHandler: await CreateKaiSDVXClassHandler(service, authDoc.token, reauthFn, fetch),
|
||||
classProvider: await CreateKaiSDVXClassProvider(service, authDoc.token, reauthFn, fetch),
|
||||
game: "sdvx",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error";
|
||||
import { GetGamePTConfig } from "tachi-common";
|
||||
import type { ClassHandler } from "lib/score-import/framework/profile-calculated-data/types";
|
||||
import type { ClassProvider } from "lib/score-import/framework/profile-calculated-data/types";
|
||||
import type { GamePTConfig, GPTString, ClassRecords, Classes } from "tachi-common";
|
||||
|
||||
// Note: This is tested by batch-manuals parser.test.ts.
|
||||
export function CreateBatchManualClassHandler(
|
||||
export function CreateBatchManualClassProvider(
|
||||
classes: Partial<Record<Classes[GPTString], string | null>>
|
||||
): ClassHandler {
|
||||
): ClassProvider {
|
||||
return (game, playtype, userID, ratings, logger) => {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
|
||||
@@ -336,9 +336,9 @@ t.test("#ParserFn", (t) => {
|
||||
logger
|
||||
);
|
||||
|
||||
t.not(res.classHandler, null);
|
||||
t.not(res.classProvider, null);
|
||||
|
||||
t.strictSame(res.classHandler!("iidx", "SP", 1, {}, logger), { dan: IIDX_DANS.KAIDEN });
|
||||
t.strictSame(res.classProvider!("iidx", "SP", 1, {}, logger), { dan: IIDX_DANS.KAIDEN });
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -354,7 +354,7 @@ t.test("#ParserFn", (t) => {
|
||||
logger
|
||||
);
|
||||
|
||||
t.equal(res.classHandler, null);
|
||||
t.equal(res.classProvider, null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CreateBatchManualClassHandler } from "./class-handler";
|
||||
import { CreateBatchManualClassProvider } from "./class-handler";
|
||||
import ScoreImportFatalError from "../../../framework/score-importing/score-import-error";
|
||||
import { TachiConfig } from "lib/setup/config";
|
||||
import { p } from "prudence";
|
||||
@@ -104,8 +104,8 @@ export function ParseBatchManualFromObject(
|
||||
|
||||
// if classes are provided, use those as a class handler. Otherwise, we
|
||||
// don't care.
|
||||
classHandler: batchManual.classes
|
||||
? CreateBatchManualClassHandler(batchManual.classes)
|
||||
classProvider: batchManual.classes
|
||||
? CreateBatchManualClassProvider(batchManual.classes)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ function GenericParseEamIIDXCSV(
|
||||
iterable: iterableData,
|
||||
context,
|
||||
game: "iidx",
|
||||
classHandler: null,
|
||||
classProvider: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ConverterFailure } from "../../framework/common/converter-failures";
|
||||
import type { DryScore } from "../../framework/common/types";
|
||||
import type { ClassHandler } from "../../framework/profile-calculated-data/types";
|
||||
import type { SDVXEamusementCSVData } from "../file/eamusement-sdvx-csv/types";
|
||||
import type { MerScore } from "../file/mer-iidx/types";
|
||||
import type { S3Score } from "../file/solid-state-squad/types";
|
||||
@@ -13,17 +12,12 @@ import type { KsHookSV6CStaticScore } from "../ir/kshook-sv6c-static/types";
|
||||
import type { KsHookSV6CContext, KsHookSV6CScore } from "../ir/kshook-sv6c/types";
|
||||
import type { LR2HookContext, LR2HookScore } from "../ir/lr2hook/types";
|
||||
import type { IRUSCContext } from "../ir/usc/types";
|
||||
import type {
|
||||
CGContext,
|
||||
CGJubeatScore,
|
||||
CGMusecaScore,
|
||||
CGPopnScore,
|
||||
CGSDVXScore,
|
||||
} from "./api-cg/types";
|
||||
import type { CGContext, CGMusecaScore, CGPopnScore, CGSDVXScore } from "./api-cg/types";
|
||||
import type { KaiContext } from "./api-kai/types";
|
||||
import type { BatchManualContext } from "./batch-manual/types";
|
||||
import type { IIDXEamusementCSVContext, IIDXEamusementCSVData } from "./eamusement-iidx-csv/types";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
import type { ClassProvider } from "lib/score-import/framework/calculated-data/types";
|
||||
import type { USCClientScore } from "server/router/ir/usc/_playtype/types";
|
||||
import type {
|
||||
BatchManualScore,
|
||||
@@ -31,7 +25,6 @@ import type {
|
||||
Game,
|
||||
ImportTypes,
|
||||
integer,
|
||||
MongoDBDocument,
|
||||
SongDocument,
|
||||
} from "tachi-common";
|
||||
import type { EmptyObject } from "utils/types";
|
||||
@@ -113,7 +106,7 @@ export interface ImportTypeContextMap {
|
||||
"api/cg-prod-popn": CGContext;
|
||||
}
|
||||
|
||||
export interface OrphanScoreDocument<T extends ImportTypes = ImportTypes> extends MongoDBDocument {
|
||||
export interface OrphanScoreDocument<T extends ImportTypes = ImportTypes> {
|
||||
importType: T;
|
||||
data: ImportTypeDataMap[T];
|
||||
context: ImportTypeContextMap[T];
|
||||
@@ -147,7 +140,7 @@ export interface ParserFunctionReturns<D, C> {
|
||||
iterable: AsyncIterable<D> | Iterable<D>;
|
||||
context: C;
|
||||
game: Game;
|
||||
classHandler: ClassHandler | null;
|
||||
classProvider: ClassProvider | null;
|
||||
}
|
||||
|
||||
export type ParserFunction<D, C, A extends Array<unknown>> = (
|
||||
|
||||
@@ -66,6 +66,6 @@ export default function ParseEamusementSDVXCSV(
|
||||
iterable,
|
||||
context: {},
|
||||
game: "sdvx",
|
||||
classHandler: null,
|
||||
classProvider: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ t.test("#ParseMerIIDX", (t) => {
|
||||
{
|
||||
game: "iidx",
|
||||
context: {},
|
||||
classHandler: null,
|
||||
classProvider: null,
|
||||
iterable: [
|
||||
{
|
||||
music_id: 3007,
|
||||
|
||||
@@ -53,7 +53,7 @@ export function ParseMerIIDX(
|
||||
}
|
||||
|
||||
return {
|
||||
classHandler: null,
|
||||
classProvider: null,
|
||||
context: {},
|
||||
iterable: jsonData as Array<MerScore>,
|
||||
game: "iidx",
|
||||
|
||||
@@ -60,7 +60,7 @@ t.test("#ParseSolidStateXML", (t) => {
|
||||
"Should return the right scores in the iterable."
|
||||
);
|
||||
t.equal(res.game, "iidx", "Should return IIDX as the game.");
|
||||
t.equal(res.classHandler, null, "Should return no class handler.");
|
||||
t.equal(res.classProvider, null, "Should return no class handler.");
|
||||
t.strictSame(res.context, {}, "Should return no context.");
|
||||
|
||||
t.end();
|
||||
@@ -97,7 +97,7 @@ t.test("#ParseSolidStateXML", (t) => {
|
||||
"Should return the right score in the iterable."
|
||||
);
|
||||
t.equal(res.game, "iidx", "Should return IIDX as the game.");
|
||||
t.equal(res.classHandler, null, "Should return no class handler.");
|
||||
t.equal(res.classProvider, null, "Should return no class handler.");
|
||||
t.strictSame(res.context, {}, "Should return no context.");
|
||||
|
||||
t.end();
|
||||
@@ -134,7 +134,7 @@ t.test("#ParseSolidStateXML", (t) => {
|
||||
"Should return the right score in the iterable."
|
||||
);
|
||||
t.equal(res.game, "iidx", "Should return IIDX as the game.");
|
||||
t.equal(res.classHandler, null, "Should return no class handler.");
|
||||
t.equal(res.classProvider, null, "Should return no class handler.");
|
||||
t.strictSame(res.context, {}, "Should return no context.");
|
||||
|
||||
t.end();
|
||||
|
||||
@@ -132,7 +132,7 @@ export function ParseSolidStateXML(
|
||||
}));
|
||||
|
||||
return {
|
||||
classHandler: null,
|
||||
classProvider: null,
|
||||
context: {},
|
||||
iterable: scoreData as Array<S3Score>,
|
||||
game: "iidx",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ScoreImportFatalError from "../../../framework/score-importing/score-import-error";
|
||||
import { WACCA_STAGEUPS } from "tachi-common";
|
||||
import type { ClassHandler } from "../../../framework/profile-calculated-data/types";
|
||||
import type { ClassProvider } from "../../../framework/ugpt-stats/types";
|
||||
import type { MyPagePlayerStage } from "./types";
|
||||
|
||||
const STAGES: Record<number, number> = {
|
||||
@@ -38,7 +38,7 @@ const STAGE_NAMES: Record<number, string> = {
|
||||
14: "XIV",
|
||||
};
|
||||
|
||||
export function CreateMyPageScraperClassHandler(stage: MyPagePlayerStage): ClassHandler {
|
||||
export function CreateMyPageScraperClassProvider(stage: MyPagePlayerStage): ClassProvider {
|
||||
return (_game, _playtype, _userID, _ratings, logger) => {
|
||||
const stageName = STAGE_NAMES[stage.id];
|
||||
|
||||
|
||||
@@ -122,17 +122,17 @@ t.test("#ParseMyPageScraperPlayerCSV", (t) => {
|
||||
|
||||
const file = MockMulterFile(buffer, "player.csv");
|
||||
|
||||
const { iterable, game, classHandler } = ParseMyPageScraperPlayerCSV(file, {}, logger);
|
||||
const { iterable, game, classProvider: classProvider } = ParseMyPageScraperPlayerCSV(file, {}, logger);
|
||||
|
||||
t.equal(game, "wacca");
|
||||
|
||||
t.strictSame(iterable, [], "Should not have any score data.");
|
||||
|
||||
t.not(classHandler, null);
|
||||
t.not(classProvider, null);
|
||||
|
||||
// There's no good way to test that the classHandler got a valid
|
||||
// There's no good way to test that the classProvider got a valid
|
||||
// MyPagePlayerStage, so we just call it to see.
|
||||
t.strictSame(classHandler!("wacca", "Single", 0, {}, logger), {
|
||||
t.strictSame(classProvider!("wacca", "Single", 0, {}, logger), {
|
||||
stageUp: WACCA_STAGEUPS.XII,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CreateMyPageScraperClassHandler } from "./class-handler";
|
||||
import { CreateMyPageScraperClassProvider } from "./class-handler";
|
||||
import ScoreImportFatalError from "../../../framework/score-importing/score-import-error";
|
||||
import { parse, CsvError as CSVError } from "csv-parse/sync";
|
||||
import { p } from "prudence";
|
||||
@@ -111,7 +111,7 @@ export function ParseMyPageScraperRecordsCSV(
|
||||
iterable,
|
||||
context: {},
|
||||
game: "wacca",
|
||||
classHandler: null,
|
||||
classProvider: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -169,6 +169,6 @@ export function ParseMyPageScraperPlayerCSV(
|
||||
iterable: [],
|
||||
context: {},
|
||||
game: "wacca",
|
||||
classHandler: CreateMyPageScraperClassHandler(stage),
|
||||
classProvider: CreateMyPageScraperClassProvider(stage),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,6 +75,6 @@ export function ParseBarbatosSingle(
|
||||
},
|
||||
game: "sdvx",
|
||||
iterable: [body] as unknown as Array<BarbatosScore | BarbatosSDVX6Score>,
|
||||
classHandler: null,
|
||||
classProvider: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -126,6 +126,6 @@ export function ParseBeatorajaSingle(
|
||||
},
|
||||
game: isPMS ? "pms" : "bms",
|
||||
iterable: [score],
|
||||
classHandler: null,
|
||||
classProvider: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CreateFerStaticClassHandler } from "./class-handler";
|
||||
import { CreateFerStaticClassProvider } from "./class-handler";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import t from "tap";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
@@ -6,11 +6,11 @@ import type { Playtype } from "tachi-common";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#FerStaticClassHandler", (t) => {
|
||||
t.test("#FerStaticClassProvider", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should curry a function", (t) => {
|
||||
const res = CreateFerStaticClassHandler({ sp_dan: 1 });
|
||||
const res = CreateFerStaticClassProvider({ sp_dan: 1 });
|
||||
|
||||
t.equal(typeof res, "function");
|
||||
|
||||
@@ -18,7 +18,7 @@ t.test("#FerStaticClassHandler", (t) => {
|
||||
});
|
||||
|
||||
t.test("Should work with no dans", (t) => {
|
||||
const res = CreateFerStaticClassHandler({})("iidx", "SP", 1, {}, logger);
|
||||
const res = CreateFerStaticClassProvider({})("iidx", "SP", 1, {}, logger);
|
||||
|
||||
t.equal(res, undefined, "Should return nothing.");
|
||||
|
||||
@@ -26,7 +26,7 @@ t.test("#FerStaticClassHandler", (t) => {
|
||||
});
|
||||
|
||||
t.test("Should update the same dan as the playtype.", (t) => {
|
||||
const fn = CreateFerStaticClassHandler({ sp_dan: 5, dp_dan: 7 });
|
||||
const fn = CreateFerStaticClassProvider({ sp_dan: 5, dp_dan: 7 });
|
||||
const res = fn("iidx", "SP", 1, {}, logger);
|
||||
|
||||
t.strictSame(res, { dan: 5 }, "Should return SP dan's value.");
|
||||
@@ -39,7 +39,7 @@ t.test("#FerStaticClassHandler", (t) => {
|
||||
});
|
||||
|
||||
t.test("Should skip if dan is invalid.", (t) => {
|
||||
const fn = CreateFerStaticClassHandler({ sp_dan: -1, dp_dan: 100 });
|
||||
const fn = CreateFerStaticClassProvider({ sp_dan: -1, dp_dan: 100 });
|
||||
const res = fn("iidx", "SP", 1, {}, logger);
|
||||
|
||||
t.equal(res, undefined, "Should skip SP dan's value.");
|
||||
@@ -52,7 +52,7 @@ t.test("#FerStaticClassHandler", (t) => {
|
||||
});
|
||||
|
||||
t.test("Should skip if playtype is invalid", (t) => {
|
||||
const fn = CreateFerStaticClassHandler({ sp_dan: 5, dp_dan: 7 });
|
||||
const fn = CreateFerStaticClassProvider({ sp_dan: 5, dp_dan: 7 });
|
||||
const res = fn("iidx", "INVALID" as Playtype, 1, {}, logger);
|
||||
|
||||
t.equal(res, undefined, "Should skip over as a failsafe.");
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { ClassHandler } from "../../../framework/profile-calculated-data/types";
|
||||
import type { ClassProvider } from "lib/score-import/framework/calculated-data/types";
|
||||
import type { integer } from "tachi-common";
|
||||
|
||||
export function CreateFerStaticClassHandler(body: Record<string, unknown>): ClassHandler {
|
||||
return (game, playtype, userID, ratings, logger) => {
|
||||
export function CreateFerStaticClassProvider(body: Record<string, unknown>): ClassProvider {
|
||||
return (gptString, userID, ratings, logger) => {
|
||||
let index;
|
||||
|
||||
if (playtype === "SP") {
|
||||
if (gptString === "iidx:SP") {
|
||||
index = body.sp_dan;
|
||||
} else if (playtype === "DP") {
|
||||
} else if (gptString === "iidx:DP") {
|
||||
index = body.dp_dan;
|
||||
} else {
|
||||
logger.warn(
|
||||
`Invalid playtype ${playtype} passed to FerStaticClassHandler. Attempting to continue.`
|
||||
`Invalid gptString ${gptString} passed to FerStaticClassProvider. Attempting to continue.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export function CreateFerStaticClassHandler(body: Record<string, unknown>): Clas
|
||||
}
|
||||
|
||||
if (!Number.isInteger(index)) {
|
||||
logger.info(`received invalid fer-static class of ${index} (${playtype}).`, { body });
|
||||
logger.info(`received invalid fer-static class of ${index} (${gptString}).`, { body });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ t.test("#ParseFervidexStatic", (t) => {
|
||||
game: "iidx",
|
||||
});
|
||||
|
||||
t.type(res.classHandler, "function", "Should have a function ready for class handling");
|
||||
t.type(res.classProvider, "function", "Should have a function ready for class handling");
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -65,7 +65,7 @@ t.test("#ParseFervidexStatic", (t) => {
|
||||
});
|
||||
|
||||
t.type(
|
||||
res.classHandler,
|
||||
res.classProvider,
|
||||
"function",
|
||||
"Should have a function ready for class handling, regardless of shouldImportScores being false"
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CreateFerStaticClassHandler } from "./class-handler";
|
||||
import { CreateFerStaticClassProvider } from "./class-handler";
|
||||
import { AssertStrAsPositiveInt } from "../../../framework/common/string-asserts";
|
||||
import ScoreImportFatalError from "../../../framework/score-importing/score-import-error";
|
||||
import { SoftwareIDToVersion } from "../fervidex/parser";
|
||||
@@ -22,7 +22,7 @@ export function ParseFervidexStatic(
|
||||
logger: KtLogger
|
||||
): ParserFunctionReturns<FervidexStaticScore, FervidexStaticContext> {
|
||||
const version = SoftwareIDToVersion(headers.model, logger);
|
||||
const classHandler = CreateFerStaticClassHandler(body);
|
||||
const classProvider = CreateFerStaticClassProvider(body);
|
||||
|
||||
// if we shouldn't import scores, just sync up dans.
|
||||
if (!headers.shouldImportScores) {
|
||||
@@ -30,7 +30,7 @@ export function ParseFervidexStatic(
|
||||
context: { version },
|
||||
game: "iidx",
|
||||
iterable: [],
|
||||
classHandler,
|
||||
classProvider,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,6 +90,6 @@ export function ParseFervidexStatic(
|
||||
context: { version },
|
||||
game: "iidx",
|
||||
iterable: scores,
|
||||
classHandler,
|
||||
classProvider,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150,6 +150,6 @@ export function ParseFervidexSingle(
|
||||
context: { version, timeReceived: Date.now(), userID },
|
||||
game: "iidx",
|
||||
iterable: [score],
|
||||
classHandler: null,
|
||||
classProvider: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,6 +41,6 @@ export function ParseKsHookSV6CStatic(
|
||||
game: "sdvx",
|
||||
iterable: data.scores,
|
||||
context: {},
|
||||
classHandler: null,
|
||||
classProvider: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,6 +69,6 @@ export function ParseKsHookSV6C(
|
||||
context: {
|
||||
timeReceived: Date.now(),
|
||||
},
|
||||
classHandler: null,
|
||||
classProvider: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ t.test("#ParseLR2Hook", (t) => {
|
||||
t.equal(res.game, "bms");
|
||||
t.type(res.context.timeReceived, "number");
|
||||
t.ok(Array.isArray(res.iterable));
|
||||
t.equal(res.classHandler, null);
|
||||
t.equal(res.classProvider, null);
|
||||
} catch (err) {
|
||||
t.fail(`[${message}] ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
@@ -83,6 +83,6 @@ export function ParseLR2Hook(
|
||||
context: {
|
||||
timeReceived: Date.now(),
|
||||
},
|
||||
classHandler: null,
|
||||
classProvider: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,6 +67,6 @@ export function ParseIRUSC(
|
||||
},
|
||||
game: "usc",
|
||||
iterable: [score] as Array<USCClientScore>,
|
||||
classHandler: null,
|
||||
classProvider: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { GetAndUpdateUsersGoals } from "lib/score-import/framework/goals/goals";
|
||||
import { UpdateChartRanking } from "lib/score-import/framework/pb/create-pb-doc";
|
||||
import { ProcessPBs } from "lib/score-import/framework/pb/process-pbs";
|
||||
import { UpdateUsersQuests } from "lib/score-import/framework/quests/quests";
|
||||
import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/profile-calculated-data/update-ugs";
|
||||
import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/ugpt-stats/update-ugpt-stats";
|
||||
import { RecalcSessions } from "utils/calculations/recalc-sessions";
|
||||
import type { Game, Playtype, ScoreDocument } from "tachi-common";
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import db from "external/mongo/db";
|
||||
import { GenericCalculatePercent } from "lib/score-import/framework/common/score-utils";
|
||||
import { FormatGame, GetGamePTConfig } from "tachi-common";
|
||||
import { FormatGame, GetGamePTConfig, GetSpecificGPTConfig } from "tachi-common";
|
||||
import { GetFolderForIDGuaranteed, HumaniseChartID } from "utils/db";
|
||||
import { GetFolderChartIDs } from "utils/folder";
|
||||
import { FormatMaxDP, HumanisedJoinArray } from "utils/misc";
|
||||
import type { ChartDocument, Game, GoalDocument, Playtype } from "tachi-common";
|
||||
import type { ChartDocument, Game, GoalDocument, Playtype, GPTString } from "tachi-common";
|
||||
|
||||
export async function CreateGoalTitle(
|
||||
charts: GoalDocument["charts"],
|
||||
@@ -119,8 +118,19 @@ async function FormatCharts(
|
||||
}
|
||||
}
|
||||
|
||||
function FormatCriteria(criteria: GoalDocument["criteria"], game: Game, playtype: Playtype) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
function FormatCriteria<GPT extends GPTString>(
|
||||
criteria: GoalDocument<GPT>["criteria"],
|
||||
gptString: GPT
|
||||
) {
|
||||
const gptConfig = GetSpecificGPTConfig(gptString);
|
||||
|
||||
switch (criteria.key) {
|
||||
case "":
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
switch (criteria.key) {
|
||||
case "scoreData.gradeIndex":
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/profile-calculated-data/update-ugs";
|
||||
import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/ugpt-stats/update-ugpt-stats";
|
||||
import { WrapScriptPromise } from "utils/misc";
|
||||
import { FormatUserDoc } from "utils/user";
|
||||
import type { Game, Playtype, ScoreDocument } from "tachi-common";
|
||||
|
||||
@@ -8,11 +8,12 @@ import { SoftwareIDToVersion } from "lib/score-import/import-types/ir/fervidex/p
|
||||
import { p } from "prudence";
|
||||
import { RequirePermissions } from "server/middleware/auth";
|
||||
import { PrudenceErrorFormatter } from "server/middleware/prudence-validate";
|
||||
import { GetGamePTConfig } from "tachi-common";
|
||||
import { UpdateClassIfGreater } from "utils/class";
|
||||
import { ParseEA3SoftID } from "utils/ea3id";
|
||||
import { IsNullishOrEmptyStr } from "utils/misc";
|
||||
import type { RequestHandler } from "express";
|
||||
import type { integer, Playtypes } from "tachi-common";
|
||||
import type { Playtypes, SpecificGamePTConfig, integer } from "tachi-common";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -316,12 +317,25 @@ router.post("/class/submit", ValidateModelHeader, async (req, res) => {
|
||||
// is 0 or 1.
|
||||
const playtype: Playtypes["iidx"] = body.play_style === 0 ? "SP" : "DP";
|
||||
|
||||
const dans = (
|
||||
GetGamePTConfig("iidx", playtype) as unknown as SpecificGamePTConfig<"iidx:DP" | "iidx:SP">
|
||||
).classes.dan.values;
|
||||
|
||||
const dan = dans[body.course_id];
|
||||
|
||||
if (dan === undefined) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Unknown course_id ${body.course_id}.`,
|
||||
});
|
||||
}
|
||||
|
||||
const r = await UpdateClassIfGreater(
|
||||
req[SYMBOL_TACHI_API_AUTH].userID!,
|
||||
"iidx",
|
||||
playtype,
|
||||
"dan",
|
||||
body.course_id
|
||||
dan.id
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/profile-calculated-data/update-ugs";
|
||||
import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/ugpt-stats/update-ugpt-stats";
|
||||
import { FormatUserDoc } from "utils/user";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CreateCalculatedData } from "lib/score-import/framework/score-calculate
|
||||
import { GetAndUpdateUsersGoals } from "lib/score-import/framework/goals/goals";
|
||||
import { ProcessPBs } from "lib/score-import/framework/pb/process-pbs";
|
||||
import { UpdateUsersQuests } from "lib/score-import/framework/quests/quests";
|
||||
import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/profile-calculated-data/update-ugs";
|
||||
import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/ugpt-stats/update-ugpt-stats";
|
||||
import { TachiConfig } from "lib/setup/config";
|
||||
import { GetGameConfig } from "tachi-common";
|
||||
import { EfficientDBIterate } from "utils/efficient-db-iterate";
|
||||
|
||||
@@ -2,8 +2,8 @@ import db from "external/mongo/db";
|
||||
import { CreateGameSettings } from "lib/game-settings/create-game-settings";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { EmitWebhookEvent } from "lib/webhooks/webhooks";
|
||||
import type { Game, GPTString, integer, Playtype, UserGameStats } from "tachi-common";
|
||||
import type { Classes } from "tachi-common/game-classes";
|
||||
import { GetGPTConfig, GetGPTString } from "tachi-common";
|
||||
import type { Game, GPTString, integer, Playtype, UserGameStats, Classes } from "tachi-common";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -13,15 +13,61 @@ const logger = CreateLogCtx(__filename);
|
||||
* to compare to, and FALSE if it is worse or equal.
|
||||
*/
|
||||
export function ReturnClassIfGreater(
|
||||
gptString: GPTString,
|
||||
classSet: Classes[GPTString],
|
||||
classVal: integer,
|
||||
classVal: string,
|
||||
userGameStats?: UserGameStats | null
|
||||
) {
|
||||
if (!userGameStats || userGameStats.classes[classSet] === undefined) {
|
||||
const gptConfig = GetGPTConfig(gptString);
|
||||
|
||||
const classInfo = gptConfig.classes[classSet];
|
||||
|
||||
if (!classInfo) {
|
||||
logger.warn(
|
||||
`Invalid ReturnClassIfGreater call. Attempted to index set '${classSet}' on ${gptString}. No such class is defined for this game.`
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return classVal > userGameStats.classes[classSet]!;
|
||||
if (!userGameStats) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const prevClass: string | null | undefined = userGameStats.classes[classSet];
|
||||
|
||||
if (prevClass === null || prevClass === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const previousClassIndex = ClassToIndex(gptString, classSet, prevClass);
|
||||
const newClassIndex = ClassToIndex(gptString, classSet, classVal);
|
||||
|
||||
return newClassIndex > previousClassIndex;
|
||||
}
|
||||
|
||||
export function ClassToIndex(gptString: GPTString, classSet: Classes[GPTString], classVal: string) {
|
||||
const gptConfig = GetGPTConfig(gptString);
|
||||
|
||||
const classInfo = gptConfig.classes[classSet];
|
||||
|
||||
if (!classInfo) {
|
||||
logger.warn(
|
||||
`Invalid ClassToIndex call. Attempted to index set '${classSet}' on ${gptString}. No such class is defined for this game. Returning -1.`
|
||||
);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
const v = classInfo.values.map((e) => e.id).indexOf(classVal);
|
||||
|
||||
if (v === -1) {
|
||||
logger.warn(
|
||||
`Attempted to index a class that doesn't exist: ${classVal} on ${classSet} (${gptString}). Returning -1.`
|
||||
);
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,10 +83,12 @@ export async function UpdateClassIfGreater(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
classSet: Classes[GPTString],
|
||||
classVal: integer
|
||||
classVal: string
|
||||
) {
|
||||
const gptString = GetGPTString(game, playtype);
|
||||
|
||||
const userGameStats = await db["game-stats"].findOne({ userID, game, playtype });
|
||||
const isGreater = ReturnClassIfGreater(classSet, classVal, userGameStats);
|
||||
const isGreater = ReturnClassIfGreater(gptString, classSet, classVal, userGameStats);
|
||||
|
||||
if (isGreater === false) {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user