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 <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-19 13:45:19 +01:00
parent de0d99588e
commit fcda0f9ff4
38 changed files with 1411 additions and 119 deletions

View File

@@ -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';

View File

@@ -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<WolfensteinData> loadAsyncWithOverride(
Future<ByteData> 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<VgaImage> 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.

View File

@@ -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<String, ByteData?> 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,
);
}
}

View File

@@ -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<EnemyAnimation, SpriteFrameRange> 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;
}
}

View File

@@ -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<Sprite> 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,

View File

@@ -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';

View File

@@ -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<EnemyAnimation, SpriteFrameRange> 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';

View File

@@ -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.

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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(),
);
}

View File

@@ -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<EntityKey, EntityAssetDefinition> _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];
}

View File

@@ -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 06).
static const List<HudKey> _faceBands = [
HudKey.faceHealthy,
HudKey.faceScratched,
HudKey.faceHurt,
HudKey.faceWounded,
HudKey.faceBadlyWounded,
HudKey.faceDying,
HudKey.faceNearDeath,
];
static final Map<HudKey, int> _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];
}
}

View File

@@ -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<MenuPicKey> _episodeKeys = [
MenuPicKey.episode1,
MenuPicKey.episode2,
MenuPicKey.episode3,
MenuPicKey.episode4,
MenuPicKey.episode5,
MenuPicKey.episode6,
];
static final Map<MenuPicKey, int> _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,
};
}
}

View File

@@ -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<int> _levelMap = [
2, 3, 4, 5, 2, 3, 4, 5, 6, 7, // Episode 1
8, 9, 10, 11, 8, 9, 11, 10, 6, 12, // Episode 2
13, 14, 15, 16, 13, 14, 15, 16, 17, 18, // Episode 3
2, 3, 4, 5, 2, 3, 4, 5, 6, 7, // Episode 4
8, 9, 10, 11, 8, 9, 11, 10, 6, 12, // Episode 5
13, 14, 15, 16, 13, 14, 15, 16, 17, 19, // Episode 6
];
// Named MusicKey → track index for keyed lookups.
static final Map<MusicKey, int> _named = {
MusicKey.menuTheme: 1,
MusicKey.level01: 2,
MusicKey.level02: 3,
MusicKey.level03: 4,
MusicKey.level04: 5,
MusicKey.level05: 6,
MusicKey.level06: 7,
MusicKey.level07: 8,
MusicKey.level08: 9,
MusicKey.level09: 10,
MusicKey.level10: 11,
MusicKey.level11: 12,
MusicKey.level12: 13,
MusicKey.level13: 14,
MusicKey.level14: 15,
MusicKey.level15: 16,
MusicKey.level16: 17,
MusicKey.level17: 18,
MusicKey.level18: 19,
};
@override
MusicRoute get menuMusic => const MusicRoute(1);
@override
MusicRoute musicForLevel(int episodeIndex, int levelIndex) {
final flat = episodeIndex * 10 + levelIndex;
if (flat >= 0 && flat < _levelMap.length) {
return MusicRoute(_levelMap[flat]);
}
// Wrap for custom episode counts beyond the built-in 6.
return MusicRoute(_levelMap[flat % _levelMap.length]);
}
@override
MusicRoute? resolve(MusicKey key) {
final index = _named[key];
return index != null ? MusicRoute(index) : null;
}
}

View File

