packages feed

intricacy-0.7: SDLGlyph.hs

-- This file is part of Intricacy
-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of version 3 of the GNU General Public License as
-- published by the Free Software Foundation, or any later version.
--
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see http://www.gnu.org/licenses/.

module SDLGlyph where

import Graphics.UI.SDL
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.State
import Control.Monad.Trans.Reader
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.Class
import Data.Map (Map)
import qualified Data.Map as Map
import qualified Data.List as List
import Control.Applicative
import System.Random (randomRIO)

import Hex
import SDLRender
import GameState
import GameStateTypes
import BoardColouring
import Physics
import Command
import Util


data ShowBlocks = ShowBlocksBlocking | ShowBlocksAll | ShowBlocksNone
    deriving (Eq, Ord, Show, Read)

data Glyph
    = TileGlyph Tile Pixel
    | BlockedArm HexDir TorqueDir Pixel
    | TurnedArm HexDir TorqueDir Pixel
    | BlockedBlock Tile HexDir Pixel
    | BlockedPush HexDir Pixel
    | CollisionMarker
    | HollowGlyph Pixel
    | HollowInnerGlyph Pixel
    | FilledHexGlyph Pixel
    | ButtonGlyph Pixel
    | UseFiveColourButton Bool
    | ShowBlocksButton ShowBlocks
    | ShowButtonTextButton Bool
    | UseSoundsButton Bool
    | WhsButtonsButton (Maybe WrHoSel)
    | FullscreenButton Bool
    | UnfreshGlyph
    deriving (Eq, Ord, Show)

type SizedGlyph = (Glyph,Int)
data CachedGlyphs = CachedGlyphs (Map SizedGlyph Surface) [SizedGlyph]
    deriving (Eq, Ord, Show)
emptyCachedGlyphs = CachedGlyphs Map.empty []
maxCachedGlyphs = 100

type RenderM = RenderT (StateT CachedGlyphs IO)
runRenderM :: RenderM a -> CachedGlyphs -> RenderContext -> IO (a,CachedGlyphs)
runRenderM m cgs rc = runStateT (runReaderT m rc) cgs

drawAt :: Glyph -> HexPos -> RenderM ()
drawAt gl pos = do
	centre <- asks renderHCentre
	drawAtRel gl (pos -^ centre)

drawAtRel :: Glyph -> HexVec -> RenderM ()
drawAtRel gl v = do
    size <- asks renderSize
    displaceRenderSVec (hexVec2SVec size v) $ renderGlyphCaching gl

