tg-bot-ts/src/systems/score.ts

179 lines
No EOL
5.4 KiB
TypeScript

/**
* 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
);
},
};