Ungefangener TypeError body.hook.x, wenn Greifen und Landung im selben Simulationsschritt erfolgen #3

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

⁨⁩⁨Ungefangener TypeError body.hook.x, wenn Greifen und Landung im selben Simulationsschritt erfolgen⁩ ⁨⁩#⁨3⁩⁨⁩"
By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩
bug
crash
gameplay
high

Komponente: GAME (simStep)
Betroffene Version: main @ 238ed66, index.html Zeile 2444

Beschreibung

Hängt sich die Figur an einen Haken und landet noch im selben Simulationsschritt auf festem
Boden (oder das Seil wird im selben Schritt schlaff), stürzt die Spielschleife mit einer
Uncaught Exception ab:

TypeError: Cannot read properties of null (reading 'x')
    at simStep (index.html:2444:43)
    at update (index.html:2655:7)

Ursache

CORE.stepBody() führt Greifen und die nachgelagerte Pendelintegration in einem Aufruf aus:

// index.html:712-726
if (cmd.grab && b.state !== "HANGING") {
  const h = findGrab(b, world);
  if (h) attachHook(b, h, ev);          // setzt ev.grabbed = true, b.hook = h
}
...
if (b.state === "HANGING") integrateHang(b, world, dt, cmd, ev);   // kann b.hook = null setzen

integrateHang() setzt b.hook = null in zwei Zweigen, während ev.grabbed bereits true ist:

// index.html:637-642  „assistierte Landung“
if (surf && b.y >= surf.y - K.STAND_OFF - 3 && (surf.y - h.y) > 24) {
  b.state = "RUNNING"; b.hook = null; ... if (ev) { ev.landed = true; ev.assistedLanding = true; }
}
// index.html:645-651  Seilspannung negativ
if (tension < 0) { b.state = "SLACK"; ... b.hook = null; ... }

Der Spiel-Handler verlässt sich darauf, dass body.hook bei ev.grabbed gesetzt ist:

// index.html:2441-2445
if (ev.grabbed) {
  S.swingPending = true;
  AUDIO.grab();
  RENDER.spawn(S.particles, body.hook.x, body.hook.y, 9, { ... });   // <-- body.hook ist null
}

Reproduktion

Geometrie entsteht direkt aus der Generierung: Hakenhöhen baseY - 248 … -300
(buildPattern, Zeile 939) und x auf [e0 + 60, g1 - 26] geklemmt (Zeile 941).
Greift die Figur im Sprung knapp vor der Landekante, beträgt der Hakenabstand ≈ 220 px
(GRAB_RADIUS = 225), der Pendel-Body wird auf h.y + ropeLen gesnappt und immediately die
assistierte Landung ausgelöst.

Node (CORE isoliert)

const w = C.makeWorld();
w.segments.push({ id:1, x0:-100, x1:500, y:K.GROUND_Y, type:'G' });
w.segments.push({ id:2, x0:1100, x1:5000, y:K.GROUND_Y, type:'G' });
const h = { id:9, x:1100-26, y:K.GROUND_Y-248, coolT:0, disabled:false, grabs:0 };
w.hooks.push(h);
const b = C.makeBody(1102, K.GROUND_Y, 400);
b.grounded = false; b.state = 'FALLING'; b.vy = 140; b.x = 1102; b.y = K.GROUND_Y-30;
const ev = C.stepBody(b, w, 1/120, { grab:true, jumpHeld:false, rollHeld:false }, 400, 0, {});
// ev.grabbed = true | ev.landed = true | b.hook = null  ->  b.hook.x  =>  TypeError

Ausgabe:

distance to hook: 219.8 (GRAB_RADIUS 225)
events: { grabbed: true, landed: true, slack: false, state: 'RUNNING' }
body.hook = null
*** CRASH (GAME simStep): TypeError: Cannot read properties of null (reading 'x')

Browser (Playwright, reale rAF-Schleife)

Welt mit Hakengrube aufgebaut, PULSE.press('grab') und die nächste echte Animationsschleife
verarbeitet den Schritt:

LIVE REPRO: {"thrown":"Cannot read properties of null (reading 'x')",
             "state":"RUNNING","hook":null,"stateNow":"PLAYING",
             "hudBefore":"55 m","hudAfter":"55 m","swingPending":true}
page errors: 1
    Cannot read properties of null (reading 'x') :: at simStep (index.html:2444:43) at update (index.html:2655:7)

