From c8f7d4d9905fb5f1c00897bc0aa6389801352d99 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 24 Jul 2026 23:12:52 +0200 Subject: [PATCH] Hauptmenue, Optionen, Anleitung, Highscores und Weltkarte implementiert Per DOSBox-Referenzaufnahmen (Xvfb + XTEST-Tastensimulation) den kompletten Original-Menuefluss erfasst und nachgebaut: - ESC am stehenden Titelbild oeffnet das Hauptmenue (Original-Verhalten -- Space/Return tun dort nichts, per Einzeltasten-Sessions verifiziert; das Original zeigt auch nach 115s passiv keinen Attract-Loop). - Hauptmenue: HIGHSCORES/ANLEITUNG/OPTIONEN/START/QUIT in roter FON_2- Schrift, Default-Auswahl START, der selektierte Eintrag BLINKT (so zeigt das Original den Cursor). PARTY1/PARTY2-Charaktergruppen links/rechts, Credits-Ticker in gelber FON_1-Schrift unter der Box. - OPTIONEN: MUSIK EIN/AUS (wirkt auf pygame.mixer.music), EFFEKTE EIN/AUS (Flag fuer spaeteres Gameplay), EXIT. - ANLEITUNG: gelbe Texttafel (Original-Joystick-Text auf Tastatur uebertragen). - HIGHSCORES: Tabelle NAME/SCORE/ITEMS/LEV mit den Original-Defaults. - START: Weltkarte (KARTE.PCC, kompletter Screen) -- v1 statisch, Taste zurueck ins Menue; Levelauswahl/Animationen folgen mit dem Gameplay. - QUIT: Programm-Ende. Technik (game/menu.py): - BitmapFont-Parser fuer die Original-Fonts: FON_1/FON_1B (9x7-Zellen, 8px-Vorschub -- nur so passen die 32-Zeichen-Zeilen der Anleitung in die Box) und FON_2 (16x15-Zellen, 3 Reihen). Outline-Rendering fuer die gelbe Schrift (Original zeichnet sie ueberall mit schwarzem Rand). - PARTY-Sprites via make_border_bg_transparent freigestellt (papierweisser BG wie bei den Rauser-Assets -- ein Schwarz-Replace loescht faelschlich die Konturen, per Vergleichsrender nachgewiesen und verworfen). - intro.py: Zustandsautomat Titelbild <-> Menue <-> Weltkarte; wait_for_key unterscheidet jetzt Fenster-X ('quit') von Taste ('key'). Verifiziert: End-to-End-Lauf des Remakes unter Xvfb mit echten XTEST-Tasten -- Intro -> ESC -> Menue -> START -> Karte -> zurueck -> Optionen -> Anleitung -> Highscores -> QUIT beendet sauber (rc=0); alle Screens als Screenshots gegen die DOSBox-Referenz geprueft. python -m game --once weiterhin exit 0. Co-Authored-By: Claude Fable 5 --- README.md | 22 ++- game/intro.py | 80 ++++++---- game/menu.py | 427 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 491 insertions(+), 38 deletions(-) create mode 100644 game/menu.py diff --git a/README.md b/README.md index 0f2871c..24d6c36 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ werden keine vorab entpackten Assets benötigt. ICO (Tilesets), MAP (Level), TFMX (Musik). - ✅ **Boot-/Intro-Sequenz** spielbar (Rauser → Factor5 → Kellogg's-Logo → Titelbild, mit Fades, Vorhang-Wipe und Musik). +- ✅ **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. @@ -55,14 +59,17 @@ python3 -m game # Intro; bleibt am Titelbild stehen (Musik loopt) python3 -m game --once # Titelbild nur kurz halten (für Tests/headless) ``` -Ablauf: Rauser → Factor5 → Kellogg's → Titelbild-Animation; danach bleibt das -fertige Titelbild stehen und die Titelmusik läuft in Schleife, bis eine Taste -das Programm beendet. +Ablauf (wie im Original): Rauser → Factor5 → Kellogg's → Titelbild-Animation; +das fertige Titelbild bleibt stehen (Titelmusik loopt). **ESC/Leertaste** +öffnet dort das Hauptmenü — START führt zur Weltkarte, QUIT beendet das +Programm, ESC im Menü kehrt zum Titelbild zurück. Steuerung: -- **Leertaste** — überspringt den aktuellen Intro-Screen (auf dem Titelbild - beendet sie das Programm) -- **ESC** — beendet das Programm +- **Leertaste** — überspringt den aktuellen Intro-Screen; am Titelbild öffnet + sie das Hauptmenü +- **ESC** — am Titelbild: Hauptmenü öffnen (wie im Original); im Menü: zurück +- **Pfeiltasten + Enter** — Menü-Navigation (die Auswahl blinkt, wie im + Original) - **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) @@ -81,7 +88,8 @@ KELLOGG_DAT=/pfad/zu/PCKELL.DAT python3 -m game game/ Das eigentliche Spiel (startbar per `python -m game`) formats.py Parser/Decoder für alle Asset-Formate aus PCKELL.DAT audio.py Wrapper um den TFMX-Musik-Renderer (rendert & cached WAVs) - intro.py Boot-/Intro-Sequenz + intro.py Boot-/Intro-Sequenz + Ablaufsteuerung + menu.py Hauptmenü, Optionen, Anleitung, Highscores, Weltkarte 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 1039d89..7bdb17f 100644 --- a/game/intro.py +++ b/game/intro.py @@ -439,15 +439,19 @@ def wait_or_skip(clock, seconds, screen_update_fn, fps=30): def wait_for_key(clock, draw_fn, fps=30): - '''Zeigt das Standbild (draw_fn(1.0)) und wartet UNENDLICH, bis der User - Leertaste oder ESC drueckt (oder das Fenster schliesst). Alt+Enter schaltet - dabei weiter Vollbild um. Die geloopte Titelmusik laeuft die ganze Zeit - weiter. Gibt immer 'quit' zurueck -- danach ist das Programm zu Ende.''' + '''Zeigt das Standbild (draw_fn(1.0)) und wartet UNENDLICH auf eine Taste. + Alt+Enter schaltet dabei weiter Vollbild um; die geloopte Titelmusik + laeuft die ganze Zeit weiter. Rueckgabe: + 'quit' -- Fenster-X (Programm beenden) + 'key' -- ESC oder Leertaste gedrueckt (im Original oeffnet ESC am + Titelbild das Hauptmenue -- der Aufrufer entscheidet).''' while True: for event in pygame.event.get(): r = _process_event(event) - if r in ('quit', 'skip'): # auf dem letzten Screen beendet beides + if event.type == pygame.QUIT: return 'quit' + if r in ('quit', 'skip'): + return 'key' draw_fn(1.0) pygame.display.flip() clock.tick(fps) @@ -868,32 +872,29 @@ def scene_kelloggs(clock, assets, audio_paths): return wait_or_skip(clock, 1.0, lambda _t: show_kelloggs(1.0)) -def scene_title(clock, assets, hold_forever=True): - '''Titelbild -- 2x2-Quadranten-Reveal, Scanlines loesen sich von der Mitte - auf. Danach bleibt das fertige, voll-farbige Titelbild STEHEN (die - Titelmusik loopt weiter), bis der User Leertaste/ESC drueckt -- dann endet - das Programm. hold_forever=False (fuer --once/Tests) haelt stattdessen nur - kurz und kehrt zurueck, statt auf eine Taste zu warten.''' +def scene_title_reveal(clock, assets): + '''Titelbild-Animation: 2x2-Quadranten-Reveal, Scanlines loesen sich von + der Mitte auf. Gibt ('quit'|None, title_img) zurueck -- 'skip' waehrend + der Wipe springt direkt zum fertigen Bild.''' quads = build_title_quadrants(assets) r = wait_or_skip(clock, TITLE_WIPE_SECONDS, lambda t: _blit_title(render_title_wipe_frame(quads, t))) - if r == 'quit': - return 'quit' - # 'skip' waehrend der Wipe-Animation -> direkt zum fertigen Titelbild title_img = build_title_image(quads) - draw = lambda _t: _blit_title(title_img) - if hold_forever: - return wait_for_key(clock, draw) - return wait_or_skip(clock, 3.0, draw) + return ('quit' if r == 'quit' else None), title_img -def run_once(clock, assets, audio_paths, hold_title=True): - '''Spielt die komplette Intro-Sequenz EINMAL ab (keine Endlosschleife mehr). - LEERTASTE ueberspringt den jeweils aktuellen Screen (die Scene liefert dann - 'skip' -> naechste Scene), ESC/Fenster-X beendet das Intro. Der letzte - Screen (Titelbild) bleibt bei hold_title=True stehen und die Titelmusik - loopt, bis Leertaste/ESC das Programm beendet. hold_title=False (--once) - haelt das Titelbild nur kurz -- fuer automatisierte/headless Laeufe.''' +def hold_title(clock, title_img): + '''Fertiges Titelbild stehen lassen (Titelmusik loopt), bis eine Taste + kommt. Rueckgabe 'quit' (Fenster-X) oder 'key' (ESC/Leertaste -> der + Aufrufer oeffnet das Hauptmenue, wie im Original ESC).''' + return wait_for_key(clock, lambda _t: _blit_title(title_img)) + + +def run_intro(clock, assets, audio_paths): + '''Spielt die Intro-Sequenz bis einschliesslich Titelbild-Animation EINMAL + ab. LEERTASTE ueberspringt den jeweils aktuellen Screen, Fenster-X/ESC + bricht ab. Rueckgabe ('quit'|None, title_img) -- title_img ist das fertige + Titelbild fuer die anschliessende Halte-Phase.''' scenes = [ lambda: scene_rauser(clock, assets, audio_paths), lambda: scene_pause(clock, 0.5), @@ -901,18 +902,17 @@ def run_once(clock, assets, audio_paths, hold_title=True): lambda: scene_pause(clock, 0.5), lambda: scene_kelloggs(clock, assets, audio_paths), lambda: scene_pause(clock, 0.5), - lambda: scene_title(clock, assets, hold_forever=hold_title), ] for scene in scenes: r = scene() if r == 'quit': - return True + return 'quit', None if r == 'skip' and pygame.mixer.get_init(): # beim Ueberspringen haengende Sound-Effekte (Sting/Factor5-Brausen) # abschneiden -- pygame.mixer.stop() betrifft nur Sound-Kanaele, die # geloopte Titelmusik (mixer.music) laeuft ungestoert weiter. pygame.mixer.stop() - return False + return scene_title_reveal(clock, assets) def main(): @@ -942,10 +942,28 @@ def main(): # fertige WAV-Pfade zu, nie mehr live-render im Hauptloop. audio_paths, quit_requested = preload_all_audio(clock, assets) - # EIN Durchlauf (keine Endlosschleife): endet am stehenden Titelbild, das - # bis Leertaste/ESC gehalten wird (Titelmusik loopt). --once haelt nur kurz. + # Ablauf (wie Original, per DOSBox-Referenz): Intro -> Titelbild steht + # (Musik loopt) -> Taste oeffnet das Hauptmenue -> START=Weltkarte, + # QUIT=Ende, ESC=zurueck zum Titelbild. --once: nach der Titelbild- + # Animation kurz halten und beenden (fuer Tests/headless). if not quit_requested: - run_once(clock, assets, audio_paths, hold_title=not once) + r, title_img = run_intro(clock, assets, audio_paths) + if r != 'quit' and title_img is not None: + if once: + wait_or_skip(clock, 3.0, lambda _t: _blit_title(title_img)) + else: + from . import menu as menu_mod + m = menu_mod.MenuAssets(assets) + state = 'title' + while state not in ('quit',): + if state == 'title': + state = 'menu' if hold_title(clock, title_img) == 'key' else 'quit' + elif state == 'menu': + r = menu_mod.run_menu(clock, m) + 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' if pygame.mixer.get_init(): pygame.mixer.music.stop() diff --git a/game/menu.py b/game/menu.py new file mode 100644 index 0000000..4da8efa --- /dev/null +++ b/game/menu.py @@ -0,0 +1,427 @@ +"""Hauptmenue, Optionsmenue, Anleitung, Highscore-Tafel und Weltkarte. + +Alles gegen echte DOSBox-Referenzaufnahmen (24.07.2026, Xvfb + XTEST-Tasten) +nachgebaut -- Ablauf im Original: + Titelbild --ESC--> Hauptmenue (faedet ein) mit Eintraegen + HIGHSCORES / ANLEITUNG / OPTIONEN / START / QUIT (rote FON_2-Schrift, + Default-Auswahl START). Der SELEKTIERTE Eintrag BLINKT (das ist der + Cursor -- kein Pfeil o.ae.). Links/rechts der Box winken die Charakter- + Gruppen PARTY1/PARTY2. Unter der Box laeuft ein Credits-Ticker in + kleiner gelber Schrift (FON_1/FON_1B) mit wechselnden Texten + ("FREEGAMES #3", "REALISATION RAUSER ADVERTAINMENT GMBH REUTLINGEN", + "PRODUZIERT VON FACTOR 5", "ANIMATIONEN UND ILLUSTRATIONEN HARTWIG + NIEDERGASSEL"). + OPTIONEN -> eigene Tafel: MUSIK EIN/AUS, EFFEKTE EIN/AUS, EXIT. + ANLEITUNG -> gelbe Text-Tafel (Joystick-Steuerung + Faehigkeiten). + HIGHSCORES -> Tabelle NAME/SCORE/ITEMS/LEV mit den Original-Defaults + (MATTHIAS 100000 ... BORIS 2500). + START -> Weltkarte (KARTE.PCC -- kompletter Screen inkl. Rahmen und + Charakteren; Animationen KARTE0-9 spaeter). + +Screens sind 320x200 (wie die Original-Videomodi) und werden wie die +Intro-Screens letterboxed auf 320x240 -> Fenster skaliert. + +Fonts (Zellen-Grids per Spaltenanalyse der PCC-Assets bestimmt): + FON_1 (320x7): 9x7-Zellen, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + AE OE UE ss + FON_1B (320x7): 9x7-Zellen, '1234567890;:!"%/()=?,.-' + Block + '#' + FON_2 (320x45): 16x15-Zellen, 3 Zeilen: + 'ABCDEFGHIJKLMNOPQRST' / 'UVWXYZ.,:!?-' / '1234567890' +""" +import sys +import time + +import pygame + +from . import intro + + +# --------------------------------------------------------------------------- +# Bitmap-Fonts aus den FON_*.PCC-Assets +# --------------------------------------------------------------------------- + +class BitmapFont: + '''Fixed-Cell-Bitmapfont aus einer PCC-Surface. Schwarz (0,0,0) ist + Hintergrund und wird transparent. mapping: dict Zeichen -> (col, row).''' + + def __init__(self, surface, cell_w, cell_h, mapping, spacing=None): + surface = surface.convert_alpha() + # Schwarz -> transparent (PCC-Hintergrund) + arr = pygame.PixelArray(surface) + arr.replace((0, 0, 0, 255), (0, 0, 0, 0)) + del arr + self.cell_w, self.cell_h = cell_w, cell_h + self.spacing = spacing if spacing is not None else cell_w + self.glyphs = {} + for ch, (col, row) in mapping.items(): + rect = pygame.Rect(col * cell_w, row * cell_h, cell_w, cell_h) + if rect.right <= surface.get_width() and rect.bottom <= surface.get_height(): + self.glyphs[ch] = surface.subsurface(rect) + + def measure(self, text): + return len(text) * self.spacing, self.cell_h + + def render(self, text, outline=None): + '''Rendert `text` auf eine neue Surface (Alpha). outline=(r,g,b): + 1px-Umrandung in dieser Farbe (wie die gelbe Original-Schrift, die + ueberall mit schwarzem Rand gezeichnet wird).''' + w, h = self.measure(text) + pad = 1 if outline else 0 + surf = pygame.Surface((w + 2 * pad, h + 2 * pad), pygame.SRCALPHA) + if outline: + # Umriss: Text 4x versetzt in outline-Farbe, dann normal drueber + mask = pygame.Surface((w, h), pygame.SRCALPHA) + x = 0 + for ch in text.upper(): + g = self.glyphs.get(ch) + if g: + mask.blit(g, (x, 0)) + x += self.spacing + tinted = mask.copy() + tint = pygame.Surface(mask.get_size(), pygame.SRCALPHA) + tint.fill(outline + (255,)) + tinted.blit(tint, (0, 0), special_flags=pygame.BLEND_RGBA_MULT) + # RGB durch outline-Farbe ersetzen, Alpha der Glyphen behalten + for dx, dy in ((0, 1), (2, 1), (1, 0), (1, 2), (0, 0), (2, 2), (0, 2), (2, 0)): + surf.blit(tinted, (dx, dy)) + x = pad + for ch in text.upper(): + g = self.glyphs.get(ch) + if g: + surf.blit(g, (x, pad)) + x += self.spacing + return surf + + +def _mapping_from_rows(rows): + m = {} + for row, chars in enumerate(rows): + for col, ch in enumerate(chars): + m[ch] = (col, row) + return m + + +class MenuAssets: + '''Laedt und cached alle Menue-relevanten Surfaces + Fonts.''' + + def __init__(self, assets): + self.assets = assets + self.menu_bg = assets.pcc_surface('MENU') # 320x200 Komplett-Screen + self.karte = assets.pcc_surface('KARTE') # 320x200 Weltkarte + # PARTY-Sprites haben (wie die Rauser-Assets) einen PAPIERWEISSEN + # Hintergrund und schwarze Konturen -- NICHT schwarz freistellen + # (das loescht die Outlines!), sondern die vom Rand aus + # zusammenhaengende weisse Flaeche per Flood-Fill transparent machen. + self.party1 = intro.make_border_bg_transparent(assets.pcc_surface('PARTY1')) + self.party2 = intro.make_border_bg_transparent(assets.pcc_surface('PARTY2')) + + # kleine gelbe Schrift: FON_1 (Buchstaben) + FON_1B (Ziffern/Zeichen) + # -- ein gemeinsamer Font, beide Surfaces uebereinandergelegt waere + # falsch (gleiche Zeilenlage), daher zwei Fonts mit gleichem Grid und + # ein kombinierter render-Wrapper. + # Zellenraster ist 9px, die Engine rendert aber mit 8px Vorschub -- + # nur so passen die 32-Zeichen-Zeilen der Anleitung in die Box + # (32 x 8 = 256px bei ~264px Box-Innenbreite, vgl. Referenz). + f1 = BitmapFont(assets.pcc_surface('FON_1'), 9, 7, + _mapping_from_rows(['ABCDEFGHIJKLMNOPQRSTUVWXYZ\xc4\xd6\xdc\xdf']), + spacing=8) + f1b = BitmapFont(assets.pcc_surface('FON_1B'), 9, 7, + _mapping_from_rows(['1234567890;:!"%/()=?,.-\x7f#']), + spacing=8) + # Glyphen zusammenfuehren (disjunkte Zeichensaetze, gleiches Grid) + f1.glyphs.update(f1b.glyphs) + self.font_small = f1 + + self.font_big = BitmapFont(assets.pcc_surface('FON_2'), 16, 15, + _mapping_from_rows(['ABCDEFGHIJKLMNOPQRST', + 'UVWXYZ.,:!?-', + '1234567890'])) + + @staticmethod + def _transparent(surface): + s = surface.convert_alpha() + arr = pygame.PixelArray(s) + arr.replace((0, 0, 0, 255), (0, 0, 0, 0)) + del arr + return s + + +YELLOW_OUTLINE = (0, 0, 0) +TICKER_TEXTS = [ + 'FREEGAMES #3', + 'REALISATION RAUSER ADVERTAINMENT GMBH REUTLINGEN', + 'PRODUZIERT VON FACTOR 5', + 'ANIMATIONEN UND ILLUSTRATIONEN HARTWIG NIEDERGASSEL', +] +TICKER_SECONDS = 4.0 # Anzeigedauer pro Credits-Text +BLINK_SECONDS = 0.35 # Blinkrhythmus des selektierten Eintrags + +MENU_ITEMS = ['HIGHSCORES', 'ANLEITUNG', 'OPTIONEN', 'START', 'QUIT'] +MENU_START_INDEX = 3 # Default-Auswahl im Original: START + +# Layout (320x200-Koordinaten, aus den DOSBox-Referenzframes vermessen) +MENU_ITEM_Y0 = 44 # y des ersten Eintrags (HIGHSCORES) +MENU_ITEM_DY = 15 # Zeilenabstand +MENU_QUIT_GAP = 7 # QUIT ist etwas abgesetzt +PARTY1_POS = (34, 80) +PARTY2_POS = (218, 84) +TICKER_Y = 172 # Ticker-Zeile unterhalb der Box + + +def _draw_ticker(canvas, m, t): + idx = int(t / TICKER_SECONDS) % len(TICKER_TEXTS) + text = TICKER_TEXTS[idx] + # lange Texte auf 2 Zeilen brechen (wie im Original: "REALISATION RAUSER / + # ADVERTAINMENT GMBH REUTLINGEN" stand zweizeilig) + max_chars = 34 + lines = [] + if len(text) > max_chars: + words, cur = text.split(), '' + for wrd in words: + if len(cur) + len(wrd) + 1 > max_chars and cur: + lines.append(cur) + cur = wrd + else: + cur = (cur + ' ' + wrd).strip() + if cur: + lines.append(cur) + else: + lines = [text] + y = TICKER_Y + for line in lines: + surf = m.font_small.render(line, outline=YELLOW_OUTLINE) + canvas.blit(surf, ((320 - surf.get_width()) // 2, y)) + y += 10 + + +def _menu_canvas(m, items, selected, blink_on, t, hidden_indices=()): + '''Baut den kompletten Menue-Screen (320x200): Box, Parties, Eintraege + (selektierter blinkt), Ticker.''' + canvas = m.menu_bg.copy() + canvas.blit(m.party1, PARTY1_POS) + canvas.blit(m.party2, PARTY2_POS) + y = MENU_ITEM_Y0 + for i, item in enumerate(items): + if i == len(items) - 1: + y += MENU_QUIT_GAP # letzter Eintrag (QUIT/EXIT) abgesetzt + visible = (i != selected) or blink_on + if i in hidden_indices: + visible = False + if visible: + surf = m.font_big.render(item) + canvas.blit(surf, ((320 - surf.get_width()) // 2, y)) + y += MENU_ITEM_DY + _draw_ticker(canvas, m, t) + return canvas + + +def _text_panel(m, lines, y0=58, line_h=10): + '''Tafel-Screen: Menue-Box mit gelbem Text gefuellt (Anleitung/Copyright- + Stil). lines: Liste von (text, x) -- x=None zentriert.''' + canvas = m.menu_bg.copy() + y = y0 + for entry in lines: + text, x = entry if isinstance(entry, tuple) else (entry, None) + if text: + surf = m.font_small.render(text, outline=YELLOW_OUTLINE) + if x is None: + x = (320 - surf.get_width()) // 2 + canvas.blit(surf, (x, y)) + y += line_h + return canvas + + +def _present(canvas): + intro._blit_centered(intro.build_letterboxed(canvas)) + + +def _menu_loop(clock, draw_fn, on_key): + '''Generische Menue-Event-Schleife. draw_fn(t) zeichnet, on_key(event) + liefert None (weiter) oder ein Ergebnis (Schleife endet). Alt+Enter und + Fenster-X werden hier zentral behandelt.''' + 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 + r = on_key(event) + if r is not None: + return r + draw_fn(time.monotonic() - start) + pygame.display.flip() + clock.tick(30) + + +def run_menu(clock, m): + '''Hauptmenue. Rueckgabe: 'start' | 'quit' | 'title' (ESC -> zurueck zum + Titelbild).''' + state = {'sel': MENU_START_INDEX} + + def draw(t): + blink_on = int(t / BLINK_SECONDS) % 2 == 0 + _present(_menu_canvas(m, MENU_ITEMS, state['sel'], blink_on, t)) + + def key(event): + if event.key == pygame.K_ESCAPE: + return 'title' + if event.key in (pygame.K_UP, pygame.K_LEFT): + state['sel'] = (state['sel'] - 1) % len(MENU_ITEMS) + elif event.key in (pygame.K_DOWN, pygame.K_RIGHT): + state['sel'] = (state['sel'] + 1) % len(MENU_ITEMS) + elif event.key in (pygame.K_RETURN, pygame.K_SPACE): + choice = MENU_ITEMS[state['sel']] + if choice == 'START': + return 'start' + if choice == 'QUIT': + return 'quit' + if choice == 'OPTIONEN': + r = run_options(clock, m) + if r == 'quit': + return 'quit' + elif choice == 'ANLEITUNG': + r = run_anleitung(clock, m) + if r == 'quit': + return 'quit' + elif choice == 'HIGHSCORES': + r = run_highscores(clock, m) + if r == 'quit': + return 'quit' + return None + + return _menu_loop(clock, draw, key) + + +def run_options(clock, m): + '''Optionsmenue: MUSIK EIN/AUS, EFFEKTE EIN/AUS, EXIT. Rueckgabe 'back' + oder 'quit'. Musik-Toggle wirkt sofort auf pygame.mixer.music.''' + state = {'sel': 2, 'musik': True, 'effekte': True} + + def items(): + return [f'MUSIK {"EIN" if state["musik"] else "AUS"}', + f'EFFEKTE {"EIN" if state["effekte"] else "AUS"}', + 'EXIT'] + + def draw(t): + blink_on = int(t / BLINK_SECONDS) % 2 == 0 + canvas = m.menu_bg.copy() + canvas.blit(m.party1, PARTY1_POS) + canvas.blit(m.party2, PARTY2_POS) + its = items() + y = 52 + for i, item in enumerate(its): + if i == len(its) - 1: + y += 18 # EXIT abgesetzt (wie Original-Referenz) + if (i != state['sel']) or blink_on: + surf = m.font_big.render(item) + canvas.blit(surf, ((320 - surf.get_width()) // 2, y)) + y += MENU_ITEM_DY + _draw_ticker(canvas, m, t) + _present(canvas) + + def key(event): + if event.key == pygame.K_ESCAPE: + return 'back' + if event.key in (pygame.K_UP, pygame.K_LEFT): + state['sel'] = (state['sel'] - 1) % 3 + elif event.key in (pygame.K_DOWN, pygame.K_RIGHT): + state['sel'] = (state['sel'] + 1) % 3 + elif event.key in (pygame.K_RETURN, pygame.K_SPACE): + if state['sel'] == 0: + state['musik'] = not state['musik'] + if pygame.mixer.get_init(): + if state['musik']: + pygame.mixer.music.unpause() + else: + pygame.mixer.music.pause() + elif state['sel'] == 1: + state['effekte'] = not state['effekte'] + # Effekte-Flag greift, sobald es im Gameplay Soundeffekte gibt + else: + return 'back' + return None + + return _menu_loop(clock, draw, key) + + +ANLEITUNG_LINES = [ + ('STEUERUNG:', 30), + ('MIT LINKS/RECHTS STEUERST DU DIE', 30), + ('FIGUR. MIT LEERTASTE ODER PFEIL', 30), + ('HOCH KANN DIE FIGUR SPRINGEN,', 30), + ('MIT PFEIL UNTEN KANN SICH DIE', 30), + ('FIGUR DUCKEN.', 30), + ('MIT ALT AKTIVIERST DU DIE', 30), + ('F\xc4HIGKEITEN DER FIGUREN...', 30), + '', + ('TONY: RENNEN', 30), + ('SMACKS: WASSERBLASEN', 30), + ('COCO: HANGELN UND KLETTERN', 30), + ('TOUCAN: FLIEGEN', 30), +] + +# Default-Highscores aus der Original-Referenzaufnahme +DEFAULT_HIGHSCORES = [ + ('MATTHIAS', 100000, 5000, 20), + ('JAN', 80000, 4000, 17), + ('MARC', 60000, 3000, 14), + ('MICHAEL', 40000, 2000, 12), + ('URTE', 20000, 1000, 10), + ('PETER', 10000, 700, 8), + ('ANTONY', 5000, 400, 6), + ('BORIS', 2500, 200, 4), +] + + +def run_anleitung(clock, m): + '''Anleitungstafel (gelber Text). Beliebige Taste/ESC -> zurueck. + Text an die Original-Joystick-Seite angelehnt, auf Tastatur uebertragen + (das Remake hat keinen Joystick-Support).''' + canvas = _text_panel(m, ANLEITUNG_LINES, y0=36, line_h=8) + + def draw(_t): + _present(canvas) + + def key(event): + return 'back' # jede Taste schliesst die Tafel + + return _menu_loop(clock, draw, key) + + +def run_highscores(clock, m): + '''Highscore-Tafel (Tabelle NAME/SCORE/ITEMS/LEV). Beliebige Taste -> + zurueck. Ticker laeuft weiter (wie im Original).''' + def draw(t): + lines = [('NAME SCORE ITEMS LEV', 55)] + for i, (name, score, items_, lev) in enumerate(DEFAULT_HIGHSCORES, 1): + lines.append((f'{i}. {name:<9}{score:>7}{items_:>7}{lev:>5}', 30)) + canvas = m.menu_bg.copy() + y = 44 # eng gepackt, damit alle 9 Zeilen in der Box (bis ~y=145) bleiben + for text, x in lines: + surf = m.font_small.render(text, outline=YELLOW_OUTLINE) + canvas.blit(surf, (x, y)) + y += 10 + _draw_ticker(canvas, m, t) + _present(canvas) + + def key(event): + return 'back' + + return _menu_loop(clock, draw, key) + + +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.''' + canvas = m.karte + + def draw(_t): + _present(canvas) + + def key(event): + return 'menu' + + return _menu_loop(clock, draw, key)