diff --git a/packages/wolf_3d_dart/lib/src/data/wl_parser.dart b/packages/wolf_3d_dart/lib/src/data/wl_parser.dart index 200754e..36d2a4b 100644 --- a/packages/wolf_3d_dart/lib/src/data/wl_parser.dart +++ b/packages/wolf_3d_dart/lib/src/data/wl_parser.dart @@ -228,6 +228,7 @@ abstract class WLParser { .map((img) => (width: img.width, height: img.height)) .toList(); registry.sharewareMenu.initWithImageSizes(sizes); + registry.sharewareHud.initWithImageSizes(sizes); } return registry; diff --git a/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_asset_registry.dart b/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_asset_registry.dart index 5f90b43..61def58 100644 --- a/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_asset_registry.dart +++ b/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_asset_registry.dart @@ -1,7 +1,7 @@ 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_hud_module.dart'; import 'package:wolf_3d_dart/src/registry/built_in/shareware_menu_module.dart'; import 'package:wolf_3d_dart/src/registry/built_in/shareware_music_module.dart'; @@ -10,7 +10,7 @@ import 'package:wolf_3d_dart/src/registry/built_in/shareware_music_module.dart'; /// - 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). +/// - HUD indices are shareware-aware and offset from retail 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. @@ -20,7 +20,9 @@ class SharewareAssetRegistry extends AssetRegistry { sfx: const RetailSfxModule(), music: const SharewareMusicModule(), entities: const SharewareEntityModule(), - hud: const RetailHudModule(), + hud: SharewareHudModule( + useOriginalWl1Map: strictOriginalShareware, + ), menu: SharewareMenuPicModule( useOriginalWl1Map: strictOriginalShareware, ), @@ -28,4 +30,7 @@ class SharewareAssetRegistry extends AssetRegistry { /// Convenience accessor to the menu module for post-load initialisation. SharewareMenuPicModule get sharewareMenu => menu as SharewareMenuPicModule; + + /// Convenience accessor to the HUD module for post-load initialisation. + SharewareHudModule get sharewareHud => hud as SharewareHudModule; } diff --git a/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_hud_module.dart b/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_hud_module.dart new file mode 100644 index 0000000..c459b94 --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/registry/built_in/shareware_hud_module.dart @@ -0,0 +1,83 @@ +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 Wolf3D shareware data variants. +/// +/// For known original WL1 data, the HUD picture table is shifted by +12 from +/// retail indices. For unknown shareware-like data, the offset is detected from +/// the STATUSBARPIC landmark at runtime. +class SharewareHudModule extends HudModule { + SharewareHudModule({this.useOriginalWl1Map = false}); + + final bool useOriginalWl1Map; + + static const int _originalWl1Offset = 12; + static const int _retailStatusBarIndex = 83; + + static const List _faceBands = [ + HudKey.faceHealthy, + HudKey.faceScratched, + HudKey.faceHurt, + HudKey.faceWounded, + HudKey.faceBadlyWounded, + HudKey.faceDying, + HudKey.faceNearDeath, + ]; + + static final Map _retailBaseline = { + HudKey.statusBar: 83, + 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, + HudKey.faceHealthy: 106, + HudKey.faceScratched: 109, + HudKey.faceHurt: 112, + HudKey.faceWounded: 115, + HudKey.faceBadlyWounded: 118, + HudKey.faceDying: 121, + HudKey.faceNearDeath: 124, + HudKey.faceDead: 127, + HudKey.pistolIcon: 89, + HudKey.machineGunIcon: 90, + HudKey.chainGunIcon: 91, + }; + + int? _offset; + + 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; i++) { + final s = sizes[i]; + if (s.width >= 280 && s.height >= 24 && s.height <= 64) { + return i - _retailStatusBarIndex; + } + } + return 0; + } + + int get _effectiveOffset => useOriginalWl1Map ? _originalWl1Offset : (_offset ?? 0); + + @override + HudAssetRef? resolve(HudKey key) { + final base = _retailBaseline[key]; + if (base == null) return null; + return HudAssetRef(base + _effectiveOffset); + } + + @override + HudKey faceKeyForHealth(int health) { + if (health <= 0) return HudKey.faceDead; + final band = ((100 - health) ~/ 16).clamp(0, _faceBands.length - 1); + return _faceBands[band]; + } +} diff --git a/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart b/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart index 2bd3cde..654fcf8 100644 --- a/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart @@ -151,36 +151,49 @@ abstract class RendererBackend /// backend can scale/map them consistently via [blitHudVgaImage]. void drawStandardVgaHud(WolfEngine engine) { final List vgaImages = engine.data.vgaImages; - final int statusBarIndex = vgaImages.indexWhere( - (img) => img.width == 320 && img.height == 40, - ); - if (statusBarIndex == -1) return; + final statusBarRef = engine.data.registry.hud.resolve(HudKey.statusBar); + final int statusBarIndex = statusBarRef?.vgaIndex ?? -1; + if (statusBarIndex >= 0 && statusBarIndex < vgaImages.length) { + blitHudVgaImage(vgaImages[statusBarIndex], 0, 160); + } - blitHudVgaImage(vgaImages[statusBarIndex], 0, 160); - _drawHudNumber(vgaImages, 1, 32, 176); - _drawHudNumber(vgaImages, engine.player.score, 96, 176); - _drawHudNumber(vgaImages, 3, 120, 176); - _drawHudNumber(vgaImages, engine.player.health, 192, 176); - _drawHudNumber(vgaImages, engine.player.ammo, 232, 176); + _drawHudNumber(engine, vgaImages, 1, 32, 176); + _drawHudNumber(engine, vgaImages, engine.player.score, 96, 176); + _drawHudNumber(engine, vgaImages, 3, 120, 176); + _drawHudNumber(engine, vgaImages, engine.player.health, 192, 176); + _drawHudNumber(engine, vgaImages, engine.player.ammo, 232, 176); _drawHudFace(engine, vgaImages); _drawHudWeaponIcon(engine, vgaImages); } void _drawHudNumber( + WolfEngine engine, List vgaImages, int value, int rightAlignX, int startY, ) { - // HUD numbers are rendered with fixed-width VGA glyphs (8 px advance). - const int zeroIndex = 96; + const digitKeys = [ + HudKey.digit0, + HudKey.digit1, + HudKey.digit2, + HudKey.digit3, + HudKey.digit4, + HudKey.digit5, + HudKey.digit6, + HudKey.digit7, + HudKey.digit8, + HudKey.digit9, + ]; + final String numStr = value.toString(); int currentX = rightAlignX - (numStr.length * 8); for (int i = 0; i < numStr.length; i++) { final int digit = int.parse(numStr[i]); - final int imageIndex = zeroIndex + digit; - if (imageIndex < vgaImages.length) { + final ref = engine.data.registry.hud.resolve(digitKeys[digit]); + final imageIndex = ref?.vgaIndex ?? -1; + if (imageIndex >= 0 && imageIndex < vgaImages.length) { blitHudVgaImage(vgaImages[imageIndex], currentX, startY); } currentX += 8; @@ -188,15 +201,24 @@ abstract class RendererBackend } void _drawHudFace(WolfEngine engine, List vgaImages) { - final int faceIndex = hudFaceVgaIndex(engine.player.health); - if (faceIndex < vgaImages.length) { + final faceRef = engine.data.registry.hud.faceForHealth( + engine.player.health, + ); + final int faceIndex = faceRef?.vgaIndex ?? -1; + if (faceIndex >= 0 && faceIndex < vgaImages.length) { blitHudVgaImage(vgaImages[faceIndex], 136, 164); } } void _drawHudWeaponIcon(WolfEngine engine, List vgaImages) { - final int weaponIndex = hudWeaponVgaIndex(engine); - if (weaponIndex < vgaImages.length) { + final HudKey weaponKey = engine.player.hasChainGun + ? HudKey.chainGunIcon + : engine.player.hasMachineGun + ? HudKey.machineGunIcon + : HudKey.pistolIcon; + final weaponRef = engine.data.registry.hud.resolve(weaponKey); + final int weaponIndex = weaponRef?.vgaIndex ?? -1; + if (weaponIndex >= 0 && weaponIndex < vgaImages.length) { blitHudVgaImage(vgaImages[weaponIndex], 256, 164); } } diff --git a/packages/wolf_3d_dart/test/registry/shareware_hud_module_test.dart b/packages/wolf_3d_dart/test/registry/shareware_hud_module_test.dart new file mode 100644 index 0000000..2c6f9aa --- /dev/null +++ b/packages/wolf_3d_dart/test/registry/shareware_hud_module_test.dart @@ -0,0 +1,48 @@ +import 'package:test/test.dart'; +import 'package:wolf_3d_dart/src/registry/built_in/shareware_hud_module.dart'; +import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; + +void main() { + group('SharewareHudModule', () { + test('uses fixed WL1 offset in strict mode', () { + final module = SharewareHudModule(useOriginalWl1Map: true); + + expect(module.resolve(HudKey.statusBar)?.vgaIndex, 95); + expect(module.resolve(HudKey.digit0)?.vgaIndex, 108); + expect(module.resolve(HudKey.digit9)?.vgaIndex, 117); + expect(module.resolve(HudKey.faceHealthy)?.vgaIndex, 118); + expect(module.resolve(HudKey.faceDead)?.vgaIndex, 139); + expect(module.resolve(HudKey.pistolIcon)?.vgaIndex, 101); + expect(module.resolve(HudKey.machineGunIcon)?.vgaIndex, 102); + expect(module.resolve(HudKey.chainGunIcon)?.vgaIndex, 103); + }); + + test('detects shareware HUD offset from statusbar in heuristic mode', () { + final module = SharewareHudModule(); + final sizes = List.generate(140, (_) => (width: 16, height: 16)); + sizes[95] = (width: 320, height: 40); + + module.initWithImageSizes(sizes); + + expect(module.resolve(HudKey.statusBar)?.vgaIndex, 95); + expect(module.resolve(HudKey.digit0)?.vgaIndex, 108); + expect(module.resolve(HudKey.faceHealthy)?.vgaIndex, 118); + }); + }); + + group('BuiltInAssetRegistryResolver HUD selection', () { + test('uses shareware HUD mapping for exact WL1 identity', () { + const resolver = BuiltInAssetRegistryResolver(); + final registry = resolver.resolve( + const RegistrySelectionContext( + gameVersion: GameVersion.shareware, + dataVersion: DataVersion.version14Shareware, + ), + ); + + expect(registry.hud.resolve(HudKey.statusBar)?.vgaIndex, 95); + expect(registry.hud.resolve(HudKey.digit0)?.vgaIndex, 108); + expect(registry.hud.resolve(HudKey.faceDead)?.vgaIndex, 139); + }); + }); +}