/// 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 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 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 get walls => activeGame.walls; /// Convenience access to the active game's sprite set. List get sprites => activeGame.sprites; /// Convenience access to digitized PCM effects. List get sounds => activeGame.sounds; /// Convenience access to AdLib/OPL effect assets. List get adLibSounds => activeGame.adLibSounds; /// Convenience access to level music tracks. List get music => activeGame.music; /// Convenience access to VGA UI and splash images. List 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 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 _tryLoad(String path) async { try { return await rootBundle.load(path); } catch (e) { debugPrint("Asset not found: $path"); return null; } } }