Refactor collectible handling and scoring system

- Introduced `CollectiblePickupContext` and `CollectiblePickupEffect` to streamline the collectible pickup logic in the Player class.
- Updated the `tryPickup` method in the Player class to utilize the new effect system for health, ammo, score, keys, and weapons.
- Refactored collectible classes to implement the new `tryCollect` method, returning a `CollectiblePickupEffect`.
- Enhanced enemy classes to expose score values based on their type.
- Added unit tests for scoring ownership and shareware menu module functionality.
- Updated rendering logic in various renderer classes to use the new mapped picture retrieval system.
- Improved the `WolfClassicMenuArt` class to utilize a more structured approach for image retrieval based on registry keys.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-19 14:25:14 +01:00
parent 6e53da7095
commit 57dde0f31c
15 changed files with 584 additions and 223 deletions

View File

@@ -21,13 +21,7 @@ void main() {
engine.init();
engine.entities.add(
Collectible(
x: engine.player.x,
y: engine.player.y,
spriteIndex: 0,
mapId: MapObject.ammoClip,
type: CollectibleType.ammo,
),
AmmoCollectible(x: engine.player.x, y: engine.player.y),
);
engine.tick(const Duration(milliseconds: 16));

View File

@@ -0,0 +1,39 @@
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('Scoring ownership', () {
test('treasure collectibles expose score values', () {
final cross = TreasureCollectible(x: 1, y: 1, mapId: MapObject.cross);
final chalice = TreasureCollectible(
x: 1,
y: 1,
mapId: MapObject.chalice,
);
final chest = TreasureCollectible(x: 1, y: 1, mapId: MapObject.chest);
final crown = TreasureCollectible(x: 1, y: 1, mapId: MapObject.crown);
final extraLife = TreasureCollectible(
x: 1,
y: 1,
mapId: MapObject.extraLife,
);
expect(cross.scoreValue, 100);
expect(chalice.scoreValue, 500);
expect(chest.scoreValue, 1000);
expect(crown.scoreValue, 5000);
expect(extraLife.scoreValue, 0);
});
test('enemy instances expose score values from enemy type metadata', () {
final guard = Guard(x: 1, y: 1, angle: 0, mapId: MapObject.guardStart);
final dog = Dog(x: 1, y: 1, angle: 0, mapId: MapObject.dogStart);
final ss = SS(x: 1, y: 1, angle: 0, mapId: MapObject.ssStart);
expect(guard.scoreValue, 100);
expect(dog.scoreValue, 200);
expect(ss.scoreValue, 100);
});
});
}

View File

@@ -0,0 +1,88 @@
import 'package:test/test.dart';
import 'package:wolf_3d_dart/src/registry/built_in/shareware_menu_module.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
void main() {
group('SharewareMenuPicModule', () {
test('uses fixed canonical WL1 mapping when strict mode is enabled', () {
final module = SharewareMenuPicModule(useOriginalWl1Map: true);
expect(module.resolve(MenuPicKey.title)?.pictureIndex, 96);
expect(module.resolve(MenuPicKey.pg13)?.pictureIndex, 97);
expect(module.resolve(MenuPicKey.credits)?.pictureIndex, 98);
expect(module.resolve(MenuPicKey.cursorActive)?.pictureIndex, 20);
expect(module.resolve(MenuPicKey.footer)?.pictureIndex, 27);
expect(module.resolve(MenuPicKey.difficultyEasy)?.pictureIndex, 29);
expect(module.resolve(MenuPicKey.episode1)?.pictureIndex, 39);
});
test('returns null for unavailable WL1 episode art in strict mode', () {
final module = SharewareMenuPicModule(useOriginalWl1Map: true);
expect(module.resolve(MenuPicKey.episode2), isNull);
expect(module.resolve(MenuPicKey.episode3), isNull);
expect(module.resolve(MenuPicKey.episode4), isNull);
expect(module.resolve(MenuPicKey.episode5), isNull);
expect(module.resolve(MenuPicKey.episode6), isNull);
});
test('heuristic mode only shifts statusbar-era indices', () {
final module = SharewareMenuPicModule();
final imageSizes = List.generate(
90,
(_) => (width: 64, height: 64),
);
imageSizes[35] = (width: 320, height: 40); // STATUSBARPIC-like
imageSizes[36] = (width: 320, height: 200); // TITLEPIC-like
imageSizes[37] = (width: 320, height: 200); // PG13PIC-like
imageSizes[38] = (width: 320, height: 200); // CREDITSPIC-like
module.initWithImageSizes(imageSizes);
// 84 shifted by -48.
expect(module.resolve(MenuPicKey.title)?.pictureIndex, 36);
// Control-panel assets should remain unshifted.
expect(module.resolve(MenuPicKey.difficultyEasy)?.pictureIndex, 17);
expect(module.resolve(MenuPicKey.episode1)?.pictureIndex, 27);
});
});
group('BuiltInAssetRegistryResolver shareware selection', () {
test(
'uses strict canonical WL1 map for exact known shareware identity',
() {
const resolver = BuiltInAssetRegistryResolver();
final registry = resolver.resolve(
const RegistrySelectionContext(
gameVersion: GameVersion.shareware,
dataVersion: DataVersion.version14Shareware,
),
);
expect(registry.menu.resolve(MenuPicKey.title)?.pictureIndex, 96);
expect(
registry.menu.resolve(MenuPicKey.cursorActive)?.pictureIndex,
20,
);
expect(registry.menu.resolve(MenuPicKey.episode2), isNull);
},
);
test('uses flexible shareware fallback for unknown identities', () {
const resolver = BuiltInAssetRegistryResolver();
final registry = resolver.resolve(
const RegistrySelectionContext(
gameVersion: GameVersion.shareware,
dataVersion: DataVersion.unknown,
),
);
// Unknown shareware falls back to baseline until image-size heuristic init.
expect(registry.menu.resolve(MenuPicKey.title)?.pictureIndex, 84);
expect(registry.menu.resolve(MenuPicKey.episode2)?.pictureIndex, 28);
});
});
}

View File

@@ -0,0 +1,43 @@
import 'dart:typed_data';
import 'package:test/test.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_menu.dart';
void main() {
test(
'legacy numeric menu IDs are remapped through registry in shareware',
() {
final data = WolfensteinData(
version: GameVersion.shareware,
dataVersion: DataVersion.version14Shareware,
registry: SharewareAssetRegistry(strictOriginalShareware: true),
walls: const [],
sprites: const [],
sounds: const [],
adLibSounds: const [],
music: const [],
episodes: const [],
vgaImages: List.generate(120, (i) => _img(height: i + 1)),
);
final art = WolfClassicMenuArt(data);
// Retail cursor constants 8/9 must resolve to shareware cursor IDs 20/21.
expect(art.mappedPic(8)?.height, 21);
expect(art.mappedPic(9)?.height, 22);
// Retail footer constant 15 must resolve to shareware footer ID 27.
expect(art.mappedPic(15)?.height, 28);
},
);
}
VgaImage _img({required int height}) {
const width = 4;
return VgaImage(
width: width,
height: height,
pixels: Uint8List(width * height),
);
}