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:
@@ -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.
|
||||
|
||||
+66
-7
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user