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:
@@ -139,86 +139,58 @@ class Player {
|
||||
///
|
||||
/// Returns `null` when the item was not collected (for example: full health).
|
||||
int? tryPickup(Collectible item) {
|
||||
bool pickedUp = false;
|
||||
int? pickupSfxId;
|
||||
final effect = item.tryCollect(
|
||||
CollectiblePickupContext(
|
||||
health: health,
|
||||
ammo: ammo,
|
||||
hasGoldKey: hasGoldKey,
|
||||
hasSilverKey: hasSilverKey,
|
||||
hasMachineGun: hasMachineGun,
|
||||
hasChainGun: hasChainGun,
|
||||
currentWeaponType: currentWeapon.type,
|
||||
),
|
||||
);
|
||||
if (effect == null) return null;
|
||||
|
||||
switch (item.type) {
|
||||
case CollectibleType.health:
|
||||
if (health >= 100) return null;
|
||||
heal(item.mapId == MapObject.food ? 4 : 25);
|
||||
pickupSfxId = item.mapId == MapObject.food
|
||||
? WolfSound.healthSmall
|
||||
: WolfSound.healthLarge;
|
||||
pickedUp = true;
|
||||
break;
|
||||
|
||||
case CollectibleType.ammo:
|
||||
if (ammo >= 99) return null;
|
||||
int previousAmmo = ammo;
|
||||
addAmmo(8);
|
||||
if (currentWeapon is Knife && previousAmmo <= 0) {
|
||||
requestWeaponSwitch(WeaponType.pistol);
|
||||
}
|
||||
pickupSfxId = WolfSound.getAmmo;
|
||||
pickedUp = true;
|
||||
break;
|
||||
|
||||
case CollectibleType.treasure:
|
||||
if (item.mapId == MapObject.cross) {
|
||||
score += 100;
|
||||
pickupSfxId = WolfSound.treasure1;
|
||||
}
|
||||
if (item.mapId == MapObject.chalice) {
|
||||
score += 500;
|
||||
pickupSfxId = WolfSound.treasure2;
|
||||
}
|
||||
if (item.mapId == MapObject.chest) {
|
||||
score += 1000;
|
||||
pickupSfxId = WolfSound.treasure3;
|
||||
}
|
||||
if (item.mapId == MapObject.crown) {
|
||||
score += 5000;
|
||||
pickupSfxId = WolfSound.treasure4;
|
||||
}
|
||||
if (item.mapId == MapObject.extraLife) {
|
||||
heal(100);
|
||||
addAmmo(25);
|
||||
pickupSfxId = WolfSound.extraLife;
|
||||
}
|
||||
pickedUp = true;
|
||||
break;
|
||||
|
||||
case CollectibleType.weapon:
|
||||
if (item.mapId == MapObject.machineGun) {
|
||||
if (weapons[WeaponType.machineGun] == null) {
|
||||
weapons[WeaponType.machineGun] = MachineGun();
|
||||
hasMachineGun = true;
|
||||
}
|
||||
addAmmo(8);
|
||||
requestWeaponSwitch(WeaponType.machineGun);
|
||||
pickupSfxId = WolfSound.getMachineGun;
|
||||
pickedUp = true;
|
||||
}
|
||||
if (item.mapId == MapObject.chainGun) {
|
||||
if (weapons[WeaponType.chainGun] == null) {
|
||||
weapons[WeaponType.chainGun] = ChainGun();
|
||||
hasChainGun = true;
|
||||
}
|
||||
addAmmo(8);
|
||||
requestWeaponSwitch(WeaponType.chainGun);
|
||||
pickupSfxId = WolfSound.getGatling;
|
||||
pickedUp = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case CollectibleType.key:
|
||||
if (item.mapId == MapObject.goldKey) hasGoldKey = true;
|
||||
if (item.mapId == MapObject.silverKey) hasSilverKey = true;
|
||||
pickupSfxId = WolfSound.getAmmo;
|
||||
pickedUp = true;
|
||||
break;
|
||||
if (effect.healthToRestore > 0) {
|
||||
heal(effect.healthToRestore);
|
||||
}
|
||||
return pickedUp ? pickupSfxId : null;
|
||||
|
||||
if (effect.ammoToAdd > 0) {
|
||||
addAmmo(effect.ammoToAdd);
|
||||
}
|
||||
|
||||
if (effect.scoreToAdd > 0) {
|
||||
score += effect.scoreToAdd;
|
||||
}
|
||||
|
||||
if (effect.grantGoldKey) {
|
||||
hasGoldKey = true;
|
||||
}
|
||||
|
||||
if (effect.grantSilverKey) {
|
||||
hasSilverKey = true;
|
||||
}
|
||||
|
||||
if (effect.grantWeapon case final weaponType?) {
|
||||
if (weapons[weaponType] == null) {
|
||||
weapons[weaponType] = switch (weaponType) {
|
||||
WeaponType.machineGun => MachineGun(),
|
||||
WeaponType.chainGun => ChainGun(),
|
||||
WeaponType.pistol => Pistol(),
|
||||
WeaponType.knife => Knife(),
|
||||
};
|
||||
}
|
||||
|
||||
if (weaponType == WeaponType.machineGun) hasMachineGun = true;
|
||||
if (weaponType == WeaponType.chainGun) hasChainGun = true;
|
||||
}
|
||||
|
||||
if (effect.requestWeaponSwitch case final weaponType?) {
|
||||
requestWeaponSwitch(weaponType);
|
||||
}
|
||||
|
||||
return effect.pickupSfxId;
|
||||
}
|
||||
|
||||
bool fire(int currentTime) {
|
||||
@@ -259,26 +231,9 @@ class Player {
|
||||
isWalkable: isWalkable,
|
||||
currentTime: currentTime,
|
||||
onEnemyKilled: (Enemy killedEnemy) {
|
||||
// Dynamic scoring based on the enemy type!
|
||||
int pointsToAdd = 0;
|
||||
|
||||
switch (killedEnemy.runtimeType.toString()) {
|
||||
case 'BrownGuard':
|
||||
pointsToAdd = 100;
|
||||
break;
|
||||
case 'Dog':
|
||||
pointsToAdd = 200;
|
||||
break;
|
||||
// You can easily plug in future enemies here!
|
||||
// case 'SSOfficer': pointsToAdd = 500; break;
|
||||
default:
|
||||
pointsToAdd = 100; // Fallback
|
||||
}
|
||||
|
||||
score += pointsToAdd;
|
||||
// Optional: Print to console so you can see it working
|
||||
score += killedEnemy.scoreValue;
|
||||
print(
|
||||
"Killed ${killedEnemy.runtimeType}! +$pointsToAdd (Score: $score)",
|
||||
"Killed ${killedEnemy.runtimeType}! +${killedEnemy.scoreValue} (Score: $score)",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,33 +1,73 @@
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entities/weapon/weapon.dart';
|
||||
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
enum CollectibleType { ammo, health, treasure, weapon, key }
|
||||
/// Immutable snapshot of player state needed to evaluate pickup rules.
|
||||
class CollectiblePickupContext {
|
||||
final int health;
|
||||
final int ammo;
|
||||
final bool hasGoldKey;
|
||||
final bool hasSilverKey;
|
||||
final bool hasMachineGun;
|
||||
final bool hasChainGun;
|
||||
final WeaponType currentWeaponType;
|
||||
|
||||
class Collectible extends Entity {
|
||||
final CollectibleType type;
|
||||
const CollectiblePickupContext({
|
||||
required this.health,
|
||||
required this.ammo,
|
||||
required this.hasGoldKey,
|
||||
required this.hasSilverKey,
|
||||
required this.hasMachineGun,
|
||||
required this.hasChainGun,
|
||||
required this.currentWeaponType,
|
||||
});
|
||||
}
|
||||
|
||||
Collectible({
|
||||
/// Generic result returned when a collectible is consumed.
|
||||
class CollectiblePickupEffect {
|
||||
final int healthToRestore;
|
||||
final int ammoToAdd;
|
||||
final int scoreToAdd;
|
||||
final int pickupSfxId;
|
||||
final bool grantGoldKey;
|
||||
final bool grantSilverKey;
|
||||
final WeaponType? grantWeapon;
|
||||
final WeaponType? requestWeaponSwitch;
|
||||
|
||||
const CollectiblePickupEffect({
|
||||
this.healthToRestore = 0,
|
||||
this.ammoToAdd = 0,
|
||||
this.scoreToAdd = 0,
|
||||
required this.pickupSfxId,
|
||||
this.grantGoldKey = false,
|
||||
this.grantSilverKey = false,
|
||||
this.grantWeapon,
|
||||
this.requestWeaponSwitch,
|
||||
});
|
||||
}
|
||||
|
||||
abstract class Collectible extends Entity {
|
||||
Collectible._({
|
||||
required super.x,
|
||||
required super.y,
|
||||
required super.spriteIndex,
|
||||
required super.mapId,
|
||||
required this.type,
|
||||
}) : super(state: EntityState.staticObj);
|
||||
|
||||
/// Additional score granted when this item is collected.
|
||||
int get scoreValue => 0;
|
||||
|
||||
/// Returns `null` when pickup should be ignored (full health, full ammo, etc).
|
||||
CollectiblePickupEffect? tryCollect(CollectiblePickupContext context);
|
||||
|
||||
static int _spriteIndexFor(int objId) => objId - 21;
|
||||
|
||||
// Define which Map IDs are actually items you can pick up
|
||||
static bool isCollectible(int objId) {
|
||||
return (objId >= 43 && objId <= 44) || // Keys
|
||||
(objId >= 47 && objId <= 56); // Health, Ammo, Weapons, Treasure, 1-Up
|
||||
}
|
||||
|
||||
static CollectibleType _getType(int objId) {
|
||||
if (objId == 43 || objId == 44) return CollectibleType.key;
|
||||
if (objId == 47 || objId == 48) return CollectibleType.health;
|
||||
if (objId == 49) return CollectibleType.ammo;
|
||||
if (objId == 50 || objId == 51) return CollectibleType.weapon;
|
||||
return CollectibleType.treasure; // 52-56
|
||||
}
|
||||
|
||||
static Collectible? trySpawn(
|
||||
int objId,
|
||||
double x,
|
||||
@@ -35,15 +75,171 @@ class Collectible extends Entity {
|
||||
Difficulty difficulty, {
|
||||
bool isSharewareMode = false,
|
||||
}) {
|
||||
if (isCollectible(objId)) {
|
||||
return Collectible(
|
||||
return switch (objId) {
|
||||
MapObject.goldKey || MapObject.silverKey => KeyCollectible(
|
||||
x: x,
|
||||
y: y,
|
||||
spriteIndex: objId - 21, // Same VSWAP math as decorations!
|
||||
mapId: objId,
|
||||
type: _getType(objId),
|
||||
),
|
||||
MapObject.food || MapObject.medkit => HealthCollectible(
|
||||
x: x,
|
||||
y: y,
|
||||
mapId: objId,
|
||||
),
|
||||
MapObject.ammoClip => AmmoCollectible(x: x, y: y),
|
||||
MapObject.machineGun || MapObject.chainGun => WeaponCollectible(
|
||||
x: x,
|
||||
y: y,
|
||||
mapId: objId,
|
||||
),
|
||||
MapObject.cross ||
|
||||
MapObject.chalice ||
|
||||
MapObject.chest ||
|
||||
MapObject.crown ||
|
||||
MapObject.extraLife => TreasureCollectible(x: x, y: y, mapId: objId),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class HealthCollectible extends Collectible {
|
||||
HealthCollectible({
|
||||
required super.x,
|
||||
required super.y,
|
||||
required super.mapId,
|
||||
}) : super._(spriteIndex: Collectible._spriteIndexFor(mapId));
|
||||
|
||||
@override
|
||||
CollectiblePickupEffect? tryCollect(CollectiblePickupContext context) {
|
||||
if (context.health >= 100) return null;
|
||||
|
||||
final bool isSmallHealth = mapId == MapObject.food;
|
||||
return CollectiblePickupEffect(
|
||||
healthToRestore: isSmallHealth ? 4 : 25,
|
||||
pickupSfxId: isSmallHealth
|
||||
? WolfSound.healthSmall
|
||||
: WolfSound.healthLarge,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AmmoCollectible extends Collectible {
|
||||
AmmoCollectible({required super.x, required super.y})
|
||||
: super._(
|
||||
mapId: MapObject.ammoClip,
|
||||
spriteIndex: Collectible._spriteIndexFor(MapObject.ammoClip),
|
||||
);
|
||||
|
||||
@override
|
||||
CollectiblePickupEffect? tryCollect(CollectiblePickupContext context) {
|
||||
if (context.ammo >= 99) return null;
|
||||
|
||||
final bool shouldAutoswitchToPistol =
|
||||
context.currentWeaponType == WeaponType.knife && context.ammo <= 0;
|
||||
|
||||
return CollectiblePickupEffect(
|
||||
ammoToAdd: 8,
|
||||
pickupSfxId: WolfSound.getAmmo,
|
||||
requestWeaponSwitch: shouldAutoswitchToPistol ? WeaponType.pistol : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WeaponCollectible extends Collectible {
|
||||
WeaponCollectible({
|
||||
required super.x,
|
||||
required super.y,
|
||||
required super.mapId,
|
||||
}) : super._(spriteIndex: Collectible._spriteIndexFor(mapId));
|
||||
|
||||
@override
|
||||
CollectiblePickupEffect? tryCollect(CollectiblePickupContext context) {
|
||||
if (mapId == MapObject.machineGun) {
|
||||
return const CollectiblePickupEffect(
|
||||
ammoToAdd: 8,
|
||||
pickupSfxId: WolfSound.getMachineGun,
|
||||
grantWeapon: WeaponType.machineGun,
|
||||
requestWeaponSwitch: WeaponType.machineGun,
|
||||
);
|
||||
}
|
||||
|
||||
if (mapId == MapObject.chainGun) {
|
||||
return const CollectiblePickupEffect(
|
||||
ammoToAdd: 8,
|
||||
pickupSfxId: WolfSound.getGatling,
|
||||
grantWeapon: WeaponType.chainGun,
|
||||
requestWeaponSwitch: WeaponType.chainGun,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class KeyCollectible extends Collectible {
|
||||
KeyCollectible({
|
||||
required super.x,
|
||||
required super.y,
|
||||
required super.mapId,
|
||||
}) : super._(spriteIndex: Collectible._spriteIndexFor(mapId));
|
||||
|
||||
@override
|
||||
CollectiblePickupEffect? tryCollect(CollectiblePickupContext context) {
|
||||
if (mapId == MapObject.goldKey) {
|
||||
if (context.hasGoldKey) return null;
|
||||
return const CollectiblePickupEffect(
|
||||
pickupSfxId: WolfSound.getAmmo,
|
||||
grantGoldKey: true,
|
||||
);
|
||||
}
|
||||
|
||||
if (mapId == MapObject.silverKey) {
|
||||
if (context.hasSilverKey) return null;
|
||||
return const CollectiblePickupEffect(
|
||||
pickupSfxId: WolfSound.getAmmo,
|
||||
grantSilverKey: true,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class TreasureCollectible extends Collectible {
|
||||
TreasureCollectible({
|
||||
required super.x,
|
||||
required super.y,
|
||||
required super.mapId,
|
||||
}) : super._(spriteIndex: Collectible._spriteIndexFor(mapId));
|
||||
|
||||
@override
|
||||
int get scoreValue => switch (mapId) {
|
||||
MapObject.cross => 100,
|
||||
MapObject.chalice => 500,
|
||||
MapObject.chest => 1000,
|
||||
MapObject.crown => 5000,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
@override
|
||||
CollectiblePickupEffect? tryCollect(CollectiblePickupContext context) {
|
||||
if (mapId == MapObject.extraLife) {
|
||||
return const CollectiblePickupEffect(
|
||||
healthToRestore: 100,
|
||||
ammoToAdd: 25,
|
||||
pickupSfxId: WolfSound.extraLife,
|
||||
);
|
||||
}
|
||||
|
||||
return CollectiblePickupEffect(
|
||||
scoreToAdd: scoreValue,
|
||||
pickupSfxId: switch (mapId) {
|
||||
MapObject.cross => WolfSound.treasure1,
|
||||
MapObject.chalice => WolfSound.treasure2,
|
||||
MapObject.chest => WolfSound.treasure3,
|
||||
MapObject.crown => WolfSound.treasure4,
|
||||
_ => WolfSound.getAmmo,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,9 @@ abstract class Enemy extends Entity {
|
||||
/// The sound played when this enemy performs its attack animation.
|
||||
int get attackSoundId => type.attackSoundId;
|
||||
|
||||
/// The score awarded when this enemy is killed.
|
||||
int get scoreValue => type.scoreValue;
|
||||
|
||||
/// The sound played once when this enemy starts dying.
|
||||
int get deathSoundId => type.deathSoundId;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ enum EnemyType {
|
||||
/// Standard Brown Guard (The most common enemy).
|
||||
guard(
|
||||
mapData: EnemyMapData(MapObject.guardStart),
|
||||
scoreValue: 100,
|
||||
alertSoundId: WolfSound.guardHalt,
|
||||
attackSoundId: WolfSound.naziFire,
|
||||
deathSoundId: WolfSound.deathScream1,
|
||||
@@ -26,6 +27,7 @@ enum EnemyType {
|
||||
/// Attack Dog (Fast melee enemy).
|
||||
dog(
|
||||
mapData: EnemyMapData(MapObject.dogStart),
|
||||
scoreValue: 200,
|
||||
alertSoundId: WolfSound.dogBark,
|
||||
attackSoundId: WolfSound.dogAttack,
|
||||
deathSoundId: WolfSound.dogDeath,
|
||||
@@ -48,6 +50,7 @@ enum EnemyType {
|
||||
/// SS Officer (Blue uniform, machine gun).
|
||||
ss(
|
||||
mapData: EnemyMapData(MapObject.ssStart),
|
||||
scoreValue: 100,
|
||||
alertSoundId: WolfSound.ssSchutzstaffel,
|
||||
attackSoundId: WolfSound.naziFire,
|
||||
deathSoundId: WolfSound.ssMeinGott,
|
||||
@@ -64,6 +67,7 @@ enum EnemyType {
|
||||
/// Undead Mutant (Exclusive to later episodes/retail).
|
||||
mutant(
|
||||
mapData: EnemyMapData(MapObject.mutantStart, tierOffset: 18),
|
||||
scoreValue: 100,
|
||||
alertSoundId: WolfSound.guardHalt,
|
||||
attackSoundId: WolfSound.naziFire,
|
||||
deathSoundId: WolfSound.deathScream2,
|
||||
@@ -81,6 +85,7 @@ enum EnemyType {
|
||||
/// High-ranking Officer (White uniform, fast pistol).
|
||||
officer(
|
||||
mapData: EnemyMapData(MapObject.officerStart),
|
||||
scoreValue: 100,
|
||||
alertSoundId: WolfSound.guardHalt,
|
||||
attackSoundId: WolfSound.naziFire,
|
||||
deathSoundId: WolfSound.deathScream3,
|
||||
@@ -102,6 +107,9 @@ enum EnemyType {
|
||||
/// The animation ranges for this enemy type.
|
||||
final EnemyAnimationMap animations;
|
||||
|
||||
/// Score granted when this enemy is killed.
|
||||
final int scoreValue;
|
||||
|
||||
/// The sound played when this enemy first becomes alerted.
|
||||
final int alertSoundId;
|
||||
|
||||
@@ -117,6 +125,7 @@ enum EnemyType {
|
||||
const EnemyType({
|
||||
required this.mapData,
|
||||
required this.animations,
|
||||
required this.scoreValue,
|
||||
required this.alertSoundId,
|
||||
required this.attackSoundId,
|
||||
required this.deathSoundId,
|
||||
|
||||
@@ -15,13 +15,15 @@ import 'package:wolf_3d_dart/src/registry/built_in/shareware_music_module.dart';
|
||||
/// [SharewareMenuPicModule.initWithImageSizes] after the VGA images are
|
||||
/// available so the shift is computed correctly.
|
||||
class SharewareAssetRegistry extends AssetRegistry {
|
||||
SharewareAssetRegistry()
|
||||
SharewareAssetRegistry({bool strictOriginalShareware = false})
|
||||
: super(
|
||||
sfx: const RetailSfxModule(),
|
||||
music: const SharewareMusicModule(),
|
||||
entities: const SharewareEntityModule(),
|
||||
hud: const RetailHudModule(),
|
||||
menu: SharewareMenuPicModule(),
|
||||
menu: SharewareMenuPicModule(
|
||||
useOriginalWl1Map: strictOriginalShareware,
|
||||
),
|
||||
);
|
||||
|
||||
/// Convenience accessor to the menu module for post-load initialisation.
|
||||
|
||||
@@ -15,7 +15,12 @@ import 'package:wolf_3d_dart/src/registry/modules/menu_pic_module.dart';
|
||||
/// module falls back to the retail indices (compatible with unrecognised
|
||||
/// or future shareware variants).
|
||||
class SharewareMenuPicModule extends MenuPicModule {
|
||||
SharewareMenuPicModule();
|
||||
SharewareMenuPicModule({this.useOriginalWl1Map = false});
|
||||
|
||||
/// When true, use the canonical original WL1 index table directly.
|
||||
///
|
||||
/// Unknown/custom shareware-like data should use the heuristic path instead.
|
||||
final bool useOriginalWl1Map;
|
||||
|
||||
// Retail-baseline indices (same layout as RetailMenuPicModule).
|
||||
static final Map<MenuPicKey, int> _retailBaseline = {
|
||||
@@ -42,6 +47,27 @@ class SharewareMenuPicModule extends MenuPicModule {
|
||||
MenuPicKey.difficultyHard: 19,
|
||||
};
|
||||
|
||||
// Canonical menu picture indices for the original v1.4 shareware data
|
||||
// identity (DataVersion.version14Shareware).
|
||||
static final Map<MenuPicKey, int> _originalWl1Map = {
|
||||
MenuPicKey.title: 96,
|
||||
MenuPicKey.credits: 98,
|
||||
MenuPicKey.pg13: 97,
|
||||
MenuPicKey.controlBackground: 35,
|
||||
MenuPicKey.footer: 27,
|
||||
MenuPicKey.heading: 14,
|
||||
MenuPicKey.optionsLabel: 19,
|
||||
MenuPicKey.cursorActive: 20,
|
||||
MenuPicKey.cursorInactive: 21,
|
||||
MenuPicKey.markerSelected: 23,
|
||||
MenuPicKey.markerUnselected: 22,
|
||||
MenuPicKey.episode1: 39,
|
||||
MenuPicKey.difficultyBaby: 28,
|
||||
MenuPicKey.difficultyEasy: 29,
|
||||
MenuPicKey.difficultyNormal: 30,
|
||||
MenuPicKey.difficultyHard: 31,
|
||||
};
|
||||
|
||||
// Landmark constant: STATUSBARPIC in retail picture-table coords.
|
||||
static const int _retailStatusBarIndex = 83;
|
||||
|
||||
@@ -79,9 +105,19 @@ class SharewareMenuPicModule extends MenuPicModule {
|
||||
|
||||
@override
|
||||
MenuPicRef? resolve(MenuPicKey key) {
|
||||
if (useOriginalWl1Map) {
|
||||
final wl1 = _originalWl1Map[key];
|
||||
return wl1 == null ? null : MenuPicRef(wl1);
|
||||
}
|
||||
|
||||
final base = _retailBaseline[key];
|
||||
if (base == null) return null;
|
||||
final shifted = base + (_offset ?? 0);
|
||||
|
||||
// The shareware/release drift is observed around STATUSBAR and later
|
||||
// full-screen art. Earlier control-panel assets keep retail indices.
|
||||
final shifted = base >= _retailStatusBarIndex
|
||||
? base + (_offset ?? 0)
|
||||
: base;
|
||||
return MenuPicRef(shifted);
|
||||
}
|
||||
|
||||
@@ -95,8 +131,9 @@ class SharewareMenuPicModule extends MenuPicModule {
|
||||
MenuPicKey.episode5,
|
||||
MenuPicKey.episode6,
|
||||
];
|
||||
if (episodeIndex >= 0 && episodeIndex < keys.length)
|
||||
if (episodeIndex >= 0 && episodeIndex < keys.length) {
|
||||
return keys[episodeIndex];
|
||||
}
|
||||
return MenuPicKey.episode1;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class BuiltInAssetRegistryResolver implements AssetRegistryResolver {
|
||||
case DataVersion.version14Retail:
|
||||
return RetailAssetRegistry();
|
||||
case DataVersion.version14Shareware:
|
||||
return SharewareAssetRegistry();
|
||||
return SharewareAssetRegistry(strictOriginalShareware: true);
|
||||
case DataVersion.unknown:
|
||||
break; // fall through to GameVersion family check
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
final art = WolfClassicMenuArt(engine.data);
|
||||
|
||||
if (engine.menuManager.activeMenu == WolfMenuScreen.gameSelect) {
|
||||
final cursor = art.pic(
|
||||
final cursor = art.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const int rowYStart = 84;
|
||||
@@ -433,7 +433,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
if (engine.menuManager.activeMenu == WolfMenuScreen.episodeSelect) {
|
||||
_fillRect320(12, 18, 296, 168, panelColor);
|
||||
|
||||
final cursor = art.pic(
|
||||
final cursor = art.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const int rowYStart = 24;
|
||||
@@ -534,7 +534,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
_blitVgaImageAscii(face, 28 + 264 - face.width - 18, 92);
|
||||
}
|
||||
|
||||
final cursor = art.pic(
|
||||
final cursor = art.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const rowYStart = 86;
|
||||
|
||||
@@ -356,7 +356,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
final art = WolfClassicMenuArt(engine.data);
|
||||
if (engine.menuManager.activeMenu == WolfMenuScreen.gameSelect) {
|
||||
_drawMenuTextCentered('SELECT GAME', 48, headingIndex, scale: 2);
|
||||
final cursor = art.pic(
|
||||
final cursor = art.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const int rowYStart = 84;
|
||||
@@ -387,7 +387,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
headingIndex,
|
||||
scale: 2,
|
||||
);
|
||||
final cursor = art.pic(
|
||||
final cursor = art.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const int rowYStart = 30;
|
||||
@@ -442,7 +442,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
scale: _menuHeadingScale,
|
||||
);
|
||||
|
||||
final bottom = art.pic(15);
|
||||
final bottom = art.mappedPic(15);
|
||||
if (bottom != null) {
|
||||
_blitVgaImage(bottom, (320 - bottom.width) ~/ 2, 200 - bottom.height - 8);
|
||||
}
|
||||
@@ -454,7 +454,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
_blitVgaImage(face, 28 + 264 - face.width - 10, 92);
|
||||
}
|
||||
|
||||
final cursor = art.pic(
|
||||
final cursor = art.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const rowYStart = 86;
|
||||
@@ -482,7 +482,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
}
|
||||
|
||||
void _drawMenuFooterArt(WolfClassicMenuArt art) {
|
||||
final bottom = art.pic(15);
|
||||
final bottom = art.mappedPic(15);
|
||||
if (bottom == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
|
||||
_drawMenuTextCentered('SELECT GAME', 38, headingColor, scale: 2);
|
||||
|
||||
final cursor = art.pic(
|
||||
final cursor = art.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
|
||||
@@ -234,7 +234,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
|
||||
_drawMenuTextCentered('WHICH EPISODE TO PLAY?', 6, headingColor, scale: 2);
|
||||
|
||||
final cursor = art.pic(
|
||||
final cursor = art.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const int rowYStart = 30;
|
||||
@@ -277,7 +277,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
}
|
||||
|
||||
void _drawCenteredMenuFooter(WolfClassicMenuArt art) {
|
||||
final bottom = art.pic(15);
|
||||
final bottom = art.mappedPic(15);
|
||||
if (bottom != null) {
|
||||
final int x = ((width - bottom.width) ~/ 2).clamp(0, width - 1);
|
||||
final int y = (height - bottom.height - 8).clamp(0, height - 1);
|
||||
@@ -339,7 +339,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
|
||||
_drawMenuTextCentered(Difficulty.menuText, 48, headingColor, scale: 2);
|
||||
|
||||
final bottom = art.pic(15);
|
||||
final bottom = art.mappedPic(15);
|
||||
if (bottom != null) {
|
||||
final x = (width - bottom.width) ~/ 2;
|
||||
final y = height - bottom.height - 8;
|
||||
@@ -353,7 +353,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
_blitVgaImage(face, panelX + panelW - face.width - 10, panelY + 22);
|
||||
}
|
||||
|
||||
final cursor = art.pic(
|
||||
final cursor = art.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const int rowYStart = panelY + 16;
|
||||
|
||||
@@ -98,103 +98,45 @@ class WolfClassicMenuArt {
|
||||
|
||||
WolfClassicMenuArt(this.data);
|
||||
|
||||
int? _resolvedIndexOffset;
|
||||
|
||||
VgaImage? get controlBackground {
|
||||
final preferred = mappedPic(WolfMenuPic.cControl);
|
||||
if (_looksLikeMenuBackdrop(preferred)) {
|
||||
return preferred;
|
||||
}
|
||||
|
||||
// Older data layouts may shift/control-panel art around nearby indices.
|
||||
for (int delta = -4; delta <= 4; delta++) {
|
||||
final candidate = mappedPic(WolfMenuPic.cControl + delta);
|
||||
if (_looksLikeMenuBackdrop(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return preferred;
|
||||
return _imageForKey(MenuPicKey.controlBackground);
|
||||
}
|
||||
|
||||
VgaImage? get title => mappedPic(WolfMenuPic.title);
|
||||
VgaImage? get title => _imageForKey(MenuPicKey.title);
|
||||
|
||||
VgaImage? get heading => mappedPic(WolfMenuPic.hTopWindow);
|
||||
VgaImage? get heading => _imageForKey(MenuPicKey.heading);
|
||||
|
||||
VgaImage? get selectedMarker => mappedPic(WolfMenuPic.cSelected);
|
||||
VgaImage? get selectedMarker => _imageForKey(MenuPicKey.markerSelected);
|
||||
|
||||
VgaImage? get unselectedMarker => mappedPic(WolfMenuPic.cNotSelected);
|
||||
VgaImage? get unselectedMarker => _imageForKey(MenuPicKey.markerUnselected);
|
||||
|
||||
VgaImage? get optionsLabel => mappedPic(WolfMenuPic.cOptions);
|
||||
VgaImage? get optionsLabel => _imageForKey(MenuPicKey.optionsLabel);
|
||||
|
||||
VgaImage? get credits => mappedPic(WolfMenuPic.credits);
|
||||
VgaImage? get credits => _imageForKey(MenuPicKey.credits);
|
||||
|
||||
VgaImage? episodeOption(int episodeIndex) {
|
||||
if (episodeIndex < 0 || episodeIndex >= WolfMenuPic.episodePics.length) {
|
||||
if (episodeIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
return mappedPic(WolfMenuPic.episodePics[episodeIndex]);
|
||||
final key = data.registry.menu.episodeKey(episodeIndex);
|
||||
return _imageForKey(key);
|
||||
}
|
||||
|
||||
VgaImage? difficultyOption(Difficulty difficulty) {
|
||||
switch (difficulty) {
|
||||
case Difficulty.baby:
|
||||
return mappedPic(WolfMenuPic.cBabyMode);
|
||||
case Difficulty.easy:
|
||||
return mappedPic(WolfMenuPic.cEasy);
|
||||
case Difficulty.medium:
|
||||
return mappedPic(WolfMenuPic.cNormal);
|
||||
case Difficulty.hard:
|
||||
return mappedPic(WolfMenuPic.cHard);
|
||||
}
|
||||
final key = data.registry.menu.difficultyKey(difficulty);
|
||||
return _imageForKey(key);
|
||||
}
|
||||
|
||||
/// Returns [index] after applying a detected version/layout offset.
|
||||
/// Legacy numeric lookup API retained for existing renderer call sites.
|
||||
///
|
||||
/// Known legacy indices are mapped through symbolic registry keys first.
|
||||
/// Unknown indices fall back to direct picture-table indexing.
|
||||
VgaImage? mappedPic(int index) {
|
||||
return pic(index + _indexOffset);
|
||||
}
|
||||
|
||||
int get _indexOffset {
|
||||
if (_resolvedIndexOffset != null) {
|
||||
return _resolvedIndexOffset!;
|
||||
final key = _legacyKeyForIndex(index);
|
||||
if (key != null) {
|
||||
return _imageForKey(key);
|
||||
}
|
||||
|
||||
// Retail and shareware generally place STATUSBAR/TITLE/PG13/CREDITS as a
|
||||
// contiguous block. If files are from a different release, infer a shift.
|
||||
for (int i = 0; i < data.vgaImages.length - 3; i++) {
|
||||
final status = data.vgaImages[i];
|
||||
if (!_looksLikeStatusBar(status)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final title = data.vgaImages[i + 1];
|
||||
final pg13 = data.vgaImages[i + 2];
|
||||
final credits = data.vgaImages[i + 3];
|
||||
if (_looksLikeFullScreen(title) &&
|
||||
_looksLikeFullScreen(pg13) &&
|
||||
_looksLikeFullScreen(credits)) {
|
||||
_resolvedIndexOffset = i - WolfMenuPic.statusBar;
|
||||
return _resolvedIndexOffset!;
|
||||
}
|
||||
}
|
||||
|
||||
_resolvedIndexOffset = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool _looksLikeStatusBar(VgaImage image) {
|
||||
return image.width >= 280 && image.height >= 24 && image.height <= 64;
|
||||
}
|
||||
|
||||
bool _looksLikeFullScreen(VgaImage image) {
|
||||
return image.width >= 280 && image.height >= 140;
|
||||
}
|
||||
|
||||
bool _looksLikeMenuBackdrop(VgaImage? image) {
|
||||
if (image == null) {
|
||||
return false;
|
||||
}
|
||||
return image.width >= 180 && image.height >= 100;
|
||||
return pic(index);
|
||||
}
|
||||
|
||||
VgaImage? pic(int index) {
|
||||
@@ -203,14 +145,67 @@ class WolfClassicMenuArt {
|
||||
}
|
||||
final image = data.vgaImages[index];
|
||||
|
||||
// Ignore known gameplay HUD art in menu composition.
|
||||
if (index == WolfMenuPic.statusBar + _indexOffset) {
|
||||
return null;
|
||||
}
|
||||
if (image.width <= 0 || image.height <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
VgaImage? _imageForKey(MenuPicKey key) {
|
||||
final ref = data.registry.menu.resolve(key);
|
||||
if (ref == null) {
|
||||
return null;
|
||||
}
|
||||
return pic(ref.pictureIndex);
|
||||
}
|
||||
|
||||
MenuPicKey? _legacyKeyForIndex(int index) {
|
||||
switch (index) {
|
||||
case WolfMenuPic.hTopWindow:
|
||||
return MenuPicKey.heading;
|
||||
case WolfMenuPic.cOptions:
|
||||
return MenuPicKey.optionsLabel;
|
||||
case WolfMenuPic.cCursor1:
|
||||
return MenuPicKey.cursorActive;
|
||||
case WolfMenuPic.cCursor2:
|
||||
return MenuPicKey.cursorInactive;
|
||||
case WolfMenuPic.cNotSelected:
|
||||
return MenuPicKey.markerUnselected;
|
||||
case WolfMenuPic.cSelected:
|
||||
return MenuPicKey.markerSelected;
|
||||
case 15:
|
||||
return MenuPicKey.footer;
|
||||
case WolfMenuPic.cBabyMode:
|
||||
return MenuPicKey.difficultyBaby;
|
||||
case WolfMenuPic.cEasy:
|
||||
return MenuPicKey.difficultyEasy;
|
||||
case WolfMenuPic.cNormal:
|
||||
return MenuPicKey.difficultyNormal;
|
||||
case WolfMenuPic.cHard:
|
||||
return MenuPicKey.difficultyHard;
|
||||
case WolfMenuPic.cControl:
|
||||
return MenuPicKey.controlBackground;
|
||||
case WolfMenuPic.cEpisode1:
|
||||
return MenuPicKey.episode1;
|
||||
case WolfMenuPic.cEpisode2:
|
||||
return MenuPicKey.episode2;
|
||||
case WolfMenuPic.cEpisode3:
|
||||
return MenuPicKey.episode3;
|
||||
case WolfMenuPic.cEpisode4:
|
||||
return MenuPicKey.episode4;
|
||||
case WolfMenuPic.cEpisode5:
|
||||
return MenuPicKey.episode5;
|
||||
case WolfMenuPic.cEpisode6:
|
||||
return MenuPicKey.episode6;
|
||||
case WolfMenuPic.title:
|
||||
return MenuPicKey.title;
|
||||
case WolfMenuPic.pg13:
|
||||
return MenuPicKey.pg13;
|
||||
case WolfMenuPic.credits:
|
||||
return MenuPicKey.credits;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user