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 WolfTransitionEffect { none, normalFade, fizzleFade }
|
||||
|
||||
enum WolfTransitionPhase { idle, covering, revealing }
|
||||
|
||||
enum _WolfIntroPhase { fadeIn, hold, fadeOut }
|
||||
|
||||
enum WolfMenuMainAction {
|
||||
@@ -102,9 +106,11 @@ class MenuManager {
|
||||
|
||||
WolfMenuScreen _activeMenu = WolfMenuScreen.difficultySelect;
|
||||
WolfMenuScreen? _transitionTarget;
|
||||
WolfTransitionEffect _transitionEffect = WolfTransitionEffect.normalFade;
|
||||
int _transitionElapsedMs = 0;
|
||||
bool _transitionSwappedMenu = false;
|
||||
WolfMenuScreen _introLandingMenu = WolfMenuScreen.mainMenu;
|
||||
WolfTransitionEffect _introEffect = WolfTransitionEffect.normalFade;
|
||||
int _introSlideIndex = 0;
|
||||
int _introElapsedMs = 0;
|
||||
_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).
|
||||
double get transitionAlpha {
|
||||
if (!isTransitioning) {
|
||||
@@ -195,6 +229,35 @@ class MenuManager {
|
||||
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 selectedGameIndex => _selectedGameIndex;
|
||||
@@ -272,6 +335,7 @@ class MenuManager {
|
||||
Difficulty? initialDifficulty,
|
||||
bool hasResumableGame = false,
|
||||
bool initialGameIsRetail = false,
|
||||
WolfTransitionEffect introEffect = WolfTransitionEffect.normalFade,
|
||||
}) {
|
||||
_gameCount = gameCount;
|
||||
_showResumeOption = hasResumableGame;
|
||||
@@ -286,6 +350,7 @@ class MenuManager {
|
||||
_introLandingMenu = WolfMenuScreen.mainMenu;
|
||||
if (gameCount > 1) {
|
||||
_activeMenu = WolfMenuScreen.gameSelect;
|
||||
_introEffect = introEffect;
|
||||
_introElapsedMs = 0;
|
||||
_introPhase = _WolfIntroPhase.fadeIn;
|
||||
_introSlideIndex = 0;
|
||||
@@ -294,9 +359,13 @@ class MenuManager {
|
||||
WolfIntroSlide.title,
|
||||
];
|
||||
} else {
|
||||
_startIntroSequence(includeRetailWarning: initialGameIsRetail);
|
||||
_startIntroSequence(
|
||||
includeRetailWarning: initialGameIsRetail,
|
||||
effect: introEffect,
|
||||
);
|
||||
}
|
||||
_transitionTarget = null;
|
||||
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
_resetEdgeState();
|
||||
@@ -306,12 +375,17 @@ class MenuManager {
|
||||
void beginIntroSplash({
|
||||
WolfMenuScreen landingMenu = WolfMenuScreen.mainMenu,
|
||||
bool includeRetailWarning = false,
|
||||
WolfTransitionEffect effect = WolfTransitionEffect.normalFade,
|
||||
}) {
|
||||
_introLandingMenu = landingMenu;
|
||||
_transitionTarget = null;
|
||||
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
_startIntroSequence(includeRetailWarning: includeRetailWarning);
|
||||
_startIntroSequence(
|
||||
includeRetailWarning: includeRetailWarning,
|
||||
effect: effect,
|
||||
);
|
||||
_resetEdgeState();
|
||||
}
|
||||
|
||||
@@ -336,8 +410,10 @@ class MenuManager {
|
||||
}
|
||||
_activeMenu = WolfMenuScreen.mainMenu;
|
||||
_transitionTarget = null;
|
||||
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
_introEffect = WolfTransitionEffect.normalFade;
|
||||
_introElapsedMs = 0;
|
||||
_resetEdgeState();
|
||||
}
|
||||
@@ -449,6 +525,7 @@ class MenuManager {
|
||||
_isSelectableChangeViewIndex,
|
||||
);
|
||||
_transitionTarget = null;
|
||||
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
_resetEdgeState();
|
||||
@@ -464,6 +541,7 @@ class MenuManager {
|
||||
_isSelectableRendererOptionIndex,
|
||||
);
|
||||
_transitionTarget = null;
|
||||
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
_resetEdgeState();
|
||||
@@ -473,11 +551,15 @@ class MenuManager {
|
||||
///
|
||||
/// Hosts can reuse this fade timing for future pre-menu splash/image
|
||||
/// sequences so transitions feel consistent across the whole app.
|
||||
void startTransition(WolfMenuScreen target) {
|
||||
void startTransition(
|
||||
WolfMenuScreen target, {
|
||||
WolfTransitionEffect effect = WolfTransitionEffect.normalFade,
|
||||
}) {
|
||||
if (_activeMenu == target) {
|
||||
return;
|
||||
}
|
||||
_transitionTarget = target;
|
||||
_transitionEffect = effect;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
_resetEdgeState();
|
||||
@@ -501,6 +583,7 @@ class MenuManager {
|
||||
}
|
||||
if (_transitionElapsedMs >= transitionDurationMs) {
|
||||
_transitionTarget = null;
|
||||
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
}
|
||||
@@ -527,8 +610,12 @@ class MenuManager {
|
||||
_consumeEdgeState(input);
|
||||
}
|
||||
|
||||
void _startIntroSequence({required bool includeRetailWarning}) {
|
||||
void _startIntroSequence({
|
||||
required bool includeRetailWarning,
|
||||
required WolfTransitionEffect effect,
|
||||
}) {
|
||||
_activeMenu = WolfMenuScreen.introSplash;
|
||||
_introEffect = effect;
|
||||
_introSlides = includeRetailWarning
|
||||
? <WolfIntroSlide>[
|
||||
WolfIntroSlide.retailWarning,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:math' as math;
|
||||
|
||||
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/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_menu.dart';
|
||||
@@ -130,6 +131,8 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
|
||||
late List<List<ColoredChar>> _screen;
|
||||
late List<List<int>> _scenePixels;
|
||||
List<List<ColoredChar>> _screenScratch = const <List<ColoredChar>>[];
|
||||
List<List<int>> _scenePixelsScratch = const <List<int>>[];
|
||||
List<int>? _mainMenuBandFirstColumn;
|
||||
String? _lastLoggedThemeName;
|
||||
|
||||
@@ -488,7 +491,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
}
|
||||
|
||||
_drawCenteredMenuFooter();
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -538,7 +541,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
}
|
||||
|
||||
_drawCenteredMenuFooter();
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -597,7 +600,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
);
|
||||
}
|
||||
_drawCenteredMenuFooter();
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -638,7 +641,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
}
|
||||
|
||||
_drawCenteredMenuFooter();
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -742,7 +745,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
}
|
||||
|
||||
_drawCenteredMenuFooter();
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -795,7 +798,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
}
|
||||
|
||||
_drawCenteredMenuFooter();
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -851,7 +854,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
);
|
||||
}
|
||||
_drawCenteredMenuFooter();
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -878,7 +881,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
}
|
||||
|
||||
_drawCenteredMenuFooter();
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
}
|
||||
|
||||
String _gameTitle(GameVersion version) {
|
||||
@@ -932,10 +935,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
_blitVgaImageAscii(image, x, y);
|
||||
}
|
||||
|
||||
_applyMenuFade(
|
||||
engine.menuManager.introOverlayAlpha,
|
||||
_rgbToPaletteColor(0x000000),
|
||||
);
|
||||
_applyIntroTransition(engine.menuManager, _rgbToPaletteColor(0x000000));
|
||||
}
|
||||
|
||||
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) {
|
||||
if (alpha <= 0.0) {
|
||||
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/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_menu.dart';
|
||||
@@ -36,6 +37,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
static const int _headerHeadingY = 24;
|
||||
|
||||
late Uint8List _screen;
|
||||
Uint8List _transitionScratch = Uint8List(0);
|
||||
List<int>? _mainMenuBandFirstColumn;
|
||||
int _offsetColumns = 0;
|
||||
int _offsetRows = 0;
|
||||
@@ -435,7 +437,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
scale: 1,
|
||||
);
|
||||
}
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -470,7 +472,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
scale: 1,
|
||||
);
|
||||
}
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -522,7 +524,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
);
|
||||
}
|
||||
}
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -615,7 +617,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
scale: 1,
|
||||
);
|
||||
}
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -662,7 +664,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
scale: 1,
|
||||
);
|
||||
}
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -676,7 +678,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
_fillRect320(28, 70, 264, 82, panelColor);
|
||||
if (_useCompactMenuLayout) {
|
||||
_drawCompactMenu(selectedDifficultyIndex, headingIndex, panelColor);
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -717,7 +719,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
);
|
||||
}
|
||||
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
}
|
||||
|
||||
void _drawCustomizeMenuHeader(
|
||||
@@ -816,10 +818,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
_blitVgaImage(image, x, y);
|
||||
}
|
||||
|
||||
_applyMenuFade(
|
||||
engine.menuManager.introOverlayAlpha,
|
||||
_rgbToPaletteIndex(0x000000),
|
||||
);
|
||||
_applyIntroTransition(engine.menuManager, _rgbToPaletteIndex(0x000000));
|
||||
}
|
||||
|
||||
void _drawRetailWarningIntro(int backgroundColor) {
|
||||
@@ -861,6 +860,117 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
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) {
|
||||
if (alpha <= 0.0) {
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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/rendering/fizzle_fade.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/wolf_3d_data_types.dart';
|
||||
@@ -18,6 +20,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
static const int _headerHeadingY = 24;
|
||||
|
||||
late FrameBuffer _buffer;
|
||||
Uint32List _transitionScratch = Uint32List(0);
|
||||
List<int>? _mainMenuBandFirstColumn;
|
||||
|
||||
@override
|
||||
@@ -233,7 +236,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
break;
|
||||
}
|
||||
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
}
|
||||
|
||||
void _drawIntroSplash(WolfEngine engine, WolfClassicMenuArt art) {
|
||||
@@ -275,10 +278,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
}
|
||||
}
|
||||
|
||||
_applyMenuFade(
|
||||
engine.menuManager.introOverlayAlpha,
|
||||
_rgbToFrameColor(0x000000),
|
||||
);
|
||||
_applyIntroTransition(engine.menuManager, _rgbToFrameColor(0x000000));
|
||||
}
|
||||
|
||||
void _drawRetailWarningIntro(int backgroundColor) {
|
||||
@@ -550,7 +550,8 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
? 0
|
||||
: ((optionEntries.length - 1) * optionsRowStep) + 10;
|
||||
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++) {
|
||||
final int optionIndex = modeCount + i;
|
||||
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) {
|
||||
if (alpha <= 0.0) {
|
||||
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
|
||||
/// order (`0xAABBGGRR`) used throughout this renderer.
|
||||
int _rgbToFrameColor(int rgb) {
|
||||
|
||||
@@ -223,6 +223,46 @@ void main() {
|
||||
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', () {
|
||||
final input = _TestInput();
|
||||
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