feat: Implement Change View and Renderer Options menus
- Added functionality to display and navigate the Change View menu in SixelRenderer and SoftwareRenderer. - Introduced methods to draw the Change View and Renderer Options menus, including handling cursor and selection states. - Updated WolfClassicMenuArt to include a customize label for the new menu. - Enhanced WolfMenuScreen to support new menu states. - Created tests for Change View menu interactions, ensuring proper transitions and renderer settings toggling. - Implemented persistence for renderer settings in Flutter, allowing settings to be saved and loaded from a local file. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
/// Shared renderer settings and capability metadata.
|
||||
library;
|
||||
|
||||
/// Engine-visible renderer modes used by host adapters and menu flow.
|
||||
enum WolfRendererMode {
|
||||
ascii('ASCII'),
|
||||
sixel('SIXEL'),
|
||||
software('SOFTWARE'),
|
||||
hardware('HARDWARE')
|
||||
;
|
||||
|
||||
const WolfRendererMode(this.label);
|
||||
|
||||
final String label;
|
||||
}
|
||||
|
||||
/// Renderer-specific toggles shown by the Change View customization submenu.
|
||||
enum WolfRendererOptionId {
|
||||
asciiTheme,
|
||||
hardwareEffects,
|
||||
fpsCounter,
|
||||
}
|
||||
|
||||
/// Host-reported renderer capability set used to filter visible menu options.
|
||||
class WolfRendererCapabilities {
|
||||
const WolfRendererCapabilities({
|
||||
required this.supportedModes,
|
||||
this.supportsAsciiThemes = false,
|
||||
this.supportsHardwareEffects = false,
|
||||
this.supportsFpsCounter = false,
|
||||
});
|
||||
|
||||
final Set<WolfRendererMode> supportedModes;
|
||||
final bool supportsAsciiThemes;
|
||||
final bool supportsHardwareEffects;
|
||||
final bool supportsFpsCounter;
|
||||
|
||||
bool supportsMode(WolfRendererMode mode) => supportedModes.contains(mode);
|
||||
}
|
||||
|
||||
/// Engine-owned renderer settings that both menu flow and host shortcuts mutate.
|
||||
class WolfRendererSettings {
|
||||
const WolfRendererSettings({
|
||||
this.mode = WolfRendererMode.software,
|
||||
this.asciiThemeId = asciiThemeBlocks,
|
||||
this.hardwareEffectsEnabled = false,
|
||||
this.fpsCounterEnabled = false,
|
||||
});
|
||||
|
||||
static const String asciiThemeBlocks = 'blocks';
|
||||
static const String asciiThemeQuadrant = 'quadrant';
|
||||
|
||||
static const List<String> asciiThemeIds = <String>[
|
||||
asciiThemeBlocks,
|
||||
asciiThemeQuadrant,
|
||||
];
|
||||
|
||||
final WolfRendererMode mode;
|
||||
final String asciiThemeId;
|
||||
final bool hardwareEffectsEnabled;
|
||||
final bool fpsCounterEnabled;
|
||||
|
||||
WolfRendererSettings copyWith({
|
||||
WolfRendererMode? mode,
|
||||
String? asciiThemeId,
|
||||
bool? hardwareEffectsEnabled,
|
||||
bool? fpsCounterEnabled,
|
||||
}) {
|
||||
return WolfRendererSettings(
|
||||
mode: mode ?? this.mode,
|
||||
asciiThemeId: asciiThemeId ?? this.asciiThemeId,
|
||||
hardwareEffectsEnabled:
|
||||
hardwareEffectsEnabled ?? this.hardwareEffectsEnabled,
|
||||
fpsCounterEnabled: fpsCounterEnabled ?? this.fpsCounterEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
WolfRendererSettings cycleAsciiTheme() {
|
||||
final int current = asciiThemeIds.indexOf(asciiThemeId);
|
||||
final int next = current == -1 ? 0 : (current + 1) % asciiThemeIds.length;
|
||||
return copyWith(asciiThemeId: asciiThemeIds[next]);
|
||||
}
|
||||
|
||||
static String asciiThemeLabel(String id) {
|
||||
switch (id) {
|
||||
case asciiThemeQuadrant:
|
||||
return 'CHARSET: QUADRANT';
|
||||
case asciiThemeBlocks:
|
||||
default:
|
||||
return 'CHARSET: BLOCKS';
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> toJson() {
|
||||
return <String, Object>{
|
||||
'mode': mode.name,
|
||||
'asciiThemeId': asciiThemeId,
|
||||
'hardwareEffectsEnabled': hardwareEffectsEnabled,
|
||||
'fpsCounterEnabled': fpsCounterEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
static WolfRendererSettings fromJson(Map<String, Object?> json) {
|
||||
final Object? modeRaw = json['mode'];
|
||||
final WolfRendererMode mode = WolfRendererMode.values.firstWhere(
|
||||
(candidate) => candidate.name == modeRaw,
|
||||
orElse: () => WolfRendererMode.software,
|
||||
);
|
||||
|
||||
final Object? themeRaw = json['asciiThemeId'];
|
||||
final String asciiThemeId =
|
||||
themeRaw is String && asciiThemeIds.contains(themeRaw)
|
||||
? themeRaw
|
||||
: asciiThemeBlocks;
|
||||
|
||||
return WolfRendererSettings(
|
||||
mode: mode,
|
||||
asciiThemeId: asciiThemeId,
|
||||
hardwareEffectsEnabled: json['hardwareEffectsEnabled'] == true,
|
||||
fpsCounterEnabled: json['fpsCounterEnabled'] == true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/// Platform-agnostic persistence contract for renderer settings.
|
||||
library;
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:wolf_3d_dart/src/engine/rendering/renderer_settings.dart';
|
||||
|
||||
/// Abstract contract that host adapters must implement to persist settings.
|
||||
abstract class RendererSettingsPersistence {
|
||||
/// Loads previously saved settings, returning `null` on first run or error.
|
||||
Future<WolfRendererSettings?> load();
|
||||
|
||||
/// Saves [settings] asynchronously; failures should be silently absorbed.
|
||||
Future<void> save(WolfRendererSettings settings);
|
||||
}
|
||||
|
||||
/// Mixin that handles JSON encode/decode so adapters only need to implement
|
||||
/// raw string read/write.
|
||||
mixin JsonRendererSettingsPersistence on RendererSettingsPersistence {
|
||||
Future<String?> readRaw();
|
||||
Future<void> writeRaw(String json);
|
||||
|
||||
@override
|
||||
Future<WolfRendererSettings?> load() async {
|
||||
try {
|
||||
final String? raw = await readRaw();
|
||||
if (raw == null || raw.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final Object? decoded = jsonDecode(raw);
|
||||
if (decoded is! Map<String, Object?>) {
|
||||
return null;
|
||||
}
|
||||
return WolfRendererSettings.fromJson(decoded);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(WolfRendererSettings settings) async {
|
||||
try {
|
||||
await writeRaw(jsonEncode(settings.toJson()));
|
||||
} catch (_) {
|
||||
// Best-effort — never break the game loop on persistence failure.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,20 @@ class WolfEngine {
|
||||
this.onQuit,
|
||||
this.onGameSelected,
|
||||
this.onEpisodeSelected,
|
||||
this.onRendererSettingsChanged,
|
||||
WolfRendererCapabilities? rendererCapabilities,
|
||||
WolfRendererSettings? rendererSettings,
|
||||
EngineAudio? engineAudio,
|
||||
}) : assert(
|
||||
}) : rendererCapabilities =
|
||||
rendererCapabilities ??
|
||||
const WolfRendererCapabilities(
|
||||
supportedModes: <WolfRendererMode>{
|
||||
WolfRendererMode.software,
|
||||
},
|
||||
supportsFpsCounter: true,
|
||||
),
|
||||
rendererSettings = rendererSettings ?? const WolfRendererSettings(),
|
||||
assert(
|
||||
data != null || (availableGames != null && availableGames.isNotEmpty),
|
||||
'Provide either data or a non-empty availableGames list.',
|
||||
),
|
||||
@@ -44,6 +56,8 @@ class WolfEngine {
|
||||
throw StateError('WolfEngine requires at least one game data set.');
|
||||
}
|
||||
menuManager.menuBackgroundRgb = menuBackgroundRgb;
|
||||
_normalizeRendererSettings();
|
||||
_syncRendererMenuModel();
|
||||
}
|
||||
|
||||
/// Total milliseconds elapsed since the engine was initialized.
|
||||
@@ -106,6 +120,15 @@ class WolfEngine {
|
||||
/// Callback triggered when episode selection changes; `null` means cleared.
|
||||
final void Function(int? episodeIndex)? onEpisodeSelected;
|
||||
|
||||
/// Callback triggered whenever renderer settings are updated by the engine.
|
||||
final void Function(WolfRendererSettings settings)? onRendererSettingsChanged;
|
||||
|
||||
/// Host-reported mode/effect capabilities that drive menu visibility.
|
||||
WolfRendererCapabilities rendererCapabilities;
|
||||
|
||||
/// Engine-owned renderer settings shared between menus and host shortcuts.
|
||||
WolfRendererSettings rendererSettings;
|
||||
|
||||
// --- State Managers ---
|
||||
|
||||
/// Manages the state and animation of doors throughout the level.
|
||||
@@ -219,6 +242,169 @@ class WolfEngine {
|
||||
log("[DEBUG] FrameBuffer resized to ${width}x$height");
|
||||
}
|
||||
|
||||
/// Replaces host capability metadata and refreshes menu visibility.
|
||||
void setRendererCapabilities(WolfRendererCapabilities capabilities) {
|
||||
rendererCapabilities = capabilities;
|
||||
_normalizeRendererSettings();
|
||||
_syncRendererMenuModel();
|
||||
}
|
||||
|
||||
/// Applies [settings], notifies host, and refreshes renderer menu visibility.
|
||||
void updateRendererSettings(WolfRendererSettings settings) {
|
||||
rendererSettings = settings;
|
||||
_normalizeRendererSettings();
|
||||
showFpsCounter = rendererSettings.fpsCounterEnabled;
|
||||
_syncRendererMenuModel();
|
||||
onRendererSettingsChanged?.call(rendererSettings);
|
||||
}
|
||||
|
||||
/// Cycles to the next available renderer mode supported by the host.
|
||||
void cycleRendererMode() {
|
||||
final List<WolfRendererMode> available = rendererCapabilities.supportedModes
|
||||
.toList(growable: false);
|
||||
if (available.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int current = available.indexOf(rendererSettings.mode);
|
||||
final int next = current == -1 ? 0 : (current + 1) % available.length;
|
||||
updateRendererSettings(rendererSettings.copyWith(mode: available[next]));
|
||||
}
|
||||
|
||||
/// Cycles the ASCII character set when the host supports ASCII rendering.
|
||||
void cycleAsciiTheme() {
|
||||
if (!rendererCapabilities.supportsAsciiThemes) {
|
||||
return;
|
||||
}
|
||||
updateRendererSettings(rendererSettings.cycleAsciiTheme());
|
||||
}
|
||||
|
||||
/// Toggles CRT/post-processing effects for hardware renderer hosts.
|
||||
void toggleHardwareEffects() {
|
||||
if (!rendererCapabilities.supportsHardwareEffects) {
|
||||
return;
|
||||
}
|
||||
updateRendererSettings(
|
||||
rendererSettings.copyWith(
|
||||
hardwareEffectsEnabled: !rendererSettings.hardwareEffectsEnabled,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Toggles the shared FPS counter setting.
|
||||
void toggleFpsCounter() {
|
||||
if (!rendererCapabilities.supportsFpsCounter) {
|
||||
return;
|
||||
}
|
||||
updateRendererSettings(
|
||||
rendererSettings.copyWith(
|
||||
fpsCounterEnabled: !rendererSettings.fpsCounterEnabled,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _normalizeRendererSettings() {
|
||||
final Set<WolfRendererMode> supported = rendererCapabilities.supportedModes;
|
||||
if (supported.isEmpty) {
|
||||
rendererSettings = rendererSettings.copyWith(
|
||||
mode: WolfRendererMode.software,
|
||||
);
|
||||
} else if (!supported.contains(rendererSettings.mode)) {
|
||||
rendererSettings = rendererSettings.copyWith(mode: supported.first);
|
||||
}
|
||||
|
||||
if (!WolfRendererSettings.asciiThemeIds.contains(
|
||||
rendererSettings.asciiThemeId,
|
||||
)) {
|
||||
rendererSettings = rendererSettings.copyWith(
|
||||
asciiThemeId: WolfRendererSettings.asciiThemeBlocks,
|
||||
);
|
||||
}
|
||||
|
||||
if (!rendererCapabilities.supportsHardwareEffects &&
|
||||
rendererSettings.hardwareEffectsEnabled) {
|
||||
rendererSettings = rendererSettings.copyWith(
|
||||
hardwareEffectsEnabled: false,
|
||||
);
|
||||
}
|
||||
|
||||
if (!rendererCapabilities.supportsFpsCounter &&
|
||||
rendererSettings.fpsCounterEnabled) {
|
||||
rendererSettings = rendererSettings.copyWith(fpsCounterEnabled: false);
|
||||
}
|
||||
showFpsCounter = rendererSettings.fpsCounterEnabled;
|
||||
}
|
||||
|
||||
void _syncRendererMenuModel() {
|
||||
final WolfRendererMode activeMode = rendererSettings.mode;
|
||||
final List<WolfMenuRendererOptionEntry> options =
|
||||
<WolfMenuRendererOptionEntry>[];
|
||||
|
||||
if (activeMode == WolfRendererMode.ascii &&
|
||||
rendererCapabilities.supportsAsciiThemes) {
|
||||
options.add(
|
||||
WolfMenuRendererOptionEntry(
|
||||
id: WolfRendererOptionId.asciiTheme,
|
||||
label: WolfRendererSettings.asciiThemeLabel(
|
||||
rendererSettings.asciiThemeId,
|
||||
),
|
||||
isChecked: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (activeMode == WolfRendererMode.hardware &&
|
||||
rendererCapabilities.supportsHardwareEffects) {
|
||||
options.add(
|
||||
WolfMenuRendererOptionEntry(
|
||||
id: WolfRendererOptionId.hardwareEffects,
|
||||
label: 'EFFECTS',
|
||||
isChecked: rendererSettings.hardwareEffectsEnabled,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (rendererCapabilities.supportsFpsCounter) {
|
||||
options.add(
|
||||
WolfMenuRendererOptionEntry(
|
||||
id: WolfRendererOptionId.fpsCounter,
|
||||
label: 'FPS COUNTER',
|
||||
isChecked: rendererSettings.fpsCounterEnabled,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final List<WolfMenuRendererEntry> rendererEntries =
|
||||
<WolfMenuRendererEntry>[];
|
||||
|
||||
void addRenderer(WolfRendererMode mode) {
|
||||
if (!rendererCapabilities.supportsMode(mode)) {
|
||||
return;
|
||||
}
|
||||
rendererEntries.add(
|
||||
WolfMenuRendererEntry(
|
||||
mode: mode,
|
||||
label: mode.label,
|
||||
hasOptions: false,
|
||||
isEnabled: true,
|
||||
isChecked: rendererSettings.mode == mode,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
addRenderer(WolfRendererMode.hardware);
|
||||
addRenderer(WolfRendererMode.software);
|
||||
addRenderer(WolfRendererMode.sixel);
|
||||
addRenderer(WolfRendererMode.ascii);
|
||||
|
||||
menuManager.setChangeViewEntries(rendererEntries);
|
||||
|
||||
menuManager.setRendererOptionEntries(
|
||||
title: 'CUSTOMIZE ${activeMode.label}',
|
||||
entries: options,
|
||||
);
|
||||
}
|
||||
|
||||
/// The primary heartbeat of the engine.
|
||||
///
|
||||
/// Updates all world subsystems based on the [elapsed] time.
|
||||
@@ -312,6 +498,12 @@ class WolfEngine {
|
||||
case WolfMenuScreen.difficultySelect:
|
||||
_tickDifficultySelectionMenu(input);
|
||||
break;
|
||||
case WolfMenuScreen.changeView:
|
||||
_tickChangeViewMenu(input);
|
||||
break;
|
||||
case WolfMenuScreen.rendererOptions:
|
||||
_tickRendererOptionsMenu(input);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,11 +537,14 @@ class WolfEngine {
|
||||
case WolfMenuMainAction.backToDemo:
|
||||
_exitTopLevelMenu();
|
||||
break;
|
||||
case WolfMenuMainAction.changeView:
|
||||
_syncRendererMenuModel();
|
||||
menuManager.showChangeViewMenu();
|
||||
break;
|
||||
case WolfMenuMainAction.sound:
|
||||
case WolfMenuMainAction.control:
|
||||
case WolfMenuMainAction.loadGame:
|
||||
case WolfMenuMainAction.saveGame:
|
||||
case WolfMenuMainAction.changeView:
|
||||
case WolfMenuMainAction.readThis:
|
||||
case WolfMenuMainAction.viewScores:
|
||||
case null:
|
||||
@@ -413,6 +608,57 @@ class WolfEngine {
|
||||
}
|
||||
}
|
||||
|
||||
void _tickChangeViewMenu(EngineInput input) {
|
||||
final menuResult = menuManager.updateChangeViewMenu(input);
|
||||
if (menuResult.goBack) {
|
||||
menuManager.showMainMenu(hasResumableGame: _hasActiveSession);
|
||||
return;
|
||||
}
|
||||
|
||||
if (menuResult.selectedMode != null) {
|
||||
updateRendererSettings(
|
||||
rendererSettings.copyWith(mode: menuResult.selectedMode),
|
||||
);
|
||||
}
|
||||
|
||||
switch (menuResult.selectedOption) {
|
||||
case WolfRendererOptionId.asciiTheme:
|
||||
cycleAsciiTheme();
|
||||
break;
|
||||
case WolfRendererOptionId.hardwareEffects:
|
||||
toggleHardwareEffects();
|
||||
break;
|
||||
case WolfRendererOptionId.fpsCounter:
|
||||
toggleFpsCounter();
|
||||
break;
|
||||
case null:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _tickRendererOptionsMenu(EngineInput input) {
|
||||
final menuResult = menuManager.updateRendererOptionsMenu(input);
|
||||
if (menuResult.goBack) {
|
||||
_syncRendererMenuModel();
|
||||
menuManager.showChangeViewMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (menuResult.selectedOption) {
|
||||
case WolfRendererOptionId.asciiTheme:
|
||||
cycleAsciiTheme();
|
||||
break;
|
||||
case WolfRendererOptionId.hardwareEffects:
|
||||
toggleHardwareEffects();
|
||||
break;
|
||||
case WolfRendererOptionId.fpsCounter:
|
||||
toggleFpsCounter();
|
||||
break;
|
||||
case null:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _beginNewGameMenuFlow() {
|
||||
onEpisodeSelected?.call(null);
|
||||
menuManager.clearEpisodeSelection();
|
||||
|
||||
@@ -7,6 +7,8 @@ enum WolfMenuScreen {
|
||||
gameSelect,
|
||||
episodeSelect,
|
||||
difficultySelect,
|
||||
changeView,
|
||||
rendererOptions,
|
||||
}
|
||||
|
||||
enum WolfIntroSlide { retailWarning, pg13, title }
|
||||
@@ -40,6 +42,36 @@ class WolfMenuMainEntry {
|
||||
final bool isEnabled;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool _isWiredMainMenuAction(WolfMenuMainAction action) {
|
||||
switch (action) {
|
||||
case WolfMenuMainAction.newGame:
|
||||
@@ -48,11 +80,12 @@ bool _isWiredMainMenuAction(WolfMenuMainAction action) {
|
||||
case WolfMenuMainAction.backToDemo:
|
||||
case WolfMenuMainAction.quit:
|
||||
return true;
|
||||
case WolfMenuMainAction.changeView:
|
||||
return true;
|
||||
case WolfMenuMainAction.sound:
|
||||
case WolfMenuMainAction.control:
|
||||
case WolfMenuMainAction.loadGame:
|
||||
case WolfMenuMainAction.saveGame:
|
||||
case WolfMenuMainAction.changeView:
|
||||
case WolfMenuMainAction.readThis:
|
||||
case WolfMenuMainAction.viewScores:
|
||||
return false;
|
||||
@@ -85,6 +118,13 @@ class MenuManager {
|
||||
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;
|
||||
int _gameCount = 1;
|
||||
|
||||
@@ -161,6 +201,18 @@ class MenuManager {
|
||||
|
||||
int get selectedEpisodeIndex => _selectedEpisodeIndex;
|
||||
|
||||
int get selectedChangeViewIndex => _selectedChangeViewIndex;
|
||||
|
||||
int get selectedRendererOptionIndex => _selectedRendererOptionIndex;
|
||||
|
||||
String get rendererOptionsTitle => _rendererOptionsTitle;
|
||||
|
||||
List<WolfMenuRendererEntry> get changeViewEntries =>
|
||||
List<WolfMenuRendererEntry>.unmodifiable(_changeViewEntries);
|
||||
|
||||
List<WolfMenuRendererOptionEntry> get rendererOptionEntries =>
|
||||
List<WolfMenuRendererOptionEntry>.unmodifiable(_rendererOptionEntries);
|
||||
|
||||
List<WolfMenuMainEntry> get mainMenuEntries {
|
||||
return List<WolfMenuMainEntry>.unmodifiable(
|
||||
<WolfMenuMainEntry>[
|
||||
@@ -290,6 +342,133 @@ class MenuManager {
|
||||
_resetEdgeState();
|
||||
}
|
||||
|
||||
int get _changeViewItemCount =>
|
||||
_changeViewEntries.length + _rendererOptionEntries.length;
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showChangeViewMenu() {
|
||||
_activeMenu = WolfMenuScreen.changeView;
|
||||
_selectedChangeViewIndex = _changeViewItemCount == 0
|
||||
? 0
|
||||
: _findSelectableIndex(
|
||||
0,
|
||||
_changeViewItemCount,
|
||||
_isSelectableChangeViewIndex,
|
||||
);
|
||||
_transitionTarget = null;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
_resetEdgeState();
|
||||
}
|
||||
|
||||
void showRendererOptionsMenu() {
|
||||
_activeMenu = WolfMenuScreen.rendererOptions;
|
||||
_selectedRendererOptionIndex = _rendererOptionEntries.isEmpty
|
||||
? 0
|
||||
: _findSelectableIndex(
|
||||
0,
|
||||
_rendererOptionEntries.length,
|
||||
_isSelectableRendererOptionIndex,
|
||||
);
|
||||
_transitionTarget = null;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
_resetEdgeState();
|
||||
}
|
||||
|
||||
/// Starts a menu transition. Input is locked until it completes.
|
||||
///
|
||||
/// Hosts can reuse this fade timing for future pre-menu splash/image
|
||||
@@ -453,6 +632,88 @@ class MenuManager {
|
||||
);
|
||||
}
|
||||
|
||||
({
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
({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,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a menu action snapshot for this frame.
|
||||
({Difficulty? selected, bool goBack}) updateDifficultySelection(
|
||||
EngineInput input,
|
||||
@@ -612,6 +873,27 @@ class MenuManager {
|
||||
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;
|
||||
}
|
||||
|
||||
WolfMenuMainEntry _mainMenuEntry({
|
||||
required WolfMenuMainAction action,
|
||||
required String label,
|
||||
|
||||
@@ -31,6 +31,7 @@ class RetailMenuPicModule extends MenuPicModule {
|
||||
MenuPicKey.footer: 15, // C_MOUSELBACKPIC (footer art)
|
||||
MenuPicKey.heading: 3, // H_TOPWINDOWPIC
|
||||
MenuPicKey.optionsLabel: 7, // C_OPTIONSPIC
|
||||
MenuPicKey.customizeLabel: 24, // C_CUSTOMIZEPIC
|
||||
// --- Cursor / markers ---
|
||||
MenuPicKey.cursorActive: 8, // C_CURSOR1PIC
|
||||
MenuPicKey.cursorInactive: 9, // C_CURSOR2PIC
|
||||
|
||||
@@ -31,6 +31,7 @@ class SharewareMenuPicModule extends MenuPicModule {
|
||||
MenuPicKey.footer: 15,
|
||||
MenuPicKey.heading: 3,
|
||||
MenuPicKey.optionsLabel: 7,
|
||||
MenuPicKey.customizeLabel: 36,
|
||||
MenuPicKey.cursorActive: 8,
|
||||
MenuPicKey.cursorInactive: 9,
|
||||
MenuPicKey.markerSelected: 11,
|
||||
@@ -57,6 +58,7 @@ class SharewareMenuPicModule extends MenuPicModule {
|
||||
MenuPicKey.footer: 27,
|
||||
MenuPicKey.heading: 14,
|
||||
MenuPicKey.optionsLabel: 19,
|
||||
MenuPicKey.customizeLabel: 36,
|
||||
MenuPicKey.cursorActive: 20,
|
||||
MenuPicKey.cursorInactive: 21,
|
||||
MenuPicKey.markerSelected: 23,
|
||||
|
||||
@@ -10,6 +10,7 @@ enum MenuPicKey {
|
||||
footer('footer'),
|
||||
heading('heading'),
|
||||
optionsLabel('optionsLabel'),
|
||||
customizeLabel('customizeLabel'),
|
||||
|
||||
// --- Cursor / selection markers ---
|
||||
cursorActive('cursorActive'),
|
||||
|
||||
@@ -642,6 +642,163 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (engine.menuManager.activeMenu == WolfMenuScreen.changeView) {
|
||||
_drawCustomizeMenuHeader(art, headingColor, bgColor);
|
||||
final cursor = art.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
final selectedMarker = art.selectedMarker;
|
||||
final unselectedMarker = art.unselectedMarker;
|
||||
const int rowYStart = 66;
|
||||
const int rowStep = 18;
|
||||
const int cursorX = 62;
|
||||
const int markerX = 92;
|
||||
const int textX = 122;
|
||||
final entries = engine.menuManager.changeViewEntries;
|
||||
final optionEntries = engine.menuManager.rendererOptionEntries;
|
||||
final int modeCount = entries.length;
|
||||
final int optionCount = optionEntries.length;
|
||||
|
||||
const int modesPanelY = 52;
|
||||
final int modesContentHeight = modeCount <= 0
|
||||
? 0
|
||||
: ((modeCount - 1) * rowStep) + 12;
|
||||
final int modesPanelHeight = math.max(56, modesContentHeight + 14);
|
||||
final int sectionHeaderY = modesPanelY + modesPanelHeight + 6;
|
||||
final int optionsPanelY = sectionHeaderY + 14;
|
||||
final int optionsContentHeight = optionCount <= 0
|
||||
? 0
|
||||
: ((optionCount - 1) * 15) + 12;
|
||||
final int optionsPanelHeight = math.max(30, optionsContentHeight + 10);
|
||||
|
||||
_fillRect320(46, modesPanelY, 228, modesPanelHeight, panelColor);
|
||||
_fillRect320(46, optionsPanelY, 228, optionsPanelHeight, panelColor);
|
||||
|
||||
for (int i = 0; i < entries.length; i++) {
|
||||
final int y = rowYStart + (i * rowStep);
|
||||
final bool isSelected = i == engine.menuManager.selectedChangeViewIndex;
|
||||
if (isSelected && cursor != null) {
|
||||
_blitVgaImageAscii(cursor, cursorX, y - 2);
|
||||
}
|
||||
|
||||
final entry = entries[i];
|
||||
final marker = entry.isChecked ? selectedMarker : unselectedMarker;
|
||||
if (marker != null) {
|
||||
_blitVgaImageAscii(marker, markerX, y);
|
||||
}
|
||||
final int textColor = entry.isEnabled
|
||||
? (isSelected ? selectedTextColor : unselectedTextColor)
|
||||
: disabledTextColor;
|
||||
_drawMenuLabelAdaptive(
|
||||
typography: menuTypography,
|
||||
text: entry.label,
|
||||
textX320: textX,
|
||||
y200: y + 1,
|
||||
color: textColor,
|
||||
panelX320: 46,
|
||||
panelW320: 228,
|
||||
panelColor: panelColor,
|
||||
);
|
||||
}
|
||||
|
||||
_drawMenuSectionHeader(
|
||||
text: engine.menuManager.rendererOptionsTitle,
|
||||
y200: sectionHeaderY,
|
||||
);
|
||||
|
||||
const int optionsRowStep = 15;
|
||||
final int optionsRowsHeight = optionCount <= 0
|
||||
? 0
|
||||
: ((optionCount - 1) * optionsRowStep) + 10;
|
||||
final int optionsRowStart =
|
||||
optionsPanelY +
|
||||
((optionsPanelHeight - optionsRowsHeight) ~/ 2).clamp(0, 200);
|
||||
for (int i = 0; i < optionEntries.length; i++) {
|
||||
final int optionIndex = modeCount + i;
|
||||
final bool isSelected =
|
||||
optionIndex == engine.menuManager.selectedChangeViewIndex;
|
||||
final int y = optionsRowStart + (i * optionsRowStep);
|
||||
if (isSelected && cursor != null) {
|
||||
_blitVgaImageAscii(cursor, cursorX, y - 2);
|
||||
}
|
||||
final entry = optionEntries[i];
|
||||
final marker = entry.isChecked ? selectedMarker : unselectedMarker;
|
||||
if (marker != null) {
|
||||
_blitVgaImageAscii(marker, markerX, y);
|
||||
}
|
||||
final int textColor = entry.isEnabled
|
||||
? (isSelected ? selectedTextColor : unselectedTextColor)
|
||||
: disabledTextColor;
|
||||
_drawMenuLabelAdaptive(
|
||||
typography: menuTypography,
|
||||
text: entry.label,
|
||||
textX320: textX,
|
||||
y200: y + 1,
|
||||
color: textColor,
|
||||
panelX320: 46,
|
||||
panelW320: 228,
|
||||
panelColor: panelColor,
|
||||
);
|
||||
}
|
||||
|
||||
_drawCenteredMenuFooter();
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
if (engine.menuManager.activeMenu == WolfMenuScreen.rendererOptions) {
|
||||
_drawCustomizeMenuHeader(art, headingColor, bgColor);
|
||||
_fillRect320(56, 52, 208, 120, panelColor);
|
||||
_drawMenuTextCentered(
|
||||
engine.menuManager.rendererOptionsTitle,
|
||||
46,
|
||||
headingColor,
|
||||
scale: 1,
|
||||
);
|
||||
|
||||
final cursor = art.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
final selectedMarker = art.selectedMarker;
|
||||
final unselectedMarker = art.unselectedMarker;
|
||||
const int rowYStart = 68;
|
||||
const int rowStep = 20;
|
||||
const int cursorX = 62;
|
||||
const int markerX = 92;
|
||||
const int textX = 122;
|
||||
final entries = engine.menuManager.rendererOptionEntries;
|
||||
for (int i = 0; i < entries.length; i++) {
|
||||
final int y = rowYStart + (i * rowStep);
|
||||
final bool isSelected =
|
||||
i == engine.menuManager.selectedRendererOptionIndex;
|
||||
final entry = entries[i];
|
||||
if (isSelected && cursor != null) {
|
||||
_blitVgaImageAscii(cursor, cursorX, y - 2);
|
||||
}
|
||||
final marker = entry.isChecked ? selectedMarker : unselectedMarker;
|
||||
if (marker != null) {
|
||||
_blitVgaImageAscii(marker, markerX, y);
|
||||
}
|
||||
final int textColor = entry.isEnabled
|
||||
? (isSelected ? selectedTextColor : unselectedTextColor)
|
||||
: disabledTextColor;
|
||||
_drawMenuLabelAdaptive(
|
||||
typography: menuTypography,
|
||||
text: entry.label,
|
||||
textX320: textX,
|
||||
y200: y + 1,
|
||||
color: textColor,
|
||||
panelX320: 56,
|
||||
panelW320: 208,
|
||||
panelColor: panelColor,
|
||||
);
|
||||
}
|
||||
|
||||
_drawCenteredMenuFooter();
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
final int selectedDifficultyIndex =
|
||||
engine.menuManager.selectedDifficultyIndex;
|
||||
|
||||
@@ -997,7 +1154,10 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
int scale = 1,
|
||||
}) {
|
||||
if (y200 == _headerHeadingY) {
|
||||
y200 = _centerHeaderTitleInBlackBand(defaultY: y200, scale: scale);
|
||||
y200 = _centerHeaderTitleInBlackBand(
|
||||
defaultY: y200,
|
||||
scale: scale,
|
||||
);
|
||||
}
|
||||
final int textWidth = WolfMenuFont.measureTextWidth(text, scale);
|
||||
final int x320 = ((320 - textWidth) ~/ 2).clamp(0, 319);
|
||||
@@ -1156,6 +1316,103 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
||||
}
|
||||
}
|
||||
|
||||
void _drawCustomizeMenuHeader(
|
||||
WolfClassicMenuArt art,
|
||||
int headingColor,
|
||||
int backgroundColor,
|
||||
) {
|
||||
_drawHeaderBarStack(
|
||||
headingY200: _headerHeadingY,
|
||||
backgroundColor: backgroundColor,
|
||||
barColor: ColorPalette.vga32Bit[0],
|
||||
);
|
||||
|
||||
final VgaImage? heading = art.customizeLabel ?? art.optionsLabel;
|
||||
if (heading != null) {
|
||||
final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
|
||||
_blitVgaImageAscii(heading, headingX, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
_drawMenuTextCentered('CUSTOMIZE', _headerHeadingY, headingColor, scale: 2);
|
||||
}
|
||||
|
||||
void _drawMenuSectionHeader({
|
||||
required String text,
|
||||
required int y200,
|
||||
}) {
|
||||
final int textW = WolfMenuFont.measureTextWidth(text, 1);
|
||||
final int boxW = (textW + 28).clamp(110, 250);
|
||||
final int x = ((320 - boxW) ~/ 2).clamp(0, 319);
|
||||
final int outer = _rgbToPaletteColor(0x727272);
|
||||
final int inner = _rgbToPaletteColor(0x8E8E8E);
|
||||
final int fill = _rgbToPaletteColor(0xC9C9C9);
|
||||
final int textColor = _rgbToPaletteColor(0x5A5A5A);
|
||||
|
||||
_fillRect320(x, y200, boxW, 12, outer);
|
||||
_fillRect320(x + 1, y200 + 1, boxW - 2, 10, inner);
|
||||
_fillRect320(x + 2, y200 + 2, boxW - 4, 8, fill);
|
||||
_drawMenuLabelAdaptive(
|
||||
typography: _resolveMenuTypography(),
|
||||
text: text,
|
||||
textX320: x + 8,
|
||||
y200: y200 + 2,
|
||||
color: textColor,
|
||||
panelX320: x + 2,
|
||||
panelW320: boxW - 4,
|
||||
panelColor: fill,
|
||||
centerWhenCompact: true,
|
||||
);
|
||||
}
|
||||
|
||||
void _drawMenuLabelAdaptive({
|
||||
required _AsciiMenuTypography typography,
|
||||
required String text,
|
||||
required int textX320,
|
||||
required int y200,
|
||||
required int color,
|
||||
required int panelX320,
|
||||
required int panelW320,
|
||||
required int panelColor,
|
||||
bool centerWhenCompact = false,
|
||||
}) {
|
||||
if (!typography.usesCompactRows) {
|
||||
_drawMenuText(text, textX320, y200, color);
|
||||
return;
|
||||
}
|
||||
|
||||
if (centerWhenCompact) {
|
||||
final int panelX = _menuX320ToColumn(panelX320);
|
||||
final int panelW = math.max(
|
||||
1,
|
||||
((panelW320 / 320.0) * projectionWidth).toInt(),
|
||||
);
|
||||
final int textW = text.length;
|
||||
final int centeredLeft = panelX + math.max(0, ((panelW - textW) ~/ 2));
|
||||
_writeLeftClipped(
|
||||
_menuY200ToRow(y200),
|
||||
text,
|
||||
color,
|
||||
panelColor,
|
||||
math.max(1, panelW - 2),
|
||||
centeredLeft,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_drawMinimalMenuRows(
|
||||
rows: <String>[text],
|
||||
selectedIndex: -1,
|
||||
rowYStart200: y200,
|
||||
rowStep200: 1,
|
||||
textX320: textX320,
|
||||
panelX320: panelX320,
|
||||
panelW320: panelW320,
|
||||
panelColor: panelColor,
|
||||
colorForRow: (_, _) => color,
|
||||
);
|
||||
}
|
||||
|
||||
void _drawCenteredMenuFooter() {
|
||||
if (_usesTerminalLayout && !_emitAnsi) {
|
||||
_drawFlutterGridMenuFooter();
|
||||
|
||||
@@ -526,6 +526,146 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (engine.menuManager.activeMenu == WolfMenuScreen.changeView) {
|
||||
_drawCustomizeMenuHeader(art, headingIndex, bgColor);
|
||||
final cursor = art.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
final selectedMarker = art.selectedMarker;
|
||||
final unselectedMarker = art.unselectedMarker;
|
||||
const int rowYStart = 66;
|
||||
const int rowStep = 18;
|
||||
const int cursorX = 62;
|
||||
const int markerX = 92;
|
||||
const int textX = 122;
|
||||
final entries = engine.menuManager.changeViewEntries;
|
||||
final optionEntries = engine.menuManager.rendererOptionEntries;
|
||||
final int modeCount = entries.length;
|
||||
final int optionCount = optionEntries.length;
|
||||
|
||||
const int modesPanelY = 52;
|
||||
final int modesContentHeight = modeCount <= 0
|
||||
? 0
|
||||
: ((modeCount - 1) * rowStep) + 12;
|
||||
final int modesPanelHeight = math.max(56, modesContentHeight + 14);
|
||||
final int sectionHeaderY = modesPanelY + modesPanelHeight + 6;
|
||||
final int optionsPanelY = sectionHeaderY + 14;
|
||||
final int optionsContentHeight = optionCount <= 0
|
||||
? 0
|
||||
: ((optionCount - 1) * 15) + 12;
|
||||
final int optionsPanelHeight = math.max(30, optionsContentHeight + 10);
|
||||
|
||||
_fillRect320(46, modesPanelY, 228, modesPanelHeight, panelColor);
|
||||
_fillRect320(46, optionsPanelY, 228, optionsPanelHeight, panelColor);
|
||||
for (int i = 0; i < entries.length; i++) {
|
||||
final int y = rowYStart + (i * rowStep);
|
||||
final bool isSelected = i == engine.menuManager.selectedChangeViewIndex;
|
||||
if (isSelected && cursor != null) {
|
||||
_blitVgaImage(cursor, cursorX, y - 2);
|
||||
}
|
||||
|
||||
final entry = entries[i];
|
||||
final marker = entry.isChecked ? selectedMarker : unselectedMarker;
|
||||
if (marker != null) {
|
||||
_blitVgaImage(marker, markerX, y);
|
||||
}
|
||||
_drawMenuText(
|
||||
entry.label,
|
||||
textX,
|
||||
y + 1,
|
||||
entry.isEnabled
|
||||
? (isSelected ? selectedTextIndex : unselectedTextIndex)
|
||||
: disabledTextIndex,
|
||||
scale: 1,
|
||||
);
|
||||
}
|
||||
|
||||
_drawMenuSectionHeader(
|
||||
text: engine.menuManager.rendererOptionsTitle,
|
||||
y200: sectionHeaderY,
|
||||
);
|
||||
|
||||
const int optionsRowStep = 15;
|
||||
final int optionsRowsHeight = optionCount <= 0
|
||||
? 0
|
||||
: ((optionCount - 1) * optionsRowStep) + 10;
|
||||
final int optionsRowStart =
|
||||
optionsPanelY +
|
||||
((optionsPanelHeight - optionsRowsHeight) ~/ 2).clamp(0, 200);
|
||||
for (int i = 0; i < optionEntries.length; i++) {
|
||||
final int optionIndex = modeCount + i;
|
||||
final bool isSelected =
|
||||
optionIndex == engine.menuManager.selectedChangeViewIndex;
|
||||
final int y = optionsRowStart + (i * optionsRowStep);
|
||||
if (isSelected && cursor != null) {
|
||||
_blitVgaImage(cursor, cursorX, y - 2);
|
||||
}
|
||||
final entry = optionEntries[i];
|
||||
final marker = entry.isChecked ? selectedMarker : unselectedMarker;
|
||||
if (marker != null) {
|
||||
_blitVgaImage(marker, markerX, y);
|
||||
}
|
||||
_drawMenuText(
|
||||
entry.label,
|
||||
textX,
|
||||
y + 1,
|
||||
entry.isEnabled
|
||||
? (isSelected ? selectedTextIndex : unselectedTextIndex)
|
||||
: disabledTextIndex,
|
||||
scale: 1,
|
||||
);
|
||||
}
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
if (engine.menuManager.activeMenu == WolfMenuScreen.rendererOptions) {
|
||||
_drawCustomizeMenuHeader(art, headingIndex, bgColor);
|
||||
_fillRect320(56, 52, 208, 120, panelColor);
|
||||
_drawMenuTextCentered(
|
||||
engine.menuManager.rendererOptionsTitle,
|
||||
46,
|
||||
headingIndex,
|
||||
scale: 1,
|
||||
);
|
||||
|
||||
final cursor = art.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
final selectedMarker = art.selectedMarker;
|
||||
final unselectedMarker = art.unselectedMarker;
|
||||
const int rowYStart = 68;
|
||||
const int rowStep = 20;
|
||||
const int cursorX = 62;
|
||||
const int markerX = 92;
|
||||
const int textX = 122;
|
||||
final entries = engine.menuManager.rendererOptionEntries;
|
||||
for (int i = 0; i < entries.length; i++) {
|
||||
final int y = rowYStart + (i * rowStep);
|
||||
final bool isSelected =
|
||||
i == engine.menuManager.selectedRendererOptionIndex;
|
||||
final entry = entries[i];
|
||||
if (isSelected && cursor != null) {
|
||||
_blitVgaImage(cursor, cursorX, y - 2);
|
||||
}
|
||||
final marker = entry.isChecked ? selectedMarker : unselectedMarker;
|
||||
if (marker != null) {
|
||||
_blitVgaImage(marker, markerX, y);
|
||||
}
|
||||
_drawMenuText(
|
||||
entry.label,
|
||||
textX,
|
||||
y + 1,
|
||||
entry.isEnabled
|
||||
? (isSelected ? selectedTextIndex : unselectedTextIndex)
|
||||
: disabledTextIndex,
|
||||
scale: 1,
|
||||
);
|
||||
}
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
return;
|
||||
}
|
||||
|
||||
final int selectedDifficultyIndex =
|
||||
engine.menuManager.selectedDifficultyIndex;
|
||||
_drawHeaderBarStack(
|
||||
@@ -580,6 +720,45 @@ class SixelRenderer extends CliRendererBackend<String> {
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
}
|
||||
|
||||
void _drawCustomizeMenuHeader(
|
||||
WolfClassicMenuArt art,
|
||||
int headingIndex,
|
||||
int backgroundColor,
|
||||
) {
|
||||
_drawHeaderBarStack(
|
||||
headingY200: _headerHeadingY,
|
||||
backgroundColor: backgroundColor,
|
||||
barColor: 0,
|
||||
);
|
||||
|
||||
final VgaImage? heading = art.customizeLabel ?? art.optionsLabel;
|
||||
if (heading != null) {
|
||||
final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
|
||||
_blitVgaImage(heading, headingX, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
_drawMenuTextCentered('CUSTOMIZE', _headerHeadingY, headingIndex, scale: 2);
|
||||
}
|
||||
|
||||
void _drawMenuSectionHeader({
|
||||
required String text,
|
||||
required int y200,
|
||||
}) {
|
||||
final int textW = WolfMenuFont.measureTextWidth(text, 1);
|
||||
final int boxW = (textW + 28).clamp(110, 250);
|
||||
final int x = ((320 - boxW) ~/ 2).clamp(0, 319);
|
||||
final int outer = _rgbToPaletteIndex(0x727272);
|
||||
final int inner = _rgbToPaletteIndex(0x8E8E8E);
|
||||
final int fill = _rgbToPaletteIndex(0xC9C9C9);
|
||||
final int textColor = _rgbToPaletteIndex(0x5A5A5A);
|
||||
|
||||
_fillRect320(x, y200, boxW, 12, outer);
|
||||
_fillRect320(x + 1, y200 + 1, boxW - 2, 10, inner);
|
||||
_fillRect320(x + 2, y200 + 2, boxW - 4, 8, fill);
|
||||
_drawMenuTextCentered(text, y200 + 2, textColor, scale: 1);
|
||||
}
|
||||
|
||||
void _drawMenuFooterArt(WolfClassicMenuArt art) {
|
||||
final bottom = art.mappedPic(15);
|
||||
if (bottom == null) {
|
||||
|
||||
@@ -209,6 +209,28 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
unselectedTextColor,
|
||||
);
|
||||
break;
|
||||
case WolfMenuScreen.changeView:
|
||||
_drawChangeViewMenu(
|
||||
engine,
|
||||
art,
|
||||
panelColor,
|
||||
headingColor,
|
||||
selectedTextColor,
|
||||
unselectedTextColor,
|
||||
disabledTextColor,
|
||||
);
|
||||
break;
|
||||
case WolfMenuScreen.rendererOptions:
|
||||
_drawRendererOptionsMenu(
|
||||
engine,
|
||||
art,
|
||||
panelColor,
|
||||
headingColor,
|
||||
selectedTextColor,
|
||||
unselectedTextColor,
|
||||
disabledTextColor,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
||||
@@ -410,6 +432,216 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
}
|
||||
}
|
||||
|
||||
void _drawChangeViewMenu(
|
||||
WolfEngine engine,
|
||||
WolfClassicMenuArt art,
|
||||
int panelColor,
|
||||
int headingColor,
|
||||
int selectedTextColor,
|
||||
int unselectedTextColor,
|
||||
int disabledTextColor,
|
||||
) {
|
||||
_drawHeaderBarStack(
|
||||
headingY200: _headerHeadingY,
|
||||
backgroundColor: _rgbToFrameColor(engine.menuManager.menuBackgroundRgb),
|
||||
barColor: ColorPalette.vga32Bit[0],
|
||||
);
|
||||
|
||||
const int modesPanelX = 46;
|
||||
const int modesPanelY = 52;
|
||||
const int modesPanelW = 228;
|
||||
const int modesPanelH = 74;
|
||||
_fillCanonicalRect(
|
||||
modesPanelX,
|
||||
modesPanelY,
|
||||
modesPanelW,
|
||||
modesPanelH,
|
||||
panelColor,
|
||||
);
|
||||
|
||||
const int optionsPanelX = 46;
|
||||
const int optionsPanelY = 146;
|
||||
const int optionsPanelW = 228;
|
||||
const int optionsPanelH = 42;
|
||||
_fillCanonicalRect(
|
||||
optionsPanelX,
|
||||
optionsPanelY,
|
||||
optionsPanelW,
|
||||
optionsPanelH,
|
||||
panelColor,
|
||||
);
|
||||
|
||||
final VgaImage? heading = art.customizeLabel ?? art.optionsLabel;
|
||||
if (heading != null) {
|
||||
final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
|
||||
_blitVgaImage(heading, headingX, 0);
|
||||
} else {
|
||||
_drawCanonicalMenuTextCentered(
|
||||
'CUSTOMIZE',
|
||||
_headerHeadingY,
|
||||
headingColor,
|
||||
scale: 2,
|
||||
);
|
||||
}
|
||||
|
||||
final VgaImage? selectedMarker = art.selectedMarker;
|
||||
final VgaImage? unselectedMarker = art.unselectedMarker;
|
||||
final VgaImage? cursor = art.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
|
||||
const int rowYStart = 66;
|
||||
const int rowStep = 18;
|
||||
const int cursorX = 62;
|
||||
const int markerX = 92;
|
||||
const int textX = 122;
|
||||
|
||||
final entries = engine.menuManager.changeViewEntries;
|
||||
final optionEntries = engine.menuManager.rendererOptionEntries;
|
||||
final int modeCount = entries.length;
|
||||
final int selectedIndex = engine.menuManager.selectedChangeViewIndex;
|
||||
|
||||
for (int i = 0; i < entries.length; i++) {
|
||||
final bool isSelected = i == selectedIndex;
|
||||
final int y = rowYStart + (i * rowStep);
|
||||
if (isSelected && cursor != null) {
|
||||
_blitVgaImage(cursor, cursorX, y - 2);
|
||||
}
|
||||
|
||||
final entry = entries[i];
|
||||
final VgaImage? marker = entry.isChecked
|
||||
? selectedMarker
|
||||
: unselectedMarker;
|
||||
if (marker != null) {
|
||||
_blitVgaImage(marker, markerX, y);
|
||||
}
|
||||
|
||||
_drawCanonicalMenuText(
|
||||
entry.label,
|
||||
textX,
|
||||
y + 1,
|
||||
entry.isEnabled
|
||||
? (isSelected ? selectedTextColor : unselectedTextColor)
|
||||
: disabledTextColor,
|
||||
);
|
||||
}
|
||||
|
||||
_drawMenuSectionHeader(
|
||||
text: engine.menuManager.rendererOptionsTitle,
|
||||
y200: 132,
|
||||
textColor: ColorPalette.vga32Bit[8],
|
||||
);
|
||||
|
||||
const int optionsRowStart = 159;
|
||||
const int optionsRowStep = 15;
|
||||
for (int i = 0; i < optionEntries.length; i++) {
|
||||
final int optionIndex = modeCount + i;
|
||||
final bool isSelected = optionIndex == selectedIndex;
|
||||
final int y = optionsRowStart + (i * optionsRowStep);
|
||||
if (isSelected && cursor != null) {
|
||||
_blitVgaImage(cursor, cursorX, y - 2);
|
||||
}
|
||||
|
||||
final entry = optionEntries[i];
|
||||
final VgaImage? marker = entry.isChecked
|
||||
? selectedMarker
|
||||
: unselectedMarker;
|
||||
if (marker != null) {
|
||||
_blitVgaImage(marker, markerX, y);
|
||||
}
|
||||
_drawCanonicalMenuText(
|
||||
entry.label,
|
||||
textX,
|
||||
y + 1,
|
||||
entry.isEnabled
|
||||
? (isSelected ? selectedTextColor : unselectedTextColor)
|
||||
: disabledTextColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawRendererOptionsMenu(
|
||||
WolfEngine engine,
|
||||
WolfClassicMenuArt art,
|
||||
int panelColor,
|
||||
int headingColor,
|
||||
int selectedTextColor,
|
||||
int unselectedTextColor,
|
||||
int disabledTextColor,
|
||||
) {
|
||||
_drawHeaderBarStack(
|
||||
headingY200: _headerHeadingY,
|
||||
backgroundColor: _rgbToFrameColor(engine.menuManager.menuBackgroundRgb),
|
||||
barColor: ColorPalette.vga32Bit[0],
|
||||
);
|
||||
|
||||
const int panelX = 56;
|
||||
const int panelY = 52;
|
||||
const int panelW = 208;
|
||||
const int panelH = 120;
|
||||
_fillCanonicalRect(panelX, panelY, panelW, panelH, panelColor);
|
||||
|
||||
final VgaImage? heading = art.customizeLabel ?? art.optionsLabel;
|
||||
if (heading != null) {
|
||||
final int headingX = ((320 - heading.width) ~/ 2).clamp(0, 319);
|
||||
_blitVgaImage(heading, headingX, 0);
|
||||
} else {
|
||||
_drawCanonicalMenuTextCentered(
|
||||
'CUSTOMIZE',
|
||||
_headerHeadingY,
|
||||
headingColor,
|
||||
scale: 2,
|
||||
);
|
||||
}
|
||||
|
||||
final VgaImage? selectedMarker = art.selectedMarker;
|
||||
final VgaImage? unselectedMarker = art.unselectedMarker;
|
||||
final VgaImage? cursor = art.mappedPic(
|
||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||
);
|
||||
|
||||
_drawCanonicalMenuTextCentered(
|
||||
engine.menuManager.rendererOptionsTitle,
|
||||
46,
|
||||
headingColor,
|
||||
scale: 1,
|
||||
);
|
||||
|
||||
const int rowYStart = 68;
|
||||
const int rowStep = 20;
|
||||
const int cursorX = 62;
|
||||
const int markerX = 92;
|
||||
const int textX = 122;
|
||||
|
||||
final entries = engine.menuManager.rendererOptionEntries;
|
||||
final int selectedIndex = engine.menuManager.selectedRendererOptionIndex;
|
||||
|
||||
for (int i = 0; i < entries.length; i++) {
|
||||
final entry = entries[i];
|
||||
final bool isSelected = i == selectedIndex;
|
||||
final int y = rowYStart + (i * rowStep);
|
||||
if (isSelected && cursor != null) {
|
||||
_blitVgaImage(cursor, cursorX, y - 2);
|
||||
}
|
||||
|
||||
final VgaImage? marker = entry.isChecked
|
||||
? selectedMarker
|
||||
: unselectedMarker;
|
||||
if (marker != null) {
|
||||
_blitVgaImage(marker, markerX, y);
|
||||
}
|
||||
|
||||
_drawCanonicalMenuText(
|
||||
entry.label,
|
||||
textX,
|
||||
y + 1,
|
||||
entry.isEnabled
|
||||
? (isSelected ? selectedTextColor : unselectedTextColor)
|
||||
: disabledTextColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawGameSelectMenu(
|
||||
WolfEngine engine,
|
||||
WolfClassicMenuArt art,
|
||||
@@ -461,6 +693,26 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
||||
}
|
||||
}
|
||||
|
||||
void _drawMenuSectionHeader({
|
||||
required String text,
|
||||
required int y200,
|
||||
required int textColor,
|
||||
}) {
|
||||
final int textW = WolfMenuFont.measureTextWidth(text, 1);
|
||||
final int boxW = (textW + 28).clamp(110, 250);
|
||||
final int x = ((320 - boxW) ~/ 2).clamp(0, 319);
|
||||
final int y = y200.clamp(0, 199);
|
||||
final int outer = _rgbToFrameColor(0x727272);
|
||||
final int inner = _rgbToFrameColor(0x8E8E8E);
|
||||
final int fill = _rgbToFrameColor(0xC9C9C9);
|
||||
final int menuTextColor = _rgbToFrameColor(0x5A5A5A);
|
||||
|
||||
_fillCanonicalRect(x, y, boxW, 12, outer);
|
||||
_fillCanonicalRect(x + 1, y + 1, boxW - 2, 10, inner);
|
||||
_fillCanonicalRect(x + 2, y + 2, boxW - 4, 8, fill);
|
||||
_drawCanonicalMenuTextCentered(text, y + 2, menuTextColor, scale: 1);
|
||||
}
|
||||
|
||||
void _drawEpisodeSelectMenu(
|
||||
WolfEngine engine,
|
||||
WolfClassicMenuArt art,
|
||||
|
||||
@@ -11,4 +11,6 @@ export 'src/engine/input/engine_input.dart';
|
||||
export 'src/engine/managers/door_manager.dart';
|
||||
export 'src/engine/managers/pushwall_manager.dart';
|
||||
export 'src/engine/player/player.dart';
|
||||
export 'src/engine/rendering/renderer_settings.dart';
|
||||
export 'src/engine/rendering/renderer_settings_persistence.dart';
|
||||
export 'src/engine/wolf_3d_engine_base.dart';
|
||||
|
||||
@@ -21,6 +21,7 @@ abstract class WolfMenuPic {
|
||||
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
|
||||
@@ -115,6 +116,8 @@ class WolfClassicMenuArt {
|
||||
|
||||
VgaImage? get optionsLabel => _imageForKey(MenuPicKey.optionsLabel);
|
||||
|
||||
VgaImage? get customizeLabel => _imageForKey(MenuPicKey.customizeLabel);
|
||||
|
||||
VgaImage? get credits => _imageForKey(MenuPicKey.credits);
|
||||
|
||||
VgaImage? episodeOption(int episodeIndex) {
|
||||
@@ -189,6 +192,8 @@ class WolfClassicMenuArt {
|
||||
return MenuPicKey.difficultyHard;
|
||||
case WolfMenuPic.cControl:
|
||||
return MenuPicKey.controlBackground;
|
||||
case WolfMenuPic.cCustomize:
|
||||
return MenuPicKey.customizeLabel;
|
||||
case WolfMenuPic.cEpisode1:
|
||||
return MenuPicKey.episode1;
|
||||
case WolfMenuPic.cEpisode2:
|
||||
|
||||
@@ -54,6 +54,7 @@ void main() {
|
||||
final engine = _buildMultiGameEngine(input: input, difficulty: null);
|
||||
|
||||
engine.init();
|
||||
_dismissIntroSplash(engine, input);
|
||||
|
||||
expect(engine.isMenuOpen, isTrue);
|
||||
expect(engine.menuManager.activeMenu, WolfMenuScreen.gameSelect);
|
||||
@@ -62,6 +63,7 @@ void main() {
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isInteracting = false;
|
||||
engine.tick(const Duration(milliseconds: 300));
|
||||
_dismissIntroSplash(engine, input);
|
||||
|
||||
expect(engine.menuManager.activeMenu, WolfMenuScreen.mainMenu);
|
||||
expect(
|
||||
@@ -85,7 +87,7 @@ void main() {
|
||||
engine.menuManager.mainMenuEntries
|
||||
.map((entry) => entry.isEnabled)
|
||||
.toList(),
|
||||
[true, false, false, false, false, false, false, false, true, true],
|
||||
[true, false, false, false, false, true, false, false, true, true],
|
||||
);
|
||||
|
||||
input.isInteracting = true;
|
||||
@@ -102,6 +104,7 @@ void main() {
|
||||
final engine = _buildEngine(input: input, difficulty: null);
|
||||
|
||||
engine.init();
|
||||
_advanceToMainMenu(engine, input);
|
||||
|
||||
expect(engine.isMenuOpen, isTrue);
|
||||
expect(engine.menuManager.activeMenu, WolfMenuScreen.mainMenu);
|
||||
@@ -124,7 +127,7 @@ void main() {
|
||||
engine.menuManager.mainMenuEntries
|
||||
.map((entry) => entry.isEnabled)
|
||||
.toList(),
|
||||
[true, false, false, false, false, false, false, false, true, true],
|
||||
[true, false, false, false, false, true, false, false, true, true],
|
||||
);
|
||||
|
||||
input.isInteracting = true;
|
||||
@@ -172,7 +175,7 @@ void main() {
|
||||
engine.menuManager.mainMenuEntries
|
||||
.map((entry) => entry.isEnabled)
|
||||
.toList(),
|
||||
[true, false, false, false, false, false, false, true, true, true],
|
||||
[true, false, false, false, false, true, false, true, true, true],
|
||||
);
|
||||
|
||||
input.isMovingForward = true;
|
||||
@@ -203,6 +206,10 @@ void main() {
|
||||
|
||||
expect(manager.selectedMainIndex, 0);
|
||||
|
||||
manager.updateMainMenu(const EngineInput(isMovingBackward: true));
|
||||
manager.updateMainMenu(const EngineInput());
|
||||
expect(manager.selectedMainIndex, 5);
|
||||
|
||||
manager.updateMainMenu(const EngineInput(isMovingBackward: true));
|
||||
manager.updateMainMenu(const EngineInput());
|
||||
expect(manager.selectedMainIndex, 8);
|
||||
@@ -232,6 +239,12 @@ void main() {
|
||||
);
|
||||
|
||||
engine.init();
|
||||
_advanceToMainMenu(engine, input);
|
||||
|
||||
input.isMovingBackward = true;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isMovingBackward = false;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
|
||||
input.isMovingBackward = true;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
@@ -269,6 +282,7 @@ void main() {
|
||||
);
|
||||
|
||||
engine.init();
|
||||
_advanceToMainMenu(engine, input);
|
||||
|
||||
input.isBack = true;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
@@ -417,6 +431,28 @@ class _SilentAudio implements EngineAudio {
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
void _dismissIntroSplash(WolfEngine engine, _TestInput input) {
|
||||
int safety = 0;
|
||||
while (engine.menuManager.activeMenu == WolfMenuScreen.introSplash &&
|
||||
safety < 160) {
|
||||
input.isInteracting = safety.isEven;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
safety++;
|
||||
}
|
||||
input.isInteracting = false;
|
||||
}
|
||||
|
||||
void _advanceToMainMenu(WolfEngine engine, _TestInput input) {
|
||||
_dismissIntroSplash(engine, input);
|
||||
if (engine.menuManager.activeMenu == WolfMenuScreen.gameSelect) {
|
||||
input.isInteracting = true;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isInteracting = false;
|
||||
engine.tick(const Duration(milliseconds: 300));
|
||||
}
|
||||
expect(engine.menuManager.activeMenu, WolfMenuScreen.mainMenu);
|
||||
}
|
||||
|
||||
SpriteMap _buildGrid() => List.generate(64, (_) => List.filled(64, 0));
|
||||
|
||||
void _fillBoundaries(SpriteMap grid, int wallId) {
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:wolf_3d_dart/src/menu/menu_manager.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||
|
||||
void main() {
|
||||
group('Change View menu wiring', () {
|
||||
test('CHANGE VIEW is wired and enabled in main menu', () {
|
||||
final engine = _buildEngine(difficulty: null);
|
||||
engine.init();
|
||||
final entries = engine.menuManager.mainMenuEntries;
|
||||
final changeViewEntry = entries.firstWhere(
|
||||
(e) => e.action == WolfMenuMainAction.changeView,
|
||||
);
|
||||
expect(changeViewEntry.isEnabled, isTrue);
|
||||
});
|
||||
|
||||
test('selecting CHANGE VIEW transitions to changeView screen', () {
|
||||
final input = _TestInput();
|
||||
final engine = _buildEngine(difficulty: null, input: input);
|
||||
engine.init();
|
||||
|
||||
// Navigate to CHANGE VIEW (index 5 in the default menu)
|
||||
engine.menuManager.showMainMenu(hasResumableGame: false);
|
||||
final int cvIndex = engine.menuManager.mainMenuEntries.indexWhere(
|
||||
(e) => e.action == WolfMenuMainAction.changeView,
|
||||
);
|
||||
// Move cursor down to CHANGE VIEW
|
||||
for (int i = 0; i < cvIndex; i++) {
|
||||
input.isMovingBackward = true;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isMovingBackward = false;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
}
|
||||
expect(engine.menuManager.selectedMainIndex, cvIndex);
|
||||
|
||||
// Confirm
|
||||
input.isInteracting = true;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isInteracting = false;
|
||||
|
||||
expect(
|
||||
engine.menuManager.activeMenu,
|
||||
WolfMenuScreen.changeView,
|
||||
);
|
||||
});
|
||||
|
||||
test('back from changeView returns to main menu', () {
|
||||
final input = _TestInput();
|
||||
final engine = _buildEngine(difficulty: null, input: input);
|
||||
engine.init();
|
||||
|
||||
engine.menuManager.showChangeViewMenu();
|
||||
|
||||
input.isBack = true;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isBack = false;
|
||||
|
||||
expect(engine.menuManager.activeMenu, WolfMenuScreen.mainMenu);
|
||||
});
|
||||
|
||||
test('renderer selection toggles mode and stays on changeView', () {
|
||||
final input = _TestInput();
|
||||
final engine = _buildEngine(
|
||||
difficulty: null,
|
||||
input: input,
|
||||
rendererCapabilities: const WolfRendererCapabilities(
|
||||
supportedModes: {WolfRendererMode.software, WolfRendererMode.ascii},
|
||||
supportsAsciiThemes: true,
|
||||
supportsFpsCounter: false,
|
||||
),
|
||||
rendererSettings: const WolfRendererSettings(
|
||||
mode: WolfRendererMode.software,
|
||||
),
|
||||
);
|
||||
engine.init();
|
||||
engine.menuManager.showChangeViewMenu();
|
||||
|
||||
// Move from SOFTWARE to ASCII.
|
||||
input.isMovingBackward = true;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isMovingBackward = false;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
|
||||
input.isInteracting = true;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isInteracting = false;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
|
||||
expect(engine.rendererSettings.mode, WolfRendererMode.ascii);
|
||||
expect(engine.menuManager.activeMenu, WolfMenuScreen.changeView);
|
||||
});
|
||||
|
||||
test('renderer option toggles are available inline in changeView', () {
|
||||
final input = _TestInput();
|
||||
final engine = _buildEngine(
|
||||
difficulty: null,
|
||||
input: input,
|
||||
rendererCapabilities: const WolfRendererCapabilities(
|
||||
supportedModes: {WolfRendererMode.software, WolfRendererMode.ascii},
|
||||
supportsAsciiThemes: true,
|
||||
supportsFpsCounter: false,
|
||||
),
|
||||
rendererSettings: const WolfRendererSettings(
|
||||
mode: WolfRendererMode.software,
|
||||
),
|
||||
);
|
||||
engine.init();
|
||||
engine.menuManager.showChangeViewMenu();
|
||||
|
||||
// Select ASCII so mode-specific settings become available.
|
||||
input.isMovingBackward = true;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isMovingBackward = false;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isInteracting = true;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isInteracting = false;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
|
||||
// Move to first inline option row and toggle it.
|
||||
input.isMovingBackward = true;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isMovingBackward = false;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isInteracting = true;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isInteracting = false;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
|
||||
expect(engine.menuManager.activeMenu, WolfMenuScreen.changeView);
|
||||
expect(
|
||||
engine.rendererSettings.asciiThemeId,
|
||||
WolfRendererSettings.asciiThemeQuadrant,
|
||||
);
|
||||
});
|
||||
|
||||
test('toggling renderer option keeps cursor on selected option', () {
|
||||
final input = _TestInput();
|
||||
final engine = _buildEngine(
|
||||
difficulty: null,
|
||||
input: input,
|
||||
rendererCapabilities: const WolfRendererCapabilities(
|
||||
supportedModes: {WolfRendererMode.ascii},
|
||||
supportsAsciiThemes: true,
|
||||
supportsFpsCounter: true,
|
||||
),
|
||||
rendererSettings: const WolfRendererSettings(
|
||||
mode: WolfRendererMode.ascii,
|
||||
),
|
||||
);
|
||||
engine.init();
|
||||
engine.menuManager.showChangeViewMenu();
|
||||
|
||||
// Move from renderer row to first option row, then to second option row.
|
||||
input.isMovingBackward = true;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isMovingBackward = false;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
|
||||
input.isMovingBackward = true;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isMovingBackward = false;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
final int modeCount = engine.menuManager.changeViewEntries.length;
|
||||
expect(engine.menuManager.selectedChangeViewIndex, modeCount + 1);
|
||||
|
||||
input.isInteracting = true;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
input.isInteracting = false;
|
||||
engine.tick(const Duration(milliseconds: 16));
|
||||
|
||||
expect(engine.menuManager.selectedChangeViewIndex, modeCount + 1);
|
||||
expect(engine.rendererSettings.fpsCounterEnabled, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('Renderer settings model', () {
|
||||
test('default capabilities include software mode', () {
|
||||
final engine = _buildEngine(difficulty: null);
|
||||
engine.init();
|
||||
expect(
|
||||
engine.rendererCapabilities.supportsMode(WolfRendererMode.software),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('updateRendererSettings mutates mode and notifies callback', () {
|
||||
WolfRendererSettings? notified;
|
||||
final engine = _buildEngine(
|
||||
difficulty: null,
|
||||
onRendererSettingsChanged: (s) => notified = s,
|
||||
rendererCapabilities: const WolfRendererCapabilities(
|
||||
supportedModes: {WolfRendererMode.software, WolfRendererMode.ascii},
|
||||
supportsAsciiThemes: true,
|
||||
supportsFpsCounter: true,
|
||||
),
|
||||
);
|
||||
engine.init();
|
||||
|
||||
engine.updateRendererSettings(
|
||||
const WolfRendererSettings(mode: WolfRendererMode.ascii),
|
||||
);
|
||||
|
||||
expect(engine.rendererSettings.mode, WolfRendererMode.ascii);
|
||||
expect(notified?.mode, WolfRendererMode.ascii);
|
||||
});
|
||||
|
||||
test('cycleRendererMode cycles through supported modes', () {
|
||||
final engine = _buildEngine(
|
||||
difficulty: null,
|
||||
rendererCapabilities: const WolfRendererCapabilities(
|
||||
supportedModes: {WolfRendererMode.software, WolfRendererMode.ascii},
|
||||
supportsAsciiThemes: true,
|
||||
supportsFpsCounter: true,
|
||||
),
|
||||
rendererSettings: const WolfRendererSettings(
|
||||
mode: WolfRendererMode.software,
|
||||
),
|
||||
);
|
||||
engine.init();
|
||||
|
||||
engine.cycleRendererMode();
|
||||
expect(engine.rendererSettings.mode, WolfRendererMode.ascii);
|
||||
|
||||
engine.cycleRendererMode();
|
||||
expect(engine.rendererSettings.mode, WolfRendererMode.software);
|
||||
});
|
||||
|
||||
test('cycleAsciiTheme cycles to next theme', () {
|
||||
final engine = _buildEngine(
|
||||
difficulty: null,
|
||||
rendererCapabilities: const WolfRendererCapabilities(
|
||||
supportedModes: {WolfRendererMode.ascii},
|
||||
supportsAsciiThemes: true,
|
||||
supportsFpsCounter: false,
|
||||
),
|
||||
rendererSettings: const WolfRendererSettings(
|
||||
asciiThemeId: WolfRendererSettings.asciiThemeBlocks,
|
||||
),
|
||||
);
|
||||
engine.init();
|
||||
|
||||
engine.cycleAsciiTheme();
|
||||
expect(
|
||||
engine.rendererSettings.asciiThemeId,
|
||||
WolfRendererSettings.asciiThemeQuadrant,
|
||||
);
|
||||
|
||||
engine.cycleAsciiTheme();
|
||||
expect(
|
||||
engine.rendererSettings.asciiThemeId,
|
||||
WolfRendererSettings.asciiThemeBlocks,
|
||||
);
|
||||
});
|
||||
|
||||
test('toggleFpsCounter toggles and syncs showFpsCounter', () {
|
||||
final engine = _buildEngine(
|
||||
difficulty: null,
|
||||
rendererCapabilities: const WolfRendererCapabilities(
|
||||
supportedModes: {WolfRendererMode.software},
|
||||
supportsFpsCounter: true,
|
||||
),
|
||||
);
|
||||
engine.init();
|
||||
expect(engine.showFpsCounter, isFalse);
|
||||
|
||||
engine.toggleFpsCounter();
|
||||
expect(engine.rendererSettings.fpsCounterEnabled, isTrue);
|
||||
expect(engine.showFpsCounter, isTrue);
|
||||
|
||||
engine.toggleFpsCounter();
|
||||
expect(engine.rendererSettings.fpsCounterEnabled, isFalse);
|
||||
expect(engine.showFpsCounter, isFalse);
|
||||
});
|
||||
|
||||
test('toggleHardwareEffects no-ops when capability is absent', () {
|
||||
final engine = _buildEngine(
|
||||
difficulty: null,
|
||||
rendererCapabilities: const WolfRendererCapabilities(
|
||||
supportedModes: {WolfRendererMode.software},
|
||||
supportsHardwareEffects: false,
|
||||
),
|
||||
rendererSettings: const WolfRendererSettings(
|
||||
hardwareEffectsEnabled: false,
|
||||
),
|
||||
);
|
||||
engine.init();
|
||||
|
||||
engine.toggleHardwareEffects();
|
||||
expect(engine.rendererSettings.hardwareEffectsEnabled, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('Renderer menu visibility', () {
|
||||
test('changeViewEntries only contain supported modes', () {
|
||||
final engine = _buildEngine(
|
||||
difficulty: null,
|
||||
rendererCapabilities: const WolfRendererCapabilities(
|
||||
supportedModes: {WolfRendererMode.ascii, WolfRendererMode.software},
|
||||
supportsAsciiThemes: true,
|
||||
supportsFpsCounter: false,
|
||||
),
|
||||
);
|
||||
engine.init();
|
||||
|
||||
final modes = engine.menuManager.changeViewEntries
|
||||
.map((e) => e.mode)
|
||||
.toSet();
|
||||
expect(
|
||||
modes,
|
||||
equals({WolfRendererMode.ascii, WolfRendererMode.software}),
|
||||
);
|
||||
});
|
||||
|
||||
test('checked renderer entry matches active mode', () {
|
||||
final engine = _buildEngine(
|
||||
difficulty: null,
|
||||
rendererCapabilities: const WolfRendererCapabilities(
|
||||
supportedModes: {WolfRendererMode.ascii, WolfRendererMode.software},
|
||||
),
|
||||
rendererSettings: const WolfRendererSettings(
|
||||
mode: WolfRendererMode.ascii,
|
||||
),
|
||||
);
|
||||
engine.init();
|
||||
|
||||
final checked = engine.menuManager.changeViewEntries
|
||||
.where((e) => e.isChecked)
|
||||
.toList();
|
||||
expect(checked.length, 1);
|
||||
expect(checked.first.mode, WolfRendererMode.ascii);
|
||||
});
|
||||
|
||||
test('ASCII theme option is shown when mode is ascii', () {
|
||||
final engine = _buildEngine(
|
||||
difficulty: null,
|
||||
rendererCapabilities: const WolfRendererCapabilities(
|
||||
supportedModes: {WolfRendererMode.ascii},
|
||||
supportsAsciiThemes: true,
|
||||
supportsFpsCounter: false,
|
||||
),
|
||||
rendererSettings: const WolfRendererSettings(
|
||||
mode: WolfRendererMode.ascii,
|
||||
),
|
||||
);
|
||||
engine.init();
|
||||
|
||||
final optionIds = engine.menuManager.rendererOptionEntries
|
||||
.map((e) => e.id)
|
||||
.toSet();
|
||||
expect(optionIds.contains(WolfRendererOptionId.asciiTheme), isTrue);
|
||||
});
|
||||
|
||||
test('ASCII theme option is hidden when mode is not ascii', () {
|
||||
final engine = _buildEngine(
|
||||
difficulty: null,
|
||||
rendererCapabilities: const WolfRendererCapabilities(
|
||||
supportedModes: {WolfRendererMode.software},
|
||||
supportsAsciiThemes: false,
|
||||
supportsFpsCounter: false,
|
||||
),
|
||||
rendererSettings: const WolfRendererSettings(
|
||||
mode: WolfRendererMode.software,
|
||||
),
|
||||
);
|
||||
engine.init();
|
||||
|
||||
final optionIds = engine.menuManager.rendererOptionEntries
|
||||
.map((e) => e.id)
|
||||
.toSet();
|
||||
expect(optionIds.contains(WolfRendererOptionId.asciiTheme), isFalse);
|
||||
});
|
||||
|
||||
test('hardware effects option only shown when hardware mode is active', () {
|
||||
final engineHardware = _buildEngine(
|
||||
difficulty: null,
|
||||
rendererCapabilities: const WolfRendererCapabilities(
|
||||
supportedModes: {WolfRendererMode.hardware},
|
||||
supportsHardwareEffects: true,
|
||||
supportsFpsCounter: false,
|
||||
),
|
||||
rendererSettings: const WolfRendererSettings(
|
||||
mode: WolfRendererMode.hardware,
|
||||
),
|
||||
);
|
||||
engineHardware.init();
|
||||
final hardwareOptIds = engineHardware.menuManager.rendererOptionEntries
|
||||
.map((e) => e.id)
|
||||
.toSet();
|
||||
expect(
|
||||
hardwareOptIds.contains(WolfRendererOptionId.hardwareEffects),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
final engineSoftware = _buildEngine(
|
||||
difficulty: null,
|
||||
rendererCapabilities: const WolfRendererCapabilities(
|
||||
supportedModes: {WolfRendererMode.software},
|
||||
supportsHardwareEffects: false,
|
||||
supportsFpsCounter: false,
|
||||
),
|
||||
rendererSettings: const WolfRendererSettings(
|
||||
mode: WolfRendererMode.software,
|
||||
),
|
||||
);
|
||||
engineSoftware.init();
|
||||
final softwareOptIds = engineSoftware.menuManager.rendererOptionEntries
|
||||
.map((e) => e.id)
|
||||
.toSet();
|
||||
expect(
|
||||
softwareOptIds.contains(WolfRendererOptionId.hardwareEffects),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('WolfRendererSettings serialization', () {
|
||||
test('round-trip through JSON is lossless', () {
|
||||
const WolfRendererSettings original = WolfRendererSettings(
|
||||
mode: WolfRendererMode.ascii,
|
||||
asciiThemeId: WolfRendererSettings.asciiThemeQuadrant,
|
||||
hardwareEffectsEnabled: true,
|
||||
fpsCounterEnabled: true,
|
||||
);
|
||||
final json = original.toJson();
|
||||
final restored = WolfRendererSettings.fromJson(json);
|
||||
|
||||
expect(restored.mode, original.mode);
|
||||
expect(restored.asciiThemeId, original.asciiThemeId);
|
||||
expect(restored.hardwareEffectsEnabled, original.hardwareEffectsEnabled);
|
||||
expect(restored.fpsCounterEnabled, original.fpsCounterEnabled);
|
||||
});
|
||||
|
||||
test('fromJson tolerates unknown mode with safe default', () {
|
||||
final settings = WolfRendererSettings.fromJson(<String, Object?>{
|
||||
'mode': 'unknown_mode_xyz',
|
||||
});
|
||||
expect(settings.mode, WolfRendererMode.software);
|
||||
});
|
||||
|
||||
test('fromJson tolerates missing fields', () {
|
||||
final settings = WolfRendererSettings.fromJson(<String, Object?>{});
|
||||
expect(settings.mode, WolfRendererMode.software);
|
||||
expect(settings.asciiThemeId, WolfRendererSettings.asciiThemeBlocks);
|
||||
expect(settings.hardwareEffectsEnabled, isFalse);
|
||||
expect(settings.fpsCounterEnabled, isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
WolfEngine _buildEngine({
|
||||
required Difficulty? difficulty,
|
||||
_TestInput? input,
|
||||
WolfRendererCapabilities? rendererCapabilities,
|
||||
WolfRendererSettings? rendererSettings,
|
||||
void Function(WolfRendererSettings)? onRendererSettingsChanged,
|
||||
}) {
|
||||
return WolfEngine(
|
||||
data: _buildTestData(),
|
||||
difficulty: difficulty,
|
||||
startingEpisode: 0,
|
||||
frameBuffer: FrameBuffer(64, 64),
|
||||
input: input ?? _TestInput(),
|
||||
engineAudio: _SilentAudio(),
|
||||
onGameWon: () {},
|
||||
rendererCapabilities: rendererCapabilities,
|
||||
rendererSettings: rendererSettings,
|
||||
onRendererSettingsChanged: onRendererSettingsChanged,
|
||||
);
|
||||
}
|
||||
|
||||
WolfensteinData _buildTestData() {
|
||||
final wallGrid = List.generate(64, (_) => List.filled(64, 0));
|
||||
final objGrid = List.generate(64, (_) => List.filled(64, 0));
|
||||
_fillBoundaries(wallGrid, 2);
|
||||
objGrid[2][2] = MapObject.playerEast;
|
||||
|
||||
return WolfensteinData(
|
||||
version: GameVersion.retail,
|
||||
dataVersion: DataVersion.unknown,
|
||||
registry: RetailAssetRegistry(),
|
||||
walls: [_sprite(1), _sprite(1), _sprite(2), _sprite(2)],
|
||||
sprites: List.generate(436, (_) => _sprite(255)),
|
||||
sounds: List.generate(200, (_) => PcmSound(Uint8List(1))),
|
||||
adLibSounds: const [],
|
||||
music: const [],
|
||||
vgaImages: const [],
|
||||
episodes: [
|
||||
Episode(
|
||||
name: 'Test Episode',
|
||||
levels: [
|
||||
WolfLevel(
|
||||
name: 'Test Level',
|
||||
wallGrid: wallGrid,
|
||||
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||
objectGrid: objGrid,
|
||||
music: Music.level01,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _fillBoundaries(List<List<int>> grid, int wallId) {
|
||||
for (int i = 0; i < 64; i++) {
|
||||
grid[0][i] = wallId;
|
||||
grid[63][i] = wallId;
|
||||
grid[i][0] = wallId;
|
||||
grid[i][63] = wallId;
|
||||
}
|
||||
}
|
||||
|
||||
Sprite _sprite(int c) => Sprite(Uint8List.fromList(List.filled(64 * 64, c)));
|
||||
|
||||
class _TestInput extends Wolf3dInput {
|
||||
@override
|
||||
void update() {}
|
||||
}
|
||||
|
||||
class _SilentAudio implements EngineAudio {
|
||||
@override
|
||||
WolfensteinData? activeGame;
|
||||
@override
|
||||
Future<void> debugSoundTest() async {}
|
||||
@override
|
||||
Future<void> init() async {}
|
||||
@override
|
||||
void playLevelMusic(Music music) {}
|
||||
@override
|
||||
void playMenuMusic() {}
|
||||
@override
|
||||
void playSoundEffect(SoundEffect effect) {}
|
||||
@override
|
||||
void playSoundEffectId(int sfxId) {}
|
||||
@override
|
||||
void stopMusic() {}
|
||||
@override
|
||||
Future<void> stopAllAudio() async {}
|
||||
@override
|
||||
void dispose() {}
|
||||
}
|
||||
Reference in New Issue
Block a user