Eingabe-Eingebungen: Fokus doppelt gebunden ohne Modifikator-Prüfung, gehaltene Zeiger-Eingabe kann hängen bleiben #8

Closed
opened 2026-08-31 21:01:56 +02:00 by gilli · 1 comment
Owner

⁨⁩⁨Eingabe-Eingebungen: Fokus doppelt gebunden ohne Modifikator-Prüfung, gehaltene Zeiger-Eingabe kann hängen bleiben⁩ ⁨⁩#⁨8⁩⁨⁩"
By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩
bug
input
low

Komponente: INPUT, GAME (boot)
Betroffene Version: main @ 238ed66, index.html Zeilen 2101–2131, 2177–2181, 2951–2955

Teil 1 – F/Shift ist doppelt gebunden, ohne Modifikatoren abzutrennen

Der Fokus wird an zwei unabhängigen Stellen behandelt:

// index.html:2101-2131  INPUT.init()
const KEYMAP = { ..., "f": "focus", "F": "focus", "Shift": "focus", "ShiftLeft": "focus", "ShiftRight": "focus" };
...
else if (act === "focus") { hooks.onGesture(); state.focusEdge = true; };   // focusEdge wird NIE gelesen
// index.html:2951-2955  GAME.boot()
window.addEventListener("keydown", function (ev) {
  if (ev.key === "f" || ev.key === "F" || ev.key === "Shift") tryFocus();
});
  • Die INPUT-Variante schreibt in ein totes Flag (state.focusEdge existiert nur hier und in der
    Deklaration Zeile 2059 – kein Leser).
  • Die tatsächlich wirkende Variante hat keine Modifikator-Prüfung und kein ev.repeat-Filter.

Folge: Strg+F / Cmd+F (Browser-Suche), Strg+S, F5-Verwechslungen u. ä. lösen
tryFocus() aus, solange der Fokus-Balken voll ist – inklusive Fokus-Sound und Bildschirm-Blitz.
Der andere Handler verhindert das nicht, weil preventDefault() nur für die
Pfeiltasten/Space-Codes gerufen wird (Zeile 2110).

Teil 2 – pointerleave hinterlässt gesetzte Halte-Zustände

// index.html:2170-2181
function up(ev) {                       // wird nur bei pointerup ausgeführt
  ...
  if (pt.side === "left") state.jumpHeld = false; else state.rollHeld = false;
  delete pointers[ev.pointerId];
  ...
}
canvas.addEventListener("pointerup", up, { passive: true });
canvas.addEventListener("pointercancel", function (ev) { delete pointers[ev.pointerId]; }, { passive: true });
canvas.addEventListener("pointerleave",  function (ev) { delete pointers[ev.pointerId]; }, { passive: true });

Wird der Zeiger nach einem Sprung gehalten, wandert vom Canvas weg und außerhalb losgelassen,
löscht pointerleave/pointercancel nur den Zeiger-Datensatz.
state.jumpHeld bleibt true, und damit bleibt der Sprunghalte-Auftrieb
(JUMP_HOLD_BOOST, MAX_JUMP_HOLD_TIME) für den nächsten Sprung aktiv, ohne dass der
Spieler die Taste hält. Aufgelöst wird es erst durch clearEdges() bei blur/Pause.

Reproduktion

Teil 1: Fokus-Balken voll (S.focusMeter = 1), dann Strg+F drücken → Fokus startet
(S.focusActive === true, AUDIO.focus(), Blitz).

Teil 2: Linken Bildschirmbereich gedrückt halten (Sprung mit Halteboost), Zeiger vom Canvas
ziehen, außerhalb loslassen, danach erneut springen → Sprunghöhe entspricht dem Halte- statt dem
Kurzhub.

Lösungsvorschlag

  • Eine einzige zentrale Tastaturbehandlung. INPUT setzt focusEdge, playerCmd/update
    verbrauchen es – oder der zweite Listener in boot() entfällt.
  • Modifikator- und Wiederholungsfilter vor allen Aktionen:
if (ev.repeat) return;
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
  • pointercancel/pointerleave wie up behandeln (Halte-Zustände zurücksetzen, ohne
    Tap-Aktion auszulösen), z. B. gemeinsame Funktion endPointer(ev, { fireTap: false }).
  • Tests: Strg+F löst keinen Fokus aus; gehaltener Linksklick + Loslassen außerhalb führt nicht
    zu einem Boost-Sprung.

