1 -- This file is part of Intricacy
2 -- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
4 -- This program is free software: you can redistribute it and/or modify
5 -- it under the terms of version 3 of the GNU General Public License as
6 -- published by the Free Software Foundation, or any later version.
8 -- You should have received a copy of the GNU General Public License
9 -- along with this program. If not, see http://www.gnu.org/licenses/.
12 {-# LANGUAGE LambdaCase #-}
13 {-# LANGUAGE TupleSections #-}
17 import Control
.Applicative
19 import Control
.Concurrent
.STM
20 import Control
.Monad
((<=<))
21 import Control
.Monad
.State
22 import Control
.Monad
.Trans
.Maybe
23 import Control
.Monad
.Trans
.Reader
25 import Data
.Function
(on
)
28 import qualified Data
.Map
as Map
31 import Data
.Time
.Clock
(getCurrentTime
)
33 import Graphics
.UI
.SDL
hiding (flip)
34 import qualified Graphics
.UI
.SDL
as SDL
35 import qualified Graphics
.UI
.SDL
.TTF
as TTF
36 import System
.FilePath
37 --import Debug.Trace (traceShow)
40 import Graphics
.UI
.SDL
.Mixer
41 import System
.Random
(randomRIO)
60 data UIState
= UIState
63 , gsSurface
:: Maybe Surface
64 , bgSurface
:: Maybe Surface
65 , cachedGlyphs
:: CachedGlyphs
66 , lastDrawArgs
:: Maybe DrawArgs
67 , miniLocks
:: Map Lock Surface
68 , registeredSelectables
:: Map HexVec Selectable
69 , contextButtons
:: [ButtonGroup
]
70 , uiOptions
:: UIOptions
71 , settingBinding
:: Maybe Command
72 , uiKeyBindings
:: Map InputMode KeyBindings
73 , dispFont
:: Maybe TTF
.Font
74 , dispFontSmall
:: Maybe TTF
.Font
75 , lastFrameTicks
:: Word32
76 , paintTileIndex
:: Int
77 , leftButtonDown
:: Maybe HexVec
78 , middleButtonDown
:: Maybe HexVec
79 , rightButtonDown
:: Maybe HexVec
80 , mousePos
:: (HexVec
,Bool)
81 , message
:: Maybe (Pixel
, String)
82 , hoverStr
:: Maybe String
83 , dispCentre
:: HexPos
84 , dispLastCol
:: PieceColouring
86 , nextAnimFrameAt
:: Maybe Word32
89 , sounds
:: Map
String [Chunk
]
92 deriving (Eq
, Ord
, Show)
93 type UIM
= StateT UIState
IO
94 nullUIState
= UIState
0 0 Nothing Nothing emptyCachedGlyphs Nothing Map
.empty Map
.empty []
95 defaultUIOptions Nothing Map
.empty Nothing Nothing
0 0 Nothing Nothing Nothing
96 (zero
,False) Nothing Nothing
(PHS zero
) Map
.empty 0 Nothing
50
101 data UIOptions
= UIOptions
102 { useFiveColouring
:: Bool
103 , showBlocks
:: ShowBlocks
104 , whsButtons
:: Maybe WrHoSel
105 , useBackground
:: Bool
107 , showButtonText
:: Bool
109 , uiAnimTime
:: Word32
110 , shortUiAnimTime
:: Word32
112 deriving (Eq
, Ord
, Show, Read)
113 defaultUIOptions
= UIOptions
False ShowBlocksBlocking Nothing
True False True True 100 20
115 modifyUIOptions
:: (UIOptions
-> UIOptions
) -> UIM
()
116 modifyUIOptions f
= modify
$ \s
-> s
{ uiOptions
= f
$ uiOptions s
}
118 renderToMain
:: RenderM a
-> UIM a
120 surf
<- liftIO getVideoSurface
121 renderToMainWithSurf surf m
122 renderToMainWithSurf
:: Surface
-> RenderM a
-> UIM a
123 renderToMainWithSurf surf m
= do
124 (scrCentre
, size
) <- getGeom
125 centre
<- gets dispCentre
126 mfont
<- gets dispFont
127 bgsurf
<- gets bgSurface
128 cgs
<- gets cachedGlyphs
129 (a
,cgs
') <- liftIO
$ runRenderM m cgs
$ RenderContext surf bgsurf centre scrCentre zero size mfont
130 modify
$ \s
-> s
{ cachedGlyphs
= cgs
' }
135 surface
<- liftIO getVideoSurface
136 liftIO
$ SDL
.flip surface
140 last <- gets lastFrameTicks
141 let next = last + 1000 `
div`
30
142 now
<- liftIO getTicks
143 -- liftIO $ print now
145 liftIO
$ delay
(next - now
)
146 modify
$ \ds
-> ds
{ lastFrameTicks
= now
}
149 data Button
= Button
{ buttonPos
:: HexVec
, buttonCmd
:: Command
, buttonHelp
:: [ButtonHelp
] }
150 deriving (Eq
, Ord
, Show)
151 type ButtonGroup
= ([Button
],(Int,Int))
152 type ButtonHelp
= (String, HexVec
)
153 singleButton
:: HexVec
-> Command
-> Int -> [ButtonHelp
] -> ButtonGroup
154 singleButton pos cmd col helps
= ([Button pos cmd helps
], (col
,0))
155 getButtons
:: InputMode
-> UIM
[ ButtonGroup
]
157 mwhs
<- gets
$ whsButtons
. uiOptions
158 cntxtButtons
<- gets contextButtons
159 return $ cntxtButtons
++ global
++ case mode
of
161 singleButton
(tl
+^hv
+^neg hw
) CmdTest
1 [("test", hu
+^neg hw
)]
162 , singleButton
(tl
+^neg hw
) CmdPlay
2 [("play", hu
+^neg hw
)]
164 , singleButton
(br
+^
2*^hu
) CmdWriteState
2 [("save", hu
+^neg hw
)] ]
166 ++ [ ([Button
(paintButtonStart
+^ hu
+^ i
*^hv
) (paintTileCmds
!!i
) []
167 | i
<- take (length paintTiles
) [0..] ],(5,0)) ]
168 IMPlay
-> whsBGs mwhs mode
169 IMReplay
-> [ markButtonGroup
]
171 [ singleButton serverPos CmdSetServer
0 [("server",7*^neg hu
)]
172 , singleButton
(serverPos
+^hw
) CmdToggleCacheOnly
0 [("offline",hv
+^
7*^neg hu
),("mode",hw
+^
5*^neg hu
)]
173 , singleButton
(codenamePos
+^
2*^neg hu
) (CmdSelCodename Nothing
) 2 [("code",hv
+^
5*^neg hu
),("name",hw
+^
5*^neg hu
)]
174 , singleButton
(serverPos
+^
2*^neg hv
+^
2*^hw
) CmdInitiation
3 [("initi",hu
+^neg hw
),("ation",hu
+^neg hv
)]
179 global
= if mode `
elem`
[IMTextInput
,IMImpatience
] then [] else
180 [ singleButton br CmdQuit
0 [("quit",hu
+^neg hw
)]
181 , singleButton
(tr
+^
3*^hv
+^
3*^hu
) CmdHelp
3 [("help",hu
+^neg hw
)] ]
182 whsBGs
:: Maybe WrHoSel
-> InputMode
-> [ ButtonGroup
]
183 whsBGs Nothing _
= []
184 whsBGs
(Just whs
) mode
=
185 let edit
= mode
== IMEdit
186 in [ ( [ Button bl
(if edit
then CmdSelect
else CmdWait
) [] ], (0,0))
187 , ( [ Button
(bl
+^dir
) (CmdDir whs dir
)
188 (if dir
==hu
then [("move",hu
+^neg hw
),(if edit
then "piece" else whsStr whs
,hu
+^neg hv
)] else [])
189 | dir
<- hexDirs
], (5,0) )
191 ([( [ Button
(bl
+^
((-2)*^hv
))
193 [("turn",hu
+^neg hw
),("cw",hu
+^neg hv
)]
194 , Button
(bl
+^
((-2)*^hw
))
196 [("turn",hu
+^neg hw
),("ccw",hu
+^neg hv
)]
197 ], (5,0) ) | whs
/= WHSWrench
]) ++
198 ([( [ Button
(bl
+^
(2*^hv
)+^hw
+^neg hu
) (CmdTile HookTile
) [("select",hu
+^neg hw
),("hook",hu
+^neg hv
)]
199 , Button
(bl
+^
(2*^hv
)+^neg hu
) (CmdTile
$ WrenchTile zero
) [("select",hu
+^neg hw
),("wrench",hu
+^neg hv
)]
200 ], (2,0) ) | whs
== WHSSelected
&& mode
/= IMEdit
])
206 markButtonGroup
= ([Button
(tl
+^hw
) CmdMark
[("set",hu
+^neg hw
),("mark",hu
+^neg hv
)]
207 , Button
(tl
+^hw
+^hv
) CmdJumpMark
[("jump",hu
+^neg hw
),("mark",hu
+^neg hv
)]
208 , Button
(tl
+^hw
+^
2*^hv
) CmdReset
[("jump",hu
+^neg hw
),("start",hu
+^neg hv
)]],(0,1))
212 data AccessedInfo
= AccessedSolved | AccessedPublic | AccessedReadNotes | AccessedUndeclared
213 deriving (Eq
, Ord
, Show)
214 data Selectable
= SelOurLock
218 | SelLockUnset
Bool ActiveLock
219 | SelSelectedCodeName Codename
221 | SelRelScoreComponent
222 | SelScoreLock
(Maybe Codename
) (Maybe AccessedReason
) ActiveLock
223 | SelUndeclared Undeclared
224 | SelReadNote NoteInfo
226 | SelSolution NoteInfo
227 | SelAccessed Codename
229 | SelSecured NoteInfo
230 | SelOldLock LockSpec
232 | SelAccessedInfo AccessedInfo
238 deriving (Eq
, Ord
, Show)
240 registerSelectable
:: HexVec
-> Int -> Selectable
-> UIM
()
241 registerSelectable v r s
=
242 modify
$ \ds
-> ds
{registeredSelectables
= foldr (
243 (`Map
.insert` s
) . (v
+^
)) (registeredSelectables ds
) (hexDisc r
)}
244 registerButtonGroup
:: ButtonGroup
-> UIM
()
245 registerButtonGroup g
= modify
$ \ds
-> ds
{contextButtons
= g
:contextButtons ds
}
246 registerButton
:: HexVec
-> Command
-> Int -> [ButtonHelp
] -> UIM
()
247 registerButton pos cmd col helps
= registerButtonGroup
$ singleButton pos cmd col helps
248 clearSelectables
,clearButtons
:: UIM
()
249 clearSelectables
= modify
$ \ds
-> ds
{registeredSelectables
= Map
.empty}
250 clearButtons
= modify
$ \ds
-> ds
{contextButtons
= []}
252 registerUndoButtons
:: Bool -> Bool -> UIM
()
253 registerUndoButtons noUndo noRedo
= do
254 unless noUndo
$ registerButton
(periphery
2+^hu
) CmdUndo
0 [("undo",hu
+^neg hw
)]
255 unless noRedo
$ registerButton
(periphery
2+^hu
+^neg hv
) CmdRedo
2 [("redo",hu
+^neg hw
)]
257 commandOfSelectable IMInit SelTut _
= Just
. CmdSolveInit
$ Just zero
258 commandOfSelectable IMInit
(SelInitLock v
) _
= Just
. CmdSolveInit
$ Just v
259 commandOfSelectable IMMeta SelOurLock _
= Just CmdEdit
260 commandOfSelectable IMMeta
(SelLock
(ActiveLock _ i
)) False = Just
$ CmdSolve
(Just i
)
261 commandOfSelectable IMMeta
(SelLock
(ActiveLock _ i
)) True = Just
$ CmdPlaceLock
(Just i
)
262 commandOfSelectable IMMeta
(SelScoreLock Nothing _
(ActiveLock _ i
)) False = Just
$ CmdSolve
(Just i
)
263 commandOfSelectable IMMeta
(SelScoreLock Nothing _
(ActiveLock _ i
)) True = Just
$ CmdPlaceLock
(Just i
)
264 commandOfSelectable IMMeta
(SelScoreLock
(Just _
) _ _
) _
= Just CmdHome
265 commandOfSelectable IMMeta
(SelLockUnset
True (ActiveLock _ i
)) _
= Just
$ CmdPlaceLock
(Just i
)
266 commandOfSelectable IMMeta
(SelSelectedCodeName _
) False = Just
$ CmdSelCodename Nothing
267 commandOfSelectable IMMeta
(SelSelectedCodeName _
) True = Just CmdHome
268 commandOfSelectable IMMeta
(SelUndeclared undecl
) _
= Just
$ CmdDeclare
$ Just undecl
269 commandOfSelectable IMMeta
(SelReadNote note
) False = Just
$ CmdSelCodename
$ Just
$ noteAuthor note
270 commandOfSelectable IMMeta
(SelReadNote note
) True = Just
$ CmdViewSolution
$ Just note
271 commandOfSelectable IMMeta
(SelSolution note
) False = Just
$ CmdSelCodename
$ Just
$ noteAuthor note
272 commandOfSelectable IMMeta
(SelSolution note
) True = Just
$ CmdViewSolution
$ Just note
273 commandOfSelectable IMMeta
(SelAccessed name
) _
= Just
$ CmdSelCodename
$ Just name
274 commandOfSelectable IMMeta
(SelRandom name
) _
= Just
$ CmdSelCodename
$ Just name
275 commandOfSelectable IMMeta
(SelSecured note
) False = Just
$ CmdSelCodename
$ Just
$ lockOwner
$ noteOn note
276 commandOfSelectable IMMeta
(SelSecured note
) True = Just
$ CmdViewSolution
$ Just note
277 commandOfSelectable IMMeta
(SelOldLock ls
) _
= Just
$ CmdPlayLockSpec
$ Just ls
278 commandOfSelectable IMMeta SelLockPath _
= Just CmdSelectLock
279 commandOfSelectable IMTextInput
(SelLock
(ActiveLock _ i
)) _
= Just
$ CmdInputSelLock i
280 commandOfSelectable IMTextInput
(SelScoreLock _ _
(ActiveLock _ i
)) _
= Just
$ CmdInputSelLock i
281 commandOfSelectable IMTextInput
(SelLockUnset _
(ActiveLock _ i
)) _
= Just
$ CmdInputSelLock i
282 commandOfSelectable IMTextInput
(SelReadNote note
) _
= Just
$ CmdInputCodename
$ noteAuthor note
283 commandOfSelectable IMTextInput
(SelSolution note
) _
= Just
$ CmdInputCodename
$ noteAuthor note
284 commandOfSelectable IMTextInput
(SelSecured note
) _
= Just
$ CmdInputCodename
$ lockOwner
$ noteOn note
285 commandOfSelectable IMTextInput
(SelRandom name
) _
= Just
$ CmdInputCodename name
286 commandOfSelectable IMTextInput
(SelUndeclared undecl
) _
= Just
$ CmdInputSelUndecl undecl
287 commandOfSelectable _ _ _
= Nothing
289 helpOfSelectable SelTut
= Just
"Enter tutorials"
290 helpOfSelectable
(SelInitLock _
) = Just
"Solve lock"
291 helpOfSelectable SelOurLock
= Just
293 helpOfSelectable
(SelSelectedCodeName name
) = Just
$
294 "Currently viewing "++name
++"."
295 helpOfSelectable SelRelScore
= Just
296 "The extent to which you are held in higher esteem than this fellow guild member."
297 helpOfSelectable SelRelScoreComponent
= Just
298 "Contribution to total relative esteem."
299 helpOfSelectable
(SelLock
(ActiveLock name i
)) = Just
$
300 name
++"'s lock "++[lockIndexChar i
]++"."
301 helpOfSelectable
(SelLockUnset
True _
) = Just
303 helpOfSelectable
(SelLockUnset
False _
) = Just
304 "An empty lock slot."
305 helpOfSelectable
(SelUndeclared _
) = Just
306 "Declare yourself able to unlock a lock by securing a note on it behind a lock of your own."
307 helpOfSelectable
(SelRandom _
) = Just
308 "Random set of guild members. Colours show relative esteem, bright red (-3) to bright green (+3)."
309 helpOfSelectable
(SelScoreLock
(Just name
) Nothing _
) = Just
$
310 "Your lock, which "++name
++" can not unlock."
311 helpOfSelectable
(SelScoreLock
(Just name
) (Just AccessedPrivyRead
) _
) = Just
$
312 "Your lock, on which "++name
++" has read three notes: -1 to relative esteem."
313 helpOfSelectable
(SelScoreLock
(Just name
) (Just
(AccessedPrivySolved
False)) _
) = Just
$
314 "Your lock, on which "++name
++" has declared a note which you have not read: -1 to relative esteem."
315 helpOfSelectable
(SelScoreLock
(Just name
) (Just
(AccessedPrivySolved
True)) _
) = Just
$
316 "Your lock, on which "++name
++" has declared a note which you have, however, read."
317 helpOfSelectable
(SelScoreLock
(Just name
) (Just AccessedPub
) _
) = Just
318 "Your lock, the secrets of which have been publically revealed: -1 to relative esteem."
319 helpOfSelectable
(SelScoreLock
(Just name
) (Just AccessedEmpty
) _
) = Just
$
320 "Your empty lock slot; "++name
++" can unlock all your locks: -1 to relative esteem."
321 helpOfSelectable
(SelScoreLock Nothing Nothing
(ActiveLock name _
)) = Just
$
322 name
++"'s lock, which you can not unlock."
323 helpOfSelectable
(SelScoreLock Nothing
(Just AccessedPrivyRead
) (ActiveLock name _
)) = Just
$
324 name
++"'s lock, on which you have read three notes: +1 to relative esteem."
325 helpOfSelectable
(SelScoreLock Nothing
(Just
(AccessedPrivySolved
False)) (ActiveLock name _
)) = Just
$
326 name
++"'s lock, on which you have declared a note which "++name
++" has not read: +1 to relative esteem."
327 helpOfSelectable
(SelScoreLock Nothing
(Just
(AccessedPrivySolved
True)) (ActiveLock name _
)) = Just
$
328 name
++"'s lock, on which you have declared a note which "++name
++" has, however, read."
329 helpOfSelectable
(SelScoreLock Nothing
(Just AccessedPub
) (ActiveLock name _
)) = Just
$
330 name
++"'s lock, the secrets of which have been publically revealed: +1 to relative esteem."
331 helpOfSelectable
(SelScoreLock Nothing
(Just AccessedEmpty
) (ActiveLock name _
)) = Just
$
332 name
++"'s empty lock slot; you can unlock all "++name
++"'s locks: +1 to relative esteem."
333 helpOfSelectable
(SelReadNote note
) = Just
$
334 "You have read "++noteAuthor note
++"'s note on this lock."
335 helpOfSelectable SelReadNoteSlot
= Just
336 "Reading three notes on this lock would suffice to reveal its secrets."
337 helpOfSelectable
(SelSecured note
) = let ActiveLock owner idx
= noteOn note
in
338 Just
$ "Secured note on "++owner
++"'s lock "++[lockIndexChar idx
]++"."
339 helpOfSelectable
(SelSolution note
) = Just
$ case noteBehind note
of
340 Just
(ActiveLock owner idx
) -> owner
++
341 " has secured their note on this lock behind their lock " ++ [lockIndexChar idx
] ++ "."
342 Nothing
-> noteAuthor note
++ "'s note on this lock is public knowledge."
343 helpOfSelectable
(SelAccessed name
) = Just
$
344 name
++ " did not pick this lock, but learnt how to unlock it by reading three notes on it."
345 helpOfSelectable SelPublicLock
= Just
346 "Notes behind retired or public locks are public; locks with three public notes are public."
347 helpOfSelectable
(SelAccessedInfo meth
) = Just
$ case meth
of
348 AccessedSolved
-> "You picked this lock and declared your solution, so may read any notes it secures."
349 AccessedPublic
-> "The secrets of this lock have been publically revealed."
350 AccessedUndeclared
-> "You have picked this lock, but are yet to declare your solution."
352 "Having read three notes on others' solutions to this lock, you have unravelled its secrets."
353 helpOfSelectable
(SelOldLock ls
) = Just
$
354 "Retired lock, #"++show ls
++". Any notes which were secured by the lock are now public knowledge."
355 helpOfSelectable SelLockPath
= Just
356 "Select a lock by its name. The names you give your locks are not revealed to others."
357 helpOfSelectable SelPrivyHeader
= Just
358 "Fellow guild members able to unlock this lock, hence able to read its secured notes."
359 helpOfSelectable SelNotesHeader
= Just
360 "Secured notes. Notes are obfuscated sketches of method, proving success but revealing little."
361 helpOfSelectable SelToolWrench
= Just
"The wrench, one of your lockpicking tools. Click and drag to move."
362 helpOfSelectable SelToolHook
= Just
"The hook, one of your lockpicking tools. Click and drag to move, use mousewheel to turn."
364 cmdAtMousePos pos
@(mPos
,central
) im selMode
= do
365 buttons
<- concatMap fst <$> getButtons im
366 sels
<- gets registeredSelectables
367 return $ listToMaybe $
369 | button
<- buttons
, mPos
== buttonPos button
, central
]
370 ++ maybe [] (\isRight
->
372 | Just sel
<- [Map
.lookup mPos sels
]
373 , Just cmd
<- [ commandOfSelectable im sel isRight
] ])
376 helpAtMousePos
:: (HexVec
, Bool) -> InputMode
-> UIM
(Maybe [Char])
377 helpAtMousePos
(mPos
,_
) _
=
378 gets
$ (helpOfSelectable
<=< Map
.lookup mPos
) . registeredSelectables
381 data UIOptButton a
= UIOptButton
{ getUIOpt
:: UIOptions
->a
, setUIOpt
:: a
->UIOptions
->UIOptions
,
382 uiOptVals
:: [a
], uiOptPos
:: HexVec
, uiOptGlyph
:: a
->Glyph
, uiOptDescr
:: a
->String,
383 uiOptModes
:: [InputMode
], onSet
:: Maybe (a
-> UIM
()) }
385 -- non-uniform type, so can't use a list...
386 uiOB1
= UIOptButton useFiveColouring
(\v o
-> o
{useFiveColouring
=v
}) [True,False]
387 (periphery
0 +^
3 *^ hu
+^ neg hv
) UseFiveColourButton
388 (\v -> if v
then "Adjacent pieces get different colours" else
389 "Pieces are coloured according to type")
390 [IMPlay
, IMReplay
, IMEdit
] Nothing
391 uiOB2
= UIOptButton showBlocks
(\v o
-> o
{showBlocks
=v
}) [ShowBlocksBlocking
,ShowBlocksAll
,ShowBlocksNone
]
392 (periphery
0 +^
2 *^ hu
+^
2 *^ neg hv
) ShowBlocksButton
394 ShowBlocksBlocking
-> "Blocking forces are annotated"
395 ShowBlocksAll
-> "Blocked and blocking forces are annotated"
396 ShowBlocksNone
-> "Blockage annotations disabled")
397 [IMPlay
, IMReplay
] Nothing
398 uiOB3
= UIOptButton whsButtons
(\v o
-> o
{whsButtons
=v
}) [Nothing
, Just WHSSelected
, Just WHSWrench
, Just WHSHook
]
399 (periphery
3 +^
3 *^ hv
) WhsButtonsButton
401 Nothing
-> "Click to show (and rebind) keyboard control buttons."
402 Just whs
-> "Showing buttons for controlling " ++ case whs
of
403 WHSSelected
-> "selected piece; right-click to rebind"
404 WHSWrench
-> "wrench; right-click to rebind"
405 WHSHook
-> "hook; right-click to rebind")
406 [IMPlay
, IMEdit
] Nothing
407 uiOB4
= UIOptButton showButtonText
(\v o
-> o
{showButtonText
=v
}) [True,False]
408 (periphery
0 +^
2 *^ hu
+^
3 *^ hv
) ShowButtonTextButton
409 (\v -> if v
then "Help text enabled" else
410 "Help text disabled")
411 [IMPlay
, IMEdit
, IMReplay
, IMMeta
, IMInit
] Nothing
412 uiOB5
= UIOptButton fullscreen
(\v o
-> o
{fullscreen
=v
}) [True,False]
413 (periphery
0 +^
4 *^ hu
+^
2 *^ hv
) FullscreenButton
414 (\v -> if v
then "Currently in fullscreen mode; click to toggle" else
415 "Currently in windowed mode; click to toggle")
416 [IMPlay
, IMEdit
, IMReplay
, IMMeta
, IMInit
] (Just
$ const $ initVideo
0 0)
417 uiOB6
= UIOptButton useSounds
(\v o
-> o
{useSounds
=v
}) [True,False]
418 (periphery
0 +^
4 *^ hu
+^ hv
) UseSoundsButton
419 (\v -> if v
then "Sound effects enabled" else
420 "Sound effects disabled")
421 [IMPlay
, IMEdit
, IMReplay
] Nothing
423 drawUIOptionButtons
:: InputMode
-> UIM
()
424 drawUIOptionButtons mode
= do
425 drawUIOptionButton mode uiOB1
426 drawUIOptionButton mode uiOB2
427 drawUIOptionButton mode uiOB3
428 drawUIOptionButton mode uiOB4
429 drawUIOptionButton mode uiOB5
431 drawUIOptionButton mode uiOB6
433 drawUIOptionButton im b
= when (im `
elem` uiOptModes b
) $ do
434 value <- gets
$ getUIOpt b
. uiOptions
435 renderToMain
$ mapM_ (\g
-> drawAtRel g
(uiOptPos b
))
436 [HollowGlyph
$ obscure purple
, uiOptGlyph b
value]
437 describeUIOptionButton
:: UIOptButton a
-> MaybeT UIM
String
438 describeUIOptionButton b
= do
439 value <- gets
$ getUIOpt b
. uiOptions
440 return $ uiOptDescr b
value
441 -- XXX: hand-hacking lenses...
442 toggleUIOption
(UIOptButton getopt setopt vals _ _ _ _ monSet
) = do
443 value <- gets
$ getopt
. uiOptions
444 let value' = cycle vals
!! max 0 (1 + fromMaybe 0 (elemIndex value vals
))
445 modifyUIOptions
$ setopt
value'
448 Just onSet
-> onSet
value'
450 readUIConfigFile
:: UIM
()
451 readUIConfigFile
= do
452 path
<- liftIO
$ confFilePath
"SDLUI.conf"
453 mOpts
<- liftIO
$ readReadFile path
455 Just opts
-> modify
$ \s
-> s
{uiOptions
= opts
}
457 writeUIConfigFile
:: UIM
()
458 writeUIConfigFile
= do
459 path
<- liftIO
$ confFilePath
"SDLUI.conf"
460 opts
<- gets uiOptions
462 liftIO
$ writeFile path
$ show opts
464 readBindings
:: UIM
()
466 path
<- liftIO
$ confFilePath
"bindings"
467 mbdgs
<- liftIO
$ readReadFile path
469 Just bdgs
-> modify
$ \s
-> s
{uiKeyBindings
= bdgs
}
471 writeBindings
:: UIM
()
473 path
<- liftIO
$ confFilePath
"bindings"
474 bdgs
<- gets uiKeyBindings
476 liftIO
$ writeFile path
$ show bdgs
478 getBindings
:: InputMode
-> UIM
[(Char, Command
)]
479 getBindings mode
= do
480 uibdgs
<- gets
(Map
.findWithDefault
[] mode
. uiKeyBindings
)
481 return $ uibdgs
++ bindings mode
483 paintTiles
:: [ Maybe Tile
]
486 , Just
$ ArmTile zero
False
487 , Just
$ PivotTile zero
488 , Just
$ SpringTile Relaxed zero
489 , Just
$ BlockTile
[]
493 paintTileCmds
= map (maybe CmdDelete CmdTile
) paintTiles
495 getEffPaintTileIndex
:: UIM
Int
496 getEffPaintTileIndex
= do
497 mods
<- liftIO getModState
498 if any (`
elem` mods
) [KeyModLeftCtrl
, KeyModRightCtrl
]
499 then return $ length paintTiles
- 1
500 else gets paintTileIndex
502 paintButtonStart
:: HexVec
503 paintButtonStart
= periphery
0 +^
(- length paintTiles `
div`
2)*^hv
505 drawPaintButtons
:: UIM
()
506 drawPaintButtons
= do
507 pti
<- getEffPaintTileIndex
508 renderToMain
$ sequence_ [
510 let gl
= case paintTiles
!!i
of
511 Nothing
-> HollowInnerGlyph
$ dim purple
512 Just t
-> TileGlyph t
$ dim purple
514 when selected
$ drawAtRel cursorGlyph pos
515 | i
<- take (length paintTiles
) [0..]
516 , let pos
= paintButtonStart
+^ i
*^hv
517 , let selected
= i
== pti
520 periphery
0 = ((3*maxlocksize
)`
div`
2)*^hu
+^
((3*maxlocksize
)`
div`
4)*^hv
521 periphery n
= rotate n
$ periphery
0
522 -- ^ XXX only peripheries 0,2,3,5 are guaranteed to be on-screen!
523 --messageLineStart = (maxlocksize+1)*^hw
524 messageLineCentre
= ((maxlocksize
+1)`
div`
2)*^hw
+^
((maxlocksize
+1+1)`
div`
2)*^neg hv
525 titlePos
= (maxlocksize
+1)*^hv
+^
((maxlocksize
+1)`
div`
2)*^hu
527 screenWidthHexes
,screenHeightHexes
:: Int
528 screenWidthHexes
= 32
529 screenHeightHexes
= 26
530 getGeom
:: UIM
(SVec
, Int)
534 let scrCentre
= SVec
(w`
div`
2) (h`
div`
2)
535 -- |size is the greatest integer such that
536 -- and [2*size*screenWidthHexes < width
537 -- , 3*ysize size*screenHeightHexes < height]
538 -- where ysize size = round $ fi size / sqrt 3
539 -- Minimum allowed size is 2 (get segfaults on SDL_FreeSurface with 1).
540 let size
= max 2 $ minimum [ (w
-1)`
div`
(2*screenWidthHexes
)
541 , floor $ sqrt 3 * (0.5 + fi
((h
-1)`
div`
(3*screenHeightHexes
)))]
542 return (scrCentre
, size
)
544 data DrawArgs
= DrawArgs
[PieceIdx
] Bool [Alert
] GameState UIOptions
545 deriving (Eq
, Ord
, Show)
547 drawMainGameState
:: [PieceIdx
] -> Bool -> [Alert
] -> GameState
-> UIM
()
548 drawMainGameState highlight colourFixed alerts st
= do
549 uiopts
<- gets uiOptions
550 drawMainGameState
' $ DrawArgs highlight colourFixed alerts st uiopts
552 drawMainGameState
' :: DrawArgs
-> UIM
()
553 drawMainGameState
' args
@(DrawArgs highlight colourFixed alerts st uiopts
) = do
554 lastArgs
<- gets lastDrawArgs
555 when (case lastArgs
of
557 Just
(DrawArgs _ _ lastAlerts lastSt _
) ->
558 lastAlerts
/= alerts || lastSt
/= st
) $
559 modify
$ \ds
-> ds
{ animFrame
= 0, nextAnimFrameAt
= Nothing
}
561 lastAnimFrame
<- gets animFrame
562 now
<- liftIO getTicks
563 anim
<- gets
(maybe False (<now
) . nextAnimFrameAt
)
565 modify
$ \ds
-> ds
{ animFrame
= lastAnimFrame
+1, nextAnimFrameAt
= Nothing
}
566 animFrameToDraw
<- gets animFrame
567 void
$ if lastArgs
== Just args
&& lastAnimFrame
== animFrameToDraw
569 vidSurf
<- liftIO getVideoSurface
570 gsSurf
<- gets
(fromJust . gsSurface
)
571 liftIO
$ blitSurface gsSurf Nothing vidSurf Nothing
573 modify
$ \ds
-> ds
{ lastDrawArgs
= Just args
}
575 -- split the alerts at intermediate states, and associate alerts
576 -- to the right states:
577 let (globalAlerts
,transitoryAlerts
) = partition isGlobalAlert alerts
578 splitAlerts frameAs
(AlertIntermediateState st
' : as) =
579 (frameAs
,st
',True) : splitAlerts
[] as
580 splitAlerts frameAs
(a
:as) =
581 splitAlerts
(a
:frameAs
) as
582 splitAlerts frameAs
[] = [(frameAs
,st
,False)]
583 isGlobalAlert
(AlertAppliedForce _
) = False
584 isGlobalAlert
(AlertIntermediateState _
) = False
585 isGlobalAlert _
= True
586 let animAlertedStates
= nub $
587 let ass
= splitAlerts
[] transitoryAlerts
588 in if last ass
== ([],st
,False) then ass
else ass
++ [([],st
,False)]
589 let frames
= length animAlertedStates
590 let (drawAlerts
',drawSt
,isIntermediate
) = animAlertedStates
!! animFrameToDraw
591 let drawAlerts
= drawAlerts
' ++ globalAlerts
592 -- let drawAlerts = takeWhile (/= AlertIntermediateState drawSt) alerts
593 nextIsSet
<- gets
(isJust . nextAnimFrameAt
)
594 when (not nextIsSet
&& frames
> animFrameToDraw
+1) $ do
595 time
<- gets
((if isIntermediate
then uiAnimTime
else shortUiAnimTime
) . uiOptions
)
596 modify
$ \ds
-> ds
{ nextAnimFrameAt
= Just
$ now
+ time
}
598 let board
= stateBoard drawSt
599 lastCol
<- gets dispLastCol
600 let coloured
= colouredPieces colourFixed drawSt
601 let colouring
= if useFiveColouring uiopts
602 then boardColouring drawSt coloured lastCol
603 else pieceTypeColouring drawSt coloured
604 modify
$ \ds
-> ds
{ dispLastCol
= colouring
}
605 gsSurf
<- gets
(fromJust . gsSurface
)
606 renderToMainWithSurf gsSurf
$ do
607 let tileGlyphs
= ownedTileGlyph colouring highlight
<$> board
609 applyAlert
(AlertAppliedForce f
@(Torque idx tdir
)) =
610 let poss
= case getpp drawSt idx
of
611 PlacedPiece pos
(Pivot arms
) -> pos
: map (+^pos
) arms
612 PlacedPiece pos
(Hook arm _
) -> [arm
+^pos
]
614 rotateGlyph
(TileGlyph
(ArmTile dir _
) col
) =
615 ArmGlyph
(-tdir
) dir col
616 rotateGlyph
(TileGlyph
(PivotTile dir
) col
) =
617 PivotGlyph
(-tdir
) dir col
618 in flip (foldr . Map
.adjust
$ rotateGlyph
) poss
619 applyAlert
(AlertAppliedForce f
@(Push idx dir
)) =
620 displaceFootprint
. displaceSprings
622 displace
= DisplacedGlyph
$ neg dir
623 displaceSpringGlyph isRoot
(TileGlyph
(SpringTile extn sdir
) col
) =
624 displaceSpringGlyph isRoot
$ SpringGlyph zero zero extn sdir col
625 displaceSpringGlyph isRoot
(SpringGlyph rdisp edisp extn sdir col
)
626 | isRoot
= SpringGlyph
(neg dir
) edisp extn sdir col
627 |
otherwise = SpringGlyph rdisp
(neg dir
) extn sdir col
628 displaceSpringGlyph _ glyph
= glyph
630 flip (foldr . Map
.adjust
$ displace
) $
631 plPieceFootprint
$ getpp drawSt idx
633 displaceSpring isRoot c
@(Connection root end
(Spring sdir _
))
634 | dir `
elem`
[sdir
,neg sdir
] =
635 Map
.adjust
(displaceSpringGlyph isRoot
) $
637 then sdir
+^ locusPos drawSt root
638 else neg sdir
+^ locusPos drawSt end
640 flip (foldr . Map
.adjust
$ displace
) $
641 connectionFootPrint drawSt c
643 displaceSpring _ _
= id
646 flip (foldr $ displaceSpring
True) (springsRootAtIdx drawSt idx
) .
647 flip (foldr $ displaceSpring
False) (springsEndAtIdx drawSt idx
)
650 applyAlerts
= flip (foldr applyAlert
) drawAlerts
653 sequence_ [ drawAt glyph pos |
654 (pos
,glyph
) <- Map
.toList
$ applyAlerts tileGlyphs
657 when (showBlocks uiopts
/= ShowBlocksNone
) $ sequence_
658 $ [drawBlocked drawSt colouring
False force |
659 showBlocks uiopts
== ShowBlocksAll
,
660 AlertBlockedForce force
<- drawAlerts
]
661 ++ [ drawBlocked drawSt colouring
True force
662 | AlertBlockingForce force
<- drawAlerts
]
663 -- ++ [ drawBlocked drawSt colouring True force |
664 -- AlertResistedForce force <- drawAlerts ]
665 ++ [ drawAt CollisionMarker pos
666 | AlertCollision pos
<- drawAlerts
]
667 -- ++ [ drawApplied drawSt colouring force
668 -- | AlertAppliedForce force <- drawAlerts ]
669 vidSurf
<- liftIO getVideoSurface
670 liftIO
$ blitSurface gsSurf Nothing vidSurf Nothing
672 playAlertSounds
:: GameState
-> [Alert
] -> UIM
()
674 playAlertSounds st alerts
= do
675 use
<- useSounds
<$> gets uiOptions
676 when use
$ mapM_ (maybe (return ()) playSound
. alertSound
) alerts
678 alertSound
(AlertBlockedForce force
) =
679 let PlacedPiece _ piece
= getpp st
$ forceIdx force
681 Wrench _
-> Just
"wrenchblocked"
682 Hook _ _
-> if isPush force
then Just
"hookblocked" else Just
"hookarmblocked"
684 alertSound
(AlertDivertedWrench _
) = Just
"wrenchscrape"
685 alertSound
(AlertAppliedForce
(Torque idx _
))
686 | isPivot
.placedPiece
.getpp st
$ idx
= Just
"pivot"
687 alertSound
(AlertAppliedForce
(Push idx dir
))
688 | isBall
.placedPiece
.getpp st
$ idx
= Just
"ballmove"
689 alertSound
(AlertAppliedForce
(Push idx dir
)) = do
690 (align
,newLen
) <- listToMaybe [(align
,newLen
)
691 | c
@(Connection
(startIdx
,_
) (endIdx
,_
) (Spring outDir natLen
)) <- connections st
692 , let align
= (if outDir
== dir
then 1 else if outDir
== neg dir
then -1 else 0)
693 * (if idx
== startIdx
then 1 else if idx
== endIdx
then -1 else 0)
695 , let newLen
= connectionLength st c
]
696 return $ "spring" ++ (if align
== 1 then "contract" else "extend")
697 ++ show (min newLen
12)
698 alertSound AlertUnlocked
= Just
"unlocked"
699 alertSound _
= Nothing
700 playSound
:: String -> UIM
()
701 playSound sound
= void
.runMaybeT
$ do
702 ss
<- MaybeT
$ Map
.lookup sound
<$> gets sounds
704 liftIO
$ randFromList ss
>>= \(Just s
) -> void
$ tryPlayChannel
(-1) s
0
705 randFromList
:: [a
] -> IO (Maybe a
)
706 randFromList
[] = return Nothing
707 randFromList
as = (Just
.(as!!)) <$> randomRIO (0,length as - 1)
709 playAlertSounds _ _
= return ()
713 drawMiniLock
:: Lock
-> HexVec
-> UIM
()
714 drawMiniLock lock v
= do
715 surface
<- gets miniLocks
>>= maybe new
return . Map
.lookup lock
716 renderToMain
$ blitAt surface v
721 let minisize
= size `
div`
ceiling (lockSize lock
% miniLocksize
)
722 let width
= size
*2*(miniLocksize
*2+1)
723 let height
= ceiling $ fi size
* sqrt 3 * fi
(miniLocksize
*2+1+1)
724 surf
<- liftIO
$ createRGBSurface
[] width height
16 0 0 0 0
725 liftIO
$ setColorKey surf
[SrcColorKey
,RLEAccel
] $ Pixel
0
726 uiopts
<- gets uiOptions
727 let st
= snd $ reframe lock
728 coloured
= colouredPieces
False st
729 colouring
= if useFiveColouring uiopts
730 then boardColouring st coloured Map
.empty
731 else pieceTypeColouring st coloured
732 draw
= sequence_ [ drawAt glyph pos |
733 (pos
,glyph
) <- Map
.toList
$ ownedTileGlyph colouring
[] <$> stateBoard st
]
734 liftIO
$ runRenderM draw emptyCachedGlyphs
$
735 RenderContext surf Nothing
(PHS zero
) (SVec
(width`
div`
2) (height`
div`
2)) zero minisize Nothing
737 modify
$ \ds
-> ds
{ miniLocks
= Map
.insert lock surf
$ miniLocks ds
}
740 -- | TODO: do this more cleverly
742 (>=50).Map
.size
<$> gets miniLocks
>>? clearMiniLocks
743 clearMiniLocks
= modify
$ \ds
-> ds
{ miniLocks
= Map
.empty}
745 drawEmptyMiniLock v
=
746 renderToMain
$ recentreAt v
$ rescaleRender
6 $ drawAtRel
(HollowInnerGlyph
$ dim white
) zero
748 getBindingStr
:: InputMode
-> UIM
(Command
-> String)
749 getBindingStr mode
= do
750 setting
<- gets settingBinding
751 uibdgs
<- gets
(Map
.findWithDefault
[] mode
. uiKeyBindings
)
753 if Just cmd
== setting
then "??"
754 else maybe "" showKey
$ findBinding
(uibdgs
++ bindings mode
) cmd
)
756 drawButtons
:: InputMode
-> UIM
()
757 drawButtons mode
= do
758 buttons
<- getButtons mode
759 bindingStr
<- getBindingStr mode
760 showBT
<- gets
(showButtonText
. uiOptions
)
761 smallFont
<- gets dispFontSmall
762 renderToMain
$ sequence_ $ concat [ [ do
763 drawAtRel
(ButtonGlyph col
) v
764 (if length bdg
> 2 then withFont smallFont
else id) $
765 renderStrColAt buttonTextCol bdg v
767 withFont smallFont
$ recentreAt v
$ rescaleRender
(1/4) $
768 sequence_ [ renderStrColAtLeft white s dv |
(s
,dv
) <- helps
]
769 |
(i
,(v
,bdg
,helps
)) <- enumerate
$ map (\b->(buttonPos b
, bindingStr
$ buttonCmd b
, buttonHelp b
)) buttonGroup
770 , let col
= dim
$ colourWheel
(base
+inc
*i
) ]
771 |
(buttonGroup
,(base
,inc
)) <- buttons
773 where enumerate
= zip [0..]
776 initMisc
= void
$ enableUnicode
True >> enableKeyRepeat
250 30 >> setCaption
"intricacy" "intricacy"
778 initVideo
:: Int -> Int -> UIM
()
780 liftIO
$ (((w
,h
)==(0,0) &&) . (InitVideo `
elem`
) <$> wasInit
[InitVideo
]) >>?
781 -- reset video so that passing (0,0) to setVideoMode sets to
782 -- current screen res rather than current window size
783 (quitSubSystem
[InitVideo
] >> initSubSystem
[InitVideo
] >> initMisc
)
785 fs
<- gets
(fullscreen
. uiOptions
)
787 (w
',h
') <- if fs ||
(w
,h
)/=(0,0) then return (w
,h
) else do
788 -- use smaller dimensions than the screen's, to work around a bug
789 -- seen on mac, whereby a resizable window created with
790 -- (w,h)=(0,0), or even with the (w,h) given by getDimensions
791 -- after creating such a window, is reported to be larger than it
793 (w
',h
') <- getDimensions
794 return (4*w
'`
div`
5,4*h
'`
div`
5)
795 setVideoMode w
' h
' 0 $ if fs
then [Fullscreen
] else [Resizable
]
797 (w
',h
') <- liftIO getDimensions
799 modify
$ \ds
-> ds
{ scrWidth
= w
' }
800 modify
$ \ds
-> ds
{ scrHeight
= h
' }
801 gssurf
<- liftIO
$ createRGBSurface
[] w
' h
' 16 0 0 0 0
802 modify
$ \ds
-> ds
{ gsSurface
= Just gssurf
, lastDrawArgs
= Nothing
}
805 let fontfn
= "VeraMoBd.ttf"
806 fontpath
<- liftIO
$ getDataPath fontfn
807 font
<- liftIO
$ TTF
.tryOpenFont fontpath size
808 smallFont
<- liftIO
$ TTF
.tryOpenFont fontpath
(2*size`
div`
3)
809 modify
$ \ds
-> ds
{ dispFont
= font
, dispFontSmall
= smallFont
}
811 useBG
<- gets
$ useBackground
. uiOptions
812 mbg
<- if useBG
then do
813 bgsurf
<- liftIO
$ createRGBSurface
[] w
' h
' 16 0 0 0 0
814 renderToMainWithSurf bgsurf
$ drawBasicBG
$ 2*max screenWidthHexes screenHeightHexes`
div`
3
817 modify
$ \ds
-> ds
{ bgSurface
= mbg
}
821 when (isNothing font
) $ liftIO
$ do
822 now
<- getCurrentTime
823 let text
= show now
++ ": Warning: font file not found at "++fontpath
++".\n"
825 appendFile "intricacy-warnings.log" text
828 getDimensions
= (videoInfoWidth
&&& videoInfoHeight
) <$> getVideoInfo
834 initialised
<- liftIO
$ tryOpenAudio defaultFrequency AudioS16Sys
1 1024
835 unless initialised
$ liftIO
$ do
836 now
<- getCurrentTime
837 let text
= show now
++ ": Warning: audio failed to initialise.\n"
839 appendFile "intricacy-warnings.log" text
840 -- liftIO $ querySpec >>= print
841 liftIO
$ allocateChannels
16
842 let seqWhileJust
(m
:ms
) = m
>>= \ret
-> case ret
of
844 Just a
-> (a
:) <$> seqWhileJust ms
845 soundsdir
<- liftIO
$ getDataPath
"sounds"
846 sounds
<- sequence [ do
847 chunks
<- liftIO
$ seqWhileJust
849 chunk
<- msum $ map (MaybeT
. tryLoadWAV
) paths
850 liftIO
$ volumeChunk chunk vol
853 , let paths
= [soundsdir
++ [pathSeparator
] ++ sound
++
854 "-" ++ (if n
< 10 then ('0':) else id) (show n
) ++ ext
855 | ext
<- [".ogg", ".wav"] ]
856 , let vol
= case sound
of
861 return (sound
,chunks
)
862 | sound
<- ["hookblocked","hookarmblocked","wrenchblocked","wrenchscrape","pivot","unlocked","ballmove"]
863 ++ ["spring" ++ d
++ show l | d
<- ["extend","contract"], l
<- [1..12]] ]
864 -- liftIO $ print sounds
865 modify
$ \s
-> s
{ sounds
= Map
.fromList sounds
}
867 initAudio
= return ()
878 drawMsgLine
= void
.runMaybeT
$ do
880 [ MaybeT
$ gets message
881 , (dimWhiteCol
,) <$> MaybeT
(gets hoverStr
)
884 renderToMain
$ blankRow messageLineCentre
885 smallFont
<- gets dispFontSmall
887 (if length str
> screenWidthHexes
* 3 then withFont smallFont
else id) $
888 renderStrColAtCentre col str messageLineCentre
890 setMsgLineNoRefresh col str
= do
891 modify
$ \s
-> s
{ message
= Just
(col
,str
) }
893 setMsgLine col str
= setMsgLineNoRefresh col str
>> refresh
896 clearMsg
= modify
$ \s
-> s
{ message
= Nothing
}
898 drawTitle
(Just title
) = renderToMain
$ renderStrColAtCentre messageCol title titlePos
899 drawTitle Nothing
= return ()
901 say
= setMsgLine messageCol
902 sayError
= setMsgLine errorCol
904 miniLockPos
= (-9)*^hw
+^ hu
905 lockLinePos
= 4*^hu
+^ miniLockPos
906 serverPos
= 12*^hv
+^
7*^neg hu
907 serverWaitPos
= serverPos
+^ hw
+^ neg hu
908 randomNamesPos
= 9*^hv
+^
2*^ neg hu
909 codenamePos
= (-6)*^hw
+^
6*^hv
910 undeclsPos
= 13*^neg hu
911 accessedOursPos
= 2*^hw
+^ codenamePos
912 locksPos
= hw
+^neg hv
913 retiredPos
= locksPos
+^
11*^hu
+^ neg hv
914 interactButtonsPos
= 9*^neg hu
+^
8*^hw
915 scoresPos
= codenamePos
+^
5*^hu
+^
2*^neg hv