Merge feature/ingame-farben in main
This commit is contained in:
commit
123c2f11d7
3 files changed
+85
-27
No files matched your search
@@ -4,24 +4,57 @@
|
|||||||
const MAX_LENGTH = 120;
|
const MAX_LENGTH = 120;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Text für "say", ohne Zeichen, mit denen sich weitere Konsolenbefehle anhängen ließen
|
* Farbcodes für den CS2-Chat (am Server geprüft). Ein Code färbt alles bis zum nächsten Code.
|
||||||
* (Anführungszeichen, Semikolon, Zeilenumbrüche). Doppelpunkt und Komma bleiben für Spielstände erlaubt.
|
* Sie kommen ausschließlich aus den Textbausteinen hier, nie aus Eingaben.
|
||||||
*/
|
*/
|
||||||
export function chatText(text) {
|
export const COLOR = {
|
||||||
|
white: '\x01',
|
||||||
|
red: '\x02',
|
||||||
|
green: '\x04',
|
||||||
|
yellow: '\x09',
|
||||||
|
blue: '\x0B',
|
||||||
|
gold: '\x10',
|
||||||
|
};
|
||||||
|
const ALLOWED_CODES = new Set(Object.values(COLOR));
|
||||||
|
// CS2 färbt wie im Spiel: CT blau, T gold
|
||||||
|
const SIDE_COLOR = { CT: COLOR.blue, T: COLOR.gold };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eingaben (Teamnamen, Map-Namen) für den Chat: ohne Anführungszeichen, Semikolon, Zeilenumbrüche und ohne
|
||||||
|
* Steuerzeichen, sodass sich weder Befehle anhängen noch eigene Farben einbauen lassen.
|
||||||
|
* Doppelpunkt und Komma bleiben für Spielstände erlaubt.
|
||||||
|
*/
|
||||||
|
export function chatText(text, maxLength = MAX_LENGTH) {
|
||||||
return String(text ?? '')
|
return String(text ?? '')
|
||||||
.replace(/["';\\\r\n\t]/g, '')
|
.replace(/["';\\\r\n\t]/g, '')
|
||||||
.replace(/[^\p{L}\p{N} .,:_\-+#!?()[\]|/&]/gu, '')
|
.replace(/[^\p{L}\p{N} .,:_\-+#!?()[\]|/&]/gu, '')
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, ' ')
|
||||||
.trim()
|
.trim()
|
||||||
.slice(0, MAX_LENGTH);
|
.slice(0, maxLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Konsolenbefehle für eine Liste von Hinweisen. Leere Zeilen fallen weg. */
|
/** Gefärbter Teil einer Zeile; danach geht es in Weiß weiter. */
|
||||||
|
const paint = (color, text) => `${color}${text}${COLOR.white}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konsolenbefehle für eine Liste von Hinweisen. Erlaubt sind nur die Farbcodes aus COLOR; alles, was einen
|
||||||
|
* weiteren Befehl einleiten könnte, fällt weg. Das Leerzeichen am Anfang braucht CS2, damit der erste Farbcode wirkt.
|
||||||
|
*/
|
||||||
export function sayCommands(lines) {
|
export function sayCommands(lines) {
|
||||||
return lines.map(chatText).filter(Boolean).map((line) => `say ${line}`);
|
return lines
|
||||||
|
// Kein trim(): Gelb (\x09) und Blau (\x0B) zählen für JavaScript als Leerraum und gingen sonst verloren
|
||||||
|
.map((line) => [...String(line ?? '')]
|
||||||
|
.filter((ch) => ALLOWED_CODES.has(ch) || !/[\x00-\x1F\x7F"';\\]/.test(ch))
|
||||||
|
.join(''))
|
||||||
|
.filter((line) => line.replace(/[\x00-\x1F]/g, '').trim())
|
||||||
|
.map((line) => `say ${line}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const other = (team) => (team === 'A' ? 'B' : 'A');
|
const other = (team) => (team === 'A' ? 'B' : 'A');
|
||||||
|
const name = (teams, team) => chatText(teams[team], 32) || 'Team';
|
||||||
|
|
||||||
|
/** Teamname in der Farbe seiner Startseite. */
|
||||||
|
const sided = (teams, team, ctTeam) => (ctTeam ? paint(SIDE_COLOR[team === ctTeam ? 'CT' : 'T'], name(teams, team)) : name(teams, team));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hinweise nach dem Laden einer Map: welche Map, wer sie gewählt hat, wer welche Seite startet,
|
* Hinweise nach dem Laden einer Map: welche Map, wer sie gewählt hat, wer welche Seite startet,
|
||||||
@@ -34,22 +67,24 @@ const other = (team) => (team === 'A' ? 'B' : 'A');
|
|||||||
*/
|
*/
|
||||||
export function mapLoadedLines({ teams, map, total, series }) {
|
export function mapLoadedLines({ teams, map, total, series }) {
|
||||||
const lines = [];
|
const lines = [];
|
||||||
const head = total > 1 ? `Map ${map.number} von ${total}: ${map.mapName}` : `Map: ${map.mapName}`;
|
const mapName = chatText(map.mapName, 32);
|
||||||
lines.push(map.pickedBy ? `${head} - gewählt von ${teams[map.pickedBy]}` : `${head} - Entscheidungsmap`);
|
const head = total > 1 ? `Map ${map.number} von ${total}: ${mapName}` : `Map: ${mapName}`;
|
||||||
|
lines.push(map.pickedBy ? `${head} - gewählt von ${name(teams, map.pickedBy)}` : `${head} - Entscheidungsmap`);
|
||||||
if (map.ctTeam) {
|
if (map.ctTeam) {
|
||||||
const t = other(map.ctTeam);
|
const t = other(map.ctTeam);
|
||||||
const chooser = map.sideChosenBy ? `${teams[map.sideChosenBy]} hat die Seite gewählt: ` : '';
|
const chooser = map.sideChosenBy ? `${name(teams, map.sideChosenBy)} hat die Seite gewählt: ` : '';
|
||||||
lines.push(`${chooser}${teams[map.ctTeam]} startet als CT, ${teams[t]} als T`);
|
lines.push(`${chooser}${sided(teams, map.ctTeam, map.ctTeam)} startet als ${paint(COLOR.blue, 'CT')}, ${sided(teams, t, map.ctTeam)} als ${paint(COLOR.gold, 'T')}`);
|
||||||
}
|
}
|
||||||
if (total > 1) lines.push(`Serienstand: ${teams.A} ${series.A}:${series.B} ${teams.B}`);
|
if (total > 1) lines.push(`Serienstand: ${name(teams, 'A')} ${series.A}:${series.B} ${name(teams, 'B')}`);
|
||||||
lines.push('Aufwärmphase - das Match startet, sobald der Organisator es freigibt');
|
lines.push(paint(COLOR.yellow, 'Aufwärmphase - das Match startet, sobald der Organisator es freigibt'));
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Hinweis beim Start des Matches (Aufwärmphase beendet). */
|
/** Hinweis beim Start des Matches (Aufwärmphase beendet). */
|
||||||
export function matchStartLines({ teams, ctTeam }) {
|
export function matchStartLines({ teams, ctTeam }) {
|
||||||
if (!ctTeam) return [`Match live! ${teams.A} gegen ${teams.B} - viel Glück, have fun`];
|
const live = paint(COLOR.green, 'Match live!');
|
||||||
return [`Match live! ${teams[ctTeam]} (CT) gegen ${teams[other(ctTeam)]} (T) - viel Glück, have fun`];
|
if (!ctTeam) return [`${live} ${name(teams, 'A')} gegen ${name(teams, 'B')} - viel Glück, have fun`];
|
||||||
|
return [`${live} ${sided(teams, ctTeam, ctTeam)} (CT) gegen ${sided(teams, other(ctTeam), ctTeam)} (T) - viel Glück, have fun`];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,17 +100,21 @@ export function matchStartLines({ teams, ctTeam }) {
|
|||||||
export function mapEndLines({ teams, map, series, decided, nextMapName, autoAdvanceSeconds, total }) {
|
export function mapEndLines({ teams, map, series, decided, nextMapName, autoAdvanceSeconds, total }) {
|
||||||
const lines = [];
|
const lines = [];
|
||||||
const score = `${map.score.A}:${map.score.B}`;
|
const score = `${map.score.A}:${map.score.B}`;
|
||||||
|
const mapName = chatText(map.mapName, 32);
|
||||||
lines.push(map.winner
|
lines.push(map.winner
|
||||||
? `Map ${map.number} (${map.mapName}) geht an ${teams[map.winner]} mit ${score}`
|
? `Map ${map.number} (${mapName}) geht an ${paint(COLOR.green, name(teams, map.winner))} mit ${score}`
|
||||||
: `Map ${map.number} (${map.mapName}) endet ${score}`);
|
: `Map ${map.number} (${mapName}) endet ${score}`);
|
||||||
if (decided) {
|
if (decided) {
|
||||||
const s = decided === 'A' ? `${series.A}:${series.B}` : `${series.B}:${series.A}`;
|
const s = decided === 'A' ? `${series.A}:${series.B}` : `${series.B}:${series.A}`;
|
||||||
lines.push(total > 1 ? `${teams[decided]} gewinnt die Serie ${s} - danke fürs Spielen!` : 'Danke fürs Spielen!');
|
lines.push(total > 1
|
||||||
|
? `${paint(COLOR.green, `${name(teams, decided)} gewinnt die Serie ${s}`)} - danke fürs Spielen!`
|
||||||
|
: 'Danke fürs Spielen!');
|
||||||
} else if (nextMapName) {
|
} else if (nextMapName) {
|
||||||
lines.push(`Serienstand: ${teams.A} ${series.A}:${series.B} ${teams.B}`);
|
const next = chatText(nextMapName, 32);
|
||||||
|
lines.push(`Serienstand: ${name(teams, 'A')} ${series.A}:${series.B} ${name(teams, 'B')}`);
|
||||||
lines.push(autoAdvanceSeconds >= 0
|
lines.push(autoAdvanceSeconds >= 0
|
||||||
? `Nächste Map: ${nextMapName}, wird in ${autoAdvanceSeconds} s geladen - bleibt auf dem Server`
|
? `Nächste Map: ${next}, wird in ${autoAdvanceSeconds} s geladen - bleibt auf dem Server`
|
||||||
: `Nächste Map: ${nextMapName} - bleibt auf dem Server`);
|
: `Nächste Map: ${next} - bleibt auf dem Server`);
|
||||||
}
|
}
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
@@ -838,7 +838,7 @@ test('Ingame-Chat: Hinweise nach dem Laden, beim Start und am Map-Ende', async (
|
|||||||
const said = [];
|
const said = [];
|
||||||
const send = app.adapter.sendCommands.bind(app.adapter);
|
const send = app.adapter.sendCommands.bind(app.adapter);
|
||||||
app.adapter.sendCommands = async (h, commands) => {
|
app.adapter.sendCommands = async (h, commands) => {
|
||||||
said.push(...commands.filter((c) => c.startsWith('say ')).map((c) => c.slice(4)));
|
said.push(...commands.filter((c) => c.startsWith('say ')).map((c) => c.slice(4).replace(/[-]/g, '').trim()));
|
||||||
return send(h, commands);
|
return send(h, commands);
|
||||||
};
|
};
|
||||||
const { id, org, tokens } = await readyWingmanMatch(call, 'bo3');
|
const { id, org, tokens } = await readyWingmanMatch(call, 'bo3');
|
||||||
|
|||||||
@@ -1,32 +1,34 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { chatText, mapEndLines, mapLoadedLines, matchStartLines, sayCommands } from '../src/servers/chatAnnouncements.js';
|
import { chatText, COLOR, mapEndLines, mapLoadedLines, matchStartLines, sayCommands } from '../src/servers/chatAnnouncements.js';
|
||||||
|
|
||||||
const teams = { A: 'Nordlicht', B: 'Hafenkante' };
|
const teams = { A: 'Nordlicht', B: 'Hafenkante' };
|
||||||
|
// Text ohne Farbcodes, zum Vergleichen des Inhalts
|
||||||
|
const plain = (lines) => lines.map((l) => l.replace(/[-]/g, ''));
|
||||||
|
|
||||||
test('Hinweise nach dem Laden: Map, Wahl, Seiten, Serienstand, Aufwärmphase', () => {
|
test('Hinweise nach dem Laden: Map, Wahl, Seiten, Serienstand, Aufwärmphase', () => {
|
||||||
const lines = mapLoadedLines({
|
const lines = mapLoadedLines({
|
||||||
teams, total: 3, series: { A: 1, B: 0 },
|
teams, total: 3, series: { A: 1, B: 0 },
|
||||||
map: { number: 2, mapName: 'Inferno', pickedBy: 'B', sideChosenBy: 'A', ctTeam: 'A' },
|
map: { number: 2, mapName: 'Inferno', pickedBy: 'B', sideChosenBy: 'A', ctTeam: 'A' },
|
||||||
});
|
});
|
||||||
assert.deepEqual(lines, [
|
assert.deepEqual(plain(lines), [
|
||||||
'Map 2 von 3: Inferno - gewählt von Hafenkante',
|
'Map 2 von 3: Inferno - gewählt von Hafenkante',
|
||||||
'Nordlicht hat die Seite gewählt: Nordlicht startet als CT, Hafenkante als T',
|
'Nordlicht hat die Seite gewählt: Nordlicht startet als CT, Hafenkante als T',
|
||||||
'Serienstand: Nordlicht 1:0 Hafenkante',
|
'Serienstand: Nordlicht 1:0 Hafenkante',
|
||||||
'Aufwärmphase - das Match startet, sobald der Organisator es freigibt',
|
'Aufwärmphase - das Match startet, sobald der Organisator es freigibt',
|
||||||
]);
|
]);
|
||||||
const bo1 = mapLoadedLines({ teams, total: 1, series: { A: 0, B: 0 }, map: { number: 1, mapName: 'Nuke', pickedBy: null, sideChosenBy: 'B', ctTeam: 'B' } });
|
const bo1 = mapLoadedLines({ teams, total: 1, series: { A: 0, B: 0 }, map: { number: 1, mapName: 'Nuke', pickedBy: null, sideChosenBy: 'B', ctTeam: 'B' } });
|
||||||
assert.equal(bo1[0], 'Map: Nuke - Entscheidungsmap');
|
assert.equal(plain(bo1)[0], 'Map: Nuke - Entscheidungsmap');
|
||||||
assert.ok(!bo1.some((l) => l.startsWith('Serienstand')), 'kein Serienstand im BO1');
|
assert.ok(!bo1.some((l) => l.startsWith('Serienstand')), 'kein Serienstand im BO1');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Hinweise bei Match-Start und Map-Ende', () => {
|
test('Hinweise bei Match-Start und Map-Ende', () => {
|
||||||
assert.deepEqual(matchStartLines({ teams, ctTeam: 'B' }), ['Match live! Hafenkante (CT) gegen Nordlicht (T) - viel Glück, have fun']);
|
assert.deepEqual(plain(matchStartLines({ teams, ctTeam: 'B' })), ['Match live! Hafenkante (CT) gegen Nordlicht (T) - viel Glück, have fun']);
|
||||||
const next = mapEndLines({
|
const next = mapEndLines({
|
||||||
teams, total: 3, decided: null, nextMapName: 'Inferno', autoAdvanceSeconds: 30, series: { A: 1, B: 0 },
|
teams, total: 3, decided: null, nextMapName: 'Inferno', autoAdvanceSeconds: 30, series: { A: 1, B: 0 },
|
||||||
map: { number: 1, mapName: 'Mirage', winner: 'A', score: { A: 13, B: 9 } },
|
map: { number: 1, mapName: 'Mirage', winner: 'A', score: { A: 13, B: 9 } },
|
||||||
});
|
});
|
||||||
assert.deepEqual(next, [
|
assert.deepEqual(plain(next), [
|
||||||
'Map 1 (Mirage) geht an Nordlicht mit 13:9',
|
'Map 1 (Mirage) geht an Nordlicht mit 13:9',
|
||||||
'Serienstand: Nordlicht 1:0 Hafenkante',
|
'Serienstand: Nordlicht 1:0 Hafenkante',
|
||||||
'Nächste Map: Inferno, wird in 30 s geladen - bleibt auf dem Server',
|
'Nächste Map: Inferno, wird in 30 s geladen - bleibt auf dem Server',
|
||||||
@@ -35,7 +37,7 @@ test('Hinweise bei Match-Start und Map-Ende', () => {
|
|||||||
teams, total: 3, decided: 'B', nextMapName: null, autoAdvanceSeconds: 30, series: { A: 0, B: 2 },
|
teams, total: 3, decided: 'B', nextMapName: null, autoAdvanceSeconds: 30, series: { A: 0, B: 2 },
|
||||||
map: { number: 2, mapName: 'Nuke', winner: 'B', score: { A: 5, B: 13 } },
|
map: { number: 2, mapName: 'Nuke', winner: 'B', score: { A: 5, B: 13 } },
|
||||||
});
|
});
|
||||||
assert.equal(won[1], 'Hafenkante gewinnt die Serie 2:0 - danke fürs Spielen!');
|
assert.equal(plain(won)[1], 'Hafenkante gewinnt die Serie 2:0 - danke fürs Spielen!');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Chattexte können keine weiteren Befehle anhängen', () => {
|
test('Chattexte können keine weiteren Befehle anhängen', () => {
|
||||||
@@ -43,3 +45,20 @@ test('Chattexte können keine weiteren Befehle anhängen', () => {
|
|||||||
assert.deepEqual(sayCommands(['Hallo; quit', '', 'Stand 13:9']), ['say Hallo quit', 'say Stand 13:9']);
|
assert.deepEqual(sayCommands(['Hallo; quit', '', 'Stand 13:9']), ['say Hallo quit', 'say Stand 13:9']);
|
||||||
assert.ok(chatText('x'.repeat(300)).length <= 120);
|
assert.ok(chatText('x'.repeat(300)).length <= 120);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Farben: CT blau, T gold, Sieger grün, Aufwärmphase gelb; Eingaben bringen keine eigenen Farben mit', () => {
|
||||||
|
const loaded = mapLoadedLines({ teams, total: 1, series: { A: 0, B: 0 }, map: { number: 1, mapName: 'Nuke', pickedBy: null, sideChosenBy: 'B', ctTeam: 'B' } });
|
||||||
|
const sides = loaded.find((l) => l.includes('startet als'));
|
||||||
|
assert.ok(sides.includes(`${COLOR.blue}Hafenkante${COLOR.white}`), 'CT-Team blau');
|
||||||
|
assert.ok(sides.includes(`${COLOR.gold}Nordlicht${COLOR.white}`), 'T-Team gold');
|
||||||
|
assert.ok(loaded.at(-1).startsWith(COLOR.yellow), 'Aufwärmphase gelb');
|
||||||
|
assert.ok(matchStartLines({ teams, ctTeam: 'A' })[0].startsWith(`${COLOR.green}Match live!`));
|
||||||
|
const end = mapEndLines({ teams, total: 1, decided: null, nextMapName: null, autoAdvanceSeconds: 30, series: { A: 1, B: 0 }, map: { number: 1, mapName: 'Nuke', winner: 'A', score: { A: 13, B: 2 } } });
|
||||||
|
assert.ok(end[0].includes(`${COLOR.green}Nordlicht${COLOR.white}`), 'Sieger grün');
|
||||||
|
|
||||||
|
// Farbcodes im Teamnamen werden entfernt, die eigenen Farben bleiben
|
||||||
|
const evil = matchStartLines({ teams: { A: `BöseRot`, B: 'Gut' }, ctTeam: 'A' })[0];
|
||||||
|
assert.ok(evil.includes(`${COLOR.blue}BöseRot${COLOR.white}`));
|
||||||
|
const [cmd] = sayCommands([`${COLOR.yellow}gelb ${COLOR.blue}blau"; quit`]);
|
||||||
|
assert.equal(cmd, `say ${COLOR.yellow}gelb ${COLOR.blue}blau quit`, 'nur erlaubte Codes, kein Befehl anhängbar, Farben am Anfang bleiben');
|
||||||
|
});
|
||||||
Reference in new issue
Block a user