Intro gegen echte DOSBox-Referenz korrigiert + Leertaste-Skip

Referenzaufnahmen aus DOSBox (Xvfb) gemacht und danach:

- Rauser-Aufloesung gefixt: die nativen PCC-Assets werden jetzt EINMAL direkt
  auf Endgroesse hochskaliert (Nearest) und der Screen direkt in Fenster-
  aufloesung gerendert -- statt vorher 0.5x-Downscale (verwirft Pixel) + x3-
  Upscale ueber eine 320er-Zwischenleinwand. Gleiche Groesse wie bisher (~33%
  Bildbreite, gegen Referenz gemessen), aber gestochen scharf. Zusaetzlich das
  tuerkise "!"-Kaestchen nachgebaut (im Original Engine-gezeichnet, in keinem
  Asset enthalten).
- Titelbild-Reveal: Grau/Entsaettigung raus (Stefan "das gray raus"); die
  Scanlines auf der unteren Haelfte loesen sich jetzt von der Mitte nach unten
  auf ("scanlines ab der mitte getrennt weg") statt gleichmaessig auszufaden.
- Factor5: Logo haelt exakt so lange wie sein Sound (TITEL2 Song 1) spielt,
  dann weiter zu Kellogg's.
- Leertaste ueberspringt jetzt den aktuellen Intro-Screen, ESC beendet das
  ganze Intro (wait_or_skip gibt 'quit'/'skip'/None; run_once ist in Scenes
  zerlegt).

HANG-FIX (dabei gefunden): das kompilierte render_tfmx-Binary war veraltet
(vor dem Song-Index-Feature aus render_tfmx.c gebaut) und renderte TITEL2 Song1
mit 180s statt 2.4s -- gekoppelt an die neue "Factor5-Dauer = Songlaenge"-Logik
haette das Intro 3 Minuten eingefroren. Fixes: audio._ensure_binary() baut jetzt
neu, wenn die Quelle neuer ist als das Binary (verhindert stale Binary nach git
pull); zusaetzlich FACTOR5_MAX_SECONDS als Sicherheits-Obergrenze.