Auswirkung

  • Abgebrochener Simulationsschritt. Der Abbruch erfolgt vor den Hindernis-Dodges, der
    Fragmentaufnahme, der Gruben-Statistik, der Kamerabewegung, generateAhead(), pruneWorld()
    und der HUD-Aktualisierung – all das entfällt für diesen Frame
    (im Messlauf: HUD-Strecke blieb unverändert).
  • Falsch gutgeschriebener Schwung: S.swingPending bleibt true. Die nächste beliebige
    Landung zählt als abgeschlossener Schwung (+40 Punkte, +12 % Fokus, Zeile 2465–2469).
  • Verlorene Rückmeldung: Kein Grab-Sound, kein Partikel, kein Whoosh für diesen Haken.
  • Erreihbar mit allen Greif-Eingaben (Leertaste in der Luft, E/K, Linksklick, Autogriff).

Lösungsvorschlag

Lokale Referenz vor dem Nullen ziehen und den Handler absichern:

if (ev.grabbed) {
  S.swingPending = true;
  AUDIO.grab();
  const gh = ev.grabPos || body.hook;          // CORE könnte die Position direkt mitgeben
  if (gh) RENDER.spawn(S.particles, gh.x, gh.y, 9, { ... });
}

Sauberer: in CORE.attachHook() die Ankerposition ins Event legen
(ev.grabPos = { x: h.x, y: h.y }) und in integrateHang() bei assistierter Landung
ev.grabbed zurücknehmen bzw. ev.swingCancelled = true setzen, damit swingPending nicht
stehen bleibt. Dazu ein Regressionstest: „Greifen direkt an der Landekante → kein Fehler,
swings zählt genau 0 oder 1“.


Dokumentiert in issues/03-greifen-landung-gleicher-schritt-crash.md (Commit c171694).

1 comments

⁨⁩⁨Ungefangener TypeError `body.hook.x`, wenn Greifen und Landung im selben Simulationsschritt erfolgen⁩ ⁨⁩#⁨3⁩⁨⁩" By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩ bug crash gameplay high > **Komponente:** GAME (`simStep`) > **Betroffene Version:** `main` @ `238ed66`, `index.html` Zeile 2444 > > ## Beschreibung > > Hängt sich die Figur an einen Haken und landet noch im selben Simulationsschritt auf festem > Boden (oder das Seil wird im selben Schritt schlaff), stürzt die Spielschleife mit einer > Uncaught Exception ab: > > ```text > TypeError: Cannot read properties of null (reading 'x') > at simStep (index.html:2444:43) > at update (index.html:2655:7) > ``` > > ## Ursache > > `CORE.stepBody()` führt Greifen und die nachgelagerte Pendelintegration in **einem** Aufruf aus: > > ```js > // index.html:712-726 > if (cmd.grab && b.state !== "HANGING") { > const h = findGrab(b, world); > if (h) attachHook(b, h, ev); // setzt ev.grabbed = true, b.hook = h > } > ... > if (b.state === "HANGING") integrateHang(b, world, dt, cmd, ev); // kann b.hook = null setzen > ``` > > `integrateHang()` setzt `b.hook = null` in zwei Zweigen, während `ev.grabbed` bereits `true` ist: > > ```js > // index.html:637-642 „assistierte Landung“ > if (surf && b.y >= surf.y - K.STAND_OFF - 3 && (surf.y - h.y) > 24) { > b.state = "RUNNING"; b.hook = null; ... if (ev) { ev.landed = true; ev.assistedLanding = true; } > } > // index.html:645-651 Seilspannung negativ > if (tension < 0) { b.state = "SLACK"; ... b.hook = null; ... } > ``` > > Der Spiel-Handler verlässt sich darauf, dass `body.hook` bei `ev.grabbed` gesetzt ist: > > ```js > // index.html:2441-2445 > if (ev.grabbed) { > S.swingPending = true; > AUDIO.grab(); > RENDER.spawn(S.particles, body.hook.x, body.hook.y, 9, { ... }); // <-- body.hook ist null > } > ``` > > ## Reproduktion > > Geometrie entsteht direkt aus der Generierung: Hakenhöhen `baseY - 248 … -300` > (`buildPattern`, Zeile 939) und `x` auf `[e0 + 60, g1 - 26]` geklemmt (Zeile 941). > Greift die Figur im Sprung knapp vor der Landekante, beträgt der Hakenabstand ≈ 220 px > (`GRAB_RADIUS` = 225), der Pendel-Body wird auf `h.y + ropeLen` gesnappt und immediately die > assistierte Landung ausgelöst. > > ### Node (CORE isoliert) > > ```js > const w = C.makeWorld(); > w.segments.push({ id:1, x0:-100, x1:500, y:K.GROUND_Y, type:'G' }); > w.segments.push({ id:2, x0:1100, x1:5000, y:K.GROUND_Y, type:'G' }); > const h = { id:9, x:1100-26, y:K.GROUND_Y-248, coolT:0, disabled:false, grabs:0 }; > w.hooks.push(h); > const b = C.makeBody(1102, K.GROUND_Y, 400); > b.grounded = false; b.state = 'FALLING'; b.vy = 140; b.x = 1102; b.y = K.GROUND_Y-30; > const ev = C.stepBody(b, w, 1/120, { grab:true, jumpHeld:false, rollHeld:false }, 400, 0, {}); > // ev.grabbed = true | ev.landed = true | b.hook = null -> b.hook.x => TypeError > ``` > > Ausgabe: > > ```text > distance to hook: 219.8 (GRAB_RADIUS 225) > events: { grabbed: true, landed: true, slack: false, state: 'RUNNING' } > body.hook = null > *** CRASH (GAME simStep): TypeError: Cannot read properties of null (reading 'x') > ``` > > ### Browser (Playwright, reale rAF-Schleife) > > Welt mit Hakengrube aufgebaut, `PULSE.press('grab')` und die nächste echte Animationsschleife > verarbeitet den Schritt: > > ```text > LIVE REPRO: {"thrown":"Cannot read properties of null (reading 'x')", > "state":"RUNNING","hook":null,"stateNow":"PLAYING", > "hudBefore":"55 m","hudAfter":"55 m","swingPending":true} > page errors: 1 > Cannot read properties of null (reading 'x') :: at simStep (index.html:2444:43) at update (index.html:2655:7) > ``` > > ## Auswirkung > > * **Abgebrochener Simulationsschritt.** Der Abbruch erfolgt vor den Hindernis-Dodges, der > Fragmentaufnahme, der Gruben-Statistik, der Kamerabewegung, `generateAhead()`, `pruneWorld()` > und der HUD-Aktualisierung – all das entfällt für diesen Frame > (im Messlauf: HUD-Strecke blieb unverändert). > * **Falsch gutgeschriebener Schwung:** `S.swingPending` bleibt `true`. Die *nächste* beliebige > Landung zählt als abgeschlossener Schwung (`+40` Punkte, `+12 %` Fokus, Zeile 2465–2469). > * **Verlorene Rückmeldung:** Kein Grab-Sound, kein Partikel, kein Whoosh für diesen Haken. > * Erreihbar mit allen Greif-Eingaben (`Leertaste` in der Luft, `E`/`K`, Linksklick, Autogriff). > > ## Lösungsvorschlag > > Lokale Referenz vor dem Nullen ziehen und den Handler absichern: > > ```js > if (ev.grabbed) { > S.swingPending = true; > AUDIO.grab(); > const gh = ev.grabPos || body.hook; // CORE könnte die Position direkt mitgeben > if (gh) RENDER.spawn(S.particles, gh.x, gh.y, 9, { ... }); > } > ``` > > Sauberer: in `CORE.attachHook()` die Ankerposition ins Event legen > (`ev.grabPos = { x: h.x, y: h.y }`) und in `integrateHang()` bei assistierter Landung > `ev.grabbed` zurücknehmen bzw. `ev.swingCancelled = true` setzen, damit `swingPending` nicht > stehen bleibt. Dazu ein Regressionstest: „Greifen direkt an der Landekante → kein Fehler, > `swings` zählt genau 0 oder 1“. > > --- > > *Dokumentiert in `issues/03-greifen-landung-gleicher-schritt-crash.md` (Commit `c171694`).* 1 comments
Author
Owner

