e-typing chobun plus

ワードの表示・打ち切り回数保存、任意の文字間のリザルト・リプレイ再生

目前為 2025-03-23 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         e-typing chobun plus
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  ワードの表示・打ち切り回数保存、任意の文字間のリザルト・リプレイ再生
// @author       tai
// @license MIT
// @match        https://www.e-typing.ne.jp/app*
// @exclude      https://www.e-typing.ne.jp/app/ad*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=e-typing.ne.jp
// @require      https://update.greasyfork.org/scripts/530545/1558131/keyboardevent-chobun.js
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

$(document).one("loadComplete", (_, setting) => {
    let type = setting.querySelector("type").textContent;
    if (type === "4" || type === "255") {
        console.log("(`・▿・´ )ノ");

        let snsURL = setting.querySelector("snsURL").textContent;
        let title = setting.querySelector("title").textContent;
        let mode = snsURL.includes("english") ? "E" : snsURL.includes("kana") ? "K" : "R";
        new Chobun(title, mode);
    } else {
        console.log("_(- ᴗ -_)_ …zzz");
    }
})



class Chobun {
    constructor(title, mode){
        this.title = title;
        this.mode = mode;

        this.chobun = JSON.parse(GM_getValue("chobun", "{}"));
        this.words = this.chobun[this.title]?.[this.mode]?.words ?? {};
        this.focusWords = [];

        this.wordList = new WordList(this.title, this.mode, this.chobun, this.words, this.focusWords);

        this.insertStyle();

        $(document).on({
            "end_countdown.etyping": this.start,
            "replay": () => {
                Result.clear();
                Replay.clear();
                Typing.clear();
                $(document).on("end_countdown.etyping", this.start);
            }
        });

        window.addEventListener("beforeunload", this.close);
        parent.$pp_overlay && (parent.$pp_overlay.fadeOut = (a,c,d) => {this.close(); return parent.$pp_overlay.animate({opacity:"hide"},a,c,d)});
    }

    start = () => {
        let typingStartTime = performance.now();
        let time, char;
        Typing.data.push({ char: null, index: null, time: null }); //logで見やすく
        this.replayFlag = false;

        const handleKeydown = e => {
            time = e.timeStamp - typingStartTime;
            char = this.mode === "K" ? e.kana : this.mode === "R" ? e.char.toUpperCase() : e.char;
        };

        document.addEventListener("keydown", handleKeydown);

        let index = 0;
        $(document).on({
            ["correct.etyping error.etyping"]: e => {
                let charData = Typing.data.at(-1);

                if (e.type === "correct") {
                    charData.index = index;
                    charData.char = char;
                    charData.time = time;
                    index++;
                } else {
                    charData.index = index;
                    charData.char = char;
                    charData.time = time;
                    charData.isMiss = true;
                }

                Typing.data.push({ char: null, index: null, time: null });
            },
            ["complete.etyping interrupt.etyping"]: e => {
                let charData = Typing.data.at(-1);
                charData.index = index;
                charData.char = char;
                charData.time = time;
                charData.isInterrupt = charData.isInterrupt = e.type === "interrupt";

                document.removeEventListener("keydown", handleKeydown);

                console.log(Typing.data);
                setTimeout(() => this.end(e.type));
            }
        })

        setTimeout(() => {
            this.word = this.mode !== "E" ? document.getElementById("exampleText").textContent : document.getElementById("sentenceText").textContent.replace(/␣/g," ");

            this.words = this.wordList.add(this.word, "show");

            if (this.focusWords.length && !this.focusWords.includes(this.word)) {
                this.replayFlag = true;
                $(document).trigger("interrupt.etyping");
            }
        })
    }

    end(type){
        const resultObserver = new MutationObserver(() => {
            if (document.getElementsByClassName("result_data").length) {
                if (this.replayFlag) {
                    resultObserver.disconnect();
                    return $(document).trigger("replay");
                }

                if (type === "complete") {
                    this.wordList.add(this.word, type);
                }

                Result.init(this.mode);

                resultObserver.disconnect();
            }
        })

        resultObserver.observe(document.getElementById("result"), { childList: true });
    }