@@ -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<SfxKey, int> _slots = {
// --- Doors & Environment ---
SfxKey.openDoor : WolfSound.openDoor,
SfxKey.closeDoor : WolfSound.closeDoor,
SfxKey.pushWall : WolfSound.pushWall,
// --- Weapons ---
SfxKey.knifeAttack : WolfSound.knifeAttack,
SfxKey.pistolFire : WolfSound.pistolFire,
SfxKey.machineGunFire : WolfSound.machineGunFire,
SfxKey.chainGunFire : WolfSound.gatlingFire,
SfxKey.enemyFire : WolfSound.naziFire,
// --- Pickups ---
SfxKey.getMachineGun : WolfSound.getMachineGun,
SfxKey.getAmmo : WolfSound.getAmmo,
SfxKey.getChainGun : WolfSound.getGatling,
SfxKey.healthSmall : WolfSound.healthSmall,
SfxKey.healthLarge : WolfSound.healthLarge,
SfxKey.treasure1 : WolfSound.treasure1,
SfxKey.treasure2 : WolfSound.treasure2,
SfxKey.treasure3 : WolfSound.treasure3,
SfxKey.treasure4 : WolfSound.treasure4,
SfxKey.extraLife : WolfSound.extraLife,
// --- Standard Enemies ---
SfxKey.guardHalt : WolfSound.guardHalt,
SfxKey.dogBark : WolfSound.dogBark,
SfxKey.dogDeath : WolfSound.dogDeath,
SfxKey.dogAttack : WolfSound.dogAttack,
SfxKey.deathScream1 : WolfSound.deathScream1,
SfxKey.deathScream2 : WolfSound.deathScream2,
SfxKey.deathScream3 : WolfSound.deathScream3,
SfxKey.ssAlert : WolfSound.ssSchutzstaffel,
SfxKey.ssDeath : WolfSound.ssMeinGott,
// --- Bosses ---
SfxKey.bossActive : WolfSound.bossActive,
SfxKey.hansGrosseDeath : WolfSound.mutti,
SfxKey.schabbs : WolfSound.schabbsHas,
SfxKey.schabbsDeath : WolfSound.eva,
SfxKey.hitlerGreeting : WolfSound.gutenTag,
SfxKey.hitlerDeath : WolfSound.scheist,
SfxKey.mechaSteps : WolfSound.mechSteps,
SfxKey.ottoAlert : WolfSound.spion,
SfxKey.gretelDeath : WolfSound.neinSoVass,
// --- UI & Progression ---
SfxKey.levelComplete : WolfSound.levelDone,
SfxKey.endBonus1 : WolfSound.endBonus1,
SfxKey.endBonus2 : WolfSound.endBonus2,
SfxKey.noBonus : WolfSound.noBonus,
SfxKey.percent100 : WolfSound.percent100,
};
@override
SoundAssetRef? resolve(SfxKey key) {
final slot = _slots[key];
return slot != null ? SoundAssetRef(slot) : null;
}
}

View File

@@ -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;
}

View File

@@ -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<EntityKey, EntityAssetDefinition> _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];
}

View File

@@ -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<MenuPicKey, int> _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,
};
}
}

View File

@@ -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<int> _levelMap = [
2, 3, 4, 5, 2, 3, 4, 5, 6, 7,
];
static final Map<MusicKey, int> _named = {
MusicKey.menuTheme: 1,
MusicKey.level01: 2,
MusicKey.level02: 3,
MusicKey.level03: 4,
MusicKey.level04: 5,
MusicKey.level05: 6,
MusicKey.level06: 7,
};
@override
MusicRoute get menuMusic => const MusicRoute(1);
@override
MusicRoute musicForLevel(int episodeIndex, int levelIndex) {
final flat = levelIndex % _levelMap.length;
return MusicRoute(_levelMap[flat]);
}
@override
MusicRoute? resolve(MusicKey key) {
final index = _named[key];
return index != null ? MusicRoute(index) : null;
}
}

View File

@@ -0,0 +1,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)';
}

View File

@@ -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 85100
static const faceScratched = HudKey('faceScratched'); // health 6984
static const faceHurt = HudKey('faceHurt'); // health 5368
static const faceWounded = HudKey('faceWounded'); // health 3752
static const faceBadlyWounded = HudKey('faceBadlyWounded'); // health 2136
static const faceDying = HudKey('faceDying'); // health 520
static const faceNearDeath = HudKey('faceNearDeath'); // health 14
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)';
}

View File

@@ -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)';
}

View File

@@ -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)';
}

View File

@@ -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)';
}

View File

@@ -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);
}

View File

@@ -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));
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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();
}
}
}

View File

@@ -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;

View File

@@ -118,6 +118,8 @@ WolfEngine _buildEngine({
return WolfEngine(
data: WolfensteinData(
version: GameVersion.retail,
dataVersion: DataVersion.unknown,
registry: RetailAssetRegistry(),
walls: [
_solidSprite(1),
_solidSprite(1),

View File

@@ -23,6 +23,8 @@ void main() {
final engine = WolfEngine(
data: WolfensteinData(
version: GameVersion.shareware,
dataVersion: DataVersion.unknown,
registry: RetailAssetRegistry(),
walls: [
_solidSprite(1),
_solidSprite(1),

View File

@@ -23,6 +23,8 @@ void main() {
final engine = WolfEngine(
data: WolfensteinData(
version: GameVersion.shareware,
dataVersion: DataVersion.unknown,
registry: RetailAssetRegistry(),
walls: [
_solidSprite(1),
_solidSprite(1),