Lichtschranke (GATE) hat keine Kollisionsbox und wird als 22-px-Stummel am oberen Bildschirmrand gezeichnet #1

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

⁨⁩⁨Lichtschranke (GATE) hat keine Kollisionsbox und wird als 22-px-Stummel am oberen Bildschirmrand gezeichnet⁩ ⁨⁩#⁨1⁩⁨⁩"
By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩
bug
critical
gameplay

Komponente: CORE (Level-Generator, Kollision), RENDER
Betroffene Version: main @ 238ed66 (übernommen aus kelpie @ 014b986), index.html

Beschreibung

Der Hindernistyp GATE (Lichtschranke – „unter hohen Hindernissen hindurchrollen“) hat im
spielbaren Zustand keine wirksame Hitbox. Der Kollisionsrechteck liegt bei y = 0 … 22,
also ganz oben am Bildschirmrand, statt bei der sichtbaren Balkenunterkante (bottom ≈ 514).

Die Figur kann in normaler Körperhaltung ungehindert jede Lichtschranke durchlaufen.
Zusätzlich wird das Hindernis beim Passieren als erfolgreiches Ausweichmanöver gewertet.

Das Visualisierungsproblem ist dieselbe Ursache: drawObstacle() leitet die sichtbare
Balkenunterkante aus demselben Rechteck ab (bottom = r.y + r.h) und zeichnet die Lichtschranke
dadurch als ca. 22 px hohen Stummel direkt am oberen Rand – nicht als bis auf Kopfhöhe
abgehängendes Tor.

Ursache

In mkObs() werden Höhe und Anker für GATE/MGATE gesetzt, aber die Y-Position auf 0
belassen:

// index.html:979-986
function mkObs(type, x, baseY, w, h) {
  const o = makeObstacle(type, x, baseY, {});
  o.w = w;
  if (type === "BLOCK") { o.h = h; o.y = baseY - h; }
  else if (type === "GATE")  { o.h = 22; o.bottom = baseY - K.GATE_CLEAR;  o.y = 0; }   // <-- o.y bleibt 0
  else if (type === "MGATE") { o.h = 22; o.bottom = baseY - K.MGATE_CLEAR; o.y = 0; }   // <-- egal, wird überschrieben
  return o;
}

obstacleRect() kennt nur für MGATE eine Neu-Berechnung aus o.bottom; für GATE wird
o.y unverwendet weitergereicht:

// index.html:506-512
function obstacleRect(o, tSec, worstCase) {
  if (o.type === "MGATE") { /* ... korrekt: y = o.bottom - osc - o.h */ }
  return { x: o.x, y: o.y, w: o.w, h: o.h };   // GATE -> { y: 0, h: 22 }
}

makeObstacle() (Zeile 499–503) berechnet o.y zunächst korrekt, wird aber von mkObs()
wieder überschrieben. Der MGATE-Zweig funktioniert nur, weil obstacleRect() ihn aus
o.bottom rekonstruiert.

Reproduktion

Isoliert (Node, ohne Browser)

Der CORE-Block ist über die Marker /*__CORE_START__*/ / /*__CORE_END__*/ extrahierbar:

python3 - <<'PY'
src = open('index.html', encoding='utf-8').read()
s, e = src.index('/*__CORE_START__*/'), src.index('/*__CORE_END__*/')
open('core.mjs','w').write(src[s:e] + "\nexport default CORE;\n")
PY
import C from './core.mjs';
const K = C.K, world = C.makeWorld(), gen = C.createGenerator(7);
const ctx = { speed: 400, minReactionSec: 1.0, sectorAbs: 3, sectorIdx: 3, distanceM: 1500, difficulty: 0.5, endless: 0, sectorDef: C.SECTORS[3] };
C.commitContent(world, gen, C.buildPattern('ROLL', gen, ctx), ctx);
const o = world.obstacles[0];
console.log(C.obstacleRect(o, 0, false));            // { y: 0, h: 22 }  <-- statt y ≈ 492

const b = C.makeBody(o.x + o.w / 2, K.GROUND_Y, 400); // stehende Figur direkt in der Tor-Säule
console.log(C.bodyHitbox(b));                         // { y: 500, h: 60 }
console.log(C.hitTest(b, world.obstacles, 0, false)); // null  <-- keine Kollision

