summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/MenuState.fs
blob: 619ac58624915ec5fbaa3e35f959a7c48ed8905a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
namespace LivingVillage.Desktop

open LivingVillage.Kernel

type MenuPage =
    | MainMenu
    | OccupationSelect
    | 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
    | StartNewGameWith of Occupation.Kind option
    | 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 = "移动" }
          { Keys = "E"; Action = "互动" }
          { Keys = "1-6"; Action = "对话" }
          { Keys = "Q"; Action = "观察" }
          { Keys = "TAB"; Action = "需求" }
          { Keys = "C"; Action = "年鉴" }
          { Keys = "L"; Action = "关系" }
          { Keys = "P"; Action = "暂停" }
          { Keys = "F1/F2/F3"; Action = "速度" }
          { Keys = "F6"; Action = "保存" }
          { Keys = "F7"; Action = "读取" }
          { Keys = "ESC"; Action = "菜单或关闭" }
          { Keys = "UP/DOWN"; Action = "选择" }
          { Keys = "ENTER"; Action = "确认" } ]

    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 -> "1倍速"
        | TwoXSetting -> "2倍速"
        | FiveXSetting -> "5倍速"
        | PauseSetting -> "暂停"

    let itemLabel item : string =
        match item with
        | ContinueItem -> "继续游戏"
        | NewGameItem -> "开始新游戏"
        | LoadItem -> "读取存档"
        | SettingsItem -> "设置"
        | ControlsItem -> "操作说明"
        | ExitItem -> "退出"

    /// 开局四选一 + 「暂不选择」(None = 旧行为,无职业)。
    let occupationOptions : Occupation.Kind option list =
        [ Some Occupation.Farmer
          Some Occupation.Fisher
          Some Occupation.Peddler
          Some Occupation.Scholar
          None ]

    let occupationLabel (occupation: Occupation.Kind option) : string =
        match occupation with
        | Some kind -> Occupation.nameOf kind
        | None -> "暂不选择"

    let selectedOccupation (state: MenuState) : Occupation.Kind option =
        occupationOptions |> List.item state.Selected

    let private optionsFor (state: MenuState) : int =
        match state.Page with
        | MainMenu -> items state |> List.length
        | OccupationSelect -> occupationOptions.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 openOccupationSelect (state: MenuState) : MenuState =
        { state with
            Page = OccupationSelect
            Selected = occupationOptions.Length - 1
            ReturnPage = state.Page
            Error = None }

    let openControls (state: MenuState) : MenuState =
        { state with Page = Controls; Selected = 0; ReturnPage = state.Page; Error = None }

    /// P key: open the full key list as a pause-help page. The simulation is paused while
    /// the page is open; the caller keeps the previous control and restores it on exit.
    let enterPauseHelp (control: SimulationControl) (state: MenuState) : MenuState * SimulationControl =
        openControls state, { control with Paused = true }

    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

    /// Leaving the pause-help page returns to the page it was opened from and restores
    /// the exact simulation control the player had before.
    let exitPauseHelp (original: SimulationControl) (state: MenuState) : MenuState * SimulationControl =
        returnFromSubpage state, original

    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
            | OccupationSelect -> openMain state, NoCommand
            | Settings
            | Controls -> returnFromSubpage state, NoCommand
            | LoadError -> openMain state, NoCommand
            | Playing -> openMain state, NoCommand
        | Confirm ->
            match state.Page with
            | MainMenu -> confirmMain state
            | OccupationSelect -> state, StartNewGameWith (selectedOccupation state)
            | Settings ->
                let nextSettings = selectedControl state
                { state with Settings = nextSettings }, ApplySettings nextSettings
            | Controls -> state, NoCommand
            | LoadError -> openMain state, NoCommand
            | Playing -> state, NoCommand