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;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
static const List<SfxKey> _knownSfxKeys = [
|
|
||||||
SfxKey.openDoor,
|
|
||||||
SfxKey.closeDoor,
|
|
||||||
SfxKey.pushWall,
|
|
||||||
SfxKey.knifeAttack,
|
|
||||||
SfxKey.pistolFire,
|
|
||||||
SfxKey.machineGunFire,
|
|
||||||
SfxKey.chainGunFire,
|
|
||||||
SfxKey.enemyFire,
|
|
||||||
SfxKey.getMachineGun,
|
|
||||||
SfxKey.getAmmo,
|
|
||||||
SfxKey.getChainGun,
|
|
||||||
SfxKey.healthSmall,
|
|
||||||
SfxKey.healthLarge,
|
|
||||||
SfxKey.treasure1,
|
|
||||||
SfxKey.treasure2,
|
|
||||||
SfxKey.treasure3,
|
|
||||||
SfxKey.treasure4,
|
|
||||||
SfxKey.extraLife,
|
|
||||||
SfxKey.guardHalt,
|
|
||||||
SfxKey.dogBark,
|
|
||||||
SfxKey.dogDeath,
|
|
||||||
SfxKey.dogAttack,
|
|
||||||
SfxKey.deathScream1,
|
|
||||||
SfxKey.deathScream2,
|
|
||||||
SfxKey.deathScream3,
|
|
||||||
SfxKey.ssAlert,
|
|
||||||
SfxKey.ssDeath,
|
|
||||||
SfxKey.bossActive,
|
|
||||||
SfxKey.hansGrosseDeath,
|
|
||||||
SfxKey.schabbs,
|
|
||||||
SfxKey.schabbsDeath,
|
|
||||||
SfxKey.hitlerGreeting,
|
|
||||||
SfxKey.hitlerDeath,
|
|
||||||
SfxKey.mechaSteps,
|
|
||||||
SfxKey.ottoAlert,
|
|
||||||
SfxKey.gretelDeath,
|
|
||||||
SfxKey.levelComplete,
|
|
||||||
SfxKey.endBonus1,
|
|
||||||
SfxKey.endBonus2,
|
|
||||||
SfxKey.noBonus,
|
|
||||||
SfxKey.percent100,
|
|
||||||
];
|
|
||||||
|
|
||||||
static const List<MusicKey> _knownMusicKeys = [
|
|
||||||
MusicKey.menuTheme,
|
|
||||||
MusicKey.level01,
|
|
||||||
MusicKey.level02,
|
|
||||||
MusicKey.level03,
|
|
||||||
MusicKey.level04,
|
|
||||||
MusicKey.level05,
|
|
||||||
MusicKey.level06,
|
|
||||||
MusicKey.level07,
|
|
||||||
MusicKey.level08,
|
|
||||||
MusicKey.level09,
|
|
||||||
MusicKey.level10,
|
|
||||||
MusicKey.level11,
|
|
||||||
MusicKey.level12,
|
|
||||||
MusicKey.level13,
|
|
||||||
MusicKey.level14,
|
|
||||||
MusicKey.level15,
|
|
||||||
MusicKey.level16,
|
|
||||||
MusicKey.level17,
|
|
||||||
MusicKey.level18,
|
|
||||||
MusicKey.level19,
|
|
||||||
MusicKey.level20,
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -130,14 +62,14 @@ class _AudioGalleryState extends State<AudioGallery> {
|
|||||||
|
|
||||||
Map<int, List<String>> _buildSfxAliases() {
|
Map<int, List<String>> _buildSfxAliases() {
|
||||||
final Map<int, Set<String>> aliasesById = {};
|
final Map<int, Set<String>> aliasesById = {};
|
||||||
for (final key in _knownSfxKeys) {
|
for (final key in SoundEffect.values) {
|
||||||
final ref = _selectedGame.registry.sfx.resolve(key);
|
final ref = _selectedGame.registry.sfx.resolve(key);
|
||||||
if (ref == null) {
|
if (ref == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
aliasesById
|
aliasesById
|
||||||
.putIfAbsent(ref.slotIndex, () => <String>{})
|
.putIfAbsent(ref.slotIndex, () => <String>{})
|
||||||
.add(_readableKeyName(key.toString(), 'SfxKey('));
|
.add(_readableKeyName(key.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
return aliasesById.map(
|
return aliasesById.map(
|
||||||
@@ -147,14 +79,14 @@ class _AudioGalleryState extends State<AudioGallery> {
|
|||||||
|
|
||||||
Map<int, List<String>> _buildMusicAliases() {
|
Map<int, List<String>> _buildMusicAliases() {
|
||||||
final Map<int, Set<String>> aliasesById = {};
|
final Map<int, Set<String>> aliasesById = {};
|
||||||
for (final key in _knownMusicKeys) {
|
for (final key in Music.values) {
|
||||||
final route = _selectedGame.registry.music.resolve(key);
|
final route = _selectedGame.registry.music.resolve(key);
|
||||||
if (route == null) {
|
if (route == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
aliasesById
|
aliasesById
|
||||||
.putIfAbsent(route.trackIndex, () => <String>{})
|
.putIfAbsent(route.trackIndex, () => <String>{})
|
||||||
.add(_readableKeyName(key.toString(), 'MusicKey('));
|
.add(_readableKeyName(key.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
return aliasesById.map(
|
return aliasesById.map(
|
||||||
@@ -162,15 +94,12 @@ class _AudioGalleryState extends State<AudioGallery> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _readableKeyName(String raw, String prefix) {
|
String _readableKeyName(String raw) {
|
||||||
final String trimmed = raw.startsWith(prefix) && raw.endsWith(')')
|
if (raw.isEmpty) {
|
||||||
? raw.substring(prefix.length, raw.length - 1)
|
|
||||||
: raw;
|
|
||||||
if (trimmed.isEmpty) {
|
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
return trimmed.replaceAllMapped(
|
return raw.replaceAllMapped(
|
||||||
RegExp(r'([a-z0-9])([A-Z])'),
|
RegExp(r'([a-z0-9])([A-Z])'),
|
||||||
(match) => '${match.group(1)} ${match.group(2)}',
|
(match) => '${match.group(1)} ${match.group(2)}',
|
||||||
);
|
);
|
||||||
@@ -203,7 +132,7 @@ class _AudioGalleryState extends State<AudioGallery> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _playSfx(int id) {
|
void _playSfx(int id) {
|
||||||
widget.wolf3d.audio.playSoundEffect(id);
|
widget.wolf3d.audio.playSoundEffectId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _toggleMusic(int trackIndex) async {
|
Future<void> _toggleMusic(int trackIndex) async {
|
||||||
|
|||||||
@@ -13,20 +13,6 @@ import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
|||||||
abstract class WLParser {
|
abstract class WLParser {
|
||||||
static const int _areaTileBase = 107;
|
static const int _areaTileBase = 107;
|
||||||
|
|
||||||
// --- Original Song Lookup Tables ---
|
|
||||||
static const List<int> _sharewareMusicMap = [
|
|
||||||
2, 3, 4, 5, 2, 3, 4, 5, 6, 7, // Episode 1
|
|
||||||
];
|
|
||||||
|
|
||||||
static const List<int> _retailMusicMap = [
|
|
||||||
2, 3, 4, 5, 2, 3, 4, 5, 6, 7, // Ep 1
|
|
||||||
8, 9, 10, 11, 8, 9, 11, 10, 6, 12, // Ep 2
|
|
||||||
13, 14, 15, 16, 13, 14, 15, 16, 17, 18, // Ep 3
|
|
||||||
2, 3, 4, 5, 2, 3, 4, 5, 6, 7, // Ep 4
|
|
||||||
8, 9, 10, 11, 8, 9, 11, 10, 6, 12, // Ep 5
|
|
||||||
13, 14, 15, 16, 13, 14, 15, 16, 17, 19, // Ep 6
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Asynchronously discovers the game version and loads all necessary files.
|
/// Asynchronously discovers the game version and loads all necessary files.
|
||||||
///
|
///
|
||||||
/// Provide a [fileFetcher] callback (e.g., Flutter's `rootBundle.load` or
|
/// Provide a [fileFetcher] callback (e.g., Flutter's `rootBundle.load` or
|
||||||
@@ -182,8 +168,6 @@ abstract class WLParser {
|
|||||||
required DataVersion dataIdentity,
|
required DataVersion dataIdentity,
|
||||||
AssetRegistry? registryOverride,
|
AssetRegistry? registryOverride,
|
||||||
}) {
|
}) {
|
||||||
final isShareware = version == GameVersion.shareware;
|
|
||||||
|
|
||||||
final audio = parseAudio(audioHed, audioT, version);
|
final audio = parseAudio(audioHed, audioT, version);
|
||||||
final vgaImages = parseVgaImages(vgaDict, vgaHead, vgaGraph);
|
final vgaImages = parseVgaImages(vgaDict, vgaHead, vgaGraph);
|
||||||
|
|
||||||
@@ -204,7 +188,7 @@ abstract class WLParser {
|
|||||||
walls: parseWalls(vswap),
|
walls: parseWalls(vswap),
|
||||||
sprites: parseSprites(vswap),
|
sprites: parseSprites(vswap),
|
||||||
sounds: parseSounds(vswap).map((bytes) => PcmSound(bytes)).toList(),
|
sounds: parseSounds(vswap).map((bytes) => PcmSound(bytes)).toList(),
|
||||||
episodes: parseEpisodes(mapHead, gameMaps, isShareware: isShareware),
|
episodes: parseEpisodes(mapHead, gameMaps, version: version),
|
||||||
vgaImages: vgaImages,
|
vgaImages: vgaImages,
|
||||||
adLibSounds: audio.adLib,
|
adLibSounds: audio.adLib,
|
||||||
music: audio.music,
|
music: audio.music,
|
||||||
@@ -438,13 +422,12 @@ abstract class WLParser {
|
|||||||
static List<Episode> parseEpisodes(
|
static List<Episode> parseEpisodes(
|
||||||
ByteData mapHead,
|
ByteData mapHead,
|
||||||
ByteData gameMaps, {
|
ByteData gameMaps, {
|
||||||
bool isShareware = true,
|
required GameVersion version,
|
||||||
}) {
|
}) {
|
||||||
List<WolfLevel> allLevels = [];
|
List<WolfLevel> allLevels = [];
|
||||||
int rlewTag = mapHead.getUint16(0, Endian.little);
|
int rlewTag = mapHead.getUint16(0, Endian.little);
|
||||||
|
|
||||||
// Select the correct music map based on the version
|
final isShareware = version == GameVersion.shareware;
|
||||||
final activeMusicMap = isShareware ? _sharewareMusicMap : _retailMusicMap;
|
|
||||||
final episodeNames = isShareware
|
final episodeNames = isShareware
|
||||||
? _sharewareEpisodeNames
|
? _sharewareEpisodeNames
|
||||||
: _retailEpisodeNames;
|
: _retailEpisodeNames;
|
||||||
@@ -510,9 +493,9 @@ abstract class WLParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- ASSIGN MUSIC ---
|
// --- ASSIGN MUSIC ---
|
||||||
int trackIndex = (i < activeMusicMap.length)
|
final episodeIndex = i ~/ 10;
|
||||||
? activeMusicMap[i]
|
final levelIndex = i % 10;
|
||||||
: activeMusicMap[i % activeMusicMap.length];
|
final levelMusic = Music.levelFor(version, episodeIndex, levelIndex);
|
||||||
|
|
||||||
allLevels.add(
|
allLevels.add(
|
||||||
WolfLevel(
|
WolfLevel(
|
||||||
@@ -520,7 +503,7 @@ abstract class WLParser {
|
|||||||
wallGrid: wallGrid,
|
wallGrid: wallGrid,
|
||||||
objectGrid: objectGrid,
|
objectGrid: objectGrid,
|
||||||
areaGrid: areaGrid,
|
areaGrid: areaGrid,
|
||||||
musicIndex: trackIndex,
|
music: levelMusic,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,64 +57,3 @@ class ImfMusic {
|
|||||||
return ImfMusic(instructions);
|
return ImfMusic(instructions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef WolfMusicMap = List<int>;
|
|
||||||
|
|
||||||
/// Map indices to original sound effects as defined in the Wolfenstein 3D source.
|
|
||||||
abstract class WolfSound {
|
|
||||||
// --- Doors & Environment ---
|
|
||||||
static const int openDoor = 8;
|
|
||||||
static const int closeDoor = 9;
|
|
||||||
static const int pushWall = 46; // Secret sliding walls
|
|
||||||
|
|
||||||
// --- Weapons & Combat ---
|
|
||||||
static const int knifeAttack = 23;
|
|
||||||
static const int pistolFire = 24;
|
|
||||||
static const int machineGunFire = 26;
|
|
||||||
static const int gatlingFire = 32; // Historically SHOOTSND in the source
|
|
||||||
static const int naziFire = 58; // Enemy gunshots
|
|
||||||
|
|
||||||
// --- Pickups & Items ---
|
|
||||||
static const int getMachineGun = 30;
|
|
||||||
static const int getAmmo = 31;
|
|
||||||
static const int getGatling = 38;
|
|
||||||
static const int healthSmall = 33; // Dog food / Meals
|
|
||||||
static const int healthLarge = 34; // First Aid
|
|
||||||
static const int treasure1 = 35; // Cross
|
|
||||||
static const int treasure2 = 36; // Chalice
|
|
||||||
static const int treasure3 = 37; // Chest
|
|
||||||
static const int treasure4 = 45; // Crown
|
|
||||||
static const int extraLife = 44; // 1-Up
|
|
||||||
|
|
||||||
// --- Enemies: Standard ---
|
|
||||||
static const int guardHalt = 21; // "Halt!"
|
|
||||||
static const int dogBark = 41;
|
|
||||||
static const int dogDeath = 62;
|
|
||||||
static const int dogAttack = 68;
|
|
||||||
static const int deathScream1 = 29;
|
|
||||||
static const int deathScream2 = 22;
|
|
||||||
static const int deathScream3 = 25;
|
|
||||||
static const int ssSchutzstaffel = 51; // "Schutzstaffel!"
|
|
||||||
static const int ssMeinGott = 63; // SS Death
|
|
||||||
|
|
||||||
// --- Enemies: Bosses (Retail Episodes 1-6) ---
|
|
||||||
static const int bossActive = 49;
|
|
||||||
static const int mutti = 50; // Hans Grosse Death
|
|
||||||
static const int ahhhg = 52;
|
|
||||||
static const int eva = 54; // Dr. Schabbs Death
|
|
||||||
static const int gutenTag = 55; // Hitler Greeting
|
|
||||||
static const int leben = 56;
|
|
||||||
static const int scheist = 57; // Hitler Death
|
|
||||||
static const int schabbsHas = 64; // Dr. Schabbs
|
|
||||||
static const int hitlerHas = 65;
|
|
||||||
static const int spion = 66; // Otto Giftmacher
|
|
||||||
static const int neinSoVass = 67; // Gretel Grosse Death
|
|
||||||
static const int mechSteps = 70; // Mecha-Hitler walking
|
|
||||||
|
|
||||||
// --- UI & Progression ---
|
|
||||||
static const int levelDone = 40;
|
|
||||||
static const int endBonus1 = 42;
|
|
||||||
static const int endBonus2 = 43;
|
|
||||||
static const int noBonus = 47;
|
|
||||||
static const int percent100 = 48;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ class WolfLevel {
|
|||||||
/// zero-based and correspond to original AREATILE-derived sectors.
|
/// zero-based and correspond to original AREATILE-derived sectors.
|
||||||
final SpriteMap areaGrid;
|
final SpriteMap areaGrid;
|
||||||
|
|
||||||
/// The index of the [ImfMusic] track to play while this level is active.
|
/// The [Music] track to play while this level is active.
|
||||||
final int musicIndex;
|
final Music music;
|
||||||
|
|
||||||
const WolfLevel({
|
const WolfLevel({
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.wallGrid,
|
required this.wallGrid,
|
||||||
required this.objectGrid,
|
required this.objectGrid,
|
||||||
required this.areaGrid,
|
required this.areaGrid,
|
||||||
required this.musicIndex,
|
required this.music,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class WolfensteinData {
|
|||||||
///
|
///
|
||||||
/// Access the five sub-modules via:
|
/// Access the five sub-modules via:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// data.registry.sfx.resolve(SfxKey.pistolFire)
|
/// data.registry.sfx.resolve(SoundEffect.pistolFire)
|
||||||
/// data.registry.music.musicForLevel(episode, level)
|
/// data.registry.music.musicForLevel(episode, level)
|
||||||
/// data.registry.entities.resolve(EntityKey.guard)
|
/// data.registry.entities.resolve(EntityKey.guard)
|
||||||
/// data.registry.hud.faceForHealth(player.health)
|
/// data.registry.hud.faceForHealth(player.health)
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ abstract class EngineAudio {
|
|||||||
WolfensteinData? activeGame;
|
WolfensteinData? activeGame;
|
||||||
Future<void> debugSoundTest();
|
Future<void> debugSoundTest();
|
||||||
void playMenuMusic();
|
void playMenuMusic();
|
||||||
void playLevelMusic(WolfLevel level);
|
void playLevelMusic(Music music);
|
||||||
void stopMusic();
|
void stopMusic();
|
||||||
Future<void> stopAllAudio();
|
Future<void> stopAllAudio();
|
||||||
void playSoundEffect(int sfxId);
|
void playSoundEffect(SoundEffect effect);
|
||||||
|
void playSoundEffectId(int sfxId);
|
||||||
Future<void> init();
|
Future<void> init();
|
||||||
void dispose();
|
void dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ class CliSilentAudio implements EngineAudio {
|
|||||||
void playMenuMusic() {}
|
void playMenuMusic() {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void playLevelMusic(WolfLevel level) {
|
void playLevelMusic(Music music) {}
|
||||||
// Optional: Print a log so you know it's working!
|
|
||||||
// debugPrint("🎵 Playing music for: ${level.name} 🎵");
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void stopMusic() {}
|
void stopMusic() {}
|
||||||
@@ -26,7 +23,14 @@ class CliSilentAudio implements EngineAudio {
|
|||||||
Future<void> stopAllAudio() async {}
|
Future<void> stopAllAudio() async {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void playSoundEffect(int sfxId) {
|
void playSoundEffect(SoundEffect effect) {
|
||||||
|
// Optional: You could use the terminal 'bell' character here
|
||||||
|
// to actually make a system beep when a sound plays!
|
||||||
|
// stdout.write('\x07');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playSoundEffectId(int sfxId) {
|
||||||
// Optional: You could use the terminal 'bell' character here
|
// Optional: You could use the terminal 'bell' character here
|
||||||
// to actually make a system beep when a sound plays!
|
// to actually make a system beep when a sound plays!
|
||||||
// stdout.write('\x07');
|
// stdout.write('\x07');
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class DoorManager {
|
|||||||
|
|
||||||
/// Callback used to trigger sound effects without tight coupling
|
/// Callback used to trigger sound effects without tight coupling
|
||||||
/// to a specific audio engine implementation.
|
/// to a specific audio engine implementation.
|
||||||
final void Function(int sfxId) onPlaySound;
|
final void Function(SoundEffect effect) onPlaySound;
|
||||||
|
|
||||||
static int _key(int x, int y) => ((y & 0xFFFF) << 16) | (x & 0xFFFF);
|
static int _key(int x, int y) => ((y & 0xFFFF) << 16) | (x & 0xFFFF);
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ class DoorManager {
|
|||||||
final newState = door.update(elapsed.inMilliseconds);
|
final newState = door.update(elapsed.inMilliseconds);
|
||||||
|
|
||||||
if (newState == DoorState.closing) {
|
if (newState == DoorState.closing) {
|
||||||
onPlaySound(WolfSound.closeDoor);
|
onPlaySound(SoundEffect.closeDoor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ class DoorManager {
|
|||||||
|
|
||||||
final int key = _key(targetX, targetY);
|
final int key = _key(targetX, targetY);
|
||||||
if (doors.containsKey(key) && doors[key]!.interact()) {
|
if (doors.containsKey(key) && doors[key]!.interact()) {
|
||||||
onPlaySound(WolfSound.openDoor);
|
onPlaySound(SoundEffect.openDoor);
|
||||||
log('[DEBUG] Player opened door at ($targetX, $targetY)');
|
log('[DEBUG] Player opened door at ($targetX, $targetY)');
|
||||||
return (x: targetX, y: targetY);
|
return (x: targetX, y: targetY);
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ class DoorManager {
|
|||||||
// AI only interacts if the door is currently fully closed (offset == 0).
|
// AI only interacts if the door is currently fully closed (offset == 0).
|
||||||
if (doors.containsKey(key) && doors[key]!.offset == 0.0) {
|
if (doors.containsKey(key) && doors[key]!.offset == 0.0) {
|
||||||
if (doors[key]!.interact()) {
|
if (doors[key]!.interact()) {
|
||||||
onPlaySound(WolfSound.openDoor);
|
onPlaySound(SoundEffect.openDoor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class PushwallManager {
|
|||||||
PushwallManager({this.onPlaySound});
|
PushwallManager({this.onPlaySound});
|
||||||
|
|
||||||
/// Optional callback used to emit audio cues when pushwalls activate.
|
/// Optional callback used to emit audio cues when pushwalls activate.
|
||||||
final void Function(int sfxId)? onPlaySound;
|
final void Function(SoundEffect effect)? onPlaySound;
|
||||||
|
|
||||||
final Map<String, Pushwall> pushwalls = {};
|
final Map<String, Pushwall> pushwalls = {};
|
||||||
Pushwall? activePushwall;
|
Pushwall? activePushwall;
|
||||||
@@ -127,7 +127,7 @@ class PushwallManager {
|
|||||||
int checkY = targetY + pw.dirY;
|
int checkY = targetY + pw.dirY;
|
||||||
if (wallGrid[checkY][checkX] == 0) {
|
if (wallGrid[checkY][checkX] == 0) {
|
||||||
activePushwall = pw;
|
activePushwall = pw;
|
||||||
onPlaySound?.call(WolfSound.pushWall);
|
onPlaySound?.call(SoundEffect.pushWall);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ class Player {
|
|||||||
/// Attempts to collect [item] and returns the SFX to play.
|
/// Attempts to collect [item] and returns the SFX to play.
|
||||||
///
|
///
|
||||||
/// Returns `null` when the item was not collected (for example: full health).
|
/// Returns `null` when the item was not collected (for example: full health).
|
||||||
int? tryPickup(Collectible item) {
|
SoundEffect? tryPickup(Collectible item) {
|
||||||
final effect = item.tryCollect(
|
final effect = item.tryCollect(
|
||||||
CollectiblePickupContext(
|
CollectiblePickupContext(
|
||||||
health: health,
|
health: health,
|
||||||
@@ -211,7 +211,7 @@ class Player {
|
|||||||
requestWeaponSwitch(weaponType);
|
requestWeaponSwitch(weaponType);
|
||||||
}
|
}
|
||||||
|
|
||||||
return effect.pickupSfxId;
|
return effect.pickupSoundEffect;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool fire(int currentTime) {
|
bool fire(int currentTime) {
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ class WolfEngine {
|
|||||||
_availableGames = availableGames ?? <WolfensteinData>[data!],
|
_availableGames = availableGames ?? <WolfensteinData>[data!],
|
||||||
audio = engineAudio ?? CliSilentAudio(),
|
audio = engineAudio ?? CliSilentAudio(),
|
||||||
doorManager = DoorManager(
|
doorManager = DoorManager(
|
||||||
onPlaySound: (sfxId) => engineAudio?.playSoundEffect(sfxId),
|
onPlaySound: (effect) => engineAudio?.playSoundEffect(effect),
|
||||||
),
|
),
|
||||||
pushwallManager = PushwallManager(
|
pushwallManager = PushwallManager(
|
||||||
onPlaySound: (sfxId) => engineAudio?.playSoundEffect(sfxId),
|
onPlaySound: (effect) => engineAudio?.playSoundEffect(effect),
|
||||||
) {
|
) {
|
||||||
if (_availableGames.isEmpty) {
|
if (_availableGames.isEmpty) {
|
||||||
throw StateError('WolfEngine requires at least one game data set.');
|
throw StateError('WolfEngine requires at least one game data set.');
|
||||||
@@ -481,7 +481,7 @@ class WolfEngine {
|
|||||||
|
|
||||||
doorManager.initDoors(currentLevel);
|
doorManager.initDoors(currentLevel);
|
||||||
pushwallManager.initPushwalls(currentLevel, _objectLevel);
|
pushwallManager.initPushwalls(currentLevel, _objectLevel);
|
||||||
audio.playLevelMusic(activeLevel);
|
audio.playLevelMusic(activeLevel.music);
|
||||||
|
|
||||||
// Spawn Player and Entities from the Object Grid
|
// Spawn Player and Entities from the Object Grid
|
||||||
bool playerSpawned = false;
|
bool playerSpawned = false;
|
||||||
@@ -540,7 +540,7 @@ class WolfEngine {
|
|||||||
|
|
||||||
/// Handles floor transitions, including the "Level 10" secret floor logic.
|
/// Handles floor transitions, including the "Level 10" secret floor logic.
|
||||||
void _onLevelCompleted({bool isSecretExit = false}) {
|
void _onLevelCompleted({bool isSecretExit = false}) {
|
||||||
audio.playSoundEffect(WolfSound.levelDone);
|
audio.playSoundEffect(SoundEffect.levelComplete);
|
||||||
audio.stopMusic();
|
audio.stopMusic();
|
||||||
player
|
player
|
||||||
..hasGoldKey = false
|
..hasGoldKey = false
|
||||||
@@ -679,7 +679,7 @@ class WolfEngine {
|
|||||||
if (entity.state == EntityState.dead &&
|
if (entity.state == EntityState.dead &&
|
||||||
entity.isDying &&
|
entity.isDying &&
|
||||||
!entity.hasPlayedDeathSound) {
|
!entity.hasPlayedDeathSound) {
|
||||||
audio.playSoundEffect(entity.deathSoundId);
|
audio.playSoundEffect(entity.deathSound);
|
||||||
entity.hasPlayedDeathSound = true;
|
entity.hasPlayedDeathSound = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,9 +787,9 @@ class WolfEngine {
|
|||||||
}
|
}
|
||||||
} else if (entity is Collectible) {
|
} else if (entity is Collectible) {
|
||||||
if (player.position.distanceTo(entity.position) < 0.5) {
|
if (player.position.distanceTo(entity.position) < 0.5) {
|
||||||
final pickupSfxId = player.tryPickup(entity);
|
final pickupSoundEffect = player.tryPickup(entity);
|
||||||
if (pickupSfxId != null) {
|
if (pickupSoundEffect != null) {
|
||||||
audio.playSoundEffect(pickupSfxId);
|
audio.playSoundEffect(pickupSoundEffect);
|
||||||
itemsToRemove.add(entity);
|
itemsToRemove.add(entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -826,7 +826,7 @@ class WolfEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entity.isAlerted = true;
|
entity.isAlerted = true;
|
||||||
audio.playSoundEffect(entity.alertSoundId);
|
audio.playSoundEffect(entity.alertSound);
|
||||||
log(
|
log(
|
||||||
'[DEBUG] Enemy #${entity.debugId} (${entity.type.name}) '
|
'[DEBUG] Enemy #${entity.debugId} (${entity.type.name}) '
|
||||||
'alerted by gunfire in area $area',
|
'alerted by gunfire in area $area',
|
||||||
@@ -1007,10 +1007,10 @@ class WolfEngine {
|
|||||||
|
|
||||||
void _playPlayerWeaponSound() {
|
void _playPlayerWeaponSound() {
|
||||||
final sfxId = switch (player.currentWeapon.type) {
|
final sfxId = switch (player.currentWeapon.type) {
|
||||||
WeaponType.knife => WolfSound.knifeAttack,
|
WeaponType.knife => SoundEffect.knifeAttack,
|
||||||
WeaponType.pistol => WolfSound.pistolFire,
|
WeaponType.pistol => SoundEffect.pistolFire,
|
||||||
WeaponType.machineGun => WolfSound.machineGunFire,
|
WeaponType.machineGun => SoundEffect.machineGunFire,
|
||||||
WeaponType.chainGun => WolfSound.gatlingFire,
|
WeaponType.chainGun => SoundEffect.chainGunFire,
|
||||||
};
|
};
|
||||||
|
|
||||||
audio.playSoundEffect(sfxId);
|
audio.playSoundEffect(sfxId);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class CollectiblePickupEffect {
|
|||||||
final int ammoToAdd;
|
final int ammoToAdd;
|
||||||
final int scoreToAdd;
|
final int scoreToAdd;
|
||||||
final int extraLivesToAdd;
|
final int extraLivesToAdd;
|
||||||
final int pickupSfxId;
|
final SoundEffect pickupSoundEffect;
|
||||||
final bool grantGoldKey;
|
final bool grantGoldKey;
|
||||||
final bool grantSilverKey;
|
final bool grantSilverKey;
|
||||||
final WeaponType? grantWeapon;
|
final WeaponType? grantWeapon;
|
||||||
@@ -40,7 +40,7 @@ class CollectiblePickupEffect {
|
|||||||
this.ammoToAdd = 0,
|
this.ammoToAdd = 0,
|
||||||
this.scoreToAdd = 0,
|
this.scoreToAdd = 0,
|
||||||
this.extraLivesToAdd = 0,
|
this.extraLivesToAdd = 0,
|
||||||
required this.pickupSfxId,
|
required this.pickupSoundEffect,
|
||||||
this.grantGoldKey = false,
|
this.grantGoldKey = false,
|
||||||
this.grantSilverKey = false,
|
this.grantSilverKey = false,
|
||||||
this.grantWeapon,
|
this.grantWeapon,
|
||||||
@@ -49,7 +49,7 @@ class CollectiblePickupEffect {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'CollectiblePickupEffect(healthToRestore: $healthToRestore, ammoToAdd: $ammoToAdd, scoreToAdd: $scoreToAdd, extraLivesToAdd: $extraLivesToAdd, pickupSfxId: $pickupSfxId, grantGoldKey: $grantGoldKey, grantSilverKey: $grantSilverKey, grantWeapon: $grantWeapon, requestWeaponSwitch: $requestWeaponSwitch)';
|
return 'CollectiblePickupEffect(healthToRestore: $healthToRestore, ammoToAdd: $ammoToAdd, scoreToAdd: $scoreToAdd, extraLivesToAdd: $extraLivesToAdd, pickupSoundEffect: $pickupSoundEffect, grantGoldKey: $grantGoldKey, grantSilverKey: $grantSilverKey, grantWeapon: $grantWeapon, requestWeaponSwitch: $requestWeaponSwitch)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +123,9 @@ class HealthCollectible extends Collectible {
|
|||||||
final bool isFood = mapId == MapObject.food;
|
final bool isFood = mapId == MapObject.food;
|
||||||
return CollectiblePickupEffect(
|
return CollectiblePickupEffect(
|
||||||
healthToRestore: isFood ? 10 : 25,
|
healthToRestore: isFood ? 10 : 25,
|
||||||
pickupSfxId: isFood ? WolfSound.healthSmall : WolfSound.healthLarge,
|
pickupSoundEffect: isFood
|
||||||
|
? SoundEffect.healthSmall
|
||||||
|
: SoundEffect.healthLarge,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,7 +151,7 @@ class AmmoCollectible extends Collectible {
|
|||||||
|
|
||||||
return CollectiblePickupEffect(
|
return CollectiblePickupEffect(
|
||||||
ammoToAdd: ammoAmount,
|
ammoToAdd: ammoAmount,
|
||||||
pickupSfxId: WolfSound.getAmmo,
|
pickupSoundEffect: SoundEffect.getAmmo,
|
||||||
requestWeaponSwitch: shouldAutoswitchToPistol ? WeaponType.pistol : null,
|
requestWeaponSwitch: shouldAutoswitchToPistol ? WeaponType.pistol : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -172,7 +174,7 @@ class WeaponCollectible extends Collectible {
|
|||||||
if (mapId == MapObject.machineGun) {
|
if (mapId == MapObject.machineGun) {
|
||||||
return const CollectiblePickupEffect(
|
return const CollectiblePickupEffect(
|
||||||
ammoToAdd: 6,
|
ammoToAdd: 6,
|
||||||
pickupSfxId: WolfSound.getMachineGun,
|
pickupSoundEffect: SoundEffect.getMachineGun,
|
||||||
grantWeapon: WeaponType.machineGun,
|
grantWeapon: WeaponType.machineGun,
|
||||||
requestWeaponSwitch: WeaponType.machineGun,
|
requestWeaponSwitch: WeaponType.machineGun,
|
||||||
);
|
);
|
||||||
@@ -181,7 +183,7 @@ class WeaponCollectible extends Collectible {
|
|||||||
if (mapId == MapObject.chainGun) {
|
if (mapId == MapObject.chainGun) {
|
||||||
return const CollectiblePickupEffect(
|
return const CollectiblePickupEffect(
|
||||||
ammoToAdd: 6,
|
ammoToAdd: 6,
|
||||||
pickupSfxId: WolfSound.getGatling,
|
pickupSoundEffect: SoundEffect.getChainGun,
|
||||||
grantWeapon: WeaponType.chainGun,
|
grantWeapon: WeaponType.chainGun,
|
||||||
requestWeaponSwitch: WeaponType.chainGun,
|
requestWeaponSwitch: WeaponType.chainGun,
|
||||||
);
|
);
|
||||||
@@ -203,7 +205,7 @@ class KeyCollectible extends Collectible {
|
|||||||
if (mapId == MapObject.goldKey) {
|
if (mapId == MapObject.goldKey) {
|
||||||
if (context.hasGoldKey) return null;
|
if (context.hasGoldKey) return null;
|
||||||
return const CollectiblePickupEffect(
|
return const CollectiblePickupEffect(
|
||||||
pickupSfxId: WolfSound.getAmmo,
|
pickupSoundEffect: SoundEffect.getAmmo,
|
||||||
grantGoldKey: true,
|
grantGoldKey: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -211,7 +213,7 @@ class KeyCollectible extends Collectible {
|
|||||||
if (mapId == MapObject.silverKey) {
|
if (mapId == MapObject.silverKey) {
|
||||||
if (context.hasSilverKey) return null;
|
if (context.hasSilverKey) return null;
|
||||||
return const CollectiblePickupEffect(
|
return const CollectiblePickupEffect(
|
||||||
pickupSfxId: WolfSound.getAmmo,
|
pickupSoundEffect: SoundEffect.getAmmo,
|
||||||
grantSilverKey: true,
|
grantSilverKey: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -243,18 +245,18 @@ class TreasureCollectible extends Collectible {
|
|||||||
healthToRestore: 99,
|
healthToRestore: 99,
|
||||||
ammoToAdd: 25,
|
ammoToAdd: 25,
|
||||||
extraLivesToAdd: 1,
|
extraLivesToAdd: 1,
|
||||||
pickupSfxId: WolfSound.extraLife,
|
pickupSoundEffect: SoundEffect.extraLife,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return CollectiblePickupEffect(
|
return CollectiblePickupEffect(
|
||||||
scoreToAdd: scoreValue,
|
scoreToAdd: scoreValue,
|
||||||
pickupSfxId: switch (mapId) {
|
pickupSoundEffect: switch (mapId) {
|
||||||
MapObject.cross => WolfSound.treasure1,
|
MapObject.cross => SoundEffect.treasure1,
|
||||||
MapObject.chalice => WolfSound.treasure2,
|
MapObject.chalice => SoundEffect.treasure2,
|
||||||
MapObject.chest => WolfSound.treasure3,
|
MapObject.chest => SoundEffect.treasure3,
|
||||||
MapObject.crown => WolfSound.treasure4,
|
MapObject.crown => SoundEffect.treasure4,
|
||||||
_ => WolfSound.getAmmo,
|
_ => SoundEffect.getAmmo,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ class HansGrosse extends Enemy {
|
|||||||
throw UnimplementedError("Hans Grosse uses manual animation logic.");
|
throw UnimplementedError("Hans Grosse uses manual animation logic.");
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get alertSoundId => WolfSound.bossActive;
|
SoundEffect get alertSound => SoundEffect.bossActive;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get attackSoundId => WolfSound.naziFire;
|
SoundEffect get attackSound => SoundEffect.enemyFire;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get deathSoundId => WolfSound.mutti;
|
SoundEffect get deathSound => SoundEffect.hansGrosseDeath;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get scoreValue => 5000;
|
int get scoreValue => 5000;
|
||||||
@@ -85,7 +85,7 @@ class HansGrosse extends Enemy {
|
|||||||
required bool Function(int area) isAreaConnectedToPlayer,
|
required bool Function(int area) isAreaConnectedToPlayer,
|
||||||
required void Function(int damage) onDamagePlayer,
|
required void Function(int damage) onDamagePlayer,
|
||||||
required void Function(int x, int y) tryOpenDoor,
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
required void Function(int sfxId) onPlaySound,
|
required void Function(SoundEffect effect) onPlaySound,
|
||||||
}) {
|
}) {
|
||||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||||
double newAngle = angle;
|
double newAngle = angle;
|
||||||
@@ -102,7 +102,7 @@ class HansGrosse extends Enemy {
|
|||||||
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
||||||
baseReactionMs: 50,
|
baseReactionMs: 50,
|
||||||
)) {
|
)) {
|
||||||
onPlaySound(alertSoundId);
|
onPlaySound(alertSound);
|
||||||
}
|
}
|
||||||
|
|
||||||
double distance = position.distanceTo(playerPosition);
|
double distance = position.distanceTo(playerPosition);
|
||||||
@@ -151,7 +151,7 @@ class HansGrosse extends Enemy {
|
|||||||
setTics(10);
|
setTics(10);
|
||||||
} else if (currentFrame == 2) {
|
} else if (currentFrame == 2) {
|
||||||
spriteIndex = _baseSprite + 6; // Fire
|
spriteIndex = _baseSprite + 6; // Fire
|
||||||
onPlaySound(attackSoundId);
|
onPlaySound(attackSound);
|
||||||
onDamagePlayer(damage);
|
onDamagePlayer(damage);
|
||||||
setTics(4);
|
setTics(4);
|
||||||
} else if (currentFrame == 3) {
|
} else if (currentFrame == 3) {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class Dog extends Enemy {
|
|||||||
required bool Function(int area) isAreaConnectedToPlayer,
|
required bool Function(int area) isAreaConnectedToPlayer,
|
||||||
required void Function(int damage) onDamagePlayer,
|
required void Function(int damage) onDamagePlayer,
|
||||||
required void Function(int x, int y) tryOpenDoor,
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
required void Function(int sfxId) onPlaySound,
|
required void Function(SoundEffect effect) onPlaySound,
|
||||||
}) {
|
}) {
|
||||||
final previousState = state;
|
final previousState = state;
|
||||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||||
@@ -60,7 +60,7 @@ class Dog extends Enemy {
|
|||||||
areaAt: areaAt,
|
areaAt: areaAt,
|
||||||
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
||||||
)) {
|
)) {
|
||||||
onPlaySound(alertSoundId);
|
onPlaySound(alertSound);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ class Dog extends Enemy {
|
|||||||
currentFrame++;
|
currentFrame++;
|
||||||
// Phase 2: The actual bite
|
// Phase 2: The actual bite
|
||||||
if (currentFrame == 1) {
|
if (currentFrame == 1) {
|
||||||
onPlaySound(attackSoundId);
|
onPlaySound(attackSound);
|
||||||
final bool attackSuccessful =
|
final bool attackSuccessful =
|
||||||
math.Random().nextDouble() < (180 / 256);
|
math.Random().nextDouble() < (180 / 256);
|
||||||
|
|
||||||
@@ -176,7 +176,9 @@ class Dog extends Enemy {
|
|||||||
// A movement magnitude threshold prevents accepting near-zero floating-
|
// A movement magnitude threshold prevents accepting near-zero floating-
|
||||||
// point residuals (e.g. cos(π/2) ≈ 6e-17) as valid movement.
|
// point residuals (e.g. cos(π/2) ≈ 6e-17) as valid movement.
|
||||||
final double minEffective = currentMoveSpeed * 0.5;
|
final double minEffective = currentMoveSpeed * 0.5;
|
||||||
final double currentDistanceToPlayer = position.distanceTo(playerPosition);
|
final double currentDistanceToPlayer = position.distanceTo(
|
||||||
|
playerPosition,
|
||||||
|
);
|
||||||
|
|
||||||
int selectedCandidateIndex = -1;
|
int selectedCandidateIndex = -1;
|
||||||
for (int i = 0; i < candidateAngles.length; i++) {
|
for (int i = 0; i < candidateAngles.length; i++) {
|
||||||
@@ -192,7 +194,8 @@ class Dog extends Enemy {
|
|||||||
tryOpenDoor: (x, y) {},
|
tryOpenDoor: (x, y) {},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (candidateMovement.x.abs() + candidateMovement.y.abs() < minEffective) {
|
if (candidateMovement.x.abs() + candidateMovement.y.abs() <
|
||||||
|
minEffective) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,16 +55,16 @@ abstract class Enemy extends Entity {
|
|||||||
EnemyType get type;
|
EnemyType get type;
|
||||||
|
|
||||||
/// The sound played when this enemy notices the player or hears combat.
|
/// The sound played when this enemy notices the player or hears combat.
|
||||||
int get alertSoundId => type.alertSoundId;
|
SoundEffect get alertSound => type.alertSound;
|
||||||
|
|
||||||
/// The sound played when this enemy performs its attack animation.
|
/// The sound played when this enemy performs its attack animation.
|
||||||
int get attackSoundId => type.attackSoundId;
|
SoundEffect get attackSound => type.attackSound;
|
||||||
|
|
||||||
/// The score awarded when this enemy is killed.
|
/// The score awarded when this enemy is killed.
|
||||||
int get scoreValue => type.scoreValue;
|
int get scoreValue => type.scoreValue;
|
||||||
|
|
||||||
/// The sound played once when this enemy starts dying.
|
/// The sound played once when this enemy starts dying.
|
||||||
int get deathSoundId => type.deathSoundId;
|
SoundEffect get deathSound => type.deathSound;
|
||||||
|
|
||||||
/// Ensures enemies drop only one item (like ammo or a key) upon death.
|
/// Ensures enemies drop only one item (like ammo or a key) upon death.
|
||||||
bool hasDroppedItem = false;
|
bool hasDroppedItem = false;
|
||||||
@@ -412,11 +412,19 @@ abstract class Enemy extends Entity {
|
|||||||
// is moving mostly north and clips a wall tile beside an open door, the
|
// is moving mostly north and clips a wall tile beside an open door, the
|
||||||
// Y (northward) slide is preferred over the X (sideways) slide.
|
// Y (northward) slide is preferred over the X (sideways) slide.
|
||||||
if (intendedMovement.y.abs() >= intendedMovement.x.abs()) {
|
if (intendedMovement.y.abs() >= intendedMovement.x.abs()) {
|
||||||
if (canMoveY) return normalizeTiny(Coordinate2D(0, intendedMovement.y));
|
if (canMoveY) {
|
||||||
if (canMoveX) return normalizeTiny(Coordinate2D(intendedMovement.x, 0));
|
return normalizeTiny(Coordinate2D(0, intendedMovement.y));
|
||||||
|
}
|
||||||
|
if (canMoveX) {
|
||||||
|
return normalizeTiny(Coordinate2D(intendedMovement.x, 0));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (canMoveX) return normalizeTiny(Coordinate2D(intendedMovement.x, 0));
|
if (canMoveX) {
|
||||||
if (canMoveY) return normalizeTiny(Coordinate2D(0, intendedMovement.y));
|
return normalizeTiny(Coordinate2D(intendedMovement.x, 0));
|
||||||
|
}
|
||||||
|
if (canMoveY) {
|
||||||
|
return normalizeTiny(Coordinate2D(0, intendedMovement.y));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return const Coordinate2D(0, 0);
|
return const Coordinate2D(0, 0);
|
||||||
}
|
}
|
||||||
@@ -534,7 +542,7 @@ abstract class Enemy extends Entity {
|
|||||||
required bool Function(int area) isAreaConnectedToPlayer,
|
required bool Function(int area) isAreaConnectedToPlayer,
|
||||||
required void Function(int x, int y) tryOpenDoor,
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
required void Function(int damage) onDamagePlayer,
|
required void Function(int damage) onDamagePlayer,
|
||||||
required void Function(int sfxId) onPlaySound,
|
required void Function(SoundEffect effect) onPlaySound,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Factory method to spawn the correct [Enemy] subclass based on a Map ID.
|
/// Factory method to spawn the correct [Enemy] subclass based on a Map ID.
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ enum EnemyType {
|
|||||||
guard(
|
guard(
|
||||||
mapData: EnemyMapData(MapObject.guardStart),
|
mapData: EnemyMapData(MapObject.guardStart),
|
||||||
scoreValue: 100,
|
scoreValue: 100,
|
||||||
alertSoundId: WolfSound.guardHalt,
|
alertSound: SoundEffect.guardHalt,
|
||||||
attackSoundId: WolfSound.naziFire,
|
attackSound: SoundEffect.enemyFire,
|
||||||
deathSoundId: WolfSound.deathScream1,
|
deathSound: SoundEffect.deathScream1,
|
||||||
animations: EnemyAnimationMap(
|
animations: EnemyAnimationMap(
|
||||||
idle: SpriteFrameRange(50, 57),
|
idle: SpriteFrameRange(50, 57),
|
||||||
walking: SpriteFrameRange(58, 89),
|
walking: SpriteFrameRange(58, 89),
|
||||||
@@ -28,9 +28,9 @@ enum EnemyType {
|
|||||||
dog(
|
dog(
|
||||||
mapData: EnemyMapData(MapObject.dogStart),
|
mapData: EnemyMapData(MapObject.dogStart),
|
||||||
scoreValue: 200,
|
scoreValue: 200,
|
||||||
alertSoundId: WolfSound.dogBark,
|
alertSound: SoundEffect.dogBark,
|
||||||
attackSoundId: WolfSound.dogAttack,
|
attackSound: SoundEffect.dogAttack,
|
||||||
deathSoundId: WolfSound.dogDeath,
|
deathSound: SoundEffect.dogDeath,
|
||||||
animations: EnemyAnimationMap(
|
animations: EnemyAnimationMap(
|
||||||
// Dogs don't have true idle sprites, so map idle to the first walk frame safely
|
// Dogs don't have true idle sprites, so map idle to the first walk frame safely
|
||||||
idle: SpriteFrameRange(99, 106),
|
idle: SpriteFrameRange(99, 106),
|
||||||
@@ -51,9 +51,9 @@ enum EnemyType {
|
|||||||
ss(
|
ss(
|
||||||
mapData: EnemyMapData(MapObject.ssStart),
|
mapData: EnemyMapData(MapObject.ssStart),
|
||||||
scoreValue: 500,
|
scoreValue: 500,
|
||||||
alertSoundId: WolfSound.ssSchutzstaffel,
|
alertSound: SoundEffect.ssAlert,
|
||||||
attackSoundId: WolfSound.naziFire,
|
attackSound: SoundEffect.enemyFire,
|
||||||
deathSoundId: WolfSound.ssMeinGott,
|
deathSound: SoundEffect.ssDeath,
|
||||||
animations: EnemyAnimationMap(
|
animations: EnemyAnimationMap(
|
||||||
idle: SpriteFrameRange(138, 145),
|
idle: SpriteFrameRange(138, 145),
|
||||||
walking: SpriteFrameRange(146, 177),
|
walking: SpriteFrameRange(146, 177),
|
||||||
@@ -68,9 +68,9 @@ enum EnemyType {
|
|||||||
mutant(
|
mutant(
|
||||||
mapData: EnemyMapData(MapObject.mutantStart, tierOffset: 18),
|
mapData: EnemyMapData(MapObject.mutantStart, tierOffset: 18),
|
||||||
scoreValue: 700,
|
scoreValue: 700,
|
||||||
alertSoundId: WolfSound.guardHalt,
|
alertSound: SoundEffect.guardHalt,
|
||||||
attackSoundId: WolfSound.naziFire,
|
attackSound: SoundEffect.enemyFire,
|
||||||
deathSoundId: WolfSound.deathScream2,
|
deathSound: SoundEffect.deathScream2,
|
||||||
animations: EnemyAnimationMap(
|
animations: EnemyAnimationMap(
|
||||||
idle: SpriteFrameRange(187, 194),
|
idle: SpriteFrameRange(187, 194),
|
||||||
walking: SpriteFrameRange(195, 226),
|
walking: SpriteFrameRange(195, 226),
|
||||||
@@ -86,9 +86,9 @@ enum EnemyType {
|
|||||||
officer(
|
officer(
|
||||||
mapData: EnemyMapData(MapObject.officerStart),
|
mapData: EnemyMapData(MapObject.officerStart),
|
||||||
scoreValue: 400,
|
scoreValue: 400,
|
||||||
alertSoundId: WolfSound.guardHalt,
|
alertSound: SoundEffect.guardHalt,
|
||||||
attackSoundId: WolfSound.naziFire,
|
attackSound: SoundEffect.enemyFire,
|
||||||
deathSoundId: WolfSound.deathScream3,
|
deathSound: SoundEffect.deathScream3,
|
||||||
animations: EnemyAnimationMap(
|
animations: EnemyAnimationMap(
|
||||||
idle: SpriteFrameRange(238, 245),
|
idle: SpriteFrameRange(238, 245),
|
||||||
walking: SpriteFrameRange(246, 277),
|
walking: SpriteFrameRange(246, 277),
|
||||||
@@ -111,13 +111,13 @@ enum EnemyType {
|
|||||||
final int scoreValue;
|
final int scoreValue;
|
||||||
|
|
||||||
/// The sound played when this enemy first becomes alerted.
|
/// The sound played when this enemy first becomes alerted.
|
||||||
final int alertSoundId;
|
final SoundEffect alertSound;
|
||||||
|
|
||||||
/// The sound played when this enemy attacks.
|
/// The sound played when this enemy attacks.
|
||||||
final int attackSoundId;
|
final SoundEffect attackSound;
|
||||||
|
|
||||||
/// The sound played when this enemy enters its death animation.
|
/// The sound played when this enemy enters its death animation.
|
||||||
final int deathSoundId;
|
final SoundEffect deathSound;
|
||||||
|
|
||||||
/// If false, this enemy type will be ignored when loading shareware data.
|
/// If false, this enemy type will be ignored when loading shareware data.
|
||||||
final bool existsInShareware;
|
final bool existsInShareware;
|
||||||
@@ -126,9 +126,9 @@ enum EnemyType {
|
|||||||
required this.mapData,
|
required this.mapData,
|
||||||
required this.animations,
|
required this.animations,
|
||||||
required this.scoreValue,
|
required this.scoreValue,
|
||||||
required this.alertSoundId,
|
required this.alertSound,
|
||||||
required this.attackSoundId,
|
required this.attackSound,
|
||||||
required this.deathSoundId,
|
required this.deathSound,
|
||||||
this.existsInShareware = true,
|
this.existsInShareware = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class Guard extends Enemy {
|
|||||||
required bool Function(int area) isAreaConnectedToPlayer,
|
required bool Function(int area) isAreaConnectedToPlayer,
|
||||||
required void Function(int damage) onDamagePlayer,
|
required void Function(int damage) onDamagePlayer,
|
||||||
required void Function(int x, int y) tryOpenDoor,
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
required void Function(int sfxId) onPlaySound,
|
required void Function(SoundEffect effect) onPlaySound,
|
||||||
}) {
|
}) {
|
||||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||||
double newAngle = angle;
|
double newAngle = angle;
|
||||||
@@ -52,7 +52,7 @@ class Guard extends Enemy {
|
|||||||
areaAt: areaAt,
|
areaAt: areaAt,
|
||||||
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
||||||
)) {
|
)) {
|
||||||
onPlaySound(alertSoundId);
|
onPlaySound(alertSound);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Discrete AI Logic (Decisions happen every 10 tics)
|
// 2. Discrete AI Logic (Decisions happen every 10 tics)
|
||||||
@@ -62,7 +62,7 @@ class Guard extends Enemy {
|
|||||||
if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
|
if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
|
||||||
currentFrame++;
|
currentFrame++;
|
||||||
if (currentFrame == 1) {
|
if (currentFrame == 1) {
|
||||||
onPlaySound(attackSoundId);
|
onPlaySound(attackSound);
|
||||||
if (tryRollRangedHit(
|
if (tryRollRangedHit(
|
||||||
distance: distance,
|
distance: distance,
|
||||||
playerPosition: playerPosition,
|
playerPosition: playerPosition,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class Mutant extends Enemy {
|
|||||||
required bool Function(int area) isAreaConnectedToPlayer,
|
required bool Function(int area) isAreaConnectedToPlayer,
|
||||||
required void Function(int damage) onDamagePlayer,
|
required void Function(int damage) onDamagePlayer,
|
||||||
required void Function(int x, int y) tryOpenDoor,
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
required void Function(int sfxId) onPlaySound,
|
required void Function(SoundEffect effect) onPlaySound,
|
||||||
}) {
|
}) {
|
||||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||||
double newAngle = angle;
|
double newAngle = angle;
|
||||||
@@ -48,7 +48,7 @@ class Mutant extends Enemy {
|
|||||||
areaAt: areaAt,
|
areaAt: areaAt,
|
||||||
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
||||||
)) {
|
)) {
|
||||||
onPlaySound(alertSoundId);
|
onPlaySound(alertSound);
|
||||||
}
|
}
|
||||||
|
|
||||||
double distance = position.distanceTo(playerPosition);
|
double distance = position.distanceTo(playerPosition);
|
||||||
@@ -122,7 +122,7 @@ class Mutant extends Enemy {
|
|||||||
if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
|
if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
|
||||||
currentFrame++;
|
currentFrame++;
|
||||||
if (currentFrame == 1) {
|
if (currentFrame == 1) {
|
||||||
onPlaySound(attackSoundId);
|
onPlaySound(attackSound);
|
||||||
if (tryRollRangedHit(
|
if (tryRollRangedHit(
|
||||||
distance: distance,
|
distance: distance,
|
||||||
playerPosition: playerPosition,
|
playerPosition: playerPosition,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class Officer extends Enemy {
|
|||||||
required bool Function(int area) isAreaConnectedToPlayer,
|
required bool Function(int area) isAreaConnectedToPlayer,
|
||||||
required void Function(int damage) onDamagePlayer,
|
required void Function(int damage) onDamagePlayer,
|
||||||
required void Function(int x, int y) tryOpenDoor,
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
required void Function(int sfxId) onPlaySound,
|
required void Function(SoundEffect effect) onPlaySound,
|
||||||
}) {
|
}) {
|
||||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||||
double newAngle = angle;
|
double newAngle = angle;
|
||||||
@@ -48,7 +48,7 @@ class Officer extends Enemy {
|
|||||||
areaAt: areaAt,
|
areaAt: areaAt,
|
||||||
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
||||||
)) {
|
)) {
|
||||||
onPlaySound(alertSoundId);
|
onPlaySound(alertSound);
|
||||||
}
|
}
|
||||||
|
|
||||||
double distance = position.distanceTo(playerPosition);
|
double distance = position.distanceTo(playerPosition);
|
||||||
@@ -122,7 +122,7 @@ class Officer extends Enemy {
|
|||||||
if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
|
if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
|
||||||
currentFrame++;
|
currentFrame++;
|
||||||
if (currentFrame == 1) {
|
if (currentFrame == 1) {
|
||||||
onPlaySound(attackSoundId);
|
onPlaySound(attackSound);
|
||||||
if (tryRollRangedHit(
|
if (tryRollRangedHit(
|
||||||
distance: distance,
|
distance: distance,
|
||||||
playerPosition: playerPosition,
|
playerPosition: playerPosition,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class SS extends Enemy {
|
|||||||
required bool Function(int area) isAreaConnectedToPlayer,
|
required bool Function(int area) isAreaConnectedToPlayer,
|
||||||
required void Function(int damage) onDamagePlayer,
|
required void Function(int damage) onDamagePlayer,
|
||||||
required void Function(int x, int y) tryOpenDoor,
|
required void Function(int x, int y) tryOpenDoor,
|
||||||
required void Function(int sfxId) onPlaySound,
|
required void Function(SoundEffect effect) onPlaySound,
|
||||||
}) {
|
}) {
|
||||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||||
double newAngle = angle;
|
double newAngle = angle;
|
||||||
@@ -47,7 +47,7 @@ class SS extends Enemy {
|
|||||||
areaAt: areaAt,
|
areaAt: areaAt,
|
||||||
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
isAreaConnectedToPlayer: isAreaConnectedToPlayer,
|
||||||
)) {
|
)) {
|
||||||
onPlaySound(alertSoundId);
|
onPlaySound(alertSound);
|
||||||
}
|
}
|
||||||
|
|
||||||
double distance = position.distanceTo(playerPosition);
|
double distance = position.distanceTo(playerPosition);
|
||||||
@@ -121,7 +121,7 @@ class SS extends Enemy {
|
|||||||
if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
|
if (processTics(elapsedDeltaMs, moveSpeed: 0)) {
|
||||||
currentFrame++;
|
currentFrame++;
|
||||||
if (currentFrame == 1) {
|
if (currentFrame == 1) {
|
||||||
onPlaySound(attackSoundId);
|
onPlaySound(attackSound);
|
||||||
if (tryRollRangedHit(
|
if (tryRollRangedHit(
|
||||||
distance: distance,
|
distance: distance,
|
||||||
playerPosition: playerPosition,
|
playerPosition: playerPosition,
|
||||||
@@ -140,7 +140,7 @@ class SS extends Enemy {
|
|||||||
if (math.Random().nextDouble() > 0.5) {
|
if (math.Random().nextDouble() > 0.5) {
|
||||||
// 50% chance to burst
|
// 50% chance to burst
|
||||||
currentFrame = 1;
|
currentFrame = 1;
|
||||||
onPlaySound(attackSoundId);
|
onPlaySound(attackSound);
|
||||||
if (tryRollRangedHit(
|
if (tryRollRangedHit(
|
||||||
distance: distance,
|
distance: distance,
|
||||||
playerPosition: playerPosition,
|
playerPosition: playerPosition,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import 'package:wolf_3d_dart/src/registry/modules/sfx_module.dart';
|
|||||||
/// Access version-specific asset IDs through the five typed modules:
|
/// Access version-specific asset IDs through the five typed modules:
|
||||||
///
|
///
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// data.registry.sfx.resolve(SfxKey.pistolFire)
|
/// data.registry.sfx.resolve(SoundEffect.pistolFire)
|
||||||
/// data.registry.music.musicForLevel(episode, level)
|
/// data.registry.music.musicForLevel(episode, level)
|
||||||
/// data.registry.entities.resolve(EntityKey.guard)
|
/// data.registry.entities.resolve(EntityKey.guard)
|
||||||
/// data.registry.hud.faceForHealth(player.health)
|
/// data.registry.hud.faceForHealth(player.health)
|
||||||
|
|||||||
@@ -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,9 +1,10 @@
|
|||||||
|
import 'package:wolf_3d_dart/src/data_types/game_version.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/asset_registry.dart';
|
import 'package:wolf_3d_dart/src/registry/asset_registry.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/built_in/built_in_music_module.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/built_in/built_in_sfx_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/retail_entity_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/retail_entity_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/retail_hud_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/retail_hud_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/retail_menu_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/retail_menu_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/retail_music_module.dart';
|
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/retail_sfx_module.dart';
|
|
||||||
|
|
||||||
/// The canonical [AssetRegistry] for all retail Wolf3D releases.
|
/// The canonical [AssetRegistry] for all retail Wolf3D releases.
|
||||||
///
|
///
|
||||||
@@ -13,8 +14,8 @@ import 'package:wolf_3d_dart/src/registry/built_in/retail_sfx_module.dart';
|
|||||||
class RetailAssetRegistry extends AssetRegistry {
|
class RetailAssetRegistry extends AssetRegistry {
|
||||||
RetailAssetRegistry()
|
RetailAssetRegistry()
|
||||||
: super(
|
: super(
|
||||||
sfx: const RetailSfxModule(),
|
sfx: const BuiltInSfxModule(GameVersion.retail),
|
||||||
music: const RetailMusicModule(),
|
music: const BuiltInMusicModule(GameVersion.retail),
|
||||||
entities: const RetailEntityModule(),
|
entities: const RetailEntityModule(),
|
||||||
hud: const RetailHudModule(),
|
hud: const RetailHudModule(),
|
||||||
menu: const RetailMenuPicModule(),
|
menu: const RetailMenuPicModule(),
|
||||||
|
|||||||
@@ -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/asset_registry.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/retail_sfx_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/built_in_music_module.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/built_in/built_in_sfx_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/shareware_entity_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/shareware_entity_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/shareware_hud_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/shareware_hud_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/shareware_menu_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/shareware_menu_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/shareware_music_module.dart';
|
|
||||||
|
|
||||||
/// The [AssetRegistry] for the Wolfenstein 3D v1.4 Shareware release.
|
/// The [AssetRegistry] for the Wolfenstein 3D v1.4 Shareware release.
|
||||||
///
|
///
|
||||||
/// - SFX slots are identical to retail (same AUDIOT layout).
|
/// - SFX slots are resolved through version-aware [SoundEffect.idFor].
|
||||||
/// - Music routing uses the 10-level shareware table.
|
/// - Music routing uses [Music.levelFor] for the 10-level shareware table.
|
||||||
/// - Entity definitions are limited to the three shareware enemies.
|
/// - Entity definitions are limited to the three shareware enemies.
|
||||||
/// - HUD indices are shareware-aware and offset from retail layout.
|
/// - HUD indices are shareware-aware and offset from retail layout.
|
||||||
/// - Menu picture indices are resolved via runtime heuristic offset; call
|
/// - Menu picture indices are resolved via runtime heuristic offset; call
|
||||||
@@ -17,8 +18,8 @@ import 'package:wolf_3d_dart/src/registry/built_in/shareware_music_module.dart';
|
|||||||
class SharewareAssetRegistry extends AssetRegistry {
|
class SharewareAssetRegistry extends AssetRegistry {
|
||||||
SharewareAssetRegistry({bool strictOriginalShareware = false})
|
SharewareAssetRegistry({bool strictOriginalShareware = false})
|
||||||
: super(
|
: super(
|
||||||
sfx: const RetailSfxModule(),
|
sfx: const BuiltInSfxModule(GameVersion.shareware),
|
||||||
music: const SharewareMusicModule(),
|
music: const BuiltInMusicModule(GameVersion.shareware),
|
||||||
entities: const SharewareEntityModule(),
|
entities: const SharewareEntityModule(),
|
||||||
hud: SharewareHudModule(
|
hud: SharewareHudModule(
|
||||||
useOriginalWl1Map: strictOriginalShareware,
|
useOriginalWl1Map: strictOriginalShareware,
|
||||||
|
|||||||
@@ -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
|
/// The resolved reference for a music track: a numeric index into
|
||||||
/// [WolfensteinData.music].
|
/// [WolfensteinData.music].
|
||||||
@@ -19,10 +19,10 @@ class MusicRoute {
|
|||||||
abstract class MusicModule {
|
abstract class MusicModule {
|
||||||
const MusicModule();
|
const MusicModule();
|
||||||
|
|
||||||
/// Resolves a named [MusicKey] to a [MusicRoute].
|
/// Resolves a named [Music] to a [MusicRoute].
|
||||||
///
|
///
|
||||||
/// Returns `null` if the key is not supported by this module.
|
/// Returns `null` if the key is not supported by this module.
|
||||||
MusicRoute? resolve(MusicKey key);
|
MusicRoute? resolve(Music key);
|
||||||
|
|
||||||
/// Resolves the level music track for a given [episodeIndex] and
|
/// Resolves the level music track for a given [episodeIndex] and
|
||||||
/// zero-based [levelIndex].
|
/// zero-based [levelIndex].
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:wolf_3d_dart/src/registry/keys/sfx_key.dart';
|
import 'package:wolf_3d_dart/src/registry/keys/sound_effect.dart';
|
||||||
|
|
||||||
/// The resolved reference for a sound effect: a numeric slot index into
|
/// The resolved reference for a sound effect: a numeric slot index into
|
||||||
/// [WolfensteinData.sounds].
|
/// [WolfensteinData.sounds].
|
||||||
@@ -11,7 +11,7 @@ class SoundAssetRef {
|
|||||||
String toString() => 'SoundAssetRef($slotIndex)';
|
String toString() => 'SoundAssetRef($slotIndex)';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Owns the mapping from symbolic [SfxKey] identifiers to numeric sound slots.
|
/// Owns the mapping from symbolic [SoundEffect] identifiers to numeric sound slots.
|
||||||
///
|
///
|
||||||
/// Implement this class to provide a custom sound layout for a modded or
|
/// Implement this class to provide a custom sound layout for a modded or
|
||||||
/// alternate game version. Pass the implementation inside a custom
|
/// alternate game version. Pass the implementation inside a custom
|
||||||
@@ -22,5 +22,5 @@ abstract class SfxModule {
|
|||||||
/// Resolves [key] to a [SoundAssetRef] containing the numeric slot index.
|
/// Resolves [key] to a [SoundAssetRef] containing the numeric slot index.
|
||||||
///
|
///
|
||||||
/// Returns `null` if the key is not supported by this module.
|
/// Returns `null` if the key is not supported by this module.
|
||||||
SoundAssetRef? resolve(SfxKey key);
|
SoundAssetRef? resolve(SoundEffect key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class WolfAudio implements EngineAudio {
|
|||||||
for (int i = 0; i < 50; i++) {
|
for (int i = 0; i < 50; i++) {
|
||||||
Future.delayed(Duration(seconds: i * 2), () {
|
Future.delayed(Duration(seconds: i * 2), () {
|
||||||
log("[AUDIO] Testing Sound ID: $i");
|
log("[AUDIO] Testing Sound ID: $i");
|
||||||
playSoundEffect(i);
|
playSoundEffectId(i);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,16 +122,21 @@ class WolfAudio implements EngineAudio {
|
|||||||
@override
|
@override
|
||||||
Future<void> playMenuMusic() async {
|
Future<void> playMenuMusic() async {
|
||||||
final data = activeGame;
|
final data = activeGame;
|
||||||
if (data == null || data.music.length <= 1) return;
|
final trackIndex = data == null
|
||||||
await playMusic(data.music[1]);
|
? null
|
||||||
|
: Music.menuTheme.trackIndexFor(data.version);
|
||||||
|
if (data == null || trackIndex == null || trackIndex >= data.music.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await playMusic(data.music[trackIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> playLevelMusic(WolfLevel level) async {
|
Future<void> playLevelMusic(Music music) async {
|
||||||
final data = activeGame;
|
final data = activeGame;
|
||||||
if (data == null || data.music.isEmpty) return;
|
if (data == null || data.music.isEmpty) return;
|
||||||
|
|
||||||
final index = level.musicIndex;
|
final index = music.trackIndexFor(data.version) ?? 0;
|
||||||
if (index < data.music.length) {
|
if (index < data.music.length) {
|
||||||
await playMusic(data.music[index]);
|
await playMusic(data.music[index]);
|
||||||
} else {
|
} else {
|
||||||
@@ -144,14 +149,24 @@ class WolfAudio implements EngineAudio {
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> playSoundEffect(int sfxId) async {
|
Future<void> playSoundEffect(SoundEffect effect) async {
|
||||||
|
final data = activeGame;
|
||||||
|
if (data == null) return;
|
||||||
|
await playSoundEffectId(effect.idFor(data.version));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> playSoundEffectId(int sfxId) async {
|
||||||
log("[AUDIO] Playing sfx id $sfxId");
|
log("[AUDIO] Playing sfx id $sfxId");
|
||||||
// The original engine uses a specific starting chunk for digitized sounds.
|
// The original engine uses a specific starting chunk for digitized sounds.
|
||||||
// In many loaders, the 'sounds' list is already just the digitized ones.
|
// In many loaders, the 'sounds' list is already just the digitized ones.
|
||||||
// If your list contains EVERYTHING, you need to add the offset (174).
|
// If your list contains EVERYTHING, you need to add the offset (174).
|
||||||
// If it's JUST digitized sounds, sfxId should work directly.
|
// If it's JUST digitized sounds, sfxId should work directly.
|
||||||
|
|
||||||
final soundsList = activeGame!.sounds;
|
final data = activeGame;
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
final soundsList = data.sounds;
|
||||||
if (sfxId < 0 || sfxId >= soundsList.length) return;
|
if (sfxId < 0 || sfxId >= soundsList.length) return;
|
||||||
|
|
||||||
final raw8bitBytes = soundsList[sfxId].bytes;
|
final raw8bitBytes = soundsList[sfxId].bytes;
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ export 'src/data_types/game_file.dart' show GameFile;
|
|||||||
export 'src/data_types/game_version.dart' show GameVersion;
|
export 'src/data_types/game_version.dart' show GameVersion;
|
||||||
export 'src/data_types/image.dart' show VgaImage;
|
export 'src/data_types/image.dart' show VgaImage;
|
||||||
export 'src/data_types/map_objects.dart' show MapObject;
|
export 'src/data_types/map_objects.dart' show MapObject;
|
||||||
export 'src/data_types/sound.dart'
|
export 'src/data_types/sound.dart' show PcmSound, ImfMusic, ImfInstruction;
|
||||||
show PcmSound, ImfMusic, ImfInstruction, WolfMusicMap, WolfSound;
|
|
||||||
export 'src/data_types/sprite.dart' hide Matrix;
|
export 'src/data_types/sprite.dart' hide Matrix;
|
||||||
export 'src/data_types/sprite_frame_range.dart' show SpriteFrameRange;
|
export 'src/data_types/sprite_frame_range.dart' show SpriteFrameRange;
|
||||||
export 'src/data_types/wolf_level.dart' show WolfLevel;
|
export 'src/data_types/wolf_level.dart' show WolfLevel;
|
||||||
@@ -34,8 +33,8 @@ export 'src/registry/built_in/shareware_asset_registry.dart'
|
|||||||
export 'src/registry/keys/entity_key.dart' show EntityKey;
|
export 'src/registry/keys/entity_key.dart' show EntityKey;
|
||||||
export 'src/registry/keys/hud_key.dart' show HudKey;
|
export 'src/registry/keys/hud_key.dart' show HudKey;
|
||||||
export 'src/registry/keys/menu_pic_key.dart' show MenuPicKey;
|
export 'src/registry/keys/menu_pic_key.dart' show MenuPicKey;
|
||||||
export 'src/registry/keys/music_key.dart' show MusicKey;
|
export 'src/registry/keys/music.dart' show Music;
|
||||||
export 'src/registry/keys/sfx_key.dart' show SfxKey;
|
export 'src/registry/keys/sound_effect.dart' show SoundEffect;
|
||||||
export 'src/registry/modules/entity_asset_module.dart'
|
export 'src/registry/modules/entity_asset_module.dart'
|
||||||
show EntityAssetModule, EntityAssetDefinition;
|
show EntityAssetModule, EntityAssetDefinition;
|
||||||
export 'src/registry/modules/hud_module.dart' show HudModule, HudAssetRef;
|
export 'src/registry/modules/hud_module.dart' show HudModule, HudAssetRef;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
engine.tick(const Duration(milliseconds: 16));
|
engine.tick(const Duration(milliseconds: 16));
|
||||||
|
|
||||||
expect(audio.sfxIds, contains(WolfSound.getAmmo));
|
expect(audio.sfxIds, contains(SoundEffect.getAmmo));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('plays guard alert when guard notices player', () {
|
test('plays guard alert when guard notices player', () {
|
||||||
@@ -47,7 +47,7 @@ void main() {
|
|||||||
engine.tick(const Duration(milliseconds: 16));
|
engine.tick(const Duration(milliseconds: 16));
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(audio.sfxIds, contains(WolfSound.guardHalt));
|
expect(audio.sfxIds, contains(SoundEffect.guardHalt));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('plays dog death sound when dog dies', () {
|
test('plays dog death sound when dog dies', () {
|
||||||
@@ -69,7 +69,7 @@ void main() {
|
|||||||
|
|
||||||
engine.tick(const Duration(milliseconds: 16));
|
engine.tick(const Duration(milliseconds: 16));
|
||||||
|
|
||||||
expect(audio.sfxIds, contains(WolfSound.dogDeath));
|
expect(audio.sfxIds, contains(SoundEffect.dogDeath));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('plays pushwall sound when triggering a secret wall', () {
|
test('plays pushwall sound when triggering a secret wall', () {
|
||||||
@@ -90,7 +90,7 @@ void main() {
|
|||||||
engine.init();
|
engine.init();
|
||||||
engine.tick(const Duration(milliseconds: 16));
|
engine.tick(const Duration(milliseconds: 16));
|
||||||
|
|
||||||
expect(audio.sfxIds, contains(WolfSound.pushWall));
|
expect(audio.sfxIds, contains(SoundEffect.pushWall));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ WolfEngine _buildEngine({
|
|||||||
wallGrid: wallGrid,
|
wallGrid: wallGrid,
|
||||||
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||||
objectGrid: objectGrid,
|
objectGrid: objectGrid,
|
||||||
musicIndex: 0,
|
music: Music.level01,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -159,7 +159,7 @@ class _CapturingAudio implements EngineAudio {
|
|||||||
@override
|
@override
|
||||||
WolfensteinData? activeGame;
|
WolfensteinData? activeGame;
|
||||||
|
|
||||||
final List<int> sfxIds = [];
|
final List<SoundEffect> sfxIds = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> debugSoundTest() async {}
|
Future<void> debugSoundTest() async {}
|
||||||
@@ -168,14 +168,24 @@ class _CapturingAudio implements EngineAudio {
|
|||||||
Future<void> init() async {}
|
Future<void> init() async {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void playLevelMusic(WolfLevel level) {}
|
void playLevelMusic(Music music) {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void playMenuMusic() {}
|
void playMenuMusic() {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void playSoundEffect(int sfxId) {
|
void playSoundEffect(SoundEffect effect) {
|
||||||
sfxIds.add(sfxId);
|
sfxIds.add(effect);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playSoundEffectId(int sfxId) {
|
||||||
|
final effect = SoundEffect.values
|
||||||
|
.where((entry) => entry.retailId == sfxId)
|
||||||
|
.firstOrNull;
|
||||||
|
if (effect != null) {
|
||||||
|
sfxIds.add(effect);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ WolfEngine _buildEngine() {
|
|||||||
wallGrid: wallGrid,
|
wallGrid: wallGrid,
|
||||||
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||||
objectGrid: objectGrid,
|
objectGrid: objectGrid,
|
||||||
musicIndex: 0,
|
music: Music.level01,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -119,13 +119,16 @@ class _SilentAudio implements EngineAudio {
|
|||||||
Future<void> init() async {}
|
Future<void> init() async {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void playLevelMusic(WolfLevel level) {}
|
void playLevelMusic(Music music) {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void playMenuMusic() {}
|
void playMenuMusic() {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void playSoundEffect(int sfxId) {}
|
void playSoundEffect(SoundEffect effect) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playSoundEffectId(int sfxId) {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void stopMusic() {}
|
void stopMusic() {}
|
||||||
|
|||||||
@@ -365,14 +365,14 @@ WolfensteinData _buildTestData({required GameVersion gameVersion}) {
|
|||||||
wallGrid: levelOneWalls,
|
wallGrid: levelOneWalls,
|
||||||
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||||
objectGrid: levelOneObjects,
|
objectGrid: levelOneObjects,
|
||||||
musicIndex: 0,
|
music: Music.level01,
|
||||||
),
|
),
|
||||||
WolfLevel(
|
WolfLevel(
|
||||||
name: 'Level 2',
|
name: 'Level 2',
|
||||||
wallGrid: levelTwoWalls,
|
wallGrid: levelTwoWalls,
|
||||||
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||||
objectGrid: levelTwoObjects,
|
objectGrid: levelTwoObjects,
|
||||||
musicIndex: 1,
|
music: Music.level02,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -396,13 +396,16 @@ class _SilentAudio implements EngineAudio {
|
|||||||
Future<void> init() async {}
|
Future<void> init() async {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void playLevelMusic(WolfLevel level) {}
|
void playLevelMusic(Music music) {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void playMenuMusic() {}
|
void playMenuMusic() {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void playSoundEffect(int sfxId) {}
|
void playSoundEffect(SoundEffect effect) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playSoundEffectId(int sfxId) {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void stopMusic() {}
|
void stopMusic() {}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ void main() {
|
|||||||
wallGrid: wallGrid,
|
wallGrid: wallGrid,
|
||||||
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||||
objectGrid: objectGrid,
|
objectGrid: objectGrid,
|
||||||
musicIndex: 0,
|
music: Music.level01,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ void main() {
|
|||||||
wallGrid: wallGrid,
|
wallGrid: wallGrid,
|
||||||
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||||
objectGrid: objectGrid,
|
objectGrid: objectGrid,
|
||||||
musicIndex: 0,
|
music: Music.level01,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ class FlutterAudioAdapter implements EngineAudio {
|
|||||||
FlutterAudioAdapter(this.wolf3d);
|
FlutterAudioAdapter(this.wolf3d);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void playLevelMusic(WolfLevel level) {
|
void playLevelMusic(Music music) {
|
||||||
wolf3d.audio.playLevelMusic(level);
|
wolf3d.audio.playLevelMusic(music);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -23,8 +23,13 @@ class FlutterAudioAdapter implements EngineAudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void playSoundEffect(int sfxId) {
|
void playSoundEffect(SoundEffect effect) {
|
||||||
wolf3d.audio.playSoundEffect(sfxId);
|
wolf3d.audio.playSoundEffect(effect);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playSoundEffectId(int sfxId) {
|
||||||
|
wolf3d.audio.playSoundEffectId(sfxId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
Reference in New Issue
Block a user