Ausgabe:

collision rect: { x: 60.58, y: 0,  w: 41.49, h: 22 }
standing body:  { x: 68.33, y: 500, w: 26,   h: 60 }
STANDING body inside gate -> collision? NO

Im echten Spiel (Playwright, window.PULSE)

Eine einzelne GATE auf flachem Boden gesetzt, 1,2 s ohne Eingabe gelaufen:

{
  "gateRectRuntime": { "x": 1135.2, "y": 0, "w": 50, "h": 22 },
  "drawnBottom": 22,
  "bodyPassedGate": true,
  "dying": false,
  "dodges": 1,
  "dodgesBefore": 0
}

Erwartetes Verhalten

  • Stehende Figur berührt den Lichtschranken-Balken → Kollision → „Lauf beendet / Kollision“.
  • Rollende Figur (34 × 34, ROLL_OFF = 17) passiert den Balken mit lichter Höhe GATE_CLEAR = 46.
  • Sichtbarer Balkenende-Punkt bei bottom = baseY - GATE_CLEAR = 514.

Auswirkung

  1. Kernmechanik fehlt: Eine der drei Grundaktionen (Rollen) ist für das Überleben unnötig.
    ROLL, JUMP_ROLL, ROLL_JUMP und HOOK_TO_ROLL sind ohne Rollpflicht.
  2. Punkte- und Fokus-Inflation: Jedes Durchlaufen zählt als Ausweichmanöver
    (simStep, Zeile 2477–2483: +50 Punkte, +4 % Fokus).
  3. Solver prüft gegen ein Geister-Hindernis: verifyContent()/simStrategy() nutzen
    dieselbe hitTest()/testBox()-Funktion. Damit verifiziert die Lösbarkeitsprüfung
    Lichtschranken-Muster gegen eine nicht existende Kollision. Die Pflicht aus AGENTS.md
    „Kein Haken darf so platziert werden, dass die Figur … unter die Unterkante einer
    Lichtschranke schlägt
    “ ist dadurch nicht durchsetzbar – der Solver kann diesen Konflikt
    nicht sehen.
  4. Visualisierung kaputt: Lichtschranke erscheint als 22-px-Markierung am oberen Rand.

Lösungsvorschlag

Entweder Y-Position beim Erzeugen setzen:

else if (type === "GATE")  { o.h = 22; o.bottom = baseY - K.GATE_CLEAR;  o.y = o.bottom - o.h; }
else if (type === "MGATE") { o.h = 22; o.bottom = baseY - K.MGATE_CLEAR; o.y = o.bottom - o.amp - o.h; }

oder – robuster, weil o.y dann nie mehr von Hand gepflegt werden muss – einen GATE-Zweig
in obstacleRect() ergänzen:

if (o.type === "GATE") return { x: o.x, y: o.bottom - o.h, w: o.w, h: o.h };

Danach ist auch drawObstacle() (Zeile 1669) wieder konsistent, und die Lösbarkeitsprüfung
erfasst Lichtschranken automatisch korrekt. Zusätzlich sollte ein Regressionstest die drei
Fälle abdecken: stehend = Treffer, rollend = kein Treffer, Sprung über bottom = kein Treffer.


Dokumentiert in issues/01-gate-lichtschranke-keine-kollision.md (Commit c171694).

1 comments

