- TextAlign: column alignment for embeds using real gg sans font metrics - EmbedHelpers: per-player grid/column layouts immune to 1024-char field limit - Layout: domain-aware formatting wrapper (wrank, bringer, cockroach, tgCount) - PersistentMessage: multi-slot support for independently-updatable embeds - Leaderboard: weekly rankings + highlights embed (most kills/deaths, next Bringer) - Result: per-TG breakdown with wRankAtSubmission snapshot for historical accuracy - /tg call, /tg poll confirm-no, /tg-admin score-inject, result/leaderboard post commands - Fix: CharacterRegistry wasn't hydrating ownerKey, breaking K/D bot-wide - Fix: Leaderboard.buildEntries used current week instead of passed-in week param - /tg-admin test-align: permanent calibration tool for embed text alignment Includes data/emojis/anima-mastery.json for new combat stat icons.
97 lines
No EOL
3.2 KiB
TypeScript
97 lines
No EOL
3.2 KiB
TypeScript
import { Config } from "@systems/config";
|
|
import { submitScore, detectSlot, normalizeSlot } from "@systems/scores";
|
|
import { getEffectiveCharacter } from "@systems/borrow";
|
|
import { format } from "@format";
|
|
import { getEmoji } from "@systems/emojis";
|
|
|
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
|
|
export interface ScoreSubmitInput {
|
|
userKey: string;
|
|
pts: number;
|
|
slot?: number | string | null; // number = already resolved, string = needs normalizing, null/undefined = auto-detect
|
|
k?: number;
|
|
d?: number;
|
|
atk?: number;
|
|
def?: number;
|
|
heal?: number;
|
|
submittedByOfficer?: boolean;
|
|
}
|
|
|
|
export type ScoreSubmitResult =
|
|
| { ok: true; message: string }
|
|
| { ok: false; message: string };
|
|
|
|
// ─── Core ─────────────────────────────────────────────────────────────────────
|
|
|
|
export namespace score {
|
|
/**
|
|
* Resolve, validate and persist a score submission for a given userKey.
|
|
* Used by both the slash command handler and the modal submit handler.
|
|
*/
|
|
export async function submitForUser(input: ScoreSubmitInput): Promise<ScoreSubmitResult> {
|
|
const { userKey, pts, k, d, atk, def, heal, submittedByOfficer = false } = input;
|
|
|
|
const { char, borrowedFrom } = getEffectiveCharacter(userKey);
|
|
if (!char) {
|
|
return { ok: false, message: "❌ No active character found. Use `/tg char set-active` first." };
|
|
}
|
|
|
|
// Resolve slot
|
|
let slot: number | null = null;
|
|
if (typeof input.slot === "number") {
|
|
slot = input.slot;
|
|
} else if (typeof input.slot === "string") {
|
|
slot = normalizeSlot(input.slot);
|
|
if (slot === null) {
|
|
return { ok: false, message: `❌ Could not parse slot "${input.slot}".` };
|
|
}
|
|
} else {
|
|
slot = detectSlot() ?? Config.get({ section: "poll", key: "slots" }).find((s) => s.active)?.tgHour ?? 20;
|
|
}
|
|
|
|
await submitScore({
|
|
userKey: borrowedFrom ?? userKey,
|
|
playedBy: borrowedFrom ? userKey : undefined,
|
|
characterName: char.name,
|
|
cls: char.class,
|
|
nation: char.nation,
|
|
pts,
|
|
k,
|
|
d,
|
|
slot,
|
|
atk,
|
|
def,
|
|
heal,
|
|
submittedByOfficer,
|
|
});
|
|
|
|
const scoreEmoji = getEmoji("score") || "📊";
|
|
const kdEmoji = getEmoji("kd") || "⚔️";
|
|
const kdNote = k !== undefined && d !== undefined ? `\n${kdEmoji} ${k}/${d}` : "";
|
|
const statsNote = [
|
|
atk !== undefined ? `ATK: ${atk}` : null,
|
|
def !== undefined ? `DEF: ${def}` : null,
|
|
heal !== undefined ? `HEAL: ${heal}` : null,
|
|
].filter(Boolean).join(" · ");
|
|
|
|
const charDisplay = format.char(char);
|
|
const borrowNote = borrowedFrom ? ` *(borrowed from ${borrowedFrom})*` : "";
|
|
|
|
const line = format.scoreSubmitLine({
|
|
slot,
|
|
char,
|
|
pts,
|
|
k,
|
|
d,
|
|
atk,
|
|
def,
|
|
heal,
|
|
});
|
|
|
|
return {
|
|
ok: true,
|
|
message: `✅ ${line}${borrowNote}`,
|
|
};
|
|
}
|
|
} |