Add built-in asset modules for Wolfenstein 3D v1.4 Shareware release
- Implement RetailSfxModule to map sound effects to numeric slots. - Create SharewareAssetRegistry to manage assets for the Shareware version. - Introduce SharewareEntityModule to define available enemies in Shareware. - Add SharewareMenuPicModule to handle menu pictures with runtime offset computation. - Implement SharewareMusicModule for music routing in Shareware. - Define keys for entities, HUD elements, menu pictures, and music tracks. - Create abstract modules for entity, HUD, menu picture, and music assets. - Add registry resolver to select appropriate asset registry based on game version and data version. - Update WolfensteinData to include new asset registry exports. - Modify tests to utilize the new asset registry structure for Shareware and Retail versions. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -2,7 +2,6 @@ import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:wolf_3d_dart/src/data/data_version.dart';
|
||||
import 'package:wolf_3d_dart/src/data/wl_parser.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart' show md5;
|
||||
import 'package:wolf_3d_dart/src/data/data_version.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
/// The primary parser for Wolfenstein 3D data formats.
|
||||
@@ -99,6 +98,70 @@ abstract class WLParser {
|
||||
);
|
||||
}
|
||||
|
||||
/// Async file-by-file equivalent of [loadAsync]; exposed for use from
|
||||
/// external callers that need a [registryOverride] in async contexts.
|
||||
static Future<WolfensteinData> loadAsyncWithOverride(
|
||||
Future<ByteData> Function(String filename) fileFetcher, {
|
||||
AssetRegistry? registryOverride,
|
||||
}) async {
|
||||
GameVersion? detectedVersion;
|
||||
ByteData? vswap;
|
||||
|
||||
for (final version in GameVersion.values) {
|
||||
try {
|
||||
vswap = await fileFetcher('VSWAP.${version.fileExtension}');
|
||||
detectedVersion = version;
|
||||
break;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (detectedVersion == null || vswap == null) {
|
||||
throw Exception('Could not locate a valid VSWAP file.');
|
||||
}
|
||||
|
||||
final ext = detectedVersion.fileExtension;
|
||||
final vswapBytes = vswap.buffer.asUint8List(
|
||||
vswap.offsetInBytes,
|
||||
vswap.lengthInBytes,
|
||||
);
|
||||
final vswapHash = md5.convert(vswapBytes).toString();
|
||||
final dataIdentity = DataVersion.fromChecksum(vswapHash);
|
||||
|
||||
ByteData gameMapsData;
|
||||
if (dataIdentity == DataVersion.version10Retail) {
|
||||
try {
|
||||
gameMapsData = await fileFetcher('MAPTEMP.$ext');
|
||||
} catch (_) {
|
||||
gameMapsData = await fileFetcher('GAMEMAPS.$ext');
|
||||
}
|
||||
} else {
|
||||
gameMapsData = await fileFetcher('GAMEMAPS.$ext');
|
||||
}
|
||||
|
||||
final rawFiles = {
|
||||
'MAPHEAD.$ext': await fileFetcher('MAPHEAD.$ext'),
|
||||
'VGADICT.$ext': await fileFetcher('VGADICT.$ext'),
|
||||
'VGAHEAD.$ext': await fileFetcher('VGAHEAD.$ext'),
|
||||
'VGAGRAPH.$ext': await fileFetcher('VGAGRAPH.$ext'),
|
||||
'AUDIOHED.$ext': await fileFetcher('AUDIOHED.$ext'),
|
||||
'AUDIOT.$ext': await fileFetcher('AUDIOT.$ext'),
|
||||
};
|
||||
|
||||
return load(
|
||||
version: detectedVersion,
|
||||
dataIdentity: dataIdentity,
|
||||
vswap: vswap,
|
||||
mapHead: rawFiles['MAPHEAD.$ext']!,
|
||||
gameMaps: gameMapsData,
|
||||
vgaDict: rawFiles['VGADICT.$ext']!,
|
||||
vgaHead: rawFiles['VGAHEAD.$ext']!,
|
||||
vgaGraph: rawFiles['VGAGRAPH.$ext']!,
|
||||
audioHed: rawFiles['AUDIOHED.$ext']!,
|
||||
audioT: rawFiles['AUDIOT.$ext']!,
|
||||
registryOverride: registryOverride,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parses all raw ByteData upfront and returns a fully populated [WolfensteinData] object.
|
||||
///
|
||||
/// By using named parameters, the compiler guarantees no files are missing or misnamed.
|
||||
@@ -115,28 +178,61 @@ abstract class WLParser {
|
||||
required ByteData audioHed,
|
||||
required ByteData audioT,
|
||||
required DataVersion dataIdentity,
|
||||
AssetRegistry? registryOverride,
|
||||
}) {
|
||||
final isShareware = version == GameVersion.shareware;
|
||||
|
||||
// v1.0/1.1 used different HUD strings and had different secret wall bugs
|
||||
final isLegacy =
|
||||
dataIdentity == DataVersion.version10Retail ||
|
||||
dataIdentity == DataVersion.version11Retail;
|
||||
|
||||
final audio = parseAudio(audioHed, audioT, version);
|
||||
final vgaImages = parseVgaImages(vgaDict, vgaHead, vgaGraph);
|
||||
|
||||
// Resolve the appropriate registry for this data identity, unless the
|
||||
// caller has supplied an explicit override (e.g. a modded asset pack).
|
||||
final registry =
|
||||
registryOverride ??
|
||||
_resolveRegistry(
|
||||
version: version,
|
||||
dataVersion: dataIdentity,
|
||||
vgaImages: vgaImages,
|
||||
);
|
||||
|
||||
return WolfensteinData(
|
||||
version: version,
|
||||
dataVersion: dataIdentity,
|
||||
registry: registry,
|
||||
walls: parseWalls(vswap),
|
||||
sprites: parseSprites(vswap),
|
||||
sounds: parseSounds(vswap).map((bytes) => PcmSound(bytes)).toList(),
|
||||
episodes: parseEpisodes(mapHead, gameMaps, isShareware: isShareware),
|
||||
vgaImages: parseVgaImages(vgaDict, vgaHead, vgaGraph),
|
||||
vgaImages: vgaImages,
|
||||
adLibSounds: audio.adLib,
|
||||
music: audio.music,
|
||||
);
|
||||
}
|
||||
|
||||
/// Selects the registry for [version]/[dataVersion] and, for shareware,
|
||||
/// initialises the menu module's runtime image-offset heuristic.
|
||||
static AssetRegistry _resolveRegistry({
|
||||
required GameVersion version,
|
||||
required DataVersion dataVersion,
|
||||
required List<VgaImage> vgaImages,
|
||||
}) {
|
||||
final context = RegistrySelectionContext(
|
||||
gameVersion: version,
|
||||
dataVersion: dataVersion,
|
||||
);
|
||||
final registry = const BuiltInAssetRegistryResolver().resolve(context);
|
||||
|
||||
// Initialise the shareware menu heuristic now that images are available.
|
||||
if (registry is SharewareAssetRegistry) {
|
||||
final sizes = vgaImages
|
||||
.map((img) => (width: img.width, height: img.height))
|
||||
.toList();
|
||||
registry.sharewareMenu.initWithImageSizes(sizes);
|
||||
}
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
/// Extracts the 64x64 wall textures from the VSWAP file.
|
||||
///
|
||||
/// Wall textures are stored sequentially at the beginning of the chunks array.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart'; // Import for MD5
|
||||
import 'package:wolf_3d_dart/src/data/data_version.dart'; // Import your enum
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
import 'io/discovery_stub.dart'
|
||||
@@ -33,6 +32,10 @@ class WolfensteinLoader {
|
||||
/// Use this for Web or Flutter assets where automated directory scanning
|
||||
/// is not possible. It automatically detects the [DataVersion] (v1.0, v1.4, etc.)
|
||||
/// by hashing the [vswap] buffer.
|
||||
///
|
||||
/// Supply [registryOverride] to use a fully custom [AssetRegistry] instead
|
||||
/// of the built-in version-detected registry. This is the primary extension
|
||||
/// point for modded or custom asset packs.
|
||||
static WolfensteinData loadFromBytes({
|
||||
required GameVersion version,
|
||||
required ByteData? vswap,
|
||||
@@ -43,6 +46,7 @@ class WolfensteinLoader {
|
||||
required ByteData? vgaGraph,
|
||||
required ByteData? audioHed,
|
||||
required ByteData? audioT,
|
||||
AssetRegistry? registryOverride,
|
||||
}) {
|
||||
// 1. Validation Check
|
||||
final Map<String, ByteData?> files = {
|
||||
@@ -67,7 +71,7 @@ class WolfensteinLoader {
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Identify identity for specialized parsing (e.g., MAPTEMP vs GAMEMAPS)
|
||||
// 2. Identify identity for specialised parsing (e.g., MAPTEMP vs GAMEMAPS).
|
||||
final vswapBytes = vswap!.buffer.asUint8List(
|
||||
vswap.offsetInBytes,
|
||||
vswap.lengthInBytes,
|
||||
@@ -75,10 +79,9 @@ class WolfensteinLoader {
|
||||
final hash = md5.convert(vswapBytes).toString();
|
||||
final dataIdentity = DataVersion.fromChecksum(hash);
|
||||
|
||||
// 3. Pass-through to parser with the detected identity
|
||||
// 3. Pass-through to parser with the detected identity and optional override.
|
||||
return WLParser.load(
|
||||
version: version,
|
||||
// Correctly identifies v1.0/1.1/1.4
|
||||
dataIdentity: dataIdentity,
|
||||
vswap: vswap,
|
||||
mapHead: mapHead!,
|
||||
@@ -88,6 +91,7 @@ class WolfensteinLoader {
|
||||
vgaGraph: vgaGraph!,
|
||||
audioHed: audioHed!,
|
||||
audioT: audioT!,
|
||||
registryOverride: registryOverride,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'package:wolf_3d_dart/src/data_types/sprite_frame_range.dart';
|
||||
|
||||
/// The set of animation states an enemy can be in.
|
||||
enum EnemyAnimation { idle, walking, attacking, pain, dying, dead }
|
||||
|
||||
/// A mapping container that defines the sprite index ranges for an enemy's life cycle.
|
||||
///
|
||||
/// Wolfenstein 3D enemies use specific ranges within the global sprite list
|
||||
/// to handle state transitions like walking, attacking, or dying.
|
||||
class EnemyAnimationMap {
|
||||
final SpriteFrameRange idle;
|
||||
final SpriteFrameRange walking;
|
||||
final SpriteFrameRange attacking;
|
||||
final SpriteFrameRange pain;
|
||||
final SpriteFrameRange dying;
|
||||
final SpriteFrameRange dead;
|
||||
|
||||
const EnemyAnimationMap({
|
||||
required this.idle,
|
||||
required this.walking,
|
||||
required this.attacking,
|
||||
required this.pain,
|
||||
required this.dying,
|
||||
required this.dead,
|
||||
});
|
||||
|
||||
/// Identifies which [EnemyAnimation] state a specific [spriteIndex] belongs to.
|
||||
///
|
||||
/// Returns `null` if the index is outside this enemy's defined animation ranges.
|
||||
EnemyAnimation? getAnimation(int spriteIndex) {
|
||||
if (idle.contains(spriteIndex)) return EnemyAnimation.idle;
|
||||
if (walking.contains(spriteIndex)) return EnemyAnimation.walking;
|
||||
if (attacking.contains(spriteIndex)) return EnemyAnimation.attacking;
|
||||
if (pain.contains(spriteIndex)) return EnemyAnimation.pain;
|
||||
if (dying.contains(spriteIndex)) return EnemyAnimation.dying;
|
||||
if (dead.contains(spriteIndex)) return EnemyAnimation.dead;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// All animation states mapped to their corresponding [SpriteFrameRange].
|
||||
Map<EnemyAnimation, SpriteFrameRange> get allRanges => {
|
||||
EnemyAnimation.idle: idle,
|
||||
EnemyAnimation.walking: walking,
|
||||
EnemyAnimation.attacking: attacking,
|
||||
EnemyAnimation.pain: pain,
|
||||
EnemyAnimation.dying: dying,
|
||||
EnemyAnimation.dead: dead,
|
||||
};
|
||||
|
||||
/// Returns the [SpriteFrameRange] associated with a specific [animation] state.
|
||||
SpriteFrameRange getRange(EnemyAnimation animation) {
|
||||
return switch (animation) {
|
||||
EnemyAnimation.idle => idle,
|
||||
EnemyAnimation.walking => walking,
|
||||
EnemyAnimation.attacking => attacking,
|
||||
EnemyAnimation.pain => pain,
|
||||
EnemyAnimation.dying => dying,
|
||||
EnemyAnimation.dead => dead,
|
||||
};
|
||||
}
|
||||
|
||||
/// Checks if any animation range in this map overlaps with any range in [other].
|
||||
bool overlapsWith(EnemyAnimationMap other) {
|
||||
final myRanges = [idle, walking, attacking, pain, dying, dead];
|
||||
final otherRanges = [
|
||||
other.idle,
|
||||
other.walking,
|
||||
other.attacking,
|
||||
other.pain,
|
||||
other.dying,
|
||||
other.dead,
|
||||
];
|
||||
|
||||
for (final mine in myRanges) {
|
||||
for (final theirs in otherRanges) {
|
||||
if (mine.overlaps(theirs)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,31 @@ import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
/// This object holds the extracted textures, sounds, music, and levels,
|
||||
/// serving as the primary data source for the game engine's renderer
|
||||
/// and audio system.
|
||||
///
|
||||
/// The [registry] field provides version-aware symbolic resolution of all
|
||||
/// asset IDs (sprites, sounds, music, HUD, menus). Consumers should prefer
|
||||
/// registry lookups over hard-coded numeric indices.
|
||||
class WolfensteinData {
|
||||
/// The version of the game these assets belong to (e.g., Shareware or Retail).
|
||||
final GameVersion version;
|
||||
|
||||
/// The exact data-file identity determined from the VSWAP MD5 checksum.
|
||||
///
|
||||
/// Use this when you need to distinguish, for example, v1.0 from v1.4 retail.
|
||||
final DataVersion dataVersion;
|
||||
|
||||
/// The modular asset registry that owns all version-sensitive numeric IDs.
|
||||
///
|
||||
/// Access the five sub-modules via:
|
||||
/// ```dart
|
||||
/// data.registry.sfx.resolve(SfxKey.pistolFire)
|
||||
/// data.registry.music.musicForLevel(episode, level)
|
||||
/// data.registry.entities.resolve(EntityKey.guard)
|
||||
/// data.registry.hud.faceForHealth(player.health)
|
||||
/// data.registry.menu.resolve(MenuPicKey.title)
|
||||
/// ```
|
||||
final AssetRegistry registry;
|
||||
|
||||
/// The collection of 64x64 wall textures extracted from VSWAP.
|
||||
final List<Sprite> walls;
|
||||
|
||||
@@ -33,6 +54,8 @@ class WolfensteinData {
|
||||
|
||||
const WolfensteinData({
|
||||
required this.version,
|
||||
required this.dataVersion,
|
||||
required this.registry,
|
||||
required this.walls,
|
||||
required this.sprites,
|
||||
required this.sounds,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
@@ -1,100 +1,3 @@
|
||||
import 'package:wolf_3d_dart/src/data_types/sprite_frame_range.dart';
|
||||
|
||||
enum EnemyAnimation { idle, walking, attacking, pain, dying, dead }
|
||||
|
||||
/// A mapping container that defines the sprite index ranges for an enemy's life cycle.
|
||||
///
|
||||
/// Wolfenstein 3D enemies use specific ranges within the global sprite list
|
||||
/// to handle state transitions like walking, attacking, or dying.
|
||||
class EnemyAnimationMap {
|
||||
final SpriteFrameRange idle;
|
||||
final SpriteFrameRange walking;
|
||||
final SpriteFrameRange attacking;
|
||||
final SpriteFrameRange pain;
|
||||
final SpriteFrameRange dying;
|
||||
final SpriteFrameRange dead;
|
||||
|
||||
const EnemyAnimationMap({
|
||||
required this.idle,
|
||||
required this.walking,
|
||||
required this.attacking,
|
||||
required this.pain,
|
||||
required this.dying,
|
||||
required this.dead,
|
||||
});
|
||||
|
||||
/// Identifies which [EnemyAnimation] state a specific [spriteIndex] belongs to.
|
||||
///
|
||||
/// Returns `null` if the index is outside this enemy's defined animation ranges.
|
||||
EnemyAnimation? getAnimation(int spriteIndex) {
|
||||
if (idle.contains(spriteIndex)) return EnemyAnimation.idle;
|
||||
if (walking.contains(spriteIndex)) return EnemyAnimation.walking;
|
||||
if (attacking.contains(spriteIndex)) return EnemyAnimation.attacking;
|
||||
if (pain.contains(spriteIndex)) return EnemyAnimation.pain;
|
||||
if (dying.contains(spriteIndex)) return EnemyAnimation.dying;
|
||||
if (dead.contains(spriteIndex)) return EnemyAnimation.dead;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the [SpriteFrameRange] associated with a specific [animation] state.
|
||||
SpriteFrameRange getRange(EnemyAnimation animation) {
|
||||
return switch (animation) {
|
||||
EnemyAnimation.idle => idle,
|
||||
EnemyAnimation.walking => walking,
|
||||
EnemyAnimation.attacking => attacking,
|
||||
EnemyAnimation.pain => pain,
|
||||
EnemyAnimation.dying => dying,
|
||||
EnemyAnimation.dead => dead,
|
||||
};
|
||||
}
|
||||
|
||||
/// Checks if any animation range in this map overlaps with any range in [other].
|
||||
bool overlapsWith(EnemyAnimationMap other) {
|
||||
final myRanges = [idle, walking, attacking, pain, dying, dead];
|
||||
final otherRanges = [
|
||||
other.idle,
|
||||
other.walking,
|
||||
other.attacking,
|
||||
other.pain,
|
||||
other.dying,
|
||||
other.dead,
|
||||
];
|
||||
|
||||
for (final myRange in myRanges) {
|
||||
for (final otherRange in otherRanges) {
|
||||
if (myRange.overlaps(otherRange)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Checks if any animation ranges within this specific enemy overlap with each other.
|
||||
///
|
||||
/// Useful for validating that an enemy isn't trying to use the same sprite
|
||||
/// for two different states (e.g., walking and attacking).
|
||||
bool hasInternalOverlaps() {
|
||||
final ranges = [idle, walking, attacking, pain, dying, dead];
|
||||
|
||||
// Compare every unique pair of ranges
|
||||
for (int i = 0; i < ranges.length; i++) {
|
||||
for (int j = i + 1; j < ranges.length; j++) {
|
||||
if (ranges[i].overlaps(ranges[j])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Map<EnemyAnimation, SpriteFrameRange> get allRanges => {
|
||||
EnemyAnimation.idle: idle,
|
||||
EnemyAnimation.walking: walking,
|
||||
EnemyAnimation.attacking: attacking,
|
||||
EnemyAnimation.pain: pain,
|
||||
EnemyAnimation.dying: dying,
|
||||
EnemyAnimation.dead: dead,
|
||||
};
|
||||
}
|
||||
// This file has been moved to data_types to avoid circular dependencies.
|
||||
// It is kept here as a re-export shim for backwards compatibility.
|
||||
export 'package:wolf_3d_dart/src/data_types/enemy_animation.dart';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
/// Defines the primary enemy types and their behavioral metadata.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_animation.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/enemies/enemy_type.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
45
packages/wolf_3d_dart/lib/src/registry/asset_registry.dart
Normal file
45
packages/wolf_3d_dart/lib/src/registry/asset_registry.dart
Normal file
@@ -0,0 +1,45 @@
|
||||
import 'package:wolf_3d_dart/src/registry/modules/entity_asset_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/modules/hud_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/modules/menu_pic_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/modules/music_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/modules/sfx_module.dart';
|
||||
|
||||
/// The top-level asset registry attached to every [WolfensteinData] instance.
|
||||
///
|
||||
/// Access version-specific asset IDs through the five typed modules:
|
||||
///
|
||||
/// ```dart
|
||||
/// data.registry.sfx.resolve(SfxKey.pistolFire)
|
||||
/// data.registry.music.musicForLevel(episode, level)
|
||||
/// data.registry.entities.resolve(EntityKey.guard)
|
||||
/// data.registry.hud.faceForHealth(player.health)
|
||||
/// data.registry.menu.resolve(MenuPicKey.title)
|
||||
/// ```
|
||||
///
|
||||
/// To provide a fully custom asset layout, implement all five module
|
||||
/// interfaces and pass them to this constructor, then supply the resulting
|
||||
/// [AssetRegistry] to [WolfensteinLoader.loadFromBytes].
|
||||
class AssetRegistry {
|
||||
const AssetRegistry({
|
||||
required this.sfx,
|
||||
required this.music,
|
||||
required this.entities,
|
||||
required this.hud,
|
||||
required this.menu,
|
||||
});
|
||||
|
||||
/// Sound-effect slot resolution.
|
||||
final SfxModule sfx;
|
||||
|
||||
/// Music track routing (menu and per-level).
|
||||
final MusicModule music;
|
||||
|
||||
/// Entity sprite-range and availability data.
|
||||
final EntityAssetModule entities;
|
||||
|
||||
/// HUD image index resolution.
|
||||
final HudModule hud;
|
||||
|
||||
/// Menu VGA picture index resolution.
|
||||
final MenuPicModule menu;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:wolf_3d_dart/src/registry/asset_registry.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/retail_entity_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/retail_hud_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/retail_menu_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/retail_music_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/retail_sfx_module.dart';
|
||||
|
||||
/// The canonical [AssetRegistry] for all retail Wolf3D releases.
|
||||
///
|
||||
/// Composes the five retail built-in modules and preserves the original
|
||||
/// id Software asset layout exactly. All other registries and tests that
|
||||
/// need a stable reference should start from this factory.
|
||||
class RetailAssetRegistry extends AssetRegistry {
|
||||
RetailAssetRegistry()
|
||||
: super(
|
||||
sfx: const RetailSfxModule(),
|
||||
music: const RetailMusicModule(),
|
||||
entities: const RetailEntityModule(),
|
||||
hud: const RetailHudModule(),
|
||||
menu: const RetailMenuPicModule(),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:wolf_3d_dart/src/data_types/enemy_animation.dart';
|
||||
import 'package:wolf_3d_dart/src/data_types/sprite_frame_range.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/keys/entity_key.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/modules/entity_asset_module.dart';
|
||||
|
||||
/// Built-in entity asset module for all retail Wolf3D releases (v1.0, v1.1, v1.4).
|
||||
///
|
||||
/// Sprite ranges mirror the constants that were previously embedded in
|
||||
/// [EnemyType] enum constructors, preserving the original id Software layout.
|
||||
class RetailEntityModule extends EntityAssetModule {
|
||||
const RetailEntityModule();
|
||||
|
||||
static final Map<EntityKey, EntityAssetDefinition> _defs = {
|
||||
EntityKey.guard: const EntityAssetDefinition(
|
||||
availableInShareware: true,
|
||||
animations: EnemyAnimationMap(
|
||||
idle: SpriteFrameRange(50, 57),
|
||||
walking: SpriteFrameRange(58, 89),
|
||||
dying: SpriteFrameRange(90, 93),
|
||||
pain: SpriteFrameRange(94, 94),
|
||||
dead: SpriteFrameRange(95, 95),
|
||||
attacking: SpriteFrameRange(96, 98),
|
||||
),
|
||||
),
|
||||
EntityKey.dog: const EntityAssetDefinition(
|
||||
availableInShareware: true,
|
||||
animations: EnemyAnimationMap(
|
||||
idle: SpriteFrameRange(99, 106),
|
||||
walking: SpriteFrameRange(99, 130),
|
||||
attacking: SpriteFrameRange(135, 137),
|
||||
pain: SpriteFrameRange(0, 0),
|
||||
dying: SpriteFrameRange(131, 133),
|
||||
dead: SpriteFrameRange(134, 134),
|
||||
),
|
||||
),
|
||||
EntityKey.ss: const EntityAssetDefinition(
|
||||
availableInShareware: true,
|
||||
animations: EnemyAnimationMap(
|
||||
idle: SpriteFrameRange(138, 145),
|
||||
walking: SpriteFrameRange(146, 177),
|
||||
attacking: SpriteFrameRange(184, 186),
|
||||
pain: SpriteFrameRange(182, 182),
|
||||
dying: SpriteFrameRange(178, 181),
|
||||
dead: SpriteFrameRange(183, 183),
|
||||
),
|
||||
),
|
||||
EntityKey.mutant: const EntityAssetDefinition(
|
||||
availableInShareware: false,
|
||||
animations: EnemyAnimationMap(
|
||||
idle: SpriteFrameRange(187, 194),
|
||||
walking: SpriteFrameRange(195, 226),
|
||||
attacking: SpriteFrameRange(234, 237),
|
||||
pain: SpriteFrameRange(231, 231),
|
||||
dying: SpriteFrameRange(227, 232, excluded: {231}),
|
||||
dead: SpriteFrameRange(233, 233),
|
||||
),
|
||||
),
|
||||
EntityKey.officer: const EntityAssetDefinition(
|
||||
availableInShareware: false,
|
||||
animations: EnemyAnimationMap(
|
||||
idle: SpriteFrameRange(238, 245),
|
||||
walking: SpriteFrameRange(246, 277),
|
||||
attacking: SpriteFrameRange(285, 287),
|
||||
pain: SpriteFrameRange(282, 282),
|
||||
dying: SpriteFrameRange(278, 283, excluded: {282}),
|
||||
dead: SpriteFrameRange(284, 284),
|
||||
),
|
||||
),
|
||||
// Bosses: no standard EnemyAnimationMap; sprite selection is handled
|
||||
// by custom update() logic in each boss class using baseSpriteIndex.
|
||||
EntityKey.hansGrosse: const EntityAssetDefinition(
|
||||
availableInShareware: false,
|
||||
baseSpriteIndex: 291,
|
||||
),
|
||||
};
|
||||
|
||||
@override
|
||||
EntityAssetDefinition? resolve(EntityKey key) => _defs[key];
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
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 all retail Wolf3D releases (v1.0, v1.1, v1.4).
|
||||
///
|
||||
/// VGA indices match the hard-coded values that were previously scattered
|
||||
/// across [RendererBackend], preserving the original id Software HUD layout.
|
||||
///
|
||||
/// Face health bands follow the original formula:
|
||||
/// `106 + ((100 - health) ~/ 16).clamp(0, 6) * 3`
|
||||
class RetailHudModule extends HudModule {
|
||||
const RetailHudModule();
|
||||
|
||||
// Ordered face keys from healthiest to most wounded (bands 0–6).
|
||||
static const List<HudKey> _faceBands = [
|
||||
HudKey.faceHealthy,
|
||||
HudKey.faceScratched,
|
||||
HudKey.faceHurt,
|
||||
HudKey.faceWounded,
|
||||
HudKey.faceBadlyWounded,
|
||||
HudKey.faceDying,
|
||||
HudKey.faceNearDeath,
|
||||
];
|
||||
|
||||
static final Map<HudKey, int> _indices = {
|
||||
HudKey.statusBar : 83,
|
||||
// Digits — '0' is index 96; subsequent digits follow sequentially.
|
||||
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,
|
||||
// BJ face bands (each band's base; 3 animated variants follow).
|
||||
HudKey.faceHealthy : 106,
|
||||
HudKey.faceScratched : 109,
|
||||
HudKey.faceHurt : 112,
|
||||
HudKey.faceWounded : 115,
|
||||
HudKey.faceBadlyWounded : 118,
|
||||
HudKey.faceDying : 121,
|
||||
HudKey.faceNearDeath : 124,
|
||||
HudKey.faceDead : 127,
|
||||
// Weapon icons.
|
||||
HudKey.pistolIcon : 89,
|
||||
HudKey.machineGunIcon : 90,
|
||||
HudKey.chainGunIcon : 91,
|
||||
};
|
||||
|
||||
@override
|
||||
HudAssetRef? resolve(HudKey key) {
|
||||
final index = _indices[key];
|
||||
return index != null ? HudAssetRef(index) : null;
|
||||
}
|
||||
|
||||
@override
|
||||
HudKey faceKeyForHealth(int health) {
|
||||
if (health <= 0) return HudKey.faceDead;
|
||||
final band = ((100 - health) ~/ 16).clamp(0, _faceBands.length - 1);
|
||||
return _faceBands[band];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'package:wolf_3d_dart/src/data_types/difficulty.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/keys/menu_pic_key.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/modules/menu_pic_module.dart';
|
||||
|
||||
/// Built-in menu-picture module for all retail Wolf3D releases (v1.0, v1.1, v1.4).
|
||||
///
|
||||
/// Picture indices match the [WolfMenuPic] constants that were previously the
|
||||
/// sole authoritative source, preserving the original id Software VGA layout.
|
||||
///
|
||||
/// Note: indices are picture-table indices (not raw VGAGRAPH chunk IDs).
|
||||
/// For example, `C_EPISODE1PIC` is chunk 30 → picture index 27.
|
||||
class RetailMenuPicModule extends MenuPicModule {
|
||||
const RetailMenuPicModule();
|
||||
|
||||
static const List<MenuPicKey> _episodeKeys = [
|
||||
MenuPicKey.episode1,
|
||||
MenuPicKey.episode2,
|
||||
MenuPicKey.episode3,
|
||||
MenuPicKey.episode4,
|
||||
MenuPicKey.episode5,
|
||||
MenuPicKey.episode6,
|
||||
];
|
||||
|
||||
static final Map<MenuPicKey, int> _indices = {
|
||||
// --- Full-screen art ---
|
||||
MenuPicKey.title : 84,
|
||||
MenuPicKey.credits : 86,
|
||||
MenuPicKey.pg13 : 85,
|
||||
// --- Control-panel chrome ---
|
||||
MenuPicKey.controlBackground : 23, // C_CONTROLPIC
|
||||
MenuPicKey.footer : 15, // C_MOUSELBACKPIC (footer art)
|
||||
MenuPicKey.heading : 3, // H_TOPWINDOWPIC
|
||||
MenuPicKey.optionsLabel : 7, // C_OPTIONSPIC
|
||||
// --- Cursor / markers ---
|
||||
MenuPicKey.cursorActive : 8, // C_CURSOR1PIC
|
||||
MenuPicKey.cursorInactive : 9, // C_CURSOR2PIC
|
||||
MenuPicKey.markerSelected : 11, // C_SELECTEDPIC
|
||||
MenuPicKey.markerUnselected : 10, // C_NOTSELECTEDPIC
|
||||
// --- Episode selection ---
|
||||
MenuPicKey.episode1 : 27,
|
||||
MenuPicKey.episode2 : 28,
|
||||
MenuPicKey.episode3 : 29,
|
||||
MenuPicKey.episode4 : 30,
|
||||
MenuPicKey.episode5 : 31,
|
||||
MenuPicKey.episode6 : 32,
|
||||
// --- Difficulty selection ---
|
||||
MenuPicKey.difficultyBaby : 16, // C_BABYMODEPIC
|
||||
MenuPicKey.difficultyEasy : 17, // C_EASYPIC
|
||||
MenuPicKey.difficultyNormal : 18, // C_NORMALPIC
|
||||
MenuPicKey.difficultyHard : 19, // C_HARDPIC
|
||||
};
|
||||
|
||||
@override
|
||||
MenuPicRef? resolve(MenuPicKey key) {
|
||||
final index = _indices[key];
|
||||
return index != null ? MenuPicRef(index) : null;
|
||||
}
|
||||
|
||||
@override
|
||||
MenuPicKey episodeKey(int episodeIndex) {
|
||||
if (episodeIndex >= 0 && episodeIndex < _episodeKeys.length) {
|
||||
return _episodeKeys[episodeIndex];
|
||||
}
|
||||
return MenuPicKey.episode1; // safe fallback
|
||||
}
|
||||
|
||||
@override
|
||||
MenuPicKey difficultyKey(Difficulty difficulty) {
|
||||
return switch (difficulty) {
|
||||
Difficulty.baby => MenuPicKey.difficultyBaby,
|
||||
Difficulty.easy => MenuPicKey.difficultyEasy,
|
||||
Difficulty.medium => MenuPicKey.difficultyNormal,
|
||||
Difficulty.hard => MenuPicKey.difficultyHard,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:wolf_3d_dart/src/registry/keys/music_key.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/modules/music_module.dart';
|
||||
|
||||
/// Built-in music module for all retail Wolf3D releases (v1.0, v1.1, v1.4).
|
||||
///
|
||||
/// Encodes the original id Software level-to-track routing table and sets
|
||||
/// track index 1 as the menu music, exactly matching the original engine.
|
||||
class RetailMusicModule extends MusicModule {
|
||||
const RetailMusicModule();
|
||||
|
||||
// Original WL_INTER.C music table — 60 entries (6 episodes × 10 levels).
|
||||
static const List<int> _levelMap = [
|
||||
2, 3, 4, 5, 2, 3, 4, 5, 6, 7, // Episode 1
|
||||
8, 9, 10, 11, 8, 9, 11, 10, 6, 12, // Episode 2
|
||||
13, 14, 15, 16, 13, 14, 15, 16, 17, 18, // Episode 3
|
||||
2, 3, 4, 5, 2, 3, 4, 5, 6, 7, // Episode 4
|
||||
8, 9, 10, 11, 8, 9, 11, 10, 6, 12, // Episode 5
|
||||
13, 14, 15, 16, 13, 14, 15, 16, 17, 19, // Episode 6
|
||||
];
|
||||
|
||||
// Named MusicKey → track index for keyed lookups.
|
||||
static final Map<MusicKey, int> _named = {
|
||||
MusicKey.menuTheme: 1,
|
||||
MusicKey.level01: 2,
|
||||
MusicKey.level02: 3,
|
||||
MusicKey.level03: 4,
|
||||
MusicKey.level04: 5,
|
||||
MusicKey.level05: 6,
|
||||
MusicKey.level06: 7,
|
||||
MusicKey.level07: 8,
|
||||
MusicKey.level08: 9,
|
||||
MusicKey.level09: 10,
|
||||
MusicKey.level10: 11,
|
||||
MusicKey.level11: 12,
|
||||
MusicKey.level12: 13,
|
||||
MusicKey.level13: 14,
|
||||
MusicKey.level14: 15,
|
||||
MusicKey.level15: 16,
|
||||
MusicKey.level16: 17,
|
||||
MusicKey.level17: 18,
|
||||
MusicKey.level18: 19,
|
||||
};
|
||||
|
||||
@override
|
||||
MusicRoute get menuMusic => const MusicRoute(1);
|
||||
|
||||
@override
|
||||
MusicRoute musicForLevel(int episodeIndex, int levelIndex) {
|
||||
final flat = episodeIndex * 10 + levelIndex;
|
||||
if (flat >= 0 && flat < _levelMap.length) {
|
||||
return MusicRoute(_levelMap[flat]);
|
||||
}
|
||||
// Wrap for custom episode counts beyond the built-in 6.
|
||||
return MusicRoute(_levelMap[flat % _levelMap.length]);
|
||||
}
|
||||
|
||||
@override
|
||||
MusicRoute? resolve(MusicKey key) {
|
||||
final index = _named[key];
|
||||
return index != null ? MusicRoute(index) : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:wolf_3d_dart/src/data_types/sound.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/keys/sfx_key.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/modules/sfx_module.dart';
|
||||
|
||||
/// Built-in SFX module for all retail Wolf3D releases (v1.0, v1.1, v1.4).
|
||||
///
|
||||
/// Maps every [SfxKey] to the corresponding numeric slot from [WolfSound],
|
||||
/// preserving the original id Software audio index layout exactly.
|
||||
class RetailSfxModule extends SfxModule {
|
||||
const RetailSfxModule();
|
||||
|
||||
static final Map<SfxKey, int> _slots = {
|
||||
// --- Doors & Environment ---
|
||||
SfxKey.openDoor : WolfSound.openDoor,
|
||||
SfxKey.closeDoor : WolfSound.closeDoor,
|
||||
SfxKey.pushWall : WolfSound.pushWall,
|
||||
// --- Weapons ---
|
||||
SfxKey.knifeAttack : WolfSound.knifeAttack,
|
||||
SfxKey.pistolFire : WolfSound.pistolFire,
|
||||
SfxKey.machineGunFire : WolfSound.machineGunFire,
|
||||
SfxKey.chainGunFire : WolfSound.gatlingFire,
|
||||
SfxKey.enemyFire : WolfSound.naziFire,
|
||||
// --- Pickups ---
|
||||
SfxKey.getMachineGun : WolfSound.getMachineGun,
|
||||
SfxKey.getAmmo : WolfSound.getAmmo,
|
||||
SfxKey.getChainGun : WolfSound.getGatling,
|
||||
SfxKey.healthSmall : WolfSound.healthSmall,
|
||||
SfxKey.healthLarge : WolfSound.healthLarge,
|
||||
SfxKey.treasure1 : WolfSound.treasure1,
|
||||
SfxKey.treasure2 : WolfSound.treasure2,
|
||||
SfxKey.treasure3 : WolfSound.treasure3,
|
||||
SfxKey.treasure4 : WolfSound.treasure4,
|
||||
SfxKey.extraLife : WolfSound.extraLife,
|
||||
// --- Standard Enemies ---
|
||||
SfxKey.guardHalt : WolfSound.guardHalt,
|
||||
SfxKey.dogBark : WolfSound.dogBark,
|
||||
SfxKey.dogDeath : WolfSound.dogDeath,
|
||||
SfxKey.dogAttack : WolfSound.dogAttack,
|
||||
SfxKey.deathScream1 : WolfSound.deathScream1,
|
||||
SfxKey.deathScream2 : WolfSound.deathScream2,
|
||||
SfxKey.deathScream3 : WolfSound.deathScream3,
|
||||
SfxKey.ssAlert : WolfSound.ssSchutzstaffel,
|
||||
SfxKey.ssDeath : WolfSound.ssMeinGott,
|
||||
// --- Bosses ---
|
||||
SfxKey.bossActive : WolfSound.bossActive,
|
||||
SfxKey.hansGrosseDeath : WolfSound.mutti,
|
||||
SfxKey.schabbs : WolfSound.schabbsHas,
|
||||
SfxKey.schabbsDeath : WolfSound.eva,
|
||||
SfxKey.hitlerGreeting : WolfSound.gutenTag,
|
||||
SfxKey.hitlerDeath : WolfSound.scheist,
|
||||
SfxKey.mechaSteps : WolfSound.mechSteps,
|
||||
SfxKey.ottoAlert : WolfSound.spion,
|
||||
SfxKey.gretelDeath : WolfSound.neinSoVass,
|
||||
// --- UI & Progression ---
|
||||
SfxKey.levelComplete : WolfSound.levelDone,
|
||||
SfxKey.endBonus1 : WolfSound.endBonus1,
|
||||
SfxKey.endBonus2 : WolfSound.endBonus2,
|
||||
SfxKey.noBonus : WolfSound.noBonus,
|
||||
SfxKey.percent100 : WolfSound.percent100,
|
||||
};
|
||||
|
||||
@override
|
||||
SoundAssetRef? resolve(SfxKey key) {
|
||||
final slot = _slots[key];
|
||||
return slot != null ? SoundAssetRef(slot) : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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_menu_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/shareware_music_module.dart';
|
||||
|
||||
/// The [AssetRegistry] for the Wolfenstein 3D v1.4 Shareware release.
|
||||
///
|
||||
/// - 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).
|
||||
/// - Menu picture indices are resolved via runtime heuristic offset; call
|
||||
/// [SharewareMenuPicModule.initWithImageSizes] after the VGA images are
|
||||
/// available so the shift is computed correctly.
|
||||
class SharewareAssetRegistry extends AssetRegistry {
|
||||
SharewareAssetRegistry()
|
||||
: super(
|
||||
sfx: const RetailSfxModule(),
|
||||
music: const SharewareMusicModule(),
|
||||
entities: const SharewareEntityModule(),
|
||||
hud: const RetailHudModule(),
|
||||
menu: SharewareMenuPicModule(),
|
||||
);
|
||||
|
||||
/// Convenience accessor to the menu module for post-load initialisation.
|
||||
SharewareMenuPicModule get sharewareMenu => menu as SharewareMenuPicModule;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:wolf_3d_dart/src/data_types/enemy_animation.dart';
|
||||
import 'package:wolf_3d_dart/src/data_types/sprite_frame_range.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/keys/entity_key.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/modules/entity_asset_module.dart';
|
||||
|
||||
/// Built-in entity asset module for the Wolfenstein 3D v1.4 Shareware release.
|
||||
///
|
||||
/// Shareware restricts the available enemy roster to Guard, Dog, and SS.
|
||||
/// Mutant and Officer are absent; Boss entries are retail-only.
|
||||
///
|
||||
/// Sprite ranges for the three available enemies are identical to retail
|
||||
/// (the VSWAP sprite layout is the same for this subset).
|
||||
class SharewareEntityModule extends EntityAssetModule {
|
||||
const SharewareEntityModule();
|
||||
|
||||
static final Map<EntityKey, EntityAssetDefinition> _defs = {
|
||||
EntityKey.guard: const EntityAssetDefinition(
|
||||
availableInShareware: true,
|
||||
animations: EnemyAnimationMap(
|
||||
idle: SpriteFrameRange(50, 57),
|
||||
walking: SpriteFrameRange(58, 89),
|
||||
dying: SpriteFrameRange(90, 93),
|
||||
pain: SpriteFrameRange(94, 94),
|
||||
dead: SpriteFrameRange(95, 95),
|
||||
attacking: SpriteFrameRange(96, 98),
|
||||
),
|
||||
),
|
||||
EntityKey.dog: const EntityAssetDefinition(
|
||||
availableInShareware: true,
|
||||
animations: EnemyAnimationMap(
|
||||
idle: SpriteFrameRange(99, 106),
|
||||
walking: SpriteFrameRange(99, 130),
|
||||
attacking: SpriteFrameRange(135, 137),
|
||||
pain: SpriteFrameRange(0, 0),
|
||||
dying: SpriteFrameRange(131, 133),
|
||||
dead: SpriteFrameRange(134, 134),
|
||||
),
|
||||
),
|
||||
EntityKey.ss: const EntityAssetDefinition(
|
||||
availableInShareware: true,
|
||||
animations: EnemyAnimationMap(
|
||||
idle: SpriteFrameRange(138, 145),
|
||||
walking: SpriteFrameRange(146, 177),
|
||||
attacking: SpriteFrameRange(184, 186),
|
||||
pain: SpriteFrameRange(182, 182),
|
||||
dying: SpriteFrameRange(178, 181),
|
||||
dead: SpriteFrameRange(183, 183),
|
||||
),
|
||||
),
|
||||
// Mutant and Officer are NOT present in shareware; resolving them
|
||||
// intentionally returns null so callers can handle absence cleanly.
|
||||
};
|
||||
|
||||
@override
|
||||
EntityAssetDefinition? resolve(EntityKey key) => _defs[key];
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import 'package:wolf_3d_dart/src/data_types/difficulty.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/keys/menu_pic_key.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/modules/menu_pic_module.dart';
|
||||
|
||||
/// Built-in menu-picture module for the Wolfenstein 3D v1.4 Shareware release.
|
||||
///
|
||||
/// Shareware VGAGRAPH contains fewer pictures than the retail version, so
|
||||
/// the episode/difficulty/control-panel art sits at a shifted position in
|
||||
/// the VGA image list. The exact shift is computed at resolve time by
|
||||
/// scanning the loaded image list for the landmark STATUSBARPIC, mirroring
|
||||
/// the runtime heuristic in the original [WolfClassicMenuArt._indexOffset].
|
||||
///
|
||||
/// Offset determination is deferred until the first [resolve] call and
|
||||
/// cached for subsequent lookups. If the landmark cannot be found the
|
||||
/// module falls back to the retail indices (compatible with unrecognised
|
||||
/// or future shareware variants).
|
||||
class SharewareMenuPicModule extends MenuPicModule {
|
||||
SharewareMenuPicModule();
|
||||
|
||||
// Retail-baseline indices (same layout as RetailMenuPicModule).
|
||||
static final Map<MenuPicKey, int> _retailBaseline = {
|
||||
MenuPicKey.title : 84,
|
||||
MenuPicKey.credits : 86,
|
||||
MenuPicKey.pg13 : 85,
|
||||
MenuPicKey.controlBackground : 23,
|
||||
MenuPicKey.footer : 15,
|
||||
MenuPicKey.heading : 3,
|
||||
MenuPicKey.optionsLabel : 7,
|
||||
MenuPicKey.cursorActive : 8,
|
||||
MenuPicKey.cursorInactive : 9,
|
||||
MenuPicKey.markerSelected : 11,
|
||||
MenuPicKey.markerUnselected : 10,
|
||||
MenuPicKey.episode1 : 27,
|
||||
MenuPicKey.episode2 : 28,
|
||||
MenuPicKey.episode3 : 29,
|
||||
MenuPicKey.episode4 : 30,
|
||||
MenuPicKey.episode5 : 31,
|
||||
MenuPicKey.episode6 : 32,
|
||||
MenuPicKey.difficultyBaby : 16,
|
||||
MenuPicKey.difficultyEasy : 17,
|
||||
MenuPicKey.difficultyNormal : 18,
|
||||
MenuPicKey.difficultyHard : 19,
|
||||
};
|
||||
|
||||
// Landmark constant: STATUSBARPIC in retail picture-table coords.
|
||||
static const int _retailStatusBarIndex = 83;
|
||||
|
||||
// Runtime-computed shift applied to every retail-baseline index.
|
||||
// null = not yet computed; 0 = no shift needed (or heuristic failed).
|
||||
int? _offset;
|
||||
|
||||
/// Provide the raw VGA image dimensions list so the module can compute
|
||||
/// the version offset on first use. Call this immediately after
|
||||
/// construction when the [WolfensteinData] instance is available.
|
||||
///
|
||||
/// [imageSizes] is a list of `(width, height)` tuples in VGA image order.
|
||||
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 - 3; i++) {
|
||||
final s = sizes[i];
|
||||
// STATUSBARPIC heuristic: wide short banner followed by three full-screen images.
|
||||
if (s.width >= 280 && s.height >= 24 && s.height <= 64) {
|
||||
final t1 = sizes[i + 1];
|
||||
final t2 = sizes[i + 2];
|
||||
final t3 = sizes[i + 3];
|
||||
if (_isFullScreen(t1) && _isFullScreen(t2) && _isFullScreen(t3)) {
|
||||
return i - _retailStatusBarIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0; // no shift — unknown layout, use retail baseline
|
||||
}
|
||||
|
||||
bool _isFullScreen(({int width, int height}) s) =>
|
||||
s.width >= 280 && s.height >= 140;
|
||||
|
||||
@override
|
||||
MenuPicRef? resolve(MenuPicKey key) {
|
||||
final base = _retailBaseline[key];
|
||||
if (base == null) return null;
|
||||
final shifted = base + (_offset ?? 0);
|
||||
return MenuPicRef(shifted);
|
||||
}
|
||||
|
||||
@override
|
||||
MenuPicKey episodeKey(int episodeIndex) {
|
||||
const keys = [
|
||||
MenuPicKey.episode1,
|
||||
MenuPicKey.episode2,
|
||||
MenuPicKey.episode3,
|
||||
MenuPicKey.episode4,
|
||||
MenuPicKey.episode5,
|
||||
MenuPicKey.episode6,
|
||||
];
|
||||
if (episodeIndex >= 0 && episodeIndex < keys.length) return keys[episodeIndex];
|
||||
return MenuPicKey.episode1;
|
||||
}
|
||||
|
||||
@override
|
||||
MenuPicKey difficultyKey(Difficulty difficulty) {
|
||||
return switch (difficulty) {
|
||||
Difficulty.baby => MenuPicKey.difficultyBaby,
|
||||
Difficulty.easy => MenuPicKey.difficultyEasy,
|
||||
Difficulty.medium => MenuPicKey.difficultyNormal,
|
||||
Difficulty.hard => MenuPicKey.difficultyHard,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:wolf_3d_dart/src/registry/keys/music_key.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/modules/music_module.dart';
|
||||
|
||||
/// Built-in music module for the Wolfenstein 3D v1.4 Shareware release.
|
||||
///
|
||||
/// Uses the original id Software shareware level-to-track routing table
|
||||
/// (10 levels, 1 episode) as opposed to the 60-entry retail table.
|
||||
class SharewareMusicModule extends MusicModule {
|
||||
const SharewareMusicModule();
|
||||
|
||||
// Original WL_INTER.C shareware music table (Episode 1 only).
|
||||
static const List<int> _levelMap = [
|
||||
2, 3, 4, 5, 2, 3, 4, 5, 6, 7,
|
||||
];
|
||||
|
||||
static final Map<MusicKey, int> _named = {
|
||||
MusicKey.menuTheme: 1,
|
||||
MusicKey.level01: 2,
|
||||
MusicKey.level02: 3,
|
||||
MusicKey.level03: 4,
|
||||
MusicKey.level04: 5,
|
||||
MusicKey.level05: 6,
|
||||
MusicKey.level06: 7,
|
||||
};
|
||||
|
||||
@override
|
||||
MusicRoute get menuMusic => const MusicRoute(1);
|
||||
|
||||
@override
|
||||
MusicRoute musicForLevel(int episodeIndex, int levelIndex) {
|
||||
final flat = levelIndex % _levelMap.length;
|
||||
return MusicRoute(_levelMap[flat]);
|
||||
}
|
||||
|
||||
@override
|
||||
MusicRoute? resolve(MusicKey key) {
|
||||
final index = _named[key];
|
||||
return index != null ? MusicRoute(index) : null;
|
||||
}
|
||||
}
|
||||
39
packages/wolf_3d_dart/lib/src/registry/keys/entity_key.dart
Normal file
39
packages/wolf_3d_dart/lib/src/registry/keys/entity_key.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
/// Extensible typed key for entity asset definitions (enemies, bosses, etc.).
|
||||
///
|
||||
/// Built-in Wolf3D entity types are exposed as named static constants.
|
||||
/// Custom modules can define new keys with `const EntityKey('myEnemy')`
|
||||
/// without modifying this class.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// registry.entities.resolve(EntityKey.guard)
|
||||
/// ```
|
||||
final class EntityKey {
|
||||
const EntityKey(this._id);
|
||||
|
||||
final String _id;
|
||||
|
||||
// --- Standard Enemies ---
|
||||
static const guard = EntityKey('guard');
|
||||
static const dog = EntityKey('dog');
|
||||
static const ss = EntityKey('ss');
|
||||
static const mutant = EntityKey('mutant');
|
||||
static const officer = EntityKey('officer');
|
||||
|
||||
// --- Bosses (Wolf3D) ---
|
||||
static const hansGrosse = EntityKey('hansGrosse');
|
||||
static const drSchabbs = EntityKey('drSchabbs');
|
||||
static const hitler = EntityKey('hitler');
|
||||
static const mechaHitler = EntityKey('mechaHitler');
|
||||
static const ottoGiftmacher = EntityKey('ottoGiftmacher');
|
||||
static const gretelGrosse = EntityKey('gretelGrosse');
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is EntityKey && other._id == _id;
|
||||
|
||||
@override
|
||||
int get hashCode => _id.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'EntityKey($_id)';
|
||||
}
|
||||
54
packages/wolf_3d_dart/lib/src/registry/keys/hud_key.dart
Normal file
54
packages/wolf_3d_dart/lib/src/registry/keys/hud_key.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
/// Extensible typed key for HUD image elements.
|
||||
///
|
||||
/// Built-in Wolf3D HUD elements are exposed as named static constants.
|
||||
/// Custom modules can define new keys with `const HudKey('myElement')`
|
||||
/// without modifying this class.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// registry.hud.resolve(HudKey.statusBar)
|
||||
/// ```
|
||||
final class HudKey {
|
||||
const HudKey(this._id);
|
||||
|
||||
final String _id;
|
||||
|
||||
// --- Layout ---
|
||||
static const statusBar = HudKey('statusBar');
|
||||
|
||||
// --- Score/Floor/Lives/Ammo digits (0-9 for each counter) ---
|
||||
static const digit0 = HudKey('digit0');
|
||||
static const digit1 = HudKey('digit1');
|
||||
static const digit2 = HudKey('digit2');
|
||||
static const digit3 = HudKey('digit3');
|
||||
static const digit4 = HudKey('digit4');
|
||||
static const digit5 = HudKey('digit5');
|
||||
static const digit6 = HudKey('digit6');
|
||||
static const digit7 = HudKey('digit7');
|
||||
static const digit8 = HudKey('digit8');
|
||||
static const digit9 = HudKey('digit9');
|
||||
|
||||
// --- BJ face health bands (each maps to the base of 3 animated frames) ---
|
||||
static const faceHealthy = HudKey('faceHealthy'); // health 85–100
|
||||
static const faceScratched = HudKey('faceScratched'); // health 69–84
|
||||
static const faceHurt = HudKey('faceHurt'); // health 53–68
|
||||
static const faceWounded = HudKey('faceWounded'); // health 37–52
|
||||
static const faceBadlyWounded = HudKey('faceBadlyWounded'); // health 21–36
|
||||
static const faceDying = HudKey('faceDying'); // health 5–20
|
||||
static const faceNearDeath = HudKey('faceNearDeath'); // health 1–4
|
||||
static const faceDead = HudKey('faceDead'); // health ≤ 0
|
||||
|
||||
// --- Weapon icons ---
|
||||
static const pistolIcon = HudKey('pistolIcon');
|
||||
static const machineGunIcon = HudKey('machineGunIcon');
|
||||
static const chainGunIcon = HudKey('chainGunIcon');
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is HudKey && other._id == _id;
|
||||
|
||||
@override
|
||||
int get hashCode => _id.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'HudKey($_id)';
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/// Extensible typed key for menu VGA picture elements.
|
||||
///
|
||||
/// Built-in Wolf3D menu pictures are exposed as named static constants.
|
||||
/// Custom modules can define new keys with `const MenuPicKey('myPic')`
|
||||
/// without modifying this class.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// registry.menu.resolve(MenuPicKey.title)
|
||||
/// ```
|
||||
final class MenuPicKey {
|
||||
const MenuPicKey(this._id);
|
||||
|
||||
final String _id;
|
||||
|
||||
// --- Full-screen art ---
|
||||
static const title = MenuPicKey('title');
|
||||
static const credits = MenuPicKey('credits');
|
||||
static const pg13 = MenuPicKey('pg13');
|
||||
|
||||
// --- Control-panel chrome ---
|
||||
static const controlBackground = MenuPicKey('controlBackground');
|
||||
static const footer = MenuPicKey('footer');
|
||||
static const heading = MenuPicKey('heading');
|
||||
static const optionsLabel = MenuPicKey('optionsLabel');
|
||||
|
||||
// --- Cursor / selection markers ---
|
||||
static const cursorActive = MenuPicKey('cursorActive');
|
||||
static const cursorInactive = MenuPicKey('cursorInactive');
|
||||
static const markerSelected = MenuPicKey('markerSelected');
|
||||
static const markerUnselected = MenuPicKey('markerUnselected');
|
||||
|
||||
// --- Episode selection ---
|
||||
static const episode1 = MenuPicKey('episode1');
|
||||
static const episode2 = MenuPicKey('episode2');
|
||||
static const episode3 = MenuPicKey('episode3');
|
||||
static const episode4 = MenuPicKey('episode4');
|
||||
static const episode5 = MenuPicKey('episode5');
|
||||
static const episode6 = MenuPicKey('episode6');
|
||||
|
||||
// --- Difficulty selection ---
|
||||
static const difficultyBaby = MenuPicKey('difficultyBaby');
|
||||
static const difficultyEasy = MenuPicKey('difficultyEasy');
|
||||
static const difficultyNormal = MenuPicKey('difficultyNormal');
|
||||
static const difficultyHard = MenuPicKey('difficultyHard');
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is MenuPicKey && other._id == _id;
|
||||
|
||||
@override
|
||||
int get hashCode => _id.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'MenuPicKey($_id)';
|
||||
}
|
||||
51
packages/wolf_3d_dart/lib/src/registry/keys/music_key.dart
Normal file
51
packages/wolf_3d_dart/lib/src/registry/keys/music_key.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
/// Extensible typed key for music tracks.
|
||||
///
|
||||
/// Built-in Wolf3D music contexts are exposed as named static constants.
|
||||
/// Custom modules can define new keys with `const MusicKey('myTrack')`
|
||||
/// without modifying this class.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// registry.music.resolve(MusicKey.menuTheme)
|
||||
/// ```
|
||||
final class MusicKey {
|
||||
const MusicKey(this._id);
|
||||
|
||||
final String _id;
|
||||
|
||||
// --- Menus & UI ---
|
||||
static const menuTheme = MusicKey('menuTheme');
|
||||
|
||||
// --- Gameplay ---
|
||||
// Generic level-slot keys used when routing by episode+floor index.
|
||||
// The MusicModule maps these to actual IMF track indices.
|
||||
static const level01 = MusicKey('level01');
|
||||
static const level02 = MusicKey('level02');
|
||||
static const level03 = MusicKey('level03');
|
||||
static const level04 = MusicKey('level04');
|
||||
static const level05 = MusicKey('level05');
|
||||
static const level06 = MusicKey('level06');
|
||||
static const level07 = MusicKey('level07');
|
||||
static const level08 = MusicKey('level08');
|
||||
static const level09 = MusicKey('level09');
|
||||
static const level10 = MusicKey('level10');
|
||||
static const level11 = MusicKey('level11');
|
||||
static const level12 = MusicKey('level12');
|
||||
static const level13 = MusicKey('level13');
|
||||
static const level14 = MusicKey('level14');
|
||||
static const level15 = MusicKey('level15');
|
||||
static const level16 = MusicKey('level16');
|
||||
static const level17 = MusicKey('level17');
|
||||
static const level18 = MusicKey('level18');
|
||||
static const level19 = MusicKey('level19');
|
||||
static const level20 = MusicKey('level20');
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is MusicKey && other._id == _id;
|
||||
|
||||
@override
|
||||
int get hashCode => _id.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'MusicKey($_id)';
|
||||
}
|
||||
77
packages/wolf_3d_dart/lib/src/registry/keys/sfx_key.dart
Normal file
77
packages/wolf_3d_dart/lib/src/registry/keys/sfx_key.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
/// Extensible typed key for sound effects.
|
||||
///
|
||||
/// Built-in Wolf3D sound effects are exposed as named static constants.
|
||||
/// Custom modules can define new keys with `const SfxKey('myCustomId')`
|
||||
/// without modifying this class.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// registry.sfx.resolve(SfxKey.pistolFire)
|
||||
/// ```
|
||||
final class SfxKey {
|
||||
const SfxKey(this._id);
|
||||
|
||||
final String _id;
|
||||
|
||||
// --- Doors & Environment ---
|
||||
static const openDoor = SfxKey('openDoor');
|
||||
static const closeDoor = SfxKey('closeDoor');
|
||||
static const pushWall = SfxKey('pushWall');
|
||||
|
||||
// --- Weapons ---
|
||||
static const knifeAttack = SfxKey('knifeAttack');
|
||||
static const pistolFire = SfxKey('pistolFire');
|
||||
static const machineGunFire = SfxKey('machineGunFire');
|
||||
static const chainGunFire = SfxKey('chainGunFire');
|
||||
static const enemyFire = SfxKey('enemyFire');
|
||||
|
||||
// --- Pickups ---
|
||||
static const getMachineGun = SfxKey('getMachineGun');
|
||||
static const getAmmo = SfxKey('getAmmo');
|
||||
static const getChainGun = SfxKey('getChainGun');
|
||||
static const healthSmall = SfxKey('healthSmall');
|
||||
static const healthLarge = SfxKey('healthLarge');
|
||||
static const treasure1 = SfxKey('treasure1');
|
||||
static const treasure2 = SfxKey('treasure2');
|
||||
static const treasure3 = SfxKey('treasure3');
|
||||
static const treasure4 = SfxKey('treasure4');
|
||||
static const extraLife = SfxKey('extraLife');
|
||||
|
||||
// --- Standard Enemies ---
|
||||
static const guardHalt = SfxKey('guardHalt');
|
||||
static const dogBark = SfxKey('dogBark');
|
||||
static const dogDeath = SfxKey('dogDeath');
|
||||
static const dogAttack = SfxKey('dogAttack');
|
||||
static const deathScream1 = SfxKey('deathScream1');
|
||||
static const deathScream2 = SfxKey('deathScream2');
|
||||
static const deathScream3 = SfxKey('deathScream3');
|
||||
static const ssAlert = SfxKey('ssAlert');
|
||||
static const ssDeath = SfxKey('ssDeath');
|
||||
|
||||
// --- Bosses ---
|
||||
static const bossActive = SfxKey('bossActive');
|
||||
static const hansGrosseDeath = SfxKey('hansGrosseDeath');
|
||||
static const schabbs = SfxKey('schabbs');
|
||||
static const schabbsDeath = SfxKey('schabbsDeath');
|
||||
static const hitlerGreeting = SfxKey('hitlerGreeting');
|
||||
static const hitlerDeath = SfxKey('hitlerDeath');
|
||||
static const mechaSteps = SfxKey('mechaSteps');
|
||||
static const ottoAlert = SfxKey('ottoAlert');
|
||||
static const gretelDeath = SfxKey('gretelDeath');
|
||||
|
||||
// --- UI & Progression ---
|
||||
static const levelComplete = SfxKey('levelComplete');
|
||||
static const endBonus1 = SfxKey('endBonus1');
|
||||
static const endBonus2 = SfxKey('endBonus2');
|
||||
static const noBonus = SfxKey('noBonus');
|
||||
static const percent100 = SfxKey('percent100');
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is SfxKey && other._id == _id;
|
||||
|
||||
@override
|
||||
int get hashCode => _id.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'SfxKey($_id)';
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:wolf_3d_dart/src/data_types/enemy_animation.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/keys/entity_key.dart';
|
||||
|
||||
/// All version-sensitive asset data for a single entity type.
|
||||
///
|
||||
/// - [animations] is `null` for bosses that drive sprite selection with
|
||||
/// custom logic and do not use the standard [EnemyAnimationMap].
|
||||
/// - [baseSpriteIndex] is the sprite-list offset used by custom-logic bosses
|
||||
/// as the base for their manual frame calculations.
|
||||
/// - [availableInShareware] mirrors the original engine's version-exclusion
|
||||
/// flag; entities with `false` are skipped during shareware level loading.
|
||||
class EntityAssetDefinition {
|
||||
const EntityAssetDefinition({
|
||||
this.animations,
|
||||
this.baseSpriteIndex,
|
||||
this.availableInShareware = true,
|
||||
});
|
||||
|
||||
final EnemyAnimationMap? animations;
|
||||
final int? baseSpriteIndex;
|
||||
final bool availableInShareware;
|
||||
}
|
||||
|
||||
/// Owns the mapping from symbolic [EntityKey] values to version-specific
|
||||
/// [EntityAssetDefinition] objects.
|
||||
///
|
||||
/// Implement this class to provide custom sprite data for a modded or
|
||||
/// alternate game version. Pass the implementation inside a custom
|
||||
/// [AssetRegistry] when loading data.
|
||||
abstract class EntityAssetModule {
|
||||
const EntityAssetModule();
|
||||
|
||||
/// Resolves [key] to an [EntityAssetDefinition].
|
||||
///
|
||||
/// Returns `null` if the key is not recognised by this module.
|
||||
EntityAssetDefinition? resolve(EntityKey key);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:wolf_3d_dart/src/registry/keys/hud_key.dart';
|
||||
|
||||
/// The resolved reference for a HUD element: a zero-based index into
|
||||
/// [WolfensteinData.vgaImages].
|
||||
class HudAssetRef {
|
||||
const HudAssetRef(this.vgaIndex);
|
||||
|
||||
final int vgaIndex;
|
||||
|
||||
@override
|
||||
String toString() => 'HudAssetRef($vgaIndex)';
|
||||
}
|
||||
|
||||
/// Owns the mapping from symbolic [HudKey] identifiers to VGA image indices.
|
||||
///
|
||||
/// Implement this class to provide a custom HUD layout for a modded or
|
||||
/// alternate game version. Pass the implementation inside a custom
|
||||
/// [AssetRegistry] when loading data.
|
||||
abstract class HudModule {
|
||||
const HudModule();
|
||||
|
||||
/// Resolves [key] to a [HudAssetRef] containing the VGA image index.
|
||||
///
|
||||
/// Returns `null` if the key is not supported by this module.
|
||||
HudAssetRef? resolve(HudKey key);
|
||||
|
||||
/// Returns the appropriate [HudKey] for BJ's face sprite given the
|
||||
/// player's current [health].
|
||||
HudKey faceKeyForHealth(int health);
|
||||
|
||||
/// Convenience: resolves BJ's face image directly from [health].
|
||||
HudAssetRef? faceForHealth(int health) => resolve(faceKeyForHealth(health));
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:wolf_3d_dart/src/data_types/difficulty.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/keys/menu_pic_key.dart';
|
||||
|
||||
/// The resolved reference for a menu picture: a zero-based index into
|
||||
/// [WolfensteinData.vgaImages].
|
||||
class MenuPicRef {
|
||||
const MenuPicRef(this.pictureIndex);
|
||||
|
||||
final int pictureIndex;
|
||||
|
||||
@override
|
||||
String toString() => 'MenuPicRef($pictureIndex)';
|
||||
}
|
||||
|
||||
/// Owns the mapping from symbolic [MenuPicKey] values to VGA picture indices.
|
||||
///
|
||||
/// Implement this class to provide custom menu art for a modded or alternate
|
||||
/// game version. Pass the implementation inside a custom [AssetRegistry] when
|
||||
/// loading data.
|
||||
abstract class MenuPicModule {
|
||||
const MenuPicModule();
|
||||
|
||||
/// Resolves [key] to a [MenuPicRef] containing the VGA picture index.
|
||||
///
|
||||
/// Returns `null` if the key is not supported by this module.
|
||||
MenuPicRef? resolve(MenuPicKey key);
|
||||
|
||||
/// Returns the [MenuPicKey] for the episode selection picture at
|
||||
/// zero-based [episodeIndex].
|
||||
MenuPicKey episodeKey(int episodeIndex);
|
||||
|
||||
/// Returns the [MenuPicKey] for the given [difficulty] selection screen.
|
||||
MenuPicKey difficultyKey(Difficulty difficulty);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:wolf_3d_dart/src/registry/keys/music_key.dart';
|
||||
|
||||
/// The resolved reference for a music track: a numeric index into
|
||||
/// [WolfensteinData.music].
|
||||
class MusicRoute {
|
||||
const MusicRoute(this.trackIndex);
|
||||
|
||||
final int trackIndex;
|
||||
|
||||
@override
|
||||
String toString() => 'MusicRoute($trackIndex)';
|
||||
}
|
||||
|
||||
/// Owns the mapping from music contexts to IMF track indices.
|
||||
///
|
||||
/// Implement this class to provide a custom music layout for a modded or
|
||||
/// alternate game version. Pass the implementation inside a custom
|
||||
/// [AssetRegistry] when loading data.
|
||||
abstract class MusicModule {
|
||||
const MusicModule();
|
||||
|
||||
/// Resolves a named [MusicKey] to a [MusicRoute].
|
||||
///
|
||||
/// Returns `null` if the key is not supported by this module.
|
||||
MusicRoute? resolve(MusicKey key);
|
||||
|
||||
/// Resolves the level music track for a given [episodeIndex] and
|
||||
/// zero-based [levelIndex].
|
||||
///
|
||||
/// This is the primary routing path for in-game level music, which the
|
||||
/// original engine mapped via a lookup table rather than per-level fields.
|
||||
MusicRoute musicForLevel(int episodeIndex, int levelIndex);
|
||||
|
||||
/// The music track to play while the main menu is shown.
|
||||
MusicRoute get menuMusic;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:wolf_3d_dart/src/registry/keys/sfx_key.dart';
|
||||
|
||||
/// The resolved reference for a sound effect: a numeric slot index into
|
||||
/// [WolfensteinData.sounds].
|
||||
class SoundAssetRef {
|
||||
const SoundAssetRef(this.slotIndex);
|
||||
|
||||
final int slotIndex;
|
||||
|
||||
@override
|
||||
String toString() => 'SoundAssetRef($slotIndex)';
|
||||
}
|
||||
|
||||
/// Owns the mapping from symbolic [SfxKey] identifiers to numeric sound slots.
|
||||
///
|
||||
/// Implement this class to provide a custom sound layout for a modded or
|
||||
/// alternate game version. Pass the implementation inside a custom
|
||||
/// [AssetRegistry] when loading data.
|
||||
abstract class SfxModule {
|
||||
const SfxModule();
|
||||
|
||||
/// Resolves [key] to a [SoundAssetRef] containing the numeric slot index.
|
||||
///
|
||||
/// Returns `null` if the key is not supported by this module.
|
||||
SoundAssetRef? resolve(SfxKey key);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:wolf_3d_dart/src/data/data_version.dart';
|
||||
import 'package:wolf_3d_dart/src/data_types/game_version.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/asset_registry.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/retail_asset_registry.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/shareware_asset_registry.dart';
|
||||
|
||||
/// The input used by [AssetRegistryResolver] to select or build a registry.
|
||||
class RegistrySelectionContext {
|
||||
const RegistrySelectionContext({
|
||||
required this.gameVersion,
|
||||
required this.dataVersion,
|
||||
});
|
||||
|
||||
/// Broad release family (Retail, Shareware, Spear of Destiny, …).
|
||||
final GameVersion gameVersion;
|
||||
|
||||
/// Exact data-file identity determined from the VSWAP checksum.
|
||||
final DataVersion dataVersion;
|
||||
}
|
||||
|
||||
/// Selects or builds an [AssetRegistry] for a given [RegistrySelectionContext].
|
||||
///
|
||||
/// Implement this interface to provide a fully custom selection strategy,
|
||||
/// for example to support a game variant not covered by the built-in resolver.
|
||||
abstract class AssetRegistryResolver {
|
||||
const AssetRegistryResolver();
|
||||
|
||||
AssetRegistry resolve(RegistrySelectionContext context);
|
||||
}
|
||||
|
||||
/// Default resolver that maps known [DataVersion] and [GameVersion] values to
|
||||
/// built-in registries.
|
||||
///
|
||||
/// Selection precedence:
|
||||
/// 1. Exact [DataVersion] match (most specific).
|
||||
/// 2. [GameVersion] family fallback.
|
||||
/// 3. Retail layout as the last-resort fallback for unknown variants.
|
||||
class BuiltInAssetRegistryResolver implements AssetRegistryResolver {
|
||||
const BuiltInAssetRegistryResolver();
|
||||
|
||||
@override
|
||||
AssetRegistry resolve(RegistrySelectionContext context) {
|
||||
// 1. Exact data-version match.
|
||||
switch (context.dataVersion) {
|
||||
case DataVersion.version10Retail:
|
||||
case DataVersion.version11Retail:
|
||||
case DataVersion.version14Retail:
|
||||
return RetailAssetRegistry();
|
||||
case DataVersion.version14Shareware:
|
||||
return SharewareAssetRegistry();
|
||||
case DataVersion.unknown:
|
||||
break; // fall through to GameVersion family check
|
||||
}
|
||||
|
||||
// 2. GameVersion family fallback.
|
||||
switch (context.gameVersion) {
|
||||
case GameVersion.retail:
|
||||
return RetailAssetRegistry();
|
||||
case GameVersion.shareware:
|
||||
return SharewareAssetRegistry();
|
||||
case GameVersion.spearOfDestiny:
|
||||
case GameVersion.spearOfDestinyDemo:
|
||||
// Spear-specific modules are not yet implemented; retail layout is
|
||||
// the closest structural match and will be replaced in a later phase.
|
||||
return RetailAssetRegistry();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,13 @@
|
||||
/// the full engine runtime.
|
||||
library;
|
||||
|
||||
export 'src/data/data_version.dart' show DataVersion;
|
||||
export 'src/data_types/cardinal_direction.dart' show CardinalDirection;
|
||||
export 'src/data_types/color_palette.dart' show ColorPalette;
|
||||
export 'src/data_types/coordinate_2d.dart' show Coordinate2D;
|
||||
export 'src/data_types/difficulty.dart' show Difficulty;
|
||||
export 'src/data_types/enemy_animation.dart'
|
||||
show EnemyAnimation, EnemyAnimationMap;
|
||||
export 'src/data_types/enemy_map_data.dart' show EnemyMapData;
|
||||
export 'src/data_types/episode.dart' show Episode;
|
||||
export 'src/data_types/frame_buffer.dart' show FrameBuffer;
|
||||
@@ -22,3 +25,24 @@ export 'src/data_types/sprite.dart' hide Matrix;
|
||||
export 'src/data_types/sprite_frame_range.dart' show SpriteFrameRange;
|
||||
export 'src/data_types/wolf_level.dart' show WolfLevel;
|
||||
export 'src/data_types/wolfenstein_data.dart' show WolfensteinData;
|
||||
// Registry public surface
|
||||
export 'src/registry/asset_registry.dart' show AssetRegistry;
|
||||
export 'src/registry/keys/entity_key.dart' show EntityKey;
|
||||
export 'src/registry/keys/hud_key.dart' show HudKey;
|
||||
export 'src/registry/keys/menu_pic_key.dart' show MenuPicKey;
|
||||
export 'src/registry/keys/music_key.dart' show MusicKey;
|
||||
export 'src/registry/keys/sfx_key.dart' show SfxKey;
|
||||
export 'src/registry/modules/entity_asset_module.dart'
|
||||
show EntityAssetModule, EntityAssetDefinition;
|
||||
export 'src/registry/modules/hud_module.dart' show HudModule, HudAssetRef;
|
||||
export 'src/registry/modules/menu_pic_module.dart'
|
||||
show MenuPicModule, MenuPicRef;
|
||||
export 'src/registry/modules/music_module.dart' show MusicModule, MusicRoute;
|
||||
export 'src/registry/modules/sfx_module.dart' show SfxModule, SoundAssetRef;
|
||||
export 'src/registry/registry_resolver.dart'
|
||||
show AssetRegistryResolver, BuiltInAssetRegistryResolver,
|
||||
RegistrySelectionContext;
|
||||
export 'src/registry/built_in/retail_asset_registry.dart'
|
||||
show RetailAssetRegistry;
|
||||
export 'src/registry/built_in/shareware_asset_registry.dart'
|
||||
show SharewareAssetRegistry;
|
||||
|
||||
@@ -118,6 +118,8 @@ WolfEngine _buildEngine({
|
||||
return WolfEngine(
|
||||
data: WolfensteinData(
|
||||
version: GameVersion.retail,
|
||||
dataVersion: DataVersion.unknown,
|
||||
registry: RetailAssetRegistry(),
|
||||
walls: [
|
||||
_solidSprite(1),
|
||||
_solidSprite(1),
|
||||
|
||||
@@ -23,6 +23,8 @@ void main() {
|
||||
final engine = WolfEngine(
|
||||
data: WolfensteinData(
|
||||
version: GameVersion.shareware,
|
||||
dataVersion: DataVersion.unknown,
|
||||
registry: RetailAssetRegistry(),
|
||||
walls: [
|
||||
_solidSprite(1),
|
||||
_solidSprite(1),
|
||||
|
||||
@@ -23,6 +23,8 @@ void main() {
|
||||
final engine = WolfEngine(
|
||||
data: WolfensteinData(
|
||||
version: GameVersion.shareware,
|
||||
dataVersion: DataVersion.unknown,
|
||||
registry: RetailAssetRegistry(),
|
||||
walls: [
|
||||
_solidSprite(1),
|
||||
_solidSprite(1),
|
||||
|
||||
Reference in New Issue
Block a user