feat: Add intro splash screen with transition effects and rendering support

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-20 12:05:34 +01:00
parent 9733516693
commit b23c02f716
6 changed files with 367 additions and 14 deletions
@@ -1,7 +1,15 @@
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
enum WolfMenuScreen { mainMenu, gameSelect, episodeSelect, difficultySelect }
enum WolfMenuScreen {
introSplash,
mainMenu,
gameSelect,
episodeSelect,
difficultySelect,
}
enum _WolfIntroPhase { fadeIn, hold, fadeOut }
enum WolfMenuMainAction {
newGame,
@@ -52,11 +60,20 @@ bool _isWiredMainMenuAction(WolfMenuMainAction action) {
/// Handles menu-only input state such as selection movement and edge triggers.
class MenuManager {
static const int transitionDurationMs = 280;
static const int introFadeDurationMs = 280;
static const int _introSlideCount = 2;
static const int _introPg13BackgroundRgb = 0x33A2E8;
static const int _introTitleBackgroundRgb = 0x000000;
WolfMenuScreen _activeMenu = WolfMenuScreen.difficultySelect;
WolfMenuScreen? _transitionTarget;
int _transitionElapsedMs = 0;
bool _transitionSwappedMenu = false;
WolfMenuScreen _introLandingMenu = WolfMenuScreen.mainMenu;
int _introSlideIndex = 0;
int _introElapsedMs = 0;
_WolfIntroPhase _introPhase = _WolfIntroPhase.fadeIn;
bool _introAdvanceRequested = false;
int _selectedMainIndex = 0;
int _selectedGameIndex = 0;
@@ -74,6 +91,27 @@ class MenuManager {
bool get isTransitioning => _transitionTarget != null;
bool get isIntroSplashActive => _activeMenu == WolfMenuScreen.introSplash;
bool get isIntroPg13Slide => _introSlideIndex == 0;
int get introBackgroundRgb =>
isIntroPg13Slide ? _introPg13BackgroundRgb : _introTitleBackgroundRgb;
double get introOverlayAlpha {
if (!isIntroSplashActive) {
return 0.0;
}
switch (_introPhase) {
case _WolfIntroPhase.fadeIn:
return (1.0 - (_introElapsedMs / introFadeDurationMs)).clamp(0.0, 1.0);
case _WolfIntroPhase.hold:
return 0.0;
case _WolfIntroPhase.fadeOut:
return (_introElapsedMs / introFadeDurationMs).clamp(0.0, 1.0);
}
}
/// Returns the fade alpha during transitions (0.0..1.0).
double get transitionAlpha {
if (!isTransitioning) {
@@ -162,15 +200,33 @@ class MenuManager {
: Difficulty.values
.indexOf(initialDifficulty)
.clamp(0, Difficulty.values.length - 1);
_activeMenu = gameCount > 1
? WolfMenuScreen.gameSelect
: WolfMenuScreen.mainMenu;
_introLandingMenu = WolfMenuScreen.mainMenu;
if (gameCount > 1) {
_activeMenu = WolfMenuScreen.gameSelect;
_introElapsedMs = 0;
_introPhase = _WolfIntroPhase.fadeIn;
_introSlideIndex = 0;
} else {
_startIntroSequence();
}
_transitionTarget = null;
_transitionElapsedMs = 0;
_transitionSwappedMenu = false;
_resetEdgeState();
}
/// Starts the intro splash sequence and lands on [landingMenu] when done.
void beginIntroSplash({
WolfMenuScreen landingMenu = WolfMenuScreen.mainMenu,
}) {
_introLandingMenu = landingMenu;
_transitionTarget = null;
_transitionElapsedMs = 0;
_transitionSwappedMenu = false;
_startIntroSequence();
_resetEdgeState();
}
/// Resets menu navigation state for a new difficulty selection flow.
void beginDifficultySelection({Difficulty? initialDifficulty}) {
beginSelectionFlow(
@@ -194,6 +250,7 @@ class MenuManager {
_transitionTarget = null;
_transitionElapsedMs = 0;
_transitionSwappedMenu = false;
_introElapsedMs = 0;
_resetEdgeState();
}
@@ -213,6 +270,11 @@ class MenuManager {
/// Advances transition timers and swaps menu at midpoint.
void tickTransition(int deltaMs) {
if (isIntroSplashActive) {
_tickIntro(deltaMs);
return;
}
if (!isTransitioning) {
return;
}
@@ -229,6 +291,85 @@ class MenuManager {
}
}
void updateIntroSplash(EngineInput input) {
if (!isIntroSplashActive) {
return;
}
final bool confirmNow = input.isInteracting;
if (confirmNow && !_prevConfirm) {
if (_introPhase == _WolfIntroPhase.fadeOut) {
// Ignore repeat confirms while already transitioning out.
} else if (_introPhase == _WolfIntroPhase.hold) {
_introPhase = _WolfIntroPhase.fadeOut;
_introElapsedMs = 0;
} else {
// Queue advance while fade-in is still in progress.
_introAdvanceRequested = true;
}
}
_consumeEdgeState(input);
}
void _startIntroSequence() {
_activeMenu = WolfMenuScreen.introSplash;
_introSlideIndex = 0;
_introElapsedMs = 0;
_introPhase = _WolfIntroPhase.fadeIn;
_introAdvanceRequested = false;
}
void _tickIntro(int deltaMs) {
if (!isIntroSplashActive) {
return;
}
_introElapsedMs += deltaMs;
switch (_introPhase) {
case _WolfIntroPhase.fadeIn:
if (_introElapsedMs >= introFadeDurationMs) {
_introElapsedMs = 0;
if (_introAdvanceRequested) {
_introPhase = _WolfIntroPhase.fadeOut;
_introAdvanceRequested = false;
} else {
_introPhase = _WolfIntroPhase.hold;
}
}
break;
case _WolfIntroPhase.hold:
// Hold indefinitely until the user confirms.
_introElapsedMs = 0;
if (_introAdvanceRequested) {
_introPhase = _WolfIntroPhase.fadeOut;
_introAdvanceRequested = false;
}
break;
case _WolfIntroPhase.fadeOut:
if (_introElapsedMs >= introFadeDurationMs) {
_advanceIntroSlide();
}
break;
}
}
void _advanceIntroSlide() {
if (_introSlideIndex < _introSlideCount - 1) {
_introSlideIndex += 1;
_introElapsedMs = 0;
_introPhase = _WolfIntroPhase.fadeIn;
_introAdvanceRequested = false;
return;
}
_activeMenu = _introLandingMenu;
_introElapsedMs = 0;
_introPhase = _WolfIntroPhase.fadeIn;
_introAdvanceRequested = false;
}
void clearEpisodeSelection() {
_selectedEpisodeIndex = 0;
}