WKStats Levelup Fix

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

  1. // ==UserScript==
  2. // @name WKStats Levelup Fix
  3. // @namespace https://greasyfork.org/en/users/11878
  4. // @version 2.0.2
  5. // @description Fix weird issues with the levelups on wkstats that are assumed to be caused by new additions of kanji to levels
  6. // @author Inserio
  7. // @match https://www.wkstats.com/progress/dashboard
  8. // @match https://www.wkstats.com/progress/level-up
  9. // @match https://www.wkstats.com/progress/projections
  10. // @match https://www.wkstats.com/
  11. // @icon https://www.google.com/s2/favicons?sz=64&domain=wkstats.com
  12. // @license MIT; http://opensource.org/licenses/MIT
  13. // @run-at document-start
  14. // @grant none
  15. // ==/UserScript==
  16. /*global wkof, wkdata, wkstats, calc_levelups, log, yyyymmdd, duration, wklogs*/
  17. (function() {
  18. 'use strict';
  19. if (document.readyState === "loading") {
  20. document.addEventListener("DOMContentLoaded", init);
  21. } else {
  22. init();
  23. }
  24.  
  25. function init() {
  26. const calc_stats = wkof.support_files['calc_stats.js'];
  27. if (calc_stats === undefined || calc_stats === null || !calc_stats.includes('1.0.7')) // only run on this version
  28. return;
  29. wkof.wait_state(/* state_var */ "wkof.wkstats.levelups",/* value */ "ready",/* callback */ overwriteFunction,/* persistent */ false);
  30. }
  31.  
  32. function overwriteFunction() {
  33. console.log('Overriding the calc_levelups() function definition');
  34. // eslint-disable-next-line no-global-assign
  35. calc_levelups = function() {
  36. wklogs['levelups'].length = 0; // clear existing logs (comment out if you want to compare results with the default version, which gets run first)
  37. log('levelups', 'Overriding previous data of calc_levelups() with modified version');
  38. let level_times = wkstats.level_times = [];
  39.  
  40. // For each level, initialize a valid range of possible level times (initial = any time)
  41. for (let level = 1; level <= wkof.user.subscription.max_level_granted; level++) {
  42. level_times[level] = {
  43. min: new Date(0),
  44. max: wkdata.load_time,
  45. dates: [],
  46. source: 'unknown',
  47. };
  48. }
  49.  
  50. // Using level resets, throw out old level start times (by marking the min start time)
  51. for (let reset_idx = 0; reset_idx < wkdata.resets.length; reset_idx++) {
  52. let reset = wkdata.resets[reset_idx];
  53. let reset_time = new Date(reset.confirmed_at);
  54. for (let level = reset.target_level; level <= wkof.user.level; level++) {
  55. let level_time = level_times[level];
  56.  
  57. // Ignore resets that happened before this level-up.
  58. if (reset_time < level_time.min) continue;
  59.  
  60. // Update the min start time.
  61. level_time.min = reset_time;
  62. delete level_times[level].reset_time;
  63. }
  64. level_times[reset.target_level].reset_time = reset_time;
  65. }
  66.  
  67. // Using the newest levelup record for each level, set known start times.
  68. let oldest_levelup = {index: -1, time: new Date('2999-01-01')};
  69. for (let levelup_idx = 0; levelup_idx < wkdata.levelups.length; levelup_idx++) {
  70. let levelup = wkdata.levelups[levelup_idx];
  71. let level = levelup.level;
  72. if (level > wkof.user.level) continue;
  73. let unlocked_time = new Date(levelup.unlocked_at);
  74. let level_time = level_times[level];
  75.  
  76. // Check if this is the oldest recorded level-up, which may be invalid.
  77. if (unlocked_time < oldest_levelup.time) {
  78. oldest_levelup = {index: levelup_idx, time: unlocked_time, level: level};
  79. }
  80.  
  81. // Ignore levelups that were invalidated by a reset.
  82. if (unlocked_time < level_time.min) continue;
  83.  
  84. // Update the level start time.
  85. level_time.min = unlocked_time;
  86. level_time.source = 'APIv2 level_progressions';
  87. if (!levelup.abandoned_at && levelup.passed_at) {
  88. level_time.max = new Date(levelup.passed_at);
  89. } else if (level === wkof.user.level) {
  90. level_time.max = new Date();
  91. }
  92. }
  93.  
  94. let items = wkdata.items;
  95. let level_progressions = wkdata.levelups;
  96. let first_recorded_date = level_progressions[Math.min(...Object.keys(level_progressions))].unlocked_at;
  97. // Find indefinite level ups by looking at lesson history
  98.  
  99. // Sort lessons by level then unlocked date
  100. items.forEach((item) => {
  101. if (
  102. (item.object !== 'kanji' && item.object !== 'radical') ||
  103. !item.assignments ||
  104. !item.assignments.unlocked_at ||
  105. item.assignments.unlocked_at >= first_recorded_date
  106. )
  107. return;
  108. let date = new Date(item.assignments.unlocked_at);
  109. if (!level_times[item.data.level]) {
  110. level_times[item.data.level] = {};
  111. }
  112. if (!level_times[item.data.level].dates[date.toDateString()]) {
  113. level_times[item.data.level].dates[date.toDateString()] = [date];
  114. }
  115. else {
  116. level_times[item.data.level].dates[date.toDateString()].push(date);
  117. }
  118. });
  119. // Discard dates with less than 10 unlocked
  120. // then discard levels with no dates
  121. // then keep earliest date for each level
  122. for (let [level, {min, max, dates, source}] of Object.entries(level_times)) {
  123. for (let [date, data] of Object.entries(dates)) {
  124. if (data.length < 10)
  125. delete dates[date];
  126. }
  127. if (Object.keys(level_times[level].dates).length === 0) {
  128. delete level_times[level].dates;
  129. continue;
  130. }
  131. //level_times[level].min = Object.values(dates).reduce((low, curr) => (low < curr ? low : curr), Date.now()).sort((a, b) => (a.getTime() - b.getTime()))[0];
  132. 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());
  133. }
  134. // Map to array of [[level0, date0], [level1, date1], ...] Format
  135. //levels = Object.entries(levels).map(([level, date]) => [Number(level), date]);
  136.  
  137. // Add definite level ups from API
  138. Object.values(level_progressions).forEach(lev => {
  139. if (level_times[lev.level].source === 'APIv2 level_progressions') return;
  140. level_times[lev.level] = {
  141. min: new Date(lev.unlocked_at),
  142. max: (lev.passed_at ? new Date(lev.passed_at) : wkdata.load_time),
  143. source: 'APIv2 level_progressions'
  144. };});
  145.  
  146. for (let level = 1; level <= wkof.user.level; level++) {
  147. let level_data = level_times[level];
  148. if (level_data.source === 'APIv2 level_progressions') continue;
  149. if (level < level_times.length - 1) {
  150. let next_level_data = level_times[level+1];
  151. if (level_data.max.getTime() === wkdata.load_time.getTime())
  152. level_data.max = next_level_data.min;
  153. }
  154. }
  155.  
  156. // Calculate durations
  157. let durations = wkstats.level_durations = [];
  158. for (let level = 1; level <= wkof.user.level; level++) {
  159. let level_time = level_times[level];
  160. durations[level] = (level_time.max - level_time.min) / 86400000;
  161. }
  162.  
  163. log('levelups','--[ Level-ups ]----------------');
  164. let level_durations = wkstats.level_durations;
  165. // Log the current level statuses.
  166. log('levelups','Started: '+yyyymmdd(wkof.user.started_at));
  167. if (wkof.user.restarted_at) {
  168. log('levelups','Restarted: '+yyyymmdd(wkof.user.restarted_at));
  169. }
  170. for (let level = 1; level <= wkof.user.level; level++) {
  171. let level_time = level_times[level];
  172. let level_duration = level_durations[level];
  173. if (level_time.reset_time) {
  174. log('levelups','Reset');
  175. }
  176. // Flag any unusual level durations.
  177. if (level < wkof.user.level && (level_duration < 3.0 || level_duration > 2000))
  178. log('levelups','###################');
  179. log('levelups','Level '+level+': ('+yyyymmdd(level_time.min)+' - '+yyyymmdd(level_time.max)+') - '+
  180. duration(level_duration)+' (source: '+level_time.source+')');
  181. }
  182. wkof.set_state('wkof.wkstats.levelups', 'ready');
  183. };
  184. // Immediately run in order to overwrite all of the configurations from the default run
  185. calc_levelups();
  186. }
  187. })();