diff --git a/NOTES.md b/NOTES.md index 84a7a6d..1fd9065 100644 --- a/NOTES.md +++ b/NOTES.md @@ -1589,3 +1589,21 @@ HUD-Render deckungsgleich mit Stefans Original-Screenshot. --once/E2E ok. (Wasser unter Abgruenden sichtbar), Loslassen faehrt zurueck. Tony-auf-Wasser+Frosties-Idle von Stefan als "sehr geil" abgenommen (kein Fix). + +## 25.07.2026 (9) -- fuenfte Feedback-Runde: Wasser-Bounce, Tod-Anim, Tuer-Peek, Kamera-Grace + +1. **Wasser-Bounce**: Nicht-Schwimmer stehen nicht mehr statisch auf der + Oberflaeche, sie HUEPFEN endlos (~6px, WATER_BOUNCE_V=-110); Steuerung + bleibt, voller manueller Sprung zum Rauskommen geht weiter (on_ground + bleibt beim Aufsetzen kurz true). Kein Idle im Wasser mehr (Tonys + Frosties-Fressen auf dem Wasser ist damit Geschichte ;-). +2. **Tod-Animation bei 0 Herzen**: ASSET-FUND -- Tony-Frame 38 ist Tony + OHNE HALSTUCH (erschrocken), Frame 40 das Halstuch einzeln; Smacks 26 + + 29 (Kappe). Sequenz: Figur fliegt (ohne Kollision) aus dem Bild, Tuch/ + Kappe flattert herab, nach 2.4s Respawn mit Leben-1 (0 Leben -> Karte). +3. **Tuer-Peek**: waehrend der 1s-Offen-Phase zeigt die Figur einen + Reinschau-Frame (Tony 34, Smacks 20 -- frontal; echte Ruecken-Frames + gibt es nicht in den BOBs). +4. **Duck-Kamera-Flackern**: 0.2s-Grace-Period -- kurze Taste/on_ground- + Blips (X11-Autorepeat sendet Release/Press-Paare) setzen den + Halte-Timer nicht mehr zurueck. diff --git a/game/level.py b/game/level.py index 701ee74..d8b913a 100644 --- a/game/level.py +++ b/game/level.py @@ -52,6 +52,10 @@ SPRITE_SINK = 4 # Fuesse sinken optisch in die Blaetter/Gras ein WATER_WADE_DEPTH = 14 # Nicht-Schwimmer sinken nur bis hierhin ein # (Original: Tony "springt auf der Wasser- # oberflaeche", taucht NICHT unter) +WATER_BOUNCE_V = -110.0 # Auto-Huepfer auf der Wasseroberflaeche + # (Endlosschleife, Stefans Beschreibung); + # manueller Sprung (voll) geht trotzdem +DEATH_ANIM_SECONDS = 2.4 # Tuch faellt, Figur fliegt aus dem Bild SOLID_BIT = 32 SLOPE_CT = 16 @@ -69,6 +73,8 @@ CHAR_DEFS = { 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 + death_frame=38, drop_frame=40, # erschrocken; fallendes HALSTUCH + peek_frame=34, # in die offene Tuer schauen item_icon='ITE0', head_index=0, ), 'SMACKS': dict( @@ -82,6 +88,8 @@ CHAR_DEFS = { swim_r=[37, 39, 41, 43], swim_l=[38, 40, 42, 44], idle_anim_r=[31, 33, 35], idle_anim_l=[32, 34, 36], spawn_frame=22, # Daumen-hoch-Geste + death_frame=26, drop_frame=29, # erschrocken; fallende KAPPE + peek_frame=20, item_icon='ITE1', head_index=1, ), 'COCO': dict( @@ -91,6 +99,7 @@ CHAR_DEFS = { idle_r=0, idle_l=1, jump_r=18, jump_l=19, duck_r=24, duck_l=25, swim_r=None, swim_l=None, idle_anim_r=None, idle_anim_l=None, spawn_frame=None, + death_frame=None, drop_frame=None, peek_frame=None, item_icon='ITE3', head_index=2, ), 'TOUCAN': dict( @@ -100,6 +109,7 @@ CHAR_DEFS = { idle_r=0, idle_l=1, jump_r=18, jump_l=19, duck_r=24, duck_l=25, swim_r=None, swim_l=None, idle_anim_r=None, idle_anim_l=None, spawn_frame=None, + death_frame=None, drop_frame=None, peek_frame=None, item_icon='ITE2', head_index=3, ), } @@ -245,6 +255,7 @@ class Player: self.anim_t = 0.0 self.idle_t = 0.0 self.spawn_t = 0.0 # Start-Sprite-Sequenz (Arm hoch/Daumen hoch) + self.force_frame = None # erzwungener Frame (Tuer-Peek, Tod) self._grounded_before = False self.set_character(char, pos) @@ -383,8 +394,13 @@ class Player: wade_max = wy + WATER_WADE_DEPTH if ny >= wade_max: ny = wade_max - self.vy = 0.0 + # Auto-Huepfer: die Oberflaeche "federt" -- kleiner Sprung in + # Endlosschleife (kein Idle im Wasser). on_ground=True laesst + # den naechsten Frame trotzdem einen VOLLEN manuellen Sprung + # zu (Rauskommen bleibt moeglich, Steuerung normal). + self.vy = WATER_BOUNCE_V self.on_ground = True + self.idle_t = 0.0 self.y = ny self._grounded_before = self.on_ground @@ -396,6 +412,8 @@ class Player: def current_frame_index(self): d = self.d right = self.facing > 0 + if self.force_frame is not None: + return self.force_frame if self.spawn_t > 0 and d.get('spawn_frame') is not None and self.on_ground: return d['spawn_frame'] if self.swimming and d['swim_r']: @@ -583,7 +601,9 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'): hearts, lives = 6, 3 # 6 Herzen (3x2) -- Original-Screenshot water_t = 0.0 duck_hold = 0.0 + down_gap = 0.0 # Grace gegen Taste/on_ground-Flackern look_down = 0.0 # Kamera-Offset beim gehaltenen Ducken + dying = None # dict(t, vy, drop_y, drop_vy) waehrend Tod-Anim while True: dt = min(clock.tick(60) / 1000.0, 1 / 20) @@ -606,13 +626,35 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'): door_open_t = 0.0 keys = pygame.key.get_pressed() - player.update(level, dt, keys) + if dying is None: + player.update(level, dt, keys) + else: + # Tod-Animation: Figur fliegt (ohne Kollision) aus dem Bild, + # das Halstuch (Tony) bzw. die Kappe (Smacks) flattert herab. + dying['t'] += dt + dying['vy'] += GRAVITY * 0.6 * dt + player.y += dying['vy'] * dt + dying['drop_y'] += dying['drop_vy'] * dt + dying['drop_vy'] = min(dying['drop_vy'] + 60 * dt, 40.0) + if dying['t'] >= DEATH_ANIM_SECONDS: + player.force_frame = None + dying = None + lives -= 1 + if lives <= 0: + return 'map' + hearts = 6 + player.x, player.y = spawn_px + player.vy = 0.0 + player.spawn_t = SPAWN_ANIM_SECONDS - # Tuer-Sequenz: offen zeigen, nach 1s die Aktion ausfuehren + # Tuer-Sequenz: offen zeigen (Figur schaut rein), nach 1s die Aktion if door_open_t is not None: door_open_t += dt + if player.d.get('peek_frame') is not None: + player.force_frame = player.d['peek_frame'] if door_open_t >= 1.0: door_open_t = None + player.force_frame = None choice = run_character_select(clock, assets, info['chars'], player.char) if choice == 'quit': @@ -621,12 +663,18 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'): player.set_character(choice) # Wasser-Schaden (alle 5s ein Herz), Schwimmer sind sicher - if level.in_water(player.y) and not player.swimming: + if dying is None and level.in_water(player.y) and not player.swimming: water_t += dt if water_t >= WATER_DAMAGE_EVERY: water_t = 0.0 hearts -= 1 - if hearts <= 0: + if hearts <= 0 and player.d.get('death_frame') is not None: + # Tod-Animation starten: erschrockener Frame, Flug nach + # oben aus dem Bild, Tuch/Kappe faellt herunter + player.force_frame = player.d['death_frame'] + dying = dict(t=0.0, vy=-260.0, + drop_y=player.y - 40.0, drop_vy=-30.0) + elif hearts <= 0: lives -= 1 hearts = 6 player.x, player.y = spawn_px @@ -640,10 +688,17 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'): # (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: + # Grace-Period: kurze Unterbrechungen (X11-Autorepeat sendet + # Release/Press-Paare; on_ground kann 1 Frame flackern) setzen den + # Halte-Timer NICHT zurueck -- Stefans "Kamera geht sporadisch + # wieder rauf, obwohl ich die Taste nicht losgelassen habe". + if keys[pygame.K_DOWN] and player.on_ground and dying is None: duck_hold += dt + down_gap = 0.0 else: - duck_hold = 0.0 + down_gap += dt + if down_gap > 0.2: + 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)) @@ -658,6 +713,10 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'): else: canvas.blit(door_sign_sprite, (door.x - cam_x, door.y - cam_y)) player.draw(canvas, cam_x, cam_y) + if dying is not None and player.d.get('drop_frame') is not None: + drop = player.surfs[player.d['drop_frame']] + canvas.blit(drop, (int(player.x - drop.get_width() / 2 - cam_x), + int(dying['drop_y'] - cam_y))) # Wasser: dunkles Overlay unterhalb der Wasserlinie (im Original ein # VGA-Palette-Effekt, nur bei machine=vgaonly sichtbar -- Stefans Fund) wy = info.get('water_y')