Refactor audio module to use built-in music and sound effect identifiers

- Introduced BuiltInMusicModule and BuiltInSfxModule to replace RetailMusicModule and RetailSfxModule.
- Updated RetailAssetRegistry and SharewareAssetRegistry to utilize the new built-in modules.
- Removed deprecated MusicKey and SfxKey classes, replacing them with Music and SoundEffect enums for better clarity and maintainability.
- Adjusted music and sound effect resolution methods to align with the new structure.
- Updated audio playback methods in WolfAudio and FlutterAudioAdapter to accept the new Music and SoundEffect types.
- Refactored tests to accommodate changes in audio event handling and ensure compatibility with the new identifiers.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-20 17:07:25 +01:00
parent 8cca66e966
commit 10417d26ba
42 changed files with 499 additions and 622 deletions

View File

@@ -46,74 +46,6 @@ class _AudioGalleryState extends State<AudioGallery> {
return 2; return 2;
} }
static const List<SfxKey> _knownSfxKeys = [
SfxKey.openDoor,
SfxKey.closeDoor,
SfxKey.pushWall,
SfxKey.knifeAttack,
SfxKey.pistolFire,
SfxKey.machineGunFire,
SfxKey.chainGunFire,
SfxKey.enemyFire,
SfxKey.getMachineGun,
SfxKey.getAmmo,
SfxKey.getChainGun,
SfxKey.healthSmall,
SfxKey.healthLarge,
SfxKey.treasure1,
SfxKey.treasure2,
SfxKey.treasure3,
SfxKey.treasure4,
SfxKey.extraLife,
SfxKey.guardHalt,
SfxKey.dogBark,
SfxKey.dogDeath,
SfxKey.dogAttack,
SfxKey.deathScream1,
SfxKey.deathScream2,
SfxKey.deathScream3,
SfxKey.ssAlert,
SfxKey.ssDeath,
SfxKey.bossActive,
SfxKey.hansGrosseDeath,
SfxKey.schabbs,
SfxKey.schabbsDeath,
SfxKey.hitlerGreeting,
SfxKey.hitlerDeath,
SfxKey.mechaSteps,
SfxKey.ottoAlert,
SfxKey.gretelDeath,
SfxKey.levelComplete,
SfxKey.endBonus1,
SfxKey.endBonus2,
SfxKey.noBonus,
SfxKey.percent100,
];
static const List<MusicKey> _knownMusicKeys = [
MusicKey.menuTheme,
MusicKey.level01,
MusicKey.level02,
MusicKey.level03,
MusicKey.level04,
MusicKey.level05,
MusicKey.level06,
MusicKey.level07,
MusicKey.level08,
MusicKey.level09,
MusicKey.level10,
MusicKey.level11,
MusicKey.level12,
MusicKey.level13,
MusicKey.level14,
MusicKey.level15,
MusicKey.level16,
MusicKey.level17,
MusicKey.level18,
MusicKey.level19,
MusicKey.level20,
];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -130,14 +62,14 @@ class _AudioGalleryState extends State<AudioGallery> {
Map<int, List<String>> _buildSfxAliases() { Map<int, List<String>> _buildSfxAliases() {
final Map<int, Set<String>> aliasesById = {}; final Map<int, Set<String>> aliasesById = {};
for (final key in _knownSfxKeys) { for (final key in SoundEffect.values) {
final ref = _selectedGame.registry.sfx.resolve(key); final ref = _selectedGame.registry.sfx.resolve(key);
if (ref == null) { if (ref == null) {
continue; continue;
} }
aliasesById aliasesById
.putIfAbsent(ref.slotIndex, () => <String>{}) .putIfAbsent(ref.slotIndex, () => <String>{})
.add(_readableKeyName(key.toString(), 'SfxKey(')); .add(_readableKeyName(key.name));
} }
return aliasesById.map( return aliasesById.map(
@@ -147,14 +79,14 @@ class _AudioGalleryState extends State<AudioGallery> {
Map<int, List<String>> _buildMusicAliases() { Map<int, List<String>> _buildMusicAliases() {
final Map<int, Set<String>> aliasesById = {}; final Map<int, Set<String>> aliasesById = {};
for (final key in _knownMusicKeys) { for (final key in Music.values) {
final route = _selectedGame.registry.music.resolve(key); final route = _selectedGame.registry.music.resolve(key);
if (route == null) { if (route == null) {
continue; continue;
} }
aliasesById aliasesById
.putIfAbsent(route.trackIndex, () => <String>{}) .putIfAbsent(route.trackIndex, () => <String>{})
.add(_readableKeyName(key.toString(), 'MusicKey(')); .add(_readableKeyName(key.name));
} }
return aliasesById.map( return aliasesById.map(
@@ -162,15 +94,12 @@ class _AudioGalleryState extends State<AudioGallery> {
); );
} }
String _readableKeyName(String raw, String prefix) { String _readableKeyName(String raw) {
final String trimmed = raw.startsWith(prefix) && raw.endsWith(')') if (raw.isEmpty) {
? raw.substring(prefix.length, raw.length - 1)
: raw;
if (trimmed.isEmpty) {
return raw; return raw;
} }
return trimmed.replaceAllMapped( return raw.replaceAllMapped(
RegExp(r'([a-z0-9])([A-Z])'), RegExp(r'([a-z0-9])([A-Z])'),
(match) => '${match.group(1)} ${match.group(2)}', (match) => '${match.group(1)} ${match.group(2)}',
); );
@@ -203,7 +132,7 @@ class _AudioGalleryState extends State<AudioGallery> {
} }
void _playSfx(int id) { void _playSfx(int id) {
widget.wolf3d.audio.playSoundEffect(id); widget.wolf3d.audio.playSoundEffectId(id);
} }
Future<void> _toggleMusic(int trackIndex) async { Future<void> _toggleMusic(int trackIndex) async {

View File

@@ -13,20 +13,6 @@ import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
abstract class WLParser { abstract class WLParser {
static const int _areaTileBase = 107; static const int _areaTileBase = 107;
// --- Original Song Lookup Tables ---
static const List<int> _sharewareMusicMap = [
2, 3, 4, 5, 2, 3, 4, 5, 6, 7, // Episode 1
];
static const List<int> _retailMusicMap = [
2, 3, 4, 5, 2, 3, 4, 5, 6, 7, // Ep 1
8, 9, 10, 11, 8, 9, 11, 10, 6, 12, // Ep 2
13, 14, 15, 16, 13, 14, 15, 16, 17, 18, // Ep 3
2, 3, 4, 5, 2, 3, 4, 5, 6, 7, // Ep 4
8, 9, 10, 11, 8, 9, 11, 10, 6, 12, // Ep 5
13, 14, 15, 16, 13, 14, 15, 16, 17, 19, // Ep 6
];
/// Asynchronously discovers the game version and loads all necessary files. /// Asynchronously discovers the game version and loads all necessary files.
/// ///
/// Provide a [fileFetcher] callback (e.g., Flutter's `rootBundle.load` or /// Provide a [fileFetcher] callback (e.g., Flutter's `rootBundle.load` or
@@ -182,8 +168,6 @@ abstract class WLParser {
required DataVersion dataIdentity, required DataVersion dataIdentity,
AssetRegistry? registryOverride, AssetRegistry? registryOverride,
}) { }) {
final isShareware = version == GameVersion.shareware;
final audio = parseAudio(audioHed, audioT, version); final audio = parseAudio(audioHed, audioT, version);
final vgaImages = parseVgaImages(vgaDict, vgaHead, vgaGraph); final vgaImages = parseVgaImages(vgaDict, vgaHead, vgaGraph);
@@ -204,7 +188,7 @@ abstract class WLParser {
walls: parseWalls(vswap), walls: parseWalls(vswap),
sprites: parseSprites(vswap), sprites: parseSprites(vswap),
sounds: parseSounds(vswap).map((bytes) => PcmSound(bytes)).toList(), sounds: parseSounds(vswap).map((bytes) => PcmSound(bytes)).toList(),
episodes: parseEpisodes(mapHead, gameMaps, isShareware: isShareware), episodes: parseEpisodes(mapHead, gameMaps, version: version),
vgaImages: vgaImages, vgaImages: vgaImages,
adLibSounds: audio.adLib, adLibSounds: audio.adLib,
music: audio.music, music: audio.music,
@@ -438,13 +422,12 @@ abstract class WLParser {
static List<Episode> parseEpisodes( static List<Episode> parseEpisodes(
ByteData mapHead, ByteData mapHead,
ByteData gameMaps, { ByteData gameMaps, {
bool isShareware = true, required GameVersion version,
}) { }) {
List<WolfLevel> allLevels = []; List<WolfLevel> allLevels = [];
int rlewTag = mapHead.getUint16(0, Endian.little); int rlewTag = mapHead.getUint16(0, Endian.little);
// Select the correct music map based on the version final isShareware = version == GameVersion.shareware;
final activeMusicMap = isShareware ? _sharewareMusicMap : _retailMusicMap;
final episodeNames = isShareware final episodeNames = isShareware
? _sharewareEpisodeNames ? _sharewareEpisodeNames
: _retailEpisodeNames; : _retailEpisodeNames;
@@ -510,9 +493,9 @@ abstract class WLParser {
} }
// --- ASSIGN MUSIC --- // --- ASSIGN MUSIC ---
int trackIndex = (i < activeMusicMap.length) final episodeIndex = i ~/ 10;
? activeMusicMap[i] final levelIndex = i % 10;
: activeMusicMap[i % activeMusicMap.length]; final levelMusic = Music.levelFor(version, episodeIndex, levelIndex);
allLevels.add( allLevels.add(
WolfLevel( WolfLevel(
@@ -520,7 +503,7 @@ abstract class WLParser {
wallGrid: wallGrid, wallGrid: wallGrid,
objectGrid: objectGrid, objectGrid: objectGrid,
areaGrid: areaGrid, areaGrid: areaGrid,
musicIndex: trackIndex, music: levelMusic,
), ),
); );
} }

View File

@@ -57,64 +57,3 @@ class ImfMusic {
return ImfMusic(instructions); return ImfMusic(instructions);
} }
} }
typedef WolfMusicMap = List<int>;
/// Map indices to original sound effects as defined in the Wolfenstein 3D source.
abstract class WolfSound {
// --- Doors & Environment ---
static const int openDoor = 8;
static const int closeDoor = 9;
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;
}

View File

@@ -22,14 +22,14 @@ class WolfLevel {
/// zero-based and correspond to original AREATILE-derived sectors. /// zero-based and correspond to original AREATILE-derived sectors.
final SpriteMap areaGrid; final SpriteMap areaGrid;
/// The index of the [ImfMusic] track to play while this level is active. /// The [Music] track to play while this level is active.
final int musicIndex; final Music music;
const WolfLevel({ const WolfLevel({
required this.name, required this.name,
required this.wallGrid, required this.wallGrid,
required this.objectGrid, required this.objectGrid,
required this.areaGrid, required this.areaGrid,
required this.musicIndex, required this.music,
}); });
} }

View File

@@ -23,7 +23,7 @@ class WolfensteinData {
/// ///
/// Access the five sub-modules via: /// Access the five sub-modules via:
/// ```dart /// ```dart
/// data.registry.sfx.resolve(SfxKey.pistolFire) /// data.registry.sfx.resolve(SoundEffect.pistolFire)
/// data.registry.music.musicForLevel(episode, level) /// data.registry.music.musicForLevel(episode, level)
/// data.registry.entities.resolve(EntityKey.guard) /// data.registry.entities.resolve(EntityKey.guard)
/// data.registry.hud.faceForHealth(player.health) /// data.registry.hud.faceForHealth(player.health)

View File

@@ -4,10 +4,11 @@ abstract class EngineAudio {
WolfensteinData? activeGame; WolfensteinData? activeGame;
Future<void> debugSoundTest(); Future<void> debugSoundTest();
void playMenuMusic(); void playMenuMusic();
void playLevelMusic(WolfLevel level); void playLevelMusic(Music music);
void stopMusic(); void stopMusic();
Future<void> stopAllAudio(); Future<void> stopAllAudio();
void playSoundEffect(int sfxId); void playSoundEffect(SoundEffect effect);
void playSoundEffectId(int sfxId);
Future<void> init(); Future<void> init();
void dispose(); void dispose();
} }

View File

@@ -14,10 +14,7 @@ class CliSilentAudio implements EngineAudio {
void playMenuMusic() {} void playMenuMusic() {}
@override @override
void playLevelMusic(WolfLevel level) { void playLevelMusic(Music music) {}
// Optional: Print a log so you know it's working!
// debugPrint("🎵 Playing music for: ${level.name} 🎵");
}
@override @override
void stopMusic() {} void stopMusic() {}
@@ -26,7 +23,14 @@ class CliSilentAudio implements EngineAudio {
Future<void> stopAllAudio() async {} Future<void> stopAllAudio() async {}
@override @override
void playSoundEffect(int sfxId) { void playSoundEffect(SoundEffect effect) {
// Optional: You could use the terminal 'bell' character here
// to actually make a system beep when a sound plays!
// stdout.write('\x07');
}
@override
void playSoundEffectId(int sfxId) {
// Optional: You could use the terminal 'bell' character here // Optional: You could use the terminal 'bell' character here
// to actually make a system beep when a sound plays! // to actually make a system beep when a sound plays!
// stdout.write('\x07'); // stdout.write('\x07');

View File

@@ -15,7 +15,7 @@ class DoorManager {
/// Callback used to trigger sound effects without tight coupling /// Callback used to trigger sound effects without tight coupling
/// to a specific audio engine implementation. /// to a specific audio engine implementation.
final void Function(int sfxId) onPlaySound; final void Function(SoundEffect effect) onPlaySound;
static int _key(int x, int y) => ((y & 0xFFFF) << 16) | (x & 0xFFFF); static int _key(int x, int y) => ((y & 0xFFFF) << 16) | (x & 0xFFFF);
@@ -41,7 +41,7 @@ class DoorManager {
final newState = door.update(elapsed.inMilliseconds); final newState = door.update(elapsed.inMilliseconds);
if (newState == DoorState.closing) { if (newState == DoorState.closing) {
onPlaySound(WolfSound.closeDoor); onPlaySound(SoundEffect.closeDoor);
} }
} }
} }
@@ -60,7 +60,7 @@ class DoorManager {
final int key = _key(targetX, targetY); final int key = _key(targetX, targetY);
if (doors.containsKey(key) && doors[key]!.interact()) { if (doors.containsKey(key) && doors[key]!.interact()) {
onPlaySound(WolfSound.openDoor); onPlaySound(SoundEffect.openDoor);
log('[DEBUG] Player opened door at ($targetX, $targetY)'); log('[DEBUG] Player opened door at ($targetX, $targetY)');
return (x: targetX, y: targetY); return (x: targetX, y: targetY);
} }
@@ -74,7 +74,7 @@ class DoorManager {
// AI only interacts if the door is currently fully closed (offset == 0). // AI only interacts if the door is currently fully closed (offset == 0).
if (doors.containsKey(key) && doors[key]!.offset == 0.0) { if (doors.containsKey(key) && doors[key]!.offset == 0.0) {
if (doors[key]!.interact()) { if (doors[key]!.interact()) {
onPlaySound(WolfSound.openDoor); onPlaySound(SoundEffect.openDoor);
} }
} }
} }

View File

@@ -23,7 +23,7 @@ class PushwallManager {
PushwallManager({this.onPlaySound}); PushwallManager({this.onPlaySound});
/// Optional callback used to emit audio cues when pushwalls activate. /// Optional callback used to emit audio cues when pushwalls activate.
final void Function(int sfxId)? onPlaySound; final void Function(SoundEffect effect)? onPlaySound;
final Map<String, Pushwall> pushwalls = {}; final Map<String, Pushwall> pushwalls = {};
Pushwall? activePushwall; Pushwall? activePushwall;
@@ -127,7 +127,7 @@ class PushwallManager {
int checkY = targetY + pw.dirY; int checkY = targetY + pw.dirY;
if (wallGrid[checkY][checkX] == 0) { if (wallGrid[checkY][checkX] == 0) {
activePushwall = pw; activePushwall = pw;
onPlaySound?.call(WolfSound.pushWall); onPlaySound?.call(SoundEffect.pushWall);
} }
} }
} }

View File

@@ -147,7 +147,7 @@ class Player {
/// Attempts to collect [item] and returns the SFX to play. /// Attempts to collect [item] and returns the SFX to play.
/// ///
/// Returns `null` when the item was not collected (for example: full health). /// Returns `null` when the item was not collected (for example: full health).
int? tryPickup(Collectible item) { SoundEffect? tryPickup(Collectible item) {
final effect = item.tryCollect( final effect = item.tryCollect(
CollectiblePickupContext( CollectiblePickupContext(
health: health, health: health,
@@ -211,7 +211,7 @@ class Player {
requestWeaponSwitch(weaponType); requestWeaponSwitch(weaponType);
} }
return effect.pickupSfxId; return effect.pickupSoundEffect;
} }
bool fire(int currentTime) { bool fire(int currentTime) {

View File

@@ -35,10 +35,10 @@ class WolfEngine {
_availableGames = availableGames ?? <WolfensteinData>[data!], _availableGames = availableGames ?? <WolfensteinData>[data!],
audio = engineAudio ?? CliSilentAudio(), audio = engineAudio ?? CliSilentAudio(),
doorManager = DoorManager( doorManager = DoorManager(
onPlaySound: (sfxId) => engineAudio?.playSoundEffect(sfxId), onPlaySound: (effect) => engineAudio?.playSoundEffect(effect),
), ),
pushwallManager = PushwallManager( pushwallManager = PushwallManager(
onPlaySound: (sfxId) => engineAudio?.playSoundEffect(sfxId), onPlaySound: (effect) => engineAudio?.playSoundEffect(effect),
) { ) {
if (_availableGames.isEmpty) { if (_availableGames.isEmpty) {
throw StateError('WolfEngine requires at least one game data set.'); throw StateError('WolfEngine requires at least one game data set.');
@@ -481,7 +481,7 @@ class WolfEngine {
doorManager.initDoors(currentLevel); doorManager.initDoors(currentLevel);
pushwallManager.initPushwalls(currentLevel, _objectLevel); pushwallManager.initPushwalls(currentLevel, _objectLevel);
audio.playLevelMusic(activeLevel); audio.playLevelMusic(activeLevel.music);
// Spawn Player and Entities from the Object Grid // Spawn Player and Entities from the Object Grid
bool playerSpawned = false; bool playerSpawned = false;
@@ -540,7 +540,7 @@ class WolfEngine {
/// Handles floor transitions, including the "Level 10" secret floor logic. /// Handles floor transitions, including the "Level 10" secret floor logic.
void _onLevelCompleted({bool isSecretExit = false}) { void _onLevelCompleted({bool isSecretExit = false}) {
audio.playSoundEffect(WolfSound.levelDone); audio.playSoundEffect(SoundEffect.levelComplete);
audio.stopMusic(); audio.stopMusic();
player player
..hasGoldKey = false ..hasGoldKey = false
@@ -679,7 +679,7 @@ class WolfEngine {
if (entity.state == EntityState.dead && if (entity.state == EntityState.dead &&
entity.isDying && entity.isDying &&
!entity.hasPlayedDeathSound) { !entity.hasPlayedDeathSound) {
audio.playSoundEffect(entity.deathSoundId); audio.playSoundEffect(entity.deathSound);
entity.hasPlayedDeathSound = true; entity.hasPlayedDeathSound = true;
} }
@@ -787,9 +787,9 @@ class WolfEngine {
} }
} else if (entity is Collectible) { } else if (entity is Collectible) {
if (player.position.distanceTo(entity.position) < 0.5) { if (player.position.distanceTo(entity.position) < 0.5) {
final pickupSfxId = player.tryPickup(entity); final pickupSoundEffect = player.tryPickup(entity);
if (pickupSfxId != null) { if (pickupSoundEffect != null) {
audio.playSoundEffect(pickupSfxId); audio.playSoundEffect(pickupSoundEffect);
itemsToRemove.add(entity); itemsToRemove.add(entity);
} }
} }
@@ -826,7 +826,7 @@ class WolfEngine {
} }
entity.isAlerted = true; entity.isAlerted = true;
audio.playSoundEffect(entity.alertSoundId); audio.playSoundEffect(entity.alertSound);
log( log(
'[DEBUG] Enemy #${entity.debugId} (${entity.type.name}) ' '[DEBUG] Enemy #${entity.debugId} (${entity.type.name}) '
'alerted by gunfire in area $area', 'alerted by gunfire in area $area',
@@ -1007,10 +1007,10 @@ class WolfEngine {
void _playPlayerWeaponSound() { void _playPlayerWeaponSound() {
final sfxId = switch (player.currentWeapon.type) { final sfxId = switch (player.currentWeapon.type) {
WeaponType.knife => WolfSound.knifeAttack, WeaponType.knife => SoundEffect.knifeAttack,
WeaponType.pistol => WolfSound.pistolFire, WeaponType.pistol => SoundEffect.pistolFire,
WeaponType.machineGun => WolfSound.machineGunFire, WeaponType.machineGun => SoundEffect.machineGunFire,
WeaponType.chainGun => WolfSound.gatlingFire, WeaponType.chainGun => SoundEffect.chainGunFire,
}; };
audio.playSoundEffect(sfxId); audio.playSoundEffect(sfxId);

View File

@@ -29,7 +29,7 @@ class CollectiblePickupEffect {
final int ammoToAdd; final int ammoToAdd;
final int scoreToAdd; final int scoreToAdd;
final int extraLivesToAdd; final int extraLivesToAdd;
final int pickupSfxId; final SoundEffect pickupSoundEffect;
final bool grantGoldKey; final bool grantGoldKey;
final bool grantSilverKey; final bool grantSilverKey;
final WeaponType? grantWeapon; final WeaponType? grantWeapon;
@@ -40,7 +40,7 @@ class CollectiblePickupEffect {
this.ammoToAdd = 0, this.ammoToAdd = 0,
this.scoreToAdd = 0, this.scoreToAdd = 0,
this.extraLivesToAdd = 0, this.extraLivesToAdd = 0,
required this.pickupSfxId, required this.pickupSoundEffect,
this.grantGoldKey = false, this.grantGoldKey = false,
this.grantSilverKey = false, this.grantSilverKey = false,
this.grantWeapon, this.grantWeapon,
@@ -49,7 +49,7 @@ class CollectiblePickupEffect {
@override @override
String toString() { String toString() {
return 'CollectiblePickupEffect(healthToRestore: $healthToRestore, ammoToAdd: $ammoToAdd, scoreToAdd: $scoreToAdd, extraLivesToAdd: $extraLivesToAdd, pickupSfxId: $pickupSfxId, grantGoldKey: $grantGoldKey, grantSilverKey: $grantSilverKey, grantWeapon: $grantWeapon, requestWeaponSwitch: $requestWeaponSwitch)'; return 'CollectiblePickupEffect(healthToRestore: $healthToRestore, ammoToAdd: $ammoToAdd, scoreToAdd: $scoreToAdd, extraLivesToAdd: $extraLivesToAdd, pickupSoundEffect: $pickupSoundEffect, grantGoldKey: $grantGoldKey, grantSilverKey: $grantSilverKey, grantWeapon: $grantWeapon, requestWeaponSwitch: $requestWeaponSwitch)';
} }
} }
@@ -123,7 +123,9 @@ class HealthCollectible extends Collectible {
final bool isFood = mapId == MapObject.food; final bool isFood = mapId == MapObject.food;
return CollectiblePickupEffect( return CollectiblePickupEffect(
healthToRestore: isFood ? 10 : 25, healthToRestore: isFood ? 10 : 25,
pickupSfxId: isFood ? WolfSound.healthSmall : WolfSound.healthLarge, pickupSoundEffect: isFood
? SoundEffect.healthSmall
: SoundEffect.healthLarge,
); );
} }
} }
@@ -149,7 +151,7 @@ class AmmoCollectible extends Collectible {
return CollectiblePickupEffect( return CollectiblePickupEffect(
ammoToAdd: ammoAmount, ammoToAdd: ammoAmount,
pickupSfxId: WolfSound.getAmmo, pickupSoundEffect: SoundEffect.getAmmo,
requestWeaponSwitch: shouldAutoswitchToPistol ? WeaponType.pistol : null, requestWeaponSwitch: shouldAutoswitchToPistol ? WeaponType.pistol : null,
); );
} }
@@ -172,7 +174,7 @@ class WeaponCollectible extends Collectible {
if (mapId == MapObject.machineGun) { if (mapId == MapObject.machineGun) {
return const CollectiblePickupEffect( return const CollectiblePickupEffect(
ammoToAdd: 6, ammoToAdd: 6,
pickupSfxId: WolfSound.getMachineGun, pickupSoundEffect: SoundEffect.getMachineGun,
grantWeapon: WeaponType.machineGun, grantWeapon: WeaponType.machineGun,
requestWeaponSwitch: WeaponType.machineGun, requestWeaponSwitch: WeaponType.machineGun,
); );
@@ -181,7 +183,7 @@ class WeaponCollectible extends Collectible {
if (mapId == MapObject.chainGun) { if (mapId == MapObject.chainGun) {
return const CollectiblePickupEffect( return const CollectiblePickupEffect(
ammoToAdd: 6, ammoToAdd: 6,
pickupSfxId: WolfSound.getGatling, pickupSoundEffect: SoundEffect.getChainGun,
grantWeapon: WeaponType.chainGun, grantWeapon: WeaponType.chainGun,
requestWeaponSwitch: WeaponType.chainGun, requestWeaponSwitch: WeaponType.chainGun,
); );
@@ -203,7 +205,7 @@ class KeyCollectible extends Collectible {
if (mapId == MapObject.goldKey) { if (mapId == MapObject.goldKey) {
if (context.hasGoldKey) return null; if (context.hasGoldKey) return null;
return const CollectiblePickupEffect( return const CollectiblePickupEffect(
pickupSfxId: WolfSound.getAmmo, pickupSoundEffect: SoundEffect.getAmmo,
grantGoldKey: true, grantGoldKey: true,
); );
} }
@@ -211,7 +213,7 @@ class KeyCollectible extends Collectible {
if (mapId == MapObject.silverKey) { if (mapId == MapObject.silverKey) {
if (context.hasSilverKey) return null; if (context.hasSilverKey) return null;
return const CollectiblePickupEffect( return const CollectiblePickupEffect(
pickupSfxId: WolfSound.getAmmo, pickupSoundEffect: SoundEffect.getAmmo,
grantSilverKey: true, grantSilverKey: true,
); );
} }
@@ -243,18 +245,18 @@ class TreasureCollectible extends Collectible {
healthToRestore: 99, healthToRestore: 99,
ammoToAdd: 25, ammoToAdd: 25,
extraLivesToAdd: 1, extraLivesToAdd: 1,
pickupSfxId: WolfSound.extraLife, pickupSoundEffect: SoundEffect.extraLife,
); );
} }
return CollectiblePickupEffect( return CollectiblePickupEffect(
scoreToAdd: scoreValue, scoreToAdd: scoreValue,
pickupSfxId: switch (mapId) { pickupSoundEffect: switch (mapId) {
MapObject.cross => WolfSound.treasure1, MapObject.cross => SoundEffect.treasure1,
MapObject.chalice => WolfSound.treasure2, MapObject.chalice => SoundEffect.treasure2,
MapObject.chest => WolfSound.treasure3, MapObject.chest => SoundEffect.treasure3,
MapObject.crown => WolfSound.treasure4, MapObject.crown => SoundEffect.treasure4,
_ => WolfSound.getAmmo, _ => SoundEffect.getAmmo,
}, },
); );
} }

View File

@@ -12,13 +12,13 @@ class HansGrosse extends Enemy {
throw UnimplementedError("Hans Grosse uses manual animation logic."); throw UnimplementedError("Hans Grosse uses manual animation logic.");
@override @override
int get alertSoundId => WolfSound.bossActive; SoundEffect get alertSound => SoundEffect.bossActive;
@override @override
int get attackSoundId => WolfSound.naziFire; SoundEffect get attackSound => SoundEffect.enemyFire;
@override @override
int get deathSoundId => WolfSound.mutti; SoundEffect get deathSound => SoundEffect.hansGrosseDeath;
@override @override
int get scoreValue => 5000; int get scoreValue => 5000;
@@ -85,7 +85,7 @@ class HansGrosse extends Enemy {
required bool Function(int area) isAreaConnectedToPlayer, required bool Function(int area) isAreaConnectedToPlayer,
required void Function(int damage) onDamagePlayer, required void Function(int damage) onDamagePlayer,
required void Function(int x, int y) tryOpenDoor, required void Function(int x, int y) tryOpenDoor,
required void Function(int sfxId) onPlaySound, required void Function(SoundEffect effect) onPlaySound,
}) { }) {
Coordinate2D movement = const Coordinate2D(0, 0); Coordinate2D movement = const Coordinate2D(0, 0);
double newAngle = angle; double newAngle = angle;
@@ -102,7 +102,7 @@ class HansGrosse extends Enemy {
isAreaConnectedToPlayer: isAreaConnectedToPlayer, isAreaConnectedToPlayer: isAreaConnectedToPlayer,
baseReactionMs: 50, baseReactionMs: 50,
)) { )) {
onPlaySound(alertSoundId); onPlaySound(alertSound);
} }
double distance = position.distanceTo(playerPosition); double distance = position.distanceTo(playerPosition);
@@ -151,7 +151,7 @@ class HansGrosse extends Enemy {
setTics(10); setTics(10);
} else if (currentFrame == 2) { } else if (currentFrame == 2) {
spriteIndex = _baseSprite + 6; // Fire spriteIndex = _baseSprite + 6; // Fire
onPlaySound(attackSoundId); onPlaySound(attackSound);
onDamagePlayer(damage); onDamagePlayer(damage);
setTics(4); setTics(4);
} else if (currentFrame == 3) { } else if (currentFrame == 3) {

View File

@@ -45,7 +45,7 @@ class Dog extends Enemy {
required bool Function(int area) isAreaConnectedToPlayer, required bool Function(int area) isAreaConnectedToPlayer,
required void Function(int damage) onDamagePlayer, required void Function(int damage) onDamagePlayer,
required void Function(int x, int y) tryOpenDoor, required void Function(int x, int y) tryOpenDoor,
required void Function(int sfxId) onPlaySound, required void Function(SoundEffect effect) onPlaySound,
}) { }) {
final previousState = state; final previousState = state;
Coordinate2D movement = const Coordinate2D(0, 0); Coordinate2D movement = const Coordinate2D(0, 0);
@@ -60,7 +60,7 @@ class Dog extends Enemy {
areaAt: areaAt, areaAt: areaAt,
isAreaConnectedToPlayer: isAreaConnectedToPlayer, isAreaConnectedToPlayer: isAreaConnectedToPlayer,
)) { )) {
onPlaySound(alertSoundId); onPlaySound(alertSound);
} }
} }
@@ -72,7 +72,7 @@ class Dog extends Enemy {
currentFrame++; currentFrame++;
// Phase 2: The actual bite // Phase 2: The actual bite
if (currentFrame == 1) { if (currentFrame == 1) {
onPlaySound(attackSoundId); onPlaySound(attackSound);
final bool attackSuccessful = final bool attackSuccessful =
math.Random().nextDouble() < (180 / 256); math.Random().nextDouble() < (180 / 256);
@@ -176,7 +176,9 @@ class Dog extends Enemy {
// A movement magnitude threshold prevents accepting near-zero floating- // A movement magnitude threshold prevents accepting near-zero floating-
// point residuals (e.g. cos(π/2) ≈ 6e-17) as valid movement. // point residuals (e.g. cos(π/2) ≈ 6e-17) as valid movement.
final double minEffective = currentMoveSpeed * 0.5; final double minEffective = currentMoveSpeed * 0.5;
final double currentDistanceToPlayer = position.distanceTo(playerPosition); final double currentDistanceToPlayer = position.distanceTo(
playerPosition,
);
int selectedCandidateIndex = -1; int selectedCandidateIndex = -1;
for (int i = 0; i < candidateAngles.length; i++) { for (int i = 0; i < candidateAngles.length; i++) {
@@ -192,7 +194,8 @@ class Dog extends Enemy {
tryOpenDoor: (x, y) {}, tryOpenDoor: (x, y) {},
); );
if (candidateMovement.x.abs() + candidateMovement.y.abs() < minEffective) { if (candidateMovement.x.abs() + candidateMovement.y.abs() <
minEffective) {
continue; continue;
} }

View File

@@ -55,16 +55,16 @@ abstract class Enemy extends Entity {
EnemyType get type; EnemyType get type;
/// The sound played when this enemy notices the player or hears combat. /// The sound played when this enemy notices the player or hears combat.
int get alertSoundId => type.alertSoundId; SoundEffect get alertSound => type.alertSound;
/// The sound played when this enemy performs its attack animation. /// The sound played when this enemy performs its attack animation.
int get attackSoundId => type.attackSoundId; SoundEffect get attackSound => type.attackSound;
/// The score awarded when this enemy is killed. /// The score awarded when this enemy is killed.
int get scoreValue => type.scoreValue; int get scoreValue => type.scoreValue;
/// The sound played once when this enemy starts dying. /// The sound played once when this enemy starts dying.
int get deathSoundId => type.deathSoundId; SoundEffect get deathSound => type.deathSound;
/// Ensures enemies drop only one item (like ammo or a key) upon death. /// Ensures enemies drop only one item (like ammo or a key) upon death.
bool hasDroppedItem = false; bool hasDroppedItem = false;
@@ -412,11 +412,19 @@ abstract class Enemy extends Entity {
// is moving mostly north and clips a wall tile beside an open door, the // is moving mostly north and clips a wall tile beside an open door, the
// Y (northward) slide is preferred over the X (sideways) slide. // Y (northward) slide is preferred over the X (sideways) slide.
if (intendedMovement.y.abs() >= intendedMovement.x.abs()) { if (intendedMovement.y.abs() >= intendedMovement.x.abs()) {
if (canMoveY) return normalizeTiny(Coordinate2D(0, intendedMovement.y)); if (canMoveY) {
if (canMoveX) return normalizeTiny(Coordinate2D(intendedMovement.x, 0)); return normalizeTiny(Coordinate2D(0, intendedMovement.y));
}
if (canMoveX) {
return normalizeTiny(Coordinate2D(intendedMovement.x, 0));
}
} else { } else {
if (canMoveX) return normalizeTiny(Coordinate2D(intendedMovement.x, 0)); if (canMoveX) {
if (canMoveY) return normalizeTiny(Coordinate2D(0, intendedMovement.y)); return normalizeTiny(Coordinate2D(intendedMovement.x, 0));
}
if (canMoveY) {
return normalizeTiny(Coordinate2D(0, intendedMovement.y));
}
} }
return const Coordinate2D(0, 0); return const Coordinate2D(0, 0);
} }
@@ -534,7 +542,7 @@ abstract class Enemy extends Entity {
required bool Function(int area) isAreaConnectedToPlayer, required bool Function(int area) isAreaConnectedToPlayer,
required void Function(int x, int y) tryOpenDoor, required void Function(int x, int y) tryOpenDoor,
required void Function(int damage) onDamagePlayer, required void Function(int damage) onDamagePlayer,
required void Function(int sfxId) onPlaySound, required void Function(SoundEffect effect) onPlaySound,
}); });
/// Factory method to spawn the correct [Enemy] subclass based on a Map ID. /// Factory method to spawn the correct [Enemy] subclass based on a Map ID.

View File

@@ -11,9 +11,9 @@ enum EnemyType {
guard( guard(
mapData: EnemyMapData(MapObject.guardStart), mapData: EnemyMapData(MapObject.guardStart),
scoreValue: 100, scoreValue: 100,
alertSoundId: WolfSound.guardHalt, alertSound: SoundEffect.guardHalt,
attackSoundId: WolfSound.naziFire, attackSound: SoundEffect.enemyFire,
deathSoundId: WolfSound.deathScream1, deathSound: SoundEffect.deathScream1,
animations: EnemyAnimationMap( animations: EnemyAnimationMap(
idle: SpriteFrameRange(50, 57), idle: SpriteFrameRange(50, 57),
walking: SpriteFrameRange(58, 89), walking: SpriteFrameRange(58, 89),
@@ -28,9 +28,9 @@ enum EnemyType {
dog( dog(
mapData: EnemyMapData(MapObject.dogStart), mapData: EnemyMapData(MapObject.dogStart),
scoreValue: 200, scoreValue: 200,
alertSoundId: WolfSound.dogBark, alertSound: SoundEffect.dogBark,
attackSoundId: WolfSound.dogAttack, attackSound: SoundEffect.dogAttack,
deathSoundId: WolfSound.dogDeath, deathSound: SoundEffect.dogDeath,
animations: EnemyAnimationMap( animations: EnemyAnimationMap(
// Dogs don't have true idle sprites, so map idle to the first walk frame safely // Dogs don't have true idle sprites, so map idle to the first walk frame safely
idle: SpriteFrameRange(99, 106), idle: SpriteFrameRange(99, 106),
@@ -51,9 +51,9 @@ enum EnemyType {
ss( ss(
mapData: EnemyMapData(MapObject.ssStart), mapData: EnemyMapData(MapObject.ssStart),
scoreValue: 500, scoreValue: 500,
alertSoundId: WolfSound.ssSchutzstaffel, alertSound: SoundEffect.ssAlert,
attackSoundId: WolfSound.naziFire, attackSound: SoundEffect.enemyFire,
deathSoundId: WolfSound.ssMeinGott, deathSound: SoundEffect.ssDeath,
animations: EnemyAnimationMap( animations: EnemyAnimationMap(
idle: SpriteFrameRange(138, 145), idle: SpriteFrameRange(138, 145),
walking: SpriteFrameRange(146, 177), walking: SpriteFrameRange(146, 177),
@@ -68,9 +68,9 @@ enum EnemyType {
mutant( mutant(
mapData: EnemyMapData(MapObject.mutantStart, tierOffset: 18), mapData: EnemyMapData(MapObject.mutantStart, tierOffset: 18),
scoreValue: 700, scoreValue: 700,
alertSoundId: WolfSound.guardHalt, alertSound: SoundEffect.guardHalt,
attackSoundId: WolfSound.naziFire, attackSound: SoundEffect.enemyFire,
deathSoundId: WolfSound.deathScream2, deathSound: SoundEffect.deathScream2,
animations: EnemyAnimationMap( animations: EnemyAnimationMap(
idle: SpriteFrameRange(187, 194), idle: SpriteFrameRange(187, 194),
walking: SpriteFrameRange(195, 226), walking: SpriteFrameRange(195, 226),
@@ -86,9 +86,9 @@ enum EnemyType {
officer( officer(
mapData: EnemyMapData(MapObject.officerStart), mapData: EnemyMapData(MapObject.officerStart),
scoreValue: 400, scoreValue: 400,
alertSoundId: WolfSound.guardHalt, alertSound: SoundEffect.guardHalt,
attackSoundId: WolfSound.naziFire, attackSound: SoundEffect.enemyFire,
deathSoundId: WolfSound.deathScream3, deathSound: SoundEffect.deathScream3,
animations: EnemyAnimationMap( animations: EnemyAnimationMap(
idle: SpriteFrameRange(238, 245), idle: SpriteFrameRange(238, 245),
walking: SpriteFrameRange(246, 277), walking: SpriteFrameRange(246, 277),
@@ -111,13 +111,13 @@ enum EnemyType {
final int scoreValue; final int scoreValue;
/// The sound played when this enemy first becomes alerted. /// The sound played when this enemy first becomes alerted.
final int alertSoundId; final SoundEffect alertSound;
/// The sound played when this enemy attacks. /// The sound played when this enemy attacks.
final int attackSoundId; final SoundEffect attackSound;
/// The sound played when this enemy enters its death animation. /// The sound played when this enemy enters its death animation.
final int deathSoundId; final SoundEffect deathSound;
/// If false, this enemy type will be ignored when loading shareware data. /// If false, this enemy type will be ignored when loading shareware data.
final bool existsInShareware; final bool existsInShareware;
@@ -126,9 +126,9 @@ enum EnemyType {
required this.mapData, required this.mapData,
required this.animations, required this.animations,
required this.scoreValue, required this.scoreValue,
required this.alertSoundId, required this.alertSound,
required this.attackSoundId, required this.attackSound,
required this.deathSoundId, required this.deathSound,
this.existsInShareware = true, this.existsInShareware = true,
}); });

View File

@@ -38,7 +38,7 @@ class Guard extends Enemy {
required bool Function(int area) isAreaConnectedToPlayer, required bool Function(int area) isAreaConnectedToPlayer,
required void Function(int damage) onDamagePlayer, required void Function(int damage) onDamagePlayer,
required void Function(int x, int y) tryOpenDoor, required void Function(int x, int y) tryOpenDoor,
required void Function(int sfxId) onPlaySound, required void Function(SoundEffect effect) onPlaySound,
}) { }) {
Coordinate2D movement = const Coordinate2D(0, 0); Coordinate2D movement = const Coordinate2D(0, 0);
double newAngle = angle; double newAngle = angle;
@@ -52,7 +52,7 @@ class Guard extends Enemy {
areaAt: areaAt, areaAt: areaAt,
isAreaConnectedToPlayer: isAreaConnectedToPlayer, isAreaConnectedToPlayer: isAreaConnectedToPlayer,
)) { )) {
onPlaySound(alertSoundId); onPlaySound(alertSound);
} }
// 2. Discrete AI Logic (Decisions happen every 10 tics) // 2. Discrete AI Logic (Decisions happen every 10 tics)
@@ -62,7 +62,7 @@ class Guard extends Enemy {
if (processTics(elapsedDeltaMs, moveSpeed: 0)) { if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
currentFrame++; currentFrame++;
if (currentFrame == 1) { if (currentFrame == 1) {
onPlaySound(attackSoundId); onPlaySound(attackSound);
if (tryRollRangedHit( if (tryRollRangedHit(
distance: distance, distance: distance,
playerPosition: playerPosition, playerPosition: playerPosition,

View File

@@ -36,7 +36,7 @@ class Mutant extends Enemy {
required bool Function(int area) isAreaConnectedToPlayer, required bool Function(int area) isAreaConnectedToPlayer,
required void Function(int damage) onDamagePlayer, required void Function(int damage) onDamagePlayer,
required void Function(int x, int y) tryOpenDoor, required void Function(int x, int y) tryOpenDoor,
required void Function(int sfxId) onPlaySound, required void Function(SoundEffect effect) onPlaySound,
}) { }) {
Coordinate2D movement = const Coordinate2D(0, 0); Coordinate2D movement = const Coordinate2D(0, 0);
double newAngle = angle; double newAngle = angle;
@@ -48,7 +48,7 @@ class Mutant extends Enemy {
areaAt: areaAt, areaAt: areaAt,
isAreaConnectedToPlayer: isAreaConnectedToPlayer, isAreaConnectedToPlayer: isAreaConnectedToPlayer,
)) { )) {
onPlaySound(alertSoundId); onPlaySound(alertSound);
} }
double distance = position.distanceTo(playerPosition); double distance = position.distanceTo(playerPosition);
@@ -122,7 +122,7 @@ class Mutant extends Enemy {
if (processTics(elapsedDeltaMs, moveSpeed: 0)) { if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
currentFrame++; currentFrame++;
if (currentFrame == 1) { if (currentFrame == 1) {
onPlaySound(attackSoundId); onPlaySound(attackSound);
if (tryRollRangedHit( if (tryRollRangedHit(
distance: distance, distance: distance,
playerPosition: playerPosition, playerPosition: playerPosition,

View File

@@ -36,7 +36,7 @@ class Officer extends Enemy {
required bool Function(int area) isAreaConnectedToPlayer, required bool Function(int area) isAreaConnectedToPlayer,
required void Function(int damage) onDamagePlayer, required void Function(int damage) onDamagePlayer,
required void Function(int x, int y) tryOpenDoor, required void Function(int x, int y) tryOpenDoor,
required void Function(int sfxId) onPlaySound, required void Function(SoundEffect effect) onPlaySound,
}) { }) {
Coordinate2D movement = const Coordinate2D(0, 0); Coordinate2D movement = const Coordinate2D(0, 0);
double newAngle = angle; double newAngle = angle;
@@ -48,7 +48,7 @@ class Officer extends Enemy {
areaAt: areaAt, areaAt: areaAt,
isAreaConnectedToPlayer: isAreaConnectedToPlayer, isAreaConnectedToPlayer: isAreaConnectedToPlayer,
)) { )) {
onPlaySound(alertSoundId); onPlaySound(alertSound);
} }
double distance = position.distanceTo(playerPosition); double distance = position.distanceTo(playerPosition);
@@ -122,7 +122,7 @@ class Officer extends Enemy {
if (processTics(elapsedDeltaMs, moveSpeed: 0)) { if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
currentFrame++; currentFrame++;
if (currentFrame == 1) { if (currentFrame == 1) {
onPlaySound(attackSoundId); onPlaySound(attackSound);
if (tryRollRangedHit( if (tryRollRangedHit(
distance: distance, distance: distance,
playerPosition: playerPosition, playerPosition: playerPosition,

View File

@@ -35,7 +35,7 @@ class SS extends Enemy {
required bool Function(int area) isAreaConnectedToPlayer, required bool Function(int area) isAreaConnectedToPlayer,
required void Function(int damage) onDamagePlayer, required void Function(int damage) onDamagePlayer,
required void Function(int x, int y) tryOpenDoor, required void Function(int x, int y) tryOpenDoor,
required void Function(int sfxId) onPlaySound, required void Function(SoundEffect effect) onPlaySound,
}) { }) {
Coordinate2D movement = const Coordinate2D(0, 0); Coordinate2D movement = const Coordinate2D(0, 0);
double newAngle = angle; double newAngle = angle;
@@ -47,7 +47,7 @@ class SS extends Enemy {
areaAt: areaAt, areaAt: areaAt,
isAreaConnectedToPlayer: isAreaConnectedToPlayer, isAreaConnectedToPlayer: isAreaConnectedToPlayer,
)) { )) {
onPlaySound(alertSoundId); onPlaySound(alertSound);
} }
double distance = position.distanceTo(playerPosition); double distance = position.distanceTo(playerPosition);
@@ -121,7 +121,7 @@ class SS extends Enemy {
if (processTics(elapsedDeltaMs, moveSpeed: 0)) { if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
currentFrame++; currentFrame++;
if (currentFrame == 1) { if (currentFrame == 1) {
onPlaySound(attackSoundId); onPlaySound(attackSound);
if (tryRollRangedHit( if (tryRollRangedHit(
distance: distance, distance: distance,
playerPosition: playerPosition, playerPosition: playerPosition,
@@ -140,7 +140,7 @@ class SS extends Enemy {
if (math.Random().nextDouble() > 0.5) { if (math.Random().nextDouble() > 0.5) {
// 50% chance to burst // 50% chance to burst
currentFrame = 1; currentFrame = 1;
onPlaySound(attackSoundId); onPlaySound(attackSound);
if (tryRollRangedHit( if (tryRollRangedHit(
distance: distance, distance: distance,
playerPosition: playerPosition, playerPosition: playerPosition,

View File

@@ -9,7 +9,7 @@ import 'package:wolf_3d_dart/src/registry/modules/sfx_module.dart';
/// Access version-specific asset IDs through the five typed modules: /// Access version-specific asset IDs through the five typed modules:
/// ///
/// ```dart /// ```dart
/// data.registry.sfx.resolve(SfxKey.pistolFire) /// data.registry.sfx.resolve(SoundEffect.pistolFire)
/// data.registry.music.musicForLevel(episode, level) /// data.registry.music.musicForLevel(episode, level)
/// data.registry.entities.resolve(EntityKey.guard) /// data.registry.entities.resolve(EntityKey.guard)
/// data.registry.hud.faceForHealth(player.health) /// data.registry.hud.faceForHealth(player.health)

View File

@@ -0,0 +1,27 @@
import 'package:wolf_3d_dart/src/data_types/game_version.dart';
import 'package:wolf_3d_dart/src/registry/keys/music.dart';
import 'package:wolf_3d_dart/src/registry/modules/music_module.dart';
/// Built-in music router backed directly by [Music] metadata.
class BuiltInMusicModule extends MusicModule {
const BuiltInMusicModule(this.version);
final GameVersion version;
@override
MusicRoute get menuMusic =>
MusicRoute(Music.menuTheme.trackIndexFor(version)!);
@override
MusicRoute musicForLevel(int episodeIndex, int levelIndex) {
return MusicRoute(
Music.trackIndexForLevel(version, episodeIndex, levelIndex)!,
);
}
@override
MusicRoute? resolve(Music key) {
final index = key.trackIndexFor(version);
return index != null ? MusicRoute(index) : null;
}
}

View File

@@ -0,0 +1,13 @@
import 'package:wolf_3d_dart/src/data_types/game_version.dart';
import 'package:wolf_3d_dart/src/registry/keys/sound_effect.dart';
import 'package:wolf_3d_dart/src/registry/modules/sfx_module.dart';
/// Built-in sound-slot resolver backed directly by [SoundEffect] metadata.
class BuiltInSfxModule extends SfxModule {
const BuiltInSfxModule(this.version);
final GameVersion version;
@override
SoundAssetRef? resolve(SoundEffect key) => SoundAssetRef(key.idFor(version));
}

View File

@@ -1,9 +1,10 @@
import 'package:wolf_3d_dart/src/data_types/game_version.dart';
import 'package:wolf_3d_dart/src/registry/asset_registry.dart'; import 'package:wolf_3d_dart/src/registry/asset_registry.dart';
import 'package:wolf_3d_dart/src/registry/built_in/built_in_music_module.dart';
import 'package:wolf_3d_dart/src/registry/built_in/built_in_sfx_module.dart';
import 'package:wolf_3d_dart/src/registry/built_in/retail_entity_module.dart'; import 'package:wolf_3d_dart/src/registry/built_in/retail_entity_module.dart';
import 'package:wolf_3d_dart/src/registry/built_in/retail_hud_module.dart'; import 'package:wolf_3d_dart/src/registry/built_in/retail_hud_module.dart';
import 'package:wolf_3d_dart/src/registry/built_in/retail_menu_module.dart'; import 'package:wolf_3d_dart/src/registry/built_in/retail_menu_module.dart';
import 'package:wolf_3d_dart/src/registry/built_in/retail_music_module.dart';
import 'package:wolf_3d_dart/src/registry/built_in/retail_sfx_module.dart';
/// The canonical [AssetRegistry] for all retail Wolf3D releases. /// The canonical [AssetRegistry] for all retail Wolf3D releases.
/// ///
@@ -13,8 +14,8 @@ import 'package:wolf_3d_dart/src/registry/built_in/retail_sfx_module.dart';
class RetailAssetRegistry extends AssetRegistry { class RetailAssetRegistry extends AssetRegistry {
RetailAssetRegistry() RetailAssetRegistry()
: super( : super(
sfx: const RetailSfxModule(), sfx: const BuiltInSfxModule(GameVersion.retail),
music: const RetailMusicModule(), music: const BuiltInMusicModule(GameVersion.retail),
entities: const RetailEntityModule(), entities: const RetailEntityModule(),
hud: const RetailHudModule(), hud: const RetailHudModule(),
menu: const RetailMenuPicModule(), menu: const RetailMenuPicModule(),

View File

@@ -1,62 +0,0 @@
import 'package:wolf_3d_dart/src/registry/keys/music_key.dart';
import 'package:wolf_3d_dart/src/registry/modules/music_module.dart';
/// Built-in music module for all retail Wolf3D releases (v1.0, v1.1, v1.4).
///
/// Encodes the original id Software level-to-track routing table and sets
/// track index 1 as the menu music, exactly matching the original engine.
class RetailMusicModule extends MusicModule {
const RetailMusicModule();
// Original WL_INTER.C music table — 60 entries (6 episodes × 10 levels).
static const List<int> _levelMap = [
2, 3, 4, 5, 2, 3, 4, 5, 6, 7, // Episode 1
8, 9, 10, 11, 8, 9, 11, 10, 6, 12, // Episode 2
13, 14, 15, 16, 13, 14, 15, 16, 17, 18, // Episode 3
2, 3, 4, 5, 2, 3, 4, 5, 6, 7, // Episode 4
8, 9, 10, 11, 8, 9, 11, 10, 6, 12, // Episode 5
13, 14, 15, 16, 13, 14, 15, 16, 17, 19, // Episode 6
];
// Named MusicKey → track index for keyed lookups.
static final Map<MusicKey, int> _named = {
MusicKey.menuTheme: 1,
MusicKey.level01: 2,
MusicKey.level02: 3,
MusicKey.level03: 4,
MusicKey.level04: 5,
MusicKey.level05: 6,
MusicKey.level06: 7,
MusicKey.level07: 8,
MusicKey.level08: 9,
MusicKey.level09: 10,
MusicKey.level10: 11,
MusicKey.level11: 12,
MusicKey.level12: 13,
MusicKey.level13: 14,
MusicKey.level14: 15,
MusicKey.level15: 16,
MusicKey.level16: 17,
MusicKey.level17: 18,
MusicKey.level18: 19,
};
@override
MusicRoute get menuMusic => const MusicRoute(1);
@override
MusicRoute musicForLevel(int episodeIndex, int levelIndex) {
final flat = episodeIndex * 10 + levelIndex;
if (flat >= 0 && flat < _levelMap.length) {
return MusicRoute(_levelMap[flat]);
}
// Wrap for custom episode counts beyond the built-in 6.
return MusicRoute(_levelMap[flat % _levelMap.length]);
}
@override
MusicRoute? resolve(MusicKey key) {
final index = _named[key];
return index != null ? MusicRoute(index) : null;
}
}

View File

@@ -1,67 +0,0 @@
import 'package:wolf_3d_dart/src/data_types/sound.dart';
import 'package:wolf_3d_dart/src/registry/keys/sfx_key.dart';
import 'package:wolf_3d_dart/src/registry/modules/sfx_module.dart';
/// Built-in SFX module for all retail Wolf3D releases (v1.0, v1.1, v1.4).
///
/// Maps every [SfxKey] to the corresponding numeric slot from [WolfSound],
/// preserving the original id Software audio index layout exactly.
class RetailSfxModule extends SfxModule {
const RetailSfxModule();
static final Map<SfxKey, int> _slots = {
// --- Doors & Environment ---
SfxKey.openDoor: WolfSound.openDoor,
SfxKey.closeDoor: WolfSound.closeDoor,
SfxKey.pushWall: WolfSound.pushWall,
// --- Weapons ---
SfxKey.knifeAttack: WolfSound.knifeAttack,
SfxKey.pistolFire: WolfSound.pistolFire,
SfxKey.machineGunFire: WolfSound.machineGunFire,
SfxKey.chainGunFire: WolfSound.gatlingFire,
SfxKey.enemyFire: WolfSound.naziFire,
// --- Pickups ---
SfxKey.getMachineGun: WolfSound.getMachineGun,
SfxKey.getAmmo: WolfSound.getAmmo,
SfxKey.getChainGun: WolfSound.getGatling,
SfxKey.healthSmall: WolfSound.healthSmall,
SfxKey.healthLarge: WolfSound.healthLarge,
SfxKey.treasure1: WolfSound.treasure1,
SfxKey.treasure2: WolfSound.treasure2,
SfxKey.treasure3: WolfSound.treasure3,
SfxKey.treasure4: WolfSound.treasure4,
SfxKey.extraLife: WolfSound.extraLife,
// --- Standard Enemies ---
SfxKey.guardHalt: WolfSound.guardHalt,
SfxKey.dogBark: WolfSound.dogBark,
SfxKey.dogDeath: WolfSound.dogDeath,
SfxKey.dogAttack: WolfSound.dogAttack,
SfxKey.deathScream1: WolfSound.deathScream1,
SfxKey.deathScream2: WolfSound.deathScream2,
SfxKey.deathScream3: WolfSound.deathScream3,
SfxKey.ssAlert: WolfSound.ssSchutzstaffel,
SfxKey.ssDeath: WolfSound.ssMeinGott,
// --- Bosses ---
SfxKey.bossActive: WolfSound.bossActive,
SfxKey.hansGrosseDeath: WolfSound.mutti,
SfxKey.schabbs: WolfSound.schabbsHas,
SfxKey.schabbsDeath: WolfSound.eva,
SfxKey.hitlerGreeting: WolfSound.gutenTag,
SfxKey.hitlerDeath: WolfSound.scheist,
SfxKey.mechaSteps: WolfSound.mechSteps,
SfxKey.ottoAlert: WolfSound.spion,
SfxKey.gretelDeath: WolfSound.neinSoVass,
// --- UI & Progression ---
SfxKey.levelComplete: WolfSound.levelDone,
SfxKey.endBonus1: WolfSound.endBonus1,
SfxKey.endBonus2: WolfSound.endBonus2,
SfxKey.noBonus: WolfSound.noBonus,
SfxKey.percent100: WolfSound.percent100,
};
@override
SoundAssetRef? resolve(SfxKey key) {
final slot = _slots[key];
return slot != null ? SoundAssetRef(slot) : null;
}
}

View File

@@ -1,14 +1,15 @@
import 'package:wolf_3d_dart/src/data_types/game_version.dart';
import 'package:wolf_3d_dart/src/registry/asset_registry.dart'; import 'package:wolf_3d_dart/src/registry/asset_registry.dart';
import 'package:wolf_3d_dart/src/registry/built_in/retail_sfx_module.dart'; import 'package:wolf_3d_dart/src/registry/built_in/built_in_music_module.dart';
import 'package:wolf_3d_dart/src/registry/built_in/built_in_sfx_module.dart';
import 'package:wolf_3d_dart/src/registry/built_in/shareware_entity_module.dart'; import 'package:wolf_3d_dart/src/registry/built_in/shareware_entity_module.dart';
import 'package:wolf_3d_dart/src/registry/built_in/shareware_hud_module.dart'; import 'package:wolf_3d_dart/src/registry/built_in/shareware_hud_module.dart';
import 'package:wolf_3d_dart/src/registry/built_in/shareware_menu_module.dart'; import 'package:wolf_3d_dart/src/registry/built_in/shareware_menu_module.dart';
import 'package:wolf_3d_dart/src/registry/built_in/shareware_music_module.dart';
/// The [AssetRegistry] for the Wolfenstein 3D v1.4 Shareware release. /// The [AssetRegistry] for the Wolfenstein 3D v1.4 Shareware release.
/// ///
/// - SFX slots are identical to retail (same AUDIOT layout). /// - SFX slots are resolved through version-aware [SoundEffect.idFor].
/// - Music routing uses the 10-level shareware table. /// - Music routing uses [Music.levelFor] for the 10-level shareware table.
/// - Entity definitions are limited to the three shareware enemies. /// - Entity definitions are limited to the three shareware enemies.
/// - HUD indices are shareware-aware and offset from retail layout. /// - HUD indices are shareware-aware and offset from retail layout.
/// - Menu picture indices are resolved via runtime heuristic offset; call /// - Menu picture indices are resolved via runtime heuristic offset; call
@@ -17,8 +18,8 @@ import 'package:wolf_3d_dart/src/registry/built_in/shareware_music_module.dart';
class SharewareAssetRegistry extends AssetRegistry { class SharewareAssetRegistry extends AssetRegistry {
SharewareAssetRegistry({bool strictOriginalShareware = false}) SharewareAssetRegistry({bool strictOriginalShareware = false})
: super( : super(
sfx: const RetailSfxModule(), sfx: const BuiltInSfxModule(GameVersion.shareware),
music: const SharewareMusicModule(), music: const BuiltInMusicModule(GameVersion.shareware),
entities: const SharewareEntityModule(), entities: const SharewareEntityModule(),
hud: SharewareHudModule( hud: SharewareHudModule(
useOriginalWl1Map: strictOriginalShareware, useOriginalWl1Map: strictOriginalShareware,

View File

@@ -1,49 +0,0 @@
import 'package:wolf_3d_dart/src/registry/keys/music_key.dart';
import 'package:wolf_3d_dart/src/registry/modules/music_module.dart';
/// Built-in music module for the Wolfenstein 3D v1.4 Shareware release.
///
/// Uses the original id Software shareware level-to-track routing table
/// (10 levels, 1 episode) as opposed to the 60-entry retail table.
class SharewareMusicModule extends MusicModule {
const SharewareMusicModule();
// Original WL_INTER.C shareware music table (Episode 1 only).
static const List<int> _levelMap = [
2,
3,
4,
5,
2,
3,
4,
5,
6,
7,
];
static final Map<MusicKey, int> _named = {
MusicKey.menuTheme: 1,
MusicKey.level01: 2,
MusicKey.level02: 3,
MusicKey.level03: 4,
MusicKey.level04: 5,
MusicKey.level05: 6,
MusicKey.level06: 7,
};
@override
MusicRoute get menuMusic => const MusicRoute(1);
@override
MusicRoute musicForLevel(int episodeIndex, int levelIndex) {
final flat = levelIndex % _levelMap.length;
return MusicRoute(_levelMap[flat]);
}
@override
MusicRoute? resolve(MusicKey key) {
final index = _named[key];
return index != null ? MusicRoute(index) : null;
}
}

View File

@@ -0,0 +1,162 @@
import 'package:wolf_3d_dart/src/data_types/game_version.dart';
/// Canonical music identifiers used by built-in registries.
enum Music {
// --- Menus & UI ---
menuTheme(retailTrackIndex: 1, sharewareTrackIndex: 1),
// --- Gameplay ---
// Generic level-slot keys used when routing by episode+floor index.
level01(retailTrackIndex: 2, sharewareTrackIndex: 2),
level02(retailTrackIndex: 3, sharewareTrackIndex: 3),
level03(retailTrackIndex: 4, sharewareTrackIndex: 4),
level04(retailTrackIndex: 5, sharewareTrackIndex: 5),
level05(retailTrackIndex: 6, sharewareTrackIndex: 6),
level06(retailTrackIndex: 7, sharewareTrackIndex: 7),
level07(retailTrackIndex: 8),
level08(retailTrackIndex: 9),
level09(retailTrackIndex: 10),
level10(retailTrackIndex: 11),
level11(retailTrackIndex: 12),
level12(retailTrackIndex: 13),
level13(retailTrackIndex: 14),
level14(retailTrackIndex: 15),
level15(retailTrackIndex: 16),
level16(retailTrackIndex: 17),
level17(retailTrackIndex: 18),
level18(retailTrackIndex: 19),
level19(),
level20(),
;
const Music({this.retailTrackIndex, this.sharewareTrackIndex});
final int? retailTrackIndex;
final int? sharewareTrackIndex;
int? trackIndexFor(GameVersion version) {
switch (version) {
case GameVersion.shareware:
return sharewareTrackIndex;
case GameVersion.retail:
case GameVersion.spearOfDestiny:
case GameVersion.spearOfDestinyDemo:
return retailTrackIndex;
}
}
static Music levelFor(GameVersion version, int episodeIndex, int levelIndex) {
final route = switch (version) {
GameVersion.shareware => _sharewareLevelRoute,
GameVersion.retail ||
GameVersion.spearOfDestiny ||
GameVersion.spearOfDestinyDemo => _retailLevelRoute,
};
final flatIndex = switch (version) {
GameVersion.shareware => levelIndex,
GameVersion.retail ||
GameVersion.spearOfDestiny ||
GameVersion.spearOfDestinyDemo => episodeIndex * 10 + levelIndex,
};
final normalizedIndex = flatIndex >= 0
? flatIndex % route.length
: (flatIndex % route.length + route.length) % route.length;
return route[normalizedIndex];
}
static int? trackIndexForLevel(
GameVersion version,
int episodeIndex,
int levelIndex,
) {
return levelFor(version, episodeIndex, levelIndex).trackIndexFor(version);
}
static Music? fromTrackIndex(GameVersion version, int trackIndex) {
for (final music in values) {
if (music.trackIndexFor(version) == trackIndex) {
return music;
}
}
return null;
}
static const List<Music> _sharewareLevelRoute = [
level01,
level02,
level03,
level04,
level01,
level02,
level03,
level04,
level05,
level06,
];
static const List<Music> _retailLevelRoute = [
level01,
level02,
level03,
level04,
level01,
level02,
level03,
level04,
level05,
level06,
level07,
level08,
level09,
level10,
level07,
level08,
level10,
level09,
level05,
level11,
level12,
level13,
level14,
level15,
level12,
level13,
level14,
level15,
level16,
level17,
level01,
level02,
level03,
level04,
level01,
level02,
level03,
level04,
level05,
level06,
level07,
level08,
level09,
level10,
level07,
level08,
level10,
level09,
level05,
level11,
level12,
level13,
level14,
level15,
level12,
level13,
level14,
level15,
level16,
level18,
];
}

View File

@@ -1,51 +0,0 @@
/// Extensible typed key for music tracks.
///
/// Built-in Wolf3D music contexts are exposed as named static constants.
/// Custom modules can define new keys with `const MusicKey('myTrack')`
/// without modifying this class.
///
/// Example:
/// ```dart
/// registry.music.resolve(MusicKey.menuTheme)
/// ```
final class MusicKey {
const MusicKey(this._id);
final String _id;
// --- Menus & UI ---
static const menuTheme = MusicKey('menuTheme');
// --- Gameplay ---
// Generic level-slot keys used when routing by episode+floor index.
// The MusicModule maps these to actual IMF track indices.
static const level01 = MusicKey('level01');
static const level02 = MusicKey('level02');
static const level03 = MusicKey('level03');
static const level04 = MusicKey('level04');
static const level05 = MusicKey('level05');
static const level06 = MusicKey('level06');
static const level07 = MusicKey('level07');
static const level08 = MusicKey('level08');
static const level09 = MusicKey('level09');
static const level10 = MusicKey('level10');
static const level11 = MusicKey('level11');
static const level12 = MusicKey('level12');
static const level13 = MusicKey('level13');
static const level14 = MusicKey('level14');
static const level15 = MusicKey('level15');
static const level16 = MusicKey('level16');
static const level17 = MusicKey('level17');
static const level18 = MusicKey('level18');
static const level19 = MusicKey('level19');
static const level20 = MusicKey('level20');
@override
bool operator ==(Object other) => other is MusicKey && other._id == _id;
@override
int get hashCode => _id.hashCode;
@override
String toString() => 'MusicKey($_id)';
}

View File

@@ -1,77 +0,0 @@
/// Extensible typed key for sound effects.
///
/// Built-in Wolf3D sound effects are exposed as named static constants.
/// Custom modules can define new keys with `const SfxKey('myCustomId')`
/// without modifying this class.
///
/// Example:
/// ```dart
/// registry.sfx.resolve(SfxKey.pistolFire)
/// ```
final class SfxKey {
const SfxKey(this._id);
final String _id;
// --- Doors & Environment ---
static const openDoor = SfxKey('openDoor');
static const closeDoor = SfxKey('closeDoor');
static const pushWall = SfxKey('pushWall');
// --- Weapons ---
static const knifeAttack = SfxKey('knifeAttack');
static const pistolFire = SfxKey('pistolFire');
static const machineGunFire = SfxKey('machineGunFire');
static const chainGunFire = SfxKey('chainGunFire');
static const enemyFire = SfxKey('enemyFire');
// --- Pickups ---
static const getMachineGun = SfxKey('getMachineGun');
static const getAmmo = SfxKey('getAmmo');
static const getChainGun = SfxKey('getChainGun');
static const healthSmall = SfxKey('healthSmall');
static const healthLarge = SfxKey('healthLarge');
static const treasure1 = SfxKey('treasure1');
static const treasure2 = SfxKey('treasure2');
static const treasure3 = SfxKey('treasure3');
static const treasure4 = SfxKey('treasure4');
static const extraLife = SfxKey('extraLife');
// --- Standard Enemies ---
static const guardHalt = SfxKey('guardHalt');
static const dogBark = SfxKey('dogBark');
static const dogDeath = SfxKey('dogDeath');
static const dogAttack = SfxKey('dogAttack');
static const deathScream1 = SfxKey('deathScream1');
static const deathScream2 = SfxKey('deathScream2');
static const deathScream3 = SfxKey('deathScream3');
static const ssAlert = SfxKey('ssAlert');
static const ssDeath = SfxKey('ssDeath');
// --- Bosses ---
static const bossActive = SfxKey('bossActive');
static const hansGrosseDeath = SfxKey('hansGrosseDeath');
static const schabbs = SfxKey('schabbs');
static const schabbsDeath = SfxKey('schabbsDeath');
static const hitlerGreeting = SfxKey('hitlerGreeting');
static const hitlerDeath = SfxKey('hitlerDeath');
static const mechaSteps = SfxKey('mechaSteps');
static const ottoAlert = SfxKey('ottoAlert');
static const gretelDeath = SfxKey('gretelDeath');
// --- UI & Progression ---
static const levelComplete = SfxKey('levelComplete');
static const endBonus1 = SfxKey('endBonus1');
static const endBonus2 = SfxKey('endBonus2');
static const noBonus = SfxKey('noBonus');
static const percent100 = SfxKey('percent100');
@override
bool operator ==(Object other) => other is SfxKey && other._id == _id;
@override
int get hashCode => _id.hashCode;
@override
String toString() => 'SfxKey($_id)';
}

View File

@@ -0,0 +1,75 @@
import 'package:wolf_3d_dart/src/data_types/game_version.dart';
/// Canonical sound-effect identifiers used by built-in registries.
enum SoundEffect {
// --- Doors & Environment ---
openDoor(retailId: 8),
closeDoor(retailId: 9),
pushWall(retailId: 46),
// --- Weapons ---
knifeAttack(retailId: 23),
pistolFire(retailId: 24),
machineGunFire(retailId: 26),
chainGunFire(retailId: 32),
enemyFire(retailId: 58),
// --- Pickups ---
getMachineGun(retailId: 30),
getAmmo(retailId: 31),
getChainGun(retailId: 38),
healthSmall(retailId: 33),
healthLarge(retailId: 34),
treasure1(retailId: 35),
treasure2(retailId: 36),
treasure3(retailId: 37),
treasure4(retailId: 45),
extraLife(retailId: 44),
// --- Standard Enemies ---
guardHalt(retailId: 21),
dogBark(retailId: 41),
dogDeath(retailId: 62),
dogAttack(retailId: 68),
deathScream1(retailId: 29),
deathScream2(retailId: 22),
deathScream3(retailId: 25),
ssAlert(retailId: 51),
ssDeath(retailId: 63),
// --- Bosses ---
bossActive(retailId: 49),
hansGrosseDeath(retailId: 50),
schabbs(retailId: 64),
schabbsDeath(retailId: 54),
hitlerGreeting(retailId: 55),
hitlerDeath(retailId: 57),
mechaSteps(retailId: 70),
ottoAlert(retailId: 66),
gretelDeath(retailId: 67),
// --- UI & Progression ---
levelComplete(retailId: 40),
endBonus1(retailId: 42),
endBonus2(retailId: 43),
noBonus(retailId: 47),
percent100(retailId: 48),
;
const SoundEffect({required this.retailId, int? sharewareId})
: sharewareId = sharewareId ?? retailId;
final int retailId;
final int sharewareId;
int idFor(GameVersion version) {
switch (version) {
case GameVersion.shareware:
return sharewareId;
case GameVersion.retail:
case GameVersion.spearOfDestiny:
case GameVersion.spearOfDestinyDemo:
return retailId;
}
}
}

View File

@@ -1,4 +1,4 @@
import 'package:wolf_3d_dart/src/registry/keys/music_key.dart'; import 'package:wolf_3d_dart/src/registry/keys/music.dart';
/// The resolved reference for a music track: a numeric index into /// The resolved reference for a music track: a numeric index into
/// [WolfensteinData.music]. /// [WolfensteinData.music].
@@ -19,10 +19,10 @@ class MusicRoute {
abstract class MusicModule { abstract class MusicModule {
const MusicModule(); const MusicModule();
/// Resolves a named [MusicKey] to a [MusicRoute]. /// Resolves a named [Music] to a [MusicRoute].
/// ///
/// Returns `null` if the key is not supported by this module. /// Returns `null` if the key is not supported by this module.
MusicRoute? resolve(MusicKey key); MusicRoute? resolve(Music key);
/// Resolves the level music track for a given [episodeIndex] and /// Resolves the level music track for a given [episodeIndex] and
/// zero-based [levelIndex]. /// zero-based [levelIndex].

View File

@@ -1,4 +1,4 @@
import 'package:wolf_3d_dart/src/registry/keys/sfx_key.dart'; import 'package:wolf_3d_dart/src/registry/keys/sound_effect.dart';
/// The resolved reference for a sound effect: a numeric slot index into /// The resolved reference for a sound effect: a numeric slot index into
/// [WolfensteinData.sounds]. /// [WolfensteinData.sounds].
@@ -11,7 +11,7 @@ class SoundAssetRef {
String toString() => 'SoundAssetRef($slotIndex)'; String toString() => 'SoundAssetRef($slotIndex)';
} }
/// Owns the mapping from symbolic [SfxKey] identifiers to numeric sound slots. /// Owns the mapping from symbolic [SoundEffect] identifiers to numeric sound slots.
/// ///
/// Implement this class to provide a custom sound layout for a modded or /// Implement this class to provide a custom sound layout for a modded or
/// alternate game version. Pass the implementation inside a custom /// alternate game version. Pass the implementation inside a custom
@@ -22,5 +22,5 @@ abstract class SfxModule {
/// Resolves [key] to a [SoundAssetRef] containing the numeric slot index. /// Resolves [key] to a [SoundAssetRef] containing the numeric slot index.
/// ///
/// Returns `null` if the key is not supported by this module. /// Returns `null` if the key is not supported by this module.
SoundAssetRef? resolve(SfxKey key); SoundAssetRef? resolve(SoundEffect key);
} }

View File

@@ -13,7 +13,7 @@ class WolfAudio implements EngineAudio {
for (int i = 0; i < 50; i++) { for (int i = 0; i < 50; i++) {
Future.delayed(Duration(seconds: i * 2), () { Future.delayed(Duration(seconds: i * 2), () {
log("[AUDIO] Testing Sound ID: $i"); log("[AUDIO] Testing Sound ID: $i");
playSoundEffect(i); playSoundEffectId(i);
}); });
} }
} }
@@ -122,16 +122,21 @@ class WolfAudio implements EngineAudio {
@override @override
Future<void> playMenuMusic() async { Future<void> playMenuMusic() async {
final data = activeGame; final data = activeGame;
if (data == null || data.music.length <= 1) return; final trackIndex = data == null
await playMusic(data.music[1]); ? null
: Music.menuTheme.trackIndexFor(data.version);
if (data == null || trackIndex == null || trackIndex >= data.music.length) {
return;
}
await playMusic(data.music[trackIndex]);
} }
@override @override
Future<void> playLevelMusic(WolfLevel level) async { Future<void> playLevelMusic(Music music) async {
final data = activeGame; final data = activeGame;
if (data == null || data.music.isEmpty) return; if (data == null || data.music.isEmpty) return;
final index = level.musicIndex; final index = music.trackIndexFor(data.version) ?? 0;
if (index < data.music.length) { if (index < data.music.length) {
await playMusic(data.music[index]); await playMusic(data.music[index]);
} else { } else {
@@ -144,14 +149,24 @@ class WolfAudio implements EngineAudio {
// ========================================== // ==========================================
@override @override
Future<void> playSoundEffect(int sfxId) async { Future<void> playSoundEffect(SoundEffect effect) async {
final data = activeGame;
if (data == null) return;
await playSoundEffectId(effect.idFor(data.version));
}
@override
Future<void> playSoundEffectId(int sfxId) async {
log("[AUDIO] Playing sfx id $sfxId"); log("[AUDIO] Playing sfx id $sfxId");
// The original engine uses a specific starting chunk for digitized sounds. // The original engine uses a specific starting chunk for digitized sounds.
// In many loaders, the 'sounds' list is already just the digitized ones. // In many loaders, the 'sounds' list is already just the digitized ones.
// If your list contains EVERYTHING, you need to add the offset (174). // If your list contains EVERYTHING, you need to add the offset (174).
// If it's JUST digitized sounds, sfxId should work directly. // If it's JUST digitized sounds, sfxId should work directly.
final soundsList = activeGame!.sounds; final data = activeGame;
if (data == null) return;
final soundsList = data.sounds;
if (sfxId < 0 || sfxId >= soundsList.length) return; if (sfxId < 0 || sfxId >= soundsList.length) return;
final raw8bitBytes = soundsList[sfxId].bytes; final raw8bitBytes = soundsList[sfxId].bytes;

View File

@@ -19,8 +19,7 @@ export 'src/data_types/game_file.dart' show GameFile;
export 'src/data_types/game_version.dart' show GameVersion; export 'src/data_types/game_version.dart' show GameVersion;
export 'src/data_types/image.dart' show VgaImage; export 'src/data_types/image.dart' show VgaImage;
export 'src/data_types/map_objects.dart' show MapObject; export 'src/data_types/map_objects.dart' show MapObject;
export 'src/data_types/sound.dart' export 'src/data_types/sound.dart' show PcmSound, ImfMusic, ImfInstruction;
show PcmSound, ImfMusic, ImfInstruction, WolfMusicMap, WolfSound;
export 'src/data_types/sprite.dart' hide Matrix; export 'src/data_types/sprite.dart' hide Matrix;
export 'src/data_types/sprite_frame_range.dart' show SpriteFrameRange; export 'src/data_types/sprite_frame_range.dart' show SpriteFrameRange;
export 'src/data_types/wolf_level.dart' show WolfLevel; export 'src/data_types/wolf_level.dart' show WolfLevel;
@@ -34,8 +33,8 @@ export 'src/registry/built_in/shareware_asset_registry.dart'
export 'src/registry/keys/entity_key.dart' show EntityKey; export 'src/registry/keys/entity_key.dart' show EntityKey;
export 'src/registry/keys/hud_key.dart' show HudKey; export 'src/registry/keys/hud_key.dart' show HudKey;
export 'src/registry/keys/menu_pic_key.dart' show MenuPicKey; export 'src/registry/keys/menu_pic_key.dart' show MenuPicKey;
export 'src/registry/keys/music_key.dart' show MusicKey; export 'src/registry/keys/music.dart' show Music;
export 'src/registry/keys/sfx_key.dart' show SfxKey; export 'src/registry/keys/sound_effect.dart' show SoundEffect;
export 'src/registry/modules/entity_asset_module.dart' export 'src/registry/modules/entity_asset_module.dart'
show EntityAssetModule, EntityAssetDefinition; show EntityAssetModule, EntityAssetDefinition;
export 'src/registry/modules/hud_module.dart' show HudModule, HudAssetRef; export 'src/registry/modules/hud_module.dart' show HudModule, HudAssetRef;

View File

@@ -25,7 +25,7 @@ void main() {
); );
engine.tick(const Duration(milliseconds: 16)); engine.tick(const Duration(milliseconds: 16));
expect(audio.sfxIds, contains(WolfSound.getAmmo)); expect(audio.sfxIds, contains(SoundEffect.getAmmo));
}); });
test('plays guard alert when guard notices player', () { test('plays guard alert when guard notices player', () {
@@ -47,7 +47,7 @@ void main() {
engine.tick(const Duration(milliseconds: 16)); engine.tick(const Duration(milliseconds: 16));
} }
expect(audio.sfxIds, contains(WolfSound.guardHalt)); expect(audio.sfxIds, contains(SoundEffect.guardHalt));
}); });
test('plays dog death sound when dog dies', () { test('plays dog death sound when dog dies', () {
@@ -69,7 +69,7 @@ void main() {
engine.tick(const Duration(milliseconds: 16)); engine.tick(const Duration(milliseconds: 16));
expect(audio.sfxIds, contains(WolfSound.dogDeath)); expect(audio.sfxIds, contains(SoundEffect.dogDeath));
}); });
test('plays pushwall sound when triggering a secret wall', () { test('plays pushwall sound when triggering a secret wall', () {
@@ -90,7 +90,7 @@ void main() {
engine.init(); engine.init();
engine.tick(const Duration(milliseconds: 16)); engine.tick(const Duration(milliseconds: 16));
expect(audio.sfxIds, contains(WolfSound.pushWall)); expect(audio.sfxIds, contains(SoundEffect.pushWall));
}); });
}); });
} }
@@ -134,7 +134,7 @@ WolfEngine _buildEngine({
wallGrid: wallGrid, wallGrid: wallGrid,
areaGrid: List.generate(64, (_) => List.filled(64, -1)), areaGrid: List.generate(64, (_) => List.filled(64, -1)),
objectGrid: objectGrid, objectGrid: objectGrid,
musicIndex: 0, music: Music.level01,
), ),
], ],
), ),
@@ -159,7 +159,7 @@ class _CapturingAudio implements EngineAudio {
@override @override
WolfensteinData? activeGame; WolfensteinData? activeGame;
final List<int> sfxIds = []; final List<SoundEffect> sfxIds = [];
@override @override
Future<void> debugSoundTest() async {} Future<void> debugSoundTest() async {}
@@ -168,14 +168,24 @@ class _CapturingAudio implements EngineAudio {
Future<void> init() async {} Future<void> init() async {}
@override @override
void playLevelMusic(WolfLevel level) {} void playLevelMusic(Music music) {}
@override @override
void playMenuMusic() {} void playMenuMusic() {}
@override @override
void playSoundEffect(int sfxId) { void playSoundEffect(SoundEffect effect) {
sfxIds.add(sfxId); sfxIds.add(effect);
}
@override
void playSoundEffectId(int sfxId) {
final effect = SoundEffect.values
.where((entry) => entry.retailId == sfxId)
.firstOrNull;
if (effect != null) {
sfxIds.add(effect);
}
} }
@override @override

View File

@@ -88,7 +88,7 @@ WolfEngine _buildEngine() {
wallGrid: wallGrid, wallGrid: wallGrid,
areaGrid: List.generate(64, (_) => List.filled(64, -1)), areaGrid: List.generate(64, (_) => List.filled(64, -1)),
objectGrid: objectGrid, objectGrid: objectGrid,
musicIndex: 0, music: Music.level01,
), ),
], ],
), ),
@@ -119,13 +119,16 @@ class _SilentAudio implements EngineAudio {
Future<void> init() async {} Future<void> init() async {}
@override @override
void playLevelMusic(WolfLevel level) {} void playLevelMusic(Music music) {}
@override @override
void playMenuMusic() {} void playMenuMusic() {}
@override @override
void playSoundEffect(int sfxId) {} void playSoundEffect(SoundEffect effect) {}
@override
void playSoundEffectId(int sfxId) {}
@override @override
void stopMusic() {} void stopMusic() {}

View File

@@ -365,14 +365,14 @@ WolfensteinData _buildTestData({required GameVersion gameVersion}) {
wallGrid: levelOneWalls, wallGrid: levelOneWalls,
areaGrid: List.generate(64, (_) => List.filled(64, -1)), areaGrid: List.generate(64, (_) => List.filled(64, -1)),
objectGrid: levelOneObjects, objectGrid: levelOneObjects,
musicIndex: 0, music: Music.level01,
), ),
WolfLevel( WolfLevel(
name: 'Level 2', name: 'Level 2',
wallGrid: levelTwoWalls, wallGrid: levelTwoWalls,
areaGrid: List.generate(64, (_) => List.filled(64, -1)), areaGrid: List.generate(64, (_) => List.filled(64, -1)),
objectGrid: levelTwoObjects, objectGrid: levelTwoObjects,
musicIndex: 1, music: Music.level02,
), ),
], ],
), ),
@@ -396,13 +396,16 @@ class _SilentAudio implements EngineAudio {
Future<void> init() async {} Future<void> init() async {}
@override @override
void playLevelMusic(WolfLevel level) {} void playLevelMusic(Music music) {}
@override @override
void playMenuMusic() {} void playMenuMusic() {}
@override @override
void playSoundEffect(int sfxId) {} void playSoundEffect(SoundEffect effect) {}
@override
void playSoundEffectId(int sfxId) {}
@override @override
void stopMusic() {} void stopMusic() {}

View File

@@ -45,7 +45,7 @@ void main() {
wallGrid: wallGrid, wallGrid: wallGrid,
areaGrid: List.generate(64, (_) => List.filled(64, -1)), areaGrid: List.generate(64, (_) => List.filled(64, -1)),
objectGrid: objectGrid, objectGrid: objectGrid,
musicIndex: 0, music: Music.level01,
), ),
], ],
), ),

View File

@@ -45,7 +45,7 @@ void main() {
wallGrid: wallGrid, wallGrid: wallGrid,
areaGrid: List.generate(64, (_) => List.filled(64, -1)), areaGrid: List.generate(64, (_) => List.filled(64, -1)),
objectGrid: objectGrid, objectGrid: objectGrid,
musicIndex: 0, music: Music.level01,
), ),
], ],
), ),

View File

@@ -8,8 +8,8 @@ class FlutterAudioAdapter implements EngineAudio {
FlutterAudioAdapter(this.wolf3d); FlutterAudioAdapter(this.wolf3d);
@override @override
void playLevelMusic(WolfLevel level) { void playLevelMusic(Music music) {
wolf3d.audio.playLevelMusic(level); wolf3d.audio.playLevelMusic(music);
} }
@override @override
@@ -23,8 +23,13 @@ class FlutterAudioAdapter implements EngineAudio {
} }
@override @override
void playSoundEffect(int sfxId) { void playSoundEffect(SoundEffect effect) {
wolf3d.audio.playSoundEffect(sfxId); wolf3d.audio.playSoundEffect(effect);
}
@override
void playSoundEffectId(int sfxId) {
wolf3d.audio.playSoundEffectId(sfxId);
} }
@override @override