LambdaHack-0.11.0.0: definition-src/Game/LambdaHack/Definition/Ability.hs
{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-}
-- | Abilities of items, actors and factions.
module Game.LambdaHack.Definition.Ability
( Skill(..), Skills, Flag(..), ActivationFlag(..), Flags(..)
, Doctrine(..), EqpSlot(..)
, getSk, addSk, checkFl, skillsToList
, zeroSkills, addSkills, sumScaledSkills
, nameDoctrine, describeDoctrine, doctrineSkills
, blockOnly, meleeAdjacent, meleeAndRanged, ignoreItems
#ifdef EXPOSE_INTERNAL
-- * Internal operations
, scaleSkills
#endif
) where
import Prelude ()
import Game.LambdaHack.Core.Prelude
import Data.Binary
import qualified Data.EnumMap.Strict as EM
import qualified Data.EnumSet as ES
import Data.Hashable (Hashable)
import GHC.Generics (Generic)
-- | Actor and faction skills. They are a subset of actor aspects.
-- See 'Game.LambdaHack.Client.UI.EffectDescription.skillDesc'
-- for documentation.
data Skill =
-- Stats, that is skills affecting permitted actions.
SkMove
| SkMelee
| SkDisplace
| SkAlter
| SkWait
| SkMoveItem
| SkProject
| SkApply
-- Assorted skills.
| SkSwimming
| SkFlying
| SkHurtMelee
| SkArmorMelee
| SkArmorRanged
| SkMaxHP
| SkMaxCalm
| SkSpeed
| SkSight -- ^ FOV radius, where 1 means a single tile FOV area
| SkSmell
| SkShine
| SkNocto
| SkHearing
| SkAggression
| SkOdor
| SkDeflectRanged -- ^ intended to reflect how many items granting complete
-- invulnerability are among organs and equipment;
-- this is not strength of deflection nor duration, etc.
| SkDeflectMelee -- ^ see above
deriving (Show, Eq, Enum, Bounded, Generic)
-- | Strength of particular skills. This is cumulative from actor
-- organs and equipment and so pertain to an actor as well as to items.
--
-- This representation is sparse, so better than a record when there are more
-- item kinds (with few skills) than actors (with many skills),
-- especially if the number of skills grows as the engine is developed.
-- It's also easier to code and maintain.
--
-- The tree is by construction sparse, so the derived equality is semantical.
newtype Skills = Skills {skills :: EM.EnumMap Skill Int}
deriving (Show, Eq, Ord, Hashable, Binary)
-- | Item flag aspects.
data Flag =
Fragile -- ^ as a projectile, break at target tile, even if no hit;
-- also, at each periodic activation a copy is destroyed
-- and all other copies require full cooldown (timeout)
| Lobable -- ^ drop at target tile, even if no hit
| Durable -- ^ don't break even when hitting or applying
| Equipable -- ^ AI and UI flag: consider equipping (may or may not
-- have 'EqpSlot', e.g., if the benefit is periodic)
| Benign -- ^ AI and UI flag: the item is not meant to harm
| Precious -- ^ AI and UI flag: don't risk identifying by use;
-- also, can't throw or apply if not calm enough;
-- also may be used for UI flavour or AI hints
| Blast -- ^ the item is an explosion blast particle
| Condition -- ^ item is a condition (buff or de-buff) of an actor
-- and is displayed as such, not activated at death;
-- this differs from belonging to the @CONDITION@ group,
-- which doesn't guarantee any behaviour or display,
-- but governs removal by items that drop @CONDITION@
| Unique -- ^ at most one copy can ever be generated
| MetaGame -- ^ once identified, the item is known until savefile deleted
| MinorEffects -- ^ override: the effects on this item are considered
-- minor and so possibly not causing identification on use,
-- and so this item will identify on pick-up
| MinorAspects -- ^ override: don't show question marks by weapons in HUD
-- even when unidentified item with this flag equipped
| -- The flags below specify all conditions under which the item activates,
-- charges permitting, in addition to universal conditions, which are
-- hitting an actor as projectiles and being explicitly triggered
-- by an actor (item destruction and combining only pertain
-- to explicitly listed effects).
Meleeable -- ^ meleeing with the item is permitted and so the item
-- activates when meleed with
| Periodic -- ^ at most one of any copies without cooldown (timeout)
-- activates each turn; the cooldown required after
-- activation is specified in @Timeout@ (or is zero);
-- the initial cooldown can also be specified
-- as @TimerDice@ in @CreateItem@ effect; uniquely, this
-- activation never destroys a copy, unless item is fragile;
-- all this happens only for items in equipment or organs;
-- kinetic damage is not applied
| UnderRanged -- ^ activates when non-projectile actor with this item
-- as equipment or organ is under ranged attack;
-- kinetic damage is not applied
| UnderMelee -- ^ activates when non-projectile actor with this item
-- as equipment or organ is under melee attack;
-- kinetic damage is not applied
deriving (Show, Eq, Enum, Bounded, Generic)
-- | These flags correspond to the last cases of @Flag@ and addtionally
-- to all the universal circumstances of item activation,
-- under which every item activates (even if vacuusly).
data ActivationFlag =
ActivationMeleeable
| ActivationPeriodic
| ActivationUnderRanged
| ActivationUnderMelee
| -- | From here on, all items affected regardless of their `Flag` content.
ActivationProjectile
| ActivationTrigger
| ActivationOnSmash
| ActivationOnCombine
| ActivationEmbed
| ActivationConsume
deriving (Show, Eq)
newtype Flags = Flags {flags :: ES.EnumSet Flag}
deriving (Show, Eq, Ord, Hashable, Binary)
-- | Doctrine of non-leader actors. Apart of determining AI operation,
-- each doctrine implies a skill modifier, that is added to the non-leader
-- skills defined in @fskillsOther@ field of @FactionKind@.
data Doctrine =
TExplore -- ^ if enemy nearby, attack, if no items, etc., explore unknown
| TFollow -- ^ always follow leader's target or his position if no target
| TFollowNoItems -- ^ follow but don't do any item management nor use
| TMeleeAndRanged -- ^ only melee and do ranged combat
| TMeleeAdjacent -- ^ only melee (or wait)
| TBlock -- ^ always only wait, even if enemy in melee range
| TRoam -- ^ if enemy nearby, attack, if no items, etc., roam randomly
| TPatrol -- ^ find an open and uncrowded area, patrol it according
-- to sight radius and fallback temporarily to @TRoam@
-- when enemy is seen by the faction and is within
-- the actor's sight radius
deriving (Show, Eq, Enum, Bounded, Generic)
instance Binary Doctrine
instance Hashable Doctrine
-- | AI and UI hints about the role of the item.
data EqpSlot =
EqpSlotMove
| EqpSlotMelee
| EqpSlotDisplace
| EqpSlotAlter
| EqpSlotWait
| EqpSlotMoveItem
| EqpSlotProject
| EqpSlotApply
| EqpSlotSwimming
| EqpSlotFlying
| EqpSlotHurtMelee
| EqpSlotArmorMelee
| EqpSlotArmorRanged
| EqpSlotMaxHP
| EqpSlotSpeed
| EqpSlotSight
| EqpSlotShine
| EqpSlotMiscBonus
| EqpSlotWeaponFast
| EqpSlotWeaponBig
deriving (Show, Eq, Ord, Enum, Bounded, Generic)
instance Binary Skill where
put = putWord8 . toEnum . fromEnum
get = fmap (toEnum . fromEnum) getWord8
instance Binary Flag where
put = putWord8 . toEnum . fromEnum
get = fmap (toEnum . fromEnum) getWord8
instance Binary EqpSlot where
put = putWord8 . toEnum . fromEnum
get = fmap (toEnum . fromEnum) getWord8
instance Hashable Skill
instance Hashable Flag
instance Hashable EqpSlot
getSk :: Skill -> Skills -> Int
{-# INLINE getSk #-}
getSk sk (Skills skills) = EM.findWithDefault 0 sk skills
addSk :: Skill -> Int -> Skills -> Skills
addSk sk n = addSkills (Skills $ EM.singleton sk n)
checkFl :: Flag -> Flags -> Bool
{-# INLINE checkFl #-}
checkFl flag (Flags flags) = flag `ES.member` flags
skillsToList :: Skills -> [(Skill, Int)]
skillsToList (Skills sk) = EM.assocs sk
zeroSkills :: Skills
zeroSkills = Skills EM.empty
-- This avoids costly compaction (required for Eq) even in case of adding
-- empty skills, etc. This function is used a lot.
addSkills :: Skills -> Skills -> Skills
addSkills (Skills sk1) (Skills sk2) =
let combine _ s1 s2 = case s1 + s2 of
0 -> Nothing
s -> Just s
in Skills $ EM.mergeWithKey combine id id sk1 sk2
scaleSkills :: (Skills, Int) -> Skills
scaleSkills (_, 0) = zeroSkills
scaleSkills (Skills sk, n) = Skills $ EM.map (n *) sk
sumScaledSkills :: [(Skills, Int)] -> Skills
sumScaledSkills = foldr (addSkills . scaleSkills) zeroSkills
nameDoctrine :: Doctrine -> Text
nameDoctrine TExplore = "explore"
nameDoctrine TFollow = "follow freely"
nameDoctrine TFollowNoItems = "follow only"
nameDoctrine TMeleeAndRanged = "fight only"
nameDoctrine TMeleeAdjacent = "melee only"
nameDoctrine TBlock = "block only"
nameDoctrine TRoam = "roam freely"
nameDoctrine TPatrol = "patrol area"
describeDoctrine :: Doctrine -> Text
describeDoctrine TExplore = "investigate unknown positions, chase targets"
describeDoctrine TFollow = "follow pointman's target or position, grab items"
describeDoctrine TFollowNoItems =
"follow pointman's target or position, ignore items"
describeDoctrine TMeleeAndRanged =
"engage in both melee and ranged combat, don't move"
describeDoctrine TMeleeAdjacent = "engage exclusively in melee, don't move"
describeDoctrine TBlock = "block and wait, don't move"
describeDoctrine TRoam = "move freely, chase targets"
describeDoctrine TPatrol = "find and patrol an area"
doctrineSkills :: Doctrine -> Skills
doctrineSkills TExplore = zeroSkills
doctrineSkills TFollow = zeroSkills
doctrineSkills TFollowNoItems = ignoreItems
doctrineSkills TMeleeAndRanged = meleeAndRanged
doctrineSkills TMeleeAdjacent = meleeAdjacent
doctrineSkills TBlock = blockOnly
doctrineSkills TRoam = zeroSkills
doctrineSkills TPatrol = zeroSkills
minusTen, blockOnly, meleeAdjacent, meleeAndRanged, ignoreItems :: Skills
-- To make sure only a lot of weak items can override move-only-leader, etc.
minusTen = Skills $ EM.fromDistinctAscList
$ zip [SkMove .. SkApply] (repeat (-10))
blockOnly = Skills $ EM.delete SkWait $ skills minusTen
meleeAdjacent = Skills $ EM.delete SkMelee $ skills blockOnly
-- Melee and reaction fire.
meleeAndRanged = Skills $ EM.delete SkProject $ skills meleeAdjacent
ignoreItems = Skills $ EM.fromList
$ zip [SkMoveItem, SkProject, SkApply] (repeat (-10))