Spielfigur läuft spiegelverkehrt und rollt sichtbar durch den Boden hindurch #13

Open
opened 2026-08-29 13:48:58 +02:00 by G1LL1 · 0 comments
G1LL1 commented 2026-08-29 13:48:58 +02:00 (Migrated from gitlab.g1ll1.com)

Komponente: RENDER (poseFor, drawFigure) im Zusammenspiel mit CORE.bodyHitbox
Betroffene Version: main @ 9809788, index.html Zeilen 1750–1838, 2709, 2896

Befund

Zwei zusammenhängende Darstellungsfehler an der Spielfigur, gemessen am laufenden Spiel
(Playwright gegen window.PULSE, Crops in tests/.tmp/):

  1. Blickrichtung/Laufrichtung widersprechen sich. Kopf und Rumpf zeigen in Fahrtrichtung,
    aber beide Gelenkpärchen beugen in die gespiegelte Richtung — die Figur wirkt, als lief sie
    rückwärts.
  2. Die Rollbewegung clippt durch den Boden. Während einer Rolle reicht die Tinte der Figur
    bis zu 32 px unter die Bodenkante und schwebt im Gegenzug zum Rollenstart 14 px über dem
    Boden. Kontakt zum Boden gibt es in der Rollanimation praktisch nie.

Ursache

Winkel-Definition

limb() zählt den Winkel von „senkrecht nach unten“ aus nach +x:

// index.html:1750-1754
function limb(o, a1, l1, a2, l2) {
  const x1 = o[0] + Math.sin(a1) * l1, y1 = o[1] + Math.cos(a1) * l1;
  const a = a1 + a2;
  return [o, [x1, y1], [x1 + Math.sin(a) * l2, y1 + Math.cos(a) * l2]];
}

+x ist nachweislich die Fahrtrichtung: b.x += b.vx * dt (Zeile 702), Kamera
body.x - K.PLAYER_SCREEN_X (Zeile 2712) und der Schal flattert mit dir = 1 nach −x
(Zeile 2899 → anchor[0] - (26 + i * 8) * dir, Zeile 1847).

1) Spiegelverkehrte Gelenkbeugung

Über 720 Samples eines vollen Laufzyklus (poseFor("RUNNING", phase)):

Feld Wertebereich über den Zyklus korrekt für Lauf nach +x
p.lean (Rumpf) konstant +0.16 vornüber
p.legA[1] / p.legB[1] (Knie) +0.34 … +0.96, nie ≤ 0 Wade muss nach hinten (−x)
p.armA[1] / p.armB[1] (Ellen) −0.78 … −0.22, nie ≥ 0 Unterarm muss nach vorn (+x)

Das menschliche Knickt das Bein nur in Wadenrichtung (Ferse zum Gesäß) — bei senkrecht
stehendem Oberschenkel also nach −x; der Ellenbeuge beugt den Unterarm nach vorn (+x).
Die Posendaten in poseFor (Zeilen 1761–1767 für RUNNING, 1770–1775 für JUMPING) notieren
beide Beugungen mit umgekehrtem Vorzeichen, sind also für eine nach links laufende Figur
geschrieben. Da der Kopf als kreisförmige Kontur keine Blickrichtung trägt, liest das Auge die
Richtung ausschließlich aus den Gelenken — und die zeigen gegen die Fahrt.

2) Roll-Pose hat keinen Anschluss an die Roll-Kollisionsbox

Die Roll-Hitbox ist bodenbündig 34 px hoch, ihr Mittelpunkt liegt auf der Hüfte:

// index.html:552  (bodyHitbox)
if (b.roll > 0) return { x: b.x - K.ROLL_W / 2, y: b.y - K.ROLL_OFF, w: K.ROLL_W, h: K.ROLL_H };
// index.html:404   ROLL_W: 34, ROLL_H: 34,  ROLL_OFF: 17
// index.html:712   grounded: b.y = surf.y - off   (off = ROLL_OFF beim Rollen)

drawFigure() dreht um genau diesen Punkt — das ist korrekt:

// index.html:1817-1818
ctx.translate(sx, sy + p.bob);
ctx.rotate(p.rot);              // p.rot = b.rollAngle, Zeile 1788 / 2896

Aber die Roll-Pose krümmt nur Arme und Beine; der Kopf bleibt auf der Stand-Geometrie
(shoulder.y = -23, FIG.headOff = -13.6, FIG.headR = 9.6):

