- 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.
63 lines
No EOL
2.8 KiB
TypeScript
63 lines
No EOL
2.8 KiB
TypeScript
import { ChatInputCommandInteraction } from "discord.js";
|
|
import { Config } from "../../systems/config";
|
|
import { resolveUser, hasOfficerRole } from "../../systems/users";
|
|
import { normalizeSlot, detectSlot } from "../../systems/scores";
|
|
import { loadResult, todayString } from "../../systems/history";
|
|
import { getEmoji } from "../../systems/emojis";
|
|
import { replyAndDelete } from "../../utils";
|
|
|
|
export async function handleScoreGet(interaction: ChatInputCommandInteraction): Promise<void> {
|
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
|
const isOfficer = hasOfficerRole(member, Config.get({ section: "roles", key: "officer" }));
|
|
const nameArg = interaction.options.getString("name");
|
|
const slotArg = interaction.options.getString("slot");
|
|
|
|
if (nameArg && !isOfficer) {
|
|
return void replyAndDelete(interaction, "❌ Only officers can view other players' scores.", true);
|
|
}
|
|
|
|
let userKey: string | null;
|
|
if (nameArg) {
|
|
userKey = nameArg;
|
|
} else {
|
|
const user = await resolveUser(member);
|
|
userKey = user.userKey;
|
|
}
|
|
|
|
if (!userKey) return void replyAndDelete(interaction, "❌ You are not registered in the system.", true);
|
|
|
|
let slot: number | null = null;
|
|
if (slotArg) {
|
|
slot = normalizeSlot(slotArg);
|
|
if (slot === null) return void replyAndDelete(interaction, `❌ Could not parse slot "${slotArg}".`, true);
|
|
} else {
|
|
slot = detectSlot() ?? Config.get({ section: "poll", key: "slots" }).find((s) => s.active)?.tgHour ?? 20;
|
|
}
|
|
|
|
const result = loadResult(todayString(), slot);
|
|
if (!result) return void replyAndDelete(interaction, `❌ No result found for **${slot}:00** TG today.`, true);
|
|
|
|
// Find score — check both direct ownership and borrowed (playedBy)
|
|
const score = result.scores.find(
|
|
(s) => s.userKey === userKey || (s as any).playedBy === userKey
|
|
);
|
|
|
|
if (!score) return void replyAndDelete(interaction, `❌ No score submitted for **${userKey}** in the **${slot}:00** TG.`, true);
|
|
|
|
const scoreEmoji = getEmoji("score") || "📊";
|
|
const kdEmoji = getEmoji("kd") || "⚔️";
|
|
const playedBy = (score as any).playedBy && (score as any).playedBy !== score.userKey
|
|
? `\n*(played by ${(score as any).playedBy})*`
|
|
: "";
|
|
|
|
const lines = [
|
|
`**${score.characterName}** (${score.class} · ${score.nation})${playedBy}`,
|
|
`${scoreEmoji} **${score.pts}** pts`,
|
|
score.stats?.atk !== undefined ? `ATK: ${score.stats.atk}` : null,
|
|
score.stats?.def !== undefined ? `DEF: ${score.stats.def}` : null,
|
|
score.stats?.heal !== undefined ? `HEAL: ${score.stats.heal}` : null,
|
|
`*Submitted at ${new Date(score.submittedAt).toLocaleTimeString("en-GB", { timeZone: process.env.TZ ?? "Etc/GMT-2" })}*`,
|
|
].filter(Boolean).join("\n");
|
|
|
|
return void replyAndDelete(interaction, lines, true);
|
|
} |