/// 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_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_renderer/wolf_3d_ascii_renderer.dart'; import 'package:wolf_3d_renderer/wolf_3d_flutter_renderer.dart'; import 'package:wolf_3d_renderer/wolf_3d_glsl_renderer.dart'; enum RendererMode { software, ascii, hardware, } 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; /// Creates a gameplay screen driven by [wolf3d]. const GameScreen({ required this.wolf3d, this.hostShortcutHandler, super.key, }); @override State createState() => _GameScreenState(); } class _GameScreenState extends State { late final WolfEngine _engine; RendererMode _rendererMode = RendererMode.hardware; AsciiTheme _asciiTheme = AsciiThemes.blocks; bool _glslEffectsEnabled = false; @override void initState() { super.initState(); _engine = widget.wolf3d.launchEngine( onGameWon: () { _engine.difficulty = null; widget.wolf3d.clearActiveDifficulty(); Navigator.of(context).pop(); }, onQuit: () { SystemNavigator.pop(); }, ); } @override Widget build(BuildContext context) { return PopScope( canPop: _engine.difficulty != null, onPopInvokedWithResult: (didPop, _) { if (!didPop && _engine.difficulty == null) { widget.wolf3d.input.queueBackAction(); } }, child: Scaffold( 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), ), ), 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), ), ), ), ], ), ); }, ), ), ); } Widget _buildRenderer() { // Keep all renderers behind the same engine so mode switching does not // reset level state or audio playback. switch (_rendererMode) { case RendererMode.software: return WolfFlutterRenderer( engine: _engine, onKeyEvent: _handleRendererKeyEvent, ); case RendererMode.ascii: return WolfAsciiRenderer( engine: _engine, theme: _asciiTheme, onKeyEvent: _handleRendererKeyEvent, ); case RendererMode.hardware: return WolfGlslRenderer( engine: _engine, effectsEnabled: _glslEffectsEnabled, onKeyEvent: _handleRendererKeyEvent, onUnavailable: _onGlslUnavailable, ); } } void _handleRendererKeyEvent(KeyEvent event) { if (event is! KeyDownEvent) { return; } if (_handleHostShortcut(event)) { return; } if (event.logicalKey == widget.wolf3d.input.rendererToggleKey) { setState(_cycleRendererMode); return; } if (event.logicalKey == widget.wolf3d.input.fpsToggleKey) { setState(_toggleFpsCounter); return; } if (event.logicalKey == widget.wolf3d.input.asciiThemeCycleKey) { if (_rendererMode == RendererMode.ascii) { setState(_cycleAsciiTheme); } else if (_rendererMode == RendererMode.hardware) { setState(_toggleGlslEffects); } } } 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() { if (!mounted || _rendererMode != RendererMode.hardware) { return; } setState(() { _rendererMode = RendererMode.software; }); } void _toggleFpsCounter() { _engine.showFpsCounter = !_engine.showFpsCounter; } void _cycleAsciiTheme() { _asciiTheme = AsciiThemes.nextOf(_asciiTheme); } void _toggleGlslEffects() { _glslEffectsEnabled = !_glslEffectsEnabled; } bool _handleHostShortcut(KeyEvent event) { final HostShortcutHandler? customHandler = widget.hostShortcutHandler; if (customHandler != null) { return customHandler(event, widget.wolf3d.input); } if (_isAltEnter(event)) { // Consume Enter so fullscreen toggling does not also activate menu items. widget.wolf3d.input.suppressInteractOnce(); unawaited(_toggleFullscreen()); return true; } return false; } bool _isAltEnter(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); } 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. } } bool get _supportsDesktopWindowing { if (kIsWeb) { return false; } return switch (defaultTargetPlatform) { TargetPlatform.linux || TargetPlatform.windows || TargetPlatform.macOS => true, _ => false, }; } }