From 7eb7f6f3b70f01e9244d4cbee7b8fd0104400dec Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 25 Jul 2026 11:19:44 +0200 Subject: [PATCH] Wasser-Mechanik nach Original, Slope-Schweben-Fix, Spawn-Pose, HUD v3 Stefans zweite Feedback-Runde: - Nicht-Schwimmer (Tony & Co.) tauchen nicht mehr unter: die Wasser- oberflaeche traegt (14px Eintauchtiefe, springen von dort moeglich, Herzverlust laeuft) -- wie im Original-Screenshot. Schwimmer (Smacks) loesen nahe der Oberflaeche mit der Sprungtaste einen echten Sprung aus und kommen wieder aus dem Wasser raus (vorher unmoeglich). - Slope-Schweben gefixt: Luft-Snap nur bei echter Durchdringung; Bergauf- Toleranz nur fuer bereits Gelandete. - Start-Sprite-Sequenz beim Levelstart/Charakterwechsel (Tony Arm hoch, Smacks Daumen hoch, Frame 22, ~1.1s, durch Eingabe abbrechbar). - Rennsprungweite: RUN_SPEED 185 -> 200 ("bis hinter die Blume"). - HUD v3 nach Original-Referenz: helle Creme-Rahmen, hoehere Boxen, Cerealien-Icon in voller Groesse, leere Herz-Plaetze dunkel. Verifiziert: Physik-Sims (Wade+Absprung, Smacks-Ausstiegssprung, Decken- Kollision unter der Bruecke korrekt) + E2E rc=0 + --once exit 0. Co-Authored-By: Claude Fable 5 --- NOTES.md | 25 ++++++++++ game/level.py | 128 ++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 117 insertions(+), 36 deletions(-) diff --git a/NOTES.md b/NOTES.md index 17df0ec..ec1d236 100644 --- a/NOTES.md +++ b/NOTES.md @@ -1480,3 +1480,28 @@ ducken als Smacks, ESC/QUIT rc=0. --once Regression exit 0. Items einsammeln (FROS_W1-Rotationssprites + ctype 4), Smacks-Blickrichtungs- Paritaet pruefen, Coco/Toucan-Frames zuordnen (aktuell Tony-Layout angenommen), Kamera-Hoehe feinjustieren, Gegner, Tuer-/Schluessel-Logik. + +## 25.07.2026 (4) -- zweite Feedback-Runde: Wasser-Mechanik, Slopes, Spawn-Pose, HUD v3 + +Stefans Live-Test (5 Screenshots), alles umgesetzt: +1. **Slope-Schweben gefixt**: in der Luft snappt der Fuss nur noch bei + echter Durchdringung der Diagonale (tol=0); nur wer schon am Boden ist, + steigt mit 8px-Toleranz bergauf. (Nebenfund: der Grashang hat ZWEI + Slope-Tiles uebereinander -- gelaufen wird auf der oberen Diagonale.) +2. **Wasser-Mechanik wie im Original**: Nicht-Schwimmer tauchen NICHT unter + -- die Oberflaeche traegt (Fuss sinkt nur 14px ein, on_ground, man kann + von ihr abspringen), Herzverlust laeuft weiter. Schwimmer (Smacks): nahe + der Oberflaeche loest die Sprungtaste einen ECHTEN Sprung aus (vorher kam + man nicht mehr aus dem Wasser raus, nur schwacher Schwimmstoss). + Smacks-Blickrichtung: passt (Stefan bestaetigt). +3. **Start-Sprite-Sequenz** beim Levelstart/Charakterwechsel: Tony streckt + den Arm hoch (Frame 22), Smacks Daumen hoch (Frame 22); ~1.1s, jede + Eingabe bricht ab. +4. **Rennsprung weiter**: RUN_SPEED 185 -> 200 ("bis hinter die Blume"). +5. **HUD v3** nach Original-Screenshot: helle Creme-Rahmen statt Gold, + groessere Boxen (18px), Icon in voller Groesse, leere Herz-Plaetze + bleiben dunkel. + +Verifiziert per Physik-Simulationen (Wade-Ebene 686=water_y+14 + Sprung +vy=-345 von dort; Smacks-Ausstiegssprung vy=-360 an offener Wasserstelle; +Smacks unter der Bruecke stoesst korrekt an die Decke) + E2E rc=0 + --once ok. diff --git a/game/level.py b/game/level.py index c67fa76..9f832f1 100644 --- a/game/level.py +++ b/game/level.py @@ -33,7 +33,8 @@ HUD_H = 24 TILE = 16 WALK_SPEED = 110.0 -RUN_SPEED = 185.0 +RUN_SPEED = 200.0 # Stefans Nachmessung: Rennsprung muss bis + # HINTER die Blume tragen -> etwas schneller GRAVITY = 900.0 # Sprunghoehe ~72px (Stefans Referenz: Tonys Kopf erreicht gerade den Ast # ueber dem Spawn): v = sqrt(2*g*h) ~= 360. @@ -45,6 +46,10 @@ RUN_ANIM_FPS = 14.0 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 +WATER_WADE_DEPTH = 14 # Nicht-Schwimmer sinken nur bis hierhin ein + # (Original: Tony "springt auf der Wasser- + # oberflaeche", taucht NICHT unter) SOLID_BIT = 32 SLOPE_CT = 16 @@ -59,6 +64,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=[44, 46, 48], idle_anim_l=[45, 47, 49], + spawn_frame=22, # Arm nach oben gestreckt item_icon='ITE0', head_index=0, ), 'SMACKS': dict( @@ -68,6 +74,7 @@ CHAR_DEFS = { idle_r=0, idle_l=1, jump_r=24, jump_l=25, duck_r=18, duck_l=19, 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 item_icon='ITE1', head_index=1, ), 'COCO': dict( @@ -76,6 +83,7 @@ CHAR_DEFS = { run_r=None, run_l=None, 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, item_icon='ITE3', head_index=2, ), 'TOUCAN': dict( @@ -84,6 +92,7 @@ CHAR_DEFS = { run_r=None, run_l=None, 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, item_icon='ITE2', head_index=3, ), } @@ -187,7 +196,6 @@ class Player: def __init__(self, assets, char, pos): self.assets = assets - self.set_character(char, pos) self.vx = 0.0 self.vy = 0.0 self.on_ground = False @@ -197,6 +205,9 @@ class Player: self.swimming = False self.anim_t = 0.0 self.idle_t = 0.0 + self.spawn_t = 0.0 # Start-Sprite-Sequenz (Arm hoch/Daumen hoch) + self._grounded_before = False + self.set_character(char, pos) def set_character(self, char, pos=None): self.char = char @@ -206,6 +217,9 @@ class Player: self.surfs = _frames_to_surfaces(frames, palette) if pos is not None: self.x, self.y = float(pos[0]), float(pos[1]) + # Start-Sprite-Sequenz beim Levelstart/Charakterwechsel (Stefans + # Beobachtung: Tony streckt die Hand nach oben, Smacks Daumen hoch) + self.spawn_t = SPAWN_ANIM_SECONDS def rect(self): return pygame.Rect(int(self.x - self.HITBOX_W / 2), @@ -213,6 +227,12 @@ class Player: self.HITBOX_W, self.HITBOX_H) def update(self, level, dt, keys): + if self.spawn_t > 0: + self.spawn_t -= dt + # Eingabe bricht die Start-Pose sofort ab + if any(keys[k] for k in (pygame.K_LEFT, pygame.K_RIGHT, + pygame.K_UP, pygame.K_DOWN, pygame.K_SPACE)): + self.spawn_t = 0.0 in_water = level.in_water(self.y) self.swimming = in_water and self.d['can_swim'] self.ducking = self.on_ground and not self.swimming and keys[pygame.K_DOWN] @@ -239,9 +259,18 @@ class Player: jump_pressed = keys[pygame.K_SPACE] or keys[pygame.K_UP] if self.swimming: - if jump_pressed: + wy = level.info.get('water_y') + near_surface = wy is not None and (self.y - wy) < 24 + if jump_pressed and near_surface: + # Stefans Bug "kommt nicht mehr aus dem Wasser raus": nahe der + # Oberflaeche wird ein ECHTER Sprung ausgeloest (traegt auf die + # Plattform), nicht nur ein Schwimmstoss. + self.vy = JUMP_VELOCITY + self.swimming = False + elif jump_pressed: self.vy = min(self.vy, SWIM_KICK) - self.vy = min(self.vy + SWIM_GRAVITY * dt, 120.0) + if self.swimming: + self.vy = min(self.vy + SWIM_GRAVITY * dt, 120.0) else: if self.on_ground and not self.ducking and jump_pressed: self.vy = JUMP_VELOCITY @@ -280,17 +309,36 @@ class Player: ny = (ty + 1) * TILE + self.HITBOX_H + 0.01 self.vy = 0.0 - # Schraegen (ctype 16) + # Schraegen (ctype 16). WICHTIG (Stefans Bug "Figuren stehen auf + # Schraegen in der Luft"): in der LUFT nur snappen, wenn der Fuss die + # Diagonale wirklich durchdrungen hat (ny >= surf). Nur wer schon am + # Boden ist (bergauf laufen), darf mit Toleranz nach OBEN aufsteigen. if self.vy >= 0: + was_grounded = self.on_ground or self._grounded_before foot_ty = int(ny // TILE) - for ty2 in (foot_ty - 1, foot_ty, foot_ty + 1): + tys = (foot_ty - 1, foot_ty, foot_ty + 1) if was_grounded else (foot_ty, foot_ty + 1) + tol = 8 if was_grounded else 0 + for ty2 in tys: surf = level.slope_surface(self.x, ty2) - if surf is not None and ny >= surf - 10: + if surf is not None and ny >= surf - tol: ny = surf - 0.01 self.vy = 0.0 self.on_ground = True break + # Nicht-Schwimmer tauchen NICHT unter (Original, Stefans Screenshot: + # Tony "springt quasi auf der Wasseroberflaeche") -- die Oberflaeche + # traegt wie weicher Boden, die Beine sinken nur WADE_DEPTH tief ein. + # Springen von dort geht (on_ground), Herzverlust laeuft weiter. + wy = level.info.get('water_y') + if wy is not None and not self.d['can_swim']: + wade_max = wy + WATER_WADE_DEPTH + if ny >= wade_max: + ny = wade_max + self.vy = 0.0 + self.on_ground = True + self.y = ny + self._grounded_before = self.on_ground def _pick(self, key_r, key_l): i = self.d[key_r] if self.facing > 0 else self.d[key_l] @@ -299,6 +347,8 @@ class Player: def current_frame_index(self): d = self.d right = self.facing > 0 + 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']: seq = d['swim_r'] if right else d['swim_l'] return seq[int(self.anim_t * 6) % len(seq)] if self.vx else seq[0] @@ -332,9 +382,12 @@ class Hud: Herz-Box (2x2-GRID), Charakter-Kopf (WECH, zeigt IMMER die aktuell gewaehlte Figur) + Leben, Uhr.''' - GOLD = (222, 186, 24) - RED = (150, 14, 14) - DARK = (84, 6, 6) + # Farben nach Stefans Original-HUD-Screenshot: HELLE (creme-weisse) + # Boxrahmen auf dunkelrotem Grund -- nicht das satte Gold der ersten + # Version. + FRAME = (244, 232, 200) + RED = (146, 10, 10) + DARK = (80, 4, 4) WHITE = (248, 248, 248) def __init__(self, assets): @@ -359,43 +412,46 @@ class Hud: self.heads_small.append(pygame.transform.smoothscale(head, (17, 16))) def _box(self, canvas, x, y, w, h): - pygame.draw.rect(canvas, self.GOLD, (x - 1, y - 1, w + 2, h + 2)) - pygame.draw.rect(canvas, self.RED, (x, y, w, h)) + pygame.draw.rect(canvas, self.FRAME, (x - 1, y - 1, w + 2, h + 2), + border_radius=3) + pygame.draw.rect(canvas, self.RED, (x, y, w, h), border_radius=2) def _text(self, canvas, x, y, text): canvas.blit(self.font.render(text, outline=(0, 0, 0)), (x, y)) def draw(self, canvas, y0, char_def, cookies, score, hearts, lives, time_left): + '''Layout nach Stefans Original-HUD-Screenshot (Positionen /2 aufs + 320er-Raster gemessen): [Kellogg's][Icon+Keks][Item][Score][Herz 2x2] + [Kopf+Leben][Uhr], Boxen fast HUD-hoch mit hellem Rahmen.''' pygame.draw.rect(canvas, self.DARK, (0, y0, 320, HUD_H)) - # Kellogg's-Leiste - pygame.draw.rect(canvas, self.WHITE, (2, y0 + 3, 56, HUD_H - 6), border_radius=6) - canvas.blit(self.kellogg, (5, y0 + 5)) - # Keks-Box mit Item-Icon des aktiven Charakters - self._box(canvas, 62, y0 + 4, 46, 16) + box_y, box_h = y0 + 3, 18 + text_y = y0 + 9 + # Kellogg's-Leiste (weiss, abgerundet) + pygame.draw.rect(canvas, self.WHITE, (2, y0 + 2, 60, HUD_H - 4), border_radius=7) + canvas.blit(self.kellogg, (7, y0 + 6)) + # Keks-Box: Cerealien-Icon (16x16, volle Groesse) + Zaehler + self._box(canvas, 66, box_y, 48, box_h) icon = self.item_icons.get(char_def['item_icon']) if icon: - canvas.blit(icon, (64, y0 + 4)) - self._text(canvas, 82, y0 + 9, f'{cookies:03d}') - # Item-Box - self._box(canvas, 114, y0 + 4, 36, 16) + canvas.blit(icon, (68, y0 + 4)) + self._text(canvas, 87, text_y, f'{cookies:03d}') + # Item-Box (leer) + self._box(canvas, 118, box_y, 38, box_h) # Score - self._box(canvas, 156, y0 + 4, 54, 16) - self._text(canvas, 159, y0 + 9, f'{score:06d}') - # Herz-Box: 2x2-Grid (Referenz-Screenshot) - self._box(canvas, 216, y0 + 2, 26, 20) - for i in range(4): - icon = self.heart_full if i < hearts else self.heart_empty - hx = 219 + (i % 2) * 10 - hy = y0 + 3 + (i // 2) * 9 - canvas.blit(icon, (hx, hy)) - # Charakter-Kopf (immer die gewaehlte Figur) + Leben - self._box(canvas, 248, y0 + 2, 36, 20) - canvas.blit(self.heads_small[char_def['head_index']], (250, y0 + 4)) - self._text(canvas, 272, y0 + 9, str(lives)) + self._box(canvas, 160, box_y, 56, box_h) + self._text(canvas, 164, text_y, f'{score:06d}') + # Herz-Box: 2x2-Grid; leere Plaetze bleiben dunkel (wie im Original) + self._box(canvas, 220, box_y, 24, box_h) + for i in range(min(hearts, 4)): + canvas.blit(self.heart_full, (222 + (i % 2) * 10, y0 + 4 + (i // 2) * 9)) + # Charakter-Kopf (zeigt IMMER die gewaehlte Figur) + Leben + self._box(canvas, 248, box_y, 32, box_h) + canvas.blit(self.heads_small[char_def['head_index']], (249, y0 + 4)) + self._text(canvas, 269, text_y, str(lives)) # Uhr m, s = divmod(max(0, int(time_left)), 60) - self._box(canvas, 288, y0 + 4, 30, 16) - self._text(canvas, 290, y0 + 9, f'{m}:{s:02d}') + self._box(canvas, 284, box_y, 34, box_h) + self._text(canvas, 287, text_y, f'{m}:{s:02d}') def run_character_select(clock, assets, available, current):