Refactor menu structure and add Flutter-specific input and persistence layers
- Moved menu-related classes to a new structure under `src/menu/`. - Introduced `WolfMenuPresentation` to handle menu art and mappings. - Added `MenuManager` tests to ensure menu state reflects game status. - Implemented `FlutterRendererSettingsPersistence` and `FlutterSaveGamePersistence` for managing settings and save files on desktop platforms. - Created `Wolf3dFlutterInput` to handle keyboard and mouse input in a Flutter environment. - Updated README to reflect new package structure and usage instructions. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -22,7 +22,7 @@ Primary entry libraries in `lib/`:
|
||||
- `wolf_3d_renderer.dart` — rendering/backends integration points.
|
||||
- `wolf_3d_audio.dart` — audio interfaces and host backends.
|
||||
- `wolf_3d_input.dart` — input abstractions.
|
||||
- `wolf_3d_menu.dart` — menu models/managers and shared `WolfMenuPalette` color accessors for hosts.
|
||||
- `wolf_3d_menu.dart` — menu models/managers and the registry-backed `WolfMenuPresentation` helpers for hosts.
|
||||
- `wolf_3d_host.dart` — host-level glue contracts.
|
||||
|
||||
Implementation details live under `lib/src/`.
|
||||
@@ -50,6 +50,221 @@ dart test
|
||||
- This package owns deterministic engine/frame progression and shared game logic.
|
||||
- Frame-buffer sizing is controlled by hosts through engine APIs.
|
||||
- Rendering code is maintained under `lib/src/rendering/`.
|
||||
- Menu coordination is split under `lib/src/menu/manager/`; public consumers should prefer `lib/wolf_3d_menu.dart` or the internal barrel at `lib/src/menu/menu_manager.dart` instead of reaching into individual implementation files.
|
||||
- Menu presentation is selected through `AssetRegistry.menuPresentation`, which keeps retail/shareware/Spear variants and user-defined menu overrides aligned with the rest of the registry system.
|
||||
|
||||
## Custom Menus
|
||||
|
||||
Custom menu support is split across two registry modules:
|
||||
|
||||
- `MenuPicModule` maps symbolic menu keys such as `MenuPicKey.title` or an episode selection entry to concrete VGA picture indices in `WolfensteinData.vgaImages`.
|
||||
- `MenuPresentationModule` defines the palette indices and higher-level menu art lookups that renderers and hosts consume.
|
||||
|
||||
That split is intentional:
|
||||
|
||||
- `MenuPicModule` answers "which image index represents this menu asset for this game/mod?"
|
||||
- `MenuPresentationModule` answers "which colors and optional art should the UI use?"
|
||||
|
||||
In practice, most custom variants will either:
|
||||
|
||||
- reuse an existing `MenuPicModule` and only change colors/presentation, or
|
||||
- provide both a custom `MenuPicModule` and a matching `MenuPresentationModule` when the menu art layout itself changes.
|
||||
|
||||
### Using Menu Presentation From Loaded Data
|
||||
|
||||
Once game data has been loaded, bind menu presentation through the active registry:
|
||||
|
||||
```dart
|
||||
final WolfMenuPresentation menu = WolfMenuPresentation(data);
|
||||
|
||||
final int panelColor = menu.panelColor;
|
||||
final VgaImage? title = menu.title;
|
||||
final VgaImage? episode1 = menu.episodeOption(0);
|
||||
```
|
||||
|
||||
This is the normal path for renderers and any UI that should track the active game variant automatically.
|
||||
|
||||
### Fallback Presentation Before Data Loads
|
||||
|
||||
Host-owned screens that appear before game data discovery can still use menu-consistent colors:
|
||||
|
||||
```dart
|
||||
const WolfMenuPresentation classicMenu = WolfMenuPresentation.classic();
|
||||
const WolfMenuPresentation spearMenu = WolfMenuPresentation.spear();
|
||||
```
|
||||
|
||||
Those fallback constructors expose colors without requiring a loaded `WolfensteinData` instance. Art getters return `null` until real data is attached.
|
||||
|
||||
### Implementing A Custom MenuPicModule
|
||||
|
||||
Use `MenuPicModule` when your mod changes which VGA pictures back the classic menu keys:
|
||||
|
||||
```dart
|
||||
class ModMenuPics extends MenuPicModule {
|
||||
const ModMenuPics();
|
||||
|
||||
@override
|
||||
MenuPicRef? resolve(MenuPicKey key) {
|
||||
switch (key) {
|
||||
case MenuPicKey.title:
|
||||
return const MenuPicRef(140);
|
||||
case MenuPicKey.optionTitle:
|
||||
return const MenuPicRef(141);
|
||||
case MenuPicKey.customizeTitle:
|
||||
return const MenuPicRef(142);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
MenuPicKey episodeKey(int episodeIndex) {
|
||||
switch (episodeIndex) {
|
||||
case 0:
|
||||
return MenuPicKey.episode1;
|
||||
case 1:
|
||||
return MenuPicKey.episode2;
|
||||
case 2:
|
||||
return MenuPicKey.episode3;
|
||||
default:
|
||||
return MenuPicKey.episode1;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
MenuPicKey difficultyKey(Difficulty difficulty) {
|
||||
switch (difficulty) {
|
||||
case Difficulty.easy:
|
||||
return MenuPicKey.skill1;
|
||||
case Difficulty.medium:
|
||||
return MenuPicKey.skill2;
|
||||
case Difficulty.hard:
|
||||
return MenuPicKey.skill3;
|
||||
case Difficulty.expert:
|
||||
return MenuPicKey.skill4;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Returning `null` from `resolve` means that the key is not provided by that module.
|
||||
|
||||
### Implementing A Custom MenuPresentationModule
|
||||
|
||||
Use `MenuPresentationModule` when you want to change menu colors, point existing menu concepts at different art, or selectively omit optional art:
|
||||
|
||||
### Custom Menu Presentation Example
|
||||
|
||||
```dart
|
||||
class ModMenuPresentation extends MenuPresentationModule {
|
||||
const ModMenuPresentation();
|
||||
|
||||
@override
|
||||
int get backgroundIndex => 111;
|
||||
|
||||
@override
|
||||
int get panelIndex => 97;
|
||||
|
||||
@override
|
||||
int get borderIndex => 87;
|
||||
|
||||
@override
|
||||
int get emphasisIndex => 10;
|
||||
|
||||
@override
|
||||
int get warningIndex => 14;
|
||||
|
||||
@override
|
||||
int get mutedIndex => 8;
|
||||
|
||||
@override
|
||||
int get selectedTextIndex => 19;
|
||||
|
||||
@override
|
||||
int get unselectedTextIndex => 23;
|
||||
|
||||
@override
|
||||
int get disabledTextIndex => 4;
|
||||
|
||||
@override
|
||||
int get headerTextIndex => 15;
|
||||
|
||||
@override
|
||||
VgaImage? controlBackground(WolfensteinData data) => null;
|
||||
|
||||
@override
|
||||
VgaImage? title(WolfensteinData data) => null;
|
||||
|
||||
@override
|
||||
VgaImage? heading(WolfensteinData data) => null;
|
||||
|
||||
@override
|
||||
VgaImage? selectedMarker(WolfensteinData data) => null;
|
||||
|
||||
@override
|
||||
VgaImage? unselectedMarker(WolfensteinData data) => null;
|
||||
|
||||
@override
|
||||
VgaImage? optionsLabel(WolfensteinData data) => null;
|
||||
|
||||
@override
|
||||
VgaImage? customizeLabel(WolfensteinData data) => null;
|
||||
|
||||
@override
|
||||
VgaImage? credits(WolfensteinData data) => null;
|
||||
|
||||
@override
|
||||
VgaImage? episodeOption(WolfensteinData data, int episodeIndex) => null;
|
||||
|
||||
@override
|
||||
VgaImage? difficultyOption(WolfensteinData data, Difficulty difficulty) =>
|
||||
null;
|
||||
|
||||
@override
|
||||
VgaImage? mappedPic(WolfensteinData data, int index) => null;
|
||||
}
|
||||
|
||||
final registry = AssetRegistry(
|
||||
sfx: mySfxModule,
|
||||
music: myMusicModule,
|
||||
entities: myEntityModule,
|
||||
hud: myHudModule,
|
||||
menu: myMenuPicModule,
|
||||
menuPresentation: const ModMenuPresentation(),
|
||||
);
|
||||
```
|
||||
|
||||
The presentation module should treat its image-returning methods as optional hooks:
|
||||
|
||||
- return a `VgaImage` when that surface has variant-specific art,
|
||||
- return `null` when the presentation intentionally has no image for that concept,
|
||||
- use `mappedPic(...)` only for legacy numeric menu art lookups that still matter for a renderer path.
|
||||
|
||||
### Wiring A Fully Custom Registry
|
||||
|
||||
To ship a complete custom menu variant, provide both modules through `AssetRegistry` when loading data:
|
||||
|
||||
```dart
|
||||
final registry = AssetRegistry(
|
||||
sfx: mySfxModule,
|
||||
music: myMusicModule,
|
||||
entities: myEntityModule,
|
||||
hud: myHudModule,
|
||||
menu: const ModMenuPics(),
|
||||
menuPresentation: const ModMenuPresentation(),
|
||||
);
|
||||
```
|
||||
|
||||
If your menu art still follows the built-in retail/shareware layout, you may not need a custom `MenuPicModule`. In that case, keep the built-in module and only swap `menuPresentation`.
|
||||
|
||||
### Choosing The Right Extension Point
|
||||
|
||||
- Change colors only: implement `MenuPresentationModule`.
|
||||
- Change symbolic menu art mapping: implement `MenuPicModule`.
|
||||
- Change both colors and art layout: implement both modules.
|
||||
- Build a host setup screen before data loads: use `WolfMenuPresentation.classic()` or `WolfMenuPresentation.spear()`.
|
||||
|
||||
For most host code, prefer the public `wolf_3d_menu.dart` surface instead of importing internal files directly.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
|
||||
@@ -2,11 +2,10 @@ import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:wolf_3d_dart/src/data/md5_hash.dart';
|
||||
import 'package:wolf_3d_dart/src/data/wl_parser.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
import '../md5_hash.dart';
|
||||
|
||||
/// dart:io implementation for directory discovery with version integrity checks.
|
||||
Future<Map<GameVersion, WolfensteinData>> discoverInDirectory({
|
||||
String? directoryPath,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
import 'io/discovery_stub.dart'
|
||||
if (dart.library.io) 'io/discovery_io.dart'
|
||||
import 'package:wolf_3d_dart/src/data/io/discovery_stub.dart'
|
||||
if (dart.library.io) 'package:wolf_3d_dart/src/data/io/discovery_io.dart'
|
||||
as platform;
|
||||
import 'md5_hash.dart';
|
||||
import 'wl_parser.dart';
|
||||
import 'package:wolf_3d_dart/src/data/md5_hash.dart';
|
||||
import 'package:wolf_3d_dart/src/data/wl_parser.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
/// The main entry point for loading Wolfenstein 3D data.
|
||||
///
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
library;
|
||||
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
|
||||
import 'menu_manager_entries.dart';
|
||||
import 'menu_manager_enums.dart';
|
||||
|
||||
part 'menu_manager_intro_mixin.dart';
|
||||
part 'menu_manager_navigation_mixin.dart';
|
||||
part 'menu_manager_selection_mixin.dart';
|
||||
|
||||
enum _WolfIntroPhase { fadeIn, hold, fadeOut }
|
||||
|
||||
abstract class _MenuManagerBase {
|
||||
static const int transitionDurationMs = 280;
|
||||
static const int introFadeDurationMs = 280;
|
||||
static const int introRetailBackgroundRgb = 0xA00000;
|
||||
static const int introPg13BackgroundRgb = 0x33A2E8;
|
||||
static const int introTitleBackgroundRgb = 0x000000;
|
||||
|
||||
WolfMenuScreen _activeMenu = WolfMenuScreen.difficultySelect;
|
||||
WolfMenuScreen? _transitionTarget;
|
||||
WolfTransitionEffect _transitionEffect = WolfTransitionEffect.normalFade;
|
||||
int _transitionElapsedMs = 0;
|
||||
bool _transitionSwappedMenu = false;
|
||||
WolfMenuScreen _introLandingMenu = WolfMenuScreen.mainMenu;
|
||||
WolfTransitionEffect _introEffect = WolfTransitionEffect.normalFade;
|
||||
int _introSlideIndex = 0;
|
||||
int _introElapsedMs = 0;
|
||||
_WolfIntroPhase _introPhase = _WolfIntroPhase.fadeIn;
|
||||
bool _introAdvanceRequested = false;
|
||||
List<WolfIntroSlide> _introSlides = <WolfIntroSlide>[
|
||||
WolfIntroSlide.pg13,
|
||||
WolfIntroSlide.title,
|
||||
];
|
||||
|
||||
int _selectedMainIndex = 0;
|
||||
int _selectedGameIndex = 0;
|
||||
int _selectedEpisodeIndex = 0;
|
||||
int _selectedDifficultyIndex = 0;
|
||||
int _selectedChangeViewIndex = 0;
|
||||
int _selectedRendererOptionIndex = 0;
|
||||
String _rendererOptionsTitle = 'CUSTOMIZE';
|
||||
List<WolfMenuRendererEntry> _changeViewEntries =
|
||||
const <WolfMenuRendererEntry>[];
|
||||
List<WolfMenuRendererOptionEntry> _rendererOptionEntries =
|
||||
const <WolfMenuRendererOptionEntry>[];
|
||||
bool _showResumeOption = false;
|
||||
bool _hasLoadableSave = false;
|
||||
int _gameCount = 1;
|
||||
|
||||
bool _prevUp = false;
|
||||
bool _prevDown = false;
|
||||
bool _prevConfirm = false;
|
||||
bool _prevBack = false;
|
||||
|
||||
int _menuBackgroundRgb = 0x890000;
|
||||
|
||||
bool get isTransitioning => _transitionTarget != null;
|
||||
|
||||
bool get isIntroSplashActive => _activeMenu == WolfMenuScreen.introSplash;
|
||||
|
||||
int get changeViewItemCount =>
|
||||
_changeViewEntries.length + _rendererOptionEntries.length;
|
||||
|
||||
void consumeEdgeState(EngineInput input) {
|
||||
_prevUp = input.isMovingForward;
|
||||
_prevDown = input.isMovingBackward;
|
||||
_prevConfirm = input.isInteracting || input.isFiring;
|
||||
_prevBack = input.isBack;
|
||||
}
|
||||
|
||||
void resetEdgeState() {
|
||||
_prevUp = false;
|
||||
_prevDown = false;
|
||||
_prevConfirm = false;
|
||||
_prevBack = false;
|
||||
}
|
||||
|
||||
int findSelectableIndex(
|
||||
int startIndex,
|
||||
int itemCount,
|
||||
bool Function(int index) selectable,
|
||||
) {
|
||||
if (itemCount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
for (int offset = 0; offset < itemCount; offset++) {
|
||||
final int index = (startIndex + offset) % itemCount;
|
||||
if (selectable(index)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return clampIndex(startIndex, itemCount);
|
||||
}
|
||||
|
||||
int moveSelectableIndex(
|
||||
int currentIndex,
|
||||
int itemCount,
|
||||
int delta,
|
||||
bool Function(int index) selectable,
|
||||
) {
|
||||
if (itemCount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
int index = currentIndex;
|
||||
for (int step = 0; step < itemCount; step++) {
|
||||
index = (index + delta + itemCount) % itemCount;
|
||||
if (selectable(index)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
int clampIndex(int index, int itemCount) {
|
||||
if (itemCount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return index.clamp(0, itemCount - 1);
|
||||
}
|
||||
|
||||
void beginSelectionFlow({
|
||||
required int gameCount,
|
||||
int initialGameIndex = 0,
|
||||
int initialEpisodeIndex = 0,
|
||||
Difficulty? initialDifficulty,
|
||||
bool hasResumableGame = false,
|
||||
bool hasLoadableSave = false,
|
||||
bool initialGameIsRetail = false,
|
||||
WolfTransitionEffect introEffect = WolfTransitionEffect.normalFade,
|
||||
});
|
||||
|
||||
int _defaultMainMenuIndex();
|
||||
}
|
||||
|
||||
/// Coordinates menu state, splash sequencing, and selection updates.
|
||||
///
|
||||
/// Hosts and renderers interact with this type through the stable
|
||||
/// `src/menu/menu_manager.dart` barrel, while the implementation remains split
|
||||
/// across focused files under `src/menu/manager/`.
|
||||
class MenuManager extends _MenuManagerBase
|
||||
with
|
||||
_MenuManagerIntroMixin,
|
||||
_MenuManagerSelectionMixin,
|
||||
_MenuManagerNavigationMixin {
|
||||
static const int transitionDurationMs = _MenuManagerBase.transitionDurationMs;
|
||||
static const int introFadeDurationMs = _MenuManagerBase.introFadeDurationMs;
|
||||
|
||||
/// Whether to show the alternate cursor frame at [elapsedMs].
|
||||
bool isCursorAltFrame(int elapsedMs) => ((elapsedMs ~/ 220) % 2) == 1;
|
||||
}
|
||||
|
||||
class _MenuAction {
|
||||
const _MenuAction({
|
||||
required this.index,
|
||||
required this.confirmed,
|
||||
required this.goBack,
|
||||
});
|
||||
|
||||
final int index;
|
||||
final bool confirmed;
|
||||
final bool goBack;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
|
||||
/// Logical actions exposed by the main menu.
|
||||
enum WolfMenuMainAction {
|
||||
newGame,
|
||||
sound,
|
||||
control,
|
||||
loadGame,
|
||||
saveGame,
|
||||
changeView,
|
||||
readThis,
|
||||
viewScores,
|
||||
endGame,
|
||||
backToGame,
|
||||
backToDemo,
|
||||
quit,
|
||||
}
|
||||
|
||||
/// Immutable description of a main-menu row.
|
||||
class WolfMenuMainEntry {
|
||||
const WolfMenuMainEntry({
|
||||
required this.action,
|
||||
required this.label,
|
||||
this.isEnabled = true,
|
||||
});
|
||||
|
||||
final WolfMenuMainAction action;
|
||||
final String label;
|
||||
final bool isEnabled;
|
||||
}
|
||||
|
||||
/// Immutable description of a renderer row in the change-view menu.
|
||||
class WolfMenuRendererEntry {
|
||||
const WolfMenuRendererEntry({
|
||||
required this.mode,
|
||||
required this.label,
|
||||
required this.hasOptions,
|
||||
this.isEnabled = true,
|
||||
this.isChecked = false,
|
||||
});
|
||||
|
||||
final WolfRendererMode mode;
|
||||
final String label;
|
||||
final bool hasOptions;
|
||||
final bool isEnabled;
|
||||
final bool isChecked;
|
||||
}
|
||||
|
||||
/// Immutable description of a renderer-specific option row.
|
||||
class WolfMenuRendererOptionEntry {
|
||||
const WolfMenuRendererOptionEntry({
|
||||
required this.id,
|
||||
required this.label,
|
||||
this.isEnabled = true,
|
||||
this.isChecked = false,
|
||||
});
|
||||
|
||||
final WolfRendererOptionId id;
|
||||
final String label;
|
||||
final bool isEnabled;
|
||||
final bool isChecked;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/// Menu screens handled by [MenuManager].
|
||||
enum WolfMenuScreen {
|
||||
introSplash,
|
||||
mainMenu,
|
||||
gameSelect,
|
||||
episodeSelect,
|
||||
difficultySelect,
|
||||
changeView,
|
||||
rendererOptions,
|
||||
}
|
||||
|
||||
/// Splash slides shown before the control-panel menu.
|
||||
enum WolfIntroSlide { retailWarning, pg13, title }
|
||||
|
||||
/// Visual effect used when entering or leaving a menu surface.
|
||||
enum WolfTransitionEffect { none, normalFade, fizzleFade }
|
||||
|
||||
/// Phase of a two-stage menu transition effect.
|
||||
enum WolfTransitionPhase { idle, covering, revealing }
|
||||
@@ -0,0 +1,324 @@
|
||||
part of 'menu_manager.dart';
|
||||
|
||||
mixin _MenuManagerIntroMixin on _MenuManagerBase {
|
||||
/// The currently visible intro slide.
|
||||
WolfIntroSlide get currentIntroSlide {
|
||||
if (_introSlides.isEmpty) {
|
||||
return WolfIntroSlide.title;
|
||||
}
|
||||
final int index = _introSlideIndex.clamp(0, _introSlides.length - 1);
|
||||
return _introSlides[index];
|
||||
}
|
||||
|
||||
/// Whether the retail warning card is currently visible.
|
||||
bool get isIntroRetailWarningSlide =>
|
||||
currentIntroSlide == WolfIntroSlide.retailWarning;
|
||||
|
||||
/// Whether the PG-13 splash is currently visible.
|
||||
bool get isIntroPg13Slide => currentIntroSlide == WolfIntroSlide.pg13;
|
||||
|
||||
/// Whether the title splash is currently visible.
|
||||
bool get isIntroTitleSlide => currentIntroSlide == WolfIntroSlide.title;
|
||||
|
||||
/// Background RGB used for the active intro slide.
|
||||
int get introBackgroundRgb {
|
||||
switch (currentIntroSlide) {
|
||||
case WolfIntroSlide.retailWarning:
|
||||
return _MenuManagerBase.introRetailBackgroundRgb;
|
||||
case WolfIntroSlide.pg13:
|
||||
return _MenuManagerBase.introPg13BackgroundRgb;
|
||||
case WolfIntroSlide.title:
|
||||
return _MenuManagerBase.introTitleBackgroundRgb;
|
||||
}
|
||||
}
|
||||
|
||||
/// Overlay alpha for the current intro transition.
|
||||
double get introOverlayAlpha {
|
||||
if (!isIntroSplashActive) {
|
||||
return 0.0;
|
||||
}
|
||||
switch (_introPhase) {
|
||||
case _WolfIntroPhase.fadeIn:
|
||||
return (1.0 - (_introElapsedMs / _MenuManagerBase.introFadeDurationMs))
|
||||
.clamp(0.0, 1.0);
|
||||
case _WolfIntroPhase.hold:
|
||||
return 0.0;
|
||||
case _WolfIntroPhase.fadeOut:
|
||||
return (_introElapsedMs / _MenuManagerBase.introFadeDurationMs).clamp(
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Effect currently applied to the intro overlay.
|
||||
WolfTransitionEffect get introOverlayEffect {
|
||||
if (!isIntroSplashActive || _introPhase == _WolfIntroPhase.hold) {
|
||||
return WolfTransitionEffect.none;
|
||||
}
|
||||
return _introEffect;
|
||||
}
|
||||
|
||||
/// Phase currently applied to the intro overlay.
|
||||
WolfTransitionPhase get introOverlayPhase {
|
||||
if (!isIntroSplashActive) {
|
||||
return WolfTransitionPhase.idle;
|
||||
}
|
||||
switch (_introPhase) {
|
||||
case _WolfIntroPhase.fadeIn:
|
||||
return WolfTransitionPhase.revealing;
|
||||
case _WolfIntroPhase.hold:
|
||||
return WolfTransitionPhase.idle;
|
||||
case _WolfIntroPhase.fadeOut:
|
||||
return WolfTransitionPhase.covering;
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalized progress for the current intro overlay phase.
|
||||
double get introOverlayPhaseProgress {
|
||||
if (!isIntroSplashActive || _introPhase == _WolfIntroPhase.hold) {
|
||||
return 0.0;
|
||||
}
|
||||
return (_introElapsedMs / _MenuManagerBase.introFadeDurationMs).clamp(
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
}
|
||||
|
||||
/// Fade alpha for active menu transitions, in the range `0.0..1.0`.
|
||||
double get transitionAlpha {
|
||||
if (!isTransitioning) {
|
||||
return 0.0;
|
||||
}
|
||||
final int half = _MenuManagerBase.transitionDurationMs ~/ 2;
|
||||
if (_transitionElapsedMs <= half) {
|
||||
return (_transitionElapsedMs / half).clamp(0.0, 1.0);
|
||||
}
|
||||
final int fadeInElapsed = _transitionElapsedMs - half;
|
||||
return (1.0 - (fadeInElapsed / half)).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
/// Effect applied to the active transition.
|
||||
WolfTransitionEffect get transitionEffect {
|
||||
if (!isTransitioning) {
|
||||
return WolfTransitionEffect.none;
|
||||
}
|
||||
return _transitionEffect;
|
||||
}
|
||||
|
||||
/// Phase of the current menu transition.
|
||||
WolfTransitionPhase get transitionPhase {
|
||||
if (!isTransitioning) {
|
||||
return WolfTransitionPhase.idle;
|
||||
}
|
||||
final int half = _MenuManagerBase.transitionDurationMs ~/ 2;
|
||||
if (_transitionElapsedMs < half) {
|
||||
return WolfTransitionPhase.covering;
|
||||
}
|
||||
return WolfTransitionPhase.revealing;
|
||||
}
|
||||
|
||||
/// Normalized progress for the current menu transition phase.
|
||||
double get transitionPhaseProgress {
|
||||
if (!isTransitioning) {
|
||||
return 0.0;
|
||||
}
|
||||
final int half = _MenuManagerBase.transitionDurationMs ~/ 2;
|
||||
if (_transitionElapsedMs < half) {
|
||||
return (_transitionElapsedMs / half).clamp(0.0, 1.0);
|
||||
}
|
||||
return ((_transitionElapsedMs - half) / half).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
/// Resets menu state for startup and optionally begins the intro sequence.
|
||||
@override
|
||||
void beginSelectionFlow({
|
||||
required int gameCount,
|
||||
int initialGameIndex = 0,
|
||||
int initialEpisodeIndex = 0,
|
||||
Difficulty? initialDifficulty,
|
||||
bool hasResumableGame = false,
|
||||
bool hasLoadableSave = false,
|
||||
bool initialGameIsRetail = false,
|
||||
WolfTransitionEffect introEffect = WolfTransitionEffect.normalFade,
|
||||
}) {
|
||||
_gameCount = gameCount;
|
||||
_showResumeOption = hasResumableGame;
|
||||
_hasLoadableSave = hasLoadableSave;
|
||||
_selectedMainIndex = _defaultMainMenuIndex();
|
||||
_selectedGameIndex = clampIndex(initialGameIndex, gameCount);
|
||||
_selectedEpisodeIndex = initialEpisodeIndex < 0 ? 0 : initialEpisodeIndex;
|
||||
_selectedDifficultyIndex = initialDifficulty == null
|
||||
? 0
|
||||
: Difficulty.values
|
||||
.indexOf(initialDifficulty)
|
||||
.clamp(0, Difficulty.values.length - 1);
|
||||
_introLandingMenu = WolfMenuScreen.mainMenu;
|
||||
if (gameCount > 1) {
|
||||
_activeMenu = WolfMenuScreen.gameSelect;
|
||||
_introEffect = introEffect;
|
||||
_introElapsedMs = 0;
|
||||
_introPhase = _WolfIntroPhase.fadeIn;
|
||||
_introSlideIndex = 0;
|
||||
_introSlides = <WolfIntroSlide>[
|
||||
WolfIntroSlide.pg13,
|
||||
WolfIntroSlide.title,
|
||||
];
|
||||
} else {
|
||||
_startIntroSequence(
|
||||
includeRetailWarning: initialGameIsRetail,
|
||||
effect: introEffect,
|
||||
);
|
||||
}
|
||||
_transitionTarget = null;
|
||||
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
resetEdgeState();
|
||||
}
|
||||
|
||||
/// Starts the intro splash flow and lands on [landingMenu] when complete.
|
||||
void beginIntroSplash({
|
||||
WolfMenuScreen landingMenu = WolfMenuScreen.mainMenu,
|
||||
bool includeRetailWarning = false,
|
||||
WolfTransitionEffect effect = WolfTransitionEffect.normalFade,
|
||||
}) {
|
||||
_introLandingMenu = landingMenu;
|
||||
_transitionTarget = null;
|
||||
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
_startIntroSequence(
|
||||
includeRetailWarning: includeRetailWarning,
|
||||
effect: effect,
|
||||
);
|
||||
resetEdgeState();
|
||||
}
|
||||
|
||||
/// Starts a transition from the current menu to [target].
|
||||
void startTransition(
|
||||
WolfMenuScreen target, {
|
||||
WolfTransitionEffect effect = WolfTransitionEffect.normalFade,
|
||||
}) {
|
||||
if (_activeMenu == target) {
|
||||
return;
|
||||
}
|
||||
_transitionTarget = target;
|
||||
_transitionEffect = effect;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
resetEdgeState();
|
||||
}
|
||||
|
||||
/// Advances active splash or menu transitions by [deltaMs].
|
||||
void tickTransition(int deltaMs) {
|
||||
if (isIntroSplashActive) {
|
||||
_tickIntro(deltaMs);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isTransitioning) {
|
||||
return;
|
||||
}
|
||||
_transitionElapsedMs += deltaMs;
|
||||
final int half = _MenuManagerBase.transitionDurationMs ~/ 2;
|
||||
if (!_transitionSwappedMenu && _transitionElapsedMs >= half) {
|
||||
_activeMenu = _transitionTarget!;
|
||||
_transitionSwappedMenu = true;
|
||||
}
|
||||
if (_transitionElapsedMs >= _MenuManagerBase.transitionDurationMs) {
|
||||
_transitionTarget = null;
|
||||
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes input for the intro splash screen.
|
||||
void updateIntroSplash(EngineInput input) {
|
||||
if (!isIntroSplashActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
final bool confirmNow = input.isInteracting;
|
||||
if (confirmNow && !_prevConfirm) {
|
||||
if (_introPhase == _WolfIntroPhase.fadeOut) {
|
||||
} else if (_introPhase == _WolfIntroPhase.hold) {
|
||||
_introPhase = _WolfIntroPhase.fadeOut;
|
||||
_introElapsedMs = 0;
|
||||
} else {
|
||||
_introAdvanceRequested = true;
|
||||
}
|
||||
}
|
||||
|
||||
consumeEdgeState(input);
|
||||
}
|
||||
|
||||
void _startIntroSequence({
|
||||
required bool includeRetailWarning,
|
||||
required WolfTransitionEffect effect,
|
||||
}) {
|
||||
_activeMenu = WolfMenuScreen.introSplash;
|
||||
_introEffect = effect;
|
||||
_introSlides = includeRetailWarning
|
||||
? <WolfIntroSlide>[
|
||||
WolfIntroSlide.retailWarning,
|
||||
WolfIntroSlide.pg13,
|
||||
WolfIntroSlide.title,
|
||||
]
|
||||
: <WolfIntroSlide>[WolfIntroSlide.pg13, WolfIntroSlide.title];
|
||||
_introSlideIndex = 0;
|
||||
_introElapsedMs = 0;
|
||||
_introPhase = _WolfIntroPhase.fadeIn;
|
||||
_introAdvanceRequested = false;
|
||||
}
|
||||
|
||||
void _tickIntro(int deltaMs) {
|
||||
if (!isIntroSplashActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
_introElapsedMs += deltaMs;
|
||||
|
||||
switch (_introPhase) {
|
||||
case _WolfIntroPhase.fadeIn:
|
||||
if (_introElapsedMs >= _MenuManagerBase.introFadeDurationMs) {
|
||||
_introElapsedMs = 0;
|
||||
if (_introAdvanceRequested) {
|
||||
_introPhase = _WolfIntroPhase.fadeOut;
|
||||
_introAdvanceRequested = false;
|
||||
} else {
|
||||
_introPhase = _WolfIntroPhase.hold;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case _WolfIntroPhase.hold:
|
||||
_introElapsedMs = 0;
|
||||
if (_introAdvanceRequested) {
|
||||
_introPhase = _WolfIntroPhase.fadeOut;
|
||||
_introAdvanceRequested = false;
|
||||
}
|
||||
break;
|
||||
case _WolfIntroPhase.fadeOut:
|
||||
if (_introElapsedMs >= _MenuManagerBase.introFadeDurationMs) {
|
||||
_advanceIntroSlide();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _advanceIntroSlide() {
|
||||
if (_introSlideIndex < _introSlides.length - 1) {
|
||||
_introSlideIndex += 1;
|
||||
_introElapsedMs = 0;
|
||||
_introPhase = _WolfIntroPhase.fadeIn;
|
||||
_introAdvanceRequested = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_activeMenu = _introLandingMenu;
|
||||
_introElapsedMs = 0;
|
||||
_introPhase = _WolfIntroPhase.fadeIn;
|
||||
_introAdvanceRequested = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
part of 'menu_manager.dart';
|
||||
|
||||
mixin _MenuManagerNavigationMixin on _MenuManagerSelectionMixin {
|
||||
/// Updates main-menu navigation and returns the selected action, if any.
|
||||
({WolfMenuMainAction? selected, bool goBack}) updateMainMenu(
|
||||
EngineInput input,
|
||||
) {
|
||||
if (isTransitioning) {
|
||||
consumeEdgeState(input);
|
||||
return (selected: null, goBack: false);
|
||||
}
|
||||
|
||||
final _MenuAction action = _updateLinearSelection(
|
||||
input,
|
||||
currentIndex: selectedMainIndex,
|
||||
itemCount: mainMenuEntries.length,
|
||||
isSelectableIndex: _isSelectableMainIndex,
|
||||
);
|
||||
_selectedMainIndex = action.index;
|
||||
return (
|
||||
selected: action.confirmed
|
||||
? mainMenuEntries[_selectedMainIndex].action
|
||||
: null,
|
||||
goBack: action.goBack,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates change-view navigation and returns either a mode or option choice.
|
||||
({
|
||||
WolfRendererMode? selectedMode,
|
||||
WolfRendererOptionId? selectedOption,
|
||||
bool goBack,
|
||||
})
|
||||
updateChangeViewMenu(EngineInput input) {
|
||||
if (isTransitioning) {
|
||||
consumeEdgeState(input);
|
||||
return (
|
||||
selectedMode: null,
|
||||
selectedOption: null,
|
||||
goBack: false,
|
||||
);
|
||||
}
|
||||
|
||||
final _MenuAction action = _updateLinearSelection(
|
||||
input,
|
||||
currentIndex: selectedChangeViewIndex,
|
||||
itemCount: changeViewItemCount,
|
||||
isSelectableIndex: _isSelectableChangeViewIndex,
|
||||
);
|
||||
_selectedChangeViewIndex = action.index;
|
||||
|
||||
if (!action.confirmed) {
|
||||
return (
|
||||
selectedMode: null,
|
||||
selectedOption: null,
|
||||
goBack: action.goBack,
|
||||
);
|
||||
}
|
||||
|
||||
if (_selectedChangeViewIndex < _changeViewEntries.length) {
|
||||
final WolfMenuRendererEntry entry =
|
||||
_changeViewEntries[_selectedChangeViewIndex];
|
||||
return (
|
||||
selectedMode: entry.mode,
|
||||
selectedOption: null,
|
||||
goBack: action.goBack,
|
||||
);
|
||||
}
|
||||
|
||||
final int optionIndex =
|
||||
_selectedChangeViewIndex - _changeViewEntries.length;
|
||||
if (optionIndex < 0 || optionIndex >= _rendererOptionEntries.length) {
|
||||
return (
|
||||
selectedMode: null,
|
||||
selectedOption: null,
|
||||
goBack: action.goBack,
|
||||
);
|
||||
}
|
||||
_selectedRendererOptionIndex = optionIndex;
|
||||
|
||||
return (
|
||||
selectedMode: null,
|
||||
selectedOption: _rendererOptionEntries[optionIndex].id,
|
||||
goBack: action.goBack,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates renderer-option navigation and returns the selected option, if any.
|
||||
({WolfRendererOptionId? selectedOption, bool goBack})
|
||||
updateRendererOptionsMenu(EngineInput input) {
|
||||
if (isTransitioning) {
|
||||
consumeEdgeState(input);
|
||||
return (selectedOption: null, goBack: false);
|
||||
}
|
||||
|
||||
final _MenuAction action = _updateLinearSelection(
|
||||
input,
|
||||
currentIndex: selectedRendererOptionIndex,
|
||||
itemCount: _rendererOptionEntries.length,
|
||||
isSelectableIndex: _isSelectableRendererOptionIndex,
|
||||
);
|
||||
_selectedRendererOptionIndex = action.index;
|
||||
|
||||
return (
|
||||
selectedOption: action.confirmed && _rendererOptionEntries.isNotEmpty
|
||||
? _rendererOptionEntries[_selectedRendererOptionIndex].id
|
||||
: null,
|
||||
goBack: action.goBack,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates difficulty selection and returns the selected difficulty, if any.
|
||||
({Difficulty? selected, bool goBack}) updateDifficultySelection(
|
||||
EngineInput input,
|
||||
) {
|
||||
if (isTransitioning) {
|
||||
consumeEdgeState(input);
|
||||
return (selected: null, goBack: false);
|
||||
}
|
||||
|
||||
final bool upNow = input.isMovingForward;
|
||||
final bool downNow = input.isMovingBackward;
|
||||
final bool confirmNow = input.isInteracting || input.isFiring;
|
||||
final bool backNow = input.isBack;
|
||||
|
||||
if (upNow && !_prevUp) {
|
||||
_selectedDifficultyIndex =
|
||||
(_selectedDifficultyIndex - 1 + Difficulty.values.length) %
|
||||
Difficulty.values.length;
|
||||
}
|
||||
|
||||
if (downNow && !_prevDown) {
|
||||
_selectedDifficultyIndex =
|
||||
(_selectedDifficultyIndex + 1) % Difficulty.values.length;
|
||||
}
|
||||
|
||||
Difficulty? selected;
|
||||
if (confirmNow && !_prevConfirm) {
|
||||
selected = Difficulty.values[_selectedDifficultyIndex];
|
||||
}
|
||||
|
||||
final bool goBack = backNow && !_prevBack;
|
||||
|
||||
_prevUp = upNow;
|
||||
_prevDown = downNow;
|
||||
_prevConfirm = confirmNow;
|
||||
_prevBack = backNow;
|
||||
|
||||
return (selected: selected, goBack: goBack);
|
||||
}
|
||||
|
||||
/// Updates game selection and returns the selected row index, if any.
|
||||
({int? selectedIndex, bool goBack}) updateGameSelection(
|
||||
EngineInput input, {
|
||||
required int gameCount,
|
||||
}) {
|
||||
if (isTransitioning) {
|
||||
consumeEdgeState(input);
|
||||
return (selectedIndex: null, goBack: false);
|
||||
}
|
||||
final _MenuAction action = _updateLinearSelection(
|
||||
input,
|
||||
currentIndex: selectedGameIndex,
|
||||
itemCount: gameCount,
|
||||
);
|
||||
_selectedGameIndex = action.index;
|
||||
return (
|
||||
selectedIndex: action.confirmed ? _selectedGameIndex : null,
|
||||
goBack: action.goBack,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates episode selection and returns the selected row index, if any.
|
||||
({int? selectedIndex, bool goBack}) updateEpisodeSelection(
|
||||
EngineInput input, {
|
||||
required int episodeCount,
|
||||
}) {
|
||||
if (isTransitioning) {
|
||||
consumeEdgeState(input);
|
||||
return (selectedIndex: null, goBack: false);
|
||||
}
|
||||
final _MenuAction action = _updateLinearSelection(
|
||||
input,
|
||||
currentIndex: selectedEpisodeIndex,
|
||||
itemCount: episodeCount,
|
||||
);
|
||||
_selectedEpisodeIndex = action.index;
|
||||
return (
|
||||
selectedIndex: action.confirmed ? _selectedEpisodeIndex : null,
|
||||
goBack: action.goBack,
|
||||
);
|
||||
}
|
||||
|
||||
_MenuAction _updateLinearSelection(
|
||||
EngineInput input, {
|
||||
required int currentIndex,
|
||||
required int itemCount,
|
||||
bool Function(int index)? isSelectableIndex,
|
||||
}) {
|
||||
final bool upNow = input.isMovingForward;
|
||||
final bool downNow = input.isMovingBackward;
|
||||
final bool confirmNow = input.isInteracting || input.isFiring;
|
||||
final bool backNow = input.isBack;
|
||||
|
||||
int nextIndex = clampIndex(currentIndex, itemCount);
|
||||
final bool Function(int index) selectable =
|
||||
isSelectableIndex ?? ((_) => true);
|
||||
|
||||
if (itemCount > 0 && !selectable(nextIndex)) {
|
||||
nextIndex = findSelectableIndex(nextIndex, itemCount, selectable);
|
||||
}
|
||||
|
||||
if (itemCount > 0) {
|
||||
if (upNow && !_prevUp) {
|
||||
nextIndex = moveSelectableIndex(nextIndex, itemCount, -1, selectable);
|
||||
}
|
||||
|
||||
if (downNow && !_prevDown) {
|
||||
nextIndex = moveSelectableIndex(nextIndex, itemCount, 1, selectable);
|
||||
}
|
||||
}
|
||||
|
||||
final bool confirmed = confirmNow && !_prevConfirm && selectable(nextIndex);
|
||||
final bool goBack = backNow && !_prevBack;
|
||||
|
||||
_prevUp = upNow;
|
||||
_prevDown = downNow;
|
||||
_prevConfirm = confirmNow;
|
||||
_prevBack = backNow;
|
||||
|
||||
return _MenuAction(index: nextIndex, confirmed: confirmed, goBack: goBack);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
part of 'menu_manager.dart';
|
||||
|
||||
mixin _MenuManagerSelectionMixin on _MenuManagerBase {
|
||||
/// Index of the selected main-menu row.
|
||||
int get selectedMainIndex => _selectedMainIndex;
|
||||
|
||||
/// Index of the selected game row.
|
||||
int get selectedGameIndex => _selectedGameIndex;
|
||||
|
||||
/// Index of the selected episode row.
|
||||
int get selectedEpisodeIndex => _selectedEpisodeIndex;
|
||||
|
||||
/// Index of the selected row within the change-view menu.
|
||||
int get selectedChangeViewIndex => _selectedChangeViewIndex;
|
||||
|
||||
/// Index of the selected renderer-options row.
|
||||
int get selectedRendererOptionIndex => _selectedRendererOptionIndex;
|
||||
|
||||
/// Index of the selected difficulty row.
|
||||
int get selectedDifficultyIndex => _selectedDifficultyIndex;
|
||||
|
||||
/// Title shown above renderer-specific options.
|
||||
String get rendererOptionsTitle => _rendererOptionsTitle;
|
||||
|
||||
/// Renderer entries shown in the change-view menu.
|
||||
List<WolfMenuRendererEntry> get changeViewEntries =>
|
||||
List<WolfMenuRendererEntry>.unmodifiable(_changeViewEntries);
|
||||
|
||||
/// Renderer option entries shown in the customize menu.
|
||||
List<WolfMenuRendererOptionEntry> get rendererOptionEntries =>
|
||||
List<WolfMenuRendererOptionEntry>.unmodifiable(_rendererOptionEntries);
|
||||
|
||||
/// The currently active menu screen.
|
||||
WolfMenuScreen get activeMenu => _activeMenu;
|
||||
|
||||
/// Background RGB used by menu renderers.
|
||||
int get menuBackgroundRgb => _menuBackgroundRgb;
|
||||
|
||||
set menuBackgroundRgb(int value) {
|
||||
_menuBackgroundRgb = value;
|
||||
}
|
||||
|
||||
/// Immutable snapshot of the current main-menu rows.
|
||||
List<WolfMenuMainEntry> get mainMenuEntries {
|
||||
return List<WolfMenuMainEntry>.unmodifiable(
|
||||
<WolfMenuMainEntry>[
|
||||
_mainMenuEntry(
|
||||
action: WolfMenuMainAction.newGame,
|
||||
label: 'NEW GAME',
|
||||
),
|
||||
_mainMenuEntry(
|
||||
action: WolfMenuMainAction.sound,
|
||||
label: 'SOUND',
|
||||
),
|
||||
_mainMenuEntry(
|
||||
action: WolfMenuMainAction.control,
|
||||
label: 'CONTROL',
|
||||
),
|
||||
_mainMenuEntry(
|
||||
action: WolfMenuMainAction.loadGame,
|
||||
label: 'LOAD GAME',
|
||||
),
|
||||
_mainMenuEntry(
|
||||
action: WolfMenuMainAction.saveGame,
|
||||
label: 'SAVE GAME',
|
||||
),
|
||||
_mainMenuEntry(
|
||||
action: WolfMenuMainAction.changeView,
|
||||
label: 'CHANGE VIEW',
|
||||
),
|
||||
_mainMenuEntry(
|
||||
action: WolfMenuMainAction.readThis,
|
||||
label: 'READ THIS!',
|
||||
),
|
||||
_mainMenuEntry(
|
||||
action: _showResumeOption
|
||||
? WolfMenuMainAction.endGame
|
||||
: WolfMenuMainAction.viewScores,
|
||||
label: _showResumeOption ? 'END GAME' : 'VIEW SCORES',
|
||||
),
|
||||
_mainMenuEntry(
|
||||
action: _showResumeOption
|
||||
? WolfMenuMainAction.backToGame
|
||||
: WolfMenuMainAction.backToDemo,
|
||||
label: _showResumeOption ? 'BACK TO GAME' : 'BACK TO DEMO',
|
||||
),
|
||||
_mainMenuEntry(action: WolfMenuMainAction.quit, label: 'QUIT'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether the main menu can return to the game-selection step.
|
||||
bool get canGoBackToGameSelection => !_showResumeOption && _gameCount > 1;
|
||||
|
||||
/// Resets state for a fresh difficulty-only selection flow.
|
||||
void beginDifficultySelection({Difficulty? initialDifficulty}) {
|
||||
beginSelectionFlow(
|
||||
gameCount: 1,
|
||||
initialGameIndex: 0,
|
||||
initialEpisodeIndex: 0,
|
||||
initialDifficulty: initialDifficulty,
|
||||
);
|
||||
_activeMenu = WolfMenuScreen.difficultySelect;
|
||||
}
|
||||
|
||||
/// Rebuilds the main menu for the current runtime state.
|
||||
void showMainMenu({
|
||||
required bool hasResumableGame,
|
||||
bool? hasLoadableSave,
|
||||
}) {
|
||||
_showResumeOption = hasResumableGame;
|
||||
if (hasLoadableSave != null) {
|
||||
_hasLoadableSave = hasLoadableSave;
|
||||
}
|
||||
final int itemCount = mainMenuEntries.length;
|
||||
if (itemCount == 0) {
|
||||
_selectedMainIndex = 0;
|
||||
} else {
|
||||
_selectedMainIndex = _defaultMainMenuIndex();
|
||||
}
|
||||
_activeMenu = WolfMenuScreen.mainMenu;
|
||||
_transitionTarget = null;
|
||||
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
_introEffect = WolfTransitionEffect.normalFade;
|
||||
_introElapsedMs = 0;
|
||||
resetEdgeState();
|
||||
}
|
||||
|
||||
/// Updates whether the LOAD GAME row is selectable.
|
||||
void setLoadGameAvailable(bool isAvailable) {
|
||||
if (_hasLoadableSave == isAvailable) {
|
||||
return;
|
||||
}
|
||||
_hasLoadableSave = isAvailable;
|
||||
final int itemCount = mainMenuEntries.length;
|
||||
if (itemCount <= 0 || !_isSelectableMainIndex(_selectedMainIndex)) {
|
||||
_selectedMainIndex = findSelectableIndex(
|
||||
clampIndex(_selectedMainIndex, itemCount),
|
||||
itemCount,
|
||||
_isSelectableMainIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the renderer rows displayed in the change-view menu.
|
||||
void setChangeViewEntries(List<WolfMenuRendererEntry> entries) {
|
||||
final WolfRendererMode? previouslySelectedMode =
|
||||
(_selectedChangeViewIndex >= 0 &&
|
||||
_selectedChangeViewIndex < _changeViewEntries.length)
|
||||
? _changeViewEntries[_selectedChangeViewIndex].mode
|
||||
: null;
|
||||
|
||||
_changeViewEntries = List<WolfMenuRendererEntry>.unmodifiable(entries);
|
||||
|
||||
final int itemCount = changeViewItemCount;
|
||||
if (itemCount == 0) {
|
||||
_selectedChangeViewIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (previouslySelectedMode != null) {
|
||||
final int modeIndex = _changeViewEntries.indexWhere(
|
||||
(entry) => entry.mode == previouslySelectedMode,
|
||||
);
|
||||
if (modeIndex >= 0 && _isSelectableChangeViewIndex(modeIndex)) {
|
||||
_selectedChangeViewIndex = modeIndex;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_selectedChangeViewIndex = findSelectableIndex(
|
||||
clampIndex(_selectedChangeViewIndex, itemCount),
|
||||
itemCount,
|
||||
_isSelectableChangeViewIndex,
|
||||
);
|
||||
}
|
||||
|
||||
/// Replaces the renderer-specific option rows displayed in the customize menu.
|
||||
void setRendererOptionEntries({
|
||||
required String title,
|
||||
required List<WolfMenuRendererOptionEntry> entries,
|
||||
}) {
|
||||
final bool wasSelectingOption =
|
||||
_selectedChangeViewIndex >= _changeViewEntries.length;
|
||||
final WolfRendererOptionId? previousOption =
|
||||
(_selectedRendererOptionIndex >= 0 &&
|
||||
_selectedRendererOptionIndex < _rendererOptionEntries.length)
|
||||
? _rendererOptionEntries[_selectedRendererOptionIndex].id
|
||||
: null;
|
||||
|
||||
_rendererOptionsTitle = title;
|
||||
_rendererOptionEntries = List<WolfMenuRendererOptionEntry>.unmodifiable(
|
||||
entries,
|
||||
);
|
||||
|
||||
final int totalCount = changeViewItemCount;
|
||||
if (_rendererOptionEntries.isEmpty || totalCount == 0) {
|
||||
_selectedRendererOptionIndex = 0;
|
||||
if (_changeViewEntries.isNotEmpty) {
|
||||
_selectedChangeViewIndex = findSelectableIndex(
|
||||
0,
|
||||
_changeViewEntries.length,
|
||||
_isSelectableChangeViewIndex,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousOption != null) {
|
||||
final int previousIndex = _rendererOptionEntries.indexWhere(
|
||||
(entry) => entry.id == previousOption,
|
||||
);
|
||||
if (previousIndex >= 0 &&
|
||||
_isSelectableRendererOptionIndex(previousIndex)) {
|
||||
_selectedRendererOptionIndex = previousIndex;
|
||||
if (wasSelectingOption) {
|
||||
_selectedChangeViewIndex = _changeViewEntries.length + previousIndex;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_selectedRendererOptionIndex = findSelectableIndex(
|
||||
clampIndex(_selectedRendererOptionIndex, _rendererOptionEntries.length),
|
||||
_rendererOptionEntries.length,
|
||||
_isSelectableRendererOptionIndex,
|
||||
);
|
||||
|
||||
if (wasSelectingOption) {
|
||||
_selectedChangeViewIndex =
|
||||
_changeViewEntries.length + _selectedRendererOptionIndex;
|
||||
} else {
|
||||
_selectedChangeViewIndex = findSelectableIndex(
|
||||
clampIndex(_selectedChangeViewIndex, totalCount),
|
||||
totalCount,
|
||||
_isSelectableChangeViewIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Switches the active menu to the renderer-selection screen.
|
||||
void showChangeViewMenu() {
|
||||
_activeMenu = WolfMenuScreen.changeView;
|
||||
_selectedChangeViewIndex = changeViewItemCount == 0
|
||||
? 0
|
||||
: findSelectableIndex(
|
||||
0,
|
||||
changeViewItemCount,
|
||||
_isSelectableChangeViewIndex,
|
||||
);
|
||||
_transitionTarget = null;
|
||||
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
resetEdgeState();
|
||||
}
|
||||
|
||||
/// Switches the active menu to the renderer-options screen.
|
||||
void showRendererOptionsMenu() {
|
||||
_activeMenu = WolfMenuScreen.rendererOptions;
|
||||
_selectedRendererOptionIndex = _rendererOptionEntries.isEmpty
|
||||
? 0
|
||||
: findSelectableIndex(
|
||||
0,
|
||||
_rendererOptionEntries.length,
|
||||
_isSelectableRendererOptionIndex,
|
||||
);
|
||||
_transitionTarget = null;
|
||||
_transitionEffect = WolfTransitionEffect.normalFade;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
resetEdgeState();
|
||||
}
|
||||
|
||||
/// Clears the selected episode back to the first row.
|
||||
void clearEpisodeSelection() {
|
||||
_selectedEpisodeIndex = 0;
|
||||
}
|
||||
|
||||
/// Consumes the current input snapshot as the new edge baseline.
|
||||
void absorbInputState(EngineInput input) {
|
||||
consumeEdgeState(input);
|
||||
}
|
||||
|
||||
/// Stores the current episode selection, clamped to the available row count.
|
||||
void setSelectedEpisodeIndex(int index, int episodeCount) {
|
||||
_selectedEpisodeIndex = clampIndex(index, episodeCount);
|
||||
}
|
||||
|
||||
/// Stores the current game selection, clamped to the available row count.
|
||||
void setSelectedGameIndex(int index, int gameCount) {
|
||||
_selectedGameIndex = clampIndex(index, gameCount);
|
||||
}
|
||||
|
||||
@override
|
||||
int _defaultMainMenuIndex() {
|
||||
final WolfMenuMainAction target = _showResumeOption
|
||||
? WolfMenuMainAction.backToGame
|
||||
: WolfMenuMainAction.newGame;
|
||||
final int found = mainMenuEntries.indexWhere(
|
||||
(entry) => entry.action == target,
|
||||
);
|
||||
return found >= 0
|
||||
? found
|
||||
: findSelectableIndex(
|
||||
0,
|
||||
mainMenuEntries.length,
|
||||
_isSelectableMainIndex,
|
||||
);
|
||||
}
|
||||
|
||||
bool _isSelectableMainIndex(int index) {
|
||||
if (index < 0 || index >= mainMenuEntries.length) {
|
||||
return false;
|
||||
}
|
||||
return mainMenuEntries[index].isEnabled;
|
||||
}
|
||||
|
||||
bool _isSelectableChangeViewIndex(int index) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
if (index < _changeViewEntries.length) {
|
||||
return _changeViewEntries[index].isEnabled;
|
||||
}
|
||||
final int optionIndex = index - _changeViewEntries.length;
|
||||
if (optionIndex >= 0 && optionIndex < _rendererOptionEntries.length) {
|
||||
return _rendererOptionEntries[optionIndex].isEnabled;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _isSelectableRendererOptionIndex(int index) {
|
||||
if (index < 0 || index >= _rendererOptionEntries.length) {
|
||||
return false;
|
||||
}
|
||||
return _rendererOptionEntries[index].isEnabled;
|
||||
}
|
||||
|
||||
bool _isWiredMainMenuAction(WolfMenuMainAction action) {
|
||||
switch (action) {
|
||||
case WolfMenuMainAction.newGame:
|
||||
case WolfMenuMainAction.loadGame:
|
||||
case WolfMenuMainAction.saveGame:
|
||||
case WolfMenuMainAction.endGame:
|
||||
case WolfMenuMainAction.backToGame:
|
||||
case WolfMenuMainAction.backToDemo:
|
||||
case WolfMenuMainAction.quit:
|
||||
case WolfMenuMainAction.changeView:
|
||||
return true;
|
||||
case WolfMenuMainAction.sound:
|
||||
case WolfMenuMainAction.control:
|
||||
case WolfMenuMainAction.readThis:
|
||||
case WolfMenuMainAction.viewScores:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
WolfMenuMainEntry _mainMenuEntry({
|
||||
required WolfMenuMainAction action,
|
||||
required String label,
|
||||
}) {
|
||||
bool isEnabled = _isWiredMainMenuAction(action);
|
||||
if (action == WolfMenuMainAction.loadGame) {
|
||||
isEnabled = isEnabled && _hasLoadableSave;
|
||||
}
|
||||
if (action == WolfMenuMainAction.saveGame) {
|
||||
isEnabled = isEnabled && _showResumeOption;
|
||||
}
|
||||
|
||||
return WolfMenuMainEntry(
|
||||
action: action,
|
||||
label: label,
|
||||
isEnabled: isEnabled,
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
/// Known VGA picture indexes used by the original Wolf3D control-panel menus.
|
||||
///
|
||||
/// Values below are picture-table indexes (not raw chunk ids).
|
||||
/// For example, `C_CONTROLPIC` is chunk 26 in `GFXV_WL6.H`, so its picture
|
||||
/// index is `26 - STARTPICS(3) = 23`.
|
||||
abstract class WolfMenuPic {
|
||||
static const int hBj = 0; // H_BJPIC
|
||||
static const int hTopWindow = 3; // H_TOPWINDOWPIC
|
||||
static const int cOptions = 7; // C_OPTIONSPIC
|
||||
static const int cCursor1 = 8; // C_CURSOR1PIC
|
||||
static const int cCursor2 = 9; // C_CURSOR2PIC
|
||||
static const int cNotSelected = 10; // C_NOTSELECTEDPIC
|
||||
static const int cSelected = 11; // C_SELECTEDPIC
|
||||
static const int cBabyMode = 16; // C_BABYMODEPIC
|
||||
static const int cEasy = 17; // C_EASYPIC
|
||||
static const int cNormal = 18; // C_NORMALPIC
|
||||
static const int cHard = 19; // C_HARDPIC
|
||||
static const int cControl = 23; // C_CONTROLPIC
|
||||
static const int cCustomize = 24; // C_CUSTOMIZEPIC
|
||||
static const int cEpisode1 = 27; // C_EPISODE1PIC
|
||||
static const int cEpisode2 = 28; // C_EPISODE2PIC
|
||||
static const int cEpisode3 = 29; // C_EPISODE3PIC
|
||||
static const int cEpisode4 = 30; // C_EPISODE4PIC
|
||||
static const int cEpisode5 = 31; // C_EPISODE5PIC
|
||||
static const int cEpisode6 = 32; // C_EPISODE6PIC
|
||||
static const int statusBar = 83; // STATUSBARPIC
|
||||
static const int title = 84; // TITLEPIC
|
||||
static const int pg13 = 85; // PG13PIC
|
||||
static const int credits = 86; // CREDITSPIC
|
||||
static const int highScores = 87; // HIGHSCORESPIC
|
||||
|
||||
static const List<int> episodePics = [
|
||||
cEpisode1,
|
||||
cEpisode2,
|
||||
cEpisode3,
|
||||
cEpisode4,
|
||||
cEpisode5,
|
||||
cEpisode6,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/classic_menu_presentation_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/spear_menu_presentation_module.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
/// Bound access to the active menu presentation for a loaded data set.
|
||||
///
|
||||
/// Renderers should construct this from [WolfensteinData] and consume its
|
||||
/// color and art accessors instead of hard-coding variant-specific menu rules.
|
||||
class WolfMenuPresentation {
|
||||
/// Loaded game data used to resolve art assets.
|
||||
///
|
||||
/// This is `null` for host-owned fallback presentations such as setup
|
||||
/// screens that need menu colors before any game data has been loaded.
|
||||
final WolfensteinData? data;
|
||||
|
||||
/// Presentation module that supplies colors and symbolic art lookups.
|
||||
final MenuPresentationModule _module;
|
||||
|
||||
/// Binds the active menu presentation from [data.registry].
|
||||
factory WolfMenuPresentation(WolfensteinData data) {
|
||||
return WolfMenuPresentation.module(
|
||||
data.registry.menuPresentation,
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a presentation from an explicit module without loaded game data.
|
||||
///
|
||||
/// This is useful for host UI that wants menu-consistent colors before any
|
||||
/// game assets have been discovered.
|
||||
const WolfMenuPresentation.module(this._module, {this.data});
|
||||
|
||||
/// Classic fallback presentation for host-owned UI outside a loaded game.
|
||||
const WolfMenuPresentation.classic()
|
||||
: this.module(const ClassicMenuPresentationModule());
|
||||
|
||||
/// Spear fallback presentation for host-owned UI outside a loaded game.
|
||||
const WolfMenuPresentation.spear()
|
||||
: this.module(const SpearMenuPresentationModule());
|
||||
|
||||
/// VGA palette index used for menu background fills and header band accents.
|
||||
int get backgroundIndex => _module.backgroundIndex;
|
||||
|
||||
/// VGA palette index used for menu panel fills.
|
||||
int get panelIndex => _module.panelIndex;
|
||||
|
||||
/// VGA palette index used for menu panel borders and separators.
|
||||
int get borderIndex => _module.borderIndex;
|
||||
|
||||
/// VGA palette index used for emphasized or affirmative UI text.
|
||||
int get emphasisIndex => _module.emphasisIndex;
|
||||
|
||||
/// VGA palette index used for warnings and cautionary text.
|
||||
int get warningIndex => _module.warningIndex;
|
||||
|
||||
/// VGA palette index used for subdued or de-emphasized text.
|
||||
int get mutedIndex => _module.mutedIndex;
|
||||
|
||||
/// VGA palette index used for the selected menu row text.
|
||||
int get selectedTextIndex => _module.selectedTextIndex;
|
||||
|
||||
/// VGA palette index used for normal menu row text.
|
||||
int get unselectedTextIndex => _module.unselectedTextIndex;
|
||||
|
||||
/// VGA palette index used for disabled menu row text.
|
||||
int get disabledTextIndex => _module.disabledTextIndex;
|
||||
|
||||
/// VGA palette index used for headings.
|
||||
int get headerTextIndex => _module.headerTextIndex;
|
||||
|
||||
/// Background color resolved to `0xAARRGGBB`.
|
||||
int get backgroundColor => ColorPalette.argbFromVgaIndex(backgroundIndex);
|
||||
|
||||
/// Panel fill color resolved to `0xAARRGGBB`.
|
||||
int get panelColor => ColorPalette.argbFromVgaIndex(panelIndex);
|
||||
|
||||
/// Border color resolved to `0xAARRGGBB`.
|
||||
int get borderColor => ColorPalette.argbFromVgaIndex(borderIndex);
|
||||
|
||||
/// Heading color resolved to `0xAARRGGBB`.
|
||||
int get titleColor => ColorPalette.argbFromVgaIndex(headerTextIndex);
|
||||
|
||||
/// Standard body text color resolved to `0xAARRGGBB`.
|
||||
int get bodyColor => ColorPalette.argbFromVgaIndex(unselectedTextIndex);
|
||||
|
||||
/// Emphasis color resolved to `0xAARRGGBB`.
|
||||
int get emphasisColor => ColorPalette.argbFromVgaIndex(emphasisIndex);
|
||||
|
||||
/// Warning color resolved to `0xAARRGGBB`.
|
||||
int get warningColor => ColorPalette.argbFromVgaIndex(warningIndex);
|
||||
|
||||
/// Muted color resolved to `0xAARRGGBB`.
|
||||
int get mutedColor => ColorPalette.argbFromVgaIndex(mutedIndex);
|
||||
|
||||
/// Selected text color resolved through the VGA 32-bit table.
|
||||
int get selectedTextColor => ColorPalette.vga32Bit[selectedTextIndex];
|
||||
|
||||
/// Normal text color resolved through the VGA 32-bit table.
|
||||
int get unselectedTextColor => ColorPalette.vga32Bit[unselectedTextIndex];
|
||||
|
||||
/// Disabled text color resolved through the VGA 32-bit table.
|
||||
int get disabledTextColor => ColorPalette.vga32Bit[disabledTextIndex];
|
||||
|
||||
/// Heading text color resolved through the VGA 32-bit table.
|
||||
int get headerTextColor => ColorPalette.vga32Bit[headerTextIndex];
|
||||
|
||||
/// Background image used by the controls/customize panel, if available.
|
||||
VgaImage? get controlBackground =>
|
||||
data == null ? null : _module.controlBackground(data!);
|
||||
|
||||
/// Title splash image, if this presentation exposes one.
|
||||
VgaImage? get title => data == null ? null : _module.title(data!);
|
||||
|
||||
/// Main menu heading art, if available.
|
||||
VgaImage? get heading => data == null ? null : _module.heading(data!);
|
||||
|
||||
/// Selected checkbox or marker image, if available.
|
||||
VgaImage? get selectedMarker =>
|
||||
data == null ? null : _module.selectedMarker(data!);
|
||||
|
||||
/// Unselected checkbox or marker image, if available.
|
||||
VgaImage? get unselectedMarker =>
|
||||
data == null ? null : _module.unselectedMarker(data!);
|
||||
|
||||
/// Main menu options banner image, if available.
|
||||
VgaImage? get optionsLabel =>
|
||||
data == null ? null : _module.optionsLabel(data!);
|
||||
|
||||
/// Customize/options heading image, if available.
|
||||
VgaImage? get customizeLabel =>
|
||||
data == null ? null : _module.customizeLabel(data!);
|
||||
|
||||
/// Credits image, if available.
|
||||
VgaImage? get credits => data == null ? null : _module.credits(data!);
|
||||
|
||||
/// Episode selection art for the zero-based [episodeIndex], if available.
|
||||
VgaImage? episodeOption(int episodeIndex) {
|
||||
return data == null ? null : _module.episodeOption(data!, episodeIndex);
|
||||
}
|
||||
|
||||
/// Difficulty selection art for [difficulty], if available.
|
||||
VgaImage? difficultyOption(Difficulty difficulty) {
|
||||
return data == null ? null : _module.difficultyOption(data!, difficulty);
|
||||
}
|
||||
|
||||
/// Legacy numeric art lookup for classic renderer code paths.
|
||||
///
|
||||
/// Returns `null` when no loaded data is attached or when the requested art
|
||||
/// does not exist in the active presentation.
|
||||
VgaImage? mappedPic(int index) {
|
||||
return data == null ? null : _module.mappedPic(data!, index);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:wolf_3d_dart/src/registry/modules/entity_asset_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/modules/hud_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/modules/menu_pic_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/modules/menu_presentation_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/modules/music_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/modules/sfx_module.dart';
|
||||
|
||||
@@ -14,9 +15,10 @@ import 'package:wolf_3d_dart/src/registry/modules/sfx_module.dart';
|
||||
/// data.registry.entities.resolve(EntityKey.guard)
|
||||
/// data.registry.hud.faceForHealth(player.health)
|
||||
/// data.registry.menu.resolve(MenuPicKey.title)
|
||||
/// data.registry.menuPresentation.headerTextIndex
|
||||
/// ```
|
||||
///
|
||||
/// To provide a fully custom asset layout, implement all five module
|
||||
/// To provide a fully custom asset layout, implement all six module
|
||||
/// interfaces and pass them to this constructor, then supply the resulting
|
||||
/// [AssetRegistry] to [WolfensteinLoader.loadFromBytes].
|
||||
class AssetRegistry {
|
||||
@@ -26,6 +28,7 @@ class AssetRegistry {
|
||||
required this.entities,
|
||||
required this.hud,
|
||||
required this.menu,
|
||||
required this.menuPresentation,
|
||||
});
|
||||
|
||||
/// Sound-effect slot resolution.
|
||||
@@ -42,4 +45,7 @@ class AssetRegistry {
|
||||
|
||||
/// Menu VGA picture index resolution.
|
||||
final MenuPicModule menu;
|
||||
|
||||
/// Menu presentation and color routing.
|
||||
final MenuPresentationModule menuPresentation;
|
||||
}
|
||||
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
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/image.dart';
|
||||
import 'package:wolf_3d_dart/src/data_types/wolfenstein_data.dart';
|
||||
import 'package:wolf_3d_dart/src/menu/wolf_menu_pic.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/keys/menu_pic_key.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/modules/menu_presentation_module.dart';
|
||||
|
||||
/// Built-in menu presentation that mirrors the classic Wolf3D UI.
|
||||
class ClassicMenuPresentationModule extends MenuPresentationModule {
|
||||
/// Creates the classic Wolf3D menu presentation.
|
||||
const ClassicMenuPresentationModule();
|
||||
|
||||
/// Approximate RGB target used to derive the classic menu heading color.
|
||||
static const int _headerTargetRgb = 0xFFF700;
|
||||
|
||||
/// Classic VGA palette index for menu background fills.
|
||||
@override
|
||||
int get backgroundIndex => 111;
|
||||
|
||||
/// Classic VGA palette index for panel fills.
|
||||
@override
|
||||
int get panelIndex => 103;
|
||||
|
||||
/// Classic VGA palette index for panel borders.
|
||||
@override
|
||||
int get borderIndex => 87;
|
||||
|
||||
/// Classic VGA palette index for emphasis text.
|
||||
@override
|
||||
int get emphasisIndex => 10;
|
||||
|
||||
/// Classic VGA palette index for warnings.
|
||||
@override
|
||||
int get warningIndex => 14;
|
||||
|
||||
/// Classic VGA palette index for muted text.
|
||||
@override
|
||||
int get mutedIndex => 8;
|
||||
|
||||
/// Classic VGA palette index for selected row text.
|
||||
@override
|
||||
int get selectedTextIndex => 19;
|
||||
|
||||
/// Classic VGA palette index for unselected row text.
|
||||
@override
|
||||
int get unselectedTextIndex => 23;
|
||||
|
||||
/// Classic VGA palette index for disabled row text.
|
||||
@override
|
||||
int get disabledTextIndex => 4;
|
||||
|
||||
/// Classic heading palette index computed from the target yellow tone.
|
||||
@override
|
||||
int get headerTextIndex =>
|
||||
ColorPalette.findClosestPaletteIndex(_headerTargetRgb);
|
||||
|
||||
/// Controls/customize panel background art.
|
||||
@override
|
||||
VgaImage? controlBackground(WolfensteinData data) =>
|
||||
_imageForKey(data, MenuPicKey.controlBackground);
|
||||
|
||||
/// Title splash art.
|
||||
@override
|
||||
VgaImage? title(WolfensteinData data) => _imageForKey(data, MenuPicKey.title);
|
||||
|
||||
/// Main menu heading art.
|
||||
@override
|
||||
VgaImage? heading(WolfensteinData data) =>
|
||||
_imageForKey(data, MenuPicKey.heading);
|
||||
|
||||
/// Selected marker art.
|
||||
@override
|
||||
VgaImage? selectedMarker(WolfensteinData data) =>
|
||||
_imageForKey(data, MenuPicKey.markerSelected);
|
||||
|
||||
/// Unselected marker art.
|
||||
@override
|
||||
VgaImage? unselectedMarker(WolfensteinData data) =>
|
||||
_imageForKey(data, MenuPicKey.markerUnselected);
|
||||
|
||||
/// Main options banner art.
|
||||
@override
|
||||
VgaImage? optionsLabel(WolfensteinData data) =>
|
||||
_imageForKey(data, MenuPicKey.optionsLabel);
|
||||
|
||||
/// Customize heading art.
|
||||
@override
|
||||
VgaImage? customizeLabel(WolfensteinData data) =>
|
||||
_imageForKey(data, MenuPicKey.customizeLabel);
|
||||
|
||||
/// Credits art.
|
||||
@override
|
||||
VgaImage? credits(WolfensteinData data) =>
|
||||
_imageForKey(data, MenuPicKey.credits);
|
||||
|
||||
/// Episode selection art resolved through the active registry mapping.
|
||||
@override
|
||||
VgaImage? episodeOption(WolfensteinData data, int episodeIndex) {
|
||||
if (episodeIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
return _imageForKey(data, data.registry.menu.episodeKey(episodeIndex));
|
||||
}
|
||||
|
||||
/// Difficulty art resolved through the active registry mapping.
|
||||
@override
|
||||
VgaImage? difficultyOption(WolfensteinData data, Difficulty difficulty) {
|
||||
return _imageForKey(data, data.registry.menu.difficultyKey(difficulty));
|
||||
}
|
||||
|
||||
/// Resolves legacy numeric IDs through symbolic keys first.
|
||||
@override
|
||||
VgaImage? mappedPic(WolfensteinData data, int index) {
|
||||
final key = _legacyKeyForIndex(index);
|
||||
if (key != null) {
|
||||
return _imageForKey(data, key);
|
||||
}
|
||||
return _pic(data, index);
|
||||
}
|
||||
|
||||
/// Loads a symbolic menu picture through the active registry.
|
||||
VgaImage? _imageForKey(WolfensteinData data, MenuPicKey key) {
|
||||
final ref = data.registry.menu.resolve(key);
|
||||
if (ref == null) {
|
||||
return null;
|
||||
}
|
||||
return _pic(data, ref.pictureIndex);
|
||||
}
|
||||
|
||||
/// Safely returns the VGA image at [index] when it contains usable pixels.
|
||||
VgaImage? _pic(WolfensteinData data, int index) {
|
||||
if (index < 0 || index >= data.vgaImages.length) {
|
||||
return null;
|
||||
}
|
||||
final image = data.vgaImages[index];
|
||||
if (image.width <= 0 || image.height <= 0) {
|
||||
return null;
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
/// Maps classic numeric menu picture IDs to symbolic menu keys.
|
||||
///
|
||||
/// This preserves the old renderer-facing numbering scheme while routing the
|
||||
/// actual picture resolution through the registry layer.
|
||||
MenuPicKey? _legacyKeyForIndex(int index) {
|
||||
switch (index) {
|
||||
case WolfMenuPic.hTopWindow:
|
||||
return MenuPicKey.heading;
|
||||
case WolfMenuPic.cOptions:
|
||||
return MenuPicKey.optionsLabel;
|
||||
case WolfMenuPic.cCursor1:
|
||||
return MenuPicKey.cursorActive;
|
||||
case WolfMenuPic.cCursor2:
|
||||
return MenuPicKey.cursorInactive;
|
||||
case WolfMenuPic.cNotSelected:
|
||||
return MenuPicKey.markerUnselected;
|
||||
case WolfMenuPic.cSelected:
|
||||
return MenuPicKey.markerSelected;
|
||||
case 15:
|
||||
return MenuPicKey.footer;
|
||||
case WolfMenuPic.cBabyMode:
|
||||
return MenuPicKey.difficultyBaby;
|
||||
case WolfMenuPic.cEasy:
|
||||
return MenuPicKey.difficultyEasy;
|
||||
case WolfMenuPic.cNormal:
|
||||
return MenuPicKey.difficultyNormal;
|
||||
case WolfMenuPic.cHard:
|
||||
return MenuPicKey.difficultyHard;
|
||||
case WolfMenuPic.cControl:
|
||||
return MenuPicKey.controlBackground;
|
||||
case WolfMenuPic.cCustomize:
|
||||
return MenuPicKey.customizeLabel;
|
||||
case WolfMenuPic.cEpisode1:
|
||||
return MenuPicKey.episode1;
|
||||
case WolfMenuPic.cEpisode2:
|
||||
return MenuPicKey.episode2;
|
||||
case WolfMenuPic.cEpisode3:
|
||||
return MenuPicKey.episode3;
|
||||
case WolfMenuPic.cEpisode4:
|
||||
return MenuPicKey.episode4;
|
||||
case WolfMenuPic.cEpisode5:
|
||||
return MenuPicKey.episode5;
|
||||
case WolfMenuPic.cEpisode6:
|
||||
return MenuPicKey.episode6;
|
||||
case WolfMenuPic.title:
|
||||
return MenuPicKey.title;
|
||||
case WolfMenuPic.pg13:
|
||||
return MenuPicKey.pg13;
|
||||
case WolfMenuPic.credits:
|
||||
return MenuPicKey.credits;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ 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/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/retail_entity_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/retail_hud_module.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/retail_menu_module.dart';
|
||||
@@ -19,5 +20,6 @@ class RetailAssetRegistry extends AssetRegistry {
|
||||
entities: const RetailEntityModule(),
|
||||
hud: const RetailHudModule(),
|
||||
menu: const RetailMenuPicModule(),
|
||||
menuPresentation: const ClassicMenuPresentationModule(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ 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/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/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/shareware_menu_module.dart';
|
||||
@@ -27,6 +28,7 @@ class SharewareAssetRegistry extends AssetRegistry {
|
||||
menu: SharewareMenuPicModule(
|
||||
useOriginalWl1Map: strictOriginalShareware,
|
||||
),
|
||||
menuPresentation: const ClassicMenuPresentationModule(),
|
||||
);
|
||||
|
||||
/// Convenience accessor to the menu module for post-load initialisation.
|
||||
|
||||
@@ -6,9 +6,9 @@ import 'package:wolf_3d_dart/src/registry/modules/menu_pic_module.dart';
|
||||
///
|
||||
/// Shareware VGAGRAPH contains fewer pictures than the retail version, so
|
||||
/// the episode/difficulty/control-panel art sits at a shifted position in
|
||||
/// the VGA image list. The exact shift is computed at resolve time by
|
||||
/// the VGA image list. The exact shift is computed at resolve time by
|
||||
/// scanning the loaded image list for the landmark STATUSBARPIC, mirroring
|
||||
/// the runtime heuristic in the original [WolfClassicMenuArt._indexOffset].
|
||||
/// the same runtime heuristic used by the built-in classic menu presentation.
|
||||
///
|
||||
/// Offset determination is deferred until the first [resolve] call and
|
||||
/// cached for subsequent lookups. If the landmark cannot be found the
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:wolf_3d_dart/src/registry/built_in/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/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/spear_menu_presentation_module.dart';
|
||||
|
||||
/// Built-in [AssetRegistry] for Spear of Destiny demo/shareware (`.SDM`).
|
||||
class SpearDemoAssetRegistry extends AssetRegistry {
|
||||
@@ -15,5 +16,6 @@ class SpearDemoAssetRegistry extends AssetRegistry {
|
||||
entities: const SpearDemoEntityModule(),
|
||||
hud: const SpearDemoHudModule(),
|
||||
menu: const SpearDemoMenuPicModule(),
|
||||
menuPresentation: const SpearMenuPresentationModule(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
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/wolfenstein_data.dart';
|
||||
|
||||
/// Provides the visual presentation for Wolf3D menus.
|
||||
///
|
||||
/// A presentation module owns both menu text colors and the symbolic art
|
||||
/// lookups needed by renderers. Pair it with a [MenuPicModule] inside an
|
||||
/// [AssetRegistry] to support built-in variants or fully custom user-defined
|
||||
/// menus.
|
||||
abstract class MenuPresentationModule {
|
||||
/// Creates a menu presentation module.
|
||||
const MenuPresentationModule();
|
||||
|
||||
/// VGA palette index used for menu background fills and header band accents.
|
||||
int get backgroundIndex;
|
||||
|
||||
/// VGA palette index used for menu panel fills.
|
||||
int get panelIndex;
|
||||
|
||||
/// VGA palette index used for panel borders and separators.
|
||||
int get borderIndex;
|
||||
|
||||
/// VGA palette index used for emphasized UI text.
|
||||
int get emphasisIndex;
|
||||
|
||||
/// VGA palette index used for warnings and cautionary text.
|
||||
int get warningIndex;
|
||||
|
||||
/// VGA palette index used for subdued UI text.
|
||||
int get mutedIndex;
|
||||
|
||||
/// VGA palette index used for the selected menu row text.
|
||||
int get selectedTextIndex;
|
||||
|
||||
/// VGA palette index used for normal menu row text.
|
||||
int get unselectedTextIndex;
|
||||
|
||||
/// VGA palette index used for disabled menu row text.
|
||||
int get disabledTextIndex;
|
||||
|
||||
/// VGA palette index used for headings and title text.
|
||||
int get headerTextIndex;
|
||||
|
||||
/// Returns the controls/customize panel background image, if supported.
|
||||
VgaImage? controlBackground(WolfensteinData data);
|
||||
|
||||
/// Returns the title splash image, if supported.
|
||||
VgaImage? title(WolfensteinData data);
|
||||
|
||||
/// Returns the primary heading art for the main menu, if supported.
|
||||
VgaImage? heading(WolfensteinData data);
|
||||
|
||||
/// Returns the selected marker image, if supported.
|
||||
VgaImage? selectedMarker(WolfensteinData data);
|
||||
|
||||
/// Returns the unselected marker image, if supported.
|
||||
VgaImage? unselectedMarker(WolfensteinData data);
|
||||
|
||||
/// Returns the main options banner image, if supported.
|
||||
VgaImage? optionsLabel(WolfensteinData data);
|
||||
|
||||
/// Returns the customize/options heading image, if supported.
|
||||
VgaImage? customizeLabel(WolfensteinData data);
|
||||
|
||||
/// Returns the credits image, if supported.
|
||||
VgaImage? credits(WolfensteinData data);
|
||||
|
||||
/// Returns episode selection art for zero-based [episodeIndex], if supported.
|
||||
VgaImage? episodeOption(WolfensteinData data, int episodeIndex);
|
||||
|
||||
/// Returns difficulty selection art for [difficulty], if supported.
|
||||
VgaImage? difficultyOption(WolfensteinData data, Difficulty difficulty);
|
||||
|
||||
/// Legacy numeric lookup retained for renderer code that still reasons in
|
||||
/// original VGA picture IDs.
|
||||
VgaImage? mappedPic(WolfensteinData data, int index);
|
||||
}
|
||||
@@ -546,10 +546,11 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
engine.menuManager.menuBackgroundRgb,
|
||||
);
|
||||
final int panelColor = _rgbToPaletteColor(engine.menuPanelRgb);
|
||||
final int headingColor = WolfMenuPalette.headerTextColor;
|
||||
final int selectedTextColor = WolfMenuPalette.selectedTextColor;
|
||||
final int unselectedTextColor = WolfMenuPalette.unselectedTextColor;
|
||||
final int disabledTextColor = WolfMenuPalette.disabledTextColor;
|
||||
final menu = WolfMenuPresentation(engine.data);
|
||||
final int headingColor = menu.headerTextColor;
|
||||
final int selectedTextColor = menu.selectedTextColor;
|
||||
final int unselectedTextColor = menu.unselectedTextColor;
|
||||
final int disabledTextColor = menu.disabledTextColor;
|
||||
final _AsciiMenuTypography menuTypography = _resolveMenuTypography();
|
||||
|
||||
if (_usesTerminalLayout) {
|
||||
@@ -558,21 +559,20 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
_fillRect(0, 0, width, height, activeTheme.solid, bgColor);
|
||||
}
|
||||
|
||||
final art = WolfClassicMenuArt(engine.data);
|
||||
final optionsLabel = art.optionsLabel;
|
||||
final optionsLabel = menu.optionsLabel;
|
||||
if (optionsLabel != null) {
|
||||
_mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
|
||||
}
|
||||
|
||||
if (engine.menuManager.activeMenu == WolfMenuScreen.introSplash) {
|
||||
_drawIntroSplash(engine, art, menuTypography);
|
||||
_drawIntroSplash(engine, menu, menuTypography);
|
||||
return;
|
||||
}
|
||||
|
||||
if (engine.menuManager.activeMenu == WolfMenuScreen.mainMenu) {
|
||||
_fillRect320(68, 52, 178, 136, panelColor);
|
||||
|
||||
final optionsLabel = art.optionsLabel;
|
||||
final optionsLabel = menu.optionsLabel;
|
||||
if (optionsLabel != null) {
|
||||
final int optionsX = ((320 - optionsLabel.width) ~/ 2).clamp(0, 319);
|
||||
_drawMainMenuOptionsSideBars(optionsLabel, optionsX);
|
||||
@@ -591,7 +591,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
);
|
||||
}
|
||||
|
||||
final cursor = art.mappedPic(
|
||||
final cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const int rowYStart = 55;
|
||||
@@ -637,7 +637,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
);
|
||||
_fillRect320(28, 58, 264, 104, panelColor);
|
||||
|
||||
final cursor = art.mappedPic(
|
||||
final cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const int rowYStart = 84;
|
||||
@@ -687,7 +687,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
);
|
||||
_fillRect320(12, 18, 296, 168, panelColor);
|
||||
|
||||
final cursor = art.mappedPic(
|
||||
final cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const int rowYStart = 24;
|
||||
@@ -720,7 +720,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
// Keep episode icons visible in compact ASCII layouts so this screen
|
||||
// still communicates the same visual affordances as full-size menus.
|
||||
for (int i = 0; i < engine.data.episodes.length; i++) {
|
||||
final image = art.episodeOption(i);
|
||||
final image = menu.episodeOption(i);
|
||||
if (image != null) {
|
||||
_blitVgaImageAscii(image, 40, rowYStart + (i * rowStep));
|
||||
}
|
||||
@@ -751,7 +751,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
if (isSelected && cursor != null) {
|
||||
_blitVgaImageAscii(cursor, 16, y + 2);
|
||||
}
|
||||
final image = art.episodeOption(i);
|
||||
final image = menu.episodeOption(i);
|
||||
if (image != null) {
|
||||
_blitVgaImageAscii(image, 40, y);
|
||||
}
|
||||
@@ -780,12 +780,12 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
}
|
||||
|
||||
if (engine.menuManager.activeMenu == WolfMenuScreen.changeView) {
|
||||
_drawCustomizeMenuHeader(art, headingColor, bgColor);
|
||||
final cursor = art.mappedPic(
|
||||
_drawCustomizeMenuHeader(menu, headingColor, bgColor);
|
||||
final cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
final selectedMarker = art.selectedMarker;
|
||||
final unselectedMarker = art.unselectedMarker;
|
||||
final selectedMarker = menu.selectedMarker;
|
||||
final unselectedMarker = menu.unselectedMarker;
|
||||
const int rowYStart = 64;
|
||||
const int rowStep = 16;
|
||||
const int cursorX = 62;
|
||||
@@ -884,7 +884,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
}
|
||||
|
||||
if (engine.menuManager.activeMenu == WolfMenuScreen.rendererOptions) {
|
||||
_drawCustomizeMenuHeader(art, headingColor, bgColor);
|
||||
_drawCustomizeMenuHeader(menu, headingColor, bgColor);
|
||||
_fillRect320(56, 52, 208, 120, panelColor);
|
||||
_drawMenuTextCentered(
|
||||
engine.menuManager.rendererOptionsTitle,
|
||||
@@ -893,11 +893,11 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
scale: 1,
|
||||
);
|
||||
|
||||
final cursor = art.mappedPic(
|
||||
final cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
final selectedMarker = art.selectedMarker;
|
||||
final unselectedMarker = art.unselectedMarker;
|
||||
final selectedMarker = menu.selectedMarker;
|
||||
final unselectedMarker = menu.unselectedMarker;
|
||||
const int rowYStart = 68;
|
||||
const int rowStep = 20;
|
||||
const int cursorX = 62;
|
||||
@@ -947,14 +947,14 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
|
||||
_fillRect320(28, 70, 264, 82, panelColor);
|
||||
|
||||
final face = art.difficultyOption(
|
||||
final face = menu.difficultyOption(
|
||||
Difficulty.values[selectedDifficultyIndex],
|
||||
);
|
||||
if (face != null) {
|
||||
_blitVgaImageAscii(face, 28 + 264 - face.width - 18, 92);
|
||||
}
|
||||
|
||||
final cursor = art.mappedPic(
|
||||
final cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const rowYStart = 86;
|
||||
@@ -1033,13 +1033,13 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
|
||||
void _drawIntroSplash(
|
||||
WolfEngine engine,
|
||||
WolfClassicMenuArt art,
|
||||
WolfMenuPresentation menu,
|
||||
_AsciiMenuTypography menuTypography,
|
||||
) {
|
||||
final image = switch (engine.menuManager.currentIntroSlide) {
|
||||
WolfIntroSlide.retailWarning => null,
|
||||
WolfIntroSlide.pg13 => art.mappedPic(WolfMenuPic.pg13),
|
||||
WolfIntroSlide.title => art.mappedPic(WolfMenuPic.title),
|
||||
WolfIntroSlide.pg13 => menu.mappedPic(WolfMenuPic.pg13),
|
||||
WolfIntroSlide.title => menu.mappedPic(WolfMenuPic.title),
|
||||
};
|
||||
|
||||
int splashBg = _rgbToPaletteColor(engine.menuManager.menuBackgroundRgb);
|
||||
@@ -1593,7 +1593,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
}
|
||||
|
||||
void _drawCustomizeMenuHeader(
|
||||
WolfClassicMenuArt art,
|
||||
WolfMenuPresentation menu,
|
||||
int headingColor,
|
||||
int backgroundColor,
|
||||
) {
|
||||
@@ -1603,7 +1603,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
barColor: ColorPalette.vga32Bit[0],
|
||||
);
|
||||
|
||||
final VgaImage? heading = art.customizeLabel ?? art.optionsLabel;
|
||||
final VgaImage? heading = menu.customizeLabel ?? menu.optionsLabel;
|
||||
if (heading != null) {
|
||||
final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
|
||||
_blitVgaImageAscii(heading, headingX, 0);
|
||||
|
||||
@@ -476,32 +476,32 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
engine.menuManager.menuBackgroundRgb,
|
||||
);
|
||||
final int panelColor = _rgbToPaletteIndex(engine.menuPanelRgb);
|
||||
final int headingIndex = WolfMenuPalette.headerTextIndex;
|
||||
final int selectedTextIndex = WolfMenuPalette.selectedTextIndex;
|
||||
final int unselectedTextIndex = WolfMenuPalette.unselectedTextIndex;
|
||||
final int disabledTextIndex = WolfMenuPalette.disabledTextIndex;
|
||||
final menu = WolfMenuPresentation(engine.data);
|
||||
final int headingIndex = menu.headerTextIndex;
|
||||
final int selectedTextIndex = menu.selectedTextIndex;
|
||||
final int unselectedTextIndex = menu.unselectedTextIndex;
|
||||
final int disabledTextIndex = menu.disabledTextIndex;
|
||||
|
||||
for (int i = 0; i < _screen.length; i++) {
|
||||
_screen[i] = bgColor;
|
||||
}
|
||||
|
||||
final art = WolfClassicMenuArt(engine.data);
|
||||
final optionsLabel = art.optionsLabel;
|
||||
final optionsLabel = menu.optionsLabel;
|
||||
if (optionsLabel != null) {
|
||||
_mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
|
||||
}
|
||||
// Draw footer first so menu panels can clip overlap in the center.
|
||||
_drawMenuFooterArt(art);
|
||||
_drawMenuFooterArt(menu);
|
||||
|
||||
if (engine.menuManager.activeMenu == WolfMenuScreen.introSplash) {
|
||||
_drawIntroSplash(engine, art);
|
||||
_drawIntroSplash(engine, menu);
|
||||
return;
|
||||
}
|
||||
|
||||
if (engine.menuManager.activeMenu == WolfMenuScreen.mainMenu) {
|
||||
_fillRect320(68, 52, 178, 136, panelColor);
|
||||
|
||||
final optionsLabel = art.optionsLabel;
|
||||
final optionsLabel = menu.optionsLabel;
|
||||
if (optionsLabel != null) {
|
||||
final int optionsX = ((320 - optionsLabel.width) ~/ 2).clamp(0, 319);
|
||||
_drawMainMenuOptionsSideBars(optionsLabel, optionsX);
|
||||
@@ -520,7 +520,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
);
|
||||
}
|
||||
|
||||
final cursor = art.mappedPic(
|
||||
final cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const int rowYStart = 55;
|
||||
@@ -558,7 +558,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
headingIndex,
|
||||
scale: 2,
|
||||
);
|
||||
final cursor = art.mappedPic(
|
||||
final cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const int rowYStart = 84;
|
||||
@@ -593,7 +593,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
headingIndex,
|
||||
scale: 2,
|
||||
);
|
||||
final cursor = art.mappedPic(
|
||||
final cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const int rowYStart = 30;
|
||||
@@ -604,7 +604,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
if (isSelected && cursor != null) {
|
||||
_blitVgaImage(cursor, 16, y + 2);
|
||||
}
|
||||
final image = art.episodeOption(i);
|
||||
final image = menu.episodeOption(i);
|
||||
if (image != null) {
|
||||
_blitVgaImage(image, 40, y);
|
||||
}
|
||||
@@ -633,12 +633,12 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
}
|
||||
|
||||
if (engine.menuManager.activeMenu == WolfMenuScreen.changeView) {
|
||||
_drawCustomizeMenuHeader(art, headingIndex, bgColor);
|
||||
final cursor = art.mappedPic(
|
||||
_drawCustomizeMenuHeader(menu, headingIndex, bgColor);
|
||||
final cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
final selectedMarker = art.selectedMarker;
|
||||
final unselectedMarker = art.unselectedMarker;
|
||||
final selectedMarker = menu.selectedMarker;
|
||||
final unselectedMarker = menu.unselectedMarker;
|
||||
const int rowYStart = 66;
|
||||
const int rowStep = 18;
|
||||
const int cursorX = 62;
|
||||
@@ -726,7 +726,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
}
|
||||
|
||||
if (engine.menuManager.activeMenu == WolfMenuScreen.rendererOptions) {
|
||||
_drawCustomizeMenuHeader(art, headingIndex, bgColor);
|
||||
_drawCustomizeMenuHeader(menu, headingIndex, bgColor);
|
||||
_fillRect320(56, 52, 208, 120, panelColor);
|
||||
_drawMenuTextCentered(
|
||||
engine.menuManager.rendererOptionsTitle,
|
||||
@@ -735,11 +735,11 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
scale: 1,
|
||||
);
|
||||
|
||||
final cursor = art.mappedPic(
|
||||
final cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
final selectedMarker = art.selectedMarker;
|
||||
final unselectedMarker = art.unselectedMarker;
|
||||
final selectedMarker = menu.selectedMarker;
|
||||
final unselectedMarker = menu.unselectedMarker;
|
||||
const int rowYStart = 68;
|
||||
const int rowStep = 20;
|
||||
const int cursorX = 62;
|
||||
@@ -781,7 +781,12 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
);
|
||||
_fillRect320(28, 70, 264, 82, panelColor);
|
||||
if (_useCompactMenuLayout) {
|
||||
_drawCompactMenu(selectedDifficultyIndex, headingIndex, panelColor);
|
||||
_drawCompactMenu(
|
||||
selectedDifficultyIndex,
|
||||
headingIndex,
|
||||
panelColor,
|
||||
menu,
|
||||
);
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
return;
|
||||
}
|
||||
@@ -793,14 +798,14 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
scale: _menuHeadingScale,
|
||||
);
|
||||
|
||||
final face = art.difficultyOption(
|
||||
final face = menu.difficultyOption(
|
||||
Difficulty.values[selectedDifficultyIndex],
|
||||
);
|
||||
if (face != null) {
|
||||
_blitVgaImage(face, 28 + 264 - face.width - 10, 92);
|
||||
}
|
||||
|
||||
final cursor = art.mappedPic(
|
||||
final cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const rowYStart = 86;
|
||||
@@ -827,7 +832,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
}
|
||||
|
||||
void _drawCustomizeMenuHeader(
|
||||
WolfClassicMenuArt art,
|
||||
WolfMenuPresentation menu,
|
||||
int headingIndex,
|
||||
int backgroundColor,
|
||||
) {
|
||||
@@ -837,7 +842,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
barColor: 0,
|
||||
);
|
||||
|
||||
final VgaImage? heading = art.customizeLabel ?? art.optionsLabel;
|
||||
final VgaImage? heading = menu.customizeLabel ?? menu.optionsLabel;
|
||||
if (heading != null) {
|
||||
final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
|
||||
_blitVgaImage(heading, headingX, 0);
|
||||
@@ -865,8 +870,8 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
_drawMenuTextCentered(text, y200 + 2, textColor, scale: 1);
|
||||
}
|
||||
|
||||
void _drawMenuFooterArt(WolfClassicMenuArt art) {
|
||||
final bottom = art.mappedPic(15);
|
||||
void _drawMenuFooterArt(WolfMenuPresentation menu) {
|
||||
final bottom = menu.mappedPic(15);
|
||||
if (bottom == null) {
|
||||
return;
|
||||
}
|
||||
@@ -890,11 +895,11 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
}
|
||||
}
|
||||
|
||||
void _drawIntroSplash(WolfEngine engine, WolfClassicMenuArt art) {
|
||||
void _drawIntroSplash(WolfEngine engine, WolfMenuPresentation menu) {
|
||||
final image = switch (engine.menuManager.currentIntroSlide) {
|
||||
WolfIntroSlide.retailWarning => null,
|
||||
WolfIntroSlide.pg13 => art.mappedPic(WolfMenuPic.pg13),
|
||||
WolfIntroSlide.title => art.mappedPic(WolfMenuPic.title),
|
||||
WolfIntroSlide.pg13 => menu.mappedPic(WolfMenuPic.pg13),
|
||||
WolfIntroSlide.title => menu.mappedPic(WolfMenuPic.title),
|
||||
};
|
||||
|
||||
int splashBg = _rgbToPaletteIndex(engine.menuManager.menuBackgroundRgb);
|
||||
@@ -1157,6 +1162,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
int selectedDifficultyIndex,
|
||||
int headingIndex,
|
||||
int panelColor,
|
||||
WolfMenuPresentation menu,
|
||||
) {
|
||||
_fillRect320(16, 52, 288, 112, panelColor);
|
||||
_drawMenuTextCentered(Difficulty.menuText, 60, headingIndex, scale: 1);
|
||||
@@ -1170,9 +1176,7 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
prefix + Difficulty.values[i].title,
|
||||
42,
|
||||
rowYStart + (i * rowStep),
|
||||
isSelected
|
||||
? WolfMenuPalette.selectedTextIndex
|
||||
: WolfMenuPalette.unselectedTextIndex,
|
||||
isSelected ? menu.selectedTextIndex : menu.unselectedTextIndex,
|
||||
scale: 1,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -254,31 +254,31 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
void drawMenu(WolfEngine engine) {
|
||||
final int bgColor = _rgbToFrameColor(engine.menuManager.menuBackgroundRgb);
|
||||
final int panelColor = _rgbToFrameColor(engine.menuPanelRgb);
|
||||
final int headingColor = WolfMenuPalette.headerTextColor;
|
||||
final int selectedTextColor = WolfMenuPalette.selectedTextColor;
|
||||
final int unselectedTextColor = WolfMenuPalette.unselectedTextColor;
|
||||
final int disabledTextColor = WolfMenuPalette.disabledTextColor;
|
||||
final menu = WolfMenuPresentation(engine.data);
|
||||
final int headingColor = menu.headerTextColor;
|
||||
final int selectedTextColor = menu.selectedTextColor;
|
||||
final int unselectedTextColor = menu.unselectedTextColor;
|
||||
final int disabledTextColor = menu.disabledTextColor;
|
||||
|
||||
for (int i = 0; i < _buffer.pixels.length; i++) {
|
||||
_buffer.pixels[i] = bgColor;
|
||||
}
|
||||
|
||||
final art = WolfClassicMenuArt(engine.data);
|
||||
final optionsLabel = art.optionsLabel;
|
||||
final optionsLabel = menu.optionsLabel;
|
||||
if (optionsLabel != null) {
|
||||
_mainMenuBandFirstColumn = _cacheFirstColumn(optionsLabel);
|
||||
}
|
||||
// Draw footer first so menu panels can clip overlap in the center.
|
||||
_drawCenteredMenuFooter(art);
|
||||
_drawCenteredMenuFooter(menu);
|
||||
|
||||
switch (engine.menuManager.activeMenu) {
|
||||
case WolfMenuScreen.introSplash:
|
||||
_drawIntroSplash(engine, art);
|
||||
_drawIntroSplash(engine, menu);
|
||||
break;
|
||||
case WolfMenuScreen.mainMenu:
|
||||
_drawMainMenu(
|
||||
engine,
|
||||
art,
|
||||
menu,
|
||||
panelColor,
|
||||
headingColor,
|
||||
selectedTextColor,
|
||||
@@ -289,7 +289,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
case WolfMenuScreen.gameSelect:
|
||||
_drawGameSelectMenu(
|
||||
engine,
|
||||
art,
|
||||
menu,
|
||||
panelColor,
|
||||
headingColor,
|
||||
selectedTextColor,
|
||||
@@ -299,7 +299,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
case WolfMenuScreen.episodeSelect:
|
||||
_drawEpisodeSelectMenu(
|
||||
engine,
|
||||
art,
|
||||
menu,
|
||||
panelColor,
|
||||
headingColor,
|
||||
selectedTextColor,
|
||||
@@ -309,7 +309,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
case WolfMenuScreen.difficultySelect:
|
||||
_drawDifficultyMenu(
|
||||
engine,
|
||||
art,
|
||||
menu,
|
||||
panelColor,
|
||||
headingColor,
|
||||
selectedTextColor,
|
||||
@@ -319,7 +319,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
case WolfMenuScreen.changeView:
|
||||
_drawChangeViewMenu(
|
||||
engine,
|
||||
art,
|
||||
menu,
|
||||
panelColor,
|
||||
headingColor,
|
||||
selectedTextColor,
|
||||
@@ -330,7 +330,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
case WolfMenuScreen.rendererOptions:
|
||||
_drawRendererOptionsMenu(
|
||||
engine,
|
||||
art,
|
||||
menu,
|
||||
panelColor,
|
||||
headingColor,
|
||||
selectedTextColor,
|
||||
@@ -343,11 +343,11 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
_applyMenuTransition(engine.menuManager, bgColor);
|
||||
}
|
||||
|
||||
void _drawIntroSplash(WolfEngine engine, WolfClassicMenuArt art) {
|
||||
void _drawIntroSplash(WolfEngine engine, WolfMenuPresentation menu) {
|
||||
final image = switch (engine.menuManager.currentIntroSlide) {
|
||||
WolfIntroSlide.retailWarning => null,
|
||||
WolfIntroSlide.pg13 => art.mappedPic(WolfMenuPic.pg13),
|
||||
WolfIntroSlide.title => art.mappedPic(WolfMenuPic.title),
|
||||
WolfIntroSlide.pg13 => menu.mappedPic(WolfMenuPic.pg13),
|
||||
WolfIntroSlide.title => menu.mappedPic(WolfMenuPic.title),
|
||||
};
|
||||
|
||||
int splashBgColor = _rgbToFrameColor(engine.menuManager.menuBackgroundRgb);
|
||||
@@ -478,7 +478,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
|
||||
void _drawMainMenu(
|
||||
WolfEngine engine,
|
||||
WolfClassicMenuArt art,
|
||||
WolfMenuPresentation menu,
|
||||
int panelColor,
|
||||
int headingColor,
|
||||
int selectedTextColor,
|
||||
@@ -491,7 +491,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
const int panelH = 136;
|
||||
_fillCanonicalRect(panelX, panelY, panelW, panelH, panelColor);
|
||||
|
||||
final optionsLabel = art.optionsLabel;
|
||||
final optionsLabel = menu.optionsLabel;
|
||||
if (optionsLabel != null) {
|
||||
final int optionsX = ((320 - optionsLabel.width) ~/ 2).clamp(0, 319);
|
||||
_drawMainMenuOptionsSideBars(optionsLabel, optionsX);
|
||||
@@ -510,7 +510,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
);
|
||||
}
|
||||
|
||||
final cursor = art.mappedPic(
|
||||
final cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const int rowYStart = 55;
|
||||
@@ -538,7 +538,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
|
||||
void _drawChangeViewMenu(
|
||||
WolfEngine engine,
|
||||
WolfClassicMenuArt art,
|
||||
WolfMenuPresentation menu,
|
||||
int panelColor,
|
||||
int headingColor,
|
||||
int selectedTextColor,
|
||||
@@ -557,7 +557,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
const int optionsPanelX = 46;
|
||||
const int optionsPanelW = 228;
|
||||
|
||||
final VgaImage? heading = art.customizeLabel ?? art.optionsLabel;
|
||||
final VgaImage? heading = menu.customizeLabel ?? menu.optionsLabel;
|
||||
if (heading != null) {
|
||||
final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
|
||||
_blitVgaImage(heading, headingX, 0);
|
||||
@@ -570,9 +570,9 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
);
|
||||
}
|
||||
|
||||
final VgaImage? selectedMarker = art.selectedMarker;
|
||||
final VgaImage? unselectedMarker = art.unselectedMarker;
|
||||
final VgaImage? cursor = art.mappedPic(
|
||||
final VgaImage? selectedMarker = menu.selectedMarker;
|
||||
final VgaImage? unselectedMarker = menu.unselectedMarker;
|
||||
final VgaImage? cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
|
||||
@@ -684,7 +684,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
|
||||
void _drawRendererOptionsMenu(
|
||||
WolfEngine engine,
|
||||
WolfClassicMenuArt art,
|
||||
WolfMenuPresentation menu,
|
||||
int panelColor,
|
||||
int headingColor,
|
||||
int selectedTextColor,
|
||||
@@ -703,7 +703,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
const int panelH = 120;
|
||||
_fillCanonicalRect(panelX, panelY, panelW, panelH, panelColor);
|
||||
|
||||
final VgaImage? heading = art.customizeLabel ?? art.optionsLabel;
|
||||
final VgaImage? heading = menu.customizeLabel ?? menu.optionsLabel;
|
||||
if (heading != null) {
|
||||
final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
|
||||
_blitVgaImage(heading, headingX, 0);
|
||||
@@ -716,9 +716,9 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
);
|
||||
}
|
||||
|
||||
final VgaImage? selectedMarker = art.selectedMarker;
|
||||
final VgaImage? unselectedMarker = art.unselectedMarker;
|
||||
final VgaImage? cursor = art.mappedPic(
|
||||
final VgaImage? selectedMarker = menu.selectedMarker;
|
||||
final VgaImage? unselectedMarker = menu.unselectedMarker;
|
||||
final VgaImage? cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
|
||||
@@ -766,7 +766,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
|
||||
void _drawGameSelectMenu(
|
||||
WolfEngine engine,
|
||||
WolfClassicMenuArt art,
|
||||
WolfMenuPresentation menu,
|
||||
int panelColor,
|
||||
int headingColor,
|
||||
int selectedTextColor,
|
||||
@@ -791,7 +791,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
scale: 2,
|
||||
);
|
||||
|
||||
final cursor = art.mappedPic(
|
||||
final cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
|
||||
@@ -837,7 +837,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
|
||||
void _drawEpisodeSelectMenu(
|
||||
WolfEngine engine,
|
||||
WolfClassicMenuArt art,
|
||||
WolfMenuPresentation menu,
|
||||
int panelColor,
|
||||
int headingColor,
|
||||
int selectedTextColor,
|
||||
@@ -862,7 +862,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
scale: 2,
|
||||
);
|
||||
|
||||
final cursor = art.mappedPic(
|
||||
final cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const int rowYStart = 30;
|
||||
@@ -879,7 +879,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
_blitVgaImage(cursor, 16, y + 2);
|
||||
}
|
||||
|
||||
final image = art.episodeOption(i);
|
||||
final image = menu.episodeOption(i);
|
||||
if (image != null) {
|
||||
_blitVgaImage(image, imageX, y);
|
||||
}
|
||||
@@ -904,8 +904,8 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
}
|
||||
}
|
||||
|
||||
void _drawCenteredMenuFooter(WolfClassicMenuArt art) {
|
||||
final bottom = art.mappedPic(15);
|
||||
void _drawCenteredMenuFooter(WolfMenuPresentation menu) {
|
||||
final bottom = menu.mappedPic(15);
|
||||
if (bottom != null) {
|
||||
final int x = ((320 - bottom.width) ~/ 2).clamp(0, 319);
|
||||
final int y = (200 - bottom.height - _menuFooterBottomMargin).clamp(
|
||||
@@ -954,7 +954,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
|
||||
void _drawDifficultyMenu(
|
||||
WolfEngine engine,
|
||||
WolfClassicMenuArt art,
|
||||
WolfMenuPresentation menu,
|
||||
int panelColor,
|
||||
int headingColor,
|
||||
int selectedTextColor,
|
||||
@@ -981,14 +981,14 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
scale: 2,
|
||||
);
|
||||
|
||||
final face = art.difficultyOption(
|
||||
final face = menu.difficultyOption(
|
||||
Difficulty.values[selectedDifficultyIndex],
|
||||
);
|
||||
if (face != null) {
|
||||
_blitVgaImage(face, panelX + panelW - face.width - 10, panelY + 22);
|
||||
}
|
||||
|
||||
final cursor = art.mappedPic(
|
||||
final cursor = menu.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
const int rowYStart = panelY + 16;
|
||||
|
||||
@@ -42,6 +42,8 @@ export 'src/registry/modules/entity_asset_module.dart'
|
||||
export 'src/registry/modules/hud_module.dart' show HudModule, HudAssetRef;
|
||||
export 'src/registry/modules/menu_pic_module.dart'
|
||||
show MenuPicModule, MenuPicRef;
|
||||
export 'src/registry/modules/menu_presentation_module.dart'
|
||||
show MenuPresentationModule;
|
||||
export 'src/registry/modules/music_module.dart' show MusicModule, MusicRoute;
|
||||
export 'src/registry/modules/sfx_module.dart' show SfxModule, SoundAssetRef;
|
||||
export 'src/registry/registry_resolver.dart'
|
||||
|
||||
@@ -1,218 +1,5 @@
|
||||
/// Shared menu helpers for Wolf3D hosts.
|
||||
library;
|
||||
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
|
||||
/// Known VGA picture indexes used by the original Wolf3D control-panel menus.
|
||||
///
|
||||
/// Values below are picture-table indexes (not raw chunk ids).
|
||||
/// For example, `C_CONTROLPIC` is chunk 26 in `GFXV_WL6.H`, so its picture
|
||||
/// index is `26 - STARTPICS(3) = 23`.
|
||||
abstract class WolfMenuPic {
|
||||
static const int hBj = 0; // H_BJPIC
|
||||
static const int hTopWindow = 3; // H_TOPWINDOWPIC
|
||||
static const int cOptions = 7; // C_OPTIONSPIC
|
||||
static const int cCursor1 = 8; // C_CURSOR1PIC
|
||||
static const int cCursor2 = 9; // C_CURSOR2PIC
|
||||
static const int cNotSelected = 10; // C_NOTSELECTEDPIC
|
||||
static const int cSelected = 11; // C_SELECTEDPIC
|
||||
static const int cBabyMode = 16; // C_BABYMODEPIC
|
||||
static const int cEasy = 17; // C_EASYPIC
|
||||
static const int cNormal = 18; // C_NORMALPIC
|
||||
static const int cHard = 19; // C_HARDPIC
|
||||
static const int cControl = 23; // C_CONTROLPIC
|
||||
static const int cCustomize = 24; // C_CUSTOMIZEPIC
|
||||
static const int cEpisode1 = 27; // C_EPISODE1PIC
|
||||
static const int cEpisode2 = 28; // C_EPISODE2PIC
|
||||
static const int cEpisode3 = 29; // C_EPISODE3PIC
|
||||
static const int cEpisode4 = 30; // C_EPISODE4PIC
|
||||
static const int cEpisode5 = 31; // C_EPISODE5PIC
|
||||
static const int cEpisode6 = 32; // C_EPISODE6PIC
|
||||
static const int statusBar = 83; // STATUSBARPIC
|
||||
static const int title = 84; // TITLEPIC
|
||||
static const int pg13 = 85; // PG13PIC
|
||||
static const int credits = 86; // CREDITSPIC
|
||||
static const int highScores = 87; // HIGHSCORESPIC
|
||||
|
||||
static const List<int> episodePics = [
|
||||
cEpisode1,
|
||||
cEpisode2,
|
||||
cEpisode3,
|
||||
cEpisode4,
|
||||
cEpisode5,
|
||||
cEpisode6,
|
||||
];
|
||||
}
|
||||
|
||||
/// Shared menu text colors resolved from the VGA palette.
|
||||
///
|
||||
/// Keep menu color choices centralized so renderers don't duplicate
|
||||
/// hard-coded palette slots or RGB conversion logic.
|
||||
abstract class WolfMenuPalette {
|
||||
static const int backgroundIndex = 111;
|
||||
static const int panelIndex = 103;
|
||||
static const int borderIndex = 87;
|
||||
static const int emphasisIndex = 10;
|
||||
static const int warningIndex = 14;
|
||||
static const int mutedIndex = 8;
|
||||
|
||||
static const int selectedTextIndex = 19;
|
||||
static const int unselectedTextIndex = 23;
|
||||
static const int disabledTextIndex = 4;
|
||||
static const int _headerTargetRgb = 0xFFF700;
|
||||
|
||||
static int? _cachedHeaderTextIndex;
|
||||
|
||||
static int get headerTextIndex => _cachedHeaderTextIndex ??=
|
||||
ColorPalette.findClosestPaletteIndex(_headerTargetRgb);
|
||||
|
||||
/// Standard ARGB colors (`0xAARRGGBB`) for UI consumers.
|
||||
static int get backgroundColor =>
|
||||
ColorPalette.argbFromVgaIndex(backgroundIndex);
|
||||
|
||||
static int get panelColor => ColorPalette.argbFromVgaIndex(panelIndex);
|
||||
|
||||
static int get borderColor => ColorPalette.argbFromVgaIndex(borderIndex);
|
||||
|
||||
static int get titleColor => ColorPalette.argbFromVgaIndex(headerTextIndex);
|
||||
|
||||
static int get bodyColor =>
|
||||
ColorPalette.argbFromVgaIndex(unselectedTextIndex);
|
||||
|
||||
static int get emphasisColor => ColorPalette.argbFromVgaIndex(emphasisIndex);
|
||||
|
||||
static int get warningColor => ColorPalette.argbFromVgaIndex(warningIndex);
|
||||
|
||||
static int get mutedColor => ColorPalette.argbFromVgaIndex(mutedIndex);
|
||||
|
||||
static int get selectedTextColor => ColorPalette.vga32Bit[selectedTextIndex];
|
||||
|
||||
static int get unselectedTextColor =>
|
||||
ColorPalette.vga32Bit[unselectedTextIndex];
|
||||
|
||||
static int get disabledTextColor => ColorPalette.vga32Bit[disabledTextIndex];
|
||||
|
||||
static int get headerTextColor => ColorPalette.vga32Bit[headerTextIndex];
|
||||
}
|
||||
|
||||
/// Structured accessors for classic Wolf3D menu art.
|
||||
class WolfClassicMenuArt {
|
||||
final WolfensteinData data;
|
||||
|
||||
WolfClassicMenuArt(this.data);
|
||||
|
||||
VgaImage? get controlBackground {
|
||||
return _imageForKey(MenuPicKey.controlBackground);
|
||||
}
|
||||
|
||||
VgaImage? get title => _imageForKey(MenuPicKey.title);
|
||||
|
||||
VgaImage? get heading => _imageForKey(MenuPicKey.heading);
|
||||
|
||||
VgaImage? get selectedMarker => _imageForKey(MenuPicKey.markerSelected);
|
||||
|
||||
VgaImage? get unselectedMarker => _imageForKey(MenuPicKey.markerUnselected);
|
||||
|
||||
VgaImage? get optionsLabel => _imageForKey(MenuPicKey.optionsLabel);
|
||||
|
||||
VgaImage? get customizeLabel => _imageForKey(MenuPicKey.customizeLabel);
|
||||
|
||||
VgaImage? get credits => _imageForKey(MenuPicKey.credits);
|
||||
|
||||
VgaImage? episodeOption(int episodeIndex) {
|
||||
if (episodeIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
final key = data.registry.menu.episodeKey(episodeIndex);
|
||||
return _imageForKey(key);
|
||||
}
|
||||
|
||||
VgaImage? difficultyOption(Difficulty difficulty) {
|
||||
final key = data.registry.menu.difficultyKey(difficulty);
|
||||
return _imageForKey(key);
|
||||
}
|
||||
|
||||
/// Legacy numeric lookup API retained for existing renderer call sites.
|
||||
///
|
||||
/// Known legacy indices are mapped through symbolic registry keys first.
|
||||
/// Unknown indices fall back to direct picture-table indexing.
|
||||
VgaImage? mappedPic(int index) {
|
||||
final key = _legacyKeyForIndex(index);
|
||||
if (key != null) {
|
||||
return _imageForKey(key);
|
||||
}
|
||||
return pic(index);
|
||||
}
|
||||
|
||||
VgaImage? pic(int index) {
|
||||
if (index < 0 || index >= data.vgaImages.length) {
|
||||
return null;
|
||||
}
|
||||
final image = data.vgaImages[index];
|
||||
|
||||
if (image.width <= 0 || image.height <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
VgaImage? _imageForKey(MenuPicKey key) {
|
||||
final ref = data.registry.menu.resolve(key);
|
||||
if (ref == null) {
|
||||
return null;
|
||||
}
|
||||
return pic(ref.pictureIndex);
|
||||
}
|
||||
|
||||
MenuPicKey? _legacyKeyForIndex(int index) {
|
||||
switch (index) {
|
||||
case WolfMenuPic.hTopWindow:
|
||||
return MenuPicKey.heading;
|
||||
case WolfMenuPic.cOptions:
|
||||
return MenuPicKey.optionsLabel;
|
||||
case WolfMenuPic.cCursor1:
|
||||
return MenuPicKey.cursorActive;
|
||||
case WolfMenuPic.cCursor2:
|
||||
return MenuPicKey.cursorInactive;
|
||||
case WolfMenuPic.cNotSelected:
|
||||
return MenuPicKey.markerUnselected;
|
||||
case WolfMenuPic.cSelected:
|
||||
return MenuPicKey.markerSelected;
|
||||
case 15:
|
||||
return MenuPicKey.footer;
|
||||
case WolfMenuPic.cBabyMode:
|
||||
return MenuPicKey.difficultyBaby;
|
||||
case WolfMenuPic.cEasy:
|
||||
return MenuPicKey.difficultyEasy;
|
||||
case WolfMenuPic.cNormal:
|
||||
return MenuPicKey.difficultyNormal;
|
||||
case WolfMenuPic.cHard:
|
||||
return MenuPicKey.difficultyHard;
|
||||
case WolfMenuPic.cControl:
|
||||
return MenuPicKey.controlBackground;
|
||||
case WolfMenuPic.cCustomize:
|
||||
return MenuPicKey.customizeLabel;
|
||||
case WolfMenuPic.cEpisode1:
|
||||
return MenuPicKey.episode1;
|
||||
case WolfMenuPic.cEpisode2:
|
||||
return MenuPicKey.episode2;
|
||||
case WolfMenuPic.cEpisode3:
|
||||
return MenuPicKey.episode3;
|
||||
case WolfMenuPic.cEpisode4:
|
||||
return MenuPicKey.episode4;
|
||||
case WolfMenuPic.cEpisode5:
|
||||
return MenuPicKey.episode5;
|
||||
case WolfMenuPic.cEpisode6:
|
||||
return MenuPicKey.episode6;
|
||||
case WolfMenuPic.title:
|
||||
return MenuPicKey.title;
|
||||
case WolfMenuPic.pg13:
|
||||
return MenuPicKey.pg13;
|
||||
case WolfMenuPic.credits:
|
||||
return MenuPicKey.credits;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
export 'src/menu/wolf_menu_pic.dart';
|
||||
export 'src/menu/wolf_menu_presentation.dart';
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
|
||||
void main() {
|
||||
group('MenuManager', () {
|
||||
test('main menu row enablement reflects resumable and loadable state', () {
|
||||
final manager = MenuManager();
|
||||
|
||||
manager.showMainMenu(hasResumableGame: false, hasLoadableSave: false);
|
||||
|
||||
expect(
|
||||
_entryFor(manager, WolfMenuMainAction.loadGame).isEnabled,
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
_entryFor(manager, WolfMenuMainAction.saveGame).isEnabled,
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
_entryFor(manager, WolfMenuMainAction.changeView).isEnabled,
|
||||
isTrue,
|
||||
);
|
||||
|
||||
manager.setLoadGameAvailable(true);
|
||||
|
||||
expect(_entryFor(manager, WolfMenuMainAction.loadGame).isEnabled, isTrue);
|
||||
expect(
|
||||
_entryFor(manager, WolfMenuMainAction.saveGame).isEnabled,
|
||||
isFalse,
|
||||
);
|
||||
|
||||
manager.showMainMenu(hasResumableGame: true, hasLoadableSave: true);
|
||||
|
||||
expect(_entryFor(manager, WolfMenuMainAction.saveGame).isEnabled, isTrue);
|
||||
expect(_entryFor(manager, WolfMenuMainAction.endGame).isEnabled, isTrue);
|
||||
expect(
|
||||
manager.mainMenuEntries.any(
|
||||
(entry) => entry.action == WolfMenuMainAction.backToGame,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('change-view navigation skips disabled renderer rows and options', () {
|
||||
final manager = MenuManager();
|
||||
|
||||
manager.setChangeViewEntries(const <WolfMenuRendererEntry>[
|
||||
WolfMenuRendererEntry(
|
||||
mode: WolfRendererMode.software,
|
||||
label: 'SOFTWARE',
|
||||
hasOptions: false,
|
||||
isEnabled: false,
|
||||
),
|
||||
WolfMenuRendererEntry(
|
||||
mode: WolfRendererMode.ascii,
|
||||
label: 'ASCII',
|
||||
hasOptions: true,
|
||||
),
|
||||
]);
|
||||
manager.setRendererOptionEntries(
|
||||
title: 'ASCII OPTIONS',
|
||||
entries: const <WolfMenuRendererOptionEntry>[
|
||||
WolfMenuRendererOptionEntry(
|
||||
id: WolfRendererOptionId.asciiTheme,
|
||||
label: 'THEME',
|
||||
isEnabled: false,
|
||||
),
|
||||
WolfMenuRendererOptionEntry(
|
||||
id: WolfRendererOptionId.fpsCounter,
|
||||
label: 'FPS',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
manager.showChangeViewMenu();
|
||||
|
||||
expect(manager.selectedChangeViewIndex, 1);
|
||||
|
||||
manager.updateChangeViewMenu(const EngineInput(isMovingBackward: true));
|
||||
manager.updateChangeViewMenu(const EngineInput());
|
||||
|
||||
expect(manager.selectedChangeViewIndex, 3);
|
||||
});
|
||||
|
||||
test(
|
||||
'renderer option selection is preserved by option id when entries refresh',
|
||||
() {
|
||||
final manager = MenuManager();
|
||||
|
||||
manager.setChangeViewEntries(const <WolfMenuRendererEntry>[
|
||||
WolfMenuRendererEntry(
|
||||
mode: WolfRendererMode.ascii,
|
||||
label: 'ASCII',
|
||||
hasOptions: true,
|
||||
),
|
||||
]);
|
||||
manager.setRendererOptionEntries(
|
||||
title: 'ASCII OPTIONS',
|
||||
entries: const <WolfMenuRendererOptionEntry>[
|
||||
WolfMenuRendererOptionEntry(
|
||||
id: WolfRendererOptionId.asciiTheme,
|
||||
label: 'THEME',
|
||||
),
|
||||
WolfMenuRendererOptionEntry(
|
||||
id: WolfRendererOptionId.fpsCounter,
|
||||
label: 'FPS',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
manager.showRendererOptionsMenu();
|
||||
manager.updateRendererOptionsMenu(
|
||||
const EngineInput(isMovingBackward: true),
|
||||
);
|
||||
manager.updateRendererOptionsMenu(const EngineInput());
|
||||
|
||||
expect(manager.selectedRendererOptionIndex, 1);
|
||||
|
||||
manager.setRendererOptionEntries(
|
||||
title: 'ASCII OPTIONS',
|
||||
entries: const <WolfMenuRendererOptionEntry>[
|
||||
WolfMenuRendererOptionEntry(
|
||||
id: WolfRendererOptionId.fpsCounter,
|
||||
label: 'FPS',
|
||||
),
|
||||
WolfMenuRendererOptionEntry(
|
||||
id: WolfRendererOptionId.asciiTheme,
|
||||
label: 'THEME',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
expect(manager.selectedRendererOptionIndex, 0);
|
||||
expect(
|
||||
manager.rendererOptionEntries[manager.selectedRendererOptionIndex].id,
|
||||
WolfRendererOptionId.fpsCounter,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('transition locks navigation and reports none when idle', () {
|
||||
final manager = MenuManager();
|
||||
|
||||
manager.showMainMenu(hasResumableGame: false);
|
||||
manager.startTransition(WolfMenuScreen.difficultySelect);
|
||||
|
||||
final result = manager.updateMainMenu(
|
||||
const EngineInput(isMovingBackward: true, isInteracting: true),
|
||||
);
|
||||
|
||||
expect(result.selected, isNull);
|
||||
expect(result.goBack, isFalse);
|
||||
expect(manager.transitionEffect, WolfTransitionEffect.normalFade);
|
||||
|
||||
manager.tickTransition(MenuManager.transitionDurationMs);
|
||||
|
||||
expect(manager.isTransitioning, isFalse);
|
||||
expect(manager.transitionEffect, WolfTransitionEffect.none);
|
||||
expect(manager.activeMenu, WolfMenuScreen.difficultySelect);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
WolfMenuMainEntry _entryFor(MenuManager manager, WolfMenuMainAction action) {
|
||||
return manager.mainMenuEntries.firstWhere((entry) => entry.action == action);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:test/test.dart';
|
||||
import 'package:wolf_3d_dart/src/registry/built_in/spear_menu_presentation_module.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||
|
||||
@@ -17,6 +18,7 @@ void main() {
|
||||
expect(registry.hud.resolve(HudKey.statusBar)?.vgaIndex, 73);
|
||||
expect(registry.hud.resolve(HudKey.digit0)?.vgaIndex, 84);
|
||||
expect(registry.hud.resolve(HudKey.pistolIcon)?.vgaIndex, 77);
|
||||
expect(registry.menuPresentation, isA<SpearMenuPresentationModule>());
|
||||
});
|
||||
|
||||
test('resolves SDM enemy sprite ranges with +4 SPEAR shift', () {
|
||||
|
||||
@@ -21,14 +21,14 @@ void main() {
|
||||
vgaImages: List.generate(120, (i) => _img(height: i + 1)),
|
||||
);
|
||||
|
||||
final art = WolfClassicMenuArt(data);
|
||||
final menu = WolfMenuPresentation(data);
|
||||
|
||||
// Retail cursor constants 8/9 must resolve to shareware cursor IDs 20/21.
|
||||
expect(art.mappedPic(8)?.height, 21);
|
||||
expect(art.mappedPic(9)?.height, 22);
|
||||
expect(menu.mappedPic(8)?.height, 21);
|
||||
expect(menu.mappedPic(9)?.height, 22);
|
||||
|
||||
// Retail footer constant 15 must resolve to shareware footer ID 27.
|
||||
expect(art.mappedPic(15)?.height, 28);
|
||||
expect(menu.mappedPic(15)?.height, 28);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,13 +41,25 @@ final Wolf3dFlutterEngine engine = await Wolf3dFlutterEngine(
|
||||
|
||||
`init()` handles platform setup, audio init, and configured game-data discovery.
|
||||
|
||||
The facade itself lives in `lib/engine/wolf3d_flutter_engine.dart` and is re-exported
|
||||
through the package barrel at `lib/wolf_3d_flutter.dart`. External consumers
|
||||
should keep importing the barrel unless they have a specific reason to target
|
||||
the engine library directly.
|
||||
|
||||
The same pattern applies to the Flutter input adapter and the desktop
|
||||
persistence adapters: they now live in focused subdirectories and are
|
||||
re-exported through `lib/wolf_3d_flutter.dart`.
|
||||
|
||||
For full host wiring examples, see:
|
||||
|
||||
- `apps/wolf_3d_gui/lib/main.dart`
|
||||
|
||||
## Package Structure
|
||||
|
||||
- `lib/wolf_3d_flutter.dart` — exports main host facade and public Flutter APIs.
|
||||
- `lib/wolf_3d_flutter.dart` — barrel export for the public Flutter package surface.
|
||||
- `lib/engine/wolf3d_flutter_engine.dart` — high-level engine facade and discovery bootstrap.
|
||||
- `lib/input/` — Flutter-specific input adapters.
|
||||
- `lib/persistence/` — desktop persistence adapters for saves and renderer settings.
|
||||
- `lib/renderer/` — renderer host widgets.
|
||||
- `lib/managers/` — runtime/session/display/persistence managers.
|
||||
- `lib/audio/` — platform-aware audio backends.
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/// Flutter-specific engine facade for discovery and runtime service wiring.
|
||||
library;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
import 'package:wolf_3d_flutter/audio/wolf3d_platform_audio.dart';
|
||||
import 'package:wolf_3d_flutter/input/wolf_3d_input_flutter.dart';
|
||||
import 'package:wolf_3d_flutter/managers/desktop_windowing_support.dart'
|
||||
as desktop_windowing_support;
|
||||
import 'package:wolf_3d_flutter/managers/game_data_directory_persistence.dart';
|
||||
|
||||
/// Flutter-specific host facade built on top of [Wolf3dEngine].
|
||||
///
|
||||
/// This type keeps platform-neutral session and engine state in
|
||||
/// `wolf_3d_dart` while owning Flutter-only concerns such as platform
|
||||
/// discovery helpers and persisted data-directory lookup.
|
||||
class Wolf3dFlutterEngine extends Wolf3dEngine {
|
||||
/// Creates an empty facade that must be initialized with [init].
|
||||
Wolf3dFlutterEngine({
|
||||
bool debug = false,
|
||||
EngineAudio? audioBackend,
|
||||
Wolf3dFlutterInput? inputBackend,
|
||||
DefaultGameDataDirectoryPersistence? dataDirectoryPersistence,
|
||||
}) : dataDirectoryPersistence =
|
||||
dataDirectoryPersistence ?? DefaultGameDataDirectoryPersistence(),
|
||||
super(
|
||||
audio: audioBackend ?? Wolf3dPlatformAudio(),
|
||||
input: inputBackend ?? Wolf3dFlutterInput(),
|
||||
) {
|
||||
if (debug) {
|
||||
enableDebug();
|
||||
}
|
||||
}
|
||||
|
||||
/// Persists and restores the preferred external game-data directory.
|
||||
final DefaultGameDataDirectoryPersistence dataDirectoryPersistence;
|
||||
|
||||
/// Last configured or loaded external game-data directory path.
|
||||
String? configuredDataDirectory;
|
||||
|
||||
/// Shared Flutter input adapter reused by gameplay screens.
|
||||
@override
|
||||
Wolf3dFlutterInput get input => super.input as Wolf3dFlutterInput;
|
||||
|
||||
/// Enables host-level debug affordances such as debug navigation UI.
|
||||
@override
|
||||
Wolf3dFlutterEngine enableDebug() {
|
||||
super.enableDebug();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Initializes the engine by loading available game data.
|
||||
///
|
||||
/// If [directory] is provided, it is persisted and treated as the primary
|
||||
/// external search root. If omitted, a previously persisted directory is
|
||||
/// used when available. [additionalDirectories] are scanned after the
|
||||
/// primary directory and are not persisted.
|
||||
///
|
||||
/// This method scans only configured external directories, deduplicating
|
||||
/// discovered versions by [GameVersion]. Shared package code does not bundle
|
||||
/// or import game data on behalf of host applications.
|
||||
Future<Wolf3dFlutterEngine> init({
|
||||
String? directory,
|
||||
Iterable<String>? additionalDirectories,
|
||||
}) async {
|
||||
await desktop_windowing_support.ensureDesktopWindowingInitialized();
|
||||
await audio.init();
|
||||
availableGames.clear();
|
||||
|
||||
final String? requestedDirectory = directory?.trim();
|
||||
final String? resolvedDirectory =
|
||||
requestedDirectory != null && requestedDirectory.isNotEmpty
|
||||
? requestedDirectory
|
||||
: await dataDirectoryPersistence.loadDataDirectory();
|
||||
configuredDataDirectory = resolvedDirectory;
|
||||
|
||||
if (requestedDirectory != null && requestedDirectory.isNotEmpty) {
|
||||
await dataDirectoryPersistence.saveDataDirectory(requestedDirectory);
|
||||
}
|
||||
|
||||
// On non-web platforms, also scan local filesystem locations for
|
||||
// user-supplied data folders so the host can pick up extra versions.
|
||||
final Set<String> directoriesToScan = <String>{};
|
||||
if (resolvedDirectory != null && resolvedDirectory.isNotEmpty) {
|
||||
directoriesToScan.add(resolvedDirectory);
|
||||
}
|
||||
|
||||
if (additionalDirectories != null) {
|
||||
for (final String directoryPath in additionalDirectories) {
|
||||
final String trimmedPath = directoryPath.trim();
|
||||
if (trimmedPath.isNotEmpty) {
|
||||
directoriesToScan.add(trimmedPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!kIsWeb && directoriesToScan.isNotEmpty) {
|
||||
for (final String directoryPath in directoriesToScan) {
|
||||
try {
|
||||
final externalGames = await WolfensteinLoader.discover(
|
||||
directoryPath: directoryPath,
|
||||
recursive: true,
|
||||
);
|
||||
for (final MapEntry<GameVersion, WolfensteinData> entry
|
||||
in externalGames.entries) {
|
||||
if (!availableGames.any(
|
||||
(WolfensteinData g) => g.version == entry.key,
|
||||
)) {
|
||||
availableGames.add(entry.value);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('External discovery failed: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||
import 'package:wolf_3d_flutter/input/wolf_3d_input_flutter.dart';
|
||||
import 'package:wolf_3d_flutter/managers/game_renderer_mode_manager.dart';
|
||||
import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart';
|
||||
|
||||
/// Semantic actions that host-level shortcuts can trigger.
|
||||
///
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
/// High-level Flutter facade for discovering game data and sharing runtime services.
|
||||
library;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
import 'package:wolf_3d_flutter/audio/wolf3d_platform_audio.dart';
|
||||
import 'package:wolf_3d_flutter/managers/desktop_windowing_support.dart'
|
||||
as desktop_windowing_support;
|
||||
import 'package:wolf_3d_flutter/managers/game_data_directory_persistence.dart';
|
||||
import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart';
|
||||
|
||||
export 'package:wolf_3d_dart/wolf_3d_audio.dart' show DebugMusicPlayer;
|
||||
|
||||
export 'audio/wolf3d_platform_audio.dart' show Wolf3dPlatformAudio;
|
||||
export 'engine/wolf3d_flutter_engine.dart' show Wolf3dFlutterEngine;
|
||||
export 'input/wolf_3d_input_flutter.dart' show Wolf3dFlutterInput;
|
||||
export 'managers/game_app_lifecycle_manager.dart' show GameAppLifecycleManager;
|
||||
export 'managers/game_data_directory_persistence.dart'
|
||||
show DefaultGameDataDirectoryPersistence;
|
||||
@@ -29,6 +21,10 @@ export 'managers/game_screen_input_manager.dart'
|
||||
HostShortcutRegistry,
|
||||
GameScreenInputManager,
|
||||
isAltEnterShortcut;
|
||||
export 'persistence/renderer_settings_persistence_flutter.dart'
|
||||
show FlutterRendererSettingsPersistence;
|
||||
export 'persistence/save_game_persistence_flutter.dart'
|
||||
show FlutterSaveGamePersistence;
|
||||
export 'screens/audio_gallery.dart' show AudioGallery;
|
||||
export 'screens/debug_tools_screen.dart' show DebugToolsScreen;
|
||||
export 'screens/game_screen.dart' show GameScreen;
|
||||
@@ -37,109 +33,3 @@ export 'screens/vga_gallery.dart' show VgaGallery;
|
||||
export 'widgets/gallery_game_selector.dart'
|
||||
show GalleryGameSelector, formatGalleryGameTitle;
|
||||
export 'widgets/wolf_menu_shell.dart' show WolfMenuShell;
|
||||
|
||||
/// Flutter-specific host facade built on top of [Wolf3dEngine].
|
||||
///
|
||||
/// This type keeps platform-neutral session/engine state in the Dart package
|
||||
/// while owning Flutter-only concerns such as platform discovery helpers.
|
||||
class Wolf3dFlutterEngine extends Wolf3dEngine {
|
||||
/// Creates an empty facade that must be initialized with [init].
|
||||
Wolf3dFlutterEngine({
|
||||
bool debug = false,
|
||||
EngineAudio? audioBackend,
|
||||
Wolf3dFlutterInput? inputBackend,
|
||||
DefaultGameDataDirectoryPersistence? dataDirectoryPersistence,
|
||||
}) : dataDirectoryPersistence =
|
||||
dataDirectoryPersistence ?? DefaultGameDataDirectoryPersistence(),
|
||||
super(
|
||||
audio: audioBackend ?? Wolf3dPlatformAudio(),
|
||||
input: inputBackend ?? Wolf3dFlutterInput(),
|
||||
) {
|
||||
if (debug) {
|
||||
enableDebug();
|
||||
}
|
||||
}
|
||||
|
||||
/// Persists and restores the preferred external game-data directory.
|
||||
final DefaultGameDataDirectoryPersistence dataDirectoryPersistence;
|
||||
|
||||
/// Last configured/loaded external game-data directory path.
|
||||
String? configuredDataDirectory;
|
||||
|
||||
/// Shared Flutter input adapter reused by gameplay screens.
|
||||
@override
|
||||
Wolf3dFlutterInput get input => super.input as Wolf3dFlutterInput;
|
||||
|
||||
/// Enables host-level debug affordances such as debug navigation UI.
|
||||
@override
|
||||
Wolf3dFlutterEngine enableDebug() {
|
||||
super.enableDebug();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Initializes the engine by loading available game data.
|
||||
///
|
||||
/// If [directory] is provided, it is persisted and treated as the primary
|
||||
/// external search root. If omitted, a previously persisted directory is
|
||||
/// used when available. [additionalDirectories] are scanned after the
|
||||
/// primary directory and are not persisted.
|
||||
///
|
||||
/// This method scans only configured external directories, deduplicating
|
||||
/// discovered versions by [GameVersion]. Shared package code does not bundle
|
||||
/// or import game data on behalf of host applications.
|
||||
Future<Wolf3dFlutterEngine> init({
|
||||
String? directory,
|
||||
Iterable<String>? additionalDirectories,
|
||||
}) async {
|
||||
await desktop_windowing_support.ensureDesktopWindowingInitialized();
|
||||
await audio.init();
|
||||
availableGames.clear();
|
||||
|
||||
final String? requestedDirectory = directory?.trim();
|
||||
final String? resolvedDirectory =
|
||||
requestedDirectory != null && requestedDirectory.isNotEmpty
|
||||
? requestedDirectory
|
||||
: await dataDirectoryPersistence.loadDataDirectory();
|
||||
configuredDataDirectory = resolvedDirectory;
|
||||
|
||||
if (requestedDirectory != null && requestedDirectory.isNotEmpty) {
|
||||
await dataDirectoryPersistence.saveDataDirectory(requestedDirectory);
|
||||
}
|
||||
|
||||
// On non-web platforms, also scan local filesystem locations for
|
||||
// user-supplied data folders so the host can pick up extra versions.
|
||||
final Set<String> directoriesToScan = <String>{};
|
||||
if (resolvedDirectory != null && resolvedDirectory.isNotEmpty) {
|
||||
directoriesToScan.add(resolvedDirectory);
|
||||
}
|
||||
|
||||
if (additionalDirectories != null) {
|
||||
for (final String directoryPath in additionalDirectories) {
|
||||
final String trimmedPath = directoryPath.trim();
|
||||
if (trimmedPath.isNotEmpty) {
|
||||
directoriesToScan.add(trimmedPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!kIsWeb && directoriesToScan.isNotEmpty) {
|
||||
for (final String directoryPath in directoriesToScan) {
|
||||
try {
|
||||
final externalGames = await WolfensteinLoader.discover(
|
||||
directoryPath: directoryPath,
|
||||
recursive: true,
|
||||
);
|
||||
for (var entry in externalGames.entries) {
|
||||
if (!availableGames.any((g) => g.version == entry.key)) {
|
||||
availableGames.add(entry.value);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("External discovery failed: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user