From 6eb903cbaa37acc5402c158bf5c7a2674ef9b807 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Sun, 15 Mar 2026 14:33:58 +0100 Subject: [PATCH] Delegate all audio management to the new audio package, then manage that through a new wolf3d class Signed-off-by: Hans Kokx --- lib/features/renderer/renderer.dart | 50 +++++++--- .../difficulty_screen.dart | 62 +----------- lib/features/screens/episode_screen.dart | 97 +++++++++++++++++++ lib/game_select_screen.dart | 4 +- lib/main.dart | 7 -- lib/wolf_3d.dart | 14 +++ .../wolf_3d_synth/lib/src/wolf_3d_audio.dart | 92 ++++++++++++++++++ packages/wolf_3d_synth/lib/wolf_3d_synth.dart | 2 +- packages/wolf_3d_synth/pubspec.yaml | 1 + pubspec.yaml | 1 - 10 files changed, 249 insertions(+), 81 deletions(-) rename lib/features/{difficulty => screens}/difficulty_screen.dart (62%) create mode 100644 lib/features/screens/episode_screen.dart create mode 100644 packages/wolf_3d_synth/lib/src/wolf_3d_audio.dart diff --git a/lib/features/renderer/renderer.dart b/lib/features/renderer/renderer.dart index 7d7eade..11e020c 100644 --- a/lib/features/renderer/renderer.dart +++ b/lib/features/renderer/renderer.dart @@ -17,21 +17,19 @@ import 'package:wolf_dart/features/player/player.dart'; import 'package:wolf_dart/features/renderer/raycast_painter.dart'; import 'package:wolf_dart/features/renderer/weapon_painter.dart'; import 'package:wolf_dart/features/ui/hud.dart'; -import 'package:wolf_dart/sprite_gallery.dart'; +import 'package:wolf_dart/wolf_3d.dart'; class WolfRenderer extends StatefulWidget { const WolfRenderer( this.data, { + required this.difficulty, + required this.startingEpisode, super.key, - this.difficulty = Difficulty.bringEmOn, - this.showSpriteGallery = false, - this.isShareware = true, }); final WolfensteinData data; final Difficulty difficulty; - final bool showSpriteGallery; - final bool isShareware; + final int startingEpisode; @override State createState() => _WolfRendererState(); @@ -56,6 +54,9 @@ class _WolfRendererState extends State double damageFlashOpacity = 0.0; + late int _currentMapIndex; + late WolfLevel _currentLevel; + List entities = []; @override @@ -64,7 +65,38 @@ class _WolfRendererState extends State _initGame(); } + void _loadLevel(int mapIndex) { + // Grab the specific level from the singleton + _currentLevel = Wolf3d.I.levels[mapIndex]; + + // TODO: Initialize player position, spawn enemies based on difficulty, etc. + debugPrint("Loaded Level: ${_currentLevel.name}"); + } + + void _onLevelCompleted() { + // When the player hits the elevator switch, advance the map + setState(() { + _currentMapIndex++; + + // Check if they beat the episode (each episode is 10 levels) + int maxLevelForEpisode = (widget.startingEpisode * 10) + 9; + + if (_currentMapIndex > maxLevelForEpisode) { + // TODO: Handle episode completion (show victory screen, return to menu) + debugPrint("Episode Completed!"); + } else { + _loadLevel(_currentMapIndex); + } + }); + } + Future _initGame() async { + // 1. Calculate the starting index + _currentMapIndex = widget.startingEpisode * 10; + + // 2. Load the initial level data + _loadLevel(_currentMapIndex); + // Get the first level out of the data class activeLevel = widget.data.levels.first; @@ -109,7 +141,7 @@ class _WolfRendererState extends State y + 0.5, widget.difficulty, widget.data.sprites.length, - isSharewareMode: widget.isShareware, + isSharewareMode: widget.data.version == GameVersion.shareware, ); if (newEntity != null) { @@ -402,10 +434,6 @@ class _WolfRendererState extends State return const Center(child: CircularProgressIndicator(color: Colors.teal)); } - if (widget.showSpriteGallery) { - return SpriteGallery(sprites: widget.data.sprites); - } - return Scaffold( backgroundColor: Colors.black, body: KeyboardListener( diff --git a/lib/features/difficulty/difficulty_screen.dart b/lib/features/screens/difficulty_screen.dart similarity index 62% rename from lib/features/difficulty/difficulty_screen.dart rename to lib/features/screens/difficulty_screen.dart index ecd4956..16c3bed 100644 --- a/lib/features/difficulty/difficulty_screen.dart +++ b/lib/features/screens/difficulty_screen.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_soloud/flutter_soloud.dart'; import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; -import 'package:wolf_3d_synth/wolf_3d_synth.dart'; import 'package:wolf_dart/features/difficulty/difficulty.dart'; import 'package:wolf_dart/features/renderer/renderer.dart'; import 'package:wolf_dart/wolf_3d.dart'; @@ -16,77 +14,23 @@ class DifficultyScreen extends StatefulWidget { } class _DifficultyScreenState extends State { - AudioSource? _menuMusicSource; - SoundHandle? _menuMusicHandle; - bool get isShareware => Wolf3d.I.activeGame.version == GameVersion.shareware; - @override - void initState() { - super.initState(); - _playMenuMusic(); - } - - Future _playMenuMusic() async { - final soloud = SoLoud.instance; - - if (!soloud.isInitialized) { - return; - } - - // 2. We only want to play music if the IMF data actually exists - if (Wolf3d.I.music.isNotEmpty) { - // Get the first track (usually the menu theme "Wondering About My Loved Ones") - final music = Wolf3d.I.music.first; - - // Render the hardware instructions into PCM and wrap in a WAV header - final pcmSamples = ImfRenderer.render(music); - - final wavBytes = ImfRenderer.createWavFile(pcmSamples); - - // 3. Load the bytes into SoLoud's memory - // The 'menu_theme.wav' string is just a dummy name to tell SoLoud it's dealing with a WAV format - _menuMusicSource = await soloud.loadMem('menu_theme.wav', wavBytes); - - // 4. Play the source and tell it to loop continuously! - _menuMusicHandle = await soloud.play( - _menuMusicSource!, - looping: true, - ); - } - } - @override void dispose() { - _cleanupAudio(); + Wolf3d.I.audio.stopMusic(); super.dispose(); } - void _cleanupAudio() { - final soloud = SoLoud.instance; - - // Stop the playback - if (_menuMusicHandle != null) { - soloud.stop(_menuMusicHandle!); - } - - // Free the raw WAV data from C++ memory - if (_menuMusicSource != null) { - soloud.disposeSource(_menuMusicSource!); - } - } - void _startGame(Difficulty difficulty, {bool showGallery = false}) { - // Stop the music and clear memory right before we push the new route - _cleanupAudio(); + Wolf3d.I.audio.stopMusic(); Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (_) => WolfRenderer( Wolf3d.I.activeGame, difficulty: difficulty, - isShareware: isShareware, - showSpriteGallery: showGallery, + startingEpisode: Wolf3d.I.activeEpisode, ), ), ); diff --git a/lib/features/screens/episode_screen.dart b/lib/features/screens/episode_screen.dart new file mode 100644 index 0000000..93f7489 --- /dev/null +++ b/lib/features/screens/episode_screen.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:wolf_dart/features/screens/difficulty_screen.dart'; +import 'package:wolf_dart/wolf_3d.dart'; + +class EpisodeScreen extends StatefulWidget { + const EpisodeScreen({super.key}); + + @override + State createState() => _EpisodeScreenState(); +} + +class _EpisodeScreenState extends State { + final List _episodeNames = [ + "Episode 1\nEscape from Wolfenstein", + "Episode 2\nOperation: Eisenfaust", + "Episode 3\nDie, Fuhrer, Die!", + "Episode 4\nA Dark Secret", + "Episode 5\nTrail of the Madman", + "Episode 6\nConfrontation", + ]; + + @override + void initState() { + super.initState(); + if (Wolf3d.I.music.isNotEmpty) { + Wolf3d.I.audio.playMusic(Wolf3d.I.music.first); + } + } + + void _selectEpisode(int index) { + Wolf3d.I.setActiveEpisode(index); + Navigator.of(context).push( + MaterialPageRoute( + // We pass the audio handles so the next screen can stop them when the game starts + builder: (context) => DifficultyScreen(), + ), + ); + } + + @override + Widget build(BuildContext context) { + // Determine how many episodes are available (10 levels per episode) + final int numberOfEpisodes = (Wolf3d.I.levels.length / 10).floor().clamp( + 1, + 6, + ); + + return Scaffold( + backgroundColor: Colors.black, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'WHICH EPISODE TO PLAY?', + style: TextStyle( + color: Colors.red, + fontSize: 32, + fontWeight: FontWeight.bold, + fontFamily: 'Courier', + ), + ), + const SizedBox(height: 40), + ListView.builder( + shrinkWrap: true, + itemCount: numberOfEpisodes, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 32.0, + ), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueGrey[900], + foregroundColor: Colors.white, + minimumSize: const Size(300, 60), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + onPressed: () => _selectEpisode(index), + child: Text( + _episodeNames[index], + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 18), + ), + ), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/game_select_screen.dart b/lib/game_select_screen.dart index a325926..661885c 100644 --- a/lib/game_select_screen.dart +++ b/lib/game_select_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; -import 'package:wolf_dart/features/difficulty/difficulty_screen.dart'; +import 'package:wolf_dart/features/screens/episode_screen.dart'; import 'package:wolf_dart/wolf_3d.dart'; class GameSelectScreen extends StatelessWidget { @@ -22,7 +22,7 @@ class GameSelectScreen extends StatelessWidget { Wolf3d.I.setActiveGame(data); Navigator.of(context).push( MaterialPageRoute( - builder: (context) => DifficultyScreen(), + builder: (context) => const EpisodeScreen(), ), ); }, diff --git a/lib/main.dart b/lib/main.dart index 39f529b..6c857da 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,17 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:flutter_soloud/flutter_soloud.dart'; import 'package:wolf_dart/game_select_screen.dart'; import 'package:wolf_dart/wolf_3d.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - await SoLoud.instance.init( - sampleRate: 44100, // Audio quality - bufferSize: 2048, // Buffer size affects latency - channels: Channels.stereo, - ); - await Wolf3d.I.init(); runApp( diff --git a/lib/wolf_3d.dart b/lib/wolf_3d.dart index 130962f..5b94e47 100644 --- a/lib/wolf_3d.dart +++ b/lib/wolf_3d.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:wolf_3d_data/wolf_3d_data.dart'; import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; +import 'package:wolf_3d_synth/wolf_3d_synth.dart'; class Wolf3d { Wolf3d._(); @@ -12,6 +13,9 @@ class Wolf3d { final List availableGames = []; WolfensteinData? _activeGame; + // --- Core Systems --- + final WolfAudio audio = WolfAudio(); + // --- Getters --- WolfensteinData get activeGame { if (_activeGame == null) { @@ -20,6 +24,15 @@ class Wolf3d { return _activeGame!; } + // --- Episode --- + int _activeEpisode = 0; + + int get activeEpisode => _activeEpisode; + + void setActiveEpisode(int episodeIndex) { + _activeEpisode = episodeIndex; + } + // Convenience getters for the active game's assets List get levels => activeGame.levels; List get walls => activeGame.walls; @@ -36,6 +49,7 @@ class Wolf3d { /// Initializes the engine by loading available game data. Future init({String? directory}) async { + await audio.init(); availableGames.clear(); // 1. Bundle asset loading (migrated from GameSelectScreen) diff --git a/packages/wolf_3d_synth/lib/src/wolf_3d_audio.dart b/packages/wolf_3d_synth/lib/src/wolf_3d_audio.dart new file mode 100644 index 0000000..ee584f5 --- /dev/null +++ b/packages/wolf_3d_synth/lib/src/wolf_3d_audio.dart @@ -0,0 +1,92 @@ +import 'package:flutter_soloud/flutter_soloud.dart'; +import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; +import 'package:wolf_3d_synth/src/imf_renderer.dart'; + +class WolfAudio { + bool _isInitialized = false; + + // --- Music State --- + AudioSource? _currentMusicSource; + SoundHandle? _currentMusicHandle; + + /// Initializes the SoLoud audio engine. + Future init() async { + if (_isInitialized) return; + + try { + await SoLoud.instance.init( + sampleRate: 44100, + bufferSize: 2048, + channels: Channels.stereo, + ); + _isInitialized = true; + print("WolfAudio: SoLoud initialized successfully."); + } catch (e) { + print("WolfAudio: Failed to initialize SoLoud - $e"); + } + } + + /// Disposes of the audio engine and frees resources. + void dispose() { + stopMusic(); + SoLoud.instance.deinit(); + _isInitialized = false; + } + + // ========================================== + // MUSIC MANAGEMENT + // ========================================== + + /// Renders and plays a specific IMF music track. + Future playMusic(ImfMusic track, {bool looping = true}) async { + if (!_isInitialized) return; + + // Stop currently playing music to prevent overlap + stopMusic(); + + try { + // Render hardware instructions into PCM and wrap in WAV + final pcmSamples = ImfRenderer.render(track); + final wavBytes = ImfRenderer.createWavFile(pcmSamples); + + _currentMusicSource = await SoLoud.instance.loadMem( + 'track.wav', + wavBytes, + ); + _currentMusicHandle = await SoLoud.instance.play( + _currentMusicSource!, + looping: looping, + ); + } catch (e) { + print("WolfAudio: Error playing music track - $e"); + } + } + + /// Halts playback and frees memory for the current track. + void stopMusic() { + if (!_isInitialized) return; + + if (_currentMusicHandle != null) { + SoLoud.instance.stop(_currentMusicHandle!); + _currentMusicHandle = null; + } + if (_currentMusicSource != null) { + SoLoud.instance.disposeSource(_currentMusicSource!); + _currentMusicSource = null; + } + } + + /// Pauses the current track. + void pauseMusic() { + if (_isInitialized && _currentMusicHandle != null) { + SoLoud.instance.setPause(_currentMusicHandle!, true); + } + } + + /// Resumes a paused track. + void resumeMusic() { + if (_isInitialized && _currentMusicHandle != null) { + SoLoud.instance.setPause(_currentMusicHandle!, false); + } + } +} diff --git a/packages/wolf_3d_synth/lib/wolf_3d_synth.dart b/packages/wolf_3d_synth/lib/wolf_3d_synth.dart index 5c15b0c..10d38a6 100644 --- a/packages/wolf_3d_synth/lib/wolf_3d_synth.dart +++ b/packages/wolf_3d_synth/lib/wolf_3d_synth.dart @@ -3,4 +3,4 @@ /// More dartdocs go here. library; -export 'src/imf_renderer.dart' show ImfRenderer; +export 'src/wolf_3d_audio.dart' show WolfAudio; diff --git a/packages/wolf_3d_synth/pubspec.yaml b/packages/wolf_3d_synth/pubspec.yaml index 02ef364..e2e7a4d 100644 --- a/packages/wolf_3d_synth/pubspec.yaml +++ b/packages/wolf_3d_synth/pubspec.yaml @@ -9,6 +9,7 @@ environment: resolution: workspace dependencies: + flutter_soloud: ^3.5.1 wolf_3d_data_types: dev_dependencies: diff --git a/pubspec.yaml b/pubspec.yaml index aa48c24..d37576c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,6 @@ dependencies: wolf_3d_synth: any flutter: sdk: flutter - flutter_soloud: ^3.5.1 dev_dependencies: flutter_test: