feat: Implement fizzle fade transition effects for menus and intros, enhancing visual transitions
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -13,6 +13,10 @@ enum WolfMenuScreen {
|
|||||||
|
|
||||||
enum WolfIntroSlide { retailWarning, pg13, title }
|
enum WolfIntroSlide { retailWarning, pg13, title }
|
||||||
|
|
||||||
|
enum WolfTransitionEffect { none, normalFade, fizzleFade }
|
||||||
|
|
||||||
|
enum WolfTransitionPhase { idle, covering, revealing }
|
||||||
|
|
||||||
enum _WolfIntroPhase { fadeIn, hold, fadeOut }
|
enum _WolfIntroPhase { fadeIn, hold, fadeOut }
|
||||||
|
|
||||||
enum WolfMenuMainAction {
|
enum WolfMenuMainAction {
|
||||||
@@ -102,9 +106,11 @@ class MenuManager {
|
|||||||
|
|
||||||
WolfMenuScreen _activeMenu = WolfMenuScreen.difficultySelect;
|
WolfMenuScreen _activeMenu = WolfMenuScreen.difficultySelect;
|
||||||
WolfMenuScreen? _transitionTarget;
|
WolfMenuScreen? _transitionTarget;
|
||||||
|
WolfTransitionEffect _transitionEffect = WolfTransitionEffect.normalFade;
|
||||||
int _transitionElapsedMs = 0;
|
int _transitionElapsedMs = 0;
|
||||||
bool _transitionSwappedMenu = false;
|
bool _transitionSwappedMenu = false;
|
||||||
WolfMenuScreen _introLandingMenu = WolfMenuScreen.mainMenu;
|
WolfMenuScreen _introLandingMenu = WolfMenuScreen.mainMenu;
|
||||||
|
WolfTransitionEffect _introEffect = WolfTransitionEffect.normalFade;
|
||||||
int _introSlideIndex = 0;
|
int _introSlideIndex = 0;
|
||||||
int _introElapsedMs = 0;
|
int _introElapsedMs = 0;
|
||||||
_WolfIntroPhase _introPhase = _WolfIntroPhase.fadeIn;
|
_WolfIntroPhase _introPhase = _WolfIntroPhase.fadeIn;
|
||||||
@@ -182,6 +188,34 @@ class MenuManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WolfTransitionEffect get introOverlayEffect {
|
||||||
|
if (!isIntroSplashActive || _introPhase == _WolfIntroPhase.hold) {
|
||||||
|
return WolfTransitionEffect.none;
|
||||||
|
}
|
||||||
|
return _introEffect;
|
||||||
|
}
|
||||||
|
|
||||||
|
WolfTransitionPhase get introOverlayPhase {
|
||||||
|
if (!isIntroSplashActive) {
|
||||||
|
return WolfTransitionPhase.idle;
|
||||||
|
}
|
||||||
|
switch (_introPhase) {
|
||||||
|
case _WolfIntroPhase.fadeIn:
|
||||||
|
return WolfTransitionPhase.revealing;
|
||||||
|
case _WolfIntroPhase.hold:
|
||||||
|
return WolfTransitionPhase.idle;
|
||||||
|
case _WolfIntroPhase.fadeOut:
|
||||||
|
return WolfTransitionPhase.covering;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double get introOverlayPhaseProgress {
|
||||||
|
if (!isIntroSplashActive || _introPhase == _WolfIntroPhase.hold) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
return (_introElapsedMs / introFadeDurationMs).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the fade alpha during transitions (0.0..1.0).
|
/// Returns the fade alpha during transitions (0.0..1.0).
|
||||||
double get transitionAlpha {
|
double get transitionAlpha {
|
||||||
if (!isTransitioning) {
|
if (!isTransitioning) {
|
||||||
@@ -195,6 +229,35 @@ class MenuManager {
|
|||||||
return (1.0 - (fadeInElapsed / half)).clamp(0.0, 1.0);
|
return (1.0 - (fadeInElapsed / half)).clamp(0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WolfTransitionEffect get transitionEffect {
|
||||||
|
if (!isTransitioning) {
|
||||||
|
return WolfTransitionEffect.none;
|
||||||
|
}
|
||||||
|
return _transitionEffect;
|
||||||
|
}
|
||||||
|
|
||||||
|
WolfTransitionPhase get transitionPhase {
|
||||||
|
if (!isTransitioning) {
|
||||||
|
return WolfTransitionPhase.idle;
|
||||||
|
}
|
||||||
|
final int half = transitionDurationMs ~/ 2;
|
||||||
|
if (_transitionElapsedMs < half) {
|
||||||
|
return WolfTransitionPhase.covering;
|
||||||
|
}
|
||||||
|
return WolfTransitionPhase.revealing;
|
||||||
|
}
|
||||||
|
|
||||||
|
double get transitionPhaseProgress {
|
||||||
|
if (!isTransitioning) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
final int half = transitionDurationMs ~/ 2;
|
||||||
|
if (_transitionElapsedMs < half) {
|
||||||
|
return (_transitionElapsedMs / half).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
return ((_transitionElapsedMs - half) / half).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
int get selectedMainIndex => _selectedMainIndex;
|
int get selectedMainIndex => _selectedMainIndex;
|
||||||
|
|
||||||
int get selectedGameIndex => _selectedGameIndex;
|
int get selectedGameIndex => _selectedGameIndex;
|
||||||
@@ -272,6 +335,7 @@ class MenuManager {
|
|||||||
Difficulty? initialDifficulty,
|
Difficulty? initialDifficulty,
|
||||||
bool hasResumableGame = false,
|
bool hasResumableGame = false,
|
||||||
bool initialGameIsRetail = false,
|
bool initialGameIsRetail = false,
|
||||||
|
WolfTransitionEffect introEffect = WolfTransitionEffect.normalFade,
|
||||||
}) {
|
}) {
|
||||||
_gameCount = gameCount;
|
_gameCount = gameCount;
|
||||||
_showResumeOption = hasResumableGame;
|
_showResumeOption = hasResumableGame;
|
||||||
@@ -286,6 +350,7 @@ class MenuManager {
|
|||||||
_introLandingMenu = WolfMenuScreen.mainMenu;
|
_introLandingMenu = WolfMenuScreen.mainMenu;
|
||||||
if (gameCount > 1) {
|
if (gameCount > 1) {
|
||||||
_activeMenu = WolfMenuScreen.gameSelect;
|
_activeMenu = WolfMenuScreen.gameSelect;
|
||||||
|
_introEffect = introEffect;
|
||||||
_introElapsedMs = 0;
|
_introElapsedMs = 0;
|
||||||
_introPhase = _WolfIntroPhase.fadeIn;
|
_introPhase = _WolfIntroPhase.fadeIn;
|
||||||
_introSlideIndex = 0;
|
_introSlideIndex = 0;
|
||||||
@@ -294,9 +359,13 @@ class MenuManager {
|
|||||||
WolfIntroSlide.title,
|
WolfIntroSlide.title,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
_startIntroSequence(includeRetailWarning: initialGameIsRetail);
|
_startIntroSequence(
|
||||||
|
includeRetailWarning: initialGameIsRetail,
|
||||||
|
effect: introEffect,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_transitionTarget = null;
|
_transitionTarget = null;
|
||||||
|
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||||
_transitionElapsedMs = 0;
|
_transitionElapsedMs = 0;
|
||||||
_transitionSwappedMenu = false;
|
_transitionSwappedMenu = false;
|
||||||
_resetEdgeState();
|
_resetEdgeState();
|
||||||
@@ -306,12 +375,17 @@ class MenuManager {
|
|||||||
void beginIntroSplash({
|
void beginIntroSplash({
|
||||||
WolfMenuScreen landingMenu = WolfMenuScreen.mainMenu,
|
WolfMenuScreen landingMenu = WolfMenuScreen.mainMenu,
|
||||||
bool includeRetailWarning = false,
|
bool includeRetailWarning = false,
|
||||||
|
WolfTransitionEffect effect = WolfTransitionEffect.normalFade,
|
||||||
}) {
|
}) {
|
||||||
_introLandingMenu = landingMenu;
|
_introLandingMenu = landingMenu;
|
||||||
_transitionTarget = null;
|
_transitionTarget = null;
|
||||||
|
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||||
_transitionElapsedMs = 0;
|
_transitionElapsedMs = 0;
|
||||||
_transitionSwappedMenu = false;
|
_transitionSwappedMenu = false;
|
||||||
_startIntroSequence(includeRetailWarning: includeRetailWarning);
|
_startIntroSequence(
|
||||||
|
includeRetailWarning: includeRetailWarning,
|
||||||
|
effect: effect,
|
||||||
|
);
|
||||||
_resetEdgeState();
|
_resetEdgeState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,8 +410,10 @@ class MenuManager {
|
|||||||
}
|
}
|
||||||
_activeMenu = WolfMenuScreen.mainMenu;
|
_activeMenu = WolfMenuScreen.mainMenu;
|
||||||
_transitionTarget = null;
|
_transitionTarget = null;
|
||||||
|
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||||
_transitionElapsedMs = 0;
|
_transitionElapsedMs = 0;
|
||||||
_transitionSwappedMenu = false;
|
_transitionSwappedMenu = false;
|
||||||
|
_introEffect = WolfTransitionEffect.normalFade;
|
||||||
_introElapsedMs = 0;
|
_introElapsedMs = 0;
|
||||||
_resetEdgeState();
|
_resetEdgeState();
|
||||||
}
|
}
|
||||||
@@ -449,6 +525,7 @@ class MenuManager {
|
|||||||
_isSelectableChangeViewIndex,
|
_isSelectableChangeViewIndex,
|
||||||
);
|
);
|
||||||
_transitionTarget = null;
|
_transitionTarget = null;
|
||||||
|
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||||
_transitionElapsedMs = 0;
|
_transitionElapsedMs = 0;
|
||||||
_transitionSwappedMenu = false;
|
_transitionSwappedMenu = false;
|
||||||
_resetEdgeState();
|
_resetEdgeState();
|
||||||
@@ -464,6 +541,7 @@ class MenuManager {
|
|||||||
_isSelectableRendererOptionIndex,
|
_isSelectableRendererOptionIndex,
|
||||||
);
|
);
|
||||||
_transitionTarget = null;
|
_transitionTarget = null;
|
||||||
|
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||||
_transitionElapsedMs = 0;
|
_transitionElapsedMs = 0;
|
||||||
_transitionSwappedMenu = false;
|
_transitionSwappedMenu = false;
|
||||||
_resetEdgeState();
|
_resetEdgeState();
|
||||||
@@ -473,11 +551,15 @@ class MenuManager {
|
|||||||
///
|
///
|
||||||
/// Hosts can reuse this fade timing for future pre-menu splash/image
|
/// Hosts can reuse this fade timing for future pre-menu splash/image
|
||||||
/// sequences so transitions feel consistent across the whole app.
|
/// sequences so transitions feel consistent across the whole app.
|
||||||
void startTransition(WolfMenuScreen target) {
|
void startTransition(
|
||||||
|
WolfMenuScreen target, {
|
||||||
|
WolfTransitionEffect effect = WolfTransitionEffect.normalFade,
|
||||||
|
}) {
|
||||||
if (_activeMenu == target) {
|
if (_activeMenu == target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_transitionTarget = target;
|
_transitionTarget = target;
|
||||||
|
_transitionEffect = effect;
|
||||||
_transitionElapsedMs = 0;
|
_transitionElapsedMs = 0;
|
||||||
_transitionSwappedMenu = false;
|
_transitionSwappedMenu = false;
|
||||||
_resetEdgeState();
|
_resetEdgeState();
|
||||||
@@ -501,6 +583,7 @@ class MenuManager {
|
|||||||
}
|
}
|
||||||
if (_transitionElapsedMs >= transitionDurationMs) {
|
if (_transitionElapsedMs >= transitionDurationMs) {
|
||||||
_transitionTarget = null;
|
_transitionTarget = null;
|
||||||
|
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||||
_transitionElapsedMs = 0;
|
_transitionElapsedMs = 0;
|
||||||
_transitionSwappedMenu = false;
|
_transitionSwappedMenu = false;
|
||||||
}
|
}
|
||||||
@@ -527,8 +610,12 @@ class MenuManager {
|
|||||||
_consumeEdgeState(input);
|
_consumeEdgeState(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startIntroSequence({required bool includeRetailWarning}) {
|
void _startIntroSequence({
|
||||||
|
required bool includeRetailWarning,
|
||||||
|
required WolfTransitionEffect effect,
|
||||||
|
}) {
|
||||||
_activeMenu = WolfMenuScreen.introSplash;
|
_activeMenu = WolfMenuScreen.introSplash;
|
||||||
|
_introEffect = effect;
|
||||||
_introSlides = includeRetailWarning
|
_introSlides = includeRetailWarning
|
||||||
? <WolfIntroSlide>[
|
? <WolfIntroSlide>[
|
||||||
WolfIntroSlide.retailWarning,
|
WolfIntroSlide.retailWarning,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:math' as math;
|
|||||||
|
|
||||||
import 'package:arcane_helper_utils/arcane_helper_utils.dart';
|
import 'package:arcane_helper_utils/arcane_helper_utils.dart';
|
||||||
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/rendering/fizzle_fade.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_menu.dart';
|
import 'package:wolf_3d_dart/wolf_3d_menu.dart';
|
||||||
@@ -130,6 +131,8 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
|
|
||||||
late List<List<ColoredChar>> _screen;
|
late List<List<ColoredChar>> _screen;
|
||||||
late List<List<int>> _scenePixels;
|
late List<List<int>> _scenePixels;
|
||||||
|
List<List<ColoredChar>> _screenScratch = const <List<ColoredChar>>[];
|
||||||
|
List<List<int>> _scenePixelsScratch = const <List<int>>[];
|
||||||
List<int>? _mainMenuBandFirstColumn;
|
List<int>? _mainMenuBandFirstColumn;
|
||||||
String? _lastLoggedThemeName;
|
String? _lastLoggedThemeName;
|
||||||
|
|
||||||
@@ -488,7 +491,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_drawCenteredMenuFooter();
|
_drawCenteredMenuFooter();
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuTransition(engine.menuManager, bgColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,7 +541,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_drawCenteredMenuFooter();
|
_drawCenteredMenuFooter();
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuTransition(engine.menuManager, bgColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,7 +600,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
_drawCenteredMenuFooter();
|
_drawCenteredMenuFooter();
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuTransition(engine.menuManager, bgColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,7 +641,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_drawCenteredMenuFooter();
|
_drawCenteredMenuFooter();
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuTransition(engine.menuManager, bgColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -742,7 +745,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_drawCenteredMenuFooter();
|
_drawCenteredMenuFooter();
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuTransition(engine.menuManager, bgColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -795,7 +798,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_drawCenteredMenuFooter();
|
_drawCenteredMenuFooter();
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuTransition(engine.menuManager, bgColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -851,7 +854,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
_drawCenteredMenuFooter();
|
_drawCenteredMenuFooter();
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuTransition(engine.menuManager, bgColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,7 +881,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_drawCenteredMenuFooter();
|
_drawCenteredMenuFooter();
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuTransition(engine.menuManager, bgColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _gameTitle(GameVersion version) {
|
String _gameTitle(GameVersion version) {
|
||||||
@@ -932,10 +935,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
_blitVgaImageAscii(image, x, y);
|
_blitVgaImageAscii(image, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyMenuFade(
|
_applyIntroTransition(engine.menuManager, _rgbToPaletteColor(0x000000));
|
||||||
engine.menuManager.introOverlayAlpha,
|
|
||||||
_rgbToPaletteColor(0x000000),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawRetailWarningIntro(
|
void _drawRetailWarningIntro(
|
||||||
@@ -1085,6 +1085,148 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _applyMenuTransition(MenuManager menuManager, int coverColor) {
|
||||||
|
switch (menuManager.transitionEffect) {
|
||||||
|
case WolfTransitionEffect.none:
|
||||||
|
return;
|
||||||
|
case WolfTransitionEffect.normalFade:
|
||||||
|
_applyMenuFade(menuManager.transitionAlpha, coverColor);
|
||||||
|
return;
|
||||||
|
case WolfTransitionEffect.fizzleFade:
|
||||||
|
_applyFizzleTransition(
|
||||||
|
phase: menuManager.transitionPhase,
|
||||||
|
progress: menuManager.transitionPhaseProgress,
|
||||||
|
coverColor: coverColor,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyIntroTransition(MenuManager menuManager, int coverColor) {
|
||||||
|
switch (menuManager.introOverlayEffect) {
|
||||||
|
case WolfTransitionEffect.none:
|
||||||
|
return;
|
||||||
|
case WolfTransitionEffect.normalFade:
|
||||||
|
_applyMenuFade(menuManager.introOverlayAlpha, coverColor);
|
||||||
|
return;
|
||||||
|
case WolfTransitionEffect.fizzleFade:
|
||||||
|
_applyFizzleTransition(
|
||||||
|
phase: menuManager.introOverlayPhase,
|
||||||
|
progress: menuManager.introOverlayPhaseProgress,
|
||||||
|
coverColor: coverColor,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyFizzleTransition({
|
||||||
|
required WolfTransitionPhase phase,
|
||||||
|
required double progress,
|
||||||
|
required int coverColor,
|
||||||
|
}) {
|
||||||
|
switch (phase) {
|
||||||
|
case WolfTransitionPhase.idle:
|
||||||
|
return;
|
||||||
|
case WolfTransitionPhase.covering:
|
||||||
|
_applyFizzleCover(progress, coverColor);
|
||||||
|
return;
|
||||||
|
case WolfTransitionPhase.revealing:
|
||||||
|
_applyFizzleReveal(progress, coverColor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyFizzleCover(double progress, int coverColor) {
|
||||||
|
final int coverCount = FizzleFade.revealCountForProgress(progress);
|
||||||
|
if (coverCount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FizzleFade.forEachCanonicalPixel(coverCount, (int x, int y) {
|
||||||
|
_fillRect320(x, y, 1, 1, coverColor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyFizzleReveal(double progress, int coverColor) {
|
||||||
|
final int revealCount = FizzleFade.revealCountForProgress(progress);
|
||||||
|
if (revealCount <= 0) {
|
||||||
|
if (_usesTerminalLayout) {
|
||||||
|
_fillTerminalRect(0, 0, width, _terminalPixelHeight, coverColor);
|
||||||
|
} else {
|
||||||
|
_fillRect(0, 0, width, height, ' ', coverColor);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (revealCount >= FizzleFade.canonicalPixelCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_captureTransitionScratch();
|
||||||
|
if (_usesTerminalLayout) {
|
||||||
|
_fillTerminalRect(0, 0, width, _terminalPixelHeight, coverColor);
|
||||||
|
} else {
|
||||||
|
_fillRect(0, 0, width, height, ' ', coverColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
FizzleFade.forEachCanonicalPixel(revealCount, (int x, int y) {
|
||||||
|
_copyCanonicalPixelFromScratch(x, y);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _captureTransitionScratch() {
|
||||||
|
if (_usesTerminalLayout) {
|
||||||
|
_scenePixelsScratch = List<List<int>>.generate(
|
||||||
|
_scenePixels.length,
|
||||||
|
(int y) => List<int>.from(_scenePixels[y]),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_screenScratch = List<List<ColoredChar>>.generate(
|
||||||
|
_screen.length,
|
||||||
|
(int y) => List<ColoredChar>.from(_screen[y]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _copyCanonicalPixelFromScratch(int x320, int y200) {
|
||||||
|
final double scaleX =
|
||||||
|
(_usesTerminalLayout ? projectionWidth : width) / 320.0;
|
||||||
|
final double scaleY =
|
||||||
|
(_usesTerminalLayout ? _terminalPixelHeight : height) / 200.0;
|
||||||
|
final int offsetX = _usesTerminalLayout ? projectionOffsetX : 0;
|
||||||
|
final int startX = offsetX + (x320 * scaleX).floor();
|
||||||
|
final int endX = offsetX + ((x320 + 1) * scaleX).ceil();
|
||||||
|
final int startY = (y200 * scaleY).floor();
|
||||||
|
final int endY = ((y200 + 1) * scaleY).ceil();
|
||||||
|
|
||||||
|
if (_usesTerminalLayout) {
|
||||||
|
for (int y = startY; y < endY; y++) {
|
||||||
|
if (y < 0 || y >= _terminalPixelHeight) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int x = startX; x < endX; x++) {
|
||||||
|
if (x < 0 || x >= width) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_scenePixels[y][x] = _scenePixelsScratch[y][x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int y = startY; y < endY; y++) {
|
||||||
|
if (y < 0 || y >= height) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int x = startX; x < endX; x++) {
|
||||||
|
if (x < 0 || x >= width) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_screen[y][x] = _screenScratch[y][x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _applyMenuFade(double alpha, int fadeColor) {
|
void _applyMenuFade(double alpha, int fadeColor) {
|
||||||
if (alpha <= 0.0) {
|
if (alpha <= 0.0) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
/// Canonical Wolf3D-style 17-bit Galois LFSR used for Fizzle Fade ordering.
|
||||||
|
///
|
||||||
|
/// The sequence mirrors the original coordinate extraction strategy:
|
||||||
|
/// low 8 bits minus one form `y`, the next 9 bits form `x`, and out-of-bounds
|
||||||
|
/// points are skipped. For menu and intro rendering we use the original
|
||||||
|
/// canonical 320x200 coordinate space and let renderers scale that to their
|
||||||
|
/// output buffer.
|
||||||
|
abstract final class FizzleFade {
|
||||||
|
static const int canonicalWidth = 320;
|
||||||
|
static const int canonicalHeight = 200;
|
||||||
|
static const int canonicalPixelCount = canonicalWidth * canonicalHeight;
|
||||||
|
static const int seed = 1;
|
||||||
|
static const int _xorMask = 0x12000;
|
||||||
|
static final List<int> _canonicalSequence = List<int>.unmodifiable(
|
||||||
|
_buildSequence(width: canonicalWidth, height: canonicalHeight),
|
||||||
|
);
|
||||||
|
|
||||||
|
static List<int> get canonicalSequence => _canonicalSequence;
|
||||||
|
|
||||||
|
static int nextState(int state) {
|
||||||
|
final bool lsbSet = (state & 1) != 0;
|
||||||
|
int next = state >> 1;
|
||||||
|
if (lsbSet) {
|
||||||
|
next ^= _xorMask;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int xForState(int state) => (state >> 8) & 0x1FF;
|
||||||
|
|
||||||
|
static int yForState(int state) => ((state & 0xFF) - 1) & 0xFF;
|
||||||
|
|
||||||
|
static bool isInBounds(int state, {required int width, required int height}) {
|
||||||
|
return xForState(state) < width && yForState(state) < height;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int revealCountForProgress(double progress, {int? pixelCount}) {
|
||||||
|
final int total = pixelCount ?? canonicalPixelCount;
|
||||||
|
return (progress.clamp(0.0, 1.0) * total).round().clamp(0, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void forEachCanonicalPixel(
|
||||||
|
int count,
|
||||||
|
void Function(int x, int y) plot,
|
||||||
|
) {
|
||||||
|
final int limit = count.clamp(0, canonicalPixelCount);
|
||||||
|
for (int i = 0; i < limit; i++) {
|
||||||
|
final int packed = _canonicalSequence[i];
|
||||||
|
plot(packed & 0xFFFF, packed >> 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<int> _buildSequence({required int width, required int height}) {
|
||||||
|
final List<int> points = <int>[];
|
||||||
|
int state = seed;
|
||||||
|
do {
|
||||||
|
if (isInBounds(state, width: width, height: height)) {
|
||||||
|
points.add((yForState(state) << 16) | xForState(state));
|
||||||
|
}
|
||||||
|
state = nextState(state);
|
||||||
|
} while (state != seed);
|
||||||
|
return UnmodifiableListView<int>(points);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:wolf_3d_dart/src/input/cli_input.dart';
|
import 'package:wolf_3d_dart/src/input/cli_input.dart';
|
||||||
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/rendering/fizzle_fade.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_menu.dart';
|
import 'package:wolf_3d_dart/wolf_3d_menu.dart';
|
||||||
@@ -36,6 +37,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
static const int _headerHeadingY = 24;
|
static const int _headerHeadingY = 24;
|
||||||
|
|
||||||
late Uint8List _screen;
|
late Uint8List _screen;
|
||||||
|
Uint8List _transitionScratch = Uint8List(0);
|
||||||
List<int>? _mainMenuBandFirstColumn;
|
List<int>? _mainMenuBandFirstColumn;
|
||||||
int _offsetColumns = 0;
|
int _offsetColumns = 0;
|
||||||
int _offsetRows = 0;
|
int _offsetRows = 0;
|
||||||
@@ -435,7 +437,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
scale: 1,
|
scale: 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuTransition(engine.menuManager, bgColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,7 +472,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
scale: 1,
|
scale: 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuTransition(engine.menuManager, bgColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,7 +524,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuTransition(engine.menuManager, bgColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,7 +617,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
scale: 1,
|
scale: 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuTransition(engine.menuManager, bgColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,7 +664,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
scale: 1,
|
scale: 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuTransition(engine.menuManager, bgColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,7 +678,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
_fillRect320(28, 70, 264, 82, panelColor);
|
_fillRect320(28, 70, 264, 82, panelColor);
|
||||||
if (_useCompactMenuLayout) {
|
if (_useCompactMenuLayout) {
|
||||||
_drawCompactMenu(selectedDifficultyIndex, headingIndex, panelColor);
|
_drawCompactMenu(selectedDifficultyIndex, headingIndex, panelColor);
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuTransition(engine.menuManager, bgColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,7 +719,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuTransition(engine.menuManager, bgColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawCustomizeMenuHeader(
|
void _drawCustomizeMenuHeader(
|
||||||
@@ -816,10 +818,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
_blitVgaImage(image, x, y);
|
_blitVgaImage(image, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyMenuFade(
|
_applyIntroTransition(engine.menuManager, _rgbToPaletteIndex(0x000000));
|
||||||
engine.menuManager.introOverlayAlpha,
|
|
||||||
_rgbToPaletteIndex(0x000000),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawRetailWarningIntro(int backgroundColor) {
|
void _drawRetailWarningIntro(int backgroundColor) {
|
||||||
@@ -861,6 +860,117 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
return bestIndex;
|
return bestIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _applyMenuTransition(MenuManager menuManager, int coverColor) {
|
||||||
|
switch (menuManager.transitionEffect) {
|
||||||
|
case WolfTransitionEffect.none:
|
||||||
|
return;
|
||||||
|
case WolfTransitionEffect.normalFade:
|
||||||
|
_applyMenuFade(menuManager.transitionAlpha, coverColor);
|
||||||
|
return;
|
||||||
|
case WolfTransitionEffect.fizzleFade:
|
||||||
|
_applyFizzleTransition(
|
||||||
|
phase: menuManager.transitionPhase,
|
||||||
|
progress: menuManager.transitionPhaseProgress,
|
||||||
|
coverColor: coverColor,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyIntroTransition(MenuManager menuManager, int coverColor) {
|
||||||
|
switch (menuManager.introOverlayEffect) {
|
||||||
|
case WolfTransitionEffect.none:
|
||||||
|
return;
|
||||||
|
case WolfTransitionEffect.normalFade:
|
||||||
|
_applyMenuFade(menuManager.introOverlayAlpha, coverColor);
|
||||||
|
return;
|
||||||
|
case WolfTransitionEffect.fizzleFade:
|
||||||
|
_applyFizzleTransition(
|
||||||
|
phase: menuManager.introOverlayPhase,
|
||||||
|
progress: menuManager.introOverlayPhaseProgress,
|
||||||
|
coverColor: coverColor,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyFizzleTransition({
|
||||||
|
required WolfTransitionPhase phase,
|
||||||
|
required double progress,
|
||||||
|
required int coverColor,
|
||||||
|
}) {
|
||||||
|
switch (phase) {
|
||||||
|
case WolfTransitionPhase.idle:
|
||||||
|
return;
|
||||||
|
case WolfTransitionPhase.covering:
|
||||||
|
_applyFizzleCover(progress, coverColor);
|
||||||
|
return;
|
||||||
|
case WolfTransitionPhase.revealing:
|
||||||
|
_applyFizzleReveal(progress, coverColor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyFizzleCover(double progress, int coverColor) {
|
||||||
|
final int coverCount = FizzleFade.revealCountForProgress(progress);
|
||||||
|
if (coverCount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FizzleFade.forEachCanonicalPixel(coverCount, (int x, int y) {
|
||||||
|
_fillRect320(x, y, 1, 1, coverColor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyFizzleReveal(double progress, int coverColor) {
|
||||||
|
final int revealCount = FizzleFade.revealCountForProgress(progress);
|
||||||
|
if (revealCount <= 0) {
|
||||||
|
_screen.fillRange(0, _screen.length, coverColor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (revealCount >= FizzleFade.canonicalPixelCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensureTransitionScratch();
|
||||||
|
_transitionScratch.setAll(0, _screen);
|
||||||
|
_screen.fillRange(0, _screen.length, coverColor);
|
||||||
|
|
||||||
|
FizzleFade.forEachCanonicalPixel(revealCount, (int x, int y) {
|
||||||
|
_copyCanonicalPixelFromScratch(x, y);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _ensureTransitionScratch() {
|
||||||
|
if (_transitionScratch.length == _screen.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_transitionScratch = Uint8List(_screen.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _copyCanonicalPixelFromScratch(int x320, int y200) {
|
||||||
|
final double scaleX = width / 320.0;
|
||||||
|
final double scaleY = height / 200.0;
|
||||||
|
final int startX = (x320 * scaleX).floor();
|
||||||
|
final int endX = ((x320 + 1) * scaleX).ceil();
|
||||||
|
final int startY = (y200 * scaleY).floor();
|
||||||
|
final int endY = ((y200 + 1) * scaleY).ceil();
|
||||||
|
|
||||||
|
for (int y = startY; y < endY; y++) {
|
||||||
|
if (y < 0 || y >= height) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final int rowOffset = y * width;
|
||||||
|
for (int x = startX; x < endX; x++) {
|
||||||
|
if (x < 0 || x >= width) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final int index = rowOffset + x;
|
||||||
|
_screen[index] = _transitionScratch[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _applyMenuFade(double alpha, int bgColor) {
|
void _applyMenuFade(double alpha, int bgColor) {
|
||||||
if (alpha <= 0.0) {
|
if (alpha <= 0.0) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/rendering/fizzle_fade.dart';
|
||||||
import 'package:wolf_3d_dart/src/rendering/menu_font.dart';
|
import 'package:wolf_3d_dart/src/rendering/menu_font.dart';
|
||||||
import 'package:wolf_3d_dart/src/rendering/renderer_backend.dart';
|
import 'package:wolf_3d_dart/src/rendering/renderer_backend.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
@@ -18,6 +20,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
static const int _headerHeadingY = 24;
|
static const int _headerHeadingY = 24;
|
||||||
|
|
||||||
late FrameBuffer _buffer;
|
late FrameBuffer _buffer;
|
||||||
|
Uint32List _transitionScratch = Uint32List(0);
|
||||||
List<int>? _mainMenuBandFirstColumn;
|
List<int>? _mainMenuBandFirstColumn;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -233,7 +236,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_applyMenuTransition(engine.menuManager, bgColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawIntroSplash(WolfEngine engine, WolfClassicMenuArt art) {
|
void _drawIntroSplash(WolfEngine engine, WolfClassicMenuArt art) {
|
||||||
@@ -275,10 +278,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyMenuFade(
|
_applyIntroTransition(engine.menuManager, _rgbToFrameColor(0x000000));
|
||||||
engine.menuManager.introOverlayAlpha,
|
|
||||||
_rgbToFrameColor(0x000000),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawRetailWarningIntro(int backgroundColor) {
|
void _drawRetailWarningIntro(int backgroundColor) {
|
||||||
@@ -550,7 +550,8 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
? 0
|
? 0
|
||||||
: ((optionEntries.length - 1) * optionsRowStep) + 10;
|
: ((optionEntries.length - 1) * optionsRowStep) + 10;
|
||||||
final int optionsRowStart =
|
final int optionsRowStart =
|
||||||
optionsPanelY + ((optionsPanelH - optionsRowsHeight) ~/ 2).clamp(0, 200);
|
optionsPanelY +
|
||||||
|
((optionsPanelH - optionsRowsHeight) ~/ 2).clamp(0, 200);
|
||||||
for (int i = 0; i < optionEntries.length; i++) {
|
for (int i = 0; i < optionEntries.length; i++) {
|
||||||
final int optionIndex = modeCount + i;
|
final int optionIndex = modeCount + i;
|
||||||
final bool isSelected = optionIndex == selectedIndex;
|
final bool isSelected = optionIndex == selectedIndex;
|
||||||
@@ -937,6 +938,40 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _applyMenuTransition(MenuManager menuManager, int coverColor) {
|
||||||
|
switch (menuManager.transitionEffect) {
|
||||||
|
case WolfTransitionEffect.none:
|
||||||
|
return;
|
||||||
|
case WolfTransitionEffect.normalFade:
|
||||||
|
_applyMenuFade(menuManager.transitionAlpha, coverColor);
|
||||||
|
return;
|
||||||
|
case WolfTransitionEffect.fizzleFade:
|
||||||
|
_applyFizzleTransition(
|
||||||
|
phase: menuManager.transitionPhase,
|
||||||
|
progress: menuManager.transitionPhaseProgress,
|
||||||
|
coverColor: coverColor,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyIntroTransition(MenuManager menuManager, int coverColor) {
|
||||||
|
switch (menuManager.introOverlayEffect) {
|
||||||
|
case WolfTransitionEffect.none:
|
||||||
|
return;
|
||||||
|
case WolfTransitionEffect.normalFade:
|
||||||
|
_applyMenuFade(menuManager.introOverlayAlpha, coverColor);
|
||||||
|
return;
|
||||||
|
case WolfTransitionEffect.fizzleFade:
|
||||||
|
_applyFizzleTransition(
|
||||||
|
phase: menuManager.introOverlayPhase,
|
||||||
|
progress: menuManager.introOverlayPhaseProgress,
|
||||||
|
coverColor: coverColor,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _applyMenuFade(double alpha, int fadeColor) {
|
void _applyMenuFade(double alpha, int fadeColor) {
|
||||||
if (alpha <= 0.0) {
|
if (alpha <= 0.0) {
|
||||||
return;
|
return;
|
||||||
@@ -959,6 +994,81 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _applyFizzleTransition({
|
||||||
|
required WolfTransitionPhase phase,
|
||||||
|
required double progress,
|
||||||
|
required int coverColor,
|
||||||
|
}) {
|
||||||
|
switch (phase) {
|
||||||
|
case WolfTransitionPhase.idle:
|
||||||
|
return;
|
||||||
|
case WolfTransitionPhase.covering:
|
||||||
|
_applyFizzleCover(progress, coverColor);
|
||||||
|
return;
|
||||||
|
case WolfTransitionPhase.revealing:
|
||||||
|
_applyFizzleReveal(progress, coverColor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyFizzleCover(double progress, int coverColor) {
|
||||||
|
final int coverCount = FizzleFade.revealCountForProgress(progress);
|
||||||
|
if (coverCount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FizzleFade.forEachCanonicalPixel(coverCount, (int x, int y) {
|
||||||
|
_fillCanonicalRect(x, y, 1, 1, coverColor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyFizzleReveal(double progress, int coverColor) {
|
||||||
|
final int revealCount = FizzleFade.revealCountForProgress(progress);
|
||||||
|
if (revealCount <= 0) {
|
||||||
|
_buffer.pixels.fillRange(0, _buffer.pixels.length, coverColor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (revealCount >= FizzleFade.canonicalPixelCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensureTransitionScratch();
|
||||||
|
_transitionScratch.setAll(0, _buffer.pixels);
|
||||||
|
_buffer.pixels.fillRange(0, _buffer.pixels.length, coverColor);
|
||||||
|
|
||||||
|
FizzleFade.forEachCanonicalPixel(revealCount, (int x, int y) {
|
||||||
|
_copyCanonicalPixelFromScratch(x, y);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _ensureTransitionScratch() {
|
||||||
|
if (_transitionScratch.length == _buffer.pixels.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_transitionScratch = Uint32List(_buffer.pixels.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _copyCanonicalPixelFromScratch(int startX320, int startY200) {
|
||||||
|
final int startX = (startX320 * _uiScaleX).floor();
|
||||||
|
final int endX = ((startX320 + 1) * _uiScaleX).ceil();
|
||||||
|
final int startY = (startY200 * _uiScaleY).floor();
|
||||||
|
final int endY = ((startY200 + 1) * _uiScaleY).ceil();
|
||||||
|
|
||||||
|
for (int y = startY; y < endY; y++) {
|
||||||
|
if (y < 0 || y >= height) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final int rowStart = y * width;
|
||||||
|
for (int x = startX; x < endX; x++) {
|
||||||
|
if (x < 0 || x >= width) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final int index = rowStart + x;
|
||||||
|
_buffer.pixels[index] = _transitionScratch[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Converts an `RRGGBB` menu color into the framebuffer's packed channel
|
/// Converts an `RRGGBB` menu color into the framebuffer's packed channel
|
||||||
/// order (`0xAABBGGRR`) used throughout this renderer.
|
/// order (`0xAABBGGRR`) used throughout this renderer.
|
||||||
int _rgbToFrameColor(int rgb) {
|
int _rgbToFrameColor(int rgb) {
|
||||||
|
|||||||
@@ -223,6 +223,46 @@ void main() {
|
|||||||
expect(manager.selectedMainIndex, 0);
|
expect(manager.selectedMainIndex, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('menu transition defaults to normal fade and can opt into fizzle', () {
|
||||||
|
final manager = MenuManager();
|
||||||
|
|
||||||
|
manager.showMainMenu(hasResumableGame: false);
|
||||||
|
manager.startTransition(WolfMenuScreen.difficultySelect);
|
||||||
|
|
||||||
|
expect(manager.transitionEffect, WolfTransitionEffect.normalFade);
|
||||||
|
expect(manager.transitionPhase, WolfTransitionPhase.covering);
|
||||||
|
expect(manager.transitionPhaseProgress, 0.0);
|
||||||
|
expect(manager.activeMenu, WolfMenuScreen.mainMenu);
|
||||||
|
|
||||||
|
manager.tickTransition(MenuManager.transitionDurationMs ~/ 4);
|
||||||
|
expect(manager.transitionPhase, WolfTransitionPhase.covering);
|
||||||
|
expect(manager.transitionPhaseProgress, closeTo(0.5, 0.001));
|
||||||
|
expect(manager.activeMenu, WolfMenuScreen.mainMenu);
|
||||||
|
|
||||||
|
manager.tickTransition(MenuManager.transitionDurationMs ~/ 4);
|
||||||
|
expect(manager.transitionPhase, WolfTransitionPhase.revealing);
|
||||||
|
expect(manager.transitionPhaseProgress, 0.0);
|
||||||
|
expect(manager.activeMenu, WolfMenuScreen.difficultySelect);
|
||||||
|
|
||||||
|
manager.tickTransition(MenuManager.transitionDurationMs ~/ 4);
|
||||||
|
expect(manager.transitionPhase, WolfTransitionPhase.revealing);
|
||||||
|
expect(manager.transitionPhaseProgress, closeTo(0.5, 0.001));
|
||||||
|
|
||||||
|
manager.tickTransition(MenuManager.transitionDurationMs ~/ 4);
|
||||||
|
expect(manager.isTransitioning, isFalse);
|
||||||
|
expect(manager.transitionEffect, WolfTransitionEffect.none);
|
||||||
|
expect(manager.transitionPhase, WolfTransitionPhase.idle);
|
||||||
|
expect(manager.transitionPhaseProgress, 0.0);
|
||||||
|
|
||||||
|
manager.startTransition(
|
||||||
|
WolfMenuScreen.mainMenu,
|
||||||
|
effect: WolfTransitionEffect.fizzleFade,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.transitionEffect, WolfTransitionEffect.fizzleFade);
|
||||||
|
expect(manager.transitionPhase, WolfTransitionPhase.covering);
|
||||||
|
});
|
||||||
|
|
||||||
test('quit selection triggers dedicated quit callback', () {
|
test('quit selection triggers dedicated quit callback', () {
|
||||||
final input = _TestInput();
|
final input = _TestInput();
|
||||||
int quitCalls = 0;
|
int quitCalls = 0;
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/rendering/fizzle_fade.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_renderer.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Fizzle fade sequence', () {
|
||||||
|
test('covers every canonical menu pixel exactly once', () {
|
||||||
|
final sequence = FizzleFade.canonicalSequence;
|
||||||
|
|
||||||
|
expect(sequence.length, FizzleFade.canonicalPixelCount);
|
||||||
|
expect(sequence.toSet().length, FizzleFade.canonicalPixelCount);
|
||||||
|
expect(sequence.first, 0);
|
||||||
|
expect(FizzleFade.nextState(FizzleFade.seed), 0x12000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Software renderer transitions', () {
|
||||||
|
test(
|
||||||
|
'default intro fade-in starts fully black and reveals by hold phase',
|
||||||
|
() {
|
||||||
|
final engine = _buildEngine(frameBuffer: FrameBuffer(320, 200));
|
||||||
|
final renderer = SoftwareRenderer();
|
||||||
|
final int black = _rgbToFrameColor(0x000000);
|
||||||
|
|
||||||
|
engine.init();
|
||||||
|
|
||||||
|
final FrameBuffer startFrame = renderer.render(engine);
|
||||||
|
expect(startFrame.pixels.every((pixel) => pixel == black), isTrue);
|
||||||
|
|
||||||
|
engine.menuManager.tickTransition(MenuManager.introFadeDurationMs);
|
||||||
|
|
||||||
|
final FrameBuffer revealedFrame = renderer.render(engine);
|
||||||
|
expect(revealedFrame.pixels.any((pixel) => pixel != black), isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'fizzle menu transition covers at midpoint and reveals target menu on completion',
|
||||||
|
() {
|
||||||
|
final engine = _buildEngine(frameBuffer: FrameBuffer(320, 200));
|
||||||
|
final baselineEngine = _buildEngine(frameBuffer: FrameBuffer(320, 200));
|
||||||
|
final difficultyEngine = _buildEngine(
|
||||||
|
frameBuffer: FrameBuffer(320, 200),
|
||||||
|
);
|
||||||
|
final renderer = SoftwareRenderer();
|
||||||
|
final int bgColor = _rgbToFrameColor(
|
||||||
|
engine.menuManager.menuBackgroundRgb,
|
||||||
|
);
|
||||||
|
|
||||||
|
engine.init();
|
||||||
|
baselineEngine.init();
|
||||||
|
difficultyEngine.init();
|
||||||
|
|
||||||
|
engine.menuManager.showMainMenu(hasResumableGame: false);
|
||||||
|
baselineEngine.menuManager.showMainMenu(hasResumableGame: false);
|
||||||
|
difficultyEngine.menuManager.beginDifficultySelection();
|
||||||
|
|
||||||
|
final List<int> mainMenuFrame = List<int>.from(
|
||||||
|
renderer.render(baselineEngine).pixels,
|
||||||
|
);
|
||||||
|
|
||||||
|
engine.menuManager.startTransition(
|
||||||
|
WolfMenuScreen.difficultySelect,
|
||||||
|
effect: WolfTransitionEffect.fizzleFade,
|
||||||
|
);
|
||||||
|
engine.menuManager.tickTransition(
|
||||||
|
MenuManager.transitionDurationMs ~/ 4,
|
||||||
|
);
|
||||||
|
final List<int> partialFrame = List<int>.from(
|
||||||
|
renderer.render(engine).pixels,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
_countDifferentPixels(partialFrame, mainMenuFrame),
|
||||||
|
greaterThan(0),
|
||||||
|
);
|
||||||
|
|
||||||
|
engine.menuManager.tickTransition(
|
||||||
|
MenuManager.transitionDurationMs ~/ 4,
|
||||||
|
);
|
||||||
|
final List<int> midpointFrame = List<int>.from(
|
||||||
|
renderer.render(engine).pixels,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(midpointFrame.every((pixel) => pixel == bgColor), isTrue);
|
||||||
|
|
||||||
|
engine.menuManager.tickTransition(
|
||||||
|
MenuManager.transitionDurationMs ~/ 2,
|
||||||
|
);
|
||||||
|
final List<int> completedFrame = List<int>.from(
|
||||||
|
renderer.render(engine).pixels,
|
||||||
|
);
|
||||||
|
final List<int> expectedDifficultyFrame = List<int>.from(
|
||||||
|
renderer.render(difficultyEngine).pixels,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(completedFrame, orderedEquals(expectedDifficultyFrame));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
WolfEngine _buildEngine({required FrameBuffer frameBuffer}) {
|
||||||
|
return WolfEngine(
|
||||||
|
data: _buildTestData(),
|
||||||
|
difficulty: null,
|
||||||
|
startingEpisode: 0,
|
||||||
|
frameBuffer: frameBuffer,
|
||||||
|
input: _TestInput(),
|
||||||
|
engineAudio: _SilentAudio(),
|
||||||
|
onGameWon: () {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
WolfensteinData _buildTestData() {
|
||||||
|
final wallGrid = _buildGrid();
|
||||||
|
final objectGrid = _buildGrid();
|
||||||
|
|
||||||
|
_fillBoundaries(wallGrid, 2);
|
||||||
|
objectGrid[2][2] = MapObject.playerEast;
|
||||||
|
|
||||||
|
return WolfensteinData(
|
||||||
|
version: GameVersion.retail,
|
||||||
|
dataVersion: DataVersion.unknown,
|
||||||
|
registry: RetailAssetRegistry(),
|
||||||
|
walls: [
|
||||||
|
_solidSprite(1),
|
||||||
|
_solidSprite(1),
|
||||||
|
_solidSprite(2),
|
||||||
|
_solidSprite(2),
|
||||||
|
],
|
||||||
|
sprites: List.generate(436, (_) => _solidSprite(255)),
|
||||||
|
sounds: List.generate(200, (_) => PcmSound(Uint8List(1))),
|
||||||
|
adLibSounds: const [],
|
||||||
|
music: const [],
|
||||||
|
vgaImages: const [],
|
||||||
|
episodes: [
|
||||||
|
Episode(
|
||||||
|
name: 'Episode 1',
|
||||||
|
levels: [
|
||||||
|
WolfLevel(
|
||||||
|
name: 'Level 1',
|
||||||
|
wallGrid: wallGrid,
|
||||||
|
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||||
|
objectGrid: objectGrid,
|
||||||
|
music: Music.level01,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TestInput extends Wolf3dInput {
|
||||||
|
@override
|
||||||
|
void update() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SilentAudio implements EngineAudio {
|
||||||
|
@override
|
||||||
|
WolfensteinData? activeGame;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> debugSoundTest() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> init() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playLevelMusic(Music music) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playMenuMusic() {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playSoundEffect(SoundEffect effect) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playSoundEffectId(int sfxId) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void stopMusic() {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> stopAllAudio() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
int _countDifferentPixels(List<int> a, List<int> b) {
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < a.length; i++) {
|
||||||
|
if (a[i] != b[i]) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _rgbToFrameColor(int rgb) {
|
||||||
|
final int r = (rgb >> 16) & 0xFF;
|
||||||
|
final int g = (rgb >> 8) & 0xFF;
|
||||||
|
final int b = rgb & 0xFF;
|
||||||
|
return 0xFF000000 | (b << 16) | (g << 8) | r;
|
||||||
|
}
|
||||||
|
|
||||||
|
SpriteMap _buildGrid() => List.generate(64, (_) => List.filled(64, 0));
|
||||||
|
|
||||||
|
void _fillBoundaries(SpriteMap grid, int wallId) {
|
||||||
|
for (int i = 0; i < 64; i++) {
|
||||||
|
grid[0][i] = wallId;
|
||||||
|
grid[63][i] = wallId;
|
||||||
|
grid[i][0] = wallId;
|
||||||
|
grid[i][63] = wallId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Sprite _solidSprite(int colorIndex) {
|
||||||
|
return Sprite(Uint8List.fromList(List.filled(64 * 64, colorIndex)));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user