mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-08 22:59:52 +03:00
checkpoint: 31 (SERVER BUILDS)
DON'T EVER DOUBT ME AGAIN
This commit is contained in:
@@ -47,7 +47,13 @@ const BMS_IMPL: GPTServerImplementation<GPTStrings["bms" | "pms"]> = {
|
||||
pb.scoreData.percent,
|
||||
IIDXLIKE_GBOUNDARIES[goalValue]!.name,
|
||||
// use notecount to turn the percent deltas into whole ex-scores.
|
||||
(v) => Math.floor((v / 100) * pb.scoreData.score).toFixed(0)
|
||||
(deltaPercent) => {
|
||||
const chartNotecount = Math.floor(
|
||||
pb.scoreData.score / (pb.scoreData.percent / 100)
|
||||
);
|
||||
|
||||
return (deltaPercent * (chartNotecount * 2)).toFixed(0);
|
||||
}
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -60,7 +60,7 @@ const IIDX_GOAL_PG_FMT: GPTGoalProgressFormatters<"iidx:DP" | "iidx:SP"> = {
|
||||
|
||||
return pb.scoreData.lamp;
|
||||
},
|
||||
grade: (pb, gradeIndex, chart) =>
|
||||
grade: (pb, gradeIndex) =>
|
||||
GradeGoalFormatter(
|
||||
IIDXLIKE_GBOUNDARIES,
|
||||
pb.scoreData.grade,
|
||||
@@ -68,7 +68,13 @@ const IIDX_GOAL_PG_FMT: GPTGoalProgressFormatters<"iidx:DP" | "iidx:SP"> = {
|
||||
IIDXLIKE_GBOUNDARIES[gradeIndex]!.name,
|
||||
|
||||
// use notecount to turn the percent deltas into whole ex-scores.
|
||||
(v) => Math.floor(v * chart.data.notecount * 2).toFixed(0)
|
||||
(deltaPercent) => {
|
||||
const chartNotecount = Math.floor(
|
||||
pb.scoreData.score / (pb.scoreData.percent / 100)
|
||||
);
|
||||
|
||||
return (deltaPercent * (chartNotecount * 2)).toFixed(0);
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
@@ -110,10 +110,6 @@ export type GPTDerivers<GPT extends GPTString> = {
|
||||
* Format a goal into a string. If a function is provided, it's called with this goals
|
||||
* criteria value, so a goal of "get 3600 on $CHART" would recieve 3600 as its
|
||||
* argument.
|
||||
*
|
||||
* Optionally, undefined can be returned to signify this failed somehow.
|
||||
* This is convenient for things like enum lookups which never fail in practice
|
||||
* but the typesystem thinks they might be undefined.
|
||||
*/
|
||||
export type GoalCriteriaFormatter = (num: number) => string;
|
||||
|
||||
|
||||
+53
-151
@@ -4,9 +4,9 @@ import fjsh from "fast-json-stable-hash";
|
||||
import { GPT_SERVER_IMPLEMENTATIONS } from "game-implementations/game-implementations";
|
||||
import { SubscribeFailReasons } from "lib/constants/err-codes";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { FormatGame, GetGPTString, GetGamePTConfig } from "tachi-common";
|
||||
import { FormatGame, GetGPTConfig, GetGPTString, GetScoreMetricConf } from "tachi-common";
|
||||
import { GetFolderChartIDs } from "utils/folder";
|
||||
import { IsNullish } from "utils/misc";
|
||||
import type { GoalCriteriaFormatter } from "game-implementations/types";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
import type { FilterQuery } from "mongodb";
|
||||
import type {
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
Playtype,
|
||||
QuestDocument,
|
||||
QuestSubscriptionDocument,
|
||||
GPTString,
|
||||
} from "tachi-common";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
@@ -71,29 +72,25 @@ export async function EvaluateGoalForUser(
|
||||
chartID: { $in: chartIDs },
|
||||
};
|
||||
|
||||
const gptString = GetGPTString(goal.game, goal.playtype);
|
||||
|
||||
switch (goal.criteria.mode) {
|
||||
case "single": {
|
||||
const res = await db["personal-bests"].findOne(scoreQuery);
|
||||
|
||||
// hack, but guaranteed to work.
|
||||
const scoreDataKey = goal.criteria.key;
|
||||
|
||||
const outOfHuman = HumaniseGoalOutOf(
|
||||
goal.game,
|
||||
goal.playtype,
|
||||
goal.criteria.key,
|
||||
goal.criteria.value
|
||||
);
|
||||
const outOfHuman = HumaniseGoalOutOf(gptString, goal.criteria.key, goal.criteria.value);
|
||||
|
||||
if (res) {
|
||||
return {
|
||||
achieved: true,
|
||||
outOf: goal.criteria.value,
|
||||
progress: res.scoreData[scoreDataKey],
|
||||
// @ts-expect-error completely ok, as it'll definitely be on these props.
|
||||
progress: res.scoreData[goal.criteria.key],
|
||||
outOfHuman,
|
||||
progressHuman: HumaniseGoalProgress(
|
||||
goal.game,
|
||||
goal.playtype,
|
||||
gptString,
|
||||
goal.criteria.key,
|
||||
goal.criteria.value,
|
||||
res
|
||||
@@ -129,10 +126,10 @@ export async function EvaluateGoalForUser(
|
||||
achieved: false,
|
||||
outOf: goal.criteria.value,
|
||||
outOfHuman,
|
||||
// @ts-expect-error completely ok, as it'll definitely be on these props.
|
||||
progress: nextBestScore.scoreData[scoreDataKey],
|
||||
progressHuman: HumaniseGoalProgress(
|
||||
goal.game,
|
||||
goal.playtype,
|
||||
gptString,
|
||||
goal.criteria.key,
|
||||
goal.criteria.value,
|
||||
nextBestScore
|
||||
@@ -212,159 +209,64 @@ type GoalKeys = GoalDocument["criteria"]["key"];
|
||||
* IIDX lamp goals.
|
||||
*/
|
||||
export function HumaniseGoalProgress(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
gptString: GPTString,
|
||||
key: GoalKeys,
|
||||
goalValue: integer,
|
||||
userPB: PBScoreDocument
|
||||
): string {
|
||||
switch (key) {
|
||||
case "scoreData.gradeIndex": {
|
||||
// for iidx, pms and bms we expect grade deltas (rarely) formatted like
|
||||
// AAA-1520
|
||||
// however, for other games (namely those that run with larger numbers)
|
||||
// we want to format this stuff like AA-172k.
|
||||
const fmtFn =
|
||||
game === "iidx" || game === "pms" || game === "bms"
|
||||
? undefined
|
||||
: (num: number) => Intl.NumberFormat("en", { notation: "compact" }).format(num);
|
||||
const gptImpl = GPT_SERVER_IMPLEMENTATIONS[gptString];
|
||||
|
||||
const goalGrade = GetGamePTConfig(game, playtype).grades[goalValue];
|
||||
// @ts-expect-error yeah this might fail, i know.
|
||||
const formatter = gptImpl.goalProgressFormatters[key];
|
||||
|
||||
if (!goalGrade) {
|
||||
throw new Error(
|
||||
`Invalid Goal -- Tried to get a grade with index '${goalValue}', but no such grade exists for ${FormatGame(
|
||||
game,
|
||||
playtype
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const { lower, upper, closer } = GenericFormatGradeDelta(
|
||||
game,
|
||||
playtype,
|
||||
userPB.scoreData.score,
|
||||
userPB.scoreData.percent,
|
||||
userPB.scoreData.grade,
|
||||
fmtFn
|
||||
);
|
||||
|
||||
// If this goal is, say, AAA $chart, and the user's deltas are AA+40, AAA-100
|
||||
// instead of picking the one with less delta from the grade (AA+40)
|
||||
// pick the one closest to the target grade.
|
||||
// Because sometimes this function wraps the grade operand in brackets
|
||||
// (see (MAX-)-50), we need a regexp for this.
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
|
||||
if (upper && new RegExp(`\\(?${goalGrade}`, "u").exec(upper)) {
|
||||
return upper;
|
||||
}
|
||||
|
||||
// for some reason, our TS compiler disagrees that this is non-nullable.
|
||||
// but my IDE thinks it is. Who knows.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
return closer === "lower" ? lower : upper!;
|
||||
}
|
||||
|
||||
case "scoreData.lampIndex": {
|
||||
switch (game) {
|
||||
case "iidx":
|
||||
case "bms":
|
||||
case "pms": {
|
||||
// @ts-expect-error This is guaranteed to exist, we're going to ignore it.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const maybeBP: number | null | undefined = userPB.scoreData.optional.bp;
|
||||
|
||||
// render BP if it exists
|
||||
if (!IsNullish(maybeBP)) {
|
||||
return `${userPB.scoreData.lamp} (BP: ${maybeBP})`;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "itg": {
|
||||
// @ts-expect-error This is guaranteed to exist, we're going to ignore it.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const maybeDiedAt: number | null | undefined = userPB.scoreData.optional.diedAt;
|
||||
|
||||
// render diedAt if it exists as "FAILED (Died 25% in)"
|
||||
if (!IsNullish(maybeDiedAt)) {
|
||||
return `${userPB.scoreData.lamp} (Died ${maybeDiedAt.toFixed(0)}% in)`;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// no special logic needed for these games, just render the lamp.
|
||||
case "chunithm":
|
||||
case "gitadora":
|
||||
case "jubeat":
|
||||
case "museca":
|
||||
case "popn":
|
||||
case "sdvx":
|
||||
case "usc":
|
||||
case "wacca":
|
||||
case "maimaidx":
|
||||
}
|
||||
|
||||
// otherwise, just roll on.
|
||||
return userPB.scoreData.lamp;
|
||||
}
|
||||
|
||||
case [`scoreData.${gptConfig.defaultMetric}`]:
|
||||
return `${userPB.scoreData.percent.toFixed(2)}%`;
|
||||
case "scoreData.score":
|
||||
return userPB.scoreData.score.toLocaleString();
|
||||
default:
|
||||
throw new Error(`Broken goal - invalid key ${key}.`);
|
||||
if (!formatter) {
|
||||
throw new Error(
|
||||
`Attempted to format progress for metric '${key}' when no such score metric exists for ${gptString}.`
|
||||
);
|
||||
}
|
||||
|
||||
return formatter(userPB, goalValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a goal's "outOf" (i.e. HARD CLEAR; AAA or score=2450) into a human-understandable
|
||||
* string.
|
||||
*/
|
||||
export function HumaniseGoalOutOf(game: Game, playtype: Playtype, key: GoalKeys, value: number) {
|
||||
const gptImpl = GPT_SERVER_IMPLEMENTATIONS[GetGPTString(game, playtype)];
|
||||
export function HumaniseGoalOutOf(gptString: GPTString, key: GoalKeys, value: number) {
|
||||
const gptConf = GetGPTConfig(gptString);
|
||||
|
||||
switch (key) {
|
||||
case "scoreData.gradeIndex": {
|
||||
const grade = gptConfig.grades[value];
|
||||
const metricConf = GetScoreMetricConf(gptConf, key);
|
||||
|
||||
if (!grade) {
|
||||
throw new Error(
|
||||
`Invalid goal: Requested a grade with index '${value}' for ${FormatGame(
|
||||
game,
|
||||
playtype
|
||||
)}, yet none existed?`
|
||||
);
|
||||
}
|
||||
|
||||
return grade;
|
||||
}
|
||||
|
||||
case "scoreData.lampIndex": {
|
||||
const lamp = gptConfig.lamps[value];
|
||||
|
||||
if (!lamp) {
|
||||
throw new Error(
|
||||
`Invalid goal: Requested a lamp with index '${value}' for ${FormatGame(
|
||||
game,
|
||||
playtype
|
||||
)}, yet none existed?`
|
||||
);
|
||||
}
|
||||
|
||||
return lamp;
|
||||
}
|
||||
|
||||
case [`scoreData.${gptConfig.defaultMetric}`]:
|
||||
return `${value.toFixed(2)}%`;
|
||||
case "scoreData.score":
|
||||
return value.toLocaleString();
|
||||
default:
|
||||
throw new Error(`Broken goal - invalid key ${key}.`);
|
||||
if (!metricConf) {
|
||||
throw new Error(
|
||||
`Attempted to format outOf for metric '${key}' when no such score metric exists for ${gptString}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (metricConf.type === "ENUM") {
|
||||
const val = metricConf.values[value];
|
||||
|
||||
if (val === undefined) {
|
||||
throw new Error(
|
||||
`Attempted to format outOf for metric '${key}' but no such enum exists at index ${value}. (${gptString})`
|
||||
);
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
const gptImpl = GPT_SERVER_IMPLEMENTATIONS[gptString];
|
||||
|
||||
// @ts-expect-error yeah this is technically unsafe, whatever
|
||||
const fmt: GoalCriteriaFormatter | undefined = gptImpl.goalCriteriaFormatters[key];
|
||||
|
||||
if (!fmt) {
|
||||
throw new Error(
|
||||
`Invalid metric '${key}' passed to format outOf, as no goalCriteriaFormatter exists for it.`
|
||||
);
|
||||
}
|
||||
|
||||
return fmt(value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user