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:
@@ -12,6 +12,10 @@ void main() async {
|
|||||||
debug: kDebugMode,
|
debug: kDebugMode,
|
||||||
).init();
|
).init();
|
||||||
|
|
||||||
|
if (kDebugMode) {
|
||||||
|
wolf3d.enableMenuHeaderBandDebugLogging(prefix: '[wolf_3d_gui]');
|
||||||
|
}
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: Wolf3dGuiApp(engine: wolf3d),
|
home: Wolf3dGuiApp(engine: wolf3d),
|
||||||
|
|||||||
@@ -238,6 +238,17 @@ The presentation module should treat its image-returning methods as optional hoo
|
|||||||
|
|
||||||
- return a `VgaImage` when that surface has variant-specific art,
|
- return a `VgaImage` when that surface has variant-specific art,
|
||||||
- return `null` when the presentation intentionally has no image for that concept,
|
- return `null` when the presentation intentionally has no image for that concept,
|
||||||
|
|
||||||
|
### Palette Conversion Guardrail
|
||||||
|
|
||||||
|
When mapping target RGB menu tones to a VGA palette index (for example, preserving Wolf classic dark-red theme), resolve nearest colors from `ColorPalette.argbFromVgaIndex()` values.
|
||||||
|
|
||||||
|
Do not use `ColorPalette.findClosestPaletteIndex()` for this specific workflow, because its channel interpretation is legacy-oriented and can produce hue-swapped matches (for example, red targets resolving to blue-ish indices).
|
||||||
|
|
||||||
|
In short:
|
||||||
|
|
||||||
|
- For variant-defined menu colors: use explicit palette indices from `MenuPresentationModule`.
|
||||||
|
- For host-defined fallback RGB tones: find nearest VGA index by comparing RGB distance against `argbFromVgaIndex()` output.
|
||||||
- use `mappedPic(...)` only for legacy numeric menu art lookups that still matter for a renderer path.
|
- use `mappedPic(...)` only for legacy numeric menu art lookups that still matter for a renderer path.
|
||||||
|
|
||||||
### Wiring A Fully Custom Registry
|
### Wiring A Fully Custom Registry
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:wolf_3d_dart/src/rendering/menu_header_band.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.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_engine.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||||
@@ -37,6 +38,33 @@ class Wolf3dEngine {
|
|||||||
/// Enables host-level debug affordances such as debug navigation UI.
|
/// Enables host-level debug affordances such as debug navigation UI.
|
||||||
Wolf3dEngine enableDebug() {
|
Wolf3dEngine enableDebug() {
|
||||||
_debugEnabled = true;
|
_debugEnabled = true;
|
||||||
|
enableMenuHeaderBandDebugLogging();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Routes shared menu header band diagnostics to [logger].
|
||||||
|
///
|
||||||
|
/// Pass `null` to disable menu header band diagnostics.
|
||||||
|
Wolf3dEngine setMenuHeaderBandDebugLogger(
|
||||||
|
void Function(String message)? logger,
|
||||||
|
) {
|
||||||
|
MenuHeaderBand.debugLogger = logger;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enables menu header band diagnostics with an optional [prefix].
|
||||||
|
Wolf3dEngine enableMenuHeaderBandDebugLogging({
|
||||||
|
String prefix = '[MENU_HEADER_BAND]',
|
||||||
|
}) {
|
||||||
|
MenuHeaderBand.debugLogger = (String message) {
|
||||||
|
print('$prefix $message');
|
||||||
|
};
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disables menu header band diagnostics.
|
||||||
|
Wolf3dEngine disableMenuHeaderBandDebugLogging() {
|
||||||
|
MenuHeaderBand.debugLogger = null;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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_engine.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_entities.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_input.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_menu.dart';
|
||||||
|
|
||||||
/// The core orchestration class for the Wolfenstein 3D engine.
|
/// The core orchestration class for the Wolfenstein 3D engine.
|
||||||
///
|
///
|
||||||
@@ -61,7 +62,7 @@ class WolfEngine {
|
|||||||
if (_availableGames.isEmpty) {
|
if (_availableGames.isEmpty) {
|
||||||
throw StateError('WolfEngine requires at least one game data set.');
|
throw StateError('WolfEngine requires at least one game data set.');
|
||||||
}
|
}
|
||||||
menuManager.menuBackgroundRgb = menuBackgroundRgb;
|
_applyMenuPresentationTheme();
|
||||||
_normalizeRendererSettings();
|
_normalizeRendererSettings();
|
||||||
_syncRendererMenuModel();
|
_syncRendererMenuModel();
|
||||||
}
|
}
|
||||||
@@ -88,10 +89,10 @@ class WolfEngine {
|
|||||||
WolfensteinData get data => _availableGames[_currentGameIndex];
|
WolfensteinData get data => _availableGames[_currentGameIndex];
|
||||||
|
|
||||||
/// Desired menu background color in 24-bit RGB.
|
/// Desired menu background color in 24-bit RGB.
|
||||||
final int menuBackgroundRgb;
|
int menuBackgroundRgb;
|
||||||
|
|
||||||
/// Desired menu panel color in 24-bit RGB.
|
/// Desired menu panel color in 24-bit RGB.
|
||||||
final int menuPanelRgb;
|
int menuPanelRgb;
|
||||||
|
|
||||||
/// The active difficulty level, affecting enemy spawning and behavior.
|
/// The active difficulty level, affecting enemy spawning and behavior.
|
||||||
Difficulty? difficulty;
|
Difficulty? difficulty;
|
||||||
@@ -231,6 +232,7 @@ class WolfEngine {
|
|||||||
void init() {
|
void init() {
|
||||||
_currentGameIndex = 0;
|
_currentGameIndex = 0;
|
||||||
audio.activeGame = data;
|
audio.activeGame = data;
|
||||||
|
_applyMenuPresentationTheme();
|
||||||
onGameSelected?.call(data);
|
onGameSelected?.call(data);
|
||||||
|
|
||||||
_currentEpisodeIndex = startingEpisode ?? 0;
|
_currentEpisodeIndex = startingEpisode ?? 0;
|
||||||
@@ -244,6 +246,7 @@ class WolfEngine {
|
|||||||
hasResumableGame: false,
|
hasResumableGame: false,
|
||||||
hasLoadableSave: _hasLoadableSave,
|
hasLoadableSave: _hasLoadableSave,
|
||||||
initialGameIsRetail: data.version == GameVersion.retail,
|
initialGameIsRetail: data.version == GameVersion.retail,
|
||||||
|
initialGameIsSpear: _isSpearVariant(data.version),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (_availableGames.length == 1) {
|
if (_availableGames.length == 1) {
|
||||||
@@ -428,6 +431,8 @@ class WolfEngine {
|
|||||||
|
|
||||||
_currentGameIndex = snapshot.currentGameIndex;
|
_currentGameIndex = snapshot.currentGameIndex;
|
||||||
audio.activeGame = data;
|
audio.activeGame = data;
|
||||||
|
_applyMenuPresentationTheme();
|
||||||
|
menuManager.setCurrentGameVersion(data.version);
|
||||||
onGameSelected?.call(data);
|
onGameSelected?.call(data);
|
||||||
|
|
||||||
_currentEpisodeIndex = snapshot.currentEpisodeIndex;
|
_currentEpisodeIndex = snapshot.currentEpisodeIndex;
|
||||||
@@ -744,6 +749,72 @@ class WolfEngine {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _applyMenuPresentationTheme() {
|
||||||
|
if (!_isSpearVariant(data.version)) {
|
||||||
|
menuBackgroundRgb = _paletteMappedRgb24(0x890000);
|
||||||
|
menuPanelRgb = _paletteMappedRgb24(0x590002);
|
||||||
|
menuManager.menuBackgroundRgb = menuBackgroundRgb;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final presentation = WolfMenuPresentation(data);
|
||||||
|
final int resolvedBackgroundIndex = _resolvedMenuColorIndex(
|
||||||
|
presentation.backgroundIndex,
|
||||||
|
);
|
||||||
|
menuBackgroundRgb = _rgb24FromVgaIndex(resolvedBackgroundIndex);
|
||||||
|
menuPanelRgb = 0x000359;
|
||||||
|
menuManager.menuBackgroundRgb = menuBackgroundRgb;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _resolvedMenuColorIndex(int paletteIndex) {
|
||||||
|
if (_isSpearVariant(data.version)) {
|
||||||
|
return paletteIndex;
|
||||||
|
}
|
||||||
|
if (paletteIndex >= 0x20 && paletteIndex <= 0x2F) {
|
||||||
|
return paletteIndex + 0x70;
|
||||||
|
}
|
||||||
|
return paletteIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/// The primary heartbeat of the engine.
|
/// The primary heartbeat of the engine.
|
||||||
///
|
///
|
||||||
/// Updates all world subsystems based on the [elapsed] time.
|
/// Updates all world subsystems based on the [elapsed] time.
|
||||||
@@ -913,6 +984,8 @@ class WolfEngine {
|
|||||||
if (menuResult.selectedIndex != null) {
|
if (menuResult.selectedIndex != null) {
|
||||||
_currentGameIndex = menuResult.selectedIndex!;
|
_currentGameIndex = menuResult.selectedIndex!;
|
||||||
audio.activeGame = data;
|
audio.activeGame = data;
|
||||||
|
_applyMenuPresentationTheme();
|
||||||
|
menuManager.setCurrentGameVersion(data.version);
|
||||||
onGameSelected?.call(data);
|
onGameSelected?.call(data);
|
||||||
_currentEpisodeIndex = 0;
|
_currentEpisodeIndex = 0;
|
||||||
onEpisodeSelected?.call(null);
|
onEpisodeSelected?.call(null);
|
||||||
@@ -949,7 +1022,11 @@ class WolfEngine {
|
|||||||
void _tickDifficultySelectionMenu(EngineInput input) {
|
void _tickDifficultySelectionMenu(EngineInput input) {
|
||||||
final menuResult = menuManager.updateDifficultySelection(input);
|
final menuResult = menuManager.updateDifficultySelection(input);
|
||||||
if (menuResult.goBack) {
|
if (menuResult.goBack) {
|
||||||
|
if (_isSingleEpisodeFlowForCurrentGame) {
|
||||||
|
menuManager.startTransition(WolfMenuScreen.mainMenu);
|
||||||
|
} else {
|
||||||
menuManager.startTransition(WolfMenuScreen.episodeSelect);
|
menuManager.startTransition(WolfMenuScreen.episodeSelect);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1021,9 +1098,21 @@ class WolfEngine {
|
|||||||
void _beginNewGameMenuFlow() {
|
void _beginNewGameMenuFlow() {
|
||||||
onEpisodeSelected?.call(null);
|
onEpisodeSelected?.call(null);
|
||||||
menuManager.clearEpisodeSelection();
|
menuManager.clearEpisodeSelection();
|
||||||
|
if (_isSingleEpisodeFlowForCurrentGame) {
|
||||||
|
menuManager.startTransition(WolfMenuScreen.difficultySelect);
|
||||||
|
return;
|
||||||
|
}
|
||||||
menuManager.startTransition(WolfMenuScreen.episodeSelect);
|
menuManager.startTransition(WolfMenuScreen.episodeSelect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get _isSingleEpisodeFlowForCurrentGame =>
|
||||||
|
_isSpearVariant(data.version) || data.episodes.length <= 1;
|
||||||
|
|
||||||
|
bool _isSpearVariant(GameVersion version) {
|
||||||
|
return version == GameVersion.spearOfDestiny ||
|
||||||
|
version == GameVersion.spearOfDestinyDemo;
|
||||||
|
}
|
||||||
|
|
||||||
void _openPauseMenu() {
|
void _openPauseMenu() {
|
||||||
if (!_hasActiveSession) {
|
if (!_hasActiveSession) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ abstract class _MenuManagerBase {
|
|||||||
bool _showResumeOption = false;
|
bool _showResumeOption = false;
|
||||||
bool _hasLoadableSave = false;
|
bool _hasLoadableSave = false;
|
||||||
int _gameCount = 1;
|
int _gameCount = 1;
|
||||||
|
bool _isSpearVariant = false;
|
||||||
|
|
||||||
bool _prevUp = false;
|
bool _prevUp = false;
|
||||||
bool _prevDown = false;
|
bool _prevDown = false;
|
||||||
@@ -129,6 +130,7 @@ abstract class _MenuManagerBase {
|
|||||||
bool hasResumableGame = false,
|
bool hasResumableGame = false,
|
||||||
bool hasLoadableSave = false,
|
bool hasLoadableSave = false,
|
||||||
bool initialGameIsRetail = false,
|
bool initialGameIsRetail = false,
|
||||||
|
bool initialGameIsSpear = false,
|
||||||
WolfTransitionEffect introEffect = WolfTransitionEffect.normalFade,
|
WolfTransitionEffect introEffect = WolfTransitionEffect.normalFade,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ mixin _MenuManagerIntroMixin on _MenuManagerBase {
|
|||||||
bool hasResumableGame = false,
|
bool hasResumableGame = false,
|
||||||
bool hasLoadableSave = false,
|
bool hasLoadableSave = false,
|
||||||
bool initialGameIsRetail = false,
|
bool initialGameIsRetail = false,
|
||||||
|
bool initialGameIsSpear = false,
|
||||||
WolfTransitionEffect introEffect = WolfTransitionEffect.normalFade,
|
WolfTransitionEffect introEffect = WolfTransitionEffect.normalFade,
|
||||||
}) {
|
}) {
|
||||||
_gameCount = gameCount;
|
_gameCount = gameCount;
|
||||||
@@ -153,6 +154,7 @@ mixin _MenuManagerIntroMixin on _MenuManagerBase {
|
|||||||
: Difficulty.values
|
: Difficulty.values
|
||||||
.indexOf(initialDifficulty)
|
.indexOf(initialDifficulty)
|
||||||
.clamp(0, Difficulty.values.length - 1);
|
.clamp(0, Difficulty.values.length - 1);
|
||||||
|
_isSpearVariant = initialGameIsSpear;
|
||||||
_introLandingMenu = WolfMenuScreen.mainMenu;
|
_introLandingMenu = WolfMenuScreen.mainMenu;
|
||||||
if (gameCount > 1) {
|
if (gameCount > 1) {
|
||||||
_activeMenu = WolfMenuScreen.gameSelect;
|
_activeMenu = WolfMenuScreen.gameSelect;
|
||||||
|
|||||||
@@ -42,8 +42,7 @@ mixin _MenuManagerSelectionMixin on _MenuManagerBase {
|
|||||||
|
|
||||||
/// Immutable snapshot of the current main-menu rows.
|
/// Immutable snapshot of the current main-menu rows.
|
||||||
List<WolfMenuMainEntry> get mainMenuEntries {
|
List<WolfMenuMainEntry> get mainMenuEntries {
|
||||||
return List<WolfMenuMainEntry>.unmodifiable(
|
final List<WolfMenuMainEntry> entries = <WolfMenuMainEntry>[
|
||||||
<WolfMenuMainEntry>[
|
|
||||||
_mainMenuEntry(
|
_mainMenuEntry(
|
||||||
action: WolfMenuMainAction.newGame,
|
action: WolfMenuMainAction.newGame,
|
||||||
label: 'NEW GAME',
|
label: 'NEW GAME',
|
||||||
@@ -68,25 +67,51 @@ mixin _MenuManagerSelectionMixin on _MenuManagerBase {
|
|||||||
action: WolfMenuMainAction.changeView,
|
action: WolfMenuMainAction.changeView,
|
||||||
label: 'CHANGE VIEW',
|
label: 'CHANGE VIEW',
|
||||||
),
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!_isSpearVariant) {
|
||||||
|
entries.add(
|
||||||
_mainMenuEntry(
|
_mainMenuEntry(
|
||||||
action: WolfMenuMainAction.readThis,
|
action: WolfMenuMainAction.readThis,
|
||||||
label: 'READ THIS!',
|
label: 'READ THIS!',
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.add(
|
||||||
_mainMenuEntry(
|
_mainMenuEntry(
|
||||||
action: _showResumeOption
|
action: _showResumeOption
|
||||||
? WolfMenuMainAction.endGame
|
? WolfMenuMainAction.endGame
|
||||||
: WolfMenuMainAction.viewScores,
|
: WolfMenuMainAction.viewScores,
|
||||||
label: _showResumeOption ? 'END GAME' : 'VIEW SCORES',
|
label: _showResumeOption ? 'END GAME' : 'VIEW SCORES',
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
entries.add(
|
||||||
_mainMenuEntry(
|
_mainMenuEntry(
|
||||||
action: _showResumeOption
|
action: _showResumeOption
|
||||||
? WolfMenuMainAction.backToGame
|
? WolfMenuMainAction.backToGame
|
||||||
: WolfMenuMainAction.backToDemo,
|
: WolfMenuMainAction.backToDemo,
|
||||||
label: _showResumeOption ? 'BACK TO GAME' : 'BACK TO DEMO',
|
label: _showResumeOption ? 'BACK TO GAME' : 'BACK TO DEMO',
|
||||||
),
|
),
|
||||||
_mainMenuEntry(action: WolfMenuMainAction.quit, label: 'QUIT'),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
entries.add(_mainMenuEntry(action: WolfMenuMainAction.quit, label: 'QUIT'));
|
||||||
|
|
||||||
|
return List<WolfMenuMainEntry>.unmodifiable(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates menu variant flags from the selected game version.
|
||||||
|
void setCurrentGameVersion(GameVersion version) {
|
||||||
|
_isSpearVariant =
|
||||||
|
version == GameVersion.spearOfDestiny ||
|
||||||
|
version == GameVersion.spearOfDestinyDemo;
|
||||||
|
_selectedMainIndex = clampIndex(_selectedMainIndex, mainMenuEntries.length);
|
||||||
|
if (!_isSelectableMainIndex(_selectedMainIndex)) {
|
||||||
|
_selectedMainIndex = findSelectableIndex(
|
||||||
|
_selectedMainIndex,
|
||||||
|
mainMenuEntries.length,
|
||||||
|
_isSelectableMainIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the main menu can return to the game-selection step.
|
/// Whether the main menu can return to the game-selection step.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:wolf_3d_dart/src/registry/built_in/classic_menu_presentation_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_menu_presentation_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/spear_menu_presentation_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/wolf/classic_menu_presentation_module.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
/// Bound access to the active menu presentation for a loaded data set.
|
/// Bound access to the active menu presentation for a loaded data set.
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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/built_in_music_module.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_demo_entity_module.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_demo_hud_module.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_demo_sfx_module.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_menu_module.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_menu_presentation_module.dart';
|
||||||
|
|
||||||
|
/// Built-in [AssetRegistry] for full Spear of Destiny (`.SOD`).
|
||||||
|
class SpearAssetRegistry extends AssetRegistry {
|
||||||
|
SpearAssetRegistry()
|
||||||
|
: super(
|
||||||
|
sfx: const SpearDemoSfxModule(),
|
||||||
|
music: const BuiltInMusicModule(GameVersion.spearOfDestiny),
|
||||||
|
entities: const SpearDemoEntityModule(),
|
||||||
|
hud: const SpearDemoHudModule(),
|
||||||
|
menu: const SpearMenuPicModule(),
|
||||||
|
menuPresentation: const SpearMenuPresentationModule(),
|
||||||
|
);
|
||||||
|
}
|
||||||
+5
-5
@@ -1,11 +1,11 @@
|
|||||||
import 'package:wolf_3d_dart/src/data_types/game_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/asset_registry.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/built_in_music_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/built_in_music_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/spear_demo_entity_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_demo_entity_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/spear_demo_hud_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_demo_hud_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/spear_demo_menu_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_demo_menu_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/spear_demo_sfx_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_demo_sfx_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/spear_menu_presentation_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_menu_presentation_module.dart';
|
||||||
|
|
||||||
/// Built-in [AssetRegistry] for Spear of Destiny demo/shareware (`.SDM`).
|
/// Built-in [AssetRegistry] for Spear of Destiny demo/shareware (`.SDM`).
|
||||||
class SpearDemoAssetRegistry extends AssetRegistry {
|
class SpearDemoAssetRegistry extends AssetRegistry {
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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 full Spear of Destiny releases (`.SOD`).
|
||||||
|
///
|
||||||
|
/// Picture indices are derived from `GFXV_SOD.H` (`chunkId - STARTPICS`).
|
||||||
|
class SpearMenuPicModule extends MenuPicModule {
|
||||||
|
const SpearMenuPicModule();
|
||||||
|
|
||||||
|
static final Map<MenuPicKey, int> _indices = {
|
||||||
|
MenuPicKey.title: 76, // TITLE1PIC
|
||||||
|
MenuPicKey.credits: 89, // CREDITSPIC
|
||||||
|
MenuPicKey.pg13: 88, // PG13PIC
|
||||||
|
|
||||||
|
MenuPicKey.controlBackground: 12, // C_CONTROLPIC
|
||||||
|
MenuPicKey.footer: 1, // C_MOUSELBACKPIC
|
||||||
|
MenuPicKey.heading: 0, // C_BACKDROPPIC
|
||||||
|
MenuPicKey.optionsLabel: 13, // C_OPTIONSPIC
|
||||||
|
MenuPicKey.customizeLabel: 6, // C_CUSTOMIZEPIC
|
||||||
|
|
||||||
|
MenuPicKey.cursorActive: 2, // C_CURSOR1PIC
|
||||||
|
MenuPicKey.cursorInactive: 3, // C_CURSOR2PIC
|
||||||
|
MenuPicKey.markerSelected: 5, // C_SELECTEDPIC
|
||||||
|
MenuPicKey.markerUnselected: 4, // C_NOTSELECTEDPIC
|
||||||
|
|
||||||
|
MenuPicKey.difficultyBaby: 18, // C_BABYMODEPIC
|
||||||
|
MenuPicKey.difficultyEasy: 19, // C_EASYPIC
|
||||||
|
MenuPicKey.difficultyNormal: 20, // C_NORMALPIC
|
||||||
|
MenuPicKey.difficultyHard: 21, // C_HARDPIC
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
MenuPicRef? resolve(MenuPicKey key) {
|
||||||
|
final int? index = _indices[key];
|
||||||
|
return index != null ? MenuPicRef(index) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
MenuPicKey episodeKey(int 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/wolf/classic_menu_presentation_module.dart';
|
||||||
|
|
||||||
|
/// Built-in menu presentation for Spear variants.
|
||||||
|
///
|
||||||
|
/// Spear currently reuses the classic control-panel palette and layout rules,
|
||||||
|
/// but keeping it as a distinct concrete type gives Spear-specific releases and
|
||||||
|
/// user mods a stable place to diverge without changing retail/shareware
|
||||||
|
/// defaults.
|
||||||
|
class SpearMenuPresentationModule extends ClassicMenuPresentationModule {
|
||||||
|
/// Creates the default Spear menu presentation.
|
||||||
|
const SpearMenuPresentationModule();
|
||||||
|
|
||||||
|
/// Spear VGA background color (`BKGDCOLOR` in `WL_MENU.H`).
|
||||||
|
@override
|
||||||
|
int get backgroundIndex => 0x9D;
|
||||||
|
|
||||||
|
/// Spear panel fill color (`BORD2COLOR` in `WL_MENU.H`).
|
||||||
|
@override
|
||||||
|
int get panelIndex => 0x93;
|
||||||
|
|
||||||
|
/// Spear panel border color (`BORDCOLOR` in `WL_MENU.H`).
|
||||||
|
@override
|
||||||
|
int get borderIndex => 0x99;
|
||||||
|
|
||||||
|
/// Spear disabled/deactivated text color (`DEACTIVE` in `WL_MENU.H`).
|
||||||
|
@override
|
||||||
|
int get disabledTextIndex => 0x9B;
|
||||||
|
}
|
||||||
+20
-25
@@ -1,4 +1,3 @@
|
|||||||
import 'package:wolf_3d_dart/src/data_types/color_palette.dart';
|
|
||||||
import 'package:wolf_3d_dart/src/data_types/difficulty.dart';
|
import 'package:wolf_3d_dart/src/data_types/difficulty.dart';
|
||||||
import 'package:wolf_3d_dart/src/data_types/image.dart';
|
import 'package:wolf_3d_dart/src/data_types/image.dart';
|
||||||
import 'package:wolf_3d_dart/src/data_types/wolfenstein_data.dart';
|
import 'package:wolf_3d_dart/src/data_types/wolfenstein_data.dart';
|
||||||
@@ -11,49 +10,45 @@ class ClassicMenuPresentationModule extends MenuPresentationModule {
|
|||||||
/// Creates the classic Wolf3D menu presentation.
|
/// Creates the classic Wolf3D menu presentation.
|
||||||
const ClassicMenuPresentationModule();
|
const ClassicMenuPresentationModule();
|
||||||
|
|
||||||
/// Approximate RGB target used to derive the classic menu heading color.
|
/// Classic menu background (`BKGDCOLOR` in `WL_MENU.H`).
|
||||||
static const int _headerTargetRgb = 0xFFF700;
|
|
||||||
|
|
||||||
/// Classic VGA palette index for menu background fills.
|
|
||||||
@override
|
@override
|
||||||
int get backgroundIndex => 111;
|
int get backgroundIndex => 0x2D;
|
||||||
|
|
||||||
/// Classic VGA palette index for panel fills.
|
/// Classic panel fill (`BORD2COLOR` in `WL_MENU.H`).
|
||||||
@override
|
@override
|
||||||
int get panelIndex => 103;
|
int get panelIndex => 0x23;
|
||||||
|
|
||||||
/// Classic VGA palette index for panel borders.
|
/// Classic panel border (`BORDCOLOR` in `WL_MENU.H`).
|
||||||
@override
|
@override
|
||||||
int get borderIndex => 87;
|
int get borderIndex => 0x29;
|
||||||
|
|
||||||
/// Classic VGA palette index for emphasis text.
|
/// Highlight text (`HIGHLIGHT` in `WL_MENU.H`).
|
||||||
@override
|
@override
|
||||||
int get emphasisIndex => 10;
|
int get emphasisIndex => 0x13;
|
||||||
|
|
||||||
/// Classic VGA palette index for warnings.
|
/// Read-screen highlight (`READHCOLOR` in `WL_MENU.H`).
|
||||||
@override
|
@override
|
||||||
int get warningIndex => 14;
|
int get warningIndex => 0x47;
|
||||||
|
|
||||||
/// Classic VGA palette index for muted text.
|
/// Read-screen body text (`READCOLOR` in `WL_MENU.H`).
|
||||||
@override
|
@override
|
||||||
int get mutedIndex => 8;
|
int get mutedIndex => 0x4A;
|
||||||
|
|
||||||
/// Classic VGA palette index for selected row text.
|
/// Selected menu text (`HIGHLIGHT` in `WL_MENU.H`).
|
||||||
@override
|
@override
|
||||||
int get selectedTextIndex => 19;
|
int get selectedTextIndex => 0x13;
|
||||||
|
|
||||||
/// Classic VGA palette index for unselected row text.
|
/// Unselected menu text (`TEXTCOLOR` in `WL_MENU.H`).
|
||||||
@override
|
@override
|
||||||
int get unselectedTextIndex => 23;
|
int get unselectedTextIndex => 0x17;
|
||||||
|
|
||||||
/// Classic VGA palette index for disabled row text.
|
/// Disabled menu text (`DEACTIVE` in `WL_MENU.H`).
|
||||||
@override
|
@override
|
||||||
int get disabledTextIndex => 4;
|
int get disabledTextIndex => 0x2B;
|
||||||
|
|
||||||
/// Classic heading palette index computed from the target yellow tone.
|
/// Header/read highlight (`READHCOLOR` in `WL_MENU.H`).
|
||||||
@override
|
@override
|
||||||
int get headerTextIndex =>
|
int get headerTextIndex => 0x47;
|
||||||
ColorPalette.findClosestPaletteIndex(_headerTargetRgb);
|
|
||||||
|
|
||||||
/// Controls/customize panel background art.
|
/// Controls/customize panel background art.
|
||||||
@override
|
@override
|
||||||
+4
-4
@@ -2,10 +2,10 @@ 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/asset_registry.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/built_in_music_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/built_in_music_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/built_in_sfx_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/built_in_sfx_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/classic_menu_presentation_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/wolf/classic_menu_presentation_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/retail_entity_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/wolf/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/menu/wolf/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/menu/wolf/retail_menu_module.dart';
|
||||||
|
|
||||||
/// The canonical [AssetRegistry] for all retail Wolf3D releases.
|
/// The canonical [AssetRegistry] for all retail Wolf3D releases.
|
||||||
///
|
///
|
||||||
+4
-4
@@ -2,10 +2,10 @@ 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/asset_registry.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/built_in_music_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/built_in_music_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/built_in_sfx_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/built_in_sfx_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/classic_menu_presentation_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/wolf/classic_menu_presentation_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/shareware_entity_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/wolf/shareware_entity_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/shareware_hud_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/wolf/shareware_hud_module.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/shareware_menu_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/wolf/shareware_menu_module.dart';
|
||||||
|
|
||||||
/// The [AssetRegistry] for the Wolfenstein 3D v1.4 Shareware release.
|
/// The [AssetRegistry] for the Wolfenstein 3D v1.4 Shareware release.
|
||||||
///
|
///
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import 'package:wolf_3d_dart/src/registry/built_in/classic_menu_presentation_module.dart';
|
|
||||||
|
|
||||||
/// Built-in menu presentation for Spear variants.
|
|
||||||
///
|
|
||||||
/// Spear currently reuses the classic control-panel palette and layout rules,
|
|
||||||
/// but keeping it as a distinct concrete type gives Spear-specific releases and
|
|
||||||
/// user mods a stable place to diverge without changing retail/shareware
|
|
||||||
/// defaults.
|
|
||||||
class SpearMenuPresentationModule extends ClassicMenuPresentationModule {
|
|
||||||
/// Creates the default Spear menu presentation.
|
|
||||||
const SpearMenuPresentationModule();
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import 'package:wolf_3d_dart/src/data/data_version.dart';
|
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/data_types/game_version.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/asset_registry.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/menu/spear/spear_asset_registry.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/shareware_asset_registry.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_demo_asset_registry.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/spear_demo_asset_registry.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/wolf/retail_asset_registry.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/wolf/shareware_asset_registry.dart';
|
||||||
|
|
||||||
/// The input used by [AssetRegistryResolver] to select or build a registry.
|
/// The input used by [AssetRegistryResolver] to select or build a registry.
|
||||||
class RegistrySelectionContext {
|
class RegistrySelectionContext {
|
||||||
@@ -62,6 +63,7 @@ class BuiltInAssetRegistryResolver implements AssetRegistryResolver {
|
|||||||
case GameVersion.shareware:
|
case GameVersion.shareware:
|
||||||
return SharewareAssetRegistry();
|
return SharewareAssetRegistry();
|
||||||
case GameVersion.spearOfDestiny:
|
case GameVersion.spearOfDestiny:
|
||||||
|
return SpearAssetRegistry();
|
||||||
case GameVersion.spearOfDestinyDemo:
|
case GameVersion.spearOfDestinyDemo:
|
||||||
return SpearDemoAssetRegistry();
|
return SpearDemoAssetRegistry();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:math' as math;
|
|||||||
import 'package:arcane_helper_utils/arcane_helper_utils.dart';
|
import 'package:arcane_helper_utils/arcane_helper_utils.dart';
|
||||||
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
||||||
import 'package:wolf_3d_dart/src/rendering/fizzle_fade.dart';
|
import 'package:wolf_3d_dart/src/rendering/fizzle_fade.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/rendering/menu_header_band.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.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_engine.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||||
@@ -547,13 +548,18 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
);
|
);
|
||||||
final int panelColor = _rgbToPaletteColor(engine.menuPanelRgb);
|
final int panelColor = _rgbToPaletteColor(engine.menuPanelRgb);
|
||||||
final menu = WolfMenuPresentation(engine.data);
|
final menu = WolfMenuPresentation(engine.data);
|
||||||
|
final bool isSpearVariant = MenuHeaderBand.isSpearVariant(
|
||||||
|
engine.data.version,
|
||||||
|
);
|
||||||
final int headingColor = menu.headerTextColor;
|
final int headingColor = menu.headerTextColor;
|
||||||
final int selectedTextColor = menu.selectedTextColor;
|
final int selectedTextColor = menu.selectedTextColor;
|
||||||
final int unselectedTextColor = menu.unselectedTextColor;
|
final int unselectedTextColor = menu.unselectedTextColor;
|
||||||
final int disabledTextColor = menu.disabledTextColor;
|
final int disabledTextColor = menu.disabledTextColor;
|
||||||
final _AsciiMenuTypography menuTypography = _resolveMenuTypography();
|
final _AsciiMenuTypography menuTypography = _resolveMenuTypography();
|
||||||
|
|
||||||
if (_usesTerminalLayout) {
|
if (isSpearVariant && menu.heading != null) {
|
||||||
|
_drawTiledMenuBackdrop(menu.heading!, bgColor);
|
||||||
|
} else if (_usesTerminalLayout) {
|
||||||
_fillTerminalRect(0, 0, width, _terminalPixelHeight, bgColor);
|
_fillTerminalRect(0, 0, width, _terminalPixelHeight, bgColor);
|
||||||
} else {
|
} else {
|
||||||
_fillRect(0, 0, width, height, activeTheme.solid, bgColor);
|
_fillRect(0, 0, width, height, activeTheme.solid, bgColor);
|
||||||
@@ -561,7 +567,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
|
|
||||||
final optionsLabel = menu.optionsLabel;
|
final optionsLabel = menu.optionsLabel;
|
||||||
if (optionsLabel != null) {
|
if (optionsLabel != null) {
|
||||||
_mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
|
_mainMenuBandFirstColumn = MenuHeaderBand.firstColumn(optionsLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (engine.menuManager.activeMenu == WolfMenuScreen.introSplash) {
|
if (engine.menuManager.activeMenu == WolfMenuScreen.introSplash) {
|
||||||
@@ -575,7 +581,11 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
final optionsLabel = menu.optionsLabel;
|
final optionsLabel = menu.optionsLabel;
|
||||||
if (optionsLabel != null) {
|
if (optionsLabel != null) {
|
||||||
final int optionsX = ((320 - optionsLabel.width) ~/ 2).clamp(0, 319);
|
final int optionsX = ((320 - optionsLabel.width) ~/ 2).clamp(0, 319);
|
||||||
_drawMainMenuOptionsSideBars(optionsLabel, optionsX);
|
_drawMainMenuOptionsSideBars(
|
||||||
|
optionsLabel,
|
||||||
|
optionsX,
|
||||||
|
debugContext: 'ascii/mainMenu',
|
||||||
|
);
|
||||||
_blitVgaImageAscii(optionsLabel, optionsX, 0);
|
_blitVgaImageAscii(optionsLabel, optionsX, 0);
|
||||||
} else {
|
} else {
|
||||||
_drawHeaderBarStack(
|
_drawHeaderBarStack(
|
||||||
@@ -630,37 +640,47 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (engine.menuManager.activeMenu == WolfMenuScreen.gameSelect) {
|
if (engine.menuManager.activeMenu == WolfMenuScreen.gameSelect) {
|
||||||
|
final optionsLabel = menu.optionsLabel;
|
||||||
|
if (optionsLabel != null) {
|
||||||
|
final int optionsX = ((320 - optionsLabel.width) ~/ 2).clamp(0, 319);
|
||||||
|
_drawMainMenuOptionsSideBars(
|
||||||
|
optionsLabel,
|
||||||
|
optionsX,
|
||||||
|
debugContext: 'ascii/gameSelect',
|
||||||
|
);
|
||||||
|
_blitVgaImageAscii(optionsLabel, optionsX, 0);
|
||||||
|
} else {
|
||||||
_drawHeaderBarStack(
|
_drawHeaderBarStack(
|
||||||
headingY200: _headerHeadingY,
|
headingY200: _headerHeadingY,
|
||||||
backgroundColor: bgColor,
|
backgroundColor: bgColor,
|
||||||
barColor: ColorPalette.vga32Bit[0],
|
barColor: ColorPalette.vga32Bit[0],
|
||||||
);
|
);
|
||||||
_fillRect320(28, 58, 264, 104, panelColor);
|
|
||||||
|
|
||||||
final cursor = menu.mappedPic(
|
|
||||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
|
||||||
);
|
|
||||||
const int rowYStart = 84;
|
|
||||||
const int rowStep = 18;
|
|
||||||
final List<String> rows = engine.availableGames
|
|
||||||
.map((game) => _gameTitle(game.version))
|
|
||||||
.toList(growable: false);
|
|
||||||
_drawMenuTextCentered(
|
_drawMenuTextCentered(
|
||||||
'SELECT GAME',
|
'SELECT GAME',
|
||||||
_headerHeadingY,
|
_headerHeadingY,
|
||||||
headingColor,
|
headingColor,
|
||||||
scale: menuTypography.headingScale,
|
scale: menuTypography.headingScale,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
_fillRect320(68, 52, 178, 136, panelColor);
|
||||||
|
|
||||||
|
final cursor = menu.mappedPic(
|
||||||
|
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||||
|
);
|
||||||
|
const int rowYStart = 55;
|
||||||
|
const int rowStep = 13;
|
||||||
|
final List<String> rows = engine.availableGames
|
||||||
|
.map((game) => MenuHeaderBand.gameTitle(game.version))
|
||||||
|
.toList(growable: false);
|
||||||
_drawSelectableMenuRows(
|
_drawSelectableMenuRows(
|
||||||
typography: menuTypography,
|
typography: menuTypography,
|
||||||
rows: rows,
|
rows: rows,
|
||||||
selectedIndex: engine.menuManager.selectedGameIndex,
|
selectedIndex: engine.menuManager.selectedGameIndex,
|
||||||
rowYStart200: rowYStart,
|
rowYStart200: rowYStart,
|
||||||
rowStep200: rowStep,
|
rowStep200: rowStep,
|
||||||
textX320: 70,
|
textX320: 100,
|
||||||
panelX320: 28,
|
panelX320: 68,
|
||||||
panelW320: 264,
|
panelW320: 178,
|
||||||
colorForRow: (int _, bool isSelected) {
|
colorForRow: (int _, bool isSelected) {
|
||||||
return isSelected ? selectedTextColor : unselectedTextColor;
|
return isSelected ? selectedTextColor : unselectedTextColor;
|
||||||
},
|
},
|
||||||
@@ -669,7 +689,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
if (cursor != null) {
|
if (cursor != null) {
|
||||||
_blitVgaImageAscii(
|
_blitVgaImageAscii(
|
||||||
cursor,
|
cursor,
|
||||||
38,
|
72,
|
||||||
(rowYStart + (engine.menuManager.selectedGameIndex * rowStep)) - 2,
|
(rowYStart + (engine.menuManager.selectedGameIndex * rowStep)) - 2,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -780,7 +800,11 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (engine.menuManager.activeMenu == WolfMenuScreen.changeView) {
|
if (engine.menuManager.activeMenu == WolfMenuScreen.changeView) {
|
||||||
_drawCustomizeMenuHeader(menu, headingColor, bgColor);
|
_drawCustomizeMenuHeader(
|
||||||
|
menu,
|
||||||
|
headingColor,
|
||||||
|
bgColor,
|
||||||
|
);
|
||||||
final cursor = menu.mappedPic(
|
final cursor = menu.mappedPic(
|
||||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||||
);
|
);
|
||||||
@@ -884,7 +908,11 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (engine.menuManager.activeMenu == WolfMenuScreen.rendererOptions) {
|
if (engine.menuManager.activeMenu == WolfMenuScreen.rendererOptions) {
|
||||||
_drawCustomizeMenuHeader(menu, headingColor, bgColor);
|
_drawCustomizeMenuHeader(
|
||||||
|
menu,
|
||||||
|
headingColor,
|
||||||
|
bgColor,
|
||||||
|
);
|
||||||
_fillRect320(56, 52, 208, 120, panelColor);
|
_fillRect320(56, 52, 208, 120, panelColor);
|
||||||
_drawMenuTextCentered(
|
_drawMenuTextCentered(
|
||||||
engine.menuManager.rendererOptionsTitle,
|
engine.menuManager.rendererOptionsTitle,
|
||||||
@@ -1018,19 +1046,6 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
_applyMenuTransition(engine.menuManager, bgColor);
|
_applyMenuTransition(engine.menuManager, bgColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _gameTitle(GameVersion version) {
|
|
||||||
switch (version) {
|
|
||||||
case GameVersion.shareware:
|
|
||||||
return 'SHAREWARE';
|
|
||||||
case GameVersion.retail:
|
|
||||||
return 'RETAIL';
|
|
||||||
case GameVersion.spearOfDestiny:
|
|
||||||
return 'SPEAR OF DESTINY';
|
|
||||||
case GameVersion.spearOfDestinyDemo:
|
|
||||||
return 'SOD DEMO';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _drawIntroSplash(
|
void _drawIntroSplash(
|
||||||
WolfEngine engine,
|
WolfEngine engine,
|
||||||
WolfMenuPresentation menu,
|
WolfMenuPresentation menu,
|
||||||
@@ -1172,17 +1187,38 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawMainMenuOptionsSideBars(VgaImage optionsLabel, int optionsX320) {
|
void _drawMainMenuOptionsSideBars(
|
||||||
_mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
|
VgaImage optionsLabel,
|
||||||
_drawScaledColumnBand(_mainMenuBandFirstColumn!);
|
int optionsX320, {
|
||||||
|
required String debugContext,
|
||||||
|
}) {
|
||||||
|
_mainMenuBandFirstColumn = MenuHeaderBand.firstColumn(optionsLabel);
|
||||||
|
MenuHeaderBand.applyFromHeadingImage(
|
||||||
|
image: optionsLabel,
|
||||||
|
imageX320: optionsX320,
|
||||||
|
debugContext: debugContext,
|
||||||
|
fillSideEdgesRow:
|
||||||
|
(
|
||||||
|
int y200,
|
||||||
|
int leftWidth320,
|
||||||
|
int rightStartX320,
|
||||||
|
int paletteIndex,
|
||||||
|
) {
|
||||||
|
final int color = ColorPalette.vga32Bit[paletteIndex];
|
||||||
|
if (leftWidth320 > 0) {
|
||||||
|
_fillRect320(0, y200, leftWidth320, 1, color);
|
||||||
}
|
}
|
||||||
|
if (rightStartX320 < 320) {
|
||||||
List<int> _cacheFirstColumn(VgaImage image) {
|
_fillRect320(
|
||||||
final List<int> column = List<int>.filled(image.height, 0);
|
rightStartX320,
|
||||||
for (int y = 0; y < image.height; y++) {
|
y200,
|
||||||
column[y] = image.decodePixel(0, y);
|
320 - rightStartX320,
|
||||||
|
1,
|
||||||
|
color,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return column;
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawScaledColumnBand(List<int> column) {
|
void _drawScaledColumnBand(List<int> column) {
|
||||||
@@ -1190,6 +1226,18 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int firstBlack = -1;
|
||||||
|
int lastBlack = -1;
|
||||||
|
for (int y = 0; y < column.length; y++) {
|
||||||
|
if (column[y] == 0) {
|
||||||
|
firstBlack = firstBlack == -1 ? y : firstBlack;
|
||||||
|
lastBlack = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (firstBlack == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final int maxDrawHeight = _usesTerminalLayout
|
final int maxDrawHeight = _usesTerminalLayout
|
||||||
? _terminalPixelHeight
|
? _terminalPixelHeight
|
||||||
: height;
|
: height;
|
||||||
@@ -1198,6 +1246,9 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
|
|
||||||
for (int dy = 0; dy < destHeight; dy++) {
|
for (int dy = 0; dy < destHeight; dy++) {
|
||||||
final int srcY = (dy / scaleY).toInt().clamp(0, column.length - 1);
|
final int srcY = (dy / scaleY).toInt().clamp(0, column.length - 1);
|
||||||
|
if (srcY < firstBlack || srcY > lastBlack) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
final int fillColor = ColorPalette.vga32Bit[column[srcY]];
|
final int fillColor = ColorPalette.vga32Bit[column[srcY]];
|
||||||
|
|
||||||
if (_usesTerminalLayout) {
|
if (_usesTerminalLayout) {
|
||||||
@@ -1606,6 +1657,11 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
final VgaImage? heading = menu.customizeLabel ?? menu.optionsLabel;
|
final VgaImage? heading = menu.customizeLabel ?? menu.optionsLabel;
|
||||||
if (heading != null) {
|
if (heading != null) {
|
||||||
final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
|
final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
|
||||||
|
_drawMainMenuOptionsSideBars(
|
||||||
|
heading,
|
||||||
|
headingX,
|
||||||
|
debugContext: 'ascii/customizeHeader',
|
||||||
|
);
|
||||||
_blitVgaImageAscii(heading, headingX, 0);
|
_blitVgaImageAscii(heading, headingX, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2516,4 +2572,20 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
int _rgbToPaletteColor(int rgb) {
|
int _rgbToPaletteColor(int rgb) {
|
||||||
return ColorPalette.vga32Bit[ColorPalette.findClosestPaletteIndex(rgb)];
|
return ColorPalette.vga32Bit[ColorPalette.findClosestPaletteIndex(rgb)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _drawTiledMenuBackdrop(VgaImage image, int fallbackColor) {
|
||||||
|
if (image.width <= 0 || image.height <= 0) {
|
||||||
|
if (_usesTerminalLayout) {
|
||||||
|
_fillTerminalRect(0, 0, width, _terminalPixelHeight, fallbackColor);
|
||||||
|
} else {
|
||||||
|
_fillRect(0, 0, width, height, activeTheme.solid, fallbackColor);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int y = 0; y < 200; y += image.height) {
|
||||||
|
for (int x = 0; x < 320; x += image.width) {
|
||||||
|
_blitVgaImageAscii(image, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
|
abstract class MenuHeaderBand {
|
||||||
|
static void Function(String message)? debugLogger;
|
||||||
|
|
||||||
|
static bool isSpearVariant(GameVersion version) {
|
||||||
|
switch (version) {
|
||||||
|
case GameVersion.spearOfDestiny:
|
||||||
|
case GameVersion.spearOfDestinyDemo:
|
||||||
|
return true;
|
||||||
|
case GameVersion.shareware:
|
||||||
|
case GameVersion.retail:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<int> firstColumn(VgaImage image) {
|
||||||
|
final List<int> column = List<int>.filled(image.height, 0);
|
||||||
|
final int sampleX = image.width > 1 ? 1 : 0;
|
||||||
|
for (int y = 0; y < image.height; y++) {
|
||||||
|
column[y] = image.decodePixel(sampleX, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int effectiveRows = _effectiveBandRowCount(column);
|
||||||
|
if (effectiveRows == column.length) {
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
return column.sublist(0, effectiveRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String gameTitle(GameVersion version) {
|
||||||
|
switch (version) {
|
||||||
|
case GameVersion.shareware:
|
||||||
|
return 'SHAREWARE';
|
||||||
|
case GameVersion.retail:
|
||||||
|
return 'RETAIL';
|
||||||
|
case GameVersion.spearOfDestiny:
|
||||||
|
return 'SPEAR OF DESTINY';
|
||||||
|
case GameVersion.spearOfDestinyDemo:
|
||||||
|
return 'SOD DEMO';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void applyFromHeadingImage({
|
||||||
|
required VgaImage image,
|
||||||
|
required int imageX320,
|
||||||
|
required void Function(
|
||||||
|
int y200,
|
||||||
|
int leftWidth320,
|
||||||
|
int rightStartX320,
|
||||||
|
int paletteIndex,
|
||||||
|
)
|
||||||
|
fillSideEdgesRow,
|
||||||
|
int maxRows = 200,
|
||||||
|
String? debugContext,
|
||||||
|
}) {
|
||||||
|
if (image.width <= 0 || image.height <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int sampledRows = image.height < maxRows ? image.height : maxRows;
|
||||||
|
if (sampledRows <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int sampleX = image.width > 1 ? 1 : 0;
|
||||||
|
final List<int> sourceByRow = List<int>.filled(sampledRows, 0);
|
||||||
|
final List<bool> isBlackByRow = List<bool>.filled(sampledRows, false);
|
||||||
|
for (int y = 0; y < sampledRows; y++) {
|
||||||
|
final int sourceIndex = image.decodePixel(sampleX, y);
|
||||||
|
sourceByRow[y] = sourceIndex;
|
||||||
|
isBlackByRow[y] = sourceIndex == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int rowCount = _effectiveBandRowCount(sourceByRow);
|
||||||
|
if (rowCount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int leftWidth320 = imageX320.clamp(0, 320);
|
||||||
|
final int rightStartX320 = (imageX320 + image.width).clamp(0, 320);
|
||||||
|
|
||||||
|
// Extend rows that fall inside the span bounded by the first and last
|
||||||
|
// black row. Leading non-black rows (coloured image border at the top)
|
||||||
|
// and trailing non-black rows let the background show through. Interior
|
||||||
|
// non-black rows (e.g. a decorative stripe sandwiched between two black
|
||||||
|
// sections) are extended with their actual palette colour.
|
||||||
|
int firstBlack = -1;
|
||||||
|
int lastBlack = -1;
|
||||||
|
for (int y = 0; y < rowCount; y++) {
|
||||||
|
if (sourceByRow[y] == 0) {
|
||||||
|
firstBlack = firstBlack == -1 ? y : firstBlack;
|
||||||
|
lastBlack = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (firstBlack == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int y = firstBlack; y <= lastBlack; y++) {
|
||||||
|
fillSideEdgesRow(y, leftWidth320, rightStartX320, sourceByRow[y]);
|
||||||
|
}
|
||||||
|
|
||||||
|
final debug = debugLogger;
|
||||||
|
if (debug != null) {
|
||||||
|
final String label = debugContext ?? 'header-band';
|
||||||
|
final String runsText = _summarizeRunsByBlackState(isBlackByRow);
|
||||||
|
debug(
|
||||||
|
'$label rows=$rowCount left=$leftWidth320 right=$rightStartX320 runs=$runsText',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _summarizeRunsByBlackState(List<bool> isBlackByRow) {
|
||||||
|
if (isBlackByRow.isEmpty) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
int runStart = 0;
|
||||||
|
bool runIsBlack = isBlackByRow[0];
|
||||||
|
final List<String> chunks = <String>[];
|
||||||
|
|
||||||
|
for (int y = 1; y < isBlackByRow.length; y++) {
|
||||||
|
if (isBlackByRow[y] == runIsBlack) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
chunks.add(
|
||||||
|
'[$runStart-${y - 1}:${runIsBlack ? 'black' : 'non-black'}]',
|
||||||
|
);
|
||||||
|
runStart = y;
|
||||||
|
runIsBlack = isBlackByRow[y];
|
||||||
|
}
|
||||||
|
chunks.add(
|
||||||
|
'[$runStart-${isBlackByRow.length - 1}:${runIsBlack ? 'black' : 'non-black'}]',
|
||||||
|
);
|
||||||
|
return chunks.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
static int _effectiveBandRowCount(List<int> sampledByRow) {
|
||||||
|
if (sampledByRow.isEmpty) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int lastBlack = -1;
|
||||||
|
for (int y = sampledByRow.length - 1; y >= 0; y--) {
|
||||||
|
if (sampledByRow[y] == 0) {
|
||||||
|
lastBlack = y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastBlack == -1) {
|
||||||
|
return sampledByRow.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int trailingRows = sampledByRow.length - (lastBlack + 1);
|
||||||
|
const int maxTrailingRows = 3;
|
||||||
|
final int keptTrailingRows = trailingRows > maxTrailingRows
|
||||||
|
? maxTrailingRows
|
||||||
|
: trailingRows;
|
||||||
|
return lastBlack + 1 + keptTrailingRows;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import 'dart:typed_data';
|
|||||||
import 'package:wolf_3d_dart/src/input/cli_input.dart';
|
import 'package:wolf_3d_dart/src/input/cli_input.dart';
|
||||||
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
||||||
import 'package:wolf_3d_dart/src/rendering/fizzle_fade.dart';
|
import 'package:wolf_3d_dart/src/rendering/fizzle_fade.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/rendering/menu_header_band.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.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_engine.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||||
@@ -477,18 +478,25 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
);
|
);
|
||||||
final int panelColor = _rgbToPaletteIndex(engine.menuPanelRgb);
|
final int panelColor = _rgbToPaletteIndex(engine.menuPanelRgb);
|
||||||
final menu = WolfMenuPresentation(engine.data);
|
final menu = WolfMenuPresentation(engine.data);
|
||||||
|
final bool isSpearVariant = MenuHeaderBand.isSpearVariant(
|
||||||
|
engine.data.version,
|
||||||
|
);
|
||||||
final int headingIndex = menu.headerTextIndex;
|
final int headingIndex = menu.headerTextIndex;
|
||||||
final int selectedTextIndex = menu.selectedTextIndex;
|
final int selectedTextIndex = menu.selectedTextIndex;
|
||||||
final int unselectedTextIndex = menu.unselectedTextIndex;
|
final int unselectedTextIndex = menu.unselectedTextIndex;
|
||||||
final int disabledTextIndex = menu.disabledTextIndex;
|
final int disabledTextIndex = menu.disabledTextIndex;
|
||||||
|
|
||||||
|
if (isSpearVariant && menu.heading != null) {
|
||||||
|
_drawTiledMenuBackdrop(menu.heading!, bgColor);
|
||||||
|
} else {
|
||||||
for (int i = 0; i < _screen.length; i++) {
|
for (int i = 0; i < _screen.length; i++) {
|
||||||
_screen[i] = bgColor;
|
_screen[i] = bgColor;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final optionsLabel = menu.optionsLabel;
|
final optionsLabel = menu.optionsLabel;
|
||||||
if (optionsLabel != null) {
|
if (optionsLabel != null) {
|
||||||
_mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
|
_mainMenuBandFirstColumn = MenuHeaderBand.firstColumn(optionsLabel);
|
||||||
}
|
}
|
||||||
// Draw footer first so menu panels can clip overlap in the center.
|
// Draw footer first so menu panels can clip overlap in the center.
|
||||||
_drawMenuFooterArt(menu);
|
_drawMenuFooterArt(menu);
|
||||||
@@ -504,7 +512,11 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
final optionsLabel = menu.optionsLabel;
|
final optionsLabel = menu.optionsLabel;
|
||||||
if (optionsLabel != null) {
|
if (optionsLabel != null) {
|
||||||
final int optionsX = ((320 - optionsLabel.width) ~/ 2).clamp(0, 319);
|
final int optionsX = ((320 - optionsLabel.width) ~/ 2).clamp(0, 319);
|
||||||
_drawMainMenuOptionsSideBars(optionsLabel, optionsX);
|
_drawMainMenuOptionsSideBars(
|
||||||
|
optionsLabel,
|
||||||
|
optionsX,
|
||||||
|
debugContext: 'sixel/mainMenu',
|
||||||
|
);
|
||||||
_blitVgaImage(optionsLabel, optionsX, 0);
|
_blitVgaImage(optionsLabel, optionsX, 0);
|
||||||
} else {
|
} else {
|
||||||
_drawHeaderBarStack(
|
_drawHeaderBarStack(
|
||||||
@@ -546,31 +558,43 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (engine.menuManager.activeMenu == WolfMenuScreen.gameSelect) {
|
if (engine.menuManager.activeMenu == WolfMenuScreen.gameSelect) {
|
||||||
|
final optionsLabel = menu.optionsLabel;
|
||||||
|
if (optionsLabel != null) {
|
||||||
|
final int optionsX = ((320 - optionsLabel.width) ~/ 2).clamp(0, 319);
|
||||||
|
_drawMainMenuOptionsSideBars(
|
||||||
|
optionsLabel,
|
||||||
|
optionsX,
|
||||||
|
debugContext: 'sixel/gameSelect',
|
||||||
|
);
|
||||||
|
_blitVgaImage(optionsLabel, optionsX, 0);
|
||||||
|
} else {
|
||||||
_drawHeaderBarStack(
|
_drawHeaderBarStack(
|
||||||
headingY200: _headerHeadingY,
|
headingY200: _headerHeadingY,
|
||||||
backgroundColor: bgColor,
|
backgroundColor: bgColor,
|
||||||
barColor: 0,
|
barColor: 0,
|
||||||
);
|
);
|
||||||
_fillRect320(28, 58, 264, 104, panelColor);
|
|
||||||
_drawMenuTextCentered(
|
_drawMenuTextCentered(
|
||||||
'SELECT GAME',
|
'SELECT GAME',
|
||||||
_headerHeadingY,
|
_headerHeadingY,
|
||||||
headingIndex,
|
headingIndex,
|
||||||
scale: 2,
|
scale: 2,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
_fillRect320(68, 52, 178, 136, panelColor);
|
||||||
|
|
||||||
final cursor = menu.mappedPic(
|
final cursor = menu.mappedPic(
|
||||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||||
);
|
);
|
||||||
const int rowYStart = 84;
|
const int rowYStart = 55;
|
||||||
const int rowStep = 18;
|
const int rowStep = 13;
|
||||||
for (int i = 0; i < engine.availableGames.length; i++) {
|
for (int i = 0; i < engine.availableGames.length; i++) {
|
||||||
final bool isSelected = i == engine.menuManager.selectedGameIndex;
|
final bool isSelected = i == engine.menuManager.selectedGameIndex;
|
||||||
if (isSelected && cursor != null) {
|
if (isSelected && cursor != null) {
|
||||||
_blitVgaImage(cursor, 38, (rowYStart + (i * rowStep)) - 2);
|
_blitVgaImage(cursor, 72, (rowYStart + (i * rowStep)) - 2);
|
||||||
}
|
}
|
||||||
_drawMenuText(
|
_drawMenuText(
|
||||||
_gameTitle(engine.availableGames[i].version),
|
MenuHeaderBand.gameTitle(engine.availableGames[i].version),
|
||||||
70,
|
100,
|
||||||
rowYStart + (i * rowStep),
|
rowYStart + (i * rowStep),
|
||||||
isSelected ? selectedTextIndex : unselectedTextIndex,
|
isSelected ? selectedTextIndex : unselectedTextIndex,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
@@ -633,7 +657,11 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (engine.menuManager.activeMenu == WolfMenuScreen.changeView) {
|
if (engine.menuManager.activeMenu == WolfMenuScreen.changeView) {
|
||||||
_drawCustomizeMenuHeader(menu, headingIndex, bgColor);
|
_drawCustomizeMenuHeader(
|
||||||
|
menu,
|
||||||
|
headingIndex,
|
||||||
|
bgColor,
|
||||||
|
);
|
||||||
final cursor = menu.mappedPic(
|
final cursor = menu.mappedPic(
|
||||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||||
);
|
);
|
||||||
@@ -726,7 +754,11 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (engine.menuManager.activeMenu == WolfMenuScreen.rendererOptions) {
|
if (engine.menuManager.activeMenu == WolfMenuScreen.rendererOptions) {
|
||||||
_drawCustomizeMenuHeader(menu, headingIndex, bgColor);
|
_drawCustomizeMenuHeader(
|
||||||
|
menu,
|
||||||
|
headingIndex,
|
||||||
|
bgColor,
|
||||||
|
);
|
||||||
_fillRect320(56, 52, 208, 120, panelColor);
|
_fillRect320(56, 52, 208, 120, panelColor);
|
||||||
_drawMenuTextCentered(
|
_drawMenuTextCentered(
|
||||||
engine.menuManager.rendererOptionsTitle,
|
engine.menuManager.rendererOptionsTitle,
|
||||||
@@ -845,6 +877,11 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
final VgaImage? heading = menu.customizeLabel ?? menu.optionsLabel;
|
final VgaImage? heading = menu.customizeLabel ?? menu.optionsLabel;
|
||||||
if (heading != null) {
|
if (heading != null) {
|
||||||
final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
|
final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
|
||||||
|
_drawMainMenuOptionsSideBars(
|
||||||
|
heading,
|
||||||
|
headingX,
|
||||||
|
debugContext: 'sixel/customizeHeader',
|
||||||
|
);
|
||||||
_blitVgaImage(heading, headingX, 0);
|
_blitVgaImage(heading, headingX, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -882,19 +919,6 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _gameTitle(GameVersion version) {
|
|
||||||
switch (version) {
|
|
||||||
case GameVersion.shareware:
|
|
||||||
return 'SHAREWARE';
|
|
||||||
case GameVersion.retail:
|
|
||||||
return 'RETAIL';
|
|
||||||
case GameVersion.spearOfDestiny:
|
|
||||||
return 'SPEAR OF DESTINY';
|
|
||||||
case GameVersion.spearOfDestinyDemo:
|
|
||||||
return 'SOD DEMO';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _drawIntroSplash(WolfEngine engine, WolfMenuPresentation menu) {
|
void _drawIntroSplash(WolfEngine engine, WolfMenuPresentation menu) {
|
||||||
final image = switch (engine.menuManager.currentIntroSlide) {
|
final image = switch (engine.menuManager.currentIntroSlide) {
|
||||||
WolfIntroSlide.retailWarning => null,
|
WolfIntroSlide.retailWarning => null,
|
||||||
@@ -1110,17 +1134,37 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
_fillRect320(0, mainBarTop, 320, 22, barColor);
|
_fillRect320(0, mainBarTop, 320, 22, barColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawMainMenuOptionsSideBars(VgaImage optionsLabel, int optionsX320) {
|
void _drawMainMenuOptionsSideBars(
|
||||||
_mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
|
VgaImage optionsLabel,
|
||||||
_drawScaledColumnBand(_mainMenuBandFirstColumn!);
|
int optionsX320, {
|
||||||
|
required String debugContext,
|
||||||
|
}) {
|
||||||
|
_mainMenuBandFirstColumn = MenuHeaderBand.firstColumn(optionsLabel);
|
||||||
|
MenuHeaderBand.applyFromHeadingImage(
|
||||||
|
image: optionsLabel,
|
||||||
|
imageX320: optionsX320,
|
||||||
|
debugContext: debugContext,
|
||||||
|
fillSideEdgesRow:
|
||||||
|
(
|
||||||
|
int y200,
|
||||||
|
int leftWidth320,
|
||||||
|
int rightStartX320,
|
||||||
|
int paletteIndex,
|
||||||
|
) {
|
||||||
|
if (leftWidth320 > 0) {
|
||||||
|
_fillRect320(0, y200, leftWidth320, 1, paletteIndex);
|
||||||
}
|
}
|
||||||
|
if (rightStartX320 < 320) {
|
||||||
List<int> _cacheFirstColumn(VgaImage image) {
|
_fillRect320(
|
||||||
final List<int> column = List<int>.filled(image.height, 0);
|
rightStartX320,
|
||||||
for (int y = 0; y < image.height; y++) {
|
y200,
|
||||||
column[y] = image.decodePixel(0, y);
|
320 - rightStartX320,
|
||||||
|
1,
|
||||||
|
paletteIndex,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return column;
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawScaledColumnBand(List<int> column) {
|
void _drawScaledColumnBand(List<int> column) {
|
||||||
@@ -1128,6 +1172,18 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int firstBlack = -1;
|
||||||
|
int lastBlack = -1;
|
||||||
|
for (int y = 0; y < column.length; y++) {
|
||||||
|
if (column[y] == 0) {
|
||||||
|
firstBlack = firstBlack == -1 ? y : firstBlack;
|
||||||
|
lastBlack = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (firstBlack == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final double scaleY = height / 200.0;
|
final double scaleY = height / 200.0;
|
||||||
final int destHeight = math.max(1, (column.length * scaleY).toInt());
|
final int destHeight = math.max(1, (column.length * scaleY).toInt());
|
||||||
|
|
||||||
@@ -1141,8 +1197,10 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
0,
|
0,
|
||||||
column.length - 1,
|
column.length - 1,
|
||||||
);
|
);
|
||||||
final int paletteIndex = column[srcY];
|
if (srcY < firstBlack || srcY > lastBlack) {
|
||||||
final int fillIndex = paletteIndex == 0 ? 0 : paletteIndex;
|
continue;
|
||||||
|
}
|
||||||
|
final int fillIndex = column[srcY];
|
||||||
final int rowStart = drawY * width;
|
final int rowStart = drawY * width;
|
||||||
for (int drawX = 0; drawX < width; drawX++) {
|
for (int drawX = 0; drawX < width; drawX++) {
|
||||||
_screen[rowStart + drawX] = fillIndex;
|
_screen[rowStart + drawX] = fillIndex;
|
||||||
@@ -1544,4 +1602,18 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
int _rgbToPaletteIndex(int rgb) {
|
int _rgbToPaletteIndex(int rgb) {
|
||||||
return ColorPalette.findClosestPaletteIndex(rgb);
|
return ColorPalette.findClosestPaletteIndex(rgb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _drawTiledMenuBackdrop(VgaImage image, int fallbackColor) {
|
||||||
|
if (image.width <= 0 || image.height <= 0) {
|
||||||
|
for (int i = 0; i < _screen.length; i++) {
|
||||||
|
_screen[i] = fallbackColor;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int y = 0; y < 200; y += image.height) {
|
||||||
|
for (int x = 0; x < 320; x += image.width) {
|
||||||
|
_blitVgaImage(image, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:typed_data';
|
|||||||
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
||||||
import 'package:wolf_3d_dart/src/rendering/fizzle_fade.dart';
|
import 'package:wolf_3d_dart/src/rendering/fizzle_fade.dart';
|
||||||
import 'package:wolf_3d_dart/src/rendering/menu_font.dart';
|
import 'package:wolf_3d_dart/src/rendering/menu_font.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/rendering/menu_header_band.dart';
|
||||||
import 'package:wolf_3d_dart/src/rendering/renderer_backend.dart';
|
import 'package:wolf_3d_dart/src/rendering/renderer_backend.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.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_engine.dart';
|
||||||
@@ -255,18 +256,25 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
final int bgColor = _rgbToFrameColor(engine.menuManager.menuBackgroundRgb);
|
final int bgColor = _rgbToFrameColor(engine.menuManager.menuBackgroundRgb);
|
||||||
final int panelColor = _rgbToFrameColor(engine.menuPanelRgb);
|
final int panelColor = _rgbToFrameColor(engine.menuPanelRgb);
|
||||||
final menu = WolfMenuPresentation(engine.data);
|
final menu = WolfMenuPresentation(engine.data);
|
||||||
|
final bool isSpearVariant = MenuHeaderBand.isSpearVariant(
|
||||||
|
engine.data.version,
|
||||||
|
);
|
||||||
final int headingColor = menu.headerTextColor;
|
final int headingColor = menu.headerTextColor;
|
||||||
final int selectedTextColor = menu.selectedTextColor;
|
final int selectedTextColor = menu.selectedTextColor;
|
||||||
final int unselectedTextColor = menu.unselectedTextColor;
|
final int unselectedTextColor = menu.unselectedTextColor;
|
||||||
final int disabledTextColor = menu.disabledTextColor;
|
final int disabledTextColor = menu.disabledTextColor;
|
||||||
|
|
||||||
|
if (isSpearVariant && menu.heading != null) {
|
||||||
|
_drawTiledMenuBackdrop(menu.heading!, bgColor);
|
||||||
|
} else {
|
||||||
for (int i = 0; i < _buffer.pixels.length; i++) {
|
for (int i = 0; i < _buffer.pixels.length; i++) {
|
||||||
_buffer.pixels[i] = bgColor;
|
_buffer.pixels[i] = bgColor;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final optionsLabel = menu.optionsLabel;
|
final optionsLabel = menu.optionsLabel;
|
||||||
if (optionsLabel != null) {
|
if (optionsLabel != null) {
|
||||||
_mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
|
_mainMenuBandFirstColumn = MenuHeaderBand.firstColumn(optionsLabel);
|
||||||
}
|
}
|
||||||
// Draw footer first so menu panels can clip overlap in the center.
|
// Draw footer first so menu panels can clip overlap in the center.
|
||||||
_drawCenteredMenuFooter(menu);
|
_drawCenteredMenuFooter(menu);
|
||||||
@@ -494,7 +502,11 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
final optionsLabel = menu.optionsLabel;
|
final optionsLabel = menu.optionsLabel;
|
||||||
if (optionsLabel != null) {
|
if (optionsLabel != null) {
|
||||||
final int optionsX = ((320 - optionsLabel.width) ~/ 2).clamp(0, 319);
|
final int optionsX = ((320 - optionsLabel.width) ~/ 2).clamp(0, 319);
|
||||||
_drawMainMenuOptionsSideBars(optionsLabel, optionsX);
|
_drawMainMenuOptionsSideBars(
|
||||||
|
optionsLabel,
|
||||||
|
optionsX,
|
||||||
|
debugContext: 'software/mainMenu',
|
||||||
|
);
|
||||||
_blitVgaImage(optionsLabel, optionsX, 0);
|
_blitVgaImage(optionsLabel, optionsX, 0);
|
||||||
} else {
|
} else {
|
||||||
_drawHeaderBarStack(
|
_drawHeaderBarStack(
|
||||||
@@ -560,6 +572,11 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
final VgaImage? heading = menu.customizeLabel ?? menu.optionsLabel;
|
final VgaImage? heading = menu.customizeLabel ?? menu.optionsLabel;
|
||||||
if (heading != null) {
|
if (heading != null) {
|
||||||
final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
|
final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
|
||||||
|
_drawMainMenuOptionsSideBars(
|
||||||
|
heading,
|
||||||
|
headingX,
|
||||||
|
debugContext: 'software/changeView',
|
||||||
|
);
|
||||||
_blitVgaImage(heading, headingX, 0);
|
_blitVgaImage(heading, headingX, 0);
|
||||||
} else {
|
} else {
|
||||||
_drawCanonicalMenuTextCentered(
|
_drawCanonicalMenuTextCentered(
|
||||||
@@ -706,6 +723,11 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
final VgaImage? heading = menu.customizeLabel ?? menu.optionsLabel;
|
final VgaImage? heading = menu.customizeLabel ?? menu.optionsLabel;
|
||||||
if (heading != null) {
|
if (heading != null) {
|
||||||
final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
|
final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
|
||||||
|
_drawMainMenuOptionsSideBars(
|
||||||
|
heading,
|
||||||
|
headingX,
|
||||||
|
debugContext: 'software/rendererOptions',
|
||||||
|
);
|
||||||
_blitVgaImage(heading, headingX, 0);
|
_blitVgaImage(heading, headingX, 0);
|
||||||
} else {
|
} else {
|
||||||
_drawCanonicalMenuTextCentered(
|
_drawCanonicalMenuTextCentered(
|
||||||
@@ -772,42 +794,52 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
int selectedTextColor,
|
int selectedTextColor,
|
||||||
int unselectedTextColor,
|
int unselectedTextColor,
|
||||||
) {
|
) {
|
||||||
|
final optionsLabel = menu.optionsLabel;
|
||||||
|
if (optionsLabel != null) {
|
||||||
|
final int optionsX = ((320 - optionsLabel.width) ~/ 2).clamp(0, 319);
|
||||||
|
_drawMainMenuOptionsSideBars(
|
||||||
|
optionsLabel,
|
||||||
|
optionsX,
|
||||||
|
debugContext: 'software/gameSelect',
|
||||||
|
);
|
||||||
|
_blitVgaImage(optionsLabel, optionsX, 0);
|
||||||
|
} else {
|
||||||
_drawHeaderBarStack(
|
_drawHeaderBarStack(
|
||||||
headingY200: _headerHeadingY,
|
headingY200: _headerHeadingY,
|
||||||
backgroundColor: _rgbToFrameColor(engine.menuManager.menuBackgroundRgb),
|
backgroundColor: _rgbToFrameColor(engine.menuManager.menuBackgroundRgb),
|
||||||
barColor: ColorPalette.vga32Bit[0],
|
barColor: ColorPalette.vga32Bit[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
const int panelX = 28;
|
|
||||||
const int panelY = 58;
|
|
||||||
const int panelW = 264;
|
|
||||||
const int panelH = 104;
|
|
||||||
_fillCanonicalRect(panelX, panelY, panelW, panelH, panelColor);
|
|
||||||
|
|
||||||
_drawCanonicalMenuTextCentered(
|
_drawCanonicalMenuTextCentered(
|
||||||
'SELECT GAME',
|
'SELECT GAME',
|
||||||
_headerHeadingY,
|
_headerHeadingY,
|
||||||
headingColor,
|
headingColor,
|
||||||
scale: 2,
|
scale: 2,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int panelX = 68;
|
||||||
|
const int panelY = 52;
|
||||||
|
const int panelW = 178;
|
||||||
|
const int panelH = 136;
|
||||||
|
_fillCanonicalRect(panelX, panelY, panelW, panelH, panelColor);
|
||||||
|
|
||||||
final cursor = menu.mappedPic(
|
final cursor = menu.mappedPic(
|
||||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||||
);
|
);
|
||||||
|
|
||||||
const int rowYStart = 78;
|
const int rowYStart = 55;
|
||||||
const int rowStep = 20;
|
const int rowStep = 13;
|
||||||
const int textX = 70;
|
const int textX = 100;
|
||||||
final int selectedIndex = engine.menuManager.selectedGameIndex;
|
final int selectedIndex = engine.menuManager.selectedGameIndex;
|
||||||
|
|
||||||
for (int i = 0; i < engine.availableGames.length; i++) {
|
for (int i = 0; i < engine.availableGames.length; i++) {
|
||||||
final bool isSelected = i == selectedIndex;
|
final bool isSelected = i == selectedIndex;
|
||||||
final int y = rowYStart + (i * rowStep);
|
final int y = rowYStart + (i * rowStep);
|
||||||
if (isSelected && cursor != null) {
|
if (isSelected && cursor != null) {
|
||||||
_blitVgaImage(cursor, panelX + 10, y - 2);
|
_blitVgaImage(cursor, panelX + 4, y - 2);
|
||||||
}
|
}
|
||||||
_drawCanonicalMenuText(
|
_drawCanonicalMenuText(
|
||||||
_gameTitle(engine.availableGames[i].version),
|
MenuHeaderBand.gameTitle(engine.availableGames[i].version),
|
||||||
textX,
|
textX,
|
||||||
y,
|
y,
|
||||||
isSelected ? selectedTextColor : unselectedTextColor,
|
isSelected ? selectedTextColor : unselectedTextColor,
|
||||||
@@ -1052,19 +1084,6 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
_buffer.pixels[(y * width) + x] = color;
|
_buffer.pixels[(y * width) + x] = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _gameTitle(GameVersion version) {
|
|
||||||
switch (version) {
|
|
||||||
case GameVersion.shareware:
|
|
||||||
return 'SHAREWARE';
|
|
||||||
case GameVersion.retail:
|
|
||||||
return 'RETAIL';
|
|
||||||
case GameVersion.spearOfDestiny:
|
|
||||||
return 'SPEAR OF DESTINY';
|
|
||||||
case GameVersion.spearOfDestinyDemo:
|
|
||||||
return 'SOD DEMO';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _applyMenuTransition(MenuManager menuManager, int coverColor) {
|
void _applyMenuTransition(MenuManager menuManager, int coverColor) {
|
||||||
switch (menuManager.transitionEffect) {
|
switch (menuManager.transitionEffect) {
|
||||||
case WolfTransitionEffect.none:
|
case WolfTransitionEffect.none:
|
||||||
@@ -1215,10 +1234,24 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
final List<int>? cachedColumn = _mainMenuBandFirstColumn;
|
final List<int>? cachedColumn = _mainMenuBandFirstColumn;
|
||||||
if (cachedColumn != null && cachedColumn.isNotEmpty) {
|
if (cachedColumn != null && cachedColumn.isNotEmpty) {
|
||||||
final int bandHeight = cachedColumn.length.clamp(0, 200);
|
final int bandHeight = cachedColumn.length.clamp(0, 200);
|
||||||
|
int firstBlack = -1;
|
||||||
|
int lastBlack = -1;
|
||||||
for (int y = 0; y < bandHeight; y++) {
|
for (int y = 0; y < bandHeight; y++) {
|
||||||
final int paletteIndex = cachedColumn[y];
|
if (cachedColumn[y] == 0) {
|
||||||
final int fillIndex = paletteIndex == 0 ? 0 : paletteIndex;
|
firstBlack = firstBlack == -1 ? y : firstBlack;
|
||||||
_fillCanonicalRect(0, y, 320, 1, ColorPalette.vga32Bit[fillIndex]);
|
lastBlack = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (firstBlack != -1) {
|
||||||
|
for (int y = firstBlack; y <= lastBlack; y++) {
|
||||||
|
_fillCanonicalRect(
|
||||||
|
0,
|
||||||
|
y,
|
||||||
|
320,
|
||||||
|
1,
|
||||||
|
ColorPalette.vga32Bit[cachedColumn[y]],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1266,22 +1299,52 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawMainMenuOptionsSideBars(VgaImage optionsLabel, int optionsX320) {
|
void _drawTiledMenuBackdrop(VgaImage image, int fallbackColor) {
|
||||||
_mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
|
if (image.width <= 0 || image.height <= 0) {
|
||||||
final List<int> firstColumn = _mainMenuBandFirstColumn!;
|
for (int i = 0; i < _buffer.pixels.length; i++) {
|
||||||
for (int y = 0; y < optionsLabel.height; y++) {
|
_buffer.pixels[i] = fallbackColor;
|
||||||
final int paletteIndex = firstColumn[y];
|
}
|
||||||
final int fillIndex = paletteIndex == 0 ? 0 : paletteIndex;
|
return;
|
||||||
_fillCanonicalRect(0, y, 320, 1, ColorPalette.vga32Bit[fillIndex]);
|
}
|
||||||
|
for (int y = 0; y < 200; y += image.height) {
|
||||||
|
for (int x = 0; x < 320; x += image.width) {
|
||||||
|
_blitVgaImage(image, x, y);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<int> _cacheFirstColumn(VgaImage image) {
|
void _drawMainMenuOptionsSideBars(
|
||||||
final List<int> column = List<int>.filled(image.height, 0);
|
VgaImage optionsLabel,
|
||||||
for (int y = 0; y < image.height; y++) {
|
int optionsX320, {
|
||||||
column[y] = image.decodePixel(0, y);
|
required String debugContext,
|
||||||
|
}) {
|
||||||
|
_mainMenuBandFirstColumn = MenuHeaderBand.firstColumn(optionsLabel);
|
||||||
|
MenuHeaderBand.applyFromHeadingImage(
|
||||||
|
image: optionsLabel,
|
||||||
|
imageX320: optionsX320,
|
||||||
|
debugContext: debugContext,
|
||||||
|
fillSideEdgesRow:
|
||||||
|
(
|
||||||
|
int y200,
|
||||||
|
int leftWidth320,
|
||||||
|
int rightStartX320,
|
||||||
|
int paletteIndex,
|
||||||
|
) {
|
||||||
|
final int color = ColorPalette.vga32Bit[paletteIndex];
|
||||||
|
if (leftWidth320 > 0) {
|
||||||
|
_fillCanonicalRect(0, y200, leftWidth320, 1, color);
|
||||||
}
|
}
|
||||||
return column;
|
if (rightStartX320 < 320) {
|
||||||
|
_fillCanonicalRect(
|
||||||
|
rightStartX320,
|
||||||
|
y200,
|
||||||
|
320 - rightStartX320,
|
||||||
|
1,
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawCanonicalMenuText(
|
void _drawCanonicalMenuText(
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ export 'src/data_types/wolf_level.dart' show WolfLevel;
|
|||||||
export 'src/data_types/wolfenstein_data.dart' show WolfensteinData;
|
export 'src/data_types/wolfenstein_data.dart' show WolfensteinData;
|
||||||
// Registry public surface
|
// Registry public surface
|
||||||
export 'src/registry/asset_registry.dart' show AssetRegistry;
|
export 'src/registry/asset_registry.dart' show AssetRegistry;
|
||||||
export 'src/registry/built_in/retail_asset_registry.dart'
|
export 'src/registry/built_in/menu/spear/spear_demo_asset_registry.dart'
|
||||||
show RetailAssetRegistry;
|
|
||||||
export 'src/registry/built_in/shareware_asset_registry.dart'
|
|
||||||
show SharewareAssetRegistry;
|
|
||||||
export 'src/registry/built_in/spear_demo_asset_registry.dart'
|
|
||||||
show SpearDemoAssetRegistry;
|
show SpearDemoAssetRegistry;
|
||||||
|
export 'src/registry/built_in/menu/wolf/retail_asset_registry.dart'
|
||||||
|
show RetailAssetRegistry;
|
||||||
|
export 'src/registry/built_in/menu/wolf/shareware_asset_registry.dart'
|
||||||
|
show SharewareAssetRegistry;
|
||||||
export 'src/registry/keys/entity_key.dart' show EntityKey;
|
export 'src/registry/keys/entity_key.dart' show EntityKey;
|
||||||
export 'src/registry/keys/hud_key.dart' show HudKey;
|
export 'src/registry/keys/hud_key.dart' show HudKey;
|
||||||
export 'src/registry/keys/menu_pic_key.dart' show MenuPicKey;
|
export 'src/registry/keys/menu_pic_key.dart' show MenuPicKey;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:test/test.dart';
|
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_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.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_entities.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_menu.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test(
|
test(
|
||||||
@@ -122,6 +124,104 @@ void main() {
|
|||||||
expect(engine.entities.last, isA<SmallAmmoCollectible>());
|
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 {
|
class _TestInput extends Wolf3dInput {
|
||||||
@@ -162,6 +262,35 @@ class _SilentAudio implements EngineAudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
WolfEngine _buildEngine() {
|
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 wallGrid = _buildGrid();
|
||||||
final objectGrid = _buildGrid();
|
final objectGrid = _buildGrid();
|
||||||
_fillBoundaries(wallGrid, 2);
|
_fillBoundaries(wallGrid, 2);
|
||||||
@@ -171,11 +300,13 @@ WolfEngine _buildEngine() {
|
|||||||
wallGrid[2][3] = 90;
|
wallGrid[2][3] = 90;
|
||||||
wallGrid[4][4] = 5;
|
wallGrid[4][4] = 5;
|
||||||
|
|
||||||
return WolfEngine(
|
return WolfensteinData(
|
||||||
data: WolfensteinData(
|
version: version,
|
||||||
version: GameVersion.retail,
|
|
||||||
dataVersion: DataVersion.unknown,
|
dataVersion: DataVersion.unknown,
|
||||||
registry: RetailAssetRegistry(),
|
registry: switch (version) {
|
||||||
|
GameVersion.spearOfDestiny => SpearAssetRegistry(),
|
||||||
|
_ => RetailAssetRegistry(),
|
||||||
|
},
|
||||||
walls: [
|
walls: [
|
||||||
_solidSprite(1),
|
_solidSprite(1),
|
||||||
_solidSprite(1),
|
_solidSprite(1),
|
||||||
@@ -201,13 +332,6 @@ WolfEngine _buildEngine() {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
difficulty: Difficulty.medium,
|
|
||||||
startingEpisode: 0,
|
|
||||||
frameBuffer: FrameBuffer(64, 64),
|
|
||||||
input: _TestInput(),
|
|
||||||
onGameWon: () {},
|
|
||||||
engineAudio: _SilentAudio(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,23 @@ void main() {
|
|||||||
expect(manager.transitionEffect, WolfTransitionEffect.none);
|
expect(manager.transitionEffect, WolfTransitionEffect.none);
|
||||||
expect(manager.activeMenu, WolfMenuScreen.difficultySelect);
|
expect(manager.activeMenu, WolfMenuScreen.difficultySelect);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('spear variant main menu omits READ THIS row', () {
|
||||||
|
final manager = MenuManager();
|
||||||
|
|
||||||
|
manager.beginSelectionFlow(
|
||||||
|
gameCount: 1,
|
||||||
|
initialGameIsSpear: true,
|
||||||
|
);
|
||||||
|
manager.showMainMenu(hasResumableGame: false, hasLoadableSave: true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
manager.mainMenuEntries.any(
|
||||||
|
(entry) => entry.action == WolfMenuMainAction.readThis,
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/wolf/classic_menu_presentation_module.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('classic menu palette matches canonical WL_MENU.H constants', () {
|
||||||
|
const module = ClassicMenuPresentationModule();
|
||||||
|
|
||||||
|
expect(module.backgroundIndex, 0x2D); // BKGDCOLOR
|
||||||
|
expect(module.panelIndex, 0x23); // BORD2COLOR
|
||||||
|
expect(module.borderIndex, 0x29); // BORDCOLOR
|
||||||
|
expect(module.disabledTextIndex, 0x2B); // DEACTIVE
|
||||||
|
|
||||||
|
expect(module.unselectedTextIndex, 0x17); // TEXTCOLOR
|
||||||
|
expect(module.selectedTextIndex, 0x13); // HIGHLIGHT
|
||||||
|
expect(module.headerTextIndex, 0x47); // READHCOLOR
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/shareware_hud_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/wolf/shareware_hud_module.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/shareware_menu_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/wolf/shareware_menu_module.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/spear_menu_presentation_module.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/menu/spear/spear_menu_presentation_module.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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/src/registry/built_in/menu/spear/spear_menu_presentation_module.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('SpearAssetRegistry', () {
|
||||||
|
test('resolves full SOD menu VGA indices', () {
|
||||||
|
final SpearAssetRegistry registry = SpearAssetRegistry();
|
||||||
|
|
||||||
|
expect(registry.menu.resolve(MenuPicKey.title)?.pictureIndex, 76);
|
||||||
|
expect(registry.menu.resolve(MenuPicKey.pg13)?.pictureIndex, 88);
|
||||||
|
expect(registry.menu.resolve(MenuPicKey.credits)?.pictureIndex, 89);
|
||||||
|
expect(
|
||||||
|
registry.menu.resolve(MenuPicKey.controlBackground)?.pictureIndex,
|
||||||
|
12,
|
||||||
|
);
|
||||||
|
expect(registry.menu.resolve(MenuPicKey.optionsLabel)?.pictureIndex, 13);
|
||||||
|
expect(registry.menuPresentation, isA<SpearMenuPresentationModule>());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('BuiltInAssetRegistryResolver full SOD selection', () {
|
||||||
|
test('uses full Spear registry for game-version fallback', () {
|
||||||
|
const BuiltInAssetRegistryResolver resolver =
|
||||||
|
BuiltInAssetRegistryResolver();
|
||||||
|
|
||||||
|
final AssetRegistry registry = resolver.resolve(
|
||||||
|
const RegistrySelectionContext(
|
||||||
|
gameVersion: GameVersion.spearOfDestiny,
|
||||||
|
dataVersion: DataVersion.unknown,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(registry, isA<SpearAssetRegistry>());
|
||||||
|
expect(registry.menu.resolve(MenuPicKey.title)?.pictureIndex, 76);
|
||||||
|
expect(registry.menu.resolve(MenuPicKey.pg13)?.pictureIndex, 88);
|
||||||
|
expect(registry.menu.resolve(MenuPicKey.credits)?.pictureIndex, 89);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/rendering/menu_header_band.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('shared Spear variant utility is correct', () {
|
||||||
|
expect(
|
||||||
|
MenuHeaderBand.isSpearVariant(GameVersion.retail),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
MenuHeaderBand.isSpearVariant(GameVersion.shareware),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
MenuHeaderBand.isSpearVariant(GameVersion.spearOfDestiny),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
MenuHeaderBand.isSpearVariant(GameVersion.spearOfDestinyDemo),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'black rows and rows sandwiched between them are extended to screen sides',
|
||||||
|
() {
|
||||||
|
// Interior column (x=1): row 0 → 9 (leading non-black, skip),
|
||||||
|
// row 1 → 0 (black, fill),
|
||||||
|
// row 2 → 0 (black, fill),
|
||||||
|
// row 3 → 4 (trailing, skip), row 4 → 3 (trailing, skip).
|
||||||
|
final image = _imageFromRows([
|
||||||
|
[5, 9, 9, 5],
|
||||||
|
[6, 0, 8, 5],
|
||||||
|
[6, 0, 8, 0],
|
||||||
|
[7, 4, 4, 7],
|
||||||
|
[7, 3, 3, 7],
|
||||||
|
]);
|
||||||
|
|
||||||
|
final List<(int, int, int, int)> edgeRows = <(int, int, int, int)>[];
|
||||||
|
|
||||||
|
MenuHeaderBand.applyFromHeadingImage(
|
||||||
|
image: image,
|
||||||
|
imageX320: 10,
|
||||||
|
fillSideEdgesRow:
|
||||||
|
(
|
||||||
|
int y200,
|
||||||
|
int leftWidth320,
|
||||||
|
int rightStartX320,
|
||||||
|
int paletteIndex,
|
||||||
|
) {
|
||||||
|
edgeRows.add((y200, leftWidth320, rightStartX320, paletteIndex));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only rows 1 and 2 have interior-column value 0 (black) → side fills.
|
||||||
|
expect(
|
||||||
|
edgeRows,
|
||||||
|
<(int, int, int, int)>[
|
||||||
|
(1, 10, 14, 0),
|
||||||
|
(2, 10, 14, 0),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('no side fills when all interior rows are non-black', () {
|
||||||
|
final image = _imageFromRows([
|
||||||
|
[9, 1, 1, 3],
|
||||||
|
[8, 2, 2, 3],
|
||||||
|
[7, 4, 5, 6],
|
||||||
|
[6, 6, 6, 6],
|
||||||
|
[5, 7, 7, 2],
|
||||||
|
]);
|
||||||
|
|
||||||
|
final List<(int, int, int, int)> edgeRows = <(int, int, int, int)>[];
|
||||||
|
|
||||||
|
MenuHeaderBand.applyFromHeadingImage(
|
||||||
|
image: image,
|
||||||
|
imageX320: 50,
|
||||||
|
fillSideEdgesRow:
|
||||||
|
(
|
||||||
|
int y200,
|
||||||
|
int leftWidth320,
|
||||||
|
int rightStartX320,
|
||||||
|
int paletteIndex,
|
||||||
|
) {
|
||||||
|
edgeRows.add((y200, leftWidth320, rightStartX320, paletteIndex));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(edgeRows, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('algorithm trims long trailing rows after final black run', () {
|
||||||
|
final image = _imageFromRows([
|
||||||
|
[9, 1, 1, 3],
|
||||||
|
[8, 2, 2, 3],
|
||||||
|
[7, 0, 0, 7],
|
||||||
|
[7, 0, 0, 7],
|
||||||
|
[7, 0, 0, 7],
|
||||||
|
[6, 6, 6, 6],
|
||||||
|
[5, 5, 5, 5],
|
||||||
|
[4, 4, 4, 4],
|
||||||
|
[3, 3, 3, 3],
|
||||||
|
[2, 2, 2, 2],
|
||||||
|
[1, 1, 1, 1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
final List<(int, int, int, int)> edgeRows = <(int, int, int, int)>[];
|
||||||
|
|
||||||
|
MenuHeaderBand.applyFromHeadingImage(
|
||||||
|
image: image,
|
||||||
|
imageX320: 50,
|
||||||
|
fillSideEdgesRow:
|
||||||
|
(
|
||||||
|
int y200,
|
||||||
|
int leftWidth320,
|
||||||
|
int rightStartX320,
|
||||||
|
int paletteIndex,
|
||||||
|
) {
|
||||||
|
edgeRows.add((y200, leftWidth320, rightStartX320, paletteIndex));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Rows 2-4 are black (interior x=1 → 0). Rows 5-10 are non-black trailing;
|
||||||
|
// effectiveBandRowCount trims to lastBlack(4)+1+3 = 8 rows processed.
|
||||||
|
// Only the 3 black rows produce fills (no interior non-black rows here).
|
||||||
|
expect(edgeRows.length, 3);
|
||||||
|
expect(
|
||||||
|
edgeRows,
|
||||||
|
<(int, int, int, int)>[
|
||||||
|
(2, 50, 54, 0),
|
||||||
|
(3, 50, 54, 0),
|
||||||
|
(4, 50, 54, 0),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// firstColumn is trimmed to 8 entries (rows 0-7).
|
||||||
|
expect(
|
||||||
|
MenuHeaderBand.firstColumn(image),
|
||||||
|
<int>[1, 2, 0, 0, 0, 6, 5, 4],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('interior non-black rows sandwiched between black rows are extended', () {
|
||||||
|
// Simulates a heading image with a decorative non-black stripe between
|
||||||
|
// two black sections (e.g. Wolf3D row 32).
|
||||||
|
// Interior column (x=1): row 0 → 5 (leading non-black, skip),
|
||||||
|
// row 1 → 0 (first black, fill with palette 0),
|
||||||
|
// row 2 → 7 (interior non-black stripe, fill with palette 7),
|
||||||
|
// row 3 → 0 (last black, fill with palette 0),
|
||||||
|
// rows 4–7 → trailing non-black (4 rows > maxTrailing=3,
|
||||||
|
// so effectiveBandRowCount trims to 7 rows).
|
||||||
|
final image = _imageFromRows([
|
||||||
|
[9, 5, 5, 9],
|
||||||
|
[6, 0, 0, 6],
|
||||||
|
[6, 7, 7, 6],
|
||||||
|
[6, 0, 0, 6],
|
||||||
|
[8, 3, 3, 8],
|
||||||
|
[8, 4, 4, 8],
|
||||||
|
[8, 2, 2, 8],
|
||||||
|
[8, 1, 1, 8],
|
||||||
|
]);
|
||||||
|
|
||||||
|
final List<(int, int, int, int)> edgeRows = <(int, int, int, int)>[];
|
||||||
|
|
||||||
|
MenuHeaderBand.applyFromHeadingImage(
|
||||||
|
image: image,
|
||||||
|
imageX320: 10,
|
||||||
|
fillSideEdgesRow:
|
||||||
|
(
|
||||||
|
int y200,
|
||||||
|
int leftWidth320,
|
||||||
|
int rightStartX320,
|
||||||
|
int paletteIndex,
|
||||||
|
) {
|
||||||
|
edgeRows.add((y200, leftWidth320, rightStartX320, paletteIndex));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Rows 1 and 3 are black → fill with palette 0.
|
||||||
|
// Row 2 is interior non-black between two black rows → fill with palette 7.
|
||||||
|
// Row 0 is before firstBlack → skip (background shows through at top).
|
||||||
|
// Rows 4–6 are after lastBlack → skip (background shows through at bottom).
|
||||||
|
expect(
|
||||||
|
edgeRows,
|
||||||
|
<(int, int, int, int)>[
|
||||||
|
(1, 10, 14, 0),
|
||||||
|
(2, 10, 14, 7),
|
||||||
|
(3, 10, 14, 0),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
VgaImage _imageFromRows(List<List<int>> rows) {
|
||||||
|
const int width = 4;
|
||||||
|
final int height = rows.length;
|
||||||
|
final Uint8List pixels = Uint8List(width * height);
|
||||||
|
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
final List<int> row = rows[y];
|
||||||
|
if (row.length != width) {
|
||||||
|
throw ArgumentError('Each row must have width=$width entries.');
|
||||||
|
}
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
pixels[(x * height) + y] = row[x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return VgaImage(width: width, height: height, pixels: pixels);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user