content: -- | Display atomic SFX commands received by the client.
  module Game.LambdaHack.Client.UI.Watch.WatchSfxAtomicM
    ( watchRespSfxAtomicUI
  #ifdef EXPOSE_INTERNAL
      -- * Internal operations
    , returnJustLeft, ppSfxMsg, strike
  #endif
    ) where
  
  import Prelude ()
  
  import Game.LambdaHack.Core.Prelude
  
  import qualified Data.EnumMap.Strict as EM
  import           Data.Int (Int64)
  import qualified Data.Text as T
  import qualified NLP.Miniutter.English as MU
  
  import           Game.LambdaHack.Atomic (SfxAtomic (..), SfxMsg (..))
  import           Game.LambdaHack.Client.MonadClient
  import           Game.LambdaHack.Client.State
  import           Game.LambdaHack.Client.UI.ActorUI
  import           Game.LambdaHack.Client.UI.Animation
  import           Game.LambdaHack.Client.UI.Content.Screen
  import           Game.LambdaHack.Client.UI.ContentClientUI
  import           Game.LambdaHack.Client.UI.EffectDescription
  import           Game.LambdaHack.Client.UI.Frame
  import           Game.LambdaHack.Client.UI.FrameM
  import           Game.LambdaHack.Client.UI.HandleHelperM
  import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd
  import           Game.LambdaHack.Client.UI.ItemDescription
  import qualified Game.LambdaHack.Client.UI.Key as K
  import           Game.LambdaHack.Client.UI.MonadClientUI
  import           Game.LambdaHack.Client.UI.Msg
  import           Game.LambdaHack.Client.UI.MsgM
  import           Game.LambdaHack.Client.UI.SessionUI
  import           Game.LambdaHack.Client.UI.SlideshowM
  import           Game.LambdaHack.Client.UI.TutorialHints (TutorialHints (..))
  import           Game.LambdaHack.Client.UI.Watch.WatchCommonM
  import           Game.LambdaHack.Common.Actor
  import           Game.LambdaHack.Common.ActorState
  import           Game.LambdaHack.Common.Faction
  import           Game.LambdaHack.Common.Item
  import qualified Game.LambdaHack.Common.ItemAspect as IA
  import           Game.LambdaHack.Common.Kind
  import           Game.LambdaHack.Common.Level
  import           Game.LambdaHack.Common.Misc
  import           Game.LambdaHack.Common.MonadStateRead
  import           Game.LambdaHack.Common.Point
  import           Game.LambdaHack.Common.ReqFailure
  import           Game.LambdaHack.Common.State
  import           Game.LambdaHack.Common.Time
  import           Game.LambdaHack.Common.Types
  import           Game.LambdaHack.Content.CaveKind (cdesc)
  import qualified Game.LambdaHack.Content.ItemKind as IK
  import qualified Game.LambdaHack.Content.TileKind as TK
  import qualified Game.LambdaHack.Core.Dice as Dice
  import           Game.LambdaHack.Core.Frequency
  import           Game.LambdaHack.Core.Random
  import qualified Game.LambdaHack.Definition.Ability as Ability
  import qualified Game.LambdaHack.Definition.Color as Color
  import           Game.LambdaHack.Definition.Defs
  
  -- | Display special effects (text, animation) sent to the client.
  -- Don't modify client state (except a few fields), but only client
  -- session (e.g., by displaying messages). This is enforced by types.
  watchRespSfxAtomicUI :: MonadClientUI m => SfxAtomic -> m ()
  {-# INLINE watchRespSfxAtomicUI #-}
  watchRespSfxAtomicUI sfx = case sfx of
    SfxStrike source target iid ->
      strike False source target iid
    SfxRecoil source target iid -> do
      sourceSeen <- getsState $ EM.member source . sactorD
      if not sourceSeen then do
        tb <- getsState $ getActorBody target
        animate (blid tb) $ blockMiss (bpos tb, bpos tb)
      else do
        CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui
        sb <- getsState $ getActorBody source
        tb <- getsState $ getActorBody source
        spart <- partActorLeader source
        tpart <- partActorLeader target
        side <- getsClient sside
        factionD <- getsState sfactionD
        localTime <- getsState $ getLocalTime (blid tb)
        itemFullWeapon <- getsState $ itemToFull iid
        let kitWeapon = quantSingle
            (weaponName, _) = partItemShort rwidth side factionD
                                            localTime itemFullWeapon kitWeapon
            weaponNameOwn = partItemShortWownW rwidth side factionD spart
                                               localTime itemFullWeapon kitWeapon
            verb = if bproj sb then "deflect" else "fend off"
            objects | iid == btrunk sb = ["the", spart]
                    | iid `EM.member` borgan sb =  ["the", weaponNameOwn]
                    | otherwise = ["the", weaponName, "of", spart]
        msgAdd MsgActionMajor $
          makeSentence $ MU.SubjectVerbSg tpart verb : objects
        animate (blid tb) $ blockMiss (bpos tb, bpos sb)
    SfxSteal source target iid ->
      strike True source target iid
    SfxRelease source target _ -> do
      spart <- partActorLeader source
      tpart <- partActorLeader target
      msgAdd MsgActionMajor $
        makeSentence [MU.SubjectVerbSg spart "release", tpart]
    SfxProject aid iid ->
      itemAidVerbMU MsgActionMajor aid "fling" iid (Left 1)
    SfxReceive aid iid ->
      itemAidVerbMU MsgActionMajor aid "receive" iid (Left 1)
    SfxApply aid iid -> do
      CCUI{coscreen=ScreenContent{rapplyVerbMap}} <- getsSession sccui
      ItemFull{itemKind} <- getsState $ itemToFull iid
      let actionPart =
            maybe "trigger"
                  MU.Text
                  (EM.lookup (IK.isymbol itemKind) rapplyVerbMap)
      itemAidVerbMU MsgActionMajor aid actionPart iid (Left 1)
    SfxCheck aid iid ->
      itemAidVerbMU MsgActionMajor aid "recover" iid (Left 1)
    SfxTrigger _ _ _ fromTile -> do
      COps{cotile} <- getsState scops
      let subject = MU.Text $ TK.tname $ okind cotile fromTile
          verb = "shake"
          msg = makeSentence ["the", MU.SubjectVerbSg subject verb]
      msgAdd MsgNeutralEvent msg
    SfxShun aid _ _ _ ->
      aidVerbMU MsgActionMajor aid "shun it"
    SfxEffect fidSource aid iid effect hpDelta -> do
      -- In most messages below @iid@ is ignored, because it's too common,
      -- e.g., caused by some combat hits, or rather obvious,
      -- e.g., in case of embedded items, or would be counterintuitive,
      -- e.g., when actor is said to be intimidated by a particle, not explosion.
      CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui
      b <- getsState $ getActorBody aid
      bUI <- getsSession $ getActorUI aid
      side <- getsClient sside
      mleader <- getsClient sleader
      itemD <- getsState sitemD
      actorMaxSk <- getsState $ getActorMaxSkills aid
      let fid = bfid b
          isOurCharacter = fid == side && not (bproj b)
          isAlive = bhp b > 0
          isOurAlive = isOurCharacter && isAlive
          isOurLeader = Just aid == mleader
          -- The message classes are close enough. It's melee or similar.
          feelLookHPBad bigAdj projAdj = do
            feelLook MsgBadMiscEvent MsgGoodMiscEvent bigAdj projAdj
            -- We can't know here if the hit was in melee, ranged or
            -- even triggering a harmful item. However, let's not talk
            -- about armor before the player has the most basic one.
            -- for melee. Most of the time the first hit in the game is,
            -- in fact, from melee, so that's a sensible default.
            --
            -- Note that the @idamage@ is called piercing (or edged) damage,
            -- even though the distinction from impact damage is fleshed
            -- out only in Allure.
            when (isOurCharacter
                  && Ability.getSk Ability.SkArmorMelee actorMaxSk > 0) $
              tutorialHintMsgAdd DamageOfDifferentKind
          feelLookHPGood = feelLook MsgGoodMiscEvent MsgBadMiscEvent
          feelLookCalm bigAdj projAdj = when isAlive $
            feelLook MsgEffectMinor MsgEffectMinor bigAdj projAdj
          -- Ignore @iid@, because it's usually obvious what item caused that
          -- and because the effects are not particularly disortienting.
          feelLook msgClassOur msgClassTheir bigAdj projAdj =
            let (verb, adjective) =
                  if bproj b
                  then ("get", projAdj)
                  else ( if isOurCharacter then "feel" else "look"
                       , if isAlive then bigAdj else projAdj )
                           -- dead body is an item, not a person
                msgClass = if | bproj b -> MsgEffectMinor
                              | isOurCharacter -> msgClassOur
                              | otherwise -> msgClassTheir
            in aidVerbMU msgClass aid $ MU.Text $ verb <+> adjective
      case effect of
        IK.Burn{} -> do
          feelLookHPBad "burned" "scorched"
          let ps = (bpos b, bpos b)
          animate (blid b) $ twirlSplash ps Color.BrRed Color.Brown
        IK.Explode{} -> return ()  -- lots of visual feedback
        IK.RefillHP p | p == 1 -> return ()  -- no spam from regeneration
        IK.RefillHP p | p == -1 -> return ()  -- no spam from poison
        IK.RefillHP{} | hpDelta > 0 -> do
          feelLookHPGood "healthier" "mended"
          let ps = (bpos b, bpos b)
          animate (blid b) $ twirlSplash ps Color.BrGreen Color.Green
        IK.RefillHP{} -> do
          feelLookHPBad "wounded" "broken"
          let ps = (bpos b, bpos b)
          animate (blid b) $ twirlSplash ps Color.BrRed Color.Red
        IK.RefillCalm{} | not isAlive -> return ()
        IK.RefillCalm{} | bproj b -> return ()
        IK.RefillCalm p | p == 1 -> return ()  -- no spam from regen items
        IK.RefillCalm p | p > 0 -> feelLookCalm "calmer" "stabilized"
        IK.RefillCalm _ -> feelLookCalm "agitated" "wobbly"
        IK.Dominate | not isAlive -> return ()
        IK.Dominate -> do
          -- For subsequent messages use the proper name, never "you".
          let subject = partActor bUI
          if fid /= fidSource then do
            -- Before domination, possibly not seen if actor (yet) not ours.
            if bcalm b == 0  -- sometimes only a coincidence, but nm
            then aidVerbMU MsgEffectMedium aid "yield, under extreme pressure"
            else do
              let verb = if isOurAlive
                         then "black out, dominated by foes"
                         else "decide abruptly to switch allegiance"
                  -- Faction is being switched, so item that caused domination
                  -- and vanished may not be known to the new faction.
                  msuffix = if iid == btrunk b || iid `EM.notMember` itemD
                            then Nothing
                            else Just $ if isOurAlive
                                        then "through"
                                        else "under the influence of"
              mitemAidVerbMU MsgEffectMedium aid verb iid msuffix
            fidNameRaw <- getsState $ gname . (EM.! fid) . sfactionD
            -- Avoid "controlled by Controlled foo".
            let fidName = T.unwords $ tail $ T.words fidNameRaw
                verb = "be no longer controlled by"
            msgLnAdd MsgEffectMajor $ makeSentence
              [MU.SubjectVerbSg subject verb, MU.Text fidName]
            when isOurAlive $ displayMoreKeep ColorFull ""  -- Ln makes it short
          else do
            -- After domination, possibly not seen, if actor (already) not ours.
            fidSourceNameRaw <- getsState $ gname . (EM.! fidSource) . sfactionD
            -- Avoid "Controlled control".
            let fidSourceName = T.unwords $ tail $ T.words fidSourceNameRaw
                verb = "be now under"
            msgAdd MsgEffectMajor $ makeSentence
              [MU.SubjectVerbSg subject verb, MU.Text fidSourceName, "control"]
        IK.Impress | not isAlive -> return ()
        IK.Impress -> aidVerbMU MsgEffectMinor aid "be awestruck"
        IK.PutToSleep | not isAlive -> return ()
        IK.PutToSleep -> do
          let verb = "be put to sleep"
              msuffix = Just $ if fidSource == bfid b then "due to" else "by"
          mitemAidVerbMU MsgEffectMajor aid verb iid msuffix
        IK.Yell | not isAlive -> return ()
        IK.Yell -> aidVerbMU MsgMiscellanous aid "start"
        IK.Summon grp p -> do
          let verbBase = if bproj b then "lure" else "summon"
              part = MU.Text $ displayGroupName grp
              object = if p == 1  -- works, because exact number sent, not dice
                       then MU.AW part
                       else MU.Ws part
              verb = MU.Phrase [verbBase, object]
              msuffix = Just "with"
          mitemAidVerbMU MsgEffectMajor aid verb iid msuffix
        IK.Ascend{} | not isAlive -> return ()
        IK.Ascend up -> do
          COps{cocave} <- getsState scops
          aidVerbMU MsgEffectMajor aid $ MU.Text $
            "find a way" <+> if up then "upstairs" else "downstairs"
          when isOurLeader $ do
            destinations <- getsState $ whereTo (blid b) (bpos b) up
                                        . sdungeon
            case destinations of
              (lid, _) : _ -> do  -- only works until different levels possible
                lvl <- getLevel lid
                let desc = cdesc $ okind cocave $ lkind lvl
                unless (T.null desc) $
                  msgAdd MsgBackdropInfo $ desc <> "\n"
                tutorialHintMsgAdd NewFloorNewOpportunity
              [] -> return ()  -- spell fizzles; normally should not be sent
        IK.Escape{} | isOurCharacter -> do
          ours <- getsState $ fidActorNotProjGlobalAssocs side
          when (length ours > 1) $ do
            (_, total) <- getsState $ calculateTotal side
            if total == 0
            then msgAdd MsgFactionIntel $
                   "The team joins" <+> makePhrase [partActor bUI]
                   <> ", forms a perimeter and leaves triumphant."
            else msgAdd MsgItemCreation $
                   "The team joins" <+> makePhrase [partActor bUI]
                   <> ", forms a perimeter, repacks its belongings and leaves triumphant."
        IK.Escape{} -> return ()
        IK.Paralyze{} | not isAlive -> return ()
        IK.Paralyze{} ->
          mitemAidVerbMU MsgEffectMedium aid "be paralyzed" iid (Just "with")
        IK.ParalyzeInWater{} | not isAlive -> return ()
        IK.ParalyzeInWater{} ->
          aidVerbMU MsgEffectMinor aid "move with difficulty"
        IK.InsertMove{} | not isAlive -> return ()
        IK.InsertMove d ->
          -- Usually self-inflicted of from embeds, so obvious, so no @iid@.
          if Dice.supDice d >= 10
          then aidVerbMU MsgEffectMedium aid "act with extreme speed"
          else do
            let msgClass = if isOurCharacter
                           then MsgEffectMedium
                           else MsgEffectMinor
            aidVerbMU msgClass aid "move swiftly"
        IK.Teleport t | Dice.supDice t <= 9 -> do
          -- Actor may be sent away before noticing the item that did it.
          let msuffix = if iid `EM.notMember` itemD
                        then Nothing
                        else Just "due to"
              msgClass = if isOurCharacter
                         then MsgEffectMedium
                         else MsgEffectMinor
          mitemAidVerbMU msgClass aid "blink" iid msuffix
        IK.Teleport{} -> do
          -- Actor may be sent away before noticing the item that did it.
          let msuffix = if iid `EM.notMember` itemD
                        then Nothing
                        else Just "by the power of"
          mitemAidVerbMU MsgEffectMedium aid "teleport" iid msuffix
        IK.CreateItem{} -> return ()
        IK.DestroyItem{} -> return ()
        IK.ConsumeItems{} -> return ()
        IK.DropItem _ _ COrgan _ -> return ()
        IK.DropItem{} ->  -- rare enough
          mitemAidVerbMU MsgEffectMedium aid "be stripped" iid (Just "with")
        IK.Recharge{} | not isAlive -> return ()
        IK.Recharge{} -> aidVerbMU MsgEffectMedium aid "charge up"
        IK.Discharge{} | not isAlive -> return ()
        IK.Discharge{} -> aidVerbMU MsgEffectMedium aid "lose charges"
        IK.PolyItem -> do
          subject <- partActorLeader aid
          let ppstore = MU.Text $ ppCStoreIn CGround
          msgAdd MsgEffectMedium $ makeSentence
            [ MU.SubjectVerbSg subject "repurpose", "what lies", ppstore
            , "to a common item of the current level" ]
        IK.RerollItem -> do
          subject <- partActorLeader aid
          let ppstore = MU.Text $ ppCStoreIn CGround
          msgAdd MsgEffectMedium $ makeSentence
            [ MU.SubjectVerbSg subject "reshape", "what lies", ppstore
            , "striving for the highest possible standards" ]
        IK.DupItem -> do
          subject <- partActorLeader aid
          let ppstore = MU.Text $ ppCStoreIn CGround
          msgAdd MsgEffectMedium $ makeSentence
            [MU.SubjectVerbSg subject "multiply", "what lies", ppstore]
        IK.Identify -> do
          subject <- partActorLeader aid
          pronoun <- partPronounLeader aid
          msgAdd MsgEffectMinor $ makeSentence
            [ MU.SubjectVerbSg subject "look at"
            , MU.WownW pronoun $ MU.Text "inventory"
            , "intensely" ]
        IK.Detect d _ -> do
          subject <- partActorLeader aid
          factionD <- getsState sfactionD
          localTime <- getsState $ getLocalTime $ blid b
          let verb = MU.Text $ detectToVerb d
              object = MU.Ws $ MU.Text $ detectToObject d
          (periodic, itemFull) <-
            if iid `EM.member` itemD then do
              itemFull <- getsState $ itemToFull iid
              let arItem = aspectRecordFull itemFull
              return (IA.checkFlag Ability.Periodic arItem, itemFull)
            else do
  #ifdef WITH_EXPENSIVE_ASSERTIONS
              -- It's not actually expensive, but it's particularly likely
              -- to fail with wild content, indicating server game rules logic
              -- needs to be fixed/extended:
              -- Observer from another faction may receive the effect information
              -- from the server, because the affected actor is visible,
              -- but the position of the item may be out of FOV. This is fine;
              -- the message is then shorter, because only the effect was seen,
              -- while the cause remains misterious.
              let !_A = if fid /= side  -- not from affected faction; observing
                        then ()
                        else error $ "item never seen by the affected actor"
                                     `showFailure` (aid, b, bUI, verb, iid, sfx)
  #endif
              return (False, undefined)
          let iidDesc =
                let (name1, powers) = partItemShort rwidth side factionD localTime
                                                    itemFull quantSingle
                in makePhrase ["the", name1, powers]
              -- If item not periodic, most likely intentional, so don't spam.
              means = [MU.Text $ "(via" <+> iidDesc <> ")" | periodic]
          msgAdd MsgEffectMinor $
            makeSentence $ [MU.SubjectVerbSg subject verb] ++ [object] ++ means
          -- Don't make it modal if all info remains after no longer seen.
          unless (fid /= side || d `elem` [IK.DetectHidden, IK.DetectExit]) $
            displayMore ColorFull ""  -- the sentence short
        IK.SendFlying{} | not isAlive -> return ()
        IK.SendFlying{} -> aidVerbMU MsgEffectMedium aid "be sent flying"
        IK.PushActor{} | not isAlive -> return ()
        IK.PushActor{} -> aidVerbMU MsgEffectMedium aid "be pushed"
        IK.PullActor{} | not isAlive -> return ()
        IK.PullActor{} -> aidVerbMU MsgEffectMedium aid "be pulled"
        IK.ApplyPerfume ->
          msgAdd MsgEffectMinor
                 "The fragrance quells all scents in the vicinity."
        IK.AtMostOneOf{} -> return ()
        IK.OneOf{} -> return ()
        IK.OnSmash{} -> error $ "" `showFailure` sfx
        IK.OnCombine{} -> error $ "" `showFailure` sfx
        IK.OnUser{} -> error $ "" `showFailure` sfx
        IK.NopEffect -> error $ "" `showFailure` sfx
        IK.AndEffect{} -> error $ "" `showFailure` sfx
        IK.OrEffect{} -> error $ "" `showFailure` sfx
        IK.SeqEffect{} -> error $ "" `showFailure` sfx
        IK.When{} -> error $ "" `showFailure` sfx
        IK.Unless{} -> error $ "" `showFailure` sfx
        IK.IfThenElse{} -> error $ "" `showFailure` sfx
        IK.VerbNoLonger{} | not isAlive -> return ()
        IK.VerbNoLonger verb ending -> do
          let msgClass = if fid == side
                         then MsgStatusStopUs
                         else MsgStatusStopThem
          subject <- partActorLeader aid
          msgAdd msgClass $
            makePhrase [MU.Capitalize $ MU.SubjectVerbSg subject $ MU.Text verb]
            <> ending
        IK.VerbMsg verb ending -> do
          subject <- partActorLeader aid
          msgAdd MsgEffectMedium $
            makePhrase [MU.Capitalize $ MU.SubjectVerbSg subject $ MU.Text verb]
            <> ending
        IK.VerbMsgFail verb ending -> do
          subject <- partActorLeader aid
          msgAdd MsgActionWarning $
            makePhrase [MU.Capitalize $ MU.SubjectVerbSg subject $ MU.Text verb]
            <> ending
    SfxItemApplied verbose iid c -> do
      if verbose
      then itemVerbMU MsgActionMinor iid (1, []) "have got activated" c
      else itemVerbMU MsgInnerWorkSpam iid (1, []) "have been triggered" c
    SfxMsgFid _ sfxMsg -> do
      mleader <- getsClient sleader
      case mleader of
        Just{} -> return ()  -- will flush messages when leader moves
        Nothing -> do
          lidV <- viewedLevelUI
          markDisplayNeeded lidV
          recordHistory
      mmsg <- ppSfxMsg sfxMsg
      case mmsg of
        Just (Left (msgClass, msg)) -> msgAdd msgClass msg
        Just (Right (msgClass, (t1, t2))) -> do
          let dotsIfShorter = if t1 == t2 then "" else ".."
          msgAddDistinct msgClass  (t1 <> dotsIfShorter, t2)
        Nothing -> return ()
    SfxRestart -> fadeOutOrIn True
    SfxCollideTile source pos -> do
      COps{cotile} <- getsState scops
      sb <- getsState $ getActorBody source
      lvl <- getLevel $ blid sb
      spart <- partActorLeader source
      let object = MU.AW $ MU.Text $ TK.tname $ okind cotile $ lvl `at` pos
      -- Neutral message, because minor damage and we don't say, which faction.
      msgAdd MsgNeutralEvent $! makeSentence
        [MU.SubjectVerbSg spart "collide", "painfully with", object]
    SfxTaunt voluntary aid -> do
      side <- getsClient sside
      b <- getsState $ getActorBody aid
      unless (bproj b && bfid b == side) $ do  -- don't spam
        spart <- partActorLeader aid
        (_heardSubject, verb) <- displayTaunt voluntary rndToActionUI aid
        let msgClass = if voluntary && bfid b == side
                       then MsgActionComplete  -- give feedback after keypress
                       else MsgMiscellanous
        msgAdd msgClass $! makeSentence [MU.SubjectVerbSg spart (MU.Text verb)]
  
  returnJustLeft :: MonadClientUI m
                 => (MsgClassShowAndSave, Text)
                 -> m (Maybe (Either (MsgClassShowAndSave, Text)
                                     (MsgClassDistinct, (Text, Text))))
  returnJustLeft = return . Just . Left
  
  ppSfxMsg :: MonadClientUI m
           => SfxMsg -> m (Maybe (Either (MsgClassShowAndSave, Text)
                                         (MsgClassDistinct, (Text, Text))))
  ppSfxMsg sfxMsg = case sfxMsg of
    SfxUnexpected reqFailure -> returnJustLeft
      ( MsgActionWarning
      , "Unexpected problem:" <+> showReqFailure reqFailure <> "." )
    SfxExpected itemName reqFailure -> returnJustLeft
      ( MsgActionWarning
      , "The" <+> itemName <+> "is not triggered:"
        <+> showReqFailure reqFailure <> "." )
    SfxExpectedEmbed iid lid reqFailure -> do
      iidSeen <- getsState $ EM.member iid . sitemD
      if iidSeen then do
        itemFull <- getsState $ itemToFull iid
        side <- getsClient sside
        factionD <- getsState sfactionD
        localTime <- getsState $ getLocalTime lid
        let (object1, object2) =
              partItemShortest maxBound side factionD localTime
                               itemFull quantSingle
            name = makePhrase [object1, object2]
        returnJustLeft
          ( MsgActionWarning
          , "The" <+> "embedded" <+> name <+> "is not activated:"
            <+> showReqFailure reqFailure <> "." )
      else return Nothing
    SfxFizzles iid c -> do
      msg <- itemVerbMUGeneral True iid (1, []) "do not work" c
      return $ Just $ Right (MsgStatusWarning, ("It didn't work.", msg))
    SfxNothingHappens iid c -> do
      msg <- itemVerbMUGeneral True iid (1, []) "do nothing, predictably" c
      return $ Just $ Right (MsgStatusBenign, ("Nothing happens.", msg))
    SfxNoItemsForTile toolsToAlterWith -> do
      revCmd <- revCmdMap
      let km = revCmd HumanCmd.AlterDir
          tItems = describeToolsAlternative toolsToAlterWith
      returnJustLeft ( MsgActionWarning
                     , "To transform the terrain, prepare the following items on the ground or in equipment:"
                       <+> tItems
                       <+> "and use the '"
                       <> T.pack (K.showKM km)
                       <> "' terrain modification command."
                     )
    SfxVoidDetection d -> returnJustLeft
      ( MsgMiscellanous
      , makeSentence ["no new", MU.Text $ detectToObject d, "detected"] )
    SfxUnimpressed aid -> do
      msbUI <- getsSession $ EM.lookup aid . sactorUI
      case msbUI of
        Nothing -> return Nothing
        Just sbUI -> do
          let subject = partActor sbUI
              verb = "be unimpressed"
          returnJustLeft ( MsgActionWarning
                         , makeSentence [MU.SubjectVerbSg subject verb] )
    SfxSummonLackCalm aid -> do
      msbUI <- getsSession $ EM.lookup aid . sactorUI
      case msbUI of
        Nothing -> return Nothing
        Just sbUI -> do
          let subject = partActor sbUI
              verb = "lack Calm to summon"
          returnJustLeft ( MsgActionWarning
                         , makeSentence [MU.SubjectVerbSg subject verb] )
    SfxSummonTooManyOwn aid -> do
      msbUI <- getsSession $ EM.lookup aid . sactorUI
      case msbUI of
        Nothing -> return Nothing
        Just sbUI -> do
          let subject = partActor sbUI
              verb = "can't keep track of their numerous friends, let alone summon any more"
          returnJustLeft (MsgActionWarning, makeSentence [subject, verb])
    SfxSummonTooManyAll aid -> do
      msbUI <- getsSession $ EM.lookup aid . sactorUI
      case msbUI of
        Nothing -> return Nothing
        Just sbUI -> do
          let subject = partActor sbUI
              verb = "can't keep track of everybody around, let alone summon anyone else"
          returnJustLeft (MsgActionWarning, makeSentence [subject, verb])
    SfxSummonFailure aid -> do
      msbUI <- getsSession $ EM.lookup aid . sactorUI
      case msbUI of
        Nothing -> return Nothing
        Just sbUI -> do
          let subject = partActor sbUI
              verb = "fail to summon anything"
          returnJustLeft ( MsgActionWarning
                         , makeSentence [MU.SubjectVerbSg subject verb] )
    SfxLevelNoMore -> returnJustLeft
      (MsgActionWarning, "No more levels in this direction.")
    SfxLevelPushed -> returnJustLeft
      (MsgActionWarning, "You notice somebody pushed to another level.")
    SfxBracedImmune aid -> do
      msbUI <- getsSession $ EM.lookup aid . sactorUI
      case msbUI of
        Nothing -> return Nothing
        Just sbUI -> do
          let subject = partActor sbUI
              verb = "be braced and so immune to translocation"
          returnJustLeft ( MsgMiscellanous
                         , makeSentence [MU.SubjectVerbSg subject verb] )
                           -- too common
    SfxEscapeImpossible -> returnJustLeft
      ( MsgActionWarning
      , "Escaping outside is unthinkable for members of this faction." )
    SfxStasisProtects -> returnJustLeft
      ( MsgMiscellanous  -- too common
      , "Paralysis and speed surge require recovery time." )
    SfxWaterParalysisResisted -> return Nothing  -- don't spam
    SfxTransImpossible -> returnJustLeft
      (MsgActionWarning, "Translocation not possible.")
    SfxIdentifyNothing -> returnJustLeft
      (MsgActionWarning, "Nothing to identify.")
    SfxPurposeNothing -> returnJustLeft
      ( MsgActionWarning
      , "The purpose of repurpose cannot be availed without an item"
        <+> ppCStoreIn CGround <> "." )
    SfxPurposeTooFew maxCount itemK -> returnJustLeft
      ( MsgActionWarning
      , "The purpose of repurpose is served by" <+> tshow maxCount
        <+> "pieces of this item, not by" <+> tshow itemK <> "." )
    SfxPurposeUnique -> returnJustLeft
      (MsgActionWarning, "Unique items can't be repurposed.")
    SfxPurposeNotCommon -> returnJustLeft
      (MsgActionWarning, "Only ordinary common items can be repurposed.")
    SfxRerollNothing -> returnJustLeft
      ( MsgActionWarning
      , "The shape of reshape cannot be assumed without an item"
        <+> ppCStoreIn CGround <> "." )
    SfxRerollNotRandom -> returnJustLeft
      (MsgActionWarning, "Only items of variable shape can be reshaped.")
    SfxDupNothing -> returnJustLeft
      ( MsgActionWarning
      , "Mutliplicity won't rise above zero without an item"
        <+> ppCStoreIn CGround <> "." )
    SfxDupUnique -> returnJustLeft
      (MsgActionWarning, "Unique items can't be multiplied.")
    SfxDupValuable -> returnJustLeft
      (MsgActionWarning, "Valuable items can't be multiplied.")
    SfxColdFish -> returnJustLeft
      ( MsgMiscellanous  -- repeatable
      , "Healing attempt from another faction is thwarted by your cold fish attitude." )
    SfxReadyGoods -> returnJustLeft
      ( MsgMiscellanous  -- repeatable
      , "Crafting is alien to you, accustomed to buying ready goods all your life." )
    SfxTimerExtended aid iid cstore delta -> do
      CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui
      aidSeen <- getsState $ EM.member aid . sactorD
      iidSeen <- getsState $ EM.member iid . sitemD
      if aidSeen && iidSeen then do
        b <- getsState $ getActorBody aid
        bUI <- getsSession $ getActorUI aid
        factionD <- getsState sfactionD
        localTime <- getsState $ getLocalTime (blid b)
        itemFull <- getsState $ itemToFull iid
        side <- getsClient sside
        bag <- getsState $ getBodyStoreBag b cstore
        let (name, powers) =
              partItem rwidth (bfid b) factionD localTime itemFull quantSingle
            total = case bag EM.! iid of
              (_, []) -> error $ "" `showFailure` (bag, iid, aid, cstore, delta)
              (_, t:_) -> deltaOfItemTimer localTime t
                -- only exceptionally not singleton list
        storeOwn <- ppContainerWownW partPronounLeader True (CActor aid cstore)
        -- Ideally we'd use a pronoun here, but the action (e.g., hit)
        -- that caused this extension can be invisible to some onlookers.
        -- So their narrative context needs to be taken into account.
        -- The upside is that the messages do not bind pronouns
        -- and so commute and so repetitions can be squashed.
        let cond = [ "condition"
                   | IA.checkFlag Ability.Condition $ aspectRecordFull itemFull ]
            usShow =
              ["the", name, powers] ++ cond
              ++ storeOwn ++ ["will now last longer"]
            usSave =
              ["the", name, powers] ++ cond
              ++ storeOwn ++ ["will now last"]
              ++ [MU.Text $ timeDeltaInSecondsText delta <+> "longer"]
              ++ [MU.Text $ "(total:" <+> timeDeltaInSecondsText total <> ")"]
            -- Note that when enemy actor causes the extension to himself,
            -- the player is not notified at all. So the shorter blurb
            -- displayed on the screen is middle ground and full is in history.
            themShow =
              [partItemShortWownW rwidth side factionD (partActor bUI) localTime
                                  itemFull quantSingle]
              ++ cond ++ ["is extended"]
            themSave =
              [partItemShortWownW rwidth side factionD (partActor bUI) localTime
                                  itemFull quantSingle]
              ++ cond ++ ["is extended by"]
              ++ [MU.Text $ timeDeltaInSecondsText delta]
              ++ [MU.Text $ "(total:" <+> timeDeltaInSecondsText total <> ")"]
            (msgClass, parts1, parts2) =
              if bfid b == side
              then (MsgStatusLongerUs, usShow, usSave)
              else (MsgStatusLongThem, themShow, themSave)
        return $ Just $ Right
          (msgClass, (makeSentence parts1, makeSentence parts2))
      else return Nothing
    SfxCollideActor source target -> do
      sourceSeen <- getsState $ EM.member source . sactorD
      targetSeen <- getsState $ EM.member target . sactorD
      if sourceSeen && targetSeen then do
        spart <- partActorLeader source
        tpart <- partActorLeader target
        -- Neutral message, because minor damage and we don't say, which faction.
        -- And the collision may even be intentional.
        returnJustLeft
          ( MsgSpecialEvent
          , makeSentence
              [MU.SubjectVerbSg spart "collide", "awkwardly with", tpart] )
      else return Nothing
    SfxItemYield iid k lid -> do
      iidSeen <- getsState $ EM.member iid . sitemD
      if iidSeen then do
        let fakeKit = quantSingle
            fakeC = CFloor lid originPoint
            verb = MU.Text $ "yield" <+> makePhrase [MU.CardinalAWs k "item"]
        msg <- itemVerbMUGeneral False iid fakeKit verb fakeC
        returnJustLeft (MsgSpecialEvent, msg)  -- differentiate wrt item creation
      else return Nothing
  
  strike :: MonadClientUI m => Bool -> ActorId -> ActorId -> ItemId -> m ()
  strike catch source target iid = assert (source /= target) $ do
    sourceSeen <- getsState $ EM.member source . sactorD
    if not sourceSeen then do
      tb <- getsState $ getActorBody target
      animate (blid tb) $ blockMiss (bpos tb, bpos tb)
    else do
      CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui
      tb <- getsState $ getActorBody target
      hurtMult <- getsState $ armorHurtBonus source target
      sb <- getsState $ getActorBody source
      sMaxSk <- getsState $ getActorMaxSkills source
      spart <- partActorLeader source
      tpart <- partActorLeader target
      spronoun <- partPronounLeader source
      tpronoun <- partPronounLeader target
      tbUI <- getsSession $ getActorUI target
      localTime <- getsState $ getLocalTime (blid tb)
      itemFullWeapon <- getsState $ itemToFull iid
      let kitWeapon = quantSingle
      side <- getsClient sside
      factionD <- getsState sfactionD
      tfact <- getsState $ (EM.! bfid tb) . sfactionD
      eqpOrgKit <- getsState $ kitAssocs target [CEqp, COrgan]
      orgKit <- getsState $ kitAssocs target [COrgan]
      let isCond (_, (itemFullArmor, _)) =
            IA.checkFlag Ability.Condition $ aspectRecordFull itemFullArmor
          -- We exclude genetic flaws, backstory items, etc., because they
          -- can't be easily taken off, so no point spamming the player.
          isOrdinaryCond ikit@(_, (itemFullArmor, _)) =
            not (IA.checkFlag Ability.MetaGame (aspectRecordFull itemFullArmor))
            && isCond ikit
          relevantSkArmor =
            if bproj sb then Ability.SkArmorRanged else Ability.SkArmorMelee
          rateArmor (iidArmor, (itemFullArmor, (k, _))) =
            ( k * IA.getSkill relevantSkArmor (aspectRecordFull itemFullArmor)
            , ( iidArmor
              , itemFullArmor ) )
          abs15 (v, _) = abs v >= 15
          condArmor = filter abs15 $ map rateArmor $ filter isOrdinaryCond orgKit
          fstGt0 (v, _) = v > 0
          wornArmor =
            filter fstGt0 $ map rateArmor $ filter (not . isCond) eqpOrgKit
      mblockArmor <- case wornArmor of
        [] -> return Nothing
        _ -> Just
             <$> rndToActionUI (frequency $ toFreq "msg armor" wornArmor)
      actorMaxSkills <- getsState sactorMaxSkills
      let (blockWithWhat, blockWithWeapon) = case mblockArmor of
            Just (iidArmor, itemFullArmor) | iidArmor /= btrunk tb ->
              let (object1, object2) =
                    partItemShortest rwidth (bfid tb) factionD localTime
                                     itemFullArmor quantSingle
                  name = MU.Phrase [object1, object2]
              in ( ["with", MU.WownW tpronoun name]
                 , Dice.supDice (IK.idamage $ itemKind itemFullArmor) > 0 )
            _ -> ([], False)
          verb = MU.Text $ IK.iverbHit $ itemKind itemFullWeapon
          partItemChoice =
            if iid `EM.member` borgan sb
            then partItemShortWownW rwidth side factionD spronoun localTime
            else partItemShortAW rwidth side factionD localTime
          weaponNameWith = if iid == btrunk sb
                           then []
                           else ["with", partItemChoice itemFullWeapon kitWeapon]
          sleepy = if bwatch tb `elem` [WSleep, WWake]
                      && tpart /= "you"
                      && bhp tb > 0
                   then "the sleepy"
                   else ""
          unBurn (IK.Burn d) = Just d
          unBurn _ = Nothing
          unRefillHP (IK.RefillHP n) | n < 0 = Just (-n)
          unRefillHP _ = Nothing
          kineticDmg =
            let dmg = Dice.supDice $ IK.idamage $ itemKind itemFullWeapon
                rawDeltaHP = into @Int64 sHurt * xM dmg `divUp` 100
            in case btrajectory sb of
              Just (_, speed) | bproj sb ->
                - modifyDamageBySpeed rawDeltaHP speed
              _ -> - rawDeltaHP
          burnDmg = - (sum $ map Dice.supDice
                       $ mapMaybe unBurn $ IK.ieffects $ itemKind itemFullWeapon)
          fillDmg =
            - (sum $ mapMaybe unRefillHP $ IK.ieffects $ itemKind itemFullWeapon)
          -- For variety, attack adverb is based on attacker's and weapon's
          -- damage potential as compared to victim's current HP.
          -- We are not taking into account victim's armor yet.
          sHurt = armorHurtCalculation (bproj sb) sMaxSk Ability.zeroSkills
          nonPiercingDmg = burnDmg + fillDmg
          sDamage = min 0 $ kineticDmg + xM nonPiercingDmg
          deadliness = 1000 * (- sDamage) `div` max 1 (bhp tb)
          strongly
            | deadliness >= 10000 = "artfully"
            | deadliness >= 5000 = "madly"
            | deadliness >= 2000 = "mercilessly"
            | deadliness >= 1000 = "murderously"  -- one blow can wipe out all HP
            | deadliness >= 700 = "devastatingly"
            | deadliness >= 500 = "vehemently"
            | deadliness >= 400 = "forcefully"
            | deadliness >= 350 = "sturdily"
            | deadliness >= 300 = "accurately"
            | deadliness >= 20 = ""  -- common, terse case, between 2% and 30%
            | deadliness >= 10 = "cautiously"
            | deadliness >= 5 = "guardedly"
            | deadliness >= 3 = "hesitantly"
            | deadliness >= 2 = "clumsily"
            | deadliness >= 1 = "haltingly"
            | otherwise = "feebly"
          -- Here we take into account armor, so we look at @hurtMult@,
          -- so we finally convey the full info about effectiveness of the strike.
          blockHowWell  -- under some conditions, the message not shown at all
            | hurtMult > 90 = "incompetently"
            | hurtMult > 80 = "too late"
            | hurtMult > 70 = "too slowly"
            | hurtMult > 20 || nonPiercingDmg < 0 =
                              if | deadliness >= 2000 -> "marginally"
                                 | deadliness >= 1000 -> "partially"
                                 | deadliness >= 100 -> "partly"  -- common
                                 | deadliness >= 50 -> "to an extent"
                                 | deadliness >= 20 -> "to a large extent"
                                 | deadliness >= 5 -> "for the major part"
                                 | otherwise -> "for the most part"
            | hurtMult > 1 = if | actorWaits tb -> "doggedly"
                                | deadliness >= 50 -> "easily"  -- common
                                | deadliness >= 20 -> "effortlessly"
                                | deadliness >= 5 -> "nonchalantly"
                                | otherwise -> "bemusedly"
            | otherwise = "almost completely"
                -- a fraction gets through, but if fast missile, can be deadly
          avertVerb = if actorWaits tb then "avert it" else "ward it off"
          blockPhrase =
            let (subjectBlock, verbBlock) =
                  if | not $ bproj sb ->
                       (tpronoun, if blockWithWeapon
                                  then "parry"
                                  else "block")
                     | tpronoun == "it"
                       || projectileHitsWeakly && tpronoun /= "you" ->
                       -- Avoid ambiguity.
                       (partActor tbUI, avertVerb)
                     | otherwise -> (tpronoun, avertVerb)
            in MU.SubjectVerbSg subjectBlock verbBlock
          surprisinglyGoodDefense = deadliness >= 20 && hurtMult <= 70
          surprisinglyBadDefense = deadliness < 20 && hurtMult > 70
          yetButAnd
            | surprisinglyGoodDefense = ", but"
            | surprisinglyBadDefense = ", yet"
            | otherwise = " and"  -- no surprises
          projectileHitsWeakly = bproj sb && deadliness < 20
          msgArmor = if not projectileHitsWeakly
                          -- ensures if attack msg terse, armor message
                          -- mentions object, so we know who is hit
                        && hurtMult > 90
                          -- at most minor armor relatively to skill of the hit
                        && (null condArmor || deadliness < 100)
                        || null blockWithWhat
                        || kineticDmg >= -1000  -- -1/1000 HP
                     then ""
                     else yetButAnd
                          <+> makePhrase ([blockPhrase, blockHowWell]
                                          ++ blockWithWhat)
          ps = (bpos tb, bpos sb)
          basicAnim
            | hurtMult > 70 = twirlSplash ps Color.BrRed Color.Red
            | hurtMult > 1 = if nonPiercingDmg >= 0  -- no extra anim
                             then blockHit ps Color.BrRed Color.Red
                             else blockMiss ps
            | otherwise = blockMiss ps
          targetIsFoe = bfid sb == side  -- no big news if others hit our foes
                        && isFoe (bfid tb) tfact side
          targetIsFriend = isFriend (bfid tb) tfact side
                             -- warning if anybody hits our friends
          msgClassMelee =
            if targetIsFriend then MsgMeleeNormalUs else MsgMeleeOthers
          msgClassRanged =
            if targetIsFriend then MsgRangedNormalUs else MsgRangedOthers
          animateAlive lid anim =
            if bhp tb > 0
            then animate lid anim
            else animate lid $ twirlSplashShort ps Color.BrRed Color.Red
          tutorialHintBenignFoe =
            when (bfid sb == side
                  && not (actorCanMeleeToHarm actorMaxSkills target tb)) $
              tutorialHintMsgAdd CannotHarmYouInMelee
      -- The messages about parrying and immediately afterwards dying
      -- sound goofy, but there is no easy way to prevent that.
      -- And it's consistent.
      -- If/when death blow instead sets HP to 1 and only the next below 1,
      -- we can check here for HP==1; also perhaps actors with HP 1 should
      -- not be able to block.
      if | catch -> do  -- charge not needed when catching
           let msg = makeSentence
                       [MU.SubjectVerbSg spart "catch", tpart, "skillfully"]
           msgAdd MsgSpecialEvent msg
           when (bfid sb == side) $
             tutorialHintMsgAdd CaughtProjectile
           animate (blid tb) $ blockHit ps Color.BrGreen Color.Green
         | not (hasCharge localTime kitWeapon) -> do
           -- Can easily happen with a thrown discharged item.
           -- Much less plausible with a wielded weapon.
           -- Theoretically possible if the weapon not identified
           -- (then timeout is a mean estimate), but they usually should be,
           -- even in foes' possession.
           let msg = if bproj sb
                     then makePhrase
                            [MU.Capitalize $ MU.SubjectVerbSg spart "connect"]
                          <> ", but it may be completely discharged."
                     else makePhrase
                            ([ MU.Capitalize $ MU.SubjectVerbSg spart "try"
                             , "to"
                             , verb
                             , tpart ]
                             ++ weaponNameWith)
                          <> if null weaponNameWith
                             then ", but there are no charges left."
                             else ", but it may be not readied yet."
           msgAdd MsgSpecialEvent msg  -- and no animation
         | bproj sb && bproj tb -> do  -- server sends unless both are blasts
           -- Short message.
           msgAdd MsgSpecialEvent $
             makeSentence [MU.SubjectVerbSg spart "intercept", tpart]
           -- Basic non-bloody animation regardless of stats.
           animateAlive (blid tb) $ blockHit ps Color.BrBlue Color.Blue
         | kineticDmg >= -1000  -- -1/1000 HP
           -- We ignore nested effects, because they are, in general, avoidable.
           && nonPiercingDmg >= 0 -> do
           let adverb | itemSuspect itemFullWeapon && bfid sb == side =
                          "tentatively"  -- we didn't identify the weapon before
                      | bproj sb = "lightly"
                      | otherwise = "delicately"
               msg = makeSentence $
                 [MU.SubjectVerbSg spart verb, tpart, adverb]
                 ++ if bproj sb then [] else weaponNameWith
           msgAdd msgClassMelee msg  -- too common for color
           when (bfid sb == side || bfid tb == side) $
             tutorialHintMsgAdd HitsWithNoDirectDamage
           animate (blid tb) $ subtleHit ps
         | bproj sb -> do  -- more terse than melee, because sometimes very spammy
           let msgRangedPowerful | targetIsFoe = MsgRangedMightyWe
                                 | targetIsFriend = MsgRangedMightyUs
                                 | otherwise = msgClassRanged
               (attackParts, msgRanged)
                 | projectileHitsWeakly =
                   ( [MU.SubjectVerbSg spart "connect"]  -- weak, so terse
                   , msgClassRanged )
                 | deadliness >= 300 =
                   ( [MU.SubjectVerbSg spart verb, tpart, "powerfully"]
                   , if targetIsFriend || deadliness >= 700
                     then msgRangedPowerful
                     else msgClassRanged )
                 | otherwise =
                   ( [MU.SubjectVerbSg spart verb, tpart]  -- strong, for a proj
                   , msgClassRanged )
           msgAdd msgRanged $ makePhrase [MU.Capitalize $ MU.Phrase attackParts]
                              <> msgArmor <> "."
           tutorialHintBenignFoe
           animateAlive (blid tb) basicAnim
         | bproj tb -> do  -- much less emotion and the victim not active.
           let attackParts =
                 [MU.SubjectVerbSg spart verb, tpart] ++ weaponNameWith
           msgAdd MsgMeleeOthers $ makeSentence attackParts
           animateAlive (blid tb) basicAnim
         | otherwise -> do  -- ordinary melee
           let msgMeleeInteresting | targetIsFoe = MsgMeleeComplexWe
                                   | targetIsFriend = MsgMeleeComplexUs
                                   | otherwise = msgClassMelee
               msgMeleePowerful | targetIsFoe = MsgMeleeMightyWe
                                | targetIsFriend = MsgMeleeMightyUs
                                | otherwise = msgClassMelee
               attackParts =
                 [MU.SubjectVerbSg spart verb, sleepy, tpart, strongly]
                 ++ weaponNameWith
               (tmpInfluenceBlurb, msgClassInfluence) =
                 if null condArmor || T.null msgArmor
                 then ("", msgClassMelee)
                 else
                   let (armor, (_, itemFullArmor)) =
                         maximumBy (comparing $ abs . fst) condArmor
                       (object1, object2) =
                         partItemShortest rwidth (bfid tb) factionD localTime
                                          itemFullArmor quantSingle
                       name = makePhrase [object1, object2]
                       msgText =
                         if hurtMult > 70
                         then (if armor <= -15
                               then ", due to being"
                               else assert (armor >= 15) ", regardless of being")
                              <+> name
                         else (if armor >= 15
                               then ", thanks to being"
                               else assert (armor <= -15) ", despite being")
                              <+> name
                   in (msgText, msgMeleeInteresting)
               msgClass = if targetIsFriend && deadliness >= 300
                             || deadliness >= 2000
                          then msgMeleePowerful
                          else msgClassInfluence
           msgAdd msgClass $ makePhrase [MU.Capitalize $ MU.Phrase attackParts]
                             <> msgArmor <> tmpInfluenceBlurb <> "."
           tutorialHintBenignFoe
           animateAlive (blid tb) basicAnim
  