Verifiziert: python -m game --once (headless) exit 0 in ~30s; Rauser/Titelbild
visuell gegen die DOSBox-Referenz gegengeprueft.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-07-24 01:39:28 +02:00
co-authored by Claude Opus 4.8
parent e7d95190eb
commit 7aea2fac26
3 changed files with 227 additions and 149 deletions
+213 -146
View File
@@ -107,6 +107,15 @@ auf fertige WAV-Pfade zu, nie mehr Live-Rendering im Hauptloop.
Aufruf: python3 -m game [--once] (--once: nach einem Durchlauf beenden,
sonst loopt die Sequenz weiter wie im Original-Titelbildschirm)
Steuerung: LEERTASTE ueberspringt den aktuellen Screen, ESC beendet das Intro.
Stand 24.07.2026 (gegen echte DOSBox-Referenzaufnahmen abgeglichen):
- Rauser wird direkt in Fensteraufloesung gerendert (ein Nearest-Upscale der
nativen Assets statt 0.5x-Downscale + x3-Upscale) -> gestochen scharf; das
tuerkise "!"-Kaestchen (Engine-gezeichnet, kein Asset) wird nachgebaut.
- Factor5-Logo haelt exakt so lange wie sein Sound (TITEL2 Song 1, ~2.4s).
- Titelbild-Reveal: KEIN Grau/keine Entsaettigung mehr; die Scanlines auf der
unteren Haelfte loesen sich von der Mitte nach unten auf.
"""
import sys
import os
@@ -148,6 +157,24 @@ PRAESENTIERT_FADE_SECONDS = 2.5
# Weichzeichnen) statt smoothscale skalieren -- siehe pcc_surface_scaled().
ASSET_DISPLAY_SCALE = 0.5
# KORREKTUR #3 (24.07.2026, endlich mit echter DOSBox-Referenz vermessen):
# Das Rauser-Logo (RAUSER1 "RAUSER"-Box + RAUSER3 "!"-Box + RAUSER2
# "ADVERTAINMENT") ist im Original nur ~105px breit auf einer 320er-Leinwand
# (~33% Breite) -- die Groesse (netto ~1.5x = 0.5x * 3x-Fensterskalierung) war
# also die ganze Zeit richtig. Der QUALITAETS-Bug war der ZWEISTUFIGE Skalier-
# weg: die nativen Assets (RAUSER1 ist 182px!) wurden erst per Nearest auf 0.5x
# RUNTER (verwirft die Haelfte der Pixel -> zerbrochene Serifen) und die 320er-
# Leinwand danach x3 HOCH. Fix: die nativen Assets EINMAL direkt auf ihre
# Endgroesse hochskalieren (netto ~1.6x, ein reiner Upscale -> alle nativen
# Pixel bleiben erhalten, gestochen scharf) und den Rauser-Screen direkt in
# Fensteraufloesung rendern (nicht ueber die 320er-Zwischenleinwand). Wert so
# gewaehlt = 1.5 = exakt die alte Netto-Groesse (0.5x * 3x), die Stefan
# groessenmaessig nie bemaengelt hat -- nur jetzt als EIN Upscale statt
# ueber den verlustreichen 0.5x-Zwischenschritt, also identische Groesse/
# Position wie bisher, aber gestochen scharf. Deckt sich mit der Referenz-
# messung (Logo ~33% Bildbreite).
RAUSER_LOGO_SCALE = 1.5
# Rauser-Timing (siehe Docstring)
RAUSER_SLIDE_SECONDS = 0.9
RAUSER_HOLD_SECONDS = 1.3
@@ -155,10 +182,21 @@ RAUSER_FADE_TO_GREY_SECONDS = 0.4
RAUSER_GREY_HOLD_SECONDS = 0.3
RAUSER_GREY = (73, 73, 73)
RAUSER_WHITE = (255, 255, 255)
# Tuerkis-Kaestchen hinter dem "!" -- aus der DOSBox-Referenz gesampelt.
# Es steckt in KEINEM PCC-Asset (RAUSER3 ist nur das nackte "!"), sondern wird
# im Original von der Engine als Vollfarb-Rechteck gezeichnet.
RAUSER_TEAL = (5, 135, 183)
# Factor5-Timing (siehe Docstring-Korrektur -- Stefans Meldung 22.07.2026
# abends): feste Anzeigedauer statt (falsch) an eine 123s-Melodie gekoppelt.
FACTOR5_HOLD_SECONDS = 5.0
# Factor5-Timing (Stefan 24.07.2026): das Factor5-Logo wird exakt so lange
# gezeigt, wie der zugehoerige Sound (TITEL2 Song 1, ~2.4s) spielt -- ist der
# Sound zu Ende, geht es zu Kellogg's weiter. FALLBACK gilt ohne Audio-Geraet
# (keine Songlaenge ermittelbar). MAX ist eine Sicherheits-Obergrenze: der
# Factor5-Sound ist bewusst kurz ("kurzes Brausen"); sollte je ein fehlerhaft
# gerendertes (zu langes) WAV im Cache landen, darf der Screen NICHT minutenlang
# haengen (genau dieser Bug ist am 24.07. durch ein veraltetes render_tfmx-
# Binary passiert -- 180s statt 2.4s).
FACTOR5_FALLBACK_SECONDS = 2.5
FACTOR5_MAX_SECONDS = 12.0
# Titelbild-Timing (siehe Docstring)
TITLE_HEIGHT_REVEAL_FRACTION = 0.55 # Anteil der Wipe-Dauer fuer das Hoehenwachstum
@@ -302,7 +340,13 @@ def blit_scaled_centered(screen, surface, window_w, window_h, bg_color=BG_COLOR)
def wait_or_skip(clock, seconds, screen_update_fn, fps=30):
'''Laeuft seconds lang, ruft pro Frame screen_update_fn(t_norm 0..1) auf.
Bricht per ESC/Klick vorzeitig ab (gibt True zurueck wenn User quit will).'''
Rueckgabe:
'quit' -- User will das ganze Intro beenden (ESC oder Fenster-X)
'skip' -- User will den aktuellen Screen ueberspringen (Leertaste)
None -- die Dauer ist regulaer abgelaufen
'quit' und 'skip' sind beide truthy, None ist falsy -- Aufrufer koennen also
kurz `if r: return r` schreiben, um Scene-Abbruch (skip) und Gesamt-Abbruch
(quit) gemeinsam nach oben durchzureichen.'''
start = time.monotonic()
while True:
now = time.monotonic()
@@ -310,12 +354,15 @@ def wait_or_skip(clock, seconds, screen_update_fn, fps=30):
if t >= 1.0:
screen_update_fn(1.0)
pygame.display.flip()
return False
return None
for event in pygame.event.get():
if event.type == pygame.QUIT:
return True
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
return True
return 'quit'
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
return 'quit'
if event.key == pygame.K_SPACE:
return 'skip'
screen_update_fn(t)
pygame.display.flip()
clock.tick(fps)
@@ -446,26 +493,43 @@ def build_kelloggs_frame(w, h, indices, palette, fade_t):
# ---------------------------------------------------------------------------
def build_rauser_parts(assets):
'''Liefert die drei (bei 0.5x Nearest-Neighbor-Groesse, siehe Docstring-
Korrektur v3) Rauser-Teile + ihre Endpositionen auf einer 320x240-
Leinwand (RAUSER1 links, RAUSER3 rechts daneben, RAUSER2 darunter
mittig).'''
'''Liefert die drei Rauser-Teile bei RAUSER_LOGO_SCALE (Nearest-Neighbor-
UPSCALE direkt aus der Nativaufloesung, siehe Docstring-Korrektur #3) +
ihre Endpositionen direkt in FENSTER-Koordinaten (960x720). RAUSER1
("RAUSER") links, RAUSER3 ("!") rechts daneben, RAUSER2 ("ADVERTAINMENT")
darunter -- der Rauser-Screen wird NICHT ueber die 320er-Zwischenleinwand
gerendert (die den Blur verursacht hat), sondern direkt in voller
Fensteraufloesung.'''
# cut_border_bg=True (Fix 22.07.2026 abends): macht den papierweissen
# Hintergrund um jede Box transparent (nur die vom Bildrand aus
# zusammenhaengende Flaeche, siehe make_border_bg_transparent) -- vorher
# blieb hier ein harter weisser Kasten stehen, der beim Fade zu Grau nicht
# mitgefadet ist (Stefans Meldung "Text bleibt hart umrandet stehen").
r1 = assets.pcc_surface_scaled('RAUSER1', cut_border_bg=True) # "RAUSER"-Box
r2 = assets.pcc_surface_scaled('RAUSER2', cut_border_bg=True) # "ADVERTAINMENT"
r3 = assets.pcc_surface_scaled('RAUSER3', cut_border_bg=True) # "!" auf blauer Box
# scale=RAUSER_LOGO_SCALE ist ein UPSCALE der nativen Assets -> pcc_surface_scaled
# macht cut_border auf der Nativ-Surface und skaliert dann per Nearest hoch,
# es geht also kein natives Pixel verloren (Gegensatz zum alten 0.5x-Downscale).
r1 = assets.pcc_surface_scaled('RAUSER1', scale=RAUSER_LOGO_SCALE, cut_border_bg=True) # "RAUSER"-Box
r2 = assets.pcc_surface_scaled('RAUSER2', scale=RAUSER_LOGO_SCALE, cut_border_bg=True) # "ADVERTAINMENT"
excl = assets.pcc_surface_scaled('RAUSER3', scale=RAUSER_LOGO_SCALE, cut_border_bg=True) # nur das nackte "!"
total_w = r1.get_width() + r3.get_width()
# Tuerkis-Kaestchen hinter dem "!" nachbauen (siehe RAUSER_TEAL -- im
# Original von der Engine gezeichnet, in keinem Asset). Gegen die DOSBox-
# Referenz vermessen: ~gleich hoch wie die RAUSER-Box, ~0.85x so breit wie
# hoch, das "!" ragt oben aus dem Kaestchen heraus.
tb_h = r1.get_height()
tb_w = max(excl.get_width() + 6, round(tb_h * 0.85))
overhang = max(0, excl.get_height() - tb_h)
r3 = pygame.Surface((tb_w, tb_h + overhang), pygame.SRCALPHA)
pygame.draw.rect(r3, RAUSER_TEAL, (0, overhang, tb_w, tb_h))
r3.blit(excl, ((tb_w - excl.get_width()) // 2, 0)) # "!" mittig, ragt oben raus
total_w = max(r1.get_width() + r3.get_width(), r2.get_width())
total_h = r1.get_height() + r2.get_height()
x0 = (320 - total_w) // 2
y0 = (240 - total_h) // 2
x0 = (WINDOW_W - total_w) // 2
y0 = (WINDOW_H - total_h) // 2
r1_final = (x0, y0)
r3_final = (x0 + r1.get_width(), y0 - (r3.get_height() - r1.get_height()) // 2)
r3_final = (x0 + r1.get_width(), y0 - overhang) # teal-Teil oben buendig mit der RAUSER-Box
r2_final = (x0, y0 + r1.get_height())
return {
'r1': r1, 'r2': r2, 'r3': r3,
@@ -493,13 +557,22 @@ def render_rauser_slide(canvas, parts, slide_t):
canvas.blit(r3, (r3_final_x, r3_y))
def run_rauser(screen, clock, assets, audio_paths):
def _draw_rauser(screen, parts, bg_color, slide_t):
'''Zeichnet einen Rauser-Frame direkt in Fensteraufloesung: Hintergrund
fuellen, dann die (transparent-hinterlegten) Logo-Teile an ihrer Slide-
Position druebersetzen.'''
screen.fill(bg_color)
render_rauser_slide(screen, parts, slide_t)
def scene_rauser(screen, clock, assets, audio_paths):
'''Rauser-Karte: weisser Grund, Slide-in, Sound-Sting bei Ankunft, Fade zu
grauem Grund, kurz halten. Gibt True zurueck wenn der User abbrechen will.
audio_paths kommt aus preload_all_audio() -- das Audio wird hier NICHT
mehr live gerendert (siehe render_audio_with_loading-Docstring zum
Hang-Bug, den das behebt), sondern nur noch aus dem fertigen WAV-Pfad
geladen (schnell, nie blockierend).'''
grauem Grund, kurz halten. Rueckgabe 'quit'/'skip'/None (siehe wait_or_skip).
Wird jetzt DIREKT in Fensteraufloesung gerendert (kein 320er-Zwischen-
canvas + Upscale mehr) -- das war die Ursache des Rauser-Aufloesungs-
Problems (siehe RAUSER_LOGO_SCALE-Kommentar).
audio_paths kommt aus preload_all_audio() -- das Audio wird hier nur noch
aus fertigen WAV-Pfaden geladen (nie live gerendert, siehe Hang-Fix).'''
parts = build_rauser_parts(assets)
sting = None
# TITEL2 Song 0 -- der kuerzere der beiden TITEL2-Songs (~2.04s), Song 1
@@ -509,55 +582,39 @@ def run_rauser(screen, clock, assets, audio_paths):
try:
sting = pygame.mixer.Sound(sting_path)
except Exception as exc:
print(f'[intro_sequence] Rauser-Sound-Sting konnte nicht geladen werden: {exc}', file=sys.stderr)
print(f'[intro] Rauser-Sound-Sting konnte nicht geladen werden: {exc}', file=sys.stderr)
sting_played = False
sting_played = [False]
def show_slide(t):
nonlocal sting_played
canvas = pygame.Surface((320, 240))
canvas.fill(RAUSER_WHITE)
render_rauser_slide(canvas, parts, t)
if t >= 1.0 and not sting_played:
sting_played = True
_draw_rauser(screen, parts, RAUSER_WHITE, t)
if t >= 1.0 and not sting_played[0]:
sting_played[0] = True
if sting is not None:
sting.play()
blit_scaled_centered(screen, canvas, WINDOW_W, WINDOW_H, bg_color=RAUSER_WHITE)
quit_requested = wait_or_skip(clock, RAUSER_SLIDE_SECONDS, show_slide)
if quit_requested:
return True
r = wait_or_skip(clock, RAUSER_SLIDE_SECONDS, show_slide)
if r:
return r
# falls die Slide-Phase zu kurz war um sting_played zu triggern (sollte
# durch t>=1.0 im letzten Frame von wait_or_skip immer der Fall sein)
if not sting_played and sting is not None:
# falls die Slide-Phase zu kurz war um den Sting zu triggern
if not sting_played[0] and sting is not None:
sting.play()
sting_played = True
sting_played[0] = True
def show_hold_white(_t):
canvas = pygame.Surface((320, 240))
canvas.fill(RAUSER_WHITE)
render_rauser_slide(canvas, parts, 1.0)
blit_scaled_centered(screen, canvas, WINDOW_W, WINDOW_H, bg_color=RAUSER_WHITE)
quit_requested = wait_or_skip(clock, RAUSER_HOLD_SECONDS, show_hold_white)
if quit_requested:
return True
r = wait_or_skip(clock, RAUSER_HOLD_SECONDS,
lambda _t: _draw_rauser(screen, parts, RAUSER_WHITE, 1.0))
if r:
return r
def show_fade_grey(t):
bg = tuple(int(RAUSER_WHITE[i] + (RAUSER_GREY[i] - RAUSER_WHITE[i]) * t) for i in range(3))
canvas = pygame.Surface((320, 240))
canvas.fill(bg)
render_rauser_slide(canvas, parts, 1.0)
blit_scaled_centered(screen, canvas, WINDOW_W, WINDOW_H, bg_color=bg)
quit_requested = wait_or_skip(clock, RAUSER_FADE_TO_GREY_SECONDS, show_fade_grey)
if quit_requested:
return True
_draw_rauser(screen, parts, bg, 1.0)
r = wait_or_skip(clock, RAUSER_FADE_TO_GREY_SECONDS, show_fade_grey)
if r:
return r
def show_hold_grey(_t):
canvas = pygame.Surface((320, 240))
canvas.fill(RAUSER_GREY)
render_rauser_slide(canvas, parts, 1.0)
blit_scaled_centered(screen, canvas, WINDOW_W, WINDOW_H, bg_color=RAUSER_GREY)
return wait_or_skip(clock, RAUSER_GREY_HOLD_SECONDS, show_hold_grey)
return wait_or_skip(clock, RAUSER_GREY_HOLD_SECONDS,
lambda _t: _draw_rauser(screen, parts, RAUSER_GREY, 1.0))
# ---------------------------------------------------------------------------
@@ -591,27 +648,35 @@ def build_title_image(quads):
return canvas
def _scanline_desaturate(surface, intensity):
'''intensity 0..1: 0 = unveraendert, 1 = komplett entsaettigt + jede
zweite Zeile abgedunkelt (Stefans "Scanlines drueber"-Beobachtung waehrend
des Titelbild-Reveals, siehe Docstring). Braucht numpy; ohne numpy wird
intensity ignoriert (Bild bleibt scharf) statt abzustuerzen.'''
if intensity <= 0 or not HAVE_NUMPY:
def _scanline_clear_from_middle(surface, clear_progress):
'''KORREKTUR (Stefan 24.07.2026 -- gegen echte DOSBox-Referenz gemessen):
KEINE Entsaettigung/kein Grau mehr (das "gray raus"), nur noch Scanlines
(jede zweite Zeile abgedunkelt). Die Scanlines loesen sich von der MITTE
des Titelbilds aus auf -- `surface` ist die untere Bildhaelfte, ihre
Zeile 0 liegt genau an der Bildmitte. clear_progress 0..1: der Anteil der
Zeilen (von oben=Mitte aus) der schon scanline-frei ist. 0 = komplett
scanlined, 1 = komplett sauber. Die Scanline-Grenze wandert also von der
Mitte nach unten weg. Braucht numpy; ohne numpy bleibt das Bild sauber
(Effekt entfaellt) statt abzustuerzen.'''
if not HAVE_NUMPY:
return surface
arr = pygame.surfarray.array3d(surface).astype(np.float32) # (w,h,3)
gray = arr.mean(axis=2, keepdims=True)
out = arr * (1 - intensity) + gray * intensity
scan = np.ones((1, arr.shape[1], 1), dtype=np.float32)
scan[0, ::2, 0] = 1.0 - 0.5 * intensity
out = np.clip(out * scan, 0, 255).astype(np.uint8)
arr = pygame.surfarray.array3d(surface).astype(np.float32) # (w, h, 3)
h = arr.shape[1]
scan = np.ones((h,), dtype=np.float32)
scan[1::2] = 0.5 # jede zweite Zeile abgedunkelt (kein Grau, nur dunkler)
clear_rows = int(round(h * max(0.0, min(1.0, clear_progress))))
if clear_rows > 0:
scan[:clear_rows] = 1.0 # obere (mittennahe) Zeilen bereits scanline-frei
out = np.clip(arr * scan[np.newaxis, :, np.newaxis], 0, 255).astype(np.uint8)
return pygame.surfarray.make_surface(out)
def render_title_wipe_frame(quads, t):
'''t: 0..1 ueber die gesamte Wipe-Dauer. Erste TITLE_HEIGHT_REVEAL_FRACTION
waechst Hoehe (oben 0->120 von oben, unten 0->120 von unten). Danach loest
sich der Scanline/Graustufen-Effekt auf der unteren Haelfte auf (Stefans
Screenshot4->5->6-Beobachtung).'''
'''t: 0..1 ueber die gesamte Wipe-Dauer. Erste TITLE_HEIGHT_REVEAL_FRACTION:
Vorhang-Reveal (obere Haelfte waechst 0->120 von oben, untere Haelfte
0->120 von unten, treffen sich in der Mitte). Danach: die Scanlines auf der
unteren Haelfte loesen sich von der Mitte nach unten auf. Obere Haelfte ist
durchgehend volle Farbe ohne Scanlines (wie in der DOSBox-Referenz).'''
a, b, c, d = quads['A'], quads['B'], quads['C'], quads['D']
half_h = a.get_height() # 120
full_w = a.get_width() + b.get_width() # 320
@@ -622,116 +687,118 @@ def render_title_wipe_frame(quads, t):
if t < TITLE_HEIGHT_REVEAL_FRACTION:
reveal = t / TITLE_HEIGHT_REVEAL_FRACTION
h = max(0, int(half_h * reveal))
scan_intensity = 1.0
clear_progress = 0.0 # waehrend des Reveals noch komplett scanlined
else:
h = half_h
scan_intensity = 1.0 - (t - TITLE_HEIGHT_REVEAL_FRACTION) / (1 - TITLE_HEIGHT_REVEAL_FRACTION)
clear_progress = (t - TITLE_HEIGHT_REVEAL_FRACTION) / (1 - TITLE_HEIGHT_REVEAL_FRACTION)
if h > 0:
# obere Haelfte (A|B): volle Farbe, waechst von oben nach unten
top_row = pygame.Surface((full_w, half_h))
top_row.blit(a, (0, 0))
top_row.blit(b, (a.get_width(), 0))
top_slice = top_row.subsurface((0, 0, full_w, h))
canvas.blit(top_slice, (0, 0))
canvas.blit(top_row.subsurface((0, 0, full_w, h)), (0, 0))
# untere Haelfte (C|D): Scanlines, loesen sich von der Mitte (=Zeile 0
# dieser Haelfte) nach unten auf; waechst von unten nach oben rein.
bot_row = pygame.Surface((full_w, half_h))
bot_row.blit(c, (0, 0))
bot_row.blit(d, (a.get_width(), 0))
bot_slice = bot_row.subsurface((0, half_h - h, full_w, h)).copy()
bot_slice = _scanline_desaturate(bot_slice, scan_intensity)
bot_scan = _scanline_clear_from_middle(bot_row, clear_progress)
bot_slice = bot_scan.subsurface((0, half_h - h, full_w, h))
canvas.blit(bot_slice, (0, half_h * 2 - h))
return canvas
def run_once(screen, clock, assets, audio_paths):
quit_requested = run_rauser(screen, clock, assets, audio_paths)
if quit_requested:
return True
def scene_pause(screen, clock, seconds):
'''Kurze schwarze Pause zwischen zwei Screens.'''
return wait_or_skip(clock, seconds, lambda _t: screen.fill(BG_COLOR))
# 2) schwarze Pause
screen.fill(BG_COLOR)
pygame.display.flip()
quit_requested = wait_or_skip(clock, 0.5, lambda t: screen.fill(BG_COLOR))
if quit_requested:
return True
# 3) Factor5 (statisches Bild). KORREKTUR (22.07.2026 abends, siehe
# Docstring): NICHT mehr ONGAME2 (das war falsch zugeordnete Level-
# Musik), sondern TITEL2-Song 1 -- ein kurzer "Brausen"-Sound (~2.4s),
# der zur Beschreibung "nur ein bisschen laenger als der Rauser-Sound"
# passt. Screen-Dauer ist jetzt FEST (~5s), nicht mehr an die (falsche)
# 123s-Songlaenge gekoppelt.
def scene_factor5(screen, clock, assets, audio_paths):
'''Factor5-Studio-Logo (statisches Bild) + TITEL2-Song 1 ("Brausen").
KORREKTUR (Stefan 24.07.2026): der Screen wird EXAKT so lange gezeigt wie
der Sound spielt -- ist er zu Ende, geht es zu Kellogg's weiter. Ohne
Audio-Geraet (headless) faellt es auf FACTOR5_FALLBACK_SECONDS zurueck.'''
factor5 = build_letterboxed(assets.pcc_surface('FACTOR5'))
hold = FACTOR5_FALLBACK_SECONDS
factor5_path = audio_paths.get(('TITEL2', 1))
if pygame.mixer.get_init() and factor5_path:
try:
factor5_sound = pygame.mixer.Sound(factor5_path)
factor5_sound.play()
snd = pygame.mixer.Sound(factor5_path)
# Screen exakt so lange wie der Sound -- aber mit Sicherheits-
# Obergrenze, damit ein evtl. fehlerhaft langes WAV das Intro nicht
# minutenlang einfriert (siehe FACTOR5_MAX_SECONDS).
hold = min(snd.get_length(), FACTOR5_MAX_SECONDS)
snd.play()
except Exception as exc:
print(f'[intro_sequence] Factor5-Sound konnte nicht geladen werden: {exc}', file=sys.stderr)
print(f'[intro] Factor5-Sound konnte nicht geladen werden: {exc}', file=sys.stderr)
return wait_or_skip(clock, hold,
lambda _t: blit_scaled_centered(screen, factor5, WINDOW_W, WINDOW_H))
def show_factor5(_t):
blit_scaled_centered(screen, factor5, WINDOW_W, WINDOW_H)
quit_requested = wait_or_skip(clock, FACTOR5_HOLD_SECONDS, show_factor5)
if quit_requested:
return True
# 4) schwarze Pause
quit_requested = wait_or_skip(clock, 0.5, lambda t: screen.fill(BG_COLOR))
if quit_requested:
return True
# 5) Kellogg's-Logo + "praesentiert"-Palettenfade
# Titelmusik (TITEL Song 0, ~109s) setzt hier ein und laeuft geloopt
# weiter bis zum Ende der Sequenz (Original hat vermutlich durchgehende
# Titelmusik ueber Logo+Titelbild+Karte -- noch nicht per Referenz-
# Sichtung bestaetigt, aber plausibelste Annahme fuers erste v1).
def scene_kelloggs(screen, clock, assets, audio_paths):
'''Kellogg's-Markenlogo; darunter faedet "praesentiert" per Palettenfade
ein. Hier setzt auch die Titelmusik (TITEL Song 0) geloopt ein.'''
titel_path = audio_paths.get(('TITEL', None))
if pygame.mixer.get_init() and titel_path:
try:
pygame.mixer.music.load(titel_path)
pygame.mixer.music.play(-1)
except Exception as exc:
print(f'[intro_sequence] Titelmusik konnte nicht geladen werden: {exc}', file=sys.stderr)
print(f'[intro] Titelmusik konnte nicht geladen werden: {exc}', file=sys.stderr)
kw, kh, kidx, kpal = assets.pcc('KELLOGGS')
def show_kelloggs(t):
frame = build_kelloggs_frame(kw, kh, kidx, kpal, t)
letterboxed = build_letterboxed(frame)
blit_scaled_centered(screen, letterboxed, WINDOW_W, WINDOW_H)
quit_requested = wait_or_skip(clock, PRAESENTIERT_FADE_SECONDS, show_kelloggs)
if quit_requested:
return True
blit_scaled_centered(screen, build_letterboxed(frame), WINDOW_W, WINDOW_H)
r = wait_or_skip(clock, PRAESENTIERT_FADE_SECONDS, show_kelloggs)
if r:
return r
# kurz mit vollem Text stehen lassen
quit_requested = wait_or_skip(clock, 1.0, lambda t: show_kelloggs(1.0))
if quit_requested:
return True
return wait_or_skip(clock, 1.0, lambda _t: show_kelloggs(1.0))
# 6) schwarze Pause
quit_requested = wait_or_skip(clock, 0.5, lambda t: screen.fill(BG_COLOR))
if quit_requested:
return True
# 7) Titelbild -- 2x2-Quadranten-Reveal mit Scanline-Aufloesung (siehe
# Docstring-Korrektur)
def scene_title(screen, clock, assets):
'''Titelbild -- 2x2-Quadranten-Reveal, Scanlines loesen sich von der Mitte
auf (siehe render_title_wipe_frame), danach das fertige Bild halten.'''
quads = build_title_quadrants(assets)
def show_title_wipe(t):
canvas = render_title_wipe_frame(quads, t)
blit_scaled_centered(screen, canvas, WINDOW_W, WINDOW_H)
quit_requested = wait_or_skip(clock, TITLE_WIPE_SECONDS, show_title_wipe)
if quit_requested:
return True
# danach: fertig zusammengesetztes Titelbild (320x240, siehe Korrektur) halten
r = wait_or_skip(clock, TITLE_WIPE_SECONDS,
lambda t: blit_scaled_centered(screen, render_title_wipe_frame(quads, t), WINDOW_W, WINDOW_H))
if r:
return r
title_img = build_title_image(quads)
return wait_or_skip(clock, 3.0,
lambda _t: blit_scaled_centered(screen, title_img, WINDOW_W, WINDOW_H))
def show_title_full(_t):
blit_scaled_centered(screen, title_img, WINDOW_W, WINDOW_H)
quit_requested = wait_or_skip(clock, 3.0, show_title_full)
return quit_requested
def run_once(screen, clock, assets, audio_paths):
'''Spielt die komplette Intro-Sequenz einmal ab. LEERTASTE ueberspringt den
jeweils aktuellen Screen (die Scene liefert dann 'skip' -> es geht sofort
zur naechsten Scene), ESC/Fenster-X beendet das Intro komplett. Gibt True
zurueck, wenn der User ganz beenden will (dann bricht auch die
Endlosschleife in main() ab).'''
scenes = [
lambda: scene_rauser(screen, clock, assets, audio_paths),
lambda: scene_pause(screen, clock, 0.5),
lambda: scene_factor5(screen, clock, assets, audio_paths),
lambda: scene_pause(screen, clock, 0.5),
lambda: scene_kelloggs(screen, clock, assets, audio_paths),
lambda: scene_pause(screen, clock, 0.5),
lambda: scene_title(screen, clock, assets),
]
for scene in scenes:
r = scene()
if r == 'quit':
return True
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
def main():