renderGlyphCaching :: Glyph -> RenderM ()
-- Glyph caching:
--  We aim to cache glyphs which are "currently" being regularly drawn, so
--  they can be blitted from RAM rather than being drawn afresh each time.
--  Rather than track statistics, we adopt the following probabilistic scheme.
renderGlyphCaching gl = do
    CachedGlyphs cmap clist <- lift get
    size <- asks renderSize
    let sgl = (gl,size)
	w = size*2 + 1
	h = ysize size*4 + 1
	newGlyphSurf = do
	    -- csurf <- liftIO $ createRGBSurface [] w h 32 0xff000000 0x00ff0000 0x0000ff00 0x000000ff
	    csurf <- liftIO $ createRGBSurface [] w h 16 0 0 0 0
	    liftIO $ setColorKey csurf [SrcColorKey,RLEAccel] $ Pixel 0
	    return csurf
	renderOnCache csurf =
            let ccxt rc = rc { renderSurf = csurf, renderSCentre = SVec (w`div`2) (h`div`2) }
	    in local ccxt $ renderGlyph gl
	addToCache cacheFull csurf = do
	    CachedGlyphs cmap clist <- lift get
	    let cmap' = Map.insert sgl csurf cmap
	    lift $ put $ if cacheFull
		then CachedGlyphs (Map.delete (last clist) cmap') (sgl:List.init clist)
		else CachedGlyphs cmap' (sgl:clist)
	promote = do
	    CachedGlyphs cmap clist <- lift get
	    lift $ put $ CachedGlyphs cmap (sgl:List.delete sgl clist)
	blitGlyph csurf = do
            surf <- asks renderSurf
	    (x,y) <- renderPos zero
	    void $ liftIO $ blitSurface csurf Nothing surf $ Just $
		Rect (x-w`div`2) (y-h`div`2) (w+1) (h+1)
    let cacheFull = Map.size cmap >= maxCachedGlyphs
    let mcsurf = Map.lookup sgl cmap
    -- with probability 1 in (maxCachedGlyphs`div`2), we put this glyph at the
    -- head of the cached list, throwing away the tail to make room if needed.
    cacheIt <- (((cacheable &&) . (not cacheFull ||)) <$>) $
	liftIO $ (==0) <$> randomRIO (0::Int,maxCachedGlyphs`div`2)
    case mcsurf of
	Nothing -> if cacheIt
	    then do
		csurf <- newGlyphSurf
		renderOnCache csurf
		addToCache cacheFull csurf
		blitGlyph csurf
	    else
		renderGlyph gl
	Just csurf -> do
	    when cacheIt promote
	    blitGlyph csurf
    where
	cacheable = case gl of
	    -- some glyphs need to be drawn with blending - those involving
	    -- anti-aliasing which bleed over the edge of the hex or which
	    -- may be drawn on top of an existing glyph.
	    -- TODO: we should find a way to deal with at least some of these;
	    -- springs in particular are common and expensive to draw.
	    -- Maybe we could truncate the spring glyphs to a hex?
	    TileGlyph (BlockTile adjs) _ -> null adjs
	    TileGlyph (SpringTile extn dir) _ -> False
	    FilledHexGlyph _ -> False
	    HollowGlyph _ -> False
	    BlockedBlock _ _ _ -> False
	    BlockedPush _ _ -> False
	    CollisionMarker -> False
	    _ -> True

renderGlyph :: Glyph -> RenderM ()
renderGlyph (TileGlyph (BlockTile adjs) col) =
    rimmedPolygonR corners col $ bright col
	where
	    corners = concat [
		if or $ map adjAt [0,1]
		then [corner $ hextant dir]
		else if adjAt $ -1
		    then []
		    else [innerCorner dir]
		| dir <- hexDirs
		, let adjAt r = rotate r dir `elem` adjs
		]

renderGlyph (TileGlyph (SpringTile extn dir) col) =
    thickLinesR points 1 $ brightness col
	where
            n :: Int
	    n = 3*case extn of
		Stretched -> 1
		Relaxed -> 2
		Compressed -> 4
	    brightness = if extn == Relaxed then dim else bright
	    dir' = if dir == zero then hu else dir
	    s = corner (hextant dir' - 1)
	    off = corner (hextant dir')
	    e = corner (hextant dir' - 3)
	    points = [ b +^ (fi i / fi n) **^ (e -^ s)
		| i <- [0..n]
		, i`mod`3 /= 1
		, let b = if i`mod`3==0 then s else off ]

renderGlyph (TileGlyph (PivotTile dir) col) = do
    rimmedCircleR zero (7/8) col $ bright col
    when (dir /= zero)
	$ aaLineR from to $ bright col
    return ()
	where
	    from = (7/8) **^ edge (neg dir)
	    to = (7/8) **^ edge dir

renderGlyph (TileGlyph (ArmTile dir _) col) =
    thickLineR from to 1 col
	where
	    dir' = if dir == zero then hu else dir
	    from = edge $ neg dir'
	    to = innerCorner dir'

renderGlyph (TileGlyph HookTile col) =
    rimmedCircleR zero (7/8) col $ bright col

renderGlyph (TileGlyph (WrenchTile mom) col) = do
    rimmedCircleR zero (1/3) col $ bright col
    when (mom /= zero) $
	let
	    from = innerCorner $ neg mom
	    to = edge $ neg mom
	    shifts = [ (1/2) **^ (b -^ a)
		| rot <- [-1,0,1]
		, let a = innerCorner $ neg mom
		, let b = innerCorner $ rotate rot $ neg mom
		]
	in sequence_
	    [ aaLineR (from+^shift) (to+^shift) $ col
	    | shift <- shifts ]

renderGlyph (TileGlyph BallTile col) =
    rimmedCircleR zero (7/8) (faint col) (obscure col)

renderGlyph (BlockedArm armdir tdir col) =
    aaLineR from to col
	where
	    from = innerCorner $ rotate (2*tdir) armdir
	    to = edge $ rotate tdir armdir

renderGlyph (TurnedArm armdir tdir col) =
    sequence_ [ arcR c r a1 a2 col | r <- [8/4,9/4] ]
    where
	c = hexVec2FVec $ neg armdir
	a0 = fi $ -60*hextant armdir
	a1' = a0 + fi tdir * 10
	a2' = a0 + fi tdir * 30
	a1 = min a1' a2'
	a2 = max a1' a2'

renderGlyph (BlockedBlock tile dir col) =
    displaceRender shift $ renderGlyph (TileGlyph tile col)
	where shift = innerCorner dir -^ edge dir

renderGlyph (BlockedPush dir col) = do
    thickLineR zero tip 1 col
    thickLineR tip (arms!!0) 1 col
    thickLineR tip (arms!!1) 1 col
    where
	tip@(FVec tx ty) = edge dir
        arms = [ FVec ((tx/2) + d*ty/4) (ty/2 - d*tx/4) | d <- [-1,1] ]

renderGlyph CollisionMarker = do
    -- rimmedCircle surf centre (size`div`3) (bright purple) $ bright purple
    aaLineR start end $ col
    aaCircleR zero rad col
    where
	[start,end] = map (((1/2)**^) . corner) [0,3]
	rad = ylen
	col = dim purple

renderGlyph (HollowGlyph col) =
    aaPolygonR corners $ opaquify col
    where corners = map corner [0..5]
renderGlyph (HollowInnerGlyph col) =
    aaPolygonR corners $ opaquify col
    where corners = map innerCorner hexDirs

renderGlyph (FilledHexGlyph col) =
    rimmedPolygonR corners col $ brightish col
    where corners = map corner [0..5]

renderGlyph (ButtonGlyph col) =
    renderGlyph (TileGlyph (BlockTile []) col)

renderGlyph (UseFiveColourButton using) =
    rescaleRender (1/2) $ sequence_ [
        displaceRender (corner h) $ renderGlyph
            (TileGlyph (BlockTile [])
                (dim $ colourWheel (if using then h`div`2 else 1)))
        | h <- [0,2,4] ]

renderGlyph (ShowBlocksButton showing) = do
    renderGlyph (TileGlyph (BlockTile []) (dim red))
    when (showing == ShowBlocksAll) $
	renderGlyph (BlockedPush hu (bright orange))
    when (showing /= ShowBlocksNone) $
	renderGlyph (BlockedPush hw (bright purple))

renderGlyph (ShowButtonTextButton showing) = do
    rescaleRender (1/2) $ displaceRender (edge (neg hu)) $
        renderGlyph (ButtonGlyph (dim yellow))
    when showing $
	sequence_ [ pixelR (FVec (1/3 + i/4) (-1/4)) (bright white) | i <- [-1..1] ]

renderGlyph (UseSoundsButton use) = do
    sequence [ arcR (FVec (-2/3) 0) r (-20) 20
            (if use then bright green else dim red)
        | r <- [1/3,2/3,1] ]
    when (not use) $
        aaLineR (innerCorner hw) (innerCorner $ neg hw) $ dim red

renderGlyph (WhsButtonsButton Nothing) = rescaleRender (1/3) $ do
    renderGlyph (ButtonGlyph (dim red))
    sequence_ [ displaceRender ((3/2) **^ edge dir) $
            renderGlyph (ButtonGlyph (dim purple))
        | dir <- hexDirs ]
renderGlyph (WhsButtonsButton (Just whs)) = rescaleRender (1/2) $ do
    when (whs /= WHSHook) $
	displaceRender (corner 0) $ renderGlyph (TileGlyph (WrenchTile zero) col)
    when (whs /= WHSWrench) $ do
	displaceRender (corner 4) $ renderGlyph (TileGlyph HookTile col)
	displaceRender (corner 2) $ renderGlyph (TileGlyph (ArmTile hv False) col)
    where
	col = dim white

renderGlyph (FullscreenButton fs) = do
    thickPolygonR corners 1 $ activeCol (not fs)
    thickPolygonR corners' 1 $ activeCol fs
    where
	activeCol True = opaquify $ dim green
	activeCol False = opaquify $ dim red
	corners = [ (2/3) **^ (if dir `elem` [hu,neg hu] then edge else innerCorner) dir
	    | dir <- hexDirs ]
	corners' = map (((2/3)**^) . corner) [0..5]

renderGlyph (UnfreshGlyph) = do
    let col = bright red
    renderGlyph (HollowInnerGlyph col)
    sequence_ [pixelR (FVec (i/4) 0) col
	| i <- [-1..1] ]

playerGlyph col = FilledHexGlyph col

cursorGlyph = HollowGlyph $ bright white

ownedTileGlyph colouring highlight (owner,t) =
    let col = colourOf colouring owner
    in TileGlyph t $ (if owner `elem` highlight then bright else dim) col

drawCursorAt :: Maybe HexPos -> RenderM ()
drawCursorAt (Just pos) = drawAt cursorGlyph pos
drawCursorAt _ = return ()

drawBasicBG :: Int -> RenderM ()
drawBasicBG maxR = sequence_ [ drawAtRel (HollowGlyph $ colAt v) v | v <- hexDisc maxR ]
    where
	colAt v@(HexVec hx hy hz) = let
		[r,g,b] = map (\h -> fi $ ((0xff*)$ 5 + abs h)`div`maxR) [hx,hy,hz]
		a = fi $ (0x90 * (maxR - abs (hexLen v)))`div`maxR
	    in rgbaToPixel (r,g,b,a)

drawBlocked :: GameState -> PieceColouring -> Bool -> Force -> RenderM ()
drawBlocked st colouring blocking (Torque idx dir) = do
    let (pos,arms) = case getpp st idx of
	    PlacedPiece pos (Pivot arms) -> (pos,arms)
	    PlacedPiece pos (Hook arm _) -> (pos,[arm])
	    _ -> (pos,[])
	col = if blocking then bright $ purple else dim $ colourOf colouring idx
    sequence_ [ drawAt (BlockedArm arm dir col) (arm +^ pos) |
	arm <- arms ]
drawBlocked st colouring blocking (Push idx dir) = do
    let footprint = plPieceFootprint $ getpp st idx
	fullfootprint = fullFootprint st idx
	col = bright $ if blocking then purple else orange
    sequence_ [ drawAt (BlockedPush dir col) pos
	| pos <- footprint
	, (dir+^pos) `notElem` fullfootprint ]
    -- drawAt (blockedPush dir $ bright orange) $ placedPos $ getpp st idx

drawApplied :: GameState -> PieceColouring -> Force -> RenderM ()
drawApplied st colouring (Torque idx dir) = do
    let (pos,arms) = case getpp st idx of
	    PlacedPiece pos (Pivot arms) -> (pos,arms)
	    PlacedPiece pos (Hook arm _) -> (pos,[arm])
	    _ -> (pos,[])
	col = dim $ colourOf colouring idx
    sequence_ [ drawAt (TurnedArm arm dir col) (arm +^ pos) |
	    arm <- arms ]
drawApplied _ _ _ = return ()