Boot-Sequenz vermessen + Intro-Player gebaut, echten PCC-Decoder-Regressionsbug gefixt
- Regression gefunden+gefixt: pcc_to_png.py war durch einen frueheren Commit versehentlich auf den kaputten Hand-RLE-Decoder zurueckgefallen (der ImageMagick-Fix ging verloren). Jetzt: kellogg_formats.load_pcc() nutzt Pillows PCX-Decoder direkt, kein eigener RLE-Code mehr im Projekt. Alle 77 PCC neu gerendert, AE=0 (pixelidentisch) zu den alten guten PNGs. - Boot-Sequenz per echter DOSBox-Screenshot-Serie (0.3s-Intervall) vermessen: Rauser-Karte -> Factor5-Logo (statisch, keine Animation) -> Kellogg's-Logo mit Palette-Fade des "praesentiert"-Texts (Indizes 34/42/43/44/45) -> Titelbild "Tony & Friends in Kellogg's Land" mit echtem Vorhang-Wipe-Effekt (320x480 aus 4 Quadranten KELL256A-D, obere Haelfte faehrt runter, untere hoch, treffen sich in der Mitte). Korrigiert eine fruehere Fehlannahme: der Wipe sitzt auf dem TITELBILD, nicht auf dem Kellogg's-Markenlogo. - tools/intro_sequence.py: erster spielbarer pygame-Meilenstein, baut die komplette Sequenz nach (Asset-Loading, Palette-Rendering, Timing, echter Palette-Fade, Wipe-Effekt). Headless getestet, visuell verifiziert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user