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:
Jack 2024-02-01 11:54:25 +01:00 committed by GitHub
parent 0920010f5e
commit 8b48347764
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 724 additions and 755 deletions

View file

@ -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,

View file

@ -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);
}
}

View file

@ -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,

View file

@ -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);
}

View file

@ -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");

View file

@ -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,

View file

@ -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) {

View file

@ -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;

View file

@ -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 (

View file

@ -89,7 +89,7 @@ export function setLeaderboard(
}
export function incrementResult(
res: MonkeyTypes.Result<MonkeyTypes.Mode>
res: SharedTypes.Result<SharedTypes.Mode>
): void {
const {
mode,

View 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;
}

View file

@ -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") {

View file

@ -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++) {

View file

@ -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 = "";

View file

@ -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;

View file

@ -2,7 +2,7 @@ const BASE_PATH = "/leaderboards";
interface LeaderboardQuery {
language: string;
mode: MonkeyTypes.Mode;
mode: SharedTypes.Mode;
mode2: string;
isDaily?: boolean;
daysBefore?: number;

View file

@ -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 },

View file

@ -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,

View file

@ -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 (

View file

@ -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
);
}

View file

@ -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;

View file

@ -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(

View file

@ -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();
});

View file

@ -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);

View file

@ -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);
});

View file

@ -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,

View file

@ -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;
}

View file

@ -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")) {

View file

@ -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);

View file

@ -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(

View file

@ -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;

View file

@ -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 = [];

View file

@ -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;

View file

@ -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();

View file

@ -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++;

View file

@ -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;

View file

@ -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");
}

View file

@ -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;

View file

@ -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,

View file

@ -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
];

View file

@ -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>;
}
}