⁨⁩⁨Lichtschranke (GATE) hat keine Kollisionsbox und wird als 22-px-Stummel am oberen Bildschirmrand gezeichnet⁩ ⁨⁩#⁨1⁩⁨⁩" By ⁨⁩⁨gilli⁩⁨⁩ — ⁨⁨⁩Closed⁨⁩⁩ bug critical gameplay > **Komponente:** CORE (Level-Generator, Kollision), RENDER > **Betroffene Version:** `main` @ `238ed66` (übernommen aus `kelpie` @ `014b986`), `index.html` > > ## Beschreibung > > Der Hindernistyp `GATE` (Lichtschranke – „unter hohen Hindernissen hindurchrollen“) hat im > spielbaren Zustand **keine wirksame Hitbox**. Der Kollisionsrechteck liegt bei `y = 0 … 22`, > also ganz oben am Bildschirmrand, statt bei der sichtbaren Balkenunterkante (`bottom ≈ 514`). > > Die Figur kann in normaler Körperhaltung ungehindert jede Lichtschranke durchlaufen. > Zusätzlich wird das Hindernis beim Passieren als erfolgreiches Ausweichmanöver gewertet. > > Das Visualisierungsproblem ist dieselbe Ursache: `drawObstacle()` leitet die sichtbare > Balkenunterkante aus demselben Rechteck ab (`bottom = r.y + r.h`) und zeichnet die Lichtschranke > dadurch als ca. 22 px hohen Stummel direkt am oberen Rand – nicht als bis auf Kopfhöhe > abgehängendes Tor. > > ## Ursache > > In `mkObs()` werden Höhe und Anker für `GATE`/`MGATE` gesetzt, aber die Y-Position auf `0` > belassen: > > ```js > // index.html:979-986 > function mkObs(type, x, baseY, w, h) { > const o = makeObstacle(type, x, baseY, {}); > o.w = w; > if (type === "BLOCK") { o.h = h; o.y = baseY - h; } > else if (type === "GATE") { o.h = 22; o.bottom = baseY - K.GATE_CLEAR; o.y = 0; } // <-- o.y bleibt 0 > else if (type === "MGATE") { o.h = 22; o.bottom = baseY - K.MGATE_CLEAR; o.y = 0; } // <-- egal, wird überschrieben > return o; > } > ``` > > `obstacleRect()` kennt nur für `MGATE` eine Neu-Berechnung aus `o.bottom`; für `GATE` wird > `o.y` unverwendet weitergereicht: > > ```js > // index.html:506-512 > function obstacleRect(o, tSec, worstCase) { > if (o.type === "MGATE") { /* ... korrekt: y = o.bottom - osc - o.h */ } > return { x: o.x, y: o.y, w: o.w, h: o.h }; // GATE -> { y: 0, h: 22 } > } > ``` > > `makeObstacle()` (Zeile 499–503) berechnet `o.y` zunächst korrekt, wird aber von `mkObs()` > wieder überschrieben. Der `MGATE`-Zweig funktioniert nur, weil `obstacleRect()` ihn aus > `o.bottom` rekonstruiert. > > ## Reproduktion > > ### Isoliert (Node, ohne Browser) > > Der CORE-Block ist über die Marker `/*__CORE_START__*/` / `/*__CORE_END__*/` extrahierbar: > > ```bash > python3 - <<'PY' > src = open('index.html', encoding='utf-8').read() > s, e = src.index('/*__CORE_START__*/'), src.index('/*__CORE_END__*/') > open('core.mjs','w').write(src[s:e] + "\nexport default CORE;\n") > PY > ``` > > ```js > import C from './core.mjs'; > const K = C.K, world = C.makeWorld(), gen = C.createGenerator(7); > const ctx = { speed: 400, minReactionSec: 1.0, sectorAbs: 3, sectorIdx: 3, distanceM: 1500, difficulty: 0.5, endless: 0, sectorDef: C.SECTORS[3] }; > C.commitContent(world, gen, C.buildPattern('ROLL', gen, ctx), ctx); > const o = world.obstacles[0]; > console.log(C.obstacleRect(o, 0, false)); // { y: 0, h: 22 } <-- statt y ≈ 492 > > const b = C.makeBody(o.x + o.w / 2, K.GROUND_Y, 400); // stehende Figur direkt in der Tor-Säule > console.log(C.bodyHitbox(b)); // { y: 500, h: 60 } > console.log(C.hitTest(b, world.obstacles, 0, false)); // null <-- keine Kollision > ``` > > Ausgabe: > > ```text > collision rect: { x: 60.58, y: 0, w: 41.49, h: 22 } > standing body: { x: 68.33, y: 500, w: 26, h: 60 } > STANDING body inside gate -> collision? NO > ``` > > ### Im echten Spiel (Playwright, `window.PULSE`) > > Eine einzelne `GATE` auf flachem Boden gesetzt, 1,2 s ohne Eingabe gelaufen: > > ```json > { > "gateRectRuntime": { "x": 1135.2, "y": 0, "w": 50, "h": 22 }, > "drawnBottom": 22, > "bodyPassedGate": true, > "dying": false, > "dodges": 1, > "dodgesBefore": 0 > } > ``` > > ## Erwartetes Verhalten > > * Stehende Figur berührt den Lichtschranken-Balken → Kollision → „Lauf beendet / Kollision“. > * Rollende Figur (34 × 34, `ROLL_OFF = 17`) passiert den Balken mit lichter Höhe `GATE_CLEAR = 46`. > * Sichtbarer Balkenende-Punkt bei `bottom = baseY - GATE_CLEAR = 514`. > > ## Auswirkung > > 1. **Kernmechanik fehlt:** Eine der drei Grundaktionen (Rollen) ist für das Überleben unnötig. > `ROLL`, `JUMP_ROLL`, `ROLL_JUMP` und `HOOK_TO_ROLL` sind ohne Rollpflicht. > 2. **Punkte- und Fokus-Inflation:** Jedes Durchlaufen zählt als Ausweichmanöver > (`simStep`, Zeile 2477–2483: `+50` Punkte, `+4 %` Fokus). > 3. **Solver prüft gegen ein Geister-Hindernis:** `verifyContent()`/`simStrategy()` nutzen > dieselbe `hitTest()`/`testBox()`-Funktion. Damit verifiziert die Lösbarkeitsprüfung > Lichtschranken-Muster gegen eine nicht existende Kollision. Die Pflicht aus AGENTS.md > „*Kein Haken darf so platziert werden, dass die Figur … unter die Unterkante einer > Lichtschranke schlägt*“ ist dadurch nicht durchsetzbar – der Solver kann diesen Konflikt > nicht sehen. > 4. **Visualisierung kaputt:** Lichtschranke erscheint als 22-px-Markierung am oberen Rand. > > ## Lösungsvorschlag > > Entweder Y-Position beim Erzeugen setzen: > > ```js > else if (type === "GATE") { o.h = 22; o.bottom = baseY - K.GATE_CLEAR; o.y = o.bottom - o.h; } > else if (type === "MGATE") { o.h = 22; o.bottom = baseY - K.MGATE_CLEAR; o.y = o.bottom - o.amp - o.h; } > ``` > > oder – robuster, weil `o.y` dann nie mehr von Hand gepflegt werden muss – einen `GATE`-Zweig > in `obstacleRect()` ergänzen: > > ```js > if (o.type === "GATE") return { x: o.x, y: o.bottom - o.h, w: o.w, h: o.h }; > ``` > > Danach ist auch `drawObstacle()` (Zeile 1669) wieder konsistent, und die Lösbarkeitsprüfung > erfasst Lichtschranken automatisch korrekt. Zusätzlich sollte ein Regressionstest die drei > Fälle abdecken: stehend = Treffer, rollend = kein Treffer, Sprung über `bottom` = kein Treffer. > > --- > > *Dokumentiert in `issues/01-gate-lichtschranke-keine-kollision.md` (Commit `c171694`).* 1 comments
Author
Owner

