diff --git a/lib/screens/difficulty_screen.dart b/lib/screens/difficulty_screen.dart index c57b8a2..fd895f6 100644 --- a/lib/screens/difficulty_screen.dart +++ b/lib/screens/difficulty_screen.dart @@ -30,6 +30,7 @@ class _DifficultyScreenState extends State { Wolf3d.I.activeGame, difficulty: difficulty, startingEpisode: Wolf3d.I.activeEpisode, + audio: Wolf3d.I.audio, ), ), ); diff --git a/packages/wolf_3d_data/lib/src/io/engine_input.dart b/packages/wolf_3d_data/lib/src/io/engine_input.dart deleted file mode 100644 index 333b170..0000000 --- a/packages/wolf_3d_data/lib/src/io/engine_input.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:wolf_3d_entities/wolf_3d_entities.dart'; - -class EngineInput { - bool isMovingForward = false; - bool isMovingBackward = false; - bool isTurningLeft = false; - bool isTurningRight = false; - bool isInteracting = false; - bool isFiring = false; - WeaponType? requestedWeapon; -} diff --git a/packages/wolf_3d_data/lib/wolf_3d_data.dart b/packages/wolf_3d_data/lib/wolf_3d_data.dart index fd0c313..0ada12c 100644 --- a/packages/wolf_3d_data/lib/wolf_3d_data.dart +++ b/packages/wolf_3d_data/lib/wolf_3d_data.dart @@ -3,6 +3,5 @@ /// More dartdocs go here. library; -export 'src/io/engine_input.dart'; export 'src/wl_parser.dart' show WLParser; export 'src/wolfenstein_loader.dart' show WolfensteinLoader; diff --git a/packages/wolf_3d_data_types/lib/src/sound.dart b/packages/wolf_3d_data_types/lib/src/sound.dart index 072ad40..f19ec2f 100644 --- a/packages/wolf_3d_data_types/lib/src/sound.dart +++ b/packages/wolf_3d_data_types/lib/src/sound.dart @@ -46,3 +46,63 @@ class ImfMusic { } typedef WolfMusicMap = List; + +/// Maps to the original sound indices from the Wolfenstein 3D source code. +/// Use these to index into `activeGame.sounds[id]`. +abstract class WolfSound { + // --- Doors & Environment --- + static const int openDoor = 18; + static const int closeDoor = 19; + static const int pushWall = 46; // Secret sliding walls + + // --- Weapons & Combat --- + static const int knifeAttack = 23; + static const int pistolFire = 24; + static const int machineGunFire = 26; + static const int gatlingFire = 32; // Historically SHOOTSND in the source + static const int naziFire = 58; // Enemy gunshots + + // --- Pickups & Items --- + static const int getMachineGun = 30; + static const int getAmmo = 31; + static const int getGatling = 38; + static const int healthSmall = 33; // Dog food / Meals + static const int healthLarge = 34; // First Aid + static const int treasure1 = 35; // Cross + static const int treasure2 = 36; // Chalice + static const int treasure3 = 37; // Chest + static const int treasure4 = 45; // Crown + static const int extraLife = 44; // 1-Up + + // --- Enemies: Standard --- + static const int guardHalt = 21; // "Halt!" + static const int dogBark = 41; + static const int dogDeath = 62; + static const int dogAttack = 68; + static const int deathScream1 = 29; + static const int deathScream2 = 22; + static const int deathScream3 = 25; + static const int ssSchutzstaffel = 51; // "Schutzstaffel!" + static const int ssMeinGott = 63; // SS Death + + // --- Enemies: Bosses (Retail Episodes 1-6) --- + static const int bossActive = 49; + static const int mutti = 50; // Hans Grosse Death + static const int ahhhg = 52; + static const int eva = 54; // Dr. Schabbs Death + static const int gutenTag = 55; // Hitler Greeting + static const int leben = 56; + static const int scheist = 57; // Hitler Death + static const int schabbsHas = 64; // Dr. Schabbs + static const int hitlerHas = 65; + static const int spion = 66; // Otto Giftmacher + static const int neinSoVass = 67; // Gretel Grosse Death + static const int mechSteps = 70; // Mecha-Hitler walking + + // --- UI & Progression --- + static const int levelDone = 40; + static const int endBonus1 = 42; + static const int endBonus2 = 43; + static const int noBonus = 47; + static const int percent100 = 48; +} diff --git a/packages/wolf_3d_data_types/lib/wolf_3d_data_types.dart b/packages/wolf_3d_data_types/lib/wolf_3d_data_types.dart index a830d82..3be1a53 100644 --- a/packages/wolf_3d_data_types/lib/wolf_3d_data_types.dart +++ b/packages/wolf_3d_data_types/lib/wolf_3d_data_types.dart @@ -12,7 +12,8 @@ export 'src/game_file.dart' show GameFile; export 'src/game_version.dart' show GameVersion; export 'src/image.dart' show VgaImage; export 'src/map_objects.dart' show MapObject; -export 'src/sound.dart' show PcmSound, ImfMusic, ImfInstruction, WolfMusicMap; +export 'src/sound.dart' + show PcmSound, ImfMusic, ImfInstruction, WolfMusicMap, WolfSound; export 'src/sprite.dart' hide Matrix; export 'src/sprite_frame_range.dart' show SpriteFrameRange; export 'src/wolf_level.dart' show WolfLevel; diff --git a/packages/wolf_3d_engine/lib/src/engine_audio.dart b/packages/wolf_3d_engine/lib/src/engine_audio.dart index 518982f..6d378af 100644 --- a/packages/wolf_3d_engine/lib/src/engine_audio.dart +++ b/packages/wolf_3d_engine/lib/src/engine_audio.dart @@ -1,7 +1,11 @@ import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; abstract class EngineAudio { + WolfensteinData? activeGame; + void playMenuMusic(); void playLevelMusic(WolfLevel level); void stopMusic(); - // You can easily add things like void playSoundEffect(SoundId id); later! + void playSoundEffect(int sfxId); + Future init(); + void dispose(); } diff --git a/packages/wolf_3d_engine/lib/src/engine_input.dart b/packages/wolf_3d_engine/lib/src/engine_input.dart new file mode 100644 index 0000000..1d803a0 --- /dev/null +++ b/packages/wolf_3d_engine/lib/src/engine_input.dart @@ -0,0 +1,22 @@ +import 'package:wolf_3d_entities/wolf_3d_entities.dart'; + +/// A pure, framework-agnostic snapshot of the player's intended actions for a single frame. +class EngineInput { + final bool isMovingForward; + final bool isMovingBackward; + final bool isTurningLeft; + final bool isTurningRight; + final bool isFiring; + final bool isInteracting; + final WeaponType? requestedWeapon; + + const EngineInput({ + this.isMovingForward = false, + this.isMovingBackward = false, + this.isTurningLeft = false, + this.isTurningRight = false, + this.isFiring = false, + this.isInteracting = false, + this.requestedWeapon, + }); +} diff --git a/packages/wolf_3d_engine/lib/src/managers/door_manager.dart b/packages/wolf_3d_engine/lib/src/managers/door_manager.dart index babf81d..8ae20b6 100644 --- a/packages/wolf_3d_engine/lib/src/managers/door_manager.dart +++ b/packages/wolf_3d_engine/lib/src/managers/door_manager.dart @@ -7,13 +7,17 @@ class DoorManager { // Key is '$x,$y' final Map doors = {}; + // Callback to play sounds without tightly coupling to the audio engine + final void Function(int sfxId) onPlaySound; + + DoorManager({required this.onPlaySound}); + void initDoors(Sprite wallGrid) { doors.clear(); for (int y = 0; y < wallGrid.length; y++) { for (int x = 0; x < wallGrid[y].length; x++) { int id = wallGrid[y][x]; if (id >= 90) { - // Assuming 90+ are doors based on your previous code doors['$x,$y'] = Door(x: x, y: y, mapId: id); } } @@ -22,7 +26,12 @@ class DoorManager { void update(Duration elapsed) { for (final door in doors.values) { - door.update(elapsed.inMilliseconds); + final newState = door.update(elapsed.inMilliseconds); + + // The Manager decides: "If a door just started closing, play the close sound." + if (newState == DoorState.closing) { + onPlaySound(WolfSound.closeDoor); + } } } @@ -32,7 +41,19 @@ class DoorManager { String key = '$targetX,$targetY'; if (doors.containsKey(key)) { - doors[key]!.interact(); + if (doors[key]!.interact()) { + // The Manager decides: "Player successfully opened a door, play the sound." + onPlaySound(WolfSound.openDoor); + } + } + } + + void tryOpenDoor(int x, int y) { + String key = '$x,$y'; + if (doors.containsKey(key) && doors[key]!.offset == 0.0) { + if (doors[key]!.interact()) { + onPlaySound(WolfSound.openDoor); + } } } @@ -47,16 +68,6 @@ class DoorManager { return offsets; } - void tryOpenDoor(int x, int y) { - String key = '$x,$y'; - if (doors.containsKey(key)) { - // If it's closed or closing, interact() will usually start it opening - if (doors[key]!.offset == 0.0) { - doors[key]!.interact(); - } - } - } - bool isDoorOpenEnough(int x, int y) { String key = '$x,$y'; if (doors.containsKey(key)) { diff --git a/packages/wolf_3d_engine/lib/src/wolf_3d_engine_base.dart b/packages/wolf_3d_engine/lib/src/wolf_3d_engine_base.dart index 2360e37..4cca58d 100644 --- a/packages/wolf_3d_engine/lib/src/wolf_3d_engine_base.dart +++ b/packages/wolf_3d_engine/lib/src/wolf_3d_engine_base.dart @@ -1,6 +1,5 @@ import 'dart:math' as math; -import 'package:wolf_3d_data/wolf_3d_data.dart'; import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; import 'package:wolf_3d_engine/wolf_3d_engine.dart'; import 'package:wolf_3d_entities/wolf_3d_entities.dart'; @@ -12,7 +11,9 @@ class WolfEngine { required this.startingEpisode, required this.onGameWon, required this.audio, - }); + }) : doorManager = DoorManager( + onPlaySound: (sfxId) => audio.playSoundEffect(sfxId), + ); final WolfensteinData data; final Difficulty difficulty; @@ -24,7 +25,8 @@ class WolfEngine { final void Function() onGameWon; // Managers - final DoorManager doorManager = DoorManager(); + final DoorManager doorManager; + final PushwallManager pushwallManager = PushwallManager(); // State @@ -94,6 +96,7 @@ class WolfEngine { final Level objectLevel = activeLevel.objectGrid; doorManager.initDoors(currentLevel); + pushwallManager.initPushwalls(currentLevel, objectLevel); audio.playLevelMusic(activeLevel); diff --git a/packages/wolf_3d_engine/lib/wolf_3d_engine.dart b/packages/wolf_3d_engine/lib/wolf_3d_engine.dart index 66d1cf9..4a68f17 100644 --- a/packages/wolf_3d_engine/lib/wolf_3d_engine.dart +++ b/packages/wolf_3d_engine/lib/wolf_3d_engine.dart @@ -4,6 +4,7 @@ library; export 'src/engine_audio.dart'; +export 'src/engine_input.dart'; export 'src/managers/door_manager.dart'; export 'src/managers/pushwall_manager.dart'; export 'src/player/player.dart'; diff --git a/packages/wolf_3d_entities/lib/src/entities/door.dart b/packages/wolf_3d_entities/lib/src/entities/door.dart index 3acdff4..b1f771d 100644 --- a/packages/wolf_3d_entities/lib/src/entities/door.dart +++ b/packages/wolf_3d_entities/lib/src/entities/door.dart @@ -3,54 +3,47 @@ enum DoorState { closed, opening, open, closing } class Door { final int x; final int y; - final int mapId; // To differentiate between regular doors and elevator doors + final int mapId; DoorState state = DoorState.closed; double offset = 0.0; - int openTime = 0; // When did the door fully open? - - // How long a door stays open before auto-closing + int openTime = 0; static const int openDurationMs = 3000; - Door({ - required this.x, - required this.y, - required this.mapId, - }); - - // Returns true if the door state changed this frame (useful for playing sounds later) - bool update(int currentTimeMs) { - bool stateChanged = false; + Door({required this.x, required this.y, required this.mapId}); + /// Updates animation. Returns the NEW state if it changed this frame, else null. + DoorState? update(int currentTimeMs) { if (state == DoorState.opening) { - offset += 0.02; // Slide speed + offset += 0.02; if (offset >= 1.0) { offset = 1.0; state = DoorState.open; openTime = currentTimeMs; - stateChanged = true; + return DoorState.open; } } else if (state == DoorState.open) { if (currentTimeMs - openTime > openDurationMs) { state = DoorState.closing; - stateChanged = true; + return DoorState.closing; } } else if (state == DoorState.closing) { - // Note: We don't check for entities blocking the door yet! offset -= 0.02; if (offset <= 0.0) { offset = 0.0; state = DoorState.closed; - stateChanged = true; + return DoorState.closed; } } - - return stateChanged; + return null; } - void interact() { + /// Triggers the opening process. Returns true if it successfully started opening. + bool interact() { if (state == DoorState.closed || state == DoorState.closing) { state = DoorState.opening; + return true; } + return false; } } diff --git a/packages/wolf_3d_flutter/lib/audio/audio_adaptor.dart b/packages/wolf_3d_flutter/lib/audio/audio_adaptor.dart index c4578b2..da0e1fd 100644 --- a/packages/wolf_3d_flutter/lib/audio/audio_adaptor.dart +++ b/packages/wolf_3d_flutter/lib/audio/audio_adaptor.dart @@ -12,4 +12,34 @@ class FlutterAudioAdapter implements EngineAudio { void stopMusic() { Wolf3d.I.audio.stopMusic(); } + + @override + void playSoundEffect(int sfxId) { + Wolf3d.I.audio.playSoundEffect(sfxId); + } + + @override + void playMenuMusic() { + Wolf3d.I.audio.playMenuMusic(); + } + + @override + Future init() async { + await Wolf3d.I.audio.init(); + } + + @override + void dispose() { + Wolf3d.I.audio.dispose(); + } + + @override + WolfensteinData? get activeGame => Wolf3d.I.activeGame; + + @override + set activeGame(WolfensteinData? value) { + if (value != null) { + Wolf3d.I.setActiveGame(value); + } + } } diff --git a/packages/wolf_3d_flutter/lib/wolf_3d.dart b/packages/wolf_3d_flutter/lib/wolf_3d.dart index bcc69e7..06fe16a 100644 --- a/packages/wolf_3d_flutter/lib/wolf_3d.dart +++ b/packages/wolf_3d_flutter/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_engine/wolf_3d_engine.dart'; import 'package:wolf_3d_synth/wolf_3d_synth.dart'; class Wolf3d { @@ -14,7 +15,7 @@ class Wolf3d { WolfensteinData? _activeGame; // --- Core Systems --- - final WolfAudio audio = WolfAudio(); + final EngineAudio audio = WolfAudio(); // --- Getters --- WolfensteinData get activeGame { diff --git a/packages/wolf_3d_input/lib/wolf_3d_input.dart b/packages/wolf_3d_input/lib/wolf_3d_input.dart index a0bddc4..6a2cd00 100644 --- a/packages/wolf_3d_input/lib/wolf_3d_input.dart +++ b/packages/wolf_3d_input/lib/wolf_3d_input.dart @@ -1,48 +1,34 @@ import 'package:flutter/services.dart'; +import 'package:wolf_3d_engine/wolf_3d_engine.dart'; import 'package:wolf_3d_entities/wolf_3d_entities.dart'; -class InputManager { +class WolfInput { Set _previousKeys = {}; bool isMovingForward = false; bool isMovingBackward = false; bool isTurningLeft = false; bool isTurningRight = false; - - // Discrete (triggers once per press) bool isInteracting = false; - - // Continuous bool isFiring = false; - WeaponType? requestedWeapon; void update() { final pressedKeys = HardwareKeyboard.instance.logicalKeysPressed; - - // Calculate all keys that were pressed exactly on this frame final newlyPressedKeys = pressedKeys.difference(_previousKeys); - // * Movement isMovingForward = pressedKeys.contains(LogicalKeyboardKey.keyW); isMovingBackward = pressedKeys.contains(LogicalKeyboardKey.keyS); isTurningLeft = pressedKeys.contains(LogicalKeyboardKey.keyA); isTurningRight = pressedKeys.contains(LogicalKeyboardKey.keyD); - // * Interaction (Space) - // Much simpler now using the newlyPressedKeys set isInteracting = newlyPressedKeys.contains(LogicalKeyboardKey.space); - // * Firing (Left Control) - // - Keeping this continuous for machine guns isFiring = pressedKeys.contains(LogicalKeyboardKey.controlLeft) && !pressedKeys.contains(LogicalKeyboardKey.space); - // * Manual Weapon Switching requestedWeapon = null; - - // Iterate through newly pressed keys and switch on them for (final LogicalKeyboardKey key in newlyPressedKeys) { switch (key) { case LogicalKeyboardKey.digit1: @@ -56,7 +42,17 @@ class InputManager { } } - // * Save state for next tick _previousKeys = Set.from(pressedKeys); } + + /// Exports the current state as a clean DTO for the engine + EngineInput get currentInput => EngineInput( + isMovingForward: isMovingForward, + isMovingBackward: isMovingBackward, + isTurningLeft: isTurningLeft, + isTurningRight: isTurningRight, + isFiring: isFiring, + isInteracting: isInteracting, + requestedWeapon: requestedWeapon, + ); } diff --git a/packages/wolf_3d_input/pubspec.yaml b/packages/wolf_3d_input/pubspec.yaml index ff7ab18..13454e6 100644 --- a/packages/wolf_3d_input/pubspec.yaml +++ b/packages/wolf_3d_input/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: flutter: sdk: flutter wolf_3d_entities: any + wolf_3d_engine: any dev_dependencies: flutter_test: diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart index 813f1b8..86d99e5 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; import 'package:wolf_3d_engine/wolf_3d_engine.dart'; -import 'package:wolf_3d_entities/wolf_3d_entities.dart'; import 'package:wolf_3d_input/wolf_3d_input.dart'; import 'package:wolf_3d_renderer/hud.dart'; import 'package:wolf_3d_renderer/raycast_painter.dart'; @@ -15,12 +14,14 @@ class WolfRenderer extends StatefulWidget { this.data, { required this.difficulty, required this.startingEpisode, + required this.audio, super.key, }); final WolfensteinData data; final Difficulty difficulty; final int startingEpisode; + final EngineAudio audio; @override State createState() => _WolfRendererState(); @@ -28,143 +29,49 @@ class WolfRenderer extends StatefulWidget { class _WolfRendererState extends State with SingleTickerProviderStateMixin { - final InputManager inputManager = InputManager(); - final DoorManager doorManager = DoorManager(); - final PushwallManager pushwallManager = PushwallManager(); + // 1. The input reader + final WolfInput inputManager = WolfInput(); + + // 2. The central brain of the game + late final WolfEngine engine; late Ticker _gameLoop; final FocusNode _focusNode = FocusNode(); - late Level currentLevel; - late WolfLevel activeLevel; final double fov = math.pi / 3; - late Player player; - - bool _isLoading = true; - - double damageFlashOpacity = 0.0; - - late int _currentEpisodeIndex; - late int _currentLevelIndex; - int? _returnLevelIndex; - - List entities = []; - @override void initState() { super.initState(); - _initGame(); - } - Future _initGame() async { - // 1. Setup our starting indices - _currentEpisodeIndex = widget.startingEpisode; - _currentLevelIndex = 0; + // Initialize the engine and hand over all the data and dependencies + engine = WolfEngine( + data: widget.data, + difficulty: widget.difficulty, + startingEpisode: widget.startingEpisode, + audio: widget.audio, + onGameWon: () { + Navigator.of(context).pop(); + }, + ); - // 2. Load the first floor! - _loadLevel(); + engine.init(); + // Start the loop _gameLoop = createTicker(_tick)..start(); _focusNode.requestFocus(); - - setState(() { - _isLoading = false; - }); } - void _loadLevel() { - // 1. Clean up the previous level's state - entities.clear(); - damageFlashOpacity = 0.0; + // --- ORCHESTRATOR --- + void _tick(Duration elapsed) { + // 1. Read the keyboard state + inputManager.update(); - // 2. Grab the exact level from our new Episode hierarchy - final episode = widget.data.episodes[_currentEpisodeIndex]; - activeLevel = episode.levels[_currentLevelIndex]; + // 2. Let the engine do all the math, physics, collision, and logic! + engine.tick(elapsed, inputManager.currentInput); - // 3. DEEP COPY the wall grid! If we don't do this, destroying walls/doors - // will permanently corrupt the map data in the Wolf3d singleton. - currentLevel = List.generate(64, (y) => List.from(activeLevel.wallGrid[y])); - final Level objectLevel = activeLevel.objectGrid; - - // 4. Initialize Managers - doorManager.initDoors(currentLevel); - pushwallManager.initPushwalls(currentLevel, objectLevel); - - // 6. Spawn Player and Entities - for (int y = 0; y < 64; y++) { - for (int x = 0; x < 64; x++) { - int objId = objectLevel[y][x]; - - if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) { - double spawnAngle = 0.0; - if (objId == MapObject.playerNorth) { - spawnAngle = 3 * math.pi / 2; - } else if (objId == MapObject.playerEast) { - spawnAngle = 0.0; - } else if (objId == MapObject.playerSouth) { - spawnAngle = math.pi / 2; - } else if (objId == MapObject.playerWest) { - spawnAngle = math.pi; - } - - player = Player(x: x + 0.5, y: y + 0.5, angle: spawnAngle); - } else { - Entity? newEntity = EntityRegistry.spawn( - objId, - x + 0.5, - y + 0.5, - widget.difficulty, - widget.data.sprites.length, - isSharewareMode: widget.data.version == GameVersion.shareware, - ); - if (newEntity != null) entities.add(newEntity); - } - } - } - - // 7. Clear non-solid blocks from the collision grid - for (int y = 0; y < 64; y++) { - for (int x = 0; x < 64; x++) { - int id = currentLevel[y][x]; - if (!((id >= 1 && id <= 63) || (id >= 90 && id <= 101))) { - currentLevel[y][x] = 0; - } - } - } - - _bumpPlayerIfStuck(); - debugPrint("Loaded Floor: ${_currentLevelIndex + 1} - ${activeLevel.name}"); - } - - void _onLevelCompleted({bool isSecretExit = false}) { - setState(() { - final currentEpisode = widget.data.episodes[_currentEpisodeIndex]; - - if (isSecretExit) { - // Save the next normal map index so we can return to it later - _returnLevelIndex = _currentLevelIndex + 1; - _currentLevelIndex = 9; // Jump to the secret map - debugPrint("Found the Secret Exit!"); - } else { - // Are we currently ON the secret map, and need to return? - if (_currentLevelIndex == 9 && _returnLevelIndex != null) { - _currentLevelIndex = _returnLevelIndex!; - _returnLevelIndex = null; - } else { - _currentLevelIndex++; // Normal progression - } - } - - // Did we just beat the last map in the episode (Map 9) or the secret map (Map 10)? - if (_currentLevelIndex >= currentEpisode.levels.length || - _currentLevelIndex > 9) { - debugPrint("Episode Completed! You win!"); - Navigator.of(context).pop(); - } else { - _loadLevel(); - } - }); + // 3. Force a UI repaint using the newly updated engine state + setState(() {}); } @override @@ -174,285 +81,10 @@ class _WolfRendererState extends State super.dispose(); } - void _bumpPlayerIfStuck() { - int pX = player.x.toInt(); - int pY = player.y.toInt(); - - if (pY < 0 || - pY >= currentLevel.length || - pX < 0 || - pX >= currentLevel[0].length || - currentLevel[pY][pX] > 0) { - double shortestDist = double.infinity; - Coordinate2D nearestSafeSpot = Coordinate2D(1.5, 1.5); - - for (int y = 0; y < currentLevel.length; y++) { - for (int x = 0; x < currentLevel[y].length; x++) { - if (currentLevel[y][x] == 0) { - Coordinate2D safeSpot = Coordinate2D(x + 0.5, y + 0.5); - double dist = safeSpot.distanceTo(player.position); - - if (dist < shortestDist) { - shortestDist = dist; - nearestSafeSpot = safeSpot; - } - } - } - } - player.x = nearestSafeSpot.x; - player.y = nearestSafeSpot.y; - } - } - - bool _isWalkable(int x, int y) { - if (currentLevel[y][x] == 0) return true; - if (currentLevel[y][x] >= 90) { - return doorManager.isDoorOpenEnough(x, y); - } - return false; - } - - // --- ORCHESTRATOR --- - void _tick(Duration elapsed) { - // 1. Process intentions and receive movement vectors - final inputResult = _processInputs(elapsed); - - doorManager.update(elapsed); - pushwallManager.update(elapsed, currentLevel); - - // 2. Explicit State Updates - player.updateWeaponSwitch(); - - player.angle += inputResult.dAngle; - - // Keep the angle neatly clamped between 0 and 2*PI - if (player.angle < 0) player.angle += 2 * math.pi; - if (player.angle >= 2 * math.pi) player.angle -= 2 * math.pi; - - final Coordinate2D validatedPos = _calculateValidatedPosition( - player.position, - inputResult.movement, - ); - - player.x = validatedPos.x; - player.y = validatedPos.y; - - _updateEntities(elapsed); - - // Explicit reassignment from a pure(r) function - damageFlashOpacity = _calculateScreenEffects(damageFlashOpacity); - - // 3. Combat - player.updateWeapon( - currentTime: elapsed.inMilliseconds, - entities: entities, - isWalkable: _isWalkable, - ); - - // 4. Render - setState(() {}); - } - - // Returns a Record containing both movement delta and rotation delta - ({Coordinate2D movement, double dAngle}) _processInputs(Duration elapsed) { - inputManager.update(); - - const double moveSpeed = 0.14; - const double turnSpeed = 0.10; - - Coordinate2D movement = const Coordinate2D(0, 0); - double dAngle = 0.0; - - if (inputManager.requestedWeapon != null) { - player.requestWeaponSwitch(inputManager.requestedWeapon!); - } - - if (inputManager.isFiring) { - player.fire(elapsed.inMilliseconds); - } else { - player.releaseTrigger(); - } - - // Calculate intended rotation - if (inputManager.isTurningLeft) dAngle -= turnSpeed; - if (inputManager.isTurningRight) dAngle += turnSpeed; - - // Calculate intended movement based on CURRENT angle - Coordinate2D forwardVec = Coordinate2D( - math.cos(player.angle), - math.sin(player.angle), - ); - - if (inputManager.isMovingForward) { - movement += forwardVec * moveSpeed; - } - if (inputManager.isMovingBackward) { - movement -= forwardVec * moveSpeed; - } - - if (inputManager.isInteracting) { - // 1. Calculate the tile exactly 1 block in front of the player - int targetX = (player.x + math.cos(player.angle)).toInt(); - int targetY = (player.y + math.sin(player.angle)).toInt(); - - // Ensure we don't check outside the map bounds - if (targetX >= 0 && targetX < 64 && targetY >= 0 && targetY < 64) { - // 2. Check the WALL grid for the physical switch texture - int wallId = currentLevel[targetY][targetX]; - if (wallId == MapObject.normalElevatorSwitch) { - // Player hit the switch! - _onLevelCompleted(isSecretExit: false); - return (movement: const Coordinate2D(0, 0), dAngle: 0.0); - } else if (wallId == MapObject.secretElevatorSwitch) { - _onLevelCompleted(isSecretExit: true); - return (movement: const Coordinate2D(0, 0), dAngle: 0.0); - } - - // 3. Check the OBJECT grid for invisible floor triggers - // (Some custom maps use these instead of wall switches) - int objId = activeLevel.objectGrid[targetY][targetX]; - if (objId == MapObject.normalExitTrigger) { - _onLevelCompleted(isSecretExit: false); - return (movement: movement, dAngle: dAngle); - } else if (objId == MapObject.secretExitTrigger) { - _onLevelCompleted(isSecretExit: true); - return (movement: movement, dAngle: dAngle); - } - } - - // 4. If it wasn't an elevator, try opening a door or pushing a wall - doorManager.handleInteraction(player.x, player.y, player.angle); - pushwallManager.handleInteraction( - player.x, - player.y, - player.angle, - currentLevel, - ); - } - - return (movement: movement, dAngle: dAngle); - } - - Coordinate2D _calculateValidatedPosition( - Coordinate2D currentPos, - Coordinate2D movement, - ) { - const double margin = 0.3; - double newX = currentPos.x; - double newY = currentPos.y; - - // Calculate potential new coordinates - Coordinate2D target = currentPos + movement; - - // Validate X (allows sliding along walls) - if (movement.x != 0) { - int checkX = (movement.x > 0) - ? (target.x + margin).toInt() - : (target.x - margin).toInt(); - - if (_isWalkable(checkX, currentPos.y.toInt())) { - newX = target.x; - } - } - - // Validate Y - if (movement.y != 0) { - int checkY = (movement.y > 0) - ? (target.y + margin).toInt() - : (target.y - margin).toInt(); - - if (_isWalkable(newX.toInt(), checkY)) { - newY = target.y; - } - } - - return Coordinate2D(newX, newY); - } - - void _updateEntities(Duration elapsed) { - List itemsToRemove = []; - List itemsToAdd = []; // NEW: Buffer for dropped items - - for (Entity entity in entities) { - if (entity is Enemy) { - // 1. Get Intent (Now passing tryOpenDoor!) - final intent = entity.update( - elapsedMs: elapsed.inMilliseconds, - playerPosition: player.position, - isWalkable: _isWalkable, - tryOpenDoor: doorManager.tryOpenDoor, - onDamagePlayer: (int damage) { - player.takeDamage(damage); - damageFlashOpacity = 0.5; - }, - ); - - // 2. Update Angle - entity.angle = intent.newAngle; - - // 3. Resolve Movement - // We NO LONGER use _calculateValidatedPosition here! - // The enemy's internal getValidMovement already did the math perfectly. - entity.x += intent.movement.x; - entity.y += intent.movement.y; - - // 4. Handle Item Drops & Score (Matches KillActor in C code) - if (entity.state == EntityState.dead && - entity.isDying && - !entity.hasDroppedItem) { - entity.hasDroppedItem = true; - - // Map ID 44 is usually the Ammo Clip in the Object Grid/Registry - Entity? droppedAmmo = EntityRegistry.spawn( - MapObject.ammoClip, - entity.x, - entity.y, - widget.difficulty, - widget.data.sprites.length, - ); - - if (droppedAmmo != null) { - itemsToAdd.add(droppedAmmo); - } - - // You will need to add a `bool hasDroppedItem = false;` to your base Enemy class. - - if (entity.runtimeType.toString() == 'BrownGuard') { - // Example: Spawn an ammo clip where the guard died - // itemsToAdd.add(Collectible(x: entity.x, y: entity.y, type: CollectibleType.ammoClip)); - } else if (entity.runtimeType.toString() == 'Dog') { - // Dogs don't drop items, but maybe they give different points! - } - } - } else if (entity is Collectible) { - if (player.position.distanceTo(entity.position) < 0.5) { - if (player.tryPickup(entity)) { - itemsToRemove.add(entity); - } - } - } - } - - // Clean up dead items and add new drops - if (itemsToRemove.isNotEmpty) { - entities.removeWhere((e) => itemsToRemove.contains(e)); - } - if (itemsToAdd.isNotEmpty) { - entities.addAll(itemsToAdd); - } - } - - // Takes an input and returns a value instead of implicitly changing state - double _calculateScreenEffects(double currentOpacity) { - if (currentOpacity > 0) { - return math.max(0.0, currentOpacity - 0.05); - } - return currentOpacity; - } - @override Widget build(BuildContext context) { - if (_isLoading) { + // Wait for the engine to finish parsing the level + if (!engine.isInitialized) { return const Center(child: CircularProgressIndicator(color: Colors.teal)); } @@ -472,36 +104,46 @@ class _WolfRendererState extends State aspectRatio: 16 / 10, child: Stack( children: [ + // --- 3D WORLD --- CustomPaint( size: Size( constraints.maxWidth, constraints.maxHeight, ), painter: RaycasterPainter( - map: currentLevel, + // Read state directly from the engine + map: engine.currentLevel, textures: widget.data.walls, - player: player, + player: engine.player, fov: fov, - doorOffsets: doorManager.getOffsetsForRenderer(), - entities: entities, + doorOffsets: engine.doorManager + .getOffsetsForRenderer(), + entities: engine.entities, sprites: widget.data.sprites, - activePushwall: pushwallManager.activePushwall, + activePushwall: + engine.pushwallManager.activePushwall, ), ), + + // --- FIRST PERSON WEAPON --- Positioned( bottom: -20, left: 0, right: 0, child: Center( child: Transform.translate( - offset: Offset(0, player.weaponAnimOffset), + offset: Offset( + 0, + engine.player.weaponAnimOffset, + ), child: SizedBox( width: 500, height: 500, child: CustomPaint( painter: WeaponPainter( sprite: - widget.data.sprites[player + widget.data.sprites[engine + .player .currentWeapon .getCurrentSpriteIndex( widget.data.sprites.length, @@ -512,11 +154,13 @@ class _WolfRendererState extends State ), ), ), - if (damageFlashOpacity > 0) + + // --- DAMAGE FLASH --- + if (engine.damageFlashOpacity > 0) Positioned.fill( child: Container( color: Colors.red.withValues( - alpha: damageFlashOpacity, + alpha: engine.damageFlashOpacity, ), ), ), @@ -527,7 +171,9 @@ class _WolfRendererState extends State }, ), ), - Hud(player: player), + + // --- UI --- + Hud(player: engine.player), ], ), ), diff --git a/packages/wolf_3d_synth/lib/src/wolf_3d_audio.dart b/packages/wolf_3d_synth/lib/src/wolf_3d_audio.dart index 701277b..a499357 100644 --- a/packages/wolf_3d_synth/lib/src/wolf_3d_audio.dart +++ b/packages/wolf_3d_synth/lib/src/wolf_3d_audio.dart @@ -2,9 +2,10 @@ import 'dart:typed_data'; import 'package:audioplayers/audioplayers.dart'; import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; +import 'package:wolf_3d_engine/wolf_3d_engine.dart'; import 'package:wolf_3d_synth/src/imf_renderer.dart'; -class WolfAudio { +class WolfAudio implements EngineAudio { bool _isInitialized = false; // --- Music State --- @@ -16,9 +17,11 @@ class WolfAudio { final List _sfxPlayers = []; int _currentSfxIndex = 0; + @override WolfensteinData? activeGame; /// Initializes the audio engine and pre-allocates the SFX pool. + @override Future init() async { if (_isInitialized) return; @@ -45,6 +48,7 @@ class WolfAudio { } /// Disposes of the audio engine and frees resources. + @override void dispose() { stopMusic(); _musicPlayer.dispose(); @@ -79,6 +83,7 @@ class WolfAudio { } } + @override Future stopMusic() async { if (!_isInitialized) return; await _musicPlayer.stop(); @@ -92,12 +97,14 @@ class WolfAudio { if (_isInitialized) await _musicPlayer.resume(); } + @override Future playMenuMusic() async { final data = activeGame; if (data == null || data.music.length <= 1) return; await playMusic(data.music[1]); } + @override Future playLevelMusic(WolfLevel level) async { final data = activeGame; if (data == null || data.music.isEmpty) return; @@ -114,21 +121,38 @@ class WolfAudio { // SFX MANAGEMENT // ========================================== - /// Plays a sound effect from a WAV byte array using the round-robin pool. - Future playSfx(Uint8List wavBytes) async { - if (!_isInitialized) return; + @override + Future playSoundEffect(int sfxId) async { + print("Playing sfx id $sfxId"); + if (!_isInitialized || activeGame == null) return; + + // 1. Get the raw 8-bit unsigned PCM bytes from the game data + final Uint8List raw8bitBytes = activeGame!.sounds[sfxId].bytes; + if (raw8bitBytes.isEmpty) return; + + // 2. Convert 8-bit Unsigned PCM -> 16-bit Signed PCM + // Wolf3D raw sounds are biased at 128. + final Int16List converted16bit = Int16List(raw8bitBytes.length); + for (int i = 0; i < raw8bitBytes.length; i++) { + // (sample - 128) shifts it to signed 8-bit range (-128 to 127) + // Multiplying by 256 scales it to 16-bit range (-32768 to 32512) + converted16bit[i] = (raw8bitBytes[i] - 128) * 256; + } + + // 3. Wrap in a WAV header (Wolf3D digitized sounds are 7000Hz Mono) + final wavBytes = ImfRenderer.createWavFile( + converted16bit, + sampleRate: 7000, + ); try { - // Grab the next available player in the pool final player = _sfxPlayers[_currentSfxIndex]; - - // Move to the next index, looping back to 0 if we hit the max _currentSfxIndex = (_currentSfxIndex + 1) % _maxSfxChannels; - // Play the sound (this interrupts whatever this specific channel was playing) + // Note: We use BytesSource because createWavFile returns Uint8List (the file bytes) await player.play(BytesSource(wavBytes)); } catch (e) { - print("WolfAudio: Error playing SFX - $e"); + print("WolfAudio: SFX Error - $e"); } } } diff --git a/packages/wolf_3d_synth/pubspec.yaml b/packages/wolf_3d_synth/pubspec.yaml index 80da1e3..8667a69 100644 --- a/packages/wolf_3d_synth/pubspec.yaml +++ b/packages/wolf_3d_synth/pubspec.yaml @@ -11,6 +11,7 @@ resolution: workspace dependencies: audioplayers: ^6.6.0 wolf_3d_data_types: + wolf_3d_engine: dev_dependencies: lints: ^6.0.0