From fcda0f9ff49cf758f1956686d39e9671246c313e Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Thu, 19 Mar 2026 13:45:19 +0100 Subject: [PATCH] Add built-in asset modules for Wolfenstein 3D v1.4 Shareware release - Implement RetailSfxModule to map sound effects to numeric slots. - Create SharewareAssetRegistry to manage assets for the Shareware version. - Introduce SharewareEntityModule to define available enemies in Shareware. - Add SharewareMenuPicModule to handle menu pictures with runtime offset computation. - Implement SharewareMusicModule for music routing in Shareware. - Define keys for entities, HUD elements, menu pictures, and music tracks. - Create abstract modules for entity, HUD, menu picture, and music assets. - Add registry resolver to select appropriate asset registry based on game version and data version. - Update WolfensteinData to include new asset registry exports. - Modify tests to utilize the new asset registry structure for Shareware and Retail versions. Signed-off-by: Hans Kokx --- .../lib/src/data/io/discovery_io.dart | 1 - .../wolf_3d_dart/lib/src/data/wl_parser.dart | 110 +++++++++++++++-- .../lib/src/data/wolfenstein_loader.dart | 14 ++- .../lib/src/data_types/enemy_animation.dart | 81 +++++++++++++ .../lib/src/data_types/wolfenstein_data.dart | 23 ++++ .../src/entities/entities/enemies/dog.dart | 1 - .../entities/enemies/enemy_animation.dart | 103 +--------------- .../entities/entities/enemies/enemy_type.dart | 1 - .../src/entities/entities/enemies/guard.dart | 1 - .../src/entities/entities/enemies/mutant.dart | 1 - .../entities/entities/enemies/officer.dart | 1 - .../lib/src/entities/entities/enemies/ss.dart | 1 - .../lib/src/registry/asset_registry.dart | 45 +++++++ .../built_in/retail_asset_registry.dart | 22 ++++ .../built_in/retail_entity_module.dart | 79 +++++++++++++ .../registry/built_in/retail_hud_module.dart | 65 ++++++++++ .../registry/built_in/retail_menu_module.dart | 76 ++++++++++++ .../built_in/retail_music_module.dart | 62 ++++++++++ .../registry/built_in/retail_sfx_module.dart | 67 +++++++++++ .../built_in/shareware_asset_registry.dart | 29 +++++ .../built_in/shareware_entity_module.dart | 56 +++++++++ .../built_in/shareware_menu_module.dart | 111 ++++++++++++++++++ .../built_in/shareware_music_module.dart | 40 +++++++ .../lib/src/registry/keys/entity_key.dart | 39 ++++++ .../lib/src/registry/keys/hud_key.dart | 54 +++++++++ .../lib/src/registry/keys/menu_pic_key.dart | 55 +++++++++ .../lib/src/registry/keys/music_key.dart | 51 ++++++++ .../lib/src/registry/keys/sfx_key.dart | 77 ++++++++++++ .../registry/modules/entity_asset_module.dart | 37 ++++++ .../lib/src/registry/modules/hud_module.dart | 33 ++++++ .../src/registry/modules/menu_pic_module.dart | 34 ++++++ .../src/registry/modules/music_module.dart | 36 ++++++ .../lib/src/registry/modules/sfx_module.dart | 26 ++++ .../lib/src/registry/registry_resolver.dart | 68 +++++++++++ .../wolf_3d_dart/lib/wolf_3d_data_types.dart | 24 ++++ .../test/engine/audio_events_test.dart | 2 + .../rasterizer/pushwall_rasterizer_test.dart | 2 + .../rendering/pushwall_renderer_test.dart | 2 + 38 files changed, 1411 insertions(+), 119 deletions(-) create mode 100644 packages/wolf_3d_dart/lib/src/data_types/enemy_animation.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/asset_registry.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/built_in/retail_asset_registry.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/built_in/retail_entity_module.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/built_in/retail_hud_module.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/built_in/retail_menu_module.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/built_in/retail_music_module.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/built_in/retail_sfx_module.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/built_in/shareware_asset_registry.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/built_in/shareware_entity_module.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/built_in/shareware_menu_module.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/built_in/shareware_music_module.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/keys/entity_key.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/keys/hud_key.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/keys/menu_pic_key.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/keys/music_key.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/keys/sfx_key.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/modules/entity_asset_module.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/modules/hud_module.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/modules/menu_pic_module.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/modules/music_module.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/modules/sfx_module.dart create mode 100644 packages/wolf_3d_dart/lib/src/registry/registry_resolver.dart diff --git a/packages/wolf_3d_dart/lib/src/data/io/discovery_io.dart b/packages/wolf_3d_dart/lib/src/data/io/discovery_io.dart index 9207884..4b313ba 100644 --- a/packages/wolf_3d_dart/lib/src/data/io/discovery_io.dart +++ b/packages/wolf_3d_dart/lib/src/data/io/discovery_io.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:crypto/crypto.dart'; -import 'package:wolf_3d_dart/src/data/data_version.dart'; import 'package:wolf_3d_dart/src/data/wl_parser.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; diff --git a/packages/wolf_3d_dart/lib/src/data/wl_parser.dart b/packages/wolf_3d_dart/lib/src/data/wl_parser.dart index 41db34d..200754e 100644 --- a/packages/wolf_3d_dart/lib/src/data/wl_parser.dart +++ b/packages/wolf_3d_dart/lib/src/data/wl_parser.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:crypto/crypto.dart' show md5; -import 'package:wolf_3d_dart/src/data/data_version.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; /// The primary parser for Wolfenstein 3D data formats. @@ -99,6 +98,70 @@ abstract class WLParser { ); } + /// Async file-by-file equivalent of [loadAsync]; exposed for use from + /// external callers that need a [registryOverride] in async contexts. + static Future loadAsyncWithOverride( + Future Function(String filename) fileFetcher, { + AssetRegistry? registryOverride, + }) async { + GameVersion? detectedVersion; + ByteData? vswap; + + for (final version in GameVersion.values) { + try { + vswap = await fileFetcher('VSWAP.${version.fileExtension}'); + detectedVersion = version; + break; + } catch (_) {} + } + + if (detectedVersion == null || vswap == null) { + throw Exception('Could not locate a valid VSWAP file.'); + } + + final ext = detectedVersion.fileExtension; + final vswapBytes = vswap.buffer.asUint8List( + vswap.offsetInBytes, + vswap.lengthInBytes, + ); + final vswapHash = md5.convert(vswapBytes).toString(); + final dataIdentity = DataVersion.fromChecksum(vswapHash); + + ByteData gameMapsData; + if (dataIdentity == DataVersion.version10Retail) { + try { + gameMapsData = await fileFetcher('MAPTEMP.$ext'); + } catch (_) { + gameMapsData = await fileFetcher('GAMEMAPS.$ext'); + } + } else { + gameMapsData = await fileFetcher('GAMEMAPS.$ext'); + } + + final rawFiles = { + 'MAPHEAD.$ext': await fileFetcher('MAPHEAD.$ext'), + 'VGADICT.$ext': await fileFetcher('VGADICT.$ext'), + 'VGAHEAD.$ext': await fileFetcher('VGAHEAD.$ext'), + 'VGAGRAPH.$ext': await fileFetcher('VGAGRAPH.$ext'), + 'AUDIOHED.$ext': await fileFetcher('AUDIOHED.$ext'), + 'AUDIOT.$ext': await fileFetcher('AUDIOT.$ext'), + }; + + return load( + version: detectedVersion, + dataIdentity: dataIdentity, + vswap: vswap, + mapHead: rawFiles['MAPHEAD.$ext']!, + gameMaps: gameMapsData, + vgaDict: rawFiles['VGADICT.$ext']!, + vgaHead: rawFiles['VGAHEAD.$ext']!, + vgaGraph: rawFiles['VGAGRAPH.$ext']!, + audioHed: rawFiles['AUDIOHED.$ext']!, + audioT: rawFiles['AUDIOT.$ext']!, + registryOverride: registryOverride, + ); + } + /// Parses all raw ByteData upfront and returns a fully populated [WolfensteinData] object. /// /// By using named parameters, the compiler guarantees no files are missing or misnamed. @@ -115,28 +178,61 @@ abstract class WLParser { required ByteData audioHed, required ByteData audioT, required DataVersion dataIdentity, + AssetRegistry? registryOverride, }) { final isShareware = version == GameVersion.shareware; - // v1.0/1.1 used different HUD strings and had different secret wall bugs - final isLegacy = - dataIdentity == DataVersion.version10Retail || - dataIdentity == DataVersion.version11Retail; - final audio = parseAudio(audioHed, audioT, version); + final vgaImages = parseVgaImages(vgaDict, vgaHead, vgaGraph); + + // Resolve the appropriate registry for this data identity, unless the + // caller has supplied an explicit override (e.g. a modded asset pack). + final registry = + registryOverride ?? + _resolveRegistry( + version: version, + dataVersion: dataIdentity, + vgaImages: vgaImages, + ); return WolfensteinData( version: version, + dataVersion: dataIdentity, + registry: registry, walls: parseWalls(vswap), sprites: parseSprites(vswap), sounds: parseSounds(vswap).map((bytes) => PcmSound(bytes)).toList(), episodes: parseEpisodes(mapHead, gameMaps, isShareware: isShareware), - vgaImages: parseVgaImages(vgaDict, vgaHead, vgaGraph), + vgaImages: vgaImages, adLibSounds: audio.adLib, music: audio.music, ); } + /// Selects the registry for [version]/[dataVersion] and, for shareware, + /// initialises the menu module's runtime image-offset heuristic. + static AssetRegistry _resolveRegistry({ + required GameVersion version, + required DataVersion dataVersion, + required List vgaImages, + }) { + final context = RegistrySelectionContext( + gameVersion: version, + dataVersion: dataVersion, + ); + final registry = const BuiltInAssetRegistryResolver().resolve(context); + + // Initialise the shareware menu heuristic now that images are available. + if (registry is SharewareAssetRegistry) { + final sizes = vgaImages + .map((img) => (width: img.width, height: img.height)) + .toList(); + registry.sharewareMenu.initWithImageSizes(sizes); + } + + return registry; + } + /// Extracts the 64x64 wall textures from the VSWAP file. /// /// Wall textures are stored sequentially at the beginning of the chunks array. diff --git a/packages/wolf_3d_dart/lib/src/data/wolfenstein_loader.dart b/packages/wolf_3d_dart/lib/src/data/wolfenstein_loader.dart index be1c379..2b451a5 100644 --- a/packages/wolf_3d_dart/lib/src/data/wolfenstein_loader.dart +++ b/packages/wolf_3d_dart/lib/src/data/wolfenstein_loader.dart @@ -1,7 +1,6 @@ import 'dart:typed_data'; -import 'package:crypto/crypto.dart'; // Import for MD5 -import 'package:wolf_3d_dart/src/data/data_version.dart'; // Import your enum +import 'package:crypto/crypto.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'io/discovery_stub.dart' @@ -33,6 +32,10 @@ class WolfensteinLoader { /// Use this for Web or Flutter assets where automated directory scanning /// is not possible. It automatically detects the [DataVersion] (v1.0, v1.4, etc.) /// by hashing the [vswap] buffer. + /// + /// Supply [registryOverride] to use a fully custom [AssetRegistry] instead + /// of the built-in version-detected registry. This is the primary extension + /// point for modded or custom asset packs. static WolfensteinData loadFromBytes({ required GameVersion version, required ByteData? vswap, @@ -43,6 +46,7 @@ class WolfensteinLoader { required ByteData? vgaGraph, required ByteData? audioHed, required ByteData? audioT, + AssetRegistry? registryOverride, }) { // 1. Validation Check final Map files = { @@ -67,7 +71,7 @@ class WolfensteinLoader { ); } - // 2. Identify identity for specialized parsing (e.g., MAPTEMP vs GAMEMAPS) + // 2. Identify identity for specialised parsing (e.g., MAPTEMP vs GAMEMAPS). final vswapBytes = vswap!.buffer.asUint8List( vswap.offsetInBytes, vswap.lengthInBytes, @@ -75,10 +79,9 @@ class WolfensteinLoader { final hash = md5.convert(vswapBytes).toString(); final dataIdentity = DataVersion.fromChecksum(hash); - // 3. Pass-through to parser with the detected identity + // 3. Pass-through to parser with the detected identity and optional override. return WLParser.load( version: version, - // Correctly identifies v1.0/1.1/1.4 dataIdentity: dataIdentity, vswap: vswap, mapHead: mapHead!, @@ -88,6 +91,7 @@ class WolfensteinLoader { vgaGraph: vgaGraph!, audioHed: audioHed!, audioT: audioT!, + registryOverride: registryOverride, ); } } diff --git a/packages/wolf_3d_dart/lib/src/data_types/enemy_animation.dart b/packages/wolf_3d_dart/lib/src/data_types/enemy_animation.dart new file mode 100644 index 0000000..d39671e --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/data_types/enemy_animation.dart @@ -0,0 +1,81 @@ +import 'package:wolf_3d_dart/src/data_types/sprite_frame_range.dart'; + +/// The set of animation states an enemy can be in. +enum EnemyAnimation { idle, walking, attacking, pain, dying, dead } + +/// A mapping container that defines the sprite index ranges for an enemy's life cycle. +/// +/// Wolfenstein 3D enemies use specific ranges within the global sprite list +/// to handle state transitions like walking, attacking, or dying. +class EnemyAnimationMap { + final SpriteFrameRange idle; + final SpriteFrameRange walking; + final SpriteFrameRange attacking; + final SpriteFrameRange pain; + final SpriteFrameRange dying; + final SpriteFrameRange dead; + + const EnemyAnimationMap({ + required this.idle, + required this.walking, + required this.attacking, + required this.pain, + required this.dying, + required this.dead, + }); + + /// Identifies which [EnemyAnimation] state a specific [spriteIndex] belongs to. + /// + /// Returns `null` if the index is outside this enemy's defined animation ranges. + EnemyAnimation? getAnimation(int spriteIndex) { + if (idle.contains(spriteIndex)) return EnemyAnimation.idle; + if (walking.contains(spriteIndex)) return EnemyAnimation.walking; + if (attacking.contains(spriteIndex)) return EnemyAnimation.attacking; + if (pain.contains(spriteIndex)) return EnemyAnimation.pain; + if (dying.contains(spriteIndex)) return EnemyAnimation.dying; + if (dead.contains(spriteIndex)) return EnemyAnimation.dead; + return null; + } + + /// All animation states mapped to their corresponding [SpriteFrameRange]. + Map get allRanges => { + EnemyAnimation.idle: idle, + EnemyAnimation.walking: walking, + EnemyAnimation.attacking: attacking, + EnemyAnimation.pain: pain, + EnemyAnimation.dying: dying, + EnemyAnimation.dead: dead, + }; + + /// Returns the [SpriteFrameRange] associated with a specific [animation] state. + SpriteFrameRange getRange(EnemyAnimation animation) { + return switch (animation) { + EnemyAnimation.idle => idle, + EnemyAnimation.walking => walking, + EnemyAnimation.attacking => attacking, + EnemyAnimation.pain => pain, + EnemyAnimation.dying => dying, + EnemyAnimation.dead => dead, + }; + } + + /// Checks if any animation range in this map overlaps with any range in [other]. + bool overlapsWith(EnemyAnimationMap other) { + final myRanges = [idle, walking, attacking, pain, dying, dead]; + final otherRanges = [ + other.idle, + other.walking, + other.attacking, + other.pain, + other.dying, + other.dead, + ]; + + for (final mine in myRanges) { + for (final theirs in otherRanges) { + if (mine.overlaps(theirs)) return true; + } + } + return false; + } +} diff --git a/packages/wolf_3d_dart/lib/src/data_types/wolfenstein_data.dart b/packages/wolf_3d_dart/lib/src/data_types/wolfenstein_data.dart index c1be21d..13b6f40 100644 --- a/packages/wolf_3d_dart/lib/src/data_types/wolfenstein_data.dart +++ b/packages/wolf_3d_dart/lib/src/data_types/wolfenstein_data.dart @@ -6,10 +6,31 @@ import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; /// This object holds the extracted textures, sounds, music, and levels, /// serving as the primary data source for the game engine's renderer /// and audio system. +/// +/// The [registry] field provides version-aware symbolic resolution of all +/// asset IDs (sprites, sounds, music, HUD, menus). Consumers should prefer +/// registry lookups over hard-coded numeric indices. class WolfensteinData { /// The version of the game these assets belong to (e.g., Shareware or Retail). final GameVersion version; + /// The exact data-file identity determined from the VSWAP MD5 checksum. + /// + /// Use this when you need to distinguish, for example, v1.0 from v1.4 retail. + final DataVersion dataVersion; + + /// The modular asset registry that owns all version-sensitive numeric IDs. + /// + /// Access the five sub-modules via: + /// ```dart + /// data.registry.sfx.resolve(SfxKey.pistolFire) + /// data.registry.music.musicForLevel(episode, level) + /// data.registry.entities.resolve(EntityKey.guard) + /// data.registry.hud.faceForHealth(player.health) + /// data.registry.menu.resolve(MenuPicKey.title) + /// ``` + final AssetRegistry registry; + /// The collection of 64x64 wall textures extracted from VSWAP. final List walls; @@ -33,6 +54,8 @@ class WolfensteinData { const WolfensteinData({ required this.version, + required this.dataVersion, + required this.registry, required this.walls, required this.sprites, required this.sounds, diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/dog.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/dog.dart index c0a9362..f427c0d 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/dog.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/dog.dart @@ -1,7 +1,6 @@ import 'dart:math' as math; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart'; -import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart'; import 'package:wolf_3d_dart/src/entities/entity.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy_animation.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy_animation.dart index 818f402..0356479 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy_animation.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy_animation.dart @@ -1,100 +1,3 @@ -import 'package:wolf_3d_dart/src/data_types/sprite_frame_range.dart'; - -enum EnemyAnimation { idle, walking, attacking, pain, dying, dead } - -/// A mapping container that defines the sprite index ranges for an enemy's life cycle. -/// -/// Wolfenstein 3D enemies use specific ranges within the global sprite list -/// to handle state transitions like walking, attacking, or dying. -class EnemyAnimationMap { - final SpriteFrameRange idle; - final SpriteFrameRange walking; - final SpriteFrameRange attacking; - final SpriteFrameRange pain; - final SpriteFrameRange dying; - final SpriteFrameRange dead; - - const EnemyAnimationMap({ - required this.idle, - required this.walking, - required this.attacking, - required this.pain, - required this.dying, - required this.dead, - }); - - /// Identifies which [EnemyAnimation] state a specific [spriteIndex] belongs to. - /// - /// Returns `null` if the index is outside this enemy's defined animation ranges. - EnemyAnimation? getAnimation(int spriteIndex) { - if (idle.contains(spriteIndex)) return EnemyAnimation.idle; - if (walking.contains(spriteIndex)) return EnemyAnimation.walking; - if (attacking.contains(spriteIndex)) return EnemyAnimation.attacking; - if (pain.contains(spriteIndex)) return EnemyAnimation.pain; - if (dying.contains(spriteIndex)) return EnemyAnimation.dying; - if (dead.contains(spriteIndex)) return EnemyAnimation.dead; - return null; - } - - /// Returns the [SpriteFrameRange] associated with a specific [animation] state. - SpriteFrameRange getRange(EnemyAnimation animation) { - return switch (animation) { - EnemyAnimation.idle => idle, - EnemyAnimation.walking => walking, - EnemyAnimation.attacking => attacking, - EnemyAnimation.pain => pain, - EnemyAnimation.dying => dying, - EnemyAnimation.dead => dead, - }; - } - - /// Checks if any animation range in this map overlaps with any range in [other]. - bool overlapsWith(EnemyAnimationMap other) { - final myRanges = [idle, walking, attacking, pain, dying, dead]; - final otherRanges = [ - other.idle, - other.walking, - other.attacking, - other.pain, - other.dying, - other.dead, - ]; - - for (final myRange in myRanges) { - for (final otherRange in otherRanges) { - if (myRange.overlaps(otherRange)) { - return true; - } - } - } - return false; - } - - /// Checks if any animation ranges within this specific enemy overlap with each other. - /// - /// Useful for validating that an enemy isn't trying to use the same sprite - /// for two different states (e.g., walking and attacking). - bool hasInternalOverlaps() { - final ranges = [idle, walking, attacking, pain, dying, dead]; - - // Compare every unique pair of ranges - for (int i = 0; i < ranges.length; i++) { - for (int j = i + 1; j < ranges.length; j++) { - if (ranges[i].overlaps(ranges[j])) { - return true; - } - } - } - - return false; - } - - Map get allRanges => { - EnemyAnimation.idle: idle, - EnemyAnimation.walking: walking, - EnemyAnimation.attacking: attacking, - EnemyAnimation.pain: pain, - EnemyAnimation.dying: dying, - EnemyAnimation.dead: dead, - }; -} +// This file has been moved to data_types to avoid circular dependencies. +// It is kept here as a re-export shim for backwards compatibility. +export 'package:wolf_3d_dart/src/data_types/enemy_animation.dart'; diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy_type.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy_type.dart index 2ff9c62..dcb2569 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy_type.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy_type.dart @@ -1,6 +1,5 @@ import 'dart:math' as math; -import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; /// Defines the primary enemy types and their behavioral metadata. diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/guard.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/guard.dart index 89fdc95..f8de7ff 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/guard.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/guard.dart @@ -1,7 +1,6 @@ import 'dart:math' as math; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart'; -import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart'; import 'package:wolf_3d_dart/src/entities/entity.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/mutant.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/mutant.dart index 6caecd1..86fdbd1 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/mutant.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/mutant.dart @@ -1,7 +1,6 @@ import 'dart:math' as math; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart'; -import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart'; import 'package:wolf_3d_dart/src/entities/entity.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/officer.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/officer.dart index 422ed5f..f8307d8 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/officer.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/officer.dart @@ -1,7 +1,6 @@ import 'dart:math' as math; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart'; -import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart'; import 'package:wolf_3d_dart/src/entities/entity.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/ss.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/ss.dart index 323270e..6df74a7 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/ss.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/ss.dart @@ -1,7 +1,6 @@ import 'dart:math' as math; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart'; -import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart'; import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart'; import 'package:wolf_3d_dart/src/entities/entity.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; diff --git a/packages/wolf_3d_dart/lib/src/registry/asset_registry.dart b/packages/wolf_3d_dart/lib/src/registry/asset_registry.dart new file mode 100644 index 0000000..f1bd5bd --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/asset_registry.dart @@ -0,0 +1,45 @@ +import 'package:wolf_3d_dart/src/registry/modules/entity_asset_module.dart'; +import 'package:wolf_3d_dart/src/registry/modules/hud_module.dart'; +import 'package:wolf_3d_dart/src/registry/modules/menu_pic_module.dart'; +import 'package:wolf_3d_dart/src/registry/modules/music_module.dart'; +import 'package:wolf_3d_dart/src/registry/modules/sfx_module.dart'; + +/// The top-level asset registry attached to every [WolfensteinData] instance. +/// +/// Access version-specific asset IDs through the five typed modules: +/// +/// ```dart +/// data.registry.sfx.resolve(SfxKey.pistolFire) +/// data.registry.music.musicForLevel(episode, level) +/// data.registry.entities.resolve(EntityKey.guard) +/// data.registry.hud.faceForHealth(player.health) +/// data.registry.menu.resolve(MenuPicKey.title) +/// ``` +/// +/// To provide a fully custom asset layout, implement all five module +/// interfaces and pass them to this constructor, then supply the resulting +/// [AssetRegistry] to [WolfensteinLoader.loadFromBytes]. +class AssetRegistry { + const AssetRegistry({ + required this.sfx, + required this.music, + required this.entities, + required this.hud, + required this.menu, + }); + + /// Sound-effect slot resolution. + final SfxModule sfx; + + /// Music track routing (menu and per-level). + final MusicModule music; + + /// Entity sprite-range and availability data. + final EntityAssetModule entities; + + /// HUD image index resolution. + final HudModule hud; + + /// Menu VGA picture index resolution. + final MenuPicModule menu; +} diff --git a/packages/wolf_3d_dart/lib/src/registry/built_in/retail_asset_registry.dart b/packages/wolf_3d_dart/lib/src/registry/built_in/retail_asset_registry.dart new file mode 100644 index 0000000..c7524f8 --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/built_in/retail_asset_registry.dart @@ -0,0 +1,22 @@ +import 'package:wolf_3d_dart/src/registry/asset_registry.dart'; +import 'package:wolf_3d_dart/src/registry/built_in/retail_entity_module.dart'; +import 'package:wolf_3d_dart/src/registry/built_in/retail_hud_module.dart'; +import 'package:wolf_3d_dart/src/registry/built_in/retail_menu_module.dart'; +import 'package:wolf_3d_dart/src/registry/built_in/retail_music_module.dart'; +import 'package:wolf_3d_dart/src/registry/built_in/retail_sfx_module.dart'; + +/// The canonical [AssetRegistry] for all retail Wolf3D releases. +/// +/// Composes the five retail built-in modules and preserves the original +/// id Software asset layout exactly. All other registries and tests that +/// need a stable reference should start from this factory. +class RetailAssetRegistry extends AssetRegistry { + RetailAssetRegistry() + : super( + sfx: const RetailSfxModule(), + music: const RetailMusicModule(), + entities: const RetailEntityModule(), + hud: const RetailHudModule(), + menu: const RetailMenuPicModule(), + ); +} diff --git a/packages/wolf_3d_dart/lib/src/registry/built_in/retail_entity_module.dart b/packages/wolf_3d_dart/lib/src/registry/built_in/retail_entity_module.dart new file mode 100644 index 0000000..b6507be --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/built_in/retail_entity_module.dart @@ -0,0 +1,79 @@ +import 'package:wolf_3d_dart/src/data_types/enemy_animation.dart'; +import 'package:wolf_3d_dart/src/data_types/sprite_frame_range.dart'; +import 'package:wolf_3d_dart/src/registry/keys/entity_key.dart'; +import 'package:wolf_3d_dart/src/registry/modules/entity_asset_module.dart'; + +/// Built-in entity asset module for all retail Wolf3D releases (v1.0, v1.1, v1.4). +/// +/// Sprite ranges mirror the constants that were previously embedded in +/// [EnemyType] enum constructors, preserving the original id Software layout. +class RetailEntityModule extends EntityAssetModule { + const RetailEntityModule(); + + static final Map _defs = { + EntityKey.guard: const EntityAssetDefinition( + availableInShareware: true, + animations: EnemyAnimationMap( + idle: SpriteFrameRange(50, 57), + walking: SpriteFrameRange(58, 89), + dying: SpriteFrameRange(90, 93), + pain: SpriteFrameRange(94, 94), + dead: SpriteFrameRange(95, 95), + attacking: SpriteFrameRange(96, 98), + ), + ), + EntityKey.dog: const EntityAssetDefinition( + availableInShareware: true, + animations: EnemyAnimationMap( + idle: SpriteFrameRange(99, 106), + walking: SpriteFrameRange(99, 130), + attacking: SpriteFrameRange(135, 137), + pain: SpriteFrameRange(0, 0), + dying: SpriteFrameRange(131, 133), + dead: SpriteFrameRange(134, 134), + ), + ), + EntityKey.ss: const EntityAssetDefinition( + availableInShareware: true, + animations: EnemyAnimationMap( + idle: SpriteFrameRange(138, 145), + walking: SpriteFrameRange(146, 177), + attacking: SpriteFrameRange(184, 186), + pain: SpriteFrameRange(182, 182), + dying: SpriteFrameRange(178, 181), + dead: SpriteFrameRange(183, 183), + ), + ), + EntityKey.mutant: const EntityAssetDefinition( + availableInShareware: false, + animations: EnemyAnimationMap( + idle: SpriteFrameRange(187, 194), + walking: SpriteFrameRange(195, 226), + attacking: SpriteFrameRange(234, 237), + pain: SpriteFrameRange(231, 231), + dying: SpriteFrameRange(227, 232, excluded: {231}), + dead: SpriteFrameRange(233, 233), + ), + ), + EntityKey.officer: const EntityAssetDefinition( + availableInShareware: false, + animations: EnemyAnimationMap( + idle: SpriteFrameRange(238, 245), + walking: SpriteFrameRange(246, 277), + attacking: SpriteFrameRange(285, 287), + pain: SpriteFrameRange(282, 282), + dying: SpriteFrameRange(278, 283, excluded: {282}), + dead: SpriteFrameRange(284, 284), + ), + ), + // Bosses: no standard EnemyAnimationMap; sprite selection is handled + // by custom update() logic in each boss class using baseSpriteIndex. + EntityKey.hansGrosse: const EntityAssetDefinition( + availableInShareware: false, + baseSpriteIndex: 291, + ), + }; + + @override + EntityAssetDefinition? resolve(EntityKey key) => _defs[key]; +} diff --git a/packages/wolf_3d_dart/lib/src/registry/built_in/retail_hud_module.dart b/packages/wolf_3d_dart/lib/src/registry/built_in/retail_hud_module.dart new file mode 100644 index 0000000..4bd26ba --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/built_in/retail_hud_module.dart @@ -0,0 +1,65 @@ +import 'package:wolf_3d_dart/src/registry/keys/hud_key.dart'; +import 'package:wolf_3d_dart/src/registry/modules/hud_module.dart'; + +/// Built-in HUD module for all retail Wolf3D releases (v1.0, v1.1, v1.4). +/// +/// VGA indices match the hard-coded values that were previously scattered +/// across [RendererBackend], preserving the original id Software HUD layout. +/// +/// Face health bands follow the original formula: +/// `106 + ((100 - health) ~/ 16).clamp(0, 6) * 3` +class RetailHudModule extends HudModule { + const RetailHudModule(); + + // Ordered face keys from healthiest to most wounded (bands 0–6). + static const List _faceBands = [ + HudKey.faceHealthy, + HudKey.faceScratched, + HudKey.faceHurt, + HudKey.faceWounded, + HudKey.faceBadlyWounded, + HudKey.faceDying, + HudKey.faceNearDeath, + ]; + + static final Map _indices = { + HudKey.statusBar : 83, + // Digits — '0' is index 96; subsequent digits follow sequentially. + HudKey.digit0 : 96, + HudKey.digit1 : 97, + HudKey.digit2 : 98, + HudKey.digit3 : 99, + HudKey.digit4 : 100, + HudKey.digit5 : 101, + HudKey.digit6 : 102, + HudKey.digit7 : 103, + HudKey.digit8 : 104, + HudKey.digit9 : 105, + // BJ face bands (each band's base; 3 animated variants follow). + HudKey.faceHealthy : 106, + HudKey.faceScratched : 109, + HudKey.faceHurt : 112, + HudKey.faceWounded : 115, + HudKey.faceBadlyWounded : 118, + HudKey.faceDying : 121, + HudKey.faceNearDeath : 124, + HudKey.faceDead : 127, + // Weapon icons. + HudKey.pistolIcon : 89, + HudKey.machineGunIcon : 90, + HudKey.chainGunIcon : 91, + }; + + @override + HudAssetRef? resolve(HudKey key) { + final index = _indices[key]; + return index != null ? HudAssetRef(index) : null; + } + + @override + HudKey faceKeyForHealth(int health) { + if (health <= 0) return HudKey.faceDead; + final band = ((100 - health) ~/ 16).clamp(0, _faceBands.length - 1); + return _faceBands[band]; + } +} diff --git a/packages/wolf_3d_dart/lib/src/registry/built_in/retail_menu_module.dart b/packages/wolf_3d_dart/lib/src/registry/built_in/retail_menu_module.dart new file mode 100644 index 0000000..80b763e --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/built_in/retail_menu_module.dart @@ -0,0 +1,76 @@ +import 'package:wolf_3d_dart/src/data_types/difficulty.dart'; +import 'package:wolf_3d_dart/src/registry/keys/menu_pic_key.dart'; +import 'package:wolf_3d_dart/src/registry/modules/menu_pic_module.dart'; + +/// Built-in menu-picture module for all retail Wolf3D releases (v1.0, v1.1, v1.4). +/// +/// Picture indices match the [WolfMenuPic] constants that were previously the +/// sole authoritative source, preserving the original id Software VGA layout. +/// +/// Note: indices are picture-table indices (not raw VGAGRAPH chunk IDs). +/// For example, `C_EPISODE1PIC` is chunk 30 → picture index 27. +class RetailMenuPicModule extends MenuPicModule { + const RetailMenuPicModule(); + + static const List _episodeKeys = [ + MenuPicKey.episode1, + MenuPicKey.episode2, + MenuPicKey.episode3, + MenuPicKey.episode4, + MenuPicKey.episode5, + MenuPicKey.episode6, + ]; + + static final Map _indices = { + // --- Full-screen art --- + MenuPicKey.title : 84, + MenuPicKey.credits : 86, + MenuPicKey.pg13 : 85, + // --- Control-panel chrome --- + MenuPicKey.controlBackground : 23, // C_CONTROLPIC + MenuPicKey.footer : 15, // C_MOUSELBACKPIC (footer art) + MenuPicKey.heading : 3, // H_TOPWINDOWPIC + MenuPicKey.optionsLabel : 7, // C_OPTIONSPIC + // --- Cursor / markers --- + MenuPicKey.cursorActive : 8, // C_CURSOR1PIC + MenuPicKey.cursorInactive : 9, // C_CURSOR2PIC + MenuPicKey.markerSelected : 11, // C_SELECTEDPIC + MenuPicKey.markerUnselected : 10, // C_NOTSELECTEDPIC + // --- Episode selection --- + MenuPicKey.episode1 : 27, + MenuPicKey.episode2 : 28, + MenuPicKey.episode3 : 29, + MenuPicKey.episode4 : 30, + MenuPicKey.episode5 : 31, + MenuPicKey.episode6 : 32, + // --- Difficulty selection --- + MenuPicKey.difficultyBaby : 16, // C_BABYMODEPIC + MenuPicKey.difficultyEasy : 17, // C_EASYPIC + MenuPicKey.difficultyNormal : 18, // C_NORMALPIC + MenuPicKey.difficultyHard : 19, // C_HARDPIC + }; + + @override + MenuPicRef? resolve(MenuPicKey key) { + final index = _indices[key]; + return index != null ? MenuPicRef(index) : null; + } + + @override + MenuPicKey episodeKey(int episodeIndex) { + if (episodeIndex >= 0 && episodeIndex < _episodeKeys.length) { + return _episodeKeys[episodeIndex]; + } + return MenuPicKey.episode1; // safe fallback + } + + @override + MenuPicKey difficultyKey(Difficulty difficulty) { + return switch (difficulty) { + Difficulty.baby => MenuPicKey.difficultyBaby, + Difficulty.easy => MenuPicKey.difficultyEasy, + Difficulty.medium => MenuPicKey.difficultyNormal, + Difficulty.hard => MenuPicKey.difficultyHard, + }; + } +} diff --git a/packages/wolf_3d_dart/lib/src/registry/built_in/retail_music_module.dart b/packages/wolf_3d_dart/lib/src/registry/built_in/retail_music_module.dart new file mode 100644 index 0000000..56133be --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/built_in/retail_music_module.dart @@ -0,0 +1,62 @@ +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 _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 _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; + } +} diff --git a/packages/wolf_3d_dart/lib/src/registry/built_in/retail_sfx_module.dart b/packages/wolf_3d_dart/lib/src/registry/built_in/retail_sfx_module.dart new file mode 100644 index 0000000..e3014bf --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/built_in/retail_sfx_module.dart @@ -0,0 +1,67 @@ +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 _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; + } +} diff --git a/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_asset_registry.dart b/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_asset_registry.dart new file mode 100644 index 0000000..66fefc7 --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_asset_registry.dart @@ -0,0 +1,29 @@ +import 'package:wolf_3d_dart/src/registry/asset_registry.dart'; +import 'package:wolf_3d_dart/src/registry/built_in/retail_hud_module.dart'; +import 'package:wolf_3d_dart/src/registry/built_in/retail_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_menu_module.dart'; +import 'package:wolf_3d_dart/src/registry/built_in/shareware_music_module.dart'; + +/// The [AssetRegistry] for the Wolfenstein 3D v1.4 Shareware release. +/// +/// - SFX slots are identical to retail (same AUDIOT layout). +/// - Music routing uses the 10-level shareware table. +/// - Entity definitions are limited to the three shareware enemies. +/// - HUD indices are identical to retail (same relative VGA layout). +/// - Menu picture indices are resolved via runtime heuristic offset; call +/// [SharewareMenuPicModule.initWithImageSizes] after the VGA images are +/// available so the shift is computed correctly. +class SharewareAssetRegistry extends AssetRegistry { + SharewareAssetRegistry() + : super( + sfx: const RetailSfxModule(), + music: const SharewareMusicModule(), + entities: const SharewareEntityModule(), + hud: const RetailHudModule(), + menu: SharewareMenuPicModule(), + ); + + /// Convenience accessor to the menu module for post-load initialisation. + SharewareMenuPicModule get sharewareMenu => menu as SharewareMenuPicModule; +} diff --git a/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_entity_module.dart b/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_entity_module.dart new file mode 100644 index 0000000..d18e01a --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_entity_module.dart @@ -0,0 +1,56 @@ +import 'package:wolf_3d_dart/src/data_types/enemy_animation.dart'; +import 'package:wolf_3d_dart/src/data_types/sprite_frame_range.dart'; +import 'package:wolf_3d_dart/src/registry/keys/entity_key.dart'; +import 'package:wolf_3d_dart/src/registry/modules/entity_asset_module.dart'; + +/// Built-in entity asset module for the Wolfenstein 3D v1.4 Shareware release. +/// +/// Shareware restricts the available enemy roster to Guard, Dog, and SS. +/// Mutant and Officer are absent; Boss entries are retail-only. +/// +/// Sprite ranges for the three available enemies are identical to retail +/// (the VSWAP sprite layout is the same for this subset). +class SharewareEntityModule extends EntityAssetModule { + const SharewareEntityModule(); + + static final Map _defs = { + EntityKey.guard: const EntityAssetDefinition( + availableInShareware: true, + animations: EnemyAnimationMap( + idle: SpriteFrameRange(50, 57), + walking: SpriteFrameRange(58, 89), + dying: SpriteFrameRange(90, 93), + pain: SpriteFrameRange(94, 94), + dead: SpriteFrameRange(95, 95), + attacking: SpriteFrameRange(96, 98), + ), + ), + EntityKey.dog: const EntityAssetDefinition( + availableInShareware: true, + animations: EnemyAnimationMap( + idle: SpriteFrameRange(99, 106), + walking: SpriteFrameRange(99, 130), + attacking: SpriteFrameRange(135, 137), + pain: SpriteFrameRange(0, 0), + dying: SpriteFrameRange(131, 133), + dead: SpriteFrameRange(134, 134), + ), + ), + EntityKey.ss: const EntityAssetDefinition( + availableInShareware: true, + animations: EnemyAnimationMap( + idle: SpriteFrameRange(138, 145), + walking: SpriteFrameRange(146, 177), + attacking: SpriteFrameRange(184, 186), + pain: SpriteFrameRange(182, 182), + dying: SpriteFrameRange(178, 181), + dead: SpriteFrameRange(183, 183), + ), + ), + // Mutant and Officer are NOT present in shareware; resolving them + // intentionally returns null so callers can handle absence cleanly. + }; + + @override + EntityAssetDefinition? resolve(EntityKey key) => _defs[key]; +} diff --git a/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_menu_module.dart b/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_menu_module.dart new file mode 100644 index 0000000..9a74f4e --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_menu_module.dart @@ -0,0 +1,111 @@ +import 'package:wolf_3d_dart/src/data_types/difficulty.dart'; +import 'package:wolf_3d_dart/src/registry/keys/menu_pic_key.dart'; +import 'package:wolf_3d_dart/src/registry/modules/menu_pic_module.dart'; + +/// Built-in menu-picture module for the Wolfenstein 3D v1.4 Shareware release. +/// +/// Shareware VGAGRAPH contains fewer pictures than the retail version, so +/// the episode/difficulty/control-panel art sits at a shifted position in +/// the VGA image list. The exact shift is computed at resolve time by +/// scanning the loaded image list for the landmark STATUSBARPIC, mirroring +/// the runtime heuristic in the original [WolfClassicMenuArt._indexOffset]. +/// +/// Offset determination is deferred until the first [resolve] call and +/// cached for subsequent lookups. If the landmark cannot be found the +/// module falls back to the retail indices (compatible with unrecognised +/// or future shareware variants). +class SharewareMenuPicModule extends MenuPicModule { + SharewareMenuPicModule(); + + // Retail-baseline indices (same layout as RetailMenuPicModule). + static final Map _retailBaseline = { + MenuPicKey.title : 84, + MenuPicKey.credits : 86, + MenuPicKey.pg13 : 85, + MenuPicKey.controlBackground : 23, + MenuPicKey.footer : 15, + MenuPicKey.heading : 3, + MenuPicKey.optionsLabel : 7, + MenuPicKey.cursorActive : 8, + MenuPicKey.cursorInactive : 9, + MenuPicKey.markerSelected : 11, + MenuPicKey.markerUnselected : 10, + MenuPicKey.episode1 : 27, + MenuPicKey.episode2 : 28, + MenuPicKey.episode3 : 29, + MenuPicKey.episode4 : 30, + MenuPicKey.episode5 : 31, + MenuPicKey.episode6 : 32, + MenuPicKey.difficultyBaby : 16, + MenuPicKey.difficultyEasy : 17, + MenuPicKey.difficultyNormal : 18, + MenuPicKey.difficultyHard : 19, + }; + + // Landmark constant: STATUSBARPIC in retail picture-table coords. + static const int _retailStatusBarIndex = 83; + + // Runtime-computed shift applied to every retail-baseline index. + // null = not yet computed; 0 = no shift needed (or heuristic failed). + int? _offset; + + /// Provide the raw VGA image dimensions list so the module can compute + /// the version offset on first use. Call this immediately after + /// construction when the [WolfensteinData] instance is available. + /// + /// [imageSizes] is a list of `(width, height)` tuples in VGA image order. + void initWithImageSizes(List<({int width, int height})> imageSizes) { + _offset = _computeOffset(imageSizes); + } + + int _computeOffset(List<({int width, int height})> sizes) { + for (int i = 0; i < sizes.length - 3; i++) { + final s = sizes[i]; + // STATUSBARPIC heuristic: wide short banner followed by three full-screen images. + if (s.width >= 280 && s.height >= 24 && s.height <= 64) { + final t1 = sizes[i + 1]; + final t2 = sizes[i + 2]; + final t3 = sizes[i + 3]; + if (_isFullScreen(t1) && _isFullScreen(t2) && _isFullScreen(t3)) { + return i - _retailStatusBarIndex; + } + } + } + return 0; // no shift — unknown layout, use retail baseline + } + + bool _isFullScreen(({int width, int height}) s) => + s.width >= 280 && s.height >= 140; + + @override + MenuPicRef? resolve(MenuPicKey key) { + final base = _retailBaseline[key]; + if (base == null) return null; + final shifted = base + (_offset ?? 0); + return MenuPicRef(shifted); + } + + @override + MenuPicKey episodeKey(int episodeIndex) { + const keys = [ + MenuPicKey.episode1, + MenuPicKey.episode2, + MenuPicKey.episode3, + MenuPicKey.episode4, + MenuPicKey.episode5, + MenuPicKey.episode6, + ]; + if (episodeIndex >= 0 && episodeIndex < keys.length) return keys[episodeIndex]; + return MenuPicKey.episode1; + } + + @override + MenuPicKey difficultyKey(Difficulty difficulty) { + return switch (difficulty) { + Difficulty.baby => MenuPicKey.difficultyBaby, + Difficulty.easy => MenuPicKey.difficultyEasy, + Difficulty.medium => MenuPicKey.difficultyNormal, + Difficulty.hard => MenuPicKey.difficultyHard, + }; + } +} diff --git a/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_music_module.dart b/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_music_module.dart new file mode 100644 index 0000000..b5ea59c --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_music_module.dart @@ -0,0 +1,40 @@ +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 _levelMap = [ + 2, 3, 4, 5, 2, 3, 4, 5, 6, 7, + ]; + + static final Map _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; + } +} diff --git a/packages/wolf_3d_dart/lib/src/registry/keys/entity_key.dart b/packages/wolf_3d_dart/lib/src/registry/keys/entity_key.dart new file mode 100644 index 0000000..b64fc79 --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/keys/entity_key.dart @@ -0,0 +1,39 @@ +/// Extensible typed key for entity asset definitions (enemies, bosses, etc.). +/// +/// Built-in Wolf3D entity types are exposed as named static constants. +/// Custom modules can define new keys with `const EntityKey('myEnemy')` +/// without modifying this class. +/// +/// Example: +/// ```dart +/// registry.entities.resolve(EntityKey.guard) +/// ``` +final class EntityKey { + const EntityKey(this._id); + + final String _id; + + // --- Standard Enemies --- + static const guard = EntityKey('guard'); + static const dog = EntityKey('dog'); + static const ss = EntityKey('ss'); + static const mutant = EntityKey('mutant'); + static const officer = EntityKey('officer'); + + // --- Bosses (Wolf3D) --- + static const hansGrosse = EntityKey('hansGrosse'); + static const drSchabbs = EntityKey('drSchabbs'); + static const hitler = EntityKey('hitler'); + static const mechaHitler = EntityKey('mechaHitler'); + static const ottoGiftmacher = EntityKey('ottoGiftmacher'); + static const gretelGrosse = EntityKey('gretelGrosse'); + + @override + bool operator ==(Object other) => other is EntityKey && other._id == _id; + + @override + int get hashCode => _id.hashCode; + + @override + String toString() => 'EntityKey($_id)'; +} diff --git a/packages/wolf_3d_dart/lib/src/registry/keys/hud_key.dart b/packages/wolf_3d_dart/lib/src/registry/keys/hud_key.dart new file mode 100644 index 0000000..6c14f84 --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/keys/hud_key.dart @@ -0,0 +1,54 @@ +/// Extensible typed key for HUD image elements. +/// +/// Built-in Wolf3D HUD elements are exposed as named static constants. +/// Custom modules can define new keys with `const HudKey('myElement')` +/// without modifying this class. +/// +/// Example: +/// ```dart +/// registry.hud.resolve(HudKey.statusBar) +/// ``` +final class HudKey { + const HudKey(this._id); + + final String _id; + + // --- Layout --- + static const statusBar = HudKey('statusBar'); + + // --- Score/Floor/Lives/Ammo digits (0-9 for each counter) --- + static const digit0 = HudKey('digit0'); + static const digit1 = HudKey('digit1'); + static const digit2 = HudKey('digit2'); + static const digit3 = HudKey('digit3'); + static const digit4 = HudKey('digit4'); + static const digit5 = HudKey('digit5'); + static const digit6 = HudKey('digit6'); + static const digit7 = HudKey('digit7'); + static const digit8 = HudKey('digit8'); + static const digit9 = HudKey('digit9'); + + // --- BJ face health bands (each maps to the base of 3 animated frames) --- + static const faceHealthy = HudKey('faceHealthy'); // health 85–100 + static const faceScratched = HudKey('faceScratched'); // health 69–84 + static const faceHurt = HudKey('faceHurt'); // health 53–68 + static const faceWounded = HudKey('faceWounded'); // health 37–52 + static const faceBadlyWounded = HudKey('faceBadlyWounded'); // health 21–36 + static const faceDying = HudKey('faceDying'); // health 5–20 + static const faceNearDeath = HudKey('faceNearDeath'); // health 1–4 + static const faceDead = HudKey('faceDead'); // health ≤ 0 + + // --- Weapon icons --- + static const pistolIcon = HudKey('pistolIcon'); + static const machineGunIcon = HudKey('machineGunIcon'); + static const chainGunIcon = HudKey('chainGunIcon'); + + @override + bool operator ==(Object other) => other is HudKey && other._id == _id; + + @override + int get hashCode => _id.hashCode; + + @override + String toString() => 'HudKey($_id)'; +} diff --git a/packages/wolf_3d_dart/lib/src/registry/keys/menu_pic_key.dart b/packages/wolf_3d_dart/lib/src/registry/keys/menu_pic_key.dart new file mode 100644 index 0000000..5279669 --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/keys/menu_pic_key.dart @@ -0,0 +1,55 @@ +/// Extensible typed key for menu VGA picture elements. +/// +/// Built-in Wolf3D menu pictures are exposed as named static constants. +/// Custom modules can define new keys with `const MenuPicKey('myPic')` +/// without modifying this class. +/// +/// Example: +/// ```dart +/// registry.menu.resolve(MenuPicKey.title) +/// ``` +final class MenuPicKey { + const MenuPicKey(this._id); + + final String _id; + + // --- Full-screen art --- + static const title = MenuPicKey('title'); + static const credits = MenuPicKey('credits'); + static const pg13 = MenuPicKey('pg13'); + + // --- Control-panel chrome --- + static const controlBackground = MenuPicKey('controlBackground'); + static const footer = MenuPicKey('footer'); + static const heading = MenuPicKey('heading'); + static const optionsLabel = MenuPicKey('optionsLabel'); + + // --- Cursor / selection markers --- + static const cursorActive = MenuPicKey('cursorActive'); + static const cursorInactive = MenuPicKey('cursorInactive'); + static const markerSelected = MenuPicKey('markerSelected'); + static const markerUnselected = MenuPicKey('markerUnselected'); + + // --- Episode selection --- + static const episode1 = MenuPicKey('episode1'); + static const episode2 = MenuPicKey('episode2'); + static const episode3 = MenuPicKey('episode3'); + static const episode4 = MenuPicKey('episode4'); + static const episode5 = MenuPicKey('episode5'); + static const episode6 = MenuPicKey('episode6'); + + // --- Difficulty selection --- + static const difficultyBaby = MenuPicKey('difficultyBaby'); + static const difficultyEasy = MenuPicKey('difficultyEasy'); + static const difficultyNormal = MenuPicKey('difficultyNormal'); + static const difficultyHard = MenuPicKey('difficultyHard'); + + @override + bool operator ==(Object other) => other is MenuPicKey && other._id == _id; + + @override + int get hashCode => _id.hashCode; + + @override + String toString() => 'MenuPicKey($_id)'; +} diff --git a/packages/wolf_3d_dart/lib/src/registry/keys/music_key.dart b/packages/wolf_3d_dart/lib/src/registry/keys/music_key.dart new file mode 100644 index 0000000..729d937 --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/keys/music_key.dart @@ -0,0 +1,51 @@ +/// 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)'; +} diff --git a/packages/wolf_3d_dart/lib/src/registry/keys/sfx_key.dart b/packages/wolf_3d_dart/lib/src/registry/keys/sfx_key.dart new file mode 100644 index 0000000..cd11b02 --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/keys/sfx_key.dart @@ -0,0 +1,77 @@ +/// 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)'; +} diff --git a/packages/wolf_3d_dart/lib/src/registry/modules/entity_asset_module.dart b/packages/wolf_3d_dart/lib/src/registry/modules/entity_asset_module.dart new file mode 100644 index 0000000..33b7d51 --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/modules/entity_asset_module.dart @@ -0,0 +1,37 @@ +import 'package:wolf_3d_dart/src/data_types/enemy_animation.dart'; +import 'package:wolf_3d_dart/src/registry/keys/entity_key.dart'; + +/// All version-sensitive asset data for a single entity type. +/// +/// - [animations] is `null` for bosses that drive sprite selection with +/// custom logic and do not use the standard [EnemyAnimationMap]. +/// - [baseSpriteIndex] is the sprite-list offset used by custom-logic bosses +/// as the base for their manual frame calculations. +/// - [availableInShareware] mirrors the original engine's version-exclusion +/// flag; entities with `false` are skipped during shareware level loading. +class EntityAssetDefinition { + const EntityAssetDefinition({ + this.animations, + this.baseSpriteIndex, + this.availableInShareware = true, + }); + + final EnemyAnimationMap? animations; + final int? baseSpriteIndex; + final bool availableInShareware; +} + +/// Owns the mapping from symbolic [EntityKey] values to version-specific +/// [EntityAssetDefinition] objects. +/// +/// Implement this class to provide custom sprite data for a modded or +/// alternate game version. Pass the implementation inside a custom +/// [AssetRegistry] when loading data. +abstract class EntityAssetModule { + const EntityAssetModule(); + + /// Resolves [key] to an [EntityAssetDefinition]. + /// + /// Returns `null` if the key is not recognised by this module. + EntityAssetDefinition? resolve(EntityKey key); +} diff --git a/packages/wolf_3d_dart/lib/src/registry/modules/hud_module.dart b/packages/wolf_3d_dart/lib/src/registry/modules/hud_module.dart new file mode 100644 index 0000000..aae704a --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/modules/hud_module.dart @@ -0,0 +1,33 @@ +import 'package:wolf_3d_dart/src/registry/keys/hud_key.dart'; + +/// The resolved reference for a HUD element: a zero-based index into +/// [WolfensteinData.vgaImages]. +class HudAssetRef { + const HudAssetRef(this.vgaIndex); + + final int vgaIndex; + + @override + String toString() => 'HudAssetRef($vgaIndex)'; +} + +/// Owns the mapping from symbolic [HudKey] identifiers to VGA image indices. +/// +/// Implement this class to provide a custom HUD layout for a modded or +/// alternate game version. Pass the implementation inside a custom +/// [AssetRegistry] when loading data. +abstract class HudModule { + const HudModule(); + + /// Resolves [key] to a [HudAssetRef] containing the VGA image index. + /// + /// Returns `null` if the key is not supported by this module. + HudAssetRef? resolve(HudKey key); + + /// Returns the appropriate [HudKey] for BJ's face sprite given the + /// player's current [health]. + HudKey faceKeyForHealth(int health); + + /// Convenience: resolves BJ's face image directly from [health]. + HudAssetRef? faceForHealth(int health) => resolve(faceKeyForHealth(health)); +} diff --git a/packages/wolf_3d_dart/lib/src/registry/modules/menu_pic_module.dart b/packages/wolf_3d_dart/lib/src/registry/modules/menu_pic_module.dart new file mode 100644 index 0000000..c48ace6 --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/modules/menu_pic_module.dart @@ -0,0 +1,34 @@ +import 'package:wolf_3d_dart/src/data_types/difficulty.dart'; +import 'package:wolf_3d_dart/src/registry/keys/menu_pic_key.dart'; + +/// The resolved reference for a menu picture: a zero-based index into +/// [WolfensteinData.vgaImages]. +class MenuPicRef { + const MenuPicRef(this.pictureIndex); + + final int pictureIndex; + + @override + String toString() => 'MenuPicRef($pictureIndex)'; +} + +/// Owns the mapping from symbolic [MenuPicKey] values to VGA picture indices. +/// +/// Implement this class to provide custom menu art for a modded or alternate +/// game version. Pass the implementation inside a custom [AssetRegistry] when +/// loading data. +abstract class MenuPicModule { + const MenuPicModule(); + + /// Resolves [key] to a [MenuPicRef] containing the VGA picture index. + /// + /// Returns `null` if the key is not supported by this module. + MenuPicRef? resolve(MenuPicKey key); + + /// Returns the [MenuPicKey] for the episode selection picture at + /// zero-based [episodeIndex]. + MenuPicKey episodeKey(int episodeIndex); + + /// Returns the [MenuPicKey] for the given [difficulty] selection screen. + MenuPicKey difficultyKey(Difficulty difficulty); +} diff --git a/packages/wolf_3d_dart/lib/src/registry/modules/music_module.dart b/packages/wolf_3d_dart/lib/src/registry/modules/music_module.dart new file mode 100644 index 0000000..e228991 --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/modules/music_module.dart @@ -0,0 +1,36 @@ +import 'package:wolf_3d_dart/src/registry/keys/music_key.dart'; + +/// The resolved reference for a music track: a numeric index into +/// [WolfensteinData.music]. +class MusicRoute { + const MusicRoute(this.trackIndex); + + final int trackIndex; + + @override + String toString() => 'MusicRoute($trackIndex)'; +} + +/// Owns the mapping from music contexts to IMF track indices. +/// +/// Implement this class to provide a custom music layout for a modded or +/// alternate game version. Pass the implementation inside a custom +/// [AssetRegistry] when loading data. +abstract class MusicModule { + const MusicModule(); + + /// Resolves a named [MusicKey] to a [MusicRoute]. + /// + /// Returns `null` if the key is not supported by this module. + MusicRoute? resolve(MusicKey key); + + /// Resolves the level music track for a given [episodeIndex] and + /// zero-based [levelIndex]. + /// + /// This is the primary routing path for in-game level music, which the + /// original engine mapped via a lookup table rather than per-level fields. + MusicRoute musicForLevel(int episodeIndex, int levelIndex); + + /// The music track to play while the main menu is shown. + MusicRoute get menuMusic; +} diff --git a/packages/wolf_3d_dart/lib/src/registry/modules/sfx_module.dart b/packages/wolf_3d_dart/lib/src/registry/modules/sfx_module.dart new file mode 100644 index 0000000..2eee8f9 --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/modules/sfx_module.dart @@ -0,0 +1,26 @@ +import 'package:wolf_3d_dart/src/registry/keys/sfx_key.dart'; + +/// The resolved reference for a sound effect: a numeric slot index into +/// [WolfensteinData.sounds]. +class SoundAssetRef { + const SoundAssetRef(this.slotIndex); + + final int slotIndex; + + @override + String toString() => 'SoundAssetRef($slotIndex)'; +} + +/// Owns the mapping from symbolic [SfxKey] identifiers to numeric sound slots. +/// +/// Implement this class to provide a custom sound layout for a modded or +/// alternate game version. Pass the implementation inside a custom +/// [AssetRegistry] when loading data. +abstract class SfxModule { + const SfxModule(); + + /// Resolves [key] to a [SoundAssetRef] containing the numeric slot index. + /// + /// Returns `null` if the key is not supported by this module. + SoundAssetRef? resolve(SfxKey key); +} diff --git a/packages/wolf_3d_dart/lib/src/registry/registry_resolver.dart b/packages/wolf_3d_dart/lib/src/registry/registry_resolver.dart new file mode 100644 index 0000000..e444b49 --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/registry_resolver.dart @@ -0,0 +1,68 @@ +import 'package:wolf_3d_dart/src/data/data_version.dart'; +import 'package:wolf_3d_dart/src/data_types/game_version.dart'; +import 'package:wolf_3d_dart/src/registry/asset_registry.dart'; +import 'package:wolf_3d_dart/src/registry/built_in/retail_asset_registry.dart'; +import 'package:wolf_3d_dart/src/registry/built_in/shareware_asset_registry.dart'; + +/// The input used by [AssetRegistryResolver] to select or build a registry. +class RegistrySelectionContext { + const RegistrySelectionContext({ + required this.gameVersion, + required this.dataVersion, + }); + + /// Broad release family (Retail, Shareware, Spear of Destiny, …). + final GameVersion gameVersion; + + /// Exact data-file identity determined from the VSWAP checksum. + final DataVersion dataVersion; +} + +/// Selects or builds an [AssetRegistry] for a given [RegistrySelectionContext]. +/// +/// Implement this interface to provide a fully custom selection strategy, +/// for example to support a game variant not covered by the built-in resolver. +abstract class AssetRegistryResolver { + const AssetRegistryResolver(); + + AssetRegistry resolve(RegistrySelectionContext context); +} + +/// Default resolver that maps known [DataVersion] and [GameVersion] values to +/// built-in registries. +/// +/// Selection precedence: +/// 1. Exact [DataVersion] match (most specific). +/// 2. [GameVersion] family fallback. +/// 3. Retail layout as the last-resort fallback for unknown variants. +class BuiltInAssetRegistryResolver implements AssetRegistryResolver { + const BuiltInAssetRegistryResolver(); + + @override + AssetRegistry resolve(RegistrySelectionContext context) { + // 1. Exact data-version match. + switch (context.dataVersion) { + case DataVersion.version10Retail: + case DataVersion.version11Retail: + case DataVersion.version14Retail: + return RetailAssetRegistry(); + case DataVersion.version14Shareware: + return SharewareAssetRegistry(); + case DataVersion.unknown: + break; // fall through to GameVersion family check + } + + // 2. GameVersion family fallback. + switch (context.gameVersion) { + case GameVersion.retail: + return RetailAssetRegistry(); + case GameVersion.shareware: + return SharewareAssetRegistry(); + case GameVersion.spearOfDestiny: + case GameVersion.spearOfDestinyDemo: + // Spear-specific modules are not yet implemented; retail layout is + // the closest structural match and will be replaced in a later phase. + return RetailAssetRegistry(); + } + } +} diff --git a/packages/wolf_3d_dart/lib/wolf_3d_data_types.dart b/packages/wolf_3d_dart/lib/wolf_3d_data_types.dart index 98fab69..c17403c 100644 --- a/packages/wolf_3d_dart/lib/wolf_3d_data_types.dart +++ b/packages/wolf_3d_dart/lib/wolf_3d_data_types.dart @@ -5,10 +5,13 @@ /// the full engine runtime. library; +export 'src/data/data_version.dart' show DataVersion; export 'src/data_types/cardinal_direction.dart' show CardinalDirection; export 'src/data_types/color_palette.dart' show ColorPalette; export 'src/data_types/coordinate_2d.dart' show Coordinate2D; export 'src/data_types/difficulty.dart' show Difficulty; +export 'src/data_types/enemy_animation.dart' + show EnemyAnimation, EnemyAnimationMap; export 'src/data_types/enemy_map_data.dart' show EnemyMapData; export 'src/data_types/episode.dart' show Episode; export 'src/data_types/frame_buffer.dart' show FrameBuffer; @@ -22,3 +25,24 @@ export 'src/data_types/sprite.dart' hide Matrix; export 'src/data_types/sprite_frame_range.dart' show SpriteFrameRange; export 'src/data_types/wolf_level.dart' show WolfLevel; export 'src/data_types/wolfenstein_data.dart' show WolfensteinData; +// Registry public surface +export 'src/registry/asset_registry.dart' show AssetRegistry; +export 'src/registry/keys/entity_key.dart' show EntityKey; +export 'src/registry/keys/hud_key.dart' show HudKey; +export 'src/registry/keys/menu_pic_key.dart' show MenuPicKey; +export 'src/registry/keys/music_key.dart' show MusicKey; +export 'src/registry/keys/sfx_key.dart' show SfxKey; +export 'src/registry/modules/entity_asset_module.dart' + show EntityAssetModule, EntityAssetDefinition; +export 'src/registry/modules/hud_module.dart' show HudModule, HudAssetRef; +export 'src/registry/modules/menu_pic_module.dart' + show MenuPicModule, MenuPicRef; +export 'src/registry/modules/music_module.dart' show MusicModule, MusicRoute; +export 'src/registry/modules/sfx_module.dart' show SfxModule, SoundAssetRef; +export 'src/registry/registry_resolver.dart' + show AssetRegistryResolver, BuiltInAssetRegistryResolver, + RegistrySelectionContext; +export 'src/registry/built_in/retail_asset_registry.dart' + show RetailAssetRegistry; +export 'src/registry/built_in/shareware_asset_registry.dart' + show SharewareAssetRegistry; diff --git a/packages/wolf_3d_dart/test/engine/audio_events_test.dart b/packages/wolf_3d_dart/test/engine/audio_events_test.dart index 2527e30..0cbf73a 100644 --- a/packages/wolf_3d_dart/test/engine/audio_events_test.dart +++ b/packages/wolf_3d_dart/test/engine/audio_events_test.dart @@ -118,6 +118,8 @@ WolfEngine _buildEngine({ return WolfEngine( data: WolfensteinData( version: GameVersion.retail, + dataVersion: DataVersion.unknown, + registry: RetailAssetRegistry(), walls: [ _solidSprite(1), _solidSprite(1), diff --git a/packages/wolf_3d_dart/test/rasterizer/pushwall_rasterizer_test.dart b/packages/wolf_3d_dart/test/rasterizer/pushwall_rasterizer_test.dart index d6a73d3..414ba85 100644 --- a/packages/wolf_3d_dart/test/rasterizer/pushwall_rasterizer_test.dart +++ b/packages/wolf_3d_dart/test/rasterizer/pushwall_rasterizer_test.dart @@ -23,6 +23,8 @@ void main() { final engine = WolfEngine( data: WolfensteinData( version: GameVersion.shareware, + dataVersion: DataVersion.unknown, + registry: RetailAssetRegistry(), walls: [ _solidSprite(1), _solidSprite(1), diff --git a/packages/wolf_3d_dart/test/rendering/pushwall_renderer_test.dart b/packages/wolf_3d_dart/test/rendering/pushwall_renderer_test.dart index 57f2f52..41341a5 100644 --- a/packages/wolf_3d_dart/test/rendering/pushwall_renderer_test.dart +++ b/packages/wolf_3d_dart/test/rendering/pushwall_renderer_test.dart @@ -23,6 +23,8 @@ void main() { final engine = WolfEngine( data: WolfensteinData( version: GameVersion.shareware, + dataVersion: DataVersion.unknown, + registry: RetailAssetRegistry(), walls: [ _solidSprite(1), _solidSprite(1),