From 973e290cf6fe99c9d5ebe71254056b19515b1fd5 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 25 Jul 2026 01:17:44 +0200 Subject: [PATCH] LEVEL 1 SPIELBAR: game/level.py -- Tiles, Kamera, Tony-Physik, HUD, Musik Weltkarte: SPACE startet Level 1 (Original-Verhalten per DOSBox-Referenz: Return tut dort nichts, kein sichtbarer Level-Cursor am Spielstart). Level 1 = W1L1.MAP -- per Template-Matching des DOSBox-Gameplay-Screenshots gegen die gerenderten Maps identifiziert (W1L0 ist NICHT Level 1). game/level.py: - Level: MAP+ICO+Weltpalette laden, komplette Map einmalig vorrendern, Kamera blittet 320x176-Ausschnitt (HUD 320x24 darunter, wie Original). - Kollision: MAP-ctype Bit 32 = solide (40=Boden, 32=Fuellung; empirisch an der Spawn-Plattform verifiziert). - Tony: TONY.BOB (53 Frames; 8-Phasen-Gehzyklus, gerade=links/ungerade= rechts), AABB-Physik gegen das Tile-Grid, laufen/springen/ducken, Fusspunkt-basiert. Physikwerte v1 nach Gefuehl (Feinschliff nach Test). - HUD-Nachbildung (Keks/Item/Score/Herzen/Leben/Uhr 10:00-Countdown) mit FON_1B-Ziffern; GAMEBAR-Icons geladen (Original-Ziffern-Palette unklar). - Levelmusik: ONGAME2 Song 0 als v1-ANNAHME (nach Gehoer korrigieren, wie beim Factor5-Sound); wird im Audio-Preload mitgerendert. - ESC -> zurueck zur Karte (Titelmusik loopt wieder), Fenster-X -> Ende. Verifiziert: E2E unter Xvfb mit gehaltenen XTEST-Tasten -- laufen (Kamera scrollt), springen, Plattformkante runterfallen, unterm Brueckenbogen zurueck, ESC->Karte->Menue->QUIT rc=0. Level-Standbild deckungsgleich zur DOSBox-Referenz. --once-Regression exit 0. Co-Authored-By: Claude Fable 5 --- NOTES.md | 42 +++++++ README.md | 13 +- game/intro.py | 23 +++- game/level.py | 335 ++++++++++++++++++++++++++++++++++++++++++++++++++ game/menu.py | 14 ++- 5 files changed, 420 insertions(+), 7 deletions(-) create mode 100644 game/level.py diff --git a/NOTES.md b/NOTES.md index 126b2cf..aa17e43 100644 --- a/NOTES.md +++ b/NOTES.md @@ -1371,3 +1371,45 @@ deckungsgleich). 2. Gameplay: Tony auf W1L0 (Map/Tiles/Sprites laengst dekodiert). 3. Copyright-Tafel-Ausloeser im Original klaeren (niedrig). 4. Highscores persistent machen + Eintrag-Eingabe (erst mit Gameplay relevant). + +## 25.07.2026 -- LEVEL 1 SPIELBAR (erster Gameplay-Slice) + +Stefans Auftrag: Weltkarte Level 1 auswaehlbar + "schauen was du aus Level 1 +machst". Ergebnis: das erste Level laeuft. + +**Referenz-Erkenntnisse (DOSBox, Xvfb+XTEST mit gehaltenen Tasten):** +- Auf der Weltkarte startet SPACE das Level (Return tut nichts, Pfeiltasten + bewegen am Spielstart keinen sichtbaren Cursor -- 0 Pixel Diff nach 2x + Right). Kurze Schwarzblende, dann steht das Level sofort. +- **Level 1 = W1L1.MAP** -- per Template-Matching des Gameplay-Screenshots + gegen die gerenderten Maps (diff W1L1=35.4 vs W1L0=56.5/W1L2=58.8; Kamera- + Match bei Map-Pixel 128,496). W1L0 ist also NICHT Level 1 (vermutlich + Bonus-/Sonderlevel). +- HUD unten 320x24 (Viewport 320x176): Kellogg's-Schriftzug, Keks-Zaehler, + Item-Box, Score, 4 Herzen, Tony-Kopf + Leben (3), Uhr zaehlt von 10:00. +- Kollision: MAP-ctype **Bit 32 = solide** (40=Boden-Oberkante=32|8, + 32=Fuellung; 0=Luft, 4=Item-Marker, 16=Special). Empirisch an der + Spawn-Plattform (Zeile 40) verifiziert. +- TONY.BOB-Frames: 8-Phasen-Gehzyklus, GERADE Indizes = links, UNGERADE = + rechts (0..15); 16/17 Sprung, 24/25 Ducken (Zuordnung v1, ggf. justieren). +- GAMEBAR.BOB: 0-9 Ziffern, 10-16 Herzen/Schluessel(rot/blau/gruen)/Kopf -- + Ziffern rendern mit bob-Regel-Palette dunkelrot statt weiss wie im + Original-HUD (Laufzeit-Palette unklar) -> HUD v1 nutzt FON_1B-Ziffern. + +**Implementiert (game/level.py, Commit siehe git):** Level-Klasse (MAP+ICO +einmal komplett vorgerendert, Kamera blittet Ausschnitt), Tony (AABB-Physik +gegen Tile-Grid: laufen 110px/s, Sprung -300px/s, g=900 -- v1 nach Gefuehl, +Feinschliff nach Stefans Test), HUD-Nachbildung, ONGAME2-Song0 als +Levelmusik-ANNAHME (wie Factor5: nach Gehoer korrigieren). ESC -> Karte +(Titelmusik startet wieder). Weltkarte: Space/Return -> Level. +ONGAME2 wird im Audio-Preload mitgerendert (Levelstart blockiert nie). + +**Verifiziert:** E2E unter Xvfb mit gehaltenen Tasten -- Tony laeuft rechts +(Kamera scrollt), faellt Plattformkante runter, laeuft links unterm +Brueckenbogen durch, ESC->Karte->Menue->QUIT rc=0. Level-Standbild praktisch +deckungsgleich mit DOSBox-Referenz (Haus/Baum/?-Block/Bruecke/Saeulen). +--once-Regression exit 0. + +**Offen:** Items (ctype 4) einsammeln, Gegner, Duck-Frame-Zuordnung pruefen, +Original-HUD-Grafik, Karten-Animationen (KARTE0-9), Levelmusik-Song nach +Gehoer, Zeitablauf/Leben-Logik, weitere Level freischalten. diff --git a/README.md b/README.md index f0006ad..f135e10 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,13 @@ werden keine vorab entpackten Assets benötigt. - ✅ **Hauptmenü** (HIGHSCORES / ANLEITUNG / OPTIONEN / START / QUIT) mit blinkender Auswahl, Credits-Ticker, Optionsmenü, Anleitungs- und Highscore-Tafel — gegen DOSBox-Referenzaufnahmen nachgebaut. -- ✅ **Weltkarte** (statisch; Levelauswahl folgt mit dem Gameplay). -- ⏳ **Nächster Meilenstein:** echter Gameplay-Loop — Tony auf Level `W1L0` - bewegen (Physik, Kollision, Level-Scrolling). Existiert noch nicht. +- ✅ **Weltkarte** — Leertaste startet Level 1 (wie im Original; am Spielstart + gibt es dort keinen sichtbaren Level-Cursor). +- ✅ **Level 1 spielbar (erster Slice):** Tony läuft, springt und duckt sich + durch `W1L1` — Tile-Rendering, Kamera-Scrolling, Kollision, HUD und + Levelmusik. Noch ohne Gegner, Items und Fähigkeiten. +- ⏳ **Als Nächstes:** Items/Kekse einsammeln, Gegner, Karten-Animationen, + weitere Level. Die vollständige, chronologische Reverse-Engineering-Historie mit allen Format-Details steht in [NOTES.md](NOTES.md). @@ -70,6 +74,8 @@ Steuerung: - **ESC** — am Titelbild: Hauptmenü öffnen (wie im Original); im Menü: zurück - **Pfeiltasten + Enter** — Menü-Navigation (die Auswahl blinkt, wie im Original) +- **Im Level** — Pfeile links/rechts: laufen, Leertaste/Pfeil hoch: springen, + Pfeil runter: ducken, ESC: zurück zur Karte - **Alt+Enter** — zwischen Fenster- und Vollbildmodus umschalten; der Zustand wird in `kelloggs.ini` gemerkt und beim nächsten Start wiederhergestellt (Standard ohne `kelloggs.ini`: Fenstermodus) @@ -90,6 +96,7 @@ game/ Das eigentliche Spiel (startbar per `python -m game`) audio.py Wrapper um den TFMX-Musik-Renderer (rendert & cached WAVs) intro.py Boot-/Intro-Sequenz + Ablaufsteuerung menu.py Hauptmenü, Optionen, Anleitung, Highscores, Weltkarte + level.py Level-Gameplay: Tiles, Kamera, Tony, Kollision, HUD tfmx_player/ MIT-lizenzierter C-Renderer für TFMX-Musik original/ Original-DOS-Spieldateien (PCKELL.DAT etc.) — NICHT im Repo tools/ Reine Dev-Werkzeuge (Assets auspacken / als PNG ansehen) diff --git a/game/intro.py b/game/intro.py index 7bdb17f..3b36896 100644 --- a/game/intro.py +++ b/game/intro.py @@ -550,10 +550,13 @@ def preload_all_audio(clock, assets): # (DOSBox-Messung ~4.8s; Stefans Erinnerung "ca. 5 Sekunden") und stoppt # den Sound beim Wechsel. render_tfmx blendet das Ende sauber aus (600ms # Fade), scene_factor5 haelt den Screen genau Songlaenge. + # ONGAME2 (Song 0) = Levelmusik-Annahme fuer Level 1 (~123s Loop) -- + # einmalig hier mitrendern, damit der Levelstart nie blockiert. jobs = [ (('TITEL2', 0), 'Rauser-Sound', 5.0), (('TITEL2', 1), 'Factor5-Sound', 2.6), (('TITEL', None), 'Titelmusik', None), + (('ONGAME2', None), 'Levelmusik', None), ] audio_paths = {} for key, label, tail in jobs: @@ -953,7 +956,18 @@ def main(): wait_or_skip(clock, 3.0, lambda _t: _blit_title(title_img)) else: from . import menu as menu_mod + from . import level as level_mod m = menu_mod.MenuAssets(assets) + + def play_title_music(): + path = audio_paths.get(('TITEL', None)) + if pygame.mixer.get_init() and path: + try: + pygame.mixer.music.load(path) + pygame.mixer.music.play(-1) + except Exception: + pass + state = 'title' while state not in ('quit',): if state == 'title': @@ -963,7 +977,14 @@ def main(): state = {'start': 'map', 'title': 'title'}.get(r, 'quit') elif state == 'map': r = menu_mod.run_worldmap(clock, m) - state = 'menu' if r == 'menu' else 'quit' + state = {'level': 'level', 'menu': 'menu'}.get(r, 'quit') + elif state == 'level': + r = level_mod.run_level(clock, assets, audio_paths) + if r == 'map': + play_title_music() # Karte/Menue laufen mit Titelmusik + state = 'map' + else: + state = 'quit' if pygame.mixer.get_init(): pygame.mixer.music.stop() diff --git a/game/level.py b/game/level.py new file mode 100644 index 0000000..4789374 --- /dev/null +++ b/game/level.py @@ -0,0 +1,335 @@ +"""Level-Gameplay: Tile-Rendering, Kamera, Tony-Steuerung, Kollision, HUD. + +Erster spielbarer Gameplay-Slice (Level 1 = W1L1.MAP, per Template-Matching +des DOSBox-Gameplay-Screenshots gegen die gerenderten Maps verifiziert -- +NICHT W1L0, das ist vermutlich ein Bonus-/Sonderlevel). + +Referenz-Fakten aus der DOSBox-Aufnahme (25.07.2026): +- Auf der Weltkarte startet SPACE das Level (Return tut dort nichts, + Pfeiltasten bewegen keinen sichtbaren Cursor -- am Spielstart ist nur + Level 1 verfuegbar). Kurzer Schwarzblende-Uebergang, dann steht das Level + sofort (kein "Get Ready"-Screen). +- Viewport: 320x176 (oben), HUD-Balken unten 320x24 (y=176..200). +- HUD: Kellogg's-Schriftzug, Keks-Zaehler (000), Item-Box, Score (000000), + 4 Herzen, Tony-Kopf mit Lebenszahl (3), Uhr zaehlt von 10:00 runter + (9:59 ~1.5s nach Levelstart beobachtet). +- Tony spawnt im Bereich um Tile ~17 (Kamera-Match bei Map-Pixel 128,496), + steht auf der "Saeulen"-Plattform (Zeile 40). + +Kollision (aus dem 7-Bit-Typfeld der MAP-Zellen, empirisch an W1L1): + Bit 32 = solide (40 = Boden-Oberkante 32|8, 32 = Fuellung darunter, + 36/44/41 = Varianten). 0 = Luft, 4 = Item-Marker, 16 = Special (Tuer?). + v1 nutzt nur `ctype & 32` als Wand/Boden. + +Tony-Sprite (TONY.BOB, 53 Frames a 32x56): 8-Phasen-Gehzyklus, GERADE +Frame-Indizes = Blick links, UNGERADE = rechts (0..15). Reihe 3 (16..23) +Arme hoch (Sprung), Reihe 4 (24..31) Ducken/Rollen. + +Musik: ONGAME2.TFX buendelt 14 Songs -- Song 0 als v1-Annahme fuer Level 1 +(nach Gehoer noch nicht zugeordnet; wie beim Factor5-Sound: Stefan hoert, +wir korrigieren den Index bei Bedarf). +""" +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 # sichtbarer Levelausschnitt +HUD_H = 24 # HUD-Balken unten (320x24) +TILE = 16 + +# Physik (v1 -- nach Spielgefuehl gegen die DOSBox-Referenz grob abgestimmt, +# Feinschliff nach Stefans Live-Test) +WALK_SPEED = 110.0 # px/s +GRAVITY = 900.0 # px/s^2 +JUMP_VELOCITY = -300.0 # px/s (Sprunghoehe ~50px = gut 3 Tiles) +WALK_ANIM_FPS = 12.0 + +LEVEL_TIME_SECONDS = 10 * 60 # Uhr im Original zaehlt von 10:00 runter + +SOLID_BIT = 32 + +# TONY.BOB-Frame-Zuordnung (siehe Modul-Docstring) +IDLE_L, IDLE_R = 0, 1 +WALK_L = [0, 2, 4, 6, 8, 10, 12, 14] +WALK_R = [1, 3, 5, 7, 9, 11, 13, 15] +JUMP_L, JUMP_R = 16, 17 +DUCK_L, DUCK_R = 24, 25 + + +def _frames_to_surfaces(frames, palette): + '''BOB-Frames (Palette-Indizes, -1 = transparent) -> pygame-Surfaces.''' + 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: + '''Laedt eine MAP (+ zugehoeriges Welt-ICO/-Palette) und rendert sie + einmalig komplett auf eine grosse Surface -- die Kamera blittet daraus + den sichtbaren Ausschnitt (W1L1: 3488x752 px, als Vollbild-Surface + problemlos).''' + + def __init__(self, assets, map_name): + container = assets.container + self.name = map_name + world = map_name[:2] # 'W1' + 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']) + + # Tiles einmalig zu Surfaces + 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()) + + # ganze Map vorrendern + 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): + x = (i % self.width) * TILE + y = (i // self.width) * TILE + self.surface.blit(tile_surfs[tile], (x, y)) + 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 # ausserhalb = solide (haelt Tony im Level) + return self.cells[ty * self.width + tx][1] + + def solid(self, tx, ty): + return bool(self.ctype(tx, ty) & SOLID_BIT) + + +class Tony: + '''Spielfigur: Position (px, Fusspunkt-basiert), einfache AABB-Physik + gegen das Tile-Grid.''' + + HITBOX_W = 20 + HITBOX_H = 52 + + def __init__(self, assets, spawn_px): + container = assets.container + frames = parse_bob(container.entries['TONY.BOB']) + palette = get_bob_palette(container.entries, 'TONY') + self.surfs = _frames_to_surfaces(frames, palette) + self.x, self.y = float(spawn_px[0]), float(spawn_px[1]) # Fussmitte + self.vx = 0.0 + self.vy = 0.0 + self.on_ground = False + self.facing = 1 # 1 = rechts, -1 = links + self.ducking = False + self.anim_t = 0.0 + + 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): + self.ducking = self.on_ground 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 + self.vx = move * WALK_SPEED + if move: + self.facing = move + self.anim_t += dt + else: + self.anim_t = 0.0 + + if self.on_ground and not self.ducking and (keys[pygame.K_SPACE] or keys[pygame.K_UP]): + self.vy = JUMP_VELOCITY + self.on_ground = False + + self.vy = min(self.vy + GRAVITY * dt, 480.0) + + # X-Bewegung + Wandkollision + nx = self.x + self.vx * dt + r = self.rect() + if self.vx > 0: + edge = nx + self.HITBOX_W / 2 + tx = int(edge // 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: + edge = nx - self.HITBOX_W / 2 + tx = int(edge // 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-Bewegung + Boden-/Deckenkollision + 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: + top = ny - self.HITBOX_H + ty = int(top // 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 + self.y = ny + + def current_frame(self): + right = self.facing > 0 + if self.ducking: + i = DUCK_R if right else DUCK_L + elif not self.on_ground: + i = JUMP_R if right else JUMP_L + elif self.vx: + seq = WALK_R if right else WALK_L + i = seq[int(self.anim_t * WALK_ANIM_FPS) % len(seq)] + else: + i = IDLE_R if right else IDLE_L + if i < len(self.surfs): + return self.surfs[i] + return self.surfs[0] + + def draw(self, screen, cam_x, cam_y): + surf = self.current_frame() + # Fusspunkt-zentriert + x = int(self.x - surf.get_width() / 2 - cam_x) + y = int(self.y - surf.get_height() - cam_y) + screen.blit(surf, (x, y)) + + +class Hud: + '''HUD-Balken (320x24) -- v1-Nachbildung des Original-Layouts. Panel + (dunkelrote Boxen mit Goldrand) selbst gezeichnet; Zahlen in der kleinen + FON-Schrift mit Outline (die Original-HUD-Ziffern sind noch keinem Asset + sauber zugeordnet -- die GAMEBAR.BOB-Ziffern rendern mit der bob-Regel- + Palette dunkelrot statt weiss, vermutlich andere Laufzeit-Palette). + Die GAMEBAR-Schluessel-/Herz-Icons (Frames 10-16) liegen fuer den + spaeteren Feinschliff schon in self.icons bereit.''' + + GOLD = (222, 186, 24) + RED = (150, 14, 14) + DARK = (84, 6, 6) + + 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.font = BitmapFont(assets.pcc_surface('FON_1B'), 9, 7, + _mapping_from_rows(['1234567890;:!"%/()=?,.-\x7f#']), + spacing=8) + + 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)) + + def _text(self, canvas, x, y, text): + surf = self.font.render(text, outline=(0, 0, 0)) + canvas.blit(surf, (x, y)) + + def draw(self, canvas, y0, cookies, score, hearts, lives, time_left): + pygame.draw.rect(canvas, self.DARK, (0, y0, 320, HUD_H)) + pad_y = y0 + 7 + # Keks-Zaehler + self._box(canvas, 30, pad_y - 3, 34, 15) + self._text(canvas, 34, pad_y, f'{cookies:03d}') + # Item-Box (leer) + self._box(canvas, 92, pad_y - 3, 40, 15) + # Score + self._box(canvas, 148, pad_y - 3, 58, 15) + self._text(canvas, 152, pad_y, f'{score:06d}') + # Herzen + for i in range(4): + col = (230, 40, 60) if i < hearts else (70, 10, 20) + hx = 222 + i * 11 + pygame.draw.circle(canvas, col, (hx + 3, pad_y + 3), 3) + pygame.draw.circle(canvas, col, (hx + 8, pad_y + 3), 3) + pygame.draw.polygon(canvas, col, [(hx, pad_y + 4), (hx + 5, pad_y + 10), (hx + 11, pad_y + 4)]) + # Leben + self._box(canvas, 272, pad_y - 3, 13, 15) + self._text(canvas, 275, pad_y, str(lives)) + # Uhr m:ss + m, s = divmod(max(0, int(time_left)), 60) + self._box(canvas, 290, pad_y - 3, 29, 15) + self._text(canvas, 292, pad_y, f'{m}:{s:02d}') + + +def run_level(clock, assets, audio_paths, map_name='W1L1', spawn_tile=(17, 40)): + '''Spielt ein Level. ESC -> zurueck zur Karte ('map'), Fenster-X -> 'quit'. + v1: Laufen/Springen/Ducken + Kollision + Kamera + HUD + Levelmusik. + Noch ohne Gegner/Items/Fahigkeiten -- die folgen schrittweise.''' + level = Level(assets, map_name) + tony = Tony(assets, (spawn_tile[0] * TILE + 8, spawn_tile[1] * TILE - 0.01)) + hud = Hud(assets) + + # Levelmusik: ONGAME2 Song 0 (v1-Annahme, siehe Docstring). Loopt. + 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 + + while True: + dt = clock.tick(60) / 1000.0 + dt = min(dt, 1 / 20) # Physik-Schutz bei Rucklern + 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' # wie im Original: ESC = zurueck zur Karte + + keys = pygame.key.get_pressed() + tony.update(level, dt, keys) + + # Kamera: Tony zentriert, an Levelraender geklemmt + cam_x = max(0, min(int(tony.x) - VIEW_W // 2, map_w_px - VIEW_W)) + cam_y = max(0, min(int(tony.y) - VIEW_H * 2 // 3, map_h_px - VIEW_H)) + + canvas.blit(level.surface, (0, 0), (cam_x, cam_y, VIEW_W, VIEW_H)) + tony.draw(canvas, cam_x, cam_y) + time_left = LEVEL_TIME_SECONDS - (time.monotonic() - start) + hud.draw(canvas, VIEW_H, cookies=0, score=0, hearts=4, lives=3, time_left=time_left) + + intro._blit_centered(intro.build_letterboxed(canvas)) + pygame.display.flip() diff --git a/game/menu.py b/game/menu.py index 4da8efa..82728dd 100644 --- a/game/menu.py +++ b/game/menu.py @@ -414,14 +414,22 @@ def run_highscores(clock, m): def run_worldmap(clock, m): '''Weltkarte (KARTE.PCC -- kompletter 320x200-Screen inkl. Rahmen und - Charakteren). v1: statisch; die kleinen Animationsframes KARTE0-9 und die - Levelauswahl folgen mit dem Gameplay. ESC/Taste -> zurueck ins Menue.''' + Charakteren). Wie im Original (DOSBox-Referenz 25.07.2026): SPACE startet + Level 1 (Return tut im Original nichts, wir nehmen beides; Pfeiltasten + bewegen am Spielstart keinen sichtbaren Cursor -- es ist nur Level 1 + verfuegbar). ESC -> zurueck ins Menue. + Rueckgabe: 'level' | 'menu' | 'quit'. Die Karten-Animationen (KARTE0-9) + folgen spaeter.''' canvas = m.karte def draw(_t): _present(canvas) def key(event): - return 'menu' + if event.key == pygame.K_ESCAPE: + return 'menu' + if event.key in (pygame.K_SPACE, pygame.K_RETURN): + return 'level' + return None return _menu_loop(clock, draw, key)