/** * Default poll layout — vertical, nation-separated fields. * This is the standard layout and always the fallback. */ import { EmbedBuilder } from "discord.js"; import { PollState, VoteEntry, Nation } from "@types"; import { WRankEntry } from "@systems/wrank"; import { Config } from "@systems/config"; import { WRank } from "@systems/wrank"; import { Bringer } from "@systems/bringer"; import { Emoji } from "@systems/emojis"; import { format } from "@format"; import { PollLayout, PollRowContext, PollEmbedOptions } from "@ui/types"; import { Leaves } from "@systems/leaves"; // ─── Row formatting ─────────────────────────────────────────────────────────── function formatWRank( wRankEntry: WRankEntry | null, context: PollRowContext ): string { if (wRankEntry) { const goal = Config.get({ section: "wrank", key: "goal" }); return format.wrank.full(wRankEntry, { goal, brackets: true }); } if (!context.nationHasRank) return ""; if (context.nationHasDelta) return format.wrank.noRank(); return "—"; } function formatRow(entry: VoteEntry, context: PollRowContext): string { const cfgFormat = Config.get({ section: "poll", key: "charDisplayFormat" }); const nation = entry.characterNation; const wRankEntry = entry.characterName && entry.characterNation ? WRank.entry(entry.characterName, entry.characterNation) : null; const wrank = formatWRank(wRankEntry, context); const classStr = entry.characterClass ? (Emoji.class(entry.characterClass) || entry.characterClass) : ""; const levelStr = entry.characterLevel ? `${entry.characterLevel}` : ""; let row = cfgFormat .replace("{wrank}", wrank) .replace("{class}", classStr) .replace("{level}", levelStr) .replace("{name}", entry.characterName ?? entry.displayName ?? "") .replace(/\s+/g, " ") .trim(); if (nation && entry.userKey) { const bringer = Bringer.get({ nation }); if (bringer && bringer === entry.characterName) { row += ` · ${format.bringer(nation)}`; } } if (entry.userKey && context.historyKey) { if (Leaves.hasLeft({ userKey: entry.userKey, historyKey: context.historyKey })) { row += ` ${Leaves.formatIndicator({ userKey: entry.userKey })}`; } } if (entry.borrowedFrom) row += ` ${Emoji.get("borrowed") || "🔗"}`; if (context.showNationEmoji && nation) row = `${Emoji.nation(nation)} ${row}`; return row; } function buildContext( entries: VoteEntry[], nation: Nation, options?: { showNationEmoji?: boolean; historyKey?: string } ): PollRowContext { const nationHasRank = entries.some((e) => e.characterName && WRank.entry(e.characterName, nation) !== null ); const nationHasDelta = entries.some((e) => { const wr = e.characterName ? WRank.entry(e.characterName, nation) : null; return wr?.previousRank !== undefined; }); return { nationHasRank, nationHasDelta, showNationEmoji: options?.showNationEmoji ?? false, historyKey: options?.historyKey, }; } // ─── Embed building ─────────────────────────────────────────────────────────── function formatNationField( nation: Nation, yesEntries: VoteEntry[], noVoters: VoteEntry[], showNoInline: boolean ): string { const context = buildContext(yesEntries, nation); const noEntries = showNoInline ? noVoters.filter((e) => e.characterNation === nation) : []; const lines = [ ...yesEntries.map((e) => formatRow(e, context)), ...noEntries.map((e) => `❌ ${formatRow(e, context)}`), ]; return lines.length > 0 ? lines.join("\n") : "—"; } function formatMessages( allMessages: { entry: VoteEntry; voteType: "yes" | "no" }[] ): string { if (allMessages.length === 0) return ""; return allMessages .map((m) => { const name = m.entry.characterName ?? m.entry.displayName; const prefix = m.voteType === "no" ? "✗ " : "✓ "; const msg = m.entry.publicMessage ? ` — ${m.entry.publicMessage}` : ""; return `${prefix}${name} · ${m.entry.votedAt}${msg}`; }) .join("\n"); } function resolveColor(state: PollState): number { if (state.confirmed === "yes") return 0x57f287; if (state.confirmed === "no") return 0xed4245; if (state.locked) return 0x888888; return 0xe8a317; } function resolveTitle( state: PollState, yesByNation: Record ): string { const capellaEmoji = Emoji.get("capella"); const procyonEmoji = Emoji.get("procyon"); const counts = !state.locked && state.confirmed === null ? ` ${capellaEmoji} ${yesByNation[Nation.Capella].length} ${procyonEmoji} ${yesByNation[Nation.Procyon].length}` : ""; const statusSuffix = state.locked ? " 🔒" : state.confirmed === "yes" ? " ✅" : state.confirmed === "no" ? " ❌" : ""; return `⚔️ TG — ${state.slot}:00${counts}${statusSuffix}`; } function resolveFooter(state: PollState, noCount: number, overrideLockMsg?: string): string { if (state.confirmed === "yes") return Config.get({ section: "poll", key: "confirmYes" }); if (state.confirmed === "no") return Config.get({ section: "poll", key: "confirmNo" }); if (state.locked) return overrideLockMsg ?? Config.get({ section: "poll", key: "lockMessage" }); return `❌ ${noCount} • Vote updates live • Anyone can vote • /tg switch to change character`; } function buildEmbed(state: PollState, options?: PollEmbedOptions): EmbedBuilder { const yesByNation: Record = { [Nation.Capella]: [], [Nation.Procyon]: [], }; const noVoters: VoteEntry[] = []; const allMessages: { entry: VoteEntry; voteType: "yes" | "no" }[] = []; const showNoInline = Config.get({ section: "poll", key: "showNoInNationField" }); for (const entry of state.yes.values()) { const nation = entry.characterNation ?? Nation.Capella; yesByNation[nation].push(entry); allMessages.push({ entry, voteType: "yes" }); } for (const entry of state.no.values()) { noVoters.push(entry); allMessages.push({ entry, voteType: "no" }); } const capellaEmoji = Emoji.get("capella"); const procyonEmoji = Emoji.get("procyon"); const embed = new EmbedBuilder() .setTitle(resolveTitle(state, yesByNation)) .setColor(resolveColor(state)) .addFields( { name: `${capellaEmoji} Capella (${yesByNation[Nation.Capella].length})`, value: formatNationField(Nation.Capella, yesByNation[Nation.Capella], noVoters, showNoInline), inline: false, }, { name: "\u200b", value: "\u200b", inline: false }, { name: `${procyonEmoji} Procyon (${yesByNation[Nation.Procyon].length})`, value: formatNationField(Nation.Procyon, yesByNation[Nation.Procyon], noVoters, showNoInline), inline: false, }, ) .setTimestamp(); const msgSection = formatMessages(allMessages); if (msgSection) { embed.addFields({ name: "\u200b", value: msgSection, inline: false }); } embed.setFooter({ text: resolveFooter(state, noVoters.length, options?.overrideLockMsg) }); return embed; } // ─── Layout export ──────────────────────────────────────────────────────────── export const defaultLayout: PollLayout = { name: "default", description: "Standard vertical layout with nation-separated fields", buildEmbed, formatRow, buildContext, };