Implement a new status effect system

This commit is contained in:
Emi Simpson 2023-12-08 09:37:40 -05:00
parent a6a7473f05
commit a555ed4ea0
Signed by: Emi
GPG key ID: A12F2C2FFDC3D847

View file

@ -65,6 +65,7 @@ module GameModel
, clearUpToNTokens , clearUpToNTokens
, getSpeed , getSpeed
, getDefense , getDefense
, statusEffects
, CharacterIdentifier , CharacterIdentifier
, owner , owner
, ownerL , ownerL
@ -128,6 +129,8 @@ module GameModel
, Hook(..) , Hook(..)
, hookTriggerL , hookTriggerL
, hookEffectL , hookEffectL
, hookToFunc
, hooksToFunc
, Modifier(..) , Modifier(..)
, Trait(..) , Trait(..)
, traitNameL , traitNameL
@ -135,6 +138,7 @@ module GameModel
, traitModifiersL , traitModifiersL
, computeStat_ , computeStat_
, runHooks_ , runHooks_
, StatusEffect(..)
) where ) where
import Util (toMaybe, dup, secondClassLensNames, (??)) import Util (toMaybe, dup, secondClassLensNames, (??))
@ -561,6 +565,18 @@ data Hook h = Hook
, hookEffect :: BoardState -> h , hookEffect :: BoardState -> h
} }
-- | Evaluate a hook for a given trigger
--
-- If the trigger does not apply to this hook, then `mempty` is returned.
hookToFunc :: Monoid h => Hook h -> Trigger -> BoardState -> h
hookToFunc (Hook {..}) trigger
| hookTrigger == trigger = hookEffect
| otherwise = mempty
-- | A version of `hookToFunc` which evaluates several hooks at once
hooksToFunc :: Monoid h => [Hook h] -> Trigger -> BoardState -> h
hooksToFunc = mconcat . fmap hookToFunc
-- | A modifier which conditionally affects thet value of some stat for a unit -- | A modifier which conditionally affects thet value of some stat for a unit
data Modifier = forall a. Monoid a => Modifier data Modifier = forall a. Monoid a => Modifier
{ modifierStat :: Stat a -- ^ Which stat this modifier effects { modifierStat :: Stat a -- ^ Which stat this modifier effects
@ -582,6 +598,32 @@ data Trait = Trait
, traitModifiers :: [Modifier] -- ^ Any modifiers this trait imposes , traitModifiers :: [Modifier] -- ^ Any modifiers this trait imposes
} }
-- | Some inflicted/bestowed trait that applies to a character
--
-- Some status effects track internal state (here, the type variable a), which affect
-- either what the effect does or how long it exists.
--
-- A couple examples of status effects:
-- - Gargamox's Leech's "superate", which adds a Deathburst to a unit, and lasts for the
-- whole game
-- - C.A.R.C.A.S.'s Ammo Goblin's "bone shards" which causes the unit to take damage when
-- pushed or pulled, and expires after dealing three damage or the end of the goblin's
-- next turn.
data StatusEffect = forall a. StatusEffect
{ seName :: String -- ^ The name of the effect
, seHooks :: [Hook (a -> CharacterIdentifier -> [Effect])] -- ^ Triggers which produce effects
, seModifiers :: [Modifier] -- ^ Any stat modifiers inflicted by this effect
-- | Triggers which update the internal state or remove the effect
--
-- Returning `Just` updates the internal state, and returning `Nothing` removes the
-- effect completely.
, seUpdate :: [Hook (a -> CharacterIdentifier -> Maybe a)]
-- | Whatever internal state is needed by this effect
, seState :: a
}
instance Show BaseStats where instance Show BaseStats where
show stats = "===== " ++ name stats ++ " =====\nMV: " ++ show (mov stats) ++ " DF: " ++ show (df stats) ++ "+ ARM: " ++ show (arm stats) show stats = "===== " ++ name stats ++ " =====\nMV: " ++ show (mov stats) ++ " DF: " ++ show (df stats) ++ "+ ARM: " ++ show (arm stats)
@ -650,6 +692,7 @@ data Character = Character
{ _baseStats :: BaseStats { _baseStats :: BaseStats
, _movedThisRound :: Bool , _movedThisRound :: Bool
, _tokenCount :: TokenCount , _tokenCount :: TokenCount
, _statusEffects :: [StatusEffect]
} }
instance Show Character where instance Show Character where
@ -697,8 +740,26 @@ makeLensesWith secondClassLensNames ''Hook
makeLensesWith secondClassLensNames ''Trait makeLensesWith secondClassLensNames ''Trait
instance Functor Hook where
fmap f hook = hook & hookEffectL . mapped %~ f
v <$ Hook {..} = Hook {hookEffect = const v, ..}
-- | Extract the hooks from a status effect, hiding internal state
seGetHooks :: StatusEffect -> [Hook (CharacterIdentifier -> [Effect])]
seGetHooks (StatusEffect {..}) = seHooks <&> (<&> ($ seState))
seDoUpdate :: Trigger -> BoardState -> CharacterIdentifier -> StatusEffect -> Maybe StatusEffect
seDoUpdate trigger board cid (StatusEffect {..}) = case relevantUpdates of
[] -> Just $ StatusEffect {..}
Nothing : _ -> Nothing
Just newState : _ -> Just $ StatusEffect {seState = newState, ..}
where
seUpdate' = seUpdate & each . mapped %~ (($ cid) . ($ seState))
relevantHooks = seUpdate' ^.. each . filtered ((trigger==) . hookTrigger) . hookEffectL
relevantUpdates = sequence relevantHooks board
instantiate :: BaseStats -> Character instantiate :: BaseStats -> Character
instantiate stats = Character stats False noTokens & tokenCount . ofToken Health .~ hp stats instantiate stats = Character stats False noTokens [] & tokenCount . ofToken Health .~ hp stats
clearUpToNTokens :: (Num a, Ord a) => Token a -> a -> Character -> Character clearUpToNTokens :: (Num a, Ord a) => Token a -> a -> Character -> Character
clearUpToNTokens t n = tokenCount . ofToken t %~ clearUpToN n clearUpToNTokens t n = tokenCount . ofToken t %~ clearUpToN n
@ -749,24 +810,28 @@ computeStat_ globalModifiers board cid stat = case allStatsAreMonoids stat of
-- | Activate all of the relevant hooks on the board -- | Activate all of the relevant hooks on the board
-- --
-- Searches through all hooks in traits and global hooks, accumulates all their effects, -- Searches through all hooks in traits, statuse effects and global hooks, accumulates all
-- then adds all the resulting effects to the effect stack. Similar to `computeStat_`, -- their effects, then adds all the resulting effects to the effect stack. Then, all
-- this is additionall searches so called global hooks, but only recognizes global hooks -- necessary status effect updates are applied. Similar to `computeStat_`, this is
-- passed as an argument. -- additionall searches so called global hooks, but only recognizes global hooks passed as
-- an argument.
runHooks_ :: [Hook [Effect]] -> Trigger -> BoardState -> BoardState runHooks_ :: [Hook [Effect]] -> Trigger -> BoardState -> BoardState
runHooks_ globalHooks trigger board = board <++ characterEffects <++ globalEffects runHooks_ globalHooks trigger board = boardWithUpdates <++ allEffects
where where
filterActivatedL :: Traversal' (Hook a) (Hook a) unitAllEffectHooks :: (CharacterIdentifier, Character) -> [Hook [Effect]]
filterActivatedL = filtered ((==trigger) . hookTrigger) unitAllEffectHooks (cid, c) = allHooks <&> (?? cid)
unitHooksL :: SimpleFold Character (BoardState -> CharacterIdentifier -> [Effect]) where
unitHooksL = baseStats . traitsL . each . traitHooksL . each . filterActivatedL . hookEffectL statusHooks :: [Hook (CharacterIdentifier -> [Effect])]
charRunHooks :: (CharacterIdentifier, Character) -> [Effect] statusHooks = c ^.. statusEffects . each . to seGetHooks . each
charRunHooks (cid, c) = classHooks :: [Hook (CharacterIdentifier -> [Effect])]
(c ^. unitHooksL) board cid classHooks = c ^.. baseStats . traitsL . each . traitHooksL . each
characterEffects :: [Effect] allHooks = statusHooks ++ classHooks
characterEffects = board ^. enumerateUnits . to charRunHooks unitUpdateStatuses :: (CharacterIdentifier, Character) -> (CharacterIdentifier, Character)
globalEffects :: [Effect] unitUpdateStatuses i@(cid, _) = i & _2 . statusEffects %~ mapMaybe (seDoUpdate trigger board cid)
globalEffects = (globalHooks ^. each . filterActivatedL . hookEffectL) board unitHooks = board ^.. enumerateUnits . to unitAllEffectHooks . each
allHooks = unitHooks ++ globalHooks
allEffects = hooksToFunc allHooks trigger board
boardWithUpdates = board & enumerateUnits %~ unitUpdateStatuses
instance Show BoardState where instance Show BoardState where
show board = join (intersperse "\n" showTiles) ++ '\n':showRound ++ "\n" ++ showCharacters show board = join (intersperse "\n" showTiles) ++ '\n':showRound ++ "\n" ++ showCharacters