function formatRow( row: ResultRow, context: NationContext, allNames: string[], allScores: string[], allKds: string[], allAtks: string[], allDefs: string[] ): string { const char = row.character; const goal = Config.get({ section: "wrank", key: "goal" }); const wrEntry = Layout.wrankEntry(char as any, row.position, row.score?.pts, 1); const classKey = (typeof char.class === "object" ? char.class?.key : char.class) as ClassKey; const scoreEmoji = Emoji.get("score") || "๐Ÿ“Š"; const scoreText = row.score ? format.scoreBold(row.score.pts) : "โ€”"; const kdText = row.score && (row.score.k || row.score.d) ? format.kd(row.score.k ?? 0, row.score.d ?? 0) : "โ€”"; // Column targets combine primary + secondary stat widths, so the main // row makes room for the stats line beneath it. const scoreColumn = [...allScores, ...allAtks]; const kdColumn = [...allKds, ...allDefs]; const tokens: Record = { wrank: Layout.wrank(wrEntry, goal, context), class: Emoji.class(classKey) || classKey || "?", name: TextAlign.padToMax(char.name, allNames), indicators: Layout.indicators(char as any, { historyKey: row.historyKey }), score: `${scoreEmoji} ${TextAlign.padToMax(scoreText, scoreColumn)}`, kd: TextAlign.gap(KD_GAP) + TextAlign.padToMax(kdText, kdColumn), }; const mainLine = Layout.formatRow(TEMPLATE, tokens); const hasStats = row.score && (row.score.atk || row.score.def || row.score.heal); log.debug(`formatRow stats check: char=${char.name} score=${JSON.stringify(row.score)} hasStats=${hasStats}`); if (!hasStats) return mainLine; const prefixText = `${tokens.wrank} ${tokens.class} ${tokens.name}${tokens.indicators}`; const prefixGap = TextAlign.padLeft("", TextAlign.estimateWidth(prefixText)); const atkText = format.statText("anima_atk", row.score!.atk, "โš”๏ธ"); const defText = format.statText("anima_def", row.score!.def, "๐Ÿ›ก๏ธ"); const healText = format.statText("circle_massheal_purple", row.score!.heal, "๐Ÿ’š"); const statsLine = `${prefixGap} ${TextAlign.gap(SCORE_GAP)}${TextAlign.padToMax(atkText, scoreColumn)} ${TextAlign.gap(DEF_GAP)}${TextAlign.padToMaxOffset(defText, kdColumn, DEF_WIDTH_OFFSET)} ${TextAlign.gap(HEAL_GAP)}${healText}`; return `${mainLine}\n${statsLine}`; } function buildEmbed(historyKey: TGKey, rows: ResultRow[]): EmbedBuilder { const { date, slot } = TGKey.parse(historyKey); const capellaRows = rows.filter((r) => r.character.nation === Nation.Capella); const procyonRows = rows.filter((r) => r.character.nation === Nation.Procyon); const capContext = Layout.nationContext(capellaRows); const proContext = Layout.nationContext(procyonRows); const sortByScore = (a: ResultRow, b: ResultRow) => (b.score?.pts ?? 0) - (a.score?.pts ?? 0); const sortedCapella = [...capellaRows].sort(sortByScore); const sortedProcyon = [...procyonRows].sort(sortByScore); const capellaNames = sortedCapella.map((r) => r.character.name); const procyonNames = sortedProcyon.map((r) => r.character.name); const capellaScores = sortedCapella.map((r) => r.score ? format.scoreBold(r.score.pts) : "โ€”"); const procyonScores = sortedProcyon.map((r) => r.score ? format.scoreBold(r.score.pts) : "โ€”"); const capellaKds = sortedCapella.map((r) => r.score && (r.score.k || r.score.d) ? format.kd(r.score.k ?? 0, r.score.d ?? 0) : "โ€”"); const procyonKds = sortedProcyon.map((r) => r.score && (r.score.k || r.score.d) ? format.kd(r.score.k ?? 0, r.score.d ?? 0) : "โ€”"); const atkEmoji = Emoji.get("atk") || "โš”๏ธ"; const defEmoji = Emoji.get("def") || "๐Ÿ›ก๏ธ"; const capellaAtks = sortedCapella.map((r) => r.score?.atk ? `${atkEmoji} ${format.number.abbrev(r.score.atk)}` : ""); const procyonAtks = sortedProcyon.map((r) => r.score?.atk ? `${atkEmoji} ${format.number.abbrev(r.score.atk)}` : ""); const capellaDefs = sortedCapella.map((r) => r.score?.def ? `${defEmoji} ${format.number.abbrev(r.score.def)}` : ""); const procyonDefs = sortedProcyon.map((r) => r.score?.def ? `${defEmoji} ${format.number.abbrev(r.score.def)}` : ""); const capK = capellaRows.reduce((s, r) => s + (r.score?.k ?? 0), 0); const capD = capellaRows.reduce((s, r) => s + (r.score?.d ?? 0), 0); const proK = procyonRows.reduce((s, r) => s + (r.score?.k ?? 0), 0); const proD = procyonRows.reduce((s, r) => s + (r.score?.d ?? 0), 0); const capellaEmoji = Emoji.get("capella"); const procyonEmoji = Emoji.get("procyon"); const capellaFormatted = sortedCapella.map((r) => formatRow(r, capContext, capellaNames, capellaScores, capellaKds, capellaAtks, capellaDefs)); const procyonFormatted = sortedProcyon.map((r) => formatRow(r, proContext, procyonNames, procyonScores, procyonKds, procyonAtks, procyonDefs)); const embed = new EmbedBuilder() .setTitle(`โš”๏ธ TG Result โ€” ${format.date(new Date(date), "dd/MM/YYYY")} ยท ${slot}:00`) .setColor(0xe8a317) .setFooter({ text: `TG Result ยท ${TGKey.toDisplay(historyKey)}` }) .setTimestamp(); EmbedHelpers.addPerPlayerColumn(embed, `${capellaEmoji} Capella${(capK || capD) ? ` โ€” ${format.kd(capK, capD)}` : ""}`, capellaFormatted); embed.addFields({ name: "\u200b", value: "\u200b", inline: false }); EmbedHelpers.addPerPlayerColumn(embed, `${procyonEmoji} Procyon${(proK || proD) ? ` โ€” ${format.kd(proK, proD)}` : ""}`, procyonFormatted); return embed; }