#!/usr/bin/env python3 """ 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: 1. Rauser-Advertainment-Karte (Komposit aus RAUSER1+2+3.PCC), ~3s 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. 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. 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 Regressions-Warnung in pcc_to_png.py). Rauser-Layout und Fade-Text-Bounding-Box sind aus den echten Screenshots per Augenmass/Index-Analyse bestimmt (siehe tools/analyze_kelloggs.py-Fund: Indizes 34/42/43/44/45 in KELLOGGS.PCC = "praesentiert"-Text, bbox x71-241 y108-141) -- nicht 100% pixelidentisch zum Original, aber nah dran. Kann bei Bedarf spaeter nachjustiert werden. Aufruf: python3 intro_sequence.py [--once] (--once: nach einem Durchlauf beenden, sonst loopt die Sequenz weiter wie im Original-Titelbildschirm) """ import sys import os import time sys.path.insert(0, os.path.dirname(__file__)) from kellogg_formats import DATContainer, load_pcc import pygame DAT_PATH = os.environ.get('KELLOGG_DAT', '/home/aria/kellogg_remake/raw/PCKELL.DAT') WINDOW_W, WINDOW_H = 960, 720 BG_COLOR = (0, 0, 0) # "praesentiert"-Text-Faelle in KELLOGGS.PCC (siehe Docstring) PRAESENTIERT_FADE_INDICES = [34, 42, 43, 44, 45] PRAESENTIERT_FADE_SECONDS = 2.5 def indices_to_surface(width, height, indices, palette): '''Baut ein pygame.Surface (RGB) aus Palette-Indizes + (r,g,b)-Liste.''' buf = bytearray(width * height * 3) for i, idx in enumerate(indices): r, g, b = palette[idx & 0xFF] buf[i*3:i*3+3] = bytes([r, g, b]) surf = pygame.image.frombuffer(bytes(buf), (width, height), 'RGB') return surf.convert() class Assets: def __init__(self, dat_path): self.container = DATContainer(dat_path) def pcc(self, name): '''name z.B. "RAUSER1" (ohne .PCC). Gibt (w,h,indices,palette) zurueck.''' return load_pcc(self.container.entries[name + '.PCC']) def pcc_surface(self, name): w, h, indices, palette = self.pcc(name) return indices_to_surface(w, h, indices, palette) 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): 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.blit(scaled, (x, y)) return scale, x, y 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).''' start = time.monotonic() while True: now = time.monotonic() t = (now - start) / seconds if seconds > 0 else 1.0 if t >= 1.0: screen_update_fn(1.0) pygame.display.flip() return False 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 screen_update_fn(t) pygame.display.flip() 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.''' canvas = pygame.Surface((canvas_w, canvas_h)) canvas.fill((0, 0, 0)) x = (canvas_w - surface.get_width()) // 2 y = (canvas_h - surface.get_height()) // 2 canvas.blit(surface, (x, y)) return canvas def build_kelloggs_frame(w, h, indices, palette, fade_t): '''fade_t: 0..1, faedet PRAESENTIERT_FADE_INDICES von blass (Richtung Hintergrundweiss) zu ihrer echten Palettenfarbe.''' dyn_pal = list(palette) for idx in PRAESENTIERT_FADE_INDICES: r, g, b = palette[idx] # start: sehr blass (nah am weissen Logo-Hintergrund), Ende: echte Farbe pale = (255, 245, 245) nr = int(pale[0] + (r - pale[0]) * fade_t) ng = int(pale[1] + (g - pale[1]) * fade_t) nb = int(pale[2] + (b - pale[2]) * fade_t) dyn_pal[idx] = (nr, ng, nb) 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') w = a.get_width() + b.get_width() h = a.get_height() + c.get_height() canvas = pygame.Surface((w, h)) canvas.blit(a, (0, 0)) 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) 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) if quit_requested: 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 (statisch) factor5 = build_letterboxed(assets.pcc_surface('FACTOR5')) def show_factor5(_t): blit_scaled_centered(screen, factor5, WINDOW_W, WINDOW_H) quit_requested = wait_or_skip(clock, 3.0, 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 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 # 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 # 6) schwarze Pause quit_requested = wait_or_skip(clock, 0.5, lambda t: screen.fill(BG_COLOR)) 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() 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)) blit_scaled_centered(screen, canvas, WINDOW_W, WINDOW_H) quit_requested = wait_or_skip(clock, 3.0, show_title_wipe) if quit_requested: return True # danach: fertig zusammengesetztes Titelbild halten 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 main(): once = '--once' in sys.argv pygame.init() screen = pygame.display.set_mode((WINDOW_W, WINDOW_H)) pygame.display.set_caption("Tony & Friends in Kellogg's Land -- Intro (Remake)") clock = pygame.time.Clock() assets = Assets(DAT_PATH) quit_requested = run_once(screen, clock, assets) while not quit_requested and not once: quit_requested = run_once(screen, clock, assets) pygame.quit() if __name__ == '__main__': main()