Остання активність 1 month ago

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

1 file changed, 156 insertions

score.ts(файл створено)

@@ -0,0 +1,156 @@
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 + }
38 + function 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 + };
Новіше Пізніше