Countdown-HUD: Pause- und Fokus-Knopf sind 3,45 s lang sichtbar, fokussierbar und beschriftet – aber beide ohne jede Funktion #27

Open
opened 2026-09-04 15:46:54 +02:00 by gilli · 0 comments
Owner

Komponente: GAME (setState 2873, pauseGame 2940–2944, tryFocus 3192–3201,
update COUNTDOWN-Zweig 3233–3257, togglePause 2953–2956, bindUI 3562–3564),
Markup 303–306, Tastatur (INPUT → hooks.onPause/onFocus)
Betroffene Version: main @ 86a481d, index.html (3.684 Zeilen, MD5 777d18c3edbb834aa41fccc7d10a7d18)

Beschreibung

setState schaltet das HUD für zwei Zustände sichtbar:

index.html:2873   el.hud.classList.toggle("hidden", !(next === "PLAYING" || next === "COUNTDOWN"));

Im HUD sitzen #btnPause (aria-label="Spiel pausieren") und #btnFocus
(aria-label="Fokus-Zeitlupe aktivieren"). Beide Handler verlangen aber PLAYING:

index.html:2941   if (S.state !== "PLAYING" || S.dying) return;     // pauseGame
index.html:3193   if (S.state !== "PLAYING" || S.dying) return;     // tryFocus

togglePause (2953) und der Tastatur-Hook P/Escape laufen über pauseGame/resumeGame —
im COUNTDOWN also ebenfalls ins Leere. Ergebnis: 3,0 s Countdown + 0,45 s Nachlauf
(S.countdown = 3.0, Übergang bei S.countdown <= -0.45, Zeilen 2922 / 3251) lang zeigen zwei
bedienbar aussehende, mit aria-label versehene Knöpfe genau daneben zu liegen, was sie tun —
nämlich nichts. Kein disabled, kein aria-disabled, kein optischer Hinweis, kein Ton.

Für Tastaturnutzer ist das schlimmer als nur „tut nichts“: Beide Knöpfe liegen in der
Tab-Reihenfolge, Tab+Leertaste auf #btnPause löst den Klick aus, der Klick tut nichts, und
es gibt keinerlei Rückmeldung woran es liegt (Issue #20 hat die Leertaste erst bedienbar gemacht
— hier trifft sie auf einen Handler, der früh zurückkehrt).

Messprotokoll

Echtes Dokument über die von tests/testpage.py gebaute Testseite (Bridge nur für den
Zugriff auf S.state, Messung an echten Klicks/Tastendrücken), Viewport 1100×640:

state nach startRun():            COUNTDOWN   (S.countdown 2,98)
#btnPause  geklickt → state:      COUNTDOWN    focusActive: false
#btnFocus  geklickt → state:      COUNTDOWN    focusActive: false
#btnPause  sichtbar: true   disabled: (kein Attribut)
Taste P im COUNTDOWN → state:     COUNTDOWN
— countdown auf -0,5 gestellt —
state jetzt:                      PLAYING
Taste P im PLAYING → state:       PAUSED       (Gegenseite funktioniert)

