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

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

View File

@@ -57,64 +57,3 @@ class ImfMusic {
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.
final SpriteMap areaGrid;
/// The index of the [ImfMusic] track to play while this level is active.
final int musicIndex;
/// The [Music] track to play while this level is active.
final Music music;
const WolfLevel({
required this.name,
required this.wallGrid,
required this.objectGrid,
required this.areaGrid,
required this.musicIndex,
required this.music,
});
}

View File

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

View File

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

View File

@@ -14,10 +14,7 @@ class CliSilentAudio implements EngineAudio {
void playMenuMusic() {}
@override
void playLevelMusic(WolfLevel level) {
// Optional: Print a log so you know it's working!
// debugPrint("🎵 Playing music for: ${level.name} 🎵");
}
void playLevelMusic(Music music) {}
@override
void stopMusic() {}
@@ -26,7 +23,14 @@ class CliSilentAudio implements EngineAudio {
Future<void> stopAllAudio() async {}
@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
// to actually make a system beep when a sound plays!
// stdout.write('\x07');

View File

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

View File

@@ -23,7 +23,7 @@ class PushwallManager {
PushwallManager({this.onPlaySound});
/// 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 = {};
Pushwall? activePushwall;
@@ -127,7 +127,7 @@ class PushwallManager {
int checkY = targetY + pw.dirY;
if (wallGrid[checkY][checkX] == 0) {
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.
///
/// Returns `null` when the item was not collected (for example: full health).
int? tryPickup(Collectible item) {
SoundEffect? tryPickup(Collectible item) {
final effect = item.tryCollect(
CollectiblePickupContext(
health: health,
@@ -211,7 +211,7 @@ class Player {
requestWeaponSwitch(weaponType);
}
return effect.pickupSfxId;
return effect.pickupSoundEffect;
}
bool fire(int currentTime) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -55,16 +55,16 @@ abstract class Enemy extends Entity {
EnemyType get type;
/// 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.
int get attackSoundId => type.attackSoundId;
SoundEffect get attackSound => type.attackSound;
/// The score awarded when this enemy is killed.
int get scoreValue => type.scoreValue;
/// 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.
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
// Y (northward) slide is preferred over the X (sideways) slide.
if (intendedMovement.y.abs() >= intendedMovement.x.abs()) {
if (canMoveY) return normalizeTiny(Coordinate2D(0, intendedMovement.y));
if (canMoveX) return normalizeTiny(Coordinate2D(intendedMovement.x, 0));
if (canMoveY) {
return normalizeTiny(Coordinate2D(0, intendedMovement.y));
}
if (canMoveX) {
return normalizeTiny(Coordinate2D(intendedMovement.x, 0));
}
} else {
if (canMoveX) return normalizeTiny(Coordinate2D(intendedMovement.x, 0));
if (canMoveY) return normalizeTiny(Coordinate2D(0, intendedMovement.y));
if (canMoveX) {
return normalizeTiny(Coordinate2D(intendedMovement.x, 0));
}
if (canMoveY) {
return normalizeTiny(Coordinate2D(0, intendedMovement.y));
}
}
return const Coordinate2D(0, 0);
}
@@ -534,7 +542,7 @@ abstract class Enemy extends Entity {
required bool Function(int area) isAreaConnectedToPlayer,
required void Function(int x, int y) tryOpenDoor,
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.

View File

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

View File

@@ -38,7 +38,7 @@ class Guard extends Enemy {
required bool Function(int area) isAreaConnectedToPlayer,
required void Function(int damage) onDamagePlayer,
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);
double newAngle = angle;
@@ -52,7 +52,7 @@ class Guard extends Enemy {
areaAt: areaAt,
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
)) {
onPlaySound(alertSoundId);
onPlaySound(alertSound);
}
// 2. Discrete AI Logic (Decisions happen every 10 tics)
@@ -62,7 +62,7 @@ class Guard extends Enemy {
if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
currentFrame++;
if (currentFrame == 1) {
onPlaySound(attackSoundId);
onPlaySound(attackSound);
if (tryRollRangedHit(
distance: distance,
playerPosition: playerPosition,

View File

@@ -36,7 +36,7 @@ class Mutant extends Enemy {
required bool Function(int area) isAreaConnectedToPlayer,
required void Function(int damage) onDamagePlayer,
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);
double newAngle = angle;
@@ -48,7 +48,7 @@ class Mutant extends Enemy {
areaAt: areaAt,
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
)) {
onPlaySound(alertSoundId);
onPlaySound(alertSound);
}
double distance = position.distanceTo(playerPosition);
@@ -122,7 +122,7 @@ class Mutant extends Enemy {
if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
currentFrame++;
if (currentFrame == 1) {
onPlaySound(attackSoundId);
onPlaySound(attackSound);
if (tryRollRangedHit(
distance: distance,
playerPosition: playerPosition,

View File

@@ -36,7 +36,7 @@ class Officer extends Enemy {
required bool Function(int area) isAreaConnectedToPlayer,
required void Function(int damage) onDamagePlayer,
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);
double newAngle = angle;
@@ -48,7 +48,7 @@ class Officer extends Enemy {
areaAt: areaAt,
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
)) {
onPlaySound(alertSoundId);
onPlaySound(alertSound);
}
double distance = position.distanceTo(playerPosition);
@@ -122,7 +122,7 @@ class Officer extends Enemy {
if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
currentFrame++;
if (currentFrame == 1) {
onPlaySound(attackSoundId);
onPlaySound(attackSound);
if (tryRollRangedHit(
distance: distance,
playerPosition: playerPosition,

View File

@@ -35,7 +35,7 @@ class SS extends Enemy {
required bool Function(int area) isAreaConnectedToPlayer,
required void Function(int damage) onDamagePlayer,
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);
double newAngle = angle;
@@ -47,7 +47,7 @@ class SS extends Enemy {
areaAt: areaAt,
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
)) {
onPlaySound(alertSoundId);
onPlaySound(alertSound);
}
double distance = position.distanceTo(playerPosition);
@@ -121,7 +121,7 @@ class SS extends Enemy {
if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
currentFrame++;
if (currentFrame == 1) {
onPlaySound(attackSoundId);
onPlaySound(attackSound);
if (tryRollRangedHit(
distance: distance,
playerPosition: playerPosition,
@@ -140,7 +140,7 @@ class SS extends Enemy {
if (math.Random().nextDouble() > 0.5) {
// 50% chance to burst
currentFrame = 1;
onPlaySound(attackSoundId);
onPlaySound(attackSound);
if (tryRollRangedHit(
distance: distance,
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:
///
/// ```dart
/// data.registry.sfx.resolve(SfxKey.pistolFire)
/// data.registry.sfx.resolve(SoundEffect.pistolFire)
/// data.registry.music.musicForLevel(episode, level)
/// data.registry.entities.resolve(EntityKey.guard)
/// 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,20 +1,21 @@
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/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_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_music_module.dart';
import 'package:wolf_3d_dart/src/registry/built_in/retail_sfx_module.dart';
/// The canonical [AssetRegistry] for all retail Wolf3D releases.
///
/// Composes the five retail built-in modules and preserves the original
/// id Software asset layout exactly. All other registries and tests that
/// id Software asset layout exactly. All other registries and tests that
/// need a stable reference should start from this factory.
class RetailAssetRegistry extends AssetRegistry {
RetailAssetRegistry()
: super(
sfx: const RetailSfxModule(),
music: const RetailMusicModule(),
sfx: const BuiltInSfxModule(GameVersion.retail),
music: const BuiltInMusicModule(GameVersion.retail),
entities: const RetailEntityModule(),
hud: const RetailHudModule(),
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/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_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_music_module.dart';
/// The [AssetRegistry] for the Wolfenstein 3D v1.4 Shareware release.
///
/// - SFX slots are identical to retail (same AUDIOT layout).
/// - Music routing uses the 10-level shareware table.
/// - SFX slots are resolved through version-aware [SoundEffect.idFor].
/// - Music routing uses [Music.levelFor] for the 10-level shareware table.
/// - Entity definitions are limited to the three shareware enemies.
/// - HUD indices are shareware-aware and offset from retail layout.
/// - 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 {
SharewareAssetRegistry({bool strictOriginalShareware = false})
: super(
sfx: const RetailSfxModule(),
music: const SharewareMusicModule(),
sfx: const BuiltInSfxModule(GameVersion.shareware),
music: const BuiltInMusicModule(GameVersion.shareware),
entities: const SharewareEntityModule(),
hud: SharewareHudModule(
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
/// [WolfensteinData.music].
@@ -19,10 +19,10 @@ class MusicRoute {
abstract class 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.
MusicRoute? resolve(MusicKey key);
MusicRoute? resolve(Music key);
/// Resolves the level music track for a given [episodeIndex] and
/// 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
/// [WolfensteinData.sounds].
@@ -11,7 +11,7 @@ class SoundAssetRef {
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
/// 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.
///
/// 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++) {
Future.delayed(Duration(seconds: i * 2), () {
log("[AUDIO] Testing Sound ID: $i");
playSoundEffect(i);
playSoundEffectId(i);
});
}
}
@@ -122,16 +122,21 @@ class WolfAudio implements EngineAudio {
@override
Future<void> playMenuMusic() async {
final data = activeGame;
if (data == null || data.music.length <= 1) return;
await playMusic(data.music[1]);
final trackIndex = data == null
? null
: Music.menuTheme.trackIndexFor(data.version);
if (data == null || trackIndex == null || trackIndex >= data.music.length) {
return;
}
await playMusic(data.music[trackIndex]);
}
@override
Future<void> playLevelMusic(WolfLevel level) async {
Future<void> playLevelMusic(Music music) async {
final data = activeGame;
if (data == null || data.music.isEmpty) return;
final index = level.musicIndex;
final index = music.trackIndexFor(data.version) ?? 0;
if (index < data.music.length) {
await playMusic(data.music[index]);
} else {
@@ -144,14 +149,24 @@ class WolfAudio implements EngineAudio {
// ==========================================
@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");
// The original engine uses a specific starting chunk for digitized sounds.
// 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 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;
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/image.dart' show VgaImage;
export 'src/data_types/map_objects.dart' show MapObject;
export 'src/data_types/sound.dart'
show PcmSound, ImfMusic, ImfInstruction, WolfMusicMap, WolfSound;
export 'src/data_types/sound.dart' show PcmSound, ImfMusic, ImfInstruction;
export 'src/data_types/sprite.dart' hide Matrix;
export 'src/data_types/sprite_frame_range.dart' show SpriteFrameRange;
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/hud_key.dart' show HudKey;
export 'src/registry/keys/menu_pic_key.dart' show MenuPicKey;
export 'src/registry/keys/music_key.dart' show MusicKey;
export 'src/registry/keys/sfx_key.dart' show SfxKey;
export 'src/registry/keys/music.dart' show Music;
export 'src/registry/keys/sound_effect.dart' show SoundEffect;
export 'src/registry/modules/entity_asset_module.dart'
show EntityAssetModule, EntityAssetDefinition;
export 'src/registry/modules/hud_module.dart' show HudModule, HudAssetRef;

View File

@@ -25,7 +25,7 @@ void main() {
);
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', () {
@@ -47,7 +47,7 @@ void main() {
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', () {
@@ -69,7 +69,7 @@ void main() {
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', () {
@@ -90,7 +90,7 @@ void main() {
engine.init();
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,
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
objectGrid: objectGrid,
musicIndex: 0,
music: Music.level01,
),
],
),
@@ -159,7 +159,7 @@ class _CapturingAudio implements EngineAudio {
@override
WolfensteinData? activeGame;
final List<int> sfxIds = [];
final List<SoundEffect> sfxIds = [];
@override
Future<void> debugSoundTest() async {}
@@ -168,14 +168,24 @@ class _CapturingAudio implements EngineAudio {
Future<void> init() async {}
@override
void playLevelMusic(WolfLevel level) {}
void playLevelMusic(Music music) {}
@override
void playMenuMusic() {}
@override
void playSoundEffect(int sfxId) {
sfxIds.add(sfxId);
void playSoundEffect(SoundEffect effect) {
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

View File

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

View File

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

View File

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

View File

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