Refactor menu rendering and improve projection sampling
- Updated AsciiRasterizer to support game and episode selection menus with improved layout and cursor handling. - Enhanced SixelRasterizer and SoftwareRasterizer to modularize menu drawing logic for game and episode selection. - Introduced new methods for drawing menus and applying fade effects across rasterizers. - Adjusted wall texture sampling in Rasterizer to anchor to projection height center for consistent rendering. - Added tests for wall texture sampling behavior to ensure legacy compatibility and new functionality. - Modified Flutter audio adapter to use nullable access for active game and adjusted game selection logic in the main class. - Cleaned up input handling in Wolf3dFlutterInput by removing unused menu tap variables. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -1,8 +1,22 @@
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
|
||||
// Keep this enum focused on currently implemented menu screens. A future
|
||||
// top-level "main" menu can be inserted before gameSelect without changing
|
||||
// transition timing or fade infrastructure.
|
||||
enum WolfMenuScreen { gameSelect, episodeSelect, difficultySelect }
|
||||
|
||||
/// Handles menu-only input state such as selection movement and edge triggers.
|
||||
class MenuManager {
|
||||
static const int transitionDurationMs = 280;
|
||||
|
||||
WolfMenuScreen _activeMenu = WolfMenuScreen.difficultySelect;
|
||||
WolfMenuScreen? _transitionTarget;
|
||||
int _transitionElapsedMs = 0;
|
||||
bool _transitionSwappedMenu = false;
|
||||
|
||||
int _selectedGameIndex = 0;
|
||||
int _selectedEpisodeIndex = 0;
|
||||
int _selectedDifficultyIndex = 0;
|
||||
|
||||
bool _prevUp = false;
|
||||
@@ -10,30 +24,122 @@ class MenuManager {
|
||||
bool _prevConfirm = false;
|
||||
bool _prevBack = false;
|
||||
|
||||
WolfMenuScreen get activeMenu => _activeMenu;
|
||||
|
||||
bool get isTransitioning => _transitionTarget != null;
|
||||
|
||||
/// Returns the fade alpha during transitions (0.0..1.0).
|
||||
double get transitionAlpha {
|
||||
if (!isTransitioning) {
|
||||
return 0.0;
|
||||
}
|
||||
final int half = transitionDurationMs ~/ 2;
|
||||
if (_transitionElapsedMs <= half) {
|
||||
return (_transitionElapsedMs / half).clamp(0.0, 1.0);
|
||||
}
|
||||
final int fadeInElapsed = _transitionElapsedMs - half;
|
||||
return (1.0 - (fadeInElapsed / half)).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
int get selectedGameIndex => _selectedGameIndex;
|
||||
|
||||
int get selectedEpisodeIndex => _selectedEpisodeIndex;
|
||||
|
||||
/// Current selected difficulty row index.
|
||||
int get selectedDifficultyIndex => _selectedDifficultyIndex;
|
||||
|
||||
/// Resets menu navigation state for a new difficulty selection flow.
|
||||
void beginDifficultySelection({Difficulty? initialDifficulty}) {
|
||||
/// Resets menu state for startup, optionally skipping game selection.
|
||||
void beginSelectionFlow({
|
||||
required int gameCount,
|
||||
int initialGameIndex = 0,
|
||||
int initialEpisodeIndex = 0,
|
||||
Difficulty? initialDifficulty,
|
||||
}) {
|
||||
_selectedGameIndex = _clampIndex(initialGameIndex, gameCount);
|
||||
_selectedEpisodeIndex = initialEpisodeIndex < 0 ? 0 : initialEpisodeIndex;
|
||||
_selectedDifficultyIndex = initialDifficulty == null
|
||||
? 0
|
||||
: Difficulty.values
|
||||
.indexOf(initialDifficulty)
|
||||
.clamp(
|
||||
0,
|
||||
Difficulty.values.length - 1,
|
||||
);
|
||||
.clamp(0, Difficulty.values.length - 1);
|
||||
_activeMenu = gameCount > 1
|
||||
? WolfMenuScreen.gameSelect
|
||||
: WolfMenuScreen.episodeSelect;
|
||||
_transitionTarget = null;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
_resetEdgeState();
|
||||
}
|
||||
|
||||
_prevUp = false;
|
||||
_prevDown = false;
|
||||
_prevConfirm = false;
|
||||
_prevBack = false;
|
||||
/// Resets menu navigation state for a new difficulty selection flow.
|
||||
void beginDifficultySelection({Difficulty? initialDifficulty}) {
|
||||
beginSelectionFlow(
|
||||
gameCount: 1,
|
||||
initialGameIndex: 0,
|
||||
initialEpisodeIndex: 0,
|
||||
initialDifficulty: initialDifficulty,
|
||||
);
|
||||
_activeMenu = WolfMenuScreen.difficultySelect;
|
||||
}
|
||||
|
||||
/// Starts a menu transition. Input is locked until it completes.
|
||||
///
|
||||
/// Hosts can reuse this fade timing for future pre-menu splash/image
|
||||
/// sequences so transitions feel consistent across the whole app.
|
||||
void startTransition(WolfMenuScreen target) {
|
||||
if (_activeMenu == target) {
|
||||
return;
|
||||
}
|
||||
_transitionTarget = target;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
_resetEdgeState();
|
||||
}
|
||||
|
||||
/// Advances transition timers and swaps menu at midpoint.
|
||||
void tickTransition(int deltaMs) {
|
||||
if (!isTransitioning) {
|
||||
return;
|
||||
}
|
||||
_transitionElapsedMs += deltaMs;
|
||||
final int half = transitionDurationMs ~/ 2;
|
||||
if (!_transitionSwappedMenu && _transitionElapsedMs >= half) {
|
||||
_activeMenu = _transitionTarget!;
|
||||
_transitionSwappedMenu = true;
|
||||
}
|
||||
if (_transitionElapsedMs >= transitionDurationMs) {
|
||||
_transitionTarget = null;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
void clearEpisodeSelection() {
|
||||
_selectedEpisodeIndex = 0;
|
||||
}
|
||||
|
||||
/// Consumes current input as the new edge baseline.
|
||||
void absorbInputState(EngineInput input) {
|
||||
_consumeEdgeState(input);
|
||||
}
|
||||
|
||||
void setSelectedEpisodeIndex(int index, int episodeCount) {
|
||||
_selectedEpisodeIndex = _clampIndex(index, episodeCount);
|
||||
}
|
||||
|
||||
void setSelectedGameIndex(int index, int gameCount) {
|
||||
_selectedGameIndex = _clampIndex(index, gameCount);
|
||||
}
|
||||
|
||||
/// Returns a menu action snapshot for this frame.
|
||||
({Difficulty? selected, bool goBack}) updateDifficultySelection(
|
||||
EngineInput input,
|
||||
) {
|
||||
if (isTransitioning) {
|
||||
_consumeEdgeState(input);
|
||||
return (selected: null, goBack: false);
|
||||
}
|
||||
|
||||
final upNow = input.isMovingForward;
|
||||
final downNow = input.isMovingBackward;
|
||||
final confirmNow = input.isInteracting || input.isFiring;
|
||||
@@ -50,30 +156,6 @@ class MenuManager {
|
||||
(_selectedDifficultyIndex + 1) % Difficulty.values.length;
|
||||
}
|
||||
|
||||
// Pointer/touch selection for hosts that provide menu tap coordinates.
|
||||
if (input.menuTapX != null && input.menuTapY != null) {
|
||||
final x320 = (input.menuTapX!.clamp(0.0, 1.0) * 320).toDouble();
|
||||
final y200 = (input.menuTapY!.clamp(0.0, 1.0) * 200).toDouble();
|
||||
|
||||
const panelX = 28.0;
|
||||
const panelY = 70.0;
|
||||
const panelW = 264.0;
|
||||
const panelH = 82.0;
|
||||
const rowYStart = 86.0;
|
||||
const rowStep = 15.0;
|
||||
|
||||
if (x320 >= panelX &&
|
||||
x320 <= panelX + panelW &&
|
||||
y200 >= panelY &&
|
||||
y200 <= panelY + panelH) {
|
||||
final index = ((y200 - rowYStart + (rowStep / 2)) / rowStep).floor();
|
||||
if (index >= 0 && index < Difficulty.values.length) {
|
||||
_selectedDifficultyIndex = index;
|
||||
return (selected: Difficulty.values[index], goBack: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Difficulty? selected;
|
||||
if (confirmNow && !_prevConfirm) {
|
||||
selected = Difficulty.values[_selectedDifficultyIndex];
|
||||
@@ -89,6 +171,112 @@ class MenuManager {
|
||||
return (selected: selected, goBack: goBack);
|
||||
}
|
||||
|
||||
({int? selectedIndex, bool goBack}) updateGameSelection(
|
||||
EngineInput input, {
|
||||
required int gameCount,
|
||||
}) {
|
||||
if (isTransitioning) {
|
||||
_consumeEdgeState(input);
|
||||
return (selectedIndex: null, goBack: false);
|
||||
}
|
||||
final _MenuAction action = _updateLinearSelection(
|
||||
input,
|
||||
currentIndex: _selectedGameIndex,
|
||||
itemCount: gameCount,
|
||||
);
|
||||
_selectedGameIndex = action.index;
|
||||
return (
|
||||
selectedIndex: action.confirmed ? _selectedGameIndex : null,
|
||||
goBack: action.goBack,
|
||||
);
|
||||
}
|
||||
|
||||
({int? selectedIndex, bool goBack}) updateEpisodeSelection(
|
||||
EngineInput input, {
|
||||
required int episodeCount,
|
||||
}) {
|
||||
if (isTransitioning) {
|
||||
_consumeEdgeState(input);
|
||||
return (selectedIndex: null, goBack: false);
|
||||
}
|
||||
final _MenuAction action = _updateLinearSelection(
|
||||
input,
|
||||
currentIndex: _selectedEpisodeIndex,
|
||||
itemCount: episodeCount,
|
||||
);
|
||||
_selectedEpisodeIndex = action.index;
|
||||
return (
|
||||
selectedIndex: action.confirmed ? _selectedEpisodeIndex : null,
|
||||
goBack: action.goBack,
|
||||
);
|
||||
}
|
||||
|
||||
_MenuAction _updateLinearSelection(
|
||||
EngineInput input, {
|
||||
required int currentIndex,
|
||||
required int itemCount,
|
||||
}) {
|
||||
final upNow = input.isMovingForward;
|
||||
final downNow = input.isMovingBackward;
|
||||
final confirmNow = input.isInteracting || input.isFiring;
|
||||
final backNow = input.isBack;
|
||||
|
||||
int nextIndex = _clampIndex(currentIndex, itemCount);
|
||||
|
||||
if (itemCount > 0) {
|
||||
if (upNow && !_prevUp) {
|
||||
nextIndex = (nextIndex - 1 + itemCount) % itemCount;
|
||||
}
|
||||
|
||||
if (downNow && !_prevDown) {
|
||||
nextIndex = (nextIndex + 1) % itemCount;
|
||||
}
|
||||
}
|
||||
|
||||
final bool confirmed = confirmNow && !_prevConfirm;
|
||||
final bool goBack = backNow && !_prevBack;
|
||||
|
||||
_prevUp = upNow;
|
||||
_prevDown = downNow;
|
||||
_prevConfirm = confirmNow;
|
||||
_prevBack = backNow;
|
||||
|
||||
return _MenuAction(index: nextIndex, confirmed: confirmed, goBack: goBack);
|
||||
}
|
||||
|
||||
void _consumeEdgeState(EngineInput input) {
|
||||
_prevUp = input.isMovingForward;
|
||||
_prevDown = input.isMovingBackward;
|
||||
_prevConfirm = input.isInteracting || input.isFiring;
|
||||
_prevBack = input.isBack;
|
||||
}
|
||||
|
||||
void _resetEdgeState() {
|
||||
_prevUp = false;
|
||||
_prevDown = false;
|
||||
_prevConfirm = false;
|
||||
_prevBack = false;
|
||||
}
|
||||
|
||||
int _clampIndex(int index, int itemCount) {
|
||||
if (itemCount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return index.clamp(0, itemCount - 1);
|
||||
}
|
||||
|
||||
/// Whether to show the alternate cursor frame at [elapsedMs].
|
||||
bool isCursorAltFrame(int elapsedMs) => ((elapsedMs ~/ 220) % 2) == 1;
|
||||
}
|
||||
|
||||
class _MenuAction {
|
||||
const _MenuAction({
|
||||
required this.index,
|
||||
required this.confirmed,
|
||||
required this.goBack,
|
||||
});
|
||||
|
||||
final int index;
|
||||
final bool confirmed;
|
||||
final bool goBack;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user