score.ts
· 5.0 KiB · TypeScript
Raw
/**
* 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, SlotHour, TGStats, TGScore } from "@types";
import { WRank } from "@systems/wrank";
import { Store } from "@systems/store";
import { Paths } from "@helpers/paths";
import { TGKey } from "@systems/tg-key";
import { RuntimeEvents } from "@systems/runtime";
export interface WeeklySummary {
userKey: UserKey;
character: Character;
scores: TGScore[];
totalPts: number;
totalK: number;
totalD: number;
tgCount: number;
currentRank?: number;
previousRank?: number;
}
function getHistoryPath(historyKey: TGKey): string {
return Paths.data("tg-history", `${historyKey}.json`);
}
function loadHistory(historyKey: TGKey): { scores: TGScore[] } {
return Store.readOrDefault(getHistoryPath(historyKey), { scores: [] });
}
function saveHistory(historyKey: TGKey, 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, historyKey }: {
character: Character;
slot: SlotHour;
historyKey?: TGKey;
}): TGScore | null {
const key = historyKey ?? TGKey.current({ slot });
const history = loadHistory(key);
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 scores: TGScore[] = [];
for (const historyKey of (week.scoreIndex[character.name] ?? [])) {
const history = loadHistory(historyKey as TGKey);
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.
*/
async submit({ character, playedBy, pts, k, d, atk, def, heal, slot, date, submittedByOfficer }: {
character: Character;
playedBy?: UserKey;
pts: number;
k?: number;
d?: number;
atk?: number;
def?: number;
heal?: number;
slot: SlotHour;
date?: string; // ← NEW, optional, defaults to today
submittedByOfficer?: boolean;
}): Promise<void> {
const resolvedDate = date ?? new Date().toISOString().slice(0, 10);
const historyKey = TGKey.from({ date: resolvedDate, slot });
const history = loadHistory(historyKey);
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: playedBy,
characterName: character.name,
class: character.class.key,
nation: character.nation,
pts,
k,
d,
stats: atk !== undefined || def !== undefined || heal !== undefined
? { atk, def, heal }
: undefined,
submittedAt: new Date().toISOString(),
slot,
date: resolvedDate,
submittedByOfficer: submittedByOfficer ?? false,
wRankAtSubmission,
};
history.scores = history.scores.filter(
(s) => !(s.userKey === character.ownerKey &&
s.characterName === character.name &&
s.slot === slot &&
s.date === resolvedDate)
);
history.scores.push(score);
saveHistory(historyKey, history);
WRank.recordScore(
character.ownerKey,
character.name,
character.class.key,
character.nation,
pts,
historyKey
);
await RuntimeEvents.emit("scoreSubmitted", { historyKey, character });
},
};
| 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 | }; |