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))
|
.map((img) => (width: img.width, height: img.height))
|
||||||
.toList();
|
.toList();
|
||||||
registry.sharewareMenu.initWithImageSizes(sizes);
|
registry.sharewareMenu.initWithImageSizes(sizes);
|
||||||
|
registry.sharewareHud.initWithImageSizes(sizes);
|
||||||
}
|
}
|
||||||
|
|
||||||
return registry;
|
return registry;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:wolf_3d_dart/src/registry/asset_registry.dart';
|
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/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_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_menu_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/shareware_music_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).
|
/// - SFX slots are identical to retail (same AUDIOT layout).
|
||||||
/// - Music routing uses the 10-level shareware table.
|
/// - Music routing uses the 10-level shareware table.
|
||||||
/// - Entity definitions are limited to the three shareware enemies.
|
/// - 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
|
/// - Menu picture indices are resolved via runtime heuristic offset; call
|
||||||
/// [SharewareMenuPicModule.initWithImageSizes] after the VGA images are
|
/// [SharewareMenuPicModule.initWithImageSizes] after the VGA images are
|
||||||
/// available so the shift is computed correctly.
|
/// available so the shift is computed correctly.
|
||||||
@@ -20,7 +20,9 @@ class SharewareAssetRegistry extends AssetRegistry {
|
|||||||
sfx: const RetailSfxModule(),
|
sfx: const RetailSfxModule(),
|
||||||
music: const SharewareMusicModule(),
|
music: const SharewareMusicModule(),
|
||||||
entities: const SharewareEntityModule(),
|
entities: const SharewareEntityModule(),
|
||||||
hud: const RetailHudModule(),
|
hud: SharewareHudModule(
|
||||||
|
useOriginalWl1Map: strictOriginalShareware,
|
||||||
|
),
|
||||||
menu: SharewareMenuPicModule(
|
menu: SharewareMenuPicModule(
|
||||||
useOriginalWl1Map: strictOriginalShareware,
|
useOriginalWl1Map: strictOriginalShareware,
|
||||||
),
|
),
|
||||||
@@ -28,4 +30,7 @@ class SharewareAssetRegistry extends AssetRegistry {
|
|||||||
|
|
||||||
/// Convenience accessor to the menu module for post-load initialisation.
|
/// Convenience accessor to the menu module for post-load initialisation.
|
||||||
SharewareMenuPicModule get sharewareMenu => menu as SharewareMenuPicModule;
|
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].
|
/// backend can scale/map them consistently via [blitHudVgaImage].
|
||||||
void drawStandardVgaHud(WolfEngine engine) {
|
void drawStandardVgaHud(WolfEngine engine) {
|
||||||
final List<VgaImage> vgaImages = engine.data.vgaImages;
|
final List<VgaImage> vgaImages = engine.data.vgaImages;
|
||||||
final int statusBarIndex = vgaImages.indexWhere(
|
final statusBarRef = engine.data.registry.hud.resolve(HudKey.statusBar);
|
||||||
(img) => img.width == 320 && img.height == 40,
|
final int statusBarIndex = statusBarRef?.vgaIndex ?? -1;
|
||||||
);
|
if (statusBarIndex >= 0 && statusBarIndex < vgaImages.length) {
|
||||||
if (statusBarIndex == -1) return;
|
|
||||||
|
|
||||||
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(engine, vgaImages, 1, 32, 176);
|
||||||
_drawHudNumber(vgaImages, engine.player.health, 192, 176);
|
_drawHudNumber(engine, vgaImages, engine.player.score, 96, 176);
|
||||||
_drawHudNumber(vgaImages, engine.player.ammo, 232, 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);
|
_drawHudFace(engine, vgaImages);
|
||||||
_drawHudWeaponIcon(engine, vgaImages);
|
_drawHudWeaponIcon(engine, vgaImages);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawHudNumber(
|
void _drawHudNumber(
|
||||||
|
WolfEngine engine,
|
||||||
List<VgaImage> vgaImages,
|
List<VgaImage> vgaImages,
|
||||||
int value,
|
int value,
|
||||||
int rightAlignX,
|
int rightAlignX,
|
||||||
int startY,
|
int startY,
|
||||||
) {
|
) {
|
||||||
// HUD numbers are rendered with fixed-width VGA glyphs (8 px advance).
|
const digitKeys = [
|
||||||
const int zeroIndex = 96;
|
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();
|
final String numStr = value.toString();
|
||||||
int currentX = rightAlignX - (numStr.length * 8);
|
int currentX = rightAlignX - (numStr.length * 8);
|
||||||
|
|
||||||
for (int i = 0; i < numStr.length; i++) {
|
for (int i = 0; i < numStr.length; i++) {
|
||||||
final int digit = int.parse(numStr[i]);
|
final int digit = int.parse(numStr[i]);
|
||||||
final int imageIndex = zeroIndex + digit;
|
final ref = engine.data.registry.hud.resolve(digitKeys[digit]);
|
||||||
if (imageIndex < vgaImages.length) {
|
final imageIndex = ref?.vgaIndex ?? -1;
|
||||||
|
if (imageIndex >= 0 && imageIndex < vgaImages.length) {
|
||||||
blitHudVgaImage(vgaImages[imageIndex], currentX, startY);
|
blitHudVgaImage(vgaImages[imageIndex], currentX, startY);
|
||||||
}
|
}
|
||||||
currentX += 8;
|
currentX += 8;
|
||||||
@@ -188,15 +201,24 @@ abstract class RendererBackend<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _drawHudFace(WolfEngine engine, List<VgaImage> vgaImages) {
|
void _drawHudFace(WolfEngine engine, List<VgaImage> vgaImages) {
|
||||||
final int faceIndex = hudFaceVgaIndex(engine.player.health);
|
final faceRef = engine.data.registry.hud.faceForHealth(
|
||||||
if (faceIndex < vgaImages.length) {
|
engine.player.health,
|
||||||
|
);
|
||||||
|
final int faceIndex = faceRef?.vgaIndex ?? -1;
|
||||||
|
if (faceIndex >= 0 && faceIndex < vgaImages.length) {
|
||||||
blitHudVgaImage(vgaImages[faceIndex], 136, 164);
|
blitHudVgaImage(vgaImages[faceIndex], 136, 164);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawHudWeaponIcon(WolfEngine engine, List<VgaImage> vgaImages) {
|
void _drawHudWeaponIcon(WolfEngine engine, List<VgaImage> vgaImages) {
|
||||||
final int weaponIndex = hudWeaponVgaIndex(engine);
|
final HudKey weaponKey = engine.player.hasChainGun
|
||||||
if (weaponIndex < vgaImages.length) {
|
? 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);
|
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