Files
wolf_dart/apps/wolf_3d_gui/lib/screens/game_screen.dart

302 lines
8.8 KiB
Dart

/// 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<GameScreen> createState() => _GameScreenState();
}
class _GameScreenState extends State<GameScreen> {
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<LogicalKeyboardKey> pressedKeys =
HardwareKeyboard.instance.logicalKeysPressed;
return pressedKeys.contains(LogicalKeyboardKey.altLeft) ||
pressedKeys.contains(LogicalKeyboardKey.altRight) ||
pressedKeys.contains(LogicalKeyboardKey.alt);
}
Future<void> _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,
};
}
}