Behoben in 5eba31c: Grab-Events tragen ihre Ankerposition selbst (ev.grabX/ev.grabY aus attachHook()), weil integrateHang() das body.hook im selben Schritt löscht; stepBody() setzt zusätzlich ev.grabCancelled, wenn der Schwung im Anfangsschritt endet, damit die Gutschrift nicht als Luftbuchung steht und die nächste beliebige Landung teuer macht. Kein Zugriff auf body.hook.x in diesem Pfad. Verifiziert: node tests/issue3.mjs (11 Prüfungen grün, beide Lösungsäste über die Generierungsmaße) und python3 tests/browser_smoke.py mit Greifen + assistierter Landung im echten simStep (0 pageerror, kein Phantom-Schwung).

Behoben in `5eba31c`: Grab-Events tragen ihre Ankerposition selbst (`ev.grabX`/`ev.grabY` aus `attachHook()`), weil `integrateHang()` das `body.hook` im selben Schritt löscht; `stepBody()` setzt zusätzlich `ev.grabCancelled`, wenn der Schwung im Anfangsschritt endet, damit die Gutschrift nicht als Luftbuchung steht und die nächste beliebige Landung teuer macht. Kein Zugriff auf `body.hook.x` in diesem Pfad. Verifiziert: `node tests/issue3.mjs` (11 Prüfungen grün, beide Lösungsäste über die Generierungsmaße) und `python3 tests/browser_smoke.py` mit Greifen + assistierter Landung im echten `simStep` (0 pageerror, kein Phantom-Schwung).
gilli closed this issue 2026-08-31 21:04:02 +02:00
Sign in to join this conversation.
No description provided.