content: {-# LANGUAGE DeriveGeneric #-}
  -- | The type of kinds of weapons, treasure, organs, blasts, etc.
  module Game.LambdaHack.Content.ItemKind
    ( pattern CONDITION, pattern COMMON_ITEM, pattern S_BONUS_HP, pattern S_BRACED, pattern S_ASLEEP, pattern S_IMPRESSED, pattern S_CURRENCY, pattern MOBILE
    , pattern CRAWL_ITEM, pattern TREASURE, pattern ANY_SCROLL, pattern ANY_GLASS, pattern ANY_POTION, pattern ANY_FLASK, pattern EXPLOSIVE, pattern ANY_JEWELRY, pattern S_SINGLE_SPARK, pattern S_SPARK, pattern S_FRAGRANCE
    , pattern HORROR, pattern VALUABLE, pattern UNREPORTED_INVENTORY, pattern AQUATIC
    , ItemKind(..), makeData
    , Aspect(..), Effect(..), Condition(..), DetectKind(..)
    , TimerDice, ThrowMod(..)
    , ItemSymbolsUsedInEngine(..), emptyItemSymbolsUsedInEngine
    , boostItemKindList, forApplyEffect, forDamageEffect, isDamagingKind
    , strengthOnCombine, strengthOnSmash, getDropOrgans
    , getMandatoryPresentAsFromKind, isEffEscape, isEffEscapeOrAscend
    , timeoutAspect, orEffect, onSmashEffect, onCombineEffect, alwaysDudEffect
    , damageUsefulness, verbMsgNoLonger, verbMsgLess, toVelocity, toLinger
    , timerNone, isTimerNone, foldTimer, toOrganBad, toOrganGood, toOrganNoTimer
    , validateSingle
    , mandatoryGroups, mandatoryGroupsSingleton
  #ifdef EXPOSE_INTERNAL
      -- * Internal operations
    , boostItemKind, onSmashOrCombineEffect
    , validateAll, validateDups, validateDamage
  #endif
    ) where
  
  import Prelude ()
  
  import Game.LambdaHack.Core.Prelude
  
  import           Data.Binary
  import           Data.Hashable (Hashable)
  import qualified Data.Text as T
  import           GHC.Generics (Generic)
  import qualified System.Random.SplitMix32 as SM
  
  import qualified Game.LambdaHack.Core.Dice as Dice
  import           Game.LambdaHack.Core.Random (nextRandom)
  import qualified Game.LambdaHack.Definition.Ability as Ability
  import           Game.LambdaHack.Definition.ContentData
  import           Game.LambdaHack.Definition.Defs
  import           Game.LambdaHack.Definition.DefsInternal
  import           Game.LambdaHack.Definition.Flavour
  
  -- * Mandatory item groups
  
  mandatoryGroupsSingleton :: [GroupName ItemKind]
  mandatoryGroupsSingleton =
         [S_BONUS_HP, S_BRACED, S_ASLEEP, S_IMPRESSED, S_CURRENCY]
  
  pattern S_BONUS_HP, S_BRACED, S_ASLEEP, S_IMPRESSED, S_CURRENCY :: GroupName ItemKind
  
  mandatoryGroups :: [GroupName ItemKind]
  mandatoryGroups =
         [CONDITION, COMMON_ITEM, MOBILE]
  
  pattern CONDITION, COMMON_ITEM, MOBILE :: GroupName ItemKind
  
  -- From Preferences.hs
  
  pattern CONDITION = GroupName "condition"
  pattern COMMON_ITEM = GroupName "common item"
  
  -- Assorted
  
  pattern S_BONUS_HP = GroupName "bonus HP"
  pattern S_BRACED = GroupName "braced"
  pattern S_ASLEEP = GroupName "asleep"
  pattern S_IMPRESSED = GroupName "impressed"
  pattern S_CURRENCY = GroupName "currency"
  pattern MOBILE = GroupName "mobile"
  
  -- * Optional item groups
  
  pattern S_SINGLE_SPARK, S_SPARK, S_FRAGRANCE, CRAWL_ITEM, TREASURE, ANY_SCROLL, ANY_GLASS, ANY_POTION, ANY_FLASK, EXPLOSIVE, ANY_JEWELRY, VALUABLE, UNREPORTED_INVENTORY, AQUATIC, HORROR :: GroupName ItemKind
  
  -- Used in Preferences.hs
  
  pattern S_SINGLE_SPARK = GroupName "single spark"
  pattern S_SPARK = GroupName "spark"
  pattern S_FRAGRANCE = GroupName "fragrance"
  
  pattern CRAWL_ITEM = GroupName "curious item"
    -- to be used only in long scenarios, such as multi-level dungeon crawl;
    -- may be a powerful or a mundate item, unlike @TREASURE@ item
  pattern TREASURE = GroupName "treasure"
    -- particularly powerful items, but may appear in any scenario
  pattern ANY_SCROLL = GroupName "scroll"
  pattern ANY_GLASS = GroupName "glass"
  pattern ANY_POTION = GroupName "potion"
  pattern ANY_FLASK = GroupName "flask"
  pattern EXPLOSIVE = GroupName "explosive"
  pattern ANY_JEWELRY = GroupName "jewelry"
  
  -- * Used elsewhere
  
  pattern VALUABLE = GroupName "valuable"
  pattern UNREPORTED_INVENTORY = GroupName "unreported inventory"
  pattern AQUATIC = GroupName "aquatic"
  
  pattern HORROR = GroupName "horror"
  
  -- | Item properties that are fixed for a given kind of items.
  -- Of these, aspects and effects are jointly called item powers.
  -- Note that this type is mutually recursive with 'Effect' and `Aspect`.
  data ItemKind = ItemKind
    { isymbol  :: ContentSymbol ItemKind
                                  -- ^ map symbol
    , iname    :: Text            -- ^ generic name; is pluralized if needed
    , ifreq    :: Freqs ItemKind  -- ^ frequency within groups
    , iflavour :: [Flavour]       -- ^ possible flavours
    , icount   :: Dice.Dice       -- ^ created in that quantity
    , irarity  :: Rarity          -- ^ rarity on given depths
    , iverbHit :: Text            -- ^ the verb for hitting
    , iweight  :: Int             -- ^ weight in grams
    , idamage  :: Dice.Dice       -- ^ basic kinetic damage
    , iaspects :: [Aspect]        -- ^ affect the actor continuously
    , ieffects :: [Effect]        -- ^ cause the effects when triggered
    , ikit     :: [(GroupName ItemKind, CStore)]
                                  -- ^ accompanying organs and equipment
    , idesc    :: Text            -- ^ description
    }
    deriving Show  -- No Eq and Ord to make extending logically sound
  
  -- | Aspects of items. Aspect @AddSkill@ is additive (starting at 0)
  -- for all items wielded by an actor and it affects the actor.
  -- The others affect only the item in question, not the actor carrying it,
  -- and so are not additive in any sense.
  data Aspect =
      Timeout Dice.Dice  -- ^ specifies the cooldown before an item may be
                         --   applied again; if a copy of an item is applied
                         --   manually (not via periodic activation),
                         --   all effects on a single copy of the item are
                         --   disabled until the copy recharges for the given
                         --   time expressed in game turns; all copies
                         --   recharge concurrently
    | AddSkill Ability.Skill Dice.Dice
                         -- ^ bonus to a skill; in content, avoid boosting
                         --   skills such as SkApply via permanent equipment,
                         --   to avoid micromanagement through swapping items
                         --   among party members before each skill use
    | SetFlag Ability.Flag
                         -- ^ item feature
    | ELabel Text        -- ^ extra label of the item; it's not pluralized
    | ToThrow ThrowMod   -- ^ parameters modifying a throw
    | PresentAs (GroupName ItemKind)
                         -- ^ until identified, presents as this unique kind
    | EqpSlot Ability.EqpSlot
                         -- ^ AI and UI flag that leaks item intended use
    | Odds Dice.Dice [Aspect] [Aspect]
                         -- ^ if level-scaled dice roll > 50,
                         --   pick the former aspects, otherwise the latter
    deriving (Show, Eq)
  
  -- | Effects of items. Can be invoked by the item wielder to affect
  -- another actor or the wielder himself.
  --
  -- Various effects of an item kind are all groupped in one list,
  -- at the cost of conditionals, sequences, etc., to ensure brevity
  -- and simplicity of content definitions. Most effects fire regardless
  -- of activation kind (the only exceptions are @OnSmash@ and @OnCombine@
  -- effects) so the deviations, handled via the conditionals, are rare
  -- and the definitions remain simple. Whether an item can be activated
  -- in any particular way, OTOH, is specified via simple flags elsewhere,
  -- again, by default, assuming that most activations are possible for all.
  data Effect =
      Burn Dice.Dice     -- ^ burn with this damage
    | Explode (GroupName ItemKind)
                         -- ^ explode producing this group of blasts
    | RefillHP Int       -- ^ modify HP of the actor by this amount
    | RefillCalm Int     -- ^ modify Calm of the actor by this amount
    | Dominate           -- ^ change actor's allegiance
    | Impress            -- ^ make actor susceptible to domination
    | PutToSleep         -- ^ put actor to sleep, also calming him
    | Yell               -- ^ make the actor yell/yawn, waking him and others up
    | Summon (GroupName ItemKind) Dice.Dice
        -- ^ summon the given number of actors of this group
    | Ascend Bool           -- ^ ascend to another level of the dungeon
    | Escape                -- ^ escape from the dungeon
    | Paralyze Dice.Dice    -- ^ paralyze for this many game clips
    | ParalyzeInWater Dice.Dice
                            -- ^ paralyze for this many game clips due to water
    | InsertMove Dice.Dice  -- ^ give actor this many extra tenths of actor move
    | Teleport Dice.Dice    -- ^ teleport actor across rougly this distance
    | CreateItem (Maybe Int) CStore (GroupName ItemKind) TimerDice
        -- ^ create an item of the group and insert into the store with the given
        --   random timer; it cardinality not specified, roll it
    | DestroyItem Int Int CStore (GroupName ItemKind)
        -- ^ destroy some items of the group from the store; see below about Ints
    | ConsumeItems [(Int, GroupName ItemKind)] [(Int, GroupName ItemKind)]
        -- ^ @ConsumeItems toUse toDestroy@ uses items matching @toUse@
        --   (destroys non-durable, without invoking OnSmash effects;
        --   applies normal effects of durable, without destroying them;
        --   the same behaviour as when transforming terrain using items)
        --   and destroys items matching @toDestroy@, invoking no effects,
        --   regardless of durability;
        --   the items are taken from @CGround@ (but not from @CEqp@),
        --   preferring non-durable (since durable can harm when used
        --   and may be more vauable when destroyed); if not all required items
        --   are present, no item are destroyed; if an item belongs to many groups
        --   in the sum of @toUse@ and @toDestroy@, it counts for all
        --   (otherwise, some orders of destroying would succeed,
        --   while others would not); even if item durable, as many copies
        --   are needed as specified, not just one applied many times;
        --   items are first destroyed and then, if any copies left, applied
    | DropItem Int Int CStore (GroupName ItemKind)
        -- ^ make the actor drop items of the given group from the given store;
        --   the first integer says how many item kinds to drop, the second,
        --   how many copies of each kind to drop;
        --   for non-organs, beware of not dropping all kinds, or cluttering
        --   store with rubbish becomes beneficial
    | Recharge Int Dice.Dice
        -- ^ reduce the cooldown period of this number of discharged items
        --   in the victim's equipment and organs by this dice of game clips;
        --   if the result is negative, set to 0, instantly recharging the item;
        --   starts with weapons with highest raw damage in equipment,
        --   then among organs, then non-weapons in equipment and among organs;
        --   beware of exploiting for healing periodic items
    | Discharge Int Dice.Dice
        -- ^ increase the cooldown period of this number of fully recharged items
        --   in the victim's equipment and organs by this dice of game clips;
        --   starts with weapons with highest raw damage in equipment,
        --   then among organs, then non-weapons in equipment and among organs;
        --   beware of exploiting for hunger inducing and similar organs
    | PolyItem
        -- ^ get a suitable (i.e., numerous enough) non-unique common item stack
        --   on the floor and polymorph it to a stack of random common items,
        --   with current depth coefficient
    | RerollItem
        -- ^ get a suitable (i.e., with any random aspects) single item
        --   (even unique) on the floor and change the random bonuses
        --   of the items randomly, with maximal depth coefficient
    | DupItem
        -- ^ exactly duplicate a single non-unique, non-valuable item on the floor
    | Identify
        -- ^ find a suitable (i.e., not identified) item, starting from
        --   the floor, and identify it
    | Detect DetectKind Int -- ^ detect something on the map in the given radius
    | SendFlying ThrowMod   -- ^ send an actor flying (push or pull, depending)
    | PushActor ThrowMod    -- ^ push an actor
    | PullActor ThrowMod    -- ^ pull an actor
    | ApplyPerfume          -- ^ remove all smell on the level
    | AtMostOneOf [Effect]  -- ^ try to trigger a single random effect of the list
    | OneOf [Effect]        -- ^ trigger, with equal probability,
                            --   one of the effects that don't end with @UseDud@
    | OnSmash Effect
        -- ^ trigger the effect when item smashed (not when applied nor meleed)
    | OnCombine Effect
        -- ^ trigger the effect only when the actor explicitly desires
        --   to combine items or otherwise subtly tinker with an
        --   item or a tile, e.g., craft items from other items in a workshop;
        --   in particular, don't trigger the effects when entering a tile;
        --   trigger exclusively the effects when activating walkable terrain
    | OnUser Effect  -- ^ apply the effect to the user, not the victim
    | NopEffect                -- ^ nothing happens, @UseDud@, no description
    | AndEffect Effect Effect  -- ^ only fire second effect if first activated
    | OrEffect Effect Effect   -- ^ only fire second effect if first not activated
    | SeqEffect [Effect]       -- ^ fire all effects in order; always suceed
    | When Condition Effect    -- ^ if condition not met, fail without a message;
                               --   better avoided, since AI can't value it well
    | Unless Condition Effect  -- ^ if condition met, fail without a message;
                               --   better avoided, since AI can't value it well
    | IfThenElse Condition Effect Effect
                               -- ^ conditional effect;
                               --   better avoided, since AI can't value it well
    | VerbNoLonger Text Text
        -- ^ a sentence with the actor causing the effect as subject, the given
        --   texts as the verb and the ending of the sentence (that may be
        --   ignored when the message is cited, e.g., as heard by someone)
        --   that is emitted when an activation causes an item to expire;
        --   no spam is emitted if a projectile; the ending is appended
        --   without a space in-between
    | VerbMsg Text Text
        -- ^ as @VerbNoLonger@ but that is emitted whenever the item is activated;
    | VerbMsgFail Text Text
        -- ^ as @VerbMsg@, but a failed effect (returns @UseId@)
    deriving (Show, Eq)
  
  data Condition =
      HpLeq Int
    | HpGeq Int
    | CalmLeq Int
    | CalmGeq Int
    | TriggeredBy Ability.ActivationFlag
    deriving (Show, Eq)
  
  data DetectKind =
      DetectAll
    | DetectActor
    | DetectLoot
    | DetectExit
    | DetectHidden
    | DetectEmbed
    | DetectStash
    deriving (Show, Eq)
  
  -- | Specification of how to randomly roll a timer at item creation
  -- to obtain a fixed timer for the item's lifetime.
  data TimerDice =
      TimerNone
    | TimerGameTurn Dice.Dice
    | TimerActorTurn Dice.Dice
    deriving Eq
  
  instance Show TimerDice where
    show TimerNone = "0"
    show (TimerGameTurn nDm) =
      show nDm ++ " " ++ if nDm == 1 then "turn" else "turns"
    show (TimerActorTurn nDm) =
      show nDm ++ " " ++ if nDm == 1 then "move" else "moves"
  
  -- | Parameters modifying a throw of a projectile or flight of pushed actor.
  -- Not additive and don't start at 0.
  data ThrowMod = ThrowMod
    { throwVelocity :: Int  -- ^ fly with this percentage of base throw speed
    , throwLinger   :: Int  -- ^ fly for this percentage of 2 turns
    , throwHP       :: Int  -- ^ start flight with this many HP
    }
    deriving (Show, Eq, Ord, Generic)
  
  instance Binary ThrowMod
  
  instance Hashable ThrowMod
  
  data ItemSymbolsUsedInEngine = ItemSymbolsUsedInEngine
    { rsymbolProjectile :: ContentSymbol ItemKind
    , rsymbolLight      :: ContentSymbol ItemKind
    , rsymbolTool       :: ContentSymbol ItemKind
    , rsymbolSpecial    :: ContentSymbol ItemKind
    , rsymbolGold       :: ContentSymbol ItemKind
    , rsymbolNecklace   :: ContentSymbol ItemKind
    , rsymbolRing       :: ContentSymbol ItemKind
    , rsymbolPotion     :: ContentSymbol ItemKind
    , rsymbolFlask      :: ContentSymbol ItemKind
    , rsymbolScroll     :: ContentSymbol ItemKind
    , rsymbolTorsoArmor :: ContentSymbol ItemKind
    , rsymbolMiscArmor  :: ContentSymbol ItemKind
    , rsymbolClothes    :: ContentSymbol ItemKind
    , rsymbolShield     :: ContentSymbol ItemKind
    , rsymbolPolearm    :: ContentSymbol ItemKind
    , rsymbolEdged      :: ContentSymbol ItemKind
    , rsymbolHafted     :: ContentSymbol ItemKind
    , rsymbolWand       :: ContentSymbol ItemKind
    , rsymbolFood       :: ContentSymbol ItemKind
    }
  
  emptyItemSymbolsUsedInEngine :: ItemSymbolsUsedInEngine
  emptyItemSymbolsUsedInEngine = ItemSymbolsUsedInEngine
    { rsymbolProjectile = toContentSymbol '0'
    , rsymbolLight      = toContentSymbol '0'
    , rsymbolTool       = toContentSymbol '0'
    , rsymbolSpecial    = toContentSymbol '0'
    , rsymbolGold       = toContentSymbol '0'
    , rsymbolNecklace   = toContentSymbol '0'
    , rsymbolRing       = toContentSymbol '0'
    , rsymbolPotion     = toContentSymbol '0'
    , rsymbolFlask      = toContentSymbol '0'
    , rsymbolScroll     = toContentSymbol '0'
    , rsymbolTorsoArmor = toContentSymbol '0'
    , rsymbolMiscArmor  = toContentSymbol '0'
    , rsymbolClothes    = toContentSymbol '0'
    , rsymbolShield     = toContentSymbol '0'
    , rsymbolPolearm    = toContentSymbol '0'
    , rsymbolEdged      = toContentSymbol '0'
    , rsymbolHafted     = toContentSymbol '0'
    , rsymbolWand       = toContentSymbol '0'
    , rsymbolFood       = toContentSymbol '0'
    }
  
  boostItemKindList :: SM.SMGen -> [ItemKind] -> [ItemKind]
  boostItemKindList _ [] = []
  boostItemKindList initialGen l =
    let (r, _) = nextRandom (length l - 1) initialGen
    in case splitAt r l of
      (pre, i : post) -> pre ++ boostItemKind i : post
      _               -> error $ "" `showFailure` l
  
  boostItemKind :: ItemKind -> ItemKind
  boostItemKind i =
    let mainlineLabel (label, _) =
          label `elem` [COMMON_ITEM, CRAWL_ITEM, TREASURE]
    in if any mainlineLabel (ifreq i)
       then i { ifreq = (COMMON_ITEM, 10000) : filter (not . mainlineLabel)
                                                      (ifreq i)
              , iaspects = delete (SetFlag Ability.Unique) $ iaspects i
              }
       else i
  
  -- | Whether the effect has a chance of exhibiting any potentially
  -- noticeable behaviour, except when the item is destroyed or combined.
  -- We assume at least one of @OneOf@ effects must be noticeable.
  forApplyEffect :: Effect -> Bool
  forApplyEffect eff = case eff of
    OnSmash{} -> False
    OnCombine{} -> False
    OnUser eff1 -> forApplyEffect eff1
    NopEffect -> False
    AndEffect eff1 eff2 -> forApplyEffect eff1 || forApplyEffect eff2
    OrEffect eff1 eff2 -> forApplyEffect eff1 || forApplyEffect eff2
    SeqEffect effs -> any forApplyEffect effs
    When _ eff1 -> forApplyEffect eff1
    Unless _ eff1 -> forApplyEffect eff1
    IfThenElse _ eff1 eff2 -> forApplyEffect eff1 || forApplyEffect eff2
    VerbNoLonger{} -> False
    VerbMsg{} -> False
    VerbMsgFail{} -> False
    ParalyzeInWater{} -> False  -- barely noticeable, spams when resisted
    _ -> True
  
  -- | Whether a non-nested effect always applies raw damage.
  forDamageEffect :: Effect -> Bool
  forDamageEffect eff = case eff of
    Burn{} -> True
    RefillHP n | n < 0 -> True
    _ -> False
  
  -- | Whether an item is damaging. Such items may trigger embedded items
  -- and may collide with bursting items mid-air.
  isDamagingKind :: ItemKind -> Bool
  isDamagingKind itemKind = Dice.infDice (idamage itemKind) > 0
                            || any forDamageEffect (ieffects itemKind)
  
  isEffEscape :: Effect -> Bool
  isEffEscape Escape{} = True
  isEffEscape (AtMostOneOf l) = any isEffEscape l
  isEffEscape (OneOf l) = any isEffEscape l
  isEffEscape (OnCombine eff) = isEffEscape eff
  isEffEscape (OnUser eff) = isEffEscape eff
  isEffEscape (AndEffect eff1 eff2) = isEffEscape eff1 || isEffEscape eff2
  isEffEscape (OrEffect eff1 eff2) = isEffEscape eff1 || isEffEscape eff2
  isEffEscape (SeqEffect effs) = any isEffEscape effs
  isEffEscape (When _ eff) = isEffEscape eff
  isEffEscape (Unless _ eff) = isEffEscape eff
  isEffEscape (IfThenElse _ eff1 eff2) = isEffEscape eff1 || isEffEscape eff2
  isEffEscape _ = False
  
  isEffEscapeOrAscend :: Effect -> Bool
  isEffEscapeOrAscend Ascend{} = True
  isEffEscapeOrAscend Escape{} = True
  isEffEscapeOrAscend (AtMostOneOf l) = any isEffEscapeOrAscend l
  isEffEscapeOrAscend (OneOf l) = any isEffEscapeOrAscend l
  isEffEscapeOrAscend (OnCombine eff) = isEffEscapeOrAscend eff
  isEffEscapeOrAscend (OnUser eff) = isEffEscapeOrAscend eff
  isEffEscapeOrAscend (AndEffect eff1 eff2) =
    isEffEscapeOrAscend eff1 || isEffEscapeOrAscend eff2
  isEffEscapeOrAscend (OrEffect eff1 eff2) =
    isEffEscapeOrAscend eff1 || isEffEscapeOrAscend eff2
  isEffEscapeOrAscend (SeqEffect effs) =
    any isEffEscapeOrAscend effs
  isEffEscapeOrAscend (When _ eff) = isEffEscapeOrAscend eff
  isEffEscapeOrAscend (Unless _ eff) = isEffEscapeOrAscend eff
  isEffEscapeOrAscend (IfThenElse _ eff1 eff2) =
    isEffEscapeOrAscend eff1 || isEffEscapeOrAscend eff2
  isEffEscapeOrAscend _ = False
  
  timeoutAspect :: Aspect -> Bool
  timeoutAspect Timeout{} = True
  timeoutAspect _ = False
  
  orEffect :: Effect -> Bool
  orEffect OrEffect{} = True
  orEffect _ = False
  
  onSmashEffect :: Effect -> Bool
  onSmashEffect OnSmash{} = True
  onSmashEffect _ = False
  
  onCombineEffect :: Effect -> Bool
  onCombineEffect OnCombine{} = True
  onCombineEffect _ = False
  
  onSmashOrCombineEffect :: Effect -> Bool
  onSmashOrCombineEffect OnSmash{} = True
  onSmashOrCombineEffect OnCombine{} = True
  onSmashOrCombineEffect _ = False
  
  alwaysDudEffect :: Effect -> Bool
  alwaysDudEffect OnSmash{} = True
  alwaysDudEffect OnCombine{} = True
  alwaysDudEffect NopEffect = True
  alwaysDudEffect _ = False
  
  strengthOnSmash :: ItemKind -> [Effect]
  strengthOnSmash =
    let f (OnSmash eff) = [eff]
        f _ = []
    in concatMap f . ieffects
  
  strengthOnCombine :: ItemKind -> [Effect]
  strengthOnCombine =
    let f (OnCombine eff) = [eff]
        f _ = []
    in concatMap f . ieffects
  
  getDropOrgans :: ItemKind -> [GroupName ItemKind]
  getDropOrgans =
    let f (DestroyItem _ _ COrgan grp) = [grp]
        f (DropItem _ _ COrgan grp) = [grp]
        f Impress = [S_IMPRESSED]
        f (AtMostOneOf l) = concatMap f l  -- even remote possibility accepted
        f (OneOf l) = concatMap f l  -- even remote possibility accepted
        f (OnUser eff) = f eff  -- no OnCombine, because checked for potions, etc.
        f (AndEffect eff1 eff2) = f eff1 ++ f eff2  -- not certain, but accepted
        f (OrEffect eff1 eff2) = f eff1 ++ f eff2  -- not certain, but accepted
        f (SeqEffect effs) = concatMap f effs
        f (When _ eff) = f eff
        f (Unless _ eff) = f eff
        f (IfThenElse _ eff1 eff2) = f eff1 ++ f eff2
        f _ = []
    in concatMap f . ieffects
  
  -- Anything under @Odds@ is ignored, because it's not mandatory.
  getMandatoryPresentAsFromKind :: ItemKind -> Maybe (GroupName ItemKind)
  getMandatoryPresentAsFromKind itemKind =
    let f (PresentAs grp) = [grp]
        f _ = []
    in listToMaybe $ concatMap f (iaspects itemKind)
  
  damageUsefulness :: ItemKind -> Double
  damageUsefulness itemKind =
    let v = min 1000 (10 * Dice.meanDice (idamage itemKind))
    in assert (v >= 0) v
  
  verbMsgNoLonger :: Text -> Effect
  verbMsgNoLonger name = VerbNoLonger ("be no longer" <+> name) "."
  
  verbMsgLess :: Text -> Effect
  verbMsgLess name = VerbMsg ("appear less" <+> name) "."
  
  toVelocity :: Int -> Aspect
  toVelocity n = ToThrow $ ThrowMod n 100 1
  
  toLinger :: Int -> Aspect
  toLinger n = ToThrow $ ThrowMod 100 n 1
  
  timerNone :: TimerDice
  timerNone = TimerNone
  
  isTimerNone :: TimerDice -> Bool
  isTimerNone tim = tim == TimerNone
  
  foldTimer :: a -> (Dice.Dice -> a) -> (Dice.Dice -> a) -> TimerDice -> a
  foldTimer a fgame factor tim = case tim of
    TimerNone -> a
    TimerGameTurn nDm -> fgame nDm
    TimerActorTurn nDm -> factor nDm
  
  toOrganBad :: GroupName ItemKind -> Dice.Dice -> Effect
  toOrganBad grp nDm = CreateItem Nothing COrgan grp (TimerGameTurn nDm)
  
  toOrganGood :: GroupName ItemKind -> Dice.Dice -> Effect
  toOrganGood grp nDm = CreateItem Nothing COrgan grp (TimerActorTurn nDm)
  
  toOrganNoTimer :: GroupName ItemKind -> Effect
  toOrganNoTimer grp = CreateItem Nothing COrgan grp TimerNone
  
  -- | Catch invalid item kind definitions.
  validateSingle :: ItemSymbolsUsedInEngine -> ItemKind -> [Text]
  validateSingle itemSymbols ik@ItemKind{..} =
    ["iname longer than 23" | T.length iname > 23]
    ++ ["icount < 0" | Dice.infDice icount < 0]
    ++ validateRarity irarity
    ++ validateDamage idamage
    -- Reject duplicate Timeout, because it's not additive.
    ++ (let ts = filter timeoutAspect iaspects
        in ["more than one Timeout specification" | length ts > 1])
    ++ [ "Conflicting Fragile and Durable"
       | SetFlag Ability.Fragile `elem` iaspects
         && SetFlag Ability.Durable `elem` iaspects ]
    ++ (let f :: Aspect -> Bool
            f EqpSlot{} = True
            f _ = False
            ts = filter f iaspects
            equipable = SetFlag Ability.Equipable `elem` iaspects
            meleeable = SetFlag Ability.Meleeable `elem` iaspects
            likelyTemplate = case ifreq of
              [(grp, 1)] -> "unknown" `T.isSuffixOf` fromGroupName grp
              _ -> False
            likelyException = isymbol `elem` [ rsymbolFood itemSymbols
                                             , rsymbolNecklace itemSymbols
                                             , rsymbolWand itemSymbols ]
                              || likelyTemplate
        in [ "EqpSlot specified but not Equipable nor Meleeable"
           | length ts == 1 && not equipable && not meleeable ]
           ++ [ "EqpSlot not specified but Equipable or Meleeable and not a likely organ or necklace or template"
              | not likelyException
                && null ts && (equipable || meleeable) ]
           ++ [ "More than one EqpSlot specified"
              | length ts > 1 ] )
    ++ [ "Redundant Equipable or Meleeable"
       | SetFlag Ability.Equipable `elem` iaspects
         && SetFlag Ability.Meleeable `elem` iaspects ]
    ++ [ "Conflicting Durable and Blast"
       | SetFlag Ability.Durable `elem` iaspects
         && SetFlag Ability.Blast `elem` iaspects ]
    ++ [ "Conflicting Durable and Condition"
       | SetFlag Ability.Durable `elem` iaspects
         && SetFlag Ability.Condition `elem` iaspects ]
    ++ [ "Conflicting Blast and Condition"
       | SetFlag Ability.Blast `elem` iaspects
         && SetFlag Ability.Condition `elem` iaspects ]
    ++ (let f :: Aspect -> Bool
            f ELabel{} = True
            f _ = False
            ts = filter f iaspects
        in ["more than one ELabel specification" | length ts > 1])
    ++ (let f :: Aspect -> Bool
            f ToThrow{} = True
            f _ = False
            ts = filter f iaspects
        in ["more than one ToThrow specification" | length ts > 1])
    ++ (let f :: Aspect -> Bool
            f PresentAs{} = True
            f _ = False
            ts = filter f iaspects
        in ["more than one PresentAs specification" | length ts > 1])
    ++ concatMap (validateDups ik . SetFlag) [minBound .. maxBound]
    ++ (let f :: Effect -> Bool
            f VerbNoLonger{} = True
            f _ = False
        in validateOnlyOne ieffects "VerbNoLonger" f)  -- may be duped if nested
    ++ (let f :: Effect -> Bool
            f VerbMsg{} = True
            f _ = False
        in validateOnlyOne ieffects "VerbMsg" f)  -- may be duplicated if nested
    ++ (let f :: Effect -> Bool
            f VerbMsgFail{} = True
            f _ = False
        in validateOnlyOne ieffects "VerbMsgFail" f)  -- may be duped if nested
    ++ validateNotNested ieffects "OnSmash or OnCombine" onSmashOrCombineEffect
         -- but duplicates permitted
    ++ let nonPositiveBurn :: Effect -> Bool
           nonPositiveBurn (Burn d) = Dice.infDice d <= 0
           nonPositiveBurn _ = False
           containingNonPositiveBurn =
             filter (checkSubEffectProp nonPositiveBurn) ieffects
       in [ "effects with non-positive Burn:" <+> tshow containingNonPositiveBurn
          | not $ null containingNonPositiveBurn ]
    ++ let emptyOneOf :: Effect -> Bool
           emptyOneOf (AtMostOneOf []) = True
           emptyOneOf (OneOf []) = True
           emptyOneOf _ = False
           containingEmptyOneOf = filter (checkSubEffectProp emptyOneOf) ieffects
       in [ "effects with empty AtMostOneOf or OneOf:"
            <+> tshow containingEmptyOneOf
          | not $ null containingEmptyOneOf ]
    ++ (let nonPositiveEffect :: Effect -> Bool
            nonPositiveEffect (CreateItem (Just n) _ _ _) | n <= 0 = True
            nonPositiveEffect (DestroyItem n k _ _) | n <= 0 || k <= 0 = True
            nonPositiveEffect (ConsumeItems tools raw)
              | any ((<= 0) . fst) (tools ++ raw) = True
            nonPositiveEffect (DropItem n k _ _) | n <= 0 || k <= 0 = True
            nonPositiveEffect (Detect _ n) | n <= 0 = True
            nonPositiveEffect _ = False
            containingNonPositiveEffect =
              filter (checkSubEffectProp nonPositiveEffect) ieffects
        in [ "effects with forbidden non-positive parameters:"
             <+> tshow containingNonPositiveEffect
           | not $ null containingNonPositiveEffect ])
    ++ (let nonPositiveEffect :: Effect -> Bool
            nonPositiveEffect (Summon _ d) | Dice.infDice d <= 0 = True
            nonPositiveEffect (Paralyze d) | Dice.infDice d <= 0 = True
            nonPositiveEffect (ParalyzeInWater d) | Dice.infDice d <= 0 = True
            nonPositiveEffect (InsertMove d) | Dice.infDice d <= 0 = True
            nonPositiveEffect (Teleport d) | Dice.infDice d <= 0 = True
            nonPositiveEffect (CreateItem _ _ _ (TimerGameTurn d))
              | Dice.infDice d <= 0 = True
            nonPositiveEffect (CreateItem _ _ _ (TimerActorTurn d))
              | Dice.infDice d <= 0 = True
            nonPositiveEffect (Recharge n d)
              | n <= 0 || Dice.infDice d <= 0 = True
            nonPositiveEffect (Discharge n d)
              | n <= 0 || Dice.infDice d <= 0 = True
            nonPositiveEffect _ = False
            containingNonPositiveEffect =
              filter (checkSubEffectProp nonPositiveEffect) ieffects
        in [ "effects with forbidden potentially non-positive or negative number or dice:"
             <+> tshow containingNonPositiveEffect
           | not $ null containingNonPositiveEffect ])
  
  -- We only check there are no duplicates at top level. If it may be nested,
  -- it may presumably be duplicated inside the nesting as well.
  validateOnlyOne :: [Effect] -> Text -> (Effect -> Bool) -> [Text]
  validateOnlyOne effs t f =
    let ts = filter f effs
    in ["more than one" <+> t <+> "specification" | length ts > 1]
  
  -- We check it's not nested one nor more levels.
  validateNotNested :: [Effect] -> Text -> (Effect -> Bool) -> [Text]
  validateNotNested effs t f =
    let g (AtMostOneOf l) = any h l
        g (OneOf l) = any h l
        g (OnSmash effect) = h effect
        g (OnCombine effect) = h effect
        g (OnUser effect) = h effect
        g (AndEffect eff1 eff2) = h eff1 || h eff2
        g (OrEffect eff1 eff2) = h eff1 || h eff2
        g (SeqEffect effs2) = any h effs2
        g (When _ effect) = h effect
        g (Unless _ effect) = h effect
        g (IfThenElse _ eff1 eff2) = h eff1 || h eff2
        g _ = False
        h effect = f effect || g effect
        ts = filter g effs
    in [ "effect" <+> t <+> "should be specified at top level, not nested"
       | not (null ts) ]
  
  checkSubEffectProp :: (Effect -> Bool) -> Effect -> Bool
  checkSubEffectProp f eff =
    let g (AtMostOneOf l) = any h l
        g (OneOf l) = any h l
        g (OnSmash effect) = h effect
        g (OnCombine effect) = h effect
        g (OnUser effect) = h effect
        g (AndEffect eff1 eff2) = h eff1 || h eff2
        g (OrEffect eff1 eff2) = h eff1 || h eff2
        g (SeqEffect effs) = any h effs
        g (When _ effect) = h effect
        g (Unless _ effect) = h effect
        g (IfThenElse _ eff1 eff2) = h eff1 || h eff2
        g _ = False
        h effect = f effect || g effect
    in h eff
  
  validateDups :: ItemKind -> Aspect -> [Text]
  validateDups ItemKind{..} feat =
    let ts = filter (== feat) iaspects
    in ["more than one" <+> tshow feat <+> "specification" | length ts > 1]
  
  validateDamage :: Dice.Dice -> [Text]
  validateDamage dice = [ "potentially negative dice:" <+> tshow dice
                        | Dice.infDice dice < 0]
  
  -- | Validate all item kinds.
  validateAll :: [ItemKind] -> ContentData ItemKind -> [Text]
  validateAll content coitem =
    let f :: Aspect -> Bool
        f PresentAs{} = True
        f _ = False
        wrongPresentAsGroups =
          [ cgroup
          | k <- content
          , let (cgroup, notSingleton) = case find f (iaspects k) of
                  Just (PresentAs grp) | not $ oisSingletonGroup coitem grp ->
                    (grp, True)
                  _ -> (undefined, False)
          , notSingleton
          ]
    in [ "PresentAs groups not singletons:" <+> tshow wrongPresentAsGroups
       | not $ null wrongPresentAsGroups ]
  
  makeData :: ItemSymbolsUsedInEngine
           -> [ItemKind] -> [GroupName ItemKind] -> [GroupName ItemKind]
           -> ContentData ItemKind
  makeData itemSymbols content groupNamesSingleton groupNames =
    let allGroupNamesTooLong = filter ((> 23) . T.length . fromGroupName)
                               $ groupNamesSingleton ++ groupNames
    in assert (null allGroupNamesTooLong
               `blame` "ItemKind: some item group names too long"
               `swith` allGroupNamesTooLong) $
       makeContentData "ItemKind" iname ifreq
                       (validateSingle itemSymbols) validateAll content
                       (mandatoryGroupsSingleton ++ groupNamesSingleton)
                       (mandatoryGroups ++ groupNames)
  