mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2026-02-04 14:39:02 +08:00
refactor: result types (#4980)
* further shared improvements - moved more types to shared - reworked the way results are typed THIS COULD CHANGE LOGIC, TEST THIS * removed comment * update the way completed event is built on the client * remove unnecessary property * comment * move hash check higher * remove todo * fix incorrect type * updated type remove field if undefined
This commit is contained in:
parent
0920010f5e
commit
8b48347764
41 changed files with 724 additions and 755 deletions
|
|
@ -116,7 +116,7 @@ describe("LeaderboardsDal", () => {
|
|||
});
|
||||
|
||||
function expectedLbEntry(rank: number, user: MonkeyTypes.User, time: string) {
|
||||
const lbBest: MonkeyTypes.PersonalBest =
|
||||
const lbBest: SharedTypes.PersonalBest =
|
||||
user.lbPersonalBests?.time[time].english;
|
||||
|
||||
return {
|
||||
|
|
@ -166,8 +166,8 @@ async function createUser(
|
|||
}
|
||||
|
||||
function lbBests(
|
||||
pb15?: MonkeyTypes.PersonalBest,
|
||||
pb60?: MonkeyTypes.PersonalBest
|
||||
pb15?: SharedTypes.PersonalBest,
|
||||
pb60?: SharedTypes.PersonalBest
|
||||
): MonkeyTypes.LbPersonalBests {
|
||||
const result = { time: {} };
|
||||
if (pb15) result.time["15"] = { english: pb15 };
|
||||
|
|
@ -179,7 +179,7 @@ function pb(
|
|||
wpm: number,
|
||||
acc: number = 90,
|
||||
timestamp: number = 1
|
||||
): MonkeyTypes.PersonalBest {
|
||||
): SharedTypes.PersonalBest {
|
||||
return {
|
||||
acc,
|
||||
consistency: 100,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as ResultDal from "../../src/dal/result";
|
|||
import { ObjectId } from "mongodb";
|
||||
import * as UserDal from "../../src/dal/user";
|
||||
|
||||
type MonkeyTypesResult = MonkeyTypes.Result<MonkeyTypes.Mode>;
|
||||
type MonkeyTypesResult = SharedTypes.DBResult<SharedTypes.Mode>;
|
||||
|
||||
let uid: string = "";
|
||||
const timestamp = Date.now() - 60000;
|
||||
|
|
@ -55,6 +55,8 @@ async function createDummyData(
|
|||
keyDurationStats: { average: 0, sd: 0 },
|
||||
difficulty: "normal",
|
||||
language: "english",
|
||||
isPb: false,
|
||||
name: "Test",
|
||||
} as MonkeyTypesResult);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,9 +15,13 @@ const mockPersonalBest = {
|
|||
timestamp: 13123123,
|
||||
};
|
||||
|
||||
const mockResultFilter = {
|
||||
_id: new ObjectId(),
|
||||
const mockResultFilter: SharedTypes.ResultFilters = {
|
||||
_id: "id",
|
||||
name: "sfdkjhgdf",
|
||||
pb: {
|
||||
no: true,
|
||||
yes: true,
|
||||
},
|
||||
difficulty: {
|
||||
normal: true,
|
||||
expert: false,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import _ from "lodash";
|
|||
import * as WeeklyXpLeaderboard from "../../services/weekly-xp-leaderboard";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { canFunboxGetPb } from "../../utils/pb";
|
||||
import { buildDbResult } from "../../utils/result";
|
||||
|
||||
try {
|
||||
if (anticheatImplemented() === false) throw new Error("undefined");
|
||||
|
|
@ -194,32 +195,37 @@ export async function addResult(
|
|||
);
|
||||
}
|
||||
|
||||
//todo add a type here
|
||||
const result = Object.assign({}, req.body.result);
|
||||
if (!user.lbOptOut && result.acc < 75) {
|
||||
const completedEvent = Object.assign(
|
||||
{},
|
||||
req.body.result
|
||||
) as SharedTypes.CompletedEvent;
|
||||
if (!user.lbOptOut && completedEvent.acc < 75) {
|
||||
throw new MonkeyError(
|
||||
400,
|
||||
"Cannot submit a result with less than 75% accuracy"
|
||||
);
|
||||
}
|
||||
result.uid = uid;
|
||||
if (isTestTooShort(result)) {
|
||||
completedEvent.uid = uid;
|
||||
if (isTestTooShort(completedEvent)) {
|
||||
const status = MonkeyStatusCodes.TEST_TOO_SHORT;
|
||||
throw new MonkeyError(status.code, status.message);
|
||||
}
|
||||
|
||||
const resulthash = result.hash;
|
||||
delete result.hash;
|
||||
delete result.stringified;
|
||||
const resulthash = completedEvent.hash;
|
||||
if (!resulthash) {
|
||||
throw new MonkeyError(400, "Missing result hash");
|
||||
}
|
||||
delete completedEvent.hash;
|
||||
delete completedEvent.stringified;
|
||||
if (req.ctx.configuration.results.objectHashCheckEnabled) {
|
||||
const serverhash = objectHash(result);
|
||||
const serverhash = objectHash(completedEvent);
|
||||
if (serverhash !== resulthash) {
|
||||
Logger.logToDb(
|
||||
"incorrect_result_hash",
|
||||
{
|
||||
serverhash,
|
||||
resulthash,
|
||||
result,
|
||||
result: completedEvent,
|
||||
},
|
||||
uid
|
||||
);
|
||||
|
|
@ -228,43 +234,41 @@ export async function addResult(
|
|||
}
|
||||
}
|
||||
|
||||
if (result.funbox) {
|
||||
const funboxes = result.funbox.split("#");
|
||||
if (completedEvent.funbox) {
|
||||
const funboxes = completedEvent.funbox.split("#");
|
||||
if (funboxes.length !== _.uniq(funboxes).length) {
|
||||
throw new MonkeyError(400, "Duplicate funboxes");
|
||||
}
|
||||
}
|
||||
|
||||
if (!areFunboxesCompatible(result.funbox)) {
|
||||
if (!areFunboxesCompatible(completedEvent.funbox ?? "")) {
|
||||
throw new MonkeyError(400, "Impossible funbox combination");
|
||||
}
|
||||
|
||||
try {
|
||||
result.keySpacingStats = {
|
||||
if (completedEvent.keySpacing !== "toolong") {
|
||||
completedEvent.keySpacingStats = {
|
||||
average:
|
||||
result.keySpacing.reduce((previous, current) => (current += previous)) /
|
||||
result.keySpacing.length,
|
||||
sd: stdDev(result.keySpacing),
|
||||
};
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
try {
|
||||
result.keyDurationStats = {
|
||||
average:
|
||||
result.keyDuration.reduce(
|
||||
completedEvent.keySpacing.reduce(
|
||||
(previous, current) => (current += previous)
|
||||
) / result.keyDuration.length,
|
||||
sd: stdDev(result.keyDuration),
|
||||
) / completedEvent.keySpacing.length,
|
||||
sd: stdDev(completedEvent.keySpacing),
|
||||
};
|
||||
}
|
||||
|
||||
if (completedEvent.keyDuration !== "toolong") {
|
||||
completedEvent.keyDurationStats = {
|
||||
average:
|
||||
completedEvent.keyDuration.reduce(
|
||||
(previous, current) => (current += previous)
|
||||
) / completedEvent.keyDuration.length,
|
||||
sd: stdDev(completedEvent.keyDuration),
|
||||
};
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
if (anticheatImplemented()) {
|
||||
if (
|
||||
!validateResult(
|
||||
result,
|
||||
completedEvent,
|
||||
(req.headers["x-client-version"] ||
|
||||
req.headers["client-version"]) as string,
|
||||
JSON.stringify(new UAParser(req.headers["user-agent"]).getResult()),
|
||||
|
|
@ -305,7 +309,7 @@ export async function addResult(
|
|||
// }
|
||||
|
||||
//convert result test duration to miliseconds
|
||||
const testDurationMilis = result.testDuration * 1000;
|
||||
const testDurationMilis = completedEvent.testDuration * 1000;
|
||||
//get latest result ordered by timestamp
|
||||
let lastResultTimestamp;
|
||||
try {
|
||||
|
|
@ -314,7 +318,7 @@ export async function addResult(
|
|||
lastResultTimestamp = null;
|
||||
}
|
||||
|
||||
result.timestamp = Math.floor(Date.now() / 1000) * 1000;
|
||||
completedEvent.timestamp = Math.floor(Date.now() / 1000) * 1000;
|
||||
|
||||
//check if now is earlier than last result plus duration (-1 second as a buffer)
|
||||
const earliestPossible = lastResultTimestamp + testDurationMilis;
|
||||
|
|
@ -337,22 +341,22 @@ export async function addResult(
|
|||
|
||||
//check keyspacing and duration here for bots
|
||||
if (
|
||||
result.mode === "time" &&
|
||||
result.wpm > 130 &&
|
||||
result.testDuration < 122 &&
|
||||
completedEvent.mode === "time" &&
|
||||
completedEvent.wpm > 130 &&
|
||||
completedEvent.testDuration < 122 &&
|
||||
(user.verified === false || user.verified === undefined) &&
|
||||
user.lbOptOut !== true &&
|
||||
user.banned !== true //no need to check again if user is already banned
|
||||
) {
|
||||
if (!result.keySpacingStats || !result.keyDurationStats) {
|
||||
if (!completedEvent.keySpacingStats || !completedEvent.keyDurationStats) {
|
||||
const status = MonkeyStatusCodes.MISSING_KEY_DATA;
|
||||
throw new MonkeyError(status.code, "Missing key data");
|
||||
}
|
||||
if (result.keyOverlap === undefined) {
|
||||
if (completedEvent.keyOverlap === undefined) {
|
||||
throw new MonkeyError(400, "Old key data format");
|
||||
}
|
||||
if (anticheatImplemented()) {
|
||||
if (!validateKeys(result, uid)) {
|
||||
if (!validateKeys(completedEvent, uid)) {
|
||||
//autoban
|
||||
const autoBanConfig = req.ctx.configuration.users.autoBan;
|
||||
if (autoBanConfig.enabled) {
|
||||
|
|
@ -383,15 +387,6 @@ export async function addResult(
|
|||
}
|
||||
}
|
||||
|
||||
delete result.keySpacing;
|
||||
delete result.keyDuration;
|
||||
delete result.smoothConsistency;
|
||||
delete result.wpmConsistency;
|
||||
delete result.keyOverlap;
|
||||
delete result.lastKeyToEnd;
|
||||
delete result.startToFirstKey;
|
||||
delete result.charTotal;
|
||||
|
||||
if (req.ctx.configuration.users.lastHashesCheck.enabled) {
|
||||
let lastHashes = user.lastReultHashes ?? [];
|
||||
if (lastHashes.includes(resulthash)) {
|
||||
|
|
@ -400,7 +395,7 @@ export async function addResult(
|
|||
{
|
||||
lastHashes,
|
||||
resulthash,
|
||||
result,
|
||||
result: completedEvent,
|
||||
},
|
||||
uid
|
||||
);
|
||||
|
|
@ -416,67 +411,73 @@ export async function addResult(
|
|||
}
|
||||
}
|
||||
|
||||
result.name = user.name;
|
||||
|
||||
try {
|
||||
result.keyDurationStats.average = roundTo2(result.keyDurationStats.average);
|
||||
result.keyDurationStats.sd = roundTo2(result.keyDurationStats.sd);
|
||||
result.keySpacingStats.average = roundTo2(result.keySpacingStats.average);
|
||||
result.keySpacingStats.sd = roundTo2(result.keySpacingStats.sd);
|
||||
} catch (e) {
|
||||
//
|
||||
if (completedEvent.keyDurationStats) {
|
||||
completedEvent.keyDurationStats.average = roundTo2(
|
||||
completedEvent.keyDurationStats.average
|
||||
);
|
||||
completedEvent.keyDurationStats.sd = roundTo2(
|
||||
completedEvent.keyDurationStats.sd
|
||||
);
|
||||
}
|
||||
if (completedEvent.keySpacingStats) {
|
||||
completedEvent.keySpacingStats.average = roundTo2(
|
||||
completedEvent.keySpacingStats.average
|
||||
);
|
||||
completedEvent.keySpacingStats.sd = roundTo2(
|
||||
completedEvent.keySpacingStats.sd
|
||||
);
|
||||
}
|
||||
|
||||
let isPb = false;
|
||||
let tagPbs: string[] = [];
|
||||
|
||||
if (!result.bailedOut) {
|
||||
if (!completedEvent.bailedOut) {
|
||||
[isPb, tagPbs] = await Promise.all([
|
||||
checkIfPb(uid, user, result),
|
||||
checkIfTagPb(uid, user, result),
|
||||
checkIfPb(uid, user, completedEvent),
|
||||
checkIfTagPb(uid, user, completedEvent),
|
||||
]);
|
||||
}
|
||||
|
||||
if (isPb) {
|
||||
result.isPb = true;
|
||||
}
|
||||
|
||||
if (result.mode === "time" && result.mode2 === "60") {
|
||||
incrementBananas(uid, result.wpm);
|
||||
if (completedEvent.mode === "time" && completedEvent.mode2 === "60") {
|
||||
incrementBananas(uid, completedEvent.wpm);
|
||||
if (isPb && user.discordId) {
|
||||
GeorgeQueue.updateDiscordRole(user.discordId, result.wpm);
|
||||
GeorgeQueue.updateDiscordRole(user.discordId, completedEvent.wpm);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
result.challenge &&
|
||||
AutoRoleList.includes(result.challenge) &&
|
||||
completedEvent.challenge &&
|
||||
AutoRoleList.includes(completedEvent.challenge) &&
|
||||
user.discordId
|
||||
) {
|
||||
GeorgeQueue.awardChallenge(user.discordId, result.challenge);
|
||||
GeorgeQueue.awardChallenge(user.discordId, completedEvent.challenge);
|
||||
} else {
|
||||
delete result.challenge;
|
||||
delete completedEvent.challenge;
|
||||
}
|
||||
|
||||
const afk = result.afkDuration ?? 0;
|
||||
const afk = completedEvent.afkDuration ?? 0;
|
||||
const totalDurationTypedSeconds =
|
||||
result.testDuration + result.incompleteTestSeconds - afk;
|
||||
updateTypingStats(uid, result.restartCount, totalDurationTypedSeconds);
|
||||
PublicDAL.updateStats(result.restartCount, totalDurationTypedSeconds);
|
||||
completedEvent.testDuration + completedEvent.incompleteTestSeconds - afk;
|
||||
updateTypingStats(
|
||||
uid,
|
||||
completedEvent.restartCount,
|
||||
totalDurationTypedSeconds
|
||||
);
|
||||
PublicDAL.updateStats(completedEvent.restartCount, totalDurationTypedSeconds);
|
||||
|
||||
const dailyLeaderboardsConfig = req.ctx.configuration.dailyLeaderboards;
|
||||
const dailyLeaderboard = getDailyLeaderboard(
|
||||
result.language,
|
||||
result.mode,
|
||||
result.mode2,
|
||||
completedEvent.language,
|
||||
completedEvent.mode,
|
||||
completedEvent.mode2,
|
||||
dailyLeaderboardsConfig
|
||||
);
|
||||
|
||||
let dailyLeaderboardRank = -1;
|
||||
|
||||
const validResultCriteria =
|
||||
canFunboxGetPb(result) &&
|
||||
!result.bailedOut &&
|
||||
canFunboxGetPb(completedEvent) &&
|
||||
!completedEvent.bailedOut &&
|
||||
user.banned !== true &&
|
||||
user.lbOptOut !== true &&
|
||||
(isDevEnvironment() || (user.timeTyping ?? 0) > 7200);
|
||||
|
|
@ -484,15 +485,19 @@ export async function addResult(
|
|||
const selectedBadgeId = user.inventory?.badges?.find((b) => b.selected)?.id;
|
||||
|
||||
if (dailyLeaderboard && validResultCriteria) {
|
||||
incrementDailyLeaderboard(result.mode, result.mode2, result.language);
|
||||
incrementDailyLeaderboard(
|
||||
completedEvent.mode,
|
||||
completedEvent.mode2,
|
||||
completedEvent.language
|
||||
);
|
||||
dailyLeaderboardRank = await dailyLeaderboard.addResult(
|
||||
{
|
||||
name: user.name,
|
||||
wpm: result.wpm,
|
||||
raw: result.rawWpm,
|
||||
acc: result.acc,
|
||||
consistency: result.consistency,
|
||||
timestamp: result.timestamp,
|
||||
wpm: completedEvent.wpm,
|
||||
raw: completedEvent.rawWpm,
|
||||
acc: completedEvent.acc,
|
||||
consistency: completedEvent.consistency,
|
||||
timestamp: completedEvent.timestamp,
|
||||
uid,
|
||||
discordAvatar: user.discordAvatar,
|
||||
discordId: user.discordId,
|
||||
|
|
@ -502,7 +507,7 @@ export async function addResult(
|
|||
);
|
||||
}
|
||||
|
||||
const streak = await UserDAL.updateStreak(uid, result.timestamp);
|
||||
const streak = await UserDAL.updateStreak(uid, completedEvent.timestamp);
|
||||
|
||||
const shouldGetBadge =
|
||||
streak >= 365 &&
|
||||
|
|
@ -532,7 +537,7 @@ export async function addResult(
|
|||
}
|
||||
|
||||
const xpGained = await calculateXp(
|
||||
result,
|
||||
completedEvent,
|
||||
req.ctx.configuration.users.xp,
|
||||
uid,
|
||||
user.xp ?? 0,
|
||||
|
|
@ -545,7 +550,7 @@ export async function addResult(
|
|||
"Calculated XP is negative",
|
||||
JSON.stringify({
|
||||
xpGained,
|
||||
result,
|
||||
result: completedEvent,
|
||||
}),
|
||||
uid
|
||||
);
|
||||
|
|
@ -583,32 +588,20 @@ export async function addResult(
|
|||
);
|
||||
}
|
||||
|
||||
if (result.bailedOut === false) delete result.bailedOut;
|
||||
if (result.blindMode === false) delete result.blindMode;
|
||||
if (result.lazyMode === false) delete result.lazyMode;
|
||||
if (result.difficulty === "normal") delete result.difficulty;
|
||||
if (result.funbox === "none") delete result.funbox;
|
||||
if (result.language === "english") delete result.language;
|
||||
if (result.numbers === false) delete result.numbers;
|
||||
if (result.punctuation === false) delete result.punctuation;
|
||||
if (result.mode !== "custom") delete result.customText;
|
||||
if (result.restartCount === 0) delete result.restartCount;
|
||||
if (result.incompleteTestSeconds === 0) delete result.incompleteTestSeconds;
|
||||
if (result.afkDuration === 0) delete result.afkDuration;
|
||||
if (result.tags.length === 0) delete result.tags;
|
||||
const dbresult = buildDbResult(completedEvent, user.name, isPb);
|
||||
|
||||
delete result.incompleteTests;
|
||||
|
||||
const addedResult = await ResultDAL.addResult(uid, result);
|
||||
const addedResult = await ResultDAL.addResult(uid, dbresult);
|
||||
|
||||
await UserDAL.incrementXp(uid, xpGained.xp);
|
||||
|
||||
if (isPb) {
|
||||
Logger.logToDb(
|
||||
"user_new_pb",
|
||||
`${result.mode + " " + result.mode2} ${result.wpm} ${result.acc}% ${
|
||||
result.rawWpm
|
||||
} ${result.consistency}% (${addedResult.insertedId})`,
|
||||
`${completedEvent.mode + " " + completedEvent.mode2} ${
|
||||
completedEvent.wpm
|
||||
} ${completedEvent.acc}% ${completedEvent.rawWpm} ${
|
||||
completedEvent.consistency
|
||||
}% (${addedResult.insertedId})`,
|
||||
uid
|
||||
);
|
||||
}
|
||||
|
|
@ -631,7 +624,7 @@ export async function addResult(
|
|||
data.weeklyXpLeaderboardRank = weeklyXpLeaderboardRank;
|
||||
}
|
||||
|
||||
incrementResult(result);
|
||||
incrementResult(completedEvent);
|
||||
|
||||
return new MonkeyResponse("Result saved", data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -571,7 +571,7 @@ export async function updateLbMemory(
|
|||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const { mode, language, rank } = req.body;
|
||||
const mode2 = req.body.mode2 as MonkeyTypes.Mode2<MonkeyTypes.Mode>;
|
||||
const mode2 = req.body.mode2 as SharedTypes.Mode2<SharedTypes.Mode>;
|
||||
|
||||
await UserDAL.updateLbMemory(uid, mode, mode2, language, rank);
|
||||
return new MonkeyResponse("Leaderboard memory updated");
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import * as db from "../init/db";
|
|||
|
||||
import { getUser, getTags } from "./user";
|
||||
|
||||
type MonkeyTypesResult = MonkeyTypes.Result<MonkeyTypes.Mode>;
|
||||
type MonkeyTypesResult = SharedTypes.DBResult<SharedTypes.Mode>;
|
||||
|
||||
export async function addResult(
|
||||
uid: string,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { flattenObjectDeep, isToday, isYesterday } from "../utils/misc";
|
|||
|
||||
const SECONDS_PER_HOUR = 3600;
|
||||
|
||||
type Result = Omit<SharedTypes.DBResult<SharedTypes.Mode>, "_id" | "name">;
|
||||
|
||||
// Export for use in tests
|
||||
export const getUsersCollection = (): Collection<WithId<MonkeyTypes.User>> =>
|
||||
db.collection<MonkeyTypes.User>("users");
|
||||
|
|
@ -230,7 +232,7 @@ export async function isDiscordIdAvailable(
|
|||
|
||||
export async function addResultFilterPreset(
|
||||
uid: string,
|
||||
filter: MonkeyTypes.ResultFilters,
|
||||
filter: SharedTypes.ResultFilters,
|
||||
maxFiltersPerUser: number
|
||||
): Promise<ObjectId> {
|
||||
// ensure limit not reached
|
||||
|
|
@ -261,8 +263,8 @@ export async function removeResultFilterPreset(
|
|||
const filterId = new ObjectId(_id);
|
||||
if (
|
||||
user.resultFilterPresets === undefined ||
|
||||
user.resultFilterPresets.filter((t) => t._id.toHexString() === _id)
|
||||
.length === 0
|
||||
user.resultFilterPresets.filter((t) => t._id.toString() === _id).length ===
|
||||
0
|
||||
) {
|
||||
throw new MonkeyError(404, "Custom filter not found");
|
||||
}
|
||||
|
|
@ -383,8 +385,8 @@ export async function removeTagPb(uid: string, _id: string): Promise<void> {
|
|||
|
||||
export async function updateLbMemory(
|
||||
uid: string,
|
||||
mode: MonkeyTypes.Mode,
|
||||
mode2: MonkeyTypes.Mode2<MonkeyTypes.Mode>,
|
||||
mode: SharedTypes.Mode,
|
||||
mode2: SharedTypes.Mode2<SharedTypes.Mode>,
|
||||
language: string,
|
||||
rank: number
|
||||
): Promise<void> {
|
||||
|
|
@ -406,7 +408,7 @@ export async function updateLbMemory(
|
|||
export async function checkIfPb(
|
||||
uid: string,
|
||||
user: MonkeyTypes.User,
|
||||
result: MonkeyTypes.Result<MonkeyTypes.Mode>
|
||||
result: Result
|
||||
): Promise<boolean> {
|
||||
const { mode } = result;
|
||||
|
||||
|
|
@ -448,7 +450,7 @@ export async function checkIfPb(
|
|||
export async function checkIfTagPb(
|
||||
uid: string,
|
||||
user: MonkeyTypes.User,
|
||||
result: MonkeyTypes.Result<MonkeyTypes.Mode>
|
||||
result: Result
|
||||
): Promise<string[]> {
|
||||
if (user.tags === undefined || user.tags.length === 0) {
|
||||
return [];
|
||||
|
|
@ -463,11 +465,11 @@ export async function checkIfTagPb(
|
|||
|
||||
const tagsToCheck: MonkeyTypes.UserTag[] = [];
|
||||
user.tags.forEach((userTag) => {
|
||||
resultTags.forEach((resultTag) => {
|
||||
for (const resultTag of resultTags ?? []) {
|
||||
if (resultTag === userTag._id.toHexString()) {
|
||||
tagsToCheck.push(userTag);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const ret: string[] = [];
|
||||
|
|
@ -676,7 +678,7 @@ export async function getPersonalBests(
|
|||
uid: string,
|
||||
mode: string,
|
||||
mode2?: string
|
||||
): Promise<MonkeyTypes.PersonalBest> {
|
||||
): Promise<SharedTypes.PersonalBest> {
|
||||
const user = await getUser(uid, "get personal bests");
|
||||
|
||||
if (mode2) {
|
||||
|
|
|
|||
180
backend/src/types/types.d.ts
vendored
180
backend/src/types/types.d.ts
vendored
|
|
@ -71,7 +71,7 @@ declare namespace MonkeyTypes {
|
|||
lbPersonalBests?: LbPersonalBests;
|
||||
name: string;
|
||||
customThemes?: CustomTheme[];
|
||||
personalBests: PersonalBests;
|
||||
personalBests: SharedTypes.PersonalBests;
|
||||
quoteRatings?: UserQuoteRatings;
|
||||
startedTests?: number;
|
||||
tags?: UserTag[];
|
||||
|
|
@ -86,7 +86,7 @@ declare namespace MonkeyTypes {
|
|||
favoriteQuotes?: Record<string, string[]>;
|
||||
needsToChangeName?: boolean;
|
||||
discordAvatar?: string;
|
||||
resultFilterPresets?: ResultFilters[];
|
||||
resultFilterPresets?: WithObjectIdArray<SharedTypes.ResultFilters[]>;
|
||||
profileDetails?: UserProfileDetails;
|
||||
inventory?: UserInventory;
|
||||
xp?: number;
|
||||
|
|
@ -114,89 +114,36 @@ declare namespace MonkeyTypes {
|
|||
selected?: boolean;
|
||||
}
|
||||
|
||||
interface ResultFilters {
|
||||
_id: ObjectId;
|
||||
name: string;
|
||||
difficulty: {
|
||||
normal: boolean;
|
||||
expert: boolean;
|
||||
master: boolean;
|
||||
};
|
||||
mode: {
|
||||
words: boolean;
|
||||
time: boolean;
|
||||
quote: boolean;
|
||||
zen: boolean;
|
||||
custom: boolean;
|
||||
};
|
||||
words: {
|
||||
10: boolean;
|
||||
25: boolean;
|
||||
50: boolean;
|
||||
100: boolean;
|
||||
custom: boolean;
|
||||
};
|
||||
time: {
|
||||
15: boolean;
|
||||
30: boolean;
|
||||
60: boolean;
|
||||
120: boolean;
|
||||
custom: boolean;
|
||||
};
|
||||
quoteLength: {
|
||||
short: boolean;
|
||||
medium: boolean;
|
||||
long: boolean;
|
||||
thicc: boolean;
|
||||
};
|
||||
punctuation: {
|
||||
on: boolean;
|
||||
off: boolean;
|
||||
};
|
||||
numbers: {
|
||||
on: boolean;
|
||||
off: boolean;
|
||||
};
|
||||
date: {
|
||||
last_day: boolean;
|
||||
last_week: boolean;
|
||||
last_month: boolean;
|
||||
last_3months: boolean;
|
||||
all: boolean;
|
||||
};
|
||||
tags: {
|
||||
[tagId: string]: boolean;
|
||||
};
|
||||
language: {
|
||||
[language: string]: boolean;
|
||||
};
|
||||
funbox: {
|
||||
none: boolean;
|
||||
[funbox: string]: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
type UserQuoteRatings = Record<string, Record<string, number>>;
|
||||
|
||||
interface LbPersonalBests {
|
||||
time: {
|
||||
[key: number]: {
|
||||
[key: string]: PersonalBest;
|
||||
[key: string]: SharedTypes.PersonalBest;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
type WithObjectId<T extends { _id: string }> = Omit<T, "_id"> & {
|
||||
_id: ObjectId;
|
||||
};
|
||||
|
||||
type WithObjectIdArray<T extends { _id: string }[]> = Omit<T, "_id"> &
|
||||
{
|
||||
_id: ObjectId;
|
||||
}[];
|
||||
|
||||
interface UserTag {
|
||||
_id: ObjectId;
|
||||
name: string;
|
||||
personalBests: PersonalBests;
|
||||
personalBests: SharedTypes.PersonalBests;
|
||||
}
|
||||
|
||||
interface LeaderboardEntry {
|
||||
_id: ObjectId;
|
||||
acc: number;
|
||||
consistency: number;
|
||||
difficulty: Difficulty;
|
||||
difficulty: SharedTypes.Difficulty;
|
||||
lazyMode: boolean;
|
||||
language: string;
|
||||
punctuation: boolean;
|
||||
|
|
@ -238,105 +185,6 @@ declare namespace MonkeyTypes {
|
|||
approved: boolean;
|
||||
}
|
||||
|
||||
type Mode = keyof PersonalBests;
|
||||
|
||||
type Mode2<M extends Mode> = keyof PersonalBests[M];
|
||||
|
||||
type StringNumber = `${number}`;
|
||||
|
||||
type Difficulty = "normal" | "expert" | "master";
|
||||
|
||||
interface PersonalBest {
|
||||
acc: number;
|
||||
consistency: number;
|
||||
difficulty: Difficulty;
|
||||
lazyMode: boolean;
|
||||
language: string;
|
||||
punctuation: boolean;
|
||||
raw: number;
|
||||
wpm: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface PersonalBests {
|
||||
time: Record<StringNumber, PersonalBest[]>;
|
||||
words: Record<StringNumber, PersonalBest[]>;
|
||||
quote: Record<StringNumber, PersonalBest[]>;
|
||||
custom: Partial<Record<"custom", PersonalBest[]>>;
|
||||
zen: Partial<Record<"zen", PersonalBest[]>>;
|
||||
}
|
||||
|
||||
interface ChartData {
|
||||
wpm: number[];
|
||||
raw: number[];
|
||||
err: number[];
|
||||
}
|
||||
|
||||
interface KeyStats {
|
||||
average: number;
|
||||
sd: number;
|
||||
}
|
||||
|
||||
interface IncompleteTest {
|
||||
acc: number;
|
||||
seconds: number;
|
||||
}
|
||||
|
||||
interface Result<M extends Mode> {
|
||||
_id: ObjectId;
|
||||
wpm: number;
|
||||
rawWpm: number;
|
||||
charStats: number[];
|
||||
correctChars?: number; // --------------
|
||||
incorrectChars?: number; // legacy results
|
||||
acc: number;
|
||||
mode: M;
|
||||
mode2: Mode2<M>;
|
||||
quoteLength: number;
|
||||
timestamp: number;
|
||||
restartCount: number;
|
||||
incompleteTestSeconds: number;
|
||||
incompleteTests: IncompleteTest[];
|
||||
testDuration: number;
|
||||
afkDuration: number;
|
||||
tags: string[];
|
||||
consistency: number;
|
||||
keyConsistency: number;
|
||||
chartData: ChartData | "toolong";
|
||||
uid: string;
|
||||
keySpacingStats: KeyStats;
|
||||
keyDurationStats: KeyStats;
|
||||
isPb?: boolean;
|
||||
bailedOut?: boolean;
|
||||
blindMode?: boolean;
|
||||
lazyMode?: boolean;
|
||||
difficulty: Difficulty;
|
||||
funbox?: string;
|
||||
language: string;
|
||||
numbers?: boolean;
|
||||
punctuation?: boolean;
|
||||
hash?: string;
|
||||
}
|
||||
|
||||
interface CompletedEvent extends MonkeyTypes.Result<MonkeyTypes.Mode> {
|
||||
keySpacing: number[] | "toolong";
|
||||
keyDuration: number[] | "toolong";
|
||||
customText: MonkeyTypes.CustomText;
|
||||
wpmConsistency: number;
|
||||
lang: string;
|
||||
challenge?: string | null;
|
||||
}
|
||||
|
||||
interface CustomText {
|
||||
text: string[];
|
||||
isWordRandom: boolean;
|
||||
isTimeRandom: boolean;
|
||||
word: number;
|
||||
time: number;
|
||||
delimiter: string;
|
||||
textLen?: number;
|
||||
}
|
||||
|
||||
interface PSA {
|
||||
sticky?: boolean;
|
||||
message: string;
|
||||
|
|
|
|||
|
|
@ -3,15 +3,13 @@ import FunboxList from "../constants/funbox-list";
|
|||
|
||||
interface CheckAndUpdatePbResult {
|
||||
isPb: boolean;
|
||||
personalBests: MonkeyTypes.PersonalBests;
|
||||
personalBests: SharedTypes.PersonalBests;
|
||||
lbPersonalBests?: MonkeyTypes.LbPersonalBests;
|
||||
}
|
||||
|
||||
type Result = MonkeyTypes.Result<MonkeyTypes.Mode>;
|
||||
type Result = Omit<SharedTypes.DBResult<SharedTypes.Mode>, "_id" | "name">;
|
||||
|
||||
export function canFunboxGetPb(
|
||||
result: MonkeyTypes.Result<MonkeyTypes.Mode>
|
||||
): boolean {
|
||||
export function canFunboxGetPb(result: Result): boolean {
|
||||
const funbox = result.funbox;
|
||||
if (!funbox || funbox === "none") return true;
|
||||
|
||||
|
|
@ -29,19 +27,19 @@ export function canFunboxGetPb(
|
|||
}
|
||||
|
||||
export function checkAndUpdatePb(
|
||||
userPersonalBests: MonkeyTypes.PersonalBests,
|
||||
userPersonalBests: SharedTypes.PersonalBests,
|
||||
lbPersonalBests: MonkeyTypes.LbPersonalBests | undefined,
|
||||
result: Result
|
||||
): CheckAndUpdatePbResult {
|
||||
const mode = result.mode;
|
||||
const mode2 = result.mode2 as MonkeyTypes.Mode2<"time">;
|
||||
const mode2 = result.mode2 as SharedTypes.Mode2<"time">;
|
||||
|
||||
const userPb = userPersonalBests ?? {};
|
||||
userPb[mode] ??= {};
|
||||
userPb[mode][mode2] ??= [];
|
||||
|
||||
const personalBestMatch = userPb[mode][mode2].find(
|
||||
(pb: MonkeyTypes.PersonalBest) => matchesPersonalBest(result, pb)
|
||||
(pb: SharedTypes.PersonalBest) => matchesPersonalBest(result, pb)
|
||||
);
|
||||
|
||||
let isPb = true;
|
||||
|
|
@ -66,7 +64,7 @@ export function checkAndUpdatePb(
|
|||
|
||||
function matchesPersonalBest(
|
||||
result: Result,
|
||||
personalBest: MonkeyTypes.PersonalBest
|
||||
personalBest: SharedTypes.PersonalBest
|
||||
): boolean {
|
||||
if (
|
||||
result.difficulty === undefined ||
|
||||
|
|
@ -88,7 +86,7 @@ function matchesPersonalBest(
|
|||
}
|
||||
|
||||
function updatePersonalBest(
|
||||
personalBest: MonkeyTypes.PersonalBest,
|
||||
personalBest: SharedTypes.PersonalBest,
|
||||
result: Result
|
||||
): boolean {
|
||||
if (personalBest.wpm >= result.wpm) {
|
||||
|
|
@ -121,7 +119,7 @@ function updatePersonalBest(
|
|||
return true;
|
||||
}
|
||||
|
||||
function buildPersonalBest(result: Result): MonkeyTypes.PersonalBest {
|
||||
function buildPersonalBest(result: Result): SharedTypes.PersonalBest {
|
||||
if (
|
||||
result.difficulty === undefined ||
|
||||
result.language === undefined ||
|
||||
|
|
@ -148,7 +146,7 @@ function buildPersonalBest(result: Result): MonkeyTypes.PersonalBest {
|
|||
}
|
||||
|
||||
function updateLeaderboardPersonalBests(
|
||||
userPersonalBests: MonkeyTypes.PersonalBests,
|
||||
userPersonalBests: SharedTypes.PersonalBests,
|
||||
lbPersonalBests: MonkeyTypes.LbPersonalBests,
|
||||
result: Result
|
||||
): void {
|
||||
|
|
@ -157,7 +155,7 @@ function updateLeaderboardPersonalBests(
|
|||
}
|
||||
|
||||
const mode = result.mode;
|
||||
const mode2 = result.mode2 as MonkeyTypes.Mode2<"time">;
|
||||
const mode2 = result.mode2 as SharedTypes.Mode2<"time">;
|
||||
|
||||
lbPersonalBests[mode] = lbPersonalBests[mode] ?? {};
|
||||
const lbMode2 = lbPersonalBests[mode][mode2];
|
||||
|
|
@ -167,7 +165,7 @@ function updateLeaderboardPersonalBests(
|
|||
|
||||
const bestForEveryLanguage = {};
|
||||
|
||||
userPersonalBests[mode][mode2].forEach((pb: MonkeyTypes.PersonalBest) => {
|
||||
userPersonalBests[mode][mode2].forEach((pb: SharedTypes.PersonalBest) => {
|
||||
const language = pb.language;
|
||||
if (
|
||||
!bestForEveryLanguage[language] ||
|
||||
|
|
@ -179,7 +177,7 @@ function updateLeaderboardPersonalBests(
|
|||
|
||||
_.each(
|
||||
bestForEveryLanguage,
|
||||
(pb: MonkeyTypes.PersonalBest, language: string) => {
|
||||
(pb: SharedTypes.PersonalBest, language: string) => {
|
||||
const languageDoesNotExist = !lbPersonalBests[mode][mode2][language];
|
||||
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export function setLeaderboard(
|
|||
}
|
||||
|
||||
export function incrementResult(
|
||||
res: MonkeyTypes.Result<MonkeyTypes.Mode>
|
||||
res: SharedTypes.Result<SharedTypes.Mode>
|
||||
): void {
|
||||
const {
|
||||
mode,
|
||||
|
|
|
|||
63
backend/src/utils/result.ts
Normal file
63
backend/src/utils/result.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { ObjectId } from "mongodb";
|
||||
|
||||
type Result = SharedTypes.DBResult<SharedTypes.Mode>;
|
||||
|
||||
export function buildDbResult(
|
||||
completedEvent: SharedTypes.CompletedEvent,
|
||||
userName: string,
|
||||
isPb: boolean
|
||||
): Result {
|
||||
const ce = completedEvent;
|
||||
const res: Result = {
|
||||
_id: new ObjectId(),
|
||||
uid: ce.uid,
|
||||
wpm: ce.wpm,
|
||||
rawWpm: ce.rawWpm,
|
||||
charStats: ce.charStats,
|
||||
acc: ce.acc,
|
||||
mode: ce.mode,
|
||||
mode2: ce.mode2,
|
||||
quoteLength: ce.quoteLength,
|
||||
timestamp: ce.timestamp,
|
||||
restartCount: ce.restartCount,
|
||||
incompleteTestSeconds: ce.incompleteTestSeconds,
|
||||
testDuration: ce.testDuration,
|
||||
afkDuration: ce.afkDuration,
|
||||
tags: ce.tags,
|
||||
consistency: ce.consistency,
|
||||
keyConsistency: ce.keyConsistency,
|
||||
chartData: ce.chartData,
|
||||
language: ce.language,
|
||||
lazyMode: ce.lazyMode,
|
||||
difficulty: ce.difficulty,
|
||||
funbox: ce.funbox,
|
||||
numbers: ce.numbers,
|
||||
punctuation: ce.punctuation,
|
||||
keySpacingStats: ce.keySpacingStats,
|
||||
keyDurationStats: ce.keyDurationStats,
|
||||
isPb: isPb,
|
||||
bailedOut: ce.bailedOut,
|
||||
blindMode: ce.blindMode,
|
||||
name: userName,
|
||||
};
|
||||
|
||||
if (ce.bailedOut === false) delete res.bailedOut;
|
||||
if (ce.blindMode === false) delete res.blindMode;
|
||||
if (ce.lazyMode === false) delete res.lazyMode;
|
||||
if (ce.difficulty === "normal") delete res.difficulty;
|
||||
if (ce.funbox === "none") delete res.funbox;
|
||||
if (ce.language === "english") delete res.language;
|
||||
if (ce.numbers === false) delete res.numbers;
|
||||
if (ce.punctuation === false) delete res.punctuation;
|
||||
if (ce.mode !== "custom") delete res.customText;
|
||||
if (ce.mode !== "quote") delete res.quoteLength;
|
||||
if (ce.restartCount === 0) delete res.restartCount;
|
||||
if (ce.incompleteTestSeconds === 0) delete res.incompleteTestSeconds;
|
||||
if (ce.afkDuration === 0) delete res.afkDuration;
|
||||
if (ce.tags.length === 0) delete res.tags;
|
||||
|
||||
if (ce.keySpacingStats === undefined) delete res.keySpacingStats;
|
||||
if (ce.keyDurationStats === undefined) delete res.keyDurationStats;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@ export function isTagPresetNameValid(name: string): boolean {
|
|||
return VALID_NAME_PATTERN.test(name);
|
||||
}
|
||||
|
||||
export function isTestTooShort(result: MonkeyTypes.CompletedEvent): boolean {
|
||||
export function isTestTooShort(result: SharedTypes.CompletedEvent): boolean {
|
||||
const { mode, mode2, customText, testDuration, bailedOut } = result;
|
||||
|
||||
if (mode === "time") {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ function hide(): void {
|
|||
$(".pageAccount .miniResultChartBg").stop(true, true).fadeOut(125);
|
||||
}
|
||||
|
||||
export function updateData(data: MonkeyTypes.ChartData): void {
|
||||
export function updateData(data: SharedTypes.ChartData): void {
|
||||
// let data = filteredResults[filteredId].chartData;
|
||||
let labels = [];
|
||||
for (let i = 1; i <= data.wpm.length; i++) {
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ function clearTables(isProfile: boolean): void {
|
|||
}
|
||||
|
||||
export function update(
|
||||
personalBests?: MonkeyTypes.PersonalBests,
|
||||
personalBests?: SharedTypes.PersonalBests,
|
||||
isProfile = false
|
||||
): void {
|
||||
clearTables(isProfile);
|
||||
|
|
@ -94,8 +94,8 @@ export function update(
|
|||
$(`.page${source} .profile .pbsTime`).html("");
|
||||
$(`.page${source} .profile .pbsWords`).html("");
|
||||
|
||||
const timeMode2s: MonkeyTypes.Mode2<"time">[] = ["15", "30", "60", "120"];
|
||||
const wordMode2s: MonkeyTypes.Mode2<"words">[] = ["10", "25", "50", "100"];
|
||||
const timeMode2s: SharedTypes.Mode2<"time">[] = ["15", "30", "60", "120"];
|
||||
const wordMode2s: SharedTypes.Mode2<"words">[] = ["10", "25", "50", "100"];
|
||||
|
||||
timeMode2s.forEach((mode2) => {
|
||||
text += buildPbHtml(personalBests, "time", mode2);
|
||||
|
|
@ -122,9 +122,9 @@ export function update(
|
|||
}
|
||||
|
||||
function buildPbHtml(
|
||||
pbs: MonkeyTypes.PersonalBests,
|
||||
pbs: SharedTypes.PersonalBests,
|
||||
mode: "time" | "words",
|
||||
mode2: MonkeyTypes.StringNumber
|
||||
mode2: SharedTypes.StringNumber
|
||||
): string {
|
||||
let retval = "";
|
||||
let dateText = "";
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import Ape from "../ape/index";
|
|||
import * as Loader from "../elements/loader";
|
||||
import { showNewResultFilterPresetPopup } from "../popups/new-result-filter-preset-popup";
|
||||
|
||||
export const defaultResultFilters: MonkeyTypes.ResultFilters = {
|
||||
export const defaultResultFilters: SharedTypes.ResultFilters = {
|
||||
_id: "default-result-filters-id",
|
||||
name: "default result filters",
|
||||
pb: {
|
||||
|
|
@ -198,12 +198,12 @@ export async function setFilterPreset(id: string): Promise<void> {
|
|||
}
|
||||
|
||||
function deepCopyFilter(
|
||||
filter: MonkeyTypes.ResultFilters
|
||||
): MonkeyTypes.ResultFilters {
|
||||
filter: SharedTypes.ResultFilters
|
||||
): SharedTypes.ResultFilters {
|
||||
return JSON.parse(JSON.stringify(filter));
|
||||
}
|
||||
|
||||
function addFilterPresetToSnapshot(filter: MonkeyTypes.ResultFilters): void {
|
||||
function addFilterPresetToSnapshot(filter: SharedTypes.ResultFilters): void {
|
||||
const snapshot = DB.getSnapshot();
|
||||
if (!snapshot) return;
|
||||
DB.setSnapshot({
|
||||
|
|
@ -270,13 +270,13 @@ function deSelectFilterPreset(): void {
|
|||
).removeClass("active");
|
||||
}
|
||||
|
||||
function getFilters(): MonkeyTypes.ResultFilters {
|
||||
function getFilters(): SharedTypes.ResultFilters {
|
||||
return filters;
|
||||
}
|
||||
|
||||
function getGroup<G extends keyof MonkeyTypes.ResultFilters>(
|
||||
function getGroup<G extends keyof SharedTypes.ResultFilters>(
|
||||
group: G
|
||||
): MonkeyTypes.ResultFilters[G] {
|
||||
): SharedTypes.ResultFilters[G] {
|
||||
return filters[group];
|
||||
}
|
||||
|
||||
|
|
@ -284,15 +284,15 @@ function getGroup<G extends keyof MonkeyTypes.ResultFilters>(
|
|||
// filters[group][filter] = value;
|
||||
// }
|
||||
|
||||
export function getFilter<G extends keyof MonkeyTypes.ResultFilters>(
|
||||
export function getFilter<G extends keyof SharedTypes.ResultFilters>(
|
||||
group: G,
|
||||
filter: MonkeyTypes.Filter<G>
|
||||
): MonkeyTypes.ResultFilters[G][MonkeyTypes.Filter<G>] {
|
||||
): SharedTypes.ResultFilters[G][MonkeyTypes.Filter<G>] {
|
||||
return filters[group][filter];
|
||||
}
|
||||
|
||||
function setAllFilters(
|
||||
group: keyof MonkeyTypes.ResultFilters,
|
||||
group: keyof SharedTypes.ResultFilters,
|
||||
value: boolean
|
||||
): void {
|
||||
Object.keys(getGroup(group)).forEach((filter) => {
|
||||
|
|
@ -313,7 +313,7 @@ export function reset(): void {
|
|||
}
|
||||
|
||||
type AboveChartDisplay = Partial<
|
||||
Record<keyof MonkeyTypes.ResultFilters, { all: boolean; array?: string[] }>
|
||||
Record<keyof SharedTypes.ResultFilters, { all: boolean; array?: string[] }>
|
||||
>;
|
||||
|
||||
export function updateActive(): void {
|
||||
|
|
@ -359,7 +359,7 @@ export function updateActive(): void {
|
|||
});
|
||||
});
|
||||
|
||||
function addText(group: keyof MonkeyTypes.ResultFilters): string {
|
||||
function addText(group: keyof SharedTypes.ResultFilters): string {
|
||||
let ret = "";
|
||||
ret += "<div class='group'>";
|
||||
if (group === "difficulty") {
|
||||
|
|
@ -457,7 +457,7 @@ export function updateActive(): void {
|
|||
}, 0);
|
||||
}
|
||||
|
||||
function toggle<G extends keyof MonkeyTypes.ResultFilters>(
|
||||
function toggle<G extends keyof SharedTypes.ResultFilters>(
|
||||
group: G,
|
||||
filter: MonkeyTypes.Filter<G>
|
||||
): void {
|
||||
|
|
@ -470,7 +470,7 @@ function toggle<G extends keyof MonkeyTypes.ResultFilters>(
|
|||
}
|
||||
const newValue = !filters[group][
|
||||
filter
|
||||
] as unknown as MonkeyTypes.ResultFilters[G][MonkeyTypes.Filter<G>];
|
||||
] as unknown as SharedTypes.ResultFilters[G][MonkeyTypes.Filter<G>];
|
||||
filters[group][filter] = newValue;
|
||||
save();
|
||||
} catch (e) {
|
||||
|
|
@ -490,7 +490,7 @@ $(
|
|||
).on("click", "button", (e) => {
|
||||
const group = $(e.target)
|
||||
.parents(".buttons")
|
||||
.attr("group") as keyof MonkeyTypes.ResultFilters;
|
||||
.attr("group") as keyof SharedTypes.ResultFilters;
|
||||
const filter = $(e.target).attr("filter") as MonkeyTypes.Filter<typeof group>;
|
||||
if ($(e.target).hasClass("allFilters")) {
|
||||
Misc.typedKeys(getFilters()).forEach((group) => {
|
||||
|
|
@ -737,11 +737,11 @@ $(".group.presetFilterButtons .filterBtns").on(
|
|||
);
|
||||
|
||||
function verifyResultFiltersStructure(
|
||||
filterIn: MonkeyTypes.ResultFilters
|
||||
): MonkeyTypes.ResultFilters {
|
||||
filterIn: SharedTypes.ResultFilters
|
||||
): SharedTypes.ResultFilters {
|
||||
const filter = deepCopyFilter(filterIn);
|
||||
Object.entries(defaultResultFilters).forEach((entry) => {
|
||||
const key = entry[0] as keyof MonkeyTypes.ResultFilters;
|
||||
const key = entry[0] as keyof SharedTypes.ResultFilters;
|
||||
const value = entry[1];
|
||||
if (filter[key] === undefined) {
|
||||
filter[key] = value;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ const BASE_PATH = "/leaderboards";
|
|||
|
||||
interface LeaderboardQuery {
|
||||
language: string;
|
||||
mode: MonkeyTypes.Mode;
|
||||
mode: SharedTypes.Mode;
|
||||
mode2: string;
|
||||
isDaily?: boolean;
|
||||
daysBefore?: number;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export default class Results {
|
|||
}
|
||||
|
||||
async save(
|
||||
result: MonkeyTypes.Result<MonkeyTypes.Mode>
|
||||
result: SharedTypes.Result<SharedTypes.Mode>
|
||||
): Ape.EndpointResponse {
|
||||
return await this.httpClient.post(BASE_PATH, {
|
||||
payload: { result },
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@ export default class Users {
|
|||
});
|
||||
}
|
||||
|
||||
async updateLeaderboardMemory<M extends MonkeyTypes.Mode>(
|
||||
async updateLeaderboardMemory<M extends SharedTypes.Mode>(
|
||||
mode: string,
|
||||
mode2: MonkeyTypes.Mode2<M>,
|
||||
mode2: SharedTypes.Mode2<M>,
|
||||
language: string,
|
||||
rank: number
|
||||
): Ape.EndpointResponse {
|
||||
|
|
@ -82,7 +82,7 @@ export default class Users {
|
|||
}
|
||||
|
||||
async addResultFilterPreset(
|
||||
filter: MonkeyTypes.ResultFilters
|
||||
filter: SharedTypes.ResultFilters
|
||||
): Ape.EndpointResponse {
|
||||
return await this.httpClient.post(`${BASE_PATH}/resultFilterPresets`, {
|
||||
payload: filter,
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export function setPunctuation(punc: boolean, nosave?: boolean): boolean {
|
|||
return true;
|
||||
}
|
||||
|
||||
export function setMode(mode: MonkeyTypes.Mode, nosave?: boolean): boolean {
|
||||
export function setMode(mode: SharedTypes.Mode, nosave?: boolean): boolean {
|
||||
if (
|
||||
!isConfigValueValid("mode", mode, [
|
||||
["time", "words", "quote", "zen", "custom"],
|
||||
|
|
@ -205,7 +205,7 @@ export function setSoundVolume(
|
|||
|
||||
//difficulty
|
||||
export function setDifficulty(
|
||||
diff: MonkeyTypes.Difficulty,
|
||||
diff: SharedTypes.Difficulty,
|
||||
nosave?: boolean
|
||||
): boolean {
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export function clearActive(): void {
|
|||
}
|
||||
|
||||
export function verify(
|
||||
result: MonkeyTypes.Result<MonkeyTypes.Mode>
|
||||
result: SharedTypes.Result<SharedTypes.Mode>
|
||||
): string | null {
|
||||
try {
|
||||
if (TestState.activeChallenge) {
|
||||
|
|
@ -295,10 +295,10 @@ export async function setup(challengeName: string): Promise<boolean> {
|
|||
} else if (challenge.parameters[1] === "time") {
|
||||
UpdateConfig.setTimeConfig(challenge.parameters[2] as number, true);
|
||||
}
|
||||
UpdateConfig.setMode(challenge.parameters[1] as MonkeyTypes.Mode, true);
|
||||
UpdateConfig.setMode(challenge.parameters[1] as SharedTypes.Mode, true);
|
||||
if (challenge.parameters[3] !== undefined) {
|
||||
UpdateConfig.setDifficulty(
|
||||
challenge.parameters[3] as MonkeyTypes.Difficulty,
|
||||
challenge.parameters[3] as SharedTypes.Difficulty,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export async function initSnapshot(): Promise<
|
|||
};
|
||||
|
||||
for (const mode of ["time", "words", "quote", "zen", "custom"]) {
|
||||
snap.personalBests[mode as keyof MonkeyTypes.PersonalBests] ??= {};
|
||||
snap.personalBests[mode as keyof SharedTypes.PersonalBests] ??= {};
|
||||
}
|
||||
|
||||
snap.banned = userData.banned;
|
||||
|
|
@ -162,9 +162,10 @@ export async function initSnapshot(): Promise<
|
|||
// }
|
||||
// LoadingPage.updateText("Downloading tags...");
|
||||
snap.customThemes = userData.customThemes ?? [];
|
||||
snap.tags = userData.tags || [];
|
||||
|
||||
snap.tags.forEach((tag) => {
|
||||
const userDataTags: MonkeyTypes.Tag[] = userData.tags ?? [];
|
||||
|
||||
userDataTags.forEach((tag) => {
|
||||
tag.display = tag.name.replaceAll("_", " ");
|
||||
tag.personalBests ??= {
|
||||
time: {},
|
||||
|
|
@ -175,10 +176,12 @@ export async function initSnapshot(): Promise<
|
|||
};
|
||||
|
||||
for (const mode of ["time", "words", "quote", "zen", "custom"]) {
|
||||
tag.personalBests[mode as keyof MonkeyTypes.PersonalBests] ??= {};
|
||||
tag.personalBests[mode as keyof SharedTypes.PersonalBests] ??= {};
|
||||
}
|
||||
});
|
||||
|
||||
snap.tags = userDataTags;
|
||||
|
||||
snap.tags = snap.tags?.sort((a, b) => {
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
|
|
@ -188,6 +191,7 @@ export async function initSnapshot(): Promise<
|
|||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
// if (ActivePage.get() === "loading") {
|
||||
// LoadingPage.updateBar(90);
|
||||
// } else {
|
||||
|
|
@ -245,7 +249,7 @@ export async function getUserResults(offset?: number): Promise<boolean> {
|
|||
return false;
|
||||
}
|
||||
|
||||
const results = response.data as MonkeyTypes.Result<MonkeyTypes.Mode>[];
|
||||
const results = response.data as SharedTypes.DBResult<SharedTypes.Mode>[];
|
||||
results?.sort((a, b) => b.timestamp - a.timestamp);
|
||||
results.forEach((result) => {
|
||||
if (result.bailedOut === undefined) result.bailedOut = false;
|
||||
|
|
@ -265,6 +269,13 @@ export async function getUserResults(offset?: number): Promise<boolean> {
|
|||
}
|
||||
if (result.afkDuration === undefined) result.afkDuration = 0;
|
||||
if (result.tags === undefined) result.tags = [];
|
||||
|
||||
if (
|
||||
result.correctChars !== undefined &&
|
||||
result.incorrectChars !== undefined
|
||||
) {
|
||||
result.charStats = [result.correctChars, result.incorrectChars, 0, 0];
|
||||
}
|
||||
});
|
||||
|
||||
if (dbSnapshot.results !== undefined && dbSnapshot.results.length > 0) {
|
||||
|
|
@ -274,9 +285,12 @@ export async function getUserResults(offset?: number): Promise<boolean> {
|
|||
const resultsWithoutDuplicates = results.filter(
|
||||
(it) => it.timestamp < oldestTimestamp
|
||||
);
|
||||
dbSnapshot.results.push(...resultsWithoutDuplicates);
|
||||
dbSnapshot.results.push(
|
||||
...(resultsWithoutDuplicates as unknown as SharedTypes.Result<SharedTypes.Mode>[])
|
||||
);
|
||||
} else {
|
||||
dbSnapshot.results = results;
|
||||
dbSnapshot.results =
|
||||
results as unknown as SharedTypes.Result<SharedTypes.Mode>[];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -367,12 +381,12 @@ export async function deleteCustomTheme(themeId: string): Promise<boolean> {
|
|||
return true;
|
||||
}
|
||||
|
||||
async function _getUserHighestWpm<M extends MonkeyTypes.Mode>(
|
||||
async function _getUserHighestWpm<M extends SharedTypes.Mode>(
|
||||
mode: M,
|
||||
mode2: MonkeyTypes.Mode2<M>,
|
||||
mode2: SharedTypes.Mode2<M>,
|
||||
punctuation: boolean,
|
||||
language: string,
|
||||
difficulty: MonkeyTypes.Difficulty,
|
||||
difficulty: SharedTypes.Difficulty,
|
||||
lazyMode: boolean
|
||||
): Promise<number> {
|
||||
function cont(): number {
|
||||
|
|
@ -401,12 +415,12 @@ async function _getUserHighestWpm<M extends MonkeyTypes.Mode>(
|
|||
return retval;
|
||||
}
|
||||
|
||||
export async function getUserAverage10<M extends MonkeyTypes.Mode>(
|
||||
export async function getUserAverage10<M extends SharedTypes.Mode>(
|
||||
mode: M,
|
||||
mode2: MonkeyTypes.Mode2<M>,
|
||||
mode2: SharedTypes.Mode2<M>,
|
||||
punctuation: boolean,
|
||||
language: string,
|
||||
difficulty: MonkeyTypes.Difficulty,
|
||||
difficulty: SharedTypes.Difficulty,
|
||||
lazyMode: boolean
|
||||
): Promise<[number, number]> {
|
||||
const snapshot = getSnapshot();
|
||||
|
|
@ -484,12 +498,12 @@ export async function getUserAverage10<M extends MonkeyTypes.Mode>(
|
|||
return retval;
|
||||
}
|
||||
|
||||
export async function getUserDailyBest<M extends MonkeyTypes.Mode>(
|
||||
export async function getUserDailyBest<M extends SharedTypes.Mode>(
|
||||
mode: M,
|
||||
mode2: MonkeyTypes.Mode2<M>,
|
||||
mode2: SharedTypes.Mode2<M>,
|
||||
punctuation: boolean,
|
||||
language: string,
|
||||
difficulty: MonkeyTypes.Difficulty,
|
||||
difficulty: SharedTypes.Difficulty,
|
||||
lazyMode: boolean
|
||||
): Promise<number> {
|
||||
const snapshot = getSnapshot();
|
||||
|
|
@ -547,12 +561,12 @@ export async function getUserDailyBest<M extends MonkeyTypes.Mode>(
|
|||
return retval;
|
||||
}
|
||||
|
||||
export async function getLocalPB<M extends MonkeyTypes.Mode>(
|
||||
export async function getLocalPB<M extends SharedTypes.Mode>(
|
||||
mode: M,
|
||||
mode2: MonkeyTypes.Mode2<M>,
|
||||
mode2: SharedTypes.Mode2<M>,
|
||||
punctuation: boolean,
|
||||
language: string,
|
||||
difficulty: MonkeyTypes.Difficulty,
|
||||
difficulty: SharedTypes.Difficulty,
|
||||
lazyMode: boolean,
|
||||
funbox: string
|
||||
): Promise<number> {
|
||||
|
|
@ -572,7 +586,7 @@ export async function getLocalPB<M extends MonkeyTypes.Mode>(
|
|||
(
|
||||
dbSnapshot.personalBests[mode][
|
||||
mode2
|
||||
] as unknown as MonkeyTypes.PersonalBest[]
|
||||
] as unknown as SharedTypes.PersonalBest[]
|
||||
).forEach((pb) => {
|
||||
if (
|
||||
pb.punctuation === punctuation &&
|
||||
|
|
@ -596,12 +610,12 @@ export async function getLocalPB<M extends MonkeyTypes.Mode>(
|
|||
return retval;
|
||||
}
|
||||
|
||||
export async function saveLocalPB<M extends MonkeyTypes.Mode>(
|
||||
export async function saveLocalPB<M extends SharedTypes.Mode>(
|
||||
mode: M,
|
||||
mode2: MonkeyTypes.Mode2<M>,
|
||||
mode2: SharedTypes.Mode2<M>,
|
||||
punctuation: boolean,
|
||||
language: string,
|
||||
difficulty: MonkeyTypes.Difficulty,
|
||||
difficulty: SharedTypes.Difficulty,
|
||||
lazyMode: boolean,
|
||||
wpm: number,
|
||||
acc: number,
|
||||
|
|
@ -627,12 +641,12 @@ export async function saveLocalPB<M extends MonkeyTypes.Mode>(
|
|||
};
|
||||
|
||||
dbSnapshot.personalBests[mode][mode2] ??=
|
||||
[] as unknown as MonkeyTypes.PersonalBests[M][MonkeyTypes.Mode2<M>];
|
||||
[] as unknown as SharedTypes.PersonalBests[M][SharedTypes.Mode2<M>];
|
||||
|
||||
(
|
||||
dbSnapshot.personalBests[mode][
|
||||
mode2
|
||||
] as unknown as MonkeyTypes.PersonalBest[]
|
||||
] as unknown as SharedTypes.PersonalBest[]
|
||||
).forEach((pb) => {
|
||||
if (
|
||||
pb.punctuation === punctuation &&
|
||||
|
|
@ -655,7 +669,7 @@ export async function saveLocalPB<M extends MonkeyTypes.Mode>(
|
|||
(
|
||||
dbSnapshot.personalBests[mode][
|
||||
mode2
|
||||
] as unknown as MonkeyTypes.PersonalBest[]
|
||||
] as unknown as SharedTypes.PersonalBest[]
|
||||
).push({
|
||||
language,
|
||||
difficulty,
|
||||
|
|
@ -675,13 +689,13 @@ export async function saveLocalPB<M extends MonkeyTypes.Mode>(
|
|||
}
|
||||
}
|
||||
|
||||
export async function getLocalTagPB<M extends MonkeyTypes.Mode>(
|
||||
export async function getLocalTagPB<M extends SharedTypes.Mode>(
|
||||
tagId: string,
|
||||
mode: M,
|
||||
mode2: MonkeyTypes.Mode2<M>,
|
||||
mode2: SharedTypes.Mode2<M>,
|
||||
punctuation: boolean,
|
||||
language: string,
|
||||
difficulty: MonkeyTypes.Difficulty,
|
||||
difficulty: SharedTypes.Difficulty,
|
||||
lazyMode: boolean
|
||||
): Promise<number> {
|
||||
function cont(): number {
|
||||
|
|
@ -706,10 +720,10 @@ export async function getLocalTagPB<M extends MonkeyTypes.Mode>(
|
|||
};
|
||||
|
||||
filteredtag.personalBests[mode][mode2] ??=
|
||||
[] as unknown as MonkeyTypes.PersonalBests[M][MonkeyTypes.Mode2<M>];
|
||||
[] as unknown as SharedTypes.PersonalBests[M][SharedTypes.Mode2<M>];
|
||||
|
||||
const personalBests = (filteredtag.personalBests[mode][mode2] ??
|
||||
[]) as MonkeyTypes.PersonalBest[];
|
||||
[]) as SharedTypes.PersonalBest[];
|
||||
|
||||
ret =
|
||||
personalBests.find(
|
||||
|
|
@ -729,13 +743,13 @@ export async function getLocalTagPB<M extends MonkeyTypes.Mode>(
|
|||
return retval;
|
||||
}
|
||||
|
||||
export async function saveLocalTagPB<M extends MonkeyTypes.Mode>(
|
||||
export async function saveLocalTagPB<M extends SharedTypes.Mode>(
|
||||
tagId: string,
|
||||
mode: M,
|
||||
mode2: MonkeyTypes.Mode2<M>,
|
||||
mode2: SharedTypes.Mode2<M>,
|
||||
punctuation: boolean,
|
||||
language: string,
|
||||
difficulty: MonkeyTypes.Difficulty,
|
||||
difficulty: SharedTypes.Difficulty,
|
||||
lazyMode: boolean,
|
||||
wpm: number,
|
||||
acc: number,
|
||||
|
|
@ -762,7 +776,7 @@ export async function saveLocalTagPB<M extends MonkeyTypes.Mode>(
|
|||
};
|
||||
|
||||
filteredtag.personalBests[mode][mode2] ??=
|
||||
[] as unknown as MonkeyTypes.PersonalBests[M][MonkeyTypes.Mode2<M>];
|
||||
[] as unknown as SharedTypes.PersonalBests[M][SharedTypes.Mode2<M>];
|
||||
|
||||
try {
|
||||
let found = false;
|
||||
|
|
@ -770,7 +784,7 @@ export async function saveLocalTagPB<M extends MonkeyTypes.Mode>(
|
|||
(
|
||||
filteredtag.personalBests[mode][
|
||||
mode2
|
||||
] as unknown as MonkeyTypes.PersonalBest[]
|
||||
] as unknown as SharedTypes.PersonalBest[]
|
||||
).forEach((pb) => {
|
||||
if (
|
||||
pb.punctuation === punctuation &&
|
||||
|
|
@ -793,7 +807,7 @@ export async function saveLocalTagPB<M extends MonkeyTypes.Mode>(
|
|||
(
|
||||
filteredtag.personalBests[mode][
|
||||
mode2
|
||||
] as unknown as MonkeyTypes.PersonalBest[]
|
||||
] as unknown as SharedTypes.PersonalBest[]
|
||||
).push({
|
||||
language,
|
||||
difficulty,
|
||||
|
|
@ -827,7 +841,7 @@ export async function saveLocalTagPB<M extends MonkeyTypes.Mode>(
|
|||
timestamp: Date.now(),
|
||||
consistency: consistency,
|
||||
},
|
||||
] as unknown as MonkeyTypes.PersonalBests[M][MonkeyTypes.Mode2<M>];
|
||||
] as unknown as SharedTypes.PersonalBests[M][SharedTypes.Mode2<M>];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -838,9 +852,9 @@ export async function saveLocalTagPB<M extends MonkeyTypes.Mode>(
|
|||
return;
|
||||
}
|
||||
|
||||
export async function updateLbMemory<M extends MonkeyTypes.Mode>(
|
||||
export async function updateLbMemory<M extends SharedTypes.Mode>(
|
||||
mode: M,
|
||||
mode2: MonkeyTypes.Mode2<M>,
|
||||
mode2: SharedTypes.Mode2<M>,
|
||||
language: string,
|
||||
rank: number,
|
||||
api = false
|
||||
|
|
@ -884,7 +898,7 @@ export async function saveConfig(config: MonkeyTypes.Config): Promise<void> {
|
|||
}
|
||||
|
||||
export function saveLocalResult(
|
||||
result: MonkeyTypes.Result<MonkeyTypes.Mode>
|
||||
result: SharedTypes.Result<SharedTypes.Mode>
|
||||
): void {
|
||||
const snapshot = getSnapshot();
|
||||
if (!snapshot) return;
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export function toggleFilterDebug(): void {
|
|||
}
|
||||
}
|
||||
|
||||
let filteredResults: MonkeyTypes.Result<MonkeyTypes.Mode>[] = [];
|
||||
let filteredResults: SharedTypes.Result<SharedTypes.Mode>[] = [];
|
||||
let visibleTableLines = 0;
|
||||
|
||||
function loadMoreLines(lineIndex?: number): void {
|
||||
|
|
@ -152,12 +152,7 @@ function loadMoreLines(lineIndex?: number): void {
|
|||
pb = "";
|
||||
}
|
||||
|
||||
let charStats = "-";
|
||||
if (result.charStats) {
|
||||
charStats = result.charStats.join("/");
|
||||
} else {
|
||||
charStats = result.correctChars + "/" + result.incorrectChars + "/-/-";
|
||||
}
|
||||
const charStats = result.charStats.join("/");
|
||||
|
||||
const date = new Date(result.timestamp);
|
||||
$(".pageAccount .history table tbody").append(`
|
||||
|
|
@ -279,7 +274,7 @@ async function fillContent(): Promise<void> {
|
|||
$(".pageAccount .history table tbody").empty();
|
||||
|
||||
DB.getSnapshot()?.results?.forEach(
|
||||
(result: MonkeyTypes.Result<MonkeyTypes.Mode>) => {
|
||||
(result: SharedTypes.Result<SharedTypes.Mode>) => {
|
||||
// totalSeconds += tt;
|
||||
|
||||
//apply filters
|
||||
|
|
@ -311,7 +306,7 @@ async function fillContent(): Promise<void> {
|
|||
}
|
||||
|
||||
if (result.mode === "time") {
|
||||
let timefilter: MonkeyTypes.Mode2<"time"> | "custom" = "custom";
|
||||
let timefilter: SharedTypes.Mode2<"time"> | "custom" = "custom";
|
||||
if (
|
||||
["15", "30", "60", "120"].includes(
|
||||
`${result.mode2}` //legacy results could have a number in mode2
|
||||
|
|
@ -331,7 +326,7 @@ async function fillContent(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
} else if (result.mode === "words") {
|
||||
let wordfilter: MonkeyTypes.Mode2Custom<"words"> = "custom";
|
||||
let wordfilter: SharedTypes.Mode2Custom<"words"> = "custom";
|
||||
if (
|
||||
["10", "25", "50", "100", "200"].includes(
|
||||
`${result.mode2}` //legacy results could have a number in mode2
|
||||
|
|
@ -1156,8 +1151,8 @@ function sortAndRefreshHistory(
|
|||
temp.push(filteredResults[idx]);
|
||||
parsedIndexes.push(idx);
|
||||
}
|
||||
filteredResults = temp as MonkeyTypes.Result<
|
||||
keyof MonkeyTypes.PersonalBests
|
||||
filteredResults = temp as SharedTypes.Result<
|
||||
keyof SharedTypes.PersonalBests
|
||||
>[];
|
||||
|
||||
$(".pageAccount .history table tbody").empty();
|
||||
|
|
@ -1207,7 +1202,7 @@ $(".pageAccount").on("click", ".miniResultChartButton", (event) => {
|
|||
const filteredId = $(event.currentTarget).attr("filteredResultsId");
|
||||
if (filteredId === undefined) return;
|
||||
MiniResultChart.updateData(
|
||||
filteredResults[parseInt(filteredId)]?.chartData as MonkeyTypes.ChartData
|
||||
filteredResults[parseInt(filteredId)]?.chartData as SharedTypes.ChartData
|
||||
);
|
||||
MiniResultChart.show();
|
||||
MiniResultChart.updatePosition(
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ el.find(".numbers").on("click", () => {
|
|||
el.find(".modeGroup button").on("click", (e) => {
|
||||
if ($(e.currentTarget).hasClass("active")) return;
|
||||
const mode = $(e.currentTarget).attr("data-mode");
|
||||
UpdateConfig.setMode(mode as MonkeyTypes.Mode);
|
||||
UpdateConfig.setMode(mode as SharedTypes.Mode);
|
||||
ManualRestart.set();
|
||||
TestLogic.restart();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import format from "date-fns/format";
|
|||
import * as Skeleton from "./skeleton";
|
||||
import { isPopupVisible } from "../utils/misc";
|
||||
|
||||
interface PersonalBest extends MonkeyTypes.PersonalBest {
|
||||
mode2: MonkeyTypes.Mode2<MonkeyTypes.Mode>;
|
||||
interface PersonalBest extends SharedTypes.PersonalBest {
|
||||
mode2: SharedTypes.Mode2<SharedTypes.Mode>;
|
||||
}
|
||||
|
||||
const wrapperId = "pbTablesPopupWrapper";
|
||||
|
||||
function update(mode: MonkeyTypes.Mode): void {
|
||||
function update(mode: SharedTypes.Mode): void {
|
||||
$("#pbTablesPopup table tbody").empty();
|
||||
$($("#pbTablesPopup table thead tr td")[0] as HTMLElement).text(mode);
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ function update(mode: MonkeyTypes.Mode): void {
|
|||
if (allmode2 === undefined) return;
|
||||
|
||||
const list: PersonalBest[] = [];
|
||||
(Object.keys(allmode2) as MonkeyTypes.Mode2<MonkeyTypes.Mode>[]).forEach(
|
||||
(Object.keys(allmode2) as SharedTypes.Mode2<SharedTypes.Mode>[]).forEach(
|
||||
function (key) {
|
||||
let pbs = allmode2[key] ?? [];
|
||||
pbs = pbs.sort(function (a, b) {
|
||||
|
|
@ -40,7 +40,7 @@ function update(mode: MonkeyTypes.Mode): void {
|
|||
}
|
||||
);
|
||||
|
||||
let mode2memory: MonkeyTypes.Mode2<MonkeyTypes.Mode>;
|
||||
let mode2memory: SharedTypes.Mode2<SharedTypes.Mode>;
|
||||
|
||||
list.forEach((pb) => {
|
||||
let dateText = `-<br><span class="sub">-</span>`;
|
||||
|
|
@ -78,7 +78,7 @@ function update(mode: MonkeyTypes.Mode): void {
|
|||
});
|
||||
}
|
||||
|
||||
function show(mode: MonkeyTypes.Mode): void {
|
||||
function show(mode: SharedTypes.Mode): void {
|
||||
Skeleton.append(wrapperId);
|
||||
if (!isPopupVisible(wrapperId)) {
|
||||
update(mode);
|
||||
|
|
|
|||
|
|
@ -245,6 +245,10 @@ $("#quoteRatePopupWrapper .submitButton").on("click", () => {
|
|||
});
|
||||
|
||||
$(".pageTest #rateQuoteButton").on("click", async () => {
|
||||
if (TestWords.randomQuote === null) {
|
||||
Notifications.add("Failed to show quote rating popup: no quote", -1);
|
||||
return;
|
||||
}
|
||||
show(TestWords.randomQuote);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -168,6 +168,10 @@ $("#quoteReportPopupWrapper .submit").on("click", async () => {
|
|||
});
|
||||
|
||||
$(".pageTest #reportQuoteButton").on("click", async () => {
|
||||
if (TestWords.randomQuote === null) {
|
||||
Notifications.add("Failed to show quote report popup: no quote", -1);
|
||||
return;
|
||||
}
|
||||
show({
|
||||
quoteId: TestWords.randomQuote?.id,
|
||||
noAnim: false,
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ $("#resultEditTagsPanelWrapper .confirmButton").on("click", async () => {
|
|||
duration: 2,
|
||||
});
|
||||
DB.getSnapshot()?.results?.forEach(
|
||||
(result: MonkeyTypes.Result<MonkeyTypes.Mode>) => {
|
||||
(result: SharedTypes.Result<SharedTypes.Mode>) => {
|
||||
if (result._id === resultId) {
|
||||
result.tags = newTags;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ function getCheckboxValue(checkbox: string): boolean {
|
|||
}
|
||||
|
||||
type SharedTestSettings = [
|
||||
MonkeyTypes.Mode | null,
|
||||
MonkeyTypes.Mode2<MonkeyTypes.Mode> | null,
|
||||
MonkeyTypes.CustomText | null,
|
||||
SharedTypes.Mode | null,
|
||||
SharedTypes.Mode2<SharedTypes.Mode> | null,
|
||||
SharedTypes.CustomText | null,
|
||||
boolean | null,
|
||||
boolean | null,
|
||||
string | null,
|
||||
MonkeyTypes.Difficulty | null,
|
||||
SharedTypes.Difficulty | null,
|
||||
string | null
|
||||
];
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ function updateURL(): void {
|
|||
settings[1] = getMode2(
|
||||
Config,
|
||||
randomQuote
|
||||
) as MonkeyTypes.Mode2<MonkeyTypes.Mode>;
|
||||
) as SharedTypes.Mode2<SharedTypes.Mode>;
|
||||
}
|
||||
|
||||
if (getCheckboxValue("customText")) {
|
||||
|
|
|
|||
|
|
@ -639,7 +639,7 @@ export async function activate(funbox?: string): Promise<boolean | undefined> {
|
|||
if (check.result === false) {
|
||||
if (check.forcedConfigs && check.forcedConfigs.length > 0) {
|
||||
if (configKey === "mode") {
|
||||
UpdateConfig.setMode(check.forcedConfigs[0] as MonkeyTypes.Mode);
|
||||
UpdateConfig.setMode(check.forcedConfigs[0] as SharedTypes.Mode);
|
||||
}
|
||||
if (configKey === "words") {
|
||||
UpdateConfig.setWordCount(check.forcedConfigs[0] as number);
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export async function init(): Promise<void> {
|
|||
const mode2 = Misc.getMode2(
|
||||
Config,
|
||||
TestWords.randomQuote
|
||||
) as MonkeyTypes.Mode2<typeof Config.mode>;
|
||||
) as SharedTypes.Mode2<typeof Config.mode>;
|
||||
let wpm;
|
||||
if (Config.paceCaret === "pb") {
|
||||
wpm = await DB.getLocalPB(
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ interface BeforeCustomText {
|
|||
}
|
||||
|
||||
interface Before {
|
||||
mode: MonkeyTypes.Mode | null;
|
||||
mode: SharedTypes.Mode | null;
|
||||
punctuation: boolean | null;
|
||||
numbers: boolean | null;
|
||||
customText: BeforeCustomText | null;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import confetti from "canvas-confetti";
|
|||
import type { AnnotationOptions } from "chartjs-plugin-annotation";
|
||||
import Ape from "../ape";
|
||||
|
||||
let result: MonkeyTypes.Result<MonkeyTypes.Mode>;
|
||||
let result: SharedTypes.Result<SharedTypes.Mode>;
|
||||
let maxChartVal: number;
|
||||
|
||||
let useUnsmoothedRaw = false;
|
||||
|
|
@ -546,7 +546,7 @@ async function updateTags(dontSave: boolean): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
function updateTestType(randomQuote: MonkeyTypes.Quote): void {
|
||||
function updateTestType(randomQuote: MonkeyTypes.Quote | null): void {
|
||||
let testType = "";
|
||||
|
||||
testType += Config.mode;
|
||||
|
|
@ -556,7 +556,7 @@ function updateTestType(randomQuote: MonkeyTypes.Quote): void {
|
|||
} else if (Config.mode === "words") {
|
||||
testType += " " + Config.words;
|
||||
} else if (Config.mode === "quote") {
|
||||
if (randomQuote.group !== undefined) {
|
||||
if (randomQuote?.group !== undefined) {
|
||||
testType += " " + ["short", "medium", "long", "thicc"][randomQuote.group];
|
||||
}
|
||||
}
|
||||
|
|
@ -653,8 +653,15 @@ function updateOther(
|
|||
}
|
||||
}
|
||||
|
||||
export function updateRateQuote(randomQuote: MonkeyTypes.Quote): void {
|
||||
export function updateRateQuote(randomQuote: MonkeyTypes.Quote | null): void {
|
||||
if (Config.mode === "quote") {
|
||||
if (randomQuote === null) {
|
||||
console.error(
|
||||
"Failed to update quote rating button: randomQuote is null"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const userqr =
|
||||
DB.getSnapshot()?.quoteRatings?.[randomQuote.language]?.[randomQuote.id];
|
||||
if (userqr) {
|
||||
|
|
@ -674,39 +681,48 @@ export function updateRateQuote(randomQuote: MonkeyTypes.Quote): void {
|
|||
}
|
||||
}
|
||||
|
||||
function updateQuoteFavorite(randomQuote: MonkeyTypes.Quote): void {
|
||||
function updateQuoteFavorite(randomQuote: MonkeyTypes.Quote | null): void {
|
||||
const icon = $(".pageTest #result #favoriteQuoteButton .icon");
|
||||
|
||||
if (Config.mode !== "quote" || Auth?.currentUser === null) {
|
||||
icon.parent().addClass("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
if (randomQuote === null) {
|
||||
console.error(
|
||||
"Failed to update quote favorite button: randomQuote is null"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
quoteLang = Config.mode === "quote" ? randomQuote.language : "";
|
||||
quoteId = Config.mode === "quote" ? randomQuote.id.toString() : "";
|
||||
|
||||
const icon = $(".pageTest #result #favoriteQuoteButton .icon");
|
||||
|
||||
if (Config.mode === "quote" && Auth?.currentUser) {
|
||||
const userFav = QuotesController.isQuoteFavorite(randomQuote);
|
||||
|
||||
icon.removeClass(userFav ? "far" : "fas").addClass(userFav ? "fas" : "far");
|
||||
icon.parent().removeClass("hidden");
|
||||
} else {
|
||||
icon.parent().addClass("hidden");
|
||||
}
|
||||
const userFav = QuotesController.isQuoteFavorite(randomQuote);
|
||||
icon.removeClass(userFav ? "far" : "fas").addClass(userFav ? "fas" : "far");
|
||||
icon.parent().removeClass("hidden");
|
||||
}
|
||||
|
||||
function updateQuoteSource(randomQuote: MonkeyTypes.Quote): void {
|
||||
function updateQuoteSource(randomQuote: MonkeyTypes.Quote | null): void {
|
||||
if (Config.mode === "quote") {
|
||||
$("#result .stats .source").removeClass("hidden");
|
||||
$("#result .stats .source .bottom").html(randomQuote.source);
|
||||
$("#result .stats .source .bottom").html(
|
||||
randomQuote?.source ?? "Error: Source unknown"
|
||||
);
|
||||
} else {
|
||||
$("#result .stats .source").addClass("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(
|
||||
res: MonkeyTypes.Result<MonkeyTypes.Mode>,
|
||||
res: SharedTypes.Result<SharedTypes.Mode>,
|
||||
difficultyFailed: boolean,
|
||||
failReason: string,
|
||||
afkDetected: boolean,
|
||||
isRepeated: boolean,
|
||||
tooShort: boolean,
|
||||
randomQuote: MonkeyTypes.Quote,
|
||||
randomQuote: MonkeyTypes.Quote | null,
|
||||
dontSave: boolean
|
||||
): Promise<void> {
|
||||
resultAnnotation = [];
|
||||
|
|
|
|||
|
|
@ -73,8 +73,8 @@ export async function instantUpdate(): Promise<void> {
|
|||
}
|
||||
|
||||
export async function update(
|
||||
previous: MonkeyTypes.Mode,
|
||||
current: MonkeyTypes.Mode
|
||||
previous: SharedTypes.Mode,
|
||||
current: SharedTypes.Mode
|
||||
): Promise<void> {
|
||||
if (previous === current) return;
|
||||
$("#testConfig .mode .textButton").removeClass("active");
|
||||
|
|
@ -282,8 +282,8 @@ ConfigEvent.subscribe((eventKey, eventValue, _nosave, eventPreviousValue) => {
|
|||
if (ActivePage.get() !== "test") return;
|
||||
if (eventKey === "mode") {
|
||||
update(
|
||||
eventPreviousValue as MonkeyTypes.Mode,
|
||||
eventValue as MonkeyTypes.Mode
|
||||
eventPreviousValue as SharedTypes.Mode,
|
||||
eventValue as SharedTypes.Mode
|
||||
);
|
||||
|
||||
let m2;
|
||||
|
|
|
|||
|
|
@ -60,8 +60,7 @@ import * as ArabicLazyMode from "../states/arabic-lazy-mode";
|
|||
let failReason = "";
|
||||
const koInputVisual = document.getElementById("koInputVisual") as HTMLElement;
|
||||
|
||||
export let notSignedInLastResult: MonkeyTypes.Result<MonkeyTypes.Mode> | null =
|
||||
null;
|
||||
export let notSignedInLastResult: SharedTypes.CompletedEvent | null = null;
|
||||
|
||||
export function clearNotSignedInResult(): void {
|
||||
notSignedInLastResult = null;
|
||||
|
|
@ -609,7 +608,7 @@ export async function addWord(): Promise<void> {
|
|||
TestWords.words.length >= CustomText.text.length) ||
|
||||
(Config.mode === "quote" &&
|
||||
TestWords.words.length >=
|
||||
(TestWords.randomQuote.textSplit?.length ?? 0)) ||
|
||||
(TestWords.randomQuote?.textSplit?.length ?? 0)) ||
|
||||
(Config.mode === "custom" &&
|
||||
CustomText.isSectionRandom &&
|
||||
WordsGenerator.sectionIndex >= CustomText.section &&
|
||||
|
|
@ -676,25 +675,8 @@ export async function addWord(): Promise<void> {
|
|||
TestUI.addWord(randomWord.word);
|
||||
}
|
||||
|
||||
interface CompletedEvent extends MonkeyTypes.Result<MonkeyTypes.Mode> {
|
||||
keySpacing: number[] | "toolong";
|
||||
keyDuration: number[] | "toolong";
|
||||
customText: MonkeyTypes.CustomText;
|
||||
wpmConsistency: number;
|
||||
lang: string;
|
||||
challenge?: string | null;
|
||||
keyOverlap: number;
|
||||
lastKeyToEnd: number;
|
||||
startToFirstKey: number;
|
||||
charTotal: number;
|
||||
}
|
||||
|
||||
type PartialCompletedEvent = Omit<Partial<CompletedEvent>, "chartData"> & {
|
||||
chartData: Partial<MonkeyTypes.ChartData>;
|
||||
};
|
||||
|
||||
interface RetrySaving {
|
||||
completedEvent: CompletedEvent | null;
|
||||
completedEvent: SharedTypes.CompletedEvent | null;
|
||||
canRetry: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -733,86 +715,30 @@ export async function retrySavingResult(): Promise<void> {
|
|||
saveResult(completedEvent, true);
|
||||
}
|
||||
|
||||
function buildCompletedEvent(difficultyFailed: boolean): CompletedEvent {
|
||||
function buildCompletedEvent(
|
||||
difficultyFailed: boolean
|
||||
): SharedTypes.CompletedEvent {
|
||||
//build completed event object
|
||||
const completedEvent: PartialCompletedEvent = {
|
||||
wpm: undefined,
|
||||
rawWpm: undefined,
|
||||
charStats: undefined,
|
||||
charTotal: undefined,
|
||||
acc: undefined,
|
||||
mode: Config.mode,
|
||||
mode2: undefined,
|
||||
quoteLength: -1,
|
||||
punctuation: Config.punctuation,
|
||||
numbers: Config.numbers,
|
||||
lazyMode: Config.lazyMode,
|
||||
timestamp: Date.now(),
|
||||
language: Config.language,
|
||||
restartCount: TestStats.restartCount,
|
||||
incompleteTests: TestStats.incompleteTests,
|
||||
incompleteTestSeconds:
|
||||
TestStats.incompleteSeconds < 0
|
||||
? 0
|
||||
: Misc.roundTo2(TestStats.incompleteSeconds),
|
||||
difficulty: Config.difficulty,
|
||||
blindMode: Config.blindMode,
|
||||
tags: undefined,
|
||||
keySpacing: TestInput.keypressTimings.spacing.array,
|
||||
keyDuration: TestInput.keypressTimings.duration.array,
|
||||
keyOverlap: Misc.roundTo2(TestInput.keyOverlap.total),
|
||||
lastKeyToEnd: undefined,
|
||||
startToFirstKey: undefined,
|
||||
consistency: undefined,
|
||||
keyConsistency: undefined,
|
||||
funbox: Config.funbox,
|
||||
bailedOut: TestState.bailedOut,
|
||||
chartData: {
|
||||
wpm: TestInput.wpmHistory,
|
||||
raw: undefined,
|
||||
err: undefined,
|
||||
},
|
||||
customText: undefined,
|
||||
testDuration: undefined,
|
||||
afkDuration: undefined,
|
||||
};
|
||||
|
||||
const stfk = Misc.roundTo2(
|
||||
let stfk = Misc.roundTo2(
|
||||
TestInput.keypressTimings.spacing.first - TestStats.start
|
||||
);
|
||||
|
||||
if (stfk < 0) {
|
||||
completedEvent.startToFirstKey = 0;
|
||||
} else {
|
||||
completedEvent.startToFirstKey = stfk;
|
||||
if (stfk < 0 || Config.mode === "zen") {
|
||||
stfk = 0;
|
||||
}
|
||||
|
||||
const lkte = Misc.roundTo2(
|
||||
let lkte = Misc.roundTo2(
|
||||
TestStats.end - TestInput.keypressTimings.spacing.last
|
||||
);
|
||||
|
||||
if (lkte < 0 || Config.mode === "zen") {
|
||||
completedEvent.lastKeyToEnd = 0;
|
||||
} else {
|
||||
completedEvent.lastKeyToEnd = lkte;
|
||||
lkte = 0;
|
||||
}
|
||||
|
||||
// stats
|
||||
const stats = TestStats.calculateStats();
|
||||
if (stats.time % 1 !== 0 && Config.mode !== "time") {
|
||||
TestStats.setLastSecondNotRound();
|
||||
}
|
||||
PaceCaret.setLastTestWpm(stats.wpm);
|
||||
completedEvent.wpm = stats.wpm;
|
||||
completedEvent.rawWpm = stats.wpmRaw;
|
||||
completedEvent.charStats = [
|
||||
stats.correctChars + stats.correctSpaces,
|
||||
stats.incorrectChars,
|
||||
stats.extraChars,
|
||||
stats.missedChars,
|
||||
];
|
||||
completedEvent.charTotal = stats.allChars;
|
||||
completedEvent.acc = stats.acc;
|
||||
|
||||
PaceCaret.setLastTestWpm(stats.wpm); //todo why is this in here?
|
||||
|
||||
// if the last second was not rounded, add another data point to the history
|
||||
if (TestStats.lastSecondNotRound && !difficultyFailed) {
|
||||
|
|
@ -865,60 +791,99 @@ function buildCompletedEvent(difficultyFailed: boolean): CompletedEvent {
|
|||
if (!keyConsistency || isNaN(keyConsistency)) {
|
||||
keyConsistency = 0;
|
||||
}
|
||||
completedEvent.keyConsistency = keyConsistency;
|
||||
completedEvent.consistency = consistency;
|
||||
completedEvent.chartData.raw = rawPerSecond;
|
||||
|
||||
const chartErr = [];
|
||||
for (let i = 0; i < TestInput.errorHistory.length; i++) {
|
||||
chartErr.push(TestInput.errorHistory[i]?.count ?? 0);
|
||||
}
|
||||
|
||||
const chartData = {
|
||||
wpm: TestInput.wpmHistory,
|
||||
raw: rawPerSecond,
|
||||
err: chartErr,
|
||||
};
|
||||
|
||||
//wpm consistency
|
||||
const stddev3 = Misc.stdDev(completedEvent.chartData.wpm ?? []);
|
||||
const avg3 = Misc.mean(completedEvent.chartData.wpm ?? []);
|
||||
const wpmConsistency = Misc.roundTo2(Misc.kogasa(stddev3 / avg3));
|
||||
completedEvent.wpmConsistency = isNaN(wpmConsistency) ? 0 : wpmConsistency;
|
||||
|
||||
completedEvent.testDuration = parseFloat(stats.time.toString());
|
||||
completedEvent.afkDuration = TestStats.calculateAfkSeconds(
|
||||
completedEvent.testDuration
|
||||
);
|
||||
|
||||
completedEvent.chartData.err = [];
|
||||
for (let i = 0; i < TestInput.errorHistory.length; i++) {
|
||||
completedEvent.chartData.err.push(TestInput.errorHistory[i]?.count ?? 0);
|
||||
}
|
||||
|
||||
if (Config.mode === "quote") {
|
||||
completedEvent.quoteLength = TestWords.randomQuote.group;
|
||||
completedEvent.language = Config.language.replace(/_\d*k$/g, "");
|
||||
} else {
|
||||
delete completedEvent.quoteLength;
|
||||
}
|
||||
|
||||
completedEvent.mode2 = Misc.getMode2(Config, TestWords.randomQuote);
|
||||
const stddev3 = Misc.stdDev(chartData.wpm ?? []);
|
||||
const avg3 = Misc.mean(chartData.wpm ?? []);
|
||||
const wpmCons = Misc.roundTo2(Misc.kogasa(stddev3 / avg3));
|
||||
const wpmConsistency = isNaN(wpmCons) ? 0 : wpmCons;
|
||||
|
||||
let customText: SharedTypes.CustomText | null = null;
|
||||
if (Config.mode === "custom") {
|
||||
completedEvent.customText = <MonkeyTypes.CustomText>{};
|
||||
completedEvent.customText.textLen = CustomText.text.length;
|
||||
completedEvent.customText.isWordRandom = CustomText.isWordRandom;
|
||||
completedEvent.customText.isTimeRandom = CustomText.isTimeRandom;
|
||||
completedEvent.customText.word = CustomText.word;
|
||||
completedEvent.customText.time = CustomText.time;
|
||||
} else {
|
||||
delete completedEvent.customText;
|
||||
customText = <SharedTypes.CustomText>{};
|
||||
customText.textLen = CustomText.text.length;
|
||||
customText.isWordRandom = CustomText.isWordRandom;
|
||||
customText.isTimeRandom = CustomText.isTimeRandom;
|
||||
customText.word = CustomText.word;
|
||||
customText.time = CustomText.time;
|
||||
}
|
||||
|
||||
//tags
|
||||
const activeTagsIds: string[] = [];
|
||||
try {
|
||||
DB.getSnapshot()?.tags?.forEach((tag) => {
|
||||
if (tag.active === true) {
|
||||
activeTagsIds.push(tag._id);
|
||||
}
|
||||
});
|
||||
} catch (e) {}
|
||||
completedEvent.tags = activeTagsIds;
|
||||
for (const tag of DB.getSnapshot()?.tags ?? []) {
|
||||
if (tag.active === true) {
|
||||
activeTagsIds.push(tag._id);
|
||||
}
|
||||
}
|
||||
|
||||
const duration = parseFloat(stats.time.toString());
|
||||
const afkDuration = TestStats.calculateAfkSeconds(duration);
|
||||
let language = Config.language;
|
||||
if (Config.mode === "quote") {
|
||||
language = Config.language.replace(/_\d*k$/g, "");
|
||||
}
|
||||
|
||||
const quoteLength = TestWords.randomQuote?.group ?? -1;
|
||||
|
||||
const completedEvent = {
|
||||
wpm: stats.wpm,
|
||||
rawWpm: stats.wpmRaw,
|
||||
charStats: [
|
||||
stats.correctChars + stats.correctSpaces,
|
||||
stats.incorrectChars,
|
||||
stats.extraChars,
|
||||
stats.missedChars,
|
||||
],
|
||||
charTotal: stats.allChars,
|
||||
acc: stats.acc,
|
||||
mode: Config.mode,
|
||||
mode2: Misc.getMode2(Config, TestWords.randomQuote),
|
||||
quoteLength: quoteLength,
|
||||
punctuation: Config.punctuation,
|
||||
numbers: Config.numbers,
|
||||
lazyMode: Config.lazyMode,
|
||||
timestamp: Date.now(),
|
||||
language: language,
|
||||
restartCount: TestStats.restartCount,
|
||||
incompleteTests: TestStats.incompleteTests,
|
||||
incompleteTestSeconds:
|
||||
TestStats.incompleteSeconds < 0
|
||||
? 0
|
||||
: Misc.roundTo2(TestStats.incompleteSeconds),
|
||||
difficulty: Config.difficulty,
|
||||
blindMode: Config.blindMode,
|
||||
tags: activeTagsIds,
|
||||
keySpacing: TestInput.keypressTimings.spacing.array,
|
||||
keyDuration: TestInput.keypressTimings.duration.array,
|
||||
keyOverlap: Misc.roundTo2(TestInput.keyOverlap.total),
|
||||
lastKeyToEnd: lkte,
|
||||
startToFirstKey: stfk,
|
||||
consistency: consistency,
|
||||
wpmConsistency: wpmConsistency,
|
||||
keyConsistency: keyConsistency,
|
||||
funbox: Config.funbox,
|
||||
bailedOut: TestState.bailedOut,
|
||||
chartData: chartData,
|
||||
customText: customText,
|
||||
testDuration: duration,
|
||||
afkDuration: afkDuration,
|
||||
} as SharedTypes.CompletedEvent;
|
||||
|
||||
if (completedEvent.mode !== "custom") delete completedEvent.customText;
|
||||
if (completedEvent.mode !== "quote") delete completedEvent.quoteLength;
|
||||
|
||||
return <CompletedEvent>completedEvent;
|
||||
return completedEvent;
|
||||
}
|
||||
|
||||
export async function finish(difficultyFailed = false): Promise<void> {
|
||||
|
|
@ -1210,7 +1175,7 @@ export async function finish(difficultyFailed = false): Promise<void> {
|
|||
}
|
||||
|
||||
async function saveResult(
|
||||
completedEvent: CompletedEvent,
|
||||
completedEvent: SharedTypes.CompletedEvent,
|
||||
isRetrying: boolean
|
||||
): Promise<void> {
|
||||
if (!TestState.savingEnabled) {
|
||||
|
|
@ -1417,7 +1382,7 @@ $(".pageTest").on("click", "#restartTestButtonWithSameWordset", () => {
|
|||
$(".pageTest").on("click", "#testConfig .mode .textButton", (e) => {
|
||||
if (TestUI.testRestarting) return;
|
||||
if ($(e.currentTarget).hasClass("active")) return;
|
||||
const mode = ($(e.currentTarget).attr("mode") ?? "time") as MonkeyTypes.Mode;
|
||||
const mode = ($(e.currentTarget).attr("mode") ?? "time") as SharedTypes.Mode;
|
||||
if (mode === undefined) return;
|
||||
UpdateConfig.setMode(mode);
|
||||
ManualRestart.set();
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ export let start: number, end: number;
|
|||
export let start2: number, end2: number;
|
||||
export let lastSecondNotRound = false;
|
||||
|
||||
export let lastResult: MonkeyTypes.Result<MonkeyTypes.Mode>;
|
||||
export let lastResult: SharedTypes.Result<SharedTypes.Mode>;
|
||||
|
||||
export function setLastResult(
|
||||
result: MonkeyTypes.Result<MonkeyTypes.Mode>
|
||||
result: SharedTypes.Result<SharedTypes.Mode>
|
||||
): void {
|
||||
lastResult = result;
|
||||
}
|
||||
|
|
@ -104,7 +104,7 @@ export function restart(): void {
|
|||
export let restartCount = 0;
|
||||
export let incompleteSeconds = 0;
|
||||
|
||||
export let incompleteTests: MonkeyTypes.IncompleteTest[] = [];
|
||||
export let incompleteTests: SharedTypes.IncompleteTest[] = [];
|
||||
|
||||
export function incrementRestartCount(): void {
|
||||
restartCount++;
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export const words = new Words();
|
|||
export let hasTab = false;
|
||||
export let hasNewline = false;
|
||||
export let hasNumbers = false;
|
||||
export let randomQuote = null as unknown as MonkeyTypes.Quote;
|
||||
export let randomQuote = null as MonkeyTypes.Quote | null;
|
||||
|
||||
export function setRandomQuote(rq: MonkeyTypes.Quote): void {
|
||||
randomQuote = rq;
|
||||
|
|
|
|||
|
|
@ -323,7 +323,8 @@ async function applyBritishEnglishToWord(
|
|||
): Promise<string> {
|
||||
if (!Config.britishEnglish) return word;
|
||||
if (!/english/.test(Config.language)) return word;
|
||||
if (Config.mode === "quote" && TestWords.randomQuote.britishText) return word;
|
||||
if (Config.mode === "quote" && TestWords.randomQuote?.britishText)
|
||||
return word;
|
||||
|
||||
return await BritishEnglish.replace(word, previousWord);
|
||||
}
|
||||
|
|
@ -608,6 +609,10 @@ async function generateQuoteWords(
|
|||
|
||||
TestWords.setRandomQuote(rq);
|
||||
|
||||
if (TestWords.randomQuote === null) {
|
||||
throw new WordGenError("Random quote is null");
|
||||
}
|
||||
|
||||
if (TestWords.randomQuote.textSplit === undefined) {
|
||||
throw new WordGenError("Random quote textSplit is undefined");
|
||||
}
|
||||
|
|
|
|||
181
frontend/src/ts/types/types.d.ts
vendored
181
frontend/src/ts/types/types.d.ts
vendored
|
|
@ -10,16 +10,6 @@ declare namespace MonkeyTypes {
|
|||
| "profileSearch"
|
||||
| "404";
|
||||
|
||||
type Difficulty = "normal" | "expert" | "master";
|
||||
|
||||
type Mode = keyof PersonalBests;
|
||||
|
||||
type Mode2<M extends Mode> = M extends M ? keyof PersonalBests[M] : never;
|
||||
|
||||
type StringNumber = `${number}`;
|
||||
|
||||
type Mode2Custom<M extends Mode> = Mode2<M> | "custom";
|
||||
|
||||
interface LanguageGroup {
|
||||
name: string;
|
||||
languages: string[];
|
||||
|
|
@ -294,16 +284,6 @@ declare namespace MonkeyTypes {
|
|||
hasCSS?: boolean;
|
||||
}
|
||||
|
||||
interface CustomText {
|
||||
text: string[];
|
||||
isWordRandom: boolean;
|
||||
isTimeRandom: boolean;
|
||||
word: number;
|
||||
time: number;
|
||||
delimiter: string;
|
||||
textLen?: number;
|
||||
}
|
||||
|
||||
interface PresetConfig extends MonkeyTypes.Config {
|
||||
tags: string[];
|
||||
}
|
||||
|
|
@ -315,31 +295,11 @@ declare namespace MonkeyTypes {
|
|||
config: ConfigChanges;
|
||||
}
|
||||
|
||||
interface PersonalBest {
|
||||
acc: number;
|
||||
consistency: number;
|
||||
difficulty: Difficulty;
|
||||
lazyMode: boolean;
|
||||
language: string;
|
||||
punctuation: boolean;
|
||||
raw: number;
|
||||
wpm: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface PersonalBests {
|
||||
time: Record<StringNumber, PersonalBest[]>;
|
||||
words: Record<StringNumber, PersonalBest[]>;
|
||||
quote: Record<StringNumber, PersonalBest[]>;
|
||||
custom: Partial<Record<"custom", PersonalBest[]>>;
|
||||
zen: Partial<Record<"zen", PersonalBest[]>>;
|
||||
}
|
||||
|
||||
interface Tag {
|
||||
_id: string;
|
||||
name: string;
|
||||
display: string;
|
||||
personalBests: PersonalBests;
|
||||
personalBests: SharedTypes.PersonalBests;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -358,59 +318,6 @@ declare namespace MonkeyTypes {
|
|||
completedTests: number;
|
||||
}
|
||||
|
||||
interface ChartData {
|
||||
wpm: number[];
|
||||
raw: number[];
|
||||
err: number[];
|
||||
unsmoothedRaw?: number[];
|
||||
}
|
||||
|
||||
interface KeyStats {
|
||||
average: number;
|
||||
sd: number;
|
||||
}
|
||||
|
||||
interface IncompleteTest {
|
||||
acc: number;
|
||||
seconds: number;
|
||||
}
|
||||
|
||||
interface Result<M extends Mode> {
|
||||
_id: string;
|
||||
wpm: number;
|
||||
rawWpm: number;
|
||||
charStats: number[];
|
||||
correctChars?: number; // --------------
|
||||
incorrectChars?: number; // legacy results
|
||||
acc: number;
|
||||
mode: M;
|
||||
mode2: Mode2<M>;
|
||||
quoteLength: number;
|
||||
timestamp: number;
|
||||
restartCount: number;
|
||||
incompleteTestSeconds: number;
|
||||
incompleteTests: IncompleteTest[];
|
||||
testDuration: number;
|
||||
afkDuration: number;
|
||||
tags: string[];
|
||||
consistency: number;
|
||||
keyConsistency: number;
|
||||
chartData: ChartData | "toolong";
|
||||
uid: string;
|
||||
keySpacingStats: KeyStats;
|
||||
keyDurationStats: KeyStats;
|
||||
isPb?: boolean;
|
||||
bailedOut?: boolean;
|
||||
blindMode?: boolean;
|
||||
lazyMode?: boolean;
|
||||
difficulty: Difficulty;
|
||||
funbox?: string;
|
||||
language: string;
|
||||
numbers?: boolean;
|
||||
punctuation?: boolean;
|
||||
hash?: string;
|
||||
}
|
||||
|
||||
interface ApeKey {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
|
|
@ -440,12 +347,12 @@ declare namespace MonkeyTypes {
|
|||
numbers: boolean;
|
||||
words: WordsModes;
|
||||
time: TimeModes;
|
||||
mode: Mode;
|
||||
mode: SharedTypes.Mode;
|
||||
quoteLength: QuoteLength[];
|
||||
language: string;
|
||||
fontSize: number;
|
||||
freedomMode: boolean;
|
||||
difficulty: Difficulty;
|
||||
difficulty: SharedTypes.Difficulty;
|
||||
blindMode: boolean;
|
||||
quickEnd: boolean;
|
||||
caretStyle: CaretStyle;
|
||||
|
|
@ -518,7 +425,7 @@ declare namespace MonkeyTypes {
|
|||
| string[]
|
||||
| MonkeyTypes.QuoteLength[]
|
||||
| MonkeyTypes.HighlightMode
|
||||
| MonkeyTypes.ResultFilters
|
||||
| SharedTypes.ResultFilters
|
||||
| MonkeyTypes.CustomBackgroundFilter
|
||||
| null
|
||||
| undefined;
|
||||
|
|
@ -574,9 +481,9 @@ declare namespace MonkeyTypes {
|
|||
banned?: boolean;
|
||||
emailVerified?: boolean;
|
||||
quoteRatings?: QuoteRatings;
|
||||
results?: Result<Mode>[];
|
||||
results?: SharedTypes.Result<SharedTypes.Mode>[];
|
||||
verified?: boolean;
|
||||
personalBests: PersonalBests;
|
||||
personalBests: SharedTypes.PersonalBests;
|
||||
name: string;
|
||||
customThemes: CustomTheme[];
|
||||
presets?: Preset[];
|
||||
|
|
@ -593,7 +500,7 @@ declare namespace MonkeyTypes {
|
|||
details?: UserDetails;
|
||||
inventory?: UserInventory;
|
||||
addedAt: number;
|
||||
filterPresets: ResultFilters[];
|
||||
filterPresets: SharedTypes.ResultFilters[];
|
||||
xp: number;
|
||||
inboxUnreadSize: number;
|
||||
streak: number;
|
||||
|
|
@ -624,74 +531,14 @@ declare namespace MonkeyTypes {
|
|||
|
||||
type FavoriteQuotes = Record<string, string[]>;
|
||||
|
||||
interface ResultFilters {
|
||||
_id: string;
|
||||
name: string;
|
||||
pb: {
|
||||
no: boolean;
|
||||
yes: boolean;
|
||||
};
|
||||
difficulty: {
|
||||
normal: boolean;
|
||||
expert: boolean;
|
||||
master: boolean;
|
||||
};
|
||||
mode: {
|
||||
words: boolean;
|
||||
time: boolean;
|
||||
quote: boolean;
|
||||
zen: boolean;
|
||||
custom: boolean;
|
||||
};
|
||||
words: {
|
||||
"10": boolean;
|
||||
"25": boolean;
|
||||
"50": boolean;
|
||||
"100": boolean;
|
||||
custom: boolean;
|
||||
};
|
||||
time: {
|
||||
"15": boolean;
|
||||
"30": boolean;
|
||||
"60": boolean;
|
||||
"120": boolean;
|
||||
custom: boolean;
|
||||
};
|
||||
quoteLength: {
|
||||
short: boolean;
|
||||
medium: boolean;
|
||||
long: boolean;
|
||||
thicc: boolean;
|
||||
};
|
||||
punctuation: {
|
||||
on: boolean;
|
||||
off: boolean;
|
||||
};
|
||||
numbers: {
|
||||
on: boolean;
|
||||
off: boolean;
|
||||
};
|
||||
date: {
|
||||
last_day: boolean;
|
||||
last_week: boolean;
|
||||
last_month: boolean;
|
||||
last_3months: boolean;
|
||||
all: boolean;
|
||||
};
|
||||
tags: Record<string, boolean>;
|
||||
language: Record<string, boolean>;
|
||||
funbox: {
|
||||
none?: boolean;
|
||||
} & Record<string, boolean>;
|
||||
}
|
||||
type Group<
|
||||
G extends keyof SharedTypes.ResultFilters = keyof SharedTypes.ResultFilters
|
||||
> = G extends G ? SharedTypes.ResultFilters[G] : never;
|
||||
|
||||
type Group<G extends keyof ResultFilters = keyof ResultFilters> = G extends G
|
||||
? ResultFilters[G]
|
||||
: never;
|
||||
|
||||
type Filter<G extends Group = Group> = G extends keyof ResultFilters
|
||||
? keyof ResultFilters[G]
|
||||
: never;
|
||||
type Filter<G extends Group = Group> =
|
||||
G extends keyof SharedTypes.ResultFilters
|
||||
? keyof SharedTypes.ResultFilters[G]
|
||||
: never;
|
||||
|
||||
interface TimerStats {
|
||||
dateNow: number;
|
||||
|
|
|
|||
|
|
@ -1013,7 +1013,7 @@ export function canQuickRestart(
|
|||
mode: string,
|
||||
words: number,
|
||||
time: number,
|
||||
CustomText: MonkeyTypes.CustomText,
|
||||
CustomText: SharedTypes.CustomText,
|
||||
customTextIsLong: boolean
|
||||
): boolean {
|
||||
const wordsLong = mode === "words" && (words >= 1000 || words === 0);
|
||||
|
|
@ -1173,10 +1173,10 @@ export async function swapElements(
|
|||
return;
|
||||
}
|
||||
|
||||
export function getMode2<M extends keyof MonkeyTypes.PersonalBests>(
|
||||
export function getMode2<M extends keyof SharedTypes.PersonalBests>(
|
||||
config: MonkeyTypes.Config,
|
||||
randomQuote: MonkeyTypes.Quote
|
||||
): MonkeyTypes.Mode2<M> {
|
||||
randomQuote: MonkeyTypes.Quote | null
|
||||
): SharedTypes.Mode2<M> {
|
||||
const mode = config.mode;
|
||||
let retVal: string;
|
||||
|
||||
|
|
@ -1189,16 +1189,16 @@ export function getMode2<M extends keyof MonkeyTypes.PersonalBests>(
|
|||
} else if (mode === "zen") {
|
||||
retVal = "zen";
|
||||
} else if (mode === "quote") {
|
||||
retVal = randomQuote.id.toString();
|
||||
retVal = `${randomQuote?.id ?? -1}`;
|
||||
} else {
|
||||
throw new Error("Invalid mode");
|
||||
}
|
||||
|
||||
return retVal as MonkeyTypes.Mode2<M>;
|
||||
return retVal as SharedTypes.Mode2<M>;
|
||||
}
|
||||
|
||||
export async function downloadResultsCSV(
|
||||
array: MonkeyTypes.Result<MonkeyTypes.Mode>[]
|
||||
array: SharedTypes.Result<SharedTypes.Mode>[]
|
||||
): Promise<void> {
|
||||
Loader.show();
|
||||
const csvString = [
|
||||
|
|
@ -1228,7 +1228,7 @@ export async function downloadResultsCSV(
|
|||
"tags",
|
||||
"timestamp",
|
||||
],
|
||||
...array.map((item: MonkeyTypes.Result<MonkeyTypes.Mode>) => [
|
||||
...array.map((item: SharedTypes.Result<SharedTypes.Mode>) => [
|
||||
item._id,
|
||||
item.isPb,
|
||||
item.wpm,
|
||||
|
|
|
|||
|
|
@ -101,13 +101,13 @@ export function loadCustomThemeFromUrl(getOverride?: string): void {
|
|||
}
|
||||
|
||||
type SharedTestSettings = [
|
||||
MonkeyTypes.Mode | null,
|
||||
MonkeyTypes.Mode2<MonkeyTypes.Mode> | null,
|
||||
MonkeyTypes.CustomText | null,
|
||||
SharedTypes.Mode | null,
|
||||
SharedTypes.Mode2<SharedTypes.Mode> | null,
|
||||
SharedTypes.CustomText | null,
|
||||
boolean | null,
|
||||
boolean | null,
|
||||
string | null,
|
||||
MonkeyTypes.Difficulty | null,
|
||||
SharedTypes.Difficulty | null,
|
||||
string | null
|
||||
];
|
||||
|
||||
|
|
|
|||
209
shared-types/types.d.ts
vendored
209
shared-types/types.d.ts
vendored
|
|
@ -107,4 +107,213 @@ declare namespace SharedTypes {
|
|||
};
|
||||
};
|
||||
}
|
||||
|
||||
type Difficulty = "normal" | "expert" | "master";
|
||||
|
||||
type Mode = keyof PersonalBests;
|
||||
|
||||
type Mode2<M extends Mode> = M extends M ? keyof PersonalBests[M] : never;
|
||||
|
||||
type StringNumber = `${number}`;
|
||||
|
||||
type Mode2Custom<M extends Mode> = Mode2<M> | "custom";
|
||||
|
||||
interface PersonalBest {
|
||||
acc: number;
|
||||
consistency: number;
|
||||
difficulty: Difficulty;
|
||||
lazyMode: boolean;
|
||||
language: string;
|
||||
punctuation: boolean;
|
||||
raw: number;
|
||||
wpm: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface PersonalBests {
|
||||
time: Record<StringNumber, PersonalBest[]>;
|
||||
words: Record<StringNumber, PersonalBest[]>;
|
||||
quote: Record<StringNumber, PersonalBest[]>;
|
||||
custom: Partial<Record<"custom", PersonalBest[]>>;
|
||||
zen: Partial<Record<"zen", PersonalBest[]>>;
|
||||
}
|
||||
|
||||
interface IncompleteTest {
|
||||
acc: number;
|
||||
seconds: number;
|
||||
}
|
||||
|
||||
interface ChartData {
|
||||
wpm: number[];
|
||||
raw: number[];
|
||||
err: number[];
|
||||
}
|
||||
|
||||
interface KeyStats {
|
||||
average: number;
|
||||
sd: number;
|
||||
}
|
||||
|
||||
interface Result<M extends Mode> {
|
||||
_id: string;
|
||||
wpm: number;
|
||||
rawWpm: number;
|
||||
charStats: number[];
|
||||
acc: number;
|
||||
mode: M;
|
||||
mode2: Mode2<M>;
|
||||
quoteLength?: number;
|
||||
timestamp: number;
|
||||
restartCount: number;
|
||||
incompleteTestSeconds: number;
|
||||
incompleteTests: IncompleteTest[];
|
||||
testDuration: number;
|
||||
afkDuration: number;
|
||||
tags: string[];
|
||||
consistency: number;
|
||||
keyConsistency: number;
|
||||
chartData: ChartData | "toolong";
|
||||
uid: string;
|
||||
keySpacingStats?: KeyStats;
|
||||
keyDurationStats?: KeyStats;
|
||||
isPb: boolean;
|
||||
bailedOut: boolean;
|
||||
blindMode: boolean;
|
||||
lazyMode: boolean;
|
||||
difficulty: Difficulty;
|
||||
funbox: string;
|
||||
language: string;
|
||||
numbers: boolean;
|
||||
punctuation: boolean;
|
||||
}
|
||||
|
||||
interface CustomText {
|
||||
text: string[];
|
||||
isWordRandom: boolean;
|
||||
isTimeRandom: boolean;
|
||||
word: number;
|
||||
time: number;
|
||||
delimiter: string;
|
||||
textLen?: number;
|
||||
}
|
||||
|
||||
type WithObjectId<T extends { _id: string }> = Omit<T, "_id"> & {
|
||||
_id: ObjectId;
|
||||
};
|
||||
|
||||
type DBResult<T extends SharedTypes.Mode> = WithObjectId<
|
||||
Omit<
|
||||
SharedTypes.Result<T>,
|
||||
| "bailedOut"
|
||||
| "blindMode"
|
||||
| "lazyMode"
|
||||
| "difficulty"
|
||||
| "funbox"
|
||||
| "language"
|
||||
| "numbers"
|
||||
| "punctuation"
|
||||
| "restartCount"
|
||||
| "incompleteTestSeconds"
|
||||
| "afkDuration"
|
||||
| "tags"
|
||||
| "incompleteTests"
|
||||
| "customText"
|
||||
| "quoteLength"
|
||||
> & {
|
||||
correctChars?: number; // --------------
|
||||
incorrectChars?: number; // legacy results
|
||||
// --------------
|
||||
name: string;
|
||||
// -------------- fields that might be removed to save space
|
||||
bailedOut?: boolean;
|
||||
blindMode?: boolean;
|
||||
lazyMode?: boolean;
|
||||
difficulty?: SharedTypes.Difficulty;
|
||||
funbox?: string;
|
||||
language?: string;
|
||||
numbers?: boolean;
|
||||
punctuation?: boolean;
|
||||
restartCount?: number;
|
||||
incompleteTestSeconds?: number;
|
||||
afkDuration?: number;
|
||||
tags?: string[];
|
||||
customText?: CustomText;
|
||||
quoteLength?: number;
|
||||
}
|
||||
>;
|
||||
|
||||
interface CompletedEvent extends Result<Mode> {
|
||||
keySpacing: number[] | "toolong";
|
||||
keyDuration: number[] | "toolong";
|
||||
customText?: CustomText;
|
||||
wpmConsistency: number;
|
||||
challenge?: string | null;
|
||||
keyOverlap: number;
|
||||
lastKeyToEnd: number;
|
||||
startToFirstKey: number;
|
||||
charTotal: number;
|
||||
stringified?: string;
|
||||
hash?: string;
|
||||
}
|
||||
|
||||
interface ResultFilters {
|
||||
_id: string;
|
||||
name: string;
|
||||
pb: {
|
||||
no: boolean;
|
||||
yes: boolean;
|
||||
};
|
||||
difficulty: {
|
||||
normal: boolean;
|
||||
expert: boolean;
|
||||
master: boolean;
|
||||
};
|
||||
mode: {
|
||||
words: boolean;
|
||||
time: boolean;
|
||||
quote: boolean;
|
||||
zen: boolean;
|
||||
custom: boolean;
|
||||
};
|
||||
words: {
|
||||
"10": boolean;
|
||||
"25": boolean;
|
||||
"50": boolean;
|
||||
"100": boolean;
|
||||
custom: boolean;
|
||||
};
|
||||
time: {
|
||||
"15": boolean;
|
||||
"30": boolean;
|
||||
"60": boolean;
|
||||
"120": boolean;
|
||||
custom: boolean;
|
||||
};
|
||||
quoteLength: {
|
||||
short: boolean;
|
||||
medium: boolean;
|
||||
long: boolean;
|
||||
thicc: boolean;
|
||||
};
|
||||
punctuation: {
|
||||
on: boolean;
|
||||
off: boolean;
|
||||
};
|
||||
numbers: {
|
||||
on: boolean;
|
||||
off: boolean;
|
||||
};
|
||||
date: {
|
||||
last_day: boolean;
|
||||
last_week: boolean;
|
||||
last_month: boolean;
|
||||
last_3months: boolean;
|
||||
all: boolean;
|
||||
};
|
||||
tags: Record<string, boolean>;
|
||||
language: Record<string, boolean>;
|
||||
funbox: {
|
||||
none?: boolean;
|
||||
} & Record<string, boolean>;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue