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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user