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:
@@ -948,3 +948,106 @@ kompilierte Binary -- Build-Artefakt, per `.gitignore` draussen wie
|
|||||||
3. ARE/Kollisionsformat klaeren, wenn Spiellogik es braucht.
|
3. ARE/Kollisionsformat klaeren, wenn Spiellogik es braucht.
|
||||||
|
|
||||||
Laeuft selbstaendig weiter, keine Rueckfrage noetig.
|
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.
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import struct, os
|
import struct, os
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
|
||||||
SRC = '/home/aria/kellogg_remake/raw/PCKELL.DAT'
|
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
OUT = '/home/aria/kellogg_remake/extracted_dat'
|
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)
|
os.makedirs(OUT, exist_ok=True)
|
||||||
|
|
||||||
with open(SRC, 'rb') as f:
|
with open(SRC, 'rb') as f:
|
||||||
|
|||||||
+277
-65
@@ -4,27 +4,43 @@ Boot-/Intro-Sequenz von "Tony & Friends in Kellogg's Land" -- erster
|
|||||||
spielbarer (bzw. hier: zuschaubarer) pygame-Meilenstein des Remakes.
|
spielbarer (bzw. hier: zuschaubarer) pygame-Meilenstein des Remakes.
|
||||||
|
|
||||||
Reihenfolge + Timing wurde am 22.07.2026 durch eine echte DOSBox-Screenshot-
|
Reihenfolge + Timing wurde am 22.07.2026 durch eine echte DOSBox-Screenshot-
|
||||||
Serie (0.3s-Intervall) waehrend des Originalboots vermessen, siehe NOTES.md
|
Serie vermessen, siehe NOTES.md Abschnitt "Boot-Sequenz vermessen". Kurzfassung:
|
||||||
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
|
2. kurze schwarze Pause
|
||||||
3. Factor5-Studio-Logo (FACTOR5.PCC, STATISCHES Bild -- keine Animation,
|
3. Factor5-Studio-Logo (FACTOR5.PCC, STATISCHES Bild -- keine Animation,
|
||||||
das "Geister"-Doppellogo ist Teil des Assets selbst), ~6s
|
das "Geister"-Doppellogo ist Teil des Assets selbst), ~6s
|
||||||
4. kurze schwarze Pause
|
4. kurze schwarze Pause
|
||||||
5. Kellogg's-Markenlogo (KELLOGGS.PCC) erscheint SOFORT komplett; darunter
|
5. Kellogg's-Markenlogo (KELLOGGS.PCC) erscheint SOFORT komplett; darunter
|
||||||
faedet der Text "praesentiert" per Palette-Fade von blass nach saettigt-
|
faedet der Text "praesentiert" per Palette-Fade von blass nach saettigt-
|
||||||
rot ein (~3s). WICHTIG: das ist NICHT der vom Stefan vermutete "Vorhang"-
|
rot ein (~3s).
|
||||||
Effekt auf dem Logo selbst -- das Logo ist die ganze Zeit scharf/komplett
|
|
||||||
da, nur der Text darunter faedet.
|
|
||||||
6. kurze schwarze Pause
|
6. kurze schwarze Pause
|
||||||
7. Titelbild "TONY & FRIENDS in Kellogg's Land" -- DAS ist der eigentliche
|
7. Titelbild "TONY & FRIENDS in Kellogg's Land" -- **KORREKTUR (22.07.2026,
|
||||||
Vorhang-Effekt: das Bild ist intern 320x480 (doppelt so hoch wie die
|
spaeter Abend): das Bild ist NICHT 320x480!** Die 4 Quadranten KELL256A
|
||||||
anderen 320x200/240-Screens!), zusammengesetzt aus den 4 Quadranten
|
(oben links) / B (oben rechts) / C (unten links) / D (unten rechts) sind
|
||||||
KELL256A (oben links) / B (oben rechts) / C (unten links) / D (unten
|
zwar je 160x240 PCC-Nativaufloesung, werden aber -- exakt wie die
|
||||||
rechts), je 160x240. Obere Haelfte (A|B) faehrt von oben nach unten ein,
|
Rauser-Assets -- nur bei **halber Groesse** (160x120) in ein 2x2-Raster
|
||||||
untere Haelfte (C|D) von unten nach oben -- wie ein Theatervorhang, nur
|
gezeichnet: A|B oben, C|D unten -> 320x240 Gesamtbild. Per Pixel-
|
||||||
vertikal, beide Haelften treffen sich in der Bildmitte. Dauer ~3s.
|
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
|
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
|
PCX-Decoder) aus PCKELL.DAT gelesen -- kein eigener RLE-Code mehr (siehe
|
||||||
@@ -49,7 +65,14 @@ import tfmx_audio
|
|||||||
|
|
||||||
import pygame
|
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
|
WINDOW_W, WINDOW_H = 960, 720
|
||||||
BG_COLOR = (0, 0, 0)
|
BG_COLOR = (0, 0, 0)
|
||||||
@@ -58,6 +81,23 @@ BG_COLOR = (0, 0, 0)
|
|||||||
PRAESENTIERT_FADE_INDICES = [34, 42, 43, 44, 45]
|
PRAESENTIERT_FADE_INDICES = [34, 42, 43, 44, 45]
|
||||||
PRAESENTIERT_FADE_SECONDS = 2.5
|
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):
|
def indices_to_surface(width, height, indices, palette):
|
||||||
'''Baut ein pygame.Surface (RGB) aus Palette-Indizes + (r,g,b)-Liste.'''
|
'''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)
|
w, h, indices, palette = self.pcc(name)
|
||||||
return indices_to_surface(w, h, indices, palette)
|
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):
|
def fit_scale(src_w, src_h, max_w, max_h):
|
||||||
return min(max_w / src_w, max_h / src_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)
|
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)))
|
new_size = (max(1, int(surface.get_width() * scale)), max(1, int(surface.get_height() * scale)))
|
||||||
scaled = pygame.transform.scale(surface, new_size)
|
scaled = pygame.transform.scale(surface, new_size)
|
||||||
x = (window_w - new_size[0]) // 2
|
x = (window_w - new_size[0]) // 2
|
||||||
y = (window_h - new_size[1]) // 2
|
y = (window_h - new_size[1]) // 2
|
||||||
screen.fill(BG_COLOR)
|
screen.fill(bg_color)
|
||||||
screen.blit(scaled, (x, y))
|
screen.blit(scaled, (x, y))
|
||||||
return scale, x, y
|
return scale, x, y
|
||||||
|
|
||||||
@@ -118,27 +179,6 @@ def wait_or_skip(clock, seconds, screen_update_fn, fps=30):
|
|||||||
clock.tick(fps)
|
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):
|
def build_letterboxed(surface, canvas_w=320, canvas_h=240):
|
||||||
'''Zentriert ein kleineres PCC-Bild (z.B. 320x199/200) auf eine 320x240-
|
'''Zentriert ein kleineres PCC-Bild (z.B. 320x199/200) auf eine 320x240-
|
||||||
Leinwand mit schwarzen Balken -- wie im echten DOSBox-Fenster beobachtet.'''
|
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)
|
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.'''
|
# Rauser: Slide-in-Animation + Sound-Sting (siehe Docstring-Korrektur)
|
||||||
a = assets.pcc_surface('KELL256A')
|
# ---------------------------------------------------------------------------
|
||||||
b = assets.pcc_surface('KELL256B')
|
|
||||||
c = assets.pcc_surface('KELL256C')
|
def build_rauser_parts(assets):
|
||||||
d = assets.pcc_surface('KELL256D')
|
'''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()
|
w = a.get_width() + b.get_width()
|
||||||
h = a.get_height() + c.get_height()
|
h = a.get_height() + c.get_height()
|
||||||
canvas = pygame.Surface((w, h))
|
canvas = pygame.Surface((w, h))
|
||||||
@@ -178,18 +349,64 @@ def build_title_image(assets):
|
|||||||
canvas.blit(b, (a.get_width(), 0))
|
canvas.blit(b, (a.get_width(), 0))
|
||||||
canvas.blit(c, (0, a.get_height()))
|
canvas.blit(c, (0, a.get_height()))
|
||||||
canvas.blit(d, (a.get_width(), 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):
|
def run_once(screen, clock, assets):
|
||||||
quit_requested = False
|
quit_requested = run_rauser(screen, clock, assets)
|
||||||
|
|
||||||
# 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:
|
if quit_requested:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -246,25 +463,20 @@ def run_once(screen, clock, assets):
|
|||||||
if quit_requested:
|
if quit_requested:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 7) Titelbild -- Vorhang-Wipe (obere Haelfte faehrt runter, untere hoch)
|
# 7) Titelbild -- 2x2-Quadranten-Reveal mit Scanline-Aufloesung (siehe
|
||||||
title_img, half_h = build_title_image(assets)
|
# Docstring-Korrektur)
|
||||||
full_w, full_h = title_img.get_size()
|
quads = build_title_quadrants(assets)
|
||||||
|
|
||||||
def show_title_wipe(t):
|
def show_title_wipe(t):
|
||||||
canvas = pygame.Surface((full_w, full_h))
|
canvas = render_title_wipe_frame(quads, t)
|
||||||
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)
|
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:
|
if quit_requested:
|
||||||
return True
|
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):
|
def show_title_full(_t):
|
||||||
blit_scaled_centered(screen, title_img, WINDOW_W, WINDOW_H)
|
blit_scaled_centered(screen, title_img, WINDOW_W, WINDOW_H)
|
||||||
quit_requested = wait_or_skip(clock, 3.0, show_title_full)
|
quit_requested = wait_or_skip(clock, 3.0, show_title_full)
|
||||||
|
|||||||
@@ -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 kellogg_formats import DATContainer, get_bob_palette, get_world_palette, parse_bob, parse_ico, parse_map
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
DAT_PATH = '/home/aria/kellogg_remake/raw/PCKELL.DAT'
|
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
OUT = '/home/aria/kellogg_remake/render_out'
|
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)
|
os.makedirs(OUT, exist_ok=True)
|
||||||
|
|
||||||
container = DATContainer(DAT_PATH)
|
container = DATContainer(DAT_PATH)
|
||||||
|
|||||||
+2
-1
@@ -25,7 +25,8 @@ import sys
|
|||||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
_PLAYER_DIR = os.path.join(_HERE, 'tfmx_player')
|
_PLAYER_DIR = os.path.join(_HERE, 'tfmx_player')
|
||||||
_BINARY = os.path.join(_PLAYER_DIR, 'render_tfmx')
|
_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():
|
def _ensure_binary():
|
||||||
|
|||||||
Reference in New Issue
Block a user