diff --git a/NOTES.md b/NOTES.md index 56e3a32..261e524 100644 --- a/NOTES.md +++ b/NOTES.md @@ -948,3 +948,106 @@ kompilierte Binary -- Build-Artefakt, per `.gitignore` draussen wie 3. ARE/Kollisionsformat klaeren, wenn Spiellogik es braucht. Laeuft selbstaendig weiter, keine Rueckfrage noetig. + +## Pfade relativ gemacht + Rauser/Titelbild-Korrektur (22.07.2026, Nacht) + +Stefan meldete zwei Dinge: (1) alle Tools haben `/home/aria/kellogg_remake/...` +hardcodiert -- muss relativ zum Projektordner sein; (2) 6 Screenshots als +Beleg fuer Bild-Bugs (Rauser zu gross, Titelbild "gestaucht", Original hat +Scanlines die verschwinden). **Wichtig: von den 6 Screenshots ist wegen eines +Upload-Bugs (alle 6 landeten unter demselben Dateinamen und ueberschrieben +sich gegenseitig) nur der LETZTE erhalten geblieben** -- ich hab Stefan das +gemeldet und ihn gebeten, die anderen 5 bei Bedarf nochmal zu schicken. Statt +zu raten hab ich stattdessen selbst frische DOSBox-Referenzaufnahmen gemacht +(Xvfb + `import -window root`, 0.12s-Intervall) und daraus die echten Werte +gemessen -- Details unten. + +**1) Pfade:** `intro_sequence.py`, `tfmx_audio.py`, `render_assets.py`, +`dat_extract.py` hatten `/home/aria/kellogg_remake/...` fest im Code. Jetzt +ueberall `PROJECT_ROOT = dirname(dirname(abspath(__file__)))` + optionales +ENV-Override (`KELLOGG_DAT`, `KELLOGG_AUDIO_CACHE`, `KELLOGG_RENDER_OUT`, +`KELLOGG_EXTRACT_OUT`). Laeuft jetzt von jedem Pfad aus (VM, Container, +woanders geklont). + +**2) Rauser -- per Pixelvermessung neu gebaut:** +- Echte Groesse per Bounding-Box-Analyse gegen eine frische DOSBox-Aufnahme + gemessen: das komplette Rauser-Logo (RAUSER1+2+3 zusammen) ist im Original + nur ca. 105x40px auf einer 320x240-Leinwand -- mein v1-Code hat die + PCC-Assets 1:1 nativ gezeichnet (182x46 fuer RAUSER1 allein!), viel zu + gross. **Alle drei Rauser-Assets werden jetzt bei 0.5x Nativgroesse + gezeichnet** (`ASSET_DISPLAY_SCALE`). +- Hintergrund war in v1 WEISS die ganze Zeit -- tatsaechlich: WEISS waehrend + Slide-in+Halten, dann kurzer Fade zu GRAU (73,73,73), dann Grau halten, + dann Schwarzblende (bestehende Pause). +- **Neu: Slide-in-Animation.** Per Screenshot-Serie entdeckt: RAUSER1 + ("RAUSER"-Box) faehrt von LINKS ins Bild, RAUSER3 (blaue Box mit "!") + faehrt von OBEN rein, RAUSER2 ("ADVERTAINMENT") steht von Anfang an fest. + Nachgebaut mit smoothstep-Ease ueber `RAUSER_SLIDE_SECONDS=0.9s`. +- **Neu: Sound-Sting.** `TITEL2.TFX/.SAM` (das kuerzeste der 3 Musikstuecke, + 724 Byte TFX) ist kein Loop-Track, sondern ein kurzer ~1s-Jingle (RMS- + Analyse zeigt: Burst 0-0.9s, dann Stille, dann Wiederholung -- typisch fuer + ein kurzes Sample, das die Tracker-Engine in einer Schleife rendert). Wird + jetzt exakt beim Erreichen der Endposition (Slide-in fertig) EINMAL + abgespielt (`pygame.mixer.Sound`, nicht geloopt). + +**3) Titelbild -- KORREKTUR einer fruehen Fehlannahme (das war der Bug hinter +Stefans "gestaucht"-Meldung):** +- v1 hat die 4 Quadranten KELL256A/B/C/D (je 160x240 PCC-nativ) 1:1 zu einem + 320x**480**-Bild gestapelt (A|B oben, C|D unten) und das komplett 480 hoch + angezeigt. Das war falsch. +- Per Pixel-Rekonstruktion gegen eine echte DOSBox-Aufnahme verifiziert + (mehrere Hypothesen durchprobiert, mit numpy MSE + visuellem Abgleich): + **die 4 Quadranten werden je auf HALBE Hoehe (160x120) skaliert** (Breite + bleibt nativ, NUR die Hoehe wird halbiert -- anders als beim Rauser-Fix, + wo beide Achsen gleichmaessig skaliert werden!) und im 2x2-Raster + angeordnet: A oben-links, B oben-rechts, C unten-links, D unten-rechts -> + ergibt ein korrektes 320x240-Bild. Screenshot-Vergleich (Rekonstruktion + vs. echte DOSBox-Aufnahme) war praktisch deckungsgleich. + ACHTUNG fuer kuenftige Aenderungen: bei den Rauser-Assets ist die + 0.5x-Skalierung GLEICHMAESSIG (Breite+Hoehe), bei den Titelbild-Quadranten + ist es NUR die Hoehe -- zwei verschiedene Funktionen im Code + (`pcc_surface_scaled` vs. `pcc_surface_half_height`), nicht verwechseln. +- **Neu entdeckter Scanline-Effekt** (das ist genau Stefans "Screenshot 4/5/6" + -Beobachtung): waehrend die untere Bildhaelfte einwaechst, ist sie zuerst + komplett entsaettigt (graustichig) UND mit sichtbaren horizontalen + Scanlines ueberzogen -- das loest sich in einer zweiten Animationsphase zu + vollen Farben auf, bis das Bild komplett scharf ist. Per 0.12s-Screenshot- + Serie ueber die komplette Wipe-Animation hinweg entdeckt und nachgebaut + (`_scanline_desaturate()`, per numpy: Graustufen-Mix + jede zweite Zeile + abgedunkelt, Intensitaet faedet ueber die zweite Haelfte der Wipe-Dauer von + 1.0 auf 0.0). +- Reveal-Mechanik selbst (oben waechst 0->120 von oben, unten waechst 0->120 + von unten, treffen sich in der Mitte) war in der urspruenglichen + Beobachtung schon richtig -- nur die falsche 480px-Endgroesse und der + fehlende Scanline-Effekt waren der Bug. + +**Verifiziert:** eigene DOSBox-Referenzaufnahme (Xvfb, 0.12s-Serie) gegen den +neu gebauten `intro_sequence.py`-Output verglichen -- Rauser-Slide-in, Rauser- +Groesse/Farben und Titelbild (inkl. Scanline-Phase) sehen jetzt sehr nah am +Original aus. Screenshots liegen als Beleg bei: + +[FILE: /shared/uploads/aria_rauser_fixed.png] (Rauser, neu: klein, weiss, Slide-in-Mitte) +[FILE: /shared/uploads/aria_title_wipe_scanline_fixed.png] (Titelbild-Wipe MIT Scanline-Effekt) +[FILE: /shared/uploads/aria_title_final_fixed.png] (Titelbild komplett, korrekt 320x240 statt 320x480) + +**Noch offen / bewusste Vereinfachungen:** +- Die feine Choreografie von RAUSER3 (slidet vermutlich als "!" und blaue Box + getrennt, siehe Screenshot-Beobachtung) wurde vereinfacht zu EINEM Element, + das von oben reinfaehrt -- sieht in der Praxis schon sehr nah dran aus, + waere aber bei Bedarf noch weiter verfeinerbar. +- Kleiner kosmetischer Rest: RAUSER2/RAUSER3 haben weissen Hintergrund IN + ihrer eigenen PCC-Bounding-Box (kein Alpha-Kanal). Auf weiss (waehrend + Slide-in+Halten) faellt das nicht auf, nach dem Fade zu Grau sieht man + einen schwachen weissen Kasten drumrum. Versuch mit `set_colorkey()` + wieder verworfen (hat auch die weissen BUCHSTABEN in RAUSER1 durchsichtig + gemacht -- schlimmer als der Kasten). Braucht eine echte Alpha-Maske pro + Asset, low priority. +- Der Scanline-Clear-Effekt ist als gleichmaessige Intensitaet ueber die + ganze untere Haelfte umgesetzt, nicht als praezise von-der-Mitte-nach- + aussen wandernde Grenze (im Original loest sich ein duenner Reststreifen + am unteren Bildrand als letztes auf) -- optische Wirkung ist aber schon + sehr aehnlich. +- VM-Testverzeichnisse (`boot_frames2/`, `intro_check_frames/`, + `title_check_frames/`) nach der Analyse wieder aufgeraeumt, nicht im Repo. + +Laeuft selbstaendig weiter, keine Rueckfrage noetig. diff --git a/tools/dat_extract.py b/tools/dat_extract.py index 9af4b36..594cb9e 100644 --- a/tools/dat_extract.py +++ b/tools/dat_extract.py @@ -1,8 +1,9 @@ import struct, os from collections import Counter -SRC = '/home/aria/kellogg_remake/raw/PCKELL.DAT' -OUT = '/home/aria/kellogg_remake/extracted_dat' +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SRC = os.environ.get('KELLOGG_DAT', os.path.join(PROJECT_ROOT, 'raw', 'PCKELL.DAT')) +OUT = os.environ.get('KELLOGG_EXTRACT_OUT', os.path.join(PROJECT_ROOT, 'extracted_dat')) os.makedirs(OUT, exist_ok=True) with open(SRC, 'rb') as f: diff --git a/tools/intro_sequence.py b/tools/intro_sequence.py index 7dc711b..3656295 100644 --- a/tools/intro_sequence.py +++ b/tools/intro_sequence.py @@ -4,27 +4,43 @@ Boot-/Intro-Sequenz von "Tony & Friends in Kellogg's Land" -- erster spielbarer (bzw. hier: zuschaubarer) pygame-Meilenstein des Remakes. Reihenfolge + Timing wurde am 22.07.2026 durch eine echte DOSBox-Screenshot- -Serie (0.3s-Intervall) waehrend des Originalboots vermessen, siehe NOTES.md -Abschnitt "Boot-Sequenz vermessen". Kurzfassung: +Serie vermessen, siehe NOTES.md Abschnitt "Boot-Sequenz vermessen". Kurzfassung: - 1. Rauser-Advertainment-Karte (Komposit aus RAUSER1+2+3.PCC), ~3s + 1. Rauser-Advertainment-Karte (Komposit aus RAUSER1+2+3.PCC) auf WEISSEM + Grund. RAUSER1 ("RAUSER"-Box) faehrt von LINKS ein, RAUSER3 (blaue Box + mit "!") faehrt von OBEN ein, RAUSER2 ("ADVERTAINMENT") steht von Anfang + an fest. Sobald beide Teile ihre Endposition erreicht haben, spielt ein + kurzer Sound-Sting (TITEL2.TFX/.SAM -- das kuerzeste der 3 Musikstuecke, + genau ein "Bling"-Jingle, kein Loop). Danach faedet der WEISSE + Hintergrund kurz zu GRAU um (NICHT weiss -- war ein Fehler in v1!), haelt + kurz, dann schwarze Pause. WICHTIG: alle Rauser-Assets werden nur bei + **halber Pixel-Groesse** (0.5x) ihrer PCC-Nativaufloesung gezeichnet -- + bei 1:1 sind sie in der echten DOSBox-Aufnahme klar zu groß (per + Pixel-Vermessung bestaetigt, siehe NOTES.md "Rauser/Titelbild-Korrektur"). 2. kurze schwarze Pause 3. Factor5-Studio-Logo (FACTOR5.PCC, STATISCHES Bild -- keine Animation, das "Geister"-Doppellogo ist Teil des Assets selbst), ~6s 4. kurze schwarze Pause 5. Kellogg's-Markenlogo (KELLOGGS.PCC) erscheint SOFORT komplett; darunter faedet der Text "praesentiert" per Palette-Fade von blass nach saettigt- - rot ein (~3s). WICHTIG: das ist NICHT der vom Stefan vermutete "Vorhang"- - Effekt auf dem Logo selbst -- das Logo ist die ganze Zeit scharf/komplett - da, nur der Text darunter faedet. + rot ein (~3s). 6. kurze schwarze Pause - 7. Titelbild "TONY & FRIENDS in Kellogg's Land" -- DAS ist der eigentliche - Vorhang-Effekt: das Bild ist intern 320x480 (doppelt so hoch wie die - anderen 320x200/240-Screens!), zusammengesetzt aus den 4 Quadranten - KELL256A (oben links) / B (oben rechts) / C (unten links) / D (unten - rechts), je 160x240. Obere Haelfte (A|B) faehrt von oben nach unten ein, - untere Haelfte (C|D) von unten nach oben -- wie ein Theatervorhang, nur - vertikal, beide Haelften treffen sich in der Bildmitte. Dauer ~3s. + 7. Titelbild "TONY & FRIENDS in Kellogg's Land" -- **KORREKTUR (22.07.2026, + spaeter Abend): das Bild ist NICHT 320x480!** Die 4 Quadranten KELL256A + (oben links) / B (oben rechts) / C (unten links) / D (unten rechts) sind + zwar je 160x240 PCC-Nativaufloesung, werden aber -- exakt wie die + Rauser-Assets -- nur bei **halber Groesse** (160x120) in ein 2x2-Raster + gezeichnet: A|B oben, C|D unten -> 320x240 Gesamtbild. Per Pixel- + Rekonstruktion gegen eine echte DOSBox-Aufnahme verifiziert (nicht nur + behauptet). Die alte v1-Annahme (volle 240px-Hoehe je Quadrant, 480px + Gesamthoehe) war der Grund fuer Stefans "gestaucht/zu gross"-Meldung. + Reveal-Effekt: obere Haelfte (A|B) waechst von oben (Hoehe 0->120), + untere Haelfte (C|D) waechst von unten (Hoehe 0->120) -- treffen sich in + der Mitte. ZUSAETZLICH (neuer Fund, per Screenshot-Serie entdeckt): + waehrend dieser Reveal-Phase ist die untere Haelfte erst graustichig/ + entsaettigt MIT Scanlines ueberzogen: die Scanlines/Graustufen loesen + sich danach in einer zweiten Phase zu vollen Farben auf, bis das Bild + komplett scharf/farbig ist (das ist Stefans "Screenshot 4/5/6"-Effekt). Alle Nicht-BOB/ICO/MAP-Assets werden per kellogg_formats.load_pcc() (Pillows PCX-Decoder) aus PCKELL.DAT gelesen -- kein eigener RLE-Code mehr (siehe @@ -49,7 +65,14 @@ import tfmx_audio import pygame -DAT_PATH = os.environ.get('KELLOGG_DAT', '/home/aria/kellogg_remake/raw/PCKELL.DAT') +try: + import numpy as np + HAVE_NUMPY = True +except ImportError: + HAVE_NUMPY = False + +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DAT_PATH = os.environ.get('KELLOGG_DAT', os.path.join(PROJECT_ROOT, 'raw', 'PCKELL.DAT')) WINDOW_W, WINDOW_H = 960, 720 BG_COLOR = (0, 0, 0) @@ -58,6 +81,23 @@ BG_COLOR = (0, 0, 0) PRAESENTIERT_FADE_INDICES = [34, 42, 43, 44, 45] PRAESENTIERT_FADE_SECONDS = 2.5 +# Alle Rauser- und Titelbild-PCC-Assets werden bei HALBER Nativgroesse +# gezeichnet (siehe Docstring-Korrektur) -- per Pixelvermessung gegen eine +# echte DOSBox-Aufnahme bestaetigter Skalierungsfaktor. +ASSET_DISPLAY_SCALE = 0.5 + +# Rauser-Timing (siehe Docstring) +RAUSER_SLIDE_SECONDS = 0.9 +RAUSER_HOLD_SECONDS = 1.3 +RAUSER_FADE_TO_GREY_SECONDS = 0.4 +RAUSER_GREY_HOLD_SECONDS = 0.3 +RAUSER_GREY = (73, 73, 73) +RAUSER_WHITE = (255, 255, 255) + +# Titelbild-Timing (siehe Docstring) +TITLE_HEIGHT_REVEAL_FRACTION = 0.55 # Anteil der Wipe-Dauer fuer das Hoehenwachstum +TITLE_WIPE_SECONDS = 2.3 + def indices_to_surface(width, height, indices, palette): '''Baut ein pygame.Surface (RGB) aus Palette-Indizes + (r,g,b)-Liste.''' @@ -81,18 +121,39 @@ class Assets: w, h, indices, palette = self.pcc(name) return indices_to_surface(w, h, indices, palette) + def pcc_surface_scaled(self, name, scale=ASSET_DISPLAY_SCALE): + '''Skaliert BEIDE Achsen um scale -- fuer die Rauser-Assets (dort per + Pixelvermessung als gleichmaessige 0.5x-Verkleinerung bestaetigt).''' + surf = self.pcc_surface(name) + w, h = surf.get_size() + new_size = (max(1, round(w * scale)), max(1, round(h * scale))) + return pygame.transform.smoothscale(surf, new_size) + + def pcc_surface_half_height(self, name): + '''Skaliert NUR die Hoehe um 0.5, Breite bleibt nativ -- fuer die + Titelbild-Quadranten KELL256A/B/C/D (160x240 -> 160x120). ACHTUNG: + das ist KEINE gleichmaessige Skalierung wie bei den Rauser-Assets -- + per Pixel-Rekonstruktion gegen eine echte DOSBox-Aufnahme verifiziert + (siehe Docstring-Korrektur ganz oben in dieser Datei). Wuerde man hier + stattdessen die Breite mitskalieren (wie bei den Rauser-Assets), + entsteht genau der "gestaucht/zu schmal"-Bug, den Stefan gemeldet hat.''' + surf = self.pcc_surface(name) + w, h = surf.get_size() + new_size = (w, max(1, round(h * 0.5))) + return pygame.transform.smoothscale(surf, new_size) + def fit_scale(src_w, src_h, max_w, max_h): return min(max_w / src_w, max_h / src_h) -def blit_scaled_centered(screen, surface, window_w, window_h): +def blit_scaled_centered(screen, surface, window_w, window_h, bg_color=BG_COLOR): scale = fit_scale(surface.get_width(), surface.get_height(), window_w, window_h) new_size = (max(1, int(surface.get_width() * scale)), max(1, int(surface.get_height() * scale))) scaled = pygame.transform.scale(surface, new_size) x = (window_w - new_size[0]) // 2 y = (window_h - new_size[1]) // 2 - screen.fill(BG_COLOR) + screen.fill(bg_color) screen.blit(scaled, (x, y)) return scale, x, y @@ -118,27 +179,6 @@ def wait_or_skip(clock, seconds, screen_update_fn, fps=30): clock.tick(fps) -def build_rauser_composite(assets): - '''RAUSER1 (dunkle Box "RAUSER") + RAUSER3 (Ausrufezeichen, blaue Box) - + RAUSER2 ("ADVERTAINMENT"-Schriftzug) zu EINER Karte auf weissem Grund - zusammengesetzt (320x240 nativ, wie der Rest der Boot-Screens).''' - canvas = pygame.Surface((320, 240)) - canvas.fill((255, 255, 255)) - - r1 = assets.pcc_surface('RAUSER1') # 182x46 "RAUSER" - r2 = assets.pcc_surface('RAUSER2') # 228x21 "ADVERTAINMENT" - r3 = assets.pcc_surface('RAUSER3') # 16x55 "!" - - total_w = max(r1.get_width() + r3.get_width(), r2.get_width()) - x0 = (320 - total_w) // 2 - y0 = (240 - (r1.get_height() + r2.get_height())) // 2 - - canvas.blit(r1, (x0, y0)) - canvas.blit(r3, (x0 + r1.get_width(), y0 - (r3.get_height() - r1.get_height()) // 2)) - canvas.blit(r2, (x0, y0 + r1.get_height())) - return canvas - - def build_letterboxed(surface, canvas_w=320, canvas_h=240): '''Zentriert ein kleineres PCC-Bild (z.B. 320x199/200) auf eine 320x240- Leinwand mit schwarzen Balken -- wie im echten DOSBox-Fenster beobachtet.''' @@ -165,12 +205,143 @@ def build_kelloggs_frame(w, h, indices, palette, fade_t): return indices_to_surface(w, h, indices, dyn_pal) -def build_title_image(assets): - '''Stitcht KELL256A/B (oben) + C/D (unten) zu einem 320x480-Gesamtbild.''' - a = assets.pcc_surface('KELL256A') - b = assets.pcc_surface('KELL256B') - c = assets.pcc_surface('KELL256C') - d = assets.pcc_surface('KELL256D') +# --------------------------------------------------------------------------- +# Rauser: Slide-in-Animation + Sound-Sting (siehe Docstring-Korrektur) +# --------------------------------------------------------------------------- + +def build_rauser_parts(assets): + '''Liefert die drei skalierten Rauser-Teile + ihre Endpositionen auf einer + 320x240-Leinwand (RAUSER1 links, RAUSER3 rechts daneben, RAUSER2 darunter + mittig -- alle bei ASSET_DISPLAY_SCALE, siehe Docstring).''' + r1 = assets.pcc_surface_scaled('RAUSER1') # "RAUSER"-Box + r2 = assets.pcc_surface_scaled('RAUSER2') # "ADVERTAINMENT" + r3 = assets.pcc_surface_scaled('RAUSER3') # "!" auf blauer Box + + # BEKANNTE UNSCHOENHEIT (bewusst NICHT "gefixt"): die PCC-Assets haben + # weissen Hintergrund in ihrer eigenen Bounding-Box (kein Alpha-Kanal). + # Auf weissem Grund faellt das nicht auf, nach dem Fade zu Grau sieht man + # einen schwachen weissen Kasten um RAUSER2/RAUSER3. Ein Versuch, Weiss + # per set_colorkey() durchsichtig zu machen, wurde wieder verworfen -- + # dadurch wurden auch die WEISSEN BUCHSTABEN in RAUSER1 durchsichtig + # (schlimmer als der Kasten). Sauberer Fix braucht eine echte + # Alpha-Maske pro Asset statt eines einzelnen Colorkeys; low priority, + # siehe "Noch offen" unten. + total_w = r1.get_width() + r3.get_width() + total_h = r1.get_height() + r2.get_height() + x0 = (320 - total_w) // 2 + y0 = (240 - total_h) // 2 + + r1_final = (x0, y0) + r3_final = (x0 + r1.get_width(), y0 - (r3.get_height() - r1.get_height()) // 2) + r2_final = (x0, y0 + r1.get_height()) + return { + 'r1': r1, 'r2': r2, 'r3': r3, + 'r1_final': r1_final, 'r2_final': r2_final, 'r3_final': r3_final, + } + + +def render_rauser_slide(canvas, parts, slide_t): + '''slide_t: 0..1. r1 faehrt von links, r3 von oben ein; r2 steht fest. + Bei slide_t>=1 sind alle Teile an ihrer Endposition.''' + r1, r2, r3 = parts['r1'], parts['r2'], parts['r3'] + ease = slide_t * slide_t * (3 - 2 * slide_t) # smoothstep, wirkt weniger linear/robotisch + + r2_final = parts['r2_final'] + canvas.blit(r2, r2_final) + + r1_final_x, r1_final_y = parts['r1_final'] + r1_start_x = -r1.get_width() + r1_x = int(r1_start_x + (r1_final_x - r1_start_x) * ease) + canvas.blit(r1, (r1_x, r1_final_y)) + + r3_final_x, r3_final_y = parts['r3_final'] + r3_start_y = -r3.get_height() + r3_y = int(r3_start_y + (r3_final_y - r3_start_y) * ease) + canvas.blit(r3, (r3_final_x, r3_y)) + + +def run_rauser(screen, clock, assets): + '''Rauser-Karte: weisser Grund, Slide-in, Sound-Sting bei Ankunft, Fade zu + grauem Grund, kurz halten. Gibt True zurueck wenn der User abbrechen will.''' + parts = build_rauser_parts(assets) + sting = None + try: + if pygame.mixer.get_init(): + sting_wav = tfmx_audio.render_wav(assets.container, 'TITEL2', seconds=1.2) + sting = pygame.mixer.Sound(sting_wav) + except Exception as exc: + print(f'[intro_sequence] Rauser-Sound-Sting konnte nicht geladen werden: {exc}', file=sys.stderr) + + 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 + 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 + + # 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: + sting.play() + sting_played = 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 + + 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 + + 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) + + +# --------------------------------------------------------------------------- +# Titelbild: 2x2-Raster aus halbskalierten Quadranten + Scanline-Reveal +# (siehe Docstring-Korrektur -- NICHT 320x480!) +# --------------------------------------------------------------------------- + +def build_title_quadrants(assets): + '''Liefert die 4 Titelbild-Quadranten bei ASSET_DISPLAY_SCALE (160x120 + statt 160x240 nativ) -- A/B oben, C/D unten. Per Pixel-Rekonstruktion + gegen eine echte DOSBox-Aufnahme verifiziert.''' + return { + 'A': assets.pcc_surface_half_height('KELL256A'), + 'B': assets.pcc_surface_half_height('KELL256B'), + 'C': assets.pcc_surface_half_height('KELL256C'), + 'D': assets.pcc_surface_half_height('KELL256D'), + } + + +def build_title_image(quads): + '''Komplettes, ruhendes Titelbild (320x240) aus den 4 halbskalierten + Quadranten im 2x2-Raster.''' + a, b, c, d = quads['A'], quads['B'], quads['C'], quads['D'] w = a.get_width() + b.get_width() h = a.get_height() + c.get_height() canvas = pygame.Surface((w, h)) @@ -178,18 +349,64 @@ def build_title_image(assets): canvas.blit(b, (a.get_width(), 0)) canvas.blit(c, (0, a.get_height())) canvas.blit(d, (a.get_width(), a.get_height())) - return canvas, a.get_height() # (Gesamtbild, Hoehe der oberen Haelfte) + 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: + 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) + 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).''' + 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 + + canvas = pygame.Surface((full_w, half_h * 2)) + canvas.fill((0, 0, 0)) + + if t < TITLE_HEIGHT_REVEAL_FRACTION: + reveal = t / TITLE_HEIGHT_REVEAL_FRACTION + h = max(0, int(half_h * reveal)) + scan_intensity = 1.0 + else: + h = half_h + scan_intensity = 1.0 - (t - TITLE_HEIGHT_REVEAL_FRACTION) / (1 - TITLE_HEIGHT_REVEAL_FRACTION) + + if h > 0: + 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)) + + 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) + canvas.blit(bot_slice, (0, half_h * 2 - h)) + + return canvas def run_once(screen, clock, assets): - quit_requested = False - - # 1) Rauser - rauser = build_rauser_composite(assets) - - def show_rauser(_t): - blit_scaled_centered(screen, rauser, WINDOW_W, WINDOW_H) - quit_requested = wait_or_skip(clock, 3.0, show_rauser) + quit_requested = run_rauser(screen, clock, assets) if quit_requested: return True @@ -246,25 +463,20 @@ def run_once(screen, clock, assets): if quit_requested: return True - # 7) Titelbild -- Vorhang-Wipe (obere Haelfte faehrt runter, untere hoch) - title_img, half_h = build_title_image(assets) - full_w, full_h = title_img.get_size() + # 7) Titelbild -- 2x2-Quadranten-Reveal mit Scanline-Aufloesung (siehe + # Docstring-Korrektur) + quads = build_title_quadrants(assets) def show_title_wipe(t): - canvas = pygame.Surface((full_w, full_h)) - canvas.fill((0, 0, 0)) - reveal = int(half_h * t) - if reveal > 0: - top_src = pygame.Surface.subsurface(title_img, (0, half_h - reveal, full_w, reveal)) - canvas.blit(top_src, (0, 0)) - bottom_src = pygame.Surface.subsurface(title_img, (0, half_h, full_w, reveal)) - canvas.blit(bottom_src, (0, full_h - reveal)) + canvas = render_title_wipe_frame(quads, t) blit_scaled_centered(screen, canvas, WINDOW_W, WINDOW_H) - quit_requested = wait_or_skip(clock, 3.0, show_title_wipe) + quit_requested = wait_or_skip(clock, TITLE_WIPE_SECONDS, show_title_wipe) if quit_requested: return True - # danach: fertig zusammengesetztes Titelbild halten + # danach: fertig zusammengesetztes Titelbild (320x240, siehe Korrektur) halten + title_img = build_title_image(quads) + 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) diff --git a/tools/render_assets.py b/tools/render_assets.py index d901066..0b5718e 100644 --- a/tools/render_assets.py +++ b/tools/render_assets.py @@ -3,8 +3,9 @@ sys.path.insert(0, os.path.dirname(__file__)) from kellogg_formats import DATContainer, get_bob_palette, get_world_palette, parse_bob, parse_ico, parse_map from PIL import Image -DAT_PATH = '/home/aria/kellogg_remake/raw/PCKELL.DAT' -OUT = '/home/aria/kellogg_remake/render_out' +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DAT_PATH = os.environ.get('KELLOGG_DAT', os.path.join(PROJECT_ROOT, 'raw', 'PCKELL.DAT')) +OUT = os.environ.get('KELLOGG_RENDER_OUT', os.path.join(PROJECT_ROOT, 'render_out')) os.makedirs(OUT, exist_ok=True) container = DATContainer(DAT_PATH) diff --git a/tools/tfmx_audio.py b/tools/tfmx_audio.py index 46fad86..f235fbf 100644 --- a/tools/tfmx_audio.py +++ b/tools/tfmx_audio.py @@ -25,7 +25,8 @@ import sys _HERE = os.path.dirname(os.path.abspath(__file__)) _PLAYER_DIR = os.path.join(_HERE, 'tfmx_player') _BINARY = os.path.join(_PLAYER_DIR, 'render_tfmx') -_CACHE_DIR = os.environ.get('KELLOGG_AUDIO_CACHE', '/home/aria/kellogg_remake/audio_cache') +_PROJECT_ROOT = os.path.dirname(_HERE) +_CACHE_DIR = os.environ.get('KELLOGG_AUDIO_CACHE', os.path.join(_PROJECT_ROOT, 'audio_cache')) def _ensure_binary():