diff --git a/src/bot/server-monitor.js b/src/bot/server-monitor.js index cae996c..b287c8d 100644 --- a/src/bot/server-monitor.js +++ b/src/bot/server-monitor.js @@ -217,58 +217,82 @@ export function buildServerNachricht(r) { } /** GSM-Style-Embed für ein Query-Ergebnis */ +// Discord setzt drei Inline-Felder in eine Reihe. Bleiben eins oder zwei +// übrig, steht die letzte Reihe halb leer und alles wirkt schief. Ein Feld +// aus unsichtbaren Zeichen füllt die Lücke — das ist der übliche Kniff, und +// billiger als eine Layout-Entscheidung pro Kombination. +const LEER = '​'; +function dreierReihe(felder) { + const drin = felder.filter(Boolean); + if (drin.length === 0) return []; + while (drin.length % 3 !== 0) drin.push({ name: LEER, value: LEER, inline: true }); + return drin; +} + +/** Auslastung als Balken — breiter als die alten zehn Zeichen */ +function spielerBalken(players, max, breite = 16) { + const anteil = Math.max(0, Math.min(1, players / max)); + const voll = Math.round(anteil * breite); + return `\`${'█'.repeat(voll)}${'░'.repeat(breite - voll)}\` **${players}**/${max} · ${Math.round(anteil * 100)} %`; +} + export function buildServerEmbed(r) { const icon = GAME_ICONS[r.type] ?? '🎮'; const zugang = zugangStand(r, r); const embed = new EmbedBuilder() .setColor(r.online ? GREEN : RED) - .setTitle(`${icon} ${r.name}${zugang.zu ? ' 🔒' : ''}`) - .addFields({ - name: 'Status', - value: `${r.online ? '🟢 **Online**' : '🔴 **Offline**'}${zugang.grund ? `\n🔒 ${zugang.grund}` : ''}`, - inline: true, - }) + .setTitle(`${icon} ${r.name}${zugang.zu ? ' 🔒' : ''}`) // Intervall aus den Werten lesen, nicht festschreiben — sonst behauptet // das Embed weiter „alle 2 min", nachdem im Panel etwas anderes steht. - .setFooter({ - text: `${brandFooter('STATUS')}${r.ping != null ? ` • ${r.ping}ms` : ''}` - + ` • alle ${tuning('monitor_interval')} min`, - }) + .setFooter({ text: `${brandFooter('STATUS')} • alle ${tuning('monitor_interval')} min` }) .setTimestamp(); if (r.image_url) embed.setThumbnail(r.image_url); - // Wie der Server sich im Spiel nennt. Nur wenn es etwas anderes ist als - // der Name im Panel — sonst stünde dieselbe Zeile zweimal da. - if (r.spielname && r.spielname.trim() && r.spielname.trim() !== r.name?.trim()) { - embed.setDescription(`*${r.spielname.trim().slice(0, 200)}*`); + + // Kopf: Name im Spiel und der Zustand in einer Zeile. Als Beschreibung + // statt als Feld — der Status ist die Überschrift und kein Datenpunkt, + // und ein eigenes Feld dafür verbrauchte eine ganze Reihe. + const kopf = []; + if (r.spielname?.trim() && r.spielname.trim() !== r.name?.trim()) { + // Kursiv statt Discords Kleintext-Auszeichnung: die rendert in Embeds + // nicht verlässlich, und dann stünde das Steuerzeichen wörtlich da. + kopf.push(`*${r.spielname.trim().slice(0, 200)}*`); + } + kopf.push([ + r.online ? '🟢 **Online**' : '🔴 **Offline**', + zugang.grund ? `🔒 ${zugang.grund}` : null, + r.online && r.ping != null ? `${r.ping} ms` : null, + ].filter(Boolean).join(' · ')); + embed.setDescription(kopf.join('\n')); + + // Die Zahl, wegen der man hinschaut — über die volle Breite + if (r.online && r.players != null && r.max) { + embed.addFields({ name: 'Auslastung', value: spielerBalken(r.players, r.max) }); } - if (r.online && r.players != null && r.max) { - const pct = Math.min(1, r.players / r.max); - const bar = '█'.repeat(Math.round(pct * 10)).padEnd(10, '░'); - embed.addFields({ - name: 'Spieler', - value: `\`${bar}\` ${r.players}/${r.max} (${Math.round(pct * 100)}%)`, - inline: true, - }); - } - if (r.map) embed.addFields({ name: 'Map', value: r.map, inline: true }); - if (r.version) embed.addFields({ name: 'Version', value: String(r.version).slice(0, 60), inline: true }); - if (r.mods != null) embed.addFields({ name: 'Mods', value: String(r.mods), inline: true }); - // Connect und Mods stehen als Knopf darunter, wenn es https ist. Alles - // andere (fivem://, steam://) macht Discord nicht klickbar — dann lieber - // als Codeblock zum Kopieren, statt einen toten Link vorzugaukeln. - for (const l of serverLinks(r)) { - if (!istWebLink(l.url)) { - embed.addFields({ name: String(l.label).trim().slice(0, 256), value: `\`\`\`\n${l.url}\n\`\`\`` }); - } - } - if (r.address) embed.addFields({ name: 'Adresse', value: `\`\`\`\n${r.address}\n\`\`\`` }); + // Genau eine volle Reihe, notfalls aufgefüllt + embed.addFields(...dreierReihe([ + r.map ? { name: 'Map', value: String(r.map).slice(0, 60), inline: true } : null, + r.version ? { name: 'Version', value: String(r.version).slice(0, 60), inline: true } : null, + r.mods != null ? { name: 'Mods', value: String(r.mods), inline: true } : null, + ])); + if (r.online && r.playerNames.length > 0) { embed.addFields({ - name: `Spieler-Liste (${r.playerNames.length})`, + name: `Wer drauf ist (${r.playerNames.length})`, value: r.playerNames.map((n) => `\`${n}\``).join(' ').slice(0, 1000), }); } + + // Zum Kopieren: die Adresse und alles, was Discord nicht klickbar macht + // (fivem://, steam://, ts3server://). Ein Codeblock je Eintrag wäre eine + // Wand — deshalb alle zusammen in einen. + const zumKopieren = [ + r.address ? `${r.address}` : null, + ...serverLinks(r).filter((l) => !istWebLink(l.url)).map((l) => `${String(l.label).trim()}: ${l.url}`), + ].filter(Boolean); + if (zumKopieren.length > 0) { + embed.addFields({ name: 'Zum Kopieren', value: `\`\`\`\n${zumKopieren.join('\n').slice(0, 1000)}\n\`\`\`` }); + } return embed; }