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:
@@ -1567,3 +1567,25 @@ HUD-Render deckungsgleich mit Stefans Original-Screenshot. --once/E2E ok.
|
|||||||
mit Stefans Original-Screenshot (Kopf-Schild + Punkte).
|
mit Stefans Original-Screenshot (Kopf-Schild + Punkte).
|
||||||
- Nebenfund: W1ANI/W2ANI/W3ANI.ICO = Tile-ANIMATIONSPHASEN (broeckelnde
|
- Nebenfund: W1ANI/W2ANI/W3ANI.ICO = Tile-ANIMATIONSPHASEN (broeckelnde
|
||||||
Bloecke etc.) -- fuer spaeter (animierte Tiles).
|
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).
|
||||||
|
|||||||
+63
-23
@@ -47,6 +47,8 @@ IDLE_ANIM_AFTER = 5.0 # Sekunden Stillstand bis zur Idle-Animation
|
|||||||
IDLE_ANIM_FPS = 2.5
|
IDLE_ANIM_FPS = 2.5
|
||||||
WATER_DAMAGE_EVERY = 5.0 # im Wasser: alle 5s ein Herz (Stefans Angabe)
|
WATER_DAMAGE_EVERY = 5.0 # im Wasser: alle 5s ein Herz (Stefans Angabe)
|
||||||
SPAWN_ANIM_SECONDS = 1.1 # Start-Sprite beim Levelstart/Charakterwechsel
|
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
|
WATER_WADE_DEPTH = 14 # Nicht-Schwimmer sinken nur bis hierhin ein
|
||||||
# (Original: Tony "springt auf der Wasser-
|
# (Original: Tony "springt auf der Wasser-
|
||||||
# oberflaeche", taucht NICHT unter)
|
# oberflaeche", taucht NICHT unter)
|
||||||
@@ -61,7 +63,9 @@ CHAR_DEFS = {
|
|||||||
bob='TONY', can_run=True, can_swim=False,
|
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],
|
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],
|
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,
|
swim_r=None, swim_l=None,
|
||||||
idle_anim_r=[44, 46, 48], idle_anim_l=[45, 47, 49],
|
idle_anim_r=[44, 46, 48], idle_anim_l=[45, 47, 49],
|
||||||
spawn_frame=22, # Arm nach oben gestreckt
|
spawn_frame=22, # Arm nach oben gestreckt
|
||||||
@@ -69,7 +73,8 @@ CHAR_DEFS = {
|
|||||||
),
|
),
|
||||||
'SMACKS': dict(
|
'SMACKS': dict(
|
||||||
bob='SMACKS', can_run=False, can_swim=True,
|
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,
|
run_r=None, run_l=None,
|
||||||
# Idle 12/13: Smacks' echte Steh-Frames (0/1 sind Schrittphasen --
|
# Idle 12/13: Smacks' echte Steh-Frames (0/1 sind Schrittphasen --
|
||||||
# Stefans Bug "beim Anhalten wird noch das Laufen-Sprite angezeigt")
|
# 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.
|
# Boden ist (bergauf laufen), darf mit Toleranz nach OBEN aufsteigen.
|
||||||
if self.vy >= 0:
|
if self.vy >= 0:
|
||||||
was_grounded = self.on_ground or self._grounded_before
|
was_grounded = self.on_ground or self._grounded_before
|
||||||
|
old_ty = int(self.y // TILE)
|
||||||
foot_ty = int(ny // 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
|
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)
|
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
|
ny = surf - 0.01
|
||||||
self.vy = 0.0
|
self.vy = 0.0
|
||||||
self.on_ground = True
|
self.on_ground = True
|
||||||
@@ -411,7 +422,7 @@ class Player:
|
|||||||
i = self.current_frame_index()
|
i = self.current_frame_index()
|
||||||
surf = self.surfs[i] if i < len(self.surfs) else self.surfs[0]
|
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),
|
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:
|
class Hud:
|
||||||
@@ -545,12 +556,16 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'):
|
|||||||
player = Player(assets, info['chars'][0], spawn_px)
|
player = Player(assets, info['chars'][0], spawn_px)
|
||||||
hud = Hud(assets)
|
hud = Hud(assets)
|
||||||
door = pygame.Rect(info['door_px']) if info.get('door_px') else None
|
door = pygame.Rect(info['door_px']) if info.get('door_px') else None
|
||||||
# Echte Tuergrafik: DOORS.BOB Frame 0 = Tuer MIT Kopf-Schild (32x48,
|
# Tuergrafiken (DOORS.BOB): Frame 0 = OFFENE Tuer (Stefans Korrektur),
|
||||||
# exakt Tuergroesse; Frames 1-4 sind die Schluessel-Tueren mit farbigen
|
# Frame 5 = Kopf-Schild einzeln. Ruhezustand = geschlossene Tile-Tuer der
|
||||||
# Knaeufen -- fuer spaeter).
|
# Map + Schild; bei ALT oeffnet sich die Tuer (Frame 0) und nach ~1s
|
||||||
door_frames = parse_bob(assets.container.entries['DOORS.BOB'])
|
# passiert die Tuer-Aktion (hier: Charakterwechsel-Menue). Frames 1-4
|
||||||
door_pal = get_bob_palette(assets.container.entries, 'DOORS')
|
# sind die Schluessel-Tueren mit farbigen Knaeufen (spaeter).
|
||||||
door_sprite = _frames_to_surfaces(door_frames, door_pal)[0]
|
_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():
|
if pygame.mixer.get_init():
|
||||||
path = audio_paths.get(('ONGAME2', None))
|
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
|
map_h_px = level.height * TILE
|
||||||
hearts, lives = 6, 3 # 6 Herzen (3x2) -- Original-Screenshot
|
hearts, lives = 6, 3 # 6 Herzen (3x2) -- Original-Screenshot
|
||||||
water_t = 0.0
|
water_t = 0.0
|
||||||
|
duck_hold = 0.0
|
||||||
|
look_down = 0.0 # Kamera-Offset beim gehaltenen Ducken
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
dt = min(clock.tick(60) / 1000.0, 1 / 20)
|
dt = min(clock.tick(60) / 1000.0, 1 / 20)
|
||||||
@@ -580,20 +597,29 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'):
|
|||||||
if event.key == pygame.K_ESCAPE:
|
if event.key == pygame.K_ESCAPE:
|
||||||
return 'map'
|
return 'map'
|
||||||
# Tuer benutzen: ALT im STEHEN vor der Tuer (nicht beim
|
# 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 event.key in (pygame.K_LALT, pygame.K_RALT):
|
||||||
if (door and door.collidepoint(player.x, player.y - 1)
|
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
|
||||||
choice = run_character_select(clock, assets,
|
and door_open_t is None):
|
||||||
info['chars'], player.char)
|
door_open_t = 0.0
|
||||||
if choice == 'quit':
|
|
||||||
return 'quit'
|
|
||||||
if choice and choice != player.char:
|
|
||||||
player.set_character(choice)
|
|
||||||
|
|
||||||
keys = pygame.key.get_pressed()
|
keys = pygame.key.get_pressed()
|
||||||
player.update(level, dt, keys)
|
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':
|
||||||
|
return 'quit'
|
||||||
|
if choice and choice != player.char:
|
||||||
|
player.set_character(choice)
|
||||||
|
|
||||||
# Wasser-Schaden (alle 5s ein Herz), Schwimmer sind sicher
|
# Wasser-Schaden (alle 5s ein Herz), Schwimmer sind sicher
|
||||||
if level.in_water(player.y) and not player.swimming:
|
if level.in_water(player.y) and not player.swimming:
|
||||||
water_t += dt
|
water_t += dt
|
||||||
@@ -610,13 +636,27 @@ def run_level(clock, assets, audio_paths, map_name='W1L1'):
|
|||||||
else:
|
else:
|
||||||
water_t = 0.0
|
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_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))
|
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:
|
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)
|
player.draw(canvas, cam_x, cam_y)
|
||||||
# Wasser: dunkles Overlay unterhalb der Wasserlinie (im Original ein
|
# Wasser: dunkles Overlay unterhalb der Wasserlinie (im Original ein
|
||||||
# VGA-Palette-Effekt, nur bei machine=vgaonly sichtbar -- Stefans Fund)
|
# VGA-Palette-Effekt, nur bei machine=vgaonly sichtbar -- Stefans Fund)
|
||||||
|
|||||||
Reference in New Issue
Block a user