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:
co-authored by
Claude Opus 4.8
parent
e7d95190eb
commit
7aea2fac26
@@ -55,8 +55,9 @@ python3 -m game # Intro in Endlosschleife (wie Original-Titelbildschi
|
|||||||
python3 -m game --once # ein Durchlauf, dann beenden
|
python3 -m game --once # ein Durchlauf, dann beenden
|
||||||
```
|
```
|
||||||
|
|
||||||
Abbruch jederzeit per `ESC`. Ohne Soundkarte (z.B. headless) läuft das Spiel
|
Steuerung im Intro: **Leertaste** überspringt den aktuellen Screen, **ESC**
|
||||||
stumm weiter statt abzustürzen.
|
beendet das Intro. Ohne Soundkarte (z.B. headless) läuft das Spiel stumm weiter
|
||||||
|
statt abzustürzen.
|
||||||
|
|
||||||
Liegt `PCKELL.DAT` woanders, kann der Pfad per Umgebungsvariable gesetzt werden:
|
Liegt `PCKELL.DAT` woanders, kann der Pfad per Umgebungsvariable gesetzt werden:
|
||||||
|
|
||||||
|
|||||||
+11
-1
@@ -47,8 +47,18 @@ _CACHE_DIR = os.environ.get('KELLOGG_AUDIO_CACHE', os.path.join(_PROJECT_ROOT, '
|
|||||||
|
|
||||||
|
|
||||||
def _ensure_binary():
|
def _ensure_binary():
|
||||||
|
'''Baut render_tfmx, wenn das Binary fehlt ODER aelter ist als eine der
|
||||||
|
Quelldateien. Das "aelter als die Quelle"-Kriterium ist wichtig: nach einem
|
||||||
|
git pull (z.B. dem Song-Index-Feature in render_tfmx.c) blieb sonst ein
|
||||||
|
veraltetes Binary liegen, das falsche/zu lange WAVs rendert -- das hat am
|
||||||
|
24.07.2026 einen 180s-statt-2.4s-Render und damit einen Intro-Hang
|
||||||
|
verursacht (Factor5-Dauer haengt an der Songlaenge).'''
|
||||||
|
srcs = [os.path.join(_PLAYER_DIR, f)
|
||||||
|
for f in ('render_tfmx.c', 'tfmx.h', 'paula.h', 'player_api.h')]
|
||||||
if os.path.exists(_BINARY):
|
if os.path.exists(_BINARY):
|
||||||
return
|
bmtime = os.path.getmtime(_BINARY)
|
||||||
|
if all(not os.path.exists(s) or os.path.getmtime(s) <= bmtime for s in srcs):
|
||||||
|
return # Binary ist aktuell
|
||||||
subprocess.run(['bash', os.path.join(_PLAYER_DIR, 'build.sh')], check=True)
|
subprocess.run(['bash', os.path.join(_PLAYER_DIR, 'build.sh')], check=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+213
-146
@@ -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,
|
Aufruf: python3 -m game [--once] (--once: nach einem Durchlauf beenden,
|
||||||
sonst loopt die Sequenz weiter wie im Original-Titelbildschirm)
|
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 sys
|
||||||
import os
|
import os
|
||||||
@@ -148,6 +157,24 @@ PRAESENTIERT_FADE_SECONDS = 2.5
|
|||||||
# Weichzeichnen) statt smoothscale skalieren -- siehe pcc_surface_scaled().
|
# Weichzeichnen) statt smoothscale skalieren -- siehe pcc_surface_scaled().
|
||||||
ASSET_DISPLAY_SCALE = 0.5
|
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-Timing (siehe Docstring)
|
||||||
RAUSER_SLIDE_SECONDS = 0.9
|
RAUSER_SLIDE_SECONDS = 0.9
|
||||||
RAUSER_HOLD_SECONDS = 1.3
|
RAUSER_HOLD_SECONDS = 1.3
|
||||||
@@ -155,10 +182,21 @@ RAUSER_FADE_TO_GREY_SECONDS = 0.4
|
|||||||
RAUSER_GREY_HOLD_SECONDS = 0.3
|
RAUSER_GREY_HOLD_SECONDS = 0.3
|
||||||
RAUSER_GREY = (73, 73, 73)
|
RAUSER_GREY = (73, 73, 73)
|
||||||
RAUSER_WHITE = (255, 255, 255)
|
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
|
# Factor5-Timing (Stefan 24.07.2026): das Factor5-Logo wird exakt so lange
|
||||||
# abends): feste Anzeigedauer statt (falsch) an eine 123s-Melodie gekoppelt.
|
# gezeigt, wie der zugehoerige Sound (TITEL2 Song 1, ~2.4s) spielt -- ist der
|
||||||
FACTOR5_HOLD_SECONDS = 5.0
|
# 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)
|
# Titelbild-Timing (siehe Docstring)
|
||||||
TITLE_HEIGHT_REVEAL_FRACTION = 0.55 # Anteil der Wipe-Dauer fuer das Hoehenwachstum
|
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):
|
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.
|
'''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()
|
start = time.monotonic()
|
||||||
while True:
|
while True:
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
@@ -310,12 +354,15 @@ def wait_or_skip(clock, seconds, screen_update_fn, fps=30):
|
|||||||
if t >= 1.0:
|
if t >= 1.0:
|
||||||
screen_update_fn(1.0)
|
screen_update_fn(1.0)
|
||||||
pygame.display.flip()
|
pygame.display.flip()
|
||||||
return False
|
return None
|
||||||
for event in pygame.event.get():
|
for event in pygame.event.get():
|
||||||
if event.type == pygame.QUIT:
|
if event.type == pygame.QUIT:
|
||||||
return True
|
return 'quit'
|
||||||
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
|
if event.type == pygame.KEYDOWN:
|
||||||
return True
|
if event.key == pygame.K_ESCAPE:
|
||||||
|
return 'quit'
|
||||||
|
if event.key == pygame.K_SPACE:
|
||||||
|
return 'skip'
|
||||||
screen_update_fn(t)
|
screen_update_fn(t)
|
||||||
pygame.display.flip()
|
pygame.display.flip()
|
||||||
clock.tick(fps)
|
clock.tick(fps)
|
||||||
@@ -446,26 +493,43 @@ def build_kelloggs_frame(w, h, indices, palette, fade_t):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def build_rauser_parts(assets):
|
def build_rauser_parts(assets):
|
||||||
'''Liefert die drei (bei 0.5x Nearest-Neighbor-Groesse, siehe Docstring-
|
'''Liefert die drei Rauser-Teile bei RAUSER_LOGO_SCALE (Nearest-Neighbor-
|
||||||
Korrektur v3) Rauser-Teile + ihre Endpositionen auf einer 320x240-
|
UPSCALE direkt aus der Nativaufloesung, siehe Docstring-Korrektur #3) +
|
||||||
Leinwand (RAUSER1 links, RAUSER3 rechts daneben, RAUSER2 darunter
|
ihre Endpositionen direkt in FENSTER-Koordinaten (960x720). RAUSER1
|
||||||
mittig).'''
|
("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
|
# cut_border_bg=True (Fix 22.07.2026 abends): macht den papierweissen
|
||||||
# Hintergrund um jede Box transparent (nur die vom Bildrand aus
|
# Hintergrund um jede Box transparent (nur die vom Bildrand aus
|
||||||
# zusammenhaengende Flaeche, siehe make_border_bg_transparent) -- vorher
|
# zusammenhaengende Flaeche, siehe make_border_bg_transparent) -- vorher
|
||||||
# blieb hier ein harter weisser Kasten stehen, der beim Fade zu Grau nicht
|
# blieb hier ein harter weisser Kasten stehen, der beim Fade zu Grau nicht
|
||||||
# mitgefadet ist (Stefans Meldung "Text bleibt hart umrandet stehen").
|
# mitgefadet ist (Stefans Meldung "Text bleibt hart umrandet stehen").
|
||||||
r1 = assets.pcc_surface_scaled('RAUSER1', cut_border_bg=True) # "RAUSER"-Box
|
# scale=RAUSER_LOGO_SCALE ist ein UPSCALE der nativen Assets -> pcc_surface_scaled
|
||||||
r2 = assets.pcc_surface_scaled('RAUSER2', cut_border_bg=True) # "ADVERTAINMENT"
|
# macht cut_border auf der Nativ-Surface und skaliert dann per Nearest hoch,
|
||||||
r3 = assets.pcc_surface_scaled('RAUSER3', cut_border_bg=True) # "!" auf blauer Box
|
# 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()
|
total_h = r1.get_height() + r2.get_height()
|
||||||
x0 = (320 - total_w) // 2
|
x0 = (WINDOW_W - total_w) // 2
|
||||||
y0 = (240 - total_h) // 2
|
y0 = (WINDOW_H - total_h) // 2
|
||||||
|
|
||||||
r1_final = (x0, y0)
|
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())
|
r2_final = (x0, y0 + r1.get_height())
|
||||||
return {
|
return {
|
||||||
'r1': r1, 'r2': r2, 'r3': r3,
|
'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))
|
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
|
'''Rauser-Karte: weisser Grund, Slide-in, Sound-Sting bei Ankunft, Fade zu
|
||||||
grauem Grund, kurz halten. Gibt True zurueck wenn der User abbrechen will.
|
grauem Grund, kurz halten. Rueckgabe 'quit'/'skip'/None (siehe wait_or_skip).
|
||||||
audio_paths kommt aus preload_all_audio() -- das Audio wird hier NICHT
|
Wird jetzt DIREKT in Fensteraufloesung gerendert (kein 320er-Zwischen-
|
||||||
mehr live gerendert (siehe render_audio_with_loading-Docstring zum
|
canvas + Upscale mehr) -- das war die Ursache des Rauser-Aufloesungs-
|
||||||
Hang-Bug, den das behebt), sondern nur noch aus dem fertigen WAV-Pfad
|
Problems (siehe RAUSER_LOGO_SCALE-Kommentar).
|
||||||
geladen (schnell, nie blockierend).'''
|
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)
|
parts = build_rauser_parts(assets)
|
||||||
sting = None
|
sting = None
|
||||||
# TITEL2 Song 0 -- der kuerzere der beiden TITEL2-Songs (~2.04s), Song 1
|
# 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:
|
try:
|
||||||
sting = pygame.mixer.Sound(sting_path)
|
sting = pygame.mixer.Sound(sting_path)
|
||||||
except Exception as exc:
|
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):
|
def show_slide(t):
|
||||||
nonlocal sting_played
|
_draw_rauser(screen, parts, RAUSER_WHITE, t)
|
||||||
canvas = pygame.Surface((320, 240))
|
if t >= 1.0 and not sting_played[0]:
|
||||||
canvas.fill(RAUSER_WHITE)
|
sting_played[0] = True
|
||||||
render_rauser_slide(canvas, parts, t)
|
|
||||||
if t >= 1.0 and not sting_played:
|
|
||||||
sting_played = True
|
|
||||||
if sting is not None:
|
if sting is not None:
|
||||||
sting.play()
|
sting.play()
|
||||||
blit_scaled_centered(screen, canvas, WINDOW_W, WINDOW_H, bg_color=RAUSER_WHITE)
|
r = wait_or_skip(clock, RAUSER_SLIDE_SECONDS, show_slide)
|
||||||
quit_requested = wait_or_skip(clock, RAUSER_SLIDE_SECONDS, show_slide)
|
if r:
|
||||||
if quit_requested:
|
return r
|
||||||
return True
|
|
||||||
|
|
||||||
# falls die Slide-Phase zu kurz war um sting_played zu triggern (sollte
|
# falls die Slide-Phase zu kurz war um den Sting zu triggern
|
||||||
# durch t>=1.0 im letzten Frame von wait_or_skip immer der Fall sein)
|
if not sting_played[0] and sting is not None:
|
||||||
if not sting_played and sting is not None:
|
|
||||||
sting.play()
|
sting.play()
|
||||||
sting_played = True
|
sting_played[0] = True
|
||||||
|
|
||||||
def show_hold_white(_t):
|
r = wait_or_skip(clock, RAUSER_HOLD_SECONDS,
|
||||||
canvas = pygame.Surface((320, 240))
|
lambda _t: _draw_rauser(screen, parts, RAUSER_WHITE, 1.0))
|
||||||
canvas.fill(RAUSER_WHITE)
|
if r:
|
||||||
render_rauser_slide(canvas, parts, 1.0)
|
return r
|
||||||
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
|
|
||||||
|
|
||||||
def show_fade_grey(t):
|
def show_fade_grey(t):
|
||||||
bg = tuple(int(RAUSER_WHITE[i] + (RAUSER_GREY[i] - RAUSER_WHITE[i]) * t) for i in range(3))
|
bg = tuple(int(RAUSER_WHITE[i] + (RAUSER_GREY[i] - RAUSER_WHITE[i]) * t) for i in range(3))
|
||||||
canvas = pygame.Surface((320, 240))
|
_draw_rauser(screen, parts, bg, 1.0)
|
||||||
canvas.fill(bg)
|
r = wait_or_skip(clock, RAUSER_FADE_TO_GREY_SECONDS, show_fade_grey)
|
||||||
render_rauser_slide(canvas, parts, 1.0)
|
if r:
|
||||||
blit_scaled_centered(screen, canvas, WINDOW_W, WINDOW_H, bg_color=bg)
|
return r
|
||||||
quit_requested = wait_or_skip(clock, RAUSER_FADE_TO_GREY_SECONDS, show_fade_grey)
|
|
||||||
if quit_requested:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def show_hold_grey(_t):
|
return wait_or_skip(clock, RAUSER_GREY_HOLD_SECONDS,
|
||||||
canvas = pygame.Surface((320, 240))
|
lambda _t: _draw_rauser(screen, parts, RAUSER_GREY, 1.0))
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -591,27 +648,35 @@ def build_title_image(quads):
|
|||||||
return canvas
|
return canvas
|
||||||
|
|
||||||
|
|
||||||
def _scanline_desaturate(surface, intensity):
|
def _scanline_clear_from_middle(surface, clear_progress):
|
||||||
'''intensity 0..1: 0 = unveraendert, 1 = komplett entsaettigt + jede
|
'''KORREKTUR (Stefan 24.07.2026 -- gegen echte DOSBox-Referenz gemessen):
|
||||||
zweite Zeile abgedunkelt (Stefans "Scanlines drueber"-Beobachtung waehrend
|
KEINE Entsaettigung/kein Grau mehr (das "gray raus"), nur noch Scanlines
|
||||||
des Titelbild-Reveals, siehe Docstring). Braucht numpy; ohne numpy wird
|
(jede zweite Zeile abgedunkelt). Die Scanlines loesen sich von der MITTE
|
||||||
intensity ignoriert (Bild bleibt scharf) statt abzustuerzen.'''
|
des Titelbilds aus auf -- `surface` ist die untere Bildhaelfte, ihre
|
||||||
if intensity <= 0 or not HAVE_NUMPY:
|
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
|
return surface
|
||||||
arr = pygame.surfarray.array3d(surface).astype(np.float32) # (w,h,3)
|
arr = pygame.surfarray.array3d(surface).astype(np.float32) # (w, h, 3)
|
||||||
gray = arr.mean(axis=2, keepdims=True)
|
h = arr.shape[1]
|
||||||
out = arr * (1 - intensity) + gray * intensity
|
scan = np.ones((h,), dtype=np.float32)
|
||||||
scan = np.ones((1, arr.shape[1], 1), dtype=np.float32)
|
scan[1::2] = 0.5 # jede zweite Zeile abgedunkelt (kein Grau, nur dunkler)
|
||||||
scan[0, ::2, 0] = 1.0 - 0.5 * intensity
|
clear_rows = int(round(h * max(0.0, min(1.0, clear_progress))))
|
||||||
out = np.clip(out * scan, 0, 255).astype(np.uint8)
|
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)
|
return pygame.surfarray.make_surface(out)
|
||||||
|
|
||||||
|
|
||||||
def render_title_wipe_frame(quads, t):
|
def render_title_wipe_frame(quads, t):
|
||||||
'''t: 0..1 ueber die gesamte Wipe-Dauer. Erste TITLE_HEIGHT_REVEAL_FRACTION
|
'''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
|
Vorhang-Reveal (obere Haelfte waechst 0->120 von oben, untere Haelfte
|
||||||
sich der Scanline/Graustufen-Effekt auf der unteren Haelfte auf (Stefans
|
0->120 von unten, treffen sich in der Mitte). Danach: die Scanlines auf der
|
||||||
Screenshot4->5->6-Beobachtung).'''
|
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']
|
a, b, c, d = quads['A'], quads['B'], quads['C'], quads['D']
|
||||||
half_h = a.get_height() # 120
|
half_h = a.get_height() # 120
|
||||||
full_w = a.get_width() + b.get_width() # 320
|
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:
|
if t < TITLE_HEIGHT_REVEAL_FRACTION:
|
||||||
reveal = t / TITLE_HEIGHT_REVEAL_FRACTION
|
reveal = t / TITLE_HEIGHT_REVEAL_FRACTION
|
||||||
h = max(0, int(half_h * reveal))
|
h = max(0, int(half_h * reveal))
|
||||||
scan_intensity = 1.0
|
clear_progress = 0.0 # waehrend des Reveals noch komplett scanlined
|
||||||
else:
|
else:
|
||||||
h = half_h
|
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:
|
if h > 0:
|
||||||
|
# obere Haelfte (A|B): volle Farbe, waechst von oben nach unten
|
||||||
top_row = pygame.Surface((full_w, half_h))
|
top_row = pygame.Surface((full_w, half_h))
|
||||||
top_row.blit(a, (0, 0))
|
top_row.blit(a, (0, 0))
|
||||||
top_row.blit(b, (a.get_width(), 0))
|
top_row.blit(b, (a.get_width(), 0))
|
||||||
top_slice = top_row.subsurface((0, 0, full_w, h))
|
canvas.blit(top_row.subsurface((0, 0, full_w, h)), (0, 0))
|
||||||
canvas.blit(top_slice, (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 = pygame.Surface((full_w, half_h))
|
||||||
bot_row.blit(c, (0, 0))
|
bot_row.blit(c, (0, 0))
|
||||||
bot_row.blit(d, (a.get_width(), 0))
|
bot_row.blit(d, (a.get_width(), 0))
|
||||||
bot_slice = bot_row.subsurface((0, half_h - h, full_w, h)).copy()
|
bot_scan = _scanline_clear_from_middle(bot_row, clear_progress)
|
||||||
bot_slice = _scanline_desaturate(bot_slice, scan_intensity)
|
bot_slice = bot_scan.subsurface((0, half_h - h, full_w, h))
|
||||||
canvas.blit(bot_slice, (0, half_h * 2 - h))
|
canvas.blit(bot_slice, (0, half_h * 2 - h))
|
||||||
|
|
||||||
return canvas
|
return canvas
|
||||||
|
|
||||||
|
|
||||||
def run_once(screen, clock, assets, audio_paths):
|
def scene_pause(screen, clock, seconds):
|
||||||
quit_requested = run_rauser(screen, clock, assets, audio_paths)
|
'''Kurze schwarze Pause zwischen zwei Screens.'''
|
||||||
if quit_requested:
|
return wait_or_skip(clock, seconds, lambda _t: screen.fill(BG_COLOR))
|
||||||
return True
|
|
||||||
|
|
||||||
# 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
|
def scene_factor5(screen, clock, assets, audio_paths):
|
||||||
# Docstring): NICHT mehr ONGAME2 (das war falsch zugeordnete Level-
|
'''Factor5-Studio-Logo (statisches Bild) + TITEL2-Song 1 ("Brausen").
|
||||||
# Musik), sondern TITEL2-Song 1 -- ein kurzer "Brausen"-Sound (~2.4s),
|
KORREKTUR (Stefan 24.07.2026): der Screen wird EXAKT so lange gezeigt wie
|
||||||
# der zur Beschreibung "nur ein bisschen laenger als der Rauser-Sound"
|
der Sound spielt -- ist er zu Ende, geht es zu Kellogg's weiter. Ohne
|
||||||
# passt. Screen-Dauer ist jetzt FEST (~5s), nicht mehr an die (falsche)
|
Audio-Geraet (headless) faellt es auf FACTOR5_FALLBACK_SECONDS zurueck.'''
|
||||||
# 123s-Songlaenge gekoppelt.
|
|
||||||
factor5 = build_letterboxed(assets.pcc_surface('FACTOR5'))
|
factor5 = build_letterboxed(assets.pcc_surface('FACTOR5'))
|
||||||
|
hold = FACTOR5_FALLBACK_SECONDS
|
||||||
factor5_path = audio_paths.get(('TITEL2', 1))
|
factor5_path = audio_paths.get(('TITEL2', 1))
|
||||||
if pygame.mixer.get_init() and factor5_path:
|
if pygame.mixer.get_init() and factor5_path:
|
||||||
try:
|
try:
|
||||||
factor5_sound = pygame.mixer.Sound(factor5_path)
|
snd = pygame.mixer.Sound(factor5_path)
|
||||||
factor5_sound.play()
|
# 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:
|
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
|
def scene_kelloggs(screen, clock, assets, audio_paths):
|
||||||
quit_requested = wait_or_skip(clock, 0.5, lambda t: screen.fill(BG_COLOR))
|
'''Kellogg's-Markenlogo; darunter faedet "praesentiert" per Palettenfade
|
||||||
if quit_requested:
|
ein. Hier setzt auch die Titelmusik (TITEL Song 0) geloopt ein.'''
|
||||||
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).
|
|
||||||
titel_path = audio_paths.get(('TITEL', None))
|
titel_path = audio_paths.get(('TITEL', None))
|
||||||
if pygame.mixer.get_init() and titel_path:
|
if pygame.mixer.get_init() and titel_path:
|
||||||
try:
|
try:
|
||||||
pygame.mixer.music.load(titel_path)
|
pygame.mixer.music.load(titel_path)
|
||||||
pygame.mixer.music.play(-1)
|
pygame.mixer.music.play(-1)
|
||||||
except Exception as exc:
|
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')
|
kw, kh, kidx, kpal = assets.pcc('KELLOGGS')
|
||||||
|
|
||||||
def show_kelloggs(t):
|
def show_kelloggs(t):
|
||||||
frame = build_kelloggs_frame(kw, kh, kidx, kpal, t)
|
frame = build_kelloggs_frame(kw, kh, kidx, kpal, t)
|
||||||
letterboxed = build_letterboxed(frame)
|
blit_scaled_centered(screen, build_letterboxed(frame), WINDOW_W, WINDOW_H)
|
||||||
blit_scaled_centered(screen, letterboxed, WINDOW_W, WINDOW_H)
|
r = wait_or_skip(clock, PRAESENTIERT_FADE_SECONDS, show_kelloggs)
|
||||||
quit_requested = wait_or_skip(clock, PRAESENTIERT_FADE_SECONDS, show_kelloggs)
|
if r:
|
||||||
if quit_requested:
|
return r
|
||||||
return True
|
|
||||||
# kurz mit vollem Text stehen lassen
|
# kurz mit vollem Text stehen lassen
|
||||||
quit_requested = wait_or_skip(clock, 1.0, lambda t: show_kelloggs(1.0))
|
return wait_or_skip(clock, 1.0, lambda _t: show_kelloggs(1.0))
|
||||||
if quit_requested:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 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
|
def scene_title(screen, clock, assets):
|
||||||
# Docstring-Korrektur)
|
'''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)
|
quads = build_title_quadrants(assets)
|
||||||
|
r = wait_or_skip(clock, TITLE_WIPE_SECONDS,
|
||||||
def show_title_wipe(t):
|
lambda t: blit_scaled_centered(screen, render_title_wipe_frame(quads, t), WINDOW_W, WINDOW_H))
|
||||||
canvas = render_title_wipe_frame(quads, t)
|
if r:
|
||||||
blit_scaled_centered(screen, canvas, WINDOW_W, WINDOW_H)
|
return r
|
||||||
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
|
|
||||||
title_img = build_title_image(quads)
|
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)
|
def run_once(screen, clock, assets, audio_paths):
|
||||||
quit_requested = wait_or_skip(clock, 3.0, show_title_full)
|
'''Spielt die komplette Intro-Sequenz einmal ab. LEERTASTE ueberspringt den
|
||||||
return quit_requested
|
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():
|
def main():
|
||||||
|
|||||||
Reference in New Issue
Block a user