summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/MenuState.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-20 15:54:00 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-20 15:54:00 +0800
commit3aa0049bb092694b5168d99bc6ac893b1db4740a (patch)
treeb3cf6a0e21442780cd72e8f6a842a4f3db1cecca /src/LivingVillage.Desktop/MenuState.fs
parent9724663602b4b1eb755bf334c475dc2c80e4b386 (diff)
downloadliving-village-3aa0049bb092694b5168d99bc6ac893b1db4740a.tar.gz
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.
Diffstat (limited to 'src/LivingVillage.Desktop/MenuState.fs')
-rw-r--r--src/LivingVillage.Desktop/MenuState.fs192
1 files changed, 192 insertions, 0 deletions
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