feat: Enhance intro splash screen with retail warning and dynamic slide management

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-20 12:18:34 +01:00
parent b23c02f716
commit 436f498778
5 changed files with 197 additions and 20 deletions

View File

@@ -184,6 +184,7 @@ class WolfEngine {
initialEpisodeIndex: _currentEpisodeIndex, initialEpisodeIndex: _currentEpisodeIndex,
initialDifficulty: difficulty, initialDifficulty: difficulty,
hasResumableGame: false, hasResumableGame: false,
initialGameIsRetail: data.version == GameVersion.retail,
); );
if (_availableGames.length == 1) { if (_availableGames.length == 1) {
@@ -373,7 +374,9 @@ class WolfEngine {
_currentEpisodeIndex = 0; _currentEpisodeIndex = 0;
onEpisodeSelected?.call(null); onEpisodeSelected?.call(null);
menuManager.clearEpisodeSelection(); menuManager.clearEpisodeSelection();
menuManager.beginIntroSplash(); menuManager.beginIntroSplash(
includeRetailWarning: data.version == GameVersion.retail,
);
} }
} }

View File

@@ -9,6 +9,8 @@ enum WolfMenuScreen {
difficultySelect, difficultySelect,
} }
enum WolfIntroSlide { retailWarning, pg13, title }
enum _WolfIntroPhase { fadeIn, hold, fadeOut } enum _WolfIntroPhase { fadeIn, hold, fadeOut }
enum WolfMenuMainAction { enum WolfMenuMainAction {
@@ -61,7 +63,7 @@ bool _isWiredMainMenuAction(WolfMenuMainAction action) {
class MenuManager { class MenuManager {
static const int transitionDurationMs = 280; static const int transitionDurationMs = 280;
static const int introFadeDurationMs = 280; static const int introFadeDurationMs = 280;
static const int _introSlideCount = 2; static const int _introRetailBackgroundRgb = 0xA00000;
static const int _introPg13BackgroundRgb = 0x33A2E8; static const int _introPg13BackgroundRgb = 0x33A2E8;
static const int _introTitleBackgroundRgb = 0x000000; static const int _introTitleBackgroundRgb = 0x000000;
@@ -74,6 +76,10 @@ class MenuManager {
int _introElapsedMs = 0; int _introElapsedMs = 0;
_WolfIntroPhase _introPhase = _WolfIntroPhase.fadeIn; _WolfIntroPhase _introPhase = _WolfIntroPhase.fadeIn;
bool _introAdvanceRequested = false; bool _introAdvanceRequested = false;
List<WolfIntroSlide> _introSlides = <WolfIntroSlide>[
WolfIntroSlide.pg13,
WolfIntroSlide.title,
];
int _selectedMainIndex = 0; int _selectedMainIndex = 0;
int _selectedGameIndex = 0; int _selectedGameIndex = 0;
@@ -93,10 +99,31 @@ class MenuManager {
bool get isIntroSplashActive => _activeMenu == WolfMenuScreen.introSplash; bool get isIntroSplashActive => _activeMenu == WolfMenuScreen.introSplash;
bool get isIntroPg13Slide => _introSlideIndex == 0; WolfIntroSlide get currentIntroSlide {
if (_introSlides.isEmpty) {
return WolfIntroSlide.title;
}
final int index = _introSlideIndex.clamp(0, _introSlides.length - 1);
return _introSlides[index];
}
int get introBackgroundRgb => bool get isIntroRetailWarningSlide =>
isIntroPg13Slide ? _introPg13BackgroundRgb : _introTitleBackgroundRgb; currentIntroSlide == WolfIntroSlide.retailWarning;
bool get isIntroPg13Slide => currentIntroSlide == WolfIntroSlide.pg13;
bool get isIntroTitleSlide => currentIntroSlide == WolfIntroSlide.title;
int get introBackgroundRgb {
switch (currentIntroSlide) {
case WolfIntroSlide.retailWarning:
return _introRetailBackgroundRgb;
case WolfIntroSlide.pg13:
return _introPg13BackgroundRgb;
case WolfIntroSlide.title:
return _introTitleBackgroundRgb;
}
}
double get introOverlayAlpha { double get introOverlayAlpha {
if (!isIntroSplashActive) { if (!isIntroSplashActive) {
@@ -189,6 +216,7 @@ class MenuManager {
int initialEpisodeIndex = 0, int initialEpisodeIndex = 0,
Difficulty? initialDifficulty, Difficulty? initialDifficulty,
bool hasResumableGame = false, bool hasResumableGame = false,
bool initialGameIsRetail = false,
}) { }) {
_gameCount = gameCount; _gameCount = gameCount;
_showResumeOption = hasResumableGame; _showResumeOption = hasResumableGame;
@@ -206,8 +234,12 @@ class MenuManager {
_introElapsedMs = 0; _introElapsedMs = 0;
_introPhase = _WolfIntroPhase.fadeIn; _introPhase = _WolfIntroPhase.fadeIn;
_introSlideIndex = 0; _introSlideIndex = 0;
_introSlides = <WolfIntroSlide>[
WolfIntroSlide.pg13,
WolfIntroSlide.title,
];
} else { } else {
_startIntroSequence(); _startIntroSequence(includeRetailWarning: initialGameIsRetail);
} }
_transitionTarget = null; _transitionTarget = null;
_transitionElapsedMs = 0; _transitionElapsedMs = 0;
@@ -218,12 +250,13 @@ class MenuManager {
/// Starts the intro splash sequence and lands on [landingMenu] when done. /// Starts the intro splash sequence and lands on [landingMenu] when done.
void beginIntroSplash({ void beginIntroSplash({
WolfMenuScreen landingMenu = WolfMenuScreen.mainMenu, WolfMenuScreen landingMenu = WolfMenuScreen.mainMenu,
bool includeRetailWarning = false,
}) { }) {
_introLandingMenu = landingMenu; _introLandingMenu = landingMenu;
_transitionTarget = null; _transitionTarget = null;
_transitionElapsedMs = 0; _transitionElapsedMs = 0;
_transitionSwappedMenu = false; _transitionSwappedMenu = false;
_startIntroSequence(); _startIntroSequence(includeRetailWarning: includeRetailWarning);
_resetEdgeState(); _resetEdgeState();
} }
@@ -312,8 +345,15 @@ class MenuManager {
_consumeEdgeState(input); _consumeEdgeState(input);
} }
void _startIntroSequence() { void _startIntroSequence({required bool includeRetailWarning}) {
_activeMenu = WolfMenuScreen.introSplash; _activeMenu = WolfMenuScreen.introSplash;
_introSlides = includeRetailWarning
? <WolfIntroSlide>[
WolfIntroSlide.retailWarning,
WolfIntroSlide.pg13,
WolfIntroSlide.title,
]
: <WolfIntroSlide>[WolfIntroSlide.pg13, WolfIntroSlide.title];
_introSlideIndex = 0; _introSlideIndex = 0;
_introElapsedMs = 0; _introElapsedMs = 0;
_introPhase = _WolfIntroPhase.fadeIn; _introPhase = _WolfIntroPhase.fadeIn;
@@ -356,7 +396,7 @@ class MenuManager {
} }
void _advanceIntroSlide() { void _advanceIntroSlide() {
if (_introSlideIndex < _introSlideCount - 1) { if (_introSlideIndex < _introSlides.length - 1) {
_introSlideIndex += 1; _introSlideIndex += 1;
_introElapsedMs = 0; _introElapsedMs = 0;
_introPhase = _WolfIntroPhase.fadeIn; _introPhase = _WolfIntroPhase.fadeIn;

View File

@@ -420,7 +420,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
final art = WolfClassicMenuArt(engine.data); final art = WolfClassicMenuArt(engine.data);
if (engine.menuManager.activeMenu == WolfMenuScreen.introSplash) { if (engine.menuManager.activeMenu == WolfMenuScreen.introSplash) {
_drawIntroSplash(engine, art); _drawIntroSplash(engine, art, menuTypography);
return; return;
} }
@@ -708,10 +708,16 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
} }
} }
void _drawIntroSplash(WolfEngine engine, WolfClassicMenuArt art) { void _drawIntroSplash(
final image = engine.menuManager.isIntroPg13Slide WolfEngine engine,
? art.mappedPic(WolfMenuPic.pg13) WolfClassicMenuArt art,
: art.mappedPic(WolfMenuPic.title); _AsciiMenuTypography menuTypography,
) {
final image = switch (engine.menuManager.currentIntroSlide) {
WolfIntroSlide.retailWarning => null,
WolfIntroSlide.pg13 => art.mappedPic(WolfMenuPic.pg13),
WolfIntroSlide.title => art.mappedPic(WolfMenuPic.title),
};
int splashBg = _rgbToPaletteColor(engine.menuManager.introBackgroundRgb); int splashBg = _rgbToPaletteColor(engine.menuManager.introBackgroundRgb);
if (engine.menuManager.isIntroPg13Slide && if (engine.menuManager.isIntroPg13Slide &&
@@ -726,6 +732,10 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
_fillRect(0, 0, width, height, activeTheme.solid, splashBg); _fillRect(0, 0, width, height, activeTheme.solid, splashBg);
} }
if (engine.menuManager.isIntroRetailWarningSlide) {
_drawRetailWarningIntro(splashBg, menuTypography);
}
if (image != null) { if (image != null) {
final int x = engine.menuManager.isIntroPg13Slide final int x = engine.menuManager.isIntroPg13Slide
? (320 - image.width).clamp(0, 319) ? (320 - image.width).clamp(0, 319)
@@ -742,6 +752,58 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
); );
} }
void _drawRetailWarningIntro(
int backgroundColor,
_AsciiMenuTypography menuTypography,
) {
final int black = ColorPalette.vga32Bit[0];
final int yellow = ColorPalette.vga32Bit[14];
final int white = ColorPalette.vga32Bit[15];
final int lineColor = ColorPalette.vga32Bit[4];
_fillRect320(0, 0, 320, 22, black);
_drawMenuTextCentered(
'Attention',
6,
yellow,
scale: menuTypography.headingScale,
);
_fillRect320(0, 23, 320, 1, lineColor);
if (menuTypography.usesCompactRows) {
final int textLeft = _menuX320ToColumn(40);
final int textRight = _menuX320ToColumn(280);
final int textWidth = math.max(1, textRight - textLeft);
void writeCenteredLine(int y200, String text) {
final String clipped = _clipWithEllipsis(text, textWidth);
final int centeredLeft =
textLeft + math.max(0, ((textWidth - clipped.length) ~/ 2));
_writeLeftClipped(
_menuY200ToRow(y200),
clipped,
white,
backgroundColor,
clipped.length,
centeredLeft,
);
}
writeCenteredLine(62, 'THIS GAME IS NOT SHAREWARE.');
writeCenteredLine(74, 'PLEASE DO NOT DISTRIBUTE IT.');
writeCenteredLine(86, 'THANKS.');
writeCenteredLine(112, 'ID SOFTWARE');
} else {
_drawMenuText('This game is NOT shareware.', 40, 56, white, scale: 1);
_drawMenuText('Please do not distribute it.', 40, 68, white, scale: 1);
_drawMenuText('Thanks.', 40, 80, white, scale: 1);
_drawMenuTextCentered('Id Software', 106, white, scale: 1);
}
_fillRect320(0, 196, 320, 4, backgroundColor);
}
int _dominantPaletteIndex(VgaImage image) { int _dominantPaletteIndex(VgaImage image) {
final List<int> histogram = List<int>.filled(256, 0); final List<int> histogram = List<int>.filled(256, 0);
for (final int colorIndex in image.pixels) { for (final int colorIndex in image.pixels) {
@@ -767,6 +829,18 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
} }
final int threshold = (alpha * 3).round().clamp(1, 3); final int threshold = (alpha * 3).round().clamp(1, 3);
if (_usesTerminalLayout) {
for (int y = 0; y < _terminalPixelHeight; y++) {
for (int x = 0; x < _terminalSceneWidth; x++) {
if (((x + y) % 3) < threshold) {
_scenePixels[y][x] = fadeColor;
}
}
}
return;
}
for (int y = 0; y < _screen.length; y++) { for (int y = 0; y < _screen.length; y++) {
final row = _screen[y]; final row = _screen[y];
for (int x = 0; x < row.length; x++) { for (int x = 0; x < row.length; x++) {

View File

@@ -561,9 +561,11 @@ class SixelRenderer extends CliRendererBackend<String> {
} }
void _drawIntroSplash(WolfEngine engine, WolfClassicMenuArt art) { void _drawIntroSplash(WolfEngine engine, WolfClassicMenuArt art) {
final image = engine.menuManager.isIntroPg13Slide final image = switch (engine.menuManager.currentIntroSlide) {
? art.mappedPic(WolfMenuPic.pg13) WolfIntroSlide.retailWarning => null,
: art.mappedPic(WolfMenuPic.title); WolfIntroSlide.pg13 => art.mappedPic(WolfMenuPic.pg13),
WolfIntroSlide.title => art.mappedPic(WolfMenuPic.title),
};
int splashBg = _rgbToPaletteIndex(engine.menuManager.introBackgroundRgb); int splashBg = _rgbToPaletteIndex(engine.menuManager.introBackgroundRgb);
if (engine.menuManager.isIntroPg13Slide && if (engine.menuManager.isIntroPg13Slide &&
@@ -576,6 +578,10 @@ class SixelRenderer extends CliRendererBackend<String> {
_screen[i] = splashBg; _screen[i] = splashBg;
} }
if (engine.menuManager.isIntroRetailWarningSlide) {
_drawRetailWarningIntro(splashBg);
}
if (image != null) { if (image != null) {
final int x = engine.menuManager.isIntroPg13Slide final int x = engine.menuManager.isIntroPg13Slide
? (320 - image.width).clamp(0, 319) ? (320 - image.width).clamp(0, 319)
@@ -592,6 +598,24 @@ class SixelRenderer extends CliRendererBackend<String> {
); );
} }
void _drawRetailWarningIntro(int backgroundColor) {
const int black = 0;
const int yellow = 14;
const int white = 15;
const int lineColor = 4;
_fillRect320(0, 0, 320, 22, black);
_drawMenuTextCentered('Attention', 6, yellow, scale: 2);
_fillRect320(0, 23, 320, 1, lineColor);
_drawMenuText('This game is NOT shareware.', 40, 56, white, scale: 1);
_drawMenuText('Please do not distribute it.', 40, 68, white, scale: 1);
_drawMenuText('Thanks.', 40, 80, white, scale: 1);
_drawMenuTextCentered('Id Software', 106, white, scale: 1);
_fillRect320(0, 196, 320, 4, backgroundColor);
}
int _dominantPaletteIndex(VgaImage image) { int _dominantPaletteIndex(VgaImage image) {
final List<int> histogram = List<int>.filled(256, 0); final List<int> histogram = List<int>.filled(256, 0);
for (final int colorIndex in image.pixels) { for (final int colorIndex in image.pixels) {

View File

@@ -209,9 +209,11 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
} }
void _drawIntroSplash(WolfEngine engine, WolfClassicMenuArt art) { void _drawIntroSplash(WolfEngine engine, WolfClassicMenuArt art) {
final image = engine.menuManager.isIntroPg13Slide final image = switch (engine.menuManager.currentIntroSlide) {
? art.mappedPic(WolfMenuPic.pg13) WolfIntroSlide.retailWarning => null,
: art.mappedPic(WolfMenuPic.title); WolfIntroSlide.pg13 => art.mappedPic(WolfMenuPic.pg13),
WolfIntroSlide.title => art.mappedPic(WolfMenuPic.title),
};
int splashBgColor = _rgbToFrameColor(engine.menuManager.introBackgroundRgb); int splashBgColor = _rgbToFrameColor(engine.menuManager.introBackgroundRgb);
int? matteIndex; int? matteIndex;
@@ -227,6 +229,10 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
_buffer.pixels[i] = splashBgColor; _buffer.pixels[i] = splashBgColor;
} }
if (engine.menuManager.isIntroRetailWarningSlide) {
_drawRetailWarningIntro(splashBgColor);
}
if (image != null) { if (image != null) {
final int x = engine.menuManager.isIntroPg13Slide final int x = engine.menuManager.isIntroPg13Slide
? (320 - image.width).clamp(0, 319) ? (320 - image.width).clamp(0, 319)
@@ -247,6 +253,36 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
); );
} }
void _drawRetailWarningIntro(int backgroundColor) {
final int black = ColorPalette.vga32Bit[0];
final int yellow = ColorPalette.vga32Bit[14];
final int white = ColorPalette.vga32Bit[15];
final int lineColor = ColorPalette.vga32Bit[4];
_fillCanonicalRect(0, 0, 320, 22, black);
_drawCanonicalMenuTextCentered('Attention', 6, yellow, scale: 2);
_fillCanonicalRect(0, 23, 320, 1, lineColor);
_drawCanonicalMenuText(
'This game is NOT shareware.',
40,
56,
white,
scale: 1,
);
_drawCanonicalMenuText(
'Please do not distribute it.',
40,
68,
white,
scale: 1,
);
_drawCanonicalMenuText('Thanks.', 40, 80, white, scale: 1);
_drawCanonicalMenuTextCentered('Id Software', 106, white, scale: 1);
_fillCanonicalRect(0, 196, 320, 4, backgroundColor);
}
int _dominantPaletteIndex(VgaImage image) { int _dominantPaletteIndex(VgaImage image) {
final List<int> histogram = List<int>.filled(256, 0); final List<int> histogram = List<int>.filled(256, 0);
for (final int colorIndex in image.pixels) { for (final int colorIndex in image.pixels) {