Wasser-Bounce, Tod-Animation (Tuch/Kappe), Tuer-Peek, Duck-Kamera-Grace

Stefans fuenfte Feedback-Runde:
- Nicht-Schwimmer huepfen endlos auf der Wasseroberflaeche (~6px Auto-
  Bounce) statt statisch zu stehen; Steuerung und voller Ausstiegs-Sprung
  bleiben moeglich. Kein Idle im Wasser.
- Tod bei 0 Herzen: Tony verliert sein HALSTUCH (Frame 38 = Tony ohne
  Tuch, Frame 40 = Tuch einzeln -- die Assets waren dafuer gebaut!),
  fliegt aus dem Bild, Tuch flattert herab; Smacks analog mit Kappe
  (26/29). Danach Respawn mit Leben-1, bei 0 Leben zurueck zur Karte.
- Tuer-Peek: Figur zeigt waehrend der Offen-Sekunde einen Reinschau-Frame.
- Duck-Kamera: 0.2s-Grace gegen X11-Autorepeat-/on_ground-Flackern
  ("Kamera geht sporadisch wieder rauf").

Verifiziert: Bounce-Amplitude ~6px endlos, voller Sprung aus dem Wasser,
Tod-Sprites gesichtet (Tuch/Kappe), --once exit 0, E2E rc=0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 13:38:08 +02:00
co-authored by Claude Fable 5
parent 0ad97e7180
commit 10c5a5b69c
2 changed files with 84 additions and 7 deletions
+18
View File
@@ -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.
+64 -5
View File
@@ -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()
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,9 +688,16 @@ 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:
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)
@@ -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')