{-# 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))
