WKStats Levelup Fix

Fix weird issues with the levelups on wkstats that are assumed to be caused by new additions of kanji to levels

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         WKStats Levelup Fix
// @namespace    https://greasyfork.org/en/users/11878
// @version      2.0.2
// @description  Fix weird issues with the levelups on wkstats that are assumed to be caused by new additions of kanji to levels
// @author       Inserio
// @match        https://www.wkstats.com/progress/dashboard
// @match        https://www.wkstats.com/progress/level-up
// @match        https://www.wkstats.com/progress/projections
// @match        https://www.wkstats.com/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=wkstats.com
// @license      MIT; http://opensource.org/licenses/MIT
// @run-at       document-start
// @grant        none
// ==/UserScript==
/*global wkof, wkdata, wkstats, calc_levelups, log, yyyymmdd, duration, wklogs*/
(function() {
    'use strict';
    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", init);
    } else {
        init();
    }

    function init() {
        const calc_stats = wkof.support_files['calc_stats.js'];
        if (calc_stats === undefined || calc_stats === null || !calc_stats.includes('1.0.7')) // only run on this version
            return;
        wkof.wait_state(/* state_var */ "wkof.wkstats.levelups",/* value */ "ready",/* callback */ overwriteFunction,/* persistent */ false);
    }

    function overwriteFunction() {
        console.log('Overriding the calc_levelups() function definition');
        // eslint-disable-next-line no-global-assign
        calc_levelups = function() {
            wklogs['levelups'].length = 0; // clear existing logs (comment out if you want to compare results with the default version, which gets run first)
            log('levelups', 'Overriding previous data of calc_levelups() with modified version');
            let level_times = wkstats.level_times = [];

            // For each level, initialize a valid range of possible level times (initial = any time)
            for (let level = 1; level <= wkof.user.subscription.max_level_granted; level++) {
                level_times[level] = {
                    min: new Date(0),
                    max: wkdata.load_time,
                    dates: [],
                    source: 'unknown',
                };
            }

            // Using level resets, throw out old level start times (by marking the min start time)
            for (let reset_idx = 0; reset_idx < wkdata.resets.length; reset_idx++) {
                let reset = wkdata.resets[reset_idx];
                let reset_time = new Date(reset.confirmed_at);
                for (let level = reset.target_level; level <= wkof.user.level; level++) {
                    let level_time = level_times[level];

                    // Ignore resets that happened before this level-up.
                    if (reset_time < level_time.min) continue;

                    // Update the min start time.
                    level_time.min = reset_time;
                    delete level_times[level].reset_time;
                }
                level_times[reset.target_level].reset_time = reset_time;
            }

            // Using the newest levelup record for each level, set known start times.
            let oldest_levelup = {index: -1, time: new Date('2999-01-01')};
            for (let levelup_idx = 0; levelup_idx < wkdata.levelups.length; levelup_idx++) {
                let levelup = wkdata.levelups[levelup_idx];
                let level = levelup.level;
                if (level > wkof.user.level) continue;
                let unlocked_time = new Date(levelup.unlocked_at);
                let level_time = level_times[level];

                // Check if this is the oldest recorded level-up, which may be invalid.
                if (unlocked_time < oldest_levelup.time) {
                    oldest_levelup = {index: levelup_idx, time: unlocked_time, level: level};
                }

                // Ignore levelups that were invalidated by a reset.
                if (unlocked_time < level_time.min) continue;

                // Update the level start time.
                level_time.min = unlocked_time;
                level_time.source = 'APIv2 level_progressions';
                if (!levelup.abandoned_at && levelup.passed_at) {
                    level_time.max = new Date(levelup.passed_at);
                } else if (level === wkof.user.level) {
                    level_time.max = new Date();
                }
            }

            let items = wkdata.items;
            let level_progressions = wkdata.levelups;
            let first_recorded_date = level_progressions[Math.min(...Object.keys(level_progressions))].unlocked_at;
            // Find indefinite level ups by looking at lesson history

            // Sort lessons by level then unlocked date
            items.forEach((item) => {
                if (
                    (item.object !== 'kanji' && item.object !== 'radical') ||
                    !item.assignments ||
                    !item.assignments.unlocked_at ||
                    item.assignments.unlocked_at >= first_recorded_date
                )
                    return;
                let date = new Date(item.assignments.unlocked_at);
                if (!level_times[item.data.level]) {
                    level_times[item.data.level] = {};
                }
                if (!level_times[item.data.level].dates[date.toDateString()]) {
                    level_times[item.data.level].dates[date.toDateString()] = [date];
                }
                else {
                    level_times[item.data.level].dates[date.toDateString()].push(date);
                }
            });
            // Discard dates with less than 10 unlocked
            // then discard levels with no dates
            // then keep earliest date for each level
            for (let [level, {min, max, dates, source}] of Object.entries(level_times)) {
                for (let [date, data] of Object.entries(dates)) {
                    if (data.length < 10)
                        delete dates[date];
                }
                if (Object.keys(level_times[level].dates).length === 0) {
                    delete level_times[level].dates;
                    continue;
                }
                //level_times[level].min = Object.values(dates).reduce((low, curr) => (low < curr ? low : curr), Date.now()).sort((a, b) => (a.getTime() - b.getTime()))[0];
                level_times[level].min = Object.values(dates).reduce((acc,item)=>{let smallest=item.reduce((a,b)=>a<b?a:b);return acc<smallest ? acc : smallest;}, new Date());
            }
            // Map to array of [[level0, date0], [level1, date1], ...] Format
            //levels = Object.entries(levels).map(([level, date]) => [Number(level), date]);

            // Add definite level ups from API
            Object.values(level_progressions).forEach(lev => {
                                                      if (level_times[lev.level].source === 'APIv2 level_progressions') return;
                                                      level_times[lev.level] = {
                                                          min: new Date(lev.unlocked_at),
                                                          max: (lev.passed_at ? new Date(lev.passed_at) : wkdata.load_time),
                                                          source: 'APIv2 level_progressions'
            };});

            for (let level = 1; level <= wkof.user.level; level++) {
                let level_data = level_times[level];
                if (level_data.source === 'APIv2 level_progressions') continue;
                if (level < level_times.length - 1) {
                    let next_level_data = level_times[level+1];
                    if (level_data.max.getTime() === wkdata.load_time.getTime())
                        level_data.max = next_level_data.min;
                }
            }

            // Calculate durations
            let durations = wkstats.level_durations = [];
            for (let level = 1; level <= wkof.user.level; level++) {
                let level_time = level_times[level];
                durations[level] = (level_time.max - level_time.min) / 86400000;
            }

            log('levelups','--[ Level-ups ]----------------');
            let level_durations = wkstats.level_durations;
            // Log the current level statuses.
            log('levelups','Started: '+yyyymmdd(wkof.user.started_at));
            if (wkof.user.restarted_at) {
                log('levelups','Restarted: '+yyyymmdd(wkof.user.restarted_at));
            }
            for (let level = 1; level <= wkof.user.level; level++) {
                let level_time = level_times[level];
                let level_duration = level_durations[level];
                if (level_time.reset_time) {
                    log('levelups','Reset');
                }
                // Flag any unusual level durations.
                if (level < wkof.user.level && (level_duration < 3.0 || level_duration > 2000))
                    log('levelups','###################');
                log('levelups','Level '+level+': ('+yyyymmdd(level_time.min)+' - '+yyyymmdd(level_time.max)+') - '+
                    duration(level_duration)+' (source: '+level_time.source+')');
            }
            wkof.set_state('wkof.wkstats.levelups', 'ready');
        };
        // Immediately run in order to overwrite all of the configurations from the default run
        calc_levelups();
    }
})();