Behoben in 174aeb7: Kollisionsbox für GATE/MGATE leitet sich aus o.bottom in obstacleRect() ab (Vorhang von der Decke bis Balkenunterkante, statt Stummel bei y=0/h=22), MGATE_CLEAR 64 → 48, plus groundEdgeAfter() für durchgehende Bodensegmente derselben Höhe. Verifiziert: node tests/issue1.mjs (27 Prüfungen grün), Rollen ist einziger Durchgang, stehend trifft sicher; deckt auch die Solver-Sicht ab. Volle Suite ./tests/run-all.sh am Stand c171694 vollständig grün.

Behoben in `174aeb7`: Kollisionsbox für GATE/MGATE leitet sich aus `o.bottom` in `obstacleRect()` ab (Vorhang von der Decke bis Balkenunterkante, statt Stummel bei y=0/h=22), `MGATE_CLEAR` 64 → 48, plus `groundEdgeAfter()` für durchgehende Bodensegmente derselben Höhe. Verifiziert: `node tests/issue1.mjs` (27 Prüfungen grün), Rollen ist einziger Durchgang, stehend trifft sicher; deckt auch die Solver-Sicht ab. Volle Suite `./tests/run-all.sh` am Stand `c171694` vollständig grün.
gilli closed this issue 2026-08-31 21:03:13 +02:00
Sign in to join this conversation.
No description provided.