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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
+91
-35
@@ -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,8 +259,17 @@ 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)
|
||||
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:
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user