From 436f498778868d5902ecc09d3bb0a674b8915e38 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Fri, 20 Mar 2026 12:18:34 +0100 Subject: [PATCH] feat: Enhance intro splash screen with retail warning and dynamic slide management Signed-off-by: Hans Kokx --- .../lib/src/engine/wolf_3d_engine_base.dart | 5 +- .../lib/src/menu/menu_manager.dart | 56 +++++++++++-- .../lib/src/rendering/ascii_renderer.dart | 84 +++++++++++++++++-- .../lib/src/rendering/sixel_renderer.dart | 30 ++++++- .../lib/src/rendering/software_renderer.dart | 42 +++++++++- 5 files changed, 197 insertions(+), 20 deletions(-) diff --git a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart index 153e4c0..ff69867 100644 --- a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart +++ b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart @@ -184,6 +184,7 @@ class WolfEngine { initialEpisodeIndex: _currentEpisodeIndex, initialDifficulty: difficulty, hasResumableGame: false, + initialGameIsRetail: data.version == GameVersion.retail, ); if (_availableGames.length == 1) { @@ -373,7 +374,9 @@ class WolfEngine { _currentEpisodeIndex = 0; onEpisodeSelected?.call(null); menuManager.clearEpisodeSelection(); - menuManager.beginIntroSplash(); + menuManager.beginIntroSplash( + includeRetailWarning: data.version == GameVersion.retail, + ); } } diff --git a/packages/wolf_3d_dart/lib/src/menu/menu_manager.dart b/packages/wolf_3d_dart/lib/src/menu/menu_manager.dart index 3b9b3a9..57f8966 100644 --- a/packages/wolf_3d_dart/lib/src/menu/menu_manager.dart +++ b/packages/wolf_3d_dart/lib/src/menu/menu_manager.dart @@ -9,6 +9,8 @@ enum WolfMenuScreen { difficultySelect, } +enum WolfIntroSlide { retailWarning, pg13, title } + enum _WolfIntroPhase { fadeIn, hold, fadeOut } enum WolfMenuMainAction { @@ -61,7 +63,7 @@ bool _isWiredMainMenuAction(WolfMenuMainAction action) { class MenuManager { static const int transitionDurationMs = 280; static const int introFadeDurationMs = 280; - static const int _introSlideCount = 2; + static const int _introRetailBackgroundRgb = 0xA00000; static const int _introPg13BackgroundRgb = 0x33A2E8; static const int _introTitleBackgroundRgb = 0x000000; @@ -74,6 +76,10 @@ class MenuManager { int _introElapsedMs = 0; _WolfIntroPhase _introPhase = _WolfIntroPhase.fadeIn; bool _introAdvanceRequested = false; + List _introSlides = [ + WolfIntroSlide.pg13, + WolfIntroSlide.title, + ]; int _selectedMainIndex = 0; int _selectedGameIndex = 0; @@ -93,10 +99,31 @@ class MenuManager { 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 => - isIntroPg13Slide ? _introPg13BackgroundRgb : _introTitleBackgroundRgb; + bool get isIntroRetailWarningSlide => + 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 { if (!isIntroSplashActive) { @@ -189,6 +216,7 @@ class MenuManager { int initialEpisodeIndex = 0, Difficulty? initialDifficulty, bool hasResumableGame = false, + bool initialGameIsRetail = false, }) { _gameCount = gameCount; _showResumeOption = hasResumableGame; @@ -206,8 +234,12 @@ class MenuManager { _introElapsedMs = 0; _introPhase = _WolfIntroPhase.fadeIn; _introSlideIndex = 0; + _introSlides = [ + WolfIntroSlide.pg13, + WolfIntroSlide.title, + ]; } else { - _startIntroSequence(); + _startIntroSequence(includeRetailWarning: initialGameIsRetail); } _transitionTarget = null; _transitionElapsedMs = 0; @@ -218,12 +250,13 @@ class MenuManager { /// Starts the intro splash sequence and lands on [landingMenu] when done. void beginIntroSplash({ WolfMenuScreen landingMenu = WolfMenuScreen.mainMenu, + bool includeRetailWarning = false, }) { _introLandingMenu = landingMenu; _transitionTarget = null; _transitionElapsedMs = 0; _transitionSwappedMenu = false; - _startIntroSequence(); + _startIntroSequence(includeRetailWarning: includeRetailWarning); _resetEdgeState(); } @@ -312,8 +345,15 @@ class MenuManager { _consumeEdgeState(input); } - void _startIntroSequence() { + void _startIntroSequence({required bool includeRetailWarning}) { _activeMenu = WolfMenuScreen.introSplash; + _introSlides = includeRetailWarning + ? [ + WolfIntroSlide.retailWarning, + WolfIntroSlide.pg13, + WolfIntroSlide.title, + ] + : [WolfIntroSlide.pg13, WolfIntroSlide.title]; _introSlideIndex = 0; _introElapsedMs = 0; _introPhase = _WolfIntroPhase.fadeIn; @@ -356,7 +396,7 @@ class MenuManager { } void _advanceIntroSlide() { - if (_introSlideIndex < _introSlideCount - 1) { + if (_introSlideIndex < _introSlides.length - 1) { _introSlideIndex += 1; _introElapsedMs = 0; _introPhase = _WolfIntroPhase.fadeIn; diff --git a/packages/wolf_3d_dart/lib/src/rendering/ascii_renderer.dart b/packages/wolf_3d_dart/lib/src/rendering/ascii_renderer.dart index c78e8d9..fa73b33 100644 --- a/packages/wolf_3d_dart/lib/src/rendering/ascii_renderer.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/ascii_renderer.dart @@ -420,7 +420,7 @@ class AsciiRenderer extends CliRendererBackend { final art = WolfClassicMenuArt(engine.data); if (engine.menuManager.activeMenu == WolfMenuScreen.introSplash) { - _drawIntroSplash(engine, art); + _drawIntroSplash(engine, art, menuTypography); return; } @@ -708,10 +708,16 @@ class AsciiRenderer extends CliRendererBackend { } } - void _drawIntroSplash(WolfEngine engine, WolfClassicMenuArt art) { - final image = engine.menuManager.isIntroPg13Slide - ? art.mappedPic(WolfMenuPic.pg13) - : art.mappedPic(WolfMenuPic.title); + void _drawIntroSplash( + WolfEngine engine, + WolfClassicMenuArt art, + _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); if (engine.menuManager.isIntroPg13Slide && @@ -726,6 +732,10 @@ class AsciiRenderer extends CliRendererBackend { _fillRect(0, 0, width, height, activeTheme.solid, splashBg); } + if (engine.menuManager.isIntroRetailWarningSlide) { + _drawRetailWarningIntro(splashBg, menuTypography); + } + if (image != null) { final int x = engine.menuManager.isIntroPg13Slide ? (320 - image.width).clamp(0, 319) @@ -742,6 +752,58 @@ class AsciiRenderer extends CliRendererBackend { ); } + 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) { final List histogram = List.filled(256, 0); for (final int colorIndex in image.pixels) { @@ -767,6 +829,18 @@ class AsciiRenderer extends CliRendererBackend { } 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++) { final row = _screen[y]; for (int x = 0; x < row.length; x++) { diff --git a/packages/wolf_3d_dart/lib/src/rendering/sixel_renderer.dart b/packages/wolf_3d_dart/lib/src/rendering/sixel_renderer.dart index 93eed6e..4603e5b 100644 --- a/packages/wolf_3d_dart/lib/src/rendering/sixel_renderer.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/sixel_renderer.dart @@ -561,9 +561,11 @@ class SixelRenderer extends CliRendererBackend { } void _drawIntroSplash(WolfEngine engine, WolfClassicMenuArt art) { - final image = engine.menuManager.isIntroPg13Slide - ? art.mappedPic(WolfMenuPic.pg13) - : art.mappedPic(WolfMenuPic.title); + final image = switch (engine.menuManager.currentIntroSlide) { + WolfIntroSlide.retailWarning => null, + WolfIntroSlide.pg13 => art.mappedPic(WolfMenuPic.pg13), + WolfIntroSlide.title => art.mappedPic(WolfMenuPic.title), + }; int splashBg = _rgbToPaletteIndex(engine.menuManager.introBackgroundRgb); if (engine.menuManager.isIntroPg13Slide && @@ -576,6 +578,10 @@ class SixelRenderer extends CliRendererBackend { _screen[i] = splashBg; } + if (engine.menuManager.isIntroRetailWarningSlide) { + _drawRetailWarningIntro(splashBg); + } + if (image != null) { final int x = engine.menuManager.isIntroPg13Slide ? (320 - image.width).clamp(0, 319) @@ -592,6 +598,24 @@ class SixelRenderer extends CliRendererBackend { ); } + 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) { final List histogram = List.filled(256, 0); for (final int colorIndex in image.pixels) { diff --git a/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart b/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart index b663c09..ffb827e 100644 --- a/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart @@ -209,9 +209,11 @@ class SoftwareRenderer extends RendererBackend { } void _drawIntroSplash(WolfEngine engine, WolfClassicMenuArt art) { - final image = engine.menuManager.isIntroPg13Slide - ? art.mappedPic(WolfMenuPic.pg13) - : art.mappedPic(WolfMenuPic.title); + final image = switch (engine.menuManager.currentIntroSlide) { + WolfIntroSlide.retailWarning => null, + WolfIntroSlide.pg13 => art.mappedPic(WolfMenuPic.pg13), + WolfIntroSlide.title => art.mappedPic(WolfMenuPic.title), + }; int splashBgColor = _rgbToFrameColor(engine.menuManager.introBackgroundRgb); int? matteIndex; @@ -227,6 +229,10 @@ class SoftwareRenderer extends RendererBackend { _buffer.pixels[i] = splashBgColor; } + if (engine.menuManager.isIntroRetailWarningSlide) { + _drawRetailWarningIntro(splashBgColor); + } + if (image != null) { final int x = engine.menuManager.isIntroPg13Slide ? (320 - image.width).clamp(0, 319) @@ -247,6 +253,36 @@ class SoftwareRenderer extends RendererBackend { ); } + 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) { final List histogram = List.filled(256, 0); for (final int colorIndex in image.pixels) {