e-typing chobun plus

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

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==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.greatest.deepsurf.us/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 = [];
    }
}