feat: TGKey branded type, PersistentMessage abstraction, createBuildEmbed factory, BaseLayout refactor
This commit is contained in:
parent
a4d772b81d
commit
b22602f431
16 changed files with 218 additions and 206 deletions
|
|
@ -27,7 +27,7 @@
|
||||||
"userKey": "ayana",
|
"userKey": "ayana",
|
||||||
"displayName": "Ayana",
|
"displayName": "Ayana",
|
||||||
"characterName": "«MonkeyHunter»",
|
"characterName": "«MonkeyHunter»",
|
||||||
"characterClass": "GL",
|
"characterClass": "DM",
|
||||||
"characterLevel": 79,
|
"characterLevel": 79,
|
||||||
"characterNation": "Procyon",
|
"characterNation": "Procyon",
|
||||||
"votedAt": "19:46"
|
"votedAt": "19:46"
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
"no": [],
|
"no": [],
|
||||||
"leaves": [
|
"leaves": [
|
||||||
{
|
{
|
||||||
"characterName": "»Flash«",
|
"characterName": " «Deystroyer»",
|
||||||
"historyKey": "2026-06-11-20"
|
"historyKey": "2026-06-11-20"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,7 @@ import { Leaves } from "@systems/leaves";
|
||||||
import { polls, updatePollMessage } from "@systems/poll";
|
import { polls, updatePollMessage } from "@systems/poll";
|
||||||
import { CharacterRegistry } from "@registry/character-registry";
|
import { CharacterRegistry } from "@registry/character-registry";
|
||||||
import { replyAndDelete } from "@utils";
|
import { replyAndDelete } from "@utils";
|
||||||
|
import { TGKey } from "@systems/tg-key";
|
||||||
function getCurrentHistoryKey(slot: number): string {
|
|
||||||
const date = new Date().toISOString().slice(0, 10);
|
|
||||||
return `${date}-${slot}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleMarkLeft(interaction: ChatInputCommandInteraction): Promise<void> {
|
export async function handleMarkLeft(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||||
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
const member = await interaction.guild!.members.fetch(interaction.user.id);
|
||||||
|
|
@ -27,7 +23,7 @@ export async function handleMarkLeft(interaction: ChatInputCommandInteraction):
|
||||||
const slot = [...polls.keys()][0];
|
const slot = [...polls.keys()][0];
|
||||||
if (slot === undefined) return void replyAndDelete(interaction, "❌ No active poll.", true);
|
if (slot === undefined) return void replyAndDelete(interaction, "❌ No active poll.", true);
|
||||||
|
|
||||||
const historyKey = getCurrentHistoryKey(slot);
|
const historyKey = TGKey.current({ slot });
|
||||||
|
|
||||||
Leaves.mark({
|
Leaves.mark({
|
||||||
characterName: char.name,
|
characterName: char.name,
|
||||||
|
|
@ -59,7 +55,7 @@ export async function handleUnmarkLeft(interaction: ChatInputCommandInteraction)
|
||||||
const slot = [...polls.keys()][0];
|
const slot = [...polls.keys()][0];
|
||||||
if (slot === undefined) return void replyAndDelete(interaction, "❌ No active poll.", true);
|
if (slot === undefined) return void replyAndDelete(interaction, "❌ No active poll.", true);
|
||||||
|
|
||||||
const historyKey = getCurrentHistoryKey(slot);
|
const historyKey = TGKey.current({ slot });
|
||||||
Leaves.unmark({ characterName: char.name, historyKey });
|
Leaves.unmark({ characterName: char.name, historyKey });
|
||||||
|
|
||||||
const channel = interaction.channel as TextChannel;
|
const channel = interaction.channel as TextChannel;
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,12 @@
|
||||||
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { Paths } from "@paths";
|
import { Paths } from "@paths";
|
||||||
import { UserKey, HistoryKey } from "@types";
|
import { UserKey } from "@types";
|
||||||
import { Store } from "@systems/store";
|
import { Store } from "@systems/store";
|
||||||
|
import { TGKey } from "@systems/tg-key";
|
||||||
|
|
||||||
interface AttendanceData {
|
interface AttendanceData {
|
||||||
[historyKey: HistoryKey]: UserKey[];
|
[historyKey: TGKey]: UserKey[];
|
||||||
}
|
}
|
||||||
|
|
||||||
let _data: AttendanceData = {};
|
let _data: AttendanceData = {};
|
||||||
|
|
@ -34,8 +35,7 @@ function save(): void {
|
||||||
* Snapshot attendance from poll state at lock time.
|
* Snapshot attendance from poll state at lock time.
|
||||||
*/
|
*/
|
||||||
snapshot(slot: number, lockedYesKeys: Set<UserKey>): void {
|
snapshot(slot: number, lockedYesKeys: Set<UserKey>): void {
|
||||||
const date = new Date().toISOString().slice(0, 10);
|
const historyKey = TGKey.current({ slot });
|
||||||
const historyKey = `${date}-${slot}` as HistoryKey;
|
|
||||||
_data[historyKey] = [...lockedYesKeys];
|
_data[historyKey] = [...lockedYesKeys];
|
||||||
save();
|
save();
|
||||||
},
|
},
|
||||||
|
|
@ -43,21 +43,21 @@ function save(): void {
|
||||||
/**
|
/**
|
||||||
* Get players who attended a specific TG.
|
* Get players who attended a specific TG.
|
||||||
*/
|
*/
|
||||||
players(historyKey: HistoryKey): UserKey[] {
|
players(historyKey: TGKey): UserKey[] {
|
||||||
return _data[historyKey] ?? [];
|
return _data[historyKey] ?? [];
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a specific player attended.
|
* Check if a specific player attended.
|
||||||
*/
|
*/
|
||||||
includes(historyKey: HistoryKey, userKey: UserKey): boolean {
|
includes(historyKey: TGKey, userKey: UserKey): boolean {
|
||||||
return (_data[historyKey] ?? []).includes(userKey);
|
return (_data[historyKey] ?? []).includes(userKey);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if all attendees have submitted scores.
|
* Check if all attendees have submitted scores.
|
||||||
*/
|
*/
|
||||||
allSubmitted(historyKey: HistoryKey): boolean {
|
allSubmitted(historyKey: TGKey): boolean {
|
||||||
const players = _data[historyKey] ?? [];
|
const players = _data[historyKey] ?? [];
|
||||||
if (players.length === 0) return false;
|
if (players.length === 0) return false;
|
||||||
try {
|
try {
|
||||||
|
|
@ -74,7 +74,7 @@ function save(): void {
|
||||||
/**
|
/**
|
||||||
* Get all history keys (for listing past TGs).
|
* Get all history keys (for listing past TGs).
|
||||||
*/
|
*/
|
||||||
all(): HistoryKey[] {
|
all(): TGKey[] {
|
||||||
return Object.keys(_data) as HistoryKey[];
|
return Object.keys(_data) as TGKey[];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -4,23 +4,22 @@ import { TGResult, TGScore, Nation } from "../types";
|
||||||
import { Nations } from "@systems/nations";
|
import { Nations } from "@systems/nations";
|
||||||
import { Store } from "@systems/store";
|
import { Store } from "@systems/store";
|
||||||
import { Paths } from "@helpers/paths";
|
import { Paths } from "@helpers/paths";
|
||||||
|
import { TGKey } from "@systems/tg-key";
|
||||||
|
|
||||||
const HISTORY_DIR = path.join(__dirname, "../../data/tg-history");
|
const HISTORY_DIR = path.join(__dirname, "../../data/tg-history");
|
||||||
|
|
||||||
function historyKey(date: string, slot: number): string {
|
|
||||||
return `${date}-${String(slot).padStart(2, "0")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function historyPath(key: string): string {
|
function historyPath(key: string): string {
|
||||||
return path.join(HISTORY_DIR, `${key}.json`);
|
return path.join(HISTORY_DIR, `${key}.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadResult(date: string, slot: number): TGResult | null {
|
export function loadResult(date: string, slot: number): TGResult | null {
|
||||||
return Store.read(historyPath(historyKey(date, slot)));
|
const path = historyPath(TGKey.from({ date, slot }));
|
||||||
|
return Store.read(path);
|
||||||
}
|
}
|
||||||
export function saveResult(result: TGResult): void {
|
export function saveResult(result: TGResult): void {
|
||||||
if (!fs.existsSync(HISTORY_DIR)) fs.mkdirSync(HISTORY_DIR, { recursive: true });
|
if (!fs.existsSync(HISTORY_DIR)) fs.mkdirSync(HISTORY_DIR, { recursive: true });
|
||||||
Store.write(historyPath(historyKey(result.date, result.slot)), result);
|
const path = historyPath(TGKey.from({ date: result.date, slot: result.slot }));
|
||||||
|
Store.write(path, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function upsertScore(score: TGScore): void {
|
export function upsertScore(score: TGScore): void {
|
||||||
|
|
|
||||||
|
|
@ -15,18 +15,19 @@
|
||||||
* Leaves.formatIndicator({ characterName })
|
* Leaves.formatIndicator({ characterName })
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { UserKey, CharName, HistoryKey } from "@types";
|
import { UserKey, CharName } from "@types";
|
||||||
import { Store } from "@systems/store";
|
import { Store } from "@systems/store";
|
||||||
import { Paths } from "@paths";
|
import { Paths } from "@paths";
|
||||||
import { Emoji } from "@systems/emojis";
|
import { Emoji } from "@systems/emojis";
|
||||||
import { Runtime } from "@systems/runtime";
|
import { Runtime } from "@systems/runtime";
|
||||||
|
import { TGKey } from "@systems/tg-key";
|
||||||
|
|
||||||
Runtime.phase("load", () => Leaves.load(), { name: "Leaves.load" });
|
Runtime.phase("load", () => Leaves.load(), { name: "Leaves.load" });
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface LeaveRecord {
|
interface LeaveRecord {
|
||||||
historyKey: HistoryKey;
|
historyKey: TGKey;
|
||||||
markedBy: UserKey;
|
markedBy: UserKey;
|
||||||
markedAt: string;
|
markedAt: string;
|
||||||
}
|
}
|
||||||
|
|
@ -60,7 +61,7 @@
|
||||||
mark({ characterName, ownerKey, historyKey, markedBy }: {
|
mark({ characterName, ownerKey, historyKey, markedBy }: {
|
||||||
characterName: CharName;
|
characterName: CharName;
|
||||||
ownerKey: UserKey;
|
ownerKey: UserKey;
|
||||||
historyKey: HistoryKey;
|
historyKey: TGKey;
|
||||||
markedBy: UserKey;
|
markedBy: UserKey;
|
||||||
}): void {
|
}): void {
|
||||||
if (!_data[characterName]) {
|
if (!_data[characterName]) {
|
||||||
|
|
@ -75,7 +76,7 @@
|
||||||
|
|
||||||
unmark({ characterName, historyKey }: {
|
unmark({ characterName, historyKey }: {
|
||||||
characterName: CharName;
|
characterName: CharName;
|
||||||
historyKey: HistoryKey;
|
historyKey: TGKey;
|
||||||
}): void {
|
}): void {
|
||||||
if (!_data[characterName]) return;
|
if (!_data[characterName]) return;
|
||||||
_data[characterName].history = _data[characterName].history.filter(
|
_data[characterName].history = _data[characterName].history.filter(
|
||||||
|
|
@ -87,7 +88,7 @@
|
||||||
|
|
||||||
hasLeft({ characterName, historyKey }: {
|
hasLeft({ characterName, historyKey }: {
|
||||||
characterName: CharName;
|
characterName: CharName;
|
||||||
historyKey: HistoryKey;
|
historyKey: TGKey;
|
||||||
}): boolean {
|
}): boolean {
|
||||||
return _data[characterName]?.history.some((r) => r.historyKey === historyKey) ?? false;
|
return _data[characterName]?.history.some((r) => r.historyKey === historyKey) ?? false;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,11 @@
|
||||||
* Score.submit({ character, borrowedFrom, pts, k, d, slot })
|
* Score.submit({ character, borrowedFrom, pts, k, d, slot })
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Character, Nation, UserKey, HistoryKey, SlotHour } from "@types";
|
import { Character, Nation, UserKey, SlotHour } from "@types";
|
||||||
import { WRank } from "@systems/wrank";
|
import { WRank } from "@systems/wrank";
|
||||||
import { Store } from "@systems/store";
|
import { Store } from "@systems/store";
|
||||||
import { Paths } from "@helpers/paths";
|
import { Paths } from "@helpers/paths";
|
||||||
|
import { TGKey } from "@systems/tg-key";
|
||||||
|
|
||||||
export interface TGScore {
|
export interface TGScore {
|
||||||
userKey: UserKey;
|
userKey: UserKey;
|
||||||
|
|
@ -48,14 +49,14 @@
|
||||||
previousRank?: number;
|
previousRank?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHistoryPath(historyKey: HistoryKey): string {
|
function getHistoryPath(historyKey: TGKey): string {
|
||||||
return Paths.data("tg-history", `${historyKey}.json`);
|
return Paths.data("tg-history", `${historyKey}.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadHistory(historyKey: HistoryKey): { scores: TGScore[] } {
|
function loadHistory(historyKey: TGKey): { scores: TGScore[] } {
|
||||||
return Store.readOrDefault(getHistoryPath(historyKey), { scores: [] });
|
return Store.readOrDefault(getHistoryPath(historyKey), { scores: [] });
|
||||||
}
|
}
|
||||||
function saveHistory(historyKey: HistoryKey, data: { scores: TGScore[] }): void {
|
function saveHistory(historyKey: TGKey, data: { scores: TGScore[] }): void {
|
||||||
Store.write(getHistoryPath(historyKey), data);
|
Store.write(getHistoryPath(historyKey), data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,13 +64,11 @@ function saveHistory(historyKey: HistoryKey, data: { scores: TGScore[] }): void
|
||||||
/**
|
/**
|
||||||
* Get a score for a character in a specific TG.
|
* Get a score for a character in a specific TG.
|
||||||
*/
|
*/
|
||||||
get({ character, slot, date }: {
|
get({ character, slot }: {
|
||||||
character: Character;
|
character: Character;
|
||||||
slot: SlotHour;
|
slot: SlotHour;
|
||||||
date?: string;
|
|
||||||
}): TGScore | null {
|
}): TGScore | null {
|
||||||
const d = date ?? new Date().toISOString().slice(0, 10);
|
const historyKey = TGKey.current({ slot });
|
||||||
const historyKey = `${d}-${slot}` as HistoryKey;
|
|
||||||
const history = loadHistory(historyKey);
|
const history = loadHistory(historyKey);
|
||||||
return history.scores.find(
|
return history.scores.find(
|
||||||
(s) => s.userKey === character.ownerKey && s.characterName === character.name
|
(s) => s.userKey === character.ownerKey && s.characterName === character.name
|
||||||
|
|
@ -82,11 +81,10 @@ function saveHistory(historyKey: HistoryKey, data: { scores: TGScore[] }): void
|
||||||
getWeeklySummary({ character }: { character: Character }): WeeklySummary {
|
getWeeklySummary({ character }: { character: Character }): WeeklySummary {
|
||||||
const week = WRank.currentWeek();
|
const week = WRank.currentWeek();
|
||||||
const entry = WRank.entry(character.name, character.nation);
|
const entry = WRank.entry(character.name, character.nation);
|
||||||
const allKeys = Object.keys(week.scoreIndex[character.name] ?? {}) as HistoryKey[];
|
|
||||||
|
|
||||||
const scores: TGScore[] = [];
|
const scores: TGScore[] = [];
|
||||||
for (const historyKey of (week.scoreIndex[character.name] ?? [])) {
|
for (const historyKey of (week.scoreIndex[character.name] ?? [])) {
|
||||||
const history = loadHistory(historyKey as HistoryKey);
|
const history = loadHistory(historyKey as TGKey);
|
||||||
const score = history.scores.find(
|
const score = history.scores.find(
|
||||||
(s) => s.userKey === character.ownerKey && s.characterName === character.name
|
(s) => s.userKey === character.ownerKey && s.characterName === character.name
|
||||||
);
|
);
|
||||||
|
|
@ -127,7 +125,7 @@ function saveHistory(historyKey: HistoryKey, data: { scores: TGScore[] }): void
|
||||||
submittedByOfficer?: boolean;
|
submittedByOfficer?: boolean;
|
||||||
}): void {
|
}): void {
|
||||||
const date = new Date().toISOString().slice(0, 10);
|
const date = new Date().toISOString().slice(0, 10);
|
||||||
const historyKey = `${date}-${slot}` as HistoryKey;
|
const historyKey = TGKey.current({ slot });
|
||||||
const history = loadHistory(historyKey);
|
const history = loadHistory(historyKey);
|
||||||
|
|
||||||
// Snapshot W.Rank before recording score
|
// Snapshot W.Rank before recording score
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { TGScore, Nation, ClassKey } from "../types";
|
||||||
import { Config } from "./config";
|
import { Config } from "./config";
|
||||||
import { upsertScore, todayString } from "./history";
|
import { upsertScore, todayString } from "./history";
|
||||||
import { WRank } from "./wrank";
|
import { WRank } from "./wrank";
|
||||||
|
import { TGKey } from "@systems/tg-key";
|
||||||
|
|
||||||
// Normalize a slot string to a 24h integer hour
|
// Normalize a slot string to a 24h integer hour
|
||||||
// Accepts: "20", "8", "8pm", "20:00", "midnight", "midday", "noon"
|
// Accepts: "20", "8", "8pm", "20:00", "midnight", "midday", "noon"
|
||||||
|
|
@ -71,7 +72,7 @@ export interface ScoreSubmission {
|
||||||
|
|
||||||
export function submitScore(sub: ScoreSubmission): void {
|
export function submitScore(sub: ScoreSubmission): void {
|
||||||
const date = sub.date ?? todayString();
|
const date = sub.date ?? todayString();
|
||||||
const historyKey = `${date}-${String(sub.slot).padStart(2, "0")}`;
|
const historyKey = TGKey.from({ date, slot: sub.slot });
|
||||||
|
|
||||||
const score: TGScore = {
|
const score: TGScore = {
|
||||||
userKey: sub.userKey,
|
userKey: sub.userKey,
|
||||||
|
|
|
||||||
52
src/systems/tg-key.ts
Normal file
52
src/systems/tg-key.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
/**
|
||||||
|
* TGKey — branded type for TG session identifiers.
|
||||||
|
* Format: "YYYY-MM-DD-SLOT" e.g. "2026-06-11-20"
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { TGKey } from "@systems/tg-key";
|
||||||
|
*
|
||||||
|
* const key = TGKey.current({ slot: 20 })
|
||||||
|
* const key = TGKey.from({ date: "2026-06-11", slot: 20 })
|
||||||
|
* TGKey.parse(key) // { date: "2026-06-11", slot: 20 }
|
||||||
|
* TGKey.toHistoryPath(key) // ".../tg-history/2026-06-11-20.json"
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Paths } from "@paths";
|
||||||
|
|
||||||
|
export type TGKey = string & { readonly __brand: "TGKey" };
|
||||||
|
|
||||||
|
export const TGKey = {
|
||||||
|
from({ date, slot }: { date: Date | string; slot: number }): TGKey {
|
||||||
|
const d = date instanceof Date ? date.toISOString().slice(0, 10) : date;
|
||||||
|
const s = String(slot).padStart(2, "0");
|
||||||
|
return `${d}-${s}` as TGKey;
|
||||||
|
},
|
||||||
|
|
||||||
|
current({ slot }: { slot: number }): TGKey {
|
||||||
|
return TGKey.from({ date: new Date(), slot });
|
||||||
|
},
|
||||||
|
|
||||||
|
parse(key: TGKey): { date: string; slot: number } {
|
||||||
|
const parts = key.split("-");
|
||||||
|
return {
|
||||||
|
date: parts.slice(0, 3).join("-"), // "YYYY-MM-DD"
|
||||||
|
slot: parseInt(parts[3], 10),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toHistoryPath(key: TGKey): string {
|
||||||
|
return Paths.data("tg-history", `${key}.json`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Format for display: "11/06/2026 · 20:00" */
|
||||||
|
toDisplay(key: TGKey): string {
|
||||||
|
const { date, slot } = TGKey.parse(key);
|
||||||
|
const [year, month, day] = date.split("-");
|
||||||
|
return `${day}/${month}/${year} · ${slot}:00`;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Check if a string is a valid TGKey */
|
||||||
|
isValid(key: string): key is TGKey {
|
||||||
|
return /^\d{4}-\d{2}-\d{2}-\d{1,2}$/.test(key);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -12,13 +12,13 @@
|
||||||
* TG.getWeeklySummary({ character })
|
* TG.getWeeklySummary({ character })
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Nation, Character, UserKey, HistoryKey, SlotHour } from "@types";
|
import { Nation, Character, UserKey, SlotHour } from "@types";
|
||||||
import { WRank } from "@systems/wrank";
|
import { WRank } from "@systems/wrank";
|
||||||
import { Bringer } from "@systems/bringer";
|
import { Bringer } from "@systems/bringer";
|
||||||
import { Score, TGScore, WeeklySummary } from "@systems/score";
|
import { Score, TGScore, WeeklySummary } from "@systems/score";
|
||||||
import { Attendance } from "@systems/attendance";
|
import { Attendance } from "@systems/attendance";
|
||||||
import { Nations } from "@systems/nations";
|
|
||||||
import { Config } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
|
import { TGKey } from "@systems/tg-key";
|
||||||
|
|
||||||
export const TG = {
|
export const TG = {
|
||||||
// ── Week ──────────────────────────────────────────────────────────────────
|
// ── Week ──────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -66,7 +66,7 @@
|
||||||
// ── Attendance ────────────────────────────────────────────────────────────
|
// ── Attendance ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
getAttendance({ historyKey, nation }: {
|
getAttendance({ historyKey, nation }: {
|
||||||
historyKey: HistoryKey;
|
historyKey: TGKey;
|
||||||
nation?: Nation;
|
nation?: Nation;
|
||||||
}): UserKey[] {
|
}): UserKey[] {
|
||||||
const players = Attendance.players(historyKey);
|
const players = Attendance.players(historyKey);
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
return players;
|
return players;
|
||||||
},
|
},
|
||||||
|
|
||||||
allSubmitted(historyKey: HistoryKey): boolean {
|
allSubmitted(historyKey: TGKey): boolean {
|
||||||
return Attendance.allSubmitted(historyKey);
|
return Attendance.allSubmitted(historyKey);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -86,7 +86,7 @@
|
||||||
slot: SlotHour;
|
slot: SlotHour;
|
||||||
date?: string;
|
date?: string;
|
||||||
}): TGScore | null {
|
}): TGScore | null {
|
||||||
return Score.get({ character, slot, date });
|
return Score.get({ character, slot });
|
||||||
},
|
},
|
||||||
|
|
||||||
getWeeklySummary({ character }: { character: Character }): WeeklySummary {
|
getWeeklySummary({ character }: { character: Character }): WeeklySummary {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
import { WRank } from "@systems/wrank";
|
import { WRank } from "@systems/wrank";
|
||||||
import { Leaves } from "@systems/leaves";
|
import { Leaves } from "@systems/leaves";
|
||||||
import { PersistentMessage } from "@systems/persistent-message";
|
import { PersistentMessage } from "@systems/persistent-message";
|
||||||
|
import { TGKey } from "@systems/tg-key";
|
||||||
|
|
||||||
const log = Logger.for("updates");
|
const log = Logger.for("updates");
|
||||||
|
|
||||||
|
|
@ -79,7 +80,7 @@
|
||||||
}[];
|
}[];
|
||||||
leaves?: {
|
leaves?: {
|
||||||
characterName: string;
|
characterName: string;
|
||||||
historyKey: string;
|
historyKey: TGKey;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,7 +224,10 @@
|
||||||
try {
|
try {
|
||||||
PollUI.setLayout(example.layout);
|
PollUI.setLayout(example.layout);
|
||||||
const state = buildExamplePollState(exampleData);
|
const state = buildExamplePollState(exampleData);
|
||||||
const exampleEmbed = PollUI.buildEmbed(state, { overrideLockMsg: `🪲 ${example.caption}` });
|
const exampleEmbed = PollUI.buildEmbed(state, {
|
||||||
|
overrideLockMsg: `🪲 ${example.caption}`,
|
||||||
|
historyKey: exampleData.leaves?.[0]?.historyKey
|
||||||
|
});
|
||||||
exampleEmbed.setTitle(`📋 Example — ${example.caption}`);
|
exampleEmbed.setTitle(`📋 Example — ${example.caption}`);
|
||||||
embeds.push(exampleEmbed);
|
embeds.push(exampleEmbed);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { HistoryKey, UserKey, CharName, Nation, ClassKey, Character, CLASSES } from "@types";
|
import { UserKey, CharName, Nation, ClassKey, Character, CLASSES } from "@types";
|
||||||
import { Config } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { Bringer } from "@systems/bringer";
|
import { Bringer } from "@systems/bringer";
|
||||||
import { Nations } from "@systems/nations";
|
import { Nations } from "@systems/nations";
|
||||||
import { Store } from "@systems/store";
|
import { Store } from "@systems/store";
|
||||||
import { Paths } from "@paths";
|
import { Paths } from "@paths";
|
||||||
|
import { TGKey } from "@systems/tg-key";
|
||||||
import { Runtime } from "@systems/runtime";
|
import { Runtime } from "@systems/runtime";
|
||||||
import { Logger } from "@systems/logger";
|
import { Logger } from "@systems/logger";
|
||||||
import { CharacterRegistry } from "@registry/character-registry";
|
import { CharacterRegistry } from "@registry/character-registry";
|
||||||
|
|
@ -40,7 +41,7 @@ export interface WRankEntry {
|
||||||
export interface WRankWeek {
|
export interface WRankWeek {
|
||||||
weekKey: string;
|
weekKey: string;
|
||||||
entries: Record<Nation, SerializableWRankEntry[]>;
|
entries: Record<Nation, SerializableWRankEntry[]>;
|
||||||
scoreIndex: Record<CharName, HistoryKey[]>;
|
scoreIndex: Record<CharName, TGKey[]>;
|
||||||
bringer: {
|
bringer: {
|
||||||
[Nation.Capella]: string | null;
|
[Nation.Capella]: string | null;
|
||||||
[Nation.Procyon]: string | null;
|
[Nation.Procyon]: string | null;
|
||||||
|
|
@ -146,7 +147,7 @@ export const WRank = {
|
||||||
cls: ClassKey,
|
cls: ClassKey,
|
||||||
nation: Nation,
|
nation: Nation,
|
||||||
pts: number,
|
pts: number,
|
||||||
historyKey: HistoryKey
|
historyKey: TGKey
|
||||||
): void {
|
): void {
|
||||||
const week = ensureWeek(WRank.weekKey());
|
const week = ensureWeek(WRank.weekKey());
|
||||||
const list = week.entries[nation];
|
const list = week.entries[nation];
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ export type UserKey = string;
|
||||||
export type DiscordId = string;
|
export type DiscordId = string;
|
||||||
export type CharName = string;
|
export type CharName = string;
|
||||||
export type SlotHour = number;
|
export type SlotHour = number;
|
||||||
export type HistoryKey = string;
|
|
||||||
export type VoteType = "yes" | "no";
|
export type VoteType = "yes" | "no";
|
||||||
export type ConfirmType = "yes" | "no";
|
export type ConfirmType = "yes" | "no";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,18 @@
|
||||||
* BaseLayout — shared poll layout functions.
|
* BaseLayout — shared poll layout functions.
|
||||||
* All layouts inherit these via BaseLayout.methods() spread.
|
* All layouts inherit these via BaseLayout.methods() spread.
|
||||||
* Override only what differs in each layout.
|
* Override only what differs in each layout.
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* export const myLayout: PollLayout = {
|
|
||||||
* ...BaseLayout.methods(),
|
|
||||||
* name: "my-layout",
|
|
||||||
* description: "...",
|
|
||||||
* buildEmbed(state, options) { ... }, // override
|
|
||||||
* };
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { VoteEntry, Nation } from "@types";
|
import { EmbedBuilder } from "discord.js";
|
||||||
|
import { VoteEntry, Nation, PollState } from "@types";
|
||||||
import { Config } from "@systems/config";
|
import { Config } from "@systems/config";
|
||||||
import { WRank, WRankEntry } from "@systems/wrank";
|
import { WRank, WRankEntry } from "@systems/wrank";
|
||||||
import { Bringer } from "@systems/bringer";
|
import { Bringer } from "@systems/bringer";
|
||||||
import { Leaves } from "@systems/leaves";
|
import { Leaves } from "@systems/leaves";
|
||||||
import { Emoji } from "@systems/emojis";
|
import { Emoji } from "@systems/emojis";
|
||||||
|
import { TGKey } from "@systems/tg-key";
|
||||||
import { format } from "@format";
|
import { format } from "@format";
|
||||||
import { PollRowContext } from "@ui/types";
|
import { PollLayout, PollRowContext, PollEmbedOptions } from "@ui/types";
|
||||||
|
|
||||||
// ─── W.Rank formatting ────────────────────────────────────────────────────────
|
// ─── W.Rank formatting ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -76,8 +70,8 @@
|
||||||
// Nation emoji prefix
|
// Nation emoji prefix
|
||||||
if (context.showNationEmoji && nation) row = `${Emoji.nation(nation)} ${row}`;
|
if (context.showNationEmoji && nation) row = `${Emoji.nation(nation)} ${row}`;
|
||||||
|
|
||||||
// Cockroach indicator (left TG)
|
// Cockroach indicator
|
||||||
if (entry.userKey && entry.characterName && context.historyKey) {
|
if (entry.characterName && context.historyKey) {
|
||||||
if (Leaves.hasLeft({ characterName: entry.characterName, historyKey: context.historyKey })) {
|
if (Leaves.hasLeft({ characterName: entry.characterName, historyKey: context.historyKey })) {
|
||||||
row += ` ${Leaves.formatIndicator({ characterName: entry.characterName })}`;
|
row += ` ${Leaves.formatIndicator({ characterName: entry.characterName })}`;
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +85,7 @@
|
||||||
export function buildContext(
|
export function buildContext(
|
||||||
entries: VoteEntry[],
|
entries: VoteEntry[],
|
||||||
nation: Nation,
|
nation: Nation,
|
||||||
options?: { showNationEmoji?: boolean; historyKey?: string }
|
options?: { showNationEmoji?: boolean; historyKey?: TGKey }
|
||||||
): PollRowContext {
|
): PollRowContext {
|
||||||
const nationHasRank = entries.some((e) => {
|
const nationHasRank = entries.some((e) => {
|
||||||
if (!e.characterName) return false;
|
if (!e.characterName) return false;
|
||||||
|
|
@ -113,7 +107,25 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Message formatting ───────────────────────────────────────────────────────
|
// ─── Shared embed helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function formatNationField(
|
||||||
|
nation: Nation,
|
||||||
|
yesEntries: VoteEntry[],
|
||||||
|
noVoters: VoteEntry[],
|
||||||
|
showNoInline: boolean,
|
||||||
|
historyKey?: TGKey
|
||||||
|
): string {
|
||||||
|
const context = buildContext(yesEntries, nation, { historyKey });
|
||||||
|
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") : "—";
|
||||||
|
}
|
||||||
|
|
||||||
export function formatMessages(
|
export function formatMessages(
|
||||||
allMessages: { entry: VoteEntry; voteType: "yes" | "no" }[]
|
allMessages: { entry: VoteEntry; voteType: "yes" | "no" }[]
|
||||||
|
|
@ -129,34 +141,14 @@
|
||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Embed helpers ────────────────────────────────────────────────────────────
|
export function resolveColor(state: PollState): number {
|
||||||
|
|
||||||
export function formatNationField(
|
|
||||||
nation: Nation,
|
|
||||||
yesEntries: VoteEntry[],
|
|
||||||
noVoters: VoteEntry[],
|
|
||||||
showNoInline: boolean,
|
|
||||||
historyKey?: string
|
|
||||||
): string {
|
|
||||||
const context = buildContext(yesEntries, nation, { historyKey });
|
|
||||||
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") : "—";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveColor(state: any): number {
|
|
||||||
if (state.confirmed === "yes") return 0x57f287;
|
if (state.confirmed === "yes") return 0x57f287;
|
||||||
if (state.confirmed === "no") return 0xed4245;
|
if (state.confirmed === "no") return 0xed4245;
|
||||||
if (state.locked) return 0x888888;
|
if (state.locked) return 0x888888;
|
||||||
return 0xe8a317;
|
return 0xe8a317;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveTitle(state: any, yesByNation: Record<Nation, VoteEntry[]>): string {
|
export function resolveTitle(state: PollState, yesByNation: Record<Nation, VoteEntry[]>): string {
|
||||||
const capellaEmoji = Emoji.get("capella");
|
const capellaEmoji = Emoji.get("capella");
|
||||||
const procyonEmoji = Emoji.get("procyon");
|
const procyonEmoji = Emoji.get("procyon");
|
||||||
const counts = !state.locked && state.confirmed === null
|
const counts = !state.locked && state.confirmed === null
|
||||||
|
|
@ -169,14 +161,14 @@
|
||||||
return `⚔️ TG — ${state.slot}:00${counts}${statusSuffix}`;
|
return `⚔️ TG — ${state.slot}:00${counts}${statusSuffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveFooter(state: any, noCount: number, overrideLockMsg?: string): string {
|
export function resolveFooter(state: PollState, noCount: number, overrideLockMsg?: string): string {
|
||||||
if (state.confirmed === "yes") return Config.get({ section: "poll", key: "confirmYes" });
|
if (state.confirmed === "yes") return Config.get({ section: "poll", key: "confirmYes" });
|
||||||
if (state.confirmed === "no") return Config.get({ section: "poll", key: "confirmNo" });
|
if (state.confirmed === "no") return Config.get({ section: "poll", key: "confirmNo" });
|
||||||
if (state.locked) return overrideLockMsg ?? Config.get({ section: "poll", key: "lockMessage" });
|
if (state.locked) return overrideLockMsg ?? Config.get({ section: "poll", key: "lockMessage" });
|
||||||
return `❌ ${noCount} • Vote updates live • Anyone can vote • /tg switch to change character`;
|
return `❌ ${noCount} • Vote updates live • Anyone can vote • /tg switch to change character`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildYesByNation(state: any): {
|
export function buildYesByNation(state: PollState): {
|
||||||
yesByNation: Record<Nation, VoteEntry[]>;
|
yesByNation: Record<Nation, VoteEntry[]>;
|
||||||
noVoters: VoteEntry[];
|
noVoters: VoteEntry[];
|
||||||
allMessages: { entry: VoteEntry; voteType: "yes" | "no" }[];
|
allMessages: { entry: VoteEntry; voteType: "yes" | "no" }[];
|
||||||
|
|
@ -189,10 +181,7 @@
|
||||||
const allMessages: { entry: VoteEntry; voteType: "yes" | "no" }[] = [];
|
const allMessages: { entry: VoteEntry; voteType: "yes" | "no" }[] = [];
|
||||||
|
|
||||||
for (const entry of state.yes.values()) {
|
for (const entry of state.yes.values()) {
|
||||||
const nation: Nation = entry.characterNation === Nation.Procyon
|
const nation: Nation = (entry.characterNation as Nation) ?? Nation.Capella;
|
||||||
? Nation.Procyon
|
|
||||||
: Nation.Capella;
|
|
||||||
|
|
||||||
yesByNation[nation].push(entry);
|
yesByNation[nation].push(entry);
|
||||||
allMessages.push({ entry, voteType: "yes" });
|
allMessages.push({ entry, voteType: "yes" });
|
||||||
}
|
}
|
||||||
|
|
@ -204,13 +193,76 @@
|
||||||
return { yesByNation, noVoters, allMessages };
|
return { yesByNation, noVoters, allMessages };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── buildEmbed factory ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function createBuildEmbed({ inline, maxInlinePlayers = 5 }: {
|
||||||
|
inline: boolean;
|
||||||
|
maxInlinePlayers?: number;
|
||||||
|
}) {
|
||||||
|
return function buildEmbed(state: PollState, options?: PollEmbedOptions): EmbedBuilder {
|
||||||
|
const { yesByNation, noVoters, allMessages } = buildYesByNation(state);
|
||||||
|
const showNoInline = Config.get({ section: "poll", key: "showNoInNationField" });
|
||||||
|
const historyKey = options?.historyKey
|
||||||
|
?? TGKey.current({ slot: state.slot });
|
||||||
|
|
||||||
|
const capellaEmoji = Emoji.get("capella");
|
||||||
|
const procyonEmoji = Emoji.get("procyon");
|
||||||
|
|
||||||
|
// Auto-stack to vertical if too many players
|
||||||
|
const maxPlayers = Math.max(
|
||||||
|
yesByNation[Nation.Capella].length,
|
||||||
|
yesByNation[Nation.Procyon].length
|
||||||
|
);
|
||||||
|
const useInline = inline && maxPlayers <= maxInlinePlayers;
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle(resolveTitle(state, yesByNation))
|
||||||
|
.setColor(resolveColor(state))
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
if (useInline) {
|
||||||
|
// Side-by-side — no spacer needed
|
||||||
|
embed.addFields(
|
||||||
|
{
|
||||||
|
name: `${capellaEmoji} Capella (${yesByNation[Nation.Capella].length})`,
|
||||||
|
value: formatNationField(Nation.Capella, yesByNation[Nation.Capella], noVoters, showNoInline, historyKey),
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `${procyonEmoji} Procyon (${yesByNation[Nation.Procyon].length})`,
|
||||||
|
value: formatNationField(Nation.Procyon, yesByNation[Nation.Procyon], noVoters, showNoInline, historyKey),
|
||||||
|
inline: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Vertical — spacer between nations
|
||||||
|
embed.addFields(
|
||||||
|
{
|
||||||
|
name: `${capellaEmoji} Capella (${yesByNation[Nation.Capella].length})`,
|
||||||
|
value: formatNationField(Nation.Capella, yesByNation[Nation.Capella], noVoters, showNoInline, historyKey),
|
||||||
|
inline: false,
|
||||||
|
},
|
||||||
|
{ name: "\u200b", value: "\u200b", inline: false },
|
||||||
|
{
|
||||||
|
name: `${procyonEmoji} Procyon (${yesByNation[Nation.Procyon].length})`,
|
||||||
|
value: formatNationField(Nation.Procyon, yesByNation[Nation.Procyon], noVoters, showNoInline, historyKey),
|
||||||
|
inline: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ─── BaseLayout factory ───────────────────────────────────────────────────────
|
// ─── BaseLayout factory ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const BaseLayout = {
|
export const BaseLayout = {
|
||||||
methods() {
|
methods() {
|
||||||
return {
|
return { formatRow, buildContext };
|
||||||
formatRow,
|
|
||||||
buildContext,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -1,53 +1,9 @@
|
||||||
import { EmbedBuilder } from "discord.js";
|
import { PollLayout } from "@ui/types";
|
||||||
import { PollState, Nation, VoteEntry } from "@types";
|
import { BaseLayout, createBuildEmbed } from "../base-layout";
|
||||||
import { Emoji } from "@systems/emojis";
|
|
||||||
import { PollLayout, PollEmbedOptions } from "@ui/types";
|
|
||||||
import {
|
|
||||||
BaseLayout,
|
|
||||||
buildYesByNation,
|
|
||||||
formatNationField,
|
|
||||||
formatMessages,
|
|
||||||
resolveColor,
|
|
||||||
resolveTitle,
|
|
||||||
resolveFooter,
|
|
||||||
} from "../base-layout";
|
|
||||||
|
|
||||||
function buildEmbed(state: PollState, options?: PollEmbedOptions): EmbedBuilder {
|
|
||||||
const { yesByNation, noVoters, allMessages } = buildYesByNation(state);
|
|
||||||
const showNoInline = false; // default layout stacks no-voters
|
|
||||||
const historyKey = `${new Date().toISOString().slice(0, 10)}-${state.slot}`;
|
|
||||||
|
|
||||||
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, historyKey),
|
|
||||||
inline: false,
|
|
||||||
},
|
|
||||||
{ name: "\u200b", value: "\u200b", inline: false },
|
|
||||||
{
|
|
||||||
name: `${procyonEmoji} Procyon (${yesByNation[Nation.Procyon].length})`,
|
|
||||||
value: formatNationField(Nation.Procyon, yesByNation[Nation.Procyon], noVoters, showNoInline, historyKey),
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultLayout: PollLayout = {
|
export const defaultLayout: PollLayout = {
|
||||||
...BaseLayout.methods(),
|
...BaseLayout.methods(),
|
||||||
name: "default",
|
name: "default",
|
||||||
description: "Standard vertical layout with nation-separated fields",
|
description: "Standard vertical layout with nation-separated fields",
|
||||||
buildEmbed,
|
buildEmbed: createBuildEmbed({ inline: false }),
|
||||||
};
|
};
|
||||||
|
|
@ -1,58 +1,9 @@
|
||||||
import { EmbedBuilder } from "discord.js";
|
import { PollLayout } from "@ui/types";
|
||||||
import { PollState, Nation } from "@types";
|
import { BaseLayout, createBuildEmbed } from "../base-layout";
|
||||||
import { Emoji } from "@systems/emojis";
|
|
||||||
import { PollLayout, PollEmbedOptions } from "@ui/types";
|
|
||||||
import {
|
|
||||||
BaseLayout,
|
|
||||||
buildYesByNation,
|
|
||||||
formatNationField,
|
|
||||||
formatMessages,
|
|
||||||
resolveColor,
|
|
||||||
resolveTitle,
|
|
||||||
resolveFooter,
|
|
||||||
} from "../base-layout";
|
|
||||||
|
|
||||||
function buildEmbed(state: PollState, options?: PollEmbedOptions): EmbedBuilder {
|
|
||||||
const { yesByNation, noVoters, allMessages } = buildYesByNation(state);
|
|
||||||
const showNoInline = false;
|
|
||||||
const historyKey = `${new Date().toISOString().slice(0, 10)}-${state.slot}`;
|
|
||||||
|
|
||||||
const capellaEmoji = Emoji.get("capella");
|
|
||||||
const procyonEmoji = Emoji.get("procyon");
|
|
||||||
|
|
||||||
const maxPlayers = Math.max(
|
|
||||||
yesByNation[Nation.Capella].length,
|
|
||||||
yesByNation[Nation.Procyon].length
|
|
||||||
);
|
|
||||||
const useInline = maxPlayers <= 5;
|
|
||||||
|
|
||||||
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, historyKey),
|
|
||||||
inline: useInline,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: `${procyonEmoji} Procyon (${yesByNation[Nation.Procyon].length})`,
|
|
||||||
value: formatNationField(Nation.Procyon, yesByNation[Nation.Procyon], noVoters, showNoInline, historyKey),
|
|
||||||
inline: useInline,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const sideBySideLayout: PollLayout = {
|
export const sideBySideLayout: PollLayout = {
|
||||||
...BaseLayout.methods(),
|
...BaseLayout.methods(),
|
||||||
name: "side-by-side",
|
name: "side-by-side",
|
||||||
description: "Nations displayed inline side by side (auto-stacks if > 5 players per nation)",
|
description: "Nations displayed inline side by side (auto-stacks if > 5 players per nation)",
|
||||||
buildEmbed,
|
buildEmbed: createBuildEmbed({ inline: true, maxInlinePlayers: 5 }),
|
||||||
};
|
};
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
import { EmbedBuilder } from "discord.js";
|
import { EmbedBuilder } from "discord.js";
|
||||||
import { PollState, VoteEntry, Nation } from "@types";
|
import { PollState, VoteEntry, Nation } from "@types";
|
||||||
|
import { TGKey } from "@systems/tg-key";
|
||||||
|
|
||||||
// ─── Poll ─────────────────────────────────────────────────────────────────────
|
// ─── Poll ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -11,12 +12,13 @@
|
||||||
nationHasRank: boolean;
|
nationHasRank: boolean;
|
||||||
nationHasDelta: boolean;
|
nationHasDelta: boolean;
|
||||||
showNationEmoji?: boolean;
|
showNationEmoji?: boolean;
|
||||||
historyKey?: string
|
historyKey?: TGKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PollEmbedOptions {
|
export interface PollEmbedOptions {
|
||||||
overrideLockMsg?: string;
|
overrideLockMsg?: string;
|
||||||
showScoreButton?: boolean;
|
showScoreButton?: boolean;
|
||||||
|
historyKey?: TGKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PollLayout {
|
export interface PollLayout {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue