Last active 1 month ago

score.ts Raw
1/**
2 * Score — manages TG score submission and retrieval.
3 *
4 * Usage:
5 * import { Score } from "@systems/score";
6 *
7 * Score.get({ character, slot, date })
8 * Score.getWeeklySummary({ userKey })
9 * Score.submit({ character, borrowedFrom, pts, k, d, slot })
10 */
11
12 import { Character, Nation, UserKey, SlotHour, TGStats, TGScore } from "@types";
13 import { WRank } from "@systems/wrank";
14 import { Store } from "@systems/store";
15 import { Paths } from "@helpers/paths";
16 import { TGKey } from "@systems/tg-key";
17 import { RuntimeEvents } from "@systems/runtime";
18
19 export interface WeeklySummary {
20 userKey: UserKey;
21 character: Character;
22 scores: TGScore[];
23 totalPts: number;
24 totalK: number;
25 totalD: number;
26 tgCount: number;
27 currentRank?: number;
28 previousRank?: number;
29 }
30
31 function getHistoryPath(historyKey: TGKey): string {
32 return Paths.data("tg-history", `${historyKey}.json`);
33 }
34
35 function loadHistory(historyKey: TGKey): { scores: TGScore[] } {
36 return Store.readOrDefault(getHistoryPath(historyKey), { scores: [] });
37}
38function saveHistory(historyKey: TGKey, data: { scores: TGScore[] }): void {
39 Store.write(getHistoryPath(historyKey), data);
40}
41
42 export const Score = {
43 /**
44 * Get a score for a character in a specific TG.
45 */
46 get({ character, slot, historyKey }: {
47 character: Character;
48 slot: SlotHour;
49 historyKey?: TGKey;
50 }): TGScore | null {
51 const key = historyKey ?? TGKey.current({ slot });
52 const history = loadHistory(key);
53 return history.scores.find(
54 (s) => s.userKey === character.ownerKey && s.characterName === character.name
55 ) ?? null;
56 },
57
58 /**
59 * Get weekly summary for a character.
60 */
61 getWeeklySummary({ character }: { character: Character }): WeeklySummary {
62 const week = WRank.currentWeek();
63 const entry = WRank.entry(character.name, character.nation);
64
65 const scores: TGScore[] = [];
66 for (const historyKey of (week.scoreIndex[character.name] ?? [])) {
67 const history = loadHistory(historyKey as TGKey);
68 const score = history.scores.find(
69 (s) => s.userKey === character.ownerKey && s.characterName === character.name
70 );
71 if (score) scores.push(score);
72 }
73
74 const totalPts = scores.reduce((sum, s) => sum + s.pts, 0);
75 const totalK = scores.reduce((sum, s) => sum + (s.k ?? 0), 0);
76 const totalD = scores.reduce((sum, s) => sum + (s.d ?? 0), 0);
77
78 return {
79 userKey: character.ownerKey,
80 character,
81 scores,
82 totalPts,
83 totalK,
84 totalD,
85 tgCount: scores.length,
86 currentRank: entry?.currentRank,
87 previousRank: entry?.previousRank,
88 };
89 },
90
91 /**
92 * Submit a score for a character.
93 * Handles W.Rank snapshot at submission time.
94 */
95 async submit({ character, playedBy, pts, k, d, atk, def, heal, slot, date, submittedByOfficer }: {
96 character: Character;
97 playedBy?: UserKey;
98 pts: number;
99 k?: number;
100 d?: number;
101 atk?: number;
102 def?: number;
103 heal?: number;
104 slot: SlotHour;
105 date?: string; // ← NEW, optional, defaults to today
106 submittedByOfficer?: boolean;
107 }): Promise<void> {
108 const resolvedDate = date ?? new Date().toISOString().slice(0, 10);
109 const historyKey = TGKey.from({ date: resolvedDate, slot });
110 const history = loadHistory(historyKey);
111
112 const existingEntry = WRank.entry(character.name, character.nation);
113 const wRankAtSubmission = existingEntry ? {
114 rank: existingEntry.currentRank,
115 delta: existingEntry.currentRank - (existingEntry.previousRank ?? existingEntry.currentRank),
116 } : undefined;
117
118 const score: TGScore = {
119 userKey: character.ownerKey,
120 playedBy: playedBy,
121 characterName: character.name,
122 class: character.class.key,
123 nation: character.nation,
124 pts,
125 k,
126 d,
127 stats: atk !== undefined || def !== undefined || heal !== undefined
128 ? { atk, def, heal }
129 : undefined,
130 submittedAt: new Date().toISOString(),
131 slot,
132 date: resolvedDate,
133 submittedByOfficer: submittedByOfficer ?? false,
134 wRankAtSubmission,
135 };
136
137 history.scores = history.scores.filter(
138 (s) => !(s.userKey === character.ownerKey &&
139 s.characterName === character.name &&
140 s.slot === slot &&
141 s.date === resolvedDate)
142 );
143 history.scores.push(score);
144 saveHistory(historyKey, history);
145
146 WRank.recordScore(
147 character.ownerKey,
148 character.name,
149 character.class.key,
150 character.nation,
151 pts,
152 historyKey
153 );
154 await RuntimeEvents.emit("scoreSubmitted", { historyKey, character });
155 },
156 };