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 <jack@monkeytype.com>
This commit is contained in:
egorguslyan 2022-11-30 12:57:48 -03:00 committed by GitHub
parent d45b487192
commit 588d14a2b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 2290 additions and 1142 deletions

View file

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

View file

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

View file

@ -0,0 +1,112 @@
const Funboxes: Record<string, MonkeyTypes.FunboxMetadata> = {
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;

View file

@ -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<MonkeyTypes.Mode>
): Promise<boolean> {
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 [];

View file

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

View file

@ -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<MonkeyTypes.Mode>;
export function canFunboxGetPb(
result: MonkeyTypes.Result<MonkeyTypes.Mode>
): 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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 =
'<i class="fas fa-fw fa-check"></i>' + funbox.name.replace(/_/g, " ");
} else {
dis = '<i class="fas fa-fw"></i>' + 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 = '<i class="fas fa-fw fa-check"></i>' + txt;
} else {
txt = '<i class="fas fa-fw"></i>' + txt;
}
if ($("#commandLine").hasClass("allCommands")) {
$(
`#commandLine .suggestions .entry[command='changeFunbox${funboxes[i].name}']`
).html(
`<div class="icon"><i class="fas fa-fw fa-gamepad"></i></div><div>Funbox > ` +
txt
);
} else {
$(
`#commandLine .suggestions .entry[command='changeFunbox${funboxes[i].name}']`
).html(txt);
}
}
if (funboxes.length > 0) {
const noneTxt =
Config.funbox === "none"
? `<i class="fas fa-fw fa-check"></i>none`
: `<i class="fas fa-fw"></i>none`;
if ($("#commandLine").hasClass("allCommands")) {
$(
`#commandLine .suggestions .entry[command='changeFunboxNone']`
).html(
`<div class="icon"><i class="fas fa-fw fa-gamepad"></i></div><div>Funbox > ` +
noneTxt
);
} else {
$(
`#commandLine .suggestions .entry[command='changeFunboxNone']`
).html(noneTxt);
}
}
},
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<M extends MonkeyTypes.Mode>(
lazyMode: boolean,
funbox: string
): Promise<number> {
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;
}

View file

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

View file

@ -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<void> {
async function flashKey(key: string, correct?: boolean): Promise<void> {
if (key == undefined) return;
//console.log("key", key);
if (key == " ") {
@ -51,41 +52,38 @@ export async function flashKey(key: string, correct: boolean): Promise<void> {
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);
}
});

View file

@ -182,10 +182,9 @@ export async function update(): Promise<void> {
if (Config.funbox !== "none") {
$(".pageTest #testModesNotice").append(
`<div class="textButton" commands="funbox"><i class="fas fa-gamepad"></i>${Config.funbox.replace(
/_/g,
" "
)}</div>`
`<div class="textButton" commands="funbox"><i class="fas fa-gamepad"></i>${Config.funbox
.replace(/_/g, " ")
.replace(/#/g, ", ")}</div>`
);
}

View file

@ -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<void> {
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<void> {
subscribers.forEach((fn) => {
try {
fn("highlight", key);
} catch (e) {
console.error("Keymap highlight event subscriber threw an error");
console.error(e);
}
});
}

View file

@ -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<void> {
subscribers.forEach((fn) => {
try {
fn(text);
} catch (e) {
console.error("TTS event subscriber threw an error");
console.error(e);
}
});
}

View file

@ -88,10 +88,12 @@ function loadMoreLines(lineIndex?: number): void {
}
if (result.funbox !== "none" && result.funbox !== undefined) {
icons += `<span aria-label="${result.funbox.replace(
/_/g,
" "
)}" data-balloon-pos="up"><i class="fas fa-gamepad"></i></span>`;
icons += `<span aria-label="${result.funbox
.replace(/_/g, " ")
.replace(
/#/g,
", "
)}" data-balloon-pos="up"><i class="fas fa-gamepad"></i></span>`;
}
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);
}

View file

