mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-07 14:28:09 +03:00
checkpoint: 25
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
* Common stuff for that all games might want to inherit.
|
||||
*/
|
||||
|
||||
import { p } from "prudence";
|
||||
import type { ConfScoreMetric } from "../../types/metrics";
|
||||
|
||||
/**
|
||||
@@ -19,7 +20,7 @@ import type { ConfScoreMetric } from "../../types/metrics";
|
||||
* as that necessitates that you either can't miss, or can't hit.
|
||||
*/
|
||||
export const FAST_SLOW_MAXCOMBO = {
|
||||
fast: { type: "INTEGER" },
|
||||
slow: { type: "INTEGER" },
|
||||
maxCombo: { type: "INTEGER" },
|
||||
fast: { type: "INTEGER", validate: p.isPositiveInteger },
|
||||
slow: { type: "INTEGER", validate: p.isPositiveInteger },
|
||||
maxCombo: { type: "INTEGER", validate: p.isPositiveInteger },
|
||||
} as const satisfies Record<string, ConfScoreMetric>;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FAST_SLOW_MAXCOMBO } from "./_common";
|
||||
import { ClassValue, zodNonNegativeInt } from "../config-utils";
|
||||
import { p } from "prudence";
|
||||
import { z } from "zod";
|
||||
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
|
||||
|
||||
@@ -134,7 +135,7 @@ const RANDOM_SCHEMA = z.enum(["MIRROR", "NONRAN", "R-RANDOM", "RANDOM", "S-RANDO
|
||||
|
||||
export const BMS_7K_CONF = {
|
||||
providedMetrics: {
|
||||
score: { type: "INTEGER" },
|
||||
score: { type: "INTEGER", chartDependentMax: true },
|
||||
|
||||
lamp: {
|
||||
type: "ENUM",
|
||||
@@ -162,7 +163,7 @@ export const BMS_7K_CONF = {
|
||||
// if #RANDOM is to ever be supported, the user's percent would become
|
||||
// a *mandatory* metric, as a chart's notecount can be completely unknown.
|
||||
// However, supporting #RANDOM is an awful pain, so I don't really care.
|
||||
percent: { type: "DECIMAL" },
|
||||
percent: { type: "DECIMAL", validate: p.isBetween(0, 100) },
|
||||
},
|
||||
|
||||
defaultMetric: "percent",
|
||||
@@ -170,19 +171,19 @@ export const BMS_7K_CONF = {
|
||||
|
||||
optionalMetrics: {
|
||||
...FAST_SLOW_MAXCOMBO,
|
||||
bp: { type: "INTEGER" },
|
||||
gauge: { type: "DECIMAL" },
|
||||
gaugeHistory: { type: "GRAPH" },
|
||||
epg: { type: "INTEGER" },
|
||||
egr: { type: "INTEGER" },
|
||||
egd: { type: "INTEGER" },
|
||||
ebd: { type: "INTEGER" },
|
||||
epr: { type: "INTEGER" },
|
||||
lpg: { type: "INTEGER" },
|
||||
lgr: { type: "INTEGER" },
|
||||
lgd: { type: "INTEGER" },
|
||||
lbd: { type: "INTEGER" },
|
||||
lpr: { type: "INTEGER" },
|
||||
bp: { type: "INTEGER", validate: p.isPositive },
|
||||
gauge: { type: "DECIMAL", validate: p.isBetween(0, 100) },
|
||||
gaugeHistory: { type: "GRAPH", validate: p.isBetween(0, 100) },
|
||||
epg: { type: "INTEGER", validate: p.isPositive },
|
||||
egr: { type: "INTEGER", validate: p.isPositive },
|
||||
egd: { type: "INTEGER", validate: p.isPositive },
|
||||
ebd: { type: "INTEGER", validate: p.isPositive },
|
||||
epr: { type: "INTEGER", validate: p.isPositive },
|
||||
lpg: { type: "INTEGER", validate: p.isPositive },
|
||||
lgr: { type: "INTEGER", validate: p.isPositive },
|
||||
lgd: { type: "INTEGER", validate: p.isPositive },
|
||||
lbd: { type: "INTEGER", validate: p.isPositive },
|
||||
lpr: { type: "INTEGER", validate: p.isPositive },
|
||||
},
|
||||
|
||||
scoreRatingAlgs: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FAST_SLOW_MAXCOMBO } from "./_common";
|
||||
import { ClassValue, zodNonNegativeInt } from "../config-utils";
|
||||
import { p } from "prudence";
|
||||
import { z } from "zod";
|
||||
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
|
||||
|
||||
@@ -28,7 +29,7 @@ export const CHUNITHMColours = [
|
||||
|
||||
export const CHUNITHM_SINGLE_CONF = {
|
||||
providedMetrics: {
|
||||
score: { type: "INTEGER" },
|
||||
score: { type: "INTEGER", validate: p.isBetween(0, 1_010_000) },
|
||||
lamp: {
|
||||
type: "ENUM",
|
||||
values: ["FAILED", "CLEAR", "FULL COMBO", "ALL JUSTICE", "ALL JUSTICE CRITICAL"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FAST_SLOW_MAXCOMBO } from "./_common";
|
||||
import { ClassValue, zodNonNegativeInt } from "../config-utils";
|
||||
import { p } from "prudence";
|
||||
import { z } from "zod";
|
||||
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
|
||||
|
||||
@@ -35,7 +36,7 @@ const GitadoraColours = [
|
||||
|
||||
export const GITADORA_GITA_CONF = {
|
||||
providedMetrics: {
|
||||
percent: { type: "DECIMAL" },
|
||||
percent: { type: "DECIMAL", validate: p.isBetween(0, 100) },
|
||||
lamp: {
|
||||
type: "ENUM",
|
||||
values: ["FAILED", "CLEAR", "FULL COMBO", "EXCELLENT"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FAST_SLOW_MAXCOMBO } from "./_common";
|
||||
import { ClassValue, zodNonNegativeInt, zodTierlistData } from "../config-utils";
|
||||
import { p } from "prudence";
|
||||
import { z } from "zod";
|
||||
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
|
||||
|
||||
@@ -49,7 +50,7 @@ const RANDOM_SCHEMA = z.enum(["NONRAN", "MIRROR", "R-RANDOM", "RANDOM", "S-RANDO
|
||||
|
||||
export const IIDX_SP_CONF = {
|
||||
providedMetrics: {
|
||||
score: { type: "INTEGER" },
|
||||
score: { type: "INTEGER", chartDependentMax: true },
|
||||
lamp: {
|
||||
type: "ENUM",
|
||||
values: [
|
||||
@@ -69,6 +70,7 @@ export const IIDX_SP_CONF = {
|
||||
derivedMetrics: {
|
||||
percent: {
|
||||
type: "DECIMAL",
|
||||
validate: p.isBetween(0, 100),
|
||||
},
|
||||
grade: {
|
||||
type: "ENUM",
|
||||
@@ -83,21 +85,21 @@ export const IIDX_SP_CONF = {
|
||||
optionalMetrics: {
|
||||
...FAST_SLOW_MAXCOMBO,
|
||||
|
||||
bp: { type: "INTEGER" },
|
||||
gauge: { type: "DECIMAL" },
|
||||
comboBreak: { type: "INTEGER" },
|
||||
bp: { type: "INTEGER", validate: p.isPositive },
|
||||
gauge: { type: "DECIMAL", validate: p.isBetween(0, 100) },
|
||||
comboBreak: { type: "INTEGER", validate: p.isPositive },
|
||||
|
||||
// The players history for the gauge type they were playing on.
|
||||
// this may fall into "NULL" if the user fails.
|
||||
gaugeHistory: { type: "NULLABLE_GRAPH" },
|
||||
scoreHistory: { type: "NULLABLE_GRAPH" },
|
||||
gaugeHistory: { type: "NULLABLE_GRAPH", validate: p.isBetween(0, 100) },
|
||||
scoreHistory: { type: "NULLABLE_GRAPH", validate: p.isBetween(0, 100) },
|
||||
|
||||
// if "GSM" is enabled (via fervidex.dll) then all graphs
|
||||
// are sent. we should store all of them.
|
||||
gsmEasy: { type: "NULLABLE_GRAPH" },
|
||||
gsmNormal: { type: "NULLABLE_GRAPH" },
|
||||
gsmHard: { type: "NULLABLE_GRAPH" },
|
||||
gsmEXHard: { type: "NULLABLE_GRAPH" },
|
||||
gsmEasy: { type: "NULLABLE_GRAPH", validate: p.isBetween(0, 100) },
|
||||
gsmNormal: { type: "NULLABLE_GRAPH", validate: p.isBetween(0, 100) },
|
||||
gsmHard: { type: "NULLABLE_GRAPH", validate: p.isBetween(0, 100) },
|
||||
gsmEXHard: { type: "NULLABLE_GRAPH", validate: p.isBetween(0, 100) },
|
||||
},
|
||||
|
||||
defaultScoreRatingAlg: "ktLampRating",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NoDecimalPlace, zodNonNegativeInt } from "../config-utils";
|
||||
import { p } from "prudence";
|
||||
import { z } from "zod";
|
||||
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
|
||||
|
||||
@@ -14,13 +15,13 @@ export const ITG_CONF = {
|
||||
|
||||
export const ITG_STAMINA_CONF = {
|
||||
providedMetrics: {
|
||||
scorePercent: { type: "DECIMAL" },
|
||||
scorePercent: { type: "DECIMAL", validate: p.isBetween(0, 100) },
|
||||
|
||||
// How far through the chart did they get?
|
||||
// 100 means they cleared.
|
||||
// 50 means they got halfway through.
|
||||
// 0 means they died instantly etc.
|
||||
survivedPercent: { type: "DECIMAL" },
|
||||
survivedPercent: { type: "DECIMAL", validate: p.isBetween(0, 100) },
|
||||
|
||||
lamp: {
|
||||
type: "ENUM",
|
||||
@@ -46,11 +47,14 @@ export const ITG_STAMINA_CONF = {
|
||||
// #1 CLEARED with 30% (130)
|
||||
// #2 FAILED 95% in with 30% (95)
|
||||
// #3 FAILED 40% in with 70% (40)
|
||||
finalPercent: { type: "DECIMAL" },
|
||||
//
|
||||
// NOTE that in our ITG implementation we don't allow for negative percents
|
||||
// as it breaks this silly metric. Also negative percents are stupid.
|
||||
finalPercent: { type: "DECIMAL", validate: p.isBetween(0, 200) },
|
||||
},
|
||||
|
||||
optionalMetrics: {
|
||||
lifebarHistory: { type: "GRAPH" },
|
||||
lifebarHistory: { type: "GRAPH", validate: p.isBetween(0, 100) },
|
||||
},
|
||||
|
||||
defaultMetric: "finalPercent",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FAST_SLOW_MAXCOMBO } from "./_common";
|
||||
import { ClassValue, zodNonNegativeInt } from "../config-utils";
|
||||
import { p } from "prudence";
|
||||
import { z } from "zod";
|
||||
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
|
||||
|
||||
@@ -27,8 +28,8 @@ const JubeatColours = [
|
||||
|
||||
export const JUBEAT_SINGLE_CONF = {
|
||||
providedMetrics: {
|
||||
score: { type: "INTEGER" },
|
||||
musicRate: { type: "DECIMAL" },
|
||||
score: { type: "INTEGER", validate: p.isBetween(0, 1_000_000) },
|
||||
musicRate: { type: "DECIMAL", chartDependentMax: true },
|
||||
lamp: {
|
||||
type: "ENUM",
|
||||
values: ["FAILED", "CLEAR", "FULL COMBO", "EXCELLENT"],
|
||||
@@ -90,17 +91,7 @@ export const JUBEAT_SINGLE_CONF = {
|
||||
|
||||
orderedJudgements: ["perfect", "great", "good", "poor", "miss"],
|
||||
|
||||
versions: [
|
||||
"jubeat",
|
||||
"ripples",
|
||||
"knit",
|
||||
"copious",
|
||||
"saucer",
|
||||
"prop",
|
||||
"Qubell",
|
||||
"clan",
|
||||
"festo",
|
||||
],
|
||||
versions: ["jubeat", "ripples", "knit", "copious", "saucer", "prop", "Qubell", "clan", "festo"],
|
||||
|
||||
chartData: z.strictObject({
|
||||
inGameID: z.union([z.array(zodNonNegativeInt), zodNonNegativeInt]),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FAST_SLOW_MAXCOMBO } from "./_common";
|
||||
import { ClassValue, NoDecimalPlace } from "../config-utils";
|
||||
import { p } from "prudence";
|
||||
import { z } from "zod";
|
||||
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
|
||||
|
||||
@@ -56,7 +57,7 @@ const MaimaiDXColours = [
|
||||
|
||||
export const MAIMAI_DX_SINGLE_CONF = {
|
||||
providedMetrics: {
|
||||
percent: { type: "DECIMAL" },
|
||||
percent: { type: "DECIMAL", validate: p.isBetween(0, 101) },
|
||||
lamp: {
|
||||
type: "ENUM",
|
||||
values: ["FAILED", "CLEAR", "FULL COMBO", "FULL COMBO+", "ALL PERFECT", "ALL PERFECT+"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FAST_SLOW_MAXCOMBO } from "./_common";
|
||||
import { NoDecimalPlace, zodNonNegativeInt } from "../config-utils";
|
||||
import { p } from "prudence";
|
||||
import { z } from "zod";
|
||||
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
|
||||
|
||||
@@ -16,7 +17,7 @@ export const MUSECA_CONF = {
|
||||
|
||||
export const MUSECA_SINGLE_CONF = {
|
||||
providedMetrics: {
|
||||
score: { type: "INTEGER" },
|
||||
score: { type: "INTEGER", validate: p.isBetween(0, 1_000_000) },
|
||||
lamp: {
|
||||
type: "ENUM",
|
||||
values: ["FAILED", "CLEAR", "CONNECT ALL", "PERFECT CONNECT ALL"],
|
||||
|
||||
@@ -41,7 +41,7 @@ function FormatSieglindePMS(sgl: number): string {
|
||||
|
||||
export const PMS_CONTROLLER_CONF = {
|
||||
providedMetrics: {
|
||||
score: { type: "INTEGER" },
|
||||
score: { type: "INTEGER", chartDependentMax: true },
|
||||
|
||||
lamp: {
|
||||
type: "ENUM",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FAST_SLOW_MAXCOMBO } from "./_common";
|
||||
import { ClassValue, zodNonNegativeInt } from "../config-utils";
|
||||
import { p } from "prudence";
|
||||
import { z } from "zod";
|
||||
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
|
||||
|
||||
@@ -27,7 +28,7 @@ const PopnClasses = [
|
||||
|
||||
export const POPN_9B_CONF = {
|
||||
providedMetrics: {
|
||||
score: { type: "INTEGER" },
|
||||
score: { type: "INTEGER", validate: p.isBetween(0, 100_000) },
|
||||
clearMedal: {
|
||||
type: "ENUM",
|
||||
values: [
|
||||
@@ -69,7 +70,7 @@ export const POPN_9B_CONF = {
|
||||
|
||||
optionalMetrics: {
|
||||
...FAST_SLOW_MAXCOMBO,
|
||||
gauge: { type: "INTEGER" },
|
||||
gauge: { type: "INTEGER", validate: p.isBetween(0, 100) },
|
||||
},
|
||||
|
||||
scoreRatingAlgs: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FAST_SLOW_MAXCOMBO } from "./_common";
|
||||
import { ClassValue, ToDecimalPlaces, zodNonNegativeInt, zodTierlistData } from "../config-utils";
|
||||
import { p } from "prudence";
|
||||
import { z } from "zod";
|
||||
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
|
||||
|
||||
@@ -72,7 +73,7 @@ export const SDVXVFClasses = [
|
||||
|
||||
export const SDVX_SINGLE_CONF = {
|
||||
providedMetrics: {
|
||||
score: { type: "INTEGER" },
|
||||
score: { type: "INTEGER", validate: p.isBetween(0, 10_000_000) },
|
||||
lamp: {
|
||||
type: "ENUM",
|
||||
values: [
|
||||
@@ -99,8 +100,8 @@ export const SDVX_SINGLE_CONF = {
|
||||
|
||||
optionalMetrics: {
|
||||
...FAST_SLOW_MAXCOMBO,
|
||||
exScore: { type: "INTEGER" },
|
||||
gauge: { type: "DECIMAL" },
|
||||
exScore: { type: "INTEGER", chartDependentMax: true },
|
||||
gauge: { type: "DECIMAL", validate: p.isBetween(0, 100) },
|
||||
},
|
||||
|
||||
scoreRatingAlgs: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FAST_SLOW_MAXCOMBO } from "./_common";
|
||||
import { SDVXVFClasses } from "./sdvx";
|
||||
import { ToDecimalPlaces } from "../config-utils";
|
||||
import { p } from "prudence";
|
||||
import { z } from "zod";
|
||||
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
|
||||
|
||||
@@ -13,7 +14,7 @@ export const USC_CONF = {
|
||||
|
||||
export const USC_CONTROLLER_CONF = {
|
||||
providedMetrics: {
|
||||
score: { type: "INTEGER" },
|
||||
score: { type: "INTEGER", validate: p.isBetween(0, 10_000_000) },
|
||||
lamp: {
|
||||
type: "ENUM",
|
||||
values: [
|
||||
@@ -40,7 +41,7 @@ export const USC_CONTROLLER_CONF = {
|
||||
|
||||
optionalMetrics: {
|
||||
...FAST_SLOW_MAXCOMBO,
|
||||
gauge: { type: "DECIMAL" },
|
||||
gauge: { type: "DECIMAL", validate: p.isBetween(0, 100) },
|
||||
},
|
||||
|
||||
scoreRatingAlgs: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FAST_SLOW_MAXCOMBO } from "./_common";
|
||||
import { ClassValue } from "../config-utils";
|
||||
import { p } from "prudence";
|
||||
import { z } from "zod";
|
||||
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
|
||||
|
||||
@@ -46,7 +47,7 @@ export const WaccaColours = [
|
||||
|
||||
export const WACCA_SINGLE_CONF = {
|
||||
providedMetrics: {
|
||||
score: { type: "INTEGER" },
|
||||
score: { type: "INTEGER", validate: p.isBetween(0, 1_000_000) },
|
||||
lamp: {
|
||||
type: "ENUM",
|
||||
values: ["FAILED", "CLEAR", "MISSLESS", "FULL COMBO", "ALL MARVELOUS"],
|
||||
|
||||
@@ -471,7 +471,8 @@ export interface PBReference {
|
||||
}
|
||||
|
||||
export interface PBScoreDocument<GPT extends GPTString = GPTString> {
|
||||
composedFrom: Array<PBReference>;
|
||||
// guaranteed to atleast have one element.
|
||||
composedFrom: [PBReference, ...Array<PBReference>];
|
||||
rankingData: {
|
||||
rank: integer;
|
||||
outOf: integer;
|
||||
|
||||
+47
-18
@@ -20,27 +20,48 @@ import type {
|
||||
// - Additional: They **may** exist. We want to store them if they exist, but don't
|
||||
// mandate their existence (i.e. fast/slow/maxCombo)
|
||||
|
||||
export type DecimalMetricValidator<GPT extends GPTString> = (
|
||||
metric: number,
|
||||
chart: ChartDocument<GPT>
|
||||
) => string | true;
|
||||
export type IntegerMetricValidator<GPT extends GPTString> = (
|
||||
metric: integer,
|
||||
chart: ChartDocument<GPT>
|
||||
) => string | true;
|
||||
export type GraphMetricValidator<GPT extends GPTString> = (
|
||||
metric: Array<number>,
|
||||
chart: ChartDocument<GPT>
|
||||
) => string | true;
|
||||
|
||||
export interface ConfDecimalScoreMetric {
|
||||
interface ConfDecimalScoreMetricNormal {
|
||||
type: "DECIMAL";
|
||||
|
||||
validate: (v: number) => string | true;
|
||||
}
|
||||
|
||||
export interface ConfIntegerScoreMetric {
|
||||
interface ConfIntegerScoreMetricNormal {
|
||||
type: "INTEGER";
|
||||
|
||||
validate: (v: number) => string | true;
|
||||
}
|
||||
|
||||
interface ConfDecimalScoreMetricChartDependent {
|
||||
type: "DECIMAL";
|
||||
|
||||
/**
|
||||
* Is the maximum/minimum value of this metric chart dependent?
|
||||
*
|
||||
* @example: IIDX's EX Score is upperbounded at 2x the chart's notecount.
|
||||
*/
|
||||
chartDependentMax: true;
|
||||
}
|
||||
|
||||
interface ConfIntegerScoreMetricChartDependent {
|
||||
type: "INTEGER";
|
||||
|
||||
/**
|
||||
* Is the maximum/minimum value of this metric chart dependent?
|
||||
*
|
||||
* @example: IIDX's EX Score is upperbounded at 2x the chart's notecount.
|
||||
*/
|
||||
chartDependentMax: true;
|
||||
}
|
||||
|
||||
export type ConfDecimalScoreMetric =
|
||||
| ConfDecimalScoreMetricChartDependent
|
||||
| ConfDecimalScoreMetricNormal;
|
||||
|
||||
export type ConfIntegerScoreMetric =
|
||||
| ConfIntegerScoreMetricChartDependent
|
||||
| ConfIntegerScoreMetricNormal;
|
||||
|
||||
/**
|
||||
* A metric for a score that represents an enum.
|
||||
*
|
||||
@@ -60,6 +81,9 @@ export interface ConfEnumScoreMetric<V extends string> {
|
||||
*/
|
||||
export interface ConfGraphScoreMetric {
|
||||
type: "GRAPH";
|
||||
|
||||
validate: (v: number) => string | true;
|
||||
size?: (v: number) => string | true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,6 +91,9 @@ export interface ConfGraphScoreMetric {
|
||||
*/
|
||||
export interface ConfNullableGraphScoreMetric {
|
||||
type: "NULLABLE_GRAPH";
|
||||
|
||||
validate: (v: number) => string | true;
|
||||
size?: (v: number) => string | true;
|
||||
}
|
||||
|
||||
export type ConfScoreMetric =
|
||||
@@ -116,7 +143,7 @@ export type ExtractEnumMetricNames<R extends Record<string, ConfScoreMetric>> =
|
||||
/**
|
||||
* What are all the metrics available for this GPT?
|
||||
*/
|
||||
export type AllMetrics = {
|
||||
export type AllConfMetrics = {
|
||||
[GPT in GPTString]: ConfDerivedMetrics[GPT] &
|
||||
ConfOptionalMetrics[GPT] &
|
||||
ConfProvidedMetrics[GPT];
|
||||
@@ -129,8 +156,10 @@ export type AllMetrics = {
|
||||
*/
|
||||
export type GetEnumValue<
|
||||
GPT extends GPTString,
|
||||
MetricName extends ExtractEnumMetricNames<AllMetrics[GPT]>
|
||||
> = AllMetrics[GPT][MetricName] extends ConfEnumScoreMetric<infer EnumValues> ? EnumValues : never;
|
||||
MetricName extends ExtractEnumMetricNames<AllConfMetrics[GPT]>
|
||||
> = AllConfMetrics[GPT][MetricName] extends ConfEnumScoreMetric<infer EnumValues>
|
||||
? EnumValues
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Turn a record of ConfigScoreMetrics into their actual literal values.
|
||||
|
||||
Vendored
+1
-1
@@ -34,7 +34,7 @@ const staticIndexes: Partial<Record<Databases, Array<Index>>> = {
|
||||
],
|
||||
"personal-bests": [
|
||||
index({ chartID: 1, userID: 1 }, UNIQUE),
|
||||
index({ chartID: 1, "scoreData.percent": 1 }),
|
||||
index({ chartID: 1, [`scoreData.${gptConfig.defaultMetric}`]: 1 }),
|
||||
index({ userID: 1, game: 1, playtype: 1 }),
|
||||
|
||||
// activity
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { IIDXLIKE_DERIVERS, IIDXLIKE_VALIDATORS, SDVXLIKE_DERIVERS } from "./games/_common";
|
||||
import { CHUNITHM_IMPL } from "./games/chunithm";
|
||||
import { GITADORA_DORA_IMPL, GITADORA_GITA_IMPL } from "./games/gitadora";
|
||||
import { ITG_STAMINA_IMPL } from "./games/itg";
|
||||
import { JUBEAT_IMPL } from "./games/jubeat";
|
||||
import { MAIMAIDX_IMPL } from "./games/maimaidx";
|
||||
import { MUSECA_IMPL } from "./games/museca";
|
||||
import { POPN_9B_IMPL } from "./games/popn";
|
||||
import { WACCA_IMPL } from "./games/wacca";
|
||||
import type { GPTImplementations } from "./types";
|
||||
|
||||
/**
|
||||
* Server-Specific implementation details for games. These handle things like validating
|
||||
* input for chart-specific metrics (i.e EXScore in IIDX is upper-bounded by
|
||||
* a chart's notecount * 2) and also instructions on how to derive metrics from
|
||||
* the provided metrics.
|
||||
*
|
||||
* Basically, anything that can't be done in the common config, specific to the server.
|
||||
*/
|
||||
export const GPT_SERVER_IMPLEMENTATIONS: GPTImplementations = {
|
||||
"bms:14K": {
|
||||
derivers: IIDXLIKE_DERIVERS,
|
||||
validators: IIDXLIKE_VALIDATORS,
|
||||
},
|
||||
"bms:7K": {
|
||||
derivers: IIDXLIKE_DERIVERS,
|
||||
validators: IIDXLIKE_VALIDATORS,
|
||||
},
|
||||
"pms:Controller": {
|
||||
derivers: IIDXLIKE_DERIVERS,
|
||||
validators: IIDXLIKE_VALIDATORS,
|
||||
},
|
||||
"pms:Keyboard": {
|
||||
derivers: IIDXLIKE_DERIVERS,
|
||||
validators: IIDXLIKE_VALIDATORS,
|
||||
},
|
||||
"iidx:SP": {
|
||||
derivers: IIDXLIKE_DERIVERS,
|
||||
validators: IIDXLIKE_VALIDATORS,
|
||||
},
|
||||
"iidx:DP": {
|
||||
derivers: IIDXLIKE_DERIVERS,
|
||||
validators: IIDXLIKE_VALIDATORS,
|
||||
},
|
||||
|
||||
"wacca:Single": WACCA_IMPL,
|
||||
"chunithm:Single": CHUNITHM_IMPL,
|
||||
"gitadora:Dora": GITADORA_DORA_IMPL,
|
||||
"gitadora:Gita": GITADORA_GITA_IMPL,
|
||||
"itg:Stamina": ITG_STAMINA_IMPL,
|
||||
"jubeat:Single": JUBEAT_IMPL,
|
||||
"maimaidx:Single": MAIMAIDX_IMPL,
|
||||
"museca:Single": MUSECA_IMPL,
|
||||
"popn:9B": POPN_9B_IMPL,
|
||||
"sdvx:Single": {
|
||||
derivers: SDVXLIKE_DERIVERS,
|
||||
validators: {
|
||||
exScore: (exScore, chart) => {
|
||||
// gotta figure this out somehow?
|
||||
throw new Error(`Unimplemented.`);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
"usc:Controller": { derivers: SDVXLIKE_DERIVERS, validators: {} },
|
||||
"usc:Keyboard": { derivers: SDVXLIKE_DERIVERS, validators: {} },
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import { InternalFailure } from "lib/score-import/framework/common/converter-failures";
|
||||
import { SDVXLIKE_GBOUNDARIES, IIDXLIKE_GBOUNDARIES } from "tachi-common";
|
||||
import type {
|
||||
ChartSpecificMetricValidator,
|
||||
GPTDerivers,
|
||||
GPTMetricValidators,
|
||||
} from "game-implementations/types";
|
||||
import type { GradeBoundary, GPTStrings, integer } from "tachi-common";
|
||||
|
||||
/**
|
||||
* Util for getting a games' grade for a given score.
|
||||
*/
|
||||
export function GetGrade<G extends string>(grades: Array<GradeBoundary<G>>, score: number): G {
|
||||
// sort grades going downwards in their boundaries.
|
||||
const descendingGrades = grades.slice(0).sort((a, b) => b.lowerBound - a.lowerBound);
|
||||
|
||||
for (const { name, lowerBound } of descendingGrades) {
|
||||
if (score >= lowerBound) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InternalFailure(`Could not resolve grade for score ${score}.`);
|
||||
}
|
||||
|
||||
export const EX_SCORE_CHECK: ChartSpecificMetricValidator<IIDXLikes> = (exScore, chart) => {
|
||||
if (exScore < 0) {
|
||||
return `EX Score cannot be negative. Got ${exScore}.`;
|
||||
}
|
||||
|
||||
if (exScore > chart.data.notecount * 2) {
|
||||
return `EX Score cannot be greater than ${
|
||||
chart.data.notecount * 2
|
||||
} for this chart. Got ${exScore}.`;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
function calculateIIDXLikePercent(exScore: integer, notecount: integer) {
|
||||
return exScore / (notecount * 2);
|
||||
}
|
||||
|
||||
type IIDXLikes = GPTStrings["bms" | "iidx" | "pms"];
|
||||
|
||||
/**
|
||||
* Derivers for both IIDX SP and DP.
|
||||
*
|
||||
* and BMS. and PMS. They use the same things.
|
||||
*/
|
||||
export const IIDXLIKE_DERIVERS: GPTDerivers<IIDXLikes> = {
|
||||
percent: ({ score }, chart) => calculateIIDXLikePercent(score, chart.data.notecount),
|
||||
grade: ({ score }, chart) => {
|
||||
const percent = calculateIIDXLikePercent(score, chart.data.notecount);
|
||||
|
||||
return GetGrade(IIDXLIKE_GBOUNDARIES, percent);
|
||||
},
|
||||
};
|
||||
|
||||
export const IIDXLIKE_VALIDATORS: GPTMetricValidators<IIDXLikes> = { score: EX_SCORE_CHECK };
|
||||
|
||||
type SDVXLikes = GPTStrings["sdvx" | "usc"];
|
||||
|
||||
export const SDVXLIKE_DERIVERS: GPTDerivers<SDVXLikes> = {
|
||||
grade: ({ score }) => GetGrade(SDVXLIKE_GBOUNDARIES, score),
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { GetGrade } from "./_common";
|
||||
import { CHUNITHM_GBOUNDARIES } from "tachi-common";
|
||||
import type { GPTServerImplementation } from "game-implementations/types";
|
||||
|
||||
export const CHUNITHM_IMPL: GPTServerImplementation<"chunithm:Single"> = {
|
||||
validators: {},
|
||||
derivers: {
|
||||
grade: ({ score }) => GetGrade(CHUNITHM_GBOUNDARIES, score),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { GetGrade } from "./_common";
|
||||
import { GITADORA_GBOUNDARIES } from "tachi-common";
|
||||
import type { GPTServerImplementation } from "game-implementations/types";
|
||||
|
||||
export const GITADORA_GITA_IMPL: GPTServerImplementation<"gitadora:Gita"> = {
|
||||
validators: {},
|
||||
derivers: {
|
||||
grade: ({ percent }) => GetGrade(GITADORA_GBOUNDARIES, percent),
|
||||
},
|
||||
};
|
||||
|
||||
export const GITADORA_DORA_IMPL: GPTServerImplementation<"gitadora:Dora"> = {
|
||||
validators: {},
|
||||
derivers: {
|
||||
grade: ({ percent }) => GetGrade(GITADORA_GBOUNDARIES, percent),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { GetGrade } from "./_common";
|
||||
import { ITG_GBOUNDARIES } from "tachi-common";
|
||||
import type { GPTServerImplementation } from "game-implementations/types";
|
||||
|
||||
export const ITG_STAMINA_IMPL: GPTServerImplementation<"itg:Stamina"> = {
|
||||
validators: {},
|
||||
derivers: {
|
||||
finalPercent: (metrics) => {
|
||||
// *important*
|
||||
// don't check if metrics.survivedPercent === 100, as due to floating
|
||||
// point inaccuracies, it's possible to have a 100% fail
|
||||
// (on extremely long charts, for example)
|
||||
if (metrics.lamp === "FAILED") {
|
||||
return metrics.survivedPercent;
|
||||
}
|
||||
|
||||
return 100 + metrics.scorePercent;
|
||||
},
|
||||
grade: ({ scorePercent, lamp }) => {
|
||||
if (lamp === "FAILED") {
|
||||
return "F";
|
||||
}
|
||||
|
||||
return GetGrade(ITG_GBOUNDARIES, scorePercent);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { GetGrade } from "./_common";
|
||||
import { p } from "prudence";
|
||||
import { JUBEAT_GBOUNDARIES } from "tachi-common";
|
||||
import type { GPTServerImplementation } from "game-implementations/types";
|
||||
|
||||
export const JUBEAT_IMPL: GPTServerImplementation<"jubeat:Single"> = {
|
||||
validators: {
|
||||
musicRate: (rate, chart) => {
|
||||
switch (chart.difficulty) {
|
||||
case "BSC":
|
||||
case "ADV":
|
||||
case "EXT":
|
||||
return p.isBetween(0, 100)(rate);
|
||||
|
||||
case "HARD BSC":
|
||||
case "HARD ADV":
|
||||
case "HARD EXT":
|
||||
return p.isBetween(0, 120)(rate);
|
||||
}
|
||||
},
|
||||
},
|
||||
derivers: {
|
||||
grade: ({ score }) => GetGrade(JUBEAT_GBOUNDARIES, score),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { GetGrade } from "./_common";
|
||||
import { MAIMAIDX_GBOUNDARIES } from "tachi-common";
|
||||
import type { GPTServerImplementation } from "game-implementations/types";
|
||||
|
||||
export const MAIMAIDX_IMPL: GPTServerImplementation<"maimaidx:Single"> = {
|
||||
validators: {},
|
||||
derivers: {
|
||||
grade: ({ percent }) => GetGrade(MAIMAIDX_GBOUNDARIES, percent),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { GetGrade } from "./_common";
|
||||
import { MUSECA_GBOUNDARIES } from "tachi-common";
|
||||
import type { GPTServerImplementation } from "game-implementations/types";
|
||||
|
||||
export const MUSECA_IMPL: GPTServerImplementation<"museca:Single"> = {
|
||||
validators: {},
|
||||
derivers: {
|
||||
grade: ({ score }) => GetGrade(MUSECA_GBOUNDARIES, score),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { GetGrade } from "./_common";
|
||||
import { POPN_GBOUNDARIES } from "tachi-common";
|
||||
import type { GPTServerImplementation } from "game-implementations/types";
|
||||
import type { GetEnumValue } from "tachi-common/types/metrics";
|
||||
|
||||
export function PopnClearMedalToLamp(
|
||||
clearMedal: GetEnumValue<"popn:9B", "clearMedal">
|
||||
): GetEnumValue<"popn:9B", "lamp"> {
|
||||
switch (clearMedal) {
|
||||
case "perfect":
|
||||
return "PERFECT";
|
||||
case "fullComboCircle":
|
||||
case "fullComboDiamond":
|
||||
case "fullComboStar":
|
||||
return "FULL COMBO";
|
||||
case "clearCircle":
|
||||
case "clearDiamond":
|
||||
case "clearStar":
|
||||
return "CLEAR";
|
||||
case "easyClear":
|
||||
return "EASY CLEAR";
|
||||
case "failedCircle":
|
||||
case "failedDiamond":
|
||||
case "failedStar":
|
||||
return "FAILED";
|
||||
}
|
||||
}
|
||||
|
||||
export const POPN_9B_IMPL: GPTServerImplementation<"popn:9B"> = {
|
||||
validators: {},
|
||||
derivers: {
|
||||
lamp: ({ clearMedal }) => PopnClearMedalToLamp(clearMedal),
|
||||
grade: ({ score, clearMedal }) => {
|
||||
const gradeString = GetGrade(POPN_GBOUNDARIES, score);
|
||||
|
||||
// lol double-calc
|
||||
const lamp = PopnClearMedalToLamp(clearMedal);
|
||||
|
||||
// grades are kneecapped at "A" if you failed.
|
||||
if (score >= 90_000 && lamp === "FAILED") {
|
||||
return "A";
|
||||
}
|
||||
|
||||
return gradeString;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { GetGrade } from "./_common";
|
||||
import { WACCA_GBOUNDARIES } from "tachi-common";
|
||||
import type { GPTServerImplementation } from "game-implementations/types";
|
||||
|
||||
export const WACCA_IMPL: GPTServerImplementation<"wacca:Single"> = {
|
||||
validators: {},
|
||||
derivers: {
|
||||
grade: ({ score }) => GetGrade(WACCA_GBOUNDARIES, score),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { ChartDocument, ConfDerivedMetrics, GPTString } from "tachi-common";
|
||||
import type { AllConfMetrics, ScoreMetricDeriver } from "tachi-common/types/metrics";
|
||||
|
||||
/**
|
||||
* Validate this chart-specific metric. This should return a string representing an
|
||||
* error message on failure, and null on success.
|
||||
*/
|
||||
export type ChartSpecificMetricValidator<GPT extends GPTString> = (
|
||||
metric: number,
|
||||
chart: ChartDocument<GPT>
|
||||
) => string | true;
|
||||
|
||||
interface ChartDependentMax {
|
||||
chartDependentMax: true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The only metrics that need validators are those that have `chartDependentMax` set.
|
||||
* Otherwise, a validator is built into the ConfScoreMetric.
|
||||
*/
|
||||
export type GPTMetricValidators<GPT extends GPTString> = {
|
||||
[M in keyof AllConfMetrics[GPT] as AllConfMetrics[GPT][M] extends ChartDependentMax
|
||||
? M
|
||||
: never]: ChartSpecificMetricValidator<GPT>;
|
||||
};
|
||||
|
||||
export type GPTDerivers<GPT extends GPTString> = {
|
||||
// @ts-expect-error This *might* be a bug in the typescript compiler
|
||||
// as this works for all GPT inputs normally.
|
||||
// Possibly some generic nonsense but like...
|
||||
|
||||
// can you really blame them for this not working?
|
||||
// can you? LOOK at what we're doing.
|
||||
[K in keyof ConfDerivedMetrics[GPT]]: ScoreMetricDeriver<ConfDerivedMetrics[GPT][K], GPT>;
|
||||
};
|
||||
|
||||
export interface GPTServerImplementation<GPT extends GPTString> {
|
||||
validators: GPTMetricValidators<GPT>;
|
||||
derivers: GPTDerivers<GPT>;
|
||||
}
|
||||
|
||||
export type GPTImplementations = {
|
||||
[GPT in GPTString]: GPTServerImplementation<GPT>;
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { SetRivalsFailReasons } from "lib/constants/err-codes";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { SendSetRivalNotification } from "lib/notifications/notification-wrappers";
|
||||
import { ServerConfig } from "lib/setup/config";
|
||||
import { FormatGame } from "tachi-common";
|
||||
import { FormatGame, GetGamePTConfig } from "tachi-common";
|
||||
import { ArrayDiff } from "utils/misc";
|
||||
import { GetUsersWithIDs, GetUserWithIDGuaranteed } from "utils/user";
|
||||
import type { BulkWriteUpdateOneOperation } from "mongodb";
|
||||
@@ -233,12 +233,14 @@ export async function GetChallengerUsers(userID: integer, game: Game, playtype:
|
||||
* this sucks though.
|
||||
*/
|
||||
export async function UpdatePlayersRivalRankings(userID: integer, game: Game, playtype: Playtype) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const rivalIDs = await GetRivalIDs(userID, game, playtype);
|
||||
|
||||
// get all of this user's chartIDs so we know what to update
|
||||
const userPBs = (await db["personal-bests"].find(
|
||||
{ userID, game, playtype },
|
||||
{ projection: { chartID: 1, "scoreData.percent": 1 } }
|
||||
{ projection: { chartID: 1, [`scoreData.${gptConfig.defaultMetric}`]: 1 } }
|
||||
)) as Array<{ chartID: string; scoreData: { percent: number } }>;
|
||||
|
||||
const bwrite: Array<BulkWriteUpdateOneOperation<PBScoreDocument>> = [];
|
||||
@@ -249,7 +251,7 @@ export async function UpdatePlayersRivalRankings(userID: integer, game: Game, pl
|
||||
(await db["personal-bests"].count({
|
||||
chartID: pb.chartID,
|
||||
userID: { $in: rivalIDs },
|
||||
"scoreData.percent": { $gt: pb.scoreData.percent },
|
||||
[`scoreData.${gptConfig.defaultMetric}`]: { $gt: pb.scoreData.percent },
|
||||
})) + 1;
|
||||
|
||||
bwrite.push({
|
||||
|
||||
@@ -12,18 +12,18 @@ import {
|
||||
WACCARate,
|
||||
} from "rg-stats";
|
||||
import { GetGPTString } from "tachi-common";
|
||||
import type { DryScore } from "../common/types";
|
||||
import type { DryScoreData } from "../common/types";
|
||||
import type { ScoreCalculator, GPTScoreCalculators } from "./types";
|
||||
import type { ChartDocument, GPTString, GPTStrings } from "tachi-common";
|
||||
import type { ChartDocument, GPTString, GPTStrings, Game } from "tachi-common";
|
||||
|
||||
const SkillCalc: ScoreCalculator<"gitadora:Dora" | "gitadora:Gita"> = (dryScore, chart) =>
|
||||
GITADORASkill.calculate(dryScore.scoreData.percent, chart.levelNum);
|
||||
const SkillCalc: ScoreCalculator<"gitadora:Dora" | "gitadora:Gita"> = (scoreData, chart) =>
|
||||
GITADORASkill.calculate(scoreData.percent, chart.levelNum);
|
||||
|
||||
const SGLCalc: ScoreCalculator<GPTStrings["bms" | "pms"]> = (dryScore, chart) => {
|
||||
const SGLCalc: ScoreCalculator<GPTStrings["bms" | "pms"]> = (scoreData, chart) => {
|
||||
const ecValue = chart.data.sglEC ?? 0;
|
||||
const hcValue = chart.data.sglHC ?? 0;
|
||||
|
||||
switch (dryScore.scoreData.lamp) {
|
||||
switch (scoreData.lamp) {
|
||||
case "FULL COMBO":
|
||||
case "EX HARD CLEAR":
|
||||
case "HARD CLEAR":
|
||||
@@ -36,16 +36,16 @@ const SGLCalc: ScoreCalculator<GPTStrings["bms" | "pms"]> = (dryScore, chart) =>
|
||||
}
|
||||
};
|
||||
|
||||
const VF6Calc: ScoreCalculator<GPTStrings["sdvx" | "usc"]> = (dryScore, chart) =>
|
||||
Volforce.calculateVF6(dryScore.scoreData.score, dryScore.scoreData.lamp, chart.levelNum);
|
||||
const VF6Calc: ScoreCalculator<GPTStrings["sdvx" | "usc"]> = (scoreData, chart) =>
|
||||
Volforce.calculateVF6(scoreData.score, scoreData.lamp, chart.levelNum);
|
||||
|
||||
const BPICalc: ScoreCalculator<GPTStrings["iidx"]> = (dryScore, chart) => {
|
||||
const BPICalc: ScoreCalculator<GPTStrings["iidx"]> = (scoreData, chart) => {
|
||||
if (chart.data.kaidenAverage === null || chart.data.worldRecord === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return PoyashiBPI.calculate(
|
||||
dryScore.scoreData.score,
|
||||
scoreData.score,
|
||||
chart.data.kaidenAverage,
|
||||
chart.data.worldRecord,
|
||||
chart.data.notecount * 2,
|
||||
@@ -62,7 +62,7 @@ const BPICalc: ScoreCalculator<GPTStrings["iidx"]> = (dryScore, chart) => {
|
||||
export const SCORE_CALCULATORS: GPTScoreCalculators = {
|
||||
"iidx:SP": {
|
||||
BPI: BPICalc,
|
||||
ktLampRating: (dryScore, chart) => {
|
||||
ktLampRating: (scoreData, chart) => {
|
||||
// if chart has no ncValue, use the rating of the chart instead.
|
||||
const ncValue = chart.data.ncTier?.value ?? chart.levelNum;
|
||||
|
||||
@@ -72,7 +72,7 @@ export const SCORE_CALCULATORS: GPTScoreCalculators = {
|
||||
const hcValue = Math.max(chart.data.hcTier?.value ?? 0, ncValue);
|
||||
const exhcValue = Math.max(chart.data.exhcTier?.value ?? 0, hcValue);
|
||||
|
||||
switch (dryScore.scoreData.lamp) {
|
||||
switch (scoreData.lamp) {
|
||||
case "FULL COMBO":
|
||||
case "EX HARD CLEAR":
|
||||
return exhcValue;
|
||||
@@ -87,11 +87,11 @@ export const SCORE_CALCULATORS: GPTScoreCalculators = {
|
||||
},
|
||||
"iidx:DP": {
|
||||
BPI: BPICalc,
|
||||
ktLampRating: (dryScore, chart) => {
|
||||
ktLampRating: (scoreData, chart) => {
|
||||
// if chart has no tier, use the rating of the chart instead.
|
||||
const ecValue = chart.data.dpTier?.value ?? chart.levelNum;
|
||||
|
||||
switch (dryScore.scoreData.lamp) {
|
||||
switch (scoreData.lamp) {
|
||||
case "FULL COMBO":
|
||||
case "EX HARD CLEAR":
|
||||
case "HARD CLEAR":
|
||||
@@ -117,38 +117,31 @@ export const SCORE_CALCULATORS: GPTScoreCalculators = {
|
||||
"usc:Keyboard": { VF6: VF6Calc },
|
||||
|
||||
"maimaidx:Single": {
|
||||
rate: (dryScore, chart) =>
|
||||
MaimaiDXRate.calculate(dryScore.scoreData.percent, chart.levelNum),
|
||||
rate: (scoreData, chart) => MaimaiDXRate.calculate(scoreData.percent, chart.levelNum),
|
||||
},
|
||||
"museca:Single": {
|
||||
curatorSkill: (dryScore, chart) =>
|
||||
CuratorSkill.calculate(dryScore.scoreData.score, chart.levelNum),
|
||||
curatorSkill: (scoreData, chart) => CuratorSkill.calculate(scoreData.score, chart.levelNum),
|
||||
},
|
||||
"chunithm:Single": {
|
||||
rating: (dryScore, chart) =>
|
||||
CHUNITHMRating.calculate(dryScore.scoreData.score, chart.levelNum),
|
||||
rating: (scoreData, chart) => CHUNITHMRating.calculate(scoreData.score, chart.levelNum),
|
||||
},
|
||||
"jubeat:Single": {
|
||||
jubility: (dryScore, chart) =>
|
||||
Jubility.calculate(
|
||||
dryScore.scoreData.score,
|
||||
dryScore.scoreData.musicRate,
|
||||
chart.levelNum
|
||||
),
|
||||
jubility: (scoreData, chart) =>
|
||||
Jubility.calculate(scoreData.score, scoreData.musicRate, chart.levelNum),
|
||||
},
|
||||
|
||||
"itg:Stamina": {
|
||||
blockRating: (dryScore, chart) => {
|
||||
if (dryScore.scoreData.lamp === "FAILED") {
|
||||
blockRating: (scoreData, chart) => {
|
||||
if (scoreData.lamp === "FAILED") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return chart.levelNum;
|
||||
},
|
||||
fastest32: (dryScore, chart) => {
|
||||
fastest32: (scoreData, chart) => {
|
||||
const diedAtMeasure =
|
||||
dryScore.scoreData.lamp === "FAILED"
|
||||
? (dryScore.scoreData.survivedPercent / 100) * chart.data.notesPerMeasure.length
|
||||
scoreData.lamp === "FAILED"
|
||||
? (scoreData.survivedPercent / 100) * chart.data.notesPerMeasure.length
|
||||
: null;
|
||||
|
||||
const fastest32 = ITGHighestUnbroken.calculateFromNPSPerMeasure(
|
||||
@@ -176,17 +169,17 @@ export const SCORE_CALCULATORS: GPTScoreCalculators = {
|
||||
},
|
||||
|
||||
"popn:9B": {
|
||||
classPoints: (dryScore, chart) =>
|
||||
classPoints: (scoreData, chart) =>
|
||||
PopnClassPoints.calculate(
|
||||
dryScore.scoreData.score,
|
||||
PopnClearMedalToLamp(dryScore.scoreData.clearMedal),
|
||||
scoreData.score,
|
||||
PopnClearMedalToLamp(scoreData.clearMedal),
|
||||
chart.levelNum
|
||||
),
|
||||
},
|
||||
"sdvx:Single": { VF6: VF6Calc },
|
||||
|
||||
"wacca:Single": {
|
||||
rate: (dryScore, chart) => WACCARate.calculate(dryScore.scoreData.score, chart.levelNum),
|
||||
rate: (scoreData, chart) => WACCARate.calculate(scoreData.score, chart.levelNum),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -195,15 +188,16 @@ export const SCORE_CALCULATORS: GPTScoreCalculators = {
|
||||
* @param scores - All of the scores in this session.
|
||||
*/
|
||||
export function CreateScoreCalcData<GPT extends GPTString>(
|
||||
dryScore: DryScore<GPT>,
|
||||
game: Game,
|
||||
dryScoreData: DryScoreData<GPT>,
|
||||
chart: ChartDocument<GPT>
|
||||
) {
|
||||
const gptString = GetGPTString(dryScore.game, chart.playtype);
|
||||
const gptString = GetGPTString(game, chart.playtype);
|
||||
|
||||
const calcData: Record<string, number | null> = {};
|
||||
|
||||
for (const [key, fn] of Object.entries(SCORE_CALCULATORS[gptString])) {
|
||||
calcData[key] = fn(dryScore, chart);
|
||||
calcData[key] = fn(dryScoreData, chart);
|
||||
}
|
||||
|
||||
return calcData;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DryScore } from "../common/types";
|
||||
import type { DryScoreData } from "../common/types";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
import type {
|
||||
ChartDocument,
|
||||
@@ -16,7 +16,7 @@ import type {
|
||||
import type { DerivedClassConfig, ProvidedClassConfig } from "tachi-common/types/game-config-utils";
|
||||
|
||||
export type ScoreCalculator<GPT extends GPTString> = (
|
||||
dryScore: DryScore<GPT>,
|
||||
scoreData: DryScoreData<GPT>,
|
||||
chart: ChartDocument<GPT>
|
||||
) => number | null;
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { InternalFailure } from "../common/converter-failures";
|
||||
import type { GradeBoundary } from "tachi-common";
|
||||
|
||||
/**
|
||||
* Util for getting a games' grade for a given score.
|
||||
*/
|
||||
export function GetGrade<G extends string>(grades: Array<GradeBoundary<G>>, score: number): G {
|
||||
// sort grades going downwards in their boundaries.
|
||||
const descendingGrades = grades.slice(0).sort((a, b) => b.lowerBound - a.lowerBound);
|
||||
|
||||
for (const { name, lowerBound } of descendingGrades) {
|
||||
if (score >= lowerBound) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InternalFailure(`Could not resolve grade for score ${score}.`);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { GetGrade } from "../common-utils";
|
||||
import { IIDXLIKE_GBOUNDARIES } from "tachi-common";
|
||||
import type { GPTDerivers } from "../types";
|
||||
import type { GPTStrings, integer } from "tachi-common";
|
||||
|
||||
function calculateIIDXLikePercent(exScore: integer, notecount: integer) {
|
||||
return exScore / (notecount * 2);
|
||||
}
|
||||
|
||||
type IIDXLikes = GPTStrings["bms" | "iidx" | "pms"];
|
||||
|
||||
/**
|
||||
* Derivers for both IIDX SP and DP.
|
||||
*
|
||||
* and BMS. and PMS. They use the same things.
|
||||
*/
|
||||
export const IIDXLIKE_DERIVERS: GPTDerivers<IIDXLikes> = {
|
||||
percent: ({ score }, chart) => calculateIIDXLikePercent(score, chart.data.notecount),
|
||||
grade: ({ score }, chart) => {
|
||||
const percent = calculateIIDXLikePercent(score, chart.data.notecount);
|
||||
|
||||
return GetGrade(IIDXLIKE_GBOUNDARIES, percent);
|
||||
},
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { GetEnumValue } from "tachi-common/types/metrics";
|
||||
|
||||
export function PopnClearMedalToLamp(
|
||||
clearMedal: GetEnumValue<"popn:9B", "clearMedal">
|
||||
): GetEnumValue<"popn:9B", "lamp"> {
|
||||
switch (clearMedal) {
|
||||
case "perfect":
|
||||
return "PERFECT";
|
||||
case "fullComboCircle":
|
||||
case "fullComboDiamond":
|
||||
case "fullComboStar":
|
||||
return "FULL COMBO";
|
||||
case "clearCircle":
|
||||
case "clearDiamond":
|
||||
case "clearStar":
|
||||
return "CLEAR";
|
||||
case "easyClear":
|
||||
return "EASY CLEAR";
|
||||
case "failedCircle":
|
||||
case "failedDiamond":
|
||||
case "failedStar":
|
||||
return "FAILED";
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { GetGrade } from "../common-utils";
|
||||
import { SDVXLIKE_GBOUNDARIES } from "tachi-common";
|
||||
import type { GPTDerivers } from "../types";
|
||||
import type { GPTStrings } from "tachi-common";
|
||||
|
||||
type SDVXLikes = GPTStrings["sdvx" | "usc"];
|
||||
|
||||
export const SDVXLIKE_DERIVERS: GPTDerivers<SDVXLikes> = {
|
||||
grade: ({ score }) => GetGrade(SDVXLIKE_GBOUNDARIES, score),
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { ConfDerivedMetrics, GPTString } from "tachi-common";
|
||||
import type { ScoreMetricDeriver } from "tachi-common/types/metrics";
|
||||
|
||||
export type GPTDerivers<GPT extends GPTString> = {
|
||||
// @ts-expect-error This *might* be a bug in the typescript compiler
|
||||
// as this works for all GPT inputs normally.
|
||||
// Possibly some generic nonsense but like...
|
||||
|
||||
// can you really blame them for this not working?
|
||||
// can you? LOOK at what we're doing.
|
||||
[K in keyof ConfDerivedMetrics[GPT]]: ScoreMetricDeriver<ConfDerivedMetrics[GPT][K], GPT>;
|
||||
};
|
||||
@@ -1,8 +1,9 @@
|
||||
import { GPT_PB_MERGE_FNS } from "./mergers/mergers";
|
||||
import { CreateEnumIndexes } from "../derivers/derivers";
|
||||
import { GPT_PB_DEFAULT_REF_NAMES, GPT_PB_MERGE_FNS } from "./mergers/mergers";
|
||||
import { CreateScoreCalcData } from "../calculated-data/score";
|
||||
import { CreateEnumIndexes } from "../score-importing/derivers";
|
||||
import db from "external/mongo/db";
|
||||
import { GetEveryonesRivalIDs } from "lib/rivals/rivals";
|
||||
import { GetGPTConfig } from "tachi-common";
|
||||
import { GetGPTConfig, GetGamePTConfig } from "tachi-common";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
import type { BulkWriteUpdateOneOperation, FilterQuery } from "mongodb";
|
||||
import type {
|
||||
@@ -12,6 +13,7 @@ import type {
|
||||
Playtype,
|
||||
ScoreDocument,
|
||||
integer,
|
||||
ChartDocument,
|
||||
} from "tachi-common";
|
||||
|
||||
export type PBScoreDocumentNoRank<GPT extends GPTString = GPTString> = Omit<
|
||||
@@ -26,10 +28,12 @@ export type PBScoreDocumentNoRank<GPT extends GPTString = GPTString> = Omit<
|
||||
export async function CreatePBDoc(
|
||||
gpt: GPTString,
|
||||
userID: integer,
|
||||
chartID: string,
|
||||
chart: ChartDocument,
|
||||
logger: KtLogger,
|
||||
asOfTimestamp?: number
|
||||
) {
|
||||
const chartID = chart.chartID;
|
||||
|
||||
const query: FilterQuery<ScoreDocument> = {
|
||||
userID,
|
||||
chartID,
|
||||
@@ -65,7 +69,12 @@ export async function CreatePBDoc(
|
||||
}
|
||||
|
||||
const pbDoc: PBScoreDocumentNoRank = {
|
||||
composedFrom: [INITIAL_REFERENCE],
|
||||
composedFrom: [
|
||||
{
|
||||
name: GPT_PB_DEFAULT_REF_NAMES[gpt],
|
||||
scoreID: defaultMetricPB.scoreID,
|
||||
},
|
||||
],
|
||||
chartID: defaultMetricPB.chartID,
|
||||
userID,
|
||||
songID: defaultMetricPB.songID,
|
||||
@@ -97,12 +106,15 @@ export async function CreatePBDoc(
|
||||
}
|
||||
}
|
||||
|
||||
// update any enum indexes that
|
||||
// update any enum indexes that might've been altered
|
||||
const { indexes, optionalIndexes } = CreateEnumIndexes(gpt, pbDoc.scoreData, logger);
|
||||
|
||||
pbDoc.scoreData.enumIndexes = indexes;
|
||||
pbDoc.scoreData.optional.enumIndexes = optionalIndexes;
|
||||
|
||||
// Recalc info about this score (incase things have changed).
|
||||
pbDoc.calculatedData = CreateScoreCalcData(pbDoc.game, pbDoc.scoreData, chart);
|
||||
|
||||
// finally, return our full pbDoc, that does NOT have the ranking props.
|
||||
// (We will add those later)
|
||||
return pbDoc;
|
||||
@@ -112,11 +124,13 @@ export async function CreatePBDoc(
|
||||
* Updates rankings on a given chart.
|
||||
*/
|
||||
export async function UpdateChartRanking(game: Game, playtype: Playtype, chartID: string) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const scores = await db["personal-bests"].find(
|
||||
{ chartID },
|
||||
{
|
||||
sort: {
|
||||
"scoreData.percent": -1,
|
||||
[`scoreData.${gptConfig.defaultMetric}`]: -1,
|
||||
timeAchieved: 1,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
/* eslint-disable require-atomic-updates */
|
||||
import { InternalFailure } from "../common/converter-failures";
|
||||
import db from "external/mongo/db";
|
||||
import { Volforce } from "rg-stats";
|
||||
import { DeleteUndefinedProps } from "utils/misc";
|
||||
import { FindChartWithChartID } from "utils/queries/charts";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
import type { FilterQuery } from "mongodb";
|
||||
import type { PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
|
||||
export async function IIDXMergeFn(
|
||||
pbDoc: PBScoreDocument<"iidx:DP" | "iidx:SP">,
|
||||
scorePB: ScoreDocument<"iidx:DP" | "iidx:SP">,
|
||||
lampPB: ScoreDocument<"iidx:DP" | "iidx:SP">,
|
||||
logger: KtLogger,
|
||||
asOfTimestamp?: number
|
||||
): Promise<boolean> {
|
||||
// lampRating needs to be updated.
|
||||
pbDoc.calculatedData.ktLampRating = lampPB.calculatedData.ktLampRating;
|
||||
|
||||
// Update lamp related iidx-specific info from the lampPB.
|
||||
pbDoc.scoreData.optional.gsmEasy = lampPB.scoreData.optional.gsmEasy;
|
||||
pbDoc.scoreData.optional.gsmNormal = lampPB.scoreData.optional.gsmNormal;
|
||||
pbDoc.scoreData.optional.gsmHard = lampPB.scoreData.optional.gsmHard;
|
||||
pbDoc.scoreData.optional.gsmEXHard = lampPB.scoreData.optional.gsmEXHard;
|
||||
|
||||
pbDoc.scoreData.optional.gauge = lampPB.scoreData.optional.gauge;
|
||||
pbDoc.scoreData.optional.gaugeHistory = lampPB.scoreData.optional.gaugeHistory;
|
||||
|
||||
pbDoc.scoreData.optional.comboBreak = lampPB.scoreData.optional.comboBreak;
|
||||
|
||||
DeleteUndefinedProps(pbDoc.scoreData.optional);
|
||||
|
||||
await MergeBPPB(pbDoc, scorePB, lampPB, logger, asOfTimestamp);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function PopnMergeFn(
|
||||
pbDoc: PBScoreDocument<"popn:9B">,
|
||||
scorePB: ScoreDocument<"popn:9B">,
|
||||
lampPB: ScoreDocument<"popn:9B">,
|
||||
_logger: KtLogger
|
||||
) {
|
||||
pbDoc.scoreData.optional.specificClearType = lampPB.scoreData.optional.specificClearType;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function BMSMergeFn(
|
||||
pbDoc: PBScoreDocument<"bms:7K" | "bms:14K">,
|
||||
scorePB: ScoreDocument<"bms:7K" | "bms:14K">,
|
||||
lampPB: ScoreDocument<"bms:7K" | "bms:14K">,
|
||||
logger: KtLogger,
|
||||
asOfTimestamp?: number
|
||||
) {
|
||||
pbDoc.calculatedData.sieglinde = lampPB.calculatedData.sieglinde;
|
||||
|
||||
pbDoc.scoreData.optional.gaugeHistory = lampPB.scoreData.optional.gaugeHistory;
|
||||
pbDoc.scoreData.optional.gauge = lampPB.scoreData.optional.gauge;
|
||||
|
||||
await MergeBPPB(pbDoc, scorePB, lampPB, logger, asOfTimestamp);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function PMSMergeFn(
|
||||
pbDoc: PBScoreDocument<"pms:Controller" | "pms:Keyboard">,
|
||||
scorePB: ScoreDocument<"pms:Controller" | "pms:Keyboard">,
|
||||
lampPB: ScoreDocument<"pms:Controller" | "pms:Keyboard">,
|
||||
logger: KtLogger,
|
||||
asOfTimestamp?: number
|
||||
) {
|
||||
pbDoc.calculatedData.sieglinde = lampPB.calculatedData.sieglinde;
|
||||
|
||||
await MergeBPPB(pbDoc, scorePB, lampPB, logger, asOfTimestamp);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function recalculates and applies VF6 to the PB document.
|
||||
*
|
||||
* This is near-identical to the SDVXMergeFn. See that.
|
||||
*/
|
||||
export async function USCMergeFn(
|
||||
pbDoc: PBScoreDocument<"usc:Controller" | "usc:Keyboard">,
|
||||
scorePB: ScoreDocument<"usc:Controller" | "usc:Keyboard">,
|
||||
lampPB: ScoreDocument<"usc:Controller" | "usc:Keyboard">,
|
||||
logger: KtLogger
|
||||
) {
|
||||
// @optimisable - see SDVXMergeFn
|
||||
const chart = await FindChartWithChartID("usc", pbDoc.chartID);
|
||||
|
||||
if (!chart) {
|
||||
logger.severe(`Chart ${pbDoc.chartID} disappeared underfoot?`);
|
||||
throw new InternalFailure(`Chart ${pbDoc.chartID} disappeared underfoot?`);
|
||||
}
|
||||
|
||||
pbDoc.calculatedData.VF6 = Volforce.calculateVF6(
|
||||
pbDoc.scoreData.score,
|
||||
pbDoc.scoreData.lamp,
|
||||
chart.levelNum
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function recalculates and applies VF6 to the PB document.
|
||||
*
|
||||
* SDVX cannot just select the larger volforce - instead, volforce has to be
|
||||
* re-calculated for any different permutation of scorePB + lampPB.
|
||||
*/
|
||||
export async function SDVXMergeFn(
|
||||
pbDoc: PBScoreDocument<"sdvx:Single">,
|
||||
scorePB: ScoreDocument<"sdvx:Single">,
|
||||
lampPB: ScoreDocument<"sdvx:Single">,
|
||||
logger: KtLogger,
|
||||
asOfTimestamp?: number
|
||||
): Promise<boolean> {
|
||||
// @optimisable
|
||||
// This is a re-fetch, but it's difficult to pass the chart all
|
||||
// the way down here due to how chartIDs (set) works. :(
|
||||
const chart = await FindChartWithChartID("sdvx", pbDoc.chartID);
|
||||
|
||||
if (!chart) {
|
||||
logger.severe(`Chart ${pbDoc.chartID} disappeared underfoot?`);
|
||||
throw new InternalFailure(`Chart ${pbDoc.chartID} disappeared underfoot?`);
|
||||
}
|
||||
|
||||
pbDoc.calculatedData.VF6 = Volforce.calculateVF6(
|
||||
pbDoc.scoreData.score,
|
||||
pbDoc.scoreData.lamp,
|
||||
chart.levelNum
|
||||
);
|
||||
|
||||
const query: FilterQuery<ScoreDocument> = {
|
||||
chartID: pbDoc.chartID,
|
||||
"scoreData.optional.exScore": { $type: "number" },
|
||||
};
|
||||
|
||||
if (asOfTimestamp !== undefined) {
|
||||
query.timeAchieved = { $lt: asOfTimestamp };
|
||||
}
|
||||
|
||||
// find the users score with the highest exScore
|
||||
const bestExScore = (await db.scores.findOne(query, {
|
||||
sort: {
|
||||
"scoreData.optional.exScore": -1,
|
||||
},
|
||||
})) as ScoreDocument<"sdvx:Single"> | null;
|
||||
|
||||
if (!bestExScore) {
|
||||
pbDoc.scoreData.optional.exScore = undefined;
|
||||
} else {
|
||||
pbDoc.scoreData.optional.exScore = bestExScore.scoreData.optional.exScore;
|
||||
|
||||
pbDoc.composedFrom.other = [{ name: "exScorePB", scoreID: bestExScore.scoreID }];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
type GPTStringWithBP =
|
||||
| "bms:7K"
|
||||
| "bms:14K"
|
||||
| "iidx:DP"
|
||||
| "iidx:SP"
|
||||
| "pms:Controller"
|
||||
| "pms:Keyboard";
|
||||
|
||||
/**
|
||||
* Given typical PB-Merge information, fetch the best `bp` for this user's scores
|
||||
* on this chart and merge it with the `pbDoc` if it's large enough.
|
||||
*
|
||||
* @returns NOTHING, mutates original input.
|
||||
*/
|
||||
async function MergeBPPB(
|
||||
pbDoc: PBScoreDocument<GPTStringWithBP>,
|
||||
scorePB: ScoreDocument<GPTStringWithBP>,
|
||||
lampPB: ScoreDocument<GPTStringWithBP>,
|
||||
logger: KtLogger,
|
||||
asOfTimestamp: number | undefined
|
||||
) {
|
||||
// bad+poor PB document. This is a weird, third indepdenent metric that IIDX players sometimes care about.
|
||||
const query: FilterQuery<ScoreDocument> = {
|
||||
userID: scorePB.userID,
|
||||
chartID: scorePB.chartID,
|
||||
"scoreData.optional.bp": { $exists: true },
|
||||
};
|
||||
|
||||
if (asOfTimestamp !== undefined) {
|
||||
query.timeAchieved = { $lt: asOfTimestamp };
|
||||
}
|
||||
|
||||
const bpPB = (await db.scores.findOne(query, {
|
||||
sort: {
|
||||
// bp 0 is the best BP, bp 1 is worse, so on
|
||||
"scoreData.optional.bp": 1,
|
||||
},
|
||||
})) as ScoreDocument<"iidx:DP" | "iidx:SP"> | null;
|
||||
|
||||
if (!bpPB) {
|
||||
logger.verbose(
|
||||
`Could not find BP PB for ${scorePB.userID} ${scorePB.chartID} in PB joining. User likely has no scores with BP defined.`,
|
||||
{ pbDoc }
|
||||
);
|
||||
|
||||
// this isn't actually an error! we just don't have to do anything.
|
||||
return;
|
||||
}
|
||||
|
||||
// by default scorePB is chosen for optional fields, so, we can skip any assignments here by returning here.
|
||||
if (bpPB.scoreID === scorePB.scoreID) {
|
||||
logger.debug(`Skipped merging BP PB as scorePB was also BP PB.`);
|
||||
return true;
|
||||
} else if (bpPB.scoreID === lampPB.scoreID) {
|
||||
pbDoc.scoreData.optional.bp = lampPB.scoreData.optional.bp;
|
||||
logger.debug(`Skipped adding BP PB as composedFrom because lampPB was also BP PB.`);
|
||||
return;
|
||||
}
|
||||
|
||||
pbDoc.scoreData.optional.bp = bpPB.scoreData.optional.bp;
|
||||
|
||||
pbDoc.composedFrom.other = [{ name: "Best BP", scoreID: bpPB.scoreID }];
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BMS_PMS_MERGERS } from "./games/bms";
|
||||
import { IIDX_MERGERS } from "./games/iidx";
|
||||
import { CreatePBMergeFor } from "./utils";
|
||||
import type { PBMergeFunction } from "./types";
|
||||
import type { GPTString } from "tachi-common";
|
||||
|
||||
@@ -16,5 +17,113 @@ export const GPT_PB_MERGE_FNS: GPTPBMergeFNs = {
|
||||
"pms:Controller": BMS_PMS_MERGERS,
|
||||
"pms:Keyboard": BMS_PMS_MERGERS,
|
||||
|
||||
"chunithm:Single": [],
|
||||
"chunithm:Single": [
|
||||
CreatePBMergeFor("enumIndexes.lamp", "Best Lamp", (base, score) => {
|
||||
base.scoreData.lamp = score.scoreData.lamp;
|
||||
}),
|
||||
],
|
||||
"sdvx:Single": [
|
||||
CreatePBMergeFor("enumIndexes.lamp", "Best Lamp", (base, score) => {
|
||||
base.scoreData.lamp = score.scoreData.lamp;
|
||||
}),
|
||||
CreatePBMergeFor("optional.exScore", "Best EX Score", (base, score) => {
|
||||
base.scoreData.optional.exScore = score.scoreData.optional.exScore;
|
||||
}),
|
||||
],
|
||||
|
||||
// musicRate is the default prop
|
||||
// but we want the user's best score to count aswell.
|
||||
"jubeat:Single": [
|
||||
CreatePBMergeFor("enumIndexes.lamp", "Best Lamp", (base, score) => {
|
||||
base.scoreData.lamp = score.scoreData.lamp;
|
||||
}),
|
||||
CreatePBMergeFor("score", "Best Score", (base, score) => {
|
||||
base.scoreData.score = score.scoreData.score;
|
||||
}),
|
||||
],
|
||||
|
||||
"maimaidx:Single": [
|
||||
CreatePBMergeFor("enumIndexes.lamp", "Best Lamp", (base, score) => {
|
||||
base.scoreData.lamp = score.scoreData.lamp;
|
||||
}),
|
||||
],
|
||||
|
||||
"museca:Single": [
|
||||
CreatePBMergeFor("enumIndexes.lamp", "Best Lamp", (base, score) => {
|
||||
base.scoreData.lamp = score.scoreData.lamp;
|
||||
}),
|
||||
],
|
||||
|
||||
"popn:9B": [
|
||||
CreatePBMergeFor("enumIndexes.clearMedal", "Best Clear", (base, score) => {
|
||||
base.scoreData.clearMedal = score.scoreData.clearMedal;
|
||||
// these are directly related. pluck both.
|
||||
base.scoreData.lamp = score.scoreData.lamp;
|
||||
}),
|
||||
],
|
||||
|
||||
"wacca:Single": [
|
||||
CreatePBMergeFor("enumIndexes.lamp", "Best Lamp", (base, score) => {
|
||||
base.scoreData.lamp = score.scoreData.lamp;
|
||||
}),
|
||||
],
|
||||
|
||||
"gitadora:Dora": [
|
||||
CreatePBMergeFor("enumIndexes.lamp", "Best Lamp", (base, score) => {
|
||||
base.scoreData.lamp = score.scoreData.lamp;
|
||||
}),
|
||||
],
|
||||
"gitadora:Gita": [
|
||||
CreatePBMergeFor("enumIndexes.lamp", "Best Lamp", (base, score) => {
|
||||
base.scoreData.lamp = score.scoreData.lamp;
|
||||
}),
|
||||
],
|
||||
|
||||
"itg:Stamina": [
|
||||
// we'll pluck the best lamp, but this game has a pretty interesting concept
|
||||
// for merging PBs. This is probably fine.
|
||||
CreatePBMergeFor("enumIndexes.lamp", "Best Lamp", (base, score) => {
|
||||
base.scoreData.lamp = score.scoreData.lamp;
|
||||
}),
|
||||
],
|
||||
|
||||
"usc:Controller": [
|
||||
CreatePBMergeFor("enumIndexes.lamp", "Best Lamp", (base, score) => {
|
||||
base.scoreData.lamp = score.scoreData.lamp;
|
||||
}),
|
||||
],
|
||||
"usc:Keyboard": [
|
||||
CreatePBMergeFor("enumIndexes.lamp", "Best Lamp", (base, score) => {
|
||||
base.scoreData.lamp = score.scoreData.lamp;
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* What should the name be for the first reference on a PB?
|
||||
*
|
||||
* A PB is initialised from the user's best score on the defaultMetric, so in essence
|
||||
* this is a name for that default metric.
|
||||
*/
|
||||
export const GPT_PB_DEFAULT_REF_NAMES: Record<GPTString, string> = {
|
||||
"bms:14K": "Best Score",
|
||||
"bms:7K": "Best Score",
|
||||
"chunithm:Single": "Best Score",
|
||||
"gitadora:Dora": "Best Percent",
|
||||
"gitadora:Gita": "Best Percent",
|
||||
"iidx:DP": "Best Score",
|
||||
"iidx:SP": "Best Score",
|
||||
"jubeat:Single": "Best Music Rate",
|
||||
"maimaidx:Single": "Best Percent",
|
||||
"museca:Single": "Best Score",
|
||||
"pms:Controller": "Best Score",
|
||||
"pms:Keyboard": "Best Score",
|
||||
"popn:9B": "Best Score",
|
||||
"sdvx:Single": "Best Score",
|
||||
"usc:Controller": "Best Score",
|
||||
"usc:Keyboard": "Best Score",
|
||||
"wacca:Single": "Best Score",
|
||||
|
||||
// this name sucks, what should we do instead? TODO.
|
||||
"itg:Stamina": "Best Result",
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { GPTString, PBReference, integer } from "tachi-common";
|
||||
* They are expected to mutate the existingPB to add/change whatever
|
||||
* properties they feel like should be merged.
|
||||
*
|
||||
* @note Don't worry about updating enumIndexes. Those are updated for you.
|
||||
*
|
||||
* They should then return some information (a name and a scoreID) to indicate
|
||||
* what this PB is composed of.
|
||||
*/
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import db from "external/mongo/db";
|
||||
import type { PBScoreDocumentNoRank } from "../create-pb-doc";
|
||||
import type { PBMergeFunction } from "./types";
|
||||
import type { FilterQuery } from "mongodb";
|
||||
import type {
|
||||
GPTString,
|
||||
ConfDerivedMetrics,
|
||||
ConfProvidedMetrics,
|
||||
PBScoreDocument,
|
||||
ScoreDocument,
|
||||
ConfOptionalMetrics,
|
||||
ConfProvidedMetrics,
|
||||
GPTString,
|
||||
ScoreDocument,
|
||||
} from "tachi-common";
|
||||
import type { ExtractEnumMetricNames } from "tachi-common/types/metrics";
|
||||
|
||||
@@ -48,12 +48,12 @@ export function HandleAsOf(
|
||||
* Utility for making a PB merge function. In short, get the best score this user has
|
||||
* on this chart for the stated metric, then run the applicator if a score was found.
|
||||
*
|
||||
* @note Don't worry about updating enumIndexes. Those are updated for you.
|
||||
* @note Don't worry about updating enumIndexes. Those are updated for you,.
|
||||
*/
|
||||
export function CreatePBMergeFor<GPT extends GPTString>(
|
||||
metric: MetricKeys<GPT>,
|
||||
name: string,
|
||||
applicator: (base: PBScoreDocument<GPT>, score: ScoreDocument<GPT>) => void
|
||||
applicator: (base: PBScoreDocumentNoRank<GPT>, score: ScoreDocument<GPT>) => void
|
||||
): PBMergeFunction<GPT> {
|
||||
return async (userID, chartID, asOfTimestamp, base) => {
|
||||
const bestScoreFor = (await db.scores.findOne(
|
||||
|
||||
+5
-106
@@ -1,27 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
// ^ ts eslint currently gets very confused about the complexity on show here
|
||||
// sorry!
|
||||
|
||||
import { GetGrade } from "./common-utils";
|
||||
import { IIDXLIKE_DERIVERS } from "./games/iidx-like";
|
||||
import { PopnClearMedalToLamp } from "./games/popn";
|
||||
import { SDVXLIKE_DERIVERS } from "./games/sdvx-like";
|
||||
import { InternalFailure } from "../common/converter-failures";
|
||||
import {
|
||||
CHUNITHM_GBOUNDARIES,
|
||||
GITADORA_GBOUNDARIES,
|
||||
GetGPTConfig,
|
||||
ITG_GBOUNDARIES,
|
||||
JUBEAT_GBOUNDARIES,
|
||||
MAIMAIDX_GBOUNDARIES,
|
||||
MUSECA_GBOUNDARIES,
|
||||
POPN_GBOUNDARIES,
|
||||
WACCA_GBOUNDARIES,
|
||||
} from "tachi-common";
|
||||
import { GPT_SERVER_IMPLEMENTATIONS } from "game-implementations/game-implementations";
|
||||
import { GetGPTConfig } from "tachi-common";
|
||||
import type { DryScore, DryScoreData } from "../common/types";
|
||||
import type { GPTDerivers } from "./types";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
@@ -29,93 +8,13 @@ import type {
|
||||
ChartDocument,
|
||||
DerivedMetrics,
|
||||
GPTString,
|
||||
ScoreData,
|
||||
integer,
|
||||
OptionalEnumIndexes,
|
||||
ScoreData,
|
||||
ScoreEnumIndexes,
|
||||
integer,
|
||||
} from "tachi-common";
|
||||
import type { MetricValue } from "tachi-common/types/metrics";
|
||||
|
||||
type AllGPTDerivers = {
|
||||
[GPT in GPTString]: GPTDerivers<GPT>;
|
||||
};
|
||||
|
||||
/**
|
||||
* How do we derive the "derivedMetrics" for each game?
|
||||
*/
|
||||
const GPT_DERIVERS: AllGPTDerivers = {
|
||||
// these games quite literally *all* work the same way.
|
||||
"bms:14K": IIDXLIKE_DERIVERS,
|
||||
"bms:7K": IIDXLIKE_DERIVERS,
|
||||
"pms:Controller": IIDXLIKE_DERIVERS,
|
||||
"pms:Keyboard": IIDXLIKE_DERIVERS,
|
||||
"iidx:SP": IIDXLIKE_DERIVERS,
|
||||
"iidx:DP": IIDXLIKE_DERIVERS,
|
||||
|
||||
// same stuff.
|
||||
"sdvx:Single": SDVXLIKE_DERIVERS,
|
||||
"usc:Controller": SDVXLIKE_DERIVERS,
|
||||
"usc:Keyboard": SDVXLIKE_DERIVERS,
|
||||
|
||||
"chunithm:Single": {
|
||||
grade: ({ score }) => GetGrade(CHUNITHM_GBOUNDARIES, score),
|
||||
},
|
||||
"wacca:Single": {
|
||||
grade: ({ score }) => GetGrade(WACCA_GBOUNDARIES, score),
|
||||
},
|
||||
"jubeat:Single": {
|
||||
grade: ({ score }) => GetGrade(JUBEAT_GBOUNDARIES, score),
|
||||
},
|
||||
"gitadora:Dora": {
|
||||
grade: ({ percent }) => GetGrade(GITADORA_GBOUNDARIES, percent),
|
||||
},
|
||||
"gitadora:Gita": {
|
||||
grade: ({ percent }) => GetGrade(GITADORA_GBOUNDARIES, percent),
|
||||
},
|
||||
"itg:Stamina": {
|
||||
finalPercent: (metrics) => {
|
||||
// *important*
|
||||
// don't check if metrics.survivedPercent === 100, as due to floating
|
||||
// point inaccuracies, it's possible to have a 100% fail
|
||||
// (on extremely long charts, for example)
|
||||
if (metrics.lamp === "FAILED") {
|
||||
return metrics.survivedPercent;
|
||||
}
|
||||
|
||||
return 100 + metrics.scorePercent;
|
||||
},
|
||||
grade: ({ scorePercent, lamp }) => {
|
||||
if (lamp === "FAILED") {
|
||||
return "F";
|
||||
}
|
||||
|
||||
return GetGrade(ITG_GBOUNDARIES, scorePercent);
|
||||
},
|
||||
},
|
||||
"maimaidx:Single": {
|
||||
grade: ({ percent }) => GetGrade(MAIMAIDX_GBOUNDARIES, percent),
|
||||
},
|
||||
"museca:Single": {
|
||||
grade: ({ score }) => GetGrade(MUSECA_GBOUNDARIES, score),
|
||||
},
|
||||
"popn:9B": {
|
||||
lamp: ({ clearMedal }) => PopnClearMedalToLamp(clearMedal),
|
||||
grade: ({ score, clearMedal }) => {
|
||||
const gradeString = GetGrade(POPN_GBOUNDARIES, score);
|
||||
|
||||
// lol double-calc
|
||||
const lamp = PopnClearMedalToLamp(clearMedal);
|
||||
|
||||
// grades are kneecapped at "A" if you failed.
|
||||
if (score >= 90_000 && lamp === "FAILED") {
|
||||
return "A";
|
||||
}
|
||||
|
||||
return gradeString;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Given the providedMetrics and chart this score is on, derive the rest of the metrics
|
||||
* we want to store.
|
||||
@@ -125,7 +24,7 @@ function DeriveMetrics<GPT extends GPTString>(
|
||||
metrics: DryScoreData<GPT>,
|
||||
chart: ChartDocument<GPT>
|
||||
) {
|
||||
const deriverImplementation: GPTDerivers<GPT> = GPT_DERIVERS[gpt];
|
||||
const deriverImplementation: GPTDerivers<GPT> = GPT_SERVER_IMPLEMENTATIONS[gpt].derivers;
|
||||
|
||||
const derivedMetrics: Record<string, MetricValue> = {};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CreateScoreCalcData } from "../calculated-data/score";
|
||||
import { CreateFullScoreData } from "../derivers/derivers";
|
||||
import { CreateFullScoreData } from "./derivers";
|
||||
import { GetGPTString } from "tachi-common";
|
||||
import type { DryScore } from "../common/types";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
|
||||
@@ -79,7 +79,7 @@ function PropToMongoProp(prop: "grade" | "lamp" | "percent" | "score") {
|
||||
case "grade":
|
||||
return "scoreData.gradeIndex";
|
||||
case "percent":
|
||||
return "scoreData.percent";
|
||||
return [`scoreData.${gptConfig.defaultMetric}`];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ function FormatCriteria<GPT extends GPTString>(
|
||||
return gptConfig.grades[criteria.value];
|
||||
case "scoreData.lampIndex":
|
||||
return gptConfig.lamps[criteria.value];
|
||||
case "scoreData.percent":
|
||||
case [`scoreData.${gptConfig.defaultMetric}`]:
|
||||
return `Get ${FormatMaxDP(criteria.value)}% on`;
|
||||
case "scoreData.score":
|
||||
return `Get a score of ${criteria.value.toLocaleString("en-GB")} on`;
|
||||
@@ -250,55 +250,44 @@ export async function ValidateGoalChartsAndCriteria(
|
||||
// checking whether the key and value make sense
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
if (criteria.key === "scoreData.gradeIndex" && !gptConfig.grades[criteria.value]) {
|
||||
throw new Error(
|
||||
`Invalid value of ${criteria.value} for grade goal. No such grade exists at that index.`
|
||||
);
|
||||
} else if (criteria.key === "scoreData.lampIndex" && !gptConfig.lamps[criteria.value]) {
|
||||
throw new Error(
|
||||
`Invalid value of ${criteria.value} for lamp goal. No such lamp exists at that index.`
|
||||
);
|
||||
} else if (
|
||||
criteria.key === "scoreData.percent" &&
|
||||
(criteria.value <= 0 || criteria.value > gptConfig.percentMax)
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid value of ${criteria.value} for percent goal. Percents must be between 0 and ${gptConfig.percentMax}.`
|
||||
);
|
||||
} else if (criteria.key === "scoreData.score") {
|
||||
if (criteria.value < 0) {
|
||||
throw new Error(`Invalid score value for goal. Can't be negative.`);
|
||||
}
|
||||
const config =
|
||||
gptConfig.providedMetrics[criteria.key] ?? gptConfig.derivedMetrics[criteria.key];
|
||||
|
||||
// troublemaker games where score is relative to notecount
|
||||
if (game === "iidx" || game === "bms" || game === "pms") {
|
||||
if (charts.type !== "single") {
|
||||
if (!config) {
|
||||
throw new Error(
|
||||
`Invalid criteria.key for ${FormatGame(game, playtype)} (Got ${criteria.key}).`
|
||||
);
|
||||
}
|
||||
|
||||
switch (config.type) {
|
||||
case "DECIMAL":
|
||||
case "INTEGER": {
|
||||
if (config.chartDependentMax && charts.type !== "single") {
|
||||
throw new Error(
|
||||
`Invalid key for ${game} with multiple charts. Creating score goals on multiple charts where score is relative to notecount is a terrible idea, and has been disabled.`
|
||||
`Creating ${criteria.key} goals on multiple charts where the maximum value is relative to the chart is a terrible idea, and has been disabled.`
|
||||
);
|
||||
}
|
||||
|
||||
const relatedChart = (await db.anyCharts[game].findOne({
|
||||
playtype,
|
||||
chartID: charts.data,
|
||||
})) as ChartDocument<
|
||||
"bms:7K" | "bms:14K" | "iidx:DP" | "iidx:SP" | "pms:Controller" | "pms:Keyboard"
|
||||
>;
|
||||
// TODO VALIDATORS
|
||||
|
||||
const notecount = relatedChart.data.notecount;
|
||||
break;
|
||||
}
|
||||
|
||||
if (criteria.value > notecount * 2) {
|
||||
case "ENUM": {
|
||||
if (!config.values[criteria.value]) {
|
||||
throw new Error(
|
||||
`Invalid value of ${
|
||||
criteria.value
|
||||
} for goal. Maximum score possible on this chart is ${notecount * 2}.`
|
||||
`Invalid value of ${criteria.value} for ${criteria.key} goal. No such ${criteria.key} exists at that index.`
|
||||
);
|
||||
}
|
||||
} else if (GenericCalculatePercent(game, criteria.value) >= gptConfig.percentMax) {
|
||||
throw new Error(
|
||||
`Score of ${criteria.value} is too large for ${FormatGame(game, playtype)}.`
|
||||
);
|
||||
|
||||
// TODO VALIDATORS
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "GRAPH":
|
||||
case "NULLABLE_GRAPH":
|
||||
throw new Error(`Cannot set a goal on ${criteria.key} as it's a graph metric.`);
|
||||
}
|
||||
|
||||
if (charts.type === "single" && criteria.mode !== "single") {
|
||||
|
||||
@@ -314,7 +314,7 @@ export function HumaniseGoalProgress(
|
||||
return userPB.scoreData.lamp;
|
||||
}
|
||||
|
||||
case "scoreData.percent":
|
||||
case [`scoreData.${gptConfig.defaultMetric}`]:
|
||||
return `${userPB.scoreData.percent.toFixed(2)}%`;
|
||||
case "scoreData.score":
|
||||
return userPB.scoreData.score.toLocaleString();
|
||||
@@ -361,7 +361,7 @@ export function HumaniseGoalOutOf(game: Game, playtype: Playtype, key: GoalKeys,
|
||||
return lamp;
|
||||
}
|
||||
|
||||
case "scoreData.percent":
|
||||
case [`scoreData.${gptConfig.defaultMetric}`]:
|
||||
return `${value.toFixed(2)}%`;
|
||||
case "scoreData.score":
|
||||
return value.toLocaleString();
|
||||
|
||||
@@ -50,7 +50,7 @@ router.post(
|
||||
{
|
||||
criteria: {
|
||||
key: p.isIn(
|
||||
"scoreData.percent",
|
||||
[`scoreData.${gptConfig.defaultMetric}`],
|
||||
"scoreData.lampIndex",
|
||||
"scoreData.gradeIndex",
|
||||
"scoreData.score"
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ t.test("POST /api/v1/users/:userID/games/:game/:playtype/targets/add-goal", asyn
|
||||
|
||||
const baseInput = {
|
||||
criteria: {
|
||||
key: "scoreData.percent",
|
||||
key: [`scoreData.${gptConfig.defaultMetric}`],
|
||||
value: 80,
|
||||
mode: "single",
|
||||
},
|
||||
|
||||
+17
-7
@@ -14,6 +14,7 @@ import { GetParentQuests } from "lib/targets/quests";
|
||||
import { p } from "prudence";
|
||||
import { RequirePermissions } from "server/middleware/auth";
|
||||
import prValidate from "server/middleware/prudence-validate";
|
||||
import { GetGamePTConfig } from "tachi-common";
|
||||
import { GetGoalForIDGuaranteed } from "utils/db";
|
||||
import { AssignToReqTachiData, GetTachiData, GetUGPT } from "utils/req-tachi-data";
|
||||
import type { RequestHandler } from "express";
|
||||
@@ -88,15 +89,10 @@ router.post(
|
||||
RequirePermissions("manage_targets"),
|
||||
prValidate({
|
||||
criteria: {
|
||||
key: p.isIn(
|
||||
"scoreData.percent",
|
||||
"scoreData.lampIndex",
|
||||
"scoreData.gradeIndex",
|
||||
"scoreData.score"
|
||||
),
|
||||
|
||||
// we do proper validation on this later.
|
||||
key: "string",
|
||||
value: p.gte(0),
|
||||
|
||||
mode: p.isIn("single", "absolute", "proportion"),
|
||||
countNum: (self, parent) => {
|
||||
if (parent.mode === "single") {
|
||||
@@ -156,6 +152,20 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const validCriteria = [
|
||||
...Object.keys(gptConfig.providedMetrics),
|
||||
Object.keys(gptConfig.derivedMetrics),
|
||||
];
|
||||
|
||||
if (!validCriteria.includes(req.body.criteria.key)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid criteria, expected any of ${validCriteria.join(", ")}.`,
|
||||
});
|
||||
}
|
||||
|
||||
const data = req.safeBody as GoalCreationBody;
|
||||
|
||||
let goal;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ServerConfig, TachiConfig } from "lib/setup/config";
|
||||
import { p } from "prudence";
|
||||
import { RejectIfBanned, RequirePermissions } from "server/middleware/auth";
|
||||
import { CreateMulterSingleUploadMiddleware } from "server/middleware/multer-upload";
|
||||
import { GetGamePTConfig } from "tachi-common";
|
||||
import { FormatPrError } from "utils/prudence";
|
||||
import { AssignToReqTachiData, GetTachiData } from "utils/req-tachi-data";
|
||||
import type { USCClientChart } from "./types";
|
||||
@@ -225,13 +226,15 @@ router.get("/charts/:chartHash/leaderboard", RetrieveChart, async (req, res) =>
|
||||
});
|
||||
}
|
||||
|
||||
const gptConfig = GetGamePTConfig("usc", chart.playtype);
|
||||
|
||||
const bestScores = (await db["personal-bests"].find(
|
||||
{
|
||||
chartID: chart.chartID,
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
"scoreData.percent": -1,
|
||||
[`scoreData.${gptConfig.defaultMetric}`]: -1,
|
||||
},
|
||||
limit: n,
|
||||
}
|
||||
|
||||
@@ -59,17 +59,19 @@ export async function TachiScoreToServerScore(
|
||||
);
|
||||
}
|
||||
|
||||
const firstScoreID = tachiScore.composedFrom[0].scoreID;
|
||||
|
||||
const scorePB = (await db.scores.findOne({
|
||||
scoreID: tachiScore.composedFrom.scorePB,
|
||||
scoreID: firstScoreID,
|
||||
})) as ScoreDocument<"usc:Controller" | "usc:Keyboard"> | null;
|
||||
|
||||
if (!scorePB) {
|
||||
logger.severe(
|
||||
`Score ${tachiScore.composedFrom.scorePB} does not exist, but is referenced in ${tachiScore.userID}'s PBDoc on ${tachiScore.chartID}?`
|
||||
`Score ${firstScoreID} does not exist, but is referenced in ${tachiScore.userID}'s PBDoc on ${tachiScore.chartID}?`
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Score ${tachiScore.composedFrom.scorePB} does not exist, but is referenced in ${tachiScore.userID}'s PBDoc on ${tachiScore.chartID}?`
|
||||
`Score ${firstScoreID} does not exist, but is referenced in ${tachiScore.userID}'s PBDoc on ${tachiScore.chartID}?`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -191,7 +193,9 @@ export async function CreatePOSTScoresResponseBody(
|
||||
score,
|
||||
serverRecord,
|
||||
isServerRecord: scorePB.userID === ktServerRecord.userID,
|
||||
isPB: scorePB.composedFrom.scorePB === scoreID,
|
||||
// it's a pb if the score is equal to what the user has as their best.
|
||||
// lamps notwithstanding.
|
||||
isPB: scorePB.scoreData.score === score.score,
|
||||
sendReplay: originalScore.scoreID,
|
||||
adjacentAbove,
|
||||
adjacentBelow,
|
||||
|
||||
Reference in New Issue
Block a user