最后活跃于 1 month ago

修订 aa1a573bf42959be64326373e05b662ba7abdd86

wrank.ts 原始文件
1import fs from "fs";
2import path from "path";
3import { WRankData, WRankWeek, WRankEntry, Nation, ClassKey } from "../types";
4import { cfg } from "./config";
5
6const WRANK_PATH = path.join(__dirname, "../../data/wrank.json");
7let _data: WRankData = {};
8
9export function loadWRank(): void {
10 try { _data = JSON.parse(fs.readFileSync(WRANK_PATH, "utf8")); }
11 catch { _data = {}; }
12}
13
14function saveWRank(): void {
15 fs.writeFileSync(WRANK_PATH, JSON.stringify(_data, null, 2));
16}
17
18export 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
26function 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
38export function getCurrentWeek(): WRankWeek {
39 return ensureWeek(getWeekKey());
40}
41
42export function getWeek(weekKey: string): WRankWeek | null {
43 return _data[weekKey] ?? null;
44}
45
46// Add or update a score submission for a player
47export 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.userKey === userKey);
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 if (!week.scoreIndex[userKey]) week.scoreIndex[userKey] = [];
91 if (!week.scoreIndex[userKey].includes(historyKey)) {
92 week.scoreIndex[userKey].push(historyKey);
93 }
94
95 recomputeRanks(week, nation);
96 updateBringer(week);
97 saveWRank();
98}
99
100function recomputeRanks(week: WRankWeek, nation: Nation): void {
101 const list = week.entries[nation.toLowerCase() as "capella" | "procyon"];
102 const sorted = [...list].sort((a, b) => b.weeklyPoints - a.weeklyPoints);
103 sorted.forEach((entry, i) => {
104 const live = list.find((e) => e.userKey === entry.userKey)!;
105 live.previousRank = live.currentRank || undefined;
106 live.currentRank = i + 1;
107 });
108}
109
110function updateBringer(week: WRankWeek): void {
111 const goal = cfg("wRankGoal");
112 for (const nation of ["capella", "procyon"] as const) {
113 // Don't overwrite manual override
114 if (nation === "capella" && week.bringer.capellaOverride) continue;
115 if (nation === "procyon" && week.bringer.procyonOverride) continue;
116
117 const qualified = week.entries[nation]
118 .filter((e) => e.tgCount >= goal)
119 .sort((a, b) => a.currentRank - b.currentRank);
120 week.bringer[nation] = qualified[0]?.characterName ?? null;
121 }
122}
123
124export function setBringerOverride(nation: Nation, charName: string): void {
125 const week = ensureWeek(getWeekKey());
126 if (nation === "Capella") week.bringer.capellaOverride = charName;
127 else week.bringer.procyonOverride = charName;
128 saveWRank();
129}
130
131export function clearBringerOverride(nation: Nation): void {
132 const week = ensureWeek(getWeekKey());
133 if (nation === "Capella") delete week.bringer.capellaOverride;
134 else delete week.bringer.procyonOverride;
135 updateBringer(week);
136 saveWRank();
137}
138
139export function getBringer(nation: Nation): string | null {
140 const week = getCurrentWeek();
141 if (nation === "Capella") return week.bringer.capellaOverride ?? week.bringer.capella;
142 return week.bringer.procyonOverride ?? week.bringer.procyon;
143}
144
145export function getEntry(userKey: string, nation: Nation): WRankEntry | null {
146 const week = getCurrentWeek();
147 const list = week.entries[nation.toLowerCase() as "capella" | "procyon"];
148 return list.find((e) => e.userKey === userKey) ?? null;
149}
150
151// Called every Monday 00:00 by cron
152export function resetWeek(): void {
153 // Week is already archived in _data by weekKey — just ensure next week exists
154 ensureWeek(getWeekKey(new Date()));
155 saveWRank();
156}