packages feed

LambdaHack 0.10.2.0 → 0.10.3.0

raw patch · 145 files changed

+8899/−7020 lines, 145 filesdep +QuickCheckdep +ansi-terminaldep +doctestdep −gtk3dep −hscursesdep −int-castdep ~asyncdep ~basedep ~enummapsetPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: QuickCheck, ansi-terminal, doctest, tasty-quickcheck, witch

Dependencies removed: gtk3, hscurses, int-cast, vty

Dependency ranges changed: async, base, enummapset, filepath, splitmix, vector

API changes (from Hackage documentation)

- Content.ItemKindEmbed: embedsGNSingleton :: [GroupName ItemKind]
- Content.ItemKindEmbed: pattern S_SANDSTONE_ROCK :: GroupName ItemKind
- Game.LambdaHack.Client: uCmdline :: UIOptions -> [String]
- Game.LambdaHack.Client.HandleAtomicM: invalidateInMelee :: MonadClient m => LevelId -> m ()
- Game.LambdaHack.Client.HandleAtomicM: invalidateInMeleeDueToItem :: MonadClient m => ActorId -> CStore -> m ()
- Game.LambdaHack.Client.UI: displayRespSfxAtomicUI :: (MonadClient m, MonadClientUI m) => SfxAtomic -> m ()
- Game.LambdaHack.Client.UI: displayRespUpdAtomicUI :: (MonadClient m, MonadClientUI m) => UpdAtomic -> m ()
- Game.LambdaHack.Client.UI: humanCommand :: forall m. (MonadClient m, MonadClientUI m) => m ReqUI
- Game.LambdaHack.Client.UI: uCmdline :: UIOptions -> [String]
- Game.LambdaHack.Client.UI.Content.Screen: [rwrap] :: ScreenContent -> X
- Game.LambdaHack.Client.UI.DisplayAtomicM: aidVerbDuplicateMU :: (MonadClient m, MonadClientUI m, MsgShared a) => a -> ActorId -> Part -> m Bool
- Game.LambdaHack.Client.UI.DisplayAtomicM: aidVerbMU :: (MonadClient m, MonadClientUI m, MsgShared a) => a -> ActorId -> Part -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: createActorUI :: (MonadClient m, MonadClientUI m) => Bool -> ActorId -> Actor -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: destroyActorUI :: (MonadClient m, MonadClientUI m) => Bool -> ActorId -> Actor -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: discover :: (MonadClient m, MonadClientUI m) => Container -> ItemId -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: displaceActorUI :: (MonadClient m, MonadClientUI m) => ActorId -> ActorId -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: displayGameOverAnalytics :: (MonadClient m, MonadClientUI m) => FactionAnalytics -> GenerationAnalytics -> m KM
- Game.LambdaHack.Client.UI.DisplayAtomicM: displayGameOverLoot :: (MonadClient m, MonadClientUI m) => (ItemBag, Int) -> GenerationAnalytics -> m KM
- Game.LambdaHack.Client.UI.DisplayAtomicM: displayRespSfxAtomicUI :: (MonadClient m, MonadClientUI m) => SfxAtomic -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: displayRespUpdAtomicUI :: (MonadClient m, MonadClientUI m) => UpdAtomic -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: itemAidDistinctMU :: (MonadClient m, MonadClientUI m) => MsgClassDistinct -> ActorId -> Part -> Part -> ItemId -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: itemAidVerbMU :: (MonadClient m, MonadClientUI m, MsgShared a) => a -> ActorId -> Part -> ItemId -> Either Int Int -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: itemVerbMU :: (MonadClient m, MonadClientUI m, MsgShared a) => a -> ItemId -> ItemQuant -> Part -> Container -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: itemVerbMUGeneral :: MonadClientUI m => Bool -> ItemId -> ItemQuant -> Part -> Container -> m Text
- Game.LambdaHack.Client.UI.DisplayAtomicM: itemVerbMUShort :: (MonadClient m, MonadClientUI m, MsgShared a) => a -> ItemId -> ItemQuant -> Part -> Container -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: lookAtMove :: (MonadClient m, MonadClientUI m) => ActorId -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: manyItemsAidVerbMU :: (MonadClient m, MonadClientUI m, MsgShared a) => a -> ActorId -> Part -> [(ItemId, ItemQuant)] -> (Int -> Either (Maybe Int) Int) -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: markDisplayNeeded :: MonadClientUI m => LevelId -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: mitemAidVerbMU :: (MonadClient m, MonadClientUI m, MsgShared a) => a -> ActorId -> Part -> ItemId -> Maybe Part -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: moveActor :: (MonadClient m, MonadClientUI m) => ActorId -> Point -> Point -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: moveItemUI :: (MonadClient m, MonadClientUI m) => ItemId -> Int -> ActorId -> CStore -> CStore -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: ppHearDistanceAdjective :: Maybe Int -> Text
- Game.LambdaHack.Client.UI.DisplayAtomicM: ppHearDistanceAdverb :: Maybe Int -> Text
- Game.LambdaHack.Client.UI.DisplayAtomicM: ppHearMsg :: MonadClientUI m => Maybe Int -> HearMsg -> m Text
- Game.LambdaHack.Client.UI.DisplayAtomicM: ppSfxMsg :: MonadClientUI m => SfxMsg -> m (Maybe (Either (MsgClassShowAndSave, Text) (MsgClassDistinct, (Text, Text))))
- Game.LambdaHack.Client.UI.DisplayAtomicM: quitFactionUI :: (MonadClient m, MonadClientUI m) => FactionId -> Maybe Status -> Maybe (FactionAnalytics, GenerationAnalytics) -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: spotItemBag :: forall m. (MonadClient m, MonadClientUI m) => Bool -> Container -> ItemBag -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: strike :: (MonadClient m, MonadClientUI m) => Bool -> ActorId -> ActorId -> ItemId -> m ()
- Game.LambdaHack.Client.UI.DisplayAtomicM: updateItemSlot :: MonadClientUI m => Container -> ItemId -> m ()
- Game.LambdaHack.Client.UI.FrameM: fadeOutOrIn :: MonadClientUI m => Bool -> m ()
- Game.LambdaHack.Client.UI.FrameM: oneLineBasicFrame :: MonadClientUI m => LevelId -> DisplayFont -> m PreFrame3
- Game.LambdaHack.Client.UI.FrameM: pushFrame :: MonadClientUI m => Bool -> m ()
- Game.LambdaHack.Client.UI.Frontend: [FrontAdd] :: KMP -> FrontReq ()
- Game.LambdaHack.Client.UI.Frontend: [FrontAutoYes] :: Bool -> FrontReq ()
- Game.LambdaHack.Client.UI.Frontend.Chosen: frontendName :: String
- Game.LambdaHack.Client.UI.Frontend.Chosen: startup :: ScreenContent -> ClientOptions -> IO RawFrontend
- Game.LambdaHack.Client.UI.HandleHelperM: displayItemLorePointedAt :: (MonadClient m, MonadClientUI m) => ItemBag -> Int -> (ItemId -> ItemFull -> Int -> Text) -> Int -> SingleItemSlots -> Bool -> m KM
- Game.LambdaHack.Client.UI.HandleHelperM: memberCycle :: (MonadClient m, MonadClientUI m) => Bool -> Direction -> m MError
- Game.LambdaHack.Client.UI.HandleHelperM: memberCycleLevel :: (MonadClient m, MonadClientUI m) => Bool -> Direction -> m MError
- Game.LambdaHack.Client.UI.HandleHelperM: viewLoreItems :: (MonadClient m, MonadClientUI m) => String -> SingleItemSlots -> ItemBag -> Text -> (Int -> SingleItemSlots -> m Bool) -> Bool -> m KM
- Game.LambdaHack.Client.UI.HandleHumanLocalM: memberCycleHuman :: (MonadClient m, MonadClientUI m) => Direction -> m MError
- Game.LambdaHack.Client.UI.HandleHumanLocalM: memberCycleLevelHuman :: (MonadClient m, MonadClientUI m) => Direction -> m MError
- Game.LambdaHack.Client.UI.HandleHumanLocalM: selectAid :: (MonadClient m, MonadClientUI m) => ActorId -> m ()
- Game.LambdaHack.Client.UI.HandleHumanM: restrictedCmdSemInCxtOfKM :: (MonadClient m, MonadClientUI m) => KM -> HumanCmd -> m (Either MError ReqUI)
- Game.LambdaHack.Client.UI.HumanCmd: CmdMainMenu :: CmdCategory
- Game.LambdaHack.Client.UI.HumanCmd: MemberCycle :: Direction -> HumanCmd
- Game.LambdaHack.Client.UI.HumanCmd: MemberCycleLevel :: Direction -> HumanCmd
- Game.LambdaHack.Client.UI.MonadClientUI: addPressedControlEsc :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.MonadClientUI: addPressedKey :: MonadClientUI m => KMP -> m ()
- Game.LambdaHack.Client.UI.MonadClientUI: getLeaderUI :: MonadClientUI m => m ActorId
- Game.LambdaHack.Client.UI.MonadClientUI: leaderSkillsClientUI :: MonadClientUI m => m Skills
- Game.LambdaHack.Client.UI.MonadClientUI: miniHintAimingBare :: Text
- Game.LambdaHack.Client.UI.MonadClientUI: miniHintAimingLore :: Text
- Game.LambdaHack.Client.UI.Msg: renderWholeReport :: [(AttrString, Int)] -> [AttrString] -> [AttrString]
- Game.LambdaHack.Client.UI.Msg: tutorialHint :: MsgClass -> Bool
- Game.LambdaHack.Client.UI.Overlay: indentSplitAttrString2 :: Bool -> Int -> AttrString -> [AttrLine]
- Game.LambdaHack.Client.UI.SessionUI: toggleMarkVision :: SessionUI -> SessionUI
- Game.LambdaHack.Client.UI.Slideshow: FontSetup :: DisplayFont -> DisplayFont -> DisplayFont -> FontSetup
- Game.LambdaHack.Client.UI.Slideshow: [monoFont] :: FontSetup -> DisplayFont
- Game.LambdaHack.Client.UI.Slideshow: [propFont] :: FontSetup -> DisplayFont
- Game.LambdaHack.Client.UI.Slideshow: [squareFont] :: FontSetup -> DisplayFont
- Game.LambdaHack.Client.UI.Slideshow: data DisplayFont
- Game.LambdaHack.Client.UI.Slideshow: data FontSetup
- Game.LambdaHack.Client.UI.Slideshow: endMsg :: String
- Game.LambdaHack.Client.UI.Slideshow: instance GHC.Classes.Eq Game.LambdaHack.Client.UI.Slideshow.DisplayFont
- Game.LambdaHack.Client.UI.Slideshow: instance GHC.Enum.Enum Game.LambdaHack.Client.UI.Slideshow.DisplayFont
- Game.LambdaHack.Client.UI.Slideshow: instance GHC.Show.Show Game.LambdaHack.Client.UI.Slideshow.DisplayFont
- Game.LambdaHack.Client.UI.Slideshow: isMonoFont :: DisplayFont -> Bool
- Game.LambdaHack.Client.UI.Slideshow: isSquareFont :: DisplayFont -> Bool
- Game.LambdaHack.Client.UI.Slideshow: maxYofOverlay :: Overlay -> Int
- Game.LambdaHack.Client.UI.Slideshow: monoFontSetup :: FontSetup
- Game.LambdaHack.Client.UI.Slideshow: moreMsg :: String
- Game.LambdaHack.Client.UI.Slideshow: multiFontSetup :: FontSetup
- Game.LambdaHack.Client.UI.Slideshow: singleFontSetup :: FontSetup
- Game.LambdaHack.Client.UI.Slideshow: textSize :: DisplayFont -> [a] -> Int
- Game.LambdaHack.Client.UI.UIOptions: [uCmdline] :: UIOptions -> [String]
- Game.LambdaHack.Client.UI.UIOptions: [uHistory1PerLine] :: UIOptions -> Bool
- Game.LambdaHack.Client.UI.UIOptions: [uScreen1PerLine] :: UIOptions -> Bool
- Game.LambdaHack.Common.Item: instance GHC.Arr.Ix Game.LambdaHack.Common.Item.ItemKindIx
- Game.LambdaHack.Common.PointArray: instance Game.LambdaHack.Common.PointArray.UnboxRepClass (Game.LambdaHack.Definition.Defs.ContentId k)
- Game.LambdaHack.Content.CaveKind: [clegendDarkTile] :: CaveKind -> GroupName TileKind
- Game.LambdaHack.Content.CaveKind: [clegendLitTile] :: CaveKind -> GroupName TileKind
- Game.LambdaHack.Content.ModeKind: LeaderAI :: AutoLeader -> LeaderMode
- Game.LambdaHack.Content.ModeKind: LeaderNull :: LeaderMode
- Game.LambdaHack.Content.ModeKind: LeaderUI :: AutoLeader -> LeaderMode
- Game.LambdaHack.Content.ModeKind: data LeaderMode
- Game.LambdaHack.Content.ModeKind: instance Data.Binary.Class.Binary Game.LambdaHack.Content.ModeKind.LeaderMode
- Game.LambdaHack.Content.ModeKind: instance GHC.Classes.Eq Game.LambdaHack.Content.ModeKind.LeaderMode
- Game.LambdaHack.Content.ModeKind: instance GHC.Generics.Generic Game.LambdaHack.Content.ModeKind.LeaderMode
- Game.LambdaHack.Content.ModeKind: instance GHC.Show.Show Game.LambdaHack.Content.ModeKind.LeaderMode
- Game.LambdaHack.Content.PlaceKind: [poverrideDark] :: PlaceKind -> [(Char, GroupName TileKind)]
- Game.LambdaHack.Content.PlaceKind: [poverrideLit] :: PlaceKind -> [(Char, GroupName TileKind)]
- Game.LambdaHack.Content.RuleKind: [rXmax] :: RuleContent -> X
- Game.LambdaHack.Content.RuleKind: [rYmax] :: RuleContent -> Y
- Game.LambdaHack.Content.RuleKind: [rsymbolProjectile] :: RuleContent -> Char
- Game.LambdaHack.Content.TileKind: pattern LEGEND_DARK :: GroupName TileKind
- Game.LambdaHack.Content.TileKind: pattern LEGEND_LIT :: GroupName TileKind
- Game.LambdaHack.Core.Prelude: intCast :: (Integral a, Integral b, IsIntSubType a b ~ 'True) => a -> b
- Game.LambdaHack.Definition.Defs: GroupName :: Text -> GroupName a
- Game.LambdaHack.Definition.Defs: [fromGroupName] :: GroupName a -> Text
- Game.LambdaHack.Definition.Defs: fromContentId :: ContentId c -> Word16
- Game.LambdaHack.Definition.Defs: instance Control.DeepSeq.NFData (Game.LambdaHack.Definition.Defs.GroupName a)
- Game.LambdaHack.Definition.Defs: instance Data.Binary.Class.Binary (Game.LambdaHack.Definition.Defs.ContentId c)
- Game.LambdaHack.Definition.Defs: instance Data.Binary.Class.Binary (Game.LambdaHack.Definition.Defs.GroupName a)
- Game.LambdaHack.Definition.Defs: instance Data.Hashable.Class.Hashable (Game.LambdaHack.Definition.Defs.ContentId c)
- Game.LambdaHack.Definition.Defs: instance Data.Hashable.Class.Hashable (Game.LambdaHack.Definition.Defs.GroupName a)
- Game.LambdaHack.Definition.Defs: instance GHC.Classes.Eq (Game.LambdaHack.Definition.Defs.ContentId c)
- Game.LambdaHack.Definition.Defs: instance GHC.Classes.Eq (Game.LambdaHack.Definition.Defs.GroupName a)
- Game.LambdaHack.Definition.Defs: instance GHC.Classes.Ord (Game.LambdaHack.Definition.Defs.ContentId c)
- Game.LambdaHack.Definition.Defs: instance GHC.Classes.Ord (Game.LambdaHack.Definition.Defs.GroupName a)
- Game.LambdaHack.Definition.Defs: instance GHC.Enum.Enum (Game.LambdaHack.Definition.Defs.ContentId c)
- Game.LambdaHack.Definition.Defs: instance GHC.Show.Show (Game.LambdaHack.Definition.Defs.ContentId c)
- Game.LambdaHack.Definition.Defs: instance GHC.Show.Show (Game.LambdaHack.Definition.Defs.GroupName a)
- Game.LambdaHack.Definition.Defs: newtype GroupName a
- Game.LambdaHack.Definition.Defs: toContentId :: Word16 -> ContentId c
- Game.LambdaHack.Definition.Flavour: Flavour :: FancyName -> Color -> Flavour
- Game.LambdaHack.Server.DungeonGen.Place: olegend :: COps -> GroupName TileKind -> Rnd (EnumMap Char (Int, Int, ContentId TileKind), EnumMap Char (ContentId TileKind))
+ Content.ItemKindActor: pattern S_SANDSTONE_ROCK :: GroupName ItemKind
+ Content.ItemKindBlast: pattern S_FOCUSED_SLOWNESS_MIST :: GroupName ItemKind
+ Content.ItemKindBlast: pattern S_VIOLENT_SLOWNESS_MIST :: GroupName ItemKind
+ Content.TileKind: pattern FLOOR_ACTOR_ITEM_DARK :: GroupName TileKind
+ Content.TileKind: pattern FLOOR_ACTOR_ITEM_LIT :: GroupName TileKind
+ Content.TileKind: pattern S_PILLAR :: GroupName TileKind
+ Content.TileKind: pattern S_RUBBLE_PILE :: GroupName TileKind
+ Content.TileKind: pattern S_SHALLOW_WATER_DARK :: GroupName TileKind
+ Content.TileKind: pattern S_SHALLOW_WATER_LIT :: GroupName TileKind
+ Content.TileKind: pattern S_WALL_DARK :: GroupName TileKind
+ Content.TileKind: pattern S_WALL_HORIZONTAL_DARK :: GroupName TileKind
+ Game.LambdaHack.Atomic: UpdMuteMessages :: FactionId -> Bool -> UpdAtomic
+ Game.LambdaHack.Atomic.CmdAtomic: UpdMuteMessages :: FactionId -> Bool -> UpdAtomic
+ Game.LambdaHack.Client: RespQueryUIunderAI :: Response
+ Game.LambdaHack.Client: uOverrideCmdline :: UIOptions -> [String]
+ Game.LambdaHack.Client.HandleAtomicM: updateInMeleeDueToActor :: MonadClient m => Actor -> m ()
+ Game.LambdaHack.Client.HandleAtomicM: updateInMeleeDueToItem :: MonadClient m => ActorId -> CStore -> m ()
+ Game.LambdaHack.Client.HandleAtomicM: updateInMeleeInDungeon :: MonadClient m => m ()
+ Game.LambdaHack.Client.LoopM: longestDelay :: POSIXTime
+ Game.LambdaHack.Client.LoopM: loopAI :: (MonadClientSetup m, MonadClientUI m, MonadClientAtomic m, MonadClientReadResponse m, MonadClientWriteRequest m) => m ()
+ Game.LambdaHack.Client.LoopM: loopUI :: (MonadClientSetup m, MonadClientUI m, MonadClientAtomic m, MonadClientReadResponse m, MonadClientWriteRequest m) => POSIXTime -> m ()
+ Game.LambdaHack.Client.MonadClient: insertInMeleeM :: MonadClient m => LevelId -> m ()
+ Game.LambdaHack.Client.Response: RespQueryUIunderAI :: Response
+ Game.LambdaHack.Client.UI: [sautoYes] :: SessionUI -> Bool
+ Game.LambdaHack.Client.UI: [smuteMessages] :: SessionUI -> Bool
+ Game.LambdaHack.Client.UI: [sregainControl] :: SessionUI -> Bool
+ Game.LambdaHack.Client.UI: [sreqDelay] :: SessionUI -> ReqDelay
+ Game.LambdaHack.Client.UI: [sreqPending] :: SessionUI -> Maybe RequestUI
+ Game.LambdaHack.Client.UI: [sreqQueried] :: SessionUI -> Bool
+ Game.LambdaHack.Client.UI: queryUIunderAI :: (MonadClient m, MonadClientUI m) => m RequestUI
+ Game.LambdaHack.Client.UI: stepQueryUI :: (MonadClient m, MonadClientUI m) => m (Maybe ReqUI)
+ Game.LambdaHack.Client.UI: stepQueryUIwithLeader :: (MonadClient m, MonadClientUI m) => m (Maybe RequestUI)
+ Game.LambdaHack.Client.UI: uOverrideCmdline :: UIOptions -> [String]
+ Game.LambdaHack.Client.UI: watchRespSfxAtomicUI :: MonadClientUI m => SfxAtomic -> m ()
+ Game.LambdaHack.Client.UI: watchRespUpdAtomicUI :: MonadClientUI m => UpdAtomic -> m ()
+ Game.LambdaHack.Client.UI.Frame: [singleSquareOverlay] :: SingleFrame -> OverlaySpace
+ Game.LambdaHack.Client.UI.FrameM: basicFrameForAnimation :: MonadClientUI m => LevelId -> Maybe Bool -> m PreFrame3
+ Game.LambdaHack.Client.UI.FrameM: restoreLeaderFromRun :: MonadClientUI m => m ()
+ Game.LambdaHack.Client.UI.Frontend.ANSI: frontendName :: String
+ Game.LambdaHack.Client.UI.Frontend.ANSI: startup :: ScreenContent -> IO RawFrontend
+ Game.LambdaHack.Client.UI.HandleHelperM: itemOverlayFromState :: SingleItemSlots -> LevelId -> ItemBag -> Bool -> CCUI -> FactionId -> DiscoveryBenefit -> FontSetup -> State -> OKX
+ Game.LambdaHack.Client.UI.HandleHelperM: okxItemLorePointedAt :: MonadClientUI m => DisplayFont -> Int -> Bool -> ItemBag -> Int -> (ItemId -> ItemFull -> Int -> Text) -> Int -> SingleItemSlots -> m OKX
+ Game.LambdaHack.Client.UI.HandleHelperM: pointmanCycle :: MonadClientUI m => ActorId -> Bool -> Direction -> m MError
+ Game.LambdaHack.Client.UI.HandleHelperM: pointmanCycleLevel :: MonadClientUI m => ActorId -> Bool -> Direction -> m MError
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: pointmanCycleHuman :: MonadClientUI m => ActorId -> Direction -> m MError
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: pointmanCycleLevelHuman :: MonadClientUI m => ActorId -> Direction -> m MError
+ Game.LambdaHack.Client.UI.HandleHumanM: addLeader :: Monad m => (ActorId -> m ()) -> CmdLeaderNeed m
+ Game.LambdaHack.Client.UI.HandleHumanM: cmdSemanticsLeader :: (MonadClient m, MonadClientUI m) => HumanCmd -> CmdLeaderNeed m
+ Game.LambdaHack.Client.UI.HandleHumanM: data CmdLeaderNeed m
+ Game.LambdaHack.Client.UI.HandleHumanM: weaveLeader :: Monad m => (ActorId -> m (FailOrCmd ReqUI)) -> CmdLeaderNeed m
+ Game.LambdaHack.Client.UI.HumanCmd: PointmanCycle :: Direction -> HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: PointmanCycleLevel :: Direction -> HumanCmd
+ Game.LambdaHack.Client.UI.InventoryM: inventoryInRightPane :: MonadClientUI m => ActorId -> SingleItemSlots -> ItemBag -> ItemDialogMode -> KeyOrSlot -> m OKX
+ Game.LambdaHack.Client.UI.InventoryM: placeCloseUp :: MonadClientUI m => [(ContentId PlaceKind, (EnumSet LevelId, Int, Int, Int))] -> Bool -> Int -> m (Text, [(DisplayFont, [Text])])
+ Game.LambdaHack.Client.UI.InventoryM: skillCloseUp :: MonadClientUI m => ActorId -> Int -> m (Text, AttrString)
+ Game.LambdaHack.Client.UI.ItemDescription: viewItemBenefitColored :: DiscoveryBenefit -> ItemId -> ItemFull -> AttrCharW32
+ Game.LambdaHack.Client.UI.MonadClientUI: getMiniHintAiming :: MonadClientUI m => m Text
+ Game.LambdaHack.Client.UI.MonadClientUI: mxhairToPos :: MonadClientUI m => m (Maybe Point)
+ Game.LambdaHack.Client.UI.Msg: isTutorialHint :: MsgClass -> Bool
+ Game.LambdaHack.Client.UI.Overlay: FontSetup :: DisplayFont -> DisplayFont -> DisplayFont -> FontSetup
+ Game.LambdaHack.Client.UI.Overlay: [monoFont] :: FontSetup -> DisplayFont
+ Game.LambdaHack.Client.UI.Overlay: [propFont] :: FontSetup -> DisplayFont
+ Game.LambdaHack.Client.UI.Overlay: [squareFont] :: FontSetup -> DisplayFont
+ Game.LambdaHack.Client.UI.Overlay: breakAtSpace :: AttrString -> (AttrString, AttrString)
+ Game.LambdaHack.Client.UI.Overlay: data DisplayFont
+ Game.LambdaHack.Client.UI.Overlay: data FontSetup
+ Game.LambdaHack.Client.UI.Overlay: instance GHC.Classes.Eq Game.LambdaHack.Client.UI.Overlay.DisplayFont
+ Game.LambdaHack.Client.UI.Overlay: instance GHC.Enum.Enum Game.LambdaHack.Client.UI.Overlay.DisplayFont
+ Game.LambdaHack.Client.UI.Overlay: instance GHC.Show.Show Game.LambdaHack.Client.UI.Overlay.DisplayFont
+ Game.LambdaHack.Client.UI.Overlay: isMonoFont :: DisplayFont -> Bool
+ Game.LambdaHack.Client.UI.Overlay: isPrefixOfNonbreakable :: AttrString -> Bool
+ Game.LambdaHack.Client.UI.Overlay: isPropFont :: DisplayFont -> Bool
+ Game.LambdaHack.Client.UI.Overlay: isSquareFont :: DisplayFont -> Bool
+ Game.LambdaHack.Client.UI.Overlay: labDescOverlay :: DisplayFont -> Int -> AttrString -> (Overlay, Overlay)
+ Game.LambdaHack.Client.UI.Overlay: maxYofOverlay :: Overlay -> Int
+ Game.LambdaHack.Client.UI.Overlay: multiFontSetup :: FontSetup
+ Game.LambdaHack.Client.UI.Overlay: nonbreakableRev :: [String]
+ Game.LambdaHack.Client.UI.Overlay: rectangleOfSpaces :: Int -> Int -> Overlay
+ Game.LambdaHack.Client.UI.Overlay: singleFontSetup :: FontSetup
+ Game.LambdaHack.Client.UI.Overlay: textSize :: DisplayFont -> [a] -> Int
+ Game.LambdaHack.Client.UI.Overlay: typesetXY :: (Int, Int) -> [AttrLine] -> Overlay
+ Game.LambdaHack.Client.UI.Overlay: xtranslateOverlay :: Int -> Overlay -> Overlay
+ Game.LambdaHack.Client.UI.Overlay: xytranslateOverlay :: Int -> Int -> Overlay -> Overlay
+ Game.LambdaHack.Client.UI.Overlay: ytranslateOverlay :: Int -> Overlay -> Overlay
+ Game.LambdaHack.Client.UI.SessionUI: ReqDelayAlarm :: ReqDelay
+ Game.LambdaHack.Client.UI.SessionUI: ReqDelayHandled :: ReqDelay
+ Game.LambdaHack.Client.UI.SessionUI: ReqDelayNot :: ReqDelay
+ Game.LambdaHack.Client.UI.SessionUI: [sautoYes] :: SessionUI -> Bool
+ Game.LambdaHack.Client.UI.SessionUI: [smuteMessages] :: SessionUI -> Bool
+ Game.LambdaHack.Client.UI.SessionUI: [sregainControl] :: SessionUI -> Bool
+ Game.LambdaHack.Client.UI.SessionUI: [sreqDelay] :: SessionUI -> ReqDelay
+ Game.LambdaHack.Client.UI.SessionUI: [sreqPending] :: SessionUI -> Maybe RequestUI
+ Game.LambdaHack.Client.UI.SessionUI: [sreqQueried] :: SessionUI -> Bool
+ Game.LambdaHack.Client.UI.SessionUI: cycleMarkVision :: SessionUI -> SessionUI
+ Game.LambdaHack.Client.UI.SessionUI: data ReqDelay
+ Game.LambdaHack.Client.UI.SessionUI: instance GHC.Classes.Eq Game.LambdaHack.Client.UI.SessionUI.ReqDelay
+ Game.LambdaHack.Client.UI.Slideshow: emptyOKX :: OKX
+ Game.LambdaHack.Client.UI.Slideshow: labDescOKX :: DisplayFont -> DisplayFont -> [(AttrString, AttrString, KeyOrSlot)] -> OKX
+ Game.LambdaHack.Client.UI.Slideshow: maxYofFontOverlayMap :: FontOverlayMap -> Int
+ Game.LambdaHack.Client.UI.Slideshow: sideBySideOKX :: Int -> Int -> OKX -> OKX -> OKX
+ Game.LambdaHack.Client.UI.Slideshow: type KeyOrSlot = Either KM SlotChar
+ Game.LambdaHack.Client.UI.Slideshow: xtranslateKXY :: Int -> KYX -> KYX
+ Game.LambdaHack.Client.UI.Slideshow: xytranslateKXY :: Int -> Int -> KYX -> KYX
+ Game.LambdaHack.Client.UI.Slideshow: xytranslateOKX :: Int -> Int -> OKX -> OKX
+ Game.LambdaHack.Client.UI.Slideshow: yrenumberKXY :: Int -> KYX -> KYX
+ Game.LambdaHack.Client.UI.Slideshow: ytranslateKXY :: Int -> KYX -> KYX
+ Game.LambdaHack.Client.UI.SlideshowM: displayChoiceScreenWithRightPane :: forall m. MonadClientUI m => (KeyOrSlot -> m OKX) -> String -> ColorMode -> Bool -> Slideshow -> [KM] -> m KeyOrSlot
+ Game.LambdaHack.Client.UI.SlideshowM: drawHighlight :: Int -> ButtonWidth -> Int -> AttrString -> AttrString
+ Game.LambdaHack.Client.UI.SlideshowM: findKYX :: Int -> [OKX] -> Maybe (OKX, KYX, Int)
+ Game.LambdaHack.Client.UI.SlideshowM: getMenuIx :: MonadClientUI m => String -> Int -> Int -> Int -> m Int
+ Game.LambdaHack.Client.UI.SlideshowM: navigationKeys :: [KM]
+ Game.LambdaHack.Client.UI.SlideshowM: saveMenuIx :: MonadClientUI m => String -> Int -> Int -> m ()
+ Game.LambdaHack.Client.UI.SlideshowM: stepChoiceScreen :: forall m. MonadClientUI m => String -> ColorMode -> Bool -> Slideshow -> [KM] -> m (Int, Int, Int, Int -> OKX -> m (Bool, KeyOrSlot, Int))
+ Game.LambdaHack.Client.UI.UIOptions: [uMsgWrapColumn] :: UIOptions -> X
+ Game.LambdaHack.Client.UI.UIOptions: [uOverrideCmdline] :: UIOptions -> [String]
+ Game.LambdaHack.Client.UI.Watch: watchRespSfxAtomicUI :: MonadClientUI m => SfxAtomic -> m ()
+ Game.LambdaHack.Client.UI.Watch: watchRespUpdAtomicUI :: MonadClientUI m => UpdAtomic -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchCommonM: aidVerbDuplicateMU :: (MonadClientUI m, MsgShared a) => a -> ActorId -> Part -> m Bool
+ Game.LambdaHack.Client.UI.Watch.WatchCommonM: aidVerbMU :: (MonadClientUI m, MsgShared a) => a -> ActorId -> Part -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchCommonM: basicFrameWithoutReport :: MonadClientUI m => LevelId -> Maybe Bool -> m PreFrame3
+ Game.LambdaHack.Client.UI.Watch.WatchCommonM: fadeOutOrIn :: MonadClientUI m => Bool -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchCommonM: itemAidDistinctMU :: MonadClientUI m => MsgClassDistinct -> ActorId -> Part -> Part -> ItemId -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchCommonM: itemAidVerbMU :: (MonadClientUI m, MsgShared a) => a -> ActorId -> Part -> ItemId -> Either Int Int -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchCommonM: itemVerbMU :: (MonadClientUI m, MsgShared a) => a -> ItemId -> ItemQuant -> Part -> Container -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchCommonM: itemVerbMUGeneral :: MonadClientUI m => Bool -> ItemId -> ItemQuant -> Part -> Container -> m Text
+ Game.LambdaHack.Client.UI.Watch.WatchCommonM: itemVerbMUShort :: (MonadClientUI m, MsgShared a) => a -> ItemId -> ItemQuant -> Part -> Container -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchCommonM: lookAtMove :: MonadClientUI m => ActorId -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchCommonM: manyItemsAidVerbMU :: (MonadClientUI m, MsgShared a) => a -> ActorId -> Part -> [(ItemId, ItemQuant)] -> (Int -> Either (Maybe Int) Int) -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchCommonM: markDisplayNeeded :: MonadClientUI m => LevelId -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchCommonM: mitemAidVerbMU :: (MonadClientUI m, MsgShared a) => a -> ActorId -> Part -> ItemId -> Maybe Part -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchCommonM: pushFrame :: MonadClientUI m => Bool -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchCommonM: pushReportFrame :: MonadClientUI m => m ()
+ Game.LambdaHack.Client.UI.Watch.WatchCommonM: stopAtMove :: MonadClientUI m => ActorId -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchQuitM: displayGameOverAnalytics :: MonadClientUI m => FactionAnalytics -> GenerationAnalytics -> m KM
+ Game.LambdaHack.Client.UI.Watch.WatchQuitM: displayGameOverLoot :: MonadClientUI m => (ItemBag, Int) -> GenerationAnalytics -> m KM
+ Game.LambdaHack.Client.UI.Watch.WatchQuitM: displayGameOverLore :: MonadClientUI m => SLore -> Bool -> GenerationAnalytics -> m KM
+ Game.LambdaHack.Client.UI.Watch.WatchQuitM: quitFactionUI :: MonadClientUI m => FactionId -> Maybe Status -> Maybe (FactionAnalytics, GenerationAnalytics) -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchQuitM: viewLoreItems :: forall m. MonadClientUI m => String -> SingleItemSlots -> ItemBag -> Text -> (ItemId -> ItemFull -> Int -> Text) -> Bool -> m KM
+ Game.LambdaHack.Client.UI.Watch.WatchSfxAtomicM: ppSfxMsg :: MonadClientUI m => SfxMsg -> m (Maybe (Either (MsgClassShowAndSave, Text) (MsgClassDistinct, (Text, Text))))
+ Game.LambdaHack.Client.UI.Watch.WatchSfxAtomicM: returnJustLeft :: MonadClientUI m => (MsgClassShowAndSave, Text) -> m (Maybe (Either (MsgClassShowAndSave, Text) (MsgClassDistinct, (Text, Text))))
+ Game.LambdaHack.Client.UI.Watch.WatchSfxAtomicM: strike :: MonadClientUI m => Bool -> ActorId -> ActorId -> ItemId -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchSfxAtomicM: watchRespSfxAtomicUI :: MonadClientUI m => SfxAtomic -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM: createActorUI :: MonadClientUI m => Bool -> ActorId -> Actor -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM: data Threat
+ Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM: destroyActorUI :: MonadClientUI m => Bool -> ActorId -> Actor -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM: discover :: MonadClientUI m => Container -> ItemId -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM: displaceActorUI :: MonadClientUI m => ActorId -> ActorId -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM: instance GHC.Classes.Eq Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM.Threat
+ Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM: moveActor :: MonadClientUI m => ActorId -> Point -> Point -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM: moveItemUI :: MonadClientUI m => ItemId -> Int -> ActorId -> CStore -> CStore -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM: ppHearDistanceAdjective :: Maybe Int -> Text
+ Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM: ppHearDistanceAdverb :: Maybe Int -> Text
+ Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM: ppHearMsg :: MonadClientUI m => Maybe Int -> HearMsg -> m Text
+ Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM: recordItemLid :: MonadClientUI m => ItemId -> Container -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM: spotItemBag :: forall m. MonadClientUI m => Bool -> Container -> ItemBag -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM: updateItemSlot :: MonadClientUI m => Container -> ItemId -> m ()
+ Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM: watchRespUpdAtomicUI :: MonadClientUI m => UpdAtomic -> m ()
+ Game.LambdaHack.Common.ClientOptions: [sbenchMessages] :: ClientOptions -> Bool
+ Game.LambdaHack.Common.ClientOptions: [sfrontendANSI] :: ClientOptions -> Bool
+ Game.LambdaHack.Common.Item: instance GHC.Ix.Ix Game.LambdaHack.Common.Item.ItemKindIx
+ Game.LambdaHack.Common.Point: insideP :: (X, Y, X, Y) -> Point -> Bool
+ Game.LambdaHack.Common.Point: instance Test.QuickCheck.Arbitrary.Arbitrary Game.LambdaHack.Common.Point.Point
+ Game.LambdaHack.Common.PointArray: instance Game.LambdaHack.Common.PointArray.UnboxRepClass (Game.LambdaHack.Definition.DefsInternal.ContentId k)
+ Game.LambdaHack.Content.ItemKind: ItemSymbolsUsedInEngine :: ContentSymbol ItemKind -> ContentSymbol ItemKind -> ContentSymbol ItemKind -> ContentSymbol ItemKind -> ContentSymbol ItemKind -> ContentSymbol ItemKind -> ContentSymbol ItemKind -> ContentSymbol ItemKind -> ContentSymbol ItemKind -> ContentSymbol ItemKind -> ContentSymbol ItemKind -> ContentSymbol ItemKind -> ContentSymbol ItemKind -> ContentSymbol ItemKind -> ContentSymbol ItemKind -> ContentSymbol ItemKind -> ContentSymbol ItemKind -> ContentSymbol ItemKind -> ContentSymbol ItemKind -> ItemSymbolsUsedInEngine
+ Game.LambdaHack.Content.ItemKind: [rsymbolClothes] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolEdged] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolFlask] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolFood] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolGold] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolHafted] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolLight] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolMiscArmor] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolNecklace] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolPolearm] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolPotion] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolProjectile] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolRing] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolScroll] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolShield] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolSpecial] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolTool] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolTorsoArmor] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: [rsymbolWand] :: ItemSymbolsUsedInEngine -> ContentSymbol ItemKind
+ Game.LambdaHack.Content.ItemKind: data ItemSymbolsUsedInEngine
+ Game.LambdaHack.Content.ItemKind: emptyItemSymbolsUsedInEngine :: ItemSymbolsUsedInEngine
+ Game.LambdaHack.Content.ModeKind: [funderAI] :: Player -> Bool
+ Game.LambdaHack.Content.PlaceKind: [plegendDark] :: PlaceKind -> EnumMap Char (GroupName TileKind)
+ Game.LambdaHack.Content.PlaceKind: [plegendLit] :: PlaceKind -> EnumMap Char (GroupName TileKind)
+ Game.LambdaHack.Content.PlaceKind: override2PlaceKind :: [(Char, GroupName TileKind)] -> [(Char, GroupName TileKind)] -> PlaceKind -> PlaceKind
+ Game.LambdaHack.Content.PlaceKind: overridePlaceKind :: [(Char, GroupName TileKind)] -> PlaceKind -> PlaceKind
+ Game.LambdaHack.Content.RuleKind: [rHeightMax] :: RuleContent -> Y
+ Game.LambdaHack.Content.RuleKind: [rWidthMax] :: RuleContent -> X
+ Game.LambdaHack.Content.RuleKind: [ritemSymbols] :: RuleContent -> ItemSymbolsUsedInEngine
+ Game.LambdaHack.Core.Prelude: into :: forall target source. From source target => source -> target
+ Game.LambdaHack.Core.Random: invalidInformationCode :: Word16
+ Game.LambdaHack.Definition.Color: HighlightBackground :: Highlight
+ Game.LambdaHack.Definition.ContentData: oexistsGroup :: ContentData a -> GroupName a -> Bool
+ Game.LambdaHack.Definition.Defs: data GroupName c
+ Game.LambdaHack.Definition.Defs: displayContentSymbol :: ContentSymbol c -> Char
+ Game.LambdaHack.Definition.Defs: displayGroupName :: GroupName c -> Text
+ Game.LambdaHack.Definition.Defs: renameFreqs :: (Text -> Text) -> Freqs c -> Freqs c
+ Game.LambdaHack.Definition.Defs: type ContentSymbol c = Char
+ Game.LambdaHack.Definition.DefsInternal: GroupName :: Text -> GroupName c
+ Game.LambdaHack.Definition.DefsInternal: [fromGroupName] :: GroupName c -> Text
+ Game.LambdaHack.Definition.DefsInternal: contentIdIndex :: ContentId c -> Int
+ Game.LambdaHack.Definition.DefsInternal: data ContentId c
+ Game.LambdaHack.Definition.DefsInternal: displayContentSymbol :: ContentSymbol c -> Char
+ Game.LambdaHack.Definition.DefsInternal: displayGroupName :: GroupName c -> Text
+ Game.LambdaHack.Definition.DefsInternal: fromContentId :: ContentId c -> Word16
+ Game.LambdaHack.Definition.DefsInternal: instance Control.DeepSeq.NFData (Game.LambdaHack.Definition.DefsInternal.GroupName c)
+ Game.LambdaHack.Definition.DefsInternal: instance Data.Binary.Class.Binary (Game.LambdaHack.Definition.DefsInternal.ContentId c)
+ Game.LambdaHack.Definition.DefsInternal: instance Data.Binary.Class.Binary (Game.LambdaHack.Definition.DefsInternal.GroupName c)
+ Game.LambdaHack.Definition.DefsInternal: instance Data.Hashable.Class.Hashable (Game.LambdaHack.Definition.DefsInternal.ContentId c)
+ Game.LambdaHack.Definition.DefsInternal: instance Data.Hashable.Class.Hashable (Game.LambdaHack.Definition.DefsInternal.GroupName c)
+ Game.LambdaHack.Definition.DefsInternal: instance GHC.Classes.Eq (Game.LambdaHack.Definition.DefsInternal.ContentId c)
+ Game.LambdaHack.Definition.DefsInternal: instance GHC.Classes.Eq (Game.LambdaHack.Definition.DefsInternal.GroupName c)
+ Game.LambdaHack.Definition.DefsInternal: instance GHC.Classes.Ord (Game.LambdaHack.Definition.DefsInternal.ContentId c)
+ Game.LambdaHack.Definition.DefsInternal: instance GHC.Classes.Ord (Game.LambdaHack.Definition.DefsInternal.GroupName c)
+ Game.LambdaHack.Definition.DefsInternal: instance GHC.Enum.Enum (Game.LambdaHack.Definition.DefsInternal.ContentId c)
+ Game.LambdaHack.Definition.DefsInternal: instance GHC.Show.Show (Game.LambdaHack.Definition.DefsInternal.ContentId c)
+ Game.LambdaHack.Definition.DefsInternal: instance GHC.Show.Show (Game.LambdaHack.Definition.DefsInternal.GroupName c)
+ Game.LambdaHack.Definition.DefsInternal: newtype GroupName c
+ Game.LambdaHack.Definition.DefsInternal: toContentId :: Word16 -> ContentId c
+ Game.LambdaHack.Definition.DefsInternal: toContentSymbol :: Char -> ContentSymbol c
+ Game.LambdaHack.Definition.DefsInternal: type ContentSymbol c = Char
+ Game.LambdaHack.Definition.Flavour: dummyFlavour :: Flavour
+ Game.LambdaHack.Definition.Flavour: stdFlavList :: [Flavour]
+ Game.LambdaHack.Server.CommonM: revealAll :: MonadServerAtomic m => FactionId -> m ()
+ Game.LambdaHack.Server.CommonM: revealPerceptionLid :: MonadServerAtomic m => FactionId -> (LevelId, Level) -> m ()
+ Game.LambdaHack.Server.CommonM: updatePerFromNew :: MonadServerAtomic m => FactionId -> LevelId -> Perception -> m ()
+ Game.LambdaHack.Server.ItemRev: keepMetaGameInformation :: ContentData ItemKind -> Vector Word16 -> Vector Word16
+ Game.LambdaHack.Server.ItemRev: rollFlavourMap :: Vector Word16 -> Rnd (EnumMap (ContentId ItemKind) Flavour, EnumMap (ContentSymbol ItemKind) (EnumSet Flavour)) -> ContentId ItemKind -> ItemKind -> Rnd (EnumMap (ContentId ItemKind) Flavour, EnumMap (ContentSymbol ItemKind) (EnumSet Flavour))
+ Game.LambdaHack.Server.LoopM: handleUIunderAI :: (MonadServerAtomic m, MonadServerComm m) => FactionId -> ActorId -> m ()
+ Game.LambdaHack.Server.PeriodicM: addManyActors :: MonadServerAtomic m => Bool -> Int -> Freqs ItemKind -> LevelId -> Time -> Maybe Point -> Int -> m Bool
+ Game.LambdaHack.Server.State: [sbandSpawned] :: StateServer -> IntMap Int
+ Paths_LambdaHack: getBinDir :: IO FilePath
+ Paths_LambdaHack: getDataDir :: IO FilePath
+ Paths_LambdaHack: getDataFileName :: FilePath -> IO FilePath
+ Paths_LambdaHack: getDynLibDir :: IO FilePath
+ Paths_LambdaHack: getLibDir :: IO FilePath
+ Paths_LambdaHack: getLibexecDir :: IO FilePath
+ Paths_LambdaHack: getSysconfDir :: IO FilePath
+ Paths_LambdaHack: version :: Version
- Game.LambdaHack.Client.AI.ConditionM: benGroundItems :: MonadClient m => ActorId -> m [(Benefit, CStore, ItemId, ItemFull, ItemQuant)]
+ Game.LambdaHack.Client.AI.ConditionM: benGroundItems :: MonadClientRead m => ActorId -> m [(Benefit, CStore, ItemId, ItemFull, ItemQuant)]
- Game.LambdaHack.Client.AI.ConditionM: condAdjTriggerableM :: MonadClient m => Skills -> ActorId -> m Bool
+ Game.LambdaHack.Client.AI.ConditionM: condAdjTriggerableM :: MonadStateRead m => Skills -> ActorId -> m Bool
- Game.LambdaHack.Client.AI.ConditionM: condAimCrucialM :: MonadClient m => ActorId -> m Bool
+ Game.LambdaHack.Client.AI.ConditionM: condAimCrucialM :: MonadClientRead m => ActorId -> m Bool
- Game.LambdaHack.Client.AI.ConditionM: condAimEnemyOrRememberedM :: MonadClient m => ActorId -> m Bool
+ Game.LambdaHack.Client.AI.ConditionM: condAimEnemyOrRememberedM :: MonadClientRead m => ActorId -> m Bool
- Game.LambdaHack.Client.AI.ConditionM: condAimEnemyOrStashM :: MonadClient m => ActorId -> m Bool
+ Game.LambdaHack.Client.AI.ConditionM: condAimEnemyOrStashM :: MonadClientRead m => ActorId -> m Bool
- Game.LambdaHack.Client.AI.ConditionM: condAimEnemyTargetedM :: MonadClient m => ActorId -> m Bool
+ Game.LambdaHack.Client.AI.ConditionM: condAimEnemyTargetedM :: MonadClientRead m => ActorId -> m Bool
- Game.LambdaHack.Client.AI.ConditionM: condAimNonEnemyPresentM :: MonadClient m => ActorId -> m Bool
+ Game.LambdaHack.Client.AI.ConditionM: condAimNonEnemyPresentM :: MonadClientRead m => ActorId -> m Bool
- Game.LambdaHack.Client.AI.ConditionM: condAloneM :: MonadClient m => [(ActorId, Actor)] -> ActorId -> m Bool
+ Game.LambdaHack.Client.AI.ConditionM: condAloneM :: MonadStateRead m => [(ActorId, Actor)] -> ActorId -> m Bool
- Game.LambdaHack.Client.AI.ConditionM: condBlocksFriendsM :: MonadClient m => ActorId -> m Bool
+ Game.LambdaHack.Client.AI.ConditionM: condBlocksFriendsM :: MonadClientRead m => ActorId -> m Bool
- Game.LambdaHack.Client.AI.ConditionM: condCanProjectM :: MonadClient m => Int -> ActorId -> m Bool
+ Game.LambdaHack.Client.AI.ConditionM: condCanProjectM :: MonadClientRead m => Int -> ActorId -> m Bool
- Game.LambdaHack.Client.AI.ConditionM: condDesirableFloorItemM :: MonadClient m => ActorId -> m Bool
+ Game.LambdaHack.Client.AI.ConditionM: condDesirableFloorItemM :: MonadClientRead m => ActorId -> m Bool
- Game.LambdaHack.Client.AI.ConditionM: condProjectListM :: MonadClient m => Int -> ActorId -> m [(Double, CStore, ItemId, ItemFull, ItemQuant)]
+ Game.LambdaHack.Client.AI.ConditionM: condProjectListM :: MonadClientRead m => Int -> ActorId -> m [(Double, CStore, ItemId, ItemFull, ItemQuant)]
- Game.LambdaHack.Client.AI.ConditionM: condSupport :: MonadClient m => [(ActorId, Actor)] -> Int -> ActorId -> m Bool
+ Game.LambdaHack.Client.AI.ConditionM: condSupport :: MonadClientRead m => [(ActorId, Actor)] -> Int -> ActorId -> m Bool
- Game.LambdaHack.Client.AI.ConditionM: condTgtNonmovingEnemyM :: MonadClient m => ActorId -> m Bool
+ Game.LambdaHack.Client.AI.ConditionM: condTgtNonmovingEnemyM :: MonadClientRead m => ActorId -> m Bool
- Game.LambdaHack.Client.AI.ConditionM: fleeList :: MonadClient m => [(ActorId, Actor)] -> ActorId -> m ([(Int, Point)], [(Int, Point)])
+ Game.LambdaHack.Client.AI.ConditionM: fleeList :: MonadClientRead m => [(ActorId, Actor)] -> ActorId -> m ([(Int, Point)], [(Int, Point)])
- Game.LambdaHack.Client.AI.PickActionM: actionStrategy :: forall m. MonadClient m => [(ActorId, Actor)] -> [(ActorId, Actor)] -> ActorId -> Bool -> m (Strategy RequestTimed)
+ Game.LambdaHack.Client.AI.PickActionM: actionStrategy :: MonadClient m => [(ActorId, Actor)] -> [(ActorId, Actor)] -> LevelId -> ActorId -> Bool -> m (Strategy RequestTimed)
- Game.LambdaHack.Client.AI.PickActionM: applyItem :: MonadClient m => Skills -> ActorId -> ApplyItemGroup -> m (Strategy RequestTimed)
+ Game.LambdaHack.Client.AI.PickActionM: applyItem :: MonadClientRead m => Skills -> ActorId -> ApplyItemGroup -> m (Strategy RequestTimed)
- Game.LambdaHack.Client.AI.PickActionM: chase :: MonadClient m => Skills -> ActorId -> Bool -> Bool -> m (Strategy RequestTimed)
+ Game.LambdaHack.Client.AI.PickActionM: chase :: MonadClientRead m => Skills -> ActorId -> Bool -> Bool -> m (Strategy RequestTimed)
- Game.LambdaHack.Client.AI.PickActionM: displaceBlocker :: MonadClient m => ActorId -> Bool -> m (Strategy RequestTimed)
+ Game.LambdaHack.Client.AI.PickActionM: displaceBlocker :: MonadClientRead m => ActorId -> Bool -> m (Strategy RequestTimed)
- Game.LambdaHack.Client.AI.PickActionM: displaceFoe :: MonadClient m => ActorId -> m (Strategy RequestTimed)
+ Game.LambdaHack.Client.AI.PickActionM: displaceFoe :: MonadClientRead m => ActorId -> m (Strategy RequestTimed)
- Game.LambdaHack.Client.AI.PickActionM: displaceTgt :: MonadClient m => ActorId -> Point -> Bool -> m (Strategy RequestTimed)
+ Game.LambdaHack.Client.AI.PickActionM: displaceTgt :: MonadClientRead m => ActorId -> Point -> Bool -> m (Strategy RequestTimed)
- Game.LambdaHack.Client.AI.PickActionM: equipItems :: MonadClient m => ActorId -> m (Strategy RequestTimed)
+ Game.LambdaHack.Client.AI.PickActionM: equipItems :: MonadClientRead m => ActorId -> m (Strategy RequestTimed)
- Game.LambdaHack.Client.AI.PickActionM: moveOrRunAid :: MonadClient m => Skills -> ActorId -> Vector -> m (Maybe RequestTimed)
+ Game.LambdaHack.Client.AI.PickActionM: moveOrRunAid :: MonadClientRead m => Skills -> ActorId -> Vector -> m (Maybe RequestTimed)
- Game.LambdaHack.Client.AI.PickActionM: moveTowards :: MonadClient m => Skills -> ActorId -> Bool -> Point -> Point -> Bool -> m (Strategy Vector)
+ Game.LambdaHack.Client.AI.PickActionM: moveTowards :: MonadClientRead m => Skills -> ActorId -> Bool -> Point -> Point -> Bool -> m (Strategy Vector)
- Game.LambdaHack.Client.AI.PickActionM: pickup :: MonadClient m => ActorId -> Bool -> m (Strategy RequestTimed)
+ Game.LambdaHack.Client.AI.PickActionM: pickup :: MonadClientRead m => ActorId -> Bool -> m (Strategy RequestTimed)
- Game.LambdaHack.Client.AI.PickActionM: projectItem :: MonadClient m => Skills -> ActorId -> m (Strategy RequestTimed)
+ Game.LambdaHack.Client.AI.PickActionM: projectItem :: MonadClientRead m => Skills -> ActorId -> m (Strategy RequestTimed)
- Game.LambdaHack.Client.AI.PickActionM: trigger :: MonadClient m => ActorId -> FleeViaStairsOrEscape -> m (Strategy RequestTimed)
+ Game.LambdaHack.Client.AI.PickActionM: trigger :: MonadClientRead m => ActorId -> FleeViaStairsOrEscape -> m (Strategy RequestTimed)
- Game.LambdaHack.Client.AI.PickActionM: unEquipItems :: MonadClient m => ActorId -> m (Strategy RequestTimed)
+ Game.LambdaHack.Client.AI.PickActionM: unEquipItems :: MonadClientRead m => ActorId -> m (Strategy RequestTimed)
- Game.LambdaHack.Client.AI.PickActionM: waitBlockNow :: MonadClient m => m (Strategy RequestTimed)
+ Game.LambdaHack.Client.AI.PickActionM: waitBlockNow :: MonadClientRead m => m (Strategy RequestTimed)
- Game.LambdaHack.Client.AI.PickActionM: yellNow :: MonadClient m => m (Strategy RequestTimed)
+ Game.LambdaHack.Client.AI.PickActionM: yellNow :: MonadClientRead m => m (Strategy RequestTimed)
- Game.LambdaHack.Client.AI.PickActionM: yieldUnneeded :: MonadClient m => ActorId -> m (Strategy RequestTimed)
+ Game.LambdaHack.Client.AI.PickActionM: yieldUnneeded :: MonadClientRead m => ActorId -> m (Strategy RequestTimed)
- Game.LambdaHack.Client.CommonM: aidTgtToPos :: ActorId -> LevelId -> Maybe Target -> State -> Maybe Point
+ Game.LambdaHack.Client.CommonM: aidTgtToPos :: Maybe ActorId -> LevelId -> Maybe Target -> State -> Maybe Point
- Game.LambdaHack.Client.HandleAtomicM: wipeBfsIfItemAffectsSkills :: MonadClient m => [CStore] -> ActorId -> m ()
+ Game.LambdaHack.Client.HandleAtomicM: wipeBfsIfItemAffectsSkills :: MonadClient m => CStore -> ActorId -> m ()
- Game.LambdaHack.Client.MonadClient: condInMeleeM :: MonadClient m => LevelId -> m Bool
+ Game.LambdaHack.Client.MonadClient: condInMeleeM :: MonadClientRead m => LevelId -> m Bool
- Game.LambdaHack.Client.State: StateClient :: Int -> EnumMap ActorId TgtAndPath -> EnumMap ActorId (Point, Time) -> EnumSet LevelId -> EnumMap ActorId BfsAndPath -> () -> DiscoveryBenefit -> PerLid -> AlterLid -> SMGen -> Maybe ActorId -> FactionId -> Bool -> Challenge -> Challenge -> Int -> EnumMap LevelId Bool -> EnumMap (ContentId ModeKind) (Map Challenge Int) -> EnumSet (ContentId ModeKind) -> EnumSet (ContentId ModeKind) -> ClientOptions -> (PrimArray PointI, PrimArray PointI) -> StateClient
+ Game.LambdaHack.Client.State: StateClient :: Int -> EnumMap ActorId TgtAndPath -> EnumMap ActorId (Point, Time) -> EnumSet LevelId -> EnumMap ActorId BfsAndPath -> () -> DiscoveryBenefit -> PerLid -> AlterLid -> SMGen -> Maybe ActorId -> FactionId -> Bool -> Challenge -> Challenge -> Int -> EnumSet LevelId -> EnumMap (ContentId ModeKind) (Map Challenge Int) -> EnumSet (ContentId ModeKind) -> EnumSet (ContentId ModeKind) -> ClientOptions -> (PrimArray PointI, PrimArray PointI) -> StateClient
- Game.LambdaHack.Client.State: [scondInMelee] :: StateClient -> EnumMap LevelId Bool
+ Game.LambdaHack.Client.State: [scondInMelee] :: StateClient -> EnumSet LevelId
- Game.LambdaHack.Client.UI: SessionUI :: Maybe Target -> Maybe Target -> ActorDictUI -> ItemDictUI -> ItemSlots -> Maybe (CStore, CStore) -> ChanFrontend -> CCUI -> UIOptions -> Maybe AimMode -> Bool -> Maybe (ItemId, CStore, Bool) -> EnumSet ActorId -> Maybe RunParams -> History -> PointUI -> KeyMacroFrame -> [KeyMacroFrame] -> EnumSet ActorId -> Int -> Bool -> Bool -> Bool -> Int -> Bool -> Bool -> Maybe Bool -> Set Msg -> Map String Int -> ChosenLore -> Bool -> Bool -> Bool -> POSIXTime -> POSIXTime -> Time -> Int -> Int -> SMGen -> SessionUI
+ Game.LambdaHack.Client.UI: SessionUI :: Maybe RequestUI -> ReqDelay -> Bool -> Bool -> Maybe Target -> Maybe Target -> ActorDictUI -> ItemDictUI -> ItemSlots -> Maybe (CStore, CStore) -> ChanFrontend -> CCUI -> UIOptions -> Maybe AimMode -> Bool -> Maybe (ItemId, CStore, Bool) -> EnumSet ActorId -> Maybe RunParams -> History -> PointUI -> Bool -> KeyMacroFrame -> [KeyMacroFrame] -> EnumSet ActorId -> Int -> Bool -> Int -> Bool -> Int -> Bool -> Bool -> Maybe Bool -> Set Msg -> Bool -> Map String Int -> ChosenLore -> Bool -> Bool -> Bool -> POSIXTime -> POSIXTime -> Time -> Int -> Int -> SMGen -> SessionUI
- Game.LambdaHack.Client.UI: [smarkVision] :: SessionUI -> Bool
+ Game.LambdaHack.Client.UI: [smarkVision] :: SessionUI -> Int
- Game.LambdaHack.Client.UI: queryUI :: (MonadClient m, MonadClientUI m) => m RequestUI
+ Game.LambdaHack.Client.UI: queryUI :: (MonadClient m, MonadClientUI m) => m (Maybe RequestUI)
- Game.LambdaHack.Client.UI.Content.Screen: ScreenContent :: X -> Y -> X -> String -> ([String], [[String]]) -> EnumMap Char Text -> [(FilePath, ByteString)] -> ScreenContent
+ Game.LambdaHack.Client.UI.Content.Screen: ScreenContent :: X -> Y -> String -> ([String], [[String]]) -> EnumMap (ContentSymbol ItemKind) Text -> [(FilePath, ByteString)] -> ScreenContent
- Game.LambdaHack.Client.UI.Content.Screen: [rapplyVerbMap] :: ScreenContent -> EnumMap Char Text
+ Game.LambdaHack.Client.UI.Content.Screen: [rapplyVerbMap] :: ScreenContent -> EnumMap (ContentSymbol ItemKind) Text
- Game.LambdaHack.Client.UI.Frame: SingleFrame :: Array AttrCharW32 -> OverlaySpace -> OverlaySpace -> SingleFrame
+ Game.LambdaHack.Client.UI.Frame: SingleFrame :: Array AttrCharW32 -> OverlaySpace -> OverlaySpace -> OverlaySpace -> SingleFrame
- Game.LambdaHack.Client.UI.Frame: type Frame = ((FrameBase, FrameForall), (OverlaySpace, OverlaySpace))
+ Game.LambdaHack.Client.UI.Frame: type Frame = ((FrameBase, FrameForall), (OverlaySpace, OverlaySpace, OverlaySpace))
- Game.LambdaHack.Client.UI.Frame: type PreFrame3 = (PreFrame, (OverlaySpace, OverlaySpace))
+ Game.LambdaHack.Client.UI.Frame: type PreFrame3 = (PreFrame, (OverlaySpace, OverlaySpace, OverlaySpace))
- Game.LambdaHack.Client.UI.FrameM: promptGetKey :: (MonadClient m, MonadClientUI m) => ColorMode -> FontOverlayMap -> Bool -> [KM] -> m KM
+ Game.LambdaHack.Client.UI.FrameM: promptGetKey :: MonadClientUI m => ColorMode -> FontOverlayMap -> Bool -> [KM] -> m KM
- Game.LambdaHack.Client.UI.FrameM: renderAnimFrames :: MonadClientUI m => Bool -> LevelId -> Animation -> m PreFrames3
+ Game.LambdaHack.Client.UI.FrameM: renderAnimFrames :: MonadClientUI m => LevelId -> Animation -> Maybe Bool -> m PreFrames3
- Game.LambdaHack.Client.UI.FrameM: stopPlayBack :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.FrameM: stopPlayBack :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHelperM: displayItemLore :: (MonadClient m, MonadClientUI m) => ItemBag -> Int -> (ItemId -> ItemFull -> Int -> Text) -> Int -> SingleItemSlots -> m Bool
+ Game.LambdaHack.Client.UI.HandleHelperM: displayItemLore :: MonadClientUI m => ItemBag -> Int -> (ItemId -> ItemFull -> Int -> Text) -> Int -> SingleItemSlots -> Bool -> m KM
- Game.LambdaHack.Client.UI.HandleHelperM: lookAtItems :: MonadClientUI m => Bool -> Point -> ActorId -> Maybe (Part, Bool) -> m (Text, Maybe Person)
+ Game.LambdaHack.Client.UI.HandleHelperM: lookAtItems :: MonadClientUI m => Bool -> Point -> LevelId -> Maybe ActorId -> Maybe (Part, Bool) -> m (Text, Maybe Person)
- Game.LambdaHack.Client.UI.HandleHelperM: lookAtPosition :: MonadClientUI m => LevelId -> Point -> m [(MsgClassShow, Text)]
+ Game.LambdaHack.Client.UI.HandleHelperM: lookAtPosition :: MonadClientUI m => Point -> LevelId -> m [(MsgClassShow, Text)]
- Game.LambdaHack.Client.UI.HandleHelperM: lookAtStash :: MonadClientUI m => LevelId -> Point -> m Text
+ Game.LambdaHack.Client.UI.HandleHelperM: lookAtStash :: MonadClientUI m => Point -> LevelId -> m Text
- Game.LambdaHack.Client.UI.HandleHelperM: lookAtTile :: MonadClientUI m => Bool -> Point -> ActorId -> LevelId -> Maybe Person -> m (Text, Text, [(Int, Part)])
+ Game.LambdaHack.Client.UI.HandleHelperM: lookAtTile :: MonadClientUI m => Bool -> Point -> LevelId -> Maybe ActorId -> Maybe Person -> m (Text, Text, [(Int, Part)])
- Game.LambdaHack.Client.UI.HandleHelperM: pickLeader :: (MonadClient m, MonadClientUI m) => Bool -> ActorId -> m Bool
+ Game.LambdaHack.Client.UI.HandleHelperM: pickLeader :: MonadClientUI m => Bool -> ActorId -> m Bool
- Game.LambdaHack.Client.UI.HandleHelperM: pickLeaderWithPointer :: (MonadClient m, MonadClientUI m) => m MError
+ Game.LambdaHack.Client.UI.HandleHelperM: pickLeaderWithPointer :: MonadClientUI m => ActorId -> m MError
- Game.LambdaHack.Client.UI.HandleHelperM: pickNumber :: (MonadClient m, MonadClientUI m) => Bool -> Int -> m (Either MError Int)
+ Game.LambdaHack.Client.UI.HandleHelperM: pickNumber :: MonadClientUI m => Bool -> Int -> m (Either MError Int)
- Game.LambdaHack.Client.UI.HandleHelperM: placesFromState :: ContentData PlaceKind -> ClientOptions -> State -> EnumMap (ContentId PlaceKind) (EnumSet LevelId, Int, Int, Int)
+ Game.LambdaHack.Client.UI.HandleHelperM: placesFromState :: ContentData PlaceKind -> Bool -> State -> EnumMap (ContentId PlaceKind) (EnumSet LevelId, Int, Int, Int)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: alterCommon :: (MonadClient m, MonadClientUI m) => Bool -> Point -> m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: alterCommon :: MonadClientUI m => ActorId -> Bool -> Point -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: alterDirHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: alterDirHuman :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: alterTileAtPos :: (MonadClient m, MonadClientUI m) => Point -> m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: alterTileAtPos :: MonadClientUI m => ActorId -> Point -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: alterWithPointerHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: alterWithPointerHuman :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: applyHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: applyHuman :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: applyItem :: (MonadClient m, MonadClientUI m) => (CStore, (ItemId, ItemFullKit)) -> m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: applyItem :: MonadClientUI m => ActorId -> (CStore, (ItemId, ItemFullKit)) -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: automateHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: automateHuman :: MonadClientUI m => m (FailOrCmd ReqUI)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: automateToggleHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: automateToggleHuman :: MonadClientUI m => m (FailOrCmd ReqUI)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: byAreaHuman :: (MonadClient m, MonadClientUI m) => (KM -> HumanCmd -> m (Either MError ReqUI)) -> [(CmdArea, HumanCmd)] -> m (Either MError ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: byAreaHuman :: MonadClientUI m => (KM -> HumanCmd -> m (Either MError ReqUI)) -> [(CmdArea, HumanCmd)] -> m (Either MError ReqUI)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: challengeMenuHuman :: (MonadClient m, MonadClientUI m) => (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: challengeMenuHuman :: MonadClientUI m => (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: chooseItemMenuHuman :: (MonadClient m, MonadClientUI m) => (KM -> HumanCmd -> m (Either MError ReqUI)) -> ItemDialogMode -> m (Either MError ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: chooseItemMenuHuman :: MonadClientUI m => ActorId -> (KM -> HumanCmd -> m (Either MError ReqUI)) -> ItemDialogMode -> m (Either MError ReqUI)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: closeDirHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: closeDirHuman :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: closeTileAtPos :: (MonadClient m, MonadClientUI m) => Point -> m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: closeTileAtPos :: MonadClientUI m => ActorId -> Point -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: continueToXhairHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: continueToXhairHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: dashboardHuman :: (MonadClient m, MonadClientUI m) => (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: dashboardHuman :: MonadClientUI m => (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: displaceAid :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: displaceAid :: MonadClientUI m => ActorId -> ActorId -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: doctrineHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: doctrineHuman :: MonadClientUI m => m (FailOrCmd ReqUI)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: gameDropHuman :: (MonadClient m, MonadClientUI m) => m ReqUI
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: gameDropHuman :: MonadClientUI m => m ReqUI
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: gameQuitHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: gameQuitHuman :: MonadClientUI m => m (FailOrCmd ReqUI)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: gameRestartHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: gameRestartHuman :: MonadClientUI m => m (FailOrCmd ReqUI)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: gameSaveHuman :: (MonadClient m, MonadClientUI m) => m ReqUI
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: gameSaveHuman :: MonadClientUI m => m ReqUI
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: gameScenarioIncr :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: gameScenarioIncr :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: generateMenu :: (MonadClient m, MonadClientUI m) => (KM -> HumanCmd -> m (Either MError ReqUI)) -> [(DisplayFont, [AttrLine])] -> [(KM, (Text, HumanCmd))] -> [String] -> String -> m (Either MError ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: generateMenu :: MonadClientUI m => (KM -> HumanCmd -> m (Either MError ReqUI)) -> FontOverlayMap -> [(String, (Text, HumanCmd, Maybe FontOverlayMap))] -> [String] -> String -> m (Either MError ReqUI)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: goToXhair :: (MonadClient m, MonadClientUI m) => Bool -> Bool -> m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: goToXhair :: (MonadClient m, MonadClientUI m) => ActorId -> Bool -> Bool -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: goToXhairExplorationMode :: (MonadClient m, MonadClientUI m) => Bool -> Bool -> m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: goToXhairExplorationMode :: (MonadClient m, MonadClientUI m) => ActorId -> Bool -> Bool -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: goToXhairGoTo :: (MonadClient m, MonadClientUI m) => Bool -> Bool -> m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: goToXhairGoTo :: (MonadClient m, MonadClientUI m) => ActorId -> Bool -> Bool -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: helpHuman :: (MonadClient m, MonadClientUI m) => (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: helpHuman :: MonadClientUI m => (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: hintHuman :: (MonadClient m, MonadClientUI m) => (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: hintHuman :: MonadClientUI m => (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: itemMenuHuman :: (MonadClient m, MonadClientUI m) => (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: itemMenuHuman :: MonadClientUI m => ActorId -> (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: mainMenuAutoOffHuman :: (MonadClient m, MonadClientUI m) => (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: mainMenuAutoOffHuman :: MonadClientUI m => (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: mainMenuAutoOnHuman :: (MonadClient m, MonadClientUI m) => (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: mainMenuAutoOnHuman :: MonadClientUI m => (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: mainMenuHuman :: (MonadClient m, MonadClientUI m) => (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: mainMenuHuman :: MonadClientUI m => (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: meleeAid :: (MonadClient m, MonadClientUI m) => ActorId -> m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: meleeAid :: (MonadClient m, MonadClientUI m) => ActorId -> ActorId -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: moveItemHuman :: forall m. (MonadClient m, MonadClientUI m) => [CStore] -> CStore -> Maybe Text -> Bool -> m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: moveItemHuman :: forall m. MonadClientUI m => ActorId -> [CStore] -> CStore -> Maybe Text -> Bool -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: moveItems :: forall m. (MonadClient m, MonadClientUI m) => [CStore] -> (CStore, [(ItemId, ItemQuant)]) -> CStore -> m RequestTimed
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: moveItems :: forall m. MonadClientUI m => ActorId -> [CStore] -> (CStore, [(ItemId, ItemQuant)]) -> CStore -> m RequestTimed
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: moveOnceToXhairHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: moveOnceToXhairHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: moveOrSelectItem :: forall m. (MonadClient m, MonadClientUI m) => [CStore] -> CStore -> Maybe Text -> Bool -> m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: moveOrSelectItem :: forall m. MonadClientUI m => ActorId -> [CStore] -> CStore -> Maybe Text -> Bool -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: moveRunHuman :: (MonadClient m, MonadClientUI m) => Bool -> Bool -> Bool -> Bool -> Vector -> m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: moveRunHuman :: (MonadClient m, MonadClientUI m) => ActorId -> Bool -> Bool -> Bool -> Bool -> Vector -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: moveSearchAlter :: (MonadClient m, MonadClientUI m) => Bool -> Vector -> m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: moveSearchAlter :: MonadClientUI m => ActorId -> Bool -> Vector -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: msgAddDone :: (MonadClient m, MonadClientUI m) => Point -> Text -> m ()
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: msgAddDone :: MonadClientUI m => ActorId -> Point -> Text -> m ()
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: pickPoint :: (MonadClient m, MonadClientUI m) => Text -> m (Maybe Point)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: pickPoint :: MonadClientUI m => ActorId -> Text -> m (Maybe Point)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: processTileActions :: forall m. (MonadClient m, MonadClientUI m) => Bool -> ActorId -> Point -> [TileAction] -> m (FailOrCmd ())
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: processTileActions :: forall m. MonadClientUI m => ActorId -> Bool -> Point -> [TileAction] -> m (FailOrCmd ())
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: projectHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: projectHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: projectItem :: (MonadClient m, MonadClientUI m) => (CStore, (ItemId, ItemFull)) -> m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: projectItem :: (MonadClient m, MonadClientUI m) => ActorId -> (CStore, (ItemId, ItemFull)) -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: runOnceAheadHuman :: (MonadClient m, MonadClientUI m) => m (Either MError RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: runOnceAheadHuman :: MonadClientUI m => ActorId -> m (Either MError RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: runOnceToXhairHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: runOnceToXhairHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: selectItemsToMove :: forall m. (MonadClient m, MonadClientUI m) => [CStore] -> CStore -> Maybe Text -> Bool -> m (FailOrCmd (CStore, [(ItemId, ItemQuant)]))
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: selectItemsToMove :: forall m. MonadClientUI m => ActorId -> [CStore] -> CStore -> Maybe Text -> Bool -> m (FailOrCmd (CStore, [(ItemId, ItemQuant)]))
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: settingsMenuHuman :: (MonadClient m, MonadClientUI m) => (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: settingsMenuHuman :: MonadClientUI m => (KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: verifyAlters :: forall m. (MonadClient m, MonadClientUI m) => Bool -> ActorId -> Point -> m (FailOrCmd ())
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: verifyAlters :: forall m. MonadClientUI m => ActorId -> Bool -> Point -> m (FailOrCmd ())
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: verifyEscape :: (MonadClient m, MonadClientUI m) => m (FailOrCmd ())
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: verifyEscape :: MonadClientUI m => m (FailOrCmd ())
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: verifyToolEffect :: (MonadClient m, MonadClientUI m) => LevelId -> CStore -> ItemFull -> m (FailOrCmd ())
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: verifyToolEffect :: MonadClientUI m => LevelId -> CStore -> ItemFull -> m (FailOrCmd ())
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: waitHuman :: MonadClientUI m => m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: waitHuman :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: waitHuman10 :: MonadClientUI m => m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: waitHuman10 :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanGlobalM: yellHuman :: MonadClientUI m => m (FailOrCmd RequestTimed)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalM: yellHuman :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed)
- Game.LambdaHack.Client.UI.HandleHumanLocalM: acceptHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: acceptHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: aimAscendHuman :: (MonadClient m, MonadClientUI m) => Int -> m MError
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: aimAscendHuman :: MonadClientUI m => Int -> m MError
- Game.LambdaHack.Client.UI.HandleHumanLocalM: aimEnemyHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: aimEnemyHuman :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: aimFloorHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: aimFloorHuman :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: aimItemHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: aimItemHuman :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: aimPointerEnemyHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: aimPointerEnemyHuman :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: aimPointerFloorHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: aimPointerFloorHuman :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: aimTgtHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: aimTgtHuman :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: allHistoryHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: allHistoryHuman :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: chooseItemApplyHuman :: forall m. (MonadClient m, MonadClientUI m) => [TriggerItem] -> m MError
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: chooseItemApplyHuman :: forall m. MonadClientUI m => ActorId -> [TriggerItem] -> m MError
- Game.LambdaHack.Client.UI.HandleHumanLocalM: chooseItemDialogMode :: (MonadClient m, MonadClientUI m) => Bool -> ItemDialogMode -> m (FailOrCmd ItemDialogMode)
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: chooseItemDialogMode :: MonadClientUI m => ActorId -> Bool -> ItemDialogMode -> m (FailOrCmd ActorId)
- Game.LambdaHack.Client.UI.HandleHumanLocalM: chooseItemDialogModeLore :: (MonadClient m, MonadClientUI m) => m (Maybe ResultItemDialogMode)
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: chooseItemDialogModeLore :: MonadClientUI m => m (Maybe ResultItemDialogMode)
- Game.LambdaHack.Client.UI.HandleHumanLocalM: chooseItemHuman :: (MonadClient m, MonadClientUI m) => ItemDialogMode -> m MError
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: chooseItemHuman :: MonadClientUI m => ActorId -> ItemDialogMode -> m MError
- Game.LambdaHack.Client.UI.HandleHumanLocalM: chooseItemProjectHuman :: forall m. (MonadClient m, MonadClientUI m) => [TriggerItem] -> m MError
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: chooseItemProjectHuman :: forall m. (MonadClient m, MonadClientUI m) => ActorId -> [TriggerItem] -> m MError
- Game.LambdaHack.Client.UI.HandleHumanLocalM: clearTargetIfItemClearHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: clearTargetIfItemClearHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: detailCycleHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: detailCycleHuman :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: doLook :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: doLook :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: eitherHistory :: forall m. (MonadClient m, MonadClientUI m) => Bool -> m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: eitherHistory :: forall m. MonadClientUI m => Bool -> m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: endAiming :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: endAiming :: (MonadClient m, MonadClientUI m) => ActorId -> m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: endAimingMsg :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: endAimingMsg :: MonadClientUI m => ActorId -> m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: lastHistoryHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: lastHistoryHuman :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: macroHuman :: (MonadClient m, MonadClientUI m) => [String] -> m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: macroHuman :: MonadClientUI m => [String] -> m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: moveXhairHuman :: (MonadClient m, MonadClientUI m) => Vector -> Int -> m MError
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: moveXhairHuman :: MonadClientUI m => Vector -> Int -> m MError
- Game.LambdaHack.Client.UI.HandleHumanLocalM: permittedApplyClient :: MonadClientUI m => m (Maybe CStore -> ItemFull -> ItemQuant -> Either ReqFailure Bool)
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: permittedApplyClient :: MonadClientUI m => ActorId -> m (Maybe CStore -> ItemFull -> ItemQuant -> Either ReqFailure Bool)
- Game.LambdaHack.Client.UI.HandleHumanLocalM: permittedProjectClient :: MonadClientUI m => m (ItemFull -> Either ReqFailure Bool)
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: permittedProjectClient :: MonadClientUI m => ActorId -> m (ItemFull -> Either ReqFailure Bool)
- Game.LambdaHack.Client.UI.HandleHumanLocalM: pickLeaderHuman :: (MonadClient m, MonadClientUI m) => Int -> m MError
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: pickLeaderHuman :: MonadClientUI m => Int -> m MError
- Game.LambdaHack.Client.UI.HandleHumanLocalM: pickLeaderWithPointerHuman :: (MonadClient m, MonadClientUI m) => m MError
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: pickLeaderWithPointerHuman :: MonadClientUI m => ActorId -> m MError
- Game.LambdaHack.Client.UI.HandleHumanLocalM: posFromXhair :: (MonadClient m, MonadClientUI m) => m (Either Text Point)
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: posFromXhair :: (MonadClient m, MonadClientUI m) => ActorId -> m (Either Text Point)
- Game.LambdaHack.Client.UI.HandleHumanLocalM: printScreenHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: printScreenHuman :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: projectCheck :: MonadClientUI m => Point -> m (Maybe ReqFailure)
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: projectCheck :: MonadClientUI m => ActorId -> Point -> m (Maybe ReqFailure)
- Game.LambdaHack.Client.UI.HandleHumanLocalM: psuitReq :: (MonadClient m, MonadClientUI m) => m (Either Text (ItemFull -> Either ReqFailure (Point, Bool)))
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: psuitReq :: (MonadClient m, MonadClientUI m) => ActorId -> m (Either Text (ItemFull -> Either ReqFailure (Point, Bool)))
- Game.LambdaHack.Client.UI.HandleHumanLocalM: recordHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: recordHuman :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: selectActorHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: selectActorHuman :: MonadClientUI m => ActorId -> m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: selectNoneHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: selectNoneHuman :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: selectWithPointerHuman :: (MonadClient m, MonadClientUI m) => m MError
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: selectWithPointerHuman :: MonadClientUI m => m MError
- Game.LambdaHack.Client.UI.HandleHumanLocalM: xhairItemHuman :: (MonadClient m, MonadClientUI m) => m MError
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: xhairItemHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m MError
- Game.LambdaHack.Client.UI.HandleHumanLocalM: xhairLegalEps :: MonadClientUI m => m (Either Text Int)
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: xhairLegalEps :: MonadClientUI m => ActorId -> m (Either Text Int)
- Game.LambdaHack.Client.UI.HandleHumanLocalM: xhairPointerEnemyHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: xhairPointerEnemyHuman :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: xhairPointerFloorHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: xhairPointerFloorHuman :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: xhairPointerMuteHuman :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: xhairPointerMuteHuman :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HandleHumanLocalM: xhairStairHuman :: (MonadClient m, MonadClientUI m) => Bool -> m MError
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: xhairStairHuman :: (MonadClient m, MonadClientUI m) => ActorId -> Bool -> m MError
- Game.LambdaHack.Client.UI.HandleHumanLocalM: xhairUnknownHuman :: (MonadClient m, MonadClientUI m) => m MError
+ Game.LambdaHack.Client.UI.HandleHumanLocalM: xhairUnknownHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m MError
- Game.LambdaHack.Client.UI.HandleHumanM: addNoError :: Monad m => m () -> m (Either MError ReqUI)
+ Game.LambdaHack.Client.UI.HandleHumanM: addNoError :: Monad m => m () -> CmdLeaderNeed m
- Game.LambdaHack.Client.UI.HumanCmd: TriggerItem :: Part -> Part -> [Char] -> TriggerItem
+ Game.LambdaHack.Client.UI.HumanCmd: TriggerItem :: Part -> Part -> [ContentSymbol ItemKind] -> TriggerItem
- Game.LambdaHack.Client.UI.HumanCmd: [tisymbols] :: TriggerItem -> [Char]
+ Game.LambdaHack.Client.UI.HumanCmd: [tisymbols] :: TriggerItem -> [ContentSymbol ItemKind]
- Game.LambdaHack.Client.UI.InventoryM: DefItemKey :: Either Text KM -> Bool -> (Either KM SlotChar -> m (Either Text ResultItemDialogMode)) -> DefItemKey m
+ Game.LambdaHack.Client.UI.InventoryM: DefItemKey :: Either Text KM -> Bool -> (KeyOrSlot -> m (Either Text ResultItemDialogMode)) -> DefItemKey m
- Game.LambdaHack.Client.UI.InventoryM: [defAction] :: DefItemKey m -> Either KM SlotChar -> m (Either Text ResultItemDialogMode)
+ Game.LambdaHack.Client.UI.InventoryM: [defAction] :: DefItemKey m -> KeyOrSlot -> m (Either Text ResultItemDialogMode)
- Game.LambdaHack.Client.UI.InventoryM: getFull :: (MonadClient m, MonadClientUI m) => m Suitability -> (Actor -> ActorUI -> Skills -> ItemDialogMode -> State -> Text) -> (Actor -> ActorUI -> Skills -> ItemDialogMode -> State -> Text) -> [CStore] -> Bool -> Bool -> m (Either Text (CStore, [(ItemId, ItemQuant)]))
+ Game.LambdaHack.Client.UI.InventoryM: getFull :: MonadClientUI m => ActorId -> m Suitability -> (Actor -> ActorUI -> Skills -> ItemDialogMode -> State -> Text) -> (Actor -> ActorUI -> Skills -> ItemDialogMode -> State -> Text) -> [CStore] -> Bool -> Bool -> m (Either Text (CStore, [(ItemId, ItemQuant)]))
- Game.LambdaHack.Client.UI.InventoryM: getGroupItem :: (MonadClient m, MonadClientUI m) => m Suitability -> Text -> Text -> Text -> Text -> [CStore] -> m (Either Text (CStore, ItemId))
+ Game.LambdaHack.Client.UI.InventoryM: getGroupItem :: MonadClientUI m => ActorId -> m Suitability -> Text -> Text -> Text -> Text -> [CStore] -> m (Either Text (CStore, ItemId))
- Game.LambdaHack.Client.UI.InventoryM: getItem :: (MonadClient m, MonadClientUI m) => m Suitability -> (Actor -> ActorUI -> Skills -> ItemDialogMode -> State -> Text) -> (Actor -> ActorUI -> Skills -> ItemDialogMode -> State -> Text) -> ItemDialogMode -> [ItemDialogMode] -> Bool -> Bool -> m (Either Text ResultItemDialogMode)
+ Game.LambdaHack.Client.UI.InventoryM: getItem :: MonadClientUI m => ActorId -> m Suitability -> (Actor -> ActorUI -> Skills -> ItemDialogMode -> State -> Text) -> (Actor -> ActorUI -> Skills -> ItemDialogMode -> State -> Text) -> ItemDialogMode -> [ItemDialogMode] -> Bool -> Bool -> m (Either Text ResultItemDialogMode)
- Game.LambdaHack.Client.UI.InventoryM: getStoreItem :: (MonadClient m, MonadClientUI m) => ItemDialogMode -> m (Either Text ResultItemDialogMode)
+ Game.LambdaHack.Client.UI.InventoryM: getStoreItem :: MonadClientUI m => ActorId -> ItemDialogMode -> m (Either Text ResultItemDialogMode)
- Game.LambdaHack.Client.UI.InventoryM: keyOfEKM :: Int -> Either [KM] SlotChar -> Maybe KM
+ Game.LambdaHack.Client.UI.InventoryM: keyOfEKM :: Int -> KeyOrSlot -> Maybe KM
- Game.LambdaHack.Client.UI.InventoryM: runDefItemKey :: (MonadClient m, MonadClientUI m) => [(KM, DefItemKey m)] -> DefItemKey m -> OKX -> [KM] -> Text -> ItemDialogMode -> m (Either Text ResultItemDialogMode)
+ Game.LambdaHack.Client.UI.InventoryM: runDefItemKey :: MonadClientUI m => ActorId -> SingleItemSlots -> ItemBag -> [(KM, DefItemKey m)] -> DefItemKey m -> OKX -> [KM] -> Text -> ItemDialogMode -> m (Either Text ResultItemDialogMode)
- Game.LambdaHack.Client.UI.InventoryM: transition :: forall m. (MonadClient m, MonadClientUI m) => m Suitability -> (Actor -> ActorUI -> Skills -> ItemDialogMode -> State -> Text) -> (Actor -> ActorUI -> Skills -> ItemDialogMode -> State -> Text) -> Bool -> Int -> ItemDialogMode -> [ItemDialogMode] -> ItemDialogState -> m (Either Text ResultItemDialogMode)
+ Game.LambdaHack.Client.UI.InventoryM: transition :: forall m. MonadClientUI m => ActorId -> m Suitability -> (Actor -> ActorUI -> Skills -> ItemDialogMode -> State -> Text) -> (Actor -> ActorUI -> Skills -> ItemDialogMode -> State -> Text) -> Bool -> Int -> ItemDialogMode -> [ItemDialogMode] -> ItemDialogState -> m (Either Text ResultItemDialogMode)
- Game.LambdaHack.Client.UI.KeyBindings: okxsN :: InputContent -> DisplayFont -> DisplayFont -> Int -> Int -> (HumanCmd -> Bool) -> Bool -> CmdCategory -> ([Text], [Text], [Text]) -> ([Text], [Text]) -> OKX
+ Game.LambdaHack.Client.UI.KeyBindings: okxsN :: InputContent -> DisplayFont -> DisplayFont -> Int -> (HumanCmd -> Bool) -> Bool -> CmdCategory -> ([Text], [Text], [Text]) -> ([Text], [Text]) -> OKX
- Game.LambdaHack.Client.UI.MonadClientUI: defaultHistory :: (MonadClient m, MonadClientUI m) => m History
+ Game.LambdaHack.Client.UI.MonadClientUI: defaultHistory :: MonadClientUI m => m History
- Game.LambdaHack.Client.UI.MonadClientUI: elapsedSessionTimeGT :: MonadClientUI m => Int -> m Bool
+ Game.LambdaHack.Client.UI.MonadClientUI: elapsedSessionTimeGT :: MonadClientRead m => POSIXTime -> Int -> m Bool
- Game.LambdaHack.Client.UI.MonadClientUI: xhairToPos :: MonadClientUI m => m (Maybe Point)
+ Game.LambdaHack.Client.UI.MonadClientUI: xhairToPos :: MonadClientUI m => m Point
- Game.LambdaHack.Client.UI.Msg: archiveReport :: Bool -> History -> History
+ Game.LambdaHack.Client.UI.Msg: archiveReport :: History -> History
- Game.LambdaHack.Client.UI.Msg: lengthHistory :: Bool -> History -> Int
+ Game.LambdaHack.Client.UI.Msg: lengthHistory :: History -> Int
- Game.LambdaHack.Client.UI.Msg: renderHistory :: Bool -> History -> [AttrString]
+ Game.LambdaHack.Client.UI.Msg: renderHistory :: History -> [AttrString]
- Game.LambdaHack.Client.UI.Msg: renderTimeReport :: Bool -> Time -> Report -> [AttrString]
+ Game.LambdaHack.Client.UI.Msg: renderTimeReport :: Time -> Report -> [AttrString]
- Game.LambdaHack.Client.UI.MsgM: msgAdd :: (MonadClient m, MonadClientUI m, MsgShared a) => a -> Text -> m ()
+ Game.LambdaHack.Client.UI.MsgM: msgAdd :: (MonadClientUI m, MsgShared a) => a -> Text -> m ()
- Game.LambdaHack.Client.UI.MsgM: msgAddDistinct :: (MonadClient m, MonadClientUI m) => MsgClassDistinct -> (Text, Text) -> m ()
+ Game.LambdaHack.Client.UI.MsgM: msgAddDistinct :: MonadClientUI m => MsgClassDistinct -> (Text, Text) -> m ()
- Game.LambdaHack.Client.UI.MsgM: msgAddDuplicate :: (MonadClient m, MonadClientUI m, MsgShared a) => a -> Text -> m Bool
+ Game.LambdaHack.Client.UI.MsgM: msgAddDuplicate :: (MonadClientUI m, MsgShared a) => a -> Text -> m Bool
- Game.LambdaHack.Client.UI.MsgM: msgLnAdd :: (MonadClient m, MonadClientUI m, MsgShared a) => a -> Text -> m ()
+ Game.LambdaHack.Client.UI.MsgM: msgLnAdd :: (MonadClientUI m, MsgShared a) => a -> Text -> m ()
- Game.LambdaHack.Client.UI.MsgM: promptMainKeys :: (MonadClient m, MonadClientUI m) => m ()
+ Game.LambdaHack.Client.UI.MsgM: promptMainKeys :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.Overlay: indentSplitAttrString :: Int -> AttrString -> [AttrLine]
+ Game.LambdaHack.Client.UI.Overlay: indentSplitAttrString :: DisplayFont -> Int -> AttrString -> [AttrLine]
- Game.LambdaHack.Client.UI.SessionUI: SessionUI :: Maybe Target -> Maybe Target -> ActorDictUI -> ItemDictUI -> ItemSlots -> Maybe (CStore, CStore) -> ChanFrontend -> CCUI -> UIOptions -> Maybe AimMode -> Bool -> Maybe (ItemId, CStore, Bool) -> EnumSet ActorId -> Maybe RunParams -> History -> PointUI -> KeyMacroFrame -> [KeyMacroFrame] -> EnumSet ActorId -> Int -> Bool -> Bool -> Bool -> Int -> Bool -> Bool -> Maybe Bool -> Set Msg -> Map String Int -> ChosenLore -> Bool -> Bool -> Bool -> POSIXTime -> POSIXTime -> Time -> Int -> Int -> SMGen -> SessionUI
+ Game.LambdaHack.Client.UI.SessionUI: SessionUI :: Maybe RequestUI -> ReqDelay -> Bool -> Bool -> Maybe Target -> Maybe Target -> ActorDictUI -> ItemDictUI -> ItemSlots -> Maybe (CStore, CStore) -> ChanFrontend -> CCUI -> UIOptions -> Maybe AimMode -> Bool -> Maybe (ItemId, CStore, Bool) -> EnumSet ActorId -> Maybe RunParams -> History -> PointUI -> Bool -> KeyMacroFrame -> [KeyMacroFrame] -> EnumSet ActorId -> Int -> Bool -> Int -> Bool -> Int -> Bool -> Bool -> Maybe Bool -> Set Msg -> Bool -> Map String Int -> ChosenLore -> Bool -> Bool -> Bool -> POSIXTime -> POSIXTime -> Time -> Int -> Int -> SMGen -> SessionUI
- Game.LambdaHack.Client.UI.SessionUI: [smarkVision] :: SessionUI -> Bool
+ Game.LambdaHack.Client.UI.SessionUI: [smarkVision] :: SessionUI -> Int
- Game.LambdaHack.Client.UI.Slideshow: attrLinesToFontMap :: Int -> [(DisplayFont, [AttrLine])] -> FontOverlayMap
+ Game.LambdaHack.Client.UI.Slideshow: attrLinesToFontMap :: [(DisplayFont, [AttrLine])] -> FontOverlayMap
- Game.LambdaHack.Client.UI.Slideshow: splitOverlay :: FontSetup -> Bool -> Int -> Int -> Int -> Report -> [KM] -> OKX -> Slideshow
+ Game.LambdaHack.Client.UI.Slideshow: splitOverlay :: FontSetup -> Int -> Int -> Int -> Report -> [KM] -> OKX -> Slideshow
- Game.LambdaHack.Client.UI.Slideshow: type KYX = (Either [KM] SlotChar, (PointUI, ButtonWidth))
+ Game.LambdaHack.Client.UI.Slideshow: type KYX = (KeyOrSlot, (PointUI, ButtonWidth))
- Game.LambdaHack.Client.UI.SlideshowM: displayChoiceScreen :: forall m. (MonadClient m, MonadClientUI m) => String -> ColorMode -> Bool -> Slideshow -> [KM] -> m (Either KM SlotChar)
+ Game.LambdaHack.Client.UI.SlideshowM: displayChoiceScreen :: forall m. MonadClientUI m => String -> ColorMode -> Bool -> Slideshow -> [KM] -> m KeyOrSlot
- Game.LambdaHack.Client.UI.SlideshowM: displayMore :: (MonadClient m, MonadClientUI m) => ColorMode -> Text -> m ()
+ Game.LambdaHack.Client.UI.SlideshowM: displayMore :: MonadClientUI m => ColorMode -> Text -> m ()
- Game.LambdaHack.Client.UI.SlideshowM: displayMoreKeep :: (MonadClient m, MonadClientUI m) => ColorMode -> Text -> m ()
+ Game.LambdaHack.Client.UI.SlideshowM: displayMoreKeep :: MonadClientUI m => ColorMode -> Text -> m ()
- Game.LambdaHack.Client.UI.SlideshowM: displaySpaceEsc :: (MonadClient m, MonadClientUI m) => ColorMode -> Text -> m Bool
+ Game.LambdaHack.Client.UI.SlideshowM: displaySpaceEsc :: MonadClientUI m => ColorMode -> Text -> m Bool
- Game.LambdaHack.Client.UI.SlideshowM: displayYesNo :: (MonadClient m, MonadClientUI m) => ColorMode -> Text -> m Bool
+ Game.LambdaHack.Client.UI.SlideshowM: displayYesNo :: MonadClientUI m => ColorMode -> Text -> m Bool
- Game.LambdaHack.Client.UI.SlideshowM: getConfirms :: (MonadClient m, MonadClientUI m) => ColorMode -> [KM] -> Slideshow -> m KM
+ Game.LambdaHack.Client.UI.SlideshowM: getConfirms :: MonadClientUI m => ColorMode -> [KM] -> Slideshow -> m KM
- Game.LambdaHack.Client.UI.UIOptions: UIOptions :: [(KM, CmdTriple)] -> [(Int, (Text, Text))] -> Bool -> Bool -> Text -> Double -> Bool -> Bool -> Int -> Double -> Bool -> Int -> [(String, Color)] -> [String] -> [(Text, FontDefinition)] -> [(Text, FontSet)] -> FullscreenMode -> UIOptions
+ Game.LambdaHack.Client.UI.UIOptions: UIOptions :: [(KM, CmdTriple)] -> [(Int, (Text, Text))] -> Bool -> Bool -> Text -> Double -> FullscreenMode -> Int -> X -> Int -> Double -> Bool -> [String] -> [(Text, FontDefinition)] -> [(Text, FontSet)] -> [(String, Color)] -> UIOptions
- Game.LambdaHack.Common.Actor: monsterGenChance :: AbsDepth -> AbsDepth -> Int -> Int -> Rnd Bool
+ Game.LambdaHack.Common.Actor: monsterGenChance :: AbsDepth -> AbsDepth -> Int -> Int -> Int
- Game.LambdaHack.Common.Area: inside :: Point -> Area -> Bool
+ Game.LambdaHack.Common.Area: inside :: Area -> Point -> Bool
- Game.LambdaHack.Common.ClientOptions: ClientOptions :: Maybe Text -> Maybe Double -> [(Text, FontDefinition)] -> [(Text, FontSet)] -> Maybe FullscreenMode -> Maybe Int -> Maybe Double -> Bool -> Maybe Bool -> Bool -> Bool -> Maybe String -> String -> Bool -> Bool -> Bool -> Bool -> Maybe Int -> Maybe Int -> Bool -> Bool -> Bool -> Bool -> ClientOptions
+ Game.LambdaHack.Common.ClientOptions: ClientOptions :: Maybe Text -> Maybe Double -> [(Text, FontDefinition)] -> [(Text, FontSet)] -> Maybe FullscreenMode -> Maybe Int -> Maybe Double -> Bool -> Maybe Bool -> Bool -> Bool -> Bool -> Maybe String -> String -> Bool -> Bool -> Bool -> Bool -> Bool -> Maybe Int -> Maybe Int -> Bool -> Bool -> Bool -> Bool -> ClientOptions
- Game.LambdaHack.Common.Point: bla :: X -> Y -> Int -> Point -> Point -> Maybe [Point]
+ Game.LambdaHack.Common.Point: bla :: Int -> Point -> Point -> Maybe [Point]
- Game.LambdaHack.Common.ReqFailure: permittedApply :: Time -> Int -> Bool -> Maybe CStore -> ItemFull -> ItemQuant -> Either ReqFailure Bool
+ Game.LambdaHack.Common.ReqFailure: permittedApply :: RuleContent -> Time -> Int -> Bool -> Maybe CStore -> ItemFull -> ItemQuant -> Either ReqFailure Bool
- Game.LambdaHack.Content.CaveKind: CaveKind :: Char -> Text -> Freqs CaveKind -> X -> Y -> DiceXY -> DiceXY -> DiceXY -> Dice -> Dice -> Rational -> Rational -> Chance -> Chance -> Int -> Int -> Freqs ItemKind -> Dice -> Freqs ItemKind -> Freqs PlaceKind -> Bool -> Bool -> GroupName TileKind -> GroupName TileKind -> GroupName TileKind -> GroupName TileKind -> GroupName TileKind -> GroupName TileKind -> GroupName TileKind -> GroupName TileKind -> GroupName TileKind -> Bool -> GroupName TileKind -> GroupName TileKind -> Int -> Dice -> Freqs PlaceKind -> Freqs PlaceKind -> Freqs PlaceKind -> [Int] -> Text -> CaveKind
+ Game.LambdaHack.Content.CaveKind: CaveKind :: Char -> Text -> Freqs CaveKind -> X -> Y -> DiceXY -> DiceXY -> DiceXY -> Dice -> Dice -> Rational -> Rational -> Chance -> Chance -> Int -> Int -> Freqs ItemKind -> Dice -> Freqs ItemKind -> Freqs PlaceKind -> Bool -> Bool -> GroupName TileKind -> GroupName TileKind -> GroupName TileKind -> GroupName TileKind -> GroupName TileKind -> GroupName TileKind -> GroupName TileKind -> GroupName TileKind -> GroupName TileKind -> Bool -> Int -> Dice -> Freqs PlaceKind -> Freqs PlaceKind -> Freqs PlaceKind -> [Int] -> Text -> CaveKind
- Game.LambdaHack.Content.ItemKind: ItemKind :: Char -> Text -> Freqs ItemKind -> [Flavour] -> Dice -> Rarity -> Text -> Int -> Dice -> [Aspect] -> [Effect] -> [(GroupName ItemKind, CStore)] -> Text -> ItemKind
+ Game.LambdaHack.Content.ItemKind: ItemKind :: ContentSymbol ItemKind -> Text -> Freqs ItemKind -> [Flavour] -> Dice -> Rarity -> Text -> Int -> Dice -> [Aspect] -> [Effect] -> [(GroupName ItemKind, CStore)] -> Text -> ItemKind
- Game.LambdaHack.Content.ItemKind: [isymbol] :: ItemKind -> Char
+ Game.LambdaHack.Content.ItemKind: [isymbol] :: ItemKind -> ContentSymbol ItemKind
- Game.LambdaHack.Content.ItemKind: makeData :: [ItemKind] -> [GroupName ItemKind] -> [GroupName ItemKind] -> ContentData ItemKind
+ Game.LambdaHack.Content.ItemKind: makeData :: ItemSymbolsUsedInEngine -> [ItemKind] -> [GroupName ItemKind] -> [GroupName ItemKind] -> ContentData ItemKind
- Game.LambdaHack.Content.ItemKind: validateSingle :: ItemKind -> [Text]
+ Game.LambdaHack.Content.ItemKind: validateSingle :: ItemSymbolsUsedInEngine -> ItemKind -> [Text]
- Game.LambdaHack.Content.ModeKind: Player :: Text -> [GroupName ItemKind] -> Skills -> Bool -> Bool -> HiCondPoly -> Bool -> Doctrine -> LeaderMode -> Bool -> Player
+ Game.LambdaHack.Content.ModeKind: Player :: Text -> [GroupName ItemKind] -> Skills -> Bool -> Bool -> HiCondPoly -> Bool -> Doctrine -> Maybe AutoLeader -> Bool -> Bool -> Player
- Game.LambdaHack.Content.ModeKind: [fleaderMode] :: Player -> LeaderMode
+ Game.LambdaHack.Content.ModeKind: [fleaderMode] :: Player -> Maybe AutoLeader
- Game.LambdaHack.Content.PlaceKind: PlaceKind :: Char -> Text -> Freqs PlaceKind -> Rarity -> Cover -> Fence -> [Text] -> [(Char, GroupName TileKind)] -> [(Char, GroupName TileKind)] -> PlaceKind
+ Game.LambdaHack.Content.PlaceKind: PlaceKind :: Char -> Text -> Freqs PlaceKind -> Rarity -> Cover -> Fence -> [Text] -> EnumMap Char (GroupName TileKind) -> EnumMap Char (GroupName TileKind) -> PlaceKind
- Game.LambdaHack.Content.PlaceKind: makeData :: [PlaceKind] -> [GroupName PlaceKind] -> [GroupName PlaceKind] -> ContentData PlaceKind
+ Game.LambdaHack.Content.PlaceKind: makeData :: ContentData TileKind -> [PlaceKind] -> [GroupName PlaceKind] -> [GroupName PlaceKind] -> ContentData PlaceKind
- Game.LambdaHack.Content.PlaceKind: validateSingle :: PlaceKind -> [Text]
+ Game.LambdaHack.Content.PlaceKind: validateSingle :: ContentData TileKind -> PlaceKind -> [Text]
- Game.LambdaHack.Content.RuleKind: RuleContent :: String -> X -> Y -> Version -> FilePath -> (String, Config) -> Int -> Int -> FilePath -> Int -> [Text] -> Char -> RuleContent
+ Game.LambdaHack.Content.RuleKind: RuleContent :: String -> X -> Y -> Version -> FilePath -> (String, Config) -> Int -> Int -> FilePath -> Int -> [Text] -> ItemSymbolsUsedInEngine -> RuleContent
- Game.LambdaHack.Core.Prelude: infixl 6 -
+ Game.LambdaHack.Core.Prelude: infixl 6 +
- Game.LambdaHack.Core.Prelude: seq :: a -> b -> b
+ Game.LambdaHack.Core.Prelude: seq :: forall (r :: RuntimeRep) a (b :: TYPE r). a -> b -> b
- Game.LambdaHack.Definition.Defs: contentIdIndex :: ContentId k -> Int
+ Game.LambdaHack.Definition.Defs: contentIdIndex :: ContentId c -> Int
- Game.LambdaHack.Definition.Defs: type Freqs a = [(GroupName a, Int)]
+ Game.LambdaHack.Definition.Defs: type Freqs c = [(GroupName c, Int)]
- Game.LambdaHack.Server.DungeonGen.Place: pover :: COps -> [(Char, GroupName TileKind)] -> Rnd (EnumMap Char (Int, Int, ContentId TileKind), EnumMap Char (ContentId TileKind))
+ Game.LambdaHack.Server.DungeonGen.Place: pover :: COps -> EnumMap Char (GroupName TileKind) -> Rnd (EnumMap Char (Maybe (Int, Int, ContentId TileKind), ContentId TileKind))
- Game.LambdaHack.Server.ItemM: prepareItemKind :: MonadServerAtomic m => Int -> LevelId -> Freqs ItemKind -> m (Frequency (ContentId ItemKind, ItemKind))
+ Game.LambdaHack.Server.ItemM: prepareItemKind :: MonadServerAtomic m => Int -> AbsDepth -> Freqs ItemKind -> m (Frequency (ContentId ItemKind, ItemKind))
- Game.LambdaHack.Server.ItemM: rollAndRegisterItem :: MonadServerAtomic m => Bool -> LevelId -> Frequency (ContentId ItemKind, ItemKind) -> Container -> Maybe Int -> m (Maybe (ItemId, ItemFullKit))
+ Game.LambdaHack.Server.ItemM: rollAndRegisterItem :: MonadServerAtomic m => Bool -> AbsDepth -> Frequency (ContentId ItemKind, ItemKind) -> Container -> Maybe Int -> m (Maybe (ItemId, ItemFullKit))
- Game.LambdaHack.Server.ItemM: rollItemAspect :: MonadServerAtomic m => Frequency (ContentId ItemKind, ItemKind) -> LevelId -> m NewItem
+ Game.LambdaHack.Server.ItemM: rollItemAspect :: MonadServerAtomic m => Frequency (ContentId ItemKind, ItemKind) -> AbsDepth -> m NewItem
- Game.LambdaHack.Server.PeriodicM: addAnyActor :: MonadServerAtomic m => Bool -> Int -> Freqs ItemKind -> LevelId -> Time -> Maybe Point -> m (Maybe ActorId)
+ Game.LambdaHack.Server.PeriodicM: addAnyActor :: MonadServerAtomic m => Bool -> Int -> Freqs ItemKind -> LevelId -> Time -> Maybe Point -> m (Maybe (ActorId, Point))
- Game.LambdaHack.Server.ProtocolM: sendQueryUI :: (MonadServerAtomic m, MonadServerComm m) => FactionId -> ActorId -> m RequestUI
+ Game.LambdaHack.Server.ProtocolM: sendQueryUI :: (MonadServerAtomic m, MonadServerComm m) => Response -> FactionId -> ActorId -> m RequestUI
- Game.LambdaHack.Server.StartM: reinitGame :: MonadServerAtomic m => m ()
+ Game.LambdaHack.Server.StartM: reinitGame :: MonadServerAtomic m => FactionDict -> m ()
- Game.LambdaHack.Server.State: StateServer :: ActorTime -> ActorTime -> ActorPushedBy -> GearOfTeams -> GearOfTeams -> EnumMap TeamContinuity Int -> FactionAnalytics -> ActorAnalytics -> GenerationAnalytics -> EnumSet ActorId -> DiscoveryKindRev -> UniqueSet -> ItemRev -> FlavourMap -> ActorId -> ItemId -> EnumMap LevelId Int -> () -> EnumMap FactionId State -> PerFid -> PerValidFid -> PerCacheFid -> FovLucidLid -> FovClearLid -> FovLitLid -> EnumSet LevelId -> Bool -> SMGen -> RNGs -> Bool -> Bool -> Bool -> ServerOptions -> ServerOptions -> StateServer
+ Game.LambdaHack.Server.State: StateServer :: ActorTime -> ActorTime -> ActorPushedBy -> GearOfTeams -> GearOfTeams -> EnumMap TeamContinuity Int -> FactionAnalytics -> ActorAnalytics -> GenerationAnalytics -> EnumSet ActorId -> DiscoveryKindRev -> UniqueSet -> ItemRev -> FlavourMap -> ActorId -> ItemId -> EnumMap LevelId Int -> IntMap Int -> () -> EnumMap FactionId State -> PerFid -> PerValidFid -> PerCacheFid -> FovLucidLid -> FovClearLid -> FovLitLid -> EnumSet LevelId -> Bool -> SMGen -> RNGs -> Bool -> Bool -> Bool -> ServerOptions -> ServerOptions -> StateServer

Files

CHANGELOG.md view
@@ -1,3 +1,32 @@+## [v0.10.3.0](https://github.com/LambdaHack/LambdaHack/compare/v0.10.2.0...v0.10.3.0)++- Work around regression https://gitlab.freedesktop.org/freetype/freetype/-/issues/1076 by making the scalable square font the default as the map font; the tiny map fonts, for which there is no such workaround, wont work for anybody with freetype 2.11.0+- Enable display of details in right pane in many menus+- Switch mouse wheel to move selection now that it changes right pane display+- Make the line where messages wrap configurable in config file+- Remove rarely used options from config file and code+- Prepare client-server for delays when operating over the network+- Reveal all map at game over and make it explorable+- Don't run with -threaded, increasing speed by a couple dozen percent+- Don't show messages during enemy turn (except when under AI control)+- Spawn many actors at once with probability correlated to spawning at all+- Gut out Ubuntu Font Family fonts+- Default to more old-fashioned small fonts in LambdaHack the game+- Add a few tutorial messages, reword some in-game texts+- Move with CI from Travis to GitHub Actions (but GHA can't keep up as well)+- Add hlint to CI+- Make place content directly define legends instead of overriding them+- Add a warning that crosshair is out of flinging range+- Add ANSI display to SDL2 frontend; remove vty, hcurses and GTK frontends+- Make Teletype frontend more usable (input, speed, overlays)+- Remove sight malus from light sources to avoid micromanagement+- Keep convenience settings between new games+- Add an option to mark FOV area with grey background+- Refactor and clean up a lot of the code+- Unhardwire various constants (hat tip to Jamie)+- Fix a couple of bugs and improve documentation+- Add whole new kinds of tests (even greater hat tip to Jamie)+ ## [v0.10.2.0](https://github.com/LambdaHack/LambdaHack/compare/v0.9.5.0...v0.10.2.0)  - Let '?' scroll help
+ CREDITS view
@@ -0,0 +1,103 @@+All kinds of contributions to the LambdaHack engine are gratefully welcome!+Some of the contributors are listed below, in chronological order.++Andres Loeh+Mikolaj Konarski+Tuukka Turto+Veronika Romashkina+Daniel Keefe+Pablo Reszczynski+Rafał Szczerski+Oleg Grenrus+Simon Michael+bulbousBullfrog+Alex Byaly+Jamie Fristrom++++Fonts 16x16xw.woff, 16x16xw.bdf, 16x16x.fnt, 8x8x.fnt and 8x8xb.fnt+are are derived from fonts taken from+https://github.com/angband/angband, copyrighted by Leon Marrick,+Sheldon Simms III and Nick McConnell and released by them under+GNU GPL, version 2 or any later version (confirmed at+http://www.thangorodrim.net/development/opensource.html).+Any further modifications by authors of LambdaHack+are also released under GNU GPL version 2.++Font 16x16xw.bdf is derived from 16x16x.fon by changing all but+a few glyphs, converting to BDF format, extending character set+and hacking the font file with bdftopcf and pcf2bdf to include+full bitmaps, not only non-zero portions, for otherwise SDL2-ttf+was not able to display the glyphs.+Font 16x16xw.woff was derived from 16x16xw.bdf by changing format+to TTF with bitsnpicas, faking descendent offsets to be 1 point lower+to prevent freetype from adding an extra pixel to the descendent,+tweaking with fontforge glyphs 3 5 6 8 A a S s b d h to prevent antialiasing+of their vital parts when zoomed out, auto-hinting, manually simplifying+hints in some glyphs and converting to WOFF format.++++Fonts BinarySansProLH-Regular.ttf.woff, BinarySansProLH-Semibold.ttf.woff+and BinaryCodeProLH-Bold.ttf.woff are compiled from sources+at https://github.com/adobe-fonts/source-sans-pro+and https://github.com/adobe-fonts/source-code-pro+published with the following copyright notice:++Copyright 2010-2019 Adobe (http://www.adobe.com/), with Reserved Font+Name 'Source'. All Rights Reserved. Source is a trademark of Adobe+in the United States and/or other countries.+This Font Software is licensed under the SIL Open Font License, Version 1.1.++The sources are modified and processed as follows (see+https://github.com/adobe-fonts/source-sans-pro/issues/193 for background):++in Roman/Instances/Semibold/font.ufo/glyphs/s.glif+put advance width="452"+rm Roman/Instances/Semibold/font.ufo/data/com.adobe.type.processedHashMap+bash build.sh+pyftfeatfreeze -f 'cv03' -S -U LH -R 'Source/Binary' target/TTF/SourceSansPro-Semibold.ttf BinarySansProLH-Semibold.ttf+pyftsubset BinarySansProLH-Semibold.ttf --unicodes="*" --flavor=woff --with-zopfli --output-file=BinarySansProLH-Semibold.ttf.woff++and similarly for BinarySansProLH-Regular.ttf.woff, while+for BinaryCodeProLH-Bold.ttf.woff the last steps are:++pyftfeatfreeze -f 'cv02' -S -U LH -R 'Source/Binary' target/TTF/SourceCodePro-Bold.ttf BinaryCodeProLH-Bold.ttf+pyftsubset BinaryCodeProLH-Bold.ttf --unicodes="*" --flavor=woff --with-zopfli --output-file=BinaryCodeProLH-Bold.ttf.woff++++Fonts DejaVuLGCSans.ttf.woff and DejaVuLGCSans-Bold.ttf.woff+are downloaded from https://github.com/dejavu-fonts/dejavu-fonts+and compresses with pyftsubset. Their copyright notice is+DejaVu fonts 2.37 (c)2004-2016 DejaVu fonts team.+Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.++Font Hack-Bold.ttf.woff is downloaded from+https://github.com/source-foundry/Hack+and compresses with pyftsubset. Its copyright notice is+Hack work is (c) 2018 Source Foundry Authors. MIT License.+Bitstream Vera Sans Mono (c) 2003 Bitstream, Inc. (with Reserved Font+Names _Bitstream_ and _Vera_). Bitstream Vera License.++++The Ubuntu Font Family fonts are currently removed, because they are+a little too tall (some glyphs on consecutive lines are touching)+and also considered non-free by Debian and so we can't include them+in Debian and some other GNU/Linux distributions anyway.+If there is sufficient interest, they may be brought back.++They were fonts ubuntu-v14-latin-ext_latin-regular.ttf.woff,+ubuntu-v14-latin-ext_latin-500.ttf.woff+and ubuntu-mono-v9-latin-ext_latin-700.ttf.woff+that were generated via google-webfonts-helper.herokuapp.com+from 'Ubuntu Font Family' fonts with the following copyright notice:+Copyright 2011 Canonical Ltd. Licensed under the Ubuntu Font Licence 1.0++The files were compressed with pyftsubset to save some space and make+sure they are created from TTF, not coverted OTF fonts. Unfortunately,+being TTF fonts and having broken hinting in the mono variant, they+require forcing TTF.Light hinting in the SDL frontend. If ever OTF fonts+emerge, they should be used instead (similarly compressed).
GameDefinition/Content/CaveKind.hs view
@@ -26,6 +26,7 @@ import           Game.LambdaHack.Content.TileKind import           Game.LambdaHack.Core.Dice import           Game.LambdaHack.Definition.Defs+import           Game.LambdaHack.Definition.DefsInternal  -- * Group name patterns @@ -88,10 +89,10 @@   , chidden       = 7   , cactorCoeff   = 65  -- the maze requires time to explore   , cactorFreq    = [(MONSTER, 60), (ANIMAL, 40)]-  , citemNum      = 6 `d` 5 + 10 - 10 `dL` 1  -- deep down quality over quantity+  , citemNum      = 6 `d` 5 + 10 - 10 `dL` 1+      -- deep down quality over quantity; generally quite random,+      -- for interesting replays at the cost of unreliable balance   , citemFreq     = [(IK.COMMON_ITEM, 40), (IK.TREASURE, 60)]-    -- note that the groups are flattened; e.g., if an item is moved to another-    -- group included here with the same weight, the outcome wouldn't change   , cplaceFreq    = [(ROGUE, 1)]   , cpassable     = False   , labyrinth     = False@@ -105,8 +106,6 @@   , cfenceTileS   = S_BASIC_OUTER_FENCE   , cfenceTileW   = S_BASIC_OUTER_FENCE   , cfenceApart   = False-  , clegendDarkTile = LEGEND_DARK-  , clegendLitTile  = LEGEND_LIT   , cminStairDist = 20   , cmaxStairsNum = 1 + 1 `d` 2   , cescapeFreq   = []@@ -280,7 +279,6 @@   , cminStairDist = 10   , cmaxStairsNum = 1   , cescapeFreq   = [(INDOOR_ESCAPE_UP, 1)]-  , cskip         = [0]   , cdesc         = "This close to the surface, the sunlight still illuminates the dungeon."   } shallowRogue = rogue@@ -312,7 +310,7 @@   , cmaxStairsNum = 0   , cescapeFreq   = [(INDOOR_ESCAPE_UP, 1)]   , cstairFreq    = []-  , cskip         = [0]+  , cstairAllowed = []   , cdesc         = ""   } brawl = rogue  -- many random solid tiles, to break LOS, since it's a day@@ -434,7 +432,7 @@   , chidden       = 0   , cactorFreq    = []   , citemNum      = 6 `d` 8-  , citemFreq     = [ (IK.COMMON_ITEM, 30), (GEM, 250)+  , citemFreq     = [ (IK.COMMON_ITEM, 30), (GEM, 500)                     , (WEAK_ARROW, 500), (HARPOON, 400)                     , (IK.EXPLOSIVE, 100) ]   , cplaceFreq    = [(ESCAPE, 1)]@@ -449,7 +447,7 @@   , cmaxStairsNum = 0   , cescapeFreq   = [(OUTDOOR_ESCAPE_DOWN, 1)]   , cstairFreq    = []-  , cskip         = [0]  -- don't start heroes on exit+  , cskip         = [0]  -- don't start heroes nor opponents on escape   , cdesc         = ""   } zoo = rogue  -- few lights and many solids, to help the less numerous heroes
GameDefinition/Content/ItemKind.hs view
@@ -23,6 +23,7 @@ import Game.LambdaHack.Definition.Ability import Game.LambdaHack.Definition.Color import Game.LambdaHack.Definition.Defs+import Game.LambdaHack.Definition.DefsInternal import Game.LambdaHack.Definition.Flavour  -- * Group name patterns@@ -31,7 +32,7 @@ groupNamesSingleton =        [S_FRAGRANCE, S_SINGLE_SPARK, S_SPARK]     ++ [FLASK_UNKNOWN, POTION_UNKNOWN, EDIBLE_PLANT_UNKNOWN, SCROLL_UNKNOWN, NECKLACE_UNKNOWN, RING_UNKNOWN, HAMMER_UNKNOWN, GEM_UNKNOWN, CURRENCY_UNKNOWN]-    ++ embedsGNSingleton ++ actorsGNSingleton ++ organsGNSingleton+    ++ actorsGNSingleton ++ organsGNSingleton     ++ blastsGNSingleton ++ temporariesGNSingleton  pattern FLASK_UNKNOWN, POTION_UNKNOWN, EDIBLE_PLANT_UNKNOWN, SCROLL_UNKNOWN, NECKLACE_UNKNOWN, RING_UNKNOWN, HAMMER_UNKNOWN, GEM_UNKNOWN, CURRENCY_UNKNOWN :: GroupName ItemKind@@ -73,9 +74,9 @@  items :: [ItemKind] items =-  [sandstoneRock, dart, spike, spike2, slingStone, slingBullet, paralizingProj, harpoon, harpoon2, net, fragmentationBomb, concussionBomb, flashBomb, firecrackerBomb, flaskTemplate, flask1, flask2, flask3, flask4, flask5, flask6, flask7, flask8, flask9, flask10, flask11, flask12, flask13, flask14, potionTemplate, potion1, potion2, potion3, potion4, potion5, potion6, potion7, potion8, potion9, potion10, potion11, potion12, potion13, potion14, potion15, scrollTemplate, scroll1, scroll2, scroll3, scroll4, scroll5, scroll6, scroll7, scroll8, scroll9, scroll10, scroll11, scroll12, scroll13, ediblePlantTemplate, ediblePlant1, ediblePlant2, ediblePlant3, ediblePlant4, ediblePlant5, ediblePlant6, ediblePlant7, light1, light2, light3, blanket, gorget, necklaceTemplate, necklace1, necklace2, necklace3, necklace4, necklace5, necklace6, necklace7, necklace8, necklace9, necklace10, motionScanner, imageItensifier, sightSharpening, ringTemplate, ring1, ring2, ring3, ring4, ring5, ring6, ring7, ring8, armorLeather, armorMail, meleeEnhancement, gloveFencing, gloveGauntlet, gloveJousting, hatUshanka, capReinforced, helmArmored, smokingJacket, buckler, shield, shield2, shield3, hammerTemplate, hammer1, hammer2, hammer3, hammerParalyze, hammerSpark, knife, daggerDropBestWeapon, sword, swordImpress, swordNullify, halberd, halberd2, halberd3, halberdPushActor, gemTemplate, gem1, gem2, gem3, gem4, gem5, currencyTemplate, currency, jumpingPole, seeingItem]+  [sandstoneRock, dart, spike, spike2, slingStone, slingBullet, paralizingProj, harpoon, harpoon2, net, fragmentationBomb, concussionBomb, flashBomb, firecrackerBomb, flaskTemplate, flask1, flask2, flask3, flask4, flask5, flask6, flask7, flask8, flask9, flask10, flask11, flask12, flask13, flask14, flask15, potionTemplate, potion1, potion2, potion3, potion4, potion5, potion6, potion7, potion8, potion9, potion10, potion11, potion12, potion13, potion14, potion15, scrollTemplate, scroll1, scroll2, scroll3, scroll4, scroll5, scroll6, scroll7, scroll8, scroll9, scroll10, scroll11, scroll12, scroll13, ediblePlantTemplate, ediblePlant1, ediblePlant2, ediblePlant3, ediblePlant4, ediblePlant5, ediblePlant6, ediblePlant7, light1, light2, light3, blanket, gorget, necklaceTemplate, necklace1, necklace2, necklace3, necklace4, necklace5, necklace6, necklace7, necklace8, necklace9, necklace10, motionScanner, imageItensifier, sightSharpening, ringTemplate, ring1, ring2, ring3, ring4, ring5, ring6, ring7, ring8, ring9, ring10, armorLeather, armorMail, meleeEnhancement, gloveFencing, gloveGauntlet, gloveJousting, hatUshanka, capReinforced, helmArmored, smokingJacket, buckler, shield, shield2, shield3, hammerTemplate, hammer1, hammer2, hammer3, hammerParalyze, hammerSpark, knife, daggerDischarge, sword, swordImpress, swordNullify, halberd, halberd2, halberd3, halberdPushActor, gemTemplate, gem1, gem2, gem3, gem4, gem5, currencyTemplate, currency, jumpingPole, seeingItem] -sandstoneRock,    dart, spike, spike2, slingStone, slingBullet, paralizingProj, harpoon, harpoon2, net, fragmentationBomb, concussionBomb, flashBomb, firecrackerBomb, flaskTemplate, flask1, flask2, flask3, flask4, flask5, flask6, flask7, flask8, flask9, flask10, flask11, flask12, flask13, flask14, potionTemplate, potion1, potion2, potion3, potion4, potion5, potion6, potion7, potion8, potion9, potion10, potion11, potion12, potion13, potion14, potion15, scrollTemplate, scroll1, scroll2, scroll3, scroll4, scroll5, scroll6, scroll7, scroll8, scroll9, scroll10, scroll11, scroll12, scroll13, ediblePlantTemplate, ediblePlant1, ediblePlant2, ediblePlant3, ediblePlant4, ediblePlant5, ediblePlant6, ediblePlant7, light1, light2, light3, blanket, gorget, necklaceTemplate, necklace1, necklace2, necklace3, necklace4, necklace5, necklace6, necklace7, necklace8, necklace9, necklace10, motionScanner, imageItensifier, sightSharpening, ringTemplate, ring1, ring2, ring3, ring4, ring5, ring6, ring7, ring8, armorLeather, armorMail, meleeEnhancement, gloveFencing, gloveGauntlet, gloveJousting, hatUshanka, capReinforced, helmArmored, smokingJacket, buckler, shield, shield2, shield3, hammerTemplate, hammer1, hammer2, hammer3, hammerParalyze, hammerSpark, knife, daggerDropBestWeapon, sword, swordImpress, swordNullify, halberd, halberd2, halberd3, halberdPushActor, gemTemplate, gem1, gem2, gem3, gem4, gem5, currencyTemplate, currency, jumpingPole, seeingItem :: ItemKind+sandstoneRock,    dart, spike, spike2, slingStone, slingBullet, paralizingProj, harpoon, harpoon2, net, fragmentationBomb, concussionBomb, flashBomb, firecrackerBomb, flaskTemplate, flask1, flask2, flask3, flask4, flask5, flask6, flask7, flask8, flask9, flask10, flask11, flask12, flask13, flask14, flask15, potionTemplate, potion1, potion2, potion3, potion4, potion5, potion6, potion7, potion8, potion9, potion10, potion11, potion12, potion13, potion14, potion15, scrollTemplate, scroll1, scroll2, scroll3, scroll4, scroll5, scroll6, scroll7, scroll8, scroll9, scroll10, scroll11, scroll12, scroll13, ediblePlantTemplate, ediblePlant1, ediblePlant2, ediblePlant3, ediblePlant4, ediblePlant5, ediblePlant6, ediblePlant7, light1, light2, light3, blanket, gorget, necklaceTemplate, necklace1, necklace2, necklace3, necklace4, necklace5, necklace6, necklace7, necklace8, necklace9, necklace10, motionScanner, imageItensifier, sightSharpening, ringTemplate, ring1, ring2, ring3, ring4, ring5, ring6, ring7, ring8, ring9, ring10, armorLeather, armorMail, meleeEnhancement, gloveFencing, gloveGauntlet, gloveJousting, hatUshanka, capReinforced, helmArmored, smokingJacket, buckler, shield, shield2, shield3, hammerTemplate, hammer1, hammer2, hammer3, hammerParalyze, hammerSpark, knife, daggerDischarge, sword, swordImpress, swordNullify, halberd, halberd2, halberd3, halberdPushActor, gemTemplate, gem1, gem2, gem3, gem4, gem5, currencyTemplate, currency, jumpingPole, seeingItem :: ItemKind  -- Keep the dice rolls and sides in aspects small so that not too many -- distinct items are generated (for display in item lore and for narrative@@ -91,29 +92,29 @@  -- * Item group symbols, partially from Nethack -symbolProjectile, _symbolLauncher, symbolLight, symbolTool, symbolSpecial, symbolGold, symbolNecklace, symbolRing, symbolPotion, symbolFlask, symbolScroll, symbolTorsoArmor, symbolMiscArmor, symbolClothes, symbolShield, symbolPolearm, symbolEdged, symbolHafted, symbolWand, _symbolStaff, symbolFood :: Char+symbolProjectile, _symbolLauncher, symbolLight, symbolTool, symbolSpecial, symbolGold, symbolNecklace, symbolRing, symbolPotion, symbolFlask, symbolScroll, symbolTorsoArmor, symbolMiscArmor, symbolClothes, symbolShield, symbolPolearm, symbolEdged, symbolHafted, symbolWand, _symbolStaff, symbolFood :: ContentSymbol ItemKind -symbolProjectile = rsymbolProjectile standardRules  -- '|'-_symbolLauncher  = '}'-symbolLight      = '('-symbolTool       = '('-symbolSpecial    = '*'  -- don't overuse, because it clashes with projectiles-symbolGold       = '$'  -- also gems-symbolNecklace   = '"'-symbolRing       = '='-symbolPotion     = '!'  -- concoction, bottle, jar, vial, canister-symbolFlask      = '!'-symbolScroll     = '?'  -- book, note, tablet, remote, chip, card-symbolTorsoArmor = '['-symbolMiscArmor  = '['-symbolClothes    = '['-symbolShield     = ']'-symbolPolearm    = ')'-symbolEdged      = ')'-symbolHafted     = ')'-symbolWand       = '/'  -- magical rod, transmitter, pistol, rifle, instrument-_symbolStaff     = '_'  -- scanner-symbolFood       = ','  -- also body part; distinct from floor: not middle dot+symbolProjectile = rsymbolProjectile $ ritemSymbols standardRules+_symbolLauncher  = toContentSymbol '}'+symbolLight      = rsymbolLight $ ritemSymbols standardRules+symbolTool       = rsymbolTool $ ritemSymbols standardRules+symbolSpecial    = rsymbolSpecial $ ritemSymbols standardRules+symbolGold       = rsymbolGold $ ritemSymbols standardRules+symbolNecklace   = rsymbolNecklace $ ritemSymbols standardRules+symbolRing       = rsymbolRing $ ritemSymbols standardRules+symbolPotion     = rsymbolPotion $ ritemSymbols standardRules+symbolFlask      = rsymbolFlask $ ritemSymbols standardRules+symbolScroll     = rsymbolScroll $ ritemSymbols standardRules+symbolTorsoArmor = rsymbolTorsoArmor $ ritemSymbols standardRules+symbolMiscArmor  = rsymbolMiscArmor $ ritemSymbols standardRules+symbolClothes    = rsymbolClothes $ ritemSymbols standardRules+symbolShield     = rsymbolShield $ ritemSymbols standardRules+symbolPolearm    = rsymbolPolearm $ ritemSymbols standardRules+symbolEdged      = rsymbolEdged $ ritemSymbols standardRules+symbolHafted     = rsymbolHafted $ ritemSymbols standardRules+symbolWand       = rsymbolWand $ ritemSymbols standardRules+_symbolStaff     = toContentSymbol '_'+symbolFood       = rsymbolFood $ ritemSymbols standardRules  -- ** Thrown weapons @@ -124,7 +125,7 @@                , (UNREPORTED_INVENTORY, 1) ]  -- too weak to spam   , iflavour = zipPlain [Green]   , icount   = 1 + 1 `d` 2  -- > 1, to let AI ignore sole pieces-  , irarity  = [(1, 50), (10, 1)]+  , irarity  = [(1, 20), (10, 1)]  -- a few already in starting stash   , iverbHit = "hit"   , iweight  = 300   , idamage  = 1 `d` 1@@ -155,7 +156,7 @@   { isymbol  = symbolProjectile   , iname    = "spike"   , ifreq    = [(COMMON_ITEM, 100), (ANY_ARROW, 50), (WEAK_ARROW, 50)]-  , iflavour = zipPlain [Cyan]+  , iflavour = zipPlain [BrCyan]   , icount   = 1 + 4 `dL` 5   , irarity  = [(1, 10), (10, 8)]   , iverbHit = "nick"@@ -174,10 +175,10 @@   } spike2 = spike   { ifreq    = [(COMMON_ITEM, 2), (ANY_ARROW, 1), (WEAK_ARROW, 1)]-  , icount   = 6 `dL` 5+  , iflavour = zipPlain [Cyan]   , iverbHit = "penetrate"   , iweight  = 200-  , idamage = 4 `d` 1+  , idamage  = 4 `d` 1   , iaspects = [ AddSkill SkHurtMelee $ (-10 + 1 `d` 2 + 1 `dL` 3) * 5                , SetFlag MinorEffects                , Odds (10 * 1 `dL` 10) [] [toVelocity 70] ]@@ -215,7 +216,10 @@   , idamage  = 1 `d` 1   , iaspects = [ AddSkill SkHurtMelee $ (-17 + 1 `d` 2 + 1 `dL` 3) * 5                    -- not too good against armor-               , ToThrow $ ThrowMod 200 100 2 ]  -- piercing+               , ToThrow $ ThrowMod 200 100 2  -- piercing+               , SetFlag Fragile ]+                   -- otherwise would rarely break and the player would have+                   -- unlimited resource and would have to pick up constantly   , ieffects = []   , idesc    = "Small almond-shaped leaden projectile that weighs more than the sling used to tie the bag. It doesn't drop out of the sling's pouch when swung and doesn't snag when released. Known to pierce through flesh, at least at maximum speed."  -- we lie, it doesn't slow down in our model; but it stops piercing alright   , ikit     = []@@ -391,7 +395,7 @@                , (ANY_GLASS, 100) ]   , icount   = 1 `dL` 5   , irarity  = [(10, 10)]-  , iaspects = ELabel "of strength renewal brew"+  , iaspects = ELabel "of strength brew"                : iaspects flaskTemplate   , ieffects = [ toOrganGood S_STRENGTHENED (20 + 1 `d` 5)                , OnSmash (Explode S_DENSE_SHOWER) ]@@ -436,8 +440,8 @@   , irarity  = [(1, 1)]  -- not every playthrough needs one   , iaspects = ELabel "of resolution"                : iaspects flaskTemplate-  , ieffects = [ toOrganGood S_RESOLUTE (500 + 1 `d` 200)  -- long, for scouting-               , RefillCalm 60  -- not to make it a drawback, via @calmEnough@+  , ieffects = [ toOrganGood S_RESOLUTE (100 + 1 `d` 20)  -- long, for scouting+               , RefillCalm 100  -- not to make it a drawback, via @calmEnough@                , OnSmash (Explode S_RESOLUTION_DUST) ]   } flask7 = flaskTemplate@@ -517,8 +521,16 @@                , toOrganBad S_DEFENSELESS (20 + 1 `d` 5)                , OnSmash (Explode S_GLASS_HAIL) ]  -- enough glass to cause that   }+flask15 = flaskTemplate+  { ifreq    = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100)+               , (ANY_GLASS, 100) ]+  , iaspects = ELabel "of snail gel"+               : iaspects flaskTemplate+  , ieffects = [ toOrganBad S_SLOWED (3 + 1 `d` 3)+               , OnSmash (Explode S_FOCUSED_SLOWNESS_MIST) ]+  } --- Vials are often not intended to be thrown. They usually natural,+-- Potions are often not intended to be thrown. They are usually natural, -- including natural stat boosts. They also include the only healing -- consumables in the game, apart of elixirs and, to a limited extent, fruits. -- They appear deeper than most flasks. Various configurations of effects.@@ -547,7 +559,7 @@   , icount   = 3 `dL` 1  -- very useful, despite appearances   , iaspects = ELabel "of rose water"                : iaspects potionTemplate-  , ieffects = [ Impress, toOrganGood S_ROSE_SMELLING (80 + 1 `d` 20)+  , ieffects = [ Impress, toOrganGood S_ROSE_SMELLING (50 + 1 `d` 10)                , OnSmash ApplyPerfume, OnSmash (Explode S_FRAGRANCE) ]   } potion2 = potionTemplate@@ -615,7 +627,7 @@                                 , Explode S_DISTRESSING_ODOR                                 , Explode $ blastNoStatOf S_IMPATIENT                                 , Explode S_HASTE_SPRAY-                                , Explode S_SLOWNESS_MIST+                                , Explode S_VIOLENT_SLOWNESS_MIST                                 , Explode S_FRAGRANCE                                 , Explode S_VIOLENT_FLASH ]) ]   }@@ -685,8 +697,7 @@                , OnSmash (Explode $ blastNoStatOf S_IMMOBILE) ]   } potion13 = potionTemplate-  { ifreq    = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100)-               , (ANY_GLASS, 100) ]+  { ifreq    = [(COMMON_ITEM, 100), (ANY_POTION, 100), (ANY_GLASS, 100)]   , irarity  = [(10, 4)]   , iaspects = ELabel "of slow resistance"                : iaspects potionTemplate@@ -694,23 +705,21 @@                , OnSmash (Explode S_ANTI_SLOW_MIST) ]   } potion14 = potionTemplate-  { ifreq    = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100)-               , (ANY_GLASS, 100) ]+  { ifreq    = [(COMMON_ITEM, 100), (ANY_POTION, 100), (ANY_GLASS, 100)]   , irarity  = [(10, 4)]   , iaspects = ELabel "of poison resistance"                : iaspects potionTemplate   , ieffects = [ toOrganNoTimer S_POISON_RESISTANT                , OnSmash (Explode S_ANTIDOTE_MIST) ]   }--- The player has full control over throwing the flask at his party,+-- The player has full control over throwing the potion at his party, -- so he can milk the explosion, so it has to be much weaker, so a weak -- healing effect is enough. OTOH, throwing a harmful flask at many enemies -- at once is not easy to arrange, so these explosions can stay powerful. potion15 = potionTemplate-  { ifreq    = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100)-               , (ANY_GLASS, 100) ]+  { ifreq    = [(COMMON_ITEM, 100), (ANY_POTION, 100), (ANY_GLASS, 100)]   , irarity  = [(1, 2), (10, 12)]-  , iaspects = ELabel "of regeneration brew"+  , iaspects = ELabel "of regeneration"                : iaspects potionTemplate   , ieffects = [ toOrganGood S_ROSE_SMELLING (80 + 1 `d` 20)                , toOrganNoTimer S_REGENERATING@@ -769,7 +778,8 @@   , irarity  = [(10, 14)]   , ieffects = [ Impress                , OneOf [ Teleport 20, Ascend False, Ascend True-                       , Summon HERO 1, Summon MOBILE_ANIMAL $ 1 `d` 2+                       , OneOf [Summon HERO 1, Summon MOBILE_ANIMAL $ 1 `d` 2]+                           -- gaining a hero particularly uncommon                        , Detect DetectLoot 20  -- the most useful of detections                        , CreateItem Nothing CGround COMMON_ITEM timerNone ] ]   }@@ -909,9 +919,7 @@   , iverbHit = "scorch"   , iweight  = 1000   , idamage  = 0-  , iaspects = [ AddSkill SkShine 3, AddSkill SkSight (-2)-                   -- not only flashes, but also sparks,-                   -- so unused by AI due to the mixed blessing+  , iaspects = [ AddSkill SkShine 3  -- no malus, to lessen micromanagement                , SetFlag Lobable, SetFlag Equipable                , EqpSlot EqpSlotShine ]                    -- not Fragile; reusable flare@@ -929,13 +937,12 @@   , iverbHit = "burn"   , iweight  = 1500   , idamage  = 1 `d` 1-  , iaspects = [ AddSkill SkShine 3, AddSkill SkSight (-1)+  , iaspects = [ AddSkill SkShine 3                , SetFlag Lobable, SetFlag Fragile, SetFlag Equipable                , EqpSlot EqpSlotShine ]-  , ieffects = [ Burn 1-               , toOrganBad S_PACIFIED (2 + 1 `d` 2)-               , OnSmash (Explode S_BURNING_OIL_2) ]-  , idesc    = "A clay lamp filled with plant oil feeding a tiny wick."+  , ieffects = [ Explode S_FOCUSED_BURNING_OIL_2+               , OnSmash (Explode S_VIOLENT_BURNING_OIL_2) ]+  , idesc    = "A small clay lamp filled with plant oil feeding a tiny wick."   , ikit     = []   } light3 = ItemKind@@ -948,12 +955,11 @@   , iverbHit = "burn"   , iweight  = 3000   , idamage  = 2 `d` 1-  , iaspects = [ AddSkill SkShine 4, AddSkill SkSight (-1)+  , iaspects = [ AddSkill SkShine 4                , SetFlag Lobable, SetFlag Fragile, SetFlag Equipable                , EqpSlot EqpSlotShine ]-  , ieffects = [ Burn 1-               , toOrganBad S_PACIFIED (4 + 1 `d` 2)-               , OnSmash (Explode S_BURNING_OIL_4) ]+  , ieffects = [ Explode S_FOCUSED_BURNING_OIL_4+               , OnSmash (Explode S_VIOLENT_BURNING_OIL_4) ]   , idesc    = "Very bright and very heavy brass lantern."   , ikit     = []   }@@ -969,10 +975,10 @@   , iweight  = 1000   , idamage  = 0   , iaspects = [ AddSkill SkShine (-10)-               , AddSkill SkArmorMelee 2, AddSkill SkMaxCalm 5+               , AddSkill SkArmorMelee 3, AddSkill SkMaxCalm 5                , SetFlag Lobable, SetFlag Equipable                , EqpSlot EqpSlotArmorMelee ]-                  -- not Fragile; reusable douse implement;+                   -- not Fragile; reusable douse implement;                    -- douses torch, lamp and lantern in one action,                    -- both in equipment and when thrown at the floor   , ieffects = []@@ -982,30 +988,31 @@  -- ** Periodic jewelry --- Morally these are the aspects, but we also need to add a fake @Timeout@,--- to let clients know that the not identified item is periodic jewelry.-iaspects_necklaceTemplate :: [Aspect]-iaspects_necklaceTemplate =-  [ PresentAs NECKLACE_UNKNOWN-  , SetFlag Periodic, SetFlag Precious, SetFlag Equipable-  , toVelocity 50 ]  -- not dense enough+-- This looks like a necklace, but is not periodic. Instead, it auto-activates+-- when under melee attack. gorget = necklaceTemplate   { iname    = "Old Gorget"   , ifreq    = [(COMMON_ITEM, 25), (TREASURE, 25)]   , iflavour = zipFancy [BrCyan]  -- looks exactly the same as one of necklaces,                                   -- but it's OK, it's an artifact   , iaspects = [ SetFlag Unique-               , Timeout $ 5 - 1 `dL` 4+               , Timeout $ 7 - 1 `dL` 4                    -- the dL dice need to be in negative positions                    -- for negative stats, such as @Timeout@, so that                    -- the @RerollItem@ effect makes the item better, not worse-               , AddSkill SkArmorMelee 3, AddSkill SkArmorRanged 2-               , AddSkill SkHearing 3-               , SetFlag Durable ]-               ++ iaspects_necklaceTemplate-  , ieffects = [RefillCalm 1]+               , AddSkill SkArmorMelee 3, AddSkill SkHearing 3+               , SetFlag UnderMelee, SetFlag Durable ]+               ++ delete (SetFlag Periodic) iaspects_necklaceTemplate+  , ieffects = [RefillCalm 15]   , idesc    = "Highly ornamental, cold, large steel medallion on a chain. Unlikely to offer much protection as an armor piece, but the old worn engraving reassures the wearer."   }+-- Morally these are the aspects, but we also need to add a fake @Timeout@,+-- to let clients know that the not identified item is periodic jewelry.+iaspects_necklaceTemplate :: [Aspect]+iaspects_necklaceTemplate =+  [ PresentAs NECKLACE_UNKNOWN+  , SetFlag Periodic, SetFlag Precious, SetFlag Equipable+  , toVelocity 50 ]  -- not dense enough -- Not identified, because id by use, e.g., via periodic activations. Fun. necklaceTemplate = ItemKind   { isymbol  = symbolNecklace@@ -1091,12 +1098,12 @@ necklace6 = necklaceTemplate   { ifreq    = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)]   , iaspects = Timeout ((3 + 1 `d` 3 - 1 `dL` 3) * 2)-               : iaspects_necklaceTemplate+               : iaspects_necklaceTemplate  -- OP if Durable; free blink   , ieffects = [Teleport $ 3 `d` 2]   } necklace7 = necklaceTemplate   { ifreq    = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)]-  , iaspects = Timeout (1 + (1 `d` 3) * 2)+  , iaspects = Timeout ((1 `d` 3) * 2)                : iaspects_necklaceTemplate   , ieffects = [PushActor (ThrowMod 100 50 1)]  -- 1 step, slow                   -- the @50@ is only for the case of very light actor, etc.@@ -1119,14 +1126,17 @@ necklace9 = necklaceTemplate   { ifreq    = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)]   , irarity  = [(4, 3)]  -- entirely optional-  , iaspects = Timeout ((1 + 1 `d` 3) * 5)+  , iaspects = Timeout ((1 + 1 `d` 3) * 5)  -- low timeout for offensive use                : iaspects_necklaceTemplate   , ieffects = [Explode S_SPARK]   } necklace10 = necklaceTemplate   { ifreq    = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)]-  , iaspects = Timeout ((1 + 1 `d` 3) * 5)+  , iaspects = Timeout ((3 + 1 `d` 3) * 10)                : iaspects_necklaceTemplate+                   -- high timeout to prevent spam obscuring messages+                   -- when other actors act and annoying bumping into+                   -- projectiles caused by own necklace when walking   , ieffects = [Explode S_FRAGRANCE]   } motionScanner = necklaceTemplate@@ -1136,8 +1146,9 @@   , iverbHit = "jingle"   , iweight  = 300  -- almost gives it away   , iaspects = [ Timeout $ 4 + 1 `dL` 6+                   -- positive dL dice, since the periodic effect is detrimental                , AddSkill SkNocto 1-               , AddSkill SkArmorMelee (-20 + (1 `dL` 3) * 5)+               , AddSkill SkArmorMelee $ (-4 + 1 `dL` 3) * 5                , EqpSlot EqpSlotMiscBonus ]                ++ iaspects_necklaceTemplate   , ieffects = [Explode S_PING_PLASH]@@ -1156,8 +1167,8 @@   , iverbHit = "bang"   , iweight  = 500   , idamage  = 0-  , iaspects = [ AddSkill SkNocto 1, AddSkill SkSight (-1)-               , AddSkill SkArmorMelee $ (-1 + 1 `dL` 6) * 3+  , iaspects = [ AddSkill SkNocto 1+               , AddSkill SkArmorMelee $ (-6 + 1 `dL` 3) * 5                , SetFlag Precious, SetFlag Equipable                , EqpSlot EqpSlotMiscBonus ]   , ieffects = []@@ -1173,7 +1184,7 @@   , irarity  = [(7, 1), (10, 12)]  -- low @ifreq@   , iweight  = 50  -- heavier that it looks, due to glass   , iaspects = [ AddSkill SkSight $ 1 + 1 `dL` 2-               , AddSkill SkHurtMelee $ (-1 + 1 `d` 3) * 3+               , AddSkill SkHurtMelee $ (1 `d` 3) * 3                , EqpSlot EqpSlotSight ]                ++ iaspects ringTemplate   , idesc    = "Lets you better focus your weaker eye."@@ -1204,48 +1215,63 @@ ring1 = ringTemplate   { ifreq    = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)]   , irarity  = [(8, 4)]-  , iaspects = [ AddSkill SkSpeed $ 1 `dL` 3-               , AddSkill SkMaxHP (-10)+  , iaspects = [ AddSkill SkSpeed $ 1 `dL` 2+               , AddSkill SkMaxHP (-20)                , EqpSlot EqpSlotSpeed ]                ++ iaspects ringTemplate   } ring2 = ringTemplate+  { ifreq    = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)]+  , irarity  = [(8, 4)]+  , iaspects = [ AddSkill SkSpeed $ 1 + 1 `dL` 3+               , AddSkill SkArmorMelee (-40)+               , EqpSlot EqpSlotSpeed ]+               ++ iaspects ringTemplate+  }+ring3 = ringTemplate   { iname    = "the Ring"   , ifreq    = [(TREASURE, 100), (ANY_JEWELRY, 100)]   , iaspects = [ SetFlag Unique, ELabel "of Rush"                , AddSkill SkSpeed $ (1 + 1 `dL` 2) * 2                , AddSkill SkMaxHP (-20)-               , AddSkill SkMaxCalm (-40)+               , AddSkill SkArmorMelee (-20)                , SetFlag Durable, EqpSlot EqpSlotSpeed ]                ++ iaspects ringTemplate   , idesc    = "Roughly-shaped metal with shallow scratches marking it."   }-ring3 = ringTemplate+ring4 = ringTemplate   { ifreq    = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)]   , irarity  = [(3, 4), (10, 8)]-  , iaspects = [ AddSkill SkHurtMelee $ (2 + 1 `d` 2 + (1 `dL` 2) * 2 ) * 3-               , AddSkill SkMaxHP $ (-3 + 1 `dL` 3) * 10+  , iaspects = [ AddSkill SkHurtMelee $ (2 + 1 `d` 3 + (1 `dL` 2) * 2 ) * 3+               , AddSkill SkMaxHP (-10)                , EqpSlot EqpSlotHurtMelee ]                ++ iaspects ringTemplate   }-ring4 = ringTemplate+ring5 = ringTemplate   { ifreq    = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)]+  , iaspects = [ AddSkill SkHurtMelee $ (4 + 1 `d` 3 + (1 `dL` 2) * 2 ) * 3+               , AddSkill SkArmorMelee (-20)+               , EqpSlot EqpSlotHurtMelee ]+               ++ iaspects ringTemplate+  }+ring6 = ringTemplate+  { ifreq    = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)]   , irarity  = [(10, 8)]   , iaspects = [ AddSkill SkMaxHP $ 5 + (1 `d` 2 + 1 `dL` 2) * 5                , AddSkill SkMaxCalm $ -30 + (1 `dL` 3) * 5                , EqpSlot EqpSlotMaxHP ]                ++ iaspects ringTemplate   }-ring5 = ringTemplate+ring7 = ringTemplate   { ifreq    = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)]-  , irarity  = [(5, 1), (10, 9)]  -- needed after other rings drop Calm-  , iaspects = [ AddSkill SkMaxCalm $ 20 + (1 `dL` 4) * 5+  , irarity  = [(5, 1), (10, 9)]  -- needed after other items drop Calm+  , iaspects = [ AddSkill SkMaxCalm $ 30 + (1 `dL` 4) * 5                , AddSkill SkHearing 6                , EqpSlot EqpSlotMiscBonus ]                ++ iaspects ringTemplate   , idesc    = "Cold, solid to the touch, perfectly round, engraved with solemn, strangely comforting, worn out words."   }-ring6 = ringTemplate  -- weak skill per eqp slot, so can be without drawbacks+ring8 = ringTemplate  -- weak skill per eqp slot, so can be without drawbacks   { ifreq    = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)]   , irarity  = [(10, 3)]   , iaspects = [ AddSkill SkShine 1@@ -1253,7 +1279,7 @@                ++ iaspects ringTemplate   , idesc    = "A sturdy ring with a large, shining stone."   }-ring7 = ringTemplate+ring9 = ringTemplate   { ifreq    = [(RING_OF_OPPORTUNITY_SNIPER, 1) ]  -- only for scenarios   , irarity  = [(1, 1)]   , iaspects = [ ELabel "of opportunity sniper"@@ -1261,7 +1287,7 @@                , EqpSlot EqpSlotProject ]                ++ iaspects ringTemplate   }-ring8 = ringTemplate+ring10 = ringTemplate   { ifreq    = [(RING_OF_OPPORTUNITY_GRENADIER, 1) ]  -- only for scenarios   , irarity  = [(1, 1)]   , iaspects = [ ELabel "of opportunity grenadier"@@ -1350,6 +1376,7 @@   , ifreq    = [(COMMON_ITEM, 100), (ARMOR_MISC, 1), (STARTING_ARMOR, 50)]   , iflavour = zipPlain [BrCyan]   , irarity  = [(1, 9), (10, 3)]+  , iverbHit = "mow"   , iweight  = 300   , idamage  = 2 `d` 1   , iaspects = [ AddSkill SkArmorMelee $ (1 + 1 `dL` 4) * 5@@ -1363,7 +1390,7 @@   , ifreq    = [(COMMON_ITEM, 100), (ARMOR_MISC, 1)]   , iflavour = zipFancy [BrRed]   , irarity  = [(1, 3), (10, 3)]-  , iverbHit = "rasp"+  , iverbHit = "ram"   , iweight  = 3000   , idamage  = 3 `d` 1   , iaspects = [ SetFlag Unique@@ -1389,7 +1416,7 @@   , iweight  = 500   , idamage  = 0   , iaspects = [ Timeout $ (2 + 1 `d` 2) * 3-               , AddSkill SkArmorMelee 5, AddSkill SkHearing (-10)+               , AddSkill SkArmorMelee 5, AddSkill SkHearing (-6)                , SetFlag Periodic, SetFlag Durable, SetFlag Equipable                , EqpSlot EqpSlotArmorMelee                , toVelocity 50 ]  -- flaps and flutters@@ -1428,8 +1455,8 @@   , idamage  = 0   , iaspects = [ AddSkill SkArmorMelee $ (1 + 1 `dL` 4) * 5                , AddSkill SkArmorRanged $ (2 + 1 `dL` 2) * 3  -- headshot-               , AddSkill SkHearing (-7), AddSkill SkSight (-1)-               , AddSkill SkSmell (-5)+               , AddSkill SkSight (-2)+               , AddSkill SkHearing (-3), AddSkill SkSmell (-5)                , SetFlag Durable, SetFlag Equipable                , EqpSlot EqpSlotArmorRanged ]   , ieffects = []@@ -1475,7 +1502,7 @@   , idamage  = 0  -- safe to be used on self   , iaspects = [ Timeout $ (3 + 1 `d` 3 - 1 `dL` 3) * 2                , AddSkill SkArmorMelee 40-                   -- not enough to compensate; won't be in eqp+                   -- not enough to compensate; won't be in AI's eqp                , AddSkill SkHurtMelee (-30)                    -- too harmful; won't be wielded as weapon                , SetFlag Durable, SetFlag Meleeable@@ -1492,7 +1519,7 @@   , idamage  = 4 `d` 1   , iaspects = [ Timeout $ (3 + 1 `d` 3 - 1 `dL` 3) * 4                , AddSkill SkArmorMelee 80-                   -- not enough to compensate; won't be in eqp+                   -- not enough to compensate; won't be in AI's eqp                , AddSkill SkHurtMelee (-70)                    -- too harmful; won't be wielded as weapon                , SetFlag Durable, SetFlag Meleeable@@ -1536,7 +1563,7 @@   , idesc    = "A short dagger for thrusting and parrying blows. Does not penetrate deeply, but is quick to move and hard to block. Especially useful in conjunction with a larger weapon."   , ikit     = []   }-daggerDropBestWeapon = knife+daggerDischarge = knife   { iname    = "The Double Dagger"   , ifreq    = [(TREASURE, 20)]   , irarity  = [(1, 3), (10, 3)]@@ -1552,7 +1579,7 @@   , ifreq    = [(HAMMER_UNKNOWN, 1)]   , iflavour = zipFancy [BrMagenta]  -- avoid "pink"   , icount   = 1-  , irarity  = [(5, 20), (8, 1)]+  , irarity  = [(3, 25), (5, 1)]   , iverbHit = "club"   , iweight  = 1600   , idamage  = 8 `d` 1  -- we are lying about the dice here, but the dungeon@@ -1620,7 +1647,7 @@   , ifreq    = [(COMMON_ITEM, 100), (STARTING_WEAPON, 30)]   , iflavour = zipPlain [BrBlue]   , icount   = 1-  , irarity  = [(4, 1), (6, 20)]+  , irarity  = [(4, 1), (6, 15)]   , iverbHit = "slash"   , iweight  = 2000   , idamage  = 10 `d` 1@@ -1659,7 +1686,7 @@   , ifreq    = [(COMMON_ITEM, 100), (STARTING_WEAPON, 20)]   , iflavour = zipPlain [BrYellow]   , icount   = 1-  , irarity  = [(5, 1), (8, 15)]+  , irarity  = [(5, 1), (8, 12)]   , iverbHit = "impale"   , iweight  = 3000   , idamage  = 12 `d` 1@@ -1784,7 +1811,7 @@   { isymbol  = symbolWand   , iname    = "jumping pole"   , ifreq    = [(COMMON_ITEM, 100)]-  , iflavour = zipPlain [White]+  , iflavour = zipFancy [White]   , icount   = 1   , irarity  = [(1, 3)]   , iverbHit = "prod"
GameDefinition/Content/ItemKindActor.hs view
@@ -1,7 +1,7 @@ -- | Actor (or rather actor body trunk) definitions. module Content.ItemKindActor   ( -- * Group name patterns-    pattern S_WOODEN_TORCH+    pattern S_WOODEN_TORCH, pattern S_SANDSTONE_ROCK   , pattern HERO, pattern SCOUT_HERO, pattern RANGER_HERO, pattern ESCAPIST_HERO, pattern AMBUSHER_HERO, pattern BRAWLER_HERO, pattern SOLDIER_HERO, pattern CIVILIAN, pattern MONSTER, pattern MOBILE_MONSTER, pattern SCOUT_MONSTER, pattern ANIMAL, pattern MOBILE_ANIMAL, pattern IMMOBILE_ANIMAL   , pattern ADD_SIGHT, pattern ARMOR_RANGED, pattern ADD_NOCTO_1, pattern WEAK_ARROW, pattern LIGHT_ATTENUATOR, pattern FIREPROOF_CLOTH, pattern RING_OF_OPPORTUNITY_SNIPER, pattern ANY_ARROW, pattern STARTING_ARMOR, pattern STARTING_WEAPON, pattern GEM   , actorsGN, actorsGNSingleton@@ -18,15 +18,16 @@ import Game.LambdaHack.Definition.Ability import Game.LambdaHack.Definition.Color import Game.LambdaHack.Definition.Defs+import Game.LambdaHack.Definition.DefsInternal import Game.LambdaHack.Definition.Flavour  -- * Group name patterns  actorsGNSingleton :: [GroupName ItemKind] actorsGNSingleton =-       [S_WOODEN_TORCH]+       [S_WOODEN_TORCH, S_SANDSTONE_ROCK] -pattern S_WOODEN_TORCH :: GroupName ItemKind+pattern S_WOODEN_TORCH, S_SANDSTONE_ROCK :: GroupName ItemKind  actorsGN :: [GroupName ItemKind] actorsGN =@@ -53,6 +54,7 @@ pattern IMMOBILE_ANIMAL = GroupName "immobile animal"  pattern S_WOODEN_TORCH = GroupName "wooden torch"+pattern S_SANDSTONE_ROCK = GroupName "sandstone rock"  pattern ADD_SIGHT = GroupName "sight improvement" pattern ARMOR_RANGED = GroupName "ranged armor"@@ -91,7 +93,7 @@               , (S_EYE_6, COrgan), (S_EAR_3, COrgan)               , (S_SAPIENT_BRAIN, COrgan) ] warrior = ItemKind-  { isymbol  = '@'+  { isymbol  = toContentSymbol '@'   , iname    = "warrior"  -- modified if initial actors in hero faction   , ifreq    = [(HERO, 100), (MOBILE, 1)]   , iflavour = zipPlain [BrWhite]@@ -112,9 +114,12 @@   , ieffects = []   , idesc    = ""  -- "A hardened veteran of combat."   , ikit     = humanOrgans+               ++ [(S_SANDSTONE_ROCK, CStash)]   } warrior2 = warrior   { iname    = "adventurer"+  , ikit     = humanOrgans+               ++ [(COMMON_ITEM, CStash)]   -- , idesc    = ""   } warrior3 = warrior@@ -164,7 +169,7 @@   , ifreq    = [(AMBUSHER_HERO, 100), (MOBILE, 1)]   , ikit     = humanOrgans  -- dark and numerous, so more kit without exploring                ++ [ (RING_OF_OPPORTUNITY_SNIPER, CEqp)-                  , (ANY_ARROW, CStash)+                  , (ANY_ARROW, CStash), (ANY_ARROW, CStash)                   , (WEAK_ARROW, CStash)                   , (EXPLOSIVE, CStash)                   , (LIGHT_ATTENUATOR, CEqp)@@ -214,7 +219,7 @@ -- They have bright colours, because they are not natural.  eye = ItemKind-  { isymbol  = 'e'+  { isymbol  = toContentSymbol 'e'   , iname    = "reducible eye"   , ifreq    = [ (MONSTER, 100), (MOBILE, 1)                , (MOBILE_MONSTER, 100), (SCOUT_MONSTER, 10) ]@@ -228,7 +233,7 @@                , AddSkill SkSpeed 20, AddSkill SkNocto 2                , AddSkill SkAggression 1                , AddSkill SkProject 2  -- can lob-               , AddSkill SkApply 1  -- can even use cultural artifacts+               , AddSkill SkApply 1  -- can use even cultural artifacts                , SetFlag Durable ]   , ieffects = []   , idesc    = "Under your stare, it reduces to the bits that define its essence. Under introspection, the bits slow down and solidify into an arbitrary form again. It must be huge inside, for holographic principle to manifest so overtly."  -- holographic principle is an anachronism for XIX or most of XX century, but "the cosmological scale effects" is too weak@@ -236,7 +241,7 @@                , (S_SAPIENT_BRAIN, COrgan) ]  -- no hearing, it's all eyes   } fastEye = ItemKind-  { isymbol  = 'j'+  { isymbol  = toContentSymbol 'j'   , iname    = "injective jaw"   , ifreq    = [ (MONSTER, 100), (MOBILE, 1)                , (MOBILE_MONSTER, 100), (SCOUT_MONSTER, 60) ]@@ -252,13 +257,13 @@                , SetFlag Durable ]   , ieffects = []   , idesc    = "Hungers but never eats. Bites but never swallows. Burrows its own image through, but never carries anything back."  -- rather weak: not about injective objects, but puny, concrete, injective functions  --- where's the madness in that?-  , ikit     = [ (S_TOOTH, COrgan), (S_LIP, COrgan)+  , ikit     = [ (S_TOOTH, COrgan), (S_LIP, COrgan)  -- at least one non-timed                , (S_SPEED_GLAND_10, COrgan)                , (S_VISION_6, COrgan), (S_EAR_3, COrgan)                , (S_SAPIENT_BRAIN, COrgan) ]   } nose = ItemKind  -- depends solely on smell-  { isymbol  = 'n'+  { isymbol  = toContentSymbol 'n'   , iname    = "point-free nose"   , ifreq    = [(MONSTER, 100), (MOBILE, 1), (MOBILE_MONSTER, 100)]   , iflavour = zipFancy [BrGreen]@@ -274,12 +279,12 @@                , SetFlag Durable ]   , ieffects = []   , idesc    = "No mouth, yet it devours everything around, constantly sniffing itself inward; pure movement structure, no constant point to focus one's maddened gaze on."-  , ikit     = [ (S_TIP, COrgan), (S_LIP, COrgan)+  , ikit     = [ (S_TIP, COrgan), (S_LIP, COrgan)  -- at least one non-timed                , (S_NOSTRIL, COrgan)                , (S_SAPIENT_BRAIN, COrgan) ]  -- no sight nor hearing   } elbow = ItemKind-  { isymbol  = 'e'+  { isymbol  = toContentSymbol 'e'   , iname    = "commutative elbow"   , ifreq    = [ (MONSTER, 100), (MOBILE, 1)                , (MOBILE_MONSTER, 100), (SCOUT_MONSTER, 30) ]@@ -305,7 +310,7 @@                , (WEAK_ARROW, CStash), (WEAK_ARROW, CStash) ]   } torsor = ItemKind-  { isymbol  = 'T'+  { isymbol  = toContentSymbol 'T'   , iname    = "The Forgetful Torsor"   , ifreq    = [(MONSTER, 100), (MOBILE, 1)]   , iflavour = zipFancy [BrCyan]@@ -326,7 +331,8 @@   , ieffects = []   , idesc    = "A principal homogeneous manifold, that acts freely and with enormous force, but whose stabilizers are trivial, making it rather helpless without a support group."   , ikit     = [ (S_RIGHT_TORSION, COrgan), (S_LEFT_TORSION, COrgan)-               , (S_PUPIL, COrgan), (S_TENTACLE, COrgan)+               , (S_PUPIL, COrgan)+               , (S_TENTACLE, COrgan)  -- low timeout, so rarely a stall                , (S_EAR_8, COrgan)                , (S_SAPIENT_BRAIN, COrgan)                , (GEM, CStash), (GEM, CStash)@@ -342,7 +348,7 @@ -- They have dull colors, except for yellow, because there is no dull variant.  goldenJackal = ItemKind  -- basically a much smaller, slower and nosy hyena-  { isymbol  = 'j'+  { isymbol  = toContentSymbol 'j'   , iname    = "golden jackal"   , ifreq    = [ (ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100)                , (SCAVENGER, 50) ]@@ -354,6 +360,8 @@   , idamage  = 0   , iaspects = [ AddSkill SkMaxHP 15, AddSkill SkMaxCalm 70                , AddSkill SkSpeed 24, AddSkill SkNocto 2+               , AddSkill SkAggression 2  -- scout+               , AddSkill SkDisplace 1  -- scout                , SetFlag Durable ]   , ieffects = []   , idesc    = "An opportunistic predator, feeding on carrion and the weak."@@ -362,7 +370,7 @@                , (S_ANIMAL_BRAIN, COrgan) ]   } griffonVulture = ItemKind  -- keep it boring and weak, because it summons-  { isymbol  = 'v'+  { isymbol  = toContentSymbol 'v'   , iname    = "griffon vulture"   , ifreq    = [ (ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100)                , (SCAVENGER, 30) ]@@ -376,7 +384,7 @@                    -- enough Calm to summon twice only if not attacked at all;                    -- loses a lot of sight after summoning                , AddSkill SkSpeed 22, AddSkill SkNocto 2-               , AddSkill SkAlter (-2)  -- can't use normal stairs nor doors+               , AddSkill SkAlter (-2)  -- can't use hard stairs nor doors                , AddSkill SkFlying 10  -- flies slowly, but far                , SetFlag Durable ]       -- Animals don't have leader, usually, so even if only one on level,@@ -391,7 +399,7 @@                , (S_ANIMAL_BRAIN, COrgan) ]   } skunk = ItemKind-  { isymbol  = 's'+  { isymbol  = toContentSymbol 's'   , iname    = "hog-nosed skunk"   , ifreq    = [(ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100)]   , iflavour = zipPlain [White]@@ -402,7 +410,7 @@   , idamage  = 0   , iaspects = [ AddSkill SkMaxHP 13, AddSkill SkMaxCalm 30                , AddSkill SkSpeed 22, AddSkill SkNocto 2-               , AddSkill SkAlter (-2)  -- can't use stairs nor doors+               , AddSkill SkAlter (-2)  -- can't use hard stairs nor doors                , AddSkill SkOdor 5  -- and no smell skill, to let it leave smell                , SetFlag Durable ]   , ieffects = []@@ -413,18 +421,19 @@                , (S_ANIMAL_BRAIN, COrgan) ]   } armadillo = ItemKind-  { isymbol  = 'a'+  { isymbol  = toContentSymbol 'a'   , iname    = "giant armadillo"   , ifreq    = [(ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100)]   , iflavour = zipPlain [Brown]   , icount   = 1   , irarity  = [(1, 7)]   , iverbHit = "thud"-  , iweight  = 80000+  , iweight  = 54000   , idamage  = 0-  , iaspects = [ AddSkill SkMaxHP 13, AddSkill SkMaxCalm 30+  , iaspects = [ AddSkill SkMaxHP 30, AddSkill SkMaxCalm 30                , AddSkill SkSpeed 20, AddSkill SkNocto 2-               , AddSkill SkAlter (-2)  -- can't use normal stairs nor doors+               , AddSkill SkHurtMelee (-70)  -- quite harmless rolled in a ball+               , AddSkill SkAlter (-2)  -- can't use hard stairs nor doors                , SetFlag Durable ]   , ieffects = []   , idesc    = "When threatened, it rolls into a ball."@@ -434,7 +443,7 @@                , (S_ANIMAL_BRAIN, COrgan) ]   } gilaMonster = ItemKind-  { isymbol  = 'g'+  { isymbol  = toContentSymbol 'g'   , iname    = "Gila monster"   , ifreq    = [(ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100)]   , iflavour = zipPlain [Magenta]@@ -445,7 +454,7 @@   , idamage  = 0   , iaspects = [ AddSkill SkMaxHP 15, AddSkill SkMaxCalm 50                , AddSkill SkSpeed 18, AddSkill SkNocto 2-               , AddSkill SkAlter (-2)  -- can't use normal stairs nor doors+               , AddSkill SkAlter (-2)  -- can't use hard stairs nor doors                , SetFlag Durable ]   , ieffects = []   , idesc    = "Numbing venom ensures that even the fastest prey has no escape."@@ -454,19 +463,19 @@                , (S_ANIMAL_BRAIN, COrgan) ]   } rattlesnake = ItemKind-  { isymbol  = 's'+  { isymbol  = toContentSymbol 's'   , iname    = "rattlesnake"   , ifreq    = [(ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100)]   , iflavour = zipPlain [Brown]   , icount   = 1-  , irarity  = [(5, 1), (10, 7)]+  , irarity  = [(5, 1), (10, 7), (20, 10)]  -- common among late spawns   , iverbHit = "thud"   , iweight  = 80000   , idamage  = 0   , iaspects = [ AddSkill SkMaxHP 28, AddSkill SkMaxCalm 60                , AddSkill SkSpeed 16, AddSkill SkNocto 2                , AddSkill SkAggression 2  -- often discharged. so flees anyway-               , AddSkill SkAlter (-2)  -- can't use normal stairs nor doors+               , AddSkill SkAlter (-2)  -- can't use hard stairs nor doors                , SetFlag Durable ]   , ieffects = []   , idesc    = "Beware its rattle - it serves as a warning of an agonising death."@@ -476,13 +485,14 @@                , (S_ANIMAL_BRAIN, COrgan) ]   } hyena = ItemKind-  { isymbol  = 'h'+  { isymbol  = toContentSymbol 'h'   , iname    = "spotted hyena"   , ifreq    = [ (ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100)                , (SCAVENGER, 20) ]   , iflavour = zipPlain [BrYellow]   , icount   = 1-  , irarity  = [(4, 1), (10, 5)]  -- gets summoned often, so low base rarity+  , irarity  = [(4, 1), (10, 5), (20, 10)]+      -- gets summoned often, so low base rarity, except among late spawns   , iverbHit = "thud"   , iweight  = 60000   , idamage  = 0@@ -491,39 +501,41 @@                , SetFlag Durable ]   , ieffects = []   , idesc    = "Skulking in the shadows, waiting for easy prey."-  , ikit     = [ (S_JAW, COrgan)+  , ikit     = [ (S_JAW, COrgan), (S_SMALL_CLAW, COrgan)                , (S_EYE_6, COrgan), (S_NOSTRIL, COrgan), (S_EAR_8, COrgan)                , (S_ANIMAL_BRAIN, COrgan) ]   } komodoDragon = ItemKind-  { isymbol  = 'k'+  { isymbol  = toContentSymbol 'k'   , iname    = "Komodo dragon"   , ifreq    = [(ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100)]   , iflavour = zipPlain [BrRed]  -- speedy, so bright red   , icount   = 1-  , irarity  = [(9, 0), (10, 11)]+  , irarity  = [(9, 0), (10, 11), (20, 20)]   , iverbHit = "thud"   , iweight  = 80000   , idamage  = 0   , iaspects = [ AddSkill SkMaxHP 40, AddSkill SkMaxCalm 60  -- regens                , AddSkill SkSpeed 17, AddSkill SkNocto 2+               , AddSkill SkHurtMelee 60  -- great fighter with low cooldowns                , AddSkill SkAggression 1  -- match the description                , SetFlag Durable ]   , ieffects = []   , idesc    = "Larger and more aggressive than any other lizard, but as easily recovering from wounds as its lesser cousins."   , ikit     = [ (S_LARGE_TAIL, COrgan), (S_JAW, COrgan)-               , (S_HOOKED_CLAW, COrgan)+               , (S_LIP, COrgan), (S_FOOT, COrgan)                , (S_SPEED_GLAND_5, COrgan), (S_ARMORED_SKIN, COrgan)                , (S_EYE_3, COrgan), (S_NOSTRIL, COrgan), (S_EAR_3, COrgan)                , (S_ANIMAL_BRAIN, COrgan) ]   }-alligator = ItemKind-  { isymbol  = 'a'+alligator = ItemKind  -- late, slow, deadly semi-tank with some armor;+                      -- too deadly to get more HP; bombs the only recourse+  { isymbol  = toContentSymbol 'a'   , iname    = "alligator"   , ifreq    = [(ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100)]   , iflavour = zipPlain [Blue]   , icount   = 1-  , irarity  = [(9, 0), (10, 12)]+  , irarity  = [(9, 0), (10, 12), (20, 10), (40, 40)]   , iverbHit = "thud"   , iweight  = 80000   , idamage  = 0@@ -541,12 +553,12 @@                , (S_ANIMAL_BRAIN, COrgan) ]   } rhinoceros = ItemKind-  { isymbol  = 'R'+  { isymbol  = toContentSymbol 'R'   , iname    = "The Maddened Rhinoceros"   , ifreq    = [(ANIMAL, 100), (MOBILE, 1)]   , iflavour = zipPlain [Brown]   , icount   = 1-  , irarity  = [(2, 0), (3, 1000), (4, 0)]  -- unique+  , irarity  = [(2, 0), (3, 1000), (4, 0)]  -- an early unique   , iverbHit = "thud"   , iweight  = 80000   , idamage  = 0@@ -554,7 +566,7 @@                , AddSkill SkMaxHP 90, AddSkill SkMaxCalm 60                , AddSkill SkSpeed 27, AddSkill SkNocto 2                , AddSkill SkAggression 2-               , AddSkill SkAlter (-1)  -- can't use normal stairs nor dig;+               , AddSkill SkAlter (-1)  -- can't use hard stairs nor dig;                                         -- a weak miniboss                , SetFlag Durable ]   , ieffects = []@@ -568,7 +580,7 @@ -- * Non-animal animals  beeSwarm = ItemKind-  { isymbol  = 'b'+  { isymbol  = toContentSymbol 'b'   , iname    = "bee swarm"   , ifreq    = [(ANIMAL, 100), (MOBILE, 1)]   , iflavour = zipPlain [Brown]@@ -579,7 +591,7 @@   , idamage  = 0   , iaspects = [ AddSkill SkMaxHP 10, AddSkill SkMaxCalm 60                , AddSkill SkSpeed 30, AddSkill SkNocto 2  -- armor in sting-               , AddSkill SkAlter (-2)  -- can't use normal stairs nor doors+               , AddSkill SkAlter (-2)  -- can't use hard stairs nor doors                , AddSkill SkWait (-2)  -- can't brace, sleep and lurk                , AddSkill SkFlying 10  -- flies slowly, but far                , SetFlag Durable ]@@ -589,20 +601,22 @@                , (S_VISION_6, COrgan), (S_EAR_6, COrgan)                , (S_INSECT_MORTALITY, COrgan), (S_ANIMAL_BRAIN, COrgan) ]   }-hornetSwarm = ItemKind-  { isymbol  = 'h'+hornetSwarm = ItemKind  -- kind of tank with armor, but short-lived+  { isymbol  = toContentSymbol 'h'   , iname    = "hornet swarm"   , ifreq    = [(ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100)]   , iflavour = zipPlain [Magenta]   , icount   = 1-  , irarity  = [(5, 1), (10, 4)]  -- should be many, because die after a time+  , irarity  = [(5, 1), (10, 4), (20, 10)]+      -- should be many, because die after a time   , iverbHit = "buzz"   , iweight  = 1000   , idamage  = 0   , iaspects = [ AddSkill SkArmorMelee 80, AddSkill SkArmorRanged 40-               , AddSkill SkMaxHP 8, AddSkill SkMaxCalm 70+               , AddSkill SkHurtMelee 50+               , AddSkill SkMaxHP 10, AddSkill SkMaxCalm 70                , AddSkill SkSpeed 30, AddSkill SkNocto 2-               , AddSkill SkAlter (-2)  -- can't use normal stairs nor doors+               , AddSkill SkAlter (-2)  -- can't use hard stairs nor doors                , AddSkill SkWait (-2)  -- can't brace, sleep and lurk                , AddSkill SkFlying 10  -- flies slowly, but far                , SetFlag Durable ]@@ -612,8 +626,8 @@                , (S_VISION_6, COrgan), (S_EAR_6, COrgan)                , (S_INSECT_MORTALITY, COrgan), (S_ANIMAL_BRAIN, COrgan) ]   }-thornbush = ItemKind-  { isymbol  = 't'+thornbush = ItemKind  -- the wimpiest kind of early tank+  { isymbol  = toContentSymbol 't'   , iname    = "thornbush"   , ifreq    = [(ANIMAL, 20), (IMMOBILE_ANIMAL, 20)]   , iflavour = zipPlain [Brown]@@ -622,7 +636,7 @@   , iverbHit = "scrape"   , iweight  = 80000   , idamage  = 0-  , iaspects = [ AddSkill SkMaxHP 20, AddSkill SkMaxCalm 999+  , iaspects = [ AddSkill SkMaxHP 30, AddSkill SkMaxCalm 999                , AddSkill SkSpeed 22, AddSkill SkNocto 2                , AddSkill SkWait 1, AddSkill SkMelee 1  -- no brain                , SetFlag Durable ]@@ -632,7 +646,7 @@                , (S_BARK, COrgan) ]   } geyserBoiling = ItemKind-  { isymbol  = 'g'+  { isymbol  = toContentSymbol 'g'   , iname    = "geyser"   , ifreq    = [(ANIMAL, 8), (IMMOBILE_ANIMAL, 30)]   , iflavour = zipPlain [Blue]@@ -650,7 +664,7 @@   , ikit     = [(S_BOILING_VENT, COrgan), (S_BOILING_FISSURE, COrgan)]   } geyserArsenic = ItemKind-  { isymbol  = 'g'+  { isymbol  = toContentSymbol 'g'   , iname    = "arsenic geyser"   , ifreq    = [(ANIMAL, 8), (IMMOBILE_ANIMAL, 40)]   , iflavour = zipPlain [Cyan]@@ -668,7 +682,7 @@   , ikit     = [(S_ARSENIC_VENT, COrgan), (S_ARSENIC_FISSURE, COrgan)]   } geyserSulfur = ItemKind-  { isymbol  = 'g'+  { isymbol  = toContentSymbol 'g'   , iname    = "sulfur geyser"   , ifreq    = [(ANIMAL, 8), (IMMOBILE_ANIMAL, 120)]   , iflavour = zipPlain [BrYellow]  -- exception, animal with bright color
GameDefinition/Content/ItemKindBlast.hs view
@@ -1,7 +1,7 @@ -- | Blast definitions. module Content.ItemKindBlast   ( -- * Group name patterns-    pattern S_FIRECRACKER, pattern S_VIOLENT_FRAGMENTATION, pattern S_FRAGMENTATION, pattern S_FOCUSED_FRAGMENTATION, pattern S_VIOLENT_CONCUSSION, pattern S_CONCUSSION, pattern S_FOCUSED_CONCUSSION, pattern S_VIOLENT_FLASH, pattern S_FOCUSED_FLASH, pattern S_GLASS_HAIL, pattern S_FOCUSED_GLASS_HAIL, pattern S_PHEROMONE, pattern S_CALMING_MIST, pattern S_DISTRESSING_ODOR, pattern S_HEALING_MIST, pattern S_HEALING_MIST_2, pattern S_WOUNDING_MIST, pattern S_DISTORTION, pattern S_SMOKE, pattern S_BOILING_WATER, pattern S_GLUE, pattern S_WASTE, pattern S_ANTI_SLOW_MIST, pattern S_ANTIDOTE_MIST, pattern S_SLEEP_MIST, pattern S_DENSE_SHOWER, pattern S_SPARSE_SHOWER, pattern S_MELEE_PROTECTIVE_BALM, pattern S_RANGE_PROTECTIVE_BALM, pattern S_DEFENSELESSNESS_RUNOUT, pattern S_RESOLUTION_DUST, pattern S_HASTE_SPRAY, pattern S_SLOWNESS_MIST, pattern S_EYE_DROP, pattern S_IRON_FILING, pattern S_SMELLY_DROPLET, pattern S_EYE_SHINE, pattern S_WHISKEY_SPRAY, pattern S_YOUTH_SPRINKLE, pattern S_POISON_CLOUD, pattern S_PING_PLASH, pattern S_VIOLENT_BURNING_OIL_2, pattern S_VIOLENT_BURNING_OIL_3, pattern S_VIOLENT_BURNING_OIL_4, pattern S_BURNING_OIL_2, pattern S_BURNING_OIL_3, pattern S_BURNING_OIL_4, pattern S_FOCUSED_BURNING_OIL_2, pattern S_FOCUSED_BURNING_OIL_3, pattern S_FOCUSED_BURNING_OIL_4+    pattern S_FIRECRACKER, pattern S_VIOLENT_FRAGMENTATION, pattern S_FRAGMENTATION, pattern S_FOCUSED_FRAGMENTATION, pattern S_VIOLENT_CONCUSSION, pattern S_CONCUSSION, pattern S_FOCUSED_CONCUSSION, pattern S_VIOLENT_FLASH, pattern S_FOCUSED_FLASH, pattern S_GLASS_HAIL, pattern S_FOCUSED_GLASS_HAIL, pattern S_PHEROMONE, pattern S_CALMING_MIST, pattern S_DISTRESSING_ODOR, pattern S_HEALING_MIST, pattern S_HEALING_MIST_2, pattern S_WOUNDING_MIST, pattern S_DISTORTION, pattern S_SMOKE, pattern S_BOILING_WATER, pattern S_GLUE, pattern S_WASTE, pattern S_ANTI_SLOW_MIST, pattern S_ANTIDOTE_MIST, pattern S_SLEEP_MIST, pattern S_DENSE_SHOWER, pattern S_SPARSE_SHOWER, pattern S_MELEE_PROTECTIVE_BALM, pattern S_RANGE_PROTECTIVE_BALM, pattern S_DEFENSELESSNESS_RUNOUT, pattern S_RESOLUTION_DUST, pattern S_HASTE_SPRAY, pattern S_VIOLENT_SLOWNESS_MIST, pattern S_SLOWNESS_MIST, pattern S_FOCUSED_SLOWNESS_MIST, pattern S_EYE_DROP, pattern S_IRON_FILING, pattern S_SMELLY_DROPLET, pattern S_EYE_SHINE, pattern S_WHISKEY_SPRAY, pattern S_YOUTH_SPRINKLE, pattern S_POISON_CLOUD, pattern S_PING_PLASH, pattern S_VIOLENT_BURNING_OIL_2, pattern S_VIOLENT_BURNING_OIL_3, pattern S_VIOLENT_BURNING_OIL_4, pattern S_BURNING_OIL_2, pattern S_BURNING_OIL_3, pattern S_BURNING_OIL_4, pattern S_FOCUSED_BURNING_OIL_2, pattern S_FOCUSED_BURNING_OIL_3, pattern S_FOCUSED_BURNING_OIL_4   , blastNoStatOf, blastBonusStatOf   , pattern ARMOR_MISC   , blastsGNSingleton, blastsGN@@ -19,18 +19,19 @@ import Game.LambdaHack.Definition.Ability import Game.LambdaHack.Definition.Color import Game.LambdaHack.Definition.Defs+import Game.LambdaHack.Definition.DefsInternal import Game.LambdaHack.Definition.Flavour  -- * Group name patterns  blastsGNSingleton :: [GroupName ItemKind] blastsGNSingleton =-       [S_FIRECRACKER, S_VIOLENT_FRAGMENTATION, S_FRAGMENTATION, S_FOCUSED_FRAGMENTATION, S_VIOLENT_CONCUSSION, S_CONCUSSION, S_FOCUSED_CONCUSSION, S_VIOLENT_FLASH, S_FOCUSED_FLASH, S_GLASS_HAIL, S_FOCUSED_GLASS_HAIL, S_PHEROMONE, S_CALMING_MIST, S_DISTRESSING_ODOR, S_HEALING_MIST, S_HEALING_MIST_2, S_WOUNDING_MIST, S_DISTORTION, S_SMOKE, S_BOILING_WATER, S_GLUE, S_WASTE, S_ANTI_SLOW_MIST, S_ANTIDOTE_MIST, S_SLEEP_MIST, S_DENSE_SHOWER, S_SPARSE_SHOWER, S_MELEE_PROTECTIVE_BALM, S_RANGE_PROTECTIVE_BALM, S_DEFENSELESSNESS_RUNOUT, S_RESOLUTION_DUST, S_HASTE_SPRAY, S_SLOWNESS_MIST, S_EYE_DROP, S_IRON_FILING, S_SMELLY_DROPLET, S_EYE_SHINE, S_WHISKEY_SPRAY, S_YOUTH_SPRINKLE, S_POISON_CLOUD, S_PING_PLASH, S_VIOLENT_BURNING_OIL_2, S_VIOLENT_BURNING_OIL_3, S_VIOLENT_BURNING_OIL_4, S_BURNING_OIL_2, S_BURNING_OIL_3, S_BURNING_OIL_4, S_FOCUSED_BURNING_OIL_2, S_FOCUSED_BURNING_OIL_3, S_FOCUSED_BURNING_OIL_4]+       [S_FIRECRACKER, S_VIOLENT_FRAGMENTATION, S_FRAGMENTATION, S_FOCUSED_FRAGMENTATION, S_VIOLENT_CONCUSSION, S_CONCUSSION, S_FOCUSED_CONCUSSION, S_VIOLENT_FLASH, S_FOCUSED_FLASH, S_GLASS_HAIL, S_FOCUSED_GLASS_HAIL, S_PHEROMONE, S_CALMING_MIST, S_DISTRESSING_ODOR, S_HEALING_MIST, S_HEALING_MIST_2, S_WOUNDING_MIST, S_DISTORTION, S_SMOKE, S_BOILING_WATER, S_GLUE, S_WASTE, S_ANTI_SLOW_MIST, S_ANTIDOTE_MIST, S_SLEEP_MIST, S_DENSE_SHOWER, S_SPARSE_SHOWER, S_MELEE_PROTECTIVE_BALM, S_RANGE_PROTECTIVE_BALM, S_DEFENSELESSNESS_RUNOUT, S_RESOLUTION_DUST, S_HASTE_SPRAY, S_VIOLENT_SLOWNESS_MIST, S_SLOWNESS_MIST, S_FOCUSED_SLOWNESS_MIST, S_EYE_DROP, S_IRON_FILING, S_SMELLY_DROPLET, S_EYE_SHINE, S_WHISKEY_SPRAY, S_YOUTH_SPRINKLE, S_POISON_CLOUD, S_PING_PLASH, S_VIOLENT_BURNING_OIL_2, S_VIOLENT_BURNING_OIL_3, S_VIOLENT_BURNING_OIL_4, S_BURNING_OIL_2, S_BURNING_OIL_3, S_BURNING_OIL_4, S_FOCUSED_BURNING_OIL_2, S_FOCUSED_BURNING_OIL_3, S_FOCUSED_BURNING_OIL_4]   ++ map firecrackerAt [1..4]   ++ map blastNoStatOf noStatGN   ++ map blastBonusStatOf bonusStatGN -pattern S_FIRECRACKER, S_VIOLENT_FRAGMENTATION, S_FRAGMENTATION, S_FOCUSED_FRAGMENTATION, S_VIOLENT_CONCUSSION, S_CONCUSSION, S_FOCUSED_CONCUSSION, S_VIOLENT_FLASH, S_FOCUSED_FLASH, S_GLASS_HAIL, S_FOCUSED_GLASS_HAIL, S_PHEROMONE, S_CALMING_MIST, S_DISTRESSING_ODOR, S_HEALING_MIST, S_HEALING_MIST_2, S_WOUNDING_MIST, S_DISTORTION, S_SMOKE, S_BOILING_WATER, S_GLUE, S_WASTE, S_ANTI_SLOW_MIST, S_ANTIDOTE_MIST, S_SLEEP_MIST, S_DENSE_SHOWER, S_SPARSE_SHOWER, S_MELEE_PROTECTIVE_BALM, S_RANGE_PROTECTIVE_BALM, S_DEFENSELESSNESS_RUNOUT, S_RESOLUTION_DUST, S_HASTE_SPRAY, S_SLOWNESS_MIST, S_EYE_DROP, S_IRON_FILING, S_SMELLY_DROPLET, S_EYE_SHINE, S_WHISKEY_SPRAY, S_YOUTH_SPRINKLE, S_POISON_CLOUD, S_PING_PLASH, S_VIOLENT_BURNING_OIL_2, S_VIOLENT_BURNING_OIL_3, S_VIOLENT_BURNING_OIL_4, S_BURNING_OIL_2, S_BURNING_OIL_3, S_BURNING_OIL_4, S_FOCUSED_BURNING_OIL_2, S_FOCUSED_BURNING_OIL_3, S_FOCUSED_BURNING_OIL_4 :: GroupName ItemKind+pattern S_FIRECRACKER, S_VIOLENT_FRAGMENTATION, S_FRAGMENTATION, S_FOCUSED_FRAGMENTATION, S_VIOLENT_CONCUSSION, S_CONCUSSION, S_FOCUSED_CONCUSSION, S_VIOLENT_FLASH, S_FOCUSED_FLASH, S_GLASS_HAIL, S_FOCUSED_GLASS_HAIL, S_PHEROMONE, S_CALMING_MIST, S_DISTRESSING_ODOR, S_HEALING_MIST, S_HEALING_MIST_2, S_WOUNDING_MIST, S_DISTORTION, S_SMOKE, S_BOILING_WATER, S_GLUE, S_WASTE, S_ANTI_SLOW_MIST, S_ANTIDOTE_MIST, S_SLEEP_MIST, S_DENSE_SHOWER, S_SPARSE_SHOWER, S_MELEE_PROTECTIVE_BALM, S_RANGE_PROTECTIVE_BALM, S_DEFENSELESSNESS_RUNOUT, S_RESOLUTION_DUST, S_HASTE_SPRAY, S_VIOLENT_SLOWNESS_MIST, S_SLOWNESS_MIST, S_FOCUSED_SLOWNESS_MIST, S_EYE_DROP, S_IRON_FILING, S_SMELLY_DROPLET, S_EYE_SHINE, S_WHISKEY_SPRAY, S_YOUTH_SPRINKLE, S_POISON_CLOUD, S_PING_PLASH, S_VIOLENT_BURNING_OIL_2, S_VIOLENT_BURNING_OIL_3, S_VIOLENT_BURNING_OIL_4, S_BURNING_OIL_2, S_BURNING_OIL_3, S_BURNING_OIL_4, S_FOCUSED_BURNING_OIL_2, S_FOCUSED_BURNING_OIL_3, S_FOCUSED_BURNING_OIL_4 :: GroupName ItemKind  blastsGN :: [GroupName ItemKind] blastsGN =@@ -70,7 +71,9 @@ pattern S_DEFENSELESSNESS_RUNOUT = GroupName "PhD defense question" pattern S_RESOLUTION_DUST = GroupName "resolution dust" pattern S_HASTE_SPRAY = GroupName "haste spray"-pattern S_SLOWNESS_MIST = GroupName "slowness mist"+pattern S_VIOLENT_SLOWNESS_MIST = GroupName "violent nitrogen mist"+pattern S_SLOWNESS_MIST = GroupName "nitrogen mist"+pattern S_FOCUSED_SLOWNESS_MIST = GroupName "focused nitrogen mist" pattern S_EYE_DROP = GroupName "eye drop" pattern S_IRON_FILING = GroupName "iron filing" pattern S_SMELLY_DROPLET = GroupName "smelly droplet"@@ -104,9 +107,9 @@  blasts :: [ItemKind] blasts =-  [spreadBurningOil2, spreadBurningOil3, spreadBurningOil4, spreadBurningOil82, spreadBurningOil83, spreadBurningOil84, focusedBurningOil2, focusedBurningOil3, focusedBurningOil4, firecracker1, firecracker2, firecracker3, firecracker4, firecracker5, spreadFragmentation, spreadFragmentation8, focusedFragmentation, spreadConcussion, spreadConcussion8, focusedConcussion, spreadFlash, spreadFlash8, focusedFlash, singleSpark, glassPiece, focusedGlass, fragrance, pheromone, mistCalming, odorDistressing, mistHealing, mistHealing2, mistWounding, distortion, smoke, boilingWater, glue, waste, mistAntiSlow, mistAntidote, mistSleep, denseShower, sparseShower, protectingBalmMelee, protectingBalmRanged, defenselessnessRunout, resolutionDust, hasteSpray, slownessMist, eyeDrop, ironFiling, smellyDroplet, eyeShine, whiskeySpray, youthSprinkle, poisonCloud, pingFlash, blastNoSkMove, blastNoSkMelee, blastNoSkDisplace, blastNoSkAlter, blastNoSkWait, blastNoSkMoveItem, blastNoSkProject, blastNoSkApply, blastBonusSkMove, blastBonusSkMelee, blastBonusSkDisplace, blastBonusSkAlter, blastBonusSkWait, blastBonusSkMoveItem, blastBonusSkProject, blastBonusSkApply]+  [spreadBurningOil2, spreadBurningOil3, spreadBurningOil4, spreadBurningOil82, spreadBurningOil83, spreadBurningOil84, focusedBurningOil2, focusedBurningOil3, focusedBurningOil4, firecracker1, firecracker2, firecracker3, firecracker4, firecracker5, spreadFragmentation, spreadFragmentation8, focusedFragmentation, spreadConcussion, spreadConcussion8, focusedConcussion, spreadFlash, spreadFlash8, focusedFlash, singleSpark, glassPiece, focusedGlass, fragrance, pheromone, mistCalming, odorDistressing, mistHealing, mistHealing2, mistWounding, distortion, smoke, boilingWater, glue, waste, mistAntiSlow, mistAntidote, mistSleep, denseShower, sparseShower, protectingBalmMelee, protectingBalmRanged, defenselessnessRunout, resolutionDust, hasteSpray, spreadNitrogen, spreadNitrogen8, focusedNitrogen, eyeDrop, ironFiling, smellyDroplet, eyeShine, whiskeySpray, youthSprinkle, poisonCloud, pingFlash, blastNoSkMove, blastNoSkMelee, blastNoSkDisplace, blastNoSkAlter, blastNoSkWait, blastNoSkMoveItem, blastNoSkProject, blastNoSkApply, blastBonusSkMove, blastBonusSkMelee, blastBonusSkDisplace, blastBonusSkAlter, blastBonusSkWait, blastBonusSkMoveItem, blastBonusSkProject, blastBonusSkApply] -spreadBurningOil2,    spreadBurningOil3, spreadBurningOil4, spreadBurningOil82, spreadBurningOil83, spreadBurningOil84, focusedBurningOil2, focusedBurningOil3, focusedBurningOil4, firecracker1, firecracker2, firecracker3, firecracker4, firecracker5, spreadFragmentation, spreadFragmentation8, focusedFragmentation, spreadConcussion, spreadConcussion8, focusedConcussion, spreadFlash, spreadFlash8, focusedFlash, singleSpark, glassPiece, focusedGlass, fragrance, pheromone, mistCalming, odorDistressing, mistHealing, mistHealing2, mistWounding, distortion, smoke, boilingWater, glue, waste, mistAntiSlow, mistAntidote, mistSleep, denseShower, sparseShower, protectingBalmMelee, protectingBalmRanged, defenselessnessRunout, resolutionDust, hasteSpray, slownessMist, eyeDrop, ironFiling, smellyDroplet, eyeShine, whiskeySpray, youthSprinkle, poisonCloud, pingFlash, blastNoSkMove, blastNoSkMelee, blastNoSkDisplace, blastNoSkAlter, blastNoSkWait, blastNoSkMoveItem, blastNoSkProject, blastNoSkApply, blastBonusSkMove, blastBonusSkMelee, blastBonusSkDisplace, blastBonusSkAlter, blastBonusSkWait, blastBonusSkMoveItem, blastBonusSkProject, blastBonusSkApply :: ItemKind+spreadBurningOil2,    spreadBurningOil3, spreadBurningOil4, spreadBurningOil82, spreadBurningOil83, spreadBurningOil84, focusedBurningOil2, focusedBurningOil3, focusedBurningOil4, firecracker1, firecracker2, firecracker3, firecracker4, firecracker5, spreadFragmentation, spreadFragmentation8, focusedFragmentation, spreadConcussion, spreadConcussion8, focusedConcussion, spreadFlash, spreadFlash8, focusedFlash, singleSpark, glassPiece, focusedGlass, fragrance, pheromone, mistCalming, odorDistressing, mistHealing, mistHealing2, mistWounding, distortion, smoke, boilingWater, glue, waste, mistAntiSlow, mistAntidote, mistSleep, denseShower, sparseShower, protectingBalmMelee, protectingBalmRanged, defenselessnessRunout, resolutionDust, hasteSpray, spreadNitrogen, spreadNitrogen8, focusedNitrogen,  eyeDrop, ironFiling, smellyDroplet, eyeShine, whiskeySpray, youthSprinkle, poisonCloud, pingFlash, blastNoSkMove, blastNoSkMelee, blastNoSkDisplace, blastNoSkAlter, blastNoSkWait, blastNoSkMoveItem, blastNoSkProject, blastNoSkApply, blastBonusSkMove, blastBonusSkMelee, blastBonusSkDisplace, blastBonusSkAlter, blastBonusSkWait, blastBonusSkMoveItem, blastBonusSkProject, blastBonusSkApply :: ItemKind  -- We take care (e.g., in burningOil below) that blasts are not faster -- than 100% fastest natural speed, or some frames would be skipped,@@ -116,16 +119,16 @@  spreadBurningOil :: Int -> GroupName ItemKind -> ItemKind spreadBurningOil n grp = ItemKind-  { isymbol  = '*'+  { isymbol  = toContentSymbol '*'   , iname    = "burning oil"   , ifreq    = [(grp, 1)]   , iflavour = zipPlain [BrYellow]-  , icount   = intToDice (4 + n * 3)+  , icount   = intToDice (4 + n * 2)   , irarity  = [(1, 1)]   , iverbHit = "sear"   , iweight  = 1   , idamage  = 0-  , iaspects = [ toVelocity (min 100 $ n `div` 2 * 10)+  , iaspects = [ toVelocity (max 10 $ min 100 $ n `div` 2 * 10)                , SetFlag Fragile, SetFlag Blast                , AddSkill SkShine 2 ]   , ieffects = [ Burn 1@@ -142,17 +145,18 @@                       -- 4 steps, 2 turns spreadBurningOil8 :: Int -> GroupName ItemKind -> ItemKind spreadBurningOil8 n grp = (spreadBurningOil (n `div` 2) grp)-  { icount   = 7+  { icount   = 7  -- 8 was too deadly   } spreadBurningOil82 = spreadBurningOil8 2 S_BURNING_OIL_2 spreadBurningOil83 = spreadBurningOil8 3 S_BURNING_OIL_3 spreadBurningOil84 = spreadBurningOil8 4 S_BURNING_OIL_4 focusedBurningOil :: Int -> GroupName ItemKind -> GroupName ItemKind -> ItemKind focusedBurningOil n grp grpExplode = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "igniting oil"   , ifreq    = [(grp, 1)]-  , iflavour = zipPlain [Brown]+  , iflavour = zipPlain [BrYellow]  -- all ignitions yellow to avoid appearing+                                    -- as a small vial explosion   , icount   = intToDice n   , irarity  = [(1, 1)]   , iverbHit = "ignite"@@ -171,7 +175,7 @@ focusedBurningOil4 = focusedBurningOil 4 S_FOCUSED_BURNING_OIL_4 S_BURNING_OIL_4 firecracker :: Int -> ItemKind firecracker n = ItemKind-  { isymbol  = '*'+  { isymbol  = toContentSymbol '*'   , iname    = "firecracker"   , ifreq    = [(if n == 5                  then S_FIRECRACKER@@ -200,11 +204,11 @@ -- * Focused blasts  spreadFragmentation = ItemKind-  { isymbol  = '*'+  { isymbol  = toContentSymbol '*'   , iname    = "fragmentation burst"   , ifreq    = [(S_VIOLENT_FRAGMENTATION, 1)]   , iflavour = zipPlain [Red]-  , icount   = 16  -- strong but few, so not always hits target+  , icount   = 8  -- strong but few, so not always hits target   , irarity  = [(1, 1)]   , iverbHit = "tear apart"   , iweight  = 1@@ -219,18 +223,18 @@   } spreadFragmentation8 = spreadFragmentation   { ifreq    = [(S_FRAGMENTATION, 1)]-  , icount   = 8+  , icount   = 5   , iaspects = [ ToThrow $ ThrowMod 100 10 2  -- 2 steps, 1 turn                , SetFlag Lobable, SetFlag Fragile, SetFlag Blast                , AddSkill SkShine 3, AddSkill SkHurtMelee $ -12 * 5 ]       -- smaller radius, so worse for area effect, but twice the direct damage   } focusedFragmentation = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "deflagration ignition"  -- black powder   , ifreq    = [(S_FOCUSED_FRAGMENTATION, 1)]   , iflavour = zipPlain [BrYellow]-  , icount   = 4  -- 32 in total vs 16; on average 4 hits+  , icount   = 3  -- 15 in total vs 8, higher spread   , irarity  = [(1, 1)]   , iverbHit = "ignite"   , iweight  = 1@@ -244,11 +248,12 @@   , ikit     = []   } spreadConcussion = ItemKind-  { isymbol  = '*'+  { isymbol  = toContentSymbol '*'   , iname    = "concussion blast"   , ifreq    = [(S_VIOLENT_CONCUSSION, 1)]   , iflavour = zipPlain [Magenta]-  , icount   = 16+  , icount   = 12  -- pushing sometimes gets the victim out of attacker range,+                   -- but also sometimes moves to a position hit again later   , irarity  = [(1, 1)]   , iverbHit = "shock"   , iweight  = 1@@ -263,24 +268,24 @@   , ieffects = [ DropItem maxBound 1 CEqp ARMOR_MISC                , PushActor (ThrowMod 400 25 1)  -- 1 step, fast; after DropItem                    -- this produces spam for braced actors; too bad-               , toOrganBad S_IMMOBILE 3  -- no balance+               , toOrganBad S_IMMOBILE 1  -- no balance                , toOrganBad S_DEAFENED 23 ]   , idesc    = "Shock wave, hot gases, some fire and smoke."   , ikit     = []   } spreadConcussion8 = spreadConcussion   { ifreq    = [(S_CONCUSSION, 1)]-  , icount   = 8+  , icount   = 6   , iaspects = [ ToThrow $ ThrowMod 100 10 2  -- 2 steps, 1 turn                , SetFlag Lobable, SetFlag Fragile, SetFlag Blast                , AddSkill SkShine 3, AddSkill SkHurtMelee $ -8 * 5 ]   } focusedConcussion = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "detonation ignition"  -- nitroglycerine   , ifreq    = [(S_FOCUSED_CONCUSSION, 1)]   , iflavour = zipPlain [BrYellow]-  , icount   = 4+  , icount   = 4  -- 24 in total vs 12, higher spread, less harm from pushing   , irarity  = [(1, 1)]   , iverbHit = "ignite"   , iweight  = 1@@ -292,11 +297,11 @@   , ikit     = []   } spreadFlash = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "magnesium flash"   , ifreq    = [(S_VIOLENT_FLASH, 1)]   , iflavour = zipPlain [BrWhite]-  , icount   = 16+  , icount   = 13   , irarity  = [(1, 1)]   , iverbHit = "dazzle"   , iweight  = 1@@ -304,27 +309,28 @@   , iaspects = [ ToThrow $ ThrowMod 100 20 4  -- 4 steps, 1 turn                , SetFlag Fragile, SetFlag Blast                , AddSkill SkShine 5 ]-  , ieffects = [toOrganBad S_BLIND 5, toOrganBad S_WEAKENED 20]+  , ieffects = [ toOrganBad S_BLIND 5+               , toOrganBad S_WEAKENED 20 ]                  -- Wikipedia says: blind for five seconds and afterimage                  -- for much longer, harming aim-  , idesc    = "A very bright flash of fire."+  , idesc    = "A very bright flash of fire, causing long-lasting afterimages."   , ikit     = []   } spreadFlash8 = spreadFlash   { iname    = "spark"   , ifreq    = [(S_SPARK, 1)]-  , icount   = 8+  , icount   = 5   , iverbHit = "singe"   , iaspects = [ ToThrow $ ThrowMod 100 10 2  -- 2 steps, 1 turn                , SetFlag Fragile, SetFlag Blast                , AddSkill SkShine 5 ]   } focusedFlash = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "magnesium ignition"   , ifreq    = [(S_FOCUSED_FLASH, 1)]   , iflavour = zipPlain [BrYellow]-  , icount   = 4+  , icount   = 5  -- 25 in total vs 13, higher spread   , irarity  = [(1, 1)]   , iverbHit = "ignite"   , iweight  = 1@@ -337,7 +343,7 @@   } singleSpark = spreadFlash   { iname    = "single spark"-  , ifreq    = [(S_SINGLE_SPARK, 1)]+  , ifreq    = [(S_SINGLE_SPARK, 1)]  -- too weak to start a fire   , icount   = 1   , iverbHit = "spark"   , iaspects = [ toLinger 5  -- 1 step, 1 turn@@ -348,16 +354,16 @@   , ikit     = []   } glassPiece = ItemKind-  { isymbol  = '*'+  { isymbol  = toContentSymbol '*'   , iname    = "glass piece"   , ifreq    = [(S_GLASS_HAIL, 1)]   , iflavour = zipPlain [Blue]-  , icount   = 8+  , icount   = 6   , irarity  = [(1, 1)]   , iverbHit = "cut"   , iweight  = 1   , idamage  = 2 `d` 1-  , iaspects = [ ToThrow $ ThrowMod 100 20 4  -- 4 steps, 1 turn+  , iaspects = [ ToThrow $ ThrowMod 100 10 4  -- 2 steps, 1 turn                , SetFlag Fragile, SetFlag Blast                , AddSkill SkHurtMelee $ -15 * 5 ]                  -- brittle, not too dense; armor blocks@@ -367,7 +373,7 @@   } focusedGlass = glassPiece  -- when blowing up windows   { ifreq    = [(S_FOCUSED_GLASS_HAIL, 1)]-  , icount   = 4+  , icount   = 2   , iaspects = [ toLinger 0  -- 0 steps, 1 turn                , SetFlag Fragile, SetFlag Blast                , AddSkill SkHurtMelee $ -15 * 5 ]@@ -377,11 +383,11 @@ -- * Assorted blasts that don't induce conditions or not used mainly for them  fragrance = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "fragrance"  -- instant, fast fragrance   , ifreq    = [(S_FRAGRANCE, 1)]   , iflavour = zipPlain [Magenta]-  , icount   = 12+  , icount   = 8   , irarity  = [(1, 1)]   , iverbHit = "engulf"   , iweight  = 1@@ -395,7 +401,7 @@   , ikit     = []   } pheromone = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "musky whiff"  -- a kind of mist rather than fragrance   , ifreq    = [(S_PHEROMONE, 1)]   , iflavour = zipPlain [BrMagenta]@@ -411,7 +417,7 @@   , ikit     = []   } mistCalming = ItemKind  -- unused-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "mist"   , ifreq    = [(S_CALMING_MIST, 1)]   , iflavour = zipPlain [BrGreen]@@ -427,7 +433,7 @@   , ikit     = []   } odorDistressing = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "distressing whiff"   , ifreq    = [(S_DISTRESSING_ODOR, 1)]   , iflavour = zipFancy [BrRed]  -- salmon@@ -445,7 +451,7 @@   , ikit     = []   } mistHealing = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "mist"  -- powerful, so slow and narrow   , ifreq    = [(S_HEALING_MIST, 1)]   , iflavour = zipFancy [BrGreen]@@ -462,7 +468,7 @@   , ikit     = []   } mistHealing2 = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "mist"   , ifreq    = [(S_HEALING_MIST_2, 1)]   , iflavour = zipPlain [Green]@@ -479,7 +485,7 @@   , ikit     = []   } mistWounding = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "mist"   , ifreq    = [(S_WOUNDING_MIST, 1)]   , iflavour = zipPlain [BrRed]@@ -495,7 +501,7 @@   , ikit     = []   } distortion = ItemKind-  { isymbol  = 'v'+  { isymbol  = toContentSymbol 'v'   , iname    = "vortex"   , ifreq    = [(S_DISTORTION, 1)]   , iflavour = zipPlain [White]@@ -511,7 +517,7 @@   , ikit     = []   } smoke = ItemKind  -- when stuff burns out  -- unused-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "smoke fume"  -- pluralizes better than 'smokes'   , ifreq    = [(S_SMOKE, 1)]   , iflavour = zipPlain [BrBlack]@@ -528,7 +534,7 @@   , ikit     = []   } boilingWater = ItemKind-  { isymbol  = '*'+  { isymbol  = toContentSymbol '*'   , iname    = "boiling water"   , ifreq    = [(S_BOILING_WATER, 1)]   , iflavour = zipPlain [White]@@ -544,7 +550,7 @@   , ikit     = []   } glue = ItemKind-  { isymbol  = '*'+  { isymbol  = toContentSymbol '*'   , iname    = "hoof glue"   , ifreq    = [(S_GLUE, 1)]   , iflavour = zipPlain [Cyan]@@ -553,14 +559,14 @@   , iverbHit = "glue"   , iweight  = 1   , idamage  = 0-  , iaspects = [ toVelocity 20  -- 4 steps, 2 turns+  , iaspects = [ toVelocity 10  -- 2 steps, 2 turns                , SetFlag Fragile, SetFlag Blast ]   , ieffects = [Paralyze 10]   , idesc    = "Thick and clinging."   , ikit     = []   } waste = ItemKind-  { isymbol  = '*'+  { isymbol  = toContentSymbol '*'   , iname    = "waste piece"   , ifreq    = [(S_WASTE, 1)]   , iflavour = zipPlain [Brown]@@ -576,7 +582,7 @@   , ikit     = []   } mistAntiSlow = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "mist"   , ifreq    = [(S_ANTI_SLOW_MIST, 1)]   , iflavour = zipFancy [BrYellow]@@ -592,7 +598,7 @@   , ikit     = []   } mistAntidote = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "mist"   , ifreq    = [(S_ANTIDOTE_MIST, 1)]   , iflavour = zipFancy [BrBlue]@@ -608,7 +614,7 @@   , ikit     = []   } mistSleep = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "mist"   , ifreq    = [(S_SLEEP_MIST, 1)]   , iflavour = zipFancy [BrMagenta]@@ -632,7 +638,7 @@ -- A few are slower 'mists'.  denseShower = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "dense shower"   , ifreq    = [(S_DENSE_SHOWER, 1)]   , iflavour = zipFancy [Green]@@ -647,7 +653,7 @@   , ikit     = []   } sparseShower = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "sparse shower"   , ifreq    = [(S_SPARSE_SHOWER, 1)]   , iflavour = zipFancy [Red]@@ -662,22 +668,23 @@   , ikit     = []   } protectingBalmMelee = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "balm droplet"   , ifreq    = [(S_MELEE_PROTECTIVE_BALM, 1)]   , iflavour = zipFancy [Brown]-  , icount   = 16+  , icount   = 6   , irarity  = [(1, 1)]   , iverbHit = "balm"   , iweight  = 1   , idamage  = 0-  , iaspects = [toLinger 10, SetFlag Fragile, SetFlag Blast]+  , iaspects = [ toLinger 0  -- 0 steps, 1 turn+               , SetFlag Fragile, SetFlag Blast ]   , ieffects = [toOrganGood S_PROTECTED_FROM_MELEE (3 + 1 `d` 3)]   , idesc    = "A thick ointment that hardens the skin."   , ikit     = []   } protectingBalmRanged = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "balm droplet"   , ifreq    = [(S_RANGE_PROTECTIVE_BALM, 1)]   , iflavour = zipPlain [BrYellow]@@ -692,7 +699,7 @@   , ikit     = []   } defenselessnessRunout = ItemKind-  { isymbol  = '?'+  { isymbol  = toContentSymbol '?'   , iname    = "PhD defense question"   , ifreq    = [(S_DEFENSELESSNESS_RUNOUT, 1)]   , iflavour = zipFancy [BrRed]@@ -707,7 +714,7 @@   , ikit     = []   } resolutionDust = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "resolution dust"   , ifreq    = [(S_RESOLUTION_DUST, 1)]   , iflavour = zipPlain [Brown]@@ -723,7 +730,7 @@   , ikit     = []   } hasteSpray = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "haste spray"   , ifreq    = [(S_HASTE_SPRAY, 1)]   , iflavour = zipFancy [BrYellow]@@ -737,24 +744,47 @@   , idesc    = "A quick spurt."   , ikit     = []   }-slownessMist = ItemKind-  { isymbol  = '`'+spreadNitrogen = ItemKind+  { isymbol  = toContentSymbol '`'   , iname    = "slowness mist"-  , ifreq    = [(S_SLOWNESS_MIST, 1)]-  , iflavour = zipPlain [BrBlue]-  , icount   = 8+  , ifreq    = [(S_VIOLENT_SLOWNESS_MIST, 1)]+  , iflavour = zipPlain [BrBlack]+  , icount   = 15   , irarity  = [(1, 1)]-  , iverbHit = "slow"-  , iweight  = 0+  , iverbHit = "freeze"+  , iweight  = 1   , idamage  = 0-  , iaspects = [toVelocity 5, SetFlag Fragile, SetFlag Blast]-                 -- 1 step, 1 turn, mist, slow-  , ieffects = [toOrganBad S_SLOWED (3 + 1 `d` 3)]-  , idesc    = "Clammy fog, making each movement an effort."+  , iaspects = [ toVelocity 10  -- 2 steps, 2 turns, mist, slow+               , SetFlag Fragile, SetFlag Blast ]+  , ieffects = [toOrganBad S_SLOWED (2 + 1 `d` 3)]+  , idesc    = "Colourless cold clammy fog, making each movement an effort."   , ikit     = []   }+spreadNitrogen8 = spreadNitrogen+  { ifreq    = [(S_SLOWNESS_MIST, 1)]+  , icount   = 7+  , iaspects = [ toVelocity 5  -- 1 step, 1 turn, mist, slow+               , SetFlag Fragile, SetFlag Blast ]+  }+focusedNitrogen = ItemKind+  { isymbol  = toContentSymbol '`'+  , iname    = "slowness mist droplet"+  , ifreq    = [(S_FOCUSED_SLOWNESS_MIST, 1)]+  , iflavour = zipFancy [White]+  , icount   = 4 -- 28 in total vs 15, higher spread+  , irarity  = [(1, 1)]+  , iverbHit = "freeze"+  , iweight  = 1+  , idamage  = 0+  , iaspects = [ toLinger 0  -- 0 steps, 1 turn+               , SetFlag Fragile, SetFlag Blast ]+  , ieffects = [ OnSmash $ Explode S_SLOWNESS_MIST+               , toOrganBad S_SLOWED (2 + 1 `d` 3) ]+  , idesc    = "Colourless and colder than ice."+  , ikit     = []+  } eyeDrop = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "eye drop"   , ifreq    = [(S_EYE_DROP, 1)]   , iflavour = zipFancy [BrCyan]@@ -769,7 +799,7 @@   , ikit     = []   } ironFiling = ItemKind  -- fast, short, strongly blinding blast-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "iron filing"   , ifreq    = [(S_IRON_FILING, 1)]   , iflavour = zipPlain [Red]@@ -784,7 +814,7 @@   , ikit     = []   } smellyDroplet = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "smelly droplet"   , ifreq    = [(S_SMELLY_DROPLET, 1)]   , iflavour = zipFancy [Blue]@@ -799,7 +829,7 @@   , ikit     = []   } eyeShine = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "eye shine"   , ifreq    = [(S_EYE_SHINE, 1)]   , iflavour = zipFancy [Cyan]@@ -814,7 +844,7 @@   , ikit     = []   } whiskeySpray = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "whiskey spray"   , ifreq    = [(S_WHISKEY_SPRAY, 1)]   , iflavour = zipFancy [Brown]@@ -829,7 +859,7 @@   , ikit     = []   } youthSprinkle = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "youth sprinkle"   , ifreq    = [(S_YOUTH_SPRINKLE, 1)]   , iflavour = zipFancy [BrGreen]@@ -845,11 +875,11 @@   , ikit     = []   } poisonCloud = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "poison cloud"   , ifreq    = [(S_POISON_CLOUD, 1)]   , iflavour = zipFancy [BrMagenta]-  , icount   = 16+  , icount   = 11  -- low, to be less deadly in a tunnel, compared to single hit   , irarity  = [(1, 1)]   , iverbHit = "poison"   , iweight  = 0  -- lingers, blocking path@@ -861,7 +891,7 @@   , ikit     = []   } pingFlash = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "flash"   , ifreq    = [(S_PING_PLASH, 1)]   , iflavour = zipFancy [Green]@@ -870,7 +900,7 @@   , iverbHit = "ping"   , iweight  = 1  -- to prevent blocking the way   , idamage  = 0-  , iaspects = [ toLinger 0+  , iaspects = [ ToThrow $ ThrowMod 200 0 1  -- 1 step, .5 turn (necklaces)                , SetFlag Fragile, SetFlag Blast                , AddSkill SkShine 2 ]   , ieffects = [OnSmash Yell]@@ -879,7 +909,7 @@   } blastNoStat :: GroupName ItemKind -> ItemKind blastNoStat grp = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "mist"   , ifreq    = [(blastNoStatOf grp, 1)]   , iflavour = zipFancy [White]@@ -904,7 +934,7 @@ blastNoSkApply = blastNoStat S_PARSIMONIOUS blastBonusStat :: GroupName ItemKind -> ItemKind blastBonusStat grp = ItemKind-  { isymbol  = '`'+  { isymbol  = toContentSymbol '`'   , iname    = "dew"   , ifreq    = [(blastBonusStatOf grp, 1)]   , iflavour = zipFancy [White]
GameDefinition/Content/ItemKindEmbed.hs view
@@ -2,8 +2,7 @@ module Content.ItemKindEmbed   ( -- * Group name patterns     pattern SCRATCH_ON_WALL, pattern OBSCENE_PICTOGRAM, pattern SUBTLE_FRESCO, pattern TREASURE_CACHE, pattern TREASURE_CACHE_TRAP, pattern SIGNAGE, pattern SMALL_FIRE, pattern BIG_FIRE, pattern FROST, pattern RUBBLE, pattern DOORWAY_TRAP_UNKNOWN, pattern DOORWAY_TRAP, pattern STAIRS_UP, pattern STAIRS_DOWN, pattern ESCAPE, pattern STAIRS_TRAP_UP, pattern STAIRS_TRAP_DOWN, pattern LECTERN, pattern SHALLOW_WATER, pattern STRAIGHT_PATH, pattern FROZEN_GROUND-  , pattern S_SANDSTONE_ROCK-  , embedsGNSingleton, embedsGN+  , embedsGN   , -- * Content     embeds   ) where@@ -20,15 +19,11 @@ import Game.LambdaHack.Definition.Ability import Game.LambdaHack.Definition.Color import Game.LambdaHack.Definition.Defs+import Game.LambdaHack.Definition.DefsInternal import Game.LambdaHack.Definition.Flavour  -- * Group name patterns -embedsGNSingleton :: [GroupName ItemKind]-embedsGNSingleton = [S_SANDSTONE_ROCK]--pattern S_SANDSTONE_ROCK :: GroupName ItemKind- embedsGN :: [GroupName ItemKind] embedsGN =        [SCRATCH_ON_WALL, OBSCENE_PICTOGRAM, SUBTLE_FRESCO, TREASURE_CACHE, TREASURE_CACHE_TRAP, SIGNAGE, SMALL_FIRE, BIG_FIRE, FROST, RUBBLE, DOORWAY_TRAP_UNKNOWN, DOORWAY_TRAP, STAIRS_UP, STAIRS_DOWN, ESCAPE, STAIRS_TRAP_UP, STAIRS_TRAP_DOWN, LECTERN, SHALLOW_WATER, STRAIGHT_PATH, FROZEN_GROUND]@@ -57,8 +52,6 @@ pattern STRAIGHT_PATH = GroupName "straight path" pattern FROZEN_GROUND = GroupName "frozen ground" -pattern S_SANDSTONE_ROCK = GroupName "sandstone rock"- -- * Content  embeds :: [ItemKind]@@ -67,7 +60,6 @@  scratchOnWall,    obscenePictogram, subtleFresco, treasureCache, treasureCacheTrap, signageExit, signageEmbed, signageMerchandise, fireSmall, fireBig, frost, rubble, doorwayTrapTemplate, doorwayTrap1, doorwayTrap2, doorwayTrap3, stairsUp, stairsDown, escape, stairsTrapUp, stairsTrapDown, lectern, shallowWater, straightPath, frozenGround :: ItemKind --- * Embedded items  -- Make sure very few walls are substantially useful, e.g., caches, -- and none that are secret. Otherwise the player will spend a lot of time@@ -76,7 +68,7 @@ -- and foregoing the fun of guessing how to find entrance to a disjoint part -- of the level by bumping the least number of secret walls. scratchOnWall = ItemKind-  { isymbol  = '?'+  { isymbol  = toContentSymbol '?'   , iname    = "claw mark"   , ifreq    = [(SCRATCH_ON_WALL, 1)]   , iflavour = zipPlain [BrBlack]@@ -87,12 +79,12 @@   , idamage  = 0   , iaspects = [SetFlag Durable]   , ieffects = [ VerbMsg "start making sense of the scratches" "."-               , Detect DetectHidden 3 ]+               , Detect DetectHidden 4 ]   , idesc    = "A seemingly random series of scratches, carved deep into the wall."   , ikit     = []   } obscenePictogram = ItemKind-  { isymbol  = '*'+  { isymbol  = toContentSymbol '*'   , iname    = "obscene pictogram"   , ifreq    = [(OBSCENE_PICTOGRAM, 1)]   , iflavour = zipPlain [BrMagenta]@@ -111,7 +103,7 @@   , ikit     = []   } subtleFresco = ItemKind-  { isymbol  = '*'+  { isymbol  = toContentSymbol '*'   , iname    = "subtle fresco"   , ifreq    = [(SUBTLE_FRESCO, 1)]   , iflavour = zipPlain [BrGreen]@@ -129,7 +121,7 @@   , ikit     = []   } treasureCache = ItemKind-  { isymbol  = 'o'+  { isymbol  = toContentSymbol 'o'   , iname    = "treasure cache"   , ifreq    = [(TREASURE_CACHE, 1)]   , iflavour = zipPlain [BrBlue]@@ -146,7 +138,7 @@ reliefMsg :: Effect reliefMsg = VerbMsg "sigh with relief when nothing explodes in your face!" "" treasureCacheTrap = ItemKind-  { isymbol  = '^'+  { isymbol  = toContentSymbol '^'   , iname    = "cache trap"   , ifreq    = [(TREASURE_CACHE_TRAP, 1)]   , iflavour = zipPlain [Red]@@ -164,10 +156,10 @@   , ikit     = []   } signageExit = ItemKind-  { isymbol  = '?'+  { isymbol  = toContentSymbol '?'   , iname    = "inscription"-  , ifreq    = [(SIGNAGE, 50)]-  , iflavour = zipPlain [BrMagenta]+  , ifreq    = [(SIGNAGE, 100)]+  , iflavour = zipPlain [BrGreen]   , icount   = 1   , irarity  = [(1, 1)]   , iverbHit = "whack"@@ -180,18 +172,20 @@   } signageEmbed = signageExit   { iname    = "notice"-  , ifreq    = [(SIGNAGE, 50)]+  , ifreq    = [(SIGNAGE, 100)]+  , iflavour = zipPlain [Cyan]   , ieffects = [Detect DetectEmbed 12]   , idesc    = "The battered poster is untitled and unsigned."   } signageMerchandise = signageExit   { iname    = "treasure map"-  , ifreq    = [(SIGNAGE, 50)]+  , ifreq    = [(SIGNAGE, 100)]+  , iflavour = zipPlain [BrCyan]   , ieffects = [Detect DetectLoot 20]   , idesc    = "In equal parts cryptic and promising."   } fireSmall = ItemKind-  { isymbol  = 'o'+  { isymbol  = toContentSymbol 'o'   , iname    = "small fire"   , ifreq    = [(SMALL_FIRE, 1)]   , iflavour = zipPlain [BrRed]@@ -206,9 +200,10 @@   , ikit     = []   } fireBig = fireSmall-  { isymbol  = '0'+  { isymbol  = toContentSymbol '0'   , iname    = "big fire"   , ifreq    = [(BIG_FIRE, 1)]+  , iflavour = zipPlain [Red]   , ieffects = [ Burn 2                , CreateItem Nothing CGround S_WOODEN_TORCH timerNone                , Explode S_SPARK ]@@ -216,7 +211,7 @@   , ikit     = []   } frost = ItemKind-  { isymbol  = '^'+  { isymbol  = toContentSymbol '^'   , iname    = "frost"   , ifreq    = [(FROST, 1)]   , iflavour = zipPlain [BrBlue]@@ -233,7 +228,7 @@   , ikit     = []   } rubble = ItemKind-  { isymbol  = '&'+  { isymbol  = toContentSymbol '&'   , iname    = "rubble"   , ifreq    = [(RUBBLE, 1)]   , iflavour = zipPlain [BrYellow]@@ -254,7 +249,7 @@   , ikit     = []   } doorwayTrapTemplate = ItemKind-  { isymbol  = '+'+  { isymbol  = toContentSymbol '+'   , iname    = "doorway trap"   , ifreq    = [(DOORWAY_TRAP_UNKNOWN, 1), (DOORWAY_TRAP, 0)]       -- the void group needed to pick the item for tile triggering@@ -287,7 +282,7 @@   -- , idesc    = ""   } stairsUp = ItemKind-  { isymbol  = '<'+  { isymbol  = toContentSymbol '<'   , iname    = "flight"   , ifreq    = [(STAIRS_UP, 1)]   , iflavour = zipPlain [BrWhite]@@ -303,16 +298,16 @@   , ikit     = []   } stairsDown = stairsUp-  { isymbol  = '>'+  { isymbol  = toContentSymbol '>'   , ifreq    = [(STAIRS_DOWN, 1)]   , ieffects = [Ascend False]   , idesc    = ""   } escape = stairsUp-  { isymbol  = 'E'+  { isymbol  = toContentSymbol '>'   , iname    = "way"   , ifreq    = [(ESCAPE, 1)]-  , iflavour = zipPlain [BrYellow]+  , iflavour = zipPlain [BrGreen]   , iaspects = [SetFlag Durable]   , ieffects = [Escape]   , idesc    = "May this nightmare have an end?"@@ -320,10 +315,10 @@                  -- for moon outdoors, spaceship, everywhere   } stairsTrapUp = ItemKind-  { isymbol  = '^'+  { isymbol  = toContentSymbol '^'   , iname    = "staircase trap"   , ifreq    = [(STAIRS_TRAP_UP, 1)]-  , iflavour = zipPlain [Red]+  , iflavour = zipPlain [BrRed]   , icount   = 1   , irarity  = [(1, 1)]   , iverbHit = "buffet"@@ -340,13 +335,14 @@ -- so that effects are invoked in the proper order and, e.g., teleport works. stairsTrapDown = stairsTrapUp   { ifreq    = [(STAIRS_TRAP_DOWN, 1)]+  , iflavour = zipPlain [Red]   , iverbHit = "open up under"   , ieffects = [ VerbMsgFail "tumble down the stairwell" "."                , toOrganGood S_DRUNK (20 + 1 `d` 5) ]   , idesc    = "A treacherous slab, to teach those who are too proud."   } lectern = ItemKind-  { isymbol  = '?'+  { isymbol  = toContentSymbol '?'   , iname    = "lectern"   , ifreq    = [(LECTERN, 1)]   , iflavour = zipFancy [BrYellow]@@ -365,7 +361,7 @@   , ikit     = []   } shallowWater = ItemKind-  { isymbol  = '~'+  { isymbol  = toContentSymbol '~'   , iname    = "shallow water"   , ifreq    = [(SHALLOW_WATER, 1)]   , iflavour = zipFancy [BrCyan]@@ -380,7 +376,7 @@   , ikit     = []   } straightPath = ItemKind-  { isymbol  = '.'+  { isymbol  = toContentSymbol '.'   , iname    = "straight path"   , ifreq    = [(STRAIGHT_PATH, 1)]   , iflavour = zipFancy [BrRed]@@ -395,7 +391,7 @@   , ikit     = []   } frozenGround = ItemKind-  { isymbol  = '.'+  { isymbol  = toContentSymbol '.'   , iname    = "shade"   , ifreq    = [(FROZEN_GROUND, 1)]   , iflavour = zipFancy [BrBlue]
GameDefinition/Content/ItemKindOrgan.hs view
@@ -2,8 +2,8 @@ module Content.ItemKindOrgan   ( -- * Group name patterns     pattern S_FIST, pattern S_FOOT, pattern S_HOOKED_CLAW, pattern S_SMALL_CLAW, pattern S_SNOUT, pattern S_SMALL_JAW, pattern S_JAW, pattern S_LARGE_JAW, pattern S_ANTLER, pattern S_HORN, pattern S_RHINO_HORN, pattern S_TENTACLE, pattern S_TIP, pattern S_LIP, pattern S_THORN, pattern S_BOILING_FISSURE, pattern S_ARSENIC_FISSURE, pattern S_SULFUR_FISSURE, pattern S_BEE_STING, pattern S_STING, pattern S_VENOM_TOOTH, pattern S_VENOM_FANG, pattern S_SCREECHING_BEAK, pattern S_LARGE_TAIL, pattern S_HUGE_TAIL, pattern S_ARMORED_SKIN, pattern S_BARK, pattern S_NOSTRIL, pattern S_RATLLE, pattern S_INSECT_MORTALITY, pattern S_SAPIENT_BRAIN, pattern S_ANIMAL_BRAIN, pattern S_SCENT_GLAND, pattern S_BOILING_VENT, pattern S_ARSENIC_VENT, pattern S_SULFUR_VENT, pattern S_EYE_3, pattern S_EYE_6, pattern S_EYE_8, pattern S_VISION_6, pattern S_VISION_12, pattern S_VISION_16, pattern S_EAR_3, pattern S_EAR_6, pattern S_EAR_8, pattern S_SPEED_GLAND_5, pattern S_SPEED_GLAND_10-  , pattern S_TOOTH, pattern S_LASH, pattern S_RIGHT_TORSION, pattern S_LEFT_TORSION, pattern S_PUPIL   , pattern SCAVENGER+  , pattern S_TOOTH, pattern S_LASH, pattern S_RIGHT_TORSION, pattern S_LEFT_TORSION, pattern S_PUPIL   , organsGNSingleton, organsGN   , -- * Content     organs@@ -15,11 +15,14 @@  import Content.ItemKindBlast import Content.ItemKindTemporary+import Content.RuleKind import Game.LambdaHack.Content.ItemKind+import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Core.Dice import Game.LambdaHack.Definition.Ability import Game.LambdaHack.Definition.Color import Game.LambdaHack.Definition.Defs+import Game.LambdaHack.Definition.DefsInternal import Game.LambdaHack.Definition.Flavour  -- * Group name patterns@@ -87,6 +90,8 @@ pattern S_SPEED_GLAND_5 = GroupName "speed gland 5" pattern S_SPEED_GLAND_10 = GroupName "speed gland 10" +pattern SCAVENGER = GroupName "scavenger"+ -- * LH-specific pattern S_TOOTH = GroupName "tooth" pattern S_LASH = GroupName "lash"@@ -94,8 +99,6 @@ pattern S_LEFT_TORSION = GroupName "left torsion" pattern S_PUPIL = GroupName "pupil" -pattern SCAVENGER = GroupName "scavenger"- -- * Content  organs :: [ItemKind]@@ -108,11 +111,14 @@ -- LH-specific tooth, lash, torsionRight, torsionLeft, pupil :: ItemKind +symbolWand :: ContentSymbol ItemKind+symbolWand = rsymbolWand $ ritemSymbols standardRules  -- * No-cooldown melee damage organs without effects  thorn = fist-  { iname    = "thorn"+  { isymbol  = symbolWand+  , iname    = "thorn"   , ifreq    = [(S_THORN, 1)]   , icount   = 2 + 1 `d` 2  -- unrealistic, but not boring   , iverbHit = "puncture"@@ -130,7 +136,7 @@   , idesc    = ""   } fist = ItemKind-  { isymbol  = ','+  { isymbol  = toContentSymbol ','   , iname    = "fist"   , ifreq    = [(S_FIST, 1)]   , iflavour = zipPlain [Red]@@ -202,7 +208,7 @@   , ifreq    = [(S_HORN, 1)]   , iverbHit = "impale"   , idamage  = 5 `d` 1-  , iaspects = [ AddSkill SkHurtMelee 10+  , iaspects = [ Timeout 7  -- no effect, but limit raw damage; two copies                , AddSkill SkArmorMelee 10 ]  -- bonus doubled                ++ iaspects fist   , idesc    = "Sharp and long, for defence or attack."@@ -221,7 +227,8 @@ -- * Direct damage organs with effects  beeSting = fist-  { iname    = "bee sting"+  { isymbol  = symbolWand+  , iname    = "bee sting"   , ifreq    = [(S_BEE_STING, 1)]   , icount   = 1   , iverbHit = "sting"@@ -233,7 +240,8 @@   , idesc    = "Painful, but beneficial."   } sting = fist-  { iname    = "sting"+  { isymbol  = symbolWand+  , iname    = "sting"   , ifreq    = [(S_STING, 1)]   , icount   = 1   , iverbHit = "inject"@@ -255,7 +263,8 @@   , idesc    = ""   } venomTooth = fist-  { iname    = "venom tooth"+  { isymbol  = symbolWand+  , iname    = "venom tooth"   , ifreq    = [(S_VENOM_TOOTH, 1)]   , iverbHit = "bite"   , idamage  = 1 `d` 1@@ -265,7 +274,8 @@   , idesc    = "A chilling numbness spreads from its bite."   } hookedClaw = fist-  { iname    = "hooked claw"+  { isymbol  = symbolWand+  , iname    = "hooked claw"   , ifreq    = [(S_HOOKED_CLAW, 1)]   , icount   = 2  -- even if more, only the fore claws used for fighting   , iverbHit = "hook"@@ -276,7 +286,8 @@   , idesc    = "A curved talon."   } screechingBeak = fist-  { iname    = "screeching beak"+  { isymbol  = symbolWand+  , iname    = "screeching beak"   , ifreq    = [(S_SCREECHING_BEAK, 1)]   , icount   = 1   , iverbHit = "peck"@@ -287,7 +298,8 @@   , idesc    = "Both a weapon and a beacon, calling more scavengers to the meal."   } antler = fist-  { iname    = "antler"+  { isymbol  = symbolWand+  , iname    = "antler"   , ifreq    = [(S_ANTLER, 1)]   , iverbHit = "ram"   , idamage  = 4 `d` 1@@ -298,7 +310,8 @@   , idesc    = ""   } rhinoHorn = fist-  { iname    = "ugly horn"  -- made of keratin, unlike real horns+  { isymbol  = symbolWand+  , iname    = "ugly horn"  -- made of keratin, unlike real horns   , ifreq    = [(S_RHINO_HORN, 1)]   , icount   = 1  -- single, unlike real horns   , iverbHit = "gore"@@ -309,7 +322,8 @@   , idesc    = "Very solid, considering it has the same composition as fingernails."   } largeTail = fist-  { iname    = "large tail"+  { isymbol  = symbolWand+  , iname    = "large tail"   , ifreq    = [(S_LARGE_TAIL, 1)]   , icount   = 1   , iverbHit = "knock"@@ -321,7 +335,8 @@   , idesc    = "Almost as long as the trunk."   } hugeTail = largeTail-  { iname    = "huge tail"+  { isymbol  = symbolWand+  , iname    = "huge tail"   , ifreq    = [(S_HUGE_TAIL, 1)]   , iverbHit = "upend"   , iaspects = [Timeout $ 3 + 1 `d` 2, AddSkill SkHurtMelee 20]@@ -334,7 +349,8 @@ -- * Melee weapons without direct damage  venomFang = fist-  { iname    = "venom fang"+  { isymbol  = symbolWand+  , iname    = "venom fang"   , ifreq    = [(S_VENOM_FANG, 1)]   , iverbHit = "bite"   , idamage  = 0@@ -357,7 +373,8 @@   , idesc    = ""   } boilingFissure = fist-  { iname    = "fissure"+  { isymbol  = symbolWand+  , iname    = "fissure"   , ifreq    = [(S_BOILING_FISSURE, 1)]   , icount   = 5 + 1 `d` 5   , iverbHit = "hiss at"@@ -382,7 +399,7 @@ -- * Armor organs  armoredSkin = ItemKind-  { isymbol  = ','+  { isymbol  = toContentSymbol ','   , iname    = "armored skin"   , ifreq    = [(S_ARMORED_SKIN, 1)]   , iflavour = zipPlain [Red]@@ -505,11 +522,12 @@   } speedGland :: Int -> GroupName ItemKind -> ItemKind speedGland n grp = armoredSkin-  { iname    = "speed gland"+  { isymbol  = symbolWand+  , iname    = "speed gland"   , ifreq    = [(grp, 1)]   , iverbHit = "spit at"-  , iaspects = [ AddSkill SkSpeed $ intToDice n-               , Timeout $ intToDice (100 `div` n)+  , iaspects = [ Timeout $ intToDice (100 `div` n)+               , AddSkill SkSpeed $ intToDice n                , SetFlag Periodic, SetFlag Durable ]   , ieffects = [RefillHP 1]   , idesc    = ""@@ -517,7 +535,8 @@ speedGland5 = speedGland 5 S_SPEED_GLAND_5 speedGland10 = speedGland 10 S_SPEED_GLAND_10 scentGland = armoredSkin-  { iname    = "scent gland"+  { isymbol  = symbolWand+  , iname    = "scent gland"   , ifreq    = [(S_SCENT_GLAND, 1)]   , icount   = 10 + 1 `d` 3  -- runs out   , iverbHit = "spray at"@@ -531,7 +550,8 @@   , idesc    = ""   } sulfurVent = armoredSkin-  { iname    = "vent"+  { isymbol  = toContentSymbol 'v'+  , iname    = "vent"   , ifreq    = [(S_SULFUR_VENT, 1)]   , iflavour = zipPlain [BrYellow]   , iverbHit = "menace"@@ -541,7 +561,8 @@   , idesc    = ""   } boilingVent = armoredSkin-  { iname    = "vent"+  { isymbol  = toContentSymbol 'v'+  , iname    = "vent"   , ifreq    = [(S_BOILING_VENT, 1)]   , iflavour = zipPlain [Blue]   , iverbHit = "menace"@@ -551,7 +572,8 @@   , idesc    = ""   } arsenicVent = armoredSkin-  { iname    = "vent"+  { isymbol  = toContentSymbol 'v'+  , iname    = "vent"   , ifreq    = [(S_ARSENIC_VENT, 1)]   , iflavour = zipPlain [Cyan]   , iverbHit = "menace"@@ -564,7 +586,7 @@ -- * Special  bonusHP = armoredSkin-  { isymbol  = 'H'  -- '+' reserved for conditions+  { isymbol  = toContentSymbol 'H'  -- '+' reserved for conditions   , iname    = "bonus HP"   , ifreq    = [(S_BONUS_HP, 1)]   , iflavour = zipPlain [BrBlue]@@ -574,7 +596,7 @@   , idesc    = "Growing up in a privileged background gave you the training and the discrete garment accessories that improve your posture and resilience."   } braced = armoredSkin-  { isymbol  = 'B'+  { isymbol  = toContentSymbol 'B'   , iname    = "braced"   , ifreq    = [(S_BRACED, 1)]   , iflavour = zipPlain [BrGreen]@@ -586,7 +608,7 @@   , idesc    = "Apart of increased resilience to attacks, being braced protects from displacement by foes and other forms of forced translocation, e.g., pushing or pulling."   } asleep = armoredSkin-  { isymbol  = 'S'+  { isymbol  = toContentSymbol 'S'   , iname    = "asleep"   , ifreq    = [(S_ASLEEP, 1)]   , iflavour = zipPlain [BrGreen]  -- regenerates HP (very slowly)@@ -600,7 +622,7 @@   , idesc    = "Sleep helps to regain health, albeit extremely slowly. Being asleep makes you vulnerable, with gradually diminishing effects as the slumber wears off over several turns. Any non-idle action, not only combat but even yawning or stretching removes a sizable portion of the sleepiness."   } impressed = armoredSkin-  { isymbol  = 'I'+  { isymbol  = toContentSymbol 'I'   , iname    = "impressed"  -- keep the same as in @ifreq@, to simplify code   , ifreq    = [(S_IMPRESSED, 1), (CONDITION, 1)]   , iflavour = zipPlain [BrRed]
GameDefinition/Content/ItemKindTemporary.hs view
@@ -18,6 +18,7 @@ import Game.LambdaHack.Definition.Ability import Game.LambdaHack.Definition.Color import Game.LambdaHack.Definition.Defs+import Game.LambdaHack.Definition.DefsInternal import Game.LambdaHack.Definition.Flavour  -- * Group name patterns@@ -98,7 +99,7 @@ tmpAspects grp aspects =   let name = fromGroupName grp  -- @iname@ must match @ifreq@, see @myBadGrps@   in ItemKind-    { isymbol  = '+'+    { isymbol  = toContentSymbol '+'     , iname    = name     , ifreq    = [(grp, 1), (CONDITION, 1)]     , iflavour = zipPlain [BrWhite]
GameDefinition/Content/ModeKind.hs view
@@ -23,6 +23,7 @@ import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Core.Dice import Game.LambdaHack.Definition.Defs+import Game.LambdaHack.Definition.DefsInternal  -- * Group name patterns @@ -132,7 +133,7 @@   , mtutorial = False   , mroster = rosterCrawl   , mcaves  = cavesCrawl-  , mendMsg = [ (Killed, "To think that followers of science and agents of enlightenment would earn death as their reward! Where did we err in our ways? Perhaps nature should not have been disturbed so brashly and the fell beasts woken up from their slumber so eagerly? Perhaps the gathered items should have been used for scientific experiments on the spot rather than hoarded as if of base covetousness? Or perhaps the challenge, chosen freely but without the foreknowledge of the grisly difficulty, was insurmountable and forlorn from the start, despite the enormous power of educated reason at out disposal?")+  , mendMsg = [ (Killed, "To think that followers of science and agents of enlightenment would earn death as their reward! Where did we err in our ways? Perhaps nature should not have been disturbed so brashly and the fell beasts woken up from their slumber so eagerly?\nPerhaps the gathered items should have been used for scientific experiments on the spot rather than hoarded as if of base covetousness? Or perhaps the challenge, chosen freely but without the foreknowledge of the grisly difficulty, was insurmountable and forlorn from the start, despite the enormous power of educated reason at out disposal?")               , (Escape, "It's better to live to tell the tale than to choke on more than one can swallow. There was no more exquisite cultural artifacts and glorious scientific wonders in these forbidding tunnels anyway. Or were there?") ]   , mrules  = T.intercalate "\n"       [ "* Many levels"@@ -198,7 +199,7 @@   , mtutorial = False   , mroster = rosterEscape   , mcaves  = cavesEscape-  , mendMsg = []+  , mendMsg = [ (Conquer, "It was enough to reach the escape area marked by yellow '>' symbol. Spilling that much blood was risky. unnecessary and alerted the authorities. Having said that --- impressive indeed.") ]   , mrules  = T.intercalate "\n"       [ "* One level only"       , "* Three heroes vs. Seven human enemies capable of concurrent attacks"@@ -487,16 +488,16 @@ rosterRaid, rosterBrawl, rosterCrawl, rosterShootout, rosterHunt, rosterEscape, rosterZoo, rosterAmbush, rosterSafari, rosterCrawlEmpty, rosterCrawlSurvival, rosterSafariSurvival, rosterBattle, rosterBattleDefense, rosterBattleSurvival, rosterDefense, rosterDefenseEmpty :: Roster  rosterRaid = Roster-  { rosterList = [ ( playerHero {fhiCondPoly = hiHeroShort}+  { rosterList = [ ( playerAnimal  -- starting over escape+                   , Nothing+                   , [(-2, 2, ANIMAL)] )+                 , ( playerHero {fhiCondPoly = hiHeroShort}                    , Just teamExplorer                    , [(-2, 2, HERO)] )                  , ( playerAntiHero { fname = "Indigo Founder"                                     , fhiCondPoly = hiHeroShort }                    , Just teamCompetitor                    , [(-2, 1, HERO)] )-                 , ( playerAnimal  -- starting over escape-                   , Nothing-                   , [(-2, 2, ANIMAL)] )                  , (playerHorror, Nothing, []) ]  -- for summoned monsters   , rosterEnemy = [ ("Explorer", "Animal Kingdom")                   , ("Explorer", "Horror Den")@@ -521,18 +522,18 @@   , rosterAlly = [] }  rosterCrawl = Roster-  { rosterList = [ ( playerHero-                   , Just teamExplorer-                   , [(-1, 3, HERO)] )-                 , ( playerMonster-                   , Nothing-                   , [(-4, 1, SCOUT_MONSTER), (-4, 3, MONSTER)] )-                 , ( playerAnimal+  { rosterList = [ ( playerAnimal  -- starting over escape                    , Nothing                    , -- Fun from the start to avoid empty initial level:                      [ (-1, 1 + 1 `d` 2, ANIMAL)                      -- Huge battle at the end:-                     , (-10, 100, MOBILE_ANIMAL) ] ) ]+                     , (-10, 100, MOBILE_ANIMAL) ] )+                 , ( playerHero  -- start on stairs so that stash is handy+                   , Just teamExplorer+                   , [(-1, 3, HERO)] )+                 , ( playerMonster+                   , Nothing+                   , [(-4, 1, SCOUT_MONSTER), (-4, 3, MONSTER)] ) ]   , rosterEnemy = [ ("Explorer", "Monster Hive")                   , ("Explorer", "Animal Kingdom") ]   , rosterAlly = [("Monster Hive", "Animal Kingdom")] }@@ -576,7 +577,6 @@ rosterEscape = Roster   { rosterList = [ ( playerAntiHero { fname = "Indigo Researcher"                                     , fcanEscape = False  -- start on escape-                                    , fneverEmpty = False  -- loot after killing                                     , fhiCondPoly = hiHeroMedium }                    , Just teamCompetitor                    , [(-7, 6, AMBUSHER_HERO), (-7, 1, SCOUT_HERO)] )@@ -670,16 +670,18 @@  rosterSafariSurvival = rosterSafari   { rosterList = [ ( playerMonsterTourist-                       { fleaderMode = LeaderAI $ AutoLeader True True-                       , fhasUI = False }+                       { fleaderMode = Just $ AutoLeader True True+                       , fhasUI = False+                       , funderAI = True }                    , Nothing                    , [(-4, 15, MONSTER)] )                  , ( playerHunamConvict                    , Just teamCivilian                    , [(-4, 3, CIVILIAN)] )                  , ( playerAnimalMagnificent-                       { fleaderMode = LeaderUI $ AutoLeader True False-                       , fhasUI = True }+                       { fleaderMode = Just $ AutoLeader True False+                       , fhasUI = True+                       , funderAI = False }                    , Nothing                    , [(-7, 20, MOBILE_ANIMAL)] )                  , ( playerAnimalExquisite@@ -702,11 +704,8 @@   , rosterAlly = [("Monster Hive", "Animal Kingdom")] }  rosterBattleDefense = rosterBattle-  { rosterList = [ ( playerHero { fcanEscape = False-                                , fhiCondPoly = hiHeroLong-                                , fleaderMode =-                                    LeaderAI $ AutoLeader False False-                                , fhasUI = False }+  { rosterList = [ ( playerAntiHero { fcanEscape = False+                                    , fhiCondPoly = hiHeroLong }                    , Just teamExplorer                    , [(-5, 5, SOLDIER_HERO)] )                  , ( playerMonster { fneverEmpty = True@@ -718,11 +717,8 @@                    , [(-5, 30, MOBILE_ANIMAL)] ) ] }  rosterBattleSurvival = rosterBattle-  { rosterList = [ ( playerHero { fcanEscape = False-                                , fhiCondPoly = hiHeroLong-                                , fleaderMode =-                                    LeaderAI $ AutoLeader False False-                                , fhasUI = False }+  { rosterList = [ ( playerAntiHero { fcanEscape = False+                                    , fhiCondPoly = hiHeroLong }                    , Just teamExplorer                    , [(-5, 5, SOLDIER_HERO)] )                  , ( playerMonster {fneverEmpty = True}
GameDefinition/Content/ModeKindPlayer.hs view
@@ -28,13 +28,15 @@   , fhiCondPoly = hiHeroLong   , fhasGender = True   , fdoctrine = TExplore-  , fleaderMode = LeaderUI $ AutoLeader False False+  , fleaderMode = Just $ AutoLeader False False   , fhasUI = True+  , funderAI = False   }  playerAntiHero = playerHero-  { fleaderMode = LeaderAI $ AutoLeader True False+  { fleaderMode = Just $ AutoLeader True False   , fhasUI = False+  , funderAI = True   }  playerCivilian = Player@@ -46,8 +48,9 @@   , fhiCondPoly = hiHeroMedium   , fhasGender = True   , fdoctrine = TPatrol-  , fleaderMode = LeaderNull  -- unorganized+  , fleaderMode = Nothing  -- unorganized   , fhasUI = False+  , funderAI = True   }  playerMonster = Player@@ -62,13 +65,15 @@   , fleaderMode =       -- No point changing leader on level, since all move and they       -- don't follow the leader.-      LeaderAI $ AutoLeader True True+      Just $ AutoLeader True True   , fhasUI = False+  , funderAI = True   }  playerAntiMonster = playerMonster-  { fleaderMode = LeaderUI $ AutoLeader True True+  { fleaderMode = Just $ AutoLeader True True   , fhasUI = True+  , funderAI = False   }  playerAnimal = Player@@ -80,8 +85,9 @@   , fhiCondPoly = hiDweller   , fhasGender = False   , fdoctrine = TRoam  -- can't pick up, so no point exploring-  , fleaderMode = LeaderNull+  , fleaderMode = Nothing   , fhasUI = False+  , funderAI = True   }  -- | A special player, for summoned actors that don't belong to any@@ -98,8 +104,9 @@   , fhiCondPoly = []   , fhasGender = False   , fdoctrine = TPatrol  -- disoriented-  , fleaderMode = LeaderNull+  , fleaderMode = Nothing   , fhasUI = False+  , funderAI = True   }  playerMonsterTourist =@@ -108,11 +115,13 @@                     , fneverEmpty = True  -- no spawning                     , fhiCondPoly = hiHeroMedium                     , fdoctrine = TFollow  -- follow-the-guide, as tourists do-                    , fleaderMode = LeaderUI $ AutoLeader False False }+                    , fleaderMode = Just $ AutoLeader False False+                    , funderAI = False }  playerHunamConvict =   playerCivilian { fname = "Hunam Convict"-                 , fleaderMode = LeaderAI $ AutoLeader True False }+                 , fleaderMode = Just $ AutoLeader True False+                 , funderAI = True }  playerAnimalMagnificent =   playerAnimal { fname = "Animal Magnificent Specimen Variety"
GameDefinition/Content/PlaceKind.hs view
@@ -13,12 +13,14 @@  import Game.LambdaHack.Core.Prelude +import qualified Data.EnumMap.Strict as EM import qualified Data.Text as T  import Content.TileKind hiding (content, groupNames, groupNamesSingleton) import Game.LambdaHack.Content.PlaceKind import Game.LambdaHack.Content.TileKind (TileKind) import Game.LambdaHack.Definition.Defs+import Game.LambdaHack.Definition.DefsInternal  -- * Group name patterns @@ -103,7 +105,33 @@       outdoorEscapes = map switchEscapeToOutdoorDown escapeDownBasic   in upEscapes ++ outdoorEscapes --- The dots below are @'\x00B7'@, as defined in @TileKind.floorSymbol@.+-- The dots below are @'\x00B7'@, as defined in `TileKind.floorSymbol`.+defaultLegendLit :: EM.EnumMap Char (GroupName TileKind)+defaultLegendLit = EM.fromList+  [ (' ', FILLER_WALL)+  , ('|', S_WALL_LIT)+  , ('-', S_WALL_HORIZONTAL_LIT)+  , ('0', S_PILLAR)+  , ('&', S_RUBBLE_PILE)+  , ('<', ESCAPE_UP)+  , ('>', ESCAPE_DOWN)+  , ('·', FLOOR_ACTOR_ITEM_LIT)+  , ('~', S_SHALLOW_WATER_LIT)+  , ('I', SIGNBOARD) ]++defaultLegendDark :: EM.EnumMap Char (GroupName TileKind)+defaultLegendDark = EM.fromList+  [ (' ', FILLER_WALL)+  , ('|', S_WALL_DARK)+  , ('-', S_WALL_HORIZONTAL_DARK)+  , ('0', S_PILLAR)+  , ('&', S_RUBBLE_PILE)+  , ('<', ESCAPE_UP)+  , ('>', ESCAPE_DOWN)+  , ('·', FLOOR_ACTOR_ITEM_DARK)+  , ('~', S_SHALLOW_WATER_DARK)+  , ('I', SIGNBOARD) ]+ deadEnd = PlaceKind  -- needs to have index 0   { psymbol  = 'd'   , pname    = "a dead end"@@ -112,8 +140,8 @@   , pcover   = CStretch   , pfence   = FNone   , ptopLeft = ["·"]-  , poverrideDark = []-  , poverrideLit = []+  , plegendDark = defaultLegendDark+  , plegendLit = defaultLegendLit   } rect = PlaceKind  -- Valid for any nonempty area, hence low frequency.   { psymbol  = 'r'@@ -125,26 +153,27 @@   , ptopLeft = [ "--"                , "|·"                ]-  , poverrideDark = []-  , poverrideLit = []+  , plegendDark = defaultLegendDark+  , plegendLit = defaultLegendLit   } rect2 = rect   { pname    = "a pen"   , pfreq    = [(ZOO, 3)]   }-rect3 = rect+rect3 = overridePlaceKind [ ('|', S_WALL_LIT)  -- visible from afar+                          , ('-', S_WALL_HORIZONTAL_LIT) ] $ rect   { pname    = "a shed"   , pfreq    = [(BRAWL, 10), (SHOOTOUT, 1)]-  , poverrideDark = [ ('|', S_WALL_LIT)  -- visible from afar-                    , ('-', S_WALL_HORIZONTAL_LIT) ]-  , poverrideLit = [ ('|', S_WALL_LIT)-                   , ('-', S_WALL_HORIZONTAL_LIT) ]   } rect4 = rect3   { pname    = "cabinet"   , pfreq    = [(ARENA, 10)]   }-rectWindows = PlaceKind+rectWindows = override2PlaceKind+                [ ('=', RECT_WINDOWS_HORIZONTAL_DARK)+                , ('!', RECT_WINDOWS_VERTICAL_DARK) ]+                [ ('=', RECT_WINDOWS_HORIZONTAL_LIT)+                , ('!', RECT_WINDOWS_VERTICAL_LIT) ] $ PlaceKind   { psymbol  = 'w'   , pname    = "a hut"   , pfreq    = [(ESCAPE, 10), (AMBUSH, 7)]@@ -154,12 +183,12 @@   , ptopLeft = [ "-="                , "!·"                ]-  , poverrideDark = [ ('=', RECT_WINDOWS_HORIZONTAL_DARK)-                    , ('!', RECT_WINDOWS_VERTICAL_DARK) ]-  , poverrideLit = [ ('=', RECT_WINDOWS_HORIZONTAL_LIT)-                   , ('!', RECT_WINDOWS_VERTICAL_LIT) ]+  , plegendDark = defaultLegendDark+  , plegendLit = defaultLegendLit   }-glasshouse = PlaceKind+glasshouse = overridePlaceKind+               [ ('=', GLASSHOUSE_HORIZONTAL_LIT)  -- visible from afar+               , ('!', GLASSHOUSE_VERTICAL_LIT) ] $ PlaceKind   { psymbol  = 'g'   , pname    = "a glasshouse"   , pfreq    = [(SHOOTOUT, 4)]@@ -169,24 +198,24 @@   , ptopLeft = [ "=="                , "!·"                ]-  , poverrideDark = [ ('=', GLASSHOUSE_HORIZONTAL_LIT)  -- visible from afar-                    , ('!', GLASSHOUSE_VERTICAL_LIT) ]-  , poverrideLit = [ ('=', GLASSHOUSE_HORIZONTAL_LIT)-                   , ('!', GLASSHOUSE_VERTICAL_LIT) ]+  , plegendDark = defaultLegendDark+  , plegendLit = defaultLegendLit   }-glasshouse2 = glasshouse+glasshouse2 = override2PlaceKind [ ('=', GLASSHOUSE_HORIZONTAL_DARK)+                                 , ('!', GLASSHOUSE_VERTICAL_DARK) ]+                                 [ ('=', GLASSHOUSE_HORIZONTAL_LIT)+                                 , ('!', GLASSHOUSE_VERTICAL_LIT) ] $ glasshouse   { pname    = "a glass cage"   , pfreq    = [(ZOO, 10)]-  , poverrideDark = [ ('=', GLASSHOUSE_HORIZONTAL_DARK)-                    , ('!', GLASSHOUSE_VERTICAL_DARK) ]-  , poverrideLit = [ ('=', GLASSHOUSE_HORIZONTAL_LIT)-                   , ('!', GLASSHOUSE_VERTICAL_LIT) ]   } glasshouse3 = glasshouse   { pname    = "a reading room"   , pfreq    = [(ARENA, 40)]   }-pulpit = PlaceKind+pulpit = overridePlaceKind [ ('=', GLASSHOUSE_HORIZONTAL_LIT)+                           , ('!', GLASSHOUSE_VERTICAL_LIT)+                           , ('0', S_PULPIT) ] $ PlaceKind+           -- except for floor, all will be lit, regardless of night/dark; OK   { psymbol  = 'p'   , pname    = "a stand dais"   , pfreq    = [(ARENA, 200), (ZOO, 200)]@@ -197,13 +226,8 @@                , "!··"                , "··0"                ]-  , poverrideDark = [ ('=', GLASSHOUSE_HORIZONTAL_LIT)-                    , ('!', GLASSHOUSE_VERTICAL_LIT)-                    , ('0', S_PULPIT) ]-  , poverrideLit = [ ('=', GLASSHOUSE_HORIZONTAL_LIT)-                   , ('!', GLASSHOUSE_VERTICAL_LIT)-                   , ('0', S_PULPIT) ]-      -- except for floor, this will all be lit, regardless of night/dark; OK+  , plegendDark = defaultLegendDark+  , plegendLit = defaultLegendLit   } ruin = PlaceKind   { psymbol  = 'R'@@ -215,16 +239,13 @@   , ptopLeft = [ "--"                , "|X"                ]-  , poverrideDark = []-  , poverrideLit = []+  , plegendDark = defaultLegendDark+  , plegendLit = defaultLegendLit   }-ruin2 = ruin+ruin2 = overridePlaceKind [ ('|', S_WALL_LIT)  -- visible from afar+                          , ('-', S_WALL_HORIZONTAL_LIT) ] $ ruin   { pname    = "blasted walls"   , pfreq    = [(AMBUSH, 50)]-  , poverrideDark = [ ('|', S_WALL_LIT)  -- visible from afar-                    , ('-', S_WALL_HORIZONTAL_LIT) ]-  , poverrideLit = [ ('|', S_WALL_LIT)-                   , ('-', S_WALL_HORIZONTAL_LIT) ]   } collapsed = PlaceKind   { psymbol  = 'c'@@ -237,8 +258,8 @@   , pfence   = FNone   , ptopLeft = [ "0"                ]-  , poverrideDark = []-  , poverrideLit = []+  , plegendDark = defaultLegendDark+  , plegendLit = defaultLegendLit   } collapsed2 = collapsed   { pfreq    = [(NOISE, 1000), (BATTLE, 200)]@@ -292,8 +313,8 @@                , "|·0·"                , "|···"                ]-  , poverrideDark = []-  , poverrideLit = []+  , plegendDark = defaultLegendDark+  , plegendLit = defaultLegendLit   } pillar2 = pillar   { pfreq    = [(ROGUE, 60), (LABORATORY, 200)]@@ -312,7 +333,7 @@                , "|····"                ]   }-pillar4 = pillar+pillar4 = overridePlaceKind [('&', CACHE)] $ pillar   { pname    = "an exquisite hall"   , pfreq    = [(ROGUE, 30000), (LABORATORY, 100000)]   , ptopLeft = [ "-----"@@ -321,10 +342,8 @@                , "|0·0·"                , "|····"                ]-  , poverrideDark = [('&', CACHE)]-  , poverrideLit = [('&', CACHE)]   }-pillar5 = pillar+pillar5 = overridePlaceKind [('&', CACHE)] $ pillar   { pname    = "a decorated hall"   , pfreq    = [(ROGUE, 30000), (LABORATORY, 100000)]   , ptopLeft = [ "-----"@@ -333,8 +352,6 @@                , "|0···"                , "|····"                ]-  , poverrideDark = [('&', CACHE)]-  , poverrideLit = [('&', CACHE)]   } colonnade = PlaceKind   { psymbol  = 'c'@@ -348,8 +365,8 @@   , ptopLeft = [ "0·"                , "··"                ]-  , poverrideDark = []-  , poverrideLit = []+  , plegendDark = defaultLegendDark+  , plegendLit = defaultLegendLit   } colonnade2 = colonnade   { prarity  = [(1, 15), (10, 15)]@@ -384,7 +401,8 @@                , "·0"                ]   }-lampPost = PlaceKind+lampPost = overridePlaceKind [ ('0', S_LAMP_POST)+                             , ('·', S_FLOOR_ACTOR_LIT) ] $ PlaceKind   { psymbol  = 'l'   , pname    = "a lamp-lit area"   , pfreq    = [(ESCAPE, 200), (AMBUSH, 200), (ZOO, 100), (BATTLE, 100)]@@ -395,8 +413,8 @@                , "·0·"                , "X·X"                ]-  , poverrideDark = [('0', S_LAMP_POST), ('·', S_FLOOR_ACTOR_LIT)]-  , poverrideLit = [('0', S_LAMP_POST), ('·', S_FLOOR_ACTOR_LIT)]+  , plegendDark = defaultLegendDark+  , plegendLit = defaultLegendLit   } lampPost2 = lampPost   { ptopLeft = [ "···"@@ -423,7 +441,11 @@                , "X···X"                ]   }-treeShade = PlaceKind+treeShade = override2PlaceKind [ ('0', S_TREE_DARK)+                               , ('s', TREE_SHADE_WALKABLE_DARK) ]+                               [ ('0', S_TREE_LIT)+                               , ('s', TREE_SHADE_WALKABLE_LIT) ] $+            overridePlaceKind [('·', S_SHADED_GROUND)] $ PlaceKind   { psymbol  = 't'   , pname    = "a tree shade"   , pfreq    = [(BRAWL, 1000)]@@ -434,14 +456,12 @@                , "s0·"                , "Xs·"                ]-  , poverrideDark = [ ('0', S_TREE_DARK)-                    , ('s', TREE_SHADE_WALKABLE_DARK)-                    , ('·', S_SHADED_GROUND) ]-  , poverrideLit = [ ('0', S_TREE_LIT)-                   , ('s', TREE_SHADE_WALKABLE_LIT)-                   , ('·', S_SHADED_GROUND) ]+  , plegendDark = defaultLegendDark+  , plegendLit = defaultLegendLit   }-fogClump = PlaceKind+fogClump = override2PlaceKind [('f', FOG_CLUMP_DARK)]+                              [('f', FOG_CLUMP_LIT)] $+           overridePlaceKind [(';', S_FOG_LIT)] $ PlaceKind   { psymbol  = 'f'   , pname    = "a foggy patch"   , pfreq    = [(SHOOTOUT, 150), (EMPTY, 15)]@@ -452,8 +472,8 @@                , ";f"                , ";X"                ]-  , poverrideDark = [('f', FOG_CLUMP_DARK), (';', S_FOG_LIT)]-  , poverrideLit = [('f', FOG_CLUMP_LIT), (';', S_FOG_LIT)]+  , plegendDark = defaultLegendDark+  , plegendLit = defaultLegendLit   } fogClump2 = fogClump   { pfreq    = [(SHOOTOUT, 500), (EMPTY, 50)]@@ -463,7 +483,11 @@                , "Xff"                ]   }-smokeClump = PlaceKind+smokeClump = override2PlaceKind [ ('f', SMOKE_CLUMP_DARK)+                                , ('·', S_FLOOR_ACTOR_DARK) ]+                                [ ('f', SMOKE_CLUMP_LIT)+                                , ('·', S_FLOOR_ACTOR_LIT) ] $+             overridePlaceKind [(';', S_SMOKE_LIT)] $ PlaceKind   { psymbol  = 's'   , pname    = "a smoky patch"   , pfreq    = [(ZOO, 50)]@@ -474,10 +498,8 @@                , ";f"                , ";X"                ]-  , poverrideDark = [ ('f', SMOKE_CLUMP_DARK), (';', S_SMOKE_LIT)-                    , ('·', S_FLOOR_ACTOR_DARK) ]-  , poverrideLit = [ ('f', SMOKE_CLUMP_LIT), (';', S_SMOKE_LIT)-                   , ('·', S_FLOOR_ACTOR_LIT) ]+  , plegendDark = defaultLegendDark+  , plegendLit = defaultLegendLit   } smokeClump2 = smokeClump   { pfreq    = [(ZOO, 500)]@@ -501,7 +523,9 @@       -- should not be used in caves with trails, because bushes should       -- not grow over such artificial trails   }-bushClump = PlaceKind+bushClump = override2PlaceKind [('f', BUSH_CLUMP_DARK)]+                               [('f', BUSH_CLUMP_LIT)] $+            overridePlaceKind [(';', S_BUSH_LIT)] $ PlaceKind   { psymbol  = 'b'   , pname    = "a bushy patch"   , pfreq    = [(SHOOTOUT, 40)]@@ -512,8 +536,8 @@                , ";X"                , ";;"                ]-  , poverrideDark = [('f', BUSH_CLUMP_DARK), (';', S_BUSH_LIT)]-  , poverrideLit = [('f', BUSH_CLUMP_LIT), (';', S_BUSH_LIT)]+  , plegendDark = defaultLegendDark+  , plegendLit = defaultLegendLit       -- should not be used in caves with trails, because bushes can't       -- grow over such artificial trails   }@@ -525,7 +549,8 @@                , ";;"                ]   }-escapeDown = PlaceKind+escapeDown = overridePlaceKind [ ('|', S_WALL_LIT)  -- visible from afar+                               , ('-', S_WALL_HORIZONTAL_LIT) ] $ PlaceKind   { psymbol  = '>'   , pname    = "an escape down"   , pfreq    = [(INDOOR_ESCAPE_DOWN, 1)]@@ -534,10 +559,8 @@   , pfence   = FGround   , ptopLeft = [ ">"                ]-  , poverrideDark = [ ('|', S_WALL_LIT)  -- visible from afar-                    , ('-', S_WALL_HORIZONTAL_LIT) ]-  , poverrideLit = [ ('|', S_WALL_LIT)-                   , ('-', S_WALL_HORIZONTAL_LIT) ]+  , plegendDark = defaultLegendDark+  , plegendLit = defaultLegendLit   } escapeDown2 = escapeDown   { pfreq    = [(INDOOR_ESCAPE_DOWN, 1000)]@@ -577,7 +600,10 @@                , "-----"                ]   }-staircase = PlaceKind+staircase = overridePlaceKind [ ('<', STAIRCASE_UP)+                              , ('>', STAIRCASE_DOWN)+                              , ('|', S_WALL_LIT)  -- visible from afar+                              , ('-', S_WALL_HORIZONTAL_LIT) ] $ PlaceKind   { psymbol  = '/'   , pname    = "a staircase"   , pfreq    = [(TINY_STAIRCASE, 1)]  -- no cover when arriving; low freq@@ -586,14 +612,8 @@   , pfence   = FGround   , ptopLeft = [ "<·>"                ]-  , poverrideDark = [ ('<', STAIRCASE_UP), ('>', STAIRCASE_DOWN)-                    , ('I', SIGNBOARD)-                    , ('|', S_WALL_LIT), ('-', S_WALL_HORIZONTAL_LIT) ]-                      -- seen from afar-  , poverrideLit = [ ('<', STAIRCASE_UP), ('>', STAIRCASE_DOWN)-                   , ('I', SIGNBOARD)-                   , ('|', S_WALL_LIT), ('-', S_WALL_HORIZONTAL_LIT) ]-                     -- seen from afar+  , plegendDark = defaultLegendDark+  , plegendLit = defaultLegendLit   } staircase1 = staircase   { prarity  = [(1, 1)]  -- no cover when arriving; so low rarity@@ -967,74 +987,54 @@   }  switchStaircaseToUp :: PlaceKind -> PlaceKind-switchStaircaseToUp s = s- { psymbol   = '<'- , pname     = pname s <+> "up"- , pfreq     = map (\(t, k) -> (GroupName $ fromGroupName t <+> "up", k))-               $ pfreq s- , poverrideDark = ('>', STAIR_TERMINAL_DARK)-                   : filter ((/= '>') . fst) (poverrideDark s)- , poverrideLit = ('>', STAIR_TERMINAL_LIT)-                  : filter ((/= '>') . fst) (poverrideLit s)- }+switchStaircaseToUp s = override2PlaceKind [('>', STAIR_TERMINAL_DARK)]+                                           [('>', STAIR_TERMINAL_LIT)] $ s+  { psymbol   = '<'+  , pname     = pname s <+> "up"+  , pfreq     = renameFreqs (<+> "up") $ pfreq s+  }  switchStaircaseToDown :: PlaceKind -> PlaceKind-switchStaircaseToDown s = s- { psymbol   = '>'- , pname     = pname s <+> "down"- , pfreq     = map (\(t, k) -> (GroupName $ fromGroupName t <+> "down", k))-               $ pfreq s- , poverrideDark = ('<', STAIR_TERMINAL_DARK)-                   : filter ((/= '<') . fst) (poverrideDark s)- , poverrideLit = ('<', STAIR_TERMINAL_LIT)-                  : filter ((/= '<') . fst) (poverrideLit s)- }+switchStaircaseToDown s = override2PlaceKind [('<', STAIR_TERMINAL_DARK)]+                                             [('<', STAIR_TERMINAL_LIT)] $ s+  { psymbol   = '>'+  , pname     = pname s <+> "down"+  , pfreq     = renameFreqs (<+> "down") $ pfreq s+  }  overrideGated :: [(Char, GroupName TileKind)] overrideGated =   [ ('<', GATED_STAIRCASE_UP), ('>', GATED_STAIRCASE_DOWN)-  , ('I', SIGNBOARD)   , ('|', S_WALL_LIT), ('-', S_WALL_HORIZONTAL_LIT) ]  -- visible from afar  switchStaircaseToGated :: PlaceKind -> PlaceKind-switchStaircaseToGated s = s- { psymbol   = 'g'- , pname     = T.unwords $ "a gated" : tail (T.words (pname s))- , pfreq     = map (first (\t -> GroupName $ "gated" <+> fromGroupName t))-               $ pfreq s- , poverrideDark = overrideGated- , poverrideLit = overrideGated- }+switchStaircaseToGated s = overridePlaceKind overrideGated $ s+  { psymbol   = 'g'+  , pname     = T.unwords $ "a gated" : tail (T.words (pname s))+  , pfreq     = renameFreqs ("gated" <+>) $ pfreq s+  }  overrideOutdoor :: [(Char, GroupName TileKind)] overrideOutdoor =   [ ('<', STAIRCASE_OUTDOOR_UP), ('>', STAIRCASE_OUTDOOR_DOWN)-  , ('I', SIGNBOARD)   , ('|', S_WALL_LIT), ('-', S_WALL_HORIZONTAL_LIT) ]  -- visible from afar  switchStaircaseToOutdoor :: PlaceKind -> PlaceKind-switchStaircaseToOutdoor s = s- { psymbol   = 'o'- , pname     = "an outdoor area exit"- , pfreq     = map (first (\t -> GroupName $ "outdoor" <+> fromGroupName t))-               $ pfreq s- , poverrideDark = overrideOutdoor- , poverrideLit = overrideOutdoor- }+switchStaircaseToOutdoor s = overridePlaceKind overrideOutdoor $ s+  { psymbol   = 'o'+  , pname     = "an outdoor area exit"+  , pfreq     = renameFreqs ("outdoor" <+>) $ pfreq s+  }  switchEscapeToUp :: PlaceKind -> PlaceKind-switchEscapeToUp s = s- { psymbol   = '<'- , pname     = "an escape up"- , pfreq     = map (\(_, n) -> (INDOOR_ESCAPE_UP, n)) $ pfreq s- , poverrideDark = ('>', ESCAPE_UP) : poverrideDark s- , poverrideLit = ('>', ESCAPE_UP) : poverrideLit s- }+switchEscapeToUp s = overridePlaceKind [('>', ESCAPE_UP)] $ s+  { psymbol   = '<'+  , pname     = "an escape up"+  , pfreq     = map (\(_, n) -> (INDOOR_ESCAPE_UP, n)) $ pfreq s+  }  switchEscapeToOutdoorDown :: PlaceKind -> PlaceKind-switchEscapeToOutdoorDown s = s- { pname     = "outdoor escape route"- , pfreq     = map (\(_, n) -> (OUTDOOR_ESCAPE_DOWN, n)) $ pfreq s- , poverrideDark = ('>', ESCAPE_OUTDOOR_DOWN) : poverrideDark s- , poverrideLit = ('>', ESCAPE_OUTDOOR_DOWN) : poverrideLit s- }+switchEscapeToOutdoorDown s = overridePlaceKind [('>', ESCAPE_OUTDOOR_DOWN)] $ s+  { pname     = "outdoor escape route"+  , pfreq     = map (\(_, n) -> (OUTDOOR_ESCAPE_DOWN, n)) $ pfreq s+  }
GameDefinition/Content/RuleKind.hs view
@@ -18,13 +18,15 @@ -- Cabal import qualified Paths_LambdaHack as Self (version) +import Game.LambdaHack.Content.ItemKind (ItemSymbolsUsedInEngine (..)) import Game.LambdaHack.Content.RuleKind+import Game.LambdaHack.Definition.DefsInternal  standardRules :: RuleContent standardRules = RuleContent   { rtitle = "LambdaHack"-  , rXmax = 80-  , rYmax = 21+  , rWidthMax = 80+  , rHeightMax = 21   , rexeVersion = Self.version   -- The strings containing the default configuration file   -- included from config.ui.default.@@ -45,5 +47,31 @@   , rscoresFile = "LambdaHack.scores"   , rnearby = 20   , rstairWordCarried = ["staircase"]  -- only one, so inert-  , rsymbolProjectile = '|'+  , ritemSymbols = ItemSymbolsUsedInEngine+      { rsymbolProjectile = toContentSymbol '|'+      , rsymbolLight      = toContentSymbol '('+      , rsymbolTool       = toContentSymbol '('+      , rsymbolSpecial    = toContentSymbol '*'+                              -- don't overuse; it clashes with projectiles+      , rsymbolGold       = toContentSymbol '$'+                              -- also gems+      , rsymbolNecklace   = toContentSymbol '"'+      , rsymbolRing       = toContentSymbol '='+      , rsymbolPotion     = toContentSymbol '!'+                              -- also concoction, bottle, jar, vial+      , rsymbolFlask      = toContentSymbol '!'+      , rsymbolScroll     = toContentSymbol '?'+                              -- also book, note, tablet, card+      , rsymbolTorsoArmor = toContentSymbol '['+      , rsymbolMiscArmor  = toContentSymbol '['+      , rsymbolClothes    = toContentSymbol '['+      , rsymbolShield     = toContentSymbol ']'+      , rsymbolPolearm    = toContentSymbol ')'+      , rsymbolEdged      = toContentSymbol ')'+      , rsymbolHafted     = toContentSymbol ')'+      , rsymbolWand       = toContentSymbol '/'+                              -- also magical rod, pistol, instrument+      , rsymbolFood       = toContentSymbol ','+          -- also body part; distinct enough from floor, which is middle dot+      }   }
GameDefinition/Content/TileKind.hs view
@@ -6,8 +6,8 @@     pattern FILLER_WALL, pattern FLOOR_CORRIDOR_LIT, pattern FLOOR_CORRIDOR_DARK, pattern TRAIL_LIT, pattern SAFE_TRAIL_LIT, pattern LAB_TRAIL_LIT, pattern DAMP_FLOOR_LIT, pattern DAMP_FLOOR_DARK, pattern OUTDOOR_OUTER_FENCE, pattern DIRT_LIT, pattern DIRT_DARK, pattern FLOOR_ARENA_LIT, pattern FLOOR_ARENA_DARK   , pattern EMPTY_SET_LIT, pattern EMPTY_SET_DARK, pattern NOISE_SET_LIT, pattern POWER_SET_LIT, pattern POWER_SET_DARK, pattern BATTLE_SET_LIT, pattern BATTLE_SET_DARK, pattern BRAWL_SET_LIT, pattern SHOOTOUT_SET_LIT, pattern ZOO_SET_LIT, pattern ZOO_SET_DARK, pattern ESCAPE_SET_LIT, pattern ESCAPE_SET_DARK, pattern AMBUSH_SET_LIT, pattern AMBUSH_SET_DARK, pattern ARENA_SET_LIT, pattern ARENA_SET_DARK     -- ** Used in PlaceKind, but not in CaveKind.-  , pattern RECT_WINDOWS_VERTICAL_LIT, pattern RECT_WINDOWS_VERTICAL_DARK, pattern RECT_WINDOWS_HORIZONTAL_LIT, pattern RECT_WINDOWS_HORIZONTAL_DARK, pattern TREE_SHADE_WALKABLE_LIT, pattern TREE_SHADE_WALKABLE_DARK, pattern SMOKE_CLUMP_LIT, pattern SMOKE_CLUMP_DARK, pattern GLASSHOUSE_VERTICAL_LIT, pattern GLASSHOUSE_VERTICAL_DARK, pattern GLASSHOUSE_HORIZONTAL_LIT, pattern GLASSHOUSE_HORIZONTAL_DARK, pattern BUSH_CLUMP_LIT, pattern BUSH_CLUMP_DARK, pattern FOG_CLUMP_LIT, pattern FOG_CLUMP_DARK, pattern STAIR_TERMINAL_LIT, pattern STAIR_TERMINAL_DARK, pattern CACHE, pattern SIGNBOARD, pattern STAIRCASE_UP, pattern ORDINARY_STAIRCASE_UP, pattern STAIRCASE_OUTDOOR_UP, pattern GATED_STAIRCASE_UP, pattern STAIRCASE_DOWN, pattern ORDINARY_STAIRCASE_DOWN, pattern STAIRCASE_OUTDOOR_DOWN, pattern GATED_STAIRCASE_DOWN, pattern ESCAPE_UP, pattern ESCAPE_DOWN, pattern ESCAPE_OUTDOOR_DOWN-  , pattern S_LAMP_POST, pattern S_TREE_LIT, pattern S_TREE_DARK, pattern S_WALL_LIT, pattern S_WALL_HORIZONTAL_LIT, pattern S_PULPIT, pattern S_BUSH_LIT, pattern S_FOG_LIT, pattern S_SMOKE_LIT, pattern S_FLOOR_ACTOR_LIT, pattern S_FLOOR_ACTOR_DARK, pattern S_FLOOR_ASHES_LIT, pattern S_FLOOR_ASHES_DARK, pattern S_SHADED_GROUND+  , pattern RECT_WINDOWS_VERTICAL_LIT, pattern RECT_WINDOWS_VERTICAL_DARK, pattern RECT_WINDOWS_HORIZONTAL_LIT, pattern RECT_WINDOWS_HORIZONTAL_DARK, pattern TREE_SHADE_WALKABLE_LIT, pattern TREE_SHADE_WALKABLE_DARK, pattern SMOKE_CLUMP_LIT, pattern SMOKE_CLUMP_DARK, pattern GLASSHOUSE_VERTICAL_LIT, pattern GLASSHOUSE_VERTICAL_DARK, pattern GLASSHOUSE_HORIZONTAL_LIT, pattern GLASSHOUSE_HORIZONTAL_DARK, pattern BUSH_CLUMP_LIT, pattern BUSH_CLUMP_DARK, pattern FOG_CLUMP_LIT, pattern FOG_CLUMP_DARK, pattern STAIR_TERMINAL_LIT, pattern STAIR_TERMINAL_DARK, pattern CACHE, pattern SIGNBOARD, pattern STAIRCASE_UP, pattern ORDINARY_STAIRCASE_UP, pattern STAIRCASE_OUTDOOR_UP, pattern GATED_STAIRCASE_UP, pattern STAIRCASE_DOWN, pattern ORDINARY_STAIRCASE_DOWN, pattern STAIRCASE_OUTDOOR_DOWN, pattern GATED_STAIRCASE_DOWN, pattern ESCAPE_UP, pattern ESCAPE_DOWN, pattern ESCAPE_OUTDOOR_DOWN, pattern FLOOR_ACTOR_ITEM_LIT, pattern FLOOR_ACTOR_ITEM_DARK+  , pattern S_PILLAR, pattern S_RUBBLE_PILE, pattern S_LAMP_POST, pattern S_TREE_LIT, pattern S_TREE_DARK, pattern S_WALL_LIT, pattern S_WALL_DARK, pattern S_WALL_HORIZONTAL_LIT, pattern S_WALL_HORIZONTAL_DARK, pattern S_PULPIT, pattern S_BUSH_LIT, pattern S_FOG_LIT, pattern S_SMOKE_LIT, pattern S_FLOOR_ACTOR_LIT, pattern S_FLOOR_ACTOR_DARK, pattern S_FLOOR_ASHES_LIT, pattern S_FLOOR_ASHES_DARK, pattern S_SHADED_GROUND, pattern S_SHALLOW_WATER_LIT, pattern S_SHALLOW_WATER_DARK   , groupNamesSingleton, groupNames     -- * Content   , content@@ -23,6 +23,7 @@ import Game.LambdaHack.Content.TileKind import Game.LambdaHack.Definition.Color import Game.LambdaHack.Definition.Defs+import Game.LambdaHack.Definition.DefsInternal  -- * Group name patterns @@ -30,18 +31,18 @@  groupNamesSingleton :: [GroupName TileKind] groupNamesSingleton =-       [S_LAMP_POST, S_TREE_LIT, S_TREE_DARK, S_WALL_LIT, S_WALL_HORIZONTAL_LIT, S_PULPIT, S_BUSH_LIT, S_FOG_LIT, S_SMOKE_LIT, S_FLOOR_ACTOR_LIT, S_FLOOR_ACTOR_DARK, S_FLOOR_ASHES_LIT, S_FLOOR_ASHES_DARK, S_SHADED_GROUND]-    ++ [S_SUSPECT_VERTICAL_WALL_LIT, S_SUSPECT_HORIZONTAL_WALL_LIT, S_CLOSED_VERTICAL_DOOR_LIT, S_CLOSED_HORIZONTAL_DOOR_LIT, S_OPEN_VERTICAL_DOOR_LIT, S_OPEN_HORIZONTAL_DOOR_LIT, S_RUBBLE_PILE, S_SHALLOW_WATER_LIT, S_SIGNBOARD_UNREAD]-    ++ [S_BUSH_DARK, S_CLOSED_HORIZONTAL_DOOR_DARK, S_CLOSED_VERTICAL_DOOR_DARK, S_OPEN_HORIZONTAL_DOOR_DARK, S_OPEN_VERTICAL_DOOR_DARK, S_SHALLOW_WATER_DARK, S_SUSPECT_HORIZONTAL_WALL_DARK, S_SUSPECT_VERTICAL_WALL_DARK, S_WALL_DARK, S_WALL_HORIZONTAL_DARK]+       [S_PILLAR, S_RUBBLE_PILE, S_LAMP_POST, S_TREE_LIT, S_TREE_DARK, S_WALL_LIT, S_WALL_DARK, S_WALL_HORIZONTAL_LIT, S_WALL_HORIZONTAL_DARK, S_PULPIT, S_BUSH_LIT, S_FOG_LIT, S_SMOKE_LIT, S_FLOOR_ACTOR_LIT, S_FLOOR_ACTOR_DARK, S_FLOOR_ASHES_LIT, S_FLOOR_ASHES_DARK, S_SHADED_GROUND, S_SHALLOW_WATER_LIT, S_SHALLOW_WATER_DARK]+    ++ [S_SUSPECT_VERTICAL_WALL_LIT, S_SUSPECT_HORIZONTAL_WALL_LIT, S_CLOSED_VERTICAL_DOOR_LIT, S_CLOSED_HORIZONTAL_DOOR_LIT, S_OPEN_VERTICAL_DOOR_LIT, S_OPEN_HORIZONTAL_DOOR_LIT, S_SIGNBOARD_UNREAD]+    ++ [S_BUSH_DARK, S_CLOSED_HORIZONTAL_DOOR_DARK, S_CLOSED_VERTICAL_DOOR_DARK, S_OPEN_HORIZONTAL_DOOR_DARK, S_OPEN_VERTICAL_DOOR_DARK, S_SUSPECT_HORIZONTAL_WALL_DARK, S_SUSPECT_VERTICAL_WALL_DARK]  -- ** Used in PlaceKind, but not in CaveKind.-pattern S_LAMP_POST, S_TREE_LIT, S_TREE_DARK, S_WALL_LIT, S_WALL_HORIZONTAL_LIT, S_PULPIT, S_BUSH_LIT, S_FOG_LIT, S_SMOKE_LIT, S_FLOOR_ACTOR_LIT, S_FLOOR_ACTOR_DARK, S_FLOOR_ASHES_LIT, S_FLOOR_ASHES_DARK, S_SHADED_GROUND :: GroupName TileKind+pattern S_PILLAR, S_RUBBLE_PILE, S_LAMP_POST, S_TREE_LIT, S_TREE_DARK, S_WALL_LIT, S_WALL_DARK, S_WALL_HORIZONTAL_LIT, S_WALL_HORIZONTAL_DARK, S_PULPIT, S_BUSH_LIT, S_FOG_LIT, S_SMOKE_LIT, S_FLOOR_ACTOR_LIT, S_FLOOR_ACTOR_DARK, S_FLOOR_ASHES_LIT, S_FLOOR_ASHES_DARK, S_SHADED_GROUND, S_SHALLOW_WATER_LIT, S_SHALLOW_WATER_DARK :: GroupName TileKind  -- ** Used only internally in other TileKind definitions or never used.-pattern S_SUSPECT_VERTICAL_WALL_LIT, S_SUSPECT_HORIZONTAL_WALL_LIT, S_CLOSED_VERTICAL_DOOR_LIT, S_CLOSED_HORIZONTAL_DOOR_LIT, S_OPEN_VERTICAL_DOOR_LIT, S_OPEN_HORIZONTAL_DOOR_LIT, S_RUBBLE_PILE, S_SHALLOW_WATER_LIT, S_SIGNBOARD_UNREAD :: GroupName TileKind+pattern S_SUSPECT_VERTICAL_WALL_LIT, S_SUSPECT_HORIZONTAL_WALL_LIT, S_CLOSED_VERTICAL_DOOR_LIT, S_CLOSED_HORIZONTAL_DOOR_LIT, S_OPEN_VERTICAL_DOOR_LIT, S_OPEN_HORIZONTAL_DOOR_LIT, S_SIGNBOARD_UNREAD :: GroupName TileKind  -- * Not used, but needed, because auto-generated. Singletons.-pattern S_BUSH_DARK, S_CLOSED_HORIZONTAL_DOOR_DARK, S_CLOSED_VERTICAL_DOOR_DARK, S_OPEN_HORIZONTAL_DOOR_DARK, S_OPEN_VERTICAL_DOOR_DARK, S_SHALLOW_WATER_DARK, S_SUSPECT_HORIZONTAL_WALL_DARK, S_SUSPECT_VERTICAL_WALL_DARK, S_WALL_DARK, S_WALL_HORIZONTAL_DARK :: GroupName TileKind+pattern S_BUSH_DARK, S_CLOSED_HORIZONTAL_DOOR_DARK, S_CLOSED_VERTICAL_DOOR_DARK, S_OPEN_HORIZONTAL_DOOR_DARK, S_OPEN_VERTICAL_DOOR_DARK, S_SUSPECT_HORIZONTAL_WALL_DARK, S_SUSPECT_VERTICAL_WALL_DARK :: GroupName TileKind  -- TODO: if we stick to the current system of generating extra kinds and their -- group names, let's also add the generated group names to @groupNames@.@@ -49,7 +50,7 @@ groupNames =        [FILLER_WALL, FLOOR_CORRIDOR_LIT, FLOOR_CORRIDOR_DARK, TRAIL_LIT, SAFE_TRAIL_LIT, LAB_TRAIL_LIT, DAMP_FLOOR_LIT, DAMP_FLOOR_DARK, OUTDOOR_OUTER_FENCE, DIRT_LIT, DIRT_DARK, FLOOR_ARENA_LIT, FLOOR_ARENA_DARK]     ++ [EMPTY_SET_LIT, EMPTY_SET_DARK, NOISE_SET_LIT, POWER_SET_LIT, POWER_SET_DARK, BATTLE_SET_LIT, BATTLE_SET_DARK, BRAWL_SET_LIT, SHOOTOUT_SET_LIT, ZOO_SET_LIT, ZOO_SET_DARK, ESCAPE_SET_LIT, ESCAPE_SET_DARK, AMBUSH_SET_LIT, AMBUSH_SET_DARK, ARENA_SET_LIT, ARENA_SET_DARK]-    ++ [RECT_WINDOWS_VERTICAL_LIT, RECT_WINDOWS_VERTICAL_DARK, RECT_WINDOWS_HORIZONTAL_LIT, RECT_WINDOWS_HORIZONTAL_DARK, TREE_SHADE_WALKABLE_LIT, TREE_SHADE_WALKABLE_DARK, SMOKE_CLUMP_LIT, SMOKE_CLUMP_DARK, GLASSHOUSE_VERTICAL_LIT, GLASSHOUSE_VERTICAL_DARK, GLASSHOUSE_HORIZONTAL_LIT, GLASSHOUSE_HORIZONTAL_DARK, BUSH_CLUMP_LIT, BUSH_CLUMP_DARK, FOG_CLUMP_LIT, FOG_CLUMP_DARK, STAIR_TERMINAL_LIT, STAIR_TERMINAL_DARK, CACHE, SIGNBOARD, STAIRCASE_UP, ORDINARY_STAIRCASE_UP, STAIRCASE_OUTDOOR_UP, GATED_STAIRCASE_UP, STAIRCASE_DOWN, ORDINARY_STAIRCASE_DOWN, STAIRCASE_OUTDOOR_DOWN, GATED_STAIRCASE_DOWN, ESCAPE_UP, ESCAPE_DOWN, ESCAPE_OUTDOOR_DOWN]+    ++ [RECT_WINDOWS_VERTICAL_LIT, RECT_WINDOWS_VERTICAL_DARK, RECT_WINDOWS_HORIZONTAL_LIT, RECT_WINDOWS_HORIZONTAL_DARK, TREE_SHADE_WALKABLE_LIT, TREE_SHADE_WALKABLE_DARK, SMOKE_CLUMP_LIT, SMOKE_CLUMP_DARK, GLASSHOUSE_VERTICAL_LIT, GLASSHOUSE_VERTICAL_DARK, GLASSHOUSE_HORIZONTAL_LIT, GLASSHOUSE_HORIZONTAL_DARK, BUSH_CLUMP_LIT, BUSH_CLUMP_DARK, FOG_CLUMP_LIT, FOG_CLUMP_DARK, STAIR_TERMINAL_LIT, STAIR_TERMINAL_DARK, CACHE, SIGNBOARD, STAIRCASE_UP, ORDINARY_STAIRCASE_UP, STAIRCASE_OUTDOOR_UP, GATED_STAIRCASE_UP, STAIRCASE_DOWN, ORDINARY_STAIRCASE_DOWN, STAIRCASE_OUTDOOR_DOWN, GATED_STAIRCASE_DOWN, ESCAPE_UP, ESCAPE_DOWN, ESCAPE_OUTDOOR_DOWN, FLOOR_ACTOR_ITEM_LIT, FLOOR_ACTOR_ITEM_DARK]     ++ [OBSCURED_VERTICAL_WALL_LIT, OBSCURED_HORIZONTAL_WALL_LIT, TRAPPED_VERTICAL_DOOR_LIT, TRAPPED_HORIZONAL_DOOR_LIT, TREE_BURNING_OR_NOT, BUSH_BURNING_OR_NOT, CACHE_OR_NOT]     ++ [BRAWL_SET_DARK, NOISE_SET_DARK, OBSCURED_HORIZONTAL_WALL_DARK, OBSCURED_VERTICAL_WALL_DARK, SHOOTOUT_SET_DARK, TRAPPED_HORIZONAL_DOOR_DARK, TRAPPED_VERTICAL_DOOR_DARK] @@ -58,7 +59,7 @@ pattern EMPTY_SET_LIT, EMPTY_SET_DARK, NOISE_SET_LIT, POWER_SET_LIT, POWER_SET_DARK, BATTLE_SET_LIT, BATTLE_SET_DARK, BRAWL_SET_LIT, SHOOTOUT_SET_LIT, ZOO_SET_LIT, ZOO_SET_DARK, ESCAPE_SET_LIT, ESCAPE_SET_DARK, AMBUSH_SET_LIT, AMBUSH_SET_DARK, ARENA_SET_LIT, ARENA_SET_DARK :: GroupName TileKind  -- ** Used in PlaceKind, but not in CaveKind.-pattern RECT_WINDOWS_VERTICAL_LIT, RECT_WINDOWS_VERTICAL_DARK, RECT_WINDOWS_HORIZONTAL_LIT, RECT_WINDOWS_HORIZONTAL_DARK, TREE_SHADE_WALKABLE_LIT, TREE_SHADE_WALKABLE_DARK, SMOKE_CLUMP_LIT, SMOKE_CLUMP_DARK, GLASSHOUSE_VERTICAL_LIT, GLASSHOUSE_VERTICAL_DARK, GLASSHOUSE_HORIZONTAL_LIT, GLASSHOUSE_HORIZONTAL_DARK, BUSH_CLUMP_LIT, BUSH_CLUMP_DARK, FOG_CLUMP_LIT, FOG_CLUMP_DARK, STAIR_TERMINAL_LIT, STAIR_TERMINAL_DARK, CACHE, SIGNBOARD, STAIRCASE_UP, ORDINARY_STAIRCASE_UP, STAIRCASE_OUTDOOR_UP, GATED_STAIRCASE_UP, STAIRCASE_DOWN, ORDINARY_STAIRCASE_DOWN, STAIRCASE_OUTDOOR_DOWN, GATED_STAIRCASE_DOWN, ESCAPE_UP, ESCAPE_DOWN, ESCAPE_OUTDOOR_DOWN :: GroupName TileKind+pattern RECT_WINDOWS_VERTICAL_LIT, RECT_WINDOWS_VERTICAL_DARK, RECT_WINDOWS_HORIZONTAL_LIT, RECT_WINDOWS_HORIZONTAL_DARK, TREE_SHADE_WALKABLE_LIT, TREE_SHADE_WALKABLE_DARK, SMOKE_CLUMP_LIT, SMOKE_CLUMP_DARK, GLASSHOUSE_VERTICAL_LIT, GLASSHOUSE_VERTICAL_DARK, GLASSHOUSE_HORIZONTAL_LIT, GLASSHOUSE_HORIZONTAL_DARK, BUSH_CLUMP_LIT, BUSH_CLUMP_DARK, FOG_CLUMP_LIT, FOG_CLUMP_DARK, STAIR_TERMINAL_LIT, STAIR_TERMINAL_DARK, CACHE, SIGNBOARD, STAIRCASE_UP, ORDINARY_STAIRCASE_UP, STAIRCASE_OUTDOOR_UP, GATED_STAIRCASE_UP, STAIRCASE_DOWN, ORDINARY_STAIRCASE_DOWN, STAIRCASE_OUTDOOR_DOWN, GATED_STAIRCASE_DOWN, ESCAPE_UP, ESCAPE_DOWN, ESCAPE_OUTDOOR_DOWN, FLOOR_ACTOR_ITEM_LIT, FLOOR_ACTOR_ITEM_DARK :: GroupName TileKind  -- ** Used only internally in other TileKind definitions or never used. pattern OBSCURED_VERTICAL_WALL_LIT, OBSCURED_HORIZONTAL_WALL_LIT, TRAPPED_VERTICAL_DOOR_LIT, TRAPPED_HORIZONAL_DOOR_LIT, TREE_BURNING_OR_NOT, BUSH_BURNING_OR_NOT, CACHE_OR_NOT :: GroupName TileKind@@ -135,13 +136,19 @@ pattern ESCAPE_UP = GroupName "escape up" pattern ESCAPE_DOWN = GroupName "escape down" pattern ESCAPE_OUTDOOR_DOWN = GroupName "escape outdoor down"+pattern FLOOR_ACTOR_ITEM_LIT = GroupName "floorActorItemLit"+pattern FLOOR_ACTOR_ITEM_DARK = GroupName "floorActorItemDark"  -- ** Used in PlaceKind, but not in CaveKind. Singletons.+pattern S_PILLAR = GroupName "pillar"+pattern S_RUBBLE_PILE = GroupName "rubble pile" pattern S_LAMP_POST = GroupName "lamp post" pattern S_TREE_LIT = GroupName "tree Lit" pattern S_TREE_DARK = GroupName "tree Dark" pattern S_WALL_LIT = GroupName "wall Lit"+pattern S_WALL_DARK = GroupName "wall Dark" pattern S_WALL_HORIZONTAL_LIT = GroupName "wall horizontal Lit"+pattern S_WALL_HORIZONTAL_DARK = GroupName "wall horizontal Dark" pattern S_PULPIT = GroupName "pulpit" pattern S_BUSH_LIT = GroupName "bush Lit" pattern S_FOG_LIT = GroupName "fog Lit"@@ -151,6 +158,8 @@ pattern S_FLOOR_ASHES_LIT = GroupName "floor with ashes Lit" pattern S_FLOOR_ASHES_DARK = GroupName "floor with ashes Dark" pattern S_SHADED_GROUND = GroupName "shaded ground"+pattern S_SHALLOW_WATER_LIT = GroupName "shallow water Lit"+pattern S_SHALLOW_WATER_DARK = GroupName "shallow water Dark"  -- ** Used only internally in other TileKind definitions. Not singletons. pattern OBSCURED_VERTICAL_WALL_LIT = GroupName "obscured vertical wall Lit"@@ -168,8 +177,6 @@ pattern S_CLOSED_HORIZONTAL_DOOR_LIT = GroupName "closed horizontal door Lit" pattern S_OPEN_VERTICAL_DOOR_LIT = GroupName "open vertical door Lit" pattern S_OPEN_HORIZONTAL_DOOR_LIT = GroupName "open horizontal door Lit"-pattern S_RUBBLE_PILE = GroupName "rubble pile"-pattern S_SHALLOW_WATER_LIT = GroupName "shallow water Lit" pattern S_SIGNBOARD_UNREAD = GroupName "signboard unread"  -- * Not used, but needed, because auto-generated. Not singletons.@@ -190,12 +197,9 @@ pattern S_CLOSED_VERTICAL_DOOR_DARK = GroupName "closed vertical door Dark" pattern S_OPEN_HORIZONTAL_DOOR_DARK = GroupName "open horizontal door Dark" pattern S_OPEN_VERTICAL_DOOR_DARK = GroupName "open vertical door Dark"-pattern S_SHALLOW_WATER_DARK = GroupName "shallow water Dark" pattern S_SUSPECT_HORIZONTAL_WALL_DARK =   GroupName "suspect horizontal wall Dark" pattern S_SUSPECT_VERTICAL_WALL_DARK = GroupName "suspect vertical wall Dark"-pattern S_WALL_DARK = GroupName "wall Dark"-pattern S_WALL_HORIZONTAL_DARK = GroupName "wall horizontal Dark"  -- * Content @@ -270,7 +274,7 @@ bedrock = TileKind   { tsymbol  = ' '   , tname    = "bedrock"-  , tfreq    = [(FILLER_WALL, 1), (LEGEND_LIT, 100), (LEGEND_DARK, 100)]+  , tfreq    = [(FILLER_WALL, 1)]   , tcolor   = defFG   , tcolor2  = defFG   , talter   = 100@@ -290,7 +294,7 @@ wall = TileKind   { tsymbol  = '|'   , tname    = "granite wall"-  , tfreq    = [ (LEGEND_LIT, 100), (S_WALL_LIT, 100)+  , tfreq    = [ (S_WALL_LIT, 100)                , (RECT_WINDOWS_VERTICAL_LIT, 80) ]   , tcolor   = BrWhite   , tcolor2  = defFG@@ -322,7 +326,7 @@ wallH = TileKind   { tsymbol  = '-'   , tname    = "sandstone wall"-  , tfreq    = [ (LEGEND_LIT, 100), (S_WALL_HORIZONTAL_LIT, 100)+  , tfreq    = [ (S_WALL_HORIZONTAL_LIT, 100)                , (RECT_WINDOWS_HORIZONTAL_LIT, 80) ]   , tcolor   = BrWhite   , tcolor2  = defFG@@ -366,9 +370,8 @@ pillar = TileKind   { tsymbol  = '0'   , tname    = "rock outcrop"-  , tfreq    = [ (CACHE_OR_NOT, 70)+  , tfreq    = [ (S_PILLAR, 1), (CACHE_OR_NOT, 70)                , (STAIR_TERMINAL_LIT, 100), (STAIR_TERMINAL_DARK, 100)-               , (LEGEND_LIT, 100), (LEGEND_DARK, 100)                , (EMPTY_SET_LIT, 20), (NOISE_SET_LIT, 700)                , (POWER_SET_DARK, 700)                , (BATTLE_SET_DARK, 200), (BRAWL_SET_LIT, 50)@@ -453,7 +456,7 @@ rubble = TileKind   { tsymbol  = '&'   , tname    = "rubble pile"-  , tfreq    = [ (S_RUBBLE_PILE, 1), (LEGEND_LIT, 1), (LEGEND_DARK, 1)+  , tfreq    = [ (S_RUBBLE_PILE, 1)                , (STAIR_TERMINAL_LIT, 4), (STAIR_TERMINAL_DARK, 4)                , (EMPTY_SET_LIT, 10), (EMPTY_SET_DARK, 10)                , (NOISE_SET_LIT, 50), (POWER_SET_DARK, 50)@@ -571,7 +574,7 @@ escapeUp = TileKind   { tsymbol  = '<'   , tname    = "exit hatch up"-  , tfreq    = [(LEGEND_LIT, 1), (LEGEND_DARK, 1), (ESCAPE_UP, 1)]+  , tfreq    = [(ESCAPE_UP, 1)]   , tcolor   = BrYellow   , tcolor2  = BrYellow   , talter   = 0  -- anybody can escape (or guard escape)@@ -580,7 +583,7 @@ escapeDown = TileKind   { tsymbol  = '>'   , tname    = "exit trapdoor down"-  , tfreq    = [(LEGEND_LIT, 1), (LEGEND_DARK, 1), (ESCAPE_DOWN, 1)]+  , tfreq    = [(ESCAPE_DOWN, 1)]   , tcolor   = BrYellow   , tcolor2  = BrYellow   , talter   = 0  -- anybody can escape (or guard escape)@@ -769,7 +772,7 @@   , tfeature = OftenActor : tfeature floorArena   } floorActorItem = floorActor-  { tfreq    = [(LEGEND_LIT, 100)]+  { tfreq    = [(FLOOR_ACTOR_ITEM_LIT, 1)]   , tfeature = VeryOftenItem : tfeature floorActor   } floorAshes = floorActor@@ -782,7 +785,7 @@ shallowWater = TileKind   { tsymbol  = '~'   , tname    = "water puddle"-  , tfreq    = [ (S_SHALLOW_WATER_LIT, 1), (LEGEND_LIT, 100)+  , tfreq    = [ (S_SHALLOW_WATER_LIT, 1)                , (EMPTY_SET_LIT, 5), (NOISE_SET_LIT, 20)                , (POWER_SET_LIT, 20), (SHOOTOUT_SET_LIT, 5) ]   , tcolor   = BrCyan
+ GameDefinition/InGameHelp.txt view
@@ -0,0 +1,179 @@+This is a snapshot of in-game help, rendered with the default config file.+For more general gameplay information see+https://github.com/LambdaHack/LambdaHack/blob/master/GameDefinition/PLAYING.md+++ Minimal cheat sheet for casual play++ Walk throughout a level with mouse or numeric keypad (right diagram below)+ or the Vi editor keys (middle) or the left-hand movement keys (left). Run until+ disturbed with Shift or Control. Go-to a position with LMB (left mouse button).+ In aiming mode, the same keys (and mouse) move the aiming crosshair.++      q w e     y k u     7 8 9+       \|/       \|/       \|/+      a-s-d     h-.-l     4-5-6+       /|\       /|\       /|\+      z x c     b j n     1 2 3++ Press `KP_5` (`5` on keypad) to wait, bracing for impact, which reduces any+ damage taken and prevents displacement by foes. Press `S-KP_5` or `C-KP_5`+ (the same key with Shift or Control) to lurk 0.1 of a turn, without bracing.++ Displace enemies by running into them with Shift/Control or S-LMB. Search,+ open, descend and melee by bumping into walls, doors, stairs and enemies.+ The best, and not on cooldown, melee weapon is automatically chosen+ for attack from your equipment and from among your body parts.++ The following few commands, joined with the movement and running keys,+ let you accomplish almost anything in the game, though not necessarily+ with the fewest keystrokes. You can also play the game exclusively+ with a mouse, or both mouse and keyboard (e.g., mouse for go-to+ and terrain inspection and keyboard for everything else). Lastly,+ you can select a command with arrows or mouse directly from the help+ screen or the dashboard and execute it on the spot.++  keys         command+  I            manage the shared inventory stash+  g or ,       grab item(s)+  ESC          clear messages/open main menu/finish aiming+  RET or INS   open dashboard/accept target+  SPACE        clear messages/show history/cycle detail level+  TAB          cycle among all party members+  *            cycle crosshair among enemies+  /            cycle crosshair among items+  M            modify any admissible terrain+  %            yell or yawn and stop sleeping+++ Optional mouse commands++ Screen area and UI mode (exploration/aiming) determine mouse click+ effects. Here we give an overview of effects of each button over+ the game map area. The list includes not only left and right buttons,+ but also the optional middle mouse button (MMB) and the mouse wheel,+ which is also used over menus to move selection. For mice without RMB,+ one can use Control key with LMB and for mice without MMB, one can use+ C-RMB or C-S-LMB.++  keys         command (exploration/aiming)+  LMB          go to pointer for 25 steps/fling at enemy+  S-LMB        run to pointer collectively for 25 steps/fling at enemy+  RMB or C-LMB start aiming at enemy under pointer/cycle detail level+  S-RMB        modify terrain at pointer+  MMB or C-RMB snap crosshair to floor under pointer/cycle detail level+  WHEEL-UP     swerve the aiming line+  WHEEL-DN     unswerve the aiming line++ Note that mouse is optional. Keyboard suffices, occasionally requiring+ a lookup for an obscure command key in help screens.+++ Mouse button effects per screen area, in exploration and in aiming modes++  Exploration    LMB (left mouse button)         RMB (right mouse button)+  message line   show history                    display help+  pointman tile  grab item(s)                    remove item(s)+  party on map   pick new pointman on screen     select party member on screen+  the map area   go to pointer for 25 steps      set crosshair to enemy+  level number   move aiming one level up        move aiming one level down+  level caption  open dashboard                  clear msgs and open main menu+  percent seen   explore nearest unknown spot    autoexplore 25 times+  crosshair info cycle crosshair among enemies   cycle crosshair among items+  party roster   pick new pointman on screen     select party member on screen+  Calm value     yell or yawn and stop sleeping  yell or yawn and stop sleeping+  HP gauge       rest (wait 25 times)            heed (lurk 0.1 turns 100 times)+  HP value       wait a turn, bracing for impact lurk 0.1 of a turn+  pointman info  auto-fling and keep choice      clear chosen item and crosshair++  Aiming Mode    LMB (left mouse button)         RMB (right mouse button)+  the map area   fling at enemy under pointer    snap crosshair to enemy+  level caption  accept target                   cancel aiming+  percent seen   aim at nearest upstairs         aim at nearest downstairs+++ All item-related commands++  keys         command+  I            manage the shared inventory stash+  O            manage the equipment outfit of the pointman+  g or ,       grab item(s)+  r            remove item(s)+  f            fling in-range projectile+  C-f          auto-fling and keep choice+  t            trigger consumable item+  C-t          trigger item and keep choice+  i            stash item into shared inventory+  o            equip item into outfit of the pointman+  G            manage items on the ground+  T            manage our total team belongings++ Note how lower case item commands (stash item, equip item) place items+ into a particular item store, while upper case item commands (manage Inventory,+ manage Outfit) open management menu for a store. Once a store menu is opened,+ you can switch stores with `<` and `>`, so the multiple commands only determine+ the starting item store. Each store is accessible from the dashboard as well.+++ Terrain exploration and modification commands++  keys         command+  TAB          cycle among all party members+  S-TAB        cycle backwards among all party members+  C-TAB        cycle among party members on the level+  C-S-TAB      cycle backwards among party members on the level+  m            modify door by closing it+  M            modify any admissible terrain+  =            select (or deselect) party member+  _            deselect (or select) all on the level+  ;            go to crosshair for 25 steps+  :            run to crosshair collectively for 25 steps+  [            explore nearest unknown spot+  ]            autoexplore 25 times+  R            rest (wait 25 times)+  C-R          heed (lurk 0.1 turns 100 times)+  0, 1 ... 9   pick a particular actor as the new pointman+++ All aiming commands++  keys         command+  ESC          clear messages/open main menu/finish aiming+  RET or INS   open dashboard/accept target+  SPACE        clear messages/show history/cycle detail level+  *            cycle crosshair among enemies+  /            cycle crosshair among items+  +            swerve the aiming line+  -            unswerve the aiming line+  \            cycle aiming modes+  C-?          set crosshair to nearest unknown spot+  C-/          set crosshair to nearest item+  C-{          aim at nearest upstairs+  C-}          aim at nearest downstairs+  <            move aiming one level up+  >            move aiming one level down+  BACKSPACE    clear chosen item and crosshair+++ Assorted commands++  keys         command+  %            yell or yawn and stop sleeping+  @            describe organs of the pointman+  #            show skill summary of the pointman+  ~            display relevant lore+  C-g          start new game+  C-x          save and exit to desktop+  C-q          quit game and start autoplay+  C-c          exit to desktop without saving+  ?            display help+  F1           display help immediately+  F12          open dashboard+  v            voice last action again+  V            voice recorded macro again+  '            start recording commands+  C-S          save game backup+  C-P          print screen+++ For more playing instructions see file PLAYING.md.
GameDefinition/Main.hs view
@@ -56,13 +56,9 @@   -- because they are not the source of the failure.   !serverOptions <- OA.execParser serverOptionsPI   resOrEx :: Either Ex.SomeException () <- Ex.try $ tieKnot serverOptions-  let unwrapEx e =-#if MIN_VERSION_async(2,2,1)-        case Ex.fromException e of-          Just (ExceptionInLinkedThread _ ex) -> unwrapEx ex-          _ ->-#endif-               e+  let unwrapEx e = case Ex.fromException e of+        Just (ExceptionInLinkedThread _ ex) -> unwrapEx ex+        _ -> e   case resOrEx of     Right () -> return ()     Left e -> case Ex.fromException $ unwrapEx e of
GameDefinition/PLAYING.md view
@@ -48,13 +48,13 @@   or shot down; less so explosions that are swarms of projectile particles   (turn-based just the same) * time passes and factions pursue their goals on a few levels-  simultaneously, while other levels are frozen (but all are persistent)+  simultaneously, while other floors are frozen (but all are persistent) * the same laws of simulated world apply to all factions and all actors,   whether player-controlled or AI-controlled; e.g., the same field of view   calculation, skill checks, equipment limitations, rules for item   and terrain use * combat mechanics is deterministic; randomness comes only from AI-  decisions and procedurally generated terrain+  decisions and procedurally generated world * there's (almost) no HP regeneration; attrition ensures all past (silly)   decisions matter; HP of every actor starts at around half max * each character has 10 uniform equipment slots, which fill quickly given@@ -77,29 +77,16 @@  Commands for starting a new game, saving and exiting the current game, configuring convenience settings and toggling AI control of the party-are listed in the main menu, brought up by the Esc key. Of the convenience-settings, the `suspect terrain` choice is of particular interest,-because it determines not only screen display of the level map,+are listed in the main menu, brought up by the Esc key.+Game difficulty level, from the new game setup menu, determines how hard+the survival in the game is. Each of the several named optional challenges+make the game additionally much harder, but usually simpler, as well.+Of the convenience settings, the `suspect terrain` choice is of particular+interest, because it determines not only screen display of the floor map, but also whether suspect tiles are considered for mouse go-to, auto-explore and for the `C-?` command that marks the nearest unexplored position.-Game difficulty, from the game setup menu, determines hitpoints at birth:-difficulty below 5 multiplies hitpoints of player characters, difficulty-over 5 multiplies hitpoints of their enemies. Each of the several named-optional challenges make the game much harder, but usually simpler, as well. -The "cold fish" challenge mode makes it impossible for player-characters to be healed by actors from other factions (this is-a significant restriction in the long crawl adventure).-The "ready goods" challenge mode disables crafting for the player,-which is however not a part of the LambdaHack game, so has no effect.-The "lone wolf" challenge mode reduces player's starting actors-to exactly one, though later on new heroes may join the party.-The "finder keeper" completely disables flinging projectiles-by the player, which affects not only ranged damage dealing,-but also throwing of consumables that buff teammates engaged in melee combat,-weaken and distract enemies, light dark corners, etc.--The game scenarios, as ordered by their number, lead the player along+Game scenarios, as ordered by their number, lead the player along an optional story arc. The first two adventures double as tutorials that offer rudimentary preparation for the main game, the long crawl. They gradually introduce exploration, stealth and melee combat,@@ -109,21 +96,22 @@ with hints about strategies known to work in the given tactical context. Alternatively, the player may postpone reading these messages and instead try to puzzle out the tactics himself --- this is not so hard, as there are-not so many moving parts to figure, at least in the short adventures.+not yet so many moving parts to figure out in the first two adventures. -As soon as the player learns to navigate initial levels of the crawl game-and starts employing ranged combat, light sources and other means-of gaining or denying battlefield intel, and dies a lot, it makes sense-to return to the remaining short adventures. They bring forth many extra-game features and tactics and prevent the player from missing half the fun-by trying to play the crawl just like a normal roguelike with spare heroes.+In the third scenario, the main 'crawl' game mode, the player starts+employing ranged combat, stealth, light sources, item and terrain alteration.+As soon as the player learns to navigate the initial levels of crawl,+but still dies a lot, it makes sense to return to the remaining+short adventures. They bring forth many extra game features+and tactics and prevent the player from missing half the fun by trying+to play the crawl just like a normal roguelike with spare heroes. The extra scenarios continue the plotline from the initial tutorial adventures in the form of flashbacks and eventually lead up to the events that start the main crawl adventure. The training they provide has narrow focus, drilling a particular skill set, even as exotic as opportunity fire-management, a frantic race to the exit and big asymmetric melee battle.+management, a frantic race to the exit and big asymmetric melee battles. The challenge the scenarios offer may be, accordingly, quite extreme,-particularly at higher difficulty levels and when striving for high scores.+particularly at higher difficulty settings and when striving for high scores.  The main adventure, the long crawl, is the only one that takes place in a multi-floor setting, requiring lots of time to beat.@@ -131,16 +119,15 @@ including terrain transformation using tools, spotting environment clues and guessing and countering opponents' strategies. The player has a choice of exploring a single level at a time or portions-of many levels along a single staircase. On some levels he may explore+of many floors along a single staircase. On some levels he may explore and loot with a single scout, eluding most opponents. On others he may be-forced to change the pace and perform a complete exterminatory sweep+forced to change pace and perform a complete exterminatory sweep involving his whole party. On yet others, his best course of action may be to defend a key location until the first wave of attackers is broken. The large game arena calls for strategic thinking, including resource-management, frantic races to the exit, big asymmetric melee battles.-Thus, the crawl scenario is the most replayable adventure, but even the small-ones can be refreshed by striving to beat a high score and then-ramping up the difficulty settings.+management and area denial. Thus, the crawl scenario is the most replayable+adventure, but even the small ones can be refreshed by striving to beat+a high score and by ramping up the difficulty settings.   Exploring the world@@ -193,7 +180,7 @@     smoke, fog, open fire                 ;     workshop, curtain, foliage            : -The four groups above, in turn, block movement but not view,+The four groups above, from top to bottom, block movement but not view, block both, block neither, block view but not movement. Additionally, each tile, regardless if open and if translucent, may be permanently lit with ambient light or not.@@ -245,7 +232,7 @@ is pressed, though that's usually not a precise enough method of controlling a team. Any sleeping hero is highlighted in blue and can be woken up by yelling with `%` key, which also taunts-or stresses up nearby enemies.+and unnerves nearby enemies.  Next on the bottom-most status line is the pointman's current and maximum Calm (morale, composure, focus, attentiveness), then his current@@ -253,13 +240,13 @@ into a dot signifies that the pointman is in a position without ambient illumination, making stealthy conduct easier. A brace sign instead of a colon after "HP" means the pointman is braced for combat-(see section [Moving and acting](#Moving-and-acting)).+(see chapter [Moving and acting](#Moving-and-acting)).  In the second half of the bottom-most status line, the pointman's name-is shown. Then come damage dice of the pointman's melee weapons and pointman's-appendages, ordered by their power. The dice of the first recharged weapon,-the one that is going to be used now, is adorned with percentage-damage bonus collected from the whole equipment of the pointman.+is shown. Then come damage dice of the pointman's melee weapons and+the pointman's appendages, ordered by their power. The dice of the first+recharged weapon, the one that is going to be used now, is adorned with+percentage damage bonus collected from the whole equipment of the pointman. If the dice are displayed with upper-case `D` instead of lower-case `d`, the weapon has additional effects apart of the usual direct damage. The nature of the effects can be appraised via the equipment outfit menu.@@ -273,11 +260,11 @@ the dice notation `xdy`, which denotes `x` rolls of `y`-sided dice. A variant written `xdLy` is additionally scaled by the level depth in proportion to the maximal level depth (at the first level the result-is always one, then it grows up to the full rolled value at the last level).+is always one; it grows up to the full rolled value at the last level). Section [Battling monsters](#Battling-monsters) below describes combat resolution in detail, including the role of the percentage bonuses. -The upper status line describes the current level in relation+The upper status line describes the currently visited level in relation to the party.      5  Lofty hall    [33% seen] dire basilisk    [__**]@@ -306,16 +293,16 @@ Moving and acting ----------------- -This section is a copy of the few initial bits of in-game help. The in-game-help pages are automatically generated based on a game's keybinding content-definitions and on overrides in the player's config file. The remaining-in-game help screens, not shown here, list all game commands grouped-by categories in detail. A text snapshot of the complete in-game help is in-[InGameHelp.txt](InGameHelp.txt).+This chapter is a copy of the few initial pages of in-game help.+The in-game help is automatically generated based on a game's keybinding+content definitions and on overrides in the player's config file.+The remaining in-game help screens, not shown here, list all game+commands grouped by categories in detail.  Walk throughout a level with mouse or numeric keypad (right diagram below) or the Vi editor keys (middle) or the left-hand movement keys (left). Run until disturbed with Shift or Control. Go-to a position with LMB (left mouse button).+In aiming mode, the same keys (and mouse) move the aiming crosshair.            q w e          y k u          7 8 9            \|/            \|/            \|/@@ -323,15 +310,14 @@            /|\            /|\            /|\           z x c          b j n          1 2 3 -In aiming mode, the same keys (and mouse) move the aiming crosshair. Press `KP_5` (`5` on keypad) to wait, bracing for impact, which reduces any damage taken and prevents displacement by foes. Press `S-KP_5` or `C-KP_5` (the same key with Shift or Control) to lurk 0.1 of a turn, without bracing.  Displace enemies by running into them with Shift/Control or S-LMB. Search,-open, descend and attack by bumping into walls, doors, stairs and enemies.-The best, not on cooldown, melee weapon is automatically chosen from your-equipment and from among your body parts.+open, descend and melee by bumping into walls, doors, stairs and enemies.+The best, and not on cooldown, melee weapon is automatically chosen+for attack from your equipment and from among your body parts.  The following few commands, joined with the movement and running keys, let you accomplish almost anything in the game, though not necessarily@@ -357,7 +343,7 @@ effects. Here we give an overview of effects of each button over the game map area. The list includes not only left and right buttons, but also the optional middle mouse button (MMB) and the mouse wheel,-which is also used over menus, to page-scroll them. For mice without RMB,+which is also used over menus to move selection. For mice without RMB, one can use Control key with LMB and for mice without MMB, one can use C-RMB or C-S-LMB. @@ -370,8 +356,8 @@     WHEEL-UP     swerve the aiming line     WHEEL-DN     unswerve the aiming line -Note that mouse is totally optional. Keyboard suffices, occasionally-requiring a lookup for some obscure command key in the help screens.+Note that mouse is optional. Keyboard suffices, occasionally requiring+a lookup for an obscure command key in help screens.   Battling monsters@@ -442,27 +428,30 @@ Attacking from a distance ------------------------- -For ranged attacks, setting the aiming crosshair before `f` to attack-is pressed is not mandatory. Crosshair is set automatically as soon+Before the player presses `f` to make a ranged attack, he may move+and set the aiming crosshair in aiming mode. However, this is+not often needed, since crosshair is set automatically as soon as a monster comes into view and can still be adjusted for as long-as the missile to fling is not chosen. However, sometimes before-flinging you want to examine the level map tile by tile by moving-the crosshair or assign persistent personal targets to party members.+as the missile to fling is not chosen.++Nevertheless, sometimes before flinging you want to examine+the level map tile by tile by moving the crosshair+or to assign persistent personal targets to party members. The latter is essential in the rare cases when your henchmen (non-pointman characters) can move autonomously or fire opportunistically (via innate skills or rare equipment). Also, if your non-pointman character is adjacent to more than one enemy, setting his target makes him melee a particular foe. -You can enter the aiming mode with the `*` and `KP_*` keys that-select enemies or the `/` and `KP_/` keys that cycle among items-on the floor. You can move crosshair with direction keys and assign-a personal target to the pointman with a `RET` key (Return, Enter).+You can enter the aiming mode with the `*` and `KP_*` keys that select+enemies or the `/` and `KP_/` keys that cycle among items on the level.+You can move crosshair with direction keys and assign a personal+target to the pointman with the `RET` key (Return, Enter). The details about the shared crosshair position are displayed in a status-line close to the bottom of the screen, as explained in section+line close to the bottom of the screen, as explained in chapter [Leading your heroes](#Leading-your-heroes) above. You cycle aiming-mode from foe to spot to vector with the ``\`` key, which is useful-when a monster vanishes but you still want to fling at its last+mode from foe to spot and to vector with the ``\`` key, which is useful,+e.g., when a monster vanishes but you still want to fling at its last known position.  @@ -507,14 +496,15 @@ A: Perhaps he's just sleeping (blue box indicates that)? If so, you can wake him up with the `%` command. If he's not asleep, his movement skill may be temporarily drained. Switch to another hero-or perform some other productive action that walking or wait-or rest with `R`.+or perform some other productive action different from walking+or wait with `KP_5` or rest with `R`.  - Q: Is autoexplore safe?  A: Not at all. It doesn't try to guess which hazardous terrain you want to avoid and which to barge through, so be prepared to abort exploration if open fire or slippery ground comes into view. Unless you have HP to spare.+Oh the other hand, running is very safe and go-to is rather safe.  - Q: Why does the percentage of explored tiles turn from 100% to 99%? @@ -534,7 +524,7 @@ and revealing their position or leaving a vantage point from which they can observe and relay enemy movement. For untrained teams, simultaneous synchronized squad movement is not feasible.-It would be practical, if all squaddies had cameras, with a few drones+It would be practical if all squaddies had cameras, with a few drones overhead for best effect, and if a team of off-site coordinators analyzed the situation and micromanaged them all. This is not the case here. @@ -563,8 +553,16 @@ of turns?  A: This is an artifact of time running independently on each level.-Any ideas on how to improve this are welcome. A workaround is to drop and then-pick up the item on the level you want to use it. When picked up,-it gets recharged after, randomly, from one to two times the normal-cooldown period of the item and then recharges normally for as long-as it's used on this level.+Any ideas on how to improve this game mechanics are welcome.+A workaround is to drop and then pick up the item on the level+you want to use it. When picked up, it gets recharged after, randomly,+from one to two times the normal cooldown period of the item+and then recharges normally while it's used on this level.++- Q: Why the bottom line displays a weapon with a timeout to the right+of a weapon without timeout? Doesn't it mean the former is never used?++A: Yes, it's never used and, quite possibly, it's your party inventory+management mistake and if not, at least a very special situation+and the display turns your attention to it. Shuffle the equipment+among your team if you want the weapon to get used.
GameDefinition/config.ui.default view
@@ -50,22 +50,27 @@ ; and/or to avoid moving due to accidental key presses. movementViKeys_hjklyubn = True movementLeftHandKeys_axwdqezc = True-; recommended: "binary" or "dejavuBold" or, if not a Debian package, "ubuntu"-chosenFontset = "binary"+; recommended: "binary" or "dejavuBold"+chosenFontset = "dejavuBold" ; 1.0 means don't scale; 1.5, 2.0, 3.0 give good results, <0.7 very bad ; for scales < 1 try fontsets with bolder auxiliary fonts and/or HintingLight allFontsScale = 1.0-; NotFullscreen (default), BigBorderlessWindow (preferred), ModeChange-; for 1920×1080 fullscreen, set allFontsScale = 1.5 above+; NotFullscreen (default), BigBorderlessWindow (preferred), ModeChange.+; For 1920×1080 fullscreen, set allFontsScale = 1.5 above. fullscreenMode = NotFullscreen-screenOneMessagePerLine = True-historyOneMessagePerLine = True+; HP percent at which warning is emitted.+hpWarningPercent = 20+; Wrap messages after this column (measured in small font, if available).+; In LambdaHack, not enough height to fit longer messages if wrapped earlier.+msgWrapColumn = 80 ; New historyMax takes effect after removal of savefiles. ; Looks best if is divisble by screenful of message (rheight - 4 = 20). historyMax = 5000+; Frames per second throttled at this value. maxFps = 24+; Animations when actors act are not displayed. noAnim = False-hpWarningPercent = 20+; Hardwired commandline arguments to process. overrideCmdline = ""  [fonts]@@ -76,9 +81,6 @@ dejavuRegular = FontProportional "DejaVuLGCSans.ttf.woff" 15 HintingHeavy dejavuBold = FontProportional "DejaVuLGCSans-Bold.ttf.woff" 13 HintingHeavy dejavuMono = FontMonospace "Hack-Bold.ttf.woff" 13 HintingHeavy-ubuntuRegular = FontProportional "ubuntu-v14-latin-ext_latin-regular.ttf.woff" 16 HintingLight-ubuntuBold = FontProportional "ubuntu-v14-latin-ext_latin-500.ttf.woff" 16 HintingLight-ubuntuMono = FontMonospace "ubuntu-mono-v9-latin-ext_latin-700.ttf.woff" 16 HintingLight ; the map fonts (the format is: kind name size hinting cellSizeAdd): ; with allFontsScale < 1, try HintingLight for fuzzy, but less distorted shapes: 16x16xwScalable = FontMapScalable "16x16xw.woff" 16 HintingHeavy 0@@ -88,26 +90,23 @@ 8x8xBitmap = FontMapBitmap "8x8x.fnt" 2  [fontsets]+; This is a temporary hack around bitmap font breakage caused by+; https://gitlab.freedesktop.org/freetype/freetype/-/issues/1076+; In a couple of years this can be removed and the small bitmap fonts will work+; fine again, too.+dejavuBold = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwScalable", fontPropRegular = "dejavuBold", fontPropBold = "dejavuBold", fontMono = "dejavuMono"}+ ; best proportional: binary = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "binaryRegular", fontPropBold = "binaryBold", fontMono = "binaryMono"}-dejavuBold = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "dejavuBold", fontPropBold = "dejavuBold", fontMono = "dejavuMono"}--; Warning: the Ubuntu Font Family fonts are considered non-free by Debian-; and so we don't include them in Debian and some other GNU/Linux packages.-; If you use such a package, feel free to download the fonts from our github-; repository or specify absolute paths to their locations in non-free Debian-; package fonts-ubuntu.-ubuntu = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "ubuntuRegular", fontPropBold = "ubuntuBold", fontMono = "ubuntuMono"}+dejavuBold_original = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "dejavuBold", fontPropBold = "dejavuBold", fontMono = "dejavuMono"}  ; decent proportional: binaryBold = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "binaryBold", fontPropBold = "binaryBold", fontMono = "binaryMono"} dejavu = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "dejavuRegular", fontPropBold = "dejavuBold", fontMono = "dejavuMono"}-ubuntuThin = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "ubuntuRegular", fontPropBold = "ubuntuRegular", fontMono = "ubuntuMono"}  ; monospace, for people that don't like proportional or many fonts: binaryMono = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "binaryMono", fontPropBold = "binaryMono", fontMono = "binaryMono"} dejavuMono = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "dejavuMono", fontPropBold = "dejavuMono", fontMono = "dejavuMono"}-ubuntuMono = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "ubuntuMono", fontPropBold = "ubuntuMono", fontMono = "ubuntuMono"}  ; square: 16x16xw = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "", fontPropBold = "", fontMono = ""}@@ -126,4 +125,4 @@ ; any more, the game crashes. To prevent that, one of the first three components ; of the game version should be bumped whenever fonts are changed. ; Configs from old versions are rejected, preventing the crash.-version = 0.10.2+version = 0.10.3
− GameDefinition/fonts/ubuntu-mono-v9-latin-ext_latin-700.ttf.woff

binary file changed (57416 → absent bytes)

− GameDefinition/fonts/ubuntu-v14-latin-ext_latin-500.ttf.woff

binary file changed (69288 → absent bytes)

− GameDefinition/fonts/ubuntu-v14-latin-ext_latin-regular.ttf.woff

binary file changed (76672 → absent bytes)

GameDefinition/game-src/Client/UI/Content/Input.hs view
@@ -29,19 +29,10 @@   -- Remember to put commands that show information (e.g., enter aiming   -- mode) first. -  -- Main menu-  [ ("s", ([CmdMainMenu], "setup and start new game>", ChallengeMenu))-  , ("x", ([CmdMainMenu], "save and exit to desktop", GameExit))-  , ("v", ([CmdMainMenu], "tweak convenience settings>", SettingsMenu))-  , ("t", ([CmdMainMenu], "toggle autoplay", AutomateToggle))-  , ("?", ([CmdMainMenu], "see command help", Help))-  , ("F12", ([CmdMainMenu], "switch to dashboard", Dashboard))-  , ("Escape", ([CmdMainMenu], "back to playing", AutomateBack))-   -- Minimal command set, in the desired presentation order.   -- A lot of these are not necessary, but may be familiar to new players.   -- Also a few non-minimal item commands to keep proper order.-  , ("I", ( [CmdMinimal, CmdItem, CmdDashboard]+  [ ("I", ( [CmdMinimal, CmdItem, CmdDashboard]           , "manage the shared inventory stash"           , ChooseItemMenu (MStore CStash) ))   , ("O", ( [CmdItem, CmdDashboard]@@ -70,7 +61,6 @@   , ("A-BackTab", memberCycleLevel Backward [])   , ("C-Tab", memberCycleLevel Forward [CmdMove])   , ("C-BackTab", memberCycleLevel Backward [CmdMove])-      -- TODO: the keys are too long to fit in help menu, unless vertically   , ("*", ( [CmdMinimal, CmdAim]           , "cycle crosshair among enemies"           , AimEnemy ))
GameDefinition/game-src/Client/UI/Content/Screen.hs view
@@ -20,7 +20,6 @@ standardLayoutAndFeatures = ScreenContent   { rwidth = 80   , rheight = 24-  , rwrap = 80  -- not enough height to fit longer messages if wrapped early   , rwebAddress = "http://lambdahack.github.io"   , rintroScreen = $(do       let path = "GameDefinition/PLAYING.md"@@ -48,9 +47,7 @@       lift intro)   , rapplyVerbMap = EM.fromList [('!', "quaff"), (',', "eat"), ('?', "read")]   , rFontFiles =--- Checking USE_SDL would be more accurate, but would require complicating--- .cabal file and slightly larger vty executable is not a problem.-#ifdef USE_JSFILE+#ifdef USE_BROWSER       [] #else       $(embedDir "GameDefinition/fonts")
GameDefinition/game-src/Implementation/MonadServerImplementation.hs view
@@ -137,7 +137,7 @@ -- initial states, in the @IO@ monad. executorSer :: COps -> CCUI -> ServerOptions -> UIOptions -> IO () executorSer cops@COps{corule} ccui soptionsNxtCmdline sUIOptions = do-  soptionsNxtRaw <- case uCmdline sUIOptions of+  soptionsNxtRaw <- case uOverrideCmdline sUIOptions of     []   -> return soptionsNxtCmdline     args -> handleParseResult $ execParserPure defaultPrefs serverOptionsPI args   -- Options for the clients modified with the configuration file.
GameDefinition/game-src/TieKnot.hs view
@@ -57,7 +57,7 @@   -- Set the X size of the dungeon from content ASAP, before it's used.   speedupHackXSizeThawed <- PA.unsafeThawPrimArray speedupHackXSize   PA.writePrimArray speedupHackXSizeThawed 0 $-    RK.rXmax Content.RuleKind.standardRules+    RK.rWidthMax Content.RuleKind.standardRules   void $ PA.unsafeFreezePrimArray speedupHackXSizeThawed   -- This setup ensures the boosting option doesn't affect generating initial   -- RNG for dungeon, etc., and also, that setting dungeon RNG on commandline@@ -69,7 +69,8 @@         if sboostRandomItem         then boostedItems ++ Content.ItemKind.otherItemContent         else Content.ItemKind.content-      coitem = IK.makeData itemContent+      coitem = IK.makeData (RK.ritemSymbols Content.RuleKind.standardRules)+                           itemContent                            Content.ItemKind.groupNamesSingleton                            Content.ItemKind.groupNames       coItemSpeedup = speedupItem coitem@@ -89,7 +90,8 @@         , comode = MK.makeData Content.ModeKind.content                                Content.ModeKind.groupNamesSingleton                                Content.ModeKind.groupNames-        , coplace = PK.makeData Content.PlaceKind.content+        , coplace = PK.makeData cotile+                                Content.PlaceKind.content                                 Content.PlaceKind.groupNamesSingleton                                 Content.PlaceKind.groupNames         , corule = RK.makeData Content.RuleKind.standardRules
+ LICENSE view
@@ -0,0 +1,26 @@+BSD 3-Clause License++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:+1. Redistributions of source code must retain the above copyright notice, this+   list of conditions and the following disclaimer.+2. Redistributions in binary form must reproduce the above copyright notice,+   this list of conditions and the following disclaimer in the documentation+   and/or other materials provided with the distribution.+3. Neither the name of the copyright holder nor the names of its+   contributors may be used to endorse or promote products derived from+   this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
LambdaHack.cabal view
@@ -1,4 +1,4 @@-cabal-version: 2.2+cabal-version: 2.4 name:          LambdaHack -- The package version. See the Haskell package versioning policy (PVP) -- for standards guiding when and how versions should be incremented.@@ -6,7 +6,7 @@ -- PVP summary:+-+------- breaking API changes --             | |  +----- minor or non-breaking API additions --             | |  | +--- code changes with no API change-version:       0.10.2.0+version:       0.10.3.0 synopsis:      A game engine library for tactical squad ASCII roguelike dungeon crawlers description: LambdaHack is a Haskell game engine library for ASCII roguelike              games of arbitrary theme, size and complexity, with optional@@ -44,11 +44,11 @@ bug-reports:   http://github.com/LambdaHack/LambdaHack/issues license:       BSD-3-Clause license-file:  COPYLEFT-tested-with:   GHC==8.2.2, GHC==8.4.4, GHC==8.6.5, GHC==8.8.4, GHC==8.10.4,+tested-with:   GHC==8.2.2, GHC==8.4.4, GHC==8.6.5, GHC==8.8.4, GHC==8.10.7,                GHC==9.0.1--- Enough files is included to build and run CI and manual tests--- and be presentable on Hackage, but not enough to create--- a user-friendly binary package, e.g., no InGameHelp.txt.+-- Enough files is included in the Hackage package to run CI and manual+-- tests off it, to be presentable on Hackage and to create user-friendly+-- binary packages. extra-source-files: GameDefinition/config.ui.default,                     GameDefinition/PLAYING.md,                     GameDefinition/fonts/16x16xw.woff,@@ -61,13 +61,15 @@                     GameDefinition/fonts/BinaryCodeProLH-Bold.ttf.woff,                     GameDefinition/fonts/DejaVuLGCSans.ttf.woff,                     GameDefinition/fonts/DejaVuLGCSans-Bold.ttf.woff,-                    GameDefinition/fonts/Hack-Bold.ttf.woff,-                    GameDefinition/fonts/ubuntu-v14-latin-ext_latin-regular.ttf.woff,-                    GameDefinition/fonts/ubuntu-v14-latin-ext_latin-500.ttf.woff,-                    GameDefinition/fonts/ubuntu-mono-v9-latin-ext_latin-700.ttf.woff,-                    Makefile,+                    GameDefinition/fonts/Hack-Bold.ttf.woff+extra-doc-files:    GameDefinition/InGameHelp.txt,+                    README.md,                     CHANGELOG.md,-                    README.md+                    LICENSE,+                    COPYLEFT,+                    CREDITS,+                    cabal.project,+                    Makefile author:        Andres Loeh, Mikolaj Konarski and others maintainer:    Mikolaj Konarski <mikolaj.konarski@funktory.com> category:      Game Engine, Game@@ -77,32 +79,6 @@   type:               git   location:           git://github.com/LambdaHack/LambdaHack.git -flag vty-  description:        switch to the vty frontend (screen reader friendly)-  default:            False-  manual:             True--flag curses-  description:        switch to the curses frontend (not fully supported)-  default:            False-  manual:             True--flag gtk-  description:        switch to the GTK frontend (not fully supported)-  default:            False-  manual:             True---- This flag is not used.--- flag sdl---   description:        switch to the SDL2 frontend---   default:            False---   manual:             True--flag jsaddle-  description:        switch to the JSaddle frontend (may be bit-rotted)-  default:            False-  manual:             True- flag with_expensive_assertions   description:        turn on expensive assertions of well-tested code   default:            False@@ -118,12 +94,17 @@   default:            True   manual:             True +flag jsaddle+  description:        switch to the JSaddle frontend (may be bit-rotted)+  default:            False+  manual:             True+ common options   default-language:   Haskell2010-  default-extensions: MonoLocalBinds, ScopedTypeVariables, OverloadedStrings+  default-extensions: MonoLocalBinds, ScopedTypeVariables, OverloadedStrings,                       BangPatterns, RecordWildCards, NamedFieldPuns, MultiWayIf,                       LambdaCase, DefaultSignatures, InstanceSigs,-                      PatternSynonyms, StrictData, CPP+                      PatternSynonyms, StrictData, CPP, TypeApplications   other-extensions:   TemplateHaskell, MultiParamTypeClasses, RankNTypes,                       TypeFamilies, FlexibleContexts, FlexibleInstances,                       DeriveFunctor, FunctionalDependencies,@@ -131,7 +112,8 @@                       DeriveFoldable, DeriveTraversable,                       ExistentialQuantification, GADTs, StandaloneDeriving,                       DataKinds, KindSignatures, DeriveGeneric, DeriveLift-  ghc-options:        -Wall -Wcompat -Worphans -Wincomplete-uni-patterns -Wincomplete-record-updates -Wimplicit-prelude -Wmissing-home-modules -Widentities -Wredundant-constraints -Wmissing-export-lists -Wpartial-fields+  ghc-options:        -Wall -Wcompat -Worphans -Wincomplete-uni-patterns -Wincomplete-record-updates -Wimplicit-prelude -Wmissing-home-modules -Widentities -Wredundant-constraints -Wmissing-export-lists -Wpartial-fields -Wunused-packages -Winvalid-haddock+-- TODO: remove -Winvalid-haddock when added to -Wall in a GHC I use for haddock   ghc-options:        -Wall-missed-specialisations   ghc-options:        -fno-ignore-asserts -fexpose-all-unfoldings -fspecialise-aggressively -fsimpl-tick-factor=200 @@ -147,7 +129,7 @@     ghcjs-options:    -DREMOVE_TELETYPE  common exe-options-  ghc-options:        -threaded -rtsopts+  ghc-options:        -rtsopts -- (Ignored by GHCJS) Minimize median lag at the cost of occasional bigger -- GC lag, which fortunately sometimes fits into idle time between turns): -- (Ignored by GHCJS) Avoid frequent GCs. Only idle-GC during a break in@@ -178,6 +160,7 @@                       Game.LambdaHack.Definition.Color                       Game.LambdaHack.Definition.ContentData                       Game.LambdaHack.Definition.Defs+                      Game.LambdaHack.Definition.DefsInternal                       Game.LambdaHack.Definition.Flavour                       Game.LambdaHack.Content.CaveKind                       Game.LambdaHack.Content.ItemKind@@ -215,12 +198,10 @@                       Game.LambdaHack.Client.UI.Content.Screen                       Game.LambdaHack.Client.UI.ContentClientUI                       Game.LambdaHack.Client.UI.DrawM-                      Game.LambdaHack.Client.UI.DisplayAtomicM                       Game.LambdaHack.Client.UI.EffectDescription                       Game.LambdaHack.Client.UI.Frame                       Game.LambdaHack.Client.UI.FrameM                       Game.LambdaHack.Client.UI.Frontend-                      Game.LambdaHack.Client.UI.Frontend.Chosen                       Game.LambdaHack.Client.UI.Frontend.Common                       Game.LambdaHack.Client.UI.Frontend.Teletype                       Game.LambdaHack.Client.UI.HandleHelperM@@ -244,6 +225,11 @@                       Game.LambdaHack.Client.UI.SlideshowM                       Game.LambdaHack.Client.UI.UIOptions                       Game.LambdaHack.Client.UI.UIOptionsParse+                      Game.LambdaHack.Client.UI.Watch+                      Game.LambdaHack.Client.UI.Watch.WatchCommonM+                      Game.LambdaHack.Client.UI.Watch.WatchQuitM+                      Game.LambdaHack.Client.UI.Watch.WatchSfxAtomicM+                      Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM                       Game.LambdaHack.Common.Analytics                       Game.LambdaHack.Common.Area                       Game.LambdaHack.Common.Actor@@ -293,6 +279,8 @@                       Game.LambdaHack.Server.ServerOptions                       Game.LambdaHack.Server.StartM                       Game.LambdaHack.Server.State+-- for doctest:+                      Paths_LambdaHack   exposed-modules:    Content.CaveKind                       Content.ItemKind                       Content.ItemKindEmbed@@ -310,10 +298,9 @@                       Client.UI.Content.Screen                       Implementation.MonadClientImplementation                       Implementation.MonadServerImplementation-  other-modules:      Paths_LambdaHack   autogen-modules:    Paths_LambdaHack   build-depends:      assert-failure >= 0.1.2 && < 0.2,-                      async      >= 2,+                      async      >= 2.2.1,                       base       >= 4.10 && < 99,                       base-compat >= 0.10.0,                       binary     >= 0.8,@@ -327,13 +314,14 @@                       ghc-prim,                       hashable   >= 1.1.2.5,                       hsini      >= 0.2,-                      int-cast   >= 0.2,+                      witch      >= 0.3,                       keys       >= 3,                       miniutter  >= 0.5.0.0,                       open-browser >= 0.2,                       optparse-applicative >= 0.13,                       pretty-show >= 1.6,                       primitive  >= 0.6.1.0,+                      QuickCheck,                       splitmix   >= 0.0.3,                       stm        >= 2.4,                       time       >= 1.4,@@ -347,25 +335,15 @@                       ghc-compact    if impl(ghcjs) || flag(jsaddle) {-    exposed-modules:    Game.LambdaHack.Client.UI.Frontend.Dom+    exposed-modules:  Game.LambdaHack.Client.UI.Frontend.Dom     build-depends:    ghcjs-dom >= 0.9.1.1     cpp-options:      -DUSE_BROWSER-  } elif flag(vty) {-    exposed-modules:    Game.LambdaHack.Client.UI.Frontend.Vty-    build-depends:    vty >= 5-    cpp-options:      -DUSE_VTY-  } elif flag(curses) {-    exposed-modules:    Game.LambdaHack.Client.UI.Frontend.Curses-    build-depends:    hscurses >= 1.4.1-    cpp-options:      -DUSE_CURSES-  } elif flag(gtk) {-    exposed-modules:    Game.LambdaHack.Client.UI.Frontend.Gtk-    build-depends:    gtk3 >= 0.12.1-    cpp-options:      -DUSE_GTK   } else {-    exposed-modules:    Game.LambdaHack.Client.UI.Frontend.Sdl-    build-depends:    sdl2 >= 2, sdl2-ttf >= 2-    cpp-options:      -DUSE_SDL+    exposed-modules:  Game.LambdaHack.Client.UI.Frontend.Sdl+                      Game.LambdaHack.Client.UI.Frontend.ANSI+    build-depends:    sdl2 >= 2,+                      sdl2-ttf >= 2,+                      ansi-terminal >= 0.10   }    if impl(ghcjs) {@@ -390,14 +368,31 @@   type:               exitcode-stdio-1.0   hs-source-dirs:     test   main-is:            Spec.hs-  other-modules:      SessionUIMock+  other-modules:      ItemDescriptionUnitTests+                      ItemKindUnitTests+                      ItemRevUnitTests+                      ReqFailureUnitTests+                      SessionUIMock+                      SessionUIUnitTests   build-depends:      ,LambdaHack-                      ,async                       ,base                       ,containers-                      ,filepath+                      ,enummapset                       ,optparse-applicative+                      ,splitmix                       ,tasty >= 1.0+                      ,tasty-quickcheck                       ,tasty-hunit                       ,text                       ,transformers+                      ,vector++test-suite doctests+  import: options, exe-options+  type:               exitcode-stdio-1.0+  main-is:            test/doctest-driver.hs+  build-tool-depends: doctest-driver-gen:doctest-driver-gen+  ghc-options:        -Wno-unused-packages+  build-depends:      ,base+                      ,doctest >= 0.13+                      ,QuickCheck
Makefile view
@@ -1,21 +1,20 @@-# some of this (not the parts run by CI) needs 'cabal install cabal-plan' play:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix play --dumpInitRngs+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix play --dumpInitRngs  shot:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix play --dumpInitRngs --printEachScreen+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix play --dumpInitRngs --printEachScreen  expose-lore:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix know --newGame 5 --dumpInitRngs --gameMode crawl --knowItems --exposePlaces --exposeItems --exposeActors --showItemSamples --benchmark --noAnim --maxFps 1000+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix know --newGame 5 --dumpInitRngs --gameMode crawl --knowItems --exposePlaces --exposeItems --exposeActors --showItemSamples --benchmark --noAnim --maxFps 1000  dig-lore:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix know --newGame 5 --dumpInitRngs --gameMode dig --knowItems --exposePlaces --exposeItems --exposeActors --showItemSamples --benchmark --noAnim --maxFps 1000+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix know --newGame 5 --dumpInitRngs --gameMode dig --knowItems --exposePlaces --exposeItems --exposeActors --showItemSamples --benchmark --noAnim --maxFps 1000  see-caves:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix know --newGame 5 --dumpInitRngs --gameMode see --knowItems --exposePlaces --exposeItems --exposeActors --showItemSamples --benchmark --noAnim --maxFps 1000+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix know --newGame 5 --dumpInitRngs --gameMode see --knowItems --exposePlaces --exposeItems --exposeActors --showItemSamples --benchmark --noAnim --maxFps 1000  short-caves:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix know --newGame 5 --dumpInitRngs --gameMode short --knowItems --exposePlaces --exposeItems --exposeActors --showItemSamples --benchmark --noAnim --maxFps 1000+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix know --newGame 5 --dumpInitRngs --gameMode short --knowItems --exposePlaces --exposeItems --exposeActors --showItemSamples --benchmark --noAnim --maxFps 1000  configure-debug: 	cabal configure --enable-profiling --profiling-detail=all-functions -fwith_expensive_assertions --disable-optimization@@ -33,10 +32,10 @@ 	google-chrome --no-sandbox --js-flags="--logfile=%t.log --prof" ../lambdahack.github.io/index.html  minific:-	npx google-closure-compiler dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.10.2.0/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js --compilation_level=ADVANCED_OPTIMIZATIONS --isolation_mode=IIFE --assume_function_wrapper --externs=dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.10.2.0/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js.externs --externs=/home/mikolaj/r/lambdahack.github.io/lz-string.extern.js --jscomp_off="*" > ../lambdahack.github.io/lambdahack.all.js+	npx google-closure-compiler dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.10.3.0/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js --compilation_level=ADVANCED_OPTIMIZATIONS --isolation_mode=IIFE --assume_function_wrapper --externs=dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.10.3.0/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js.externs --externs=/home/mikolaj/r/lambdahack.github.io/lz-string.extern.js --jscomp_off="*" > ../lambdahack.github.io/lambdahack.all.js  minificForNode:-	npx google-closure-compiler dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.10.2.0/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js --compilation_level=ADVANCED_OPTIMIZATIONS --isolation_mode=IIFE --assume_function_wrapper --externs=/home/mikolaj/r/lambdahack.github.io/lz-string.extern.js --externs=dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.10.2.0/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js.externs --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/assert.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/child_process.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/crypto.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/dns.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/events.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/globals.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/https.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/os.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/punycode.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/readline.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/stream.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/tls.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/url.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/vm.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/buffer.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/cluster.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/dgram.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/domain.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/fs.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/http.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/net.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/path.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/querystring.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/repl.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/string_decoder.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/tty.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/util.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/zlib.js --jscomp_off="*" > ../lambdahack.github.io/lambdahack.all.js+	npx google-closure-compiler dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.10.3.0/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js --compilation_level=ADVANCED_OPTIMIZATIONS --isolation_mode=IIFE --assume_function_wrapper --externs=/home/mikolaj/r/lambdahack.github.io/lz-string.extern.js --externs=dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.10.3.0/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js.externs --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/assert.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/child_process.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/crypto.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/dns.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/events.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/globals.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/https.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/os.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/punycode.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/readline.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/stream.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/tls.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/url.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/vm.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/buffer.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/cluster.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/dgram.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/domain.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/fs.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/http.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/net.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/path.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/querystring.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/repl.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/string_decoder.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/tty.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/util.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/zlib.js --jscomp_off="*" > ../lambdahack.github.io/lambdahack.all.js  # Low delay to display animations swiftly and not bore the public too much. # Delay can't be lower than 2, because browsers sometimes treat delay 1@@ -46,61 +45,61 @@ 	../gifsicle/src/gifsicle -O3 --careful -d2 --colors 255 --no-extensions --no-conserve-memory -l ~/.LambdaHack/screenshots/prtscn*.gif -o ~/.LambdaHack/screenshots/screenshot.gif  frontendRaid:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode raid --exposeActors+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --benchMessages --gameMode raid --exposeActors  frontendBrawl:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode brawl+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode brawl --benchMessages  frontendShootout:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode shootout+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode shootout  frontendHunt:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode hunt+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode hunt  frontendEscape:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 3 --dumpInitRngs --automateAll --gameMode escape+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 3 --dumpInitRngs --automateAll --gameMode escape  frontendZoo:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 2 --dumpInitRngs --automateAll --gameMode zoo --exposeActors+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 2 --dumpInitRngs --automateAll --gameMode zoo --exposeActors  frontendAmbush:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode ambush+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode ambush  frontendCrawl:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 1 --dumpInitRngs --automateAll --gameMode crawl --exposeItems --exposeActors+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 1 --dumpInitRngs --automateAll --gameMode crawl --exposeItems --exposeActors  frontendCrawlEmpty:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 1 --dumpInitRngs --automateAll --gameMode crawlEmpty+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 1 --dumpInitRngs --automateAll --gameMode crawlEmpty --maxFps 100000 --benchmark  frontendCrawlSurvival:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 9 --dumpInitRngs --automateAll --gameMode crawlSurvival --maxFps 1000+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 9 --dumpInitRngs --automateAll --gameMode crawlSurvival --maxFps 100000 --benchmark  frontendSafari:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode safari --exposeActors+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode safari --exposeActors  frontendSafariSurvival:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode safariSurvival --exposeActors+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode safariSurvival --exposeActors --maxFps 100000 --benchmark  frontendBattle:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 4 --dumpInitRngs --automateAll --gameMode battle --exposeActors+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 4 --dumpInitRngs --automateAll --gameMode battle --exposeActors  frontendBattleDefense:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 6 --dumpInitRngs --automateAll --gameMode battleDefense --exposeActors+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 6 --dumpInitRngs --automateAll --gameMode battleDefense --exposeActors --maxFps 100000 --benchmark  frontendBattleSurvival:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 6 --dumpInitRngs --automateAll --gameMode battleSurvival --exposeActors+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 6 --dumpInitRngs --automateAll --gameMode battleSurvival --exposeActors --maxFps 100000 --benchmark  frontendDefense:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 9 --dumpInitRngs --automateAll --gameMode defense --exposeItems --exposeActors+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 9 --dumpInitRngs --automateAll --gameMode defense --exposeItems --exposeActors  frontendDefenseEmpty:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 9 --dumpInitRngs --automateAll --gameMode defenseEmpty+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 9 --dumpInitRngs --automateAll --gameMode defenseEmpty --maxFps 100000 --benchmark  fastCrawl:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 1 --dumpInitRngs --automateAll --gameMode crawl --exposeItems --exposeActors --showItemSamples --noAnim --maxFps 100000 --benchmark+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 1 --dumpInitRngs --automateAll --gameMode crawl --exposeItems --exposeActors --showItemSamples --noAnim --maxFps 100000 --benchmark  slowCrawl:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 1 --dumpInitRngs --automateAll --gameMode crawl --exposeItems --exposeActors --showItemSamples+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 1 --dumpInitRngs --automateAll --gameMode crawl --exposeItems --exposeActors --showItemSamples  # different benchmarks use different arguments RNGOPTS=--setDungeonRng "SMGen 123 123" --setMainRng "SMGen 123 125"@@ -108,164 +107,163 @@ RNGOPTS2=--setDungeonRng "SMGen 129 123" --setMainRng "SMGen 129 125"  benchMemoryAnim:-	$$(cabal-plan list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 1 --maxFps 100000 --benchmark --stopAfterFrames 33000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS2)  --frontendNull --noAnim +RTS -s -A1M -RTS+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 1 --maxFps 100000 --benchmark --benchMessages --stopAfterFrames 33000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS2) --frontendLazy +RTS -s -A1M -RTS  benchBattle:-	./LambdaHack --dbgMsgSer --logPriority 4 --newGame 3 --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfterFrames 1500 --automateAll --keepAutomated --gameMode battle $(RNGOPTS1)+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 3 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 1500 --automateAll --keepAutomated --gameMode battle $(RNGOPTS1)  benchAnimBattle:-	./LambdaHack --dbgMsgSer --logPriority 4 --newGame 3 --maxFps 100000 --frontendNull --benchmark --stopAfterFrames 7000 --automateAll --keepAutomated --gameMode battle $(RNGOPTS1)+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 3 --maxFps 100000 --frontendLazy --benchmark --benchMessages --stopAfterFrames 7000 --automateAll --keepAutomated --gameMode battle $(RNGOPTS1)  benchFrontendBattle:-	./LambdaHack --dbgMsgSer --logPriority 4 --newGame 3 --noAnim --maxFps 100000 --benchmark --stopAfterFrames 2000 --automateAll --keepAutomated --gameMode battle $(RNGOPTS1)+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 3 --noAnim --maxFps 100000 --benchmark --benchMessages --stopAfterFrames 2000 --automateAll --keepAutomated --gameMode battle $(RNGOPTS1)  benchCrawl:-	./LambdaHack --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfterFrames 7000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS)+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 7000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS)  benchFrontendCrawl:-	./LambdaHack --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --benchmark --stopAfterFrames 7000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS)+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --benchmark --benchMessages --stopAfterFrames 7000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS)  benchDig:-	./LambdaHack --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfterFrames 1 --automateAll --keepAutomated --gameMode dig $(RNGOPTS)+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 1 --automateAll --keepAutomated --gameMode dig $(RNGOPTS)  benchNull: benchBattle benchAnimBattle benchCrawl  bench: benchBattle benchAnimBattle benchFrontendBattle benchCrawl benchFrontendCrawl  nativeBenchCrawl:-	$$(cabal-plan list-bin exe:LambdaHack)		   --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfterFrames 2000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS)+	$$(cabal list-bin exe:LambdaHack)		   --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 2000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS)  nativeBenchBattle:-	$$(cabal-plan list-bin exe:LambdaHack)		   --dbgMsgSer --logPriority 4 --newGame 3 --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfterFrames 1000 --automateAll --keepAutomated --gameMode battle $(RNGOPTS)+	$$(cabal list-bin exe:LambdaHack)		   --dbgMsgSer --logPriority 4 --newGame 3 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 1000 --automateAll --keepAutomated --gameMode battle $(RNGOPTS)  nativeBench: nativeBenchBattle nativeBenchCrawl  nodeBenchCrawl:-	node $$(cabal-plan list-bin exe:LambdaHack).jsexe/all.js --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfterFrames 2000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS)+	node $$(cabal list-bin exe:LambdaHack).jsexe/all.js --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 2000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS)  nodeBenchBattle:-	node $$(cabal-plan list-bin exe:LambdaHack).jsexe/all.js --dbgMsgSer --logPriority 4 --newGame 3 --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfterFrames 1000 --automateAll --keepAutomated --gameMode battle $(RNGOPTS)+	node $$(cabal list-bin exe:LambdaHack).jsexe/all.js --dbgMsgSer --logPriority 4 --newGame 3 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 1000 --automateAll --keepAutomated --gameMode battle $(RNGOPTS)  nodeBench: nodeBenchBattle nodeBenchCrawl  nodeMinifiedBench:-	node ../lambdahack.github.io/lambdahack.all.js --dbgMsgSer --logPriority 4 --newGame 3 --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfterFrames 1000 --automateAll --keepAutomated --gameMode battle $(RNGOPTS)-	node ../lambdahack.github.io/lambdahack.all.js --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfterFrames 2000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS)+	node ../lambdahack.github.io/lambdahack.all.js --dbgMsgSer --logPriority 4 --newGame 3 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 1000 --automateAll --keepAutomated --gameMode battle $(RNGOPTS)+	node ../lambdahack.github.io/lambdahack.all.js --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 2000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS)  -test-travis: test-short test-medium--test-travis2: test-sniff benchNull+test: test-short test-medium benchNull -test: test-sniff test-short test-medium benchNull+test-gha: test testCrawl-medium testCrawl-stopAfterGameOver testDefense-medium test-sniff  test-short: test-short-new test-short-load  test-medium: testRaid-medium testBrawl-medium testShootout-medium testHunt-medium testEscape-medium testZoo-medium testAmbush-medium testCrawlEmpty-medium testCrawl-medium-know testSafari-medium testSafariSurvival-medium testBattle-medium testBattleDefense-medium testBattleSurvival-medium testDig-medium testDefenseEmpty-medium testMany-teletype  test-sniff:-	bash -c "./LambdaHack --dbgMsgSer --logPriority 4 --newGame 5 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterFrames 1  --dumpInitRngs --automateAll --keepAutomated --gameMode raid --sniff &> /tmp/teletypetest.log"+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 5 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterFrames 1  --dumpInitRngs --automateAll --keepAutomated --gameMode raid --sniff > /tmp/teletypetest.log 2>&1  testMany-teletype:-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 9 --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 50 --dumpInitRngs --automateAll --keepAutomated 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 9 --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 50 --dumpInitRngs --automateAll --keepAutomated 2> /tmp/teletypetest.log  testMany-sdlInit:-	./LambdaHack --dbgMsgSer --logPriority 0 --boostRandomItem --newGame 9 --maxFps 100000 --benchmark --stopAfterSeconds 50 --dumpInitRngs --automateAll --keepAutomated 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 0 --boostRandomItem --newGame 9 --maxFps 100000 --benchmark --benchMessages --stopAfterSeconds 50 --dumpInitRngs --automateAll --keepAutomated 2> /tmp/teletypetest.log  testRaid-medium:-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode raid 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode raid 2> /tmp/teletypetest.log  testBrawl-medium:-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode brawl 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode brawl 2> /tmp/teletypetest.log  testShootout-medium:-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode shootout 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode shootout 2> /tmp/teletypetest.log  testHunt-medium:-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode hunt 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode hunt 2> /tmp/teletypetest.log  testEscape-medium:-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 3 --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode escape 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 3 --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode escape 2> /tmp/teletypetest.log  testZoo-medium:-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 2 --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode zoo 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 2 --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode zoo 2> /tmp/teletypetest.log  testAmbush-medium:-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode ambush 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --noAnim --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode ambush 2> /tmp/teletypetest.log  testCrawl-medium:-	./LambdaHack --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterGameOver --stopAfterSeconds 200 --dumpInitRngs --automateAll --keepAutomated --gameMode crawl --assertExplored 5 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 600 --dumpInitRngs --automateAll --keepAutomated --gameMode crawl --assertExplored 5 2> /tmp/teletypetest.log -testCrawl-medium-double:-	./LambdaHack --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendLazy --benchmark --stopAfterGameOver --stopAfterSeconds 590 --dumpInitRngs --automateAll --keepAutomated --gameMode crawl --assertExplored 4 +testCrawl-stopAfterGameOver:+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 9 --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 100 --dumpInitRngs --automateAll --keepAutomated --gameMode crawl --stopAfterGameOver 2> /tmp/teletypetest.log+ testCrawlEmpty-medium:-	./LambdaHack --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode crawlEmpty 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode crawlEmpty 2> /tmp/teletypetest.log  testCrawl-medium-know:-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix know --newGame 1 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 100 --dumpInitRngs --automateAll --keepAutomated --gameMode crawl --knowItems --exposePlaces --exposeItems --exposeActors --showItemSamples 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix know --newGame 1 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 100 --dumpInitRngs --automateAll --keepAutomated --gameMode crawl --knowItems --exposePlaces --exposeItems --exposeActors --showItemSamples 2> /tmp/teletypetest.log  testSafari-medium:-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 100 --dumpInitRngs --automateAll --keepAutomated --gameMode safari 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 100 --dumpInitRngs --automateAll --keepAutomated --gameMode safari 2> /tmp/teletypetest.log  testSafariSurvival-medium:-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode safariSurvival 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --noAnim --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode safariSurvival 2> /tmp/teletypetest.log  testBattle-medium:-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 3 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode battle 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 3 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode battle 2> /tmp/teletypetest.log  testBattleDefense-medium:-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 7 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode battleDefense 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 7 --noAnim --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode battleDefense 2> /tmp/teletypetest.log  testBattleSurvival-medium:-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 7 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode battleSurvival 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 7 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode battleSurvival 2> /tmp/teletypetest.log  testDefense-medium:-	./LambdaHack --dbgMsgSer --logPriority 4 --newGame 9 --noAnim --maxFps 100000 --frontendLazy --benchmark --stopAfterSeconds 100 --dumpInitRngs --automateAll --keepAutomated --gameMode defense 2> /tmp/teletypetest.log--testDig-medium:-	./LambdaHack --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterFrames 100 --dumpInitRngs --automateAll --keepAutomated --gameMode dig 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 9 --noAnim --maxFps 100000 --frontendLazy --benchmark --stopAfterSeconds 600 --dumpInitRngs --automateAll --keepAutomated --gameMode defense 2> /tmp/teletypetest.log  testDefenseEmpty-medium:-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 9 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode defenseEmpty 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 9 --noAnim --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode defenseEmpty 2> /tmp/teletypetest.log +testDig-medium:+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterFrames 100 --dumpInitRngs --automateAll --keepAutomated --gameMode dig 2> /tmp/teletypetest.log+ testCrawl-appveyor: 	./LambdaHack --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfterGameOver --stopAfterSeconds 300 --dumpInitRngs --automateAll --keepAutomated --gameMode crawl --assertExplored 5  testDefense-appveyor:-	./LambdaHack --dbgMsgSer --logPriority 4 --newGame 9 --noAnim --maxFps 100000 --frontendLazy --benchmark --stopAfterSeconds 600 --dumpInitRngs --automateAll --keepAutomated --gameMode defense+	./LambdaHack --dbgMsgSer --logPriority 4 --newGame 9 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterSeconds 600 --dumpInitRngs --automateAll --keepAutomated --gameMode defense  test-short-new:-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix raid --dumpInitRngs --automateAll --keepAutomated --gameMode raid --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix brawl --dumpInitRngs --automateAll --keepAutomated --gameMode brawl --showItemSamples --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix shootout --dumpInitRngs --automateAll --keepAutomated --gameMode shootout --showItemSamples --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix hunt --dumpInitRngs --automateAll --keepAutomated --gameMode hunt --showItemSamples --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix escape --dumpInitRngs --automateAll --keepAutomated --gameMode escape --showItemSamples --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix zoo --dumpInitRngs --automateAll --keepAutomated --gameMode zoo --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix ambush --dumpInitRngs --automateAll --keepAutomated --gameMode ambush --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix crawl --dumpInitRngs --automateAll --keepAutomated --gameMode crawl --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix safari --dumpInitRngs --automateAll --keepAutomated --gameMode safari --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix safariSurvival --dumpInitRngs --automateAll --keepAutomated --gameMode safariSurvival --showItemSamples --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix battle --showItemSamples --dumpInitRngs --automateAll --keepAutomated --gameMode battle --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix battleDefense --dumpInitRngs --automateAll --keepAutomated --gameMode battleDefense --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix battleSurvival --dumpInitRngs --automateAll --keepAutomated --gameMode battleSurvival --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix raid --dumpInitRngs --automateAll --keepAutomated --gameMode raid --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix brawl --dumpInitRngs --automateAll --keepAutomated --gameMode brawl --showItemSamples --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix shootout --dumpInitRngs --automateAll --keepAutomated --gameMode shootout --showItemSamples --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix hunt --dumpInitRngs --automateAll --keepAutomated --gameMode hunt --showItemSamples --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix escape --dumpInitRngs --automateAll --keepAutomated --gameMode escape --showItemSamples --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix zoo --dumpInitRngs --automateAll --keepAutomated --gameMode zoo --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix ambush --dumpInitRngs --automateAll --keepAutomated --gameMode ambush --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix crawl --dumpInitRngs --automateAll --keepAutomated --gameMode crawl --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix safari --dumpInitRngs --automateAll --keepAutomated --gameMode safari --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix safariSurvival --dumpInitRngs --automateAll --keepAutomated --gameMode safariSurvival --showItemSamples --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix battle --showItemSamples --dumpInitRngs --automateAll --keepAutomated --gameMode battle --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix battleDefense --dumpInitRngs --automateAll --keepAutomated --gameMode battleDefense --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix battleSurvival --dumpInitRngs --automateAll --keepAutomated --gameMode battleSurvival --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log  # $(RNGOPTS) is needed for determinism relative to seed # generated before game save test-short-load:-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix raid --dumpInitRngs --automateAll --keepAutomated --gameMode raid --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix brawl --dumpInitRngs --automateAll --keepAutomated --gameMode brawl --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix shootout --dumpInitRngs --automateAll --keepAutomated --gameMode shootout --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix hunt --dumpInitRngs --automateAll --keepAutomated --gameMode hunt --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix escape --dumpInitRngs --automateAll --keepAutomated --gameMode escape --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix zoo --dumpInitRngs --automateAll --keepAutomated --gameMode zoo --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix ambush --dumpInitRngs --automateAll --keepAutomated --gameMode ambush --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix crawl --dumpInitRngs --automateAll --keepAutomated --gameMode crawl --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix safari --dumpInitRngs --automateAll --keepAutomated --gameMode safari --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix safariSurvival --dumpInitRngs --automateAll --keepAutomated --gameMode safariSurvival --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix battle --dumpInitRngs --automateAll --keepAutomated --gameMode battle --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix battleDefense --dumpInitRngs --automateAll --keepAutomated --gameMode battleDefense --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log-	./LambdaHack --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix battleSurvival --dumpInitRngs --automateAll --keepAutomated --gameMode battleSurvival --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix raid --dumpInitRngs --automateAll --keepAutomated --gameMode raid --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix brawl --dumpInitRngs --automateAll --keepAutomated --gameMode brawl --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix shootout --dumpInitRngs --automateAll --keepAutomated --gameMode shootout --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix hunt --dumpInitRngs --automateAll --keepAutomated --gameMode hunt --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix escape --dumpInitRngs --automateAll --keepAutomated --gameMode escape --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix zoo --dumpInitRngs --automateAll --keepAutomated --gameMode zoo --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix ambush --dumpInitRngs --automateAll --keepAutomated --gameMode ambush --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix crawl --dumpInitRngs --automateAll --keepAutomated --gameMode crawl --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix safari --dumpInitRngs --automateAll --keepAutomated --gameMode safari --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix safariSurvival --dumpInitRngs --automateAll --keepAutomated --gameMode safariSurvival --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix battle --dumpInitRngs --automateAll --keepAutomated --gameMode battle --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix battleDefense --dumpInitRngs --automateAll --keepAutomated --gameMode battleDefense --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log+	$$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix battleSurvival --dumpInitRngs --automateAll --keepAutomated --gameMode battleSurvival --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log   build-binary-v1:@@ -277,14 +275,11 @@ 	cabal v1-copy --destdir=LambdaHackTheGameInstall  copy-binary:-	cp $$(cabal-plan list-bin exe:LambdaHack) LambdaHackTheGame+	cp $$(cabal list-bin exe:LambdaHack) LambdaHackTheGame  configure-binary-v2: 	cabal configure --disable-tests --disable-library-profiling --disable-profiling --enable-optimization -configure-binary-v2-vty:-	cabal configure -fvty --disable-tests --disable-library-profiling --disable-profiling --disable-documentation --enable-optimization- build-binary-v2: 	cabal build --only-dependencies . 	cabal build exe:LambdaHack@@ -315,10 +310,3 @@ 	LH_VERSION=$$(cat ~/.LambdaHack/stdout.txt); \ 	OS_VERSION=$$(sw_vers -productVersion); \ 	tar -czf LambdaHack_$${LH_VERSION}_macosx-$${OS_VERSION}-amd64.tar.gz LambdaHackTheGame--build-directory-vty: configure-binary-v2-vty build-binary-v2 copy-directory copy-binary--build-binary-screen-reader-ubuntu: build-directory-vty-	LambdaHackTheGame/LambdaHack --version > /dev/null; \-	LH_VERSION=$$(cat ~/.LambdaHack/stdout.txt); \-	tar -czf LambdaHack_$${LH_VERSION}_screen-reader-ubuntu-16.04-amd64.tar.gz LambdaHackTheGame
README.md view
@@ -1,10 +1,9 @@ LambdaHack ========== -[![Build Status](https://secure.travis-ci.org/LambdaHack/LambdaHack.svg?master)](https://travis-ci.org/LambdaHack/LambdaHack) [![Hackage](https://img.shields.io/hackage/v/LambdaHack.svg)](https://hackage.haskell.org/package/LambdaHack) [![Join the chat at Discord](https://img.shields.io/discord/688792755564052486.svg?label=chat%20on%20Discord&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/87Ghnws)-[![Join the chat at Matrix](https://img.shields.io/matrix/lambdahack:mozilla.org.svg?label=chat%20on%20Matrix&logo=matrix&server_fqdn=mozilla.modular.im)](https://matrix.to/#/!HnbpAHMjOGHlYtrASl:mozilla.org?via=mozilla.modular.im)+[![Join the chat at Matrix](https://img.shields.io/matrix/lambdahack:mozilla.org.svg?label=chat%20on%20Matrix&logo=matrix&server_fqdn=mozilla.modular.im)](https://matrix.to/#/#lambdahack:mozilla.org)  LambdaHack is a Haskell[1] game engine library for ASCII roguelike[2] games of arbitrary theme, size and complexity, with optional@@ -16,6 +15,7 @@ of shooting down explosive projectiles. A couple were shot down close enough to enemies to harm them. Others exploded closer to our party members and took out of the air the projectiles that would otherwise harm them.+Actual in-game footage.  ![gameplay screenshot](https://raw.githubusercontent.com/LambdaHack/media/master/screenshot/allureofthestars.com.shooting.down.explosives.gif) @@ -39,11 +39,11 @@ and the engine builds the world and runs it. The library lets you compile a ready-to-play game binary, using either the supplied or a custom-made main loop.-Several frontends are available (SDL2 is the default+A couple of frontends are available (SDL2 is the default for desktop and there is a JavaScript browser frontend) and many other generic engine components are easily overridden,-but the fundamental source of flexibility lies-in the strict and type-safe separation of code from the content+but the fundamental source of flexibility lies in the strict+and enforced with types separation of code from the content and of clients (human and AI-controlled) from the server.  Please see the changelog file for recent improvements@@ -54,12 +54,12 @@ Please offer feedback to mikolaj.konarski@funktory.com or, preferably, on any of the public forums. -Other games known to use the LambdaHack library:+Games from different repos known to use the LambdaHack library:  * Allure of the Stars[6], a near-future Sci-Fi game -Note: the engine and the sample game are bundled together in a single-Hackage[3] package released under the permissive `BSD3` license.+Note: the engine and the LambdaHack sample game are bundled together+in a single Hackage[3] package released under the permissive `BSD3` license. You are welcome to create your own games by forking and modifying the single package, but please consider eventually splitting your changes into a separate content-heavy package that depends on the upstream@@ -84,7 +84,7 @@ you may prefer to use a native binary for your architecture, if it exists.  Pre-compiled game binaries are available through the release page[11]-(and, for Windows, dev versions continuously from AppVeyor[18]).+(and Linux dev versions from GitHub Actions[18] and Windows from AppVeyor[19]). To use a pre-compiled binary archive, unpack it and run the executable in the unpacked directory or use program shortcuts from the installer, if available. On Linux, make sure you have the SDL2 libraries installed@@ -97,19 +97,19 @@ ---------------------------------  The game UI can be configured via a config file.-The default settings, the same that are built into the binary,+The default config settings, the same that are built into the binary, are on github at [GameDefinition/config.ui.default](https://github.com/LambdaHack/LambdaHack/blob/master/GameDefinition/config.ui.default).-When the game is run for the first time, or whenever the settings file+When the game is run for the first time, or whenever the config file is deleted, the file is written to the default user data location,-which is `~/.Allure/` on Linux,+which is `~/.LambdaHack/` on Linux, `C:\Users\<username>\AppData\Roaming\LambdaHack\` (or `C:\Documents And Settings\user\Application Data\LambdaHack\` or something else altogether) on Windows and `Inspect/Application/Local Storage` under RMB menu when run inside the Chrome browser. If the user config file is outdated or corrupted, it's automatically-moved away together with old savefiles, which guarantees that the new-default config file is ultimately put in its place.+moved away together with old savefiles. At the next game start,+the new default config file appears at its place.  Screen fonts and, consequently, window size can be changed by editing the config file in the user data folder. The default bitmap font@@ -118,8 +118,8 @@ and results in a game window of exactly 720p HD dimensions. The `8x8xb.fnt` bitmap font results in a tiny window and covers latin-1 characters only. The config file parameter `allFontsScale` permits further window size-adjustments, automatically switching to the scalable `16x16xw.woff`-version of the game map font. Config file option `chosenFontset` governs+adjustments, automatically switching to the scalable version of the large+game map font (`16x16xw.woff`). Config file option `chosenFontset` governs not only the main game map font, but also the shape of the rectangular fonts, if any, in which longer texts are overlaid over the map. @@ -129,7 +129,7 @@ too small or, in fullscreen or on retina displays in OS X, the screen may be automatically scaled as a whole, not each letter separately, softening letter edges of the square fonts that should-be pixel-perfect and crisp.+rather be pixel-perfect and crisp.  If you don't have a numeric keypad, you can use the left-hand movement key setup (axwdqezc) or Vi editor keys (aka roguelike keys) or mouse.@@ -140,38 +140,41 @@ may be mouse for menus, go-to, inspecting the map, aiming at distant positions and keyboard for everything else. -If you are using a terminal frontend, e.g. the best supported vty frontend,-then numeric keypad (especially keypad `*` and `/`) may not work correctly,-depending on versions of libraries, terminfo and terminal emulators.-Toggling the Num Lock key may help or make issues worse. As a workaround,-in the vty frontend, numbers are used for movement, which sadly prevents-the number keys from selecting heroes. The commands that require pressing-Control and Shift together won't work either, but fortunately they are-not crucial to gameplay.+If you run the ANSI terminal frontend (`--frontendANSI` on commandline),+then numeric keypad (especially keypad `*`, `/` and `5`) may not work+correctly, depending on the terminal emulator you use. Toggling+the Num Lock key may help or make issues worse. As a work around+these issues, numbers are used for movement in the ANSI frontend,+which sadly prevents the number keys from selecting heroes.+The commands that require pressing Control and Shift together won't work+either, but fortunately they are not crucial to gameplay. -Some effort went into making the vty frontend usable with screen readers,+Some effort went into making the ANSI frontend usable with screen readers, but without feedback it's hard to say how accessible that setup is.+This doesn't work on Windows, due to extra code that would be required. As a side effect of screen reader support, there is no aiming line-nor path in vty frontend and some of map position highlighting+nor path in ANSI frontend and some of map position highlighting is performed using the terminal cursor. Screen readers may also work better with animations turned off, using `--noAnim` or the corresponding-config file option.+config file or main game menu options.   Compilation of the library and sample game from source ------------------------------------------------------ -The recommended frontend is based on SDL2, so you need the SDL2 libraries-for your OS. On Linux, remember to install the -dev versions as well,-e.g., libsdl2-dev and libsdl2-ttf-dev on Ubuntu Linux 16.04.-Other frontends are compiled similarly, but compilation to JavaScript-for the browser is more complicated and requires the ghcjs[15] compiler-and optionally the Google Closure Compiler[16].+To compile with the standard frontend based on SDL2, you need the SDL2+libraries for your OS. On Linux, remember to install the -dev versions+as well, e.g., libsdl2-dev and libsdl2-ttf-dev on Ubuntu Linux 16.04.+Compilation to JavaScript for the browser is more complicated+and requires the ghcjs[15] compiler and optionally the Google Closure+Compiler[16].  The latest official version of the LambdaHack library can be downloaded,-compiled for SDL2 and installed automatically using the Cabal tool,-which is already a part of your OS distribution, or available within-The Haskell Platform[7]. Get the library from Hackage[3] as follows+compiled for SDL2 and installed automatically using the 'cabal' tool,+which may already be a part of your OS distribution, but if it's too old+(version 3.4 or later is required) you can download the whole current+compilation suite as described at https://www.haskell.org/downloads/.+Get the LambdaHack package from Hackage[3] as follows      cabal update     cabal run LambdaHack@@ -182,9 +185,8 @@ the following speeds up the turn-around a lot      cp cabal.project.local.development cabal.project.local-    cabal install cabal-plan -and then compile with+and then you can compile (and recompile) with      cabal build . @@ -192,51 +194,50 @@      make play -There is a built-in black and white line terminal frontend, suitable-for teletype terminals or a keyboard and a printer (but it's going to use-a lot of paper, unless you disable animations with `--noAnim`). It is used-in CI and for some tests and benchmarks defined in Makefile. To compile-with one of the less rudimentary terminal frontends (in which case you are-on your own regarding font choice and color setup and you won't have-the spiffy colorful squares outlining special positions that exist in SDL2-frontend, but only crude cursor highlights), use Cabal flags, e.g,-to switch to the vty console frontend optimized for screen readers, run--    cabal run -fvty LambdaHack+The SDL2 frontend binary also contains the ANSI terminal frontend+(`--frontendANSI` on commandline) intended for screen readers+and a simplified black and white line terminal frontend (`--frontendTeletype`)+suitable for teletype terminals or a keyboard and a printer (but it's going+to use a lot of paper, unless you disable animations with `--noAnim`).+The teletype frontend is used in CI and for some tests and benchmarks defined+in Makefile. The terminal frontends leave you on your own regarding font+choice and color setup and you won't have the colorful squares outlining+special positions that exist in the SDL2 frontend, but only crude+cursor highlights. The terminal frontends should run on Windows,+but Windows disables console for GUI applications, so they don't.   Testing and debugging --------------------- +Unit tests and integration tests can be run and displayed with++    cabal test test --enable-tests --test-show-details=direct++To prepare doctests[7], set `tests: True` in your `cabal.project.local`.+Afterwards, doctests can be executed with++    cabal build . && cabal exec cabal test doctests++and their detailed results observed in a log file.+(This repeating of `cabal build` before each testsuite run+is required due to bug https://github.com/haskell/cabal/issues/7522.+Contributions to cabal development are very welcome.)+ The [Makefile](https://github.com/LambdaHack/LambdaHack/blob/master/Makefile)-contains many sample test commands.+contains many sample automated playtest commands. Numerous tests that use the screensaver game modes (AI vs. AI)-and the teletype frontend are gathered in `make test`.-Of these, travis runs `test-travis` on each push to github.+and the teletype frontend are gathered in `make test-locally`.+Some of these are run by CI  on each push to github. Test commands with prefix `frontend` start AI vs. AI games with-the standard, user-friendly frontend and auto-locating the game binary.+the standard SDL2 frontend to view them on.  Run `LambdaHack --help` to see a brief description of all debug options. Of these, the `--sniff` option is very useful (though verbose and initially cryptic), for displaying the traffic between clients-and the server. Some options in the config file may prove useful too,-though they mostly overlap with commandline options (and will be totally-merged at some point).--You can use HPC with the game as follows (details vary according-to HPC version).--    cabal clean-    cabal build --enable-coverage-    make test-    hpc report --hpcdir=dist/hpc/dyn/mix/LambdaHack --hpcdir=dist/hpc/dyn/mix/LambdaHack-xxx/ LambdaHack-    hpc markup --hpcdir=dist/hpc/dyn/mix/LambdaHack --hpcdir=dist/hpc/dyn/mix/LambdaHack-xxx/ LambdaHack--A quick manual playing session, after the automated tests, would be-in order as well, since the tests don't touch the topmost UI layer.-Note that a debug option of the form `--stopAfter*` is required to cleanly-terminate any automated test. This is needed to gather any HPC info,-because HPC requires a clean exit to save data files.+and the server. Some options in the config file may prove useful+for debugging too, though they mostly overlap with commandline options+(and will be totally merged at some point).   Coding style@@ -250,6 +251,10 @@ Generally, relax and try to stick to the style apparent in a file you are editing. Put big formatting changes in separate commits. +CI checks the code with `hlint .` using the very liberal configuration file at+[.hlint.yaml](https://github.com/LambdaHack/LambdaHack/blob/master/.hlint.yaml).+If hlint is still too naggy, feel free to add more exceptions.+ Haddocks are provided for all module headers and for all functions and types from major modules, in particular for the modules that are interfaces for a whole directory of modules. Apart of that, only very important@@ -266,9 +271,59 @@ This codebase is an experiment in extensive use of states without lens. So far, it works, doesn't result in much larger files or lots of repetition and has the added benefits that newcomers don't need-to learn any lens library. Record punning, etc., definitely helps.+to learn any optics library. Record punning, etc., definitely help.  +First steps reading the codebase+--------------------------------++A good start may be++https://github.com/LambdaHack/LambdaHack/blob/master/GameDefinition/game-src/Client/UI/Content/Input.hs++That's where keyboard keys are assigned commands, help texts and categories+(including special categories indicating that a group of keys+also forms an in-game menu). This file is specific to a particular game+(hence `GameDefinition` in the path) and the engine dynamically creates+in-game help screens based on this file and on player config file+that can partially overwrite it.++The commands assigned to keys are interpreted by the UI client+(each faction in the game uses a client and the player's client additionally+has UI capabilities) in the following module:++https://github.com/LambdaHack/LambdaHack/blob/master/engine-src/Game/LambdaHack/Client/UI/HandleHumanM.hs++By this point you've seen one of the six major command sets (`HumanCmd`,+the others being `Effect`, `UpdAtomic`, `Request`, `Response`, `FrontReq`)+and one of around ten distinct interpreters for the commands+(mostly in `Handle*` modules). You've also seen a bit of the UI+client code, but not the AI client nor the server (game arbiter) code.+The wiki[17] contains not entirely outdated further reading about+the client-server architecture.++At this point, before trying to grasp anything more and drown in abstraction,+you are welcome to pick up a few `good first issue`-labeled tickets+and get some hands-on experience with the codebase.++For further study, note that most of the commands are interpreted in monads.+Server and clients share some of the customized monadic API, but their+monads are implemented differently (in `*Implementation` modules).+All these monads are state monads (managing different aspects of game state),+therefore the semantics of a command is a state transformer with extra+side effects (e.g., frontend drawing).++The "main loop" is the following: the UI client receives keystrokes+and interprets the commands they correspond to. As soon as one of the commands+is not just local UI manipulation, but a request to change the main game state,+such a request is packaged and sent to the server (e.g., a request+to move a hero to the north). The server responds "not possible,+there is a wall" or reacts by sending to clients (to all UI and AI clients+that can see the event) a series of game state-changing responses.+AI clients, likewise, send to the server requests, generated based+on the perceived game state changes and the AI goals of each AI faction.++ Further information ------------------- @@ -287,9 +342,12 @@ [4]: https://github.com/LambdaHack/LambdaHack/wiki [5]: https://github.com/LambdaHack/LambdaHack [6]: http://allureofthestars.com-[7]: https://www.haskell.org/platform+[7]: https://github.com/sol/doctest [9]: https://github.com/LambdaHack/LambdaHack/wiki/Sample-dungeon-crawler [10]: https://github.com/AllureOfTheStars/Allure [11]: https://github.com/LambdaHack/LambdaHack/releases [15]: https://github.com/ghcjs/ghcjs [16]: https://www.npmjs.com/package/google-closure-compiler+[17]: https://github.com/LambdaHack/LambdaHack/wiki/Client-server-architecture+[18]: https://github.com/LambdaHack/LambdaHack/actions+[19]: https://ci.appveyor.com/project/Mikolaj/lambdahack
+ cabal.project view
@@ -0,0 +1,7 @@+packages: ./++package sdl2+    flags: +no-linear++package splitmix+    flags: +optimised-mixer
definition-src/Game/LambdaHack/Content/CaveKind.hs view
@@ -22,55 +22,56 @@ import           Game.LambdaHack.Core.Random import           Game.LambdaHack.Definition.ContentData import           Game.LambdaHack.Definition.Defs+import           Game.LambdaHack.Definition.DefsInternal  -- | Parameters for the generation of dungeon levels. -- Warning: for efficiency, avoid embedded items in any of the common tiles. data CaveKind = CaveKind-  { csymbol         :: Char             -- ^ a symbol-  , cname           :: Text             -- ^ short description-  , cfreq           :: Freqs CaveKind   -- ^ frequency within groups-  , cXminSize       :: X                -- ^ minimal X size of the whole cave-  , cYminSize       :: Y                -- ^ minimal Y size of the whole cave-  , ccellSize       :: Dice.DiceXY      -- ^ size of a map cell holding a place-  , cminPlaceSize   :: Dice.DiceXY      -- ^ minimal size of places; for merging-  , cmaxPlaceSize   :: Dice.DiceXY      -- ^ maximal size of places; for growing-  , cdarkOdds       :: Dice.Dice        -- ^ the odds a place is dark+  { csymbol       :: Char             -- ^ a symbol+  , cname         :: Text             -- ^ short description+  , cfreq         :: Freqs CaveKind   -- ^ frequency within groups+  , cXminSize     :: X                -- ^ minimal X size of the whole cave+  , cYminSize     :: Y                -- ^ minimal Y size of the whole cave+  , ccellSize     :: Dice.DiceXY      -- ^ size of a map cell holding a place+  , cminPlaceSize :: Dice.DiceXY      -- ^ minimal size of places; for merging+  , cmaxPlaceSize :: Dice.DiceXY      -- ^ maximal size of places; for growing+  , cdarkOdds     :: Dice.Dice        -- ^ the odds a place is dark                                         --   (level-scaled dice roll > 50)-  , cnightOdds      :: Dice.Dice        -- ^ the odds the cave is dark+  , cnightOdds    :: Dice.Dice        -- ^ the odds the cave is dark                                         --   (level-scaled dice roll > 50)-  , cauxConnects    :: Rational         -- ^ a proportion of extra connections-  , cmaxVoid        :: Rational+  , cauxConnects  :: Rational         -- ^ a proportion of extra connections+  , cmaxVoid      :: Rational       -- ^ at most this proportion of rooms may be void-  , cdoorChance     :: Chance           -- ^ the chance of a door in an opening-  , copenChance     :: Chance           -- ^ if there's a door, is it open?-  , chidden         :: Int              -- ^ if not open, hidden one in n times-  , cactorCoeff     :: Int              -- ^ the lower, the more monsters spawn-  , cactorFreq      :: Freqs ItemKind   -- ^ actor groups to consider-  , citemNum        :: Dice.Dice        -- ^ number of initial items in the cave-  , citemFreq       :: Freqs ItemKind   -- ^ item groups to consider-  , cplaceFreq      :: Freqs PlaceKind  -- ^ place groups to consider-  , cpassable       :: Bool+  , cdoorChance   :: Chance           -- ^ the chance of a door in an opening+  , copenChance   :: Chance           -- ^ if there's a door, is it open?+  , chidden       :: Int              -- ^ if not open, hidden one in n times+  , cactorCoeff   :: Int              -- ^ the lower, the more monsters spawn+  , cactorFreq    :: Freqs ItemKind   -- ^ actor groups to consider+  , citemNum      :: Dice.Dice        -- ^ number of initial items in the cave+  , citemFreq     :: Freqs ItemKind   -- ^ item groups to consider;+      -- note that the groups are flattened; e.g., if an item is moved to another+      -- included group with the same weight, the outcome doesn't change+  , cplaceFreq    :: Freqs PlaceKind  -- ^ place groups to consider+  , cpassable     :: Bool       -- ^ are passable default tiles permitted-  , labyrinth       :: Bool                -- ^ waste of time for AI to explore-  , cdefTile        :: GroupName TileKind  -- ^ the default cave tile-  , cdarkCorTile    :: GroupName TileKind  -- ^ the dark cave corridor tile-  , clitCorTile     :: GroupName TileKind  -- ^ the lit cave corridor tile-  , cwallTile       :: GroupName TileKind  -- ^ the tile used for @FWall@ fence-  , ccornerTile     :: GroupName TileKind  -- ^ tile used for the fence corners-  , cfenceTileN     :: GroupName TileKind  -- ^ the outer fence N wall-  , cfenceTileE     :: GroupName TileKind  -- ^ the outer fence E wall-  , cfenceTileS     :: GroupName TileKind  -- ^ the outer fence S wall-  , cfenceTileW     :: GroupName TileKind  -- ^ the outer fence W wall-  , cfenceApart     :: Bool                -- ^ are places touching fence banned-  , clegendDarkTile :: GroupName TileKind  -- ^ the dark place plan legend-  , clegendLitTile  :: GroupName TileKind  -- ^ the lit place plan legend-  , cminStairDist   :: Int                 -- ^ minimal distance between stairs-  , cmaxStairsNum   :: Dice.Dice           -- ^ maximum number of stairs-  , cescapeFreq     :: Freqs PlaceKind     -- ^ escape groups, if any-  , cstairFreq      :: Freqs PlaceKind     -- ^ place groups for created stairs-  , cstairAllowed   :: Freqs PlaceKind     -- ^ extra groups for inherited-  , cskip           :: [Int]  -- ^ which faction starting positions to skip-  , cdesc           :: Text   -- ^ full cave description+  , labyrinth     :: Bool                -- ^ waste of time for AI to explore+  , cdefTile      :: GroupName TileKind  -- ^ the default cave tile+  , cdarkCorTile  :: GroupName TileKind  -- ^ the dark cave corridor tile+  , clitCorTile   :: GroupName TileKind  -- ^ the lit cave corridor tile+  , cwallTile     :: GroupName TileKind  -- ^ the tile used for @FWall@ fence+  , ccornerTile   :: GroupName TileKind  -- ^ tile used for the fence corners+  , cfenceTileN   :: GroupName TileKind  -- ^ the outer fence N wall+  , cfenceTileE   :: GroupName TileKind  -- ^ the outer fence E wall+  , cfenceTileS   :: GroupName TileKind  -- ^ the outer fence S wall+  , cfenceTileW   :: GroupName TileKind  -- ^ the outer fence W wall+  , cfenceApart   :: Bool                -- ^ are places touching fence banned+  , cminStairDist :: Int                 -- ^ minimal distance between stairs+  , cmaxStairsNum :: Dice.Dice           -- ^ maximum number of stairs+  , cescapeFreq   :: Freqs PlaceKind     -- ^ escape groups, if any+  , cstairFreq    :: Freqs PlaceKind     -- ^ place groups for created stairs+  , cstairAllowed :: Freqs PlaceKind     -- ^ extra groups for inherited+  , cskip         :: [Int]  -- ^ which faction starting positions to skip+  , cdesc         :: Text   -- ^ full cave description   }   deriving Show  -- No Eq and Ord to make extending logically sound 
definition-src/Game/LambdaHack/Content/ItemKind.hs view
@@ -7,16 +7,18 @@   , 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 #ifdef EXPOSE_INTERNAL     -- * Internal operations   , boostItemKind, onSmashOrCombineEffect-  , validateSingle, validateAll, validateDups, validateDamage+  , validateAll, validateDups, validateDamage   , mandatoryGroups, mandatoryGroupsSingleton #endif   ) where@@ -36,6 +38,7 @@ 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@@ -100,7 +103,8 @@ -- 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  :: Char            -- ^ map symbol+  { isymbol  :: ContentSymbol ItemKind+                                -- ^ map symbol   , iname    :: Text            -- ^ generic name; is pluralized if needed   , ifreq    :: Freqs ItemKind  -- ^ frequency within groups   , iflavour :: [Flavour]       -- ^ possible flavours@@ -317,6 +321,51 @@  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 =@@ -505,8 +554,8 @@ toOrganNoTimer grp = CreateItem Nothing COrgan grp TimerNone  -- | Catch invalid item kind definitions.-validateSingle :: ItemKind -> [Text]-validateSingle ik@ItemKind{..} =+validateSingle :: ItemSymbolsUsedInEngine -> ItemKind -> [Text]+validateSingle itemSymbols ik@ItemKind{..} =   ["iname longer than 23" | T.length iname > 23]   ++ ["icount < 0" | Dice.infDice icount < 0]   ++ validateRarity irarity@@ -526,7 +575,10 @@           likelyTemplate = case ifreq of             [(grp, 1)] -> "unknown" `T.isSuffixOf` fromGroupName grp             _ -> False-          likelyException = isymbol `elem` [',', '"', '-'] || likelyTemplate+          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"@@ -696,14 +748,16 @@   in [ "PresentAs groups not singletons:" <+> tshow wrongPresentAsGroups      | not $ null wrongPresentAsGroups ] -makeData :: [ItemKind] -> [GroupName ItemKind] -> [GroupName ItemKind]+makeData :: ItemSymbolsUsedInEngine+         -> [ItemKind] -> [GroupName ItemKind] -> [GroupName ItemKind]          -> ContentData ItemKind-makeData content groupNamesSingleton groupNames =+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 validateAll content+     makeContentData "ItemKind" iname ifreq+                     (validateSingle itemSymbols) validateAll content                      (mandatoryGroupsSingleton ++ groupNamesSingleton)                      (mandatoryGroups ++ groupNames)
definition-src/Game/LambdaHack/Content/ModeKind.hs view
@@ -5,7 +5,7 @@   , ModeKind(..), makeData   , Caves, Roster(..), TeamContinuity(..), Outcome(..)   , HiCondPoly, HiSummand, HiPolynomial, HiIndeterminant(..)-  , Player(..), LeaderMode(..), AutoLeader(..)+  , Player(..), AutoLeader(..)   , teamExplorer, victoryOutcomes, deafeatOutcomes, nameOutcomePast   , nameOutcomeVerb, endMessageOutcome, screensave #ifdef EXPOSE_INTERNAL@@ -29,6 +29,7 @@ import qualified Game.LambdaHack.Definition.Ability as Ability import           Game.LambdaHack.Definition.ContentData import           Game.LambdaHack.Definition.Defs+import           Game.LambdaHack.Definition.DefsInternal  -- | Game mode specification. data ModeKind = ModeKind@@ -118,23 +119,17 @@   , fdoctrine    :: Ability.Doctrine                                 -- ^ non-leaders behave according to this                                 --   doctrine; can be changed during the game-  , fleaderMode  :: LeaderMode  -- ^ the mode of switching the leader+  , fleaderMode  :: Maybe AutoLeader+                                -- ^ whether the faction can have a leader+                                --   and what's its switching mode;   , fhasUI       :: Bool        -- ^ does the faction have a UI client                                 --   (for control or passive observation)+  , funderAI     :: Bool        -- ^ is the faction under AI control   }   deriving (Show, Eq, Generic)  instance Binary Player --- | If a faction with @LeaderUI@ and @LeaderAI@ has any actor, it has a leader.-data LeaderMode =-    LeaderNull  -- ^ faction can have no leader, is whole under AI control-  | LeaderAI AutoLeader -- ^ leader under AI control-  | LeaderUI AutoLeader -- ^ leader under UI control, assumes @fhasUI@-  deriving (Show, Eq, Generic)--instance Binary LeaderMode- data AutoLeader = AutoLeader   { autoDungeon :: Bool       -- ^ leader switching between levels is automatically done by the server@@ -196,9 +191,12 @@  screensave :: AutoLeader -> ModeKind -> ModeKind screensave auto mk =-  let f x@(Player{fleaderMode = LeaderAI{}}, _, _) = x+  let f x@(Player{funderAI=True}, _, _) = x       f (player, teamContinuity, initial) =-          (player {fleaderMode = LeaderAI auto}, teamContinuity, initial)+          ( player { funderAI = True+                   , fleaderMode = Just auto }+          , teamContinuity+          , initial )   in mk { mroster = (mroster mk) {rosterList = map f $ rosterList $ mroster mk}         , mreason = "This is one of the screensaver scenarios, not available from the main menu, with all factions controlled by AI. Feel free to take over or relinquish control at any moment, but to register a legitimate high score, choose a standard scenario instead.\n" <> mreason mk         }@@ -237,19 +235,21 @@      in concatMap (checkDipl "rosterEnemy") rosterEnemy         ++ concatMap (checkDipl "rosterAlly") rosterAlly   ++ let keys = concatMap fst caves+         minD = minimum keys+         maxD = maximum keys          f (_, _, l) = concatMap g l          g i3@(ln, _, _) =            [ "initial actor levels not among caves:" <+> tshow i3            | ln `notElem` keys ]      in concatMap f rosterList+        ++ [ "player confused by both positive and negative level numbers"+           | signum minD /= signum maxD ]+        ++ [ "player confused by level numer zero"+           | any (== 0) keys ]  validateSinglePlayer :: Player -> [Text] validateSinglePlayer Player{..} =   [ "fname empty:" <+> fname | T.null fname ]-  ++ [ "no UI client, but UI leader:" <+> fname-     | not fhasUI && case fleaderMode of-                       LeaderUI _ -> True-                       _ -> False ]   ++ [ "fskillsOther not negative:" <+> fname      | any ((>= 0) . snd) $ Ability.skillsToList fskillsOther ] 
definition-src/Game/LambdaHack/Content/PlaceKind.hs view
@@ -4,7 +4,7 @@ module Game.LambdaHack.Content.PlaceKind   ( PlaceKind(..), makeData   , Cover(..), Fence(..)-  , PlaceEntry(..), deadEndId+  , PlaceEntry(..), deadEndId, overridePlaceKind, override2PlaceKind #ifdef EXPOSE_INTERNAL     -- * Internal operations   , validateSingle, validateAll@@ -16,24 +16,26 @@ import Game.LambdaHack.Core.Prelude  import           Data.Binary+import qualified Data.EnumMap.Strict as EM import qualified Data.Text as T import           GHC.Generics (Generic)  import Game.LambdaHack.Content.TileKind (TileKind) import Game.LambdaHack.Definition.ContentData import Game.LambdaHack.Definition.Defs+import Game.LambdaHack.Definition.DefsInternal  -- | Parameters for the generation of small areas within a dungeon level. data PlaceKind = PlaceKind-  { psymbol       :: Char          -- ^ a symbol-  , pname         :: Text          -- ^ short description, singular or plural-  , pfreq         :: Freqs PlaceKind  -- ^ frequency within groups-  , prarity       :: Rarity        -- ^ rarity on given depths-  , pcover        :: Cover         -- ^ how to fill whole place using the corner-  , pfence        :: Fence         -- ^ whether to fence place with solid border-  , ptopLeft      :: [Text]        -- ^ plan of the top-left corner of the place-  , poverrideDark :: [(Char, GroupName TileKind)]  -- ^ dark legend override-  , poverrideLit  :: [(Char, GroupName TileKind)]  -- ^ lit legend override+  { psymbol     :: Char          -- ^ a symbol+  , pname       :: Text          -- ^ short description, singular or plural+  , pfreq       :: Freqs PlaceKind  -- ^ frequency within groups+  , prarity     :: Rarity        -- ^ rarity on given depths+  , pcover      :: Cover         -- ^ how to fill whole place using the corner+  , pfence      :: Fence         -- ^ whether to fence place with solid border+  , ptopLeft    :: [Text]        -- ^ plan of the top-left corner of the place+  , plegendDark :: EM.EnumMap Char (GroupName TileKind)  -- ^ dark legend+  , plegendLit  :: EM.EnumMap Char (GroupName TileKind)  -- ^ lit legend   }   deriving Show  -- No Eq and Ord to make extending logically sound @@ -56,6 +58,14 @@   | FNone   -- ^ skip the fence and fill all with the place proper   deriving (Show, Eq) +-- | Places are rooms and other dungeon features, their names can be seen+-- on a level map by aiming at a position that is an entry to the place+-- (an individual entrance point, an approach area around the place+-- or a phantom entry not on the map, but only used for statistics+-- to witness the place exists). Entries are proxies for initial places+-- created on the level (which may be otherwise eradicated by burrowing+-- the walls, etc.) and so used for dungeon statistics.+-- The statistics are presented in the @Dashboard/displace place lore@ menu. data PlaceEntry =     PEntry (ContentId PlaceKind)   | PAround (ContentId PlaceKind)@@ -68,32 +78,49 @@ {-# INLINE deadEndId #-} deadEndId = toContentId 0 -validateOverride :: [(Char, GroupName TileKind)] -> [Text]-validateOverride ov =-  let symbols = sort $ map fst ov-      duplicated = filter (uncurry (==)) $ zip symbols ('\0' : symbols)-  in [ "duplicated override symbols:"-        <+> T.pack (intersperse ' ' $ map fst duplicated)-     | not (null duplicated) ]+overridePlaceKind :: [(Char, GroupName TileKind)] -> PlaceKind -> PlaceKind+overridePlaceKind l pk = pk+  { plegendDark = EM.fromList l `EM.union` plegendDark pk+  , plegendLit = EM.fromList l `EM.union` plegendLit pk } +override2PlaceKind :: [(Char, GroupName TileKind)]+                   -> [(Char, GroupName TileKind)]+                   -> PlaceKind+                   -> PlaceKind+override2PlaceKind lDark lLit pk = pk+  { plegendDark = EM.fromList lDark `EM.union` plegendDark pk+  , plegendLit = EM.fromList lLit `EM.union` plegendLit pk }+ -- | Catch invalid place kind definitions. In particular, verify that -- the top-left corner map is rectangular and not empty.-validateSingle :: PlaceKind -> [Text]-validateSingle PlaceKind{..} =+validateSingle :: ContentData TileKind -> PlaceKind -> [Text]+validateSingle cotile PlaceKind{..} =   let dxcorner = case ptopLeft of         [] -> 0         l : _ -> T.length l+      inLegend :: Text -> EM.EnumMap Char (GroupName TileKind) -> Char -> [Text]+      inLegend _ _ 'X' = []  -- special placeholder symbol; TODO: unhardwire+      inLegend legendName m c = case EM.lookup c m of+        Nothing -> [tshow c <+> "tile code not found in" <+> legendName]+        Just grp -> [ tshow c <+> "tile code has group"+                      <+> displayGroupName grp+                      <+> "with null frequency in tile content"+                    | not $ oexistsGroup cotile grp ]+      inLegendAll legendName m = concatMap (inLegend legendName m)+                                           (concatMap T.unpack ptopLeft)   in [ "top-left corner empty" | dxcorner == 0 ]      ++ [ "top-left corner not rectangular"         | any (/= dxcorner) (map T.length ptopLeft) ]+     ++ inLegendAll "plegendDark" plegendDark+     ++ inLegendAll "plegendLit" plegendLit      ++ validateRarity prarity-     ++ validateOverride poverrideDark-     ++ validateOverride poverrideLit  -- | Validate all place kinds. validateAll :: [PlaceKind] -> ContentData PlaceKind -> [Text] validateAll _ _ = []  -- so far, always valid -makeData :: [PlaceKind] -> [GroupName PlaceKind] -> [GroupName PlaceKind]+makeData :: ContentData TileKind+         -> [PlaceKind] -> [GroupName PlaceKind] -> [GroupName PlaceKind]          -> ContentData PlaceKind-makeData = makeContentData "PlaceKind" pname pfreq validateSingle validateAll+makeData cotile = makeContentData "PlaceKind" pname pfreq+                                  (validateSingle cotile) validateAll
definition-src/Game/LambdaHack/Content/RuleKind.hs view
@@ -15,14 +15,16 @@ import qualified Data.Ini.Types as Ini import           Data.Version +import Game.LambdaHack.Content.ItemKind+  (ItemSymbolsUsedInEngine, emptyItemSymbolsUsedInEngine) import Game.LambdaHack.Definition.Defs  -- | The type of game rules and assorted game data. data RuleContent = RuleContent   { rtitle            :: String    -- ^ title of the game (not lib)-  , rXmax             :: X         -- ^ maximum level width; for now,+  , rWidthMax         :: X         -- ^ maximum level width; for now,                                    --   keep equal to ScreenContent.rwidth-  , rYmax             :: Y         -- ^ maximum level height; for now,+  , rHeightMax        :: Y         -- ^ maximum level height; for now,                                    --   keep equal to ScreenContent.rheight - 3   , rexeVersion       :: Version   -- ^ version of the game   , rcfgUIName        :: FilePath  -- ^ name of the UI config file@@ -34,14 +36,15 @@   , rnearby           :: Int       -- ^ what is a close distance between actors   , rstairWordCarried :: [Text]    -- ^ words that can't be dropped from stair                                    --   name as it goes through levels-  , rsymbolProjectile :: Char+  , ritemSymbols      :: ItemSymbolsUsedInEngine+                                   -- ^ item symbols treated specially in engine   }  emptyRuleContent :: RuleContent emptyRuleContent = RuleContent   { rtitle = ""-  , rXmax = 0-  , rYmax = 0+  , rWidthMax = 0+  , rHeightMax = 0   , rexeVersion = makeVersion []   , rcfgUIName = ""   , rcfgUIDefault = ("", Ini.emptyConfig)@@ -50,7 +53,7 @@   , rscoresFile = ""   , rnearby = 0   , rstairWordCarried = []-  , rsymbolProjectile = '0'+  , ritemSymbols = emptyItemSymbolsUsedInEngine   }  -- | Catch invalid rule kind definitions.
definition-src/Game/LambdaHack/Content/TileKind.hs view
@@ -1,7 +1,7 @@ -- | The type of tile kinds. Every terrain tile in the game is -- an instantiated tile kind. module Game.LambdaHack.Content.TileKind-  ( pattern S_UNKNOWN_SPACE, pattern LEGEND_LIT, pattern LEGEND_DARK, pattern S_UNKNOWN_OUTER_FENCE, pattern S_BASIC_OUTER_FENCE, pattern AQUATIC+  ( pattern S_UNKNOWN_SPACE, pattern S_UNKNOWN_OUTER_FENCE, pattern S_BASIC_OUTER_FENCE, pattern AQUATIC   , TileKind(..), ProjectileTriggers(..), Feature(..)   , makeData   , isUknownSpace, unknownId@@ -24,6 +24,7 @@ import Game.LambdaHack.Definition.Color import Game.LambdaHack.Definition.ContentData import Game.LambdaHack.Definition.Defs+import Game.LambdaHack.Definition.DefsInternal  -- | The type of kinds of terrain tiles. See @Tile.hs@ for explanation -- of the absence of a corresponding type @Tile@ that would hold@@ -189,16 +190,11 @@ pattern S_UNKNOWN_SPACE, S_UNKNOWN_OUTER_FENCE, S_BASIC_OUTER_FENCE :: GroupName TileKind  mandatoryGroups :: [GroupName TileKind]-mandatoryGroups =-       [LEGEND_LIT, LEGEND_DARK]--pattern LEGEND_LIT, LEGEND_DARK :: GroupName TileKind+mandatoryGroups = []  pattern S_UNKNOWN_SPACE = GroupName "unknown space" pattern S_UNKNOWN_OUTER_FENCE = GroupName "unknown outer fence" pattern S_BASIC_OUTER_FENCE = GroupName "basic outer fence"-pattern LEGEND_LIT = GroupName "legendLit"-pattern LEGEND_DARK = GroupName "legendDark"  -- * Optional item groups @@ -237,7 +233,7 @@ talterForStairs = 3  floorSymbol :: Char-floorSymbol = '\x00B7'+floorSymbol = '·'  -- '\x00B7'  -- Alter skill schema: -- 0  can be altered by everybody (escape)
definition-src/Game/LambdaHack/Core/Prelude.hs view
@@ -12,7 +12,7 @@   , module Control.Exception.Assert.Sugar    , Text, (<+>), tshow, divUp, sum, (<$$>), partitionM, length, null, comparing-  , intCast, fromIntegralWrap, toIntegralCrash, intToDouble, int64ToDouble+  , into, fromIntegralWrap, toIntegralCrash, intToDouble, int64ToDouble   , mapM_, forM_    , (***), (&&&), first, second@@ -49,7 +49,6 @@ import           Data.Hashable import qualified Data.HashMap.Strict as HM import           Data.Int (Int64)-import           Data.IntCast (intCast) import           Data.Key import           Data.List.Compat hiding (foldl, foldl1, length, null, sum) import qualified Data.List.Compat as List@@ -62,6 +61,7 @@ import           NLP.Miniutter.English ((<+>)) import qualified NLP.Miniutter.English as MU import qualified Prelude.Compat+import           Witch (into)  -- | Show and pack the result. tshow :: Show a => a -> Text
definition-src/Game/LambdaHack/Core/Random.hs view
@@ -4,7 +4,7 @@     Rnd     -- * Random operations   , randomR, randomR0, nextRandom, randomWord32-  , oneOf, shuffle, shuffleExcept, frequency+  , oneOf, shuffle, invalidInformationCode, shuffleExcept, frequency     -- * Fractional chance   , Chance, chance     -- * Casting dice scaled with level@@ -56,18 +56,15 @@ -- to keep it working for arbitrary fixed number of bits. nextRandom :: forall a. (Integral a) => a -> SM.SMGen -> (a, SM.SMGen) {-# INLINE nextRandom #-}-nextRandom h g = assert ((intCast :: a -> Integer) h-                         <= (intCast :: Int32 -> Integer) maxBound) $+nextRandom h g = assert (toInteger h+                         <= (toInteger :: Int32 -> Integer) maxBound) $   let (w32, g') = SM.bitmaskWithRejection32'                     ((fromIntegralWrap :: a -> Word32) h) g       -- `fromIntegralWrap` is fine here, because wrapping is OK.       x = (fromIntegralWrap :: Word32 -> a) w32   in if x > h      then error $ "nextRandom internal error"-                  `showFailure`-                    ( (intCast :: a -> Integer) x-                    , (intCast :: a -> Integer) h-                    , w32 )+                  `showFailure` (toInteger x, toInteger h, w32)      else (x, g')  -- | Get a random 'Word32' using full range.@@ -90,16 +87,23 @@   x <- oneOf l   (x :) <$> shuffle (delete x l) +-- | Code that means the information (e.g., flavour or hidden kind index)+-- should be regenerated, because it could not be transferred from+-- previous playthrough (it's random in each playthrough or there was+-- no previous playthrough).+invalidInformationCode :: Word16+invalidInformationCode = maxBound+ -- | Generates a random permutation, except for the existing mapping. shuffleExcept :: U.Vector Word16 -> Int -> [Word16] -> Rnd [Word16] shuffleExcept v len l0 = assert (len == length l0) $-  shuffleE 0 (l0 \\ filter (/= maxBound) (U.toList v))+  shuffleE 0 (l0 \\ filter (/= invalidInformationCode) (U.toList v))  where   shuffleE :: Int -> [Word16] -> Rnd [Word16]   shuffleE i _ | i == len = return []   shuffleE i l = do     let a0 = v U.! i-    if a0 == maxBound then do+    if a0 == invalidInformationCode then do       a <- oneOf l       (a :) <$> shuffleE (succ i) (delete a l)     else
definition-src/Game/LambdaHack/Definition/Color.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving, MagicHash #-}+{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-} -- | Colours and text attributes. module Game.LambdaHack.Definition.Color   ( -- * Colours@@ -27,10 +27,7 @@ import           Data.Binary import           Data.Bits (unsafeShiftL, unsafeShiftR, (.&.)) import qualified Data.Char as Char-import           GHC.Exts (Int (I#)) import           GHC.Generics (Generic)-import           GHC.Prim (int2Word#)-import           GHC.Word (Word32 (W32#))  -- | Colours supported by the major frontends. data Color =@@ -143,10 +140,12 @@ -- | Additional map cell highlight, e.g., a colorful square around the cell -- or a colorful background. ----- Note: the highlight underscored by the terminal cursor is--- the maximal element of this type present of this screen.+-- Warning: the highlight underscored by the terminal cursor is+-- the maximal element of this type present on a screen,+-- so don't add new highlights to the end. data Highlight =     HighlightNone+  | HighlightBackground   | HighlightGreen   | HighlightBlue   | HighlightBrown@@ -164,6 +163,7 @@ highlightToColor :: Highlight -> Color highlightToColor hi = case hi of   HighlightNone -> Black  -- should be transparent, but is OK in web frontend+  HighlightBackground -> BrBlack  -- gets a special colour, but as a background   HighlightGreen -> Green   HighlightBlue -> Blue   HighlightBrown -> Brown@@ -175,7 +175,7 @@   HighlightYellow -> BrYellow  -- obscures, but mostly used around bright white   HighlightYellowAim -> BrYellow   HighlightRedAim -> Red-  HighlightNoneCursor -> Black  -- used in vty for cursor via @maxIndexByA@+  HighlightNoneCursor -> Black  -- used in ANSI for cursor via @maxIndexByA@  -- | Text attributes: foreground color and highlight. data Attr = Attr@@ -242,18 +242,23 @@  attrChar2ToW32 :: Color -> Char -> AttrCharW32 {-# INLINE attrChar2ToW32 #-}-attrChar2ToW32 fg acChar =-  case unsafeShiftL (fromEnum fg) 8 + unsafeShiftL (Char.ord acChar) 16 of-    I# i -> AttrCharW32 $ W32# (int2Word# i)-{- the hacks save one allocation (?) (before fits-in-32bits check) compared to-  AttrCharW32 $ toEnum-  $ unsafeShiftL (fromEnum fg) 8 + unsafeShiftL (Char.ord acChar) 16--}+attrChar2ToW32 fg =+  let fgNum = unsafeShiftL (fromEnum fg) 8+  in \acChar -> AttrCharW32 $ toEnum $ fgNum + unsafeShiftL (Char.ord acChar) 16+--+-- These hacks save one allocation (?) (before fits-in-32bits check) compared+-- to the above, but they fail in GHC 9.2.0 and possibly don't do anything+-- for JS, which is the only real bottleneck, so disabled:+--+--import GHC.Prim (int2Word#)+--  case unsafeShiftL (fromEnum fg) 8 + unsafeShiftL (Char.ord acChar) 16 of+--    I# i -> AttrCharW32 $ W32# (int2Word# i)  attrChar1ToW32 :: Char -> AttrCharW32 {-# INLINE attrChar1ToW32 #-} attrChar1ToW32 =   let fgNum = unsafeShiftL (fromEnum White) 8-  in \acChar ->-    case fgNum + unsafeShiftL (Char.ord acChar) 16 of-      I# i -> AttrCharW32 $ W32# (int2Word# i)+  in \acChar -> AttrCharW32 $ toEnum $ fgNum + unsafeShiftL (Char.ord acChar) 16+--+--    case fgNum + unsafeShiftL (Char.ord acChar) 16 of+--      I# i -> AttrCharW32 $ W32# (int2Word# i)
definition-src/Game/LambdaHack/Definition/ContentData.hs view
@@ -11,7 +11,7 @@   ( ContentData   , validateRarity, validFreqs   , emptyContentData, makeContentData-  , okind, omemberGroup, oisSingletonGroup, ouniqGroup, opick+  , okind, omemberGroup, oexistsGroup, oisSingletonGroup, ouniqGroup, opick   , ofoldlWithKey', ofoldlGroup', omapVector, oimapVector, olength   ) where @@ -28,6 +28,7 @@ import Game.LambdaHack.Core.Frequency import Game.LambdaHack.Core.Random import Game.LambdaHack.Definition.Defs+import Game.LambdaHack.Definition.DefsInternal  -- | Verified and preprocessed content data of a particular kind. data ContentData c = ContentData@@ -149,6 +150,11 @@  omemberGroup :: ContentData a -> GroupName a -> Bool omemberGroup ContentData{groupFreq} cgroup = cgroup `M.member` groupFreq++oexistsGroup :: ContentData a -> GroupName a -> Bool+oexistsGroup ContentData{groupFreq} cgroup = case M.lookup cgroup groupFreq of+  Nothing -> False+  Just l -> all ((> 0) . fst) l  oisSingletonGroup :: ContentData a -> GroupName a -> Bool oisSingletonGroup ContentData{groupFreq} cgroup =
definition-src/Game/LambdaHack/Definition/Defs.hs view
@@ -1,9 +1,12 @@-{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-}+{-# LANGUAGE DeriveGeneric #-} -- | Basic types for content definitions. module Game.LambdaHack.Definition.Defs-  ( X, Y, GroupName(..)-  , Freqs, Rarity, linearInterpolation-  , ContentId, toContentId, fromContentId, contentIdIndex+  ( GroupName, displayGroupName+  , ContentId, contentIdIndex+  , ContentSymbol, displayContentSymbol+  , X, Y+  , Freqs, renameFreqs+  , Rarity, linearInterpolation   , CStore(..), ppCStore, ppCStoreIn, verbCStore   , SLore(..), ItemDialogMode(..), ppSLore, headingSLore   , ppItemDialogMode, ppItemDialogModeIn, ppItemDialogModeFrom@@ -16,26 +19,24 @@  import Control.DeepSeq import Data.Binary-import Data.Hashable import GHC.Generics (Generic) +import Game.LambdaHack.Definition.DefsInternal+ -- | X spacial dimension for points and vectors. type X = Int  -- | Y xpacial dimension for points and vectors. type Y = Int --- If ever needed, we can use a symbol table here, since content--- is never serialized. But we'd need to cover the few cases--- (e.g., @litemFreq@) where @GroupName@ goes into savegame.-newtype GroupName a = GroupName {fromGroupName :: Text}-  deriving (Show, Eq, Ord, Hashable, Binary, NFData)- -- | For each group that the kind belongs to, denoted by a @GroupName@ -- in the first component of a pair, the second component of a pair shows -- how common the kind is within the group.-type Freqs a = [(GroupName a, Int)]+type Freqs c = [(GroupName c, Int)] +renameFreqs :: (Text -> Text) -> Freqs c -> Freqs c+renameFreqs f = map (first (GroupName . f . fromGroupName))+ -- | Rarity on given depths. The first element of the pair is normally -- in (0, 10] interval and, e.g., if there are 20 levels, 0.5 represents -- the first level and 10 the last. Exceptionally, it may be larger than 10,@@ -87,22 +88,6 @@   in y1 + ceiling             (intToDouble (y2 - y1) * (levelDepth10 - x1 * totalDepth)              / ((x2 - x1) * totalDepth))---- | Content identifiers for the content type @c@.-newtype ContentId c = ContentId Word16-  deriving (Show, Eq, Ord, Enum, Hashable, Binary)--toContentId :: Word16 -> ContentId c-{-# INLINE toContentId #-}-toContentId = ContentId--fromContentId :: ContentId c -> Word16-{-# INLINE fromContentId #-}-fromContentId (ContentId k) = k--contentIdIndex :: ContentId k -> Int-{-# INLINE contentIdIndex #-}-contentIdIndex (ContentId k) = fromEnum k  -- | Actor's item stores. data CStore =
+ definition-src/Game/LambdaHack/Definition/DefsInternal.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+-- | Very basic types for content definitions with their internals exposed.+module Game.LambdaHack.Definition.DefsInternal+  ( GroupName(..), displayGroupName+  , ContentId, toContentId, fromContentId, contentIdIndex+  , ContentSymbol, toContentSymbol, displayContentSymbol+  ) where++import Prelude ()++import Game.LambdaHack.Core.Prelude++import Control.DeepSeq+import Data.Binary+import Data.Hashable++-- If ever needed, we can use a symbol table here, since content+-- is never serialized. But we'd need to cover the few cases+-- (e.g., @litemFreq@) where @GroupName@ goes into savegame.+newtype GroupName c = GroupName {fromGroupName :: Text}+  deriving (Show, Eq, Ord, Hashable, Binary, NFData)++-- | This does not need to be 1-1, so should not be used in place of the+-- 'Eq' instance, etc.+displayGroupName :: GroupName c -> Text+displayGroupName = fromGroupName++-- | Content identifiers for the content type @c@.+newtype ContentId c = ContentId Word16+  deriving (Show, Eq, Ord, Enum, Hashable, Binary)++toContentId :: Word16 -> ContentId c+{-# INLINE toContentId #-}+toContentId = ContentId++fromContentId :: ContentId c -> Word16+{-# INLINE fromContentId #-}+fromContentId (ContentId k) = k++contentIdIndex :: ContentId c -> Int+{-# INLINE contentIdIndex #-}+contentIdIndex (ContentId k) = fromEnum k++-- TODO: temporary, not to break compilation too soon:+type ContentSymbol c = Char+toContentSymbol :: Char -> ContentSymbol c+toContentSymbol = id+displayContentSymbol :: ContentSymbol c -> Char+displayContentSymbol = id++-- TODO: The intended definitions. Error they are going to cause will+-- point out all the remaining item symbols hardwired in the engine+-- and make any future accidental hardwiring harder.+-- TODO2: extend to other content kinds than item kinds.+{-+-- | An abstract view on the symbol of a content item definition.+-- Hiding the constructor prevents hardwiring symbols inside the engine+-- by accident (this is still possible via conversion functions,+-- if one insists, so the abstraction is leaky, but that's fine).+newtype ContentSymbol c = ContentSymbol Char+  deriving (Show, Eq, Ord, Generic, Binary, NFData)++-- | This is a 1-1 inclusion. Don't use, if an equal named symbol already+-- exists in rules content.+toContentSymbol :: Char -> ContentSymbol c+{-# INLINE toContentSymbol #-}+toContentSymbol = ContentSymbol++-- | This does not need to be 1-1, so should not be used in place of the+-- 'Eq' instance, etc.+displayContentSymbol :: ContentSymbol c -> Char+{-# INLINE displayContentSymbol #-}+displayContentSymbol (ContentSymbol c) = c+--}
definition-src/Game/LambdaHack/Definition/Flavour.hs view
@@ -2,9 +2,10 @@ -- | The appearance of in-game items, as communicated to the player. module Game.LambdaHack.Definition.Flavour   ( -- * The @Flavour@ type-    Flavour(Flavour)+    Flavour   , -- * Constructors     zipPlain, zipFancy, zipLiquid, zipGlassPlain, zipGlassFancy, zipStory+  , dummyFlavour, stdFlavList   , -- * Accessors     flavourToColor, flavourToName     -- * Assorted@@ -43,7 +44,13 @@  instance Binary Flavour where   put = put . (toIntegralCrash :: Int -> Word16) . fromEnum-  get = fmap (toEnum . (intCast :: Word16 -> Int)) get+  get = fmap (toEnum . (into :: Word16 -> Int)) get  -- @Int doesn't suffice++dummyFlavour :: Flavour+dummyFlavour = Flavour Story Black++stdFlavList :: [Flavour]+stdFlavList = [Flavour fn bc | fn <- [minBound..maxBound], bc <- stdCol]  -- | Turn a colour set into a flavour set. zipPlain, zipFancy, zipLiquid, zipGlassPlain, zipGlassFancy, zipStory :: [Color] -> [Flavour]
engine-src/Game/LambdaHack/Atomic/CmdAtomic.hs view
@@ -56,7 +56,7 @@   | SfxAtomic SfxAtomic  -- ^ atomic special effects   deriving Show --- TODO: needed in the future, if efect, to save undo information:+-- TODO: needed in the future, if ever, to save undo information: -- --  deriving (Show, Eq, Generic) --@@ -135,6 +135,7 @@   | UpdWriteSave   | UpdHearFid FactionId (Maybe Int) HearMsg       -- in @UpdAtomic@ to let AI analyze and count+  | UpdMuteMessages FactionId Bool   deriving Show  -- | Symbolic representation of text messages about heard noises,@@ -275,6 +276,7 @@   UpdKillExit{} -> Nothing   UpdWriteSave -> Nothing   UpdHearFid{} -> Nothing+  UpdMuteMessages fid b -> Just $ UpdMuteMessages fid $ not b  undoSfxAtomic :: SfxAtomic -> SfxAtomic undoSfxAtomic cmd = case cmd of
engine-src/Game/LambdaHack/Atomic/HandleAtomicWrite.hs view
@@ -132,6 +132,7 @@   UpdKillExit{} -> return ()   UpdWriteSave -> return ()   UpdHearFid{} -> return ()+  UpdMuteMessages{} -> return ()  -- Actor's items may or may not be already present in @sitemD@, -- regardless if they are already present otherwise in the dungeon.@@ -452,7 +453,7 @@                -> m () updLeadFaction fid source target = assert (source /= target) $ do   fact <- getsState $ (EM.! fid) . sfactionD-  let !_A = assert (fleaderMode (gplayer fact) /= LeaderNull) ()+  let !_A = assert (fleaderMode (gplayer fact) /= Nothing) ()     -- @PosNone@ ensures this   mtb <- getsState $ \s -> flip getActorBody s <$> target   let !_A = assert (maybe True (not . bproj) mtb
engine-src/Game/LambdaHack/Atomic/PosAtomicRead.hs view
@@ -169,6 +169,7 @@   UpdKillExit fid -> return $! PosFid fid   UpdWriteSave -> return PosAll   UpdHearFid fid _ _ -> return $! PosFid fid+  UpdMuteMessages fid _ -> return $! PosFid fid  -- | Produce the positions where the atomic special effect takes place. posSfxAtomic :: MonadStateRead m => SfxAtomic -> m PosAtomic@@ -255,6 +256,7 @@   UpdKillExit{} -> []   UpdWriteSave -> []   UpdHearFid{} -> []+  UpdMuteMessages{} -> []  -- | All items introduced by the atomic special effect, to be used in it. iidSfxAtomic :: SfxAtomic -> [ItemId]
engine-src/Game/LambdaHack/Client.hs view
@@ -14,7 +14,7 @@   , Response (..)     -- * Re-exported from "Game.LambdaHack.Client.UI"   , CCUI-  , UIOptions, applyUIOptions, uCmdline, mkUIOptions+  , UIOptions, applyUIOptions, uOverrideCmdline, mkUIOptions   ) where  import Prelude ()
engine-src/Game/LambdaHack/Client/AI.hs view
@@ -32,7 +32,7 @@ -- faction is under human or computer control). queryAI :: MonadClient m => ActorId -> m RequestAI queryAI aid = do-  -- @sleader@ may be different from @gleader@ due to @stopPlayBack@,+  -- @sleader@ may be different from @gleader@ due to @restoreLeaderFromRun@,   -- but only leaders may change faction leader, so we fix that beforehand:   body <- getsState $ getActorBody aid   foeAssocs <- getsState $ foeRegularAssocs (bfid body) (blid body)
engine-src/Game/LambdaHack/Client/AI/ConditionM.hs view
@@ -50,7 +50,7 @@ import           Game.LambdaHack.Common.Vector import qualified Game.LambdaHack.Content.ItemKind as IK import           Game.LambdaHack.Content.ModeKind-import           Game.LambdaHack.Content.RuleKind+import qualified Game.LambdaHack.Content.RuleKind as RK import qualified Game.LambdaHack.Core.Dice as Dice import qualified Game.LambdaHack.Definition.Ability as Ability import           Game.LambdaHack.Definition.Defs@@ -59,7 +59,7 @@ -- used in the strict monadic computations they are in.  -- | Require that a target enemy is visible by the party.-condAimEnemyTargetedM :: MonadClient m => ActorId -> m Bool+condAimEnemyTargetedM :: MonadClientRead m => ActorId -> m Bool condAimEnemyTargetedM aid = do   btarget <- getsClient $ getTarget aid   return $ case btarget of@@ -67,7 +67,7 @@     _ -> False  -- | Require that a target enemy or enemy stash is visible by the party.-condAimEnemyOrStashM :: MonadClient m => ActorId -> m Bool+condAimEnemyOrStashM :: MonadClientRead m => ActorId -> m Bool condAimEnemyOrStashM aid = do   btarget <- getsClient $ getTarget aid   return $ case btarget of@@ -76,7 +76,7 @@     _ -> False  -- | Require that a target enemy is remembered on the actor's level.-condAimEnemyOrRememberedM :: MonadClient m => ActorId -> m Bool+condAimEnemyOrRememberedM :: MonadClientRead m => ActorId -> m Bool condAimEnemyOrRememberedM aid = do   b <- getsState $ getActorBody aid   btarget <- getsClient $ getTarget aid@@ -87,7 +87,7 @@     _ -> False  -- | Require that a target non-enemy is visible by the party.-condAimNonEnemyPresentM :: MonadClient m => ActorId -> m Bool+condAimNonEnemyPresentM :: MonadClientRead m => ActorId -> m Bool condAimNonEnemyPresentM aid = do   btarget <- getsClient $ getTarget aid   return $ case btarget of@@ -96,7 +96,7 @@  -- | Require that the target is crucial to success, e.g., an item, -- or that it's not too far away and so the changes to get it are high.-condAimCrucialM :: MonadClient m => ActorId -> m Bool+condAimCrucialM :: MonadClientRead m => ActorId -> m Bool condAimCrucialM aid = do   b <- getsState $ getActorBody aid   mtgtMPath <- getsClient $ EM.lookup aid . stargetD@@ -114,7 +114,7 @@     _ -> False  -- includes the case of target with no path  -- | Check if the target is a nonmoving enemy.-condTgtNonmovingEnemyM :: MonadClient m => ActorId -> m Bool+condTgtNonmovingEnemyM :: MonadClientRead m => ActorId -> m Bool condTgtNonmovingEnemyM aid = do   btarget <- getsClient $ getTarget aid   case btarget of@@ -125,7 +125,7 @@  -- | Require the actor stands on or adjacent to a triggerable tile -- (e.g., stairs).-condAdjTriggerableM :: MonadClient m => Ability.Skills -> ActorId -> m Bool+condAdjTriggerableM :: MonadStateRead m => Ability.Skills -> ActorId -> m Bool condAdjTriggerableM actorSk aid = do   COps{coTileSpeedup} <- getsState scops   b <- getsState $ getActorBody aid@@ -164,7 +164,7 @@   in sortBy (comparing fst) $ map addDist allThreats  -- | Require the actor blocks the paths of any of his party members.-condBlocksFriendsM :: MonadClient m => ActorId -> m Bool+condBlocksFriendsM :: MonadClientRead m => ActorId -> m Bool condBlocksFriendsM aid = do   b <- getsState $ getActorBody aid   targetD <- getsClient stargetD@@ -189,10 +189,10 @@     getsState (fullAssocs aid [CEqp])  -- | Require that the actor can project any items.-condCanProjectM :: MonadClient m => Int -> ActorId -> m Bool+condCanProjectM :: MonadClientRead m => Int -> ActorId -> m Bool condCanProjectM skill aid = do   side <- getsClient sside-  curChal <- getsClient $ scurChal+  curChal <- getsClient scurChal   fact <- getsState $ (EM.! side) . sfactionD   if skill < 1      || ckeeper curChal && fhasUI (gplayer fact)@@ -203,7 +203,7 @@     -- is reached.     not . null <$> condProjectListM skill aid -condProjectListM :: MonadClient m+condProjectListM :: MonadClientRead m                  => Int -> ActorId                  -> m [(Double, CStore, ItemId, ItemFull, ItemQuant)] condProjectListM skill aid = do@@ -280,12 +280,12 @@         && 0 > IA.getSkill Ability.SkHurtMelee arItem  -- | Require that the actor stands over a desirable item.-condDesirableFloorItemM :: MonadClient m => ActorId -> m Bool+condDesirableFloorItemM :: MonadClientRead m => ActorId -> m Bool condDesirableFloorItemM aid = not . null <$> benGroundItems aid  -- | Produce the list of items on the ground beneath the actor -- that are worth picking up.-benGroundItems :: MonadClient m+benGroundItems :: MonadClientRead m                => ActorId                -> m [(Benefit, CStore, ItemId, ItemFull, ItemQuant)] benGroundItems aid = do@@ -303,12 +303,13 @@  desirableItem :: COps -> Bool -> Double -> IA.AspectRecord -> IK.ItemKind -> Int               -> Bool-desirableItem COps{corule=RuleContent{rsymbolProjectile}}+desirableItem COps{corule}               canEsc benPickup arItem itemKind k =-  let loneProjectile = IK.isymbol itemKind == rsymbolProjectile-                       && k == 1-                       && Dice.infDice (IK.icount itemKind) > 1-                            -- never generated as lone; usually means weak+  let loneProjectile =+        IK.isymbol itemKind == IK.rsymbolProjectile (RK.ritemSymbols corule)+        && k == 1+        && Dice.infDice (IK.icount itemKind) > 1+             -- never generated as lone; usually means weak       useful = if canEsc                then benPickup > 0                     || IA.checkFlag Ability.Precious arItem@@ -318,7 +319,8 @@                  in benPickup > 0 && not preciousNotUseful   in useful && not loneProjectile -condSupport :: MonadClient m => [(ActorId, Actor)] -> Int -> ActorId -> m Bool+condSupport :: MonadClientRead m+            => [(ActorId, Actor)] -> Int -> ActorId -> m Bool {-# INLINE condSupport #-} condSupport friendAssocs param aid = do   mtgtMPath <- getsClient $ EM.lookup aid . stargetD@@ -348,7 +350,7 @@ -- The numbers reflect fleeing AI conditions for non-aggresive actors -- so that actors don't wait for support that is not possible due to not -- enough friends on the level, even counting sleeping ones.-condAloneM :: MonadClient m => [(ActorId, Actor)] -> ActorId -> m Bool+condAloneM :: MonadStateRead m => [(ActorId, Actor)] -> ActorId -> m Bool condAloneM friendAssocs aid = do   b <- getsState $ getActorBody aid   mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b@@ -366,7 +368,7 @@   return $ not aInAmbient  -- tile is dark, so actor could hide  -- | Produce a list of acceptable adjacent points to flee to.-fleeList :: MonadClient m+fleeList :: MonadClientRead m          => [(ActorId, Actor)] -> ActorId -> m ([(Int, Point)], [(Int, Point)]) fleeList foeAssocs aid = do   COps{coTileSpeedup} <- getsState scops
engine-src/Game/LambdaHack/Client/AI/PickActionM.hs view
@@ -54,6 +54,7 @@ import           Game.LambdaHack.Definition.Ability import qualified Game.LambdaHack.Definition.Ability as Ability import           Game.LambdaHack.Definition.Defs+import qualified Game.LambdaHack.Definition.DefsInternal as DefsInternal  -- | Pick the most desirable AI ation for the actor. pickAction :: MonadClient m@@ -68,7 +69,7 @@   let !_A = assert (not (bproj body)                     `blame` "AI gets to manually move its projectiles"                     `swith` (aid, bfid body, side)) ()-  stratAction <- actionStrategy foeAssocs friendAssocs aid retry+  stratAction <- actionStrategy foeAssocs friendAssocs (blid body) aid retry   let bestAction = bestVariant stratAction       !_A = assert (not (nullFreq bestAction)  -- equiv to nullStrategy                     `blame` "no AI action for actor"@@ -78,16 +79,36 @@  -- AI strategy based on actor's sight, smell, etc. -- Never empty.-actionStrategy :: forall m. MonadClient m-               => [(ActorId, Actor)] -> [(ActorId, Actor)] -> ActorId -> Bool+actionStrategy :: MonadClient m+               => [(ActorId, Actor)] -> [(ActorId, Actor)]+               -> LevelId -> ActorId -> Bool                -> m (Strategy RequestTimed)-actionStrategy foeAssocs friendAssocs aid retry = do+actionStrategy foeAssocs friendAssocs lid aid retry = do+  condInMelee <- condInMeleeM lid+  randomAggressionThreshold <- rndToAction $ randomR0 10+  actionStrategyRead condInMelee foeAssocs friendAssocs+                     randomAggressionThreshold lid aid retry++-- This is close to being in @MonadClientRead@, but not quite, due to+-- random numbers used explicitly in a couple of places (e.g., weapon choice),+-- which should be fixed and expressed via @Strategy@ instead,+-- and due to recoding the fleeing preference, which should be+-- factored out and done in the @actionStrategy@ wrapper.+--+-- After the AI code is totally rewritten soon, this should be revisited.+actionStrategyRead :: forall m. MonadClient m+                   => Bool+                   -> [(ActorId, Actor)] -> [(ActorId, Actor)]+                   -> Int -> LevelId -> ActorId -> Bool+                   -> m (Strategy RequestTimed)+actionStrategyRead condInMelee foeAssocs friendAssocs+                   randomAggressionThreshold lid aid retry = do   COps{coTileSpeedup} <- getsState scops   mleader <- getsClient sleader   body <- getsState $ getActorBody aid-  lvl <- getLevel (blid body)-  localTime <- getsState $ getLocalTime (blid body)-  condInMelee <- condInMeleeM $ blid body+  let !_A = assert (blid body == lid) ()+  lvl <- getLevel lid+  localTime <- getsState $ getLocalTime lid   condAimEnemyTargeted <- condAimEnemyTargetedM aid   condAimEnemyOrStash <- condAimEnemyOrStashM aid   condAimEnemyOrRemembered <- condAimEnemyOrRememberedM aid@@ -115,14 +136,13 @@   condFloorWeapon <- condFloorWeaponM aid   condDesirableFloorItem <- condDesirableFloorItemM aid   condTgtNonmovingEnemy <- condTgtNonmovingEnemyM aid-  randomAggressionThreshold <- rndToAction $ randomR0 10   explored <- getsClient sexplored   -- This doesn't treat actors guarding stash specially, so on such levels   -- man sleeping actors may reside for a long time. Variety, OK.   let awakeAndNotGuarding (_, b) =         bpos b /= bpos body         && bwatch b /= WSleep-        && Just (blid b, bpos b) /= gstash fact+        && Just (lid, bpos b) /= gstash fact       anyFriendOnLevelAwake = any awakeAndNotGuarding friendAssocs       actorMaxSk = actorMaxSkills EM.! aid       recentlyFled = maybe False (\(_, time) -> timeRecent5 localTime time)@@ -145,7 +165,7 @@                 _ -> False               && mayFallAsleep               && Just aid /= mleader  -- best teammate for a task so stop dozing-      lidExplored = ES.member (blid body) explored+      lidExplored = ES.member lid explored       panicFleeL = fleeL ++ badVic       condHpTooLow = hpTooLow body actorMaxSk       heavilyDistressed =  -- actor hit by a proj or similarly distressed@@ -255,9 +275,9 @@                     || condManyThreatsAdj && not condSupport1 && not condSolo                   | case gstash fact of                       Nothing -> False-                      Just (lid, pos) ->-                        lid == blid body-                        && chessDist pos (bpos body) <= 2 -> False+                      Just (lid2, pos) ->+                        lid2 == lid && chessDist pos (bpos body) <= 2+                        -> False                   | condInMelee -> False                       -- No fleeing when others melee and no critical threat                       -- (otherwise no target nor action would be possible).@@ -340,8 +360,7 @@       -- Note that all monadic actions here are performed when the strategy       -- is being chosen so, e.g., none of them can be @flee@ or the actor       -- would be marked in state as fleeing even when the strategy is-      -- not chosen. TODO: introduce @MonadClientRead@ and so ensure with types-      -- that none of these actions modifie state.+      -- not chosen.       distant :: [([Skill], m (Frequency RequestTimed), Bool)]       distant =         [ ( [SkMoveItem]@@ -375,7 +394,7 @@                               200)             $ chase actorSk aid avoidAmbient retry           , condCanMelee-            && Just (blid body, bpos body) /= gstash fact+            && Just (lid, bpos body) /= gstash fact             && (if condInMelee then condAimEnemyOrStash                 else (condAimEnemyOrRemembered                       || condAimNonEnemyPresent)@@ -410,7 +429,7 @@             && if condInMelee                then condCanMelee && condAimEnemyOrStash                else (not (condThreat 2) || not condMeleeBad)-                    && (Just (blid body, bpos body) /= gstash fact+                    && (Just (lid, bpos body) /= gstash fact                         || heavilyDistressed  -- guard strictly, until harmed                         || length oursExploring <= 1                         || condOurAdj  -- or if teammates adjacent@@ -477,13 +496,13 @@   then return $! returN "sleep" ReqWait   else tryStrategies sums -waitBlockNow :: MonadClient m => m (Strategy RequestTimed)+waitBlockNow :: MonadClientRead m => m (Strategy RequestTimed) waitBlockNow = return $! returN "wait" ReqWait -yellNow :: MonadClient m => m (Strategy RequestTimed)+yellNow :: MonadClientRead m => m (Strategy RequestTimed) yellNow = return $! returN "yell" ReqYell -pickup :: MonadClient m => ActorId -> Bool -> m (Strategy RequestTimed)+pickup :: MonadClientRead m => ActorId -> Bool -> m (Strategy RequestTimed) pickup aid onlyWeapon = do   benItemL <- benGroundItems aid   b <- getsState $ getActorBody aid@@ -515,7 +534,7 @@ -- and with @benInEqp@ (which implies @goesIntoEqp@). -- Such items are moved between any stores, as needed. In this case, -- from stash to eqp.-equipItems :: MonadClient m => ActorId -> m (Strategy RequestTimed)+equipItems :: MonadClientRead m => ActorId -> m (Strategy RequestTimed) equipItems aid = do   body <- getsState $ getActorBody aid   actorMaxSk <- getsState $ getActorMaxSkills aid@@ -586,7 +605,7 @@             then reject             else returN "equipItems" $ ReqMoveItems prepared -yieldUnneeded :: MonadClient m => ActorId -> m (Strategy RequestTimed)+yieldUnneeded :: MonadClientRead m => ActorId -> m (Strategy RequestTimed) yieldUnneeded aid = do   body <- getsState $ getActorBody aid   actorMaxSk <- getsState $ getActorMaxSkills aid@@ -623,7 +642,7 @@  -- This only concerns items that @equipItems@ handles, that is -- with a slot and with @benInEqp@ (which implies @goesIntoEqp@).-unEquipItems :: MonadClient m => ActorId -> m (Strategy RequestTimed)+unEquipItems :: MonadClientRead m => ActorId -> m (Strategy RequestTimed) unEquipItems aid = do   body <- getsState $ getActorBody aid   actorMaxSk <- getsState $ getActorMaxSkills aid@@ -823,7 +842,7 @@ -- TODO: In @actionStrategy@ we require minimal @SkAlter@ even for the case -- of triggerable tile underfoot. Let's say this quirk is a specialization -- of AI actors, because there are usually many, so not all need to trigger.-trigger :: MonadClient m+trigger :: MonadClientRead m         => ActorId -> FleeViaStairsOrEscape         -> m (Strategy RequestTimed) trigger aid fleeVia = do@@ -843,7 +862,7 @@          && not (occupiedProjLvl pos lvl) -- AlterBlockActor          && EM.notMember pos (lfloor lvl) ]  -- AlterBlockItem -projectItem :: MonadClient m+projectItem :: MonadClientRead m             => Ability.Skills -> ActorId -> m (Strategy RequestTimed) projectItem actorSk aid = do   btarget <- getsClient $ getTarget aid@@ -851,7 +870,7 @@   -- We query target, not path, because path is not needed for flinging.   -- Even if unknown tiles exist between us and the target, we assume   -- they are walkable and not just transparent and we happily try to shoot.-  mfpos <- getsState $ aidTgtToPos aid (blid b) btarget+  mfpos <- getsState $ aidTgtToPos (Just aid) (blid b) btarget   case (btarget, mfpos) of     (_, Just fpos) | adjacent (bpos b) fpos -> return reject     (Just (TEnemy aidE), Just fpos) -> do@@ -893,10 +912,11 @@ data ApplyItemGroup = ApplyAll | ApplyFirstAid   deriving Eq -applyItem :: MonadClient m+applyItem :: MonadClientRead m           => Ability.Skills -> ActorId -> ApplyItemGroup           -> m (Strategy RequestTimed) applyItem actorSk aid applyGroup = do+  COps{corule} <- getsState scops   b <- getsState $ getActorBody aid   fact <- getsState $ (EM.! bfid b) . sfactionD   condShineWouldBetray <- condShineWouldBetrayM aid@@ -915,7 +935,7 @@       canEsc = fcanEscape (gplayer fact)       permittedActor cstore itemFull kit =         fromRight False-        $ permittedApply localTime skill calmE cstore itemFull kit+        $ permittedApply corule localTime skill calmE cstore itemFull kit       disqualify :: Bool -> IK.Effect -> Bool       -- These effects tweak items, which is only situationally beneficial       -- and not really the best idea while in combat.@@ -962,9 +982,9 @@         let itemKind = getKind iid         in if maybe False (> 0) $ lookup IK.CONDITION $ IK.ifreq itemKind            then Just $ if benInEqp (discoBenefit EM.! iid)-                       then Right $ GroupName $ IK.iname itemKind+                       then Right $ DefsInternal.GroupName $ IK.iname itemKind                          -- conveniently, @iname@ matches @ifreq@-                       else Left $ GroupName $ IK.iname itemKind+                       else Left $ DefsInternal.GroupName $ IK.iname itemKind            else Nothing) (EM.keys $ borgan b)       fTool benAv@( Benefit{benApply}, cstore, iid                   , itemFull@ItemFull{itemKind}, _ ) =@@ -1059,7 +1079,7 @@  -- The result of all these conditions is that AI displaces rarely, -- but it can't be helped as long as the enemy is smart enough to form fronts.-displaceFoe :: MonadClient m => ActorId -> m (Strategy RequestTimed)+displaceFoe :: MonadClientRead m => ActorId -> m (Strategy RequestTimed) displaceFoe aid = do   COps{coTileSpeedup} <- getsState scops   b <- getsState $ getActorBody aid@@ -1093,7 +1113,7 @@   foes <- mapM qualifyActor adjFoes   return $! liftFrequency $ toFreq "displaceFoe" $ catMaybes foes -displaceBlocker :: MonadClient m => ActorId -> Bool -> m (Strategy RequestTimed)+displaceBlocker :: MonadClientRead m => ActorId -> Bool -> m (Strategy RequestTimed) displaceBlocker aid retry = do   b <- getsState $ getActorBody aid   mtgtMPath <- getsClient $ EM.lookup aid . stargetD@@ -1109,7 +1129,7 @@         displaceTgt aid q retry     _ -> return reject  -- goal reached -displaceTgt :: MonadClient m+displaceTgt :: MonadClientRead m             => ActorId -> Point -> Bool -> m (Strategy RequestTimed) displaceTgt source tpos retry = do   COps{coTileSpeedup} <- getsState scops@@ -1165,14 +1185,14 @@                  else reject     _ -> return reject  -- DisplaceProjectiles and no blocker at all -chase :: MonadClient m+chase :: MonadClientRead m       => Ability.Skills -> ActorId -> Bool -> Bool -> m (Strategy RequestTimed) chase actorSk aid avoidAmbient retry = do   body <- getsState $ getActorBody aid   fact <- getsState $ (EM.! bfid body) . sfactionD   mtgtMPath <- getsClient $ EM.lookup aid . stargetD   let -- With no leader, the goal is vague, so permit arbitrary detours.-      relaxed = fleaderMode (gplayer fact) == LeaderNull+      relaxed = isNothing $ fleaderMode (gplayer fact)       strAmbient avoid = case mtgtMPath of         Just TgtAndPath{tapPath=Just AndPath{pathList=q : _, ..}} ->           if pathGoal == bpos body@@ -1185,7 +1205,7 @@          else return strAvoided   mapStrategyM (moveOrRunAid actorSk aid) str -moveTowards :: MonadClient m+moveTowards :: MonadClientRead m             => Ability.Skills -> ActorId -> Bool -> Point -> Point -> Bool             -> m (Strategy Vector) moveTowards actorSk aid avoidAmbient target goal relaxed = do@@ -1240,7 +1260,7 @@ -- Actor moves or searches or alters or attacks. -- This function is very general, even though it's often used in contexts -- when only one or two of the many cases can possibly occur.-moveOrRunAid :: MonadClient m+moveOrRunAid :: MonadClientRead m              => Ability.Skills -> ActorId -> Vector -> m (Maybe RequestTimed) moveOrRunAid actorSk source dir = do   COps{coTileSpeedup} <- getsState scops
engine-src/Game/LambdaHack/Client/AI/PickTargetM.hs view
@@ -82,7 +82,7 @@               => [(ActorId, Actor)] -> [(ActorId, Actor)] -> ActorId               -> m (Maybe TgtAndPath) computeTarget foeAssocs friendAssocs aid = do-  cops@COps{cocave, corule=RuleContent{rXmax, rYmax, rnearby}, coTileSpeedup}+  cops@COps{cocave, corule=RuleContent{rWidthMax, rHeightMax, rnearby}, coTileSpeedup}     <- getsState scops   b <- getsState $ getActorBody aid   mleader <- getsClient sleader@@ -109,7 +109,7 @@       -- This is also triggered by @UpdLeadFaction@.       Just <$> createPath aid tapTgt     Just tap@TgtAndPath{tapTgt,tapPath=Just AndPath{..}} -> do-      mvalidPos <- getsState $ aidTgtToPos aid (blid b) (Just tapTgt)+      mvalidPos <- getsState $ aidTgtToPos (Just aid) (blid b) (Just tapTgt)       return $!         if | isNothing mvalidPos -> Nothing  -- wrong level            | bpos b == pathGoal ->@@ -316,7 +316,7 @@                       if nullFreq ctriggers then do                         let oldpos = fromMaybe (bpos b) (boldpos b)                             vOld = bpos b `vectorToFrom` oldpos-                            pNew = shiftBounded rXmax rYmax (bpos b) vOld+                            pNew = shiftBounded rWidthMax rHeightMax (bpos b) vOld                         if slackDoctrine && not isStuck && calmE && not focused                            && isUnit vOld && bpos b /= pNew                                 -- both are needed, e.g., when just teleported@@ -337,7 +337,7 @@                           -- animals. Heroes idling on the level help a lot.                           let pathSource = bpos b                               traSlack7 = trajectoryToPathBounded-                                            rXmax rYmax pathSource+                                            rWidthMax rHeightMax pathSource                                             (replicate 7 v)  -- > 6 from take6                               pathList = map head $ group traSlack7                               pathGoal = last pathList@@ -465,6 +465,10 @@             | bpos b == pos -> tellOthersNothingHere             | recentlyFled -> pickNewTarget                 -- forget enemy positions to prevent attacking them again soon+            | not (couldMoveLastTurn || null nearbyFoes) -> pickNewTarget+                -- if only, possibly, shooting, forget hotspots, target foes;+                -- this results in only pointman humans chasing old foes+                -- in preference of new visible ones, but it's fine             | otherwise -> do               -- Here pick the closer enemy, the remembered or seen, to avoid               -- loops when approaching new enemy obscures him behind obstacle
engine-src/Game/LambdaHack/Client/BfsM.hs view
@@ -213,7 +213,7 @@                                  , pathLen = length walkable + 1 }             in TgtAndPath{tapTgt = newTgt, tapPath = Just newPath}       stopAtUnwalkable Nothing = TgtAndPath{tapTgt, tapPath=Nothing}-  mpos <- getsState $ aidTgtToPos aid (blid b) (Just tapTgt)+  mpos <- getsState $ aidTgtToPos (Just aid) (blid b) (Just tapTgt)   case mpos of     Nothing -> return TgtAndPath{tapTgt, tapPath=Nothing}     Just p -> do@@ -453,7 +453,7 @@ closestTriggers :: MonadClient m => FleeViaStairsOrEscape -> ActorId                 -> m [(Int, (Point, (Point, ItemBag)))] closestTriggers fleeVia aid = do-  COps{corule=RuleContent{rXmax, rYmax}} <- getsState scops+  COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops   b <- getsState $ getActorBody aid   lvl <- getLevel (blid b)   let pbags = EM.assocs $ lembed lvl@@ -466,7 +466,8 @@   -- OTOH, siege of stairs or escapes is more effective.   bfs <- getCacheBfs aid   let vicTrigger (cid, (p0, bag)) =-        map (\p -> (cid, (p, (p0, bag)))) $ vicinityBounded rXmax rYmax p0+        map (\p -> (cid, (p, (p0, bag))))+            (vicinityBounded rWidthMax rHeightMax p0)       vicAll = concatMap vicTrigger efeat   return $!     let mix (benefit, ppbag) dist =@@ -480,7 +481,7 @@ -- We assume weapons in equipment are better than any among organs -- or at least provide some essential diversity. -- Disabled if, due to doctrine, actors follow leader and so would--- repeatedly move towards and away form stairs at leader change,+-- repeatedly move towards and away from stairs at leader change, -- depending on current leader's gear. -- Number of items of a single kind is ignored, because variety is needed. condEnoughGearM :: MonadClientRead m => ActorId -> m Bool
engine-src/Game/LambdaHack/Client/CommonM.hs view
@@ -45,9 +45,9 @@  -- | Calculate the position of an actor's target. -- This matches @pathGoal@, but sometimes path is not defined.-aidTgtToPos :: ActorId -> LevelId -> Maybe Target -> State -> Maybe Point+aidTgtToPos :: Maybe ActorId -> LevelId -> Maybe Target -> State -> Maybe Point aidTgtToPos _ _ Nothing _ = Nothing-aidTgtToPos aid lidV (Just tgt) s = case tgt of+aidTgtToPos maid lidV (Just tgt) s = case tgt of   TEnemy a ->     let body = getActorBody a s     in if blid body == lidV then Just (bpos body) else Nothing@@ -56,11 +56,13 @@     in if blid body == lidV then Just (bpos body) else Nothing   TPoint _ lid p ->     if lid == lidV then Just p else Nothing-  TVector v ->-    let COps{corule=RuleContent{rXmax, rYmax}} = scops s-        b = getActorBody aid s-        shifted = shiftBounded rXmax rYmax (bpos b) v-    in if shifted == bpos b && v /= Vector 0 0 then Nothing else Just shifted+  TVector v -> case maid of+    Nothing -> Nothing+    Just aid ->+      let COps{corule=RuleContent{rWidthMax, rHeightMax}} = scops s+          b = getActorBody aid s+          shifted = shiftBounded rWidthMax rHeightMax (bpos b) v+      in if shifted == bpos b && v /= Vector 0 0 then Nothing else Just shifted  -- | Counts the number of steps until the projectile would hit a non-projectile -- actor or obstacle. Starts searching with the given eps and returns@@ -69,10 +71,10 @@ -- Treats unknown tiles as walkable, but prefers known. makeLine :: Bool -> Actor -> Point -> Int -> COps -> Level -> Maybe Int makeLine onlyFirst body fpos epsOld cops lvl =-  let COps{corule=RuleContent{rXmax, rYmax}, coTileSpeedup} = cops+  let COps{coTileSpeedup} = cops       dist = chessDist (bpos body) fpos       calcScore :: Int -> Int-      calcScore eps = case bla rXmax rYmax eps (bpos body) fpos of+      calcScore eps = case bla eps (bpos body) fpos of         Just bl ->           let blDist = take (dist - 1) bl  -- goal not checked; actor well aware               noActor p = p == fpos || not (occupiedBigLvl p lvl)
engine-src/Game/LambdaHack/Client/HandleAtomicM.hs view
@@ -4,7 +4,7 @@   , cmdAtomicSemCli #ifdef EXPOSE_INTERNAL     -- * Internal operations-  , invalidateInMelee, invalidateInMeleeDueToItem+  , updateInMeleeDueToActor, updateInMeleeDueToItem, updateInMeleeInDungeon   , wipeBfsIfItemAffectsSkills, tileChangeAffectsBfs   , createActor, destroyActor   , addItemToDiscoBenefit, perception@@ -31,6 +31,7 @@ import           Game.LambdaHack.Client.State import           Game.LambdaHack.Common.Actor import           Game.LambdaHack.Common.ActorState+import           Game.LambdaHack.Common.ClientOptions import           Game.LambdaHack.Common.Faction import           Game.LambdaHack.Common.Item import           Game.LambdaHack.Common.Kind@@ -60,62 +61,72 @@   UpdCreateActor aid b ais -> createActor aid b ais   UpdDestroyActor aid b _ -> destroyActor aid b True   UpdCreateItem _ iid _ _ (CActor aid store) -> do-    wipeBfsIfItemAffectsSkills [store] aid+    wipeBfsIfItemAffectsSkills store aid     addItemToDiscoBenefit iid-    invalidateInMeleeDueToItem aid store+    updateInMeleeDueToItem aid store   UpdCreateItem _ iid _ _ _ -> addItemToDiscoBenefit iid   UpdDestroyItem _ _ _ _ (CActor aid store) -> do-    wipeBfsIfItemAffectsSkills [store] aid-    invalidateInMeleeDueToItem aid store+    wipeBfsIfItemAffectsSkills store aid+    updateInMeleeDueToItem aid store   UpdDestroyItem{} -> return ()   UpdSpotActor aid b -> do     ais <- getsState $ getCarriedAssocsAndTrunk b     createActor aid b ais   UpdLoseActor aid b -> destroyActor aid b False   UpdSpotItem _ iid _ (CActor aid store) -> do-    wipeBfsIfItemAffectsSkills [store] aid+    wipeBfsIfItemAffectsSkills store aid     addItemToDiscoBenefit iid-    invalidateInMeleeDueToItem aid store+    updateInMeleeDueToItem aid store   UpdSpotItem _ iid _ _ -> addItemToDiscoBenefit iid   UpdLoseItem _ _ _ (CActor aid store) -> do-    wipeBfsIfItemAffectsSkills [store] aid-    invalidateInMeleeDueToItem aid store+    wipeBfsIfItemAffectsSkills store aid+    updateInMeleeDueToItem aid store   UpdLoseItem{} -> return ()   UpdSpotItemBag _ (CActor aid store) bag -> do-    wipeBfsIfItemAffectsSkills [store] aid+    wipeBfsIfItemAffectsSkills store aid     mapM_ addItemToDiscoBenefit $ EM.keys bag-    invalidateInMeleeDueToItem aid store+    updateInMeleeDueToItem aid store   UpdSpotItemBag _ _ bag ->     mapM_ addItemToDiscoBenefit $ EM.keys bag   UpdLoseItemBag _ (CActor aid store) _ -> do-    wipeBfsIfItemAffectsSkills [store] aid-    invalidateInMeleeDueToItem aid store+    wipeBfsIfItemAffectsSkills store aid+    updateInMeleeDueToItem aid store   UpdLoseItemBag{} -> return ()   UpdMoveActor aid _ _ -> do     invalidateBfsAid aid-    b <- getsState $ getActorBody aid-    -- BFS not invalidated, because distant actors may still move out+    -- other BFSes not invalidated, because distant actors may still move out     -- of the way and close actors are considered when attempting to move     -- and then BFS is invalidated, if needed.-    invalidateInMelee (blid b)-  UpdWaitActor aid _fromW toW ->+    b <- getsState $ getActorBody aid+    updateInMeleeDueToActor b+  UpdWaitActor aid _fromW toW -> do     -- So that we can later ignore such actors when updating targets     -- and not risk they being pushed/displaced and targets getting illegal.     when (toW == WSleep) $       modifyClient $ updateTarget aid (const Nothing)+    b <- getsState $ getActorBody aid+    updateInMeleeDueToActor b  -- @bwatch@ checked in several places   UpdDisplaceActor source target -> do     invalidateBfsAid source     invalidateBfsAid target-    b <- getsState $ getActorBody source-    -- BFS not invalidated, because distant actors may still move out+    -- other BFSes not invalidated, because distant actors may still move out     -- of the way and close actors are considered when attempting to move     -- and then BFS is invalidated, if needed.-    invalidateInMelee (blid b)+    sb <- getsState $ getActorBody source+    -- At least one of these is not a projectile and both move, so update.+    insertInMeleeM (blid sb)   UpdMoveItem _ _ aid s1 s2 -> do-    wipeBfsIfItemAffectsSkills [s1, s2] aid-    invalidateInMeleeDueToItem aid s1-    invalidateInMeleeDueToItem aid s2-  UpdRefillHP{} -> return ()+    wipeBfsIfItemAffectsSkills s1 aid+    wipeBfsIfItemAffectsSkills s2 aid+    updateInMeleeDueToItem aid s1+    updateInMeleeDueToItem aid s2+  UpdRefillHP _ 0 -> return ()+  UpdRefillHP aid delta -> do+    b <- getsState $ getActorBody aid+    unless (bproj b+            || signum (bhp b)  -- new HP+               == signum (bhp b - delta)) $  -- old HP+      insertInMeleeM (blid b)  -- @bhp@ checked in several places   UpdRefillCalm{} -> return ()   UpdTrajectory{} -> return ()   UpdQuitFaction fid _ toSt _ -> do@@ -148,8 +159,16 @@                         `blame` "unexpected leader"                         `swith` (cmd, mleader)) ()       modifyClient $ \cli -> cli {_sleader = target}-  UpdDiplFaction{} -> return ()-  UpdAutoFaction{} ->+  UpdDiplFaction{} ->+    -- Depends on who is a foe as opposed to a neutral actor.+    updateInMeleeInDungeon+  UpdAutoFaction{} -> do+    -- Regaining control of faction cancels some --stopAfter*.+    -- This is really a UI client issue, but is in general client state+    -- to make it simpler to set this via commandline.+    modifyClient $ \cli ->+      cli {soptions = (soptions cli) { sstopAfterSeconds = Nothing+                                     , sstopAfterFrames = Nothing }}     -- @condBFS@ depends on the setting we change here (e.g., smarkSuspect).     invalidateBfsAll   UpdRecordKill{} -> return ()@@ -235,35 +254,40 @@   UpdDiscoverServer{} -> error "server command leaked to client"   UpdCoverServer{} -> error "server command leaked to client"   UpdPerception lid outPer inPer -> perception lid outPer inPer-  UpdRestart side sfper s scurChal soptions srandom -> do+  UpdRestart side sfper _ scurChal soptionsNew srandom -> do     COps{cocave} <- getsState scops     fact <- getsState $ (EM.! side) . sfactionD     snxtChal <- getsClient snxtChal+    smarkSuspect <- getsClient smarkSuspect     svictories <- getsClient svictories     scampings <- getsClient scampings     srestarts <- getsClient srestarts     stabs <- getsClient stabs+    soptionsOld <- getsClient soptions     let h lvl = CK.labyrinth (okind cocave $ lkind lvl)                 && not (fhasGender $ gplayer fact)           -- Not to burrow through a labyrinth instead of leaving it for           -- the human player and to prevent AI losing time there instead           -- of congregating at exits.-        sexplored = EM.keysSet $ EM.filter h $ sdungeon s-        cli = emptyStateClient side+    sexplored <- getsState $ EM.keysSet . EM.filter h . sdungeon+    let cli = emptyStateClient side     putClient cli { sexplored                   -- , sundo = [UpdAtomic cmd]                   , sfper                   , srandom                   , scurChal                   , snxtChal-                  , scondInMelee = EM.empty+                  , smarkSuspect                   , svictories                   , scampings                   , srestarts-                  , soptions+                  , soptions =+                      soptionsNew {snoAnim =  -- persist @snoAnim@ between games+                        snoAnim soptionsOld `mplus` snoAnim soptionsNew}                   , stabs }     salter <- getsState createSalter     modifyClient $ \cli1 -> cli1 {salter}+    updateInMeleeInDungeon   UpdRestartServer{} -> return ()   UpdResume _side sfperNew -> do #ifdef WITH_EXPENSIVE_ASSERTIONS@@ -278,23 +302,29 @@   UpdKillExit _fid -> killExit   UpdWriteSave -> saveClient   UpdHearFid{} -> return ()+  UpdMuteMessages{} -> return () --- This field is only needed in AI client, but it's on-demand for each level--- and so fairly cheap.-invalidateInMelee :: MonadClient m => LevelId -> m ()-invalidateInMelee lid =-  modifyClient $ \cli -> cli {scondInMelee = EM.delete lid (scondInMelee cli)}+updateInMeleeDueToActor :: MonadClient m => Actor -> m ()+updateInMeleeDueToActor b =+  unless (bproj b) $+    insertInMeleeM (blid b) -invalidateInMeleeDueToItem :: MonadClient m => ActorId -> CStore -> m ()-invalidateInMeleeDueToItem aid store =+updateInMeleeDueToItem :: MonadClient m => ActorId -> CStore -> m ()+updateInMeleeDueToItem aid store =   when (store `elem` [CEqp, COrgan]) $ do     b <- getsState $ getActorBody aid-    invalidateInMelee (blid b)+    updateInMeleeDueToActor b +updateInMeleeInDungeon :: MonadClient m => m ()+updateInMeleeInDungeon = do+  dungeon <- getsState sdungeon+  mapM_ insertInMeleeM $ EM.keys dungeon+ -- For now, only checking the stores.-wipeBfsIfItemAffectsSkills :: MonadClient m => [CStore] -> ActorId -> m ()-wipeBfsIfItemAffectsSkills stores aid =-  unless (null $ intersect stores [CEqp, COrgan]) $ invalidateBfsAid aid+wipeBfsIfItemAffectsSkills :: MonadClient m => CStore -> ActorId -> m ()+wipeBfsIfItemAffectsSkills store aid =+  when (store `elem` [CEqp, COrgan]) $+    invalidateBfsAid aid  tileChangeAffectsBfs :: COps                      -> ContentId TileKind -> ContentId TileKind@@ -316,7 +346,7 @@   modifyClient $ \cli -> cli {stargetD = EM.map affect3 (stargetD cli)}   mapM_ (addItemToDiscoBenefit . fst) ais   unless (bproj b) $ invalidateBfsPathLid b-  invalidateInMelee (blid b)+  updateInMeleeDueToActor b  destroyActor :: MonadClient m => ActorId -> Actor -> Bool -> m () destroyActor aid b destroy = do@@ -354,7 +384,7 @@         in TgtAndPath (affect aid3 tapTgt) newMPath   modifyClient $ \cli -> cli {stargetD = EM.mapWithKey affect3 (stargetD cli)}   unless (bproj b) $ invalidateBfsPathLid b-  invalidateInMelee (blid b)+  updateInMeleeDueToActor b  addItemToDiscoBenefit :: MonadClient m => ItemId -> m () addItemToDiscoBenefit iid = do
engine-src/Game/LambdaHack/Client/HandleResponseM.hs view
@@ -1,8 +1,7 @@ {-# LANGUAGE FlexibleContexts #-} -- | Semantics of responses sent by the server to clients. module Game.LambdaHack.Client.HandleResponseM-  ( MonadClientWriteRequest(..)-  , MonadClientAtomic(..)+  ( MonadClientAtomic(..), MonadClientWriteRequest(..)   , handleResponse   ) where @@ -17,15 +16,10 @@ import Game.LambdaHack.Client.Request import Game.LambdaHack.Client.Response import Game.LambdaHack.Client.UI+import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.State --- | Client monad in which one can send requests to the client.-class MonadClient m => MonadClientWriteRequest m where-  sendRequestAI :: RequestAI -> m ()-  sendRequestUI :: RequestUI -> m ()-  clientHasUI   :: m Bool- -- | Monad for executing atomic game state transformations on a client. class MonadClient m => MonadClientAtomic m where   -- | Execute an atomic update that changes the client's 'State'.@@ -34,6 +28,12 @@   -- an atomic update by the server on its copy of the client's 'State'.   execPutState :: State -> m () +-- | Client monad in which one can send requests to the client.+class MonadClient m => MonadClientWriteRequest m where+  sendRequestAI :: RequestAI -> m ()+  sendRequestUI :: RequestUI -> m ()+  clientHasUI   :: m Bool+ -- | Handle server responses. -- -- Note that for clients communicating with the server over the net,@@ -52,18 +52,35 @@     execPutState newState     cmdAtomicSemCli oldState cmdA     hasUI <- clientHasUI-    when hasUI $ displayRespUpdAtomicUI cmdA+    when hasUI $ watchRespUpdAtomicUI cmdA   RespUpdAtomicNoState cmdA -> do     oldState <- getState     execUpdAtomic cmdA     cmdAtomicSemCli oldState cmdA     hasUI <- clientHasUI-    when hasUI $ displayRespUpdAtomicUI cmdA+    when hasUI $ watchRespUpdAtomicUI cmdA   RespQueryAI aid -> do     cmdC <- queryAI aid     sendRequestAI cmdC   RespSfxAtomic sfx ->-    displayRespSfxAtomicUI sfx+    watchRespSfxAtomicUI sfx+  RespQueryUIunderAI -> do+    req <- queryUIunderAI+    sendRequestUI req   RespQueryUI -> do-    cmdH <- queryUI-    sendRequestUI cmdH+    -- Stop displaying the prompt, if any.+    modifySession $ \sess -> sess {sreqDelay = ReqDelayNot}+    sreqPending <- getsSession sreqPending+    req <- case sreqPending of+      Nothing -> do+        -- Server sending @RespQueryUI@ means that it's sent everything+        -- and is now ready to receive a request ASAP, so no point polling+        -- and instead query the player repeatedly until request generated.+        let loop = do+              mreq <- queryUI+              maybe loop pure mreq+        loop+      Just reqPending -> do+        modifySession $ \sess -> sess {sreqPending = Nothing}+        return reqPending+    sendRequestUI req
engine-src/Game/LambdaHack/Client/LoopM.hs view
@@ -6,7 +6,7 @@   , loopCli #ifdef EXPOSE_INTERNAL     -- * Internal operations-  , initAI, initUI+  , initAI, initUI, loopAI, longestDelay, loopUI #endif   ) where @@ -14,6 +14,10 @@  import Game.LambdaHack.Core.Prelude +import qualified Data.EnumMap.Strict as EM+import           Data.Time.Clock+import           Data.Time.Clock.POSIX+ import Game.LambdaHack.Atomic import Game.LambdaHack.Client.HandleAtomicM import Game.LambdaHack.Client.HandleResponseM@@ -21,7 +25,14 @@ import Game.LambdaHack.Client.Response import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI+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.Common.ClientOptions+import Game.LambdaHack.Common.Faction+import Game.LambdaHack.Common.MonadStateRead+import Game.LambdaHack.Common.State  -- | Client monad in which one can receive responses from the server. class MonadClient m => MonadClientReadResponse m where@@ -113,13 +124,107 @@   -- State and client state now valid.   debugPossiblyPrint $ cliendKindText <+> "client"                        <+> tshow side <+> "started 4/4."-  loop+  if hasUI+  then loopUI 0+  else loopAI   side2 <- getsClient sside   debugPossiblyPrint $ cliendKindText <+> "client" <+> tshow side2                        <+> "(initially" <+> tshow side <> ") stopped."- where-  loop = do-    cmd <- receiveResponse-    handleResponse cmd-    quit <- getsClient squit-    unless quit loop++loopAI :: ( MonadClientSetup m+          , MonadClientUI m+          , MonadClientAtomic m+          , MonadClientReadResponse m+          , MonadClientWriteRequest m )+       => m ()+loopAI = do+  cmd <- receiveResponse+  handleResponse cmd+  quit <- getsClient squit+  unless quit+    loopAI++-- | Alarm after this many seconds without server querying us for a command.+longestDelay :: POSIXTime+longestDelay = secondsToNominalDiffTime 1+                 -- really high to accomodate slow browsers++-- | The argument is the time of last UI query from the server.+-- After @longestDelay@ seconds past this date, the client considers itself+-- ignored and displays a warning and, at a keypress, gives+-- direct control to the player, no longer waiting for the server+-- to prompt it to do so.+loopUI :: ( MonadClientSetup m+          , MonadClientUI m+          , MonadClientAtomic m+          , MonadClientReadResponse m+          , MonadClientWriteRequest m )+       => POSIXTime -> m ()+loopUI timeSinceLastQuery = do+  sreqPending <- getsSession sreqPending+  sreqDelay <- getsSession sreqDelay+  sregainControl <- getsSession sregainControl+  keyPressed <- anyKeyPressed+  let alarm = timeSinceLastQuery > longestDelay+  if | not alarm  -- no alarm starting right now+       && -- no need to mark AI for control regain ASAP:+          (sreqDelay == ReqDelayNot  -- no old alarm still in effect+           || sregainControl  -- AI control already marked for regain+           || (not keyPressed  -- player does not insist by keypress+               && sreqDelay /= ReqDelayHandled)) -> do  -- or by hack+       timeBefore <- liftIO getPOSIXTime+       cmd <- receiveResponse+       timeAfter <- liftIO getPOSIXTime+       handleResponse cmd+       -- @squit@ can be changed only in @handleResponse@, so this is the only+       -- place where it needs to be checked.+       quit <- getsClient squit+       unless quit $ case cmd of+         RespQueryUI -> loopUI 0+         RespQueryUIunderAI ->+           loopUI $ succ longestDelay  -- permit fast AI control regain+         _ -> do+           when (isJust sreqPending) $ do+             msgAdd MsgActionAlert "Warning: server updated game state after current command was issued by the client but before it was received by the server."+           -- This measures only the server's delay.+           loopUI $ timeSinceLastQuery - timeBefore + timeAfter+     | not sregainControl && (keyPressed+                              || sreqDelay == ReqDelayHandled+                              || isJust sreqPending) -> do+         -- ignore alarm if to be handled by AI control regain code elsewhere+       -- Checking for special case for UI under AI control, because the default+       -- behaviour is in this case too alarming for the player, especially+       -- during the insert coin demo before game is started.+       side <- getsClient sside+       fact <- getsState $ (EM.! side) . sfactionD+       if isAIFact fact then+         -- Mark for immediate control regain from AI.+         modifySession $ \sess -> sess {sregainControl = True}+       else do  -- should work fine even if UI faction has no leader ATM+         -- Stop displaying the prompt, if any, but keep UI simple.+         modifySession $ \sess -> sess {sreqDelay = ReqDelayHandled}+         let msg = if isNothing sreqPending+                   then "Server delayed asking us for a command. Regardless, UI is made accessible. Press ESC twice to listen to server some more."+                   else "Server delayed receiving a command from us. The command is cancelled. Issue a new one."+         msgAdd MsgActionAlert msg+         mreqNew <- queryUI+         -- TODO: once this is really used, verify that if a request+         -- overwritten, nothing breaks due to some things in our ClientState+         -- and SessionUI (but fortunately not in State nor ServerState)+         -- already set as if it was performed.+         modifySession $ \sess -> sess {sreqPending = mreqNew}+         -- Now relax completely.+         modifySession $ \sess -> sess {sreqDelay = ReqDelayNot}+         -- We may yet not know if server is ready, but perhaps server+         -- tried hard to contact us while we took control and now it sleeps+         -- for a bit, so let's give it the benefit of the doubt+         -- and a slight pause before we alarm the player again.+       loopUI 0+     | otherwise -> do+       -- We know server is not ready.+       modifySession $ \sess -> sess {sreqDelay = ReqDelayAlarm}+       -- We take a slight pause during which we display encouragement+       -- to press a key and we receive game state changes.+       -- The pause is cut short by any keypress, so it does not+       -- make UI reaction any less snappy (animations do, but that's fine).+       loopUI 0
engine-src/Game/LambdaHack/Client/MonadClient.hs view
@@ -7,7 +7,8 @@   , MonadClient(modifyClient)     -- * Assorted primitives   , getClient, putClient-  , debugPossiblyPrint, createTabBFS, dumpTextFile, rndToAction, condInMeleeM+  , debugPossiblyPrint, createTabBFS, dumpTextFile, rndToAction+  , condInMeleeM, insertInMeleeM   ) where  import Prelude ()@@ -16,7 +17,7 @@  import           Control.Monad.ST.Strict (stToIO) import qualified Control.Monad.Trans.State.Strict as St-import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES import qualified Data.Primitive.PrimArray as PA import qualified Data.Text.IO as T import           System.FilePath@@ -61,9 +62,9 @@  createTabBFS :: MonadClient m => m (PA.PrimArray PointI) createTabBFS = do-  COps{corule=RuleContent{rXmax, rYmax}} <- getsState scops+  COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops   liftIO $ stToIO $ do-    tabAMutable <- PA.newPrimArray (rXmax * rYmax)  -- always enough+    tabAMutable <- PA.newPrimArray (rWidthMax * rHeightMax)  -- always enough     PA.unsafeFreezePrimArray tabAMutable  dumpTextFile :: MonadClientRead m => Text -> FilePath -> m FilePath@@ -82,15 +83,18 @@   modifyClient $ \cli -> cli {srandom = gen2}   return a -condInMeleeM :: MonadClient m => LevelId -> m Bool+condInMeleeM :: MonadClientRead m => LevelId -> m Bool condInMeleeM lid = do   condInMelee <- getsClient scondInMelee-  case EM.lookup lid condInMelee of-    Just inM -> return inM-    Nothing -> do-      side <- getsClient sside-      actorMaxSkills <- getsState sactorMaxSkills-      inM <- getsState $ inMelee actorMaxSkills side lid-      modifyClient $ \cli ->-        cli {scondInMelee = EM.insert lid inM condInMelee}-      return inM+  return $! lid `ES.member` condInMelee++insertInMeleeM :: MonadClient m => LevelId -> m ()+insertInMeleeM lid = do+  side <- getsClient sside+  actorMaxSkills <- getsState sactorMaxSkills+  inM <- getsState $ inMelee actorMaxSkills side lid+  modifyClient $ \cli ->+--    cli {scondInMelee = ES.alterF (const inM) lid $ scondInMelee cli}+    cli {scondInMelee = if inM+                        then ES.insert lid $ scondInMelee cli+                        else ES.delete lid $ scondInMelee cli}
engine-src/Game/LambdaHack/Client/Preferences.hs view
@@ -347,7 +347,7 @@ fakeItem kindId kind km =   let jkind    = IdentityObvious kindId       jfid     = Nothing  -- the default-      jflavour = Flavour (toEnum 0) (toEnum 0) -- dummy+      jflavour = dummyFlavour       itemBase = Item{..}       itemDisco = ItemDiscoMean km   in ItemFull itemBase kindId kind itemDisco True
engine-src/Game/LambdaHack/Client/Response.hs view
@@ -11,8 +11,8 @@ import Game.LambdaHack.Core.Prelude  import Game.LambdaHack.Atomic-import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.State+import Game.LambdaHack.Common.Types  -- | Abstract syntax of responses sent by server to an AI or UI client -- (or a universal client that can handle both roles, which is why@@ -28,6 +28,8 @@     -- ^ compute an AI move for the actor and send (the semantics of) it   | RespSfxAtomic SfxAtomic     -- ^ perform special effects (animations, messages, etc.)+  | RespQueryUIunderAI+    -- ^ check if the UI client wants to regain control   | RespQueryUI     -- ^ prompt the human player for a command and send (the semantics of) it   deriving Show
engine-src/Game/LambdaHack/Client/State.hs view
@@ -64,9 +64,8 @@   , scurChal      :: Challenge      -- ^ current game challenge setup   , snxtChal      :: Challenge      -- ^ next game challenge setup   , smarkSuspect  :: Int            -- ^ whether to mark suspect features-  , scondInMelee  :: EM.EnumMap LevelId Bool-      -- ^ whether we are in melee, per level; this is not a set, due to-      --   an unbenchmarked theory that a constant shape map is faster+  , scondInMelee  :: ES.EnumSet LevelId+                                    -- ^ whether we are in melee, per level   , svictories    :: EM.EnumMap (ContentId ModeKind) (M.Map Challenge Int)                                     -- ^ won games at particular difficulty lvls   , scampings     :: ES.EnumSet (ContentId ModeKind)  -- ^ camped games@@ -143,7 +142,7 @@     , scurChal = defaultChallenge     , snxtChal = defaultChallenge     , smarkSuspect = 1-    , scondInMelee = EM.empty+    , scondInMelee = ES.empty     , svictories = EM.empty     , scampings = ES.empty     , srestarts = ES.empty@@ -153,8 +152,7 @@  -- | Cycle the 'smarkSuspect' setting. cycleMarkSuspect :: StateClient -> StateClient-cycleMarkSuspect s@StateClient{smarkSuspect} =-  s {smarkSuspect = (smarkSuspect + 1) `mod` 3}+cycleMarkSuspect cli = cli {smarkSuspect = succ (smarkSuspect cli) `mod` 3}  -- | Update target parameters within client state. updateTarget :: ActorId -> (Maybe Target -> Maybe Target) -> StateClient
engine-src/Game/LambdaHack/Client/UI.hs view
@@ -3,19 +3,19 @@ -- of the game state. module Game.LambdaHack.Client.UI   ( -- * Querying the human player-    queryUI+    queryUI, queryUIunderAI     -- * UI monad and session type   , MonadClientUI(..), SessionUI(..)     -- * Updating UI state wrt game state changes-  , displayRespUpdAtomicUI, displayRespSfxAtomicUI+  , watchRespUpdAtomicUI, watchRespSfxAtomicUI     -- * Startup and initialization   , CCUI(..)-  , UIOptions, applyUIOptions, uCmdline, mkUIOptions+  , UIOptions, applyUIOptions, uOverrideCmdline, mkUIOptions     -- * Operations exposed for "Game.LambdaHack.Client.LoopM"   , ChanFrontend, chanFrontend, tryRestore, clientPrintUI #ifdef EXPOSE_INTERNAL     -- * Internal operations-  , humanCommand+  , stepQueryUIwithLeader, stepQueryUI #endif   ) where @@ -33,7 +33,6 @@ import           Game.LambdaHack.Client.State import           Game.LambdaHack.Client.UI.Content.Input import           Game.LambdaHack.Client.UI.ContentClientUI-import           Game.LambdaHack.Client.UI.DisplayAtomicM import           Game.LambdaHack.Client.UI.Frame import           Game.LambdaHack.Client.UI.FrameM import           Game.LambdaHack.Client.UI.Frontend@@ -43,142 +42,174 @@ import           Game.LambdaHack.Client.UI.MonadClientUI import           Game.LambdaHack.Client.UI.Msg import           Game.LambdaHack.Client.UI.MsgM+import           Game.LambdaHack.Client.UI.Overlay import           Game.LambdaHack.Client.UI.SessionUI import           Game.LambdaHack.Client.UI.Slideshow import           Game.LambdaHack.Client.UI.SlideshowM import           Game.LambdaHack.Client.UI.UIOptions import           Game.LambdaHack.Client.UI.UIOptionsParse+import           Game.LambdaHack.Client.UI.Watch import           Game.LambdaHack.Common.Actor import           Game.LambdaHack.Common.ActorState import           Game.LambdaHack.Common.ClientOptions import           Game.LambdaHack.Common.Faction import           Game.LambdaHack.Common.MonadStateRead import           Game.LambdaHack.Common.State-import           Game.LambdaHack.Common.Types import           Game.LambdaHack.Content.ModeKind  -- | Handle the move of a human player.-queryUI :: (MonadClient m, MonadClientUI m) => m RequestUI+queryUI :: (MonadClient m, MonadClientUI m) => m (Maybe RequestUI) queryUI = do-  side <- getsClient sside-  fact <- getsState $ (EM.! side) . sfactionD-  if isAIFact fact then do-    recordHistory-    keyPressed <- anyKeyPressed-    if keyPressed && fleaderMode (gplayer fact) /= LeaderNull then do-      -- Menu is entered in @displayRespUpdAtomicUI@ at @UpdAutoFaction@.-      discardPressedKey-      -- Regaining control of faction cancels some --stopAfter*.-      modifyClient $ \cli ->-        cli {soptions = (soptions cli) { sstopAfterSeconds = Nothing-                                       , sstopAfterFrames = Nothing }}-      return (ReqUIAutomate, Nothing)  -- stop AI-    else do-      -- As long as UI faction is under AI control, check, once per move,-      -- for benchmark game stop.-      stopAfterFrames <- getsClient $ sstopAfterFrames . soptions-      bench <- getsClient $ sbenchmark . soptions-      let exitCmd = if bench then ReqUIGameDropAndExit else ReqUIGameSaveAndExit-      case stopAfterFrames of-        Nothing -> do-          stopAfterSeconds <- getsClient $ sstopAfterSeconds . soptions-          case stopAfterSeconds of-            Nothing -> return (ReqUINop, Nothing)-            Just stopS -> do-              exit <- elapsedSessionTimeGT stopS-              if exit then do-                tellAllClipPS-                return (exitCmd, Nothing)  -- ask server to exit-              else return (ReqUINop, Nothing)-        Just stopF -> do-          allNframes <- getsSession sallNframes-          gnframes <- getsSession snframes-          if allNframes + gnframes >= stopF then do+  sreqQueried <- getsSession sreqQueried+  let !_A = assert (not sreqQueried) ()  -- querying not nested+  modifySession $ \sess -> sess {sreqQueried = True}+  let loop = do+        mres <- stepQueryUIwithLeader+        saimMode <- getsSession saimMode+        case mres of+          Nothing | isJust saimMode -> loop  -- loop until aiming finished+          _ -> return mres+  mres <- loop+  modifySession $ \sess -> sess {sreqQueried = False}+  return mres++queryUIunderAI :: (MonadClient m, MonadClientUI m) => m RequestUI+queryUIunderAI = do+ -- Record history so the player can browse it later on.+ recordHistory+ -- As long as UI faction is under AI control, check, once per move,+ -- for immediate control regain or benchmark game stop.+ sregainControl <- getsSession sregainControl+ if sregainControl then do+   modifySession $ \sess -> sess { sregainControl = False+                                 , sreqDelay = ReqDelayNot+                                 , sreqPending = Nothing }  -- just in case+   -- The keys mashed to gain control are not considered a command.+   resetPressedKeys+   -- Menu is entered in @displayRespUpdAtomicUI@ at @UpdAutoFaction@+   -- and @stopAfter@ is canceled in @cmdAtomicSemCli@+   -- when handling the results of the request below.+   return (ReqUIAutomate, Nothing)+ else do+  stopAfterFrames <- getsClient $ sstopAfterFrames . soptions+  bench <- getsClient $ sbenchmark . soptions+  let exitCmd = if bench then ReqUIGameDropAndExit else ReqUIGameSaveAndExit+  case stopAfterFrames of+    Nothing -> do+      stopAfterSeconds <- getsClient $ sstopAfterSeconds . soptions+      case stopAfterSeconds of+        Nothing -> return (ReqUINop, Nothing)+        Just stopS -> do+          sstartPOSIX <- getsSession sstart+          exit <- elapsedSessionTimeGT sstartPOSIX stopS+          if exit then do             tellAllClipPS             return (exitCmd, Nothing)  -- ask server to exit           else return (ReqUINop, Nothing)-  else do-    let mleader = gleader fact-        !_A = assert (isJust mleader) ()-    req <- humanCommand-    leader2 <- getLeaderUI-    -- Don't send the leader switch to the server with these commands,-    -- to avoid leader death at resume if his HP <= 0. That would violate-    -- the principle that save and reload doesn't change game state.-    let saveCmd cmd = case cmd of-          ReqUIGameDropAndExit -> True-          ReqUIGameSaveAndExit -> True-          ReqUIGameSave -> True-          _ -> False-    return (req, if mleader /= Just leader2 && not (saveCmd req)-                 then Just leader2-                 else Nothing)+    Just stopF -> do+      allNframes <- getsSession sallNframes+      gnframes <- getsSession snframes+      if allNframes + gnframes >= stopF then do+        tellAllClipPS+        return (exitCmd, Nothing)  -- ask server to exit+      else return (ReqUINop, Nothing) +stepQueryUIwithLeader :: (MonadClient m, MonadClientUI m)+                       => m (Maybe RequestUI)+stepQueryUIwithLeader = do+  side <- getsClient sside+  mleader <- getsState $ gleader . (EM.! side) . sfactionD+  mreq <- stepQueryUI+  case mreq of+    Nothing -> return Nothing+    Just req -> do+      mleader2 <- getsClient sleader+      -- Don't send the leader switch to the server with these commands,+      -- to avoid leader death at resume if his HP <= 0. That would violate+      -- the principle that save and reload doesn't change game state.+      let saveCmd cmd = case cmd of+            ReqUIGameDropAndExit -> True+            ReqUIGameSaveAndExit -> True+            ReqUIGameSave -> True+            _ -> False+      return $ Just (req, if mleader /= mleader2 && not (saveCmd req)+                          then mleader2+                          else Nothing)+ -- | Let the human player issue commands until any command takes time.-humanCommand :: forall m. (MonadClient m, MonadClientUI m) => m ReqUI-humanCommand = do+stepQueryUI :: (MonadClient m, MonadClientUI m) => m (Maybe ReqUI)+stepQueryUI = do   FontSetup{propFont} <- getFontSetup-  modifySession $ \sess -> sess {slastLost = ES.empty}-  let loop :: Maybe ActorId -> m ReqUI-      loop mOldLeader = do-        keyPressed <- anyKeyPressed-        macroFrame <- getsSession smacroFrame-        -- This message, in particular, disturbs.-        when (keyPressed && not (null (unKeyMacro (keyPending macroFrame)))) $-          msgAdd MsgActionWarning "*interrupted*"-        report <- getsSession $ newReport . shistory-        modifySession $ \sess -> sess {sreportNull = nullVisibleReport report}-        slides <- reportToSlideshowKeepHalt False []-        over <- case unsnoc slides of-          Nothing -> return []-          Just (allButLast, (ov, _)) ->-            if allButLast == emptySlideshow-            then do-              -- Display the only generated slide while waiting for next key.-              -- Strip the "--end-" prompt from it, by ignoring @MonoFont@.-              let ovProp = ov EM.! propFont-              return $! if EM.size ov > 1 then ovProp else init ovProp-            else do-              -- Show, one by one, all slides, awaiting confirmation for each.-              void $ getConfirms ColorFull [K.spaceKM, K.escKM] slides-              -- Indicate that report wiped out.-              modifySession $ \sess -> sess {sreportNull = True}-              -- Display base frame at the end.-              return []-        leader <- getLeaderUI-        b <- getsState $ getActorBody leader-        when (bhp b <= 0 && Just leader /= mOldLeader) $ displayMore ColorBW-          "If you move, the exertion will kill you. Consider asking for first aid instead."-        let ovs = EM.fromList [(propFont, over)]-        km <- promptGetKey ColorFull ovs False []-        abortOrCmd <- do-          -- Look up the key.-          CCUI{coinput=InputContent{bcmdMap}} <- getsSession sccui-          case km `M.lookup` bcmdMap of-            Just (_, _, cmd) -> do-              modifySession $ \sess -> sess {swaitTimes = if swaitTimes sess > 0-                                                          then - swaitTimes sess-                                                          else 0}-              restrictedCmdSemInCxtOfKM km cmd-            _ -> let msgKey = "unknown command '" <> K.showKM km <> "'"-                 in weaveJust <$> failWith (T.pack msgKey)-        -- GC macro stack if there are no actions left to handle,-        -- removing all unnecessary macro frames at once,-        -- but leaving the last one for user's in-game macros.-        modifySession $ \sess ->-          let (smacroFrameNew, smacroStackMew) =-                dropEmptyMacroFrames (smacroFrame sess) (smacroStack sess)-          in sess { smacroFrame = smacroFrameNew-                  , smacroStack = smacroStackMew }-        -- The command was failed or successful and if the latter,-        -- possibly took some time.-        case abortOrCmd of-          Right cmdS ->-            -- Exit the loop and let other actors act. No next key needed-            -- and no report could have been generated.-            return cmdS-          Left Nothing -> loop $ Just leader-          Left (Just err) -> do-            msgAdd MsgActionAlert $ showFailError err-            loop $ Just leader-  loop Nothing+  keyPressed <- anyKeyPressed+  macroFrame <- getsSession smacroFrame+  -- This message, in particular, disturbs.+  when (keyPressed && not (null (unKeyMacro (keyPending macroFrame)))) $+    msgAdd MsgActionWarning "*interrupted*"+  report <- getsSession $ newReport . shistory+  modifySession $ \sess -> sess {sreportNull = nullVisibleReport report}+  slides <- reportToSlideshowKeepHalt False []+  ovs <- case unsnoc slides of+    Nothing -> return EM.empty+    Just (allButLast, (ov, _)) ->+      if allButLast == emptySlideshow+      then do+        -- Display the only generated slide while waiting for next key.+        -- Strip the "--end-" prompt from it, by ignoring @MonoFont@.+        let ovProp = ov EM.! propFont+        return $!+          EM.singleton propFont $ if EM.size ov > 1 then ovProp else init ovProp+      else do+        -- Show, one by one, all slides, awaiting confirmation for each.+        void $ getConfirms ColorFull [K.spaceKM, K.escKM] slides+        -- Indicate that report wiped out.+        modifySession $ \sess -> sess {sreportNull = True}+        -- Display base frame at the end.+        return EM.empty+  mleader <- getsClient sleader+  case mleader of+    Nothing -> return ()+    Just leader -> do+      body <- getsState $ getActorBody leader+      lastLost <- getsSession slastLost+      if bhp body <= 0 then do+        side <- getsClient sside+        fact <- getsState $ (EM.! side) . sfactionD+        let gameOver = maybe False ((/= Camping) . stOutcome) (gquit fact)+        when (not gameOver && leader `ES.notMember` lastLost) $ do+          -- Hacky reuse of @slastLost@ for near-death spam prevention.+          modifySession $ \sess ->+            sess {slastLost = ES.insert leader lastLost}+          displayMore ColorBW "If you move, the exertion will kill you. Consider asking for first aid instead."+      else+        modifySession $ \sess -> sess {slastLost = ES.empty}+  km <- promptGetKey ColorFull ovs False []+  abortOrCmd <- do+    -- Look up the key.+    CCUI{coinput=InputContent{bcmdMap}} <- getsSession sccui+    case km `M.lookup` bcmdMap of+      Just (_, _, cmd) -> do+        modifySession $ \sess -> sess {swaitTimes = if swaitTimes sess > 0+                                                    then - swaitTimes sess+                                                    else 0}+        cmdSemInCxtOfKM km cmd+      _ -> let msgKey = "unknown command '" <> K.showKM km <> "'"+           in weaveJust <$> failWith (T.pack msgKey)+  -- GC macro stack if there are no actions left to handle,+  -- removing all unnecessary macro frames at once,+  -- but leaving the last one for user's in-game macros.+  modifySession $ \sess ->+    let (smacroFrameNew, smacroStackMew) =+          dropEmptyMacroFrames (smacroFrame sess) (smacroStack sess)+    in sess { smacroFrame = smacroFrameNew+            , smacroStack = smacroStackMew }+  -- The command was failed or successful and if the latter,+  -- possibly took some time.+  case abortOrCmd of+    Right cmdS ->+      -- Exit the loop and let other actors act. No next key needed+      -- and no report could have been generated.+      return $ Just cmdS+    Left Nothing -> return Nothing+    Left (Just err) -> do+      msgAdd MsgActionAlert $ showFailError err+      return Nothing
engine-src/Game/LambdaHack/Client/UI/Animation.hs view
@@ -29,7 +29,7 @@ -- | Animation is a list of frame modifications to play one by one, -- where each modification if a map from positions to level map symbols. newtype Animation = Animation [OverlaySpace]-  deriving (Eq, Show)+  deriving (Show, Eq)  -- | Render animations on top of a screen frame. --@@ -72,6 +72,10 @@                         -- not the action.                         [mzip (p1, mattr1)] +-- | Empty animation with a frame of delay, to be used to momentarily display+-- something for the player to see, e.g., the aiming line when swerving it.+-- Don't use this if there are multi-line messages on the screen,+-- because the text blinking is going to be distracting. pushAndDelay :: Animation pushAndDelay = Animation [[]] 
engine-src/Game/LambdaHack/Client/UI/Content/Input.hs view
@@ -61,6 +61,7 @@         ++ [ (K.mkKM "KP_Begin", waitTriple)            , (K.mkKM "C-KP_Begin", wait10Triple)            , (K.mkKM "KP_5", wait10Triple)+           , (K.mkKM "S-KP_5", wait10Triple)  -- rxvt            , (K.mkKM "C-KP_5", wait10Triple) ]         ++ [(K.mkKM "period", waitTriple) | uVi0]         ++ [(K.mkKM "C-period", wait10Triple) | uVi0]@@ -69,30 +70,24 @@       -- This is the most common case of duplicate keys and it usually       -- has an easy solution, so it's tested for first.       !_A = flip assert () $-        let isNotMainMenu (_, (cats, _, _)) = all (`notElem` [CmdMainMenu]) cats-            rawContentNoMainMenu = filter isNotMainMenu rawContent-            movementKeys = map fst movementDefinitions-            filteredNoMainMenu = filter (\(k, _) -> k `notElem` movementKeys)-                                        rawContentNoMainMenu-        in rawContentNoMainMenu == filteredNoMainMenu+        let movementKeys = map fst movementDefinitions+            filteredNoMovement = filter (\(k, _) -> k `notElem` movementKeys)+                                        rawContent+        in rawContent == filteredNoMovement            `blame` "commands overwrite the enabled movement keys (you can disable some in config file and try again)"-           `swith` rawContentNoMainMenu \\ filteredNoMainMenu+           `swith` rawContent \\ filteredNoMovement       bcmdList = rawContent ++ movementDefinitions       -- This catches repetitions (usually) not involving movement keys.       rejectRepetitions k t1 t2 =         error $ "duplicate key among command definitions (you can instead disable some movement key sets in config file and overwrite the freed keys)" `showFailure` (k, t1, t2)   in InputContent-  { bcmdMap = M.fromListWithKey rejectRepetitions-      [ (k, triple)-      | (k, triple@(cats, _, _)) <- bcmdList-      , all (`notElem` [CmdMainMenu]) cats-      ]+  { bcmdMap = M.fromListWithKey rejectRepetitions bcmdList   , bcmdList   , brevMap = M.fromListWith (flip (++)) $ concat       [ [(cmd, [k])]       | (k, (cats, _desc, cmd)) <- bcmdList       , not (null cats)-        && all (`notElem` [CmdMainMenu, CmdDebug]) cats+        && all (/= CmdDebug) cats       ]   } @@ -231,10 +226,16 @@                                       , aiming = flingICmd })                 (projectI ts) +-- | flingTs - list containing one flingable projectile+-- >>> flingTs+-- [TriggerItem {tiverb = Text "fling", tiobject = Text "in-range projectile", tisymbols = ""}]+--+-- I question the value of that test. But would Bob Martin like it+-- on the grounds it's like double-bookkeeping? flingTs :: [TriggerItem]-flingTs = [TriggerItem { tiverb = "start flinging"-                       , tiobject = "projectiles"-                       , tisymbols = "" }]+flingTs = [TriggerItem { tiverb = "fling"+                       , tiobject = "in-range projectile"+                       , tisymbols = [] }]  applyIK :: [TriggerItem] -> CmdTriple applyIK ts =@@ -273,11 +274,11 @@                      , "cycle"                        <+> (if d == Backward then "backwards" else "")                        <+> "among all party members"-                     , MemberCycle d )+                     , PointmanCycle d )  memberCycleLevel :: Direction -> [CmdCategory] -> CmdTriple memberCycleLevel d cats = ( cats                           , "cycle"                             <+> (if d == Backward then "backwards" else "")                             <+> " among party members on the level"-                          , MemberCycleLevel d )+                          , PointmanCycleLevel d )
engine-src/Game/LambdaHack/Client/UI/Content/Screen.hs view
@@ -15,18 +15,18 @@ import qualified Data.EnumMap.Strict as EM import qualified Data.Text as T +import Game.LambdaHack.Content.ItemKind (ItemKind) import Game.LambdaHack.Definition.Defs  -- | Screen layout and features definition. data ScreenContent = ScreenContent   { rwidth        :: X         -- ^ screen width   , rheight       :: Y         -- ^ screen height-  , rwrap         :: X         -- ^ wrap messages after this number of columns   , rwebAddress   :: String    -- ^ an extra blurb line for the main menu   , rintroScreen  :: ([String], [[String]])                                -- ^ the intro screen (first help screen) text                                --   and the rest of the manual-  , rapplyVerbMap :: EM.EnumMap Char T.Text+  , rapplyVerbMap :: EM.EnumMap (ContentSymbol ItemKind) T.Text                                -- ^ verbs to use for apply actions   , rFontFiles    :: [(FilePath, BS.ByteString)]                                -- ^ embedded game-supplied font files
engine-src/Game/LambdaHack/Client/UI/ContentClientUI.hs view
@@ -22,7 +22,6 @@   { coinput = InputContent M.empty [] M.empty   , coscreen = ScreenContent { rwidth = 0                              , rheight = 0-                             , rwrap = 0                              , rwebAddress = ""                              , rintroScreen = ([], [])                              , rapplyVerbMap = EM.empty
− engine-src/Game/LambdaHack/Client/UI/DisplayAtomicM.hs
@@ -1,2418 +0,0 @@-{-# LANGUAGE TupleSections #-}--- | Display atomic commands received by the client.-module Game.LambdaHack.Client.UI.DisplayAtomicM-  ( displayRespUpdAtomicUI, displayRespSfxAtomicUI-#ifdef EXPOSE_INTERNAL-    -- * Internal operations-  , ppHearMsg, ppHearDistanceAdjective, ppHearDistanceAdverb-  , updateItemSlot, markDisplayNeeded, lookAtMove-  , aidVerbMU, aidVerbDuplicateMU, itemVerbMUGeneral, itemVerbMU-  , itemVerbMUShort, itemAidVerbMU, mitemAidVerbMU, itemAidDistinctMU-  , manyItemsAidVerbMU-  , createActorUI, destroyActorUI, spotItemBag, moveActor, displaceActorUI-  , moveItemUI, quitFactionUI-  , displayGameOverLoot, displayGameOverAnalytics-  , discover, ppSfxMsg, strike-#endif-  ) where--import Prelude ()--import Game.LambdaHack.Core.Prelude--import           Control.Concurrent (threadDelay)-import qualified Data.Char as Char-import qualified Data.EnumMap.Strict as EM-import qualified Data.EnumSet as ES-import qualified Data.Map.Strict as M-import qualified Data.Text as T-import           Data.Tuple-import           GHC.Exts (inline)-import qualified NLP.Miniutter.English as MU--import           Game.LambdaHack.Atomic-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.DrawM-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           Game.LambdaHack.Client.UI.ItemSlot-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.UIOptions-import           Game.LambdaHack.Common.Actor-import           Game.LambdaHack.Common.ActorState-import           Game.LambdaHack.Common.Analytics-import           Game.LambdaHack.Common.ClientOptions-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 qualified Game.LambdaHack.Common.Tile as Tile-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           Game.LambdaHack.Content.ModeKind-import qualified Game.LambdaHack.Content.ModeKind as MK-import           Game.LambdaHack.Content.RuleKind-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-import           Game.LambdaHack.Definition.Flavour---- * RespUpdAtomicUI---- | Visualize atomic updates sent to the client. This is done--- in the global state after the command is executed and after--- the client state is modified by the command.--- Doesn't modify client state (except a few fields), but only client--- session (e.g., by displaying messages). This is enforced by types.-displayRespUpdAtomicUI :: (MonadClient m, MonadClientUI m) => UpdAtomic -> m ()-{-# INLINE displayRespUpdAtomicUI #-}-displayRespUpdAtomicUI cmd = case cmd of-  -- Create/destroy actors and items.-  UpdRegisterItems{} -> return ()-  UpdCreateActor aid body _ -> createActorUI True aid body-  UpdDestroyActor aid body _ -> destroyActorUI True aid body-  UpdCreateItem verbose iid _ kit@(kAdd, _) c -> do-    recordItemLid iid c-    updateItemSlot c iid-    if verbose then case c of-      CActor aid store -> do-        b <- getsState $ getActorBody aid-        case store of-          _ | bproj b ->-            itemVerbMU MsgItemCreation iid kit "appear" c-          COrgan -> do-            localTime <- getsState $ getLocalTime (blid b)-            arItem <- getsState $ aspectRecordFromIid iid-            if | IA.checkFlag Ability.Blast arItem -> return ()-               | IA.checkFlag Ability.Condition arItem -> do-                 side <- getsClient sside-                 discoBenefit <- getsClient sdiscoBenefit-                 bag <- getsState $ getContainerBag c-                 itemKind <- getsState $ getIidKind iid-                 let more = case EM.lookup iid bag of-                       Just (kTotal, _) | kTotal /= kAdd -> Just kTotal-                       _ -> Nothing-                     verbShow = MU.Text $-                       "become"-                       <+> case kit of-                         (1, _ : _) -> "somewhat"-                         (1, []) | isNothing more -> ""-                         _ | isNothing more -> "many-fold"-                         _ -> "additionally"-                     verbSave = MU.Text $-                       "become"-                       <+> case kit of-                         (1, t:_) ->  -- only exceptionally not singleton list-                                      -- or even more than one copy total-                           let total = deltaOfItemTimer localTime t-                           in timeDeltaInSecondsText total-                         (1, []) | isNothing more -> ""-                         (k, _) ->  -- usually the list empty; ignore anyway-                           (if isJust more then "additionally" else "")-                           <+> tshow k <> "-fold"-                           <+> case more of-                                 Nothing -> ""-                                 Just kTotal ->-                                   "(total:" <+> tshow kTotal <> "-fold)"-                     good = benInEqp (discoBenefit EM.! iid)-                     msgClass = case lookup IK.S_ASLEEP $ IK.ifreq itemKind of-                       Just n | n > 0 -> MsgStatusSleep-                       _ -> if | bfid b /= side -> MsgStatusOthers-                               | good -> MsgStatusGoodUs-                               | otherwise -> MsgStatusBadUs-                 -- This describes all such items already among organs,-                 -- which is useful, because it shows "charging".-                 itemAidDistinctMU msgClass aid verbShow verbSave iid-                 when (bfid b == side && not good) $-                   -- Others get conditions too often and good ones are not-                   -- dire enough and also too common.-                   msgAdd MsgTutorialHint "Temporary conditions, especially the bad ones, pass quickly, usually after just a few turns. While active, they are listed in the '@' organ menu and the effects of most of them are seen in the '#' skill menu."-               | otherwise -> do-                 wown <- ppContainerWownW partActorLeader True c-                 itemVerbMU MsgItemCreation iid kit-                            (MU.Text $ makePhrase $ "grow" : wown) c-          _ -> do-            wown <- ppContainerWownW partActorLeader True c-            itemVerbMU MsgItemCreation iid kit-                       (MU.Text $ makePhrase $ "appear" : wown) c-      CEmbed lid _ -> markDisplayNeeded lid-      CFloor lid _ -> do-        factionD <- getsState sfactionD-        itemVerbMU MsgItemCreation iid kit-                   (MU.Text $ "appear" <+> ppContainer factionD c) c-        markDisplayNeeded lid-      CTrunk{} -> return ()-    else do-      lid <- getsState $ lidFromC c-      markDisplayNeeded lid-  UpdDestroyItem verbose iid _ kit c ->-    if verbose then case c of-      CActor aid _  -> do-        b <- getsState $ getActorBody aid-        if bproj b then-          itemVerbMUShort MsgItemRuination iid kit "break" c-        else do-          ownW <- ppContainerWownW partActorLeader False c-          let verb = MU.Text $ makePhrase $ "vanish from" : ownW-          itemVerbMUShort MsgItemRuination iid kit verb c-      CEmbed lid _ -> markDisplayNeeded lid-      CFloor lid _ -> do-        factionD <- getsState sfactionD-        itemVerbMUShort MsgItemRuination iid kit-                        (MU.Text $ "break" <+> ppContainer factionD c) c-        markDisplayNeeded lid-      CTrunk{} -> return ()-    else do-      lid <- getsState $ lidFromC c-      markDisplayNeeded lid-  UpdSpotActor aid body -> createActorUI False aid body-  UpdLoseActor aid body -> destroyActorUI False aid body-  UpdSpotItem verbose iid kit c -> spotItemBag verbose c $ EM.singleton iid kit-  UpdLoseItem True iid kit c@(CActor aid _) -> do-    b <- getsState $ getActorBody aid-    when (not (bproj b) && bhp b > 0) $ do  -- don't spam-      ownW <- ppContainerWownW partActorLeader False c-      let verb = MU.Text $ makePhrase $ "be removed from" : ownW-      itemVerbMUShort MsgItemMovement iid kit verb c-  UpdLoseItem{} -> return ()-  UpdSpotItemBag verbose c bag -> spotItemBag verbose c bag-  UpdLoseItemBag{} -> return ()  -- rarely interesting and can be very long-  -- Move actors and items.-  UpdMoveActor aid source target -> moveActor aid source target-  UpdWaitActor aid WSleep _ -> do-    aidVerbMU MsgStatusWakeup aid "wake up"-    side <- getsClient sside-    b <- getsState $ getActorBody aid-    unless (bfid b == side) $-      msgAdd MsgTutorialHint "Woken up actors regain stats and skills, including sight radius and melee armor, over several turns. To avoid waking them up, make sure they don't lose HP nor too much Calm through noises, particularly close ones. Beware, however, that they slowly regenerate HP as they sleep and eventually wake up at full HP."-  UpdWaitActor{} -> return ()  -- falling asleep handled uniformly elsewhere-  UpdDisplaceActor source target -> displaceActorUI source target-  UpdMoveItem iid k aid c1 c2 -> moveItemUI iid k aid c1 c2-  -- Change actor attributes.-  UpdRefillHP _ 0 -> return ()-  UpdRefillHP aid hpDelta -> do-    let coarseDelta = abs hpDelta `div` oneM-        tDelta = if coarseDelta == 0-                 then if hpDelta > 0 then "a little" else "a fraction of an HP"-                 else tshow coarseDelta <+> "HP"-    b <- getsState $ getActorBody aid-    unless (bproj b) $-      aidVerbMU MsgNumericReport aid $ MU.Text-                ((if hpDelta > 0 then "heal" else "lose") <+> tDelta)-    arena <- getArenaUI-    side <- getsClient sside-    if | bproj b && (EM.null (beqp b) || isNothing (btrajectory b)) ->-           return ()  -- ignore caught proj or one hitting a wall-       | bhp b <= 0 && hpDelta < 0-         && (bfid b == side && not (bproj b) || arena == blid b) -> do-         let (firstFall, hurtExtra) = case (bfid b == side, bproj b) of-               (True, True) -> ("drop down", "tumble down")-               (True, False) -> ("fall down", "suffer woeful mutilation")-               (False, True) -> ("plummet", "crash")-               (False, False) -> ("collapse", "be reduced to a bloody pulp")-             verbDie = if alreadyDeadBefore then hurtExtra else firstFall-             -- Rarely, this is wrong, because 2 other actors hit the victim-             -- at exactly the same time. No big problem. Doubled "dies"-             -- messages appears instead of "dies; is mutilated".-             alreadyDeadBefore = bhp b - hpDelta <= 0-         tfact <- getsState $ (EM.! bfid b) . sfactionD-         bUI <- getsSession $ getActorUI aid-         subjectRaw <- partActorLeader aid-         let subject = if alreadyDeadBefore || subjectRaw == "you"-                       then subjectRaw-                       else partActor bUI  -- avoid "fallen"-             msgDie = makeSentence [MU.SubjectVerbSg subject verbDie]-             targetIsFoe = isFoe (bfid b) tfact side-             targetIsFriend = isFriend (bfid b) tfact side-             msgClass | bproj b = MsgDeathBoring-                      | targetIsFoe = MsgDeathVictory-                      | targetIsFriend = MsgDeathDeafeat-                      | otherwise = MsgDeathBoring-         if | bproj b -> msgAdd msgClass msgDie-            | bfid b == side -> do-              msgLnAdd msgClass $ msgDie <+> "Alas!"-              displayMore ColorBW ""-            | otherwise -> msgLnAdd msgClass msgDie-         -- We show death anims only if not dead already before this refill.-         let deathAct = if bfid b == side-                        then deathBody (bpos b)-                        else shortDeathBody (bpos b)-         unless (bproj b || alreadyDeadBefore) $ animate (blid b) deathAct-       | otherwise -> do-         when (hpDelta >= bhp b && bhp b > 0) $-           aidVerbMU MsgActionWarning aid "return from the brink of death"-         mleader <- getsClient sleader-         when (Just aid == mleader) $ do-           actorMaxSk <- getsState $ getActorMaxSkills aid-           -- Regenerating actors never stop gaining HP, so we need to stop-           -- reporting it after they reach full HP for the first time.-           -- Also, no spam for non-leaders.-           when (bhp b >= xM (Ability.getSk Ability.SkMaxHP actorMaxSk)-                 && bhp b - hpDelta < xM (Ability.getSk Ability.SkMaxHP-                                                  actorMaxSk)) $-             msgAdd MsgSpecialEvent "You recover your health fully. Any further gains will be transient."-         when (bfid b == side && not (bproj b)) $ do-           when (abs hpDelta >= oneM) $ markDisplayNeeded (blid b)-           when (hpDelta < 0) $ do-             when (hpDelta <= xM (-3)) $ msgAdd MsgTutorialHint "You took a lot of damage from one source. If the danger persists, consider retreating towards your teammates or buffing up or an instant escape, if consumables permit."-             sUIOptions <- getsSession sUIOptions-             currentWarning <--               getsState $ checkWarningHP sUIOptions aid (bhp b)-             when currentWarning $ do-               previousWarning <--                 getsState $ checkWarningHP sUIOptions aid (bhp b - hpDelta)-               unless previousWarning $-                 aidVerbMU MsgRiskOfDeath aid-                           "be down to a dangerous health level"-  UpdRefillCalm _ 0 -> return ()-  UpdRefillCalm aid calmDelta -> do-    side <- getsClient sside-    b <- getsState $ getActorBody aid-    when (bfid b == side && not (bproj b)) $ do-      if | calmDelta > 0 -> do  -- regeneration or effect-           mleader <- getsClient sleader-           when (Just aid == mleader) $ do-             actorMaxSk <- getsState $ getActorMaxSkills aid-             let bPrev = b {bcalm = bcalm b - calmDelta}-             when (calmEnough b actorMaxSk-                   && not (calmEnough bPrev actorMaxSk)) $-               msgAdd MsgNeutralEvent "You are again calm enough to manage your equipment outfit."-           -- If the leader regenerates Calm more often than once per-           -- standard game turn, this will not be reflected, for smoother-           -- and faster display. However, every halt for keypress-           -- shows Calm, so this only matters for macros, where speed is good.-           when (abs calmDelta > oneM) $ markDisplayNeeded (blid b)-         | calmDelta == minusM1 -> do-           fact <- getsState $ (EM.! side) . sfactionD-           s <- getState-           let closeFoe (!p, aid2) =  -- mimics isHeardFoe-                 let b2 = getActorBody aid2 s-                 in inline chessDist p (bpos b) <= 3-                    && not (actorWaitsOrSleeps b2)  -- uncommon-                    && inline isFoe side fact (bfid b2)  -- costly-               anyCloseFoes = any closeFoe $ EM.assocs $ lbig-                                           $ sdungeon s EM.! blid b-           unless anyCloseFoes $ do  -- obvious where the feeling comes from-             duplicated <- aidVerbDuplicateMU MsgHeardNearby aid-                                              "hear something"-             unless duplicated stopPlayBack-         | otherwise ->  -- low deltas from hits; displayed elsewhere-           return ()-      when (calmDelta < 0) $ do-        sUIOptions <- getsSession sUIOptions-        currentWarning <--          getsState $ checkWarningCalm sUIOptions aid (bcalm b)-        when currentWarning $ do-          previousWarning <--            getsState $ checkWarningCalm sUIOptions aid (bcalm b - calmDelta)-          unless previousWarning $-            -- This messages is not shown if impression happens after-            -- Calm is low enough. However, this is rare and HUD shows the red.-            aidVerbMU MsgRiskOfDeath aid-                      "have grown agitated and impressed enough to be in danger of defecting"-  UpdTrajectory _ _ mt ->  -- if projectile dies just after, force one frame-    when (isNothing mt) $ pushFrame False-  -- Change faction attributes.-  UpdQuitFaction fid _ toSt manalytics -> quitFactionUI fid toSt manalytics-  UpdSpotStashFaction verbose fid lid pos -> do-    when verbose $ do-      side <- getsClient sside-      if fid == side then-        msgLnAdd MsgFactionIntel-                 "You set up the shared inventory stash of your team."-      else do-        fact <- getsState $ (EM.! fid) . sfactionD-        let fidName = MU.Text $ gname fact-        msgAdd MsgFactionIntel $-          makeSentence [ "you have found the current"-                       , MU.WownW fidName "hoard location" ]-    animate lid $ actorX pos-  UpdLoseStashFaction verbose fid lid pos -> do-    when verbose $ do-      side <- getsClient sside-      if fid == side then-        msgAdd MsgFactionIntel-               "You've lost access to your shared inventory stash!"-      else do-        fact <- getsState $ (EM.! fid) . sfactionD-        let fidName = MU.Text $ gname fact-        msgAdd MsgFactionIntel $-          makeSentence [fidName, "no longer control their hoard"]-    animate lid $ vanish pos-  UpdLeadFaction fid (Just source) mtgt@(Just target) -> do-    mleader <- getsClient sleader-    when (mtgt /= mleader) $ do-      fact <- getsState $ (EM.! fid) . sfactionD-      lidV <- viewedLevelUI-      when (isAIFact fact) $ markDisplayNeeded lidV-      -- This faction can't run with multiple actors, so this is not-      -- a leader change while running, but rather server changing-      -- their leader, which the player should be alerted to.-      when (noRunWithMulti fact) stopPlayBack-      actorD <- getsState sactorD-      case EM.lookup source actorD of-        Just sb | bhp sb <= 0 -> assert (not $ bproj sb) $ do-          -- Regardless who the leader is, give proper names here, not 'you'.-          sbUI <- getsSession $ getActorUI source-          tbUI <- getsSession $ getActorUI target-          let subject = partActor tbUI-              object  = partActor sbUI-          msgAdd MsgPointmanSwap $-            makeSentence [ MU.SubjectVerbSg subject "take command"-                         , "from", object ]-        _ -> return ()-      lookAtMove target-  UpdLeadFaction _ Nothing mtgt@(Just target) -> do-    mleader <- getsClient sleader-    when (mtgt /= mleader) $-      lookAtMove target-  UpdLeadFaction{} -> return ()-  UpdDiplFaction fid1 fid2 _ toDipl -> do-    name1 <- getsState $ gname . (EM.! fid1) . sfactionD-    name2 <- getsState $ gname . (EM.! fid2) . sfactionD-    let showDipl Unknown = "unknown to each other"-        showDipl Neutral = "in neutral diplomatic relations"-        showDipl Alliance = "allied"-        showDipl War = "at war"-    msgAdd MsgFactionIntel $-      name1 <+> "and" <+> name2 <+> "are now" <+> showDipl toDipl <> "."-  UpdDoctrineFaction{} -> return ()-  UpdAutoFaction fid b -> do-    side <- getsClient sside-    lidV <- viewedLevelUI-    markDisplayNeeded lidV-    when (fid == side) $ do-      unless b addPressedControlEsc  -- sets @swasAutomated@, enters main menu-      setFrontAutoYes b  -- now can stop auto-accepting prompts-  UpdRecordKill{} -> return ()-  -- Alter map.-  UpdAlterTile lid p fromTile toTile -> do-    COps{cotile} <- getsState scops-    markDisplayNeeded lid-    let feats = TK.tfeature $ okind cotile fromTile-        toAlter feat =-          case feat of-            TK.OpenTo tgroup -> Just tgroup-            TK.CloseTo tgroup -> Just tgroup-            TK.ChangeTo tgroup -> Just tgroup-            TK.OpenWith _ _ tgroup -> Just tgroup-            TK.CloseWith _ _ tgroup -> Just tgroup-            TK.ChangeWith _ _ tgroup -> Just tgroup-            _ -> Nothing-        groupsToAlterTo = mapMaybe toAlter feats-        freq = map fst $ filter (\(_, q) -> q > 0)-               $ TK.tfreq $ okind cotile toTile-        unexpected = null $ intersect freq groupsToAlterTo-    mactorAtPos <- getsState $ posToBig p lid-    mleader <- getsClient sleader-    when (unexpected || isJust mactorAtPos && mactorAtPos /= mleader) $ do-      -- Player notices @fromTile can't be altered into @toTIle@,-      -- which is uncanny, so we produce a message.-      -- This happens when the player missed an earlier search of the tile-      -- performed by another faction.-      let subject = ""  -- a hack, because we don't handle adverbs well-          verb = "turn into"-          msg = makeSentence $-            [ "the", MU.Text $ TK.tname $ okind cotile fromTile-            , "at position", MU.Text $ tshow p ]-            ++ ["suddenly" | unexpected]  -- adverb-            ++ [ MU.SubjectVerbSg subject verb-               , MU.AW $ MU.Text $ TK.tname $ okind cotile toTile ]-      msgAdd (if unexpected then MsgSpecialEvent else MsgNeutralEvent) msg-  UpdAlterExplorable lid _ -> markDisplayNeeded lid-  UpdAlterGold{} -> return ()  -- not displayed on HUD-  UpdSearchTile aid _p toTile -> do-    COps{cotile} <- getsState scops-    subject <- partActorLeader aid-    let fromTile = fromMaybe (error $ show toTile) $ Tile.hideAs cotile toTile-        subject2 = MU.Text $ TK.tname $ okind cotile fromTile-        object = MU.Text $ TK.tname $ okind cotile toTile-    let msg = makeSentence [ MU.SubjectVerbSg subject "reveal"-                           , "that the"-                           , MU.SubjectVerbSg subject2 "be"-                           , MU.AW object ]-    unless (subject2 == object) $ do-      msgAdd MsgTerrainReveal msg-      msgAdd MsgTutorialHint "Solid terrain drawn in pink is not fully known until searched. This is usually done by bumping into it, which also triggers effects and transformations the terrain is capable of. Once revealed, the terrain can be inspected in aiming mode started with the '*' key or with mouse."-  UpdHideTile{} -> return ()-  UpdSpotTile{} -> return ()-  UpdLoseTile{} -> return ()-  UpdSpotEntry{} -> return ()-  UpdLoseEntry{} -> return ()-  UpdAlterSmell{} -> return ()-  UpdSpotSmell{} -> return ()-  UpdLoseSmell{} -> return ()-  -- Assorted.-  UpdTimeItem{} -> return ()-  UpdAgeGame{} -> do-    sdisplayNeeded <- getsSession sdisplayNeeded-    sturnDisplayed <- getsSession sturnDisplayed-    time <- getsState stime-    let clipN = time `timeFit` timeClip-        clipMod = clipN `mod` clipsInTurn-        turnPing = clipMod == 0  -- e.g., to see resting counter-    if | sdisplayNeeded -> pushFrame True-           -- adds delay, because it's not an extra animation-like frame,-           -- but showing some real information accumulated up to this point-       | turnPing && not sturnDisplayed -> pushFrame False-       | otherwise -> return ()-    when turnPing $-      modifySession $ \sess -> sess {sturnDisplayed = False}-  UpdUnAgeGame{} -> return ()-  UpdDiscover c iid _ _ -> discover c iid-  UpdCover{} -> return ()  -- don't spam when doing undo-  UpdDiscoverKind{} -> return ()  -- don't spam when server tweaks stuff-  UpdCoverKind{} -> return ()  -- don't spam when doing undo-  UpdDiscoverAspect{} -> return ()  -- don't spam when server tweaks stuff-  UpdCoverAspect{} -> return ()  -- don't spam when doing undo-  UpdDiscoverServer{} -> error "server command leaked to client"-  UpdCoverServer{} -> error "server command leaked to client"-  UpdPerception{} -> return ()-  UpdRestart fid _ _ _ _ srandom -> do-    cops@COps{cocave, comode, corule} <- getsState scops-    oldSess <- getSession-    svictories <- getsClient svictories-    snxtChal <- getsClient snxtChal-    let uiOptions@UIOptions{uHistory1PerLine} = sUIOptions oldSess-        f !acc _p !i _a = i : acc-        modes = zip [0..] $ ofoldlGroup' comode CAMPAIGN_SCENARIO f []-        g :: (Int, ContentId ModeKind) -> Int-        g (_, mode) = case EM.lookup mode svictories of-          Nothing -> 0-          Just cm -> fromMaybe 0 (M.lookup snxtChal cm)-        (snxtScenario, _) = minimumBy (comparing g) modes-        nxtGameTutorial = MK.mtutorial $ snd $ nxtGameMode cops snxtScenario-    putSession $-      (emptySessionUI uiOptions)-        { schanF = schanF oldSess-        , sccui = sccui oldSess-        , shistory = shistory oldSess-        , snxtScenario-        , scurTutorial = snxtTutorial oldSess  -- quite random for screensavers-        , snxtTutorial = nxtGameTutorial-        , soverrideTut = soverrideTut oldSess-        , sstart = sstart oldSess-        , sgstart = sgstart oldSess-        , sallTime = sallTime oldSess-        , snframes = snframes oldSess-        , sallNframes = sallNframes oldSess-        , srandomUI = srandom-        }-    when (sstart oldSess == 0) resetSessionStart-    when (lengthHistory uHistory1PerLine (shistory oldSess) == 0) $ do-      let title = T.pack $ rtitle corule-      msgAdd MsgBookKeeping $ "Welcome to" <+> title <> "!"-      -- Generate initial history. Only for UI clients.-      shistory <- defaultHistory-      modifySession $ \sess -> sess {shistory}-    recordHistory  -- to ensure EOL even at creation of history-    lid <- getArenaUI-    lvl <- getLevel lid-    gameMode <- getGameMode-    curChal <- getsClient scurChal-    fact <- getsState $ (EM.! fid) . sfactionD-    let loneMode = case ginitial fact of-          [] -> True-          [(_, 1, _)] -> True-          _ -> False-    msgAdd MsgBookKeeping "-------------------------------------------------"-    recordHistory-    msgAdd MsgActionWarning-           ("New game started in" <+> mname gameMode <+> "mode.")-    msgAdd MsgPlotExposition $ mdesc gameMode-    let desc = cdesc $ okind cocave $ lkind lvl-    unless (T.null desc) $ do-      msgLnAdd MsgBackdropFocus "You take in your surroundings."-      msgAdd MsgBackdropInfo desc-    -- We can fool the player only once (per scenario), but let's not do it-    -- in the same way each time. TODO: PCG-    blurb <- rndToActionUI $ oneOf-      [ "You think you saw movement."-      , "Something catches your peripherial vision."-      , "You think you felt a tremor under your feet."-      , "A whiff of chilly air passes around you."-      , "You notice a draft just when it dies down."-      , "The ground nearby is stained along some faint lines."-      , "Scarce black motes slowly settle on the ground."-      , "The ground in the immediate area is empty, as if just swiped."-      ]-    msgLnAdd MsgBadMiscEvent blurb  -- being here is a bad turn of events-    when (cwolf curChal && not loneMode) $-      msgAdd MsgActionWarning "Being a lone wolf, you begin without companions."-    when (lengthHistory uHistory1PerLine (shistory oldSess) > 1) $-      fadeOutOrIn False-    setFrontAutoYes $ isAIFact fact-    -- Forget the furious keypresses when dying in the previous game.-    resetPressedKeys-    -- Help newbies when actors obscured by text and no obvious key to press:-    displayMore ColorFull "\nAre you up for the challenge?"-    msgAdd MsgPromptGeneric-           "A grand story starts right here! (Press '?' for context and help.)"-  UpdRestartServer{} -> return ()-  UpdResume fid _ -> do-    COps{cocave} <- getsState scops-    resetSessionStart-    fact <- getsState $ (EM.! fid) . sfactionD-    setFrontAutoYes $ isAIFact fact-    unless (isAIFact fact) $ do-      lid <- getArenaUI-      lvl <- getLevel lid-      gameMode <- getGameMode-      msgAdd MsgActionAlert $ "Continuing" <+> mname gameMode <> "."-      msgAdd MsgPromptGeneric $ mdesc gameMode-      let desc = cdesc $ okind cocave $ lkind lvl-      unless (T.null desc) $ do-        msgLnAdd MsgPromptFocus "You remember your surroundings."-        msgAdd MsgPromptGeneric desc-      displayMore ColorFull "\nAre you up for the challenge?"-      msgAdd MsgPromptGeneric-             "Prove yourself! (Press '?' for context and help.)"-  UpdResumeServer{} -> return ()-  UpdKillExit{} -> do-#ifdef USE_JSFILE-      -- Some browsers seem to trash Local Storage when page reloaded or closed-      -- or the browser closed, while they still internally finish the saving-      -- in the background, so wait 2s. If the exit is without a save,-      -- the wait is spurious, but it's not supposed to be common.-      -- TODO: replace the @liftIO@ with a @MonadClientUI@ delay function.-    liftIO $ threadDelay 2000000-#else-    liftIO $ threadDelay 200000-#endif-    -- The prompt is necessary to force frontend to show this before exiting.-    void $ displayMore ColorBW "Done."  -- in case it follows "Saving..."-    side <- getsClient sside-    debugPossiblyPrintUI $ "Client" <+> tshow side <+> "closing frontend."-    frontendShutdown-    debugPossiblyPrintUI $ "Client" <+> tshow side <+> "closed frontend."-  UpdWriteSave -> msgAdd MsgInnerWorkSpam "Saving backup."-  UpdHearFid _ distance hearMsg -> do-    mleader <- getsClient sleader-    case mleader of-      Just{} -> return ()  -- will display stuff when leader moves-      Nothing -> do-        lidV <- viewedLevelUI-        markDisplayNeeded lidV-        recordHistory-    msg <- ppHearMsg distance hearMsg-    let msgClass = case distance of-          Nothing -> MsgHeardOutside-          Just 0 -> MsgHeardNearby-          Just _ -> MsgHeardFaraway-    msgAdd msgClass msg-    case hearMsg of-      HearUpd UpdDestroyActor{} ->-        msgAdd MsgTutorialHint "Events out of your sight radius (as listed in the '#' skill menu) can sometimes be heard, depending on your hearing radius. Some, such as death shrieks, can always be heard regardless of skill and distance, including when they come from a different floor."-      HearTaunt{} ->-        msgAdd MsgTutorialHint "Enemies you can't see are sometimes heard yelling and emitting other noises. Whether you can hear them, depends on their distance and your hearing radius, as listed in the '#' skill menu."-      _ -> return ()--updateItemSlot :: MonadClientUI m => Container -> ItemId -> m ()-updateItemSlot c iid = do-  arItem <- getsState $ aspectRecordFromIid iid-  ItemSlots itemSlots <- getsSession sslots-  let slore = IA.loreFromContainer arItem c-      lSlots = itemSlots EM.! slore-  case lookup iid $ map swap $ EM.assocs lSlots of-    Nothing -> do-      let l = assignSlot lSlots-          f = EM.insert l iid-          newSlots = ItemSlots $ EM.adjust f slore itemSlots-      modifySession $ \sess -> sess {sslots = newSlots}-    Just _l -> return ()  -- slot already assigned--markDisplayNeeded :: MonadClientUI m => LevelId -> m ()-markDisplayNeeded lid = do-  lidV <- viewedLevelUI-  when (lidV == lid) $ modifySession $ \sess -> sess {sdisplayNeeded = True}--lookAtMove :: (MonadClient m, MonadClientUI m) => ActorId -> m ()-lookAtMove aid = do-  mleader <- getsClient sleader-  body <- getsState $ getActorBody aid-  side <- getsClient sside-  aimMode <- getsSession saimMode-  when (not (bproj body)-        && bfid body == side-        && isNothing aimMode) $ do  -- aiming does a more extensive look-    stashBlurb <- lookAtStash (blid body) (bpos body)-    (itemsBlurb, _) <- lookAtItems True (bpos body) aid Nothing-    let msgClass = if Just aid == mleader-                   then MsgAtFeetMajor-                   else MsgAtFeetMinor-        blurb = stashBlurb <+> itemsBlurb-    unless (T.null blurb) $-      msgAdd msgClass blurb-  fact <- getsState $ (EM.! bfid body) . sfactionD-  adjBigAssocs <- getsState $ adjacentBigAssocs body-  adjProjAssocs <- getsState $ adjacentProjAssocs body-  if not (bproj body) && bfid body == side then do-    let foe (_, b2) = isFoe (bfid body) fact (bfid b2)-        adjFoes = filter foe $ adjBigAssocs ++ adjProjAssocs-    unless (null adjFoes) stopPlayBack-  else when (isFoe (bfid body) fact side) $ do-    let our (_, b2) = bfid b2 == side-        adjOur = filter our adjBigAssocs-    unless (null adjOur) stopPlayBack--aidVerbMU :: (MonadClient m, MonadClientUI m, MsgShared a)-          => a -> ActorId -> MU.Part -> m ()-aidVerbMU msgClass aid verb = do-  subject <- partActorLeader aid-  msgAdd msgClass $ makeSentence [MU.SubjectVerbSg subject verb]--aidVerbDuplicateMU :: (MonadClient m, MonadClientUI m, MsgShared a)-                   => a -> ActorId -> MU.Part -> m Bool-aidVerbDuplicateMU msgClass aid verb = do-  subject <- partActorLeader aid-  msgAddDuplicate msgClass (makeSentence [MU.SubjectVerbSg subject verb])--itemVerbMUGeneral :: MonadClientUI m-                  => Bool -> ItemId -> ItemQuant -> MU.Part -> Container-                  -> m Text-itemVerbMUGeneral verbose iid kit@(k, _) verb c = assert (k > 0) $ do-  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui-  lid <- getsState $ lidFromC c-  localTime <- getsState $ getLocalTime lid-  itemFull <- getsState $ itemToFull iid-  side <- getsClient sside-  factionD <- getsState sfactionD-  let arItem = aspectRecordFull itemFull-      partItemWsChosen | verbose = partItemWs-                       | otherwise = partItemWsShort-      subject = partItemWsChosen rwidth side factionD k localTime itemFull kit-      msg | k > 1 && not (IA.checkFlag Ability.Condition arItem) =-              makeSentence [MU.SubjectVerb MU.PlEtc MU.Yes subject verb]-          | otherwise = makeSentence [MU.SubjectVerbSg subject verb]-  return $! msg--itemVerbMU :: (MonadClient m, MonadClientUI m, MsgShared a)-           => a -> ItemId -> ItemQuant -> MU.Part -> Container -> m ()-itemVerbMU msgClass iid kit verb c = do-  msg <- itemVerbMUGeneral True iid kit verb c-  msgAdd msgClass msg--itemVerbMUShort :: (MonadClient m, MonadClientUI m, MsgShared a)-                => a -> ItemId -> ItemQuant -> MU.Part -> Container-                -> m ()-itemVerbMUShort msgClass iid kit verb c = do-  msg <- itemVerbMUGeneral False iid kit verb c-  msgAdd msgClass msg--itemAidVerbMU :: (MonadClient m, MonadClientUI m, MsgShared a)-              => a -> ActorId -> MU.Part -> ItemId -> Either Int Int-              -> m ()-itemAidVerbMU msgClass aid verb iid ek = do-  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui-  body <- getsState $ getActorBody aid-  side <- getsClient sside-  factionD <- getsState sfactionD-  let lid = blid body-      fakeKit = quantSingle-  localTime <- getsState $ getLocalTime lid-  subject <- partActorLeader aid-  -- The item may no longer be in @c@, but it was.-  itemFull <- getsState $ itemToFull iid-  let object = case ek of-        Left n ->-          partItemWs rwidth side factionD n localTime itemFull fakeKit-        Right n ->-          let (name1, powers) =-                partItemShort rwidth side factionD localTime itemFull fakeKit-          in MU.Phrase ["the", MU.Car1Ws n name1, powers]-      msg = makeSentence [MU.SubjectVerbSg subject verb, object]-  msgAdd msgClass msg--mitemAidVerbMU :: (MonadClient m, MonadClientUI m, MsgShared a)-               => a -> ActorId -> MU.Part -> ItemId -> Maybe MU.Part-               -> m ()-mitemAidVerbMU msgClass aid verb iid msuffix = do-  itemD <- getsState sitemD-  case msuffix of-    Just suffix | iid `EM.member` itemD ->-      itemAidVerbMU msgClass aid (MU.Phrase [verb, suffix]) iid (Right 1)-    _ -> do-#ifdef WITH_EXPENSIVE_ASSERTIONS-      side <- getsClient sside-      b <- getsState $ getActorBody aid-      bUI <- getsSession $ getActorUI aid-      -- 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.-      assert (isNothing msuffix  -- item description not requested-              || bfid b /= side  -- not from affected faction; only observing-              `blame` "item never seen by the affected actor"-              `swith` (aid, b, bUI, verb, iid, msuffix)) $-#endif-        aidVerbMU msgClass aid verb--itemAidDistinctMU :: (MonadClient m, MonadClientUI m)-                  => MsgClassDistinct -> ActorId -> MU.Part -> MU.Part -> ItemId-                  -> m ()-itemAidDistinctMU msgClass aid verbShow verbSave iid = do-  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui-  body <- getsState $ getActorBody aid-  side <- getsClient sside-  factionD <- getsState sfactionD-  let lid = blid body-      fakeKit = quantSingle-  localTime <- getsState $ getLocalTime lid-  subject <- partActorLeader aid-  -- The item may no longer be in @c@, but it was.-  itemFull <- getsState $ itemToFull iid-  let object = let (name, powers) =-                     partItem rwidth side factionD localTime itemFull fakeKit-               in MU.Phrase [name, powers]-      t1 = makeSentence [MU.SubjectVerbSg subject verbShow, object]-      t2 = makeSentence [MU.SubjectVerbSg subject verbSave, object]-      dotsIfShorter = if t1 == t2 then "" else ".."-  msgAddDistinct msgClass (t1 <> dotsIfShorter, t2)--manyItemsAidVerbMU :: (MonadClient m, MonadClientUI m, MsgShared a)-                   => a -> ActorId -> MU.Part-                   -> [(ItemId, ItemQuant)] -> (Int -> Either (Maybe Int) Int)-                   -> m ()-manyItemsAidVerbMU msgClass aid verb sortedAssocs ekf = do-  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui-  body <- getsState $ getActorBody aid-  side <- getsClient sside-  factionD <- getsState sfactionD-  let lid = blid body-      fakeKit = quantSingle-  localTime <- getsState $ getLocalTime lid-  subject <- partActorLeader aid-  -- The item may no longer be in @c@, but it was.-  itemToF <- getsState $ flip itemToFull-  let object (iid, (k, _)) =-        let itemFull = itemToF iid-        in case ekf k of-          Left (Just n) ->-            partItemWs rwidth side factionD n localTime itemFull fakeKit-          Left Nothing ->-            let (name, powers) =-                  partItem rwidth side factionD localTime itemFull fakeKit-            in MU.Phrase [name, powers]-          Right n ->-            let (name1, powers) =-                  partItemShort rwidth side factionD localTime itemFull fakeKit-            in MU.Phrase ["the", MU.Car1Ws n name1, powers]-      msg = makeSentence [ MU.SubjectVerbSg subject verb-                         , MU.WWandW $ map object sortedAssocs]-  msgAdd msgClass msg--createActorUI :: (MonadClient m, MonadClientUI m)-              => Bool -> ActorId -> Actor -> m ()-createActorUI born aid body = do-  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui-  side <- getsClient sside-  when (bfid body == side && not (bproj body)) $ do-    let upd = ES.insert aid-    modifySession $ \sess -> sess {sselected = upd $ sselected sess}-  factionD <- getsState sfactionD-  let fact = factionD EM.! bfid body-  localTime <- getsState $ getLocalTime $ blid body-  itemFull@ItemFull{itemBase, itemKind} <- getsState $ itemToFull (btrunk body)-  actorUI <- getsSession sactorUI-  let arItem = aspectRecordFull itemFull-  unless (aid `EM.member` actorUI) $ do-    UIOptions{uHeroNames} <- getsSession sUIOptions-    let baseColor = flavourToColor $ jflavour itemBase-        basePronoun | not (bproj body)-                      && IK.isymbol itemKind == '@'-                      && fhasGender (gplayer fact) = "he"-                    | otherwise = "it"-        nameFromNumber fn k = if k == 0-                              then makePhrase [MU.Ws $ MU.Text fn, "Captain"]-                              else fn <+> tshow k-        heroNamePronoun k =-          if gcolor fact /= Color.BrWhite-          then (nameFromNumber (fname $ gplayer fact) k, "he")-          else fromMaybe (nameFromNumber (fname $ gplayer fact) k, "he")-               $ lookup k uHeroNames-        (n, bsymbol) =-          if | bproj body -> (0, if IA.checkFlag Ability.Blast arItem-                                 then IK.isymbol itemKind-                                 else '*')-             | baseColor /= Color.BrWhite -> (0, IK.isymbol itemKind)-             | otherwise -> case bnumber body of-                 Nothing ->-                   error $ "numbered actor without server-assigned number"-                           `showFailure` (aid, body)-                 Just bn -> (bn, if 0 < bn && bn < 10-                                 then Char.intToDigit bn-                                 else '@')-        (object1, object2) =-          partItemShortest rwidth (bfid body) factionD localTime-                           itemFull quantSingle-        (bname, bpronoun) =-          if | bproj body ->-               let adj = case btrajectory body of-                     Just (tra, _) | length tra < 5 -> "falling"-                     _ -> "flying"-               in (makePhrase [adj, object1, object2], basePronoun)-             | baseColor /= Color.BrWhite ->-               (makePhrase [object1, object2], basePronoun)-             | otherwise -> heroNamePronoun n-        bcolor | bproj body = if IA.checkFlag Ability.Blast arItem-                              then baseColor-                              else Color.BrWhite-               | baseColor == Color.BrWhite = gcolor fact-               | otherwise = baseColor-        bUI = ActorUI{..}-    modifySession $ \sess ->-      sess {sactorUI = EM.insert aid bUI actorUI}-  let (verb, joinYou) =-        if born-        then if bfid body == side-             then ("join you", True)-             else ("appear suddenly", False)-        else ("be spotted", False)-  mapM_ (\(iid, store) -> do-           let c = if not (bproj body) && iid == btrunk body-                   then CTrunk (bfid body) (blid body) (bpos body)-                   else CActor aid store-           updateItemSlot c iid-           recordItemLid iid c)-        ((btrunk body, CEqp)  -- store will be overwritten, unless projectile-         : filter ((/= btrunk body) . fst) (getCarriedIidCStore body))-  -- Don't spam if the actor was already visible (but, e.g., on a tile that is-  -- invisible this turn (in that case move is broken down to lose+spot)-  -- or on a distant tile, via teleport while the observer teleported, too).-  lastLost <- getsSession slastLost-  firstEnemy <--    if bfid body /= side then do-      firstE <--        if not (bproj body) && isFoe (bfid body) fact side then do-          -- Aim even if nobody can shoot at the enemy. Let's home in on him-          -- and then we can aim or melee. We set permit to False, because it's-          -- technically very hard to check aimability here, because we are-          -- in-between turns and, e.g., leader's move has not yet been taken-          -- into account.-          xhair <- getsSession sxhair-          case xhair of-            Just (TVector _) -> return ()  -- explicitly set; keep it-            _ -> modifySession $ \sess ->-                   sess { sxhair = Just $ TEnemy aid-                        , sitemSel = Nothing } -- reset flinging totally-          foes <- getsState $ foeRegularList side (blid body)-          itemsSize <- getsState $ guardItemSize body-          if ES.member aid lastLost-          then return False-          else if length foes > 1-               then do-                 when (itemsSize > 0) $ do-                   msgAdd MsgSpottedThreat "Another armed threat!"-                 return False-               else do-                 if itemsSize > 0-                 then msgAdd MsgSpottedThreat "Armed intrusion ahead!"-                 else msgAdd MsgSpottedThreat "You are not alone!"-                 return True-        else return False-      stopPlayBack-      return firstE-    else return False-  if | EM.null actorUI && bfid body == side ->-       return ()  -- don't speak about yourself in 3rd person-     | born && bproj body ->-         pushFrame False  -- make sure first position displayed-     | ES.member aid lastLost || bproj body -> markDisplayNeeded (blid body)-     | otherwise -> do-       aidVerbMU MsgSpottedActor aid verb-       if | joinYou -> msgAdd MsgTutorialHint "You survive this mission, or die trying, as a team. After a few moves, feel free to switch the controlled teammate (marked on the map with the yellow box) using the Tab key to another party member (marked with a green box)."  -- assuming newbies don't remap their keys-          | firstEnemy -> msgAdd MsgTutorialHint "Enemies can be dealt with using melee (by bumping), ranged combat, terrain effects, stealth (not being seen) or hasty retreat (particularly if they are asleep)."-          | otherwise -> return ()-       animate (blid body) $ actorX (bpos body)--destroyActorUI :: (MonadClient m, MonadClientUI m)-               => Bool -> ActorId -> Actor -> m ()-destroyActorUI destroy aid b = do-  trunk <- getsState $ getItemBody $ btrunk b-  let baseColor = flavourToColor $ jflavour trunk-  unless (baseColor == Color.BrWhite) $  -- keep setup for heroes, etc.-    modifySession $ \sess -> sess {sactorUI = EM.delete aid $ sactorUI sess}-  let dummyTarget = TPoint TKnown (blid b) (bpos b)-      affect tgt = case tgt of-        Just (TEnemy a) | a == aid -> Just $-          if destroy then-            -- If *really* nothing more interesting, the actor will-            -- go to last known location to perhaps find other foes.-            dummyTarget-          else-            -- If enemy only hides (or we stepped behind obstacle) find him.-            TPoint (TEnemyPos a) (blid b) (bpos b)-        Just (TNonEnemy a) | a == aid -> Just dummyTarget-        _ -> tgt-  modifySession $ \sess -> sess {sxhair = affect $ sxhair sess}-  unless (bproj b) $-    modifySession $ \sess -> sess {slastLost = ES.insert aid $ slastLost sess}-  side <- getsClient sside-  fact <- getsState $ (EM.! side) . sfactionD-  let gameOver = isJust $ gquit fact  -- we are the UI faction, so we determine-  unless gameOver $ do-    when (bfid b == side && not (bproj b)) $ do-      stopPlayBack-      let upd = ES.delete aid-      modifySession $ \sess -> sess {sselected = upd $ sselected sess}-      when destroy $ do-        mleader <- getsClient sleader-        when (isJust mleader)-          -- This is especially handy when the dead actor was a leader-          -- on a different level than the new one:-          clearAimMode-    -- If pushed, animate spotting again, to draw attention to pushing.-    markDisplayNeeded (blid b)--spotItemBag :: forall m. (MonadClient m, MonadClientUI m)-            => Bool -> Container -> ItemBag -> m ()-spotItemBag verbose c bag = do-  -- This is due to a move, or similar, which will be displayed,-  -- so no extra @markDisplayNeeded@ needed here and in similar places.-  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui-  side <- getsClient sside-  getKind <- getsState $ flip getIidKindId-  lid <- getsState $ lidFromC c-  localTime <- getsState $ getLocalTime lid-  factionD <- getsState sfactionD-  -- Queried just once, so many copies of a new item can be reported. OK.-  ItemSlots itemSlots <- getsSession sslots-  sxhairOld <- getsSession sxhair-  let resetXhair = case c of-        CFloor _ p -> case sxhairOld of-          Just TEnemy{} -> return ()  -- probably too important to overwrite-          Just (TPoint TEnemyPos{} _ _) -> return ()-          Just (TPoint TStash{} _ _) -> return ()-          Just (TVector _) -> return ()  -- explicitly set; keep it-          _ -> do-            -- Don't steal xhair if it's only an item on another level.-            -- For enemies, OTOH, capture xhair to alarm player.-            lidV <- viewedLevelUI-            when (lid == lidV) $ do-              bagFloor <- getsState $ getFloorBag lid p-              modifySession $ \sess ->-                sess { sxhair = Just $ TPoint (TItem bagFloor) lidV p-                     , sitemSel = Nothing }  -- reset flinging totally-        _ -> return ()-      locatedWhere = ppContainer factionD c-      beLocated = MU.Text $-        "be located" <+> if locatedWhere == ppContainer EM.empty c-                         then ""  -- boring-                         else locatedWhere-      subjectMaybe :: (ItemId, ItemQuant) -> m (Maybe (Int, MU.Part, MU.Part))-      subjectMaybe (iid, kit@(k, _)) = do-        recordItemLid iid c-        itemFull <- getsState $ itemToFull iid-        let arItem = aspectRecordFull itemFull-            slore = IA.loreFromContainer arItem c-        case lookup iid $ map swap $ EM.assocs $ itemSlots EM.! slore of-          Nothing -> do  -- never seen or would have a slot-            updateItemSlot c iid-            case c of-              CFloor{} -> do-                let subjectShort = partItemWsShortest rwidth side factionD k-                                                      localTime itemFull kit-                    subjectLong = partItemWsLong rwidth side factionD k-                                                 localTime itemFull kit-                return $ Just (k, subjectShort, subjectLong)-              _ -> return Nothing-          _ -> return Nothing  -- this item or another with the same @iid@-                               -- seen already (has a slot assigned); old news-      -- @SortOn@ less efficient here, because function cheap.-      sortItems iis = sortOn (getKind . fst) iis-      sortedAssocs = sortItems $ EM.assocs bag-  subjectMaybes <- mapM subjectMaybe sortedAssocs-  let subjects = catMaybes subjectMaybes-      sendMsg plural = do-        let subjectShort = MU.WWandW $ map (\(_, part, _) -> part) subjects-            subjectLong = MU.WWandW $ map (\(_, _, part) -> part) subjects-            msg subject =-              if plural-              then makeSentence [MU.SubjectVerb MU.PlEtc MU.Yes-                                                subject beLocated]-              else makeSentence [MU.SubjectVerbSg subject beLocated]-            msgShort = msg subjectShort-            msgLong = msg subjectLong-            dotsIfShorter = if msgShort == msgLong then "" else ".."-        resetXhair-        msgAddDistinct MsgSpottedItem (msgShort <> dotsIfShorter, msgLong)-  case subjects of-    [] -> return ()-    [(1, _, _)] -> sendMsg False-    _ -> sendMsg True-  when verbose $ case c of-    CActor aid store -> do-      let verb = MU.Text $ verbCStore store-      b <- getsState $ getActorBody aid-      fact <- getsState $ (EM.! bfid b) . sfactionD-      let underAI = isAIFact fact-      mleader <- getsClient sleader-      if Just aid == mleader && not underAI then-        manyItemsAidVerbMU MsgItemMovement aid verb sortedAssocs Right-      else when (not (bproj b) && bhp b > 0) $  -- don't announce death drops-        manyItemsAidVerbMU MsgItemMovement aid verb sortedAssocs (Left . Just)-    _ -> return ()--recordItemLid :: MonadClientUI m => ItemId -> Container -> m ()-recordItemLid iid c = do-  mjlid <- getsSession $ EM.lookup iid . sitemUI-  when (isNothing mjlid) $ do-    lid <- getsState $ lidFromC c-    modifySession $ \sess ->-      sess {sitemUI = EM.insert iid lid $ sitemUI sess}--moveActor :: (MonadClient m, MonadClientUI m)-          => ActorId -> Point -> Point -> m ()-moveActor aid source target = do-  -- If source and target tile distant, assume it's a teleportation-  -- and display an animation. Note: jumps and pushes go through all-  -- intervening tiles, so won't be considered. Note: if source or target-  -- not seen, the (half of the) animation would be boring, just a delay,-  -- not really showing a transition, so we skip it (via 'breakUpdAtomic').-  -- The message about teleportation is sometimes shown anyway, just as the X.-  body <- getsState $ getActorBody aid-  if adjacent source target-  then markDisplayNeeded (blid body)-  else do-    let ps = (source, target)-    animate (blid body) $ teleport ps-  lookAtMove aid--displaceActorUI :: (MonadClient m, MonadClientUI m)-                => ActorId -> ActorId -> m ()-displaceActorUI source target = do-  mleader <- getsClient sleader-  sb <- getsState $ getActorBody source-  tb <- getsState $ getActorBody target-  spart <- partActorLeader source-  tpart <- partActorLeader target-  let msgClass = if mleader `elem` map Just [source, target]-                 then MsgActionMajor  -- to interrupt run after a displace;-                 else MsgActionMinor  -- configurable-      msg = makeSentence [MU.SubjectVerbSg spart "displace", tpart]-  msgAdd msgClass msg-  lookAtMove source-  when (bfid sb /= bfid tb) $-    lookAtMove target  -- in case only this one is ours-  side <- getsClient sside-  -- Ours involved, but definitely not requested by player via UI.-  when (side `elem` [bfid sb, bfid tb] && mleader /= Just source) stopPlayBack-  let ps = (bpos tb, bpos sb)-  animate (blid sb) $ swapPlaces ps---- @UpdMoveItem@ is relatively rare (except within the player's faction),--- but it ensures that even if only one of the stores is visible--- (e.g., stash floor is not or actor posision is not), some messages--- will be printed (via verbose @UpdLoseItem@).-moveItemUI :: (MonadClient m, MonadClientUI m)-           => ItemId -> Int -> ActorId -> CStore -> CStore-           -> m ()-moveItemUI iid k aid cstore1 cstore2 = do-  let verb = MU.Text $ verbCStore cstore2-  b <- getsState $ getActorBody aid-  fact <- getsState $ (EM.! bfid b) . sfactionD-  let underAI = isAIFact fact-  mleader <- getsClient sleader-  ItemSlots itemSlots <- getsSession sslots-  case lookup iid $ map swap $ EM.assocs $ itemSlots EM.! SItem of-    Just _l ->-      -- So far organs can't be put into stash, so no need to call-      -- @updateItemSlot@ to add or reassign lore category.-      if cstore1 == CGround && Just aid == mleader && not underAI then-        itemAidVerbMU MsgItemMovement aid verb iid (Right k)-      else when (not (bproj b) && bhp b > 0) $  -- don't announce death drops-        itemAidVerbMU MsgItemMovement aid verb iid (Left k)-    Nothing -> error $-      "" `showFailure` (iid, k, aid, cstore1, cstore2)--quitFactionUI :: (MonadClient m, MonadClientUI m)-              => FactionId -> Maybe Status-              -> Maybe (FactionAnalytics, GenerationAnalytics)-              -> m ()-quitFactionUI fid toSt manalytics = do-  ClientOptions{sexposeItems} <- getsClient soptions-  fact <- getsState $ (EM.! fid) . sfactionD-  let fidName = MU.Text $ gname fact-      person = if fhasGender $ gplayer fact then MU.PlEtc else MU.Sg3rd-      horror = isHorrorFact fact-      camping = maybe True ((== Camping) . stOutcome) toSt-  side <- getsClient sside-  when (fid == side && not camping) $ do-    tellGameClipPS-    resetGameStart-  gameMode <- getGameMode-  allNframes <- getsSession sallNframes-  let startingPart = case toSt of-        _ | horror -> Nothing  -- Ignore summoned actors' factions.-        Just Status{stOutcome=stOutcome@Restart, stNewGame=Just gn} ->-          Just $ MU.Text $ nameOutcomeVerb stOutcome-                           <+> "to restart in" <+> fromGroupName gn <+> "mode"-                             -- when multiplayer: "order mission restart in"-        Just Status{stOutcome=Restart, stNewGame=Nothing} ->-          error $ "" `showFailure` (fid, toSt)-        Just Status{stOutcome} -> Just $ MU.Text $ nameOutcomeVerb stOutcome-          -- when multiplayer, for @Camping@: "order save and exit"-        Nothing -> Nothing-      middlePart = case toSt of-        _ | fid /= side -> Nothing-        Just Status{stOutcome} -> lookup stOutcome $ mendMsg gameMode-        Nothing -> Nothing-      partingPart = if fid /= side || allNframes == -1-                    then Nothing-                    else endMessageOutcome . stOutcome <$> toSt-  case startingPart of-    Nothing -> return ()-    Just sp ->-      let blurb = makeSentence [MU.SubjectVerb person MU.Yes fidName sp]-      in msgLnAdd MsgFinalOutcome blurb-  case (toSt, partingPart) of-    (Just status, Just pp) -> do-      noConfirmsGame <- isNoConfirmsGame-      go <- if noConfirmsGame-            then return False-            else displaySpaceEsc ColorFull ""  -- short, just @startingPart@-      recordHistory-        -- we are going to exit or restart, so record and clear, but only once-      (itemBag, total) <- getsState $ calculateTotal side-      when go $ do-        case middlePart of-          Nothing -> return ()-          Just sp1 -> do-            factionD <- getsState sfactionD-            itemToF <- getsState $ flip itemToFull-            let getTrunkFull (aid, b) = (aid, itemToF $ btrunk b)-            ourTrunks <- getsState $ map getTrunkFull-                                     . fidActorNotProjGlobalAssocs side-            let smartFaction fact2 = fleaderMode (gplayer fact2) /= LeaderNull-                canBeSmart = any (smartFaction . snd)-                canBeOurFaction = any (\(fid2, _) -> fid2 == side)-                smartEnemy trunkFull =-                  let possible =-                        possibleActorFactions (itemKind trunkFull) factionD-                  in not (canBeOurFaction possible) && canBeSmart possible-                smartEnemiesOurs = filter (smartEnemy . snd) ourTrunks-                uniqueActor trunkFull = IA.checkFlag Ability.Unique-                                        $ aspectRecordFull trunkFull-                uniqueEnemiesOurs = filter (uniqueActor . snd) smartEnemiesOurs-                smartUniqueEnemyCaptured = not $ null uniqueEnemiesOurs-                smartEnemyCaptured = not $ null smartEnemiesOurs-            smartEnemySentence <- case uniqueEnemiesOurs ++ smartEnemiesOurs of-              [] -> return ""-              (enemyAid, _) : _ -> do-                bUI <- getsSession $ getActorUI enemyAid-                return $! makePhrase [MU.Capitalize (partActor bUI)] <> "?"-            let won = maybe False ((`elem` victoryOutcomes) . stOutcome) toSt-                lost = maybe False ((`elem` deafeatOutcomes) . stOutcome) toSt-                msgClass | won = MsgGoodMiscEvent-                         | lost = MsgBadMiscEvent-                         | otherwise = MsgNeutralEvent-                (sp2, escPrompt) =-                  if | lost -> ("", "Accept the unacceptable?")-                     | smartUniqueEnemyCaptured ->-                       ( "\nOh, wait, who is this, towering behind your escaping crew?" <+> smartEnemySentence <+> "This changes everything. For everybody. Everywhere. Forever. Did you plan for this? Are you sure it was your idea?"-                       , "What happens now?" )-                     | smartEnemyCaptured ->-                       ( "\nOh, wait, who is this, hunched among your escaping crew?" <+> smartEnemySentence <+> "Suddenly, this makes your crazy story credible. Suddenly, the door of knowledge opens again."-                       , "How will you play that move?" )-                     | otherwise -> ("", "Let's see what we've got here.")-            msgAdd msgClass sp1-            msgAdd MsgFactionIntel sp2-            void $ displaySpaceEsc ColorFull escPrompt-        case manalytics of-          Nothing -> return ()-          Just (factionAn, generationAn) ->-            cycleLore []-              [ displayGameOverLoot (itemBag, total) generationAn-              , displayGameOverLore SOrgan True generationAn-              , displayGameOverAnalytics factionAn generationAn-              , displayGameOverLore SCondition sexposeItems generationAn-              , displayGameOverLore SBlast True generationAn-              , displayGameOverLore SEmbed True generationAn ]-      unless noConfirmsGame $ do-        -- Show score for any UI client after any kind of game exit,-        -- even though it's saved only for human UI clients at game over-        -- (that is not a noConfirms or benchmark game).-        scoreSlides <- scoreToSlideshow total status-        void $ getConfirms ColorFull [K.spaceKM, K.escKM] scoreSlides-      -- The last prompt stays onscreen during shutdown, etc.-      when (not noConfirmsGame || camping) $ do-        msgAdd MsgPromptGeneric pp-        when camping $ msgAdd MsgPromptGeneric "Saving..."-        pushFrame False  -- don't leave frozen prompts on the browser screen-    _ -> when (isJust startingPart && (stOutcome <$> toSt) == Just Killed) $-      -- Needed not to overlook the competitor dying in raid scenario.-      displayMore ColorFull ""--displayGameOverLoot :: (MonadClient m, MonadClientUI m)-                    => (ItemBag, Int) -> GenerationAnalytics -> m K.KM-displayGameOverLoot (heldBag, total) generationAn = do-  ClientOptions{sexposeItems} <- getsClient soptions-  COps{coitem} <- getsState scops-  ItemSlots itemSlots <- getsSession sslots-  -- We assume "gold grain", not "grain" with label "of gold":-  let currencyName = IK.iname $ okind coitem $ ouniqGroup coitem IK.S_CURRENCY-      lSlotsRaw = EM.filter (`EM.member` heldBag) $ itemSlots EM.! SItem-      generationItem = generationAn EM.! SItem-      (itemBag, lSlots) =-        if sexposeItems-        then let generationBag = EM.map (\k -> (-k, [])) generationItem-                 bag = heldBag `EM.union` generationBag-                 slots = EM.fromDistinctAscList $ zip allSlots $ EM.keys bag-             in (bag, slots)-        else (heldBag, lSlotsRaw)-      promptFun iid itemFull2 k =-        let worth = itemPrice 1 $ itemKind itemFull2-            lootMsg = if worth == 0 then "" else-              let pile = if k == 1 then "exemplar" else "hoard"-              in makeSentence $-                   ["this treasure", pile, "is worth"]-                   ++ (if k > 1 then [ MU.Cardinal k, "times"] else [])-                   ++ [MU.CarWs worth $ MU.Text currencyName]-            holdsMsg =-              let n = generationItem EM.! iid-              in if | max 0 k == 1 && n == 1 ->-                      "You keep the only specimen extant:"-                    | max 0 k == 0 && n == 1 ->-                      "You don't have the only hypothesized specimen:"-                    | max 0 k == 0 && n == 0 ->-                      "No such specimen was recorded:"-                    | otherwise ->-                        makePhrase [ "You hold"-                                   , if k == n-                                     then "all pieces"-                                     else MU.CardinalAWs (max 0 k) "piece"-                                   , "out of"-                                   , MU.Car n-                                   , "scattered:" ]-        in lootMsg <+> holdsMsg-  dungeonTotal <- getsState sgold-  let promptGold = spoilsBlurb currencyName total dungeonTotal-      -- Total number of items is meaningless in the presence of so much junk.-      prompt =-        promptGold-        <+> (if sexposeItems-             then "Non-positive count means none held but this many generated."-             else "")-      examItem = displayItemLore itemBag 0 promptFun-  viewLoreItems "GameOverLoot" lSlots itemBag prompt examItem True--displayGameOverAnalytics :: (MonadClient m, MonadClientUI m)-                         => FactionAnalytics -> GenerationAnalytics-                         -> m K.KM-displayGameOverAnalytics factionAn generationAn = do-  ClientOptions{sexposeActors} <- getsClient soptions-  side <- getsClient sside-  ItemSlots itemSlots <- getsSession sslots-  let ourAn = akillCounts-              $ EM.findWithDefault emptyAnalytics side factionAn-      foesAn = EM.unionsWith (+)-               $ concatMap EM.elems $ catMaybes-               $ map (`EM.lookup` ourAn) [KillKineticMelee .. KillOtherPush]-      trunkBagRaw = EM.map (, []) foesAn-      lSlotsRaw = EM.filter (`EM.member` trunkBagRaw) $ itemSlots EM.! STrunk-      killedBag = EM.fromList $ map (\iid -> (iid, trunkBagRaw EM.! iid))-                                    (EM.elems lSlotsRaw)-      generationTrunk = generationAn EM.! STrunk-      (trunkBag, lSlots) =-        if sexposeActors-        then let generationBag = EM.map (\k -> (-k, [])) generationTrunk-                 bag = killedBag `EM.union` generationBag-                 slots = EM.fromDistinctAscList $ zip allSlots $ EM.keys bag-             in (bag, slots)-        else (killedBag, lSlotsRaw)-      total = sum $ filter (> 0) $ map fst $ EM.elems trunkBag-      -- Not just "killed 1 out of 4", because it's sometimes "2 out of 1",-      -- if an enemy was revived.-      promptFun :: ItemId -> ItemFull-> Int -> Text-      promptFun iid _ k =-        let n = generationTrunk EM.! iid-        in makePhrase [ "You recall the adversary, which you killed on"-                      , MU.CarWs (max 0 k) "occasion", "while reports mention"-                      , MU.CarWs n "individual", "in total:" ]-      prompt =-        makeSentence ["your team vanquished", MU.CarWs total "adversary"]-          -- total reported would include our own, so not given-        <+> (if sexposeActors-             then "Non-positive count means none killed but this many reported."-             else "")-      examItem = displayItemLore trunkBag 0 promptFun-  viewLoreItems "GameOverAnalytics" lSlots trunkBag prompt examItem False--displayGameOverLore :: (MonadClient m, MonadClientUI m)-                    => SLore -> Bool -> GenerationAnalytics -> m K.KM-displayGameOverLore slore exposeCount generationAn = do-  let generationLore = generationAn EM.! slore-      generationBag = EM.map (\k -> (if exposeCount then k else 1, []))-                             generationLore-      total = sum $ map fst $ EM.elems generationBag-      slots = EM.fromDistinctAscList $ zip allSlots $ EM.keys generationBag-      promptFun :: ItemId -> ItemFull-> Int -> Text-      promptFun _ _ k =-        makeSentence-          [ "this", MU.Text (ppSLore slore), "manifested during your quest"-          , MU.CarWs k "time" ]-      verb = if slore `elem` [SCondition, SBlast]-             then "experienced"-             else "saw"-      prompt | total == 0 =-               makeSentence [ "you didn't experience any"-                            , MU.Ws $ MU.Text (headingSLore slore)-                            , "this time" ]-             | otherwise =-               makeSentence [ "you", verb, "the following variety of"-                            , MU.CarWs total $ MU.Text (headingSLore slore) ]-      examItem = displayItemLore generationBag 0 promptFun-      displayRanged = slore `notElem` [SOrgan, STrunk]-  viewLoreItems ("GameOverLore" ++ show slore)-                slots generationBag prompt examItem displayRanged---- The item may be used up already and so not present in the container,--- e.g., if the item destroyed itself. This is OK. Message is still needed.-discover :: (MonadClient m, MonadClientUI m) => Container -> ItemId -> m ()-discover c iid = do-  COps{coitem} <- getsState scops-  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui-  lid <- getsState $ lidFromC c-  globalTime <- getsState stime-  localTime <- getsState $ getLocalTime lid-  itemFull <- getsState $ itemToFull iid-  bag <- getsState $ getContainerBag c-  side <- getsClient sside-  factionD <- getsState sfactionD-  (noMsg, nameWhere) <- case c of-    CActor aidOwner storeOwner -> do-      bOwner <- getsState $ getActorBody aidOwner-      name <- if bproj bOwner-              then return []-              else ppContainerWownW partActorLeader True c-      let arItem = aspectRecordFull itemFull-          inMetaGame = IA.checkFlag Ability.MetaGame arItem-          isOurOrgan = bfid bOwner == side-                       && storeOwner == COrgan-                       && not inMetaGame-            -- assume own faction organs known intuitively,-            -- except backstories and other meta game items-      return (isOurOrgan, name)-    CTrunk _ _ p | p == originPoint -> return (True, [])-      -- the special reveal at game over, using fake @CTrunk@; don't spam-    _ -> return (False, [])-  let kit = EM.findWithDefault quantSingle iid bag-              -- may be used up by that time-      knownName = makePhrase-        [partItemMediumAW rwidth side factionD localTime itemFull kit]-      flav = flavourToName $ jflavour $ itemBase itemFull-      (object1, object2) =-        partItemShortest rwidth side factionD localTime itemFull kit-      name1 = makePhrase [object1, object2]-      -- Make sure the two names in the message differ.-      (ikObvious, itemKind) = case jkind $ itemBase itemFull of-        IdentityObvious ik -> (True, ik)-        IdentityCovered _ix ik -> (False, ik)-          -- fake kind (template); OK, we talk about appearances-      name2 = IK.iname $ okind coitem itemKind-      name = if ikObvious && T.unwords (tail (T.words knownName)) /= name1-             then name1  -- avoid "a pair turns out to be"-             else name2  -- avoid "chip of scientific explanation"-      unknownName = MU.Phrase $ [MU.Text flav, MU.Text name] ++ nameWhere-      msg = makeSentence-        [ "the"-        , MU.SubjectVerbSg unknownName "turn out to be"-        , MU.Text knownName ]-  unless (noMsg || globalTime == timeZero) $  -- no spam about initial equipment-    msgAdd MsgItemDiscovery msg--ppHearMsg :: MonadClientUI m => Maybe Int -> HearMsg -> m Text-ppHearMsg distance hearMsg = case hearMsg of-  HearUpd cmd -> do-    COps{coTileSpeedup} <- getsState scops-    let sound = case cmd of-          UpdDestroyActor{} -> "shriek"-          UpdCreateItem{} -> "clatter"-          UpdTrajectory{} -> "thud"  -- A non-blast projectle hits a tile.-          UpdAlterTile _ _ fromTile toTile ->-            if | Tile.isOpenable coTileSpeedup fromTile-                 && Tile.isClosable coTileSpeedup toTile-                 || Tile.isClosable coTileSpeedup fromTile-                    && Tile.isOpenable coTileSpeedup toTile -> "creaking sound"-               | Tile.isWalkable coTileSpeedup fromTile-                 && Tile.isWalkable coTileSpeedup toTile -> "splash"-               | otherwise -> "rumble"-          UpdAlterExplorable _ k ->-            if k > 0 then "grinding noise" else "fizzing noise"-          _ -> error $ "" `showFailure` cmd-        adjective = MU.Text $ ppHearDistanceAdjective distance-        msg = makeSentence ["you hear", MU.AW $ MU.Phrase [adjective, sound]]-    return $! msg-  HearStrike ik -> do-    COps{coitem} <- getsState scops-    let verb = IK.iverbHit $ okind coitem ik-        adverb = MU.Text $ ppHearDistanceAdverb distance-        msg = makeSentence [ "you", adverb, "hear something"-                           , MU.Text verb, "someone" ]-    return $! msg-  HearSummon isProj grp p -> do-    let verb = if isProj then "something lure" else "somebody summon"-        part = MU.Text $ fromGroupName grp-        object = if p == 1  -- works, because exact number sent, not dice-                 then MU.AW part-                 else MU.Ws part-        adverb = MU.Text $ ppHearDistanceAdverb distance-    return $! makeSentence ["you", adverb, "hear", verb, object]-  HearCollideTile -> do-    let adverb = MU.Text $ ppHearDistanceAdverb distance-    return $! makeSentence ["you", adverb, "hear someone crash into something"]-  HearTaunt t -> do-    let adverb = MU.Text $ ppHearDistanceAdverb distance-    return $! makePhrase ["You", adverb, "overhear", MU.Text t]--ppHearDistanceAdjective :: Maybe Int -> Text-ppHearDistanceAdjective Nothing = "indistinct"-ppHearDistanceAdjective (Just 0) = "very close"-ppHearDistanceAdjective (Just 1) = "close"-ppHearDistanceAdjective (Just 2) = ""-ppHearDistanceAdjective (Just 3) = "remote"-ppHearDistanceAdjective (Just 4) = "distant"-ppHearDistanceAdjective (Just _) = "far-off"--ppHearDistanceAdverb :: Maybe Int -> Text-ppHearDistanceAdverb Nothing = "indistinctly"-ppHearDistanceAdverb (Just 0) = "very clearly"-ppHearDistanceAdverb (Just 1) = "clearly"-ppHearDistanceAdverb (Just 2) = ""-ppHearDistanceAdverb (Just 3) = "remotely"-ppHearDistanceAdverb (Just 4) = "distantly"-ppHearDistanceAdverb (Just _) = "barely"---- * RespSfxAtomicUI---- | 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.-displayRespSfxAtomicUI :: (MonadClient m, MonadClientUI m) => SfxAtomic -> m ()-{-# INLINE displayRespSfxAtomicUI #-}-displayRespSfxAtomicUI 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 = case EM.lookup (IK.isymbol itemKind) rapplyVerbMap of-          Just verb -> MU.Text verb-          Nothing -> "trigger"-    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-    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-          when isOurCharacter $ msgAdd MsgTutorialHint "You took damage of a different kind than the normal piercing hit, which means you armor didn't block any part of it. Normally, your HP (hit points, health) do not regenerate, so losing them is a big deal. Apply healing concoctions or take a long sleep to replenish your HP (but in this hectic environment not even uninterrupted resting that leads to sleep is easy)."-        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 MsgEffectMinor 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 MsgEffectMinor 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 $ fromGroupName 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"-              msgAdd MsgTutorialHint "New floor is new opportunities, though the old level is still there and others may roam it after you left. Viewing all floors, without moving between them, can be done using the '<' and '>' keys."-            [] -> 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 aidVerbMU MsgEffectMinor 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"-        mitemAidVerbMU MsgEffectMinor 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 "propelled by"-        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 "heat up"-      IK.Discharge{} | not isAlive -> return ()-      IK.Discharge{} -> aidVerbMU MsgEffectMedium aid "cool down"-      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 iid c ->-    itemVerbMU MsgInnerWorkSpam iid (1, []) "have been triggered" c-  SfxMsgFid _ sfxMsg -> do-    mleader <- getsClient sleader-    case mleader of-      Just{} -> return ()  -- will display stuff 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-      msgAdd MsgMiscellanous $!-        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 :: (MonadClient m, 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)-    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 = intCast 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-    -- 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) $-           msgAdd MsgTutorialHint "You managed to catch a projectile, thanks to being braced and hitting it exactly when it was at arm's reach. The obtained item has been put into the shared stash of the party."-         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-         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 <> "."-         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 <> "."-         actorMaxSkills <- getsState sactorMaxSkills-         when (bfid sb == side-               && not (actorCanMeleeToHarm actorMaxSkills target tb)) $-           msgAdd MsgTutorialHint "This enemy can't harm you. Left alone could it possibly be of some use?"-         animateAlive (blid tb) basicAnim
engine-src/Game/LambdaHack/Client/UI/DrawM.hs view
@@ -72,6 +72,7 @@ import qualified Game.LambdaHack.Definition.Ability as Ability import qualified Game.LambdaHack.Definition.Color as Color import           Game.LambdaHack.Definition.Defs+import qualified Game.LambdaHack.Definition.DefsInternal as DefsInternal  targetDesc :: MonadClientUI m => Maybe Target -> m (Maybe Text, Maybe Text) targetDesc mtarget = do@@ -120,14 +121,11 @@               _ -> return $! "many items at" <+> tshow p           else return $! "an exact spot on level" <+> tshow (abs $ fromEnum lid)         return (Just pointedText, Nothing)-    Just TVector{} ->-      case mleader of-        Nothing -> return (Just "a relative shift", Nothing)-        Just aid -> do-          mtgtPos <- getsState $ aidTgtToPos aid lidV mtarget-          let invalidMsg = "an invalid relative shift"-              validMsg p = "shift to" <+> tshow p-          return (Just $ maybe invalidMsg validMsg mtgtPos, Nothing)+    Just TVector{} -> do+      mtgtPos <- getsState $ aidTgtToPos mleader lidV mtarget+      let invalidMsg = "a relative shift"+          validMsg p = "shift to" <+> tshow p+      return (Just $ maybe invalidMsg validMsg mtgtPos, Nothing)     Nothing -> return (Nothing, Nothing)  targetDescXhair :: MonadClientUI m@@ -147,7 +145,7 @@  drawFrameTerrain :: forall m. MonadClientUI m => LevelId -> m (U.Vector Word32) drawFrameTerrain drawnLevelId = do-  COps{corule=RuleContent{rXmax}, cotile, coTileSpeedup} <- getsState scops+  COps{corule=RuleContent{rWidthMax}, cotile, coTileSpeedup} <- getsState scops   StateClient{smarkSuspect} <- getClient   -- Not @ScreenContent@, because indexing in level's data.   Level{ltile=PointArray.Array{avector}, lembed} <- getLevel drawnLevelId@@ -174,12 +172,12 @@                | otherwise = tcolor2         in Color.attrChar2ToW32 fg tsymbol       g :: PointI -> Word16 -> Word32-      g !pI !tile = Color.attrCharW32 $ dis pI (toContentId tile)+      g !pI !tile = Color.attrCharW32 $ dis pI (DefsInternal.toContentId tile)       caveVector :: U.Vector Word32       caveVector = U.imap g avector       messageVector =-        U.replicate rXmax (Color.attrCharW32 Color.spaceAttrW32)-      statusVector = U.fromListN (2 * rXmax) $ map Color.attrCharW32 frameStatus+        U.replicate rWidthMax (Color.attrCharW32 Color.spaceAttrW32)+      statusVector = U.fromListN (2 * rWidthMax) $ map Color.attrCharW32 frameStatus   -- The vector package is so smart that the 3 vectors are not allocated   -- separately at all, but written to the big vector at once.   -- But even with double allocation it would be faster than writing@@ -188,7 +186,7 @@  drawFrameContent :: forall m. MonadClientUI m => LevelId -> m FrameForall drawFrameContent drawnLevelId = do-  COps{corule=RuleContent{rXmax}} <- getsState scops+  COps{corule=RuleContent{rWidthMax}} <- getsState scops   SessionUI{smarkSmell} <- getSession   -- Not @ScreenContent@, because indexing in level's data.   Level{lsmell, ltime, lfloor} <- getLevel drawnLevelId@@ -211,7 +209,7 @@         let g :: (PointI, a) -> ST s ()             g (!pI, !a0) = do               let w = Color.attrCharW32 $ f pI a0-              VM.write v (pI + rXmax) w+              VM.write v (pI + rWidthMax) w         mapM_ g l       -- We don't usually show embedded items, because normally we don't       -- want them to clutter the display. If they are really important,@@ -233,21 +231,20 @@  if isNothing saimMode || sreportNull  then return $! FrameForall $ \_ -> return ()  else do-  COps{corule=RuleContent{rXmax, rYmax}, coTileSpeedup} <- getsState scops+  COps{corule=RuleContent{rWidthMax, rHeightMax}, coTileSpeedup}+    <- getsState scops   StateClient{seps} <- getClient   -- Not @ScreenContent@, because pathing in level's map.   Level{ltile=PointArray.Array{avector}} <- getLevel drawnLevelId   totVisible <- totalVisible <$> getPerFid drawnLevelId   mleader <- getsClient sleader-  mpos <- getsState $ \s -> bpos . (`getActorBody` s) <$> mleader-  xhairPosRaw <- xhairToPos-  let xhairPos = fromMaybe (fromMaybe originPoint mpos) xhairPosRaw+  xhairPos <- xhairToPos   bline <- case mleader of     Just leader -> do       Actor{bpos, blid} <- getsState $ getActorBody leader       return $! if blid /= drawnLevelId                 then []-                else fromMaybe [] $ bla rXmax rYmax seps bpos xhairPos+                else fromMaybe [] $ bla seps bpos xhairPos     _ -> return []   mpath <- maybe (return Nothing) (\aid -> do     mtgtMPath <- getsClient $ EM.lookup aid . stargetD@@ -263,6 +260,7 @@           trajectoryToPath prPos (fst p)         _ -> []       shiftedLine = delete xhairPos+                    $ takeWhile (insideP (0, 0, rWidthMax - 1, rHeightMax - 1))                     $ if null shiftedBTrajectory                       then bline                       else shiftedBTrajectory@@ -275,9 +273,9 @@                 _ | isUknownSpace tile -> Color.BrBlack                 _ | Tile.isSuspect coTileSpeedup tile -> Color.BrMagenta                 (True, True)   -> Color.BrGreen-                (True, False)  -> Color.BrCyan+                (True, False)  -> Color.BrRed                 (False, True)  -> Color.Green-                (False, False) -> Color.Cyan+                (False, False) -> Color.Red         in Color.attrChar2ToW32 fgOnPathOrLine ch       mapVTL :: forall s. (Point -> ContentId TileKind -> Color.AttrCharW32)              -> [Point]@@ -287,8 +285,8 @@             g !p0 = do               let pI = fromEnum p0                   tile = avector U.! pI-                  w = Color.attrCharW32 $ f p0 (toContentId tile)-              VM.write v (pI + rXmax) w+                  w = Color.attrCharW32 $ f p0 (DefsInternal.toContentId tile)+              VM.write v (pI + rWidthMax) w         mapM_ g l       upd :: FrameForall       upd = FrameForall $ \v -> do@@ -298,7 +296,7 @@  drawFrameActor :: forall m. MonadClientUI m => LevelId -> m FrameForall drawFrameActor drawnLevelId = do-  COps{corule=RuleContent{rXmax}} <- getsState scops+  COps{corule=RuleContent{rWidthMax}} <- getsState scops   SessionUI{sactorUI, sselected, sUIOptions} <- getSession   -- Not @ScreenContent@, because indexing in level's data.   Level{lbig, lproj} <- getLevel drawnLevelId@@ -347,7 +345,7 @@         let g :: (PointI, a) -> ST s ()             g (!pI, !a0) = do               let w = Color.attrCharW32 $ f a0-              VM.write v (pI + rXmax) w+              VM.write v (pI + rWidthMax) w         mapM_ g l       upd :: FrameForall       upd = FrameForall $ \v -> do@@ -359,28 +357,23 @@ drawFrameExtra :: forall m. MonadClientUI m                => ColorMode -> LevelId -> m FrameForall drawFrameExtra dm drawnLevelId = do-  COps{corule=RuleContent{rXmax, rYmax}} <- getsState scops+  COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops   SessionUI{saimMode, smarkVision} <- getSession   -- Not @ScreenContent@, because indexing in level's data.   totVisible <- totalVisible <$> getPerFid drawnLevelId-  mxhairPos <- xhairToPos+  mxhairPos <- mxhairToPos   mtgtPos <- do     mleader <- getsClient sleader-    case mleader of-      Nothing -> return Nothing-      Just leader -> do-        mtgt <- getsClient $ getTarget leader-        getsState $ aidTgtToPos leader drawnLevelId mtgt+    mtgt <- getsClient $ maybe (const Nothing) getTarget mleader+    getsState $ aidTgtToPos mleader drawnLevelId mtgt   side <- getsClient sside   factionD <- getsState sfactionD-  let visionMarks =-        if smarkVision-        then IS.toList $ ES.enumSetToIntSet totVisible-        else []+  let visionMarks = IS.toList $ ES.enumSetToIntSet totVisible       backlightVision :: Color.AttrChar -> Color.AttrChar       backlightVision ac = case ac of-        Color.AttrChar (Color.Attr fg _) ch ->-          Color.AttrChar (Color.Attr fg Color.HighlightGrey) ch+        Color.AttrChar (Color.Attr fg Color.HighlightNone) ch ->+          Color.AttrChar (Color.Attr fg Color.HighlightBackground) ch+        _ -> ac       writeSquare !hi (Color.AttrChar (Color.Attr fg bg) ch) =         let hiUnlessLeader | bg == Color.HighlightYellow = bg                            | otherwise = hi@@ -391,14 +384,14 @@       mapVL f l v = do         let g :: PointI -> ST s ()             g !pI = do-              w0 <- VM.read v (pI + rXmax)+              w0 <- VM.read v (pI + rWidthMax)               let w = Color.attrCharW32 . Color.attrCharToW32                       . f . Color.attrCharFromW32 . Color.AttrCharW32 $ w0-              VM.write v (pI + rXmax) w+              VM.write v (pI + rWidthMax) w         mapM_ g l-      -- Here @rXmax@ and @rYmax@ are correct, because we are not+      -- Here @rWidthMax@ and @rHeightMax@ are correct, because we are not       -- turning the whole screen into black&white, but only the level map.-      lDungeon = [0..rXmax * rYmax - 1]+      lDungeon = [0..rWidthMax * rHeightMax - 1]       xhairColor = if isJust saimMode                    then Color.HighlightRedAim                    else Color.HighlightRed@@ -412,7 +405,8 @@       stashesToDisplay = mapMaybe locateStash $ EM.assocs factionD       upd :: FrameForall       upd = FrameForall $ \v -> do-        when (isJust saimMode) $ mapVL backlightVision visionMarks v+        when (isJust saimMode && smarkVision >= 1 || smarkVision == 2) $+          mapVL backlightVision visionMarks v         case mtgtPos of           Nothing -> return ()           Just p -> mapVL (writeSquare Color.HighlightGrey) [fromEnum p] v@@ -426,10 +420,10 @@  drawFrameStatus :: MonadClientUI m => LevelId -> m AttrString drawFrameStatus drawnLevelId = do-  cops@COps{corule=RuleContent{rXmax=_rXmax}} <- getsState scops+  cops@COps{corule=RuleContent{rWidthMax=_rWidthMax}} <- getsState scops   SessionUI{sselected, saimMode, swaitTimes, sitemSel} <- getSession   mleader <- getsClient sleader-  xhairPos <- xhairToPos+  mxhairPos <- mxhairToPos   mbfs <- maybe (return Nothing) (\aid -> Just <$> getCacheBfs aid) mleader   (mhairDesc, mxhairHP, mxhairWatchfulness) <- targetDescXhair   lvl <- getLevel drawnLevelId@@ -482,13 +476,13 @@             text = fromMaybe (pText <+> lText) mt         in if T.null text then "" else " " <> text       -- The indicators must fit, they are the actual information.-      pathCsr = displayPathText xhairPos mxhairHP+      pathCsr = displayPathText mxhairPos mxhairHP       trimTgtDesc n t = assert (not (T.null t) && n > 2 `blame` (t, n)) $         if T.length t <= n then t else T.take (n - 3) t <> "..."       -- The indicators must fit, they are the actual information.       widthXhairOrItem = widthTgt - T.length pathCsr       nMember = MU.Ord $ 1 + sum (EM.elems $ gvictims fact)-      fallback = if MK.fleaderMode (gplayer fact) == MK.LeaderNull+      fallback = if MK.fleaderMode (gplayer fact) == Nothing                  then "This faction never picks a pointman"                  else makePhrase                         ["Waiting for", nMember, "team member to spawn"]@@ -571,7 +565,7 @@   -- Keep it at least partially lazy, to avoid allocating the whole list:   return #ifdef WITH_EXPENSIVE_ASSERTIONS-    $ assert (length status == 2 * _rXmax+    $ assert (length status == 2 * _rWidthMax               `blame` map Color.charFromW32 status) #endif         status@@ -587,8 +581,8 @@   soptions <- getsClient soptions   let upd = FrameForall $ \v -> do         unFrameForall updContent v-        -- vty frontend is screen-reader friendly, so avoid visual fluff-        unless (frontendName soptions == "vty") $ unFrameForall updPath v+        -- ANSI frontend is screen-reader friendly, so avoid visual fluff+        unless (frontendName soptions == "ANSI") $ unFrameForall updPath v         unFrameForall updActor v         unFrameForall updExtra v   return (baseTerrain, upd)@@ -693,7 +687,7 @@ drawLeaderDamage :: MonadClientUI m => Int -> ActorId -> m AttrString drawLeaderDamage width leader = do   kitAssRaw <- getsState $ kitAssocs leader [CEqp, COrgan]-  actorCurAndMaxSk <- leaderSkillsClientUI+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   let unBurn (IK.Burn d) = Just d       unBurn _ = Nothing       unRefillHP (IK.RefillHP n) = Just n@@ -703,7 +697,7 @@             (IK.ieffects $ itemKind itemFull)       ppDice :: Bool -> (Bool, Int, Int, ItemFullKit)              -> [(Bool, (AttrString, AttrString))]-      ppDice showInBrief (hasEffect, timeout, nch, (itemFull, (k, _))) =+      ppDice showInBrief (hasEffect, timeout, ncha, (itemFull, (k, _))) =         let dice = IK.idamage $ itemKind itemFull             tdice = case Dice.reduceDice dice of               Just d | showInBrief -> show d@@ -727,9 +721,9 @@                  ++ map (Color.attrChar2ToW32 chp) tRefillHP             possiblyHasTimeout = timeout > 0 || itemSuspect itemFull         in if possiblyHasTimeout-           then replicate (k - nch)+           then replicate (k - ncha)                           (False, (ldice Color.Cyan, lBurnHP False))-                ++ replicate nch (True, (ldice Color.BrCyan, lBurnHP True))+                ++ replicate ncha (True, (ldice Color.BrCyan, lBurnHP True))            else [(True, (ldice Color.BrBlue, lBurnHP True))]       lbonus :: AttrString       lbonus =
engine-src/Game/LambdaHack/Client/UI/EffectDescription.hs view
@@ -36,7 +36,7 @@ instance Binary DetailLevel  defaultDetailLevel :: DetailLevel-defaultDetailLevel = DetailAll  -- TODO: take from config file+defaultDetailLevel = DetailAll  -- TODO: take from config file, #217  -- | Suffix to append to a basic content name if the content causes the effect. --@@ -53,13 +53,13 @@   case effect of     Burn d -> wrapInParens (tshow d                             <+> if Dice.supDice d > 1 then "burns" else "burn")-    Explode t -> "of" <+> fromGroupName t <+> "explosion"+    Explode t -> "of" <+> displayGroupName t <+> "explosion"     RefillHP p | p > 0 -> "of healing" <+> wrapInParens (affixBonus p)     RefillHP 0 -> error $ "" `showFailure` effect-    RefillHP p -> "of wounding" <+> wrapInParens (affixBonus p)+    RefillHP p -> "of wounding" <+> wrapInParens (tshow $ abs p)     RefillCalm p | p > 0 -> "of soothing" <+> wrapInParens (affixBonus p)     RefillCalm 0 -> error $ "" `showFailure` effect-    RefillCalm p -> "of dismaying" <+> wrapInParens (affixBonus p)+    RefillCalm p -> "of dismaying" <+> wrapInParens (tshow $ abs p)     Dominate -> "of domination"     Impress -> "of impression"     PutToSleep -> "of sleep"@@ -67,7 +67,7 @@     Summon grp d -> makePhrase       [ "of summoning"       , if Dice.supDice d <= 1 then "" else MU.Text $ tshow d-      , MU.Ws $ MU.Text $ fromGroupName grp ]+      , MU.Ws $ MU.Text $ displayGroupName grp ]     ApplyPerfume -> "of smell removal"     Ascend True -> "of ascending"     Ascend False -> "of descending"@@ -100,9 +100,9 @@     Teleport dice -> "of teleport" <+> wrapInParens (tshow dice)     CreateItem _ COrgan grp tim ->       let stime = if isTimerNone tim then "" else "for" <+> tshow tim <> ":"-      in "(keep" <+> stime <+> fromGroupName grp <> ")"+      in "(keep" <+> stime <+> displayGroupName grp <> ")"     CreateItem _ _ grp _ ->-      makePhrase ["of gain", MU.AW $ MU.Text $ fromGroupName grp]+      makePhrase ["of gain", MU.AW $ MU.Text $ displayGroupName grp]     DestroyItem{} -> "of loss"     ConsumeItems{} -> "of consumption from the ground"       -- too much noise from crafting@@ -117,7 +117,7 @@             if store == COrgan             then ("nullify", "")             else ("drop", "from" <+> snd (ppCStore store))-      in "of" <+> verb <+> preT <+> fromGroupName grp <+> postT <+> fromStore+      in "of" <+> verb <+> preT <+> displayGroupName grp <+> postT <+> fromStore     Recharge n dice ->       let times = if n == 1 then "" else tshow n <+> "times"       in case Dice.reduceDice dice of@@ -520,7 +520,7 @@  describeTools :: [(Int, GroupName ItemKind)] -> MU.Part describeTools =-  let carAWs (k, grp) = MU.CarAWs k (MU.Text $ fromGroupName grp)+  let carAWs (k, grp) = MU.CarAWs k (MU.Text $ displayGroupName grp)   in MU.WWandW . map carAWs  describeToolsAlternative :: [[(Int, GroupName ItemKind)]] -> Text
engine-src/Game/LambdaHack/Client/UI/Frame.hs view
@@ -1,5 +1,10 @@ {-# LANGUAGE RankNTypes #-} -- | Screen frames.+--+-- Note that @PointArray.Array@ here represents a screen frame and so+-- screen positions are denoted by @Point@, contrary to the convention+-- that @Point@ refers to game map coordinates, as outlined+-- in description of 'PointSquare' that should normally be used in that role. module Game.LambdaHack.Client.UI.Frame   ( ColorMode(..)   , FrameST, FrameForall(..), FrameBase(..), Frame@@ -17,7 +22,7 @@ import Game.LambdaHack.Core.Prelude  import           Control.Monad.ST.Strict-import qualified Data.IntMap.Strict as IM+import           Data.Function import qualified Data.Vector.Generic as G import qualified Data.Vector.Unboxed as U import qualified Data.Vector.Unboxed.Mutable as VM@@ -47,11 +52,12 @@   {unFrameBase :: forall s. ST s (G.Mutable U.Vector s Word32)}  -- | A frame, that is, a base frame and all its modifications.-type Frame = ((FrameBase, FrameForall), (OverlaySpace, OverlaySpace))+type Frame = ( (FrameBase, FrameForall)+             , (OverlaySpace, OverlaySpace, OverlaySpace) )  -- | Components of a frame, before it's decided if the first can be overwritten -- in-place or needs to be copied.-type PreFrame3 = (PreFrame, (OverlaySpace, OverlaySpace))+type PreFrame3 = (PreFrame, (OverlaySpace, OverlaySpace, OverlaySpace))  -- | Sequence of screen frames, including delays. Potentially based on a single -- base frame.@@ -74,17 +80,22 @@         writeAt (off + 1) rest   writeAt offset al --- | A frame that is padded to fill the whole screen with an optional--- overlay to display in proportional font.+-- | A frame that is padded to fill the whole screen with optional+-- overlays to display in proportional, square and monospace fonts. -- -- Note that we don't provide a list of color-highlighed box positions -- to be drawn separately, because overlays need to obscure not only map, -- but the highlights as well, so highlights need to be included earlier.+--+-- See the description of 'PointSquare' for explanation of why screen+-- coordinates in @singleArray@ are @Point@ even though they should be+-- 'PointSquare'. data SingleFrame = SingleFrame-  { singleArray       :: PointArray.Array Color.AttrCharW32-  , singlePropOverlay :: OverlaySpace-  , singleMonoOverlay :: OverlaySpace }-  deriving (Eq, Show)+  { singleArray         :: PointArray.Array Color.AttrCharW32+  , singlePropOverlay   :: OverlaySpace+  , singleSquareOverlay :: OverlaySpace+  , singleMonoOverlay   :: OverlaySpace }+  deriving (Show, Eq)  type OverlaySpace = [(PointUI, AttrString)] @@ -93,6 +104,7 @@   SingleFrame (PointArray.replicateA rwidth rheight Color.spaceAttrW32)               []               []+              []  -- | Truncate the overlay: for each line, if it's too long, it's truncated -- and if there are too many lines, excess is dropped and warning is appended.@@ -104,7 +116,7 @@ truncateOverlay halveXstart width rheight wipeAdjacentRaw fillLen onBlank ov =   let wipeAdjacent = wipeAdjacentRaw && not onBlank       canvasLength = if onBlank then rheight else rheight - 2-      supHeight = maximum $ 0 : map (\(PointUI _ y, _) -> y) ov+      supHeight = maxYofOverlay ov       trimmedY = canvasLength - 1       -- Sadly, this does not trim the other, concurrent, overlays that may       -- obscure the last line and so contend with the "trimmed" message.@@ -122,8 +134,13 @@            else let (PointUI xLast yLast, _) =                       minimumBy (comparing $ \(PointUI x _, _) -> x) supHs                 in [(PointUI xLast (yLast + 1), emptyAttrLine)]-      ovTop = IM.elems $ IM.fromListWith (++)-              $ map (\pal@(PointUI _ y, _) -> (y, [pal]))+      -- This is crude, because an al at lower x may be longer, but KISS.+      -- This also gives a solid rule which al overwrite others+      -- when merging overlays, independent of the order of merging+      -- (except for duplicate x, for which initial order is retained).+      -- The order functions is cheap, we use @sortBy@, not @sortOn@.+      ovTop = groupBy ((==) `on` \(PointUI _ y, _) -> y)+              $ sortBy (comparing $ \(PointUI x y, _) -> (y, x))               $ if supHeight >= canvasLength                 then ovTopFiltered ++ [trimmedAlert]                 else ov ++ extraLine@@ -131,23 +148,23 @@       -- on there being no gaps and a natural order.       -- Probably also gives messy results when X offsets are not all the same.       -- Below we at least mitigate the case of multiple lines per row.-      f lenPrev lenNext lal =-        -- This is crude, because an al at lower x may be longer, but KISS.-        -- @sortOn@ is significantly faster than @SortBy@ here,-        -- though it takes more memory. The below is an explicit compromise.-        let xlal = map (\pll@(PointUI x _, _) -> (x, pll)) lal-        in case sortBy (comparing fst) xlal of-          [] -> error "empty list of overlay lines at the given row"-          minAl : rest ->-            g lenPrev lenNext fillLen minAl-            : map (g 0 0 0) rest-      g lenPrev lenNext fillL (xstartRaw, (p, layerLine)) =+      f _ _ [] = error "empty list of overlay lines at the given row"+      f (yPrev, lenPrev) (yNext, lenNext) (minAl@(PointUI _ yCur, _) : rest) =+        g (if yPrev == yCur - 1 then lenPrev else 0)+          (if yNext == yCur + 1 then lenNext else 0)+          fillLen+          minAl+        : map (g 0 0 0) rest+      g lenPrev lenNext fillL (p@(PointUI xstartRaw _), layerLine) =         let xstart = if halveXstart then xstartRaw `div` 2 else xstartRaw             -- TODO: lenPrev and lenNext is from the same kind of font;             -- if fonts are mixed, too few spaces are added.             -- We'd need to keep a global store of line lengths             -- for every position on the screen, filled first going             -- over all texts and only afterwards texts rendered.+            -- And prop font measure would still make this imprecise.+            -- TODO: rewrite ovBackdrop according to this idea,+            -- but then process square font only mode with the same mechanism.             maxLen = if wipeAdjacent then max lenPrev lenNext else 0             fillFromStart = max fillL (1 + maxLen) - xstart             available = width - xstart@@ -155,7 +172,10 @@       rightExtentOfLine (PointUI xstartRaw _, al) =         let xstart = if halveXstart then xstartRaw `div` 2 else xstartRaw         in min (width - 1) (xstart + length (attrLine al))-      lens = map (maximum . map rightExtentOfLine) ovTop+      yAndLen [] = (-99, 0)+      yAndLen als@((PointUI _ y, _) : _) =+        (y, maximum $ map rightExtentOfLine als)+      lens = map yAndLen ovTop       f2 = map g2       g2 (p@(PointUI xstartRaw _), layerLine) =         let xstart = if halveXstart then xstartRaw `div` 2 else xstartRaw@@ -163,7 +183,7 @@         in (p, truncateAttrLine False available 0 layerLine)   in concat $ if onBlank               then map f2 ovTop-              else zipWith3 f (0 : lens) (drop 1 lens ++ [0]) ovTop+              else zipWith3 f ((-9, 0) : lens) (drop 1 lens ++ [(999, 0)]) ovTop  -- | Add a space at the message end, for display overlayed over the level map. -- Also trim (do not wrap!) too long lines. Also add many spaces when under
engine-src/Game/LambdaHack/Client/UI/FrameM.hs view
@@ -1,10 +1,10 @@ -- | A set of Frame monad operations. module Game.LambdaHack.Client.UI.FrameM-  ( pushFrame, promptGetKey, addToMacro, dropEmptyMacroFrames, lastMacroFrame-  , stopPlayBack, animate, fadeOutOrIn+  ( drawOverlay, promptGetKey, addToMacro, dropEmptyMacroFrames+  , lastMacroFrame, stopPlayBack, renderAnimFrames, animate #ifdef EXPOSE_INTERNAL     -- * Internal operations-  , drawOverlay, oneLineBasicFrame, renderAnimFrames, resetPlayBack+  , resetPlayBack, restoreLeaderFromRun, basicFrameForAnimation #endif   ) where @@ -25,7 +25,6 @@ import           Game.LambdaHack.Client.UI.ContentClientUI import           Game.LambdaHack.Client.UI.DrawM import           Game.LambdaHack.Client.UI.Frame-import qualified Game.LambdaHack.Client.UI.Frontend as Frontend import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd import qualified Game.LambdaHack.Client.UI.Key as K import           Game.LambdaHack.Client.UI.MonadClientUI@@ -56,26 +55,20 @@                   return (m, FrameForall $ \_v -> return ())                 else drawHudFrame dm lid   FontSetup{..} <- getFontSetup-  soptions <- getsClient soptions-  let isTeletype = Frontend.frontendName soptions == "teletype"-      propWidth = if isMonoFont propFont then 2 * rwidth else 4 * rwidth-      ovProp | not $ isSquareFont propFont+  let propWidth = if isMonoFont propFont then 2 * rwidth else 4 * rwidth+      ovProp | not (isSquareFont propFont)              = truncateOverlay False propWidth rheight False 0 onBlank                $ EM.findWithDefault [] propFont ovs-             | isTeletype  -- hack for debug output-             = map (second attrLine) $ concat $ EM.elems ovs              | otherwise = []-      ovMono = if not $ isSquareFont monoFont+      ovMono = if not (isSquareFont monoFont)                then truncateOverlay False (2 * rwidth) rheight False 0 onBlank                     $ EM.findWithDefault [] monoFont ovs                else []-      ovOther | not $ isSquareFont propFont-              = truncateOverlay True rwidth rheight True 0 onBlank-                $ EM.findWithDefault [] squareFont ovs-                  -- no backdrop for square font, so @wipeAdjacent@-                  -- needs to be @True@ or the extra blank line starts too late-              | isTeletype  -- hack for debug output-              = []+      ovSquare | not (isSquareFont propFont)+               = truncateOverlay False (2 * rwidth) rheight False 0 onBlank+                 $ EM.findWithDefault [] squareFont ovs+              | otherwise = []+      ovOther | not (isSquareFont propFont) = []               | otherwise               = truncateOverlay True rwidth rheight True 20 onBlank                 $ concat $ EM.elems ovs@@ -89,11 +82,15 @@                  monoOutline =                    truncateOverlay False (2 * rwidth) rheight True 0 onBlank                    $ EM.findWithDefault [] monoFont ovs+                 squareOutline =+                   truncateOverlay False (2 * rwidth) rheight True 0 onBlank+                   $ EM.findWithDefault [] squareFont ovs                  g x al Nothing = Just (x, x + length al - 1)                  g x al (Just (xmin, xmax)) =                    Just (min xmin x, max xmax (x + length al - 1))                  f em (PointUI x y, al) = EM.alter (g x al) y em-                 extentMap = foldl' f EM.empty $ propOutline ++ monoOutline+                 extentMap = foldl' f EM.empty+                             $ propOutline ++ monoOutline ++ squareOutline                  listBackdrop (y, (xmin, xmax)) =                    ( PointUI (2 * (xmin `div` 2)) y                    , blankAttrString@@ -103,44 +100,28 @@         else []       overlayedFrame = overlayFrame rwidth ovOther                        $ overlayFrame rwidth ovBackdrop basicFrame-  return (overlayedFrame, (ovProp, ovMono))--oneLineBasicFrame :: MonadClientUI m => LevelId -> DisplayFont -> m PreFrame3-oneLineBasicFrame arena font = do-  report <- getReportUI False-  let par1 = firstParagraph $ foldr (<+:>) [] $ renderReport True report-      truncRep = EM.fromList [(font, [(PointUI 0 0, par1)])]-  drawOverlay ColorFull False truncRep arena---- | Push the frame depicting the current level to the frame queue.--- Only one line of the report is shown, as in animations,--- because it may not be our turn, so we can't clear the message--- to see what is underneath.-pushFrame :: MonadClientUI m => Bool -> m ()-pushFrame delay = do-  -- The delay before reaction to keypress was too long in case of many-  -- projectiles flying and ending flight, so frames need to be skipped.-  keyPressed <- anyKeyPressed-  unless keyPressed $ do-    lidV <- viewedLevelUI-    FontSetup{propFont} <- getFontSetup-    frame <- oneLineBasicFrame lidV propFont-    -- Pad with delay before and after to let player see, e.g., door being-    -- opened a few ticks after it came into vision, the same turn.-    displayFrames lidV $-      if delay then [Nothing, Just frame, Nothing] else [Just frame]+  return (overlayedFrame, (ovProp, ovSquare, ovMono)) -promptGetKey :: (MonadClient m, MonadClientUI m)+promptGetKey :: MonadClientUI m              => ColorMode -> FontOverlayMap -> Bool -> [K.KM]              -> m K.KM promptGetKey dm ovs onBlank frontKeyKeys = do   lidV <- viewedLevelUI   report <- getsSession $ newReport . shistory-  let interrupted = anyInReport disturbsResting report+  sreqQueried <- getsSession sreqQueried+  let interrupted =+        -- If server is not querying for request, then the key is needed due to+        -- a special event, not ordinary querying the player for command,+        -- so interrupt.+        not sreqQueried+        -- Any alarming message interupts macros.+        || anyInReport disturbsResting report   macroFrame <- getsSession smacroFrame   km <- case keyPending macroFrame of-    KeyMacro (km : kms) | (null frontKeyKeys || km `elem` frontKeyKeys)-                          && not interrupted -> do+    KeyMacro (km : kms) | not interrupted+                          -- A faulty key in a macro is a good reason+                          -- to interrupt it, as well.+                          && (null frontKeyKeys || km `elem` frontKeyKeys) -> do       -- No need to display the frame, because a frame was displayed       -- when the player chose to play a macro and each turn or more often       -- a frame is displayed elsewhere.@@ -150,30 +131,33 @@         sess {smacroFrame = (smacroFrame sess) {keyPending = KeyMacro kms}}       msgAdd MsgMacroOperation $ "Voicing '" <> tshow km <> "'."       return km-    KeyMacro (_ : _) -> do+    KeyMacro kms -> do+      if null kms then do+        -- There was no macro. Not important if there was a reason+        -- for interrupt or not.+        when (dm /= ColorFull) $ do+          -- This marks a special event, regardless of @sreqQueried@.+          side <- getsClient sside+          fact <- getsState $ (EM.! side) . sfactionD+          unless (isAIFact fact) -- don't forget special autoplay keypresses+            -- Forget the furious keypresses just before a special event.+            resetPressedKeys+        -- Running, if any, must have ended naturally, because no macro.+        -- Therefore no need to restore leader back to initial run leader,+        -- but running itself is cancelled below.+      else do+        -- The macro was not empty, but not played, so it must have been+        -- interrupted, so we can't continue playback, so wipe out the macro.+        resetPlayBack+        -- This might have been an unexpected end of a run, too.+        restoreLeaderFromRun+        -- Macro was killed, so emergency, so reset input, too.+        resetPressedKeys       frontKeyFrame <- drawOverlay dm onBlank ovs lidV-      -- We can't continue playback, so wipe out macros, etc.-      resetPlayBack-      resetPressedKeys       recordHistory       modifySession $ \sess ->-        sess { sdisplayNeeded = False-             , sturnDisplayed = True }-      connFrontendFrontKey frontKeyKeys frontKeyFrame-    KeyMacro [] -> do-      frontKeyFrame <- drawOverlay dm onBlank ovs lidV-      when (dm /= ColorFull) $ do-        side <- getsClient sside-        fact <- getsState $ (EM.! side) . sfactionD-        unless (isAIFact fact) -- don't forget special autoplay keypresses-          -- Forget the furious keypresses just before a special event.-          resetPressedKeys-      recordHistory-      -- If we ask for a key, then we don't want to run any more-      -- and we want to avoid changing leader back to initial run leader-      -- at the nearest @stopPlayBack@, etc.-      modifySession $ \sess ->         sess { srunning = Nothing+             , sxhairGoTo = Nothing              , sdisplayNeeded = False              , sturnDisplayed = True }       connFrontendFrontKey frontKeyKeys frontKeyFrame@@ -181,9 +165,12 @@   -- to also capture choice of items from menus, etc.   -- Notice that keys coming from macros (from content, in-game, config)   -- are recorded as well and this is well defined and essential.-  CCUI{coinput=InputContent{bcmdMap}} <- getsSession sccui-  modifySession $ \sess ->-    sess {smacroFrame = addToMacro bcmdMap km $ smacroFrame sess}+  --+  -- Only keys pressed when player is queried for a command are recorded.+  when sreqQueried $ do+    CCUI{coinput=InputContent{bcmdMap}} <- getsSession sccui+    modifySession $ \sess ->+      sess {smacroFrame = addToMacro bcmdMap km $ smacroFrame sess}   return km  addToMacro :: M.Map K.KM HumanCmd.CmdTriple -> K.KM -> KeyMacroFrame@@ -209,18 +196,20 @@ lastMacroFrame mf [] = mf lastMacroFrame _ (mf : mfs) = lastMacroFrame mf mfs -stopPlayBack :: (MonadClient m, MonadClientUI m) => m ()+stopPlayBack :: MonadClientUI m => m () stopPlayBack = msgAdd MsgStopPlayback "!" +-- | We wipe any actions in progress, but keep the data needed to repeat+-- the last global macros and the last command. resetPlayBack :: MonadClientUI m => m ()-resetPlayBack = do-  -- We wipe any actions in progress, but keep the data needed to repeat-  -- the last global macros and the last command.+resetPlayBack =   modifySession $ \sess ->     let lastFrame = lastMacroFrame (smacroFrame sess) (smacroStack sess)     in sess { smacroFrame = lastFrame {keyPending = mempty}-            , smacroStack = []-            , sxhairGoTo = Nothing }+            , smacroStack = [] }++restoreLeaderFromRun :: MonadClientUI m => m ()+restoreLeaderFromRun = do   srunning <- getsSession srunning   case srunning of     Nothing -> return ()@@ -234,25 +223,40 @@       memA <- getsState $ memActor runLeader arena       when (memA && not (noRunWithMulti fact)) $         updateClientLeader runLeader-      modifySession (\sess -> sess {srunning = Nothing}) +-- This is not our turn, so we can't obstruct screen with messages+-- and message reformatting causes distraction, so there's no point+-- trying to squeeze the report into the single available line,+-- except when it's not our turn permanently, because AI runs UI.+basicFrameForAnimation :: MonadClientUI m+                        => LevelId -> Maybe Bool -> m PreFrame3+basicFrameForAnimation arena forceReport = do+  FontSetup{propFont} <- getFontSetup+  sbenchMessages <- getsClient $ sbenchMessages . soptions+  side <- getsClient sside+  fact <- getsState $ (EM.! side) . sfactionD+  report <- getReportUI False+  let par1 = firstParagraph $ foldr (<+:>) [] $ renderReport True report+      underAI = isAIFact fact+      -- If messages are benchmarked, they can't be displayed under AI,+      -- because this is not realistic when player is in control.+      truncRep | not sbenchMessages && fromMaybe underAI forceReport =+                   EM.fromList [(propFont, [(PointUI 0 0, par1)])]+               | otherwise = EM.empty+  drawOverlay ColorFull False truncRep arena+ -- | Render animations on top of the current screen frame. renderAnimFrames :: MonadClientUI m-                 => Bool -> LevelId -> Animation -> m PreFrames3-renderAnimFrames onBlank arena anim = do+                 => LevelId -> Animation -> Maybe Bool -> m PreFrames3+renderAnimFrames arena anim forceReport = do   CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui   snoAnim <- getsClient $ snoAnim . soptions-  FontSetup{..} <- getFontSetup-  -- This hack is needed so that the prop part of the overlay does not-  -- overwrite the fadeout animation.-  let ovFont = if not onBlank || fromMaybe False snoAnim-               then propFont-               else squareFont-  basicFrame <- oneLineBasicFrame arena ovFont-  return $! if fromMaybe False snoAnim-            then [Just basicFrame]-            else map (fmap (\fr -> (fr, snd basicFrame)))-                 $ renderAnim rwidth (fst basicFrame) anim+  basicFrame <- basicFrameForAnimation arena forceReport+  smuteMessages <- getsSession smuteMessages+  return $! if | smuteMessages -> []+               | fromMaybe False snoAnim -> [Just basicFrame]+               | otherwise -> map (fmap (\fr -> (fr, snd basicFrame)))+                              $ renderAnim rwidth (fst basicFrame) anim  -- | Render and display animations on top of the current screen frame. animate :: MonadClientUI m => LevelId -> Animation -> m ()@@ -261,13 +265,5 @@   -- projectiles hitting actors, so frames need to be skipped.   keyPressed <- anyKeyPressed   unless keyPressed $ do-    frames <- renderAnimFrames False arena anim+    frames <- renderAnimFrames arena anim Nothing     displayFrames arena frames--fadeOutOrIn :: MonadClientUI m => Bool -> m ()-fadeOutOrIn out = do-  arena <- getArenaUI-  CCUI{coscreen} <- getsSession sccui-  animMap <- rndToActionUI $ fadeout coscreen out 2-  animFrs <- renderAnimFrames True arena animMap-  displayFrames arena (tail animFrs)  -- no basic frame between fadeout and in
engine-src/Game/LambdaHack/Client/UI/Frontend.hs view
@@ -21,7 +21,6 @@ import           Control.Concurrent.Async import qualified Control.Concurrent.STM as STM import           Control.Monad.ST.Strict-import           Data.IORef import           Data.Kind (Type) import qualified Data.Text.IO as T import qualified Data.Vector.Generic as G@@ -31,16 +30,21 @@  import           Game.LambdaHack.Client.UI.Content.Screen import           Game.LambdaHack.Client.UI.Frame-import qualified Game.LambdaHack.Client.UI.Frontend.Chosen as Chosen import           Game.LambdaHack.Client.UI.Frontend.Common import qualified Game.LambdaHack.Client.UI.Frontend.Teletype as Teletype import           Game.LambdaHack.Client.UI.Key (KMP (..)) import qualified Game.LambdaHack.Client.UI.Key as K-import           Game.LambdaHack.Client.UI.PointUI import           Game.LambdaHack.Common.ClientOptions import qualified Game.LambdaHack.Common.PointArray as PointArray import qualified Game.LambdaHack.Definition.Color as Color +#ifdef USE_BROWSER+import qualified Game.LambdaHack.Client.UI.Frontend.Dom as Chosen+#else+import qualified Game.LambdaHack.Client.UI.Frontend.ANSI as ANSI+import qualified Game.LambdaHack.Client.UI.Frontend.Sdl as Chosen+#endif+ -- | The instructions sent by clients to the raw frontend, indexed -- by the returned value. data FrontReq :: Type -> Type where@@ -52,14 +56,10 @@   FrontKey :: [K.KM] -> Frame -> FrontReq KMP   -- | Tell if a keypress is pending.   FrontPressed :: FrontReq Bool-  -- | Discard a key in the queue, if any.+  -- | Discard a single key in the queue, if any.   FrontDiscardKey :: FrontReq ()   -- | Discard all keys in the queue.   FrontResetKeys :: FrontReq ()-  -- | Add a key to the queue.-  FrontAdd :: KMP -> FrontReq ()-  -- | Set in the frontend that it should auto-answer prompts.-  FrontAutoYes :: Bool -> FrontReq ()   -- | Shut the frontend down.   FrontShutdown :: FrontReq ()   -- | Take screenshot.@@ -72,8 +72,7 @@ -- | Machinery allocated for an individual frontend at its startup, -- unchanged for its lifetime. data FrontSetup = FrontSetup-  { fautoYesRef   :: IORef Bool-  , fasyncTimeout :: Async ()+  { fasyncTimeout :: Async ()   , fdelay        :: MVar Int   } @@ -85,6 +84,9 @@ #ifndef REMOVE_TELETYPE               | sfrontendTeletype soptions = Teletype.startup coscreen #endif+#ifndef USE_BROWSER+              | sfrontendANSI soptions = ANSI.startup coscreen+#endif               | otherwise = Chosen.startup coscreen soptions       maxFps = fromMaybe defaultMaxFps $ smaxFps soptions       delta = max 1 $ round $ intToDouble microInSec / max 0.000001 maxFps@@ -93,7 +95,6 @@     T.hPutStr stdout "Frontend startup up.\n"       -- hPutStrLn not atomic enough     hFlush stdout-  fautoYesRef <- newIORef $ not $ sdisableAutoYes soptions   fdelay <- newMVar 0   fasyncTimeout <- async $ frameTimeoutThread delta fdelay rf   -- Warning: not linking @fasyncTimeout@, so it'd better not crash.@@ -105,17 +106,12 @@ -- if the list is empty). Repeat if an unexpected key received. getKey :: FrontSetup -> RawFrontend -> [K.KM] -> Frame -> IO KMP getKey fs rf@RawFrontend{fchanKey} keys frame = do-  autoYes <- readIORef $ fautoYesRef fs-  if autoYes && (null keys || K.spaceKM `elem` keys) then do-    display rf frame-    return $! KMP {kmpKeyMod = K.spaceKM, kmpPointer = PointUI 0 0}-  else do-    -- Wait until timeout is up, not to skip the last frame of animation.-    display rf frame-    kmp <- STM.atomically $ STM.readTQueue fchanKey-    if null keys || kmpKeyMod kmp `elem` keys-    then return kmp-    else getKey fs rf keys frame+  -- Wait until timeout is up, not to skip the last frame of animation.+  display rf frame+  kmp <- STM.atomically $ STM.readTQueue fchanKey+  if null keys || kmpKeyMod kmp `elem` keys+  then return kmp+  else getKey fs rf keys frame  -- Read UI requests from the client and send them to the frontend, fchanFrontend :: FrontSetup -> RawFrontend -> ChanFrontend@@ -131,8 +127,6 @@     FrontDiscardKey ->       void $ STM.atomically $ STM.tryReadTQueue (fchanKey rf)     FrontResetKeys -> resetChanKey (fchanKey rf)-    FrontAdd kmp -> STM.atomically $ STM.writeTQueue (fchanKey rf) kmp-    FrontAutoYes b -> writeIORef fautoYesRef b     FrontShutdown -> do       cancel fasyncTimeout       -- In case the last frame display is pending:@@ -142,7 +136,7 @@  display :: RawFrontend -> Frame -> IO () display rf@RawFrontend{fshowNow, fcoscreen=ScreenContent{rwidth, rheight}}-        ((m, upd), (ovProp, ovMono)) = do+        ((m, upd), (ovProp, ovSquare, ovMono)) = do   let new :: forall s. ST s (G.Mutable U.Vector s Word32)       new = do         v <- unFrameBase m@@ -150,7 +144,7 @@         return v       singleArray = PointArray.Array rwidth rheight (U.create new)   putMVar fshowNow () -- 1. wait for permission to display; 3. ack-  fdisplay rf $ SingleFrame singleArray ovProp ovMono+  fdisplay rf $ SingleFrame singleArray ovProp ovSquare ovMono  defaultMaxFps :: Double defaultMaxFps = 24@@ -194,6 +188,9 @@ #ifndef REMOVE_TELETYPE      | sfrontendTeletype soptions -> Teletype.frontendName #endif+#ifndef USE_BROWSER+     | sfrontendANSI soptions -> ANSI.frontendName+#endif      | otherwise -> Chosen.frontendName  lazyStartup :: ScreenContent -> IO RawFrontend@@ -203,9 +200,13 @@ nullStartup coscreen = createRawFrontend coscreen seqFrame (return ())  seqFrame :: SingleFrame -> IO ()-seqFrame SingleFrame{singleArray} =+seqFrame SingleFrame{..} =   let seqAttr () attr = Color.colorToRGB (Color.fgFromW32 attr)                         `seq` Color.bgFromW32 attr                         `seq` Color.charFromW32 attr == ' '                         `seq` ()-  in return $! PointArray.foldlA' seqAttr () singleArray+      !_Force1 = PointArray.foldlA' seqAttr () singleArray+      !_Force2 = length singlePropOverlay+      !_Force3 = length singleSquareOverlay+      !_Force4 = length singleMonoOverlay+  in return ()
+ engine-src/Game/LambdaHack/Client/UI/Frontend/ANSI.hs view
@@ -0,0 +1,315 @@+-- | Text frontend based on ANSI (via ansi-terminal).+module Game.LambdaHack.Client.UI.Frontend.ANSI+  ( startup, frontendName+  ) where++import Prelude ()++import Game.LambdaHack.Core.Prelude++import           Control.Concurrent.Async+import           Data.Char (chr, ord)+import qualified Data.Text as T+import qualified System.Console.ANSI as ANSI+import qualified System.IO as SIO++import           Game.LambdaHack.Client.UI.Content.Screen+import           Game.LambdaHack.Client.UI.Frame+import           Game.LambdaHack.Client.UI.Frontend.Common+import qualified Game.LambdaHack.Client.UI.Key as K+import           Game.LambdaHack.Client.UI.PointUI+import           Game.LambdaHack.Common.Point+import qualified Game.LambdaHack.Common.PointArray as PointArray+import           Game.LambdaHack.Content.TileKind (floorSymbol)+import qualified Game.LambdaHack.Definition.Color as Color++-- No session data maintained by this frontend++-- | The name of the frontend.+frontendName :: String+frontendName = "ANSI"++-- | Starts the main program loop using the frontend input and output.+startup :: ScreenContent -> IO RawFrontend+startup coscreen@ScreenContent{rwidth, rheight} = do+  myx <- ANSI.getTerminalSize+  case myx of+    Just (y, x) | x < rwidth || y < rheight ->+      error $ T.unpack+            $ "The terminal is too small. It should have"+              <+> tshow rwidth+              <+> "columns and"+              <+> tshow rheight+              <+> "rows, but is has"+              <+> tshow x+              <+> "columns and"+              <+> tshow y+              <+> "rows. Resize it and run the program again."+    _ -> do+      rf <- createRawFrontend coscreen (display coscreen) (shutdown coscreen)+      let storeKeys :: IO ()+          storeKeys = do+            c <- SIO.getChar  -- blocks here, so no polling+            s <- do+              if c == '\ESC' then do+                ready <- SIO.hReady SIO.stdin+                if ready then do+                  c2 <- SIO.getChar+                  case c2 of+                    '\ESC' -> return [c]+                    '[' -> keycodeInput [c, c2]+                    'O' -> keycodeInput [c, c2]+                    _ -> return [c, c2]  -- Alt modifier+                else return [c]+              else return [c]+            let K.KM{..} = keyTranslate s+            saveKMP rf modifier key (PointUI 0 0)+            storeKeys+          keycodeInput :: String -> IO String+          keycodeInput inputSoFar = do+            ready <- SIO.hReady SIO.stdin+            if ready then do+              c <- SIO.getChar+              if ord '@' <= ord c && ord c <= ord '~'  -- terminator+              then return $ inputSoFar ++ [c]+              else keycodeInput  $ inputSoFar ++ [c]+            else return inputSoFar+      SIO.hSetBuffering SIO.stdin SIO.NoBuffering+      SIO.hSetBuffering SIO.stderr $ SIO.BlockBuffering $+        Just $ 2 * rwidth * rheight+      void $ async storeKeys+      return $! rf++-- This is contrived, because we don't want to depend on libraries+-- that read and interpret terminfo or similar on different architectures.+-- The "works" comments are mostly about Gnome terminal.+-- On the Gnome terminal, fo the keys mention on game help screen,+-- the following don't work: C-TAB, C-S-TAB, C-R, C-?. C-/, C-{, C-}, C-q,+-- C-S, C-P, C-keypad. This is acceptable. No worth adding functionality+-- for decoding modifiers that would, with much luck, enable C-keypad,+-- but no other broken keys. Unless more is broken on other terminals.+-- On rxvt, sadly, KP_5 is a dead key.+keyTranslate :: String -> K.KM+keyTranslate e = (\(key, modifier) -> K.KM modifier key) $+  case e of+    "\ESC" -> (K.Esc, K.NoModifier)  -- equals @^[@+    '\ESC' : '[' : rest -> keycodeTranslate rest+    '\ESC' : 'O' : rest -> ocodeTranslate rest+    ['\ESC', c] -> (K.Char c, K.Alt)+    "\b" -> (K.BackSpace, K.NoModifier)  -- same as "\BS" and "\^H" but fails+    "\DEL" -> (K.BackSpace, K.NoModifier)  -- works; go figure+    "\n" -> (K.Return, K.NoModifier)+    "\r" -> (K.Return, K.NoModifier)+    " "  -> (K.Space, K.NoModifier)+    "\t" -> (K.Tab, K.NoModifier)  -- apparently equals @\^I@ and @\HT@+    [c] | ord '\^A' <= ord c && ord c <= ord '\^Z' ->+          -- Alas, only lower-case letters.+          (K.Char $ chr $ ord c - ord '\^A' + ord 'a', K.Control)+        | -- On (some) terminal emulators Shift-keypad direction produces+          -- the same code as number keys. A sensible workaround for that+          -- is using Control for running, but it's not clear how portable+          -- this is, so we do not rely on this exclusively. Since movement+          -- keys are more important than leader picking, we are disabling+          -- the latter and interpreting the keypad numbers as movement.+          --+          -- BTW, S-KP_5 and C-KP_5 are probably still not correctly handled+          -- on some terminals, so this may be the biggest portability problem.+          c `elem` ['1'..'9'] -> (K.KP c, K.NoModifier)+        | otherwise           -> (K.Char c, K.NoModifier)+    _ -> (K.Unknown e, K.NoModifier)++-- From https://en.wikipedia.org/wiki/ANSI_escape_code#Terminal_input_sequences+keycodeTranslate :: String -> (K.Key, K.Modifier)+keycodeTranslate e =+  case e of+    "1~" -> (K.Home, K.NoModifier)+    "2~" -> (K.Insert, K.NoModifier)+    "3~" -> (K.Delete, K.NoModifier)+    "4~" -> (K.End , K.NoModifier)+    "5~" -> (K.PgUp, K.NoModifier)+    "6~" -> (K.PgDn, K.NoModifier)+    "7~" -> (K.Home, K.NoModifier)+    "8~" -> (K.End, K.NoModifier)+    "9~" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)+    "10~" -> (K.Fun 0, K.NoModifier)+    "11~" -> (K.Fun 1, K.NoModifier)+    "12~" -> (K.Fun 2, K.NoModifier)+    "13~" -> (K.Fun 3, K.NoModifier)+    "14~" -> (K.Fun 4, K.NoModifier)+    "15~" -> (K.Fun 5, K.NoModifier)+    "17~" -> (K.Fun 6, K.NoModifier)+    "18~" -> (K.Fun 7, K.NoModifier)+    "19~" -> (K.Fun 8, K.NoModifier)+    "20~" -> (K.Fun 9, K.NoModifier)+    "21~" -> (K.Fun 10, K.NoModifier)+    "22~" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)+    "23~" -> (K.Fun 11, K.NoModifier)+    "24~" -> (K.Fun 12, K.NoModifier)+    "25~" -> (K.Fun 13, K.NoModifier)+    "26~" -> (K.Fun 14, K.NoModifier)+    "27~" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)+    "28~" -> (K.Fun 15, K.NoModifier)+    "29~" -> (K.Fun 16, K.NoModifier)+    "30~" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)+    "32~" -> (K.Fun 18 , K.NoModifier)+    "33~" -> (K.Fun 19 , K.NoModifier)+    "34~" -> (K.Fun 20 , K.NoModifier)+    "35~" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)++    "A" -> (K.Up, K.NoModifier)+    "B" -> (K.Down, K.NoModifier)+    "C" -> (K.Right, K.NoModifier)+    "D" -> (K.Left, K.NoModifier)+    "E" -> (K.Begin, K.NoModifier)+    "F" -> (K.End, K.NoModifier)+    "G" -> (K.KP '5', K.NoModifier)+    "H" -> (K.Home, K.NoModifier)+    "I" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)+    "J" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)+    "K" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)+    "L" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)+    "M" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)+    "N" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)+    "O" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)+    "1P" -> (K.Fun 1, K.NoModifier)+    "1Q" -> (K.Fun 2, K.NoModifier)+    "1R" -> (K.Fun 3, K.NoModifier)+    "1S" -> (K.Fun 4, K.NoModifier)+    "T" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)+    "U" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)+    "V" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)+    "W" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)+    "X" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)+    "Y" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)+    "Z" -> (K.BackTab, K.NoModifier)++--    "r" -> (K.Begin, K.NoModifier)+--    "u" -> (K.Begin, K.NoModifier)++    _ -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier)++-- From guesswork, cargo-culting and @sed -n l@.+ocodeTranslate :: String -> (K.Key, K.Modifier)+ocodeTranslate e =+  case e of+    "P" -> (K.Fun 1, K.NoModifier)+    "Q" -> (K.Fun 2, K.NoModifier)+    "R" -> (K.Fun 3, K.NoModifier)+    "S" -> (K.Fun 4, K.NoModifier)++    "p" -> (K.KP '0', K.Shift)+    "q" -> (K.KP '1', K.Shift)+    "r" -> (K.KP '2', K.Shift)+    "s" -> (K.KP '3', K.Shift)+    "t" -> (K.KP '4', K.Shift)+    "u" -> (K.KP '5', K.Shift)+    "v" -> (K.KP '6', K.Shift)+    "w" -> (K.KP '7', K.Shift)+    "x" -> (K.KP '8', K.Shift)+    "y" -> (K.KP '9', K.Shift)++    _ -> (K.Unknown $ "\\ESCO" ++ e, K.NoModifier)++shutdown :: ScreenContent -> IO ()+shutdown ScreenContent{rheight} = do+  -- The lowest position guaranteed to exist.+  ANSI.hSetCursorPosition SIO.stderr (rheight - 1) 0+  SIO.hFlush SIO.stdout >> SIO.hFlush SIO.stderr++-- | Output to the screen via the frontend.+display :: ScreenContent+        -> SingleFrame+        -> IO ()+display ScreenContent{rwidth} SingleFrame{singleArray} = do+  ANSI.hHideCursor SIO.stderr+  let cutInChunks [] = []+      cutInChunks l = let (ch, r) = splitAt rwidth l+                      in ch : cutInChunks r+      f (!y, chunk) = do+        ANSI.hSetCursorPosition SIO.stderr y 0+        SIO.hPutStr SIO.stderr $ g chunk+      g = concatMap h+      -- Not emitting ANSI if the previous character had the same fg and bg+      -- gains little in terms of maximal lag, due to checkerboard levels/rooms+      -- (even though it triples FPS in normal rooms; but comparing with+      -- previous frame gains even more in normal cases and copes well+      -- with checkerboard; both not worth the effort for this frontend).+      h !w =+        let acChar = squashChar $ Color.charFromW32 w+            (fg, bg) = setAttr $ Color.attrFromW32 w+        in ANSI.setSGRCode [ uncurry (ANSI.SetColor ANSI.Foreground)+                             $ colorTranslate fg+                           , uncurry (ANSI.SetColor ANSI.Background)+                             $ colorTranslate bg ]+           ++ [acChar]+{-+This is dubious, because I can't force bright background colour with that,+only bright foregrounds. And I have at least one bright backround: bright black.+        -- A hack to get bright colors via the bold attribute.+        -- Depending on terminal settings this is needed or not+        -- and the characters really get bold or not.+        -- HSCurses does this by default, in Vty you have to request the hack,+        -- with ANSI we probably need it as well.+        ANSI.hSetSGR SIO.stderr [ANSI.SetConsoleIntensity+                                 $ if Color.isBright fg+                                   then ANSI.BoldIntensity+                                   else ANSI.NormalIntensity]+-}+  mapM_ f $ zip [0 ..] $ cutInChunks $ PointArray.toListA singleArray+  let Point{..} = PointArray.maxIndexByA (comparing Color.bgFromW32) singleArray+  ANSI.hSetCursorPosition SIO.stderr py px+  ANSI.hShowCursor SIO.stderr+  -- Do not trash people's terminals when interrupted:+  ANSI.hSetSGR SIO.stderr [uncurry (ANSI.SetColor ANSI.Foreground)+                           $ colorTranslate Color.White]+  ANSI.hSetSGR SIO.stderr [uncurry (ANSI.SetColor ANSI.Background)+                           $ colorTranslate Color.Black]+  SIO.hFlush SIO.stderr++squashChar :: Char -> Char+squashChar c = if c == floorSymbol then '.' else c++setAttr :: Color.Attr -> (Color.Color, Color.Color)+setAttr Color.Attr{..} =+  let (fg1, bg1) = case bg of+        Color.HighlightNone -> (fg, Color.Black)+        Color.HighlightWhite ->+          if fg /= Color.Magenta+          then (fg, Color.Magenta)+          else (fg, Color.BrBlack)+        Color.HighlightRed ->+          if fg /= Color.Red+          then (fg, Color.Red)+          else (fg, Color.defFG)+        Color.HighlightYellow -> (fg, Color.Black)  -- cursor used instead+        Color.HighlightYellowAim -> (Color.Black, Color.defFG)+        Color.HighlightRedAim ->+          if fg /= Color.Red+          then (fg, Color.Red)+          else (fg, Color.defFG)+        Color.HighlightNoneCursor -> (fg, Color.Black)+        _ -> if fg /= Color.highlightToColor bg+             then (fg, Color.highlightToColor bg)+             else (fg, if fg == Color.BrBlack+                       then Color.Black+                       else Color.BrBlack)+  in (fg1, bg1)++colorTranslate :: Color.Color -> (ANSI.ColorIntensity, ANSI.Color)+colorTranslate Color.Black     = (ANSI.Dull, ANSI.Black)+colorTranslate Color.Red       = (ANSI.Dull, ANSI.Red)+colorTranslate Color.Green     = (ANSI.Dull, ANSI.Green)+colorTranslate Color.Brown     = (ANSI.Dull, ANSI.Yellow)+colorTranslate Color.Blue      = (ANSI.Dull, ANSI.Blue)+colorTranslate Color.Magenta   = (ANSI.Dull, ANSI.Magenta)+colorTranslate Color.Cyan      = (ANSI.Dull, ANSI.Cyan)+colorTranslate Color.White     = (ANSI.Dull, ANSI.White)+colorTranslate Color.AltWhite  = (ANSI.Dull, ANSI.White)+colorTranslate Color.BrBlack   = (ANSI.Vivid, ANSI.Black)+colorTranslate Color.BrRed     = (ANSI.Vivid, ANSI.Red)+colorTranslate Color.BrGreen   = (ANSI.Vivid, ANSI.Green)+colorTranslate Color.BrYellow  = (ANSI.Vivid, ANSI.Yellow)+colorTranslate Color.BrBlue    = (ANSI.Vivid, ANSI.Blue)+colorTranslate Color.BrMagenta = (ANSI.Vivid, ANSI.Magenta)+colorTranslate Color.BrCyan    = (ANSI.Vivid, ANSI.Cyan)+colorTranslate Color.BrWhite   = (ANSI.Vivid, ANSI.White)
− engine-src/Game/LambdaHack/Client/UI/Frontend/Chosen.hs
@@ -1,21 +0,0 @@--- | Re-export the operations of the chosen raw frontend--- (determined at compile time with cabal flags).-module Game.LambdaHack.Client.UI.Frontend.Chosen-  ( startup, frontendName-  ) where--import Prelude ()--#ifdef USE_CURSES-import Game.LambdaHack.Client.UI.Frontend.Curses-#elif USE_VTY-import Game.LambdaHack.Client.UI.Frontend.Vty-#elif USE_GTK-import Game.LambdaHack.Client.UI.Frontend.Gtk-#elif USE_SDL-import Game.LambdaHack.Client.UI.Frontend.Sdl-#elif USE_BROWSER-import Game.LambdaHack.Client.UI.Frontend.Dom-#else-import Game.LambdaHack.Client.UI.Frontend.Sdl-#endif
engine-src/Game/LambdaHack/Client/UI/Frontend/Common.hs view
@@ -42,7 +42,7 @@ -- https://github.com/AllureOfTheStars/Allure/issues/79 -- In case any other exotic architecture requires the main thread, -- we make the hack the default for all (on frontends that require a bound--- thread, e.g., SLD2 or GTK).+-- thread, e.g., SLD2). startupBound :: (MVar RawFrontend -> IO ()) -> IO RawFrontend startupBound k = do   rfMVar <- newEmptyMVar
− engine-src/Game/LambdaHack/Client/UI/Frontend/Curses.hs
@@ -1,188 +0,0 @@--- | Text frontend based on HSCurses. This frontend is not fully supported--- due to the limitations of the curses library (keys, colours, last character--- of the last line).-module Game.LambdaHack.Client.UI.Frontend.Curses-  ( startup, frontendName-  ) where--import Prelude ()--import Game.LambdaHack.Core.Prelude--import           Control.Concurrent.Async-import           Data.Char (chr, ord)-import qualified Data.Map.Strict as M-import qualified UI.HSCurses.Curses as C-import qualified UI.HSCurses.CursesHelper as C--import           Game.LambdaHack.Client.UI.Content.Screen-import           Game.LambdaHack.Client.UI.Frame-import           Game.LambdaHack.Client.UI.Frontend.Common-import qualified Game.LambdaHack.Client.UI.Key as K-import           Game.LambdaHack.Client.UI.PointUI-import           Game.LambdaHack.Common.ClientOptions-import qualified Game.LambdaHack.Common.PointArray as PointArray-import qualified Game.LambdaHack.Definition.Color as Color---- | Session data maintained by the frontend.-data FrontendSession = FrontendSession-  { swin    :: C.Window  -- ^ the window to draw to-  , sstyles :: M.Map (Color.Color, Color.Color) C.CursesStyle-      -- ^ map from fore/back colour pairs to defined curses styles-  }---- | The name of the frontend.-frontendName :: String-frontendName = "curses"---- | Starts the main program loop using the frontend input and output.-startup :: ScreenContent -> ClientOptions -> IO RawFrontend-startup coscreen _soptions = do-  C.start-  void $ C.cursSet C.CursorInvisible-  let s = [ ((fg, bg), C.Style (toFColor fg) (toBColor bg))-          | -- Almost no more color combinations possible: 15*4, 64 is max.-            fg <- Color.legalFgCol-          , bg <- [Color.Black, Color.Blue, Color.White, Color.BrBlack] ]-  nr <- C.colorPairs-  when (nr < length s) $-    C.end >> error ("terminal has too few color pairs" `showFailure` nr)-  let (ks, vs) = unzip s-  ws <- C.convertStyles vs-  let swin = C.stdScr-      sstyles = M.fromDistinctAscList (zip ks ws)-      sess = FrontendSession{..}-  rf <- createRawFrontend coscreen (display coscreen sess) shutdown-  let storeKeys :: IO ()-      storeKeys = do-        K.KM{..} <- keyTranslate <$> C.getKey C.refresh-        saveKMP rf modifier key (PointUI 0 0)-        storeKeys-  void $ async storeKeys-  return $! rf--shutdown :: IO ()-shutdown = C.end---- | Output to the screen via the frontend.-display :: ScreenContent-        -> FrontendSession-        -> SingleFrame-        -> IO ()-display coscreen FrontendSession{..} SingleFrame{singleArray} = do-  -- let defaultStyle = C.defaultCursesStyle-  -- Terminals with white background require this:-  let defaultStyle = sstyles M.! (Color.defFG, Color.Black)-  C.erase-  C.setStyle defaultStyle-  -- We need to remove the last character from the status line,-  -- because otherwise it would overflow a standard size xterm window,-  -- due to the curses historical limitations.-  let sf = chunk $ map Color.attrCharFromW32-                 $ PointArray.toListA singleArray-      level = init sf ++ [init $ last sf]-      nm = zip [0..] $ map (zip [0..]) level-      chunk [] = []-      chunk l = let (ch, r) = splitAt (rwidth coscreen) l-                in ch : chunk r-  sequence_ [ C.setStyle (M.findWithDefault defaultStyle acAttr2 sstyles)-              >> C.mvWAddStr swin y x [acChar]-            | (y, line) <- nm-            , (x, Color.AttrChar{acAttr=Color.Attr{..}, ..}) <- line-            , let acAttr2 = case bg of-                    Color.HighlightNone -> (fg, Color.Black)-                    Color.HighlightGreen -> (fg, Color.Black)  -- too few bgs-                    Color.HighlightBlue ->-                      if fg /= Color.Blue-                      then (fg, Color.Blue)-                      else (fg, Color.BrBlack)-                    Color.HighlightBrown -> (fg, Color.Black)-                    Color.HighlightCyan -> (fg, Color.Black)-                    Color.HighlightGrey ->-                      if fg /= Color.BrBlack-                      then (fg, Color.BrBlack)-                      else (fg, Color.defFG)-                    Color.HighlightWhite -> (fg, Color.Black)-                    Color.HighlightMagenta -> (fg, Color.Black)-                    Color.HighlightRed ->-                      if fg /= Color.Red-                      then (fg, Color.Red)-                      else (fg, Color.defFG)-                    Color.HighlightYellow -> (Color.Black, Color.defFG)-                    Color.HighlightYellowAim -> (Color.Black, Color.defFG)-                    Color.HighlightRedAim ->-                      if fg /= Color.Red-                      then (fg, Color.Red)-                      else (fg, Color.defFG)-                    Color.HighlightNoneCursor -> (fg, Color.Black) ]-  C.refresh--keyTranslate :: C.Key -> K.KM-keyTranslate e = (\(key, modifier) -> K.KM modifier key) $-  case e of-    C.KeyChar '\ESC' -> (K.Esc,     K.NoModifier)-    C.KeyExit        -> (K.Esc,     K.NoModifier)-    C.KeyChar '\n'   -> (K.Return,  K.NoModifier)-    C.KeyChar '\r'   -> (K.Return,  K.NoModifier)-    C.KeyEnter       -> (K.Return,  K.NoModifier)-    C.KeyChar ' '    -> (K.Space,   K.NoModifier)-    C.KeyChar '\t'   -> (K.Tab,     K.NoModifier)-    C.KeyBTab        -> (K.BackTab, K.NoModifier)-    C.KeyBackspace   -> (K.BackSpace, K.NoModifier)-    C.KeyUp          -> (K.Up,      K.NoModifier)-    C.KeyDown        -> (K.Down,    K.NoModifier)-    C.KeyLeft        -> (K.Left,    K.NoModifier)-    C.KeySLeft       -> (K.Left,    K.NoModifier)-    C.KeyRight       -> (K.Right,   K.NoModifier)-    C.KeySRight      -> (K.Right,   K.NoModifier)-    C.KeyHome        -> (K.Home,    K.NoModifier)-    C.KeyEnd         -> (K.End,     K.NoModifier)-    C.KeyPPage       -> (K.PgUp,    K.NoModifier)-    C.KeyNPage       -> (K.PgDn,    K.NoModifier)-    C.KeyBeg         -> (K.Begin,   K.NoModifier)-    C.KeyB2          -> (K.Begin,   K.NoModifier)-    C.KeyClear       -> (K.Begin,   K.NoModifier)-    C.KeyIC          -> (K.Insert,  K.NoModifier)-    -- No KP_ keys; see <https://github.com/skogsbaer/hscurses/issues/10>-    C.KeyChar c-      -- This case needs to be considered after Tab, since, apparently,-      -- on some terminals ^i == Tab and Tab is more important for us.-      | ord '\^A' <= ord c && ord c <= ord '\^Z' ->-        -- Alas, only lower-case letters.-        (K.Char $ chr $ ord c - ord '\^A' + ord 'a', K.Control)-        -- Movement keys are more important than leader picking,-        -- so disabling the latter and interpreting the keypad numbers-        -- as movement:-      | c `elem` ['1'..'9'] -> (K.KP c, K.NoModifier)-      | otherwise           -> (K.Char c, K.NoModifier)-    _                       -> (K.Unknown (show e), K.NoModifier)--toFColor :: Color.Color -> C.ForegroundColor-toFColor Color.Black     = C.BlackF-toFColor Color.Red       = C.DarkRedF-toFColor Color.Green     = C.DarkGreenF-toFColor Color.Brown     = C.BrownF-toFColor Color.Blue      = C.DarkBlueF-toFColor Color.Magenta   = C.PurpleF-toFColor Color.Cyan      = C.DarkCyanF-toFColor Color.White     = C.WhiteF-toFColor Color.AltWhite  = C.WhiteF-toFColor Color.BrBlack   = C.GreyF-toFColor Color.BrRed     = C.RedF-toFColor Color.BrGreen   = C.GreenF-toFColor Color.BrYellow  = C.YellowF-toFColor Color.BrBlue    = C.BlueF-toFColor Color.BrMagenta = C.MagentaF-toFColor Color.BrCyan    = C.CyanF-toFColor Color.BrWhite   = C.BrightWhiteF--toBColor :: Color.Color -> C.BackgroundColor-toBColor Color.Black     = C.BlackB-toBColor Color.Red       = C.DarkRedB-toBColor Color.Green     = C.DarkGreenB-toBColor Color.Brown     = C.BrownB-toBColor Color.Blue      = C.DarkBlueB-toBColor Color.Magenta   = C.PurpleB-toBColor Color.Cyan      = C.DarkCyanB-toBColor Color.White     = C.WhiteB-toBColor _               = C.BlackB  -- a limitation of curses
engine-src/Game/LambdaHack/Client/UI/Frontend/Dom.hs view
@@ -1,8 +1,13 @@ -- | Text frontend running in a browser. module Game.LambdaHack.Client.UI.Frontend.Dom-  ( startup, frontendName+  (+#ifdef USE_BROWSER+-- to molify doctest, but don't break stylish-haskell parsing+   startup, frontendName+#endif   ) where +#ifdef USE_BROWSER import Prelude ()  import Game.LambdaHack.Core.Prelude@@ -35,11 +40,7 @@ import GHCJS.DOM.HTMLCollection (itemUnsafe) import GHCJS.DOM.HTMLElement (focus) import GHCJS.DOM.HTMLTableElement-  ( HTMLTableElement (HTMLTableElement)-  , getRows-  , setCellPadding-  , setCellSpacing-  )+  (HTMLTableElement (HTMLTableElement), getRows, setCellPadding, setCellSpacing) import GHCJS.DOM.HTMLTableRowElement   (HTMLTableRowElement (HTMLTableRowElement), getCells) import GHCJS.DOM.KeyboardEvent@@ -262,7 +263,11 @@            | otherwise -> setTextContent cell $ Just [acChar]         setProp style "color" $ Color.colorToRGB fg         let bgPrev = Color.bgFromW32 $ Color.AttrCharW32 wPrev-        when (bg /= bgPrev) $+        when (bg /= bgPrev) $ do+          let background = if bg == Color.HighlightBackground+                           then "#251F1F"+                           else Color.colorToRGB Color.Black+          setProp style "background-color" background           setProp style "border-color"                         (Color.colorToRGB $ Color.highlightToColor bg)         return $! i + 1@@ -275,3 +280,4 @@   -- This attempts to ensure no redraws while callback executes   -- and a single redraw when it completes.   requestAnimationFrame_ scurrentWindow callback+#endif
− engine-src/Game/LambdaHack/Client/UI/Frontend/Gtk.hs
@@ -1,279 +0,0 @@-{-# OPTIONS_GHC -Wno-unused-do-bind #-}--- | Text frontend based on Gtk.-module Game.LambdaHack.Client.UI.Frontend.Gtk-  ( startup, frontendName-  ) where--import Prelude ()--import Game.LambdaHack.Core.Prelude--import           Control.Concurrent-import qualified Control.Monad.IO.Class as IO-import           Data.Bits (unsafeShiftL)-import qualified Data.IntMap.Strict as IM-import           Data.IORef-import qualified Data.Text as T-import           Data.Word (Word32)-import qualified Game.LambdaHack.Common.PointArray as PointArray-import           Graphics.UI.Gtk-import           System.Exit (exitFailure)--import           Game.LambdaHack.Client.UI.Content.Screen-import           Game.LambdaHack.Client.UI.Frame-import           Game.LambdaHack.Client.UI.Frontend.Common-import qualified Game.LambdaHack.Client.UI.Key as K-import           Game.LambdaHack.Client.UI.PointUI-import           Game.LambdaHack.Common.ClientOptions-import qualified Game.LambdaHack.Definition.Color as Color-import           Game.LambdaHack.Definition.Defs---- | Session data maintained by the frontend.-data FrontendSession = FrontendSession-  { sview :: TextView           -- ^ the widget to draw to-  , stags :: IM.IntMap TextTag  -- ^ text color tags for fg/bg-  }---- | The name of the frontend.-frontendName :: String-frontendName = "gtk"---- | Set up and start the main GTK loop providing input and output.------ Because of Windows, GTK needs to be on a bound thread,--- so we can't avoid the communication overhead of bound threads.-startup :: ScreenContent -> ClientOptions -> IO RawFrontend-startup coscreen soptions = startupBound $ startupFun coscreen soptions--startupFun :: ScreenContent -> ClientOptions -> MVar RawFrontend -> IO ()-startupFun coscreen _ rfMVar = do-  -- Init GUI.-  unsafeInitGUIForThreadedRTS-  -- Text attributes.-  let emulateBox Color.Attr{..} = case bg of-        Color.HighlightNone -> (fg, Color.Black)-        Color.HighlightGreen ->-          if fg /= Color.Green-          then (fg, Color.Green)-          else (fg, Color.BrBlack)-        Color.HighlightBlue ->-          if fg /= Color.Blue-          then (fg, Color.Blue)-          else (fg, Color.BrBlack)-        Color.HighlightBrown ->-          if fg /= Color.Blue-          then (fg, Color.Brown)-          else (fg, Color.BrBlack)-        Color.HighlightCyan ->-          if fg /= Color.Blue-          then (fg, Color.Cyan)-          else (fg, Color.BrBlack)-        Color.HighlightGrey ->-          if fg /= Color.BrBlack-          then (fg, Color.BrBlack)-          else (fg, Color.defFG)-        Color.HighlightWhite -> (fg, Color.Black)-        Color.HighlightMagenta -> (fg, Color.Black)-        Color.HighlightRed ->-          if fg /= Color.Red-          then (fg, Color.Red)-          else (fg, Color.defFG)-        Color.HighlightYellow -> (Color.Black, Color.defFG)  -- no cursor-        Color.HighlightYellowAim -> (Color.Black, Color.defFG)-        Color.HighlightRedAim ->-          if fg /= Color.Red-          then (fg, Color.Red)-          else (fg, Color.defFG)-        Color.HighlightNoneCursor -> (fg, Color.Black)-  ttt <- textTagTableNew-  stags <- IM.fromDistinctAscList <$>-             mapM (\ak -> do-                      tt <- textTagNew Nothing-                      textTagTableAdd ttt tt-                      doAttr tt (emulateBox ak)-                      return (fromAttr ak, tt))-               [ Color.Attr{fg, bg}-               | fg <- Color.legalFgCol-               , bg <- [minBound..maxBound] ]-  -- Text buffer.-  tb <- textBufferNew (Just ttt)-  -- Create text view.-  sview <- textViewNewWithBuffer tb-  textViewSetEditable sview False-  textViewSetCursorVisible sview False-  widgetDelEvents sview [SmoothScrollMask, TouchMask]-  widgetAddEvents sview [ScrollMask]-  let sess = FrontendSession{..}-  rf <- createRawFrontend coscreen (display coscreen sess) shutdown-  putMVar rfMVar rf-  let modTranslate mods = modifierTranslate-        (Control `elem` mods)-        (Shift `elem` mods)-        (any (`elem` mods) [Alt, Alt2, Alt3, Alt4, Alt5])-        (any (`elem` mods) [Meta, Super])-  sview `on` keyPressEvent $ do-    n <- eventKeyName-    mods <- eventModifier-    let key = K.keyTranslate $ T.unpack n-        modifier = modTranslate mods  -- Shift included-        modifierNoShift = case modifier of  -- to prevent S-!, etc.-          K.Shift -> K.NoModifier-          K.ControlShift -> K.Control-          K.AltShift -> K.Alt-          _ -> modifier-    when (key == K.Esc) $ IO.liftIO $ resetChanKey (fchanKey rf)-    IO.liftIO $ saveKMP rf modifierNoShift key (PointUI 0 0)-    return True-  -- Set the font specified in config, if any.-  -- The list are monospace fonts that have fixed size regardless-  -- of boldness (on some OSes at least).-  f <- fontDescriptionFromString ("DejaVu Sans Mono,Consolas,Courier New,Liberation Mono,Courier,FreeMono,Monospace 16px" :: String)-  widgetModifyFont sview (Just f)-  IO.liftIO $ do-    textViewSetLeftMargin sview 3-    textViewSetRightMargin sview 3-  -- Take care of the mouse events.-  sview `on` scrollEvent $ do-    IO.liftIO $ resetChanKey (fchanKey rf)-    scrollDir <- eventScrollDirection-    (wx, wy) <- eventCoordinates-    mods <- eventModifier-    let modifier = modTranslate mods  -- Shift included-    IO.liftIO $ do-      (bx, by) <--        textViewWindowToBufferCoords sview TextWindowText-                                     (round wx, round wy)-      (iter, _) <- textViewGetIterAtPosition sview bx by-      cx <- textIterGetLineOffset iter-      cy <- textIterGetLine iter-      let pointer = PointUI cx cy-          -- Store the mouse event coords in the keypress channel.-          storeK key = saveKMP rf modifier key pointer-      case scrollDir of-        ScrollUp -> storeK K.WheelNorth-        ScrollDown -> storeK K.WheelSouth-        _ -> return ()  -- ignore any fancy new gizmos-    return True  -- disable selection-  currentfont <- newIORef f-  Just defDisplay <- displayGetDefault-  cursor <- cursorNewForDisplay defDisplay Tcross  -- Target Crosshair Arrow-  sview `on` buttonPressEvent $ return True  -- disable selection-  sview `on` buttonReleaseEvent $ do-    IO.liftIO $ resetChanKey (fchanKey rf)-    but <- eventButton-    (wx, wy) <- eventCoordinates-    mods <- eventModifier-    let modifier = modTranslate mods  -- Shift included-    IO.liftIO $ do-      when (but == RightButton && modifier == K.Control) $ do-        fsd <- fontSelectionDialogNew ("Choose font" :: String)-        cf  <- readIORef currentfont-        fds <- fontDescriptionToString cf-        fontSelectionDialogSetFontName fsd (fds :: String)-        fontSelectionDialogSetPreviewText fsd ("eee...@.##+##" :: String)-        resp <- dialogRun fsd-        when (resp == ResponseOk) $ do-          fn <- fontSelectionDialogGetFontName fsd-          case fn :: Maybe String of-            Just fn' -> do-              fd <- fontDescriptionFromString fn'-              writeIORef currentfont fd-              widgetModifyFont sview (Just fd)-            Nothing  -> return ()-        widgetDestroy fsd-      mdrawWin <- displayGetWindowAtPointer defDisplay-      let setCursor (drawWin, _, _) =-            drawWindowSetCursor drawWin (Just cursor)-      maybe (return ()) setCursor mdrawWin-      (bx, by) <--        textViewWindowToBufferCoords sview TextWindowText-                                     (round wx, round wy)-      (iter, _) <- textViewGetIterAtPosition sview bx by-      cx <- textIterGetLineOffset iter-      cy <- textIterGetLine iter-      let mkey = case but of-            LeftButton -> Just K.LeftButtonRelease-            MiddleButton -> Just K.MiddleButtonRelease-            RightButton -> Just K.RightButtonRelease-            _ -> Nothing  -- probably a glitch-          pointer = PointUI cx cy-      -- Store the mouse event coords in the keypress channel.-      maybe (return ())-            (\key -> IO.liftIO $ saveKMP rf modifier key pointer) mkey-    return True-  -- Modify default colours.-  let black = Color minBound minBound minBound  -- Color.defBG == Color.Black-      white = Color 0xB800 0xBF00 0xCB00        -- Color.defFG == Color.White-  widgetModifyBg sview StateNormal black-  widgetModifyFg sview StateNormal white-  -- Set up the main window.-  w <- windowNew-  containerAdd w sview-  -- We assume it's intentional window kill by the player,-  -- so game is not saved, unlike with assertion failure, etc.-  w `on` deleteEvent $ IO.liftIO $ do-    putStrLn "Window killed"-    mainQuit-    exitFailure-  widgetShowAll w-  mainGUI--shutdown :: IO ()-shutdown = postGUISync mainQuit--fromAttr :: Color.Attr -> Int-fromAttr Color.Attr{..} = unsafeShiftL (fromEnum fg) 8 + fromEnum bg--doAttr :: TextTag -> (Color.Color, Color.Color) -> IO ()-doAttr tt (fg, bg)-  | fg == Color.defFG && bg == Color.Black = return ()-  | fg == Color.defFG =-    set tt [textTagBackground := Color.colorToRGB bg]-  | bg == Color.Black =-    set tt $ extraAttr-             ++ [textTagForeground := Color.colorToRGB fg]-  | otherwise =-    set tt $ extraAttr-             ++ [ textTagForeground := Color.colorToRGB fg-                , textTagBackground := Color.colorToRGB bg ]--extraAttr :: [AttrOp TextTag]-extraAttr = [textTagWeight := fromEnum WeightBold]---          , textTagStretch := StretchUltraExpanded---- | Add a frame to be drawn.-display :: ScreenContent-        -> FrontendSession-        -> SingleFrame-        -> IO ()-display coscreen FrontendSession{..} SingleFrame{singleArray} = do-  let f !w (!n, !l) = if n == -1-                      then (rwidth coscreen - 2, Color.charFromW32 w : '\n' : l)-                      else (n - 1, Color.charFromW32 w : l)-      (_, levelChar) =-        PointArray.foldrA' f (rwidth coscreen - 1, []) singleArray-      !gfChar = T.pack levelChar-  postGUISync $ do-    tb <- textViewGetBuffer sview-    textBufferSetText tb gfChar-    ib <- textBufferGetStartIter tb-    ie <- textIterCopy ib-    let defEnum = toEnum $ fromAttr Color.defAttr-        setTo :: (X, Word32) -> Color.AttrCharW32 -> IO (X, Word32)-        setTo (!lx, !previous) !w-          | (lx + 1) `mod` (rwidth coscreen + 1) /= 0 = do-            let current :: Word32-                current = Color.attrCharW32 w-            if current == previous-            then return (lx + 1, previous)-            else do-              textIterSetOffset ie lx-              when (previous /= defEnum) $-                textBufferApplyTag tb (stags IM.! fromEnum previous) ib ie-              textIterSetOffset ib lx-              return (lx + 1, current)-        setTo (lx, previous) w = setTo (lx + 1, previous) w-    (lx, previous) <- PointArray.foldMA' setTo (-1, defEnum) singleArray-    textIterSetOffset ie lx-    when (previous /= defEnum) $-      textBufferApplyTag tb (stags IM.! fromEnum previous) ib ie
engine-src/Game/LambdaHack/Client/UI/Frontend/Sdl.hs view
@@ -230,7 +230,7 @@                                      SDL.TextureAccessTarget screenV2         SDL.rendererRenderTarget srenderer SDL.$= Just texture         SDL.rendererDrawBlendMode srenderer SDL.$= SDL.BlendNone-        SDL.rendererDrawColor srenderer SDL.$= colorToRGBA Color.Black+        SDL.rendererDrawColor srenderer SDL.$= blackRGBA         SDL.clear srenderer  -- clear the texture         return texture   basicTexture <- initTexture@@ -395,17 +395,18 @@       boxSize = 2 * halfSize       vp :: Int -> Int -> Vect.Point Vect.V2 CInt       vp x y = Vect.P $ Vect.V2 (toEnum x) (toEnum y)-      drawHighlight !x !y !color = do+      drawHighlight !col !row !color = do         SDL.rendererDrawColor srenderer SDL.$= colorToRGBA color         let tt2Square = Vect.V2 (toEnum boxSize) (toEnum boxSize)-            rect = SDL.Rectangle (vp (x * boxSize) (y * boxSize)) tt2Square+            rect = SDL.Rectangle (vp (col * boxSize) (row * boxSize)) tt2Square         SDL.drawRect srenderer $ Just rect-        SDL.rendererDrawColor srenderer SDL.$= colorToRGBA Color.Black+        SDL.rendererDrawColor srenderer SDL.$= blackRGBA           -- reset back to black-      chooseAndDrawHighlight !x !y !bg = case bg of+      chooseAndDrawHighlight !col !row !bg = case bg of         Color.HighlightNone -> return ()+        Color.HighlightBackground -> return ()         Color.HighlightNoneCursor -> return ()-        _ -> drawHighlight x y $ Color.highlightToColor bg+        _ -> drawHighlight col row $ Color.highlightToColor bg       -- This also frees the surface it gets.       scaleSurfaceToTexture :: Int -> SDL.Surface -> IO SDL.Texture       scaleSurfaceToTexture xsize textSurfaceRaw = do@@ -421,7 +422,7 @@             tgtR = vp xtgt ytgt             tt2 = Vect.V2 (toEnum xsize) (toEnum boxSize)         textSurface <- SDL.createRGBSurface tt2 SDL.ARGB8888-        SDL.surfaceFillRect textSurface Nothing (colorToRGBA Color.Black)+        SDL.surfaceFillRect textSurface Nothing blackRGBA         -- We crop surface rather than texture to set the resulting         -- texture as @TextureAccessStatic@ via @createTextureFromSurface@,         -- which otherwise we wouldn't be able to do.@@ -432,14 +433,15 @@         SDL.freeSurface textSurface         return textTexture       -- This also frees the surface it gets.-      scaleSurfaceToTextureProp :: Int -> Int -> SDL.Surface+      scaleSurfaceToTextureProp :: Int -> Int -> SDL.Surface -> Bool                                 -> IO (Int, SDL.Texture)-      scaleSurfaceToTextureProp x row textSurfaceRaw = do+      scaleSurfaceToTextureProp x row textSurfaceRaw allSpace = do         Vect.V2 sw sh <- SDL.surfaceDimensions textSurfaceRaw         let widthRaw = fromEnum sw-            width = if widthRaw > rwidth coscreen * boxSize - x-                    then (rwidth coscreen - 1) * boxSize - x-                    else widthRaw+            remainingWidth = rwidth coscreen * boxSize - x+            width | widthRaw <= remainingWidth = widthRaw+                  | allSpace = remainingWidth+                  | otherwise = remainingWidth - boxSize             height = min boxSize $ fromEnum sh             xsrc = 0             ysrc = max 0 (fromEnum sh - height) `divUp` 2@@ -450,7 +452,7 @@             tgtR = vp xtgt ytgt             tt2Prop = Vect.V2 (toEnum width) (toEnum boxSize)         textSurface <- SDL.createRGBSurface tt2Prop SDL.ARGB8888-        SDL.surfaceFillRect textSurface Nothing (colorToRGBA Color.Black)+        SDL.surfaceFillRect textSurface Nothing blackRGBA         -- We crop surface rather than texture to set the resulting         -- texture as @TextureAccessStatic@ via @createTextureFromSurface@,         -- which otherwise we wouldn't be able to do.@@ -462,57 +464,32 @@         SDL.freeSurface textSurfaceRaw         textTexture <- SDL.createTextureFromSurface srenderer textSurface         SDL.freeSurface textSurface-        when (width /= widthRaw) $ do-          let greyDollar = Color.trimmedLineAttrW32-          void $ setChar (fromEnum Point{px = rwidth coscreen - 1, py = row})-                         (Color.attrCharW32 greyDollar, 0)+        when (width /= widthRaw && not allSpace) $+          setSquareChar (rwidth coscreen - 1) row Color.trimmedLineAttrW32         return (width, textTexture)       -- <https://www.libsdl.org/projects/SDL_ttf/docs/SDL_ttf_42.html#SEC42>-      setChar :: PointI -> (Word32, Word32) -> IO Int-      setChar !i (!w, !wPrev) =+      -- Note that @Point@ here refers to screen coordinates with square font+      -- (as @PointSquare@ normally should) and not game map coordinates.+      -- See "Game.LambdaHack.Client.UI.Frame" for explanation of this+      -- irregularity.+      setMapChar :: PointI -> (Word32, Word32) -> IO Int+      setMapChar !i (!w, !wPrev) =         if w == wPrev         then return $! i + 1         else do           let Point{..} = toEnum i-          atlas <- readIORef squareAtlas-          let Color.AttrChar{ acAttr=Color.Attr{fg=fgRaw, bg}-                            , acChar=acCharRaw } =-                Color.attrCharFromW32 $ Color.AttrCharW32 w-              fg | py `mod` 2 == 0 && fgRaw == Color.White = Color.AltWhite-                 | otherwise = fgRaw-              ac = Color.attrChar2ToW32 fg acCharRaw-          textTexture <- case EM.lookup ac atlas of-            Nothing -> do-              -- Make all visible floors bold (no bold fold variant for 16x16x,-              -- so only the dot can be bold).-              let acChar = if not (Color.isBright fg)-                              && acCharRaw == floorSymbol  -- '\x00B7'-                           then if mapFontIsBitmap-                                then '\x0007'-                                else '\x22C5'-                           else acCharRaw-              textSurfaceRaw <- TTF.shadedGlyph squareFont (colorToRGBA fg)-                                                (colorToRGBA Color.Black) acChar-              textTexture <- scaleSurfaceToTexture boxSize textSurfaceRaw-              writeIORef squareAtlas $ EM.insert ac textTexture atlas-              return textTexture-            Just textTexture -> return textTexture-          let tt2Square = Vect.V2 (toEnum boxSize) (toEnum boxSize)-              tgtR = SDL.Rectangle (vp (px * boxSize) (py * boxSize)) tt2Square-          SDL.copy srenderer textTexture Nothing (Just tgtR)-          -- Potentially overwrite a portion of the glyph.-          chooseAndDrawHighlight px py bg+          setSquareChar px py (Color.AttrCharW32 w)           return $! i + 1       drawMonoOverlay :: OverlaySpace -> IO ()       drawMonoOverlay =         mapM_ (\(PointUI x y, al) ->                  let lineCut = take (2 * rwidth coscreen - x) al-                 in drawMonoLine (x * halfSize) y lineCut)+                 in drawMonoLine x y lineCut)       drawMonoLine :: Int -> Int -> AttrString -> IO ()       drawMonoLine _ _ [] = return ()       drawMonoLine x row (w : rest) = do         setMonoChar x row w-        drawMonoLine (x + halfSize) row rest+        drawMonoLine (x + 1) row rest       setMonoChar :: Int -> Int -> Color.AttrCharW32 -> IO ()       setMonoChar !x !row !w = do         atlas <- readIORef smonoAtlas@@ -527,14 +504,60 @@           Nothing -> do             textSurfaceRaw <-               TTF.shadedGlyph (fromJust smonoFont) (colorToRGBA fg)-                              (colorToRGBA Color.Black) acChar+                              blackRGBA acChar             textTexture <- scaleSurfaceToTexture halfSize textSurfaceRaw             writeIORef smonoAtlas $ EM.insert ac textTexture atlas             return textTexture           Just textTexture -> return textTexture         let tt2Mono = Vect.V2 (toEnum halfSize) (toEnum boxSize)-            tgtR = SDL.Rectangle (vp x (row * boxSize)) tt2Mono+            tgtR = SDL.Rectangle (vp (x * halfSize) (row * boxSize)) tt2Mono         SDL.copy srenderer textTexture Nothing (Just tgtR)+      drawSquareOverlay :: OverlaySpace -> IO ()+      drawSquareOverlay =+        mapM_ (\(pUI, al) ->+                 let PointSquare col row = uiToSquare pUI+                     lineCut = take (rwidth coscreen - col) al+                 in drawSquareLine col row lineCut)+      drawSquareLine :: Int -> Int -> AttrString -> IO ()+      drawSquareLine _ _ [] = return ()+      drawSquareLine col row (w : rest) = do+        setSquareChar col row w+        drawSquareLine (col + 1) row rest+      setSquareChar :: Int -> Int -> Color.AttrCharW32 -> IO ()+      setSquareChar !col !row !w = do+        atlas <- readIORef squareAtlas+        let Color.AttrChar{ acAttr=Color.Attr{fg=fgRaw, bg}+                          , acChar=acCharRaw } =+              Color.attrCharFromW32 w+            fg | row `mod` 2 == 0 && fgRaw == Color.White = Color.AltWhite+               | otherwise = fgRaw+            ac = if bg == Color.HighlightBackground+                 then w+                 else Color.attrChar2ToW32 fg acCharRaw+        textTexture <- case EM.lookup ac atlas of+          Nothing -> do+            -- Make all visible floors bold (no bold font variant for 16x16x,+            -- so only the dot can be bold).+            let acChar = if not (Color.isBright fg)+                            && acCharRaw == floorSymbol  -- '\x00B7'+                         then if mapFontIsBitmap+                              then '\x0007'+                              else '\x22C5'+                         else acCharRaw+                background = if bg == Color.HighlightBackground+                             then greyRGBA+                             else blackRGBA+            textSurfaceRaw <- TTF.shadedGlyph squareFont (colorToRGBA fg)+                                              background acChar+            textTexture <- scaleSurfaceToTexture boxSize textSurfaceRaw+            writeIORef squareAtlas $ EM.insert ac textTexture atlas+            return textTexture+          Just textTexture -> return textTexture+        let tt2Square = Vect.V2 (toEnum boxSize) (toEnum boxSize)+            tgtR = SDL.Rectangle (vp (col * boxSize) (row * boxSize)) tt2Square+        SDL.copy srenderer textTexture Nothing (Just tgtR)+        -- Potentially overwrite a portion of the glyph.+        chooseAndDrawHighlight col row bg       drawPropOverlay :: OverlaySpace -> IO ()       drawPropOverlay =         mapM_ (\(PointUI x y, al) ->@@ -568,9 +591,11 @@         let font = if fg >= Color.White && fg /= Color.BrBlack                    then spropFont                    else sboldFont+            allSpace = T.all Char.isSpace t         textSurfaceRaw <- TTF.shaded (fromJust font) (colorToRGBA fg)-                                     (colorToRGBA Color.Black) t-        (width, textTexture) <- scaleSurfaceToTextureProp x row textSurfaceRaw+                                     blackRGBA t+        (width, textTexture) <-+          scaleSurfaceToTextureProp x row textSurfaceRaw allSpace         let tgtR = SDL.Rectangle (vp x (row * boxSize))                                  (Vect.V2 (toEnum width) (toEnum boxSize))         -- Potentially overwrite some of the screen.@@ -580,12 +605,13 @@   let arraysEqual = singleArray curFrame == singleArray prevFrame       overlaysEqual =         singleMonoOverlay curFrame == singleMonoOverlay prevFrame+        && singleSquareOverlay curFrame == singleSquareOverlay prevFrame         && singlePropOverlay curFrame == singlePropOverlay prevFrame   basicTexture <- readIORef sbasicTexture  -- previous content still present   unless arraysEqual $ do     SDL.rendererRenderTarget srenderer SDL.$= Just basicTexture-    U.foldM'_ setChar 0 $ U.zip (PointArray.avector $ singleArray curFrame)-                                (PointArray.avector $ singleArray prevFrame)+    U.foldM'_ setMapChar 0 $ U.zip (PointArray.avector $ singleArray curFrame)+                                   (PointArray.avector $ singleArray prevFrame)   unless (arraysEqual && overlaysEqual) $ do     texture <- readIORef stexture     SDL.rendererRenderTarget srenderer SDL.$= Just texture@@ -594,6 +620,7 @@     -- the proportional one and so to have a warning message about overrun     -- that needs to be overlaid on top of the proportional overlay.     drawPropOverlay $ singlePropOverlay curFrame+    drawSquareOverlay $ singleSquareOverlay curFrame     drawMonoOverlay $ singleMonoOverlay curFrame     writeIORef spreviousFrame curFrame     SDL.rendererRenderTarget srenderer SDL.$= Nothing@@ -757,9 +784,17 @@ sDL_ALPHA_OPAQUE :: Word8 sDL_ALPHA_OPAQUE = 255 +blackRGBA :: SDL.V4 Word8+blackRGBA = SDL.V4 0 0 0 sDL_ALPHA_OPAQUE++-- A third of @colorToRGBA Color.BrBlack@ to compensate for the use+-- as background (high area) as opposed to glyphs (usually small area).+greyRGBA :: SDL.V4 Word8+greyRGBA = SDL.V4 0x25 0x1F 0x1F sDL_ALPHA_OPAQUE+ -- This code is sadly duplicated from "Game.LambdaHack.Definition.Color". colorToRGBA :: Color.Color -> SDL.V4 Word8-colorToRGBA Color.Black     = SDL.V4 0 0 0 sDL_ALPHA_OPAQUE+colorToRGBA Color.Black     = blackRGBA colorToRGBA Color.Red       = SDL.V4 0xD5 0x05 0x05 sDL_ALPHA_OPAQUE colorToRGBA Color.Green     = SDL.V4 0x05 0x9D 0x05 sDL_ALPHA_OPAQUE colorToRGBA Color.Brown     = SDL.V4 0xCA 0x4A 0x05 sDL_ALPHA_OPAQUE
engine-src/Game/LambdaHack/Client/UI/Frontend/Teletype.hs view
@@ -33,13 +33,13 @@   rf <- createRawFrontend coscreen (display coscreen) shutdown   let storeKeys :: IO ()       storeKeys = do-        l <- SIO.getLine  -- blocks here, so no polling-        let c = case l of-              [] -> '\n'  -- empty line counts as RET-              hd : _ -> hd-            K.KM{..} = keyTranslate c+        c <- SIO.getChar  -- blocks here, so no polling+        let K.KM{..} = keyTranslate c         saveKMP rf modifier key (PointUI 0 0)         storeKeys+  SIO.hSetBuffering SIO.stdin SIO.NoBuffering+  SIO.hSetBuffering SIO.stderr $ SIO.BlockBuffering $+    Just $ 2 * rwidth coscreen * rheight coscreen   void $ async storeKeys   return $! rf @@ -50,7 +50,7 @@ display :: ScreenContent         -> SingleFrame         -> IO ()-display coscreen SingleFrame{..} = do+display coscreen SingleFrame{singleArray} = do   let f w l =         let acCharRaw = Color.charFromW32 w             acChar = if acCharRaw == floorSymbol then '.' else acCharRaw@@ -59,9 +59,8 @@       chunk [] = []       chunk l = let (ch, r) = splitAt (rwidth coscreen) l                 in ch : chunk r-  SIO.hPutStrLn SIO.stderr $ unlines levelChar-  mapM_ (SIO.hPutStrLn SIO.stderr) $-    map (map Color.charFromW32 . snd) singlePropOverlay+  SIO.hPutStr SIO.stderr $ unlines levelChar+  SIO.hFlush SIO.stderr  keyTranslate :: Char -> K.KM keyTranslate e = (\(key, modifier) -> K.KM modifier key) $
− engine-src/Game/LambdaHack/Client/UI/Frontend/Vty.hs
@@ -1,175 +0,0 @@--- | Text frontend based on Vty.-module Game.LambdaHack.Client.UI.Frontend.Vty-  ( startup, frontendName-  ) where--import Prelude ()--import Game.LambdaHack.Core.Prelude--import           Control.Concurrent.Async-import           Graphics.Vty-import qualified Graphics.Vty as Vty--import           Game.LambdaHack.Client.UI.Content.Screen-import           Game.LambdaHack.Client.UI.Frame-import           Game.LambdaHack.Client.UI.Frontend.Common-import qualified Game.LambdaHack.Client.UI.Key as K-import           Game.LambdaHack.Client.UI.PointUI-import           Game.LambdaHack.Common.ClientOptions-import           Game.LambdaHack.Common.Point-import qualified Game.LambdaHack.Common.PointArray as PointArray-import           Game.LambdaHack.Content.TileKind (floorSymbol)-import qualified Game.LambdaHack.Definition.Color as Color---- | Session data maintained by the frontend.-newtype FrontendSession = FrontendSession-  { svty :: Vty  -- ^ internal vty session-  }---- | The name of the frontend.-frontendName :: String-frontendName = "vty"---- | Starts the main program loop using the frontend input and output.-startup :: ScreenContent -> ClientOptions -> IO RawFrontend-startup coscreen _soptions = do-  svty <- mkVty mempty-  let sess = FrontendSession{..}-  rf <- createRawFrontend coscreen (display coscreen sess) (Vty.shutdown svty)-  let storeKeys :: IO ()-      storeKeys = do-        e <- nextEvent svty  -- blocks here, so no polling-        case e of-          EvKey n mods ->-            saveKMP rf (modTranslate mods) (keyTranslate n) (PointUI 0 0)-          _ -> return ()-        storeKeys-  void $ async storeKeys-  return $! rf---- | Output to the screen via the frontend.-display :: ScreenContent-        -> FrontendSession-        -> SingleFrame-        -> IO ()-display coscreen FrontendSession{svty} SingleFrame{singleArray} = do-  let img = foldr (<->) emptyImage-            . map (foldr (<|>) emptyImage-                     . map (\w -> char (setAttr $ Color.attrFromW32 w)-                                       (squashChar $ Color.charFromW32 w)))-            $ chunk $ PointArray.toListA singleArray-      pic1 = picForImage img-      Point{..} = PointArray.maxIndexByA (comparing Color.bgFromW32) singleArray-      pic2 = pic1 {picCursor = AbsoluteCursor px py}-      chunk [] = []-      chunk l = let (ch, r) = splitAt (rwidth coscreen) l-                in ch : chunk r-  update svty pic2--keyTranslate :: Key -> K.Key-keyTranslate n =-  case n of-    KEsc          -> K.Esc-    KEnter        -> K.Return-    (KChar ' ')   -> K.Space-    (KChar '\t')  -> K.Tab-    KBackTab      -> K.BackTab-    KBS           -> K.BackSpace-    KUp           -> K.Up-    KDown         -> K.Down-    KLeft         -> K.Left-    KRight        -> K.Right-    KHome         -> K.Home-    KEnd          -> K.End-    KPageUp       -> K.PgUp-    KPageDown     -> K.PgDn-    KBegin        -> K.Begin-    KCenter       -> K.Begin-    KIns          -> K.Insert-    -- S-KP_5 and C-KP_5 are still not correctly handled in vty-    -- on some terminals so we have to use 1--9 for movement instead of-    -- leader change.-    (KChar c)-      | c `elem` ['1'..'9'] -> K.KP c  -- movement, not leader change-      | otherwise           -> K.Char c-    _             -> K.Unknown (show n)---- | Translates modifiers to our own encoding.-modTranslate :: [Modifier] -> K.Modifier-modTranslate mods =-  modifierTranslate-    (MCtrl `elem` mods) (MShift `elem` mods) (MAlt `elem` mods) False---- A hack to get bright colors via the bold attribute. Depending on terminal--- settings this is needed or not and the characters really get bold or not.--- HSCurses does this by default, but in Vty you have to request the hack.-hack :: Color.Color -> Attr -> Attr-hack c a = if Color.isBright c then withStyle a bold else a--setAttr :: Color.Attr -> Attr-setAttr Color.Attr{..} =--- This optimization breaks display for white background terminals:---  if (fg, bg) == Color.defAttr---  then def_attr---  else-  let (fg1, bg1) = case bg of-        Color.HighlightNone -> (fg, Color.Black)-        Color.HighlightGreen ->-          if fg /= Color.Green-          then (fg, Color.Green)-          else (fg, Color.BrBlack)-        Color.HighlightBlue ->-          if fg /= Color.Blue-          then (fg, Color.Blue)-          else (fg, Color.BrBlack)-        Color.HighlightBrown ->-          if fg /= Color.Blue-          then (fg, Color.Brown)-          else (fg, Color.BrBlack)-        Color.HighlightCyan ->-          if fg /= Color.Blue-          then (fg, Color.Cyan)-          else (fg, Color.BrBlack)-        Color.HighlightGrey ->-          if fg /= Color.BrBlack-          then (fg, Color.BrBlack)-          else (fg, Color.defFG)-        Color.HighlightWhite -> (fg, Color.Magenta)-        Color.HighlightMagenta -> (fg, Color.Magenta)-        Color.HighlightRed ->-          if fg /= Color.Red-          then (fg, Color.Red)-          else (fg, Color.defFG)-        Color.HighlightYellow -> (fg, Color.Black)  -- cursor used instead-        Color.HighlightYellowAim -> (Color.Black, Color.defFG)-        Color.HighlightRedAim ->-          if fg /= Color.Red-          then (fg, Color.Red)-          else (fg, Color.defFG)-        Color.HighlightNoneCursor -> (fg, Color.Black)-  in hack fg1 $ hack bg1 $-       defAttr { attrForeColor = SetTo (aToc fg1)-               , attrBackColor = SetTo (aToc bg1) }--squashChar :: Char -> Char-squashChar c = if c == floorSymbol then '.' else c--aToc :: Color.Color -> Color-aToc Color.Black     = black-aToc Color.Red       = red-aToc Color.Green     = green-aToc Color.Brown     = yellow-aToc Color.Blue      = blue-aToc Color.Magenta   = magenta-aToc Color.Cyan      = cyan-aToc Color.White     = white-aToc Color.AltWhite  = white-aToc Color.BrBlack   = brightBlack-aToc Color.BrRed     = brightRed-aToc Color.BrGreen   = brightGreen-aToc Color.BrYellow  = brightYellow-aToc Color.BrBlue    = brightBlue-aToc Color.BrMagenta = brightMagenta-aToc Color.BrCyan    = brightCyan-aToc Color.BrWhite   = brightWhite
engine-src/Game/LambdaHack/Client/UI/HandleHelperM.hs view
@@ -1,20 +1,18 @@-{-# LANGUAGE TupleSections #-} -- | Helper functions for both inventory management and human commands. module Game.LambdaHack.Client.UI.HandleHelperM   ( FailError, showFailError, MError, mergeMError, FailOrCmd, failWith   , failSer, failMsg, weaveJust-  , memberCycle, memberCycleLevel, partyAfterLeader+  , pointmanCycle, pointmanCycleLevel, partyAfterLeader   , pickLeader, pickLeaderWithPointer   , itemOverlay, skillsOverlay   , placesFromState, placesOverlay   , describeMode, modesOverlay   , pickNumber, guardItemSize, lookAtItems, lookAtStash, lookAtPosition-  , displayItemLore, displayItemLorePointedAt-  , viewLoreItems, cycleLore, spoilsBlurb+  , displayItemLore, okxItemLorePointedAt, cycleLore, spoilsBlurb   , ppContainerWownW, nxtGameMode #ifdef EXPOSE_INTERNAL     -- * Internal operations-  , lookAtTile, lookAtActors, guardItemVerbs+  , itemOverlayFromState, lookAtTile, lookAtActors, guardItemVerbs #endif   ) where @@ -105,14 +103,13 @@ weaveJust (Left ferr) = Left $ Just ferr weaveJust (Right a) = Right a --- | Switches current member to the next on the level, if any, wrapping.-memberCycleLevel :: (MonadClient m, MonadClientUI m)-                 => Bool -> Direction -> m MError-memberCycleLevel verbose direction = do+-- | Switches current pointman to the next on the level, if any, wrapping.+pointmanCycleLevel :: MonadClientUI m+                   => ActorId -> Bool -> Direction -> m MError+pointmanCycleLevel leader verbose direction = do   side <- getsClient sside   fact <- getsState $ (EM.! side) . sfactionD   lidV <- viewedLevelUI-  leader <- getLeaderUI   body <- getsState $ getActorBody leader   hs <- partyAfterLeader leader   let (autoDun, _) = autoDungeonLevel fact@@ -122,19 +119,19 @@   case filter (\(_, b, _) -> blid b == lidV) hsSort of     _ | autoDun && lidV /= blid body ->       failMsg $ showReqFailure NoChangeDunLeader-    [] -> failMsg "cannot pick any other member on this level"+    [] -> failMsg "cannot pick any other pointman on this level"     (np, b, _) : _ -> do       success <- pickLeader verbose np       let !_A = assert (success `blame` "same leader"                                 `swith` (leader, np, b)) ()       return Nothing --- | Switches current member to the previous in the whole dungeon, wrapping.-memberCycle :: (MonadClient m, MonadClientUI m) => Bool -> Direction -> m MError-memberCycle verbose direction = do+-- | Switches current pointman to the previous in the whole dungeon, wrapping.+pointmanCycle :: MonadClientUI m+              => ActorId -> Bool -> Direction -> m MError+pointmanCycle leader verbose direction = do   side <- getsClient sside   fact <- getsState $ (EM.! side) . sfactionD-  leader <- getLeaderUI   hs <- partyAfterLeader leader   let (autoDun, _) = autoDungeonLevel fact   let hsSort = case direction of@@ -151,7 +148,7 @@  partyAfterLeader :: MonadClientUI m => ActorId -> m [(ActorId, Actor, ActorUI)] partyAfterLeader leader = do-  side <- getsState $ bfid . getActorBody leader+  side <- getsClient sside   sactorUI <- getsSession sactorUI   allOurs <- getsState $ fidActorNotProjGlobalAssocs side -- not only on level   let allOursUI = map (\(aid, b) -> (aid, b, sactorUI EM.! aid)) allOurs@@ -161,16 +158,16 @@   return $! gt ++ lt  -- | Select a faction leader. False, if nothing to do.-pickLeader :: (MonadClient m, MonadClientUI m) => Bool -> ActorId -> m Bool+pickLeader :: MonadClientUI m => Bool -> ActorId -> m Bool pickLeader verbose aid = do-  leader <- getLeaderUI-  if leader == aid+  mleader <- getsClient sleader+  if mleader == Just aid     then return False -- already picked     else do       body <- getsState $ getActorBody aid       bodyUI <- getsSession $ getActorUI aid       let !_A = assert (not (bproj body)-                        `blame` "projectile chosen as the leader"+                        `blame` "projectile chosen as the pointman"                         `swith` (aid, body)) ()       -- Even if it's already the leader, give his proper name, not 'you'.       let subject = partActor bodyUI@@ -182,13 +179,14 @@       modifySession $ \sess -> sess {saimMode =         (\aimMode -> aimMode {aimLevelId = blid body}) <$> saimMode sess}       -- Inform about items, etc.-      (itemsBlurb, _) <- lookAtItems True (bpos body) aid Nothing-      stashBlurb <- lookAtStash (blid body) (bpos body)+      (itemsBlurb, _) <-+        lookAtItems True (bpos body) (blid body) (Just aid) Nothing+      stashBlurb <- lookAtStash (bpos body) (blid body)       when verbose $ msgAdd MsgAtFeetMinor $ stashBlurb <+> itemsBlurb       return True -pickLeaderWithPointer :: (MonadClient m, MonadClientUI m) => m MError-pickLeaderWithPointer = do+pickLeaderWithPointer :: MonadClientUI m => ActorId -> m MError+pickLeaderWithPointer leader = do   CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui   lidV <- viewedLevelUI   side <- getsClient sside@@ -209,7 +207,7 @@   pUI <- getsSession spointer   let p@(Point px py) = squareToMap $ uiToSquare pUI   -- Pick even if no space in status line for the actor's symbol.-  if | py == rheight - 2 && px == 0 -> memberCycle True Forward+  if | py == rheight - 2 && px == 0 -> pointmanCycle leader True Forward      | py == rheight - 2 ->          case drop (px - 1) viewed of            [] -> return Nothing@@ -223,29 +221,41 @@ itemOverlay :: MonadClientUI m             => SingleItemSlots -> LevelId -> ItemBag -> Bool -> m OKX itemOverlay lSlots lid bag displayRanged = do-  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui-  localTime <- getsState $ getLocalTime lid-  itemToF <- getsState $ flip itemToFull+  sccui <- getsSession sccui   side <- getsClient sside-  factionD <- getsState sfactionD-  combGround <- getsState $ combinedGround side-  combOrgan <- getsState $ combinedOrgan side-  combEqp <- getsState $ combinedEqp side-  stashBag <- getsState $ getFactionStashBag side   discoBenefit <- getsClient sdiscoBenefit-  FontSetup{..} <- getFontSetup-  let !_A = assert (allB (`elem` EM.elems lSlots) (EM.keys bag)+  fontSetup <- getFontSetup+  okx <- getsState $ itemOverlayFromState lSlots lid bag displayRanged+                                          sccui side discoBenefit fontSetup+  return $! okx++itemOverlayFromState :: SingleItemSlots -> LevelId -> ItemBag -> Bool+                     -> CCUI -> FactionId -> DiscoveryBenefit -> FontSetup+                     -> State+                     -> OKX+itemOverlayFromState lSlots lid bag displayRanged+                     sccui side discoBenefit FontSetup{..} s =+  let CCUI{coscreen=ScreenContent{rwidth}} = sccui+      localTime = getLocalTime lid s+      itemToF = flip itemToFull s+      factionD = sfactionD s+      combGround = combinedGround side s+      combOrgan = combinedOrgan side s+      combEqp = combinedEqp side s+      stashBag = getFactionStashBag side s+      !_A = assert (allB (`elem` EM.elems lSlots) (EM.keys bag)                     `blame` (lid, bag, lSlots)) ()-      markEqp iid t =-        if | (iid `EM.member` combOrgan-             || iid `EM.member` combEqp)-             && iid `EM.notMember` stashBag-             && iid `EM.notMember` combGround -> T.snoc (T.init t) ']'-               -- all ready to fight with-           | iid `EM.member` stashBag -> T.snoc (T.init t) '}'-               -- some spares in shared stash+      markEqp iid periodic k ncha t =+        if | periodic -> T.snoc (T.init t) '/'  -- if equipped, no charges+           | ncha == 0 -> T.snoc (T.init t) '-'  -- no charges left+           | k > ncha -> T.snoc (T.init t) '+'  -- not all charges left+           | iid `EM.member` combOrgan || iid `EM.member` combEqp ->+             if iid `EM.member` stashBag || iid `EM.member` combGround+             then T.snoc (T.init t) '}'  -- some spares in shared stash+             else T.snoc (T.init t) ']'  -- all ready to fight with            | otherwise -> t-      pr (l, iid) =+      pr :: (SlotChar, ItemId) -> Maybe (AttrString, AttrString, KeyOrSlot)+      pr (c, iid) =         case EM.lookup iid bag of           Nothing -> Nothing           Just kit@(k, _) ->@@ -253,30 +263,19 @@                 arItem = aspectRecordFull itemFull                 colorSymbol =                   if IA.checkFlag Ability.Condition arItem-                  then let color = if benInEqp (discoBenefit EM.! iid)-                                   then Color.BrGreen-                                   else Color.BrRed-                       in Color.attrChar2ToW32 color-                                               (IK.isymbol $ itemKind itemFull)+                  then viewItemBenefitColored discoBenefit iid itemFull                   else viewItem itemFull                 phrase = makePhrase                   [partItemWsRanged rwidth side factionD displayRanged                                     DetailMedium 4 k localTime itemFull kit]-                al1 = attrStringToAL-                      $ textToAS (markEqp iid $ slotLabel l)-                        ++ [Color.spaceAttrW32 | isSquareFont propFont]-                        ++ [colorSymbol]-                xal2 = ( textSize squareFont $ attrLine al1-                       , attrStringToAL $ Color.spaceAttrW32 : textToAS phrase )-                kx = (Right l, ( PointUI 0 0-                               , ButtonWidth propFont (5 + T.length phrase) ))-            in Just ((al1, xal2), kx)-      (ts, kxs) = unzip $ mapMaybe pr $ EM.assocs lSlots-      (tsLab, tsDesc) = unzip ts-      ovsLab = EM.singleton squareFont $ offsetOverlay tsLab-      ovsDesc = EM.singleton propFont $ offsetOverlayX tsDesc-      renumber y (km, (PointUI x _, len)) = (km, (PointUI x y, len))-  return (EM.unionWith (++) ovsLab ovsDesc, zipWith renumber [0..] kxs )+                ncha = ncharges localTime kit+                periodic = IA.checkFlag Ability.Periodic arItem+                !tLab = markEqp iid periodic k ncha $ slotLabel c+                aLab = textToAS tLab ++ [colorSymbol]+                !tDesc = " " <> phrase+            in Just (aLab, textToAS tDesc, Right c)+      l = mapMaybe pr $ EM.assocs lSlots+  in labDescOKX squareFont propFont l  skillsOverlay :: MonadClientUI m => ActorId -> m OKX skillsOverlay aid = do@@ -290,14 +289,15 @@             slotLab = slotLabel c             lab = textToAL slotLab             labLen = textSize squareFont $ attrLine lab-            indentation = if isSquareFont propFont then 42 else 20+            indentation = if isSquareFont propFont then 52 else 26             valueText = skillToDecorator skill b                         $ Ability.getSk skill actorMaxSk             triple = ( lab                      , (labLen, textToAL skName)-                     , (labLen + indentation, textToAL valueText) )+                     , (indentation, textToAL valueText) )+            lenButton = 26 + T.length valueText         in (triple, (Right c, ( PointUI 0 y-                              , ButtonWidth propFont (28 + T.length slotLab) )))+                              , ButtonWidth propFont lenButton )))       (ts, kxs) = unzip $ zipWith prSlot (zip [0..] allSlots) skillSlots       (skLab, skDescr, skValue) = unzip3 ts       skillLab = EM.singleton squareFont $ offsetOverlay skLab@@ -305,16 +305,22 @@       skillValue = EM.singleton monoFont $ offsetOverlayX skValue   return (EM.unionsWith (++) [skillLab, skillDescr, skillValue], kxs) -placesFromState :: ContentData PK.PlaceKind -> ClientOptions -> State+-- | Extract whole-dungeon statistics for each place kind,+-- counting the number of occurrences of each type of+-- `Game.LambdaHack.Content.PlaceKind.PlaceEntry`+-- for the given place kind and gathering the set of levels+-- on which any entry for that place kind can be found.+placesFromState :: ContentData PK.PlaceKind -> Bool -> State                 -> EM.EnumMap (ContentId PK.PlaceKind)                               (ES.EnumSet LevelId, Int, Int, Int)-placesFromState coplace ClientOptions{sexposePlaces} s =-  let addEntries (!es1, !ne1, !na1, !nd1) (!es2, !ne2, !na2, !nd2) =+placesFromState coplace sexposePlaces s =+  let addEntries (!es1, !nEntries1, !nArounds1, !nExists1)+                 (!es2, !nEntries2, !nArounds2, !nExists2) =         let !es = ES.union es1 es2-            !ne = ne1 + ne2-            !na = na1 + na2-            !nd = nd1 + nd2-        in (es, ne, na, nd)+            !nEntries = nEntries1 + nEntries2+            !nArounds = nArounds1 + nArounds2+            !nExists = nExists1 + nExists2+        in (es, nEntries, nArounds, nExists)       placesFromLevel :: (LevelId, Level)                       -> EM.EnumMap (ContentId PK.PlaceKind)                                     (ES.EnumSet LevelId, Int, Int, Int)@@ -326,47 +332,48 @@             f (PK.PExists pk) em =               EM.insertWith addEntries pk (ES.singleton lid, 0, 0, 1) em         in EM.foldr' f EM.empty lentry+             -- go through place entrances and depending on the place+             -- add an entry for it, whether Entry/Around/Exists,+             -- the effect being we're counting #s of each type       insertZeros !em !pk _ = EM.insert pk (ES.empty, 0, 0, 0) em+      -- The initial places are overwritten except for those+      -- that have no entries in the dungeon at all,+      -- and in `sexposePlaces` debug mode these will be shown even though+      -- the stats will be zeros (which is a valuable warning!).       initialPlaces | not sexposePlaces = EM.empty                     | otherwise = ofoldlWithKey' coplace insertZeros EM.empty   in EM.unionWith addEntries        initialPlaces        (EM.unionsWith addEntries $ map placesFromLevel $ EM.assocs $ sdungeon s)+        -- gather per-place-kind statistics for each level,+        -- then aggregate them over all levels, remembering that the place+        -- appeared on the given level (but not how man times)  placesOverlay :: MonadClientUI m => m OKX placesOverlay = do   COps{coplace} <- getsState scops   soptions <- getsClient soptions   FontSetup{..} <- getFontSetup-  places <- getsState $ placesFromState coplace soptions-  let prSlot :: (Int, SlotChar)+  places <- getsState $ placesFromState coplace (sexposePlaces soptions)+  let prSlot :: SlotChar              -> (ContentId PK.PlaceKind, (ES.EnumSet LevelId, Int, Int, Int))-             -> (AttrLine, (Int, AttrLine), KYX)-      prSlot (y, c) (pk, (es, _, _, _)) =+             -> (AttrString, AttrString, KeyOrSlot)+      prSlot c (pk, (es, _, _, _)) =         let placeName = PK.pname $ okind coplace pk             markPlace t = if ES.null es                           then T.snoc (T.init t) '>'                           else t-            !tSlot = markPlace $ slotLabel c  -- free @places@ as you go-            !lenSlot = 2 * T.length tSlot-            !tBlurb = " "-                      <> placeName-                      <+> if ES.null es-                          then ""-                          else "("-                               <> makePhrase [MU.CarWs (ES.size es) "level"]-                               <> ")"-            !lenButton = lenSlot + T.length tBlurb-            !pButton = PointUI 0 y-            !widthButton = ButtonWidth propFont lenButton-        in ( textToAL tSlot-           , (lenSlot, textToAL tBlurb)-           , (Right c, (pButton, widthButton)) )-      (plLab, plDesc, kxs) = unzip3 $ zipWith prSlot (zip [0..] allSlots)-                                    $ EM.assocs places-      placeLab = EM.singleton squareFont $ offsetOverlay plLab-      placeDesc = EM.singleton propFont $ offsetOverlayX plDesc-  return (EM.unionWith (++) placeLab placeDesc, kxs)+            !tLab = markPlace $ slotLabel c  -- ! to free @places@ as you go+            !tDesc = " "+                     <> placeName+                     <+> if ES.null es+                         then ""+                         else "("+                              <> makePhrase [MU.CarWs (ES.size es) "level"]+                              <> ")"+        in (textToAS tLab, textToAS tDesc, Right c)+      l = zipWith prSlot allSlots $ EM.assocs places+  return $! labDescOKX squareFont propFont l  describeMode :: MonadClientUI m              => Bool -> ContentId MK.ModeKind@@ -379,6 +386,9 @@   scoreDict <- getsState shigh   scampings <- getsClient scampings   srestarts <- getsClient srestarts+  side <- getsClient sside+  total <- getsState $ snd . calculateTotal side+  dungeonTotal <- getsState sgold   let gameMode = okind comode gameModeId       duplicateEOL '\n' = "\n\n"       duplicateEOL c = T.singleton c@@ -397,8 +407,13 @@         if T.null desc         then Nothing         else Just [(monoFont, header), (propFont, textToAS desc)]+      survivingHow = if | total == 0 -> "(barely)"+                        | total < dungeonTotal `div` 2 -> "(so far)"+                        | otherwise -> ""       title = if addTitle-              then "\nYou are surviving the '"+              then "\nYou are"+                   <+> survivingHow+                   <+> "surviving the '"                    <> MK.mname gameMode                    <> "' adventure.\n"               else ""@@ -458,17 +473,15 @@            <+:> (textFgToAS color (T.toTitle $ MK.nameOutcomePast outcome)                  <+:> textToAS lastRemark)            <> textToAS ":"-      shiftPointUI x (PointUI x0 y0) = PointUI (x0 + x) y0   return $! if isSquareFont propFont             then EM.singleton squareFont  -- single column, single font-                 $ offsetOverlayX-                 $ map (\t -> (2, t))+                 $ xtranslateOverlay 2 $ offsetOverlay                  $ concatMap snd $ blurb ++ blurbEnd             else EM.unionWith (++)-                 (EM.map (map (first $ shiftPointUI 1))-                  $ attrLinesToFontMap 0 blurb)-                 (EM.map (map (first $ shiftPointUI $ rwidth + 1))-                  $ attrLinesToFontMap 0 blurbEnd)+                 (EM.map (xtranslateOverlay 1)+                  $ attrLinesToFontMap blurb)+                 (EM.map (xtranslateOverlay $ rwidth + 1)+                  $ attrLinesToFontMap blurbEnd)  modesOverlay :: MonadClientUI m => m OKX modesOverlay = do@@ -478,10 +491,10 @@   nxtChal <- getsClient snxtChal  -- mark victories only for current difficulty   let f !acc _p !i !a = (i, a) : acc       campaignModes = ofoldlGroup' comode MK.CAMPAIGN_SCENARIO f []-      prSlot :: (Int, SlotChar)+      prSlot :: SlotChar              -> (ContentId MK.ModeKind, MK.ModeKind)-             -> (AttrLine, (Int, AttrLine), KYX)-      prSlot (y, c) (gameModeId, gameMode) =+             -> (AttrString, AttrString, KeyOrSlot)+      prSlot c (gameModeId, gameMode) =         let modeName = MK.mname gameMode             victories = case EM.lookup gameModeId svictories of               Nothing -> 0@@ -489,23 +502,13 @@             markMode t = if victories > 0                          then T.snoc (T.init t) '>'                          else t-            !tSlot = markMode $ slotLabel c-            !lenSlot = 2 * T.length tSlot-            !tBlurb = " " <> modeName-            !lenButton = lenSlot + T.length tBlurb-            !pButton = PointUI 0 y-            !widthButton = ButtonWidth propFont lenButton-        in ( textToAL tSlot-           , (lenSlot, textToAL tBlurb)-           , (Right c, (pButton, widthButton)) )-      (plLab, plDesc, kxs) =-        unzip3 $ zipWith prSlot (zip [0..] allSlots) campaignModes-      placeLab = EM.singleton squareFont $ offsetOverlay plLab-      placeDesc = EM.singleton propFont $ offsetOverlayX plDesc-  return (EM.unionWith (++) placeLab placeDesc, kxs)+            !tLab = markMode $ slotLabel c+            !tDesc = " " <> modeName+        in (textToAS tLab, textToAS tDesc, Right c)+      l = zipWith prSlot allSlots campaignModes+  return $! labDescOKX squareFont propFont l -pickNumber :: (MonadClient m, MonadClientUI m)-           => Bool -> Int -> m (Either MError Int)+pickNumber :: MonadClientUI m => Bool -> Int -> m (Either MError Int) pickNumber askNumber kAll = assert (kAll >= 1) $ do   let shownKeys = [ K.returnKM, K.spaceKM, K.mkChar '+', K.mkChar '-'                   , K.backspaceKM, K.escKM ]@@ -544,16 +547,16 @@ lookAtTile :: MonadClientUI m            => Bool             -- ^ can be seen right now?            -> Point            -- ^ position to describe-           -> ActorId          -- ^ the actor that looks            -> LevelId          -- ^ level the position is at+           -> Maybe ActorId    -- ^ the actor that looks            -> Maybe MU.Person  -- ^ grammatical person of the item(s), if any            -> m (Text, Text, [(Int, MU.Part)])-lookAtTile canSee p aid lidV mperson = do+lookAtTile canSee p lidV maid mperson = do   CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui   cops@COps{cotile, coplace} <- getsState scops   side <- getsClient sside   factionD <- getsState sfactionD-  b <- getsState $ getActorBody aid+  mb <- getsState $ \s -> flip getActorBody s <$> maid   lvl <- getLevel lidV   saimMode <- getsSession saimMode   embeds <- getsState $ getEmbedBag lidV p@@ -563,11 +566,12 @@   getKind <- getsState $ flip getIidKind   let inhabitants = posToAidsLvl p lvl       detail = maybe DetailAll detailLevel saimMode-      aims = isJust $ makeLine False b p seps cops lvl+      aims = isJust $ maybe Nothing (\b -> makeLine False b p seps cops lvl) mb       tkid = lvl `at` p       tile = okind cotile tkid       vis | TK.tname tile == "unknown space" = "that is"-          | not (null inhabitants) && bpos b /= p = "the terrain here is"+          | not (null inhabitants)+            && (bpos <$> mb) /= Just p = "the terrain here is"           | not canSee = "you remember"           | not aims = "you are aware of"  -- walkable path a proxy for in LOS           | otherwise = "you see"@@ -720,40 +724,49 @@ lookAtItems :: MonadClientUI m             => Bool     -- ^ can be seen right now?             -> Point    -- ^ position to describe-            -> ActorId  -- ^ the actor that looks+            -> LevelId  -- ^ level the position is at+            -> Maybe ActorId+                        -- ^ the actor that looks             -> Maybe (MU.Part, Bool)                         -- ^ pronoun for the big actor at the position, if any,                         --   and whether the big actor is alive             -> m (Text, Maybe MU.Person)-lookAtItems canSee p aid mactorPronounAlive = do+lookAtItems canSee p lidV maid mactorPronounAlive = do   CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui   side <- getsClient sside   itemToF <- getsState $ flip itemToFull-  b <- getsState $ getActorBody aid+  mb <- getsState $ \s -> flip getActorBody s <$> maid   -- Not using @viewedLevelUI@, because @aid@ may be temporarily not a leader.   saimMode <- getsSession saimMode-  let lidV = maybe (blid b) aimLevelId saimMode-      standingOn = p == bpos b && lidV == blid b+  let standingOn = Just p == (bpos <$> mb) && Just lidV == (blid <$> mb)       -- In exploration mode the detail level depends on whether the actor       -- that looks stand over the items, because then he can check details       -- with inventory commands (or look in aiming mode).-      detailExploration =-        if standingOn && bfid b == side then DetailMedium else DetailAll+      detailExploration = if standingOn && Just side == (bfid <$> mb)+                          then DetailMedium+                          else DetailAll       detail = maybe detailExploration detailLevel saimMode   localTime <- getsState $ getLocalTime lidV-  subjectAid <- partActorLeader aid   is <- getsState $ getFloorBag lidV p   factionD <- getsState sfactionD   globalTime <- getsState stime   getKind <- getsState $ flip getIidKindId-  leaderPronoun <- partPronounLeader aid-  let mLeader = if standingOn then Just (leaderPronoun, bhp b >= 0) else Nothing-      mactorPronounAliveLeader = maybe mLeader Just mactorPronounAlive-      (subject, verb) = case mactorPronounAliveLeader of-        Just (actorPronoun, actorAlive) ->-          (actorPronoun, if actorAlive then "stand over" else "fall over")-        Nothing -> (subjectAid, if canSee then "notice" else "remember")-      nWs (iid, kit@(k, _)) =+  mLeader <- case maid of+    Just aid | standingOn -> do+      leaderPronoun <- partPronounLeader aid+      return $ Just (leaderPronoun, (bhp <$> mb) >= Just 0)+    _ -> return Nothing+  let mactorPronounAliveLeader = maybe mLeader Just mactorPronounAlive+  (subject, verb) <- case mactorPronounAliveLeader of+    Just (actorPronoun, actorAlive) ->+      return (actorPronoun, if actorAlive then "stand over" else "fall over")+    Nothing -> case maid of+      Just aid -> do+        subjectAid <- partActorLeader aid+        return (subjectAid, if canSee then "notice" else "remember")+      Nothing ->+        return ("one", if canSee then "can see" else "may remember")+  let nWs (iid, kit@(k, _)) =         partItemWsDetail detail                          rwidth side factionD k localTime (itemToF iid) kit       (object, person) = case EM.assocs is of@@ -773,8 +786,8 @@            else makeSentence [MU.SubjectVerbSg subject (MU.Text verb), object]          , if isNothing mactorPronounAlive then Just person else Nothing ) -lookAtStash :: MonadClientUI m => LevelId -> Point -> m Text-lookAtStash lidV p = do+lookAtStash :: MonadClientUI m => Point -> LevelId -> m Text+lookAtStash p lidV = do   side <- getsClient sside   factionD <- getsState sfactionD   let locateStash (fid, fact) = case gstash fact of@@ -789,23 +802,25 @@ -- | Produces a textual description of everything at the requested -- level's position. lookAtPosition :: MonadClientUI m-               => LevelId -> Point -> m [(MsgClassShow, Text)]-lookAtPosition lidV p = do+               => Point -> LevelId -> m [(MsgClassShow, Text)]+lookAtPosition p lidV = do   COps{cotile} <- getsState scops   side <- getsClient sside-  leader <- getLeaderUI   per <- getPerFid lidV   let canSee = ES.member p (totalVisible per)   (actorsBlurb, mactorPronounAlive, actorsDesc) <- lookAtActors p lidV-  (itemsBlurb, mperson) <- lookAtItems canSee p leader mactorPronounAlive+  mleader <- getsClient sleader+  (itemsBlurb, mperson) <-+    lookAtItems canSee p lidV mleader mactorPronounAlive   let tperson = if T.null itemsBlurb then Nothing else mperson-  (tileBlurb, placeBlurb, embedsList) <- lookAtTile canSee p leader lidV tperson+  (tileBlurb, placeBlurb, embedsList) <-+    lookAtTile canSee p lidV mleader tperson   inhabitants <- getsState $ posToAidAssocs p lidV   let actorMsgClass =         if (bfid . snd <$> inhabitants) == [side]         then MsgPromptGeneric  -- our single proj or non-proj; tame         else MsgPromptActors-  stashBlurb <- lookAtStash lidV p+  stashBlurb <- lookAtStash p lidV   lvl@Level{lsmell, ltime} <- getLevel lidV   saimMode <- getsSession saimMode   let detail = maybe DetailAll detailLevel saimMode@@ -889,30 +904,41 @@             then [(MsgPromptFocus, tileBlurb)]             else ms -displayItemLore :: (MonadClient m, MonadClientUI m)+displayItemLore :: MonadClientUI m                 => ItemBag -> Int -> (ItemId -> ItemFull -> Int -> Text) -> Int-                -> SingleItemSlots-                -> m Bool-displayItemLore itemBag meleeSkill promptFun slotIndex lSlots = do-  km <- displayItemLorePointedAt itemBag meleeSkill promptFun slotIndex-                                 lSlots False+                -> SingleItemSlots -> Bool+                -> m K.KM+displayItemLore itemBag meleeSkill promptFun slotIndex lSlots addTilde = do+  CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui+  FontSetup{propFont} <- getFontSetup+  let lSlotsElems = EM.elems lSlots+      lSlotsBound = length lSlotsElems - 1+  let keys = [K.spaceKM, K.escKM]+             ++ [K.mkChar '~' | addTilde]+             ++ [K.upKM | slotIndex /= 0]+             ++ [K.downKM | slotIndex /= lSlotsBound]+  okx <- okxItemLorePointedAt+           propFont rwidth False itemBag meleeSkill promptFun slotIndex lSlots+  slides <- overlayToSlideshow (rheight - 2) keys okx+  km <- getConfirms ColorFull keys slides   case K.key km of-    K.Space -> return True-    K.Esc -> return False-    _ -> error $ "" `showFailure` km+    K.Up -> displayItemLore itemBag meleeSkill promptFun (slotIndex - 1)+                            lSlots addTilde+    K.Down -> displayItemLore itemBag meleeSkill promptFun (slotIndex + 1)+                              lSlots addTilde+    _ -> return km -displayItemLorePointedAt-  :: (MonadClient m, MonadClientUI m)-  => ItemBag -> Int -> (ItemId -> ItemFull -> Int -> Text) -> Int-  -> SingleItemSlots -> Bool-  -> m K.KM-displayItemLorePointedAt itemBag meleeSkill promptFun slotIndex-                         lSlots addTilde = do-  CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui+okxItemLorePointedAt :: MonadClientUI m+                     => DisplayFont -> Int -> Bool -> ItemBag -> Int+                     -> (ItemId -> ItemFull -> Int -> Text)+                     -> Int -> SingleItemSlots+                     -> m OKX+okxItemLorePointedAt descFont width inlineMsg itemBag meleeSkill promptFun+                     slotIndex lSlots = do+  FontSetup{squareFont} <- getFontSetup   side <- getsClient sside   arena <- getArenaUI   let lSlotsElems = EM.elems lSlots-      lSlotsBound = length lSlotsElems - 1       iid2 = lSlotsElems !! slotIndex       kit2@(k, _) = itemBag EM.! iid2   itemFull2 <- getsState $ itemToFull iid2@@ -920,67 +946,25 @@   factionD <- getsState sfactionD   -- The hacky level 0 marks items never seen, but sent by server at gameover.   jlid <- getsSession $ fromMaybe (toEnum 0) <$> EM.lookup iid2 . sitemUI-  FontSetup{..} <- getFontSetup-  let descAl = itemDesc rwidth True side factionD meleeSkill+  let descAs = itemDesc width True side factionD meleeSkill                         CGround localTime jlid itemFull2 kit2-      (descSymAl, descBlurbAl) = span (/= Color.spaceAttrW32) descAl-      descSym = offsetOverlay $ splitAttrString rwidth rwidth descSymAl-      descBlurb = offsetOverlayX $-        case splitAttrString rwidth rwidth $ stringToAS "xx" ++ descBlurbAl of-          [] -> error "splitting AttrString loses characters"-          al1 : rest ->-            (2, attrStringToAL $ drop 2 $ attrLine al1) : map (0,) rest-      ov = EM.insertWith (++) squareFont descSym-           $ EM.singleton propFont descBlurb-      keys = [K.spaceKM, K.escKM]-             ++ [K.mkChar '~' | addTilde]-             ++ [K.upKM | slotIndex /= 0]-             ++ [K.downKM | slotIndex /= lSlotsBound]-  msgAdd MsgPromptGeneric $ promptFun iid2 itemFull2 k-  slides <- overlayToSlideshow (rheight - 2) keys (ov, [])-  km <- getConfirms ColorFull keys slides-  case K.key km of-    K.Up ->-      displayItemLorePointedAt itemBag meleeSkill promptFun (slotIndex - 1)-                               lSlots addTilde-    K.Down ->-      displayItemLorePointedAt itemBag meleeSkill promptFun (slotIndex + 1)-                               lSlots addTilde-    _ -> return km--viewLoreItems :: (MonadClient m, MonadClientUI m)-              => String -> SingleItemSlots -> ItemBag -> Text-              -> (Int -> SingleItemSlots -> m Bool) -> Bool-              -> m K.KM-viewLoreItems menuName lSlotsRaw trunkBag prompt examItem displayRanged = do-  CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui-  arena <- getArenaUI-  itemToF <- getsState $ flip itemToFull-  let keysPre = [K.spaceKM, K.mkChar '<', K.mkChar '>', K.escKM]-      lSlots = sortSlotMap itemToF lSlotsRaw-  msgAdd MsgPromptGeneric prompt-  io <- itemOverlay lSlots arena trunkBag displayRanged-  itemSlides <- overlayToSlideshow (rheight - 2) keysPre io-  let keyOfEKM (Left km) = km-      keyOfEKM (Right SlotChar{slotChar}) = [K.mkChar slotChar]-      allOKX = concatMap snd $ slideshow itemSlides-      keysMain = keysPre ++ concatMap (keyOfEKM . fst) allOKX-      viewAtSlot slot = do-        let ix0 = fromMaybe (error $ show slot)-                            (findIndex (== slot) $ EM.keys lSlots)-        go2 <- examItem ix0 lSlots-        if go2-        then viewLoreItems menuName lSlots trunkBag prompt-                           examItem displayRanged-        else return K.escKM-  ekm <- displayChoiceScreen menuName ColorFull False itemSlides keysMain-  case ekm of-    Left km | km `elem` [K.spaceKM, K.mkChar '<', K.mkChar '>', K.escKM] ->-      return km-    Left K.KM{key=K.Char l} -> viewAtSlot $ SlotChar 0 l-      -- other prefixes are not accessible via keys; tough luck; waste of effort-    Left km -> error $ "" `showFailure` km-    Right slot -> viewAtSlot slot+      (ovLab, ovDesc) = labDescOverlay squareFont width descAs+      prompt = promptFun iid2 itemFull2 k+      promptBlurb | T.null prompt = []+                  | otherwise = offsetOverlay $ splitAttrString width width+                                $ textFgToAS Color.Brown $ prompt <> "\n\n"+  (descSym2, descBlurb2) <-+    if inlineMsg+    then do+      let len = length promptBlurb+      return ( ytranslateOverlay len ovLab+             , promptBlurb ++ ytranslateOverlay len ovDesc )+    else do+      msgAdd MsgPromptGeneric prompt+      return (ovLab, ovDesc)+  let ov = EM.insertWith (++) squareFont descSym2+           $ EM.singleton descFont descBlurb2+  return (ov, [])  cycleLore :: MonadClientUI m => [m K.KM] -> [m K.KM] -> m () cycleLore _ [] = return ()
engine-src/Game/LambdaHack/Client/UI/HandleHumanGlobalM.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE TupleSections #-} -- | Semantics of "Game.LambdaHack.Client.UI.HumanCmd" -- client commands that return server requests. -- A couple of them do not take time, the rest does.@@ -105,12 +104,13 @@ import qualified Game.LambdaHack.Definition.Ability as Ability import qualified Game.LambdaHack.Definition.Color as Color import           Game.LambdaHack.Definition.Defs+import qualified Game.LambdaHack.Definition.DefsInternal as DefsInternal  -- * ByArea  -- | Pick command depending on area the mouse pointer is in. -- The first matching area is chosen. If none match, only interrupt.-byAreaHuman :: (MonadClient m, MonadClientUI m)+byAreaHuman :: MonadClientUI m             => (K.KM -> HumanCmd -> m (Either MError ReqUI))             -> [(CmdArea, HumanCmd)]             -> m (Either MError ReqUI)@@ -122,7 +122,7 @@                       -- for the whole UI screen in square font coordinates       pointerInArea a = do         rs <- areaToRectangles a-        return $! any (inside p) $ catMaybes rs+        return $! any (flip inside p) $ catMaybes rs   cmds <- filterM (pointerInArea . fst) l   case cmds of     [] -> do@@ -141,18 +141,20 @@  case ca of   CaMessage -> return [(0, 0, rwidth - 1, 0)]   CaMapLeader -> do  -- takes preference over @CaMapParty@ and @CaMap@-    leader <- getLeaderUI-    b <- getsState $ getActorBody leader-    let PointSquare x y = mapToSquare $ bpos b-    return [(x, y, x, y)]+    mleader <- getsClient sleader+    case mleader of+      Nothing -> return []+      Just leader -> do+        b <- getsState $ getActorBody leader+        let PointSquare x y = mapToSquare $ bpos b+        return [(x, y, x, y)]   CaMapParty -> do  -- takes preference over @CaMap@     lidV <- viewedLevelUI     side <- getsClient sside     ours <- getsState $ filter (not . bproj) . map snd                         . actorAssocs (== side) lidV-    let rectFromB p =-          let PointSquare x y = mapToSquare p-          in (x, y, x, y)+    let rectFromB p = let PointSquare x y = mapToSquare p+                      in (x, y, x, y)     return $! map (rectFromB . bpos) ours   CaMap ->     let PointSquare xo yo = mapToSquare originPoint@@ -258,14 +260,19 @@                     -> m (Either MError ReqUI) executeIfClearHuman c1 = do   sreportNull <- getsSession sreportNull-  if sreportNull then c1 else return $ Left Nothing+  sreqDelay <- getsSession sreqDelay+  -- When server query delay is handled, don't complicate things by clearing+  -- screen instead of running the command.+  if sreportNull || sreqDelay == ReqDelayHandled+  then c1+  else return $ Left Nothing  -- * Wait  -- | Leader waits a turn (and blocks, etc.).-waitHuman :: MonadClientUI m => m (FailOrCmd RequestTimed)-waitHuman = do-  actorCurAndMaxSk <- leaderSkillsClientUI+waitHuman :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed)+waitHuman leader = do+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   if Ability.getSk Ability.SkWait actorCurAndMaxSk > 0 then do     modifySession $ \sess -> sess {swaitTimes = abs (swaitTimes sess) + 1}     return $ Right ReqWait@@ -274,9 +281,9 @@ -- * Wait10  -- | Leader waits a 1/10th of a turn (and doesn't block, etc.).-waitHuman10 :: MonadClientUI m => m (FailOrCmd RequestTimed)-waitHuman10 = do-  actorCurAndMaxSk <- leaderSkillsClientUI+waitHuman10 :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed)+waitHuman10 leader = do+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   if Ability.getSk Ability.SkWait actorCurAndMaxSk >= 4 then do     modifySession $ \sess -> sess {swaitTimes = abs (swaitTimes sess) + 1}     return $ Right ReqWait10@@ -285,9 +292,9 @@ -- * Yell  -- | Leader yells or yawns, if sleeping.-yellHuman :: MonadClientUI m => m (FailOrCmd RequestTimed)-yellHuman = do-  actorCurAndMaxSk <- leaderSkillsClientUI+yellHuman :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed)+yellHuman leader = do+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   if Ability.getSk Ability.SkWait actorCurAndMaxSk > 0      -- If waiting drained and really, potentially, no other possible action,      -- still allow yelling.@@ -300,11 +307,10 @@ -- * MoveDir and RunDir  moveRunHuman :: (MonadClient m, MonadClientUI m)-             => Bool -> Bool -> Bool -> Bool -> Vector+             => ActorId -> Bool -> Bool -> Bool -> Bool -> Vector              -> m (FailOrCmd RequestTimed)-moveRunHuman initialStep finalGoal run runAhead dir = do-  leader <- getLeaderUI-  actorCurAndMaxSk <- leaderSkillsClientUI+moveRunHuman leader initialStep finalGoal run runAhead dir = do+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   arena <- getArenaUI   sb <- getsState $ getActorBody leader   fact <- getsState $ (EM.! bfid sb) . sfactionD@@ -337,7 +343,7 @@   tgts <- getsState $ posToAidAssocs tpos arena   case tgts of     [] -> do  -- move or search or alter-      runStopOrCmd <- moveSearchAlter run dir+      runStopOrCmd <- moveSearchAlter leader run dir       case runStopOrCmd of         Left stopMsg -> return $ Left stopMsg         Right runCmd -> do@@ -350,7 +356,7 @@                     && Ability.getSk Ability.SkDisplace actorCurAndMaxSk > 0 ->       -- No @stopPlayBack@: initial displace is benign enough.       -- Displacing requires accessibility, but it's checked later on.-      displaceAid target+      displaceAid leader target     _ : _ : _ | run                 && initialStep                 && Ability.getSk Ability.SkDisplace actorCurAndMaxSk > 0 ->@@ -372,15 +378,14 @@       if Ability.getSk Ability.SkMelee actorCurAndMaxSk > 0       then -- No problem if there are many projectiles at the spot. We just            -- attack the first one.-           meleeAid target+           meleeAid leader target       else failSer MeleeUnskilled     _ : _ -> failWith "actor in the way"  -- | Actor attacks an enemy actor or his own projectile. meleeAid :: (MonadClient m, MonadClientUI m)-         => ActorId -> m (FailOrCmd RequestTimed)-meleeAid target = do-  leader <- getLeaderUI+         => ActorId -> ActorId -> m (FailOrCmd RequestTimed)+meleeAid leader target = do   side <- getsClient sside   tb <- getsState $ getActorBody target   sfact <- getsState $ (EM.! side) . sfactionD@@ -414,10 +419,9 @@  -- | Actor swaps position with another. displaceAid :: MonadClientUI m-            => ActorId -> m (FailOrCmd RequestTimed)-displaceAid target = do+            => ActorId -> ActorId -> m (FailOrCmd RequestTimed)+displaceAid leader target = do   COps{coTileSpeedup} <- getsState scops-  leader <- getLeaderUI   sb <- getsState $ getActorBody leader   tb <- getsState $ getActorBody target   tfact <- getsState $ (EM.! bfid tb) . sfactionD@@ -451,12 +455,11 @@        else failSer DisplaceAccess  -- | Leader moves or searches or alters. No visible actor at the position.-moveSearchAlter :: (MonadClient m, MonadClientUI m)-                => Bool -> Vector -> m (FailOrCmd RequestTimed)-moveSearchAlter run dir = do+moveSearchAlter :: MonadClientUI m+                => ActorId -> Bool -> Vector -> m (FailOrCmd RequestTimed)+moveSearchAlter leader run dir = do   COps{coTileSpeedup} <- getsState scops-  leader <- getLeaderUI-  actorCurAndMaxSk <- leaderSkillsClientUI+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   sb <- getsState $ getActorBody leader   let moveSkill = Ability.getSk Ability.SkMove actorCurAndMaxSk       spos = bpos sb           -- source position@@ -477,24 +480,23 @@       setXHairFromGUI sxhair       if run then do         -- Explicit request to examine the terrain.-        blurb <- lookAtPosition (blid sb) tpos+        blurb <- lookAtPosition tpos (blid sb)         mapM_ (uncurry msgAdd) blurb         failWith $ "the terrain is" <+>           if | Tile.isModifiable coTileSpeedup t -> "potentially modifiable"              | alterable -> "potentially triggerable"              | otherwise -> "completely inert"-      else alterCommon True tpos+      else alterCommon leader True tpos   return $! runStopOrCmd -alterCommon :: (MonadClient m, MonadClientUI m)-            => Bool -> Point -> m (FailOrCmd RequestTimed)-alterCommon bumping tpos = do+alterCommon :: MonadClientUI m+            => ActorId -> Bool -> Point -> m (FailOrCmd RequestTimed)+alterCommon leader bumping tpos = do   CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui   cops@COps{cotile, coTileSpeedup} <- getsState scops   side <- getsClient sside   factionD <- getsState sfactionD-  leader <- getLeaderUI-  actorCurAndMaxSk <- leaderSkillsClientUI+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   sb <- getsState $ getActorBody leader   let alterSkill = Ability.getSk Ability.SkAlter actorCurAndMaxSk       spos = bpos sb@@ -532,7 +534,7 @@        && not underFeet        && alterSkill < Tile.alterMinSkill coTileSpeedup t -> do          -- Rather rare (requires high skill), so describe the tile.-         blurb <- lookAtPosition (blid sb) tpos+         blurb <- lookAtPosition tpos (blid sb)          mapM_ (uncurry msgAdd) blurb          modificationFailureHint          failSer AlterUnwalked@@ -544,13 +546,13 @@          -- Don't mislead describing terrain, if other actor is to blame.          failSer AlterBlockActor      | otherwise -> do  -- promising-         verAlters <- verifyAlters bumping leader tpos+         verAlters <- verifyAlters leader bumping tpos          case verAlters of            Right () ->              if bumping then                return $ Right $ ReqMove $ vectorToFrom tpos spos              else do-               msgAddDone tpos "modify"+               msgAddDone leader tpos "modify"                return $ Right $ ReqAlter tpos            Left err -> return $ Left err          -- Even when bumping, we don't use ReqMove, because we don't want@@ -567,12 +569,11 @@  -- * RunOnceAhead -runOnceAheadHuman :: (MonadClient m, MonadClientUI m)-                  => m (Either MError RequestTimed)-runOnceAheadHuman = do+runOnceAheadHuman :: MonadClientUI m+                  => ActorId -> m (Either MError RequestTimed)+runOnceAheadHuman leader = do   side <- getsClient sside   fact <- getsState $ (EM.! side) . sfactionD-  leader <- getLeaderUI   keyPressed <- anyKeyPressed   srunning <- getsSession srunning   -- When running, stop if disturbed. If not running, stop at once.@@ -601,41 +602,49 @@ -- * MoveOnceToXhair  moveOnceToXhairHuman :: (MonadClient m, MonadClientUI m)-                     => m (FailOrCmd RequestTimed)-moveOnceToXhairHuman = goToXhair True False+                     => ActorId -> m (FailOrCmd RequestTimed)+moveOnceToXhairHuman leader = goToXhair leader True False  goToXhair :: (MonadClient m, MonadClientUI m)-          => Bool -> Bool -> m (FailOrCmd RequestTimed)-goToXhair initialStep run = do+          => ActorId -> Bool -> Bool -> m (FailOrCmd RequestTimed)+goToXhair leader initialStep run = do   aimMode <- getsSession saimMode   -- Movement is legal only outside aiming mode.   if isJust aimMode   then failWith "cannot move in aiming mode"-  else goToXhairExplorationMode initialStep run+  else goToXhairExplorationMode leader initialStep run  goToXhairExplorationMode :: (MonadClient m, MonadClientUI m)-                         => Bool -> Bool -> m (FailOrCmd RequestTimed)-goToXhairExplorationMode initialStep run = do-  xhair <- getsSession sxhair-  xhairGoTo <- getsSession sxhairGoTo-  mfail <--    if not (isNothing xhairGoTo) && xhairGoTo /= xhair-    then failWith "crosshair position changed"-    else do-      when (isNothing xhairGoTo) $  -- set it up for next steps-        modifySession $ \sess -> sess {sxhairGoTo = xhair}-      goToXhairGoTo initialStep run-  when (isLeft mfail) $-    modifySession $ \sess -> sess {sxhairGoTo = Nothing}-  return mfail+                         => ActorId -> Bool -> Bool+                         -> m (FailOrCmd RequestTimed)+goToXhairExplorationMode leader initialStep run = do+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader+  sb <- getsState $ getActorBody leader+  let moveSkill = Ability.getSk Ability.SkMove actorCurAndMaxSk+  -- If skill is too low, no path in @Bfs@ is going to be found,+  -- but we check the skill (and sleep) to give a more accurate message.+  if | moveSkill > 0 -> do+       xhair <- getsSession sxhair+       xhairGoTo <- getsSession sxhairGoTo+       mfail <-+         if not (isNothing xhairGoTo) && xhairGoTo /= xhair+         then failWith "crosshair position changed"+         else do+           when (isNothing xhairGoTo) $  -- set it up for next steps+             modifySession $ \sess -> sess {sxhairGoTo = xhair}+           goToXhairGoTo leader initialStep run+       when (isLeft mfail) $+         modifySession $ \sess -> sess {sxhairGoTo = Nothing}+       return mfail+     | bwatch sb == WSleep -> failSer MoveUnskilledAsleep+     | otherwise -> failSer MoveUnskilled  goToXhairGoTo :: (MonadClient m, MonadClientUI m)-              => Bool -> Bool -> m (FailOrCmd RequestTimed)-goToXhairGoTo initialStep run = do-  leader <- getLeaderUI+              => ActorId -> Bool -> Bool -> m (FailOrCmd RequestTimed)+goToXhairGoTo leader initialStep run = do   b <- getsState $ getActorBody leader-  xhairPos <- xhairToPos-  case xhairPos of+  mxhairPos <- mxhairToPos+  case mxhairPos of     Nothing -> failWith "crosshair position invalid"     Just c -> do       running <- getsSession srunning@@ -647,7 +656,7 @@           case runOutcome of             Left stopMsg -> return $ Left stopMsg             Right (finalGoal, dir) ->-              moveRunHuman initialStep finalGoal run False dir+              moveRunHuman leader initialStep finalGoal run False dir         _ | c == bpos b -> failWith "position reached"         _ -> do           let !_A = assert (initialStep || not run) ()@@ -659,13 +668,13 @@                 "no route to crosshair (press again to go there anyway)"             _ | initialStep && adjacent (bpos b) c -> do               let dir = towards (bpos b) c-              moveRunHuman initialStep True run False dir+              moveRunHuman leader initialStep True run False dir             Nothing -> failWith "no route to crosshair"             Just AndPath{pathList=[]} -> failWith "almost there"             Just AndPath{pathList = p1 : _} -> do               let finalGoal = p1 == c                   dir = towards (bpos b) p1-              moveRunHuman initialStep finalGoal run False dir+              moveRunHuman leader initialStep finalGoal run False dir  multiActorGoTo :: (MonadClient m, MonadClientUI m)                => LevelId -> Point -> RunParams -> m (FailOrCmd (Bool, Vector))@@ -675,8 +684,8 @@     RunParams{runMembers = r : rs, runWaiting} -> do       onLevel <- getsState $ memActor r arena       b <- getsState $ getActorBody r-      xhairPos <- xhairToPos-      if not onLevel || xhairPos == Just (bpos b) then do+      mxhairPos <- mxhairToPos+      if not onLevel || mxhairPos == Just (bpos b) then do         let paramNew = paramOld {runMembers = rs}         multiActorGoTo arena c paramNew       else do@@ -710,38 +719,37 @@ -- * RunOnceToXhair  runOnceToXhairHuman :: (MonadClient m, MonadClientUI m)-                    => m (FailOrCmd RequestTimed)-runOnceToXhairHuman = goToXhair True True+                    => ActorId -> m (FailOrCmd RequestTimed)+runOnceToXhairHuman leader = goToXhair leader True True  -- * ContinueToXhair  continueToXhairHuman :: (MonadClient m, MonadClientUI m)-                     => m (FailOrCmd RequestTimed)-continueToXhairHuman = goToXhair False False{-irrelevant-}+                     => ActorId -> m (FailOrCmd RequestTimed)+continueToXhairHuman leader = goToXhair leader False False{-irrelevant-}  -- * MoveItem -moveItemHuman :: forall m. (MonadClient m, MonadClientUI m)-              => [CStore] -> CStore -> Maybe Text -> Bool+moveItemHuman :: forall m. MonadClientUI m+              => ActorId -> [CStore] -> CStore -> Maybe Text -> Bool               -> m (FailOrCmd RequestTimed)-moveItemHuman stores destCStore mverb auto = do+moveItemHuman leader stores destCStore mverb auto = do   let !_A = assert (destCStore `notElem` stores) ()-  actorCurAndMaxSk <- leaderSkillsClientUI+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   if Ability.getSk Ability.SkMoveItem actorCurAndMaxSk > 0-  then moveOrSelectItem stores destCStore mverb auto+  then moveOrSelectItem leader stores destCStore mverb auto   else failSer MoveItemUnskilled  -- This cannot be structured as projecting or applying, with @ByItemMode@ -- and @ChooseItemToMove@, because at least in case of grabbing items, -- more than one item is chosen, which doesn't fit @sitemSel@. Separating -- grabbing of multiple items as a distinct command is too high a price.-moveOrSelectItem :: forall m. (MonadClient m, MonadClientUI m)-                 => [CStore] -> CStore -> Maybe Text -> Bool+moveOrSelectItem :: forall m. MonadClientUI m+                 => ActorId -> [CStore] -> CStore -> Maybe Text -> Bool                  -> m (FailOrCmd RequestTimed)-moveOrSelectItem storesRaw destCStore mverb auto = do-  leader <- getLeaderUI+moveOrSelectItem leader storesRaw destCStore mverb auto = do   b <- getsState $ getActorBody leader-  actorCurAndMaxSk <- leaderSkillsClientUI+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b   let calmE = calmEnough b actorCurAndMaxSk       overStash = mstash == Just (blid b, bpos b)@@ -767,7 +775,7 @@       bag <- getsState $ getBodyStoreBag b fromCStore       case iid `EM.lookup` bag of         Nothing ->  -- the case of old selection or selection from another actor-          moveOrSelectItem stores destCStore mverb auto+          moveOrSelectItem leader stores destCStore mverb auto         Just (k, it) -> assert (k > 0) $ do           let eqpFree = eqpFreeN b               kToPick | destCStore == CEqp = min eqpFree k@@ -778,31 +786,31 @@              | otherwise -> do                socK <- pickNumber (not auto) kToPick                case socK of-                 Left Nothing -> moveOrSelectItem stores destCStore mverb auto+                 Left Nothing ->+                   moveOrSelectItem leader stores destCStore mverb auto                  Left (Just err) -> return $ Left err                  Right kChosen ->                    let is = (fromCStore, [(iid, (kChosen, take kChosen it))])-                   in Right <$> moveItems stores is destCStore+                   in Right <$> moveItems leader stores is destCStore     _ -> do-      mis <- selectItemsToMove stores destCStore mverb auto+      mis <- selectItemsToMove leader stores destCStore mverb auto       case mis of         Left err -> return $ Left err         Right (fromCStore, [(iid, _)]) | stores /= [CGround] -> do           modifySession $ \sess ->             sess {sitemSel = Just (iid, fromCStore, False)}-          moveOrSelectItem stores destCStore mverb auto+          moveOrSelectItem leader stores destCStore mverb auto         Right is@(fromCStore, _) ->           if | fromCStore == CEqp && not calmE -> failSer ItemNotCalm              | fromCStore == CGround && overStash -> failSer ItemOverStash-             | otherwise -> Right <$> moveItems stores is destCStore+             | otherwise -> Right <$> moveItems leader stores is destCStore -selectItemsToMove :: forall m. (MonadClient m, MonadClientUI m)-                  => [CStore] -> CStore -> Maybe Text -> Bool+selectItemsToMove :: forall m. MonadClientUI m+                  => ActorId -> [CStore] -> CStore -> Maybe Text -> Bool                   -> m (FailOrCmd (CStore, [(ItemId, ItemQuant)]))-selectItemsToMove stores destCStore mverb auto = do+selectItemsToMove leader stores destCStore mverb auto = do   let verb = fromMaybe (verbCStore destCStore) mverb-  leader <- getLeaderUI-  actorCurAndMaxSk <- leaderSkillsClientUI+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   b <- getsState $ getActorBody leader   mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b   lastItemMove <- getsSession slastItemMove@@ -859,7 +867,7 @@                     IA.goesIntoEqp $ aspectRecordFull itemFull)              else (prompt, return SuitsEverything)        ggi <--         getFull psuit+         getFull leader psuit                  (\body _ actorSk cCur _ ->                     prompt <+> ppItemDialogBody body actorSk cCur)                  (\body _ actorSk cCur _ ->@@ -872,13 +880,12 @@            return $ Right (fromCStore, l)          Left err -> failWith err -moveItems :: forall m. (MonadClient m, MonadClientUI m)-          => [CStore] -> (CStore, [(ItemId, ItemQuant)]) -> CStore+moveItems :: forall m. MonadClientUI m+          => ActorId -> [CStore] -> (CStore, [(ItemId, ItemQuant)]) -> CStore           -> m RequestTimed-moveItems stores (fromCStore, l) destCStore = do+moveItems leader stores (fromCStore, l) destCStore = do   let !_A = assert (fromCStore /= destCStore && fromCStore `elem` stores) ()-  leader <- getLeaderUI-  actorCurAndMaxSk <- leaderSkillsClientUI+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   b <- getsState $ getActorBody leader   discoBenefit <- getsClient sdiscoBenefit   let calmE = calmEnough b actorCurAndMaxSk@@ -932,10 +939,11 @@  -- * Project -projectHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd RequestTimed)-projectHuman = do+projectHuman :: (MonadClient m, MonadClientUI m)+             => ActorId -> m (FailOrCmd RequestTimed)+projectHuman leader = do   curChal <- getsClient scurChal-  actorCurAndMaxSk <- leaderSkillsClientUI+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   if | ckeeper curChal ->        failSer ProjectFinderKeeper      | Ability.getSk Ability.SkProject actorCurAndMaxSk <= 0 ->@@ -946,7 +954,6 @@        case itemSel of          Just (_, COrgan, _) -> failWith "can't fling an organ"          Just (iid, fromCStore, _) -> do-           leader <- getLeaderUI            b <- getsState $ getActorBody leader            bag <- getsState $ getBodyStoreBag b fromCStore            case iid `EM.lookup` bag of@@ -954,20 +961,19 @@              Just _kit -> do                itemFull <- getsState $ itemToFull iid                let i = (fromCStore, (iid, itemFull))-               projectItem i+               projectItem leader i          Nothing -> failWith "no item to fling"  projectItem :: (MonadClient m, MonadClientUI m)-            => (CStore, (ItemId, ItemFull))+            => ActorId -> (CStore, (ItemId, ItemFull))             -> m (FailOrCmd RequestTimed)-projectItem (fromCStore, (iid, itemFull)) = do-  leader <- getLeaderUI-  actorCurAndMaxSk <- leaderSkillsClientUI+projectItem leader (fromCStore, (iid, itemFull)) = do+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   b <- getsState $ getActorBody leader   let calmE = calmEnough b actorCurAndMaxSk   if fromCStore == CEqp && not calmE then failSer ItemNotCalm   else do-    mpsuitReq <- psuitReq+    mpsuitReq <- psuitReq leader     case mpsuitReq of       Left err -> failWith err       Right psuitReqFun ->@@ -994,9 +1000,9 @@  -- * Apply -applyHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd RequestTimed)-applyHuman = do-  actorCurAndMaxSk <- leaderSkillsClientUI+applyHuman :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed)+applyHuman leader = do+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   if Ability.getSk Ability.SkApply                    actorCurAndMaxSk <= 0 then  -- detailed check later     failSer ApplyUnskilled@@ -1004,29 +1010,28 @@     itemSel <- getsSession sitemSel     case itemSel of       Just (iid, fromCStore, _) -> do-        leader <- getLeaderUI         b <- getsState $ getActorBody leader         bag <- getsState $ getBodyStoreBag b fromCStore         case iid `EM.lookup` bag of           Nothing -> failWith "no item to trigger"           Just kit -> do             itemFull <- getsState $ itemToFull iid-            applyItem (fromCStore, (iid, (itemFull, kit)))+            applyItem leader (fromCStore, (iid, (itemFull, kit)))       Nothing -> failWith "no item to trigger" -applyItem :: (MonadClient m, MonadClientUI m)-          => (CStore, (ItemId, ItemFullKit))+applyItem :: MonadClientUI m+          => ActorId -> (CStore, (ItemId, ItemFullKit))           -> m (FailOrCmd RequestTimed)-applyItem (fromCStore, (iid, (itemFull, kit))) = do-  leader <- getLeaderUI-  actorCurAndMaxSk <- leaderSkillsClientUI+applyItem leader (fromCStore, (iid, (itemFull, kit))) = do+  COps{corule} <- getsState scops+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   b <- getsState $ getActorBody leader   localTime <- getsState $ getLocalTime (blid b)   let skill = Ability.getSk Ability.SkApply actorCurAndMaxSk       calmE = calmEnough b actorCurAndMaxSk       arItem = aspectRecordFull itemFull   if fromCStore == CEqp && not calmE then failSer ItemNotCalm-  else case permittedApply localTime skill calmE (Just fromCStore)+  else case permittedApply corule localTime skill calmE (Just fromCStore)                            itemFull kit of     Left reqFail -> failSer reqFail     Right _ -> do@@ -1051,10 +1056,9 @@ -- * AlterDir  -- | Ask for a direction and alter a tile, if possible.-alterDirHuman :: (MonadClient m, MonadClientUI m)-              => m (FailOrCmd RequestTimed)-alterDirHuman = pickPoint "modify" >>= \case-  Just p -> alterTileAtPos p+alterDirHuman :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed)+alterDirHuman leader = pickPoint leader "modify" >>= \case+  Just p -> alterTileAtPos leader p   Nothing -> failWith "never mind"  -- | Try to alter a tile using a feature at the given position.@@ -1063,25 +1067,23 @@ -- item can be triggered, because the player explicitely requested -- the action. Consequently, even if all embedded items are recharching, -- the time will be wasted and the server will describe the failure in detail.-alterTileAtPos :: (MonadClient m, MonadClientUI m)-               => Point-               -> m (FailOrCmd RequestTimed)-alterTileAtPos pos = do-  leader <- getLeaderUI+alterTileAtPos :: MonadClientUI m+               => ActorId -> Point -> m (FailOrCmd RequestTimed)+alterTileAtPos leader pos = do   sb <- getsState $ getActorBody leader   let sxhair = Just $ TPoint TUnknown (blid sb) pos   -- Point xhair to see details with `~`.   setXHairFromGUI sxhair-  alterCommon False pos+  alterCommon leader False pos  -- | Verify that the tile can be transformed or any embedded item effect -- triggered and the player is aware if the effect is dangerous or grave, -- such as ending the game.-verifyAlters :: forall m. (MonadClient m, MonadClientUI m)-             => Bool -> ActorId -> Point -> m (FailOrCmd ())-verifyAlters bumping source tpos = do+verifyAlters :: forall m. MonadClientUI m+             => ActorId -> Bool -> Point -> m (FailOrCmd ())+verifyAlters leader bumping tpos = do   COps{cotile, coTileSpeedup} <- getsState scops-  sb <- getsState $ getActorBody source+  sb <- getsState $ getActorBody leader   arItem <- getsState $ aspectRecordFromIid $ btrunk sb   embeds <- getsState $ getEmbedBag (blid sb) tpos   lvl <- getLevel $ blid sb@@ -1105,18 +1107,18 @@      && not underFeet      && Tile.isModifiable coTileSpeedup tile   then failSer AlterBlockItem-  else processTileActions bumping source tpos tileActions+  else processTileActions leader bumping tpos tileActions -processTileActions :: forall m. (MonadClient m, MonadClientUI m)-                   => Bool -> ActorId -> Point -> [Tile.TileAction]+processTileActions :: forall m. MonadClientUI m+                   => ActorId -> Bool -> Point -> [Tile.TileAction]                    -> m (FailOrCmd ())-processTileActions bumping source tpos tas = do+processTileActions leader bumping tpos tas = do   COps{coTileSpeedup} <- getsState scops   getKind <- getsState $ flip getIidKind-  sb <- getsState $ getActorBody source+  sb <- getsState $ getActorBody leader   lvl <- getLevel $ blid sb   sar <- getsState $ aspectRecordFromIid $ btrunk sb-  let sourceIsMist = IA.checkFlag Ability.Blast sar+  let leaderIsMist = IA.checkFlag Ability.Blast sar                      && Dice.infDice (IK.idamage $ getKind $ btrunk sb) <= 0       tileMinSkill = Tile.alterMinSkill coTileSpeedup $ lvl `at` tpos       processTA :: Maybe Bool -> [Tile.TileAction] -> Bool@@ -1142,7 +1144,7 @@           -- take the risk of wasted turn to verify the assumption.           -- If the item recharges, the wasted turns let the player wait.           let useResult = fromMaybe False museResult-          if | sourceIsMist+          if | leaderIsMist                || bproj sb && tileMinSkill > 0 ->  -- local skill check                processTA (Just useResult) rest bumpFailed                  -- embed won't fire; try others@@ -1167,8 +1169,8 @@           if not bumping || null tools0 then             if fromMaybe True museResult then do               -- UI requested, so this is voluntary, so item loss is fine.-              kitAssG <- getsState $ kitAssocs source [CGround]-              kitAssE <- getsState $ kitAssocs source [CEqp]+              kitAssG <- getsState $ kitAssocs leader [CGround]+              kitAssE <- getsState $ kitAssocs leader [CEqp]               let kitAss = listToolsToConsume kitAssG kitAssE                   grps0 = map (\(x, y) -> (False, x, y)) tools0                     -- apply if durable@@ -1194,7 +1196,7 @@     Right Nothing -> return $ Right ()     Right (Just (useResult, bumpFailed)) -> do       let !_A = assert (not useResult || bumpFailed) ()-      blurb <- lookAtPosition (blid sb) tpos+      blurb <- lookAtPosition tpos (blid sb)       mapM_ (uncurry msgAdd) blurb       if bumpFailed then do         revCmd <- revCmdMap@@ -1210,7 +1212,7 @@       else failWith "unable to activate nor modify at this time"         -- related to, among others, @SfxNoItemsForTile@ on the server -verifyEscape :: (MonadClient m, MonadClientUI m) => m (FailOrCmd ())+verifyEscape :: MonadClientUI m => m (FailOrCmd ()) verifyEscape = do   side <- getsClient sside   fact <- getsState $ (EM.! side) . sfactionD@@ -1237,7 +1239,7 @@     then failWith "here's your chance"     else return $ Right () -verifyToolEffect :: (MonadClient m, MonadClientUI m)+verifyToolEffect :: MonadClientUI m                  => LevelId -> CStore -> ItemFull -> m (FailOrCmd ()) verifyToolEffect lid store itemFull = do   CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui@@ -1260,42 +1262,40 @@ -- * AlterWithPointer  -- | Try to alter a tile using a feature under the pointer.-alterWithPointerHuman :: (MonadClient m, MonadClientUI m)-                      => m (FailOrCmd RequestTimed)-alterWithPointerHuman = do-  COps{corule=RuleContent{rXmax, rYmax}} <- getsState scops+alterWithPointerHuman :: MonadClientUI m+                      => ActorId -> m (FailOrCmd RequestTimed)+alterWithPointerHuman leader = do+  COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops   pUI <- getsSession spointer-  let p@(Point px py) = squareToMap $ uiToSquare pUI-  if px >= 0 && py >= 0 && px < rXmax && py < rYmax-  then alterTileAtPos p+  let p = squareToMap $ uiToSquare pUI+  if insideP (0, 0, rWidthMax - 1, rHeightMax - 1) p+  then alterTileAtPos leader p   else failWith "never mind"  -- * CloseDir  -- | Close nearby open tile; ask for direction, if there is more than one.-closeDirHuman :: (MonadClient m, MonadClientUI m)-              => m (FailOrCmd RequestTimed)-closeDirHuman = do+closeDirHuman :: MonadClientUI m+              => ActorId -> m (FailOrCmd RequestTimed)+closeDirHuman leader = do   COps{coTileSpeedup} <- getsState scops-  leader <- getLeaderUI   b <- getsState $ getActorBody leader   lvl <- getLevel $ blid b   let vPts = vicinityUnsafe $ bpos b       openPts = filter (Tile.isClosable coTileSpeedup . at lvl) vPts   case openPts of     []  -> failSer CloseNothing-    [o] -> closeTileAtPos o-    _   -> pickPoint "close" >>= \case+    [o] -> closeTileAtPos leader o+    _   -> pickPoint leader "close" >>= \case       Nothing -> failWith "never mind"-      Just p -> closeTileAtPos p+      Just p -> closeTileAtPos leader p  -- | Close tile at given position.-closeTileAtPos :: (MonadClient m, MonadClientUI m)-               => Point -> m (FailOrCmd RequestTimed)-closeTileAtPos tpos = do+closeTileAtPos :: MonadClientUI m+               => ActorId -> Point -> m (FailOrCmd RequestTimed)+closeTileAtPos leader tpos = do   COps{coTileSpeedup} <- getsState scops-  leader <- getLeaderUI-  actorCurAndMaxSk <- leaderSkillsClientUI+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   b <- getsState $ getActorBody leader   alterable <- getsState $ tileAlterable (blid b) tpos   lvl <- getLevel $ blid b@@ -1319,14 +1319,13 @@           -> failSer AlterBlockActor          | otherwise           -> do-             msgAddDone tpos "close"+             msgAddDone leader tpos "close"              return $ Right (ReqAlter tpos)  -- | Adds message with proper names.-msgAddDone :: (MonadClient m, MonadClientUI m) => Point -> Text -> m ()-msgAddDone p verb = do+msgAddDone :: MonadClientUI m => ActorId -> Point -> Text -> m ()+msgAddDone leader p verb = do   COps{cotile} <- getsState scops-  leader <- getLeaderUI   b <- getsState $ getActorBody leader   lvl <- getLevel $ blid b   let tname = TK.tname $ okind cotile $ lvl `at` p@@ -1340,9 +1339,8 @@   msgAdd MsgActionComplete $ "You" <+> verb <+> "the" <+> s <+> dir <> "."  -- | Prompts user to pick a point.-pickPoint :: (MonadClient m, MonadClientUI m) => Text -> m (Maybe Point)-pickPoint verb = do-  leader <- getLeaderUI+pickPoint :: MonadClientUI m => ActorId -> Text -> m (Maybe Point)+pickPoint leader verb = do   b <- getsState $ getActorBody leader   UIOptions{uVi, uLeftHand} <- getsSession sUIOptions   let dirKeys = K.dirAllKey uVi uLeftHand@@ -1362,7 +1360,7 @@ -- * Help  -- | Display command help.-helpHuman :: (MonadClient m, MonadClientUI m)+helpHuman :: MonadClientUI m           => (K.KM -> HumanCmd -> m (Either MError ReqUI))           -> m (Either MError ReqUI) helpHuman cmdSemInCxtOfKM = do@@ -1402,14 +1400,13 @@         then packIntoScreens ls (l : acc) (length l + 1 + h)         else intercalate [""] (reverse acc) : packIntoScreens (l : ls) [] 0       manualScreens = packIntoScreens (snd rintroScreen) [] 0-      shiftPointUI x (PointUI x0 y0) = PointUI (x0 + x) y0       sideBySide =         if isSquareFont monoFont         then \(screen1, screen2) ->  -- single column, two screens           map offsetOverlay $ filter (not . null) [screen1, screen2]         else \(screen1, screen2) ->  -- two columns, single screen           [offsetOverlay screen1-           ++ map (first $ shiftPointUI rwidth) (offsetOverlay screen2)]+           ++ xtranslateOverlay rwidth (offsetOverlay screen2)]       listPairs (a : b : rest) = (a, b) : listPairs rest       listPairs [a] = [(a, [])]       listPairs [] = []@@ -1439,7 +1436,7 @@ -- * Hint  -- | Display hint or, if already displayed, display help.-hintHuman :: (MonadClient m, MonadClientUI m)+hintHuman :: MonadClientUI m           => (K.KM -> HumanCmd -> m (Either MError ReqUI))           -> m (Either MError ReqUI) hintHuman cmdSemInCxtOfKM = do@@ -1453,14 +1450,14 @@ -- * Dashboard  -- | Display the dashboard.-dashboardHuman :: (MonadClient m, MonadClientUI m)+dashboardHuman :: MonadClientUI m                => (K.KM -> HumanCmd -> m (Either MError ReqUI))                -> m (Either MError ReqUI) dashboardHuman cmdSemInCxtOfKM = do   CCUI{coinput, coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui   fontSetup@FontSetup{..} <- getFontSetup   let offsetCol2 = 2-      (ov0, kxs0) = okxsN coinput monoFont propFont 0 offsetCol2 (const False)+      (ov0, kxs0) = okxsN coinput monoFont propFont offsetCol2 (const False)                           False CmdDashboard ([], [], []) ([], [])       al1 = textToAS "Dashboard"   let splitHelp (al, okx) = splitOKX fontSetup False rwidth (rheight - 2) rwidth@@ -1477,15 +1474,16 @@  -- * ItemMenu -itemMenuHuman :: (MonadClient m, MonadClientUI m)-              => (K.KM -> HumanCmd -> m (Either MError ReqUI))+itemMenuHuman :: MonadClientUI m+              => ActorId+              -> (K.KM -> HumanCmd -> m (Either MError ReqUI))               -> m (Either MError ReqUI)-itemMenuHuman cmdSemInCxtOfKM = do+itemMenuHuman leader cmdSemInCxtOfKM = do+  COps{corule} <- getsState scops   itemSel <- getsSession sitemSel   fontSetup@FontSetup{..} <- getFontSetup   case itemSel of     Just (iid, fromCStore, _) -> do-      leader <- getLeaderUI       b <- getsState $ getActorBody leader       bUI <- getsSession $ getActorUI leader       bag <- getsState $ getBodyStoreBag b fromCStore@@ -1493,7 +1491,7 @@         Nothing -> weaveJust <$> failWith "no item to open item menu for"         Just kit -> do           CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui-          actorCurAndMaxSk <- leaderSkillsClientUI+          actorCurAndMaxSk <- getsState $ getActorMaxSkills leader           itemFull <- getsState $ itemToFull iid           localTime <- getsState $ getLocalTime (blid b)           found <- getsState $ findIid leader (bfid b) iid@@ -1513,31 +1511,23 @@           let foundPrefix = textToAS $                 if null foundTexts then "" else "The item is also in:"               markParagraphs = rheight >= 45-              descAl = itemDesc rwidth markParagraphs (bfid b) factionD+              descAs = itemDesc rwidth markParagraphs (bfid b) factionD                                 (Ability.getSk Ability.SkHurtMelee                                                actorCurAndMaxSk)                                 fromCStore localTime jlid itemFull kit-              (descSymAl, descBlurbAl) = span (/= Color.spaceAttrW32) descAl-              descSym = offsetOverlay $ splitAttrString rwidth rwidth descSymAl-              descBlurb = offsetOverlayX $-                case splitAttrString rwidth rwidth-                     $ stringToAS "xx" ++ descBlurbAl of-                  [] -> error "splitting AttrString loses characters"-                  al1 : rest ->-                    (2, attrStringToAL $ drop 2 $ attrLine al1) : map (0,) rest-              alPrefix = map (\(PointUI x y, al) ->-                                (PointUI x (y + length descBlurb), al))+              (ovLab, ovDesc) = labDescOverlay squareFont rwidth descAs+              ovPrefix = ytranslateOverlay (length ovDesc)                          $ offsetOverlay                          $ splitAttrString rwidth rwidth foundPrefix-              ystart = length descBlurb + length alPrefix - 1+              ystart = length ovDesc + length ovPrefix - 1               xstart = textSize monoFont (Color.spaceAttrW32-                                          : attrLine (snd $ last alPrefix))+                                          : attrLine (snd $ last ovPrefix))               foundKeys = map (K.KM K.NoModifier . K.Fun)                               [1 .. length foundAlt]  -- starting from 1!           let ks = zip foundKeys foundTexts               width = if isSquareFont monoFont then 2 * rwidth else rwidth               (ovFoundRaw, kxsFound) = wrapOKX monoFont ystart xstart width ks-              ovFound = alPrefix ++ ovFoundRaw+              ovFound = ovPrefix ++ ovFoundRaw           report <- getReportUI True           CCUI{coinput} <- getsSession sccui           mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b@@ -1558,8 +1548,8 @@                 Apply{} ->                   let skill = Ability.getSk Ability.SkApply actorCurAndMaxSk                   in not $ either (const False) id-                     $ permittedApply localTime skill calmE (Just fromCStore)-                                      itemFull kit+                     $ permittedApply corule localTime skill calmE+                                      (Just fromCStore) itemFull kit                 Project{} ->                   let skill = Ability.getSk Ability.SkProject actorCurAndMaxSk                   in not $ either (const False) id@@ -1568,10 +1558,10 @@               fmt n k h = " " <> T.justifyLeft n ' ' k <> " " <> h               offsetCol2 = 11               keyCaption = fmt offsetCol2 "keys" "command"-              offset = 1 + maxYofOverlay (descBlurb ++ ovFound)-              (ov0, kxs0) = okxsN coinput monoFont propFont offset offsetCol2-                                  greyedOut True CmdItemMenu-                                  ([], [], ["", keyCaption]) ([], [])+              offset = 1 + maxYofOverlay (ovDesc ++ ovFound)+              (ov0, kxs0) = xytranslateOKX 0 offset $+                 okxsN coinput monoFont propFont offsetCol2 greyedOut+                       True CmdItemMenu ([], [], ["", keyCaption]) ([], [])               t0 = makeSentence [ MU.SubjectVerbSg (partActor bUI) "choose"                                 , "an item", MU.Text $ ppCStoreIn fromCStore ]               alRep = foldr (<+:>) [] $ renderReport True report@@ -1582,8 +1572,8 @@                          [K.spaceKM, K.escKM] okx               sli = toSlideshow fontSetup                     $ splitHelp ( al1-                                , ( EM.insertWith (++) squareFont descSym-                                    $ EM.insertWith (++) propFont descBlurb+                                , ( EM.insertWith (++) squareFont ovLab+                                    $ EM.insertWith (++) propFont ovDesc                                     $ EM.insertWith (++) monoFont ovFound ov0                                         -- mono font, because there are buttons                                   , kxsFound ++ kxs0 ))@@ -1593,7 +1583,8 @@           case ekm of             Left km -> case km `M.lookup` bcmdMap coinput of               _ | km == K.escKM -> weaveJust <$> failWith "never mind"-              _ | km == K.spaceKM -> weaveJust <$> failWith "back to list"+              _ | km == K.spaceKM ->+                chooseItemMenuHuman leader cmdSemInCxtOfKM (MStore fromCStore)               _ | km `elem` foundKeys -> case km of                 K.KM{key=K.Fun n} -> do                   let (newAid, (bNew, newCStore)) = foundAlt !! (n - 1)@@ -1605,7 +1596,7 @@                        void $ pickLeader True newAid                        modifySession $ \sess ->                          sess {sitemSel = Just (iid, newCStore, False)}-                       itemMenuHuman cmdSemInCxtOfKM+                       itemMenuHuman newAid cmdSemInCxtOfKM                 _ -> error $ "" `showFailure` km               Just (_desc, _cats, cmd) -> do                 modifySession $ \sess ->@@ -1617,110 +1608,114 @@  -- * ChooseItemMenu -chooseItemMenuHuman :: (MonadClient m, MonadClientUI m)-                    => (K.KM -> HumanCmd -> m (Either MError ReqUI))+chooseItemMenuHuman :: MonadClientUI m+                    => ActorId+                    -> (K.KM -> HumanCmd -> m (Either MError ReqUI))                     -> ItemDialogMode                     -> m (Either MError ReqUI)-chooseItemMenuHuman cmdSemInCxtOfKM c0 = do-  let chooseItemMenu c1 = do-        res <- chooseItemDialogMode True c1-        case res of-          Right c2 -> do-            res2 <- itemMenuHuman cmdSemInCxtOfKM-            backToList <- failMsg "back to list"-            case res2 of-              Left err | err == backToList -> chooseItemMenu c2-              _ -> return res2-          Left err -> return $ Left $ Just err-  chooseItemMenu c0+chooseItemMenuHuman leader1 cmdSemInCxtOfKM c1 = do+  res2 <- chooseItemDialogMode leader1 True c1+  case res2 of+    Right leader2 -> itemMenuHuman leader2 cmdSemInCxtOfKM+    Left err -> return $ Left $ Just err  -- * MainMenu -generateMenu :: (MonadClient m, MonadClientUI m)+generateMenu :: MonadClientUI m              => (K.KM -> HumanCmd -> m (Either MError ReqUI))-             -> [(DisplayFont, [AttrLine])]-             -> [(K.KM, (Text, HumanCmd))]+             -> FontOverlayMap+             -> [(String, (Text, HumanCmd, Maybe FontOverlayMap))]              -> [String]              -> String              -> m (Either MError ReqUI)-generateMenu cmdSemInCxtOfKM blurb kds gameInfo menuName = do+generateMenu cmdSemInCxtOfKM blurb kdsRaw gameInfo menuName = do   COps{corule} <- getsState scops-  CCUI{coscreen=ScreenContent{rwidth, rheight, rwebAddress}} <--    getsSession sccui+  CCUI{coscreen=ScreenContent{rheight, rwebAddress}} <- getsSession sccui   FontSetup{..} <- getFontSetup-  let bindings =  -- key bindings to display-        let fmt (k, (d, _)) =-              ( Just k+  let kds = map (first K.mkKM) kdsRaw+      bindings =  -- key bindings to display+        let fmt (km, (d, _, _)) =+              ( Just km               , T.unpack-                $  T.justifyLeft 3 ' ' (T.pack $ K.showKM k) <> " " <> d )+                $ T.justifyLeft 3 ' ' (T.pack $ K.showKM km) <> " " <> d )         in map fmt kds-      generate :: Int -> (Maybe K.KM, String) -> ((Int, AttrLine), Maybe KYX)+      generate :: Int -> (Maybe K.KM, String) -> Maybe KYX       generate y (mkey, binding) =         let lenB = length binding-            yxx key = (Left [key], ( PointUI 2 y-                                   , ButtonWidth squareFont lenB ))-            myxx = yxx <$> mkey-        in ((2, stringToAL binding), myxx)+            yxx key = (Left key, ( PointUI 0 y+                                 , ButtonWidth squareFont lenB ))+        in yxx <$> mkey       titleLine = rtitle corule ++ " "                   ++ showVersion (rexeVersion corule) ++ " "       rawLines = zip (repeat Nothing)                      (["", titleLine ++ "[" ++ rwebAddress ++ "]", ""]                       ++ gameInfo)                  ++ bindings-      (menuOvLines, mkyxs) = unzip $ zipWith generate [0..] rawLines       browserKey = ( Right $ SlotChar 1042 'a'-                   , ( PointUI (2 + 2 * length titleLine) 1+                   , ( PointUI (2 * length titleLine) 1                      , ButtonWidth squareFont (2 + length rwebAddress) ) )-      kyxs = browserKey : catMaybes mkyxs-      introLen = sum $ map (length . snd) blurb-      start0 = max 0 (rheight - introLen-                      - if isSquareFont propFont then 1 else 2)-      shiftPointUI (PointUI x0 y0) = PointUI (x0 + rwidth) y0-      ov0 = EM.map (map (first shiftPointUI)) $ attrLinesToFontMap start0 blurb-      ov = EM.insertWith (++) squareFont (offsetOverlayX menuOvLines) ov0+      kyxs = browserKey : catMaybes (zipWith generate [0..] rawLines)+      ov = EM.singleton squareFont $ offsetOverlay+                                   $ map (stringToAL . snd) rawLines+      kxy = xytranslateOKX 2 0 (ov, kyxs)   menuIxMap <- getsSession smenuIxMap   unless (menuName `M.member` menuIxMap) $     modifySession $ \sess -> sess {smenuIxMap = M.insert menuName 1 menuIxMap}-  ekm <- displayChoiceScreen menuName ColorFull True-                             (menuToSlideshow (ov, kyxs)) [K.escKM]+  let prepareBlurb ovs =+        let introLen = 1 + maxYofFontOverlayMap ovs+            start0 = max 0 (rheight - introLen+                            - if isSquareFont propFont then 1 else 2)+        in EM.map (xytranslateOverlay (-2) (start0 - 2)) ovs+          -- subtracting 2 from X and Y to negate the indentation in+          -- @displayChoiceScreenWithRightPane@+      returnDefaultOKS = return (prepareBlurb blurb, [])+      displayInRightPane (Right _) = returnDefaultOKS+      displayInRightPane (Left km) = case km `lookup` kds of+        Just (_, _, mblurbRight) -> case mblurbRight of+          Nothing -> returnDefaultOKS+          Just blurbRight -> return (prepareBlurb blurbRight, [])+        Nothing -> error "displayInRightPane: unexpected key"+  ekm <- displayChoiceScreenWithRightPane displayInRightPane+                                          menuName ColorFull True+                                          (menuToSlideshow kxy) [K.escKM]   case ekm of     Left km -> case km `lookup` kds of-      Just (_desc, cmd) -> cmdSemInCxtOfKM km cmd+      Just (_desc, cmd, _) -> cmdSemInCxtOfKM km cmd       Nothing -> weaveJust <$> failWith "never mind"     Right (SlotChar 1042 'a') -> do       success <- tryOpenBrowser rwebAddress       if success-      then generateMenu cmdSemInCxtOfKM blurb kds gameInfo menuName+      then generateMenu cmdSemInCxtOfKM blurb kdsRaw gameInfo menuName       else weaveJust <$> failWith "failed to open web browser"     Right _slot -> error $ "" `showFailure` ekm  -- | Display the main menu.-mainMenuHuman :: (MonadClient m, MonadClientUI m)+mainMenuHuman :: MonadClientUI m               => (K.KM -> HumanCmd -> m (Either MError ReqUI))               -> m (Either MError ReqUI) mainMenuHuman cmdSemInCxtOfKM = do-  CCUI{ coinput=InputContent{bcmdList}-      , coscreen=ScreenContent{rintroScreen} } <- getsSession sccui+  CCUI{coscreen=ScreenContent{rintroScreen}} <- getsSession sccui   FontSetup{propFont} <- getFontSetup   gameMode <- getGameMode   curChal <- getsClient scurChal   let offOn b = if b then "on" else "off"-      tcurDiff   = "   with difficulty:" <+> tshow (cdiff curChal)-      tcurFish   = "       cold fish:" <+> offOn (cfish curChal)-      tcurGoods  = "     ready goods:" <+> offOn (cgoods curChal)-      tcurWolf   = "       lone wolf:" <+> offOn (cwolf curChal)-      tcurKeeper = "   finder keeper:" <+> offOn (ckeeper curChal)       -- Key-description-command tuples.-      kds = [(km, (desc, cmd)) | (km, ([CmdMainMenu], desc, cmd)) <- bcmdList]+      kds = [ ("s", ("setup and start new game>", ChallengeMenu, Nothing))+            , ("x", ("save and exit to desktop", GameExit, Nothing))+            , ("c", ("tweak convenience settings>", SettingsMenu, Nothing))+            , ("t", ("toggle autoplay", AutomateToggle, Nothing))+            , ("?", ("see command help", Help, Nothing))+            , ("F12", ("switch to dashboard", Dashboard, Nothing))+            , ("Escape", ("back to playing", AutomateBack, Nothing)) ]       gameName = MK.mname gameMode       gameInfo = map T.unpack                    [ "Now playing:" <+> gameName                    , ""-                   , tcurDiff-                   , tcurFish-                   , tcurGoods-                   , tcurWolf-                   , tcurKeeper+                   , "   with difficulty:" <+> tshow (cdiff curChal)+                   , "       cold fish:" <+> offOn (cfish curChal)+                   , "     ready goods:" <+> offOn (cgoods curChal)+                   , "       lone wolf:" <+> offOn (cwolf curChal)+                   , "   finder keeper:" <+> offOn (ckeeper curChal)                    , "" ]       glueLines (l1 : l2 : rest) =         if | null l1 -> l1 : glueLines (l2 : rest)@@ -1730,12 +1725,13 @@       backstory | isSquareFont propFont = fst rintroScreen                 | otherwise = glueLines $ fst rintroScreen       backstoryAL = map stringToAL $ map (dropWhile (== ' ')) backstory-  generateMenu cmdSemInCxtOfKM [(propFont, backstoryAL)] kds gameInfo "main"+      blurb = attrLinesToFontMap [(propFont, backstoryAL)]+  generateMenu cmdSemInCxtOfKM blurb kds gameInfo "main"  -- * MainMenuAutoOn  -- | Display the main menu and set @swasAutomated@.-mainMenuAutoOnHuman :: (MonadClient m, MonadClientUI m)+mainMenuAutoOnHuman :: MonadClientUI m                     => (K.KM -> HumanCmd -> m (Either MError ReqUI))                     -> m (Either MError ReqUI) mainMenuAutoOnHuman cmdSemInCxtOfKM = do@@ -1745,7 +1741,7 @@ -- * MainMenuAutoOff  -- | Display the main menu and unset @swasAutomated@.-mainMenuAutoOffHuman :: (MonadClient m, MonadClientUI m)+mainMenuAutoOffHuman :: MonadClientUI m                      => (K.KM -> HumanCmd -> m (Either MError ReqUI))                      -> m (Either MError ReqUI) mainMenuAutoOffHuman cmdSemInCxtOfKM = do@@ -1755,10 +1751,13 @@ -- * SettingsMenu  -- | Display the settings menu.-settingsMenuHuman :: (MonadClient m, MonadClientUI m)+settingsMenuHuman :: MonadClientUI m                   => (K.KM -> HumanCmd -> m (Either MError ReqUI))                   -> m (Either MError ReqUI) settingsMenuHuman cmdSemInCxtOfKM = do+  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui+  UIOptions{uMsgWrapColumn} <- getsSession sUIOptions+  FontSetup{..} <- getFontSetup   markSuspect <- getsClient smarkSuspect   markVision <- getsSession smarkVision   markSmell <- getsSession smarkSmell@@ -1767,42 +1766,62 @@   factDoctrine <- getsState $ MK.fdoctrine . gplayer . (EM.! side) . sfactionD   overrideTut <- getsSession soverrideTut   let offOn b = if b then "on" else "off"-      offOnUnset mb = case mb of-        Nothing -> "no override"-        Just b -> if b then "force on" else "force off"       offOnAll n = case n of         0 -> "none"         1 -> "untried"         2 -> "all"         _ -> error $ "" `showFailure` n+      neverEver n = case n of+        0 -> "never"+        1 -> "aiming"+        2 -> "always"+        _ -> error $ "" `showFailure` n+      offOnUnset mb = case mb of+        Nothing -> "pass"+        Just b -> if b then "force on" else "force off"       tsuspect = "mark suspect terrain:" <+> offOnAll markSuspect-      tvisible = "show visible zone:" <+> offOn markVision+      tvisible = "show visible zone:" <+> neverEver markVision       tsmell = "display smell clues:" <+> offOn markSmell       tanim = "play animations:" <+> offOn (not noAnim)       tdoctrine = "squad doctrine:" <+> Ability.nameDoctrine factDoctrine       toverride = "override tutorial hints:" <+> offOnUnset overrideTut-      -- Key-description-command tuples.-      kds = [ (K.mkKM "s", (tsuspect, MarkSuspect))-            , (K.mkKM "v", (tvisible, MarkVision))-            , (K.mkKM "c", (tsmell, MarkSmell))-            , (K.mkKM "a", (tanim, MarkAnim))-            , (K.mkKM "t", (tdoctrine, Doctrine))-            , (K.mkKM "o", (toverride, OverrideTut))-            , (K.mkKM "Escape", ("back to main menu", MainMenu)) ]+      width = if isSquareFont propFont+              then rwidth `div` 2+              else min uMsgWrapColumn (rwidth - 2)+      textToBlurb t = Just $ attrLinesToFontMap+        [ ( monoFont+          , splitAttrString width width+            $ textToAS t ) ]+      -- Key-description-command-text tuples.+      kds = [ ("s", ( tsuspect, MarkSuspect+                    , textToBlurb "* mark suspect terrain\nThis setting affects the ongoing and the next games. It determines which suspect terrain is marked in special color on the map: none, untried (not searched nor revealed), all. It correspondingly determines which, if any, suspect tiles are considered for mouse go-to, auto-explore and for the command that marks the nearest unexplored position." ))+            , ("v", (tvisible, MarkVision+                    , textToBlurb "* show visible zone\nThis setting affects the ongoing and the next games. It determines the conditions under which the area visible to the party is marked on the map via a gray background: never, when aiming, always." ))+            , ("c", (tsmell, MarkSmell+                    , textToBlurb "* display smell clues\nThis setting affects the ongoing and the next games. It determines whether the map displays any smell traces (regardless of who left them) detected by a party member that can track via smell (as determined by the smell radius skill; not common among humans)." ))+            , ("a", (tanim, MarkAnim+                    , textToBlurb "* play animations\nThis setting affects the ongoing and the next games. It determines whether important events, such combat, are highlighted by animations. This overrides the corresponding config file setting." ))+            , ("d", (tdoctrine, Doctrine+                    , textToBlurb "* squad doctrine\nThis setting affects the ongoing game, but does not persist to the next games. It determines the behaviour of henchmen (non-pointman characters) in the party and, in particular, if they are permitted to move autonomously or fire opportunistically (assuming they are able to, usually due to rare equipment). This setting has a poor UI that will be improved in the future." ))+            , ("t", (toverride, OverrideTut+                    , textToBlurb "* override tutorial hints\nThis setting affects the ongoing and the next games. It determines whether tutorial hints are, respectively, not overridden with respect to the setting that was chosen when starting the current game, forced to be off, forced to be on." ))+            , ("Escape", ( "back to main menu", MainMenu+                         , Just EM.empty )) ]       gameInfo = map T.unpack                    [ "Tweak convenience settings:"                    , "" ]-  generateMenu cmdSemInCxtOfKM [] kds gameInfo "settings"+  generateMenu cmdSemInCxtOfKM EM.empty kds gameInfo "settings"  -- * ChallengeMenu  -- | Display the challenge menu.-challengeMenuHuman :: (MonadClient m, MonadClientUI m)-                    => (K.KM -> HumanCmd -> m (Either MError ReqUI))-                    -> m (Either MError ReqUI)+challengeMenuHuman :: MonadClientUI m+                   => (K.KM -> HumanCmd -> m (Either MError ReqUI))+                   -> m (Either MError ReqUI) challengeMenuHuman cmdSemInCxtOfKM = do   cops <- getsState scops-  CCUI{coscreen=ScreenContent{rwidth, rwrap}} <- getsSession sccui+  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui+  UIOptions{uMsgWrapColumn} <- getsSession sUIOptions   FontSetup{..} <- getFontSetup   svictories <- getsClient svictories   snxtScenario <- getsSession snxtScenario@@ -1818,40 +1837,23 @@       offOn b = if b then "on" else "off"       starTut t = if isJust overrideTut then "*" <> t else t       displayTutorialHints = fromMaybe nxtTutorial overrideTut-      tnextTutorial = "tutorial hints (in pink):"-                      <+> starTut (offOn displayTutorialHints)-      tnextDiff = "difficulty (lower easier):" <+> tshow (cdiff nxtChal)-      tnextFish   = "cold fish (rather hard):"-                    <+> offOn (cfish nxtChal)-      tnextGoods  = "ready goods (hard):"-                    <+> offOn (cgoods nxtChal)-      tnextWolf   = "lone wolf (very hard):"-                    <+> offOn (cwolf nxtChal)-      tnextKeeper = "finder keeper (hard):"-                    <+> offOn (ckeeper nxtChal)-      -- Key-description-command tuples.-      kds = [ (K.mkKM "s", (tnextScenario, GameScenarioIncr))-            , (K.mkKM "t", (tnextTutorial, GameTutorialToggle))-            , (K.mkKM "d", (tnextDiff, GameDifficultyIncr))-            , (K.mkKM "f", (tnextFish, GameFishToggle))-            , (K.mkKM "r", (tnextGoods, GameGoodsToggle))-            , (K.mkKM "w", (tnextWolf, GameWolfToggle))-            , (K.mkKM "k", (tnextKeeper, GameKeeperToggle))-            , (K.mkKM "g", ("start new game", GameRestart))-            , (K.mkKM "Escape", ("back to main menu", MainMenu)) ]-      gameInfo = map T.unpack [ "Setup and start new game:"-                              , "" ]-      widthProp = if isSquareFont propFont-                  then rwidth `div` 2-                  else min rwrap (rwidth - 2)+      tnextTutorial = "tutorial hints:" <+> starTut (offOn displayTutorialHints)+      tnextDiff = "difficulty level:" <+> tshow (cdiff nxtChal)+      tnextFish = "cold fish (rather hard):" <+> offOn (cfish nxtChal)+      tnextGoods = "ready goods (hard):" <+> offOn (cgoods nxtChal)+      tnextWolf = "lone wolf (very hard):" <+> offOn (cwolf nxtChal)+      tnextKeeper = "finder keeper (hard):" <+> offOn (ckeeper nxtChal)+      width = if isSquareFont propFont+              then rwidth `div` 2+              else min uMsgWrapColumn (rwidth - 2)       widthMono = if isSquareFont propFont                   then rwidth `div` 2                   else rwidth - 2       duplicateEOL '\n' = "\n\n"       duplicateEOL c = T.singleton c-      blurb =+      blurb = Just $ attrLinesToFontMap         [ ( propFont-          , splitAttrString widthProp widthProp+          , splitAttrString width width             $ textFgToAS Color.BrBlack             $ T.concatMap duplicateEOL (MK.mdesc gameMode)               <> "\n\n" )@@ -1861,11 +1863,33 @@             $ MK.mrules gameMode               <> "\n\n" )         , ( propFont-          , splitAttrString widthProp widthProp+          , splitAttrString width width             $ textToAS             $ T.concatMap duplicateEOL (MK.mreason gameMode) )         ]-  generateMenu cmdSemInCxtOfKM blurb kds gameInfo "challenge"+      textToBlurb t = Just $ attrLinesToFontMap+        [ ( monoFont+          , splitAttrString width width  -- not widthMono!+            $ textToAS t ) ]+      -- Key-description-command-text tuples.+      kds = [ ("s", (tnextScenario, GameScenarioIncr, blurb))+            , ("t", ( tnextTutorial, GameTutorialToggle+                    , textToBlurb "* tutorial hints\nThis determines whether tutorial hint messages will be shown in the next game that's about to be started. They are rendered in pink and can be re-read from message history. Display of tutorial hints in the current game can be overridden from the convenience settings menu."))+            , ("d", ( tnextDiff, GameDifficultyIncr+                    , textToBlurb "* difficulty level\nThis determines the difficulty of survival in the next game that's about to be started. Lower numbers result in easier game. In particular, difficulty below 5 multiplies hitpoints of player characters and difficulty over 5 multiplies hitpoints of their enemies. Game score scales with difficulty."))+            , ("f", ( tnextFish, GameFishToggle+                    , textToBlurb "* cold fish\nThis challenge mode setting will affect the next game that's about to be started. When on, it makes it impossible for player characters to be healed by actors from other factions (this is a significant restriction in the long crawl adventure)."))+            , ("r", ( tnextGoods, GameGoodsToggle+                    , textToBlurb "* ready goods\nThis challenge mode setting will affect the next game that's about to be started. When on, it disables crafting for the player, making the selection of equipment, especially melee weapons, very limited, unless the player has the luck to find the rare powerful ready weapons (this applies only if the chosen adventure supports crafting at all)."))+            , ("w", ( tnextWolf, GameWolfToggle+                    , textToBlurb "* lone wolf\nThis challenge mode setting will affect the next game that's about to be started. When on, it reduces player's starting actors to exactly one, though later on new heroes may join the party. This makes the game very hard in the long run."))+            , ("k", ( tnextKeeper, GameKeeperToggle+                    , textToBlurb "* finder keeper\nThis challenge mode setting will affect the next game that's about to be started. When on, it completely disables flinging projectiles by the player, which affects not only ranged damage dealing, but also throwing of consumables that buff teammates engaged in melee combat, weaken and distract enemies, light dark corners, etc."))+            , ("g", ("start new game", GameRestart, blurb))+            , ("Escape", ("back to main menu", MainMenu, Nothing)) ]+      gameInfo = map T.unpack [ "Setup and start new game:"+                              , "" ]+  generateMenu cmdSemInCxtOfKM EM.empty kds gameInfo "challenge"  -- * GameTutorialToggle @@ -1918,7 +1942,7 @@  -- * GameScenarioIncr -gameScenarioIncr :: (MonadClient m, MonadClientUI m) => m ()+gameScenarioIncr :: MonadClientUI m => m () gameScenarioIncr = do   cops <- getsState scops   oldScenario <- getsSession snxtScenario@@ -1928,7 +1952,7 @@  -- * GameRestart -gameRestartHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd ReqUI)+gameRestartHuman :: MonadClientUI m => m (FailOrCmd ReqUI) gameRestartHuman = do   cops <- getsState scops   noConfirmsGame <- isNoConfirmsGame@@ -1948,7 +1972,8 @@     -- via main menu and assumes the fist word of such game modes     -- is present in their frequencies.     let (mainName, _) = T.span (\c -> Char.isAlpha c || c == ' ') nxtGameName-        nxtGameGroup = GroupName $ T.intercalate " " $ take 2 $ T.words mainName+        nxtGameGroup = DefsInternal.GroupName $ T.intercalate " "+                       $ take 2 $ T.words mainName     return $ Right $ ReqUIGameRestart nxtGameGroup snxtChal   else do     msg2 <- rndToActionUI $ oneOf@@ -1959,7 +1984,7 @@ -- * GameQuit  -- TODO: deduplicate with gameRestartHuman-gameQuitHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd ReqUI)+gameQuitHuman :: MonadClientUI m => m (FailOrCmd ReqUI) gameQuitHuman = do   noConfirmsGame <- isNoConfirmsGame   gameMode <- getGameMode@@ -1980,12 +2005,12 @@  -- * GameDrop -gameDropHuman :: (MonadClient m, MonadClientUI m) => m ReqUI+gameDropHuman :: MonadClientUI m => m ReqUI gameDropHuman = do   modifySession $ \sess -> sess {sallNframes = -1}  -- hack, but we crash anyway   msgAdd MsgPromptGeneric "Interrupt! Trashing the unsaved game. The program exits now."   clientPrintUI "Interrupt! Trashing the unsaved game. The program exits now."-    -- this is not shown by vty frontend, but at least shown by sdl2 one+    -- this is not shown by ANSI frontend, but at least shown by sdl2 one   return ReqUIGameDropAndExit  -- * GameExit@@ -1996,7 +2021,7 @@  -- * GameSave -gameSaveHuman :: (MonadClient m, MonadClientUI m) => m ReqUI+gameSaveHuman :: MonadClientUI m => m ReqUI gameSaveHuman = do   -- Announce before the saving started, since it can take a while.   msgAdd MsgInnerWorkSpam "Saving game backup."@@ -2007,7 +2032,7 @@ -- Note that the difference between seek-target and follow-the-leader doctrine -- can influence even a faction with passive actors. E.g., if a passive actor -- has an extra active skill from equipment, he moves every turn.-doctrineHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd ReqUI)+doctrineHuman :: MonadClientUI m => m (FailOrCmd ReqUI) doctrineHuman = do   fid <- getsClient sside   fromT <- getsState $ MK.fdoctrine . gplayer . (EM.! fid) . sfactionD@@ -2026,7 +2051,7 @@  -- * Automate -automateHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd ReqUI)+automateHuman :: MonadClientUI m => m (FailOrCmd ReqUI) automateHuman = do   clearAimMode   proceed <- displayYesNo ColorBW "Do you really want to cede control to AI?"@@ -2036,7 +2061,7 @@  -- * AutomateToggle -automateToggleHuman :: (MonadClient m, MonadClientUI m) => m (FailOrCmd ReqUI)+automateToggleHuman :: MonadClientUI m => m (FailOrCmd ReqUI) automateToggleHuman = do   swasAutomated <- getsSession swasAutomated   if swasAutomated
engine-src/Game/LambdaHack/Client/UI/HandleHumanLocalM.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE TupleSections #-} -- | Semantics of "Game.LambdaHack.Client.UI.HumanCmd" -- client commands that do not return server requests,, -- but only change internal client state.@@ -10,7 +9,7 @@   , chooseItemHuman, chooseItemDialogMode   , chooseItemProjectHuman, chooseItemApplyHuman   , psuitReq, triggerSymbols, pickLeaderHuman, pickLeaderWithPointerHuman-  , memberCycleHuman, memberCycleLevelHuman+  , pointmanCycleHuman, pointmanCycleLevelHuman   , selectActorHuman, selectNoneHuman, selectWithPointerHuman   , repeatHuman, repeatHumanTransition   , repeatLastHuman, repeatLastHumanTransition@@ -31,7 +30,7 @@     -- * Internal operations   , chooseItemDialogModeLore, permittedProjectClient, projectCheck   , xhairLegalEps, posFromXhair-  , permittedApplyClient, selectAid, eitherHistory, endAiming, endAimingMsg+  , permittedApplyClient, eitherHistory, endAiming, endAimingMsg   , doLook, flashAiming #endif   ) where@@ -40,7 +39,7 @@  import Game.LambdaHack.Core.Prelude -import           Data.Either (fromRight)+import           Data.Either import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.Map.Strict as M@@ -72,7 +71,6 @@ import           Game.LambdaHack.Client.UI.SessionUI import           Game.LambdaHack.Client.UI.Slideshow import           Game.LambdaHack.Client.UI.SlideshowM-import           Game.LambdaHack.Client.UI.UIOptions import           Game.LambdaHack.Common.Actor import           Game.LambdaHack.Common.ActorState import           Game.LambdaHack.Common.ClientOptions@@ -92,7 +90,6 @@ import           Game.LambdaHack.Common.Vector import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Content.ModeKind as MK-import qualified Game.LambdaHack.Content.PlaceKind as PK import           Game.LambdaHack.Content.RuleKind import qualified Game.LambdaHack.Definition.Ability as Ability import qualified Game.LambdaHack.Definition.Color as Color@@ -100,7 +97,7 @@  -- * Macro -macroHuman :: (MonadClient m, MonadClientUI m) => [String] -> m ()+macroHuman :: MonadClientUI m => [String] -> m () macroHuman ks = do   modifySession $ \sess ->     let kms = K.mkKM <$> ks@@ -108,7 +105,7 @@            macroHumanTransition kms (smacroFrame sess) (smacroStack sess)     in sess { smacroFrame = smacroFrameNew             , smacroStack = smacroStackMew }-  msgAdd MsgMacroOperation $ "Macro activated:" <+> T.pack (intercalate " " ks)+  msgAdd MsgMacroOperation $ "Macro activated:" <+> T.pack (unwords ks)  -- | Push a new macro frame to the stack whenever repeating a macro. macroHumanTransition :: [K.KM] -> KeyMacroFrame -> [KeyMacroFrame]@@ -121,12 +118,11 @@  -- | Display items from a given container store and possibly let the user -- chose one.-chooseItemHuman :: (MonadClient m, MonadClientUI m)-                => ItemDialogMode -> m MError-chooseItemHuman c = either Just (const Nothing) <$> chooseItemDialogMode False c+chooseItemHuman :: MonadClientUI m => ActorId -> ItemDialogMode -> m MError+chooseItemHuman leader c =+  either Just (const Nothing) <$> chooseItemDialogMode leader False c -chooseItemDialogModeLore :: (MonadClient m, MonadClientUI m)-                         => m (Maybe ResultItemDialogMode)+chooseItemDialogModeLore :: MonadClientUI m => m (Maybe ResultItemDialogMode) chooseItemDialogModeLore = do   schosenLore <- getsSession schosenLore   (inhabitants, embeds) <- case schosenLore of@@ -155,9 +151,10 @@           modifySession $ \sess -> sess {schosenLore = ChosenNothing}           return Nothing -chooseItemDialogMode :: (MonadClient m, MonadClientUI m)-                     => Bool -> ItemDialogMode -> m (FailOrCmd ItemDialogMode)-chooseItemDialogMode permitLoreCycle c = do+chooseItemDialogMode :: MonadClientUI m+                     => ActorId -> Bool -> ItemDialogMode+                     -> m (FailOrCmd ActorId)+chooseItemDialogMode leader0 permitLoreCycle c = do   CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui   FontSetup{..} <- getFontSetup   side <- getsClient sside@@ -169,11 +166,13 @@     case mggiLore of       Just rlore -> return (Right rlore, True)       Nothing -> do-        ggi <- getStoreItem c+        ggi <- getStoreItem leader0 c         return (ggi, False)+  -- Pointman could have been changed in @getStoreItem@ above.+  mleader <- getsClient sleader+  let leader = fromMaybe (error "UI manipulation killed the pointman") mleader   recordHistory  -- item chosen, wipe out already shown msgs-  leader <- getLeaderUI-  actorCurAndMaxSk <- leaderSkillsClientUI+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   let meleeSkill = Ability.getSk Ability.SkHurtMelee actorCurAndMaxSk   bUI <- getsSession $ getActorUI leader   case ggi of@@ -181,7 +180,7 @@       RStore fromCStore [iid] -> do         modifySession $ \sess ->           sess {sitemSel = Just (iid, fromCStore, False)}-        return $ Right $ MStore fromCStore+        return $ Right leader       RStore{} -> error $ "" `showFailure` result       ROrgans iid itemBag lSlots -> do         let blurb itemFull =@@ -192,9 +191,12 @@               makeSentence [ partActor bUI, "is aware of"                            , MU.AW $ blurb itemFull ]             ix0 = fromMaybe (error $ "" `showFailure` result)-                  $ findIndex (== iid) $ EM.elems lSlots-        go <- displayItemLore itemBag meleeSkill promptFun ix0 lSlots-        if go then chooseItemDialogMode False MOrgans else failWith "never mind"+                  $ elemIndex iid $ EM.elems lSlots+        km <- displayItemLore itemBag meleeSkill promptFun ix0 lSlots False+        case K.key km of+          K.Space -> chooseItemDialogMode leader False MOrgans+          K.Esc -> failWith "never mind"+          _ -> error $ "" `showFailure` km       ROwned iid -> do         found <- getsState $ findIid leader side iid         let (newAid, bestStore) = case leader `lookup` found of@@ -207,28 +209,24 @@         arena <- getArenaUI         b2 <- getsState $ getActorBody newAid         let (autoDun, _) = autoDungeonLevel fact-        if | newAid == leader -> return $ Right MOwned+        if | newAid == leader -> return $ Right leader            | blid b2 /= arena && autoDun ->              failSer NoChangeDunLeader            | otherwise -> do-             -- We switch leader only here, not in lore screens, because-             -- lore is only about inspecting items, no activation submenu.+             -- We switch leader only here, not when processing results+             -- of lore screens, because lore is only about inspecting items.              void $ pickLeader True newAid-             return $ Right MOwned+             return $ Right newAid       RSkills slotIndex0 -> do+        -- This can be used in the future, e.g., to increase stats from+        -- level-up stat points, so let's keep it even if it shows+        -- no extra info compared to right pane display in menu.         let slotListBound = length skillSlots - 1             displayOneSlot slotIndex = do-              b <- getsState $ getActorBody leader-              let skill = skillSlots !! slotIndex-                  valueText = skillToDecorator skill b-                              $ Ability.getSk skill actorCurAndMaxSk-                  prompt2 = makeSentence-                    [ MU.WownW (partActor bUI) (MU.Text $ skillName skill)-                    , "is", MU.Text valueText ]-                  ov0 = EM.singleton propFont+              (prompt2, attrString) <- skillCloseUp leader slotIndex+              let ov0 = EM.singleton propFont                         $ offsetOverlay-                        $ indentSplitAttrString rwidth-                        $ textToAS $ skillDesc skill+                        $ splitAttrString rwidth rwidth attrString                   keys = [K.spaceKM, K.escKM]                          ++ [K.upKM | slotIndex /= 0]                          ++ [K.downKM | slotIndex /= slotListBound]@@ -236,7 +234,7 @@               slides <- overlayToSlideshow (rheight - 2) keys (ov0, [])               km <- getConfirms ColorFull keys slides               case K.key km of-                K.Space -> chooseItemDialogMode False MSkills+                K.Space -> chooseItemDialogMode leader False MSkills                 K.Up -> displayOneSlot $ slotIndex - 1                 K.Down -> displayOneSlot $ slotIndex + 1                 K.Esc -> failWith "never mind"@@ -244,7 +242,7 @@         displayOneSlot slotIndex0       RLore slore iid itemBag lSlots -> do         let ix0 = fromMaybe (error $ "" `showFailure` result)-                  $ findIndex (== iid) $ EM.elems lSlots+                  $ elemIndex iid $ EM.elems lSlots             promptFun _ _ _ =               makeSentence [ MU.SubjectVerbSg (partActor bUI) "remember"                            , MU.AW $ MU.Text (headingSLore slore) ]@@ -252,13 +250,13 @@         let lorePending = loreFound && case schosenLore of               ChosenLore [] [] -> False               _ -> True-        km <- displayItemLorePointedAt itemBag meleeSkill promptFun ix0-                                       lSlots lorePending+        km <- displayItemLore itemBag meleeSkill promptFun ix0+                              lSlots lorePending         case K.key km of           K.Space -> do             modifySession $ \sess -> sess {schosenLore = ChosenNothing}-            chooseItemDialogMode False (MLore slore)-          K.Char '~' -> chooseItemDialogMode True c+            chooseItemDialogMode leader False (MLore slore)+          K.Char '~' -> chooseItemDialogMode leader True c           K.Esc -> do             modifySession $ \sess -> sess {schosenLore = ChosenNothing}             failWith "never mind"@@ -266,42 +264,17 @@       RPlaces slotIndex0 -> do         COps{coplace} <- getsState scops         soptions <- getsClient soptions-        places <- getsState $ EM.assocs . placesFromState coplace soptions+        -- This is computed just once for the whole series of up and down arrow+        -- navigations, avoid quadratic blowup.+        places <- getsState $ EM.assocs+                              . placesFromState coplace (sexposePlaces soptions)         let slotListBound = length places - 1             displayOneSlot slotIndex = do-              let (pk, (es, ne, na, _)) = places !! slotIndex-                  pkind = okind coplace pk-                  prompt2 = makeSentence-                    [ MU.SubjectVerbSg (partActor bUI) "remember"-                    , MU.Text $ PK.pname pkind ]-                  freqsText = "Frequencies:" <+> T.intercalate " "-                    (map (\(grp, n) -> "(" <> fromGroupName grp-                                       <> ", " <> tshow n <> ")")-                     $ PK.pfreq pkind)-                  onLevels | ES.null es = []-                           | otherwise =-                    [makeSentence-                       [ "Appears on"-                       , MU.CarWs (ES.size es) "level" <> ":"-                       , MU.WWandW $ map MU.Car $ sort-                                   $ map (abs . fromEnum) $ ES.elems es ]]-                  placeParts = ["it has" | ne > 0 || na > 0]-                               ++ [MU.CarWs ne "entrance" | ne > 0]-                               ++ ["and" | ne > 0 && na > 0]-                               ++ [MU.CarWs na "surrounding" | na > 0]-                  partsSentence | null placeParts = ""-                                | otherwise = makeSentence placeParts-                  -- Ideally, place layout would be in SquareFont and the rest-                  -- in PropFont, but this is mostly a debug screen, so KISS.-                  ov0 = EM.singleton monoFont-                        $ offsetOverlay-                        $ concatMap (indentSplitAttrString rwidth . textToAS)-                        $ ["", partsSentence]-                          ++ (if sexposePlaces soptions-                              then [ "", freqsText-                                   , "" ] ++ PK.ptopLeft pkind-                              else [])-                          ++ [""] ++ onLevels+              (prompt2, blurbs) <-+                placeCloseUp places (sexposePlaces soptions) slotIndex+              let splitText = splitAttrString rwidth rwidth . textToAS+                  ov0 = attrLinesToFontMap+                        $ map (second (concatMap splitText)) blurbs                   keys = [K.spaceKM, K.escKM]                          ++ [K.upKM | slotIndex /= 0]                          ++ [K.downKM | slotIndex /= slotListBound]@@ -309,7 +282,7 @@               slides <- overlayToSlideshow (rheight - 2) keys (ov0, [])               km <- getConfirms ColorFull keys slides               case K.key km of-                K.Space -> chooseItemDialogMode False MPlaces+                K.Space -> chooseItemDialogMode leader False MPlaces                 K.Up -> displayOneSlot $ slotIndex - 1                 K.Down -> displayOneSlot $ slotIndex + 1                 K.Esc -> failWith "never mind"@@ -331,7 +304,7 @@                     Just cm -> fromMaybe 0 (M.lookup nxtChal cm)                   verb = if victories > 0 then "remember" else "forsee"                   prompt2 = makeSentence-                    [ MU.SubjectVerbSg (partActor bUI) verb+                    [ MU.SubjectVerbSg "you" verb                     , MU.Text $ "the '" <> MK.mname gameMode <> "' adventure" ]                   keys = [K.spaceKM, K.escKM]                          ++ [K.upKM | slotIndex /= 0]@@ -341,7 +314,7 @@               ekm2 <- displayChoiceScreen "" ColorFull True slides keys               let km = either id (error $ "" `showFailure` ekm2) ekm2               case K.key km of-                K.Space -> chooseItemDialogMode False MModes+                K.Space -> chooseItemDialogMode leader False MModes                 K.Up -> displayOneSlot $ slotIndex - 1                 K.Down -> displayOneSlot $ slotIndex + 1                 K.Esc -> failWith "never mind"@@ -352,9 +325,8 @@ -- * ChooseItemProject  chooseItemProjectHuman :: forall m. (MonadClient m, MonadClientUI m)-                       => [HumanCmd.TriggerItem] -> m MError-chooseItemProjectHuman ts = do-  leader <- getLeaderUI+                       => ActorId -> [HumanCmd.TriggerItem] -> m MError+chooseItemProjectHuman leader ts = do   b <- getsState $ getActorBody leader   mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b   let overStash = mstash == Just (blid b, bpos b)@@ -366,7 +338,7 @@         tr : _ -> (HumanCmd.tiverb tr, HumanCmd.tiobject tr)       verb = makePhrase [verb1]       triggerSyms = triggerSymbols ts-  mpsuitReq <- psuitReq+  mpsuitReq <- psuitReq leader   case mpsuitReq of     -- If xhair aim invalid, no item is considered a (suitable) missile.     Left err -> failMsg err@@ -380,20 +352,25 @@           itemFull <- getsState $ itemToFull iid           bag <- getsState $ getBodyStoreBag b fromCStore           case iid `EM.lookup` bag of-            Just _ | either (const False) snd (psuitReqFun itemFull) ->+            Just _ | isRight (psuitReqFun itemFull) ->+              -- The player knows what he's doing. We warn him about range+              -- and experimenting with unknown precious items is fine.               return Nothing             _ -> do               modifySession $ \sess -> sess {sitemSel = Nothing}-              chooseItemProjectHuman ts+              chooseItemProjectHuman leader ts         Nothing -> do           let psuit =                 return $ SuitsSomething $ \_ itemFull _kit ->+                  -- Here the player does not explicitly pick an item,+                  -- so we may exclude precious unknown items, etc.                   either (const False) snd (psuitReqFun itemFull)                   && (null triggerSyms                       || IK.isymbol (itemKind itemFull) `elem` triggerSyms)               prompt = makePhrase ["What", object1, "to"]               promptGeneric = "What to"-          ggi <- getGroupItem psuit prompt promptGeneric verb "fling" stores+          ggi <- getGroupItem leader psuit prompt promptGeneric verb "fling"+                              stores           case ggi of             Right (fromCStore, iid) -> do               modifySession $ \sess ->@@ -402,25 +379,23 @@             Left err -> failMsg err  permittedProjectClient :: MonadClientUI m-                       => m (ItemFull -> Either ReqFailure Bool)-permittedProjectClient = do-  leader <- getLeaderUI-  actorCurAndMaxSk <- leaderSkillsClientUI+                       => ActorId -> m (ItemFull -> Either ReqFailure Bool)+permittedProjectClient leader = do+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   b <- getsState $ getActorBody leader   let skill = Ability.getSk Ability.SkProject actorCurAndMaxSk       calmE = calmEnough b actorCurAndMaxSk   return $ permittedProject False skill calmE -projectCheck :: MonadClientUI m => Point -> m (Maybe ReqFailure)-projectCheck tpos = do-  COps{corule=RuleContent{rXmax, rYmax}, coTileSpeedup} <- getsState scops-  leader <- getLeaderUI+projectCheck :: MonadClientUI m => ActorId -> Point -> m (Maybe ReqFailure)+projectCheck leader tpos = do+  COps{coTileSpeedup} <- getsState scops   eps <- getsClient seps   sb <- getsState $ getActorBody leader   let lid = blid sb       spos = bpos sb   -- Not @ScreenContent@, because not drawing here.-  case bla rXmax rYmax eps spos tpos of+  case bla eps spos tpos of     Nothing -> return $ Just ProjectAimOnself     Just [] -> error $ "project from the edge of level"                        `showFailure` (spos, tpos, sb)@@ -441,10 +416,9 @@ -- -- Note: Simple Perception check is not enough for the check, -- e.g., because the target actor can be obscured by a glass wall.-xhairLegalEps :: MonadClientUI m => m (Either Text Int)-xhairLegalEps = do-  cops@COps{corule=RuleContent{rXmax, rYmax}} <- getsState scops-  leader <- getLeaderUI+xhairLegalEps :: MonadClientUI m => ActorId -> m (Either Text Int)+xhairLegalEps leader = do+  cops@COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops   b <- getsState $ getActorBody leader   lidV <- viewedLevelUI   let !_A = assert (lidV == blid b) ()@@ -479,41 +453,42 @@       else return $ Left "can't fling at a target on remote level"     Just (TVector v) -> do       -- Not @ScreenContent@, because not drawing here.-      let shifted = shiftBounded rXmax rYmax (bpos b) v+      let shifted = shiftBounded rWidthMax rHeightMax (bpos b) v       if shifted == bpos b && v /= Vector 0 0       then return $ Left "selected translation is void"       else findNewEps True shifted  -- @True@, because the goal is vague anyway -posFromXhair :: (MonadClient m, MonadClientUI m) => m (Either Text Point)-posFromXhair = do-  canAim <- xhairLegalEps+posFromXhair :: (MonadClient m, MonadClientUI m)+             => ActorId -> m (Either Text Point)+posFromXhair leader = do+  canAim <- xhairLegalEps leader   case canAim of     Right newEps -> do       -- Modify @seps@, permanently.       modifyClient $ \cli -> cli {seps = newEps}-      mpos <- xhairToPos-      case mpos of-        Nothing -> error $ "" `showFailure` mpos+      mxhairPos <- mxhairToPos+      case mxhairPos of+        Nothing -> error $ "" `showFailure` mxhairPos         Just pos -> do-          munit <- projectCheck pos+          munit <- projectCheck leader pos           case munit of             Nothing -> return $ Right pos             Just reqFail -> return $ Left $ showReqFailure reqFail     Left cause -> return $ Left cause --- | On top of @permittedProjectClient@, it also checks legality+-- | On top of `permittedProjectClient`, it also checks legality -- of aiming at the target and projection range. It also modifies @eps@. psuitReq :: (MonadClient m, MonadClientUI m)-         => m (Either Text (ItemFull -> Either ReqFailure (Point, Bool)))-psuitReq = do-  leader <- getLeaderUI+         => ActorId+         -> m (Either Text (ItemFull -> Either ReqFailure (Point, Bool)))+psuitReq leader = do   b <- getsState $ getActorBody leader   lidV <- viewedLevelUI   if lidV /= blid b   then return $ Left "can't fling on remote level"   else do-    mpos <- posFromXhair-    p <- permittedProjectClient+    mpos <- posFromXhair leader+    p <- permittedProjectClient leader     case mpos of       Left err -> return $ Left err       Right pos -> return $ Right $ \itemFull ->@@ -532,10 +507,9 @@  -- * ChooseItemApply -chooseItemApplyHuman :: forall m. (MonadClient m, MonadClientUI m)-                     => [HumanCmd.TriggerItem] -> m MError-chooseItemApplyHuman ts = do-  leader <- getLeaderUI+chooseItemApplyHuman :: forall m. MonadClientUI m+                     => ActorId -> [HumanCmd.TriggerItem] -> m MError+chooseItemApplyHuman leader ts = do   b <- getsState $ getActorBody leader   mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b   let overStash = mstash == Just (blid b, bpos b)@@ -557,23 +531,23 @@       -- this item, so he knows what he's doing (unless really absurd).       itemFull <- getsState $ itemToFull iid       bag <- getsState $ getBodyStoreBag b fromCStore-      mp <- permittedApplyClient+      mp <- permittedApplyClient leader       case iid `EM.lookup` bag of-        Just kit | either (const False) id-                          (mp (Just fromCStore) itemFull kit) ->+        Just kit | fromRight False (mp (Just fromCStore) itemFull kit) ->           return Nothing         _ -> do           modifySession $ \sess -> sess {sitemSel = Nothing}-          chooseItemApplyHuman ts+          chooseItemApplyHuman leader ts     Nothing -> do       let psuit :: m Suitability           psuit = do-            mp <- permittedApplyClient+            mp <- permittedApplyClient leader             return $ SuitsSomething $ \cstore itemFull kit ->-              either (const False) id (mp cstore itemFull kit)+              fromRight False (mp cstore itemFull kit)               && (null triggerSyms                   || IK.isymbol (itemKind itemFull) `elem` triggerSyms)-      ggi <- getGroupItem psuit prompt promptGeneric verb "trigger" stores+      ggi <- getGroupItem leader psuit prompt promptGeneric verb "trigger"+                          stores       case ggi of         Right (fromCStore, iid) -> do           modifySession $ \sess ->@@ -582,20 +556,21 @@         Left err -> failMsg err  permittedApplyClient :: MonadClientUI m-                     => m (Maybe CStore -> ItemFull -> ItemQuant+                     => ActorId+                     -> m (Maybe CStore -> ItemFull -> ItemQuant                            -> Either ReqFailure Bool)-permittedApplyClient = do-  leader <- getLeaderUI-  actorCurAndMaxSk <- leaderSkillsClientUI+permittedApplyClient leader = do+  COps{corule} <- getsState scops+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   b <- getsState $ getActorBody leader   let skill = Ability.getSk Ability.SkApply actorCurAndMaxSk       calmE = calmEnough b actorCurAndMaxSk   localTime <- getsState $ getLocalTime (blid b)-  return $ permittedApply localTime skill calmE+  return $ permittedApply corule localTime skill calmE  -- * PickLeader -pickLeaderHuman :: (MonadClient m, MonadClientUI m) => Int -> m MError+pickLeaderHuman :: MonadClientUI m => Int -> m MError pickLeaderHuman k = do   side <- getsClient sside   fact <- getsState $ (EM.! side) . sfactionD@@ -621,31 +596,25 @@  -- * PickLeaderWithPointer -pickLeaderWithPointerHuman :: (MonadClient m, MonadClientUI m) => m MError+pickLeaderWithPointerHuman :: MonadClientUI m => ActorId -> m MError pickLeaderWithPointerHuman = pickLeaderWithPointer --- * MemberCycle+-- * PointmanCycle --- | Switch current member to the next on the viewed level, if any, wrapping.-memberCycleLevelHuman :: (MonadClient m, MonadClientUI m)-                      => Direction -> m MError-memberCycleLevelHuman = memberCycleLevel True+-- | Switch current pointman to the next on the viewed level, if any, wrapping.+pointmanCycleLevelHuman :: MonadClientUI m => ActorId -> Direction -> m MError+pointmanCycleLevelHuman leader = pointmanCycleLevel leader True --- * MemberBack+-- * PointmanBack --- | Switch current member to the previous in the whole dungeon, wrapping.-memberCycleHuman :: (MonadClient m, MonadClientUI m) => Direction -> m MError-memberCycleHuman = memberCycle True+-- | Switch current pointman to the previous in the whole dungeon, wrapping.+pointmanCycleHuman :: MonadClientUI m => ActorId -> Direction -> m MError+pointmanCycleHuman leader = pointmanCycle leader True  -- * SelectActor -selectActorHuman :: (MonadClient m, MonadClientUI m) => m ()-selectActorHuman = do-  leader <- getLeaderUI-  selectAid leader--selectAid :: (MonadClient m, MonadClientUI m) => ActorId -> m ()-selectAid leader = do+selectActorHuman :: MonadClientUI m => ActorId -> m ()+selectActorHuman leader = do   bodyUI <- getsSession $ getActorUI leader   wasMemeber <- getsSession $ ES.member leader . sselected   let upd = if wasMemeber@@ -659,7 +628,7 @@  -- * SelectNone -selectNoneHuman :: (MonadClient m, MonadClientUI m) => m ()+selectNoneHuman :: MonadClientUI m => m () selectNoneHuman = do   side <- getsClient sside   lidV <- viewedLevelUI@@ -678,9 +647,9 @@  -- * SelectWithPointer -selectWithPointerHuman :: (MonadClient m, MonadClientUI m) => m MError+selectWithPointerHuman :: MonadClientUI m => m MError selectWithPointerHuman = do-  COps{corule=RuleContent{rYmax}} <- getsState scops+  COps{corule=RuleContent{rHeightMax}} <- getsState scops   lidV <- viewedLevelUI   -- Not @ScreenContent@, because not drawing here.   side <- getsClient sside@@ -692,15 +661,15 @@   pUI <- getsSession spointer   let p@(Point px py) = squareToMap $ uiToSquare pUI   -- Select even if no space in status line for the actor's symbol.-  if | py == rYmax + 1 && px == 0 -> selectNoneHuman >> return Nothing-     | py == rYmax + 1 ->+  if | py == rHeightMax + 1 && px == 0 -> selectNoneHuman >> return Nothing+     | py == rHeightMax + 1 ->          case drop (px - 1) viewed of            [] -> failMsg "not pointing at an actor"-           (aid, _, _) : _ -> selectAid aid >> return Nothing+           (aid, _, _) : _ -> selectActorHuman aid >> return Nothing      | otherwise ->          case find (\(_, b) -> bpos b == p) ours of            Nothing -> failMsg "not pointing at an actor"-           Just (aid, _) -> selectAid aid >> return Nothing+           Just (aid, _) -> selectActorHuman aid >> return Nothing  -- * Repeat @@ -739,7 +708,7 @@ -- * Record  -- | Starts and stops recording of macros.-recordHuman :: (MonadClient m, MonadClientUI m) => m ()+recordHuman :: MonadClientUI m => m () recordHuman = do   smacroFrameOld <- getsSession smacroFrame   let (smacroFrameNew, msg) = recordHumanTransition smacroFrameOld@@ -761,19 +730,18 @@  -- * AllHistory -allHistoryHuman :: (MonadClient m, MonadClientUI m) => m ()+allHistoryHuman :: MonadClientUI m => m () allHistoryHuman = eitherHistory True -eitherHistory :: forall m. (MonadClient m, MonadClientUI m) => Bool -> m ()+eitherHistory :: forall m. MonadClientUI m => Bool -> m () eitherHistory showAll = do   CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui-  UIOptions{uHistory1PerLine} <- getsSession sUIOptions   history <- getsSession shistory   arena <- getArenaUI   localTime <- getsState $ getLocalTime arena   global <- getsState stime   FontSetup{..} <- getFontSetup-  let renderedHistoryRaw = renderHistory uHistory1PerLine history+  let renderedHistoryRaw = renderHistory history       histBoundRaw = length renderedHistoryRaw       placeholderLine = textFgToAS Color.BrBlack         "Newest_messages_are_at_the_bottom._Press_END_to_get_there."@@ -783,16 +751,17 @@                         ++ renderedHistoryRaw       histBound = placeholderCount + histBoundRaw       splitRow as =-        let (spNo, spYes) = span (/= Color.spaceAttrW32) as-            par1 = case filter (/= emptyAttrLine) $ linesAttr spYes of+        let (tLab, tDesc) = span (/= Color.spaceAttrW32) as+            labLen = textSize monoFont tLab+            par1 = case filter (/= emptyAttrLine) $ linesAttr tDesc of               [] -> emptyAttrLine               [l] -> l               ls -> attrStringToAL $ intercalate [Color.spaceAttrW32]                                    $ map attrLine ls-        in (attrStringToAL spNo, (textSize monoFont spNo, par1))-      (histLab, histDesc) = unzip $ map splitRow renderedHistory-      rhLab = EM.singleton monoFont $ offsetOverlay histLab-      rhDesc = EM.singleton propFont $ offsetOverlayX histDesc+        in (attrStringToAL tLab, (labLen, par1))+      (tsLab, tsDesc) = unzip $ map splitRow renderedHistory+      ovs = EM.insertWith (++) monoFont (offsetOverlay tsLab)+            $ EM.singleton propFont $ offsetOverlayX tsDesc       turnsGlobal = global `timeFitUp` timeTurn       turnsLocal = localTime `timeFitUp` timeTurn       msg = makeSentence@@ -810,14 +779,14 @@         : K.mkChar '.' #endif         : [K.escKM]-  okxs <- overlayToSlideshow (rheight - 2) keysAllHistory-                             (EM.unionWith (++) rhLab rhDesc, kxs)-  let maxIx = length (concatMap snd $ slideshow okxs) - 1+  slides <- overlayToSlideshow (rheight - 2) keysAllHistory (ovs, kxs)+  let maxIx = length (concatMap snd $ slideshow slides) - 1       menuName = "history"   modifySession $ \sess ->     sess {smenuIxMap = M.insert menuName maxIx $ smenuIxMap sess}   let displayAllHistory = do-        ekm <- displayChoiceScreen menuName ColorFull False okxs keysAllHistory+        ekm <- displayChoiceScreen menuName ColorFull False slides+                                   keysAllHistory         case ekm of           Left km | km == K.mkChar '.' -> do             let t = T.unlines $ map (T.pack . map Color.charFromW32)@@ -836,19 +805,9 @@         let timeReport = case drop histSlot renderedHistoryRaw of               [] -> error $ "" `showFailure` histSlot               tR : _ -> tR-            ov0 =-              let (spNo, spYes) = span (/= Color.spaceAttrW32) timeReport-                  lenNo = textSize monoFont spNo-                  spYesX = case splitAttrString (rwidth - lenNo - 1) rwidth-                                                spYes of-                    [] -> []-                    l : ls ->-                      ( lenNo-                      , firstParagraph $ Color.spaceAttrW32 : attrLine l )-                      : map (0,) ls-              in EM.insertWith (++) monoFont-                               (offsetOverlay [attrStringToAL spNo])-                 $ EM.singleton propFont $ offsetOverlayX spYesX+            (ovLab, ovDesc) = labDescOverlay monoFont rwidth timeReport+            ov0 = EM.insertWith (++) monoFont ovLab+                  $ EM.singleton propFont ovDesc             prompt = makeSentence               [ "the", MU.Ordinal $ histSlot + 1               , "most recent record follows" ]@@ -856,8 +815,8 @@                    ++ [K.upKM | histSlot /= 0]                    ++ [K.downKM | histSlot /= histBoundRaw - 1]         msgAdd MsgPromptGeneric prompt-        slides <- overlayToSlideshow (rheight - 2) keys (ov0, [])-        km <- getConfirms ColorFull keys slides+        slides2 <- overlayToSlideshow (rheight - 2) keys (ov0, [])+        km <- getConfirms ColorFull keys slides2         case K.key km of           K.Space -> displayAllHistory           K.Up -> displayOneReport $ histSlot - 1@@ -870,13 +829,13 @@  -- * LastHistory -lastHistoryHuman :: (MonadClient m, MonadClientUI m) => m ()+lastHistoryHuman :: MonadClientUI m => m () lastHistoryHuman = eitherHistory False  -- * MarkVision  markVisionHuman :: MonadClientUI m => m ()-markVisionHuman = modifySession toggleMarkVision+markVisionHuman = modifySession cycleMarkVision  -- * MarkSmell @@ -906,39 +865,51 @@  -- * PrintScreen -printScreenHuman :: (MonadClient m, MonadClientUI m) => m ()+printScreenHuman :: MonadClientUI m => m () printScreenHuman = do   msgAdd MsgActionAlert "Screenshot printed."   printScreen  -- * Cancel --- | End aiming mode, rejecting the current position.+-- | End aiming mode, rejecting the current position, unless when on+-- remote level, in which case, return to our level. cancelHuman :: MonadClientUI m => m () cancelHuman = do-  saimMode <- getsSession saimMode-  when (isJust saimMode) clearAimMode+  maimMode <- getsSession saimMode+  case maimMode of+    Just aimMode -> do+      let lidV = aimLevelId aimMode+      lidOur <- getArenaUI+      if lidV == lidOur+      then clearAimMode+      else do+        xhairPos <- xhairToPos+        let sxhair = Just $ TPoint TKnown lidOur xhairPos+        modifySession $ \sess ->+          sess {saimMode = Just aimMode {aimLevelId = lidOur}}+        setXHairFromGUI sxhair+        doLook+    Nothing -> return ()  -- * Accept  -- | Accept the current crosshair position as target, ending -- aiming mode, if active.-acceptHuman :: (MonadClient m, MonadClientUI m) => m ()-acceptHuman = do-  endAiming-  endAimingMsg+acceptHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m ()+acceptHuman leader = do+  endAiming leader+  endAimingMsg leader   clearAimMode  -- | End aiming mode, accepting the current position.-endAiming :: (MonadClient m, MonadClientUI m) => m ()-endAiming = do-  leader <- getLeaderUI+endAiming :: (MonadClient m, MonadClientUI m) => ActorId -> m ()+endAiming leader = do   sxhair <- getsSession sxhair   modifyClient $ updateTarget leader $ const sxhair -endAimingMsg :: (MonadClient m, MonadClientUI m) => m ()-endAimingMsg = do-  leader <- getLeaderUI+endAimingMsg :: MonadClientUI m => ActorId -> m ()+endAimingMsg leader = do   subject <- partActorLeader leader   tgt <- getsClient $ getTarget leader   (mtargetMsg, _) <- targetDesc tgt@@ -951,7 +922,7 @@ -- * DetailCycle  -- | Cycle detail level of aiming mode descriptions, starting up.-detailCycleHuman :: (MonadClient m, MonadClientUI m) => m ()+detailCycleHuman :: MonadClientUI m => m () detailCycleHuman = do   modifySession $ \sess -> sess {saimMode =     (\aimMode -> aimMode {detailLevel = detailCycle $ detailLevel aimMode})@@ -963,30 +934,43 @@  -- * ClearTargetIfItemClear -clearTargetIfItemClearHuman :: (MonadClient m, MonadClientUI m) => m ()-clearTargetIfItemClearHuman = do+clearTargetIfItemClearHuman :: (MonadClient m, MonadClientUI m)+                            => ActorId -> m ()+clearTargetIfItemClearHuman leader = do   itemSel <- getsSession sitemSel   when (isNothing itemSel) $ do     setXHairFromGUI Nothing-    leader <- getLeaderUI     modifyClient $ updateTarget leader (const Nothing)     doLook  -- | Perform look around in the current position of the xhair. -- Does nothing outside aiming mode.-doLook :: (MonadClient m, MonadClientUI m) => m ()+doLook :: MonadClientUI m => m () doLook = do   saimMode <- getsSession saimMode   case saimMode of-    Nothing -> return ()     Just aimMode -> do-      leader <- getLeaderUI       let lidV = aimLevelId aimMode-      mxhairPos <- xhairToPos-      b <- getsState $ getActorBody leader-      let xhairPos = fromMaybe (bpos b) mxhairPos-      blurb <- lookAtPosition lidV xhairPos-      mapM_ (uncurry msgAdd) blurb+      mxhairPos <- mxhairToPos+      xhairPos <- xhairToPos+      blurb <- lookAtPosition xhairPos lidV+      itemSel <- getsSession sitemSel+      mleader <- getsClient sleader+      outOfRangeBlurb <- case (itemSel, mxhairPos, mleader) of+        (Just (iid, _, _), Just pos, Just leader) -> do+          b <- getsState $ getActorBody leader+          if lidV /= blid b  -- no range warnings on remote levels+             || detailLevel aimMode < DetailAll  -- no spam+          then return []+          else do+            itemFull <- getsState $ itemToFull iid+            let arItem = aspectRecordFull itemFull+            return [ (MsgPromptGeneric, "This position is out of range when flinging the selected item.")+                   | 1 + IA.totalRange arItem (itemKind itemFull)+                     < chessDist (bpos b) pos ]+        _ -> return []+      mapM_ (uncurry msgAdd) $ blurb ++ outOfRangeBlurb+    _ -> return ()  -- * ItemClear @@ -996,24 +980,24 @@ -- * MoveXhair  -- | Move the xhair. Assumes aiming mode.-moveXhairHuman :: (MonadClient m, MonadClientUI m) => Vector -> Int -> m MError+moveXhairHuman :: MonadClientUI m => Vector -> Int -> m MError moveXhairHuman dir n = do-  COps{corule=RuleContent{rXmax, rYmax}} <- getsState scops-  leader <- getLeaderUI-  saimMode <- getsSession saimMode-  let lidV = maybe (error $ "" `showFailure` leader) aimLevelId saimMode   -- Not @ScreenContent@, because not drawing here.-  lpos <- getsState $ bpos . getActorBody leader+  COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops+  saimMode <- getsSession saimMode+  let lidV = maybe (error $ "" `showFailure` (dir, n)) aimLevelId saimMode   xhair <- getsSession sxhair-  mxhairPos <- xhairToPos-  let xhairPos = fromMaybe lpos mxhairPos-      shiftB pos = shiftBounded rXmax rYmax pos dir+  xhairPos <- xhairToPos+  let shiftB pos = shiftBounded rWidthMax rHeightMax pos dir       newPos = iterate shiftB xhairPos !! n   if newPos == xhairPos then failMsg "never mind"   else do-    let sxhair = case xhair of-          Just TVector{} -> Just $ TVector $ newPos `vectorToFrom` lpos-          _ -> Just $ TPoint TKnown lidV newPos+    mleader <- getsClient sleader+    sxhair <- case (xhair, mleader) of+     (Just TVector{}, Just leader) -> do+       lpos <- getsState $ bpos . getActorBody leader+       return $ Just $ TVector $ newPos `vectorToFrom` lpos+     _ -> return $ Just $ TPoint TKnown lidV newPos     setXHairFromGUI sxhair     doLook     return Nothing@@ -1021,7 +1005,7 @@ -- * AimTgt  -- | Start aiming.-aimTgtHuman :: (MonadClient m, MonadClientUI m) => m ()+aimTgtHuman :: MonadClientUI m => m () aimTgtHuman = do   -- (Re)start aiming at the current level.   lidV <- viewedLevelUI@@ -1034,32 +1018,33 @@ -- * AimFloor  -- | Cycle aiming mode. Do not change position of the xhair,--- switch among things at that position.-aimFloorHuman :: (MonadClient m, MonadClientUI m) => m ()+-- switch target to point at different things at that position.+aimFloorHuman :: MonadClientUI m => m () aimFloorHuman = do   lidV <- viewedLevelUI-  leader <- getLeaderUI-  lpos <- getsState $ bpos . getActorBody leader-  mxhairPos <- xhairToPos+  mleader <- getsClient sleader+  mlpos <- case mleader of+    Nothing -> return Nothing+    Just leader -> getsState $ Just . bpos . getActorBody leader+  xhairPos <- xhairToPos   xhair <- getsSession sxhair   saimMode <- getsSession saimMode   bsAll <- getsState $ actorAssocs (const True) lidV   side <- getsClient sside   fact <- getsState $ (EM.! side) . sfactionD-  let xhairPos = fromMaybe lpos mxhairPos-      sxhair = case xhair of+  let sxhair = case xhair of         _ | isNothing saimMode ->  -- first key press: keep target           xhair         Just TEnemy{} -> Just $ TPoint TKnown lidV xhairPos         Just TNonEnemy{} -> Just $ TPoint TKnown lidV xhairPos-        Just TPoint{} | xhairPos /= lpos ->+        Just TPoint{} | Just lpos <- mlpos, xhairPos /= lpos ->           Just $ TVector $ xhairPos `vectorToFrom` lpos         Just TVector{} ->           -- If many actors, we pick here the first that would be picked           -- by '*', so that all other projectiles on the tile come next,           -- when pressing '*', without any intervening actors from other tiles.           -- This is why we use @actorAssocs@ above instead of @posToAidAssocs@.-          case find (\(_, b) -> Just (bpos b) == mxhairPos) bsAll of+          case find (\(_, b) -> bpos b == xhairPos) bsAll of             Just (aid, b) -> Just $ if isFoe side fact (bfid b)                                     then TEnemy aid                                     else TNonEnemy aid@@ -1073,20 +1058,24 @@  -- * AimEnemy -aimEnemyHuman :: (MonadClient m, MonadClientUI m) => m ()+aimEnemyHuman :: MonadClientUI m => m () aimEnemyHuman = do   lidV <- viewedLevelUI-  leader <- getLeaderUI-  lpos <- getsState $ bpos . getActorBody leader-  mxhairPos <- xhairToPos+  mleader <- getsClient sleader+  mlpos <- case mleader of+    Nothing -> return Nothing+    Just leader -> getsState $ Just . bpos . getActorBody leader+  mxhairPos <- mxhairToPos   xhair <- getsSession sxhair   saimMode <- getsSession saimMode   side <- getsClient sside   fact <- getsState $ (EM.! side) . sfactionD   bsAll <- getsState $ actorAssocs (const True) lidV   let -- On the same position, big actors come before projectiles.-      ordPos (_, b) = (chessDist lpos $ bpos b, bpos b, bproj b)-      dbs = sortOn ordPos bsAll+      ordPos lpos (_, b) = (chessDist lpos $ bpos b, bpos b, bproj b)+      dbs = case mlpos of+        Nothing -> bsAll+        Just lpos -> sortOn (ordPos lpos) bsAll       pickUnderXhair =  -- switch to the actor under xhair, if any         fromMaybe (-1) $ findIndex ((== mxhairPos) . Just . bpos . snd) dbs       (pickEnemies, i) = case xhair of@@ -1117,13 +1106,15 @@  -- * AimItem -aimItemHuman :: (MonadClient m, MonadClientUI m) => m ()+aimItemHuman :: MonadClientUI m => m () aimItemHuman = do   side <- getsClient sside   lidV <- viewedLevelUI-  leader <- getLeaderUI-  lpos <- getsState $ bpos . getActorBody leader-  mxhairPos <- xhairToPos+  mleader <- getsClient sleader+  mlpos <- case mleader of+    Nothing -> return Nothing+    Just leader -> getsState $ Just . bpos . getActorBody leader+  mxhairPos <- mxhairToPos   xhair <- getsSession sxhair   saimMode <- getsSession saimMode   Level{lfloor} <- getLevel lidV@@ -1133,8 +1124,10 @@         Just (lid, pos) | lid == lidV -> EM.delete pos lfloor         _ -> lfloor       bsAll = EM.keys lfloorBarStash-      ordPos p = (chessDist lpos p, p)-      dbs = sortOn ordPos bsAll+      ordPos lpos p = (chessDist lpos p, p)+      dbs = case mlpos of+        Nothing -> bsAll+        Just lpos -> sortOn (ordPos lpos) bsAll       pickUnderXhair =  -- switch to the item under xhair, if any         let i = fromMaybe (-1)                 $ findIndex ((== mxhairPos) . Just) dbs@@ -1142,11 +1135,11 @@       (lt, gt) = case xhair of         Just (TPoint _ lid pos)           | isJust saimMode && lid == lidV ->  -- pick next item-            let i = fromMaybe (-1) $ findIndex (== pos) dbs+            let i = fromMaybe (-1) $ elemIndex pos dbs             in splitAt (i + 1) dbs         Just (TPoint _ lid pos)           | lid == lidV ->  -- first key press, retarget old item-            let i = fromMaybe (-1) $ findIndex (== pos) dbs+            let i = fromMaybe (-1) $ elemIndex pos dbs             in splitAt i dbs         _ -> pickUnderXhair       gtlt = gt ++ lt@@ -1164,7 +1157,7 @@  -- | Change the displayed level in aiming mode to (at most) -- k levels shallower. Enters aiming mode, if not already in one.-aimAscendHuman :: (MonadClient m, MonadClientUI m) => Int -> m MError+aimAscendHuman :: MonadClientUI m => Int -> m MError aimAscendHuman k = do   dungeon <- getsState sdungeon   lidV <- viewedLevelUI@@ -1176,11 +1169,8 @@             [] -> lid             nlid : _ -> nlid           lidK = iterate ascendOne lidV !! abs k-      leader <- getLeaderUI-      lpos <- getsState $ bpos . getActorBody leader-      mxhairPos <- xhairToPos-      let xhairPos = fromMaybe lpos mxhairPos-          sxhair = Just $ TPoint TKnown lidK xhairPos+      xhairPos <- xhairToPos+      let sxhair = Just $ TPoint TKnown lidK xhairPos       modifySession $ \sess -> sess {saimMode =         let newDetail = maybe defaultDetailLevel detailLevel (saimMode sess)         in Just $ AimMode lidK newDetail}@@ -1220,9 +1210,8 @@  -- * XhairUnknown -xhairUnknownHuman :: (MonadClient m, MonadClientUI m) => m MError-xhairUnknownHuman = do-  leader <- getLeaderUI+xhairUnknownHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m MError+xhairUnknownHuman leader = do   b <- getsState $ getActorBody leader   mpos <- closestUnknown leader   case mpos of@@ -1235,9 +1224,8 @@  -- * XhairItem -xhairItemHuman :: (MonadClient m, MonadClientUI m) => m MError-xhairItemHuman = do-  leader <- getLeaderUI+xhairItemHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m MError+xhairItemHuman leader = do   b <- getsState $ getActorBody leader   items <- closestItems leader   case items of@@ -1251,9 +1239,9 @@  -- * XhairStair -xhairStairHuman :: (MonadClient m, MonadClientUI m) => Bool -> m MError-xhairStairHuman up = do-  leader <- getLeaderUI+xhairStairHuman :: (MonadClient m, MonadClientUI m)+                => ActorId -> Bool -> m MError+xhairStairHuman leader up = do   b <- getsState $ getActorBody leader   stairs <- closestTriggers (if up then ViaStairsUp else ViaStairsDown) leader   case stairs of@@ -1267,7 +1255,7 @@  -- * XhairPointerFloor -xhairPointerFloorHuman :: (MonadClient m, MonadClientUI m) => m ()+xhairPointerFloorHuman :: MonadClientUI m => m () xhairPointerFloorHuman = do   saimMode <- getsSession saimMode   aimPointerFloorHuman@@ -1276,7 +1264,7 @@  -- * XhairPointerMute -xhairPointerMuteHuman :: (MonadClient m, MonadClientUI m) => m ()+xhairPointerMuteHuman :: MonadClientUI m => m () xhairPointerMuteHuman = do   saimMode <- getsSession saimMode   aimPointerFloorLoud False@@ -1285,7 +1273,7 @@  -- * XhairPointerEnemy -xhairPointerEnemyHuman :: (MonadClient m, MonadClientUI m) => m ()+xhairPointerEnemyHuman :: MonadClientUI m => m () xhairPointerEnemyHuman = do   saimMode <- getsSession saimMode   aimPointerEnemyHuman@@ -1294,17 +1282,17 @@  -- * AimPointerFloor -aimPointerFloorHuman :: (MonadClient m, MonadClientUI m) => m ()+aimPointerFloorHuman :: MonadClientUI m => m () aimPointerFloorHuman = aimPointerFloorLoud True -aimPointerFloorLoud :: (MonadClient m, MonadClientUI m) => Bool -> m ()+aimPointerFloorLoud :: MonadClientUI m => Bool -> m () aimPointerFloorLoud loud = do-  COps{corule=RuleContent{rXmax, rYmax}} <- getsState scops+  COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops   lidV <- viewedLevelUI   -- Not @ScreenContent@, because not drawing here.   pUI <- getsSession spointer-  let p@(Point px py) = squareToMap $ uiToSquare pUI-  if px >= 0 && py >= 0 && px < rXmax && py < rYmax+  let p = squareToMap $ uiToSquare pUI+  if insideP (0, 0, rWidthMax - 1, rHeightMax - 1) p   then do     oldXhair <- getsSession sxhair     let sxhair = Just $ TPoint TUnknown lidV p@@ -1324,14 +1312,14 @@  -- * AimPointerEnemy -aimPointerEnemyHuman :: (MonadClient m, MonadClientUI m) => m ()+aimPointerEnemyHuman :: MonadClientUI m => m () aimPointerEnemyHuman = do-  COps{corule=RuleContent{rXmax, rYmax}} <- getsState scops+  COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops   lidV <- viewedLevelUI   -- Not @ScreenContent@, because not drawing here.   pUI <- getsSession spointer-  let p@(Point px py) = squareToMap $ uiToSquare pUI-  if px >= 0 && py >= 0 && px < rXmax && py < rYmax+  let p = squareToMap $ uiToSquare pUI+  if insideP (0, 0, rWidthMax - 1, rHeightMax - 1) p   then do     bsAll <- getsState $ actorAssocs (const True) lidV     oldXhair <- getsSession sxhair
engine-src/Game/LambdaHack/Client/UI/HandleHumanM.hs view
@@ -1,9 +1,10 @@ -- | Semantics of human player commands. module Game.LambdaHack.Client.UI.HandleHumanM-  ( restrictedCmdSemInCxtOfKM, updateKeyLast+  ( cmdSemInCxtOfKM, updateKeyLast #ifdef EXPOSE_INTERNAL     -- * Internal operations-  , noRemoteHumanCmd, cmdSemInCxtOfKM, cmdSemantics, addNoError+  , noRemoteHumanCmd, CmdLeaderNeed, cmdSemantics, cmdSemanticsLeader+  , addNoError, addLeader, weaveLeader #endif   ) where @@ -13,6 +14,7 @@  import           Game.LambdaHack.Client.MonadClient import           Game.LambdaHack.Client.Request+import           Game.LambdaHack.Client.State import           Game.LambdaHack.Client.UI.HandleHelperM import           Game.LambdaHack.Client.UI.HandleHumanGlobalM import           Game.LambdaHack.Client.UI.HandleHumanLocalM@@ -20,26 +22,7 @@ import qualified Game.LambdaHack.Client.UI.Key as K import           Game.LambdaHack.Client.UI.MonadClientUI import           Game.LambdaHack.Client.UI.SessionUI---- | The semantics of human player commands in terms of the client monad,--- in context of the given @km@ as the last action.------ Some time cosuming commands are enabled even in aiming mode, but cannot be--- invoked in aiming mode on a remote level (level different than--- the level of the leader), which is caught here.-restrictedCmdSemInCxtOfKM :: (MonadClient m, MonadClientUI m)-                          => K.KM -> HumanCmd -> m (Either MError ReqUI)-restrictedCmdSemInCxtOfKM km cmd =-  if noRemoteHumanCmd cmd then do-    -- If in aiming mode, check if the current level is the same-    -- as player level and refuse performing the action otherwise.-    arena <- getArenaUI-    lidV <- viewedLevelUI-    if arena /= lidV then-      weaveJust <$> failWith-        "command disabled on a remote level, press ESC to switch back"-    else cmdSemInCxtOfKM km cmd-  else cmdSemInCxtOfKM km cmd+import           Game.LambdaHack.Common.Types  -- | Commands that are forbidden on a remote level, because they -- would usually take time when invoked on one, but not necessarily do@@ -65,7 +48,13 @@   Record{} -> macroFrame   _ -> macroFrame {keyLast = Just km} --- Semantics of the command in context of the given @km@ as the last action.+-- | The semantics of human player commands in terms of the client monad,+-- in context of the given @km@ as the last action.+--+-- Some time cosuming commands are enabled even in aiming mode, but cannot be+-- invoked in aiming mode on a remote level (level different than+-- the level of the leader). Commands that require a pointman fail+-- when no leader is designated. cmdSemInCxtOfKM :: (MonadClient m, MonadClientUI m)                 => K.KM -> HumanCmd -> m (Either MError ReqUI) cmdSemInCxtOfKM km cmd = do@@ -73,111 +62,168 @@     sess {smacroFrame = updateKeyLast km cmd $ smacroFrame sess}   cmdSemantics cmd +data CmdLeaderNeed m =+    CmdNoNeed (m (Either MError ReqUI))+  | CmdLeader (ActorId -> m (Either MError ReqUI))+ cmdSemantics :: (MonadClient m, MonadClientUI m)              => HumanCmd -> m (Either MError ReqUI)-cmdSemantics cmd = case cmd of+cmdSemantics cmd = case cmdSemanticsLeader cmd of+  CmdNoNeed mreq -> mreq+  CmdLeader f -> do+    mleader <- getsClient sleader+    case mleader of+      Nothing -> weaveJust <$> failWith+        "command disabled when no pointman designated, choose another command"+      Just leader -> do+        if noRemoteHumanCmd cmd then do+          -- If in aiming mode, check if the current level is the same+          -- as player level and refuse performing the action otherwise.+          arena <- getArenaUI+          lidV <- viewedLevelUI+          if arena /= lidV then+            weaveJust <$> failWith+              "command disabled on a remote level, press ESC to switch back"+          else f leader+        else f leader++cmdSemanticsLeader :: (MonadClient m, MonadClientUI m)+                   => HumanCmd -> CmdLeaderNeed m+cmdSemanticsLeader cmd = case cmd of   Macro kms -> addNoError $ macroHuman kms-  ByArea l -> byAreaHuman cmdSemInCxtOfKM l+  ByArea l -> CmdNoNeed $ byAreaHuman cmdSemInCxtOfKM l   ByAimMode AimModeCmd{..} ->-    byAimModeHuman (cmdSemantics exploration) (cmdSemantics aiming)+    CmdNoNeed $ byAimModeHuman (cmdSemantics exploration) (cmdSemantics aiming)   ComposeIfLocal cmd1 cmd2 ->-    composeIfLocalHuman (cmdSemantics cmd1) (cmdSemantics cmd2)+    CmdNoNeed $ composeIfLocalHuman (cmdSemantics cmd1) (cmdSemantics cmd2)   ComposeUnlessError cmd1 cmd2 ->-    composeUnlessErrorHuman (cmdSemantics cmd1) (cmdSemantics cmd2)+    CmdNoNeed $ composeUnlessErrorHuman (cmdSemantics cmd1) (cmdSemantics cmd2)   Compose2ndLocal cmd1 cmd2 ->-    compose2ndLocalHuman (cmdSemantics cmd1) (cmdSemantics cmd2)-  LoopOnNothing cmd1 -> loopOnNothingHuman (cmdSemantics cmd1)-  ExecuteIfClear cmd1 -> executeIfClearHuman (cmdSemantics cmd1)+    CmdNoNeed $ compose2ndLocalHuman (cmdSemantics cmd1) (cmdSemantics cmd2)+  LoopOnNothing cmd1 -> CmdNoNeed $ loopOnNothingHuman (cmdSemantics cmd1)+  ExecuteIfClear cmd1 -> CmdNoNeed $ executeIfClearHuman (cmdSemantics cmd1) -  Wait -> weaveJust <$> (ReqUITimed <$$> waitHuman)-  Wait10 -> weaveJust <$> (ReqUITimed <$$> waitHuman10)-  Yell -> weaveJust <$> (ReqUITimed <$$> yellHuman)-  MoveDir v ->-    weaveJust <$> (ReqUITimed <$$> moveRunHuman True True False False v)-  RunDir v -> weaveJust <$> (ReqUITimed <$$> moveRunHuman True True True True v)-  RunOnceAhead -> ReqUITimed <$$> runOnceAheadHuman-  MoveOnceToXhair -> weaveJust <$> (ReqUITimed <$$> moveOnceToXhairHuman)-  RunOnceToXhair  -> weaveJust <$> (ReqUITimed <$$> runOnceToXhairHuman)-  ContinueToXhair -> weaveJust <$> (ReqUITimed <$$> continueToXhairHuman)+  Wait -> weaveLeader $ \leader -> ReqUITimed <$$> waitHuman leader+  Wait10 -> weaveLeader $ \leader -> ReqUITimed <$$> waitHuman10 leader+  Yell -> weaveLeader $ \leader -> ReqUITimed <$$> yellHuman leader+  MoveDir v -> weaveLeader $ \leader ->+                 ReqUITimed <$$> moveRunHuman leader True True False False v+  RunDir v -> weaveLeader $ \leader ->+                ReqUITimed <$$> moveRunHuman leader True True True True v+  RunOnceAhead ->+    CmdLeader $ \leader -> ReqUITimed <$$> runOnceAheadHuman leader+  MoveOnceToXhair -> weaveLeader $ \leader ->+                       ReqUITimed <$$> moveOnceToXhairHuman leader+  RunOnceToXhair  -> weaveLeader $ \leader ->+                       ReqUITimed <$$> runOnceToXhairHuman leader+  ContinueToXhair -> weaveLeader $ \leader ->+                       ReqUITimed <$$> continueToXhairHuman leader   MoveItem stores toCStore mverb auto ->-    weaveJust <$> (ReqUITimed <$$> moveItemHuman stores toCStore mverb auto)-  Project -> weaveJust <$> (ReqUITimed <$$> projectHuman)-  Apply -> weaveJust <$> (ReqUITimed <$$> applyHuman)-  AlterDir -> weaveJust <$> (ReqUITimed <$$> alterDirHuman)-  AlterWithPointer -> weaveJust <$> (ReqUITimed <$$> alterWithPointerHuman)-  CloseDir -> weaveJust <$> (ReqUITimed <$$> closeDirHuman)-  Help -> helpHuman cmdSemInCxtOfKM-  Hint -> hintHuman cmdSemInCxtOfKM-  ItemMenu -> itemMenuHuman cmdSemInCxtOfKM-  ChooseItemMenu dialogMode -> chooseItemMenuHuman cmdSemInCxtOfKM dialogMode-  MainMenu -> mainMenuHuman cmdSemInCxtOfKM-  MainMenuAutoOn -> mainMenuAutoOnHuman cmdSemInCxtOfKM-  MainMenuAutoOff -> mainMenuAutoOffHuman cmdSemInCxtOfKM-  Dashboard -> dashboardHuman cmdSemInCxtOfKM-  GameTutorialToggle -> gameTutorialToggle-                        >> challengeMenuHuman cmdSemInCxtOfKM-  GameDifficultyIncr -> gameDifficultyIncr-                        >> challengeMenuHuman cmdSemInCxtOfKM-  GameFishToggle -> gameFishToggle >> challengeMenuHuman cmdSemInCxtOfKM-  GameGoodsToggle -> gameGoodsToggle >> challengeMenuHuman cmdSemInCxtOfKM-  GameWolfToggle -> gameWolfToggle >> challengeMenuHuman cmdSemInCxtOfKM-  GameKeeperToggle -> gameKeeperToggle >> challengeMenuHuman cmdSemInCxtOfKM-  GameScenarioIncr -> gameScenarioIncr >> challengeMenuHuman cmdSemInCxtOfKM+    weaveLeader $ \leader ->+      ReqUITimed <$$> moveItemHuman leader stores toCStore mverb auto+  Project -> weaveLeader $ \leader -> ReqUITimed <$$> projectHuman leader+  Apply -> weaveLeader $ \leader -> ReqUITimed <$$> applyHuman leader+  AlterDir -> weaveLeader $ \leader -> ReqUITimed <$$> alterDirHuman leader+  AlterWithPointer ->+    weaveLeader $ \leader -> ReqUITimed <$$> alterWithPointerHuman leader+  CloseDir -> weaveLeader $ \leader -> ReqUITimed <$$> closeDirHuman leader+  Help -> CmdNoNeed $ helpHuman cmdSemInCxtOfKM+  Hint -> CmdNoNeed $ hintHuman cmdSemInCxtOfKM+  ItemMenu -> CmdLeader $ \leader -> itemMenuHuman leader cmdSemInCxtOfKM+  ChooseItemMenu dialogMode ->+    CmdLeader $ \leader -> chooseItemMenuHuman leader cmdSemInCxtOfKM dialogMode+  MainMenu -> CmdNoNeed $ mainMenuHuman cmdSemInCxtOfKM+  MainMenuAutoOn -> CmdNoNeed $ mainMenuAutoOnHuman cmdSemInCxtOfKM+  MainMenuAutoOff -> CmdNoNeed $ mainMenuAutoOffHuman cmdSemInCxtOfKM+  Dashboard -> CmdNoNeed $ dashboardHuman cmdSemInCxtOfKM+  GameTutorialToggle ->+    CmdNoNeed $ gameTutorialToggle >> challengeMenuHuman cmdSemInCxtOfKM+  GameDifficultyIncr ->+    CmdNoNeed $ gameDifficultyIncr >> challengeMenuHuman cmdSemInCxtOfKM+  GameFishToggle ->+    CmdNoNeed $ gameFishToggle >> challengeMenuHuman cmdSemInCxtOfKM+  GameGoodsToggle ->+    CmdNoNeed $ gameGoodsToggle >> challengeMenuHuman cmdSemInCxtOfKM+  GameWolfToggle ->+    CmdNoNeed $ gameWolfToggle >> challengeMenuHuman cmdSemInCxtOfKM+  GameKeeperToggle ->+    CmdNoNeed $ gameKeeperToggle >> challengeMenuHuman cmdSemInCxtOfKM+  GameScenarioIncr ->+    CmdNoNeed $ gameScenarioIncr >> challengeMenuHuman cmdSemInCxtOfKM -  GameRestart -> weaveJust <$> gameRestartHuman-  GameQuit -> weaveJust <$> gameQuitHuman-  GameDrop -> weaveJust <$> fmap Right gameDropHuman-  GameExit -> weaveJust <$> fmap Right gameExitHuman-  GameSave -> weaveJust <$> fmap Right gameSaveHuman-  Doctrine -> weaveJust <$> doctrineHuman-  Automate -> weaveJust <$> automateHuman-  AutomateToggle -> weaveJust <$> automateToggleHuman-  AutomateBack -> automateBackHuman+  GameRestart -> CmdNoNeed $ weaveJust <$> gameRestartHuman+  GameQuit -> CmdNoNeed $ weaveJust <$> gameQuitHuman+  GameDrop -> CmdNoNeed $ weaveJust <$> fmap Right gameDropHuman+  GameExit -> CmdNoNeed $ weaveJust <$> fmap Right gameExitHuman+  GameSave -> CmdNoNeed $ weaveJust <$> fmap Right gameSaveHuman+  Doctrine -> CmdNoNeed $ weaveJust <$> doctrineHuman+  Automate -> CmdNoNeed $ weaveJust <$> automateHuman+  AutomateToggle -> CmdNoNeed $ weaveJust <$> automateToggleHuman+  AutomateBack -> CmdNoNeed automateBackHuman -  ChooseItem dialogMode -> Left <$> chooseItemHuman dialogMode-  ChooseItemProject ts -> Left <$> chooseItemProjectHuman ts-  ChooseItemApply ts -> Left <$> chooseItemApplyHuman ts-  PickLeader k -> Left <$> pickLeaderHuman k-  PickLeaderWithPointer -> Left <$> pickLeaderWithPointerHuman-  MemberCycle direction -> Left <$> memberCycleHuman direction-  MemberCycleLevel direction -> Left <$> memberCycleLevelHuman direction-  SelectActor -> addNoError selectActorHuman+  ChooseItem dialogMode ->+    CmdLeader $ \leader -> Left <$> chooseItemHuman leader dialogMode+  ChooseItemProject ts ->+    CmdLeader $ \leader -> Left <$> chooseItemProjectHuman leader ts+  ChooseItemApply ts ->+    CmdLeader $ \leader -> Left <$> chooseItemApplyHuman leader ts+  PickLeader k -> CmdNoNeed $ Left <$> pickLeaderHuman k+  PickLeaderWithPointer ->+    CmdLeader $ \leader -> Left <$> pickLeaderWithPointerHuman leader+  PointmanCycle direction ->+    CmdLeader $ \leader -> Left <$> pointmanCycleHuman leader direction+  PointmanCycleLevel direction ->+    CmdLeader $ \leader -> Left <$> pointmanCycleLevelHuman leader direction+  SelectActor -> addLeader selectActorHuman   SelectNone -> addNoError selectNoneHuman-  SelectWithPointer -> Left <$> selectWithPointerHuman+  SelectWithPointer -> CmdNoNeed $ Left <$> selectWithPointerHuman   Repeat n -> addNoError $ repeatHuman n   RepeatLast n -> addNoError $ repeatLastHuman n   Record -> addNoError recordHuman   AllHistory -> addNoError allHistoryHuman   LastHistory -> addNoError lastHistoryHuman-  MarkVision -> markVisionHuman >> settingsMenuHuman cmdSemInCxtOfKM-  MarkSmell -> markSmellHuman >> settingsMenuHuman cmdSemInCxtOfKM-  MarkSuspect -> markSuspectHuman >> settingsMenuHuman cmdSemInCxtOfKM-  MarkAnim -> markAnimHuman >> settingsMenuHuman cmdSemInCxtOfKM-  OverrideTut -> overrideTutHuman >> settingsMenuHuman cmdSemInCxtOfKM-  SettingsMenu -> settingsMenuHuman cmdSemInCxtOfKM-  ChallengeMenu -> challengeMenuHuman cmdSemInCxtOfKM+  MarkVision ->+    CmdNoNeed $ markVisionHuman >> settingsMenuHuman cmdSemInCxtOfKM+  MarkSmell ->+    CmdNoNeed $ markSmellHuman >> settingsMenuHuman cmdSemInCxtOfKM+  MarkSuspect ->+    CmdNoNeed $ markSuspectHuman >> settingsMenuHuman cmdSemInCxtOfKM+  MarkAnim ->+    CmdNoNeed $ markAnimHuman >> settingsMenuHuman cmdSemInCxtOfKM+  OverrideTut ->+    CmdNoNeed $ overrideTutHuman >> settingsMenuHuman cmdSemInCxtOfKM+  SettingsMenu -> CmdNoNeed $ settingsMenuHuman cmdSemInCxtOfKM+  ChallengeMenu -> CmdNoNeed $ challengeMenuHuman cmdSemInCxtOfKM   PrintScreen -> addNoError printScreenHuman    Cancel -> addNoError cancelHuman-  Accept -> addNoError acceptHuman+  Accept -> addLeader acceptHuman   DetailCycle -> addNoError detailCycleHuman-  ClearTargetIfItemClear -> addNoError clearTargetIfItemClearHuman+  ClearTargetIfItemClear -> addLeader clearTargetIfItemClearHuman   ItemClear -> addNoError itemClearHuman-  MoveXhair v k -> Left <$> moveXhairHuman v k+  MoveXhair v k -> CmdNoNeed $ Left <$> moveXhairHuman v k   AimTgt -> addNoError aimTgtHuman   AimFloor -> addNoError aimFloorHuman   AimEnemy -> addNoError aimEnemyHuman   AimItem -> addNoError aimItemHuman-  AimAscend k -> Left <$> aimAscendHuman k+  AimAscend k -> CmdNoNeed $ Left <$> aimAscendHuman k   EpsIncr b -> addNoError $ epsIncrHuman b-  XhairUnknown -> Left <$> xhairUnknownHuman-  XhairItem -> Left <$> xhairItemHuman-  XhairStair up -> Left <$> xhairStairHuman up+  XhairUnknown -> CmdLeader $ \leader -> Left <$> xhairUnknownHuman leader+  XhairItem -> CmdLeader $ \leader -> Left <$> xhairItemHuman leader+  XhairStair up -> CmdLeader $ \leader -> Left <$> xhairStairHuman leader up   XhairPointerFloor -> addNoError xhairPointerFloorHuman   XhairPointerMute -> addNoError xhairPointerMuteHuman   XhairPointerEnemy -> addNoError xhairPointerEnemyHuman   AimPointerFloor -> addNoError aimPointerFloorHuman   AimPointerEnemy -> addNoError aimPointerEnemyHuman -addNoError :: Monad m => m () -> m (Either MError ReqUI)-addNoError cmdCli = cmdCli >> return (Left Nothing)+addNoError :: Monad m => m () -> CmdLeaderNeed m+addNoError cmdCli = CmdNoNeed $ cmdCli >> return (Left Nothing)++addLeader :: Monad m => (ActorId -> m ()) -> CmdLeaderNeed m+addLeader cmdCli =+  CmdLeader $ \leader -> cmdCli leader >> return (Left Nothing)++weaveLeader :: Monad m => (ActorId -> m (FailOrCmd ReqUI)) -> CmdLeaderNeed m+weaveLeader cmdCli =+  CmdLeader $ \leader -> weaveJust <$> cmdCli leader
engine-src/Game/LambdaHack/Client/UI/HumanCmd.hs view
@@ -17,10 +17,11 @@ import qualified NLP.Miniutter.English as MU  import Game.LambdaHack.Common.Vector+import Game.LambdaHack.Content.ItemKind (ItemKind) import Game.LambdaHack.Definition.Defs  data CmdCategory =-    CmdMainMenu | CmdDashboard | CmdItemMenu+    CmdDashboard | CmdItemMenu   | CmdMove | CmdItem | CmdAim | CmdMeta | CmdMouse   | CmdInternal | CmdDebug | CmdMinimal   deriving (Show, Read, Eq, Generic)@@ -30,7 +31,6 @@ instance Binary CmdCategory  categoryDescription :: CmdCategory -> Text-categoryDescription CmdMainMenu = "Main menu" categoryDescription CmdDashboard = "Dashboard" categoryDescription CmdItemMenu = "Item menu commands" categoryDescription CmdMove = "Terrain exploration and modification commands"@@ -156,8 +156,8 @@   | ChooseItemApply [TriggerItem]   | PickLeader Int   | PickLeaderWithPointer-  | MemberCycle Direction-  | MemberCycleLevel Direction+  | PointmanCycle Direction+  | PointmanCycleLevel Direction   | SelectActor   | SelectNone   | SelectWithPointer@@ -204,7 +204,7 @@ -- | Description of how item manipulation is triggered and communicated -- to the player. data TriggerItem =-  TriggerItem {tiverb :: MU.Part, tiobject :: MU.Part, tisymbols :: [Char]}+  TriggerItem {tiverb :: MU.Part, tiobject :: MU.Part, tisymbols :: [ContentSymbol ItemKind]}   deriving (Show, Eq, Ord, Generic)  instance Read TriggerItem where
engine-src/Game/LambdaHack/Client/UI/InventoryM.hs view
@@ -2,10 +2,11 @@ module Game.LambdaHack.Client.UI.InventoryM   ( Suitability(..), ResultItemDialogMode(..)   , slotsOfItemDialogMode, getFull, getGroupItem, getStoreItem+  , skillCloseUp, placeCloseUp #ifdef EXPOSE_INTERNAL     -- * Internal operations   , ItemDialogState(..), accessModeBag, storeItemPrompt, getItem-  , DefItemKey(..), transition, keyOfEKM, runDefItemKey+  , DefItemKey(..), transition, keyOfEKM, runDefItemKey, inventoryInRightPane #endif   ) where @@ -16,6 +17,7 @@ import qualified Data.Char as Char import           Data.Either import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES import qualified Data.Text as T import qualified NLP.Miniutter.English as MU @@ -24,6 +26,7 @@ import           Game.LambdaHack.Client.UI.ActorUI 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.HandleHelperM import           Game.LambdaHack.Client.UI.HumanCmd@@ -32,11 +35,13 @@ import           Game.LambdaHack.Client.UI.MonadClientUI import           Game.LambdaHack.Client.UI.Msg import           Game.LambdaHack.Client.UI.MsgM+import           Game.LambdaHack.Client.UI.Overlay import           Game.LambdaHack.Client.UI.SessionUI import           Game.LambdaHack.Client.UI.Slideshow import           Game.LambdaHack.Client.UI.SlideshowM import           Game.LambdaHack.Common.Actor import           Game.LambdaHack.Common.ActorState+import           Game.LambdaHack.Common.ClientOptions import           Game.LambdaHack.Common.Faction import           Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA@@ -46,7 +51,9 @@ import           Game.LambdaHack.Common.State import           Game.LambdaHack.Common.Types import qualified Game.LambdaHack.Content.ItemKind as IK+import qualified Game.LambdaHack.Content.PlaceKind as PK import qualified Game.LambdaHack.Definition.Ability as Ability+import qualified Game.LambdaHack.Definition.Color as Color import           Game.LambdaHack.Definition.Defs  data ItemDialogState = ISuitable | IAll@@ -105,8 +112,9 @@ -- Note that this does not guarantee the chosen item belongs to the group, -- as the player can override the choice. -- Used e.g., for applying and projecting.-getGroupItem :: (MonadClient m, MonadClientUI m)-             => m Suitability+getGroupItem :: MonadClientUI m+             => ActorId+             -> m Suitability                           -- ^ which items to consider suitable              -> Text      -- ^ specific prompt for only suitable items              -> Text      -- ^ generic prompt@@ -114,7 +122,7 @@              -> Text      -- ^ the generic verb to use              -> [CStore]  -- ^ stores to cycle through              -> m (Either Text (CStore, ItemId))-getGroupItem psuit prompt promptGeneric verb verbGeneric stores = do+getGroupItem leader psuit prompt promptGeneric verb verbGeneric stores = do   side <- getsClient sside   mstash <- getsState $ \s -> gstash $ sfactionD s EM.! side   let ppItemDialogBody v body actorSk cCur = case cCur of@@ -123,7 +131,7 @@         MStore CGround | mstash == Just (blid body, bpos body) ->           "greedily attempt to" <+> v <+> ppItemDialogModeIn cCur         _ -> v <+> ppItemDialogModeFrom cCur-  soc <- getFull psuit+  soc <- getFull leader psuit                  (\body _ actorSk cCur _ ->                     prompt <+> ppItemDialogBody verb body actorSk cCur)                  (\body _ actorSk cCur _ ->@@ -138,10 +146,11 @@ -- | Display all items from a store and let the human player choose any -- or switch to any other store. -- Used, e.g., for viewing inventory and item descriptions.-getStoreItem :: (MonadClient m, MonadClientUI m)-             => ItemDialogMode   -- ^ initial mode+getStoreItem :: MonadClientUI m+             => ActorId         -- ^ the pointman+             -> ItemDialogMode  -- ^ initial mode              -> m (Either Text ResultItemDialogMode)-getStoreItem cInitial = do+getStoreItem leader cInitial = do   side <- getsClient sside   let itemCs = map MStore [CStash, CEqp, CGround]         -- No @COrgan@, because triggerable organs are rare and,@@ -156,7 +165,8 @@       post = dropWhile (== cInitial) rest       remCs = post ++ pre       prompt = storeItemPrompt side-  getItem (return SuitsEverything) prompt prompt cInitial remCs True False+  getItem leader (return SuitsEverything) prompt prompt cInitial remCs+          True False  storeItemPrompt :: FactionId                 -> Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State@@ -219,13 +229,8 @@                          $ ouniqGroup coitem IK.S_CURRENCY           dungeonTotal = sgold s           (_, total) = calculateTotal side s-          n = countItems CStash-          verbOwned = if | n == 0 -> "find nothing among"-                         | otherwise -> "review"-      in makePhrase-           [ MU.Text $ spoilsBlurb currencyName total dungeonTotal-           , MU.Capitalize $ MU.SubjectVerbSg subject verbOwned-           , MU.Text t ]+      in T.init $ spoilsBlurb currencyName total dungeonTotal+        -- no space for more, e.g., the pointman, but it can't be changed anyway     MSkills ->       makePhrase         [ MU.Capitalize $ MU.SubjectVerbSg subject "estimate"@@ -246,8 +251,9 @@ -- | Let the human player choose a single, preferably suitable, -- item from a list of items. Don't display stores empty for all actors. -- Start with a non-empty store.-getFull :: (MonadClient m, MonadClientUI m)-        => m Suitability    -- ^ which items to consider suitable+getFull :: MonadClientUI m+        => ActorId+        -> m Suitability    -- ^ which items to consider suitable         -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State             -> Text)        -- ^ specific prompt for only suitable items         -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State@@ -257,8 +263,7 @@                             --   in the starting mode is suitable         -> Bool             -- ^ whether to permit multiple items as a result         -> m (Either Text (CStore, [(ItemId, ItemQuant)]))-getFull psuit prompt promptGeneric stores askWhenLone permitMulitple = do-  leader <- getLeaderUI+getFull leader psuit prompt promptGeneric stores askWhenLone permitMulitple = do   mpsuit <- psuit   let psuitFun = case mpsuit of         SuitsEverything -> \_ _ _ -> True@@ -286,7 +291,7 @@                 post = dropWhile (== cInit) rest             in (MStore cInit, map MStore $ post ++ pre)           (modeFirst, modeRest) = breakStores firstStore-      res <- getItem psuit prompt promptGeneric modeFirst modeRest+      res <- getItem leader psuit prompt promptGeneric modeFirst modeRest                      askWhenLone permitMulitple       case res of         Left t -> return $ Left t@@ -298,8 +303,9 @@  -- | Let the human player choose a single, preferably suitable, -- item from a list of items.-getItem :: (MonadClient m, MonadClientUI m)-        => m Suitability    -- ^ which items to consider suitable+getItem :: MonadClientUI m+        => ActorId+        -> m Suitability    -- ^ which items to consider suitable         -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State             -> Text)        -- ^ specific prompt for only suitable items         -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State@@ -310,29 +316,30 @@                             --   in the starting mode is suitable         -> Bool             -- ^ whether to permit multiple items as a result         -> m (Either Text ResultItemDialogMode)-getItem psuit prompt promptGeneric cCur cRest askWhenLone permitMulitple = do-  leader <- getLeaderUI+getItem leader psuit prompt promptGeneric cCur cRest askWhenLone+        permitMulitple = do   accessCBag <- getsState $ accessModeBag leader   let storeAssocs = EM.assocs . accessCBag       allAssocs = concatMap storeAssocs (cCur : cRest)   case (allAssocs, cCur) of     ([(iid, _)], MStore rstore) | null cRest && not askWhenLone ->       return $ Right $ RStore rstore [iid]-    _ -> transition psuit prompt promptGeneric permitMulitple+    _ -> transition leader psuit prompt promptGeneric permitMulitple                     0 cCur cRest ISuitable  data DefItemKey m = DefItemKey   { defLabel  :: Either Text K.KM   , defCond   :: Bool-  , defAction :: Either K.KM SlotChar -> m (Either Text ResultItemDialogMode)+  , defAction :: KeyOrSlot -> m (Either Text ResultItemDialogMode)   }  data Suitability =     SuitsEverything   | SuitsSomething (Maybe CStore -> ItemFull -> ItemQuant -> Bool) -transition :: forall m. (MonadClient m, MonadClientUI m)-           => m Suitability+transition :: forall m. MonadClientUI m+           => ActorId+           -> m Suitability            -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State                -> Text)            -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State@@ -343,11 +350,17 @@            -> [ItemDialogMode]            -> ItemDialogState            -> m (Either Text ResultItemDialogMode)-transition psuit prompt promptGeneric permitMulitple+transition leader psuit prompt promptGeneric permitMulitple            numPrefix cCur cRest itemDialogState = do-  let recCall = transition psuit prompt promptGeneric permitMulitple-  leader <- getLeaderUI-  actorCurAndMaxSk <- leaderSkillsClientUI+  let recCall numPrefix2 cCur2 cRest2 itemDialogState2 = do+        -- Pointman could have been changed by keypresses near the end of+        -- the current recursive call, so refresh it for the next call.+        mleader <- getsClient sleader+        let leader2 = fromMaybe (error "UI manipulation killed the pointman")+                                mleader+        transition leader2 psuit prompt promptGeneric permitMulitple+                   numPrefix2 cCur2 cRest2 itemDialogState2+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader   body <- getsState $ getActorBody leader   bodyUI <- getsSession $ getActorUI leader   fact <- getsState $ (EM.! bfid body) . sfactionD@@ -418,23 +431,23 @@       maySwitchLeader MModes = False       maySwitchLeader _ = True       cycleKeyDef direction =-        let km = revCmd $ MemberCycle direction+        let km = revCmd $ PointmanCycle direction         in (km, DefItemKey                { defLabel = if direction == Forward then Right km else Left ""                , defCond = maySwitchLeader cCur && not (autoDun || null hs)                , defAction = \_ -> do-                   err <- memberCycle False direction+                   err <- pointmanCycle leader False direction                    let !_A = assert (isNothing err `blame` err) ()                    recCall numPrefix cCur cRest itemDialogState                })       cycleLevelKeyDef direction =-        let km = revCmd $ MemberCycleLevel direction+        let km = revCmd $ PointmanCycleLevel direction         in (km, DefItemKey                 { defLabel = Left ""                 , defCond = maySwitchLeader cCur                             && any (\(_, b, _) -> blid b == blid body) hs                 , defAction = \_ -> do-                    err <- memberCycleLevel False direction+                    err <- pointmanCycleLevel leader False direction                     let !_A = assert (isNothing err `blame` err) ()                     recCall numPrefix cCur cRest itemDialogState                 })@@ -465,7 +478,7 @@            { defLabel = Left ""            , defCond = maySwitchLeader cCur && not (null hs)            , defAction = \_ -> do-               merror <- pickLeaderWithPointer+               merror <- pickLeaderWithPointer leader                case merror of                  Nothing -> recCall numPrefix cCur cRest itemDialogState                  Just{} -> return $ Left "not a menu item nor teammate position"@@ -536,7 +549,8 @@                                   $ elemIndex slot allSlots                   in return (Right (resultConstructor slotIndex))               }-        runDefItemKey keyDefs skillsDef io slotKeys promptChosen cCur+        runDefItemKey leader lSlots bagFiltered keyDefs skillsDef io slotKeys+                      promptChosen cCur   case cCur of     MSkills -> do       io <- skillsOverlay leader@@ -553,9 +567,10 @@       io <- itemOverlay lSlots (blid body) bagFiltered displayRanged       let slotKeys = mapMaybe (keyOfEKM numPrefix . Right)                      $ EM.keys bagItemSlots-      runDefItemKey keyDefs lettersDef io slotKeys promptChosen cCur+      runDefItemKey leader lSlots bagFiltered keyDefs lettersDef io slotKeys+                    promptChosen cCur -keyOfEKM :: Int -> Either [K.KM] SlotChar -> Maybe K.KM+keyOfEKM :: Int -> KeyOrSlot -> Maybe K.KM keyOfEKM _ (Left kms) = error $ "" `showFailure` kms keyOfEKM numPrefix (Right SlotChar{..}) | slotPrefix == numPrefix =   Just $ K.mkChar slotChar@@ -563,15 +578,18 @@  -- We don't create keys from slots in @okx@, so they have to be -- exolicitly given in @slotKeys@.-runDefItemKey :: (MonadClient m, MonadClientUI m)-              => [(K.KM, DefItemKey m)]+runDefItemKey :: MonadClientUI m+              => ActorId+              -> SingleItemSlots+              -> ItemBag+              -> [(K.KM, DefItemKey m)]               -> DefItemKey m               -> OKX               -> [K.KM]               -> Text               -> ItemDialogMode               -> m (Either Text ResultItemDialogMode)-runDefItemKey keyDefs lettersDef okx slotKeys prompt cCur = do+runDefItemKey leader lSlots bag keyDefs lettersDef okx slotKeys prompt cCur = do   let itemKeys = slotKeys ++ map fst keyDefs       wrapB s = "[" <> s <> "]"       (keyLabelsRaw, keys) = partitionEithers $ map (defLabel . snd) keyDefs@@ -581,10 +599,120 @@   msgAdd MsgPromptGeneric $ prompt <+> choice   CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui   ekm <- do-    okxs <- overlayToSlideshow (rheight - 2) keys okx-    displayChoiceScreen (show cCur) ColorFull False okxs itemKeys+    sli <- overlayToSlideshow (rheight - 2) keys okx+    displayChoiceScreenWithRightPane+      (inventoryInRightPane leader lSlots bag cCur)+      (show cCur) ColorFull False sli itemKeys   case ekm of     Left km -> case km `lookup` keyDefs of       Just keyDef -> defAction keyDef ekm       Nothing -> defAction lettersDef ekm  -- pressed; with current prefix     Right _slot -> defAction lettersDef ekm  -- selected; with the given prefix++inventoryInRightPane :: MonadClientUI m+                     => ActorId -> SingleItemSlots -> ItemBag -> ItemDialogMode+                     -> KeyOrSlot+                     -> m OKX+inventoryInRightPane leader lSlots bag c ekm = case ekm of+  Left{} -> return emptyOKX+  Right slot -> do+    CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui+    FontSetup{..} <- getFontSetup+    let -- Lower width, to permit extra vertical space at the start,+        -- because gameover menu prompts are sometimes wide and/or long.+       width = rwidth - 2+       slotIndex = fromMaybe (error "illegal slot") $ elemIndex slot allSlots+    case c of+      _ | isSquareFont propFont -> return emptyOKX+      MSkills -> do+        (prompt, attrString) <- skillCloseUp leader slotIndex+        let promptAS | T.null prompt = []+                     | otherwise = textFgToAS Color.Brown $ prompt <> "\n\n"+            ov = EM.singleton propFont $ offsetOverlay+                                       $ splitAttrString width width+                                       $ promptAS ++ attrString+        return (ov, [])+      MPlaces -> do+        COps{coplace} <- getsState scops+        soptions <- getsClient soptions+        -- This is very slow when many places are exposed,+        -- because this is computed once per place menu keypress.+        -- Fortunately, the mode after entering a place and with pressing+        -- up and down arrow keys is not quadratic, so should be used instead,+        -- particularly with @sexposePlaces@.+        places <- getsState $ EM.assocs+                              . placesFromState coplace (sexposePlaces soptions)+        (prompt, blurbs) <-+          placeCloseUp places (sexposePlaces soptions) slotIndex+        let promptAS | T.null prompt = []+                     | otherwise = textFgToAS Color.Brown $ prompt <> "\n\n"+            ov = attrLinesToFontMap+                 $ map (second $ concatMap (splitAttrString width width))+                 $ (propFont, [promptAS]) : map (second $ map textToAS) blurbs+        return (ov, [])+      MModes -> return emptyOKX+        -- modes cover the right part of screen, so let's keep it empty+      _ -> do+        let ix0 = fromMaybe (error $ show slot)+                            (elemIndex slot $ EM.keys lSlots)+            promptFun _iid _itemFull _k = ""+              -- TODO, e.g., if the party still owns any copies, if the actor+              -- was ever killed by us or killed ours, etc.+              -- This can be the same prompt or longer than what entering+              -- the item screen shows.+        -- Mono font used, because lots of numbers in these blurbs+        -- and because some prop fonts wider than mono (e.g., in the+        -- dejavuBold font set).+        -- A side effect is a larger space between the symbol and description,+        -- so this is not a bug, not a double space, not worth focusing on.+        okxItemLorePointedAt+          monoFont (rwidth - 2) True bag 0 promptFun ix0 lSlots++skillCloseUp :: MonadClientUI m => ActorId -> Int -> m (Text, AttrString)+skillCloseUp leader slotIndex = do+  b <- getsState $ getActorBody leader+  bUI <- getsSession $ getActorUI leader+  actorCurAndMaxSk <- getsState $ getActorMaxSkills leader+  let skill = skillSlots !! slotIndex+      valueText = skillToDecorator skill b+                  $ Ability.getSk skill actorCurAndMaxSk+      prompt = makeSentence+        [ MU.WownW (partActor bUI) (MU.Text $ skillName skill)+        , "is", MU.Text valueText ]+      attrString = textToAS $ skillDesc skill+  return (prompt, attrString)++placeCloseUp :: MonadClientUI m+             => [(ContentId PK.PlaceKind, (ES.EnumSet LevelId, Int, Int, Int))]+             -> Bool+             -> Int+             -> m (Text, [(DisplayFont, [Text])])+placeCloseUp places sexposePlaces slotIndex = do+  COps{coplace} <- getsState scops+  FontSetup{..} <- getFontSetup+  let (pk, (es, ne, na, _)) = places !! slotIndex+      pkind = okind coplace pk+      prompt = makeSentence ["you remember", MU.Text $ PK.pname pkind]+      freqsText = "Frequencies:" <+> T.intercalate " "+        (map (\(grp, n) -> "(" <> displayGroupName grp+                           <> ", " <> tshow n <> ")")+         $ PK.pfreq pkind)+      onLevels | ES.null es = []+               | otherwise = [makeSentence+                               [ "Appears on"+                               , MU.CarWs (ES.size es) "level" <> ":"+                               , MU.WWandW $ map MU.Car $ sort+                                 $ map (abs . fromEnum) $ ES.elems es ]]+      placeParts = ["it has" | ne > 0 || na > 0]+                   ++ [MU.CarWs ne "entrance" | ne > 0]+                   ++ ["and" | ne > 0 && na > 0]+                   ++ [MU.CarWs na "surrounding" | na > 0]+      partsSentence | null placeParts = []+                    | otherwise = [makeSentence placeParts, "\n"]+      -- Ideally, place layout would be in SquareFont and the rest+      -- in PropFont, but this is mostly a debug screen, so KISS.+      blurbs = [(propFont, partsSentence)]+               ++ [(monoFont, [freqsText, "\n"]) | sexposePlaces]+               ++ [(squareFont, PK.ptopLeft pkind ++ ["\n"]) | sexposePlaces]+               ++ [(propFont, onLevels)]+  return (prompt, blurbs)
engine-src/Game/LambdaHack/Client/UI/ItemDescription.hs view
@@ -4,7 +4,7 @@   , partItemWsDetail, partItemWs, partItemWsShortest, partItemWsShort   , partItemWsLong, partItemWsRanged   , partItemShortAW, partItemMediumAW, partItemShortWownW-  , viewItem, itemDesc+  , viewItem, viewItemBenefitColored, itemDesc #ifdef EXPOSE_INTERNAL     -- * Internal operations   , partItemN, textAllPowers@@ -17,6 +17,7 @@  import           Data.Char (isAlpha, isAlphaNum) import qualified Data.EnumMap.Strict as EM+import           Data.Int (Int64) import qualified Data.Text as T import qualified NLP.Miniutter.English as MU @@ -58,22 +59,22 @@       timeout = IA.aTimeout arItem       temporary = IA.checkFlag Ability.Fragile arItem                   && IA.checkFlag Ability.Periodic arItem-      lenCh = itemK - ncharges localTime (itemK, itemTimers)+      ncha = ncharges localTime (itemK, itemTimers)       charges | temporary = case itemTimers of-                  [] -> if lenCh == 0+                  [] -> if itemK == ncha                         then ""                         else error $ "partItemN3: charges with null timer"                                      `showFailure`                                      (side, itemFull, itemK, itemTimers)-                  t : _ -> if lenCh == 0+                  t : _ -> if itemK == ncha                            then "(ready to expire)"                            else let total = deltaOfItemTimer localTime t                                 in "for" <+> timeDeltaInSecondsText total-              | lenCh == 0 = ""-              | itemK == 1 && lenCh == 1 = "(charging)"-              | itemK == lenCh = "(all charging)"-              | otherwise = "(" <> tshow lenCh <+> "charging)"-      skipRecharging = detailLevel <= DetailLow && lenCh >= itemK+              | itemK == ncha = ""+              | itemK == 1 && ncha == 0 = "(charging)"+              | ncha == 0 = "(all charging)"+              | otherwise = "(" <> tshow (itemK - ncha) <+> "charging)"+      skipRecharging = detailLevel <= DetailLow && ncha == 0       (orTs, powerTs, rangedDamage) =         textAllPowers width detailLevel skipRecharging itemFull       lsource = case jfid itemBase of@@ -376,8 +377,23 @@ {-# INLINE viewItem #-} viewItem itemFull =   Color.attrChar2ToW32 (flavourToColor $ jflavour $ itemBase itemFull)-                       (IK.isymbol $ itemKind itemFull)+                       (displayContentSymbol $ IK.isymbol $ itemKind itemFull) +viewItemBenefitColored :: DiscoveryBenefit -> ItemId -> ItemFull+                       -> Color.AttrCharW32+viewItemBenefitColored discoBenefit iid itemFull =+  -- The map @discoBenefit@ is normally used by AI to tell it in what role+  -- an item can be employed. In particular, ` benInEqp` says if an item+  -- buffs stats enough (and nerfs not too much) to be worth equipping.+  -- Here it's (ab)used to tell if an item (only a status effect item+  -- in this case, marked with `Ability.Condition`) is beneficial or not+  -- and to signal that in the organs UI menu.+  let color = if benInEqp (discoBenefit EM.! iid)+                  then Color.BrGreen+                  else Color.BrRed+  in Color.attrChar2ToW32+       color (displayContentSymbol $ IK.isymbol $ itemKind itemFull)+ itemDesc :: Int -> Bool -> FactionId -> FactionDict -> Int -> CStore -> Time          -> LevelId -> ItemFull -> ItemQuant          -> AttrString@@ -420,9 +436,9 @@                               else aHurtMeleeOfItem                   mult = 100 + min 100 (max (-95) multRaw)                   percentDeltaHP = xM meanDmg `divUp` 100-                  rawDeltaHP = intCast mult * percentDeltaHP+                  rawDeltaHP = into @Int64 mult * percentDeltaHP                   pmult = 100 + min 100 (max (-95) aHurtMeleeOfItem)-                  prawDeltaHP = intCast pmult * percentDeltaHP+                  prawDeltaHP = into @Int64 pmult * percentDeltaHP                   pdeltaHP = modifyDamageBySpeed prawDeltaHP speed                   minDeltaHP = 5 * percentDeltaHP                   mDeltaHP = modifyDamageBySpeed minDeltaHP speed@@ -475,7 +491,7 @@       -- two hands and two legs is not that enlightening and the number       -- is not shown in organ lore, so this should wait until we add       -- proper hyperlinks both ways instead of relying of names.-      ikitToPart = MU.Text . T.intercalate ", " . map (fromGroupName . fst)+      ikitToPart = MU.Text . T.intercalate ", " . map (displayGroupName . fst)       (ikitOrganNames, ikitOtherNames) =         partition ((== COrgan) . snd) $ IK.ikit itemKind       ikitDesc | null ikitOrganNames = ""
engine-src/Game/LambdaHack/Client/UI/Key.hs view
@@ -131,7 +131,7 @@ showKey RightButtonRelease = "RMB" showKey WheelNorth = "WHEEL-UP" showKey WheelSouth = "WHEEL-DN"-showKey (Unknown s) = "'" ++ s ++ "'"+showKey (Unknown s) = s showKey DeadKey      = "DEADKEY"  -- | Show a key with a modifier, if any.
engine-src/Game/LambdaHack/Client/UI/KeyBindings.hs view
@@ -16,7 +16,6 @@ import           Game.LambdaHack.Client.UI.Content.Screen import           Game.LambdaHack.Client.UI.ContentClientUI import           Game.LambdaHack.Client.UI.HumanCmd-import           Game.LambdaHack.Client.UI.ItemSlot import qualified Game.LambdaHack.Client.UI.Key as K import           Game.LambdaHack.Client.UI.Overlay import           Game.LambdaHack.Client.UI.PointUI@@ -36,6 +35,7 @@       [ "Walk throughout a level with mouse or numeric keypad (right diagram below)"       , "or the Vi editor keys (middle) or the left-hand movement keys (left). Run until"       , "disturbed with Shift or Control. Go-to a position with LMB (left mouse button)."+      , "In aiming mode, the same keys (and mouse) move the aiming crosshair."       ]     movSchema =       [ "     q w e     y k u     7 8 9"@@ -45,15 +45,14 @@       , "     z x c     b j n     1 2 3"       ]     movBlurb2 =-      [ "In aiming mode, the same keys (and mouse) move the aiming crosshair."-      , "Press `KP_5` (`5` on keypad) to wait, bracing for impact, which reduces any"+      [ "Press `KP_5` (`5` on keypad) to wait, bracing for impact, which reduces any"       , "damage taken and prevents displacement by foes. Press `S-KP_5` or `C-KP_5`"       , "(the same key with Shift or Control) to lurk 0.1 of a turn, without bracing."       , ""       , "Displace enemies by running into them with Shift/Control or S-LMB. Search,"-      , "open, descend and attack by bumping into walls, doors, stairs and enemies."-      , "The best, not on cooldown, melee weapon is automatically chosen from your"-      , "equipment and from among your body parts."+      , "open, descend and melee by bumping into walls, doors, stairs and enemies."+      , "The best, and not on cooldown, melee weapon is automatically chosen"+      , "for attack from your equipment and from among your body parts."       ]     minimalBlurb =       [ "The following few commands, joined with the movement and running keys,"@@ -76,20 +75,20 @@       , "effects. Here we give an overview of effects of each button over"       , "the game map area. The list includes not only left and right buttons,"       , "but also the optional middle mouse button (MMB) and the mouse wheel,"-      , "which is also used over menus, to page-scroll them. For mice without RMB,"+      , "which is also used over menus to move selection. For mice without RMB,"       , "one can use Control key with LMB and for mice without MMB, one can use"       , "C-RMB or C-S-LMB."       ]     mouseAreasBlurb =-      [ "Next we show mouse button effects per screen area, in exploration mode"-      , "and (if different) in aiming mode. Note that this is all optional. Keyboard"-      , "suffices, at worst requiring the more obscure commands listed later on."+      [ "Next we show mouse button effects per screen area, in exploration and"+      , "(if different) aiming mode. Note that mouse is optional. Keyboard suffices,"+      , "occasionally requiring a lookup for an obscure command key in help screens."       ]     mouseAreasMini =       [ "Mouse button effects per screen area, in exploration and in aiming modes"       ]     movTextEnd = "Press SPACE or PGDN to advance or ESC to see the map again."-    lastHelpEnd = "Use mouse wheel or PGUP to go back and ESC to see the map again."+    lastHelpEnd = "Use PGUP to go back and ESC to see the map again."     seeAlso = "For more playing instructions see file PLAYING.md."     offsetCol2 = 12     pickLeaderDescription =@@ -102,20 +101,13 @@     keyCaption = fmt offsetCol2 "keys" "command"     mouseOverviewCaption = fmt offsetCol2 "keys" "command (exploration/aiming)"     spLen = textSize monoFont " "-    pamoveRight :: Int -> (PointUI, a) -> (PointUI, a)-    pamoveRight xoff (PointUI x y, a) = (PointUI (x + xoff) y, a)-    okxs cat headers footers =-      let (ovs, kyx) = okxsN coinput monoFont propFont 0 offsetCol2-                             (const False) True cat headers footers-      in ( EM.map (map (pamoveRight spLen)) ovs-         , map (second $ pamoveRight spLen) kyx )-    renumber dy (km, (PointUI x y, len)) = (km, (PointUI x (y + dy), len))-    renumberOv dy = map (\(PointUI x y, al) -> (PointUI x (y + dy), al))+    okxs cat headers footers = xytranslateOKX spLen 0 $+      okxsN coinput monoFont propFont offsetCol2 (const False)+            True cat headers footers     mergeOKX :: OKX -> OKX -> OKX-    mergeOKX (ovs1, ks1) (ovs2, ks2) =-      let off = 1 + EM.foldr (\ov acc -> max acc (maxYofOverlay ov)) 0 ovs1-      in ( EM.unionWith (++) ovs1 $ EM.map (renumberOv off) ovs2-         , ks1 ++ map (renumber off) ks2 )+    mergeOKX okx1 okx2 =+      let off = 1 + maxYofFontOverlayMap (fst okx1)+      in sideBySideOKX 0 off okx1 okx2     catLength cat = length $ filter (\(_, (cats, desc, _)) ->       cat `elem` cats && (desc /= "" || CmdInternal `elem` cats)) bcmdList     keyM = 13@@ -126,7 +118,7 @@     fmm a b c = fmt (keyM + 1) a $ fmt0 keyB (truncatem b) (truncatem c)     areaCaption t = fmm t "LMB (left mouse button)" "RMB (right mouse button)"     keySel :: (forall a. (a, a) -> a) -> K.KM-           -> [(CmdArea, Either K.KM SlotChar, Text)]+           -> [(CmdArea, KeyOrSlot, Text)]     keySel sel key =       let cmd = case M.lookup key bcmdMap of             Just (_, _, cmd2) -> cmd2@@ -156,15 +148,15 @@           kst2 = keySel sel key2           f (ca1, Left km1, _) (ca2, Left km2, _) y =             assert (ca1 == ca2 `blame` (ca1, ca2, km1, km2, kst1, kst2))-              [ (Left [km1], ( PointUI (doubleIfSquare $ keyM + 4) y-                             , ButtonWidth monoFont keyB ))-              , (Left [km2], ( PointUI (doubleIfSquare $ keyB + keyM + 5) y-                             , ButtonWidth monoFont keyB )) ]+              [ (Left km1, ( PointUI (doubleIfSquare $ keyM + 4) y+                           , ButtonWidth monoFont keyB ))+              , (Left km2, ( PointUI (doubleIfSquare $ keyB + keyM + 5) y+                           , ButtonWidth monoFont keyB )) ]           f c d e = error $ "" `showFailure` (c, d, e)           kxs = concat $ zipWith3 f kst1 kst2 [1 + length header..]-          menuLeft = map (\(ca1, _, _) -> areaDescription ca1) kst1-          menuMiddle = map (\(_, _, desc) -> desc) kst1-          menuRight = map (\(_, _, desc) -> desc) kst2+          menuLeft = map (\(ca1, _, _) -> textToAL $ areaDescription ca1) kst1+          menuMiddle = map (\(_, _, desc) -> textToAL desc) kst1+          menuRight = map (\(_, _, desc) -> textToAL desc) kst2           y0 = 1 + length header       in ( EM.unionsWith (++)              [ typesetInMono $ "" : header@@ -176,24 +168,17 @@                $ typesetXY (doubleIfSquare $ keyB + keyM + 5, y0) menuRight ]          , kxs )     typesetInSquare :: [Text] -> FontOverlayMap-    typesetInSquare = EM.singleton squareFont . offsetOverlayX-                      . map (\t -> (spLen, textToAL t))+    typesetInSquare =+      EM.singleton squareFont . typesetXY (spLen, 0) . map textToAL     typesetInMono :: [Text] -> FontOverlayMap-    typesetInMono = EM.singleton monoFont . offsetOverlayX-                    . map (\t -> (spLen, textToAL t))+    typesetInMono =+      EM.singleton monoFont . typesetXY (spLen, 0) . map textToAL     typesetInProp :: [Text] -> FontOverlayMap-    typesetInProp = EM.singleton propFont . offsetOverlayX-                    . map (\t -> (spLen, textToAL t))-    typesetXY :: (Int, Int) -> [Text] -> Overlay-    typesetXY (xoffset, yoffset) =-      map (\(y, t) -> (PointUI xoffset (y + yoffset), textToAL t)) . zip [0..]+    typesetInProp =+      EM.singleton propFont . typesetXY (spLen, 0) . map textToAL     sideBySide :: [(Text, OKX)] -> [(Text, OKX)]-    sideBySide ((_t1, (ovs1, kyx1)) : (t2, (ovs2, kyx2)) : rest)-      | not $ isSquareFont propFont =-        (t2, ( EM.unionWith (++) ovs1 (EM.map (map (pamoveRight rwidth)) ovs2)-             , sortOn (\(_, (PointUI x y, _)) -> (y, x))-               $ kyx1 ++ map (second $ pamoveRight rwidth) kyx2 ))-        : sideBySide rest+    sideBySide ((_t1, okx1) : (t2, okx2) : rest) | not (isSquareFont propFont) =+      (t2, sideBySideOKX rwidth 0 okx1 okx2) : sideBySide rest     sideBySide l = l   in sideBySide $ concat     [ if catLength CmdMinimal@@ -305,12 +290,12 @@ -- -- The length of the button may be wrong if the two supplied fonts -- have very different widths.-okxsN :: InputContent -> DisplayFont -> DisplayFont -> Int -> Int-      -> (HumanCmd -> Bool) -> Bool -> CmdCategory-      -> ([Text], [Text], [Text]) -> ([Text], [Text]) -> OKX-okxsN InputContent{..} keyFont descFont offset offsetCol2 greyedOut+okxsN :: InputContent -> DisplayFont -> DisplayFont -> Int -> (HumanCmd -> Bool)+      -> Bool -> CmdCategory -> ([Text], [Text], [Text]) -> ([Text], [Text])+      -> OKX+okxsN InputContent{..} labFont descFont offsetCol2 greyedOut       showManyKeys cat (headerMono1, headerProp, headerMono2)-                       (footerMono, footerProp) =+      (footerMono, footerProp) =   let fmt k h = (T.singleton '\x00a0' <> k, h)       coImage :: HumanCmd -> [K.KM]       coImage cmd = M.findWithDefault (error $ "" `showFailure` cmd) cmd brevMap@@ -318,24 +303,25 @@       keyKnown km = case K.key km of         K.Unknown{} -> False         _ -> True-      keys :: [(Either [K.KM] SlotChar, (Bool, (Text, Text)))]-      keys = [ (Left kmsRes, (greyedOut cmd, fmt keyNames desc))+      keys :: [(KeyOrSlot, (Bool, (Text, Text)))]+      keys = [ (Left km, (greyedOut cmd, fmt keyNames desc))              | (_, (cats, desc, cmd)) <- bcmdList              , let kms = coImage cmd                    knownKeys = filter keyKnown kms                    keyNames =                      disp $ (if showManyKeys then id else take 1) knownKeys                    kmsRes = if desc == "" then knownKeys else kms+                   km = case kmsRes of+                     [] -> K.escKM+                     km1 : _ -> km1              , cat `elem` cats              , desc /= "" || CmdInternal `elem` cats]-      spLen = textSize keyFont " "+      spLen = textSize labFont " "       f (ks, (_, (_, t2))) y =         (ks, ( PointUI spLen y-             , ButtonWidth keyFont (offsetCol2 + 2 + T.length t2 - 1)))-      kxs = zipWith f keys [offset + length headerMono1-                                   + length headerProp-                                   + length headerMono2 ..]-      renumberOv = map (\(PointUI x y, al) -> (PointUI x (y + offset), al))+             , ButtonWidth labFont (offsetCol2 + 2 + T.length t2 - 1)))+      kxs = zipWith f keys+              [length headerMono1 + length headerProp + length headerMono2 ..]       ts = map (\t -> (False, (t, ""))) headerMono1            ++ map (\t -> (False, ("", t))) headerProp            ++ map (\t -> (False, (t, ""))) headerMono2@@ -351,6 +337,6 @@              in (al1, ( if T.null t1 then 0 else spLen * (offsetCol2 + 2)                       , textToAL t2 ))       (greyLab, greyDesc) = unzip $ map greyToAL ts-  in ( EM.insertWith (++) descFont (renumberOv (offsetOverlayX greyDesc))-       $ EM.singleton keyFont $ renumberOv $ offsetOverlay greyLab+  in ( EM.insertWith (++) descFont (offsetOverlayX greyDesc)+       $ EM.singleton labFont (offsetOverlay greyLab)      , kxs )
engine-src/Game/LambdaHack/Client/UI/MonadClientUI.hs view
@@ -11,17 +11,15 @@   , clientPrintUI, debugPossiblyPrintUI, getSession, putSession, displayFrames   , connFrontendFrontKey, setFrontAutoYes, frontendShutdown, printScreen   , chanFrontend, anyKeyPressed, discardPressedKey, resetPressedKeys-  , addPressedControlEsc, revCmdMap, getReportUI, computeChosenLore-  , miniHintAimingBare, miniHintAimingLore-  , getLeaderUI, getArenaUI, viewedLevelUI-  , xhairToPos, setXHairFromGUI, clearAimMode-  , getFontSetup, scoreToSlideshow, defaultHistory+  , revCmdMap, getReportUI, getMiniHintAiming, computeChosenLore+  , getArenaUI, viewedLevelUI, mxhairToPos, xhairToPos, setXHairFromGUI+  , clearAimMode, getFontSetup, scoreToSlideshow, defaultHistory   , tellAllClipPS, tellGameClipPS, elapsedSessionTimeGT   , resetSessionStart, resetGameStart, partActorLeader, partPronounLeader-  , tryRestore, leaderSkillsClientUI, rndToActionUI, tryOpenBrowser+  , tryRestore, rndToActionUI, tryOpenBrowser #ifdef EXPOSE_INTERNAL     -- * Internal operations-  , connFrontend, displayFrame, addPressedKey+  , connFrontend, displayFrame #endif   ) where @@ -58,7 +56,7 @@ import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd import qualified Game.LambdaHack.Client.UI.Key as K import           Game.LambdaHack.Client.UI.Msg-import           Game.LambdaHack.Client.UI.PointUI+import           Game.LambdaHack.Client.UI.Overlay import           Game.LambdaHack.Client.UI.SessionUI import           Game.LambdaHack.Client.UI.Slideshow import           Game.LambdaHack.Client.UI.UIOptions@@ -81,7 +79,6 @@ import           Game.LambdaHack.Content.ModeKind import           Game.LambdaHack.Content.RuleKind import           Game.LambdaHack.Core.Random-import qualified Game.LambdaHack.Definition.Ability as Ability  -- Assumes no interleaving with other clients, because each UI client -- in a different terminal/window/machine.@@ -98,7 +95,10 @@     hFlush stdout  -- | The monad that gives the client access to UI operations,--- but not to modifying client state.+-- but not to modifying client state, except for the client-side pointman+-- (as opposed to pointman stores in faction data in main game state),+-- which is more of a UI concept, but is shared with AI to be able+-- to keep it when switching AI on/off and to save on typing. class MonadClientRead m => MonadClientUI m where   getsSession :: (SessionUI -> a) -> m a   modifySession :: (SessionUI -> SessionUI) -> m ()@@ -130,16 +130,18 @@ -- | Push frames or delays to the frame queue. The frames depict -- the @lid@ level. displayFrames :: MonadClientUI m => LevelId -> PreFrames3 -> m ()+displayFrames _ [] = return () displayFrames lid frs = do   let framesRaw = case frs of         [] -> []-        [Just ((bfr, ffr), (ovProp, ovMono))] ->-          [Just ((FrameBase $ U.unsafeThaw bfr, ffr), (ovProp, ovMono))]+        [Just ((bfr, ffr), (ovProp, ovSquare, ovMono))] ->+          [Just ( (FrameBase $ U.unsafeThaw bfr, ffr)+                , (ovProp, ovSquare, ovMono) )]         _ ->           -- Due to the frames coming from the same base frame,           -- we have to copy it to avoid picture corruption.-          map (fmap $ \((bfr, ffr), (ovProp, ovMono)) ->-                ((FrameBase $ U.thaw bfr, ffr), (ovProp, ovMono))) frs+          map (fmap $ \((bfr, ffr), (ovProp, ovSquare, ovMono)) ->+                ((FrameBase $ U.thaw bfr, ffr), (ovProp, ovSquare, ovMono))) frs   -- If display level different than the man viewed level,   -- e.g., when our actor is attacked on a remote level,   -- then pad with tripple delay to give more time to see the remote frames(s).@@ -155,14 +157,20 @@ -- | Write 'Frontend.FrontKey' UI request to the frontend, read the reply, -- set pointer, return key. connFrontendFrontKey :: MonadClientUI m => [K.KM] -> PreFrame3 -> m K.KM-connFrontendFrontKey frontKeyKeys ((bfr, ffr), (ovProp, ovMono)) = do-  let frontKeyFrame = ((FrameBase $ U.unsafeThaw bfr, ffr), (ovProp, ovMono))-  kmp <- connFrontend $ Frontend.FrontKey frontKeyKeys frontKeyFrame-  modifySession $ \sess -> sess {spointer = K.kmpPointer kmp}-  return $! K.kmpKeyMod kmp+connFrontendFrontKey frontKeyKeys ((bfr, ffr), (ovProp, ovSquare, ovMono)) = do+  let frontKeyFrame =+        ((FrameBase $ U.unsafeThaw bfr, ffr), (ovProp, ovSquare, ovMono))+  sautoYes <- getsSession sautoYes+  if sautoYes && (null frontKeyKeys || K.spaceKM `elem` frontKeyKeys) then do+    connFrontend $ Frontend.FrontFrame frontKeyFrame+    return K.spaceKM+  else do+    kmp <- connFrontend $ Frontend.FrontKey frontKeyKeys frontKeyFrame+    modifySession $ \sess -> sess {spointer = K.kmpPointer kmp}+    return $! K.kmpKeyMod kmp  setFrontAutoYes :: MonadClientUI m => Bool -> m ()-setFrontAutoYes b = connFrontend $ Frontend.FrontAutoYes b+setFrontAutoYes b = modifySession $ \sess -> sess {sautoYes = b}  frontendShutdown :: MonadClientUI m => m () frontendShutdown = connFrontend Frontend.FrontShutdown@@ -185,13 +193,6 @@ resetPressedKeys :: MonadClientUI m => m () resetPressedKeys = connFrontend Frontend.FrontResetKeys -addPressedKey :: MonadClientUI m => K.KMP -> m ()-addPressedKey = connFrontend . Frontend.FrontAdd--addPressedControlEsc :: MonadClientUI m => m ()-addPressedControlEsc = addPressedKey K.KMP { K.kmpKeyMod = K.controlEscKM-                                           , K.kmpPointer = PointUI 0 0 }- revCmdMap :: MonadClientUI m => m (HumanCmd.HumanCmd -> K.KM) revCmdMap = do   CCUI{coinput=InputContent{brevMap}} <- getsSession sccui@@ -203,60 +204,56 @@  getReportUI :: MonadClientUI m => Bool -> m Report getReportUI insideMenu = do-  side <- getsClient sside   saimMode <- getsSession saimMode-  (inhabitants, embeds) <--    if isJust saimMode then computeChosenLore else return ([], [])   sUIOptions <- getsSession sUIOptions   report <- getsSession $ newReport . shistory-  fact <- getsState $ (EM.! side) . sfactionD-  let newcomerHelp = True  -- TODO-      detailAtDefault = (detailLevel <$> saimMode) == Just defaultDetailLevel+  sreqDelay <- getsSession sreqDelay+  miniHintAiming <- getMiniHintAiming+  -- Different from ordinary tutorial hints in that shown more than once.+  let detailAtDefault = (detailLevel <$> saimMode) == Just defaultDetailLevel       detailMinimal = (detailLevel <$> saimMode) == Just minBound-      underAI = isAIFact fact       prefixColors = uMessageColors sUIOptions-      -- Here we assume newbies don't override default keys.-      miniHintAiming = if null inhabitants && null embeds-                       then miniHintAimingBare-                       else miniHintAimingLore       promptAim = toMsgShared prefixColors MsgPromptGeneric-                  $ miniHintAiming <> "\n"-      promptAI = toMsgShared prefixColors MsgPromptAction-                             "<press any key for main menu>"-  return $! if | newcomerHelp && not insideMenu-                 && detailAtDefault && not detailMinimal ->+                              (miniHintAiming <> "\n")+      promptDelay = toMsgShared prefixColors MsgPromptAction+                                "<press any key to regain control>"+  return $! if | not insideMenu && detailAtDefault && not detailMinimal ->                    consReport promptAim report-               | underAI -> consReport promptAI report+               | sreqDelay == ReqDelayAlarm && not insideMenu ->+                   consReport promptDelay report                | otherwise -> report +getMiniHintAiming :: MonadClientUI m => m Text+getMiniHintAiming = do+  saimMode <- getsSession saimMode+  (inhabitants, embeds) <-+    if isJust saimMode then computeChosenLore else return ([], [])+  sreqDelay <- getsSession sreqDelay+  mleader <- getsClient sleader+  let loreCommandAvailable = not (null inhabitants && null embeds)+                             && isJust mleader+  -- Here we assume newbies don't override default keys.+  return $! T.unwords $ concat+    [ ["Aiming mode:"]+    , ["'~' for lore," | loreCommandAvailable ]+    , ["'f' to fling," | sreqDelay /= ReqDelayHandled]+    , [if loreCommandAvailable && sreqDelay /= ReqDelayHandled+       then "SPACE or RMB to hush,"  -- shorter, because less space left+       else "SPACE or RMB to cycle detail,"]+    , ["ESC to cancel."] ]+ computeChosenLore :: MonadClientUI m                   => m ([(ActorId, Actor)], [(ItemId, ItemQuant)]) computeChosenLore = do   side <- getsClient sside-  mxhairPos <- xhairToPos-  leader0 <- getLeaderUI-  b0 <- getsState $ getActorBody leader0-  let xhairPos = fromMaybe (bpos b0) mxhairPos+  xhairPos <- xhairToPos   lidV <- viewedLevelUI   let isOurs (_, b) = bfid b == side   inhabitants0 <- getsState $ filter (not . isOurs)-                  . posToAidAssocs xhairPos lidV+                              . posToAidAssocs xhairPos lidV   embeds0 <- getsState $ EM.assocs . getEmbedBag lidV xhairPos   return (inhabitants0, embeds0) -miniHintAimingBare :: Text-miniHintAimingBare = "Aiming mode: press 'f' to fling, SPACE or RMB to cycle detail, ESC to cancel."--miniHintAimingLore :: Text-miniHintAimingLore = "Aiming mode: '~' for lore, 'f' to fling, SPACE or RMB to hush, ESC to cancel."--getLeaderUI :: MonadClientUI m => m ActorId-getLeaderUI = do-  cli <- getClient-  case sleader cli of-    Nothing -> error $ "leader expected but not found" `showFailure` cli-    Just leader -> return leader- getArenaUI :: MonadClientUI m => m LevelId getArenaUI = do   let fallback = do@@ -282,16 +279,22 @@   saimMode <- getsSession saimMode   return $! maybe arena aimLevelId saimMode -xhairToPos :: MonadClientUI m => m (Maybe Point)-xhairToPos = do+mxhairToPos :: MonadClientUI m => m (Maybe Point)+mxhairToPos = do   lidV <- viewedLevelUI   mleader <- getsClient sleader   sxhair <- getsSession sxhair-  case mleader of-    Nothing -> return Nothing  -- e.g., when game start and no leader yet-    Just aid -> getsState $ aidTgtToPos aid lidV sxhair-                  -- e.g., xhair on another level+  getsState $ aidTgtToPos mleader lidV sxhair +xhairToPos :: MonadClientUI m => m Point+xhairToPos = do+  mxhairPos <- mxhairToPos+  mleader <- getsClient sleader+  fallback <- case mleader of+    Nothing -> return originPoint+    Just leader -> getsState $ bpos . getActorBody leader+  return $! fromMaybe fallback mxhairPos+ setXHairFromGUI :: MonadClientUI m => Maybe Target -> m () setXHairFromGUI xhair2 = do   xhair0 <- getsSession sxhair@@ -304,20 +307,20 @@ clearAimMode :: MonadClientUI m => m () clearAimMode = do   lidVOld <- viewedLevelUI  -- not in aiming mode at this point-  mxhairPos <- xhairToPos  -- computed while still in aiming mode+  xhairPos <- xhairToPos  -- computed while still in aiming mode   modifySession $ \sess -> sess {saimMode = Nothing}   lidV <- viewedLevelUI  -- not in aiming mode at this point   when (lidVOld /= lidV) $ do-    leader <- getLeaderUI-    lpos <- getsState $ bpos . getActorBody leader     sxhairOld <- getsSession sxhair-    let xhairPos = fromMaybe lpos mxhairPos-        sxhair = case sxhairOld of+    let sxhair = case sxhairOld of           Just TPoint{} -> Just $ TPoint TUnknown lidV xhairPos             -- the point is possibly unknown on this level; unimportant anyway           _ -> sxhairOld     setXHairFromGUI sxhair +-- We can't support setup @FontSetup SquareFont MonoFont MonoFont@+-- at this time, because the mono layer needs to overwrite the prop layer+-- and so has to be distinct even if the underlying font is mono for both. getFontSetup :: MonadClientUI m => m FontSetup getFontSetup = do   soptions@ClientOptions{schosenFontset, sfontsets} <- getsClient soptions@@ -328,11 +331,7 @@         Just fs -> fs       multiFont = Frontend.frontendName soptions == "sdl"                   && not (T.null (fontPropRegular chosenFontset))-  return $! if | not multiFont -> singleFontSetup-               | fontPropRegular chosenFontset == fontMono chosenFontset-                 && fontPropBold chosenFontset == fontMono chosenFontset ->-                 monoFontSetup-               | otherwise -> multiFontSetup+  return $! if multiFont then multiFontSetup else singleFontSetup  scoreToSlideshow :: MonadClientUI m => Int -> Status -> m Slideshow scoreToSlideshow total status = do@@ -372,7 +371,7 @@             then sli             else emptySlideshow -defaultHistory :: (MonadClient m, MonadClientUI m) => m History+defaultHistory :: MonadClientUI m => m History defaultHistory = do   sUIOptions <- getsSession sUIOptions   curTutorial <- getsSession scurTutorial@@ -429,10 +428,13 @@         <+> "Average clips per second:" <+> tshow cps <> "."         <+> "Average FPS:" <+> tshow fps <> "." -elapsedSessionTimeGT :: MonadClientUI m => Int -> m Bool-elapsedSessionTimeGT stopAfter = do+-- TODO: for speed and resolutiion use+-- https://hackage.haskell.org/package/chronos+-- or the number_of_nanonseconds functionality+-- in Data.Time.Clock.System, once it arrives there+elapsedSessionTimeGT :: MonadClientRead m => POSIXTime -> Int -> m Bool+elapsedSessionTimeGT sstartPOSIX stopAfter = do   current <- liftIO getPOSIXTime-  sstartPOSIX <- getsSession sstart   return $! (fromIntegralWrap :: Int -> NominalDiffTime) stopAfter             + sstartPOSIX             <= current@@ -494,13 +496,6 @@     dataDir <- liftIO appDataDir     liftIO $ tryWriteFile (dataDir </> cfgUIName) configString     return res---- For a leader, the skills are both current and max skills.-leaderSkillsClientUI :: MonadClientUI m => m Ability.Skills-{-# INLINE leaderSkillsClientUI #-}-leaderSkillsClientUI = do-  leader <- getLeaderUI-  getsState $ getActorMaxSkills leader  -- | Invoke pseudo-random computation with the generator kept in the session. rndToActionUI :: MonadClientUI m => Rnd a -> m a
engine-src/Game/LambdaHack/Client/UI/Msg.hs view
@@ -16,9 +16,9 @@     -- * Internal operations   , UAttrString, uToAttrString, attrStringToU   , toMsg, MsgPrototype, tripleFromProto-  , scrapsRepeats, tutorialHint, msgColor+  , scrapsRepeats, isTutorialHint, msgColor   , RepMsgNK, nullRepMsgNK-  , emptyReport, renderWholeReport, renderRepetition+  , emptyReport, renderRepetition   , scrapRepetitionSingle, scrapRepetition, renderTimeReport #endif   ) where@@ -300,11 +300,10 @@     MsgInnerWorkSpam -> True     MsgNumericReport -> True   MsgClassIgnore _ -> False  -- ignored, so no need to scrap-  MsgClassDistinct x -> case x of-    _ -> True+  MsgClassDistinct _x -> True -tutorialHint :: MsgClass -> Bool-tutorialHint = \case+isTutorialHint :: MsgClass -> Bool+isTutorialHint = \case   MsgClassShowAndSave x -> case x of  -- show and save: least surprise     MsgTutorialHint -> True     _ -> False@@ -434,19 +433,13 @@ consReport :: Msg -> Report -> Report consReport msg (Report r) = Report $ r ++ [RepMsgNK msg 1 1] --- | Render a report as a (possibly very long) 'AttrString'. Filter out--- messages not meant for display, unless not showing, but saving to history.+-- | Render a report as a (possibly very long) list of 'AttrString'. renderReport :: Bool -> Report -> [AttrString] renderReport displaying (Report r) =   let rep = map (\(RepMsgNK msg n k) -> if displaying                                         then (msgShow msg, n)                                         else (msgSave msg, k)) r-  in renderWholeReport rep []---- | Render a report as a (possibly very long) 'AttrString'.-renderWholeReport :: [(AttrString, Int)] -> [AttrString] -> [AttrString]-renderWholeReport [] acc = acc-renderWholeReport (x : xs) acc = renderWholeReport xs (renderRepetition x : acc)+  in reverse $ map renderRepetition rep  renderRepetition :: (AttrString, Int) -> AttrString renderRepetition (asRaw, n) =@@ -558,13 +551,13 @@       newH = History { newReport = Report $ repMsgNK : r                      , newTime = time                      , .. }-      msgIsHint = tutorialHint (msgClass msg)-      msgUsedAsHit = S.member msg usedHints-      newUsedHints = if msgIsHint && displayHints && not msgUsedAsHit+      msgIsHint = isTutorialHint (msgClass msg)+      msgUsedAsHint = S.member msg usedHints+      newUsedHints = if msgIsHint && displayHints && not msgUsedAsHint                      then S.insert msg usedHints                      else usedHints   in -- Tutorial hint shown only when tutorial enabled and hint not yet shown.-     if | msgIsHint && (not displayHints || msgUsedAsHit) ->+     if | msgIsHint && (not displayHints || msgUsedAsHint) ->           (usedHints, oldHistory, False)         | not (scrapsRepeats $ msgClass msg)           || nullRepMsgNK repMsgNK ->@@ -587,26 +580,25 @@  -- | Archive old report to history, filtering out messages with 0 duplicates -- and prompts. Set up new report with a new timestamp.-archiveReport :: Bool -> History -> History-archiveReport uHistory1PerLine History{newReport=Report newMsgs, ..} =+archiveReport :: History -> History+archiveReport History{newReport=Report newMsgs, ..} =   let newFiltered@(Report r) = Report $ filter (not . nullRepMsgNK) newMsgs   in if null r      then -- Drop empty new report.           History emptyReport timeZero oldReport oldTime archivedHistory      else let lU = map attrStringToU-                   $ renderTimeReport uHistory1PerLine oldTime oldReport+                   $ renderTimeReport oldTime oldReport           in History emptyReport timeZero newFiltered newTime              $ foldl' (\ !h !v -> RB.cons v h) archivedHistory lU -renderTimeReport :: Bool -> Time -> Report -> [AttrString]-renderTimeReport uHistory1PerLine t rep@(Report r) =+renderTimeReport :: Time -> Report -> [AttrString]+renderTimeReport t rep@(Report r) =   let turns = t `timeFitUp` timeTurn       repMsgs = renderReport False rep       mgsClasses = reverse $ map (showSimpleMsgClass . msgClass . repMsg) r       turnsString = show turns       isSpace32 = Char.isSpace . Color.charFromW32       worthSaving = not . all isSpace32-      renderAS as = stringToAS (turnsString ++ ": ") ++ dropWhile isSpace32 as       renderClass (as, msgClassString) =         let lenUnderscore = 17 - length msgClassString                             + max 0 (3 - length turnsString)@@ -615,19 +607,17 @@                   ("[" ++ replicate lenUnderscore '_' ++ msgClassString ++ "]")            ++ [Color.spaceAttrW32]            ++ dropWhile isSpace32 as-  in if uHistory1PerLine-     then map renderClass $ filter (worthSaving . fst) $ zip repMsgs mgsClasses-     else map renderAS $ filter worthSaving [foldr (<+:>) [] repMsgs]+  in map renderClass $ filter (worthSaving . fst) $ zip repMsgs mgsClasses -lengthHistory :: Bool -> History -> Int-lengthHistory uHistory1PerLine History{oldReport, archivedHistory} =+lengthHistory :: History -> Int+lengthHistory History{oldReport, archivedHistory} =   RB.length archivedHistory-  + length (renderTimeReport uHistory1PerLine timeZero oldReport)-    -- matches @renderHistory@+  + length (renderTimeReport timeZero oldReport)+      -- matches @renderHistory@  -- | Render history as many lines of text. New report is not rendered. -- It's expected to be empty when history is shown.-renderHistory :: Bool -> History -> [AttrString]-renderHistory uHistory1PerLine History{..} =+renderHistory :: History -> [AttrString]+renderHistory History{..} =   map uToAttrString (RB.toList archivedHistory)-  ++ renderTimeReport uHistory1PerLine oldTime oldReport+  ++ renderTimeReport oldTime oldReport
engine-src/Game/LambdaHack/Client/UI/MsgM.hs view
@@ -10,7 +10,6 @@  import           Game.LambdaHack.Client.MonadClient import           Game.LambdaHack.Client.State-import           Game.LambdaHack.Client.UI.EffectDescription import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd import qualified Game.LambdaHack.Client.UI.Key as K import           Game.LambdaHack.Client.UI.MonadClientUI@@ -22,9 +21,11 @@ import           Game.LambdaHack.Common.State import           Game.LambdaHack.Definition.Defs +sniffMessages :: Bool+sniffMessages = False+ -- | Add a shared message to the current report. Say if it was a duplicate.-msgAddDuplicate :: (MonadClient m, MonadClientUI m, MsgShared a)-                => a -> Text -> m Bool+msgAddDuplicate :: (MonadClientUI m, MsgShared a) => a -> Text -> m Bool msgAddDuplicate msgClass t = do   sUIOptions <- getsSession sUIOptions   time <- getsState stime@@ -34,17 +35,19 @@   usedHints <- getsSession susedHints   lid <- getArenaUI   condInMelee <- condInMeleeM lid+  smuteMessages <- getsSession smuteMessages   let displayHints = fromMaybe curTutorial overrideTut       msg = toMsgShared (uMessageColors sUIOptions) msgClass t       (nusedHints, nhistory, duplicate) =         addToReport usedHints displayHints condInMelee history msg time-  modifySession $ \sess -> sess {shistory = nhistory, susedHints = nusedHints}+  unless smuteMessages $ do+    modifySession $ \sess -> sess {shistory = nhistory, susedHints = nusedHints}+    when sniffMessages $ clientPrintUI t   return duplicate  -- | Add a message comprising of two different texts, one to show, the other -- to save to messages log, to the current report.-msgAddDistinct :: (MonadClient m, MonadClientUI m)-               => MsgClassDistinct -> (Text, Text) -> m ()+msgAddDistinct :: MonadClientUI m => MsgClassDistinct -> (Text, Text) -> m () msgAddDistinct msgClass (t1, t2) = do   sUIOptions <- getsSession sUIOptions   time <- getsState stime@@ -54,25 +57,30 @@   usedHints <- getsSession susedHints   lid <- getArenaUI   condInMelee <- condInMeleeM lid+  smuteMessages <- getsSession smuteMessages   let displayHints = fromMaybe curTutorial overrideTut       msg = toMsgDistinct (uMessageColors sUIOptions) msgClass t1 t2       (nusedHints, nhistory, _) =         addToReport usedHints displayHints condInMelee history msg time-  modifySession $ \sess -> sess {shistory = nhistory, susedHints = nusedHints}+  unless smuteMessages $ do+    modifySession $ \sess -> sess {shistory = nhistory, susedHints = nusedHints}+    when sniffMessages $ clientPrintUI t1  -- | Add a message to the current report.-msgAdd :: (MonadClient m, MonadClientUI m, MsgShared a) => a -> Text -> m ()+msgAdd :: (MonadClientUI m, MsgShared a) => a -> Text -> m () msgAdd msgClass t = void $ msgAddDuplicate msgClass t  -- | Add a message to the current report. End previously collected report, -- if any, with newline.-msgLnAdd :: (MonadClient m, MonadClientUI m, MsgShared a) => a -> Text -> m ()+msgLnAdd :: (MonadClientUI m, MsgShared a) => a -> Text -> m () msgLnAdd msgClass t = do-  modifySession $ \sess -> sess {shistory = addEolToNewReport $ shistory sess}+  smuteMessages <- getsSession smuteMessages+  unless smuteMessages $+    modifySession $ \sess -> sess {shistory = addEolToNewReport $ shistory sess}   msgAdd msgClass t  -- | Add a prompt with basic keys description.-promptMainKeys :: (MonadClient m, MonadClientUI m) => m ()+promptMainKeys :: MonadClientUI m => m () promptMainKeys = do   side <- getsClient sside   ours <- getsState $ fidActorNotProjGlobalAssocs side@@ -85,6 +93,7 @@   saimMode <- getsSession saimMode   UIOptions{uVi, uLeftHand} <- getsSession sUIOptions   xhair <- getsSession sxhair+  miniHintAiming <- getMiniHintAiming   -- The silly "axwdqezc" name of keys is chosen to match "hjklyubn",   -- which the usual way of writing them.   let moveKeys | uVi && uLeftHand = "keypad or axwdqezc or hjklyubn"@@ -92,7 +101,6 @@                | uVi = "keypad or hjklyubn"                | otherwise = "keypad"       manyTeammates = length ours > 1-      detailAtDefault = (detailLevel <$> saimMode) == Just defaultDetailLevel       -- @Tab@ here is not a button, which we would write consistently       -- as @TAB@, just as in our internal in-game key naming, but a key name       -- as written on the keyboard, hence most useful to a newbie.@@ -116,7 +124,7 @@         <+> keepTab         <+> moreHelp            | otherwise =-        (if detailAtDefault then "" else miniHintAimingBare)+        miniHintAiming         <+> tgtKindVerb xhair         <+> "with" <+> moveKeys <+> "keys or mouse."         <+> keepTab@@ -133,7 +141,5 @@  -- | Store new report in the history and archive old report. recordHistory :: MonadClientUI m => m ()-recordHistory = do-  UIOptions{uHistory1PerLine} <- getsSession sUIOptions-  modifySession $ \sess ->-    sess {shistory = archiveReport uHistory1PerLine $ shistory sess}+recordHistory =+  modifySession $ \sess -> sess {shistory = archiveReport $ shistory sess}
engine-src/Game/LambdaHack/Client/UI/Overlay.hs view
@@ -1,18 +1,24 @@-{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RankNTypes, TupleSections #-} -- | Screen overlays. module Game.LambdaHack.Client.UI.Overlay-  ( -- * AttrString+  ( -- * DisplayFont+    DisplayFont, isPropFont, isSquareFont, isMonoFont, textSize+  , -- * FontSetup+    FontSetup(..), multiFontSetup, singleFontSetup+  , -- * AttrString     AttrString, blankAttrString, textToAS, textFgToAS, stringToAS   , (<+:>), (<\:>)     -- * AttrLine-  , AttrLine, attrLine, emptyAttrLine, attrStringToAL, firstParagraph, linesAttr-  , textToAL, textFgToAL, stringToAL, splitAttrString-  , indentSplitAttrString, indentSplitAttrString2+  , AttrLine, attrLine, emptyAttrLine, attrStringToAL, firstParagraph+  , textToAL, textFgToAL, stringToAL, linesAttr+  , splitAttrString, indentSplitAttrString     -- * Overlay-  , Overlay, offsetOverlay, offsetOverlayX, updateLine+  , Overlay, xytranslateOverlay, xtranslateOverlay, ytranslateOverlay+  , offsetOverlay, offsetOverlayX, typesetXY+  , updateLine, rectangleOfSpaces, maxYofOverlay, labDescOverlay #ifdef EXPOSE_INTERNAL     -- * Internal operations-  , splitAttrPhrase+  , nonbreakableRev, isPrefixOfNonbreakable, breakAtSpace, splitAttrPhrase #endif   ) where @@ -26,6 +32,53 @@ import           Game.LambdaHack.Client.UI.PointUI import qualified Game.LambdaHack.Definition.Color as Color +-- * DisplayFont++-- | Three types of fonts used in the UI. Overlays (layers, more or less)+-- in proportional font are overwritten by layers in square font,+-- which are overwritten by layers in mono font.+-- All overlays overwrite the rendering of the game map, which is+-- the underlying basic UI frame, comprised of square font glyps.+--+-- This type needs to be kept abstract to ensure that frontend-enforced+-- or user config-enforced font assignments in 'FontSetup'+-- (e.g., stating that the supposedly proportional font is overriden+-- to be the square font) can't be ignored. Otherwise a programmer+-- could use arbirary @DisplayFont@, instead of the one taken from 'FontSetup',+-- and so, e.g., calculating the width of an overlay so constructed+-- in order to decide where another overlay can start would be inconsistent+-- what what font is really eventually used when rendering.+--+-- Note that the order of constructors has limited effect,+-- but it illustrates how overwriting is explicitly implemented+-- in frontends that support all fonts.+data DisplayFont = PropFont | SquareFont | MonoFont+  deriving (Show, Eq, Enum)++isPropFont, isSquareFont, isMonoFont :: DisplayFont -> Bool+isPropFont = (== PropFont)+isSquareFont = (== SquareFont)+isMonoFont = (== MonoFont)++textSize :: DisplayFont -> [a] -> Int+textSize SquareFont l = 2 * length l+textSize MonoFont l = length l+textSize PropFont _ = error "size of proportional font texts is not defined"++-- * FontSetup++data FontSetup = FontSetup+  { squareFont :: DisplayFont+  , monoFont   :: DisplayFont+  , propFont   :: DisplayFont+  }++multiFontSetup :: FontSetup+multiFontSetup = FontSetup SquareFont MonoFont PropFont++singleFontSetup :: FontSetup+singleFontSetup = FontSetup SquareFont SquareFont SquareFont+ -- * AttrString  -- | String of colourful text. End of line characters permitted.@@ -96,7 +149,8 @@  -- * AttrLine --- | Line of colourful text. End of line characters forbidden.+-- | Line of colourful text. End of line characters forbidden. Trailing+-- @White@ space forbidden. newtype AttrLine = AttrLine {attrLine :: AttrString}   deriving (Show, Eq) @@ -159,9 +213,11 @@  where   cons ~(h, t) = h : t --- | Split a string into lines. Avoids breaking the line at a character--- other than space. Remove space characters from the starts and ends--- of created lines. Newlines are respected.+-- | Split a string into lines. Avoid breaking the line at a character+-- other than space. Remove the spaces on which lines are broken,+-- keep other spaces. In expensive assertions mode (dev debug mode)+-- fail at trailing spaces, but keep leading spaces, e.g., to make+-- distance from a text in another font. Newlines are respected. -- -- Note that we only split wrt @White@ space, nothing else, -- and the width, in the first argument, is calculated in characters,@@ -170,22 +226,16 @@ splitAttrString :: Int -> Int -> AttrString -> [AttrLine] splitAttrString w0 w1 l = case linesAttr l of   [] -> []-  x : xs ->-    (splitAttrPhrase w0 w1-     . AttrLine . dropWhile (== Color.spaceAttrW32) . attrLine) x-    ++ concatMap (splitAttrPhrase w1 w1-                  . AttrLine . dropWhile (== Color.spaceAttrW32) . attrLine) xs--indentSplitAttrString :: Int -> AttrString -> [AttrLine]-indentSplitAttrString w l =-  let ts = splitAttrString w (w - 1) l-  in case ts of-    [] -> []-    hd : tl -> hd : map (AttrLine . ([Color.spaceAttrW32] ++) . attrLine) tl+  x : xs -> splitAttrPhrase w0 w1 x ++ concatMap (splitAttrPhrase w1 w1) xs -indentSplitAttrString2 :: Bool -> Int -> AttrString -> [AttrLine]-indentSplitAttrString2 isProp w l =-  let nspaces = if isProp then 4 else 2+indentSplitAttrString :: DisplayFont -> Int -> AttrString -> [AttrLine]+indentSplitAttrString font w l =+  -- Sadly this depends on how wide the space is in propotional font,+  -- which varies wildly, so we err on the side of larger indent.+  let nspaces = case font of+        SquareFont -> 1+        MonoFont -> 2+        PropFont -> 4       ts = splitAttrString w (w - nspaces) l       -- Proportional spaces are very narrow.       spaces = replicate nspaces Color.spaceAttrW32@@ -224,6 +274,16 @@ -- simply not shown. type Overlay = [(PointUI, AttrLine)] +xytranslateOverlay :: Int -> Int -> Overlay -> Overlay+xytranslateOverlay dx dy =+  map (\(PointUI x y, al) -> (PointUI (x + dx) (y + dy), al))++xtranslateOverlay :: Int -> Overlay -> Overlay+xtranslateOverlay dx = xytranslateOverlay dx 0++ytranslateOverlay :: Int -> Overlay -> Overlay+ytranslateOverlay = xytranslateOverlay 0+ offsetOverlay :: [AttrLine] -> Overlay offsetOverlay l = map (first $ PointUI 0) $ zip [0..] l @@ -231,9 +291,33 @@ offsetOverlayX l =   map (\(y, (x, al)) -> (PointUI x y, al)) $ zip [0..] l +typesetXY :: (Int, Int) -> [AttrLine] -> Overlay+typesetXY (xoffset, yoffset) =+  map (\(y, al) -> (PointUI xoffset (y + yoffset), al)) . zip [0..]+ -- @f@ should not enlarge the line beyond screen width nor introduce linebreaks. updateLine :: Int -> (Int -> AttrString -> AttrString) -> Overlay -> Overlay updateLine y f ov =   let upd (p@(PointUI px py), AttrLine l) =         if py == y then (p, AttrLine $ f px l) else (p, AttrLine l)   in map upd ov++rectangleOfSpaces :: Int -> Int -> Overlay+rectangleOfSpaces x y =+  let blankAttrLine = AttrLine $ replicate x Color.nbspAttrW32+  in offsetOverlay $ replicate y blankAttrLine++maxYofOverlay :: Overlay -> Int+maxYofOverlay ov = let yOfOverlay (PointUI _ y, _) = y+                   in maximum $ 0 : map yOfOverlay ov++labDescOverlay :: DisplayFont -> Int -> AttrString -> (Overlay, Overlay)+labDescOverlay labFont width as =+  let (tLab, tDesc) = span (/= Color.spaceAttrW32) as+      labLen = textSize labFont tLab+      ovLab = offsetOverlay [attrStringToAL tLab]+      ovDesc = offsetOverlayX $+        case splitAttrString (width - labLen) width tDesc of+          [] -> []+          l : ls -> (labLen, l) : map (0,) ls+  in (ovLab, ovDesc)
engine-src/Game/LambdaHack/Client/UI/PointUI.hs view
@@ -19,12 +19,18 @@ -- on odd coordinates). These are not game map coordinates, -- becuse UI is larger and more fine-grained than just the game map. data PointUI = PointUI Int Int-  deriving (Eq, Show)+  deriving (Show, Eq)  -- | Coordinates of the big square fonts. These are not game map coordinates, -- because the latter are offset by @mapStartY@ and represented by @Point@.+--+-- However, confusingly, @Point@ is also used for square font glyph coordinates,+-- though exclusively in context of rendered frames to be sent to a frontend,+-- namely @PointArray.Array@, which is indexed by @Point@ and is a vector,+-- and so traditionally indexed starting from zero and not from minus one,+-- as would be needed for consistency. data PointSquare = PointSquare Int Int-  deriving (Eq, Show)+  deriving (Show, Eq)  squareToUI :: PointSquare -> PointUI {-# INLINE squareToUI #-}
engine-src/Game/LambdaHack/Client/UI/SessionUI.hs view
@@ -1,10 +1,10 @@ {-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-} -- | The client UI session state. module Game.LambdaHack.Client.UI.SessionUI-  ( SessionUI(..), ItemDictUI, AimMode(..), KeyMacro(..), KeyMacroFrame(..)-  , RunParams(..), ChosenLore(..)+  ( SessionUI(..), ReqDelay(..), ItemDictUI, AimMode(..), KeyMacro(..)+  , KeyMacroFrame(..), RunParams(..), ChosenLore(..)   , emptySessionUI, emptyMacroFrame-  , toggleMarkVision, toggleMarkSmell, cycleOverrideTut, getActorUI+  , cycleMarkVision, toggleMarkSmell, cycleOverrideTut, getActorUI   ) where  import Prelude ()@@ -18,7 +18,9 @@ import qualified Data.Set as S import           Data.Time.Clock.POSIX import           GHC.Generics (Generic)+import qualified System.Random.SplitMix32 as SM +import           Game.LambdaHack.Client.Request import           Game.LambdaHack.Client.State import           Game.LambdaHack.Client.UI.ActorUI import           Game.LambdaHack.Client.UI.ContentClientUI@@ -34,25 +36,32 @@ import           Game.LambdaHack.Common.Time import           Game.LambdaHack.Common.Types import           Game.LambdaHack.Definition.Defs-import qualified System.Random.SplitMix32 as SM  -- | The information that is used across a client playing session, -- including many consecutive games in a single session. -- Some of it is saved, some is reset when a new playing session starts. -- An important component is the frontend session. data SessionUI = SessionUI-  { sxhair         :: Maybe Target       -- ^ the common xhair-  , sxhairGoTo     :: Maybe Target       -- ^ xhair set for last GoTo-  , sactorUI       :: ActorDictUI        -- ^ assigned actor UI presentations-  , sitemUI        :: ItemDictUI         -- ^ assigned item first seen level-  , sslots         :: ItemSlots          -- ^ map from slots to items+  { sreqPending    :: Maybe RequestUI+                                    -- ^ request created by a UI query+                                    --   but not yet sent to the server+  , sreqDelay      :: ReqDelay      -- ^ server delayed sending query to client+                                    --   or receiving request from client+  , sreqQueried    :: Bool          -- ^ player is now queried for a command+  , sregainControl :: Bool          -- ^ player requested to regain control+                                    --   from AI ASAP+  , sxhair         :: Maybe Target  -- ^ the common xhair+  , sxhairGoTo     :: Maybe Target  -- ^ xhair set for last GoTo+  , sactorUI       :: ActorDictUI   -- ^ assigned actor UI presentations+  , sitemUI        :: ItemDictUI    -- ^ assigned item first seen level+  , sslots         :: ItemSlots     -- ^ map from slots to items   , slastItemMove  :: Maybe (CStore, CStore)-                                         -- ^ last item move stores-  , schanF         :: ChanFrontend       -- ^ connection with the frontend-  , sccui          :: CCUI               -- ^ UI client content-  , sUIOptions     :: UIOptions          -- ^ UI options as set by the player-  , saimMode       :: Maybe AimMode      -- ^ aiming mode-  , sxhairMoused   :: Bool               -- ^ last mouse aiming not vacuus+                                    -- ^ last item move stores+  , schanF         :: ChanFrontend  -- ^ connection with the frontend+  , sccui          :: CCUI          -- ^ UI client content+  , sUIOptions     :: UIOptions     -- ^ UI options as set by the player+  , saimMode       :: Maybe AimMode -- ^ aiming mode+  , sxhairMoused   :: Bool          -- ^ last mouse aiming not vacuus   , sitemSel       :: Maybe (ItemId, CStore, Bool)                                     -- ^ selected item, if any, it's store and                                     --   whether to override suitability check@@ -62,6 +71,7 @@                                     -- ^ parameters of the current run, if any   , shistory       :: History       -- ^ history of messages   , spointer       :: PointUI       -- ^ mouse pointer position+  , sautoYes       :: Bool          -- ^ whether to auto-clear prompts   , smacroFrame    :: KeyMacroFrame -- ^ the head of the key macro stack   , smacroStack    :: [KeyMacroFrame]                                     -- ^ the tail of the key macro stack@@ -69,13 +79,14 @@                                     -- ^ actors that just got out of sight   , swaitTimes     :: Int           -- ^ player just waited this many times   , swasAutomated  :: Bool          -- ^ the player just exited AI automation-  , smarkVision    :: Bool          -- ^ mark leader and party FOV+  , smarkVision    :: Int           -- ^ mark leader and party FOV   , smarkSmell     :: Bool          -- ^ mark smell, if the leader can smell   , snxtScenario   :: Int           -- ^ next game scenario number   , scurTutorial   :: Bool          -- ^ whether current game is a tutorial   , snxtTutorial   :: Bool          -- ^ whether next game is to be tutorial   , soverrideTut   :: Maybe Bool    -- ^ override display of tutorial hints   , susedHints     :: S.Set Msg     -- ^ tutorial hints already shown this game+  , smuteMessages  :: Bool          -- ^ whether to mute all new messages   , smenuIxMap     :: M.Map String Int                                     -- ^ indices of last used menu items   , schosenLore    :: ChosenLore    -- ^ last lore chosen to display@@ -94,6 +105,9 @@   , srandomUI      :: SM.SMGen      -- ^ current random generator for UI   } +data ReqDelay = ReqDelayNot | ReqDelayHandled | ReqDelayAlarm+  deriving Eq+ -- | Local macro buffer frame. Predefined macros have their own in-game macro -- buffer, allowing them to record in-game macro, queue actions and repeat -- the last macro's action.@@ -104,7 +118,7 @@                                              --   repeat from Right   , keyPending     :: KeyMacro               -- ^ actions pending to be handled   , keyLast        :: Maybe K.KM             -- ^ last pressed key-  } deriving (Show)+  } deriving Show  -- This can stay a map forever, not a vector, because it's added to often, -- but never read from, except when the user requests item details.@@ -146,7 +160,11 @@ emptySessionUI :: UIOptions -> SessionUI emptySessionUI sUIOptions =   SessionUI-    { sxhair = Nothing+    { sreqPending = Nothing+    , sreqDelay = ReqDelayNot+    , sreqQueried = False+    , sregainControl = False+    , sxhair = Nothing     , sxhairGoTo = Nothing     , sactorUI = EM.empty     , sitemUI = EM.empty@@ -164,18 +182,20 @@     , srunning = Nothing     , shistory = emptyHistory 0     , spointer = PointUI 0 0+    , sautoYes = False     , smacroFrame = emptyMacroFrame     , smacroStack = []     , slastLost = ES.empty     , swaitTimes = 0     , swasAutomated = False-    , smarkVision = False+    , smarkVision = 1     , smarkSmell = True     , snxtScenario = 0     , scurTutorial = False     , snxtTutorial = True  -- matches @snxtScenario = 0@     , soverrideTut = Nothing     , susedHints = S.empty+    , smuteMessages = False     , smenuIxMap = M.empty     , schosenLore = ChosenNothing     , sdisplayNeeded = False@@ -192,8 +212,8 @@ emptyMacroFrame :: KeyMacroFrame emptyMacroFrame = KeyMacroFrame (Right mempty) mempty Nothing -toggleMarkVision :: SessionUI -> SessionUI-toggleMarkVision sess = sess {smarkVision = not (smarkVision sess)}+cycleMarkVision :: SessionUI -> SessionUI+cycleMarkVision sess = sess {smarkVision = succ (smarkVision sess) `mod` 3}  toggleMarkSmell :: SessionUI -> SessionUI toggleMarkSmell sess = sess {smarkSmell = not (smarkSmell sess)}@@ -220,7 +240,7 @@     put sitemSel     put sselected     put srunning-    put $ archiveReport True shistory+    put $ archiveReport shistory       -- avoid displaying ending messages again at game start     put smarkVision     put smarkSmell@@ -249,18 +269,24 @@     soverrideTut <- get     susedHints <- get     g <- get-    let sxhairGoTo = Nothing+    let sreqPending = Nothing+        sreqDelay = ReqDelayNot+        sreqQueried = False+        sregainControl = False+        sxhairGoTo = Nothing         slastItemMove = Nothing         schanF = ChanFrontend $ const $           error $ "Binary: ChanFrontend" `showFailure` ()         sccui = emptyCCUI         sxhairMoused = True         spointer = PointUI 0 0+        sautoYes = False         smacroFrame = emptyMacroFrame         smacroStack = []         slastLost = ES.empty         swaitTimes = 0         swasAutomated = False+        smuteMessages = False         smenuIxMap = M.empty         schosenLore = ChosenNothing         sdisplayNeeded = False  -- displayed regardless
engine-src/Game/LambdaHack/Client/UI/Slideshow.hs view
@@ -1,14 +1,15 @@ -- | Slideshows. module Game.LambdaHack.Client.UI.Slideshow-  ( DisplayFont, isSquareFont, isMonoFont, FontOverlayMap, FontSetup(..)-  , multiFontSetup, monoFontSetup, singleFontSetup, textSize-  , ButtonWidth(..), KYX, OKX, Slideshow(slideshow)-  , emptySlideshow, unsnoc, toSlideshow, attrLinesToFontMap-  , maxYofOverlay, menuToSlideshow, wrapOKX, splitOverlay, splitOKX+  ( FontOverlayMap, maxYofFontOverlayMap+  , KeyOrSlot, ButtonWidth(..)+  , KYX, xytranslateKXY, xtranslateKXY, ytranslateKXY, yrenumberKXY+  , OKX, emptyOKX, xytranslateOKX, sideBySideOKX, labDescOKX+  , Slideshow(slideshow), emptySlideshow, unsnoc, toSlideshow+  , attrLinesToFontMap, menuToSlideshow, wrapOKX, splitOverlay, splitOKX   , highSlideshow #ifdef EXPOSE_INTERNAL     -- * Internal operations-  , moreMsg, endMsg, keysOKX, showTable, showNearbyScores+  , keysOKX, showTable, showNearbyScores #endif   ) where @@ -27,38 +28,12 @@ import qualified Game.LambdaHack.Common.HighScore as HighScore import qualified Game.LambdaHack.Definition.Color as Color -data DisplayFont = SquareFont | MonoFont | PropFont-  deriving (Show, Eq, Enum)--isSquareFont :: DisplayFont -> Bool-isSquareFont SquareFont = True-isSquareFont _ = False--isMonoFont :: DisplayFont -> Bool-isMonoFont MonoFont = True-isMonoFont _ = False- type FontOverlayMap = EM.EnumMap DisplayFont Overlay -data FontSetup = FontSetup-  { squareFont :: DisplayFont-  , monoFont   :: DisplayFont-  , propFont   :: DisplayFont-  }--multiFontSetup :: FontSetup-multiFontSetup = FontSetup SquareFont MonoFont PropFont--monoFontSetup :: FontSetup-monoFontSetup = FontSetup SquareFont MonoFont MonoFont--singleFontSetup :: FontSetup-singleFontSetup = FontSetup SquareFont SquareFont SquareFont+maxYofFontOverlayMap :: FontOverlayMap -> Int+maxYofFontOverlayMap ovs = maximum (0 : map maxYofOverlay (EM.elems ovs)) -textSize :: DisplayFont -> [a] -> Int-textSize SquareFont l = 2 * length l-textSize MonoFont l = length l-textSize PropFont _ = error "size of proportional font texts is not defined"+type KeyOrSlot = Either K.KM SlotChar  -- TODO: probably best merge the PointUI into that and represent -- the position as characters, too, translating to UI positions as needed.@@ -72,13 +47,60 @@   deriving (Show, Eq)  -- | A key or an item slot label at a given position on the screen.-type KYX = (Either [K.KM] SlotChar, (PointUI, ButtonWidth))+type KYX = (KeyOrSlot, (PointUI, ButtonWidth)) +xytranslateKXY :: Int -> Int -> KYX -> KYX+xytranslateKXY dx dy (km, (PointUI x y, len)) =+  (km, (PointUI (x + dx) (y + dy), len))++xtranslateKXY :: Int -> KYX -> KYX+xtranslateKXY dx = xytranslateKXY dx 0++ytranslateKXY :: Int -> KYX -> KYX+ytranslateKXY = xytranslateKXY 0++yrenumberKXY :: Int -> KYX -> KYX+yrenumberKXY ynew (km, (PointUI x _, len)) = (km, (PointUI x ynew, len))+ -- | An Overlay of text with an associated list of keys or slots--- that activated when the specified screen position is pointed at.+-- that activate when the specified screen position is pointed at. -- The list should be sorted wrt rows and then columns. type OKX = (FontOverlayMap, [KYX]) +emptyOKX :: OKX+emptyOKX = (EM.empty, [])++xytranslateOKX ::Int -> Int -> OKX -> OKX+xytranslateOKX dx dy (ovs, kyxs) =+  ( EM.map (xytranslateOverlay dx dy) ovs+  , map (xytranslateKXY dx dy) kyxs )++sideBySideOKX :: Int -> Int -> OKX -> OKX -> OKX+sideBySideOKX dx dy (ovs1, kyxs1) (ovs2, kyxs2) =+  let (ovs3, kyxs3) = xytranslateOKX dx dy (ovs2, kyxs2)+  in ( EM.unionWith (++) ovs1 ovs3+     , sortOn (\(_, (PointUI x y, _)) -> (y, x)) $ kyxs1 ++ kyxs3 )++-- The bangs are to free the possibly very long input list ASAP.+labDescOKX :: DisplayFont -> DisplayFont+           -> [(AttrString, AttrString, KeyOrSlot)]+           -> OKX+labDescOKX labFont descFont l =+  let descFontSize | isPropFont descFont = length  -- may be less or a bit more+                   | otherwise = textSize descFont+      processRow :: (AttrString, AttrString, KeyOrSlot)+                 -> (AttrLine, (Int, AttrLine), KYX)+      processRow (!tLab, !tDesc, !ekm) =+        let labLen = textSize labFont tLab+            lenButton = labLen + descFontSize tDesc+        in ( attrStringToAL tLab+           , (labLen, attrStringToAL tDesc)+           , (ekm, (PointUI 0 0, ButtonWidth descFont lenButton)) )+      (tsLab, tsDesc, kxs) = unzip3 $ map processRow l+      ovs = EM.insertWith (++) labFont (offsetOverlay tsLab)+            $ EM.singleton descFont $ offsetOverlayX tsDesc+  in (ovs, zipWith yrenumberKXY [0..] kxs)+ -- | A list of active screenfulls to be shown one after another. -- Each screenful has an independent numbering of rows and columns. newtype Slideshow = Slideshow {slideshow :: [OKX]}@@ -94,52 +116,54 @@     okx : rest -> Just (Slideshow $ reverse rest, okx)  toSlideshow :: FontSetup -> [OKX] -> Slideshow-toSlideshow FontSetup{..} okxs = Slideshow $ addFooters False okxsNotNull+toSlideshow FontSetup{..} okxs = Slideshow $ addFooters False okxs  where-  okxFilter (ov, kyxs) =-    (ov, filter (either (not . null) (const True) . fst) kyxs)-  okxsNotNull = map okxFilter okxs-  pofOv :: Overlay -> PointUI-  pofOv [] = PointUI 0 0-  pofOv l = let pyAfterLast = 1 + maxYofOverlay l  -- append after last line-            in PointUI 0 pyAfterLast   atEnd = flip (++)-  appendToFontOverlayMap :: FontOverlayMap -> AttrLine-                         -> (FontOverlayMap, PointUI, DisplayFont)-  appendToFontOverlayMap ovs al =-    let maxYminXofOverlay ov = let ymxOfOverlay (PointUI x y, _) = (- y, x)-                               in minimum $ (0, 0) : map ymxOfOverlay ov-        -- @SortOn@ less efficient here, because function cheap.+  appendToFontOverlayMap :: FontOverlayMap -> String+                         -> (FontOverlayMap, PointUI, DisplayFont, Int)+  appendToFontOverlayMap ovs msg =+    let maxYminXofOverlay ov =+          let ymxOfOverlay (PointUI x y, _) = (- y, x)+          in minimum $ maxBound : map ymxOfOverlay ov+        -- @sortOn@ less efficient here, because function cheap.         assocsYX = sortBy (comparing snd)                    $ EM.assocs $ EM.map maxYminXofOverlay ovs-        (fontMax, unique) = case assocsYX of-          [] -> (monoFont, False)-          (font, (y, _x)) : rest -> (font, all (\(_, (y2, _)) -> y /= y2) rest)-        insertAl ovF =-          let p = pofOv ovF-              displayFont = case fontMax of-                SquareFont | unique -> SquareFont-                _ -> monoFont-          in (EM.insertWith atEnd displayFont [(p, al)] ovs, p, displayFont)-    in case EM.lookup fontMax ovs of-      Just ovF -> insertAl ovF-      Nothing -> insertAl []+        (fontMax, yMax) = case assocsYX of+          [] -> (monoFont, 0)+          (font, (yNeg, _x)) : rest ->+            let unique = all (\(_, (yNeg2, _)) -> yNeg /= yNeg2) rest+            in ( if isSquareFont font && unique+                 then font+                 else monoFont+               , - yNeg )+        pMax = PointUI 0 (yMax + 1)  -- append after last line+    in ( EM.insertWith atEnd fontMax [(pMax, stringToAL msg)] ovs+       , pMax+       , fontMax+       , length msg )   addFooters :: Bool -> [OKX] -> [OKX]-  addFooters _ [] = error $ "" `showFailure` okxsNotNull+  addFooters _ [] = error $ "" `showFailure` okxs   addFooters _ [(als, [])] =-    let (ovs, p, font) = appendToFontOverlayMap als (stringToAL endMsg)-    in [(ovs, [(Left [K.safeSpaceKM], (p, ButtonWidth font 15))])]+    -- TODO: make sure this case never coincides with the space button+    -- actually returning to top, as opposed to finishing preview.+    let (ovs, p, font, width) = appendToFontOverlayMap als "--end--"+    in [(ovs, [(Left K.safeSpaceKM, (p, ButtonWidth font width))])]   addFooters False [(als, kxs)] = [(als, kxs)]   addFooters True [(als, kxs)] =-    let (ovs, p, font) = appendToFontOverlayMap als (stringToAL endMsg)-    in [(ovs, kxs ++ [(Left [K.safeSpaceKM], (p, ButtonWidth font 15))])]+    let (ovs, p, font, width) = appendToFontOverlayMap als "--back to top--"+    in [(ovs, kxs ++ [(Left K.safeSpaceKM, (p, ButtonWidth font width))])]   addFooters _ ((als, kxs) : rest) =-    let (ovs, p, font) = appendToFontOverlayMap als (stringToAL moreMsg)-    in (ovs, kxs ++ [(Left [K.safeSpaceKM], (p, ButtonWidth font 8))])+    let (ovs, p, font, width) = appendToFontOverlayMap als "--more--"+    in (ovs, kxs ++ [(Left K.safeSpaceKM, (p, ButtonWidth font width))])        : addFooters True rest -attrLinesToFontMap :: Int -> [(DisplayFont, [AttrLine])] -> FontOverlayMap-attrLinesToFontMap start0 blurb =+-- | This appends vertically a list of blurbs into a single font overlay map.+-- Not to be used if some blurbs need to be places overlapping vertically,+-- e.g., when the square font symbol needs to be in the same line+-- as the start of the descritpion of the denoted item+-- or when mono font buttons need to be after a prompt.+attrLinesToFontMap :: [(DisplayFont, [AttrLine])] -> FontOverlayMap+attrLinesToFontMap blurb =   let zipAttrLines :: Int -> [AttrLine] -> (Overlay, Int)       zipAttrLines start als =         ( map (first $ PointUI 0) $ zip [start ..] als@@ -150,19 +174,9 @@         let (als2, start2) = zipAttrLines start als         in ( EM.insertWith (++) font als2 em            , start2 )-      (ov, _) = foldl' addOverlay (EM.empty, start0) blurb+      (ov, _) = foldl' addOverlay (EM.empty, 0) blurb   in ov -moreMsg :: String-moreMsg = "--more--"--endMsg :: String-endMsg = "--back to top--"--maxYofOverlay :: Overlay -> Int-maxYofOverlay ov = let yOfOverlay (PointUI _ y, _) = y-                   in maximum $ 0 : map yOfOverlay ov- menuToSlideshow :: OKX -> Slideshow menuToSlideshow (als, kxs) =   assert (not (EM.null als || null kxs)) $ Slideshow [(als, kxs)]@@ -187,8 +201,8 @@                 , ( xlineStart                   , s : kL                   , kV-                  , (Left [key], ( PointUI x y-                                 , ButtonWidth displayFont (length s) ))+                  , (Left key, ( PointUI x y+                               , ButtonWidth displayFont (length s) ))                     : kX ) )       ((ystop, _), (xlineStop, kL1, kV1, kX1)) =         foldl' f ((ystart, xstart), (xstart, [], [], [])) ks@@ -204,21 +218,12 @@  -- The font argument is for the report and keys overlay. Others already have -- assigned fonts.-splitOverlay :: FontSetup -> Bool -> Int -> Int -> Int-             -> Report -> [K.KM] -> OKX+splitOverlay :: FontSetup -> Int -> Int -> Int -> Report -> [K.KM] -> OKX              -> Slideshow-splitOverlay fontSetup uScreen1PerLine width height wrap-             report keys (ls0, kxs0) =+splitOverlay fontSetup width height wrap report keys (ls0, kxs0) =   let renderedReport = renderReport True report-      reportAS = if uScreen1PerLine-                 then foldr (<\:>) [] renderedReport-                 else foldr (<+:>) [] renderedReport-      msgLong = not uScreen1PerLine-                && null keys && EM.null ls0 && null kxs0-                && length reportAS <= 2 * width-                && all ((/= '\n') . Color.charFromW32) reportAS-                     -- if fits in one long line, don't wrap into short lines-  in toSlideshow fontSetup $ splitOKX fontSetup msgLong width height wrap+      reportAS = foldr (<\:>) [] renderedReport+  in toSlideshow fontSetup $ splitOKX fontSetup False width height wrap                                       reportAS keys (ls0, kxs0)  -- Note that we only split wrt @White@ space, nothing else.@@ -227,9 +232,7 @@          -> [OKX] splitOKX FontSetup{..} msgLong width height wrap reportAS keys (ls0, kxs0) =   assert (height > 2) $-  let indentSplitSpaces = indentSplitAttrString2-                            (not (isMonoFont propFont || isSquareFont propFont))-      reportParagraphs = linesAttr reportAS+  let reportParagraphs = linesAttr reportAS       -- TODO: until SDL support for measuring prop font text is released,       -- we have to use MonoFont for the paragraph that ends with buttons.       (repProp, repMono) =@@ -254,27 +257,28 @@           let firstWidth = if length (attrLine r) <= 2 * msgWidth                            then msgWidth                            else msgWrap-          in (indentSplitSpaces firstWidth . attrLine) r  -- first possibly long-             ++ concatMap (indentSplitSpaces msgWrap . attrLine) rs+          in (indentSplitAttrString propFont firstWidth . attrLine) r+               -- first possibly long+             ++ concatMap (indentSplitAttrString propFont msgWrap . attrLine) rs       -- TODO: refactor this ugly pile of copy-paste       repPropW = offsetOverlay-                 $ concatMap (indentSplitSpaces width . attrLine) repProp+                 $ concatMap (indentSplitAttrString propFont width . attrLine)+                             repProp       -- If the mono portion first on the line, let it take half width,       -- but if previous lines shorter, match them and only buttons       -- are permitted to stick out.       monoWidth = if null repProp then msgWidth else msgWrap-      repMono0 = map (\(PointUI x y, al) ->-                        (PointUI x (y + length repProp0), al))+      repMono0 = ytranslateOverlay (length repProp0)                  $ offsetOverlay-                 $ indentSplitAttrString monoWidth $ attrLine repMono-      repMonoW = map (\(PointUI x y, al) ->-                        (PointUI x (y + length repPropW), al))+                 $ indentSplitAttrString monoFont monoWidth $ attrLine repMono+      repMonoW = ytranslateOverlay (length repPropW)                  $ offsetOverlay-                 $ indentSplitAttrString width $ attrLine repMono+                 $ indentSplitAttrString monoFont width $ attrLine repMono       repWhole0 = offsetOverlay-                  $ concatMap (indentSplitSpaces msgWidth . attrLine)+                  $ concatMap (indentSplitAttrString propFont msgWidth+                               . attrLine)                               reportParagraphs-      repWhole1 = map (\(PointUI x y, al) -> (PointUI x (y + 1), al)) repWhole0+      repWhole1 = ytranslateOverlay 1 repWhole0       lenOfRep0 = length repProp0 + length repMono0       lenOfRepW = length repPropW + length repMonoW       startOfKeys = if null repMono0@@ -287,18 +291,16 @@                   ++ [Color.nbspAttrW32]       (lX0, keysX0) = keysOKX monoFont 0 (length pressAKey) width keys       (lX1, keysX1) = keysOKX monoFont 1 0 width keys-      (lX, keysX) = keysOKX monoFont (lenOfRep0 - 1) startOfKeys+      (lX, keysX) = keysOKX monoFont (max 0 $ lenOfRep0 - 1) startOfKeys                             (2 * width) keys-      (lXW, keysXW) = keysOKX monoFont (lenOfRepW - 1) startOfKeysW+      (lXW, keysXW) = keysOKX monoFont (max 0 $ lenOfRepW - 1) startOfKeysW                               (2 * width) keys-      renumber dy (km, (PointUI x y, len)) = (km, (PointUI x (y + dy), len))-      renumberOv dy = map (\(PointUI x y, al) -> (PointUI x (y + dy), al))       splitO :: Int -> (Overlay, Overlay, [KYX]) -> OKX -> [OKX]       splitO yoffset (hdrProp, hdrMono, rk) (ls, kxs) =         let hdrOff | null hdrProp && null hdrMono = 0                    | otherwise = 1 + maxYofOverlay hdrMono-            keyRenumber = map $ renumber (hdrOff - yoffset)-            lineRenumber = EM.map $ renumberOv (hdrOff - yoffset)+            keyTranslate = map $ ytranslateKXY (hdrOff - yoffset)+            lineTranslate = EM.map $ ytranslateOverlay (hdrOff - yoffset)             yoffsetNew = yoffset + height - hdrOff - 1             ltOffset :: (PointUI, a) -> Bool             ltOffset (PointUI _ y, _) = y < yoffsetNew@@ -307,9 +309,9 @@             prependHdr = EM.insertWith (++) propFont hdrProp                          . EM.insertWith (++) monoFont hdrMono         in if all null $ EM.elems post  -- all fits on one screen-           then [(prependHdr $ lineRenumber pre, rk ++ keyRenumber kxs)]+           then [(prependHdr $ lineTranslate pre, rk ++ keyTranslate kxs)]            else let (preX, postX) = span (\(_, pa) -> ltOffset pa) kxs-                in (prependHdr $ lineRenumber pre, rk ++ keyRenumber preX)+                in (prependHdr $ lineTranslate pre, rk ++ keyTranslate preX)                    : splitO yoffsetNew (hdrProp, hdrMono, rk) (post, postX)       firstParaReport = firstParagraph reportAS       hdrShortened = ( [(PointUI 0 0, firstParaReport)]@@ -319,12 +321,12 @@       ((lsInit, kxsInit), (headerProp, headerMono, rkxs)) =         -- Check whether all space taken by report and keys.         if | (lenOfRep0 + length lX) < height ->  -- display normally-             ((EM.empty, []), (repProp0, lX ++ repMono0, keysX))+             (emptyOKX, (repProp0, lX ++ repMono0, keysX))            | (lenOfRepW + length lXW) < height ->  -- display widely-             ((EM.empty, []), (repPropW, lXW ++ repMonoW, keysXW))+             (emptyOKX, (repPropW, lXW ++ repMonoW, keysXW))            | length reportParagraphs == 1              && length (attrLine firstParaReport) <= 2 * width ->-             ( (EM.empty, [])  -- already shown in full in @hdrShortened@+             ( emptyOKX  -- already shown in full in @hdrShortened@              , hdrShortened )            | otherwise -> case lX0 of                [] ->
engine-src/Game/LambdaHack/Client/UI/SlideshowM.hs view
@@ -2,7 +2,12 @@ module Game.LambdaHack.Client.UI.SlideshowM   ( overlayToSlideshow, reportToSlideshow, reportToSlideshowKeepHalt   , displaySpaceEsc, displayMore, displayMoreKeep, displayYesNo, getConfirms-  , displayChoiceScreen+  , displayChoiceScreen, displayChoiceScreenWithRightPane+#ifdef EXPOSE_INTERNAL+    -- * Internal operations+  , getMenuIx, saveMenuIx, stepChoiceScreen, navigationKeys, findKYX+  , drawHighlight+#endif   ) where  import Prelude ()@@ -14,12 +19,10 @@ import qualified Data.Map.Strict as M import qualified Data.Text as T -import           Game.LambdaHack.Client.MonadClient import           Game.LambdaHack.Client.UI.Content.Screen import           Game.LambdaHack.Client.UI.ContentClientUI import           Game.LambdaHack.Client.UI.Frame import           Game.LambdaHack.Client.UI.FrameM-import           Game.LambdaHack.Client.UI.ItemSlot import qualified Game.LambdaHack.Client.UI.Key as K import           Game.LambdaHack.Client.UI.MonadClientUI import           Game.LambdaHack.Client.UI.Msg@@ -36,19 +39,18 @@ overlayToSlideshow :: MonadClientUI m                    => Int -> [K.KM] -> OKX -> m Slideshow overlayToSlideshow y keys okx = do-  CCUI{coscreen=ScreenContent{rwidth, rwrap}} <- getsSession sccui-  UIOptions{uScreen1PerLine} <- getsSession sUIOptions+  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui+  UIOptions{uMsgWrapColumn} <- getsSession sUIOptions   report <- getReportUI True   recordHistory  -- report will be shown soon, remove it to history   fontSetup <- getFontSetup-  return $! splitOverlay fontSetup uScreen1PerLine rwidth y rwrap-                         report keys okx+  return $! splitOverlay fontSetup rwidth y uMsgWrapColumn report keys okx  -- | Split current report into a slideshow. reportToSlideshow :: MonadClientUI m => [K.KM] -> m Slideshow reportToSlideshow keys = do   CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui-  overlayToSlideshow (rheight - 2) keys (EM.empty, [])+  overlayToSlideshow (rheight - 2) keys emptyOKX  -- | Split current report into a slideshow. Keep report unchanged. -- Assume the game either halts waiting for a key after this is shown,@@ -58,19 +60,18 @@ -- if important). Unless the prompts interrupt the macro, which is as well. reportToSlideshowKeepHalt :: MonadClientUI m => Bool -> [K.KM] -> m Slideshow reportToSlideshowKeepHalt insideMenu keys = do-  CCUI{coscreen=ScreenContent{rwidth, rheight, rwrap}} <- getsSession sccui-  UIOptions{uScreen1PerLine} <- getsSession sUIOptions+  CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui+  UIOptions{uMsgWrapColumn} <- getsSession sUIOptions   report <- getReportUI insideMenu   -- Don't do @recordHistory@; the message is important, but related   -- to the messages that come after, so should be shown together.   fontSetup <- getFontSetup-  return $! splitOverlay fontSetup uScreen1PerLine rwidth (rheight - 2) rwrap-                         report keys (EM.empty, [])+  return $! splitOverlay fontSetup rwidth (rheight - 2) uMsgWrapColumn+                         report keys emptyOKX  -- | Display a message. Return value indicates if the player wants to continue. -- Feature: if many pages, only the last SPACE exits (but first ESC).-displaySpaceEsc :: (MonadClient m, MonadClientUI m)-                => ColorMode -> Text -> m Bool+displaySpaceEsc :: MonadClientUI m => ColorMode -> Text -> m Bool displaySpaceEsc dm prompt = do   unless (T.null prompt) $ msgLnAdd MsgPromptGeneric prompt   -- Two frames drawn total (unless @prompt@ very long).@@ -80,13 +81,13 @@  -- | Display a message. Ignore keypresses. -- Feature: if many pages, only the last SPACE exits (but first ESC).-displayMore :: (MonadClient m, MonadClientUI m) => ColorMode -> Text -> m ()+displayMore :: MonadClientUI m => ColorMode -> Text -> m () displayMore dm prompt = do   unless (T.null prompt) $ msgLnAdd MsgPromptGeneric prompt   slides <- reportToSlideshow [K.spaceKM]   void $ getConfirms dm [K.spaceKM, K.escKM] slides -displayMoreKeep :: (MonadClient m, MonadClientUI m) => ColorMode -> Text -> m ()+displayMoreKeep :: MonadClientUI m => ColorMode -> Text -> m () displayMoreKeep dm prompt = do   unless (T.null prompt) $ msgLnAdd MsgPromptGeneric prompt   slides <- reportToSlideshowKeepHalt True [K.spaceKM]@@ -94,7 +95,7 @@  -- | Print a yes/no question and return the player's answer. Use black -- and white colours to turn player's attention to the choice.-displayYesNo :: (MonadClient m, MonadClientUI m) => ColorMode -> Text -> m Bool+displayYesNo :: MonadClientUI m => ColorMode -> Text -> m Bool displayYesNo dm prompt = do   unless (T.null prompt) $ msgLnAdd MsgPromptGeneric prompt   let yn = map K.mkChar ['y', 'n']@@ -102,99 +103,169 @@   km <- getConfirms dm (K.escKM : yn) slides   return $! km == K.mkChar 'y' -getConfirms :: (MonadClient m, MonadClientUI m)+getConfirms :: MonadClientUI m             => ColorMode -> [K.KM] -> Slideshow -> m K.KM getConfirms dm extraKeys slides = do   ekm <- displayChoiceScreen "" dm False slides extraKeys   return $! either id (error $ "" `showFailure` ekm) ekm  -- | Display a, potentially, multi-screen menu and return the chosen--- key or item slot label (and the index in the whole menu so that the cursor+-- key or item slot label (and save the index in the whole menu so that the cursor -- can again be placed at that spot next time menu is displayed). ----- This function is the only source of menus and so, effectively, UI modes.-displayChoiceScreen :: forall m . (MonadClient m, MonadClientUI m)+-- This function is one of only two sources of menus and so,+-- effectively, UI modes.+displayChoiceScreen :: forall m . MonadClientUI m                     => String -> ColorMode -> Bool -> Slideshow -> [K.KM]-                    -> m (Either K.KM SlotChar)-displayChoiceScreen menuName dm sfBlank frsX extraKeys = do-  let frs = slideshow frsX-      keys = concatMap (concatMap (fromLeft [] . fst) . snd) frs-             ++ extraKeys-      !_A = assert (K.escKM `elem` extraKeys) ()-      navigationKeys = [ K.leftButtonReleaseKM, K.rightButtonReleaseKM-                       , K.returnKM, K.spaceKM-                       , K.upKM, K.leftKM, K.downKM, K.rightKM-                       , K.pgupKM, K.pgdnKM, K.wheelNorthKM, K.wheelSouthKM-                       , K.homeKM, K.endKM, K.controlP ]-                       ++ [K.mkChar '?' | menuName == "help"]  -- a hack-      legalKeys = keys ++ navigationKeys-      -- The arguments go from first menu line and menu page to the last,-      -- in order. Their indexing is from 0. We select the nearest item-      -- with the index equal or less to the pointer.-      findKYX :: Int -> [OKX] -> Maybe (OKX, KYX, Int)-      findKYX _ [] = Nothing-      findKYX pointer (okx@(_, kyxs) : frs2) =-        case drop pointer kyxs of-          [] ->  -- not enough menu items on this page-            case findKYX (pointer - length kyxs) frs2 of-              Nothing ->  -- no more menu items in later pages-                case reverse kyxs of-                  [] -> Nothing-                  kyx : _ -> Just (okx, kyx, length kyxs - 1)-              res -> res-          kyx : _ -> Just (okx, kyx, pointer)-      maxIx = length (concatMap snd frs) - 1+                    -> m KeyOrSlot+displayChoiceScreen =+  displayChoiceScreenWithRightPane $ const $ return emptyOKX++-- | Display a, potentially, multi-screen menu and return the chosen+-- key or item slot label (and save the index in the whole menu so that the cursor+-- can again be placed at that spot next time menu is displayed).+-- Additionally, display something on the right half of the screen,+-- depending on which menu item is currently highlighted+--+-- This function is one of only two sources of menus and so,+-- effectively, UI modes.+displayChoiceScreenWithRightPane+  :: forall m . MonadClientUI m+  => (KeyOrSlot -> m OKX)+  -> String -> ColorMode -> Bool -> Slideshow -> [K.KM]+  -> m KeyOrSlot+displayChoiceScreenWithRightPane displayInRightPane+                                 menuName dm sfBlank frsX extraKeys = do+  (maxIx, initIx, clearIx, m) <-+    stepChoiceScreen menuName dm sfBlank frsX extraKeys+  let loop :: Int -> KeyOrSlot -> m (KeyOrSlot, Int)+      loop pointer km = do+        okxRight <- displayInRightPane km+        (final, km1, pointer1) <- m pointer okxRight+        if final+        then return (km1, pointer1)+        else loop pointer1 km1+  pointer0 <- getMenuIx menuName maxIx initIx clearIx+  let km0 = case findKYX pointer0 $ slideshow frsX of+        Nothing -> error $ "no menu keys" `showFailure` frsX+        Just (_, (ekm, _), _) -> ekm+  (km, pointer) <- loop pointer0 km0+  saveMenuIx menuName initIx pointer+  return km++getMenuIx :: MonadClientUI m => String -> Int -> Int -> Int -> m Int+getMenuIx menuName maxIx initIx clearIx = do+  menuIxMap <- getsSession smenuIxMap+  -- Beware, values in @menuIxMap@ may be negative (meaning: a key, not slot).+  let menuIx = if menuName == ""+               then clearIx+               else maybe clearIx (+ initIx) (M.lookup menuName menuIxMap)+                      -- this may still be negative, from different context+  return $! max clearIx $ min maxIx menuIx  -- so clamp to point at item, not key++saveMenuIx :: MonadClientUI m => String -> Int -> Int -> m ()+saveMenuIx menuName initIx pointer =+  unless (menuName == "") $+    modifySession $ \sess ->+      sess {smenuIxMap = M.insert menuName (pointer - initIx) $ smenuIxMap sess}++-- | This is one step of UI menu management user session.+--+-- There is limited looping involved to return a changed position+-- in the menu each time so that the surrounding code has anything+-- interesting to do. The exception is when finally confirming a selection,+-- in which case it's usually not changed compared to last step,+-- but it's presented differently to indicate it was confirmed.+--+-- Any extra keys in the `OKX` argument on top of the those in @Slideshow@+-- argument need to be contained in the @[K.KM]@ argument. Otherwise+-- they are not accepted.+stepChoiceScreen :: forall m . MonadClientUI m+                 => String -> ColorMode -> Bool -> Slideshow -> [K.KM]+                 -> m ( Int, Int, Int+                      , Int -> OKX -> m (Bool, KeyOrSlot, Int) )+stepChoiceScreen menuName dm sfBlank frsX extraKeys = do+  CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui+  FontSetup{..} <- getFontSetup+  let !_A = assert (K.escKM `elem` extraKeys) ()+      frs = slideshow frsX+      keys = concatMap (lefts . map fst . snd) frs ++ extraKeys+      legalKeys = keys+                  ++ navigationKeys+                  ++ (if menuName == "help"+                      then [K.mkChar '?', K.mkKM "F1"]+                      else [])  -- a hack       allOKX = concatMap snd frs+      maxIx = length allOKX - 1       initIx = case findIndex (isRight . fst) allOKX of         Just p -> p         _ -> 0  -- can't be @length allOKX@ or a multi-page item menu                 -- mangles saved index of other item munus       clearIx = if initIx > maxIx then 0 else initIx-      page :: Int -> m (Either K.KM SlotChar, Int)-      page pointer = assert (pointer >= 0) $ case findKYX pointer frs of+      canvasLength = if sfBlank then rheight else rheight - 2+      trimmedY = canvasLength - 1 - 2  -- will be translated down 2 lines+      trimmedAlert = ( PointUI 0 trimmedY+                     , stringToAL "--a portion of the text trimmed--" )+      page :: Int -> OKX -> m (Bool, KeyOrSlot, Int)+      page pointer (ovsRight0, kyxsRight) = assert (pointer >= 0)+                                            $ case findKYX pointer frs of         Nothing -> error $ "no menu keys" `showFailure` frs-        Just ( (ovs, kyxs)-             , (ekm, (PointUI x1 y, ButtonWidth fontX1 len))+        Just ( (ovs0, kyxs1)+             , (ekm, (PointUI x1 y, buttonWidth))              , ixOnPage ) -> do-          let highableAttrs =-                [Color.defAttr, Color.defAttr {Color.fg = Color.BrBlack}]-              highAttr x | Color.acAttr x `notElem` highableAttrs-                           || Color.acChar x == ' ' = x-              highAttr x = x {Color.acAttr =-                                (Color.acAttr x) {Color.fg = Color.BrWhite}}-              cursorAttr x = x {Color.acAttr =-                                  (Color.acAttr x)-                                    {Color.bg = Color.HighlightNoneCursor}}-              -- This also highlights dull white item symbols, but who cares.-              lenUI = if isSquareFont fontX1 then len * 2 else len-              drawHighlight xstart xs | x1 + lenUI < xstart = xs-                                      | otherwise =-                let x1MinusXStartChars = if isSquareFont fontX1-                                         then (x1 - xstart) `div` 2-                                         else x1 - xstart-                    (xs1, xsRest) = splitAt x1MinusXStartChars xs-                    (xs2, xs3) = splitAt len xsRest-                    highW32 = Color.attrCharToW32-                              . highAttr-                              . Color.attrCharFromW32-                    cursorW32 = Color.attrCharToW32-                                . cursorAttr-                                . Color.attrCharFromW32-                    xs2High = case map highW32 xs2 of-                      [] -> []-                      xh : xhrest -> cursorW32 xh : xhrest-                in xs1 ++ xs2High ++ xs3-              ovs1 = EM.map (updateLine y drawHighlight) ovs-              ignoreKey = page pointer+          let ovs1 = EM.map (updateLine y $ drawHighlight x1 buttonWidth) ovs0+              -- We add spaces in proportional font under the report rendered+              -- in mono font and the right pane text in prop font,+              -- but over menu lines in proportional font that can be+              -- very long an should not peek from under the right pane text.+              --+              -- We translate the pane by two characters right, because it looks+              -- better when a couple last characters of a line vanish+              -- off-screen than when characters touch in the middle+              -- of the screen. The code producing right panes should take care+              -- to generate lines two shorter than usually.+              --+              -- We move the pane two characters down, because normally+              -- reports should not be longer than three lines+              -- and the third no longer than half width.+              -- We also add two to three lines of backdrop at the bottom.+              ymax = maxYofFontOverlayMap ovsRight0+              -- Apparently prop spaces can be really narrow, hence so many.+              -- With square font, this obscures the link in main menu,+              -- so would need to complicated.+              spaceRectangle | isSquareFont propFont = []+                             | otherwise =+                                 rectangleOfSpaces (rwidth * 4)+                                                   (min canvasLength $ ymax + 5)+              trim = filter (\(PointUI _ yRight, _) -> yRight < trimmedY)+              -- The alert not clickable, because the player can enter+              -- the menu entry and scroll through the unabridged blurb.+              ovsRight1 = if ymax <= trimmedY+                          then ovsRight0+                          else EM.unionWith (++)+                                 (EM.map trim ovsRight0)+                                 (EM.singleton monoFont [trimmedAlert])+              ovsRight = EM.unionWith (++)+                           (EM.singleton propFont spaceRectangle)+                           (EM.map (xytranslateOverlay 2 2) ovsRight1)+              (ovs, kyxs) =+                if EM.null ovsRight0+                then (ovs1, kyxs1)+                else sideBySideOKX rwidth 0 (ovs1, kyxs1) (ovsRight, kyxsRight)+              tmpResult pointer1 = case findKYX pointer1 frs of+                Nothing -> error $ "no menu keys" `showFailure` frs+                Just (_, (ekm1, _), _) -> return (False, ekm1, pointer1)+              ignoreKey = return (False, ekm, pointer)               pageLen = length kyxs               xix :: KYX -> Bool               xix (_, (PointUI x1' _, _)) = x1' <= x1 + 2 && x1' >= x1 - 2               firstRowOfNextPage = pointer + pageLen - ixOnPage               restOKX = drop firstRowOfNextPage allOKX+              -- This does not take into account the right pane, which is fine.               firstItemOfNextPage = case findIndex (isRight . fst) restOKX of                 Just p -> p + firstRowOfNextPage                 _ -> firstRowOfNextPage-              interpretKey :: K.KM -> m (Either K.KM SlotChar, Int)+              interpretKey :: K.KM -> m (Bool, KeyOrSlot, Int)               interpretKey ikm =                 case K.key ikm of                   _ | ikm == K.controlP -> do@@ -202,12 +273,11 @@                     printScreen                     ignoreKey                   K.Return -> case ekm of-                    Left (km : _) ->+                    Left km ->                       if K.key km == K.Return && km `elem` keys-                      then return (Left km, pointer)+                      then return (True, Left km, pointer)                       else interpretKey km-                    Left [] -> error $ "" `showFailure` ikm-                    Right c -> return (Right c, pointer)+                    Right c -> return (True, Right c, pointer)                   K.LeftButtonRelease -> do                     PointUI mx my <- getsSession spointer                     let onChoice (_, (PointUI cx cy, ButtonWidth font clen)) =@@ -215,68 +285,123 @@                                    | otherwise = clen                           in my == cy && mx >= cx && mx < cx + blen                     case find onChoice kyxs of-                      Nothing | ikm `elem` keys -> return (Left ikm, pointer)+                      Nothing | ikm `elem` keys ->+                        return (True, Left ikm, pointer)                       Nothing -> if K.spaceKM `elem` keys-                                 then return (Left K.spaceKM, pointer)+                                 then return (True, Left K.spaceKM, pointer)                                  else ignoreKey                       Just (ckm, _) -> case ckm of-                        Left (km : _) ->+                        Left km ->                           if K.key km == K.Return && km `elem` keys-                          then return (Left km, pointer)+                          then return (True, Left km, pointer)                           else interpretKey km-                        Left [] -> error $ "" `showFailure` ikm-                        Right c  -> return (Right c, pointer)+                        Right c  -> return (True, Right c, pointer)                   K.RightButtonRelease ->-                    if | ikm `elem` keys -> return (Left ikm, pointer)-                       | K.escKM `elem` keys -> return (Left K.escKM, pointer)+                    if | ikm `elem` keys -> return (True, Left ikm, pointer)+                       | K.escKM `elem` keys ->+                           return (True, Left K.escKM, pointer)                        | otherwise -> ignoreKey                   K.Space | firstItemOfNextPage <= maxIx ->-                    page firstItemOfNextPage-                  K.Char '?' | firstItemOfNextPage <= maxIx-                               && menuName == "help" ->  -- a hack-                    page firstItemOfNextPage+                    tmpResult firstItemOfNextPage+                  _ | K.key ikm `elem` [K.Char '?', K.Fun 1]+                      && firstItemOfNextPage <= maxIx+                      && menuName == "help" ->  -- a hack+                    tmpResult firstItemOfNextPage                   K.Unknown "SAFE_SPACE" ->                     if firstItemOfNextPage <= maxIx-                    then page firstItemOfNextPage-                    else page clearIx+                    then tmpResult firstItemOfNextPage+                    else tmpResult clearIx                   _ | ikm `elem` keys ->-                    return (Left ikm, pointer)-                  K.Up -> case findIndex xix $ reverse $ take ixOnPage kyxs of-                    Nothing -> interpretKey ikm{K.key=K.Left}-                    Just ix -> page (max 0 (pointer - ix - 1))-                  K.Left -> if pointer == 0 then page maxIx-                            else page (max 0 (pointer - 1))-                  K.Down -> case findIndex xix $ drop (ixOnPage + 1) kyxs of-                    Nothing -> interpretKey ikm{K.key=K.Right}-                    Just ix -> page (pointer + ix + 1)-                  K.Right -> if pointer == maxIx then page 0-                             else page (min maxIx (pointer + 1))-                  K.Home -> page clearIx-                  K.End -> page maxIx-                  _ | K.key ikm `elem` [K.PgUp, K.WheelNorth] ->-                    page (max 0 (pointer - ixOnPage - 1))-                  _ | K.key ikm `elem` [K.PgDn, K.WheelSouth] ->+                    return (True, Left ikm, pointer)+                  _ | K.key ikm `elem` [K.Up, K.WheelNorth] ->+                    case findIndex xix $ reverse $ take ixOnPage kyxs of+                      Nothing -> interpretKey ikm{K.key=K.Left}+                      Just ix -> tmpResult (max 0 (pointer - ix - 1))+                  _ | K.key ikm `elem` [K.Down, K.WheelSouth] ->+                    case findIndex xix $ drop (ixOnPage + 1) kyxs of+                      Nothing -> interpretKey ikm{K.key=K.Right}+                      Just ix -> tmpResult (pointer + ix + 1)+                  K.Left -> if pointer == 0 then tmpResult maxIx+                            else tmpResult (max 0 (pointer - 1))+                  K.Right -> if pointer == maxIx then tmpResult 0+                             else tmpResult (min maxIx (pointer + 1))+                  K.Home -> tmpResult clearIx+                  K.End -> tmpResult maxIx+                  K.PgUp ->+                    tmpResult (max 0 (pointer - ixOnPage - 1))+                  K.PgDn ->                     -- This doesn't scroll by screenful when header very long                     -- and menu non-empty, but that scenario is rare, so OK,                     -- arrow keys may be used instead.-                    page (min maxIx firstItemOfNextPage)+                    tmpResult (min maxIx firstItemOfNextPage)                   K.Space -> if pointer == maxIx-                             then page clearIx-                             else page maxIx+                             then tmpResult clearIx+                             else tmpResult maxIx                   _ -> error $ "unknown key" `showFailure` ikm-          pkm <- promptGetKey dm ovs1 sfBlank legalKeys+          pkm <- promptGetKey dm ovs sfBlank legalKeys           interpretKey pkm-  menuIxMap <- getsSession smenuIxMap-  -- Beware, values in @menuIxMap@ may be negative (meaning: a key, not slot).-  let menuIx | menuName == "" = clearIx-             | otherwise =-               maybe clearIx (+ initIx) (M.lookup menuName menuIxMap)-                 -- this may be negative, from different context-  (km, pointer) <- if null frs-                   then return (Left K.escKM, menuIx)-                   else page $ max clearIx $ min maxIx menuIx-                          -- the saved index could be from different context-  unless (menuName == "") $-    modifySession $ \sess ->-      sess {smenuIxMap = M.insert menuName (pointer - initIx) menuIxMap}-  assert (either (`elem` keys) (const True) km) $ return km+      m pointer okxRight =+        if null frs+        then return (True, Left K.escKM, pointer)+        else do+          (final, km, pointer1) <- page pointer okxRight+          let !_A1 = assert (either (`elem` keys) (const True) km) ()+          -- Pointer at a button included, hence greater than 0, not @clearIx@.+          let !_A2 = assert (0 <= pointer1 && pointer1 <= maxIx+                             `blame`  (pointer1, maxIx)) ()+          return (final, km, pointer1)+  return (maxIx, initIx, clearIx, m)++navigationKeys :: [K.KM]+navigationKeys = [ K.leftButtonReleaseKM, K.rightButtonReleaseKM+                 , K.returnKM, K.spaceKM+                 , K.upKM, K.downKM, K.wheelNorthKM, K.wheelSouthKM+                 , K.leftKM, K.rightKM, K.pgupKM, K.pgdnKM+                 , K.homeKM, K.endKM, K.controlP ]++-- | Find a position in a menu.+-- The arguments go from first menu line and menu page to the last,+-- in order. Their indexing is from 0. We select the nearest item+-- with the index equal or less to the pointer.+findKYX :: Int -> [OKX] -> Maybe (OKX, KYX, Int)+findKYX _ [] = Nothing+findKYX pointer (okx@(_, kyxs) : frs2) =+  case drop pointer kyxs of+    [] ->  -- not enough menu items on this page+      case findKYX (pointer - length kyxs) frs2 of+        Nothing ->  -- no more menu items in later pages+          case reverse kyxs of+            [] -> Nothing+            kyx : _ -> Just (okx, kyx, length kyxs - 1)+        res -> res+    kyx : _ -> Just (okx, kyx, pointer)++drawHighlight :: Int -> ButtonWidth -> Int -> AttrString -> AttrString+drawHighlight x1 (ButtonWidth font len) xstart as =+  let highableAttrs = [Color.defAttr, Color.defAttr {Color.fg = Color.BrBlack}]+      highAttr c | Color.acAttr c `notElem` highableAttrs+                   || Color.acChar c == ' ' = c+      highAttr c = c {Color.acAttr =+                        (Color.acAttr c) {Color.fg = Color.BrWhite}}+      cursorAttr c = c {Color.acAttr =+                          (Color.acAttr c)+                            {Color.bg = Color.HighlightNoneCursor}}+      -- This also highlights dull white item symbols, but who cares.+      lenUI = if isSquareFont font then len * 2 else len+      x1MinusXStartChars = if isSquareFont font+                           then (x1 - xstart) `div` 2+                           else x1 - xstart+      (as1, asRest) = splitAt x1MinusXStartChars as+      (as2, as3) = splitAt len asRest+      highW32 = Color.attrCharToW32+                . highAttr+                . Color.attrCharFromW32+      cursorW32 = Color.attrCharToW32+                  . cursorAttr+                  . Color.attrCharFromW32+      as2High = case map highW32 as2 of+        [] -> []+        ch : chrest -> cursorW32 ch : chrest+  in if x1 + lenUI < xstart+     then as+     else as1 ++ as2High ++ as3
engine-src/Game/LambdaHack/Client/UI/UIOptions.hs view
@@ -17,33 +17,27 @@ import           Game.LambdaHack.Common.ClientOptions (FullscreenMode) import           Game.LambdaHack.Common.Misc import qualified Game.LambdaHack.Definition.Color as Color+import           Game.LambdaHack.Definition.Defs --- | Options that affect the UI of the client, specified in the config file. More documentation is in the default config file.+-- | Options that affect the UI of the client, specified in the config file.+-- More documentation is in the default config file. data UIOptions = UIOptions-  { -- commands-    uCommands         :: [(K.KM, CmdTriple)]-    -- hero names+  { uCommands         :: [(K.KM, CmdTriple)]   , uHeroNames        :: [(Int, (Text, Text))]-    -- ui   , uVi               :: Bool   , uLeftHand         :: Bool   , uChosenFontset    :: Text   , uAllFontsScale    :: Double-  , uScreen1PerLine   :: Bool-  , uHistory1PerLine  :: Bool+  , uFullscreenMode   :: FullscreenMode+  , uhpWarningPercent :: Int+  , uMsgWrapColumn    :: X   , uHistoryMax       :: Int   , uMaxFps           :: Double   , uNoAnim           :: Bool-  , uhpWarningPercent :: Int-      -- ^ HP percent at which warning is emitted.-  , uMessageColors    :: [(String, Color.Color)]-      -- ^ Prefixes of message class constructor names paired with colors.-      --   The first prefix that matches, wins.-  , uCmdline          :: [String]-      -- ^ Hardwired commandline arguments to process.+  , uOverrideCmdline  :: [String]   , uFonts            :: [(Text, FontDefinition)]   , uFontsets         :: [(Text, FontSet)]-  , uFullscreenMode   :: FullscreenMode+  , uMessageColors    :: [(String, Color.Color)]   }   deriving (Show, Generic) 
engine-src/Game/LambdaHack/Client/UI/UIOptionsParse.hs view
@@ -76,15 +76,13 @@       uLeftHand = getOption "movementLeftHandKeys_axwdqezc"       uChosenFontset = getOption "chosenFontset"       uAllFontsScale = getOption "allFontsScale"-      uScreen1PerLine = getOption "screenOneMessagePerLine"-      uHistory1PerLine = getOption "historyOneMessagePerLine"+      uFullscreenMode = getOption "fullscreenMode"+      uhpWarningPercent = getOption "hpWarningPercent"+      uMsgWrapColumn = getOption "msgWrapColumn"       uHistoryMax = getOption "historyMax"       uMaxFps = max 1 $ getOption "maxFps"       uNoAnim = getOption "noAnim"-      uhpWarningPercent = getOption "hpWarningPercent"-      uMessageColors =-        map (second readError) $ Ini.allItems "message_colors" cfg-      uCmdline = glueSeed $ words $ getOption "overrideCmdline"+      uOverrideCmdline = glueSeed $ words $ getOption "overrideCmdline"       uFonts =         let toFont (ident, fontString) = (T.pack ident, readError fontString)             section = Ini.allItems "fonts" cfg@@ -94,7 +92,8 @@               (T.pack ident, readError fontSetString)             section = Ini.allItems "fontsets" cfg         in map toFontSet section-      uFullscreenMode = getOption "fullscreenMode"+      uMessageColors =+        map (second readError) $ Ini.allItems "message_colors" cfg   in UIOptions{..}  glueSeed :: [String] -> [String]@@ -159,8 +158,6 @@         schosenFontset opts `mplus` Just (uChosenFontset uioptions)}) .      (\opts -> opts {sallFontsScale =         sallFontsScale opts `mplus` Just (uAllFontsScale uioptions)}) .-     (\opts -> opts {sfonts = uFonts uioptions}) .-     (\opts -> opts {sfontsets = uFontsets uioptions}) .      (\opts -> opts {sfullscreenMode =         sfullscreenMode opts `mplus` Just (uFullscreenMode uioptions)}) .      (\opts -> opts {smaxFps =@@ -168,4 +165,7 @@      (\opts -> opts {snoAnim =         snoAnim opts `mplus` Just (uNoAnim uioptions)}) .      (\opts -> opts {stitle =-        stitle opts `mplus` Just (rtitle corule)})+        stitle opts `mplus` Just (rtitle corule)}) .+     (\opts -> opts {sfonts = uFonts uioptions}) .+     (\opts -> opts {sfontsets = uFontsets uioptions}) .+     id
+ engine-src/Game/LambdaHack/Client/UI/Watch.hs view
@@ -0,0 +1,9 @@+-- | Display atomic commands received by the client.+module Game.LambdaHack.Client.UI.Watch+  ( watchRespUpdAtomicUI, watchRespSfxAtomicUI+  ) where++import Prelude ()++import Game.LambdaHack.Client.UI.Watch.WatchSfxAtomicM+import Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM
+ engine-src/Game/LambdaHack/Client/UI/Watch/WatchCommonM.hs view
@@ -0,0 +1,302 @@+-- | Common code for displaying atomic update and SFX commands.+module Game.LambdaHack.Client.UI.Watch.WatchCommonM+  ( pushFrame, pushReportFrame, fadeOutOrIn, markDisplayNeeded+  , lookAtMove, stopAtMove+  , aidVerbMU, aidVerbDuplicateMU, itemVerbMUGeneral, itemVerbMU+  , itemVerbMUShort, itemAidVerbMU, mitemAidVerbMU, itemAidDistinctMU+  , manyItemsAidVerbMU+#ifdef EXPOSE_INTERNAL+    -- * Internal operations+  , basicFrameWithoutReport+#endif+  ) where++import Prelude ()++import Game.LambdaHack.Core.Prelude++import qualified Data.EnumMap.Strict as EM+import qualified Data.Text as T+import qualified NLP.Miniutter.English as MU++import           Game.LambdaHack.Client.MonadClient+import           Game.LambdaHack.Client.State+import           Game.LambdaHack.Client.UI.Animation+import           Game.LambdaHack.Client.UI.Content.Screen+import           Game.LambdaHack.Client.UI.ContentClientUI+import           Game.LambdaHack.Client.UI.Frame+import           Game.LambdaHack.Client.UI.FrameM+import           Game.LambdaHack.Client.UI.HandleHelperM+import           Game.LambdaHack.Client.UI.ItemDescription+import           Game.LambdaHack.Client.UI.MonadClientUI+import           Game.LambdaHack.Client.UI.Msg+import           Game.LambdaHack.Client.UI.MsgM+import           Game.LambdaHack.Client.UI.Overlay+import           Game.LambdaHack.Client.UI.PointUI+import           Game.LambdaHack.Client.UI.SessionUI+import           Game.LambdaHack.Client.UI.Slideshow+import           Game.LambdaHack.Client.UI.SlideshowM+import           Game.LambdaHack.Common.Actor+import           Game.LambdaHack.Common.ActorState+import           Game.LambdaHack.Common.ClientOptions+import           Game.LambdaHack.Common.Faction+import           Game.LambdaHack.Common.Item+import qualified Game.LambdaHack.Common.ItemAspect as IA+import           Game.LambdaHack.Common.Misc+import           Game.LambdaHack.Common.MonadStateRead+import           Game.LambdaHack.Common.State+import           Game.LambdaHack.Common.Types+import qualified Game.LambdaHack.Definition.Ability as Ability++-- This is not our turn, so we can't obstruct screen with messages+-- and message reformatting causes distraction, so there's no point+-- trying to squeeze the report into the single available line,+-- except when it's not our turn permanently, because AI runs UI.+--+-- The only real drawback of this is that when resting for longer time+-- I can't see the boring messages accumulate until a non-boring interrupts me.+basicFrameWithoutReport :: MonadClientUI m+                        => LevelId -> Maybe Bool -> m PreFrame3+basicFrameWithoutReport arena forceReport = do+  FontSetup{propFont} <- getFontSetup+  sbenchMessages <- getsClient $ sbenchMessages . soptions+  side <- getsClient sside+  fact <- getsState $ (EM.! side) . sfactionD+  let underAI = isAIFact fact+  truncRep <-+    if | sbenchMessages -> do+         slides <- reportToSlideshowKeepHalt False []+         case slideshow slides of+           [] -> return EM.empty+           (ov, _) : _ -> do+             -- See @stepQueryUI@. This strips either "--end-" or "--more-".+             let ovProp = ov EM.! propFont+             return $!+               EM.singleton propFont+               $ if EM.size ov > 1 then ovProp else init ovProp+       | fromMaybe underAI forceReport -> do+         report <- getReportUI False+         let par1 = firstParagraph $ foldr (<+:>) [] $ renderReport True report+         return $! EM.fromList [(propFont, [(PointUI 0 0, par1)])]+       | otherwise -> return EM.empty+  drawOverlay ColorFull False truncRep arena++-- | Push the frame depicting the current level to the frame queue.+-- Only one line of the report is shown, as in animations,+-- because it may not be our turn, so we can't clear the message+-- to see what is underneath.+pushFrame :: MonadClientUI m => Bool -> m ()+pushFrame delay = do+  -- The delay before reaction to keypress was too long in case of many+  -- projectiles flying and ending flight, so frames need to be skipped.+  keyPressed <- anyKeyPressed+  unless keyPressed $ do+    lidV <- viewedLevelUI+    frame <- basicFrameWithoutReport lidV Nothing+    -- Pad with delay before and after to let player see, e.g., door being+    -- opened a few ticks after it came into vision, the same turn.+    displayFrames lidV $+      if delay then [Nothing, Just frame, Nothing] else [Just frame]++pushReportFrame :: MonadClientUI m => m ()+pushReportFrame = do+  lidV <- viewedLevelUI+  frame <- basicFrameWithoutReport lidV (Just True)+  displayFrames lidV [Just frame]++fadeOutOrIn :: MonadClientUI m => Bool -> m ()+fadeOutOrIn out = do+  arena <- getArenaUI+  CCUI{coscreen} <- getsSession sccui+  animMap <- rndToActionUI $ fadeout coscreen out 2+  animFrs <- renderAnimFrames arena animMap (Just False)+  displayFrames arena (tail animFrs)  -- no basic frame between fadeout and in++markDisplayNeeded :: MonadClientUI m => LevelId -> m ()+markDisplayNeeded lid = do+  lidV <- viewedLevelUI+  when (lidV == lid) $ modifySession $ \sess -> sess {sdisplayNeeded = True}++lookAtMove :: MonadClientUI m => ActorId -> m ()+lookAtMove aid = do+  mleader <- getsClient sleader+  body <- getsState $ getActorBody aid+  side <- getsClient sside+  aimMode <- getsSession saimMode+  when (not (bproj body)+        && bfid body == side+        && isNothing aimMode) $ do  -- aiming does a more extensive look+    stashBlurb <- lookAtStash (bpos body) (blid body)+    (itemsBlurb, _) <-+      lookAtItems True (bpos body) (blid body) (Just aid) Nothing+    let msgClass = if Just aid == mleader+                   then MsgAtFeetMajor+                   else MsgAtFeetMinor+        blurb = stashBlurb <+> itemsBlurb+    unless (T.null blurb) $+      msgAdd msgClass blurb++stopAtMove :: MonadClientUI m => ActorId -> m ()+stopAtMove aid = do+  body <- getsState $ getActorBody aid+  side <- getsClient sside+  fact <- getsState $ (EM.! bfid body) . sfactionD+  adjBigAssocs <- getsState $ adjacentBigAssocs body+  adjProjAssocs <- getsState $ adjacentProjAssocs body+  if not (bproj body) && bfid body == side then do+    let foe (_, b2) = isFoe (bfid body) fact (bfid b2)+        adjFoes = filter foe $ adjBigAssocs ++ adjProjAssocs+    unless (null adjFoes) stopPlayBack+  else when (isFoe (bfid body) fact side) $ do+    let our (_, b2) = bfid b2 == side+        adjOur = filter our adjBigAssocs+    unless (null adjOur) stopPlayBack++aidVerbMU :: (MonadClientUI m, MsgShared a) => a -> ActorId -> MU.Part -> m ()+aidVerbMU msgClass aid verb = do+  subject <- partActorLeader aid+  msgAdd msgClass $ makeSentence [MU.SubjectVerbSg subject verb]++aidVerbDuplicateMU :: (MonadClientUI m, MsgShared a)+                   => a -> ActorId -> MU.Part -> m Bool+aidVerbDuplicateMU msgClass aid verb = do+  subject <- partActorLeader aid+  msgAddDuplicate msgClass (makeSentence [MU.SubjectVerbSg subject verb])++itemVerbMUGeneral :: MonadClientUI m+                  => Bool -> ItemId -> ItemQuant -> MU.Part -> Container+                  -> m Text+itemVerbMUGeneral verbose iid kit@(k, _) verb c = assert (k > 0) $ do+  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui+  lid <- getsState $ lidFromC c+  localTime <- getsState $ getLocalTime lid+  itemFull <- getsState $ itemToFull iid+  side <- getsClient sside+  factionD <- getsState sfactionD+  let arItem = aspectRecordFull itemFull+      partItemWsChosen | verbose = partItemWs+                       | otherwise = partItemWsShort+      subject = partItemWsChosen rwidth side factionD k localTime itemFull kit+      msg | k > 1 && not (IA.checkFlag Ability.Condition arItem) =+              makeSentence [MU.SubjectVerb MU.PlEtc MU.Yes subject verb]+          | otherwise = makeSentence [MU.SubjectVerbSg subject verb]+  return $! msg++itemVerbMU :: (MonadClientUI m, MsgShared a)+           => a -> ItemId -> ItemQuant -> MU.Part -> Container -> m ()+itemVerbMU msgClass iid kit verb c = do+  msg <- itemVerbMUGeneral True iid kit verb c+  msgAdd msgClass msg++itemVerbMUShort :: (MonadClientUI m, MsgShared a)+                => a -> ItemId -> ItemQuant -> MU.Part -> Container+                -> m ()+itemVerbMUShort msgClass iid kit verb c = do+  msg <- itemVerbMUGeneral False iid kit verb c+  msgAdd msgClass msg++itemAidVerbMU :: (MonadClientUI m, MsgShared a)+              => a -> ActorId -> MU.Part -> ItemId -> Either Int Int+              -> m ()+itemAidVerbMU msgClass aid verb iid ek = do+  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui+  body <- getsState $ getActorBody aid+  side <- getsClient sside+  factionD <- getsState sfactionD+  let lid = blid body+      fakeKit = quantSingle+  localTime <- getsState $ getLocalTime lid+  subject <- partActorLeader aid+  -- The item may no longer be in @c@, but it was.+  itemFull <- getsState $ itemToFull iid+  let object = case ek of+        Left n ->+          partItemWs rwidth side factionD n localTime itemFull fakeKit+        Right n ->+          let (name1, powers) =+                partItemShort rwidth side factionD localTime itemFull fakeKit+          in MU.Phrase ["the", MU.Car1Ws n name1, powers]+      msg = makeSentence [MU.SubjectVerbSg subject verb, object]+  msgAdd msgClass msg++mitemAidVerbMU :: (MonadClientUI m, MsgShared a)+               => a -> ActorId -> MU.Part -> ItemId -> Maybe MU.Part+               -> m ()+mitemAidVerbMU msgClass aid verb iid msuffix = do+  itemD <- getsState sitemD+  case msuffix of+    Just suffix | iid `EM.member` itemD ->+      itemAidVerbMU msgClass aid (MU.Phrase [verb, suffix]) iid (Right 1)+    _ -> do+#ifdef WITH_EXPENSIVE_ASSERTIONS+      side <- getsClient sside+      b <- getsState $ getActorBody aid+      bUI <- getsSession $ getActorUI aid+      -- 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.+      assert (isNothing msuffix  -- item description not requested+              || bfid b /= side  -- not from affected faction; only observing+              `blame` "item never seen by the affected actor"+              `swith` (aid, b, bUI, verb, iid, msuffix)) $+#endif+        aidVerbMU msgClass aid verb++itemAidDistinctMU :: MonadClientUI m+                  => MsgClassDistinct -> ActorId -> MU.Part -> MU.Part -> ItemId+                  -> m ()+itemAidDistinctMU msgClass aid verbShow verbSave iid = do+  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui+  body <- getsState $ getActorBody aid+  side <- getsClient sside+  factionD <- getsState sfactionD+  let lid = blid body+      fakeKit = quantSingle+  localTime <- getsState $ getLocalTime lid+  subject <- partActorLeader aid+  -- The item may no longer be in @c@, but it was.+  itemFull <- getsState $ itemToFull iid+  let object = let (name, powers) =+                     partItem rwidth side factionD localTime itemFull fakeKit+               in MU.Phrase [name, powers]+      t1 = makeSentence [MU.SubjectVerbSg subject verbShow, object]+      t2 = makeSentence [MU.SubjectVerbSg subject verbSave, object]+      dotsIfShorter = if t1 == t2 then "" else ".."+  msgAddDistinct msgClass (t1 <> dotsIfShorter, t2)++manyItemsAidVerbMU :: (MonadClientUI m, MsgShared a)+                   => a -> ActorId -> MU.Part+                   -> [(ItemId, ItemQuant)] -> (Int -> Either (Maybe Int) Int)+                   -> m ()+manyItemsAidVerbMU msgClass aid verb sortedAssocs ekf = do+  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui+  body <- getsState $ getActorBody aid+  side <- getsClient sside+  factionD <- getsState sfactionD+  let lid = blid body+      fakeKit = quantSingle+  localTime <- getsState $ getLocalTime lid+  subject <- partActorLeader aid+  -- The item may no longer be in @c@, but it was.+  itemToF <- getsState $ flip itemToFull+  let object (iid, (k, _)) =+        let itemFull = itemToF iid+        in case ekf k of+          Left (Just n) ->+            partItemWs rwidth side factionD n localTime itemFull fakeKit+          Left Nothing ->+            let (name, powers) =+                  partItem rwidth side factionD localTime itemFull fakeKit+            in MU.Phrase [name, powers]+          Right n ->+            let (name1, powers) =+                  partItemShort rwidth side factionD localTime itemFull fakeKit+            in MU.Phrase ["the", MU.Car1Ws n name1, powers]+      msg = makeSentence [ MU.SubjectVerbSg subject verb+                         , MU.WWandW $ map object sortedAssocs]+  msgAdd msgClass msg
+ engine-src/Game/LambdaHack/Client/UI/Watch/WatchQuitM.hs view
@@ -0,0 +1,365 @@+{-# LANGUAGE TupleSections #-}+-- | Display all the initial (not including high scores) screens at game over.+module Game.LambdaHack.Client.UI.Watch.WatchQuitM+  ( quitFactionUI+#ifdef EXPOSE_INTERNAL+    -- * Internal operations+  , displayGameOverLoot, displayGameOverAnalytics, displayGameOverLore+  , viewLoreItems+#endif+  ) where++import Prelude ()++import Game.LambdaHack.Core.Prelude++import qualified Data.EnumMap.Strict as EM+import qualified NLP.Miniutter.English as MU++import           Game.LambdaHack.Client.MonadClient+import           Game.LambdaHack.Client.State+import           Game.LambdaHack.Client.UI.ActorUI+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.HandleHelperM+import           Game.LambdaHack.Client.UI.ItemSlot+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.Overlay+import           Game.LambdaHack.Client.UI.SessionUI+import           Game.LambdaHack.Client.UI.Slideshow+import           Game.LambdaHack.Client.UI.SlideshowM+import           Game.LambdaHack.Client.UI.Watch.WatchCommonM+import           Game.LambdaHack.Common.Actor+import           Game.LambdaHack.Common.ActorState+import           Game.LambdaHack.Common.Analytics+import           Game.LambdaHack.Common.ClientOptions+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.Misc+import           Game.LambdaHack.Common.MonadStateRead+import           Game.LambdaHack.Common.State+import           Game.LambdaHack.Common.Types+import qualified Game.LambdaHack.Content.ItemKind as IK+import           Game.LambdaHack.Content.ModeKind+import qualified Game.LambdaHack.Definition.Ability as Ability+import           Game.LambdaHack.Definition.Defs++quitFactionUI :: MonadClientUI m+              => FactionId -> Maybe Status+              -> Maybe (FactionAnalytics, GenerationAnalytics)+              -> m ()+quitFactionUI fid toSt manalytics = do+  ClientOptions{sexposeItems} <- getsClient soptions+  fact <- getsState $ (EM.! fid) . sfactionD+  let fidName = MU.Text $ gname fact+      person = if fhasGender $ gplayer fact then MU.PlEtc else MU.Sg3rd+      horror = isHorrorFact fact+      camping = maybe True ((== Camping) . stOutcome) toSt+  side <- getsClient sside+  when (fid == side && not camping) $ do+    tellGameClipPS+    resetGameStart+  gameMode <- getGameMode+  allNframes <- getsSession sallNframes+  let startingPart = case toSt of+        _ | horror -> Nothing  -- Ignore summoned actors' factions.+        Just Status{stOutcome=stOutcome@Restart, stNewGame=Just gn} ->+          Just $ MU.Text $ nameOutcomeVerb stOutcome+                           <+> "to restart in"+                           <+> displayGroupName gn+                           <+> "mode"+                             -- when multiplayer: "order mission restart in"+        Just Status{stOutcome=Restart, stNewGame=Nothing} ->+          error $ "" `showFailure` (fid, toSt)+        Just Status{stOutcome} -> Just $ MU.Text $ nameOutcomeVerb stOutcome+          -- when multiplayer, for @Camping@: "order save and exit"+        Nothing -> Nothing+      middlePart = case toSt of+        _ | fid /= side -> Nothing+        Just Status{stOutcome} -> lookup stOutcome $ mendMsg gameMode+        Nothing -> Nothing+      partingPart = if fid /= side || allNframes == -1+                    then Nothing+                    else endMessageOutcome . stOutcome <$> toSt+  case startingPart of+    Nothing -> return ()+    Just sp ->+      let blurb = makeSentence [MU.SubjectVerb person MU.Yes fidName sp]+      in msgLnAdd MsgFinalOutcome blurb+  case (toSt, partingPart) of+    (Just status, Just pp) -> do+      noConfirmsGame <- isNoConfirmsGame+      go <- if noConfirmsGame+            then return False+            else displaySpaceEsc ColorFull ""  -- short, just @startingPart@+      recordHistory+        -- we are going to exit or restart, so record and clear, but only once+      (itemBag, total) <- getsState $ calculateTotal side+      when go $ do+        case middlePart of+          Nothing -> return ()+          Just sp1 -> do+            factionD <- getsState sfactionD+            itemToF <- getsState $ flip itemToFull+            let getTrunkFull (aid, b) = (aid, itemToF $ btrunk b)+            ourTrunks <- getsState $ map getTrunkFull+                                     . fidActorNotProjGlobalAssocs side+            let smartFaction fact2 = fleaderMode (gplayer fact2) /= Nothing+                canBeSmart = any (smartFaction . snd)+                canBeOurFaction = any (\(fid2, _) -> fid2 == side)+                smartEnemy trunkFull =+                  let possible =+                        possibleActorFactions (itemKind trunkFull) factionD+                  in not (canBeOurFaction possible) && canBeSmart possible+                smartEnemiesOurs = filter (smartEnemy . snd) ourTrunks+                uniqueActor trunkFull = IA.checkFlag Ability.Unique+                                        $ aspectRecordFull trunkFull+                uniqueEnemiesOurs = filter (uniqueActor . snd) smartEnemiesOurs+                smartUniqueEnemyCaptured = not $ null uniqueEnemiesOurs+                smartEnemyCaptured = not $ null smartEnemiesOurs+            smartEnemySentence <- case uniqueEnemiesOurs ++ smartEnemiesOurs of+              [] -> return ""+              (enemyAid, _) : _ -> do+                bUI <- getsSession $ getActorUI enemyAid+                return $! makePhrase [MU.Capitalize (partActor bUI)] <> "?"+            let won = maybe False ((`elem` victoryOutcomes) . stOutcome) toSt+                lost = maybe False ((`elem` deafeatOutcomes) . stOutcome) toSt+                msgClass | won = MsgGoodMiscEvent+                         | lost = MsgBadMiscEvent+                         | otherwise = MsgNeutralEvent+                (sp2, escPrompt) =+                  if | lost -> ("", "Accept the unacceptable?")+                     | smartUniqueEnemyCaptured ->+                       ( "\nOh, wait, who is this, towering behind your escaping crew?" <+> smartEnemySentence <+> "This changes everything. For everybody. Everywhere. Forever. Did you plan for this? Are you sure it was your idea?"+                       , "What happens now?" )+                     | smartEnemyCaptured ->+                       ( "\nOh, wait, who is this, hunched among your escaping crew?" <+> smartEnemySentence <+> "Suddenly, this makes your crazy story credible. Suddenly, the door of knowledge opens again."+                       , "How will you play that move?" )+                     | otherwise -> ("", "Let's see what we've got here.")+            msgAdd msgClass sp1+            msgAdd MsgFactionIntel sp2+            void $ displaySpaceEsc ColorFull escPrompt+        case manalytics of+          Nothing -> return ()+          Just (factionAn, generationAn) ->+            cycleLore []+              [ displayGameOverLoot (itemBag, total) generationAn+              , displayGameOverLore SOrgan True generationAn+              , displayGameOverAnalytics factionAn generationAn+              , displayGameOverLore SCondition sexposeItems generationAn+              , displayGameOverLore SBlast True generationAn+              , displayGameOverLore SEmbed True generationAn ]+      go2 <- if noConfirmsGame then return False else do+        -- Show score for any UI client after any kind of game exit,+        -- even though it's saved only for human UI clients at game over+        -- (that is not a noConfirms or benchmark game).+        scoreSlides <- scoreToSlideshow total status+        km <- getConfirms ColorFull [K.spaceKM, K.escKM] scoreSlides+        return $! km == K.spaceKM+      let epilogue = do+            when camping $ msgAdd MsgPromptGeneric "Saving..."+            -- Don't leave frozen old prompts on the browser screen.+            pushReportFrame+      if go2 && not noConfirmsGame && not camping then do+        msgAdd MsgPromptGeneric $ pp <+> "(Press RET to have one last look at the arena of your struggle before it gets forgotten.)"+        slides <-+          reportToSlideshowKeepHalt True [K.returnKM, K.spaceKM, K.escKM]+        km <- getConfirms ColorFull [K.returnKM, K.spaceKM, K.escKM] slides+        if km == K.returnKM then do+          -- Enter aiming mode. At exit, game arena is wiped out.+          lidV <- viewedLevelUI+          let saimMode = Just $ AimMode lidV defaultDetailLevel+          modifySession $ \sess -> sess { sreqDelay = ReqDelayHandled+                                        , saimMode }+        else epilogue+      else do+        when (not noConfirmsGame || camping) $ do+          -- The last prompt stays onscreen during shutdown, etc.+          msgAdd MsgPromptGeneric pp+          epilogue+    _ ->+      when (isJust startingPart && (stOutcome <$> toSt) == Just Killed) $ do+        msgAdd MsgTutorialHint "When a whole faction gets eliminated, no new members of the party will ever appear and its stashed belongings may remain far off, unclaimed and undefended. While some adventures require elimination a faction (to be verified in the adventure description screen in the help menu), for others it's an optional task, if possible at all. Instead, finding an exit may be necessary to win. It's enough if one character finds and triggers the exit. Others automatically follow, duly hauling all common belongings."+        -- Needed not to overlook the competitor dying in raid scenario.+        displayMore ColorFull ""++displayGameOverLoot :: MonadClientUI m+                    => (ItemBag, Int) -> GenerationAnalytics -> m K.KM+displayGameOverLoot (heldBag, total) generationAn = do+  ClientOptions{sexposeItems} <- getsClient soptions+  COps{coitem} <- getsState scops+  ItemSlots itemSlots <- getsSession sslots+  -- We assume "gold grain", not "grain" with label "of gold":+  let currencyName = IK.iname $ okind coitem $ ouniqGroup coitem IK.S_CURRENCY+      lSlotsRaw = EM.filter (`EM.member` heldBag) $ itemSlots EM.! SItem+      generationItem = generationAn EM.! SItem+      (itemBag, lSlots) =+        if sexposeItems+        then let generationBag = EM.map (\k -> (-k, [])) generationItem+                 bag = heldBag `EM.union` generationBag+                 slots = EM.fromDistinctAscList $ zip allSlots $ EM.keys bag+             in (bag, slots)+        else (heldBag, lSlotsRaw)+      promptFun iid itemFull2 k =+        let worth = itemPrice 1 $ itemKind itemFull2+            lootMsg = if worth == 0 then "" else+              let pile = if k <= 1 then "exemplar" else "hoard"+              in makeSentence $+                   ["this treasure", pile, "is worth"]+                   ++ (if k > 1 then [ MU.Cardinal k, "times"] else [])+                   ++ [MU.CarWs worth $ MU.Text currencyName]+            holdsMsg =+              let n = generationItem EM.! iid+              in if | max 0 k == 1 && n == 1 ->+                      "You keep the only specimen extant:"+                    | max 0 k == 0 && n == 1 ->+                      "You don't have the only hypothesized specimen:"+                    | max 0 k == 0 && n == 0 ->+                      "No such specimen was recorded:"+                    | otherwise ->+                        makePhrase [ "You hold"+                                   , if k == n+                                     then "all pieces"+                                     else MU.CardinalAWs (max 0 k) "piece"+                                   , "out of"+                                   , MU.Car n+                                   , "scattered:" ]+        in lootMsg <+> holdsMsg+  dungeonTotal <- getsState sgold+  let promptGold = spoilsBlurb currencyName total dungeonTotal+      -- Total number of items is meaningless in the presence of so much junk.+      prompt =+        promptGold+        <+> (if sexposeItems+             then "Non-positive count means none held but this many generated."+             else "")+  viewLoreItems "GameOverLoot" lSlots itemBag prompt promptFun True++displayGameOverAnalytics :: MonadClientUI m+                         => FactionAnalytics -> GenerationAnalytics+                         -> m K.KM+displayGameOverAnalytics factionAn generationAn = do+  ClientOptions{sexposeActors} <- getsClient soptions+  side <- getsClient sside+  ItemSlots itemSlots <- getsSession sslots+  let ourAn = akillCounts+              $ EM.findWithDefault emptyAnalytics side factionAn+      foesAn = EM.unionsWith (+)+               $ concatMap EM.elems $ catMaybes+               $ map (`EM.lookup` ourAn) [KillKineticMelee .. KillOtherPush]+      trunkBagRaw = EM.map (, []) foesAn+      lSlotsRaw = EM.filter (`EM.member` trunkBagRaw) $ itemSlots EM.! STrunk+      killedBag = EM.fromList $ map (\iid -> (iid, trunkBagRaw EM.! iid))+                                    (EM.elems lSlotsRaw)+      generationTrunk = generationAn EM.! STrunk+      (trunkBag, lSlots) =+        if sexposeActors+        then let generationBag = EM.map (\k -> (-k, [])) generationTrunk+                 bag = killedBag `EM.union` generationBag+                 slots = EM.fromDistinctAscList $ zip allSlots $ EM.keys bag+             in (bag, slots)+        else (killedBag, lSlotsRaw)+      total = sum $ filter (> 0) $ map fst $ EM.elems trunkBag+      -- Not just "killed 1 out of 4", because it's sometimes "2 out of 1",+      -- if an enemy was revived.+      promptFun :: ItemId -> ItemFull-> Int -> Text+      promptFun iid _ k =+        let n = generationTrunk EM.! iid+        in makePhrase [ "You recall the adversary, which you killed on"+                      , MU.CarWs (max 0 k) "occasion", "while reports mention"+                      , MU.CarWs n "individual", "in total:" ]+      prompt =+        makeSentence ["your team vanquished", MU.CarWs total "adversary"]+          -- total reported would include our own, so not given+        <+> (if sexposeActors+             then "Non-positive count means none killed but this many reported."+             else "")+  viewLoreItems "GameOverAnalytics" lSlots trunkBag prompt promptFun False++displayGameOverLore :: MonadClientUI m+                    => SLore -> Bool -> GenerationAnalytics -> m K.KM+displayGameOverLore slore exposeCount generationAn = do+  let generationLore = generationAn EM.! slore+      generationBag = EM.map (\k -> (if exposeCount then k else 1, []))+                             generationLore+      total = sum $ map fst $ EM.elems generationBag+      slots = EM.fromDistinctAscList $ zip allSlots $ EM.keys generationBag+      promptFun :: ItemId -> ItemFull-> Int -> Text+      promptFun _ _ k =+        makeSentence+          [ "this", MU.Text (ppSLore slore), "manifested during your quest"+          , MU.CarWs k "time" ]+      verb = if | slore `elem` [SCondition, SBlast] -> "experienced"+                | slore == SEmbed -> "strived through"+                | otherwise -> "lived among"+      prompt = case total of+        0 -> makeSentence [ "you didn't experience any"+                          , MU.Ws $ MU.Text (headingSLore slore)+                          , "this time" ]+        1 -> makeSentence [ "you", verb, "the following"+                          , MU.Text (headingSLore slore) ]+        _ -> makeSentence [ "you", verb, "the following variety of"+                          , MU.CarWs total $ MU.Text (headingSLore slore) ]+      displayRanged = slore `notElem` [SOrgan, STrunk]+  viewLoreItems ("GameOverLore" ++ show slore)+                slots generationBag prompt promptFun displayRanged++viewLoreItems :: forall m . MonadClientUI m+              => String -> SingleItemSlots -> ItemBag -> Text+              -> (ItemId -> ItemFull -> Int -> Text)+              -> Bool+              -> m K.KM+viewLoreItems menuName lSlotsRaw trunkBag prompt promptFun displayRanged = do+  CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui+  FontSetup{..} <- getFontSetup+  arena <- getArenaUI+  itemToF <- getsState $ flip itemToFull+  let keysPre = [K.spaceKM, K.mkChar '<', K.mkChar '>', K.escKM]+      lSlots = sortSlotMap itemToF lSlotsRaw+  msgAdd MsgPromptGeneric prompt+  io <- itemOverlay lSlots arena trunkBag displayRanged+  itemSlides <- overlayToSlideshow (rheight - 2) keysPre io+  let keyOfEKM (Left km) = km+      keyOfEKM (Right SlotChar{slotChar}) = K.mkChar slotChar+      allOKX = concatMap snd $ slideshow itemSlides+      keysMain = keysPre ++ map (keyOfEKM . fst) allOKX+      displayInRightPane :: KeyOrSlot -> m OKX+      displayInRightPane ekm = case ekm of+        _ | isSquareFont propFont -> return emptyOKX+        Left{} -> return emptyOKX+        Right slot -> do+          let ix0 = fromMaybe (error $ show slot)+                              (elemIndex slot $ EM.keys lSlots)+          -- Mono font used, because lots of numbers in these blurbs+          -- and because some prop fonts wider than mono (e.g., in the+          -- dejavuBold font set).+          -- Lower width, to permit extra vertical space at the start,+          -- because gameover menu prompts are sometimes wide and/or long.+          okxItemLorePointedAt+            monoFont (rwidth - 2) True trunkBag 0 promptFun ix0 lSlots+      viewAtSlot :: SlotChar -> m K.KM+      viewAtSlot slot = do+        let ix0 = fromMaybe (error $ show slot)+                            (elemIndex slot $ EM.keys lSlots)+        km <- displayItemLore trunkBag 0 promptFun ix0 lSlots False+        case K.key km of+          K.Space -> viewLoreItems menuName lSlots trunkBag prompt+                                   promptFun displayRanged+          K.Esc -> return km+          _ -> error $ "" `showFailure` km+  ekm <- displayChoiceScreenWithRightPane displayInRightPane+           menuName ColorFull False itemSlides keysMain+  case ekm of+    Left km | km `elem` [K.spaceKM, K.mkChar '<', K.mkChar '>', K.escKM] ->+      return km+    Left K.KM{key=K.Char l} -> viewAtSlot $ SlotChar 0 l+      -- other prefixes are not accessible via keys; tough luck; waste of effort+    Left km -> error $ "" `showFailure` km+    Right slot -> viewAtSlot slot
+ engine-src/Game/LambdaHack/Client/UI/Watch/WatchSfxAtomicM.hs view
@@ -0,0 +1,989 @@+-- | 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+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.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) $+            msgAdd MsgTutorialHint "You took damage of a different kind than the normal piercing hit, which means your armor couldn't block any part of it. Normally, your HP (hit points, health) do not regenerate, so losing them is a big deal. Apply healing concoctions or take a long sleep to replenish your HP (but in this hectic environment not even uninterrupted resting that leads to sleep is easy)."+        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"+              msgAdd MsgTutorialHint "New floor is new opportunities, though the old level is still there and others may roam it after you left. Viewing all floors, without moving between them, can be done using the '<' and '>' keys."+            [] -> 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 "heat up"+      IK.Discharge{} | not isAlive -> return ()+      IK.Discharge{} -> aidVerbMU MsgEffectMedium aid "cool down"+      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 iid c ->+    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)) $+            msgAdd MsgTutorialHint "This enemy can't harm you in melee. Left alone could it possibly be of some use?"+    -- 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) $+           msgAdd MsgTutorialHint "You managed to catch a projectile, thanks to being braced and hitting it exactly when it was at arm's reach. The obtained item has been put into the shared stash of your party."+         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) $+           msgAdd MsgTutorialHint "Some hits don't cause piercing, impact, burning nor any other direct damage. However, they can have other effects, bad, good or both."+         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
+ engine-src/Game/LambdaHack/Client/UI/Watch/WatchUpdAtomicM.hs view
@@ -0,0 +1,1109 @@+-- | Display atomic update commands received by the client.+module Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM+  ( watchRespUpdAtomicUI+#ifdef EXPOSE_INTERNAL+    -- * Internal operations+  , updateItemSlot, Threat, createActorUI, destroyActorUI, spotItemBag+  , recordItemLid, moveActor, displaceActorUI, moveItemUI+  , discover, ppHearMsg, ppHearDistanceAdjective, ppHearDistanceAdverb+#endif+  ) where++import Prelude ()++import Game.LambdaHack.Core.Prelude++import           Control.Concurrent (threadDelay)+import qualified Data.Char as Char+import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES+import qualified Data.Map.Strict as M+import qualified Data.Text as T+import           Data.Tuple+import           GHC.Exts (inline)+import qualified NLP.Miniutter.English as MU++import           Game.LambdaHack.Atomic+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.DrawM+import           Game.LambdaHack.Client.UI.Frame+import           Game.LambdaHack.Client.UI.FrameM+import           Game.LambdaHack.Client.UI.HandleHelperM+import           Game.LambdaHack.Client.UI.ItemDescription+import           Game.LambdaHack.Client.UI.ItemSlot+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.UIOptions+import           Game.LambdaHack.Client.UI.Watch.WatchCommonM+import           Game.LambdaHack.Client.UI.Watch.WatchQuitM+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.State+import qualified Game.LambdaHack.Common.Tile as Tile+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           Game.LambdaHack.Content.ModeKind+import qualified Game.LambdaHack.Content.ModeKind as MK+import           Game.LambdaHack.Content.RuleKind+import qualified Game.LambdaHack.Content.TileKind as TK+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+import           Game.LambdaHack.Definition.Flavour++-- | Visualize atomic updates sent to the client. This is done+-- in the global state after the command is executed and after+-- the client state is modified by the command.+-- Doesn't modify client state (except a few fields), but only client+-- session (e.g., by displaying messages). This is enforced by types.+watchRespUpdAtomicUI :: MonadClientUI m => UpdAtomic -> m ()+{-# INLINE watchRespUpdAtomicUI #-}+watchRespUpdAtomicUI cmd = case cmd of+  -- Create/destroy actors and items.+  UpdRegisterItems{} -> return ()+  UpdCreateActor aid body _ -> createActorUI True aid body+  UpdDestroyActor aid body _ -> destroyActorUI True aid body+  UpdCreateItem verbose iid _ kit@(kAdd, _) c -> do+    recordItemLid iid c+    updateItemSlot c iid+    if verbose then case c of+      CActor aid store -> do+        b <- getsState $ getActorBody aid+        case store of+          _ | bproj b ->+            itemVerbMU MsgItemCreation iid kit "appear" c+          COrgan -> do+            localTime <- getsState $ getLocalTime (blid b)+            arItem <- getsState $ aspectRecordFromIid iid+            if | IA.checkFlag Ability.Blast arItem -> return ()+               | IA.checkFlag Ability.Condition arItem -> do+                 side <- getsClient sside+                 discoBenefit <- getsClient sdiscoBenefit+                 bag <- getsState $ getContainerBag c+                 itemKind <- getsState $ getIidKind iid+                 let more = case EM.lookup iid bag of+                       Just (kTotal, _) | kTotal /= kAdd -> Just kTotal+                       _ -> Nothing+                     verbShow = MU.Text $+                       "become"+                       <+> case kit of+                         (1, _ : _) -> "somewhat"+                         (1, []) | isNothing more -> ""+                         _ | isNothing more -> "many-fold"+                         _ -> "additionally"+                     verbSave = MU.Text $+                       "become"+                       <+> case kit of+                         (1, t:_) ->  -- only exceptionally not singleton list+                                      -- or even more than one copy total+                           let total = deltaOfItemTimer localTime t+                           in timeDeltaInSecondsText total+                         (1, []) | isNothing more -> ""+                         (k, _) ->  -- usually the list empty; ignore anyway+                           (if isJust more then "additionally" else "")+                           <+> tshow k <> "-fold"+                           <+> case more of+                                 Nothing -> ""+                                 Just kTotal ->+                                   "(total:" <+> tshow kTotal <> "-fold)"+                     good = benInEqp (discoBenefit EM.! iid)+                     msgClass = case lookup IK.S_ASLEEP $ IK.ifreq itemKind of+                       Just n | n > 0 -> MsgStatusSleep+                       _ -> if | bfid b /= side -> MsgStatusOthers+                               | good -> MsgStatusGoodUs+                               | otherwise -> MsgStatusBadUs+                 -- This describes all such items already among organs,+                 -- which is useful, because it shows "charging".+                 itemAidDistinctMU msgClass aid verbShow verbSave iid+                 when (bfid b == side && not good) $+                   -- Others get conditions too often and good ones are not+                   -- dire enough and also too common.+                   msgAdd MsgTutorialHint "Temporary conditions, especially the bad ones, pass quickly, usually after just a few turns. While active, they are listed in the '@' organ menu and the effects of most of them are seen in the '#' skill menu."+               | otherwise -> do+                 wown <- ppContainerWownW partActorLeader True c+                 itemVerbMU MsgItemCreation iid kit+                            (MU.Text $ makePhrase $ "grow" : wown) c+          _ -> do+            wown <- ppContainerWownW partActorLeader True c+            itemVerbMU MsgItemCreation iid kit+                       (MU.Text $ makePhrase $ "appear" : wown) c+      CEmbed lid _ -> markDisplayNeeded lid+      CFloor lid _ -> do+        factionD <- getsState sfactionD+        itemVerbMU MsgItemCreation iid kit+                   (MU.Text $ "appear" <+> ppContainer factionD c) c+        markDisplayNeeded lid+      CTrunk{} -> return ()+    else do+      lid <- getsState $ lidFromC c+      markDisplayNeeded lid+  UpdDestroyItem verbose iid _ kit c ->+    if verbose then case c of+      CActor aid _  -> do+        b <- getsState $ getActorBody aid+        if bproj b then+          itemVerbMUShort MsgItemRuination iid kit "break" c+        else do+          ownW <- ppContainerWownW partActorLeader False c+          let verb = MU.Text $ makePhrase $ "vanish from" : ownW+          itemVerbMUShort MsgItemRuination iid kit verb c+      CEmbed lid _ -> markDisplayNeeded lid+      CFloor lid _ -> do+        factionD <- getsState sfactionD+        itemVerbMUShort MsgItemRuination iid kit+                        (MU.Text $ "break" <+> ppContainer factionD c) c+        markDisplayNeeded lid+      CTrunk{} -> return ()+    else do+      lid <- getsState $ lidFromC c+      markDisplayNeeded lid+  UpdSpotActor aid body -> createActorUI False aid body+  UpdLoseActor aid body -> destroyActorUI False aid body+  UpdSpotItem verbose iid kit c -> spotItemBag verbose c $ EM.singleton iid kit+  UpdLoseItem True iid kit c@(CActor aid _) -> do+    b <- getsState $ getActorBody aid+    when (not (bproj b) && bhp b > 0) $ do  -- don't spam+      ownW <- ppContainerWownW partActorLeader False c+      let verb = MU.Text $ makePhrase $ "be removed from" : ownW+      itemVerbMUShort MsgItemMovement iid kit verb c+  UpdLoseItem{} -> return ()+  UpdSpotItemBag verbose c bag -> spotItemBag verbose c bag+  UpdLoseItemBag{} -> return ()  -- rarely interesting and can be very long+  -- Move actors and items.+  UpdMoveActor aid source target -> moveActor aid source target+  UpdWaitActor aid WSleep _ -> do+    aidVerbMU MsgStatusWakeup aid "wake up"+    msgAdd MsgTutorialHint "Woken up actors regain stats and skills, including sight radius and melee armor, over several turns."+  UpdWaitActor aid WWake _ -> do+    side <- getsClient sside+    b <- getsState $ getActorBody aid+    unless (bfid b == side) $+      msgAdd MsgTutorialHint "To avoid waking enemies up, make sure they don't lose HP nor too much Calm through noises, particularly close ones. Beware, however, that they slowly regenerate HP as they sleep and eventually wake up at full HP."+  UpdWaitActor{} -> return ()  -- falling asleep handled uniformly elsewhere+  UpdDisplaceActor source target -> displaceActorUI source target+  UpdMoveItem iid k aid c1 c2 -> moveItemUI iid k aid c1 c2+  -- Change actor attributes.+  UpdRefillHP _ 0 -> return ()+  UpdRefillHP aid hpDelta -> do+    let coarseDelta = abs hpDelta `div` oneM+        tDelta = if coarseDelta == 0+                 then if hpDelta > 0 then "a little" else "a fraction of an HP"+                 else tshow coarseDelta <+> "HP"+    b <- getsState $ getActorBody aid+    unless (bproj b) $+      aidVerbMU MsgNumericReport aid $ MU.Text+                ((if hpDelta > 0 then "heal" else "lose") <+> tDelta)+    arena <- getArenaUI+    side <- getsClient sside+    if | bproj b && (EM.null (beqp b) || isNothing (btrajectory b)) ->+           return ()  -- ignore caught proj or one hitting a wall+       | bhp b <= 0 && hpDelta < 0+         && (bfid b == side && not (bproj b) || arena == blid b) -> do+         let (firstFall, hurtExtra) = case (bfid b == side, bproj b) of+               (True, True) -> ("drop down", "tumble down")+               (True, False) -> ("fall down", "suffer woeful mutilation")+               (False, True) -> ("plummet", "crash")+               (False, False) -> ("collapse", "be reduced to a bloody pulp")+             verbDie = if alreadyDeadBefore then hurtExtra else firstFall+             -- Rarely, this is wrong, because 2 other actors hit the victim+             -- at exactly the same time. No big problem. Doubled "dies"+             -- messages appears instead of "dies; is mutilated".+             alreadyDeadBefore = bhp b - hpDelta <= 0+         tfact <- getsState $ (EM.! bfid b) . sfactionD+         bUI <- getsSession $ getActorUI aid+         subjectRaw <- partActorLeader aid+         let subject = if alreadyDeadBefore || subjectRaw == "you"+                       then subjectRaw+                       else partActor bUI  -- avoid "fallen"+             msgDie = makeSentence [MU.SubjectVerbSg subject verbDie]+             targetIsFoe = isFoe (bfid b) tfact side+             targetIsFriend = isFriend (bfid b) tfact side+             msgClass | bproj b = MsgDeathBoring+                      | targetIsFoe = MsgDeathVictory+                      | targetIsFriend = MsgDeathDeafeat+                      | otherwise = MsgDeathBoring+         if | bproj b -> msgAdd msgClass msgDie+            | bfid b == side -> do+              msgLnAdd msgClass $ msgDie <+> "Alas!"+              displayMore ColorBW ""+            | otherwise -> msgLnAdd msgClass msgDie+         -- We show death anims only if not dead already before this refill.+         let deathAct = if bfid b == side+                        then deathBody (bpos b)+                        else shortDeathBody (bpos b)+         unless (bproj b || alreadyDeadBefore) $ animate (blid b) deathAct+       | otherwise -> do+         when (hpDelta >= bhp b && bhp b > 0) $+           aidVerbMU MsgActionWarning aid "return from the brink of death"+         mleader <- getsClient sleader+         when (Just aid == mleader) $ do+           actorMaxSk <- getsState $ getActorMaxSkills aid+           -- Regenerating actors never stop gaining HP, so we need to stop+           -- reporting it after they reach full HP for the first time.+           -- Also, no spam for non-leaders.+           when (bhp b >= xM (Ability.getSk Ability.SkMaxHP actorMaxSk)+                 && bhp b - hpDelta < xM (Ability.getSk Ability.SkMaxHP+                                                  actorMaxSk)) $+             msgAdd MsgSpecialEvent "You recover your health fully. Any further gains will be transient."+         when (bfid b == side && not (bproj b)) $ do+           when (abs hpDelta >= oneM) $ markDisplayNeeded (blid b)+           when (hpDelta < 0) $ do+             when (hpDelta <= xM (-3)) $ msgAdd MsgTutorialHint "You took a lot of damage from one source. If the danger persists, consider retreating towards your teammates or buffing up or an instant escape, if consumables permit."+             sUIOptions <- getsSession sUIOptions+             currentWarning <-+               getsState $ checkWarningHP sUIOptions aid (bhp b)+             when currentWarning $ do+               previousWarning <-+                 getsState $ checkWarningHP sUIOptions aid (bhp b - hpDelta)+               unless previousWarning $+                 aidVerbMU MsgRiskOfDeath aid+                           "be down to a dangerous health level"+  UpdRefillCalm _ 0 -> return ()+  UpdRefillCalm aid calmDelta -> do+    side <- getsClient sside+    b <- getsState $ getActorBody aid+    when (bfid b == side && not (bproj b)) $ do+      if | calmDelta > 0 -> do  -- regeneration or effect+           mleader <- getsClient sleader+           when (Just aid == mleader) $ do+             actorMaxSk <- getsState $ getActorMaxSkills aid+             let bPrev = b {bcalm = bcalm b - calmDelta}+             when (calmEnough b actorMaxSk+                   && not (calmEnough bPrev actorMaxSk)) $+               msgAdd MsgNeutralEvent "You are again calm enough to manage your equipment outfit."+           -- If the leader regenerates Calm more often than once per+           -- standard game turn, this will not be reflected, for smoother+           -- and faster display. However, every halt for keypress+           -- shows Calm, so this only matters for macros, where speed is good.+           when (abs calmDelta > oneM) $ markDisplayNeeded (blid b)+         | calmDelta == minusM1 -> do+           fact <- getsState $ (EM.! side) . sfactionD+           s <- getState+           let closeFoe (!p, aid2) =  -- mimics isHeardFoe+                 let b2 = getActorBody aid2 s+                 in inline chessDist p (bpos b) <= 3+                    && not (actorWaitsOrSleeps b2)  -- uncommon+                    && inline isFoe side fact (bfid b2)  -- costly+               anyCloseFoes = any closeFoe $ EM.assocs $ lbig+                                           $ sdungeon s EM.! blid b+           unless anyCloseFoes $ do  -- obvious where the feeling comes from+             duplicated <- aidVerbDuplicateMU MsgHeardNearby aid+                                              "hear something"+             unless duplicated stopPlayBack+         | otherwise ->  -- low deltas from hits; displayed elsewhere+           return ()+      when (calmDelta < 0) $ do+        sUIOptions <- getsSession sUIOptions+        currentWarning <-+          getsState $ checkWarningCalm sUIOptions aid (bcalm b)+        when currentWarning $ do+          previousWarning <-+            getsState $ checkWarningCalm sUIOptions aid (bcalm b - calmDelta)+          unless previousWarning $+            -- This messages is not shown if impression happens after+            -- Calm is low enough. However, this is rare and HUD shows the red.+            aidVerbMU MsgRiskOfDeath aid+                      "have grown agitated and impressed enough to be in danger of defecting"+  UpdTrajectory _ _ mt ->  -- if projectile dies just after, force one frame+    when (isNothing mt) $ pushFrame False+  -- Change faction attributes.+  UpdQuitFaction fid _ toSt manalytics -> quitFactionUI fid toSt manalytics+  UpdSpotStashFaction verbose fid lid pos -> do+    side <- getsClient sside+    when verbose $ do+      if fid == side then+        msgLnAdd MsgFactionIntel+                 "You set up the shared inventory stash of your team."+      else do+        fact <- getsState $ (EM.! fid) . sfactionD+        let fidName = MU.Text $ gname fact+        msgAdd MsgFactionIntel $+          makeSentence [ "you have found the current"+                       , MU.WownW fidName "hoard location" ]+    unless (fid == side) $+      animate lid $ actorX pos+  UpdLoseStashFaction verbose fid lid pos -> do+    when verbose $ do+      side <- getsClient sside+      if fid == side then+        msgAdd MsgFactionIntel+               "You've lost access to your shared inventory stash!"+      else do+        fact <- getsState $ (EM.! fid) . sfactionD+        let fidName = MU.Text $ gname fact+        msgAdd MsgFactionIntel $+          makeSentence [fidName, "no longer control their hoard"]+    animate lid $ vanish pos+  UpdLeadFaction fid (Just source) mtgt@(Just target) -> do+    mleader <- getsClient sleader+    when (mtgt /= mleader) $ do+      fact <- getsState $ (EM.! fid) . sfactionD+      lidV <- viewedLevelUI+      when (isAIFact fact) $ markDisplayNeeded lidV+      -- This faction can't run with multiple actors, so this is not+      -- a leader change while running, but rather server changing+      -- their leader, which the player should be alerted to.+      when (noRunWithMulti fact) stopPlayBack+      actorD <- getsState sactorD+      case EM.lookup source actorD of+        Just sb | bhp sb <= 0 -> assert (not $ bproj sb) $ do+          -- Regardless who the leader is, give proper names here, not 'you'.+          sbUI <- getsSession $ getActorUI source+          tbUI <- getsSession $ getActorUI target+          let subject = partActor tbUI+              object  = partActor sbUI+          msgAdd MsgPointmanSwap $+            makeSentence [ MU.SubjectVerbSg subject "take command"+                         , "from", object ]+        _ -> return ()+      lookAtMove target+  UpdLeadFaction _ Nothing mtgt@(Just target) -> do+    mleader <- getsClient sleader+    when (mtgt /= mleader) $+      lookAtMove target+  UpdLeadFaction{} -> return ()+  UpdDiplFaction fid1 fid2 _ toDipl -> do+    name1 <- getsState $ gname . (EM.! fid1) . sfactionD+    name2 <- getsState $ gname . (EM.! fid2) . sfactionD+    let showDipl Unknown = "unknown to each other"+        showDipl Neutral = "in neutral diplomatic relations"+        showDipl Alliance = "allied"+        showDipl War = "at war"+    msgAdd MsgFactionIntel $+      name1 <+> "and" <+> name2 <+> "are now" <+> showDipl toDipl <> "."+  UpdDoctrineFaction{} -> return ()+  UpdAutoFaction fid b -> do+    side <- getsClient sside+    lidV <- viewedLevelUI+    markDisplayNeeded lidV+    when (fid == side) $ do+      unless b $+        -- Clear macros and invoke a special main menu entrance macro+        -- that sets @swasAutomated@, preparing for AI control at exit.+        modifySession $ \sess ->+          sess { smacroFrame =+                   emptyMacroFrame {keyPending = KeyMacro [K.controlEscKM]}+               , smacroStack = [] }+      setFrontAutoYes b  -- now can start/stop auto-accepting prompts+  UpdRecordKill{} -> return ()+  -- Alter map.+  UpdAlterTile lid p fromTile toTile -> do+    COps{cotile} <- getsState scops+    markDisplayNeeded lid+    let feats = TK.tfeature $ okind cotile fromTile+        toAlter feat =+          case feat of+            TK.OpenTo tgroup -> Just tgroup+            TK.CloseTo tgroup -> Just tgroup+            TK.ChangeTo tgroup -> Just tgroup+            TK.OpenWith _ _ tgroup -> Just tgroup+            TK.CloseWith _ _ tgroup -> Just tgroup+            TK.ChangeWith _ _ tgroup -> Just tgroup+            _ -> Nothing+        groupsToAlterTo = mapMaybe toAlter feats+        freq = map fst $ filter (\(_, q) -> q > 0)+               $ TK.tfreq $ okind cotile toTile+        unexpected = null $ intersect freq groupsToAlterTo+    mactorAtPos <- getsState $ posToBig p lid+    mleader <- getsClient sleader+    when (unexpected || isJust mactorAtPos && mactorAtPos /= mleader) $ do+      -- Player notices @fromTile can't be altered into @toTIle@,+      -- which is uncanny, so we produce a message.+      -- This happens when the player missed an earlier search of the tile+      -- performed by another faction.+      let subject = ""  -- a hack, because we don't handle adverbs well+          verb = "turn into"+          msg = makeSentence $+            [ "the", MU.Text $ TK.tname $ okind cotile fromTile+            , "at position", MU.Text $ tshow p ]+            ++ ["suddenly" | unexpected]  -- adverb+            ++ [ MU.SubjectVerbSg subject verb+               , MU.AW $ MU.Text $ TK.tname $ okind cotile toTile ]+      msgAdd (if unexpected then MsgSpecialEvent else MsgNeutralEvent) msg+  UpdAlterExplorable lid _ -> markDisplayNeeded lid+  UpdAlterGold{} -> return ()  -- not displayed on HUD+  UpdSearchTile aid _p toTile -> do+    COps{cotile} <- getsState scops+    subject <- partActorLeader aid+    let fromTile = fromMaybe (error $ show toTile) $ Tile.hideAs cotile toTile+        subject2 = MU.Text $ TK.tname $ okind cotile fromTile+        object = MU.Text $ TK.tname $ okind cotile toTile+    let msg = makeSentence [ MU.SubjectVerbSg subject "reveal"+                           , "that the"+                           , MU.SubjectVerbSg subject2 "be"+                           , MU.AW object ]+    unless (subject2 == object) $ do+      msgAdd MsgTerrainReveal msg+      msgAdd MsgTutorialHint "Solid terrain drawn in pink is not fully known until searched. This is usually done by bumping into it, which also triggers effects and transformations the terrain is capable of. Once revealed, the terrain can be inspected in aiming mode started with the '*' key or with mouse."+  UpdHideTile{} -> return ()+  UpdSpotTile{} -> return ()+  UpdLoseTile{} -> return ()+  UpdSpotEntry{} -> return ()+  UpdLoseEntry{} -> return ()+  UpdAlterSmell{} -> return ()+  UpdSpotSmell{} -> return ()+  UpdLoseSmell{} -> return ()+  -- Assorted.+  UpdTimeItem{} -> return ()+  UpdAgeGame{} -> do+    sdisplayNeeded <- getsSession sdisplayNeeded+    sturnDisplayed <- getsSession sturnDisplayed+    time <- getsState stime+    let clipN = time `timeFit` timeClip+        clipMod = clipN `mod` clipsInTurn+        turnPing = clipMod == 0  -- e.g., to see resting counter+    if | sdisplayNeeded -> pushFrame True+           -- adds delay, because it's not an extra animation-like frame,+           -- but showing some real information accumulated up to this point+       | turnPing && not sturnDisplayed -> pushFrame False+       | otherwise -> return ()+    when turnPing $+      modifySession $ \sess -> sess {sturnDisplayed = False}+  UpdUnAgeGame{} -> return ()+  UpdDiscover c iid _ _ -> discover c iid+  UpdCover{} -> return ()  -- don't spam when doing undo+  UpdDiscoverKind{} -> return ()  -- don't spam when server tweaks stuff+  UpdCoverKind{} -> return ()  -- don't spam when doing undo+  UpdDiscoverAspect{} -> return ()  -- don't spam when server tweaks stuff+  UpdCoverAspect{} -> return ()  -- don't spam when doing undo+  UpdDiscoverServer{} -> error "server command leaked to client"+  UpdCoverServer{} -> error "server command leaked to client"+  UpdPerception{} -> return ()+  UpdRestart fid _ _ _ _ srandom -> do+    cops@COps{cocave, comode, corule} <- getsState scops+    oldSess <- getSession+    svictories <- getsClient svictories+    snxtChal <- getsClient snxtChal+    let uiOptions = sUIOptions oldSess+        f !acc _p !i _a = i : acc+        modes = zip [0..] $ ofoldlGroup' comode CAMPAIGN_SCENARIO f []+        g :: (Int, ContentId ModeKind) -> Int+        g (_, mode) = case EM.lookup mode svictories of+          Nothing -> 0+          Just cm -> fromMaybe 0 (M.lookup snxtChal cm)+        (snxtScenario, _) = minimumBy (comparing g) modes+        nxtGameTutorial = MK.mtutorial $ snd $ nxtGameMode cops snxtScenario+    putSession $+      (emptySessionUI uiOptions)+        { schanF = schanF oldSess+        , sccui = sccui oldSess+        , shistory = shistory oldSess+        , smarkVision = smarkVision oldSess+        , smarkSmell = smarkSmell oldSess+        , snxtScenario+        , scurTutorial = snxtTutorial oldSess  -- quite random for screensavers+        , snxtTutorial = nxtGameTutorial+        , soverrideTut = soverrideTut oldSess+        , sstart = sstart oldSess+        , sgstart = sgstart oldSess+        , sallTime = sallTime oldSess+        , snframes = snframes oldSess+        , sallNframes = sallNframes oldSess+        , srandomUI = srandom+        }+    when (sstart oldSess == 0) resetSessionStart+    when (lengthHistory (shistory oldSess) == 0) $ do+      let title = T.pack $ rtitle corule+      msgAdd MsgBookKeeping $ "Welcome to" <+> title <> "!"+      -- Generate initial history. Only for UI clients.+      shistory <- defaultHistory+      modifySession $ \sess -> sess {shistory}+    recordHistory  -- to ensure EOL even at creation of history+    lid <- getArenaUI+    lvl <- getLevel lid+    gameMode <- getGameMode+    curChal <- getsClient scurChal+    fact <- getsState $ (EM.! fid) . sfactionD+    let loneMode = case ginitial fact of+          [] -> True+          [(_, 1, _)] -> True+          _ -> False+    msgAdd MsgBookKeeping "-------------------------------------------------"+    recordHistory+    msgAdd MsgActionWarning+           ("New game started in" <+> mname gameMode <+> "mode.")+    msgAdd MsgPlotExposition $ mdesc gameMode+    let desc = cdesc $ okind cocave $ lkind lvl+    unless (T.null desc) $ do+      msgLnAdd MsgBackdropFocus "You take in your surroundings."+      msgAdd MsgBackdropInfo desc+    -- We can fool the player only once (per scenario), but let's not do it+    -- in the same way each time. TODO: PCG+    blurb <- rndToActionUI $ oneOf+      [ "You think you saw movement."+      , "Something catches your peripherial vision."+      , "You think you felt a tremor under your feet."+      , "A whiff of chilly air passes around you."+      , "You notice a draft just when it dies down."+      , "The ground nearby is stained along some faint lines."+      , "Scarce black motes slowly settle on the ground."+      , "The ground in the immediate area is empty, as if just swiped."+      ]+    msgLnAdd MsgBadMiscEvent blurb  -- being here is a bad turn of events+    when (cwolf curChal && not loneMode) $+      msgAdd MsgActionWarning "Being a lone wolf, you begin without companions."+    when (lengthHistory (shistory oldSess) > 1) $+      fadeOutOrIn False+    setFrontAutoYes $ isAIFact fact+    -- Forget the furious keypresses when dying in the previous game.+    resetPressedKeys+    -- Help newbies when actors obscured by text and no obvious key to press:+    displayMore ColorFull "\nAre you up for the challenge?"+    msgAdd MsgPromptGeneric+           "A grand story starts right here! (Press '?' for context and help.)"+  UpdRestartServer{} -> return ()+  UpdResume fid _ -> do+    COps{cocave} <- getsState scops+    resetSessionStart+    fact <- getsState $ (EM.! fid) . sfactionD+    setFrontAutoYes $ isAIFact fact+    unless (isAIFact fact) $ do+      lid <- getArenaUI+      lvl <- getLevel lid+      gameMode <- getGameMode+      msgAdd MsgActionAlert $ "Continuing" <+> mname gameMode <> "."+      msgAdd MsgPromptGeneric $ mdesc gameMode+      let desc = cdesc $ okind cocave $ lkind lvl+      unless (T.null desc) $ do+        msgLnAdd MsgPromptFocus "You remember your surroundings."+        msgAdd MsgPromptGeneric desc+      displayMore ColorFull "\nAre you up for the challenge?"+      msgAdd MsgPromptGeneric+             "Prove yourself! (Press '?' for context and help.)"+  UpdResumeServer{} -> return ()+  UpdKillExit{} -> do+#ifdef USE_JSFILE+      -- Some browsers seem to trash Local Storage when page reloaded or closed+      -- or the browser closed, while they still internally finish the saving+      -- in the background, so wait 2s. If the exit is without a save,+      -- the wait is spurious, but it's not supposed to be common.+      -- TODO: replace the @liftIO@ with a @MonadClientUI@ delay function.+    liftIO $ threadDelay 2000000+#else+    liftIO $ threadDelay 200000+#endif+    -- The prompt is necessary to force frontend to show this before exiting.+    void $ displayMore ColorBW "Done."  -- in case it follows "Saving..."+    side <- getsClient sside+    debugPossiblyPrintUI $ "Client" <+> tshow side <+> "closing frontend."+    frontendShutdown+    debugPossiblyPrintUI $ "Client" <+> tshow side <+> "closed frontend."+  UpdWriteSave -> msgAdd MsgInnerWorkSpam "Saving backup."+  UpdHearFid _ distance hearMsg -> do+    mleader <- getsClient sleader+    case mleader of+      Just{} -> return ()  -- will flush messages when leader moves+      Nothing -> do+        lidV <- viewedLevelUI+        markDisplayNeeded lidV+        recordHistory+    msg <- ppHearMsg distance hearMsg+    let msgClass = case distance of+          Nothing -> MsgHeardOutside+          Just 0 -> MsgHeardNearby+          Just _ -> MsgHeardFaraway+    msgAdd msgClass msg+    case hearMsg of+      HearUpd UpdDestroyActor{} ->+        msgAdd MsgTutorialHint "Events out of your sight radius (as listed in the '#' skill menu) can sometimes be heard, depending on your hearing radius skill. Some, such as death shrieks, can always be heard regardless of skill and distance, including when they come from a different floor."+      HearTaunt{} -> do+        globalTime <- getsState stime+        when (globalTime > timeTurn) $  -- avoid too many hints at the start+          msgAdd MsgTutorialHint "Enemies you can't see are sometimes heard yelling and emitting other noises. Whether you can hear them, depends on their distance and your hearing radius, as listed in the '#' skill menu."+      _ -> return ()+  UpdMuteMessages _ smuteMessages ->+    modifySession $ \sess -> sess {smuteMessages}++updateItemSlot :: MonadClientUI m => Container -> ItemId -> m ()+updateItemSlot c iid = do+  arItem <- getsState $ aspectRecordFromIid iid+  ItemSlots itemSlots <- getsSession sslots+  let slore = IA.loreFromContainer arItem c+      lSlots = itemSlots EM.! slore+  case lookup iid $ map swap $ EM.assocs lSlots of+    Nothing -> do+      let l = assignSlot lSlots+          f = EM.insert l iid+          newSlots = ItemSlots $ EM.adjust f slore itemSlots+      modifySession $ \sess -> sess {sslots = newSlots}+    Just _l -> return ()  -- slot already assigned++data Threat =+    ThreatNone+  | ThreatUnarmed+  | ThreatArmed+  | ThreatAnotherUnarmed+  | ThreatAnotherArmed+  deriving Eq++createActorUI :: MonadClientUI m => Bool -> ActorId -> Actor -> m ()+createActorUI born aid body = do+  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui+  side <- getsClient sside+  factionD <- getsState sfactionD+  let fact = factionD EM.! bfid body+  localTime <- getsState $ getLocalTime $ blid body+  itemFull@ItemFull{itemBase, itemKind} <- getsState $ itemToFull (btrunk body)+  actorUI <- getsSession sactorUI+  let arItem = aspectRecordFull itemFull+  unless (aid `EM.member` actorUI) $ do+    UIOptions{uHeroNames} <- getsSession sUIOptions+    let baseColor = flavourToColor $ jflavour itemBase+        basePronoun | not (bproj body)+                      && IK.isymbol itemKind == '@'+                      && fhasGender (gplayer fact) = "he"+                    | otherwise = "it"+        nameFromNumber fn k = if k == 0+                              then makePhrase [MU.Ws $ MU.Text fn, "Captain"]+                              else fn <+> tshow k+        heroNamePronoun k =+          if gcolor fact /= Color.BrWhite+          then (nameFromNumber (fname $ gplayer fact) k, "he")+          else fromMaybe (nameFromNumber (fname $ gplayer fact) k, "he")+               $ lookup k uHeroNames+        (n, bsymbol) =+          if | bproj body -> (0, if IA.checkFlag Ability.Blast arItem+                                 then IK.isymbol itemKind+                                 else '*')+             | baseColor /= Color.BrWhite -> (0, IK.isymbol itemKind)+             | otherwise -> case bnumber body of+                 Nothing ->+                   error $ "numbered actor without server-assigned number"+                           `showFailure` (aid, body)+                 Just bn -> (bn, if 0 < bn && bn < 10+                                 then Char.intToDigit bn+                                 else '@')+        (object1, object2) =+          partItemShortest rwidth (bfid body) factionD localTime+                           itemFull quantSingle+        (bname, bpronoun) =+          if | bproj body ->+               let adj = case btrajectory body of+                     Just (tra, _) | length tra < 5 -> "falling"+                     _ -> "flying"+               in (makePhrase [adj, object1, object2], basePronoun)+             | baseColor /= Color.BrWhite ->+               (makePhrase [object1, object2], basePronoun)+             | otherwise -> heroNamePronoun n+        bcolor | bproj body = if IA.checkFlag Ability.Blast arItem+                              then baseColor+                              else Color.BrWhite+               | baseColor == Color.BrWhite = gcolor fact+               | otherwise = baseColor+        bUI = ActorUI{..}+    modifySession $ \sess ->+      sess {sactorUI = EM.insert aid bUI actorUI}+  mapM_ (\(iid, store) -> do+           let c = if not (bproj body) && iid == btrunk body+                   then CTrunk (bfid body) (blid body) (bpos body)+                   else CActor aid store+           updateItemSlot c iid+           recordItemLid iid c)+        ((btrunk body, CEqp)  -- store will be overwritten, unless projectile+         : filter ((/= btrunk body) . fst) (getCarriedIidCStore body))+  if | bproj body -> do+       when (bfid body /= side)+         stopPlayBack+       pushFrame False  -- make sure first (seen (again)) position displayed+     | bfid body == side -> do+       let upd = ES.insert aid+       modifySession $ \sess -> sess {sselected = upd $ sselected sess}+       unless (EM.null actorUI) $ do  -- don't announce the very first party member+         when born $ do+           let verb = "join you"+           aidVerbMU MsgSpottedActor aid verb+           msgAdd MsgTutorialHint "You survive this mission, or die trying, as a team. After a few moves, feel free to switch the controlled teammate (marked on the map with the yellow box) using the Tab key to another party member (marked with a green box)."  -- assuming newbies don't remap their keys+           animate (blid body) $ actorX (bpos body)+     | otherwise -> do+       -- Don't spam if the actor was already visible+       -- (but, e.g., on a tile that is invisible this turn+       -- (in that case move is broken down to lose+spot)+       -- or on a distant tile, via teleport while the observer+       -- teleported, too).+       lastLost <- getsSession slastLost+       if ES.member aid lastLost+       then markDisplayNeeded (blid body)+       else do+         stopPlayBack+         let verb = if born then "appear suddenly" else "be spotted"+         threat <-+           if isFoe (bfid body) fact side then do+             -- Aim even if nobody can shoot at the enemy.+             -- Let's home in on him and then we can aim or melee.+             -- We set permit to False, because it's technically+             -- very hard to check aimability here, because we are+             -- in-between turns and, e.g., leader's move has not yet+             -- been taken into account.+             xhair <- getsSession sxhair+             case xhair of+               Just (TVector _) -> return ()  -- explicitly set; keep it+               _ -> modifySession $ \sess ->+                      sess { sxhair = Just $ TEnemy aid+                           , sitemSel = Nothing } -- reset flinging totally+             foes <- getsState $ foeRegularList side (blid body)+             itemsSize <- getsState $ guardItemSize body+             if length foes <= 1 then+               if itemsSize == 0 then do+                 msgAdd MsgSpottedThreat "You are not alone!"+                 return ThreatUnarmed+               else do+                 msgAdd MsgSpottedThreat "Armed intrusion ahead!"+                 return ThreatArmed+             else+               if itemsSize == 0 then+                 return ThreatAnotherUnarmed+               else do+                 msgAdd MsgSpottedThreat "Another threat, armed!"+                 return ThreatAnotherArmed+           else return ThreatNone  -- member of neutral faction+         aidVerbMU MsgSpottedActor aid verb+         friendAssocs <- getsState $ friendRegularAssocs side (blid body)+         case threat of+           ThreatNone -> return ()  -- too rare to care ATM+           ThreatUnarmed ->+             msgAdd MsgTutorialHint "Enemies are normally dealt with using melee (by bumping when adjacent) or ranged combat (by 'f'linging items at them)."+           ThreatArmed ->+             msgAdd MsgTutorialHint "Enemies can be dealt with not only via combat, but also with clever use of terrain effects, stealth (not emitting nor reflecting light) or hasty retreat (particularly when foes are asleep or drowsy)."+           _ | length friendAssocs <= 1 -> return ()  -- one member on level+           ThreatAnotherUnarmed ->+             msgAdd MsgTutorialHint "When dealing with groups of enemies, remember than you fight as a team. Switch the pointman (marked on the map with the yellow box) using the Tab key until you move each teammate to a tactically advantageous position. Avoid meleeing alone."+           ThreatAnotherArmed ->+             msgAdd MsgTutorialHint "When dealing with groups of armed enemies, remember than you fight as a team. Switch the pointman (marked on the map with the yellow box) using the Tab key until you move each teammate to a tactically advantageous position. Retreat, if necessary to form a front line. Soften the foes with missiles, especially of exploding kind."+         animate (blid body) $ actorX (bpos body)++destroyActorUI :: MonadClientUI m => Bool -> ActorId -> Actor -> m ()+destroyActorUI destroy aid b = do+  trunk <- getsState $ getItemBody $ btrunk b+  let baseColor = flavourToColor $ jflavour trunk+  unless (baseColor == Color.BrWhite) $  -- keep setup for heroes, etc.+    modifySession $ \sess -> sess {sactorUI = EM.delete aid $ sactorUI sess}+  let dummyTarget = TPoint TKnown (blid b) (bpos b)+      affect tgt = case tgt of+        Just (TEnemy a) | a == aid -> Just $+          if destroy then+            -- If *really* nothing more interesting, the actor will+            -- go to last known location to perhaps find other foes.+            dummyTarget+          else+            -- If enemy only hides (or we stepped behind obstacle) find him.+            TPoint (TEnemyPos a) (blid b) (bpos b)+        Just (TNonEnemy a) | a == aid -> Just dummyTarget+        _ -> tgt+  modifySession $ \sess -> sess {sxhair = affect $ sxhair sess}+  unless (bproj b || destroy) $+    modifySession $ \sess -> sess {slastLost = ES.insert aid $ slastLost sess}+  side <- getsClient sside+  fact <- getsState $ (EM.! side) . sfactionD+  let gameOver = isJust $ gquit fact  -- we are the UI faction, so we determine+  unless gameOver $ do+    when (bfid b == side && not (bproj b)) $ do+      stopPlayBack+      let upd = ES.delete aid+      modifySession $ \sess -> sess {sselected = upd $ sselected sess}+      when destroy $ do+        mleader <- getsClient sleader+        when (isJust mleader)+          -- This is especially handy when the dead actor was a leader+          -- on a different level than the new one:+          clearAimMode+    -- If pushed, animate spotting again, to draw attention to pushing.+    markDisplayNeeded (blid b)++spotItemBag :: forall m. MonadClientUI m+            => Bool -> Container -> ItemBag -> m ()+spotItemBag verbose c bag = do+  -- This is due to a move, or similar, which will be displayed,+  -- so no extra @markDisplayNeeded@ needed here and in similar places.+  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui+  side <- getsClient sside+  getKind <- getsState $ flip getIidKindId+  lid <- getsState $ lidFromC c+  localTime <- getsState $ getLocalTime lid+  factionD <- getsState sfactionD+  -- Queried just once, so many copies of a new item can be reported. OK.+  ItemSlots itemSlots <- getsSession sslots+  sxhairOld <- getsSession sxhair+  let resetXhair = case c of+        CFloor _ p -> case sxhairOld of+          Just TEnemy{} -> return ()  -- probably too important to overwrite+          Just (TPoint TEnemyPos{} _ _) -> return ()+          Just (TPoint TStash{} _ _) -> return ()+          Just (TVector _) -> return ()  -- explicitly set; keep it+          _ -> do+            -- Don't steal xhair if it's only an item on another level.+            -- For enemies, OTOH, capture xhair to alarm player.+            lidV <- viewedLevelUI+            when (lid == lidV) $ do+              bagFloor <- getsState $ getFloorBag lid p+              modifySession $ \sess ->+                sess { sxhair = Just $ TPoint (TItem bagFloor) lidV p+                     , sitemSel = Nothing }  -- reset flinging totally+        _ -> return ()+      locatedWhere = ppContainer factionD c+      beLocated = MU.Text $+        "be located" <+> if locatedWhere == ppContainer EM.empty c+                         then ""  -- boring+                         else locatedWhere+      subjectMaybe :: (ItemId, ItemQuant) -> m (Maybe (Int, MU.Part, MU.Part))+      subjectMaybe (iid, kit@(k, _)) = do+        recordItemLid iid c+        itemFull <- getsState $ itemToFull iid+        let arItem = aspectRecordFull itemFull+            slore = IA.loreFromContainer arItem c+        case lookup iid $ map swap $ EM.assocs $ itemSlots EM.! slore of+          Nothing -> do  -- never seen or would have a slot+            updateItemSlot c iid+            case c of+              CFloor{} -> do+                let subjectShort = partItemWsShortest rwidth side factionD k+                                                      localTime itemFull kit+                    subjectLong = partItemWsLong rwidth side factionD k+                                                 localTime itemFull kit+                return $ Just (k, subjectShort, subjectLong)+              _ -> return Nothing+          _ -> return Nothing  -- this item or another with the same @iid@+                               -- seen already (has a slot assigned); old news+      -- @SortOn@ less efficient here, because function cheap.+      sortItems = sortOn (getKind . fst)+      sortedAssocs = sortItems $ EM.assocs bag+  subjectMaybes <- mapM subjectMaybe sortedAssocs+  let subjects = catMaybes subjectMaybes+      sendMsg plural = do+        let subjectShort = MU.WWandW $ map (\(_, part, _) -> part) subjects+            subjectLong = MU.WWandW $ map (\(_, _, part) -> part) subjects+            msg subject =+              if plural+              then makeSentence [MU.SubjectVerb MU.PlEtc MU.Yes+                                                subject beLocated]+              else makeSentence [MU.SubjectVerbSg subject beLocated]+            msgShort = msg subjectShort+            msgLong = msg subjectLong+            dotsIfShorter = if msgShort == msgLong then "" else ".."+        resetXhair+        msgAddDistinct MsgSpottedItem (msgShort <> dotsIfShorter, msgLong)+  case subjects of+    [] -> return ()+    [(1, _, _)] -> sendMsg False+    _ -> sendMsg True+  when verbose $ case c of+    CActor aid store -> do+      let verb = MU.Text $ verbCStore store+      b <- getsState $ getActorBody aid+      fact <- getsState $ (EM.! bfid b) . sfactionD+      let underAI = isAIFact fact+      mleader <- getsClient sleader+      if Just aid == mleader && not underAI then+        manyItemsAidVerbMU MsgItemMovement aid verb sortedAssocs Right+      else when (not (bproj b) && bhp b > 0) $  -- don't announce death drops+        manyItemsAidVerbMU MsgItemMovement aid verb sortedAssocs (Left . Just)+    _ -> return ()++recordItemLid :: MonadClientUI m => ItemId -> Container -> m ()+recordItemLid iid c = do+  mjlid <- getsSession $ EM.lookup iid . sitemUI+  when (isNothing mjlid) $ do+    lid <- getsState $ lidFromC c+    modifySession $ \sess ->+      sess {sitemUI = EM.insert iid lid $ sitemUI sess}++moveActor :: MonadClientUI m => ActorId -> Point -> Point -> m ()+moveActor aid source target = do+  -- If source and target tile distant, assume it's a teleportation+  -- and display an animation. Note: jumps and pushes go through all+  -- intervening tiles, so won't be considered. Note: if source or target+  -- not seen, the (half of the) animation would be boring, just a delay,+  -- not really showing a transition, so we skip it (via 'breakUpdAtomic').+  -- The message about teleportation is sometimes shown anyway, just as the X.+  body <- getsState $ getActorBody aid+  if adjacent source target+  then markDisplayNeeded (blid body)+  else do+    let ps = (source, target)+    animate (blid body) $ teleport ps+  lookAtMove aid+  stopAtMove aid++displaceActorUI :: MonadClientUI m => ActorId -> ActorId -> m ()+displaceActorUI source target = do+  mleader <- getsClient sleader+  sb <- getsState $ getActorBody source+  tb <- getsState $ getActorBody target+  spart <- partActorLeader source+  tpart <- partActorLeader target+  let msgClass = if mleader `elem` map Just [source, target]+                 then MsgActionMajor  -- to interrupt run after a displace;+                 else MsgActionMinor  -- configurable, animation is feedback+      msg = makeSentence [MU.SubjectVerbSg spart "displace", tpart]+  msgAdd msgClass msg+  lookAtMove source+  stopAtMove source+  when (bfid sb /= bfid tb) $ do+    lookAtMove target  -- in case only this one is ours+    stopAtMove target+  side <- getsClient sside+  -- Ours involved, but definitely not requested by player via UI.+  when (side `elem` [bfid sb, bfid tb] && mleader /= Just source) stopPlayBack+  let ps = (bpos tb, bpos sb)+  animate (blid sb) $ swapPlaces ps++-- @UpdMoveItem@ is relatively rare (except within the player's faction),+-- but it ensures that even if only one of the stores is visible+-- (e.g., stash floor is not or actor posision is not), some messages+-- will be printed (via verbose @UpdLoseItem@).+moveItemUI :: MonadClientUI m+           => ItemId -> Int -> ActorId -> CStore -> CStore+           -> m ()+moveItemUI iid k aid cstore1 cstore2 = do+  let verb = MU.Text $ verbCStore cstore2+  b <- getsState $ getActorBody aid+  fact <- getsState $ (EM.! bfid b) . sfactionD+  let underAI = isAIFact fact+  mleader <- getsClient sleader+  ItemSlots itemSlots <- getsSession sslots+  case lookup iid $ map swap $ EM.assocs $ itemSlots EM.! SItem of+    Just _l ->+      -- So far organs can't be put into stash, so no need to call+      -- @updateItemSlot@ to add or reassign lore category.+      if cstore1 == CGround && Just aid == mleader && not underAI then+        itemAidVerbMU MsgActionMajor aid verb iid (Right k)+      else when (not (bproj b) && bhp b > 0) $  -- don't announce death drops+        itemAidVerbMU MsgActionMajor aid verb iid (Left k)+    Nothing -> error $+      "" `showFailure` (iid, k, aid, cstore1, cstore2)++-- The item may be used up already and so not present in the container,+-- e.g., if the item destroyed itself. This is OK. Message is still needed.+discover :: MonadClientUI m => Container -> ItemId -> m ()+discover c iid = do+  COps{coitem} <- getsState scops+  CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui+  lid <- getsState $ lidFromC c+  globalTime <- getsState stime+  localTime <- getsState $ getLocalTime lid+  itemFull <- getsState $ itemToFull iid+  bag <- getsState $ getContainerBag c+  side <- getsClient sside+  factionD <- getsState sfactionD+  (noMsg, nameWhere) <- case c of+    CActor aidOwner storeOwner -> do+      bOwner <- getsState $ getActorBody aidOwner+      name <- if bproj bOwner+              then return []+              else ppContainerWownW partActorLeader True c+      let arItem = aspectRecordFull itemFull+          inMetaGame = IA.checkFlag Ability.MetaGame arItem+          isOurOrgan = bfid bOwner == side+                       && storeOwner == COrgan+                       && not inMetaGame+            -- assume own faction organs known intuitively,+            -- except backstories and other meta game items+      return (isOurOrgan, name)+    CTrunk _ _ p | p == originPoint -> return (True, [])+      -- the special reveal at game over, using fake @CTrunk@; don't spam+    _ -> return (False, [])+  let kit = EM.findWithDefault quantSingle iid bag+              -- may be used up by that time+      knownName = makePhrase+        [partItemMediumAW rwidth side factionD localTime itemFull kit]+      flav = flavourToName $ jflavour $ itemBase itemFull+      (object1, object2) =+        partItemShortest rwidth side factionD localTime itemFull kit+      name1 = makePhrase [object1, object2]+      -- Make sure the two names in the message differ.+      (ikObvious, itemKind) = case jkind $ itemBase itemFull of+        IdentityObvious ik -> (True, ik)+        IdentityCovered _ix ik -> (False, ik)+          -- fake kind (template); OK, we talk about appearances+      name2 = IK.iname $ okind coitem itemKind+      name = if ikObvious && T.unwords (tail (T.words knownName)) /= name1+             then name1  -- avoid "a pair turns out to be"+             else name2  -- avoid "chip of scientific explanation"+      unknownName = MU.Phrase $ [MU.Text flav, MU.Text name] ++ nameWhere+      msg = makeSentence+        [ "the"+        , MU.SubjectVerbSg unknownName "turn out to be"+        , MU.Text knownName ]+  unless (noMsg || globalTime == timeZero) $  -- no spam about initial equipment+    msgAdd MsgItemDiscovery msg++ppHearMsg :: MonadClientUI m => Maybe Int -> HearMsg -> m Text+ppHearMsg distance hearMsg = case hearMsg of+  HearUpd cmd -> do+    COps{coTileSpeedup} <- getsState scops+    let sound = case cmd of+          UpdDestroyActor{} -> "shriek"+          UpdCreateItem{} -> "clatter"+          UpdTrajectory{} -> "thud"  -- A non-blast projectle hits a tile.+          UpdAlterTile _ _ fromTile toTile ->+            if | Tile.isOpenable coTileSpeedup fromTile+                 && Tile.isClosable coTileSpeedup toTile+                 || Tile.isClosable coTileSpeedup fromTile+                    && Tile.isOpenable coTileSpeedup toTile -> "creaking sound"+               | Tile.isWalkable coTileSpeedup fromTile+                 && Tile.isWalkable coTileSpeedup toTile -> "splash"+               | otherwise -> "rumble"+          UpdAlterExplorable _ k ->+            if k > 0 then "grinding noise" else "fizzing noise"+          _ -> error $ "" `showFailure` cmd+        adjective = MU.Text $ ppHearDistanceAdjective distance+        msg = makeSentence ["you hear", MU.AW $ MU.Phrase [adjective, sound]]+    return $! msg+  HearStrike ik -> do+    COps{coitem} <- getsState scops+    let verb = IK.iverbHit $ okind coitem ik+        adverb = MU.Text $ ppHearDistanceAdverb distance+        msg = makeSentence [ "you", adverb, "hear something"+                           , MU.Text verb, "someone" ]+    return $! msg+  HearSummon isProj grp p -> do+    let verb = if isProj then "something lure" else "somebody 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+        adverb = MU.Text $ ppHearDistanceAdverb distance+    return $! makeSentence ["you", adverb, "hear", verb, object]+  HearCollideTile -> do+    let adverb = MU.Text $ ppHearDistanceAdverb distance+    return $! makeSentence ["you", adverb, "hear someone crash into something"]+  HearTaunt t -> do+    let adverb = MU.Text $ ppHearDistanceAdverb distance+    return $! makePhrase ["You", adverb, "overhear", MU.Text t]++ppHearDistanceAdjective :: Maybe Int -> Text+ppHearDistanceAdjective Nothing = "indistinct"+ppHearDistanceAdjective (Just 0) = "very close"+ppHearDistanceAdjective (Just 1) = "close"+ppHearDistanceAdjective (Just 2) = ""+ppHearDistanceAdjective (Just 3) = "remote"+ppHearDistanceAdjective (Just 4) = "distant"+ppHearDistanceAdjective (Just _) = "far-off"++ppHearDistanceAdverb :: Maybe Int -> Text+ppHearDistanceAdverb Nothing = "indistinctly"+ppHearDistanceAdverb (Just 0) = "very clearly"+ppHearDistanceAdverb (Just 1) = "clearly"+ppHearDistanceAdverb (Just 2) = ""+ppHearDistanceAdverb (Just 3) = "remotely"+ppHearDistanceAdverb (Just 4) = "distantly"+ppHearDistanceAdverb (Just _) = "barely"
engine-src/Game/LambdaHack/Common/Actor.hs view
@@ -21,7 +21,6 @@ import           Data.Binary import qualified Data.EnumMap.Strict as EM import           Data.Int (Int64)-import           Data.Ratio import           GHC.Generics (Generic)  import           Game.LambdaHack.Common.Item@@ -31,7 +30,6 @@ import           Game.LambdaHack.Common.Types import           Game.LambdaHack.Common.Vector 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 @@ -266,27 +264,29 @@   let bagCarried (cstore, bag) = map (,cstore) $ EM.keys bag   in concatMap bagCarried [(CEqp, beqp b), (COrgan, borgan b)] --- | Chance that a new monster is generated. Depends on the number--- of monsters already present, and on the level depth and its cave kind.-monsterGenChance :: Dice.AbsDepth -> Dice.AbsDepth -> Int -> Int -> Rnd Bool+-- | Chance, in parts per million, that a new monster is generated.+-- Depends on the number of monsters already present, and on the level depth+-- and its cave kind.+--+-- Note that sometimes monsters spawn in groups, increasing danger,+-- but many monsters are generated asleep, decreasing initial danger.+monsterGenChance :: Dice.AbsDepth -> Dice.AbsDepth -> Int -> Int -> Int monsterGenChance (Dice.AbsDepth ldepth) (Dice.AbsDepth totalDepth)                  lvlSpawned actorCoeff =-  assert (totalDepth > 0 && ldepth > 0) $-    -- The sustained spawn speed is now trebled compared to the comment below,-    -- to compensate for some monsters generated asleep:-    ---    -- Heroes have to endure a level depth-sized wave of immediate-    -- spawners for each level and only then the monsters start+  assert (totalDepth > 0 && ldepth > 0) $  -- ensured by content validation+    -- Heroes have to endure a level-depth-proportional wave of immediate+    -- spawners for each level. Then the monsters start     -- to trickle more and more slowly, at the speed dictated-    -- by @actorCoeff@ specified in cave kind.-    -- On level 1/10, first 4 monsters spawn immediately, at level 5/10,-    -- 8 spawn immediately. In general at level n, n+3 spawn at once.+    -- by @actorCoeff@ specified in cave kind. Finally, spawning flattens out.     let scaledDepth = ldepth * 10 `div` totalDepth         -- Never spawn too rarely so that camping is never safe.-        maxCoeff = 100 * 30  -- safe level after 30 spawns flattens out-        coeff = min maxCoeff $ actorCoeff * (lvlSpawned - scaledDepth - 2)-    in chance $ 3 % intCast (coeff `max` 1)-         -- 3 --- trebled+        maxCoeff = 100 * 30  -- spawning on a level with benign @actorCoeff@+                             -- flattens out after 30 spawns+        coeff = max 1 $ min maxCoeff $ actorCoeff * (lvlSpawned - scaledDepth - 2)+        million = 1000000+    in 2 * million `div` coeff+         -- @2@ added to compensate for monsters generated asleep,+         -- without decreasing each @actorCoeff@ in content  -- | How long until an actor's smell vanishes from a tile. smellTimeout :: Delta Time
engine-src/Game/LambdaHack/Common/Area.hs view
@@ -24,6 +24,7 @@                           else Nothing  fromArea :: Area -> (X, Y, X, Y)+{-# INLINE fromArea #-} fromArea (Area x0 y0 x1 y1) = (x0, y0, x1, y1)  -- Funny thing, Trivial area, a point, has span 1 in each dimension.@@ -37,10 +38,9 @@ isTrivialArea (Area x0 y0 x1 y1) = x0 == x1 && y0 == y1  -- | Checks that a point belongs to an area.-inside :: Point -> Area -> Bool+inside :: Area -> Point -> Bool {-# INLINE inside #-}-inside (Point x y) (Area x0 y0 x1 y1) =-  x1 >= x && x >= x0 && y1 >= y && y >= y0+inside = insideP . fromArea  -- | Shrink the given area on all fours sides by the amount. shrink :: Area -> Maybe Area
engine-src/Game/LambdaHack/Common/ClientOptions.hs view
@@ -54,9 +54,13 @@       -- ^ Start a new game, overwriting the save file.   , sbenchmark        :: Bool       -- ^ Don't create directories and files and show time stats.+  , sbenchMessages    :: Bool+      -- ^ Display messages in realistic was under AI control (e.g., for benchmarking).   , stitle            :: Maybe String   , ssavePrefixCli    :: String       -- ^ Prefix of the save game file name.+  , sfrontendANSI     :: Bool+      -- ^ Whether to use the ANSI frontend.   , sfrontendTeletype :: Bool       -- ^ Whether to use the stdout/stdin frontend.   , sfrontendNull     :: Bool@@ -90,8 +94,10 @@   , snoAnim = Nothing   , snewGameCli = False   , sbenchmark = False+  , sbenchMessages = False   , stitle = Nothing   , ssavePrefixCli = ""+  , sfrontendANSI = False   , sfrontendTeletype = False   , sfrontendNull = False   , sfrontendLazy = False
engine-src/Game/LambdaHack/Common/Faction.hs view
@@ -134,29 +134,19 @@   let skillsOther = fskillsOther $ gplayer fact   in Ability.getSk Ability.SkMove skillsOther >= 0      || case fleaderMode (gplayer fact) of-          LeaderNull -> True-          LeaderAI AutoLeader{} -> True-          LeaderUI AutoLeader{..} -> autoDungeon || autoLevel+          Nothing -> True+          Just AutoLeader{..} -> autoDungeon || autoLevel  isAIFact :: Faction -> Bool-isAIFact fact =-  case fleaderMode (gplayer fact) of-    LeaderNull -> True-    LeaderAI _ -> True-    LeaderUI _ -> False+isAIFact fact = funderAI (gplayer fact)  autoDungeonLevel :: Faction -> (Bool, Bool) autoDungeonLevel fact = case fleaderMode (gplayer fact) of-                          LeaderNull -> (False, False)-                          LeaderAI AutoLeader{..} -> (autoDungeon, autoLevel)-                          LeaderUI AutoLeader{..} -> (autoDungeon, autoLevel)+                          Nothing -> (False, False)+                          Just AutoLeader{..} -> (autoDungeon, autoLevel)  automatePlayer :: Bool -> Player -> Player-automatePlayer st pl =-  let autoLeader False Player{fleaderMode=LeaderAI auto} = LeaderUI auto-      autoLeader True Player{fleaderMode=LeaderUI auto} = LeaderAI auto-      autoLeader _ Player{fleaderMode} = fleaderMode-  in pl {fleaderMode = autoLeader st pl}+automatePlayer funderAI pl = pl {funderAI}  -- | Check if factions are at war. Assumes symmetry. isFoe :: FactionId -> Faction -> FactionId -> Bool
engine-src/Game/LambdaHack/Common/Item.hs view
@@ -51,12 +51,12 @@ -- is available elsewhere (both @IdentityObvious@ and @IdentityCovered@ -- items may or may not need identification of their aspect record). data Item = Item-  { jkind    :: ItemIdentity  -- ^ the kind of the item, or an indiretion-  , jfid     :: Maybe FactionId-                              -- ^ the faction that created the item, if any-  , jflavour :: Flavour       -- ^ flavour, always the real one, not hidden;-                              --   people may not recognize shape, but they-                              --   remember colour and old vs fancy look+  { jkind    :: ItemIdentity     -- ^ the kind of the item, or an indirection+  , jfid     :: Maybe FactionId  -- ^ the faction that created the item, if any+  , jflavour :: Flavour          -- ^ flavour, always the real one,+                                 --   it's not hidden; people may not recognize+                                 --   shape, but they remember colour and old+                                 --   vs fancy look   }   deriving (Show, Eq, Generic) @@ -305,7 +305,7 @@             -- to value the effects at more than 0, even though they are strong             -- and also to prefer weapons with burn or wound over pure damage,             -- which is a good rule of thumb before late game at least.-            hasEffect = any (\eff -> IK.forApplyEffect eff)+            hasEffect = any IK.forApplyEffect                             (IK.ieffects $ itemKind itemFull)             ncha = ncharges localTime kit         in if ignoreCharges || ncha > 0
engine-src/Game/LambdaHack/Common/JSFile.hs view
@@ -1,10 +1,15 @@ {-# LANGUAGE JavaScriptFFI #-} -- | Saving/loading to JS storeage, mimicking operations on files. module Game.LambdaHack.Common.JSFile-  ( encodeEOF, strictDecodeEOF+  (+#ifdef USE_JSFILE+-- to molify doctest, but don't break stylish-haskell parsing+    encodeEOF, strictDecodeEOF   , tryCreateDir, doesFileExist, tryWriteFile, readFile, renameFile+#endif   ) where +#ifdef USE_JSFILE import Prelude ()  import Game.LambdaHack.Core.Prelude@@ -100,3 +105,4 @@     Just item -> do       setItem storage path2 item  -- overwrites       removeItem storage path+#endif
engine-src/Game/LambdaHack/Common/Level.hs view
@@ -149,10 +149,10 @@           `blame` "null floors found" `swith` m) m  hashConsSingle :: ItemFloor -> ItemFloor-hashConsSingle m =+hashConsSingle =   EM.map (EM.map (\case                     (1, []) -> quantSingle-                    kit -> kit)) m+                    kit -> kit))  assertSparseProjectiles :: ProjectileMap -> ProjectileMap assertSparseProjectiles m =@@ -248,7 +248,7 @@           search !k = do             pxyRelative <- randomR0 (xspan * yspan - 1)             -- Here we can't use @fromEnum@ and/or work with the @Int@-            -- representation, because the span is different than @rXmax@.+            -- representation, because the span is different than @rWidthMax@.             let Point{..} = punindex xspan pxyRelative                 pos = Point (x0 + px) (y0 + py)                 tile = ltile PointArray.! pos@@ -266,7 +266,8 @@ -- the level in the order of path distance from the starting position (BFS). -- The starting position needn't be passable and is always included. nearbyPassablePoints :: COps -> Level -> Point -> [Point]-nearbyPassablePoints cops@COps{corule=RuleContent{rXmax, rYmax}} lvl start =+nearbyPassablePoints cops@COps{corule=RuleContent{rWidthMax, rHeightMax}}+                     lvl start =   let passable p = Tile.isEasyOpen (coTileSpeedup cops) (lvl `at` p)       -- The error is mostly probably caused by place content creating       -- enclosed spaces in conjunction with map edges. To verify,@@ -275,7 +276,7 @@                          else let offset = fromEnum start `mod` length l                               in drop offset l ++ take offset l       passableVic p = semiRandomWrap $ filter passable-                      $ vicinityBounded rXmax rYmax p+                      $ vicinityBounded rWidthMax rHeightMax p       siftSingle :: Point                  -> (ES.EnumSet Point, [Point])                  -> (ES.EnumSet Point, [Point])
engine-src/Game/LambdaHack/Common/Misc.hs view
@@ -102,9 +102,11 @@   let name = takeWhile Char.isAlphaNum progName   getAppUserDataDirectory name +-- | Multiplies by a million. xM :: Int -> Int64-xM k = intCast k * 1000000+xM k = into @Int64 k * 1000000 +-- | Multiplies by a million, double precision. xD :: Double -> Double xD k = k * 1000000 
engine-src/Game/LambdaHack/Common/Point.hs view
@@ -3,7 +3,7 @@ module Game.LambdaHack.Common.Point   ( Point(..), PointI   , chessDist, euclidDistSq, adjacent, bla, fromTo-  , originPoint+  , originPoint, insideP   , speedupHackXSize #ifdef EXPOSE_INTERNAL     -- * Internal operations@@ -19,6 +19,7 @@ import           Data.Int (Int32) import qualified Data.Primitive.PrimArray as PA import           GHC.Generics (Generic)+import           Test.QuickCheck  import Game.LambdaHack.Definition.Defs @@ -39,8 +40,11 @@ speedupHackXSize = PA.primArrayFromList [80]  -- updated at program startup  -- | 2D points in cartesian representation. Coordinates grow to the right--- and down, so that the (0, 0) point is in the top-left corner of the screen.--- Coordinates are never negative.+-- and down, so that the (0, 0) point is in the top-left corner+-- of the screen. Coordinates are never negative+-- (unlike for 'Game.LambdaHack.Common.Vector.Vector')+-- and the @X@ coordinate never reaches the screen width as read+-- from 'speedupHackXSize'. data Point = Point   { px :: X   , py :: Y@@ -52,7 +56,9 @@  instance Binary Point where   put = put . (toIntegralCrash :: Int -> Int32) . fromEnum-  get = fmap (toEnum . (intCast :: Int32 -> Int)) get+  get = fmap (toEnum . (fromIntegralWrap :: Int32 -> Int)) get+    -- `fromIntegralWrap` is fine here, because we converted the integer+    -- in the opposite direction first, so it fits even in 31 bit `Int`  -- Note that @Ord@ on @Int@ is not monotonic wrt @Ord@ on @Point@. -- We need to keep it that way, because we want close xs to have close indexes,@@ -74,10 +80,36 @@                  (py, px) = n `quotRem` xsize              in Point{..} +instance Arbitrary Point where+  arbitrary = do+    let xsize = PA.indexPrimArray speedupHackXSize 0+    n <- getSize+    Point <$> choose (0, min n (xsize - 1))+          <*> choose (0, n)+ -- | Enumeration representation of @Point@. type PointI = Int +-- This is hidden from Haddock, but run by doctest:+-- $+-- prop> (toEnum :: PointI -> Point) (fromEnum p) == p+-- prop> (fromEnum :: Point -> PointI) (toEnum p) == p+ -- | The distance between two points in the chessboard metric.+--+-- >>> chessDist (Point 0 0) (Point 0 0)+-- 0+-- >>> chessDist (Point (-1) 0) (Point 0 0)+-- 1+-- >>> chessDist (Point (-1) 0) (Point (-1) 1)+-- 1+-- >>> chessDist (Point (-1) 0) (Point 0 1)+-- 1+-- >>> chessDist (Point (-1) 0) (Point 1 1)+-- 2+--+-- prop> chessDist p1 p2 >= 0+-- prop> chessDist p1 p2 ^ (2 :: Int) <= euclidDistSq p1 p2 chessDist :: Point -> Point -> Int chessDist (Point x0 y0) (Point x1 y1) = max (abs (x1 - x0)) (abs (y1 - y0)) @@ -94,18 +126,15 @@  -- | Bresenham's line algorithm generalized to arbitrary starting @eps@ -- (@eps@ value of 0 gives the standard BLA).--- Skips the source point and goes through the second point--- to the edge of the level. Gives @Nothing@ if the points are equal.--- The target is given as @Point@ to permit aiming out of the level,--- e.g., to get uniform distributions of directions for explosions--- close to the edge of the level.-bla :: X -> Y -> Int -> Point -> Point -> Maybe [Point]-bla rXmax rYmax eps source target =+-- Skips the source point and goes through the second point to infinity.+-- Gives @Nothing@ if the points are equal. The target is given as @Point@,+-- not @PointI@, to permit aiming out of the level, e.g., to get+-- uniform distributions of directions for explosions close to the edge+-- of the level.+bla :: Int -> Point -> Point -> Maybe [Point]+bla eps source target =   if source == target then Nothing-  else Just $-    let inBounds p@(Point x y) =-          rXmax > x && x >= 0 && rYmax > y && y >= 0 && p /= source-    in takeWhile inBounds $ tail $ blaXY eps source target+  else Just $ tail $ blaXY eps source target  -- | Bresenham's line algorithm generalized to arbitrary starting @eps@ -- (@eps@ value of 0 gives the standard BLA). Includes the source point@@ -143,3 +172,8 @@  originPoint :: Point originPoint = Point 0 0++-- | Checks that a point belongs to an area.+insideP :: (X, Y, X, Y) -> Point -> Bool+{-# INLINE insideP #-}+insideP (x0, y0, x1, y1) (Point x y) = x1 >= x && x >= x0 && y1 >= y && y >= y0
engine-src/Game/LambdaHack/Common/PointArray.hs view
@@ -25,6 +25,7 @@ import           Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Definition.Color as Color import           Game.LambdaHack.Definition.Defs+import qualified Game.LambdaHack.Definition.DefsInternal as DefsInternal  class ( Ord c, Eq (UnboxRep c), Ord (UnboxRep c), Bounded (UnboxRep c)       , Binary (UnboxRep c), U.Unbox (UnboxRep c) )@@ -44,8 +45,8 @@  instance UnboxRepClass (ContentId k) where   type UnboxRep (ContentId k) = Word16-  toUnboxRepUnsafe = fromContentId-  fromUnboxRep = toContentId+  toUnboxRepUnsafe = DefsInternal.fromContentId+  fromUnboxRep = DefsInternal.toContentId  instance UnboxRepClass Color.AttrCharW32 where   type UnboxRep Color.AttrCharW32 = Word32
engine-src/Game/LambdaHack/Common/ReqFailure.hs view
@@ -16,6 +16,7 @@ import qualified Game.LambdaHack.Common.ItemAspect as IA import           Game.LambdaHack.Common.Time import qualified Game.LambdaHack.Content.ItemKind as IK+import qualified Game.LambdaHack.Content.RuleKind as RK import qualified Game.LambdaHack.Definition.Ability as Ability import           Game.LambdaHack.Definition.Defs @@ -220,16 +221,19 @@          && skill < 3 -> False        | otherwise -> permittedPreciousAI calmE itemFull -permittedApply :: Time -> Int -> Bool -> Maybe CStore -> ItemFull -> ItemQuant+permittedApply :: RK.RuleContent -> Time -> Int -> Bool -> Maybe CStore+               -> ItemFull -> ItemQuant                -> Either ReqFailure Bool-permittedApply localTime skill calmE mstore+permittedApply corule localTime skill calmE mstore                itemFull@ItemFull{itemKind, itemSuspect} kit =   if | skill < 1 -> Left ApplyUnskilled      | skill < 2-       && IK.isymbol itemKind /= '"'-       && (IK.isymbol itemKind /= ','+       && IK.isymbol itemKind /= IK.rsymbolNecklace (RK.ritemSymbols corule)+       && (IK.isymbol itemKind /= IK.rsymbolFood (RK.ritemSymbols corule)            || mstore /= Just CGround) -> Left ApplyFood-     | skill < 3 && IK.isymbol itemKind == '?' -> Left ApplyRead+     | skill < 3+       && IK.isymbol itemKind == IK.rsymbolScroll (RK.ritemSymbols corule) ->+         Left ApplyRead      | skill < 4        && let arItem = aspectRecordFull itemFull           in IA.checkFlag Ability.Periodic arItem -> Left ApplyPeriodic
engine-src/Game/LambdaHack/Common/State.hs view
@@ -1,4 +1,5 @@--- | The common server and client basic game state type and its operations.+-- | The common, for server and clients, main game state type+-- and its operations. module Game.LambdaHack.Common.State   ( -- * Basic game state, local or global     State@@ -48,12 +49,20 @@ import qualified Game.LambdaHack.Definition.Ability as Ability import           Game.LambdaHack.Definition.Defs --- | View on the basic game state.--- The @remembered@ fields, in client copies of the state, carry only--- a subset of the full information that the server keeps.--- Clients never directly change their @State@, but apply--- atomic actions sent by the server to do so (and/or the server applies--- the actions to each client state in turn).+-- | The main game state, the basic one, pertaining to a single game,+-- not to a single playing session or an intersection of both.+-- This state persists between playing sessions, until the particular game ends.+-- Anything that persists between games is stored in server state,+-- client state or client UI session state.+--+-- Another differentiating property of this state is that it's kept+-- separately on the server and each of the clients (players, human or AI)+-- and separately updated, according to what each player can observe.+-- It's never updated directly, but always through atomic commands+-- ("CmdAtomic") that are filtered and interpreted differently+-- on server and on each client. Therefore, the type is a view on the+-- game state, not the real game state, except on the server that+-- alone stores the full game information. data State = State   { _sdungeon        :: Dungeon    -- ^ remembered dungeon   , _stotalDepth     :: Dice.AbsDepth@@ -162,7 +171,7 @@            , lembed = EM.empty            , lbig = EM.empty            , lproj = EM.empty-           , ltile = unknownTileMap larea outerId (rXmax corule) (rYmax corule)+           , ltile = unknownTileMap larea outerId (rWidthMax corule) (rHeightMax corule)            , lentry = EM.empty            , larea            , lsmell = EM.empty@@ -175,8 +184,8 @@            }  unknownTileMap :: Area -> ContentId TileKind -> X -> Y -> TileMap-unknownTileMap larea outerId rXmax rYmax =-  let unknownMap = PointArray.replicateA rXmax rYmax TK.unknownId+unknownTileMap larea outerId rWidthMax rHeightMax =+  let unknownMap = PointArray.replicateA rWidthMax rHeightMax TK.unknownId       outerUpdate = zip (areaInnerBorder larea) $ repeat outerId   in unknownMap PointArray.// outerUpdate 
engine-src/Game/LambdaHack/Common/Thread.hs view
@@ -11,7 +11,7 @@ import Control.Concurrent.MVar  -- Swiped from <http://www.haskell.org/ghc/docs/latest/html/libraries/base/Control-Concurrent.html>--- Ported to Async to link exceptions, to let travis tests fail.+-- Ported to Async to link exceptions, to let CI tests fail.  forkChild :: MVar [Async ()] -> IO () -> IO () forkChild children io = do
engine-src/Game/LambdaHack/Common/Time.hs view
@@ -60,7 +60,7 @@ timeTurn :: Time timeTurn = Time 500000 --- | This many ticks fits in a single second. Do not export,+-- | This many ticks fits in a single second. timeSecond :: Time timeSecond = Time $ timeTicks timeTurn * turnsInSecond @@ -142,18 +142,18 @@ -- | Scale the time vector by an @Int@ scalar value. timeDeltaScale :: Delta Time -> Int -> Delta Time {-# INLINE timeDeltaScale #-}-timeDeltaScale (Delta (Time t)) s = Delta (Time (t * intCast s))+timeDeltaScale (Delta (Time t)) s = Delta (Time (t * into @Int64 s))  -- | Take the given percent of the time vector. timeDeltaPercent :: Delta Time -> Int -> Delta Time {-# INLINE timeDeltaPercent #-} timeDeltaPercent (Delta (Time t)) s =-  Delta (Time (t * intCast s `div` 100))+  Delta (Time (t * into @Int64 s `div` 100))  -- | Divide a time vector. timeDeltaDiv :: Delta Time -> Int -> Delta Time {-# INLINE timeDeltaDiv #-}-timeDeltaDiv (Delta (Time t)) n = Delta (Time (t `div` intCast n))+timeDeltaDiv (Delta (Time t)) n = Delta (Time (t `div` into @Int64 n))  -- | Represent the main 10 thresholds of a time range by digits, -- given the total length of the time range.@@ -190,7 +190,7 @@ -- | Constructor for content definitions. toSpeed :: Int -> Speed {-# INLINE toSpeed #-}-toSpeed s = Speed $ intCast s * sInMs `div` 10+toSpeed s = Speed $ into @Int64 s * sInMs `div` 10  -- | Readable representation of speed in the format used in content definitions. fromSpeed :: Speed -> Int@@ -281,12 +281,12 @@ -- See <https://github.com/LambdaHack/LambdaHack/wiki/Item-statistics>. speedFromWeight :: Int -> Int -> Speed speedFromWeight !weight !throwVelocity =-  let w = intCast weight+  let w = into @Int64 weight       mpMs | w < 250 = sInMs * 20            | w < 1500 = sInMs * 20 * 1250 `div` (w + 1000)            | w < 10500 = sInMs * (11500 - w) `div` 1000            | otherwise = minimalSpeed * 2  -- move one step and drop-      v = mpMs * intCast throwVelocity `div` 100+      v = mpMs * into @Int64 throwVelocity `div` 100       -- We round down to the nearest multiple of 2M (unless the speed       -- is very low), to ensure both turns of flight cover the same distance       -- and that the speed matches the distance traveled exactly.
engine-src/Game/LambdaHack/Common/Vector.hs view
@@ -45,7 +45,9 @@  instance Binary Vector where   put = put . (toIntegralCrash :: Int -> Int32) . fromEnum-  get = fmap (toEnum . (intCast :: Int32 -> Int)) get+  get = fmap (toEnum . (fromIntegralWrap :: Int32 -> Int)) get+    -- `fromIntegralWrap` is fine here, because we converted the integer+    -- in the opposite direction first, so it fits even in 31 bit `Int`  -- Note that the conversion is not monotonic wrt the natural @Ord@ instance, -- to keep it in sync with Point.@@ -121,21 +123,16 @@                     assFail = error $ "not a unit vector" `showFailure` v                 in EM.findWithDefault assFail v m --- | Checks that a point belongs to an area.-insideP :: Point -> (X, Y, X, Y) -> Bool-{-# INLINE insideP #-}-insideP (Point x y) (x0, y0, x1, y1) = x1 >= x && x >= x0 && y1 >= y && y >= y0- -- | All (8 at most) closest neighbours of a point within an area. vicinityBounded :: X -> Y   -- ^ limit the search to this area                 -> Point    -- ^ position to find neighbours of                 -> [Point]-vicinityBounded rXmax rYmax p =-  if insideP p (1, 1, rXmax - 2, rYmax - 2)+vicinityBounded rWidthMax rHeightMax p =+  if insideP (1, 1, rWidthMax - 2, rHeightMax - 2) p   then vicinityUnsafe p   else [ res | dxy <- moves              , let res = shift p dxy-             , insideP res (0, 0, rXmax - 1, rYmax - 1) ]+             , insideP (0, 0, rWidthMax - 1, rHeightMax - 1) res ]  vicinityUnsafe :: Point -> [Point] {-# INLINE vicinityUnsafe #-}@@ -145,10 +142,10 @@ vicinityCardinal :: X -> Y   -- ^ limit the search to this area                  -> Point    -- ^ position to find neighbours of                  -> [Point]-vicinityCardinal rXmax rYmax p =+vicinityCardinal rWidthMax rHeightMax p =   [ res | dxy <- movesCardinal         , let res = shift p dxy-        , insideP res (0, 0, rXmax - 1, rYmax - 1) ]+        , insideP (0, 0, rWidthMax - 1, rHeightMax - 1) res ]  vicinityCardinalUnsafe :: Point -> [Point] vicinityCardinalUnsafe p = [ shift p dxy | dxy <- movesCardinal ]@@ -172,8 +169,8 @@  -- | Translate a point by a vector, but only if the result fits in an area. shiftBounded :: X -> Y -> Point -> Vector -> Point-shiftBounded rXmax rYmax pos v@(Vector xv yv) =-  if insideP pos (-xv, -yv, rXmax - xv - 1, rYmax - yv - 1)+shiftBounded rWidthMax rHeightMax pos v@(Vector xv yv) =+  if insideP (-xv, -yv, rWidthMax - xv - 1, rHeightMax - yv - 1) pos   then shift pos v   else pos @@ -186,9 +183,9 @@ -- | A list of points that a list of vectors leads to, bounded by level size. trajectoryToPathBounded :: X -> Y -> Point -> [Vector] -> [Point] trajectoryToPathBounded _ _ _ [] = []-trajectoryToPathBounded rXmax rYmax start (v : vs) =-  let next = shiftBounded rXmax rYmax start v-  in next : trajectoryToPathBounded rXmax rYmax next vs+trajectoryToPathBounded rWidthMax rHeightMax start (v : vs) =+  let next = shiftBounded rWidthMax rHeightMax start v+  in next : trajectoryToPathBounded rWidthMax rHeightMax next vs  -- | The vector between the second point and the first. We have --
engine-src/Game/LambdaHack/Server/Commandline.hs view
@@ -24,11 +24,12 @@ -- remaining commandline should be passed and parsed by the client to extract -- client and ui options from and singnal an error if anything was left. -import Game.LambdaHack.Common.ClientOptions-import Game.LambdaHack.Common.Faction-import Game.LambdaHack.Content.ModeKind-import Game.LambdaHack.Definition.Defs-import Game.LambdaHack.Server.ServerOptions+import           Game.LambdaHack.Common.ClientOptions+import           Game.LambdaHack.Common.Faction+import           Game.LambdaHack.Content.ModeKind+import           Game.LambdaHack.Definition.Defs+import qualified Game.LambdaHack.Definition.DefsInternal as DefsInternal+import           Game.LambdaHack.Server.ServerOptions  -- | Parser for server options from commandline arguments. serverOptionsPI :: ParserInfo ServerOptions@@ -64,22 +65,24 @@   sstopAfterGameOver <- stopAfterGameOverP   sprintEachScreen  <- printEachScreenP   sbenchmark        <- benchmarkP+  sbenchMessages    <- benchMessagesP   sdungeonRng       <- setDungeonRngP   smainRng          <- setMainRngP   sdumpInitRngs     <- dumpInitRngsP+  sdbgMsgCli        <- dbgMsgCliP   sdbgMsgSer        <- dbgMsgSerP+  slogPriority      <- logPriorityP   sassertExplored   <- assertExploredP   schosenFontset    <- chosenFontsetP   sallFontsScale    <- allFontsScaleP-  slogPriority      <- logPriorityP   smaxFps           <- maxFpsP   sdisableAutoYes   <- disableAutoYesP   snoAnim           <- noAnimP   ssavePrefixSer    <- savePrefixP+  sfrontendANSI     <- frontendANSIP   sfrontendTeletype <- frontendTeletypeP   sfrontendNull     <- frontendNullP   sfrontendLazy     <- frontendLazyP-  sdbgMsgCli        <- dbgMsgCliP    pure ServerOptions     {@@ -162,7 +165,7 @@   -- This ignores all but the first word of a game mode name   -- and assumes the fist word is present among its frequencies.   toGameMode :: String -> GroupName ModeKind-  toGameMode = GroupName . head . T.words . T.pack+  toGameMode = DefsInternal.GroupName . head . T.words . T.pack   nonEmptyStr :: ReadM String   nonEmptyStr = eitherReader $ \case     "" -> Left "name of game mode cannot be empty"@@ -218,6 +221,11 @@   switch (  long "benchmark"          <> help "Restrict file IO, print timing stats" ) +benchMessagesP :: Parser Bool+benchMessagesP =+  switch (  long "benchMessages"+         <> help "Display messages in realistic was under AI control (for benchmarks)" )+ setDungeonRngP :: Parser (Maybe SM.SMGen) setDungeonRngP = optional $   option auto (  long "setDungeonRng"@@ -235,11 +243,33 @@   switch (  long "dumpInitRngs"          <> help "Dump the RNG seeds used to initialize the game" ) +dbgMsgCliP :: Parser Bool+dbgMsgCliP =+  switch (  long "dbgMsgCli"+         <> help "Emit extra internal client debug messages" )+ dbgMsgSerP :: Parser Bool dbgMsgSerP =   switch (  long "dbgMsgSer"          <> help "Emit extra internal server debug messages" ) +logPriorityP :: Parser (Maybe Int)+logPriorityP = optional $+  option (auto >>= verifyLogPriority) $+       long "logPriority"+    <> showDefault+    <> value 5+    <> metavar "N"+    <> help ( "Log only messages of priority at least N, where 1 (all) is "+           ++ "the lowest and 5 logs errors only; use value 0 for testing on "+           ++ "CIs without graphics access; setting priority to 0 causes "+           ++ "SDL frontend to init and quit at once" )+  where+    verifyLogPriority n =+      if n >= 0 && n <= 5+      then return n+      else readerError "N has to be 0 or a positive integer not larger than 5"+ assertExploredP :: Parser (Maybe Int) assertExploredP = optional $ max 1 <$>   option auto (  long "assertExplored"@@ -264,23 +294,6 @@               <> metavar "D"               <> help "Display at most D frames per second" ) -logPriorityP :: Parser (Maybe Int)-logPriorityP = optional $-  option (auto >>= verifyLogPriority) $-       long "logPriority"-    <> showDefault-    <> value 5-    <> metavar "N"-    <> help ( "Log only messages of priority at least N, where 1 (all) is "-           ++ "the lowest and 5 logs errors only; use value 0 for testing on "-           ++ "CIs without graphics access; setting priority to 0 causes "-           ++ "SDL frontend to init and quit at once" )-  where-    verifyLogPriority n =-      if n >= 0 && n <= 5-      then return n-      else readerError "N has to be 0 or a positive integer not larger than 5"- disableAutoYesP :: Parser Bool disableAutoYesP =   switch (  long "disableAutoYes"@@ -300,6 +313,11 @@             <> value ""             <> help "Prepend PREFIX to all savefile names" ) +frontendANSIP :: Parser Bool+frontendANSIP =+  switch (  long "frontendANSI"+         <> help "Use the ANSI terminal frontend (best for screen readers)" )+ frontendTeletypeP :: Parser Bool frontendTeletypeP =   switch (  long "frontendTeletype"@@ -314,8 +332,3 @@ frontendLazyP =   switch (  long "frontendLazy"          <> help "Use frontend that not even computes frames (for benchmarks)" )--dbgMsgCliP :: Parser Bool-dbgMsgCliP =-  switch (  long "dbgMsgCli"-         <> help "Emit extra internal client debug messages" )
engine-src/Game/LambdaHack/Server/CommonM.hs view
@@ -1,16 +1,18 @@ {-# LANGUAGE TupleSections #-} -- | Server operations common to many modules. module Game.LambdaHack.Server.CommonM-  ( revealItems, generalMoveItem, deduceQuits, writeSaveAll, verifyCaches-  , deduceKilled, electLeader, setFreshLeader, updatePer, recomputeCachePer-  , projectFail, addActorFromGroup, registerActor, discoverIfMinorEffects-  , pickWeaponServer, currentSkillsServer, allGroupItems+  ( revealAll, generalMoveItem, deduceQuits+  , writeSaveAll, verifyCaches, deduceKilled, electLeader, setFreshLeader+  , updatePer, projectFail, addActorFromGroup, registerActor+  , discoverIfMinorEffects, pickWeaponServer, currentSkillsServer, allGroupItems   , addCondition, removeConditionSingle, addSleep, removeSleepSingle   , addKillToAnalytics #ifdef EXPOSE_INTERNAL     -- * Internal operations-  , containerMoveItem, quitF, keepArenaFact, anyActorsAlive, projectBla-  , addProjectile, addNonProjectile, addActorIid, getCacheLucid, getCacheTotal+  , revealItems, revealPerceptionLid, containerMoveItem, quitF, keepArenaFact+  , anyActorsAlive, updatePerFromNew, recomputeCachePer+  , projectBla, addProjectile, addNonProjectile, addActorIid+  , getCacheLucid, getCacheTotal #endif   ) where @@ -19,6 +21,7 @@ import Game.LambdaHack.Core.Prelude  import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES import qualified Data.IntMap.Strict as IM import           Data.Ratio @@ -26,6 +29,7 @@ import           Game.LambdaHack.Common.Actor import           Game.LambdaHack.Common.ActorState import           Game.LambdaHack.Common.Analytics+import           Game.LambdaHack.Common.Area import           Game.LambdaHack.Common.ClientOptions import           Game.LambdaHack.Common.Faction import           Game.LambdaHack.Common.Item@@ -44,10 +48,10 @@ import           Game.LambdaHack.Content.ItemKind (ItemKind) import qualified Game.LambdaHack.Content.ItemKind as IK import           Game.LambdaHack.Content.ModeKind-import           Game.LambdaHack.Content.RuleKind import           Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability import           Game.LambdaHack.Definition.Defs+import qualified Game.LambdaHack.Definition.DefsInternal as DefsInternal import           Game.LambdaHack.Server.Fov import           Game.LambdaHack.Server.ItemM import           Game.LambdaHack.Server.ItemRev@@ -109,6 +113,32 @@   mapM_ discoverSample $ EM.keys $ nonDupGen EM.! SCondition   mapM_ discoverSample $ EM.keys $ nonDupGen EM.! SBlast +revealAll :: MonadServerAtomic m => FactionId -> m ()+revealAll fid = do+  revealItems fid+  execUpdAtomic $ UpdMuteMessages fid True+  dungeon <- getsState sdungeon+  -- Perception needs to be sent explicitly, because normal management+  -- assumes an action must happen on a level to invalidate and regenerate+  -- perception on the level (and actors must survive!).+  -- Also, we'd rather hack here and in `verifyCaches` that complicate+  -- the already complex perception creation and caching code.+  mapM_ (revealPerceptionLid fid) $ EM.assocs dungeon+  execUpdAtomic $ UpdMuteMessages fid False++revealPerceptionLid :: MonadServerAtomic m+                    => FactionId -> (LevelId, Level) -> m ()+revealPerceptionLid fid (lid, lvl) = do+  let (x0, y0, x1, y1) = fromArea $ larea lvl+      fullSet = ES.fromDistinctAscList [ Point x y+                                       | y <- [y0 .. y1]+                                       , x <- [x0 .. x1] ]+      perNew = Perception+        { psight = PerVisible fullSet+        , psmell = PerSmelled ES.empty  -- don't obscure+        }+  updatePerFromNew fid lid perNew+ -- | Generate the atomic updates that jointly perform a given item move. generalMoveItem :: MonadStateRead m                 => Bool -> ItemId -> Int -> Container -> Container@@ -143,16 +173,17 @@     Just Conquer -> return ()     Just Escape -> return ()     _ -> do+      let !_A = assert (stOutcome status `notElem` [Camping, Restart]+                        `blame` "Camping and Restart are handled separately"+                        `swith` (stOutcome <$> oldSt, status, fid)) ()       -- This runs regardless of the _new_ status.       manalytics <-         if fhasUI $ gplayer fact then do           keepAutomated <- getsServer $ skeepAutomated . soptions           -- Try to remove AI control of the UI faction, to show gameover info.-          when (isAIFact fact-                && fleaderMode (gplayer fact) /= LeaderNull-                && not keepAutomated) $+          when (isAIFact fact && not keepAutomated) $             execUpdAtomic $ UpdAutoFaction fid False-          revealItems fid+          revealAll fid           -- Likely, by this time UI faction is no longer AI-controlled,           -- so the score will get registered.           registerScore status fid@@ -218,7 +249,7 @@  -- | Save game on server and all clients. writeSaveAll :: MonadServerAtomic m => Bool -> Bool -> m ()-writeSaveAll uiRequested evenForNoConfirmGames= do+writeSaveAll uiRequested evenForNoConfirmGames = do   bench <- getsServer $ sbenchmark . sclientOptions . soptions   noConfirmsGame <- isNoConfirmsGame   when (uiRequested@@ -244,7 +275,16 @@   ( fovLitLid, fovClearLid, fovLucidLid    ,perValidFid, perCacheFid, perFid ) <- getsState perFidInDungeon   rngs <- getsServer srngs  -- initial display may scroll off terminal memory-  let !_A7 = assert (sfovLitLid == fovLitLid+  factionD <- getsState sfactionD+  -- Perception off UI faction at game over is illegal (revealed to the player+  -- in 'revealAll'), which is fine, because it's never used.+  -- Don't verify perception in such cases. All the caches from which+  -- legal perception would be created at that point are legal and verified,+  -- which is almost as tight.+  let gameOverUI fact = fhasUI (gplayer fact)+                        && maybe False ((/= Camping) . stOutcome) (gquit fact)+      isGameOverUI = any gameOverUI $ EM.elems factionD+      !_A7 = assert (sfovLitLid == fovLitLid                      `blame` "wrong accumulated sfovLitLid"                      `swith` (sfovLitLid, fovLitLid, rngs)) ()       !_A6 = assert (sfovClearLid == fovClearLid@@ -262,7 +302,7 @@       !_A2 = assert (sperCacheFid == perCacheFid                      `blame` "wrong accumulated sperCacheFid"                      `swith` (sperCacheFid, perCacheFid, rngs)) ()-      !_A1 = assert (sperFid == perFid+      !_A1 = assert (isGameOverUI || sperFid == perFid                      `blame` "wrong accumulated perception"                      `swith` (sperFid, perFid, rngs)) ()   return ()@@ -273,7 +313,7 @@ -- So, leaderless factions and spawner factions do not keep an arena, -- even though the latter usually has a leader for most of the game. keepArenaFact :: Faction -> Bool-keepArenaFact fact = fleaderMode (gplayer fact) /= LeaderNull+keepArenaFact fact = fleaderMode (gplayer fact) /= Nothing                      && fneverEmpty (gplayer fact)  -- We assume the actor in the second argument has HP <= 0 or is going to be@@ -313,7 +353,7 @@ setFreshLeader :: MonadServerAtomic m => FactionId -> ActorId -> m () setFreshLeader fid aid = do   fact <- getsState $ (EM.! fid) . sfactionD-  unless (fleaderMode (gplayer fact) == LeaderNull) $ do+  unless (fleaderMode (gplayer fact) == Nothing) $ do     -- First update and send Perception so that the new leader     -- may report his environment.     b <- getsState $ getActorBody aid@@ -323,27 +363,33 @@     execUpdAtomic $ UpdLeadFaction fid (gleader fact) (Just aid)  updatePer :: MonadServerAtomic m => FactionId -> LevelId -> m ()-{-# INLINE updatePer #-} updatePer fid lid = do+  -- Performed in the State after action, e.g., with a new actor.+  perNew <- recomputeCachePer fid lid+  updatePerFromNew fid lid perNew++updatePerFromNew :: MonadServerAtomic m+                 => FactionId -> LevelId -> Perception -> m ()+updatePerFromNew fid lid perNew = do+  -- Even if nothing needed to be done, perception is now validated.   modifyServer $ \ser ->     ser {sperValidFid = EM.adjust (EM.insert lid True) fid $ sperValidFid ser}   sperFidOld <- getsServer sperFid   let perOld = sperFidOld EM.! fid EM.! lid-  -- Performed in the State after action, e.g., with a new actor.-  perNew <- recomputeCachePer fid lid-  let inPer = diffPer perNew perOld+      inPer = diffPer perNew perOld       outPer = diffPer perOld perNew-  unless (nullPer outPer && nullPer inPer) $+  unless (nullPer outPer && nullPer inPer) $ do+    -- Perception is modified on the server and sent to the client+    -- together with all the revealed info.+    let fper = EM.adjust (EM.insert lid perNew) fid+    modifyServer $ \ser -> ser {sperFid = fper $ sperFid ser}     execSendPer fid lid outPer inPer perNew  recomputeCachePer :: MonadServer m => FactionId -> LevelId -> m Perception recomputeCachePer fid lid = do   total <- getCacheTotal fid lid   fovLucid <- getCacheLucid lid-  perNew <- getsState $ perceptionFromPTotal fid lid fovLucid total-  let fper = EM.adjust (EM.insert lid perNew) fid-  modifyServer $ \ser -> ser {sperFid = fper $ sperFid ser}-  return perNew+  getsState $ perceptionFromPTotal fid lid fovLucid total  -- The missile item is removed from the store only if the projection -- went into effect (no failure occured).@@ -360,11 +406,11 @@             -> Bool       -- ^ whether the item is a blast             -> m (Maybe ReqFailure) projectFail propeller origin oxy tpxy eps center iid cstore blast = do-  COps{corule=RuleContent{rXmax, rYmax}, coTileSpeedup} <- getsState scops+  COps{coTileSpeedup} <- getsState scops   body <- getsState $ getActorBody origin   let lid = blid body   lvl <- getLevel lid-  case bla rXmax rYmax eps oxy tpxy of+  case bla eps oxy tpxy of     Nothing -> return $ Just ProjectAimOnself     Just [] -> error $ "projecting from the edge of level"                        `showFailure` (oxy, tpxy)@@ -449,10 +495,11 @@                   => GroupName ItemKind -> FactionId -> Point -> LevelId -> Time                   -> m (Maybe ActorId) addActorFromGroup actorGroup fid pos lid time = do+  Level{ldepth} <- getLevel lid   -- We bootstrap the actor by first creating the trunk of the actor's body   -- that contains the fixed properties of all actors of that kind.-  freq <- prepareItemKind 0 lid [(actorGroup, 1)]-  m2 <- rollItemAspect freq lid+  freq <- prepareItemKind 0 ldepth [(actorGroup, 1)]+  m2 <- rollItemAspect freq ldepth   case m2 of     NoNewItem -> return Nothing     NewItem itemKnown itemFull itemQuant -> do@@ -607,22 +654,23 @@     forM_ (healthOrgans ++ map (Nothing,) (IK.ikit itemKind))           $ \(mk, (ikGrp, cstore)) -> do      -- TODO: remove ASAP. This is a hack that prevents AI from stealing-     -- backstories until there is enough of the in Allure.+     -- backstories until there is enough of them in Allure.      -- Instead, pre-generate 20 player heroes to make sure all unique      -- backstories are available to the player and so that the order-     -- of games played doesn't affect their avilability.-     if ikGrp == GroupName "backstory"+     -- of games played doesn't affect their availability.+     if ikGrp == DefsInternal.GroupName "backstory"         && isJust bnumberTeam         && (snd <$> bnumberTeam) /= Just teamExplorer      then return ()      else do       let container = CActor aid cstore+      Level{ldepth} <- getLevel lid       mIidEtc <- case lookup ikGrp gearList of         Nothing -> do           let itemFreq = [(ikGrp, 1)]           -- Power depth of new items unaffected by number of spawned actors.-          freq <- prepareItemKind 0 lid itemFreq-          mIidEtc <- rollAndRegisterItem False lid freq container mk+          freq <- prepareItemKind 0 ldepth itemFreq+          mIidEtc <- rollAndRegisterItem False ldepth freq container mk           case (bnumberTeam, mIidEtc) of             (Just (number, teamContinuity), Just (_, (itemFull2, _))) -> do               let arItem2 = aspectRecordFull itemFull2@@ -645,7 +693,7 @@             ser {steamGearCur = EM.alter alt teamContinuity steamGearCur}           let itemKind2 = okind coitem itemKindId2               freq = pure (itemKindId2, itemKind2)-          rollAndRegisterItem False lid freq container mk+          rollAndRegisterItem False ldepth freq container mk       case mIidEtc of         Nothing -> error $ "" `showFailure` (lid, ikGrp, container, mk)         Just (iid, (itemFull2, _)) ->@@ -702,8 +750,7 @@ currentSkillsServer :: MonadServer m => ActorId -> m Ability.Skills currentSkillsServer aid  = do   body <- getsState $ getActorBody aid-  fact <- getsState $ (EM.! bfid body) . sfactionD-  let mleader = gleader fact+  mleader <- getsState $ gleader . (EM.! bfid body) . sfactionD   getsState $ actorCurrentSkills mleader aid  getCacheLucid :: MonadServer m => LevelId -> m FovLucid@@ -761,10 +808,11 @@              => Bool -> GroupName ItemKind -> ActorId -> m () addCondition verbose name aid = do   b <- getsState $ getActorBody aid+  Level{ldepth} <- getLevel $ blid b   let c = CActor aid COrgan   -- Power depth of new items unaffected by number of spawned actors.-  freq <- prepareItemKind 0 (blid b) [(name, 1)]-  mresult <- rollAndRegisterItem verbose (blid b) freq c Nothing+  freq <- prepareItemKind 0 ldepth [(name, 1)]+  mresult <- rollAndRegisterItem verbose ldepth freq c Nothing   assert (isJust mresult) $ return ()  removeConditionSingle :: MonadServerAtomic m
engine-src/Game/LambdaHack/Server/DebugM.hs view
@@ -63,6 +63,7 @@   RespSfxAtomic sfx -> do  -- not so crucial so no details     ps <- posSfxAtomic sfx     serverPrint $ debugShow (fid, "RespSfxAtomic" :: Text, ps)+  RespQueryUIunderAI -> serverPrint "RespQueryUIunderAI"   RespQueryUI -> serverPrint "RespQueryUI"  debugPretty :: MonadServer m => FactionId -> Text -> UpdAtomic -> m ()
engine-src/Game/LambdaHack/Server/DungeonGen.hs view
@@ -41,6 +41,7 @@ import qualified Game.LambdaHack.Core.Dice as Dice import           Game.LambdaHack.Core.Random import           Game.LambdaHack.Definition.Defs+import qualified Game.LambdaHack.Definition.DefsInternal as DefsInternal import           Game.LambdaHack.Server.DungeonGen.AreaRnd import           Game.LambdaHack.Server.DungeonGen.Cave import           Game.LambdaHack.Server.DungeonGen.Place@@ -49,7 +50,9 @@ convertTileMaps :: COps -> Bool -> Rnd (ContentId TileKind)                 -> Maybe (Rnd (ContentId TileKind)) -> Area -> TileMapEM                 -> Rnd TileMap-convertTileMaps COps{corule=RuleContent{rXmax, rYmax}, cotile, coTileSpeedup}+convertTileMaps COps{ corule=RuleContent{rWidthMax, rHeightMax}+                    , cotile+                    , coTileSpeedup }                 areAllWalkable cdefTile mpickPassable darea ltile = do   let outerId = ouniqGroup cotile TK.S_UNKNOWN_OUTER_FENCE       runCdefTile :: (SM.SMGen, (Int, [(Int, ContentId TileKind)]))@@ -57,7 +60,7 @@                      , (SM.SMGen, (Int, [(Int, ContentId TileKind)])) )       runCdefTile (gen1, (pI, assocs)) =         let p = toEnum pI-        in if p `inside` darea+        in if inside darea p            then case assocs of              (p2, t2) : rest | p2 == pI -> (t2, (gen1, (pI + 1, rest)))              _ -> let (tile, gen2) = St.runState cdefTile gen1@@ -66,7 +69,7 @@       runUnfold gen =         let (gen1, gen2) = SM.splitSMGen gen         in (PointArray.unfoldrNA-              rXmax rYmax runCdefTile+              rWidthMax rHeightMax runCdefTile               (gen1, (0, IM.assocs $ EM.enumMapToIntMap ltile)), gen2)   converted1 <- St.state runUnfold   case mpickPassable of@@ -88,7 +91,7 @@           yeven Point{..} = py `mod` 2 == 0           activeArea = fromMaybe (error $ "" `showFailure` darea) $ shrink darea           connect included blocks walkableTile array =-            let g p c = if p `inside` activeArea+            let g p c = if inside activeArea p                            && included p                            && not (Tile.isEasyOpen coTileSpeedup c)                            && p `EM.notMember` ltile@@ -142,18 +145,18 @@         let (lxPrev, lyPrev) = unzip $ map ((px &&& py) . fst) stairsFromUp             -- Stairs take some space, hence the additions.             lxMin = max 0-                    $ -4 - d + minimum (rXmax - 1 : lxPrev)-            lxMax = min (rXmax - 1)+                    $ -4 - d + minimum (rWidthMax - 1 : lxPrev)+            lxMax = min (rWidthMax - 1)                     $ 4 + d + maximum (0 : lxPrev)             lyMin = max 0-                    $ -3 - d + minimum (rYmax - 1 : lyPrev)-            lyMax = min (rYmax - 1)+                    $ -3 - d + minimum (rHeightMax - 1 : lyPrev)+            lyMax = min (rHeightMax - 1)                     $ 3 + d + maximum (0 : lyPrev)             -- Pick minimal cave size that fits all previous stairs.             xspan = max (lxMax - lxMin + 1) $ cXminSize kc             yspan = max (lyMax - lyMin + 1) $ cYminSize kc-            x0 = min lxMin (rXmax - xspan)-            y0 = min lyMin (rYmax - yspan)+            x0 = min lxMin (rWidthMax - xspan)+            y0 = min lyMin (rHeightMax - yspan)         in fromMaybe (error $ "" `showFailure` kc)            $ toArea (x0, y0, x0 + xspan - 1, y0 + yspan - 1)       (lstairsDouble, lstairsSingleUp) = splitAt doubleDownStairs stairsFromUp@@ -190,16 +193,13 @@           Nothing -> return acc  -- calling again won't change anything   pstairsSingleDown <- addSingleDown [] singleDownStairs   let freqDouble carried =-        filter (\(gn, _) -> carried `elem` T.words (fromGroupName gn))+        filter (\(gn, _) ->+                  carried `elem` T.words (DefsInternal.fromGroupName gn))         $ cstairFreq kc ++ cstairAllowed kc       fixedStairsDouble = map (second freqDouble) lstairsDouble-      freqUp carried =-        map (first (\gn -> GroupName $ fromGroupName gn <+> "up"))-        $ freqDouble carried+      freqUp carried = renameFreqs (<+> "up") $ freqDouble carried       fixedStairsUp = map (second freqUp) lstairsSingleUp-      freqDown =-        map (first (\gn -> GroupName $ fromGroupName gn <+> "down"))-        $ cstairFreq kc+      freqDown = renameFreqs (<+> "down") $ cstairFreq kc       fixedStairsDown = map (, freqDown) pstairsSingleDown       pallExits = pallUpAndEscape ++ pstairsSingleDown       fixedCenters = EM.fromList $@@ -348,12 +348,8 @@         return $! zip ns lShuffled   cavesShuffled <- mapM shuffleSegment caves   let cavesFlat = concat cavesShuffled-      keys = map fst cavesFlat-      minD = minimum keys-      maxD = maximum keys-      freshTotalDepth = assert (signum minD == signum maxD)-                        $ Dice.AbsDepth-                        $ max 10 $ max (abs minD) (abs maxD)+      absKeys = map (abs . fst) cavesFlat+      freshTotalDepth = Dice.AbsDepth $ maximum $ 10 : absKeys       getCaveKindNum :: (Int, GroupName CaveKind)                      -> Rnd ((LevelId, ContentId CaveKind, CaveKind), Int)       getCaveKindNum (ln, genName) = do
engine-src/Game/LambdaHack/Server/DungeonGen/AreaRnd.hs view
@@ -266,7 +266,7 @@           in case toArea (min sx tx, y0, max sx tx, y1) of             Just a -> (Vert, a, Point sx (say1 + 1), Point tx (tay0 - 1))             Nothing -> error $ "" `showFailure` (sx, sy, tx, ty, s3, t3)-      nin p = not $ p `inside` sa || p `inside` ta+      nin p = not $ inside sa p || inside ta p       !_A = assert (sslim || tslim                     || allB nin [p0, p1] `blame` (sx, sy, tx, ty, s3, t3)) ()   cor@(c1, c2, c3, c4) <- mkCorridor hv p0 (sa == so) p1 (ta == to) area
engine-src/Game/LambdaHack/Server/DungeonGen/Cave.hs view
@@ -208,7 +208,7 @@                                   $ shrink ar                       !_A0 = shrink innerArea                       !_A1 = assert (isJust _A0 `blame` (innerArea, gs2, kc)) ()-                      !_A2 = assert (p `inside` fromJust _A0+                      !_A2 = assert (inside (fromJust _A0) p                                      `blame` (p, innerArea, gs)) ()                       r = mkFixed maxPlaceSize innerArea p                       !_A3 = assert (isJust (shrink r)@@ -294,7 +294,7 @@   -- only the suspect variant on client gets replaced by this upon searching.   let sub2Area = fromMaybe (error $ "" `showFailure` kc) $ shrink subArea       sub3Area = fromMaybe (error $ "" `showFailure` kc) $ shrink sub2Area-      likelySecret = (`inside` sub3Area)+      likelySecret = inside sub3Area       obscure p t = if isChancePos 1 chidden dsecret p && likelySecret p                     then Tile.obscureAs cotile t                     else return t
engine-src/Game/LambdaHack/Server/DungeonGen/Place.hs view
@@ -4,7 +4,7 @@   ( Place(..), TileMapEM, buildPlace, isChancePos, buildFenceRnd #ifdef EXPOSE_INTERNAL     -- * Internal operations-  , placeCheck, interiorArea, olegend, pover, buildFence, buildFenceMap+  , placeCheck, interiorArea, pover, buildFence, buildFenceMap   , tilePlace #endif   ) where@@ -15,7 +15,6 @@  import qualified Data.Bits as Bits import qualified Data.EnumMap.Strict as EM-import qualified Data.EnumSet as ES import qualified Data.Text as T import           Data.Word (Word32) @@ -148,7 +147,6 @@                  || pfence kr == FNone && smallPattern)           then return dnight           else oddsDice levelDepth totalDepth cdarkOdds-  let qlegend = if dark then clegendDarkTile else clegendLitTile   rBetter <- case minnerArea of     Just innerArea | pcover kr `elem` [CVerbatim, CMirror] -> do       -- A hack: if a verbatim place was rolled, redo computing the area@@ -166,18 +164,16 @@     _ -> return r   let qarea = fromMaybe (error $ "" `showFailure` (kr, r))               $ interiorArea kr rBetter-      override = if dark then poverrideDark kr else poverrideLit kr-  (overrideOneIn, overDefault) <- pover cops override-  (legendOneIn, legend) <- olegend cops qlegend+      plegend = if dark then plegendDark kr else plegendLit kr+  mOneIn <- pover cops plegend   cmap <- tilePlace qarea kr-  let mOneIn :: EM.EnumMap Char (Int, Int, ContentId TileKind)-      mOneIn = EM.union overrideOneIn legendOneIn-      m :: EM.EnumMap Char (ContentId TileKind)-      m = EM.union overDefault legend-      lookupOneIn :: Point -> Char -> ContentId TileKind-      lookupOneIn xy c = case EM.lookup c mOneIn of-        Just (k, n, tk) | isChancePos k n dsecret xy -> tk-        _ -> EM.findWithDefault (error $ "" `showFailure` (c, mOneIn, m)) c m+  let lookupOneIn :: Point -> Char -> ContentId TileKind+      lookupOneIn xy c =+        let tktk = EM.findWithDefault+                     (error $ "" `showFailure` (c, mOneIn)) c mOneIn+        in case tktk of+          (Just (k, n, tkSpice), _) | isChancePos k n dsecret xy -> tkSpice+          (_, tk) -> tk       qmap = EM.mapWithKey lookupOneIn cmap   qfence <- buildFence cops kc dnight darkCorTile litCorTile                        dark (pfence kr) qarea@@ -194,50 +190,36 @@      then z `mod` ((n + k) `divUp` k) == 0      else z `mod` ((n + k) `divUp` n) /= 0 --- | Roll a legend of a place plan: a map from plan symbols to tile kinds.-olegend :: COps -> GroupName TileKind-        -> Rnd ( EM.EnumMap Char (Int, Int, ContentId TileKind)-               , EM.EnumMap Char (ContentId TileKind) )-olegend COps{cotile} cgroup =-  let getSymbols !acc _ _ !tk = ES.insert (TK.tsymbol tk) acc-      symbols = ofoldlGroup' cotile cgroup getSymbols ES.empty-      getLegend s !acc = do-        (mOneIn, m) <- acc-        let p f t = TK.tsymbol t == s && f (Tile.kindHasFeature TK.Spice t)-        tk <- fmap (fromMaybe $ error $ "" `showFailure` (cgroup, s))-              $ opick cotile cgroup (p not)-        mtkSpice <- opick cotile cgroup (p id)-        return $! case mtkSpice of-          Nothing -> (mOneIn, EM.insert s tk m)-          Just tkSpice ->-            -- Unlikely, but possible that ordinary legend has spice.-            let n = fromMaybe (error $ show cgroup)-                              (lookup cgroup (TK.tfreq (okind cotile tk)))-                k = fromMaybe (error $ show cgroup)-                              (lookup cgroup (TK.tfreq (okind cotile tkSpice)))-            in (EM.insert s (k, n, tkSpice) mOneIn, EM.insert s tk m)-      legend = ES.foldr' getLegend (return (EM.empty, EM.empty)) symbols-  in legend--pover :: COps -> [(Char, GroupName TileKind)]-      -> Rnd ( EM.EnumMap Char (Int, Int, ContentId TileKind)-             , EM.EnumMap Char (ContentId TileKind) )-pover COps{cotile} poverride =-  let getLegend (s, cgroup) acc = do-        (mOneIn, m) <- acc+-- This can't be optimized by memoization (storing these results per place),+-- because it would fix random assignment of tiles to groups+-- for all instances of a place throughout dungeon. Right now the assignment+-- is fixed for any single place instance and it's consistent and interesting.+-- Even fixing this per level would make levels less interesting.+--+-- This could be precomputed for groups that contain only one tile,+-- but for these, no random rolls are performed, so little would be saved.+pover :: COps -> EM.EnumMap Char (GroupName TileKind)+      -> Rnd ( EM.EnumMap Char ( Maybe (Int, Int, ContentId TileKind)+                               , ContentId TileKind ) )+pover COps{cotile} plegend =+  let assignKN :: GroupName TileKind -> ContentId TileKind -> ContentId TileKind+               -> (Int, Int, ContentId TileKind)+      assignKN cgroup tk tkSpice =+        -- Very likely that legends have spice.+        let n = fromMaybe (error $ show cgroup)+                          (lookup cgroup (TK.tfreq (okind cotile tk)))+            k = fromMaybe (error $ show cgroup)+                          (lookup cgroup (TK.tfreq (okind cotile tkSpice)))+        in (k, n, tkSpice)+      getLegend :: GroupName TileKind+                -> Rnd ( Maybe (Int, Int, ContentId TileKind)+                       , ContentId TileKind )+      getLegend cgroup = do         mtkSpice <- opick cotile cgroup (Tile.kindHasFeature TK.Spice)-        tk <- fromMaybe (error $ "" `showFailure` (s, cgroup, poverride))+        tk <- fromMaybe (error $ "" `showFailure` (cgroup, plegend))               <$> opick cotile cgroup (not . Tile.kindHasFeature TK.Spice)-        return $! case mtkSpice of-          Nothing -> (mOneIn, EM.insert s tk m)-          Just tkSpice ->-            -- Very likely that overrides have spice.-            let n = fromMaybe (error $ show cgroup)-                              (lookup cgroup (TK.tfreq (okind cotile tk)))-                k = fromMaybe (error $ show cgroup)-                              (lookup cgroup (TK.tfreq (okind cotile tkSpice)))-            in (EM.insert s (k, n, tkSpice) mOneIn, EM.insert s tk m)-  in foldr getLegend (return (EM.empty, EM.empty)) poverride+        return (assignKN cgroup tk <$> mtkSpice, tk)+  in mapM getLegend plegend  -- | Construct a fence around a place. buildFence :: COps -> CaveKind -> Bool
engine-src/Game/LambdaHack/Server/HandleAtomicM.hs view
@@ -234,6 +234,7 @@   UpdKillExit{} -> return ()   UpdWriteSave{} -> return ()   UpdHearFid{} -> return ()+  UpdMuteMessages{} -> return ()  validateFloor :: MonadServer m => ItemId -> LevelId -> m () validateFloor iid lid = do
engine-src/Game/LambdaHack/Server/HandleEffectM.hs view
@@ -117,7 +117,7 @@     totalDepth <- getsState stotalDepth     Level{ldepth} <- getLevel (blid sb)     dmg <- rndToAction $ castDice ldepth totalDepth $ IK.idamage itemKind-    let rawDeltaHP = intCast hurtMult * xM dmg `divUp` 100+    let rawDeltaHP = into @Int64 hurtMult * xM dmg `divUp` 100         speedDeltaHP = case btrajectory sb of           Just (_, speed) | bproj sb -> - modifyDamageBySpeed rawDeltaHP speed           _ -> - rawDeltaHP@@ -148,7 +148,7 @@   when serious $ cutCalm target   tb <- getsState $ getActorBody target   fact <- getsState $ (EM.! bfid tb) . sfactionD-  unless (bproj tb || fleaderMode (gplayer fact) == LeaderNull) $+  unless (bproj tb || fleaderMode (gplayer fact) == Nothing) $     -- If leader just lost all HP, change the leader early (not when destroying     -- the actor), to let players rescue him, especially if he's slowed     -- by the attackers.@@ -531,8 +531,9 @@       -- functions that have the store hardwired.       container = CActor target COrgan   -- Power depth of new items unaffected by number of spawned actors.-  freq <- prepareItemKind 0 (blid tb) itemFreq-  m2 <- rollAndRegisterItem False (blid tb) freq container Nothing+  Level{ldepth} <- getLevel $ blid tb+  freq <- prepareItemKind 0 ldepth itemFreq+  m2 <- rollAndRegisterItem False ldepth freq container Nothing   acounter <- getsServer $ fromEnum . sacounter   let (iid, (ItemFull{itemKind}, (itemK, _))) =         fromMaybe (error $ "" `showFailure` cgroup) m2@@ -676,7 +677,7 @@              Just (hiImpressionFid, hiImpressionK) ->                 hiImpressionFid == bfid sb                   -- highest impression needs to be by us-                && (fleaderMode (gplayer fact) /= LeaderNull+                && (fleaderMode (gplayer fact) /= Nothing                     || hiImpressionK >= 10)                      -- to tame/hack animal/robot, impress them a lot first        if permitted then do@@ -861,7 +862,7 @@   if bhp tb <= 0 then  -- avoid yelling corpses     return UseDud  -- the yell never manifested   else do-    when (not (bproj tb))+    unless (bproj tb)       execSfx     execSfxAtomic $ SfxTaunt False target     when (not (bproj tb) && deltaBenign (bcalmDelta tb)) $@@ -877,13 +878,12 @@              -> m UseResult effectSummon grp nDm iid source target effActivation = do   -- Obvious effect, nothing announced.-  cops@COps{coTileSpeedup} <- getsState scops   sb <- getsState $ getActorBody source   tb <- getsState $ getActorBody target   sMaxSk <- getsState $ getActorMaxSkills source   tMaxSk <- getsState $ getActorMaxSkills target   totalDepth <- getsState stotalDepth-  lvl@Level{ldepth, lbig} <- getLevel (blid tb)+  Level{ldepth, lbig} <- getLevel (blid tb)   nFriends <- getsState $ length . friendRegularAssocs (bfid sb) (blid sb)   discoAspect <- getsState sdiscoAspect   power0 <- rndToAction $ castDice ldepth totalDepth nDm@@ -926,29 +926,16 @@        return UseId      | otherwise -> do        unless (bproj sb) $ updateCalm source deltaCalm-       let validTile t = not $ Tile.isNoActor coTileSpeedup t-           ps = nearbyFreePoints cops lvl validTile (bpos tb)        localTime <- getsState $ getLocalTime (blid tb)        -- Make sure summoned actors start acting after the victim.        let actorTurn = ticksPerMeter $ gearSpeed tMaxSk            targetTime = timeShift localTime actorTurn            afterTime = timeShift targetTime $ Delta timeClip-       when (length (take power ps) < power) $-          debugPossiblyPrint $-            "Server: effectSummon: failed to find enough free positions at"-            <+> tshow (blid tb, bpos tb)-       bs <- forM (take power ps) $ \p -> do-         -- Mark as summoned to prevent immediate chain summoning.-         -- Summon from current depth, not deeper due to many spawns already.-         maid <- addAnyActor True 0 [(grp, 1)] (blid tb) afterTime (Just p)-         case maid of-           Nothing -> return False  -- suspect content; server debug elsewhere-           Just aid -> do-             b <- getsState $ getActorBody aid-             mleader <- getsState $ gleader . (EM.! bfid b) . sfactionD-             when (isNothing mleader) $ setFreshLeader (bfid b) aid-             return True-       if or bs then do+       -- Mark as summoned to prevent immediate chain summoning.+       -- Summon from current depth, not deeper due to many spawns already.+       anySummoned <- addManyActors True 0 [(grp, 1)] (blid tb) afterTime+                                    (Just $ bpos tb) power+       if anySummoned then do          execSfxAtomic $ SfxEffect (bfid sb) source iid effect 0          return UseUp        else do@@ -1336,22 +1323,24 @@  else do   cops <- getsState scops   sb <- getsState $ getActorBody source+  actorMaxSk <- getsState $ getActorMaxSkills target   totalDepth <- getsState stotalDepth   lvlTb <- getLevel (blid tb)-  dungeon <- getsState sdungeon-  let maxLidLvl = maximumBy (comparing (ldepth . snd)) $ EM.assocs dungeon-      -- If the number of items independent of depth, make also the timer-      -- the item kind choice and aspects independent of depth.-      -- Prime example is crafting. TODO: base this on skill.-      (lid, lvl) = if isJust mcount then maxLidLvl else (blid tb, lvlTb)-      depth = ldepth lvl+  let -- If the number of items independent of depth in @mcount@,+      -- make also the timer, the item kind choice and aspects+      -- independent of depth, via fixing the generation depth of the item+      -- to @totalDepth@. Prime example of provided @mcount@ is crafting.+      -- TODO: base this on a resource that can be consciously spent,+      -- not on a skill that grows over time or that only one actor+      -- maxes out and so needs to always be chosen for crafting.+      -- See https://www.reddit.com/r/roguelikedev/comments/phukcq/game_design_question_how_to_base_item_generation/+      depth = if isJust mcount then totalDepth else ldepth lvlTb       fscale unit nDm = do         k0 <- rndToAction $ castDice depth totalDepth nDm         let k = max 1 k0  -- KISS, don't freak out if dice permit 0         return $! timeDeltaScale unit k       fgame = fscale (Delta timeTurn)       factor nDm = do-        actorMaxSk <- getsState $ getActorMaxSkills target         -- A bit added to make sure length 1 effect doesn't randomly         -- end, or not, before the end of first turn, which would make,         -- e.g., hasting, useless. This needs to be higher than 10%@@ -1365,7 +1354,7 @@   uniqueSet <- getsServer suniqueSet   -- Power depth of new items unaffected by number of spawned actors, so 0.   let freq = newItemKind cops uniqueSet [(grp, 1)] depth totalDepth 0-  m2 <- rollItemAspect freq lid+  m2 <- rollItemAspect freq depth   case m2 of     NoNewItem -> return UseDud  -- e.g., unique already generated     NewItem itemKnownRaw itemFullRaw (kRaw, itRaw) -> do@@ -1679,7 +1668,7 @@  if bproj tb then return UseDud else do  -- slows down, but rarely any effect   localTime <- getsState $ getLocalTime (blid tb)   totalDepth <- getsState stotalDepth-  Level{ldepth} <- getLevel (blid tb)+  Level{ldepth} <- getLevel $ blid tb   power <- rndToAction $ castDice ldepth totalDepth dice   let timeUnit = if reducingCooldown                  then absoluteTimeNegate timeClip@@ -1803,12 +1792,12 @@            execSfx            identifyIid iid c itemKindId itemKind            execUpdAtomic $ UpdDestroyItem False iid itemBase kit c-           dungeon <- getsState sdungeon-           let maxLid = fst $ maximumBy (comparing (ldepth . snd))-                            $ EM.assocs dungeon-               roll100 :: Int -> m (ItemKnown, ItemFull)+           totalDepth <- getsState stotalDepth+           let roll100 :: Int -> m (ItemKnown, ItemFull)                roll100 n = do-                 m2 <- rollItemAspect freq maxLid+                 -- Not only rerolled, but at highest depth possible,+                 -- resulting in highest potential for bonuses.+                 m2 <- rollItemAspect freq totalDepth                  case m2 of                    NoNewItem ->                      error "effectRerollItem: can't create rerolled item"@@ -1918,6 +1907,8 @@   COps{coitem, coTileSpeedup} <- getsState scops   b <- getsState $ getActorBody target   lvl <- getLevel $ blid b+  sClient <- getsServer $ (EM.! bfid b) . sclientStates+  let lvlClient = (EM.! blid b) . sdungeon $ sClient   s <- getState   getKind <- getsState $ flip getIidKindServer   factionD <- getsState sfactionD@@ -1966,7 +1957,13 @@           let (ls1, ls2) = lstair lvl           in ((`elem` ls1 ++ ls2 ++ lescape lvl), const $ return False)         IK.DetectHidden ->-          let predicateH p = Tile.isHideAs coTileSpeedup $ lvl `at` p+          let predicateH p = let tClient = lvlClient `at` p+                                 tServer = lvl `at` p+                             in Tile.isHideAs coTileSpeedup tServer+                                && tClient /= tServer+                -- the actor searches only tiles he doesn't know already,+                -- preventing misleading messages (and giving less information+                -- to eavesdropping parties)               revealEmbed p = do                 embeds <- getsState $ getEmbedBag (blid b) p                 unless (EM.null embeds) $@@ -1984,7 +1981,7 @@                         Just entry ->                           execUpdAtomic $ UpdSpotEntry (blid b) [(p, entry)]                 mapM_ f l-                return $! not $ null l  -- KISS, even if client knows all+                return $! not $ null l           in (predicateH, actionH)         IK.DetectEmbed -> ((`EM.member` lembed lvl), const $ return False)         IK.DetectStash -> (stashPredicate, const $ return False)@@ -1996,15 +1993,15 @@               => IK.DetectKind -> (Point -> Bool) -> ([Point] -> m Bool)               -> m () -> Int -> ActorId -> m UseResult effectDetectX d predicate action execSfx radius target = do-  COps{corule=RuleContent{rXmax, rYmax}} <- getsState scops+  COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops   b <- getsState $ getActorBody target   sperFidOld <- getsServer sperFid   let perOld = sperFidOld EM.! bfid b EM.! blid b       Point x0 y0 = bpos b       perList = filter predicate         [ Point x y-        | y <- [max 0 (y0 - radius) .. min (rYmax - 1) (y0 + radius)]-        , x <- [max 0 (x0 - radius) .. min (rXmax - 1) (x0 + radius)]+        | y <- [max 0 (y0 - radius) .. min (rHeightMax - 1) (y0 + radius)]+        , x <- [max 0 (x0 - radius) .. min (rWidthMax - 1) (x0 + radius)]         ]       extraPer = emptyPer {psight = PerVisible $ ES.fromDistinctAscList perList}       inPer = diffPer extraPer perOld@@ -2057,8 +2054,7 @@       execSfxAtomic $ SfxMsgFid (bfid tb) $ SfxBracedImmune target     return UseUp  -- waste it to prevent repeated throwing at immobile actors   else do-   COps{corule=RuleContent{rXmax, rYmax}} <- getsState scops-   case bla rXmax rYmax eps (bpos tb) fpos of+   case bla eps (bpos tb) fpos of     Nothing -> error $ "" `showFailure` (fpos, tb)     Just [] -> error $ "projecting from the edge of level"                        `showFailure` (fpos, tb)
engine-src/Game/LambdaHack/Server/HandleRequestM.hs view
@@ -681,7 +681,7 @@              => Bool -> EffToUse -> Bool -> ActorId -> Point              -> m (Maybe ReqFailure) reqAlterFail bumping effToUse voluntary source tpos = do-  cops@COps{cotile, coTileSpeedup} <- getsState scops+  cops@COps{cotile, coTileSpeedup, corule} <- getsState scops   sb <- getsState $ getActorBody source   actorMaxSk <- getsState $ getActorMaxSkills source   let calmE = calmEnough sb actorMaxSk@@ -713,7 +713,8 @@         let itemFull = itemToF iid             -- Let even completely apply-unskilled actors trigger basic embeds.             -- See the note about no skill check when melee triggers effects.-            legal = permittedApply localTime maxBound calmE Nothing itemFull kit+            legal = permittedApply corule localTime maxBound calmE Nothing+                                   itemFull kit         case legal of           Left ApplyNoEffects -> return UseDud  -- pure flavour embed           Left reqFail -> do@@ -1124,6 +1125,7 @@          -> CStore   -- ^ the location of the item          -> m () reqApply aid iid cstore = do+  COps{corule} <- getsState scops   let req = ReqApply iid cstore   b <- getsState $ getActorBody aid   actorMaxSk <- getsState $ getActorMaxSkills aid@@ -1138,7 +1140,7 @@         actorSk <- currentSkillsServer aid         localTime <- getsState $ getLocalTime (blid b)         let skill = Ability.getSk Ability.SkApply actorSk-            legal = permittedApply localTime skill calmE (Just cstore)+            legal = permittedApply corule localTime skill calmE (Just cstore)                                    itemFull kit         case legal of           Left reqFail -> execFailure aid req reqFail@@ -1150,13 +1152,15 @@                => ActorId -> GroupName ModeKind -> Challenge                -> m () reqGameRestart aid groupName scurChalSer = do-  -- This call to `revealItems` is really needed, because the other-  -- happens only at natural game conclusion, not at forced quitting.   noConfirmsGame <- isNoConfirmsGame   factionD <- getsState sfactionD   let fidsUI = map fst $ filter (\(_, fact) -> fhasUI (gplayer fact))                                 (EM.assocs factionD)-  unless noConfirmsGame $ mapM_ revealItems fidsUI+  -- This call to `revealItems` and `revealPerception` is really needed,+  -- because the other happens only at natural game conclusion,+  -- not at forced quitting.+  unless noConfirmsGame $+    mapM_ revealAll fidsUI   -- Announcing end of game, we send lore, because game is over.   b <- getsState $ getActorBody aid   oldSt <- getsState $ gquit . (EM.! bfid b) . sfactionD
engine-src/Game/LambdaHack/Server/ItemM.hs view
@@ -36,6 +36,7 @@ import           Game.LambdaHack.Content.ItemKind (ItemKind) import qualified Game.LambdaHack.Content.ItemKind as IK import           Game.LambdaHack.Content.TileKind (TileKind)+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@@ -148,21 +149,22 @@ createCaveItem :: MonadServerAtomic m => Point -> LevelId -> m () createCaveItem pos lid = do   COps{cocave} <- getsState scops-  Level{lkind} <- getLevel lid+  Level{lkind, ldepth} <- getLevel lid   let container = CFloor lid pos       litemFreq = citemFreq $ okind cocave lkind   -- Power depth of new items unaffected by number of spawned actors.-  freq <- prepareItemKind 0 lid litemFreq-  mIidEtc <- rollAndRegisterItem True lid freq container Nothing+  freq <- prepareItemKind 0 ldepth litemFreq+  mIidEtc <- rollAndRegisterItem True ldepth freq container Nothing   createKitItems lid pos mIidEtc  createEmbedItem :: MonadServerAtomic m                 => LevelId -> Point -> GroupName ItemKind -> m () createEmbedItem lid pos grp = do+  Level{ldepth} <- getLevel lid   let container = CEmbed lid pos   -- Power depth of new items unaffected by number of spawned actors.-  freq <- prepareItemKind 0 lid [(grp, 1)]-  mIidEtc <- rollAndRegisterItem True lid freq container Nothing+  freq <- prepareItemKind 0 ldepth [(grp, 1)]+  mIidEtc <- rollAndRegisterItem True ldepth freq container Nothing   createKitItems lid pos mIidEtc  -- Create, register and insert all initial kit items.@@ -172,7 +174,7 @@   Nothing -> error $ "" `showFailure` (lid, pos, mIidEtc)   Just (_, (itemFull, _)) -> do     cops <- getsState scops-    lvl <- getLevel lid+    lvl@Level{ldepth} <- getLevel lid     let ikit = IK.ikit $ itemKind itemFull         nearbyPassable = take (20 + length ikit)                          $ nearbyPassablePoints cops lvl pos@@ -187,8 +189,8 @@                       else CEmbed lid pos           itemFreq = [(ikGrp, 1)]       -- Power depth of new items unaffected by number of spawned actors.-      freq <- prepareItemKind 0 lid itemFreq-      mresult <- rollAndRegisterItem False lid freq container Nothing+      freq <- prepareItemKind 0 ldepth itemFreq+      mresult <- rollAndRegisterItem False ldepth freq container Nothing       assert (isJust mresult) $ return ()  -- Tiles already placed, so it's possible to scatter companion items@@ -201,24 +203,22 @@   mapM_ (createEmbedItem lid pos) embedGroups  prepareItemKind :: MonadServerAtomic m-                => Int -> LevelId -> Freqs ItemKind+                => Int -> Dice.AbsDepth -> Freqs ItemKind                 -> m (Frequency (ContentId IK.ItemKind, ItemKind))-prepareItemKind lvlSpawned lid itemFreq = do+prepareItemKind lvlSpawned ldepth itemFreq = do   cops <- getsState scops   uniqueSet <- getsServer suniqueSet   totalDepth <- getsState stotalDepth-  Level{ldepth} <- getLevel lid   return $! newItemKind cops uniqueSet itemFreq ldepth totalDepth lvlSpawned  rollItemAspect :: MonadServerAtomic m-               => Frequency (ContentId IK.ItemKind, ItemKind) -> LevelId+               => Frequency (ContentId IK.ItemKind, ItemKind) -> Dice.AbsDepth                -> m NewItem-rollItemAspect freq lid = do+rollItemAspect freq ldepth = do   cops <- getsState scops   flavour <- getsServer sflavour   discoRev <- getsServer sdiscoKindRev   totalDepth <- getsState stotalDepth-  Level{ldepth} <- getLevel lid   m2 <- rndToAction $ newItem cops freq flavour discoRev ldepth totalDepth   case m2 of     NewItem (ItemKnown _ arItem _) ItemFull{itemKindId} _ -> do@@ -230,13 +230,13 @@  rollAndRegisterItem :: MonadServerAtomic m                     => Bool-                    -> LevelId+                    -> Dice.AbsDepth                     -> Frequency (ContentId IK.ItemKind, ItemKind)                     -> Container                     -> Maybe Int                     -> m (Maybe (ItemId, ItemFullKit))-rollAndRegisterItem verbose lid freq container mk = do-  m2 <- rollItemAspect freq lid+rollAndRegisterItem verbose ldepth freq container mk = do+  m2 <- rollItemAspect freq ldepth   case m2 of     NoNewItem -> return Nothing     NewItem itemKnown itemFull kit -> do
engine-src/Game/LambdaHack/Server/ItemRev.hs view
@@ -3,11 +3,17 @@ -- server state nor our custom monads. module Game.LambdaHack.Server.ItemRev   ( ItemKnown(..), NewItem(..), ItemRev, UniqueSet-  , buildItem, newItemKind, newItem+  , newItemKind, newItem     -- * Item discovery types   , DiscoveryKindRev, emptyDiscoveryKindRev, serverDiscos     -- * The @FlavourMap@ type   , FlavourMap, emptyFlavourMap, dungeonFlavourMap+    -- * Important implementation parts, exposed for tests+  , rollFlavourMap+#ifdef EXPOSE_INTERNAL+    -- * Internal operations+  , buildItem, keepMetaGameInformation+#endif   ) where  import Prelude ()@@ -17,8 +23,8 @@ import           Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES-import           Data.Hashable (Hashable) import qualified Data.HashMap.Strict as HM+import           Data.Hashable (Hashable) import           Data.Vector.Binary () import qualified Data.Vector.Unboxed as U import           GHC.Generics (Generic)@@ -33,7 +39,6 @@ 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 import           Game.LambdaHack.Definition.Flavour @@ -144,23 +149,38 @@  serverDiscos :: COps -> DiscoveryKindRev              -> Rnd (DiscoveryKind, DiscoveryKindRev)-serverDiscos COps{coitem} (DiscoveryKindRev discoRev0) = do+serverDiscos COps{coitem} (DiscoveryKindRev discoRevFromPreviousGame) = do   let ixs = [0..toEnum (olength coitem - 1)]-      inMetaGame kindId =-        IK.SetFlag Ability.MetaGame `elem` IK.iaspects (okind coitem kindId)-      keepMeta i ix = if inMetaGame (toEnum i) then ix else maxBound   shuffled <--    if U.null discoRev0+    if U.null discoRevFromPreviousGame     then shuffle ixs-    else shuffleExcept (U.imap keepMeta discoRev0) (olength coitem) ixs-  let f (!ikMap, (!ix) : rest) !kmKind _ =-        (EM.insert (toItemKindIx ix) kmKind ikMap, rest)-      f (ikMap, []) ik _ =-        error $ "too short ixs" `showFailure` (ik, ikMap)-      (discoS, _) = ofoldlWithKey' coitem f (EM.empty, shuffled)-      udiscoRev = U.fromListN (olength coitem) shuffled+    else shuffleExcept (keepMetaGameInformation coitem discoRevFromPreviousGame)+                       (olength coitem)+                       ixs+  let udiscoRev = U.fromListN (olength coitem) shuffled+      f :: (ContentId ItemKind, Word16) -> (ItemKindIx, ContentId ItemKind)+      f (ik, ikx) = (toItemKindIx ikx, ik)+      -- Not @fromDistinctAscList@, because it's the reverse map.+      discoS = EM.fromList $ map f $ zip [toEnum 0 ..] $ U.toList udiscoRev   return (discoS, DiscoveryKindRev udiscoRev) +-- | Keep in a vector the information that is retained from playthrough+-- to playthrough. The information being, e.g., @ItemKindIx@ or @Flavour@.+-- The information is morally indexed by @ContentId ItemKind@ and its @Enum@+-- instance fits in @Word16@.+keepMetaGameInformation :: ContentData ItemKind+                        -> U.Vector Word16+                        -> U.Vector Word16+keepMetaGameInformation coitem informationFromPreviousGame =+  let inMetaGame :: ContentId ItemKind -> Bool+      inMetaGame kindId =+        IK.SetFlag Ability.MetaGame `elem` IK.iaspects (okind coitem kindId)+      keepMeta :: Int -> Word16 -> Word16+      keepMeta i ix = if inMetaGame (toEnum i)+                      then ix+                      else invalidInformationCode+  in U.imap keepMeta informationFromPreviousGame+ -- | Flavours assigned by the server to item kinds, in this particular game. -- This is total and never changes, hence implemented as vector. -- Morally, it's indexed by @ContentId ItemKind@ and elements are @Flavour@.@@ -170,18 +190,15 @@ emptyFlavourMap :: FlavourMap emptyFlavourMap = FlavourMap U.empty -stdFlav :: ES.EnumSet Flavour-stdFlav = ES.fromList [ Flavour fn bc-                      | fn <- [minBound..maxBound], bc <- Color.stdCol ]- -- | Assigns flavours to item kinds. Assures no flavor is repeated for the same -- symbol, except for items with only one permitted flavour.-rollFlavourMap :: U.Vector Word16-               -> Rnd ( EM.EnumMap (ContentId ItemKind) Flavour-                      , EM.EnumMap Char (ES.EnumSet Flavour) )-               -> ContentId ItemKind -> ItemKind-               -> Rnd ( EM.EnumMap (ContentId ItemKind) Flavour-                      , EM.EnumMap Char (ES.EnumSet Flavour) )+rollFlavourMap+  :: U.Vector Word16+  -> Rnd ( EM.EnumMap (ContentId ItemKind) Flavour+         , EM.EnumMap (ContentSymbol ItemKind) (ES.EnumSet Flavour) )+  -> ContentId ItemKind -> ItemKind+  -> Rnd ( EM.EnumMap (ContentId ItemKind) Flavour+         , EM.EnumMap (ContentSymbol ItemKind) (ES.EnumSet Flavour) ) rollFlavourMap uFlavMeta !rnd !key !ik = case IK.iflavour ik of   [] -> error "empty iflavour"   [flavour] -> do@@ -191,7 +208,7 @@   flvs -> do     (!assocs, !availableMap) <- rnd     let a0 = uFlavMeta U.! toEnum (fromEnum key)-    if a0 == maxBound then do+    if a0 == invalidInformationCode then do       if length flvs < 6 then do  -- too few to even attempt unique assignment         flavour <- oneOf flvs         return ( EM.insert key flavour assocs@@ -211,19 +228,18 @@  -- | Randomly chooses flavour for all item kinds for this game. dungeonFlavourMap :: COps -> FlavourMap -> Rnd FlavourMap-dungeonFlavourMap COps{coitem} (FlavourMap uFlav0) = do-  let inMetaGame kindId =-        IK.SetFlag Ability.MetaGame `elem` IK.iaspects (okind coitem kindId)-      keepMeta i fl = if inMetaGame (toEnum i) then fl else maxBound-      uFlavMeta = if U.null uFlav0-                  then U.replicate (olength coitem) maxBound-                  else U.imap keepMeta uFlav0+dungeonFlavourMap COps{coitem} (FlavourMap flavourMapFromPreviousGame) = do+  let uFlavMeta = if U.null flavourMapFromPreviousGame+                  then U.replicate (olength coitem) invalidInformationCode+                  else keepMetaGameInformation coitem flavourMapFromPreviousGame       flavToAvailable :: EM.EnumMap Char (ES.EnumSet Flavour) -> Int -> Word16                       -> EM.EnumMap Char (ES.EnumSet Flavour)       flavToAvailable em i fl =         let ik = okind coitem (toEnum i)-            setBase = EM.findWithDefault stdFlav (IK.isymbol ik) em-            setMeta = if fl == maxBound+            setBase = EM.findWithDefault (ES.fromList stdFlavList)+                                         (IK.isymbol ik)+                                         em+            setMeta = if fl == invalidInformationCode                       then setBase                       else ES.delete (toEnum $ fromEnum fl) setBase         in EM.insert (IK.isymbol ik) setMeta em
engine-src/Game/LambdaHack/Server/LoopM.hs view
@@ -7,7 +7,7 @@   , factionArena, arenasForLoop, handleFidUpd, loopUpd, endClip   , manageCalmAndDomination, applyPeriodicLevel   , handleTrajectories, hTrajectories, advanceTrajectory-  , handleActors, hActors, dieSer, restartGame+  , handleActors, hActors, handleUIunderAI, dieSer, restartGame #endif   ) where @@ -19,7 +19,7 @@ import qualified Data.EnumSet as ES  import           Game.LambdaHack.Atomic-import           Game.LambdaHack.Client (ReqUI (..))+import           Game.LambdaHack.Client (ReqUI (..), Response (..)) import           Game.LambdaHack.Common.Actor import           Game.LambdaHack.Common.ActorState import           Game.LambdaHack.Common.Analytics@@ -96,6 +96,7 @@       rngs <- getsServer srngs       when (sdumpInitRngs serverOptions) $ dumpRngs rngs     _ -> do  -- starting new game for this savefile (--newGame or fresh save)+      factionDold <- getsState sfactionD       s <- gameReset serverOptions Nothing Nothing              -- get RNG from item boost       -- Set up commandline options.@@ -106,7 +107,7 @@       execUpdAtomic $ UpdRestartServer s       updConn       initPer-      reinitGame+      reinitGame factionDold       writeSaveAll False False   loopUpd updConn @@ -301,8 +302,8 @@         -- expiration of short conditions, e.g., 1-turn haste. TODO.         applyPeriodicLevel       4 ->-        -- Add monsters each turn, not each clip.-        unless (ES.null arenas) spawnMonster+        -- Spawn monsters at most once per turn.+        spawnMonster       _ -> return ()   -- @applyPeriodicLevel@ might have, e.g., dominated actors, ending the game.   -- It could not have unended the game, though.@@ -329,8 +330,9 @@ #ifndef USE_JSFILE   unless breakLoop2 $  -- if by chance requested and periodic saves coincide     -- Periodic save needs to be at the end, so that restore can start-    -- at the beginning.-    when (clipN `mod` rwriteSaveClips corule == 0) $ writeSaveAll False False+    -- at the beginning. Double save on first turn is avoided with @succ@.+    when (succ clipN `mod` rwriteSaveClips corule == 0) $+      writeSaveAll False False #endif  -- | Check if the given actor is dominated and update his calm.@@ -343,7 +345,7 @@         Nothing -> return False         Just (hiImpressionFid, hiImpressionK) -> do           fact <- getsState $ (EM.! bfid b) . sfactionD-          if fleaderMode (gplayer fact) /= LeaderNull+          if fleaderMode (gplayer fact) /= Nothing                -- animals/robots/human drones never Calm-dominated              || hiImpressionK >= 10                -- unless very high impression, e.g., in a dominated hero@@ -610,19 +612,13 @@       aidIsLeader = mleader == Just aid       mainUIactor = fhasUI (gplayer fact)                     && (aidIsLeader-                        || fleaderMode (gplayer fact) == LeaderNull)+                        || fleaderMode (gplayer fact) == Nothing)       -- Checking @breakLoop@, to avoid doubly setting faction status to Camping       -- in case AI-controlled UI client asks to exit game at exactly       -- the same moment as natural game over was detected.       mainUIunderAI = mainUIactor && isAIFact fact && not breakLoop-  when mainUIunderAI $ do-    cmdS <- sendQueryUI side aid-    case fst cmdS of-      ReqUINop -> return ()-      ReqUIAutomate -> execUpdAtomic $ UpdAutoFaction side False-      ReqUIGameDropAndExit -> reqGameDropAndExit aid-      ReqUIGameSaveAndExit -> reqGameSaveAndExit aid-      _ -> error $ "" `showFailure` cmdS+  when mainUIunderAI $+    handleUIunderAI side aid   factNew <- getsState $ (EM.! side) . sfactionD   let doQueryAI = not mainUIactor || isAIFact factNew   breakASAP <- getsServer sbreakASAP@@ -641,7 +637,7 @@         mtimed <- handleRequestAI cmd         return (aidNew, mtimed)       else do-        (cmd, maid) <- sendQueryUI side aid+        (cmd, maid) <- sendQueryUI RespQueryUI side aid         aidNew <- mswitchLeader maid         mtimed <- handleRequestUI side aidNew cmd         return (aidNew, mtimed)@@ -658,6 +654,17 @@         -- If breaking out of the game lopp, pretend there was a non-wait move.         if breakASAP2 then return True else hActors as +handleUIunderAI :: (MonadServerAtomic m, MonadServerComm m)+                => FactionId -> ActorId -> m ()+handleUIunderAI side aid = do+  cmdS <- sendQueryUI RespQueryUIunderAI side aid+  case fst cmdS of+    ReqUINop -> return ()+    ReqUIAutomate -> execUpdAtomic $ UpdAutoFaction side False+    ReqUIGameDropAndExit -> reqGameDropAndExit aid+    ReqUIGameSaveAndExit -> reqGameSaveAndExit aid+    _ -> error $ "" `showFailure` cmdS+ dieSer :: MonadServerAtomic m => ActorId -> Actor -> m () dieSer aid b2 = do   if bproj b2 then@@ -716,6 +723,7 @@   execSfxAtomic SfxRestart   soptionsNxt <- getsServer soptionsNxt   srandom <- getsServer srandom+  factionDold <- getsState sfactionD   -- Create new factions.   s <- gameReset soptionsNxt mgameMode (Just srandom)   -- Note how we also no longer assert exploration, because there may not be@@ -730,7 +738,7 @@   -- Spawn new clients, as needed, according to new factions.   updConn   initPer-  reinitGame+  reinitGame factionDold   -- Save a just started noConfirm game to preserve history of the just   -- ended normal game, in case the user exits brutally.   writeSaveAll False True
engine-src/Game/LambdaHack/Server/MonadServer.hs view
@@ -173,6 +173,7 @@   factionD <- getsState sfactionD   bench <- getsServer $ sbenchmark . sclientOptions . soptions   noConfirmsGame <- isNoConfirmsGame+  sbandSpawned <- getsServer sbandSpawned   let fact = factionD EM.! fid       path = dataDir </> scoresFile       outputScore (worthMentioning, (ntable, pos)) =@@ -181,6 +182,8 @@         if bench || noConfirmsGame || isAIFact fact then           debugPossiblyPrint $ T.intercalate "\n"           $ HighScore.showScore tz pos (HighScore.getRecord pos ntable)+            ++ ["           Spawned groups:"+                <+> T.unwords (tail (T.words (tshow sbandSpawned)))]         else           let nScoreDict = EM.insert gameModeId ntable scoreDict           in when worthMentioning $ liftIO $
engine-src/Game/LambdaHack/Server/PeriodicM.hs view
@@ -1,13 +1,13 @@ -- | Server operations performed periodically in the game loop -- and related operations. module Game.LambdaHack.Server.PeriodicM-  ( spawnMonster, addAnyActor+  ( spawnMonster, addManyActors   , advanceTime, advanceTimeTraj, overheadActorTime, swapTime   , updateCalm, leadLevelSwitch   , endOrLoop #ifdef EXPOSE_INTERNAL     -- * Internal operations-  , rollSpawnPos, gameExit+  , addAnyActor, rollSpawnPos, gameExit #endif   ) where @@ -18,6 +18,7 @@ import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import           Data.Int (Int64)+import qualified Data.IntMap.Strict as IM import qualified Data.Text as T  import           Game.LambdaHack.Atomic@@ -56,8 +57,9 @@ -- We assume heroes are never spawned. spawnMonster :: MonadServerAtomic m => m () spawnMonster = do-  COps{cocave} <- getsState scops-  arenas <- getsServer sarenas+ COps{cocave} <- getsState scops+ arenas <- getsServer sarenas+ unless (ES.null arenas) $ do   -- Do this on only one of the arenas to prevent micromanagement,   -- e.g., spreading leaders across levels to bump monster generation.   arena <- rndToAction $ oneOf $ ES.elems arenas@@ -71,33 +73,36 @@      | otherwise -> do        totalDepth <- getsState stotalDepth        lvlSpawned <- getsServer $ fromMaybe 0 . EM.lookup arena . snumSpawned-       rc <- rndToAction-             $ monsterGenChance ldepth totalDepth lvlSpawned (CK.cactorCoeff ck)-       when rc $ do+       let perMillion =+             monsterGenChance ldepth totalDepth lvlSpawned (CK.cactorCoeff ck)+           million = 1000000+       k <- rndToAction $ randomR (1, million)+       when (k <= perMillion) $ do+         let numToSpawn | 10 * k <= perMillion = 3+                        | 4 * k <= perMillion = 2+                        | otherwise = 1+             alt Nothing = Just 1+             alt (Just n) = Just $ n + 1          modifyServer $ \ser ->-           ser {snumSpawned = EM.insert arena (lvlSpawned + 1)-                              $ snumSpawned ser}+           ser { snumSpawned = EM.insert arena (lvlSpawned + numToSpawn)+                               $ snumSpawned ser+               , sbandSpawned = IM.alter alt numToSpawn+                                $ sbandSpawned ser }          localTime <- getsState $ getLocalTime arena-         maid <- addAnyActor False lvlSpawned (CK.cactorFreq ck) arena-                             localTime Nothing-         case maid of-           Nothing -> return ()  -- suspect content; server debug elsewhere-           Just aid -> do-             b <- getsState $ getActorBody aid-             mleader <- getsState $ gleader . (EM.! bfid b) . sfactionD-             when (isNothing mleader) $ setFreshLeader (bfid b) aid+         void $ addManyActors False lvlSpawned (CK.cactorFreq ck) arena localTime+                              Nothing numToSpawn  addAnyActor :: MonadServerAtomic m             => Bool -> Int -> Freqs ItemKind -> LevelId -> Time -> Maybe Point-            -> m (Maybe ActorId)+            -> m (Maybe (ActorId, Point)) addAnyActor summoned lvlSpawned actorFreq lid time mpos = do   -- We bootstrap the actor by first creating the trunk of the actor's body   -- that contains the fixed properties of all actors of that kind.   cops <- getsState scops-  lvl <- getLevel lid+  lvl@Level{ldepth} <- getLevel lid   factionD <- getsState sfactionD-  freq <- prepareItemKind lvlSpawned lid actorFreq-  m2 <- rollItemAspect freq lid+  freq <- prepareItemKind lvlSpawned ldepth actorFreq+  m2 <- rollItemAspect freq ldepth   case m2 of     NoNewItem -> do       debugPossiblyPrint $ T.pack $@@ -123,13 +128,47 @@           rndToAction rollPos       case mrolledPos of         Just pos ->-          Just <$> registerActor summoned itemKnownRaw (itemFullRaw, itemQuant)-                                 fid pos lid time+          Just . (\aid -> (aid, pos))+          <$> registerActor summoned itemKnownRaw (itemFullRaw, itemQuant)+                            fid pos lid time         Nothing -> do           debugPossiblyPrint             "Server: addAnyActor: failed to find any free position"           return Nothing +addManyActors :: MonadServerAtomic m+              => Bool -> Int -> Freqs ItemKind -> LevelId -> Time -> Maybe Point+              -> Int+              -> m Bool+addManyActors summoned lvlSpawned actorFreq lid time mpos+              howMany = assert (howMany >= 1) $ do+  mInitialLAidPos <- case mpos of+    Just pos -> return $ Just ([], pos)+    Nothing ->+      (\(aid, pos) -> ([aid], pos))+      <$$> addAnyActor summoned lvlSpawned actorFreq lid time Nothing+  case mInitialLAidPos of+    Nothing -> return False  -- suspect content; server debug elsewhere+    Just (laid, pos) -> do+      cops@COps{coTileSpeedup} <- getsState scops+      lvl <- getLevel lid+      let validTile t = not $ Tile.isNoActor coTileSpeedup t+          ps = nearbyFreePoints cops lvl validTile pos+          psNeeded = take (howMany - length laid) ps+      when (length psNeeded < howMany - length laid) $+        debugPossiblyPrint $+          "Server: addManyActors: failed to find enough free positions at"+          <+> tshow (lid, pos)+      maidposs <- forM psNeeded $+        addAnyActor summoned lvlSpawned actorFreq lid time . Just+      case laid ++ map fst (catMaybes maidposs) of+        [] -> return False+        aid : _ -> do+          b <- getsState $ getActorBody aid+          mleader <- getsState $ gleader . (EM.! bfid b) . sfactionD+          when (isNothing mleader) $ setFreshLeader (bfid b) aid+          return True+ rollSpawnPos :: COps -> ES.EnumSet Point              -> Bool -> Bool -> LevelId -> Level -> FactionId -> State              -> Rnd (Maybe Point)@@ -284,10 +323,8 @@   factionD <- getsState sfactionD   let canSwitch fact = fst (autoDungeonLevel fact)                        -- a hack to help AI, until AI client can switch levels-                       || case fleaderMode (gplayer fact) of-                            LeaderNull -> False-                            LeaderAI _ -> True-                            LeaderUI _ -> False+                       || funderAI (gplayer fact)+                          && isJust (fleaderMode (gplayer fact))       flipFaction (_, fact) | not $ canSwitch fact = return ()       flipFaction (fid, fact) =         case gleader fact of
engine-src/Game/LambdaHack/Server/ProtocolM.hs view
@@ -147,9 +147,8 @@   return req  sendQueryUI :: (MonadServerAtomic m, MonadServerComm m)-            => FactionId -> ActorId -> m RequestUI-sendQueryUI fid _aid = do-  let respUI = RespQueryUI+            => Response -> FactionId -> ActorId -> m RequestUI+sendQueryUI respUI fid _aid = do   debug <- getsServer $ sniff . soptions   when debug $ debugResponse fid respUI   chan <- getsDict (EM.! fid)
engine-src/Game/LambdaHack/Server/StartM.hs view
@@ -66,8 +66,8 @@     ser { sfovLitLid, sfovClearLid, sfovLucidLid         , sperValidFid, sperCacheFid, sperFid } -reinitGame :: MonadServerAtomic m => m ()-reinitGame = do+reinitGame :: MonadServerAtomic m => FactionDict -> m ()+reinitGame factionDold = do   COps{coitem} <- getsState scops   pers <- getsServer sperFid   ServerOptions{scurChalSer, sknowMap, sshowItemSamples, sclientOptions}@@ -91,6 +91,7 @@       defLocal = updateDiscoKind (const discoKindFiltered) defL   factionD <- getsState sfactionD   clientStatesOld <- getsServer sclientStates+  modifyServer $ \ser -> ser {sclientStates = EM.map (const defLocal) factionD}   -- Some item kinds preserve their identity and flavour throughout   -- the whole metagame, until the savefiles is removed.   -- These are usually not man-made items, because these can be made@@ -99,16 +100,33 @@   -- so the random aspects of items, stored in @sdiscoAspect@   -- are not preserved (a lot of other state components would need   -- to be partially preserved, too, both on server and clients).-  let includeMetaGame fid = case fid `EM.lookup` clientStatesOld of-        Nothing -> defLocal-        Just sOld ->-          let disco = sdiscoKind sOld-              inMetaGame kindId = IK.SetFlag Ability.MetaGame-                                  `elem` IK.iaspects (okind coitem kindId)-              discoMetaGame = EM.filter inMetaGame disco-          in updateDiscoKind (discoMetaGame `EM.union`) defLocal-      clientStatesNew = EM.mapWithKey (\fid _ -> includeMetaGame fid) factionD-  modifyServer $ \ser -> ser {sclientStates = clientStatesNew}+  --+  -- This is a terrible temporary hack until Player becomes content+  -- and we persistently store Player information on the server.+  let metaHolder factionDict = case find (\(_, fact) ->+                                      gteamCont fact == Just (TeamContinuity 1))+                                    $ EM.assocs factionDict of+        Nothing ->+          fst <$> find (\(_, fact) -> isNothing (gteamCont fact))+                       (EM.assocs factionDict)+        Just (fid, _) -> Just fid+      mmetaHolderOld = metaHolder factionDold+  case mmetaHolderOld of+    Just metaHolderOld -> do+      let metaDiscoOld =+            let sOld = clientStatesOld EM.! metaHolderOld+                disco = sdiscoKind sOld+                inMetaGame kindId = IK.SetFlag Ability.MetaGame+                                    `elem` IK.iaspects (okind coitem kindId)+            in EM.filter inMetaGame disco+          defDiscoOld = updateDiscoKind (metaDiscoOld `EM.union`) defLocal+          metaHolderNew = fromJust $ metaHolder factionD+      modifyServer $ \ser ->+        ser {sclientStates = EM.insert metaHolderNew defDiscoOld+                             $ sclientStates ser}+    Nothing -> return ()  -- probably no previous games+  -- Hack ends.+  clientStatesNew <- getsServer sclientStates   let updRestart fid = UpdRestart fid (pers EM.! fid) (clientStatesNew EM.! fid)                                   scurChalSer sclientOptions   mapWithKeyM_ (\fid _ -> do@@ -151,7 +169,8 @@       trunkKindIds = ES.elems $ foldl' addGroupToSet ES.empty groups       minLid = fst $ minimumBy (comparing (ldepth . snd))                    $ EM.assocs dungeon-      regItem itemKindId = do+  Level{ldepth} <- getLevel minLid+  let regItem itemKindId = do         let itemKind = okind coitem itemKindId             freq = pure (itemKindId, itemKind)         case possibleActorFactions itemKind factionD of@@ -159,7 +178,7 @@           (fid, _) : _ -> do             let c = CTrunk fid minLid originPoint                 jfid = Just fid-            m2 <- rollItemAspect freq minLid+            m2 <- rollItemAspect freq ldepth             case m2 of               NoNewItem -> error "sampleTrunks: can't create actor trunk"               NewItem (ItemKnown kindIx ar _) itemFullRaw itemQuant -> do@@ -184,11 +203,12 @@       itemKindIds = ES.elems $ foldl' addGroupToSet ES.empty groups       minLid = fst $ minimumBy (comparing (ldepth . snd))                    $ EM.assocs dungeon-      regItem itemKindId = do+  Level{ldepth} <- getLevel minLid+  let regItem itemKindId = do         let itemKind = okind coitem itemKindId             freq = pure (itemKindId, itemKind)             c = CFloor minLid originPoint-        m2 <- rollItemAspect freq minLid+        m2 <- rollItemAspect freq ldepth         case m2 of           NoNewItem -> error "sampleItems: can't create sample item"           NewItem itemKnown itemFull _ ->@@ -209,7 +229,7 @@               -> Roster               -> Rnd FactionDict resetFactions factionDold gameModeIdOld curDiffSerOld totalDepth players = do-  let rawCreate (ixRaw, (gplayer@Player{..}, gteamCont, initialActors)) = do+  let rawCreate (ix, (gplayer@Player{..}, gteamCont, initialActors)) = do         let castInitialActors (ln, d, actorGroup) = do               n <- castDice (Dice.AbsDepth $ abs ln) totalDepth d               return (ln, n, actorGroup)@@ -218,10 +238,11 @@               mapFromFuns Color.legalFgCol                           [colorToTeamName, colorToPlainName, colorToFancyName]             colorName = T.toLower $ head $ T.words fname-            prefix = case fleaderMode of-              LeaderNull -> "Loose"-              LeaderAI _ -> "Autonomous"-              LeaderUI _ -> "Controlled"+            prefix = case (fleaderMode, funderAI) of+              (Nothing, False) -> "Uncoordinated"+              (Nothing, True) -> "Loose"+              (Just{}, False) -> "Autonomous"+              (Just{}, True) -> "Controlled"             gnameNew = prefix <+> if fhasGender                                   then makePhrase [MU.Ws $ MU.Text fname]                                   else fname@@ -240,12 +261,8 @@             gvictims = EM.empty             gvictimsD = gvictimsDnew             gstash = Nothing-            ix = case gteamCont of-              Just (TeamContinuity k) -> k-              _ -> ixRaw         return (toEnum $ if fhasUI then ix else -ix, Faction{..})-  -- We assume @TeamContinuity@ are small integers.-  lFs <- mapM rawCreate $ zip [100..] $ rosterList players+  lFs <- mapM rawCreate $ zip [1..] $ rosterList players   let swapIx l =         let findPlayerName name = find ((name ==) . fname . gplayer . snd)             f (name1, name2) =@@ -394,7 +411,7 @@  -- | Find starting postions for all factions. Try to make them distant -- from each other. Place as many of the factions, as possible,--- over stairs. Place the last faction(s) over escape(s)+-- over stairs. Place the first faction(s) over escape(s) -- (we assume they are guardians of the escapes). -- This implies the inital factions (if any) start far from escapes. findEntryPoss :: COps -> Level -> Int -> Rnd [Point]@@ -402,18 +419,17 @@               lvl@Level{lkind, larea, lstair, lescape}               kRaw = do   let lskip = CK.cskip $ okind cocave lkind-      k = kRaw + length lskip  -- is @lskip@ is bogus, will be too large; OK+      k = kRaw + length lskip  -- if @lskip@ is bogus, will be too large; OK       (_, xspan, yspan) = spanArea larea       factionDist = max xspan yspan - 10       dist !poss !cmin !l _ = all (\ !pos -> chessDist l pos > cmin) poss       tryFind _ 0 = return []       tryFind !ps !n = do         let ds = [ dist ps factionDist-                 , dist ps $ 2 * factionDist `div` 3                  , dist ps $ factionDist `div` 2                  , dist ps $ factionDist `div` 3-                 , dist ps $ factionDist `div` 4-                 , dist ps $ factionDist `div` 5+                 , dist ps $ max 5 $ factionDist `div` 5+                 , dist ps $ max 2 $ factionDist `div` 10                  ]         mp <- findPosTry2 500 lvl  -- try really hard, for skirmish fairness                 (\_ !t -> Tile.isWalkable coTileSpeedup t
engine-src/Game/LambdaHack/Server/State.hs view
@@ -60,6 +60,8 @@   , sacounter     :: ActorId        -- ^ stores next actor index   , sicounter     :: ItemId         -- ^ stores next item index   , snumSpawned   :: EM.EnumMap LevelId Int+                                    -- ^ how many spawned so far on the level+  , sbandSpawned  :: IM.IntMap Int  -- ^ how many times such group spawned   , sundo         :: () -- [CmdAtomic] -- ^ atomic commands performed to date   , sclientStates :: EM.EnumMap FactionId State                                     -- ^ each faction state, as seen by clients@@ -117,6 +119,7 @@     , sacounter = toEnum 0     , sicounter = toEnum 0     , snumSpawned = EM.empty+    , sbandSpawned = IM.fromList [(1, 0), (2, 0), (3, 0)]     , sundo = ()     , sclientStates = EM.empty     , sperFid = EM.empty@@ -173,6 +176,7 @@     put sacounter     put sicounter     put snumSpawned+    put sbandSpawned     put sclientStates     put (show srandom)     put srngs@@ -195,6 +199,7 @@     sacounter <- get     sicounter <- get     snumSpawned <- get+    sbandSpawned <- get     sclientStates <- get     g <- get     srngs <- get
+ test/ItemDescriptionUnitTests.hs view
@@ -0,0 +1,66 @@+module ItemDescriptionUnitTests (itemDescriptionUnitTests) where++import Prelude ()++import Game.LambdaHack.Core.Prelude++import qualified Data.EnumMap.Strict as EM++import Test.Tasty+import Test.Tasty.HUnit++import           Game.LambdaHack.Client.UI.ItemDescription+import           Game.LambdaHack.Common.Item+import           Game.LambdaHack.Common.ItemAspect+import           Game.LambdaHack.Content.ItemKind+import           Game.LambdaHack.Core.Dice+import qualified Game.LambdaHack.Definition.Ability as Ability+import           Game.LambdaHack.Definition.Color+import           Game.LambdaHack.Definition.Flavour++itemDescriptionUnitTests :: TestTree+itemDescriptionUnitTests = testGroup "itemDescriptionUnitTests" $+  let greenFlavour = head $ zipPlain [Green]+      testItemBase = Item+        { jkind = IdentityObvious (toEnum 667)+        , jfid = Nothing+        , jflavour = greenFlavour+        }+      testItemKind = ItemKind+        { isymbol  = 'x'+        , iname    = "12345678901234567890123"+        , ifreq    = [ (UNREPORTED_INVENTORY, 1) ]+        , iflavour = zipPlain [Green]+        , icount   = 1 + 1 `d` 2+        , irarity  = [(1, 50), (10, 1)]+        , iverbHit = "hit"+        , iweight  = 300+        , idamage  = 1 `d` 1+        , iaspects = [ AddSkill Ability.SkHurtMelee $ -16 * 5+                     , SetFlag Ability.Fragile+                     , toVelocity 70 ]+        , ieffects = []+        , idesc    = "A really cool test item."+        , ikit     = []+        }+      testItemFull = ItemFull+        { itemBase = testItemBase+        , itemKindId = toEnum 667+        , itemKind = testItemKind+        , itemDisco = ItemDiscoFull emptyAspectRecord+        , itemSuspect = True+        }+  in+  [ testCase "testItem_viewItem_Blackx" $+      viewItem testItemFull+      @?= attrChar2ToW32 Green 'x'+  , testCase "testItem!_viewItem_Black!" $+      viewItem testItemFull { itemKind = testItemKind { isymbol = '!' }}+      @?= attrChar2ToW32 Green '!'+  , testCase "testItem_viewItemBenefitColored_isEquip_Greenx" $+      viewItemBenefitColored (EM.singleton (toEnum 42) (Benefit True 0 0 0 0)) (toEnum 42) testItemFull+      @?= attrChar2ToW32 BrGreen 'x'+  , testCase "testItem_viewItemBenefitColored_isNotEquip_Redx" $+      viewItemBenefitColored (EM.singleton (toEnum 42) (Benefit False 0 0 0 0)) (toEnum 42) testItemFull+      @?= attrChar2ToW32 BrRed 'x'+  ]
+ test/ItemKindUnitTests.hs view
@@ -0,0 +1,78 @@+module ItemKindUnitTests (itemKindUnitTests) where++import Prelude ()++import Game.LambdaHack.Core.Prelude++import Test.Tasty+import Test.Tasty.HUnit++import           Game.LambdaHack.Content.ItemKind+import qualified Game.LambdaHack.Content.RuleKind as RK+import           Game.LambdaHack.Core.Dice+import qualified Game.LambdaHack.Definition.Ability as Ability+import           Game.LambdaHack.Definition.Color+import           Game.LambdaHack.Definition.Flavour++import qualified Content.RuleKind++itemKindUnitTests :: TestTree+itemKindUnitTests = testGroup "itemKindUnitTests" $+  let standardSymbols = RK.ritemSymbols Content.RuleKind.standardRules+      customSymbols = emptyItemSymbolsUsedInEngine {rsymbolNecklace = '*'}+      testItemKind = ItemKind+        { isymbol  = 'x'+        , iname    = "12345678901234567890123"+        , ifreq    = [ (UNREPORTED_INVENTORY, 1) ]+        , iflavour = zipPlain [Green]+        , icount   = 1 + 1 `d` 2+        , irarity  = [(1, 50), (10, 1)]+        , iverbHit = "hit"+        , iweight  = 300+        , idamage  = 1 `d` 1+        , iaspects = [ AddSkill Ability.SkHurtMelee $ -16 * 5+                     , SetFlag Ability.Fragile+                     , toVelocity 70 ]+        , ieffects = []+        , idesc    = "A really cool test item."+        , ikit     = []+        }+  in+  [ testCase "overlonginame_validateSingle_errs" $+      validateSingle standardSymbols+                     testItemKind { iname = "123456789012345678901234" }+      @?= ["iname longer than 23"]+  , testCase "shortEnoughiname_validateSingle_noErr" $+      validateSingle standardSymbols+                     testItemKind+      @?= []+  , testCase "equipableNoSlotxSymbol_validateSingle_errs" $+      validateSingle standardSymbols+                     testItemKind { iaspects = [ SetFlag Ability.Equipable ] }+      @?= ["EqpSlot not specified but Equipable or Meleeable and not a likely organ or necklace or template"]+  , testCase "equipableNoSlot,Symbol_validateSingle_noErr" $+      validateSingle standardSymbols+                     testItemKind { isymbol = ','+                                  , iaspects = [ SetFlag Ability.Equipable ] }+      @?= []+  , testCase "equipableNoSlot\"Symbol_validateSingle_noErr" $+      validateSingle standardSymbols+                     testItemKind { isymbol = '"'+                                  , iaspects = [ SetFlag Ability.Equipable ] }+      @?= []+  , testCase "equipableNoSlot/Symbol_validateSingle_noErr" $+      validateSingle standardSymbols+                     testItemKind { isymbol = '/'+                                  , iaspects = [ SetFlag Ability.Equipable ] }+      @?= []+  , testCase "equipableNoSlot*CustomRules_validateSingle_noErr" $+      validateSingle customSymbols+                     testItemKind { isymbol = '*'+                                  , iaspects = [ SetFlag Ability.Equipable ] }+      @?= []+  , testCase "equipableNoSlot\"CustomRules_validateSingle_errs" $+      validateSingle customSymbols+                     testItemKind { isymbol = '"'+                                  , iaspects = [ SetFlag Ability.Equipable ] }+      @?= ["EqpSlot not specified but Equipable or Meleeable and not a likely organ or necklace or template"]+  ]
+ test/ItemRevUnitTests.hs view
@@ -0,0 +1,77 @@+module ItemRevUnitTests (itemRevUnitTests) where++import Prelude ()++import Game.LambdaHack.Core.Prelude++import qualified Control.Monad.Trans.State.Strict as St+import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES+import qualified Data.Vector.Unboxed as U+import qualified System.Random.SplitMix32 as SM++import Test.Tasty+import Test.Tasty.HUnit++import           Game.LambdaHack.Content.ItemKind+import           Game.LambdaHack.Core.Dice+import           Game.LambdaHack.Core.Random+import qualified Game.LambdaHack.Definition.Ability as Ability+import           Game.LambdaHack.Definition.Color+import           Game.LambdaHack.Definition.DefsInternal+import           Game.LambdaHack.Definition.Flavour+import           Game.LambdaHack.Server.ItemRev++itemRevUnitTests :: TestTree+itemRevUnitTests = testGroup "itemRevUnitTests" $+  let testItemKind = ItemKind+        { isymbol  = 'x'+        , iname    = "12345678901234567890123"+        , ifreq    = [ (UNREPORTED_INVENTORY, 1) ]+        , iflavour = zipStory [Black]+        , icount   = 1 + 1 `d` 2+        , irarity  = [(1, 50), (10, 1)]+        , iverbHit = "hit"+        , iweight  = 300+        , idamage  = 1 `d` 1+        , iaspects = [ AddSkill Ability.SkHurtMelee $ -16 * 5+                     , SetFlag Ability.Fragile+                     , toVelocity 70 ]+        , ieffects = []+        , idesc    = "A really cool test item."+        , ikit     = []+        }+      testItemKind2Flavours = testItemKind+        { iflavour = zipStory [Black,Green] }+      emptyIdToFlavourSymbolToFlavourSetPair = ( EM.empty, EM.empty )+      singletonIdToFlavourSymbolToFlavourSetPair =+        ( EM.singleton (toContentId 0) dummyFlavour+        , EM.singleton 'x' (ES.singleton dummyFlavour) )+      flavourBlack = head $ zipStory [Black]+      flavourGreen = head $ zipStory [Green]+  in+  [ testCase "empty & default initializers -> first is single dummy result" $+      let rndMapPair0 = return emptyIdToFlavourSymbolToFlavourSetPair+          (mapPair1, _) = St.runState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) testItemKind) $ SM.mkSMGen 1+        in fst mapPair1 @?= EM.singleton (toContentId 0) dummyFlavour+  , testCase "empty & default initializers -> second is empty" $+      let rndMapPair0 = return emptyIdToFlavourSymbolToFlavourSetPair+          (mapPair1, _) = St.runState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) testItemKind) $ SM.mkSMGen 1+        in snd mapPair1 @?= EM.empty+  , testCase "singleton initializers -> first is single dummy result" $+      let rndMapPair0 = return singletonIdToFlavourSymbolToFlavourSetPair+          (mapPair1, _) = St.runState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) testItemKind) $ SM.mkSMGen 1+        in fst mapPair1 @?= EM.singleton (toContentId 0) dummyFlavour+  , testCase "singleton initializers -> second is single dummy result" $+      let rndMapPair0 = return singletonIdToFlavourSymbolToFlavourSetPair+          (mapPair1, _) = St.runState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) testItemKind) $ SM.mkSMGen 1+        in snd mapPair1 @?= EM.singleton 'x' (ES.singleton dummyFlavour)+  , testCase "rollFlavourMap on two flavours -> first flavour can be rolled" $  -- relies on us not messing with RNG+      let rndMapPair0 = return singletonIdToFlavourSymbolToFlavourSetPair+          (mapPair1, _) = St.runState (rollFlavourMap (U.singleton invalidInformationCode) rndMapPair0 (toContentId 0) testItemKind2Flavours) $ SM.mkSMGen 1+        in fst mapPair1 @?= EM.singleton (toContentId 0) flavourBlack+  , testCase "rollFlavourMap on two flavours -> second flavour can be rolled" $  -- relies on us not messing with RNG+      let rndMapPair0 = return singletonIdToFlavourSymbolToFlavourSetPair+          (mapPair1, _) = St.runState (rollFlavourMap (U.singleton invalidInformationCode) rndMapPair0 (toContentId 0) testItemKind2Flavours) $ SM.mkSMGen 2+        in fst mapPair1 @?= EM.singleton (toContentId 0) flavourGreen+  ]
+ test/ReqFailureUnitTests.hs view
@@ -0,0 +1,79 @@+module ReqFailureUnitTests (reqFailureUnitTests) where++import Prelude ()++import Game.LambdaHack.Core.Prelude++import Test.Tasty+import Test.Tasty.HUnit++import           Game.LambdaHack.Common.Item+import           Game.LambdaHack.Common.ItemAspect+import           Game.LambdaHack.Common.ReqFailure+import           Game.LambdaHack.Common.Time+import           Game.LambdaHack.Content.ItemKind+import           Game.LambdaHack.Core.Dice+import qualified Game.LambdaHack.Definition.Ability as Ability+import           Game.LambdaHack.Definition.Color+import           Game.LambdaHack.Definition.Defs+import           Game.LambdaHack.Definition.Flavour++import qualified Content.RuleKind++reqFailureUnitTests :: TestTree+reqFailureUnitTests = testGroup "reqFailureUnitTests" $+  let testItemKind = ItemKind+        { isymbol  = 'x'+        , iname    = "12345678901234567890123"+        , ifreq    = [ (UNREPORTED_INVENTORY, 1) ]+        , iflavour = zipPlain [Green]+        , icount   = 1 + 1 `d` 2+        , irarity  = [(1, 50), (10, 1)]+        , iverbHit = "hit"+        , iweight  = 300+        , idamage  = 1 `d` 1+        , iaspects = [ AddSkill Ability.SkHurtMelee $ -16 * 5+                     , SetFlag Ability.Fragile+                     , toVelocity 70 ]+        , ieffects = []+        , idesc    = "A really cool test item."+        , ikit     = []+        }+      testItemFull = ItemFull+        { itemBase = Item+            { jkind = IdentityObvious (toEnum 667)+            , jfid = Nothing+            , jflavour = dummyFlavour+            }+        , itemKindId = toEnum 667+        , itemKind = testItemKind+        , itemDisco = ItemDiscoFull emptyAspectRecord+        , itemSuspect = True+        }+      standardRules = Content.RuleKind.standardRules+  in+  [ testCase "oneSkillAndxsymbol_permittedApply_FailureApplyFood" $+      permittedApply standardRules timeZero 1 True Nothing+                     testItemFull quantSingle+      @?= Left ApplyFood+  , testCase "oneSkillAnd,symbolAndCGround_permittedApply_True" $+      permittedApply standardRules timeZero 1 True (Just CGround)+                     testItemFull {itemKind = testItemKind{isymbol = ','}}+                     quantSingle+      @?= Right True+  , testCase "oneSkillAnd\"symbol_permittedApply_True" $+      permittedApply standardRules timeZero 1 True Nothing+                     testItemFull {itemKind = testItemKind{isymbol = '"'}}+                     quantSingle+      @?= Right True+  , testCase "twoSkillAnd?symbol_permittedApply_FailureApplyRead" $+      permittedApply standardRules timeZero 2 True Nothing+                     testItemFull {itemKind = testItemKind{isymbol = '?'}}+                     quantSingle+      @?= Left ApplyRead+  , testCase "twoSkillAnd,symbol_premittedApply_True" $+      permittedApply standardRules timeZero 2 True Nothing+                     testItemFull {itemKind = testItemKind{isymbol = ','}}+                     quantSingle+      @?= Right True+  ]
test/SessionUIMock.hs view
@@ -1,5 +1,5 @@ module SessionUIMock-  ( unwindMacros, unwindMacrosAcc+  ( unwindMacros   ) where  import Prelude ()@@ -119,8 +119,8 @@         return (Just km)     KeyMacro [] -> return Nothing -unwindMacros :: IC.InputContent -> KeyMacro -> [(BufferTrace, ActionLog)]-unwindMacros coinput keyPending =+unwindMacrosFull :: IC.InputContent -> KeyMacro -> [(BufferTrace, ActionLog)]+unwindMacrosFull coinput keyPending =   let initSession = SessionUIMock         { smacroFrame = emptyMacroFrame {keyPending}         , smacroStack = []@@ -132,11 +132,13 @@ accumulateActions ba =   let (buffers, actions) = unzip ba       actionlog = concat <$> inits actions-  in zip buffers actionlog+  in if snd (last ba) == "Macro looped"+     then ba+     else zip buffers actionlog -unwindMacrosAcc :: IC.InputContent -> KeyMacro -> [(BufferTrace, ActionLog)]-unwindMacrosAcc coinput keyPending =-  accumulateActions $ unwindMacros coinput keyPending+unwindMacros :: IC.InputContent -> KeyMacro -> [(BufferTrace, ActionLog)]+unwindMacros coinput keyPending =+  accumulateActions $ unwindMacrosFull coinput keyPending  renderTrace :: [KeyMacroFrame] -> BufferTrace renderTrace macroFrames =
+ test/SessionUIUnitTests.hs view
@@ -0,0 +1,312 @@+module SessionUIUnitTests (macroTests) where++import Prelude ()++import Game.LambdaHack.Core.Prelude++import qualified Data.Map.Strict as M+import           Test.Tasty+import           Test.Tasty.HUnit+import           Test.Tasty.QuickCheck++import qualified Game.LambdaHack.Client.UI.Content.Input as IC+import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd+import qualified Game.LambdaHack.Client.UI.Key as K+import           Game.LambdaHack.Client.UI.SessionUI++import qualified Client.UI.Content.Input as Content.Input++import SessionUIMock++-- Run @test -p "In-game" --quickcheck-verbose@ to verify that quickcheck+-- properties are not too often satisfied voidly.+macroTests :: TestTree+macroTests = testGroup "macroTests" $+  let coinput = IC.makeData Nothing Content.Input.standardKeysAndMouse+      stringToKeyMacro = KeyMacro . map (K.mkKM . (: []))+      listToKeyMacro = KeyMacro . map K.mkKM+      bindInput l input =+        let ltriple = M.fromList $ map (\(k, ks) ->+              (K.mkKM k, ([], "", HumanCmd.Macro $ map (: []) ks))) l+        in input {IC.bcmdMap = M.union ltriple $ IC.bcmdMap input}+  in [ testCase "Macro 1 from PR#192 description" $+         fst <$> unwindMacros coinput (stringToKeyMacro "'j''j'")+         @?= [ [ (Right "",     "'j''j'",  "")  ]+             , [ (Left  "",     "j''j'",   "") ]+             , [ (Left  "j",    "''j'",    "j") ]+             , [ (Right "j",    "'j'",     "j") ]+             , [ (Left  "",     "j'",      "j") ]+             , [ (Left  "j",    "'",       "j") ]+             , [ (Right "j",    "",        "j") ]+             ]+     , testCase "Macro 1 from Issue#189 description" $+         snd (last (unwindMacros (bindInput [ ("a", "'bc'V")+                                            , ("c", "'aaa'V") ] coinput)+                                 (stringToKeyMacro "a")))+         @?= "Macro looped"+     , testCase "Macro 2 from Issue#189 description" $+         snd (last (unwindMacros (bindInput [("a", "'x'")] coinput)+                                 (stringToKeyMacro "'a'")))+         @?= "x"+     , testCase "Macro 3 from Issue#189 description" $+         snd (last (unwindMacros coinput+                                 (stringToKeyMacro "'x''x'")))+         @?= "xx"+     , testCase "Macro 4 from Issue#189 description" $+         snd (last (unwindMacros coinput+                                 (stringToKeyMacro "'x''x'V")))+         @?= "xxx"+     , testCase "Macro 5 from Issue#189 description" $+         snd (last (unwindMacros coinput+                                 (stringToKeyMacro "x'x'V")))+         @?= "xxx"+     , testCase "Macro test 10" $+         snd (last (unwindMacros coinput+                                 (stringToKeyMacro "x'y'V")))+         @?= "xyy"+     , testCase "Macro test 11" $+         snd (last (unwindMacros coinput+                                 (stringToKeyMacro "'x''y'V")))+         @?= "xyy"+     , testCase "Macro test 12" $+         snd (last (unwindMacros coinput+                                 (listToKeyMacro ["x", "C-V"])))+         @?= "x"+     , testCase "Macro test 13" $+         snd (last (unwindMacros coinput+                                 (listToKeyMacro ["'", "x", "'", "C-V"])))+         @?= "xxxxxxxxxxxxxxxxxxxxxxxxxx"+     , testCase "Macro test 14" $+         snd (last (unwindMacros coinput+                                 (listToKeyMacro ["'", "x", "'", "y", "C-V"])))+         @?= "xyxxxxxxxxxxxxxxxxxxxxxxxxx"+     , testCase "Macro test 15" $+         snd (last (unwindMacros (bindInput [("a", "x")] coinput)+                                 (stringToKeyMacro "'a'V")))+         @?= "xx"+     , testCase "Macro test 16" $+         snd (last (unwindMacros (bindInput [("a", "'x'")] coinput)+                                 (stringToKeyMacro "'a'V")))+         @?= "xx"+     , testCase "Macro test 17" $+         snd (last (unwindMacros (bindInput [("a", "'x'V")] coinput)+                                 (stringToKeyMacro "a")))+         @?= "xx"+     , testCase "Macro test 18" $+         snd (last (unwindMacros (bindInput [("a", "'x'V")] coinput)+                                 (stringToKeyMacro "'a'")))+         @?= "xx"+     , testCase "Macro test 19" $+         snd (last (unwindMacros (bindInput [("a", "'x'V")] coinput)+                                 (stringToKeyMacro "'a'V")))+         @?= "xxxx"+     , testCase "Macro test 20" $+         snd (last (unwindMacros (bindInput [ ("a", "'bz'V")+                                            , ("c", "'aaa'V") ] coinput)+                                 (stringToKeyMacro "c")))+         @?= "bzbzbzbzbzbzbzbzbzbzbzbz"+     , testCase "RepeatLast test 10" $+         snd (last (unwindMacros coinput+                                 (stringToKeyMacro "x'y'v")))+         @?= "xyy"+     , testCase "RepeatLast test 11" $+         snd (last (unwindMacros coinput+                                 (stringToKeyMacro "'x'yv")))+         @?= "xyy"+     , testCase "RepeatLast test 12" $+         snd (last (unwindMacros coinput+                                 (listToKeyMacro ["v", "C-v"])))+         @?= ""+     , testCase "RepeatLast test 13" $+         snd (last (unwindMacros coinput+                                 (listToKeyMacro ["'", "x", "'", "C-v"])))+         @?= "xxxxxxxxxxxxxxxxxxxxxxxxxx"+     , testCase "RepeatLast test 14" $+         snd (last (unwindMacros coinput+                                 (listToKeyMacro ["'", "x", "'", "V", "C-v"])))+         @?= "xxxxxxxxxxxxxxxxxxxxxxxxxxx"+     , testCase "RepeatLast test 15" $+         snd (last (unwindMacros (bindInput [("a", "x")] coinput)+                                 (stringToKeyMacro "av")))+         @?= "xx"+     , testCase "RepeatLast test 16" $+         snd (last (unwindMacros (bindInput [("a", "'x'")] coinput)+                                 (stringToKeyMacro "'a'v")))+         @?= "xx"+     , testCase "RepeatLast test 17" $+         snd (last (unwindMacros (bindInput [("a", "'x'v")] coinput)+                                 (stringToKeyMacro "a")))+         @?= "xx"+     , testCase "RepeatLast test 18" $+         snd (last (unwindMacros (bindInput [("a", "'x'v")] coinput)+                                 (stringToKeyMacro "'a'")))+         @?= "xx"+     , testCase "RepeatLast test 19" $+         snd (last (unwindMacros (bindInput [("a", "'x'v")] coinput)+                                 (stringToKeyMacro "'a'v")))+         @?= "xxxx"+     , testCase "RepeatLast test 20" $+         snd (last (unwindMacros (bindInput [ ("a", "'bz'v")+                                            , ("c", "'aaa'v") ] coinput)+                                 (stringToKeyMacro "c")))+         @?= "bzzbzzbzzbzz"+     , testCase "RepeatLast test 21" $+         snd (last (unwindMacros (bindInput [("a", "'x'V")] coinput)+                                 (stringToKeyMacro "'a'v")))+         @?= "xxxx"+     , testCase "RepeatLast test 22" $+         snd (last (unwindMacros (bindInput [("a", "'xy'V")] coinput)+                                 (stringToKeyMacro "'aa'v")))+         @?= "xyxyxyxyxyxy"+     , testCase "RepeatLast test 23" $+         snd (last (unwindMacros (bindInput [("a", "'xy'v")] coinput)+                                 (stringToKeyMacro "'aa'V")))+         @?= "xyyxyyxyyxyy"+     , testCase "RepeatLast test 24" $+         snd (last (unwindMacros (bindInput [("a", "'xy'vv")] coinput)+                                 (stringToKeyMacro "'aa'vv")))+         @?= "xyyyxyyyxyyyxyyy"+     , testCase "RepeatLast test 25" $+         snd (last (unwindMacros (bindInput [("a", "'xyv'v")] coinput)+                                 (stringToKeyMacro "'a'a'vv'")))+         @?= "xyyyxyyyxyyyxyyy"+     , testCase "RepeatLast test 26" $+         snd (last (unwindMacros (bindInput [ ("a", "'xy'v")+                                            , ("b", "'za'v")+                                            , ("c", "'ab'v") ] coinput)+                                 (stringToKeyMacro "'c'v")))+         @?= "xyyzxyyxyyzxyyxyyxyyzxyyxyyzxyyxyy"+     , testCase "RepeatLast test 27" $+         snd (last (unwindMacros (bindInput [ ("a", "'xy'V")+                                            , ("b", "'za'v")+                                            , ("c", "'ab'v") ] coinput)+                                 (stringToKeyMacro "'c'v")))+         @?= "xyxyzxyxyxyxyzxyxyxyxyxyxyzxyxyxyxyzxyxyxyxy"+     , testCase "RepeatLast test 28" $+         snd (last (unwindMacros (bindInput [ ("a", "'xy'v")+                                            , ("b", "'za'V")+                                            , ("c", "'ab'v") ] coinput)+                                 (stringToKeyMacro "'c'v")))+         @?= "xyyzxyyzxyyzxyyzxyyxyyzxyyzxyyzxyyzxyy"+     , testCase "RepeatLast test 29" $+         snd (last (unwindMacros (bindInput [ ("a", "'xy'v")+                                            , ("b", "'za'V")+                                            , ("c", "'ab'V") ] coinput)+                                 (stringToKeyMacro "'c'v")))+         @?= "xyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyy"+     , testCase "RepeatLast test 30" $+         snd (last (unwindMacros (bindInput [ ("a", "'xy'v")+                                            , ("b", "'za'V")+                                            , ("c", "'ab'V") ] coinput)+                                 (stringToKeyMacro "'c'V")))+         @?= "xyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyy"+     , testCase "RepeatLast test 31" $+         snd (last (unwindMacros (bindInput [ ("a", "'xy'v")+                                            , ("b", "'za'v")+                                            , ("c", "'ab'V") ] coinput)+                                 (stringToKeyMacro "'c'V")))+         @?= "xyyzxyyxyyxyyzxyyxyyxyyzxyyxyyxyyzxyyxyy"+     , testCase "RepeatLast test 32" $+         snd (last (unwindMacros (bindInput [ ("a", "'xy'v")+                                            , ("b", "'za'v") ] coinput)+                                 (stringToKeyMacro "'ab'vv")))+         @?= "xyyzxyyxyyzxyyxyyzxyyxyy"+     , testCase "RepeatLast test 33" $+         snd (last (unwindMacros (bindInput [ ("a", "'xy'V") ] coinput)+                                 (stringToKeyMacro "a'za'vvv")))+         @?= "xyxyzxyxyxyxyxyxyxyxy"+     , testCase "RepeatLast test 34" $+         snd (last (unwindMacros (bindInput [ ("a", "'xy'v")+                                            , ("c", "a'za'Vv") ] coinput)+                                 (stringToKeyMacro "'c'v")))+         @?= "xyyzxyyzxyyzxyyxyyzxyyzxyyzxyy"+     , testCase "RepeatLast test 35" $+         snd (last (unwindMacros (bindInput [ ("a", "'xy'v")+                                            , ("b", "'za'V") ] coinput)+                                 (stringToKeyMacro "'ab'Vv")))+         @?= "xyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyy"+     , testCase "RepeatLast test 36" $+         snd (last (unwindMacros (bindInput [ ("a", "'xy'v")+                                            , ("b", "za'za'") ] coinput)+                                 (stringToKeyMacro "'ab'V'ab'V")))+         @?= "xyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyy"+     , testCase "RepeatLast test 37" $+         snd (last (unwindMacros (bindInput [ ("b", "z'xy'vv")+                                            , ("c", "'xyvb'V") ] coinput)+                                 (stringToKeyMacro "'c'V")))+         @?= "xyyzxyyyxyyzxyyyxyyzxyyyxyyzxyyy"+     , testCase "RepeatLast test 38" $+         snd (last (unwindMacros coinput (stringToKeyMacro "'xv'V")))+         @?= "xxxx"+     , testCase "RepeatLast test 39" $+         fst <$> unwindMacros coinput (stringToKeyMacro "'xv'V")+         @?= [[(Right   "", "'xv'V",  "")],+              [(Left    "",  "xv'V",  "")],+              [(Left   "x",   "v'V", "x")],+              [(Left   "x",   "x'V", "x")],+              [(Left  "xx",    "'V", "x")],+              [(Right "xx",     "V", "x")],+              [(Right   "",    "xx",  ""), (Right "xx",    "", "V")],+              [(Right   "",     "x", "x"), (Right "xx",    "", "V")],+              [(Right "xx",      "", "V")]]+     , testCase "RepeatLast test 40" $+         snd (last (unwindMacros coinput (stringToKeyMacro "'xy'Vv")))+         @?= "xyxyxy"+     , testCase+         "RepeatLast test 41; named macros not referentially transparent" $+         snd (last (unwindMacros (bindInput [("a", "'xy'V")] coinput)+                                 (stringToKeyMacro "av")))+         @?= "xyxyxyxy"  -- because @a@ repeated; good!+     , testCase "RepeatLast test 42" $+         snd (last (unwindMacros (bindInput [("a", "xy")] coinput)+                                 (stringToKeyMacro "'a'Vv")))+         @?= "xyxyxy"  -- because @V@ repeated; good!+     , testCase "RepeatLast test 43" $+         snd (last (unwindMacros coinput (stringToKeyMacro "'xyV'V")))+         @?= "xyxy"+     , testCase "RepeatLast test 44" $+         snd (last (unwindMacros coinput (stringToKeyMacro "'xyV'v")))+         @?= "xyxy"+     , testCase "RepeatLast test 45" $+         snd (last (unwindMacros (bindInput [("a", "xyV")] coinput)+                                 (stringToKeyMacro "'a'V")))+         @?= "xyxy"+     , testCase "RepeatLast test 46" $+         snd (last (unwindMacros (bindInput [("a", "xyV")] coinput)+                                 (stringToKeyMacro "'a'v")))+         @?= "xyxy"+     , testProperty "In-game macro and equivalent predefined macro agree" $+         forAll (listOf (elements "`ABvV")) $+           \macro ->+             let bindings = bindInput [("a", macro)] coinput+                 inGameResult =+                   snd (last (unwindMacros coinput+                                           (stringToKeyMacro macro)))+             in inGameResult+                === snd (last (unwindMacros bindings+                                            (stringToKeyMacro "a")))+                .&&. inGameResult =/= "Macro looped"  -- may not loop+     , testProperty "In-game and predefined with limited minimal bindings" $+         forAll (listOf (elements "````''''ABCABCABCABCvVvVvVa")) $  -- may loop+           \macro ->+             let bindings = bindInput [("a", macro)] coinput+             in snd (last (unwindMacros bindings+                                        (stringToKeyMacro macro)))+                === snd (last (unwindMacros bindings+                                            (stringToKeyMacro "a")))+     , testProperty "In-game and predefined with limited multiple bindings" $+         forAll (listOf (elements "```ABCDvVabccc")) $+           \macro ->+             -- The macros may still loop due to mutual recursion,+             -- even though direct recursion is ruled out by filtering.+             let macroA = filter (/= 'a') macro+                 macroB = filter (/= 'b') $ take 5 $ reverse macro+                 bindings = bindInput [ ("a", macroA)+                                      , ("b", macroB)+                                      , ("c", "A'B''CD'") ]+                                      coinput+             in snd (last (unwindMacros bindings+                                        (stringToKeyMacro macroA)))+                === snd (last (unwindMacros bindings+                                            (stringToKeyMacro "a")))+     ]
test/Spec.hs view
@@ -4,294 +4,37 @@  import Game.LambdaHack.Core.Prelude -import qualified Data.Map.Strict as M import qualified Data.Text as T import           Options.Applicative import           System.IO.Unsafe (unsafePerformIO) import           Test.Tasty import           Test.Tasty.HUnit -import qualified Game.LambdaHack.Client.UI.Content.Input as IC-import           Game.LambdaHack.Client.UI.Frontend.Chosen-import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd-import qualified Game.LambdaHack.Client.UI.Key as K-import           Game.LambdaHack.Client.UI.SessionUI import           Game.LambdaHack.Client.UI.UIOptions import           Game.LambdaHack.Client.UI.UIOptionsParse import           Game.LambdaHack.Common.ClientOptions import qualified Game.LambdaHack.Content.RuleKind as RK import           Game.LambdaHack.Server -import qualified Client.UI.Content.Input as Content.Input import qualified Content.RuleKind import           TieKnot -import SessionUIMock+import ItemDescriptionUnitTests+import ItemKindUnitTests+import ItemRevUnitTests+import ReqFailureUnitTests+import SessionUIUnitTests  main :: IO () main = defaultMain tests  tests :: TestTree-tests = testGroup "Tests" [macroTests, integrationTests]--macroTests :: TestTree-macroTests = testGroup "macroTests" $-  let coinput = IC.makeData Nothing Content.Input.standardKeysAndMouse-      stringToKeyMacro = KeyMacro . map (K.mkKM . (: []))-      listToKeyMacro = KeyMacro . map K.mkKM-      bindInput l input =-        let ltriple = M.fromList $ map (\(k, ks) ->-              (K.mkKM k, ([], "", HumanCmd.Macro $ map (: []) ks))) l-        in input {IC.bcmdMap = M.union ltriple $ IC.bcmdMap input}-  in [ testCase "Macro 1 from PR#192 description" $-         fst <$> unwindMacros coinput (stringToKeyMacro "'j''j'")-         @?= [ [ (Right "",     "'j''j'",  "")  ]-             , [ (Left  "",     "j''j'",   "") ]-             , [ (Left  "j",    "''j'",    "j") ]-             , [ (Right "j",    "'j'",     "j") ]-             , [ (Left  "",     "j'",      "j") ]-             , [ (Left  "j",    "'",       "j") ]-             , [ (Right "j",    "",        "j") ]-             ]-     , testCase "Macro 1 from Issue#189 description" $-         snd (last (unwindMacros (bindInput [ ("a", "'bc'V")-                                            , ("c", "'aaa'V") ] coinput)-                                 (stringToKeyMacro "a")))-         @?= "Macro looped"-     , testCase "Macro 2 from Issue#189 description" $-         snd (last (unwindMacrosAcc (bindInput [("a", "'x'")] coinput)-                                    (stringToKeyMacro "'a'")))-         @?= "x"-     , testCase "Macro 3 from Issue#189 description" $-         snd (last (unwindMacrosAcc coinput-                                    (stringToKeyMacro "'x''x'")))-         @?= "xx"-     , testCase "Macro 4 from Issue#189 description" $-         snd (last (unwindMacrosAcc coinput-                                    (stringToKeyMacro "'x''x'V")))-         @?= "xxx"-     , testCase "Macro 5 from Issue#189 description" $-         snd (last (unwindMacrosAcc coinput-                                    (stringToKeyMacro "x'x'V")))-         @?= "xxx"-     , testCase "Macro test 10" $-         snd (last (unwindMacrosAcc coinput-                                    (stringToKeyMacro "x'y'V")))-         @?= "xyy"-     , testCase "Macro test 11" $-         snd (last (unwindMacrosAcc coinput-                                    (stringToKeyMacro "'x''y'V")))-         @?= "xyy"-     , testCase "Macro test 12" $-         snd (last (unwindMacrosAcc coinput-                                    (listToKeyMacro ["x", "C-V"])))-         @?= "x"-     , testCase "Macro test 13" $-         snd (last (unwindMacrosAcc coinput-                                    (listToKeyMacro ["'", "x", "'", "C-V"])))-         @?= "xxxxxxxxxxxxxxxxxxxxxxxxxx"-     , testCase "Macro test 14" $-         snd (last (unwindMacrosAcc coinput-                                    (listToKeyMacro-                                       ["'", "x", "'", "y", "C-V"])))-         @?= "xyxxxxxxxxxxxxxxxxxxxxxxxxx"-     , testCase "Macro test 15" $-         snd (last (unwindMacrosAcc (bindInput [("a", "x")] coinput)-                                    (stringToKeyMacro "'a'V")))-         @?= "xx"-     , testCase "Macro test 16" $-         snd (last (unwindMacrosAcc (bindInput [("a", "'x'")] coinput)-                                    (stringToKeyMacro "'a'V")))-         @?= "xx"-     , testCase "Macro test 17" $-         snd (last (unwindMacrosAcc (bindInput [("a", "'x'V")] coinput)-                                    (stringToKeyMacro "a")))-         @?= "xx"-     , testCase "Macro test 18" $-         snd (last (unwindMacrosAcc (bindInput [("a", "'x'V")] coinput)-                                    (stringToKeyMacro "'a'")))-         @?= "xx"-     , testCase "Macro test 19" $-         snd (last (unwindMacrosAcc (bindInput [("a", "'x'V")] coinput)-                                    (stringToKeyMacro "'a'V")))-         @?= "xxxx"-     , testCase "Macro test 20" $-         snd (last (unwindMacrosAcc (bindInput [ ("a", "'bz'V")-                                               , ("c", "'aaa'V") ] coinput)-                                    (stringToKeyMacro "c")))-         @?= "bzbzbzbzbzbzbzbzbzbzbzbz"-     , testCase "RepeatLast test 10" $-         snd (last (unwindMacrosAcc coinput-                                    (stringToKeyMacro "x'y'v")))-         @?= "xyy"-     , testCase "RepeatLast test 11" $-         snd (last (unwindMacrosAcc coinput-                                    (stringToKeyMacro "'x'yv")))-         @?= "xyy"-     , testCase "RepeatLast test 12" $-         snd (last (unwindMacrosAcc coinput-                                    (listToKeyMacro ["v", "C-v"])))-         @?= ""-     , testCase "RepeatLast test 13" $-         snd (last (unwindMacrosAcc coinput-                                    (listToKeyMacro ["'", "x", "'", "C-v"])))-         @?= "xxxxxxxxxxxxxxxxxxxxxxxxxx"-     , testCase "RepeatLast test 14" $-         snd (last (unwindMacrosAcc coinput-                                    (listToKeyMacro-                                       ["'", "x", "'", "V", "C-v"])))-         @?= "xxxxxxxxxxxxxxxxxxxxxxxxxxx"-     , testCase "RepeatLast test 15" $-         snd (last (unwindMacrosAcc (bindInput [("a", "x")] coinput)-                                    (stringToKeyMacro "av")))-         @?= "xx"-     , testCase "RepeatLast test 16" $-         snd (last (unwindMacrosAcc (bindInput [("a", "'x'")] coinput)-                                    (stringToKeyMacro "'a'v")))-         @?= "xx"-     , testCase "RepeatLast test 17" $-         snd (last (unwindMacrosAcc (bindInput [("a", "'x'v")] coinput)-                                    (stringToKeyMacro "a")))-         @?= "xx"-     , testCase "RepeatLast test 18" $-         snd (last (unwindMacrosAcc (bindInput [("a", "'x'v")] coinput)-                                    (stringToKeyMacro "'a'")))-         @?= "xx"-     , testCase "RepeatLast test 19" $-         snd (last (unwindMacrosAcc (bindInput [("a", "'x'v")] coinput)-                                    (stringToKeyMacro "'a'v")))-         @?= "xxxx"-     , testCase "RepeatLast test 20" $-         snd (last (unwindMacrosAcc (bindInput [ ("a", "'bz'v")-                                         , ("c", "'aaa'v") ] coinput)-                                    (stringToKeyMacro "c")))-         @?= "bzzbzzbzzbzz"-     , testCase "RepeatLast test 21" $-         snd (last (unwindMacrosAcc (bindInput [("a", "'x'V")] coinput)-                                    (stringToKeyMacro "'a'v")))-         @?= "xxxx"-     , testCase "RepeatLast test 22" $-         snd (last (unwindMacrosAcc (bindInput [("a", "'xy'V")] coinput)-                                    (stringToKeyMacro "'aa'v")))-         @?= "xyxyxyxyxyxy"-     , testCase "RepeatLast test 23" $-         snd (last (unwindMacrosAcc (bindInput [("a", "'xy'v")] coinput)-                                    (stringToKeyMacro "'aa'V")))-         @?= "xyyxyyxyyxyy"-     , testCase "RepeatLast test 24" $-         snd (last (unwindMacrosAcc (bindInput [("a", "'xy'vv")] coinput)-                                    (stringToKeyMacro "'aa'vv")))-         @?= "xyyyxyyyxyyyxyyy"-     , testCase "RepeatLast test 25" $-         snd (last (unwindMacrosAcc (bindInput [("a", "'xyv'v")] coinput)-                                    (stringToKeyMacro "'a'a'vv'")))-         @?= "xyyyxyyyxyyyxyyy"-     , testCase "RepeatLast test 26" $-         snd (last (unwindMacrosAcc (bindInput [ ("a", "'xy'v")-                                               , ("b", "'za'v")-                                               , ("c", "'ab'v") ] coinput)-                                    (stringToKeyMacro "'c'v")))-         @?= "xyyzxyyxyyzxyyxyyxyyzxyyxyyzxyyxyy"-     , testCase "RepeatLast test 27" $-         snd (last (unwindMacrosAcc (bindInput [ ("a", "'xy'V")-                                               , ("b", "'za'v")-                                               , ("c", "'ab'v") ] coinput)-                                    (stringToKeyMacro "'c'v")))-         @?= "xyxyzxyxyxyxyzxyxyxyxyxyxyzxyxyxyxyzxyxyxyxy"-     , testCase "RepeatLast test 28" $-         snd (last (unwindMacrosAcc (bindInput [ ("a", "'xy'v")-                                               , ("b", "'za'V")-                                               , ("c", "'ab'v") ] coinput)-                                    (stringToKeyMacro "'c'v")))-         @?= "xyyzxyyzxyyzxyyzxyyxyyzxyyzxyyzxyyzxyy"-     , testCase "RepeatLast test 29" $-         snd (last (unwindMacrosAcc (bindInput [ ("a", "'xy'v")-                                               , ("b", "'za'V")-                                               , ("c", "'ab'V") ] coinput)-                                    (stringToKeyMacro "'c'v")))-         @?= "xyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyy"-     , testCase "RepeatLast test 30" $-         snd (last (unwindMacrosAcc (bindInput [ ("a", "'xy'v")-                                               , ("b", "'za'V")-                                               , ("c", "'ab'V") ] coinput)-                                    (stringToKeyMacro "'c'V")))-         @?= "xyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyy"-     , testCase "RepeatLast test 31" $-         snd (last (unwindMacrosAcc (bindInput [ ("a", "'xy'v")-                                               , ("b", "'za'v")-                                               , ("c", "'ab'V") ] coinput)-                                    (stringToKeyMacro "'c'V")))-         @?= "xyyzxyyxyyxyyzxyyxyyxyyzxyyxyyxyyzxyyxyy"-     , testCase "RepeatLast test 32" $-         snd (last (unwindMacrosAcc (bindInput [ ("a", "'xy'v")-                                               , ("b", "'za'v") ] coinput)-                                    (stringToKeyMacro "'ab'vv")))-         @?= "xyyzxyyxyyzxyyxyyzxyyxyy"-     , testCase "RepeatLast test 33" $-         snd (last (unwindMacrosAcc (bindInput [ ("a", "'xy'V") ] coinput)-                                    (stringToKeyMacro "a'za'vvv")))-         @?= "xyxyzxyxyxyxyxyxyxyxy"-     , testCase "RepeatLast test 34" $-         snd (last (unwindMacrosAcc (bindInput [ ("a", "'xy'v")-                                               , ("c", "a'za'Vv") ] coinput)-                                    (stringToKeyMacro "'c'v")))-         @?= "xyyzxyyzxyyzxyyxyyzxyyzxyyzxyy"-     , testCase "RepeatLast test 35" $-         snd (last (unwindMacrosAcc (bindInput [ ("a", "'xy'v")-                                               , ("b", "'za'V") ] coinput)-                                    (stringToKeyMacro "'ab'Vv")))-         @?= "xyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyy"-     , testCase "RepeatLast test 36" $-         snd (last (unwindMacrosAcc (bindInput [ ("a", "'xy'v")-                                               , ("b", "za'za'") ] coinput)-                                    (stringToKeyMacro "'ab'V'ab'V")))-         @?= "xyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyy"-     , testCase "RepeatLast test 37" $-         snd (last (unwindMacrosAcc (bindInput [ ("b", "z'xy'vv")-                                               , ("c", "'xyvb'V") ] coinput)-                                    (stringToKeyMacro "'c'V")))-         @?= "xyyzxyyyxyyzxyyyxyyzxyyyxyyzxyyy"-     , testCase "RepeatLast test 38" $-         snd (last (unwindMacrosAcc coinput (stringToKeyMacro "'xv'V")))-         @?= "xxxx"-     , testCase "RepeatLast test 39" $-         fst <$> unwindMacros coinput (stringToKeyMacro "'xv'V")-         @?= [[(Right   "", "'xv'V",  "")],-              [(Left    "",  "xv'V",  "")],-              [(Left   "x",   "v'V", "x")],-              [(Left   "x",   "x'V", "x")],-              [(Left  "xx",    "'V", "x")],-              [(Right "xx",     "V", "x")],-              [(Right   "",    "xx",  ""), (Right "xx",    "", "V")],-              [(Right   "",     "x", "x"), (Right "xx",    "", "V")],-              [(Right "xx",      "", "V")]]-     , testCase "RepeatLast test 40" $-         snd (last (unwindMacrosAcc coinput (stringToKeyMacro "'xy'Vv")))-         @?= "xyxyxy"-     , testCase-         "RepeatLast test 41; named macros not referentially transparent" $-         snd (last (unwindMacrosAcc (bindInput [("a", "'xy'V")] coinput)-                                    (stringToKeyMacro "av")))-         @?= "xyxyxyxy"  -- because @a@ repeated; good!-     , testCase "RepeatLast test 42" $-         snd (last (unwindMacrosAcc (bindInput [("a", "xy")] coinput)-                                    (stringToKeyMacro "'a'Vv")))-         @?= "xyxyxy"  -- because @V@ repeated; good!-     , testCase "RepeatLast test 43" $-         snd (last (unwindMacrosAcc coinput (stringToKeyMacro "'xyV'V")))-         @?= "xyxy"-     , testCase "RepeatLast test 44" $-         snd (last (unwindMacrosAcc coinput (stringToKeyMacro "'xyV'v")))-         @?= "xyxy"-     , testCase "RepeatLast test 45" $-         snd (last (unwindMacrosAcc (bindInput [("a", "xyV")] coinput)-                                    (stringToKeyMacro "'a'V")))-         @?= "xyxy"-     , testCase "RepeatLast test 46" $-         snd (last (unwindMacrosAcc (bindInput [("a", "xyV")] coinput)-                                    (stringToKeyMacro "'a'v")))-         @?= "xyxy"-     ]+tests = testGroup "Tests" [ itemDescriptionUnitTests+                          , itemKindUnitTests+                          , itemRevUnitTests+                          , reqFailureUnitTests+                          , macroTests+                          , integrationTests ]  integrationTests :: TestTree integrationTests = testGroup "integrationTests" $@@ -302,23 +45,22 @@       serverOptions <- handleParseResult $ execParserPure defaultPrefs serverOptionsPI args       tieKnot serverOptions   ]+#ifndef USE_BROWSER   ++   let corule = RK.makeData Content.RuleKind.standardRules       uiOptions = unsafePerformIO $ mkUIOptions corule defClientOptions       testFontset :: (Int, String) -> TestTree       testFontset (n, fontsetName) =-        testCase ("SDL fronted; init only; " ++ fontsetName ++ " fontset") $-          when (frontendName == "sdl") $ do-            -- This test only works when run from the same directory that-            -- the .cabal files is in. And this is what Debian needs, so OK.-            -- The hacky log priority 0 tells SDL frontend to init-            -- and quit at once, for testing on CIs without graphics access.-            let seed = "SMGen " ++ show (13 + 2 * n) ++ " " ++ show (15 + 4 * n)-                args2 = words "--dbgMsgSer --logPriority 0 --newGame 3 --maxFps 100000 --benchmark --stopAfterFrames 5 --automateAll --keepAutomated --gameMode battle"-                        ++ [ "--setDungeonRng", seed, "--setMainRng", seed-                           , "--fontset", fontsetName ]-            serverOptions2 <- handleParseResult $ execParserPure defaultPrefs serverOptionsPI args2-            tieKnot serverOptions2-      isUbuntu = ("ubuntu" `T.isPrefixOf`)-  in map testFontset $ zip [0..] $ map T.unpack $ filter (not . isUbuntu)-     $ map fst $ uFontsets uiOptions+        testCase ("SDL fronted; init only; " ++ fontsetName ++ " fontset") $ do+          -- This test only works when run from the same directory that+          -- the .cabal file is in. And this is what Debian needs, so OK.+          -- The hacky log priority 0 tells SDL frontend to init+          -- and quit at once, for testing on CIs without graphics access.+          let seed = "SMGen " ++ show (13 + 2 * n) ++ " " ++ show (15 + 4 * n)+              args2 = words "--dbgMsgSer --logPriority 0 --newGame 3 --maxFps 100000 --benchmark --stopAfterFrames 5 --automateAll --keepAutomated --gameMode battle"+                      ++ [ "--setDungeonRng", seed, "--setMainRng", seed+                         , "--fontset", fontsetName ]+          serverOptions2 <- handleParseResult $ execParserPure defaultPrefs serverOptionsPI args2+          tieKnot serverOptions2+  in map testFontset $ zip [0..] $ map T.unpack $ map fst $ uFontsets uiOptions+#endif
+ test/doctest-driver.hs view
@@ -0,0 +1,10 @@+{-# OPTIONS_GHC -F -pgmF doctest-driver-gen -optF --verbose -optF definition-src -optF engine-src -optF -XMonoLocalBinds -optF -XScopedTypeVariables -optF -XOverloadedStrings -optF -XBangPatterns -optF -XRecordWildCards -optF -XNamedFieldPuns -optF -XMultiWayIf -optF -XLambdaCase -optF -XDefaultSignatures -optF -XInstanceSigs -optF -XPatternSynonyms -optF -XStrictData -optF -XCPP -optF -XTypeApplications #-}+  -- This needs to match @default-extensions@ of @common options@+  -- of LambdaHack.cabal. So far, there's no way to automate that.+  -- This is slow, but @--fast@ doesn't help at all, so not enabled.+  -- No slowdown from @--verbose@.+  --+  -- After @--test-show-details@ is fixed for doctests, we can recommend+  -- it in README and unify how normal tests and doctests are run, by setting+  -- @write-ghc-environment-files: always@ in @cabal.project@ and dropping+  -- @exec@.