diff --git a/NOTES.md b/NOTES.md index d5d0729..84a7a6d 100644 --- a/NOTES.md +++ b/NOTES.md @@ -1567,3 +1567,25 @@ HUD-Render deckungsgleich mit Stefans Original-Screenshot. --once/E2E ok. mit Stefans Original-Screenshot (Kopf-Schild + Punkte). - Nebenfund: W1ANI/W2ANI/W3ANI.ICO = Tile-ANIMATIONSPHASEN (broeckelnde Bloecke etc.) -- fuer spaeter (animierte Tiles). + +## 25.07.2026 (8) -- vierte Feedback-Runde: Tuer-Sequenz, Kamera, Feinschliff + +1. **Fuesse schwebten**: Sprites sinken beim Zeichnen 4px ein (SPRITE_SINK) + -- im Original stehen die Figuren IN den Blaettern, nicht drauf. +2. **Steh-Frames**: Tony idle=16/17 (0/1 sind Schrittphasen -- "letzter + Lauf-Frame blieb stehen"); Smacks-Walk auf [0..11] gekuerzt (12-15 sind + Steh-/Uebergangsframes, Idle 12/13 jetzt nicht mehr im Zyklus). +3. **Tuer-Sequenz**: DOORS[0] ist die OFFENE Tuer (Stefans Korrektur). + Ruhezustand = geschlossene Tile-Tuer + Kopf-Schild (DOORS[5]); ALT -> + Tuer zeigt Frame 0 (offen), nach 1s folgt die Tuer-Aktion (Charakter- + Menue). Gilt kuenftig fuer alle Tueren (Schluessel-Tueren DOORS 1-4). +4. **Slope-Tunneling** beim Landen gefixt: SWEPT-Check ueber alle zwischen + alter und neuer Position ueberstrichenen Zeilen (bei 8px/Frame Fall + wurde die duenne Graskante sonst uebersprungen) + Herkunftsbedingung + (alte Position muss ueber der Kante gewesen sein). Stress-Test: 50 + Drops ueber die ganze Hangbreite, 0 durchgefallen. +5. **Kamera**: Standard so, dass die Figur bei ~75% Hoehe steht (mehr Blick + nach unten); DUCKEN >=1.2s gehalten faehrt die Kamera sanft ~40px runter + (Wasser unter Abgruenden sichtbar), Loslassen faehrt zurueck. + +Tony-auf-Wasser+Frosties-Idle von Stefan als "sehr geil" abgenommen (kein Fix). diff --git a/game/level.py b/game/level.py index 4ea4c10..701ee74 100644 --- a/game/level.py +++ b/game/level.py @@ -47,6 +47,8 @@ IDLE_ANIM_AFTER = 5.0 # Sekunden Stillstand bis zur Idle-Animation IDLE_ANIM_FPS = 2.5 WATER_DAMAGE_EVERY = 5.0 # im Wasser: alle 5s ein Herz (Stefans Angabe) SPAWN_ANIM_SECONDS = 1.1 # Start-Sprite beim Levelstart/Charakterwechsel +SPRITE_SINK = 4 # Fuesse sinken optisch in die Blaetter/Gras ein + # (Stefans "Figuren schweben ein paar Pixel") WATER_WADE_DEPTH = 14 # Nicht-Schwimmer sinken nur bis hierhin ein # (Original: Tony "springt auf der Wasser- # oberflaeche", taucht NICHT unter) @@ -61,7 +63,9 @@ CHAR_DEFS = { bob='TONY', can_run=True, can_swim=False, walk_r=[0, 2, 4, 6, 8, 10, 12, 14], walk_l=[1, 3, 5, 7, 9, 11, 13, 15], run_r=[26, 28, 30, 32], run_l=[27, 29, 31, 33], - idle_r=0, idle_l=1, jump_r=18, jump_l=19, duck_r=24, duck_l=25, + # 16/17 = echte Steh-Frames (0/1 sind Schrittphasen -- "letzter + # Lauf-Frame bleibt stehen"-Bug) + idle_r=16, idle_l=17, jump_r=18, jump_l=19, duck_r=24, duck_l=25, swim_r=None, swim_l=None, idle_anim_r=[44, 46, 48], idle_anim_l=[45, 47, 49], spawn_frame=22, # Arm nach oben gestreckt @@ -69,7 +73,8 @@ CHAR_DEFS = { ), 'SMACKS': dict( bob='SMACKS', can_run=False, can_swim=True, - walk_r=[0, 2, 4, 6, 8, 10, 12, 14], walk_l=[1, 3, 5, 7, 9, 11, 13, 15], + # Zyklus OHNE 12-15 (das sind Steh-/Uebergangsframes) + walk_r=[0, 2, 4, 6, 8, 10], walk_l=[1, 3, 5, 7, 9, 11], run_r=None, run_l=None, # Idle 12/13: Smacks' echte Steh-Frames (0/1 sind Schrittphasen -- # Stefans Bug "beim Anhalten wird noch das Laufen-Sprite angezeigt") @@ -353,12 +358,18 @@ class Player: # Boden ist (bergauf laufen), darf mit Toleranz nach OBEN aufsteigen. if self.vy >= 0: was_grounded = self.on_ground or self._grounded_before + old_ty = int(self.y // TILE) foot_ty = int(ny // TILE) - tys = (foot_ty - 1, foot_ty, foot_ty + 1) if was_grounded else (foot_ty, foot_ty + 1) + # SWEPT: alle zwischen alter und neuer Position ueberstrichenen + # Zeilen pruefen -- bei schnellem Fall (bis 8px/Frame) konnte die + # duenne Graskante sonst uebersprungen werden (Stefans "falle + # durch den Slope-Boden wenn ich ungluecklich lande"). + start = min(old_ty, foot_ty) - (1 if was_grounded else 0) tol = 8 if was_grounded else 0 - for ty2 in tys: + for ty2 in range(start, foot_ty + 2): surf = level.slope_surface(self.x, ty2) - if surf is not None and ny >= surf - tol: + if (surf is not None and ny >= surf - tol + and self.y <= surf + tol + 6): ny = surf - 0.01 self.vy = 0.0 self.on_ground = True @@ -411,7 +422,7 @@ class Player: i = self.current_frame_index() surf = self.surfs[i] if i < len(self.surfs) else self.surfs[0] screen.blit(surf, (int(self.x - surf.get_width() / 2 - cam_x), - int(self.y - surf.get_height() - cam_y))) + int(self.y - surf.get_height() - cam_y) + SPRITE_SINK)) class Hud: @@ -545,12 +556,16 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'): player = Player(assets, info['chars'][0], spawn_px) hud = Hud(assets) door = pygame.Rect(info['door_px']) if info.get('door_px') else None - # Echte Tuergrafik: DOORS.BOB Frame 0 = Tuer MIT Kopf-Schild (32x48, - # exakt Tuergroesse; Frames 1-4 sind die Schluessel-Tueren mit farbigen - # Knaeufen -- fuer spaeter). - door_frames = parse_bob(assets.container.entries['DOORS.BOB']) - door_pal = get_bob_palette(assets.container.entries, 'DOORS') - door_sprite = _frames_to_surfaces(door_frames, door_pal)[0] + # Tuergrafiken (DOORS.BOB): Frame 0 = OFFENE Tuer (Stefans Korrektur), + # Frame 5 = Kopf-Schild einzeln. Ruhezustand = geschlossene Tile-Tuer der + # Map + Schild; bei ALT oeffnet sich die Tuer (Frame 0) und nach ~1s + # passiert die Tuer-Aktion (hier: Charakterwechsel-Menue). Frames 1-4 + # sind die Schluessel-Tueren mit farbigen Knaeufen (spaeter). + _door_surfs = _frames_to_surfaces(parse_bob(assets.container.entries['DOORS.BOB']), + get_bob_palette(assets.container.entries, 'DOORS')) + door_open_sprite = _door_surfs[0] + door_sign_sprite = _door_surfs[5] + door_open_t = None # None = zu; sonst Sekunden seit ALT-Druck if pygame.mixer.get_init(): path = audio_paths.get(('ONGAME2', None)) @@ -567,6 +582,8 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'): map_h_px = level.height * TILE hearts, lives = 6, 3 # 6 Herzen (3x2) -- Original-Screenshot water_t = 0.0 + duck_hold = 0.0 + look_down = 0.0 # Kamera-Offset beim gehaltenen Ducken while True: dt = min(clock.tick(60) / 1000.0, 1 / 20) @@ -580,20 +597,29 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'): if event.key == pygame.K_ESCAPE: return 'map' # Tuer benutzen: ALT im STEHEN vor der Tuer (nicht beim - # Rennen -- Stefans Regel) -> Charakterwechsel-Menue + # Rennen -- Stefans Regel) -> Tuer oeffnet sich (Frame 0), + # ~1s spaeter kommt die Tuer-Aktion (Charakterwechsel-Menue) if event.key in (pygame.K_LALT, pygame.K_RALT): if (door and door.collidepoint(player.x, player.y - 1) - and player.on_ground and abs(player.vx) < 1): - choice = run_character_select(clock, assets, - info['chars'], player.char) - if choice == 'quit': - return 'quit' - if choice and choice != player.char: - player.set_character(choice) + and player.on_ground and abs(player.vx) < 1 + and door_open_t is None): + door_open_t = 0.0 keys = pygame.key.get_pressed() player.update(level, dt, keys) + # Tuer-Sequenz: offen zeigen, nach 1s die Aktion ausfuehren + if door_open_t is not None: + door_open_t += dt + if door_open_t >= 1.0: + door_open_t = None + choice = run_character_select(clock, assets, + info['chars'], player.char) + if choice == 'quit': + return 'quit' + if choice and choice != player.char: + player.set_character(choice) + # Wasser-Schaden (alle 5s ein Herz), Schwimmer sind sicher if level.in_water(player.y) and not player.swimming: water_t += dt @@ -610,13 +636,27 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'): else: water_t = 0.0 + # Kamera: Standard so, dass die Figur bei ~75%% der Hoehe steht + # (Stefans Referenz-Screenshot); haelt man DUCKEN gedrueckt, faehrt + # die Kamera nach kurzer Zeit sanft ~40px nach unten (Wasser unterm + # Abgrund sichtbar -- Original-Verhalten). + if player.ducking and player.on_ground: + duck_hold += dt + else: + duck_hold = 0.0 + target_down = 40.0 if duck_hold >= 1.2 else 0.0 + look_down += (target_down - look_down) * min(1.0, dt * 3.0) cam_x = max(0, min(int(player.x) - VIEW_W // 2, map_w_px - VIEW_W)) - cam_y = max(0, min(int(player.y) - VIEW_H * 2 // 3, map_h_px - VIEW_H)) + cam_y = max(0, min(int(player.y) - int(VIEW_H * 0.75) + int(look_down), + map_h_px - VIEW_H)) canvas.blit(level.surface, (0, 0), (cam_x, cam_y, VIEW_W, VIEW_H)) - # Tuer mit Kopf-Schild (DOORS.BOB Frame 0) ueber der Tile-Tuer + # Tuer: zu = Tile-Tuer + Kopf-Schild; offen (nach ALT) = Frame 0 if door: - canvas.blit(door_sprite, (door.x - cam_x, door.y - cam_y)) + if door_open_t is not None: + canvas.blit(door_open_sprite, (door.x - cam_x, door.y - cam_y)) + else: + canvas.blit(door_sign_sprite, (door.x - cam_x, door.y - cam_y)) player.draw(canvas, cam_x, cam_y) # Wasser: dunkles Overlay unterhalb der Wasserlinie (im Original ein # VGA-Palette-Effekt, nur bei machine=vgaonly sichtbar -- Stefans Fund)