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

Open
opened 2026-08-29 01:41:04 +02:00 by G1LL1 · 3 comments
G1LL1 commented 2026-08-29 01:41:04 +02:00 (Migrated from gitlab.g1ll1.com)

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 gitlab-issues/03-greifen-landung-gleicher-schritt-crash.md (Commit 238ed66). Alle Befunde ausgeführt und gemessen; Repro-Schritte im Text.

**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 `gitlab-issues/03-greifen-landung-gleicher-schritt-crash.md` (Commit `238ed66`). Alle Befunde ausgeführt und gemessen; Repro-Schritte im Text.*
G1LL1 commented 2026-08-29 01:41:12 +02:00 (Migrated from gitlab.g1ll1.com)

mentioned in issue #12

mentioned in issue #12
G1LL1 commented 2026-08-29 01:45:23 +02:00 (Migrated from gitlab.g1ll1.com)

changed the description

changed the description
G1LL1 commented 2026-08-29 01:48:07 +02:00 (Migrated from gitlab.g1ll1.com)

changed the description

changed the description
Sign in to join this conversation.
No description provided.