Delegate all audio management to the new audio package, then manage that through a new wolf3d class

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-15 14:33:58 +01:00
parent 070110adae
commit 6eb903cbaa
10 changed files with 249 additions and 81 deletions

View File

@@ -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<WolfRenderer> createState() => _WolfRendererState();
@@ -56,6 +54,9 @@ class _WolfRendererState extends State<WolfRenderer>
double damageFlashOpacity = 0.0;
late int _currentMapIndex;
late WolfLevel _currentLevel;
List<Entity> entities = [];
@override
@@ -64,7 +65,38 @@ class _WolfRendererState extends State<WolfRenderer>
_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<void> _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<WolfRenderer>
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<WolfRenderer>
return const Center(child: CircularProgressIndicator(color: Colors.teal));
}
if (widget.showSpriteGallery) {
return SpriteGallery(sprites: widget.data.sprites);
}
return Scaffold(
backgroundColor: Colors.black,
body: KeyboardListener(

View File

@@ -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<DifficultyScreen> {
AudioSource? _menuMusicSource;
SoundHandle? _menuMusicHandle;
bool get isShareware => Wolf3d.I.activeGame.version == GameVersion.shareware;
@override
void initState() {
super.initState();
_playMenuMusic();
}
Future<void> _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,
),
),
);

View File

@@ -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<EpisodeScreen> createState() => _EpisodeScreenState();
}
class _EpisodeScreenState extends State<EpisodeScreen> {
final List<String> _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),
),
),
);
},
),
],
),
),
);
}
}

View File

@@ -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(),
),
);
},

View File

@@ -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(

View File

@@ -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<WolfensteinData> 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<WolfLevel> get levels => activeGame.levels;
List<Sprite> get walls => activeGame.walls;
@@ -36,6 +49,7 @@ class Wolf3d {
/// Initializes the engine by loading available game data.
Future<void> init({String? directory}) async {
await audio.init();
availableGames.clear();
// 1. Bundle asset loading (migrated from GameSelectScreen)