Refactor menu rendering and asset registry structure

- Updated SoftwareRenderer to incorporate MenuHeaderBand for handling spear variant menus and improved backdrop drawing.
- Refactored asset registry imports to organize menu-related assets under a dedicated menu structure.
- Enhanced game session snapshot tests to validate menu theme restoration for spear variant games.
- Added tests for classic menu presentation module to ensure palette consistency with canonical constants.
- Implemented tests for spear asset registry to verify correct menu VGA index resolutions.
- Created unit tests for MenuHeaderBand to validate functionality in rendering menu headers and sidebars.
- Adjusted HUD module imports to align with new menu structure.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-24 23:35:56 +01:00
parent 5c309c2240
commit d393ca98ec
40 changed files with 1327 additions and 296 deletions
@@ -1,10 +1,12 @@
import 'dart:typed_data';
import 'package:test/test.dart';
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_asset_registry.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
import 'package:wolf_3d_dart/wolf_3d_input.dart';
import 'package:wolf_3d_dart/wolf_3d_menu.dart';
void main() {
test(
@@ -122,6 +124,104 @@ void main() {
expect(engine.entities.last, isA<SmallAmmoCollectible>());
},
);
test('restoreSaveState applies menu theme from restored active game', () {
final engine = _buildEngineWithTwoGames();
engine.init();
final GameSessionSnapshot snapshot = engine.captureSaveState();
final GameSessionSnapshot restoredSnapshot = GameSessionSnapshot(
currentGameIndex: 1,
currentEpisodeIndex: snapshot.currentEpisodeIndex,
currentLevelIndex: snapshot.currentLevelIndex,
returnLevelIndex: snapshot.returnLevelIndex,
difficulty: snapshot.difficulty,
timeAliveMs: snapshot.timeAliveMs,
lastAcousticAlertTime: snapshot.lastAcousticAlertTime,
isMapOverlayVisible: snapshot.isMapOverlayVisible,
isMenuOverlayVisible: snapshot.isMenuOverlayVisible,
player: snapshot.player,
currentLevel: snapshot.currentLevel,
areaGrid: snapshot.areaGrid,
areasByPlayer: snapshot.areasByPlayer,
entities: snapshot.entities,
doors: snapshot.doors,
pushwalls: snapshot.pushwalls,
);
engine.restoreSaveState(restoredSnapshot);
final presentation = WolfMenuPresentation(engine.data);
final bool isSpear =
engine.data.version == GameVersion.spearOfDestiny ||
engine.data.version == GameVersion.spearOfDestinyDemo;
final int expectedBackground = isSpear
? _rgb24FromVgaIndex(
_resolvedMenuColorIndex(
presentation.backgroundIndex,
engine.data.version,
),
)
: _paletteMappedRgb24(0x890000);
final int expectedPanel = isSpear
? 0x000359
: _paletteMappedRgb24(0x590002);
expect(engine.currentGameIndex, 1);
expect(engine.menuBackgroundRgb, expectedBackground);
expect(engine.menuPanelRgb, expectedPanel);
expect(engine.menuManager.menuBackgroundRgb, expectedBackground);
});
}
int _rgb24FromVgaIndex(int paletteIndex) {
final int argb = ColorPalette.argbFromVgaIndex(paletteIndex);
final int r = (argb >> 16) & 0xFF;
final int g = (argb >> 8) & 0xFF;
final int b = argb & 0xFF;
return (r << 16) | (g << 8) | b;
}
int _resolvedMenuColorIndex(int paletteIndex, GameVersion version) {
final bool isSpear =
version == GameVersion.spearOfDestiny ||
version == GameVersion.spearOfDestinyDemo;
if (!isSpear && paletteIndex >= 0x20 && paletteIndex <= 0x2F) {
return paletteIndex + 0x70;
}
return paletteIndex;
}
int _paletteMappedRgb24(int rgb) {
final int index = _closestVgaIndexForRgb24(rgb);
return _rgb24FromVgaIndex(index);
}
int _closestVgaIndexForRgb24(int rgb24) {
final int targetR = (rgb24 >> 16) & 0xFF;
final int targetG = (rgb24 >> 8) & 0xFF;
final int targetB = rgb24 & 0xFF;
int bestIndex = 0;
int bestDistance = 0x7FFFFFFF;
for (int index = 0; index < 256; index++) {
final int argb = ColorPalette.argbFromVgaIndex(index);
final int r = (argb >> 16) & 0xFF;
final int g = (argb >> 8) & 0xFF;
final int b = argb & 0xFF;
final int dr = targetR - r;
final int dg = targetG - g;
final int db = targetB - b;
final int distance = (dr * dr) + (dg * dg) + (db * db);
if (distance < bestDistance) {
bestDistance = distance;
bestIndex = index;
}
}
return bestIndex;
}
class _TestInput extends Wolf3dInput {
@@ -162,6 +262,35 @@ class _SilentAudio implements EngineAudio {
}
WolfEngine _buildEngine() {
final data = _buildTestData(version: GameVersion.retail);
return WolfEngine(
data: data,
difficulty: Difficulty.medium,
startingEpisode: 0,
frameBuffer: FrameBuffer(64, 64),
input: _TestInput(),
onGameWon: () {},
engineAudio: _SilentAudio(),
);
}
WolfEngine _buildEngineWithTwoGames() {
final retail = _buildTestData(version: GameVersion.retail);
final spear = _buildTestData(version: GameVersion.spearOfDestiny);
return WolfEngine(
availableGames: <WolfensteinData>[retail, spear],
difficulty: Difficulty.medium,
startingEpisode: 0,
frameBuffer: FrameBuffer(64, 64),
input: _TestInput(),
onGameWon: () {},
engineAudio: _SilentAudio(),
);
}
WolfensteinData _buildTestData({required GameVersion version}) {
final wallGrid = _buildGrid();
final objectGrid = _buildGrid();
_fillBoundaries(wallGrid, 2);
@@ -171,43 +300,38 @@ WolfEngine _buildEngine() {
wallGrid[2][3] = 90;
wallGrid[4][4] = 5;
return WolfEngine(
data: WolfensteinData(
version: GameVersion.retail,
dataVersion: DataVersion.unknown,
registry: RetailAssetRegistry(),
walls: [
_solidSprite(1),
_solidSprite(1),
_solidSprite(2),
_solidSprite(2),
],
sprites: List.generate(436, (_) => _solidSprite(255)),
sounds: List.generate(200, (_) => PcmSound(Uint8List(1))),
adLibSounds: const [],
music: const [],
vgaImages: const [],
episodes: [
Episode(
name: 'Episode 1',
levels: [
WolfLevel(
name: 'Level 1',
wallGrid: wallGrid,
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
objectGrid: objectGrid,
music: Music.level01,
),
],
),
],
),
difficulty: Difficulty.medium,
startingEpisode: 0,
frameBuffer: FrameBuffer(64, 64),
input: _TestInput(),
onGameWon: () {},
engineAudio: _SilentAudio(),
return WolfensteinData(
version: version,
dataVersion: DataVersion.unknown,
registry: switch (version) {
GameVersion.spearOfDestiny => SpearAssetRegistry(),
_ => RetailAssetRegistry(),
},
walls: [
_solidSprite(1),
_solidSprite(1),
_solidSprite(2),
_solidSprite(2),
],
sprites: List.generate(436, (_) => _solidSprite(255)),
sounds: List.generate(200, (_) => PcmSound(Uint8List(1))),
adLibSounds: const [],
music: const [],
vgaImages: const [],
episodes: [
Episode(
name: 'Episode 1',
levels: [
WolfLevel(
name: 'Level 1',
wallGrid: wallGrid,
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
objectGrid: objectGrid,
music: Music.level01,
),
],
),
],
);
}