Level-Feedback: Laufrichtung, Schraegen-Kollision, HUD, Rennen

Stefans annotierter Live-Test-Screenshot, alle 4 Punkte:
- Laufrichtung gefixt: TONY.BOB gerade Frames = rechts (war vertauscht,
  Figur lief rueckwaerts).
- Schraegen-Kollision: ctype 16 = Slope-Tile (per ctype-Farbvisualisierung
  exakt auf dem Grashang der Referenz identifiziert -- keine Tuer). 45-Grad-
  Diagonale, Richtung per Nachbar-Heuristik, am Fussmittelpunkt geprueft,
  Snap nur bei fallender Bewegung (kein Herunterziehen im Sprung).
  Simulation verifiziert: Hang hoch bis Plateau, kein Durchfallen mehr.
- HUD: weisse Kellogg's-Leiste (Schriftzug aus KELLOGGS.PCC gecroppt),
  Herzen als echte GAMEBAR-Sprites (10=voll/11=leer), Uhr-Box/Layout neu
  verteilt und Startzeit 9:59 (4-stellige Anzeige passt immer in die Zeile).
- RENNEN: ALT halten (Original-Anleitung: Tonys Faehigkeit) bzw. LSHIFT,
  185 px/s + schnellere Laufanimation.

Verifiziert: Slope-Simulation (min_y=559=Plateau, kein Durchfallen),
HUD-Standbild (Leiste/Herzen/Uhr komplett), E2E-Lauf rc=0, --once exit 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-07-25 10:01:14 +02:00
co-authored by Claude Fable 5
parent 973e290cf6
commit 0bb66b73a4
2 changed files with 114 additions and 27 deletions
+25
View File
@@ -1413,3 +1413,28 @@ deckungsgleich mit DOSBox-Referenz (Haus/Baum/?-Block/Bruecke/Saeulen).
**Offen:** Items (ctype 4) einsammeln, Gegner, Duck-Frame-Zuordnung pruefen, **Offen:** Items (ctype 4) einsammeln, Gegner, Duck-Frame-Zuordnung pruefen,
Original-HUD-Grafik, Karten-Animationen (KARTE0-9), Levelmusik-Song nach Original-HUD-Grafik, Karten-Animationen (KARTE0-9), Levelmusik-Song nach
Gehoer, Zeitablauf/Leben-Logik, weitere Level freischalten. Gehoer, Zeitablauf/Leben-Logik, weitere Level freischalten.
## 25.07.2026 (2) -- Stefans Level-Feedback eingearbeitet
Stefans annotierter Screenshot (4 Punkte), alle behoben:
1. **Figur lief rueckwaerts** -- TONY.BOB: GERADE Frames = RECHTS, ungerade
= links (Zuordnung war exakt vertauscht).
2. **Schraegen: fiel durch** -- ctype 16 ist KEINE Tuer, sondern das
SCHRAEGEN-Tile (als blaue Diagonale in der ctype-Farbvisualisierung exakt
auf dem Grashang gefunden). 45-Grad-Slope-Kollision implementiert
(Richtung per Nachbar-Heuristik, Fussmittelpunkt-basiert, Snap-Toleranz
10px). Simulation: Tony laeuft den Hang bis Plateau-Hoehe (y=559) hoch,
faellt nirgends mehr durch.
3. **HUD**: weisse Kellogg's-Leiste ergaenzt (Schriftzug aus KELLOGGS.PCC
gecroppt + klein skaliert), Herzen jetzt echte GAMEBAR-Sprites (Frame 10
voll / 11 leer; 13-15 sind die rot/blau/gruenen Schluessel -- fuer
Stefans "Tueren mit farbigem Knauf brauchen Schluessel" spaeter), Uhr-
Layout gefixt + Start bei 9:59 (4-stellig passt immer; Original zeigte
~1.5s nach Start ohnehin 9:59).
4. **RENNEN** -- Tonys Faehigkeit lt. Original-Anleitung ("MIT ALT
AKTIVIERST DU DIE FAEHIGKEITEN... TONY: RENNEN"): ALT halten (LSHIFT als
moderne Alternative), 185 statt 110 px/s, schnellere Lauf-Animation.
Physik-Feel laut Stefan gut. Levelmusik-Song prueft er nach Gehoer.
Offen bleiben: Tueren oeffnen (ALT an Tuer, Schluessel-Logik), Items,
Gegner, Karten-Animationen.
+89 -27
View File
@@ -41,23 +41,33 @@ VIEW_W, VIEW_H = 320, 176 # sichtbarer Levelausschnitt
HUD_H = 24 # HUD-Balken unten (320x24) HUD_H = 24 # HUD-Balken unten (320x24)
TILE = 16 TILE = 16
# Physik (v1 -- nach Spielgefuehl gegen die DOSBox-Referenz grob abgestimmt, # Physik (v1 -- nach Spielgefuehl gegen die DOSBox-Referenz grob abgestimmt;
# Feinschliff nach Stefans Live-Test) # Stefans Live-Test 25.07.: "fuehlt sich gut an")
WALK_SPEED = 110.0 # px/s WALK_SPEED = 110.0 # px/s
RUN_SPEED = 185.0 # px/s -- Tonys Faehigkeit: RENNEN (ALT halten,
# wie im Original lt. Anleitung; LSHIFT geht auch)
GRAVITY = 900.0 # px/s^2 GRAVITY = 900.0 # px/s^2
JUMP_VELOCITY = -300.0 # px/s (Sprunghoehe ~50px = gut 3 Tiles) JUMP_VELOCITY = -300.0 # px/s (Sprunghoehe ~50px = gut 3 Tiles)
WALK_ANIM_FPS = 12.0 WALK_ANIM_FPS = 12.0
LEVEL_TIME_SECONDS = 10 * 60 # Uhr im Original zaehlt von 10:00 runter # 9:59 statt 10:00: die Original-Anzeige stand ~1.5s nach Levelstart schon auf
# 9:59, und 4-stellig ("9:59") passt sicher in die Uhr-Box (320px-Zeile).
LEVEL_TIME_SECONDS = 599
SOLID_BIT = 32 SOLID_BIT = 32
SLOPE_CT = 16 # ctype 16 = Schraegen-Tile (Grashaenge) --
# als blaue Diagonale in der ctype-Visualisierung
# exakt auf dem Hang der DOSBox-Referenz gefunden
# ("faellt durch"-Bug von Stefans Live-Test)
# TONY.BOB-Frame-Zuordnung (siehe Modul-Docstring) # TONY.BOB-Frame-Zuordnung. KORREKTUR (Stefans Live-Test 25.07.2026: "Figur
IDLE_L, IDLE_R = 0, 1 # laeuft rueckwaerts, Richtung stimmt"): GERADE Indizes sind RECHTS,
WALK_L = [0, 2, 4, 6, 8, 10, 12, 14] # UNGERADE LINKS -- genau andersrum als zuerst angenommen.
WALK_R = [1, 3, 5, 7, 9, 11, 13, 15] IDLE_R, IDLE_L = 0, 1
JUMP_L, JUMP_R = 16, 17 WALK_R = [0, 2, 4, 6, 8, 10, 12, 14]
DUCK_L, DUCK_R = 24, 25 WALK_L = [1, 3, 5, 7, 9, 11, 13, 15]
JUMP_R, JUMP_L = 16, 17
DUCK_R, DUCK_L = 24, 25
def _frames_to_surfaces(frames, palette): def _frames_to_surfaces(frames, palette):
@@ -120,6 +130,26 @@ class Level:
def solid(self, tx, ty): def solid(self, tx, ty):
return bool(self.ctype(tx, ty) & SOLID_BIT) return bool(self.ctype(tx, ty) & SOLID_BIT)
def slope_surface(self, px, ty):
'''Boden-y der 45-Grad-Schraege im Slope-Tile (ctype 16) der Zeile ty
an Pixel-x px -- oder None, wenn dort kein Slope-Tile liegt.
Steigungsrichtung per Nachbar-Heuristik: schliesst rechts-oberhalb ein
weiteres Slope-/Solid-Tile an, steigt die Diagonale nach rechts
(Flaeche faellt nach links ab), sonst gespiegelt.'''
tx = int(px // TILE)
if self.ctype(tx, ty) != SLOPE_CT:
return None
fx = px - tx * TILE # 0..15 im Tile
rising_right = (self.ctype(tx + 1, ty - 1) == SLOPE_CT or
self.solid(tx + 1, ty - 1) or self.solid(tx + 1, ty))
rising_left = (self.ctype(tx - 1, ty - 1) == SLOPE_CT or
self.solid(tx - 1, ty - 1) or self.solid(tx - 1, ty))
if rising_right and not rising_left:
return (ty + 1) * TILE - 1 - fx # links unten -> rechts oben
if rising_left and not rising_right:
return ty * TILE + fx # links oben -> rechts unten
return (ty + 1) * TILE - 1 - fx # Default: nach rechts steigend
class Tony: class Tony:
'''Spielfigur: Position (px, Fusspunkt-basiert), einfache AABB-Physik '''Spielfigur: Position (px, Fusspunkt-basiert), einfache AABB-Physik
@@ -154,10 +184,16 @@ class Tony:
move -= 1 move -= 1
if keys[pygame.K_RIGHT]: if keys[pygame.K_RIGHT]:
move += 1 move += 1
self.vx = move * WALK_SPEED # Tonys Faehigkeit: RENNEN -- ALT halten (wie im Original laut
# Anleitung "MIT ALT AKTIVIERST DU DIE FAEHIGKEITEN"), LSHIFT als
# moderne Alternative. (Alt+Enter bleibt Vollbild -- kein Konflikt,
# das faengt die Eventschleife ab.)
mods = pygame.key.get_mods()
running = bool(mods & (pygame.KMOD_ALT | pygame.KMOD_SHIFT))
self.vx = move * (RUN_SPEED if running else WALK_SPEED)
if move: if move:
self.facing = move self.facing = move
self.anim_t += dt self.anim_t += dt * (1.5 if running else 1.0)
else: else:
self.anim_t = 0.0 self.anim_t = 0.0
@@ -200,6 +236,22 @@ class Tony:
if any(level.solid(tx, ty) for tx in range(left_tx, right_tx + 1)): if any(level.solid(tx, ty) for tx in range(left_tx, right_tx + 1)):
ny = (ty + 1) * TILE + self.HITBOX_H + 0.01 ny = (ty + 1) * TILE + self.HITBOX_H + 0.01
self.vy = 0.0 self.vy = 0.0
# Schraegen (ctype 16): Boden folgt der 45-Grad-Diagonale. Am
# FUSSMITTELPUNKT geprueft (nicht an den Hitbox-Kanten -- so "schwebt"
# Tony nicht an der Hangkante). Zeile darueber mitpruefen, damit er
# beim Bergauflaufen ins naechsthoehere Slope-Tile aufsteigt; Snap nur
# bei fallender/stehender Bewegung und kleiner Distanz (kein
# Herunterziehen mitten im Sprung).
if self.vy >= 0:
foot_ty = int(ny // TILE)
for ty2 in (foot_ty - 1, foot_ty, foot_ty + 1):
surf = level.slope_surface(self.x, ty2)
if surf is not None and ny >= surf - 10:
ny = surf - 0.01
self.vy = 0.0
self.on_ground = True
break
self.y = ny self.y = ny
def current_frame(self): def current_frame(self):
@@ -238,15 +290,24 @@ class Hud:
RED = (150, 14, 14) RED = (150, 14, 14)
DARK = (84, 6, 6) DARK = (84, 6, 6)
WHITE = (248, 248, 248)
def __init__(self, assets): def __init__(self, assets):
from .menu import BitmapFont, _mapping_from_rows from .menu import BitmapFont, _mapping_from_rows
container = assets.container container = assets.container
frames = parse_bob(container.entries['GAMEBAR.BOB']) frames = parse_bob(container.entries['GAMEBAR.BOB'])
palette = get_bob_palette(container.entries, 'GAMEBAR') palette = get_bob_palette(container.entries, 'GAMEBAR')
self.icons = _frames_to_surfaces(frames, palette) self.icons = _frames_to_surfaces(frames, palette)
self.heart_full = self.icons[10] # 9x8, pink/rot
self.heart_empty = self.icons[11]
self.font = BitmapFont(assets.pcc_surface('FON_1B'), 9, 7, self.font = BitmapFont(assets.pcc_surface('FON_1B'), 9, 7,
_mapping_from_rows(['1234567890;:!"%/()=?,.-\x7f#']), _mapping_from_rows(['1234567890;:!"%/()=?,.-\x7f#']),
spacing=8) spacing=8)
# Kellogg's-Schriftzug fuer die weisse HUD-Leiste links (Stefans
# Feedback: "die weisse Kellogg's-Leiste fehlt"): aus KELLOGGS.PCC
# gecroppt (Schriftzug ohne "praesentiert") und klein skaliert.
logo = assets.pcc_surface('KELLOGGS').subsurface((36, 26, 252, 66))
self.kellogg = pygame.transform.smoothscale(logo, (50, 13))
def _box(self, canvas, x, y, w, h): def _box(self, canvas, x, y, w, h):
pygame.draw.rect(canvas, self.GOLD, (x - 1, y - 1, w + 2, h + 2)) pygame.draw.rect(canvas, self.GOLD, (x - 1, y - 1, w + 2, h + 2))
@@ -258,29 +319,30 @@ class Hud:
def draw(self, canvas, y0, cookies, score, hearts, lives, time_left): def draw(self, canvas, y0, cookies, score, hearts, lives, time_left):
pygame.draw.rect(canvas, self.DARK, (0, y0, 320, HUD_H)) pygame.draw.rect(canvas, self.DARK, (0, y0, 320, HUD_H))
pad_y = y0 + 7 pad_y = y0 + 8
# Kellogg's-Leiste (weiss, abgerundet) ganz links
pygame.draw.rect(canvas, self.WHITE, (2, y0 + 3, 58, HUD_H - 6),
border_radius=6)
canvas.blit(self.kellogg, (6, y0 + 5))
# Keks-Zaehler # Keks-Zaehler
self._box(canvas, 30, pad_y - 3, 34, 15) self._box(canvas, 66, pad_y - 3, 32, 14)
self._text(canvas, 34, pad_y, f'{cookies:03d}') self._text(canvas, 70, pad_y, f'{cookies:03d}')
# Item-Box (leer) # Item-Box (leer)
self._box(canvas, 92, pad_y - 3, 40, 15) self._box(canvas, 104, pad_y - 3, 38, 14)
# Score # Score
self._box(canvas, 148, pad_y - 3, 58, 15) self._box(canvas, 148, pad_y - 3, 56, 14)
self._text(canvas, 152, pad_y, f'{score:06d}') self._text(canvas, 152, pad_y, f'{score:06d}')
# Herzen # Herzen (echte GAMEBAR-Sprites)
for i in range(4): for i in range(4):
col = (230, 40, 60) if i < hearts else (70, 10, 20) icon = self.heart_full if i < hearts else self.heart_empty
hx = 222 + i * 11 canvas.blit(icon, (210 + i * 11, pad_y - 1))
pygame.draw.circle(canvas, col, (hx + 3, pad_y + 3), 3)
pygame.draw.circle(canvas, col, (hx + 8, pad_y + 3), 3)
pygame.draw.polygon(canvas, col, [(hx, pad_y + 4), (hx + 5, pad_y + 10), (hx + 11, pad_y + 4)])
# Leben # Leben
self._box(canvas, 272, pad_y - 3, 13, 15) self._box(canvas, 259, pad_y - 3, 18, 14)
self._text(canvas, 275, pad_y, str(lives)) self._text(canvas, 264, pad_y, str(lives))
# Uhr m:ss # Uhr m:ss (Format 4-stellig, Startzeit 9:59 -> passt immer)
m, s = divmod(max(0, int(time_left)), 60) m, s = divmod(max(0, int(time_left)), 60)
self._box(canvas, 290, pad_y - 3, 29, 15) self._box(canvas, 283, pad_y - 3, 35, 14)
self._text(canvas, 292, pad_y, f'{m}:{s:02d}') self._text(canvas, 286, pad_y, f'{m}:{s:02d}')
def run_level(clock, assets, audio_paths, map_name='W1L1', spawn_tile=(17, 40)): def run_level(clock, assets, audio_paths, map_name='W1L1', spawn_tile=(17, 40)):