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

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