Files
Funkin/source/funkin/ui/UIStateMachine.hx
T
Cameron TaylorandHundrec 040605e2a9 use UIStateMachine in FreeplayState instead of checking/modifying controls.active everywhere
thus making it easier to fix the "unlocked inputs" issue (fix FUNK-2021)
2025-12-12 08:16:37 -05:00

105 lines
2.6 KiB
Haxe

package funkin.ui;
/**
* Simple state machine for UI components
* Replaces scattered boolean flags with clean state management
*/
enum UIState
{
Idle;
Interacting;
EnteringMainMenu;
EnteringFreeplay;
Exiting;
Disabled;
}
/**
* Note: Not to be confust with FlxState or FlxSubState!
* State as in the design pattern!
* https://refactoring.guru/design-patterns/state
*
* TODO: Generalize this a bit more to allow the UIState enum be defined with any enum
*/
@:nullSafety
class UIStateMachine
{
public var currentState(default, null):UIState = Idle;
public var previousState(default, null):UIState = Idle;
var validTransitions:Map<UIState, Array<UIState>>;
var onStateChange:Array<(UIState, UIState) -> Void> = [];
public function new(?transitions:Map<UIState, Array<UIState>>)
{
// Default valid transitions if none provided
validTransitions = transitions != null ? transitions : [
Idle => [Interacting, EnteringMainMenu, EnteringFreeplay, Exiting, Disabled],
EnteringMainMenu => [Idle, Exiting, Disabled, Interacting],
Interacting => [Idle, EnteringMainMenu, EnteringFreeplay, Exiting, Disabled],
Exiting => [Idle],
Disabled => [Idle],
EnteringFreeplay => [Idle]
];
}
public function canTransition(from:UIState, to:UIState):Bool
{
if (from != currentState) return false;
var allowedStates = validTransitions.get(from);
return allowedStates != null && allowedStates.contains(to);
}
public function transition(newState:UIState):Bool
{
// Allow same-state transitions (idempotent)
if (currentState == newState)
{
trace('State transition: ${currentState} -> ${newState} (no change)');
return true;
}
if (!canTransition(currentState, newState))
{
trace('Invalid state transition: ${currentState} -> ${newState}');
return false;
}
previousState = currentState;
currentState = newState;
trace('State transition: ${previousState} -> ${currentState}');
// Notify listeners
for (callback in onStateChange)
{
callback(previousState, currentState);
}
return true;
}
public function onStateChanged(callback:(UIState, UIState) -> Void):Void
{
onStateChange.push(callback);
}
public function reset():Void
{
previousState = currentState;
currentState = Idle;
}
public function is(state:UIState):Bool
{
return currentState == state;
}
public function canInteract():Bool
{
// Entering is an enabled state since we want to be able to interact even during the screen fade wipe thing
return currentState == Idle || currentState == EnteringMainMenu;
}
}