Dokumentiert in issues/08-eingabe-fokus-doppelt-und-zeiger-haengt.md (Commit c171694).

1 comments

⁨⁩⁨Eingabe-Eingebungen: Fokus doppelt gebunden ohne Modifikator-Prüfung, gehaltene Zeiger-Eingabe kann hängen bleiben⁩ ⁨⁩#⁨8⁩⁨⁩" By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩ bug input low > **Komponente:** INPUT, GAME (`boot`) > **Betroffene Version:** `main` @ `238ed66`, `index.html` Zeilen 2101–2131, 2177–2181, 2951–2955 > > ## Teil 1 – `F`/`Shift` ist doppelt gebunden, ohne Modifikatoren abzutrennen > > Der Fokus wird an zwei unabhängigen Stellen behandelt: > > ```js > // index.html:2101-2131 INPUT.init() > const KEYMAP = { ..., "f": "focus", "F": "focus", "Shift": "focus", "ShiftLeft": "focus", "ShiftRight": "focus" }; > ... > else if (act === "focus") { hooks.onGesture(); state.focusEdge = true; }; // focusEdge wird NIE gelesen > ``` > > ```js > // index.html:2951-2955 GAME.boot() > window.addEventListener("keydown", function (ev) { > if (ev.key === "f" || ev.key === "F" || ev.key === "Shift") tryFocus(); > }); > ``` > > * Die INPUT-Variante schreibt in ein totes Flag (`state.focusEdge` existiert nur hier und in der > Deklaration Zeile 2059 – kein Leser). > * Die tatsächlich wirkende Variante hat **keine Modifikator-Prüfung** und kein `ev.repeat`-Filter. > > Folge: `Strg+F` / `Cmd+F` (Browser-Suche), `Strg+S`, `F5`-Verwechslungen u. ä. lösen > `tryFocus()` aus, solange der Fokus-Balken voll ist – inklusive Fokus-Sound und Bildschirm-Blitz. > Der andere Handler verhindert das nicht, weil `preventDefault()` nur für die > Pfeiltasten/`Space`-Codes gerufen wird (Zeile 2110). > > ## Teil 2 – `pointerleave` hinterlässt gesetzte Halte-Zustände > > ```js > // index.html:2170-2181 > function up(ev) { // wird nur bei pointerup ausgeführt > ... > if (pt.side === "left") state.jumpHeld = false; else state.rollHeld = false; > delete pointers[ev.pointerId]; > ... > } > canvas.addEventListener("pointerup", up, { passive: true }); > canvas.addEventListener("pointercancel", function (ev) { delete pointers[ev.pointerId]; }, { passive: true }); > canvas.addEventListener("pointerleave", function (ev) { delete pointers[ev.pointerId]; }, { passive: true }); > ``` > > Wird der Zeiger nach einem Sprung gehalten, wandert vom Canvas weg und außerhalb losgelassen, > löscht `pointerleave`/`pointercancel` nur den Zeiger-Datensatz. > `state.jumpHeld` bleibt `true`, und damit bleibt der Sprunghalte-Auftrieb > (`JUMP_HOLD_BOOST`, `MAX_JUMP_HOLD_TIME`) für den **nächsten** Sprung aktiv, ohne dass der > Spieler die Taste hält. Aufgelöst wird es erst durch `clearEdges()` bei `blur`/Pause. > > ## Reproduktion > > Teil 1: Fokus-Balken voll (`S.focusMeter = 1`), dann `Strg+F` drücken → Fokus startet > (`S.focusActive === true`, `AUDIO.focus()`, Blitz). > > Teil 2: Linken Bildschirmbereich gedrückt halten (Sprung mit Halteboost), Zeiger vom Canvas > ziehen, außerhalb loslassen, danach erneut springen → Sprunghöhe entspricht dem Halte- statt dem > Kurzhub. > > ## Lösungsvorschlag > > * Eine einzige zentrale Tastaturbehandlung. `INPUT` setzt `focusEdge`, `playerCmd`/`update` > verbrauchen es – oder der zweite Listener in `boot()` entfällt. > * Modifikator- und Wiederholungsfilter vor allen Aktionen: > > ```js > if (ev.repeat) return; > if (ev.ctrlKey || ev.metaKey || ev.altKey) return; > ``` > > * `pointercancel`/`pointerleave` wie `up` behandeln (Halte-Zustände zurücksetzen, ohne > Tap-Aktion auszulösen), z. B. gemeinsame Funktion `endPointer(ev, { fireTap: false })`. > * Tests: `Strg+F` löst keinen Fokus aus; gehaltener Linksklick + Loslassen außerhalb führt nicht > zu einem Boost-Sprung. > > --- > > *Dokumentiert in `issues/08-eingabe-fokus-doppelt-und-zeiger-haengt.md` (Commit `c171694`).* 1 comments
Author
Owner

