最終更新 1 month ago

nuno revised this gist 1 month ago. Go to revision

1 file changed, 156 insertions

wrank.ts(file created)

@@ -0,0 +1,156 @@
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.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 +
100 + function 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 +
110 + function 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 +
124 + export 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 +
131 + export 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 +
139 + export 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 +
145 + export 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
152 + export 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 + }
Newer Older