From 588d14a2b33f243c7ef3390eaaf49a7cb3d0d05d Mon Sep 17 00:00:00 2001 From: egorguslyan Date: Wed, 30 Nov 2022 12:57:48 -0300 Subject: [PATCH] Multiple funboxes (#3578) egorguslyan * input-controller * result * Finishing logic * Numbers + layoutfluid * One interface * Filter results * tts error on undefined Extencions like NoScript can partly block scripts on the page. If speech synthesis is not loaded, notification shows up without freezing the page * Improved randomcase * Prevent dublicates in command line * Change filter logic * Prettier * Convert numbers * num * Quote and zen modes * withWords * Misc * Expand funboxes list for pb saving * Move list to backend * Move to constants * Async withWords, checkFunbox tweak * Prettier * Forbid nonexistent funboxes * Disable speech if language is ignored TtS's init() uses setLanguage() * canGetPb * Less circular imports * Ligatures typo * Simon says blocks word highlight * blockWordHighlight backend * Changed imports * usesLayout * JSON schema * Display notification instead of reseting * canGetPB * One getWordHtml * Dividing properties * No sync * blockedModes * forcedConfig * Infinitness parameter, list sync * applyConfig, memory Remove extra applyConfig somewhere; Memory in quotes and custom modes * I lost this code after merging * Remove arrowKeys * isFunboxCompatible * Fix logic * sync canGetPb * remove FunboxObjectType * baloons * moved cangetpb function to pb utils * updated the pb check to be easier to understand * Refactor isFunboxCompatible * Check modes conflicts * Strict highlightMode type * Only one allowed or blocked highlight mode * More checks * Undefined only, not false * Prettier * Highlight modes * added intersect helper function * reworked forced config - storing allowed modes as an array, not string - first value will be used if config is outside of the allowed values - instead of checking if highlight mode is allowed, checking if the whole config is available - removed the "Finite" forced config and replaced it with "noInfiniteDuration" property - config event listener now checks every config change, not just highlight mode. this will ensure any future forced configs will work straight out of the box * ManualRestart in commandline * fixed funbox commands not correctly showing which funbox is active * Upd list * Reduce list * split funbox into smaller files moved funbox files into its own folder * missing none command * added function to convert camel case to space separated words * changed config validation to be blocking the change rather than reacting to the change * reduced code duplication * allowing sub color flash * moved keymap key higlighting and flashing into an observable event * moved tts into a observable event * passing funbox into config validation funcitons * replaced getActive with get * only keeping functions structure in the list, moved the actual function bodies to funbox.ts done to remove a circular dependency still need to finish the rest of the funboxes * removed empty function definitions (typing issues) * removed unnecessary type * unnecessary check * moved mode checking to config validation * longer notification * checking funboxes before changing mode * moved more functions * fixed incorrect type * checking funboxes when setting punctuation and numbers * Rest of funboxes * fixed funbox commands showing tags text and icon * checking if funbox can be set with the current config * better error message * validating with setting time and words importing from a new file * added a function to capitalise the first letter of a string * using function from a new file new parameters * moved test length check to a function in a different file * moved some funbox validation into its own file * only showing notifications if the setWordCount returned true * moved funbox validation to its own file * setting manual restart when trying to set funbox to nonoe * moving this validation to before activating the funbox * returning forcedConfigs along side if current value is allowed moved infinite check to checkFunboxForcedConfigs * removed function, replaced by funox validation * removing duplicates * throwing if no intersection * wrong type * always allowing setting funbox sometimes it might be possible to update the config * checking forced configs first, and updating config if possible only setting funbox to none when couldnt update config * basic difficulty levels * xp funbox bonus * removed console logs * renamed import, renamed type * lowercase b for consistency across the codebase * renamed variable for readability * renamed for clarity * converted metadata to object * changed from beforesubgroup on the command to before list on the subgroup * using code suggested by bruce * renamed type * removed console log * merch banner fix * important animation * updating the icon of "none" funbox command * removed unnecessary import Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Miodec --- backend/src/api/controllers/result.ts | 24 +- backend/src/constants/base-configuration.ts | 5 + backend/src/constants/funbox.ts | 112 +++ backend/src/dal/user.ts | 19 +- backend/src/types/types.d.ts | 6 + backend/src/utils/pb.ts | 12 + frontend/scripts/json-validation.js | 6 +- frontend/src/styles/animations.scss | 8 +- frontend/src/ts/account/result-filters.ts | 4 +- frontend/src/ts/commandline/commands.ts | 7 +- frontend/src/ts/commandline/index.ts | 9 +- .../commandline/lists/custom-themes-list.ts | 2 +- frontend/src/ts/commandline/lists/funbox.ts | 79 ++- frontend/src/ts/commandline/lists/presets.ts | 6 +- frontend/src/ts/commandline/lists/tags.ts | 6 +- frontend/src/ts/config.ts | 62 +- .../ts/controllers/challenge-controller.ts | 25 +- .../src/ts/controllers/input-controller.ts | 118 ++-- frontend/src/ts/db.ts | 7 +- frontend/src/ts/elements/account-button.ts | 8 + frontend/src/ts/elements/keymap.ts | 75 +- frontend/src/ts/elements/modes-notice.ts | 7 +- frontend/src/ts/observables/keymap-event.ts | 33 + frontend/src/ts/observables/tts-event.ts | 18 + frontend/src/ts/pages/account.ts | 19 +- frontend/src/ts/pages/settings.ts | 38 +- frontend/src/ts/pages/test.ts | 7 +- .../src/ts/popups/custom-word-amount-popup.ts | 23 +- frontend/src/ts/test/funbox.ts | 348 ---------- frontend/src/ts/test/funbox/funbox-list.ts | 206 ++++++ frontend/src/ts/test/funbox/funbox-memory.ts | 23 + .../src/ts/test/funbox/funbox-validation.ts | 298 ++++++++ frontend/src/ts/test/funbox/funbox.ts | 646 ++++++++++++++++++ .../src/ts/test/funbox/memory-funbox-timer.ts | 53 ++ frontend/src/ts/test/poetry.ts | 9 +- frontend/src/ts/test/result.ts | 25 +- frontend/src/ts/test/test-logic.ts | 633 ++++++++--------- frontend/src/ts/test/test-stats.ts | 9 +- frontend/src/ts/test/test-timer.ts | 5 +- frontend/src/ts/test/test-ui.ts | 53 +- frontend/src/ts/test/tts.ts | 9 +- frontend/src/ts/test/weak-spot.ts | 2 +- frontend/src/ts/test/wikipedia.ts | 12 +- frontend/src/ts/test/wordset.ts | 115 +--- frontend/src/ts/types/types.d.ts | 65 +- frontend/src/ts/utils/misc.ts | 58 +- frontend/static/funbox/_list.json | 118 ++-- 47 files changed, 2290 insertions(+), 1142 deletions(-) create mode 100644 backend/src/constants/funbox.ts create mode 100644 frontend/src/ts/observables/keymap-event.ts create mode 100644 frontend/src/ts/observables/tts-event.ts delete mode 100644 frontend/src/ts/test/funbox.ts create mode 100644 frontend/src/ts/test/funbox/funbox-list.ts create mode 100644 frontend/src/ts/test/funbox/funbox-memory.ts create mode 100644 frontend/src/ts/test/funbox/funbox-validation.ts create mode 100644 frontend/src/ts/test/funbox/funbox.ts create mode 100644 frontend/src/ts/test/funbox/memory-funbox-timer.ts diff --git a/backend/src/api/controllers/result.ts b/backend/src/api/controllers/result.ts index 709308765..1187c4f49 100644 --- a/backend/src/api/controllers/result.ts +++ b/backend/src/api/controllers/result.ts @@ -36,6 +36,8 @@ import { getDailyLeaderboard } from "../../utils/daily-leaderboards"; import AutoRoleList from "../../constants/auto-roles"; import * as UserDAL from "../../dal/user"; import { buildMonkeyMail } from "../../utils/monkey-mail"; +import FunboxesMetadata from "../../constants/funbox"; +import _ from "lodash"; import * as WeeklyXpLeaderboard from "../../services/weekly-xp-leaderboard"; try { @@ -509,10 +511,16 @@ async function calculateXp( charStats, punctuation, numbers, + funbox, } = result; - const { enabled, gainMultiplier, maxDailyBonus, minDailyBonus } = - xpConfiguration; + const { + enabled, + gainMultiplier, + maxDailyBonus, + minDailyBonus, + funboxBonus: funboxBonusConfiguration, + } = xpConfiguration; if (mode === "zen" || !enabled) { return { @@ -556,6 +564,18 @@ async function calculateXp( } } + if (funboxBonusConfiguration > 0) { + const funboxModifier = _.sumBy(funbox.split("#"), (funboxName) => { + const funbox = FunboxesMetadata[funboxName as string]; + const difficultyLevel = funbox?.difficultyLevel ?? 0; + return Math.max(difficultyLevel * funboxBonusConfiguration, 0); + }); + if (funboxModifier > 0) { + modifier += funboxModifier; + breakdown["funbox"] = Math.round(baseXp * funboxModifier); + } + } + if (xpConfiguration.streak.enabled) { const streakModifier = parseFloat( mapRange( diff --git a/backend/src/constants/base-configuration.ts b/backend/src/constants/base-configuration.ts index 97f352e41..cad627eea 100644 --- a/backend/src/constants/base-configuration.ts +++ b/backend/src/constants/base-configuration.ts @@ -44,6 +44,7 @@ export const BASE_CONFIGURATION: MonkeyTypes.Configuration = { }, xp: { enabled: false, + funboxBonus: 0, gainMultiplier: 0, maxDailyBonus: 0, minDailyBonus: 0, @@ -238,6 +239,10 @@ export const CONFIGURATION_FORM_SCHEMA: ObjectSchema = { type: "number", label: "Gain Multiplier", }, + funboxBonus: { + type: "number", + label: "Funbox Bonus", + }, maxDailyBonus: { type: "number", label: "Max Daily Bonus", diff --git a/backend/src/constants/funbox.ts b/backend/src/constants/funbox.ts new file mode 100644 index 000000000..204f8a0d0 --- /dev/null +++ b/backend/src/constants/funbox.ts @@ -0,0 +1,112 @@ +const Funboxes: Record = { + nausea: { + canGetPb: true, + difficultyLevel: 2, + }, + round_round_baby: { + canGetPb: true, + difficultyLevel: 3, + }, + simon_says: { + canGetPb: true, + difficultyLevel: 1, + }, + mirror: { + canGetPb: true, + difficultyLevel: 3, + }, + tts: { + canGetPb: true, + difficultyLevel: 1, + }, + choo_choo: { + canGetPb: true, + difficultyLevel: 2, + }, + arrows: { + canGetPb: false, + difficultyLevel: 1, + }, + rAnDoMcAsE: { + canGetPb: false, + difficultyLevel: 2, + }, + capitals: { + canGetPb: false, + difficultyLevel: 1, + }, + layoutfluid: { + canGetPb: true, + difficultyLevel: 1, + }, + earthquake: { + canGetPb: true, + difficultyLevel: 1, + }, + space_balls: { + canGetPb: true, + difficultyLevel: 0, + }, + gibberish: { + canGetPb: false, + difficultyLevel: 1, + }, + "58008": { + canGetPb: false, + difficultyLevel: 1, + }, + ascii: { + canGetPb: false, + difficultyLevel: 1, + }, + specials: { + canGetPb: false, + difficultyLevel: 1, + }, + plus_one: { + canGetPb: true, + difficultyLevel: 0, + }, + plus_two: { + canGetPb: true, + difficultyLevel: 0, + }, + read_ahead_easy: { + canGetPb: true, + difficultyLevel: 1, + }, + read_ahead: { + canGetPb: true, + difficultyLevel: 2, + }, + read_ahead_hard: { + canGetPb: true, + difficultyLevel: 3, + }, + memory: { + canGetPb: true, + difficultyLevel: 3, + }, + nospace: { + canGetPb: false, + difficultyLevel: 0, + }, + poetry: { + canGetPb: false, + difficultyLevel: 0, + }, + wikipedia: { + canGetPb: false, + difficultyLevel: 0, + }, + weakspot: { + canGetPb: false, + difficultyLevel: 0, + }, + pseudolang: { + canGetPb: false, + difficultyLevel: 0, + }, +}; + +export default Funboxes; diff --git a/backend/src/dal/user.ts b/backend/src/dal/user.ts index 2550d77c9..063d66dee 100644 --- a/backend/src/dal/user.ts +++ b/backend/src/dal/user.ts @@ -1,7 +1,7 @@ import _ from "lodash"; import { isUsernameValid } from "../utils/validation"; import { updateUserEmail } from "../utils/auth"; -import { checkAndUpdatePb } from "../utils/pb"; +import { canFunboxGetPb, checkAndUpdatePb } from "../utils/pb"; import * as db from "../init/db"; import MonkeyError from "../utils/error"; import { Collection, ObjectId, WithId, Long, UpdateFilter } from "mongodb"; @@ -346,11 +346,9 @@ export async function checkIfPb( user: MonkeyTypes.User, result: MonkeyTypes.Result ): Promise { - const { mode, funbox } = result; + const { mode } = result; - if (funbox !== "none" && funbox !== "plus_one" && funbox !== "plus_two") { - return false; - } + if (!canFunboxGetPb(result)) return false; if (mode === "quote") { return false; @@ -396,15 +394,8 @@ export async function checkIfTagPb( return []; } - const { mode, tags: resultTags, funbox } = result; - if ( - funbox !== undefined && - funbox !== "none" && - funbox !== "plus_one" && - funbox !== "plus_two" - ) { - return []; - } + const { mode, tags: resultTags } = result; + if (!canFunboxGetPb(result)) return []; if (mode === "quote") { return []; diff --git a/backend/src/types/types.d.ts b/backend/src/types/types.d.ts index b3fb0dbf1..01728f7b3 100644 --- a/backend/src/types/types.d.ts +++ b/backend/src/types/types.d.ts @@ -43,6 +43,7 @@ declare namespace MonkeyTypes { }; xp: { enabled: boolean; + funboxBonus: number; gainMultiplier: number; maxDailyBonus: number; minDailyBonus: number; @@ -468,4 +469,9 @@ declare namespace MonkeyTypes { ratings: number; totalRating: number; } + + interface FunboxMetadata { + canGetPb: boolean; + difficultyLevel: number; + } } diff --git a/backend/src/utils/pb.ts b/backend/src/utils/pb.ts index 58fd563e2..e9199b6b9 100644 --- a/backend/src/utils/pb.ts +++ b/backend/src/utils/pb.ts @@ -1,4 +1,5 @@ import _ from "lodash"; +import FunboxesMetadata from "../constants/funbox"; interface CheckAndUpdatePbResult { isPb: boolean; @@ -8,6 +9,17 @@ interface CheckAndUpdatePbResult { type Result = MonkeyTypes.Result; +export function canFunboxGetPb( + result: MonkeyTypes.Result +): boolean { + const funbox = result.funbox; + if (!funbox || funbox === "none") return true; + + return funbox + .split("#") + .every((funboxName) => FunboxesMetadata[funboxName]?.canGetPb === true); +} + export function checkAndUpdatePb( userPersonalBests: MonkeyTypes.PersonalBests, lbPersonalBests: MonkeyTypes.LbPersonalBests | undefined, diff --git a/frontend/scripts/json-validation.js b/frontend/scripts/json-validation.js index 20da56134..961854e41 100644 --- a/frontend/scripts/json-validation.js +++ b/frontend/scripts/json-validation.js @@ -63,11 +63,11 @@ function validateOthers() { type: "object", properties: { name: { type: "string" }, - type: { type: "string" }, info: { type: "string" }, - affectsWordGeneration: { type: "boolean" }, + canGetPb: { type: "boolean" }, + alias: { type: "string" }, }, - required: ["name", "type", "info"], + required: ["name", "info", "canGetPb"], }, }; const funboxValidator = JSONValidator.validate(funboxData, funboxSchema); diff --git a/frontend/src/styles/animations.scss b/frontend/src/styles/animations.scss index cf9bf6dab..d0a1b4ff0 100644 --- a/frontend/src/styles/animations.scss +++ b/frontend/src/styles/animations.scss @@ -66,16 +66,16 @@ @keyframes flashHighlight { 0% { - background-color: var(--bg-color); + background-color: var(--bg-color) !important; } 10% { - background-color: var(--main-color); + background-color: var(--main-color) !important; } 40% { - background-color: var(--main-color); + background-color: var(--main-color) !important; } 100% { - background-color: var(--bg-color); + background-color: var(--bg-color) !important; } } diff --git a/frontend/src/ts/account/result-filters.ts b/frontend/src/ts/account/result-filters.ts index 2c517e67a..457966952 100644 --- a/frontend/src/ts/account/result-filters.ts +++ b/frontend/src/ts/account/result-filters.ts @@ -651,7 +651,9 @@ $(".pageAccount .topFilters .button.currentConfigFilter").on("click", () => { if (Config.funbox === "none") { filters.funbox.none = true; } else { - filters.funbox[Config.funbox] = true; + for (const f of Config.funbox.split("#")) { + filters.funbox[f] = true; + } } filters["tags"]["none"] = true; diff --git a/frontend/src/ts/commandline/commands.ts b/frontend/src/ts/commandline/commands.ts index 734a60fb0..0fb4734b2 100644 --- a/frontend/src/ts/commandline/commands.ts +++ b/frontend/src/ts/commandline/commands.ts @@ -90,7 +90,6 @@ import KeymapLayoutsCommands, { import Config, * as UpdateConfig from "../config"; import * as Misc from "../utils/misc"; -import * as TestLogic from "../test/test-logic"; import { randomizeTheme } from "../controllers/theme-controller"; import * as CustomTextPopup from "../popups/custom-text-popup"; import * as Settings from "../pages/settings"; @@ -122,6 +121,11 @@ Misc.getLanguageList() Misc.getFunboxList() .then((funboxes) => { updateFunboxCommands(funboxes); + if (FunboxCommands[0].subgroup) { + FunboxCommands[0].subgroup.beforeList = (): void => { + updateFunboxCommands(funboxes); + }; + } }) .catch((e) => { console.error( @@ -231,7 +235,6 @@ export const commands: MonkeyTypes.CommandsSubgroup = { UpdateConfig.setCustomLayoutfluid( input as MonkeyTypes.CustomLayoutFluidSpaces ); - if (Config.funbox === "layoutfluid") TestLogic.restart(); }, }, diff --git a/frontend/src/ts/commandline/index.ts b/frontend/src/ts/commandline/index.ts index 3315694a4..4c70ab8c5 100644 --- a/frontend/src/ts/commandline/index.ts +++ b/frontend/src/ts/commandline/index.ts @@ -139,6 +139,9 @@ function updateSuggested(): void { .split(" ") .filter((s, i) => s || i == 0); //remove empty entries after first const list = CommandlineLists.current[CommandlineLists.current.length - 1]; + + if (list.beforeList) list.beforeList(); + if ( inputVal[0] === "" && Config.singleListCommandLine === "on" && @@ -258,8 +261,8 @@ function trigger(command: string): void { showInput(obj.id, escaped, obj.defaultValue ? obj.defaultValue() : ""); } else if (obj.subgroup) { subgroup = true; - if (obj.beforeSubgroup) { - obj.beforeSubgroup(); + if (obj.subgroup.beforeList) { + obj.subgroup.beforeList(); } CommandlineLists.current.push( obj.subgroup as MonkeyTypes.CommandsSubgroup @@ -308,7 +311,7 @@ function addChildCommands( } if ((commandItem as MonkeyTypes.Command).subgroup) { const command = commandItem as MonkeyTypes.Command; - if (command.beforeSubgroup) command.beforeSubgroup(); + if (command.subgroup?.beforeList) command.subgroup.beforeList(); try { ( (commandItem as MonkeyTypes.Command) diff --git a/frontend/src/ts/commandline/lists/custom-themes-list.ts b/frontend/src/ts/commandline/lists/custom-themes-list.ts index fea457977..99953b209 100644 --- a/frontend/src/ts/commandline/lists/custom-themes-list.ts +++ b/frontend/src/ts/commandline/lists/custom-themes-list.ts @@ -6,6 +6,7 @@ import * as ThemeController from "../../controllers/theme-controller"; export const subgroup: MonkeyTypes.CommandsSubgroup = { title: "Custom themes list...", // configKey: "customThemeId", + beforeList: (): void => update(), list: [], }; @@ -15,7 +16,6 @@ const commands: MonkeyTypes.Command[] = [ display: "Custom themes...", icon: "fa-palette", subgroup, - beforeSubgroup: (): void => update(), available: (): boolean => { return !!Auth?.currentUser; }, diff --git a/frontend/src/ts/commandline/lists/funbox.ts b/frontend/src/ts/commandline/lists/funbox.ts index 93fee300e..430dd59fc 100644 --- a/frontend/src/ts/commandline/lists/funbox.ts +++ b/frontend/src/ts/commandline/lists/funbox.ts @@ -1,5 +1,7 @@ -import * as Funbox from "../../test/funbox"; +import * as Funbox from "../../test/funbox/funbox"; import * as TestLogic from "../../test/test-logic"; +import * as ManualRestart from "../../test/manual-restart-tracker"; +import Config from "../../config"; const subgroup: MonkeyTypes.CommandsSubgroup = { title: "Funbox...", @@ -11,7 +13,7 @@ const subgroup: MonkeyTypes.CommandsSubgroup = { configValue: "none", alias: "off", exec: (): void => { - if (Funbox.setFunbox("none", null)) { + if (Funbox.setFunbox("none")) { TestLogic.restart(); } }, @@ -29,16 +31,81 @@ const commands: MonkeyTypes.Command[] = [ }, ]; -function update(funboxes: MonkeyTypes.FunboxObject[]): void { +function update(funboxes: MonkeyTypes.FunboxMetadata[]): void { + subgroup.list = []; + subgroup.list.push({ + id: "changeFunboxNone", + display: "none", + configValue: "none", + alias: "off", + exec: (): void => { + ManualRestart.set(); + if (Funbox.setFunbox("none")) { + TestLogic.restart(); + } + }, + }); funboxes.forEach((funbox) => { + let dis; + if (Config.funbox.includes(funbox.name)) { + dis = + '' + funbox.name.replace(/_/g, " "); + } else { + dis = '' + funbox.name.replace(/_/g, " "); + } + subgroup.list.push({ id: "changeFunbox" + funbox.name, - display: funbox.name.replace(/_/g, " "), + noIcon: true, + display: dis, + // visible: Funbox.isFunboxCompatible(funbox.name, funbox.type), + sticky: true, alias: funbox.alias, configValue: funbox.name, exec: (): void => { - if (Funbox.setFunbox(funbox.name, funbox.type)) { - TestLogic.restart(); + Funbox.toggleFunbox(funbox.name); + ManualRestart.set(); + TestLogic.restart(); + + for (let i = 0; i < funboxes.length; i++) { + // subgroup.list[i].visible = Funbox.isFunboxCompatible(funboxes[i].name, funboxes[i].type); + + let txt = funboxes[i].name.replace(/_/g, " "); + if (Config.funbox.includes(funboxes[i].name)) { + txt = '' + txt; + } else { + txt = '' + txt; + } + if ($("#commandLine").hasClass("allCommands")) { + $( + `#commandLine .suggestions .entry[command='changeFunbox${funboxes[i].name}']` + ).html( + `
Funbox > ` + + txt + ); + } else { + $( + `#commandLine .suggestions .entry[command='changeFunbox${funboxes[i].name}']` + ).html(txt); + } + } + if (funboxes.length > 0) { + const noneTxt = + Config.funbox === "none" + ? `none` + : `none`; + if ($("#commandLine").hasClass("allCommands")) { + $( + `#commandLine .suggestions .entry[command='changeFunboxNone']` + ).html( + `
Funbox > ` + + noneTxt + ); + } else { + $( + `#commandLine .suggestions .entry[command='changeFunboxNone']` + ).html(noneTxt); + } } }, }); diff --git a/frontend/src/ts/commandline/lists/presets.ts b/frontend/src/ts/commandline/lists/presets.ts index e48b49f4b..ad9cc592b 100644 --- a/frontend/src/ts/commandline/lists/presets.ts +++ b/frontend/src/ts/commandline/lists/presets.ts @@ -8,6 +8,9 @@ import { Auth } from "../../firebase"; const subgroup: MonkeyTypes.CommandsSubgroup = { title: "Presets...", list: [], + beforeList: (): void => { + update(); + }, }; const commands: MonkeyTypes.Command[] = [ @@ -17,9 +20,6 @@ const commands: MonkeyTypes.Command[] = [ display: "Presets...", icon: "fa-sliders-h", subgroup, - beforeSubgroup: (): void => { - update(); - }, available: (): boolean => { return !!Auth?.currentUser; }, diff --git a/frontend/src/ts/commandline/lists/tags.ts b/frontend/src/ts/commandline/lists/tags.ts index 15860b5f0..186599ccd 100644 --- a/frontend/src/ts/commandline/lists/tags.ts +++ b/frontend/src/ts/commandline/lists/tags.ts @@ -9,6 +9,9 @@ import { Auth } from "../../firebase"; const subgroup: MonkeyTypes.CommandsSubgroup = { title: "Change tags...", list: [], + beforeList: (): void => { + update(); + }, }; const commands: MonkeyTypes.Command[] = [ @@ -18,9 +21,6 @@ const commands: MonkeyTypes.Command[] = [ display: "Tags...", icon: "fa-tag", subgroup, - beforeSubgroup: (): void => { - update(); - }, available: (): boolean => { return !!Auth?.currentUser; }, diff --git a/frontend/src/ts/config.ts b/frontend/src/ts/config.ts index f84df7596..290267a6b 100644 --- a/frontend/src/ts/config.ts +++ b/frontend/src/ts/config.ts @@ -12,6 +12,7 @@ import { Auth } from "./firebase"; import * as AnalyticsController from "./controllers/analytics-controller"; import * as AccountButton from "./elements/account-button"; import { debounce } from "throttle-debounce"; +import { canSetConfigWithCurrentFunboxes } from "./test/funbox/funbox-validation"; export let localStorageConfig: MonkeyTypes.Config; export let dbConfigLoaded = false; @@ -92,6 +93,10 @@ export async function saveFullConfigToLocalStorage( export function setNumbers(numb: boolean, nosave?: boolean): boolean { if (!isConfigValueValid("numbers", numb, ["boolean"])) return false; + if (!canSetConfigWithCurrentFunboxes("numbers", numb, config.funbox)) { + return false; + } + if (config.mode === "quote") { numb = false; } @@ -106,6 +111,10 @@ export function setNumbers(numb: boolean, nosave?: boolean): boolean { export function setPunctuation(punc: boolean, nosave?: boolean): boolean { if (!isConfigValueValid("punctuation", punc, ["boolean"])) return false; + if (!canSetConfigWithCurrentFunboxes("punctuation", punc, config.funbox)) { + return false; + } + if (config.mode === "quote") { punc = false; } @@ -125,10 +134,10 @@ export function setMode(mode: MonkeyTypes.Mode, nosave?: boolean): boolean { return false; } - if (mode !== "words" && config.funbox === "memory") { - Notifications.add("Memory funbox can only be used with words mode.", 0); + if (!canSetConfigWithCurrentFunboxes("mode", mode, config.funbox)) { return false; } + const previous = config.mode; config.mode = mode; if (config.mode == "custom") { @@ -235,6 +244,36 @@ export function setFunbox(funbox: string, nosave?: boolean): boolean { return true; } +export function toggleFunbox( + funbox: string, + nosave?: boolean +): number | boolean { + if (!isConfigValueValid("funbox", funbox, ["string"])) return false; + + let r; + + const funboxArray = config.funbox.split("#"); + if (funboxArray[0] == "none") funboxArray.splice(0, 1); + if (!funboxArray.includes(funbox)) { + funboxArray.push(funbox); + config.funbox = funboxArray.sort().join("#"); + r = funboxArray.indexOf(funbox); + } else { + r = funboxArray.indexOf(funbox); + funboxArray.splice(r, 1); + if (funboxArray.length == 0) { + config.funbox = "none"; + } else { + config.funbox = funboxArray.join("#"); + } + r = -r - 1; + } + saveToLocalStorage("funbox", nosave); + ConfigEvent.dispatch("funbox", config.funbox); + + return r; +} + export function setBlindMode(blind: boolean, nosave?: boolean): boolean { if (!isConfigValueValid("blind mode", blind, ["boolean"])) return false; @@ -812,16 +851,7 @@ export function setHighlightMode( return false; } - if ( - mode === "word" && - (config.funbox === "nospace" || - config.funbox === "read_ahead" || - config.funbox === "read_ahead_easy" || - config.funbox === "read_ahead_hard" || - config.funbox === "tts" || - config.funbox === "arrows") - ) { - Notifications.add("Can't use word highlight with this funbox", 0); + if (!canSetConfigWithCurrentFunboxes("highlightMode", mode, config.funbox)) { return false; } @@ -970,6 +1000,10 @@ export function setTimeConfig( ): boolean { if (!isConfigValueValid("time", time, ["number"])) return false; + if (!canSetConfigWithCurrentFunboxes("words", time, config.funbox)) { + return false; + } + const newTime = isNaN(time) || time < 0 ? DefaultConfig.time : time; config.time = newTime; @@ -1030,6 +1064,10 @@ export function setWordCount( ): boolean { if (!isConfigValueValid("words", wordCount, ["number"])) return false; + if (!canSetConfigWithCurrentFunboxes("words", wordCount, config.funbox)) { + return false; + } + const newWordCount = wordCount < 0 || wordCount > 100000 ? DefaultConfig.words : wordCount; diff --git a/frontend/src/ts/controllers/challenge-controller.ts b/frontend/src/ts/controllers/challenge-controller.ts index 7c80b9db0..5c73440f6 100644 --- a/frontend/src/ts/controllers/challenge-controller.ts +++ b/frontend/src/ts/controllers/challenge-controller.ts @@ -2,7 +2,7 @@ import * as Misc from "../utils/misc"; import * as Notifications from "../elements/notifications"; import * as ManualRestart from "../test/manual-restart-tracker"; import * as CustomText from "../test/custom-text"; -import * as Funbox from "../test/funbox"; +import * as Funbox from "../test/funbox/funbox"; import Config, * as UpdateConfig from "../config"; import * as TestUI from "../test/test-ui"; import * as ConfigEvent from "../observables/config-event"; @@ -94,10 +94,29 @@ export function verify( } } } else if (requirementType == "funbox") { - const funboxMode = requirementValue["exact"]; + const funboxMode = requirementValue["exact"] + .toString() + .split("#") + .sort() + .join("#"); if (funboxMode != result.funbox) { requirementsMet = false; - failReasons.push(`${funboxMode} funbox not active`); + for (const f of funboxMode.split("#")) { + if ( + result.funbox?.split("#").find((rf) => rf == f) == undefined + ) { + failReasons.push(`${f} funbox not active`); + } + } + if (result.funbox?.split("#")) { + for (const f of result.funbox.split("#")) { + if ( + funboxMode.split("#").find((rf) => rf == f) == undefined + ) { + failReasons.push(`${f} funbox active`); + } + } + } } } else if (requirementType == "raw") { const rawMode = Object.keys(requirementValue)[0]; diff --git a/frontend/src/ts/controllers/input-controller.ts b/frontend/src/ts/controllers/input-controller.ts index 6581054f6..729825e0a 100644 --- a/frontend/src/ts/controllers/input-controller.ts +++ b/frontend/src/ts/controllers/input-controller.ts @@ -2,18 +2,15 @@ import * as TestLogic from "../test/test-logic"; import * as TestUI from "../test/test-ui"; import * as TestStats from "../test/test-stats"; import * as Monkey from "../test/monkey"; -import Config, * as UpdateConfig from "../config"; -import * as Keymap from "../elements/keymap"; +import Config from "../config"; import * as Misc from "../utils/misc"; import * as LiveAcc from "../test/live-acc"; import * as LiveBurst from "../test/live-burst"; -import * as Funbox from "../test/funbox"; +import * as Funbox from "../test/funbox/funbox"; import * as Sound from "./sound-controller"; import * as Caret from "../test/caret"; import * as ManualRestart from "../test/manual-restart-tracker"; -import * as Notifications from "../elements/notifications"; import * as CustomText from "../test/custom-text"; -import * as Settings from "../pages/settings"; import * as LayoutEmulator from "../test/layout-emulator"; import * as PaceCaret from "../test/pace-caret"; import * as TimerProgress from "../test/timer-progress"; @@ -30,6 +27,9 @@ import * as TestWords from "../test/test-words"; import * as Hangul from "hangul-js"; import * as CustomTextState from "../states/custom-text-name"; import { navigate } from "../observables/navigate-event"; +import * as FunboxList from "../test/funbox/funbox-list"; +import * as Settings from "../pages/settings"; +import * as KeymapEvent from "../observables/keymap-event"; import { IgnoredKeys } from "../constants/ignored-keys"; let dontInsertSpace = false; @@ -56,7 +56,7 @@ function updateUI(): void { if (Config.keymapMode === "next" && Config.mode !== "zen") { if (!Config.language.startsWith("korean")) { - Keymap.highlightKey( + KeymapEvent.highlight( TestWords.words .getCurrent() .charAt(TestInput.input.current.length) @@ -86,13 +86,13 @@ function updateUI(): void { inputCharLength - koCurrWord[inputGroupLength].length ]; - Keymap.highlightKey(koChar); + KeymapEvent.highlight(koChar); } catch (e) { - Keymap.highlightKey(""); + KeymapEvent.highlight(""); } } else { //for new words - Keymap.highlightKey(koCurrWord[0][0]); + KeymapEvent.highlight(koCurrWord[0][0]); } } } @@ -123,7 +123,9 @@ function backspaceToPrevious(): void { TestInput.input.current = TestInput.input.popHistory(); TestInput.corrected.popHistory(); - if (Config.funbox === "nospace" || Config.funbox === "arrows") { + if ( + FunboxList.get(Config.funbox).find((f) => f.properties?.includes("nospace")) + ) { TestInput.input.current = TestInput.input.current.slice(0, -1); setWordsInput(" " + TestInput.input.current + " "); } @@ -148,36 +150,25 @@ function handleSpace(): void { } const currentWord: string = TestWords.words.getCurrent(); - if (Config.funbox === "layoutfluid" && Config.mode !== "time") { - // here I need to check if Config.customLayoutFluid exists because of my - // scuffed solution of returning whenever value is undefined in the setCustomLayoutfluid function - const layouts: string[] = Config.customLayoutfluid - ? Config.customLayoutfluid.split("#") - : ["qwerty", "dvorak", "colemak"]; - let index = 0; - const outOf: number = TestWords.words.length; - index = Math.floor( - (TestInput.input.history.length + 1) / (outOf / layouts.length) - ); - if (Config.layout !== layouts[index] && layouts[index] !== undefined) { - Notifications.add(`--- !!! ${layouts[index]} !!! ---`, 0); + + for (const f of FunboxList.get(Config.funbox)) { + if (f.functions?.handleSpace) { + f.functions.handleSpace(); } - UpdateConfig.setLayout(layouts[index]); - UpdateConfig.setKeymapLayout(layouts[index]); - Keymap.highlightKey( - TestWords.words - .getCurrent() - .charAt(TestInput.input.current.length) - .toString() - ); - Settings.groups["layout"]?.updateInput(); } + Settings.groups["layout"]?.updateInput(); + dontInsertSpace = true; const burst: number = TestStats.calculateBurst(); LiveBurst.update(Math.round(burst)); TestInput.pushBurstToHistory(burst); + const nospace = + FunboxList.get(Config.funbox).find((f) => + f.properties?.includes("nospace") + ) !== undefined; + //correct word or in zen mode const isWordCorrect: boolean = currentWord === TestInput.input.current || Config.mode == "zen"; @@ -193,12 +184,12 @@ function handleSpace(): void { Caret.updatePosition(); TestInput.incrementKeypressCount(); TestInput.pushKeypressWord(TestWords.words.currentIndex); - if (Config.funbox !== "nospace" && Config.funbox !== "arrows") { + if (!nospace) { Sound.playClick(); } Replay.addReplayEvent("submitCorrectWord"); } else { - if (Config.funbox !== "nospace" && Config.funbox !== "arrows") { + if (!nospace) { if (!Config.playSoundOnError || Config.blindMode) { Sound.playClick(); } else { @@ -303,7 +294,7 @@ function handleSpace(): void { } //end of line wrap if (Config.keymapMode === "react") { - Keymap.flashKey(" ", true); + KeymapEvent.flash(" ", true); } if ( Config.mode === "words" || @@ -356,21 +347,12 @@ function isCharCorrect(char: string, charIndex: number): boolean { } } - if (Config.funbox === "arrows") { - if ((char === "w" || char === "ArrowUp") && originalChar === "↑") { - return true; - } - if ((char === "s" || char === "ArrowDown") && originalChar === "↓") { - return true; - } - if ((char === "a" || char === "ArrowLeft") && originalChar === "←") { - return true; - } - if ((char === "d" || char === "ArrowRight") && originalChar === "→") { - return true; - } + const funbox = FunboxList.get(Config.funbox).find( + (f) => f.functions?.isCharCorrect + ); + if (funbox?.functions?.isCharCorrect) { + return funbox.functions.isCharCorrect(char, originalChar); } - if ( (char === "’" || char === "‘" || char === "'") && (originalChar === "’" || originalChar === "‘" || originalChar === "'") @@ -415,12 +397,17 @@ function handleChar( return; } - if (char === "\n" && Config.funbox === "58008") { - char = " "; + for (const f of FunboxList.get(Config.funbox)) { + if (f.functions?.handleChar) char = f.functions.handleChar(char); } + const nospace = + FunboxList.get(Config.funbox).find((f) => + f.properties?.includes("nospace") + ) !== undefined; + if (char !== "\n" && char !== "\t" && /\s/.test(char)) { - if (Config.funbox === "nospace" || Config.funbox === "arrows") return; + if (nospace) return; handleSpace(); //insert space for expert and master or strict space, @@ -528,7 +515,7 @@ function handleChar( //keymap if (Config.keymapMode === "react") { - Keymap.flashKey(char, thisCharCorrect); + KeymapEvent.flash(char, thisCharCorrect); } if (!correctShiftUsed && Config.difficulty != "master") return; @@ -649,7 +636,7 @@ function handleChar( //simulate space press in nospace funbox if ( - ((Config.funbox === "nospace" || Config.funbox === "arrows") && + (nospace && TestInput.input.current.length === TestWords.words.getCurrent().length) || (char === "\n" && thisCharCorrect) ) { @@ -861,11 +848,6 @@ $(document).keydown(async (event) => { } } - if (Config.funbox !== "arrows" && /Arrow/i.test(event.key)) { - event.preventDefault(); - return; - } - Monkey.type(); if (event.key === "Backspace" && TestInput.input.current.length === 0) { @@ -921,22 +903,22 @@ $(document).keydown(async (event) => { (await ShiftTracker.isUsingOppositeShift(event)) !== false; } - if (Config.funbox === "arrows") { - let char: string = event.key; - if (["ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown"].includes(char)) { - if (char === "ArrowLeft") char = "a"; - if (char === "ArrowRight") char = "d"; - if (char === "ArrowDown") char = "s"; - if (char === "ArrowUp") char = "w"; + const funbox = FunboxList.get(Config.funbox).find( + (f) => f.functions?.preventDefaultEvent + ); + if (funbox?.functions?.preventDefaultEvent) { + if (await funbox.functions.preventDefaultEvent(event)) { event.preventDefault(); - handleChar(char, TestInput.input.current.length); + handleChar(event.key, TestInput.input.current.length); updateUI(); setWordsInput(" " + TestInput.input.current); if (Config.tapeMode !== "off") { TestUI.scrollTape(); } } - } else if ( + } + + if ( Config.layout !== "default" && !( event.ctrlKey || diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index 99291bb27..e2956913f 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -5,6 +5,7 @@ import DefaultConfig from "./constants/default-config"; import { Auth } from "./firebase"; import { defaultSnap } from "./constants/default-snapshot"; import * as ConnectionState from "./states/connection"; +import { getFunboxList } from "./utils/misc"; let dbSnapshot: MonkeyTypes.Snapshot | undefined; @@ -511,7 +512,11 @@ export async function getLocalPB( lazyMode: boolean, funbox: string ): Promise { - if (funbox !== "none" && funbox !== "plus_one" && funbox !== "plus_two") { + const funboxes = (await getFunboxList()).filter((fb) => { + return funbox?.split("#").includes(fb.name); + }); + + if (!funboxes.every((f) => f.canGetPb)) { return 0; } diff --git a/frontend/src/ts/elements/account-button.ts b/frontend/src/ts/elements/account-button.ts index 7cbc4b35b..049cafc7a 100644 --- a/frontend/src/ts/elements/account-button.ts +++ b/frontend/src/ts/elements/account-button.ts @@ -293,6 +293,14 @@ async function animateXpBreakdown( if (skipBreakdown) return; + if (breakdown["funbox"]) { + await Misc.sleep(delay); + await append(`funbox +${breakdown["funbox"]}`); + total += breakdown["funbox"]; + } + + if (skipBreakdown) return; + if (breakdown["streak"]) { await Misc.sleep(delay); await append(`streak +${breakdown["streak"]}`); diff --git a/frontend/src/ts/elements/keymap.ts b/frontend/src/ts/elements/keymap.ts index 0c76350af..76128b2f2 100644 --- a/frontend/src/ts/elements/keymap.ts +++ b/frontend/src/ts/elements/keymap.ts @@ -2,12 +2,13 @@ import Config from "../config"; import * as ThemeColors from "./theme-colors"; import * as SlowTimer from "../states/slow-timer"; import * as ConfigEvent from "../observables/config-event"; +import * as KeymapEvent from "../observables/keymap-event"; import * as Misc from "../utils/misc"; import * as Hangul from "hangul-js"; import * as Notifications from "../elements/notifications"; import * as ActivePage from "../states/active-page"; -export function highlightKey(currentKey: string): void { +function highlightKey(currentKey: string): void { if (Config.mode === "zen") return; if (currentKey === "") currentKey = " "; try { @@ -37,7 +38,7 @@ export function highlightKey(currentKey: string): void { } } -export async function flashKey(key: string, correct: boolean): Promise { +async function flashKey(key: string, correct?: boolean): Promise { if (key == undefined) return; //console.log("key", key); if (key == " ") { @@ -51,41 +52,38 @@ export async function flashKey(key: string, correct: boolean): Promise { const themecolors = await ThemeColors.getAll(); try { + let css = { + color: themecolors.bg, + backgroundColor: themecolors.sub, + borderColor: themecolors.sub, + }; + if (correct || Config.blindMode) { - $(key) - .stop(true, true) - .css({ - color: themecolors.bg, - backgroundColor: themecolors.main, - borderColor: themecolors.main, - }) - .animate( - { - color: themecolors.sub, - backgroundColor: "transparent", - borderColor: themecolors.sub, - }, - SlowTimer.get() ? 0 : 500, - "easeOutExpo" - ); + css = { + color: themecolors.bg, + backgroundColor: themecolors.main, + borderColor: themecolors.main, + }; } else { - $(key) - .stop(true, true) - .css({ - color: themecolors.bg, - backgroundColor: themecolors.error, - borderColor: themecolors.error, - }) - .animate( - { - color: themecolors.sub, - backgroundColor: "transparent", - borderColor: themecolors.sub, - }, - SlowTimer.get() ? 0 : 500, - "easeOutExpo" - ); + css = { + color: themecolors.bg, + backgroundColor: themecolors.error, + borderColor: themecolors.error, + }; } + + $(key) + .stop(true, true) + .css(css) + .animate( + { + color: themecolors.sub, + backgroundColor: "transparent", + borderColor: themecolors.sub, + }, + SlowTimer.get() ? 0 : 500, + "easeOutExpo" + ); } catch (e) {} } @@ -291,3 +289,12 @@ ConfigEvent.subscribe((eventKey) => { refresh(); } }); + +KeymapEvent.subscribe((mode, key, correct) => { + if (mode === "highlight") { + highlightKey(key); + } + if (mode === "flash") { + flashKey(key, correct); + } +}); diff --git a/frontend/src/ts/elements/modes-notice.ts b/frontend/src/ts/elements/modes-notice.ts index 645f41d4b..17501dbae 100644 --- a/frontend/src/ts/elements/modes-notice.ts +++ b/frontend/src/ts/elements/modes-notice.ts @@ -182,10 +182,9 @@ export async function update(): Promise { if (Config.funbox !== "none") { $(".pageTest #testModesNotice").append( - `
${Config.funbox.replace( - /_/g, - " " - )}
` + `
${Config.funbox + .replace(/_/g, " ") + .replace(/#/g, ", ")}
` ); } diff --git a/frontend/src/ts/observables/keymap-event.ts b/frontend/src/ts/observables/keymap-event.ts new file mode 100644 index 000000000..d3c5d47fa --- /dev/null +++ b/frontend/src/ts/observables/keymap-event.ts @@ -0,0 +1,33 @@ +type SubscribeFunction = ( + mode: "highlight" | "flash", + key: string, + correct?: boolean +) => void; + +const subscribers: SubscribeFunction[] = []; + +export function subscribe(fn: SubscribeFunction): void { + subscribers.push(fn); +} + +export async function flash(key: string, correct?: boolean): Promise { + subscribers.forEach((fn) => { + try { + fn("flash", key, correct); + } catch (e) { + console.error("Keymap flash event subscriber threw an error"); + console.error(e); + } + }); +} + +export async function highlight(key: string): Promise { + subscribers.forEach((fn) => { + try { + fn("highlight", key); + } catch (e) { + console.error("Keymap highlight event subscriber threw an error"); + console.error(e); + } + }); +} diff --git a/frontend/src/ts/observables/tts-event.ts b/frontend/src/ts/observables/tts-event.ts new file mode 100644 index 000000000..78035cdae --- /dev/null +++ b/frontend/src/ts/observables/tts-event.ts @@ -0,0 +1,18 @@ +type SubscribeFunction = (text: string) => void; + +const subscribers: SubscribeFunction[] = []; + +export function subscribe(fn: SubscribeFunction): void { + subscribers.push(fn); +} + +export async function dispatch(text: string): Promise { + subscribers.forEach((fn) => { + try { + fn(text); + } catch (e) { + console.error("TTS event subscriber threw an error"); + console.error(e); + } + }); +} diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts index dc26067b0..723ce773d 100644 --- a/frontend/src/ts/pages/account.ts +++ b/frontend/src/ts/pages/account.ts @@ -88,10 +88,12 @@ function loadMoreLines(lineIndex?: number): void { } if (result.funbox !== "none" && result.funbox !== undefined) { - icons += ``; + icons += ``; } if (result.chartData === undefined) { @@ -429,7 +431,14 @@ function fillContent(): void { return; } } else { - if (!ResultFilters.getFilter("funbox", result.funbox)) { + let counter = 0; + for (const f of result.funbox.split("#")) { + if (ResultFilters.getFilter("funbox", f)) { + counter++; + break; + } + } + if (counter == 0) { if (filterDebug) { console.log(`skipping result due to funbox filter`, result); } diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index 17b5d5883..6e0c486a7 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -3,7 +3,7 @@ import Config, * as UpdateConfig from "../config"; import * as Sound from "../controllers/sound-controller"; import * as Misc from "../utils/misc"; import * as DB from "../db"; -import * as Funbox from "../test/funbox"; +import { toggleFunbox } from "../test/funbox/funbox"; import * as TagController from "../controllers/tag-controller"; import * as PresetController from "../controllers/preset-controller"; import * as ThemePicker from "../settings/theme-picker"; @@ -16,6 +16,7 @@ import * as CookiePopup from "../popups/cookie-popup"; import Page from "./page"; import { Auth } from "../firebase"; import Ape from "../ape"; +import { areFunboxesCompatible } from "../test/funbox/funbox-validation"; interface SettingsGroups { [key: string]: SettingsGroup; @@ -526,9 +527,7 @@ export async function fillSettingsPage(): Promise { funboxEl.append( `
${funbox.name.replace( + }" data-balloon-pos="up" data-balloon-length="fit" style="transform:scaleX(-1);">${funbox.name.replace( /_/g, " " )}
` @@ -537,9 +536,10 @@ export async function fillSettingsPage(): Promise { funboxEl.append( `
${funbox.name.replace(/_/g, " ")}
` + }" data-balloon-pos="up" data-balloon-length="fit">${funbox.name.replace( + /_/g, + " " + )}
` ); } }); @@ -691,9 +691,24 @@ export function updateAuthSections(): void { function setActiveFunboxButton(): void { $(`.pageSettings .section.funbox .button`).removeClass("active"); - $( - `.pageSettings .section.funbox .button[funbox='${Config.funbox}']` - ).addClass("active"); + $(`.pageSettings .section.funbox .button`).removeClass("disabled"); + Misc.getFunboxList().then((funboxModes) => { + funboxModes.forEach((funbox) => { + if ( + !areFunboxesCompatible(Config.funbox, funbox.name) && + !Config.funbox.split("#").includes(funbox.name) + ) { + $( + `.pageSettings .section.funbox .button[funbox='${funbox.name}']` + ).addClass("disabled"); + } + }); + }); + Config.funbox.split("#").forEach((funbox) => { + $(`.pageSettings .section.funbox .button[funbox='${funbox}']`).addClass( + "active" + ); + }); } function refreshTagsSettingsSection(): void { @@ -933,8 +948,7 @@ $(".pageSettings .section.minBurst").on("click", ".button.save", () => { //funbox $(".pageSettings .section.funbox").on("click", ".button", (e) => { const funbox = $(e.currentTarget).attr("funbox"); - const type = $(e.currentTarget).attr("type"); - Funbox.setFunbox(funbox, type); + toggleFunbox(funbox); setActiveFunboxButton(); }); diff --git a/frontend/src/ts/pages/test.ts b/frontend/src/ts/pages/test.ts index b54e2f8d7..b9791e903 100644 --- a/frontend/src/ts/pages/test.ts +++ b/frontend/src/ts/pages/test.ts @@ -1,11 +1,11 @@ -import Config from "../config"; import * as TestStats from "../test/test-stats"; import * as TestUI from "../test/test-ui"; import * as ManualRestart from "../test/manual-restart-tracker"; import * as TestLogic from "../test/test-logic"; -import * as Funbox from "../test/funbox"; +import * as Funbox from "../test/funbox/funbox"; import Page from "./page"; import { updateTestPageAds } from "../controllers/ad-controller"; +import * as ModesNotice from "../elements/modes-notice"; import * as Keymap from "../elements/keymap"; export const page = new Page( @@ -15,6 +15,7 @@ export const page = new Page( async () => { TestLogic.restart(); Funbox.clear(); + ModesNotice.update(); $("#wordsInput").focusout(); }, async () => { @@ -27,7 +28,7 @@ export const page = new Page( TestLogic.restart({ noAnim: true, }); - Funbox.activate(Config.funbox); + Funbox.activate(); Keymap.refresh(); }, async () => { diff --git a/frontend/src/ts/popups/custom-word-amount-popup.ts b/frontend/src/ts/popups/custom-word-amount-popup.ts index bcbd1fe21..40e3ef2c0 100644 --- a/frontend/src/ts/popups/custom-word-amount-popup.ts +++ b/frontend/src/ts/popups/custom-word-amount-popup.ts @@ -36,17 +36,18 @@ function apply(): void { const val = parseInt($("#customWordAmountPopup input").val() as string); if (val !== null && !isNaN(val) && val >= 0 && isFinite(val)) { - UpdateConfig.setWordCount(val as MonkeyTypes.WordsModes); - ManualRestart.set(); - TestLogic.restart(); - if (val > 2000) { - Notifications.add("Stay safe and take breaks!", 0); - } else if (val == 0) { - Notifications.add( - "Infinite words! Make sure to use Bail Out from the command line to save your result.", - 0, - 7 - ); + if (UpdateConfig.setWordCount(val as MonkeyTypes.WordsModes)) { + ManualRestart.set(); + TestLogic.restart(); + if (val > 2000) { + Notifications.add("Stay safe and take breaks!", 0); + } else if (val == 0) { + Notifications.add( + "Infinite words! Make sure to use Bail Out from the command line to save your result.", + 0, + 7 + ); + } } } else { Notifications.add("Custom word amount must be at least 1", 0); diff --git a/frontend/src/ts/test/funbox.ts b/frontend/src/ts/test/funbox.ts deleted file mode 100644 index 29a137e46..000000000 --- a/frontend/src/ts/test/funbox.ts +++ /dev/null @@ -1,348 +0,0 @@ -import * as TestWords from "./test-words"; -import * as Notifications from "../elements/notifications"; -import * as Misc from "../utils/misc"; -import * as ManualRestart from "./manual-restart-tracker"; -import Config, * as UpdateConfig from "../config"; -import * as TTS from "./tts"; -import * as ModesNotice from "../elements/modes-notice"; - -let modeSaved: MonkeyTypes.FunboxObjectType | null = null; -let memoryTimer: number | null = null; -let memoryInterval: NodeJS.Timeout | null = null; - -type SetFunction = (...params: any[]) => any; - -let settingsMemory: { - [key: string]: { value: any; setFunction: SetFunction }; -} = {}; - -function rememberSetting( - settingName: string, - value: any, - setFunction: SetFunction -): void { - settingsMemory[settingName] ??= { - value, - setFunction, - }; -} - -function loadMemory(): void { - Object.keys(settingsMemory).forEach((setting) => { - settingsMemory[setting].setFunction(settingsMemory[setting].value, true); - }); - settingsMemory = {}; -} - -function showMemoryTimer(): void { - $("#typingTest #memoryTimer").stop(true, true).animate( - { - opacity: 1, - }, - 125 - ); -} - -function hideMemoryTimer(): void { - $("#typingTest #memoryTimer").stop(true, true).animate( - { - opacity: 0, - }, - 125 - ); -} - -export function resetMemoryTimer(): void { - if (memoryInterval !== null) { - clearInterval(memoryInterval); - memoryInterval = null; - } - memoryTimer = null; - hideMemoryTimer(); -} - -function updateMemoryTimer(sec: number): void { - $("#typingTest #memoryTimer").text( - `Timer left to memorise all words: ${sec}s` - ); -} - -export function startMemoryTimer(): void { - resetMemoryTimer(); - memoryTimer = Math.round(Math.pow(TestWords.words.length, 1.2)); - updateMemoryTimer(memoryTimer); - showMemoryTimer(); - memoryInterval = setInterval(() => { - if (memoryTimer === null) return; - memoryTimer -= 1; - memoryTimer == 0 ? hideMemoryTimer() : updateMemoryTimer(memoryTimer); - if (memoryTimer <= 0) { - resetMemoryTimer(); - $("#wordsWrapper").addClass("hidden"); - } - }, 1000); -} - -export function reset(): void { - resetMemoryTimer(); -} - -export function toggleScript(...params: string[]): void { - if (Config.funbox === "tts") { - TTS.speak(params[0]); - } -} - -export function setFunbox( - funbox: string, - mode: MonkeyTypes.FunboxObjectType | null -): boolean { - modeSaved = mode; - loadMemory(); - UpdateConfig.setFunbox(funbox, false); - return true; -} - -export async function clear(): Promise { - $("#funBoxTheme").attr("href", ``); - $("#words").removeClass("nospace"); - $("#words").removeClass("arrows"); - reset(); - $("#wordsWrapper").removeClass("hidden"); - ManualRestart.set(); - ModesNotice.update(); - return true; -} - -export async function activate(funbox?: string): Promise { - let mode = modeSaved; - - if (funbox === undefined || funbox === null) { - funbox = Config.funbox; - } - let funboxInfo; - try { - funboxInfo = await Misc.getFunbox(funbox); - } catch (e) { - Notifications.add( - Misc.createErrorMessage(e, "Failed to activate funbox"), - -1 - ); - UpdateConfig.setFunbox("none", true); - await clear(); - return false; - } - - $("#funBoxTheme").attr("href", ``); - $("#words").removeClass("nospace"); - $("#words").removeClass("arrows"); - - let language; - try { - language = await Misc.getCurrentLanguage(Config.language); - } catch (e) { - Notifications.add( - Misc.createErrorMessage(e, "Failed to activate funbox"), - -1 - ); - UpdateConfig.setFunbox("none", true); - await clear(); - return false; - } - - if (language.ligatures) { - if (funbox == "choo_choo" || funbox == "earthquake") { - Notifications.add( - "Current language does not support this funbox mode", - 0 - ); - UpdateConfig.setFunbox("none", true); - await clear(); - return; - } - } - if (funbox !== "none" && (Config.mode === "zen" || Config.mode == "quote")) { - if (funboxInfo?.affectsWordGeneration === true) { - Notifications.add( - `${Misc.capitalizeFirstLetterOfEachWord( - Config.mode - )} mode does not support the ${funbox} funbox`, - 0 - ); - UpdateConfig.setFunbox("none", true); - await clear(); - return; - } - } - // if (funbox === "none") { - - reset(); - - $("#wordsWrapper").removeClass("hidden"); - // } - if (funbox === "none" && mode === undefined) { - mode = null; - } else if ( - (funbox !== "none" && mode === undefined) || - (funbox !== "none" && mode === null) - ) { - let list; - try { - list = await Misc.getFunboxList(); - } catch (e) { - Notifications.add( - Misc.createErrorMessage(e, "Failed to activate funbox"), - -1 - ); - await clear(); - return; - } - mode = list.filter((f) => f.name === funbox)[0].type; - } - - ManualRestart.set(); - if (mode === "style") { - if (funbox != undefined) { - $("#funBoxTheme").attr("href", `funbox/${funbox}.css`); - } - - if (funbox === "simon_says") { - UpdateConfig.setKeymapMode("next", true); - } - - if ( - (funbox === "read_ahead" || - funbox === "read_ahead_easy" || - funbox === "read_ahead_hard") && - Config.highlightMode === "word" - ) { - UpdateConfig.setHighlightMode("letter", true); - } - } else if (mode === "script") { - if (funbox === "tts") { - $("#funBoxTheme").attr("href", `funbox/simon_says.css`); - UpdateConfig.setKeymapMode("off", true); - UpdateConfig.setHighlightMode("letter", true); - } else if (funbox === "layoutfluid") { - UpdateConfig.setLayout( - Config.customLayoutfluid - ? Config.customLayoutfluid.split("#")[0] - : "qwerty", - true - ); - UpdateConfig.setKeymapLayout( - Config.customLayoutfluid - ? Config.customLayoutfluid.split("#")[0] - : "qwerty", - true - ); - } else if (funbox === "memory") { - UpdateConfig.setMode("words", true); - UpdateConfig.setShowAllLines(true, true); - if (Config.keymapMode === "next") { - UpdateConfig.setKeymapMode("react", true); - } - } else if (funbox === "nospace") { - $("#words").addClass("nospace"); - UpdateConfig.setHighlightMode("letter", true); - } else if (funbox === "arrows") { - $("#words").addClass("arrows"); - UpdateConfig.setHighlightMode("letter", true); - } - } - // ModesNotice.update(); - return true; -} - -export async function rememberSettings(): Promise { - const funbox = Config.funbox; - let mode = modeSaved; - if (funbox === "none" && mode === undefined) { - mode = null; - } else if ( - (funbox !== "none" && mode === undefined) || - (funbox !== "none" && mode === null) - ) { - let list; - try { - list = await Misc.getFunboxList(); - } catch (e) { - Notifications.add( - Misc.createErrorMessage(e, "Failed to remember setting"), - -1 - ); - await clear(); - return; - } - mode = list.filter((f) => f.name === funbox)[0].type; - } - if (mode === "style") { - if (funbox === "simon_says") { - rememberSetting( - "keymapMode", - Config.keymapMode, - UpdateConfig.setKeymapMode - ); - } - - if ( - funbox === "read_ahead" || - funbox === "read_ahead_easy" || - funbox === "read_ahead_hard" - ) { - rememberSetting( - "highlightMode", - Config.highlightMode, - UpdateConfig.setHighlightMode - ); - } - } else if (mode === "script") { - if (funbox === "tts") { - rememberSetting( - "keymapMode", - Config.keymapMode, - UpdateConfig.setKeymapMode - ); - } else if (funbox === "layoutfluid") { - rememberSetting( - "keymapMode", - Config.keymapMode, - UpdateConfig.setKeymapMode - ); - rememberSetting("layout", Config.layout, UpdateConfig.setLayout); - rememberSetting( - "keymapLayout", - Config.keymapLayout, - UpdateConfig.setKeymapLayout - ); - } else if (funbox === "memory") { - rememberSetting("mode", Config.mode, UpdateConfig.setMode); - rememberSetting( - "showAllLines", - Config.showAllLines, - UpdateConfig.setShowAllLines - ); - if (Config.keymapMode === "next") { - rememberSetting( - "keymapMode", - Config.keymapMode, - UpdateConfig.setKeymapMode - ); - } - } else if (funbox === "nospace") { - rememberSetting( - "highlightMode", - Config.highlightMode, - UpdateConfig.setHighlightMode - ); - } else if (funbox === "arrows") { - rememberSetting( - "highlightMode", - Config.highlightMode, - UpdateConfig.setHighlightMode - ); - } else if (funbox === "58008") { - rememberSetting("numbers", Config.numbers, UpdateConfig.setNumbers); - } - } -} diff --git a/frontend/src/ts/test/funbox/funbox-list.ts b/frontend/src/ts/test/funbox/funbox-list.ts new file mode 100644 index 000000000..5bff43146 --- /dev/null +++ b/frontend/src/ts/test/funbox/funbox-list.ts @@ -0,0 +1,206 @@ +const list: MonkeyTypes.FunboxMetadata[] = [ + { + name: "nausea", + info: "I think I'm gonna be sick.", + }, + { + name: "round_round_baby", + info: "...right round, like a record baby. Right, round round round.", + }, + { + name: "simon_says", + info: "Type what simon says.", + properties: ["changesWordsVisibility", "usesLayout"], + forcedConfig: { + highlightMode: ["letter", "off"], + }, + }, + { + name: "mirror", + info: "Everything is mirrored!", + }, + { + name: "tts", + info: "Listen closely.", + properties: ["changesWordsVisibility", "speaks"], + forcedConfig: { + highlightMode: ["letter", "off"], + }, + }, + { + name: "choo_choo", + info: "All the letters are spinning!", + properties: ["noLigatures", "conflictsWithSymmetricChars"], + }, + { + name: "arrows", + info: "Eurobeat Intensifies...", + properties: [ + "ignoresLanguage", + "ignoresLayout", + "nospace", + "noLetters", + "symmetricChars", + ], + forcedConfig: { + punctuation: [false], + numbers: [false], + highlightMode: ["letter", "off"], + }, + }, + { + name: "rAnDoMcAsE", + info: "I kInDa LiKe HoW iNeFfIcIeNt QwErTy Is.", + properties: ["changesCapitalisation"], + }, + { + name: "capitals", + info: "Capitalize Every Word.", + properties: ["changesCapitalisation"], + }, + { + name: "layoutfluid", + info: "Switch between layouts specified below proportionately to the length of the test.", + properties: ["changesLayout", "noInfiniteDuration"], + }, + { + name: "earthquake", + info: "Everybody get down! The words are shaking!", + properties: ["noLigatures"], + }, + { + name: "space_balls", + info: "In a galaxy far far away.", + }, + { + name: "gibberish", + info: "Anvbuefl dizzs eoos alsb?", + properties: ["ignoresLanguage", "unspeakable"], + }, + { + name: "58008", + alias: "numbers", + info: "A special mode for accountants.", + properties: ["ignoresLanguage", "ignoresLayout", "noLetters"], + forcedConfig: { + numbers: [false], + }, + }, + { + name: "ascii", + info: "Where was the ampersand again?. Only ASCII characters.", + properties: ["ignoresLanguage", "noLetters", "unspeakable"], + forcedConfig: { + punctuation: [false], + numbers: [false], + }, + }, + { + name: "specials", + info: "!@#$%^&*. Only special characters.", + properties: ["ignoresLanguage", "noLetters", "unspeakable"], + forcedConfig: { + punctuation: [false], + numbers: [false], + }, + }, + { + name: "plus_one", + info: "React quickly! Only one future word is visible.", + properties: ["changesWordsVisibility", "toPush:2", "noInfiniteDuration"], + }, + { + name: "plus_two", + info: "Only two future words are visible.", + properties: ["changesWordsVisibility", "toPush:3", "noInfiniteDuration"], + }, + { + name: "read_ahead_easy", + info: "Only the current word is invisible.", + properties: ["changesWordsVisibility"], + forcedConfig: { + highlightMode: ["letter", "off"], + }, + }, + { + name: "read_ahead", + info: "Current and the next word are invisible!", + properties: ["changesWordsVisibility"], + forcedConfig: { + highlightMode: ["letter", "off"], + }, + }, + { + name: "read_ahead_hard", + info: "Current and the next two words are invisible!", + properties: ["changesWordsVisibility"], + forcedConfig: { + highlightMode: ["letter", "off"], + }, + }, + { + name: "memory", + info: "Test your memory. Remember the words and type them blind.", + properties: ["changesWordsVisibility", "noInfiniteDuration"], + forcedConfig: { + mode: ["words", "quote", "custom"], + }, + }, + { + name: "nospace", + info: "Whoneedsspacesanyway?", + properties: ["nospace"], + forcedConfig: { + highlightMode: ["letter", "off"], + }, + }, + { + name: "poetry", + info: "Practice typing some beautiful prose.", + properties: ["noInfiniteDuration"], + forcedConfig: { + punctuation: [false], + numbers: [false], + }, + }, + { + name: "wikipedia", + info: "Practice typing wikipedia sections.", + properties: ["noInfiniteDuration"], + forcedConfig: { + punctuation: [false], + numbers: [false], + }, + }, + { + name: "weakspot", + info: "Focus on slow and mistyped letters.", + }, + { + name: "pseudolang", + info: "Nonsense words that look like the current language.", + properties: ["unspeakable"], + }, +]; + +export function getAll(): MonkeyTypes.FunboxMetadata[] { + return list; +} + +export function get(config: string): MonkeyTypes.FunboxMetadata[] { + const funboxes: MonkeyTypes.FunboxMetadata[] = []; + for (const i of config.split("#")) { + const f = list.find((f) => f.name === i); + if (f) funboxes.push(f); + } + return funboxes; +} + +export function setFunboxFunctions( + name: string, + obj: MonkeyTypes.FunboxFunctions +): void { + const fb = list.find((f) => f.name === name); + if (!fb) throw new Error(`Funbox ${name} not found.`); + fb.functions = obj; +} diff --git a/frontend/src/ts/test/funbox/funbox-memory.ts b/frontend/src/ts/test/funbox/funbox-memory.ts new file mode 100644 index 000000000..af9116931 --- /dev/null +++ b/frontend/src/ts/test/funbox/funbox-memory.ts @@ -0,0 +1,23 @@ +type SetFunction = (...params: any[]) => any; + +let settingsMemory: { + [key: string]: { value: any; setFunction: SetFunction }; +} = {}; + +export function save( + settingName: string, + value: any, + setFunction: SetFunction +): void { + settingsMemory[settingName] ??= { + value, + setFunction, + }; +} + +export function load(): void { + Object.keys(settingsMemory).forEach((setting) => { + settingsMemory[setting].setFunction(settingsMemory[setting].value, true); + }); + settingsMemory = {}; +} diff --git a/frontend/src/ts/test/funbox/funbox-validation.ts b/frontend/src/ts/test/funbox/funbox-validation.ts new file mode 100644 index 000000000..4d33efea8 --- /dev/null +++ b/frontend/src/ts/test/funbox/funbox-validation.ts @@ -0,0 +1,298 @@ +import * as FunboxList from "./funbox-list"; +import * as Notifications from "../../elements/notifications"; +import * as Misc from "../../utils/misc"; + +export function checkFunboxForcedConfigs( + key: string, + value: MonkeyTypes.ConfigValues, + funbox: string +): { + result: boolean; + forcedConfigs?: Array; +} { + if (FunboxList.get(funbox).length === 0) return { result: true }; + + if (key === "words" || key === "time") { + if (value == 0) { + if (funbox === "nospace") { + console.log("break"); + } + const fb = FunboxList.get(funbox).filter((f) => + f.properties?.includes("noInfiniteDuration") + ); + if (fb.length > 0) { + return { + result: false, + forcedConfigs: [key === "words" ? 10 : 15], + }; + } else { + return { result: true }; + } + } else { + return { result: true }; + } + } else { + const forcedConfigs: Record = {}; + // collect all forced configs + for (const fb of FunboxList.get(funbox)) { + if (fb.forcedConfig) { + //push keys to forcedConfigs, if they don't exist. if they do, intersect the values + for (const key in fb.forcedConfig) { + if (forcedConfigs[key] === undefined) { + forcedConfigs[key] = fb.forcedConfig[key]; + } else { + forcedConfigs[key] = Misc.intersect( + forcedConfigs[key], + fb.forcedConfig[key], + true + ); + } + } + } + } + + //check if the key is in forcedConfigs, if it is check the value, if its not, return true + if (forcedConfigs[key] === undefined) { + return { result: true }; + } else { + if (forcedConfigs[key].length === 0) { + throw new Error("No intersection of forced configs"); + } + return { + result: forcedConfigs[key].includes(value), + forcedConfigs: forcedConfigs[key], + }; + } + } +} + +// function: canSetConfigWithCurrentFunboxes +// checks using checkFunboxForcedConfigs. if it returns true, return true +// if it returns false, show a notification and return false +export function canSetConfigWithCurrentFunboxes( + key: string, + value: MonkeyTypes.ConfigValues, + funbox: string, + noNotification = false +): boolean { + let errorCount = 0; + if (key === "mode") { + let fb: MonkeyTypes.FunboxMetadata[] = []; + fb = fb.concat( + FunboxList.get(funbox).filter( + (f) => + f.forcedConfig?.["mode"] !== undefined && + !f.forcedConfig?.["mode"].includes(value) + ) + ); + if (value === "zen") { + fb = fb.concat( + FunboxList.get(funbox).filter( + (f) => + f.functions?.getWord || + f.functions?.pullSection || + f.functions?.alterText || + f.functions?.withWords || + f.properties?.includes("changesCapitalisation") || + f.properties?.includes("nospace") || + f.properties?.find((fp) => fp.startsWith("toPush:")) || + f.properties?.includes("changesWordsVisibility") || + f.properties?.includes("speaks") || + f.properties?.includes("changesLayout") + ) + ); + } + if (value === "quote" || value == "custom") { + fb = fb.concat( + FunboxList.get(funbox).filter( + (f) => + f.functions?.getWord || + f.functions?.pullSection || + f.functions?.withWords + ) + ); + } + + if (fb.length > 0) { + errorCount += 1; + } + } + if (key === "words" || key === "time") { + if (!checkFunboxForcedConfigs(key, value, funbox).result) { + if (!noNotification) { + Notifications.add("Active funboxes do not support infinite tests", 0); + return false; + } else { + errorCount += 1; + } + } + } else if (!checkFunboxForcedConfigs(key, value, funbox).result) { + errorCount += 1; + } + + if (errorCount > 0) { + if (!noNotification) { + Notifications.add( + `You can't set ${Misc.camelCaseToWords( + key + )} to ${value} with currently active funboxes.`, + 0, + 5 + ); + } + return false; + } else { + return true; + } +} + +export function canSetFunboxWithConfig( + funbox: string, + config: MonkeyTypes.Config +): boolean { + let funboxToCheck = config.funbox; + if (funboxToCheck === "none") { + funboxToCheck = funbox; + } else { + funboxToCheck += "#" + funbox; + } + let errorCount = 0; + for (const [configKey, configValue] of Object.entries(config)) { + if ( + !canSetConfigWithCurrentFunboxes( + configKey, + configValue, + funboxToCheck, + true + ) + ) { + errorCount += 1; + } + } + if (errorCount > 0) { + Notifications.add( + `You can't enable ${funbox.replace( + /_/g, + " " + )} with currently active config.`, + 0, + 5 + ); + return false; + } else { + return true; + } +} + +export function areFunboxesCompatible( + funboxes: string, + withFunbox?: string +): boolean { + if (withFunbox === "none" || funboxes === "none") return true; + let funboxesToCheck = FunboxList.get(funboxes); + if (withFunbox !== undefined) { + funboxesToCheck = funboxesToCheck.concat( + FunboxList.getAll().filter((f) => f.name == withFunbox) + ); + } + + const allFunboxesAreValid = + FunboxList.get(funboxes).filter( + (f) => funboxes.split("#").find((cf) => cf == f.name) !== undefined + ).length == funboxes.split("#").length; + const oneWordModifierMax = + funboxesToCheck.filter( + (f) => + f.functions?.getWord || + f.functions?.pullSection || + f.functions?.withWords + ).length <= 1; + const layoutUsability = + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp == "changesLayout") + ).length == 0 || + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp == "ignoresLayout" || fp == "usesLayout") + ).length == 0; + const oneNospaceOrToPushMax = + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp == "nospace" || fp.startsWith("toPush")) + ).length <= 1; + const oneChangesWordsVisibilityMax = + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp == "changesWordsVisibility") + ).length <= 1; + const capitalisationChangePosibility = + funboxesToCheck.filter((f) => f.properties?.find((fp) => fp == "noLetters")) + .length == 0 || + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp == "changesCapitalisation") + ).length == 0; + const noConflictsWithSymmetricChars = + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp == "conflictsWithSymmetricChars") + ).length == 0 || + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp == "symmetricChars") + ).length == 0; + const canSpeak = + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp == "speaks" || fp == "unspeakable") + ).length <= 1; + const hasLanguageToSpeak = + funboxesToCheck.filter((f) => f.properties?.find((fp) => fp == "speaks")) + .length == 0 || + funboxesToCheck.filter((f) => + f.properties?.find((fp) => fp == "ignoresLanguage") + ).length == 0; + const oneToPushOrPullSectionMax = + funboxesToCheck.filter( + (f) => + f.properties?.find((fp) => fp.startsWith("toPush:")) || + f.functions?.pullSection + ).length <= 1; + const oneApplyCSSMax = + funboxesToCheck.filter((f) => f.functions?.applyCSS).length <= 1; + const onePunctuateWordMax = + funboxesToCheck.filter((f) => f.functions?.punctuateWord).length <= 1; + const oneCharCheckerMax = + funboxesToCheck.filter((f) => f.functions?.isCharCorrect).length <= 1; + const oneCharReplacerMax = + funboxesToCheck.filter((f) => f.functions?.getWordHtml).length <= 1; + const allowedConfig = {} as MonkeyTypes.FunboxForcedConfig; + let noConfigConflicts = true; + for (const f of funboxesToCheck) { + if (!f.forcedConfig) continue; + for (const key in f.forcedConfig) { + if (allowedConfig[key]) { + if ( + Misc.intersect(allowedConfig[key], f.forcedConfig[key], true) + .length === 0 + ) { + noConfigConflicts = false; + break; + } + } else { + allowedConfig[key] = f.forcedConfig[key]; + } + } + } + + return ( + allFunboxesAreValid && + oneWordModifierMax && + layoutUsability && + oneNospaceOrToPushMax && + oneChangesWordsVisibilityMax && + capitalisationChangePosibility && + noConflictsWithSymmetricChars && + canSpeak && + hasLanguageToSpeak && + oneToPushOrPullSectionMax && + oneApplyCSSMax && + onePunctuateWordMax && + oneCharCheckerMax && + oneCharReplacerMax && + noConfigConflicts + ); +} diff --git a/frontend/src/ts/test/funbox/funbox.ts b/frontend/src/ts/test/funbox/funbox.ts new file mode 100644 index 000000000..f2ec7ec67 --- /dev/null +++ b/frontend/src/ts/test/funbox/funbox.ts @@ -0,0 +1,646 @@ +import * as Notifications from "../../elements/notifications"; +import * as Misc from "../../utils/misc"; +import * as ManualRestart from "../manual-restart-tracker"; +import Config, * as UpdateConfig from "../../config"; +import * as MemoryTimer from "./memory-funbox-timer"; +import * as FunboxMemory from "./funbox-memory"; +import * as FunboxList from "./funbox-list"; +import { save } from "./funbox-memory"; +import * as TTSEvent from "../../observables/tts-event"; +import * as KeymapEvent from "../../observables/keymap-event"; +import * as TestWords from "../test-words"; +import * as TestInput from "../test-input"; +import * as WeakSpot from "../weak-spot"; +import { getPoem } from "../poetry"; +import { getSection } from "../wikipedia"; +import { + areFunboxesCompatible, + checkFunboxForcedConfigs, +} from "./funbox-validation"; + +const prefixSize = 2; + +class CharDistribution { + public chars: { [char: string]: number }; + public count: number; + constructor() { + this.chars = {}; + this.count = 0; + } + + public addChar(char: string): void { + this.count++; + if (char in this.chars) { + this.chars[char]++; + } else { + this.chars[char] = 1; + } + } + + public randomChar(): string { + const randomIndex = Misc.randomIntFromRange(0, this.count - 1); + let runningCount = 0; + for (const [char, charCount] of Object.entries(this.chars)) { + runningCount += charCount; + if (runningCount > randomIndex) { + return char; + } + } + + return Object.keys(this.chars)[0]; + } +} + +class PseudolangWordGenerator extends Misc.Wordset { + public ngrams: { [prefix: string]: CharDistribution } = {}; + constructor(words: string[]) { + super(words); + // Can generate an unbounded number of words in theory. + this.length = Infinity; + + for (let word of words) { + // Mark the end of each word with a space. + word += " "; + let prefix = ""; + for (const c of word) { + // Add `c` to the distribution of chars that can come after `prefix`. + if (!(prefix in this.ngrams)) { + this.ngrams[prefix] = new CharDistribution(); + } + this.ngrams[prefix].addChar(c); + prefix = (prefix + c).substr(-prefixSize); + } + } + } + + public override randomWord(): string { + let word = ""; + for (;;) { + const prefix = word.substr(-prefixSize); + const charDistribution = this.ngrams[prefix]; + if (!charDistribution) { + // This shouldn't happen if this.ngrams is complete. If it does + // somehow, start generating a new word. + word = ""; + continue; + } + // Pick a random char from the distribution that comes after `prefix`. + const nextChar = charDistribution.randomChar(); + if (nextChar == " ") { + // A space marks the end of the word, so stop generating and return. + break; + } + word += nextChar; + } + return word; + } +} + +FunboxList.setFunboxFunctions("nausea", { + applyCSS(): void { + $("#funBoxTheme").attr("href", `funbox/nausea.css`); + }, +}); + +FunboxList.setFunboxFunctions("round_round_baby", { + applyCSS(): void { + $("#funBoxTheme").attr("href", `funbox/round_round_baby.css`); + }, +}); + +FunboxList.setFunboxFunctions("simon_says", { + applyCSS(): void { + $("#funBoxTheme").attr("href", `funbox/simon_says.css`); + }, + applyConfig(): void { + UpdateConfig.setKeymapMode("next", true); + }, + rememberSettings(): void { + save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode); + }, +}); + +FunboxList.setFunboxFunctions("mirror", { + applyCSS(): void { + $("#funBoxTheme").attr("href", `funbox/mirror.css`); + }, +}); + +FunboxList.setFunboxFunctions("tts", { + applyCSS(): void { + $("#funBoxTheme").attr("href", `funbox/simon_says.css`); + }, + applyConfig(): void { + UpdateConfig.setKeymapMode("off", true); + }, + rememberSettings(): void { + save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode); + }, + toggleScript(params: string[]): void { + if (window.speechSynthesis == undefined) { + Notifications.add("Failed to load text-to-speech script", -1); + return; + } + TTSEvent.dispatch(params[0]); + }, +}); + +FunboxList.setFunboxFunctions("choo_choo", { + applyCSS(): void { + $("#funBoxTheme").attr("href", `funbox/choo_choo.css`); + }, +}); + +FunboxList.setFunboxFunctions("arrows", { + getWord(): string { + return Misc.getArrows(); + }, + applyConfig(): void { + $("#words").addClass("arrows"); + }, + rememberSettings(): void { + save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode); + }, + handleChar(char: string): string { + if (char === "a" || char === "ArrowLeft") { + return "←"; + } + if (char === "s" || char === "ArrowDown") { + return "↓"; + } + if (char === "w" || char === "ArrowUp") { + return "↑"; + } + if (char === "d" || char === "ArrowRight") { + return "→"; + } + return char; + }, + isCharCorrect(char: string, originalChar: string): boolean { + if ((char === "a" || char === "ArrowLeft") && originalChar === "←") { + return true; + } + if ((char === "s" || char === "ArrowDown") && originalChar === "↓") { + return true; + } + if ((char === "w" || char === "ArrowUp") && originalChar === "↑") { + return true; + } + if ((char === "d" || char === "ArrowRight") && originalChar === "→") { + return true; + } + return false; + }, + async preventDefaultEvent( + event: JQuery.KeyDownEvent + ): Promise { + // TODO What's better? + // return /Arrow/i.test(event.key); + return ["ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown"].includes( + event.key + ); + }, + getWordHtml(char: string, letterTag?: boolean): string { + let retval = ""; + if (char === "↑") { + if (letterTag) retval += ``; + retval += ``; + if (letterTag) retval += ``; + } + if (char === "↓") { + if (letterTag) retval += ``; + retval += ``; + if (letterTag) retval += ``; + } + if (char === "←") { + if (letterTag) retval += ``; + retval += ``; + if (letterTag) retval += ``; + } + if (char === "→") { + if (letterTag) retval += ``; + retval += ``; + if (letterTag) retval += ``; + } + return retval; + }, +}); + +FunboxList.setFunboxFunctions("rAnDoMcAsE", { + alterText(word: string): string { + let randomcaseword = word[0]; + for (let i = 1; i < word.length; i++) { + if (randomcaseword[i - 1] == randomcaseword[i - 1].toUpperCase()) { + randomcaseword += word[i].toLowerCase(); + } else { + randomcaseword += word[i].toUpperCase(); + } + } + return randomcaseword; + }, +}); + +FunboxList.setFunboxFunctions("capitals", { + alterText(word: string): string { + return Misc.capitalizeFirstLetterOfEachWord(word); + }, +}); + +FunboxList.setFunboxFunctions("layoutfluid", { + applyConfig(): void { + UpdateConfig.setLayout( + Config.customLayoutfluid.split("#")[0] + ? Config.customLayoutfluid.split("#")[0] + : "qwerty", + true + ); + UpdateConfig.setKeymapLayout( + Config.customLayoutfluid.split("#")[0] + ? Config.customLayoutfluid.split("#")[0] + : "qwerty", + true + ); + }, + rememberSettings(): void { + save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode); + save("layout", Config.layout, UpdateConfig.setLayout); + save("keymapLayout", Config.keymapLayout, UpdateConfig.setKeymapLayout); + }, + handleSpace(): void { + if (Config.mode !== "time") { + // here I need to check if Config.customLayoutFluid exists because of my + // scuffed solution of returning whenever value is undefined in the setCustomLayoutfluid function + const layouts: string[] = Config.customLayoutfluid + ? Config.customLayoutfluid.split("#") + : ["qwerty", "dvorak", "colemak"]; + let index = 0; + const outOf: number = TestWords.words.length; + index = Math.floor( + (TestInput.input.history.length + 1) / (outOf / layouts.length) + ); + if (Config.layout !== layouts[index] && layouts[index] !== undefined) { + Notifications.add(`--- !!! ${layouts[index]} !!! ---`, 0); + } + if (layouts[index]) { + UpdateConfig.setLayout(layouts[index]); + UpdateConfig.setKeymapLayout(layouts[index]); + } + KeymapEvent.highlight( + TestWords.words + .getCurrent() + .charAt(TestInput.input.current.length) + .toString() + ); + } + }, + getResultContent(): string { + return Config.customLayoutfluid.replace(/#/g, " "); + }, + restart(): void { + if (this.applyConfig) this.applyConfig(); + KeymapEvent.highlight( + TestWords.words + .getCurrent() + .substring( + TestInput.input.current.length, + TestInput.input.current.length + 1 + ) + .toString() + ); + }, +}); + +FunboxList.setFunboxFunctions("earthquake", { + applyCSS(): void { + $("#funBoxTheme").attr("href", `funbox/earthquake.css`); + }, +}); + +FunboxList.setFunboxFunctions("space_balls", { + applyCSS(): void { + $("#funBoxTheme").attr("href", `funbox/space_balls.css`); + }, +}); + +FunboxList.setFunboxFunctions("gibberish", { + getWord(): string { + return Misc.getGibberish(); + }, +}); + +FunboxList.setFunboxFunctions("58008", { + getWord(): string { + let num = Misc.getNumbers(7); + if (Config.language.startsWith("kurdish")) { + num = Misc.convertNumberToArabic(num); + } else if (Config.language.startsWith("nepali")) { + num = Misc.convertNumberToNepali(num); + } + return num; + }, + punctuateWord(word: string): string { + if (word.length > 3) { + if (Math.random() < 0.5) { + word = Misc.setCharAt( + word, + Misc.randomIntFromRange(1, word.length - 2), + "." + ); + } + if (Math.random() < 0.75) { + const index = Misc.randomIntFromRange(1, word.length - 2); + if ( + word[index - 1] !== "." && + word[index + 1] !== "." && + word[index + 1] !== "0" + ) { + const special = Misc.randomElementFromArray(["/", "*", "-", "+"]); + word = Misc.setCharAt(word, index, special); + } + } + } + return word; + }, + rememberSettings(): void { + save("numbers", Config.numbers, UpdateConfig.setNumbers); + }, + handleChar(char: string): string { + if (char === "\n") { + return " "; + } + return char; + }, +}); + +FunboxList.setFunboxFunctions("ascii", { + getWord(): string { + return Misc.getASCII(); + }, +}); + +FunboxList.setFunboxFunctions("specials", { + getWord(): string { + return Misc.getSpecials(); + }, +}); + +FunboxList.setFunboxFunctions("read_ahead_easy", { + applyCSS(): void { + $("#funBoxTheme").attr("href", `funbox/read_ahead_easy.css`); + }, + rememberSettings(): void { + save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode); + }, +}); + +FunboxList.setFunboxFunctions("read_ahead", { + applyCSS(): void { + $("#funBoxTheme").attr("href", `funbox/read_ahead.css`); + }, + rememberSettings(): void { + save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode); + }, +}); + +FunboxList.setFunboxFunctions("read_ahead_hard", { + applyCSS(): void { + $("#funBoxTheme").attr("href", `funbox/read_ahead_hard.css`); + }, + rememberSettings(): void { + save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode); + }, +}); + +FunboxList.setFunboxFunctions("memory", { + applyConfig(): void { + $("#wordsWrapper").addClass("hidden"); + UpdateConfig.setShowAllLines(true, true); + if (Config.keymapMode === "next") { + UpdateConfig.setKeymapMode("react", true); + } + }, + rememberSettings(): void { + save("mode", Config.mode, UpdateConfig.setMode); + save("showAllLines", Config.showAllLines, UpdateConfig.setShowAllLines); + if (Config.keymapMode === "next") { + save("keymapMode", Config.keymapMode, UpdateConfig.setKeymapMode); + } + }, + start(): void { + MemoryTimer.reset(); + $("#wordsWrapper").addClass("hidden"); + }, + restart(): void { + MemoryTimer.start(); + if (Config.keymapMode === "next") { + UpdateConfig.setKeymapMode("react"); + } + }, +}); + +FunboxList.setFunboxFunctions("nospace", { + applyConfig(): void { + $("#words").addClass("nospace"); + }, + rememberSettings(): void { + save("highlightMode", Config.highlightMode, UpdateConfig.setHighlightMode); + }, +}); + +FunboxList.setFunboxFunctions("poetry", { + async pullSection(): Promise { + return getPoem(); + }, +}); + +FunboxList.setFunboxFunctions("wikipedia", { + async pullSection(lang?: string): Promise { + return getSection(lang ? lang : "english"); + }, +}); + +FunboxList.setFunboxFunctions("weakspot", { + getWord(wordset?: Misc.Wordset): string { + if (wordset !== undefined) return WeakSpot.getWord(wordset); + else return ""; + }, +}); + +FunboxList.setFunboxFunctions("pseudolang", { + async withWords(words?: string[]): Promise { + if (words !== undefined) return new PseudolangWordGenerator(words); + return new Misc.Wordset([]); + }, +}); + +export function toggleScript(...params: string[]): void { + FunboxList.get(Config.funbox).forEach((funbox) => { + if (funbox.functions?.toggleScript) funbox.functions.toggleScript(params); + }); +} + +export function setFunbox(funbox: string): boolean { + FunboxMemory.load(); + UpdateConfig.setFunbox(funbox, false); + return true; +} + +export function toggleFunbox(funbox: string): boolean { + if (funbox == "none") setFunbox("none"); + if ( + !areFunboxesCompatible(Config.funbox, funbox) && + !Config.funbox.split("#").includes(funbox) + ) { + Notifications.add( + `${Misc.capitalizeFirstLetter( + funbox.replace(/_/g, " ") + )} funbox is not compatible with the current funbox selection`, + 0 + ); + return true; + } + FunboxMemory.load(); + const e = UpdateConfig.toggleFunbox(funbox, false); + if (e === false || e === true) return false; + return true; +} + +export async function clear(): Promise { + $("#funBoxTheme").attr("href", ``); + $("#words").removeClass("nospace"); + $("#words").removeClass("arrows"); + $("#wordsWrapper").removeClass("hidden"); + MemoryTimer.reset(); + ManualRestart.set(); + return true; +} + +export async function activate(funbox?: string): Promise { + if (funbox === undefined || funbox === null) { + funbox = Config.funbox; + } else if (Config.funbox != funbox) { + Config.funbox = funbox; + } + + // The configuration might be edited with dev tools, + // so we need to double check its validity + if (!areFunboxesCompatible(Config.funbox)) { + Notifications.add( + Misc.createErrorMessage( + undefined, + `Failed to activate funbox: funboxes ${Config.funbox.replace( + /_/g, + " " + )} are not compatible` + ), + -1 + ); + UpdateConfig.setFunbox("none", true); + await clear(); + return false; + } + + MemoryTimer.reset(); + $("#wordsWrapper").removeClass("hidden"); + $("#funBoxTheme").attr("href", ``); + $("#words").removeClass("nospace"); + $("#words").removeClass("arrows"); + + let language; + try { + language = await Misc.getCurrentLanguage(Config.language); + } catch (e) { + Notifications.add( + Misc.createErrorMessage(e, "Failed to activate funbox"), + -1 + ); + UpdateConfig.setFunbox("none", true); + await clear(); + return false; + } + + if (language.ligatures) { + if ( + FunboxList.get(Config.funbox).find((f) => + f.properties?.includes("noLigatures") + ) + ) { + Notifications.add( + "Current language does not support this funbox mode", + 0 + ); + UpdateConfig.setFunbox("none", true); + await clear(); + return; + } + } + + let canSetSoFar = true; + + for (const [configKey, configValue] of Object.entries(Config)) { + const check = checkFunboxForcedConfigs( + configKey, + configValue, + Config.funbox + ); + if (check.result === true) continue; + if (check.result === false) { + if (check.forcedConfigs && check.forcedConfigs.length > 0) { + if (configKey === "mode") { + UpdateConfig.setMode(check.forcedConfigs[0] as MonkeyTypes.Mode); + } + if (configKey === "words") { + UpdateConfig.setWordCount(check.forcedConfigs[0] as number); + } + if (configKey === "time") { + UpdateConfig.setTimeConfig(check.forcedConfigs[0] as number); + } + if (configKey === "punctuation") { + UpdateConfig.setPunctuation(check.forcedConfigs[0] as boolean); + } + if (configKey === "numbers") { + UpdateConfig.setNumbers(check.forcedConfigs[0] as boolean); + } + if (configKey === "highlightMode") { + UpdateConfig.setHighlightMode( + check.forcedConfigs[0] as MonkeyTypes.HighlightMode + ); + } + } else { + canSetSoFar = false; + break; + } + } + } + + if (!canSetSoFar) { + if (Config.funbox.includes("#")) { + Notifications.add( + `Failed to activate funboxes ${Config.funbox}: no intersecting forced configs. Disabling funbox`, + -1 + ); + } else { + Notifications.add( + `Failed to activate funbox ${Config.funbox}: no forced configs. Disabling funbox`, + -1 + ); + } + UpdateConfig.setFunbox("none", true); + await clear(); + return; + } + + ManualRestart.set(); + FunboxList.get(Config.funbox).forEach(async (funbox) => { + if (funbox.functions?.applyCSS) funbox.functions.applyCSS(); + if (funbox.functions?.applyConfig) funbox.functions.applyConfig(); + }); + // ModesNotice.update(); + return true; +} + +export async function rememberSettings(): Promise { + FunboxList.get(Config.funbox).forEach(async (funbox) => { + if (funbox.functions?.rememberSettings) funbox.functions.rememberSettings(); + }); +} diff --git a/frontend/src/ts/test/funbox/memory-funbox-timer.ts b/frontend/src/ts/test/funbox/memory-funbox-timer.ts new file mode 100644 index 000000000..6c9523242 --- /dev/null +++ b/frontend/src/ts/test/funbox/memory-funbox-timer.ts @@ -0,0 +1,53 @@ +import * as TestWords from "../test-words"; + +let memoryTimer: number | null = null; +let memoryInterval: NodeJS.Timeout | null = null; + +export function show(): void { + $("#typingTest #memoryTimer").stop(true, true).animate( + { + opacity: 1, + }, + 125 + ); +} + +export function hide(): void { + $("#typingTest #memoryTimer").stop(true, true).animate( + { + opacity: 0, + }, + 125 + ); +} + +export function reset(): void { + if (memoryInterval !== null) { + clearInterval(memoryInterval); + memoryInterval = null; + } + memoryTimer = null; + hide(); +} + +export function start(): void { + reset(); + memoryTimer = Math.round(Math.pow(TestWords.words.length, 1.2)); + update(memoryTimer); + show(); + memoryInterval = setInterval(() => { + if (memoryTimer === null) return; + memoryTimer -= 1; + memoryTimer == 0 ? hide() : update(memoryTimer); + if (memoryTimer <= 0) { + reset(); + $("#wordsWrapper").addClass("hidden"); + } + }, 1000); +} + +export function update(sec: number): void { + $("#typingTest #memoryTimer").text( + `Timer left to memorise all words: ${sec}s` + ); +} diff --git a/frontend/src/ts/test/poetry.ts b/frontend/src/ts/test/poetry.ts index 71b411c38..f8a5d5bd7 100644 --- a/frontend/src/ts/test/poetry.ts +++ b/frontend/src/ts/test/poetry.ts @@ -1,14 +1,13 @@ import axios from "axios"; +import { Section } from "../utils/misc"; const bannedChars = ["—", "_", " "]; const maxWords = 100; const apiURL = "https://poetrydb.org/random"; -export class Poem { - public title: string; - public author: string; - public words: string[]; +export class Poem extends Section { constructor(title: string, author: string, words: string[]) { + super(title, author, words); this.title = title; this.author = author; this.words = words; @@ -45,7 +44,7 @@ interface PoemObject { author: string; } -export async function getPoem(): Promise { +export async function getPoem(): Promise
{ console.log("Getting poem"); try { diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index 197573627..8ff61297b 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -19,6 +19,7 @@ import * as TestConfig from "./test-config"; import { Chart } from "chart.js"; import { Auth } from "../firebase"; import * as SlowTimer from "../states/slow-timer"; +import * as FunboxList from "./funbox/funbox-list"; // eslint-disable-next-line no-duplicate-imports -- need to ignore because eslint doesnt know what import type is import type { PluginChartOptions, ScaleChartOptions } from "chart.js"; @@ -99,10 +100,15 @@ async function updateGraph(): Promise { const fc = await ThemeColors.get("sub"); if (Config.funbox !== "none") { - let content = Config.funbox; - if (Config.funbox === "layoutfluid") { - content += " " + Config.customLayoutfluid.replace(/#/g, " "); + let content = ""; + for (const f of FunboxList.get(Config.funbox)) { + content += f.name; + if (f.functions?.getResultContent) { + content += "(" + f.functions.getResultContent() + ")"; + } + content += " "; } + content = content.trimEnd(); resultAnnotation.push({ display: true, id: "funbox-label", @@ -524,12 +530,11 @@ function updateTestType(randomQuote: MonkeyTypes.Quote): void { testType += " " + ["short", "medium", "long", "thicc"][randomQuote.group]; } } - if ( - Config.mode != "custom" && - Config.funbox !== "gibberish" && - Config.funbox !== "ascii" && - Config.funbox !== "58008" - ) { + const ignoresLanguage = + FunboxList.get(Config.funbox).find((f) => + f.properties?.includes("ignoresLanguage") + ) !== undefined; + if (Config.mode != "custom" && !ignoresLanguage) { testType += "
" + result.language.replace(/_/g, " "); } if (Config.punctuation) { @@ -545,7 +550,7 @@ function updateTestType(randomQuote: MonkeyTypes.Quote): void { testType += "
lazy"; } if (Config.funbox !== "none") { - testType += "
" + Config.funbox.replace(/_/g, " "); + testType += "
" + Config.funbox.replace(/_/g, " ").replace(/#/g, ", "); } if (Config.difficulty == "expert") { testType += "
expert"; diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 633babf52..6b76d2b34 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -11,7 +11,7 @@ import * as TestStats from "./test-stats"; import * as PractiseWords from "./practise-words"; import * as ShiftTracker from "./shift-tracker"; import * as Focus from "./focus"; -import * as Funbox from "./funbox"; +import * as Funbox from "./funbox/funbox"; import * as Keymap from "../elements/keymap"; import * as ThemeController from "../controllers/theme-controller"; import * as PaceCaret from "./pace-caret"; @@ -28,10 +28,7 @@ import * as OutOfFocus from "./out-of-focus"; import * as AccountButton from "../elements/account-button"; import * as DB from "../db"; import * as Replay from "./replay"; -import * as Poetry from "./poetry"; -import * as Wikipedia from "./wikipedia"; import * as TodayTracker from "./today-tracker"; -import * as WeakSpot from "./weak-spot"; import * as Wordset from "./wordset"; import * as ChallengeContoller from "../controllers/challenge-controller"; import * as QuoteRatePopup from "../popups/quote-rate-popup"; @@ -57,6 +54,9 @@ import { Auth } from "../firebase"; import * as AdController from "../controllers/ad-controller"; import * as TestConfig from "./test-config"; import * as ConnectionState from "../states/connection"; +import * as FunboxList from "./funbox/funbox-list"; +import * as MemoryFunboxTimer from "./funbox/memory-funbox-timer"; +import * as KeymapEvent from "../observables/keymap-event"; let failReason = ""; const koInputVisual = document.getElementById("koInputVisual") as HTMLElement; @@ -92,229 +92,212 @@ export async function punctuateWord( const lastChar = Misc.getLastChar(previousWord); - if (Config.funbox === "58008") { - if (currentWord.length > 3) { - if (Math.random() < 0.5) { - word = Misc.setCharAt( - word, - Misc.randomIntFromRange(1, word.length - 2), - "." - ); + const funbox = FunboxList.get(Config.funbox).find( + (f) => f.functions?.punctuateWord + ); + if (funbox?.functions?.punctuateWord) { + return funbox.functions.punctuateWord(word); + } + if ( + currentLanguage != "code" && + currentLanguage != "georgian" && + (index == 0 || shouldCapitalize(lastChar)) + ) { + //always capitalise the first word or if there was a dot unless using a code alphabet or the Georgian language + + word = Misc.capitalizeFirstLetterOfEachWord(word); + + if (currentLanguage == "turkish") { + word = word.replace(/I/g, "İ"); + } + + if (currentLanguage == "spanish" || currentLanguage == "catalan") { + const rand = Math.random(); + if (rand > 0.9) { + word = "¿" + word; + spanishSentenceTracker = "?"; + } else if (rand > 0.8) { + word = "¡" + word; + spanishSentenceTracker = "!"; } - if (Math.random() < 0.75) { - const index = Misc.randomIntFromRange(1, word.length - 2); - if ( - word[index - 1] !== "." && - word[index + 1] !== "." && - word[index + 1] !== "0" + } + } else if ( + (Math.random() < 0.1 && + lastChar != "." && + lastChar != "," && + index != maxindex - 2) || + index == maxindex - 1 + ) { + if (currentLanguage == "spanish" || currentLanguage == "catalan") { + if (spanishSentenceTracker == "?" || spanishSentenceTracker == "!") { + word += spanishSentenceTracker; + spanishSentenceTracker = ""; + } + } else { + const rand = Math.random(); + if (rand <= 0.8) { + if (currentLanguage == "kurdish") { + word += "."; + } else if (currentLanguage === "nepali") { + word += "।"; + } else { + word += "."; + } + } else if (rand > 0.8 && rand < 0.9) { + if (currentLanguage == "french") { + word = "?"; + } else if ( + currentLanguage == "arabic" || + currentLanguage == "persian" || + currentLanguage == "urdu" || + currentLanguage == "kurdish" ) { - const special = Misc.randomElementFromArray(["/", "*", "-", "+"]); - word = Misc.setCharAt(word, index, special); + word += "؟"; + } else if (currentLanguage == "greek") { + word += ";"; + } else { + word += "?"; + } + } else { + if (currentLanguage == "french") { + word = "!"; + } else { + word += "!"; } } } - } else { - if ( - currentLanguage != "code" && - currentLanguage != "georgian" && - (index == 0 || shouldCapitalize(lastChar)) - ) { - //always capitalise the first word or if there was a dot unless using a code alphabet or the Georgian language - - word = Misc.capitalizeFirstLetterOfEachWord(word); - - if (currentLanguage == "turkish") { - word = word.replace(/I/g, "İ"); - } - - if (currentLanguage == "spanish" || currentLanguage == "catalan") { - const rand = Math.random(); - if (rand > 0.9) { - word = "¿" + word; - spanishSentenceTracker = "?"; - } else if (rand > 0.8) { - word = "¡" + word; - spanishSentenceTracker = "!"; - } - } - } else if ( - (Math.random() < 0.1 && - lastChar != "." && - lastChar != "," && - index != maxindex - 2) || - index == maxindex - 1 - ) { - if (currentLanguage == "spanish" || currentLanguage == "catalan") { - if (spanishSentenceTracker == "?" || spanishSentenceTracker == "!") { - word += spanishSentenceTracker; - spanishSentenceTracker = ""; - } - } else { - const rand = Math.random(); - if (rand <= 0.8) { - if (currentLanguage == "kurdish") { - word += "."; - } else if (currentLanguage === "nepali") { - word += "।"; - } else { - word += "."; - } - } else if (rand > 0.8 && rand < 0.9) { - if (currentLanguage == "french") { - word = "?"; - } else if ( - currentLanguage == "arabic" || - currentLanguage == "persian" || - currentLanguage == "urdu" || - currentLanguage == "kurdish" - ) { - word += "؟"; - } else if (currentLanguage == "greek") { - word += ";"; - } else { - word += "?"; - } - } else { - if (currentLanguage == "french") { - word = "!"; - } else { - word += "!"; - } - } - } - } else if ( - Math.random() < 0.01 && - lastChar != "," && - lastChar != "." && - currentLanguage !== "russian" - ) { - word = `"${word}"`; - } else if ( - Math.random() < 0.011 && - lastChar != "," && - lastChar != "." && - currentLanguage !== "russian" && - currentLanguage !== "ukrainian" - ) { - word = `'${word}'`; - } else if (Math.random() < 0.012 && lastChar != "," && lastChar != ".") { - if (currentLanguage == "code") { - const r = Math.random(); - if (r < 0.25) { - word = `(${word})`; - } else if (r < 0.5) { - word = `{${word}}`; - } else if (r < 0.75) { - word = `[${word}]`; - } else { - word = `<${word}>`; - } - } else { + } else if ( + Math.random() < 0.01 && + lastChar != "," && + lastChar != "." && + currentLanguage !== "russian" + ) { + word = `"${word}"`; + } else if ( + Math.random() < 0.011 && + lastChar != "," && + lastChar != "." && + currentLanguage !== "russian" && + currentLanguage !== "ukrainian" + ) { + word = `'${word}'`; + } else if (Math.random() < 0.012 && lastChar != "," && lastChar != ".") { + if (currentLanguage == "code") { + const r = Math.random(); + if (r < 0.25) { word = `(${word})`; - } - } else if ( - Math.random() < 0.013 && - lastChar != "," && - lastChar != "." && - lastChar != ";" && - lastChar != "؛" && - lastChar != ":" - ) { - if (currentLanguage == "french") { - word = ":"; - } else if (currentLanguage == "greek") { - word = "·"; + } else if (r < 0.5) { + word = `{${word}}`; + } else if (r < 0.75) { + word = `[${word}]`; } else { - word += ":"; + word = `<${word}>`; } - } else if ( - Math.random() < 0.014 && - lastChar != "," && - lastChar != "." && - previousWord != "-" - ) { - word = "-"; - } else if ( - Math.random() < 0.015 && - lastChar != "," && - lastChar != "." && - lastChar != ";" && - lastChar != "؛" && - lastChar != ":" - ) { - if (currentLanguage == "french") { - word = ";"; - } else if (currentLanguage == "greek") { - word = "·"; - } else if (currentLanguage == "arabic" || currentLanguage == "kurdish") { - word += "؛"; - } else { - word += ";"; - } - } else if (Math.random() < 0.2 && lastChar != ",") { - if ( - currentLanguage == "arabic" || - currentLanguage == "urdu" || - currentLanguage == "persian" || - currentLanguage == "kurdish" - ) { - word += "،"; - } else { - word += ","; - } - } else if (Math.random() < 0.25 && currentLanguage == "code") { - const specials = ["{", "}", "[", "]", "(", ")", ";", "=", "+", "%", "/"]; - const specialsC = [ - "{", - "}", - "[", - "]", - "(", - ")", - ";", - "=", - "+", - "%", - "/", - "/*", - "*/", - "//", - "!=", - "==", - "<=", - ">=", - "||", - "&&", - "<<", - ">>", - "%=", - "&=", - "*=", - "++", - "+=", - "--", - "-=", - "/=", - "^=", - "|=", - ]; - - if ( - (Config.language.startsWith("code_c") && - !Config.language.startsWith("code_css")) || - Config.language.startsWith("code_arduino") - ) { - word = Misc.randomElementFromArray(specialsC); - } else { - word = Misc.randomElementFromArray(specials); - } - } else if ( - Math.random() < 0.5 && - currentLanguage === "english" && - (await EnglishPunctuation.check(word)) - ) { - word = await applyEnglishPunctuationToWord(word); + } else { + word = `(${word})`; } + } else if ( + Math.random() < 0.013 && + lastChar != "," && + lastChar != "." && + lastChar != ";" && + lastChar != "؛" && + lastChar != ":" + ) { + if (currentLanguage == "french") { + word = ":"; + } else if (currentLanguage == "greek") { + word = "·"; + } else { + word += ":"; + } + } else if ( + Math.random() < 0.014 && + lastChar != "," && + lastChar != "." && + previousWord != "-" + ) { + word = "-"; + } else if ( + Math.random() < 0.015 && + lastChar != "," && + lastChar != "." && + lastChar != ";" && + lastChar != "؛" && + lastChar != ":" + ) { + if (currentLanguage == "french") { + word = ";"; + } else if (currentLanguage == "greek") { + word = "·"; + } else if (currentLanguage == "arabic" || currentLanguage == "kurdish") { + word += "؛"; + } else { + word += ";"; + } + } else if (Math.random() < 0.2 && lastChar != ",") { + if ( + currentLanguage == "arabic" || + currentLanguage == "urdu" || + currentLanguage == "persian" || + currentLanguage == "kurdish" + ) { + word += "،"; + } else { + word += ","; + } + } else if (Math.random() < 0.25 && currentLanguage == "code") { + const specials = ["{", "}", "[", "]", "(", ")", ";", "=", "+", "%", "/"]; + const specialsC = [ + "{", + "}", + "[", + "]", + "(", + ")", + ";", + "=", + "+", + "%", + "/", + "/*", + "*/", + "//", + "!=", + "==", + "<=", + ">=", + "||", + "&&", + "<<", + ">>", + "%=", + "&=", + "*=", + "++", + "+=", + "--", + "-=", + "/=", + "^=", + "|=", + ]; + + if ( + (Config.language.startsWith("code_c") && + !Config.language.startsWith("code_css")) || + Config.language.startsWith("code_arduino") + ) { + word = Misc.randomElementFromArray(specialsC); + } else { + word = Misc.randomElementFromArray(specials); + } + } else if ( + Math.random() < 0.5 && + currentLanguage === "english" && + (await EnglishPunctuation.check(word)) + ) { + word = await applyEnglishPunctuationToWord(word); } return word; } @@ -351,9 +334,8 @@ export function startTest(): boolean { TestTimer.clear(); Monkey.show(); - if (Config.funbox === "memory") { - Funbox.resetMemoryTimer(); - $("#wordsWrapper").addClass("hidden"); + for (const f of FunboxList.get(Config.funbox)) { + if (f.functions?.start) f.functions.start(); } try { @@ -514,7 +496,7 @@ export function restart(options = {} as RestartOptions): void { $("#showWordHistoryButton").removeClass("loaded"); $("#restartTestButton").blur(); - Funbox.resetMemoryTimer(); + MemoryFunboxTimer.reset(); QuoteRatePopup.clearQuoteStats(); if (ActivePage.get() == "test" && window.scrollY > 0) { window.scrollTo({ top: 0, behavior: "smooth" }); @@ -573,34 +555,19 @@ export function restart(options = {} as RestartOptions): void { await Funbox.rememberSettings(); - if (Config.funbox === "arrows") { - UpdateConfig.setPunctuation(false, true); - UpdateConfig.setNumbers(false, true); - } else if (Config.funbox === "58008") { - UpdateConfig.setNumbers(false, true); - } else if (Config.funbox === "specials") { - UpdateConfig.setPunctuation(false, true); - UpdateConfig.setNumbers(false, true); - } else if (Config.funbox === "ascii") { - UpdateConfig.setPunctuation(false, true); - UpdateConfig.setNumbers(false, true); - } - if ( - options.withSameWordset && - (Config.funbox === "plus_one" || Config.funbox === "plus_two") - ) { - const toPush = []; - if (Config.funbox === "plus_one") { - toPush.push(TestWords.words.get(0)); - toPush.push(TestWords.words.get(1)); + if (options.withSameWordset) { + const funboxToPush = FunboxList.get(Config.funbox) + .find((f) => f.properties?.find((fp) => fp.startsWith("toPush"))) + ?.properties?.find((fp) => fp.startsWith("toPush:")); + if (funboxToPush) { + const toPushCount = +funboxToPush.split(":")[1]; + const toPush = []; + for (let i = 0; i < toPushCount; i++) { + toPush.push(TestWords.words.get(i)); + } + TestWords.words.reset(); + toPush.forEach((word) => TestWords.words.push(word)); } - if (Config.funbox === "plus_two") { - toPush.push(TestWords.words.get(0)); - toPush.push(TestWords.words.get(1)); - toPush.push(TestWords.words.get(2)); - } - TestWords.words.reset(); - toPush.forEach((word) => TestWords.words.push(word)); } if (!options.withSameWordset && !shouldQuoteRepeat) { TestState.setRepeated(false); @@ -617,7 +584,7 @@ export function restart(options = {} as RestartOptions): void { TestInput.input.reset(); TestUI.showWords(); if (Config.keymapMode === "next" && Config.mode !== "zen") { - Keymap.highlightKey( + KeymapEvent.highlight( TestWords.words .getCurrent() .substring( @@ -652,11 +619,8 @@ export function restart(options = {} as RestartOptions): void { (document.querySelector("#liveAcc")).innerHTML = "100%"; (document.querySelector("#liveBurst")).innerHTML = "0"; - if (Config.funbox === "memory") { - Funbox.startMemoryTimer(); - if (Config.keymapMode === "next") { - UpdateConfig.setKeymapMode("react"); - } + for (const f of FunboxList.get(Config.funbox)) { + if (f.functions?.restart) f.functions.restart(); } if (Config.showAverage !== "off") { @@ -668,37 +632,13 @@ export function restart(options = {} as RestartOptions): void { const mode2 = Misc.getMode2(Config, TestWords.randomQuote); let fbtext = ""; if (Config.funbox !== "none") { - fbtext = " " + Config.funbox; + fbtext = " " + Config.funbox.split("#").join(" "); } $(".pageTest #premidTestMode").text( `${Config.mode} ${mode2} ${Config.language.replace(/_/g, " ")}${fbtext}` ); $(".pageTest #premidSecondsLeft").text(Config.time); - if (Config.funbox === "layoutfluid") { - UpdateConfig.setLayout( - Config.customLayoutfluid - ? Config.customLayoutfluid.split("#")[0] - : "qwerty", - true - ); - UpdateConfig.setKeymapLayout( - Config.customLayoutfluid - ? Config.customLayoutfluid.split("#")[0] - : "qwerty", - true - ); - Keymap.highlightKey( - TestWords.words - .getCurrent() - .substring( - TestInput.input.current.length, - TestInput.input.current.length + 1 - ) - .toString() - ); - } - $("#result").addClass("hidden"); $("#testModesNotice").removeClass("hidden").css({ opacity: 1, @@ -731,36 +671,21 @@ export function restart(options = {} as RestartOptions): void { ); } -function applyFunboxesToWord(word: string, wordset?: Wordset.Wordset): string { - if (Config.funbox === "rAnDoMcAsE") { - let randomcaseword = ""; - for (let i = 0; i < word.length; i++) { - if (i % 2 != 0) { - randomcaseword += word[i].toUpperCase(); - } else { - randomcaseword += word[i]; - } +function getFunboxWord(word: string, wordset?: Misc.Wordset): string { + const wordFunbox = FunboxList.get(Config.funbox).find( + (f) => f.functions?.getWord + ); + if (wordFunbox?.functions?.getWord) { + word = wordFunbox.functions.getWord(wordset); + } + return word; +} + +function applyFunboxesToWord(word: string): string { + for (const f of FunboxList.get(Config.funbox)) { + if (f.functions?.alterText) { + word = f.functions.alterText(word); } - word = randomcaseword; - } else if (Config.funbox === "capitals") { - word = Misc.capitalizeFirstLetterOfEachWord(word); - } else if (Config.funbox === "gibberish") { - word = Misc.getGibberish(); - } else if (Config.funbox === "arrows") { - word = Misc.getArrows(); - } else if (Config.funbox === "58008") { - word = Misc.getNumbers(7); - if (Config.language.startsWith("kurdish")) { - word = Misc.convertNumberToArabic(word); - } else if (Config.language.startsWith("nepali")) { - word = Misc.convertNumberToNepali(word); - } - } else if (Config.funbox === "specials") { - word = Misc.getSpecials(); - } else if (Config.funbox === "ascii") { - word = Misc.getASCII(); - } else if (wordset !== undefined && Config.funbox === "weakspot") { - word = WeakSpot.getWord(wordset); } return word; } @@ -783,7 +708,7 @@ function applyLazyModeToWord( } async function getNextWord( - wordset: Wordset.Wordset, + wordset: Misc.Wordset, language: MonkeyTypes.LanguageObject, wordsBound: number ): Promise { @@ -846,7 +771,7 @@ async function getNextWord( randomWord = randomWord.replace(/ +/gm, " "); randomWord = randomWord.replace(/^ | $/gm, ""); randomWord = applyLazyModeToWord(randomWord, language); - randomWord = applyFunboxesToWord(randomWord, wordset); + randomWord = getFunboxWord(randomWord, wordset); randomWord = await applyBritishEnglishToWord(randomWord); if (Config.punctuation) { @@ -869,6 +794,8 @@ async function getNextWord( } } + randomWord = applyFunboxesToWord(randomWord); + return randomWord; } @@ -948,13 +875,12 @@ export async function init(): Promise { } let wordsBound = 100; - if (Config.funbox === "plus_one") { - wordsBound = 2; - if (Config.mode === "words" && Config.words < wordsBound) { - wordsBound = Config.words; - } - } else if (Config.funbox === "plus_two") { - wordsBound = 3; + + const funboxToPush = FunboxList.get(Config.funbox) + .find((f) => f.properties?.find((fp) => fp.startsWith("toPush"))) + ?.properties?.find((fp) => fp.startsWith("toPush:")); + if (funboxToPush) { + wordsBound = +funboxToPush.split(":")[1]; if (Config.mode === "words" && Config.words < wordsBound) { wordsBound = Config.words; } @@ -1020,36 +946,32 @@ export async function init(): Promise { if (Config.mode == "custom") { wordList = CustomText.text; } - const wordset = Wordset.withWords(wordList, Config.funbox); + const wordset = await Wordset.withWords(wordList); + let wordCount = 0; - if ( - (Config.funbox == "wikipedia" || Config.funbox == "poetry") && - Config.mode != "custom" - ) { - let wordCount = 0; - - // If mode is words, get as many sections as you need until the wordCount is fullfilled + const sectionFunbox = FunboxList.get(Config.funbox).find( + (f) => f.functions?.pullSection + ); + if (sectionFunbox?.functions?.pullSection) { while ( (Config.mode == "words" && Config.words >= wordCount) || (Config.mode === "time" && wordCount < 100) ) { - const section = - Config.funbox == "wikipedia" - ? await Wikipedia.getSection(Config.language) - : await Poetry.getPoem(); + const section = await sectionFunbox.functions.pullSection( + Config.language + ); - if (Config.funbox == "poetry" && section === false) { + if (section === false) { Notifications.add( - "Error while getting poetry. Please try again later", + "Error while getting section. Please try again later", -1 ); - UpdateConfig.setFunbox("none"); + UpdateConfig.toggleFunbox(sectionFunbox.name); restart(); return; } if (section === undefined) continue; - if (section === false) continue; for (const word of section.words) { if (wordCount >= Config.words && Config.mode == "words") { @@ -1060,7 +982,9 @@ export async function init(): Promise { TestWords.words.push(word); } } - } else { + } + + if (wordCount == 0) { for (let i = 0; i < wordsBound; i++) { const randomWord = await getNextWord(wordset, language, wordsBound); @@ -1220,7 +1144,7 @@ export async function init(): Promise { // } else { TestUI.showWords(); if (Config.keymapMode === "next" && Config.mode !== "zen") { - Keymap.highlightKey( + KeymapEvent.highlight( TestWords.words .getCurrent() .substring( @@ -1236,8 +1160,11 @@ export async function init(): Promise { export async function addWord(): Promise { let bound = 100; - if (Config.funbox === "plus_one") bound = 1; - if (Config.funbox === "plus_two") bound = 2; + const funboxToPush = FunboxList.get(Config.funbox) + .find((f) => f.properties?.find((fp) => fp.startsWith("toPush"))) + ?.properties?.find((fp) => fp.startsWith("toPush:")); + const toPushCount: string | undefined = funboxToPush?.split(":")[1]; + if (toPushCount) bound = +toPushCount - 1; if ( TestWords.words.length - TestInput.input.history.length > bound || (Config.mode === "words" && @@ -1257,25 +1184,26 @@ export async function addWord(): Promise { return; } - if (Config.funbox === "wikipedia" || Config.funbox == "poetry") { + const sectionFunbox = FunboxList.get(Config.funbox).find( + (f) => f.functions?.pullSection + ); + if (sectionFunbox?.functions?.pullSection) { if (TestWords.words.length - TestWords.words.currentIndex < 20) { - const section = - Config.funbox == "wikipedia" - ? await Wikipedia.getSection(Config.language) - : await Poetry.getPoem(); + const section = await sectionFunbox.functions.pullSection( + Config.language + ); - if (Config.funbox == "poetry" && section === false) { + if (section === false) { Notifications.add( - "Error while getting poetry. Please try again later", + "Error while getting section. Please try again later", -1 ); - UpdateConfig.setFunbox("none"); + UpdateConfig.toggleFunbox(sectionFunbox.name); restart(); return; } if (section === undefined) return; - if (section === false) return; let wordCount = 0; for (const word of section.words) { @@ -1286,8 +1214,6 @@ export async function addWord(): Promise { TestWords.words.push(word); TestUI.addWord(word); } - } else { - return; } } @@ -1299,7 +1225,7 @@ export async function addWord(): Promise { ...(await Misc.getCurrentLanguage(Config.language)), words: CustomText.text, }; - const wordset = Wordset.withWords(language.words, Config.funbox); + const wordset = await Wordset.withWords(language.words); const randomWord = await getNextWord(wordset, language, bound); @@ -1551,6 +1477,7 @@ export async function finish(difficultyFailed = false): Promise { TestTimer.clear(); Funbox.clear(); Monkey.hide(); + ModesNotice.update(); //need one more calculation for the last word if test auto ended if (TestInput.burstHistory.length !== TestInput.input.getHistory().length) { @@ -2091,6 +2018,12 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => { if (eventKey === "showAllLines" && !nosave) restart(); if (eventKey === "keymapMode" && !nosave) restart(); if (eventKey === "tapeMode" && !nosave) restart(); + if ( + eventKey === "customLayoutFluid" && + Config.funbox.includes("layoutfluid") + ) { + restart(); + } } if (eventKey === "lazyMode" && eventValue === false && !nosave) { rememberLazyMode = false; diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts index 93ee926a3..7982839a7 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -3,6 +3,7 @@ import Config from "../config"; import * as Misc from "../utils/misc"; import * as TestInput from "./test-input"; import * as TestWords from "./test-words"; +import * as FunboxList from "./funbox/funbox-list"; interface CharCount { spaces: number; @@ -273,7 +274,9 @@ export function calculateWpmAndRaw(): MonkeyTypes.WordsPerMinuteAndRaw { correctWordChars += toAdd.correct; } } - if (Config.funbox === "nospace" || Config.funbox === "arrows") { + if ( + FunboxList.get(Config.funbox).find((f) => f.properties?.includes("nospace")) + ) { spaces = 0; } chars += currTestInput.length; @@ -444,7 +447,9 @@ function countChars(): CharCount { spaces++; } } - if (Config.funbox === "nospace" || Config.funbox === "arrows") { + if ( + FunboxList.get(Config.funbox).find((f) => f.properties?.includes("nospace")) + ) { spaces = 0; correctspaces = 0; } diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts index e6f6bd9d6..fa71c26af 100644 --- a/frontend/src/ts/test/test-timer.ts +++ b/frontend/src/ts/test/test-timer.ts @@ -83,7 +83,10 @@ function calculateAcc(): number { function layoutfluid(): void { if (timerDebug) console.time("layoutfluid"); - if (Config.funbox === "layoutfluid" && Config.mode === "time") { + if ( + Config.funbox.split("#").includes("layoutfluid") && + Config.mode === "time" + ) { const layouts = Config.customLayoutfluid ? Config.customLayoutfluid.split("#") : ["qwerty", "dvorak", "colemak"]; diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index ae91f0a32..51e01c28f 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -16,6 +16,7 @@ import * as Hangul from "hangul-js"; import format from "date-fns/format"; import { Auth } from "../firebase"; import { skipXpBreakdown } from "../elements/account-button"; +import * as FunboxList from "./funbox/funbox-list"; ConfigEvent.subscribe((eventKey, eventValue) => { if (eventValue === undefined || typeof eventValue !== "boolean") return; @@ -104,20 +105,12 @@ export function updateActiveElement(backspace?: boolean): void { function getWordHTML(word: string): string { let newlineafter = false; let retval = `
`; + const funbox = FunboxList.get(Config.funbox).find( + (f) => f.functions?.getWordHtml + ); for (let c = 0; c < word.length; c++) { - if (Config.funbox === "arrows") { - if (word.charAt(c) === "↑") { - retval += ``; - } - if (word.charAt(c) === "↓") { - retval += ``; - } - if (word.charAt(c) === "←") { - retval += ``; - } - if (word.charAt(c) === "→") { - retval += ``; - } + if (funbox?.functions?.getWordHtml) { + retval += funbox.functions.getWordHtml(word.charAt(c), true); } else if (word.charAt(c) === "\t") { retval += ``; } else if (word.charAt(c) === "\n") { @@ -425,6 +418,9 @@ export function updateWordElement(showError = !Config.blindMode): void { wordHighlightClassString = "correct"; } + const funbox = FunboxList.get(Config.funbox).find( + (f) => f.functions?.getWordHtml + ); for (let i = 0; i < input.length; i++) { const charCorrect = currentWord[i] == input[i]; @@ -436,18 +432,10 @@ export function updateWordElement(showError = !Config.blindMode): void { let currentLetter = currentWord[i]; let tabChar = ""; let nlChar = ""; - if (Config.funbox === "arrows") { - if (currentLetter === "↑") { - currentLetter = ``; - } - if (currentLetter === "↓") { - currentLetter = ``; - } - if (currentLetter === "←") { - currentLetter = ``; - } - if (currentLetter === "→") { - currentLetter = ``; + if (funbox?.functions?.getWordHtml) { + const cl = funbox.functions.getWordHtml(currentLetter); + if (cl != "") { + currentLetter = cl; } } else if (currentLetter === "\t") { tabChar = "tabChar"; @@ -510,19 +498,8 @@ export function updateWordElement(showError = !Config.blindMode): void { } for (let i = input.length; i < currentWord.length; i++) { - if (Config.funbox === "arrows") { - if (currentWord[i] === "↑") { - ret += ``; - } - if (currentWord[i] === "↓") { - ret += ``; - } - if (currentWord[i] === "←") { - ret += ``; - } - if (currentWord[i] === "→") { - ret += ``; - } + if (funbox?.functions?.getWordHtml) { + ret += funbox.functions.getWordHtml(currentWord[i], true); } else if (currentWord[i] === "\t") { ret += ``; } else if (currentWord[i] === "\n") { diff --git a/frontend/src/ts/test/tts.ts b/frontend/src/ts/test/tts.ts index 0abf55d17..37bc9fce7 100644 --- a/frontend/src/ts/test/tts.ts +++ b/frontend/src/ts/test/tts.ts @@ -1,6 +1,7 @@ import Config from "../config"; import * as Misc from "../utils/misc"; import * as ConfigEvent from "../observables/config-event"; +import * as TTSEvent from "../observables/tts-event"; let voice: SpeechSynthesisUtterance | undefined; @@ -38,5 +39,11 @@ ConfigEvent.subscribe((eventKey, eventValue) => { init(); } } - if (eventKey === "language" && Config.funbox === "tts") setLanguage(); + if (eventKey === "language" && Config.funbox.split("#").includes("tts")) { + setLanguage(); + } +}); + +TTSEvent.subscribe((text) => { + speak(text); }); diff --git a/frontend/src/ts/test/weak-spot.ts b/frontend/src/ts/test/weak-spot.ts index a51bfbf41..31532b129 100644 --- a/frontend/src/ts/test/weak-spot.ts +++ b/frontend/src/ts/test/weak-spot.ts @@ -1,5 +1,5 @@ import * as TestInput from "./test-input"; -import { Wordset } from "./wordset"; +import { Wordset } from "../utils/misc"; // Changes how quickly it 'learns' scores - very roughly the score for a char // is based on last perCharCount occurrences. Make it smaller to adjust faster. diff --git a/frontend/src/ts/test/wikipedia.ts b/frontend/src/ts/test/wikipedia.ts index af993ad41..a9b6821bd 100644 --- a/frontend/src/ts/test/wikipedia.ts +++ b/frontend/src/ts/test/wikipedia.ts @@ -1,16 +1,6 @@ import * as Loader from "../elements/loader"; import * as Misc from "../utils/misc"; - -export class Section { - public title: string; - public author: string; - public words: string[]; - constructor(title: string, author: string, words: string[]) { - this.title = title; - this.author = author; - this.words = words; - } -} +import { Section } from "../utils/misc"; export async function getTLD( languageGroup: MonkeyTypes.LanguageGroup diff --git a/frontend/src/ts/test/wordset.ts b/frontend/src/ts/test/wordset.ts index f074e7457..f0babff61 100644 --- a/frontend/src/ts/test/wordset.ts +++ b/frontend/src/ts/test/wordset.ts @@ -1,109 +1,18 @@ -import { randomElementFromArray, randomIntFromRange } from "../utils/misc"; +import * as FunboxList from "./funbox/funbox-list"; +import { Wordset } from "../utils/misc"; +import Config from "../config"; let currentWordset: Wordset | null = null; -let currentWordGenerator: WordGenerator | null = null; -export class Wordset { - public words: string[]; - public length: number; - constructor(words: string[]) { - this.words = words; - this.length = this.words.length; +export async function withWords(words: string[]): Promise { + const wordFunbox = FunboxList.get(Config.funbox).find( + (f) => f.functions?.withWords + ); + if (wordFunbox?.functions?.withWords) { + return wordFunbox.functions.withWords(words); } - - public randomWord(): string { - return randomElementFromArray(this.words); - } -} - -const prefixSize = 2; - -class CharDistribution { - public chars: { [char: string]: number }; - public count: number; - constructor() { - this.chars = {}; - this.count = 0; - } - - public addChar(char: string): void { - this.count++; - if (char in this.chars) { - this.chars[char]++; - } else { - this.chars[char] = 1; - } - } - - public randomChar(): string { - const randomIndex = randomIntFromRange(0, this.count - 1); - let runningCount = 0; - for (const [char, charCount] of Object.entries(this.chars)) { - runningCount += charCount; - if (runningCount > randomIndex) { - return char; - } - } - - return Object.keys(this.chars)[0]; - } -} - -class WordGenerator extends Wordset { - public ngrams: { [prefix: string]: CharDistribution } = {}; - constructor(words: string[]) { - super(words); - // Can generate an unbounded number of words in theory. - this.length = Infinity; - - for (let word of words) { - // Mark the end of each word with a space. - word += " "; - let prefix = ""; - for (const c of word) { - // Add `c` to the distribution of chars that can come after `prefix`. - if (!(prefix in this.ngrams)) { - this.ngrams[prefix] = new CharDistribution(); - } - this.ngrams[prefix].addChar(c); - prefix = (prefix + c).substr(-prefixSize); - } - } - } - - public override randomWord(): string { - let word = ""; - for (;;) { - const prefix = word.substr(-prefixSize); - const charDistribution = this.ngrams[prefix]; - if (!charDistribution) { - // This shouldn't happen if this.ngrams is complete. If it does - // somehow, start generating a new word. - word = ""; - continue; - } - // Pick a random char from the distribution that comes after `prefix`. - const nextChar = charDistribution.randomChar(); - if (nextChar == " ") { - // A space marks the end of the word, so stop generating and return. - break; - } - word += nextChar; - } - return word; - } -} - -export function withWords(words: string[], funbox: string): Wordset { - if (funbox == "pseudolang") { - if (currentWordGenerator == null || words !== currentWordGenerator.words) { - currentWordGenerator = new WordGenerator(words); - } - return currentWordGenerator; - } else { - if (currentWordset == null || words !== currentWordset.words) { - currentWordset = new Wordset(words); - } - return currentWordset; + if (currentWordset == null || words !== currentWordset.words) { + currentWordset = new Wordset(words); } + return currentWordset; } diff --git a/frontend/src/ts/types/types.d.ts b/frontend/src/ts/types/types.d.ts index 34c6eddb9..d4addaaa1 100644 --- a/frontend/src/ts/types/types.d.ts +++ b/frontend/src/ts/types/types.d.ts @@ -1,3 +1,5 @@ +type typesSeparatedWithHash = T | `${T}#${typesSeparatedWithHash}`; + declare namespace MonkeyTypes { type Difficulty = "normal" | "expert" | "master"; @@ -141,8 +143,6 @@ declare namespace MonkeyTypes { type MinimumBurst = "off" | "fixed" | "flex"; - type FunboxObjectType = "script" | "style"; - type IndicateTypos = "off" | "below" | "replace"; type CustomLayoutFluid = `${string}#${string}#${string}`; @@ -183,12 +183,65 @@ declare namespace MonkeyTypes { display?: string; } - interface FunboxObject { + type FunboxProperty = + | "symmetricChars" + | "conflictsWithSymmetricChars" + | "changesWordsVisibility" + | "speaks" + | "unspeakable" + | "changesLayout" + | "ignoresLayout" + | "usesLayout" + | "ignoresLanguage" + | "noLigatures" + | "noLetters" + | "changesCapitalisation" + | "nospace" + | `toPush:${number}` + | "noInfiniteDuration"; + + interface FunboxFunctions { + getWord?: (wordset?: Misc.Wordset) => string; + punctuateWord?: (word: string) => string; + withWords?: (words?: string[]) => Promise; + alterText?: (word: string) => string; + applyCSS?: () => void; + applyConfig?: () => void; + rememberSettings?: () => void; + toggleScript?: (params: string[]) => void; + pullSection?: (language?: string) => Promise; + handleSpace?: () => void; + handleChar?: (char: string) => string; + isCharCorrect?: (char: string, originalChar: string) => boolean; + preventDefaultEvent?: ( + event: JQuery.KeyDownEvent + ) => Promise; + handleKeydown?: ( + event: JQuery.KeyDownEvent + ) => Promise; + getResultContent?: () => string; + start?: () => void; + restart?: () => void; + getWordHtml?: (char: string, letterTag?: boolean) => string; + } + + interface FunboxForcedConfig { + [key: string]: ConfigValues[]; + // punctuation?: boolean; + // numbers?: boolean; + // highlightMode?: typesSeparatedWithHash; + // words?: FunboxModeDuration; + // time?: FunboxModeDuration; + } + + interface FunboxMetadata { name: string; - type: FunboxObjectType; info: string; + canGetPb?: boolean; alias?: string; - affectsWordGeneration?: boolean; + forcedConfig?: MonkeyTypes.FunboxForcedConfig; + properties?: FunboxProperty[]; + functions?: FunboxFunctions; } interface CustomText { @@ -686,7 +739,6 @@ declare namespace MonkeyTypes { exec?: (input?: string) => void; hover?: () => void; available?: () => void; - beforeSubgroup?: () => void; shouldFocusTestUI?: boolean; } @@ -694,6 +746,7 @@ declare namespace MonkeyTypes { title: string; configKey?: keyof Config; list: Command[]; + beforeList?: () => void; } interface Theme { diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 8d6cd25a4..d3b4dd1ba 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -151,15 +151,15 @@ export async function findCurrentGroup( return retgroup; } -let funboxList: MonkeyTypes.FunboxObject[] | undefined; -export async function getFunboxList(): Promise { +let funboxList: MonkeyTypes.FunboxMetadata[] | undefined; +export async function getFunboxList(): Promise { if (!funboxList) { - let list = await cachedFetchJson( + let list = await cachedFetchJson( "/./funbox/_list.json" ); list = list.sort(function ( - a: MonkeyTypes.FunboxObject, - b: MonkeyTypes.FunboxObject + a: MonkeyTypes.FunboxMetadata, + b: MonkeyTypes.FunboxMetadata ) { const nameA = a.name.toLowerCase(); const nameB = b.name.toLowerCase(); @@ -176,8 +176,8 @@ export async function getFunboxList(): Promise { export async function getFunbox( funbox: string -): Promise { - const list: MonkeyTypes.FunboxObject[] = await getFunboxList(); +): Promise { + const list: MonkeyTypes.FunboxMetadata[] = await getFunboxList(); return list.find(function (element) { return element.name == funbox; }); @@ -403,6 +403,10 @@ export function capitalizeFirstLetterOfEachWord(str: string): string { .join(" "); } +export function capitalizeFirstLetter(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + export function isASCIILetter(c: string): boolean { return c.length === 1 && /[a-z]/i.test(c); } @@ -1285,6 +1289,30 @@ export function memoizeAsync Promise>( }) as T; } +export class Wordset { + public words: string[]; + public length: number; + constructor(words: string[]) { + this.words = words; + this.length = this.words.length; + } + + public randomWord(): string { + return randomElementFromArray(this.words); + } +} + +export class Section { + public title: string; + public author: string; + public words: string[]; + constructor(title: string, author: string, words: string[]) { + this.title = title; + this.author = author; + this.words = words; + } +} + export function isPasswordStrong(password: string): boolean { const hasCapital = !!password.match(/[A-Z]/); const hasNumber = !!password.match(/[\d]/); @@ -1301,8 +1329,24 @@ export function areSortedArraysEqual(a: unknown[], b: unknown[]): boolean { return a.length === b.length && a.every((v, i) => v === b[i]); } +export function intersect(a: T[], b: T[], removeDuplicates = false): T[] { + let t; + if (b.length > a.length) (t = b), (b = a), (a = t); // indexOf to loop over shorter + const filtered = a.filter(function (e) { + return b.indexOf(e) > -1; + }); + return removeDuplicates ? [...new Set(filtered)] : filtered; +} + export function htmlToText(html: string): string { const el = document.createElement("div"); el.innerHTML = html; return el.textContent || el.innerText || ""; } + +export function camelCaseToWords(str: string): string { + return str + .replace(/([A-Z])/g, " $1") + .trim() + .toLowerCase(); +} diff --git a/frontend/static/funbox/_list.json b/frontend/static/funbox/_list.json index b6be2480d..a95fec8e0 100644 --- a/frontend/static/funbox/_list.json +++ b/frontend/static/funbox/_list.json @@ -1,148 +1,138 @@ [ { "name": "nausea", - "type": "style", - "info": "I think I'm gonna be sick." + "info": "I think I'm gonna be sick.", + "canGetPb": true }, { "name": "round_round_baby", - "type": "style", - "info": "...right round, like a record baby. Right, round round round." + "info": "...right round, like a record baby. Right, round round round.", + "canGetPb": true }, { "name": "simon_says", - "type": "style", - "info": "Type what simon says." + "info": "Type what simon says.", + "canGetPb": true }, { "name": "mirror", - "type": "style", - "info": "Everything is mirrored!" + "info": "Everything is mirrored!", + "canGetPb": true }, { "name": "tts", - "type": "script", - "info": "Listen closely." + "info": "Listen closely.", + "canGetPb": true }, { "name": "choo_choo", - "type": "style", - "info": "All the letters are spinning!" + "info": "All the letters are spinning!", + "canGetPb": true }, { "name": "arrows", - "type": "script", - "affectsWordGeneration": true, - "info": "Eurobeat Intensifies..." + "info": "Eurobeat Intensifies...", + "canGetPb": false }, { "name": "rAnDoMcAsE", - "type": "script", - "info": "I kInDa LiKe HoW iNeFfIcIeNt QwErTy Is." + "info": "I kInDa LiKe HoW iNeFfIcIeNt QwErTy Is.", + "canGetPb": false }, { "name": "capitals", - "type": "script", - "info": "Capitalize Every Word." + "info": "Capitalize Every Word.", + "canGetPb": false }, { "name": "layoutfluid", - "type": "script", - "info": "Switch between layouts specified below proportionately to the length of the test." + "info": "Switch between layouts specified below proportionately to the length of the test.", + "canGetPb": true }, { "name": "earthquake", - "type": "style", - "info": "Everybody get down! The words are shaking!" + "info": "Everybody get down! The words are shaking!", + "canGetPb": true }, { "name": "space_balls", - "type": "style", - "info": "In a galaxy far far away." + "info": "In a galaxy far far away.", + "canGetPb": true }, { "name": "gibberish", - "type": "script", - "affectsWordGeneration": true, - "info": "Anvbuefl dizzs eoos alsb?" + "info": "Anvbuefl dizzs eoos alsb?", + "canGetPb": false }, { "name": "58008", - "type": "script", "alias": "numbers", - "affectsWordGeneration": true, - "info": "A special mode for accountants." + "info": "A special mode for accountants.", + "canGetPb": false }, { "name": "ascii", - "type": "script", - "affectsWordGeneration": true, - "info": "Where was the ampersand again?. Only ASCII characters." + "info": "Where was the ampersand again?. Only ASCII characters.", + "canGetPb": false }, { "name": "specials", - "type": "script", - "affectsWordGeneration": true, - "info": "!@#$%^&*. Only special characters." + "info": "!@#$%^&*. Only special characters.", + "canGetPb": false }, { "name": "plus_one", - "type": "script", - "info": "React quickly! Only one future word is visible." + "info": "React quickly! Only one future word is visible.", + "canGetPb": true }, { "name": "plus_two", - "type": "script", - "info": "Only two future words are visible." + "info": "Only two future words are visible.", + "canGetPb": true }, { "name": "read_ahead_easy", - "type": "style", - "info": "Only the current word is invisible." + "info": "Only the current word is invisible.", + "canGetPb": true }, { "name": "read_ahead", - "type": "style", - "info": "Current and the next word are invisible!" + "info": "Current and the next word are invisible!", + "canGetPb": true }, { "name": "read_ahead_hard", - "type": "style", - "info": "Current and the next two words are invisible!" + "info": "Current and the next two words are invisible!", + "canGetPb": true }, { "name": "memory", - "type": "script", - "info": "Test your memory. Remember the words and type them blind." + "info": "Test your memory. Remember the words and type them blind.", + "canGetPb": true }, { "name": "nospace", - "type": "script", - "info": "Whoneedsspacesanyway?" + "info": "Whoneedsspacesanyway?", + "canGetPb": false }, { "name": "poetry", - "type": "script", - "affectsWordGeneration": true, - "info": "Practice typing some beautiful prose." + "info": "Practice typing some beautiful prose.", + "canGetPb": false }, { "name": "wikipedia", - "type": "script", - "affectsWordGeneration": true, - "info": "Practice typing wikipedia sections." + "info": "Practice typing wikipedia sections.", + "canGetPb": false }, { "name": "weakspot", - "type": "script", - "affectsWordGeneration": true, - "info": "Focus on slow and mistyped letters." + "info": "Focus on slow and mistyped letters.", + "canGetPb": false }, { "name": "pseudolang", - "type": "script", - "affectsWordGeneration": true, - "info": "Nonsense words that look like the current language." + "info": "Nonsense words that look like the current language.", + "canGetPb": false } ] -