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:
@@ -46,74 +46,6 @@ class _AudioGalleryState extends State<AudioGallery> {
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -130,14 +62,14 @@ class _AudioGalleryState extends State<AudioGallery> {
|
||||
|
||||
Map<int, List<String>> _buildSfxAliases() {
|
||||
final Map<int, Set<String>> aliasesById = {};
|
||||
for (final key in _knownSfxKeys) {
|
||||
for (final key in SoundEffect.values) {
|
||||
final ref = _selectedGame.registry.sfx.resolve(key);
|
||||
if (ref == null) {
|
||||
continue;
|
||||
}
|
||||
aliasesById
|
||||
.putIfAbsent(ref.slotIndex, () => <String>{})
|
||||
.add(_readableKeyName(key.toString(), 'SfxKey('));
|
||||
.add(_readableKeyName(key.name));
|
||||
}
|
||||
|
||||
return aliasesById.map(
|
||||
@@ -147,14 +79,14 @@ class _AudioGalleryState extends State<AudioGallery> {
|
||||
|
||||
Map<int, List<String>> _buildMusicAliases() {
|
||||
final Map<int, Set<String>> aliasesById = {};
|
||||
for (final key in _knownMusicKeys) {
|
||||
for (final key in Music.values) {
|
||||
final route = _selectedGame.registry.music.resolve(key);
|
||||
if (route == null) {
|
||||
continue;
|
||||
}
|
||||
aliasesById
|
||||
.putIfAbsent(route.trackIndex, () => <String>{})
|
||||
.add(_readableKeyName(key.toString(), 'MusicKey('));
|
||||
.add(_readableKeyName(key.name));
|
||||
}
|
||||
|
||||
return aliasesById.map(
|
||||
@@ -162,15 +94,12 @@ class _AudioGalleryState extends State<AudioGallery> {
|
||||
);
|
||||
}
|
||||
|
||||
String _readableKeyName(String raw, String prefix) {
|
||||
final String trimmed = raw.startsWith(prefix) && raw.endsWith(')')
|
||||
? raw.substring(prefix.length, raw.length - 1)
|
||||
: raw;
|
||||
if (trimmed.isEmpty) {
|
||||
String _readableKeyName(String raw) {
|
||||
if (raw.isEmpty) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
return trimmed.replaceAllMapped(
|
||||
return raw.replaceAllMapped(
|
||||
RegExp(r'([a-z0-9])([A-Z])'),
|
||||
(match) => '${match.group(1)} ${match.group(2)}',
|
||||
);
|
||||
@@ -203,7 +132,7 @@ class _AudioGalleryState extends State<AudioGallery> {
|
||||
}
|
||||
|
||||
void _playSfx(int id) {
|
||||
widget.wolf3d.audio.playSoundEffect(id);
|
||||
widget.wolf3d.audio.playSoundEffectId(id);
|
||||
}
|
||||
|
||||
Future<void> _toggleMusic(int trackIndex) async {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
162
packages/wolf_3d_dart/lib/src/registry/keys/music.dart
Normal file
162
packages/wolf_3d_dart/lib/src/registry/keys/music.dart
Normal 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,
|
||||
];
|
||||
}
|
||||
@@ -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)';
|
||||
}
|
||||
@@ -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)';
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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].
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -45,7 +45,7 @@ void main() {
|
||||
wallGrid: wallGrid,
|
||||
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||
objectGrid: objectGrid,
|
||||
musicIndex: 0,
|
||||
music: Music.level01,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -45,7 +45,7 @@ void main() {
|
||||
wallGrid: wallGrid,
|
||||
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||
objectGrid: objectGrid,
|
||||
musicIndex: 0,
|
||||
music: Music.level01,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -8,8 +8,8 @@ class FlutterAudioAdapter implements EngineAudio {
|
||||
FlutterAudioAdapter(this.wolf3d);
|
||||
|
||||
@override
|
||||
void playLevelMusic(WolfLevel level) {
|
||||
wolf3d.audio.playLevelMusic(level);
|
||||
void playLevelMusic(Music music) {
|
||||
wolf3d.audio.playLevelMusic(music);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -23,8 +23,13 @@ class FlutterAudioAdapter implements EngineAudio {
|
||||
}
|
||||
|
||||
@override
|
||||
void playSoundEffect(int sfxId) {
|
||||
wolf3d.audio.playSoundEffect(sfxId);
|
||||
void playSoundEffect(SoundEffect effect) {
|
||||
wolf3d.audio.playSoundEffect(effect);
|
||||
}
|
||||
|
||||
@override
|
||||
void playSoundEffectId(int sfxId) {
|
||||
wolf3d.audio.playSoundEffectId(sfxId);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
Reference in New Issue
Block a user