/// Active gameplay screen for the Flutter host. library; import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:window_manager/window_manager.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_renderer.dart'; import 'package:wolf_3d_flutter/renderer/wolf_3d_ascii_renderer.dart'; import 'package:wolf_3d_flutter/renderer/wolf_3d_flutter_renderer.dart'; import 'package:wolf_3d_flutter/renderer/wolf_3d_glsl_renderer.dart'; import 'package:wolf_3d_flutter/wolf_3d_flutter.dart'; import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart'; import 'package:wolf_3d_gui/screens/debug_tools_screen.dart'; enum RendererMode { /// Software pixel renderer presented via decoded framebuffer images. software, /// Text-mode renderer for debugging and retro terminal aesthetics. ascii, /// GLSL renderer with optional CRT-style post processing. hardware, } /// Semantic actions that host-level shortcuts can trigger. /// /// These intents are intentionally UI-host focused (windowing, app shell), not /// engine gameplay actions. The engine continues to receive input through /// [Wolf3dFlutterInput]. enum HostShortcutIntent { /// Toggle desktop fullscreen on/off. toggleFullscreen, } /// Declarative mapping from a key pattern to a host shortcut intent. /// /// [matches] identifies whether a key event should trigger this binding. /// [suppressedActions] are one-frame engine actions that should be blocked /// when the binding is consumed (for example, blocking `interact` on Alt+Enter /// so Enter does not activate menu selections). class HostShortcutBinding { /// Predicate that returns true when this shortcut should fire. final bool Function(KeyEvent event) matches; /// Host operation to perform when [matches] succeeds. final HostShortcutIntent intent; /// Engine actions to suppress for a single input update tick. final Set suppressedActions; /// Creates a host shortcut binding with optional suppressed engine actions. const HostShortcutBinding({ required this.matches, required this.intent, this.suppressedActions = const {}, }); } /// Ordered set of host shortcut bindings. /// /// The first binding whose [HostShortcutBinding.matches] returns true wins. /// This keeps behavior deterministic when multiple shortcuts could overlap. class HostShortcutRegistry { /// Ordered bindings consulted for each incoming key-down event. final List bindings; /// Creates a registry with explicit [bindings]. const HostShortcutRegistry({ required this.bindings, }); /// Returns the first binding that matches [event], or null when none do. HostShortcutBinding? match(KeyEvent event) { for (final HostShortcutBinding binding in bindings) { if (binding.matches(event)) { return binding; } } return null; } /// Default host shortcuts used by [GameScreen]. /// /// Alt+Enter toggles fullscreen and suppresses the engine `interact` action /// for one frame so Enter does not also activate menu/game interactions. static const HostShortcutRegistry defaults = HostShortcutRegistry( bindings: [ HostShortcutBinding( matches: _isAltEnterShortcut, intent: HostShortcutIntent.toggleFullscreen, suppressedActions: {WolfInputAction.interact}, ), ], ); } /// Optional imperative host shortcut override. /// /// Return true when the event was fully handled. The handler receives the /// shared [Wolf3dFlutterInput] so it can suppress engine actions as needed. typedef HostShortcutHandler = bool Function( KeyEvent event, Wolf3dFlutterInput input, ); /// Launches a [WolfEngine] via [Wolf3d] and exposes renderer/input integrations. class GameScreen extends StatefulWidget { /// Shared application facade owning the engine, audio, and input. final Wolf3d wolf3d; /// Optional host-level shortcut override. /// /// Return `true` when the event was consumed. Handlers may call /// [Wolf3dFlutterInput.suppressActionOnce] to keep actions from reaching the /// engine update loop. final HostShortcutHandler? hostShortcutHandler; /// Declarative host shortcut registry used when [hostShortcutHandler] is null. final HostShortcutRegistry hostShortcutRegistry; /// Optional host app-exit hook. /// /// Defaults to [SystemNavigator.pop] in production, but tests can inject a /// fake callback to assert quit behavior. final Future Function()? appExitHandler; /// Skips engine bootstrap for lightweight widget tests. @visibleForTesting final bool skipEngineBootstrapForTest; /// Triggers the quit flow during init for deterministic widget tests. @visibleForTesting final bool triggerQuitOnInitForTest; /// Creates a gameplay screen driven by [wolf3d]. const GameScreen({ required this.wolf3d, this.hostShortcutHandler, this.hostShortcutRegistry = HostShortcutRegistry.defaults, this.appExitHandler, this.skipEngineBootstrapForTest = false, this.triggerQuitOnInitForTest = false, super.key, }); @override State createState() => _GameScreenState(); } class _GameScreenState extends State { WolfEngine? _engine; final DefaultRendererSettingsPersistence _persistence = DefaultRendererSettingsPersistence(); final DefaultSaveGamePersistence _savePersistence = DefaultSaveGamePersistence(); Future? _quitFuture; /// Mirrors [WolfRendererSettings.mode] into the Flutter renderer enum. RendererMode _rendererMode = RendererMode.hardware; @override void initState() { super.initState(); if (widget.skipEngineBootstrapForTest) { if (widget.triggerQuitOnInitForTest) { unawaited(_quitApplication()); } return; } const Set supportedModes = { WolfRendererMode.hardware, WolfRendererMode.software, WolfRendererMode.ascii, }; final engine = widget.wolf3d.launchEngine( rendererCapabilities: const WolfRendererCapabilities( supportedModes: supportedModes, supportsAsciiThemes: true, supportsHardwareEffects: true, supportsBloom: true, supportsFpsCounter: true, ), rendererSettings: const WolfRendererSettings( mode: WolfRendererMode.hardware, ), onRendererSettingsChanged: (settings) { unawaited(_persistence.save(settings)); if (mounted) { setState(() { _syncRendererModeFrom(settings); }); } }, onGameWon: () { _engine?.difficulty = null; widget.wolf3d.clearActiveDifficulty(); Navigator.of(context).pop(); }, onQuit: () { unawaited(_quitApplication()); }, saveGamePersistence: _savePersistence, ); _engine = engine; _syncRendererModeFrom(engine.rendererSettings); _loadPersistedSettings(); } Future _loadPersistedSettings() async { final engine = _engine; if (engine == null) { return; } 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 void dispose() { unawaited(widget.wolf3d.shutdownAudio()); super.dispose(); } Future _quitApplication() async { final existing = _quitFuture; if (existing != null) { await existing; return; } final quit = () async { await widget.wolf3d.shutdownAudio(); final handler = widget.appExitHandler; if (handler != null) { await handler(); } else { await SystemNavigator.pop(); } }(); _quitFuture = quit; await quit; } @override Widget build(BuildContext context) { final engine = _engine; if (engine == null) { return const Scaffold(body: SizedBox.shrink()); } return PopScope( canPop: engine.difficulty != null, onPopInvokedWithResult: (didPop, _) { if (!didPop && engine.difficulty == null) { widget.wolf3d.input.queueBackAction(); } }, child: Scaffold( floatingActionButton: kDebugMode && engine.difficulty != null ? FloatingActionButton( onPressed: _openDebugTools, tooltip: 'Open Debug Tools', child: const Icon(Icons.bug_report), ) : null, body: LayoutBuilder( builder: (context, constraints) { return Listener( onPointerDown: (event) { widget.wolf3d.input.onPointerDown(event); }, onPointerUp: widget.wolf3d.input.onPointerUp, onPointerMove: widget.wolf3d.input.onPointerMove, onPointerHover: widget.wolf3d.input.onPointerMove, child: Stack( children: [ _buildRenderer(), if (!engine.isInitialized) Container( color: Colors.black, child: const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(color: Colors.teal), SizedBox(height: 20), Text( 'GET PSYCHED!', style: TextStyle( color: Colors.teal, fontFamily: 'monospace', ), ), ], ), ), ), // A second full-screen overlay keeps the presentation simple while // the engine is still warming up or decoding the first frame. if (!engine.isInitialized) Container( color: Colors.black, child: const Center( child: CircularProgressIndicator(color: Colors.teal), ), ), ], ), ); }, ), ), ); } Widget _buildRenderer() { final engine = _engine; if (engine == null) { return const SizedBox.shrink(); } // Keep all renderers behind the same engine so mode switching does not // reset level state or audio playback. final WolfRendererSettings settings = engine.rendererSettings; switch (_rendererMode) { case RendererMode.software: return WolfFlutterRenderer( engine: engine, onKeyEvent: _handleRendererKeyEvent, ); case RendererMode.ascii: final AsciiTheme theme = settings.asciiThemeId == WolfRendererSettings.asciiThemeQuadrant ? AsciiThemes.quadrant : AsciiThemes.blocks; return WolfAsciiRenderer( engine: engine, theme: theme, onKeyEvent: _handleRendererKeyEvent, ); case RendererMode.hardware: return WolfGlslRenderer( engine: engine, effectsEnabled: settings.hardwareEffectsEnabled, bloomEnabled: settings.bloomEnabled, onKeyEvent: _handleRendererKeyEvent, onUnavailable: _onGlslUnavailable, ); } } void _handleRendererKeyEvent(KeyEvent event) { if (event is! KeyDownEvent) { return; } // Host shortcuts must be processed before game actions so they can // suppress overlapping keys (for example Alt+Enter consuming Enter). if (_handleHostShortcut(event)) { return; } if (event.logicalKey == widget.wolf3d.input.rendererToggleKey) { _engine?.cycleRendererMode(); return; } if (event.logicalKey == widget.wolf3d.input.fpsToggleKey) { setState(() => _engine?.toggleFpsCounter()); return; } if (event.logicalKey == widget.wolf3d.input.asciiThemeCycleKey) { if (_rendererMode == RendererMode.ascii) { _engine?.cycleAsciiTheme(); } else if (_rendererMode == RendererMode.hardware) { _engine?.toggleHardwareEffects(); } } } void _onGlslUnavailable() { if (!mounted || _rendererMode != RendererMode.hardware) { return; } final engine = _engine; if (engine == null) { return; } engine.updateRendererSettings( engine.rendererSettings.copyWith(mode: WolfRendererMode.software), ); } void _openDebugTools() { Navigator.of(context).push( MaterialPageRoute( builder: (_) => DebugToolsScreen(wolf3d: widget.wolf3d), ), ); } bool _handleHostShortcut(KeyEvent event) { final HostShortcutHandler? customHandler = widget.hostShortcutHandler; if (customHandler != null) { // Custom handlers take full precedence to support future menu-driven // rebinding/override systems without modifying this screen. return customHandler(event, widget.wolf3d.input); } final HostShortcutBinding? binding = widget.hostShortcutRegistry.match( event, ); if (binding == null) { return false; } // Suppress conflicting gameplay/menu actions for one update frame. for (final WolfInputAction action in binding.suppressedActions) { widget.wolf3d.input.suppressActionOnce(action); } switch (binding.intent) { case HostShortcutIntent.toggleFullscreen: unawaited(_toggleFullscreen()); } return true; } /// Toggles desktop fullscreen state when supported by the host platform. /// /// This no-ops on unsupported targets and safely ignores missing plugin /// hosts to keep gameplay input resilient in embedded/test environments. Future _toggleFullscreen() async { if (!_supportsDesktopWindowing) { return; } try { final bool isFullScreen = await windowManager.isFullScreen(); await windowManager.setFullScreen(!isFullScreen); } on MissingPluginException { // No-op on hosts where the window manager plugin is unavailable. } } /// Whether runtime desktop window management APIs are expected to work. bool get _supportsDesktopWindowing { if (kIsWeb) { return false; } return switch (defaultTargetPlatform) { TargetPlatform.linux || TargetPlatform.windows || TargetPlatform.macOS => true, _ => false, }; } } /// Returns true when [event] is Enter/NumpadEnter while Alt is pressed. bool _isAltEnterShortcut(KeyEvent event) { final bool isEnter = event.logicalKey == LogicalKeyboardKey.enter || event.logicalKey == LogicalKeyboardKey.numpadEnter; if (!isEnter) { return false; } final Set pressedKeys = HardwareKeyboard.instance.logicalKeysPressed; return pressedKeys.contains(LogicalKeyboardKey.altLeft) || pressedKeys.contains(LogicalKeyboardKey.altRight) || pressedKeys.contains(LogicalKeyboardKey.alt); }