nuno 修订了这个 Gist 1 month ago. 转到此修订
1 file changed, 157 insertions
wrank.ts(文件已创建)
| @@ -0,0 +1,157 @@ | |||
| 1 | + | import fs from "fs"; | |
| 2 | + | import path from "path"; | |
| 3 | + | import { WRankData, WRankWeek, WRankEntry, Nation, ClassKey } from "../types"; | |
| 4 | + | import { cfg } from "./config"; | |
| 5 | + | ||
| 6 | + | const WRANK_PATH = path.join(__dirname, "../../data/wrank.json"); | |
| 7 | + | let _data: WRankData = {}; | |
| 8 | + | ||
| 9 | + | export function loadWRank(): void { | |
| 10 | + | try { _data = JSON.parse(fs.readFileSync(WRANK_PATH, "utf8")); } | |
| 11 | + | catch { _data = {}; } | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | function saveWRank(): void { | |
| 15 | + | fs.writeFileSync(WRANK_PATH, JSON.stringify(_data, null, 2)); | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | export function getWeekKey(date: Date = new Date()): string { | |
| 19 | + | const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); | |
| 20 | + | d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7)); | |
| 21 | + | const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); | |
| 22 | + | const week = Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7); | |
| 23 | + | return `${d.getUTCFullYear()}-W${String(week).padStart(2, "0")}`; | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | function ensureWeek(weekKey: string): WRankWeek { | |
| 27 | + | if (!_data[weekKey]) { | |
| 28 | + | _data[weekKey] = { | |
| 29 | + | weekKey, | |
| 30 | + | entries: { capella: [], procyon: [] }, | |
| 31 | + | scoreIndex: {}, | |
| 32 | + | bringer: { capella: null, procyon: null }, | |
| 33 | + | }; | |
| 34 | + | } | |
| 35 | + | return _data[weekKey]; | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | export function getCurrentWeek(): WRankWeek { | |
| 39 | + | return ensureWeek(getWeekKey()); | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | export function getWeek(weekKey: string): WRankWeek | null { | |
| 43 | + | return _data[weekKey] ?? null; | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // Add or update a score submission for a player | |
| 47 | + | export function recordScore( | |
| 48 | + | userKey: string, | |
| 49 | + | characterName: string, | |
| 50 | + | cls: ClassKey, | |
| 51 | + | nation: Nation, | |
| 52 | + | pts: number, | |
| 53 | + | historyKey: string // e.g. "2026-05-31-20" | |
| 54 | + | ): void { | |
| 55 | + | const weekKey = getWeekKey(); | |
| 56 | + | const week = ensureWeek(weekKey); | |
| 57 | + | const list = week.entries[nation.toLowerCase() as "capella" | "procyon"]; | |
| 58 | + | ||
| 59 | + | const existing = list.find((e) => e.characterName === characterName); | |
| 60 | + | ||
| 61 | + | if (existing) { | |
| 62 | + | // Check if this slot was already counted | |
| 63 | + | const alreadyCounted = week.scoreIndex[userKey]?.includes(historyKey); | |
| 64 | + | if (!alreadyCounted) { | |
| 65 | + | existing.weeklyPoints += pts; | |
| 66 | + | existing.tgCount += 1; | |
| 67 | + | } else { | |
| 68 | + | // Overwrite: recalculate by removing old pts for this slot | |
| 69 | + | // We'll just set the new pts — full recalc would require reading history | |
| 70 | + | // For now, simple overwrite of total is handled at score submission level | |
| 71 | + | existing.weeklyPoints = existing.weeklyPoints - (existing.weeklyPoints / existing.tgCount) + pts; | |
| 72 | + | } | |
| 73 | + | existing.characterName = characterName; | |
| 74 | + | existing.class = cls; | |
| 75 | + | existing.nation = nation; | |
| 76 | + | } else { | |
| 77 | + | list.push({ | |
| 78 | + | userKey, | |
| 79 | + | characterName, | |
| 80 | + | class: cls, | |
| 81 | + | nation, | |
| 82 | + | weeklyPoints: pts, | |
| 83 | + | tgCount: 1, | |
| 84 | + | currentRank: 0, | |
| 85 | + | previousRank: undefined, | |
| 86 | + | }); | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // Update score index | |
| 90 | + | const indexKey = characterName; | |
| 91 | + | if (!week.scoreIndex[indexKey]) week.scoreIndex[indexKey] = []; | |
| 92 | + | if (!week.scoreIndex[indexKey].includes(historyKey)) { | |
| 93 | + | week.scoreIndex[indexKey].push(historyKey); | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | recomputeRanks(week, nation); | |
| 97 | + | updateBringer(week); | |
| 98 | + | saveWRank(); | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | function recomputeRanks(week: WRankWeek, nation: Nation): void { | |
| 102 | + | const list = week.entries[nation.toLowerCase() as "capella" | "procyon"]; | |
| 103 | + | const sorted = [...list].sort((a, b) => b.weeklyPoints - a.weeklyPoints); | |
| 104 | + | sorted.forEach((entry, i) => { | |
| 105 | + | const live = list.find((e) => e.characterName === entry.characterName)!; | |
| 106 | + | live.previousRank = live.currentRank || undefined; | |
| 107 | + | live.currentRank = i + 1; | |
| 108 | + | }); | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | function updateBringer(week: WRankWeek): void { | |
| 112 | + | const goal = cfg("wRankGoal"); | |
| 113 | + | for (const nation of ["capella", "procyon"] as const) { | |
| 114 | + | // Don't overwrite manual override | |
| 115 | + | if (nation === "capella" && week.bringer.capellaOverride) continue; | |
| 116 | + | if (nation === "procyon" && week.bringer.procyonOverride) continue; | |
| 117 | + | ||
| 118 | + | const qualified = week.entries[nation] | |
| 119 | + | .filter((e) => e.tgCount >= goal) | |
| 120 | + | .sort((a, b) => a.currentRank - b.currentRank); | |
| 121 | + | week.bringer[nation] = qualified[0]?.characterName ?? null; | |
| 122 | + | } | |
| 123 | + | } | |
| 124 | + | ||
| 125 | + | export function setBringerOverride(nation: Nation, charName: string): void { | |
| 126 | + | const week = ensureWeek(getWeekKey()); | |
| 127 | + | if (nation === "Capella") week.bringer.capellaOverride = charName; | |
| 128 | + | else week.bringer.procyonOverride = charName; | |
| 129 | + | saveWRank(); | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | export function clearBringerOverride(nation: Nation): void { | |
| 133 | + | const week = ensureWeek(getWeekKey()); | |
| 134 | + | if (nation === "Capella") delete week.bringer.capellaOverride; | |
| 135 | + | else delete week.bringer.procyonOverride; | |
| 136 | + | updateBringer(week); | |
| 137 | + | saveWRank(); | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | export function getBringer(nation: Nation): string | null { | |
| 141 | + | const week = getCurrentWeek(); | |
| 142 | + | if (nation === "Capella") return week.bringer.capellaOverride ?? week.bringer.capella; | |
| 143 | + | return week.bringer.procyonOverride ?? week.bringer.procyon; | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | export function getEntry(characterName: string, nation: Nation): WRankEntry | null { | |
| 147 | + | const week = getCurrentWeek(); | |
| 148 | + | const list = week.entries[nation.toLowerCase() as "capella" | "procyon"]; | |
| 149 | + | return list.find((e) => e.characterName === characterName) ?? null; | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | // Called every Monday 00:00 by cron | |
| 153 | + | export function resetWeek(): void { | |
| 154 | + | // Week is already archived in _data by weekKey — just ensure next week exists | |
| 155 | + | ensureWeek(getWeekKey(new Date())); | |
| 156 | + | saveWRank(); | |
| 157 | + | } | |
上一页
下一页