// index.html:1782-1788
} else if (state === "ROLLING") {
  p.curl = 1;                  // <- wird nirgends gelesen (einziges Vorkommen ausser Initialisierung Zeile 1758)
  p.legA = [1.6, 1.85];
  ...

Damit ist die gezeichnete Figur um den Drehkreis herum 49,2 px weit ausgedehnt (Silhouette
y −49,2 … +3,4 bezogen auf die Hüfte, also 52,6 px hoch), während nach unten nur
ROLL_OFF = 17 px bis zur Bodenoberfläche frei sind. Alles, was bei der Verdrehung nach
unten wandert, landet im Erdreich.

Nachgerechnet (Original-Kinematik aus FIG/limb/poseFor, Pixel gegen den gezeichneten
Canvas gemessen):

rollAngle tiefster gezeichneter Punkt Relief Kopf-Oberkante über Boden
3,4 px unter Hüfte 13,6 px über Boden (kein Kontakt) 66,2 px → ragt 20 px ins Tor
90° 17,0 px 0,0 px (Zufall) 22,8 px
175° 49,4 px 32,4 px UNTER Boden −13,0 px
250° 16,2 px 0,8 px frei
  • Überlappung mit dem Erdreich: 160° von 360° pro Umdrehung.
  • rollAngle wächst mit vx / 30 rad/s (Zeile 2709), Roll-Dauer 0,48–0,62 s
    (rollDuration, Zeile 739) → 0,99 … 1,95 Umdrehungen pro Roll, davon 44 % = 0,23 s bis
    0,28 s
    mit sichtbarer Tinte unter dem Boden. Bei jeder Rollbewegung, mehrfach.
  • Zweiter Widerspruch Physik/Bild: die Lichtschranke lässt im Kanal Boden … +46 px
    (GATE_CLEAR 46, Zeile 413) passagefrei, die Roll-Pose hält den Kopf bei kleiner Verdrehung
    aber bis auf 66 px über den Boden — der Kopf zeichnet also durch den Balken, obwohl die
    Kollision korrekt frei ist.

Reproduktion

  1. python3 tests/browser_flow.py-artigen Headless-Start verwenden (window.PULSE),
    requestAnimationFrame auf () => 0 setzen.
  2. Ebene Testwelt ohne Hindernisse, PULSE.startRun().
  3. Für Befund 2:
const S = PULSE.S, K = PULSE.CORE.K, b = S.body;
b.roll = 0.5; b.state = 'ROLLING'; b.grounded = true;
b.y = K.GROUND_Y - K.ROLL_OFF; b.vy = 0;
b.rollAngle = Math.PI;        // 180°
PULSE.render();

Bildcrop um b.x - S.camX, K.GROUND_Y: Kopf liegt vollständig unter der Bodenkante.
Auch im normalen Spielbetrieb mit S/ auf freiem Boden sichtbar, ohne dass die
Kollision etwas damit zu tun hat.
4. Befund 1: b.phase einmal von 0 bis durchziehen — nie ein Bild, in dem Knie und
Ellenbeuge zur Fahrtrichtung passen.

Messprotokoll (Diagnoseskripte, nicht im Repo):

== Laufzyklus ==   Knie leg[1]: 0.34 .. 0.96 (nur positiv)   Ellen arm[1]: -0.78 .. -0.22 (nur negativ)
== Roll-Pose ==    Höhe 52,6 px, Radius um Hüfte 49,2 px | ROLL_OFF 17
                   Überlappung 160°/360°, tiefster Durchhänger 32,4 px bei 175°
Canvas-Pixel gegen die gezeichnete Bodenkante (Kanten- und Schraffurband ausgenommen):
  0° 0,0   30° 0,0   60° 0,0   90° 0,0  120° 16,5  150° 29,5  180° 33,5
 210° 25,5 240° 10,0 270° 0,0  300° 0,0 330° 0,0

Auswirkung

  • Die Signaturbewegung des Spiels (Roll unter die Lichtschranke) sieht falsch aus: Figur
    halb im Boden, dazu Kopfkontakt mit Balken, die es spielerisch nicht gibt.
  • Die Richtung des Laufens ist aus der Animation nicht ablesbar; der Eindruck „läuft rückwärts“
    entsteht in jeder Laufphase.
  • Kein Effekt auf Treffer, Punkte oder Stabilität — ein reiner Darstellungsfehler, aber an der
    sichtbarsten Animationsgruppe.
  • Die bestehende Testsuite erfasst das nicht: weder tests/issue1.mjs noch tests/issue2.mjs
    noch die Browser-Suiten prüfen die gezeichnete Sil gegen die Kollisionsbox.

Lösungsvorschlag

Zu 1 — Gelenkvorzeichen auf die Fahrtrichtung drehen (bevorzugen gegenüber spiegeln):
die zweiten Komponenten der Bein-/Arm-Paare in poseFor auf die anatomisch richtige Seite
ziehen (leg*[1] negativ, arm*[1] positiv) und dabei p.lean, p.headNod sowie die
Sprungdrehung opts.rot = clamp(-b.vy / 4200, …) (Zeile 2898) gegenprüfen. Ein pauschales
ctx.scale(-1, 1) in drawFigure würde auch aim (Hang-Ausrichtung, Zeile 1791–1796),
opts.rot und die Schalrichtung mitdrehen und müsste an allen Stellen nachgezogen werden.

Zu 2 — Roll-Pose wirklich einrollen: alle Gelenkpunkte müssen innerhalb des Roll-Kreises
liegen, damit die Drehung um b.y den Boden nie schneidet. Konkret: Rumpf zur Kugel krümmen,
Kopf zwischen/unter die Knie nehmen, so dass max(|y|) ≤ ROLL_OFF über alle Gelenke plus
Kopfradius gilt. p.curl (Zeilen 1758/1783) ist als Ansatz vorhanden, wird aber nie gelesen —
sinnvoll wäre ein Krümmungsfaktor, der FIG-Längen um die Hüfte zusammenzieht. Alternativ
bewusst größer bauen und dann die Roll-Kollisionsbox und die Tor-Kanäle mitnehmen — das ist
aber ein Dreierpack aus CORE.K, Tests und Commit-Notiz (Arbeitsregel 4) und verändert
GATE_CLEAR-Toleranzen.

Regressionstest dazu (deterministisch, ohne Browser): Posen aus poseFor über die
Zeichnungskinematik in Weltkoordinaten auflösen und fordern

  • tiefster gezeichneter Punkt der Rolling- und Running-Pose ≤ Bodenoberfläche,
  • höchster gezeichneter Punkt der Rolling-Pose ≤ GATE_CLEAR über Boden,
  • Roll-Silhouette liegt für alle rollAngle ∈ [0, 2π) innerhalb der Roll-Hitbox plus
    Strichstärken-Toleranz.

Dokumentiert in gitlab-issues/13-laufpose-spiegelverkehrt-und-roll-clippt-durch-den-boden.md (Commit 9809788).

**Komponente:** RENDER (`poseFor`, `drawFigure`) im Zusammenspiel mit `CORE.bodyHitbox` **Betroffene Version:** `main` @ `9809788`, `index.html` Zeilen 1750–1838, 2709, 2896 ## Befund Zwei zusammenhängende Darstellungsfehler an der Spielfigur, gemessen am laufenden Spiel (Playwright gegen `window.PULSE`, Crops in `tests/.tmp/`): 1. **Blickrichtung/Laufrichtung widersprechen sich.** Kopf und Rumpf zeigen in Fahrtrichtung, aber beide Gelenkpärchen beugen in die gespiegelte Richtung — die Figur wirkt, als lief sie rückwärts. 2. **Die Rollbewegung clippt durch den Boden.** Während einer Rolle reicht die Tinte der Figur bis zu 32 px unter die Bodenkante und schwebt im Gegenzug zum Rollenstart 14 px über dem Boden. Kontakt zum Boden gibt es in der Rollanimation praktisch nie. ## Ursache ### Winkel-Definition `limb()` zählt den Winkel von „senkrecht nach unten“ aus **nach +x**: ```js // index.html:1750-1754 function limb(o, a1, l1, a2, l2) { const x1 = o[0] + Math.sin(a1) * l1, y1 = o[1] + Math.cos(a1) * l1; const a = a1 + a2; return [o, [x1, y1], [x1 + Math.sin(a) * l2, y1 + Math.cos(a) * l2]]; } ``` +x ist nachweislich die Fahrtrichtung: `b.x += b.vx * dt` (Zeile 702), Kamera `body.x - K.PLAYER_SCREEN_X` (Zeile 2712) und der Schal flattert mit `dir = 1` nach −x (Zeile 2899 → `anchor[0] - (26 + i * 8) * dir`, Zeile 1847). ### 1) Spiegelverkehrte Gelenkbeugung Über 720 Samples eines vollen Laufzyklus (`poseFor("RUNNING", phase)`): | Feld | Wertebereich über den Zyklus | korrekt für Lauf nach +x | |---|---|---| | `p.lean` (Rumpf) | konstant `+0.16` | ✅ vornüber | | `p.legA[1]` / `p.legB[1]` (Knie) | `+0.34 … +0.96`, **nie ≤ 0** | ❌ Wade muss nach hinten (−x) | | `p.armA[1]` / `p.armB[1]` (Ellen) | `−0.78 … −0.22`, **nie ≥ 0** | ❌ Unterarm muss nach vorn (+x) | Das menschliche Knickt das Bein nur in Wadenrichtung (Ferse zum Gesäß) — bei senkrecht stehendem Oberschenkel also nach −x; der Ellenbeuge beugt den Unterarm nach vorn (+x). Die Posendaten in `poseFor` (Zeilen 1761–1767 für `RUNNING`, 1770–1775 für `JUMPING`) notieren beide Beugungen mit umgekehrtem Vorzeichen, sind also für eine nach **links** laufende Figur geschrieben. Da der Kopf als kreisförmige Kontur keine Blickrichtung trägt, liest das Auge die Richtung ausschließlich aus den Gelenken — und die zeigen gegen die Fahrt. ### 2) Roll-Pose hat keinen Anschluss an die Roll-Kollisionsbox Die Roll-Hitbox ist bodenbündig 34 px hoch, ihr Mittelpunkt liegt auf der Hüfte: ```js // index.html:552 (bodyHitbox) if (b.roll > 0) return { x: b.x - K.ROLL_W / 2, y: b.y - K.ROLL_OFF, w: K.ROLL_W, h: K.ROLL_H }; // index.html:404 ROLL_W: 34, ROLL_H: 34, ROLL_OFF: 17 // index.html:712 grounded: b.y = surf.y - off (off = ROLL_OFF beim Rollen) ``` `drawFigure()` dreht um genau diesen Punkt — das ist korrekt: ```js // index.html:1817-1818 ctx.translate(sx, sy + p.bob); ctx.rotate(p.rot); // p.rot = b.rollAngle, Zeile 1788 / 2896 ``` Aber die Roll-Pose krümmt nur Arme und Beine; der Kopf bleibt auf der Stand-Geometrie (`shoulder.y = -23`, `FIG.headOff = -13.6`, `FIG.headR = 9.6`): ```js // index.html:1782-1788 } else if (state === "ROLLING") { p.curl = 1; // <- wird nirgends gelesen (einziges Vorkommen ausser Initialisierung Zeile 1758) p.legA = [1.6, 1.85]; ... ``` Damit ist die gezeichnete Figur um den Drehkreis herum **49,2 px weit ausgedehnt** (Silhouette y −49,2 … +3,4 bezogen auf die Hüfte, also 52,6 px hoch), während nach unten nur `ROLL_OFF` = **17 px** bis zur Bodenoberfläche frei sind. Alles, was bei der Verdrehung nach unten wandert, landet im Erdreich. Nachgerechnet (Original-Kinematik aus `FIG`/`limb`/`poseFor`, Pixel gegen den gezeichneten Canvas gemessen): | `rollAngle` | tiefster gezeichneter Punkt | Relief | Kopf-Oberkante über Boden | |---|---|---|---| | 0° | 3,4 px unter Hüfte | 13,6 px **über** Boden (kein Kontakt) | 66,2 px → ragt 20 px ins Tor | | 90° | 17,0 px | 0,0 px (Zufall) | 22,8 px | | 175° | 49,4 px | **32,4 px UNTER Boden** | −13,0 px | | 250° | 16,2 px | 0,8 px frei | — | * Überlappung mit dem Erdreich: **160° von 360°** pro Umdrehung. * `rollAngle` wächst mit `vx / 30` rad/s (Zeile 2709), Roll-Dauer 0,48–0,62 s (`rollDuration`, Zeile 739) → **0,99 … 1,95 Umdrehungen pro Roll**, davon **44 % = 0,23 s bis 0,28 s** mit sichtbarer Tinte unter dem Boden. Bei jeder Rollbewegung, mehrfach. * Zweiter Widerspruch Physik/Bild: die Lichtschranke lässt im Kanal Boden … +46 px (`GATE_CLEAR` 46, Zeile 413) passagefrei, die Roll-Pose hält den Kopf bei kleiner Verdrehung aber bis auf 66 px über den Boden — der Kopf zeichnet also **durch den Balken**, obwohl die Kollision korrekt frei ist. ## Reproduktion 1. `python3 tests/browser_flow.py`-artigen Headless-Start verwenden (`window.PULSE`), `requestAnimationFrame` auf `() => 0` setzen. 2. Ebene Testwelt ohne Hindernisse, `PULSE.startRun()`. 3. Für Befund 2: ```js const S = PULSE.S, K = PULSE.CORE.K, b = S.body; b.roll = 0.5; b.state = 'ROLLING'; b.grounded = true; b.y = K.GROUND_Y - K.ROLL_OFF; b.vy = 0; b.rollAngle = Math.PI; // 180° PULSE.render(); ``` Bildcrop um `b.x - S.camX`, `K.GROUND_Y`: Kopf liegt vollständig unter der Bodenkante. Auch im normalen Spielbetrieb mit `S`/`↓` auf freiem Boden sichtbar, ohne dass die Kollision etwas damit zu tun hat. 4. Befund 1: `b.phase` einmal von `0` bis `2π` durchziehen — nie ein Bild, in dem Knie und Ellenbeuge zur Fahrtrichtung passen. Messprotokoll (Diagnoseskripte, nicht im Repo): ```text == Laufzyklus == Knie leg[1]: 0.34 .. 0.96 (nur positiv) Ellen arm[1]: -0.78 .. -0.22 (nur negativ) == Roll-Pose == Höhe 52,6 px, Radius um Hüfte 49,2 px | ROLL_OFF 17 Überlappung 160°/360°, tiefster Durchhänger 32,4 px bei 175° Canvas-Pixel gegen die gezeichnete Bodenkante (Kanten- und Schraffurband ausgenommen): 0° 0,0 30° 0,0 60° 0,0 90° 0,0 120° 16,5 150° 29,5 180° 33,5 210° 25,5 240° 10,0 270° 0,0 300° 0,0 330° 0,0 ``` ## Auswirkung * Die Signaturbewegung des Spiels (Roll unter die Lichtschranke) sieht falsch aus: Figur halb im Boden, dazu Kopfkontakt mit Balken, die es spielerisch nicht gibt. * Die Richtung des Laufens ist aus der Animation nicht ablesbar; der Eindruck „läuft rückwärts“ entsteht in jeder Laufphase. * Kein Effekt auf Treffer, Punkte oder Stabilität — ein reiner Darstellungsfehler, aber an der sichtbarsten Animationsgruppe. * Die bestehende Testsuite erfasst das nicht: weder `tests/issue1.mjs` noch `tests/issue2.mjs` noch die Browser-Suiten prüfen die gezeichnete Sil gegen die Kollisionsbox. ## Lösungsvorschlag **Zu 1 — Gelenkvorzeichen auf die Fahrtrichtung drehen** (bevorzugen gegenüber spiegeln): die zweiten Komponenten der Bein-/Arm-Paare in `poseFor` auf die anatomisch richtige Seite ziehen (`leg*[1]` negativ, `arm*[1]` positiv) und dabei `p.lean`, `p.headNod` sowie die Sprungdrehung `opts.rot = clamp(-b.vy / 4200, …)` (Zeile 2898) gegenprüfen. Ein pauschales `ctx.scale(-1, 1)` in `drawFigure` würde auch `aim` (Hang-Ausrichtung, Zeile 1791–1796), `opts.rot` und die Schalrichtung mitdrehen und müsste an allen Stellen nachgezogen werden. **Zu 2 — Roll-Pose wirklich einrollen:** alle Gelenkpunkte müssen innerhalb des Roll-Kreises liegen, damit die Drehung um `b.y` den Boden nie schneidet. Konkret: Rumpf zur Kugel krümmen, Kopf zwischen/unter die Knie nehmen, so dass `max(|y|) ≤ ROLL_OFF` über alle Gelenke plus Kopfradius gilt. `p.curl` (Zeilen 1758/1783) ist als Ansatz vorhanden, wird aber nie gelesen — sinnvoll wäre ein Krümmungsfaktor, der `FIG`-Längen um die Hüfte zusammenzieht. Alternativ bewusst größer bauen und dann die Roll-Kollisionsbox und die Tor-Kanäle mitnehmen — das ist aber ein Dreierpack aus `CORE.K`, Tests und Commit-Notiz (Arbeitsregel 4) und verändert `GATE_CLEAR`-Toleranzen. **Regressionstest dazu** (deterministisch, ohne Browser): Posen aus `poseFor` über die Zeichnungskinematik in Weltkoordinaten auflösen und fordern * tiefster gezeichneter Punkt der Rolling- und Running-Pose ≤ Bodenoberfläche, * höchster gezeichneter Punkt der Rolling-Pose ≤ `GATE_CLEAR` über Boden, * Roll-Silhouette liegt für alle `rollAngle ∈ [0, 2π)` innerhalb der Roll-Hitbox plus Strichstärken-Toleranz. --- *Dokumentiert in `gitlab-issues/13-laufpose-spiegelverkehrt-und-roll-clippt-durch-den-boden.md` (Commit `9809788`).*
Sign in to join this conversation.
No description provided.