/** * Score — manages TG score submission and retrieval. * * Usage: * import { Score } from "@systems/score"; * * Score.get({ character, slot, date }) * Score.getWeeklySummary({ userKey }) * Score.submit({ character, borrowedFrom, pts, k, d, slot }) */ import { Character, Nation, UserKey, HistoryKey, SlotHour } from "@types"; import { WRank } from "@systems/wrank"; import { Store } from "@systems/store"; import { Paths } from "@helpers/paths"; export interface TGScore { userKey: UserKey; playedBy?: UserKey; // if borrowed characterName: string; class: string; nation: Nation; pts: number; k?: number; d?: number; atk?: number; def?: number; heal?: number; submittedAt: string; slot: SlotHour; date: string; submittedByOfficer: boolean; wRankAtSubmission?: { rank: number; delta: number; }; } export interface WeeklySummary { userKey: UserKey; character: Character; scores: TGScore[]; totalPts: number; totalK: number; totalD: number; tgCount: number; currentRank?: number; previousRank?: number; } function getHistoryPath(historyKey: HistoryKey): string { return Paths.data("tg-history", `${historyKey}.json`); } function loadHistory(historyKey: HistoryKey): { scores: TGScore[] } { return Store.readOrDefault(getHistoryPath(historyKey), { scores: [] }); } function saveHistory(historyKey: HistoryKey, data: { scores: TGScore[] }): void { Store.write(getHistoryPath(historyKey), data); } export const Score = { /** * Get a score for a character in a specific TG. */ get({ character, slot, date }: { character: Character; slot: SlotHour; date?: string; }): TGScore | null { const d = date ?? new Date().toISOString().slice(0, 10); const historyKey = `${d}-${slot}` as HistoryKey; const history = loadHistory(historyKey); return history.scores.find( (s) => s.userKey === character.ownerKey && s.characterName === character.name ) ?? null; }, /** * Get weekly summary for a character. */ getWeeklySummary({ character }: { character: Character }): WeeklySummary { const week = WRank.currentWeek(); const entry = WRank.entry(character.name, character.nation); const allKeys = Object.keys(week.scoreIndex[character.name] ?? {}) as HistoryKey[]; const scores: TGScore[] = []; for (const historyKey of (week.scoreIndex[character.name] ?? [])) { const history = loadHistory(historyKey as HistoryKey); const score = history.scores.find( (s) => s.userKey === character.ownerKey && s.characterName === character.name ); if (score) scores.push(score); } const totalPts = scores.reduce((sum, s) => sum + s.pts, 0); const totalK = scores.reduce((sum, s) => sum + (s.k ?? 0), 0); const totalD = scores.reduce((sum, s) => sum + (s.d ?? 0), 0); return { userKey: character.ownerKey, character, scores, totalPts, totalK, totalD, tgCount: scores.length, currentRank: entry?.currentRank, previousRank: entry?.previousRank, }; }, /** * Submit a score for a character. * Handles W.Rank snapshot at submission time. */ submit({ character, borrowedFrom, pts, k, d, atk, def, heal, slot, submittedByOfficer }: { character: Character; borrowedFrom?: UserKey; pts: number; k?: number; d?: number; atk?: number; def?: number; heal?: number; slot: SlotHour; submittedByOfficer?: boolean; }): void { const date = new Date().toISOString().slice(0, 10); const historyKey = `${date}-${slot}` as HistoryKey; const history = loadHistory(historyKey); // Snapshot W.Rank before recording score const existingEntry = WRank.entry(character.name, character.nation); const wRankAtSubmission = existingEntry ? { rank: existingEntry.currentRank, delta: existingEntry.currentRank - (existingEntry.previousRank ?? existingEntry.currentRank), } : undefined; const score: TGScore = { userKey: character.ownerKey, playedBy: borrowedFrom, characterName: character.name, class: character.class.key, nation: character.nation, pts, k, d, atk, def, heal, submittedAt: new Date().toISOString(), slot, date, submittedByOfficer: submittedByOfficer ?? false, wRankAtSubmission, }; // Upsert — replace existing score for same character/slot history.scores = history.scores.filter( (s) => !(s.userKey === character.ownerKey && s.characterName === character.name && s.slot === slot && s.date === date) ); history.scores.push(score); saveHistory(historyKey, history); // Record in W.Rank WRank.recordScore( character.ownerKey, character.name, character.class.key, character.nation, pts, historyKey ); }, };