{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE RecordWildCards #-}
--------------------------------------------------------------------------------
module Xanthous.Entities.Character
( -- * Character datatype
Character(..)
, characterName
, inventory
, characterDamage
, characterHitpoints'
, characterHitpoints
, hitpointRecoveryRate
, speed
, body
-- ** Inventory
, Inventory(..)
, backpack
, wielded
, items
, InventoryPosition(..)
, describeInventoryPosition
, inventoryPosition
, itemsWithPosition
, removeItemFromPosition
-- *** Wielded items
, Wielded(..)
, hands
, leftHand
, rightHand
, inLeftHand
, inRightHand
, doubleHanded
, wieldedItems
, WieldedItem(..)
, wieldedItem
, wieldableItem
, asWieldedItem
-- *** Body
, Body(..)
, initialBody
, knuckles
, Knuckles(..)
, fistDamageChance
, damageKnuckles
, fistfightingDamage
-- * Character functions
, mkCharacter
, pickUpItem
, isDead
, isFullyHealed
, damage
) where
--------------------------------------------------------------------------------
import Xanthous.Prelude
--------------------------------------------------------------------------------
import Brick
import Data.Aeson.Generic.DerivingVia
import Data.Aeson (ToJSON, FromJSON)
import Data.Coerce (coerce)
import Test.QuickCheck
import Test.QuickCheck.Instances.Vector ()
import Test.QuickCheck.Arbitrary.Generic
import Test.QuickCheck.Gen (chooseUpTo)
import Test.QuickCheck.Checkers (EqProp)
import Control.Monad.State.Lazy (execState)
import Control.Monad.Trans.State.Lazy (execStateT)
--------------------------------------------------------------------------------
import Xanthous.Util.QuickCheck
import Xanthous.Game.State
import Xanthous.Entities.Item
import Xanthous.Data
( TicksPerTile, Hitpoints, Per, Ticks, (|*|), positioned
, Positioned(..)
)
import Xanthous.Entities.RawTypes (WieldableItem, wieldable)
import qualified Xanthous.Entities.RawTypes as Raw
import Xanthous.Util (EqEqProp(EqEqProp), modifyKL, removeFirst)
import Xanthous.Monad (say_)
--------------------------------------------------------------------------------
data WieldedItem = WieldedItem
{ _wieldedItem :: Item
, _wieldableItem :: WieldableItem
-- ^ Invariant: item ^. itemType . wieldable ≡ Just wieldableItem
}
deriving stock (Eq, Show, Ord, Generic)
deriving anyclass (NFData, CoArbitrary, Function)
deriving (ToJSON, FromJSON)
via WithOptions '[ FieldLabelModifier '[Drop 1] ]
WieldedItem
makeFieldsNoPrefix ''WieldedItem
asWieldedItem :: Prism' Item WieldedItem
asWieldedItem = prism' hither yon
where
yon item = WieldedItem item <$> item ^. itemType . wieldable
hither (WieldedItem item _) = item
instance Brain WieldedItem where
step ticks (Positioned p wi) =
over positioned (\i -> WieldedItem i $ wi ^. wieldableItem)
<$> step ticks (Positioned p $ wi ^. wieldedItem)
instance Draw WieldedItem where
draw = draw . view wieldedItem
instance Entity WieldedItem where
entityAttributes = entityAttributes . view wieldedItem
description = description . view wieldedItem
entityChar = entityChar . view wieldedItem
instance Arbitrary WieldedItem where
arbitrary = genericArbitrary <&> \wi ->
wi & wieldedItem . itemType . wieldable ?~ wi ^. wieldableItem
data Wielded
= DoubleHanded WieldedItem
| Hands { _leftHand :: !(Maybe WieldedItem)
, _rightHand :: !(Maybe WieldedItem)
}
deriving stock (Eq, Show, Ord, Generic)
deriving anyclass (NFData, CoArbitrary, Function)
deriving Arbitrary via GenericArbitrary Wielded
deriving (ToJSON, FromJSON)
via WithOptions '[ 'SumEnc 'ObjWithSingleField ]
Wielded
nothingWielded :: Wielded
nothingWielded = Hands Nothing Nothing
hands :: Prism' Wielded (Maybe WieldedItem, Maybe WieldedItem)
hands = prism' (uncurry Hands) $ \case
Hands l r -> Just (l, r)
_ -> Nothing
leftHand :: Traversal' Wielded (Maybe WieldedItem)
leftHand = hands . _1
inLeftHand :: WieldedItem -> Wielded
inLeftHand wi = Hands (Just wi) Nothing
rightHand :: Traversal' Wielded (Maybe WieldedItem)
rightHand = hands . _2
inRightHand :: WieldedItem -> Wielded
inRightHand wi = Hands Nothing (Just wi)
doubleHanded :: Prism' Wielded WieldedItem
doubleHanded = prism' DoubleHanded $ \case
DoubleHanded i -> Just i
_ -> Nothing
wieldedItems :: Traversal' Wielded WieldedItem
wieldedItems k (DoubleHanded wielded) = DoubleHanded <$> k wielded
wieldedItems k (Hands l r) = Hands <$> _Just k l <*> _Just k r
data Inventory = Inventory
{ _backpack :: Vector Item
, _wielded :: Wielded
}
deriving stock (Eq, Show, Ord, Generic)
deriving anyclass (NFData, CoArbitrary, Function)
deriving Arbitrary via GenericArbitrary Inventory
deriving (ToJSON, FromJSON)
via WithOptions '[ FieldLabelModifier '[Drop 1] ]
Inventory
makeFieldsNoPrefix ''Inventory
items :: Traversal' Inventory Item
items k (Inventory bp w) = Inventory
<$> traversed k bp
<*> (wieldedItems . wieldedItem) k w
type instance Element Inventory = Item
instance MonoFunctor Inventory where
omap = over items
instance MonoFoldable Inventory where
ofoldMap = foldMapOf items
ofoldr = foldrOf items
ofoldl' = foldlOf' items
otoList = toListOf items
oall = allOf items
oany = anyOf items
onull = nullOf items
ofoldr1Ex = foldr1Of items
ofoldl1Ex' = foldl1Of' items
headEx = headEx . toListOf items
lastEx = lastEx . toListOf items
instance MonoTraversable Inventory where
otraverse = traverseOf items
instance Semigroup Inventory where
inv₁ <> inv₂ =
let backpack' = inv₁ ^. backpack <> inv₂ ^. backpack
(wielded', backpack'') = case (inv₁ ^. wielded, inv₂ ^. wielded) of
(wielded₁, wielded₂@(DoubleHanded _)) ->
(wielded₂, backpack' <> fromList (wielded₁ ^.. wieldedItems . wieldedItem))
(wielded₁, wielded₂@(Hands (Just _) (Just _))) ->
(wielded₂, backpack' <> fromList (wielded₁ ^.. wieldedItems . wieldedItem))
(wielded₁, Hands Nothing Nothing) -> (wielded₁, backpack')
(Hands Nothing Nothing, wielded₂) -> (wielded₂, backpack')
(Hands (Just l₁) Nothing, Hands Nothing (Just r₂)) ->
(Hands (Just l₁) (Just r₂), backpack')
(wielded₁@(DoubleHanded _), wielded₂) ->
(wielded₁, backpack' <> fromList (wielded₂ ^.. wieldedItems . wieldedItem))
(Hands Nothing (Just r₁), Hands Nothing (Just r₂)) ->
(Hands Nothing (Just r₂), r₁ ^. wieldedItem <| backpack')
(Hands Nothing r₁, Hands (Just l₂) Nothing) ->
(Hands (Just l₂) r₁, backpack')
(Hands (Just l₁) Nothing, Hands (Just l₂) Nothing) ->
(Hands (Just l₂) Nothing, l₁ ^. wieldedItem <| backpack')
(Hands (Just l₁) (Just r₁), Hands Nothing (Just r₂)) ->
(Hands (Just l₁) (Just r₂), r₁ ^. wieldedItem <| backpack')
(Hands (Just l₁) (Just r₁), Hands (Just l₂) Nothing) ->
(Hands (Just l₂) (Just r₁), l₁ ^. wieldedItem <| backpack')
in Inventory backpack'' wielded'
instance Monoid Inventory where
mempty = Inventory mempty $ Hands Nothing Nothing
-- | Representation for where in the inventory an item might be
data InventoryPosition
= Backpack
| LeftHand
| RightHand
| BothHands
deriving stock (Eq, Show, Ord, Generic)
deriving anyclass (NFData, CoArbitrary, Function)
deriving Arbitrary via GenericArbitrary InventoryPosition
-- | Return a human-readable description of the given 'InventoryPosition'
describeInventoryPosition :: InventoryPosition -> Text
describeInventoryPosition Backpack = "In backpack"
describeInventoryPosition LeftHand = "Wielded, in left hand"
describeInventoryPosition RightHand = "Wielded, in right hand"
describeInventoryPosition BothHands = "Wielded, in both hands"
-- | Given a position in the inventory, return a traversal on the inventory over
-- all the items in that position
inventoryPosition :: InventoryPosition -> Traversal' Inventory Item
inventoryPosition Backpack = backpack . traversed
inventoryPosition LeftHand = wielded . leftHand . _Just . wieldedItem
inventoryPosition RightHand = wielded . leftHand . _Just . wieldedItem
inventoryPosition BothHands = wielded . doubleHanded . wieldedItem
-- | A fold over all the items in the inventory accompanied by their position in
-- the inventory
--
-- Invariant: This will return items in the same order as 'items'
itemsWithPosition :: Fold Inventory (InventoryPosition, Item)
itemsWithPosition = folding $ (<>) <$> backpackItems <*> handItems
where
backpackItems = toListOf $ backpack . folded . to (Backpack ,)
handItems inventory = case inventory ^. wielded of
DoubleHanded i -> pure (BothHands, i ^. wieldedItem)
Hands l r -> (l ^.. folded . wieldedItem . to (LeftHand ,))
<> (r ^.. folded . wieldedItem . to (RightHand ,))
-- | Remove the first item equal to 'Item' from the given position in the
-- inventory
removeItemFromPosition :: InventoryPosition -> Item -> Inventory -> Inventory
removeItemFromPosition Backpack item inv
= inv & backpack %~ removeFirst (== item)
removeItemFromPosition LeftHand item inv
= inv & wielded . leftHand %~ filter ((/= item) . view wieldedItem)
removeItemFromPosition RightHand item inv
= inv & wielded . rightHand %~ filter ((/= item) . view wieldedItem)
removeItemFromPosition BothHands item inv
| has (wielded . doubleHanded . wieldedItem . filtered (== item)) inv
= inv & wielded .~ nothingWielded
| otherwise
= inv
--------------------------------------------------------------------------------
-- | The status of the character's knuckles
--
-- This struct is used to track the damage and then eventual build-up of
-- calluses when the character is fighting with their fists
data Knuckles = Knuckles
{ -- | How damaged are the knuckles currently, from 0 to 5?
--
-- At 0, no calluses will form
-- At 1 and up, the character will form calluses after a while
-- At 5, continuing to fistfight will deal the character even more damage
_knuckleDamage :: !Word
-- | How built-up are the character's calluses, from 0 to 5?
--
-- Each level of calluses decreases the likelihood of being damaged when
-- fistfighting by 1%, up to 5 where the character will never be damaged
-- fistfighting
, _knuckleCalluses :: !Word
-- | Number of turns that have passed since the last time the knuckles were
-- damaged
, _ticksSinceDamaged :: Ticks
}
deriving stock (Show, Eq, Ord, Generic)
deriving anyclass (NFData, CoArbitrary, Function)
deriving EqProp via EqEqProp Knuckles
deriving (ToJSON, FromJSON)
via WithOptions '[ FieldLabelModifier '[Drop 1] ]
Knuckles
makeLenses ''Knuckles
instance Semigroup Knuckles where
(Knuckles d₁ c₁ t₁) <> (Knuckles d₂ c₂ t₂) = Knuckles
(min (d₁ + d₂) 5)
(min (c₁ + c₂) 5)
(max t₁ t₂)
instance Monoid Knuckles where
mempty = Knuckles 0 0 0
instance Arbitrary Knuckles where
arbitrary = do
_knuckleDamage <- fromIntegral <$> chooseUpTo 5
_knuckleCalluses <- fromIntegral <$> chooseUpTo 5
_ticksSinceDamaged <- arbitrary
pure Knuckles{..}
-- | Likelihood that the character fighting with their fists will damage
-- themselves
fistDamageChance :: Knuckles -> Float
fistDamageChance knuckles
| calluses == 5 = 0
| otherwise = baseChance - (0.01 * fromIntegral calluses)
where
baseChance = 0.08
calluses = knuckles ^. knuckleCalluses
-- | Damage the knuckles by a level (capping at the max knuckle damage)
damageKnuckles :: Knuckles -> Knuckles
damageKnuckles = execState $ do
knuckleDamage %= min 5 . succ
ticksSinceDamaged .= 0
-- | Damage taken when fistfighting and 'fistDamageChance' has occurred
fistfightingDamage :: Knuckles -> Hitpoints
fistfightingDamage knuckles
| knuckles ^. knuckleDamage == 5 = 2
| otherwise = 1
stepKnuckles :: Ticks -> Knuckles -> AppM Knuckles
stepKnuckles ticks = execStateT . whenM (uses knuckleDamage (> 0)) $ do
ticksSinceDamaged += ticks
whenM (uses ticksSinceDamaged (>= 2000)) $ do
dam <- knuckleDamage <<.= 0
knuckleCalluses %= min 5 . (+ dam)
ticksSinceDamaged .= 0
lift $ say_ ["character", "body", "knuckles", "calluses"]
-- | Status of the character's body
data Body = Body
{ _knuckles :: !Knuckles
}
deriving stock (Show, Eq, Ord, Generic)
deriving anyclass (NFData, CoArbitrary, Function)
deriving Arbitrary via GenericArbitrary Body
deriving (ToJSON, FromJSON)
via WithOptions '[ FieldLabelModifier '[Drop 1] ]
Body
makeLenses ''Body
initialBody :: Body
initialBody = Body { _knuckles = mempty }
--------------------------------------------------------------------------------
data Character = Character
{ _inventory :: !Inventory
, _characterName :: !(Maybe Text)
, _characterHitpoints' :: !Double
, _speed :: !TicksPerTile
, _body :: !Body
}
deriving stock (Show, Eq, Ord, Generic)
deriving anyclass (NFData, CoArbitrary, Function)
deriving (ToJSON, FromJSON)
via WithOptions '[ FieldLabelModifier '[Drop 1] ]
Character
makeLenses ''Character
characterHitpoints :: Character -> Hitpoints
characterHitpoints = views characterHitpoints' floor
scrollOffset :: Int
scrollOffset = 5
instance Draw Character where
draw _ = visibleRegion rloc rreg $ str "@"
where
rloc = Location (negate scrollOffset, negate scrollOffset)
rreg = (2 * scrollOffset, 2 * scrollOffset)
drawPriority = const maxBound -- Character should always be on top, for now
instance Brain Character where
step ticks = execStateT $ do
positioned . characterHitpoints' %= \hp ->
if hp > fromIntegral initialHitpoints
then hp
else hp + hitpointRecoveryRate |*| ticks
modifyKL (positioned . body . knuckles) $ lift . stepKnuckles ticks
instance Entity Character where
description _ = "yourself"
entityChar _ = "@"
instance Arbitrary Character where
arbitrary = genericArbitrary
initialHitpoints :: Hitpoints
initialHitpoints = 10
hitpointRecoveryRate :: Double `Per` Ticks
hitpointRecoveryRate = 1.0 / (15 * coerce defaultSpeed)
defaultSpeed :: TicksPerTile
defaultSpeed = 100
mkCharacter :: Character
mkCharacter = Character
{ _inventory = mempty
, _characterName = Nothing
, _characterHitpoints' = fromIntegral initialHitpoints
, _speed = defaultSpeed
, _body = initialBody
}
defaultCharacterDamage :: Hitpoints
defaultCharacterDamage = 1
-- | Returns the damage that the character currently does with an attack
-- TODO use double-handed/left-hand/right-hand here
characterDamage :: Character -> Hitpoints
characterDamage
= fromMaybe defaultCharacterDamage
. preview (inventory . wielded . wieldedItems . wieldableItem . Raw.damage)
-- | Is the character fully healed up to or past their initial hitpoints?
isFullyHealed :: Character -> Bool
isFullyHealed = (>= initialHitpoints) . characterHitpoints
-- | Is the character dead?
isDead :: Character -> Bool
isDead = (== 0) . characterHitpoints
pickUpItem :: Item -> Character -> Character
pickUpItem it = inventory . backpack %~ (it <|)
damage :: Hitpoints -> Character -> Character
damage (fromIntegral -> amount) = characterHitpoints' %~ \case
n | n <= amount -> 0
| otherwise -> n - amount
{-# ANN module ("Hlint: ignore Use newtype instead of data" :: String) #-}