Play (wrong) door sound effect when the door opens or closes
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -30,6 +30,7 @@ class _DifficultyScreenState extends State<DifficultyScreen> {
|
||||
Wolf3d.I.activeGame,
|
||||
difficulty: difficulty,
|
||||
startingEpisode: Wolf3d.I.activeEpisode,
|
||||
audio: Wolf3d.I.audio,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -46,3 +46,63 @@ class ImfMusic {
|
||||
}
|
||||
|
||||
typedef WolfMusicMap = List<int>;
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<void> init();
|
||||
void dispose();
|
||||
}
|
||||
|
||||
22
packages/wolf_3d_engine/lib/src/engine_input.dart
Normal file
22
packages/wolf_3d_engine/lib/src/engine_input.dart
Normal file
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -7,13 +7,17 @@ class DoorManager {
|
||||
// Key is '$x,$y'
|
||||
final Map<String, Door> 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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<LogicalKeyboardKey> _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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
wolf_3d_entities: any
|
||||
wolf_3d_engine: any
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -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<WolfRenderer> createState() => _WolfRendererState();
|
||||
@@ -28,143 +29,49 @@ class WolfRenderer extends StatefulWidget {
|
||||
|
||||
class _WolfRendererState extends State<WolfRenderer>
|
||||
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<Entity> entities = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initGame();
|
||||
}
|
||||
|
||||
Future<void> _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<WolfRenderer>
|
||||
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<Entity> itemsToRemove = [];
|
||||
List<Entity> 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<WolfRenderer>
|
||||
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<WolfRenderer>
|
||||
),
|
||||
),
|
||||
),
|
||||
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<WolfRenderer>
|
||||
},
|
||||
),
|
||||
),
|
||||
Hud(player: player),
|
||||
|
||||
// --- UI ---
|
||||
Hud(player: engine.player),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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<AudioPlayer> _sfxPlayers = [];
|
||||
int _currentSfxIndex = 0;
|
||||
|
||||
@override
|
||||
WolfensteinData? activeGame;
|
||||
|
||||
/// Initializes the audio engine and pre-allocates the SFX pool.
|
||||
@override
|
||||
Future<void> 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<void> stopMusic() async {
|
||||
if (!_isInitialized) return;
|
||||
await _musicPlayer.stop();
|
||||
@@ -92,12 +97,14 @@ class WolfAudio {
|
||||
if (_isInitialized) await _musicPlayer.resume();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> playMenuMusic() async {
|
||||
final data = activeGame;
|
||||
if (data == null || data.music.length <= 1) return;
|
||||
await playMusic(data.music[1]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> 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<void> playSfx(Uint8List wavBytes) async {
|
||||
if (!_isInitialized) return;
|
||||
@override
|
||||
Future<void> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ resolution: workspace
|
||||
dependencies:
|
||||
audioplayers: ^6.6.0
|
||||
wolf_3d_data_types:
|
||||
wolf_3d_engine:
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^6.0.0
|
||||
|
||||
Reference in New Issue
Block a user