Tuer-Oeffnungs-Sequenz, Slope-Landungs-Fix, Kamera-Logik, Sprite-Feinschliff

Stefans vierte Feedback-Runde:
- Sprites sinken 4px in den Boden ein (Fuesse standen sichtbar UEBER den
  Blaettern; Original laesst sie einsinken).
- Echte Steh-Frames: Tony 16/17, Smacks-Walkzyklus ohne die Steh-Frames
  12-15 ("letzter Lauf-Frame blieb beim Anhalten stehen").
- Tuer: DOORS[0] ist die OFFENE Tuer -> Ruhe = Tile-Tuer + Kopf-Schild
  (DOORS[5]); ALT oeffnet die Tuer sichtbar und nach 1s folgt die Aktion
  (Charakterwechsel-Menue) -- Muster fuer alle Tueren inkl. der
  Schluessel-Tueren (DOORS 1-4).
- Slope-Landung: Swept-Kollision ueber alle ueberstrichenen Zeilen +
  Herkunftsbedingung -- kein Durchfallen mehr bei schnellen Landungen
  (Stress-Test: 50 Drops ueber die Hangbreite, 0 Durchfaller).
- Kamera: Figur steht bei ~75% Bildhoehe; DUCKEN gehalten (>=1.2s) faehrt
  die Kamera sanft ~40px nach unten (Wasser unter Abgruenden sichtbar).

Verifiziert: Tunneling-Stresstest 0/50, Idle-Frames korrekt, Standbild
(Fuesse in Blaettern, Schild auf zu-Tuer, Kamerahoehe), --once exit 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 13:25:44 +02:00
co-authored by Claude Fable 5
parent bb9c23aae5
commit 0ad97e7180
2 changed files with 85 additions and 23 deletions
+22
View File
@@ -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).
+60 -20
View File
@@ -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,10 +597,22 @@ 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):
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':
@@ -591,9 +620,6 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'):
if choice and choice != player.char:
player.set_character(choice)
keys = pygame.key.get_pressed()
player.update(level, dt, keys)
# 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)