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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
949a7eeda2
commit
c8f7d4d990
+49
-31
@@ -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()
|
||||
|
||||
+427
@@ -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)
|
||||
Reference in New Issue
Block a user