tg-bot-ts/src/ui/embed-helpers.ts
Nuno Duque Nunes 9e8877483d feat: Leaderboard & Result systems with aligned columns, call/confirm-no commands, persistent message slots
- 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.
2026-06-20 03:04:52 +01:00

133 lines
No EOL
4.1 KiB
TypeScript

/**
* EmbedHelpers — utilities for working within Discord embed limits
* and controlling inline field grid layout.
*
* Usage:
* import { EmbedHelpers } from "@ui/embed-helpers";
*
* EmbedHelpers.chunkRows(rows)
* EmbedHelpers.addNationFields(embed, header, rows, inline)
* EmbedHelpers.addPerPlayerGrid(embed, [{ header, rows }, { header, rows }])
*/
interface EmbedLike {
addFields: (...fields: { name: string; value: string; inline: boolean }[]) => any;
}
// ─── Chunking (for non-inline / single-field cases) ──────────────────────────
function chunkRows(rows: string[], separator: string = "\n", maxLen: number = 1024): string[] {
if (rows.length === 0) return [];
const chunks: string[] = [];
let current = "";
for (const row of rows) {
const candidate = current ? `${current}${separator}${row}` : row;
if (candidate.length > maxLen && current) {
chunks.push(current);
current = row;
} else {
current = candidate;
}
}
if (current) chunks.push(current);
console.log(`[EmbedHelpers] chunkRows: ${rows.length} rows -> ${chunks.length} chunk(s), lengths: ${chunks.map(c => c.length).join(", ")}`);
return chunks;
}
function addNationFields(
embed: EmbedLike,
header: string,
rows: string[],
inline: boolean = false
): void {
const chunks = chunkRows(rows);
if (chunks.length === 0) {
embed.addFields({ name: header, value: "—", inline });
return;
}
chunks.forEach((chunk, i) => {
embed.addFields({
name: i === 0 ? header : "\u200b",
value: chunk,
inline,
});
});
}
// ─── Per-player grid ──────────────────────────────────────────────────────────
/**
* Render two columns as a clean 2-column grid, one player per field row.
* The nation header is embedded as bold text at the top of the first
* player's field (not a separate field) to avoid an extra header-to-content gap.
*/
function addPerPlayerGrid(
embed: EmbedLike,
columns: { header: string; rows: string[] }[]
): void {
if (columns.length !== 2) {
for (const col of columns) {
addNationFields(embed, col.header, col.rows, true);
}
return;
}
const [left, right] = columns;
const maxLen = Math.max(left.rows.length, right.rows.length, 1);
for (let i = 0; i < maxLen; i++) {
const leftRow = left.rows[i];
const rightRow = right.rows[i];
const leftValue = i === 0
? `**${left.header}**\n${leftRow ?? "—"}`
: (leftRow ?? "\u200b");
const rightValue = i === 0
? `**${right.header}**\n${rightRow ?? "—"}`
: (rightRow ?? "\u200b");
embed.addFields(
{ name: "\u200b", value: leftValue, inline: true },
{ name: "\u200b", value: rightValue, inline: true },
{ name: "\u200b", value: "\u200b", inline: true },
);
}
}
// ─── Single-column per-player fields ──────────────────────────────────────────
/**
* Render a single-column list, one player per field (no chunking needed —
* each field only holds one row, well under the 1024 char limit regardless
* of padding/content length). Header embedded as bold text in the first
* player's field to avoid an extra header-to-content gap.
*/
function addPerPlayerColumn(
embed: EmbedLike,
header: string,
rows: string[]
): void {
if (rows.length === 0) {
embed.addFields({ name: header, value: "—", inline: false });
return;
}
rows.forEach((row, i) => {
const value = i === 0 ? `**${header}**\n\u200b\n${row}` : row;
embed.addFields({ name: "\u200b", value, inline: false });
});
}
// ─── Namespace ────────────────────────────────────────────────────────────────
export const EmbedHelpers = {
chunkRows,
addNationFields,
addPerPlayerGrid,
addPerPlayerColumn,
};