Behoben in 9cd8ba8 (main).

Teil 1 – Doppelbindung und Modifikatoren

  • Eine Tastatureingang: INPUT.init() ist der einzige keydown-Listener. Der Fokus liegt in
    der KEYMAP (f/F/Shift/ShiftLeft/ShiftRight) und ruft hooks.onFocus – denselben
    Hook, den der Fokus-Button bedient. Der zweite Listener in boot() entfällt (inkl. seines
    Tab-Noops), das leselose state.focusEdge ebenfalls.
  • Filter vor allen Aktionen: const chord = ev.ctrlKey || ev.metaKey || ev.altKey; und
    if (ev.repeat || chord) return;. preventDefault() für Leertaste/Pfeiltasten läuft nur ohne
    Chord, damit Cmd+P, Strg+S & Co. beim Browser bleiben.
  • Bewusst nicht gefiltert: Shift (Großbuchstaben erreichen die Keymap über ev.key, "S"
    ist die Rolle – ein Shift-Filter hätte sie verschluckt) und der keyup-Handler (eine bei
    gedrücktem Shift freigegebene Taste muss ihr Halte-Flag verlieren, sonst entsteht das Klemmen
    aus Teil 2 neu).

Gemessen vorher (Fokus-Balken voll, echte Tastaturevents): Strg+F, Cmd+F, Alt+F setzten
S.focusActive = true und leerten den Balken; Strg+M schaltete den Ton von an auf aus. Nachher:
keine Reaktion, Balken bleibt voll.

Teil 2 – gehaltene Zeiger-Eingabe

