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:
2026-03-18 20:06:18 +01:00
parent d93f467163
commit 0e143892f0
15 changed files with 1090 additions and 204 deletions

View File

@@ -43,7 +43,7 @@ class FlutterAudioAdapter implements EngineAudio {
}
@override
WolfensteinData? get activeGame => wolf3d.activeGame;
WolfensteinData? get activeGame => wolf3d.maybeActiveGame;
@override
set activeGame(WolfensteinData? value) {

View File

@@ -40,12 +40,15 @@ class Wolf3d {
return _activeGame!;
}
/// Nullable access to the selected game, useful during menu bootstrap.
WolfensteinData? get maybeActiveGame => _activeGame;
// Episode selection lives on the facade so menus can configure gameplay
// before constructing a new engine instance.
int _activeEpisode = 0;
int? _activeEpisode;
/// Index of the episode currently selected in the UI flow.
int get activeEpisode => _activeEpisode;
int? get activeEpisode => _activeEpisode;
Difficulty? _activeDifficulty;
@@ -80,16 +83,30 @@ class Wolf3d {
/// engine so it can be retrieved via [engine]. [onGameWon] is invoked when
/// the player completes the final level of the episode.
WolfEngine launchEngine({required void Function() onGameWon}) {
if (availableGames.isEmpty) {
throw StateError(
'No game data was discovered. Add game files before launching the engine.',
);
}
_engine = WolfEngine(
data: activeGame,
availableGames: availableGames,
difficulty: _activeDifficulty,
startingEpisode: _activeEpisode,
frameBuffer: FrameBuffer(320, 200),
menuBackgroundRgb: menuBackgroundRgb,
menuPanelRgb: menuPanelRgb,
audio: audio,
engineAudio: audio,
input: input,
onGameWon: onGameWon,
onMenuExit: onGameWon,
onGameSelected: (game) {
_activeGame = game;
audio.activeGame = game;
},
onEpisodeSelected: (episodeIndex) {
_activeEpisode = episodeIndex;
},
);
_engine!.init();
return _engine!;
@@ -107,8 +124,18 @@ class Wolf3d {
_activeEpisode = episodeIndex;
}
/// Clears any selected episode so menu flow starts fresh.
void clearActiveEpisode() {
_activeEpisode = null;
}
/// Convenience access to the active episode's level list.
List<WolfLevel> get levels => activeGame.episodes[activeEpisode].levels;
List<WolfLevel> get levels {
if (_activeEpisode == null) {
throw StateError('No active episode selected.');
}
return activeGame.episodes[_activeEpisode!].levels;
}
/// Convenience access to the active game's wall textures.
List<Sprite> get walls => activeGame.walls;
@@ -141,6 +168,7 @@ class Wolf3d {
}
_activeGame = game;
_activeEpisode = null;
audio.activeGame = game;
}

View File

@@ -58,8 +58,6 @@ class Wolf3dFlutterInput extends Wolf3dInput {
double _mouseDeltaY = 0.0;
bool _previousMouseRightDown = false;
bool _queuedBack = false;
double? _queuedMenuTapX;
double? _queuedMenuTapY;
// Mouse-look is optional so touch or keyboard-only hosts can keep the same
// adapter without incurring accidental pointer-driven movement.
@@ -118,12 +116,6 @@ class Wolf3dFlutterInput extends Wolf3dInput {
_queuedBack = true;
}
/// Queues a one-frame menu tap with normalized coordinates [0..1].
void queueMenuTap({required double x, required double y}) {
_queuedMenuTapX = x.clamp(0.0, 1.0);
_queuedMenuTapY = y.clamp(0.0, 1.0);
}
/// Returns whether any bound key for [action] is currently pressed.
bool _isActive(WolfInputAction action, Set<LogicalKeyboardKey> pressedKeys) {
return bindings[action]!.any((key) => pressedKeys.contains(key));
@@ -167,8 +159,8 @@ class Wolf3dFlutterInput extends Wolf3dInput {
isBack =
_isNewlyPressed(WolfInputAction.back, newlyPressedKeys) || _queuedBack;
menuTapX = _queuedMenuTapX;
menuTapY = _queuedMenuTapY;
menuTapX = null;
menuTapY = null;
// Left click or Ctrl to fire
isFiring =
@@ -194,7 +186,5 @@ class Wolf3dFlutterInput extends Wolf3dInput {
_previousKeys = Set.from(pressedKeys);
_previousMouseRightDown = isMouseRightDown;
_queuedBack = false;
_queuedMenuTapX = null;
_queuedMenuTapY = null;
}
}