about summary refs log tree commit diff
path: root/users/grfn/xanthous/src/Xanthous/Entities/Character.hs
{-# 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) #-}