diff --git a/NOTES.md b/NOTES.md index aa17e43..4f7018a 100644 --- a/NOTES.md +++ b/NOTES.md @@ -1413,3 +1413,28 @@ deckungsgleich mit DOSBox-Referenz (Haus/Baum/?-Block/Bruecke/Saeulen). **Offen:** Items (ctype 4) einsammeln, Gegner, Duck-Frame-Zuordnung pruefen, Original-HUD-Grafik, Karten-Animationen (KARTE0-9), Levelmusik-Song nach 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. diff --git a/game/level.py b/game/level.py index 4789374..c3797fd 100644 --- a/game/level.py +++ b/game/level.py @@ -41,23 +41,33 @@ VIEW_W, VIEW_H = 320, 176 # sichtbarer Levelausschnitt HUD_H = 24 # HUD-Balken unten (320x24) TILE = 16 -# Physik (v1 -- nach Spielgefuehl gegen die DOSBox-Referenz grob abgestimmt, -# Feinschliff nach Stefans Live-Test) +# Physik (v1 -- nach Spielgefuehl gegen die DOSBox-Referenz grob abgestimmt; +# Stefans Live-Test 25.07.: "fuehlt sich gut an") 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 JUMP_VELOCITY = -300.0 # px/s (Sprunghoehe ~50px = gut 3 Tiles) 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 +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) -IDLE_L, IDLE_R = 0, 1 -WALK_L = [0, 2, 4, 6, 8, 10, 12, 14] -WALK_R = [1, 3, 5, 7, 9, 11, 13, 15] -JUMP_L, JUMP_R = 16, 17 -DUCK_L, DUCK_R = 24, 25 +# TONY.BOB-Frame-Zuordnung. KORREKTUR (Stefans Live-Test 25.07.2026: "Figur +# laeuft rueckwaerts, Richtung stimmt"): GERADE Indizes sind RECHTS, +# UNGERADE LINKS -- genau andersrum als zuerst angenommen. +IDLE_R, IDLE_L = 0, 1 +WALK_R = [0, 2, 4, 6, 8, 10, 12, 14] +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): @@ -120,6 +130,26 @@ class Level: def solid(self, tx, ty): 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: '''Spielfigur: Position (px, Fusspunkt-basiert), einfache AABB-Physik @@ -154,10 +184,16 @@ class Tony: move -= 1 if keys[pygame.K_RIGHT]: 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: self.facing = move - self.anim_t += dt + self.anim_t += dt * (1.5 if running else 1.0) else: 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)): ny = (ty + 1) * TILE + self.HITBOX_H + 0.01 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 def current_frame(self): @@ -238,15 +290,24 @@ class Hud: RED = (150, 14, 14) DARK = (84, 6, 6) + WHITE = (248, 248, 248) + def __init__(self, assets): from .menu import BitmapFont, _mapping_from_rows container = assets.container frames = parse_bob(container.entries['GAMEBAR.BOB']) palette = get_bob_palette(container.entries, 'GAMEBAR') 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, _mapping_from_rows(['1234567890;:!"%/()=?,.-\x7f#']), 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): 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): 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 - self._box(canvas, 30, pad_y - 3, 34, 15) - self._text(canvas, 34, pad_y, f'{cookies:03d}') + self._box(canvas, 66, pad_y - 3, 32, 14) + self._text(canvas, 70, pad_y, f'{cookies:03d}') # Item-Box (leer) - self._box(canvas, 92, pad_y - 3, 40, 15) + self._box(canvas, 104, pad_y - 3, 38, 14) # 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}') - # Herzen + # Herzen (echte GAMEBAR-Sprites) for i in range(4): - col = (230, 40, 60) if i < hearts else (70, 10, 20) - hx = 222 + i * 11 - 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)]) + icon = self.heart_full if i < hearts else self.heart_empty + canvas.blit(icon, (210 + i * 11, pad_y - 1)) # Leben - self._box(canvas, 272, pad_y - 3, 13, 15) - self._text(canvas, 275, pad_y, str(lives)) - # Uhr m:ss + self._box(canvas, 259, pad_y - 3, 18, 14) + self._text(canvas, 264, pad_y, str(lives)) + # Uhr m:ss (Format 4-stellig, Startzeit 9:59 -> passt immer) m, s = divmod(max(0, int(time_left)), 60) - self._box(canvas, 290, pad_y - 3, 29, 15) - self._text(canvas, 292, pad_y, f'{m}:{s:02d}') + self._box(canvas, 283, pad_y - 3, 35, 14) + self._text(canvas, 286, pad_y, f'{m}:{s:02d}') def run_level(clock, assets, audio_paths, map_name='W1L1', spawn_tile=(17, 40)):