1// ============================================================================ 2// game.js ââ 餿 / é²åé輯 + éå¡è寵ç©è³æè¼å ¥ 3// çç´æ²ç·ï¼XP éæª» [0,300,800,1600,2800,4500] å°æéæ®µ 1-6 4// ============================================================================ 5 6(function () { 7 "use strict"; 8 9 var Game = { 10 lessons: null, // { series:[], lessons:[] } 11 creatures: null, // { xpThresholds, stageNames, lines:[] } 12 articles: null, // { series:{}, articles:[] }ï¼task-006 æç« 實æ°ï¼ 13 14 load: async function () { 15 var a = await fetch("data/lessons.json").then(function (r) { return r.json(); }); 16 var b = await fetch("data/creatures.json").then(function (r) { return r.json(); }); 17 var c = await fetch("data/articles.json").then(function (r) { return r.json(); }); 18 this.lessons = a; 19 this.creatures = b; 20 this.articles = c; 21 return this; 22 }, 23 24 getLine: function (lineId) { 25 return this.creatures.lines.filter(function (l) { return l.id === lineId; })[0] || null; 26 }, 27 28 getLesson: function (lessonId) { 29 return this.lessons.lessons.filter(function (l) { return l.id === lessonId; })[0] || null; 30 }, 31 32 // ---- æç« 實æ°ï¼task-006ï¼---- 33 getArticle: function (id) { 34 var list = (this.articles && this.articles.articles) || []; 35 return list.filter(function (a) { return a.id === id; })[0] || null; 36 }, 37 38 // 便·å²æä½³é度æ¨è¦é·åº¦æªä½ï¼åçµé檻ï¼<20 ç(1) / 20â35 ä¸(2) / >35 é·(3)ï¼è±æ WPMã䏿 CPM åï¼ 39 recommendedLevel: function (speed) { 40 speed = speed || 0; 41 if (speed < 20) return 1; 42 if (speed <= 35) return 2; 43 return 3; 44 }, 45 46 levelLabel: function (lv) { 47 return lv === 1 ? "ç" : (lv === 2 ? "ä¸" : "é·"); 48 }, 49 50 // ä¾ XP æ±ç®åéæ®µï¼1-6ï¼ 51 stageForXp: function (xp) { 52 var t = this.creatures.xpThresholds; 53 var stage = 1; 54 for (var i = 0; i < t.length; i++) { 55 if (xp >= t[i]) stage = i + 1; 56 } 57 return stage; 58 }, 59 60 stageInfo: function (lineId, stage) { 61 var line = this.getLine(lineId); 62 if (!line) return null; 63 return line.stages[stage - 1] || null; 64 }, 65 66 stageName: function (stage) { 67 return this.creatures.stageNames[stage - 1] || ("éæ®µ " + stage); 68 }, 69 70 // --- çç´ï¼Lv 1â15ï¼2026-09-18ï¼-------------------------------------- 71 // ãå¤åã仿¯ä¸é¢ç stageï¼1â6ï¼æ±ºå®é²ååï¼æè¡æ¦è rules ä¹åªèªå®ï¼ï¼ 72 // ãçç´ãæ¯ç¨ç«ç䏿¢ç·ï¼Lv 1â6 éæª»è stage ç¸åï¼æ²æäººéç´ï¼ï¼ 73 // Lv 7â15 å¤åå卿çµå½¢æ ãé 稱èååãå¸çåå ±å¤ªå¿«æ»¿ç´ï¼ä½¿ç¨è è£å®æå° 15 ç´ã 74 levelThresholds: function () { 75 return this.creatures.levelThresholds || this.creatures.xpThresholds; 76 }, 77 78 levelForXp: function (xp) { 79 var t = this.levelThresholds(); 80 var level = 1; 81 for (var i = 0; i < t.length; i++) { 82 if (xp >= t[i]) level = i + 1; 83 } 84 return level; 85 }, 86 87 levelName: function (level) { 88 var names = this.creatures.levelNames || this.creatures.stageNames; 89 return names[level - 1] || ("Lv " + level); 90 }, 91 92 maxLevel: function () { 93 return this.levelThresholds().length; 94 }, 95 96 // æ¬ç´é²åº¦ï¼{ level, isMax, curThreshold, nextThreshold, toNext, percent } 97 levelProgress: function (xp) { 98 var t = this.levelThresholds(); 99 var level = this.levelForXp(xp); 100 var cur = t[level - 1]; 101 var isMax = level >= t.length; 102 var next = isMax ? cur : t[level]; 103 return { 104 level: level, 105 isMax: isMax, 106 curThreshold: cur, 107 nextThreshold: next, 108 toNext: isMax ? 0 : next - xp, 109 percent: isMax ? 100 : Math.round(Math.min(100, ((xp - cur) / (next - cur)) * 100)) 110 }; 111 }, 112 113 // å XPï¼åå³ { xp, oldStage, newStage, evolved } 114 addXp: function (profile, amount) { 115 var oldStage = this.stageForXp(profile.xp); 116 var oldLevel = this.levelForXp(profile.xp); 117 profile.xp += amount; 118 var newStage = this.stageForXp(profile.xp); 119 var newLevel = this.levelForXp(profile.xp); 120 return { 121 xp: profile.xp, 122 oldStage: oldStage, 123 newStage: newStage, 124 evolved: newStage > oldStage, // å¤åè®äºï¼æé²ååï¼ 125 oldLevel: oldLevel, 126 newLevel: newLevel, 127 leveledUp: newLevel > oldLevel // çç´åäºï¼Lv 7 èµ·å¤åä¸è®ãåªæç¨±èï¼ 128 }; 129 }, 130 131 // ====================================================================== 132 // v1.2 鲿¥ç³»çµ±ï¼task-004ï¼ââ æ¸å¼å ¨é¨åçµï¼è¦ SPECãv1.2 鲿¥ç³»çµ±ã 133 // ====================================================================== 134 135 HISTORY_MAX: 300, 136
137 // 鿬¡çµç® append ä¸çæ·å²ï¼è¶ é 300 䏿èï¼client 端è£åªï¼ 138 pushHistory: function (profile, entry) { 139 if (!Array.isArray(profile.history)) profile.history = []; 140 profile.history.push(entry); 141 while (profile.history.length > this.HISTORY_MAX) profile.history.shift(); 142 return profile.history; 143 }, 144 145 // 䏿éåº¦éæª»ï¼åçµï¼ï¼series â { two:{spd,acc}, three:{spd,acc} } 146 // è±æéµä½æ¾å¯¬ï¼è±æå®å/å¥åï¼ä¸æä¸ç³»åãé度ï¼è±æ WPMã䏿 CPMã 147 STAR_THRESHOLDS: { 148 "keys": { two: { spd: 20, acc: 90 }, three: { spd: 30, acc: 95 } }, 149 "words": { two: { spd: 25, acc: 90 }, three: { spd: 35, acc: 95 } }, 150 "sentences": { two: { spd: 25, acc: 90 }, three: { spd: 35, acc: 95 } }, 151 "zh-chars": { two: { spd: 25, acc: 90 }, three: { spd: 40, acc: 95 } }, 152 "zh-words": { two: { spd: 25, acc: 90 }, three: { spd: 40, acc: 95 } }, 153 "zh-sentences": { two: { spd: 25, acc: 90 }, three: { spd: 40, acc: 95 } } 154 }, 155 156 // 宿¬¡çµç®å¤æï¼å®æè©²éè³å° 1â ï¼ééæª»å 2â /3â ï¼ 157 starFor: function (seriesId, speed, accPct) { 158 var t = this.STAR_THRESHOLDS[seriesId]; 159 if (!t) return 1; 160 if (speed >= t.three.spd && accPct >= t.three.acc) return 3; 161 if (speed >= t.two.spd && accPct >= t.two.acc) return 2; 162 return 1; 163 }, 164 165 // 馿¬¡åæä¸æ¬¡æ§çåµ XPï¼2â +20ã3â +50ï¼ééï¼è·¨éç´¯å ï¼åæä¸éç¼ï¼ 166 starBonus: function (prevStar, newStar) { 167 var bonus = 0; 168 for (var t = (prevStar || 0) + 1; t <= newStar; t++) { 169 if (t === 2) bonus += 20; 170 else if (t === 3) bonus += 50; 171 } 172 return bonus; 173 }, 174 175 // è¨éæ¬éæé«æä¸¦åå³ { prevStar, newStar, bonus } 176 recordStar: function (profile, lessonId, star) { 177 if (!profile.stars || typeof profile.stars !== "object") profile.stars = {}; 178 var prev = profile.stars[lessonId] || 0; 179 var next = Math.max(prev, star); 180 var bonus = this.starBonus(prev, next); 181 profile.stars[lessonId] = next; 182 return { prevStar: prev, newStar: next, bonus: bonus }; 183 }, 184 185 // ---- å°åæåï¼UTC+8ï¼ç¡æ¥å ç¯ç´ï¼æ¬é±ä¸ 00:00 ç UTC æ¯«ç§ ---- 186 taipeiWeekStart: function (nowMs) { 187 var OFF = 8 * 3600 * 1000; 188 var d = new Date((nowMs == null ? Date.now() : nowMs) + OFF); // 以 UTC æ¹æ³è®åºï¼å°åæ¬å° 189 var day = d.getUTCDay(); // 0=æ¥..6=å 190 var sinceMon = (day === 0) ? 6 : day - 1; 191 var midnight = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()); 192 return midnight - sinceMon * 86400000 - OFF; // 忍æç實 UTC æ¯«ç§ 193 }, 194 195 // ææ¨¡å¼çé±å¢å¹ ï¼{ gain, weekBest, priorBest, mode } æ nullï¼ç¡æ¬é±æç¡å åæ·å²ï¼ 196 modeGain: function (history, mode, weekStart) { 197 var weekBest = -Infinity, priorBest = -Infinity; 198 (history || []).forEach(function (h) { 199 if (h.mode !== mode) return; 200 if (h.t >= weekStart) { if (h.speed > weekBest) weekBest = h.speed; } 201 else { if (h.speed > priorBest) priorBest = h.speed; } 202 }); 203 if (weekBest === -Infinity || priorBest === -Infinity) return null; 204 return { gain: weekBest - priorBest, weekBest: weekBest, priorBest: priorBest, mode: mode }; 205 }, 206 207 // åè±æ/ä¸æå ©æ¨¡å¼ä¸å¢å¹ è¼å¤§è ç¶æåºå¼ï¼çç¡æå null 208 weeklyGain: function (profile, nowMs) { 209 var ws = this.taipeiWeekStart(nowMs); 210 var en = this.modeGain(profile.history, "en", ws); 211 var zh = this.modeGain(profile.history, "zh", ws); 212 if (!en && !zh) return null; 213 if (en && zh) return en.gain >= zh.gain ? en : zh; 214 return en || zh; 215 }, 216 217 // ---- éå è² endgame ---- 218 shinyUnlocked: function (profile) { 219 var line = this.getLine(profile.creatureId); 220 if (!line || typeof line.shinyXp !== "number") return false; 221 return profile.xp >= line.shinyXp; 222 }, 223 224 // éå 顯示è³è¨ { img, name, stageLabel }ï¼ååå â¨ï¼ 225 shinyInfo: function (lineId) { 226 var line = this.getLine(lineId); 227 if (!line || !line.shinyImg) return null; 228 var s6 = line.stages[line.stages.length - 1]; 229 return { 230 img: line.shinyImg, 231 name: s6.name + " â¨", 232 stageLabel: "çè ã»éå " 233 }; 234 } 235 }; 236 237 window.Game = Game; 238})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.