Refactor menu structure and add Flutter-specific input and persistence layers
- Moved menu-related classes to a new structure under `src/menu/`. - Introduced `WolfMenuPresentation` to handle menu art and mappings. - Added `MenuManager` tests to ensure menu state reflects game status. - Implemented `FlutterRendererSettingsPersistence` and `FlutterSaveGamePersistence` for managing settings and save files on desktop platforms. - Created `Wolf3dFlutterInput` to handle keyboard and mouse input in a Flutter environment. - Updated README to reflect new package structure and usage instructions. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
/// Flutter-specific input adapter for the Wolf3D engine.
|
||||
///
|
||||
/// This class merges keyboard and pointer events into the frame-based fields
|
||||
/// exposed by [Wolf3dInput]. It is designed to be owned by higher-level app
|
||||
/// code and reused across menu and gameplay screens.
|
||||
library;
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||
|
||||
/// Translates Flutter keyboard and mouse state into engine-friendly actions.
|
||||
class Wolf3dFlutterInput extends Wolf3dInput {
|
||||
/// Keyboard shortcut used by the Flutter host to cycle renderer modes.
|
||||
LogicalKeyboardKey rendererToggleKey = LogicalKeyboardKey.keyR;
|
||||
|
||||
/// Keyboard shortcut used by the Flutter host to cycle ASCII themes.
|
||||
LogicalKeyboardKey asciiThemeCycleKey = LogicalKeyboardKey.keyT;
|
||||
|
||||
/// Keyboard shortcut used by the Flutter host to toggle the FPS counter.
|
||||
LogicalKeyboardKey fpsToggleKey = LogicalKeyboardKey.backquote;
|
||||
|
||||
/// Human-friendly label for [rendererToggleKey] shown in on-screen hints.
|
||||
String get rendererToggleKeyLabel => _formatShortcutLabel(rendererToggleKey);
|
||||
|
||||
/// Human-friendly label for [asciiThemeCycleKey] shown in on-screen hints.
|
||||
String get asciiThemeCycleKeyLabel =>
|
||||
_formatShortcutLabel(asciiThemeCycleKey);
|
||||
|
||||
/// Human-friendly label for [fpsToggleKey] shown in on-screen hints.
|
||||
String get fpsToggleKeyLabel => _formatShortcutLabel(fpsToggleKey);
|
||||
|
||||
String _formatShortcutLabel(LogicalKeyboardKey key) {
|
||||
if (key == LogicalKeyboardKey.space) {
|
||||
return 'SPACE';
|
||||
}
|
||||
if (key == LogicalKeyboardKey.tab) {
|
||||
return 'TAB';
|
||||
}
|
||||
final label = key.keyLabel;
|
||||
if (label.isNotEmpty) {
|
||||
return label.toUpperCase();
|
||||
}
|
||||
final debugName = key.debugName;
|
||||
if (debugName == null || debugName.isEmpty) {
|
||||
return 'KEY';
|
||||
}
|
||||
return debugName.replaceFirst('Logical Keyboard Key ', '').toUpperCase();
|
||||
}
|
||||
|
||||
/// Mapping from logical game actions to one or more keyboard bindings.
|
||||
///
|
||||
/// Each action can be rebound by replacing the matching set. The defaults
|
||||
/// support both WASD and arrow-key movement for desktop hosts.
|
||||
Map<WolfInputAction, Set<LogicalKeyboardKey>> bindings = {
|
||||
WolfInputAction.forward: {
|
||||
LogicalKeyboardKey.keyW,
|
||||
LogicalKeyboardKey.arrowUp,
|
||||
},
|
||||
WolfInputAction.backward: {
|
||||
LogicalKeyboardKey.keyS,
|
||||
LogicalKeyboardKey.arrowDown,
|
||||
},
|
||||
WolfInputAction.turnLeft: {
|
||||
LogicalKeyboardKey.keyA,
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
},
|
||||
WolfInputAction.turnRight: {
|
||||
LogicalKeyboardKey.keyD,
|
||||
LogicalKeyboardKey.arrowRight,
|
||||
},
|
||||
WolfInputAction.mapToggle: {LogicalKeyboardKey.tab},
|
||||
WolfInputAction.fire: {
|
||||
LogicalKeyboardKey.controlLeft,
|
||||
LogicalKeyboardKey.controlRight,
|
||||
},
|
||||
WolfInputAction.interact: {
|
||||
LogicalKeyboardKey.space,
|
||||
LogicalKeyboardKey.enter,
|
||||
LogicalKeyboardKey.numpadEnter,
|
||||
},
|
||||
WolfInputAction.back: {LogicalKeyboardKey.escape},
|
||||
WolfInputAction.weapon1: {LogicalKeyboardKey.digit1},
|
||||
WolfInputAction.weapon2: {LogicalKeyboardKey.digit2},
|
||||
WolfInputAction.weapon3: {LogicalKeyboardKey.digit3},
|
||||
WolfInputAction.weapon4: {LogicalKeyboardKey.digit4},
|
||||
};
|
||||
|
||||
/// Whether the primary mouse button is currently held.
|
||||
bool isMouseLeftDown = false;
|
||||
|
||||
/// Whether the secondary mouse button is currently held.
|
||||
bool isMouseRightDown = false;
|
||||
double _mouseDeltaX = 0.0;
|
||||
double _mouseDeltaY = 0.0;
|
||||
bool _previousMouseRightDown = false;
|
||||
bool _queuedBack = false;
|
||||
|
||||
// One-frame suppression lets host shortcuts (for example Alt+Enter) consume
|
||||
// overlapping gameplay keys before the engine reads keyboard state.
|
||||
final Set<WolfInputAction> _suppressedActionsOnce = <WolfInputAction>{};
|
||||
|
||||
// Mouse-look is optional so touch or keyboard-only hosts can keep the same
|
||||
// adapter without incurring accidental pointer-driven movement.
|
||||
bool _isMouseLookEnabled = false;
|
||||
|
||||
/// Whether pointer deltas should be interpreted as movement/turn input.
|
||||
bool get mouseLookEnabled => _isMouseLookEnabled;
|
||||
|
||||
/// Enables or disables mouse-look style control.
|
||||
set mouseLookEnabled(bool value) {
|
||||
_isMouseLookEnabled = value;
|
||||
// Clear any built-up delta when turning it off so it doesn't
|
||||
// suddenly jump if they turn it back on.
|
||||
if (!value) {
|
||||
_mouseDeltaX = 0.0;
|
||||
_mouseDeltaY = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
Set<LogicalKeyboardKey> _previousKeys = {};
|
||||
|
||||
/// Rebinds [action] to a single [key], replacing any previous bindings.
|
||||
void bindKey(WolfInputAction action, LogicalKeyboardKey key) {
|
||||
bindings[action] = {};
|
||||
bindings[action]?.add(key);
|
||||
}
|
||||
|
||||
/// Removes [key] from the current binding set for [action].
|
||||
void unbindKey(WolfInputAction action, LogicalKeyboardKey key) {
|
||||
bindings[action]?.remove(key);
|
||||
}
|
||||
|
||||
/// Updates button state for a newly pressed pointer.
|
||||
void onPointerDown(PointerDownEvent event) {
|
||||
if (event.buttons & kPrimaryMouseButton != 0) isMouseLeftDown = true;
|
||||
if (event.buttons & kSecondaryMouseButton != 0) isMouseRightDown = true;
|
||||
}
|
||||
|
||||
/// Updates button state when a pointer is released.
|
||||
void onPointerUp(PointerUpEvent event) {
|
||||
if (event.buttons & kPrimaryMouseButton == 0) isMouseLeftDown = false;
|
||||
if (event.buttons & kSecondaryMouseButton == 0) isMouseRightDown = false;
|
||||
}
|
||||
|
||||
/// Accumulates pointer delta so it can be consumed on the next engine frame.
|
||||
void onPointerMove(PointerEvent event) {
|
||||
// Only capture movement if mouselook is actually enabled
|
||||
if (_isMouseLookEnabled) {
|
||||
_mouseDeltaX += event.delta.dx;
|
||||
_mouseDeltaY += event.delta.dy;
|
||||
}
|
||||
}
|
||||
|
||||
/// Queues a one-frame back action, typically from system back gestures.
|
||||
void queueBackAction() {
|
||||
_queuedBack = true;
|
||||
}
|
||||
|
||||
/// Suppresses [action] for the next [update] tick only.
|
||||
///
|
||||
/// This is primarily used by host-level shortcuts that share physical keys
|
||||
/// with gameplay/menu actions.
|
||||
void suppressActionOnce(WolfInputAction action) {
|
||||
_suppressedActionsOnce.add(action);
|
||||
}
|
||||
|
||||
/// Convenience helper for host shortcuts that consume Enter/Interact.
|
||||
void suppressInteractOnce() {
|
||||
suppressActionOnce(WolfInputAction.interact);
|
||||
}
|
||||
|
||||
/// Returns whether any bound key for [action] is currently pressed.
|
||||
bool _isActive(WolfInputAction action, Set<LogicalKeyboardKey> pressedKeys) {
|
||||
// Host-consumed actions should appear inactive for exactly one update.
|
||||
if (_suppressedActionsOnce.contains(action)) {
|
||||
return false;
|
||||
}
|
||||
return bindings[action]!.any((key) => pressedKeys.contains(key));
|
||||
}
|
||||
|
||||
/// Returns whether [action] was pressed during the current frame only.
|
||||
bool _isNewlyPressed(
|
||||
WolfInputAction action,
|
||||
Set<LogicalKeyboardKey> newlyPressed,
|
||||
) {
|
||||
// Suppressed actions also block edge-triggered press detection.
|
||||
if (_suppressedActionsOnce.contains(action)) {
|
||||
return false;
|
||||
}
|
||||
return bindings[action]!.any((key) => newlyPressed.contains(key));
|
||||
}
|
||||
|
||||
@override
|
||||
void update() {
|
||||
final pressedKeys = HardwareKeyboard.instance.logicalKeysPressed;
|
||||
final newlyPressedKeys = pressedKeys.difference(_previousKeys);
|
||||
|
||||
// Resolve digital keyboard state first so mouse-look only augments input.
|
||||
bool kbForward = _isActive(WolfInputAction.forward, pressedKeys);
|
||||
bool kbBackward = _isActive(WolfInputAction.backward, pressedKeys);
|
||||
bool kbLeft = _isActive(WolfInputAction.turnLeft, pressedKeys);
|
||||
bool kbRight = _isActive(WolfInputAction.turnRight, pressedKeys);
|
||||
|
||||
// Mouse-look intentionally maps pointer deltas back onto the engine's
|
||||
// simple boolean input contract instead of introducing analog turns.
|
||||
isMovingForward = kbForward || (_isMouseLookEnabled && _mouseDeltaY < -1.5);
|
||||
isMovingBackward =
|
||||
kbBackward || (_isMouseLookEnabled && _mouseDeltaY > 1.5);
|
||||
isTurningLeft = kbLeft || (_isMouseLookEnabled && _mouseDeltaX < -1.5);
|
||||
isTurningRight = kbRight || (_isMouseLookEnabled && _mouseDeltaX > 1.5);
|
||||
isMapToggle = _isNewlyPressed(WolfInputAction.mapToggle, newlyPressedKeys);
|
||||
|
||||
// Deltas are one-frame impulses, so consume them immediately after use.
|
||||
_mouseDeltaX = 0.0;
|
||||
_mouseDeltaY = 0.0;
|
||||
|
||||
// Right click or Spacebar to interact
|
||||
isInteracting =
|
||||
_isNewlyPressed(WolfInputAction.interact, newlyPressedKeys) ||
|
||||
(mouseLookEnabled && isMouseRightDown && !_previousMouseRightDown);
|
||||
|
||||
isBack =
|
||||
_isNewlyPressed(WolfInputAction.back, newlyPressedKeys) || _queuedBack;
|
||||
menuTapX = null;
|
||||
menuTapY = null;
|
||||
|
||||
// Left click or Ctrl to fire
|
||||
isFiring =
|
||||
_isActive(WolfInputAction.fire, pressedKeys) ||
|
||||
(mouseLookEnabled && isMouseLeftDown);
|
||||
|
||||
requestedWeapon = null;
|
||||
if (_isNewlyPressed(WolfInputAction.weapon1, newlyPressedKeys)) {
|
||||
requestedWeapon = WeaponType.knife;
|
||||
}
|
||||
if (_isNewlyPressed(WolfInputAction.weapon2, newlyPressedKeys)) {
|
||||
requestedWeapon = WeaponType.pistol;
|
||||
}
|
||||
if (_isNewlyPressed(WolfInputAction.weapon3, newlyPressedKeys)) {
|
||||
requestedWeapon = WeaponType.machineGun;
|
||||
}
|
||||
if (_isNewlyPressed(WolfInputAction.weapon4, newlyPressedKeys)) {
|
||||
requestedWeapon = WeaponType.chainGun;
|
||||
}
|
||||
|
||||
// Preserve prior frame state so edge-triggered actions like interact and
|
||||
// weapon switching only fire once per physical key press.
|
||||
_previousKeys = Set.from(pressedKeys);
|
||||
_previousMouseRightDown = isMouseRightDown;
|
||||
_queuedBack = false;
|
||||
|
||||
// Clear one-shot suppression so the action behaves normally next frame.
|
||||
_suppressedActionsOnce.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user