Refactor menu rendering and state management
- Introduced _AsciiMenuTypography and _AsciiMenuRowFont enums to manage typography settings for menu rendering. - Updated AsciiRenderer to utilize new typography settings for main menu and game select screens. - Enhanced SixelRenderer and SoftwareRenderer to support new menu rendering logic, including sidebars for options labels. - Added disabled text color handling in WolfMenuPalette for better visual feedback on menu entries. - Implemented a new method _drawSelectableMenuRows to streamline the drawing of menu rows based on selection state. - Created a comprehensive test suite for level state carry-over and pause menu functionality, ensuring player state is preserved across levels and menus. - Adjusted footer rendering to account for layout changes and improved visual consistency across different renderers. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -1,10 +1,53 @@
|
||||
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 }
|
||||
enum WolfMenuScreen { mainMenu, gameSelect, episodeSelect, difficultySelect }
|
||||
|
||||
enum WolfMenuMainAction {
|
||||
newGame,
|
||||
sound,
|
||||
control,
|
||||
loadGame,
|
||||
saveGame,
|
||||
changeView,
|
||||
readThis,
|
||||
viewScores,
|
||||
endGame,
|
||||
backToGame,
|
||||
backToDemo,
|
||||
quit,
|
||||
}
|
||||
|
||||
class WolfMenuMainEntry {
|
||||
const WolfMenuMainEntry({
|
||||
required this.action,
|
||||
required this.label,
|
||||
this.isEnabled = true,
|
||||
});
|
||||
|
||||
final WolfMenuMainAction action;
|
||||
final String label;
|
||||
final bool isEnabled;
|
||||
}
|
||||
|
||||
bool _isWiredMainMenuAction(WolfMenuMainAction action) {
|
||||
switch (action) {
|
||||
case WolfMenuMainAction.newGame:
|
||||
case WolfMenuMainAction.endGame:
|
||||
case WolfMenuMainAction.backToGame:
|
||||
case WolfMenuMainAction.backToDemo:
|
||||
case WolfMenuMainAction.quit:
|
||||
return true;
|
||||
case WolfMenuMainAction.sound:
|
||||
case WolfMenuMainAction.control:
|
||||
case WolfMenuMainAction.loadGame:
|
||||
case WolfMenuMainAction.saveGame:
|
||||
case WolfMenuMainAction.changeView:
|
||||
case WolfMenuMainAction.readThis:
|
||||
case WolfMenuMainAction.viewScores:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles menu-only input state such as selection movement and edge triggers.
|
||||
class MenuManager {
|
||||
@@ -15,9 +58,12 @@ class MenuManager {
|
||||
int _transitionElapsedMs = 0;
|
||||
bool _transitionSwappedMenu = false;
|
||||
|
||||
int _selectedMainIndex = 0;
|
||||
int _selectedGameIndex = 0;
|
||||
int _selectedEpisodeIndex = 0;
|
||||
int _selectedDifficultyIndex = 0;
|
||||
bool _showResumeOption = false;
|
||||
int _gameCount = 1;
|
||||
|
||||
bool _prevUp = false;
|
||||
bool _prevDown = false;
|
||||
@@ -41,10 +87,60 @@ class MenuManager {
|
||||
return (1.0 - (fadeInElapsed / half)).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
int get selectedMainIndex => _selectedMainIndex;
|
||||
|
||||
int get selectedGameIndex => _selectedGameIndex;
|
||||
|
||||
int get selectedEpisodeIndex => _selectedEpisodeIndex;
|
||||
|
||||
List<WolfMenuMainEntry> get mainMenuEntries {
|
||||
return List<WolfMenuMainEntry>.unmodifiable(
|
||||
<WolfMenuMainEntry>[
|
||||
_mainMenuEntry(
|
||||
action: WolfMenuMainAction.newGame,
|
||||
label: 'NEW GAME',
|
||||
),
|
||||
_mainMenuEntry(
|
||||
action: WolfMenuMainAction.sound,
|
||||
label: 'SOUND',
|
||||
),
|
||||
_mainMenuEntry(
|
||||
action: WolfMenuMainAction.control,
|
||||
label: 'CONTROL',
|
||||
),
|
||||
_mainMenuEntry(
|
||||
action: WolfMenuMainAction.loadGame,
|
||||
label: 'LOAD GAME',
|
||||
),
|
||||
_mainMenuEntry(
|
||||
action: WolfMenuMainAction.saveGame,
|
||||
label: 'SAVE GAME',
|
||||
),
|
||||
_mainMenuEntry(
|
||||
action: WolfMenuMainAction.changeView,
|
||||
label: 'CHANGE VIEW',
|
||||
),
|
||||
_mainMenuEntry(
|
||||
action: WolfMenuMainAction.readThis,
|
||||
label: 'READ THIS!',
|
||||
),
|
||||
_mainMenuEntry(
|
||||
action: _showResumeOption
|
||||
? WolfMenuMainAction.endGame
|
||||
: WolfMenuMainAction.viewScores,
|
||||
label: _showResumeOption ? 'END GAME' : 'VIEW SCORES',
|
||||
),
|
||||
_mainMenuEntry(
|
||||
action: _showResumeOption
|
||||
? WolfMenuMainAction.backToGame
|
||||
: WolfMenuMainAction.backToDemo,
|
||||
label: _showResumeOption ? 'BACK TO GAME' : 'BACK TO DEMO',
|
||||
),
|
||||
_mainMenuEntry(action: WolfMenuMainAction.quit, label: 'QUIT'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Current selected difficulty row index.
|
||||
int get selectedDifficultyIndex => _selectedDifficultyIndex;
|
||||
|
||||
@@ -54,7 +150,11 @@ class MenuManager {
|
||||
int initialGameIndex = 0,
|
||||
int initialEpisodeIndex = 0,
|
||||
Difficulty? initialDifficulty,
|
||||
bool hasResumableGame = false,
|
||||
}) {
|
||||
_gameCount = gameCount;
|
||||
_showResumeOption = hasResumableGame;
|
||||
_selectedMainIndex = _defaultMainMenuIndex();
|
||||
_selectedGameIndex = _clampIndex(initialGameIndex, gameCount);
|
||||
_selectedEpisodeIndex = initialEpisodeIndex < 0 ? 0 : initialEpisodeIndex;
|
||||
_selectedDifficultyIndex = initialDifficulty == null
|
||||
@@ -64,7 +164,7 @@ class MenuManager {
|
||||
.clamp(0, Difficulty.values.length - 1);
|
||||
_activeMenu = gameCount > 1
|
||||
? WolfMenuScreen.gameSelect
|
||||
: WolfMenuScreen.episodeSelect;
|
||||
: WolfMenuScreen.mainMenu;
|
||||
_transitionTarget = null;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
@@ -82,6 +182,21 @@ class MenuManager {
|
||||
_activeMenu = WolfMenuScreen.difficultySelect;
|
||||
}
|
||||
|
||||
void showMainMenu({required bool hasResumableGame}) {
|
||||
_showResumeOption = hasResumableGame;
|
||||
final int itemCount = mainMenuEntries.length;
|
||||
if (itemCount == 0) {
|
||||
_selectedMainIndex = 0;
|
||||
} else {
|
||||
_selectedMainIndex = _defaultMainMenuIndex();
|
||||
}
|
||||
_activeMenu = WolfMenuScreen.mainMenu;
|
||||
_transitionTarget = null;
|
||||
_transitionElapsedMs = 0;
|
||||
_transitionSwappedMenu = false;
|
||||
_resetEdgeState();
|
||||
}
|
||||
|
||||
/// Starts a menu transition. Input is locked until it completes.
|
||||
///
|
||||
/// Hosts can reuse this fade timing for future pre-menu splash/image
|
||||
@@ -131,6 +246,29 @@ class MenuManager {
|
||||
_selectedGameIndex = _clampIndex(index, gameCount);
|
||||
}
|
||||
|
||||
({WolfMenuMainAction? selected, bool goBack}) updateMainMenu(
|
||||
EngineInput input,
|
||||
) {
|
||||
if (isTransitioning) {
|
||||
_consumeEdgeState(input);
|
||||
return (selected: null, goBack: false);
|
||||
}
|
||||
|
||||
final _MenuAction action = _updateLinearSelection(
|
||||
input,
|
||||
currentIndex: _selectedMainIndex,
|
||||
itemCount: mainMenuEntries.length,
|
||||
isSelectableIndex: _isSelectableMainIndex,
|
||||
);
|
||||
_selectedMainIndex = action.index;
|
||||
return (
|
||||
selected: action.confirmed
|
||||
? mainMenuEntries[_selectedMainIndex].action
|
||||
: null,
|
||||
goBack: action.goBack,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a menu action snapshot for this frame.
|
||||
({Difficulty? selected, bool goBack}) updateDifficultySelection(
|
||||
EngineInput input,
|
||||
@@ -215,6 +353,7 @@ class MenuManager {
|
||||
EngineInput input, {
|
||||
required int currentIndex,
|
||||
required int itemCount,
|
||||
bool Function(int index)? isSelectableIndex,
|
||||
}) {
|
||||
final upNow = input.isMovingForward;
|
||||
final downNow = input.isMovingBackward;
|
||||
@@ -222,18 +361,24 @@ class MenuManager {
|
||||
final backNow = input.isBack;
|
||||
|
||||
int nextIndex = _clampIndex(currentIndex, itemCount);
|
||||
final bool Function(int index) selectable =
|
||||
isSelectableIndex ?? ((_) => true);
|
||||
|
||||
if (itemCount > 0 && !selectable(nextIndex)) {
|
||||
nextIndex = _findSelectableIndex(nextIndex, itemCount, selectable);
|
||||
}
|
||||
|
||||
if (itemCount > 0) {
|
||||
if (upNow && !_prevUp) {
|
||||
nextIndex = (nextIndex - 1 + itemCount) % itemCount;
|
||||
nextIndex = _moveSelectableIndex(nextIndex, itemCount, -1, selectable);
|
||||
}
|
||||
|
||||
if (downNow && !_prevDown) {
|
||||
nextIndex = (nextIndex + 1) % itemCount;
|
||||
nextIndex = _moveSelectableIndex(nextIndex, itemCount, 1, selectable);
|
||||
}
|
||||
}
|
||||
|
||||
final bool confirmed = confirmNow && !_prevConfirm;
|
||||
final bool confirmed = confirmNow && !_prevConfirm && selectable(nextIndex);
|
||||
final bool goBack = backNow && !_prevBack;
|
||||
|
||||
_prevUp = upNow;
|
||||
@@ -258,6 +403,78 @@ class MenuManager {
|
||||
_prevBack = false;
|
||||
}
|
||||
|
||||
int _defaultMainMenuIndex() {
|
||||
final WolfMenuMainAction target = _showResumeOption
|
||||
? WolfMenuMainAction.backToGame
|
||||
: WolfMenuMainAction.newGame;
|
||||
final int found = mainMenuEntries.indexWhere(
|
||||
(entry) => entry.action == target,
|
||||
);
|
||||
return found >= 0
|
||||
? found
|
||||
: _findSelectableIndex(
|
||||
0,
|
||||
mainMenuEntries.length,
|
||||
_isSelectableMainIndex,
|
||||
);
|
||||
}
|
||||
|
||||
bool get canGoBackToGameSelection => !_showResumeOption && _gameCount > 1;
|
||||
|
||||
bool _isSelectableMainIndex(int index) {
|
||||
if (index < 0 || index >= mainMenuEntries.length) {
|
||||
return false;
|
||||
}
|
||||
return mainMenuEntries[index].isEnabled;
|
||||
}
|
||||
|
||||
WolfMenuMainEntry _mainMenuEntry({
|
||||
required WolfMenuMainAction action,
|
||||
required String label,
|
||||
}) {
|
||||
return WolfMenuMainEntry(
|
||||
action: action,
|
||||
label: label,
|
||||
isEnabled: _isWiredMainMenuAction(action),
|
||||
);
|
||||
}
|
||||
|
||||
int _findSelectableIndex(
|
||||
int startIndex,
|
||||
int itemCount,
|
||||
bool Function(int index) selectable,
|
||||
) {
|
||||
if (itemCount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
for (int offset = 0; offset < itemCount; offset++) {
|
||||
final int index = (startIndex + offset) % itemCount;
|
||||
if (selectable(index)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return _clampIndex(startIndex, itemCount);
|
||||
}
|
||||
|
||||
int _moveSelectableIndex(
|
||||
int currentIndex,
|
||||
int itemCount,
|
||||
int delta,
|
||||
bool Function(int index) selectable,
|
||||
) {
|
||||
if (itemCount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
int index = currentIndex;
|
||||
for (int step = 0; step < itemCount; step++) {
|
||||
index = (index + delta + itemCount) % itemCount;
|
||||
if (selectable(index)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
int _clampIndex(int index, int itemCount) {
|
||||
if (itemCount <= 0) {
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user