Pfade relativ statt hardcodiert + Rauser/Titelbild-Korrektur per Pixelvermessung

- intro_sequence.py, tfmx_audio.py, render_assets.py, dat_extract.py:
  /home/aria/kellogg_remake/... durch PROJECT_ROOT-relative Pfade ersetzt
  (mit optionalem ENV-Override), laeuft jetzt von jedem Checkout aus.
- Rauser-Logo: war 1:1 nativ gezeichnet (viel zu gross) und weisser statt
  grauer Hintergrund. Per frischer DOSBox-Referenzaufnahme (Xvfb-Screenshot-
  Serie) neu vermessen: 0.5x Skalierung, Slide-in-Animation (RAUSER-Box von
  links, "!"-Box von oben, ADVERTAINMENT steht fest), Sound-Sting (TITEL2,
  einmalig) bei Ankunft, danach Fade weiss->grau.
- Titelbild: fruehere Annahme (4 Quadranten voll gestapelt = 320x480) war
  falsch und die Ursache fuer den gemeldeten Squish-Bug. Per Pixel-
  Rekonstruktion verifiziert: jeder Quadrant wird nur auf halber HOEHE
  (160x120 statt 160x240, Breite bleibt nativ) im 2x2-Raster angeordnet ->
  korrektes 320x240-Bild. Zusaetzlich neu entdeckter und nachgebauter
  Scanline/Graustufen-Reveal-Effekt auf der unteren Bildhaelfte.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ARIA
2026-07-22 21:01:47 +00:00
co-authored by Claude Sonnet 5
parent a34be7ce05
commit 6d196020db
5 changed files with 388 additions and 70 deletions
+103
View File
@@ -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.
+3 -2
View File
@@ -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:
+277 -65
View File
@@ -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)
+3 -2
View File
@@ -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)
+2 -1
View File
@@ -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():