{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-} -- | Abilities of items, actors and factions. module Game.LambdaHack.Definition.Ability ( Skill(..), Skills, Flag(..), Flags(..), Tactic(..), EqpSlot(..) , getSk, addSk, checkFl, skillsToList , zeroSkills, addSkills, sumScaledSkills , nameTactic, describeTactic, tacticSkills , blockOnly, meleeAdjacent, meleeAndRanged, ignoreItems #ifdef EXPOSE_INTERNAL -- * Internal operations , compactSkills, 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 | SkSmell | SkShine | SkNocto | SkHearing | SkAggression | SkOdor deriving (Show, Eq, Ord, Generic, Enum, Bounded) -- | 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, Generic, 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) | Meleeable -- ^ AI and UI flag: consider meleeing with | 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; -- this differs from belonging to the @condition@ group, -- which doesn't guarantee display as a condition, -- but governs removal by items that drop @condition@ | Unique -- ^ at most one copy can ever be generated | 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 | MinorEffects -- ^ override: the effects on this item are considered -- minor and so not causing identification on use, -- and so this item will identify on pick-up deriving (Show, Eq, Ord, Generic, Enum, Bounded) newtype Flags = Flags {flags :: ES.EnumSet Flag} deriving (Show, Eq, Ord, Generic, Hashable, Binary) -- | Tactic of non-leader actors. Apart of determining AI operation, -- each tactic implies a skill modifier, that is added to the non-leader skills -- defined in @fskillsOther@ field of @Player@. data Tactic = 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, Ord, Enum, Bounded, Generic) instance Binary Tactic instance Hashable Tactic -- | 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 compactSkills :: EM.EnumMap Skill Int -> EM.EnumMap Skill Int compactSkills = EM.filter (/= 0) addSkills :: Skills -> Skills -> Skills addSkills (Skills sk1) (Skills sk2) = Skills $ compactSkills $ EM.unionWith (+) sk1 sk2 scaleSkills :: Int -> EM.EnumMap Skill Int -> EM.EnumMap Skill Int scaleSkills n = EM.map (n *) sumScaledSkills :: [(Skills, Int)] -> Skills sumScaledSkills l = Skills $ compactSkills $ EM.unionsWith (+) $ map (\(Skills sk, k) -> scaleSkills k sk) l nameTactic :: Tactic -> Text nameTactic TExplore = "explore" nameTactic TFollow = "follow freely" nameTactic TFollowNoItems = "follow only" nameTactic TMeleeAndRanged = "fight only" nameTactic TMeleeAdjacent = "melee only" nameTactic TBlock = "block only" nameTactic TRoam = "roam freely" nameTactic TPatrol = "patrol area" describeTactic :: Tactic -> Text describeTactic TExplore = "investigate unknown positions, chase targets" describeTactic TFollow = "follow leader's target or position, grab items" describeTactic TFollowNoItems = "follow leader's target or position, ignore items" describeTactic TMeleeAndRanged = "engage in both melee and ranged combat, don't move" describeTactic TMeleeAdjacent = "engage exclusively in melee, don't move" describeTactic TBlock = "block and wait, don't move" describeTactic TRoam = "move freely, chase targets" describeTactic TPatrol = "find and patrol an area (WIP)" tacticSkills :: Tactic -> Skills tacticSkills TExplore = zeroSkills tacticSkills TFollow = zeroSkills tacticSkills TFollowNoItems = ignoreItems tacticSkills TMeleeAndRanged = meleeAndRanged tacticSkills TMeleeAdjacent = meleeAdjacent tacticSkills TBlock = blockOnly tacticSkills TRoam = zeroSkills tacticSkills 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))