-
Notifications
You must be signed in to change notification settings - Fork 1
/
Update.hs
477 lines (389 loc) · 13.4 KB
/
Update.hs
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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
module Update where
import Control.Monad.Trans.State
import qualified Data.HashMap.Strict as HM
import qualified Data.Set as Set
import Game hiding (Model)
import Game.Prelude
import Graphics.Gloss.Interface.IO.Game
(Event(..), Key(..), KeyState(..), MouseButton(..), SpecialKey(..))
import Layout
import Model
data Input
= UserEvent Event
| OpponentOrders Player Orders
| TimePassed Float
-- | __Player guide previous__: 'Base'
--
-- Use left click to select, right click to move. In detail, to:
--
-- === Select a base
--
-- 1. Left click it.
--
-- === Change what a friendly base is building
--
-- 1. Select it.
-- 2. Click one of the production options on the right (e.g. @Corvette@).
--
-- === Move a single ship
--
-- 1. Select the base where it is.
-- 2. Select the ship by left clicking it from the list on the right.
-- 3. Right click the destination base.
--
-- === Move all the friendly ships at a base
--
-- 1. Select the base.
-- 2. Right click the destination base.
-- (Note that this doesn't move @Station@s. They're so slow you'll usually
-- want to leave them behind).
--
-- This ends the essential section of the player guide. If you want to learn
-- the rest by experimentation you can start playing now. If not read on.
--
-- __Next__: 'Game.Update.Diplomacy.diplomacy'
update :: Input -> Model -> Model
update i m =
case i of
UserEvent event ->
execState (userInput event) m
TimePassed _ ->
tickOrTock m
OpponentOrders p o ->
execState checkForNewTurn (m & modelOpponentOrdersL %~ HM.insert p o)
--------------------------------------------------------------------------------
-- * Time passed
--------------------------------------------------------------------------------
tickOrTock :: Model -> Model
tickOrTock =
modelTickL %~ next
where
next :: Tick -> Tick
next = \case
Tick -> Tock
Tock -> Tick
--------------------------------------------------------------------------------
-- * checkForNewTurn
--
-- Called after we receive opponent orders or end our own turn.
--------------------------------------------------------------------------------
checkForNewTurn :: State Model ()
checkForNewTurn = do
b <- isTurnOver
when b updateTurnEnd
where
isTurnOver :: State Model Bool
isTurnOver = do
playerTurnEnded <- use modelTurnEndedL
opOrders <- use modelOpponentOrdersL
pure $ playerTurnEnded && HM.size opOrders > 0
updateTurnEnd :: State Model ()
updateTurnEnd = do
currentPlayer <- use modelWhoAmIL
allOrders <- do
playerOrders <- use modelOrdersL
opponentOrders <- use modelOpponentOrdersL
pure $ opponentOrders <> HM.singleton currentPlayer playerOrders
modelGameL %= Game.update allOrders
lastTurnLog <- use (modelGameL . modelLogL)
modelPopupL .= map (uncurry CombatLog) (hmToList (logCombat lastTurnLog))
modelOrdersL .= mempty
modelOpponentOrdersL .= mempty
resetPaginations
perhapsClearSelection
modelTurnEndedL .= False
resetPaginations :: State Model ()
resetPaginations =
modelPlaceScrollL .= mempty
perhapsClearSelection :: State Model ()
perhapsClearSelection = do
currentPlayer <- use modelWhoAmIL
ships <- use (modelGameL . modelShipsL)
selected <- use modelSelectionL
case selected of
SelectionNone ->
pure ()
SelectionPlace _ ->
pure ()
SelectionShip shipId -> do
let ship = getShip shipId ships
case shipLocation ship of
Destroyed ->
modelSelectionL .= SelectionNone
InFlight{} ->
when (shipPlayer ship /= currentPlayer) $
modelSelectionL .= SelectionNone
AtPlace _ ->
pure ()
--------------------------------------------------------------------------------
-- * Result check
--------------------------------------------------------------------------------
data UpdateResult
= Normal
| PlayerEndedTurn Orders
| Exit
updateResult :: Model -> Model -> UpdateResult
updateResult oldModel m
| modelExit m = Exit
| playerEndedTurn = PlayerEndedTurn (modelOrders oldModel)
| otherwise = Normal
where
playerEndedTurn :: Bool
playerEndedTurn =
(not (modelTurnEnded oldModel) && modelTurnEnded m)
|| turnRolledForward
turnRolledForward :: Bool
turnRolledForward =
let f = modelTurn . modelGame
in f oldModel /= f m
--------------------------------------------------------------------------------
-- * User input
--------------------------------------------------------------------------------
userInput :: Event -> State Model ()
userInput event =
case event of
EventKey (SpecialKey KeyEsc) Down _ _ ->
modelExitL .= True
_ -> do
model <- get
case modelPopup model of
[] ->
updateNormal event
popup:rest ->
updatePopup event popup rest
updatePopup :: Event -> CombatLog -> [CombatLog] -> State Model ()
updatePopup event _ rest = do
case event of
EventKey (SpecialKey KeyEnter) Down _ _ ->
modelPopupL .= rest
_ ->
pure ()
updateNormal :: Event -> State Model ()
updateNormal event = do
model <- get
let currentPlayer = modelWhoAmI model
places = modelPlaces (modelGame model)
ships = modelShips (modelGame model)
whenCanMove :: State Model () -> State Model ()
whenCanMove move =
case outcome (modelGame model) of
Victor _ ->
pure ()
AllDefeated ->
pure ()
Ongoing ->
when (not (modelTurnEnded model)) move
case event of
EventMotion (x,y) -> do
let
screenPoint :: ScreenPoint
screenPoint =
ScreenPoint x y
modelCursorDotL .= screenPoint
dragToPan <- use modelDragToPanL
case dragToPan of
NotDragging ->
pure ()
PossibleDragStart initial@(ScreenPoint initialX initialY) -> do
if distance (x,y) (initialX,initialY) < 10
then
pure ()
else do
modelDragToPanL .= Dragging initial
Dragging old -> do
BoardPoint oldX oldY <- runScreenToBoardPoint old
BoardPoint newX newY <- runScreenToBoardPoint screenPoint
let f :: BoardPoint -> BoardPoint
f (BoardPoint panX panY) =
BoardPoint (panX + (oldX - newX)) (panY + (oldY - newY))
modelPanL %= f
modelDragToPanL .= Dragging screenPoint
EventResize (width, height) ->
modelScreenSizeL .= Box (realToFrac width) (realToFrac height)
-- The meaning of latestMouseX, latestMouseY needs to be documented in gloss.
EventKey key upOrDown _ (latestMouseX, latestMouseY) -> do
let
screenPoint :: ScreenPoint
screenPoint =
ScreenPoint latestMouseX latestMouseY
case upOrDown of
Up ->
case key of
MouseButton LeftButton -> do
dragToPan <- use modelDragToPanL
case dragToPan of
-- We didn't move very much while we had the mouse down
-- so we weren't dragging at all! Instead we were
-- left clicking to clear the selection.
PossibleDragStart _ ->
modelSelectionL .= SelectionNone
NotDragging ->
pure ()
Dragging _ ->
pure ()
modelDragToPanL .= NotDragging
_ ->
pure ()
Down ->
case key of
Char 'c' ->
modelPanL .= BoardPoint 0 0
Char '=' -> do
currentZoom <- use modelZoomL
when (currentZoom /= maxZoom) (panTowardsCursor screenPoint)
modelZoomL %= zoomIn
Char '-' ->
modelZoomL %= zoomOut
Char _ ->
pure ()
SpecialKey KeySpace ->
whenCanMove $ do
modelTurnEndedL .= True
checkForNewTurn
SpecialKey _ ->
pure ()
MouseButton WheelUp -> do
currentZoom <- use modelZoomL
when (currentZoom /= maxZoom) (panTowardsCursor screenPoint)
modelZoomL %= zoomIn
MouseButton WheelDown ->
modelZoomL %= zoomOut
MouseButton button -> do
case interpretMouseMsg model screenPoint button of
BaseSelect placeId ->
modelSelectionL .= SelectionPlace placeId
SwitchBuilding placeId buildOrder ->
whenCanMove $ do
let place = getPlace placeId places
case placeType place of
Ruin ->
pure ()
PBase base ->
when (baseOwner base == PlayerOwner currentPlayer) $ do
if baseBuilding base == buildOrder
then
modelOrdersL . ordersBuildL %= HM.delete placeId
else
modelOrdersL . ordersBuildL %= HM.insert placeId buildOrder
ShipSelect shipId ->
modelSelectionL .= SelectionShip shipId
ShipsEmbark shipIds _ destId ->
whenCanMove $
for_ shipIds $ \shipId -> do
let ship = getShip shipId ships
when (shipPlayer ship == currentPlayer) $
modelOrdersL . ordersEmbarkL %= HM.insert shipId destId
PreviousPage placeId ->
modelPlaceScrollL %= HM.adjust (subtract 1) placeId
NextPage placeId ->
modelPlaceScrollL %= HM.insertWith (+) placeId 1
EmptySpace ->
modelDragToPanL .= PossibleDragStart screenPoint
NoOp ->
pure ()
-- | Do this softly so zooming in doesn't fling us away from the map.
--
-- This calculation isn't needed by View so no need to move it into Msg.
panTowardsCursor :: ScreenPoint -> State Model ()
panTowardsCursor screenPoint = do
uiPoint <- runScreenToUIPoint screenPoint
case uiPoint of
Left _ -> pure ()
Right selectedPoint ->
modelPanL %= panTowards selectedPoint
where
panTowards :: BoardPoint -> BoardPoint -> BoardPoint
panTowards selectedPoint panPoint@(BoardPoint px py) =
let
sel = fromBoardPoint selectedPoint
pan = fromBoardPoint panPoint
d = distance sel pan
speed = d / 3
(x,y) = deltas (angleBetweenPoints pan sel) speed
in BoardPoint (px + x) (py + y)
runScreenToUIPoint :: ScreenPoint -> State Model (Either HudPoint BoardPoint)
runScreenToUIPoint screenPoint = do
screenSize <- use modelScreenSizeL
pan <- use modelPanL
zoom <- use modelZoomL
pure (screenToUIPoint screenSize zoom pan screenPoint)
runScreenToBoardPoint :: ScreenPoint -> State Model BoardPoint
runScreenToBoardPoint screenPoint = do
pan <- use modelPanL
zoom <- use modelZoomL
pure (screenToBoardPoint zoom pan screenPoint)
data Msg
= BaseSelect PlaceId
| SwitchBuilding PlaceId BuildOrder
| ShipSelect ShipId
| ShipsEmbark (Set ShipId) PlaceId PlaceId -- ^ ships, departure, destination
| PreviousPage PlaceId
| NextPage PlaceId
| EmptySpace
| NoOp
interpretMouseMsg :: Model -> ScreenPoint -> MouseButton -> Msg
interpretMouseMsg m@Model{..} screenPoint button = do
case button of
LeftButton ->
handleLeftButton
RightButton ->
fromMaybe NoOp handleRightButton
_ ->
NoOp
where
mChosenItem :: Maybe Item
mChosenItem =
uiLayoutLookup m screenPoint
handleLeftButton :: Msg
handleLeftButton =
case mChosenItem of
Nothing ->
EmptySpace
Just chosenItem ->
case chosenItem of
HudItem item _ ->
case item of
ItemHudShip id _ ->
ShipSelect id
ItemBuildButton placeId buildOrder clickable _ ->
case clickable of
NotClickable ->
NoOp
Clickable ->
SwitchBuilding placeId buildOrder
ItemPreviousPage placeId _ ->
PreviousPage placeId
ItemNextPage placeId _ ->
NextPage placeId
HudItself mPlaceId _ _ ->
maybe NoOp BaseSelect mPlaceId
BoardItem item _ ->
case item of
ItemBase id _ ->
BaseSelect id
ItemShip id _ ->
ShipSelect id
handleRightButton :: Maybe Msg
handleRightButton = do
chosenItem <- mChosenItem
case chosenItem of
BoardItem (ItemBase chosenId _) _ -> do
case modelSelection of
SelectionNone ->
Nothing
SelectionPlace placeId ->
let ships = shipsAtPlace placeId (modelShips modelGame)
shipsLessStations = HM.filter (\s -> shipType s /= Station) ships
ids = Set.fromList (HM.keys shipsLessStations)
in Just (ShipsEmbark ids placeId chosenId)
SelectionShip shipId ->
let ship = getShip shipId (modelShips modelGame)
in case shipLocation ship of
InFlight{} ->
Nothing
Destroyed ->
Nothing
AtPlace departureId ->
Just (ShipsEmbark (Set.singleton shipId) departureId chosenId)
_ ->
Nothing