swarm-0.5.0.0: src/Swarm/TUI/View/CellDisplay.hs
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Rendering of cells in the map view
--
-- SPDX-License-Identifier: BSD-3-Clause
module Swarm.TUI.View.CellDisplay where
import Brick
import Control.Lens (to, view, (&), (.~), (^.))
import Data.ByteString (ByteString)
import Data.Hash.Murmur
import Data.List.NonEmpty qualified as NE
import Data.Map qualified as M
import Data.Maybe (maybeToList)
import Data.Semigroup (sconcat)
import Data.Tagged (unTagged)
import Data.Text (Text)
import Data.Word (Word32)
import Linear.Affine ((.-.))
import Swarm.Game.CESK (TickNumber (..))
import Swarm.Game.Display (
Attribute (AEntity),
Display,
defaultEntityDisplay,
displayAttr,
displayChar,
displayPriority,
hidden,
)
import Swarm.Game.Entity
import Swarm.Game.Robot
import Swarm.Game.Scenario.Topography.EntityFacade
import Swarm.Game.State
import Swarm.Game.Terrain
import Swarm.Game.Universe
import Swarm.Game.World qualified as W
import Swarm.TUI.Editor.Masking
import Swarm.TUI.Editor.Model
import Swarm.TUI.Editor.Util qualified as EU
import Swarm.TUI.Model.Name
import Swarm.TUI.Model.UI
import Swarm.TUI.View.Attribute.Attr
import Witch (from)
import Witch.Encoding qualified as Encoding
-- | Render a display as a UI widget.
renderDisplay :: Display -> Widget n
renderDisplay disp = withAttr (disp ^. displayAttr . to toAttrName) $ str [displayChar disp]
-- | Render the 'Display' for a specific location.
drawLoc :: UIState -> GameState -> Cosmic W.Coords -> Widget Name
drawLoc ui g cCoords@(Cosmic _ coords) =
if shouldHideWorldCell ui coords
then str " "
else drawCell
where
showRobots = ui ^. uiShowRobots
we = ui ^. uiWorldEditor . worldOverdraw
drawCell = renderDisplay $ displayLoc showRobots we g cCoords
-- | Subset of the game state needed to render the world
data RenderingInput = RenderingInput
{ multiworldInfo :: W.MultiWorld Int Entity
, isKnownFunc :: EntityPaint -> Bool
}
displayTerrainCell ::
WorldOverdraw ->
RenderingInput ->
Cosmic W.Coords ->
Display
displayTerrainCell worldEditor ri coords =
terrainMap M.! EU.getEditorTerrainAt worldEditor (multiworldInfo ri) coords
displayRobotCell ::
GameState ->
Cosmic W.Coords ->
[Display]
displayRobotCell g coords =
map (view robotDisplay) $
robotsAtLocation (fmap W.coordsToLoc coords) g
-- | Extract the relevant subset of information from the 'GameState' to be able
-- to compute whether an entity is "known".
mkEntityKnowledge :: GameState -> EntityKnowledgeDependencies
mkEntityKnowledge gs =
EntityKnowledgeDependencies
{ isCreativeMode = gs ^. creativeMode
, globallyKnownEntities = gs ^. discovery . knownEntities
, theFocusedRobot = focusedRobot gs
}
-- | The subset of information required to compute whether
-- an entity is "known", and therefore should be rendered
-- normally vs as a question mark.
data EntityKnowledgeDependencies = EntityKnowledgeDependencies
{ isCreativeMode :: Bool
, globallyKnownEntities :: [Text]
, theFocusedRobot :: Maybe Robot
}
-- | Determines whether an entity should be rendered
-- normally vs as a question mark.
getEntityIsKnown :: EntityKnowledgeDependencies -> EntityPaint -> Bool
getEntityIsKnown knowledge ep = case ep of
Facade (EntityFacade _ _) -> True
Ref e -> or reasonsToShow
where
reasonsToShow =
[ isCreativeMode knowledge
, e `hasProperty` Known
, (e ^. entityName) `elem` globallyKnownEntities knowledge
, showBasedOnRobotKnowledge
]
showBasedOnRobotKnowledge = maybe False (`robotKnows` e) $ theFocusedRobot knowledge
displayEntityCell ::
WorldOverdraw ->
RenderingInput ->
Cosmic W.Coords ->
[Display]
displayEntityCell worldEditor ri coords =
maybeToList $ displayForEntity <$> maybeEntity
where
(_, maybeEntity) = EU.getEditorContentAt worldEditor (multiworldInfo ri) coords
displayForEntity :: EntityPaint -> Display
displayForEntity e = (if isKnownFunc ri e then id else hidden) $ getDisplay e
-- | Get the 'Display' for a specific location, by combining the
-- 'Display's for the terrain, entity, and robots at the location, and
-- taking into account "static" based on the distance to the robot
-- being @view@ed.
displayLoc :: Bool -> WorldOverdraw -> GameState -> Cosmic W.Coords -> Display
displayLoc showRobots we g cCoords@(Cosmic _ coords) =
staticDisplay g coords
<> displayLocRaw we ri robots cCoords
where
ri = RenderingInput (g ^. landscape . multiWorld) (getEntityIsKnown $ mkEntityKnowledge g)
robots =
if showRobots
then displayRobotCell g cCoords
else []
-- | Get the 'Display' for a specific location, by combining the
-- 'Display's for the terrain, entity, and robots at the location.
displayLocRaw ::
WorldOverdraw ->
RenderingInput ->
-- | Robot displays
[Display] ->
Cosmic W.Coords ->
Display
displayLocRaw worldEditor ri robotDisplays coords =
sconcat $ terrain NE.:| entity <> robotDisplays
where
terrain = displayTerrainCell worldEditor ri coords
entity = displayEntityCell worldEditor ri coords
-- | Random "static" based on the distance to the robot being
-- @view@ed.
staticDisplay :: GameState -> W.Coords -> Display
staticDisplay g coords = maybe mempty displayStatic (getStatic g coords)
-- | Draw static given a number from 0-15 representing the state of
-- the four quarter-pixels in a cell
displayStatic :: Word32 -> Display
displayStatic s =
defaultEntityDisplay (staticChar s)
& displayPriority .~ maxBound -- Static has higher priority than anything else
& displayAttr .~ AEntity
-- | Given a value from 0--15, considered as 4 bits, pick the
-- character with the corresponding quarter pixels turned on.
staticChar :: Word32 -> Char
staticChar = \case
0 -> ' '
1 -> '▖'
2 -> '▗'
3 -> '▄'
4 -> '▘'
5 -> '▌'
6 -> '▚'
7 -> '▙'
8 -> '▝'
9 -> '▞'
10 -> '▐'
11 -> '▟'
12 -> '▀'
13 -> '▛'
14 -> '▜'
15 -> '█'
_ -> ' '
-- | Random "static" based on the distance to the robot being
-- @view@ed. A cell can either be static-free (represented by
-- @Nothing@) or can have one of sixteen values (representing the
-- state of the four quarter-pixels in one cell).
getStatic :: GameState -> W.Coords -> Maybe Word32
getStatic g coords
| isStatic = Just (h `mod` 16)
| otherwise = Nothing
where
-- Offset from the location of the view center to the location under
-- consideration for display.
offset = W.coordsToLoc coords .-. (g ^. viewCenter . planar)
-- Hash.
h =
murmur3 1 . unTagged . from @String @(Encoding.UTF_8 ByteString) . show $
-- include the current tick count / 16 in the hash, so the pattern of static
-- changes once every 16 ticks
(offset, getTickNumber (g ^. temporal . ticks) `div` 16)
-- Hashed probability, i.e. convert the hash into a floating-point number between 0 and 1
hp :: Double
hp = fromIntegral h / fromIntegral (maxBound :: Word32)
isStatic = case focusedRange g of
-- If we're not viewing a robot, display static. This
-- can happen if e.g. the robot we were viewing drowned.
-- This is overridden by creative mode, e.g. when no robots
-- have been defined for the scenario.
Nothing -> not $ g ^. creativeMode
-- Don't display static if the robot is close, or when we're in
-- creative mode or the player is allowed to scroll the world.
Just Close -> False
-- At medium distances, replace cell with static with a
-- probability that increases with distance.
Just (MidRange s) -> hp < 1 - cos (s * (pi / 2))
-- Far away, everything is static.
Just Far -> True