Add shareware HUD module and integrate with asset registry for improved HUD handling
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<HudKey> _faceBands = [
|
||||
HudKey.faceHealthy,
|
||||
HudKey.faceScratched,
|
||||
HudKey.faceHurt,
|
||||
HudKey.faceWounded,
|
||||
HudKey.faceBadlyWounded,
|
||||
HudKey.faceDying,
|
||||
HudKey.faceNearDeath,
|
||||
];
|
||||
|
||||
static final Map<HudKey, int> _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];
|
||||
}
|
||||
}
|
||||
@@ -151,36 +151,49 @@ abstract class RendererBackend<T>
|
||||
/// backend can scale/map them consistently via [blitHudVgaImage].
|
||||
void drawStandardVgaHud(WolfEngine engine) {
|
||||
final List<VgaImage> 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);
|
||||
_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<VgaImage> 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<T>
|
||||
}
|
||||
|
||||
void _drawHudFace(WolfEngine engine, List<VgaImage> 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<VgaImage> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user