From 3aa0049bb092694b5168d99bc6ac893b1db4740a Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Sun, 20 Sep 2026 15:54:00 +0800 Subject: feat(m6b): add playable menu and save flow [Change Nature] - This commit adds the M6b desktop menu and persistence integration. [New Capability] - Provide new game, continue, settings, controls, load error, save, load, and exit flows. - Provide a bounded autoplay flow covering menu navigation and persistence. [Implementation] - Add pure menu state transitions and focused M6b coverage. - Connect menu commands to simulation controls, WorldSave, view resets, and rendering. [Impact] - Desktop runtime gains the playable M6b front end without changing out-of-scope files. - Release focused and solution test suites remain passing. --- src/LivingVillage.Desktop/MenuState.fs | 192 +++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 src/LivingVillage.Desktop/MenuState.fs (limited to 'src/LivingVillage.Desktop/MenuState.fs') diff --git a/src/LivingVillage.Desktop/MenuState.fs b/src/LivingVillage.Desktop/MenuState.fs new file mode 100644 index 0000000..78d846c --- /dev/null +++ b/src/LivingVillage.Desktop/MenuState.fs @@ -0,0 +1,192 @@ +namespace LivingVillage.Desktop + +open LivingVillage.Kernel + +type MenuPage = + | MainMenu + | Settings + | Controls + | Playing + | LoadError + +type MenuItem = + | ContinueItem + | NewGameItem + | LoadItem + | SettingsItem + | ControlsItem + | ExitItem + +type MenuSetting = + | OneXSetting + | TwoXSetting + | FiveXSetting + | PauseSetting + +type MenuInput = + | Up + | Down + | Confirm + | Back + +type MenuCommand = + | NoCommand + | ContinueGame + | StartNewGame + | LoadGame + | ApplySettings of SimulationControl + | ExitGame + +type ControlRow = + { Keys: string + Action: string } + +type MenuState = + { Page: MenuPage + Selected: int + HasCurrentWorld: bool + ReturnPage: MenuPage + Settings: SimulationControl + Error: string option } + +module MenuState = + + let settings : MenuSetting list = + [ OneXSetting; TwoXSetting; FiveXSetting; PauseSetting ] + + let controlRows : ControlRow list = + [ { Keys = "WASD"; Action = "MOVE" } + { Keys = "E"; Action = "INTERACT" } + { Keys = "1-6"; Action = "DIALOGUE" } + { Keys = "Q"; Action = "OBSERVE" } + { Keys = "TAB"; Action = "NEEDS" } + { Keys = "C"; Action = "CHRONICLE" } + { Keys = "L"; Action = "RELATIONS" } + { Keys = "P"; Action = "PAUSE" } + { Keys = "F1/F2/F3"; Action = "SPEED" } + { Keys = "F6"; Action = "SAVE" } + { Keys = "F7"; Action = "LOAD" } + { Keys = "ESC"; Action = "MENU OR CLOSE" } + { Keys = "UP/DOWN"; Action = "SELECT" } + { Keys = "ENTER"; Action = "CONFIRM" } ] + + let create (simulationControl: SimulationControl) (hasCurrentWorld: bool) : MenuState = + { Page = MainMenu + Selected = 0 + HasCurrentWorld = hasCurrentWorld + ReturnPage = MainMenu + Settings = simulationControl + Error = None } + + let items (state: MenuState) : MenuItem list = + if state.HasCurrentWorld then + [ ContinueItem; NewGameItem; LoadItem; SettingsItem; ControlsItem; ExitItem ] + else + [ NewGameItem; LoadItem; SettingsItem; ControlsItem; ExitItem ] + + let settingsLabel setting : string = + match setting with + | OneXSetting -> "1X" + | TwoXSetting -> "2X" + | FiveXSetting -> "5X" + | PauseSetting -> "PAUSE" + + let itemLabel item : string = + match item with + | ContinueItem -> "CONTINUE" + | NewGameItem -> "NEW GAME" + | LoadItem -> "LOAD SAVE" + | SettingsItem -> "SETTINGS" + | ControlsItem -> "CONTROLS" + | ExitItem -> "QUIT" + + let private optionsFor (state: MenuState) : int = + match state.Page with + | MainMenu -> items state |> List.length + | Settings -> settings.Length + | _ -> 0 + + let private moveBy (delta: int) (state: MenuState) : MenuState = + let count = optionsFor state + if count = 0 then + state + else + let next = (state.Selected + delta) % count + let wrapped = if next < 0 then next + count else next + { state with Selected = wrapped } + + let move direction state = + match direction with + | Up -> moveBy -1 state + | Down -> moveBy 1 state + | Confirm + | Back -> state + + let selectedItem (state: MenuState) : MenuItem = + items state |> List.item state.Selected + + let selectedSetting (state: MenuState) : MenuSetting = + settings |> List.item state.Selected + + let private selectedControl (state: MenuState) : SimulationControl = + match selectedSetting state with + | OneXSetting -> { state.Settings with Paused = false; Speed = OneX } + | TwoXSetting -> { state.Settings with Paused = false; Speed = TwoX } + | FiveXSetting -> { state.Settings with Paused = false; Speed = FiveX } + | PauseSetting -> { state.Settings with Paused = true } + + let enterGame (state: MenuState) : MenuState = + { state with Page = Playing; Selected = 0; ReturnPage = MainMenu; Error = None } + + let setCurrentWorld (hasCurrentWorld: bool) (state: MenuState) : MenuState = + { state with HasCurrentWorld = hasCurrentWorld } + + let setSettings (simulationControl: SimulationControl) (state: MenuState) : MenuState = + { state with Settings = simulationControl } + + let openMain (state: MenuState) : MenuState = + { state with Page = MainMenu; Selected = 0; ReturnPage = MainMenu; Error = None } + + let openSettings (state: MenuState) : MenuState = + { state with Page = Settings; Selected = 0; ReturnPage = state.Page; Error = None } + + let openControls (state: MenuState) : MenuState = + { state with Page = Controls; Selected = 0; ReturnPage = state.Page; Error = None } + + let showLoadError (message: string) (state: MenuState) : MenuState = + { state with Page = LoadError; Selected = 0; ReturnPage = MainMenu; Error = Some message } + + let private returnFromSubpage (state: MenuState) : MenuState = + match state.ReturnPage with + | Playing -> { state with Page = Playing; Selected = 0; Error = None } + | _ -> openMain state + + let private confirmMain (state: MenuState) : MenuState * MenuCommand = + match selectedItem state with + | ContinueItem -> enterGame state, ContinueGame + | NewGameItem -> state, StartNewGame + | LoadItem -> state, LoadGame + | SettingsItem -> openSettings state, NoCommand + | ControlsItem -> openControls state, NoCommand + | ExitItem -> state, ExitGame + + let update (input: MenuInput) (state: MenuState) : MenuState * MenuCommand = + match input with + | Up -> move Up state, NoCommand + | Down -> move Down state, NoCommand + | Back -> + match state.Page with + | MainMenu -> state, ExitGame + | Settings + | Controls -> returnFromSubpage state, NoCommand + | LoadError -> openMain state, NoCommand + | Playing -> openMain state, NoCommand + | Confirm -> + match state.Page with + | MainMenu -> confirmMain state + | Settings -> + let nextSettings = selectedControl state + { state with Settings = nextSettings }, ApplySettings nextSettings + | Controls -> state, NoCommand + | LoadError -> openMain state, NoCommand + | Playing -> state, NoCommand -- cgit v1.2.3