@ -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<void> {
funboxEl.append(
`<div class="funbox button" funbox='${funbox.name}' aria-label="${
funbox.info
}" data-balloon-pos="up" data-balloon-length="fit" type="${
funbox.type
}" style="transform:scaleX(-1);">${funbox.name.replace(
}" data-balloon-pos="up" data-balloon-length="fit" style="transform:scaleX(-1);">${funbox.name.replace(
/_/g,
" "
)}</div>`
@ -537,9 +536,10 @@ export async function fillSettingsPage(): Promise<void> {
funboxEl.append(
`<div class="funbox button" funbox='${funbox.name}' aria-label="${
funbox.info
}" data-balloon-pos="up" data-balloon-length="fit" type="${
funbox.type
}">${funbox.name.replace(/_/g, " ")}</div>`
}" data-balloon-pos="up" data-balloon-length="fit">${funbox.name.replace(
/_/g,
" "
)}</div>`
);
}
});
@ -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 = <string>$(e.currentTarget).attr("funbox");
const type = <MonkeyTypes.FunboxObjectType>$(e.currentTarget).attr("type");
Funbox.setFunbox(funbox, type);
toggleFunbox(funbox);
setActiveFunboxButton();
});

View file

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

View file

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

View file

@ -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<boolean> {
$("#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<boolean | undefined> {
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<void> {
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);
}
}
}

View file

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

View file

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

View file

@ -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<MonkeyTypes.ConfigValues>;
} {
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<string, MonkeyTypes.ConfigValues[]> = {};
// 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(<MonkeyTypes.ConfigValues>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
);
}

View file

@ -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<Document, null, Document, Document>
): Promise<boolean> {
// 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 += `<letter>`;
retval += `<i class="fas fa-arrow-up"></i>`;
if (letterTag) retval += `</letter>`;
}
if (char === "↓") {
if (letterTag) retval += `<letter>`;
retval += `<i class="fas fa-arrow-down"></i>`;
if (letterTag) retval += `</letter>`;
}
if (char === "←") {
if (letterTag) retval += `<letter>`;
retval += `<i class="fas fa-arrow-left"></i>`;
if (letterTag) retval += `</letter>`;
}
if (char === "→") {
if (letterTag) retval += `<letter>`;
retval += `<i class="fas fa-arrow-right"></i>`;
if (letterTag) retval += `</letter>`;
}
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<Misc.Section | false> {
return getPoem();
},
});
FunboxList.setFunboxFunctions("wikipedia", {
async pullSection(lang?: string): Promise<Misc.Section | false> {
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<Misc.Wordset> {
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<boolean> {
$("#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<boolean | undefined> {
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<void> {
FunboxList.get(Config.funbox).forEach(async (funbox) => {
if (funbox.functions?.rememberSettings) funbox.functions.rememberSettings();
});
}

View file

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

View file

@ -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<Poem | false> {
export async function getPoem(): Promise<Section | false> {
console.log("Getting poem");
try {

View file

@ -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<void> {
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 += "<br>" + result.language.replace(/_/g, " ");
}
if (Config.punctuation) {
@ -545,7 +550,7 @@ function updateTestType(randomQuote: MonkeyTypes.Quote): void {
testType += "<br>lazy";
}
if (Config.funbox !== "none") {
testType += "<br>" + Config.funbox.replace(/_/g, " ");
testType += "<br>" + Config.funbox.replace(/_/g, " ").replace(/#/g, ", ");
}
if (Config.difficulty == "expert") {
testType += "<br>expert";

View file

@ -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 {
(<HTMLElement>document.querySelector("#liveAcc")).innerHTML = "100%";
(<HTMLElement>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<string> {
@ -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<void> {
}
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<void> {
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<void> {
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<void> {
// } 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<void> {
export async function addWord(): Promise<void> {
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<void> {
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<void> {
TestWords.words.push(word);
TestUI.addWord(word);
}
} else {
return;
}
}
@ -1299,7 +1225,7 @@ export async function addWord(): Promise<void> {
...(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<void> {
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;

View file

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

View file

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

View file

@ -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 = `<div class='word'>`;
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 += `<letter><i class="fas fa-arrow-up"></i></letter>`;
}
if (word.charAt(c) === "↓") {
retval += `<letter><i class="fas fa-arrow-down"></i></letter>`;
}
if (word.charAt(c) === "←") {
retval += `<letter><i class="fas fa-arrow-left"></i></letter>`;
}
if (word.charAt(c) === "→") {
retval += `<letter><i class="fas fa-arrow-right"></i></letter>`;
}
if (funbox?.functions?.getWordHtml) {
retval += funbox.functions.getWordHtml(word.charAt(c), true);
} else if (word.charAt(c) === "\t") {
retval += `<letter class='tabChar'><i class="fas fa-long-arrow-alt-right"></i></letter>`;
} 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 = `<i class="fas fa-arrow-up"></i>`;
}
if (currentLetter === "↓") {
currentLetter = `<i class="fas fa-arrow-down"></i>`;
}
if (currentLetter === "←") {
currentLetter = `<i class="fas fa-arrow-left"></i>`;
}
if (currentLetter === "→") {
currentLetter = `<i class="fas fa-arrow-right"></i>`;
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 += `<letter><i class="fas fa-arrow-up"></i></letter>`;
}
if (currentWord[i] === "↓") {
ret += `<letter><i class="fas fa-arrow-down"></i></letter>`;
}
if (currentWord[i] === "←") {
ret += `<letter><i class="fas fa-arrow-left"></i></letter>`;
}
if (currentWord[i] === "→") {
ret += `<letter><i class="fas fa-arrow-right"></i></letter>`;
}
if (funbox?.functions?.getWordHtml) {
ret += funbox.functions.getWordHtml(currentWord[i], true);
} else if (currentWord[i] === "\t") {
ret += `<letter class='tabChar'><i class="fas fa-long-arrow-alt-right"></i></letter>`;
} else if (currentWord[i] === "\n") {

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,5 @@
type typesSeparatedWithHash<T> = T | `${T}#${typesSeparatedWithHash<T>}`;
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<Misc.Wordset>;
alterText?: (word: string) => string;
applyCSS?: () => void;
applyConfig?: () => void;
rememberSettings?: () => void;
toggleScript?: (params: string[]) => void;
pullSection?: (language?: string) => Promise<Misc.Section | false>;
handleSpace?: () => void;
handleChar?: (char: string) => string;
isCharCorrect?: (char: string, originalChar: string) => boolean;
preventDefaultEvent?: (
event: JQuery.KeyDownEvent<Document, null, Document, Document>
) => Promise<boolean>;
handleKeydown?: (
event: JQuery.KeyDownEvent<Document, null, Document, Document>
) => Promise<void>;
getResultContent?: () => string;
start?: () => void;
restart?: () => void;
getWordHtml?: (char: string, letterTag?: boolean) => string;
}
interface FunboxForcedConfig {
[key: string]: ConfigValues[];
// punctuation?: boolean;
// numbers?: boolean;
// highlightMode?: typesSeparatedWithHash<HighlightMode>;
// 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 {

View file

@ -151,15 +151,15 @@ export async function findCurrentGroup(
return retgroup;
}
let funboxList: MonkeyTypes.FunboxObject[] | undefined;
export async function getFunboxList(): Promise<MonkeyTypes.FunboxObject[]> {
let funboxList: MonkeyTypes.FunboxMetadata[] | undefined;
export async function getFunboxList(): Promise<MonkeyTypes.FunboxMetadata[]> {
if (!funboxList) {
let list = await cachedFetchJson<MonkeyTypes.FunboxObject[]>(
let list = await cachedFetchJson<MonkeyTypes.FunboxMetadata[]>(
"/./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<MonkeyTypes.FunboxObject[]> {
export async function getFunbox(
funbox: string
): Promise<MonkeyTypes.FunboxObject | undefined> {
const list: MonkeyTypes.FunboxObject[] = await getFunboxList();
): Promise<MonkeyTypes.FunboxMetadata | undefined> {
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<T extends (...args: any) => Promise<any>>(
}) 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<T>(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();
}

View file

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