Der Fokus-Knopf ist im COUNTDOWN doppelt grundlos: S.focusMeter steht zu dem Zeitpunkt auf
0,25 (startRun, Zeile 2920) — tryFocus würde auch im PLAYING ablehnen, weil die Anzeige
„Fokus 25 %“ heißt. Der Pause-Knopf ist der eigentliche Defekt: während des Countdowns gibt es
keinen Weg zurück ins Menü.
Wer Starten versehentlich ausgelöst hat (und Leertaste ist seit
#20 genau dafür die Hauptaktion im Menü — ein zu frühes Drücken auf dem Start-Knopf genügt) muss
3,45 s warten, danach erst pausieren und dann „Zum Menü“.

Ursache im Code

Der COUNTDOWN-Zustand wurde als „schon fast PLAYING“ gebaut (HUD an, Welt steht, Figur läuft
auf der Stelle, Zeile 3255) und die beiden Knopf-Handler fragen S.state hart gegen PLAYING
ab. Es gibt keine dritte Möglichkeit („Knopf ist sichtbar, aber außer Kraft“) und kein Feedback.

Folgeverhalten

  • 3,45 s untätige Bedienelemente pro Laufstart — bei jedem einzelnen Start.
  • Kein Abbruch/Weg-zurück während des Countdowns; R (Neu starten) ist ebenfalls gesperrt
    (onRestart, Zeile 3660: nur PLAYING/PAUSED/GAME_OVER).
  • Barrierefreiheit: Ein Bedienelement, dessen Auslösen folgenlos bleibt, ist ein
    Name/Role/Value-Problem (WCAG 4.1.2), nicht nur Kosmetik. Sehende Nutzer erkennen den
    Unterschied zu einem funktionierenden Knopf nicht.

Nicht verletzt (bewusst abgegrenzt)

  • Issue #20 hält: Leertaste auf einem fokussierten Knopf wird nicht verhindert, die
    Overlay-Hauptaktion funktioniert, ohne Overlay gehören die Tasten dem Spiel.
  • Die Pause selbst ist unverändert korrekt (visualTime steht still, Audio friert ein — #7):
    Im PAUSED-Zweig bleibt S.visualTime nachweislich konstant (gemessen über 400 ms).
  • Der Countdown selbst läuft korrekt durch (COUNTDOWN → PLAYING bei -0,45).

Lösungsvorschlag

Zwei saubere Varianten — die erste ist die mit dem geringsten Eingriff:

  1. Knöpfe im Countdown deaktiviert zeigen. In setState zusätzlich
    el.btnPause.disabled = (next === "COUNTDOWN"); el.btnFocus.disabled = (next === "COUNTDOWN");
    (bei Bedarf nur für btnPause), plus CSS-Regel für :disabled und aria-disabled automatisch
    über das Attribut. Ein deaktivierter Knopf ist bedienbar verstanden; ein Follow-up-Knopf
    „Zum Menü“ im Countdown erübrigt sich damit nicht, wäre aber die zweite Variante:
  2. Countdown abbrechbar machen. pauseGame() auch für COUNTDOWN erlauben (Pause merkt sich
    den Rückkehr-Zustand) oder direkt Escape/„Zum Menü“ im Countdown auf toMenu() schalten.
    Das ist die funktionale Antwort auf „versehentlich gestartet“.

Welche auch immer: Der Zustand muss am Knopf ablesbar sein, nicht nur im Code.

Erwarteter Test

  • tests/browser_issue27.py — echtes Dokument, echter Klick/Tastendruck im COUNTDOWN:
    (a) #btnPause und #btnFocus sind entweder disabled (dann: Klick ändert den Zustand nicht,
    disabled-Attribut und geänderte Erscheinung sind gesetzt), oder (b) der Klick wirkt
    (Zustand wechselt). Gemessen wird zusätzlich die Gegenseite: im PLAYING muss #btnPause
    weiterhin PAUSED ergeben. Vorher rot: sichtbar + nicht disabled + kein Zustandswechsel.

Dokumentiert in issues/27-countdown-hud-knoepfe-sichtbar-ohne-funktion.md (Commit 86a481d).

**Komponente:** GAME (`setState` 2873, `pauseGame` 2940–2944, `tryFocus` 3192–3201, `update` COUNTDOWN-Zweig 3233–3257, `togglePause` 2953–2956, `bindUI` 3562–3564), Markup 303–306, Tastatur (`INPUT` → `hooks.onPause`/`onFocus`) **Betroffene Version:** `main` @ `86a481d`, `index.html` (3.684 Zeilen, MD5 `777d18c3edbb834aa41fccc7d10a7d18`) ## Beschreibung `setState` schaltet das HUD für zwei Zustände sichtbar: ```js index.html:2873 el.hud.classList.toggle("hidden", !(next === "PLAYING" || next === "COUNTDOWN")); ``` Im HUD sitzen `#btnPause` (`aria-label="Spiel pausieren"`) und `#btnFocus` (`aria-label="Fokus-Zeitlupe aktivieren"`). Beide Handler verlangen aber `PLAYING`: ```js index.html:2941 if (S.state !== "PLAYING" || S.dying) return; // pauseGame index.html:3193 if (S.state !== "PLAYING" || S.dying) return; // tryFocus ``` `togglePause` (2953) und der Tastatur-Hook `P`/`Escape` laufen über `pauseGame`/`resumeGame` — im `COUNTDOWN` also ebenfalls ins Leere. Ergebnis: **3,0 s Countdown + 0,45 s Nachlauf** (`S.countdown = 3.0`, Übergang bei `S.countdown <= -0.45`, Zeilen 2922 / 3251) lang zeigen zwei bedienbar aussehende, mit `aria-label` versehene Knöpfe genau daneben zu liegen, was sie tun — nämlich nichts. Kein `disabled`, kein `aria-disabled`, kein optischer Hinweis, kein Ton. Für Tastaturnutzer ist das schlimmer als nur „tut nichts“: Beide Knöpfe liegen in der Tab-Reihenfolge, `Tab`+`Leertaste` auf `#btnPause` löst den Klick aus, der Klick tut nichts, und es gibt keinerlei Rückmeldung woran es liegt (Issue #20 hat die Leertaste erst bedienbar gemacht — hier trifft sie auf einen Handler, der früh zurückkehrt). ## Messprotokoll Echtes Dokument über die von `tests/testpage.py` gebaute Testseite (Bridge nur für den Zugriff auf `S.state`, Messung an echten Klicks/Tastendrücken), Viewport 1100×640: ``` state nach startRun(): COUNTDOWN (S.countdown 2,98) #btnPause geklickt → state: COUNTDOWN focusActive: false #btnFocus geklickt → state: COUNTDOWN focusActive: false #btnPause sichtbar: true disabled: (kein Attribut) Taste P im COUNTDOWN → state: COUNTDOWN — countdown auf -0,5 gestellt — state jetzt: PLAYING Taste P im PLAYING → state: PAUSED (Gegenseite funktioniert) ``` Der Fokus-Knopf ist im COUNTDOWN doppelt grundlos: `S.focusMeter` steht zu dem Zeitpunkt auf 0,25 (`startRun`, Zeile 2920) — `tryFocus` würde auch im `PLAYING` ablehnen, weil die Anzeige „Fokus 25 %“ heißt. Der Pause-Knopf ist der eigentliche Defekt: **während des Countdowns gibt es keinen Weg zurück ins Menü.** Wer `Starten` versehentlich ausgelöst hat (und `Leertaste` ist seit #20 genau dafür die Hauptaktion im Menü — ein zu frühes Drücken auf dem Start-Knopf genügt) muss 3,45 s warten, danach erst pausieren und dann „Zum Menü“. ## Ursache im Code Der COUNTDOWN-Zustand wurde als „schon fast `PLAYING`“ gebaut (HUD an, Welt steht, Figur läuft auf der Stelle, Zeile 3255) und die beiden Knopf-Handler fragen `S.state` hart gegen `PLAYING` ab. Es gibt keine dritte Möglichkeit („Knopf ist sichtbar, aber außer Kraft“) und kein Feedback. ## Folgeverhalten * 3,45 s untätige Bedienelemente pro Laufstart — bei jedem einzelnen Start. * Kein Abbruch/Weg-zurück während des Countdowns; `R` (Neu starten) ist ebenfalls gesperrt (`onRestart`, Zeile 3660: nur `PLAYING`/`PAUSED`/`GAME_OVER`). * Barrierefreiheit: Ein Bedienelement, dessen Auslösen folgenlos bleibt, ist ein Name/Role/Value-Problem (WCAG 4.1.2), nicht nur Kosmetik. Sehende Nutzer erkennen den Unterschied zu einem funktionierenden Knopf nicht. ## Nicht verletzt (bewusst abgegrenzt) * Issue #20 hält: Leertaste auf einem fokussierten Knopf wird nicht verhindert, die Overlay-Hauptaktion funktioniert, ohne Overlay gehören die Tasten dem Spiel. * Die Pause selbst ist unverändert korrekt (`visualTime` steht still, Audio friert ein — #7): Im PAUSED-Zweig bleibt `S.visualTime` nachweislich konstant (gemessen über 400 ms). * Der Countdown selbst läuft korrekt durch (`COUNTDOWN → PLAYING` bei `-0,45`). ## Lösungsvorschlag Zwei saubere Varianten — die erste ist die mit dem geringsten Eingriff: 1. **Knöpfe im Countdown deaktiviert zeigen.** In `setState` zusätzlich `el.btnPause.disabled = (next === "COUNTDOWN"); el.btnFocus.disabled = (next === "COUNTDOWN");` (bei Bedarf nur für `btnPause`), plus CSS-Regel für `:disabled` und `aria-disabled` automatisch über das Attribut. Ein deaktivierter Knopf ist bedienbar *verstanden*; ein Follow-up-Knopf „Zum Menü“ im Countdown erübrigt sich damit nicht, wäre aber die zweite Variante: 2. **Countdown abbrechbar machen.** `pauseGame()` auch für `COUNTDOWN` erlauben (Pause merkt sich den Rückkehr-Zustand) oder direkt `Escape`/„Zum Menü“ im Countdown auf `toMenu()` schalten. Das ist die funktionale Antwort auf „versehentlich gestartet“. Welche auch immer: Der Zustand muss *am Knopf ablesbar* sein, nicht nur im Code. ## Erwarteter Test * `tests/browser_issue27.py` — echtes Dokument, echter Klick/Tastendruck im `COUNTDOWN`: (a) `#btnPause` und `#btnFocus` sind entweder `disabled` (dann: Klick ändert den Zustand nicht, `disabled`-Attribut und geänderte Erscheinung sind gesetzt), oder (b) der Klick wirkt (Zustand wechselt). Gemessen wird zusätzlich die Gegenseite: im `PLAYING` muss `#btnPause` weiterhin `PAUSED` ergeben. Vorher rot: sichtbar + nicht disabled + kein Zustandswechsel. --- *Dokumentiert in `issues/27-countdown-hud-knoepfe-sichtbar-ohne-funktion.md` (Commit `86a481d`).*
Sign in to join this conversation.
No description provided.