    insertStyle(){
        document.head.insertAdjacentHTML("afterbegin",`<style>
            #exampleList {
                width: 371px !important;
            }

            .entered {
                color: #ffd0a6;
            }

            .sentence {
                font-size: 20px;
                font-family: "Consolas", "Cascadia Mono", "Menlo", "DejaVu Sans Mono", monospace;
                line-break: anywhere;
            }

            .sentence span {
                cursor: pointer;
            }

            .sentence .hover {
                outline: 1px solid #000000;
            }

            .sentence .selected {
                background-color: rgba(5, 127, 255, 0.8);
                outline: 1px solid #0000ff;
            }

            .result_data.fixed {
                background-color: rgba(255, 255, 0, 0.5) !important;
            }
        </style>`);
    }

    close(){
        parent.document.getElementById("word_list").remove();
    }
}



class Result {
    static init(mode){
        this.mode = mode;

        this.sentence = document.getElementsByClassName("sentence")[0];
        !document.getElementById("latency") && this.plus(Typing.data);

        this.prev = document.getElementById("prev");
        this.savePrev = this.prev.innerHTML;

        this.sentence.title = "クリックでこの文字を固定(もう一度押して解除)\n\nショートカット:\n[s] リザルトを固定 (もう一度押して解除)\n[a] リプレイ再生\n[Escape] リプレイ停止、リザルト画面初期化";
        [...this.sentence.children].forEach(e => e.textContent = e.textContent === " " ? "_" : e.textContent);
        this.mode === "K" && (this.sentence.style.fontSize = "16px");

        Replay.init();

        this.fixed = false;
        this.selected = null;
        if (Typing.latestIndex()) {
            this.sentence.addEventListener("click", e => e.target.matches(".sentence span") && !this.fixed && this.#sentenceClick(e));
            this.sentence.addEventListener("mouseover", e => e.target.matches(".sentence span") && !this.fixed && this.#sentenceMouseOver(e));
            this.sentence.addEventListener("mouseleave", e => !this.fixed && this.#sentenceMouseLeave(e));

            document.addEventListener("keydown", this.#handleKeydown);
            parent.document.addEventListener("keydown", this.#handleKeydown);
        }
    }

    static plus(typingData){
        document.getElementById("app").style.height = "502px";
        document.querySelector("#result article").style.height = "452px";
        document.getElementById("current").style.height = "367px";
        document.getElementById("prev").style.height = "367px";
        document.getElementById("exampleList").style.height = "284px";
        document.querySelectorAll(".result_data").forEach(e => { e.children[0].children[7].remove(); e.style.height = "318px" });


        document.getElementsByClassName("result_data")[1].children[0].insertAdjacentHTML("beforeend", `<li id="previous_latency"><div class="data">${this.latency === undefined ? "-" : (this.latency / 1000).toFixed(3)}</div></li><li id="previous_rkpm"><div class="data">${this.rkpm === undefined ? "-" : this.rkpm.toFixed(2)}</div></li>`);

        this.latency = Typing.latency();
        this.rkpm = Typing.rkpm();
        document.getElementsByClassName("result_data")[0].children[0].insertAdjacentHTML("beforeend", `<li id="latency"><div class="title">Latency</div><div class="data">${(this.latency / 1000).toFixed(3)}</div></li><li id="rkpm"><div class="title">RKPM</div><div class="data">${this.rkpm.toFixed(2)}</div></li>`);


        this.sentence.innerHTML = this.sentence.textContent.split("").map((char, i) => {
            let charData = Typing.data.findLast(e => e.index === i);
            let isMiss = Typing.data.some(e => e.index === i && e.isMiss);

            return !charData || charData.isInterrupt ? `<span style="opacity: 0.6; display: inline;">${char}</span>` : isMiss ? `<span class="miss">${char}</span>` : `<span>${char}</span>`;
        }).join("");
    }

    static show(start, end, indexBreak = true){
        start = Math.max(0, start);
        end = indexBreak ? Math.min(Typing.latestIndex() - (Typing.data.at(-1).isInterrupt ? 1 : 0), end) : end;

        const data = Typing.result(start, end, indexBreak);

        document.querySelector("#prev h1").textContent = indexBreak ? `${start + 1}~${end + 1}まで` : `${data.typingCount}${data.missTypeCount ? " (" + data.missTypeCount + ")文字" : "文字"}`;
        let prevRsltElem = document.getElementsByClassName("result_data")[1].getElementsByClassName("data");
        prevRsltElem[0].textContent = data.score.toFixed(2);
        prevRsltElem[1].textContent = data.level;
        prevRsltElem[2].textContent = data.inputTime;
        prevRsltElem[3].textContent = data.typingCount;
        prevRsltElem[4].textContent = data.missTypeCount;
        prevRsltElem[5].textContent = data.wpm.toFixed(2);
        prevRsltElem[6].textContent = (data.correctRate / 100).toFixed(2) + "%";
        prevRsltElem[7].textContent = (data.latency / 1000).toFixed(3);
        prevRsltElem[8].textContent = data.rkpm.toFixed(2);
    }

    static #sentenceClick = e => {
        let sentences = [...e.target.parentNode.children];

        if (this.selected === null) {
            this.selected = Math.min(Typing.latestIndex() - (Typing.data.at(-1).isInterrupt ? 1 : 0), sentences.indexOf(e.target));
            this.show(this.selected, this.selected);
            sentences[this.selected].classList.add("selected");
        } else {
            this.selected = null;
            this.show(0, sentences.indexOf(e.target));
            document.getElementsByClassName("selected")[0]?.classList.remove("selected");
        }
    }

    static #sentenceMouseOver = e => {
        let sentences = [...e.target.parentNode.children];
        let targetIndex = sentences.indexOf(e.target);

        document.getElementsByClassName("hover")[0]?.classList.remove("hover");
        sentences[Math.min(Typing.latestIndex() - (Typing.data.at(-1).isInterrupt ? 1 : 0), targetIndex)].classList.add("hover");

        let [start, end] = [this.selected || 0, targetIndex].sort((a, b) => a - b);
        this.show(start, end);
    }

    static #sentenceMouseLeave = e => {
        if (e.relatedTarget?.className !== "time-tooltip" && e.relatedTarget?.parentElement.className !== "time-tooltip") {
            this.selected = null;
            document.getElementsByClassName("hover")[0]?.classList.remove("hover");
            document.getElementsByClassName("selected")[0]?.classList.remove("selected");

            this.prev.innerHTML = this.savePrev;
        }
    }

    static #handleKeydown = e => {
        switch (e.key) {
            case "s":
                this.fixed && this.selected && (this.selected = null, document.getElementsByClassName("selected")[0]?.classList.remove("selected"));
                this.fixed && document.getElementsByClassName("hover")[0]?.classList.remove("hover");


                this.fixed = !this.fixed;
                document.getElementsByClassName("result_data")[1].classList.toggle("fixed");
                break;
        }
    }

    static clear(){
        this.prev = null;
        this.savePrev = null;

        document.removeEventListener("keydown", this.#handleKeydown);
        parent.document.removeEventListener("keydown", this.#handleKeydown);
    }
}

class WordList {
    constructor(title, mode, chobun, words, focusWords){
        this.title = title;
        this.mode = mode;

        this.chobun = chobun;
        this.words = words;
        this.focusWords = focusWords;

        this.pDoc = parent.document;
        this.insert();
    }

    insert(){
        let top = parent.scrollY + 137.5;
        let left = this.pDoc.documentElement.clientWidth / 2 - 374;

        this.pDoc.body.insertAdjacentHTML("afterbegin",`
            <table id="word_list" style="top: ${top + 371 + 90}px; left: ${left + 10 + 57.5 + 608 * 3 / 4}px;">
                <tbody id="words"></tbody>
            </table>`);

        this.pDoc.head.insertAdjacentHTML("afterbegin",`
            <style>
                #word_list {
                    position: absolute;
                    z-index: 15000;
                    color: black;
                    padding: 5px;
                    background-color: rgba(5, 127, 255, 0.8);
                    outline: 1px solid #0000ff;
                    box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5);
                    border-radius: 10px;
                    border-collapse: separate;
                    user-select: none;
                }

                #word_list:hover {
                    cursor: grab;
                }

                #word_list:active {
                    cursor: grabbing;
                }

                #words td {
                    color: black;
                    max-width: 300px;
                    height: 20px;
                    line-height: 2;
                    padding-left: 5px;
                    white-space: nowrap;
                    overflow: hidden;
                    text-overflow: ellipsis;
                }

                .focus_word {
                    outline: 1px solid aqua;
                    background-color: rgba(127, 255, 212, 0.5);
                    border-radius: 10px;
                }

                .highlight::after {
                    content: "";
                    position: absolute;
                    animation: pathmove 3s ease-in-out infinite;
                    opacity: 0;
                    box-shadow: 0px -3px 1px 1px rgba(222, 222, 7, 0.9);
                }

                @keyframes pathmove {
                    0% { width: 0; left: 0; opacity: 0; }
                    10% { opacity: 1; }
                    30% { width: 200px; }
                    70% { left: 60%; opacity: 1; }
                    100% { width: 30px; left: 60%; opacity: 0; }
                }
            `);



        this.wordList = this.pDoc.getElementById("word_list");

        this.wordList.addEventListener("pointermove", function(e){
            if (e.buttons) {
                this.style.left = this.offsetLeft + e.movementX + "px";
                this.style.top = this.offsetTop + e.movementY + "px";
                this.setPointerCapture(e.pointerId);
            }
        });

        this.wordList.addEventListener("click", e => {
            if (e.target.matches("td") && [...e.target.classList].includes("word")) {
                let targetWord = e.target.textContent;
                if (!this.focusWords.includes(targetWord)) {
                    this.focusWords.push(targetWord);
                    e.target.classList.add("focus_word");
                } else {
                    this.focusWords.splice(this.focusWords.findIndex(word => word === targetWord), 1);
                    e.target.classList.remove("focus_word");
                }
            }
        })

        this.show(Object.keys(this.words));
    }

    add(word, type){
        this.words[word] = this.words[word] || { count: 0, compCount: 0 };
        this.words[word].count += type === "show" ? 1 : 0;
        this.words[word].compCount += type === "complete" ? 1 : 0;

        this.chobun[this.title] ??= {};
        this.chobun[this.title][this.mode] = { words: this.words };
        GM_setValue("chobun", JSON.stringify(this.chobun));

        this.show([word]);
    }

    show(addedWords){
        let innerHTML = Object.keys(this.words).sort((a, b) => this.words[b].count - this.words[a].count).reduce((accHTML, word) => {
            let count = this.words[word].count;
            let compCount = this.words[word].compCount;
            let compRate = (compCount / count * 100).toFixed(2);
            let isFocusWord = this.focusWords.includes(word);

            return accHTML + `<tr>
                <td title="${compRate}%">${compCount}/${count}</td>
                <td title="${word}" class="word${isFocusWord ? " focus_word" : ""}">${word}</td>
            </tr>`;
        }, "") || "<td>Let's typing!</td>";

        this.pDoc.getElementById("words").innerHTML = innerHTML;
        this.highlight(addedWords);
    }

    highlight(addedWords){
        addedWords.forEach(addedWord => {
            let target = this.wordList.querySelector(`[title="${addedWord}"]`);
            target.insertAdjacentHTML("beforeend", "<div class='highlight'></div>");
        })

        setTimeout(() => { [...this.wordList.getElementsByClassName("highlight")].forEach(e => e.remove()); }, 3000);
    }
}

class Replay {
    static scrollLine = 7;

    static init(){
        document.getElementById("btn_area").insertAdjacentHTML("beforeend",`<a id="miss_only_btn" class="btn">リプレイ</a>`);
        document.getElementById("miss_only_btn").addEventListener("click", () => Replay.load(...[Result.selected || 0, !document.getElementsByClassName("hover")[0] ? Typing.latestIndex() : [...document.getElementsByClassName("sentence")[0].children].indexOf(document.getElementsByClassName("hover")[0])].sort((a, b) => a - b), true));

        document.addEventListener("keydown", this.#handleKeydown);
        parent.document.addEventListener("keydown", this.#handleKeydown);
    }

    static load(start, end, play = true){
        this.data = Typing.dataSlice(start, end, true);

        this.sentence = document.getElementsByClassName("sentence")[0];
        this.sentences = document.querySelectorAll(".sentence span");

        this.charWidth = this.sentences[0].getBoundingClientRect().width;
        this.charHeight = this.sentences[0].getBoundingClientRect().height;
        this.lineLimit = Math.floor(this.sentence.getBoundingClientRect().width / this.charWidth);


        play && this.play(start, end);
    }

    static play(start, end){
        document.querySelector("#prev h1").textContent = "-";
        document.getElementsByClassName("result_data")[1].querySelectorAll(".data").forEach(e => e.textContent = "-");
        this.sentences.forEach((e, i) => (i < start || i > end) && (e.style = "opacity: 0.6; display: inline;"));
        this.sentences.forEach((_, i) => this.sentences[i].classList.remove("miss", "entered"));

        Result.fixed = true;
        document.getElementsByClassName("result_data")[1].classList.add("fixed");

        document.getElementById("exampleList").scrollTo({ top: this.sentences[start].offsetTop });

        this.stop = false;
        let startIndex = Typing.data.findIndex(e => e.index === start);
        let i = 0;
        let startTime = performance.now();
        this.tick(() => {
            if (!this.data?.[i] || !document.getElementsByClassName("sentence")[0]) {
                return false;
            }

            let currentTime = performance.now() - startTime;
            let charTime = this.data[i].time;

            if (currentTime >= charTime) {
                if (this.data[i].isInterrupt) {
                    return false;
                }

                let char = this.data[i].char;
                let isMiss = this.data[i].isMiss;
                let index = this.data[i].index;

                this.sentences[index].textContent = char;
                this.sentences[index].classList.add(isMiss ? "miss" : "entered");
                Result.show(startIndex, startIndex + i, false);
                i++;

                if (!isMiss && this.lineLimit * (this.scrollLine - Number(!!(start % this.lineLimit))) <= index - start && !(index % this.lineLimit)) {
                    document.getElementById("exampleList").scrollBy(0, this.charHeight);
                }
            }

            return true;
        })
    }

    static tick(callback) {
        if (this.currentTick) {
            cancelAnimationFrame(this.currentTick);
            console.log("stop");
        }

        const loop = () => {
            if (!callback() || this.stop) {
                console.log(this.stop ? "stop" : "end");
                this.data = null;
                this.currentTick = null;

                Typing.data.forEach(e => {
                    if (!e.isInterrupt) {
                        this.sentences[e.index].style = "";
                        e.isMiss && this.sentences[e.index].classList.add("miss");
                    }
                })
                return;
            }
            this.currentTick = requestAnimationFrame(loop);
        };

        this.currentTick = requestAnimationFrame(loop);
    }

    static #handleKeydown = e => {
        switch (e.key) {
            case "a":
                document.getElementById("miss_only_btn").click();
                break;
            case "Escape":
                this.stop = true;

                this.sentences && Typing.data.forEach(e => {
                    !e.isMiss && !e.isInterrupt && (this.sentences[e.index].textContent = e.char);
                    e.isMiss && this.sentences[e.index].classList.add("miss");
                    this.sentences[e.index].style = e.isInterrupt ? "opacity: 0.6; display: inline;" : "";
                    this.sentences[e.index].classList.remove("entered");
                })

                Result.selected && (Result.selected = null, document.getElementsByClassName("selected")[0]?.classList.remove("selected"));
                document.getElementsByClassName("hover")[0]?.classList.remove("hover");

                Result.fixed = false;
                document.getElementsByClassName("result_data")[1].classList.remove("fixed");
                setTimeout(() => Result.prev.innerHTML = Result.savePrev);
                break;
        }
    }

    static clear(){
        this.stop = false;
        this.data = null;
        this.sentence = null;
        this.sentences = null;

        document.removeEventListener("keydown", this.#handleKeydown);
        parent.document.removeEventListener("keydown", this.#handleKeydown);
    }
}



class Typing {
    static levelList = ["E-", "E", "E+", "D-", "D", "D+", "C-", "C", "C+", "B-", "B", "B+", "A-", "A", "A+", "S", "Good!", "Fast", "Thunder", "Ninja", "Comet", "Professor", "LaserBeam", "EddieVH", "Meijin", "Rocket", "Tatujin", "Jedi", "Godhand", "Joker", "Error"];
    static data = [];

    static score(data = this.data){
        const ms = this.data.at(-1).time;
        const typingCount = this.typingCount(data);
        const missTypeCount = this.missTypeCount(data);
        const correctRate = Math.floor(Math.max(10000 * (typingCount - missTypeCount) / typingCount, 0));
        return 60000 * (typingCount - missTypeCount) / ms * (correctRate / 10000) ** 2;
    }

    static level(score){
        return this.levelList[score < 22 ? 0 : score < 39 ? 1 : score < 56 ? 2 : score < 73 ? 3 : score < 90 ? 4 : score < 107 ? 5 : score < 124 ? 6 : score < 141 ? 7 : score < 158 ? 8 : score < 175 ? 9 : score < 192 ? 10 : score < 209 ? 11 : score < 226 ? 12 : score < 243 ? 13 : score < 260 ? 14 : score < 277 ? 15 : score < 300 ? 16 : score < 325 ? 17 : score < 350 ? 18 : score < 375 ? 19 : score < 400 ? 20 : score < 450 ? 21 : score < 500 ? 22 : score < 550 ? 23 : score < 600 ? 24 : score < 650 ? 25 : score < 700 ? 26 : score < 750 ? 27 : score < 800 ? 28 : score < 1100 ? 29 : 30];
    }

    static inputTime(data = this.data){
        const ms = data.at(-1).time;
        return (ms < 60000 ? "" : Math.floor(ms / 60000) + "分") + (ms / 1000 % 60).toFixed(2).replace(".","秒");
    }

    static typingCount(data = this.data){
        return data.filter(e => !e.isMiss && !e.isInterrupt).length;
    }

    static missTypeCount(data = this.data){
        return data.filter(e => e.isMiss && !e.isInterrupt).length;
    }

    static wpm(data = this.data){
        const ms = this.data.at(-1).time;
        const typingCount = this.typingCount(data);
        return Math.floor(typingCount * (6000000 / ms)) / 100;
    }

    static correctRate(data = this.data){
        const typingCount = this.typingCount(data);
        const missTypeCount = this.missTypeCount(data);
        return Math.floor(Math.max(10000 * (typingCount - missTypeCount) / typingCount, 0));
    }

    static latency(data = this.data){
        return data.find(e => !e.isMiss && !e.isInterrupt)?.time || NaN;
    }

    static rkpm(data = this.data){
        const ms = this.data.at(-1).time;
        const typingCount = this.typingCount(data);
        const latency = this.latency(data);
        return (typingCount - 1) / (ms - latency) * 60000 || 0;
    }

    static result(start = 0, end = this.data.length, indexBreak){
        const data = this.dataSlice(start, end, indexBreak);

        const latency = this.latency(data);
        const ms = data.at(-1).time;
        const inputTime = (ms < 60000 ? "" : Math.floor(ms / 60000) + "分") + (ms / 1000 % 60).toFixed(2).replace(".","秒");

        const typingCount = this.typingCount(data);
        const wpm = Math.floor(typingCount * (6000000 / ms)) / 100;

        const missTypeCount = this.missTypeCount(data);
        const correctRate = Math.floor(Math.max(10000 * (typingCount - missTypeCount) / typingCount, 0));
        const score = 60000 * (typingCount - missTypeCount) / ms * (correctRate / 10000) ** 2;
        const level = this.level(score);

        const rkpm = (typingCount - 1) / (ms - latency) * 60000 || 0;

        return {
            score: score,
            level: level,
            inputTime: inputTime,
            typingCount: typingCount,
            missTypeCount: missTypeCount,
            wpm: wpm,
            correctRate: correctRate,
            latency: latency,
            rkpm: rkpm
        }
    }

    static dataSlice(start, end, indexBreak){
        let data = indexBreak ? this.data.slice(this.data.findIndex(e => e.index === start), this.data.findLastIndex(e => e.index === Math.min(end, this.latestIndex())) + 1) : this.data.slice(start, end + 1);
        return start === 0 ? data : data.map(e => ({ ...e, time: e.time - this.data.findLast(e => e.index === (!indexBreak ? this.data[start].index : start) - 1).time }));
    }

    static latestIndex(){
        return this.data.at(-1).index;
    }

    static clear(){
        this.data = [];
    }
}