War mit dem gemeinsamen endPointer(ev, withTap) in 5884804 (Issue #14) bereits erledigt und
ist jetzt erstmals eigens gemessen: pointerup → endPointer(ev, true),
pointercancel/pointerleave → endPointer(ev, false) (Halte-Flag des diesen Zeigers
zurücksetzen, kein Tap).

Test

tests/browser_issue8.py (in tests/run-all.sh), Viewport 1440×760 – nur dort liegt Rand neben
der 16:9-Bühne, sonst kann der Zeiger das Spielfeld nicht verlassen. Echte Pointer-/Tastaturevents,
requestAnimationFrame stillgelegt, Simulation über PULSE.update(1/120):

  • Strg+F/Cmd+F/Alt+F kein Fokus, Strg+M kein Tonwechsel, ev.repeat verbraucht kein
    Greifen – und unverändert grün: F und Shift allein lösen Fokus aus, Shift+S rollt,
    Escape pausiert und setzt fort.
  • Sprung per Wisch mit gehaltenem Zeiger: jumpHeld endet in dem Augenblick, in dem der Zeiger den
    Canvas verlässt (Scheitel 203 px statt 229 px Dauerhalt; Kontrolle Kurzhub 165 px), außerhalb
    loslassen hinterlässt kein jumpHeld und keinen halboffenen Zeiger, das nächste
    Sprung-Kommando meldet jumpHeld false, pointercancel bricht ohne Tap-Aktion ab.

Gegenüberstellung, jeweils mit PULSE_HTML: gegen git show 9a1fd56:index.html (Stand vor diesem
Fix) sind 4 Prüfungen rot und nach dem Fix grün; gegen eine Fassung, die pointercancel/pointerleave
zurück auf delete pointers[ev.pointerId] reduziert, sind 6 Prüfungen rot
(jumpHeld/anyPointerDown bleiben true, Scheitel 229 = voller Boost).

Übrige Suite grün: alle Node-Suiten, soak.mjs, browser_smoke.py, browser_issue5/7/14.py.
browser_flow.py zeigt wie dokumentiert (AGENTS.md, „Bekannte Test-Unschärfe“) vereinzelt Frames
knapp über der Bildraten-Schranke – wechselt zwischen alter und neuer Fassung (3× alternierend:
HEAD 1 Treffer, neue Fassung 1 Treffer), also kein Zusammenhang mit dieser Änderung.

Doku nachgezogen: AGENTS.md (Invarianten-Abschnitt „Tastatur hat genau einen Eingang“),
tests/README.md, tests/run-all.sh.

Behoben in `9cd8ba8` (main). **Teil 1 – Doppelbindung und Modifikatoren** * Eine Tastatureingang: `INPUT.init()` ist der einzige `keydown`-Listener. Der Fokus liegt in der `KEYMAP` (`f`/`F`/`Shift`/`ShiftLeft`/`ShiftRight`) und ruft `hooks.onFocus` – denselben Hook, den der Fokus-Button bedient. Der zweite Listener in `boot()` entfällt (inkl. seines `Tab`-Noops), das leselose `state.focusEdge` ebenfalls. * Filter vor allen Aktionen: `const chord = ev.ctrlKey || ev.metaKey || ev.altKey;` und `if (ev.repeat || chord) return;`. `preventDefault()` für Leertaste/Pfeiltasten läuft nur ohne Chord, damit `Cmd+P`, `Strg+S` & Co. beim Browser bleiben. * Bewusst *nicht* gefiltert: `Shift` (Großbuchstaben erreichen die Keymap über `ev.key`, `"S"` ist die Rolle – ein Shift-Filter hätte sie verschluckt) und der `keyup`-Handler (eine bei gedrücktem Shift freigegebene Taste muss ihr Halte-Flag verlieren, sonst entsteht das Klemmen aus Teil 2 neu). Gemessen vorher (Fokus-Balken voll, echte Tastaturevents): `Strg+F`, `Cmd+F`, `Alt+F` setzten `S.focusActive = true` und leerten den Balken; `Strg+M` schaltete den Ton von an auf aus. Nachher: keine Reaktion, Balken bleibt voll. **Teil 2 – gehaltene Zeiger-Eingabe** War mit dem gemeinsamen `endPointer(ev, withTap)` in `5884804` (Issue #14) bereits erledigt und ist jetzt erstmals eigens gemessen: `pointerup` → `endPointer(ev, true)`, `pointercancel`/`pointerleave` → `endPointer(ev, false)` (Halte-Flag des *diesen* Zeigers zurücksetzen, kein Tap). **Test** `tests/browser_issue8.py` (in `tests/run-all.sh`), Viewport 1440×760 – nur dort liegt Rand neben der 16:9-Bühne, sonst kann der Zeiger das Spielfeld nicht verlassen. Echte Pointer-/Tastaturevents, `requestAnimationFrame` stillgelegt, Simulation über `PULSE.update(1/120)`: * `Strg+F`/`Cmd+F`/`Alt+F` kein Fokus, `Strg+M` kein Tonwechsel, `ev.repeat` verbraucht kein Greifen – und unverändert grün: `F` und `Shift` allein lösen Fokus aus, `Shift+S` rollt, `Escape` pausiert und setzt fort. * Sprung per Wisch mit gehaltenem Zeiger: `jumpHeld` endet in dem Augenblick, in dem der Zeiger den Canvas verlässt (Scheitel 203 px statt 229 px Dauerhalt; Kontrolle Kurzhub 165 px), außerhalb loslassen hinterlässt kein `jumpHeld` und keinen halboffenen Zeiger, das nächste Sprung-Kommando meldet `jumpHeld false`, `pointercancel` bricht ohne Tap-Aktion ab. Gegenüberstellung, jeweils mit `PULSE_HTML`: gegen `git show 9a1fd56:index.html` (Stand vor diesem Fix) sind 4 Prüfungen rot und nach dem Fix grün; gegen eine Fassung, die `pointercancel`/`pointerleave` zurück auf `delete pointers[ev.pointerId]` reduziert, sind 6 Prüfungen rot (`jumpHeld`/`anyPointerDown` bleiben `true`, Scheitel 229 = voller Boost). Übrige Suite grün: alle Node-Suiten, `soak.mjs`, `browser_smoke.py`, `browser_issue5/7/14.py`. `browser_flow.py` zeigt wie dokumentiert (`AGENTS.md`, „Bekannte Test-Unschärfe“) vereinzelt Frames knapp über der Bildraten-Schranke – wechselt zwischen alter und neuer Fassung (3× alternierend: HEAD 1 Treffer, neue Fassung 1 Treffer), also kein Zusammenhang mit dieser Änderung. Doku nachgezogen: `AGENTS.md` (Invarianten-Abschnitt „Tastatur hat genau einen Eingang“), `tests/README.md`, `tests/run-all.sh`.
gilli closed this issue 2026-09-01 10:48:02 +02:00
Sign in to join this conversation.
No description provided.