最后活跃于 1 month ago

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 + }
上一页 下一页