179 lines
No EOL
5.4 KiB
TypeScript
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
|
|
);
|
|
},
|
|
}; |