"""Level-Gameplay: Tile-Rendering, Kamera, Spielfiguren, Kollision, HUD. Basis-Referenzen: DOSBox-Aufnahmen (Xvfb+XTEST) + Stefans annotierte Live-Screenshots (25.07.2026). Wichtigste Fakten: - Level 1 = W1L1.MAP (Template-Matching). Viewport 320x176, HUD 320x24. - Kollision: MAP-ctype Bit 32 = solide, ctype 16 = 45-Grad-Schraege. - WASSER existiert (nur mit DOSBox machine=vgaonly sichtbar -- klassischer VGA-Palette-Effekt): dunkles Blau unterhalb der Wasserlinie; wer drin steht, verliert alle 5s ein Herz. SMACKS kann schwimmen (kein Schaden). - Pro Level ist hinterlegt, welche Charaktere waehlbar sind (Level 1: nur TONY und SMACKS) -- Quelle im Original vermutlich .ARE (Format noch nicht geknackt, siehe NOTES), hier v1 als LEVEL_INFO-Konstanten. - Tuer mit Kopf-Symbol: ALT im STEHEN davor oeffnet das Charakter- Auswahlmenue ("MIT WEM MOECHTEST DU SPIELEN?", WECH_1-4 = Koepfe farbig, WECH_5-8 = grau/nicht verfuegbar). ALT beim Laufen = RENNEN (nur Tony). Frame-Zuordnungen (nummerierte Sheets gesichtet): TONY.BOB (53): 0-15 Gehzyklus (gerade=RECHTS), 18/19 Sprung(gestreckt), 24/25 Ducken, 26-33 RENN-"Rad" (4 Phasen x 2), 44-49 Idle-Packung (3x2). SMACKS.BOB (51): 0-15 Gehzyklus, 18/19 Ducken(kauern), 24/25 Sprung, 31-36 Idle-Packung, 37-44 SCHWIMMEN (4 Phasen x 2), 45-47 Wasserblasen. """ import sys import time import pygame from . import intro from .formats import parse_map, parse_ico, parse_bob, get_world_palette, get_bob_palette VIEW_W, VIEW_H = 320, 176 HUD_H = 24 TILE = 16 WALK_SPEED = 110.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. JUMP_VELOCITY = -360.0 SWIM_GRAVITY = 240.0 # Smacks im Wasser: traege sinken SWIM_KICK = -140.0 # Schwimmstoss mit Space WALK_ANIM_FPS = 12.0 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 # Charakter-Definitionen. Richtungs-Paritaet: gerade Frames = rechts # (an Tony per Live-Test bestaetigt; bei Smacks analog angenommen). CHAR_DEFS = { 'TONY': dict( 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, 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( 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], run_r=None, run_l=None, 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( bob='COCO', can_run=False, 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=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( bob='TOUCAN', can_run=False, 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=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, ), } CHAR_ORDER = ['TONY', 'SMACKS', 'COCO', 'TOUCAN'] # Level-Metadaten (v1 als Konstanten -- die Quelle im Original ist vermutlich # das noch nicht geknackte .ARE-Format; dessen Header enthaelt Pixel- # Koordinatenpaare, siehe NOTES). LEVEL_INFO = { 'W1L1': dict( spawn=(17, 40), time=599, chars=['TONY', 'SMACKS'], # Level 1: nur die beiden (Stefan) water_y=42 * TILE, # Wasseroberflaeche unter der Bruecke door_px=(12 * TILE, 37 * TILE, 2 * TILE, 3 * TILE), # Haus-Tuer (x,y,w,h) ), } WATER_TINT = (10, 20, 120, 110) # dunkelblaues Overlay def _frames_to_surfaces(frames, palette): out = [] for fr in frames: surf = pygame.Surface((fr.width, fr.height), pygame.SRCALPHA) for y in range(fr.height): for x in range(fr.width): idx = fr.pixels[y * fr.width + x] if 0 <= idx <= 255: r, g, b = palette[idx] surf.set_at((x, y), (r, g, b, 255)) out.append(surf) return out class Level: '''MAP+ICO einmal komplett vorrendern; Kamera blittet den Ausschnitt.''' def __init__(self, assets, map_name): container = assets.container self.name = map_name self.info = LEVEL_INFO.get(map_name, dict(spawn=(2, 2), time=599, chars=['TONY'], water_y=None, door_px=None)) world = map_name[:2] self.width, self.height, self.cells = parse_map(container.entries[map_name + '.MAP']) palette = get_world_palette(container.entries, world) tiles = parse_ico(container.entries[world + '.ICO']) tile_surfs = [] for grid in tiles: s = pygame.Surface((TILE, TILE)) for y in range(TILE): for x in range(TILE): idx = grid[y * TILE + x] r, g, b = palette[idx & 0xFF] s.set_at((x, y), (r, g, b)) tile_surfs.append(s.convert()) self.surface = pygame.Surface((self.width * TILE, self.height * TILE)) self.surface.fill((0, 0, 0)) for i, (tile, _ct) in enumerate(self.cells): if tile < len(tile_surfs): self.surface.blit(tile_surfs[tile], ((i % self.width) * TILE, (i // self.width) * TILE)) self.surface = self.surface.convert() self.palette = palette def ctype(self, tx, ty): if tx < 0 or ty < 0 or tx >= self.width or ty >= self.height: return SOLID_BIT return self.cells[ty * self.width + tx][1] def solid(self, tx, ty): return bool(self.ctype(tx, ty) & SOLID_BIT) def slope_surface(self, px, ty): tx = int(px // TILE) if self.ctype(tx, ty) != SLOPE_CT: return None fx = px - tx * TILE rising_right = (self.ctype(tx + 1, ty - 1) == SLOPE_CT or self.solid(tx + 1, ty - 1) or self.solid(tx + 1, ty)) rising_left = (self.ctype(tx - 1, ty - 1) == SLOPE_CT or self.solid(tx - 1, ty - 1) or self.solid(tx - 1, ty)) if rising_right and not rising_left: return (ty + 1) * TILE - 1 - fx if rising_left and not rising_right: return ty * TILE + fx return (ty + 1) * TILE - 1 - fx def in_water(self, py): wy = self.info.get('water_y') return wy is not None and py >= wy class Player: '''Spielfigur (Tony/Smacks/...): Fusspunkt-basierte AABB-Physik.''' HITBOX_W = 20 HITBOX_H = 52 def __init__(self, assets, char, pos): self.assets = assets self.vx = 0.0 self.vy = 0.0 self.on_ground = False self.facing = 1 self.ducking = False self.running = False 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 self.d = CHAR_DEFS[char] frames = parse_bob(self.assets.container.entries[self.d['bob'] + '.BOB']) palette = get_bob_palette(self.assets.container.entries, self.d['bob']) 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), int(self.y - self.HITBOX_H), 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] move = 0 if not self.ducking: if keys[pygame.K_LEFT]: move -= 1 if keys[pygame.K_RIGHT]: move += 1 mods = pygame.key.get_mods() wants_run = bool(mods & (pygame.KMOD_ALT | pygame.KMOD_SHIFT)) self.running = wants_run and self.d['can_run'] and move != 0 and not self.swimming speed = RUN_SPEED if self.running else WALK_SPEED if self.swimming: speed *= 0.7 self.vx = move * speed if move: self.facing = move self.anim_t += dt * (RUN_ANIM_FPS / WALK_ANIM_FPS if self.running else 1.0) self.idle_t = 0.0 else: self.anim_t = 0.0 self.idle_t += dt jump_pressed = keys[pygame.K_SPACE] or keys[pygame.K_UP] if self.swimming: 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: self.vy = JUMP_VELOCITY self.on_ground = False self.idle_t = 0.0 self.vy = min(self.vy + GRAVITY * dt, 480.0) # X + Wandkollision nx = self.x + self.vx * dt r = self.rect() if self.vx > 0: tx = int((nx + self.HITBOX_W / 2) // TILE) if any(level.solid(tx, ty) for ty in range(r.top // TILE, (r.bottom - 1) // TILE + 1)): nx = tx * TILE - self.HITBOX_W / 2 - 0.01 elif self.vx < 0: tx = int((nx - self.HITBOX_W / 2) // TILE) if any(level.solid(tx, ty) for ty in range(r.top // TILE, (r.bottom - 1) // TILE + 1)): nx = (tx + 1) * TILE + self.HITBOX_W / 2 + 0.01 self.x = nx # Y + Boden/Decke ny = self.y + self.vy * dt left_tx = int((self.x - self.HITBOX_W / 2) // TILE) right_tx = int((self.x + self.HITBOX_W / 2 - 1) // TILE) if self.vy >= 0: ty = int(ny // TILE) if any(level.solid(tx, ty) for tx in range(left_tx, right_tx + 1)): ny = ty * TILE - 0.01 self.vy = 0.0 self.on_ground = True else: self.on_ground = False else: ty = int((ny - self.HITBOX_H) // TILE) if any(level.solid(tx, ty) for tx in range(left_tx, right_tx + 1)): ny = (ty + 1) * TILE + self.HITBOX_H + 0.01 self.vy = 0.0 # 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) 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 - 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] return i 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] if self.ducking: return self._pick('duck_r', 'duck_l') if not self.on_ground: return self._pick('jump_r', 'jump_l') if self.vx: if self.running and d['run_r']: seq = d['run_r'] if right else d['run_l'] fps = RUN_ANIM_FPS else: seq = d['walk_r'] if right else d['walk_l'] fps = WALK_ANIM_FPS return seq[int(self.anim_t * fps) % len(seq)] if self.idle_t >= IDLE_ANIM_AFTER and d['idle_anim_r']: seq = d['idle_anim_r'] if right else d['idle_anim_l'] return seq[int((self.idle_t - IDLE_ANIM_AFTER) * IDLE_ANIM_FPS) % len(seq)] return self._pick('idle_r', 'idle_l') def draw(self, screen, cam_x, cam_y): 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))) class Hud: '''HUD-Balken nach Stefans Referenz-Screenshot: Kellogg's-Leiste, Keks-Box MIT Item-Icon (ITE0-3 je Charakter), Item-Box, Score-Box, Herz-Box (2x2-GRID), Charakter-Kopf (WECH, zeigt IMMER die aktuell gewaehlte Figur) + Leben, Uhr.''' # 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): from .menu import BitmapFont, _mapping_from_rows container = assets.container frames = parse_bob(container.entries['GAMEBAR.BOB']) palette = get_bob_palette(container.entries, 'GAMEBAR') self.icons = _frames_to_surfaces(frames, palette) self.heart_full = self.icons[10] self.heart_empty = self.icons[11] self.font = BitmapFont(assets.pcc_surface('FON_1B'), 9, 7, _mapping_from_rows(['1234567890;:!"%/()=?,.-\x7f#']), spacing=8) logo = assets.pcc_surface('KELLOGGS').subsurface((36, 26, 252, 66)) self.kellogg = pygame.transform.smoothscale(logo, (50, 13)) # Item-Icons (Cerealien) + Charakter-Koepfe klein self.item_icons = {n: intro.make_border_bg_transparent(assets.pcc_surface(n)) for n in ('ITE0', 'ITE1', 'ITE2', 'ITE3')} self.heads_small = [] for i in range(1, 5): head = intro.make_border_bg_transparent(assets.pcc_surface(f'WECH_{i}')) self.heads_small.append(pygame.transform.smoothscale(head, (17, 16))) def _box(self, canvas, 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)) 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, (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, 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, 284, box_y, 34, box_h) self._text(canvas, 287, text_y, f'{m}:{s:02d}') def run_character_select(clock, assets, available, current): '''Tuer-Menue "MIT WEM MOECHTEST DU SPIELEN?" -- Koepfe WECH_1-4 (farbig) bzw. WECH_5-8 (grau = in diesem Level nicht verfuegbar). Links/Rechts + Return; ESC bricht ab. Rueckgabe: Charaktername, None (abgebrochen) oder 'quit' (Fenster-X).''' from .menu import MenuAssets, BLINK_SECONDS m = MenuAssets(assets) heads_color = [intro.make_border_bg_transparent(assets.pcc_surface(f'WECH_{i}')) for i in range(1, 5)] heads_grey = [intro.make_border_bg_transparent(assets.pcc_surface(f'WECH_{i}')) for i in range(5, 9)] names = CHAR_ORDER sel = [names.index(current) if current in names else 0] start = time.monotonic() while True: for event in pygame.event.get(): if event.type == pygame.QUIT: return 'quit' if event.type == pygame.KEYDOWN: if event.key == pygame.K_RETURN and (event.mod & pygame.KMOD_ALT): intro.toggle_fullscreen() continue if event.key == pygame.K_ESCAPE: return None if event.key in (pygame.K_LEFT, pygame.K_UP): sel[0] = (sel[0] - 1) % 4 elif event.key in (pygame.K_RIGHT, pygame.K_DOWN): sel[0] = (sel[0] + 1) % 4 elif event.key in (pygame.K_RETURN, pygame.K_SPACE): if names[sel[0]] in available: return names[sel[0]] t = time.monotonic() - start blink_on = int(t / BLINK_SECONDS) % 2 == 0 canvas = m.menu_bg.copy() title = m.font_small.render('MIT WEM M\xd6CHTEST DU SPIELEN?', outline=(0, 0, 0)) canvas.blit(title, ((320 - title.get_width()) // 2, 48)) for i, name in enumerate(names): x = 52 + i * 60 head = heads_color[i] if name in available else heads_grey[i] if i == sel[0] and not blink_on: pass # Auswahl blinkt else: canvas.blit(head, (x, 74)) label = m.font_small.render(name, outline=(0, 0, 0)) canvas.blit(label, (x + 24 - label.get_width() // 2, 124)) intro._blit_centered(intro.build_letterboxed(canvas)) pygame.display.flip() clock.tick(30) def run_level(clock, assets, audio_paths, map_name='W1L1'): '''Level spielen. ESC -> 'map', Fenster-X -> 'quit'.''' level = Level(assets, map_name) info = level.info spawn_px = (info['spawn'][0] * TILE + 8, info['spawn'][1] * TILE - 0.01) player = Player(assets, info['chars'][0], spawn_px) hud = Hud(assets) door = pygame.Rect(info['door_px']) if info.get('door_px') else None door_head = pygame.transform.smoothscale( intro.make_border_bg_transparent(assets.pcc_surface('WECH_1')), (16, 15)) if pygame.mixer.get_init(): path = audio_paths.get(('ONGAME2', None)) if path: try: pygame.mixer.music.load(path) pygame.mixer.music.play(-1) except Exception as exc: print(f'[level] Levelmusik konnte nicht geladen werden: {exc}', file=sys.stderr) canvas = pygame.Surface((320, 200)) start = time.monotonic() map_w_px = level.width * TILE map_h_px = level.height * TILE hearts, lives = 4, 3 water_t = 0.0 while True: dt = min(clock.tick(60) / 1000.0, 1 / 20) for event in pygame.event.get(): if event.type == pygame.QUIT: return 'quit' if event.type == pygame.KEYDOWN: if event.key == pygame.K_RETURN and (event.mod & pygame.KMOD_ALT): intro.toggle_fullscreen() continue if event.key == pygame.K_ESCAPE: return 'map' # Tuer benutzen: ALT im STEHEN vor der Tuer (nicht beim # Rennen -- Stefans Regel) -> 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): 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) 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 if water_t >= WATER_DAMAGE_EVERY: water_t = 0.0 hearts -= 1 if hearts <= 0: lives -= 1 hearts = 4 player.x, player.y = spawn_px player.vy = 0.0 if lives <= 0: return 'map' else: water_t = 0.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)) canvas.blit(level.surface, (0, 0), (cam_x, cam_y, VIEW_W, VIEW_H)) # Tuer-Symbol (Kopf ueber dem Tuerfenster -- Charakterwechsel-Hinweis) if door: canvas.blit(door_head, (door.x + door.w // 2 - 8 - 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) wy = info.get('water_y') if wy is not None and wy - cam_y < VIEW_H: top = max(0, wy - cam_y) overlay = pygame.Surface((VIEW_W, VIEW_H - top), pygame.SRCALPHA) overlay.fill(WATER_TINT) canvas.blit(overlay, (0, top)) time_left = info['time'] - (time.monotonic() - start) hud.draw(canvas, VIEW_H, player.d, cookies=0, score=0, hearts=hearts, lives=lives, time_left=time_left) intro._blit_centered(intro.build_letterboxed(canvas)) pygame.display.flip()