- 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>
240 lines
7.6 KiB
Dart
240 lines
7.6 KiB
Dart
/// High-level Flutter facade for discovering game data and sharing runtime services.
|
|
library;
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:wolf_3d_dart/wolf_3d_data.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_synth.dart';
|
|
import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart';
|
|
|
|
/// Coordinates asset discovery, audio initialization, and input reuse for apps.
|
|
class Wolf3d {
|
|
/// Creates an empty facade that must be initialized with [init].
|
|
Wolf3d();
|
|
|
|
/// All successfully discovered or bundled game data sets.
|
|
final List<WolfensteinData> availableGames = [];
|
|
WolfensteinData? _activeGame;
|
|
|
|
/// Shared engine audio backend used by menus and gameplay sessions.
|
|
final EngineAudio audio = WolfAudio();
|
|
|
|
/// Engine menu background color as 24-bit RGB.
|
|
int menuBackgroundRgb = 0x890000;
|
|
|
|
/// Engine menu panel color as 24-bit RGB.
|
|
int menuPanelRgb = 0x590002;
|
|
|
|
/// Shared Flutter input adapter reused by gameplay screens.
|
|
final Wolf3dFlutterInput input = Wolf3dFlutterInput();
|
|
|
|
/// The currently selected game data set.
|
|
///
|
|
/// Throws a [StateError] until [setActiveGame] has been called.
|
|
WolfensteinData get activeGame {
|
|
if (_activeGame == null) {
|
|
throw StateError("No active game selected. Call setActiveGame() first.");
|
|
}
|
|
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;
|
|
|
|
/// Index of the episode currently selected in the UI flow.
|
|
int? get activeEpisode => _activeEpisode;
|
|
|
|
Difficulty? _activeDifficulty;
|
|
|
|
/// The difficulty applied when [launchEngine] creates a new session.
|
|
Difficulty? get activeDifficulty => _activeDifficulty;
|
|
|
|
/// Stores [difficulty] so the next [launchEngine] call uses it.
|
|
void setActiveDifficulty(Difficulty difficulty) {
|
|
_activeDifficulty = difficulty;
|
|
}
|
|
|
|
/// Clears any previously selected difficulty so the engine can prompt for one.
|
|
void clearActiveDifficulty() {
|
|
_activeDifficulty = null;
|
|
}
|
|
|
|
WolfEngine? _engine;
|
|
|
|
/// The most recently launched engine.
|
|
///
|
|
/// Throws a [StateError] until [launchEngine] has been called.
|
|
WolfEngine get engine {
|
|
if (_engine == null) {
|
|
throw StateError('No engine launched. Call launchEngine() first.');
|
|
}
|
|
return _engine!;
|
|
}
|
|
|
|
/// Creates and initializes a [WolfEngine] for the current session config.
|
|
///
|
|
/// Uses [activeGame], [activeEpisode], and [activeDifficulty]. Stores the
|
|
/// 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(
|
|
availableGames: availableGames,
|
|
difficulty: _activeDifficulty,
|
|
startingEpisode: _activeEpisode,
|
|
frameBuffer: FrameBuffer(320, 200),
|
|
menuBackgroundRgb: menuBackgroundRgb,
|
|
menuPanelRgb: menuPanelRgb,
|
|
engineAudio: audio,
|
|
input: input,
|
|
onGameWon: onGameWon,
|
|
onMenuExit: onGameWon,
|
|
onGameSelected: (game) {
|
|
_activeGame = game;
|
|
audio.activeGame = game;
|
|
},
|
|
onEpisodeSelected: (episodeIndex) {
|
|
_activeEpisode = episodeIndex;
|
|
},
|
|
);
|
|
_engine!.init();
|
|
return _engine!;
|
|
}
|
|
|
|
/// Sets the active episode for the current [activeGame].
|
|
void setActiveEpisode(int episodeIndex) {
|
|
if (_activeGame == null) {
|
|
throw StateError("No active game selected. Call setActiveGame() first.");
|
|
}
|
|
if (episodeIndex < 0 || episodeIndex >= _activeGame!.episodes.length) {
|
|
throw RangeError("Episode index out of range for the active game.");
|
|
}
|
|
|
|
_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 {
|
|
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;
|
|
|
|
/// Convenience access to the active game's sprite set.
|
|
List<Sprite> get sprites => activeGame.sprites;
|
|
|
|
/// Convenience access to digitized PCM effects.
|
|
List<PcmSound> get sounds => activeGame.sounds;
|
|
|
|
/// Convenience access to AdLib/OPL effect assets.
|
|
List<PcmSound> get adLibSounds => activeGame.adLibSounds;
|
|
|
|
/// Convenience access to level music tracks.
|
|
List<ImfMusic> get music => activeGame.music;
|
|
|
|
/// Convenience access to VGA UI and splash images.
|
|
List<VgaImage> get vgaImages => activeGame.vgaImages;
|
|
|
|
/// Makes [game] the active data set and points shared services at it.
|
|
void setActiveGame(WolfensteinData game) {
|
|
if (!availableGames.contains(game)) {
|
|
throw ArgumentError(
|
|
"The provided game data is not in the list of available games.",
|
|
);
|
|
}
|
|
|
|
if (_activeGame == game) {
|
|
return; // No change needed
|
|
}
|
|
|
|
_activeGame = game;
|
|
_activeEpisode = null;
|
|
audio.activeGame = game;
|
|
}
|
|
|
|
/// Initializes the engine by loading available game data.
|
|
Future<Wolf3d> init({String? directory}) async {
|
|
await audio.init();
|
|
availableGames.clear();
|
|
|
|
// Bundled assets let the GUI work out of the box on supported platforms.
|
|
final versionsToTry = [
|
|
(version: GameVersion.retail, path: 'retail'),
|
|
(version: GameVersion.shareware, path: 'shareware'),
|
|
];
|
|
|
|
for (final version in versionsToTry) {
|
|
try {
|
|
final ext = version.version.fileExtension;
|
|
final folder = 'packages/wolf_3d_assets/assets/${version.path}';
|
|
|
|
final data = WolfensteinLoader.loadFromBytes(
|
|
version: version.version,
|
|
vswap: await _tryLoad('$folder/VSWAP.$ext'),
|
|
mapHead: await _tryLoad('$folder/MAPHEAD.$ext'),
|
|
gameMaps: await _tryLoad('$folder/GAMEMAPS.$ext'),
|
|
vgaDict: await _tryLoad('$folder/VGADICT.$ext'),
|
|
vgaHead: await _tryLoad('$folder/VGAHEAD.$ext'),
|
|
vgaGraph: await _tryLoad('$folder/VGAGRAPH.$ext'),
|
|
audioHed: await _tryLoad('$folder/AUDIOHED.$ext'),
|
|
audioT: await _tryLoad('$folder/AUDIOT.$ext'),
|
|
);
|
|
|
|
availableGames.add(data);
|
|
} catch (e) {
|
|
debugPrint(e.toString());
|
|
}
|
|
}
|
|
|
|
// On non-web platforms, also scan the local filesystem for user-supplied
|
|
// data folders so the host can pick up extra versions automatically.
|
|
if (!kIsWeb) {
|
|
try {
|
|
final externalGames = await WolfensteinLoader.discover(
|
|
directoryPath: directory,
|
|
recursive: true,
|
|
);
|
|
for (var entry in externalGames.entries) {
|
|
if (!availableGames.any((g) => g.version == entry.key)) {
|
|
availableGames.add(entry.value);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debugPrint("External discovery failed: $e");
|
|
}
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/// Loads an asset from the Flutter bundle, returning `null` when absent.
|
|
Future<ByteData?> _tryLoad(String path) async {
|
|
try {
|
|
return await rootBundle.load(path);
|
|
} catch (e) {
|
|
debugPrint("Asset not found: $path");
|
|
return null;
|
|
}
|
|
}
|
|
}
|