Veto: Seitenwahl per Messerrunde, freie Messer-Server in der Lobby, Organisator legt fest, wer beginnt

This commit is contained in:
mregosar committed 2026-10-29 22:27:07 +01:00
1 parent 7b6ce01d05
commit cdf41df01c
9 files changed
+138 -8

No files matched your search

+4 -1
View File
@@ -384,7 +384,10 @@ function MatchAdminCard({ m, run, busy, token, reload, setNotice }) {
<div className="admin-actions">
<Link className="btn btn-sm" to={`/m/${id}`}>Ansehen</Link>
{m.status === 'lobby' && btn('Veto starten', { path: '/veto/start' }, 'btn-primary')}
{m.status === 'lobby' && m.vetoStart !== 'knife' && btn('Veto starten', { path: '/veto/start' }, 'btn-primary')}
{m.status === 'lobby' && m.vetoStart === 'knife' && ['A', 'B'].map((t) => (
<span key={t}>{btn(`${m.teams[t].name} beginnt das Veto`, { path: '/veto/start', body: { firstTeam: t }, confirm: `Messerrunde vorbei: ${m.teams[t].name} beginnt das Veto?`, success: 'Veto gestartet' }, 'btn-primary')}</span>
))}
{(m.server?.status === 'configured') && btn('Aufwärmphase beenden', { path: '/server/warmup-end' })}
{configured && !paused && btn('Pausieren', { path: '/server/pause', confirm: 'Match pausieren?' }, 'btn-warn')}
{configured && paused && btn('Fortsetzen', { path: '/server/resume' }, 'btn-primary')}
+1
View File
@@ -89,6 +89,7 @@ export default function CreateMatch({ config }) {
<option value="coin">Münzwurf beim Veto-Start</option>
<option value="A">{form.teamA || 'Team A'}</option>
<option value="B">{form.teamB || 'Team B'}</option>
<option value="knife">Messerrunde: Organisator legt danach fest, wer beginnt</option>
</select>
</label>
+67 -1
View File
@@ -170,7 +170,21 @@ function Stage(ctx) {
{missing.map((t) => <li key={t.key}><strong className={`team-${t.key.toLowerCase()}`}>{t.name}:</strong> {readinessGap(t, state.requireFullTeams)}</li>)}
</ul>
)}
{isOrg ? (
{state.vetoStart === 'knife' && <KnifeRound state={state} />}
{isOrg && state.vetoStart === 'knife' ? (
<div className="knife-decide">
<p>Wer hat die Messerrunde gewonnen? Dieses Team beginnt das Veto.</p>
<div className="row">
{['A', 'B'].map((t) => (
<button key={t} className={`btn btn-lg knife-team-${t.toLowerCase()}`} disabled={!state.canStartVeto || !!busy}
onClick={() => act('veto/start', { firstTeam: t }, `Veto gestartet – ${state.teams[t].name} beginnt`)}>
{state.teams[t].name} beginnt
</button>
))}
</div>
{!state.canStartVeto && <p className="small muted">Die Knöpfe werden aktiv, sobald beide Teams bereit sind.</p>}
</div>
) : isOrg ? (
<>
<button className="btn btn-primary btn-lg" disabled={!state.canStartVeto || !!busy}
onClick={() => act('veto/start', null, 'Veto gestartet')}>
@@ -178,6 +192,8 @@ function Stage(ctx) {
</button>
{!state.canStartVeto && <p className="small muted">Der Knopf wird aktiv, sobald beide Teams bereit sind.</p>}
</>
) : state.vetoStart === 'knife' ? (
<p className="small muted">Nach der Messerrunde legt der Organisator fest, welches Team das Veto beginnt.</p>
) : (
<p className="small muted">
{missing.length === 0
@@ -200,6 +216,56 @@ function Stage(ctx) {
}
/** Sagt konkret, was einem Team noch fehlt, damit es als bereit gilt. */
/**
* Seitenwahl per Messerrunde: freie Messer-Server vorschlagen, passende Teamgröße zuerst.
* Frei = niemand drauf (oder Spielerzahl unbekannt).
*/
function KnifeRound({ state }) {
const [servers, setServers] = useState(null);
useEffect(() => {
let alive = true;
const load = () => api('/knife-servers').then((res) => alive && setServers(res.servers)).catch(() => alive && setServers([]));
load();
const t = setInterval(load, 10_000);
return () => {
alive = false;
clearInterval(t);
};
}, []);
const free = (servers ?? []).filter((k) => !k.playerCount);
const matching = free.filter((k) => k.mode === state.mode);
const shown = matching.length ? matching : free;
return (
<div className="knife-round">
<h3>Seitenwahl per Messerrunde</h3>
<p className="small">
Beide Teams gehen zusammen auf einen freien Messer-Server und spielen eine Runde nur mit Messer. Danach legt der Organisator fest,
welches Team das Veto beginnt.
</p>
{servers === null ? (
<p className="small muted">Suche freie Messer-Server …</p>
) : shown.length === 0 ? (
<p className="small tone-pending">Gerade ist kein Messer-Server frei. Fragt die Turnierleitung, oder wartet, bis einer frei wird.</p>
) : (
<>
{!matching.length && <p className="small muted">Kein freier Server mit eurer Teamgröße, hier die anderen freien:</p>}
<ul className="knife-list">
{shown.map((k) => (
<li key={k.id}>
<strong>{k.name}</strong>
{k.modeShort && <span className="knife-mode">{k.modeShort}</span>}
<span className="muted small">{k.mapName}</span>
<code>{k.connect}</code>
<a className="btn btn-sm btn-primary" href={k.steamUrl}>Verbinden</a>
</li>
))}
</ul>
</>
)}
</div>
);
}
function readinessGap(team, requireFull) {
const r = team.readiness;
const gaps = [];