Остання активність 3 weeks ago

nuno ревизій цього gist 3 weeks ago. До ревизії

1 file changed, 238 insertions

gistfile1.txt(файл створено)

@@ -0,0 +1,238 @@
1 + import fs from "fs";
2 + import path from "path";
3 + import { HistoryKey, UserKey, CharName, Nation, ClassKey } from "@types";
4 + import { Config } from "@systems/config";
5 + import { Bringer } from "@systems/bringer";
6 + import { Nations } from "@systems/nations";
7 + import { Store } from "@systems/store";
8 + import { Paths } from "@paths";
9 + import { Runtime } from "@systems/runtime";
10 +
11 + // ─── Runtime ──────────────────────────────────────────────────────────────────
12 + Runtime.phase("load", () => WRank.load(), { name: "WRank.load" });
13 +
14 + const WRANK_PATH = path.join(__dirname, "../../data/wrank.json");
15 + let _data: WRankData = {};
16 +
17 + /** Raw shape stored in wrank.json */
18 + interface SerializableWRankEntry {
19 + userKey: UserKey;
20 + characterName: CharName;
21 + class: ClassKey;
22 + nation: Nation;
23 + weeklyPoints: number;
24 + tgCount: number;
25 + currentRank: number;
26 + previousRank?: number;
27 + }
28 +
29 + export interface WRankWeek {
30 + weekKey: string; // "2026-W22"
31 + entries: Record<"capella" | "procyon", SerializableWRankEntry[]>; // still serializable for now
32 + scoreIndex: Record<CharName, HistoryKey[]>;
33 + bringer: {
34 + capella: string | null; // userKey of bringer, null if none qualified
35 + procyon: string | null;
36 + capellaOverride?: string; // manually set by officer
37 + procyonOverride?: string;
38 + };
39 + }
40 +
41 + export interface WRankData {
42 + [weekKey: string]: WRankWeek;
43 + }
44 +
45 + export function loadWRank(): void {
46 + _data = Store.readOrDefault<WRankData>(Paths.data("wrank.json"), {});
47 + }
48 +
49 + export function saveWRank(): void {
50 + Store.write(Paths.data("wrank.json"), _data);
51 + }
52 +
53 + export function getWeekKey(date: Date = new Date()): string {
54 + const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
55 + d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
56 + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
57 + const week = Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
58 + return `${d.getUTCFullYear()}-W${String(week).padStart(2, "0")}`;
59 + }
60 +
61 + function ensureWeek(weekKey: string): WRankWeek {
62 + if (!_data[weekKey]) {
63 + _data[weekKey] = {
64 + weekKey,
65 + entries: { capella: [], procyon: [] },
66 + scoreIndex: {},
67 + bringer: { capella: null, procyon: null },
68 + };
69 + }
70 + return _data[weekKey];
71 + }
72 +
73 + export function getCurrentWeek(): WRankWeek {
74 + return ensureWeek(WRank.weekKey());
75 + }
76 +
77 + export function getWeek(weekKey: string): WRankWeek | null {
78 + return _data[weekKey] ?? null;
79 + }
80 +
81 + // Add or update a score submission for a player
82 + export function recordScore(
83 + userKey: string,
84 + characterName: string,
85 + cls: ClassKey,
86 + nation: Nation,
87 + pts: number,
88 + historyKey: string // e.g. "2026-05-31-20"
89 + ): void {
90 + const weekKey = WRank.weekKey();
91 + const week = ensureWeek(weekKey);
92 + const list = week.entries[Nations.key(nation)];
93 +
94 + const existing = list.find((e) => e.characterName === characterName);
95 +
96 + if (existing) {
97 + // Check if this slot was already counted
98 + const alreadyCounted = week.scoreIndex[userKey]?.includes(historyKey);
99 + if (!alreadyCounted) {
100 + existing.weeklyPoints += pts;
101 + existing.tgCount += 1;
102 + } else {
103 + // Overwrite: recalculate by removing old pts for this slot
104 + // We'll just set the new pts — full recalc would require reading history
105 + // For now, simple overwrite of total is handled at score submission level
106 + existing.weeklyPoints = existing.weeklyPoints - (existing.weeklyPoints / existing.tgCount) + pts;
107 + }
108 + existing.characterName = characterName;
109 + existing.class = cls;
110 + existing.nation = nation;
111 + } else {
112 + list.push({
113 + userKey,
114 + characterName,
115 + class: cls,
116 + nation,
117 + weeklyPoints: pts,
118 + tgCount: 1,
119 + currentRank: 0,
120 + previousRank: undefined,
121 + });
122 + }
123 +
124 + // Update score index
125 + const indexKey = characterName;
126 + if (!week.scoreIndex[indexKey]) week.scoreIndex[indexKey] = [];
127 + if (!week.scoreIndex[indexKey].includes(historyKey)) {
128 + week.scoreIndex[indexKey].push(historyKey);
129 + }
130 +
131 + recomputeRanks(week, nation);
132 + saveWRank();
133 + }
134 +
135 + function recomputeRanks(week: WRankWeek, nation: Nation): void {
136 + const list = week.entries[nation.toLowerCase() as "capella" | "procyon"];
137 + const sorted = [...list].sort((a, b) => b.weeklyPoints - a.weeklyPoints);
138 + sorted.forEach((entry, i) => {
139 + const live = list.find((e) => e.characterName === entry.characterName)!;
140 + const newRank = i + 1;
141 + // Only snapshot previousRank when rank actually changes
142 + if (live.currentRank !== 0 && live.currentRank !== newRank) {
143 + live.previousRank = live.currentRank;
144 + }
145 + live.currentRank = newRank;
146 + });
147 + }
148 +
149 + function updateBringer(week: WRankWeek): void {
150 + const goal = Config.get({ section: "wrank", key: "goal" });
151 + for (const nation of ["capella", "procyon"] as const) {
152 + // Don't overwrite manual override
153 + if (nation === "capella" && week.bringer.capellaOverride) continue;
154 + if (nation === "procyon" && week.bringer.procyonOverride) continue;
155 +
156 + const qualified = week.entries[nation]
157 + .filter((e) => e.tgCount >= goal)
158 + .sort((a, b) => a.currentRank - b.currentRank);
159 + week.bringer[nation] = qualified[0]?.characterName ?? null;
160 + }
161 + }
162 +
163 + export function setBringerOverride(nation: Nation, charName: string): void {
164 + const week = ensureWeek(WRank.weekKey());
165 + if (nation === Nation.Capella) week.bringer.capellaOverride = charName;
166 + else week.bringer.procyonOverride = charName;
167 + saveWRank();
168 + }
169 +
170 + export function clearBringerOverride(nation: Nation): void {
171 + const week = ensureWeek(WRank.weekKey());
172 + if (nation === Nation.Capella) delete week.bringer.capellaOverride;
173 + else delete week.bringer.procyonOverride;
174 + updateBringer(week);
175 + saveWRank();
176 + }
177 +
178 + export function getBringer(nation: Nation): string | null {
179 + const week = getCurrentWeek();
180 + if (nation === Nation.Capella) return week.bringer.capellaOverride ?? week.bringer.capella;
181 + return week.bringer.procyonOverride ?? week.bringer.procyon;
182 + }
183 +
184 + export function getEntry(characterName: string, nation: Nation): SerializableWRankEntry | null {
185 + const week = getCurrentWeek();
186 + const list = week.entries[nation.toLowerCase() as "capella" | "procyon"];
187 + console.log(`[getEntry] weekKey=${week.weekKey} nation=${nation} listLength=${list?.length} looking for=${characterName}`);
188 + console.log(`[getEntry] available:`, list?.map(e => e.characterName));
189 + return list.find((e) => e.characterName === characterName) ?? null;
190 + }
191 +
192 + // Called every Monday 00:00 by cron
193 + export function resetWeek(): void {
194 + // Week is already archived in _data by weekKey — just ensure next week exists
195 + const prevWeekKey = WRank.weekKey(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000));
196 + const prevWeek = _data[prevWeekKey];
197 + const newWeek = ensureWeek(WRank.weekKey(new Date()));
198 +
199 + if (prevWeek) {
200 + // Carry Bringer forward — W.Rank 1 with goal achieved becomes new Bringer
201 + Bringer.update({ week: prevWeek });
202 + for (const nation of [Nation.Capella, Nation.Procyon]) {
203 + const key = nation === Nation.Capella ? "capella" : "procyon";
204 + const bringer = prevWeek.bringer[key];
205 + if (bringer) {
206 + // Set as override in new week so it carries forward
207 + newWeek.bringer[key] = bringer;
208 + }
209 + }
210 + }
211 + WRank.save();
212 + }
213 +
214 + function snapshot(): void {
215 + const week = WRank.currentWeek();
216 + for (const nation of ["capella", "procyon"] as const) {
217 + for (const entry of week.entries[nation]) {
218 + if (entry.currentRank !== 0) {
219 + entry.previousRank = entry.currentRank;
220 + }
221 + }
222 + }
223 + WRank.save();
224 + console.log("[WRank] Midnight snapshot complete.");
225 + }
226 +
227 +
228 + export const WRank = {
229 + save: saveWRank,
230 + load: loadWRank,
231 + currentWeek: getCurrentWeek,
232 + weekFromKey: getWeek,
233 + weekKey: getWeekKey,
234 + recordScore,
235 + entry: getEntry,
236 + resetWeek,
237 + snapshot,
238 + };
Новіше Пізніше