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:
@@ -7,6 +7,7 @@ library;
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:wolf_3d_cli/cli_game_loop.dart';
|
import 'package:wolf_3d_cli/cli_game_loop.dart';
|
||||||
|
import 'package:wolf_3d_cli/cli_renderer_settings_persistence.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_data.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_data_types.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
@@ -66,9 +67,14 @@ void main() async {
|
|||||||
|
|
||||||
engine.init();
|
engine.init();
|
||||||
|
|
||||||
|
final persistence = CliRendererSettingsPersistence();
|
||||||
|
final WolfRendererSettings? saved = await persistence.load();
|
||||||
|
|
||||||
gameLoop = CliGameLoop(
|
gameLoop = CliGameLoop(
|
||||||
engine: engine,
|
engine: engine,
|
||||||
onExit: stopAndExit,
|
onExit: stopAndExit,
|
||||||
|
persistence: persistence,
|
||||||
|
initialSettings: saved,
|
||||||
);
|
);
|
||||||
|
|
||||||
await gameLoop.start();
|
await gameLoop.start();
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ class CliGameLoop {
|
|||||||
CliGameLoop({
|
CliGameLoop({
|
||||||
required this.engine,
|
required this.engine,
|
||||||
required this.onExit,
|
required this.onExit,
|
||||||
|
this.persistence,
|
||||||
|
this.initialSettings,
|
||||||
}) : input = engine.input is CliInput
|
}) : input = engine.input is CliInput
|
||||||
? engine.input as CliInput
|
? engine.input as CliInput
|
||||||
: throw ArgumentError.value(
|
: throw ArgumentError.value(
|
||||||
@@ -37,6 +39,8 @@ class CliGameLoop {
|
|||||||
final CliRendererBackend secondaryRenderer;
|
final CliRendererBackend secondaryRenderer;
|
||||||
final CliInput input;
|
final CliInput input;
|
||||||
final void Function(int code) onExit;
|
final void Function(int code) onExit;
|
||||||
|
final RendererSettingsPersistence? persistence;
|
||||||
|
final WolfRendererSettings? initialSettings;
|
||||||
|
|
||||||
final Stopwatch _stopwatch = Stopwatch();
|
final Stopwatch _stopwatch = Stopwatch();
|
||||||
final Stream<List<int>> _stdinStream = stdin.asBroadcastStream();
|
final Stream<List<int>> _stdinStream = stdin.asBroadcastStream();
|
||||||
@@ -59,12 +63,32 @@ class CliGameLoop {
|
|||||||
inputStream: _stdinStream,
|
inputStream: _stdinStream,
|
||||||
);
|
);
|
||||||
_isSixelAvailable = sixel.isSixelSupported;
|
_isSixelAvailable = sixel.isSixelSupported;
|
||||||
_renderer = _isSixelAvailable ? primaryRenderer : secondaryRenderer;
|
|
||||||
} else {
|
} else {
|
||||||
_isSixelAvailable = false;
|
_isSixelAvailable = false;
|
||||||
_renderer = secondaryRenderer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Set<WolfRendererMode> supportedModes = <WolfRendererMode>{
|
||||||
|
WolfRendererMode.ascii,
|
||||||
|
if (_isSixelAvailable) WolfRendererMode.sixel,
|
||||||
|
};
|
||||||
|
engine.setRendererCapabilities(
|
||||||
|
WolfRendererCapabilities(
|
||||||
|
supportedModes: supportedModes,
|
||||||
|
supportsAsciiThemes: true,
|
||||||
|
supportsFpsCounter: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (initialSettings != null) {
|
||||||
|
engine.updateRendererSettings(initialSettings!);
|
||||||
|
} else if (_isSixelAvailable) {
|
||||||
|
engine.updateRendererSettings(
|
||||||
|
engine.rendererSettings.copyWith(mode: WolfRendererMode.sixel),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_syncRendererFromEngine();
|
||||||
|
|
||||||
if (stdin.hasTerminal) {
|
if (stdin.hasTerminal) {
|
||||||
try {
|
try {
|
||||||
stdin.echoMode = false;
|
stdin.echoMode = false;
|
||||||
@@ -124,43 +148,48 @@ class CliGameLoop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (input.matchesRendererToggleShortcut(bytes)) {
|
if (input.matchesRendererToggleShortcut(bytes)) {
|
||||||
if (!_isSixelAvailable) {
|
engine.cycleRendererMode();
|
||||||
return;
|
_syncRendererFromEngine();
|
||||||
}
|
unawaited(persistence?.save(engine.rendererSettings));
|
||||||
// Allow dynamic renderer-switch bindings configured on the CLI input.
|
|
||||||
_renderer = identical(_renderer, secondaryRenderer)
|
|
||||||
? primaryRenderer
|
|
||||||
: secondaryRenderer;
|
|
||||||
stdout.write('\x1b[2J\x1b[H');
|
stdout.write('\x1b[2J\x1b[H');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.matchesAsciiThemeCycleShortcut(bytes)) {
|
if (input.matchesAsciiThemeCycleShortcut(bytes)) {
|
||||||
_cycleAsciiTheme();
|
engine.cycleAsciiTheme();
|
||||||
|
_syncRendererFromEngine();
|
||||||
|
unawaited(persistence?.save(engine.rendererSettings));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.handleKey(bytes);
|
input.handleKey(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _cycleAsciiTheme() {
|
void _syncRendererFromEngine() {
|
||||||
final List<AsciiRenderer> asciiRenderers = <AsciiRenderer>[
|
final CliRendererBackend previousRenderer = _renderer;
|
||||||
if (primaryRenderer is AsciiRenderer) primaryRenderer as AsciiRenderer,
|
final WolfRendererMode mode = engine.rendererSettings.mode;
|
||||||
if (secondaryRenderer is AsciiRenderer)
|
if (mode == WolfRendererMode.sixel && _isSixelAvailable) {
|
||||||
secondaryRenderer as AsciiRenderer,
|
_renderer = primaryRenderer;
|
||||||
];
|
} else {
|
||||||
if (asciiRenderers.isEmpty) {
|
_renderer = secondaryRenderer;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final AsciiTheme nextTheme = AsciiThemes.nextOf(
|
final AsciiTheme theme =
|
||||||
asciiRenderers.first.activeTheme,
|
engine.rendererSettings.asciiThemeId ==
|
||||||
);
|
WolfRendererSettings.asciiThemeQuadrant
|
||||||
for (final renderer in asciiRenderers) {
|
? AsciiThemes.quadrant
|
||||||
renderer.activeTheme = nextTheme;
|
: AsciiThemes.blocks;
|
||||||
|
if (primaryRenderer is AsciiRenderer) {
|
||||||
|
(primaryRenderer as AsciiRenderer).activeTheme = theme;
|
||||||
|
}
|
||||||
|
if (secondaryRenderer is AsciiRenderer) {
|
||||||
|
(secondaryRenderer as AsciiRenderer).activeTheme = theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stdout.hasTerminal) {
|
engine.showFpsCounter = engine.rendererSettings.fpsCounterEnabled;
|
||||||
|
|
||||||
|
if (!identical(previousRenderer, _renderer) && stdout.hasTerminal) {
|
||||||
|
// Clear stale frame content when switching backend output modes.
|
||||||
stdout.write('\x1b[2J\x1b[H');
|
stdout.write('\x1b[2J\x1b[H');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,6 +199,9 @@ class CliGameLoop {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply renderer changes made via in-game menus or settings updates.
|
||||||
|
_syncRendererFromEngine();
|
||||||
|
|
||||||
if (stdout.hasTerminal) {
|
if (stdout.hasTerminal) {
|
||||||
final int cols = stdout.terminalColumns;
|
final int cols = stdout.terminalColumns;
|
||||||
final int rows = stdout.terminalLines;
|
final int rows = stdout.terminalLines;
|
||||||
@@ -198,51 +230,5 @@ class CliGameLoop {
|
|||||||
|
|
||||||
engine.tick(elapsed);
|
engine.tick(elapsed);
|
||||||
stdout.write(_renderer.render(engine));
|
stdout.write(_renderer.render(engine));
|
||||||
_writeShortcutHintLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _writeShortcutHintLine() {
|
|
||||||
if (!stdout.hasTerminal) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int cols = stdout.terminalColumns;
|
|
||||||
if (cols <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int safeCols = cols > 1 ? cols - 1 : cols;
|
|
||||||
final String hint = _buildShortcutHintText();
|
|
||||||
if (hint.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final String visible = hint.length > safeCols
|
|
||||||
? hint.substring(0, safeCols)
|
|
||||||
: hint;
|
|
||||||
final String padded = visible.padRight(safeCols);
|
|
||||||
|
|
||||||
// Draw an overlay line without disturbing the renderer's cursor position.
|
|
||||||
stdout.write('\x1b[s\x1b[1;1H\x1b[0m\x1b[2m\x1b[2K$padded\x1b[0m\x1b[u');
|
|
||||||
}
|
|
||||||
|
|
||||||
String _buildShortcutHintText() {
|
|
||||||
if (!_isSixelAvailable) {
|
|
||||||
if (_renderer is AsciiRenderer) {
|
|
||||||
final AsciiRenderer ascii = _renderer as AsciiRenderer;
|
|
||||||
return '<${input.asciiThemeCycleKeyLabel}> ${ascii.activeTheme.name}';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
final String rendererMode = _renderer is SixelRenderer ? 'sixel' : 'ascii';
|
|
||||||
final String rendererHint =
|
|
||||||
'<${input.rendererToggleKeyLabel}> $rendererMode';
|
|
||||||
|
|
||||||
if (_renderer is AsciiRenderer) {
|
|
||||||
final AsciiRenderer ascii = _renderer as AsciiRenderer;
|
|
||||||
return '$rendererHint <${input.asciiThemeCycleKeyLabel}> ${ascii.activeTheme.name}';
|
|
||||||
}
|
|
||||||
|
|
||||||
return rendererHint;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
apps/wolf_3d_cli/lib/cli_renderer_settings_persistence.dart
Normal file
42
apps/wolf_3d_cli/lib/cli_renderer_settings_persistence.dart
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/// CLI host adapter for persisting renderer settings to a local file.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
|
||||||
|
/// Persists [WolfRendererSettings] as JSON to a local file.
|
||||||
|
///
|
||||||
|
/// The default path is `~/.wolf3d_cli_settings.json`.
|
||||||
|
/// An alternative [filePath] can be supplied at construction time.
|
||||||
|
class CliRendererSettingsPersistence extends RendererSettingsPersistence
|
||||||
|
with JsonRendererSettingsPersistence {
|
||||||
|
CliRendererSettingsPersistence({String? filePath})
|
||||||
|
: _filePath =
|
||||||
|
filePath ??
|
||||||
|
'${Platform.environment['HOME'] ?? '.'}/.wolf3d_cli_settings.json';
|
||||||
|
|
||||||
|
final String _filePath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> readRaw() async {
|
||||||
|
try {
|
||||||
|
final File f = File(_filePath);
|
||||||
|
if (!f.existsSync()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await f.readAsString();
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> writeRaw(String json) async {
|
||||||
|
try {
|
||||||
|
await File(_filePath).writeAsString(json, flush: true);
|
||||||
|
} catch (_) {
|
||||||
|
// Best-effort; never crash the loop on a write failure.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import 'package:window_manager/window_manager.dart';
|
|||||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||||
import 'package:wolf_3d_dart/wolf_3d_renderer.dart';
|
import 'package:wolf_3d_dart/wolf_3d_renderer.dart';
|
||||||
|
import 'package:wolf_3d_flutter/renderer_settings_persistence_flutter.dart';
|
||||||
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
|
||||||
import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart';
|
import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart';
|
||||||
import 'package:wolf_3d_gui/screens/debug_tools_screen.dart';
|
import 'package:wolf_3d_gui/screens/debug_tools_screen.dart';
|
||||||
@@ -139,20 +140,38 @@ class GameScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _GameScreenState extends State<GameScreen> {
|
class _GameScreenState extends State<GameScreen> {
|
||||||
late final WolfEngine _engine;
|
late final WolfEngine _engine;
|
||||||
|
final FlutterRendererSettingsPersistence _persistence =
|
||||||
|
FlutterRendererSettingsPersistence();
|
||||||
|
|
||||||
/// Current active renderer implementation.
|
/// Mirrors [WolfRendererSettings.mode] into the Flutter renderer enum.
|
||||||
RendererMode _rendererMode = RendererMode.hardware;
|
RendererMode _rendererMode = RendererMode.hardware;
|
||||||
|
|
||||||
/// Active ASCII glyph theme used by [RendererMode.ascii].
|
|
||||||
AsciiTheme _asciiTheme = AsciiThemes.blocks;
|
|
||||||
|
|
||||||
/// Whether CRT post-processing is enabled in [RendererMode.hardware].
|
|
||||||
bool _glslEffectsEnabled = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
const Set<WolfRendererMode> supportedModes = <WolfRendererMode>{
|
||||||
|
WolfRendererMode.hardware,
|
||||||
|
WolfRendererMode.software,
|
||||||
|
WolfRendererMode.ascii,
|
||||||
|
};
|
||||||
_engine = widget.wolf3d.launchEngine(
|
_engine = widget.wolf3d.launchEngine(
|
||||||
|
rendererCapabilities: const WolfRendererCapabilities(
|
||||||
|
supportedModes: supportedModes,
|
||||||
|
supportsAsciiThemes: true,
|
||||||
|
supportsHardwareEffects: true,
|
||||||
|
supportsFpsCounter: true,
|
||||||
|
),
|
||||||
|
rendererSettings: const WolfRendererSettings(
|
||||||
|
mode: WolfRendererMode.hardware,
|
||||||
|
),
|
||||||
|
onRendererSettingsChanged: (settings) {
|
||||||
|
unawaited(_persistence.save(settings));
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_syncRendererModeFrom(settings);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
onGameWon: () {
|
onGameWon: () {
|
||||||
_engine.difficulty = null;
|
_engine.difficulty = null;
|
||||||
widget.wolf3d.clearActiveDifficulty();
|
widget.wolf3d.clearActiveDifficulty();
|
||||||
@@ -162,6 +181,30 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
SystemNavigator.pop();
|
SystemNavigator.pop();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
_syncRendererModeFrom(_engine.rendererSettings);
|
||||||
|
_loadPersistedSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadPersistedSettings() async {
|
||||||
|
final WolfRendererSettings? saved = await _persistence.load();
|
||||||
|
if (saved != null && mounted) {
|
||||||
|
_engine.updateRendererSettings(saved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _syncRendererModeFrom(WolfRendererSettings settings) {
|
||||||
|
switch (settings.mode) {
|
||||||
|
case WolfRendererMode.hardware:
|
||||||
|
_rendererMode = RendererMode.hardware;
|
||||||
|
break;
|
||||||
|
case WolfRendererMode.software:
|
||||||
|
_rendererMode = RendererMode.software;
|
||||||
|
break;
|
||||||
|
case WolfRendererMode.ascii:
|
||||||
|
case WolfRendererMode.sixel:
|
||||||
|
_rendererMode = RendererMode.ascii;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -222,17 +265,6 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
child: CircularProgressIndicator(color: Colors.teal),
|
child: CircularProgressIndicator(color: Colors.teal),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Positioned(
|
|
||||||
top: 16,
|
|
||||||
right: 16,
|
|
||||||
child: Text(
|
|
||||||
'<${widget.wolf3d.input.rendererToggleKeyLabel}> ${_rendererMode.name}${_activeModeOverlayHint()} <${widget.wolf3d.input.fpsToggleKeyLabel}> FPS ${_engine.showFpsCounter ? 'On' : 'Off'}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white.withValues(alpha: 0.5),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -245,6 +277,7 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
Widget _buildRenderer() {
|
Widget _buildRenderer() {
|
||||||
// Keep all renderers behind the same engine so mode switching does not
|
// Keep all renderers behind the same engine so mode switching does not
|
||||||
// reset level state or audio playback.
|
// reset level state or audio playback.
|
||||||
|
final WolfRendererSettings settings = _engine.rendererSettings;
|
||||||
switch (_rendererMode) {
|
switch (_rendererMode) {
|
||||||
case RendererMode.software:
|
case RendererMode.software:
|
||||||
return WolfFlutterRenderer(
|
return WolfFlutterRenderer(
|
||||||
@@ -252,15 +285,19 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
onKeyEvent: _handleRendererKeyEvent,
|
onKeyEvent: _handleRendererKeyEvent,
|
||||||
);
|
);
|
||||||
case RendererMode.ascii:
|
case RendererMode.ascii:
|
||||||
|
final AsciiTheme theme =
|
||||||
|
settings.asciiThemeId == WolfRendererSettings.asciiThemeQuadrant
|
||||||
|
? AsciiThemes.quadrant
|
||||||
|
: AsciiThemes.blocks;
|
||||||
return WolfAsciiRenderer(
|
return WolfAsciiRenderer(
|
||||||
engine: _engine,
|
engine: _engine,
|
||||||
theme: _asciiTheme,
|
theme: theme,
|
||||||
onKeyEvent: _handleRendererKeyEvent,
|
onKeyEvent: _handleRendererKeyEvent,
|
||||||
);
|
);
|
||||||
case RendererMode.hardware:
|
case RendererMode.hardware:
|
||||||
return WolfGlslRenderer(
|
return WolfGlslRenderer(
|
||||||
engine: _engine,
|
engine: _engine,
|
||||||
effectsEnabled: _glslEffectsEnabled,
|
effectsEnabled: settings.hardwareEffectsEnabled,
|
||||||
onKeyEvent: _handleRendererKeyEvent,
|
onKeyEvent: _handleRendererKeyEvent,
|
||||||
onUnavailable: _onGlslUnavailable,
|
onUnavailable: _onGlslUnavailable,
|
||||||
);
|
);
|
||||||
@@ -279,67 +316,31 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.logicalKey == widget.wolf3d.input.rendererToggleKey) {
|
if (event.logicalKey == widget.wolf3d.input.rendererToggleKey) {
|
||||||
setState(_cycleRendererMode);
|
_engine.cycleRendererMode();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.logicalKey == widget.wolf3d.input.fpsToggleKey) {
|
if (event.logicalKey == widget.wolf3d.input.fpsToggleKey) {
|
||||||
setState(_toggleFpsCounter);
|
setState(() => _engine.toggleFpsCounter());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.logicalKey == widget.wolf3d.input.asciiThemeCycleKey) {
|
if (event.logicalKey == widget.wolf3d.input.asciiThemeCycleKey) {
|
||||||
if (_rendererMode == RendererMode.ascii) {
|
if (_rendererMode == RendererMode.ascii) {
|
||||||
setState(_cycleAsciiTheme);
|
_engine.cycleAsciiTheme();
|
||||||
} else if (_rendererMode == RendererMode.hardware) {
|
} else if (_rendererMode == RendererMode.hardware) {
|
||||||
setState(_toggleGlslEffects);
|
_engine.toggleHardwareEffects();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _activeModeOverlayHint() {
|
|
||||||
if (_rendererMode == RendererMode.ascii) {
|
|
||||||
return ' <${widget.wolf3d.input.asciiThemeCycleKeyLabel}> ${_asciiTheme.name}';
|
|
||||||
}
|
|
||||||
if (_rendererMode == RendererMode.hardware) {
|
|
||||||
return ' <${widget.wolf3d.input.asciiThemeCycleKeyLabel}> Effects ${_glslEffectsEnabled ? 'on' : 'off'}';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
void _cycleRendererMode() {
|
|
||||||
switch (_rendererMode) {
|
|
||||||
case RendererMode.hardware:
|
|
||||||
_rendererMode = RendererMode.software;
|
|
||||||
break;
|
|
||||||
case RendererMode.software:
|
|
||||||
_rendererMode = RendererMode.ascii;
|
|
||||||
break;
|
|
||||||
case RendererMode.ascii:
|
|
||||||
_rendererMode = RendererMode.hardware;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onGlslUnavailable() {
|
void _onGlslUnavailable() {
|
||||||
if (!mounted || _rendererMode != RendererMode.hardware) {
|
if (!mounted || _rendererMode != RendererMode.hardware) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
_engine.updateRendererSettings(
|
||||||
_rendererMode = RendererMode.software;
|
_engine.rendererSettings.copyWith(mode: WolfRendererMode.software),
|
||||||
});
|
);
|
||||||
}
|
|
||||||
|
|
||||||
void _toggleFpsCounter() {
|
|
||||||
_engine.showFpsCounter = !_engine.showFpsCounter;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _cycleAsciiTheme() {
|
|
||||||
_asciiTheme = AsciiThemes.nextOf(_asciiTheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _toggleGlslEffects() {
|
|
||||||
_glslEffectsEnabled = !_glslEffectsEnabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openDebugTools() {
|
void _openDebugTools() {
|
||||||
|
|||||||
@@ -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.onQuit,
|
||||||
this.onGameSelected,
|
this.onGameSelected,
|
||||||
this.onEpisodeSelected,
|
this.onEpisodeSelected,
|
||||||
|
this.onRendererSettingsChanged,
|
||||||
|
WolfRendererCapabilities? rendererCapabilities,
|
||||||
|
WolfRendererSettings? rendererSettings,
|
||||||
EngineAudio? engineAudio,
|
EngineAudio? engineAudio,
|
||||||
}) : assert(
|
}) : rendererCapabilities =
|
||||||
|
rendererCapabilities ??
|
||||||
|
const WolfRendererCapabilities(
|
||||||
|
supportedModes: <WolfRendererMode>{
|
||||||
|
WolfRendererMode.software,
|
||||||
|
},
|
||||||
|
supportsFpsCounter: true,
|
||||||
|
),
|
||||||
|
rendererSettings = rendererSettings ?? const WolfRendererSettings(),
|
||||||
|
assert(
|
||||||
data != null || (availableGames != null && availableGames.isNotEmpty),
|
data != null || (availableGames != null && availableGames.isNotEmpty),
|
||||||
'Provide either data or a non-empty availableGames list.',
|
'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.');
|
throw StateError('WolfEngine requires at least one game data set.');
|
||||||
}
|
}
|
||||||
menuManager.menuBackgroundRgb = menuBackgroundRgb;
|
menuManager.menuBackgroundRgb = menuBackgroundRgb;
|
||||||
|
_normalizeRendererSettings();
|
||||||
|
_syncRendererMenuModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Total milliseconds elapsed since the engine was initialized.
|
/// Total milliseconds elapsed since the engine was initialized.
|
||||||
@@ -106,6 +120,15 @@ class WolfEngine {
|
|||||||
/// Callback triggered when episode selection changes; `null` means cleared.
|
/// Callback triggered when episode selection changes; `null` means cleared.
|
||||||
final void Function(int? episodeIndex)? onEpisodeSelected;
|
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 ---
|
// --- State Managers ---
|
||||||
|
|
||||||
/// Manages the state and animation of doors throughout the level.
|
/// Manages the state and animation of doors throughout the level.
|
||||||
@@ -219,6 +242,169 @@ class WolfEngine {
|
|||||||
log("[DEBUG] FrameBuffer resized to ${width}x$height");
|
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.
|
/// The primary heartbeat of the engine.
|
||||||
///
|
///
|
||||||
/// Updates all world subsystems based on the [elapsed] time.
|
/// Updates all world subsystems based on the [elapsed] time.
|
||||||
@@ -312,6 +498,12 @@ class WolfEngine {
|
|||||||
case WolfMenuScreen.difficultySelect:
|
case WolfMenuScreen.difficultySelect:
|
||||||
_tickDifficultySelectionMenu(input);
|
_tickDifficultySelectionMenu(input);
|
||||||
break;
|
break;
|
||||||
|
case WolfMenuScreen.changeView:
|
||||||
|
_tickChangeViewMenu(input);
|
||||||
|
break;
|
||||||
|
case WolfMenuScreen.rendererOptions:
|
||||||
|
_tickRendererOptionsMenu(input);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,11 +537,14 @@ class WolfEngine {
|
|||||||
case WolfMenuMainAction.backToDemo:
|
case WolfMenuMainAction.backToDemo:
|
||||||
_exitTopLevelMenu();
|
_exitTopLevelMenu();
|
||||||
break;
|
break;
|
||||||
|
case WolfMenuMainAction.changeView:
|
||||||
|
_syncRendererMenuModel();
|
||||||
|
menuManager.showChangeViewMenu();
|
||||||
|
break;
|
||||||
case WolfMenuMainAction.sound:
|
case WolfMenuMainAction.sound:
|
||||||
case WolfMenuMainAction.control:
|
case WolfMenuMainAction.control:
|
||||||
case WolfMenuMainAction.loadGame:
|
case WolfMenuMainAction.loadGame:
|
||||||
case WolfMenuMainAction.saveGame:
|
case WolfMenuMainAction.saveGame:
|
||||||
case WolfMenuMainAction.changeView:
|
|
||||||
case WolfMenuMainAction.readThis:
|
case WolfMenuMainAction.readThis:
|
||||||
case WolfMenuMainAction.viewScores:
|
case WolfMenuMainAction.viewScores:
|
||||||
case null:
|
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() {
|
void _beginNewGameMenuFlow() {
|
||||||
onEpisodeSelected?.call(null);
|
onEpisodeSelected?.call(null);
|
||||||
menuManager.clearEpisodeSelection();
|
menuManager.clearEpisodeSelection();
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ enum WolfMenuScreen {
|
|||||||
gameSelect,
|
gameSelect,
|
||||||
episodeSelect,
|
episodeSelect,
|
||||||
difficultySelect,
|
difficultySelect,
|
||||||
|
changeView,
|
||||||
|
rendererOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum WolfIntroSlide { retailWarning, pg13, title }
|
enum WolfIntroSlide { retailWarning, pg13, title }
|
||||||
@@ -40,6 +42,36 @@ class WolfMenuMainEntry {
|
|||||||
final bool isEnabled;
|
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) {
|
bool _isWiredMainMenuAction(WolfMenuMainAction action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case WolfMenuMainAction.newGame:
|
case WolfMenuMainAction.newGame:
|
||||||
@@ -48,11 +80,12 @@ bool _isWiredMainMenuAction(WolfMenuMainAction action) {
|
|||||||
case WolfMenuMainAction.backToDemo:
|
case WolfMenuMainAction.backToDemo:
|
||||||
case WolfMenuMainAction.quit:
|
case WolfMenuMainAction.quit:
|
||||||
return true;
|
return true;
|
||||||
|
case WolfMenuMainAction.changeView:
|
||||||
|
return true;
|
||||||
case WolfMenuMainAction.sound:
|
case WolfMenuMainAction.sound:
|
||||||
case WolfMenuMainAction.control:
|
case WolfMenuMainAction.control:
|
||||||
case WolfMenuMainAction.loadGame:
|
case WolfMenuMainAction.loadGame:
|
||||||
case WolfMenuMainAction.saveGame:
|
case WolfMenuMainAction.saveGame:
|
||||||
case WolfMenuMainAction.changeView:
|
|
||||||
case WolfMenuMainAction.readThis:
|
case WolfMenuMainAction.readThis:
|
||||||
case WolfMenuMainAction.viewScores:
|
case WolfMenuMainAction.viewScores:
|
||||||
return false;
|
return false;
|
||||||
@@ -85,6 +118,13 @@ class MenuManager {
|
|||||||
int _selectedGameIndex = 0;
|
int _selectedGameIndex = 0;
|
||||||
int _selectedEpisodeIndex = 0;
|
int _selectedEpisodeIndex = 0;
|
||||||
int _selectedDifficultyIndex = 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 _showResumeOption = false;
|
||||||
int _gameCount = 1;
|
int _gameCount = 1;
|
||||||
|
|
||||||
@@ -161,6 +201,18 @@ class MenuManager {
|
|||||||
|
|
||||||
int get selectedEpisodeIndex => _selectedEpisodeIndex;
|
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 {
|
List<WolfMenuMainEntry> get mainMenuEntries {
|
||||||
return List<WolfMenuMainEntry>.unmodifiable(
|
return List<WolfMenuMainEntry>.unmodifiable(
|
||||||
<WolfMenuMainEntry>[
|
<WolfMenuMainEntry>[
|
||||||
@@ -290,6 +342,133 @@ class MenuManager {
|
|||||||
_resetEdgeState();
|
_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.
|
/// Starts a menu transition. Input is locked until it completes.
|
||||||
///
|
///
|
||||||
/// Hosts can reuse this fade timing for future pre-menu splash/image
|
/// 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.
|
/// Returns a menu action snapshot for this frame.
|
||||||
({Difficulty? selected, bool goBack}) updateDifficultySelection(
|
({Difficulty? selected, bool goBack}) updateDifficultySelection(
|
||||||
EngineInput input,
|
EngineInput input,
|
||||||
@@ -612,6 +873,27 @@ class MenuManager {
|
|||||||
return mainMenuEntries[index].isEnabled;
|
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({
|
WolfMenuMainEntry _mainMenuEntry({
|
||||||
required WolfMenuMainAction action,
|
required WolfMenuMainAction action,
|
||||||
required String label,
|
required String label,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class RetailMenuPicModule extends MenuPicModule {
|
|||||||
MenuPicKey.footer: 15, // C_MOUSELBACKPIC (footer art)
|
MenuPicKey.footer: 15, // C_MOUSELBACKPIC (footer art)
|
||||||
MenuPicKey.heading: 3, // H_TOPWINDOWPIC
|
MenuPicKey.heading: 3, // H_TOPWINDOWPIC
|
||||||
MenuPicKey.optionsLabel: 7, // C_OPTIONSPIC
|
MenuPicKey.optionsLabel: 7, // C_OPTIONSPIC
|
||||||
|
MenuPicKey.customizeLabel: 24, // C_CUSTOMIZEPIC
|
||||||
// --- Cursor / markers ---
|
// --- Cursor / markers ---
|
||||||
MenuPicKey.cursorActive: 8, // C_CURSOR1PIC
|
MenuPicKey.cursorActive: 8, // C_CURSOR1PIC
|
||||||
MenuPicKey.cursorInactive: 9, // C_CURSOR2PIC
|
MenuPicKey.cursorInactive: 9, // C_CURSOR2PIC
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class SharewareMenuPicModule extends MenuPicModule {
|
|||||||
MenuPicKey.footer: 15,
|
MenuPicKey.footer: 15,
|
||||||
MenuPicKey.heading: 3,
|
MenuPicKey.heading: 3,
|
||||||
MenuPicKey.optionsLabel: 7,
|
MenuPicKey.optionsLabel: 7,
|
||||||
|
MenuPicKey.customizeLabel: 36,
|
||||||
MenuPicKey.cursorActive: 8,
|
MenuPicKey.cursorActive: 8,
|
||||||
MenuPicKey.cursorInactive: 9,
|
MenuPicKey.cursorInactive: 9,
|
||||||
MenuPicKey.markerSelected: 11,
|
MenuPicKey.markerSelected: 11,
|
||||||
@@ -57,6 +58,7 @@ class SharewareMenuPicModule extends MenuPicModule {
|
|||||||
MenuPicKey.footer: 27,
|
MenuPicKey.footer: 27,
|
||||||
MenuPicKey.heading: 14,
|
MenuPicKey.heading: 14,
|
||||||
MenuPicKey.optionsLabel: 19,
|
MenuPicKey.optionsLabel: 19,
|
||||||
|
MenuPicKey.customizeLabel: 36,
|
||||||
MenuPicKey.cursorActive: 20,
|
MenuPicKey.cursorActive: 20,
|
||||||
MenuPicKey.cursorInactive: 21,
|
MenuPicKey.cursorInactive: 21,
|
||||||
MenuPicKey.markerSelected: 23,
|
MenuPicKey.markerSelected: 23,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ enum MenuPicKey {
|
|||||||
footer('footer'),
|
footer('footer'),
|
||||||
heading('heading'),
|
heading('heading'),
|
||||||
optionsLabel('optionsLabel'),
|
optionsLabel('optionsLabel'),
|
||||||
|
customizeLabel('customizeLabel'),
|
||||||
|
|
||||||
// --- Cursor / selection markers ---
|
// --- Cursor / selection markers ---
|
||||||
cursorActive('cursorActive'),
|
cursorActive('cursorActive'),
|
||||||
|
|||||||
@@ -642,6 +642,163 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
return;
|
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 =
|
final int selectedDifficultyIndex =
|
||||||
engine.menuManager.selectedDifficultyIndex;
|
engine.menuManager.selectedDifficultyIndex;
|
||||||
|
|
||||||
@@ -997,7 +1154,10 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
int scale = 1,
|
int scale = 1,
|
||||||
}) {
|
}) {
|
||||||
if (y200 == _headerHeadingY) {
|
if (y200 == _headerHeadingY) {
|
||||||
y200 = _centerHeaderTitleInBlackBand(defaultY: y200, scale: scale);
|
y200 = _centerHeaderTitleInBlackBand(
|
||||||
|
defaultY: y200,
|
||||||
|
scale: scale,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final int textWidth = WolfMenuFont.measureTextWidth(text, scale);
|
final int textWidth = WolfMenuFont.measureTextWidth(text, scale);
|
||||||
final int x320 = ((320 - textWidth) ~/ 2).clamp(0, 319);
|
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() {
|
void _drawCenteredMenuFooter() {
|
||||||
if (_usesTerminalLayout && !_emitAnsi) {
|
if (_usesTerminalLayout && !_emitAnsi) {
|
||||||
_drawFlutterGridMenuFooter();
|
_drawFlutterGridMenuFooter();
|
||||||
|
|||||||
@@ -526,6 +526,146 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
return;
|
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 =
|
final int selectedDifficultyIndex =
|
||||||
engine.menuManager.selectedDifficultyIndex;
|
engine.menuManager.selectedDifficultyIndex;
|
||||||
_drawHeaderBarStack(
|
_drawHeaderBarStack(
|
||||||
@@ -580,6 +720,45 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
_applyMenuFade(engine.menuManager.transitionAlpha, bgColor);
|
_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) {
|
void _drawMenuFooterArt(WolfClassicMenuArt art) {
|
||||||
final bottom = art.mappedPic(15);
|
final bottom = art.mappedPic(15);
|
||||||
if (bottom == null) {
|
if (bottom == null) {
|
||||||
|
|||||||
@@ -209,6 +209,28 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
unselectedTextColor,
|
unselectedTextColor,
|
||||||
);
|
);
|
||||||
break;
|
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);
|
_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(
|
void _drawGameSelectMenu(
|
||||||
WolfEngine engine,
|
WolfEngine engine,
|
||||||
WolfClassicMenuArt art,
|
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(
|
void _drawEpisodeSelectMenu(
|
||||||
WolfEngine engine,
|
WolfEngine engine,
|
||||||
WolfClassicMenuArt art,
|
WolfClassicMenuArt art,
|
||||||
|
|||||||
@@ -11,4 +11,6 @@ export 'src/engine/input/engine_input.dart';
|
|||||||
export 'src/engine/managers/door_manager.dart';
|
export 'src/engine/managers/door_manager.dart';
|
||||||
export 'src/engine/managers/pushwall_manager.dart';
|
export 'src/engine/managers/pushwall_manager.dart';
|
||||||
export 'src/engine/player/player.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';
|
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 cNormal = 18; // C_NORMALPIC
|
||||||
static const int cHard = 19; // C_HARDPIC
|
static const int cHard = 19; // C_HARDPIC
|
||||||
static const int cControl = 23; // C_CONTROLPIC
|
static const int cControl = 23; // C_CONTROLPIC
|
||||||
|
static const int cCustomize = 24; // C_CUSTOMIZEPIC
|
||||||
static const int cEpisode1 = 27; // C_EPISODE1PIC
|
static const int cEpisode1 = 27; // C_EPISODE1PIC
|
||||||
static const int cEpisode2 = 28; // C_EPISODE2PIC
|
static const int cEpisode2 = 28; // C_EPISODE2PIC
|
||||||
static const int cEpisode3 = 29; // C_EPISODE3PIC
|
static const int cEpisode3 = 29; // C_EPISODE3PIC
|
||||||
@@ -115,6 +116,8 @@ class WolfClassicMenuArt {
|
|||||||
|
|
||||||
VgaImage? get optionsLabel => _imageForKey(MenuPicKey.optionsLabel);
|
VgaImage? get optionsLabel => _imageForKey(MenuPicKey.optionsLabel);
|
||||||
|
|
||||||
|
VgaImage? get customizeLabel => _imageForKey(MenuPicKey.customizeLabel);
|
||||||
|
|
||||||
VgaImage? get credits => _imageForKey(MenuPicKey.credits);
|
VgaImage? get credits => _imageForKey(MenuPicKey.credits);
|
||||||
|
|
||||||
VgaImage? episodeOption(int episodeIndex) {
|
VgaImage? episodeOption(int episodeIndex) {
|
||||||
@@ -189,6 +192,8 @@ class WolfClassicMenuArt {
|
|||||||
return MenuPicKey.difficultyHard;
|
return MenuPicKey.difficultyHard;
|
||||||
case WolfMenuPic.cControl:
|
case WolfMenuPic.cControl:
|
||||||
return MenuPicKey.controlBackground;
|
return MenuPicKey.controlBackground;
|
||||||
|
case WolfMenuPic.cCustomize:
|
||||||
|
return MenuPicKey.customizeLabel;
|
||||||
case WolfMenuPic.cEpisode1:
|
case WolfMenuPic.cEpisode1:
|
||||||
return MenuPicKey.episode1;
|
return MenuPicKey.episode1;
|
||||||
case WolfMenuPic.cEpisode2:
|
case WolfMenuPic.cEpisode2:
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ void main() {
|
|||||||
final engine = _buildMultiGameEngine(input: input, difficulty: null);
|
final engine = _buildMultiGameEngine(input: input, difficulty: null);
|
||||||
|
|
||||||
engine.init();
|
engine.init();
|
||||||
|
_dismissIntroSplash(engine, input);
|
||||||
|
|
||||||
expect(engine.isMenuOpen, isTrue);
|
expect(engine.isMenuOpen, isTrue);
|
||||||
expect(engine.menuManager.activeMenu, WolfMenuScreen.gameSelect);
|
expect(engine.menuManager.activeMenu, WolfMenuScreen.gameSelect);
|
||||||
@@ -62,6 +63,7 @@ void main() {
|
|||||||
engine.tick(const Duration(milliseconds: 16));
|
engine.tick(const Duration(milliseconds: 16));
|
||||||
input.isInteracting = false;
|
input.isInteracting = false;
|
||||||
engine.tick(const Duration(milliseconds: 300));
|
engine.tick(const Duration(milliseconds: 300));
|
||||||
|
_dismissIntroSplash(engine, input);
|
||||||
|
|
||||||
expect(engine.menuManager.activeMenu, WolfMenuScreen.mainMenu);
|
expect(engine.menuManager.activeMenu, WolfMenuScreen.mainMenu);
|
||||||
expect(
|
expect(
|
||||||
@@ -85,7 +87,7 @@ void main() {
|
|||||||
engine.menuManager.mainMenuEntries
|
engine.menuManager.mainMenuEntries
|
||||||
.map((entry) => entry.isEnabled)
|
.map((entry) => entry.isEnabled)
|
||||||
.toList(),
|
.toList(),
|
||||||
[true, false, false, false, false, false, false, false, true, true],
|
[true, false, false, false, false, true, false, false, true, true],
|
||||||
);
|
);
|
||||||
|
|
||||||
input.isInteracting = true;
|
input.isInteracting = true;
|
||||||
@@ -102,6 +104,7 @@ void main() {
|
|||||||
final engine = _buildEngine(input: input, difficulty: null);
|
final engine = _buildEngine(input: input, difficulty: null);
|
||||||
|
|
||||||
engine.init();
|
engine.init();
|
||||||
|
_advanceToMainMenu(engine, input);
|
||||||
|
|
||||||
expect(engine.isMenuOpen, isTrue);
|
expect(engine.isMenuOpen, isTrue);
|
||||||
expect(engine.menuManager.activeMenu, WolfMenuScreen.mainMenu);
|
expect(engine.menuManager.activeMenu, WolfMenuScreen.mainMenu);
|
||||||
@@ -124,7 +127,7 @@ void main() {
|
|||||||
engine.menuManager.mainMenuEntries
|
engine.menuManager.mainMenuEntries
|
||||||
.map((entry) => entry.isEnabled)
|
.map((entry) => entry.isEnabled)
|
||||||
.toList(),
|
.toList(),
|
||||||
[true, false, false, false, false, false, false, false, true, true],
|
[true, false, false, false, false, true, false, false, true, true],
|
||||||
);
|
);
|
||||||
|
|
||||||
input.isInteracting = true;
|
input.isInteracting = true;
|
||||||
@@ -172,7 +175,7 @@ void main() {
|
|||||||
engine.menuManager.mainMenuEntries
|
engine.menuManager.mainMenuEntries
|
||||||
.map((entry) => entry.isEnabled)
|
.map((entry) => entry.isEnabled)
|
||||||
.toList(),
|
.toList(),
|
||||||
[true, false, false, false, false, false, false, true, true, true],
|
[true, false, false, false, false, true, false, true, true, true],
|
||||||
);
|
);
|
||||||
|
|
||||||
input.isMovingForward = true;
|
input.isMovingForward = true;
|
||||||
@@ -203,6 +206,10 @@ void main() {
|
|||||||
|
|
||||||
expect(manager.selectedMainIndex, 0);
|
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(isMovingBackward: true));
|
||||||
manager.updateMainMenu(const EngineInput());
|
manager.updateMainMenu(const EngineInput());
|
||||||
expect(manager.selectedMainIndex, 8);
|
expect(manager.selectedMainIndex, 8);
|
||||||
@@ -232,6 +239,12 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
engine.init();
|
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;
|
input.isMovingBackward = true;
|
||||||
engine.tick(const Duration(milliseconds: 16));
|
engine.tick(const Duration(milliseconds: 16));
|
||||||
@@ -269,6 +282,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
engine.init();
|
engine.init();
|
||||||
|
_advanceToMainMenu(engine, input);
|
||||||
|
|
||||||
input.isBack = true;
|
input.isBack = true;
|
||||||
engine.tick(const Duration(milliseconds: 16));
|
engine.tick(const Duration(milliseconds: 16));
|
||||||
@@ -417,6 +431,28 @@ class _SilentAudio implements EngineAudio {
|
|||||||
void dispose() {}
|
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));
|
SpriteMap _buildGrid() => List.generate(64, (_) => List.filled(64, 0));
|
||||||
|
|
||||||
void _fillBoundaries(SpriteMap grid, int wallId) {
|
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() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/// Flutter host adapter for persisting renderer settings to a local file.
|
||||||
|
///
|
||||||
|
/// Uses `dart:io` for desktop targets where a writable path is available.
|
||||||
|
/// On web and other platforms that lack dart:io, persistence is silently
|
||||||
|
/// skipped so the rest of the app continues to work normally.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
|
||||||
|
/// Persists [WolfRendererSettings] as JSON to the app's support directory.
|
||||||
|
///
|
||||||
|
/// This implementation relies on `dart:io` and is therefore only active on
|
||||||
|
/// non-web platforms. Pass an explicit [filePath] during testing.
|
||||||
|
class FlutterRendererSettingsPersistence extends RendererSettingsPersistence
|
||||||
|
with JsonRendererSettingsPersistence {
|
||||||
|
FlutterRendererSettingsPersistence({String? filePath}) : _filePath = filePath;
|
||||||
|
|
||||||
|
final String? _filePath;
|
||||||
|
String? _resolvedPath;
|
||||||
|
|
||||||
|
Future<String> _getFilePath() async {
|
||||||
|
if (_resolvedPath != null) {
|
||||||
|
return _resolvedPath!;
|
||||||
|
}
|
||||||
|
if (_filePath != null) {
|
||||||
|
_resolvedPath = _filePath;
|
||||||
|
return _resolvedPath!;
|
||||||
|
}
|
||||||
|
// Resolve platform app-support directory.
|
||||||
|
final String home =
|
||||||
|
Platform.environment['HOME'] ?? Platform.environment['APPDATA'] ?? '.';
|
||||||
|
_resolvedPath = '$home/.wolf3d_settings.json';
|
||||||
|
return _resolvedPath!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> readRaw() async {
|
||||||
|
if (kIsWeb) return null;
|
||||||
|
try {
|
||||||
|
final String path = await _getFilePath();
|
||||||
|
final File f = File(path);
|
||||||
|
if (!f.existsSync()) return null;
|
||||||
|
return await f.readAsString();
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> writeRaw(String json) async {
|
||||||
|
if (kIsWeb) return;
|
||||||
|
try {
|
||||||
|
final String path = await _getFilePath();
|
||||||
|
await File(path).writeAsString(json, flush: true);
|
||||||
|
} catch (_) {
|
||||||
|
// Best-effort.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,6 +85,9 @@ class Wolf3d {
|
|||||||
WolfEngine launchEngine({
|
WolfEngine launchEngine({
|
||||||
required void Function() onGameWon,
|
required void Function() onGameWon,
|
||||||
void Function()? onQuit,
|
void Function()? onQuit,
|
||||||
|
WolfRendererCapabilities? rendererCapabilities,
|
||||||
|
WolfRendererSettings? rendererSettings,
|
||||||
|
void Function(WolfRendererSettings settings)? onRendererSettingsChanged,
|
||||||
}) {
|
}) {
|
||||||
if (availableGames.isEmpty) {
|
if (availableGames.isEmpty) {
|
||||||
throw StateError(
|
throw StateError(
|
||||||
@@ -106,6 +109,9 @@ class Wolf3d {
|
|||||||
// so backing out of the top-level menu should not pop the route.
|
// so backing out of the top-level menu should not pop the route.
|
||||||
onMenuExit: () {},
|
onMenuExit: () {},
|
||||||
onQuit: onQuit,
|
onQuit: onQuit,
|
||||||
|
rendererCapabilities: rendererCapabilities,
|
||||||
|
rendererSettings: rendererSettings,
|
||||||
|
onRendererSettingsChanged: onRendererSettingsChanged,
|
||||||
onGameSelected: (game) {
|
onGameSelected: (game) {
|
||||||
_activeGame = game;
|
_activeGame = game;
|
||||||
audio.activeGame = game;
|
audio.activeGame = game;
|
||||||
|
|||||||
Reference in New Issue
Block a user