Refactor menu rendering and improve projection sampling
- Updated AsciiRasterizer to support game and episode selection menus with improved layout and cursor handling. - Enhanced SixelRasterizer and SoftwareRasterizer to modularize menu drawing logic for game and episode selection. - Introduced new methods for drawing menus and applying fade effects across rasterizers. - Adjusted wall texture sampling in Rasterizer to anchor to projection height center for consistent rendering. - Added tests for wall texture sampling behavior to ensure legacy compatibility and new functionality. - Modified Flutter audio adapter to use nullable access for active game and adjusted game selection logic in the main class. - Cleaned up input handling in Wolf3dFlutterInput by removing unused menu tap variables. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -13,28 +13,50 @@ import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||
/// input systems, and the world state.
|
||||
class WolfEngine {
|
||||
WolfEngine({
|
||||
required this.data,
|
||||
required this.startingEpisode,
|
||||
WolfensteinData? data,
|
||||
List<WolfensteinData>? availableGames,
|
||||
this.startingEpisode,
|
||||
required this.onGameWon,
|
||||
required this.input,
|
||||
required this.frameBuffer,
|
||||
this.difficulty,
|
||||
this.menuBackgroundRgb = 0x890000,
|
||||
this.menuPanelRgb = 0x590002,
|
||||
EngineAudio? audio,
|
||||
}) : audio = audio ?? CliSilentAudio(),
|
||||
this.onMenuExit,
|
||||
this.onGameSelected,
|
||||
this.onEpisodeSelected,
|
||||
EngineAudio? engineAudio,
|
||||
}) : assert(
|
||||
data != null || (availableGames != null && availableGames.isNotEmpty),
|
||||
'Provide either data or a non-empty availableGames list.',
|
||||
),
|
||||
_availableGames = availableGames ?? <WolfensteinData>[data!],
|
||||
audio = engineAudio ?? CliSilentAudio(),
|
||||
doorManager = DoorManager(
|
||||
onPlaySound: (sfxId) => audio?.playSoundEffect(sfxId),
|
||||
onPlaySound: (sfxId) => engineAudio?.playSoundEffect(sfxId),
|
||||
),
|
||||
pushwallManager = PushwallManager(
|
||||
onPlaySound: (sfxId) => audio?.playSoundEffect(sfxId),
|
||||
);
|
||||
onPlaySound: (sfxId) => engineAudio?.playSoundEffect(sfxId),
|
||||
) {
|
||||
if (_availableGames.isEmpty) {
|
||||
throw StateError('WolfEngine requires at least one game data set.');
|
||||
}
|
||||
}
|
||||
|
||||
/// Total milliseconds elapsed since the engine was initialized.
|
||||
int _timeAliveMs = 0;
|
||||
|
||||
/// The static game data (textures, sounds, maps) parsed from original files.
|
||||
final WolfensteinData data;
|
||||
/// The discovered game data sets available for selection.
|
||||
final List<WolfensteinData> _availableGames;
|
||||
|
||||
/// Available game data sets for menu rendering and selection.
|
||||
List<WolfensteinData> get availableGames =>
|
||||
List.unmodifiable(_availableGames);
|
||||
|
||||
int _currentGameIndex = 0;
|
||||
|
||||
/// The currently active game data set.
|
||||
WolfensteinData get data => _availableGames[_currentGameIndex];
|
||||
|
||||
/// Desired menu background color in 24-bit RGB.
|
||||
final int menuBackgroundRgb;
|
||||
@@ -52,7 +74,7 @@ class WolfEngine {
|
||||
int get timeAliveMs => _timeAliveMs;
|
||||
|
||||
/// The episode index where the game session begins.
|
||||
final int startingEpisode;
|
||||
final int? startingEpisode;
|
||||
|
||||
/// Handles music and sound effect playback.
|
||||
late final EngineAudio audio;
|
||||
@@ -60,6 +82,15 @@ class WolfEngine {
|
||||
/// Callback triggered when the final level of an episode is completed.
|
||||
final void Function() onGameWon;
|
||||
|
||||
/// Callback triggered when backing out of the top-level menu.
|
||||
final void Function()? onMenuExit;
|
||||
|
||||
/// Callback triggered whenever the active game changes from menu flow.
|
||||
final void Function(WolfensteinData game)? onGameSelected;
|
||||
|
||||
/// Callback triggered when episode selection changes; `null` means cleared.
|
||||
final void Function(int? episodeIndex)? onEpisodeSelected;
|
||||
|
||||
// --- State Managers ---
|
||||
|
||||
/// Manages the state and animation of doors throughout the level.
|
||||
@@ -107,11 +138,24 @@ class WolfEngine {
|
||||
|
||||
/// Initializes the engine, sets the starting episode, and loads the first level.
|
||||
void init() {
|
||||
_currentGameIndex = 0;
|
||||
audio.activeGame = data;
|
||||
_currentEpisodeIndex = startingEpisode;
|
||||
onGameSelected?.call(data);
|
||||
|
||||
_currentEpisodeIndex = startingEpisode ?? 0;
|
||||
_currentLevelIndex = 0;
|
||||
|
||||
menuManager.beginDifficultySelection(initialDifficulty: difficulty);
|
||||
menuManager.beginSelectionFlow(
|
||||
gameCount: _availableGames.length,
|
||||
initialGameIndex: _currentGameIndex,
|
||||
initialEpisodeIndex: _currentEpisodeIndex,
|
||||
initialDifficulty: difficulty,
|
||||
);
|
||||
|
||||
if (_availableGames.length == 1) {
|
||||
menuManager.setSelectedGameIndex(0, 1);
|
||||
onEpisodeSelected?.call(null);
|
||||
}
|
||||
|
||||
if (difficulty != null) {
|
||||
_loadLevel();
|
||||
@@ -146,7 +190,8 @@ class WolfEngine {
|
||||
final currentInput = input.currentInput;
|
||||
|
||||
if (difficulty == null) {
|
||||
_tickDifficultyMenu(currentInput);
|
||||
menuManager.tickTransition(delta.inMilliseconds);
|
||||
_tickMenu(currentInput);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -181,21 +226,94 @@ class WolfEngine {
|
||||
);
|
||||
}
|
||||
|
||||
void _tickDifficultyMenu(EngineInput input) {
|
||||
void _tickMenu(EngineInput input) {
|
||||
if (menuManager.isTransitioning) {
|
||||
menuManager.absorbInputState(input);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (menuManager.activeMenu) {
|
||||
case WolfMenuScreen.gameSelect:
|
||||
_tickGameSelectionMenu(input);
|
||||
break;
|
||||
case WolfMenuScreen.episodeSelect:
|
||||
_tickEpisodeSelectionMenu(input);
|
||||
break;
|
||||
case WolfMenuScreen.difficultySelect:
|
||||
_tickDifficultySelectionMenu(input);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _tickGameSelectionMenu(EngineInput input) {
|
||||
final menuResult = menuManager.updateGameSelection(
|
||||
input,
|
||||
gameCount: _availableGames.length,
|
||||
);
|
||||
|
||||
if (menuResult.goBack) {
|
||||
_exitTopLevelMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
if (menuResult.selectedIndex != null) {
|
||||
_currentGameIndex = menuResult.selectedIndex!;
|
||||
audio.activeGame = data;
|
||||
onGameSelected?.call(data);
|
||||
_currentEpisodeIndex = 0;
|
||||
onEpisodeSelected?.call(null);
|
||||
menuManager.clearEpisodeSelection();
|
||||
menuManager.startTransition(WolfMenuScreen.episodeSelect);
|
||||
}
|
||||
}
|
||||
|
||||
void _tickEpisodeSelectionMenu(EngineInput input) {
|
||||
final menuResult = menuManager.updateEpisodeSelection(
|
||||
input,
|
||||
episodeCount: data.episodes.length,
|
||||
);
|
||||
|
||||
if (menuResult.goBack) {
|
||||
onEpisodeSelected?.call(null);
|
||||
menuManager.clearEpisodeSelection();
|
||||
if (_availableGames.length > 1) {
|
||||
menuManager.startTransition(WolfMenuScreen.gameSelect);
|
||||
} else {
|
||||
_exitTopLevelMenu();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (menuResult.selectedIndex != null) {
|
||||
_currentEpisodeIndex = menuResult.selectedIndex!;
|
||||
onEpisodeSelected?.call(_currentEpisodeIndex);
|
||||
menuManager.startTransition(WolfMenuScreen.difficultySelect);
|
||||
}
|
||||
}
|
||||
|
||||
void _tickDifficultySelectionMenu(EngineInput input) {
|
||||
final menuResult = menuManager.updateDifficultySelection(input);
|
||||
if (menuResult.goBack) {
|
||||
// Explicitly keep the engine in menu mode when leaving this screen.
|
||||
difficulty = null;
|
||||
onGameWon();
|
||||
menuManager.startTransition(WolfMenuScreen.episodeSelect);
|
||||
return;
|
||||
}
|
||||
|
||||
if (menuResult.selected != null) {
|
||||
difficulty = menuResult.selected;
|
||||
_currentLevelIndex = 0;
|
||||
_returnLevelIndex = null;
|
||||
_loadLevel();
|
||||
}
|
||||
}
|
||||
|
||||
void _exitTopLevelMenu() {
|
||||
if (onMenuExit != null) {
|
||||
onMenuExit!.call();
|
||||
return;
|
||||
}
|
||||
onGameWon();
|
||||
}
|
||||
|
||||
/// Wipes the current world state and builds a new floor from map data.
|
||||
void _loadLevel() {
|
||||
entities.clear();
|
||||
|
||||
Reference in New Issue
Block a user