{-# LANGUAGE DeriveGeneric #-} -- | The type of item aspects and its operations. module Game.LambdaHack.Common.ItemAspect ( AspectRecord(..), KindMean(..) , emptyAspectRecord, addMeanAspect, castAspect, aspectsRandom , aspectRecordToList, rollAspectRecord, getSkill, checkFlag, meanAspect , onlyMinorEffects, itemTrajectory, totalRange, isHumanTrinket , goesIntoEqp, loreFromContainer #ifdef EXPOSE_INTERNAL -- * Internal operations , ceilingMeanDice #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Control.Monad.Trans.State.Strict as St import Data.Binary import qualified Data.EnumSet as ES import Data.Hashable (Hashable) import qualified Data.Text as T import GHC.Generics (Generic) import qualified System.Random.SplitMix32 as SM import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs -- | Record of skills conferred by an item as well as of item flags -- and other item aspects. data AspectRecord = AspectRecord { aTimeout :: Int , aSkills :: Ability.Skills , aFlags :: Ability.Flags , aELabel :: Text , aToThrow :: IK.ThrowMod , aPresentAs :: Maybe (GroupName IK.ItemKind) , aEqpSlot :: Maybe Ability.EqpSlot } deriving (Show, Eq, Ord, Generic) instance Hashable AspectRecord instance Binary AspectRecord -- | Partial information about an item, deduced from its item kind. -- These are assigned to each 'IK.ItemKind'. The @kmConst@ flag says whether -- the item's aspect record is constant rather than random or dependent -- on item creation dungeon level. data KindMean = KindMean { kmConst :: Bool -- ^ whether the item doesn't need second identification , kmMean :: AspectRecord -- ^ mean value of item's possible aspect records } deriving (Show, Eq, Ord) emptyAspectRecord :: AspectRecord emptyAspectRecord = AspectRecord { aTimeout = 0 , aSkills = Ability.zeroSkills , aFlags = Ability.Flags ES.empty , aELabel = "" , aToThrow = IK.ThrowMod 100 100 1 , aPresentAs = Nothing , aEqpSlot = Nothing } castAspect :: Dice.AbsDepth -> Dice.AbsDepth -> AspectRecord -> IK.Aspect -> Rnd AspectRecord castAspect !ldepth !totalDepth !ar !asp = case asp of IK.Timeout d -> do n <- castDice ldepth totalDepth d return $! assert (aTimeout ar == 0) $ ar {aTimeout = n} IK.AddSkill sk d -> do n <- castDice ldepth totalDepth d return $! if n /= 0 then ar {aSkills = Ability.addSk sk n (aSkills ar)} else ar IK.SetFlag feat -> return $! ar {aFlags = Ability.Flags $ ES.insert feat (Ability.flags $ aFlags ar)} IK.ELabel t -> return $! ar {aELabel = t} IK.ToThrow tt -> return $! ar {aToThrow = tt} IK.PresentAs ha -> return $! ar {aPresentAs = Just ha} IK.EqpSlot slot -> return $! ar {aEqpSlot = Just slot} IK.Odds d aspects1 aspects2 -> do pick1 <- oddsDice ldepth totalDepth d foldlM' (castAspect ldepth totalDepth) ar $ if pick1 then aspects1 else aspects2 -- If @False@, aspects of this kind are most probably fixed, not random -- nor dependent on dungeon level where the item is created. aspectsRandom :: [IK.Aspect] -> Bool aspectsRandom ass = let rollM depth = foldlM' (castAspect (Dice.AbsDepth depth) (Dice.AbsDepth 10)) emptyAspectRecord ass gen = SM.mkSMGen 0 (ar0, gen0) = St.runState (rollM 0) gen (ar1, gen1) = St.runState (rollM 10) gen0 in show gen /= show gen0 || show gen /= show gen1 || ar0 /= ar1 addMeanAspect :: AspectRecord -> IK.Aspect -> AspectRecord addMeanAspect !ar !asp = case asp of IK.Timeout d -> let n = ceilingMeanDice d in assert (aTimeout ar == 0) $ ar {aTimeout = n} IK.AddSkill sk d -> let n = ceilingMeanDice d in if n /= 0 then ar {aSkills = Ability.addSk sk n (aSkills ar)} else ar IK.SetFlag feat -> ar {aFlags = Ability.Flags $ ES.insert feat (Ability.flags $ aFlags ar)} IK.ELabel t -> ar {aELabel = t} IK.ToThrow tt -> ar {aToThrow = tt} IK.PresentAs ha -> ar {aPresentAs = Just ha} IK.EqpSlot slot -> ar {aEqpSlot = Just slot} IK.Odds{} -> ar -- can't tell, especially since we don't know the level ceilingMeanDice :: Dice.Dice -> Int ceilingMeanDice d = ceiling $ Dice.meanDice d aspectRecordToList :: AspectRecord -> [IK.Aspect] aspectRecordToList AspectRecord{..} = [IK.Timeout $ Dice.intToDice aTimeout | aTimeout /= 0] ++ [ IK.AddSkill sk $ Dice.intToDice n | (sk, n) <- Ability.skillsToList aSkills ] ++ [IK.SetFlag feat | feat <- ES.elems $ Ability.flags aFlags] ++ [IK.ELabel aELabel | not $ T.null aELabel] ++ [IK.ToThrow aToThrow | aToThrow /= IK.ThrowMod 100 100 1] ++ maybe [] (\ha -> [IK.PresentAs ha]) aPresentAs ++ maybe [] (\slot -> [IK.EqpSlot slot]) aEqpSlot rollAspectRecord :: [IK.Aspect] -> Dice.AbsDepth -> Dice.AbsDepth -> Rnd AspectRecord rollAspectRecord ass ldepth totalDepth = foldlM' (castAspect ldepth totalDepth) emptyAspectRecord ass getSkill :: Ability.Skill -> AspectRecord -> Int {-# INLINE getSkill #-} getSkill sk ar = Ability.getSk sk $ aSkills ar checkFlag :: Ability.Flag -> AspectRecord -> Bool {-# INLINE checkFlag #-} checkFlag flag ar = Ability.checkFl flag (aFlags ar) meanAspect :: IK.ItemKind -> AspectRecord meanAspect kind = foldl' addMeanAspect emptyAspectRecord (IK.iaspects kind) -- Kinetic damage is not considered major effect, even though it -- identifies an item, when one hits with it. However, it's tedious -- to wait for weapon identification until first hit and also -- if a weapon is periodically activated, the kinetic damage would not apply, -- so we'd need special cases that force identification or warn -- or here not consider kinetic damage a major effect if item is periodic. -- So we opt for KISS and identify effect-less weapons at pick-up, -- not at first hit. onlyMinorEffects :: AspectRecord -> IK.ItemKind -> Bool onlyMinorEffects ar kind = checkFlag Ability.MinorEffects ar -- override || all IK.alwaysDudEffect (IK.ieffects kind) -- exhibits no major effects itemTrajectory :: AspectRecord -> IK.ItemKind -> [Point] -> ([Vector], (Speed, Int)) itemTrajectory ar itemKind path = let IK.ThrowMod{..} = aToThrow ar in computeTrajectory (IK.iweight itemKind) throwVelocity throwLinger path totalRange :: AspectRecord -> IK.ItemKind -> Int totalRange ar itemKind = snd $ snd $ itemTrajectory ar itemKind [] isHumanTrinket :: IK.ItemKind -> Bool isHumanTrinket itemKind = maybe False (> 0) $ lookup IK.VALUABLE $ IK.ifreq itemKind -- risk from treasure hunters goesIntoEqp :: AspectRecord -> Bool goesIntoEqp ar = checkFlag Ability.Equipable ar || checkFlag Ability.Meleeable ar loreFromContainer :: AspectRecord -> Container -> SLore loreFromContainer arItem c = case c of CFloor{} -> SItem CEmbed{} -> SEmbed CActor _ store -> if | checkFlag Ability.Blast arItem -> SBlast | checkFlag Ability.Condition arItem -> SCondition | otherwise -> loreFromMode $ MStore store CTrunk{} -> if checkFlag Ability.Blast arItem then SBlast else STrunk