feat: Add Spear of Destiny demo support with dedicated asset registry and entity definitions

- Introduced SpearDemoAssetRegistry for managing assets specific to the Spear of Destiny demo.
- Created SpearDemoEntityModule to define enemy animations with adjusted sprite ranges.
- Implemented SpearDemoHudModule and SpearDemoMenuPicModule for HUD and menu assets.
- Added SpearDemoSfxModule for sound effect mappings specific to the demo version.
- Updated enemy classes (Guard, Mutant, Officer, SS) to support custom animation sets.
- Modified entity registry to accept a custom AssetRegistry for spawning entities.
- Enhanced rendering with CRT phosphor bloom effect in GLSL shaders.
- Adjusted ASCII and software renderer layouts for improved UI spacing.
- Added tests for SpearDemoAssetRegistry to ensure correct asset resolution and enemy spawning.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-23 10:37:50 +01:00
parent 528d6276b1
commit a84c677845
28 changed files with 641 additions and 70 deletions
@@ -0,0 +1,80 @@
import 'package:test/test.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
void main() {
group('SpearDemoAssetRegistry', () {
test('resolves SDM menu and HUD VGA indices', () {
final registry = SpearDemoAssetRegistry();
expect(
registry.menu.resolve(MenuPicKey.controlBackground)?.pictureIndex,
12,
);
expect(registry.menu.resolve(MenuPicKey.optionsLabel)?.pictureIndex, 13);
expect(registry.menu.resolve(MenuPicKey.episode1), isNull);
expect(registry.hud.resolve(HudKey.statusBar)?.vgaIndex, 73);
expect(registry.hud.resolve(HudKey.digit0)?.vgaIndex, 84);
expect(registry.hud.resolve(HudKey.pistolIcon)?.vgaIndex, 77);
});
test('resolves SDM enemy sprite ranges with +4 SPEAR shift', () {
final registry = SpearDemoAssetRegistry();
final guard = registry.entities.resolve(EntityKey.guard);
final dog = registry.entities.resolve(EntityKey.dog);
expect(guard?.animations?.idle.start, 54);
expect(guard?.animations?.attacking.end, 102);
expect(dog?.animations?.walking.start, 103);
expect(dog?.animations?.dead.start, 138);
});
test('uses SDM digitized sound slot mapping', () {
final registry = SpearDemoAssetRegistry();
expect(registry.sfx.resolve(SoundEffect.openDoor)?.slotIndex, 3);
expect(registry.sfx.resolve(SoundEffect.closeDoor)?.slotIndex, 2);
expect(registry.sfx.resolve(SoundEffect.pushWall)?.slotIndex, 13);
expect(registry.sfx.resolve(SoundEffect.levelComplete)?.slotIndex, 22);
});
});
group('BuiltInAssetRegistryResolver Spear SDM selection', () {
test('uses Spear demo registry for exact SDM identity', () {
const resolver = BuiltInAssetRegistryResolver();
final registry = resolver.resolve(
const RegistrySelectionContext(
gameVersion: GameVersion.spearOfDestinyDemo,
dataVersion: DataVersion.spearOfDestinyShareware,
),
);
expect(registry, isA<SpearDemoAssetRegistry>());
expect(registry.hud.resolve(HudKey.statusBar)?.vgaIndex, 73);
expect(registry.sfx.resolve(SoundEffect.openDoor)?.slotIndex, 3);
expect(
registry.entities.resolve(EntityKey.guard)?.animations?.idle.start,
54,
);
});
test('runtime enemy spawn consumes registry animation ranges', () {
final registry = SpearDemoAssetRegistry();
final enemy = Enemy.spawn(
MapObject.guardStart,
4.5,
5.5,
Difficulty.medium,
registry: registry,
);
expect(enemy, isA<Guard>());
expect(enemy?.state, EntityState.ambushing);
expect(enemy?.spriteIndex, 54); // SDM guard idle start
});
});
}