LambdaHack 0.4.100.0 → 0.4.101.0
raw patch · 123 files changed
+7451/−4918 lines, 123 filesdep ~containersdep ~randomPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependency ranges changed: containers, random
API changes (from Hackage documentation)
- Game.LambdaHack.Atomic: SfxActivate :: !ActorId -> !ItemId -> !CStore -> SfxAtomic
- Game.LambdaHack.Atomic: UpdOldFidActor :: !ActorId -> !FactionId -> !FactionId -> UpdAtomic
- Game.LambdaHack.Atomic.CmdAtomic: SfxActivate :: !ActorId -> !ItemId -> !CStore -> SfxAtomic
- Game.LambdaHack.Atomic.CmdAtomic: UpdOldFidActor :: !ActorId -> !FactionId -> !FactionId -> UpdAtomic
- Game.LambdaHack.Client: exeFrontend :: (MonadAtomic m, MonadClientUI m, MonadClientReadResponse ResponseUI m, MonadClientWriteRequest RequestUI m, MonadAtomic n, MonadClientReadResponse ResponseAI n, MonadClientWriteRequest RequestAI n) => (m () -> SessionUI -> State -> StateClient -> chanServerUI -> IO ()) -> (n () -> SessionUI -> State -> StateClient -> chanServerAI -> IO ()) -> KeyKind -> COps -> DebugModeCli -> ((FactionId -> chanServerUI -> IO ()) -> (FactionId -> chanServerAI -> IO ()) -> IO ()) -> IO ()
- Game.LambdaHack.Client.CommonClient: getModeClient :: MonadClient m => m ModeKind
- Game.LambdaHack.Client.CommonClient: maxActorSkillsClient :: MonadClient m => ActorId -> m Skills
- Game.LambdaHack.Client.ItemSlot: newtype SlotChar
- Game.LambdaHack.Client.State: runDist :: RunParams -> !Int
- Game.LambdaHack.Client.State: runInitDir :: RunParams -> !(Maybe Vector)
- Game.LambdaHack.Client.State: sgameMode :: StateClient -> !(GroupName ModeKind)
- Game.LambdaHack.Client.UI.Frontend: FrontRunningFrame :: !SingleFrame -> FrontReq
- Game.LambdaHack.Client.UI.HandleHumanGlobalClient: stepToTargetHuman :: MonadClientUI m => m (SlideOrCmd RequestAnyAbility)
- Game.LambdaHack.Client.UI.HandleHumanLocalClient: allOwnedHuman :: MonadClientUI m => m Slideshow
- Game.LambdaHack.Client.UI.HandleHumanLocalClient: describeItemHuman :: MonadClientUI m => CStore -> m Slideshow
- Game.LambdaHack.Client.UI.HandleHumanLocalClient: tgtItemHuman :: MonadClientUI m => m Slideshow
- Game.LambdaHack.Client.UI.HandleHumanLocalClient: tgtStairHuman :: MonadClientUI m => Bool -> m Slideshow
- Game.LambdaHack.Client.UI.HandleHumanLocalClient: tgtUnknownHuman :: MonadClientUI m => m Slideshow
- Game.LambdaHack.Client.UI.HumanCmd: AllOwned :: HumanCmd
- Game.LambdaHack.Client.UI.HumanCmd: StepToTarget :: HumanCmd
- Game.LambdaHack.Client.UI.HumanCmd: TgtItem :: HumanCmd
- Game.LambdaHack.Client.UI.HumanCmd: TgtStair :: !Bool -> HumanCmd
- Game.LambdaHack.Client.UI.HumanCmd: TgtUnknown :: HumanCmd
- Game.LambdaHack.Client.UI.InventoryClient: failMsg :: MonadClientUI m => Msg -> m Slideshow
- Game.LambdaHack.Client.UI.InventoryClient: getAnyItem :: MonadClientUI m => Part -> [CStore] -> [CStore] -> Bool -> Bool -> m (SlideOrCmd ((ItemId, ItemFull), Container))
- Game.LambdaHack.Client.UI.MonadClientUI: displayFrames :: MonadClientUI m => Frames -> m ()
- Game.LambdaHack.Client.UI.MonadClientUI: stopRunning :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.RunClient: moveRunAid :: MonadClient m => ActorId -> Vector -> m (Either Msg RequestAnyAbility)
- Game.LambdaHack.Client.UI.WidgetClient: displayPushIfLid :: MonadClientUI m => LevelId -> m ()
- Game.LambdaHack.Common.Ability: maxSkills :: Skills -> Skills -> Skills
- Game.LambdaHack.Common.Actor: boldfid :: Actor -> !FactionId
- Game.LambdaHack.Common.Actor: mapActorItems_ :: Monad m => (ItemId -> ItemQuant -> m a) -> Actor -> m ()
- Game.LambdaHack.Common.ActorState: maxActorSkills :: ActorId -> [ItemFull] -> State -> Skills
- Game.LambdaHack.Common.ActorState: sharedAllOwned :: Bool -> Actor -> State -> ItemBag
- Game.LambdaHack.Common.Dice: diceScale :: Dice -> Int
- Game.LambdaHack.Common.Faction: Camping :: Outcome
- Game.LambdaHack.Common.Faction: Conquer :: Outcome
- Game.LambdaHack.Common.Faction: Defeated :: Outcome
- Game.LambdaHack.Common.Faction: Escape :: Outcome
- Game.LambdaHack.Common.Faction: Killed :: Outcome
- Game.LambdaHack.Common.Faction: Restart :: Outcome
- Game.LambdaHack.Common.Faction: canMoveFact :: Faction -> Bool -> Bool
- Game.LambdaHack.Common.Faction: data Outcome
- Game.LambdaHack.Common.Faction: instance Binary Outcome
- Game.LambdaHack.Common.Faction: instance Enum Outcome
- Game.LambdaHack.Common.Faction: instance Eq Outcome
- Game.LambdaHack.Common.Faction: instance Ord Outcome
- Game.LambdaHack.Common.Faction: instance Show Outcome
- Game.LambdaHack.Common.PointArray: mapWithKeyM_A :: Enum c => Monad m => (Point -> c -> m ()) -> Array c -> m ()
- Game.LambdaHack.Common.Request: ReqMoveItem :: !ItemId -> !Int -> !CStore -> !CStore -> RequestTimed AbMoveItem
- Game.LambdaHack.Common.Tile: isPassableKind :: TileKind -> Bool
- Game.LambdaHack.Content.ItemKind: aspectTrav :: Aspect a -> (a -> State s b) -> State s (Aspect b)
- Game.LambdaHack.Content.RuleKind: rsharedStash :: RuleKind -> !Bool
- Game.LambdaHack.Server: mainSer :: (MonadAtomic m, MonadServerReadRequest m) => [String] -> COps -> (m () -> IO ()) -> (COps -> DebugModeCli -> ((FactionId -> ChanServer ResponseUI RequestUI -> IO ()) -> (FactionId -> ChanServer ResponseAI RequestAI -> IO ()) -> IO ()) -> IO ()) -> IO ()
- Game.LambdaHack.Server.CommonServer: maxActorSkillsServer :: MonadServer m => ActorId -> m Skills
- Game.LambdaHack.Server.Fov: data PerceptionLit
- Game.LambdaHack.Server.Fov: instance Show PerceptionLit
- Game.LambdaHack.Server.Fov: type ActorEqpBody = [((ActorId, Actor), [ItemFull])]
- Game.LambdaHack.Server.Fov.Digital: debugLine :: Line -> (Bool, String)
- Game.LambdaHack.Server.Fov.Digital: debugSteeper :: Bump -> Bump -> Bump -> Bool
+ Game.LambdaHack.Atomic: SfxApply :: !ActorId -> !ItemId -> !CStore -> SfxAtomic
+ Game.LambdaHack.Atomic: UpdFidImpressedActor :: !ActorId -> !FactionId -> !FactionId -> UpdAtomic
+ Game.LambdaHack.Atomic.CmdAtomic: SfxApply :: !ActorId -> !ItemId -> !CStore -> SfxAtomic
+ Game.LambdaHack.Atomic.CmdAtomic: UpdFidImpressedActor :: !ActorId -> !FactionId -> !FactionId -> UpdAtomic
+ Game.LambdaHack.Client: loopAI :: (MonadAtomic m, MonadClientReadResponse ResponseAI m, MonadClientWriteRequest RequestAI m) => DebugModeCli -> m ()
+ Game.LambdaHack.Client: loopUI :: (MonadClientUI m, MonadAtomic m, MonadClientReadResponse ResponseUI m, MonadClientWriteRequest RequestUI m) => DebugModeCli -> m ()
+ Game.LambdaHack.Client: srtFrontend :: (DebugModeCli -> SessionUI -> State -> StateClient -> chanServerUI -> IO ()) -> (DebugModeCli -> SessionUI -> State -> StateClient -> chanServerAI -> IO ()) -> KeyKind -> COps -> DebugModeCli -> ((FactionId -> chanServerUI -> IO ()) -> (FactionId -> chanServerAI -> IO ()) -> IO ()) -> IO ()
+ Game.LambdaHack.Client.AI.ConditionClient: condEnoughGearM :: MonadClient m => ActorId -> m Bool
+ Game.LambdaHack.Client.AI.ConditionClient: condTgtEnemyAdjFriendM :: MonadClient m => ActorId -> m Bool
+ Game.LambdaHack.Client.AI.ConditionClient: condTgtNonmovingM :: MonadClient m => ActorId -> m Bool
+ Game.LambdaHack.Client.AI.ConditionClient: desirableItem :: Bool -> Maybe Int -> ItemFull -> Bool
+ Game.LambdaHack.Client.AI.ConditionClient: hinders :: Bool -> Bool -> Bool -> Bool -> Actor -> [ItemFull] -> ItemFull -> Bool
+ Game.LambdaHack.Client.AI.Strategy: instance Foldable Strategy
+ Game.LambdaHack.Client.AI.Strategy: instance Traversable Strategy
+ Game.LambdaHack.Client.BfsClient: invalidateBfs :: ActorId -> EnumMap ActorId (Bool, Array BfsDistance, Point, Int, Maybe [Point]) -> EnumMap ActorId (Bool, Array BfsDistance, Point, Int, Maybe [Point])
+ Game.LambdaHack.Client.ItemSlot: data SlotChar
+ Game.LambdaHack.Client.ItemSlot: slotPrefix :: SlotChar -> Int
+ Game.LambdaHack.Client.Key: Alt :: Modifier
+ Game.LambdaHack.Client.Key: LeftButtonPress :: Key
+ Game.LambdaHack.Client.Key: MiddleButtonPress :: Key
+ Game.LambdaHack.Client.Key: RightButtonPress :: Key
+ Game.LambdaHack.Client.Key: Shift :: Modifier
+ Game.LambdaHack.Client.Key: instance Constructor C1_20Key
+ Game.LambdaHack.Client.Key: instance Constructor C1_21Key
+ Game.LambdaHack.Client.Key: instance Constructor C1_22Key
+ Game.LambdaHack.Client.Key: instance Constructor C1_2Modifier
+ Game.LambdaHack.Client.Key: instance Constructor C1_3Modifier
+ Game.LambdaHack.Client.Key: instance NFData KM
+ Game.LambdaHack.Client.Key: instance NFData Key
+ Game.LambdaHack.Client.Key: instance NFData Modifier
+ Game.LambdaHack.Client.Key: instance Selector S1_0_2KM
+ Game.LambdaHack.Client.Key: leftButtonKM :: KM
+ Game.LambdaHack.Client.Key: pointer :: KM -> !Point
+ Game.LambdaHack.Client.Key: rightButtonKM :: KM
+ Game.LambdaHack.Client.Key: toKM :: Modifier -> Key -> KM
+ Game.LambdaHack.Client.State: runInitial :: RunParams -> !Bool
+ Game.LambdaHack.Client.State: runWaiting :: RunParams -> !Int
+ Game.LambdaHack.Client.State: slastKM :: StateClient -> !KM
+ Game.LambdaHack.Client.State: slastStore :: StateClient -> !CStore
+ Game.LambdaHack.Client.UI.Animation: actorX :: Point -> Char -> Color -> Animation
+ Game.LambdaHack.Client.UI.Animation: moveProj :: (Point, Point, Point) -> Char -> Color -> Animation
+ Game.LambdaHack.Client.UI.Config: instance Constructor C1_0Config
+ Game.LambdaHack.Client.UI.Config: instance Datatype D1Config
+ Game.LambdaHack.Client.UI.Config: instance Generic Config
+ Game.LambdaHack.Client.UI.Config: instance Selector S1_0_0Config
+ Game.LambdaHack.Client.UI.Config: instance Selector S1_0_1Config
+ Game.LambdaHack.Client.UI.Config: instance Selector S1_0_2Config
+ Game.LambdaHack.Client.UI.Config: instance Selector S1_0_3Config
+ Game.LambdaHack.Client.UI.Config: instance Selector S1_0_4Config
+ Game.LambdaHack.Client.UI.Config: instance Selector S1_0_5Config
+ Game.LambdaHack.Client.UI.Config: instance Selector S1_0_6Config
+ Game.LambdaHack.Client.UI.Config: instance Selector S1_0_7Config
+ Game.LambdaHack.Client.UI.Config: instance Selector S1_0_8Config
+ Game.LambdaHack.Client.UI.Config: instance Selector S1_0_9Config
+ Game.LambdaHack.Client.UI.Content.KeyKind: macroLeftButtonPress :: HumanCmd
+ Game.LambdaHack.Client.UI.Content.KeyKind: macroShiftLeftButtonPress :: HumanCmd
+ Game.LambdaHack.Client.UI.Frontend: frontFromTop :: FrontReq -> !(Maybe Bool)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalClient: continueToCursorHuman :: MonadClientUI m => m (SlideOrCmd RequestAnyAbility)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalClient: describeItemHuman :: MonadClientUI m => ItemDialogMode -> m (SlideOrCmd (RequestTimed AbMoveItem))
+ Game.LambdaHack.Client.UI.HandleHumanGlobalClient: moveOnceToCursorHuman :: MonadClientUI m => m (SlideOrCmd RequestAnyAbility)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalClient: runOnceAheadHuman :: MonadClientUI m => m (SlideOrCmd RequestAnyAbility)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalClient: runOnceToCursorHuman :: MonadClientUI m => m (SlideOrCmd RequestAnyAbility)
+ Game.LambdaHack.Client.UI.HandleHumanLocalClient: cursorItemHuman :: MonadClientUI m => m Slideshow
+ Game.LambdaHack.Client.UI.HandleHumanLocalClient: cursorPointerEnemyHuman :: MonadClientUI m => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalClient: cursorPointerFloorHuman :: MonadClientUI m => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalClient: cursorStairHuman :: MonadClientUI m => Bool -> m Slideshow
+ Game.LambdaHack.Client.UI.HandleHumanLocalClient: cursorUnknownHuman :: MonadClientUI m => m Slideshow
+ Game.LambdaHack.Client.UI.HandleHumanLocalClient: selectWithPointer :: MonadClientUI m => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalClient: stopIfTgtModeHuman :: MonadClientUI m => m ()
+ Game.LambdaHack.Client.UI.HandleHumanLocalClient: tgtPointerEnemyHuman :: MonadClientUI m => m Slideshow
+ Game.LambdaHack.Client.UI.HandleHumanLocalClient: tgtPointerFloorHuman :: MonadClientUI m => m Slideshow
+ Game.LambdaHack.Client.UI.HumanCmd: CmdInternal :: CmdCategory
+ Game.LambdaHack.Client.UI.HumanCmd: CmdMouse :: CmdCategory
+ Game.LambdaHack.Client.UI.HumanCmd: ContinueToCursor :: HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: CursorItem :: HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: CursorPointerEnemy :: HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: CursorPointerFloor :: HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: CursorStair :: !Bool -> HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: CursorUnknown :: HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: MoveOnceToCursor :: HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: RunOnceAhead :: HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: RunOnceToCursor :: HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: SelectWithPointer :: HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: StopIfTgtMode :: HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: TgtPointerEnemy :: HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: TgtPointerFloor :: HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_0CmdCategory
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_0HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_0Trigger
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_10HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_11HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_12HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_13HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_14HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_15HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_16HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_17HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_18HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_19HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_1CmdCategory
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_1HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_1Trigger
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_20HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_21HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_22HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_23HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_24HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_25HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_26HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_27HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_28HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_29HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_2CmdCategory
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_2HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_2Trigger
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_30HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_31HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_32HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_33HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_34HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_35HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_36HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_37HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_38HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_39HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_3CmdCategory
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_3HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_40HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_41HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_42HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_43HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_44HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_45HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_46HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_47HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_48HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_49HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_4CmdCategory
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_4HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_50HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_5CmdCategory
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_5HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_6CmdCategory
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_6HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_7CmdCategory
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_7HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_8CmdCategory
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_8HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_9CmdCategory
+ Game.LambdaHack.Client.UI.HumanCmd: instance Constructor C1_9HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Datatype D1CmdCategory
+ Game.LambdaHack.Client.UI.HumanCmd: instance Datatype D1HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Datatype D1Trigger
+ Game.LambdaHack.Client.UI.HumanCmd: instance Generic CmdCategory
+ Game.LambdaHack.Client.UI.HumanCmd: instance Generic HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance Generic Trigger
+ Game.LambdaHack.Client.UI.HumanCmd: instance NFData CmdCategory
+ Game.LambdaHack.Client.UI.HumanCmd: instance NFData HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: instance NFData Trigger
+ Game.LambdaHack.Client.UI.HumanCmd: instance Selector S1_0_0Trigger
+ Game.LambdaHack.Client.UI.HumanCmd: instance Selector S1_0_1Trigger
+ Game.LambdaHack.Client.UI.HumanCmd: instance Selector S1_0_2Trigger
+ Game.LambdaHack.Client.UI.HumanCmd: instance Selector S1_1_0Trigger
+ Game.LambdaHack.Client.UI.HumanCmd: instance Selector S1_1_1Trigger
+ Game.LambdaHack.Client.UI.HumanCmd: instance Selector S1_1_2Trigger
+ Game.LambdaHack.Client.UI.HumanCmd: instance Selector S1_2_0Trigger
+ Game.LambdaHack.Client.UI.HumanCmd: instance Selector S1_2_1Trigger
+ Game.LambdaHack.Client.UI.HumanCmd: instance Selector S1_2_2Trigger
+ Game.LambdaHack.Client.UI.InventoryClient: SuitsEverything :: Suitability
+ Game.LambdaHack.Client.UI.InventoryClient: SuitsNothing :: Msg -> Suitability
+ Game.LambdaHack.Client.UI.InventoryClient: SuitsSomething :: (ItemFull -> Bool) -> Suitability
+ Game.LambdaHack.Client.UI.InventoryClient: cursorPointerEnemy :: MonadClientUI m => Bool -> Bool -> m Slideshow
+ Game.LambdaHack.Client.UI.InventoryClient: cursorPointerFloor :: MonadClientUI m => Bool -> Bool -> m Slideshow
+ Game.LambdaHack.Client.UI.InventoryClient: data Suitability
+ Game.LambdaHack.Client.UI.InventoryClient: describeItemC :: MonadClientUI m => ItemDialogMode -> m (SlideOrCmd (RequestTimed AbMoveItem))
+ Game.LambdaHack.Client.UI.InventoryClient: doLook :: MonadClientUI m => Bool -> m Slideshow
+ Game.LambdaHack.Client.UI.InventoryClient: epsIncrHuman :: MonadClientUI m => Bool -> m Slideshow
+ Game.LambdaHack.Client.UI.InventoryClient: getAnyItems :: MonadClientUI m => Part -> [CStore] -> [CStore] -> Bool -> Bool -> m (SlideOrCmd ([(ItemId, ItemFull)], ItemDialogMode))
+ Game.LambdaHack.Client.UI.InventoryClient: moveCursorHuman :: MonadClientUI m => Vector -> Int -> m Slideshow
+ Game.LambdaHack.Client.UI.InventoryClient: tgtClearHuman :: MonadClientUI m => m Slideshow
+ Game.LambdaHack.Client.UI.InventoryClient: tgtEnemyHuman :: MonadClientUI m => m Slideshow
+ Game.LambdaHack.Client.UI.InventoryClient: tgtFloorHuman :: MonadClientUI m => m Slideshow
+ Game.LambdaHack.Client.UI.MsgClient: failMsg :: MonadClientUI m => Msg -> m Slideshow
+ Game.LambdaHack.Client.UI.WidgetClient: describeMainKeys :: MonadClientUI m => m Msg
+ Game.LambdaHack.Common.Actor: bfidImpressed :: Actor -> !FactionId
+ Game.LambdaHack.Common.Actor: bfidOriginal :: Actor -> !FactionId
+ Game.LambdaHack.Common.Actor: hpHuge :: Actor -> Bool
+ Game.LambdaHack.Common.Actor: keySelected :: (ActorId, Actor) -> (Bool, Bool, Char, Color, ActorId)
+ Game.LambdaHack.Common.Actor: ppCStoreIn :: CStore -> Text
+ Game.LambdaHack.Common.ActorState: findIid :: ActorId -> FactionId -> ItemId -> State -> [(Actor, CStore)]
+ Game.LambdaHack.Common.ActorState: getCarriedIidCStore :: Actor -> [(ItemId, CStore)]
+ Game.LambdaHack.Common.ActorState: goesIntoSha :: ItemFull -> Bool
+ Game.LambdaHack.Common.ActorState: hasCharge :: Time -> ItemFull -> Bool
+ Game.LambdaHack.Common.ActorState: isMelee :: ItemFull -> Bool
+ Game.LambdaHack.Common.ActorState: isMeleeEqp :: ItemFull -> Bool
+ Game.LambdaHack.Common.ActorState: mapActorItems_ :: Monad m => (CStore -> ItemId -> ItemQuant -> m a) -> Actor -> State -> m ()
+ Game.LambdaHack.Common.ActorState: strongestMelee :: Bool -> Time -> [(ItemId, ItemFull)] -> [(Int, (ItemId, ItemFull))]
+ Game.LambdaHack.Common.Dice: diceMult :: Dice -> Int
+ Game.LambdaHack.Common.Dice: instance NFData Dice
+ Game.LambdaHack.Common.Faction: instance Ord Faction
+ Game.LambdaHack.Common.Faction: instance Ord Target
+ Game.LambdaHack.Common.Frequency: instance NFData a => NFData (Frequency a)
+ Game.LambdaHack.Common.HighScore: getTable :: Id ModeKind -> ScoreDict -> ScoreTable
+ Game.LambdaHack.Common.HighScore: type ScoreDict = EnumMap (Id ModeKind) ScoreTable
+ Game.LambdaHack.Common.ItemDescription: partItemMediumAW :: CStore -> LevelId -> Time -> ItemFull -> Part
+ Game.LambdaHack.Common.ItemStrongest: strengthEffect :: (Effect -> [b]) -> ItemFull -> [b]
+ Game.LambdaHack.Common.Misc: MOwned :: ItemDialogMode
+ Game.LambdaHack.Common.Misc: MStats :: ItemDialogMode
+ Game.LambdaHack.Common.Misc: MStore :: CStore -> ItemDialogMode
+ Game.LambdaHack.Common.Misc: data ItemDialogMode
+ Game.LambdaHack.Common.Misc: instance Constructor C1_0GroupName
+ Game.LambdaHack.Common.Misc: instance Constructor C1_0ItemDialogMode
+ Game.LambdaHack.Common.Misc: instance Constructor C1_1ItemDialogMode
+ Game.LambdaHack.Common.Misc: instance Constructor C1_2ItemDialogMode
+ Game.LambdaHack.Common.Misc: instance Datatype D1GroupName
+ Game.LambdaHack.Common.Misc: instance Datatype D1ItemDialogMode
+ Game.LambdaHack.Common.Misc: instance Eq ItemDialogMode
+ Game.LambdaHack.Common.Misc: instance Generic (GroupName a)
+ Game.LambdaHack.Common.Misc: instance Generic ItemDialogMode
+ Game.LambdaHack.Common.Misc: instance NFData (GroupName a)
+ Game.LambdaHack.Common.Misc: instance NFData CStore
+ Game.LambdaHack.Common.Misc: instance NFData ItemDialogMode
+ Game.LambdaHack.Common.Misc: instance NFData Part
+ Game.LambdaHack.Common.Misc: instance NFData Person
+ Game.LambdaHack.Common.Misc: instance NFData Polarity
+ Game.LambdaHack.Common.Misc: instance Ord ItemDialogMode
+ Game.LambdaHack.Common.Misc: instance Read ItemDialogMode
+ Game.LambdaHack.Common.Misc: instance Show ItemDialogMode
+ Game.LambdaHack.Common.Misc: isRight :: Either a b -> Bool
+ Game.LambdaHack.Common.MonadStateRead: getGameMode :: MonadStateRead m => m ModeKind
+ Game.LambdaHack.Common.Msg: endMsg :: Msg
+ Game.LambdaHack.Common.Point: dummyPoint :: Point
+ Game.LambdaHack.Common.Point: instance Constructor C1_0Point
+ Game.LambdaHack.Common.Point: instance Datatype D1Point
+ Game.LambdaHack.Common.Point: instance Generic Point
+ Game.LambdaHack.Common.Point: instance NFData Point
+ Game.LambdaHack.Common.Point: instance Read Point
+ Game.LambdaHack.Common.Point: instance Selector S1_0_0Point
+ Game.LambdaHack.Common.Point: instance Selector S1_0_1Point
+ Game.LambdaHack.Common.PointArray: mapWithKeyMA :: Enum c => Monad m => (Point -> c -> m ()) -> Array c -> m ()
+ Game.LambdaHack.Common.PointArray: minIndexesA :: Enum c => Array c -> [Point]
+ Game.LambdaHack.Common.PointArray: safeSetA :: Enum c => c -> Array c -> Array c
+ Game.LambdaHack.Common.PointArray: unsafeSetA :: Enum c => c -> Array c -> Array c
+ Game.LambdaHack.Common.PointArray: unsafeUpdateA :: Enum c => Array c -> [(Point, c)] -> Array c
+ Game.LambdaHack.Common.Request: ApplyCharging :: ReqFailure
+ Game.LambdaHack.Common.Request: ProjectNotRanged :: ReqFailure
+ Game.LambdaHack.Common.Request: ReqMoveItems :: ![(ItemId, Int, CStore, CStore)] -> RequestTimed AbMoveItem
+ Game.LambdaHack.Common.State: sgameModeId :: State -> Id ModeKind
+ Game.LambdaHack.Common.Tile: isPassableNoSuspect :: Ops TileKind -> Id TileKind -> Bool
+ Game.LambdaHack.Common.Tile: isPassableNoSuspectTab :: TileSpeedup -> !Tab
+ Game.LambdaHack.Common.Vector: instance Constructor C1_0Vector
+ Game.LambdaHack.Common.Vector: instance Datatype D1Vector
+ Game.LambdaHack.Common.Vector: instance Generic Vector
+ Game.LambdaHack.Common.Vector: instance NFData Vector
+ Game.LambdaHack.Common.Vector: instance Selector S1_0_0Vector
+ Game.LambdaHack.Common.Vector: instance Selector S1_0_1Vector
+ Game.LambdaHack.Content.ItemKind: Unique :: Aspect a
+ Game.LambdaHack.Content.ItemKind: instance Constructor C1_13Aspect
+ Game.LambdaHack.Content.ItemKind: instance Foldable Aspect
+ Game.LambdaHack.Content.ItemKind: instance NFData Effect
+ Game.LambdaHack.Content.ItemKind: instance NFData ThrowMod
+ Game.LambdaHack.Content.ItemKind: instance NFData TimerDice
+ Game.LambdaHack.Content.ItemKind: instance Traversable Aspect
+ Game.LambdaHack.Content.ItemKind: slotName :: EqpSlot -> Text
+ Game.LambdaHack.Content.ModeKind: Camping :: Outcome
+ Game.LambdaHack.Content.ModeKind: Conquer :: Outcome
+ Game.LambdaHack.Content.ModeKind: Defeated :: Outcome
+ Game.LambdaHack.Content.ModeKind: Escape :: Outcome
+ Game.LambdaHack.Content.ModeKind: HiBlitz :: HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: HiConst :: HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: HiKill :: HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: HiLoot :: HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: HiLoss :: HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: HiSurvival :: HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: Killed :: Outcome
+ Game.LambdaHack.Content.ModeKind: Restart :: Outcome
+ Game.LambdaHack.Content.ModeKind: data HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: data Outcome
+ Game.LambdaHack.Content.ModeKind: fhiCondPoly :: Player a -> !HiCondPoly
+ Game.LambdaHack.Content.ModeKind: instance Binary HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: instance Binary Outcome
+ Game.LambdaHack.Content.ModeKind: instance Bounded Outcome
+ Game.LambdaHack.Content.ModeKind: instance Constructor C1_0HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: instance Constructor C1_0Outcome
+ Game.LambdaHack.Content.ModeKind: instance Constructor C1_1HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: instance Constructor C1_1Outcome
+ Game.LambdaHack.Content.ModeKind: instance Constructor C1_2HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: instance Constructor C1_2Outcome
+ Game.LambdaHack.Content.ModeKind: instance Constructor C1_3HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: instance Constructor C1_3Outcome
+ Game.LambdaHack.Content.ModeKind: instance Constructor C1_4HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: instance Constructor C1_4Outcome
+ Game.LambdaHack.Content.ModeKind: instance Constructor C1_5HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: instance Constructor C1_5Outcome
+ Game.LambdaHack.Content.ModeKind: instance Datatype D1HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: instance Datatype D1Outcome
+ Game.LambdaHack.Content.ModeKind: instance Enum Outcome
+ Game.LambdaHack.Content.ModeKind: instance Eq HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: instance Eq Outcome
+ Game.LambdaHack.Content.ModeKind: instance Generic HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: instance Generic Outcome
+ Game.LambdaHack.Content.ModeKind: instance Ord AutoLeader
+ Game.LambdaHack.Content.ModeKind: instance Ord HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: instance Ord LeaderMode
+ Game.LambdaHack.Content.ModeKind: instance Ord Outcome
+ Game.LambdaHack.Content.ModeKind: instance Ord a => Ord (Player a)
+ Game.LambdaHack.Content.ModeKind: instance Selector S1_0_12Player
+ Game.LambdaHack.Content.ModeKind: instance Show HiIndeterminant
+ Game.LambdaHack.Content.ModeKind: instance Show Outcome
+ Game.LambdaHack.Content.ModeKind: type HiCondPoly = [HiSummand]
+ Game.LambdaHack.Content.ModeKind: type HiPolynomial = [(HiIndeterminant, Double)]
+ Game.LambdaHack.Content.ModeKind: type HiSummand = (HiPolynomial, [Outcome])
+ Game.LambdaHack.Content.TileKind: instance NFData Feature
+ Game.LambdaHack.Server: debugArgs :: [String] -> IO DebugModeSer
+ Game.LambdaHack.Server: loopSer :: (MonadAtomic m, MonadServerReadRequest m) => COps -> DebugModeSer -> (FactionId -> ChanServer ResponseUI RequestUI -> IO ()) -> (FactionId -> ChanServer ResponseAI RequestAI -> IO ()) -> m ()
+ Game.LambdaHack.Server: sdebugCli :: DebugModeSer -> DebugModeCli
+ Game.LambdaHack.Server: speedupCOps :: Bool -> COps -> COps
+ Game.LambdaHack.Server.Fov: PerceptionDynamicLit :: [Point] -> PerceptionDynamicLit
+ Game.LambdaHack.Server.Fov: PerceptionReachable :: [Point] -> PerceptionReachable
+ Game.LambdaHack.Server.Fov: instance Show PerceptionDynamicLit
+ Game.LambdaHack.Server.Fov: newtype PerceptionDynamicLit
+ Game.LambdaHack.Server.Fov: newtype PerceptionReachable
+ Game.LambdaHack.Server.Fov: pdynamicLit :: PerceptionDynamicLit -> [Point]
+ Game.LambdaHack.Server.Fov: preachable :: PerceptionReachable -> [Point]
+ Game.LambdaHack.Server.Fov.Digital: _debugLine :: Line -> (Bool, String)
+ Game.LambdaHack.Server.Fov.Digital: _debugSteeper :: Bump -> Bump -> Bump -> Bool
+ Game.LambdaHack.Server.ItemRev: type UniqueSet = EnumSet (Id ItemKind)
+ Game.LambdaHack.Server.PeriodicServer: managePerTurn :: (MonadAtomic m, MonadServer m) => ActorId -> m ()
+ Game.LambdaHack.Server.State: FovCache3 :: !Int -> !Int -> !Int -> FovCache3
+ Game.LambdaHack.Server.State: data FovCache3
+ Game.LambdaHack.Server.State: emptyFovCache3 :: FovCache3
+ Game.LambdaHack.Server.State: fovLight :: FovCache3 -> !Int
+ Game.LambdaHack.Server.State: fovSight :: FovCache3 -> !Int
+ Game.LambdaHack.Server.State: fovSmell :: FovCache3 -> !Int
+ Game.LambdaHack.Server.State: instance Binary FovCache3
+ Game.LambdaHack.Server.State: instance Eq FovCache3
+ Game.LambdaHack.Server.State: instance Show FovCache3
+ Game.LambdaHack.Server.State: sItemFovCache :: StateServer -> !(EnumMap ItemId FovCache3)
+ Game.LambdaHack.Server.State: skeepAutomated :: DebugModeSer -> !Bool
+ Game.LambdaHack.Server.State: snumSpawned :: StateServer -> !(EnumMap LevelId Int)
+ Game.LambdaHack.Server.State: suniqueSet :: StateServer -> !UniqueSet
- Game.LambdaHack.Atomic: SfxEffect :: !FactionId -> !ActorId -> !(Effect) -> SfxAtomic
+ Game.LambdaHack.Atomic: SfxEffect :: !FactionId -> !ActorId -> !Effect -> SfxAtomic
- Game.LambdaHack.Atomic: SfxRecoil :: !ActorId -> !ActorId -> !ItemId -> !HitAtomic -> SfxAtomic
+ Game.LambdaHack.Atomic: SfxRecoil :: !ActorId -> !ActorId -> !ItemId -> !CStore -> !HitAtomic -> SfxAtomic
- Game.LambdaHack.Atomic: SfxStrike :: !ActorId -> !ActorId -> !ItemId -> !HitAtomic -> SfxAtomic
+ Game.LambdaHack.Atomic: SfxStrike :: !ActorId -> !ActorId -> !ItemId -> !CStore -> !HitAtomic -> SfxAtomic
- Game.LambdaHack.Atomic: UpdCover :: !LevelId -> !Point -> !ItemId -> !(Id ItemKind) -> !ItemSeed -> UpdAtomic
+ Game.LambdaHack.Atomic: UpdCover :: !Container -> !ItemId -> !(Id ItemKind) -> !ItemSeed -> UpdAtomic
- Game.LambdaHack.Atomic: UpdCoverKind :: !LevelId -> !Point -> !ItemId -> !(Id ItemKind) -> UpdAtomic
+ Game.LambdaHack.Atomic: UpdCoverKind :: !Container -> !ItemId -> !(Id ItemKind) -> UpdAtomic
- Game.LambdaHack.Atomic: UpdCoverSeed :: !LevelId -> !Point -> !ItemId -> !ItemSeed -> UpdAtomic
+ Game.LambdaHack.Atomic: UpdCoverSeed :: !Container -> !ItemId -> !ItemSeed -> UpdAtomic
- Game.LambdaHack.Atomic: UpdDiscover :: !LevelId -> !Point -> !ItemId -> !(Id ItemKind) -> !ItemSeed -> UpdAtomic
+ Game.LambdaHack.Atomic: UpdDiscover :: !Container -> !ItemId -> !(Id ItemKind) -> !ItemSeed -> UpdAtomic
- Game.LambdaHack.Atomic: UpdDiscoverKind :: !LevelId -> !Point -> !ItemId -> !(Id ItemKind) -> UpdAtomic
+ Game.LambdaHack.Atomic: UpdDiscoverKind :: !Container -> !ItemId -> !(Id ItemKind) -> UpdAtomic
- Game.LambdaHack.Atomic: UpdDiscoverSeed :: !LevelId -> !Point -> !ItemId -> !ItemSeed -> UpdAtomic
+ Game.LambdaHack.Atomic: UpdDiscoverSeed :: !Container -> !ItemId -> !ItemSeed -> UpdAtomic
- Game.LambdaHack.Atomic: UpdRestart :: !FactionId -> !DiscoveryKind -> !FactionPers -> !State -> !DebugModeCli -> !(GroupName ModeKind) -> UpdAtomic
+ Game.LambdaHack.Atomic: UpdRestart :: !FactionId -> !DiscoveryKind -> !FactionPers -> !State -> !DebugModeCli -> UpdAtomic
- Game.LambdaHack.Atomic.CmdAtomic: SfxEffect :: !FactionId -> !ActorId -> !(Effect) -> SfxAtomic
+ Game.LambdaHack.Atomic.CmdAtomic: SfxEffect :: !FactionId -> !ActorId -> !Effect -> SfxAtomic
- Game.LambdaHack.Atomic.CmdAtomic: SfxRecoil :: !ActorId -> !ActorId -> !ItemId -> !HitAtomic -> SfxAtomic
+ Game.LambdaHack.Atomic.CmdAtomic: SfxRecoil :: !ActorId -> !ActorId -> !ItemId -> !CStore -> !HitAtomic -> SfxAtomic
- Game.LambdaHack.Atomic.CmdAtomic: SfxStrike :: !ActorId -> !ActorId -> !ItemId -> !HitAtomic -> SfxAtomic
+ Game.LambdaHack.Atomic.CmdAtomic: SfxStrike :: !ActorId -> !ActorId -> !ItemId -> !CStore -> !HitAtomic -> SfxAtomic
- Game.LambdaHack.Atomic.CmdAtomic: UpdCover :: !LevelId -> !Point -> !ItemId -> !(Id ItemKind) -> !ItemSeed -> UpdAtomic
+ Game.LambdaHack.Atomic.CmdAtomic: UpdCover :: !Container -> !ItemId -> !(Id ItemKind) -> !ItemSeed -> UpdAtomic
- Game.LambdaHack.Atomic.CmdAtomic: UpdCoverKind :: !LevelId -> !Point -> !ItemId -> !(Id ItemKind) -> UpdAtomic
+ Game.LambdaHack.Atomic.CmdAtomic: UpdCoverKind :: !Container -> !ItemId -> !(Id ItemKind) -> UpdAtomic
- Game.LambdaHack.Atomic.CmdAtomic: UpdCoverSeed :: !LevelId -> !Point -> !ItemId -> !ItemSeed -> UpdAtomic
+ Game.LambdaHack.Atomic.CmdAtomic: UpdCoverSeed :: !Container -> !ItemId -> !ItemSeed -> UpdAtomic
- Game.LambdaHack.Atomic.CmdAtomic: UpdDiscover :: !LevelId -> !Point -> !ItemId -> !(Id ItemKind) -> !ItemSeed -> UpdAtomic
+ Game.LambdaHack.Atomic.CmdAtomic: UpdDiscover :: !Container -> !ItemId -> !(Id ItemKind) -> !ItemSeed -> UpdAtomic
- Game.LambdaHack.Atomic.CmdAtomic: UpdDiscoverKind :: !LevelId -> !Point -> !ItemId -> !(Id ItemKind) -> UpdAtomic
+ Game.LambdaHack.Atomic.CmdAtomic: UpdDiscoverKind :: !Container -> !ItemId -> !(Id ItemKind) -> UpdAtomic
- Game.LambdaHack.Atomic.CmdAtomic: UpdDiscoverSeed :: !LevelId -> !Point -> !ItemId -> !ItemSeed -> UpdAtomic
+ Game.LambdaHack.Atomic.CmdAtomic: UpdDiscoverSeed :: !Container -> !ItemId -> !ItemSeed -> UpdAtomic
- Game.LambdaHack.Atomic.CmdAtomic: UpdRestart :: !FactionId -> !DiscoveryKind -> !FactionPers -> !State -> !DebugModeCli -> !(GroupName ModeKind) -> UpdAtomic
+ Game.LambdaHack.Atomic.CmdAtomic: UpdRestart :: !FactionId -> !DiscoveryKind -> !FactionPers -> !State -> !DebugModeCli -> UpdAtomic
- Game.LambdaHack.Atomic.MonadStateWrite: deleteItemActor :: MonadStateWrite m => ItemId -> Int -> ActorId -> CStore -> m ()
+ Game.LambdaHack.Atomic.MonadStateWrite: deleteItemActor :: MonadStateWrite m => ItemId -> ItemQuant -> ActorId -> CStore -> m ()
- Game.LambdaHack.Atomic.MonadStateWrite: deleteItemContainer :: MonadStateWrite m => ItemId -> Int -> Container -> m ()
+ Game.LambdaHack.Atomic.MonadStateWrite: deleteItemContainer :: MonadStateWrite m => ItemId -> ItemQuant -> Container -> m ()
- Game.LambdaHack.Client.AI: refreshTarget :: MonadClient m => ActorId -> (ActorId, Actor) -> m (Maybe ((ActorId, Actor), (Target, PathEtc)))
+ Game.LambdaHack.Client.AI: refreshTarget :: MonadClient m => (ActorId, Actor) -> m (Maybe (Target, PathEtc))
- Game.LambdaHack.Client.AI.ConditionClient: condCanProjectM :: MonadClient m => ActorId -> m Bool
+ Game.LambdaHack.Client.AI.ConditionClient: condCanProjectM :: MonadClient m => Bool -> ActorId -> m Bool
- Game.LambdaHack.Client.AI.ConditionClient: fleeList :: MonadClient m => Bool -> ActorId -> m [(Int, Point)]
+ Game.LambdaHack.Client.AI.ConditionClient: fleeList :: MonadClient m => ActorId -> m ([(Int, Point)], [(Int, Point)])
- Game.LambdaHack.Client.AI.PickActorClient: pickActorToMove :: MonadClient m => (ActorId -> (ActorId, Actor) -> m (Maybe ((ActorId, Actor), (Target, PathEtc)))) -> ActorId -> m (ActorId, Actor)
+ Game.LambdaHack.Client.AI.PickActorClient: pickActorToMove :: MonadClient m => ((ActorId, Actor) -> m (Maybe (Target, PathEtc))) -> ActorId -> m (ActorId, Actor)
- Game.LambdaHack.Client.AI.PickTargetClient: targetStrategy :: MonadClient m => ActorId -> ActorId -> m (Strategy (Target, Maybe PathEtc))
+ Game.LambdaHack.Client.AI.PickTargetClient: targetStrategy :: MonadClient m => ActorId -> m (Strategy (Target, Maybe PathEtc))
- Game.LambdaHack.Client.BfsClient: closestItems :: MonadClient m => ActorId -> m ([(Int, (Point, Maybe ItemBag))])
+ Game.LambdaHack.Client.BfsClient: closestItems :: MonadClient m => ActorId -> m [(Int, (Point, Maybe ItemBag))]
- Game.LambdaHack.Client.BfsClient: closestTriggers :: MonadClient m => Maybe Bool -> Bool -> ActorId -> m [Point]
+ Game.LambdaHack.Client.BfsClient: closestTriggers :: MonadClient m => Maybe Bool -> ActorId -> m (Frequency Point)
- Game.LambdaHack.Client.CommonClient: aidTgtAims :: MonadClient m => ActorId -> LevelId -> Maybe Target -> m (Either Text Int)
+ Game.LambdaHack.Client.CommonClient: aidTgtAims :: MonadClient m => ActorId -> LevelId -> Maybe Target -> m (Either Msg Int)
- Game.LambdaHack.Client.CommonClient: makeLine :: MonadClient m => Actor -> Point -> Int -> m (Maybe Int)
+ Game.LambdaHack.Client.CommonClient: makeLine :: MonadClient m => Bool -> Actor -> Point -> Int -> m (Maybe Int)
- Game.LambdaHack.Client.CommonClient: pickWeaponClient :: MonadClient m => ActorId -> ActorId -> m [RequestTimed AbMelee]
+ Game.LambdaHack.Client.CommonClient: pickWeaponClient :: MonadClient m => ActorId -> ActorId -> m (Maybe (RequestTimed AbMelee))
- Game.LambdaHack.Client.CommonClient: updateItemSlot :: MonadClient m => Maybe ActorId -> ItemId -> m ()
+ Game.LambdaHack.Client.CommonClient: updateItemSlot :: MonadClient m => CStore -> Maybe ActorId -> ItemId -> m SlotChar
- Game.LambdaHack.Client.ItemSlot: SlotChar :: Char -> SlotChar
+ Game.LambdaHack.Client.ItemSlot: SlotChar :: Int -> Char -> SlotChar
- Game.LambdaHack.Client.ItemSlot: allSlots :: [SlotChar]
+ Game.LambdaHack.Client.ItemSlot: allSlots :: Int -> [SlotChar]
- Game.LambdaHack.Client.ItemSlot: assignSlot :: Item -> FactionId -> Maybe Actor -> ItemSlots -> SlotChar -> State -> Either SlotChar Int
+ Game.LambdaHack.Client.ItemSlot: assignSlot :: CStore -> Item -> FactionId -> Maybe Actor -> ItemSlots -> SlotChar -> State -> SlotChar
- Game.LambdaHack.Client.ItemSlot: slotLabel :: Either SlotChar Int -> Part
+ Game.LambdaHack.Client.ItemSlot: slotLabel :: SlotChar -> Part
- Game.LambdaHack.Client.ItemSlot: type ItemSlots = (EnumMap SlotChar ItemId, IntMap ItemId)
+ Game.LambdaHack.Client.ItemSlot: type ItemSlots = (EnumMap SlotChar ItemId, EnumMap SlotChar ItemId)
- Game.LambdaHack.Client.Key: KM :: !Modifier -> !Key -> KM
+ Game.LambdaHack.Client.Key: KM :: !Key -> !Modifier -> !Point -> KM
- Game.LambdaHack.Client.State: RunParams :: !ActorId -> ![ActorId] -> !Int -> !(Maybe Text) -> !(Maybe Vector) -> RunParams
+ Game.LambdaHack.Client.State: RunParams :: !ActorId -> ![ActorId] -> !Bool -> !(Maybe Text) -> !Int -> RunParams
- Game.LambdaHack.Client.State: StateClient :: !(Maybe TgtMode) -> !Target -> !Int -> !(EnumMap ActorId (Target, Maybe PathEtc)) -> !(EnumSet LevelId) -> !(EnumMap ActorId (Array BfsDistance, Point, Int, Maybe [Point])) -> !(EnumSet ActorId) -> !(Maybe RunParams) -> !Report -> !History -> !(EnumMap LevelId Time) -> ![CmdAtomic] -> !DiscoveryKind -> !DiscoveryEffect -> !FactionPers -> !StdGen -> !LastRecord -> ![KM] -> !(EnumSet ActorId) -> !Int -> !(Maybe ActorId) -> !FactionId -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Int -> !ItemSlots -> !SlotChar -> !(GroupName ModeKind) -> !EscAI -> !DebugModeCli -> StateClient
+ Game.LambdaHack.Client.State: StateClient :: !(Maybe TgtMode) -> !Target -> !Int -> !(EnumMap ActorId (Target, Maybe PathEtc)) -> !(EnumSet LevelId) -> !(EnumMap ActorId (Bool, Array BfsDistance, Point, Int, Maybe [Point])) -> !(EnumSet ActorId) -> !(Maybe RunParams) -> !Report -> !History -> !(EnumMap LevelId Time) -> ![CmdAtomic] -> !DiscoveryKind -> !DiscoveryEffect -> !FactionPers -> !StdGen -> !KM -> !LastRecord -> ![KM] -> !(EnumSet ActorId) -> !Int -> !(Maybe ActorId) -> !FactionId -> !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !Int -> !ItemSlots -> !SlotChar -> !CStore -> !EscAI -> !DebugModeCli -> StateClient
- Game.LambdaHack.Client.State: sbfsD :: StateClient -> !(EnumMap ActorId (Array BfsDistance, Point, Int, Maybe [Point]))
+ Game.LambdaHack.Client.State: sbfsD :: StateClient -> !(EnumMap ActorId (Bool, Array BfsDistance, Point, Int, Maybe [Point]))
- Game.LambdaHack.Client.UI: humanCommand :: MonadClientUI m => Maybe Msg -> m RequestUI
+ Game.LambdaHack.Client.UI: humanCommand :: MonadClientUI m => m RequestUI
- Game.LambdaHack.Client.UI.DrawClient: draw :: MonadClient m => Bool -> ColorMode -> LevelId -> Maybe Point -> Maybe Point -> Maybe (Array BfsDistance, Maybe [Point]) -> (Text, Maybe Text) -> (Text, Maybe Text) -> Overlay -> m SingleFrame
+ Game.LambdaHack.Client.UI.DrawClient: draw :: MonadClient m => ColorMode -> LevelId -> Maybe Point -> Maybe Point -> Maybe (Array BfsDistance, Maybe [Point]) -> (Text, Maybe Text) -> (Text, Maybe Text) -> Overlay -> m SingleFrame
- Game.LambdaHack.Client.UI.Frontend: FrontSlides :: ![KM] -> ![SingleFrame] -> FrontReq
+ Game.LambdaHack.Client.UI.Frontend: FrontSlides :: ![KM] -> ![SingleFrame] -> !(Maybe Bool) -> FrontReq
- Game.LambdaHack.Client.UI.Frontend.Chosen: RawFrontend :: (Bool -> Maybe SingleFrame -> IO ()) -> (SingleFrame -> IO KM) -> IO () -> !(Maybe (MVar ())) -> !DebugModeCli -> RawFrontend
+ Game.LambdaHack.Client.UI.Frontend.Chosen: RawFrontend :: (Maybe SingleFrame -> IO ()) -> (SingleFrame -> IO KM) -> IO () -> !(Maybe (MVar ())) -> !DebugModeCli -> RawFrontend
- Game.LambdaHack.Client.UI.Frontend.Chosen: fdisplay :: RawFrontend -> Bool -> Maybe SingleFrame -> IO ()
+ Game.LambdaHack.Client.UI.Frontend.Chosen: fdisplay :: RawFrontend -> Maybe SingleFrame -> IO ()
- Game.LambdaHack.Client.UI.Frontend.Std: fdisplay :: FrontendSession -> Bool -> Maybe SingleFrame -> IO ()
+ Game.LambdaHack.Client.UI.Frontend.Std: fdisplay :: FrontendSession -> Maybe SingleFrame -> IO ()
- Game.LambdaHack.Client.UI.HandleHumanGlobalClient: moveItemHuman :: MonadClientUI m => [CStore] -> CStore -> (Maybe Part) -> Bool -> m (SlideOrCmd (RequestTimed AbMoveItem))
+ Game.LambdaHack.Client.UI.HandleHumanGlobalClient: moveItemHuman :: MonadClientUI m => [CStore] -> CStore -> Maybe Part -> Bool -> m (SlideOrCmd (RequestTimed AbMoveItem))
- Game.LambdaHack.Client.UI.HandleHumanGlobalClient: moveRunHuman :: MonadClientUI m => Bool -> Vector -> m (SlideOrCmd RequestAnyAbility)
+ Game.LambdaHack.Client.UI.HandleHumanGlobalClient: moveRunHuman :: MonadClientUI m => Bool -> Bool -> Bool -> Bool -> Vector -> m (SlideOrCmd RequestAnyAbility)
- Game.LambdaHack.Client.UI.HandleHumanLocalClient: selectActorHuman :: MonadClientUI m => m Slideshow
+ Game.LambdaHack.Client.UI.HandleHumanLocalClient: selectActorHuman :: MonadClientUI m => m ()
- Game.LambdaHack.Client.UI.HumanCmd: DescribeItem :: !CStore -> HumanCmd
+ Game.LambdaHack.Client.UI.HumanCmd: DescribeItem :: !ItemDialogMode -> HumanCmd
- Game.LambdaHack.Client.UI.InventoryClient: getGroupItem :: MonadClientUI m => (ItemFull -> Bool) -> Text -> Text -> [CStore] -> [CStore] -> m (SlideOrCmd ((ItemId, ItemFull), Container))
+ Game.LambdaHack.Client.UI.InventoryClient: getGroupItem :: MonadClientUI m => m Suitability -> Text -> Text -> Bool -> [CStore] -> [CStore] -> m (SlideOrCmd ((ItemId, ItemFull), ItemDialogMode))
- Game.LambdaHack.Client.UI.InventoryClient: getStoreItem :: MonadClientUI m => (Actor -> [ItemFull] -> Container -> Text) -> Container -> Bool -> m (SlideOrCmd ((ItemId, ItemFull), Container))
+ Game.LambdaHack.Client.UI.InventoryClient: getStoreItem :: MonadClientUI m => (Actor -> [ItemFull] -> ItemDialogMode -> Text) -> ItemDialogMode -> m (SlideOrCmd ((ItemId, ItemFull), ItemDialogMode))
- Game.LambdaHack.Client.UI.MonadClientUI: displayFrame :: MonadClientUI m => Bool -> Maybe SingleFrame -> m ()
+ Game.LambdaHack.Client.UI.MonadClientUI: displayFrame :: MonadClientUI m => Maybe SingleFrame -> m ()
- Game.LambdaHack.Client.UI.MonadClientUI: getKeyOverlayCommand :: MonadClientUI m => Bool -> Overlay -> m KM
+ Game.LambdaHack.Client.UI.MonadClientUI: getKeyOverlayCommand :: MonadClientUI m => Maybe Bool -> Overlay -> m KM
- Game.LambdaHack.Client.UI.MsgClient: itemOverlay :: MonadClient m => Container -> LevelId -> ItemBag -> m Overlay
+ Game.LambdaHack.Client.UI.MsgClient: itemOverlay :: MonadClient m => CStore -> LevelId -> ItemBag -> m Overlay
- Game.LambdaHack.Client.UI.RunClient: continueRun :: MonadClient m => RunParams -> m (Either Msg (RunParams, RequestAnyAbility))
+ Game.LambdaHack.Client.UI.RunClient: continueRun :: MonadClient m => LevelId -> RunParams -> m (Either Msg RequestAnyAbility)
- Game.LambdaHack.Client.UI.WidgetClient: displayPush :: MonadClientUI m => m ()
+ Game.LambdaHack.Client.UI.WidgetClient: displayPush :: MonadClientUI m => Msg -> m ()
- Game.LambdaHack.Client.UI.WidgetClient: overlayToBlankSlideshow :: MonadClientUI m => Msg -> Overlay -> m Slideshow
+ Game.LambdaHack.Client.UI.WidgetClient: overlayToBlankSlideshow :: MonadClientUI m => Bool -> Msg -> Overlay -> m Slideshow
- Game.LambdaHack.Common.Actor: Actor :: !ItemId -> !Char -> !Text -> !Text -> !Color -> !Time -> !Int64 -> !ResDelta -> !Int64 -> !ResDelta -> !Point -> !Point -> !LevelId -> !LevelId -> !FactionId -> !FactionId -> !(Maybe ([Vector], Speed)) -> !ItemBag -> !ItemBag -> !ItemBag -> !Bool -> !Bool -> Actor
+ Game.LambdaHack.Common.Actor: Actor :: !ItemId -> !Char -> !Text -> !Text -> !Color -> !Time -> !Int64 -> !ResDelta -> !Int64 -> !ResDelta -> !Point -> !Point -> !LevelId -> !LevelId -> !FactionId -> !FactionId -> !FactionId -> !(Maybe ([Vector], Speed)) -> !ItemBag -> !ItemBag -> !ItemBag -> !Bool -> !Bool -> Actor
- Game.LambdaHack.Common.Actor: ppCStore :: CStore -> Text
+ Game.LambdaHack.Common.Actor: ppCStore :: CStore -> (Text, Text)
- Game.LambdaHack.Common.ActorState: goesIntoInv :: Item -> Bool
+ Game.LambdaHack.Common.ActorState: goesIntoInv :: ItemFull -> Bool
- Game.LambdaHack.Common.ActorState: tryFindHeroK :: State -> FactionId -> Int -> Maybe (ActorId, Actor)
+ Game.LambdaHack.Common.ActorState: tryFindHeroK :: FactionId -> Int -> State -> Maybe (ActorId, Actor)
- Game.LambdaHack.Common.Dice: meanDice :: Dice -> Rational
+ Game.LambdaHack.Common.Dice: meanDice :: Dice -> Int
- Game.LambdaHack.Common.Dice: meanDiceXY :: DiceXY -> (Rational, Rational)
+ Game.LambdaHack.Common.Dice: meanDiceXY :: DiceXY -> (Int, Int)
- Game.LambdaHack.Common.Frequency: maxFreq :: (Show a, Ord a) => Frequency a -> a
+ Game.LambdaHack.Common.Frequency: maxFreq :: Ord a => Frequency a -> Maybe a
- Game.LambdaHack.Common.Frequency: meanFreq :: (Show a, Integral a) => Frequency a -> Rational
+ Game.LambdaHack.Common.Frequency: meanFreq :: Frequency Int -> Int
- Game.LambdaHack.Common.Frequency: minFreq :: (Show a, Ord a) => Frequency a -> a
+ Game.LambdaHack.Common.Frequency: minFreq :: Ord a => Frequency a -> Maybe a
- Game.LambdaHack.Common.HighScore: empty :: ScoreTable
+ Game.LambdaHack.Common.HighScore: empty :: ScoreDict
- Game.LambdaHack.Common.HighScore: highSlideshow :: ScoreTable -> Int -> Slideshow
+ Game.LambdaHack.Common.HighScore: highSlideshow :: ScoreTable -> Int -> Text -> Slideshow
- Game.LambdaHack.Common.HighScore: register :: ScoreTable -> Int -> Time -> Status -> ClockTime -> Int -> Text -> EnumMap (Id ItemKind) Int -> EnumMap (Id ItemKind) Int -> Bool -> (Bool, (ScoreTable, Int))
+ Game.LambdaHack.Common.HighScore: register :: ScoreTable -> Int -> Time -> Status -> ClockTime -> Int -> Text -> EnumMap (Id ItemKind) Int -> EnumMap (Id ItemKind) Int -> HiCondPoly -> (Bool, (ScoreTable, Int))
- Game.LambdaHack.Common.ItemDescription: itemDesc :: Container -> LevelId -> Time -> ItemFull -> Overlay
+ Game.LambdaHack.Common.ItemDescription: itemDesc :: CStore -> LevelId -> Time -> ItemFull -> Overlay
- Game.LambdaHack.Common.ItemDescription: partItem :: Container -> LevelId -> Time -> ItemFull -> (Part, Part)
+ Game.LambdaHack.Common.ItemDescription: partItem :: CStore -> LevelId -> Time -> ItemFull -> (Bool, Part, Part)
- Game.LambdaHack.Common.ItemDescription: partItemAW :: Container -> LevelId -> Time -> ItemFull -> Part
+ Game.LambdaHack.Common.ItemDescription: partItemAW :: CStore -> LevelId -> Time -> ItemFull -> Part
- Game.LambdaHack.Common.ItemDescription: partItemN :: Bool -> Int -> Container -> LevelId -> Time -> ItemFull -> (Part, Part)
+ Game.LambdaHack.Common.ItemDescription: partItemN :: Int -> Int -> CStore -> LevelId -> Time -> ItemFull -> (Bool, Part, Part)
- Game.LambdaHack.Common.ItemDescription: partItemWownW :: Part -> Container -> LevelId -> Time -> ItemFull -> Part
+ Game.LambdaHack.Common.ItemDescription: partItemWownW :: Part -> CStore -> LevelId -> Time -> ItemFull -> Part
- Game.LambdaHack.Common.ItemDescription: partItemWs :: Int -> Container -> LevelId -> Time -> ItemFull -> Part
+ Game.LambdaHack.Common.ItemDescription: partItemWs :: Int -> CStore -> LevelId -> Time -> ItemFull -> Part
- Game.LambdaHack.Common.ItemDescription: textAllAE :: Bool -> Container -> ItemFull -> [Text]
+ Game.LambdaHack.Common.ItemDescription: textAllAE :: Int -> Bool -> CStore -> ItemFull -> [Text]
- Game.LambdaHack.Common.Level: Level :: !AbsDepth -> !ActorPrio -> !ItemFloor -> !ItemFloor -> !TileMap -> !X -> !Y -> !SmellMap -> !Text -> !([Point], [Point]) -> !Int -> !Int -> !Time -> !Int -> !(Freqs ItemKind) -> !Int -> !(Freqs ItemKind) -> !Int -> !Int -> !Bool -> Level
+ Game.LambdaHack.Common.Level: Level :: !AbsDepth -> !ActorPrio -> !ItemFloor -> !ItemFloor -> !TileMap -> !X -> !Y -> !SmellMap -> !Text -> !([Point], [Point]) -> !Int -> !Int -> !Time -> !Int -> !(Freqs ItemKind) -> !Int -> !(Freqs ItemKind) -> !Int -> !Int -> ![Point] -> Level
- Game.LambdaHack.Common.Level: lescape :: Level -> !Bool
+ Game.LambdaHack.Common.Level: lescape :: Level -> ![Point]
- Game.LambdaHack.Common.Misc: type Rarity = [(Int, Int)]
+ Game.LambdaHack.Common.Misc: type Rarity = [(Double, Int)]
- Game.LambdaHack.Common.Msg: splitOverlay :: Bool -> Y -> Overlay -> Overlay -> Slideshow
+ Game.LambdaHack.Common.Msg: splitOverlay :: Maybe Bool -> Y -> Overlay -> Overlay -> Slideshow
- Game.LambdaHack.Common.Msg: toSlideshow :: Bool -> [[Text]] -> Slideshow
+ Game.LambdaHack.Common.Msg: toSlideshow :: Maybe Bool -> [[Text]] -> Slideshow
- Game.LambdaHack.Common.Request: permittedApply :: [Char] -> Int -> ItemFull -> Actor -> [ItemFull] -> Either ReqFailure Bool
+ Game.LambdaHack.Common.Request: permittedApply :: [Char] -> Time -> Int -> ItemFull -> Actor -> [ItemFull] -> Either ReqFailure Bool
- Game.LambdaHack.Common.State: defStateGlobal :: Dungeon -> AbsDepth -> FactionDict -> COps -> ScoreTable -> State
+ Game.LambdaHack.Common.State: defStateGlobal :: Dungeon -> AbsDepth -> FactionDict -> COps -> ScoreDict -> Id ModeKind -> State
- Game.LambdaHack.Common.State: shigh :: State -> ScoreTable
+ Game.LambdaHack.Common.State: shigh :: State -> ScoreDict
- Game.LambdaHack.Common.Tile: TileSpeedup :: !Tab -> !Tab -> !Tab -> !Tab -> !Tab -> !Tab -> !Tab -> TileSpeedup
+ Game.LambdaHack.Common.Tile: TileSpeedup :: !Tab -> !Tab -> !Tab -> !Tab -> !Tab -> !Tab -> !Tab -> !Tab -> TileSpeedup
- Game.LambdaHack.Content.ItemKind: Burn :: !Int -> Effect
+ Game.LambdaHack.Content.ItemKind: Burn :: !Dice -> Effect
- Game.LambdaHack.Content.ItemKind: Identify :: !CStore -> Effect
+ Game.LambdaHack.Content.ItemKind: Identify :: Effect
- Game.LambdaHack.Content.ItemKind: PolyItem :: !CStore -> Effect
+ Game.LambdaHack.Content.ItemKind: PolyItem :: Effect
- Game.LambdaHack.Content.ModeKind: Player :: !Text -> !(GroupName ItemKind) -> !Skills -> !Bool -> !Bool -> !Bool -> !Bool -> !Tactic -> !a -> !a -> !LeaderMode -> !Bool -> Player a
+ Game.LambdaHack.Content.ModeKind: Player :: !Text -> !(GroupName ItemKind) -> !Skills -> !Bool -> !Bool -> !HiCondPoly -> !Bool -> !Bool -> !Tactic -> !a -> !a -> !LeaderMode -> !Bool -> Player a
- Game.LambdaHack.Content.RuleKind: RuleKind :: !Char -> !Text -> !(Freqs RuleKind) -> !(Maybe (Point -> Point -> Bool)) -> !(Maybe (Point -> Point -> Bool)) -> !Text -> (FilePath -> IO FilePath) -> !Version -> !FilePath -> !String -> !Text -> !Bool -> !FovMode -> !Int -> !Int -> !FilePath -> !String -> !Bool -> !Int -> RuleKind
+ Game.LambdaHack.Content.RuleKind: RuleKind :: !Char -> !Text -> !(Freqs RuleKind) -> !(Maybe (Point -> Point -> Bool)) -> !(Maybe (Point -> Point -> Bool)) -> !Text -> (FilePath -> IO FilePath) -> !Version -> !FilePath -> !String -> !Text -> !Bool -> !FovMode -> !Int -> !Int -> !FilePath -> !String -> !Int -> RuleKind
- Game.LambdaHack.Server.CommonServer: deduceKilled :: (MonadAtomic m, MonadServer m) => Actor -> m ()
+ Game.LambdaHack.Server.CommonServer: deduceKilled :: (MonadAtomic m, MonadServer m) => ActorId -> Actor -> m ()
- Game.LambdaHack.Server.CommonServer: deduceQuits :: (MonadAtomic m, MonadServer m) => Actor -> Status -> m ()
+ Game.LambdaHack.Server.CommonServer: deduceQuits :: (MonadAtomic m, MonadServer m) => FactionId -> Maybe (ActorId, Actor) -> Status -> m ()
- Game.LambdaHack.Server.CommonServer: revealItems :: (MonadAtomic m, MonadServer m) => Maybe FactionId -> Maybe Actor -> m ()
+ Game.LambdaHack.Server.CommonServer: revealItems :: (MonadAtomic m, MonadServer m) => Maybe FactionId -> Maybe (ActorId, Actor) -> m ()
- Game.LambdaHack.Server.DungeonGen: levelFromCaveKind :: COps -> CaveKind -> AbsDepth -> TileMap -> ([Point], [Point]) -> Int -> Freqs ItemKind -> Int -> Freqs ItemKind -> Int -> Bool -> Level
+ Game.LambdaHack.Server.DungeonGen: levelFromCaveKind :: COps -> CaveKind -> AbsDepth -> TileMap -> ([Point], [Point]) -> Int -> Freqs ItemKind -> Int -> Freqs ItemKind -> Int -> [Point] -> Level
- Game.LambdaHack.Server.EndServer: endOrLoop :: (MonadAtomic m, MonadServer m) => m () -> m () -> m () -> m () -> m ()
+ Game.LambdaHack.Server.EndServer: endOrLoop :: (MonadAtomic m, MonadServer m) => m () -> (Maybe (GroupName ModeKind) -> m ()) -> m () -> m () -> m ()
- Game.LambdaHack.Server.Fov: fidLidPerception :: COps -> FovMode -> PersLit -> FactionId -> LevelId -> Level -> Perception
+ Game.LambdaHack.Server.Fov: fidLidPerception :: FovMode -> PersLit -> FactionId -> LevelId -> Level -> Perception
- Game.LambdaHack.Server.Fov: type PersLit = EnumMap LevelId (PerceptionLit, EnumMap FactionId ActorEqpBody, Array Bool)
+ Game.LambdaHack.Server.Fov: type PersLit = EnumMap LevelId (EnumMap FactionId [(Actor, FovCache3)], Array Bool, Array Bool)
- Game.LambdaHack.Server.ItemRev: newItem :: COps -> FlavourMap -> DiscoveryKindRev -> Freqs ItemKind -> LevelId -> AbsDepth -> AbsDepth -> Rnd (Maybe (ItemKnown, ItemFull, ItemSeed, GroupName ItemKind))
+ Game.LambdaHack.Server.ItemRev: newItem :: COps -> FlavourMap -> DiscoveryKindRev -> UniqueSet -> Freqs ItemKind -> Int -> LevelId -> AbsDepth -> AbsDepth -> Rnd (Maybe (ItemKnown, ItemFull, ItemDisco, ItemSeed, GroupName ItemKind))
- Game.LambdaHack.Server.ItemServer: registerItem :: (MonadAtomic m, MonadServer m) => Item -> ItemKnown -> ItemSeed -> Int -> Container -> Bool -> m ItemId
+ Game.LambdaHack.Server.ItemServer: registerItem :: (MonadAtomic m, MonadServer m) => ItemFull -> ItemKnown -> ItemSeed -> Int -> Container -> Bool -> m ItemId
- Game.LambdaHack.Server.ItemServer: rollItem :: (MonadAtomic m, MonadServer m) => LevelId -> Freqs ItemKind -> m (Maybe (ItemKnown, ItemFull, ItemSeed, GroupName ItemKind))
+ Game.LambdaHack.Server.ItemServer: rollItem :: (MonadAtomic m, MonadServer m) => Int -> LevelId -> Freqs ItemKind -> m (Maybe (ItemKnown, ItemFull, ItemDisco, ItemSeed, GroupName ItemKind))
- Game.LambdaHack.Server.LoopServer: loopSer :: (MonadAtomic m, MonadServerReadRequest m) => DebugModeSer -> (FactionId -> ChanServer ResponseUI RequestUI -> IO ()) -> (FactionId -> ChanServer ResponseAI RequestAI -> IO ()) -> COps -> m ()
+ Game.LambdaHack.Server.LoopServer: loopSer :: (MonadAtomic m, MonadServerReadRequest m) => COps -> DebugModeSer -> (FactionId -> ChanServer ResponseUI RequestUI -> IO ()) -> (FactionId -> ChanServer ResponseAI RequestAI -> IO ()) -> m ()
- Game.LambdaHack.Server.MonadServer: restoreScore :: MonadServer m => COps -> m ScoreTable
+ Game.LambdaHack.Server.MonadServer: restoreScore :: MonadServer m => COps -> m ScoreDict
- Game.LambdaHack.Server.StartServer: gameReset :: MonadServer m => COps -> DebugModeSer -> Maybe StdGen -> m State
+ Game.LambdaHack.Server.StartServer: gameReset :: MonadServer m => COps -> DebugModeSer -> Maybe (GroupName ModeKind) -> Maybe StdGen -> m State
- Game.LambdaHack.Server.State: DebugModeSer :: !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !(Maybe (GroupName ModeKind)) -> !Bool -> !(Maybe Int) -> !(Maybe StdGen) -> !(Maybe StdGen) -> !(Maybe FovMode) -> !Bool -> !Int -> !Bool -> !(Maybe String) -> !Bool -> !DebugModeCli -> DebugModeSer
+ Game.LambdaHack.Server.State: DebugModeSer :: !Bool -> !Bool -> !Bool -> !Bool -> !Bool -> !(Maybe (GroupName ModeKind)) -> !Bool -> !Bool -> !(Maybe Int) -> !(Maybe StdGen) -> !(Maybe StdGen) -> !(Maybe FovMode) -> !Bool -> !Int -> !Bool -> !(Maybe String) -> !Bool -> !DebugModeCli -> DebugModeSer
- Game.LambdaHack.Server.State: StateServer :: !DiscoveryKind -> !DiscoveryKindRev -> !DiscoveryEffect -> !ItemSeedDict -> !ItemRev -> !FlavourMap -> !ActorId -> !ItemId -> !(EnumMap LevelId Time) -> ![CmdAtomic] -> !Pers -> !StdGen -> !RNGs -> !Bool -> !Bool -> !ClockTime -> !ClockTime -> !Time -> !(EnumMap FactionId [(Int, (Text, Text))]) -> !DebugModeSer -> !DebugModeSer -> StateServer
+ Game.LambdaHack.Server.State: StateServer :: !DiscoveryKind -> !DiscoveryKindRev -> !UniqueSet -> !DiscoveryEffect -> !ItemSeedDict -> !ItemRev -> !(EnumMap ItemId FovCache3) -> !FlavourMap -> !ActorId -> !ItemId -> !(EnumMap LevelId Int) -> !(EnumMap LevelId Time) -> ![CmdAtomic] -> !Pers -> !StdGen -> !RNGs -> !Bool -> !Bool -> !ClockTime -> !ClockTime -> !Time -> !(EnumMap FactionId [(Int, (Text, Text))]) -> !DebugModeSer -> !DebugModeSer -> StateServer
Files
- CHANGELOG.md +27/−1
- Game/LambdaHack/Atomic.hs +4/−4
- Game/LambdaHack/Atomic/BroadcastAtomicWrite.hs +12/−14
- Game/LambdaHack/Atomic/CmdAtomic.hs +38/−31
- Game/LambdaHack/Atomic/HandleAtomicWrite.hs +67/−48
- Game/LambdaHack/Atomic/MonadAtomic.hs +8/−2
- Game/LambdaHack/Atomic/MonadStateWrite.hs +39/−37
- Game/LambdaHack/Atomic/PosAtomicRead.hs +39/−24
- Game/LambdaHack/Client.hs +4/−36
- Game/LambdaHack/Client/AI.hs +23/−16
- Game/LambdaHack/Client/AI/ConditionClient.hs +168/−46
- Game/LambdaHack/Client/AI/HandleAbilityClient.hs +375/−293
- Game/LambdaHack/Client/AI/PickActorClient.hs +51/−26
- Game/LambdaHack/Client/AI/PickTargetClient.hs +156/−121
- Game/LambdaHack/Client/AI/Preferences.hs +64/−52
- Game/LambdaHack/Client/AI/Strategy.hs +5/−2
- Game/LambdaHack/Client/Bfs.hs +16/−13
- Game/LambdaHack/Client/BfsClient.hs +110/−53
- Game/LambdaHack/Client/CommonClient.hs +86/−66
- Game/LambdaHack/Client/HandleAtomicClient.hs +42/−77
- Game/LambdaHack/Client/HandleResponseClient.hs +2/−4
- Game/LambdaHack/Client/ItemSlot.hs +49/−34
- Game/LambdaHack/Client/Key.hs +54/−17
- Game/LambdaHack/Client/LoopClient.hs +3/−2
- Game/LambdaHack/Client/State.hs +17/−16
- Game/LambdaHack/Client/UI.hs +29/−55
- Game/LambdaHack/Client/UI/Animation.hs +106/−47
- Game/LambdaHack/Client/UI/Config.hs +4/−3
- Game/LambdaHack/Client/UI/Content/KeyKind.hs +15/−0
- Game/LambdaHack/Client/UI/DisplayAtomicClient.hs +284/−195
- Game/LambdaHack/Client/UI/DrawClient.hs +56/−47
- Game/LambdaHack/Client/UI/Frontend.hs +35/−30
- Game/LambdaHack/Client/UI/Frontend/Chosen.hs +5/−5
- Game/LambdaHack/Client/UI/Frontend/Curses.hs +4/−5
- Game/LambdaHack/Client/UI/Frontend/Gtk.hs +83/−46
- Game/LambdaHack/Client/UI/Frontend/Std.hs +4/−5
- Game/LambdaHack/Client/UI/Frontend/Vty.hs +17/−13
- Game/LambdaHack/Client/UI/HandleHumanClient.hs +20/−12
- Game/LambdaHack/Client/UI/HandleHumanGlobalClient.hs +402/−165
- Game/LambdaHack/Client/UI/HandleHumanLocalClient.hs +140/−284
- Game/LambdaHack/Client/UI/HumanCmd.hs +73/−41
- Game/LambdaHack/Client/UI/InventoryClient.hs +1021/−415
- Game/LambdaHack/Client/UI/KeyBindings.hs +64/−49
- Game/LambdaHack/Client/UI/MonadClientUI.hs +65/−48
- Game/LambdaHack/Client/UI/MsgClient.hs +24/−18
- Game/LambdaHack/Client/UI/RunClient.hs +88/−135
- Game/LambdaHack/Client/UI/StartupFrontendClient.hs +7/−5
- Game/LambdaHack/Client/UI/WidgetClient.hs +92/−40
- Game/LambdaHack/Common/Ability.hs +4/−7
- Game/LambdaHack/Common/Actor.hs +75/−59
- Game/LambdaHack/Common/ActorState.hs +135/−46
- Game/LambdaHack/Common/Dice.hs +39/−27
- Game/LambdaHack/Common/EffectDescription.hs +17/−18
- Game/LambdaHack/Common/Faction.hs +6/−26
- Game/LambdaHack/Common/File.hs +1/−1
- Game/LambdaHack/Common/Frequency.hs +26/−20
- Game/LambdaHack/Common/HighScore.hs +52/−47
- Game/LambdaHack/Common/Item.hs +3/−2
- Game/LambdaHack/Common/ItemDescription.hs +76/−44
- Game/LambdaHack/Common/ItemStrongest.hs +20/−38
- Game/LambdaHack/Common/Kind.hs +12/−11
- Game/LambdaHack/Common/Level.hs +4/−3
- Game/LambdaHack/Common/Misc.hs +33/−6
- Game/LambdaHack/Common/MonadStateRead.hs +9/−2
- Game/LambdaHack/Common/Msg.hs +27/−17
- Game/LambdaHack/Common/Point.hs +12/−4
- Game/LambdaHack/Common/PointArray.hs +43/−5
- Game/LambdaHack/Common/Random.hs +4/−4
- Game/LambdaHack/Common/Request.hs +44/−29
- Game/LambdaHack/Common/Save.hs +1/−1
- Game/LambdaHack/Common/State.hs +23/−11
- Game/LambdaHack/Common/Tile.hs +41/−28
- Game/LambdaHack/Common/Time.hs +5/−6
- Game/LambdaHack/Common/Vector.hs +21/−12
- Game/LambdaHack/Content/CaveKind.hs +1/−1
- Game/LambdaHack/Content/ItemKind.hs +55/−51
- Game/LambdaHack/Content/ModeKind.hs +32/−4
- Game/LambdaHack/Content/RuleKind.hs +0/−1
- Game/LambdaHack/Content/TileKind.hs +6/−0
- Game/LambdaHack/SampleImplementation/SampleMonadClient.hs +4/−1
- Game/LambdaHack/SampleImplementation/SampleMonadServer.hs +17/−3
- Game/LambdaHack/Server.hs +8/−47
- Game/LambdaHack/Server/Commandline.hs +4/−0
- Game/LambdaHack/Server/CommonServer.hs +84/−65
- Game/LambdaHack/Server/DebugServer.hs +17/−13
- Game/LambdaHack/Server/DungeonGen.hs +28/−27
- Game/LambdaHack/Server/DungeonGen/AreaRnd.hs +4/−4
- Game/LambdaHack/Server/DungeonGen/Cave.hs +9/−8
- Game/LambdaHack/Server/DungeonGen/Place.hs +19/−16
- Game/LambdaHack/Server/EndServer.hs +14/−16
- Game/LambdaHack/Server/Fov.hs +106/−112
- Game/LambdaHack/Server/Fov/Digital.hs +23/−9
- Game/LambdaHack/Server/HandleEffectServer.hs +258/−192
- Game/LambdaHack/Server/HandleRequestServer.hs +89/−77
- Game/LambdaHack/Server/ItemRev.hs +42/−20
- Game/LambdaHack/Server/ItemServer.hs +71/−29
- Game/LambdaHack/Server/LoopServer.hs +64/−65
- Game/LambdaHack/Server/MonadServer.hs +14/−12
- Game/LambdaHack/Server/PeriodicServer.hs +83/−62
- Game/LambdaHack/Server/ProtocolServer.hs +2/−1
- Game/LambdaHack/Server/StartServer.hs +62/−47
- Game/LambdaHack/Server/State.hs +43/−4
- GameDefinition/Client/UI/Content/KeyKind.hs +83/−57
- GameDefinition/Content/CaveKind.hs +67/−20
- GameDefinition/Content/ItemKind.hs +276/−159
- GameDefinition/Content/ItemKindActor.hs +240/−97
- GameDefinition/Content/ItemKindBlast.hs +70/−35
- GameDefinition/Content/ItemKindOrgan.hs +177/−25
- GameDefinition/Content/ItemKindTemporary.hs +11/−11
- GameDefinition/Content/ModeKind.hs +139/−43
- GameDefinition/Content/ModeKindPlayer.hs +62/−18
- GameDefinition/Content/PlaceKind.hs +22/−2
- GameDefinition/Content/RuleKind.hs +1/−2
- GameDefinition/Content/TileKind.hs +5/−5
- GameDefinition/PLAYING.md +122/−148
- GameDefinition/TieKnot.hs +26/−14
- GameDefinition/config.ui.default +37/−37
- GameDefinition/scores binary
- LICENSE +2/−2
- LambdaHack.cabal +24/−16
- Makefile +73/−143
- README.md +24/−24
- test/test.hs +1/−1
CHANGELOG.md view
@@ -1,4 +1,30 @@-## [v0.4.100.0, aka 'The last thaw'](https://github.com/LambdaHack/LambdaHack/compare/v0.4.99.0..v0.4.100.0)+## [v0.4.101.0, aka 'Officially fun'](https://github.com/LambdaHack/LambdaHack/compare/v0.4.100.0...v0.4.101.0)++- the game is now officially fun to play+- introduce unique boss monsters and unique artifact items+- add animals that heal the player+- let AI gang up, attempt stealth and react to player aggressiveness+- spawn actors fast and close to the enemy+- spawn actors less and less often on a given level, but with growing depth+- prefer weapons with effects, if recharged+- make the bracing melee bonus additive, not multiplicative+- let explosions buffet actors around+- make braced actors immune to translocation effects+- use mouse for movement, actor selection, aiming+- don't run straight with selected actors, but go-to cross-hair with them+- speed up default frame rate, slot down projectiles visually+- rework item manipulation UI+- you can pick up many items at once and it costs only one turn+- allow actors to apply and project from the shared stash+- reverse messages shown in player diary+- display actor organs and stats+- split highscore tables wrt game modes+- move score calculation formula to content+- don't keep the default/example config file commented out; was misleading+- I was naughty again and changed v0.5.0.0 of LambdaHack content API slightly+ one last time++## [v0.4.100.0, aka 'The last thaw'](https://github.com/LambdaHack/LambdaHack/compare/v0.4.99.0...v0.4.100.0) - unexpectedly thaw and freeze again v0.5.0.0 of LambdaHack content API - unexpectedly implement timeouts and temporary effects easily without FRP
Game/LambdaHack/Atomic.hs view
@@ -1,14 +1,14 @@--- | Atomic game state transformations. TODO: haddocks.+-- | Atomic game state transformations. -- -- See -- <https://github.com/LambdaHack/LambdaHack/wiki/Client-server-architecture>. module Game.LambdaHack.Atomic- ( -- * Re-exported from MonadAtomic+ ( -- * Re-exported from "Game.LambdaHack.Atomic.MonadAtomic" MonadAtomic(..) , broadcastUpdAtomic, broadcastSfxAtomic- -- * Re-exported from CmdAtomic+ -- * Re-exported from "Game.LambdaHack.Atomic.CmdAtomic" , CmdAtomic(..), UpdAtomic(..), SfxAtomic(..), HitAtomic(..)- -- * Re-exported from PosAtomicRead+ -- * Re-exported from "Game.LambdaHack.Atomic.PosAtomicRead" , PosAtomic(..), posUpdAtomic, posSfxAtomic, seenAtomicCli, generalMoveItem ) where
Game/LambdaHack/Atomic/BroadcastAtomicWrite.hs view
@@ -37,7 +37,7 @@ handleCmdAtomicServer :: forall m. MonadStateWrite m => PosAtomic -> CmdAtomic -> m () handleCmdAtomicServer posAtomic atomic =- when (seenAtomicSer posAtomic) $ do+ when (seenAtomicSer posAtomic) $ -- storeUndo atomic handleCmdAtomic atomic @@ -69,15 +69,14 @@ psBroken <- mapM posSfxAtomic atomicBroken return (ps, False, map SfxAtomic atomicBroken, psBroken) let atomicPsBroken = zip atomicBroken psBroken- -- TODO: assert also that the sum of psBroken is equal to ps- -- TODO: with deep equality these assertions can be expensive. Optimize.- assert (case ps of- PosSight{} -> True- PosFidAndSight{} -> True- PosFidAndSer (Just _) _ -> True- _ -> not resets- && (null atomicBroken- || atomicBroken == [atomic])) skip+ -- TODO: assert also that the sum of psBroken is equal to ps;+ -- with deep equality these assertions can be expensive; optimize.+ let !_A = assert (case ps of+ PosSight{} -> True+ PosFidAndSight{} -> True+ PosFidAndSer (Just _) _ -> True+ _ -> not resets+ `blame` (ps, resets)) () -- Perform the action on the server. handleCmdAtomicServer ps atomic -- Update lights in the dungeon. This is lazy, may not be needed or partially.@@ -85,7 +84,7 @@ -- Send some actions to the clients, one faction at a time. let sendUI fid cmdUI = when (fhasUI $ gplayer $ factionD EM.! fid) $ doSendUpdateUI fid cmdUI- sendAI fid cmdAI = doSendUpdateAI fid cmdAI+ sendAI = doSendUpdateAI sendA fid cmd = do sendUI fid $ RespUpdAtomicUI cmd sendAI fid $ RespUpdAtomicAI cmd@@ -126,12 +125,11 @@ let remember = atomicRemember lid inPer sOld seenNew = seenAtomicCli False fid perNew seenOld = seenAtomicCli False fid perOld- -- TODO: these assertions are probably expensive psRem <- mapM posUpdAtomic remember -- Verify that we remember only currently seen things.- assert (allB seenNew psRem) skip+ let !_A = assert (allB seenNew psRem) () -- Verify that we remember only new things.- assert (allB (not . seenOld) psRem) skip+ let !_A = assert (allB (not . seenOld) psRem) () mapM_ (sendA fid) remember anySend lid fid perOld perNew else anySend lid fid perOld perOld
Game/LambdaHack/Atomic/CmdAtomic.hs view
@@ -17,6 +17,7 @@ , undoUpdAtomic, undoSfxAtomic, undoCmdAtomic ) where +import Control.Applicative import Data.Binary import Data.Int (Int64) import GHC.Generics (Generic)@@ -24,7 +25,6 @@ import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ClientOptions import qualified Game.LambdaHack.Common.Color as Color-import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.Kind as Kind@@ -37,18 +37,23 @@ import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.ItemKind (ItemKind)-import Game.LambdaHack.Content.ModeKind+import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.TileKind (TileKind) import qualified Game.LambdaHack.Content.TileKind as TK +-- | Abstract syntax of atomic commands, that is, atomic game state+-- transformations. data CmdAtomic =- UpdAtomic !UpdAtomic- | SfxAtomic !SfxAtomic+ UpdAtomic !UpdAtomic -- ^ atomic updates+ | SfxAtomic !SfxAtomic -- ^ atomic special effects deriving (Show, Eq, Generic) instance Binary CmdAtomic --- | Abstract syntax of atomic commands.+-- | Abstract syntax of atomic updates, that is, atomic commands+-- that really change the state. Most of them are an encoding of a game+-- state diff, though they also carry some intentional hints+-- that help clients determine whether and how to communicate them to players. data UpdAtomic = -- Create/destroy actors and items. UpdCreateActor !ActorId !Actor ![(ItemId, Item)]@@ -68,7 +73,7 @@ | UpdAgeActor !ActorId !(Delta Time) | UpdRefillHP !ActorId !Int64 | UpdRefillCalm !ActorId !Int64- | UpdOldFidActor !ActorId !FactionId !FactionId+ | UpdFidImpressedActor !ActorId !FactionId !FactionId | UpdTrajectory !ActorId !(Maybe ([Vector], Speed)) !(Maybe ([Vector], Speed)) | UpdColorActor !ActorId !Color.Color !Color.Color -- Change faction attributes.@@ -92,15 +97,14 @@ -- Assorted. | UpdTimeItem !ItemId !Container !ItemTimer !ItemTimer | UpdAgeGame !(Delta Time) ![LevelId]- | UpdDiscover !LevelId !Point !ItemId !(Kind.Id ItemKind) !ItemSeed- | UpdCover !LevelId !Point !ItemId !(Kind.Id ItemKind) !ItemSeed- | UpdDiscoverKind !LevelId !Point !ItemId !(Kind.Id ItemKind)- | UpdCoverKind !LevelId !Point !ItemId !(Kind.Id ItemKind)- | UpdDiscoverSeed !LevelId !Point !ItemId !ItemSeed- | UpdCoverSeed !LevelId !Point !ItemId !ItemSeed+ | UpdDiscover !Container !ItemId !(Kind.Id ItemKind) !ItemSeed+ | UpdCover !Container !ItemId !(Kind.Id ItemKind) !ItemSeed+ | UpdDiscoverKind !Container !ItemId !(Kind.Id ItemKind)+ | UpdCoverKind !Container !ItemId !(Kind.Id ItemKind)+ | UpdDiscoverSeed !Container !ItemId !ItemSeed+ | UpdCoverSeed !Container !ItemId !ItemSeed | UpdPerception !LevelId !Perception !Perception- | UpdRestart- !FactionId !DiscoveryKind !FactionPers !State !DebugModeCli !(GroupName ModeKind)+ | UpdRestart !FactionId !DiscoveryKind !FactionPers !State !DebugModeCli | UpdRestartServer !State | UpdResume !FactionId !FactionPers | UpdResumeServer !State@@ -112,17 +116,18 @@ instance Binary UpdAtomic --- | Abstract syntax of atomic special effects.+-- | Abstract syntax of atomic special effects, that is, atomic commands+-- that only display special effects and don't change the state. data SfxAtomic =- SfxStrike !ActorId !ActorId !ItemId !HitAtomic- | SfxRecoil !ActorId !ActorId !ItemId !HitAtomic+ SfxStrike !ActorId !ActorId !ItemId !CStore !HitAtomic+ | SfxRecoil !ActorId !ActorId !ItemId !CStore !HitAtomic | SfxProject !ActorId !ItemId !CStore | SfxCatch !ActorId !ItemId !CStore- | SfxActivate !ActorId !ItemId !CStore+ | SfxApply !ActorId !ItemId !CStore | SfxCheck !ActorId !ItemId !CStore | SfxTrigger !ActorId !Point !TK.Feature | SfxShun !ActorId !Point !TK.Feature- | SfxEffect !FactionId !ActorId !(IK.Effect)+ | SfxEffect !FactionId !ActorId !IK.Effect | SfxMsgFid !FactionId !Msg | SfxMsgAll !Msg | SfxActorStart !ActorId@@ -130,6 +135,7 @@ instance Binary SfxAtomic +-- | Determine if a strike special effect should depict a block of an attack. data HitAtomic = HitClear | HitBlock !Int deriving (Show, Eq, Generic) @@ -152,7 +158,8 @@ UpdAgeActor aid delta -> Just $ UpdAgeActor aid (timeDeltaReverse delta) UpdRefillHP aid n -> Just $ UpdRefillHP aid (-n) UpdRefillCalm aid n -> Just $ UpdRefillCalm aid (-n)- UpdOldFidActor aid fromFid toFid -> Just $ UpdOldFidActor aid toFid fromFid+ UpdFidImpressedActor aid fromFid toFid ->+ Just $ UpdFidImpressedActor aid toFid fromFid UpdTrajectory aid fromT toT -> Just $ UpdTrajectory aid toT fromT UpdColorActor aid fromCol toCol -> Just $ UpdColorActor aid toCol fromCol UpdQuitFaction fid mb fromSt toSt -> Just $ UpdQuitFaction fid mb toSt fromSt@@ -175,12 +182,12 @@ UpdLoseSmell lid sms -> Just $ UpdSpotSmell lid sms UpdTimeItem iid c fromIt toIt -> Just $ UpdTimeItem iid c toIt fromIt UpdAgeGame delta lids -> Just $ UpdAgeGame (timeDeltaReverse delta) lids- UpdDiscover lid p iid ik seed -> Just $ UpdCover lid p iid ik seed- UpdCover lid p iid ik seed -> Just $ UpdDiscover lid p iid ik seed- UpdDiscoverKind lid p iid ik -> Just $ UpdCoverKind lid p iid ik- UpdCoverKind lid p iid ik -> Just $ UpdDiscoverKind lid p iid ik- UpdDiscoverSeed lid p iid seed -> Just $ UpdCoverSeed lid p iid seed- UpdCoverSeed lid p iid seed -> Just $ UpdDiscoverSeed lid p iid seed+ UpdDiscover c iid ik seed -> Just $ UpdCover c iid ik seed+ UpdCover c iid ik seed -> Just $ UpdDiscover c iid ik seed+ UpdDiscoverKind c iid ik -> Just $ UpdCoverKind c iid ik+ UpdCoverKind c iid ik -> Just $ UpdDiscoverKind c iid ik+ UpdDiscoverSeed c iid seed -> Just $ UpdCoverSeed c iid seed+ UpdCoverSeed c iid seed -> Just $ UpdDiscoverSeed c iid seed UpdPerception lid outPer inPer -> Just $ UpdPerception lid inPer outPer UpdRestart{} -> Just cmd -- here history ends; change direction UpdRestartServer{} -> Just cmd -- here history ends; change direction@@ -193,12 +200,12 @@ undoSfxAtomic :: SfxAtomic -> SfxAtomic undoSfxAtomic cmd = case cmd of- SfxStrike source target iid b -> SfxRecoil source target iid b- SfxRecoil source target iid b -> SfxStrike source target iid b+ SfxStrike source target iid cstore b -> SfxRecoil source target iid cstore b+ SfxRecoil source target iid cstore b -> SfxStrike source target iid cstore b SfxProject aid iid cstore -> SfxCatch aid iid cstore SfxCatch aid iid cstore -> SfxProject aid iid cstore- SfxActivate aid iid cstore -> SfxCheck aid iid cstore- SfxCheck aid iid cstore -> SfxActivate aid iid cstore+ SfxApply aid iid cstore -> SfxCheck aid iid cstore+ SfxCheck aid iid cstore -> SfxApply aid iid cstore SfxTrigger aid p feat -> SfxShun aid p feat SfxShun aid p feat -> SfxTrigger aid p feat SfxEffect{} -> cmd -- not ideal?@@ -207,5 +214,5 @@ SfxActorStart{} -> cmd undoCmdAtomic :: CmdAtomic -> Maybe CmdAtomic-undoCmdAtomic (UpdAtomic cmd) = fmap UpdAtomic $ undoUpdAtomic cmd+undoCmdAtomic (UpdAtomic cmd) = UpdAtomic <$> undoUpdAtomic cmd undoCmdAtomic (SfxAtomic sfx) = Just $ SfxAtomic $ undoSfxAtomic sfx
Game/LambdaHack/Atomic/HandleAtomicWrite.hs view
@@ -60,7 +60,8 @@ UpdAgeActor aid t -> updAgeActor aid t UpdRefillHP aid n -> updRefillHP aid n UpdRefillCalm aid n -> updRefillCalm aid n- UpdOldFidActor aid fromFid toFid -> updOldFidActor aid fromFid toFid+ UpdFidImpressedActor aid fromFid toFid ->+ updFidImpressedActor aid fromFid toFid UpdTrajectory aid fromT toT -> updTrajectory aid fromT toT UpdColorActor aid fromCol toCol -> updColorActor aid fromCol toCol UpdQuitFaction fid mbody fromSt toSt -> updQuitFaction fid mbody fromSt toSt@@ -89,8 +90,8 @@ UpdDiscoverSeed{} -> return () UpdCoverSeed{} -> return () UpdPerception _ outPer inPer ->- assert (not (nullPer outPer && nullPer inPer)) skip- UpdRestart _ _ _ s _ _ -> updRestart s+ assert (not (nullPer outPer && nullPer inPer)) (return ())+ UpdRestart _ _ _ s _ -> updRestart s UpdRestartServer s -> updRestartServer s UpdResume{} -> return () UpdResumeServer s -> updResumeServer s@@ -146,8 +147,8 @@ -- that do not appear anywhere else, for simplicity and speed. itemD <- getsState sitemD let match (iid, item) = itemsMatch (itemD EM.! iid) item- assert (allB match ais `blame` "destroyed actor items not found"- `twith` (aid, body, ais, itemD)) skip+ let !_A = assert (allB match ais `blame` "destroyed actor items not found"+ `twith` (aid, body, ais, itemD)) () -- Remove actor from @sactorD@. let f Nothing = assert `failure` "actor already removed" `twith` (aid, body) f (Just b) = assert (b == body `blame` "inconsistent destroyed actor body"@@ -175,34 +176,35 @@ modifyState $ updateItemD $ EM.insertWith f iid item insertItemContainer iid kit c --- TODO: verify kit -- | Destroy some copies (possibly not all) of an item. updDestroyItem :: MonadStateWrite m => ItemId -> Item -> ItemQuant -> Container -> m ()-updDestroyItem iid item (k, _) c = assert (k > 0) $ do+updDestroyItem iid item kit@(k, _) c = assert (k > 0) $ do -- Do not remove the item from @sitemD@ nor from @sitemRev@, -- It's incredibly costly and not noticeable for the player. -- However, assert the item is registered in @sitemD@. itemD <- getsState sitemD- assert ((case iid `EM.lookup` itemD of- Nothing -> False- Just item0 -> itemsMatch item0 item)- `blame` "item already removed"- `twith` (iid, item, itemD)) skip- deleteItemContainer iid k c+ let !_A = assert ((case iid `EM.lookup` itemD of+ Nothing -> False+ Just item0 -> itemsMatch item0 item)+ `blame` "item already removed"+ `twith` (iid, item, itemD)) ()+ deleteItemContainer iid kit c updMoveActor :: MonadStateWrite m => ActorId -> Point -> Point -> m () updMoveActor aid fromP toP = assert (fromP /= toP) $ do b <- getsState $ getActorBody aid- assert (fromP == bpos b `blame` "unexpected moved actor position"- `twith` (aid, fromP, toP, bpos b, b)) skip+ let !_A = assert (fromP == bpos b+ `blame` "unexpected moved actor position"+ `twith` (aid, fromP, toP, bpos b, b)) () updateActor aid $ \body -> body {bpos = toP, boldpos = fromP} updWaitActor :: MonadStateWrite m => ActorId -> Bool -> m () updWaitActor aid toWait = do b <- getsState $ getActorBody aid- assert (toWait /= bwait b `blame` "unexpected waited actor time"- `twith` (aid, toWait, bwait b, b)) skip+ let !_A = assert (toWait /= bwait b+ `blame` "unexpected waited actor time"+ `twith` (aid, toWait, bwait b, b)) () updateActor aid $ \body -> body {bwait = toWait} updDisplaceActor :: MonadStateWrite m => ActorId -> ActorId -> m ()@@ -220,17 +222,31 @@ case iid `EM.lookup` bag of Nothing -> assert `failure` (iid, k, aid, c1, c2) Just (_, it) -> do- deleteItemActor iid k aid c1+ deleteItemActor iid (k, take k it) aid c1 insertItemActor iid (k, take k it) aid c2 --- TODO: optimize (a single call to updatePrio is enough)+-- This is equaivalent to (but much cheaper than) updDestroyActor+-- followed by updCreateActor. updAgeActor :: MonadStateWrite m => ActorId -> Delta Time -> m () updAgeActor aid delta = assert (delta /= Delta timeZero) $ do body <- getsState $ getActorBody aid- ais <- getsState $ getCarriedAssocs body- updDestroyActor aid body ais let newBody = body {btime = timeShift (btime body) delta}- updCreateActor aid newBody ais+ -- Remove actor from @sprio@ at old time.+ rmPrio Nothing = assert `failure` "actor already removed"+ `twith` (aid, body)+ rmPrio (Just l) = assert (aid `elem` l `blame` "actor already removed"+ `twith` (aid, body, l))+ $ let l2 = delete aid l+ in if null l2 then Nothing else Just l2+ -- Add actor to @sprio@ at new time.+ addPrio Nothing = Just [aid]+ addPrio (Just l) = assert (aid `notElem` l `blame` "actor already added"+ `twith` (aid, body, l))+ $ Just $ aid : l+ updPrio = EM.alter addPrio (btime newBody) . EM.alter rmPrio (btime body)+ updateLevel (blid body) $ updatePrio updPrio+ -- Modify actor body in @sactorD@.+ modifyState $ updateActorD $ EM.adjust (const newBody) aid updRefillHP :: MonadStateWrite m => ActorId -> Int64 -> m () updRefillHP aid n =@@ -254,11 +270,11 @@ else oldD {resCurrentTurn = resCurrentTurn oldD + n} } -updOldFidActor :: MonadStateWrite m => ActorId -> FactionId -> FactionId -> m ()-updOldFidActor aid fromFid toFid = assert (fromFid /= toFid) $ do+updFidImpressedActor :: MonadStateWrite m => ActorId -> FactionId -> FactionId -> m ()+updFidImpressedActor aid fromFid toFid = assert (fromFid /= toFid) $ updateActor aid $ \b ->- assert (boldfid b == fromFid `blame` (aid, fromFid, toFid, b))- $ b {boldfid = toFid}+ assert (bfidImpressed b == fromFid `blame` (aid, fromFid, toFid, b))+ $ b {bfidImpressed = toFid} updTrajectory :: MonadStateWrite m => ActorId@@ -267,27 +283,30 @@ -> m () updTrajectory aid fromT toT = assert (fromT /= toT) $ do body <- getsState $ getActorBody aid- assert (fromT == btrajectory body `blame` "unexpected actor trajectory"- `twith` (aid, fromT, toT, body)) skip+ let !_A = assert (fromT == btrajectory body+ `blame` "unexpected actor trajectory"+ `twith` (aid, fromT, toT, body)) () updateActor aid $ \b -> b {btrajectory = toT} updColorActor :: MonadStateWrite m => ActorId -> Color.Color -> Color.Color -> m () updColorActor aid fromCol toCol = assert (fromCol /= toCol) $ do body <- getsState $ getActorBody aid- assert (fromCol == bcolor body `blame` "unexpected actor color"- `twith` (aid, fromCol, toCol, body)) skip+ let !_A = assert (fromCol == bcolor body+ `blame` "unexpected actor color"+ `twith` (aid, fromCol, toCol, body)) () updateActor aid $ \b -> b {bcolor = toCol} updQuitFaction :: MonadStateWrite m => FactionId -> Maybe Actor -> Maybe Status -> Maybe Status -> m () updQuitFaction fid mbody fromSt toSt = do- assert (fromSt /= toSt `blame` (fid, mbody, fromSt, toSt)) skip- assert (maybe True ((fid ==) . bfid) mbody) skip+ let !_A = assert (fromSt /= toSt `blame` (fid, mbody, fromSt, toSt)) ()+ let !_A = assert (maybe True ((fid ==) . bfid) mbody) () fact <- getsState $ (EM.! fid) . sfactionD- assert (fromSt == gquit fact `blame` "unexpected actor quit status"- `twith` (fid, fromSt, toSt, fact)) skip+ let !_A = assert (fromSt == gquit fact+ `blame` "unexpected actor quit status"+ `twith` (fid, fromSt, toSt, fact)) () let adj fa = fa {gquit = toSt} updateFaction fid adj @@ -299,14 +318,14 @@ -> m () updLeadFaction fid source target = assert (source /= target) $ do fact <- getsState $ (EM.! fid) . sfactionD- assert (fleaderMode (gplayer fact) /= LeaderNull) skip+ let !_A = assert (fleaderMode (gplayer fact) /= LeaderNull) () -- @PosNone@ ensures this mtb <- getsState $ \s -> flip getActorBody s . fst <$> target- assert (maybe True (not . bproj) mtb- `blame` (fid, source, target, mtb, fact)) skip- assert (source == gleader fact- `blame` "unexpected actor leader"- `twith` (fid, source, target, mtb, fact)) skip+ let !_A = assert (maybe True (not . bproj) mtb+ `blame` (fid, source, target, mtb, fact)) ()+ let !_A = assert (source == gleader fact+ `blame` "unexpected actor leader"+ `twith` (fid, source, target, mtb, fact)) () let adj fa = fa {gleader = target} updateFaction fid adj @@ -316,10 +335,10 @@ assert (fid1 /= fid2 && fromDipl /= toDipl) $ do fact1 <- getsState $ (EM.! fid1) . sfactionD fact2 <- getsState $ (EM.! fid2) . sfactionD- assert (fromDipl == EM.findWithDefault Unknown fid2 (gdipl fact1)- && fromDipl == EM.findWithDefault Unknown fid1 (gdipl fact2)- `blame` "unexpected actor diplomacy status"- `twith` (fid1, fid2, fromDipl, toDipl, fact1, fact2)) skip+ let !_A = assert (fromDipl == EM.findWithDefault Unknown fid2 (gdipl fact1)+ && fromDipl == EM.findWithDefault Unknown fid1 (gdipl fact2)+ `blame` "unexpected actor diplomacy status"+ `twith` (fid1, fid2, fromDipl, toDipl, fact1, fact2)) () let adj fid fact = fact {gdipl = EM.insert fid toDipl (gdipl fact)} updateFaction fid1 (adj fid2) updateFaction fid2 (adj fid1)@@ -343,12 +362,12 @@ updRecordKill :: MonadStateWrite m => ActorId -> Kind.Id ItemKind -> Int -> m () updRecordKill aid ikind k = do b <- getsState $ getActorBody aid- assert (not (bproj b) `blame` (aid, b)) skip+ let !_A = assert (not (bproj b) `blame` (aid, b)) let alterKind mn = let n = fromMaybe 0 mn + k in if n == 0 then Nothing else Just n adjFact fact = fact {gvictims = EM.alter alterKind ikind $ gvictims fact}- updateFaction (bfid b) adjFact+ updateFaction (bfidOriginal b) adjFact -- | Alter an attribute (actually, the only, the defining attribute) -- of a visible tile. This is similar to e.g., @UpdTrajectory@.@@ -458,8 +477,8 @@ bag <- getsState $ getCBag c case iid `EM.lookup` bag of Just (k, it) -> do- assert (fromIt == it `blame` (k, it, iid, c, fromIt, toIt)) skip- deleteItemContainer iid k c+ let !_A = assert (fromIt == it `blame` (k, it, iid, c, fromIt, toIt)) ()+ deleteItemContainer iid (k, fromIt) c insertItemContainer iid (k, toIt) c Nothing -> assert `failure` (bag, iid, c, fromIt, toIt)
Game/LambdaHack/Atomic/MonadAtomic.hs view
@@ -1,4 +1,4 @@--- | Atomic monads.+-- | Atomic monads for handling atomic game state transformations. module Game.LambdaHack.Atomic.MonadAtomic ( MonadAtomic(..) , broadcastUpdAtomic, broadcastSfxAtomic@@ -7,23 +7,29 @@ import Data.Key (mapWithKeyM_) import Game.LambdaHack.Atomic.CmdAtomic-import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Faction+import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.State +-- | The monad for executing atomic game state transformations. class MonadStateRead m => MonadAtomic m where+ -- | Execute an arbitrary atomic game state transformation. execAtomic :: CmdAtomic -> m ()+ -- | Execute an atomic command that really changes the state. execUpdAtomic :: UpdAtomic -> m () execUpdAtomic = execAtomic . UpdAtomic+ -- | Execute an atomic command that only displays special effects. execSfxAtomic :: SfxAtomic -> m () execSfxAtomic = execAtomic . SfxAtomic +-- | Create and broadcast a set of atomic updates, one for each client. broadcastUpdAtomic :: MonadAtomic m => (FactionId -> UpdAtomic) -> m () broadcastUpdAtomic fcmd = do factionD <- getsState sfactionD mapWithKeyM_ (\fid _ -> execUpdAtomic $ fcmd fid) factionD +-- | Create and broadcast a set of atomic special effects, one for each client. broadcastSfxAtomic :: MonadAtomic m => (FactionId -> SfxAtomic) -> m () broadcastSfxAtomic fcmd = do
Game/LambdaHack/Atomic/MonadStateWrite.hs view
@@ -124,71 +124,73 @@ updateFaction fid $ \fact -> fact {gsha = upd (gsha fact)} deleteItemContainer :: MonadStateWrite m- => ItemId -> Int -> Container -> m ()-deleteItemContainer iid k c = case c of- CFloor lid pos -> deleteItemFloor iid k lid pos- CEmbed lid pos -> deleteItemEmbed iid k lid pos- CActor aid store -> deleteItemActor iid k aid store- CTrunk{} -> return ()+ => ItemId -> ItemQuant -> Container -> m ()+deleteItemContainer iid kit c = case c of+ CFloor lid pos -> deleteItemFloor iid kit lid pos+ CEmbed lid pos -> deleteItemEmbed iid kit lid pos+ CActor aid store -> deleteItemActor iid kit aid store+ CTrunk{} -> assert `failure` c deleteItemFloor :: MonadStateWrite m- => ItemId -> Int -> LevelId -> Point -> m ()-deleteItemFloor iid k lid pos =+ => ItemId -> ItemQuant -> LevelId -> Point -> m ()+deleteItemFloor iid kit lid pos = let rmFromFloor (Just bag) =- let nbag = rmFromBag k iid bag+ let nbag = rmFromBag kit iid bag in if EM.null nbag then Nothing else Just nbag rmFromFloor Nothing = assert `failure` "item already removed"- `twith` (iid, k, lid, pos)+ `twith` (iid, kit, lid, pos) in updateLevel lid $ updateFloor $ EM.alter rmFromFloor pos deleteItemEmbed :: MonadStateWrite m- => ItemId -> Int -> LevelId -> Point -> m ()-deleteItemEmbed iid k lid pos =+ => ItemId -> ItemQuant -> LevelId -> Point -> m ()+deleteItemEmbed iid kit lid pos = let rmFromFloor (Just bag) =- let nbag = rmFromBag k iid bag+ let nbag = rmFromBag kit iid bag in if EM.null nbag then Nothing else Just nbag rmFromFloor Nothing = assert `failure` "item already removed"- `twith` (iid, k, lid, pos)+ `twith` (iid, kit, lid, pos) in updateLevel lid $ updateEmbed $ EM.alter rmFromFloor pos deleteItemActor :: MonadStateWrite m- => ItemId -> Int -> ActorId -> CStore -> m ()-deleteItemActor iid k aid cstore = case cstore of+ => ItemId -> ItemQuant -> ActorId -> CStore -> m ()+deleteItemActor iid kit aid cstore = case cstore of CGround -> do b <- getsState $ getActorBody aid- deleteItemFloor iid k (blid b) (bpos b)- COrgan -> deleteItemBody iid k aid- CEqp -> deleteItemEqp iid k aid- CInv -> deleteItemInv iid k aid+ deleteItemFloor iid kit (blid b) (bpos b)+ COrgan -> deleteItemBody iid kit aid+ CEqp -> deleteItemEqp iid kit aid+ CInv -> deleteItemInv iid kit aid CSha -> do b <- getsState $ getActorBody aid- deleteItemSha iid k (bfid b)+ deleteItemSha iid kit (bfid b) -deleteItemBody :: MonadStateWrite m => ItemId -> Int -> ActorId -> m ()-deleteItemBody iid k aid = do- updateActor aid $ \b -> b {borgan = rmFromBag k iid (borgan b) }+deleteItemBody :: MonadStateWrite m => ItemId -> ItemQuant -> ActorId -> m ()+deleteItemBody iid kit aid =+ updateActor aid $ \b -> b {borgan = rmFromBag kit iid (borgan b) } -deleteItemEqp :: MonadStateWrite m => ItemId -> Int -> ActorId -> m ()-deleteItemEqp iid k aid = do- updateActor aid $ \b -> b {beqp = rmFromBag k iid (beqp b)}+deleteItemEqp :: MonadStateWrite m => ItemId -> ItemQuant -> ActorId -> m ()+deleteItemEqp iid kit aid =+ updateActor aid $ \b -> b {beqp = rmFromBag kit iid (beqp b)} -deleteItemInv :: MonadStateWrite m => ItemId -> Int -> ActorId -> m ()-deleteItemInv iid k aid = do- updateActor aid $ \b -> b {binv = rmFromBag k iid (binv b)}+deleteItemInv :: MonadStateWrite m => ItemId -> ItemQuant -> ActorId -> m ()+deleteItemInv iid kit aid =+ updateActor aid $ \b -> b {binv = rmFromBag kit iid (binv b)} -deleteItemSha :: MonadStateWrite m => ItemId -> Int -> FactionId -> m ()-deleteItemSha iid k fid = do- updateFaction fid $ \fact -> fact {gsha = rmFromBag k iid (gsha fact)}+deleteItemSha :: MonadStateWrite m => ItemId -> ItemQuant -> FactionId -> m ()+deleteItemSha iid kit fid =+ updateFaction fid $ \fact -> fact {gsha = rmFromBag kit iid (gsha fact)} -- Removing the part of the kit from the front of the list, -- so that @DestroyItem kit (CreateItem kit x) == x@.-rmFromBag :: Int -> ItemId -> ItemBag -> ItemBag-rmFromBag k iid bag =+rmFromBag :: ItemQuant -> ItemId -> ItemBag -> ItemBag+rmFromBag kit@(k, rmIt) iid bag = let rfb Nothing = assert `failure` "rm from empty slot" `twith` (k, iid, bag) rfb (Just (n, it)) = case compare n k of LT -> assert `failure` "rm more than there is"- `twith` (n, k, iid, bag)+ `twith` (n, kit, iid, bag) EQ -> Nothing- GT -> Just (n - k, drop k it)+ GT -> assert (rmIt == take k it+ `blame` (rmIt, take k it, n, kit, iid, bag))+ $ Just (n - k, drop k it) in EM.alter rfb iid bag
Game/LambdaHack/Atomic/PosAtomicRead.hs view
@@ -16,7 +16,6 @@ import Game.LambdaHack.Atomic.CmdAtomic import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState-import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.Kind as Kind@@ -28,13 +27,15 @@ import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile+import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.ModeKind -- All functions here that take an atomic action are executed -- in the state just before the action is executed. --- | The type representing visibility of actions to factions,--- based on the position of the action, etc.+-- | The type representing visibility of atomic commands to factions,+-- based on the position of the command, etc. Note that the server+-- sees and smells all positions. data PosAtomic = PosSight !LevelId ![Point] -- ^ whomever sees all the positions, notices | PosFidAndSight ![FactionId] !LevelId ![Point]@@ -47,11 +48,18 @@ | PosNone -- ^ never broadcasted, but sent manually deriving (Show, Eq) --- | Produce the positions where the action takes place.--- The goal of the mechanics: client should not get significantly--- more information by looking at the atomic commands he is able to see--- than by looking at the state changes they enact. E.g., @UpdDisplaceActor@--- in a black room, with one actor carrying a 0-radius light would not be+-- | Produce the positions where the atomic update takes place.+--+-- The goal of the mechanics is to ensure the commands don't carry+-- significantly more information than their corresponding state diffs would.+-- In other words, the atomic commands involving the positions seen by a client+-- should convey similar information as the client would get by directly+-- observing the changes the commands enact on the visible portion of server+-- game state. The client is then free to change its copy of game state+-- accordingly or not --- it only partially reflects reality anyway.+--+-- E.g., @UpdDisplaceActor@ in a black room,+-- with one actor carrying a 0-radius light would not be -- distinguishable by looking at the state (or the screen) from @UpdMoveActor@ -- of the illuminated actor, hence such @UpdDisplaceActor@ should not be -- observable, but @UpdMoveActor@ should be (or the former should be perceived@@ -99,7 +107,7 @@ UpdAgeActor aid _ -> singleAid aid UpdRefillHP aid _ -> singleAid aid UpdRefillCalm aid _ -> singleAid aid- UpdOldFidActor aid _ _ -> singleAid aid+ UpdFidImpressedActor aid _ _ -> singleAid aid UpdTrajectory aid _ _ -> singleAid aid UpdColorActor aid _ _ -> singleAid aid UpdQuitFaction{} -> return PosAll@@ -133,14 +141,14 @@ return $! PosSmell lid ps UpdTimeItem _ c _ _ -> singleContainer c UpdAgeGame _ _ -> return PosAll- UpdDiscover lid p _ _ _ -> return $! PosSight lid [p]- UpdCover lid p _ _ _ -> return $! PosSight lid [p]- UpdDiscoverKind lid p _ _ -> return $! PosSight lid [p]- UpdCoverKind lid p _ _ -> return $! PosSight lid [p]- UpdDiscoverSeed lid p _ _ -> return $! PosSight lid [p]- UpdCoverSeed lid p _ _ -> return $! PosSight lid [p]+ UpdDiscover c _ _ _ -> singleContainer c+ UpdCover c _ _ _ -> singleContainer c+ UpdDiscoverKind c _ _ -> singleContainer c+ UpdCoverKind c _ _ -> singleContainer c+ UpdDiscoverSeed c _ _ -> singleContainer c+ UpdCoverSeed c _ _ -> singleContainer c UpdPerception{} -> return PosNone- UpdRestart fid _ _ _ _ _ -> return $! PosFid fid+ UpdRestart fid _ _ _ _ -> return $! PosFid fid UpdRestartServer _ -> return PosSer UpdResume fid _ -> return $! PosFid fid UpdResumeServer _ -> return PosSer@@ -152,18 +160,22 @@ -- | Produce the positions where the atomic special effect takes place. posSfxAtomic :: MonadStateRead m => SfxAtomic -> m PosAtomic posSfxAtomic cmd = case cmd of- SfxStrike source target _ _ -> do+ SfxStrike _ _ _ CSha _ -> -- shared stash is private+ return PosNone -- TODO: PosSerAndFidIfSight; but probably never used+ SfxStrike source target _ _ _ -> do (slid, sp) <- posOfAid source (tlid, tp) <- posOfAid target return $! assert (slid == tlid) $ PosSight slid [sp, tp]- SfxRecoil source target _ _ -> do+ SfxRecoil _ _ _ CSha _ -> -- shared stash is private+ return PosNone -- TODO: PosSerAndFidIfSight; but probably never used+ SfxRecoil source target _ _ _ -> do (slid, sp) <- posOfAid source (tlid, tp) <- posOfAid target return $! assert (slid == tlid) $ PosSight slid [sp, tp]- SfxProject aid _ _ -> singleAid aid- SfxCatch aid _ _ -> singleAid aid- SfxActivate aid _ _ -> singleAid aid- SfxCheck aid _ _ -> singleAid aid+ SfxProject aid _ cstore -> singleContainer $ CActor aid cstore+ SfxCatch aid _ cstore -> singleContainer $ CActor aid cstore+ SfxApply aid _ cstore -> singleContainer $ CActor aid cstore+ SfxCheck aid _ cstore -> singleContainer $ CActor aid cstore SfxTrigger aid p _ -> do (lid, pa) <- posOfAid aid return $! PosSight lid [pa, p]@@ -273,7 +285,7 @@ -- | Decompose an atomic special effect. breakSfxAtomic :: MonadStateRead m => SfxAtomic -> m [SfxAtomic] breakSfxAtomic cmd = case cmd of- SfxStrike source target _ _ -> do+ SfxStrike source target _ _ _ -> do -- Hack: make a fight detectable even if one of combatants not visible. sb <- getsState $ getActorBody source return $! [ SfxEffect (bfid sb) source (IK.RefillCalm (-1))@@ -302,6 +314,8 @@ , MU.AW $ MU.Phrase $ distant ++ [sound] ] return $! hear <$> msound +-- | Given the client, it's perception and an atomic command, determine+-- if the client notices the command. seenAtomicCli :: Bool -> FactionId -> Perception -> PosAtomic -> Bool seenAtomicCli knowEvents fid per posAtomic = case posAtomic of@@ -322,10 +336,11 @@ PosNone -> False _ -> True +-- | Generate the atomic updates that jointly perform a given item move. generalMoveItem :: MonadStateRead m => ItemId -> Int -> Container -> Container -> m [UpdAtomic]-generalMoveItem iid k c1 c2 = do+generalMoveItem iid k c1 c2 = case (c1, c2) of (CActor aid1 cstore1, CActor aid2 cstore2) | aid1 == aid2 -> return [UpdMoveItem iid k aid1 cstore1 cstore2]
Game/LambdaHack/Client.hs view
@@ -4,43 +4,11 @@ -- See -- <https://github.com/LambdaHack/LambdaHack/wiki/Client-server-architecture>. module Game.LambdaHack.Client- ( exeFrontend+ ( -- * Re-exported from "Game.LambdaHack.Client.LoopClient"+ loopAI, loopUI+ -- * Re-exported from "Game.LambdaHack.Client.UI"+ , srtFrontend ) where -import Game.LambdaHack.Atomic import Game.LambdaHack.Client.LoopClient-import Game.LambdaHack.Client.ProtocolClient-import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI-import Game.LambdaHack.Common.ClientOptions-import Game.LambdaHack.Common.Faction-import qualified Game.LambdaHack.Common.Kind as Kind-import Game.LambdaHack.Common.Request-import Game.LambdaHack.Common.Response-import Game.LambdaHack.Common.State---- | Wire together game content, the main loop of game clients,--- the main game loop assigned to this frontend (possibly containing--- the server loop, if the whole game runs in one process),--- UI config and the definitions of game commands.-exeFrontend :: ( MonadAtomic m, MonadClientUI m- , MonadClientReadResponse ResponseUI m- , MonadClientWriteRequest RequestUI m- , MonadAtomic n- , MonadClientReadResponse ResponseAI n- , MonadClientWriteRequest RequestAI n )- => (m () -> SessionUI -> State -> StateClient- -> chanServerUI- -> IO ())- -> (n () -> SessionUI -> State -> StateClient- -> chanServerAI- -> IO ())- -> KeyKind -> Kind.COps -> DebugModeCli- -> ((FactionId -> chanServerUI -> IO ())- -> (FactionId -> chanServerAI -> IO ())- -> IO ())- -> IO ()-exeFrontend executorUI executorAI copsClient cops sdebugCli exeServer =- srtFrontend (executorUI . loopUI)- (executorAI . loopAI)- copsClient cops sdebugCli exeServer
Game/LambdaHack/Client/AI.hs view
@@ -2,9 +2,9 @@ -- | Ways for the client to use AI to produce server requests, based on -- the client's view of the game state. module Game.LambdaHack.Client.AI- ( -- * Public API- queryAI, pongAI+ ( queryAI, pongAI #ifdef EXPOSE_INTERNAL+ -- * Internal operations , refreshTarget, pickAction #endif ) where@@ -51,16 +51,20 @@ pongAI :: MonadClient m => m RequestAI pongAI = return ReqAIPong +-- | Verify and possibly change the target of an actor. This function both+-- updates the target in the client state and returns the new target explicitly. refreshTarget :: MonadClient m- => ActorId -> (ActorId, Actor)- -> m (Maybe ((ActorId, Actor), (Target, PathEtc)))-refreshTarget oldLeader (aid, body) = do+ => (ActorId, Actor) -- ^ the actor to refresh+ -> m (Maybe (Target, PathEtc))+refreshTarget (aid, body) = do side <- getsClient sside- assert (bfid body == side `blame` "AI tries to move an enemy actor"- `twith` (aid, body, side)) skip- assert (not (bproj body) `blame` "AI gets to manually move its projectiles"- `twith` (aid, body, side)) skip- stratTarget <- targetStrategy oldLeader aid+ let !_A = assert (bfid body == side+ `blame` "AI tries to move an enemy actor"+ `twith` (aid, body, side)) ()+ let !_A = assert (not (bproj body)+ `blame` "AI gets to manually move its projectiles"+ `twith` (aid, body, side)) ()+ stratTarget <- targetStrategy aid tgtMPath <- if nullStrategy stratTarget then -- No sensible target; wipe out the old one.@@ -79,18 +83,21 @@ <> "\nHandleAI target:" <+> tshow tgtMPath -- trace _debug skip modifyClient $ \cli ->- cli {stargetD = EM.alter (const $ tgtMPath) aid (stargetD cli)}+ cli {stargetD = EM.alter (const tgtMPath) aid (stargetD cli)} return $! case tgtMPath of- Just (tgt, Just pathEtc) -> Just ((aid, body), (tgt, pathEtc))+ Just (tgt, Just pathEtc) -> Just (tgt, pathEtc) _ -> Nothing +-- | Pick an action the actor will perfrom this turn. pickAction :: MonadClient m => (ActorId, Actor) -> m RequestAnyAbility pickAction (aid, body) = do side <- getsClient sside- assert (bfid body == side `blame` "AI tries to move enemy actor"- `twith` (aid, bfid body, side)) skip- assert (not (bproj body) `blame` "AI gets to manually move its projectiles"- `twith` (aid, bfid body, side)) skip+ let !_A = assert (bfid body == side+ `blame` "AI tries to move enemy actor"+ `twith` (aid, bfid body, side)) ()+ let !_A = assert (not (bproj body)+ `blame` "AI gets to manually move its projectiles"+ `twith` (aid, bfid body, side)) () stratAction <- actionStrategy aid -- Run the AI: chose an action from those given by the AI strategy. rndToAction $ frequency $ bestVariant stratAction
Game/LambdaHack/Client/AI/ConditionClient.hs view
@@ -3,19 +3,24 @@ module Game.LambdaHack.Client.AI.ConditionClient ( condTgtEnemyPresentM , condTgtEnemyRememberedM+ , condTgtEnemyAdjFriendM+ , condTgtNonmovingM , condAnyFoeAdjM , condHpTooLowM , condOnTriggerableM , condBlocksFriendsM , condFloorWeaponM , condNoEqpWeaponM+ , condEnoughGearM , condCanProjectM , condNotCalmEnoughM , condDesirableFloorItemM , condMeleeBadM , condLightBetraysM , benAvailableItems+ , hinders , benGroundItems+ , desirableItem , threatDistList , fleeList ) where@@ -47,8 +52,10 @@ import Game.LambdaHack.Common.Request import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile+import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Vector import qualified Game.LambdaHack.Content.ItemKind as IK+import Game.LambdaHack.Content.ModeKind -- | Require that the target enemy is visible by the party. condTgtEnemyPresentM :: MonadClient m => ActorId -> m Bool@@ -67,6 +74,31 @@ Just (TEnemyPos _ lid _ permit) | lid == blid b -> not permit _ -> False +-- | Require that the target enemy is adjacent to at least one friend.+condTgtEnemyAdjFriendM :: MonadClient m => ActorId -> m Bool+condTgtEnemyAdjFriendM aid = do+ btarget <- getsClient $ getTarget aid+ case btarget of+ Just (TEnemy enemy _) -> do+ be <- getsState $ getActorBody enemy+ b <- getsState $ getActorBody aid+ fact <- getsState $ (EM.! bfid b) . sfactionD+ let friendlyFid fid = fid == bfid b || isAllied fact fid+ friends <- getsState $ actorRegularList friendlyFid (blid b)+ return $ any (adjacent (bpos be) . bpos) friends -- keep it lazy+ _ -> return False++-- | Check if the target is nonmoving.+condTgtNonmovingM :: MonadClient m => ActorId -> m Bool+condTgtNonmovingM aid = do+ btarget <- getsClient $ getTarget aid+ case btarget of+ Just (TEnemy enemy _) -> do+ activeItems <- activeItemsClient enemy+ let actorMaxSkE = sumSkills activeItems+ return $! EM.findWithDefault 0 Ability.AbMove actorMaxSkE <= 0+ _ -> return False+ -- | Require that any non-dying foe is adjacent. condAnyFoeAdjM :: MonadStateRead m => ActorId -> m Bool condAnyFoeAdjM aid = do@@ -102,7 +134,9 @@ allAtWar <- getsState $ actorRegularAssocs (isAtWar fact) (blid b) let strongActor (aid2, b2) = do activeItems <- activeItemsClient aid2- return $! not $ hpTooLow b2 activeItems+ let actorMaxSkE = sumSkills activeItems+ nonmoving = EM.findWithDefault 0 Ability.AbMove actorMaxSkE <= 0+ return $! not $ (hpTooLow b2 activeItems || nonmoving) allThreats <- filterM strongActor allAtWar let addDist (aid2, b2) = (chessDist (bpos b) (bpos b2), (aid2, b2)) return $ sortBy (comparing fst) $ map addDist allThreats@@ -119,28 +153,37 @@ _ -> False return $ any blocked ours -- keep it lazy --- | Require the actor stands over a weapon.+-- | Require the actor stands over a weapon that would be auto-equipped. condFloorWeaponM :: MonadClient m => ActorId -> m Bool condFloorWeaponM aid = do floorAssocs <- fullAssocsClient aid [CGround]- -- We do consider OFF weapons, because e.g., enemies might have turned- -- them off or they can be wrong for other party members, but are OK for us.- let lootIsWeapon =- not $ null $ strongestSlot IK.EqpSlotWeapon floorAssocs- return $ lootIsWeapon -- keep it lazy+ let lootIsWeapon = any (isMeleeEqp . snd) floorAssocs+ return lootIsWeapon -- keep it lazy -- | Check whether the actor has no weapon in equipment. condNoEqpWeaponM :: MonadClient m => ActorId -> m Bool condNoEqpWeaponM aid = do allAssocs <- fullAssocsClient aid [CEqp]- -- We do not consider OFF weapons, because they apparently are not good.- return $ null $ strongestSlot IK.EqpSlotWeapon allAssocs+ return $ all (not . isMelee . snd) allAssocs -- keep it lazy++-- | Check whether the actor has enough gear to go look for enemies.+condEnoughGearM :: MonadClient m => ActorId -> m Bool+condEnoughGearM aid = do+ eqpAssocs <- fullAssocsClient aid [CEqp]+ invAssocs <- fullAssocsClient aid [CInv]+ return $ any (isMelee . snd) eqpAssocs+ || length (eqpAssocs ++ invAssocs) >= 5 -- keep it lazy -- | Require that the actor can project any items.-condCanProjectM :: MonadClient m => ActorId -> m Bool-condCanProjectM aid = do- actorSk <- actorSkillsClient aid+condCanProjectM :: MonadClient m => Bool -> ActorId -> m Bool+condCanProjectM maxSkills aid = do+ actorSk <- if maxSkills+ then do+ activeItems <- activeItemsClient aid+ return $! sumSkills activeItems+ else+ actorSkillsClient aid let skill = EM.findWithDefault 0 Ability.AbProject actorSk q _ itemFull b activeItems = either (const False) id@@ -164,12 +207,20 @@ b <- getsState $ getActorBody aid activeItems <- activeItemsClient aid fact <- getsState $ (EM.! bfid b) . sfactionD+ condAnyFoeAdj <- condAnyFoeAdjM aid+ condLightBetrays <- condLightBetraysM aid+ condTgtEnemyPresent <- condTgtEnemyPresentM aid+ condNotCalmEnough <- condNotCalmEnoughM aid let ben cstore bag = [ ((benefit, (k, cstore)), (iid, itemFull)) | (iid, kit@(k, _)) <- EM.assocs bag , let itemFull = itemToF iid kit- , let benefit = totalUsefulness cops b activeItems fact itemFull- , permitted (fst <$> benefit) itemFull b activeItems ]+ benefit = totalUsefulness cops b activeItems fact itemFull+ hind = hinders condAnyFoeAdj condLightBetrays+ condTgtEnemyPresent condNotCalmEnough+ b activeItems itemFull+ , permitted (fst <$> benefit) itemFull b activeItems+ && (cstore /= CEqp || hind) ] benCStore cs = do bag <- getsState $ getActorBag aid cs return $! ben cs bag@@ -177,6 +228,37 @@ return $ concat perBag -- keep it lazy +-- TODO: also take into account dynamic lights *not* wielded by the actor+hinders :: Bool -> Bool -> Bool -> Bool -> Actor -> [ItemFull] -> ItemFull+ -> Bool+hinders condAnyFoeAdj condLightBetrays condTgtEnemyPresent+ condNotCalmEnough -- perhaps enemies don't have projectiles+ body activeItems itemFull =+ let itemLit = isJust $ strengthFromEqpSlot IK.EqpSlotAddLight itemFull+ in -- Fast actors want to hide in darkness to ambush opponents and want+ -- to hit hard for the short span they get to survive melee.+ bspeed body activeItems > speedNormal+ && (condNotCalmEnough && itemLit+ || 0 > fromMaybe 0 (strengthFromEqpSlot IK.EqpSlotAddHurtMelee+ itemFull)+ || 0 > fromMaybe 0 (strengthFromEqpSlot IK.EqpSlotAddHurtRanged+ itemFull))+ -- In the presence of enemies (seen, or unseen but distressing)+ -- actors want to hide in the dark.+ || let heavilyDistressed = -- actor hit by a proj or similarly distressed+ deltaSerious (bcalmDelta body)+ in condNotCalmEnough+ && (heavilyDistressed || condTgtEnemyPresent)+ && condLightBetrays && not condAnyFoeAdj+ && itemLit+ -- TODO:+ -- teach AI to turn shields OFF (or stash) when ganging up on an enemy+ -- (friends close, only one enemy close)+ -- and turning on afterwards (AI plays for time, especially spawners+ -- so shields are preferable by default;+ -- also, turning on when no friends and enemies close is too late,+ -- AI should flee or fire at such times, not muck around with eqp)+ -- | Require the actor is not calm enough. condNotCalmEnoughM :: MonadClient m => ActorId -> m Bool condNotCalmEnoughM aid = do@@ -199,35 +281,73 @@ benGroundItems aid = do b <- getsState $ getActorBody aid canEscape <- factionCanEscape (bfid b)- let -- A hack to prevent monsters from picking up treasure.- preciousWithoutSlot item =- IK.Precious `elem` jfeature item -- risk from treasure hunters- && isNothing (strengthEqpSlot item) -- unlikely to be useful- desirableItem use ItemFull{itemBase} _ _- | canEscape = use /= Just 0- || IK.Precious `elem` jfeature itemBase- | otherwise = use /= Just 0- && not (isNothing use -- needs resources to id- && preciousWithoutSlot itemBase)- benAvailableItems aid desirableItem [CGround]+ benAvailableItems aid (\use itemFull _ _ ->+ desirableItem canEscape use itemFull) [CGround] +desirableItem :: Bool -> Maybe Int -> ItemFull -> Bool+desirableItem canEsc use itemFull =+ let item = itemBase itemFull+ freq = case itemDisco itemFull of+ Nothing -> []+ Just ItemDisco{itemKind} -> IK.ifreq itemKind+ in if canEsc+ then use /= Just 0+ || IK.Precious `elem` jfeature item+ else+ -- A hack to prevent monsters from picking up unidentified treasure.+ let preciousWithoutSlot =+ IK.Precious `elem` jfeature item -- risk from treasure hunters+ && isNothing (strengthEqpSlot item) -- unlikely to be useful+ in use /= Just 0+ && not (isNothing use -- needs resources to id+ && preciousWithoutSlot)+ -- TODO: terrible hack for the identified healing gems and normal+ -- gems identified with a scroll+ && maybe True (<= 0) (lookup "gem" freq)+ -- | Require the actor is in a bad position to melee or can't melee at all. condMeleeBadM :: MonadClient m => ActorId -> m Bool condMeleeBadM aid = do b <- getsState $ getActorBody aid+ btarget <- getsClient $ getTarget aid+ mtgtPos <- aidTgtToPos aid (blid b) btarget+ condTgtEnemyPresent <- condTgtEnemyPresentM aid+ condTgtEnemyRemembered <- condTgtEnemyRememberedM aid fact <- getsState $ (EM.! bfid b) . sfactionD- condNoUsableWeapon <- null <$> pickWeaponClient aid aid- let friendlyFid fid = fid == bfid b || isAllied fact fid+ activeItems <- activeItemsClient aid+ let condNoUsableWeapon = all (not . isMelee) activeItems+ friendlyFid fid = fid == bfid b || isAllied fact fid friends <- getsState $ actorRegularAssocs friendlyFid (blid b)- let closeEnough b2 = let dist = chessDist (bpos b) (bpos b2)- in dist < 3 && dist > 0+ friendlyFacts <-+ getsState $ map snd . filter (friendlyFid . fst) . EM.assocs . sfactionD+ Level{lactorFreq} <- getLevel $ blid b+ let freqNames = map fst lactorFreq+ factNames = map (fgroup . gplayer) friendlyFacts+ spawnerOnLvl = any (`elem` freqNames) factNames+ closeEnough b2 = let dist = chessDist (bpos b) (bpos b2)+ in dist > 0 && (dist <= 2 || approaching b2)+ -- 3 is the condThreatAtHand distance that AI keeps when alone.+ approaching = case mtgtPos of+ Just tgtPos | condTgtEnemyPresent || condTgtEnemyRemembered ->+ \b1 -> chessDist (bpos b1) tgtPos <= 3+ _ -> const False closeFriends = filter (closeEnough . snd) friends strongActor (aid2, b2) = do- activeItems <- activeItemsClient aid2- return $! not $ hpTooLow b2 activeItems+ activeItems2 <- activeItemsClient aid2+ let condUsableWeapon2 = any isMelee activeItems2+ actorMaxSk2 = sumSkills activeItems2+ canMelee2 = EM.findWithDefault 0 Ability.AbMelee actorMaxSk2 > 0+ hpGood = not $ hpTooLow b2 activeItems2+ return $! hpGood && condUsableWeapon2 && canMelee2 strongCloseFriends <- filterM strongActor closeFriends- let noFriendlyHelp = length closeFriends < 3 && null strongCloseFriends+ let noFriendlyHelp = length closeFriends < 3+ && null strongCloseFriends+ && (length friends > 1 -- solo fighters aggresive+ || spawnerOnLvl)+ && not (hpHuge b) -- uniques, etc., aggresive+ let actorMaxSk = sumSkills activeItems return $ condNoUsableWeapon+ || EM.findWithDefault 0 Ability.AbMelee actorMaxSk <= 0 || noFriendlyHelp -- still not getting friends' help -- no $!; keep it lazy @@ -238,16 +358,18 @@ b <- getsState $ getActorBody aid eqpItems <- map snd <$> fullAssocsClient aid [CEqp] let actorEqpShines = sumSlotNoFilter IK.EqpSlotAddLight eqpItems > 0- aInAmbient<- getsState $ actorInAmbient b+ aInAmbient <- getsState $ actorInAmbient b return $! not aInAmbient -- tile is dark, so actor could hide && actorEqpShines -- but actor betrayed by his equipped light -- | Produce a list of acceptable adjacent points to flee to.-fleeList :: MonadClient m => Bool -> ActorId -> m [(Int, Point)]-fleeList panic aid = do+fleeList :: MonadClient m => ActorId -> m ([(Int, Point)], [(Int, Point)])+fleeList aid = do cops <- getsState scops mtgtMPath <- getsClient $ EM.lookup aid . stargetD let tgtPath = case mtgtMPath of -- prefer fleeing along the path to target+ Just (TEnemy{}, _) -> [] -- don't flee towards an enemy+ Just (TEnemyPos{}, _) -> [] Just (_, Just (_ : path, _)) -> path _ -> [] b <- getsState $ getActorBody aid@@ -261,15 +383,15 @@ | otherwise = minimum $ map (chessDist p) posFoes dVic = map (dist &&& id) myVic -- Flee, if possible. Access required.- accVic = filter (accessibleHere . snd) $ dVic+ accVic = filter (accessibleHere . snd) dVic gtVic = filter ((> dist (bpos b)) . fst) accVic- -- At least don't get closer to enemies, but don't stay adjacent.- eqVic = filter (\(d, _) -> d == dist (bpos b) && d > 1) accVic- rewardPath (d, p) =- if p `elem` tgtPath then Just (9 * d, p)- else if any (\q -> chessDist p q == 1) tgtPath then Just (d, p)- else Nothing- goodVic = mapMaybe rewardPath gtVic- ++ filter ((`elem` tgtPath) . snd) eqVic- pathVic = goodVic ++ if panic then accVic \\ goodVic else []- return pathVic -- keep it lazy, until other conditions verify danger+ eqVic = filter ((== dist (bpos b)) . fst) accVic+ ltVic = filter ((< dist (bpos b)) . fst) accVic+ rewardPath mult (d, p)+ | p `elem` tgtPath = (100 * mult * d, p)+ | any (\q -> chessDist p q == 1) tgtPath = (10 * mult * d, p)+ | otherwise = (mult * d, p)+ goodVic = map (rewardPath 10000) gtVic+ ++ map (rewardPath 100) eqVic+ badVic = map (rewardPath 1) ltVic+ return (goodVic, badVic) -- keep it lazy
Game/LambdaHack/Client/AI/HandleAbilityClient.hs view
@@ -46,7 +46,6 @@ 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.TileKind as TK type ToAny a = Strategy (RequestTimed a) -> Strategy RequestAnyAbility@@ -59,49 +58,60 @@ actionStrategy :: forall m. MonadClient m => ActorId -> m (Strategy RequestAnyAbility) actionStrategy aid = do- Kind.COps{corule} <- getsState scops- let stdRuleset = Kind.stdRuleset corule- nearby = rnearby stdRuleset body <- getsState $ getActorBody aid activeItems <- activeItemsClient aid fact <- getsState $ (EM.! bfid body) . sfactionD condTgtEnemyPresent <- condTgtEnemyPresentM aid condTgtEnemyRemembered <- condTgtEnemyRememberedM aid+ condTgtEnemyAdjFriend <- condTgtEnemyAdjFriendM aid condAnyFoeAdj <- condAnyFoeAdjM aid threatDistL <- threatDistList aid condHpTooLow <- condHpTooLowM aid condOnTriggerable <- condOnTriggerableM aid condBlocksFriends <- condBlocksFriendsM aid condNoEqpWeapon <- condNoEqpWeaponM aid- condNoUsableWeapon <- null <$> pickWeaponClient aid aid+ let condNoUsableWeapon = all (not . isMelee) activeItems+ condEnoughGear <- condEnoughGearM aid condFloorWeapon <- condFloorWeaponM aid- condCanProject <- condCanProjectM aid+ condCanProject <- condCanProjectM False aid condNotCalmEnough <- condNotCalmEnoughM aid condDesirableFloorItem <- condDesirableFloorItemM aid condMeleeBad <- condMeleeBadM aid- fleeL <- fleeList False aid- panicFleeL <- fleeList True aid- let condThreatAdj = not $ null $ takeWhile ((== 1) . fst) threatDistL+ condTgtNonmoving <- condTgtNonmovingM aid+ aInAmbient <- getsState $ actorInAmbient body+ explored <- getsClient sexplored+ (fleeL, badVic) <- fleeList aid+ let lidExplored = ES.member (blid body) explored+ panicFleeL = fleeL ++ badVic+ actorShines = sumSlotNoFilter IK.EqpSlotAddLight activeItems > 0+ condThreatAdj = not $ null $ takeWhile ((== 1) . fst) threatDistL condThreatAtHand = not $ null $ takeWhile ((<= 2) . fst) threatDistL- condThreatNearby = not $ null $ takeWhile ((<= nearby) . fst) threatDistL+ condThreatNearby = not $ null $ takeWhile ((<= 9) . fst) threatDistL speed1_5 = speedScale (3%2) (bspeed body activeItems) condFastThreatAdj = any (\(_, (_, b)) -> bspeed b activeItems > speed1_5) $ takeWhile ((== 1) . fst) threatDistL- condCanFlee = not (null fleeL || condFastThreatAdj)- actorSk <- actorSkillsClient aid- let stratToFreq :: MonadStateRead m+ heavilyDistressed = -- actor hit by a proj or similarly distressed+ deltaSerious (bcalmDelta body)+ let actorMaxSk = sumSkills activeItems+ abInMaxSkill ab = EM.findWithDefault 0 ab actorMaxSk > 0+ stratToFreq :: MonadStateRead m => Int -> m (Strategy RequestAnyAbility) -> m (Frequency RequestAnyAbility) stratToFreq scale mstrat = do st <- mstrat- return $! scaleFreq scale $ bestVariant st -- TODO: flatten instead?+ return $! if scale == 0+ then mzero+ else scaleFreq scale $ bestVariant st -- TODO: flatten instead?+ -- Order matters within the list, because it's summed with .| after+ -- filtering. Also, the results of prefix, distant and suffix+ -- are summed with .| at the end. prefix, suffix :: [([Ability], m (Strategy RequestAnyAbility), Bool)] prefix =- [ ( [AbApply], (toAny :: ToAny AbApply)+ [ ( [AbApply], (toAny :: ToAny 'AbApply) <$> applyItem aid ApplyFirstAid , condHpTooLow && not condAnyFoeAdj && not condOnTriggerable ) -- don't block stairs, perhaps ascend- , ( [AbTrigger], (toAny :: ToAny AbTrigger)+ , ( [AbTrigger], (toAny :: ToAny 'AbTrigger) <$> trigger aid True -- flee via stairs, even if to wrong level -- may return via different stairs@@ -109,238 +119,320 @@ && ((condNotCalmEnough || condHpTooLow) && condThreatNearby && not condTgtEnemyPresent || condMeleeBad && condThreatAdj) )- , ( [AbMove]- , flee aid fleeL- , condMeleeBad && condThreatAdj && condCanFlee ) , ( [AbDisplace] , displaceFoe aid -- only swap with an enemy to expose him , condBlocksFriends && condAnyFoeAdj && not condOnTriggerable && not condDesirableFloorItem )- , ( [AbMoveItem], (toAny :: ToAny AbMoveItem)+ , ( [AbMoveItem], (toAny :: ToAny 'AbMoveItem) <$> pickup aid True- , condNoEqpWeapon && condFloorWeapon && not condHpTooLow )- , ( [AbMelee], (toAny :: ToAny AbMelee)+ , condNoEqpWeapon && condFloorWeapon && not condHpTooLow+ && abInMaxSkill AbMelee )+ , ( [AbMelee], (toAny :: ToAny 'AbMelee) <$> meleeBlocker aid -- only melee target or blocker , condAnyFoeAdj- || EM.findWithDefault 0 AbDisplace actorSk <= 0- -- melee friends, not displace+ || not (abInMaxSkill AbDisplace) -- melee friends, not displace && fleaderMode (gplayer fact) == LeaderNull -- not restrained- && (condTgtEnemyPresent || condTgtEnemyRemembered) ) -- excited- , ( [AbTrigger], (toAny :: ToAny AbTrigger)+ && condTgtEnemyPresent ) -- excited+ , ( [AbTrigger], (toAny :: ToAny 'AbTrigger) <$> trigger aid False- , condOnTriggerable && not condDesirableFloorItem )+ , condOnTriggerable && not condDesirableFloorItem+ && (lidExplored || condEnoughGear)+ && not condTgtEnemyPresent )+ , ( [AbMove]+ , flee aid fleeL+ , condMeleeBad && not condFastThreatAdj+ -- Don't keep fleeing if was just hit, unless can't melee at all.+ && not (heavilyDistressed+ && abInMaxSkill AbMelee+ && not condNoUsableWeapon)+ && condThreatAtHand ) , ( [AbDisplace] -- prevents some looping movement , displaceBlocker aid -- fires up only when path blocked , not condDesirableFloorItem )- , ( [AbMoveItem], (toAny :: ToAny AbMoveItem)+ , ( [AbMoveItem], (toAny :: ToAny 'AbMoveItem) <$> equipItems aid -- doesn't take long, very useful if safe -- only if calm enough, so high priority- , not condAnyFoeAdj && not condDesirableFloorItem ) ]+ , not (condAnyFoeAdj+ || condDesirableFloorItem+ || condNotCalmEnough) )+ ]+ -- Order doesn't matter, scaling does. distant :: [([Ability], m (Frequency RequestAnyAbility), Bool)] distant =- [ ( [AbProject] -- for high-value target, shoot even in melee- , stratToFreq 2 $ (toAny :: ToAny AbProject)+ [ ( [AbMoveItem]+ , stratToFreq 20000 $ (toAny :: ToAny 'AbMoveItem)+ <$> yieldUnneeded aid -- 20000 to unequip ASAP, unless is thrown+ , True )+ , ( [AbProject] -- for high-value target, shoot even in melee+ , stratToFreq 2 $ (toAny :: ToAny 'AbProject) <$> projectItem aid , condTgtEnemyPresent && condCanProject && not condOnTriggerable ) , ( [AbApply]- , stratToFreq 2 $ (toAny :: ToAny AbApply)- <$> applyItem aid ApplyAll -- use any option or scroll+ , stratToFreq 2 $ (toAny :: ToAny 'AbApply)+ <$> applyItem aid ApplyAll -- use any potion or scroll , (condTgtEnemyPresent || condThreatNearby) -- can affect enemies && not condOnTriggerable ) , ( [AbMove]- , stratToFreq (if not condTgtEnemyPresent || condMeleeBad- then 1+ , stratToFreq (if not condTgtEnemyPresent+ then 3 -- if enemy only remembered, investigate anyway+ else if condTgtNonmoving+ then 0+ else if condTgtEnemyAdjFriend+ then 1000 -- friends probably pummeled, go to help else 100)- $ chase aid True+ $ chase aid True (condMeleeBad && condThreatNearby+ && not aInAmbient && not actorShines) , (condTgtEnemyPresent || condTgtEnemyRemembered)- && not condDesirableFloorItem- && not condNoUsableWeapon ) ]+ && not (condDesirableFloorItem && not condThreatAtHand)+ && abInMaxSkill AbMelee+ && not condNoUsableWeapon )+ ]+ -- Order matters again. suffix =- [ ( [AbMoveItem], (toAny :: ToAny AbMoveItem)- <$> pickup aid False- , True ) -- unconditionally, e.g., to give to other party members- , ( [AbMove]- , flee aid fleeL- , condMeleeBad && (condNotCalmEnough && condThreatNearby- || condThreatAtHand)- && condCanFlee )- , ( [AbMelee], (toAny :: ToAny AbMelee)+ [ ( [AbMelee], (toAny :: ToAny 'AbMelee) <$> meleeAny aid -- avoid getting damaged for naught , condAnyFoeAdj ) , ( [AbMove]- -- TODO: forget old target (e.g., tile), to start shooting,- -- unless can't shoot, etc.- , flee aid panicFleeL -- panic mode; chasing would be pointless- , condMeleeBad && condThreatNearby && (condNotCalmEnough- || condThreatAtHand- || condNoUsableWeapon) )- , ( [AbMoveItem], (toAny :: ToAny AbMoveItem)- <$> unEquipItems aid -- late, because better to throw than unequip+ , flee aid panicFleeL -- ultimate panic mode, displaces foes+ , condAnyFoeAdj )+ , ( [AbMoveItem], (toAny :: ToAny 'AbMoveItem)+ <$> pickup aid False+ , not condThreatAtHand ) -- e.g., to give to other party members+ , ( [AbMoveItem], (toAny :: ToAny 'AbMoveItem)+ <$> unEquipItems aid -- late, because these items not bad , True ) , ( [AbMove]- , chase aid False- , True )- , ( [AbWait], (toAny :: ToAny AbWait)+ , chase aid True (condTgtEnemyPresent+ -- Don't keep hiding in darkness if hit right now,+ -- unless can't melee at all.+ && not (heavilyDistressed+ && abInMaxSkill AbMelee+ && not condNoUsableWeapon)+ && condMeleeBad && condThreatNearby+ && not aInAmbient && not actorShines)+ , not (condTgtNonmoving && condThreatAtHand) )++ -- TODO: unless tgt can't melee+ ]+ fallback =+ [ ( [AbWait], (toAny :: ToAny 'AbWait) <$> waitBlockNow -- Wait until friends sidestep; ensures strategy is never empty. -- TODO: try to switch leader away before that (we already -- switch him afterwards)- , True ) ]+ , True )+ ] -- TODO: don't msum not to evaluate until needed- abInSkill ab = EM.findWithDefault 0 ab actorSk > 0+ -- Check current, not maximal skills, since this can be a non-leader action.+ actorSk <- actorSkillsClient aid+ let abInSkill ab = EM.findWithDefault 0 ab actorSk > 0 checkAction :: ([Ability], m a, Bool) -> Bool- checkAction (abts, _, cond) = cond && all abInSkill abts+ checkAction (abts, _, cond) = all abInSkill abts && cond sumS abAction = do let as = filter checkAction abAction- strats <- sequence $ map (\(_, m, _) -> m) as+ strats <- mapM (\(_, m, _) -> m) as return $! msum strats sumF abFreq = do let as = filter checkAction abFreq- strats <- sequence $ map (\(_, m, _) -> m) as+ strats <- mapM (\(_, m, _) -> m) as return $! msum strats- combineDistant as = fmap liftFrequency $ sumF as+ combineDistant as = liftFrequency <$> sumF as sumPrefix <- sumS prefix comDistant <- combineDistant distant sumSuffix <- sumS suffix- return $! sumPrefix .| comDistant .| sumSuffix+ sumFallback <- sumS fallback+ return $! sumPrefix .| comDistant .| sumSuffix .| sumFallback -- | A strategy to always just wait.-waitBlockNow :: MonadClient m => m (Strategy (RequestTimed AbWait))+waitBlockNow :: MonadClient m => m (Strategy (RequestTimed 'AbWait)) waitBlockNow = return $! returN "wait" ReqWait pickup :: MonadClient m- => ActorId -> Bool -> m (Strategy (RequestTimed AbMoveItem))+ => ActorId -> Bool -> m (Strategy (RequestTimed 'AbMoveItem)) pickup aid onlyWeapon = do benItemL <- benGroundItems aid- let isWeapon (_, (_, itemFull)) =- maybe False ((== IK.EqpSlotWeapon) . fst)- $ strengthEqpSlot $ itemBase itemFull+ b <- getsState $ getActorBody aid+ activeItems <- activeItemsClient aid+ -- This calmE is outdated when one of the items increases max Calm+ -- (e.g., in pickup, which handles many items at once), but this is OK,+ -- the server accepts item movement based on calm at the start, not end+ -- or in the middle.+ -- The calmE is inaccurate also if an item not IDed, but that's intended+ -- and the server will ignore and warn (and content may avoid that,+ -- e.g., making all rings identified)+ let calmE = calmEnough b activeItems+ isWeapon (_, (_, itemFull)) = isMeleeEqp itemFull filterWeapon | onlyWeapon = filter isWeapon | otherwise = id- cmp ((Nothing, _), _) = 5 -- experimenting is fun- cmp ((Just (n, _), _), _) = abs n- -- Pick up the best desirable item, if any.- case reverse $ sortBy (comparing cmp) $ filterWeapon benItemL of- ((_, (k, _)), (iid, itemFull)) : _ -> do- b <- getsState $ getActorBody aid- -- TODO: instead of pickup to eqp and then move to inv, pickup to inv- let toCStore = if goesIntoInv (itemBase itemFull)- || eqpOverfull b k- then CInv- else CEqp- return $! returN "pickup" $ ReqMoveItem iid k CGround toCStore- [] -> return reject+ prepareOne (oldN, l4) ((_, (k, _)), (iid, itemFull)) =+ -- TODO: instead of pickup to eqp and then move to inv, pickup to inv+ let n = oldN + k+ (newN, toCStore)+ | calmE && goesIntoSha itemFull = (oldN, CSha)+ | goesIntoInv itemFull || eqpOverfull b n = (oldN, CInv)+ | otherwise = (n, CEqp)+ in (newN, (iid, k, CGround, toCStore) : l4)+ (_, prepared) = foldl' prepareOne (0, []) $ filterWeapon benItemL+ return $! if null prepared+ then reject+ else returN "pickup" $ ReqMoveItems prepared -equipItems :: MonadClient m => ActorId -> m (Strategy (RequestTimed AbMoveItem))+equipItems :: MonadClient m => ActorId -> m (Strategy (RequestTimed 'AbMoveItem)) equipItems aid = do- cops@Kind.COps{corule} <- getsState scops- let RuleKind{rsharedStash} = Kind.stdRuleset corule+ cops <- getsState scops body <- getsState $ getActorBody aid activeItems <- activeItemsClient aid+ let calmE = calmEnough body activeItems fact <- getsState $ (EM.! bfid body) . sfactionD eqpAssocs <- fullAssocsClient aid [CEqp] invAssocs <- fullAssocsClient aid [CInv] shaAssocs <- fullAssocsClient aid [CSha]+ condAnyFoeAdj <- condAnyFoeAdjM aid condLightBetrays <- condLightBetraysM aid- let improve :: CStore -> ([(Int, (ItemId, ItemFull))],- [(Int, (ItemId, ItemFull))])- -> Strategy (RequestTimed AbMoveItem)- improve fromCStore (bestInv, bestEqp) =- case (bestInv, bestEqp) of- ((_, (iidInv, _)) : _, []) | not (eqpOverfull body 1) ->- returN "wield any"- $ ReqMoveItem iidInv 1 fromCStore CEqp- ((vInv, (iidInv, _)) : _, (vEqp, _) : _) | not (eqpOverfull body 1)- && vInv > vEqp ->- returN "wield better"- $ ReqMoveItem iidInv 1 fromCStore CEqp- _ -> reject+ condTgtEnemyPresent <- condTgtEnemyPresentM aid+ let improve :: CStore+ -> (Int, [(ItemId, Int, CStore, CStore)])+ -> ( IK.EqpSlot+ , ( [(Int, (ItemId, ItemFull))]+ , [(Int, (ItemId, ItemFull))] ) )+ -> (Int, [(ItemId, Int, CStore, CStore)])+ improve fromCStore (oldN, l4) (slot, (bestInv, bestEqp)) =+ let n = 1 + oldN+ in case (bestInv, bestEqp) of+ ((_, (iidInv, _)) : _, []) | not (eqpOverfull body n) ->+ (n, (iidInv, 1, fromCStore, CEqp) : l4)+ ((vInv, (iidInv, _)) : _, (vEqp, _) : _)+ | not (eqpOverfull body n)+ && (vInv > vEqp || not (toShare slot)) ->+ (n, (iidInv, 1, fromCStore, CEqp) : l4)+ _ -> (oldN, l4) -- We filter out unneeded items. In particular, we ignore them in eqp -- when comparing to items we may want to equip. Anyway, the unneeded -- items should be removed in yieldUnneeded earlier or soon after. filterNeeded (_, itemFull) =- not $ unneeded cops condLightBetrays body activeItems fact itemFull+ not $ unneeded cops condAnyFoeAdj condLightBetrays+ condTgtEnemyPresent (not calmE)+ body activeItems fact itemFull bestThree = bestByEqpSlot (filter filterNeeded eqpAssocs) (filter filterNeeded invAssocs) (filter filterNeeded shaAssocs)- bEqpInv = msum $ map (improve CInv)- $ map (\(_, (eqp, inv, _)) -> (inv, eqp)) bestThree- bEqpSha = msum $ map (improve CSha)- $ map (\(_, (eqp, _, sha)) -> (sha, eqp)) bestThree- if nullStrategy bEqpInv- then if rsharedStash && calmEnough body activeItems- then return $! bEqpSha- else return reject- else return $! bEqpInv+ bEqpInv = foldl' (improve CInv) (0, [])+ $ map (\((slot, _), (eqp, inv, _)) ->+ (slot, (inv, eqp))) bestThree+ bEqpBoth | calmE =+ foldl' (improve CSha) bEqpInv+ $ map (\((slot, _), (eqp, _, sha)) ->+ (slot, (sha, eqp))) bestThree+ | otherwise = bEqpInv+ (_, prepared) = bEqpBoth+ return $! if null prepared+ then reject+ else returN "equipItems" $ ReqMoveItems prepared +toShare :: IK.EqpSlot -> Bool+toShare IK.EqpSlotPeriodic = False+toShare _ = True++yieldUnneeded :: MonadClient m+ => ActorId -> m (Strategy (RequestTimed 'AbMoveItem))+yieldUnneeded aid = do+ cops <- getsState scops+ body <- getsState $ getActorBody aid+ activeItems <- activeItemsClient aid+ let calmE = calmEnough body activeItems+ fact <- getsState $ (EM.! bfid body) . sfactionD+ eqpAssocs <- fullAssocsClient aid [CEqp]+ condAnyFoeAdj <- condAnyFoeAdjM aid+ condLightBetrays <- condLightBetraysM aid+ condTgtEnemyPresent <- condTgtEnemyPresentM aid+ -- Here AI hides from the human player the Ring of Speed And Bleeding,+ -- which is a bit harsh, but fair. However any subsequent such+ -- rings will not be picked up at all, so the human player+ -- doesn't lose much fun. Additionally, if AI learns alchemy later on,+ -- they can repair the ring, wield it, drop at death and it's+ -- in play again.+ let yieldSingleUnneeded (iidEqp, itemEqp) =+ let csha = if calmE then CSha else CInv+ in if harmful cops body activeItems fact itemEqp+ then [(iidEqp, itemK itemEqp, CEqp, CInv)]+ else if hinders condAnyFoeAdj condLightBetrays+ condTgtEnemyPresent (not calmE)+ body activeItems itemEqp+ then [(iidEqp, itemK itemEqp, CEqp, csha)]+ else []+ yieldAllUnneeded = concatMap yieldSingleUnneeded eqpAssocs+ return $! if null yieldAllUnneeded+ then reject+ else returN "yieldUnneeded" $ ReqMoveItems yieldAllUnneeded+ unEquipItems :: MonadClient m- => ActorId -> m (Strategy (RequestTimed AbMoveItem))+ => ActorId -> m (Strategy (RequestTimed 'AbMoveItem)) unEquipItems aid = do- cops@Kind.COps{corule} <- getsState scops- let RuleKind{rsharedStash} = Kind.stdRuleset corule+ cops <- getsState scops body <- getsState $ getActorBody aid activeItems <- activeItemsClient aid+ let calmE = calmEnough body activeItems fact <- getsState $ (EM.! bfid body) . sfactionD eqpAssocs <- fullAssocsClient aid [CEqp] invAssocs <- fullAssocsClient aid [CInv] shaAssocs <- fullAssocsClient aid [CSha]+ condAnyFoeAdj <- condAnyFoeAdjM aid condLightBetrays <- condLightBetraysM aid- let yieldSingleUnneeded (iidEqp, itemEqp) =- let csha = if rsharedStash && calmEnough body activeItems- then CSha- else CInv- in if harmful cops body activeItems fact itemEqp- then Just $ ReqMoveItem iidEqp (itemK itemEqp) CEqp CInv -- throw- else if hinders condLightBetrays body activeItems itemEqp- then Just $ ReqMoveItem iidEqp (itemK itemEqp) CEqp csha -- share- else Nothing- yieldUnneeded = mapMaybe yieldSingleUnneeded eqpAssocs- improve :: CStore -> ( IK.EqpSlot+ condTgtEnemyPresent <- condTgtEnemyPresentM aid+ -- Here AI hides from the human player the Ring of Speed And Bleeding,+ -- which is a bit harsh, but fair. However any subsequent such+ -- rings will not be picked up at all, so the human player+ -- doesn't lose much fun. Additionally, if AI learns alchemy later on,+ -- they can repair the ring, wield it, drop at death and it's+ -- in play again.+ let improve :: CStore -> ( IK.EqpSlot , ( [(Int, (ItemId, ItemFull))] , [(Int, (ItemId, ItemFull))] ) )- -> Strategy (RequestTimed AbMoveItem)- improve fromCStore (slot, (bestInv, bestEqp)) =- case (bestInv, bestEqp) of- _ | slot == IK.EqpSlotPeriodic+ -> [(ItemId, Int, CStore, CStore)]+ improve fromCStore (slot, (bestSha, bestEOrI)) =+ case (bestSha, bestEOrI) of+ _ | not (toShare slot) && fromCStore == CEqp- && not (eqpOverfull body 0) ->- -- Don't get rid of periodic items from eqp unless eqp full.- reject- (_, (vEqp, (iidEqp, _)) : _) | getK bestEqp > 1- && betterThanInv vEqp bestInv ->+ && not (eqpOverfull body 1) -> -- keep periodic items up to M-1+ []+ (_, (vEOrI, (iidEOrI, _)) : _) | (toShare slot || fromCStore == CInv)+ && getK bestEOrI > 1+ && betterThanSha vEOrI bestSha -> -- To share the best items with others, if they care.- returN "yield rest"- $ ReqMoveItem iidEqp (getK bestEqp - 1) fromCStore CSha- (_, _ : (vEqp, (iidEqp, _)) : _) | betterThanInv vEqp bestInv ->+ [(iidEOrI, getK bestEOrI - 1, fromCStore, CSha)]+ (_, _ : (vEOrI, (iidEOrI, _)) : _) | (toShare slot+ || fromCStore == CInv)+ && betterThanSha vEOrI bestSha -> -- To share the second best items with others, if they care.- returN "yield worse"- $ ReqMoveItem iidEqp (getK bestEqp) fromCStore CSha- _ -> reject+ [(iidEOrI, getK bestEOrI, fromCStore, CSha)]+ (_, (vEOrI, (_, _)) : _) | fromCStore == CEqp+ && eqpOverfull body 1+ && worseThanSha vEOrI bestSha ->+ -- To make place in eqp for an item better than any ours.+ [(fst $ snd $ last bestEOrI, 1, fromCStore, CSha)]+ _ -> [] getK [] = 0 getK ((_, (_, itemFull)) : _) = itemK itemFull- betterThanInv _ [] = True- betterThanInv vEqp ((vInv, _) : _) = vEqp > vInv- bestThree = bestByEqpSlot eqpAssocs invAssocs shaAssocs- bInvSha = msum $ map (improve CInv)+ betterThanSha _ [] = True+ betterThanSha vEOrI ((vSha, _) : _) = vEOrI > vSha+ worseThanSha _ [] = False+ worseThanSha vEOrI ((vSha, _) : _) = vEOrI < vSha+ filterNeeded (_, itemFull) =+ not $ unneeded cops condAnyFoeAdj condLightBetrays+ condTgtEnemyPresent (not calmE)+ body activeItems fact itemFull+ bestThree =+ bestByEqpSlot eqpAssocs invAssocs (filter filterNeeded shaAssocs)+ bInvSha = concatMap (improve CInv) $ map (\((slot, _), (_, inv, sha)) -> (slot, (sha, inv))) bestThree- bEqpSha = msum $ map (improve CEqp)+ bEqpSha = concatMap (improve CEqp) $ map (\((slot, _), (eqp, _, sha)) -> (slot, (sha, eqp))) bestThree- case yieldUnneeded of- [] -> if rsharedStash && calmEnough body activeItems- then if nullStrategy bInvSha- then return $! bEqpSha- else return $! bInvSha- else return reject- _ ->- -- Here AI hides from the human player the Ring of Speed And Bleeding,- -- which is a bit harsh, but fair. However any subsequent such- -- rings will not be picked up at all, so the human player- -- doesn't lose much fun. Additionally, if AI learns alchemy later on,- -- they can repair the ring, wield it, drop at death and it's- -- in play again.- return $! liftFrequency $ uniformFreq "yield unneeded" yieldUnneeded+ prepared = if calmE then bInvSha ++ bEqpSha else []+ return $! if null prepared+ then reject+ else returN "unEquipItems" $ ReqMoveItems prepared groupByEqpSlot :: [(ItemId, ItemFull)] -> M.Map (IK.EqpSlot, Text) [(ItemId, ItemFull)]@@ -364,35 +456,12 @@ shaMap = M.map (\g -> ([], [], g)) $ groupByEqpSlot shaAssocs appendThree (g1, g2, g3) (h1, h2, h3) = (g1 ++ h1, g2 ++ h2, g3 ++ h3) eqpInvShaMap = M.unionsWith appendThree [eqpMap, invMap, shaMap]- bestSingle eqpSlot g = strongestSlot eqpSlot g+ bestSingle = strongestSlot bestThree (eqpSlot, _) (g1, g2, g3) = (bestSingle eqpSlot g1, bestSingle eqpSlot g2, bestSingle eqpSlot g3) in M.assocs $ M.mapWithKey bestThree eqpInvShaMap -hinders :: Bool -> Actor -> [ItemFull] -> ItemFull -> Bool-hinders condLightBetrays body activeItems itemFull =- -- Fast actors want to hide in darkness to ambush opponents and want- -- to hit hard for the short span they get to survive melee.- (bspeed body activeItems > speedNormal- && (isJust (strengthFromEqpSlot IK.EqpSlotAddLight itemFull)- || 0 > fromMaybe 0 (strengthFromEqpSlot IK.EqpSlotAddHurtMelee- itemFull)- || 0 > fromMaybe 0 (strengthFromEqpSlot IK.EqpSlotAddHurtRanged- itemFull)))- -- Distressed actors want to hide in the dark.- || (let heavilyDistressed = -- actor hit by a proj or similarly distressed- deltaSerious (bcalmDelta body)- in condLightBetrays && heavilyDistressed- && isJust (strengthFromEqpSlot IK.EqpSlotAddLight itemFull))- -- TODO:- -- teach AI to turn shields OFF (or stash) when ganging up on an enemy- -- (friends close, only one enemy close)- -- and turning on afterwards (AI plays for time, especially spawners- -- so shields are preferable by default;- -- also, turning on when no friends and enemies close is too late,- -- AI should flee or fire at such times, not muck around with eqp)- harmful :: Kind.COps -> Actor -> [ItemFull] -> Faction -> ItemFull -> Bool harmful cops body activeItems fact itemFull = -- Items that are known and their effects are not stricly beneficial@@ -400,14 +469,22 @@ maybe False (\(u, _) -> u <= 0) (totalUsefulness cops body activeItems fact itemFull) -unneeded :: Kind.COps -> Bool -> Actor -> [ItemFull] -> Faction -> ItemFull+unneeded :: Kind.COps -> Bool -> Bool -> Bool -> Bool+ -> Actor -> [ItemFull] -> Faction -> ItemFull -> Bool-unneeded cops condLightBetrays body activeItems fact itemFull =+unneeded cops condAnyFoeAdj condLightBetrays+ condTgtEnemyPresent condNotCalmEnough+ body activeItems fact itemFull = harmful cops body activeItems fact itemFull- || hinders condLightBetrays body activeItems itemFull+ || hinders condAnyFoeAdj condLightBetrays+ condTgtEnemyPresent condNotCalmEnough+ body activeItems itemFull+ || let calm10 = calmEnough10 body activeItems -- unneeded risk+ itemLit = isJust $ strengthFromEqpSlot IK.EqpSlotAddLight itemFull+ in itemLit && not calm10 -- Everybody melees in a pinch, even though some prefer ranged attacks.-meleeBlocker :: MonadClient m => ActorId -> m (Strategy (RequestTimed AbMelee))+meleeBlocker :: MonadClient m => ActorId -> m (Strategy (RequestTimed 'AbMelee)) meleeBlocker aid = do b <- getsState $ getActorBody aid fact <- getsState $ (EM.! bfid b) . sfactionD@@ -417,9 +494,9 @@ Just (_, Just (_ : q : _, (goal, _))) -> do -- We prefer the goal (e.g., when no accessible, but adjacent), -- but accept @q@ even if it's only a blocking enemy position.- let maim = if adjacent (bpos b) goal then Just goal- else if adjacent (bpos b) q then Just q- else Nothing -- MeleeDistant+ let maim | adjacent (bpos b) goal = Just goal+ | adjacent (bpos b) q = Just q+ | otherwise = Nothing -- MeleeDistant mBlocker <- case maim of Nothing -> return Nothing Just aim -> getsState $ posToActor aim (blid b)@@ -436,7 +513,7 @@ && EM.findWithDefault 0 AbMove actorSk > 0 -- blocked move && bhp body2 < bhp b) -- respect power then do- mel <- pickWeaponClient aid aid2+ mel <- maybeToList <$> pickWeaponClient aid aid2 return $! liftFrequency $ uniformFreq "melee in the way" mel else return reject Nothing -> return reject@@ -444,7 +521,7 @@ -- Everybody melees in a pinch, skills and weapons allowing, -- even though some prefer ranged attacks.-meleeAny :: MonadClient m => ActorId -> m (Strategy (RequestTimed AbMelee))+meleeAny :: MonadClient m => ActorId -> m (Strategy (RequestTimed 'AbMelee)) meleeAny aid = do b <- getsState $ getActorBody aid fact <- getsState $ (EM.! bfid b) . sfactionD@@ -452,13 +529,20 @@ let adjFoes = filter (adjacent (bpos b) . bpos . snd) allFoes mels <- mapM (pickWeaponClient aid . fst) adjFoes -- TODO: prioritize somehow- let freq = uniformFreq "melee adjacent" $ concat mels+ let freq = uniformFreq "melee adjacent" $ catMaybes mels return $! liftFrequency freq -- TODO: take charging status into account--- Fast monsters don't pay enough attention to features.+-- TODO: make sure the stairs are specifically targetted and not+-- an item on them, etc., so that we don't leave level if items visible.+-- When invalidating target, make sure the stairs should really be taken.+-- | The level the actor is on is either explored or the actor already+-- has a weapon equipped, so no need to explore further, he tries to find+-- enemies on other levels.+-- We don't verify the stairs are targeted by the actor, but at least+-- the actor doesn't target a visible enemy at this point. trigger :: MonadClient m- => ActorId -> Bool -> m (Strategy (RequestTimed AbTrigger))+ => ActorId -> Bool -> m (Strategy (RequestTimed 'AbTrigger)) trigger aid fleeViaStairs = do cops@Kind.COps{cotile=Kind.Ops{okind}} <- getsState scops dungeon <- getsState sdungeon@@ -470,70 +554,69 @@ lvl <- getLevel lid unexploredD <- unexploredDepth s <- getState- per <- getPerFid lid- let canSee = ES.member (bpos b) (totalVisible per)- unexploredCurrent = ES.notMember lid explored+ let lidExplored = ES.member lid explored allExplored = ES.size explored == EM.size dungeon t = lvl `at` bpos b feats = TK.tfeature $ okind t ben feat = case feat of- TK.Cause (IK.Ascend k) -> -- change levels sensibly, in teams- let aimless = ftactic (gplayer fact) `elem` [TRoam, TPatrol]- easeDelta = abs (fromEnum lid) - abs (fromEnum lid + k)+ TK.Cause (IK.Ascend k) -> do -- change levels sensibly, in teams+ (lid2, pos2) <- getsState $ whereTo lid (bpos b) k . sdungeon+ per <- getPerFid lid2+ let canSee = ES.member (bpos b) (totalVisible per)+ aimless = ftactic (gplayer fact) `elem` [TRoam, TPatrol]+ easier = signum k /= signum (fromEnum lid) unexpForth = unexploredD (signum k) lid unexpBack = unexploredD (- signum k) lid- expBenefit =- if aimless- then 100 -- faction is not exploring, so switch at will- else if unexploredCurrent- then 0 -- don't leave the level until explored- else if unexpForth- then if easeDelta > 0 -- alway try as easy level as possible- || not unexpBack -- no other choice for exploration- then 1000 * abs easeDelta- else 0- else if unexpBack- then 0 -- wait for stairs in the opposite direciton- else if lescape lvl- then 0 -- all explored, stay on the escape level- else 2 -- no escape anywhere, switch levels occasionally- (lid2, pos2) = whereTo lid (bpos b) k dungeon+ expBenefit+ | aimless = 100 -- faction is not exploring, so switch at will+ | unexpForth =+ if easier -- alway try as easy level as possible+ || not unexpBack+ && lidExplored -- no other choice for exploration+ then 1000+ else 0+ | not lidExplored = 0 -- fully explore current+ | unexpBack = 0 -- wait for stairs in the opposite direciton+ | not $ null $ lescape lvl = 0+ -- all explored, stay on the escape level+ | otherwise = 2 -- no escape, switch levels occasionally actorsThere = posToActors pos2 lid2 s- in if boldpos b == bpos b -- probably used stairs last turn+ return $!+ if boldpos b == bpos b -- probably used stairs last turn && boldlid b == lid2 -- in the opposite direction then 0 -- avoid trivial loops (pushing, being pushed, etc.)- else let leaderless = fleaderMode (gplayer fact) == LeaderNull- eben = case actorsThere of+ else let eben = case actorsThere of [] | canSee -> expBenefit- _ | leaderless -> 0 -- leaderless clog stairs easily _ -> min 1 expBenefit -- risk pushing in if fleeViaStairs then 1000 * eben + 1 -- strongly prefer correct direction else eben- TK.Cause ef@IK.Escape{} -> do -- flee via this way, too+ TK.Cause ef@IK.Escape{} -> return $ -- flee via this way, too -- Only some factions try to escape but they first explore all -- for high score. if not (fcanEscape $ gplayer fact) || not allExplored then 0 else effectToBenefit cops b activeItems fact ef TK.Cause ef | not fleeViaStairs ->- effectToBenefit cops b activeItems fact ef- _ -> 0- benFeat = zip (map ben feats) feats+ return $! effectToBenefit cops b activeItems fact ef+ _ -> return 0+ benFeats <- mapM ben feats+ let benFeat = zip benFeats feats return $! liftFrequency $ toFreq "trigger"- $ [ (benefit, ReqTrigger (Just feat))- | (benefit, feat) <- benFeat- , benefit > 0 ]+ [ (benefit, ReqTrigger (Just feat))+ | (benefit, feat) <- benFeat+ , benefit > 0 ] -projectItem :: MonadClient m => ActorId -> m (Strategy (RequestTimed AbProject))+projectItem :: MonadClient m => ActorId -> m (Strategy (RequestTimed 'AbProject)) projectItem aid = do btarget <- getsClient $ getTarget aid- b@Actor{bpos} <- getsState $ getActorBody aid+ b <- getsState $ getActorBody aid mfpos <- aidTgtToPos aid (blid b) btarget seps <- getsClient seps case (btarget, mfpos) of+ (_, Just fpos) | chessDist (bpos b) fpos == 1 -> return reject (Just TEnemy{}, Just fpos) -> do- mnewEps <- makeLine b fpos seps+ mnewEps <- makeLine False b fpos seps case mnewEps of Just newEps -> do actorSk <- actorSkillsClient aid@@ -547,26 +630,23 @@ localTime <- getsState $ getLocalTime (blid b) let coeff CGround = 2 coeff COrgan = 3 -- can't give to others- coeff CEqp = 1+ coeff CEqp = 100000 -- must hinder currently coeff CInv = 1 coeff CSha = undefined -- banned- fRanged ((mben, (_, cstore)), (iid, itemFull@ItemFull{..})) =- let it1 = case strengthFromEqpSlot IK.EqpSlotTimeout- itemFull of- Nothing -> []- Just timeout ->- let timeoutTurns =- timeDeltaScale (Delta timeTurn) timeout- pending startT =- timeShift startT timeoutTurns > localTime- in filter pending itemTimer- len = length it1- recharged = len < itemK+ fRanged ( (mben, (_, cstore))+ , (iid, itemFull@ItemFull{itemBase}) ) =+ -- We assume if the item has a timeout, most effects are under+ -- Recharging, so no point projecting if not recharged.+ -- This is not an obvious assumption, so recharging is not+ -- included in permittedProject and can be tweaked here easily.+ let recharged = hasCharge localTime itemFull trange = totalRange itemBase- bestRange = chessDist bpos fpos + 2 -- margin for fleeing+ bestRange =+ chessDist (bpos b) fpos + 2 -- margin for fleeing rangeMult = -- penalize wasted or unsafely low range 10 + max 0 (10 - abs (trange - bestRange))- durableBonus = if IK.Durable `elem` jfeature itemBase+ durable = IK.Durable `elem` jfeature itemBase+ durableBonus = if durable then 2 -- we or foes keep it after the throw else 1 benR = durableBonus@@ -575,7 +655,10 @@ Nothing -> -1 -- experiment if no options Just (_, ben) -> ben * (if recharged then 1 else 0)- in if benR < 0 && trange >= chessDist bpos fpos+ in if -- Durable weapon is usually too useful for melee.+ not (isMeleeEqp itemFull)+ && benR < 0+ && trange >= chessDist (bpos b) fpos then Just ( -benR * rangeMult `div` 10 , ReqProject fpos newEps iid cstore ) else Nothing@@ -588,17 +671,17 @@ deriving Eq applyItem :: MonadClient m- => ActorId -> ApplyItemGroup -> m (Strategy (RequestTimed AbApply))+ => ActorId -> ApplyItemGroup -> m (Strategy (RequestTimed 'AbApply)) applyItem aid applyGroup = do actorSk <- actorSkillsClient aid+ b <- getsState $ getActorBody aid+ localTime <- getsState $ getLocalTime (blid b) let skill = EM.findWithDefault 0 AbProject actorSk- q _ itemFull b activeItems =+ q _ itemFull _ activeItems = either (const False) id- $ permittedApply " " skill itemFull b activeItems+ $ permittedApply " " localTime skill itemFull b activeItems benList <- benAvailableItems aid q [CEqp, CInv, CGround]- b <- getsState $ getActorBody aid organs <- mapM (getsState . getItemBody) $ EM.keys $ borgan b- localTime <- getsState $ getLocalTime (blid b) let itemLegal itemFull = case applyGroup of ApplyFirstAid -> let getP (IK.RefillHP p) _ | p > 0 = True@@ -611,19 +694,11 @@ ApplyAll -> True coeff CGround = 2 coeff COrgan = 3 -- can't give to others- coeff CEqp = 1+ coeff CEqp = 100000 -- must hinder currently coeff CInv = 1 coeff CSha = undefined -- banned- fTool ((mben, (_, cstore)), (iid, itemFull@ItemFull{..})) =- let it1 = case strengthFromEqpSlot IK.EqpSlotTimeout itemFull of- Nothing -> []- Just timeout ->- let timeoutTurns = timeDeltaScale (Delta timeTurn) timeout- pending startT = timeShift startT timeoutTurns > localTime- in filter pending itemTimer- len = length it1- recharged = len < itemK- durableBonus = if IK.Durable `elem` jfeature itemBase+ fTool ((mben, (_, cstore)), (iid, itemFull@ItemFull{itemBase})) =+ let durableBonus = if IK.Durable `elem` jfeature itemBase then 5 -- we keep it after use else 1 oldGrps = map (toGroupName . jname) organs@@ -641,23 +716,20 @@ -- common and easy to drop organ, etc. let newGrps = strengthDropOrgan itemFull hasDropOrgan = not $ null newGrps- in hasDropOrgan && null (intersect newGrps oldGrps)+ in hasDropOrgan && null (newGrps `intersect` oldGrps) benR = case mben of Nothing -> 0 -- experimenting is fun, but it's better to risk- -- foes' skin than ours -- TODO: when {activated}+ -- foes' skin than ours -- TODO: when {applied} -- is implemented, enable this for items too heavy, -- etc. for throwing Just (_, ben) -> ben- * (if recharged then 1 else 0) -- wait until can be used * (if not createOrganAgain then 1 else 0) * (if not dropOrganVoid then 1 else 0) * durableBonus * coeff cstore- in if itemLegal itemFull- then if benR > 0- then Just (benR, ReqApply iid cstore)- else Nothing+ in if itemLegal itemFull && benR > 0+ then Just (benR, ReqApply iid cstore) else Nothing benTool = mapMaybe fTool benList return $! liftFrequency $ toFreq "applyItem" benTool@@ -684,15 +756,15 @@ let friendlyFid fid = fid == bfid b || isAllied fact fid friends <- getsState $ actorRegularList friendlyFid (blid b) allFoes <- getsState $ actorRegularAssocs (isAtWar fact) (blid b)- let accessibleHere = accessible cops lvl $ bpos b -- DisplaceAccess- displaceable body = -- DisplaceAccess, DisplaceDying, DisplaceSupported- accessibleHere (bpos body)- && adjacent (bpos body) (bpos b)+ let accessibleHere = accessible cops lvl $ bpos b+ displaceable body = -- DisplaceAccess+ adjacent (bpos body) (bpos b) && accessibleHere (bpos body) nFriends body = length $ filter (adjacent (bpos body) . bpos) friends nFrHere = nFriends b + 1 qualifyActor (aid2, body2) = do activeItems <- activeItemsClient aid2 dEnemy <- getsState $ dispEnemy aid aid2 activeItems+ -- DisplaceDying, DisplaceBraced, DisplaceImmobile, DisplaceSupported let nFr = nFriends body2 return $! if displaceable body2 && dEnemy && nFr < nFrHere then Just (nFr * nFr, bpos body2 `vectorToFrom` bpos b)@@ -716,7 +788,7 @@ displaceTowards aid source target = do cops <- getsState scops b <- getsState $ getActorBody aid- assert (source == bpos b && adjacent source target) skip+ let !_A = assert (source == bpos b && adjacent source target) () lvl <- getLevel $ blid b if boldpos b /= target -- avoid trivial loops && accessible cops lvl source target then do -- DisplaceAccess@@ -728,10 +800,13 @@ mtgtMPath <- getsClient $ EM.lookup aid2 . stargetD case mtgtMPath of Just (tgt, Just (p : q : rest, (goal, len)))- | q == source && p == target -> do- let newTgt = Just (tgt, Just (q : rest, (goal, len - 1)))+ | q == source && p == target+ || waitedLastTurn b2 -> do+ let newTgt = if q == source && p == target+ then Just (tgt, Just (q : rest, (goal, len - 1)))+ else Nothing modifyClient $ \cli ->- cli {stargetD = EM.alter (const $ newTgt) aid (stargetD cli)}+ cli {stargetD = EM.alter (const newTgt) aid (stargetD cli)} return $! returN "displace friend" $ target `vectorToFrom` source Just _ -> return reject Nothing -> do@@ -740,17 +815,21 @@ dEnemy <- getsState $ dispEnemy aid aid2 activeItems if not (isAtWar tfact (bfid b)) || dEnemy then return $! returN "displace other" $ target `vectorToFrom` source- else return reject -- DisplaceDying, DisplaceSupported+ else return reject -- DisplaceDying, etc. _ -> return reject -- DisplaceProjectiles or trying to displace leader else return reject -chase :: MonadClient m => ActorId -> Bool -> m (Strategy RequestAnyAbility)-chase aid doDisplace = do+chase :: MonadClient m+ => ActorId -> Bool -> Bool -> m (Strategy RequestAnyAbility)+chase aid doDisplace avoidAmbient = do+ Kind.COps{cotile} <- getsState scops body <- getsState $ getActorBody aid fact <- getsState $ (EM.! bfid body) . sfactionD mtgtMPath <- getsClient $ EM.lookup aid . stargetD+ lvl <- getLevel $ blid body+ let isAmbient pos = Tile.isLit cotile (lvl `at` pos) str <- case mtgtMPath of- Just (_, Just (p : q : _, (goal, _))) ->+ Just (_, Just (p : q : _, (goal, _))) | not $ avoidAmbient && isAmbient q -> -- With no leader, the goal is vague, so permit arbitrary detours. moveTowards aid p q goal (fleaderMode (gplayer fact) == LeaderNull) _ -> return reject -- goal reached@@ -765,7 +844,10 @@ moveTowards aid source target goal relaxed = do cops@Kind.COps{cotile} <- getsState scops b <- getsState $ getActorBody aid- assert (source == bpos b && adjacent source target) skip+ let !_A = assert (source == bpos b+ `blame` (source, bpos b, aid, b, goal)) ()+ !_B = assert (adjacent source target+ `blame` (source, target, aid, b, goal)) () lvl <- getLevel $ blid b fact <- getsState $ (EM.! bfid b) . sfactionD friends <- getsState $ actorList (not . isAtWar fact) $ blid b@@ -816,26 +898,26 @@ tfact <- getsState $ (EM.! bfid b2) . sfactionD activeItems <- activeItemsClient target dEnemy <- getsState $ dispEnemy source target activeItems- if boldpos sb /= tpos -- avoid trivial Displace loops- && accessible cops lvl spos tpos -- DisplaceAccess- && (not (isAtWar tfact (bfid sb))- || dEnemy) -- DisplaceDying, DisplaceSupported- then- return $! Just $ RequestAnyAbility $ ReqDisplace target- else do+ if boldpos sb == tpos && not (waitedLastTurn sb) -- avoid Displace loops+ || not (accessible cops lvl spos tpos) -- DisplaceAccess+ then return Nothing+ else if isAtWar tfact (bfid sb) && not dEnemy -- DisplaceDying, etc.+ then do wps <- pickWeaponClient source target case wps of- [] -> return Nothing- wp : _ -> return $! Just $ RequestAnyAbility wp+ Nothing -> return Nothing+ Just wp -> return $! Just $ RequestAnyAbility wp+ else do+ return $! Just $ RequestAnyAbility $ ReqDisplace target ((target, _), _) : _ -> do -- can be a foe, as well as friend (e.g., proj.) -- No problem if there are many projectiles at the spot. We just -- attack the first one. -- Attacking does not require full access, adjacency is enough. wps <- pickWeaponClient source target case wps of- [] -> return Nothing- wp : _ -> return $! Just $ RequestAnyAbility wp- [] -> do -- move or search or alter+ Nothing -> return Nothing+ Just wp -> return $! Just $ RequestAnyAbility wp+ [] -> -- move or search or alter if accessible cops lvl spos tpos then -- Movement requires full access. return $! Just $ RequestAnyAbility $ ReqMove dir
Game/LambdaHack/Client/AI/PickActorClient.hs view
@@ -1,8 +1,11 @@+{-# LANGUAGE TupleSections #-} -- | Semantics of most 'ResponseAI' client commands. module Game.LambdaHack.Client.AI.PickActorClient ( pickActorToMove ) where +import Control.Applicative+import Control.Arrow import Control.Exception.Assert.Sugar import Control.Monad import qualified Data.EnumMap.Strict as EM@@ -12,13 +15,16 @@ import Game.LambdaHack.Client.AI.ConditionClient import Game.LambdaHack.Client.AI.PickTargetClient+import Game.LambdaHack.Client.BfsClient import Game.LambdaHack.Client.CommonClient import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State+import Game.LambdaHack.Common.Ability import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Frequency+import Game.LambdaHack.Common.ItemStrongest import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc@@ -31,8 +37,7 @@ import Game.LambdaHack.Content.ModeKind pickActorToMove :: MonadClient m- => (ActorId -> (ActorId, Actor)- -> m (Maybe ((ActorId, Actor), (Target, PathEtc))))+ => ((ActorId, Actor) -> m (Maybe (Target, PathEtc))) -> ActorId -> m (ActorId, Actor) pickActorToMove refreshTarget oldAid = do@@ -46,7 +51,7 @@ t = lvl `at` bpos oldBody mleader <- getsClient _sleader ours <- getsState $ actorRegularAssocs (== side) arena- let explore = void $ refreshTarget oldAid (oldAid, oldBody)+ let explore = void $ refreshTarget (oldAid, oldBody) pickOld = do if mleader == Just oldAid then explore else case ftactic $ gplayer fact of@@ -63,16 +68,16 @@ else do -- Copy over the leader's target, if any, or follow his bpos. tgtLeader <- do- mtgt <- getsClient $ (EM.lookup leader) . stargetD+ mtgt <- getsClient $ EM.lookup leader . stargetD case mtgt of Nothing -> return $! TEnemy leader True Just (tgtLeader, _) -> return tgtLeader modifyClient $ \cli ->- cli { sbfsD = EM.delete oldAid (sbfsD cli)+ cli { sbfsD = invalidateBfs oldAid (sbfsD cli) , seps = seps cli + 773 } -- randomize paths mpath <- createPath oldAid tgtLeader- let tgtMPath = maybe (tgtLeader, Nothing)- (\(tgt, p) -> (tgt, Just p)) mpath+ let tgtMPath =+ maybe (tgtLeader, Nothing) (second Just) mpath modifyClient $ \cli -> cli {stargetD = EM.alter (const $ Just tgtMPath) oldAid (stargetD cli)}@@ -98,25 +103,35 @@ -- the old leader, if he is among the best candidates -- (to make the AI appear more human-like and easier to observe). -- TODO: this also takes melee into account, but not shooting.- oursTgt <- fmap catMaybes $ mapM (refreshTarget oldAid) ours- let actorWeak ((aid, body), _) = do+ let refresh aidBody = do+ mtgt <- refreshTarget aidBody+ return $! (aidBody,) <$> mtgt+ oursTgt <- catMaybes <$> mapM refresh ours+ let actorVulnerable ((aid, body), _) = do activeItems <- activeItemsClient aid condMeleeBad <- condMeleeBadM aid threatDistL <- threatDistList aid- fleeL <- fleeList False aid- let condThreatAdj =- not $ null $ takeWhile ((== 1) . fst) threatDistL+ (fleeL, _) <- fleeList aid+ let actorMaxSk = sumSkills activeItems+ abInMaxSkill ab = EM.findWithDefault 0 ab actorMaxSk > 0+ condNoUsableWeapon = all (not . isMelee) activeItems+ canMelee = abInMaxSkill AbMelee && not condNoUsableWeapon+ condCanFlee = not (null fleeL)+ condThreatAtHandVeryClose =+ not $ null $ takeWhile ((<= 2) . fst) threatDistL+ threatAdj = takeWhile ((== 1) . fst) threatDistL+ condThreatAdj = not $ null threatAdj condFastThreatAdj = any (\(_, (_, b)) -> bspeed b activeItems > bspeed body activeItems)- $ takeWhile ((== 1) . fst) threatDistL- condCanFlee = not (null fleeL || condFastThreatAdj)+ threatAdj heavilyDistressed = -- Actor hit by a projectile or similarly distressed. deltaSerious (bcalmDelta body)- return $! if condThreatAdj- then condMeleeBad && condCanFlee- else heavilyDistressed+ return $! if canMelee && condThreatAdj then False+ else if condThreatAtHandVeryClose+ then condCanFlee && condMeleeBad && not condFastThreatAdj+ else heavilyDistressed -- shot at -- TODO: modify when reaction fire is possible actorHearning (_, (TEnemyPos{}, (_, (_, d)))) | d <= 2 = return False -- noise probably due to fleeing target@@ -129,16 +144,25 @@ -- AI has to be prudent and not lightly waste leader for meleeing, -- even if his target is distant actorMeleeing ((aid, _), _) = condAnyFoeAdjM aid- oursWeak <- filterM actorWeak oursTgt- oursStrong <- filterM (fmap not . actorWeak) oursTgt -- TODO: partitionM- oursMeleeing <- filterM actorMeleeing oursStrong- oursNotMeleeing <- filterM (fmap not . actorMeleeing) oursStrong+ actorMeleeBad ((aid, _), _) = do+ threatDistL <- threatDistList aid+ let condThreatMedium = -- if foes far, friends may still come+ not $ null $ takeWhile ((<= 5) . fst) threatDistL+ condMeleeBad <- condMeleeBadM aid+ return $! condThreatMedium && condMeleeBad+ oursVulnerable <- filterM actorVulnerable oursTgt+ oursSafe <- filterM (fmap not . actorVulnerable) oursTgt+ -- TODO: partitionM+ oursMeleeing <- filterM actorMeleeing oursSafe+ oursNotMeleeing <- filterM (fmap not . actorMeleeing) oursSafe oursHearing <- filterM actorHearning oursNotMeleeing oursNotHearing <- filterM (fmap not . actorHearning) oursNotMeleeing+ oursMeleeBad <- filterM actorMeleeBad oursNotHearing+ oursNotMeleeBad <- filterM (fmap not . actorMeleeBad) oursNotHearing let targetTEnemy (_, (TEnemy{}, _)) = True targetTEnemy (_, (TEnemyPos{}, _)) = True targetTEnemy _ = False- (oursTEnemy, oursOther) = partition targetTEnemy oursNotHearing+ (oursTEnemy, oursOther) = partition targetTEnemy oursNotMeleeBad -- These are not necessarily stuck (perhaps can go around), -- but their current path is blocked by friends. targetBlocked our@((_aid, _b), (_tgt, (path, _etc))) =@@ -152,7 +176,8 @@ -- so far we only detect blocked and only in Other mode -- && not (aid == oldAid && waitedLastTurn b time) -- not stuck -- this only prevents staying stuck- (oursBlocked, oursPos) = partition targetBlocked oursOther+ (oursBlocked, oursPos) =+ partition targetBlocked $ oursOther ++ oursMeleeBad -- Lower overhead is better. overheadOurs :: ((ActorId, Actor), (Target, PathEtc)) -> (Int, Int, Bool)@@ -160,7 +185,7 @@ if targetTEnemy our then -- TODO: take weapon, walk and fight speed, etc. into account ( d + if targetBlocked our then 2 else 0 -- possible delay, hacky- , - 10 * (fromIntegral $ bhp b `div` (10 * oneM))+ , - 10 * fromIntegral (bhp b `div` (10 * oneM)) , aid /= oldAid ) else -- Keep proper formation, not too dense, not to sparse.@@ -206,13 +231,13 @@ not (adjacent (bpos b) goal) -- not in melee range already && goodGeneric our goodTEnemy our = goodGeneric our- oursWeakGood = filter goodTEnemy oursWeak+ oursVulnerableGood = filter goodTEnemy oursVulnerable oursTEnemyGood = filter goodTEnemy oursTEnemy oursPosGood = filter goodGeneric oursPos oursMeleeingGood = filter goodGeneric oursMeleeing oursHearingGood = filter goodTEnemy oursHearing oursBlockedGood = filter goodGeneric oursBlocked- candidates = [ sortOurs oursWeakGood+ candidates = [ sortOurs oursVulnerableGood , sortOurs oursTEnemyGood , sortOurs oursPosGood , sortOurs oursMeleeingGood
Game/LambdaHack/Client/AI/PickTargetClient.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE TupleSections #-} -- | Let AI pick the best target for an actor. module Game.LambdaHack.Client.AI.PickTargetClient ( targetStrategy, createPath@@ -5,8 +6,10 @@ import Control.Applicative import Control.Exception.Assert.Sugar+import Control.Monad import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES+import Data.List import Data.Maybe import Game.LambdaHack.Client.AI.ConditionClient@@ -21,7 +24,7 @@ import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction-import Game.LambdaHack.Common.Item+import Game.LambdaHack.Common.Frequency import Game.LambdaHack.Common.ItemStrongest import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level@@ -39,13 +42,13 @@ -- | AI proposes possible targets for the actor. Never empty. targetStrategy :: forall m. MonadClient m- => ActorId -> ActorId -> m (Strategy (Target, Maybe PathEtc))-targetStrategy oldLeader aid = do+ => ActorId -> m (Strategy (Target, Maybe PathEtc))+targetStrategy aid = do cops@Kind.COps{corule, cotile=cotile@Kind.Ops{ouniqGroup}} <- getsState scops let stdRuleset = Kind.stdRuleset corule nearby = rnearby stdRuleset itemToF <- itemToFullClient- modifyClient $ \cli -> cli { sbfsD = EM.delete aid (sbfsD cli)+ modifyClient $ \cli -> cli { sbfsD = invalidateBfs aid (sbfsD cli) , seps = seps cli + 773 } -- randomize paths b <- getsState $ getActorBody aid activeItems <- activeItemsClient aid@@ -72,24 +75,25 @@ then Just (tgt, (q : rest, (goal, len - 1))) -- step along path else Nothing -- veered off the path ([p], (goal, _)) -> do- assert (p == goal `blame` (aid, b, mtgtMPath)) skip+ let !_A = assert (p == goal `blame` (aid, b, mtgtMPath)) () if bpos b == p then Just (tgt, path) -- goal reached; stay there picking up items else Nothing -- somebody pushed us off the goal; let's target again ([], _) -> assert `failure` (aid, b, mtgtMPath) Nothing -> return Nothing -- no target assigned yet- assert (not $ bproj b) skip -- would work, but is probably a bug+ let !_A = assert (not $ bproj b) () -- would work, but is probably a bug fact <- getsState $ (EM.! bfid b) . sfactionD allFoes <- getsState $ actorRegularAssocs (isAtWar fact) (blid b) dungeon <- getsState sdungeon- itemD <- getsState sitemD -- We assume the actor eventually becomes a leader (or has the same -- set of abilities as the leader, anyway) and set his target accordingly.- actorSk <- maxActorSkillsClient aid- condCanProject <- condCanProjectM aid- condMeleeBad <- condMeleeBadM aid+ let actorMaxSk = sumSkills activeItems+ actorMinSk <- getsState $ actorSkills Nothing aid activeItems+ condCanProject <- condCanProjectM True aid condHpTooLow <- condHpTooLowM aid+ condEnoughGear <- condEnoughGearM aid+ condMeleeBad <- condMeleeBadM aid let friendlyFid fid = fid == bfid b || isAllied fact fid friends <- getsState $ actorRegularList friendlyFid (blid b) -- TODO: refine all this when some actors specialize in ranged attacks@@ -99,51 +103,72 @@ canEscape <- factionCanEscape (bfid b) explored <- getsClient sexplored smellRadius <- sumOrganEqpClient IK.EqpSlotAddSmell aid- let canSmell = smellRadius > 0+ let condNoUsableWeapon = all (not . isMelee) activeItems+ lidExplored = ES.member (blid b) explored+ allExplored = ES.size explored == EM.size dungeon+ canSmell = smellRadius > 0 meleeNearby | canEscape = nearby `div` 2 -- not aggresive | otherwise = nearby rangedNearby = 2 * meleeNearby- targetableMelee body =- chessDist (bpos body) (bpos b) < meleeNearby- && not condMeleeBad+ -- Don't target nonmoving actors at all if bad melee,+ -- because nonmoving can't be lured nor ambushed.+ -- This is especially important for fences, tower defense actors, etc.+ -- If content gives nonmoving actor loot, this becomes problematic.+ targetableMelee aidE body = do+ activeItemsE <- activeItemsClient aidE+ let actorMaxSkE = sumSkills activeItemsE+ attacksFriends = any (adjacent (bpos body) . bpos) friends+ n = if attacksFriends then rangedNearby else meleeNearby+ nonmoving = EM.findWithDefault 0 AbMove actorMaxSkE <= 0+ return {-keep lazy-} $+ chessDist (bpos body) (bpos b) < n+ && not condNoUsableWeapon+ && EM.findWithDefault 0 AbMelee actorMaxSk > 0+ && not (hpTooLow b activeItems)+ && not (nonmoving && condMeleeBad) targetableRangedOrSpecial body = chessDist (bpos body) (bpos b) < rangedNearby- && (condCanProject- || hpTooLow body activeItems -- easy prey- || any (adjacent (bpos body) . bpos) friends) -- attacks friends!- targetableEnemy body =- targetableMelee body || targetableRangedOrSpecial body- nearbyFoes = filter (targetableEnemy . snd) allFoes- unknownId = ouniqGroup "unknown space"- itemUsefulness iid k =- fst <$> totalUsefulness cops b activeItems fact (itemToF iid k)- -- TODO: factor out from here and benGroundItems- desirableItem iid item k- | canEscape = itemUsefulness iid k /= Just 0- || IK.Precious `elem` jfeature item- | otherwise =- let use = itemUsefulness iid k- -- A hack to prevent monsters from picking up treasure.- preciousWithoutSlot item2 =- IK.Precious `elem` jfeature item2 -- risk from treasure hunters- && isNothing (strengthEqpSlot item2) -- unlikely to be useful- in use /= Just 0- && not (isNothing use -- needs resources to id- && preciousWithoutSlot item)+ && condCanProject+ targetableEnemy (aidE, body) = do+ tMelee <- targetableMelee aidE body+ return $! targetableRangedOrSpecial body || tMelee+ nearbyFoes <- filterM targetableEnemy allFoes+ let unknownId = ouniqGroup "unknown space"+ itemUsefulness itemFull =+ fst <$> totalUsefulness cops b activeItems fact itemFull desirableBag bag = any (\(iid, k) ->- desirableItem iid (itemD EM.! iid) k)- $ EM.assocs bag+ let itemFull = itemToF iid k+ use = itemUsefulness itemFull+ in desirableItem canEscape use itemFull) $ EM.assocs bag desirable (_, (_, Nothing)) = True desirable (_, (_, Just bag)) = desirableBag bag -- TODO: make more common when weak ranged foes preferred, etc. focused = bspeed b activeItems < speedNormal || condHpTooLow+ couldMoveLastTurn =+ let axtorSk = if (fst <$> gleader fact) == Just aid+ then actorMaxSk+ else actorMinSk+ in EM.findWithDefault 0 AbMove axtorSk > 0+ isStuck = waitedLastTurn b && couldMoveLastTurn+ slackTactic = ftactic (gplayer fact) `elem` [TBlock, TRoam, TPatrol] setPath :: Target -> m (Strategy (Target, Maybe PathEtc)) setPath tgt = do mpath <- createPath aid tgt- return $! returN "pickNewTarget"- $ maybe (tgt, Nothing) (\(t, p) -> (t, Just p)) mpath+ let take5 (TEnemy{}, pgl) =+ (tgt, Just pgl) -- for projecting, even by roaming actors+ take5 (_, pgl@(path, (goal, _))) =+ if slackTactic then+ -- Best path only followed 5 moves; then straight on.+ let path5 = take 5 path+ vtgt | bpos b == goal = tgt+ | otherwise = TVector $ towards (bpos b) goal+ in (vtgt, Just (path5, (last path5, length path5 - 1)))+ else (tgt, Just pgl)+ return $! returN "setPath" $ maybe (tgt, Nothing) take5 mpath pickNewTarget :: m (Strategy (Target, Maybe PathEtc)) pickNewTarget = do+ -- This is mostly lazy and used between 0 and 3 times below.+ ctriggers <- closestTriggers Nothing aid -- TODO: for foes, items, etc. consider a few nearby, not just one cfoes <- closestFoes nearbyFoes aid case cfoes of@@ -157,65 +182,76 @@ else return [] case smpos of [] -> do- citems <- if EM.findWithDefault 0 AbMoveItem actorSk > 0- then closestItems aid- else return []- case filter desirable citems of- [] | ftactic (gplayer fact) == TRoam -> do- mtgtPrev <- getsClient $ getTarget aid- let vOld = bpos b `vectorToFrom` boldpos b- v = case (mtgtPrev, isUnit vOld) of- (Just (TVector tgtPrev), True) ->- if euclidDistSqVector tgtPrev vOld <= 2- then tgtPrev- else vOld- (Just (TVector tgtPrev), False) -> tgtPrev- (_, True) -> vOld- (_, False) -> Vector 1 1 -- south-east- -- Items and smells considered every 5 moves.- -- Thanks to sentinels, @path@ is never null.- path = trajectoryToPathBounded- lxsize lysize (bpos b) (replicate 5 v)- return $! returN "tgt with no playerLeader"- ( TVector v- , Just (bpos b : path, (last path, length path)) )- [] -> do- let lidExplored = ES.member (blid b) explored- upos <- if lidExplored- then return Nothing- else closestUnknown aid- case upos of- Nothing -> do- csuspect <- if lidExplored- then return []- else closestSuspect aid- case csuspect of- [] -> do- ctriggers <-- if EM.findWithDefault 0 AbTrigger actorSk > 0- then closestTriggers Nothing False aid- else return []- case ctriggers of+ let ctriggersEarly =+ if EM.findWithDefault 0 AbTrigger actorMaxSk > 0+ && condEnoughGear+ then ctriggers+ else mzero+ if nullFreq ctriggersEarly then do+ citems <-+ if EM.findWithDefault 0 AbMoveItem actorMaxSk > 0+ then closestItems aid+ else return []+ case filter desirable citems of+ [] -> do+ let vToTgt v0 = do+ let vFreq = toFreq "vFreq"+ $ (20, v0) : map (1,) moves+ v <- rndToAction $ frequency vFreq+ -- Items and smells, etc. considered every 7 moves.+ let tra = trajectoryToPathBounded+ lxsize lysize (bpos b) (replicate 7 v)+ path = nub $ bpos b : tra+ return $! returN "tgt with no exploration"+ ( TVector v+ , if length path == 1+ then Nothing+ else Just (path, (last path, length path - 1)) )+ vOld = bpos b `vectorToFrom` boldpos b+ pNew = shiftBounded lxsize lysize (bpos b) vOld+ if slackTactic && not isStuck+ && isUnit vOld && bpos b /= pNew+ && accessible cops lvl (bpos b) pNew+ then vToTgt vOld+ else do+ upos <- if lidExplored+ then return Nothing+ else closestUnknown aid+ case upos of+ Nothing -> do+ csuspect <- if lidExplored+ then return []+ else closestSuspect aid+ case csuspect of [] -> do- -- All stones turned, time to win or die.- afoes <- closestFoes allFoes aid- case afoes of- (_, (aid2, _)) : _ ->- setPath $ TEnemy aid2 False- [] -> do- getDistant <-- rndToAction $ oneOf- $ [fmap (: []) . furthestKnown]- ++ [ closestTriggers Nothing True- | EM.size dungeon > 1 ]- kpos <- getDistant aid- case kpos of- [] -> return reject- p : _ -> setPath $ TPoint (blid b) p+ let ctriggersMiddle =+ if EM.findWithDefault 0 AbTrigger+ actorMaxSk > 0+ && not allExplored+ then ctriggers+ else mzero+ if nullFreq ctriggersMiddle then do+ -- All stones turned, time to win or die.+ afoes <- closestFoes allFoes aid+ case afoes of+ (_, (aid2, _)) : _ ->+ setPath $ TEnemy aid2 False+ [] -> do+ if nullFreq ctriggers then do+ furthest <- furthestKnown aid+ setPath $ TPoint (blid b) furthest+ else do+ p <- rndToAction $ frequency ctriggers+ setPath $ TPoint (blid b) p+ else do+ p <- rndToAction $ frequency ctriggers+ setPath $ TPoint (blid b) p p : _ -> setPath $ TPoint (blid b) p- p : _ -> setPath $ TPoint (blid b) p- Just p -> setPath $ TPoint (blid b) p- (_, (p, _)) : _ -> setPath $ TPoint (blid b) p+ Just p -> setPath $ TPoint (blid b) p+ (_, (p, _)) : _ -> setPath $ TPoint (blid b) p+ else do+ p <- rndToAction $ frequency ctriggers+ setPath $ TPoint (blid b) p (_, (p, _)) : _ -> setPath $ TPoint (blid b) p tellOthersNothingHere pos = do let f (tgt, _) = case tgt of@@ -250,21 +286,21 @@ (oldTgt, Just ( bpos b : path , (p, fromMaybe (assert `failure` mpath) $ accessBfs bfs p) ))- TEnemyPos _ lid p permit ->+ TEnemyPos _ lid p permit -- Chase last position even if foe hides or dies, -- to find his companions, loot, etc.- if lid /= blid b -- wrong level- || chessDist (bpos b) p >= nearby -- too far and not visible- || permit -- never follow a friend more than 1 step- then pickNewTarget- else if p == bpos b- then tellOthersNothingHere p- else return $! returN "TEnemyPos" (oldTgt, Just updatedPath)+ | lid /= blid b -- wrong level+ || chessDist (bpos b) p >= nearby -- too far and not visible+ || permit -- never follow a friend more than 1 step+ -> pickNewTarget+ | p == bpos b -> tellOthersNothingHere p+ | otherwise ->+ return $! returN "TEnemyPos" (oldTgt, Just updatedPath) _ | not $ null nearbyFoes -> pickNewTarget -- prefer close foes to anything TPoint lid pos -> do- let allExplored = ES.size explored == EM.size dungeon bag <- getsState $ getCBag $ CFloor lid pos+ let t = lvl `at` pos if lid /= blid b -- wrong level -- Below we check the target could not be picked again in -- pickNewTarget, and only in this case it is invalidated.@@ -272,16 +308,18 @@ -- shows up) and not changed all the time mid-route -- to equally interesting, but perhaps a bit closer targets, -- most probably already targeted by other actors.- || (EM.findWithDefault 0 AbMoveItem actorSk <= 0 -- closestItems- || not (desirableBag bag))- && (not canSmell -- closestSmell- || pos == bpos b -- in case server resends deleted smell+ ||+ (EM.findWithDefault 0 AbMoveItem actorMaxSk <= 0+ || not (desirableBag bag)) -- closestItems+ &&+ (pos == bpos b+ || (not canSmell -- closestSmell || let sml = EM.findWithDefault timeZero pos (lsmell lvl) in sml <= ltime lvl)- && let t = lvl `at` pos- in if ES.notMember lid explored+ && if not lidExplored then t /= unknownId -- closestUnknown && not (Tile.isSuspect cotile t) -- closestSuspect+ && not (condEnoughGear && Tile.isStair cotile t) else -- closestTriggers -- Try to kill that very last enemy for his loot before -- leaving the level or dungeon.@@ -293,21 +331,16 @@ -- We don't determine if the stairs are interesting -- (this changes with time), but allow the actor -- to reach them and then retarget, unless he can't- -- trigger them in the first place.- && (EM.findWithDefault 0 AbTrigger actorSk <= 0- || pos == bpos b+ -- trigger them at all.+ && (EM.findWithDefault 0 AbTrigger actorMaxSk <= 0 || not (Tile.isStair cotile t)) -- The remaining case is furthestKnown. This is -- always an unimportant target, so we forget it -- if the actor is stuck (waits, though could move; -- or has zeroed individual moving skill, -- but then should change targets often anyway).- && let isStuck =- waitedLastTurn b- && canMoveFact fact (oldLeader == aid)- in pos == bpos b- || isStuck- || not allExplored+ && (isStuck+ || not allExplored)) then pickNewTarget else return $! returN "TPoint" (oldTgt, Just updatedPath) TVector{} | len > 1 ->@@ -324,6 +357,8 @@ mpos <- aidTgtToPos aid (blid b) (Just tgt) case mpos of Nothing -> return Nothing+-- TODO: for now, an extra turn at target is needed, e.g., to pick up items+-- Just p | p == bpos b -> return Nothing Just p -> do (bfs, mpath) <- getCacheBfsAndPath aid p return $! case mpath of
Game/LambdaHack/Client/AI/Preferences.hs view
@@ -3,9 +3,8 @@ ( totalUsefulness, effectToBenefit ) where -import qualified Control.Monad.State as St+import Control.Applicative import qualified Data.EnumMap.Strict as EM-import Data.Maybe import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState@@ -28,73 +27,86 @@ let dungeonDweller = not $ fcanEscape $ gplayer fact in case eff of IK.NoEffect _ -> 0- IK.Hurt d -> -(min 99 $ round (10 * Dice.meanDice d))- IK.Burn p -> -15 * p -- usually splash damage, etc.- IK.Explode _ -> -10+ IK.Hurt d -> -(min 100 $ 10 * Dice.meanDice d)+ IK.Burn d -> -(min 150 $ 15 * Dice.meanDice d)+ -- often splash damage, etc.+ IK.Explode _ -> 0 -- depends on explosion IK.RefillHP p -> let hpMax = sumSlotNoFilter IK.EqpSlotAddMaxHP activeItems in if p > 0- then 1 + 10 * min p (fromIntegral $ (xM hpMax - bhp b) `divUp` oneM)- else max (-99) (10 * p)+ -- TODO: when picking up, always deem valuable; when drinking, only if+ -- HP not maxxed.+ then 10 * min p (max 0 $ fromIntegral+ $ (xM hpMax - bhp b) `divUp` oneM)+ else max (-99) (11 * p) IK.OverfillHP p -> let hpMax = sumSlotNoFilter IK.EqpSlotAddMaxHP activeItems in if p > 0- then 2 + 10 * min p (fromIntegral $ (xM hpMax - bhp b) `divUp` oneM)- else max (-99) (10 * p)+ then 11 * min p (max 1 $ fromIntegral+ $ (xM hpMax - bhp b) `divUp` oneM)+ else max (-99) (11 * p) IK.RefillCalm p -> let calmMax = sumSlotNoFilter IK.EqpSlotAddMaxCalm activeItems in if p > 0- then 1 + min p (fromIntegral $ (xM calmMax - bcalm b) `divUp` oneM)+ then min p (max 0 $ fromIntegral+ $ (xM calmMax - bcalm b) `divUp` oneM) else max (-20) p IK.OverfillCalm p -> let calmMax = sumSlotNoFilter IK.EqpSlotAddMaxCalm activeItems in if p > 0- then 2 + min p (fromIntegral $ (xM calmMax - bcalm b) `divUp` oneM)+ then min p (max 1 $ fromIntegral+ $ (xM calmMax - bcalm b) `divUp` oneM) else max (-20) p IK.Dominate -> -200 IK.Impress -> -10- IK.CallFriend d -> round $ 20 * Dice.meanDice d- IK.Summon{} | dungeonDweller -> 1 -- probably summons friends or crazies- IK.Summon{} -> 0 -- probably generates enemies- IK.Ascend{} -> 1 -- change levels sensibly, in teams- IK.Escape{} -> 10000 -- AI wants to win; spawners to guard- IK.Paralyze d -> round $ -20 * Dice.meanDice d- IK.InsertMove d -> round $ 50 * Dice.meanDice d+ IK.CallFriend d -> 100 * Dice.meanDice d+ IK.Summon _ d | dungeonDweller ->+ -- Probably summons friends or crazies.+ -- TODO: should be Negative, to use Calm of enemy, but also positive+ -- to use with own Calm, if needed.+ 50 * Dice.meanDice d+ IK.Summon{} -> 0 -- probably generates enemies+ IK.Ascend{} -> 1 -- low, to only change levels sensibly, in teams+ -- TODO: use if low HP and enemies at hand+ IK.Escape{} -> 10000 -- AI wants to win; spawners to guard+ IK.Paralyze d -> -20 * Dice.meanDice d+ IK.InsertMove d -> 50 * Dice.meanDice d IK.Teleport d ->- let p = round $ Dice.meanDice d- in if p <= 9 then 10 -- blink to shoot at foe- else if p <= 19 then 1 -- neither escape nor repositioning- else -5 * p -- get rid of the foe+ let p = Dice.meanDice d+ in if p <= 8 -- blink to shoot at foe+ && dungeonDweller -- non-dwellers have to explore and escape ASAP+ then 1+ else -p -- get rid of the foe IK.CreateItem COrgan grp _ -> -- TODO: use the timeout let (total, count) = organBenefit grp cops b in total `divUp` count -- average over all matching grp; rarities ignored- IK.CreateItem _ _ _ -> 30+ IK.CreateItem{} -> 30 -- TODO IK.DropItem COrgan grp True -> -- calculated for future use, general pickup let (total, _) = organBenefit grp cops b in - total -- sum over all matching grp; simplification: rarities ignored IK.DropItem _ _ False -> -15 IK.DropItem _ _ True -> -30- IK.PolyItem _ -> 0 -- AI would loop- IK.Identify _ -> 1 -- not higher, or AI would loop+ IK.PolyItem -> 0 -- AI can't estimate item desirability vs average+ IK.Identify -> 0 -- AI doesn't know how to use IK.SendFlying _ -> -10 -- but useful on self sometimes, too IK.PushActor _ -> -10 -- but useful on self sometimes, too IK.PullActor _ -> -10 IK.DropBestWeapon -> -50 IK.ActivateInv ' ' -> -100 IK.ActivateInv _ -> -50- IK.ApplyPerfume -> -10+ IK.ApplyPerfume -> 0 -- depends on the smell sense of friends and foes IK.OneOf _ -> 1 -- usually a mixed blessing, but slightly beneficial IK.OnSmash _ -> -10- IK.Recharging e -> effectToBenefit cops b activeItems fact e- `divUp` 3 -- TODO: use Timeout+ IK.Recharging e ->+ -- Used, e.g., in @periodicBens@, which takes timeout into account, too.+ effectToBenefit cops b activeItems fact e IK.Temporary _ -> 0 -- TODO: calculating this for "temporary conditions" takes forever organBenefit :: GroupName ItemKind -> Kind.COps -> Actor -> (Int, Int) organBenefit t cops@Kind.COps{coitem=Kind.Ops{ofoldrGroup}} b =- let travA x = St.evalState (IK.aspectTrav x (return . round . Dice.meanDice)) ()- f p _ kind (sacc, pacc) =- let paspect asp = p * aspectToBenefit cops b (travA asp)+ let f p _ kind (sacc, pacc) =+ let paspect asp = p * aspectToBenefit cops b (Dice.meanDice <$> asp) in ( sacc + sum (map paspect $ IK.iaspects kind) , pacc + p ) in ofoldrGroup t f (0, 0)@@ -103,18 +115,19 @@ aspectToBenefit :: Kind.COps -> Actor -> IK.Aspect Int -> Int aspectToBenefit _cops _b asp = case asp of+ IK.Unique{} -> 0 IK.Periodic{} -> 0 IK.Timeout{} -> 0- IK.AddMaxHP p -> p * 10- IK.AddMaxCalm p -> p `divUp` 2- IK.AddSpeed p -> p * 10000- IK.AddSkills m -> 5 * sum (EM.elems m)- IK.AddHurtMelee p -> p `divUp` 3+ IK.AddHurtMelee p -> p IK.AddHurtRanged p -> p `divUp` 5 -- TODO: should be summed with damage IK.AddArmorMelee p -> p `divUp` 5 IK.AddArmorRanged p -> p `divUp` 10+ IK.AddMaxHP p -> p+ IK.AddMaxCalm p -> p `div` 5+ IK.AddSpeed p -> p * 10000+ IK.AddSkills m -> 5 * sum (EM.elems m) IK.AddSight p -> p * 10- IK.AddSmell p -> p * 2+ IK.AddSmell p -> p * 10 IK.AddLight p -> p * 10 -- | Determine the total benefit from having an item in eqp or inv,@@ -134,26 +147,25 @@ Just timeout -> map (\eff -> eff * 10 `divUp` timeout) periodicEffBens selfBens = aspBens ++ periodicBens- eqpSum = if not (null selfBens) && minimum selfBens < -10- && maximum selfBens > 10- then 0 -- significant mixed blessings out of AI control- else sum selfBens+ selfSum = sum selfBens+ mixedBlessing =+ not (null selfBens)+ && (selfSum > 0 && minimum selfBens < -10+ || selfSum < 0 && maximum selfBens > 10) effSum = sum effBens- isWeapon =- isJust (strengthFromEqpSlot IK.EqpSlotWeapon itemFull)- totalSum = if goesIntoInv $ itemBase itemFull- then effSum- else if isWeapon && effSum < 0- then - effSum + eqpSum- else eqpSum+ isWeapon = isMeleeEqp itemFull+ totalSum+ | isWeapon && effSum < 0 = - effSum + selfSum+ | goesIntoInv itemFull = effSum+ | mixedBlessing =+ 0 -- significant mixed blessings out of AI control+ | otherwise = selfSum -- if the weapon heals the enemy, it+ -- won't be used but can be equipped in (totalSum, effSum) in case itemDisco itemFull of Just ItemDisco{itemAE=Just ItemAspectEffect{jaspects, jeffects}} -> Just $ ben jeffects jaspects Just ItemDisco{itemKind=IK.ItemKind{iaspects, ieffects}} ->- let travA x =- St.evalState (IK.aspectTrav x (return . round . Dice.meanDice))- ()- jaspects = map travA iaspects+ let jaspects = map (fmap Dice.meanDice) iaspects in Just $ ben ieffects jaspects _ -> Nothing
Game/LambdaHack/Client/AI/Strategy.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE TupleSections #-}+{-# LANGUAGE DeriveFoldable, DeriveTraversable, TupleSections #-} -- | AI strategies to direct actors not controlled directly by human players. -- No operation in this module involves the 'State' or 'Action' type. module Game.LambdaHack.Client.AI.Strategy@@ -8,8 +8,10 @@ import Control.Applicative import Control.Monad+import Data.Foldable (Foldable) import Data.Maybe import Data.Text (Text)+import Data.Traversable (Traversable) import Game.LambdaHack.Common.Frequency as Frequency import Game.LambdaHack.Common.Msg@@ -17,7 +19,7 @@ -- | A strategy is a choice of (non-empty) frequency tables -- of possible actions. newtype Strategy a = Strategy { runStrategy :: [Frequency a] }- deriving Show+ deriving (Show, Foldable, Traversable) -- | Strategy is a monad. TODO: Can we write this as a monad transformer? instance Monad Strategy where@@ -98,6 +100,7 @@ returN :: Text -> a -> Strategy a returN name x = Strategy $ return $! uniformFreq name [x] +-- TODO: express with traverse? mapStrategyM :: Monad m => (a -> m (Maybe b)) -> Strategy a -> m (Strategy b) mapStrategyM f s = do let mapFreq freq = do
Game/LambdaHack/Client/Bfs.hs view
@@ -1,10 +1,10 @@ {-# LANGUAGE CPP, GeneralizedNewtypeDeriving #-} -- | Breadth first search algorithms. module Game.LambdaHack.Client.Bfs- ( -- * Public API- BfsDistance, MoveLegal(..), apartBfs+ ( BfsDistance, MoveLegal(..), apartBfs , fillBfs, findPathBfs, accessBfs #ifdef EXPOSE_INTERNAL+ -- * Internal operations , minKnownBfs #endif ) where@@ -33,11 +33,16 @@ minKnownBfs = toEnum $ (1 + fromEnum (maxBound :: BfsDistance)) `div` 2 -- | The distance value that denote no legal path between points.+-- The next value is the minimal distance value assigned to paths+-- that don't enter any unknown tiles. apartBfs :: BfsDistance apartBfs = pred minKnownBfs --- TODO: costly; peephole optimize, optmize BFS, don't call so often+-- TODO: costly; use a ring buffer instead of the lists, don't call so often -- | Fill out the given BFS array.+-- Unsafe @PointArray@ operations are OK here, because the intermediate+-- values of the vector don't leak anywhere outside nor are kept unevaluated+-- and so they can't be overwritten by the unsafe side-effect. fillBfs :: (Point -> Point -> MoveLegal) -- ^ is a move from known tile legal -> (Point -> Point -> Bool) -- ^ is a move from unknown legal -> Point -- ^ starting position@@ -71,7 +76,7 @@ (mvsK, mvsU) = foldl' fKnown ([], []) moves upd = zip mvsK (repeat distance) ++ zip mvsU (repeat distCompl)- !a3 = a2 PointArray.// upd+ !a3 = PointArray.unsafeUpdateA a2 upd in (mvsK ++ succK2, mvsU ++ succU2, a3) processUnknown (succU2, a2) pos = let fUnknown lU move =@@ -83,18 +88,16 @@ else lU mvsU = foldl' fUnknown [] moves upd = zip mvsU (repeat distCompl)- !a3 = a2 PointArray.// upd+ !a3 = PointArray.unsafeUpdateA a2 upd in (mvsU ++ succU2, a3) (succU4, !a4) = foldl' processUnknown ([], a) predU (succK6, succU6, !a6) = foldl' processKnown ([], succU4, a4) predK- in if null succK6 && null succU6- then a6 -- no more dungeon positions to check- else if distance == predMaxKnownBfs -- wasting one Known slot- then a6 -- too far- else bfs (succ distance) succK6 succU6 a6- in PointArray.forceA -- no more modifications of this array- $ bfs (succ minKnownBfs) [origin] []- (aInitial PointArray.// [(origin, minKnownBfs)])+ in if null succK6 && null succU6 -- no more dungeon positions to check+ || distance == predMaxKnownBfs -- wasting one Known slot+ then a6 -- too far+ else bfs (succ distance) succK6 succU6 a6+ in bfs (succ minKnownBfs) [origin] []+ (PointArray.unsafeUpdateA aInitial [(origin, minKnownBfs)]) -- TODO: Use http://harablog.wordpress.com/2011/09/07/jump-point-search/ -- to determine a few really different paths and compare them,
Game/LambdaHack/Client/BfsClient.hs view
@@ -1,11 +1,12 @@ {-# LANGUAGE CPP, TupleSections #-} -- | Breadth first search and realted algorithms using the client monad. module Game.LambdaHack.Client.BfsClient- ( getCacheBfsAndPath, getCacheBfs, accessCacheBfs+ ( invalidateBfs, getCacheBfsAndPath, getCacheBfs, accessCacheBfs , unexploredDepth, closestUnknown, closestSuspect, closestSmell, furthestKnown , closestTriggers, closestItems, closestFoes ) where +import Control.Applicative import Control.Arrow ((&&&)) import Control.Exception.Assert.Sugar import Control.Monad@@ -22,7 +23,10 @@ import qualified Game.LambdaHack.Common.Ability as Ability import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState+import Game.LambdaHack.Common.Faction+import Game.LambdaHack.Common.Frequency import Game.LambdaHack.Common.Item+import Game.LambdaHack.Common.ItemStrongest import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.MonadStateRead@@ -32,8 +36,20 @@ import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time+import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.TileKind (TileKind) +invalidateBfs :: ActorId+ -> EM.EnumMap ActorId+ ( Bool, PointArray.Array BfsDistance+ , Point, Int, Maybe [Point])+ -> EM.EnumMap ActorId+ ( Bool, PointArray.Array BfsDistance+ , Point, Int, Maybe [Point])+invalidateBfs =+ EM.adjust+ (\(_, bfs, target, seps, mpath) -> (False, bfs, target, seps, mpath))+ -- | Get cached BFS data and path or, if not stored, generate, -- store and return. Due to laziness, they are not calculated until needed. getCacheBfsAndPath :: forall m. MonadClient m@@ -49,19 +65,29 @@ pathAndStore bfs = do let mpath = findPathBfs isEnterable passUnknown origin target seps bfs modifyClient $ \cli ->- cli {sbfsD = EM.insert aid (bfs, target, seps, mpath) (sbfsD cli)}+ cli {sbfsD = EM.insert aid (True, bfs, target, seps, mpath)+ (sbfsD cli)} return (bfs, mpath) mbfs <- getsClient $ EM.lookup aid . sbfsD case mbfs of- Just (bfs, targetOld, sepsOld, mpath)+ Just (True, bfs, targetOld, sepsOld, mpath) -- TODO: hack: in screensavers this is not always ensured, so check here: | bfs PointArray.! bpos b == succ apartBfs -> if targetOld == target && sepsOld == seps then return (bfs, mpath) else pathAndStore bfs _ -> do+ -- Reduce the number of pointers to @bfsInvalid@, to help @safeSetA@.+ modifyClient $ \cli -> cli {sbfsD = EM.delete aid $ sbfsD cli} Level{lxsize, lysize} <- getLevel $ blid b- let vInitial = PointArray.replicateA lxsize lysize apartBfs+ let vInitial = case mbfs of+ Just (_, bfsInvalid, _, _, _) -> -- TODO: we should verify size+ -- We need to use the safe set, because previous values+ -- of the BFS array for the actor can be stuck unevaluated+ -- in thunks and we are not allowed to overwrite them.+ PointArray.safeSetA apartBfs bfsInvalid+ _ ->+ PointArray.replicateA lxsize lysize apartBfs bfs = fillBfs isEnterable passUnknown origin vInitial pathAndStore bfs @@ -70,8 +96,8 @@ getCacheBfs aid = do mbfs <- getsClient $ EM.lookup aid . sbfsD case mbfs of- Just (bfs, _, _, _) -> return bfs- Nothing -> fmap fst $ getCacheBfsAndPath aid (Point 0 0)+ Just (True, bfs, _, _, _) -> return bfs+ _ -> fst <$> getCacheBfsAndPath aid (Point 0 0) condBFS :: MonadClient m => ActorId@@ -86,15 +112,22 @@ -- to reset BFS after leader changes, but it would still lead to -- wasted movement if, e.g., non-leaders move but only leaders open doors -- and leader change is very rare.- actorSk <- maxActorSkillsClient aid+ activeItems <- activeItemsClient aid+ let actorMaxSk = sumSkills activeItems lvl <- getLevel $ blid b+ smarkSuspect <- getsClient smarkSuspect+ fact <- getsState $ (EM.! bfid b) . sfactionD+ let underAI = isAIFact fact+ enterSuspect = smarkSuspect || underAI+ isPassable | enterSuspect = Tile.isPassable+ | otherwise = Tile.isPassableNoSuspect -- We treat doors as an open tile and don't add an extra step for opening -- the doors, because other actors open and use them, too, -- so it's amortized. We treat unknown tiles specially. let unknownId = ouniqGroup "unknown space" chAccess = checkAccess cops lvl- canOpenDoors = EM.findWithDefault 0 Ability.AbAlter actorSk > 0- chDoorAccess = if canOpenDoors then [checkDoorAccess cops lvl] else []+ canOpenDoors = EM.findWithDefault 0 Ability.AbAlter actorMaxSk > 0+ chDoorAccess = [checkDoorAccess cops lvl | canOpenDoors] conditions = catMaybes $ chAccess : chDoorAccess -- Legality of move from a known tile, assuming doors freely openable. isEnterable :: Point -> Point -> MoveLegal@@ -107,7 +140,7 @@ then if not (Tile.isSuspect cotile st) && allOK then MoveToUnknown else MoveBlocked- else if Tile.isPassable cotile tt+ else if isPassable cotile tt && not (Tile.isChangeable cotile st) -- takes time to change && allOK then MoveToOpen@@ -144,20 +177,25 @@ -- | Closest reachable unknown tile position, if any. closestUnknown :: MonadClient m => ActorId -> m (Maybe Point) closestUnknown aid = do+ body <- getsState $ getActorBody aid+ lvl@Level{lxsize, lysize} <- getLevel $ blid body bfs <- getCacheBfs aid- getMinIndex <- rndToAction $ oneOf [ PointArray.minIndexA- , PointArray.minLastIndexA ]- let closestPos = getMinIndex bfs- dist = bfs PointArray.! closestPos+ let closestPoss = PointArray.minIndexesA bfs+ dist = bfs PointArray.! head closestPoss if dist >= apartBfs then do- body <- getsState $ getActorBody aid- lvl <- getLevel $ blid body when (lclear lvl == lseen lvl) $ do -- explored fully, mark it once for all- assert (lclear lvl >= lseen lvl) skip+ let !_A = assert (lclear lvl >= lseen lvl) () modifyClient $ \cli -> cli {sexplored = ES.insert (blid body) (sexplored cli)} return Nothing- else return $ Just closestPos+ else do+ let unknownAround p =+ let vic = vicinity lxsize lysize p+ posUnknown pos = bfs PointArray.! pos < apartBfs+ vicUnknown = filter posUnknown vic+ in length vicUnknown+ cmp = comparing unknownAround+ return $ Just $ maximumBy cmp closestPoss -- TODO: this is costly, because target has to be changed every -- turn when walking along trail. But inverting the sort and going@@ -209,53 +247,71 @@ -- TODO: We assume linear dungeon in @unexploredD@, -- because otherwise we'd need to calculate shortest paths in a graph, etc. -- | Closest (wrt paths) triggerable open tiles.--- The second argument can ever be true only if there's--- no escape from the dungeon.-closestTriggers :: MonadClient m => Maybe Bool -> Bool -> ActorId -> m [Point]-closestTriggers onlyDir exploredToo aid = do+-- The level the actor is on is either explored or the actor already+-- has a weapon equipped, so no need to explore further, he tries to find+-- enemies on other levels.+closestTriggers :: MonadClient m => Maybe Bool -> ActorId -> m (Frequency Point)+closestTriggers onlyDir aid = do Kind.COps{cotile} <- getsState scops body <- getsState $ getActorBody aid+ explored <- getsClient sexplored let lid = blid body lvl <- getLevel lid dungeon <- getsState sdungeon- explored <- getsClient sexplored+ let escape = any (not . null . lescape) $ EM.elems dungeon unexploredD <- unexploredDepth let allExplored = ES.size explored == EM.size dungeon+ -- If lid not explored, aid equips a weapon and so can leave level.+ lidExplored = ES.member (blid body) explored f :: [(Int, Point)] -> Point -> Kind.Id TileKind -> [(Int, Point)] f acc p t = if Tile.isWalkable cotile t && not (null $ Tile.causeEffects cotile t)- then if exploredToo- then (1, p) : acc -- direction irrelevant- else case Tile.ascendTo cotile t of- [] ->- -- Escape (or guard) after exploring, for high score, etc.- if allExplored then (1, p) : acc else acc- l ->- let g k = k > 0- && onlyDir /= Just False- && unexploredD 1 lid- ||- k < 0- && onlyDir /= Just True- && unexploredD (-1) lid+ then case Tile.ascendTo cotile t of+ [] ->+ -- Escape (or guard) only after exploring, for high score, etc.+ if isNothing onlyDir && allExplored+ then (9999999, p) : acc -- all from that level congregate here+ else acc+ l ->+ if not escape && allExplored+ -- Direction irrelevant; wander randomly.+ then map (,p) l ++ acc+ else let g k =+ let easier = signum k /= signum (fromEnum lid)+ unexpForth = unexploredD (signum k) lid+ unexpBack = unexploredD (- signum k) lid+ aiCond = if unexpForth+ then easier+ || not unexpBack && lidExplored+ else not unexpBack && lidExplored+ && (null $ lescape lvl)+ in maybe aiCond (\d -> d == (k > 0)) onlyDir in map (,p) (filter g l) ++ acc else acc- let triggersAll = PointArray.ifoldlA f [] $ ltile lvl+ triggersAll = PointArray.ifoldlA f [] $ ltile lvl -- Don't target stairs under the actor. Most of the time they -- are blocked and stay so, so we seek other stairs, if any.- -- If no other stairs in this direction, let's wait here.- triggers | length triggersAll > 1 =- filter ((/= bpos body) . snd) triggersAll- | otherwise = triggersAll- case triggers of- [] -> return []- _ -> do- bfs <- getCacheBfs aid+ -- If no other stairs in this direction, let's wait here,+ -- unless the actor has just returned via the very stairs.+ triggers = filter ((/= bpos body) . snd) triggersAll+ bfs <- getCacheBfs aid+ return $ case triggers of -- keep lazy+ [] -> mzero+ _ | isNothing onlyDir && not escape && allExplored ->+ -- Distance also irrelevant, to ensure random wandering.+ toFreq "closestTriggers when allExplored" triggers+ _ -> -- Prefer stairs to easier levels.- let mix (k, p) dist = ((abs (fromEnum lid + k), dist), p)- ds = mapMaybe (\(k, p) -> fmap (mix (k, p)) (accessBfs bfs p))- triggers- return $! map snd $ sortBy (comparing fst) ds+ -- If exactly one escape, these stairs will all be in one direction.+ let mix (k, p) dist =+ let easier = signum k /= signum (fromEnum lid)+ depthDelta = if easier then 2 else 1+ distDelta = fromEnum (maxBound :: BfsDistance)+ - fromEnum apartBfs+ - dist+ in (depthDelta * distDelta * distDelta, p)+ ds = mapMaybe (\(k, p) -> mix (k, p) <$> accessBfs bfs p) triggers+ in toFreq "closestTriggers" ds unexploredDepth :: MonadClient m => m (Int -> LevelId -> Bool) unexploredDepth = do@@ -263,14 +319,15 @@ explored <- getsClient sexplored let allExplored = ES.size explored == EM.size dungeon unexploredD p =- let unex lid = allExplored && lescape (dungeon EM.! lid)+ let unex lid = allExplored+ && (not $ null $ lescape $ dungeon EM.! lid) || ES.notMember lid explored || unexploredD p lid in any unex . ascendInBranch dungeon p return unexploredD -- | Closest (wrt paths) items and changeable tiles (e.g., item caches).-closestItems :: MonadClient m => ActorId -> m ([(Int, (Point, Maybe ItemBag))])+closestItems :: MonadClient m => ActorId -> m [(Int, (Point, Maybe ItemBag))] closestItems aid = do Kind.COps{cotile} <- getsState scops body <- getsState $ getActorBody aid@@ -291,7 +348,7 @@ -- | Closest (wrt paths) enemy actors. closestFoes :: MonadClient m => [(ActorId, Actor)] -> ActorId -> m [(Int, (ActorId, Actor))]-closestFoes foes aid = do+closestFoes foes aid = case foes of [] -> return [] _ -> do
Game/LambdaHack/Client/CommonClient.hs view
@@ -3,16 +3,15 @@ module Game.LambdaHack.Client.CommonClient ( getPerFid, aidTgtToPos, aidTgtAims, makeLine , partAidLeader, partActorLeader, partPronounLeader- , actorSkillsClient, maxActorSkillsClient- , updateItemSlot, fullAssocsClient, activeItemsClient- , itemToFullClient, pickWeaponClient, sumOrganEqpClient, getModeClient+ , actorSkillsClient, updateItemSlot, fullAssocsClient, activeItemsClient+ , itemToFullClient, pickWeaponClient, sumOrganEqpClient ) where +import Control.Applicative import Control.Exception.Assert.Sugar+import Control.Monad import qualified Data.EnumMap.Strict as EM-import qualified Data.IntMap.Strict as IM import Data.Maybe-import Data.Text (Text) import Data.Tuple import qualified NLP.Miniutter.English as MU @@ -22,7 +21,6 @@ import qualified Game.LambdaHack.Common.Ability as Ability import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState-import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.ItemStrongest@@ -30,21 +28,22 @@ import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead+import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.Random import Game.LambdaHack.Common.Request import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Vector-import Game.LambdaHack.Content.ModeKind+import qualified Game.LambdaHack.Content.ItemKind as IK -- | Get the current perception of a client. getPerFid :: MonadClient m => LevelId -> m Perception getPerFid lid = do fper <- getsClient sfper- return $! fromMaybe (assert `failure` "no perception at given level"- `twith` (lid, fper))- $ EM.lookup lid fper+ let assFail = assert `failure` "no perception at given level"+ `twith` (lid, fper)+ return $! EM.findWithDefault assFail lid fper -- | The part of speech describing the actor or "you" if a leader -- of the client's faction. The actor may be not present in the dungeon.@@ -107,23 +106,37 @@ -- because the target actor can be obscured by a glass wall -- or be out of sight range, but in weapon range. aidTgtAims :: MonadClient m- => ActorId -> LevelId -> Maybe Target -> m (Either Text Int)+ => ActorId -> LevelId -> Maybe Target -> m (Either Msg Int) aidTgtAims aid lidV tgt = do- oldEps <- getsClient seps+ let findNewEps onlyFirst pos = do+ oldEps <- getsClient seps+ b <- getsState $ getActorBody aid+ mnewEps <- makeLine onlyFirst b pos oldEps+ case mnewEps of+ Just newEps -> return $ Right newEps+ Nothing ->+ return $ Left+ $ if onlyFirst then "aiming blocked at the first step"+ else "aiming line to the opponent blocked somewhere" case tgt of Just (TEnemy a _) -> do body <- getsState $ getActorBody a let pos = bpos body+ if blid body == lidV+ then findNewEps False pos+ else return $ Left "selected opponent not on this level"+ Just TEnemyPos{} -> return $ Left "selected opponent not visible"+ Just (TPoint lid pos) ->+ if lid == lidV+ then findNewEps True pos+ else return $ Left "selected position not on this level"+ Just (TVector v) -> do b <- getsState $ getActorBody aid- if blid b == lidV then do- mnewEps <- makeLine b pos oldEps- case mnewEps of- Just newEps -> return $ Right newEps- Nothing -> return $ Left "aiming line to the opponent blocked"- else return $ Left "target opponent not on this level"- Just TEnemyPos{} -> return $ Left "target opponent not visible"- Just TPoint{} -> return $ Right oldEps- Just TVector{} -> return $ Right oldEps+ Level{lxsize, lysize} <- getLevel lidV+ let shifted = shiftBounded lxsize lysize (bpos b) v+ if shifted == bpos b && v /= Vector 0 0+ then return $ Left "selected translation is void"+ else findNewEps True shifted Nothing -> do scursor <- getsClient scursor aidTgtAims aid lidV $ Just scursor@@ -132,8 +145,8 @@ -- an actor or obstacle. Starts searching with the given eps and returns -- the first found eps for which the number reaches the distance between -- actor and target position, or Nothing if none can be found.-makeLine :: MonadClient m => Actor -> Point -> Int -> m (Maybe Int)-makeLine body fpos epsOld = do+makeLine :: MonadClient m => Bool -> Actor -> Point -> Int -> m (Maybe Int)+makeLine onlyFirst body fpos epsOld = do cops@Kind.COps{cotile=Kind.Ops{ouniqGroup}} <- getsState scops lvl@Level{lxsize, lysize} <- getLevel (blid body) bs <- getsState $ filter (not . bproj)@@ -147,53 +160,64 @@ noActor p = all ((/= p) . bpos) bs || p == fpos accessU = all noActor blDist && all (uncurry $ accessibleUnknown cops lvl) blZip+ accessFirst | not onlyFirst = False+ | otherwise =+ all noActor (take 1 blDist)+ && all (uncurry $ accessibleUnknown cops lvl) (take 1 blZip) nUnknown = length $ filter ((== unknownId) . (lvl `at`)) blDist- in if accessU then - nUnknown else minBound+ in if accessU then - nUnknown+ else if accessFirst then -10000+ else minBound Nothing -> assert `failure` (body, fpos, epsOld)- tryLines curEps (acc, _) | curEps >= epsOld + dist = acc+ tryLines curEps (acc, _) | curEps == epsOld + dist = acc tryLines curEps (acc, bestScore) = let curScore = calcScore curEps newAcc = if curScore > bestScore then (Just curEps, curScore) else (acc, bestScore) in tryLines (curEps + 1) newAcc- return $! if dist <= 1- then Nothing -- ProjectBlockActor, ProjectAimOnself- else tryLines epsOld (Nothing, minBound)+ return $! if dist <= 0 then Nothing -- ProjectAimOnself+ else if calcScore epsOld > minBound then Just epsOld -- keep old+ else tryLines (epsOld + 1) (Nothing, minBound) -- generate best actorSkillsClient :: MonadClient m => ActorId -> m Ability.Skills actorSkillsClient aid = do activeItems <- activeItemsClient aid+ body <- getsState $ getActorBody aid+ fact <- getsState $ (EM.! bfid body) . sfactionD+ side <- getsClient sside -- Newest Leader in _sleader, not yet in sfactionD.- mleader <- getsClient _sleader+ mleader1 <- if side == bfid body then getsClient _sleader else return Nothing+ let mleader2 = fst <$> gleader fact+ mleader = mleader1 `mplus` mleader2 getsState $ actorSkills mleader aid activeItems -maxActorSkillsClient :: MonadClient m- => ActorId -> m Ability.Skills-maxActorSkillsClient aid = do- activeItems <- activeItemsClient aid- getsState $ maxActorSkills aid activeItems--updateItemSlot :: MonadClient m => Maybe ActorId -> ItemId -> m ()-updateItemSlot maid iid = do- slots@(letterSlots, numberSlots) <- getsClient sslots- case ( lookup iid $ map swap $ EM.assocs letterSlots- , lookup iid $ map swap $ IM.assocs numberSlots ) of- (Nothing, Nothing) -> do+updateItemSlot :: MonadClient m+ => CStore -> Maybe ActorId -> ItemId -> m SlotChar+updateItemSlot store maid iid = do+ slots@(itemSlots, organSlots) <- getsClient sslots+ let onlyOrgans = store == COrgan+ lSlots = if onlyOrgans then organSlots else itemSlots+ incrementPrefix m l iid2 = EM.insert l iid2 $+ case EM.lookup l m of+ Nothing -> m+ Just iidOld ->+ let lNew = SlotChar (slotPrefix l + 1) (slotChar l)+ in incrementPrefix m lNew iidOld+ case lookup iid $ map swap $ EM.assocs lSlots of+ Nothing -> do side <- getsClient sside item <- getsState $ getItemBody iid lastSlot <- getsClient slastSlot mb <- maybe (return Nothing) (fmap Just . getsState . getActorBody) maid- el <- getsState $ assignSlot item side mb slots lastSlot- case el of- Left l ->- modifyClient $ \cli ->- cli { sslots = (EM.insert l iid letterSlots, numberSlots)- , slastSlot = max l (slastSlot cli) }- Right l ->- modifyClient $ \cli ->- cli { sslots = (letterSlots, IM.insert l iid numberSlots) }- _ -> return () -- slot already assigned; a letter or a number+ l <- getsState $ assignSlot store item side mb slots lastSlot+ let newSlots | onlyOrgans = ( itemSlots+ , incrementPrefix organSlots l iid )+ | otherwise = ( incrementPrefix itemSlots l iid+ , organSlots )+ modifyClient $ \cli -> cli {sslots = newSlots}+ return l+ Just l -> return l -- slot already assigned; a letter or a number fullAssocsClient :: MonadClient m => ActorId -> [CStore] -> m [(ItemId, ItemFull)]@@ -221,37 +245,33 @@ -- Client has to choose the weapon based on its partial knowledge, -- because if server chose it, it would leak item discovery information. pickWeaponClient :: MonadClient m- => ActorId -> ActorId -> m [RequestTimed Ability.AbMelee]+ => ActorId -> ActorId+ -> m (Maybe (RequestTimed 'Ability.AbMelee)) pickWeaponClient source target = do eqpAssocs <- fullAssocsClient source [CEqp] bodyAssocs <- fullAssocsClient source [COrgan] actorSk <- actorSkillsClient source sb <- getsState $ getActorBody source+ localTime <- getsState $ getLocalTime (blid sb) let allAssocs = eqpAssocs ++ bodyAssocs calm10 = calmEnough10 sb $ map snd allAssocs forced = assert (not $ bproj sb) False- legalPrecious = either (const False) id . permittedPrecious calm10 forced- strongest = strongestSlotNoFilter IK.EqpSlotWeapon allAssocs- strongestLegal = filter (legalPrecious . snd . snd) strongest- case strongestLegal of- _ | EM.findWithDefault 0 Ability.AbMelee actorSk <= 0 -> return []- [] -> return []+ permitted = permittedPrecious calm10 forced+ preferredPrecious = either (const False) id . permitted+ strongest = strongestMelee True localTime allAssocs+ strongestPreferred = filter (preferredPrecious . snd . snd) strongest+ case strongestPreferred of+ _ | EM.findWithDefault 0 Ability.AbMelee actorSk <= 0 -> return Nothing+ [] -> return Nothing iis@((maxS, _) : _) -> do let maxIis = map snd $ takeWhile ((== maxS) . fst) iis- -- TODO: pick the item according to the frequency of its kind. (iid, _) <- rndToAction $ oneOf maxIis -- Prefer COrgan, to hint to the player to trash the equivalent CEqp item. let cstore = if isJust (lookup iid bodyAssocs) then COrgan else CEqp- return $! [ReqMelee target iid cstore]+ return $ Just $ ReqMelee target iid cstore sumOrganEqpClient :: MonadClient m => IK.EqpSlot -> ActorId -> m Int sumOrganEqpClient eqpSlot aid = do activeItems <- activeItemsClient aid return $! sumSlotNoFilter eqpSlot activeItems--getModeClient :: MonadClient m => m ModeKind-getModeClient = do- Kind.COps{comode=Kind.Ops{okind, ouniqGroup}} <- getsState scops- t <- getsClient sgameMode- return $! okind $ ouniqGroup t
Game/LambdaHack/Client/HandleAtomicClient.hs view
@@ -10,7 +10,6 @@ import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Maybe-import qualified Data.Text as T import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Atomic@@ -28,11 +27,9 @@ import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.Perception-import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Content.ItemKind (ItemKind)-import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Content.TileKind as TK -- * RespUpdAtomicAI@@ -42,15 +39,6 @@ -- of commands kept for each command received. cmdAtomicFilterCli :: MonadClient m => UpdAtomic -> m [UpdAtomic] cmdAtomicFilterCli cmd = case cmd of- UpdMoveActor aid _ toP -> do- cmdSml <- deleteSmell aid toP- return $ [cmd] ++ cmdSml- UpdDisplaceActor source target -> do- bs <- getsState $ getActorBody source- bt <- getsState $ getActorBody target- cmdSource <- deleteSmell source (bpos bt)- cmdTarget <- deleteSmell target (bpos bs)- return $ [cmd] ++ cmdSource ++ cmdTarget UpdAlterTile lid p fromTile toTile -> do Kind.COps{cotile=Kind.Ops{okind}} <- getsState scops lvl <- getLevel lid@@ -66,7 +54,7 @@ let subject = "" -- a hack, we we don't handle adverbs well verb = "turn into" msg = makeSentence [ "the", MU.Text $ TK.tname $ okind t- , "at position", MU.Text $ T.pack $ show p+ , "at position", MU.Text $ tshow p , "suddenly" -- adverb , MU.SubjectVerbSg subject verb , MU.AW $ MU.Text $ TK.tname $ okind toTile ]@@ -91,9 +79,7 @@ UpdLearnSecrets aid fromS _toS -> do b <- getsState $ getActorBody aid lvl <- getLevel $ blid b- return $! if lsecret lvl == fromS- then [cmd] -- secrets revealed now- else [] -- secrets already revealed previously+ return $! [cmd | lsecret lvl == fromS] -- secrets not revealed previously UpdSpotTile lid ts -> do Kind.COps{cotile} <- getsState scops lvl <- getLevel lid@@ -107,17 +93,7 @@ || t /= Tile.hideAs cotile tClient) newTs = filter notKnown ts return $! if null newTs then [] else [UpdSpotTile lid newTs]- UpdAlterSmell lid p fromSm _toSm -> do- lvl <- getLevel lid- let msml = EM.lookup p $ lsmell lvl- return $ if msml /= fromSm then- -- Revert to the server smell before server command executes.- -- This is needed due to our hacky removal of traversed smells- -- in @deleteSmell@.- [UpdAlterSmell lid p msml fromSm, cmd]- else- [cmd]- UpdDiscover lid p iid _ seed -> do+ UpdDiscover c iid _ seed -> do itemD <- getsState sitemD case EM.lookup iid itemD of Nothing -> return []@@ -128,9 +104,9 @@ discoEffect <- getsClient sdiscoEffect if iid `EM.member` discoEffect then return []- else return [UpdDiscoverSeed lid p iid seed]+ else return [UpdDiscoverSeed c iid seed] else return [cmd]- UpdCover lid p iid ik _ -> do+ UpdCover c iid ik _ -> do itemD <- getsState sitemD case EM.lookup iid itemD of Nothing -> return []@@ -142,8 +118,8 @@ discoEffect <- getsClient sdiscoEffect if iid `EM.notMember` discoEffect then return [cmd]- else return [UpdCoverKind lid p iid ik]- UpdDiscoverKind _ _ iid _ -> do+ else return [UpdCoverKind c iid ik]+ UpdDiscoverKind _ iid _ -> do itemD <- getsState sitemD case EM.lookup iid itemD of Nothing -> return []@@ -152,7 +128,7 @@ if jkindIx item `EM.notMember` discoKind then return [] else return [cmd]- UpdCoverKind _ _ iid _ -> do+ UpdCoverKind _ iid _ -> do itemD <- getsState sitemD case EM.lookup iid itemD of Nothing -> return []@@ -161,7 +137,7 @@ if jkindIx item `EM.notMember` discoKind then return [] else return [cmd]- UpdDiscoverSeed _ _ iid _ -> do+ UpdDiscoverSeed _ iid _ -> do itemD <- getsState sitemD case EM.lookup iid itemD of Nothing -> return []@@ -174,7 +150,7 @@ if iid `EM.member` discoEffect then return [] else return [cmd]- UpdCoverSeed _ _ iid _ -> do+ UpdCoverSeed _ iid _ -> do itemD <- getsState sitemD case EM.lookup iid itemD of Nothing -> return []@@ -234,29 +210,18 @@ -- TODO: these assertions are probably expensive psActor <- mapM posUpdAtomic outActor -- Verify that we forget only previously seen actors.- assert (allB seenOld psActor) skip+ let !_A = assert (allB seenOld psActor) () -- Verify that we forget only currently invisible actors.- assert (allB (not . seenNew) psActor) skip+ let !_A = assert (allB (not . seenNew) psActor) () let inTileSmell = inFloor ++ inEmbed ++ inSmell psItemSmell <- mapM posUpdAtomic inTileSmell -- Verify that we forget only previously invisible items and smell.- assert (allB (not . seenOld) psItemSmell) skip+ let !_A = assert (allB (not . seenOld) psItemSmell) () -- Verify that we forget only currently seen items and smell.- assert (allB seenNew psItemSmell) skip+ let !_A = assert (allB seenNew psItemSmell) () return $! cmd : outActor ++ inTileSmell _ -> return [cmd] -deleteSmell :: MonadClient m => ActorId -> Point -> m [UpdAtomic]-deleteSmell aid pos = do- b <- getsState $ getActorBody aid- smellRadius <- sumOrganEqpClient IK.EqpSlotAddSmell aid- if smellRadius <= 0 then return []- else do- lvl <- getLevel $ blid b- let msml = EM.lookup pos $ lsmell lvl- return $- maybe [] (\sml -> [UpdAlterSmell (blid b) pos (Just sml) Nothing]) msml- -- | Effect of atomic actions on client state is calculated -- in the global state before the command is executed. cmdAtomicSemCli :: MonadClient m => UpdAtomic -> m ()@@ -269,9 +234,10 @@ side <- getsClient sside when (side == fid) $ do mleader <- getsClient _sleader- assert (mleader == fmap fst source -- somebody changed the leader for us- || mleader == fmap fst target -- we changed the leader ourselves- `blame` "unexpected leader" `twith` (cmd, mleader)) skip+ let !_A = assert (mleader == fmap fst source -- somebody changed the leader for us+ || mleader == fmap fst target -- we changed the leader ourselves+ `blame` "unexpected leader"+ `twith` (cmd, mleader)) () modifyClient $ \cli -> cli {_sleader = fmap fst target} case target of Nothing -> return ()@@ -289,18 +255,18 @@ cli { stargetD = case (mtgt, mleader) of (Just tgt, Just leader) -> EM.singleton leader tgt _ -> EM.empty }- UpdDiscover lid p iid ik seed -> do- discoverKind lid p iid ik- discoverSeed lid p iid seed- UpdCover lid p iid ik seed -> do- coverSeed lid p iid seed- coverKind lid p iid ik- UpdDiscoverKind lid p iid ik -> discoverKind lid p iid ik- UpdCoverKind lid p iid ik -> coverKind lid p iid ik- UpdDiscoverSeed lid p iid seed -> discoverSeed lid p iid seed- UpdCoverSeed lid p iid seed -> coverSeed lid p iid seed+ UpdDiscover c iid ik seed -> do+ discoverKind c iid ik+ discoverSeed c iid seed+ UpdCover c iid ik seed -> do+ coverSeed c iid seed+ coverKind c iid ik+ UpdDiscoverKind c iid ik -> discoverKind c iid ik+ UpdCoverKind c iid ik -> coverKind c iid ik+ UpdDiscoverSeed c iid seed -> discoverSeed c iid seed+ UpdCoverSeed c iid seed -> coverSeed c iid seed UpdPerception lid outPer inPer -> perception lid outPer inPer- UpdRestart side sdiscoKind sfper _ sdebugCli sgameMode -> do+ UpdRestart side sdiscoKind sfper _ sdebugCli -> do shistory <- getsClient shistory sreport <- getsClient sreport isAI <- getsClient sisAI@@ -309,7 +275,6 @@ , sfper -- , sundo = [UpdAtomic cmd] , scurDifficulty = sdifficultyCli sdebugCli- , sgameMode , sdebugCli } UpdResume _fid sfper -> modifyClient $ \cli -> cli {sfper} UpdKillExit _fid -> killExit@@ -368,26 +333,26 @@ modifyClient $ \cli -> cli {sfper = f (sfper cli)} discoverKind :: MonadClient m- => LevelId -> Point -> ItemId -> Kind.Id ItemKind -> m ()-discoverKind lid p iid ik = do+ => Container -> ItemId -> Kind.Id ItemKind -> m ()+discoverKind c iid ik = do item <- getsState $ getItemBody iid let f Nothing = Just ik f Just{} = assert `failure` "already discovered"- `twith` (lid, p, iid, ik)+ `twith` (c, iid, ik) modifyClient $ \cli -> cli {sdiscoKind = EM.alter f (jkindIx item) (sdiscoKind cli)} coverKind :: MonadClient m- => LevelId -> Point -> ItemId -> Kind.Id ItemKind -> m ()-coverKind lid p iid ik = do+ => Container -> ItemId -> Kind.Id ItemKind -> m ()+coverKind c iid ik = do item <- getsState $ getItemBody iid- let f Nothing = assert `failure` "already covered" `twith` (lid, p, iid, ik)+ let f Nothing = assert `failure` "already covered" `twith` (c, iid, ik) f (Just ik2) = assert (ik == ik2 `blame` "unexpected covered item kind" `twith` (ik, ik2)) Nothing modifyClient $ \cli -> cli {sdiscoKind = EM.alter f (jkindIx item) (sdiscoKind cli)} discoverSeed :: MonadClient m- => LevelId -> Point -> ItemId -> ItemSeed -> m ()-discoverSeed lid p iid seed = do+ => Container -> ItemId -> ItemSeed -> m ()+discoverSeed c iid seed = do Kind.COps{coitem=Kind.Ops{okind}} <- getsState scops discoKind <- getsClient sdiscoKind item <- getsState $ getItemBody iid@@ -395,18 +360,18 @@ totalDepth <- getsState stotalDepth case EM.lookup (jkindIx item) discoKind of Nothing -> assert `failure` "kind not known"- `twith` (lid, p, iid, seed)+ `twith` (c, iid, seed) Just ik -> do let kind = okind ik f Nothing = Just $ seedToAspectsEffects seed kind ldepth totalDepth f Just{} = assert `failure` "already discovered"- `twith` (lid, p, iid, seed)+ `twith` (c, iid, seed) modifyClient $ \cli -> cli {sdiscoEffect = EM.alter f iid (sdiscoEffect cli)} coverSeed :: MonadClient m- => LevelId -> Point -> ItemId -> ItemSeed -> m ()-coverSeed lid p iid ik = do- let f Nothing = assert `failure` "already covered" `twith` (lid, p, iid, ik)+ => Container -> ItemId -> ItemSeed -> m ()+coverSeed c iid ik = do+ let f Nothing = assert `failure` "already covered" `twith` (c, iid, ik) f Just{} = Nothing -- checking that old and new agree is too much work modifyClient $ \cli -> cli {sdiscoEffect = EM.alter f iid (sdiscoEffect cli)}
Game/LambdaHack/Client/HandleResponseClient.hs view
@@ -4,8 +4,6 @@ ( handleResponseAI, handleResponseUI ) where -import Control.Exception.Assert.Sugar- import Game.LambdaHack.Atomic import Game.LambdaHack.Client.AI import Game.LambdaHack.Client.HandleAtomicClient@@ -19,8 +17,8 @@ storeUndo :: MonadClient m => CmdAtomic -> m () storeUndo _atomic =- maybe skip (\a -> modifyClient $ \cli -> cli {sundo = a : sundo cli})- $ Nothing -- TODO: undoCmdAtomic atomic+ maybe (return ()) (\a -> modifyClient $ \cli -> cli {sundo = a : sundo cli})+ Nothing -- TODO: undoCmdAtomic atomic handleResponseAI :: (MonadAtomic m, MonadClientWriteRequest RequestAI m) => ResponseAI -> m ()
Game/LambdaHack/Client/ItemSlot.hs view
@@ -6,14 +6,15 @@ , allSlots, slotLabel, slotRange, assignSlot ) where +import Control.Exception.Assert.Sugar import Data.Binary+import Data.Bits (shiftL, shiftR) import Data.Char import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES-import qualified Data.IntMap.Strict as IM import Data.List-import Data.Maybe import Data.Monoid+import Data.Ord (comparing) import Data.Text (Text) import qualified Data.Text as T import qualified NLP.Miniutter.English as MU@@ -25,17 +26,27 @@ import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.State -newtype SlotChar = SlotChar {slotChar :: Char}- deriving (Show, Eq, Binary)+data SlotChar = SlotChar {slotPrefix :: Int, slotChar :: Char}+ deriving (Show, Eq) instance Ord SlotChar where- compare x y = compare (fromEnum x) (fromEnum y)+ compare = comparing fromEnum +instance Binary SlotChar where+ put = put . fromEnum+ get = fmap toEnum get+ instance Enum SlotChar where- fromEnum (SlotChar x) = fromEnum x + if isUpper x then 1000 else 0- toEnum e = SlotChar $ toEnum $ e - (if e > 1000 then 1000 else 0)+ fromEnum (SlotChar n c) =+ ord c + (if isUpper c then 100 else 0) + shiftL n 8+ toEnum e =+ let n = shiftR e 8+ c0 = e - shiftL n 8+ c100 = c0 - if c0 > 150 then 100 else 0+ in SlotChar n (chr c100) -type ItemSlots = (EM.EnumMap SlotChar ItemId, IM.IntMap ItemId)+type ItemSlots = ( EM.EnumMap SlotChar ItemId+ , EM.EnumMap SlotChar ItemId ) slotRange :: [SlotChar] -> Text slotRange ls =@@ -58,36 +69,40 @@ , slotChar d ] | otherwise = T.pack [slotChar c, '-', slotChar d] -allSlots :: [SlotChar]-allSlots = map SlotChar $ ['a'..'z'] ++ ['A'..'Z']+allSlots :: Int -> [SlotChar]+allSlots n = map (SlotChar n) $ ['a'..'z'] ++ ['A'..'Z'] +allZeroSlots :: [SlotChar]+allZeroSlots = allSlots 0+ -- | Assigns a slot to an item, for inclusion in the inventory or equipment -- of a hero. Tries to to use the requested slot, if any.-assignSlot :: Item -> FactionId -> Maybe Actor -> ItemSlots -> SlotChar- -> State- -> Either SlotChar Int-assignSlot item fid mbody (letterSlots, numberSlots) lastSlot s =- if jsymbol item == '$'- then Left $ SlotChar '$'- else case free of- freeChar : _ -> Left freeChar- [] -> Right $ head freeNumbers+assignSlot :: CStore -> Item -> FactionId -> Maybe Actor -> ItemSlots+ -> SlotChar -> State+ -> SlotChar+assignSlot store item fid mbody (itemSlots, organSlots) lastSlot s =+ assert (maybe True (\b -> bfid b == fid) mbody)+ $ if jsymbol item == '$'+ then SlotChar 0 '$'+ else head $ fresh ++ free where- candidates = take (length allSlots)- $ drop (1 + fromJust (elemIndex lastSlot allSlots))- $ cycle allSlots- onPerson = maybe (sharedAllOwnedFid True fid s)- (\body -> sharedAllOwned True body s)+ offset = maybe 0 (+1) (elemIndex lastSlot allZeroSlots)+ onlyOrgans = store == COrgan+ len0 = length allZeroSlots+ candidatesZero = take len0 $ drop offset $ cycle allZeroSlots+ candidates = candidatesZero ++ concat [allSlots n | n <- [1..]]+ onPerson = sharedAllOwnedFid onlyOrgans fid s+ onGround = maybe EM.empty -- consider floor only under the acting actor+ (\b -> getCBag (CFloor (blid b) (bpos b)) s) mbody- onGroud = maybe EM.empty- (\b -> getCBag (CFloor (blid b) (bpos b)) s)- mbody- inBags = ES.unions $ map EM.keysSet [onPerson, onGroud]- f l = maybe True (`ES.notMember` inBags) $ EM.lookup l letterSlots+ inBags = ES.unions $ map EM.keysSet $ onPerson : [ onGround | not onlyOrgans]+ lSlots = if onlyOrgans then organSlots else itemSlots+ f l = maybe True (`ES.notMember` inBags) $ EM.lookup l lSlots free = filter f candidates- g l = maybe True (`ES.notMember` inBags) $ IM.lookup l numberSlots- freeNumbers = filter g [0..]+ g l = l `EM.notMember` lSlots+ fresh = filter g $ take ((slotPrefix lastSlot + 1) * len0) candidates -slotLabel :: Either SlotChar Int -> MU.Part-slotLabel (Left c) = MU.String [slotChar c]-slotLabel Right{} = "0"+slotLabel :: SlotChar -> MU.Part+slotLabel x = MU.String+ $ (if slotPrefix x == 0 then [] else show $ slotPrefix x)+ ++ [slotChar x]
Game/LambdaHack/Client/Key.hs view
@@ -3,10 +3,11 @@ module Game.LambdaHack.Client.Key ( Key(..), showKey, handleDir, dirAllKey , moveBinding, mkKM, keyTranslate- , Modifier(..), KM(..), showKM- , escKM, spaceKM, returnKM, pgupKM, pgdnKM+ , Modifier(..), KM(..), toKM, showKM+ , escKM, spaceKM, returnKM, pgupKM, pgdnKM, leftButtonKM, rightButtonKM ) where +import Control.DeepSeq import Control.Exception.Assert.Sugar import Data.Binary import qualified Data.Char as Char@@ -16,6 +17,7 @@ import Prelude hiding (Left, Right) import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.Vector -- | Frontend-independent datatype to represent keys.@@ -39,30 +41,45 @@ | Home | KP !Char -- ^ a keypad key for a character (digits and operators) | Char !Char -- ^ a single printable character+ | LeftButtonPress -- ^ left mouse button pressed+ | MiddleButtonPress -- ^ middle mouse button pressed+ | RightButtonPress -- ^ right mouse button pressed | Unknown !Text -- ^ an unknown key, registered to warn the user deriving (Read, Ord, Eq, Generic) instance Binary Key +instance NFData Key+ -- | Our own encoding of modifiers. Incomplete. data Modifier = NoModifier+ | Shift | Control+ | Alt deriving (Read, Ord, Eq, Generic) instance Binary Modifier -data KM = KM {modifier :: !Modifier, key :: !Key}+instance NFData Modifier++data KM = KM { key :: !Key+ , modifier :: !Modifier+ , pointer :: !Point } deriving (Read, Ord, Eq, Generic) +instance NFData KM+ instance Show KM where show = T.unpack . showKM instance Binary KM +toKM :: Modifier -> Key -> KM+toKM modifier key = KM{pointer=dummyPoint, ..}+ -- Common and terse names for keys. showKey :: Key -> Text-showKey (Char c) = T.singleton c showKey Esc = "ESC" showKey Return = "RET" showKey Space = "SPACE"@@ -81,28 +98,40 @@ showKey Insert = "INSERT" showKey Delete = "DELETE" showKey (KP c) = "KEYPAD_" <> T.singleton c+showKey (Char c) = T.singleton c+showKey LeftButtonPress = "LEFT-BUTTON"+showKey MiddleButtonPress = "MIDDLE-BUTTON"+showKey RightButtonPress = "RIGHT-BUTTON" showKey (Unknown s) = s -- | Show a key with a modifier, if any. showKM :: KM -> Text+showKM KM{modifier=Shift, key} = "SHIFT-" <> showKey key showKM KM{modifier=Control, key} = "CTRL-" <> showKey key+showKM KM{modifier=Alt, key} = "ALT-" <> showKey key showKM KM{modifier=NoModifier, key} = showKey key escKM :: KM-escKM = KM {modifier = NoModifier, key = Esc}+escKM = toKM NoModifier Esc spaceKM :: KM-spaceKM = KM {modifier = NoModifier, key = Space}+spaceKM = toKM NoModifier Space returnKM :: KM-returnKM = KM {modifier = NoModifier, key = Return}+returnKM = toKM NoModifier Return pgupKM :: KM-pgupKM = KM {modifier = NoModifier, key = PgUp}+pgupKM = toKM NoModifier PgUp pgdnKM :: KM-pgdnKM = KM {modifier = NoModifier, key = PgDn}+pgdnKM = toKM NoModifier PgDn +leftButtonKM :: KM+leftButtonKM = toKM NoModifier LeftButtonPress++rightButtonKM :: KM+rightButtonKM = toKM NoModifier RightButtonPress+ dirKeypadKey :: [Key] dirKeypadKey = [Home, Up, PgUp, Right, PgDn, Down, End, Left] @@ -142,7 +171,7 @@ dirRunControl :: [Key] dirRunControl = dirKeypadKey ++ dirKeypadShiftKey- ++ (map Char dirKeypadShiftChar)+ ++ map Char dirKeypadShiftChar dirAllKey :: Bool -> Bool -> [Key] dirAllKey configVi configLaptop =@@ -164,9 +193,9 @@ moveBinding configVi configLaptop move run = let assign f (km, dir) = (km, f dir) mapMove modifier keys =- map (assign move) (zip (zipWith KM (repeat modifier) keys) moves)+ map (assign move) (zip (map (toKM modifier) keys) $ cycle moves) mapRun modifier keys =- map (assign run) (zip (zipWith KM (repeat modifier) keys) moves)+ map (assign run) (zip (map (toKM modifier) keys) $ cycle moves) in mapMove NoModifier (dirMoveNoModifier configVi configLaptop) ++ mapRun NoModifier (dirRunNoModifier configVi configLaptop) ++ mapRun Control dirRunControl@@ -174,12 +203,13 @@ mkKM :: String -> KM mkKM s = let mkKey sk = case keyTranslate sk of- Unknown _ ->- assert `failure` "unknown key" `twith` s+ Unknown _ -> assert `failure` "unknown key" `twith` s key -> key in case s of- ('C':'T':'R':'L':'-':rest) -> KM {key=mkKey rest, modifier=Control}- _ -> KM {key=mkKey s, modifier=NoModifier}+ ('S':'H':'I':'F':'T':'-':rest) -> toKM Shift (mkKey rest)+ ('C':'T':'R':'L':'-':rest) -> toKM Control (mkKey rest)+ ('A':'L':'T':'-':rest) -> toKM Alt (mkKey rest)+ _ -> toKM NoModifier (mkKey s) -- | Translate key from a GTK string description to our internal key type. -- To be used, in particular, for the command bindings and macros@@ -198,7 +228,8 @@ keyTranslate "asterisk" = Char '*' keyTranslate "KP_Multiply" = KP '*' keyTranslate "slash" = Char '/'-keyTranslate "KP_Divide" = Char '/'+keyTranslate "KP_Divide" = KP '/'+keyTranslate "bar" = Char '|' keyTranslate "backslash" = Char '\\' keyTranslate "underscore" = Char '_' keyTranslate "minus" = Char '-'@@ -211,6 +242,9 @@ keyTranslate "braceleft" = Char '{' keyTranslate "braceright" = Char '}' keyTranslate "ampersand" = Char '&'+keyTranslate "at" = Char '@'+keyTranslate "asciitilde" = Char '~'+keyTranslate "exclam" = Char '!' keyTranslate "apostrophe" = Char '\'' keyTranslate "Escape" = Esc keyTranslate "Return" = Return@@ -249,6 +283,9 @@ keyTranslate "Delete" = Delete keyTranslate "KP_Delete" = Delete keyTranslate "KP_Enter" = Return+keyTranslate "LeftButtonPress" = LeftButtonPress+keyTranslate "MiddleButtonPress" = MiddleButtonPress+keyTranslate "RightButtonPress" = RightButtonPress keyTranslate ['K','P','_',c] = KP c keyTranslate [c] = Char c keyTranslate s = Unknown $ T.pack s
Game/LambdaHack/Client/LoopClient.hs view
@@ -9,7 +9,6 @@ import qualified Data.Text as T import Game.LambdaHack.Atomic-import Game.LambdaHack.Client.CommonClient import Game.LambdaHack.Client.HandleResponseClient import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.ProtocolClient@@ -46,6 +45,7 @@ Nothing -> return () return False +-- | The main game loop for an AI client. loopAI :: ( MonadAtomic m , MonadClientReadResponse ResponseAI m , MonadClientWriteRequest RequestAI m )@@ -77,6 +77,7 @@ quit <- getsClient squit unless quit loop +-- | The main game loop for a UI client. loopUI :: ( MonadClientUI m , MonadAtomic m , MonadClientReadResponse ResponseUI m@@ -91,7 +92,7 @@ cmd1 <- receiveResponse case (restored, cmd1) of (True, RespUpdAtomicUI UpdResume{}) -> do- mode <- getModeClient+ mode <- getGameMode msgAdd $ mdesc mode handleResponseUI cmd1 (True, RespUpdAtomicUI UpdRestart{}) -> do
Game/LambdaHack/Client/State.hs view
@@ -11,7 +11,6 @@ import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES-import qualified Data.IntMap.Strict as IM import Data.Text (Text) import qualified Data.Text as T import qualified NLP.Miniutter.English as MU@@ -36,7 +35,6 @@ import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Vector-import Game.LambdaHack.Content.ModeKind -- | Client state, belonging to a single faction. -- Some of the data, e.g, the history, carries over@@ -52,7 +50,7 @@ , sexplored :: !(ES.EnumSet LevelId) -- ^ the set of fully explored levels , sbfsD :: !(EM.EnumMap ActorId- ( PointArray.Array BfsDistance+ ( Bool, PointArray.Array BfsDistance , Point, Int, Maybe [Point]) ) -- ^ pathfinding distances for our actors -- and paths to their targets, if any@@ -69,6 +67,7 @@ , sdiscoEffect :: !DiscoveryEffect -- ^ remembered effects&Co of items , sfper :: !FactionPers -- ^ faction perception indexed by levels , srandom :: !R.StdGen -- ^ current random generator+ , slastKM :: !K.KM -- ^ last issued key command , slastRecord :: !LastRecord -- ^ state of key sequence recording , slastPlay :: ![K.KM] -- ^ state of key sequence playback , slastLost :: !(ES.EnumSet ActorId)@@ -85,7 +84,7 @@ , scurDifficulty :: !Int -- ^ current game difficulty level , sslots :: !ItemSlots -- ^ map from slots to items , slastSlot :: !SlotChar -- ^ last used slot- , sgameMode :: !(GroupName ModeKind) -- ^ current game mode+ , slastStore :: !CStore -- ^ last used store , sescAI :: !EscAI -- ^ just canceled AI control with ESC , sdebugCli :: !DebugModeCli -- ^ client debugging mode }@@ -101,10 +100,10 @@ data RunParams = RunParams { runLeader :: !ActorId -- ^ the original leader from run start , runMembers :: ![ActorId] -- ^ the list of actors that take part- , runDist :: !Int -- ^ distance of the run so far- -- (plus one, if multiple runners)+ , runInitial :: !Bool -- ^ initial run continuation by any+ -- run participant, including run leader , runStopMsg :: !(Maybe Text) -- ^ message with the next stop reason- , runInitDir :: !(Maybe Vector) -- ^ the direction of the initial step+ , runWaiting :: !Int -- ^ waiting for others to move out of the way } deriving (Show) @@ -138,6 +137,7 @@ , sdiscoEffect = EM.empty , sfper = EM.empty , srandom = R.mkStdGen 42 -- will be set later+ , slastKM = K.escKM , slastRecord = ([], [], 0) , slastPlay = [] , slastLost = ES.empty@@ -150,9 +150,9 @@ , smarkSmell = True , smarkSuspect = False , scurDifficulty = difficultyDefault- , sslots = (EM.empty, IM.empty)- , slastSlot = SlotChar 'a'- , sgameMode = "starting"+ , sslots = (EM.empty, EM.empty)+ , slastSlot = SlotChar 0 'Z'+ , slastStore = CInv , sescAI = EscAINothing , sdebugCli = defDebugModeCli }@@ -224,7 +224,7 @@ put scurDifficulty put sslots put slastSlot- put sgameMode+ put slastStore put sdebugCli -- TODO: this is overwritten at once get = do stgtMode <- get@@ -250,11 +250,12 @@ scurDifficulty <- get sslots <- get slastSlot <- get- sgameMode <- get+ slastStore <- get sdebugCli <- get let sbfsD = EM.empty sfper = EM.empty srandom = read g+ slastKM = K.escKM slastRecord = ([], [], 0) slastPlay = [] slastLost = ES.empty@@ -267,13 +268,13 @@ put RunParams{..} = do put runLeader put runMembers- put runDist+ put runInitial put runStopMsg- put runInitDir+ put runWaiting get = do runLeader <- get runMembers <- get- runDist<- get+ runInitial <- get runStopMsg <- get- runInitDir <- get+ runWaiting <- get return $! RunParams{..}
Game/LambdaHack/Client/UI.hs view
@@ -13,6 +13,7 @@ -- * Operations exposed for LoopClient , ColorMode(..), displayMore, msgAdd #ifdef EXPOSE_INTERNAL+ -- * Internal operations , humanCommand #endif ) where@@ -28,7 +29,6 @@ import qualified Game.LambdaHack.Client.Key as K import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State-import Game.LambdaHack.Client.UI.Config import Game.LambdaHack.Client.UI.Content.KeyKind import Game.LambdaHack.Client.UI.DisplayAtomicClient import Game.LambdaHack.Client.UI.HandleHumanClient@@ -36,12 +36,13 @@ import Game.LambdaHack.Client.UI.KeyBindings import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.MsgClient-import Game.LambdaHack.Client.UI.RunClient import Game.LambdaHack.Client.UI.StartupFrontendClient import Game.LambdaHack.Client.UI.WidgetClient import Game.LambdaHack.Common.Faction+import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.Request import Game.LambdaHack.Common.State import Game.LambdaHack.Content.ModeKind@@ -52,56 +53,30 @@ side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD let (leader, mtgt) = fromMaybe (assert `failure` fact) $ gleader fact- srunning <- getsClient srunning- -- When running, stop if disturbed. If not running, let the human- -- player issue commands, until any command takes time.- req <- case srunning of- Nothing -> humanCommand Nothing- Just RunParams{runMembers}- | noRunWithMulti fact && runMembers /= [leader] -> do- stopRunning- Config{configRunStopMsgs} <- askConfig- let msg = if configRunStopMsgs- then Just $ "Run stop: automatic leader change"- else Nothing- humanCommand msg- Just runParams -> do- runOutcome <- continueRun runParams- case runOutcome of- Left stopMsg -> do- stopRunning- Config{configRunStopMsgs} <- askConfig- let msg = if configRunStopMsgs- then Just $ "Run stop:" <+> stopMsg- else Nothing- humanCommand msg- Right (paramNew, runCmd) -> do- modifyClient $ \cli -> cli {srunning = Just paramNew}- displayPush- return $! anyToUI $ runCmd+ req <- humanCommand leader2 <- getLeaderUI mtgt2 <- getsClient $ fmap fst . EM.lookup leader2 . stargetD if (leader2, mtgt2) /= (leader, mtgt) then return $! ReqUILeader leader2 mtgt2 req else return $! req --- | Determine and process the next human player command. The argument is--- the last stop message due to running, if any.-humanCommand :: forall m. MonadClientUI m- => Maybe Msg -> m RequestUI-humanCommand msgRunStop = do+-- | Let the human player issue commands until any command takes time.+humanCommand :: forall m. MonadClientUI m => m RequestUI+humanCommand = do -- For human UI we invalidate whole @sbfsD@ at the start of each- -- UI player input, which is an overkill, but doesn't affects- -- screensavers, because they are UI, but not human.+ -- UI player input that start a player move, which is an overkill,+ -- but doesn't slow screensavers, because they are UI,+ -- but not human. modifyClient $ \cli -> cli {sbfsD = EM.empty, slastLost = ES.empty}- let loop :: Maybe (Bool, Overlay) -> m RequestUI+ let loop :: Either Bool (Maybe Bool, Overlay) -> m RequestUI loop mover = do (lastBlank, over) <- case mover of- Nothing -> do- -- Display current state if no slideshow or if interrupted.- sli <- promptToSlideshow ""- return (False, head . snd $! slideshow sli)- Just bLast ->+ Left b -> do+ -- Display current state and keys if no slideshow or if interrupted.+ keys <- if b then describeMainKeys else return ""+ sli <- promptToSlideshow keys+ return (Nothing, head . snd $! slideshow sli)+ Right bLast -> -- (Re-)display the last slide while waiting for the next key. return bLast (seqCurrent, seqPrevious, k) <- getsClient slastRecord@@ -112,16 +87,16 @@ _ -> do let slastRecord = ([], seqCurrent ++ seqPrevious, k - 1) modifyClient $ \cli -> cli {slastRecord}+ lastPlay <- getsClient slastPlay km <- getKeyOverlayCommand lastBlank over -- Messages shown, so update history and reset current report.- recordHistory+ when (null lastPlay) recordHistory abortOrCmd <- do -- Look up the key. Binding{bcmdMap} <- askBinding- case M.lookup km bcmdMap of+ case M.lookup km{K.pointer=dummyPoint} bcmdMap of Just (_, _, cmd) -> do -- Query and clear the last command key.- stgtMode <- getsClient stgtMode modifyClient $ \cli -> cli {swaitTimes = if swaitTimes cli > 0 then - swaitTimes cli@@ -137,11 +112,12 @@ cmdHumanSem cmd _ -> do modifyClient $ \cli -> cli {sescAI = EscAINothing}- if km == K.escKM && isNothing stgtMode && isJust mover+ stgtMode <- getsClient stgtMode+ if km == K.escKM && isNothing stgtMode && isRight mover then cmdHumanSem Clear else cmdHumanSem cmd Nothing -> let msgKey = "unknown command <" <> K.showKM km <> ">"- in fmap Left $ promptToSlideshow msgKey+ in failWith msgKey -- The command was failed or successful and if the latter, -- possibly took some time. case abortOrCmd of@@ -154,23 +130,21 @@ -- Analyse the obtained slides. let (onBlank, sli) = slideshow slides mLast <- case sli of- [] -> return Nothing+ [] -> do+ stgtMode <- getsClient stgtMode+ return $ Left $ isJust stgtMode || km == K.escKM [sLast] -> -- Avoid displaying the single slide twice.- return $ Just (onBlank, sLast)+ return $ Right (onBlank, sLast) _ -> do -- Show, one by one, all slides, awaiting confirmation -- for all but the last one (which is displayed twice, BTW). -- Note: the code that generates the slides is responsible -- for inserting the @more@ prompt. go <- getInitConfirms ColorFull [km] slides- return $! if go then Just (onBlank, last sli) else Nothing+ return $! if go then Right (onBlank, last sli) else Left True loop mLast- case msgRunStop of- Nothing -> loop Nothing- Just msg -> do- sli <- promptToSlideshow msg- loop $ Just (False, head . snd $ slideshow sli)+ loop $ Left False -- | Client signals to the server that it's still online, flushes frames -- (if needed) and sends some extra info.
Game/LambdaHack/Client/UI/Animation.hs view
@@ -4,7 +4,8 @@ ( SingleFrame(..), decodeLine, encodeLine , overlayOverlay , Animation, Frames, renderAnim, restrictAnim- , twirlSplash, blockHit, blockMiss, deathBody, swapPlaces, fadeout+ , twirlSplash, blockHit, blockMiss, deathBody, actorX+ , swapPlaces, moveProj, fadeout ) where import Control.Exception.Assert.Sugar@@ -84,7 +85,7 @@ let fLine y lineOld = let f l (x, acOld) = let pos = Point x y- !ac = fromMaybe acOld $ EM.lookup pos am+ !ac = EM.findWithDefault acOld pos am in ac : l in foldl' f [] (zip [lxsize-1,lxsize-2..0] (reverse lineOld)) sfLevel = -- fully evaluated inside@@ -93,13 +94,13 @@ $ foldl' f [] (zip [lysize-1,lysize-2..0] $ reverse $ map decodeLine levelOld) in Just SingleFrame{..} -- a thunk within Just- in Nothing : map (modifyFrame basicFrame) anim ++ [Nothing]+ in map (modifyFrame basicFrame) anim blank :: Maybe AttrChar blank = Nothing -coloredSymbol :: Color -> Char -> Maybe AttrChar-coloredSymbol color symbol = Just $ AttrChar (Attr color defBG) symbol+cSym :: Color -> Char -> Maybe AttrChar+cSym color symbol = Just $ AttrChar (Attr color defBG) symbol mzipPairs :: (Point, Point) -> (Maybe AttrChar, Maybe AttrChar) -> [(Point, AttrChar)]@@ -111,6 +112,13 @@ -- not the action. [mzip (p1, mattr1)] +mzipTriples :: (Point, Point, Point)+ -> (Maybe AttrChar, Maybe AttrChar, Maybe AttrChar)+ -> [(Point, AttrChar)]+mzipTriples (p1, p2, p3) (mattr1, mattr2, mattr3) =+ let mzip (pos, mattr) = fmap (\x -> (pos, x)) mattr+ in catMaybes [mzip (p1, mattr1), mzip (p2, mattr2), mzip (p3, mattr3)]+ restrictAnim :: ES.EnumSet Point -> Animation -> Animation restrictAnim vis (Animation as) = let f imap =@@ -118,71 +126,122 @@ in if EM.null common then Nothing else Just common in Animation $ mapMaybe f as +-- TODO: in all but moveProj duplicate first and/or last frame, if required,+-- since they are no longer duplicated in renderAnim+ -- | Attack animation. A part of it also reused for self-damage and healing. twirlSplash :: (Point, Point) -> Color -> Color -> Animation twirlSplash poss c1 c2 = Animation $ map (EM.fromList . mzipPairs poss)- [ (blank , coloredSymbol BrCyan '\'')- , (blank , coloredSymbol BrYellow '^')- , (coloredSymbol c1 '/', coloredSymbol BrCyan '^')- , (coloredSymbol c1 '-', blank)- , (coloredSymbol c1 '\\',blank)- , (coloredSymbol c1 '|', blank)- , (coloredSymbol c2 '%', blank)- , (coloredSymbol c2 '/', blank)+ [ (blank , cSym BrCyan '\'')+ , (blank , cSym BrYellow '\'')+ , (blank , cSym BrYellow '^')+ , (cSym c1 '\\',cSym BrCyan '^')+ , (cSym c1 '|', cSym BrCyan '^')+ , (cSym c1 '%', blank)+ , (cSym c1 '/', blank)+ , (cSym c1 '-', blank)+ , (cSym c1 '\\',blank)+ , (cSym c2 '|', blank)+ , (cSym c2 '%', blank)+ , (cSym c2 '%', blank)+ , (cSym c2 '/', blank) ] -- | Attack that hits through a block. blockHit :: (Point, Point) -> Color -> Color -> Animation blockHit poss c1 c2 = Animation $ map (EM.fromList . mzipPairs poss)- [ (blank , coloredSymbol BrCyan '\'')- , (blank , coloredSymbol BrYellow '^')- , (blank , coloredSymbol BrCyan '^')- , (coloredSymbol BrBlue '{', coloredSymbol BrYellow '\'')- , (coloredSymbol BrBlue '{', blank)- , (coloredSymbol BrBlue '}', blank)- , (coloredSymbol BrBlue '}', blank)- , (coloredSymbol c1 '\\',blank)- , (coloredSymbol c1 '|', blank)- , (coloredSymbol c2 '%', blank)- , (coloredSymbol c2 '/', blank)+ [ (blank , cSym BrCyan '\'')+ , (blank , cSym BrYellow '\'')+ , (blank , cSym BrYellow '^')+ , (blank , cSym BrCyan '^')+ , (cSym BrBlue '{', cSym BrCyan '\'')+ , (cSym BrBlue '{', cSym BrYellow '\'')+ , (cSym BrBlue '{', cSym BrYellow '\'')+ , (cSym BrBlue '}', blank)+ , (cSym BrBlue '}', blank)+ , (cSym BrBlue '}', blank)+ , (cSym c1 '\\',blank)+ , (cSym c1 '|', blank)+ , (cSym c1 '/', blank)+ , (cSym c1 '-', blank)+ , (cSym c2 '\\',blank)+ , (cSym c2 '|', blank)+ , (cSym c2 '/', blank) ] -- | Attack that is blocked. blockMiss :: (Point, Point) -> Animation blockMiss poss = Animation $ map (EM.fromList . mzipPairs poss)- [ (blank , coloredSymbol BrCyan '\'')- , (coloredSymbol BrBlue '{', coloredSymbol BrYellow '^')- , (coloredSymbol BrBlue '{', blank)- , (coloredSymbol BrBlue '}', blank)- , (coloredSymbol Blue '}', blank)+ [ (blank , cSym BrCyan '\'')+ , (blank , cSym BrYellow '^')+ , (cSym BrBlue '{', cSym BrYellow '\'')+ , (cSym BrBlue '{', cSym BrCyan '\'')+ , (cSym BrBlue '{', blank)+ , (cSym BrBlue '}', blank)+ , (cSym BrBlue '}', blank)+ , (cSym Blue '}', blank)+ , (cSym Blue '}', blank) ] -- | Death animation for an organic body. deathBody :: Point -> Animation deathBody pos = Animation $ map (maybe EM.empty (EM.singleton pos))- [ coloredSymbol BrRed '\\'- , coloredSymbol BrRed '\\'- , coloredSymbol BrRed '|'- , coloredSymbol BrRed '|'- , coloredSymbol BrRed '%'- , coloredSymbol BrRed '%'- , coloredSymbol Red '%'- , coloredSymbol Red '%'- , coloredSymbol Red '%'- , coloredSymbol Red ';'- , coloredSymbol Red ';'- , coloredSymbol Red ','+ [ cSym BrRed '\\'+ , cSym BrRed '\\'+ , cSym BrRed '|'+ , cSym BrRed '|'+ , cSym BrRed '%'+ , cSym BrRed '%'+ , cSym BrRed '-'+ , cSym BrRed '-'+ , cSym BrRed '\\'+ , cSym BrRed '\\'+ , cSym BrRed '|'+ , cSym BrRed '|'+ , cSym BrRed '%'+ , cSym BrRed '%'+ , cSym BrRed '%'+ , cSym Red '%'+ , cSym Red '%'+ , cSym Red '%'+ , cSym Red '%'+ , cSym Red ';'+ , cSym Red ';'+ , cSym Red ',' ] +-- | Mark actor location animation.+actorX :: Point -> Char -> Color.Color -> Animation+actorX pos symbol color = Animation $ map (maybe EM.empty (EM.singleton pos))+ [ cSym BrRed 'X'+ , cSym BrRed 'X'+ , cSym BrRed symbol+ , cSym color symbol+ , cSym color symbol+ , cSym color symbol+ , cSym color symbol+ ]+ -- | Swap-places animation, both hostile and friendly. swapPlaces :: (Point, Point) -> Animation swapPlaces poss = Animation $ map (EM.fromList . mzipPairs poss)- [ (coloredSymbol BrMagenta 'o', coloredSymbol Magenta 'o')- , (coloredSymbol BrMagenta 'd', coloredSymbol Magenta 'p')- , (coloredSymbol Magenta 'p', coloredSymbol BrMagenta 'd')- , (coloredSymbol Magenta 'o', blank)+ [ (cSym BrMagenta 'o', cSym Magenta 'o')+ , (cSym BrMagenta 'd', cSym Magenta 'p')+ , (cSym BrMagenta '.', cSym Magenta 'p')+ , (cSym Magenta 'p', cSym Magenta '.')+ , (cSym Magenta 'p', cSym BrMagenta 'd')+ , (cSym Magenta 'p', cSym BrMagenta 'd')+ , (cSym Magenta 'o', blank) ] +moveProj :: (Point, Point, Point) -> Char -> Color.Color -> Animation+moveProj poss symbol color = Animation $ map (EM.fromList . mzipTriples poss)+ [ (cSym BrBlack '.', cSym color symbol , cSym color '.')+-- , (cSym BrBlack '.', cSym BrBlack symbol, cSym color symbol)+ , (cSym BrBlack '.', cSym BrBlack '.' , cSym color symbol)+ , (blank , cSym BrBlack '.' , cSym color symbol)+ ]+ fadeout :: Bool -> Bool -> Int -> X -> Y -> Rnd Animation fadeout out topRight step lxsize lysize = do let xbound = lxsize - 1@@ -213,5 +272,5 @@ return $! EM.fromList l startN = if out then 3 else 1 fs = [startN, startN + step .. 3 * lxsize `divUp` 4 + 2]- as <- mapM rollFrame $ if out then fs else reverse fs- return $! Animation as+ as <- mapM rollFrame fs+ return $! Animation $ if out then as else reverse (EM.empty : as)
Game/LambdaHack/Client/UI/Config.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE DeriveGeneric, FlexibleContexts #-} -- | Personal game configuration file type definitions. module Game.LambdaHack.Client.UI.Config ( Config(..), mkConfig, applyConfigToDebug@@ -15,6 +16,7 @@ import Data.Text (Text) import qualified Data.Text as T import Game.LambdaHack.Common.ClientOptions+import GHC.Generics (Generic) import System.Directory import System.FilePath import Text.Read@@ -43,7 +45,7 @@ , configNoAnim :: !Bool , configRunStopMsgs :: !Bool }- deriving Show+ deriving (Show, Generic) instance NFData Config @@ -90,8 +92,7 @@ mkConfig Kind.COps{corule} = do let stdRuleset = Kind.stdRuleset corule cfgUIName = rcfgUIName stdRuleset- commentsUIDefault = init $ map (drop 2) $ lines $ rcfgUIDefault stdRuleset -- TODO: init is a hack until Ini accepts empty files- sUIDefault = unlines commentsUIDefault+ sUIDefault = rcfgUIDefault stdRuleset cfgUIDefault = either (assert `failure`) id $ Ini.parse sUIDefault dataDir <- appDataDir let userPath = dataDir </> cfgUIName <.> "ini"
Game/LambdaHack/Client/UI/Content/KeyKind.hs view
@@ -1,6 +1,7 @@ -- | The type of key-command mappings to be used for the UI. module Game.LambdaHack.Client.UI.Content.KeyKind ( KeyKind(..)+ , macroLeftButtonPress, macroShiftLeftButtonPress ) where import qualified Game.LambdaHack.Client.Key as K@@ -11,3 +12,17 @@ { rhumanCommands :: ![(K.KM, ([CmdCategory], HumanCmd))] -- ^ default client UI commands }++macroLeftButtonPress :: HumanCmd+macroLeftButtonPress =+ Macro "go to pointer for 100 steps"+ [ "ALT-space", "ALT-minus"+ , "SHIFT-MiddleButtonPress", "CTRL-semicolon"+ , "CTRL-period", "V" ]++macroShiftLeftButtonPress :: HumanCmd+macroShiftLeftButtonPress =+ Macro "run collectively to pointer for 100 steps"+ [ "ALT-space"+ , "SHIFT-MiddleButtonPress", "CTRL-colon"+ , "CTRL-period", "V" ]
Game/LambdaHack/Client/UI/DisplayAtomicClient.hs view
@@ -5,9 +5,8 @@ import Control.Exception.Assert.Sugar import Control.Monad-import qualified Data.EnumSet as ES import qualified Data.EnumMap.Strict as EM-import qualified Data.IntMap.Strict as IM+import qualified Data.EnumSet as ES import Data.Maybe import Data.Monoid import Data.Tuple@@ -39,6 +38,7 @@ import Game.LambdaHack.Common.Time import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.ModeKind+import Game.LambdaHack.Content.RuleKind import qualified Game.LambdaHack.Content.TileKind as TK -- * RespUpdAtomicUI@@ -57,7 +57,7 @@ -- the client state is modified by the command. displayRespUpdAtomicUI :: MonadClientUI m => Bool -> State -> StateClient -> UpdAtomic -> m ()-displayRespUpdAtomicUI verbose _oldState oldStateClient cmd = case cmd of+displayRespUpdAtomicUI verbose oldState oldStateClient cmd = case cmd of -- Create/destroy actors and items. UpdCreateActor aid body _ -> createActorUI aid body verbose "appear" UpdDestroyActor aid body _ -> do@@ -66,41 +66,46 @@ when (bfid body == side && not (bproj body)) stopPlayBack UpdCreateItem iid _ kit c -> do case c of- CActor aid COrgan -> do- let verb =- MU.Text $ "become" <+> case fst kit of- 1 -> ""- k -> tshow k <> "-fold"- -- This describes all such items already among organs,- -- which is useful, because it shows "charging".- itemAidVerbMU aid verb iid (Left Nothing) COrgan CActor aid store -> do- when (store == CGround) $ updateItemSlotSide aid iid- itemVerbMU iid kit (MU.Text $ "appear" <+> ppContainer c) c+ l <- updateItemSlotSide store aid iid+ case store of+ COrgan -> do+ let verb =+ MU.Text $ "become" <+> case fst kit of+ 1 -> ""+ k -> tshow k <> "-fold"+ -- This describes all such items already among organs,+ -- which is useful, because it shows "charging".+ itemAidVerbMU aid verb iid (Left Nothing) COrgan+ _ -> do+ itemVerbMU iid kit (MU.Text $ "appear" <+> ppContainer c) c+ mleader <- getsClient _sleader+ when (Just aid == mleader) $+ modifyClient $ \cli -> cli { slastSlot = l+ , slastStore = store } CEmbed{} -> return () CFloor{} -> do- updateItemSlot Nothing iid+ -- If you want an item to be assigned to @slastSlot@, create it+ -- in @CActor aid CGround@, not in @CFloor@.+ void $ updateItemSlot CGround Nothing iid itemVerbMU iid kit (MU.Text $ "appear" <+> ppContainer c) c- CTrunk{} -> return ()+ CTrunk{} -> assert `failure` c stopPlayBack UpdDestroyItem iid _ kit c -> itemVerbMU iid kit "disappear" c UpdSpotActor aid body _ -> createActorUI aid body verbose "be spotted" UpdLoseActor aid body _ -> destroyActorUI aid body "be missing in action" "be lost" verbose UpdSpotItem iid _ kit c -> do- -- We assign slots to all items visible on the floor,- -- but some of the slots are later on recycled and then- -- we report spotting the items again.- (letterSlots, numberSlots) <- getsClient sslots- case ( lookup iid $ map swap $ EM.assocs letterSlots- , lookup iid $ map swap $ IM.assocs numberSlots ) of- (Nothing, Nothing) -> do+ (itemSlots, _) <- getsClient sslots+ case lookup iid $ map swap $ EM.assocs itemSlots of+ Nothing -> -- never seen or would have a slot case c of- CActor aid CGround -> updateItemSlotSide aid iid- CActor{} -> return ()+ CActor aid store ->+ -- Enemy actor fetching an item from shared stash, most probably.+ void $ updateItemSlotSide store aid iid CEmbed{} -> return () CFloor lid p -> do- updateItemSlot Nothing iid+ void $ updateItemSlot CGround Nothing iid scursorOld <- getsClient scursor case scursorOld of TEnemy{} -> return () -- probably too important to overwrite@@ -109,25 +114,26 @@ itemVerbMU iid kit "be spotted" c stopPlayBack CTrunk{} -> return ()- _ -> return () -- seen recently (still has a slot assigned)- UpdLoseItem{} -> skip+ _ -> return () -- seen already (has a slot assigned)+ UpdLoseItem{} -> return () -- Move actors and items.- UpdMoveActor aid _ _ -> lookAtMove aid+ UpdMoveActor aid source target -> moveActor oldState aid source target UpdWaitActor aid _ -> when verbose $ aidVerbMU aid "wait" UpdDisplaceActor source target -> displaceActorUI source target- UpdMoveItem iid k aid c1 c2 -> moveItemUI verbose iid k aid c1 c2+ UpdMoveItem iid k aid c1 c2 -> moveItemUI iid k aid c1 c2 -- Change actor attributes.- UpdAgeActor{} -> skip- UpdRefillHP _ 0 -> skip+ UpdAgeActor{} -> return ()+ UpdRefillHP _ 0 -> return () UpdRefillHP aid n -> do when verbose $ aidVerbMU aid $ MU.Text $ (if n > 0 then "heal" else "lose")- <+> tshow (abs $ n `divUp` oneM) <> "HP"+ <+> tshow (abs $ n `divUp` oneM) <> "HP" mleader <- getsClient _sleader when (Just aid == mleader) $ do b <- getsState $ getActorBody aid hpMax <- sumOrganEqpClient IK.EqpSlotAddMaxHP aid- when (bhp b == xM hpMax && hpMax > 0) $ do+ when (bhp b >= xM hpMax && hpMax > 0+ && resCurrentTurn (bhpDelta b) > 0) $ do actorVerbMU aid b "recover your health fully" stopPlayBack UpdRefillCalm aid calmDelta ->@@ -141,9 +147,9 @@ when (null closeFoes) $ do -- obvious where the feeling comes from aidVerbMU aid "hear something" msgDuplicateScrap- UpdOldFidActor{} -> skip- UpdTrajectory{} -> skip- UpdColorActor{} -> skip+ UpdFidImpressedActor{} -> return ()+ UpdTrajectory{} -> return ()+ UpdColorActor{} -> return () -- Change faction attributes. UpdQuitFaction fid mbody _ toSt -> quitFactionUI fid mbody toSt UpdLeadFaction fid (Just (source, _)) (Just (target, _)) -> do@@ -168,7 +174,7 @@ -- TODO: report when server changes spawner's leader; -- perhaps don't switch _sleader in HandleAtomicClient, -- compare here and switch here? too hacky? fails for AI?- UpdLeadFaction{} -> skip+ UpdLeadFaction{} -> return () UpdDiplFaction fid1 fid2 _ toDipl -> do name1 <- getsState $ gname . (EM.! fid1) . sfactionD name2 <- getsState $ gname . (EM.! fid2) . sfactionD@@ -177,11 +183,11 @@ showDipl Alliance = "allied" showDipl War = "at war" msgAdd $ name1 <+> "and" <+> name2 <+> "are now" <+> showDipl toDipl <> "."- UpdTacticFaction{} -> skip+ UpdTacticFaction{} -> return () UpdAutoFaction fid b -> do side <- getsClient sside when (fid == side) $ setFrontAutoYes b- UpdRecordKill{} -> skip+ UpdRecordKill{} -> return () -- Alter map. UpdAlterTile{} -> when verbose $ return () -- TODO: door opens UpdAlterClear _ k -> msgAdd $ if k > 0@@ -203,24 +209,25 @@ , "a hidden" , MU.Text $ TK.tname $ okind toTile ] msgAdd msg- UpdLearnSecrets{} -> skip- UpdSpotTile{} -> skip- UpdLoseTile{} -> skip- UpdAlterSmell{} -> skip- UpdSpotSmell{} -> skip- UpdLoseSmell{} -> skip+ UpdLearnSecrets{} -> return ()+ UpdSpotTile{} -> return ()+ UpdLoseTile{} -> return ()+ UpdAlterSmell{} -> return ()+ UpdSpotSmell{} -> return ()+ UpdLoseSmell{} -> return () -- Assorted.- UpdTimeItem{} -> skip- UpdAgeGame{} -> skip- UpdDiscover lid p iid _ _ -> discover lid p oldStateClient iid- UpdCover{} -> skip -- don't spam when doing undo- UpdDiscoverKind lid p iid _ -> discover lid p oldStateClient iid- UpdCoverKind{} -> skip -- don't spam when doing undo- UpdDiscoverSeed lid p iid _ -> discover lid p oldStateClient iid- UpdCoverSeed{} -> skip -- don't spam when doing undo- UpdPerception{} -> skip- UpdRestart fid _ _ _ _ _ -> do- mode <- getModeClient+ UpdTimeItem{} -> return ()+ UpdAgeGame{} -> return ()+ UpdDiscover c iid _ _ -> discover c oldStateClient iid+ UpdCover{} -> return () -- don't spam when doing undo+ UpdDiscoverKind c iid _ -> discover c oldStateClient iid+ UpdCoverKind{} -> return () -- don't spam when doing undo+ UpdDiscoverSeed c iid _ -> discover c oldStateClient iid+ UpdCoverSeed{} -> return () -- don't spam when doing undo+ UpdPerception{} -> return ()+ UpdRestart fid _ _ _ _ -> do+ void $ tryTakeMVarSescMVar -- clear ESC-pressed from end of previous game+ mode <- getGameMode msgAdd $ "New game started in" <+> mname mode <+> "mode." <+> mdesc mode -- TODO: use a vertical animation instead, e.g., roll down, -- and reveal the first frame of a new game, not blank screen.@@ -228,23 +235,24 @@ when (lengthHistory history > 1) $ fadeOutOrIn False fact <- getsState $ (EM.! fid) . sfactionD setFrontAutoYes $ isAIFact fact- UpdRestartServer{} -> skip+ UpdRestartServer{} -> return () UpdResume fid _ -> do fact <- getsState $ (EM.! fid) . sfactionD setFrontAutoYes $ isAIFact fact- UpdResumeServer{} -> skip- UpdKillExit{} -> skip+ UpdResumeServer{} -> return ()+ UpdKillExit{} -> return () UpdWriteSave -> when verbose $ msgAdd "Saving backup." UpdMsgAll msg -> msgAdd msg UpdRecordHistory _ -> recordHistory -updateItemSlotSide :: MonadClient m => ActorId -> ItemId -> m ()-updateItemSlotSide aid iid = do+updateItemSlotSide :: MonadClient m+ => CStore -> ActorId -> ItemId -> m SlotChar+updateItemSlotSide store aid iid = do side <- getsClient sside b <- getsState $ getActorBody aid if bfid b == side- then updateItemSlot (Just aid) iid- else updateItemSlot Nothing iid+ then updateItemSlot store (Just aid) iid+ else updateItemSlot store Nothing iid lookAtMove :: MonadClientUI m => ActorId -> m () lookAtMove aid = do@@ -281,7 +289,7 @@ lid <- getsState $ lidFromC c localTime <- getsState $ getLocalTime lid itemToF <- itemToFullClient- let subject = partItemWs k c lid localTime (itemToF iid kit)+ let subject = partItemWs k (storeFromC c) lid localTime (itemToF iid kit) msg | k > 1 = makeSentence [MU.SubjectVerb MU.PlEtc MU.Yes subject verb] | otherwise = makeSentence [MU.SubjectVerbSg subject verb] msgAdd msg@@ -294,28 +302,28 @@ -> ItemId -> Either (Maybe Int) Int -> CStore -> m () itemAidVerbMU aid verb iid ek cstore = do- let c = CActor aid cstore- bag <- getsState $ getCBag c+ bag <- getsState $ getActorBag aid cstore -- The item may no longer be in @c@, but it was case iid `EM.lookup` bag of Nothing -> assert `failure` (aid, verb, iid, cstore) Just kit@(k, _) -> do itemToF <- itemToFullClient- lid <- getsState $ lidFromC c+ body <- getsState $ getActorBody aid+ let lid = blid body localTime <- getsState $ getLocalTime lid subject <- partAidLeader aid let itemFull = itemToF iid kit object = case ek of Left (Just n) -> assert (n <= k `blame` (aid, verb, iid, cstore))- $ partItemWs n c lid localTime itemFull+ $ partItemWs n cstore lid localTime itemFull Left Nothing ->- let (name, stats) = partItem c lid localTime itemFull+ let (_, name, stats) = partItem cstore lid localTime itemFull in MU.Phrase [name, stats] Right n -> assert (n <= k `blame` (aid, verb, iid, cstore)) $ let itemSecret = itemNoDisco (itemBase itemFull, n)- (secretName, secretAE) = partItem c lid localTime itemSecret+ (_, secretName, secretAE) = partItem cstore lid localTime itemSecret name = MU.Phrase [secretName, secretAE] nameList = if n == 1 then ["the", name]@@ -339,18 +347,12 @@ createActorUI :: MonadClientUI m => ActorId -> Actor -> Bool -> MU.Part -> m () createActorUI aid body verbose verb = do+ mapM_ (\(iid, store) -> void $ updateItemSlotSide store aid iid)+ (getCarriedIidCStore body) side <- getsClient sside- -- 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 <- getsClient slastLost- when (ES.notMember aid lastLost- && (bfid body /= side- && not (bproj body) || verbose)) $- actorVerbMU aid body verb when (bfid body /= side) $ do fact <- getsState $ (EM.! bfid body) . sfactionD- when (not (bproj body) && isAtWar fact side) $ do+ when (not (bproj body) && isAtWar fact side) $ -- Target even if nobody can aim 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@@ -358,47 +360,62 @@ -- into account. modifyClient $ \cli -> cli {scursor = TEnemy aid False} stopPlayBack+ -- 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 <- getsClient slastLost+ when (ES.notMember aid lastLost+ && (not (bproj body) || verbose)) $ do+ actorVerbMU aid body verb+ animFrs <- animate (blid body)+ $ actorX (bpos body) (bsymbol body) (bcolor body)+ displayActorStart body animFrs lookAtMove aid destroyActorUI :: MonadClientUI m => ActorId -> Actor -> MU.Part -> MU.Part -> Bool -> m () destroyActorUI aid body verb verboseVerb verbose = do+ Kind.COps{corule} <- getsState scops side <- getsClient sside- if (bfid body == side && bhp body <= 0 && not (bproj body)) then do- actorVerbMU aid body verb- void $ displayMore ColorBW ""+ when (bfid body == side) $ do+ let upd = ES.delete aid+ modifyClient $ \cli -> cli {sselected = upd $ sselected cli}+ if bfid body == side && bhp body <= 0 && not (bproj body) then do+ when verbose $ actorVerbMU aid body verb+ let firstDeathEnds = rfirstDeathEnds $ Kind.stdRuleset corule+ fid = bfid body+ fact <- getsState $ (EM.! fid) . sfactionD+ actorsAlive <- anyActorsAlive fid (Just aid)+ -- TODO: deduplicate wrt Server+ -- TODO; actually show the --more- prompt, but not between fadeout frames+ unless (fneverEmpty (gplayer fact)+ && (not actorsAlive || firstDeathEnds)) $+ void $ displayMore ColorBW "" else when verbose $ actorVerbMU aid body verboseVerb modifyClient $ \cli -> cli {slastLost = ES.insert aid $ slastLost cli} -moveItemUI :: MonadClientUI m- => Bool -> ItemId -> Int -> ActorId -> CStore -> CStore- -> m ()-moveItemUI verbose iid k aid cstore1 cstore2 = do- let verb = verbCStore cstore2- when (cstore2 == CGround) $ updateItemSlotSide aid iid- b <- getsState $ getActorBody aid- fact <- getsState $ (EM.! bfid b) . sfactionD- let underAI = isAIFact fact- mleader <- getsClient _sleader- if (cstore1 == CGround && Just aid == mleader && not underAI) then do- itemAidVerbMU aid (MU.Text verb) iid (Right k) cstore2- localTime <- getsState $ getLocalTime (blid b)- itemToF <- itemToFullClient- (letterSlots, _) <- getsClient sslots- let c2 = CActor aid cstore2- bag <- getsState $ getCBag c2- let kit@(n, _) = bag EM.! iid- case lookup iid $ map swap $ EM.assocs letterSlots of- Just l -> msgAdd $ makePhrase- [ "\n"- , slotLabel $ Left l- , "-"- , partItemWs n c2 (blid b) localTime (itemToF iid kit)- , "\n" ]- Nothing -> return ()- else when (cstore2 /= CGround || verbose) $ -- dropping is silent- itemAidVerbMU aid (MU.Text verb) iid (Left $ Just k) cstore2+-- TODO: deduplicate wrt Server+anyActorsAlive :: MonadClient m => FactionId -> Maybe ActorId -> m Bool+anyActorsAlive fid maid = do+ fact <- getsState $ (EM.! fid) . sfactionD+ if fleaderMode (gplayer fact) /= LeaderNull+ then return $! isJust $ gleader fact+ else do+ as <- getsState $ fidActorNotProjAssocs fid+ return $! not $ null $ maybe as (\aid -> filter ((/= aid) . fst) as) maid +moveActor :: MonadClientUI m => State -> ActorId -> Point -> Point -> m ()+moveActor oldState aid source target = do+ lookAtMove aid+ body <- getsState $ getActorBody aid+ when (bproj body) $ do+ let oldpos = case EM.lookup aid $ sactorD oldState of+ Nothing -> assert `failure` (sactorD oldState, aid)+ Just b -> boldpos b+ let ps = (oldpos, source, target)+ animFrs <- animate (blid body) $ moveProj ps (bsymbol body) (bcolor body)+ displayActorStart body animFrs+ displaceActorUI :: MonadClientUI m => ActorId -> ActorId -> m () displaceActorUI source target = do sb <- getsState $ getActorBody source@@ -414,6 +431,37 @@ animFrs <- animate (blid sb) $ swapPlaces ps displayActorStart sb animFrs +moveItemUI :: MonadClientUI m+ => ItemId -> Int -> ActorId -> CStore -> CStore+ -> m ()+moveItemUI iid k aid cstore1 cstore2 = do+ let verb = verbCStore cstore2+ b <- getsState $ getActorBody aid+ fact <- getsState $ (EM.! bfid b) . sfactionD+ let underAI = isAIFact fact+ mleader <- getsClient _sleader+ bag <- getsState $ getActorBag aid cstore2+ let kit@(n, _) = bag EM.! iid+ itemToF <- itemToFullClient+ (itemSlots, _) <- getsClient sslots+ case lookup iid $ map swap $ EM.assocs itemSlots of+ Just l -> do+ when (Just aid == mleader) $+ modifyClient $ \cli -> cli { slastSlot = l+ , slastStore = cstore2 }+ if cstore1 == CGround && Just aid == mleader && not underAI then do+ itemAidVerbMU aid (MU.Text verb) iid (Right k) cstore2+ localTime <- getsState $ getLocalTime (blid b)+ msgAdd $ makePhrase+ [ "\n"+ , slotLabel l+ , "-"+ , partItemWs n cstore2 (blid b) localTime (itemToF iid kit)+ , "\n" ]+ else when (not (bproj b) && bhp b > 0) $ -- don't announce death drops+ itemAidVerbMU aid (MU.Text verb) iid (Left $ Just k) cstore2+ Nothing -> assert `failure` (iid, itemToF iid kit)+ quitFactionUI :: MonadClientUI m => FactionId -> Maybe Actor -> Maybe Status -> m () quitFactionUI fid mbody toSt = do@@ -471,8 +519,7 @@ <+> moreMsg if EM.null bag then return (mempty, 0) else do- mapM_ (updateItemSlot Nothing) $ EM.keys bag- io <- itemOverlay (CFloor (blid b) (bpos b)) (blid b) bag+ io <- itemOverlay CGround (blid b) bag sli <- overlayToSlideshow itemMsg io return (sli, tot) (itemSlides, total) <- case mbody of@@ -501,29 +548,33 @@ _ -> return () discover :: MonadClientUI m- => LevelId -> Point -> StateClient -> ItemId -> m ()-discover lid p oldcli iid = do+ => Container -> StateClient -> ItemId -> m ()+discover c oldcli iid = do+ let cstore = storeFromC c+ lid <- getsState $ lidFromC c cops <- getsState scops localTime <- getsState $ getLocalTime lid itemToF <- itemToFullClient- let itemFull = itemToF iid (1, [])- c = CFloor lid p- (knownName, knownAEText) = partItem c lid localTime itemFull+ bag <- getsState $ getCBag c+ let kit = EM.findWithDefault (1, []) iid bag+ itemFull = itemToF iid kit+ knownName = partItemMediumAW cstore lid localTime itemFull -- Wipe out the whole knowledge of the item to make sure the two names -- in the message differ even if, e.g., the item is described as -- "of many effects". itemSecret = itemNoDisco (itemBase itemFull, itemK itemFull)- (secretName, secretAEText) = partItem c lid localTime itemSecret+ (_, secretName, secretAEText) = partItem cstore lid localTime itemSecret msg = makeSentence [ "the", MU.SubjectVerbSg (MU.Phrase [secretName, secretAEText]) "turn out to be"- , MU.AW $ MU.Phrase [knownName, knownAEText] ]+ , knownName ] oldItemFull = itemToFull cops (sdiscoKind oldcli) (sdiscoEffect oldcli) iid (itemBase itemFull) (1, []) -- Compare descriptions of all aspects and effects to determine -- if the discovery was meaningful to the player.- when (textAllAE False c itemFull /= textAllAE False c oldItemFull) $+ when (textAllAE 7 False cstore itemFull+ /= textAllAE 7 False cstore oldItemFull) $ msgAdd msg -- * RespSfxAtomicUI@@ -531,19 +582,21 @@ -- | Display special effects (text, animation) sent to the client. displayRespSfxAtomicUI :: MonadClientUI m => Bool -> SfxAtomic -> m () displayRespSfxAtomicUI verbose sfx = case sfx of- SfxStrike source target iid b -> strike source target iid b- SfxRecoil source target _ _ -> do+ SfxStrike source target iid cstore b -> strike source target iid cstore b+ SfxRecoil source target _ _ _ -> do spart <- partAidLeader source tpart <- partAidLeader target msgAdd $ makeSentence [MU.SubjectVerbSg spart "shrink away from", tpart]- SfxProject aid iid cstore ->+ SfxProject aid iid cstore -> do+ setLastSlot aid iid cstore itemAidVerbMU aid "aim" iid (Left $ Just 1) cstore SfxCatch aid iid cstore -> itemAidVerbMU aid "catch" iid (Left $ Just 1) cstore- SfxActivate aid iid cstore ->- itemAidVerbMU aid "activate" iid (Left $ Just 1) cstore+ SfxApply aid iid cstore -> do+ setLastSlot aid iid cstore+ itemAidVerbMU aid "apply" iid (Left $ Just 1) cstore SfxCheck aid iid cstore ->- itemAidVerbMU aid "deactivate" iid (Left $ Just 1) cstore+ itemAidVerbMU aid "deapply" iid (Left $ Just 1) cstore SfxTrigger aid _p _feat -> when verbose $ aidVerbMU aid "trigger" -- TODO: opens door, etc. SfxShun aid _p _ ->@@ -552,8 +605,9 @@ b <- getsState $ getActorBody aid side <- getsClient sside let fid = bfid b- if bhp b <= 0 && not (bproj b) || bhp b < 0 then do- -- We assume the effect is the cause of incapacitation.+ if bhp b <= 0 then do+ -- We assume the effect is the cause of incapacitation, but in case+ -- of projectile, to reduce spam, we verify with @canKill@. let firstFall | fid == side && bproj b = "fall apart" | fid == side = "fall down" | bproj b = "break up"@@ -562,35 +616,43 @@ | fid == side = "be stomped flat" | bproj b = "be shattered into little pieces" | otherwise = "be reduced to a bloody pulp"- subject <- partActorLeader aid b- let deadPreviousTurn p = p < 0- && (bhp b <= p && not (bproj b)- || bhp b < p)- (deadBefore, verbDie) =+ -- Aspect bonuses ignored, so hurtExtra will add variety sometimes.+ deadPreviousTurn dp = bhp b <= dp+ harm2 dp = if deadPreviousTurn dp+ then (True, Just hurtExtra)+ else (False, Just firstFall)+ (deadBefore, mverbDie) = case effect of- IK.Hurt p | deadPreviousTurn (xM $ Dice.maxDice p) ->- (True, hurtExtra)- IK.RefillHP p | deadPreviousTurn (xM p) -> (True, hurtExtra)- IK.OverfillHP p | deadPreviousTurn (xM p) -> (True, hurtExtra)- _ -> (False, firstFall)- msgDie = makeSentence [MU.SubjectVerbSg subject verbDie]- msgAdd msgDie- when (fid == side && not (bproj b)) $ do- animDie <- if deadBefore- then animate (blid b)- $ twirlSplash (bpos b, bpos b) Color.Red Color.Red- else animate (blid b) $ deathBody $ bpos b- displayActorStart b animDie+ IK.Hurt p -> harm2 (- (xM $ Dice.maxDice p))+ IK.RefillHP p | p < 0 -> harm2 (xM p)+ IK.OverfillHP p | p < 0 -> harm2 (xM p)+ IK.Burn p -> harm2 (- (xM $ Dice.maxDice p))+ _ -> (False, Nothing)+ case mverbDie of+ Nothing -> return () -- only brutal effects work on dead/dying actor+ Just verbDie -> do+ subject <- partActorLeader aid b+ let msgDie = makeSentence [MU.SubjectVerbSg subject verbDie]+ msgAdd msgDie+ when (fid == side && not (bproj b)) $ do+ animDie <- if deadBefore+ then animate (blid b)+ $ twirlSplash (bpos b, bpos b) Color.Red Color.Red+ else animate (blid b) $ deathBody $ bpos b+ displayActorStart b animDie else case effect of IK.NoEffect t -> msgAdd $ "Nothing happens." <+> t- IK.Hurt{} -> skip -- avoid spam; SfxStrike just sent- IK.Burn{} ->+ IK.Hurt{} -> return () -- avoid spam; SfxStrike just sent+ IK.Burn{} -> do if fid == side then actorVerbMU aid b "feel burned" else actorVerbMU aid b "look burned"- IK.Explode{} -> skip -- lots of visual feedback- IK.RefillHP p | p == 1 -> skip -- no spam from regeneration+ let ps = (bpos b, bpos b)+ animFrs <- animate (blid b) $ twirlSplash ps Color.BrRed Color.Red+ displayActorStart b animFrs+ IK.Explode{} -> return () -- lots of visual feedback+ IK.RefillHP p | p == 1 -> return () -- no spam from regeneration IK.RefillHP p | p > 0 -> do if fid == side then actorVerbMU aid b "feel healthier"@@ -599,7 +661,7 @@ let ps = (bpos b, bpos b) animFrs <- animate (blid b) $ twirlSplash ps Color.BrBlue Color.Blue displayActorStart b animFrs- IK.RefillHP p | p == -1 -> skip -- no spam from poison+ IK.RefillHP p | p == -1 -> return () -- no spam from poison IK.RefillHP _ -> do if fid == side then actorVerbMU aid b "feel wounded"@@ -624,7 +686,7 @@ let ps = (bpos b, bpos b) animFrs <- animate (blid b) $ twirlSplash ps Color.BrRed Color.Red displayActorStart b animFrs- IK.RefillCalm p | p == 1 -> skip -- no spam from regen items+ IK.RefillCalm p | p == 1 -> return () -- no spam from regen items IK.RefillCalm p | p > 0 -> do if fid == side then actorVerbMU aid b "feel calmer"@@ -661,7 +723,7 @@ -- For subsequent messages use the proper name, never "you". let subject = partActor b if fid /= fidSource then do -- before domination- if bcalm b == 0 then do -- sometimes only a coincidence, but nm+ if bcalm b == 0 then -- sometimes only a coincidence, but nm aidVerbMU aid $ MU.Text "yield, under extreme pressure" else if fid == side then aidVerbMU aid $ MU.Text "black out, dominated by foes"@@ -677,49 +739,55 @@ let verb = "be now under" msgAdd $ makeSentence [MU.SubjectVerbSg subject verb, MU.Text fidSourceName, "control"]- IK.Impress{} ->+ stopPlayBack+ IK.Impress -> actorVerbMU aid b- $ if boldfid b /= bfid b+ $ if bfidImpressed b /= bfid b then- "get refocused by the fragrant moisture"+ "get calmed and refocused"+-- TODO: only show for liquids; for others say "='flash', etc.+-- "get refocused by the fragrant moisture" else- "inhale the sweet smell that weakens resolve and erodes loyalty"- IK.CallFriend{} -> skip- IK.Summon{} -> skip+ "experience anxiety that weakens resolve and erodes loyalty"+-- TODO: "inhale the sweet smell that weakens resolve and erodes loyalty"+ IK.CallFriend{} -> return ()+ IK.Summon{} -> return () IK.Ascend k | k > 0 -> actorVerbMU aid b "find a way upstairs" IK.Ascend k | k < 0 -> actorVerbMU aid b "find a way downstairs" IK.Ascend{} -> assert `failure` sfx- IK.Escape{} -> skip+ IK.Escape{} -> return () IK.Paralyze{} -> actorVerbMU aid b "be paralyzed" IK.InsertMove{} -> actorVerbMU aid b "act with extreme speed" IK.Teleport t | t > 9 -> actorVerbMU aid b "teleport" IK.Teleport{} -> actorVerbMU aid b "blink"- IK.CreateItem{} -> skip- IK.DropItem COrgan _ True -> skip+ IK.CreateItem{} -> return ()+ IK.DropItem COrgan _ True -> return () IK.DropItem _ _ False -> actorVerbMU aid b "be stripped" -- TODO IK.DropItem _ _ True -> actorVerbMU aid b "be violently stripped"- IK.PolyItem cstore -> do+ IK.PolyItem -> do localTime <- getsState $ getLocalTime $ blid b- allAssocs <- fullAssocsClient aid [cstore]+ allAssocs <- fullAssocsClient aid [CGround] case allAssocs of [] -> return () -- invisible items? (_, ItemFull{..}) : _ -> do+ subject <- partActorLeader aid b let itemSecret = itemNoDisco (itemBase, itemK)- (secretName, secretAEText) = partItem (CActor aid cstore) (blid b) localTime itemSecret- subject = partActor b+ -- TODO: plural form of secretName? only when K > 1?+ -- At this point we don't easily know how many consumed.+ (_, secretName, secretAEText) = partItem CGround (blid b) localTime itemSecret verb = "repurpose"- store = MU.Text $ ppCStore cstore+ store = MU.Text $ ppCStoreIn CGround msgAdd $ makeSentence [ MU.SubjectVerbSg subject verb , "the", secretName, secretAEText, store ]- IK.Identify cstore -> do -- here cstore is the real location- allAssocs <- fullAssocsClient aid [cstore]+ IK.Identify -> do+ allAssocs <- fullAssocsClient aid [CGround] case allAssocs of [] -> return () -- invisible items? (_, ItemFull{..}) : _ -> do- let subject = partActor b- verb = "inspect"- store = MU.Text $ ppCStore cstore+ subject <- partActorLeader aid b+ let verb = "inspect"+ store = MU.Text $ ppCStoreIn CGround msgAdd $ makeSentence [ MU.SubjectVerbSg subject verb , "an item", store ]@@ -727,10 +795,10 @@ IK.PushActor{} -> actorVerbMU aid b "be pushed" IK.PullActor{} -> actorVerbMU aid b "be pulled" IK.DropBestWeapon -> actorVerbMU aid b "be disarmed"- IK.ActivateInv{} -> skip+ IK.ActivateInv{} -> return () IK.ApplyPerfume -> msgAdd "The fragrance quells all scents in the vicinity."- IK.OneOf{} -> skip+ IK.OneOf{} -> return () IK.OnSmash{} -> assert `failure` sfx IK.Recharging{} -> assert `failure` sfx IK.Temporary t -> actorVerbMU aid b $ MU.Text t@@ -739,41 +807,58 @@ SfxActorStart aid -> do arena <- getArenaUI b <- getsState $ getActorBody aid- activeItems <- activeItemsClient aid+-- activeItems <- activeItemsClient aid when (blid b == arena) $ do- -- If time clip has passed since any actor advanced level time- -- or if the actor is so fast that he was capable of already moving- -- this clip (for simplicity, we don't check if he actually did)+ -- If time clip has passed since any actor advanced @timeCutOff@+--TODO -- or if the actor is so fast that he was capable of already moving+-- -- this clip (for simplicity, we don't check if he actually did) -- or if the actor is newborn or is about to die, -- we end the frame early, before his current move. -- In the result, he moves at most once per frame, and thanks to this, -- his multiple moves are not collapsed into one frame. -- If the actor changes his speed this very clip, the test can faii, -- but it's rare and results in a minor UI issue, so we don't care.+ localTime <- getsState $ getLocalTime (blid b) timeCutOff <- getsClient $ EM.findWithDefault timeZero arena . sdisplayed- when (btime b >= timeShift timeCutOff (Delta timeClip)- || btime b >= timeShiftFromSpeed b activeItems timeCutOff+ when (localTime >= timeShift timeCutOff (Delta timeClip)+--TODO || btime b >= timeShiftFromSpeed b activeItems timeCutOff || actorNewBorn b || actorDying b) $ do- let ageDisp displayed = EM.insert arena (btime b) displayed- modifyClient $ \cli -> cli {sdisplayed = ageDisp $ sdisplayed cli}- -- If considerable time passed, show delay.- let delta = btime b `timeDeltaToFrom` timeCutOff- when (delta > Delta timeClip) displayDelay -- If key will be requested, don't show the frame, because during -- the request extra message may be shown, so the other frame is better. mleader <- getsClient _sleader fact <- getsState $ (EM.! bfid b) . sfactionD let underAI = isAIFact fact- unless (Just aid == mleader && not underAI) $+ unless (Just aid == mleader && not underAI) $ do -- Something new is gonna happen on this level (otherwise we'd send -- @UpdAgeLevel@ later on, with a larger time increment), -- so show crrent game state, before it changes.- displayPush+ -- If considerable time passed, show delay. TODO: do this more+ -- accurately --- check if, eg., projectiles generated enough+ -- frames to cover the delay and if not, add here, too.+ -- Right now, if even one projectile flies, the whole 4-clip delay+ -- is skipped.+ let delta = localTime `timeDeltaToFrom` timeCutOff+ when (delta > Delta timeClip && not (bproj b))+ displayDelay+ let ageDisp = EM.insert arena localTime+ modifyClient $ \cli -> cli {sdisplayed = ageDisp $ sdisplayed cli}+ displayPush "" +setLastSlot :: MonadClientUI m => ActorId -> ItemId -> CStore -> m ()+setLastSlot aid iid cstore = do+ b <- getsState $ getActorBody aid+ mleader <- getsClient _sleader+ when (Just aid == mleader) $ do+ (itemSlots, _) <- getsClient sslots+ case lookup iid $ map swap $ EM.assocs itemSlots of+ Just l -> modifyClient $ \cli -> cli { slastSlot = l+ , slastStore = cstore }+ Nothing -> assert `failure` (iid, cstore, aid, b)+ strike :: MonadClientUI m- => ActorId -> ActorId -> ItemId -> HitAtomic -> m ()-strike source target iid hitStatus = assert (source /= target) $ do+ => ActorId -> ActorId -> ItemId -> CStore -> HitAtomic -> m ()+strike source target iid cstore hitStatus = assert (source /= target) $ do itemToF <- itemToFullClient sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target@@ -781,24 +866,28 @@ tpart <- partActorLeader target tb spronoun <- partPronounLeader source sb localTime <- getsState $ getLocalTime (blid sb)- -- We don't show the charging status in the weapon strike message. Succinct.- let itemFull = itemToF iid (1, [])+ bag <- getsState $ getActorBag source cstore+ let kit = EM.findWithDefault (1, []) iid bag+ itemFull = itemToF iid kit verb = case itemDisco itemFull of Nothing -> "hit" -- not identified Just ItemDisco{itemKind} -> IK.iverbHit itemKind isOrgan = iid `EM.member` borgan sb partItemChoice = if isOrgan- then partItemWownW spronoun (CActor source COrgan) (blid sb) localTime- -- The @CEqp@ store may be fake, but it results in an item description- -- for an active item, which is the right one for a weapon.- else partItemAW (CActor source CEqp) (blid sb) localTime+ then partItemWownW spronoun COrgan (blid sb) localTime+ else partItemAW cstore (blid sb) localTime msg HitClear = makeSentence $ [MU.SubjectVerbSg spart verb, tpart] ++ if bproj sb then [] else ["with", partItemChoice itemFull] msg (HitBlock n) =+ -- This sounds funny when the victim falls down immediately,+ -- 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. let sActs = if bproj sb then [ MU.SubjectVerbSg spart "connect" ]
Game/LambdaHack/Client/UI/DrawClient.hs view
@@ -8,9 +8,9 @@ import Control.Exception.Assert.Sugar import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES-import qualified Data.IntMap.Strict as IM import Data.List import Data.Maybe+import Data.Ord import Data.Text (Text) import qualified Data.Text as T @@ -19,6 +19,7 @@ import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.Animation+import qualified Game.LambdaHack.Common.Ability as Ability import Game.LambdaHack.Common.Actor as Actor import Game.LambdaHack.Common.ActorState import qualified Game.LambdaHack.Common.Color as Color@@ -35,6 +36,7 @@ import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray+import Game.LambdaHack.Common.Request import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time@@ -54,12 +56,12 @@ -- to the frontends to display separately or overlay over map, -- depending on the frontend. draw :: MonadClient m- => Bool -> ColorMode -> LevelId+ => ColorMode -> LevelId -> Maybe Point -> Maybe Point -> Maybe (PointArray.Array BfsDistance, Maybe [Point]) -> (Text, Maybe Text) -> (Text, Maybe Text) -> Overlay -> m SingleFrame-draw sfBlank dm drawnLevelId cursorPos tgtPos bfsmpathRaw+draw dm drawnLevelId cursorPos tgtPos bfsmpathRaw (cursorDesc, mcursorHP) (targetDesc, mtargetHP) sfTop = do cops <- getsState scops mleader <- getsClient _sleader@@ -95,12 +97,9 @@ let tile = lvl `at` pos0 tk = tokind tile floorBag = EM.findWithDefault EM.empty pos0 $ lfloor lvl- (letterSlots, numberSlots) = sslots cli- bagLetterSlots = EM.filter (`EM.member` floorBag) letterSlots- bagNumberSlots = IM.filter (`EM.member` floorBag) numberSlots- floorIids = reverse (EM.elems bagLetterSlots)- ++ IM.elems bagNumberSlots- ++ EM.keys floorBag+ (itemSlots, _) = sslots cli+ bagItemSlots = EM.filter (`EM.member` floorBag) itemSlots+ floorIids = EM.elems bagItemSlots -- first slot will be shown sml = EM.findWithDefault timeZero pos0 lsmell smlt = sml `timeDeltaToFrom` ltime viewActor aid Actor{bsymbol, bcolor, bhp, bproj}@@ -133,7 +132,7 @@ && (elem pos0 bl || elem pos0 shiftedBTrajectory) -> ('*', atttrOnPathOrLine) -- line takes precedence over path _ | isJust stgtMode- && (maybe False (elem pos0) mpath) ->+ && maybe False (elem pos0) mpath -> (';', Color.defAttr {Color.fg = fgOnPathOrLine}) Just (aid, m) -> viewActor aid m _ | smarkSmell && sml > ltime ->@@ -180,7 +179,7 @@ in fits <> ellipsis cursorText = let n = widthTgt - T.length pathCsr - 8- in (if isJust stgtMode then "cursor>" else "Cursor:")+ in (if isJust stgtMode then "x-hair>" else "X-hair:") <+> trimTgtDesc n cursorDesc cursorGap = T.replicate (widthTgt - T.length pathCsr - T.length cursorText) " "@@ -218,6 +217,7 @@ sfLevel = -- fully evaluated let f l y = let !line = fLine y in line : l in foldl' f [] [lysize-1,lysize-2..0]+ sfBlank = False return $! SingleFrame{..} inverseVideo :: Color.Attr@@ -264,17 +264,17 @@ -- 'wait' command. slashes = ["/", "|", "\\", "|"] slashPick = slashes !! (max 0 (waitT - 1) `mod` length slashes)- checkDelta ResDelta{..} =- if resCurrentTurn < 0 || resPreviousTurn < 0- then addColor Color.BrRed -- alarming news have priority- else if resCurrentTurn > 0 || resPreviousTurn > 0- then addColor Color.BrGreen- else addAttr -- only if nothing at all noteworthy+ checkDelta ResDelta{..}+ | resCurrentTurn < 0 || resPreviousTurn < 0+ = addColor Color.BrRed -- alarming news have priority+ | resCurrentTurn > 0 || resPreviousTurn > 0+ = addColor Color.BrGreen+ | otherwise = addAttr -- only if nothing at all noteworthy calmAddAttr = checkDelta calmDelta darkPick | darkL = "." | otherwise = ":" calmHeader = calmAddAttr $ calmHeaderText <> darkPick- calmText = bcalmS <> (if darkL then slashPick else "/") <> acalmS+ calmText = bcalmS <> (if darkL then slashPick else "/") <> acalmS bracePick | bracedL = "}" | otherwise = ":" hpAddAttr = checkDelta hpDelta@@ -292,12 +292,24 @@ (T.unpack t) stats <- case mleader of Just leader -> do+ actorSk <- actorSkillsClient leader+ b <- getsState $ getActorBody leader+ localTime <- getsState $ getLocalTime (blid b) allAssocs <- fullAssocsClient leader [CEqp, COrgan] let activeItems = map snd allAssocs- damage = case strongestSlotNoFilter IK.EqpSlotWeapon allAssocs of- (_, (_, itemFull)) : _->+ calm10 = calmEnough10 b $ map snd allAssocs+ forced = assert (not $ bproj b) False+ permitted = permittedPrecious calm10 forced+ preferredPrecious = either (const False) id . permitted+ strongest = strongestMelee False localTime allAssocs+ strongestPreferred = filter (preferredPrecious . snd . snd) strongest+ damage = case strongestPreferred of+ _ | EM.findWithDefault 0 Ability.AbMelee actorSk <= 0 -> "0"+ [] -> "0"+ (_average, (_, itemFull)) : _ -> let getD :: IK.Effect -> Maybe Dice.Dice -> Maybe Dice.Dice- getD (IK.Hurt dice) _ = Just dice+ getD (IK.Hurt dice) acc = Just $ dice + fromMaybe 0 acc+ getD (IK.Burn dice) acc = Just $ dice + fromMaybe 0 acc getD _ acc = acc mdice = case itemDisco itemFull of Just ItemDisco{itemAE=Just ItemAspectEffect{jeffects}} ->@@ -316,7 +328,6 @@ <> tshow bonus <> if unknownBonus then "%?" else "%" in tdice <> tbonus- [] -> "0" return $! damage Nothing -> return "" return $! if T.null stats || T.length stats >= width then []@@ -328,28 +339,24 @@ mleader <- getsClient _sleader selected <- getsClient sselected side <- getsClient sside- s <- getState- let viewOurs (aid, Actor{bsymbol, bcolor, bhp})- | otherwise =- let cattr = Color.defAttr {Color.fg = bcolor}- sattr- | Just aid == mleader = inverseVideo- | ES.member aid selected =- -- TODO: in the future use a red rectangle instead- -- of background and mark them on the map, too;- -- also, perhaps blink all selected on the map,- -- when selection changes- if bcolor /= Color.Blue- then cattr {Color.bg = Color.Blue}- else cattr {Color.bg = Color.Magenta}- | otherwise = cattr- in ( (bhp > 0, bsymbol /= '@', bsymbol, bcolor, aid)- , Color.AttrChar sattr $ if bhp > 0 then bsymbol else '%' )- ours = filter (not . bproj . snd)- $ actorAssocs (== side) drawnLevelId s+ allOurs <- getsState $ filter ((== side) . bfid) . EM.elems . sactorD+ ours <- getsState $ filter (not . bproj . snd)+ . actorAssocs (== side) drawnLevelId+ let viewOurs (aid, Actor{bsymbol, bcolor, bhp}) =+ let cattr = Color.defAttr {Color.fg = bcolor}+ sattr+ | Just aid == mleader = inverseVideo+ | ES.member aid selected =+ -- TODO: in the future use a red rectangle instead+ -- of background and mark them on the map, too;+ -- also, perhaps blink all selected on the map,+ -- when selection changes+ if bcolor /= Color.Blue+ then cattr {Color.bg = Color.Blue}+ else cattr {Color.bg = Color.Magenta}+ | otherwise = cattr+ in Color.AttrChar sattr $ if bhp > 0 then bsymbol else '%' maxViewed = width - 2- -- Don't show anything if the only actor on the level is the leader.- -- He's clearly highlighted on the level map, anyway. star = let sattr = case ES.size selected of 0 -> Color.defAttr {Color.fg = Color.BrBlack} n | n == length ours ->@@ -357,12 +364,14 @@ _ -> Color.defAttr char = if length ours > maxViewed then '$' else '*' in Color.AttrChar sattr char- viewed = take maxViewed $ sort $ map viewOurs ours+ viewed = map viewOurs $ take maxViewed+ $ sortBy (comparing keySelected) ours addAttr t = map (Color.AttrChar Color.defAttr) (T.unpack t)- allOurs = filter ((== side) . bfid) $ EM.elems $ sactorD s- party = if length allOurs <= 1+ -- Don't show anything if the only actor in the dungeon is the leader.+ -- He's clearly highlighted on the level map, anyway.+ party = if length allOurs == 1 && length ours == 1 || length ours == 0 then []- else [star] ++ map snd viewed ++ addAttr " "+ else [star] ++ viewed ++ addAttr " " return $! party drawPlayerName :: MonadClient m => Int -> m [Color.AttrChar]
Game/LambdaHack/Client/UI/Frontend.hs view
@@ -1,7 +1,7 @@ -- | Display game data on the screen and receive user input -- using one of the available raw frontends and derived operations. module Game.LambdaHack.Client.UI.Frontend- ( -- * Connection types.+ ( -- * Connection types FrontReq(..), ChanFrontend(..) -- * Re-exported part of the raw frontend , frontendName@@ -16,21 +16,24 @@ import qualified Data.Text.IO as T import System.IO +import Data.Maybe import qualified Game.LambdaHack.Client.Key as K import Game.LambdaHack.Client.UI.Animation import Game.LambdaHack.Client.UI.Frontend.Chosen import Game.LambdaHack.Common.ClientOptions+import Game.LambdaHack.Common.Point +-- | The instructions sent by clients to the raw frontend over a channel. data FrontReq = FrontNormalFrame {frontFrame :: !SingleFrame} -- ^ show a frame- | FrontRunningFrame {frontFrame :: !SingleFrame}- -- ^ show a frame in running mode (don't insert delay between frames) | FrontDelay -- ^ perform a single explicit delay | FrontKey {frontKM :: ![K.KM], frontFr :: !SingleFrame} -- ^ flush frames, possibly show fadeout/fadein and ask for a keypress- | FrontSlides {frontClear :: ![K.KM], frontSlides :: ![SingleFrame]}+ | FrontSlides { frontClear :: ![K.KM]+ , frontSlides :: ![SingleFrame]+ , frontFromTop :: !(Maybe Bool) } -- ^ show a whole slideshow without interleaving with other clients | FrontAutoYes !Bool -- ^ set the frontend option for auto-answering prompts@@ -44,8 +47,12 @@ , requestF :: !(STM.TQueue FrontReq) } -startupF :: DebugModeCli- -> (Maybe (MVar ()) -> (ChanFrontend -> IO ()) -> IO ())+-- | Initialize the frontend and apply the given continuation to the results+-- of the initialization.+startupF :: DebugModeCli -- ^ debug settings+ -> (Maybe (MVar ())+ -> (ChanFrontend -> IO ())+ -> IO ()) -- ^ continuation -> IO () startupF dbg cont = (if sfrontendNull dbg then nullStartup@@ -63,25 +70,19 @@ promptGetKey fs [] frame = fpromptGetKey fs frame promptGetKey fs keys frame = do km <- fpromptGetKey fs frame- if km `elem` keys+ if km{K.pointer=dummyPoint} `elem` keys then return km else promptGetKey fs keys frame -getConfirmGeneric :: Bool -> RawFrontend -> [K.KM] -> SingleFrame- -> IO (Maybe Bool)+getConfirmGeneric :: Bool -> RawFrontend -> [K.KM] -> SingleFrame -> IO K.KM getConfirmGeneric autoYes fs clearKeys frame = do let DebugModeCli{sdisableAutoYes} = fdebugCli fs if autoYes && not sdisableAutoYes then do- fdisplay fs True (Just frame)- return $ Just True+ fdisplay fs (Just frame)+ return K.spaceKM else do let extraKeys = [K.spaceKM, K.escKM, K.pgupKM, K.pgdnKM]- km <- promptGetKey fs (clearKeys ++ extraKeys) frame- return $! if km == K.escKM- then Nothing- else if km == K.pgupKM- then Just False- else Just True+ promptGetKey fs (clearKeys ++ extraKeys) frame -- Read UI requests from the client and send them to the frontend, loopFrontend :: RawFrontend -> ChanFrontend -> IO ()@@ -95,13 +96,10 @@ efr <- STM.atomically $ STM.readTQueue requestF case efr of FrontNormalFrame{..} -> do- fdisplay fs False (Just frontFrame)- loop autoYes- FrontRunningFrame{..} -> do- fdisplay fs True (Just frontFrame)+ fdisplay fs (Just frontFrame) loop autoYes FrontDelay -> do- fdisplay fs False Nothing+ fdisplay fs Nothing loop autoYes FrontKey{..} -> do km <- promptGetKey fs frontKM frontFr@@ -116,18 +114,25 @@ let displayFrs frs srf = case frs of [] -> assert `failure` "null slides" `twith` frs- [x] -> do- fdisplay fs False (Just x)+ [x] | isNothing frontFromTop -> do+ fdisplay fs (Just x) writeKM K.spaceKM x : xs -> do- go <- getConfirmGeneric autoYes fs frontClear x- case go of- Nothing -> writeKM K.escKM- Just True -> displayFrs xs (x : srf)- Just False -> case srf of+ K.KM{..} <- getConfirmGeneric autoYes fs frontClear x+ case key of+ K.Esc -> writeKM K.escKM+ K.PgUp -> case srf of [] -> displayFrs frs srf y : ys -> displayFrs (y : frs) ys- displayFrs frontSlides []+ K.Space -> case xs of+ [] -> writeKM K.escKM -- hack+ _ -> displayFrs xs (x : srf)+ _ -> case xs of -- K.PgDn and any other permitted key+ [] -> displayFrs frs srf+ _ -> displayFrs xs (x : srf)+ case (frontFromTop, reverse frontSlides) of+ (Just False, r : rs) -> displayFrs [r] rs+ _ -> displayFrs frontSlides [] loop autoYes FrontAutoYes b -> loop b
Game/LambdaHack/Client/UI/Frontend/Chosen.hs view
@@ -26,7 +26,7 @@ frontendName = Chosen.frontendName data RawFrontend = RawFrontend- { fdisplay :: Bool -> Maybe SingleFrame -> IO ()+ { fdisplay :: Maybe SingleFrame -> IO () , fpromptGetKey :: SingleFrame -> IO K.KM , fsyncFrames :: IO () , fescMVar :: !(Maybe (MVar ()))@@ -36,7 +36,7 @@ chosenStartup :: DebugModeCli -> (RawFrontend -> IO ()) -> IO () chosenStartup fdebugCli cont = Chosen.startup fdebugCli $ \fs ->- cont $ RawFrontend+ cont RawFrontend { fdisplay = Chosen.fdisplay fs , fpromptGetKey = Chosen.fpromptGetKey fs , fsyncFrames = Chosen.fsyncFrames fs@@ -47,7 +47,7 @@ stdStartup :: DebugModeCli -> (RawFrontend -> IO ()) -> IO () stdStartup fdebugCli cont = Std.startup fdebugCli $ \fs ->- cont $ RawFrontend+ cont RawFrontend { fdisplay = Std.fdisplay fs , fpromptGetKey = Std.fpromptGetKey fs , fsyncFrames = Std.fsyncFrames fs@@ -59,8 +59,8 @@ nullStartup fdebugCli cont = -- Std used to fork (async) the server thread, to avoid bound thread overhead. Std.startup fdebugCli $ \_ ->- cont $ RawFrontend- { fdisplay = \_ _ -> return ()+ cont RawFrontend+ { fdisplay = \_ -> return () , fpromptGetKey = \_ -> return K.escKM , fsyncFrames = return () , fescMVar = Nothing
Game/LambdaHack/Client/UI/Frontend/Curses.hs view
@@ -61,11 +61,10 @@ -- | Output to the screen via the frontend. fdisplay :: FrontendSession -- ^ frontend session data- -> Bool -> Maybe SingleFrame -- ^ the screen frame to draw -> IO ()-fdisplay _ _ Nothing = return ()-fdisplay FrontendSession{..} _ (Just rawSF) = do+fdisplay _ Nothing = return ()+fdisplay FrontendSession{..} (Just rawSF) = do let SingleFrame{sfLevel} = overlayOverlay rawSF -- let defaultStyle = C.defaultCursesStyle -- Terminals with white background require this:@@ -93,11 +92,11 @@ -- | Display a prompt, wait for any key. fpromptGetKey :: FrontendSession -> SingleFrame -> IO K.KM fpromptGetKey sess frame = do- fdisplay sess True $ Just frame+ fdisplay sess $ Just frame nextEvent keyTranslate :: C.Key -> K.KM-keyTranslate e = (\(key, modifier) -> K.KM {..}) $+keyTranslate e = (\(key, modifier) -> K.toKM modifier key) $ case e of C.KeyChar '\ESC' -> (K.Esc, K.NoModifier) C.KeyExit -> (K.Esc, K.NoModifier)
Game/LambdaHack/Client/UI/Frontend/Gtk.hs view
@@ -15,7 +15,6 @@ import Control.Concurrent.Async import qualified Control.Concurrent.STM as STM import qualified Control.Exception as Ex hiding (handle)-import Control.Exception.Assert.Sugar import Control.Monad import Control.Monad.Reader import qualified Data.ByteString.Char8 as BS@@ -33,6 +32,7 @@ import Game.LambdaHack.Common.ClientOptions import qualified Game.LambdaHack.Common.Color as Color import Game.LambdaHack.Common.LQueue+import Game.LambdaHack.Common.Point data FrameState = FPushed -- frames stored in a queue, to be drawn in equal time intervals@@ -79,7 +79,7 @@ case fs of FPushed{..} -> putMVar sframeState FPushed{fpushed = f fpushed, ..}- _ ->+ FNone -> putMVar sframeState fs -- | The name of the frontend.@@ -101,7 +101,7 @@ unsafeInitGUIForThreadedRTS -- Text attributes. ttt <- textTagTableNew- stags <- fmap M.fromList $+ stags <- M.fromList <$> mapM (\ ak -> do tt <- textTagNew Nothing textTagTableAdd ttt tt@@ -116,7 +116,7 @@ textViewSetEditable sview False textViewSetCursorVisible sview False -- Set up the channel for keyboard input.- schanKey <- STM.atomically $ STM.newTQueue+ schanKey <- STM.atomically STM.newTQueue -- Set up the frame state. let frameState = FNone -- Create the session record.@@ -131,6 +131,9 @@ -- Fork the thread that periodically draws a frame from a queue, if any. aPoll <- async $ pollFramesAct sess `Ex.finally` postGUISync mainQuit link aPoll+ let flushChanKey = do+ res <- STM.atomically $ STM.tryReadTQueue schanKey+ when (isJust res) flushChanKey -- Fill the keyboard channel. sview `on` keyPressEvent $ do n <- eventKeyName@@ -140,28 +143,37 @@ #else let !key = K.keyTranslate n #endif- !modifier = modifierTranslate mods- readAll = do- res <- STM.atomically $ STM.tryReadTQueue schanKey- when (isJust res) $ readAll+ !modifier = let md = modifierTranslate mods+ in if md == K.Shift then K.NoModifier else md+ !pointer = dummyPoint liftIO $ do unless (deadKey n) $ do -- If ESC, also mark it specially and reset the key channel. when (key == K.Esc) $ do void $ tryPutMVar escMVar ()- readAll+ flushChanKey -- Store the key in the channel.- STM.atomically $ STM.writeTQueue schanKey K.KM{key, modifier}+ STM.atomically $ STM.writeTQueue schanKey K.KM{..} return True -- Set the font specified in config, if any. f <- fontDescriptionFromString $ fromMaybe "" sfont widgetModifyFont sview (Just f)+ liftIO $ do+ textViewSetLeftMargin sview 3+ textViewSetRightMargin sview 3 -- Prepare font chooser dialog. currentfont <- newIORef f+ Just display <- displayGetDefault+ -- TODO: change cursor depending on targeting mode, etc.; hard+ cursor <- cursorNewForDisplay display Tcross -- Target Crosshair Arrow sview `on` buttonPressEvent $ do+ liftIO flushChanKey but <- eventButton- liftIO $ case but of- RightButton -> do+ (wx, wy) <- eventCoordinates+ mods <- eventModifier+ let !modifier = modifierTranslate mods -- Shift included+ liftIO $ do+ when (but == RightButton && modifier == K.Control) $ do fsd <- fontSelectionDialogNew ("Choose font" :: String) cf <- readIORef currentfont fds <- fontDescriptionToString cf@@ -177,8 +189,28 @@ widgetModifyFont sview (Just fd) Nothing -> return () widgetDestroy fsd- return True- _ -> return False+ -- We shouldn't pass on the click if the user has selected something.+ hasSelection <- textBufferHasSelection tb+ unless hasSelection $ do+ mdrawWin <- displayGetWindowAtPointer display+ 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 !key = case but of+ LeftButton -> K.LeftButtonPress+ MiddleButton -> K.MiddleButtonPress+ RightButton -> K.RightButtonPress+ _ -> K.LeftButtonPress+ !pointer = Point cx (cy - 1)+ -- Store the mouse even coords in the keypress channel.+ STM.atomically $ STM.writeTQueue schanKey K.KM{..}+ return $! but == RightButton -- not to disable selection -- Modify default colours. let black = Color minBound minBound minBound -- Color.defBG == Color.Black white = Color 0xC500 0xBC00 0xB800 -- Color.defFG == Color.White@@ -244,7 +276,7 @@ microInSec = 1000000 defaultMaxFps :: Int-defaultMaxFps = 15+defaultMaxFps = 30 -- | Poll the frame queue often and draw frames at fixed intervals. pollFramesWait :: FrontendSession -> ClockTime -> IO ()@@ -300,7 +332,7 @@ -- (otherwise it would change the state), so poll often. threadDelay $ microInSec `div` maxPolls maxFps pollFramesAct sess- _ -> do+ FNone -> do putMVar sframeState fs -- Not in the Push state, so poll lazily to catch the next state change. -- The slow polling also gives the game logic a head start@@ -311,8 +343,8 @@ -- | Add a game screen frame to the frame drawing channel, or show -- it ASAP if @immediate@ display is requested and the channel is empty.-pushFrame :: FrontendSession -> Bool -> Bool -> Maybe SingleFrame -> IO ()-pushFrame sess noDelay immediate rawFrame = do+pushFrame :: FrontendSession -> Bool -> Maybe SingleFrame -> IO ()+pushFrame sess immediate rawFrame = do let FrontendSession{sframeState, slastFull} = sess -- Full evaluation is done outside the mvar locks. let !frame = case rawFrame of@@ -329,24 +361,27 @@ case fs of FPushed{..} -> putMVar sframeState- $ if isNothing nextFrame && anyFollowed+ $ if isNothing nextFrame && anyFollowed && isJust rawFrame then fs -- old news else FPushed{fpushed = writeLQueue fpushed nextFrame, ..} FNone | immediate -> do -- If the frame not repeated, draw it.- maybe skip (postGUIAsync . output sess) nextFrame+ maybe (return ()) (postGUIAsync . output sess) nextFrame -- Frame sent, we may now safely release the queue lock. putMVar sframeState FNone FNone ->- -- Never start playing with an empty frame.- let fpushed = if isJust nextFrame- then writeLQueue newLQueue nextFrame- else newLQueue- fshown = dummyFrame- in putMVar sframeState FPushed{..}+ putMVar sframeState+ $ if isNothing nextFrame && anyFollowed && isJust rawFrame+ then fs -- old news+ else FPushed{ fpushed = writeLQueue newLQueue nextFrame+ , fshown = dummyFrame } case nextFrame of- Nothing -> putMVar slastFull (lastFrame, True)- Just f -> putMVar slastFull (f, noDelay)+ Nothing -> putMVar slastFull (lastFrame, not (case fs of+ FNone -> True+ FPushed{} -> False+ && immediate+ && not anyFollowed))+ Just f -> putMVar slastFull (f, False) evalFrame :: FrontendSession -> SingleFrame -> GtkFrame evalFrame FrontendSession{stags} rawSF =@@ -379,7 +414,7 @@ then Nothing -- no sense repeating else Just frame -- Draw the last frame ASAP.- maybe skip (postGUIAsync . output sess) nextFrame+ maybe (return ()) (postGUIAsync . output sess) nextFrame Nothing -> return () FNone -> return () -- Wipe out the frame queue. Release the lock.@@ -387,10 +422,9 @@ -- | Add a frame to be drawn. fdisplay :: FrontendSession -- ^ frontend session data- -> Bool -> Maybe SingleFrame -- ^ the screen frame to draw -> IO ()-fdisplay sess noDelay = pushFrame sess noDelay False+fdisplay sess = pushFrame sess False -- Display all queued frames, synchronously. displayAllFramesSync :: FrontendSession -> FrameState -> IO ()@@ -417,7 +451,7 @@ Nothing -> -- The queue is empty. return ()- _ ->+ FNone -> -- Not in Push state to start with. return () @@ -433,7 +467,7 @@ fpromptGetKey :: FrontendSession -> SingleFrame -> IO K.KM fpromptGetKey sess@FrontendSession{..} frame = do- pushFrame sess True True $ Just frame+ pushFrame sess True $ Just frame km <- STM.atomically $ STM.readTQueue schanKey case km of K.KM{key=K.Space} ->@@ -470,24 +504,27 @@ -- | Translates modifiers to our own encoding. modifierTranslate :: [Modifier] -> K.Modifier-modifierTranslate mods =- if Control `elem` mods then K.Control else K.NoModifier+modifierTranslate mods+ | Control `elem` mods = K.Control+ | any (`elem` mods) [Meta, Super, Alt, Alt2, Alt3, Alt4, Alt5] = K.Alt+ | Shift `elem` mods = K.Shift+ | otherwise = K.NoModifier doAttr :: DebugModeCli -> TextTag -> Color.Attr -> IO () doAttr sdebugCli tt attr@Color.Attr{fg, bg} | attr == Color.defAttr = return ()- | fg == Color.defFG = set tt $ extraAttr sdebugCli- ++ [textTagBackground := Color.colorToRGB bg]- | bg == Color.defBG = set tt $ extraAttr sdebugCli- ++ [textTagForeground := Color.colorToRGB fg]- | otherwise = set tt $ extraAttr sdebugCli- ++ [ textTagForeground := Color.colorToRGB fg- , textTagBackground := Color.colorToRGB bg ]+ | fg == Color.defFG =+ set tt $ extraAttr sdebugCli+ ++ [textTagBackground := Color.colorToRGB bg]+ | bg == Color.defBG =+ set tt $ extraAttr sdebugCli+ ++ [textTagForeground := Color.colorToRGB fg]+ | otherwise =+ set tt $ extraAttr sdebugCli+ ++ [ textTagForeground := Color.colorToRGB fg+ , textTagBackground := Color.colorToRGB bg ] extraAttr :: DebugModeCli -> [AttrOp TextTag] extraAttr DebugModeCli{scolorIsBold} =- if scolorIsBold == Just True- then [ textTagWeight := fromEnum WeightBold+ [textTagWeight := fromEnum WeightBold | scolorIsBold == Just True] -- , textTagStretch := StretchUltraExpanded- ]- else []
Game/LambdaHack/Client/UI/Frontend/Std.hs view
@@ -39,11 +39,10 @@ -- | Output to the screen via the frontend. fdisplay :: FrontendSession -- ^ frontend session data- -> Bool -> Maybe SingleFrame -- ^ the screen frame to draw -> IO ()-fdisplay _ _ Nothing = return ()-fdisplay _ _ (Just rawSF) =+fdisplay _ Nothing = return ()+fdisplay _ (Just rawSF) = let SingleFrame{sfLevel} = overlayOverlay rawSF bs = map (BS.pack . map Color.acChar . decodeLine) sfLevel ++ [BS.empty] in mapM_ BS.putStrLn bs@@ -63,11 +62,11 @@ -- | Display a prompt, wait for any key. fpromptGetKey :: FrontendSession -> SingleFrame -> IO K.KM fpromptGetKey sess frame = do- fdisplay sess True $ Just frame+ fdisplay sess $ Just frame nextEvent keyTranslate :: Char -> K.KM-keyTranslate e = (\(key, modifier) -> K.KM {..}) $+keyTranslate e = (\(key, modifier) -> K.toKM modifier key) $ case e of '\ESC' -> (K.Esc, K.NoModifier) '\n' -> (K.Return, K.NoModifier)
Game/LambdaHack/Client/UI/Frontend/Vty.hs view
@@ -10,19 +10,20 @@ import Control.Concurrent import Control.Concurrent.Async+import qualified Control.Concurrent.STM as STM import qualified Control.Exception as Ex hiding (handle)+import Control.Monad import Data.Default+import Data.Maybe import Graphics.Vty import qualified Graphics.Vty as Vty-import qualified Control.Concurrent.STM as STM-import Control.Monad-import Data.Maybe import qualified Game.LambdaHack.Client.Key as K import Game.LambdaHack.Client.UI.Animation import Game.LambdaHack.Common.ClientOptions import qualified Game.LambdaHack.Common.Color as Color import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.Point -- | Session data maintained by the frontend. data FrontendSession = FrontendSession@@ -41,7 +42,7 @@ startup :: DebugModeCli -> (FrontendSession -> IO ()) -> IO () startup sdebugCli k = do svty <- mkVty def- schanKey <- STM.atomically $ STM.newTQueue+ schanKey <- STM.atomically STM.newTQueue escMVar <- newEmptyMVar let sess = FrontendSession{sescMVar = Just escMVar, ..} void $ async $ storeKeys sess@@ -55,28 +56,28 @@ EvKey n mods -> do let !key = keyTranslate n !modifier = modifierTranslate mods+ !pointer = dummyPoint readAll = do res <- STM.atomically $ STM.tryReadTQueue schanKey- when (isJust res) $ readAll+ when (isJust res) readAll -- If ESC, also mark it specially and reset the key channel. case sescMVar of- Just escMVar -> do+ Just escMVar -> when (key == K.Esc) $ do void $ tryPutMVar escMVar () readAll Nothing -> return () -- Store the key in the channel.- STM.atomically $ STM.writeTQueue schanKey K.KM{key, modifier}+ STM.atomically $ STM.writeTQueue schanKey K.KM{..} _ -> return () storeKeys sess -- | Output to the screen via the frontend. fdisplay :: FrontendSession -- ^ frontend session data- -> Bool -> Maybe SingleFrame -- ^ the screen frame to draw -> IO ()-fdisplay _ _ Nothing = return ()-fdisplay FrontendSession{svty} _ (Just rawSF) =+fdisplay _ Nothing = return ()+fdisplay FrontendSession{svty} (Just rawSF) = let SingleFrame{sfLevel} = overlayOverlay rawSF img = (foldr (<->) emptyImage . map (foldr (<|>) emptyImage@@ -111,7 +112,7 @@ -- | Display a prompt, wait for any key. fpromptGetKey :: FrontendSession -> SingleFrame -> IO K.KM fpromptGetKey sess frame = do- fdisplay sess True $ Just frame+ fdisplay sess $ Just frame nextKeyEvent sess -- TODO: Ctrl-Home and Ctrl-End are the same as Home and End on some terminals@@ -142,8 +143,11 @@ -- | Translates modifiers to our own encoding. modifierTranslate :: [Modifier] -> K.Modifier-modifierTranslate mods =- if MCtrl `elem` mods then K.Control else K.NoModifier+modifierTranslate mods+ | MCtrl `elem` mods = K.Control+ | MAlt `elem` mods = K.Alt+ | MShift `elem` mods = K.Shift+ | otherwise = K.NoModifier -- TODO: with vty 5.0 check if bold is still needed. -- A hack to get bright colors via the bold attribute. Depending on terminal
Game/LambdaHack/Client/UI/HandleHumanClient.hs view
@@ -19,14 +19,14 @@ -- invoked in targeting mode on a remote level (level different than -- the level of the leader). cmdHumanSem :: MonadClientUI m => HumanCmd -> m (SlideOrCmd RequestUI)-cmdHumanSem cmd = do+cmdHumanSem cmd = if noRemoteHumanCmd cmd then do -- If in targeting mode, check if the current level is the same -- as player level and refuse performing the action otherwise. arena <- getArenaUI lidV <- viewedLevel- if (arena /= lidV) then- failWith $ "command disabled on a remote level, press ESC to switch back"+ if arena /= lidV then+ failWith "command disabled on a remote level, press ESC to switch back" else cmdAction cmd else cmdAction cmd @@ -34,16 +34,20 @@ cmdAction :: MonadClientUI m => HumanCmd -> m (SlideOrCmd RequestUI) cmdAction cmd = case cmd of -- Global.- Move v -> fmap anyToUI <$> moveRunHuman False v- Run v -> fmap anyToUI <$> moveRunHuman True v+ Move v -> fmap anyToUI <$> moveRunHuman True True False False v+ Run v -> fmap anyToUI <$> moveRunHuman True True True True v Wait -> Right <$> fmap ReqUITimed waitHuman MoveItem cLegalRaw toCStore mverb _ auto -> fmap ReqUITimed <$> moveItemHuman cLegalRaw toCStore mverb auto+ DescribeItem cstore -> fmap ReqUITimed <$> describeItemHuman cstore Project ts -> fmap ReqUITimed <$> projectHuman ts Apply ts -> fmap ReqUITimed <$> applyHuman ts AlterDir ts -> fmap ReqUITimed <$> alterDirHuman ts TriggerTile ts -> fmap ReqUITimed <$> triggerTileHuman ts- StepToTarget -> fmap anyToUI <$> stepToTargetHuman+ RunOnceAhead -> fmap anyToUI <$> runOnceAheadHuman+ MoveOnceToCursor -> fmap anyToUI <$> moveOnceToCursorHuman+ RunOnceToCursor -> fmap anyToUI <$> runOnceToCursorHuman+ ContinueToCursor -> fmap anyToUI <$> continueToCursorHuman GameRestart t -> gameRestartHuman t GameExit -> gameExitHuman@@ -56,11 +60,11 @@ PickLeader k -> Left <$> pickLeaderHuman k MemberCycle -> Left <$> memberCycleHuman MemberBack -> Left <$> memberBackHuman- DescribeItem cstore -> Left <$> describeItemHuman cstore- AllOwned -> Left <$> allOwnedHuman- SelectActor -> Left <$> selectActorHuman+ SelectActor -> addNoSlides selectActorHuman SelectNone -> addNoSlides selectNoneHuman Clear -> addNoSlides clearHuman+ StopIfTgtMode -> addNoSlides stopIfTgtModeHuman+ SelectWithPointer -> addNoSlides selectWithPointer Repeat n -> addNoSlides $ repeatHuman n Record -> Left <$> recordHuman History -> Left <$> historyHuman@@ -74,14 +78,18 @@ MoveCursor v k -> Left <$> moveCursorHuman v k TgtFloor -> Left <$> tgtFloorHuman TgtEnemy -> Left <$> tgtEnemyHuman- TgtUnknown -> Left <$> tgtUnknownHuman- TgtItem -> Left <$> tgtItemHuman- TgtStair up -> Left <$> tgtStairHuman up TgtAscend k -> Left <$> tgtAscendHuman k EpsIncr b -> Left <$> epsIncrHuman b TgtClear -> Left <$> tgtClearHuman+ CursorUnknown -> Left <$> cursorUnknownHuman+ CursorItem -> Left <$> cursorItemHuman+ CursorStair up -> Left <$> cursorStairHuman up Cancel -> Left <$> cancelHuman mainMenuHuman Accept -> Left <$> acceptHuman helpHuman+ CursorPointerFloor -> addNoSlides cursorPointerFloorHuman+ CursorPointerEnemy -> addNoSlides cursorPointerEnemyHuman+ TgtPointerFloor -> Left <$> tgtPointerFloorHuman+ TgtPointerEnemy -> Left <$> tgtPointerEnemyHuman addNoSlides :: Monad m => m () -> m (SlideOrCmd RequestUI) addNoSlides cmdCli = cmdCli >> return (Left mempty)
Game/LambdaHack/Client/UI/HandleHumanGlobalClient.hs view
@@ -7,9 +7,10 @@ -- TODO: document module Game.LambdaHack.Client.UI.HandleHumanGlobalClient ( -- * Commands that usually take time- moveRunHuman, waitHuman, moveItemHuman+ moveRunHuman, waitHuman, moveItemHuman, describeItemHuman , projectHuman, applyHuman, alterDirHuman, triggerTileHuman- , stepToTargetHuman+ , runOnceAheadHuman, moveOnceToCursorHuman+ , runOnceToCursorHuman, continueToCursorHuman -- * Commands that never take time , gameRestartHuman, gameExitHuman, gameSaveHuman, tacticHuman, automateHuman ) where@@ -22,7 +23,6 @@ import Data.List import Data.Maybe import Data.Monoid-import qualified Data.Text as T import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Client.BfsClient@@ -64,16 +64,42 @@ -- * Move and Run moveRunHuman :: MonadClientUI m- => Bool -> Vector -> m (SlideOrCmd RequestAnyAbility)-moveRunHuman run dir = do+ => Bool -> Bool -> Bool -> Bool -> Vector+ -> m (SlideOrCmd RequestAnyAbility)+moveRunHuman initialStep finalGoal run runAhead dir = do tgtMode <- getsClient stgtMode if isJust tgtMode then- fmap Left $ moveCursorHuman dir (if run then 10 else 1)+ Left <$> moveCursorHuman dir (if run then 10 else 1) else do arena <- getArenaUI leader <- getLeaderUI sb <- getsState $ getActorBody leader fact <- getsState $ (EM.! bfid sb) . sfactionD+ -- Start running in the given direction. The first turn of running+ -- succeeds much more often than subsequent turns, because we ignore+ -- most of the disturbances, since the player is mostly aware of them+ -- and still explicitly requests a run, knowing how it behaves.+ sel <- getsClient sselected+ let runMembers = if runAhead || noRunWithMulti fact+ then [leader] -- TODO: warn?+ else ES.toList (ES.delete leader sel) ++ [leader]+ runParams = RunParams { runLeader = leader+ , runMembers+ , runInitial = True+ , runStopMsg = Nothing+ , runWaiting = 0 }+ macroRun25 = ["CTRL-comma", "CTRL-V"]+ when (initialStep && run) $ do+ modifyClient $ \cli ->+ cli {srunning = Just runParams}+ when runAhead $+ modifyClient $ \cli ->+ cli {slastPlay = map K.mkKM macroRun25 ++ slastPlay cli}+ -- When running, the invisible actor is hit (not displaced!),+ -- so that running in the presence of roving invisible+ -- actors is equivalent to moving (with visible actors+ -- this is not a problem, since runnning stops early enough).+ -- TODO: stop running at invisible actor let tpos = bpos sb `shift` dir -- We start by checking actors at the the target position, -- which gives a partial information (actors can be invisible),@@ -82,39 +108,22 @@ tgts <- getsState $ posToActors tpos arena case tgts of [] -> do -- move or search or alter- -- Start running in the given direction. The first turn of running- -- succeeds much more often than subsequent turns, because we ignore- -- most of the disturbances, since the player is mostly aware of them- -- and still explicitly requests a run, knowing how it behaves.- runStopOrCmd <- moveRunAid leader dir+ runStopOrCmd <- moveSearchAlterAid leader dir case runStopOrCmd of Left stopMsg -> failWith stopMsg- Right runCmd@(RequestAnyAbility ReqMove{}) -> do- sel <- getsClient sselected- let runMembers = if noRunWithMulti fact- then [leader] -- TODO: warn?- else ES.toList (ES.delete leader sel) ++ [leader]- runParams = RunParams { runLeader = leader- , runMembers- , runDist = 0- , runStopMsg = Nothing- , runInitDir = Just dir }- when run $ modifyClient $ \cli -> cli {srunning = Just runParams}+ Right runCmd ->+ -- Don't check @initialStep@ and @finalGoal@+ -- and don't stop going to target: door opening is mundane enough. return $ Right runCmd- -- Any other command does ends the run immediately.- Right runCmd -> return $ Right runCmd- -- When running, the invisible actor is hit (not displaced!),- -- so that running in the presence of roving invisible- -- actors is equivalent to moving (with visible actors- -- this is not a problem, since runnning stops early enough).- -- TODO: stop running at invisible actor- [((target, _), _)] | run ->+ [((target, _), _)] | run && initialStep ->+ -- No @stopPlayBack@: initial displace is benign enough. -- Displacing requires accessibility, but it's checked later on. fmap RequestAnyAbility <$> displaceAid target- _ : _ : _ | run -> do- assert (all (bproj . snd . fst) tgts) skip+ _ : _ : _ | run && initialStep -> do+ let !_A = assert (all (bproj . snd . fst) tgts) () failSer DisplaceProjectiles- ((target, tb), _) : _ -> do+ ((target, tb), _) : _ | initialStep && finalGoal -> do+ stopPlayBack -- don't ever auto-repeat melee -- No problem if there are many projectiles at the spot. We just -- attack the first one. -- We always see actors from our own faction.@@ -124,16 +133,17 @@ else do -- Select adjacent actor by bumping into him. Takes no time. success <- pickLeader True target- assert (success `blame` "bump self"- `twith` (leader, target, tb)) skip+ let !_A = assert (success `blame` "bump self"+ `twith` (leader, target, tb)) () return $ Left mempty else -- Attacking does not require full access, adjacency is enough. fmap RequestAnyAbility <$> meleeAid target+ _ : _ -> failWith "actor in the way" -- | Actor atttacks an enemy actor or his own projectile. meleeAid :: MonadClientUI m- => ActorId -> m (SlideOrCmd (RequestTimed AbMelee))+ => ActorId -> m (SlideOrCmd (RequestTimed 'AbMelee)) meleeAid target = do leader <- getLeaderUI sb <- getsState $ getActorBody leader@@ -141,18 +151,18 @@ sfact <- getsState $ (EM.! bfid sb) . sfactionD mel <- pickWeaponClient leader target case mel of- [] -> failWith "nothing to melee with"- wp : _ -> do+ Nothing -> failWith "nothing to melee with"+ Just wp -> do let returnCmd = return $ Right wp res | bproj tb || isAtWar sfact (bfid tb) = returnCmd | isAllied sfact (bfid tb) = do go1 <- displayYesNo ColorBW "You are bound by an alliance. Really attack?"- if not go1 then failWith "Attack canceled." else returnCmd+ if not go1 then failWith "attack canceled" else returnCmd | otherwise = do go2 <- displayYesNo ColorBW "This attack will start a war. Are you sure?"- if not go2 then failWith "Attack canceled." else returnCmd+ if not go2 then failWith "attack canceled" else returnCmd res -- Seeing the actor prevents altering a tile under it, but that -- does not limit the player, he just doesn't waste a turn@@ -160,7 +170,7 @@ -- | Actor swaps position with another. displaceAid :: MonadClientUI m- => ActorId -> m (SlideOrCmd (RequestTimed AbDisplace))+ => ActorId -> m (SlideOrCmd (RequestTimed 'AbDisplace)) displaceAid target = do cops <- getsState scops leader <- getLeaderUI@@ -169,9 +179,8 @@ tfact <- getsState $ (EM.! bfid tb) . sfactionD activeItems <- activeItemsClient target disp <- getsState $ dispEnemy leader target activeItems- actorSk <- getsState $ maxActorSkills target activeItems- let immobile = EM.findWithDefault 0 AbDisplace actorSk <= 0- && EM.findWithDefault 0 AbMove actorSk <= 0+ let actorMaxSk = sumSkills activeItems+ immobile = EM.findWithDefault 0 AbMove actorMaxSk <= 0 spos = bpos sb tpos = bpos tb adj = checkAdjacent sb tb@@ -192,15 +201,60 @@ tgts <- getsState $ posToActors tpos lid case tgts of [] -> assert `failure` (leader, sb, target, tb)- [_] -> do- return $ Right $ ReqDisplace target+ [_] -> return $ Right $ ReqDisplace target _ -> failSer DisplaceProjectiles else failSer DisplaceAccess +-- | Actor moves or searches or alters. No visible actor at the position.+moveSearchAlterAid :: MonadClient m+ => ActorId -> Vector -> m (Either Msg RequestAnyAbility)+moveSearchAlterAid source dir = do+ cops@Kind.COps{cotile} <- getsState scops+ sb <- getsState $ getActorBody source+ let lid = blid sb+ lvl <- getLevel lid+ let spos = bpos sb -- source position+ tpos = spos `shift` dir -- target position+ t = lvl `at` tpos+ runStopOrCmd+ -- Movement requires full access.+ | accessible cops lvl spos tpos =+ -- A potential invisible actor is hit. War started without asking.+ Right $ RequestAnyAbility $ ReqMove dir+ -- No access, so search and/or alter the tile. Non-walkability is+ -- not implied by the lack of access.+ | not (Tile.isWalkable cotile t)+ && (not (knownLsecret lvl)+ || (isSecretPos lvl tpos -- possible secrets here+ && (Tile.isSuspect cotile t -- not yet searched+ || Tile.hideAs cotile t /= t)) -- search again+ || Tile.isOpenable cotile t+ || Tile.isClosable cotile t+ || Tile.isChangeable cotile t)+ = if EM.member tpos $ lfloor lvl then+ Left $ showReqFailure AlterBlockItem+ else+ Right $ RequestAnyAbility $ ReqAlter tpos Nothing+ -- We don't use MoveSer, because we don't hit invisible actors.+ -- The potential invisible actor, e.g., in a wall or in+ -- an inaccessible doorway, is made known, taking a turn.+ -- If server performed an attack for free+ -- on the invisible actor anyway, the player (or AI)+ -- would be tempted to repeatedly hit random walls+ -- in hopes of killing a monster lurking within.+ -- If the action had a cost, misclicks would incur the cost, too.+ -- Right now the player may repeatedly alter tiles trying to learn+ -- about invisible pass-wall actors, but when an actor detected,+ -- it costs a turn and does not harm the invisible actors,+ -- so it's not so tempting.+ -- Ignore a known boring, not accessible tile.+ | otherwise = Left "never mind"+ return $! runStopOrCmd+ -- * Wait -- | Leader waits a turn (and blocks, etc.).-waitHuman :: MonadClientUI m => m (RequestTimed AbWait)+waitHuman :: MonadClientUI m => m (RequestTimed 'AbWait) waitHuman = do modifyClient $ \cli -> cli {swaitTimes = abs (swaitTimes cli) + 1} return ReqWait@@ -208,76 +262,122 @@ -- * MoveItem moveItemHuman :: MonadClientUI m- => [CStore] -> CStore -> (Maybe MU.Part) -> Bool- -> m (SlideOrCmd (RequestTimed AbMoveItem))+ => [CStore] -> CStore -> Maybe MU.Part -> Bool+ -> m (SlideOrCmd (RequestTimed 'AbMoveItem)) moveItemHuman cLegalRaw destCStore mverb auto = do- assert (destCStore `notElem` cLegalRaw) skip+ let !_A = assert (destCStore `notElem` cLegalRaw) () let verb = fromMaybe (MU.Text $ verbCStore destCStore) mverb leader <- getLeaderUI b <- getsState $ getActorBody leader activeItems <- activeItemsClient leader- let cLegal = if calmEnough b activeItems- then cLegalRaw- else if destCStore == CSha- then []- else delete CSha cLegalRaw+ -- This calmE is outdated when one of the items increases max Calm+ -- (e.g., in pickup, which handles many items at once), but this is OK,+ -- the server accepts item movement based on calm at the start, not end+ -- or in the middle.+ -- The calmE is inaccurate also if an item not IDed, but that's intended+ -- and the server will ignore and warn (and content may avoid that,+ -- e.g., making all rings identified)+ let calmE = calmEnough b activeItems+ cLegal | calmE = cLegalRaw+ | destCStore == CSha = []+ | otherwise = delete CSha cLegalRaw+ ret4 :: MonadClientUI m+ => CStore -> [(ItemId, ItemFull)]+ -> Int -> [(ItemId, Int, CStore, CStore)]+ -> m (Either Slideshow [(ItemId, Int, CStore, CStore)])+ ret4 _ [] _ acc = return $ Right $ reverse acc+ ret4 fromCStore ((iid, itemFull) : rest) oldN acc = do+ let k = itemK itemFull+ retRec toCStore =+ let n = oldN + if toCStore == CEqp then k else 0+ in ret4 fromCStore rest n ((iid, k, fromCStore, toCStore) : acc)+ if cLegalRaw == [CGround] -- normal pickup+ then case destCStore of+ CEqp | calmE && goesIntoSha itemFull ->+ retRec CSha+ CEqp | goesIntoInv itemFull ->+ retRec CInv+ CEqp | eqpOverfull b (oldN + k) -> do+ msgAdd $ "Warning:" <+> showReqFailure EqpOverfull <> "."+ retRec CInv+ _ ->+ retRec destCStore+ else case destCStore of+ CEqp | eqpOverfull b (oldN + k) -> failSer EqpOverfull+ _ -> retRec destCStore ggi <- if auto- then getAnyItem verb cLegalRaw cLegal False False- else getAnyItem verb cLegalRaw cLegal True True+ then getAnyItems verb cLegalRaw cLegal False False+ else getAnyItems verb cLegalRaw cLegal True True case ggi of- Right ((iid, itemFull), CActor _ fromCStore) -> do- let k = itemK itemFull- retReq toCStore =- return $ Right $ ReqMoveItem iid k fromCStore toCStore- if fromCStore == CGround- then case destCStore of- CEqp | goesIntoInv (itemBase itemFull) ->- retReq CInv- CEqp | eqpOverfull b k -> do- msgAdd $ "Warning:" <+> showReqFailure EqpOverfull <> "."- retReq CInv- _ ->- retReq destCStore- else case destCStore of- CEqp | eqpOverfull b k -> failSer EqpOverfull- _ -> retReq destCStore+ Right (l, MStore fromCStore) -> do+ leader2 <- getLeaderUI+ b2 <- getsState $ getActorBody leader2+ activeItems2 <- activeItemsClient leader2+ let calmE2 = calmEnough b2 activeItems2+ -- This is not ideal, because the failure message comes late,+ -- but it's simple and good enough.+ if not calmE2 && destCStore == CSha then failSer ItemNotCalm+ else do+ l4 <- ret4 fromCStore l 0 []+ return $! case l4 of+ Left sli -> Left sli+ Right [] -> assert `failure` ggi+ Right lr -> Right $ ReqMoveItems lr Left slides -> return $ Left slides _ -> assert `failure` ggi +-- * DescribeItem++-- | Display items from a given container store and describe the chosen one.+describeItemHuman :: MonadClientUI m+ => ItemDialogMode -> m (SlideOrCmd (RequestTimed 'AbMoveItem))+describeItemHuman = describeItemC+ -- * Project -projectHuman :: MonadClientUI m- => [Trigger] -> m (SlideOrCmd (RequestTimed AbProject))+projectHuman :: forall m. MonadClientUI m+ => [Trigger] -> m (SlideOrCmd (RequestTimed 'AbProject)) projectHuman ts = do leader <- getLeaderUI- tgtPos <- leaderTgtToPos+ lidV <- viewedLevel+ oldTgtMode <- getsClient stgtMode+ -- Show the targeting line, temporarily.+ modifyClient $ \cli -> cli {stgtMode = Just $ TgtMode lidV}+ -- Set cursor to the personal target, permanently. tgt <- getsClient $ getTarget leader- case tgtPos of- Nothing -> failWith "last target invalid"- Just pos -> do- -- Set cursor to the personal target, temporarily.- oldCursor <- getsClient scursor- modifyClient $ \cli -> cli {scursor = fromMaybe (scursor cli) tgt}- -- Show the targeting line, temporarily.- oldTgtMode <- getsClient stgtMode- lidV <- viewedLevel- modifyClient $ \cli -> cli {stgtMode = Just $ TgtMode lidV}- canAim <- leaderTgtAims- oldEps <- getsClient seps- outcome <- case canAim of- Right newEps -> do- -- Modify @seps@,, temporarily.- modifyClient $ \cli -> cli {seps = newEps}- projectPos ts pos+ modifyClient $ \cli -> cli {scursor = fromMaybe (scursor cli) tgt}+ -- Let the user pick the item to fling.+ let posFromCursor :: m (Either Msg Point)+ posFromCursor = do+ canAim <- aidTgtAims leader lidV Nothing+ case canAim of+ Right newEps -> do+ -- Modify @seps@, permanently.+ modifyClient $ \cli -> cli {seps = newEps}+ mpos <- aidTgtToPos leader lidV Nothing+ case mpos of+ Nothing -> assert `failure` (tgt, leader, lidV)+ Just pos -> do+ munit <- projectCheck pos+ case munit of+ Nothing -> return $ Right pos+ Just reqFail -> return $ Left $ showReqFailure reqFail+ Left cause -> return $ Left cause+ mitem <- projectItem ts posFromCursor+ outcome <- case mitem of+ Right (iid, fromCStore) -> do+ mpos <- posFromCursor+ case mpos of+ Right pos -> do+ eps <- getsClient seps+ return $ Right $ ReqProject pos eps iid fromCStore Left cause -> failWith cause- modifyClient $ \cli -> cli { stgtMode = oldTgtMode- , scursor = oldCursor- , seps = oldEps }- return outcome+ Left sli -> return $ Left sli+ modifyClient $ \cli -> cli {stgtMode = oldTgtMode}+ return outcome -projectPos :: MonadClientUI m- => [Trigger] -> Point -> m (SlideOrCmd (RequestTimed AbProject))-projectPos ts tpos = do+projectCheck :: MonadClientUI m => Point -> m (Maybe ReqFailure)+projectCheck tpos = do Kind.COps{cotile} <- getsState scops leader <- getLeaderUI eps <- getsClient seps@@ -286,48 +386,70 @@ spos = bpos sb Level{lxsize, lysize} <- getLevel lid case bla lxsize lysize eps spos tpos of- Nothing -> failSer ProjectAimOnself+ Nothing -> return $ Just ProjectAimOnself Just [] -> assert `failure` "project from the edge of level"- `twith` (spos, tpos, sb, ts)+ `twith` (spos, tpos, sb) Just (pos : _) -> do lvl <- getLevel lid let t = lvl `at` pos if not $ Tile.isWalkable cotile t- then failSer ProjectBlockTerrain+ then return $ Just ProjectBlockTerrain else do mab <- getsState $ posToActor pos lid if maybe True (bproj . snd . fst) mab- then projectEps ts tpos eps- else failSer ProjectBlockActor+ then return Nothing+ else return $ Just ProjectBlockActor -projectEps :: MonadClientUI m- => [Trigger] -> Point -> Int- -> m (SlideOrCmd (RequestTimed AbProject))-projectEps ts tpos eps = do+projectItem :: forall m. MonadClientUI m+ => [Trigger] -> m (Either Msg Point)+ -> m (SlideOrCmd (ItemId, CStore))+projectItem ts posFromCursor = do leader <- getLeaderUI b <- getsState $ getActorBody leader+ activeItems <- activeItemsClient leader actorSk <- actorSkillsClient leader let skill = EM.findWithDefault 0 AbProject actorSk- activeItems <- activeItemsClient leader- let cLegal = [CGround, CInv, CEqp]+ cLegal = [CGround, CInv, CEqp, CSha] (verb1, object1) = case ts of [] -> ("aim", "item") tr : _ -> (verb tr, object tr) triggerSyms = triggerSymbols ts- p itemFull@ItemFull{itemBase} =- let legal = permittedProject triggerSyms False skill itemFull b activeItems- in case legal of- Left{} -> legal- Right False -> legal- Right True -> Right $ totalRange itemBase >= chessDist (bpos b) tpos+ psuitReq :: m (Either Msg (ItemFull -> Either ReqFailure Bool))+ psuitReq = do+ mpos <- posFromCursor+ case mpos of+ Left err -> return $ Left err+ Right pos -> return $ Right $ \itemFull@ItemFull{itemBase} -> do+ let legal = permittedProject triggerSyms False skill+ itemFull b activeItems+ case legal of+ Left{} -> legal+ Right False -> legal+ Right True ->+ Right $ totalRange itemBase >= chessDist (bpos b) pos+ psuit :: m Suitability+ psuit = do+ mpsuitReq <- psuitReq+ case mpsuitReq of+ -- If target invalid, no item is considered a (suitable) missile.+ Left err -> return $ SuitsNothing err+ Right psuitReqFun -> return $ SuitsSomething $ \itemFull ->+ case psuitReqFun itemFull of+ Left _ -> False+ Right suit -> suit prompt = makePhrase ["What", object1, "to", verb1]- promptGeneric = "What item to fling"- ggi <- getGroupItem (either (const False) id . p) prompt promptGeneric cLegal cLegal+ promptGeneric = "What to fling"+ ggi <- getGroupItem psuit prompt promptGeneric True+ cLegal cLegal case ggi of- Right ((iid, itemFull), CActor _ fromCStore) ->- case p itemFull of- Left reqFail -> failSer reqFail- Right _ -> return $ Right $ ReqProject tpos eps iid fromCStore+ Right ((iid, itemFull), MStore fromCStore) -> do+ mpsuitReq <- psuitReq+ case mpsuitReq of+ Left err -> failWith err+ Right psuitReqFun ->+ case psuitReqFun itemFull of+ Left reqFail -> failSer reqFail+ Right _ -> return $ Right (iid, fromCStore) Left slides -> return $ Left slides _ -> assert `failure` ggi @@ -339,24 +461,27 @@ -- * Apply applyHuman :: MonadClientUI m- => [Trigger] -> m (SlideOrCmd (RequestTimed AbApply))+ => [Trigger] -> m (SlideOrCmd (RequestTimed 'AbApply)) applyHuman ts = do leader <- getLeaderUI b <- getsState $ getActorBody leader actorSk <- actorSkillsClient leader let skill = EM.findWithDefault 0 AbProject actorSk activeItems <- activeItemsClient leader- let cLegal = [CGround, CInv, CEqp]+ localTime <- getsState $ getLocalTime (blid b)+ let cLegal = [CGround, CInv, CEqp, CSha] (verb1, object1) = case ts of- [] -> ("activate", "item")+ [] -> ("apply", "item") tr : _ -> (verb tr, object tr) triggerSyms = triggerSymbols ts- p itemFull = permittedApply triggerSyms skill itemFull b activeItems+ p itemFull =+ permittedApply triggerSyms localTime skill itemFull b activeItems prompt = makePhrase ["What", object1, "to", verb1]- promptGeneric = "What item to activate"- ggi <- getGroupItem (either (const False) id . p) prompt promptGeneric cLegal cLegal+ promptGeneric = "What to apply"+ ggi <- getGroupItem (return $ SuitsSomething $ either (const False) id . p)+ prompt promptGeneric False cLegal cLegal case ggi of- Right ((iid, itemFull), CActor _ fromCStore) ->+ Right ((iid, itemFull), MStore fromCStore) -> case p itemFull of Left reqFail -> failSer reqFail Right _ -> return $ Right $ ReqApply iid fromCStore@@ -365,37 +490,43 @@ -- * AlterDir +-- TODO: accept mouse, too -- | Ask for a direction and alter a tile, if possible. alterDirHuman :: MonadClientUI m- => [Trigger] -> m (SlideOrCmd (RequestTimed AbAlter))+ => [Trigger] -> m (SlideOrCmd (RequestTimed 'AbAlter)) alterDirHuman ts = do Config{configVi, configLaptop} <- askConfig let verb1 = case ts of [] -> "alter" tr : _ -> verb tr- keys = zipWith K.KM (repeat K.NoModifier)- (K.dirAllKey configVi configLaptop)+ keys = map (K.toKM K.NoModifier) (K.dirAllKey configVi configLaptop) prompt = makePhrase ["What to", verb1 <> "? [movement key"] me <- displayChoiceUI prompt emptyOverlay keys case me of Left slides -> failSlides slides- Right e -> K.handleDir configVi configLaptop e (flip alterTile ts)+ Right e -> K.handleDir configVi configLaptop e (`alterTile` ts) (failWith "never mind") -- | Player tries to alter a tile using a feature. alterTile :: MonadClientUI m- => Vector -> [Trigger] -> m (SlideOrCmd (RequestTimed AbAlter))+ => Vector -> [Trigger] -> m (SlideOrCmd (RequestTimed 'AbAlter)) alterTile dir ts = do cops@Kind.COps{cotile} <- getsState scops leader <- getLeaderUI b <- getsState $ getActorBody leader lvl <- getLevel $ blid b+ as <- getsState $ actorList (const True) (blid b) let tpos = bpos b `shift` dir t = lvl `at` tpos alterFeats = alterFeatures ts case filter (\feat -> Tile.hasFeature cotile feat t) alterFeats of [] -> failWith $ guessAlter cops alterFeats t- feat : _ -> return $ Right $ ReqAlter tpos $ Just feat+ feat : _ ->+ if EM.notMember tpos $ lfloor lvl then+ if unoccupied as tpos then+ return $ Right $ ReqAlter tpos $ Just feat+ else failSer AlterBlockActor+ else failSer AlterBlockItem alterFeatures :: [Trigger] -> [TK.Feature] alterFeatures [] = []@@ -416,7 +547,7 @@ -- | Leader tries to trigger the tile he's standing on. triggerTileHuman :: MonadClientUI m- => [Trigger] -> m (SlideOrCmd (RequestTimed AbTrigger))+ => [Trigger] -> m (SlideOrCmd (RequestTimed 'AbTrigger)) triggerTileHuman ts = do tgtMode <- getsClient stgtMode if isJust tgtMode then do@@ -427,12 +558,12 @@ mk = getK ts case mk of Nothing -> failWith "never mind"- Just k -> fmap Left $ tgtAscendHuman k+ Just k -> Left <$> tgtAscendHuman k else triggerTile ts -- | Player tries to trigger a tile using a feature. triggerTile :: MonadClientUI m- => [Trigger] -> m (SlideOrCmd (RequestTimed AbTrigger))+ => [Trigger] -> m (SlideOrCmd (RequestTimed 'AbTrigger)) triggerTile ts = do cops@Kind.COps{cotile} <- getsState scops leader <- getLeaderUI@@ -465,18 +596,18 @@ "This is the way out, but where would you go in this alien world?" else do go <- displayYesNo ColorFull "This is the way out. Really leave now?"- if not go then failWith "Game resumed."+ if not go then failWith "game resumed" else do (_, total) <- getsState $ calculateTotal b if total == 0 then do -- The player can back off at each of these steps. go1 <- displayMore ColorBW "Afraid of the challenge? Leaving so soon and empty-handed?"- if not go1 then failWith "Brave soul!"+ if not go1 then failWith "brave soul!" else do go2 <- displayMore ColorBW "Next time try to grab some loot before escape!"- if not go2 then failWith "Here's your chance!"+ if not go2 then failWith "here's your chance!" else return $ Right () else return $ Right () _ -> return $ Right ()@@ -494,33 +625,139 @@ else assert `failure` fs guessTrigger _ _ _ = "never mind" --- * StepToTarget+-- * RunOnceAhead -stepToTargetHuman :: MonadClientUI m => m (SlideOrCmd RequestAnyAbility)-stepToTargetHuman = do+runOnceAheadHuman :: MonadClientUI m => m (SlideOrCmd RequestAnyAbility)+runOnceAheadHuman = do+ side <- getsClient sside+ fact <- getsState $ (EM.! side) . sfactionD+ leader <- getLeaderUI+ srunning <- getsClient srunning+ -- When running, stop if disturbed. If not running, stop at once.+ case srunning of+ Nothing -> do+ stopPlayBack+ return $ Left mempty+ Just RunParams{runMembers}+ | noRunWithMulti fact && runMembers /= [leader] -> do+ stopPlayBack+ Config{configRunStopMsgs} <- askConfig+ if configRunStopMsgs+ then failWith "run stop: automatic leader change"+ else return $ Left mempty+ Just runParams -> do+ arena <- getArenaUI+ runOutcome <- continueRun arena runParams+ case runOutcome of+ Left stopMsg -> do+ stopPlayBack+ Config{configRunStopMsgs} <- askConfig+ if configRunStopMsgs+ then failWith $ "run stop:" <+> stopMsg+ else return $ Left mempty+ Right runCmd ->+ return $ Right runCmd++-- * MoveOnceToCursor++moveOnceToCursorHuman :: MonadClientUI m => m (SlideOrCmd RequestAnyAbility)+moveOnceToCursorHuman = goToCursor True False++goToCursor :: MonadClientUI m+ => Bool -> Bool -> m (SlideOrCmd RequestAnyAbility)+goToCursor initialStep run = do tgtMode <- getsClient stgtMode -- Movement is legal only outside targeting mode.- -- TODO: use this command for something in targeting mode.- if isJust tgtMode then failWith "cannot move in targeting mode"+ if isJust tgtMode then failWith "cannot move in aiming mode" else do leader <- getLeaderUI b <- getsState $ getActorBody leader- tgtPos <- leaderTgtToPos- case tgtPos of- Nothing -> failWith "target not set"- Just c | c == bpos b -> failWith "target reached"+ cursorPos <- cursorToPos+ case cursorPos of+ Nothing -> failWith "no leader"+ Just c | c == bpos b ->+ if initialStep+ then return $ Right $ RequestAnyAbility ReqWait+ else do+ report <- getsClient sreport+ if nullReport report+ then return $ Left mempty+ -- Mark that the messages are accumulated, not just from last move.+ else failWith "crosshair now reached" Just c -> do- (_, mpath) <- getCacheBfsAndPath leader c+ running <- getsClient srunning+ case running of+ -- Don't use running params from previous run or goto-cursor.+ Just paramOld | not initialStep -> do+ arena <- getArenaUI+ runOutcome <- multiActorGoTo arena c paramOld+ case runOutcome of+ Left stopMsg -> failWith stopMsg+ Right (finalGoal, dir) ->+ moveRunHuman initialStep finalGoal run False dir+ _ -> do+ let !_A = assert (initialStep || not run) ()+ (_, mpath) <- getCacheBfsAndPath leader c+ case mpath of+ Nothing -> failWith "no route to crosshair"+ Just [] -> assert `failure` (leader, b, c)+ Just (p1 : _) -> do+ let finalGoal = p1 == c+ dir = towards (bpos b) p1+ moveRunHuman initialStep finalGoal run False dir++multiActorGoTo :: MonadClient m+ => LevelId -> Point -> RunParams+ -> m (Either Msg (Bool, Vector))+multiActorGoTo arena c paramOld =+ case paramOld of+ RunParams{runMembers = []} ->+ return $ Left "selected actors no longer there"+ RunParams{runMembers = r : rs, runWaiting} -> do+ onLevel <- getsState $ memActor r arena+ if not onLevel then do+ let paramNew = paramOld {runMembers = rs}+ multiActorGoTo arena c paramNew+ else do+ s <- getState+ modifyClient $ updateLeader r s+ let runMembersNew = rs ++ [r]+ paramNew = paramOld { runMembers = runMembersNew+ , runWaiting = 0}+ b <- getsState $ getActorBody r+ (_, mpath) <- getCacheBfsAndPath r c case mpath of- Nothing -> failWith "no route to target"- Just [] -> assert `failure` (leader, b, bpos b, c)+ Nothing -> return $ Left "no route to crosshair"+ Just [] ->+ -- This actor already at goal; will be caught in goToCursor.+ return $ Left "" Just (p1 : _) -> do- as <- getsState $ posToActors p1 (blid b)- if not $ null as then- failWith "actor in the path to target"- else- moveRunHuman False $ towards (bpos b) p1+ let finalGoal = p1 == c+ dir = towards (bpos b) p1+ tpos = bpos b `shift` dir+ tgts <- getsState $ posToActors tpos arena+ case tgts of+ [] -> do+ modifyClient $ \cli -> cli {srunning = Just paramNew}+ return $ Right (finalGoal, dir)+ [((target, _), _)]+ | target `elem` rs || runWaiting <= length rs ->+ -- Let r wait until all others move. Mark it in runWaiting+ -- to avoid cycles. When all wait for each other, fail.+ multiActorGoTo arena c paramNew{runWaiting=runWaiting + 1}+ _ ->+ return $ Left "actor in the way" +-- * RunOnceToCursor++runOnceToCursorHuman :: MonadClientUI m => m (SlideOrCmd RequestAnyAbility)+runOnceToCursorHuman = goToCursor True True++-- * ContinueToCursor++continueToCursorHuman :: MonadClientUI m => m (SlideOrCmd RequestAnyAbility)+continueToCursorHuman = goToCursor False False{-irrelevant-}+ -- * GameRestart; does not take time gameRestartHuman :: MonadClientUI m => GroupName ModeKind -> m (SlideOrCmd RequestUI)@@ -541,8 +778,8 @@ b2 <- displayYesNo ColorBW "Current progress will be lost! Really restart the game?" msg2 <- rndToAction $ oneOf- [ "Yea, would be a pity to leave them all to die."- , "Yea, a shame to get your own team stranded." ]+ [ "yea, would be a pity to leave them all to die"+ , "yea, a shame to get your own team stranded" ] if not b2 then failWith msg2 else restart @@ -555,7 +792,7 @@ leader <- getLeaderUI DebugModeCli{sdifficultyCli} <- getsClient sdebugCli return $ Right $ ReqUIGameExit leader sdifficultyCli- else failWith "Save and exit canceled."+ else failWith "save and exit canceled" -- * GameSave; does not take time @@ -582,10 +819,10 @@ let toT = if fromT == maxBound then minBound else succ fromT go <- displayMore ColorFull $ "Switching tactic to"- <+> T.pack (show toT) -- tshow eats up parens+ <+> tshow toT <> ". (This clears targets.)" if not go- then failWith "Tactic change canceled."+ then failWith "tactic change canceled" else return $ Right $ ReqUITactic toT -- * Automate; does not take time@@ -599,5 +836,5 @@ else do go <- displayMore ColorBW "Ceding control to AI (ESC to regain)." if not go- then failWith "Automation canceled."+ then failWith "automation canceled" else return $ Right ReqUIAutomate
Game/LambdaHack/Client/UI/HandleHumanLocalClient.hs view
@@ -5,14 +5,17 @@ ( -- * Assorted commands gameDifficultyCycle , pickLeaderHuman, memberCycleHuman, memberBackHuman- , describeItemHuman, allOwnedHuman- , selectActorHuman, selectNoneHuman, clearHuman, repeatHuman, recordHuman+ , selectActorHuman, selectNoneHuman, clearHuman+ , stopIfTgtModeHuman, selectWithPointer, repeatHuman, recordHuman , historyHuman, markVisionHuman, markSmellHuman, markSuspectHuman , helpHuman, mainMenuHuman, macroHuman -- * Commands specific to targeting , moveCursorHuman, tgtFloorHuman, tgtEnemyHuman- , tgtUnknownHuman, tgtItemHuman, tgtStairHuman, tgtAscendHuman- , epsIncrHuman, tgtClearHuman, cancelHuman, acceptHuman+ , tgtAscendHuman, epsIncrHuman, tgtClearHuman+ , cursorUnknownHuman, cursorItemHuman, cursorStairHuman+ , cancelHuman, acceptHuman+ , cursorPointerFloorHuman, cursorPointerEnemyHuman+ , tgtPointerFloorHuman, tgtPointerEnemyHuman ) where -- Cabal@@ -47,20 +50,16 @@ import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction-import Game.LambdaHack.Common.Item-import Game.LambdaHack.Common.ItemDescription+import Game.LambdaHack.Common.Frequency import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level-import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Msg-import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.Request import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time-import Game.LambdaHack.Common.Vector import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.RuleKind import qualified Game.LambdaHack.Content.TileKind as TK@@ -81,18 +80,26 @@ side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD arena <- getArenaUI- let (autoDun, autoLvl) = autoDungeonLevel fact- s <- getState- case tryFindHeroK s side k of- Nothing -> failMsg "No such member of the party."- Just (aid, b) ->- if blid b == arena && autoLvl- then failMsg $ showReqFailure NoChangeLvlLeader- else if autoDun- then failMsg $ showReqFailure NoChangeDunLeader- else do- void $ pickLeader True aid- return mempty+ mhero <- getsState $ tryFindHeroK side k+ allA <- getsState $ EM.assocs . sactorD+ let mactor = let factionA = filter (\(_, body) ->+ not (bproj body) && bfid body == side) allA+ hs = sortBy (comparing keySelected) factionA+ in case drop k hs of+ [] -> Nothing+ aidb : _ -> Just aidb+ mchoice = mhero `mplus` mactor+ (autoDun, autoLvl) = autoDungeonLevel fact+ case mchoice of+ Nothing -> failMsg "no such member of the party"+ Just (aid, b)+ | blid b == arena && autoLvl ->+ failMsg $ showReqFailure NoChangeLvlLeader+ | autoDun ->+ failMsg $ showReqFailure NoChangeDunLeader+ | otherwise -> do+ void $ pickLeader True aid+ return mempty -- * MemberCycle @@ -106,55 +113,17 @@ memberBackHuman :: MonadClientUI m => m Slideshow memberBackHuman = memberBack True --- * DescribeItem---- | Display items from a given container store and describe the chosen one.-describeItemHuman :: MonadClientUI m => CStore -> m Slideshow-describeItemHuman cstore = do- leader <- getLeaderUI- describeItemC (CActor leader cstore) False--describeItemC :: MonadClientUI m => Container -> Bool -> m Slideshow-describeItemC c noEnter = do- let subject body = partActor body- verbSha body activeItems = if calmEnough body activeItems- then "notice"- else "paw distractedly"- prompt body activeItems c2 = case c2 of- CActor _ CSha ->- makePhrase- [MU.Capitalize- $ MU.SubjectVerbSg (subject body) (verbSha body activeItems)]- CTrunk{} ->- makePhrase- [MU.Capitalize $ MU.SubjectVerbSg (subject body) "recall"]- _ ->- makePhrase- [MU.Capitalize $ MU.SubjectVerbSg (subject body) "see"]- ggi <- getStoreItem prompt c noEnter- case ggi of- Right ((_, itemFull), c2) -> do- lid2 <- getsState $ lidFromC c2- localTime <- getsState $ getLocalTime lid2- overlayToSlideshow "" $ itemDesc c2 lid2 localTime itemFull- Left slides -> return slides---- * AllOwned---- | Display the sum of equipments and inventory of the whole party.-allOwnedHuman :: MonadClientUI m => m Slideshow-allOwnedHuman = do- leader <- getLeaderUI- b <- getsState $ getActorBody leader- describeItemC (CTrunk (bfid b) (blid b) (bpos b)) False- -- * SelectActor -- TODO: make the message (and for selectNoneHuman, pickLeader, etc.) -- optional, since they have a clear representation in the UI elsewhere.-selectActorHuman :: MonadClientUI m => m Slideshow+selectActorHuman :: MonadClientUI m => m () selectActorHuman = do leader <- getLeaderUI+ selectAidHuman leader++selectAidHuman :: MonadClientUI m => ActorId -> m ()+selectAidHuman leader = do body <- getsState $ getActorBody leader wasMemeber <- getsClient $ ES.member leader . sselected let upd = if wasMemeber@@ -165,7 +134,6 @@ msgAdd $ makeSentence [subject, if wasMemeber then "deselected" else "selected"]- return mempty -- * SelectNone @@ -192,6 +160,33 @@ clearHuman :: Monad m => m () clearHuman = return () +-- * StopIfTgtMode++stopIfTgtModeHuman :: MonadClientUI m => m ()+stopIfTgtModeHuman = do+ tgtMode <- getsClient stgtMode+ when (isJust tgtMode) stopPlayBack++-- * SelectWithPointer++selectWithPointer:: MonadClientUI m => m ()+selectWithPointer = do+ km <- getsClient slastKM+ let Point{..} = K.pointer km+ lidV <- viewedLevel+ Level{lysize} <- getLevel lidV+ side <- getsClient sside+ ours <- getsState $ filter (not . bproj . snd)+ . actorAssocs (== side) lidV+ -- Select even if no space in status line for the actor's symbol.+ let viewed = sortBy (comparing keySelected) ours+ when (py == lysize + 1 && px <= length viewed && px >= 0) $ do+ if px == 0 then+ selectNoneHuman+ else+ selectAidHuman $ fst $ viewed !! (px - 1)+ stopPlayBack+ -- * Repeat -- Note that walk followed by repeat should not be equivalent to run,@@ -241,7 +236,7 @@ , "(this level:" , MU.Text (tshow turnsLocal) <> ")" ] <+> "Past messages:"- overlayToBlankSlideshow msg $ renderHistory history+ overlayToBlankSlideshow False msg $ renderHistory history -- * MarkVision, MarkSmell, MarkSuspect @@ -259,6 +254,8 @@ markSuspectHuman :: MonadClientUI m => m () markSuspectHuman = do+ -- @condBFS@ depends on the setting we change here.+ modifyClient $ \cli -> cli {sbfsD = EM.empty} modifyClient toggleMarkSuspect cur <- getsClient smarkSuspect msgAdd $ "Suspect terrain display toggled" <+> if cur then "on." else "off."@@ -331,7 +328,7 @@ overwrite $ pasteVersion $ map T.unpack $ stripFrame mainMenuArt case menuOverlay of [] -> assert `failure` "empty Main Menu overlay" `twith` mainMenuArt- hd : tl -> overlayToBlankSlideshow hd (toOverlay tl)+ hd : tl -> overlayToBlankSlideshow True hd (toOverlay tl) -- TODO: keys don't work if tl/=[] -- * Macro@@ -342,199 +339,15 @@ -- * MoveCursor --- | Move the cursor. Assumes targeting mode.-moveCursorHuman :: MonadClientUI m => Vector -> Int -> m Slideshow-moveCursorHuman dir n = do- leader <- getLeaderUI- stgtMode <- getsClient stgtMode- let lidV = maybe (assert `failure` leader) tgtLevelId stgtMode- Level{lxsize, lysize} <- getLevel lidV- lpos <- getsState $ bpos . getActorBody leader- scursor <- getsClient scursor- cursorPos <- cursorToPos- let cpos = fromMaybe lpos cursorPos- shiftB pos = shiftBounded lxsize lysize pos dir- newPos = iterate shiftB cpos !! n- if newPos == cpos then failMsg "never mind"- else do- let tgt = case scursor of- TVector{} -> TVector $ newPos `vectorToFrom` lpos- _ -> TPoint lidV newPos- modifyClient $ \cli -> cli {scursor = tgt}- doLook---- | Perform look around in the current position of the cursor.--- Normally expects targeting mode and so that a leader is picked.-doLook :: MonadClientUI m => m Slideshow-doLook = do- Kind.COps{cotile=Kind.Ops{ouniqGroup}} <- getsState scops- let unknownId = ouniqGroup "unknown space"- stgtMode <- getsClient stgtMode- case stgtMode of- Nothing -> return mempty- Just tgtMode -> do- leader <- getLeaderUI- let lidV = tgtLevelId tgtMode- lvl <- getLevel lidV- cursorPos <- cursorToPos- per <- getPerFid lidV- b <- getsState $ getActorBody leader- let p = fromMaybe (bpos b) cursorPos- canSee = ES.member p (totalVisible per)- inhabitants <- if canSee- then getsState $ posToActors p lidV- else return []- seps <- getsClient seps- mnewEps <- makeLine b p seps- itemToF <- itemToFullClient- let aims = isJust mnewEps- enemyMsg = case inhabitants of- [] -> ""- ((_, body), _) : rest ->- -- Even if it's the leader, give his proper name, not 'you'.- let subjects = map (partActor . snd . fst) inhabitants- subject = MU.WWandW subjects- verb = "be here"- desc = if not (null rest) -- many actors- then ""- else case itemDisco $ itemToF (btrunk body) (1, []) of- Nothing -> ""- Just ItemDisco{itemKind} -> IK.idesc itemKind- pdesc = if desc == "" then "" else "(" <> desc <> ")"- in makeSentence [MU.SubjectVerbSg subject verb] <+> pdesc- vis | lvl `at` p == unknownId = "that is"- | not canSee = "you remember"- | not aims = "you are aware of"- | otherwise = "you see"- -- Show general info about current position.- lookMsg <- lookAt True vis canSee p leader enemyMsg- -- Check if there's something lying around at current position.- is <- getsState $ getCBag $ CFloor lidV p- if EM.size is <= 2 then- promptToSlideshow lookMsg- else do- msgAdd lookMsg -- TODO: do not add to history- floorItemOverlay lidV p---- | Create a list of item names.-floorItemOverlay :: MonadClientUI m => LevelId -> Point -> m Slideshow-floorItemOverlay lid p = describeItemC (CFloor lid p) True+-- in InventoryClient -- * TgtFloor --- | Cycle targeting mode. Do not change position of the cursor,--- switch among things at that position.-tgtFloorHuman :: MonadClientUI m => m Slideshow-tgtFloorHuman = do- lidV <- viewedLevel- leader <- getLeaderUI- lpos <- getsState $ bpos . getActorBody leader- cursorPos <- cursorToPos- scursor <- getsClient scursor- stgtMode <- getsClient stgtMode- bsAll <- getsState $ actorAssocs (const True) lidV- let cursor = fromMaybe lpos cursorPos- tgt = case scursor of- _ | isNothing stgtMode -> -- first key press: keep target- scursor- TEnemy a True -> TEnemy a False- TEnemy{} -> TPoint lidV cursor- TEnemyPos{} -> TPoint lidV cursor- TPoint{} -> TVector $ cursor `vectorToFrom` lpos- TVector{} ->- -- For projectiles, we pick here the first that would be picked- -- by '*', so that all other projectiles on the tile come next,- -- without any intervening actors from other tiles.- case find (\(_, m) -> Just (bpos m) == cursorPos) bsAll of- Just (im, _) -> TEnemy im True- Nothing -> TPoint lidV cursor- modifyClient $ \cli -> cli {scursor = tgt, stgtMode = Just $ TgtMode lidV}- doLook+-- in InventoryClient -- * TgtEnemy -tgtEnemyHuman :: MonadClientUI m => m Slideshow-tgtEnemyHuman = do- lidV <- viewedLevel- leader <- getLeaderUI- lpos <- getsState $ bpos . getActorBody leader- cursorPos <- cursorToPos- scursor <- getsClient scursor- stgtMode <- getsClient stgtMode- side <- getsClient sside- fact <- getsState $ (EM.! side) . sfactionD- bsAll <- getsState $ actorAssocs (const True) lidV- let ordPos (_, b) = (chessDist lpos $ bpos b, bpos b)- dbs = sortBy (comparing ordPos) bsAll- pickUnderCursor = -- switch to the enemy under cursor, if any- let i = fromMaybe (-1)- $ findIndex ((== cursorPos) . Just . bpos . snd) dbs- in splitAt i dbs- (permitAnyActor, (lt, gt)) = case scursor of- TEnemy a permit | isJust stgtMode -> -- pick next enemy- let i = fromMaybe (-1) $ findIndex ((== a) . fst) dbs- in (permit, splitAt (i + 1) dbs)- TEnemy a permit -> -- first key press, retarget old enemy- let i = fromMaybe (-1) $ findIndex ((== a) . fst) dbs- in (permit, splitAt i dbs)- TEnemyPos _ _ _ permit -> (permit, pickUnderCursor)- _ -> (False, pickUnderCursor) -- the sensible default is only-foes- gtlt = gt ++ lt- isEnemy b = isAtWar fact (bfid b)- && not (bproj b)- lf = filter (isEnemy . snd) gtlt- tgt | permitAnyActor = case gtlt of- (a, _) : _ -> TEnemy a True- [] -> scursor -- no actors in sight, stick to last target- | otherwise = case lf of- (a, _) : _ -> TEnemy a False- [] -> scursor -- no seen foes in sight, stick to last target- -- Register the chosen enemy, to pick another on next invocation.- modifyClient $ \cli -> cli {scursor = tgt, stgtMode = Just $ TgtMode lidV}- doLook---- * TgtUnknown--tgtUnknownHuman :: MonadClientUI m => m Slideshow-tgtUnknownHuman = do- leader <- getLeaderUI- b <- getsState $ getActorBody leader- mpos <- closestUnknown leader- case mpos of- Nothing -> failMsg "no more unknown spots left"- Just p -> do- let tgt = TPoint (blid b) p- modifyClient $ \cli -> cli {scursor = tgt}- doLook---- * TgtItem--tgtItemHuman :: MonadClientUI m => m Slideshow-tgtItemHuman = do- leader <- getLeaderUI- b <- getsState $ getActorBody leader- items <- closestItems leader- case items of- [] -> failMsg "no more items remembered or visible"- (_, (p, _)) : _ -> do- let tgt = TPoint (blid b) p- modifyClient $ \cli -> cli {scursor = tgt}- doLook---- * TgtStair--tgtStairHuman :: MonadClientUI m => Bool -> m Slideshow-tgtStairHuman up = do- leader <- getLeaderUI- b <- getsState $ getActorBody leader- stairs <- closestTriggers (Just up) False leader- case stairs of- [] -> failMsg $ "no stairs"- <+> if up then "up" else "down"- p : _ -> do- let tgt = TPoint (blid b) p- modifyClient $ \cli -> cli {scursor = tgt}- doLook+-- in InventoryClient -- * TgtAscend @@ -558,7 +371,7 @@ case rightStairs of Just cpos -> do -- stairs, in the right direction (nln, npos) <- getsState $ whereTo lidV cpos k . sdungeon- assert (nln /= lidV `blame` "stairs looped" `twith` nln) skip+ let !_A = assert (nln /= lidV `blame` "stairs looped" `twith` nln) () nlvl <- getLevel nln -- Do not freely reveal the other end of the stairs. let ascDesc (TK.Cause (IK.Ascend _)) = True@@ -568,47 +381,64 @@ then TPoint nln npos -- already known as an exit, focus on it else scursorOld -- unknown, do not reveal modifyClient $ \cli -> cli {scursor, stgtMode = Just (TgtMode nln)}- doLook+ doLook False Nothing -> -- no stairs in the right direction case ascendInBranch dungeon k lidV of [] -> failMsg "no more levels in this direction" nln : _ -> do modifyClient $ \cli -> cli {stgtMode = Just (TgtMode nln)}- doLook+ doLook False -- * EpsIncr --- | Tweak the @eps@ parameter of the targeting digital line.-epsIncrHuman :: MonadClientUI m => Bool -> m Slideshow-epsIncrHuman b = do- stgtMode <- getsClient stgtMode- if isJust stgtMode- then do- modifyClient $ \cli -> cli {seps = seps cli + if b then 1 else -1}- return mempty- else failMsg "never mind" -- no visual feedback, so no sense+-- in InventoryClient -- * TgtClear -tgtClearHuman :: MonadClientUI m => m Slideshow-tgtClearHuman = do+-- in InventoryClient++-- * CursorUnknown++cursorUnknownHuman :: MonadClientUI m => m Slideshow+cursorUnknownHuman = do leader <- getLeaderUI- tgt <- getsClient $ getTarget leader- case tgt of- Just _ -> do- modifyClient $ updateTarget leader (const Nothing)- return mempty- Nothing -> do- scursorOld <- getsClient scursor- b <- getsState $ getActorBody leader- let scursor = case scursorOld of- TEnemy _ permit -> TEnemy leader permit- TEnemyPos _ _ _ permit -> TEnemy leader permit- TPoint{} -> TPoint (blid b) (bpos b)- TVector{} -> TVector (Vector 0 0)- modifyClient $ \cli -> cli {scursor}- doLook+ b <- getsState $ getActorBody leader+ mpos <- closestUnknown leader+ case mpos of+ Nothing -> failMsg "no more unknown spots left"+ Just p -> do+ let tgt = TPoint (blid b) p+ modifyClient $ \cli -> cli {scursor = tgt}+ doLook False +-- * CursorItem++cursorItemHuman :: MonadClientUI m => m Slideshow+cursorItemHuman = do+ leader <- getLeaderUI+ b <- getsState $ getActorBody leader+ items <- closestItems leader+ case items of+ [] -> failMsg "no more items remembered or visible"+ (_, (p, _)) : _ -> do+ let tgt = TPoint (blid b) p+ modifyClient $ \cli -> cli {scursor = tgt}+ doLook False++-- * CursorStair++cursorStairHuman :: MonadClientUI m => Bool -> m Slideshow+cursorStairHuman up = do+ leader <- getLeaderUI+ b <- getsState $ getActorBody leader+ stairs <- closestTriggers (Just up) leader+ case reverse $ sort $ runFrequency stairs of+ [] -> failMsg $ "no stairs" <+> if up then "up" else "down"+ (_, p) : _ -> do+ let tgt = TPoint (blid b) p+ modifyClient $ \cli -> cli {scursor = tgt}+ doLook False+ -- * Cancel -- | Cancel something, e.g., targeting mode, resetting the cursor@@ -624,7 +454,7 @@ targetReject :: MonadClientUI m => m Slideshow targetReject = do modifyClient $ \cli -> cli {stgtMode = Nothing}- failMsg "targeting canceled"+ failMsg "target not set" -- * Accept @@ -659,3 +489,29 @@ (targetMsg, _) <- targetDescLeader leader subject <- partAidLeader leader msgAdd $ makeSentence [MU.SubjectVerbSg subject "target", MU.Text targetMsg]++-- * CursorPointerFloor++cursorPointerFloorHuman :: MonadClientUI m => m ()+cursorPointerFloorHuman = do+ look <- cursorPointerFloor False False+ let !_A = assert (look == mempty `blame` look) ()+ modifyClient $ \cli -> cli {stgtMode = Nothing}++-- * CursorPointerEnemy++cursorPointerEnemyHuman :: MonadClientUI m => m ()+cursorPointerEnemyHuman = do+ look <- cursorPointerEnemy False False+ let !_A = assert (look == mempty `blame` look) ()+ modifyClient $ \cli -> cli {stgtMode = Nothing}++-- * TgtPointerFloor++tgtPointerFloorHuman :: MonadClientUI m => m Slideshow+tgtPointerFloorHuman = cursorPointerFloor True False++-- * TgtPointerEnemy++tgtPointerEnemyHuman :: MonadClientUI m => m Slideshow+tgtPointerEnemyHuman = cursorPointerEnemy True False
Game/LambdaHack/Client/UI/HumanCmd.hs view
@@ -1,12 +1,15 @@+{-# LANGUAGE DeriveGeneric #-} -- | Abstract syntax human player commands. module Game.LambdaHack.Client.UI.HumanCmd ( CmdCategory(..), HumanCmd(..), Trigger(..) , noRemoteHumanCmd, categoryDescription, cmdDescription ) where +import Control.DeepSeq import Control.Exception.Assert.Sugar import Data.Maybe import Data.Text (Text)+import GHC.Generics (Generic) import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Common.Actor (verbCStore)@@ -17,19 +20,23 @@ import qualified Game.LambdaHack.Content.TileKind as TK data CmdCategory =- CmdMenu | CmdMove | CmdItem | CmdTgt | CmdAuto | CmdMeta- | CmdDebug | CmdMinimal- deriving (Show, Read, Eq)+ CmdMenu | CmdMove | CmdItem | CmdTgt | CmdAuto | CmdMeta | CmdMouse+ | CmdInternal | CmdDebug | CmdMinimal+ deriving (Show, Read, Eq, Generic) +instance NFData CmdCategory+ categoryDescription :: CmdCategory -> Text categoryDescription CmdMenu = "Main Menu" categoryDescription CmdMove = "Terrain exploration and alteration" categoryDescription CmdItem = "Item use"-categoryDescription CmdTgt = "Targeting"+categoryDescription CmdTgt = "Aiming and targeting" categoryDescription CmdAuto = "Automation" categoryDescription CmdMeta = "Assorted"+categoryDescription CmdMouse = "Mouse"+categoryDescription CmdInternal = "Internal" categoryDescription CmdDebug = "Debug"-categoryDescription CmdMinimal = "Minimal cheat sheet for casual play"+categoryDescription CmdMinimal = "The minimal command set" -- | Abstract syntax of player commands. data HumanCmd =@@ -39,11 +46,15 @@ | Run !Vector | Wait | MoveItem ![CStore] !CStore !(Maybe MU.Part) !MU.Part !Bool+ | DescribeItem !ItemDialogMode | Project ![Trigger] | Apply ![Trigger] | AlterDir ![Trigger] | TriggerTile ![Trigger]- | StepToTarget+ | RunOnceAhead+ | MoveOnceToCursor+ | RunOnceToCursor+ | ContinueToCursor -- Below this line, commands do not take time. | GameRestart !(GroupName ModeKind) | GameExit@@ -56,11 +67,11 @@ | PickLeader !Int | MemberCycle | MemberBack- | DescribeItem !CStore- | AllOwned | SelectActor | SelectNone | Clear+ | StopIfTgtMode+ | SelectWithPointer | Repeat !Int | Record | History@@ -74,23 +85,31 @@ | MoveCursor !Vector !Int | TgtFloor | TgtEnemy- | TgtUnknown- | TgtItem- | TgtStair !Bool | TgtAscend !Int | EpsIncr !Bool | TgtClear+ | CursorUnknown+ | CursorItem+ | CursorStair !Bool | Cancel | Accept- deriving (Show, Read, Eq, Ord)+ | CursorPointerFloor+ | CursorPointerEnemy+ | TgtPointerFloor+ | TgtPointerEnemy+ deriving (Show, Read, Eq, Ord, Generic) +instance NFData HumanCmd+ data Trigger = ApplyItem {verb :: !MU.Part, object :: !MU.Part, symbol :: !Char} | AlterFeature {verb :: !MU.Part, object :: !MU.Part, feature :: !TK.Feature} | TriggerFeature {verb :: !MU.Part, object :: !MU.Part, feature :: !TK.Feature}- deriving (Show, Read, Eq, Ord)+ deriving (Show, Read, Eq, Ord, Generic) +instance NFData Trigger+ -- | Commands that are forbidden on a remote level, because they -- would usually take time when invoked on one. -- Note that some commands that take time are not included,@@ -101,7 +120,9 @@ MoveItem{} -> True Apply{} -> True AlterDir{} -> True- StepToTarget -> True+ MoveOnceToCursor -> True+ RunOnceToCursor -> True+ ContinueToCursor -> True _ -> False -- | Description of player commands.@@ -113,11 +134,21 @@ MoveItem _ store2 mverb object _ -> let verb = fromMaybe (MU.Text $ verbCStore store2) mverb in makePhrase [verb, object]+ DescribeItem (MStore CGround) -> "manage items on the ground"+ DescribeItem (MStore COrgan) -> "describe organs of the leader"+ DescribeItem (MStore CEqp) -> "manage equipment of the leader"+ DescribeItem (MStore CInv) -> "manage inventory pack of the leader"+ DescribeItem (MStore CSha) -> "manage the shared party stash"+ DescribeItem MOwned -> "describe all owned items"+ DescribeItem MStats -> "show the stats summary of the leader" Project ts -> triggerDescription ts Apply ts -> triggerDescription ts AlterDir ts -> triggerDescription ts TriggerTile ts -> triggerDescription ts- StepToTarget -> "make one step towards the target"+ RunOnceAhead -> "run once ahead"+ MoveOnceToCursor -> "move one step towards the crosshair"+ RunOnceToCursor -> "run selected one step towards the crosshair"+ ContinueToCursor -> "continue towards the crosshair" GameRestart t -> -- TODO: use mname for the game mode instead of t@@ -131,45 +162,46 @@ PickLeader{} -> "pick leader" MemberCycle -> "cycle among party members on the level" MemberBack -> "cycle among all party members"- DescribeItem CGround -> "describe items on the ground"- DescribeItem COrgan -> "describe organs"- DescribeItem CEqp -> "describe equipment of the leader"- DescribeItem CInv -> "describe backpack inventory of the leader"- DescribeItem CSha -> "describe the shared party stash"- AllOwned -> "describe all owned items" SelectActor -> "select (or deselect) a party member" SelectNone -> "deselect (or select) all on the level" Clear -> "clear messages"+ StopIfTgtMode -> "stop playback if in aiming mode"+ SelectWithPointer -> "select actors if pointer over actor list" Repeat 1 -> "voice again the recorded commands" Repeat n -> "voice the recorded commands" <+> tshow n <+> "times" Record -> "start recording commands" History -> "display player diary"- MarkVision -> "mark visible zone"- MarkSmell -> "mark smell clues"- MarkSuspect -> "mark suspect terrain"+ MarkVision -> "toggle visible zone display"+ MarkSmell -> "toggle smell clues display"+ MarkSuspect -> "toggle suspect terrain display" Help -> "display help" MainMenu -> "display the Main Menu" Macro t _ -> t - MoveCursor v 1 -> "move cursor" <+> compassText v+ MoveCursor v 1 -> "move crosshair" <+> compassText v MoveCursor v k ->- "move cursor up to" <+> tshow k <+> "steps" <+> compassText v- TgtFloor -> "cycle targeting mode"- TgtEnemy -> "target enemy"- TgtUnknown -> "target the closest unknown spot"- TgtItem -> "target the closest item"- TgtStair up -> "target the closest stairs" <+> if up then "up" else "down"- TgtAscend k | k == 1 -> "target next shallower level"- TgtAscend k | k >= 2 -> "target" <+> tshow k <+> "levels shallower"- TgtAscend k | k == -1 -> "target next deeper level"- TgtAscend k | k <= -2 -> "target" <+> tshow (-k) <+> "levels deeper"- TgtAscend _ -> assert `failure` "void level change when targeting"+ "move crosshair up to" <+> tshow k <+> "steps" <+> compassText v+ TgtFloor -> "cycle aiming styles"+ TgtEnemy -> "aim at an enemy"+ TgtAscend k | k == 1 -> "aim at next shallower level"+ TgtAscend k | k >= 2 -> "aim at" <+> tshow k <+> "levels shallower"+ TgtAscend k | k == -1 -> "aim at next deeper level"+ TgtAscend k | k <= -2 -> "aim at" <+> tshow (-k) <+> "levels deeper"+ TgtAscend _ -> assert `failure` "void level change when aiming" `twith` cmd- EpsIncr True -> "swerve targeting line"- EpsIncr False -> "unswerve targeting line"- TgtClear -> "clear target/cursor"- Cancel -> "cancel action, open Main Menu"- Accept -> "accept choice"+ EpsIncr True -> "swerve the aiming line"+ EpsIncr False -> "unswerve the aiming line"+ TgtClear -> "reset target/crosshair"+ CursorUnknown -> "set crosshair to the closest unknown spot"+ CursorItem -> "set crosshair to the closest item"+ CursorStair up -> "set crosshair to the closest stairs"+ <+> if up then "up" else "down"+ Cancel -> "cancel action, open Main Menu"+ Accept -> "accept target/choice"+ CursorPointerFloor -> "set crosshair to floor under pointer"+ CursorPointerEnemy -> "set crosshair to enemy under pointer"+ TgtPointerFloor -> "enter aiming mode and describe a tile"+ TgtPointerEnemy -> "enter aiming mode and describe an enemy" triggerDescription :: [Trigger] -> Text triggerDescription [] = "trigger a thing"
Game/LambdaHack/Client/UI/InventoryClient.hs view
@@ -1,415 +1,1021 @@--- | Inventory management and party cycling.--- TODO: document-module Game.LambdaHack.Client.UI.InventoryClient- ( failMsg, getGroupItem, getAnyItem, getStoreItem- , memberCycle, memberBack, pickLeader- ) where--import Control.Exception.Assert.Sugar-import Control.Monad-import qualified Data.Char as Char-import qualified Data.EnumMap.Strict as EM-import Data.Function-import qualified Data.IntMap.Strict as IM-import Data.List-import Data.Maybe-import Data.Monoid-import Data.Text (Text)-import qualified Data.Text as T-import qualified NLP.Miniutter.English as MU--import Game.LambdaHack.Client.CommonClient-import Game.LambdaHack.Client.ItemSlot-import qualified Game.LambdaHack.Client.Key as K-import Game.LambdaHack.Client.MonadClient-import Game.LambdaHack.Client.State-import Game.LambdaHack.Client.UI.MonadClientUI-import Game.LambdaHack.Client.UI.MsgClient-import Game.LambdaHack.Client.UI.WidgetClient-import Game.LambdaHack.Common.Actor-import Game.LambdaHack.Common.ActorState-import Game.LambdaHack.Common.Faction-import Game.LambdaHack.Common.Item-import Game.LambdaHack.Common.Misc-import Game.LambdaHack.Common.MonadStateRead-import Game.LambdaHack.Common.Msg-import Game.LambdaHack.Common.Request-import Game.LambdaHack.Common.State--failMsg :: MonadClientUI m => Msg -> m Slideshow-failMsg msg = do- stopPlayBack- assert (not $ T.null msg) $ promptToSlideshow msg---- | Let a human player choose any item from a given group.--- Note that this does not guarantee the chosen item belongs to the group,--- as the player can override the choice.-getGroupItem :: MonadClientUI m- => (ItemFull -> Bool) -- ^ which items to consider suitable- -> Text -- ^ specific prompt for only suitable items- -> Text -- ^ generic prompt- -> [CStore] -- ^ initial legal containers- -> [CStore] -- ^ legal containers after Calm taken into account- -> m (SlideOrCmd ((ItemId, ItemFull), Container))-getGroupItem psuit prompt promptGeneric cLegalRaw cLegalAfterCalm = do- side <- getsClient sside- leader <- getLeaderUI- let aidNotEmpty store aid = do- bag <- getsState $ getCBag (CActor aid store)- return $! not $ EM.null bag- partyNotEmpty store = do- as <- getsState $ fidActorNotProjAssocs side- bs <- mapM (aidNotEmpty store . fst) as- return $! or bs- -- Don't display stores empty for all actors- cLegalNotEmpty <- filterM partyNotEmpty cLegalAfterCalm- -- Move a store that is empty for this actor to the back.- getCStoreBag <- getsState $ \s cstore -> getCBag (CActor leader cstore) s- let hasThisActor = not . EM.null . getCStoreBag- cLegal = case find hasThisActor cLegalAfterCalm of- Nothing -> cLegalNotEmpty- Just cThisActor -> cThisActor : delete cThisActor cLegalNotEmpty- getItem psuit (\_ _ _ -> prompt) (\_ _ _ -> promptGeneric)- (map (CActor leader) cLegalRaw)- (map (CActor leader) cLegal)- True INone---- | Let the human player choose any item from a list of items--- and let him specify the number of items.-getAnyItem :: MonadClientUI m- => MU.Part -- ^ the verb describing the action- -> [CStore] -- ^ initial legal containers- -> [CStore] -- ^ legal containers after Calm taken into account- -> Bool -- ^ whether to ask, when the only item- -- in the starting container is suitable- -> Bool -- ^ whether to ask for the number of items- -> m (SlideOrCmd ((ItemId, ItemFull), Container))-getAnyItem verb cLegalRaw cLegalAfterCalm askWhenLone askNumber = do- leader <- getLeaderUI- let prompt = makePhrase ["What to", verb]- soc <- getItem (const True) (\_ _ _ -> prompt) (\_ _ _ -> prompt)- (map (CActor leader) cLegalRaw)- (map (CActor leader) cLegalAfterCalm)- askWhenLone ISuitable- case soc of- Left _ -> return soc- Right ((iid, itemFull), c) -> do- socK <- pickNumber askNumber $ itemK itemFull- case socK of- Left slides -> return $ Left slides- Right k ->- return $ Right ((iid, itemFull{itemK=k}), c)---- | Display all items from a store and let the human player choose any--- or switch to any other store.-getStoreItem :: MonadClientUI m- => (Actor -> [ItemFull] -> Container -> Text)- -- ^ how to describe suitable items- -> Container -- ^ initial container- -> Bool -- ^ whether Enter should be disabled- -> m (SlideOrCmd ((ItemId, ItemFull), Container))-getStoreItem prompt cInitial noEnter = do- leader <- getLeaderUI- let allStores = map (CActor leader) [CEqp, CInv, CSha, CGround]- cLegalRaw = cInitial : delete cInitial allStores- dialogState = if noEnter then INoEnter else ISuitable- getItem (const True) prompt prompt cLegalRaw cLegalRaw- True dialogState--data ItemDialogState = INone | ISuitable | IAll | INoEnter- deriving (Show, Eq)---- | Let the human player choose a single, preferably suitable,--- item from a list of items.-getItem :: MonadClientUI m- => (ItemFull -> Bool) -- ^ which items to consider suitable- -> (Actor -> [ItemFull] -> Container -> Text)- -- ^ specific prompt for only suitable items- -> (Actor -> [ItemFull] -> Container -> Text)- -- ^ generic prompt- -> [Container] -- ^ initial legal containers- -> [Container] -- ^ legal containers with Calm taken into account- -> Bool -- ^ whether to ask, when the only item- -- in the starting container is suitable- -> ItemDialogState -- ^ the dialog state to start in- -> m (SlideOrCmd ((ItemId, ItemFull), Container))-getItem psuit prompt promptGeneric cLegalRaw cLegal askWhenLone initalState = do- leader <- getLeaderUI- accessCBag <- getsState $ flip getCBag- let storeAssocs = EM.assocs . accessCBag- allAssocs = concatMap storeAssocs cLegal- rawAssocs = concatMap storeAssocs cLegalRaw- mapM_ (updateItemSlot (Just leader)) $- concatMap (EM.keys . accessCBag) cLegal- case (cLegal, allAssocs) of- ([cStart], [(iid, k)]) | not askWhenLone -> do- itemToF <- itemToFullClient- return $ Right ((iid, itemToF iid k), cStart)- (_ : _, _ : _) ->- transition psuit prompt promptGeneric cLegal initalState- _ -> if null rawAssocs then do- let tLegal = map (MU.Text . ppContainer) cLegalRaw- ppLegal = makePhrase [MU.WWxW "nor" tLegal]- failWith $ "no items" <+> ppLegal- else failSer ItemNotCalm--data DefItemKey m = DefItemKey- { defLabel :: Text -- ^ can be undefined if not @defCond@- , defCond :: !Bool- , defAction :: K.Key -> m (SlideOrCmd ((ItemId, ItemFull), Container))- }--transition :: forall m. MonadClientUI m- => (ItemFull -> Bool) -- ^ which items to consider suitable- -> (Actor -> [ItemFull] -> Container -> Text)- -- ^ specific prompt for only suitable items- -> (Actor -> [ItemFull] -> Container -> Text)- -- ^ generic prompt- -> [Container]- -> ItemDialogState- -> m (SlideOrCmd ((ItemId, ItemFull), Container))-transition _ _ _ [] iDS = assert `failure` iDS-transition psuit prompt promptGeneric cLegal@(cCur:cRest) itemDialogState = do- (letterSlots, numberSlots) <- getsClient sslots- leader <- getLeaderUI- body <- getsState $ getActorBody leader- activeItems <- activeItemsClient leader- fact <- getsState $ (EM.! bfid body) . sfactionD- hs <- partyAfterLeader leader- bag <- getsState $ getCBag cCur- itemToF <- itemToFullClient- let getResult :: ItemId -> ((ItemId, ItemFull), Container)- getResult iid = ((iid, itemToF iid (bag EM.! iid)), cCur)- filterP iid kit = psuit $ itemToF iid kit- bagSuit = EM.filterWithKey filterP bag- bagLetterSlots = EM.filter (`EM.member` bag) letterSlots- bagNumberSlots = IM.filter (`EM.member` bag) numberSlots- suitableLetterSlots = EM.filter (`EM.member` bagSuit) letterSlots- (autoDun, autoLvl) = autoDungeonLevel fact- normalizeState INoEnter = INone- normalizeState x = x- keyDefs :: [(K.Key, DefItemKey m)]- keyDefs = filter (defCond . snd)- [ (K.Char '?', DefItemKey- { defLabel = "?"- , defCond = True- , defAction = \_ -> case normalizeState itemDialogState of- INone ->- if EM.null bagSuit- then transition psuit prompt promptGeneric cLegal IAll- else transition psuit prompt promptGeneric cLegal ISuitable- ISuitable | bag /= bagSuit ->- transition psuit prompt promptGeneric cLegal IAll- _ -> transition psuit prompt promptGeneric cLegal INone- })- , (K.Char '/', DefItemKey- { defLabel = "/"- , defCond = length cLegal > 1- , defAction = \_ ->- transition psuit prompt promptGeneric- (cRest ++ [cCur]) (normalizeState itemDialogState)- })- , (K.Return,- let enterSlots = if itemDialogState == IAll- then bagLetterSlots- else suitableLetterSlots- in DefItemKey- { defLabel = case EM.maxViewWithKey enterSlots of- Nothing -> assert `failure` "no suitable items"- `twith` enterSlots- Just ((l, _), _) -> "RET(" <> T.singleton (slotChar l) <> ")"- , defCond = not (EM.null enterSlots- || itemDialogState == INoEnter)- , defAction = \_ -> case EM.maxView enterSlots of- Nothing -> assert `failure` "no suitable items"- `twith` enterSlots- Just (iid, _) -> return $ Right $ getResult iid- })- , (K.Char '0', DefItemKey -- TODO: accept any number and pick the item- { defLabel = "0"- , defCond = not $ IM.null bagNumberSlots- , defAction = \_ -> case IM.minView bagNumberSlots of- Nothing -> assert `failure` "no numbered items"- `twith` bagNumberSlots- Just (iid, _) -> return $ Right $ getResult iid- })- , (K.Tab, DefItemKey- { defLabel = "TAB"- , defCond = not (autoLvl- || null (filter (\(_, b) ->- blid b == blid body) hs))- , defAction = \_ -> do- err <- memberCycle False- assert (err == mempty `blame` err) skip- newLeader <- getLeaderUI- let newC c = case c of- CActor _ cstore -> CActor newLeader cstore- _ -> c- newLegal = map newC cLegal- accessCBag <- getsState $ flip getCBag- mapM_ (updateItemSlot (Just newLeader)) $- concatMap (EM.keys . accessCBag) newLegal- transition psuit prompt promptGeneric newLegal itemDialogState- })- , (K.BackTab, DefItemKey- { defLabel = "SHIFT-TAB"- , defCond = not (autoDun || null hs)- , defAction = \_ -> do- err <- memberBack False- assert (err == mempty `blame` err) skip- newLeader <- getLeaderUI- let newC c = case c of- CActor _ cstore -> CActor newLeader cstore- _ -> c- newLegal = map newC cLegal- accessCBag <- getsState $ flip getCBag- mapM_ (updateItemSlot (Just newLeader)) $- concatMap (EM.keys . accessCBag) newLegal- transition psuit prompt promptGeneric newLegal itemDialogState- })- ]- lettersDef :: DefItemKey m- lettersDef = DefItemKey- { defLabel = slotRange $ EM.keys labelLetterSlots- , defCond = True- , defAction = \key -> case key of- K.Char l -> case EM.lookup (SlotChar l) bagLetterSlots of- Nothing -> assert `failure` "unexpected slot"- `twith` (l, bagLetterSlots)- Just iid -> return $ Right $ getResult iid- _ -> assert `failure` "unexpected key:" `twith` K.showKey key- }- ppCur = ppContainer cCur- (labelLetterSlots, bagFiltered, promptChosen) =- case itemDialogState of- ISuitable -> (suitableLetterSlots,- bagSuit,- prompt body activeItems cCur <+> ppCur <> ":")- IAll -> (bagLetterSlots,- bag,- promptGeneric body activeItems cCur <+> ppCur <> ":")- _ -> (suitableLetterSlots,- EM.empty,- prompt body activeItems cCur <+> ppCur <> ":")- io <- itemOverlay cCur (blid body) bagFiltered- runDefItemKey keyDefs lettersDef io bagLetterSlots promptChosen--runDefItemKey :: MonadClientUI m- => [(K.Key, DefItemKey m)]- -> DefItemKey m- -> Overlay- -> EM.EnumMap SlotChar ItemId- -> Text- -> m (SlideOrCmd ((ItemId, ItemFull), Container))-runDefItemKey keyDefs lettersDef io labelLetterSlots prompt = do- let itemKeys =- let slotKeys = map (K.Char . slotChar) (EM.keys labelLetterSlots)- defKeys = map fst keyDefs- in zipWith K.KM (repeat K.NoModifier) $ slotKeys ++ defKeys- choice = let letterRange = defLabel lettersDef- letterLabel | T.null letterRange = []- | otherwise = [letterRange]- keyLabels = letterLabel ++ map (defLabel . snd) keyDefs- in "[" <> T.intercalate ", " keyLabels- akm <- displayChoiceUI (prompt <+> choice) io itemKeys- case akm of- Left slides -> failSlides slides- Right K.KM{..} -> do- assert (modifier == K.NoModifier) skip- case lookup key keyDefs of- Just keyDef -> defAction keyDef key- Nothing -> defAction lettersDef key--pickNumber :: MonadClientUI m => Bool -> Int -> m (SlideOrCmd Int)-pickNumber askNumber kAll = do- let kDefault = kAll- if askNumber && kAll > 1 then do- let tDefault = tshow kDefault- kbound = min 9 kAll- kprompt = "Choose number [1-" <> tshow kbound- <> ", RET(" <> tDefault <> ")"- kkeys = zipWith K.KM (repeat K.NoModifier)- $ map (K.Char . Char.intToDigit) [1..kbound]- ++ [K.Return]- kkm <- displayChoiceUI kprompt emptyOverlay kkeys- case kkm of- Left slides -> failSlides slides- Right K.KM{key} ->- case key of- K.Char l -> return $ Right $ Char.digitToInt l- K.Return -> return $ Right kDefault- _ -> assert `failure` "unexpected key:" `twith` kkm- else return $ Right kAll---- | Switches current member to the next on the level, if any, wrapping.-memberCycle :: MonadClientUI m => Bool -> m Slideshow-memberCycle verbose = do- side <- getsClient sside- fact <- getsState $ (EM.! side) . sfactionD- leader <- getLeaderUI- body <- getsState $ getActorBody leader- hs <- partyAfterLeader leader- let autoLvl = snd $ autoDungeonLevel fact- case filter (\(_, b) -> blid b == blid body) hs of- _ | autoLvl -> failMsg $ showReqFailure NoChangeLvlLeader- [] -> failMsg "Cannot pick any other member on this level."- (np, b) : _ -> do- success <- pickLeader verbose np- assert (success `blame` "same leader" `twith` (leader, np, b)) skip- return mempty---- | Switches current member to the previous in the whole dungeon, wrapping.-memberBack :: MonadClientUI m => Bool -> m Slideshow-memberBack verbose = do- side <- getsClient sside- fact <- getsState $ (EM.! side) . sfactionD- leader <- getLeaderUI- hs <- partyAfterLeader leader- let autoDun = fst $ autoDungeonLevel fact- case reverse hs of- _ | autoDun -> failMsg $ showReqFailure NoChangeDunLeader- [] -> failMsg "No other member in the party."- (np, b) : _ -> do- success <- pickLeader verbose np- assert (success `blame` "same leader" `twith` (leader, np, b)) skip- return mempty--partyAfterLeader :: MonadStateRead m => ActorId -> m [(ActorId, Actor)]-partyAfterLeader leader = do- faction <- getsState $ bfid . getActorBody leader- allA <- getsState $ EM.assocs . sactorD- s <- getState- let hs9 = mapMaybe (tryFindHeroK s faction) [0..9]- factionA = filter (\(_, body) ->- not (bproj body) && bfid body == faction) allA- hs = hs9 ++ deleteFirstsBy ((==) `on` fst) factionA hs9- i = fromMaybe (-1) $ findIndex ((== leader) . fst) hs- (lt, gt) = (take i hs, drop (i + 1) hs)- return $! gt ++ lt---- | Select a faction leader. False, if nothing to do.-pickLeader :: MonadClientUI m => Bool -> ActorId -> m Bool-pickLeader verbose aid = do- leader <- getLeaderUI- stgtMode <- getsClient stgtMode- if leader == aid- then return False -- already picked- else do- pbody <- getsState $ getActorBody aid- assert (not (bproj pbody) `blame` "projectile chosen as the leader"- `twith` (aid, pbody)) skip- -- Even if it's already the leader, give his proper name, not 'you'.- let subject = partActor pbody- when verbose $ msgAdd $ makeSentence [subject, "picked as a leader"]- -- Update client state.- s <- getState- modifyClient $ updateLeader aid s- -- Move the cursor, if active, to the new level.- case stgtMode of- Nothing -> return ()- Just _ ->- modifyClient $ \cli -> cli {stgtMode = Just $ TgtMode $ blid pbody}- -- Inform about items, etc.- lookMsg <- lookAt False "" True (bpos pbody) aid ""- when verbose $ msgAdd lookMsg- return True+{-# LANGUAGE DataKinds #-}+-- | Inventory management and party cycling.+-- TODO: document+module Game.LambdaHack.Client.UI.InventoryClient+ ( Suitability(..)+ , getGroupItem, getAnyItems, getStoreItem+ , memberCycle, memberBack, pickLeader+ , cursorPointerFloor, cursorPointerEnemy+ , moveCursorHuman, tgtFloorHuman, tgtEnemyHuman, epsIncrHuman, tgtClearHuman+ , doLook, describeItemC+ ) where++import Control.Applicative+import Control.Exception.Assert.Sugar+import Control.Monad+import Data.Char (intToDigit)+import qualified Data.Char as Char+import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES+import Data.List+import qualified Data.Map.Strict as M+import Data.Maybe+import Data.Monoid+import Data.Ord+import Data.Text (Text)+import qualified Data.Text as T+import qualified NLP.Miniutter.English as MU++import Game.LambdaHack.Client.CommonClient+import Game.LambdaHack.Client.ItemSlot+import qualified Game.LambdaHack.Client.Key as K+import Game.LambdaHack.Client.MonadClient+import Game.LambdaHack.Client.State+import Game.LambdaHack.Client.UI.HumanCmd+import Game.LambdaHack.Client.UI.KeyBindings+import Game.LambdaHack.Client.UI.MonadClientUI+import Game.LambdaHack.Client.UI.MsgClient+import Game.LambdaHack.Client.UI.WidgetClient+import qualified Game.LambdaHack.Common.Ability as Ability+import Game.LambdaHack.Common.Actor+import Game.LambdaHack.Common.ActorState+import Game.LambdaHack.Common.Faction+import Game.LambdaHack.Common.Item+import Game.LambdaHack.Common.ItemDescription+import Game.LambdaHack.Common.ItemStrongest+import qualified Game.LambdaHack.Common.Kind as Kind+import Game.LambdaHack.Common.Level+import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Common.MonadStateRead+import Game.LambdaHack.Common.Msg+import Game.LambdaHack.Common.Perception+import Game.LambdaHack.Common.Point+import Game.LambdaHack.Common.Request+import Game.LambdaHack.Common.State+import Game.LambdaHack.Common.Vector+import qualified Game.LambdaHack.Content.ItemKind as IK++data ItemDialogState = ISuitable | IAll | INoSuitable | INoAll+ deriving (Show, Eq)++ppItemDialogMode :: ItemDialogMode -> (Text, Text)+ppItemDialogMode (MStore cstore) = ppCStore cstore+ppItemDialogMode MOwned = ("in", "our possession")+ppItemDialogMode MStats = ("among", "strenghts")++ppItemDialogModeIn :: ItemDialogMode -> Text+ppItemDialogModeIn c = let (tIn, t) = ppItemDialogMode c in tIn <+> t++ppItemDialogModeFrom :: ItemDialogMode -> Text+ppItemDialogModeFrom c = let (_tIn, t) = ppItemDialogMode c in "from" <+> t++storeFromMode :: ItemDialogMode -> CStore+storeFromMode c = case c of+ MStore cstore -> cstore+ MOwned -> CGround -- needed to decide display mode in textAllAE+ MStats -> CGround -- needed to decide display mode in textAllAE++accessModeBag :: ActorId -> State -> ItemDialogMode -> ItemBag+accessModeBag leader s (MStore cstore) = getActorBag leader cstore s+accessModeBag leader s MOwned = let fid = bfid $ getActorBody leader s+ in sharedAllOwnedFid False fid s+accessModeBag _ _ MStats = EM.empty++-- | Let a human player choose any item from a given group.+-- 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 :: MonadClientUI m+ => m Suitability+ -- ^ which items to consider suitable+ -> Text -- ^ specific prompt for only suitable items+ -> Text -- ^ generic prompt+ -> Bool -- ^ whether to enable setting cursor with mouse+ -> [CStore] -- ^ initial legal modes+ -> [CStore] -- ^ legal modes after Calm taken into account+ -> m (SlideOrCmd ((ItemId, ItemFull), ItemDialogMode))+getGroupItem psuit prompt promptGeneric cursor cLegalRaw cLegalAfterCalm = do+ let dialogState = if cursor then INoSuitable else ISuitable+ soc <- getFull psuit+ (\_ _ cCur -> prompt <+> ppItemDialogModeFrom cCur)+ (\_ _ cCur -> promptGeneric <+> ppItemDialogModeFrom cCur)+ cursor cLegalRaw cLegalAfterCalm True False dialogState+ case soc of+ Left sli -> return $ Left sli+ Right ([(iid, itemFull)], c) -> return $ Right ((iid, itemFull), c)+ Right _ -> assert `failure` soc++-- | Let the human player choose any item from a list of items+-- and let him specify the number of items.+-- Used, e.g., for picking up and inventory manipulation.+getAnyItems :: MonadClientUI m+ => MU.Part -- ^ the verb describing the action+ -> [CStore] -- ^ initial legal modes+ -> [CStore] -- ^ legal modes after Calm taken into account+ -> Bool -- ^ whether to ask, when the only item+ -- in the starting mode is suitable+ -> Bool -- ^ whether to ask for the number of items+ -> m (SlideOrCmd ([(ItemId, ItemFull)], ItemDialogMode))+getAnyItems verb cLegalRaw cLegalAfterCalm askWhenLone askNumber = do+ let prompt _ _ cCur =+ makePhrase ["What to", verb, MU.Text $ ppItemDialogModeFrom cCur]+ soc <- getFull (return SuitsEverything)+ prompt prompt False+ cLegalRaw cLegalAfterCalm+ askWhenLone True ISuitable+ case soc of+ Left _ -> return soc+ Right ([(iid, itemFull)], c) -> do+ socK <- pickNumber askNumber $ itemK itemFull+ case socK of+ Left slides -> return $ Left slides+ Right k ->+ return $ Right ([(iid, itemFull{itemK=k})], c)+ Right _ -> return soc++-- | 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 :: MonadClientUI m+ => (Actor -> [ItemFull] -> ItemDialogMode -> Text)+ -- ^ how to describe suitable items+ -> ItemDialogMode -- ^ initial mode+ -> m (SlideOrCmd ((ItemId, ItemFull), ItemDialogMode))+getStoreItem prompt cInitial = do+ let allCs = map MStore [CEqp, CInv, CSha]+ ++ [MOwned]+ ++ map MStore [CGround, COrgan]+ ++ [MStats]+ (pre, rest) = break (== cInitial) allCs+ post = dropWhile (== cInitial) rest+ remCs = post ++ pre+ soc <- getItem (return SuitsEverything)+ prompt prompt False cInitial remCs+ True False (cInitial:remCs) ISuitable+ case soc of+ Left sli -> return $ Left sli+ Right ([(iid, itemFull)], c) -> return $ Right ((iid, itemFull), c)+ Right _ -> assert `failure` soc++-- | 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 :: MonadClientUI m+ => m Suitability+ -- ^ which items to consider suitable+ -> (Actor -> [ItemFull] -> ItemDialogMode -> Text)+ -- ^ specific prompt for only suitable items+ -> (Actor -> [ItemFull] -> ItemDialogMode -> Text)+ -- ^ generic prompt+ -> Bool -- ^ whether to enable setting cursor with mouse+ -> [CStore] -- ^ initial legal modes+ -> [CStore] -- ^ legal modes with Calm taken into account+ -> Bool -- ^ whether to ask, when the only item+ -- in the starting mode is suitable+ -> Bool -- ^ whether to permit multiple items as a result+ -> ItemDialogState -- ^ the dialog state to start in+ -> m (SlideOrCmd ([(ItemId, ItemFull)], ItemDialogMode))+getFull psuit prompt promptGeneric cursor cLegalRaw cLegalAfterCalm+ askWhenLone permitMulitple initalState = do+ side <- getsClient sside+ leader <- getLeaderUI+ let aidNotEmpty store aid = do+ bag <- getsState $ getCBag (CActor aid store)+ return $! not $ EM.null bag+ partyNotEmpty store = do+ as <- getsState $ fidActorNotProjAssocs side+ bs <- mapM (aidNotEmpty store . fst) as+ return $! or bs+ mpsuit <- psuit+ let psuitFun = case mpsuit of+ SuitsEverything -> const True+ SuitsNothing _ -> const False+ SuitsSomething f -> f+ -- Move the first store that is non-empty for suitable items for this actor+ -- to the front, if any.+ getCStoreBag <- getsState $ \s cstore -> getCBag (CActor leader cstore) s+ let hasThisActor = not . EM.null . getCStoreBag+ case filter hasThisActor cLegalAfterCalm of+ [] ->+ if isNothing (find hasThisActor cLegalRaw) then do+ let contLegalRaw = map MStore cLegalRaw+ tLegal = map (MU.Text . ppItemDialogModeIn) contLegalRaw+ ppLegal = makePhrase [MU.WWxW "nor" tLegal]+ failWith $ "no items" <+> ppLegal+ else failSer ItemNotCalm+ haveThis@(headThisActor : _) -> do+ itemToF <- itemToFullClient+ let suitsThisActor store =+ let bag = getCStoreBag store+ in any (\(iid, kit) -> psuitFun $ itemToF iid kit) $ EM.assocs bag+ cThisActor cDef = case find suitsThisActor haveThis of+ Nothing -> cDef+ Just cSuits -> cSuits+ -- Don't display stores totally empty for all actors.+ cLegal <- filterM partyNotEmpty cLegalRaw+ let breakStores cInit =+ let (pre, rest) = break (== cInit) cLegal+ post = dropWhile (== cInit) rest+ in (MStore cInit, map MStore $ post ++ pre)+ -- The last used store may go before even the first nonempty store.+ lastStore <- getsClient slastStore+ firstStore <-+ if lastStore `notElem` cLegalAfterCalm+ then return $! cThisActor headThisActor+ else do+ (itemSlots, organSlots) <- getsClient sslots+ let lSlots = if lastStore == COrgan then organSlots else itemSlots+ lastSlot <- getsClient slastSlot+ case EM.lookup lastSlot lSlots of+ Nothing -> return $! cThisActor headThisActor+ Just lastIid -> case EM.lookup lastIid $ getCStoreBag lastStore of+ Nothing -> return $! cThisActor headThisActor+ Just kit -> do+ let lastItemFull = itemToF lastIid kit+ lastSuits = psuitFun lastItemFull+ cLast = cThisActor lastStore+ return $! if lastSuits && cLast /= CGround+ then lastStore+ else cLast+ let (modeFirst, modeRest) = breakStores firstStore+ getItem psuit prompt promptGeneric cursor modeFirst modeRest+ askWhenLone permitMulitple (map MStore $ cLegal) initalState++-- | Let the human player choose a single, preferably suitable,+-- item from a list of items.+getItem :: MonadClientUI m+ => m Suitability+ -- ^ which items to consider suitable+ -> (Actor -> [ItemFull] -> ItemDialogMode -> Text)+ -- ^ specific prompt for only suitable items+ -> (Actor -> [ItemFull] -> ItemDialogMode -> Text)+ -- ^ generic prompt+ -> Bool -- ^ whether to enable setting cursor with mouse+ -> ItemDialogMode -- ^ first mode, legal or not+ -> [ItemDialogMode] -- ^ the (rest of) legal modes+ -> Bool -- ^ whether to ask, when the only item+ -- in the starting mode is suitable+ -> Bool -- ^ whether to permit multiple items as a result+ -> [ItemDialogMode] -- ^ all legal modes+ -> ItemDialogState -- ^ the dialog state to start in+ -> m (SlideOrCmd ([(ItemId, ItemFull)], ItemDialogMode))+getItem psuit prompt promptGeneric cursor cCur cRest askWhenLone permitMulitple+ cLegal initalState = do+ leader <- getLeaderUI+ accessCBag <- getsState $ accessModeBag leader+ let storeAssocs = EM.assocs . accessCBag+ allAssocs = concatMap storeAssocs (cCur : cRest)+ case (cRest, allAssocs) of+ ([], [(iid, k)]) | not askWhenLone -> do+ itemToF <- itemToFullClient+ return $ Right ([(iid, itemToF iid k)], cCur)+ _ ->+ transition psuit prompt promptGeneric cursor permitMulitple cLegal+ 0 cCur cRest initalState++data DefItemKey m = DefItemKey+ { defLabel :: Text -- ^ can be undefined if not @defCond@+ , defCond :: !Bool+ , defAction :: K.KM -> m (SlideOrCmd ([(ItemId, ItemFull)], ItemDialogMode))+ }++data Suitability =+ SuitsEverything+ | SuitsNothing Msg+ | SuitsSomething (ItemFull -> Bool)++transition :: forall m. MonadClientUI m+ => m Suitability+ -> (Actor -> [ItemFull] -> ItemDialogMode -> Text)+ -> (Actor -> [ItemFull] -> ItemDialogMode -> Text)+ -> Bool+ -> Bool+ -> [ItemDialogMode]+ -> Int+ -> ItemDialogMode+ -> [ItemDialogMode]+ -> ItemDialogState+ -> m (SlideOrCmd ([(ItemId, ItemFull)], ItemDialogMode))+transition psuit prompt promptGeneric cursor permitMulitple cLegal+ numPrefix cCur cRest itemDialogState = do+ let recCall =+ transition psuit prompt promptGeneric cursor permitMulitple cLegal+ (itemSlots, organSlots) <- getsClient sslots+ leader <- getLeaderUI+ body <- getsState $ getActorBody leader+ activeItems <- activeItemsClient leader+ fact <- getsState $ (EM.! bfid body) . sfactionD+ hs <- partyAfterLeader leader+ bagAll <- getsState $ \s -> accessModeBag leader s cCur+ lastSlot <- getsClient slastSlot+ itemToF <- itemToFullClient+ Binding{brevMap} <- askBinding+ mpsuit <- psuit -- when throwing, this sets eps and checks cursor validity+ (suitsEverything, psuitFun) <- case mpsuit of+ SuitsEverything -> return (True, const True)+ SuitsNothing err -> do+ slides <- promptToSlideshow $ err <+> moreMsg+ void $ getInitConfirms ColorFull [] $ slides <> toSlideshow Nothing [[]]+ return (False, const False)+ -- When throwing, this function takes missile range into accout.+ SuitsSomething f -> return (False, f)+ let getSingleResult :: ItemId -> (ItemId, ItemFull)+ getSingleResult iid = (iid, itemToF iid (bagAll EM.! iid))+ getResult :: ItemId -> ([(ItemId, ItemFull)], ItemDialogMode)+ getResult iid = ([getSingleResult iid], cCur)+ getMultResult :: [ItemId] -> ([(ItemId, ItemFull)], ItemDialogMode)+ getMultResult iids = (map getSingleResult iids, cCur)+ filterP iid kit = psuitFun $ itemToF iid kit+ bagAllSuit = EM.filterWithKey filterP bagAll+ isOrgan = cCur == MStore COrgan+ lSlots = if isOrgan then organSlots else itemSlots+ bagItemSlotsAll = EM.filter (`EM.member` bagAll) lSlots+ -- Predicate for slot matching the current prefix, unless the prefix+ -- is 0, in which case we display all slots, even if they require+ -- the user to start with number keys to get to them.+ -- Could be generalized to 1 if prefix 1x exists, etc., but too rare.+ hasPrefixOpen x _ = slotPrefix x == numPrefix || numPrefix == 0+ bagItemSlotsOpen = EM.filterWithKey hasPrefixOpen bagItemSlotsAll+ hasPrefix x _ = slotPrefix x == numPrefix+ bagItemSlots = EM.filterWithKey hasPrefix bagItemSlotsOpen+ bag = EM.fromList $ map (\iid -> (iid, bagAll EM.! iid))+ (EM.elems bagItemSlotsOpen)+ suitableItemSlotsAll = EM.filter (`EM.member` bagAllSuit) lSlots+ suitableItemSlotsOpen =+ EM.filterWithKey hasPrefixOpen suitableItemSlotsAll+ suitableItemSlots = EM.filterWithKey hasPrefix suitableItemSlotsOpen+ bagSuit = EM.fromList $ map (\iid -> (iid, bagAllSuit EM.! iid))+ (EM.elems suitableItemSlotsOpen)+ (autoDun, autoLvl) = autoDungeonLevel fact+ multipleSlots = if itemDialogState `elem` [IAll, INoAll]+ then bagItemSlotsAll+ else suitableItemSlotsAll+ keyDefs :: [(K.KM, DefItemKey m)]+ keyDefs = filter (defCond . snd) $+ [ (K.toKM K.NoModifier $ K.Char '?', DefItemKey+ { defLabel = "?"+ , defCond = not (EM.null bag)+ , defAction = \_ -> recCall numPrefix cCur cRest+ $ case itemDialogState of+ INoSuitable -> if EM.null bagSuit then IAll else ISuitable+ ISuitable -> if suitsEverything then INoAll else IAll+ IAll -> if EM.null bag then INoSuitable else INoAll+ INoAll -> if suitsEverything then ISuitable else INoSuitable+ })+ , (K.toKM K.NoModifier $ K.Char '/', DefItemKey+ { defLabel = "/"+ , defCond = not $ null cRest+ , defAction = \_ -> do+ let calmE = calmEnough body activeItems+ mcCur = filter (`elem` cLegal) [cCur]+ (cCurAfterCalm, cRestAfterCalm) = case cRest ++ mcCur of+ c1@(MStore CSha) : c2 : rest | not calmE ->+ (c2, c1 : rest)+ [MStore CSha] | not calmE -> assert `failure` cRest+ c1 : rest -> (c1, rest)+ [] -> assert `failure` cRest+ recCall numPrefix cCurAfterCalm cRestAfterCalm itemDialogState+ })+ , (K.toKM K.NoModifier $ K.Char '*', DefItemKey+ { defLabel = "*"+ , defCond = permitMulitple && not (EM.null multipleSlots)+ , defAction = \_ ->+ let eslots = EM.elems multipleSlots+ in return $ Right $ getMultResult eslots+ })+ , (K.toKM K.NoModifier K.Return, DefItemKey+ { defLabel = if lastSlot `EM.member` labelItemSlotsOpen+ then let l = makePhrase [slotLabel lastSlot]+ in "RET(" <> l <> ")" -- l is on the screen list+ else "RET"+ , defCond = not (EM.null labelItemSlotsOpen)+ , defAction = \_ -> case EM.lookup lastSlot labelItemSlotsOpen of+ Just iid -> return $ Right $ getResult iid+ Nothing -> case EM.minViewWithKey labelItemSlotsOpen of+ Nothing -> assert `failure` "labelItemSlotsOpen empty"+ `twith` labelItemSlotsOpen+ Just ((l, _), _) -> do+ modifyClient $ \cli ->+ cli { slastSlot = l+ , slastStore = storeFromMode cCur }+ recCall numPrefix cCur cRest itemDialogState+ })+ , let km = M.findWithDefault (K.toKM K.NoModifier K.Tab)+ MemberCycle brevMap+ in (km, DefItemKey+ { defLabel = K.showKM km+ , defCond = not (cCur == MOwned+ || autoLvl+ || not (any (\(_, b) -> blid b == blid body) hs))+ , defAction = \_ -> do+ err <- memberCycle False+ let !_A = assert (err == mempty `blame` err) ()+ (cCurUpd, cRestUpd) <- legalWithUpdatedLeader cCur cRest+ recCall numPrefix cCurUpd cRestUpd itemDialogState+ })+ , let km = M.findWithDefault (K.toKM K.NoModifier K.BackTab)+ MemberBack brevMap+ in (km, DefItemKey+ { defLabel = K.showKM km+ , defCond = not (cCur == MOwned || autoDun || null hs)+ , defAction = \_ -> do+ err <- memberBack False+ let !_A = assert (err == mempty `blame` err) ()+ (cCurUpd, cRestUpd) <- legalWithUpdatedLeader cCur cRest+ recCall numPrefix cCurUpd cRestUpd itemDialogState+ })+ , let km = M.findWithDefault (K.toKM K.NoModifier (K.KP '/'))+ TgtFloor brevMap+ in cursorCmdDef False km tgtFloorHuman+ , let hackyCmd = Macro "" ["KP_Divide"] -- no keypad, but arrows enough+ km = M.findWithDefault (K.toKM K.NoModifier K.RightButtonPress)+ hackyCmd brevMap+ in cursorCmdDef False km tgtEnemyHuman+ , let km = M.findWithDefault (K.toKM K.NoModifier (K.KP '*'))+ TgtEnemy brevMap+ in cursorCmdDef False km tgtEnemyHuman+ , let hackyCmd = Macro "" ["KP_Multiply"] -- no keypad, but arrows OK+ km = M.findWithDefault (K.toKM K.NoModifier K.RightButtonPress)+ hackyCmd brevMap+ in cursorCmdDef False km tgtEnemyHuman+ , let km = M.findWithDefault (K.toKM K.NoModifier K.BackSpace)+ TgtClear brevMap+ in cursorCmdDef False km tgtClearHuman+ ]+ ++ numberPrefixes+ ++ [ let plusMinus = K.Char $ if b then '+' else '-'+ km = M.findWithDefault (K.toKM K.NoModifier plusMinus)+ (EpsIncr b) brevMap+ in cursorCmdDef False km (epsIncrHuman b)+ | b <- [True, False]+ ]+ ++ arrows+ ++ [+ let km = M.findWithDefault (K.toKM K.NoModifier K.MiddleButtonPress)+ CursorPointerEnemy brevMap+ in cursorCmdDef False km (cursorPointerEnemy False False)+ , let km = M.findWithDefault (K.toKM K.Shift K.MiddleButtonPress)+ CursorPointerFloor brevMap+ in cursorCmdDef False km (cursorPointerFloor False False)+ , let km = M.findWithDefault (K.toKM K.NoModifier K.RightButtonPress)+ TgtPointerEnemy brevMap+ in cursorCmdDef True km (cursorPointerEnemy True True)+ ]+ prefixCmdDef d =+ (K.toKM K.NoModifier $ K.Char (intToDigit d), DefItemKey+ { defLabel = ""+ , defCond = True+ , defAction = \_ ->+ recCall (10 * numPrefix + d) cCur cRest itemDialogState+ })+ numberPrefixes = map prefixCmdDef [0..9]+ cursorCmdDef verbose km cmd =+ (km, DefItemKey+ { defLabel = "keypad, mouse"+ , defCond = cursor && EM.null bagFiltered+ , defAction = \_ -> do+ look <- cmd+ when verbose $+ void $ getInitConfirms ColorFull []+ $ look <> toSlideshow Nothing [[]]+ recCall numPrefix cCur cRest itemDialogState+ })+ arrows =+ let kCmds = K.moveBinding False False+ (`moveCursorHuman` 1) (`moveCursorHuman` 10)+ in map (uncurry $ cursorCmdDef False) kCmds+ lettersDef :: DefItemKey m+ lettersDef = DefItemKey+ { defLabel = slotRange $ EM.keys labelItemSlots+ , defCond = True+ , defAction = \K.KM{key} -> case key of+ K.Char l -> case EM.lookup (SlotChar numPrefix l) bagItemSlots of+ Nothing -> assert `failure` "unexpected slot"+ `twith` (l, bagItemSlots)+ Just iid -> return $ Right $ getResult iid+ _ -> assert `failure` "unexpected key:" `twith` K.showKey key+ }+ (labelItemSlotsOpen, labelItemSlots, bagFiltered, promptChosen) =+ case itemDialogState of+ ISuitable -> (suitableItemSlotsOpen,+ suitableItemSlots,+ bagSuit,+ prompt body activeItems cCur <> ":")+ IAll -> (bagItemSlotsOpen,+ bagItemSlots,+ bag,+ promptGeneric body activeItems cCur <> ":")+ INoSuitable -> (suitableItemSlotsOpen,+ suitableItemSlots,+ EM.empty,+ prompt body activeItems cCur <> ":")+ INoAll -> (bagItemSlotsOpen,+ bagItemSlots,+ EM.empty,+ promptGeneric body activeItems cCur <> ":")+ io <- case cCur of+ MStats -> statsOverlay leader -- TODO: describe each stat when selected+ _ -> itemOverlay (storeFromMode cCur) (blid body) bagFiltered+ runDefItemKey keyDefs lettersDef io bagItemSlots promptChosen++statsOverlay :: MonadClient m => ActorId -> m Overlay+statsOverlay aid = do+ b <- getsState $ getActorBody aid+ activeItems <- activeItemsClient aid+ let block n = n + if braced b then 50 else 0+ prSlot :: (IK.EqpSlot, Int -> Text) -> Text+ prSlot (eqpSlot, f) =+ let fullText t =+ " "+ <> makePhrase [ MU.Text $ T.justifyLeft 22 ' '+ $ IK.slotName eqpSlot+ , MU.Text t ]+ <> " "+ valueText = f $ sumSlotNoFilter eqpSlot activeItems+ in fullText valueText+ -- Some values can be negative, for others 0 is equivalent but shorter.+ slotList = -- TODO: [IK.EqpSlotAddHurtMelee..IK.EqpSlotAddLight]+ [ (IK.EqpSlotAddHurtMelee, \t -> tshow t <> "%")+ -- TODO: not applicable right now, IK.EqpSlotAddHurtRanged+ , (IK.EqpSlotAddArmorMelee, \t -> "[" <> tshow (block t) <> "%]")+ , (IK.EqpSlotAddArmorRanged, \t -> "{" <> tshow (block t) <> "%}")+ , (IK.EqpSlotAddMaxHP, \t -> tshow $ max 0 t)+ , (IK.EqpSlotAddMaxCalm, \t -> tshow $ max 0 t)+ , (IK.EqpSlotAddSpeed, \t -> tshow (max 0 t) <> "m/10s")+ , (IK.EqpSlotAddSight, \t ->+ tshow (max 0 $ min (fromIntegral $ bcalm b `div` (5 * oneM)) t)+ <> "m")+ , (IK.EqpSlotAddSmell, \t -> tshow (max 0 t) <> "m")+ , (IK.EqpSlotAddLight, \t -> tshow (max 0 t) <> "m")+ ]+ skills = sumSkills activeItems+ -- TODO: are negative total skills meaningful?+ prAbility :: Ability.Ability -> Text+ prAbility ability =+ let fullText t =+ " "+ <> makePhrase [ MU.Text $ T.justifyLeft 22 ' '+ $ "ability" <+> tshow ability+ , MU.Text t ]+ <> " "+ valueText = tshow $ EM.findWithDefault 0 ability skills+ in fullText valueText+ abilityList = [minBound..maxBound]+ return $! toOverlay $ map prSlot slotList ++ map prAbility abilityList++legalWithUpdatedLeader :: MonadClientUI m+ => ItemDialogMode+ -> [ItemDialogMode]+ -> m (ItemDialogMode, [ItemDialogMode])+legalWithUpdatedLeader cCur cRest = do+ leader <- getLeaderUI+ let newLegal = cCur : cRest -- not updated in any way yet+ b <- getsState $ getActorBody leader+ activeItems <- activeItemsClient leader+ let calmE = calmEnough b activeItems+ legalAfterCalm = case newLegal of+ c1@(MStore CSha) : c2 : rest | not calmE -> (c2, c1 : rest)+ [MStore CSha] | not calmE -> (MStore CGround, newLegal)+ c1 : rest -> (c1, rest)+ [] -> assert `failure` (cCur, cRest)+ return legalAfterCalm++runDefItemKey :: MonadClientUI m+ => [(K.KM, DefItemKey m)]+ -> DefItemKey m+ -> Overlay+ -> EM.EnumMap SlotChar ItemId+ -> Text+ -> m (SlideOrCmd ([(ItemId, ItemFull)], ItemDialogMode))+runDefItemKey keyDefs lettersDef io labelItemSlots prompt = do+ let itemKeys =+ let slotKeys = map (K.Char . slotChar) (EM.keys labelItemSlots)+ defKeys = map fst keyDefs+ in map (K.toKM K.NoModifier) slotKeys ++ defKeys+ choice = let letterRange = defLabel lettersDef+ keyLabelsRaw = letterRange : map (defLabel . snd) keyDefs+ keyLabels = filter (not . T.null) keyLabelsRaw+ in "[" <> T.intercalate ", " (nub keyLabels)+ akm <- displayChoiceUI (prompt <+> choice) io itemKeys+ case akm of+ Left slides -> failSlides slides+ Right km ->+ case lookup km{K.pointer=dummyPoint} keyDefs of+ Just keyDef -> defAction keyDef km+ Nothing -> defAction lettersDef km++pickNumber :: MonadClientUI m => Bool -> Int -> m (SlideOrCmd Int)+pickNumber askNumber kAll = do+ let kDefault = kAll+ if askNumber && kAll > 1 then do+ let tDefault = tshow kDefault+ kbound = min 9 kAll+ kprompt = "Choose number [1-" <> tshow kbound+ <> ", RET(" <> tDefault <> ")"+ kkeys = map (K.toKM K.NoModifier)+ $ map (K.Char . Char.intToDigit) [1..kbound]+ ++ [K.Return]+ kkm <- displayChoiceUI kprompt emptyOverlay kkeys+ case kkm of+ Left slides -> failSlides slides+ Right K.KM{key} ->+ case key of+ K.Char l -> return $ Right $ Char.digitToInt l+ K.Return -> return $ Right kDefault+ _ -> assert `failure` "unexpected key:" `twith` kkm+ else return $ Right kAll++-- | Switches current member to the next on the level, if any, wrapping.+memberCycle :: MonadClientUI m => Bool -> m Slideshow+memberCycle verbose = do+ side <- getsClient sside+ fact <- getsState $ (EM.! side) . sfactionD+ leader <- getLeaderUI+ body <- getsState $ getActorBody leader+ hs <- partyAfterLeader leader+ let autoLvl = snd $ autoDungeonLevel fact+ case filter (\(_, b) -> blid b == blid body) hs of+ _ | autoLvl -> failMsg $ showReqFailure NoChangeLvlLeader+ [] -> failMsg "cannot pick any other member on this level"+ (np, b) : _ -> do+ success <- pickLeader verbose np+ let !_A = assert (success `blame` "same leader" `twith` (leader, np, b)) ()+ return mempty++-- | Switches current member to the previous in the whole dungeon, wrapping.+memberBack :: MonadClientUI m => Bool -> m Slideshow+memberBack verbose = do+ side <- getsClient sside+ fact <- getsState $ (EM.! side) . sfactionD+ leader <- getLeaderUI+ hs <- partyAfterLeader leader+ let autoDun = fst $ autoDungeonLevel fact+ case reverse hs of+ _ | autoDun -> failMsg $ showReqFailure NoChangeDunLeader+ [] -> failMsg "no other member in the party"+ (np, b) : _ -> do+ success <- pickLeader verbose np+ let !_A = assert (success `blame` "same leader" `twith` (leader, np, b)) ()+ return mempty++partyAfterLeader :: MonadStateRead m => ActorId -> m [(ActorId, Actor)]+partyAfterLeader leader = do+ faction <- getsState $ bfid . getActorBody leader+ allA <- getsState $ EM.assocs . sactorD+ let factionA = filter (\(_, body) ->+ not (bproj body) && bfid body == faction) allA+ hs = sortBy (comparing keySelected) factionA+ i = fromMaybe (-1) $ findIndex ((== leader) . fst) hs+ (lt, gt) = (take i hs, drop (i + 1) hs)+ return $! gt ++ lt++-- | Select a faction leader. False, if nothing to do.+pickLeader :: MonadClientUI m => Bool -> ActorId -> m Bool+pickLeader verbose aid = do+ leader <- getLeaderUI+ stgtMode <- getsClient stgtMode+ if leader == aid+ then return False -- already picked+ else do+ pbody <- getsState $ getActorBody aid+ let !_A = assert (not (bproj pbody)+ `blame` "projectile chosen as the leader"+ `twith` (aid, pbody)) ()+ -- Even if it's already the leader, give his proper name, not 'you'.+ let subject = partActor pbody+ when verbose $ msgAdd $ makeSentence [subject, "picked as a leader"]+ -- Update client state.+ s <- getState+ modifyClient $ updateLeader aid s+ -- Move the cursor, if active, to the new level.+ case stgtMode of+ Nothing -> return ()+ Just _ ->+ modifyClient $ \cli -> cli {stgtMode = Just $ TgtMode $ blid pbody}+ -- Inform about items, etc.+ lookMsg <- lookAt False "" True (bpos pbody) aid ""+ when verbose $ msgAdd lookMsg+ return True++cursorPointerFloor :: MonadClientUI m => Bool -> Bool -> m Slideshow+cursorPointerFloor verbose addMoreMsg = do+ km <- getsClient slastKM+ let newPos@Point{..} = K.pointer km+ lidV <- viewedLevel+ Level{lxsize, lysize} <- getLevel lidV+ if px < 0 || py < 0 || px >= lxsize || py >= lysize then do+ stopPlayBack+ return mempty+ else do+ let scursor = TPoint lidV newPos+ modifyClient $ \cli -> cli {scursor, stgtMode = Just $ TgtMode lidV}+ if verbose then+ doLook addMoreMsg+ else do+ displayPush "" -- flash the targeting line and path+ displayDelay -- for a bit longer+ return mempty++cursorPointerEnemy :: MonadClientUI m => Bool -> Bool -> m Slideshow+cursorPointerEnemy verbose addMoreMsg = do+ km <- getsClient slastKM+ let newPos@Point{..} = K.pointer km+ lidV <- viewedLevel+ Level{lxsize, lysize} <- getLevel lidV+ if px < 0 || py < 0 || px >= lxsize || py >= lysize then do+ stopPlayBack+ return mempty+ else do+ bsAll <- getsState $ actorAssocs (const True) lidV+ let scursor =+ case find (\(_, m) -> bpos m == newPos) bsAll of+ Just (im, _) -> TEnemy im True+ Nothing -> TPoint lidV newPos+ modifyClient $ \cli -> cli {scursor, stgtMode = Just $ TgtMode lidV}+ if verbose then+ doLook addMoreMsg+ else do+ displayPush "" -- flash the targeting line and path+ displayDelay -- for a bit longer+ return mempty++-- | Move the cursor. Assumes targeting mode.+moveCursorHuman :: MonadClientUI m => Vector -> Int -> m Slideshow+moveCursorHuman dir n = do+ leader <- getLeaderUI+ stgtMode <- getsClient stgtMode+ let lidV = maybe (assert `failure` leader) tgtLevelId stgtMode+ Level{lxsize, lysize} <- getLevel lidV+ lpos <- getsState $ bpos . getActorBody leader+ scursor <- getsClient scursor+ cursorPos <- cursorToPos+ let cpos = fromMaybe lpos cursorPos+ shiftB pos = shiftBounded lxsize lysize pos dir+ newPos = iterate shiftB cpos !! n+ if newPos == cpos then failMsg "never mind"+ else do+ let tgt = case scursor of+ TVector{} -> TVector $ newPos `vectorToFrom` lpos+ _ -> TPoint lidV newPos+ modifyClient $ \cli -> cli {scursor = tgt}+ doLook False++-- | Cycle targeting mode. Do not change position of the cursor,+-- switch among things at that position.+tgtFloorHuman :: MonadClientUI m => m Slideshow+tgtFloorHuman = do+ lidV <- viewedLevel+ leader <- getLeaderUI+ lpos <- getsState $ bpos . getActorBody leader+ cursorPos <- cursorToPos+ scursor <- getsClient scursor+ stgtMode <- getsClient stgtMode+ bsAll <- getsState $ actorAssocs (const True) lidV+ let cursor = fromMaybe lpos cursorPos+ tgt = case scursor of+ _ | isNothing stgtMode -> -- first key press: keep target+ scursor+ TEnemy a True -> TEnemy a False+ TEnemy{} -> TPoint lidV cursor+ TEnemyPos{} -> TPoint lidV cursor+ TPoint{} -> TVector $ cursor `vectorToFrom` lpos+ TVector{} ->+ -- For projectiles, we pick here the first that would be picked+ -- by '*', so that all other projectiles on the tile come next,+ -- without any intervening actors from other tiles.+ case find (\(_, m) -> Just (bpos m) == cursorPos) bsAll of+ Just (im, _) -> TEnemy im True+ Nothing -> TPoint lidV cursor+ modifyClient $ \cli -> cli {scursor = tgt, stgtMode = Just $ TgtMode lidV}+ doLook False++tgtEnemyHuman :: MonadClientUI m => m Slideshow+tgtEnemyHuman = do+ lidV <- viewedLevel+ leader <- getLeaderUI+ lpos <- getsState $ bpos . getActorBody leader+ cursorPos <- cursorToPos+ scursor <- getsClient scursor+ stgtMode <- getsClient stgtMode+ side <- getsClient sside+ fact <- getsState $ (EM.! side) . sfactionD+ bsAll <- getsState $ actorAssocs (const True) lidV+ let ordPos (_, b) = (chessDist lpos $ bpos b, bpos b)+ dbs = sortBy (comparing ordPos) bsAll+ pickUnderCursor = -- switch to the enemy under cursor, if any+ let i = fromMaybe (-1)+ $ findIndex ((== cursorPos) . Just . bpos . snd) dbs+ in splitAt i dbs+ (permitAnyActor, (lt, gt)) = case scursor of+ TEnemy a permit | isJust stgtMode -> -- pick next enemy+ let i = fromMaybe (-1) $ findIndex ((== a) . fst) dbs+ in (permit, splitAt (i + 1) dbs)+ TEnemy a permit -> -- first key press, retarget old enemy+ let i = fromMaybe (-1) $ findIndex ((== a) . fst) dbs+ in (permit, splitAt i dbs)+ TEnemyPos _ _ _ permit -> (permit, pickUnderCursor)+ _ -> (False, pickUnderCursor) -- the sensible default is only-foes+ gtlt = gt ++ lt+ isEnemy b = isAtWar fact (bfid b)+ && not (bproj b)+ lf = filter (isEnemy . snd) gtlt+ tgt | permitAnyActor = case gtlt of+ (a, _) : _ -> TEnemy a True+ [] -> scursor -- no actors in sight, stick to last target+ | otherwise = case lf of+ (a, _) : _ -> TEnemy a False+ [] -> scursor -- no seen foes in sight, stick to last target+ -- Register the chosen enemy, to pick another on next invocation.+ modifyClient $ \cli -> cli {scursor = tgt, stgtMode = Just $ TgtMode lidV}+ doLook False++-- | Tweak the @eps@ parameter of the targeting digital line.+epsIncrHuman :: MonadClientUI m => Bool -> m Slideshow+epsIncrHuman b = do+ stgtMode <- getsClient stgtMode+ if isJust stgtMode+ then do+ modifyClient $ \cli -> cli {seps = seps cli + if b then 1 else -1}+ return mempty+ else failMsg "never mind" -- no visual feedback, so no sense++tgtClearHuman :: MonadClientUI m => m Slideshow+tgtClearHuman = do+ leader <- getLeaderUI+ tgt <- getsClient $ getTarget leader+ case tgt of+ Just _ -> do+ modifyClient $ updateTarget leader (const Nothing)+ return mempty+ Nothing -> do+ scursorOld <- getsClient scursor+ b <- getsState $ getActorBody leader+ let scursor = case scursorOld of+ TEnemy _ permit -> TEnemy leader permit+ TEnemyPos _ _ _ permit -> TEnemy leader permit+ TPoint{} -> TPoint (blid b) (bpos b)+ TVector{} -> TVector (Vector 0 0)+ modifyClient $ \cli -> cli {scursor}+ doLook False++-- | Perform look around in the current position of the cursor.+-- Normally expects targeting mode and so that a leader is picked.+doLook :: MonadClientUI m => Bool -> m Slideshow+doLook addMoreMsg = do+ Kind.COps{cotile=Kind.Ops{ouniqGroup}} <- getsState scops+ let unknownId = ouniqGroup "unknown space"+ stgtMode <- getsClient stgtMode+ case stgtMode of+ Nothing -> return mempty+ Just tgtMode -> do+ leader <- getLeaderUI+ let lidV = tgtLevelId tgtMode+ lvl <- getLevel lidV+ cursorPos <- cursorToPos+ per <- getPerFid lidV+ b <- getsState $ getActorBody leader+ let p = fromMaybe (bpos b) cursorPos+ canSee = ES.member p (totalVisible per)+ inhabitants <- if canSee+ then getsState $ posToActors p lidV+ else return []+ seps <- getsClient seps+ mnewEps <- makeLine False b p seps+ itemToF <- itemToFullClient+ let aims = isJust mnewEps+ enemyMsg = case inhabitants of+ [] -> ""+ ((_, body), _) : rest ->+ -- Even if it's the leader, give his proper name, not 'you'.+ let subjects = map (partActor . snd . fst) inhabitants+ subject = MU.WWandW subjects+ verb = "be here"+ desc = if not (null rest) -- many actors+ then ""+ else case itemDisco $ itemToF (btrunk body) (1, []) of+ Nothing -> ""+ Just ItemDisco{itemKind} -> IK.idesc itemKind+ pdesc = if desc == "" then "" else "(" <> desc <> ")"+ in makeSentence [MU.SubjectVerbSg subject verb] <+> pdesc+ vis | lvl `at` p == unknownId = "that is"+ | not canSee = "you remember"+ | not aims = "you are aware of"+ | otherwise = "you see"+ -- Show general info about current position.+ lookMsg <- lookAt True vis canSee p leader enemyMsg+{- targeting is kind of a menu (or at least mode), so this is menu inside+ a menu, which is messy, hence disabled until UI overhauled:+ -- Check if there's something lying around at current position.+ is <- getsState $ getCBag $ CFloor lidV p+ if EM.size is <= 2 then+ promptToSlideshow lookMsg+ else do+ msgAdd lookMsg -- TODO: do not add to history+ floorItemOverlay lidV p+-}+ promptToSlideshow $ lookMsg <+> if addMoreMsg then moreMsg else ""+++-- | Create a list of item names.+_floorItemOverlay :: MonadClientUI m+ => LevelId -> Point+ -> m (SlideOrCmd (RequestTimed 'Ability.AbMoveItem))+_floorItemOverlay _lid _p = describeItemC MOwned {-CFloor lid p-}++describeItemC :: MonadClientUI m+ => ItemDialogMode+ -> m (SlideOrCmd (RequestTimed 'Ability.AbMoveItem))+describeItemC c = do+ let subject = partActor+ verbSha body activeItems = if calmEnough body activeItems+ then "notice"+ else "paw distractedly"+ prompt body activeItems c2 =+ let (tIn, t) = ppItemDialogMode c2+ in case c2 of+ MStore CGround -> -- TODO: variant for actors without (unwounded) feet+ makePhrase+ [ MU.Capitalize $ MU.SubjectVerbSg (subject body) "notice"+ , MU.Text "at"+ , MU.WownW (MU.Text $ bpronoun body) $ MU.Text "feet" ]+ MStore CSha ->+ makePhrase+ [ MU.Capitalize+ $ MU.SubjectVerbSg (subject body) (verbSha body activeItems)+ , MU.Text tIn+ , MU.Text t ]+ MStore COrgan ->+ makePhrase+ [ MU.Capitalize $ MU.SubjectVerbSg (subject body) "feel"+ , MU.Text tIn+ , MU.WownW (MU.Text $ bpronoun body) $ MU.Text t ]+ MOwned ->+ makePhrase+ [ MU.Capitalize $ MU.SubjectVerbSg (subject body) "recall"+ , MU.Text tIn+ , MU.Text t ]+ MStats ->+ makePhrase+ [ MU.Capitalize $ MU.SubjectVerbSg (subject body) "estimate"+ , MU.WownW (MU.Text $ bpronoun body) $ MU.Text t ]+ _ ->+ makePhrase+ [ MU.Capitalize $ MU.SubjectVerbSg (subject body) "see"+ , MU.Text tIn+ , MU.WownW (MU.Text $ bpronoun body) $ MU.Text t ]+ ggi <- getStoreItem prompt c+ case ggi of+ Right ((iid, itemFull), c2) -> do+ leader <- getLeaderUI+ b <- getsState $ getActorBody leader+ activeItems <- activeItemsClient leader+ let calmE = calmEnough b activeItems+ localTime <- getsState $ getLocalTime (blid b)+ let io = itemDesc (storeFromMode c2) (blid b) localTime itemFull+ case c2 of+ MStore COrgan -> do+ let symbol = jsymbol (itemBase itemFull)+ blurb | symbol == '+' = "drop temporary conditions"+ | otherwise = "amputate organs"+ -- TODO: also forbid on the server, except in special cases.+ Left <$> overlayToSlideshow ("Can't"+ <+> blurb+ <> ", but here's the description.") io+ MStore CSha | not calmE -> do+ Left <$> overlayToSlideshow "Not enough calm to take items from the shared stash, but here's the description." io+ MStore fromCStore -> do+ let prompt2 = "Where to move the item?"+ fstores :: [(K.Key, (CStore, Text))]+ fstores =+ filter ((/= fromCStore) . fst . snd) $+ [ (K.Char 'e', (CEqp, "'e'quipment"))+ , (K.Char 'p', (CInv, "inventory 'p'ack")) ]+ ++ [ (K.Char 's', (CSha, "shared 's'tash")) | calmE ]+ ++ [ (K.Char 'g', (CGround, "'g'round")) ]+ choice = "[" <> T.intercalate ", " (map (snd . snd) fstores)+ keys = map (K.toKM K.NoModifier . K.Char) "epsg"+ akm <- displayChoiceUI (prompt2 <+> choice) io keys+ case akm of+ Left slides -> failSlides slides+ Right km -> do+ socK <- pickNumber True $ itemK itemFull+ case socK of+ Left slides -> return $ Left slides+ Right k -> do+ let lr toCStore = return $ Right $ ReqMoveItems+ [(iid, k, fromCStore, toCStore)]+ case lookup (K.key km) fstores of+ Just (store, _) -> lr store+ Nothing -> return $ Left mempty+ MOwned -> do+ -- We can't move items from MOwned, because different copies may come+ -- from different stores and we can't guess player's intentions.+ found <- getsState $ findIid leader (bfid b) iid+ let !_A = assert (not (null found) `blame` ggi) ()+ let ppLoc (_, CSha) = MU.Text $ ppCStoreIn CSha <+> "of the party"+ ppLoc (b2, store) = MU.Text $ ppCStoreIn store <+> "of" <+> bname b2+ foundTexts = map ppLoc found+ prompt2 = makeSentence ["The item is", MU.WWandW foundTexts]+ Left <$> overlayToSlideshow prompt2 io+ MStats -> assert `failure` ggi+ Left slides -> return $ Left slides
Game/LambdaHack/Client/UI/KeyBindings.hs view
@@ -34,13 +34,21 @@ -> Config -- ^ game config -> Binding -- ^ concrete binding stdBinding copsClient !Config{configCommands, configVi, configLaptop} =- let heroSelect k = ( K.KM { key=K.Char (Char.intToDigit k)- , modifier=K.NoModifier }+ let heroSelect k = ( K.toKM K.NoModifier (K.Char (Char.intToDigit k)) , ([CmdMeta], PickLeader k) ) cmdWithHelp = rhumanCommands copsClient ++ configCommands cmdAll = cmdWithHelp- ++ [(K.mkKM "KP_Begin", ([CmdMove], Wait))]+ ++ [ (K.mkKM "KP_Begin", ([CmdMove], Wait))+ , (K.mkKM "CTRL-KP_Begin", ([CmdMove], Macro "" ["KP_Begin"]))+ , (K.mkKM "KP_5", ([CmdMove], Macro "" ["KP_Begin"]))+ , (K.mkKM "CTRL-KP_5", ([CmdMove], Macro "" ["KP_Begin"])) ]+ ++ (if configVi+ then [ (K.mkKM "period", ([CmdMove], Macro "" ["KP_Begin"])) ]+ else if configLaptop+ then [ (K.mkKM "i", ([CmdMove], Macro "" ["KP_Begin"]))+ , (K.mkKM "I", ([CmdMove], Macro "" ["KP_Begin"])) ]+ else []) ++ K.moveBinding configVi configLaptop (\v -> ([CmdMove], Move v)) (\v -> ([CmdMove], Run v)) ++ fmap heroSelect [0..6]@@ -48,35 +56,18 @@ in Binding { bcmdMap = M.fromList $ map (second mkDescribed) cmdAll , bcmdList = map (second mkDescribed) cmdWithHelp- , brevMap = M.fromList $ map swap $ map (second snd) cmdAll+ , brevMap = M.fromList $ map (swap . second snd) cmdAll } -- | Produce a set of help screens from the key bindings. keyHelp :: Binding -> Slideshow keyHelp Binding{bcmdList} = let- minimalBlurb =- [ "Move throughout a level with numerical keypad or, optionally, other keys."- , "Run ahead (until anything disturbs you) with SHIFT (or CTRL) and a key."- , ""- , " 7 8 9 7 8 9 y k u"- , " \\|/ \\|/ \\|/"- , " 4-5-6 u-i-o h-.-l"- , " /|\\ /|\\ /|\\"- , " 1 2 3 j k l b j n"- , ""- , "Interact with the dungeon using the following basic commands."- , ""- ]- minCatBlurb =- [ ""- , "Press SPACE to see detailed descriptions of all commands."- ] movBlurb =- [ "Move throughout a level with numerical keypad (left diagram)"- , "or its compact laptop replacement (middle) or Vi text editor keys"+ [ "Walk throughout a level with mouse or numeric keypad (left diagram)"+ , "or its compact laptop replacement (middle) or the Vi text editor keys" , "(right, also known as \"Rogue-like keys\"; can be enabled in config.ui.ini)."- , "Run ahead, until anything disturbs you, with SHIFT (or CTRL) and a key."+ , "Run, until disturbed, with left mouse button or SHIFT (or CTRL) and a key." , "" , " 7 8 9 7 8 9 y k u" , " \\|/ \\|/ \\|/"@@ -84,8 +75,8 @@ , " /|\\ /|\\ /|\\" , " 1 2 3 j k l b j n" , ""- , "In targeting mode the very same keys move the targeting cursor."- , "Press '5' or 'i' or '.' to wait, bracing for blows, which reduces"+ , "In aiming mode the same keys (or mouse) move the crosshair (the white box)."+ , "Press 'KEYPAD_5' (or 'i' or '.') to wait, bracing for blows, which reduces" , "any damage taken and makes it impossible for foes to displace you." , "You displace enemies or friends by bumping into them with SHIFT (or CTRL)." , ""@@ -93,8 +84,20 @@ , "The best item to attack with is automatically chosen from among" , "weapons in your personal equipment and your unwounded organs." , ""- , "Press SPACE to see command descriptions."+ , "Press SPACE to see the minimal command set." ]+ minimalBlurb =+ [ "The following minimal command set lets you accomplish anything in the game,"+ , "though not neccessarily with the fewest number of keystrokes."+ , "Most of the other commands are shorthands, defined as macros"+ , "(with the exception of the advanced commands for assigning non-default"+ , "tactics and targets to your autonomous henchmen, if you have any)."+ , ""+ ]+ casualEndBlurb =+ [ ""+ , "Press SPACE to see the detailed descriptions of all commands."+ ] categoryBlurb = [ "" , "Press SPACE to see the next page of command descriptions."@@ -102,40 +105,52 @@ lastBlurb = [ "" , "For more playing instructions see file PLAYING.md."- , "Press SPACE to clear the messages and see the map again."+ , "Press PGUP to return to previous pages or ESC to see the map again." ]- fmt k h = T.justifyRight 72 ' '- $ T.justifyLeft 15 ' ' k- <> T.justifyLeft 48 ' ' h+ pickLeaderDescription =+ [ fmt 16 "0, 1, ... 6" "pick a particular actor as the new leader"+ ]+ casualDescription = "Minimal cheat sheet for casual play"+ fmt n k h = T.justifyRight 72 ' '+ $ T.justifyLeft n ' ' k+ <> T.justifyLeft 48 ' ' h fmts s = " " <> T.justifyLeft 71 ' ' s- minimalText = map fmts minimalBlurb movText = map fmts movBlurb- minCatText = map fmts minCatBlurb+ minimalText = map fmts minimalBlurb+ casualEndText = map fmts casualEndBlurb categoryText = map fmts categoryBlurb lastText = map fmts lastBlurb- keyCaption = fmt "keys" "command" coImage :: K.KM -> [K.KM] coImage k = k : sort [ from- | (from, (_, _, Macro _ [to])) <- bcmdList- , K.mkKM to == k ]+ | (from, (_, cats, Macro _ [to])) <- bcmdList+ , K.mkKM to == k+ , any (`notElem` [CmdDebug, CmdInternal]) cats ] disp k = T.concat $ intersperse " and " $ map K.showKM $ coImage k- keys cat = [ fmt (disp k) h- | (k, (h, cats, _)) <- bcmdList, cat `elem` cats, h /= "" ]- in toSlideshow True- [ [categoryDescription CmdMinimal- <> ". [press SPACE to see all commands]"] ++ [""]- ++ minimalText- ++ [keyCaption] ++ keys CmdMinimal ++ minCatText ++ [moreMsg]- , ["Movement. [press SPACE to advance]"] ++ [""]+ keysN n cat = [ fmt n (disp k) h+ | (k, (h, cats, _)) <- bcmdList, cat `elem` cats, h /= "" ]+ -- TODO: measure the longest key sequence and set the caption automatically+ keyCaptionN n = fmt n "keys" "command"+ keys = keysN 16+ keyCaption = keyCaptionN 16+ in toSlideshow (Just True)+ [ [casualDescription <+> "(1/2). [press SPACE to see more]"] ++ [""] ++ movText ++ [moreMsg]- , [categoryDescription CmdMove <> ". [press SPACE to advance]"] ++ [""]- ++ [keyCaption] ++ keys CmdMove ++ categoryText ++ [moreMsg]+ , [casualDescription <+> "(2/2). [press SPACE to see all commands]"] ++ [""]+ ++ minimalText+ ++ [keyCaption] ++ keys CmdMinimal ++ casualEndText ++ [moreMsg]+ , ["All terrain exploration and alteration commands"+ <> ". [press SPACE to advance]"] ++ [""]+ ++ [keyCaptionN 10] ++ keysN 10 CmdMove ++ categoryText ++ [moreMsg] , [categoryDescription CmdItem <> ". [press SPACE to advance]"] ++ [""]- ++ [keyCaption] ++ keys CmdItem ++ categoryText ++ [moreMsg]+ ++ [keyCaptionN 10] ++ keysN 10 CmdItem ++ categoryText ++ [moreMsg] , [categoryDescription CmdTgt <> ". [press SPACE to advance]"] ++ [""] ++ [keyCaption] ++ keys CmdTgt ++ categoryText ++ [moreMsg] , [categoryDescription CmdAuto <> ". [press SPACE to advance]"] ++ [""] ++ [keyCaption] ++ keys CmdAuto ++ categoryText ++ [moreMsg]- , [categoryDescription CmdMeta <> "."] ++ [""]- ++ [keyCaption] ++ keys CmdMeta ++ lastText+ , [categoryDescription CmdMeta <> ". [press SPACE to advance]"] ++ [""]+ ++ [keyCaption] ++ keys CmdMeta ++ pickLeaderDescription+ ++ categoryText ++ [moreMsg]+ , [categoryDescription CmdMouse+ <> ". [press PGUP to see previous, ESC to cancel]"] ++ [""]+ ++ [keyCaptionN 21] ++ keysN 21 CmdMouse ++ lastText ++ [endMsg] ]
Game/LambdaHack/Client/UI/MonadClientUI.hs view
@@ -9,15 +9,16 @@ -- * Display and key input , ColorMode(..) , promptGetKey, getKeyOverlayCommand, getInitConfirms- , displayFrame, displayDelay, displayFrames, displayActorStart, drawOverlay+ , displayFrame, displayDelay, displayActorStart, drawOverlay -- * Assorted primitives- , stopPlayBack, stopRunning, askConfig, askBinding+ , stopPlayBack, askConfig, askBinding , syncFrames, setFrontAutoYes, tryTakeMVarSescMVar, scoreToSlideshow , getLeaderUI, getArenaUI, viewedLevel , targetDescLeader, targetDescCursor , leaderTgtToPos, leaderTgtAims, cursorToPos ) where +import Control.Applicative import Control.Concurrent import Control.Concurrent.STM import Control.Exception.Assert.Sugar@@ -26,7 +27,6 @@ import Data.Maybe import Data.Monoid import Data.Text (Text)-import qualified Data.Text as T import qualified NLP.Miniutter.English as MU import System.Time @@ -42,7 +42,6 @@ import Game.LambdaHack.Client.UI.KeyBindings import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState-import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Common.Faction import qualified Game.LambdaHack.Common.HighScore as HighScore import Game.LambdaHack.Common.ItemDescription@@ -52,12 +51,14 @@ import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.State+import Game.LambdaHack.Common.Time+import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.ModeKind -- | The information that is constant across a client playing session, -- including many consecutive games in a single session, -- but is completely disregarded and reset when a new playing session starts.--- Auxiliary AI and computer player clients have no @sfs@ nor @sbinding@.+-- This includes a frontend session and keybinding info. data SessionUI = SessionUI { schanF :: !ChanFrontend -- ^ connection with the frontend , sbinding :: !Binding -- ^ binding of keys to commands@@ -65,6 +66,7 @@ , sconfig :: !Config } +-- | The monad that gives the client access to UI operations. class MonadClient m => MonadClientUI m where getsSession :: (SessionUI -> a) -> m a liftIO :: IO a -> m a@@ -83,28 +85,41 @@ promptGetKey :: MonadClientUI m => [K.KM] -> SingleFrame -> m K.KM promptGetKey frontKM frontFr = do- escPressed <- tryTakeMVarSescMVar -- this also clears the ESC-pressed marker+ -- Assume we display the arena when we prompt for a key and possibly+ -- insert a delay and reset cutoff.+ arena <- getArenaUI+ localTime <- getsState $ getLocalTime arena+ -- No delay, because this is before the UI actor acts. Ideally the frame+ -- would not be changed either.+ -- However, set sdisplayed so that there's no extra delay after the actor+ -- acts either, because waiting for the key introduces enough delay.+ -- Or this is running, etc., which we want fast.+ let ageDisp = EM.insert arena localTime+ modifyClient $ \cli -> cli {sdisplayed = ageDisp $ sdisplayed cli}+ escPressed <- tryTakeMVarSescMVar -- this also clears the ESC-pressed marker lastPlayOld <- getsClient slastPlay km <- case lastPlayOld of km : kms | not escPressed && (null frontKM || km `elem` frontKM) -> do- displayFrame True $ Just frontFr+ displayFrame $ Just frontFr -- Sync frames so that ESC doesn't skip frames. syncFrames modifyClient $ \cli -> cli {slastPlay = kms} return km _ -> do- unless (null lastPlayOld) stopPlayBack -- we can't continue playback+ stopPlayBack -- we can't continue playback; wipe out old srunning writeConnFrontend FrontKey{..}- readConnFrontend+ km <- readConnFrontend+ modifyClient $ \cli -> cli {slastKM = km}+ return km (seqCurrent, seqPrevious, k) <- getsClient slastRecord let slastRecord = (km : seqCurrent, seqPrevious, k) modifyClient $ \cli -> cli {slastRecord} return km -- | Display an overlay and wait for a human player command.-getKeyOverlayCommand :: MonadClientUI m => Bool -> Overlay -> m K.KM+getKeyOverlayCommand :: MonadClientUI m => Maybe Bool -> Overlay -> m K.KM getKeyOverlayCommand onBlank overlay = do- frame <- drawOverlay onBlank ColorFull overlay+ frame <- drawOverlay (isJust onBlank) ColorFull overlay promptGetKey [] frame -- | Display a slideshow, awaiting confirmation for each slide except the last.@@ -112,13 +127,10 @@ => ColorMode -> [K.KM] -> Slideshow -> m Bool getInitConfirms dm frontClear slides = do let (onBlank, ovs) = slideshow slides- frontSlides <- drawOverlays onBlank dm ovs- -- The first two cases are optimizations:+ frontFromTop = onBlank+ frontSlides <- drawOverlays (isJust onBlank) dm ovs case frontSlides of [] -> return True- [x] -> do- displayFrame False $ Just x- return True _ -> do writeConnFrontend FrontSlides{..} km <- readConnFrontend@@ -126,48 +138,50 @@ -- block a ping and the ping would not see the ESC. return $! km /= K.escKM -displayFrame :: MonadClientUI m => Bool -> Maybe SingleFrame -> m ()-displayFrame isRunning mf = do+displayFrame :: MonadClientUI m => Maybe SingleFrame -> m ()+displayFrame mf = do let frame = case mf of Nothing -> FrontDelay- Just fr | isRunning -> FrontRunningFrame fr Just fr -> FrontNormalFrame fr writeConnFrontend frame displayDelay :: MonadClientUI m => m ()-displayDelay = writeConnFrontend FrontDelay---- | Push frames or delays to the frame queue.-displayFrames :: MonadClientUI m => Frames -> m ()-displayFrames = mapM_ (displayFrame False)+displayDelay = sequence_ $ replicate 4 $ writeConnFrontend FrontDelay -- | Push frames or delays to the frame queue. Additionally set @sdisplayed@. -- because animations not always happen after @SfxActorStart@ on the leader's -- level (e.g., death can lead to leader change to another level mid-turn, -- and there could be melee and animations on that level at the same moment).+-- Insert delays, so that the animations don't look rushed. displayActorStart :: MonadClientUI m => Actor -> Frames -> m () displayActorStart b frs = do- mapM_ (displayFrame False) frs- let ageDisp displayed = EM.insert (blid b) (btime b) displayed+ timeCutOff <- getsClient $ EM.findWithDefault timeZero (blid b) . sdisplayed+ localTime <- getsState $ getLocalTime (blid b)+ let delta = localTime `timeDeltaToFrom` timeCutOff+ when (delta > Delta timeClip && not (bproj b))+ displayDelay+ let ageDisp = EM.insert (blid b) localTime modifyClient $ \cli -> cli {sdisplayed = ageDisp $ sdisplayed cli}+ mapM_ displayFrame frs -- | Draw the current level with the overlay on top.-drawOverlay :: MonadClientUI m => Bool -> ColorMode -> Overlay -> m SingleFrame+drawOverlay :: MonadClientUI m+ => Bool -> ColorMode -> Overlay -> m SingleFrame drawOverlay sfBlank@True _ sfTop = do let sfLevel = [] sfBottom = [] return $! SingleFrame {..}-drawOverlay sfBlank@False dm sfTop = do+drawOverlay False dm sfTop = do lid <- viewedLevel mleader <- getsClient _sleader tgtPos <- leaderTgtToPos cursorPos <- cursorToPos let anyPos = fromMaybe (Point 0 0) cursorPos- pathFromLeader leader = fmap Just $ getCacheBfsAndPath leader anyPos+ pathFromLeader leader = Just <$> getCacheBfsAndPath leader anyPos bfsmpath <- maybe (return Nothing) pathFromLeader mleader tgtDesc <- maybe (return ("------", Nothing)) targetDescLeader mleader cursorDesc <- targetDescCursor- draw sfBlank dm lid cursorPos tgtPos bfsmpath cursorDesc tgtDesc sfTop+ draw dm lid cursorPos tgtPos bfsmpath cursorDesc tgtDesc sfTop drawOverlays :: MonadClientUI m => Bool -> ColorMode -> [Overlay] -> m [SingleFrame]@@ -183,12 +197,8 @@ { slastPlay = [] , slastRecord = let (seqCurrent, seqPrevious, _) = slastRecord cli in (seqCurrent, seqPrevious, 0)- , swaitTimes = - swaitTimes cli+ , swaitTimes = - abs (swaitTimes cli) }- stopRunning--stopRunning :: MonadClientUI m => m ()-stopRunning = do srunning <- getsClient srunning case srunning of Nothing -> return ()@@ -202,7 +212,7 @@ s <- getState when (memActor runLeader arena s && not (noRunWithMulti fact)) $ modifyClient $ updateLeader runLeader s- modifyClient (\cli -> cli { srunning = Nothing })+ modifyClient (\cli -> cli {srunning = Nothing}) askConfig :: MonadClientUI m => m Config askConfig = getsSession sconfig@@ -215,9 +225,11 @@ syncFrames :: MonadClientUI m => m () syncFrames = do -- Hack.- writeConnFrontend FrontSlides{frontClear=[], frontSlides=[]}+ writeConnFrontend+ FrontSlides{frontClear=[], frontSlides=[], frontFromTop=Nothing} km <- readConnFrontend- assert (km == K.spaceKM) skip+ let !_A = assert (km == K.spaceKM) ()+ return () setFrontAutoYes :: MonadClientUI m => Bool -> m () setFrontAutoYes b = writeConnFrontend $ FrontAutoYes b@@ -239,13 +251,17 @@ -- TODO: we should do this, and make sure we do that after server -- saved the updated score table, and not register, but read from it. -- Otherwise the score is not accurate, e.g., the number of victims.- table <- getsState shigh+ scoreDict <- getsState shigh+ gameModeId <- getsState sgameModeId+ gameMode <- getGameMode time <- getsState stime date <- liftIO getClockTime scurDifficulty <- getsClient scurDifficulty factionD <- getsState sfactionD- loots <- factionLoots fid- let showScore (ntable, pos) = HighScore.highSlideshow ntable pos+ let table = HighScore.getTable gameModeId scoreDict+ gameModeName = mname gameMode+ showScore (ntable, pos) =+ HighScore.highSlideshow ntable pos gameModeName diff | not $ fhasUI $ gplayer fact = difficultyDefault | otherwise = scurDifficulty theirVic (fi, fa) | isAtWar fact fi@@ -258,7 +274,8 @@ (worthMentioning, rScore) = HighScore.register table total time status date diff (fname $ gplayer fact)- ourVictims theirVictims loots+ ourVictims theirVictims+ (fhiCondPoly $ gplayer fact) return $! if worthMentioning then showScore rScore else mempty getLeaderUI :: MonadClientUI m => m ActorId@@ -312,7 +329,7 @@ return (bname b, hpIndicator) Just (TEnemyPos _ lid p _) -> do let hotText = if lid == lidV- then "hot spot" <+> (T.pack . show) p+ then "hot spot" <+> tshow p else "a hot spot on level" <+> tshow (abs $ fromEnum lid) return (hotText, Nothing) Just (TPoint lid p) -> do@@ -321,15 +338,15 @@ then do bag <- getsState $ getCBag (CFloor lid p) case EM.assocs bag of- [] -> return $! "exact spot" <+> (T.pack . show) p+ [] -> return $! "exact spot" <+> tshow p [(iid, kit@(k, _))] -> do localTime <- getsState $ getLocalTime lid itemToF <- itemToFullClient- let (name, stats) = partItem (CFloor lid p) lid localTime (itemToF iid kit)+ let (_, name, stats) = partItem CGround lid localTime (itemToF iid kit) return $! makePhrase $ if k == 1 then [name, stats] -- "a sword" too wordy else [MU.CarWs k name, stats]- _ -> return $! "many items at" <+> (T.pack . show) p+ _ -> return $! "many items at" <+> tshow p else return $! "an exact spot on level" <+> tshow (abs $ fromEnum lid) return (pointedText, Nothing) Just TVector{} ->@@ -338,9 +355,9 @@ Just aid -> do tgtPos <- aidTgtToPos aid lidV target let invalidMsg = "an invalid relative shift"- validMsg p = "shift to" <+> (T.pack . show) p+ validMsg p = "shift to" <+> tshow p return (maybe invalidMsg validMsg tgtPos, Nothing)- Nothing -> return ("cursor location", Nothing)+ Nothing -> return ("crosshair location", Nothing) targetDescLeader :: MonadClientUI m => ActorId -> m (Text, Maybe Text) targetDescLeader leader = do
Game/LambdaHack/Client/UI/MsgClient.hs view
@@ -1,15 +1,14 @@ -- | Client monad for interacting with a human through UI. module Game.LambdaHack.Client.UI.MsgClient ( msgAdd, msgReset, recordHistory- , SlideOrCmd, failWith, failSlides, failSer+ , SlideOrCmd, failWith, failSlides, failSer, failMsg , lookAt, itemOverlay ) where -import Control.Arrow (first)+import Control.Applicative import Control.Exception.Assert.Sugar import Control.Monad import qualified Data.EnumMap.Strict as EM-import qualified Data.IntMap.Strict as IM import Data.Maybe import Data.Monoid import Data.Text (Text)@@ -62,7 +61,8 @@ failWith :: MonadClientUI m => Msg -> m (SlideOrCmd a) failWith msg = do stopPlayBack- assert (not $ T.null msg) $ fmap Left $ promptToSlideshow msg+ let starMsg = "*" <> msg <> "*"+ assert (not $ T.null msg) $ Left <$> promptToSlideshow starMsg failSlides :: MonadClientUI m => Slideshow -> m (SlideOrCmd a) failSlides slides = do@@ -72,6 +72,12 @@ failSer :: MonadClientUI m => ReqFailure -> m (SlideOrCmd a) failSer = failWith . showReqFailure +failMsg :: MonadClientUI m => Msg -> m Slideshow+failMsg msg = do+ stopPlayBack+ let starMsg = "*" <> msg <> "*"+ assert (not $ T.null msg) $ promptToSlideshow starMsg+ -- | Produces a textual description of the terrain and items at an already -- explored position. Mute for unknown positions. -- The detailed variant is for use in the targeting mode.@@ -86,23 +92,24 @@ lookAt detailed tilePrefix canSee pos aid msg = do cops@Kind.COps{cotile=cotile@Kind.Ops{okind}} <- getsState scops itemToF <- itemToFullClient- lidV <- viewedLevel+ b <- getsState $ getActorBody aid+ stgtMode <- getsClient stgtMode+ let lidV = maybe (blid b) tgtLevelId stgtMode lvl <- getLevel lidV localTime <- getsState $ getLocalTime lidV- b <- getsState $ getActorBody aid subject <- partAidLeader aid is <- getsState $ getCBag $ CFloor lidV pos let verb = MU.Text $ if pos == bpos b then "stand on" else if canSee then "notice" else "remember"- let nWs (iid, kit@(k, _)) = partItemWs k (CFloor lidV pos) lidV localTime (itemToF iid kit)+ let nWs (iid, kit@(k, _)) = partItemWs k CGround lidV localTime (itemToF iid kit) isd = case detailed of _ | EM.size is == 0 -> "" _ | EM.size is <= 2 -> makeSentence [ MU.SubjectVerbSg subject verb , MU.WWandW $ map nWs $ EM.assocs is]- True -> "\n"- _ -> "Items here."+-- disabled together with overlay in doLook True -> "\n"+ _ -> makeSentence [MU.Cardinal (EM.size is), "items here"] tile = lvl `at` pos obscured | knownLsecret lvl && tile /= hideTile cops lvl pos = "partially obscured"@@ -121,14 +128,15 @@ -- | Create a list of item names. itemOverlay :: MonadClient m- => Container -> LevelId -> ItemBag -> m Overlay+ => CStore -> LevelId -> ItemBag -> m Overlay itemOverlay c lid bag = do localTime <- getsState $ getLocalTime lid itemToF <- itemToFullClient- (letterSlots, numberSlots) <- getsClient sslots- assert (all (`elem` EM.elems letterSlots ++ IM.elems numberSlots)- (EM.keys bag)- `blame` (c, lid, bag, letterSlots, numberSlots)) skip+ (itemSlots, organSlots) <- getsClient sslots+ let isOrgan = c == COrgan+ lSlots = if isOrgan then organSlots else itemSlots+ let !_A = assert (all (`elem` EM.elems lSlots) (EM.keys bag)+ `blame` (c, lid, bag, lSlots)) () let pr (l, iid) = case EM.lookup iid bag of Nothing -> Nothing@@ -139,7 +147,5 @@ -- symbol = jsymbol $ itemBase itemFull in Just $ makePhrase [ slotLabel l, "-" -- MU.String [symbol] , partItemWs k c lid localTime itemFull ]- <> " "- return $! toOverlay $ mapMaybe pr- $ map (first Left) (EM.assocs letterSlots)- ++ (map (first Right) (IM.assocs numberSlots))+ <> " "+ return $! toOverlay $ mapMaybe pr $ EM.assocs lSlots
Game/LambdaHack/Client/UI/RunClient.hs view
@@ -11,7 +11,7 @@ -- Some things are never ignored, such as: enemies seen, imporant messages -- heard, solid tiles and actors in the way. module Game.LambdaHack.Client.UI.RunClient- ( continueRun, moveRunAid+ ( continueRun ) where import Control.Exception.Assert.Sugar@@ -39,158 +39,110 @@ -- | Continue running in the given direction. continueRun :: MonadClient m- => RunParams -> m (Either Msg (RunParams, RequestAnyAbility))-continueRun paramOld =- case paramOld of- RunParams{ runMembers = []- , runStopMsg = Just stopMsg } -> return $ Left stopMsg- RunParams{ runLeader- , runMembers = r : rs- , runDist = 0- , runStopMsg- , runInitDir = Just dir } ->- if r == runLeader then do- -- Start a many-actor run with distance 1, to prevent changing- -- direction on first turn, if the original direction is blocked.- -- We want our runners to keep formation.- let runDistNew = if null rs then 0 else 1- continueRun paramOld{runDist = runDistNew, runInitDir = Nothing}- else do- runOutcome <- continueRunDir r 0 (Just dir)- case runOutcome of- Left "" -> do -- hack; means that zeroth step OK- runStopOrCmd <- moveRunAid r dir- let runMembersNew = if isJust runStopMsg then rs else rs ++ [r]- paramNew = paramOld {runMembers = runMembersNew}- case runStopOrCmd of- Left stopMsg -> assert `failure` (paramOld, stopMsg)- Right runCmd -> do- s <- getState- modifyClient $ updateLeader r s- return $ Right (paramNew, runCmd)- Left runStopMsgCurrent -> do- let runStopMsgNew = fromMaybe runStopMsgCurrent runStopMsg- paramNew = paramOld { runMembers = rs- , runStopMsg = Just runStopMsgNew }- continueRun paramNew- _ -> assert `failure` (paramOld, runOutcome)- RunParams{ runLeader- , runMembers = r : rs- , runDist- , runStopMsg- , runInitDir = Nothing } -> do- let runDistNew = if r == runLeader then runDist + 1 else runDist- mdirOrRunStopMsgCurrent <- continueRunDir r runDistNew Nothing+ => LevelId -> RunParams+ -> m (Either Msg RequestAnyAbility)+continueRun arena paramOld = case paramOld of+ RunParams{ runMembers = []+ , runStopMsg = Just stopMsg } -> return $ Left stopMsg+ RunParams{ runMembers = []+ , runStopMsg = Nothing } ->+ return $ Left "selected actors no longer there"+ RunParams{ runLeader+ , runMembers = r : rs+ , runInitial+ , runStopMsg } -> do+ -- If runInitial and r == runLeader, it means the leader moves+ -- again, after all other members, in step 0,+ -- so we call continueRunDir with True to change direction once+ -- and then unset runInitial.+ let runInitialNew = runInitial && r /= runLeader+ paramIni = paramOld {runInitial = runInitialNew}+ onLevel <- getsState $ memActor r arena+ onLevelLeader <- getsState $ memActor runLeader arena+ if not onLevel then do+ let paramNew = paramIni {runMembers = rs }+ continueRun arena paramNew+ else if not onLevelLeader then do+ let paramNew = paramIni {runLeader = r}+ continueRun arena paramNew+ else do+ mdirOrRunStopMsgCurrent <- continueRunDir paramOld let runStopMsgCurrent = either Just (const Nothing) mdirOrRunStopMsgCurrent runStopMsgNew = runStopMsg `mplus` runStopMsgCurrent -- We check @runStopMsgNew@, because even if the current actor -- runs OK, we want to stop soon if some others had to stop. runMembersNew = if isJust runStopMsgNew then rs else rs ++ [r]- paramNew = paramOld { runMembers = runMembersNew- , runDist = runDistNew+ paramNew = paramIni { runMembers = runMembersNew , runStopMsg = runStopMsgNew } case mdirOrRunStopMsgCurrent of- Left _ -> continueRun paramNew -- run all undisturbed; only one time+ Left _ -> continueRun arena paramNew+ -- run all others undisturbed; one time Right dir -> do s <- getState modifyClient $ updateLeader r s- return $ Right (paramNew, RequestAnyAbility $ ReqMove dir)+ modifyClient $ \cli -> cli {srunning = Just paramNew}+ return $ Right $ RequestAnyAbility $ ReqMove dir -- The potential invisible actor is hit. War is started without asking.- _ -> assert `failure` paramOld --- | Actor moves or searches or alters. No visible actor at the position.-moveRunAid :: MonadClient m- => ActorId -> Vector -> m (Either Msg RequestAnyAbility)-moveRunAid source dir = do- cops@Kind.COps{cotile} <- getsState scops- sb <- getsState $ getActorBody source- let lid = blid sb- lvl <- getLevel lid- let spos = bpos sb -- source position- tpos = spos `shift` dir -- target position- t = lvl `at` tpos- runStopOrCmd =- -- Movement requires full access.- if accessible cops lvl spos tpos then- -- The potential invisible actor is hit. War started without asking.- Right $ RequestAnyAbility $ ReqMove dir- -- No access, so search and/or alter the tile. Non-walkability is- -- not implied by the lack of access.- else if not (Tile.isWalkable cotile t)- && (not (knownLsecret lvl)- || (isSecretPos lvl tpos -- possible secrets here- && (Tile.isSuspect cotile t -- not yet searched- || Tile.hideAs cotile t /= t)) -- search again- || Tile.isOpenable cotile t- || Tile.isClosable cotile t- || Tile.isChangeable cotile t) then- if EM.member tpos $ lfloor lvl then- Left $ showReqFailure AlterBlockItem- else- Right $ RequestAnyAbility $ ReqAlter tpos Nothing- -- We don't use MoveSer, because we don't hit invisible actors.- -- The potential invisible actor, e.g., in a wall or in- -- an inaccessible doorway, is made known, taking a turn.- -- If server performed an attack for free- -- on the invisible actor anyway, the player (or AI)- -- would be tempted to repeatedly hit random walls- -- in hopes of killing a monster lurking within.- -- If the action had a cost, misclicks would incur the cost, too.- -- Right now the player may repeatedly alter tiles trying to learn- -- about invisible pass-wall actors, but when an actor detected,- -- it costs a turn and does not harm the invisible actors,- -- so it's not so tempting.- -- Ignore a known boring, not accessible tile.- else Left "never mind"- return $! runStopOrCmd- -- | This function implements the actual logic of running. It checks if we -- have to stop running because something interesting cropped up, -- it ajusts the direction given by the vector if we reached -- a corridor's corner (we never change direction except in corridors) -- and it increments the counter of traversed tiles.+--+-- Note that while goto-cursor commands ignore items on the way,+-- here we stop wnenever we touch an item. Running is more cautious+-- to compensate that the player cannot specify the end-point of running.+-- It's also more suited to open, already explored terrain. Goto-cursor+-- works better with unknown terrain, e.g., it stops whenever an item+-- is spotted, but then ignores the item, leaving it to the player+-- to mark the item position as a goal of the next goto. continueRunDir :: MonadClient m- => ActorId -> Int -> Maybe Vector -> m (Either Msg Vector)-continueRunDir aid distLast mdir = do- sreport <- getsClient sreport -- TODO: check the message before it goes into history- let boringMsgs = map BS.pack [ "You hear a distant"- , "reveals that the" ]- boring repLine = any (`BS.isInfixOf` repLine) boringMsgs- -- TODO: use a regexp from the UI config instead- msgShown = isJust $ findInReport (not . boring) sreport- if msgShown then return $ Left "message shown"- else do- let maxDistance = 20- cops@Kind.COps{cotile} <- getsState scops- body <- getsState $ getActorBody aid- let lid = blid body- lvl <- getLevel lid- let posHere = bpos body- posLast = boldpos body- dirLast = posHere `vectorToFrom` posLast- dir = fromMaybe dirLast mdir- posThere = posHere `shift` dir- actorsThere <- getsState $ posToActors posThere lid- let openableLast = Tile.isOpenable cotile (lvl `at` (posHere `shift` dir))- check- | not $ null actorsThere = return $ Left "actor in the way"- -- don't displace actors, except with leader in step 1- | distLast >= maxDistance =- return $ Left $ "reached max run distance" <+> tshow maxDistance- | accessibleDir cops lvl posHere dir =- if distLast == 0- then return $ Left "" -- hack; means that zeroth step OK- else checkAndRun aid dir- | distLast /= 1 = return $ Left "blocked"- -- don't change direction, except in step 1- | openableLast = return $ Left "blocked by a closed door"- -- the player may prefer to open the door- | otherwise =- -- Assume turning is permitted, because this is the start- -- of the run, so the situation is mostly known to the player- tryTurning aid- check+ => RunParams -> m (Either Msg Vector)+continueRunDir params = case params of+ RunParams{ runMembers = [] } -> assert `failure` params+ RunParams{ runLeader+ , runMembers = aid : _+ , runInitial } -> do+ sreport <- getsClient sreport -- TODO: check the message before it goes into history+ let boringMsgs = map BS.pack [ "You hear a distant"+ , "reveals that the" ]+ boring repLine = any (`BS.isInfixOf` repLine) boringMsgs+ -- TODO: use a regexp from the UI config instead+ -- or have symbolic messages and pattern-match+ msgShown = isJust $ findInReport (not . boring) sreport+ if msgShown then return $ Left "message shown"+ else do+ cops@Kind.COps{cotile} <- getsState scops+ rbody <- getsState $ getActorBody runLeader+ let rposHere = bpos rbody+ rposLast = boldpos rbody+ -- Match run-leader dir, because we want runners to keep formation.+ dir = rposHere `vectorToFrom` rposLast+ body <- getsState $ getActorBody aid+ let lid = blid body+ lvl <- getLevel lid+ let posHere = bpos body+ posThere = posHere `shift` dir+ actorsThere <- getsState $ posToActors posThere lid+ let openableLast = Tile.isOpenable cotile (lvl `at` (posHere `shift` dir))+ check+ | not $ null actorsThere = return $ Left "actor in the way"+ -- don't displace actors, except with leader in step 0+ | accessibleDir cops lvl posHere dir =+ if runInitial && aid /= runLeader+ then return $ Right dir -- zeroth step always OK+ else checkAndRun aid dir+ | not (runInitial && aid == runLeader) = return $ Left "blocked"+ -- don't change direction, except in step 1 and by run-leader+ | openableLast = return $ Left "blocked by a closed door"+ -- the player may prefer to open the door+ | otherwise =+ -- Assume turning is permitted, because this is the start+ -- of the run, so the situation is mostly known to the player+ tryTurning aid+ check tryTurning :: MonadClient m => ActorId -> m (Either Msg Vector)@@ -274,7 +226,8 @@ `notElem` map posHasItems rightPsLast check | not $ null actorsThere = return $ Left "actor in the way"- -- Actor in possibly another direction tnat original.+ -- Actor in possibly another direction tnan original.+ -- (e.g., called from @tryTurning@). | terrainChangeLeft = return $ Left "terrain change on the left" | terrainChangeRight = return $ Left "terrain change on the right" | itemChangeLeft = return $ Left "item change on the left"
Game/LambdaHack/Client/UI/StartupFrontendClient.hs view
@@ -19,20 +19,22 @@ import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.State --- | Wire together game content, the main loop of game clients,+-- | Wire together game content, the main loops of game clients, -- the main game loop assigned to this frontend (possibly containing -- the server loop, if the whole game runs in one process), -- UI config and the definitions of game commands. srtFrontend :: (DebugModeCli -> SessionUI -> State -> StateClient -> chanServerUI- -> IO ())+ -> IO ()) -- ^ UI main loop -> (DebugModeCli -> SessionUI -> State -> StateClient -> chanServerAI- -> IO ())- -> KeyKind -> Kind.COps -> DebugModeCli+ -> IO ()) -- ^ AI main loop+ -> KeyKind -- ^ key and command content+ -> Kind.COps -- ^ game content+ -> DebugModeCli -- ^ client debug parameters -> ((FactionId -> chanServerUI -> IO ()) -> (FactionId -> chanServerAI -> IO ())- -> IO ())+ -> IO ()) -- ^ frontend main loop -> IO () srtFrontend executorUI executorAI copsClient cops sdebugCli exeServer = do
Game/LambdaHack/Client/UI/WidgetClient.hs view
@@ -1,21 +1,27 @@ -- | A set of widgets for UI clients. module Game.LambdaHack.Client.UI.WidgetClient- ( displayMore, displayYesNo, displayChoiceUI, displayPush, displayPushIfLid+ ( displayMore, displayYesNo, displayChoiceUI, displayPush, describeMainKeys , promptToSlideshow, overlayToSlideshow, overlayToBlankSlideshow , animate, fadeOutOrIn ) where -import Control.Monad+import Control.Applicative import qualified Data.EnumMap.Strict as EM+import qualified Data.Map.Strict as M import Data.Maybe import Data.Monoid+import qualified Data.Text as T import Game.LambdaHack.Client.BfsClient import qualified Game.LambdaHack.Client.Key as K import Game.LambdaHack.Client.MonadClient hiding (liftIO) import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.Animation+import Game.LambdaHack.Client.UI.Config+import Game.LambdaHack.Client.UI.Content.KeyKind import Game.LambdaHack.Client.UI.DrawClient+import Game.LambdaHack.Client.UI.HumanCmd+import Game.LambdaHack.Client.UI.KeyBindings import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction@@ -28,8 +34,8 @@ -- | A yes-no confirmation. getYesNo :: MonadClientUI m => SingleFrame -> m Bool getYesNo frame = do- let keys = [ K.KM {key=K.Char 'y', modifier=K.NoModifier}- , K.KM {key=K.Char 'n', modifier=K.NoModifier}+ let keys = [ K.toKM K.NoModifier (K.Char 'y')+ , K.toKM K.NoModifier (K.Char 'n') , K.escKM ] K.KM {key} <- promptGetKey keys frame@@ -43,7 +49,7 @@ displayMore dm prompt = do slides <- promptToSlideshow $ prompt <+> moreMsg -- Two frames drawn total (unless 'prompt' very long).- getInitConfirms dm [] $ slides <> toSlideshow False [[]]+ getInitConfirms dm [] $ slides <> toSlideshow Nothing [[]] -- | Print a yes/no question and return the player's answer. Use black -- and white colours to turn player's attention to the choice.@@ -60,43 +66,87 @@ displayChoiceUI :: MonadClientUI m => Msg -> Overlay -> [K.KM] -> m (Either Slideshow K.KM) displayChoiceUI prompt ov keys = do- (_, ovs) <- fmap slideshow $ overlayToSlideshow (prompt <> ", ESC]") ov- let legalKeys = [K.spaceKM, K.escKM]- ++ keys- loop [] = fmap Left $ promptToSlideshow "never mind"- loop (x : xs) = do- frame <- drawOverlay False ColorFull x- km@K.KM {..} <- promptGetKey legalKeys frame- case key of- K.Esc -> fmap Left $ promptToSlideshow "never mind"- K.Space -> loop xs- _ -> return $ Right km- loop ovs+ (_, ovs) <- slideshow <$> overlayToSlideshow (prompt <> ", ESC]") ov+ let extraKeys = [K.spaceKM, K.escKM, K.pgupKM, K.pgdnKM]+ legalKeys = keys ++ extraKeys+ loop frs srf =+ case frs of+ [] -> Left <$> promptToSlideshow "*never mind*"+ x : xs -> do+ frame <- drawOverlay False ColorFull x+ km@K.KM{..} <- promptGetKey legalKeys frame+ case key of+ _ | km `elem` keys -> return $ Right km -- km can be PgUp, etc.+ K.Esc -> Left <$> promptToSlideshow "*never mind*"+ K.PgUp -> case srf of+ [] -> loop frs srf+ y : ys -> loop (y : frs) ys+ K.Space -> case xs of+ [] -> Left <$> promptToSlideshow "*never mind*"+ _ -> loop xs (x : srf)+ _ -> case xs of -- K.PgDn and any other permitted key+ [] -> loop frs srf+ _ -> loop xs (x : srf)+ loop ovs [] -- TODO: if more slides, don't take head, but do as in getInitConfirms, -- but then we have to clear the messages or they get redisplayed -- each time screen is refreshed. -- | Push the frame depicting the current level to the frame queue. -- Only one screenful of the report is shown, the rest is ignored.-displayPush :: MonadClientUI m => m ()-displayPush = do- side <- getsClient sside- fact <- getsState $ (EM.! side) . sfactionD- sls <- promptToSlideshow ""+displayPush :: MonadClientUI m => Msg -> m ()+displayPush prompt = do+ sls <- promptToSlideshow prompt let slide = head . snd $ slideshow sls- underAI = isAIFact fact frame <- drawOverlay False ColorFull slide- -- Visually speed up (by remving all empty frames) the show of the sequence- -- of the move frames if the player is running.- srunning <- getsClient srunning- lastPlay <- getsClient slastPlay- displayFrame (isJust srunning || not (null lastPlay) || underAI)- (Just frame)+ displayFrame (Just frame) -displayPushIfLid :: MonadClientUI m => LevelId -> m ()-displayPushIfLid lid = do- arena <- getArenaUI- when (arena == lid) displayPush+describeMainKeys :: MonadClientUI m => m Msg+describeMainKeys = do+ side <- getsClient sside+ fact <- getsState $ (EM.! side) . sfactionD+ let underAI = isAIFact fact+ stgtMode <- getsClient stgtMode+ Binding{brevMap} <- askBinding+ Config{configVi, configLaptop} <- askConfig+ cursor <- getsClient scursor+ let kmLeftButtonPress =+ M.findWithDefault (K.toKM K.NoModifier K.LeftButtonPress)+ macroLeftButtonPress brevMap+ kmEscape =+ M.findWithDefault (K.toKM K.NoModifier K.Esc) Cancel brevMap+ kmCtrlx =+ M.findWithDefault (K.toKM K.Control (K.KP 'x')) GameExit brevMap+ kmRightButtonPress =+ M.findWithDefault (K.toKM K.NoModifier K.RightButtonPress)+ TgtPointerEnemy brevMap+ kmReturn =+ M.findWithDefault (K.toKM K.NoModifier K.Return) Accept brevMap+ moveKeys | configVi = "hjklyubn, "+ | configLaptop = "uk8o79jl, "+ | otherwise = ""+ tgtKind = case cursor of+ TEnemy _ True -> "at actor"+ TEnemy _ False -> "at enemy"+ TEnemyPos _ _ _ True -> "at actor"+ TEnemyPos _ _ _ False -> "at enemy"+ TPoint{} -> "at position"+ TVector{} -> "with a vector"+ keys | underAI = ""+ | isNothing stgtMode =+ "Explore with keypad or keys or mouse: ["+ <> moveKeys+ <> T.intercalate ", "+ (map K.showKM [kmLeftButtonPress, kmCtrlx, kmEscape])+ <> "]"+ | otherwise =+ "Aim" <+> tgtKind <+> "with keypad or keys or mouse: ["+ <> moveKeys+ <> T.intercalate ", "+ (map K.showKM [kmRightButtonPress, kmReturn, kmEscape])+ <> "]"+ report <- getsClient sreport+ return $! if nullReport report then keys else "" -- | The prompt is shown after the current message, but not added to history. -- This is useful, e.g., in targeting mode, not to spam history.@@ -114,7 +164,7 @@ Level{lxsize, lysize} <- getLevel lid -- TODO: screen length or viewLevel sreport <- getsClient sreport let msg = splitReport lxsize (prependMsg promptAI (addMsg sreport prompt))- return $! splitOverlay False (lysize + 1) msg overlay+ return $! splitOverlay Nothing (lysize + 1) msg overlay msgPromptAI :: MonadClientUI m => m Msg msgPromptAI = do@@ -123,11 +173,13 @@ let underAI = isAIFact fact return $! if underAI then "[press ESC for Main Menu]" else "" -overlayToBlankSlideshow :: MonadClientUI m => Msg -> Overlay -> m Slideshow-overlayToBlankSlideshow prompt overlay = do+overlayToBlankSlideshow :: MonadClientUI m+ => Bool -> Msg -> Overlay -> m Slideshow+overlayToBlankSlideshow startAtTop prompt overlay = do lid <- getArenaUI Level{lysize} <- getLevel lid -- TODO: screen length or viewLevel- return $! splitOverlay True (lysize + 3) (toOverlay [prompt]) overlay+ return $! splitOverlay (Just startAtTop) (lysize + 3)+ (toOverlay [prompt]) overlay -- TODO: restrict the animation to 'per' before drawing. -- | Render animations on top of the current screen frame.@@ -139,7 +191,7 @@ tgtPos <- leaderTgtToPos cursorPos <- cursorToPos let anyPos = fromMaybe (Point 0 0) cursorPos- pathFromLeader leader = fmap Just $ getCacheBfsAndPath leader anyPos+ pathFromLeader leader = Just <$> getCacheBfsAndPath leader anyPos bfsmpath <- maybe (return Nothing) pathFromLeader mleader tgtDesc <- maybe (return ("------", Nothing)) targetDescLeader mleader cursorDesc <- targetDescCursor@@ -147,7 +199,7 @@ let over = renderReport (prependMsg promptAI sreport) topLineOnly = truncateToOverlay over basicFrame <-- draw False ColorFull arena cursorPos tgtPos+ draw ColorFull arena cursorPos tgtPos bfsmpath cursorDesc tgtDesc topLineOnly snoAnim <- getsClient $ snoAnim . sdebugCli return $! if fromMaybe False snoAnim@@ -161,4 +213,4 @@ Level{lxsize, lysize} <- getLevel lid animMap <- rndToAction $ fadeout out topRight 2 lxsize lysize animFrs <- animate lid animMap- displayFrames animFrs+ mapM_ displayFrame animFrs
Game/LambdaHack/Common/Ability.hs view
@@ -2,7 +2,7 @@ -- | AI strategy abilities. module Game.LambdaHack.Common.Ability ( Ability(..), Skills- , zeroSkills, unitSkills, addSkills, maxSkills, scaleSkills+ , zeroSkills, unitSkills, addSkills, scaleSkills ) where import Data.Binary@@ -27,7 +27,7 @@ type Skills = EM.EnumMap Ability Int zeroSkills :: Skills-zeroSkills = EM.fromDistinctAscList $ zip [minBound..maxBound] (repeat 0)+zeroSkills = EM.empty unitSkills :: Skills unitSkills = EM.fromDistinctAscList $ zip [minBound..maxBound] (repeat 1)@@ -35,9 +35,6 @@ addSkills :: Skills -> Skills -> Skills addSkills = EM.unionWith (+) -maxSkills :: Skills -> Skills -> Skills-maxSkills = EM.unionWith max- scaleSkills :: Int -> Skills -> Skills scaleSkills n = EM.map (n *) @@ -49,8 +46,8 @@ show AbWait = "wait" show AbMoveItem = "manage items" show AbProject = "fling"- show AbApply = "activate"- show AbTrigger = "trigger tile"+ show AbApply = "apply"+ show AbTrigger = "trigger floor" instance Binary Ability where put = putWord8 . toEnum . fromEnum
Game/LambdaHack/Common/Actor.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-} -- | Actors in the game: heroes, monsters, etc. No operation in this module -- involves the 'State' or 'Action' type. module Game.LambdaHack.Common.Actor@@ -9,10 +8,10 @@ , deltaSerious, deltaMild, xM, minusM, minusTwoM, oneM , bspeed, actorTemplate, timeShiftFromSpeed, braced, waitedLastTurn , actorDying, actorNewBorn, unoccupied- , hpTooLow, calmEnough, calmEnough10, hpEnough, hpEnough10+ , hpTooLow, hpHuge, calmEnough, calmEnough10, hpEnough, hpEnough10 -- * Assorted , ActorDict, smellTimeout, checkAdjacent- , mapActorItems_, ppContainer, ppCStore, verbCStore+ , keySelected, ppContainer, ppCStore, ppCStoreIn, verbCStore ) where import Control.Exception.Assert.Sugar@@ -27,6 +26,7 @@ import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.ItemStrongest import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.Random import Game.LambdaHack.Common.Time@@ -39,35 +39,42 @@ -- to the original value from @ActorKind@ over time. E.g., HP. data Actor = Actor { -- The trunk of the actor's body (present also in @borgan@ or @beqp@)- btrunk :: !ItemId+ btrunk :: !ItemId+ -- Presentation- , bsymbol :: !Char -- ^ individual map symbol- , bname :: !Text -- ^ individual name- , bpronoun :: !Text -- ^ individual pronoun- , bcolor :: !Color.Color -- ^ individual map color+ , bsymbol :: !Char -- ^ individual map symbol+ , bname :: !Text -- ^ individual name+ , bpronoun :: !Text -- ^ individual pronoun+ , bcolor :: !Color.Color -- ^ individual map color+ -- Resources- , btime :: !Time -- ^ absolute time of next action- , bhp :: !Int64 -- ^ current hit points * 1M- , bhpDelta :: !ResDelta -- ^ HP delta this turn * 1M- , bcalm :: !Int64 -- ^ current calm * 1M- , bcalmDelta :: !ResDelta -- ^ calm delta this turn * 1M+ , btime :: !Time -- ^ absolute time of next action+ , bhp :: !Int64 -- ^ current hit points * 1M+ , bhpDelta :: !ResDelta -- ^ HP delta this turn * 1M+ , bcalm :: !Int64 -- ^ current calm * 1M+ , bcalmDelta :: !ResDelta -- ^ calm delta this turn * 1M+ -- Location- , bpos :: !Point -- ^ current position- , boldpos :: !Point -- ^ previous position- , blid :: !LevelId -- ^ current level- , boldlid :: !LevelId -- ^ previous level- , bfid :: !FactionId -- ^ faction the actor belongs to- , boldfid :: !FactionId -- ^ previous faction of the actor- , btrajectory :: !(Maybe ([Vector], Speed)) -- ^ trajectory the actor must- -- travel and his travel speed+ , bpos :: !Point -- ^ current position+ , boldpos :: !Point -- ^ previous position+ , blid :: !LevelId -- ^ current level+ , boldlid :: !LevelId -- ^ previous level+ , bfid :: !FactionId -- ^ faction the actor currently belongs to+ , bfidImpressed :: !FactionId -- ^ the faction actor is attracted to+ , bfidOriginal :: !FactionId -- ^ the original faction of the actor+ , btrajectory :: !(Maybe ([Vector], Speed))+ -- ^ trajectory the actor must+ -- travel and his travel speed+ -- Items- , borgan :: !ItemBag -- ^ organs- , beqp :: !ItemBag -- ^ personal equipment- , binv :: !ItemBag -- ^ personal inventory+ , borgan :: !ItemBag -- ^ organs+ , beqp :: !ItemBag -- ^ personal equipment+ , binv :: !ItemBag -- ^ personal inventory+ -- Assorted- , bwait :: !Bool -- ^ is the actor waiting right now?- , bproj :: !Bool -- ^ is a projectile? (shorthand only,- -- this can be deduced from bkind)+ , bwait :: !Bool -- ^ is the actor waiting right now?+ , bproj :: !Bool -- ^ is a projectile? (shorthand only,+ -- this can be deduced from bkind) } deriving (Show, Eq) @@ -98,13 +105,16 @@ -- which monster is generated. How many and which monsters are generated -- will also depend on the cave kind used to build the level. monsterGenChance :: AbsDepth -> AbsDepth -> Int -> Int -> Rnd Bool-monsterGenChance (AbsDepth n) (AbsDepth depth) numMonsters actorCoeff =- assert (depth > 0)- -- Mimics @castDice@. On level 1, First 2 monsters appear fast.- $ let scaledDepth = 5 * n `div` depth+monsterGenChance _ _ _ 0 = return False+monsterGenChance (AbsDepth n) (AbsDepth totalDepth) lvlSpawned actorCoeff =+ assert (totalDepth > 0 && n > 0)+ -- Mimics @castDice@. On level 5/10, first 6 monsters appear fast.+ $ let scaledDepth = n * 10 `div` totalDepth+ -- Heroes have to endure two lvl-sized waves of spawners for each level.+ numSpawnedCoeff = lvlSpawned `div` 2 in chance $ 1%(fromIntegral- $ (10 * actorCoeff * (numMonsters - scaledDepth))- `max` actorCoeff)+ ((actorCoeff * (numSpawnedCoeff - scaledDepth))+ `max` 1)) -- | The part of speech describing the actor. partActor :: Actor -> MU.Part@@ -130,7 +140,8 @@ binv = EM.empty borgan = EM.empty bwait = False- boldfid = bfid+ bfidImpressed = bfid+ bfidOriginal = bfid bhpDelta = ResDelta 0 0 bcalmDelta = ResDelta 0 0 bproj = False@@ -152,43 +163,44 @@ -- | Whether an actor is braced for combat this clip. braced :: Actor -> Bool-braced b = bwait b+braced = bwait -- | The actor waited last turn. waitedLastTurn :: Actor -> Bool-waitedLastTurn b = bwait b+waitedLastTurn = bwait actorDying :: Actor -> Bool-actorDying b = if bproj b- then bhp b < 0- || maybe True (null . fst) (btrajectory b)- else bhp b <= 0+actorDying b = bhp b <= 0+ || bproj b && maybe True (null . fst) (btrajectory b) actorNewBorn :: Actor -> Bool actorNewBorn b = boldpos b == Point 0 0 && not (waitedLastTurn b)- && not (btime b < timeTurn)+ && btime b >= timeTurn hpTooLow :: Actor -> [ItemFull] -> Bool hpTooLow b activeItems = let maxHP = sumSlotNoFilter IK.EqpSlotAddMaxHP activeItems- in bhp b <= oneM || 5 * bhp b < xM maxHP && bhp b < xM 10+ in bhp b <= oneM || 5 * bhp b < xM maxHP && bhp b <= xM 10 +hpHuge :: Actor -> Bool+hpHuge b = bhp b > xM 30+ calmEnough :: Actor -> [ItemFull] -> Bool calmEnough b activeItems = let calmMax = max 1 $ sumSlotNoFilter IK.EqpSlotAddMaxCalm activeItems in 2 * xM calmMax <= 3 * bcalm b calmEnough10 :: Actor -> [ItemFull] -> Bool-calmEnough10 b activeItems = calmEnough b activeItems && bcalm b >= xM 10+calmEnough10 b activeItems = calmEnough b activeItems && bcalm b > xM 10 hpEnough :: Actor -> [ItemFull] -> Bool hpEnough b activeItems = let hpMax = max 1 $ sumSlotNoFilter IK.EqpSlotAddMaxHP activeItems- in 2 * xM hpMax <= 3 * bhp b+ in xM hpMax <= 3 * bhp b hpEnough10 :: Actor -> [ItemFull] -> Bool-hpEnough10 b activeItems = hpEnough b activeItems && bhp b >= xM 10+hpEnough10 b activeItems = hpEnough b activeItems && bhp b > xM 10 -- | Checks for the presence of actors in a position. -- Does not check if the tile is walkable.@@ -205,24 +217,26 @@ checkAdjacent :: Actor -> Actor -> Bool checkAdjacent sb tb = blid sb == blid tb && adjacent (bpos sb) (bpos tb) -mapActorItems_ :: Monad m => (ItemId -> ItemQuant -> m a) -> Actor -> m ()-mapActorItems_ f Actor{binv, beqp, borgan} = do- let is = EM.assocs beqp ++ EM.assocs binv ++ EM.assocs borgan- mapM_ (uncurry f) is+keySelected :: (ActorId, Actor) -> (Bool, Bool, Char, Color.Color, ActorId)+keySelected (aid, Actor{bsymbol, bcolor, bhp}) =+ (bhp > 0, bsymbol /= '@', bsymbol, bcolor, aid) ppContainer :: Container -> Text ppContainer CFloor{} = "nearby" ppContainer CEmbed{} = "embedded nearby"-ppContainer (CActor _ cstore) = ppCStore cstore-ppContainer CTrunk{} = "in our possession"+ppContainer (CActor _ cstore) = ppCStoreIn cstore+ppContainer c@CTrunk{} = assert `failure` c -ppCStore :: CStore -> Text-ppCStore CGround = "on the ground"-ppCStore COrgan = "among organs"-ppCStore CEqp = "in equipment"-ppCStore CInv = "in inventory"-ppCStore CSha = "in shared stash"+ppCStore :: CStore -> (Text, Text)+ppCStore CGround = ("on", "the ground")+ppCStore COrgan = ("among", "organs")+ppCStore CEqp = ("in", "equipment")+ppCStore CInv = ("in", "pack")+ppCStore CSha = ("in", "shared stash") +ppCStoreIn :: CStore -> Text+ppCStoreIn c = let (tIn, t) = ppCStore c in tIn <+> t+ verbCStore :: CStore -> Text verbCStore CGround = "drop" verbCStore COrgan = "implant"@@ -252,7 +266,8 @@ put btime put bwait put bfid- put boldfid+ put bfidImpressed+ put bfidOriginal put bproj get = do btrunk <- get@@ -275,7 +290,8 @@ btime <- get bwait <- get bfid <- get- boldfid <- get+ bfidImpressed <- get+ bfidOriginal <- get bproj <- get return $! Actor{..}
Game/LambdaHack/Common/ActorState.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE TupleSections #-} -- | Operations on the 'Actor' type that need the 'State' type, -- but not the 'Action' type. -- TODO: Document an export list after it's rewritten according to #17.@@ -6,25 +7,29 @@ , actorAssocsLvl, actorAssocs, actorList , actorRegularAssocsLvl, actorRegularAssocs, actorRegularList , bagAssocs, bagAssocsK, calculateTotal- , mergeItemQuant, sharedAllOwned, sharedAllOwnedFid- , getCBag, getActorBag, getBodyActorBag, getActorAssocs- , nearbyFreePoints, whereTo, getCarriedAssocs+ , mergeItemQuant, sharedAllOwnedFid, findIid+ , getCBag, getActorBag, getBodyActorBag, mapActorItems_, getActorAssocs+ , nearbyFreePoints, whereTo, getCarriedAssocs, getCarriedIidCStore , posToActors, posToActor, getItemBody, memActor, getActorBody , tryFindHeroK, getLocalTime, itemPrice, regenCalmDelta- , actorInAmbient, actorSkills, maxActorSkills, dispEnemy- , fullAssocs, itemToFull, goesIntoInv, eqpOverfull- , storeFromC, lidFromC, aidFromC+ , actorInAmbient, actorSkills, dispEnemy+ , fullAssocs, itemToFull, goesIntoInv, goesIntoSha, eqpOverfull+ , storeFromC, lidFromC, aidFromC, hasCharge+ , strongestMelee, isMelee, isMeleeEqp ) where +import Control.Applicative import Control.Exception.Assert.Sugar import qualified Data.Char as Char import qualified Data.EnumMap.Strict as EM import Data.Int (Int64) import Data.List import Data.Maybe+import qualified Data.Ord as Ord import qualified Game.LambdaHack.Common.Ability as Ability import Game.LambdaHack.Common.Actor+import qualified Game.LambdaHack.Common.Dice as Dice import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.ItemStrongest@@ -86,8 +91,8 @@ getItemBody :: ItemId -> State -> Item getItemBody iid s =- fromMaybe (assert `failure` "item body not found"- `twith` (iid, s)) $ EM.lookup iid $ sitemD s+ let assFail = assert `failure` "item body not found" `twith` (iid, s)+ in EM.findWithDefault assFail iid $ sitemD s bagAssocs :: State -> ItemBag -> [(ItemId, Item)] bagAssocs s bag =@@ -133,7 +138,7 @@ -- | Calculate loot's worth for a faction of a given actor. calculateTotal :: Actor -> State -> (ItemBag, Int) calculateTotal body s =- let bag = sharedAllOwned False body s+ let bag = sharedAllOwned body s items = map (\(iid, (k, _)) -> (getItemBody iid s, k)) $ EM.assocs bag in (bag, sum $ map itemPrice items) @@ -152,27 +157,33 @@ in EM.unionsWith mergeItemQuant $ map beqp $ if null bs then [body] else bs -sharedOrgan :: Actor -> State -> ItemBag-sharedOrgan body s =- let bs = fidActorNotProjList (bfid body) s- in EM.unionsWith mergeItemQuant- $ map borgan $ if null bs then [body] else bs--sharedAllOwned :: Bool -> Actor -> State -> ItemBag-sharedAllOwned organs body s =+sharedAllOwned :: Actor -> State -> ItemBag+sharedAllOwned body s = let shaBag = gsha $ sfactionD s EM.! bfid body in EM.unionsWith mergeItemQuant $ [sharedEqp body s, sharedInv body s, shaBag]- ++ [sharedOrgan body s | organs] sharedAllOwnedFid :: Bool -> FactionId -> State -> ItemBag-sharedAllOwnedFid organs fid s =+sharedAllOwnedFid onlyOrgans fid s = let shaBag = gsha $ sfactionD s EM.! fid bs = fidActorNotProjList fid s in EM.unionsWith mergeItemQuant- $ map binv bs ++ map beqp bs ++ [shaBag]- ++ if organs then map borgan bs else []+ $ if onlyOrgans+ then map borgan bs+ else map binv bs ++ map beqp bs ++ [shaBag] +findIid :: ActorId -> FactionId -> ItemId -> State -> [(Actor, CStore)]+findIid leader fid iid s =+ let actors = fidActorNotProjAssocs fid s+ itemsOfActor (aid, b) =+ let itemsOfCStore store =+ let bag = getBodyActorBag b store s+ in map (\iid2 -> (iid2, (b, store))) (EM.keys bag)+ stores = [CInv, CEqp] ++ [CSha | aid == leader]+ in concatMap itemsOfCStore stores+ items = concatMap itemsOfActor actors+ in map snd $ filter ((== iid) . fst) items+ -- | Price an item, taking count into consideration. itemPrice :: (Item, Int) -> Int itemPrice (item, jcount) =@@ -188,8 +199,8 @@ tryFindActor s p = find (p . snd) $ EM.assocs $ sactorD s -tryFindHeroK :: State -> FactionId -> Int -> Maybe (ActorId, Actor)-tryFindHeroK s fact k =+tryFindHeroK :: FactionId -> Int -> State -> Maybe (ActorId, Actor)+tryFindHeroK fact k s = let c | k == 0 = '@' | k > 0 && k < 10 = Char.intToDigit k | otherwise = assert `failure` "no digit" `twith` k@@ -226,13 +237,19 @@ -- | Gets actor body from the current level. Error if not found. getActorBody :: ActorId -> State -> Actor getActorBody aid s =- fromMaybe (assert `failure` "body not found" `twith` (aid, s))- $ EM.lookup aid $ sactorD s+ let assFail = assert `failure` "body not found" `twith` (aid, s)+ in EM.findWithDefault assFail aid $ sactorD s getCarriedAssocs :: Actor -> State -> [(ItemId, Item)] getCarriedAssocs b s =- bagAssocs s $ EM.unionsWith (const) [binv b, beqp b, borgan b]+ bagAssocs s $ EM.unionsWith const [binv b, beqp b, borgan b] +getCarriedIidCStore :: Actor -> [(ItemId, CStore)]+getCarriedIidCStore b =+ let bagCarried (cstore, bag) = map (,cstore) $ EM.keys bag+ in concatMap bagCarried+ [(CInv, binv b), (CEqp, beqp b), (COrgan, borgan b)]+ getCBag :: Container -> State -> ItemBag {-# INLINE getCBag #-} getCBag c s = case c of@@ -241,7 +258,7 @@ CEmbed lid p -> EM.findWithDefault EM.empty p $ lembed (sdungeon s EM.! lid) CActor aid cstore -> getActorBag aid cstore s- CTrunk fid _ _ -> sharedAllOwnedFid False fid s+ CTrunk{} -> assert `failure` c getActorBag :: ActorId -> CStore -> State -> ItemBag {-# INLINE getActorBag #-}@@ -260,6 +277,18 @@ CInv -> binv b CSha -> gsha $ sfactionD s EM.! bfid b +mapActorItems_ :: Monad m+ => (CStore -> ItemId -> ItemQuant -> m a) -> Actor+ -> State+ -> m ()+mapActorItems_ f b s = do+ let notProcessed = [CGround]+ sts = [minBound..maxBound] \\ notProcessed+ g cstore = do+ let bag = getBodyActorBag b cstore s+ mapM_ (uncurry $ f cstore) $ EM.assocs bag+ mapM_ g sts+ getActorAssocs :: ActorId -> CStore -> State -> [(ItemId, Item)] getActorAssocs aid cstore s = bagAssocs s $ getActorBag aid cstore s @@ -286,7 +315,7 @@ -- Worry actor by enemies felt (even if not seen) -- on the level within 3 steps. fact = (EM.! bfid b) . sfactionD $ s- allFoes = actorRegularList (isAtWar fact) (blid b) $ s+ allFoes = actorRegularList (isAtWar fact) (blid b) s isHeard body = not (waitedLastTurn body) && chessDist (bpos b) (bpos body) <= 3 noisyFoes = filter isHeard allFoes@@ -305,20 +334,11 @@ let body = getActorBody aid s fact = (EM.! bfid body) . sfactionD $ s factionSkills- | Just aid == mleader = Ability.unitSkills+ | Just aid == mleader = Ability.zeroSkills | otherwise = fskillsOther $ gplayer fact itemSkills = sumSkills activeItems in itemSkills `Ability.addSkills` factionSkills -maxActorSkills :: ActorId -> [ItemFull] -> State -> Ability.Skills-maxActorSkills aid activeItems s =- let body = getActorBody aid s- fact = (EM.! bfid body) . sfactionD $ s- factionSkills = Ability.maxSkills Ability.unitSkills- (fskillsOther $ gplayer fact)- itemSkills = sumSkills activeItems- in itemSkills `Ability.addSkills` factionSkills- -- Check whether an actor can displace an enemy. We assume they are adjacent. dispEnemy :: ActorId -> ActorId -> [ItemFull] -> State -> Bool dispEnemy source target activeItems s =@@ -327,14 +347,13 @@ friendlyFid fid = fid == bfid b || isAllied fact fid sup = actorRegularList friendlyFid (blid b) s in any (adjacent (bpos b) . bpos) sup- actorSk = maxActorSkills target activeItems s+ actorMaxSk = sumSkills activeItems sb = getActorBody source s tb = getActorBody target s in bproj tb || not (actorDying tb || braced tb- || EM.findWithDefault 0 Ability.AbDisplace actorSk <= 0- && EM.findWithDefault 0 Ability.AbMove actorSk <= 0+ || EM.findWithDefault 0 Ability.AbMove actorMaxSk <= 0 || hasSupport sb && hasSupport tb) -- solo actors are flexible fullAssocs :: Kind.COps -> DiscoveryKind -> DiscoveryEffect@@ -358,9 +377,17 @@ , itemAE = EM.lookup iid discoEffect } in ItemFull {..} -goesIntoInv :: Item -> Bool-goesIntoInv item = isNothing $ strengthEqpSlot item+-- Non-durable item that hurts doesn't go into equipment by default,+-- but if it is in equipment or among organs, it's used for melee+-- nevertheless, e.g., thorns.+goesIntoInv :: ItemFull -> Bool+goesIntoInv itemFull = not (isJust (strengthEqpSlot $ itemBase itemFull))+-- TODO: not needed if EqpSlotWeapon stays || isMeleeEqp itemFull) +goesIntoSha :: ItemFull -> Bool+goesIntoSha itemFull = IK.Precious `elem` jfeature (itemBase itemFull)+ && goesIntoInv itemFull+ eqpOverfull :: Actor -> Int -> Bool eqpOverfull b n = let size = sum $ map fst $ EM.elems $ beqp b in assert (size <= 10 `blame` (b, n, size))@@ -371,7 +398,7 @@ CFloor{} -> CGround CEmbed{} -> CGround CActor _ cstore -> cstore- CTrunk{} -> CGround+ CTrunk{} -> assert `failure` c -- | Determine the dungeon level of the container. If the item is in a shared -- stash, the level depends on which actor asks.@@ -379,10 +406,72 @@ lidFromC (CFloor lid _) _ = lid lidFromC (CEmbed lid _) _ = lid lidFromC (CActor aid _) s = blid $ getActorBody aid s-lidFromC (CTrunk _ lid _) _ = lid+lidFromC c@CTrunk{} _ = assert `failure` c aidFromC :: Container -> Maybe ActorId aidFromC CFloor{} = Nothing aidFromC CEmbed{} = Nothing aidFromC (CActor aid _) = Just aid-aidFromC CTrunk{} = Nothing+aidFromC c@CTrunk{} = assert `failure` c++hasCharge :: Time -> ItemFull -> Bool+hasCharge localTime itemFull@ItemFull{..} =+ let it1 = case strengthFromEqpSlot IK.EqpSlotTimeout itemFull of+ Nothing -> [] -- if item not IDed, assume no timeout, to ID by use+ Just timeout ->+ let timeoutTurns = timeDeltaScale (Delta timeTurn) timeout+ charging startT = timeShift startT timeoutTurns > localTime+ in filter charging itemTimer+ len = length it1+ in len < itemK++strMelee :: Bool -> Time -> ItemFull -> Maybe Int+strMelee effectBonus localTime itemFull =+ let durable = IK.Durable `elem` jfeature (itemBase itemFull)+ recharged = hasCharge localTime itemFull+ -- We assume extra weapon effects are useful and so such+ -- weapons are preferred over weapons with no effects.+ -- If the player doesn't like a particular weapon's extra effect,+ -- he has to manage this manually.+ p (IK.Hurt d) = [Dice.meanDice d]+ p (IK.Burn d) = [Dice.meanDice d]+ p IK.NoEffect{} = []+ p IK.OnSmash{} = []+ -- Hackish extra bonus to force Summon as first effect used+ -- before Calm of enemy is depleted.+ p (IK.Recharging IK.Summon{}) = [999 | recharged && effectBonus]+ -- We assume the weapon is still worth using, even if some effects+ -- are charging; in particular, we assume Hurt or Burn are not+ -- under Recharging.+ p IK.Recharging{} = [100 | recharged && effectBonus]+ p IK.Temporary{} = []+ p _ = [100 | effectBonus]+ psum = sum (strengthEffect p itemFull)+ in if not (isMelee itemFull) || psum == 0+ then Nothing+ else Just $ psum + if durable then 1000 else 0++strongestMelee :: Bool -> Time -> [(ItemId, ItemFull)]+ -> [(Int, (ItemId, ItemFull))]+strongestMelee effectBonus localTime is =+ let f = strMelee effectBonus localTime+ g (iid, itemFull) = (\v -> (v, (iid, itemFull))) <$> f itemFull+ in sortBy (flip $ Ord.comparing fst) $ mapMaybe g is++isMelee :: ItemFull -> Bool+isMelee itemFull =+ let p IK.Hurt{} = True+ p IK.Burn{} = True+ p _ = False+ in case itemDisco itemFull of+ Just ItemDisco{itemAE=Just ItemAspectEffect{jeffects}} ->+ any p jeffects+ Just ItemDisco{itemKind=IK.ItemKind{IK.ieffects}} ->+ any p ieffects+ Nothing -> False++-- Melee weapon so good (durable) that goes into equipment by default.+isMeleeEqp :: ItemFull -> Bool+isMeleeEqp itemFull =+ let durable = IK.Durable `elem` jfeature (itemBase itemFull)+ in isMelee itemFull && durable
Game/LambdaHack/Common/Dice.hs view
@@ -3,22 +3,24 @@ -- | Representation of dice for parameters scaled with current level depth. module Game.LambdaHack.Common.Dice ( -- * Frequency distribution for casting dice scaled with level depth- Dice, diceConst, diceLevel, diceScale, (|*|)+ Dice, diceConst, diceLevel, diceMult, (|*|) , d, ds, dl, intToDice , maxDice, minDice, meanDice, reduceDice -- * Dice for rolling a pair of integer parameters representing coordinates. , DiceXY(..), maxDiceXY, minDiceXY, meanDiceXY #ifdef EXPOSE_INTERNAL+ -- * Internal operations , SimpleDice #endif ) where import Control.Applicative+import Control.DeepSeq import Data.Binary import qualified Data.Char as Char import Data.Hashable (Hashable) import qualified Data.IntMap.Strict as IM-import Data.Ratio+import Data.Maybe import Data.Text (Text) import qualified Data.Text as T import Data.Tuple@@ -68,11 +70,11 @@ -> SimpleDice -> SimpleDice -> SimpleDice liftA2AdditiveName name f fra frb = let frRes = liftA2 f fra frb- nameRes =- if nameFrequency fra == "0" then+ nameRes+ | nameFrequency fra == "0" = (if name == "+" then "" else name) <+> nameFrequency frb- else if nameFrequency frb == "0" then nameFrequency fra- else nameFrequency fra <+> name <+> nameFrequency frb+ | nameFrequency frb == "0" = nameFrequency fra+ | otherwise = nameFrequency fra <+> name <+> nameFrequency frb in renameFreq nameRes frRes dieSimple :: Int -> SimpleDice@@ -95,26 +97,29 @@ data Dice = Dice { diceConst :: SimpleDice , diceLevel :: SimpleDice- , diceScale :: Int+ , diceMult :: Int } deriving (Read, Eq, Ord, Generic) +-- Read and Show should be inverses in this case. instance Show Dice where show Dice{..} = T.unpack $- let rawScaled = nameFrequency diceLevel- scaled = if rawScaled == "0" then "" else rawScaled- signAndScaled = case T.uncons scaled of+ let rawMult = nameFrequency diceLevel+ scaled = if rawMult == "0" then "" else rawMult+ signAndMult = case T.uncons scaled of Just ('-', _) -> scaled _ -> "+" <+> scaled in (if nameFrequency diceLevel == "0" then nameFrequency diceConst else if nameFrequency diceConst == "0" then scaled- else nameFrequency diceConst <+> signAndScaled)- <+> if diceScale == 1 then "" else "|*|" <+> tshow diceScale+ else nameFrequency diceConst <+> signAndMult)+ <+> if diceMult == 1 then "" else "|*|" <+> tshow diceMult instance Hashable Dice instance Binary Dice +instance NFData Dice+ instance Num Dice where (Dice dc1 dl1 ds1) + (Dice dc2 dl2 ds2) = Dice (scaleFreq ds1 dc1 + scaleFreq ds2 dc2)@@ -145,51 +150,58 @@ negate = affectBothDice negate abs = affectBothDice abs signum = affectBothDice signum- fromInteger n = Dice (fromInteger n) (fromInteger 0) 1+ fromInteger n = Dice (fromInteger n) 0 1 affectBothDice :: (SimpleDice -> SimpleDice) -> Dice -> Dice affectBothDice f (Dice dc1 dl1 ds1) = Dice (f dc1) (f dl1) ds1 +-- | A single simple dice. d :: Int -> Dice-d n = Dice (dieSimple n) (fromInteger 0) 1+d n = Dice (dieSimple n) 0 1 +-- | Dice scaled with level. ds :: Int -> Dice-ds n = Dice (fromInteger 0) (dieLevelSimple n) 1+ds n = Dice 0 (dieLevelSimple n) 1 dl :: Int -> Dice dl = ds -- Not exposed to save on documentation. _z :: Int -> Dice-_z n = Dice (zdieSimple n) (fromInteger 0) 1+_z n = Dice (zdieSimple n) 0 1 _zl :: Int -> Dice-_zl n = Dice (fromInteger 0) (zdieLevelSimple n) 1+_zl n = Dice 0 (zdieLevelSimple n) 1 intToDice :: Int -> Dice intToDice = fromInteger . fromIntegral +infixl 5 |*|+-- | Multiplying the dice, after all randomness is resolved, by a constant.+-- Infix declaration ensures that @1 + 2 |*| 3@ parses as @(1 + 2) |*| 3@. (|*|) :: Dice -> Int -> Dice Dice dc1 dl1 ds1 |*| s2 = Dice dc1 dl1 (ds1 * s2) -- | Maximal value of dice. The scaled part taken assuming maximum level.--- Assumes the frequencies are not null. maxDice :: Dice -> Int-maxDice Dice{..} = (maxFreq diceConst + maxFreq diceLevel) * diceScale+maxDice Dice{..} = (fromMaybe 0 (maxFreq diceConst)+ + fromMaybe 0 (maxFreq diceLevel))+ * diceMult -- | Minimal value of dice. The scaled part ignored.--- Assumes the frequencies are not null. minDice :: Dice -> Int-minDice Dice{..} = minFreq diceConst * diceScale+minDice Dice{..} = fromMaybe 0 (minFreq diceConst) * diceMult --- | Mean value of dice. The scaled part taken assuming average level.+-- | Mean value of dice. The level-dependent part is taken assuming+-- the highest level, because that's where the game is the hardest. -- Assumes the frequencies are not null.-meanDice :: Dice -> Rational-meanDice Dice{..} = meanFreq diceConst * fromIntegral diceScale- + meanFreq diceLevel * fromIntegral diceScale * (1%2)+meanDice :: Dice -> Int+meanDice Dice{..} = (meanFreq diceConst + meanFreq diceLevel) * diceMult reduceDice :: Dice -> Maybe Int-reduceDice de = if minDice de == maxDice de then Just (minDice de) else Nothing+reduceDice de =+ let minD = minDice de+ in if minD == maxDice de then Just minD else Nothing -- | Dice for rolling a pair of integer parameters pertaining to, -- respectively, the X and Y cartesian 2D coordinates.@@ -209,5 +221,5 @@ minDiceXY (DiceXY x y) = (minDice x, minDice y) -- | Mean value of DiceXY.-meanDiceXY :: DiceXY -> (Rational, Rational)+meanDiceXY :: DiceXY -> (Int, Int) meanDiceXY (DiceXY x y) = (meanDice x, meanDice y)
Game/LambdaHack/Common/EffectDescription.hs view
@@ -6,13 +6,11 @@ ) where import Control.Exception.Assert.Sugar-import qualified Control.Monad.State as St import qualified Data.EnumMap.Strict as EM import Data.Text (Text) import qualified Data.Text as T import qualified NLP.Miniutter.English as MU -import Game.LambdaHack.Common.Actor import qualified Game.LambdaHack.Common.Dice as Dice import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Msg@@ -32,10 +30,10 @@ effectToSuff :: Effect -> Text effectToSuff effect = case effect of- NoEffect t -> t+ NoEffect _ -> "" -- printed specially Hurt dice -> wrapInParens (tshow dice)- Burn p | p <= 0 -> assert `failure` effect- Burn p -> wrapInParens (makePhrase [MU.CarWs p "burn"])+ Burn d -> wrapInParens (tshow d+ <+> if d > 1 then "burns" else "burn") Explode t -> "of" <+> tshow t <+> "explosion" RefillHP p | p > 0 -> "of limited healing" <+> wrapInParens (affixBonus p)@@ -101,8 +99,8 @@ let grpText = tshow grp hitText = if hit then "smash" else "drop" in "of" <+> hitText <+> grpText -- TMI: <+> ppCStore store- PolyItem store -> "of repurpose" <+> ppCStore store- Identify store -> "of identify starting" <+> ppCStore store+ PolyItem -> "of repurpose on the ground"+ Identify -> "of identify on the ground" SendFlying tmod -> "of impact" <+> tmodToSuff "" tmod PushActor tmod -> "of pushing" <+> tmodToSuff "" tmod PullActor tmod -> "of pulling" <+> tmodToSuff "" tmod@@ -125,23 +123,24 @@ in if vSuff == "" && tSuff == "" then "" else verb <+> "with" <+> vSuff <+> tSuff -aspectToSuff :: Show a => Aspect a -> (a -> Text) -> Text-aspectToSuff aspect f =- rawAspectToSuff $ St.evalState (aspectTrav aspect $ return . f) ()- rawAspectToSuff :: Aspect Text -> Text rawAspectToSuff aspect = case aspect of+ Unique -> "" -- marked by capital letters in name Periodic{} -> "" -- printed specially Timeout{} -> "" -- printed specially- AddMaxHP t -> wrapInParens $ t <+> "HP"- AddMaxCalm t -> wrapInParens $ t <+> "Calm"- AddSpeed t -> wrapInParens $ t <+> "speed"- AddSkills p -> wrapInParens $ "+" <+> T.pack (show $ EM.toList p) AddHurtMelee t -> wrapInParens $ t <> "% melee" AddHurtRanged t -> wrapInParens $ t <> "% ranged" AddArmorMelee t -> "[" <> t <> "%]" AddArmorRanged t -> "{" <> t <> "%}"+ AddMaxHP t -> wrapInParens $ t <+> "HP"+ AddMaxCalm t -> wrapInParens $ t <+> "Calm"+ AddSpeed t -> wrapInParens $ t <+> "speed"+ AddSkills p ->+ let skillToSuff (skill, bonus) =+ (if bonus > 0 then "+" else "")+ <> tshow bonus <+> tshow skill+ in wrapInParens $ T.intercalate " " $ map skillToSuff $ EM.assocs p AddSight t -> wrapInParens $ t <+> "sight" AddSmell t -> wrapInParens $ t <+> "smell" AddLight t -> wrapInParens $ t <+> "light"@@ -159,10 +158,10 @@ Tactic tactics -> "overrides tactics to" <+> tshow tactics effectToSuffix :: Effect -> Text-effectToSuffix effect = effectToSuff effect+effectToSuffix = effectToSuff aspectToSuffix :: Aspect Int -> Text-aspectToSuffix aspect = aspectToSuff aspect affixBonus+aspectToSuffix = rawAspectToSuff . fmap affixBonus affixBonus :: Int -> Text affixBonus p = case compare p 0 of@@ -185,4 +184,4 @@ kindEffectToSuffix = effectToSuffix kindAspectToSuffix :: Aspect Dice.Dice -> Text-kindAspectToSuffix aspect = aspectToSuff aspect affixDice+kindAspectToSuffix = rawAspectToSuff . fmap affixDice
Game/LambdaHack/Common/Faction.hs view
@@ -2,13 +2,14 @@ -- | Factions taking part in the game: e.g., two human players controlling -- the hero faction battling the monster and the animal factions. module Game.LambdaHack.Common.Faction- ( FactionId, FactionDict, Faction(..), Diplomacy(..), Outcome(..), Status(..)+ ( FactionId, FactionDict, Faction(..), Diplomacy(..), Status(..) , Target(..) , isHorrorFact- , canMoveFact, noRunWithMulti, isAIFact, autoDungeonLevel, automatePlayer+ , noRunWithMulti, isAIFact, autoDungeonLevel, automatePlayer , isAtWar, isAllied , difficultyBound, difficultyDefault, difficultyCoeff #ifdef EXPOSE_INTERNAL+ -- * Internal operations , Dipl #endif ) where@@ -43,7 +44,7 @@ , gsha :: !ItemBag -- ^ faction's shared inventory , gvictims :: !(EM.EnumMap (Kind.Id ItemKind) Int) -- ^ members killed }- deriving (Show, Eq)+ deriving (Show, Eq, Ord) -- | Diplomacy states. Higher overwrite lower in case of assymetric content. data Diplomacy =@@ -55,16 +56,6 @@ type Dipl = EM.EnumMap FactionId Diplomacy --- | Outcome of a game.-data Outcome =- Killed -- ^ the faction was eliminated- | Defeated -- ^ the faction lost the game in another way- | Camping -- ^ game is supended- | Conquer -- ^ the player won by eliminating all rivals- | Escape -- ^ the player escaped the dungeon alive- | Restart -- ^ game is restarted- deriving (Show, Eq, Ord, Enum)- -- | Current game status. data Status = Status { stOutcome :: !Outcome -- ^ current game outcome@@ -82,7 +73,7 @@ -- ^ last seen position of the targeted actor | TPoint !LevelId !Point -- ^ target a concrete spot | TVector !Vector -- ^ target position relative to actor- deriving (Show, Eq)+ deriving (Show, Eq, Ord) -- | Tell whether the faction consists of summoned horrors only. --@@ -95,13 +86,6 @@ isHorrorFact :: Faction -> Bool isHorrorFact fact = fgroup (gplayer fact) == "horror" -canMoveFact :: Faction -> Bool -> Bool-canMoveFact fact isLeader =- let skillsOther = fskillsOther $ gplayer fact- in if isLeader- then True- else EM.findWithDefault 0 Ability.AbMove skillsOther > 0- -- A faction where other actors move at once or where some of leader change -- is automatic can't run with multiple actors at once. That would be -- overpowered or too complex to keep correct.@@ -113,7 +97,7 @@ noRunWithMulti :: Faction -> Bool noRunWithMulti fact = let skillsOther = fskillsOther $ gplayer fact- in EM.findWithDefault 0 Ability.AbMove skillsOther > 0+ in EM.findWithDefault 0 Ability.AbMove skillsOther >= 0 || case fleaderMode (gplayer fact) of LeaderNull -> True LeaderAI AutoLeader{} -> True@@ -179,10 +163,6 @@ return $! Faction{..} instance Binary Diplomacy where- put = putWord8 . toEnum . fromEnum- get = fmap (toEnum . fromEnum) getWord8--instance Binary Outcome where put = putWord8 . toEnum . fromEnum get = fmap (toEnum . fromEnum) getWord8
Game/LambdaHack/Common/File.hs view
@@ -88,7 +88,7 @@ Just pathsDataIn -> do let pathsDataOut = dataDir </> fout bOut <- doesFileExist pathsDataOut- when (not bOut) $+ unless bOut $ Ex.handle (\(_ :: Ex.IOException) -> return ()) (copyFile pathsDataIn pathsDataOut) in mapM_ cpFile files
Game/LambdaHack/Common/Frequency.hs view
@@ -14,21 +14,24 @@ import Control.Applicative import Control.Arrow (first, second)+import Control.DeepSeq import Control.Exception.Assert.Sugar import Control.Monad import Data.Binary import Data.Foldable (Foldable)+import qualified Data.Foldable as F import Data.Hashable (Hashable)-import Data.Ratio import Data.Text (Text) import Data.Traversable (Traversable) import GHC.Generics (Generic) +import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Msg -- TODO: do not expose runFrequency -- | The frequency distribution type. Not normalized (operations may -- or may not group the same elements and sum their frequencies).+-- However, elements with zero frequency are removed upon construction. -- -- The @Eq@ instance compares raw representations, not relative, -- normalized frequencies, so operations don't need to preserve@@ -77,6 +80,8 @@ instance Binary a => Binary (Frequency a) +instance NFData a => NFData (Frequency a)+ -- | Uniform discrete frequency distribution. uniformFreq :: Text -> [a] -> Frequency a uniformFreq name l = Frequency (map (\x -> (1, x)) l) name@@ -84,9 +89,9 @@ -- | Takes a name and a list of frequencies and items -- into the frequency distribution. toFreq :: Text -> [(Int, a)] -> Frequency a-toFreq = flip Frequency+toFreq name l = Frequency (filter ((> 0 ) . fst) l) name --- | Scale frequecy distribution, multiplying it+-- | Scale frequency distribution, multiplying it -- by a positive integer constant. scaleFreq :: Show a => Int -> Frequency a -> Frequency a scaleFreq n (Frequency xs name) =@@ -100,27 +105,28 @@ -- | Set frequency of an element. setFreq :: Eq a => Frequency a -> a -> Int -> Frequency a setFreq (Frequency xs name) x n =- let f (_, y) | y == x = (n, x)- f my = my- in Frequency (map f xs) name+ let xsNew = [(n, x) | n <= 0] ++ filter ((/= x) . snd) xs+ in Frequency xsNew name -- | Test if the frequency distribution is empty. nullFreq :: Frequency a -> Bool-nullFreq (Frequency fs _) = all (<= 0) $ map fst fs+{-# INLINE nullFreq #-}+nullFreq (Frequency fs _) = null fs -maxFreq :: (Show a, Ord a) => Frequency a -> a-maxFreq fr@(Frequency xs _) = case filter ((> 0 ) . fst) xs of- [] -> assert `failure` fr- ys -> maximum $ map snd ys+maxFreq :: Ord a => Frequency a -> Maybe a+{-# INLINE maxFreq #-}+maxFreq fr = if nullFreq fr then Nothing else Just $ F.maximum fr -minFreq :: (Show a, Ord a) => Frequency a -> a-minFreq fr@(Frequency xs _) = case filter ((> 0 ) . fst) xs of- [] -> assert `failure` fr- ys -> minimum $ map snd ys+minFreq :: Ord a => Frequency a -> Maybe a+{-# INLINE minFreq #-}+minFreq fr = if nullFreq fr then Nothing else Just $ F.minimum fr -meanFreq :: (Show a, Integral a) => Frequency a -> Rational-meanFreq fr@(Frequency xs _) = case filter ((> 0 ) . fst) xs of+-- | Average value of an @Int@ distribution, rounded up to avoid truncating+-- it in the other code higher up, which would equate 1d0 with 1d1.+meanFreq :: Frequency Int -> Int+{-# INLINE meanFreq #-}+meanFreq fr@(Frequency xs _) = case xs of [] -> assert `failure` fr- ys -> let sumP = sum $ map fst ys- sumX = sum [ fromIntegral p * x | (p, x) <- ys ]- in if sumX == 0 then 0 else fromIntegral sumX % fromIntegral sumP+ _ -> let sumX = sum [ p * x | (p, x) <- xs ]+ sumP = sum $ map fst xs+ in sumX `divUp` sumP
Game/LambdaHack/Common/HighScore.hs view
@@ -2,8 +2,10 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} -- | High score table operations. module Game.LambdaHack.Common.HighScore- ( ScoreTable, empty, register, showScore, getRecord, highSlideshow+ ( ScoreDict, ScoreTable+ , empty, register, showScore, getTable, getRecord, highSlideshow #ifdef EXPOSE_INTERNAL+ -- * Internal operations , ScoreRecord #endif ) where@@ -25,6 +27,8 @@ import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.Time import Game.LambdaHack.Content.ItemKind (ItemKind)+import Game.LambdaHack.Content.ModeKind (HiCondPoly, HiIndeterminant (..),+ ModeKind, Outcome (..)) -- | A single score record. Records are ordered in the highscore table, -- from the best to the worst, in lexicographic ordering wrt the fields below.@@ -51,6 +55,16 @@ instance Binary ScoreRecord +-- | The list of scores, in decreasing order.+newtype ScoreTable = ScoreTable [ScoreRecord]+ deriving (Eq, Binary)++instance Show ScoreTable where+ show _ = "a score table"++-- | A dictionary from game mode IDs to scores tables.+type ScoreDict = EM.EnumMap (Kind.Id ModeKind) ScoreTable+ -- | Show a single high score, from the given ranking in the high score table. showScore :: (Int, ScoreRecord) -> [Text] showScore (pos, score) =@@ -72,28 +86,24 @@ diff = difficulty score diffText | diff == difficultyDefault = "" | otherwise = "difficulty" <+> tshow diff <> ", "- tturns = makePhrase $ [MU.CarWs turns "turn"]+ tturns = makePhrase [MU.CarWs turns "turn"] in [ tpos <> "." <+> tscore <+> gplayerName score <+> died <> "," <+> victims <> "," , " " <> diffText <> "after" <+> tturns <+> "on" <+> curDate <> "." ] +getTable :: Kind.Id ModeKind -> ScoreDict -> ScoreTable+getTable = EM.findWithDefault (ScoreTable [])+ getRecord :: Int -> ScoreTable -> ScoreRecord getRecord pos (ScoreTable table) = fromMaybe (assert `failure` (pos, table)) $ listToMaybe $ drop (pred pos) table --- | The list of scores, in decreasing order.-newtype ScoreTable = ScoreTable [ScoreRecord]- deriving (Eq, Binary)--instance Show ScoreTable where- show _ = "a score table"- -- | Empty score table-empty :: ScoreTable-empty = ScoreTable []+empty :: ScoreDict+empty = EM.empty -- | Insert a new score into the table, Return new table and the ranking. -- Make sure the table doesn't grow too large.@@ -113,36 +123,29 @@ -> Text -- ^ name of the faction's gplayer -> EM.EnumMap (Kind.Id ItemKind) Int -- ^ allies lost -> EM.EnumMap (Kind.Id ItemKind) Int -- ^ foes killed- -> Bool -- ^ whether the faction fights against spawners+ -> HiCondPoly -> (Bool, (ScoreTable, Int)) register table total time status@Status{stOutcome} date difficulty gplayerName- ourVictims theirVictims loots =- let pBase =- if loots- -- Heroes rejoice in loot and mourn their victims.- then fromIntegral total- -- Spawners or skirmishers get no bonus from loot and no malus- -- from loses, but try to kill opponents fast and blodily,- -- or at least hold up for long and incur heavy losses.- else let turnsSpent = timeFitUp time timeTurn- speedup = max 0 $ 1000000 - 100 * turnsSpent- survival = 100 * turnsSpent- in if stOutcome `elem` [Conquer, Escape]- -- Up to 1000 points for quick victory, so up to 10000 turns.- then sqrt $ fromIntegral speedup- -- Up to 1000 points for surviving long, so up to 10000 turns.- else min 1000- $ sqrt $ fromIntegral survival- pBonus =- if loots- then max 0 (1000 - 100 * sum (EM.elems ourVictims))- else 1000 + 100 * sum (EM.elems theirVictims)- pSum :: Double- pSum = if stOutcome `elem` [Conquer, Escape]- then pBase + fromIntegral pBonus- else pBase+ ourVictims theirVictims hiCondPoly =+ let turnsSpent = fromIntegral $ timeFitUp time timeTurn+ hiInValue (hi, c) = case hi of+ HiConst -> c+ HiLoot -> c * fromIntegral total+ HiBlitz -> -- Up to 1000000/c turns matter.+ sqrt $ max 0 (1000000 + c * turnsSpent)+ HiSurvival -> -- Up to 1000000/c turns matter.+ sqrt $ max 0 (min 1000000 $ c * turnsSpent)+ HiKill -> c * fromIntegral (sum (EM.elems theirVictims))+ HiLoss -> c * fromIntegral (sum (EM.elems ourVictims))+ hiPolynomialValue = sum . map hiInValue+ hiSummandValue (hiPoly, outcomes) =+ if stOutcome `elem` outcomes+ then max 0 (hiPolynomialValue hiPoly)+ else 0+ hiCondValue = sum . map hiSummandValue points = (ceiling :: Double -> Int)- $ pSum * 1.5 ^^ (- (difficultyCoeff difficulty))+ $ hiCondValue hiCondPoly+ * 1.5 ^^ (- (difficultyCoeff difficulty)) negTime = absoluteTimeNegate time score = ScoreRecord{..} in (points > 0, insertPos score table)@@ -153,7 +156,7 @@ tshowable (ScoreTable table) start height = let zipped = zip [1..] table screenful = take height . drop (start - 1) $ zipped- in (intercalate ["\n"] $ map showScore screenful) ++ [moreMsg]+ in intercalate ["\n"] (map showScore screenful) ++ [moreMsg] -- | Produce a couple of renderings of the high scores table. showCloseScores :: Int -> ScoreTable -> Int -> [[Text]]@@ -166,12 +169,13 @@ -- | Generate a slideshow with the current and previous scores. highSlideshow :: ScoreTable -- ^ current score table -> Int -- ^ position of the current score in the table+ -> Text -- ^ the name of the game mode -> Slideshow-highSlideshow table pos =+highSlideshow table pos gameModeName = let (_, nlines) = normalLevelBound -- TODO: query terminal size instead height = nlines `div` 3 posStatus = status $ getRecord pos table- (subject, person, msgUnless) =+ (efforts, person, msgUnless) = case stOutcome posStatus of Killed | stDepth posStatus <= 1 -> ("your short-lived struggle", MU.Sg3rd, "(no bonus)")@@ -186,17 +190,18 @@ Conquer -> ("your ruthless victory", MU.Sg3rd, if pos <= height- then "among the greatest heroes"+ then "among the best" -- "greatest heroes" doesn't fit else "(bonus included)") Escape -> ("your dashing coup", MU.Sg3rd, if pos <= height- then "among the greatest heroes"+ then "among the best" else "(bonus included)") Restart -> ("your abortive attempt", MU.Sg3rd, "(no bonus)")+ subject =+ makePhrase [efforts, "in", MU.Capitalize $ MU.Text gameModeName] msg = makeSentence- [ MU.SubjectVerb person MU.Yes subject "award you"- , MU.Ordinal pos, "place"- , msgUnless ]- in toSlideshow False $ map ([msg, "\n"] ++) $ showCloseScores pos table height+ [ MU.SubjectVerb person MU.Yes (MU.Text subject) "award you"+ , MU.Ordinal pos, "place", msgUnless ]+ in toSlideshow Nothing $ map ([msg, "\n"] ++) $ showCloseScores pos table height
Game/LambdaHack/Common/Item.hs view
@@ -17,6 +17,7 @@ import Data.Hashable (Hashable) import qualified Data.Ix as Ix import Data.Text (Text)+import Data.Traversable (traverse) import GHC.Generics (Generic) import System.Random (mkStdGen) @@ -48,7 +49,7 @@ data ItemAspectEffect = ItemAspectEffect { jaspects :: ![Aspect Int] -- ^ the aspects of the item- , jeffects :: ![Effect] -- ^ the effects when activated+ , jeffects :: ![Effect] -- ^ the effects when applied } deriving (Show, Eq, Generic) @@ -108,7 +109,7 @@ -> ItemAspectEffect seedToAspectsEffects (ItemSeed itemSeed) kind ldepth totalDepth = let castD = castDice ldepth totalDepth- rollA = mapM (flip aspectTrav castD) (iaspects kind)+ rollA = mapM (traverse castD) (iaspects kind) jaspects = St.evalState rollA (mkStdGen itemSeed) jeffects = ieffects kind in ItemAspectEffect{..}
Game/LambdaHack/Common/ItemDescription.hs view
@@ -1,6 +1,6 @@ -- | Descripitons of items. module Game.LambdaHack.Common.ItemDescription- ( partItemN, partItem, partItemWs, partItemAW, partItemWownW+ ( partItemN, partItem, partItemWs, partItemAW, partItemMediumAW, partItemWownW , itemDesc, textAllAE, viewItem ) where @@ -10,7 +10,6 @@ import qualified Data.Text as T import qualified NLP.Miniutter.English as MU -import Game.LambdaHack.Common.ActorState import qualified Game.LambdaHack.Common.Color as Color import qualified Game.LambdaHack.Common.Dice as Dice import Game.LambdaHack.Common.EffectDescription@@ -25,40 +24,51 @@ -- TODO: remove _lid if still unused after some time -- | The part of speech describing the item parameterized by the number -- of effects/aspects to show..-partItemN :: Bool -> Int -> Container -> LevelId -> Time -> ItemFull- -> (MU.Part, MU.Part)+partItemN :: Int -> Int -> CStore -> LevelId -> Time -> ItemFull+ -> (Bool, MU.Part, MU.Part) partItemN fullInfo n c _lid localTime itemFull = let genericName = jname $ itemBase itemFull in case itemDisco itemFull of Nothing -> let flav = flavourToName $ jflavour $ itemBase itemFull- in (MU.Text $ flav <+> genericName, "")- Just _ ->- let effTs = filter (not . T.null) $ textAllAE fullInfo c itemFull- it1 = case strengthFromEqpSlot IK.EqpSlotTimeout itemFull of- Nothing -> []+ in (False, MU.Text $ flav <+> genericName, "")+ Just iDisco ->+ let (toutN, it1) = case strengthFromEqpSlot IK.EqpSlotTimeout itemFull of+ Nothing -> (0, []) Just timeout -> let timeoutTurns = timeDeltaScale (Delta timeTurn) timeout- f startT = timeShift startT timeoutTurns > localTime- in filter f (itemTimer itemFull)+ charging startT = timeShift startT timeoutTurns > localTime+ in (timeout, filter charging (itemTimer itemFull)) len = length it1- timer = if len == 0- then ""- else if itemK itemFull == 1 && len == 1- then "(charging)"- else "(" <> tshow len <+> "charging" <> ")"+ chargingAdj | toutN == 0 = "temporary"+ | otherwise = "charging"+ timer | len == 0 = ""+ | itemK itemFull == 1 && len == 1 = "(" <> chargingAdj <> ")"+ | otherwise = "(" <> tshow len <+> chargingAdj <> ")"+ skipRecharging = fullInfo <= 4 && len >= itemK itemFull+ effTs = filter (not . T.null)+ $ textAllAE fullInfo skipRecharging c itemFull ts = take n effTs- ++ (if length effTs > n then ["(...)"] else [])+ ++ ["(...)" | length effTs > n] ++ [timer]- in (MU.Text genericName, MU.Phrase $ map MU.Text ts)+ isUnique aspects = IK.Unique `elem` aspects+ unique = case iDisco of+ ItemDisco{itemAE=Just ItemAspectEffect{jaspects}} ->+ isUnique jaspects+ ItemDisco{itemKind} ->+ isUnique $ IK.iaspects itemKind+ capName = if unique+ then MU.Capitalize $ MU.Text genericName+ else MU.Text genericName+ in (unique, capName, MU.Phrase $ map MU.Text ts) -- | The part of speech describing the item.-partItem :: Container -> LevelId -> Time -> ItemFull -> (MU.Part, MU.Part)-partItem = partItemN False 4+partItem :: CStore -> LevelId -> Time -> ItemFull -> (Bool, MU.Part, MU.Part)+partItem = partItemN 5 4 -textAllAE :: Bool -> Container -> ItemFull -> [Text]-textAllAE fullInfo c ItemFull{itemBase, itemDisco} =- let features | fullInfo = map featureToSuff $ sort $ jfeature itemBase+textAllAE :: Int -> Bool -> CStore -> ItemFull -> [Text]+textAllAE fullInfo skipRecharging cstore ItemFull{itemBase, itemDisco} =+ let features | fullInfo >= 9 = map featureToSuff $ sort $ jfeature itemBase | otherwise = [] in case itemDisco of Nothing -> features@@ -69,13 +79,16 @@ timeoutAspect :: IK.Aspect a -> Bool timeoutAspect IK.Timeout{} = True timeoutAspect _ = False+ noEffect :: IK.Effect -> Bool+ noEffect IK.NoEffect{} = True+ noEffect _ = False hurtEffect :: IK.Effect -> Bool hurtEffect (IK.Hurt _) = True+ hurtEffect (IK.Burn _) = True hurtEffect _ = False notDetail :: IK.Effect -> Bool- notDetail IK.Explode{} = fullInfo+ notDetail IK.Explode{} = fullInfo >= 6 notDetail _ = True- cstore = storeFromC c active = cstore `elem` [CEqp, COrgan] || cstore == CGround && isJust (strengthEqpSlot itemBase) splitAE :: (Num a, Show a, Ord a)@@ -86,13 +99,13 @@ splitAE reduce_a aspects ppA effects ppE = let mperiodic = find periodicAspect aspects mtimeout = find timeoutAspect aspects+ mnoEffect = find noEffect effects restAs = sort aspects (hurtEs, restEs) = partition hurtEffect $ sort $ filter notDetail effects- aes = map ppE hurtEs- ++ if active- then map ppA restAs ++ map ppE restEs- else map ppE restEs ++ map ppA restAs+ aes = if active+ then map ppA restAs ++ map ppE restEs+ else map ppE restEs ++ map ppA restAs rechargingTs = T.intercalate (T.singleton ' ') $ filter (not . T.null) $ map ppE $ stripRecharging restEs@@ -101,7 +114,7 @@ $ map ppE $ stripOnSmash restEs durable = IK.Durable `elem` jfeature itemBase periodicOrTimeout = case mperiodic of- _ | T.null rechargingTs -> ""+ _ | skipRecharging || T.null rechargingTs -> "" Just IK.Periodic -> case mtimeout of Just (IK.Timeout 0) | not durable ->@@ -118,37 +131,54 @@ _ -> "" onSmash = if T.null onSmashTs then "" else "(on smash:" <+> onSmashTs <> ")"- in [periodicOrTimeout] ++ aes ++ [onSmash | fullInfo]+ noEff = case mnoEffect of+ Just (IK.NoEffect t) -> [t]+ _ -> []+ in noEff ++ if fullInfo >= 5 || fullInfo >= 2 && null noEff+ then [periodicOrTimeout] ++ map ppE hurtEs ++ aes+ ++ [onSmash | fullInfo >= 7]+ else map ppE hurtEs aets = case itemAE of Just ItemAspectEffect{jaspects, jeffects} -> splitAE tshow jaspects aspectToSuffix jeffects effectToSuffix Nothing ->- splitAE (\d -> maybe "?" tshow $ Dice.reduceDice d)+ splitAE (maybe "?" tshow . Dice.reduceDice) (IK.iaspects itemKind) kindAspectToSuffix (IK.ieffects itemKind) kindEffectToSuffix in aets ++ features -- TODO: use kit-partItemWs :: Int -> Container -> LevelId -> Time -> ItemFull -> MU.Part+partItemWs :: Int -> CStore -> LevelId -> Time -> ItemFull -> MU.Part partItemWs count c lid localTime itemFull =- let (name, stats) = partItem c lid localTime itemFull- in MU.Phrase [MU.CarWs count name, stats]+ let (unique, name, stats) = partItem c lid localTime itemFull+ in if unique && count == 1+ then MU.Phrase ["the", name, stats]+ else MU.Phrase [MU.CarWs count name, stats] -partItemAW :: Container -> LevelId -> Time -> ItemFull -> MU.Part+partItemAW :: CStore -> LevelId -> Time -> ItemFull -> MU.Part partItemAW c lid localTime itemFull =- let (name, stats) = partItem c lid localTime itemFull- in MU.AW $ MU.Phrase [name, stats]+ let (unique, name, stats) = partItemN 4 4 c lid localTime itemFull+ in if unique+ then MU.Phrase ["the", name, stats]+ else MU.AW $ MU.Phrase [name, stats] -partItemWownW :: MU.Part -> Container -> LevelId -> Time -> ItemFull -> MU.Part+partItemMediumAW :: CStore -> LevelId -> Time -> ItemFull -> MU.Part+partItemMediumAW c lid localTime itemFull =+ let (unique, name, stats) = partItemN 5 100 c lid localTime itemFull+ in if unique+ then MU.Phrase ["the", name, stats]+ else MU.AW $ MU.Phrase [name, stats]++partItemWownW :: MU.Part -> CStore -> LevelId -> Time -> ItemFull -> MU.Part partItemWownW partA c lid localTime itemFull =- let (name, stats) = partItem c lid localTime itemFull+ let (_, name, stats) = partItemN 4 4 c lid localTime itemFull in MU.WownW partA $ MU.Phrase [name, stats] -itemDesc :: Container -> LevelId -> Time -> ItemFull -> Overlay+itemDesc :: CStore -> LevelId -> Time -> ItemFull -> Overlay itemDesc c lid localTime itemFull =- let (name, stats) = partItemN True 99 c lid localTime itemFull+ let (_, name, stats) = partItemN 10 100 c lid localTime itemFull nstats = makePhrase [name, stats] desc = case itemDisco itemFull of Nothing -> "This item is as unremarkable as can be."@@ -157,10 +187,12 @@ (scaledWeight, unitWeight) = if weight > 1000 then (tshow $ fromIntegral weight / (1000 :: Double), "kg")- else (tshow weight, "g")+ else if weight > 0+ then (tshow weight, "g")+ else ("", "") ln = abs $ fromEnum $ jlid (itemBase itemFull) colorSymbol = uncurry (flip Color.AttrChar) (viewItem $ itemBase itemFull)- f color = Color.AttrChar Color.defAttr color+ f = Color.AttrChar Color.defAttr lxsize = fst normalLevelBound + 1 -- TODO blurb = "D" -- dummy
Game/LambdaHack/Common/ItemStrongest.hs view
@@ -3,7 +3,7 @@ module Game.LambdaHack.Common.ItemStrongest ( -- * Strongest items strengthOnSmash, strengthCreateOrgan, strengthDropOrgan- , strengthToThrow, strengthEqpSlot, strengthFromEqpSlot+ , strengthToThrow, strengthEqpSlot, strengthFromEqpSlot, strengthEffect , strongestSlotNoFilter, strongestSlot, sumSlotNoFilter, sumSkills -- * Assorted , totalRange, computeTrajectory, itemTrajectory@@ -12,7 +12,6 @@ import Control.Applicative import Control.Exception.Assert.Sugar-import qualified Control.Monad.State as St import qualified Data.EnumMap.Strict as EM import Data.List import Data.Maybe@@ -35,10 +34,9 @@ concatMap f jaspects Just ItemDisco{itemKind=ItemKind{iaspects}} -> -- Approximation. For some effects lower values are better,- -- so we can't put 999 here (and for summation, this is wrong).- let trav x = St.evalState (aspectTrav x (return . round . Dice.meanDice))- ()- in concatMap f $ map trav iaspects+ -- so we just offer the mean of the dice. This is also correct+ -- for summation, on average.+ concatMap f $ map (fmap Dice.meanDice) iaspects Nothing -> [] strengthAspectMaybe :: Show b => (Aspect Int -> [b]) -> ItemFull -> Maybe b@@ -49,6 +47,7 @@ xs -> assert `failure` (xs, itemFull) strengthEffect :: (Effect -> [b]) -> ItemFull -> [b]+{-# INLINE strengthEffect #-} strengthEffect f itemFull = case itemDisco itemFull of Just ItemDisco{itemAE=Just ItemAspectEffect{jeffects}} ->@@ -60,31 +59,6 @@ strengthFeature :: (Feature -> [b]) -> Item -> [b] strengthFeature f item = concatMap f (jfeature item) --- Simplification: does not take into account effects inside @Recharging@,--- because @Hurt@, etc., are unlikely to have a timeout.-strengthMelee :: ItemFull -> Maybe Int-strengthMelee itemFull =- let durable = Durable `elem` jfeature (itemBase itemFull)- p (Hurt d) = [floor (Dice.meanDice d)]- p (Burn k) = [k]- p _ = []- hasExtraEffects = case itemDisco itemFull of- Just ItemDisco{itemAE=Just ItemAspectEffect{jeffects}} ->- any (\ef -> null $ p ef) jeffects- Just ItemDisco{itemKind=ItemKind{ieffects}} ->- any (\ef -> null $ p ef) ieffects- Nothing -> False- -- We assume extra weapon effects are usually useful and so such- -- weapons are preferred over weapons with the same power, by default.- -- If the player doesn't like a particular weapon's extra effect,- -- he has to manage this manually.- bonusExtraEffects = if hasExtraEffects then 1 else 0- psum = sum (strengthEffect p itemFull)- in if psum == 0- then Nothing- else Just $ bonusExtraEffects + psum + if durable then 100 else 0---- Called only by the server, so 999 is OK. strengthOnSmash :: ItemFull -> [Effect] strengthOnSmash = let p (OnSmash eff) = [eff]@@ -146,7 +120,7 @@ strengthAddSkills :: Ability.Ability -> ItemFull -> Maybe Int strengthAddSkills ab =- let p (AddSkills a) = [fromMaybe 0 $ EM.lookup ab a]+ let p (AddSkills a) = [EM.findWithDefault 0 ab a] p _ = [] in strengthAspectMaybe p @@ -245,11 +219,19 @@ EqpSlotAddLight -> strengthAddLight EqpSlotWeapon -> strengthMelee +strengthMelee :: ItemFull -> Maybe Int+strengthMelee itemFull =+ let p (Hurt d) = [Dice.meanDice d]+ p (Burn d) = [Dice.meanDice d]+ p _ = []+ psum = sum (strengthEffect p itemFull)+ in if psum == 0 then Nothing else Just psum+ strongestSlotNoFilter :: EqpSlot -> [(ItemId, ItemFull)] -> [(Int, (ItemId, ItemFull))] strongestSlotNoFilter eqpSlot is = let f = strengthFromEqpSlot eqpSlot- g (iid, itemFull) = (\v -> (v, (iid, itemFull))) <$> (f itemFull)+ g (iid, itemFull) = (\v -> (v, (iid, itemFull))) <$> f itemFull in sortBy (flip $ Ord.comparing fst) $ mapMaybe g is strongestSlot :: EqpSlot -> [(ItemId, ItemFull)]@@ -262,14 +244,14 @@ in strongestSlotNoFilter eqpSlot slotIs sumSlotNoFilter :: EqpSlot -> [ItemFull] -> Int-sumSlotNoFilter eqpSlot is = assert (eqpSlot /= EqpSlotWeapon) $ -- no 999+sumSlotNoFilter eqpSlot is = let f = strengthFromEqpSlot eqpSlot g itemFull = (* itemK itemFull) <$> f itemFull in sum $ mapMaybe g is sumSkills :: [ItemFull] -> Ability.Skills sumSkills is =- let g itemFull = (Ability.scaleSkills (itemK itemFull))+ let g itemFull = Ability.scaleSkills (itemK itemFull) <$> strengthAllAddSkills itemFull in foldr Ability.addSkills Ability.zeroSkills $ mapMaybe g is @@ -290,21 +272,21 @@ allRecharging :: [Effect] -> [Effect] allRecharging effs =- let getRechargingEffect :: Effect -> Maybe (Effect)+ let getRechargingEffect :: Effect -> Maybe Effect getRechargingEffect e@Recharging{} = Just e getRechargingEffect _ = Nothing in mapMaybe getRechargingEffect effs stripRecharging :: [Effect] -> [Effect] stripRecharging effs =- let getRechargingEffect :: Effect -> Maybe (Effect)+ let getRechargingEffect :: Effect -> Maybe Effect getRechargingEffect (Recharging e) = Just e getRechargingEffect _ = Nothing in mapMaybe getRechargingEffect effs stripOnSmash :: [Effect] -> [Effect] stripOnSmash effs =- let getOnSmashEffect :: Effect -> Maybe (Effect)+ let getOnSmashEffect :: Effect -> Maybe Effect getOnSmashEffect (OnSmash e) = Just e getOnSmashEffect _ = Nothing in mapMaybe getOnSmashEffect effs
Game/LambdaHack/Common/Kind.hs view
@@ -10,7 +10,6 @@ import qualified Data.Ix as Ix import Data.List import qualified Data.Map.Strict as M-import Data.Maybe (fromMaybe) import qualified Data.Text as T import Game.LambdaHack.Common.ContentDef@@ -61,11 +60,12 @@ kindFreq = let tuples = [ (cgroup, (n, (i, k))) | (i, k) <- EM.assocs kindMap- , (cgroup, n) <- getFreq k, n > 0 ]+ , (cgroup, n) <- getFreq k+ , n > 0 ] f m (cgroup, nik) = M.insertWith (++) cgroup [nik] m in foldl' f M.empty tuples- okind i = fromMaybe (assert `failure` "no kind" `twith` (i, kindMap))- $ EM.lookup i kindMap+ okind i = let assFail = assert `failure` "no kind" `twith` (i, kindMap)+ in EM.findWithDefault assFail i kindMap correct a = not (T.null (getName a)) && all ((> 0) . snd) (getFreq a) singleOffenders = [ (offences, a) | a <- content,@@ -76,14 +76,14 @@ assert (null singleOffenders `blame` "some content items not valid" `twith` singleOffenders) $ assert (null allOffences `blame` "the content set not valid"- `twith` (allOffences, content)) $+ `twith` (allOffences, content)) -- By this point 'content' can be GCd. Ops { okind , ouniqGroup = \cgroup ->- let freq = fromMaybe (assert `failure` "no unique group"- `twith` (cgroup, kindFreq))- $ M.lookup cgroup kindFreq+ let freq = let assFail = assert `failure` "no unique group"+ `twith` (cgroup, kindFreq)+ in M.findWithDefault assFail cgroup kindFreq in case freq of [(n, (i, _))] | n > 0 -> i l -> assert `failure` "not unique" `twith` (l, cgroup, kindFreq)@@ -100,12 +100,13 @@ frequency [ i | (i, k) <- kindFreq M.! cgroup, p k ] -} _ -> return Nothing- , ofoldrWithKey = \f z -> foldr (\(i, a) -> f i a) z- $ EM.assocs kindMap+ , ofoldrWithKey = \f z -> foldr (uncurry f) z $ EM.assocs kindMap , ofoldrGroup = \cgroup f z -> case M.lookup cgroup kindFreq of Just freq -> foldr (\(p, (i, a)) -> f p i a) z freq- _ -> z+ _ -> assert `failure` "no group '" <> tshow cgroup+ <> "' among content that has groups"+ <+> tshow (M.keys kindFreq) , obounds = ( fst $ EM.findMin kindMap , fst $ EM.findMax kindMap ) , ospeedup = Nothing -- define elsewhere
Game/LambdaHack/Common/Level.hs view
@@ -76,7 +76,7 @@ , lsmell :: !SmellMap -- ^ remembered smells on the level , ldesc :: !Text -- ^ level description , lstair :: !([Point], [Point])- -- ^ destinations of (up, down) stairs+ -- ^ positions of (up, down) stairs , lseen :: !Int -- ^ currently remembered clear tiles , lclear :: !Int -- ^ total number of initially clear tiles , ltime :: !Time -- ^ date of the last activity on the level@@ -86,7 +86,7 @@ , litemFreq :: !(Freqs ItemKind) -- ^ frequency of initial items; [] for clients , lsecret :: !Int -- ^ secret tile seed , lhidden :: !Int -- ^ secret tile density- , lescape :: !Bool -- ^ has an IK.Escape tile+ , lescape :: ![Point] -- ^ positions of IK.Escape tiles } deriving (Show, Eq) @@ -154,7 +154,8 @@ isSecretPos :: Level -> Point -> Bool isSecretPos lvl (Point x y) =- (lsecret lvl `Bits.rotateR` x `Bits.xor` y + x) `mod` lhidden lvl == 0+ not (lhidden lvl == 0)+ && (lsecret lvl `Bits.rotateR` x `Bits.xor` y + x) `mod` lhidden lvl == 0 hideTile :: Kind.COps -> Level -> Point -> Kind.Id TileKind hideTile Kind.COps{cotile} lvl p =
Game/LambdaHack/Common/Misc.hs view
@@ -5,12 +5,15 @@ ( -- * Game object identifiers FactionId, LevelId, AbsDepth(..), ActorId -- * Item containers- , Container(..), CStore(..)+ , Container(..), CStore(..), ItemDialogMode(..) -- * Assorted , normalLevelBound, divUp, GroupName, toGroupName, Freqs, breturn , serverSaveName, Rarity, validateRarity, Tactic(..)+ -- * Backward compatibility+ , isRight ) where +import Control.DeepSeq import Control.Monad import Data.Binary import qualified Data.EnumMap.Strict as EM@@ -27,7 +30,7 @@ import qualified Data.Text as T import Data.Traversable (traverse) import GHC.Generics (Generic)-import NLP.Miniutter.English ()+import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Common.Point @@ -41,13 +44,14 @@ infixl 7 `divUp` -- | Integer division, rounding up. divUp :: Integral a => a -> a -> a+{-# INLINE divUp #-} divUp n k = (n + k - 1) `div` k -- 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 Text- deriving (Eq, Ord, Read, Hashable, Binary)+ deriving (Eq, Ord, Read, Hashable, Binary, Generic) instance IsString (GroupName a) where fromString = GroupName . T.pack@@ -55,6 +59,8 @@ instance Show (GroupName a) where show (GroupName gn) = T.unpack gn +instance NFData (GroupName a)+ toGroupName :: Text -> GroupName a toGroupName = GroupName @@ -64,7 +70,7 @@ type Freqs a = [(GroupName a, Int)] -- | Rarity on given depths.-type Rarity = [(Int, Int)]+type Rarity = [(Double, Int)] validateRarity :: Rarity -> [Text] validateRarity rarity =@@ -72,10 +78,10 @@ in [ "rarity not sorted" | sortedRarity /= rarity ] ++ [ "rarity depth thresholds not unique" | nubBy ((==) `on` fst) sortedRarity /= sortedRarity ]- ++ [ "rarity depth not between 1 and 10"+ ++ [ "rarity depth not between 0 and 10" | case (sortedRarity, reverse sortedRarity) of ((lowest, _) : _, (highest, _) : _) ->- lowest < 1 || highest > 10+ lowest <= 0 || highest > 10 _ -> False ] -- | @breturn b a = [a | b]@@@ -105,6 +111,13 @@ instance Hashable CStore +instance NFData CStore++data ItemDialogMode = MStore CStore | MOwned | MStats+ deriving (Show, Read, Eq, Ord, Generic)++instance NFData ItemDialogMode+ -- | A unique identifier of a faction in a game. newtype FactionId = FactionId Int deriving (Show, Eq, Ord, Enum, Binary)@@ -152,6 +165,12 @@ instance Hashable Tactic +-- TODO: remove me when we no longer suppoert GHC 7.6.*+isRight :: Either a b -> Bool+isRight e = case e of+ Right{} -> True+ Left{} -> False+ -- Data.Binary instance (Enum k, Binary k, Binary e) => Binary (EM.EnumMap k e) where@@ -206,3 +225,11 @@ instance (Enum k, Hashable k, Hashable e) => Hashable (EM.EnumMap k e) where {-# INLINEABLE hashWithSalt #-} hashWithSalt s x = hashWithSalt s (EM.toAscList x)++-- Control.DeepSeq++instance NFData MU.Part++instance NFData MU.Person++instance NFData MU.Polarity
Game/LambdaHack/Common/MonadStateRead.hs view
@@ -2,7 +2,7 @@ -- player actions. Has no access to the the main action type. module Game.LambdaHack.Common.MonadStateRead ( MonadStateRead(..)- , getLevel, nUI, posOfAid, factionCanEscape, factionLoots+ , getLevel, nUI, posOfAid, factionCanEscape, factionLoots, getGameMode ) where import qualified Data.EnumMap.Strict as EM@@ -10,6 +10,7 @@ import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction+import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.State@@ -36,7 +37,7 @@ factionCanEscape fid = do fact <- getsState $ (EM.! fid) . sfactionD dungeon <- getsState sdungeon- let escape = any lescape $ EM.elems dungeon+ let escape = any (not . null . lescape) $ EM.elems dungeon return $! escape && fcanEscape (gplayer fact) -- TODO: "treasure" is hardwired; tie this code with calculateTotal@@ -48,3 +49,9 @@ loots = any hasTreasure $ EM.elems dungeon canEscape <- factionCanEscape fid return $! canEscape && loots++getGameMode :: MonadStateRead m => m ModeKind+getGameMode = do+ Kind.COps{comode=Kind.Ops{okind}} <- getsState scops+ t <- getsState sgameModeId+ return $! okind t
Game/LambdaHack/Common/Msg.hs view
@@ -2,7 +2,7 @@ -- | Game messages displayed on top of the screen for the player to read. module Game.LambdaHack.Common.Msg ( makePhrase, makeSentence- , Msg, (<>), (<+>), tshow, toWidth, moreMsg, yesnoMsg, truncateMsg+ , Msg, (<>), (<+>), tshow, toWidth, moreMsg, endMsg, yesnoMsg, truncateMsg , Report, emptyReport, nullReport, singletonReport, addMsg, prependMsg , splitReport, renderReport, findInReport, lastMsgOfReport , History, emptyHistory, lengthHistory, singletonHistory@@ -22,10 +22,10 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeUtf8, encodeUtf8)+import Data.Vector.Binary () import qualified Data.Vector.Generic as G import qualified Data.Vector.Unboxed as U import qualified NLP.Miniutter.English as MU-import qualified Text.Show.Pretty as Show.Pretty import Game.LambdaHack.Common.Color import Game.LambdaHack.Common.Misc@@ -36,9 +36,9 @@ (<+>) :: Text -> Text -> Text (<+>) = (MU.<+>) --- Pretty print and pack the result of @show@.+-- Show and pack the result of @show@. tshow :: Show a => a -> Text-tshow x = T.pack $ Show.Pretty.ppShow x+tshow x = T.pack $ show x toWidth :: Int -> Text -> Text toWidth n x = T.take n (T.justifyLeft n ' ' x)@@ -56,6 +56,10 @@ moreMsg :: Msg moreMsg = "--more-- " +-- | The \"end of screenfuls of text\" mark.+endMsg :: Msg+endMsg = "--end-- "+ -- | The confirmation request message. yesnoMsg :: Msg yesnoMsg = "[yn]"@@ -181,8 +185,11 @@ -- | Render history as many lines of text, wrapping if necessary. renderHistory :: History -> Overlay renderHistory (History h) =- let w = fst normalLevelBound + 1- in toOverlay $ concatMap (splitReportForHistory w) h+ let (x, y) = normalLevelBound+ screenLength = y + 2+ reportLines = concatMap (splitReportForHistory (x + 1)) $ reverse h+ padding = screenLength - length reportLines `mod` screenLength+ in toOverlay $ replicate padding "" ++ reportLines splitReportForHistory :: X -> (Time, Report) -> [Text] splitReportForHistory w (time, r) =@@ -206,7 +213,7 @@ type ScreenLine = U.Vector Int32 toScreenLine :: Text -> ScreenLine-toScreenLine t = let f c = AttrChar defAttr c+toScreenLine t = let f = AttrChar defAttr in encodeLine $ map f $ T.unpack t encodeLine :: [AttrChar] -> ScreenLine@@ -229,14 +236,17 @@ toOverlay :: [Text] -> Overlay toOverlay = let lxsize = fst normalLevelBound + 1 -- TODO- in Overlay . map toScreenLine . map (truncateMsg lxsize)+ in Overlay . map (toScreenLine . truncateMsg lxsize) -- | Split an overlay into a slideshow in which each overlay, -- prefixed by @msg@ and postfixed by @moreMsg@ except for the last one, -- fits on the screen wrt height (but lines may be too wide).-splitOverlay :: Bool -> Y -> Overlay -> Overlay -> Slideshow+splitOverlay :: Maybe Bool -> Y -> Overlay -> Overlay -> Slideshow splitOverlay onBlank yspace (Overlay msg) (Overlay ls) = let len = length msg+ endB = [ toScreenLine+ $ endMsg <> "[press PGUP to see previous, ESC to cancel]"+ | onBlank == Just False ] in if len >= yspace then -- no space left for @ls@ Slideshow (onBlank, [Overlay $ take (yspace - 1) msg@@ -244,7 +254,7 @@ else let splitO over = let (pre, post) = splitAt (yspace - 1) $ msg ++ over in if null (drop 1 post) -- (don't call @length@ on @ls@)- then [Overlay $ msg ++ over] -- all fits on one screen+ then [Overlay $ msg ++ over ++ endB] -- all fits on screen else let rest = splitO post in Overlay (pre ++ [toScreenLine moreMsg]) : rest in Slideshow (onBlank, splitO ls)@@ -252,19 +262,19 @@ -- | A few overlays, displayed one by one upon keypress. -- When displayed, they are trimmed, not wrapped -- and any lines below the lower screen edge are not visible.--- If the boolean flag is set, the overlay is displayed over a blank screen,--- including the bottom lines.-newtype Slideshow = Slideshow {slideshow :: (Bool, [Overlay])}+-- If the first pair element is not @Nothing@, the overlay is displayed+-- over a blank screen, including the bottom lines. The boolean flag+-- then indicates whether to start at the topmost screenful or bottommost.+newtype Slideshow = Slideshow {slideshow :: (Maybe Bool, [Overlay])} deriving (Show, Eq) instance Monoid Slideshow where- mempty = Slideshow (False, [])- mappend (Slideshow (b1, l1)) (Slideshow (b2, l2)) =- Slideshow (b1 || b2, l1 ++ l2)+ mempty = Slideshow (Nothing, [])+ mappend (Slideshow (b1, l1)) (Slideshow (_, l2)) = Slideshow (b1, l1 ++ l2) -- | Declare the list of raw overlays to be fit for display on the screen. -- In particular, current @Report@ is eiter empty or unimportant -- or contained in the overlays and if any vertical or horizontal -- trimming of the overlays happens, this is intended.-toSlideshow :: Bool -> [[Text]] -> Slideshow+toSlideshow :: Maybe Bool -> [[Text]] -> Slideshow toSlideshow onBlank l = Slideshow (onBlank, map toOverlay l)
Game/LambdaHack/Common/Point.hs view
@@ -1,13 +1,16 @@+{-# LANGUAGE DeriveGeneric #-} -- | Basic operations on 2D points represented as linear offsets. module Game.LambdaHack.Common.Point- ( X, Y, Point(..), maxLevelDimExponent+ ( X, Y, Point(..), dummyPoint, maxLevelDimExponent , chessDist, euclidDistSq, adjacent, inside, bla, fromTo ) where +import Control.DeepSeq import Control.Exception.Assert.Sugar import Data.Binary import Data.Bits (unsafeShiftL, unsafeShiftR, (.&.)) import Data.Int (Int32)+import GHC.Generics (Generic) -- | Spacial dimension for points and vectors. type X = Int@@ -22,7 +25,7 @@ { px :: !X , py :: !Y }- deriving (Eq, Ord)+ deriving (Read, Eq, Ord, Generic) instance Show Point where show (Point x y) = show (x, y)@@ -31,6 +34,8 @@ put = put . (fromIntegral :: Int -> Int32) . fromEnum get = fmap (toEnum . (fromIntegral :: Int32 -> Int)) get +instance NFData Point+ -- This conversion cannot be used for PointArray indexing, -- because it is not contiguous --- we don't know the horizontal -- width of the levels nor of the screen.@@ -39,6 +44,9 @@ fromEnum = fromEnumPoint toEnum = toEnumPoint +dummyPoint :: Point+dummyPoint = Point (-10000) (-10000)+ -- | The maximum number of bits for level X and Y dimension (16). -- The value is chosen to support architectures with 32-bit Ints. maxLevelDimExponent :: Int@@ -129,8 +137,8 @@ | z0 <= z1 = [z0..z1] | otherwise = [z0,z0-1..z1] result- | x0 == x1 = map (\ y -> Point x0 y) (fromTo1 y0 y1)- | y0 == y1 = map (\ x -> Point x y0) (fromTo1 x0 x1)+ | x0 == x1 = map (Point x0) (fromTo1 y0 y1)+ | y0 == y1 = map (`Point` y0) (fromTo1 x0 x1) | otherwise = assert `failure` "diagonal fromTo" `twith` ((x0, y0), (x1, y1)) in result
Game/LambdaHack/Common/PointArray.hs view
@@ -2,17 +2,20 @@ module Game.LambdaHack.Common.PointArray ( Array , (!), (//), replicateA, replicateMA, generateA, generateMA, sizeA- , foldlA, ifoldlA, mapA, imapA, mapWithKeyM_A- , minIndexA, minLastIndexA, maxIndexA, maxLastIndexA, forceA+ , foldlA, ifoldlA, mapA, imapA, mapWithKeyMA+ , safeSetA, unsafeSetA, unsafeUpdateA+ , minIndexA, minLastIndexA, minIndexesA, maxIndexA, maxLastIndexA, forceA ) where import Control.Arrow ((***)) import Control.Monad+import Control.Monad.ST.Strict import Data.Binary import Data.Vector.Binary () import qualified Data.Vector.Fusion.Stream as Stream import qualified Data.Vector.Generic as G import qualified Data.Vector.Unboxed as U+import qualified Data.Vector.Unboxed.Mutable as VM import Game.LambdaHack.Common.Point @@ -59,6 +62,14 @@ (//) Array{..} l = let v = avector U.// map (pindex axsize *** cnv) l in Array{avector = v, ..} +unsafeUpdateA :: Enum c => Array c -> [(Point, c)] -> Array c+{-# INLINE unsafeUpdateA #-}+unsafeUpdateA Array{..} l = runST $ do+ vThawed <- U.unsafeThaw avector+ mapM_ (\(p, c) -> VM.write vThawed (pindex axsize p) (cnv c)) l+ vFrozen <- U.unsafeFreeze vThawed+ return $! Array{avector = vFrozen, ..}+ -- | Create an array from a replicated element. replicateA :: Enum c => X -> Y -> c -> Array c {-# INLINE replicateA #-}@@ -117,12 +128,27 @@ let v = U.imap (\n c -> cnv $ f (punindex axsize n) (cnv c)) avector in Array{avector = v, ..} +-- | Set all elements to the given value, in place.+unsafeSetA :: Enum c => c -> Array c -> Array c+{-# INLINE unsafeSetA #-}+unsafeSetA c Array{..} = runST $ do+ vThawed <- U.unsafeThaw avector+ VM.set vThawed (cnv c)+ vFrozen <- U.unsafeFreeze vThawed+ return $! Array{avector = vFrozen, ..}++-- | Set all elements to the given value, in place, if possible.+safeSetA :: Enum c => c -> Array c -> Array c+{-# INLINE safeSetA #-}+safeSetA c Array{..} =+ Array{avector = U.modify (\v -> VM.set v (cnv c)) avector, ..}+ -- | Map monadically over an array (function applied to each element -- and its index) and ignore the results.-mapWithKeyM_A :: Enum c => Monad m+mapWithKeyMA :: Enum c => Monad m => (Point -> c -> m ()) -> Array c -> m ()-{-# INLINE mapWithKeyM_A #-}-mapWithKeyM_A f Array{..} =+{-# INLINE mapWithKeyMA #-}+mapWithKeyMA f Array{..} = U.ifoldl' (\a n c -> a >> f (punindex axsize n) (cnv c)) (return ()) avector@@ -143,6 +169,18 @@ $ avector where imin (i, x) (j, y) = i `seq` j `seq` if x >= y then (j, y) else (i, x)++-- | Yield the point coordinates of all the minimum elements of the array.+-- The array may not be empty.+minIndexesA :: Enum c => Array c -> [Point]+{-# INLINE minIndexesA #-}+minIndexesA Array{..} =+ map (punindex axsize)+ $ Stream.foldl' imin [] . Stream.indexed . G.stream+ $ avector+ where+ imin acc (i, x) = i `seq` if x == minE then i : acc else acc+ minE = cnv $ U.minimum avector -- | Yield the point coordinates of the first maximum element of the array. -- The array may not be empty.
Game/LambdaHack/Common/Random.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE DeriveGeneric #-} -- | Representation of probabilities and random computations. module Game.LambdaHack.Common.Random ( -- * The @Rng@ monad@@ -77,12 +76,13 @@ -- Note that at the first level, the scaled dice are always ignored. castDice :: AbsDepth -> AbsDepth -> Dice.Dice -> Rnd Int castDice (AbsDepth n) (AbsDepth depth) dice = do- assert (n >= 0 && n <= depth `blame` "invalid depth for dice rolls"- `twith` (n, depth)) skip+ let !_A = assert (n >= 0 && n <= depth+ `blame` "invalid depth for dice rolls"+ `twith` (n, depth)) () dc <- frequency $ Dice.diceConst dice dl <- frequency $ Dice.diceLevel dice return $! (dc + (dl * max 0 (n - 1)) `div` max 1 (depth - 1))- * Dice.diceScale dice+ * Dice.diceMult dice -- | Cast dice scaled with current level depth and return @True@ -- if the results is greater than 50.
Game/LambdaHack/Common/Request.hs view
@@ -9,17 +9,20 @@ , permittedPrecious, permittedProject, permittedApply ) where +import Data.Maybe import Data.Text (Text) import Game.LambdaHack.Atomic import Game.LambdaHack.Common.Ability import Game.LambdaHack.Common.Actor+import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.ItemStrongest import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.Point+import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Vector import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.ModeKind@@ -58,16 +61,15 @@ -- | Client-server requests that take game time. Sent by both AI and UI clients. data RequestTimed :: Ability -> * where- ReqMove :: !Vector -> RequestTimed AbMove- ReqMelee :: !ActorId -> !ItemId -> !CStore -> RequestTimed AbMelee- ReqDisplace :: !ActorId -> RequestTimed AbDisplace- ReqAlter :: !Point -> !(Maybe TK.Feature) -> RequestTimed AbAlter- ReqWait :: RequestTimed AbWait- ReqMoveItem :: !ItemId -> !Int -> !CStore -> !CStore- -> RequestTimed AbMoveItem- ReqProject :: !Point -> !Int -> !ItemId -> !CStore -> RequestTimed AbProject- ReqApply :: !ItemId -> !CStore -> RequestTimed AbApply- ReqTrigger :: !(Maybe TK.Feature) -> RequestTimed AbTrigger+ ReqMove :: !Vector -> RequestTimed 'AbMove+ ReqMelee :: !ActorId -> !ItemId -> !CStore -> RequestTimed 'AbMelee+ ReqDisplace :: !ActorId -> RequestTimed 'AbDisplace+ ReqAlter :: !Point -> !(Maybe TK.Feature) -> RequestTimed 'AbAlter+ ReqWait :: RequestTimed 'AbWait+ ReqMoveItems :: ![(ItemId, Int, CStore, CStore)] -> RequestTimed 'AbMoveItem+ ReqProject :: !Point -> !Int -> !ItemId -> !CStore -> RequestTimed 'AbProject+ ReqApply :: !ItemId -> !CStore -> RequestTimed 'AbApply+ ReqTrigger :: !(Maybe TK.Feature) -> RequestTimed 'AbTrigger deriving instance Show (RequestTimed a) @@ -90,6 +92,7 @@ | ApplyUnskilled | ApplyRead | ApplyOutOfReach+ | ApplyCharging | ItemNothing | ItemNotCalm | NotCalmPrecious@@ -97,6 +100,7 @@ | ProjectBlockTerrain | ProjectBlockActor | ProjectUnskilled+ | ProjectNotRanged | ProjectFragile | ProjectOutOfReach | TriggerNothing@@ -123,6 +127,7 @@ ApplyUnskilled -> False -- unidentified skill items ApplyRead -> False -- unidentified skill items ApplyOutOfReach -> True+ ApplyCharging -> False -- if aspects unknown, charging unknown ItemNothing -> True ItemNotCalm -> False -- unidentified skill items NotCalmPrecious -> False -- unidentified skill items@@ -130,6 +135,7 @@ ProjectBlockTerrain -> True -- adjacent terrain always visible ProjectBlockActor -> True -- adjacent actor always visible ProjectUnskilled -> False -- unidentified skill items+ ProjectNotRanged -> False -- unidentified skill items ProjectFragile -> False -- unidentified skill items ProjectOutOfReach -> True TriggerNothing -> True -- terrain underneath always visibl@@ -146,16 +152,17 @@ DisplaceProjectiles -> "trying to displace multiple projectiles" DisplaceDying -> "trying to displace a dying foe" DisplaceBraced -> "trying to displace a braced foe"- DisplaceImmobile -> "trying to displace an immoblie foe"+ DisplaceImmobile -> "trying to displace an immobile foe" DisplaceSupported -> "trying to displace a supported foe" AlterDistant -> "trying to alter a distant tile" AlterBlockActor -> "blocked by an actor" AlterBlockItem -> "jammed by an item" AlterNothing -> "wasting time on altering nothing" EqpOverfull -> "cannot equip any more items"- ApplyUnskilled -> "unskilled actors cannot read"- ApplyRead -> "to read an item requires activate skill 2"+ ApplyUnskilled -> "unskilled actors cannot apply items"+ ApplyRead -> "activating this kind of items requires skill level 2" ApplyOutOfReach -> "cannot apply an item out of reach"+ ApplyCharging -> "cannot apply an item that is still charging" ItemNothing -> "wasting time on void item manipulation" ItemNotCalm -> "you are too alarmed to sort through the shared stash" NotCalmPrecious -> "you are too alarmed to handle such an exquisite item"@@ -163,13 +170,14 @@ ProjectBlockTerrain -> "aiming obstructed by terrain" ProjectBlockActor -> "aiming blocked by an actor" ProjectUnskilled -> "unskilled actors cannot aim"- ProjectFragile -> "to lob a fragile item requires fling skill 2"+ ProjectNotRanged -> "to fling a non-missile requires fling skill 2"+ ProjectFragile -> "to lob a fragile item requires fling skill 3" ProjectOutOfReach -> "cannot aim an item out of reach" TriggerNothing -> "wasting time on triggering nothing" NoChangeDunLeader -> "no manual level change for your team" NoChangeLvlLeader -> "no manual leader change for your team" --- The item should not be activated nor thrown because it's too delicate+-- The item should not be applied nor thrown because it's too delicate -- to operate when not calm or becuse it's too precious to identify by use. permittedPrecious :: Bool -> Bool -> ItemFull -> Either ReqFailure Bool permittedPrecious calm10 forced itemFull =@@ -185,11 +193,15 @@ permittedProject triggerSyms forced skill itemFull@ItemFull{itemBase} b activeItems = let calm10 = calmEnough10 b activeItems+ mhurtRanged = strengthFromEqpSlot IK.EqpSlotAddHurtRanged itemFull in if not forced && skill < 1 then Left ProjectUnskilled else if not forced+ && isNothing mhurtRanged+ && skill < 2 then Left ProjectNotRanged+ else if not forced && IK.Fragile `elem` jfeature itemBase- && skill < 2 then Left ProjectFragile+ && skill < 3 then Left ProjectFragile else let legal = permittedPrecious calm10 forced itemFull in case legal of@@ -210,19 +222,22 @@ else jsymbol itemBase `elem` triggerSyms in hasEffects && permittedSlot -permittedApply :: [Char] -> Int -> ItemFull -> Actor -> [ItemFull]+permittedApply :: [Char] -> Time -> Int -> ItemFull -> Actor -> [ItemFull] -> Either ReqFailure Bool-permittedApply triggerSyms skill itemFull@ItemFull{itemBase} b activeItems =+permittedApply triggerSyms localTime skill itemFull@ItemFull{itemBase}+ b activeItems = let calm10 = calmEnough10 b activeItems in if skill < 1 then Left ApplyUnskilled- else if jsymbol itemBase == '?'- && skill < 2 then Left ApplyRead- else- let legal = permittedPrecious calm10 False itemFull- in case legal of- Left{} -> legal- Right False -> legal- Right True -> Right $- if ' ' `elem` triggerSyms- then IK.Applicable `elem` jfeature itemBase- else jsymbol itemBase `elem` triggerSyms+ else if jsymbol itemBase == '?' && skill < 2 then Left ApplyRead+ -- We assume if the item has a timeout, all or most of interesting effects+ -- are under Recharging, so no point activating if not recharged.+ else if not $ hasCharge localTime itemFull+ then Left ApplyCharging+ else let legal = permittedPrecious calm10 False itemFull+ in case legal of+ Left{} -> legal+ Right False -> legal+ Right True -> Right $+ if ' ' `elem` triggerSyms+ then IK.Applicable `elem` jfeature itemBase+ else jsymbol itemBase `elem` triggerSyms
Game/LambdaHack/Common/Save.hs view
@@ -103,7 +103,7 @@ handler e = do let msg = "Restore failed. The error message is:" <+> (T.unwords . T.lines) (tshow e)- delayPrint $ msg+ delayPrint msg return Nothing either handler return res
Game/LambdaHack/Common/State.hs view
@@ -3,7 +3,7 @@ ( -- * Basic game state, local or global State -- * State components- , sdungeon, stotalDepth, sactorD, sitemD, sfactionD, stime, scops, shigh+ , sdungeon, stotalDepth, sactorD, sitemD, sfactionD, stime, scops, shigh, sgameModeId -- * State operations , defStateGlobal, emptyState, localFromGlobal , updateDungeon, updateDepth, updateActorD, updateItemD@@ -24,6 +24,8 @@ import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.Time+import Game.LambdaHack.Content.ItemKind (ItemKind)+import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Content.TileKind (TileKind) -- | View on game state. "Remembered" fields carry a subset of the info@@ -37,18 +39,20 @@ , _sfactionD :: !FactionDict -- ^ remembered sides still in game , _stime :: !Time -- ^ global game time , _scops :: Kind.COps -- ^ remembered content- , _shigh :: !HighScore.ScoreTable -- ^ high score table+ , _shigh :: !HighScore.ScoreDict -- ^ high score table+ , _sgameModeId :: !(Kind.Id ModeKind) -- ^ current game mode } deriving (Show, Eq) -- TODO: add a flag 'fresh' and when saving levels, don't save -- and when loading regenerate this level. unknownLevel :: Kind.COps -> AbsDepth -> X -> Y- -> Text -> ([Point], [Point]) -> Int -> Int -> Int -> Bool+ -> Text -> ([Point], [Point]) -> Int+ -> (Freqs ItemKind) -> Int -> Int -> [Point] -> Level unknownLevel Kind.COps{cotile=Kind.Ops{ouniqGroup}} ldepth lxsize lysize ldesc lstair lclear- lsecret lhidden lescape =+ litemFreq lsecret lhidden lescape = let unknownId = ouniqGroup "unknown space" outerId = ouniqGroup "basic outer fence" in Level { ldepth@@ -67,7 +71,7 @@ , lactorCoeff = 0 , lactorFreq = [] , litemNum = 0- , litemFreq = []+ , litemFreq -- = [] --- TODO: field needed by factionLoots , lsecret , lhidden , lescape@@ -84,10 +88,10 @@ in unknownMap PointArray.// outerUpdate -- | Initial complete global game state.-defStateGlobal :: Dungeon -> AbsDepth- -> FactionDict -> Kind.COps -> HighScore.ScoreTable+defStateGlobal :: Dungeon -> AbsDepth -> FactionDict -> Kind.COps+ -> HighScore.ScoreDict -> Kind.Id ModeKind -> State-defStateGlobal _sdungeon _stotalDepth _sfactionD _scops _shigh =+defStateGlobal _sdungeon _stotalDepth _sfactionD _scops _shigh _sgameModeId = State { _sactorD = EM.empty , _sitemD = EM.empty@@ -107,11 +111,14 @@ , _stime = timeZero , _scops = undefined , _shigh = HighScore.empty+ , _sgameModeId = toEnum 0 -- the initial value is unused } -- TODO: make lstair secret until discovered; use this later on for -- goUp in targeting mode (land on stairs of on the same location up a level -- if this set of stsirs is unknown).+-- TODO: RNG should be secret, too, but we also want it to be deterministic,+-- to aid in bug replication -- | Local state created by removing secret information from global -- state components. localFromGlobal :: State -> State@@ -120,8 +127,8 @@ { _sdungeon = EM.map (\Level{..} -> unknownLevel _scops ldepth lxsize lysize ldesc lstair lclear- lsecret lhidden lescape)- _sdungeon+ litemFreq lsecret lhidden lescape)+ _sdungeon , .. } @@ -174,9 +181,12 @@ scops :: State -> Kind.COps scops = _scops -shigh :: State -> HighScore.ScoreTable+shigh :: State -> HighScore.ScoreDict shigh = _shigh +sgameModeId :: State -> Kind.Id ModeKind+sgameModeId = _sgameModeId+ instance Binary State where put State{..} = do put _sdungeon@@ -186,6 +196,7 @@ put _sfactionD put _stime put _shigh+ put _sgameModeId get = do _sdungeon <- get _stotalDepth <- get@@ -194,5 +205,6 @@ _sfactionD <- get _stime <- get _shigh <- get+ _sgameModeId <- get let _scops = undefined -- overwritten by recreated cops return $! State{..}
Game/LambdaHack/Common/Tile.hs view
@@ -16,15 +16,18 @@ module Game.LambdaHack.Common.Tile ( SmellTime , kindHasFeature, hasFeature- , isClear, isLit, isWalkable, isPassableKind, isPassable, isDoor, isSuspect+ , isClear, isLit, isWalkable+ , isPassable, isPassableNoSuspect, isDoor, isSuspect , isExplorable, lookSimilar, speedup , openTo, closeTo, embedItems, causeEffects, revealAs, hideAs , isOpenable, isClosable, isChangeable, isEscape, isStair, ascendTo #ifdef EXPOSE_INTERNAL+ -- * Internal operations , TileSpeedup(..), Tab, createTab, accessTab #endif ) where +import Control.Applicative import Control.Exception.Assert.Sugar import qualified Data.Array.Unboxed as A import Data.Maybe@@ -45,13 +48,14 @@ type instance Kind.Speedup TileKind = TileSpeedup data TileSpeedup = TileSpeedup- { isClearTab :: !Tab- , isLitTab :: !Tab- , isWalkableTab :: !Tab- , isPassableTab :: !Tab- , isDoorTab :: !Tab- , isSuspectTab :: !Tab- , isChangeableTab :: !Tab+ { isClearTab :: !Tab+ , isLitTab :: !Tab+ , isWalkableTab :: !Tab+ , isPassableTab :: !Tab+ , isPassableNoSuspectTab :: !Tab+ , isDoorTab :: !Tab+ , isSuspectTab :: !Tab+ , isChangeableTab :: !Tab } newtype Tab = Tab (A.UArray (Kind.Id TileKind) Bool)@@ -80,16 +84,14 @@ -- Essential for efficiency of "FOV", hence tabulated. isClear :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool {-# INLINE isClear #-}-isClear Kind.Ops{ospeedup = Just TileSpeedup{isClearTab}} =- \k -> accessTab isClearTab k+isClear Kind.Ops{ospeedup = Just TileSpeedup{isClearTab}} = accessTab isClearTab isClear cotile = assert `failure` "no speedup" `twith` Kind.obounds cotile -- | Whether a tile is lit on its own. -- Essential for efficiency of "Perception", hence tabulated. isLit :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool {-# INLINE isLit #-}-isLit Kind.Ops{ospeedup = Just TileSpeedup{isLitTab}} =- \k -> accessTab isLitTab k+isLit Kind.Ops{ospeedup = Just TileSpeedup{isLitTab}} = accessTab isLitTab isLit cotile = assert `failure` "no speedup" `twith` Kind.obounds cotile -- | Whether actors can walk into a tile.@@ -97,7 +99,7 @@ isWalkable :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool {-# INLINE isWalkable #-} isWalkable Kind.Ops{ospeedup = Just TileSpeedup{isWalkableTab}} =- \k -> accessTab isWalkableTab k+ accessTab isWalkableTab isWalkable cotile = assert `failure` "no speedup" `twith` Kind.obounds cotile -- | Whether actors can walk into a tile, perhaps opening a door first,@@ -106,15 +108,25 @@ isPassable :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool {-# INLINE isPassable #-} isPassable Kind.Ops{ospeedup = Just TileSpeedup{isPassableTab}} =- \k -> accessTab isPassableTab k+ accessTab isPassableTab isPassable cotile = assert `failure` "no speedup" `twith` Kind.obounds cotile +-- | Whether actors can walk into a tile, perhaps opening a door first,+-- perhaps a hidden door.+-- Essential for efficiency of pathfinding, hence tabulated.+isPassableNoSuspect :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool+{-# INLINE isPassableNoSuspect #-}+isPassableNoSuspect Kind.Ops{ospeedup =+ Just TileSpeedup{isPassableNoSuspectTab}} =+ accessTab isPassableNoSuspectTab+isPassableNoSuspect cotile =+ assert `failure` "no speedup" `twith` Kind.obounds cotile+ -- | Whether a tile is a door, open or closed. -- Essential for efficiency of pathfinding, hence tabulated. isDoor :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool {-# INLINE isDoor #-}-isDoor Kind.Ops{ospeedup = Just TileSpeedup{isDoorTab}} =- \k -> accessTab isDoorTab k+isDoor Kind.Ops{ospeedup = Just TileSpeedup{isDoorTab}} = accessTab isDoorTab isDoor cotile = assert `failure` "no speedup" `twith` Kind.obounds cotile -- | Whether a tile is suspect.@@ -122,7 +134,7 @@ isSuspect :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool {-# INLINE isSuspect #-} isSuspect Kind.Ops{ospeedup = Just TileSpeedup{isSuspectTab}} =- \k -> accessTab isSuspectTab k+ accessTab isSuspectTab isSuspect cotile = assert `failure` "no speedup" `twith` Kind.obounds cotile -- | Whether a tile kind (specified by its id) has a ChangeTo feature.@@ -130,7 +142,7 @@ isChangeable :: Kind.Ops TileKind -> Kind.Id TileKind -> Bool {-# INLINE isChangeable #-} isChangeable Kind.Ops{ospeedup = Just TileSpeedup{isChangeableTab}} =- \k -> accessTab isChangeableTab k+ accessTab isChangeableTab isChangeable cotile = assert `failure` "no speedup" `twith` Kind.obounds cotile -- | Whether one can easily explore a tile, possibly finding a treasure@@ -163,7 +175,8 @@ $ kindHasFeature TK.Clear isLitTab = createTab cotile $ not . kindHasFeature TK.Dark isWalkableTab = createTab cotile $ kindHasFeature TK.Walkable- isPassableTab = createTab cotile isPassableKind+ isPassableTab = createTab cotile $ isPassableKind True+ isPassableNoSuspectTab = createTab cotile $ isPassableKind False isDoorTab = createTab cotile $ \tk -> let getTo TK.OpenTo{} = True getTo TK.CloseTo{} = True@@ -176,12 +189,12 @@ in any getTo $ TK.tfeature tk in TileSpeedup {..} -isPassableKind :: TileKind -> Bool-isPassableKind tk =+isPassableKind :: Bool -> TileKind -> Bool+isPassableKind passSuspect tk = let getTo TK.Walkable = True getTo TK.OpenTo{} = True getTo TK.ChangeTo{} = True -- can change to passable and may have loot- getTo TK.Suspect = True+ getTo TK.Suspect | passSuspect = True getTo _ = False in any getTo $ TK.tfeature tk @@ -193,8 +206,8 @@ [] -> return t groups -> do grp <- oneOf groups- fmap (fromMaybe $ assert `failure` grp)- $ opick grp (const True)+ (fromMaybe $ assert `failure` grp)+ <$> opick grp (const True) closeTo :: Kind.Ops TileKind -> Kind.Id TileKind -> Rnd (Kind.Id TileKind) closeTo Kind.Ops{okind, opick} t = do@@ -204,8 +217,8 @@ [] -> return t groups -> do grp <- oneOf groups- fmap (fromMaybe $ assert `failure` grp)- $ opick grp (const True)+ (fromMaybe $ assert `failure` grp)+ <$> opick grp (const True) embedItems :: Kind.Ops TileKind -> Kind.Id TileKind -> [GroupName ItemKind] embedItems Kind.Ops{okind} t =@@ -227,8 +240,8 @@ [] -> return t groups -> do grp <- oneOf groups- fmap (fromMaybe $ assert `failure` grp)- $ opick grp (const True)+ (fromMaybe $ assert `failure` grp)+ <$> opick grp (const True) hideAs :: Kind.Ops TileKind -> Kind.Id TileKind -> Kind.Id TileKind hideAs Kind.Ops{okind, ouniqGroup} t =
Game/LambdaHack/Common/Time.hs view
@@ -186,11 +186,10 @@ -- 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.- multiple2M = sInMs * if v > 2 * sInMs- then 2 * (v `div` (2 * sInMs))- else v `div` sInMs- minimumSpeed = if mpMs == minimalSpeed then minimalSpeed else sInMs- in Speed $ max minimumSpeed multiple2M+ multiple2M = if v > 2 * sInMs+ then 2 * sInMs * (v `div` (2 * sInMs))+ else v+ in Speed $ max minimalSpeed multiple2M -- | Calculate maximum range in meters of a projectile from its speed. -- See <https://github.com/LambdaHack/LambdaHack/wiki/Item-statistics>.@@ -203,4 +202,4 @@ rangeFromSpeedAndLinger :: Speed -> Int -> Int rangeFromSpeedAndLinger speed linger = let range = rangeFromSpeed speed- in linger * range `div` 100+ in linger * range `divUp` 100
Game/LambdaHack/Common/Vector.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE DeriveGeneric #-} -- | Basic operations on 2D vectors represented in an efficient, -- but not unique, way. module Game.LambdaHack.Common.Vector@@ -9,12 +9,13 @@ , RadianAngle, rotate, towards ) where +import Control.DeepSeq import Control.Exception.Assert.Sugar import Data.Binary import qualified Data.EnumMap.Strict as EM import Data.Int (Int32)-import Data.Maybe import Data.Text (Text)+import GHC.Generics (Generic) import Game.LambdaHack.Common.Point @@ -25,7 +26,7 @@ { vx :: !X , vy :: !Y }- deriving (Eq, Ord, Show, Read)+ deriving (Eq, Ord, Show, Read, Generic) instance Binary Vector where put = put . (fromIntegral :: Int -> Int32) . fromEnum@@ -35,6 +36,8 @@ fromEnum = fromEnumVector toEnum = toEnumVector +instance NFData Vector+ -- | Maximal supported vector X and Y coordinates. maxVectorDim :: Int {-# INLINE maxVectorDim #-}@@ -48,11 +51,9 @@ {-# INLINE toEnumVector #-} toEnumVector n = let (y, x) = n `quotRem` (2 ^ maxLevelDimExponent)- (vx, vy) = if x > maxVectorDim- then (x - 2 ^ maxLevelDimExponent, y + 1)- else if x < - maxVectorDim- then (x + 2 ^ maxLevelDimExponent, y - 1)- else (x, y)+ (vx, vy) | x > maxVectorDim = (x - 2 ^ maxLevelDimExponent, y + 1)+ | x < - maxVectorDim = (x + 2 ^ maxLevelDimExponent, y - 1)+ | otherwise = (x, y) in Vector{..} -- | Tells if a vector has length 1 in the chessboard metric.@@ -88,6 +89,7 @@ -- | Vectors of all unit moves in the chessboard metric, -- clockwise, starting north-west. moves :: [Vector]+{-# NOINLINE moves #-} moves = map (uncurry Vector) [(-1, -1), (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0)]@@ -97,8 +99,8 @@ compassText :: Vector -> Text compassText v = let m = EM.fromList $ zip moves moveTexts- in fromMaybe (assert `failure` "not a unit vector"- `twith` v) $ EM.lookup v m+ assFail = assert `failure` "not a unit vector" `twith` v+ in EM.findWithDefault assFail v m -- | Vectors of all cardinal direction unit moves, clockwise, starting north. movesCardinal :: [Vector]@@ -113,9 +115,16 @@ -> Point -- ^ position to find neighbours of -> [Point] vicinity lxsize lysize p =+ if inside p (1, 1, lxsize - 2, lysize - 2)+ then vicinityUnsafe p+ else [ res | dxy <- moves+ , let res = shift p dxy+ , inside res (0, 0, lxsize - 1, lysize - 1) ]++vicinityUnsafe :: Point -> [Point]+vicinityUnsafe p = [ res | dxy <- moves- , let res = shift p dxy- , inside res (0, 0, lxsize - 1, lysize - 1) ]+ , let res = shift p dxy ] -- | All (4 at most) cardinal direction neighbours of a point within an area. vicinityCardinal :: X -> Y -- ^ limit the search to this area
Game/LambdaHack/Content/CaveKind.hs view
@@ -85,6 +85,6 @@ -- of a cave with a given name. validateAllCaveKind :: [CaveKind] -> [Text] validateAllCaveKind lk =- if any (\k -> maybe False (> 0) $ lookup "campaign random" $ cfreq k) lk+ if any (maybe False (> 0) . lookup "campaign random" . cfreq) lk then [] else ["no cave defined for \"campaign random\""]
Game/LambdaHack/Content/ItemKind.hs view
@@ -1,20 +1,23 @@-{-# LANGUAGE DeriveFunctor, DeriveGeneric #-}+{-# LANGUAGE DeriveFoldable, DeriveFunctor, DeriveGeneric, DeriveTraversable #-} -- | The type of kinds of weapons, treasure, organs, blasts and actors. module Game.LambdaHack.Content.ItemKind ( ItemKind(..) , Effect(..), TimerDice(..) , Aspect(..), ThrowMod(..) , Feature(..), EqpSlot(..)- , aspectTrav+ , slotName , toVelocity, toLinger, toOrganGameTurn, toOrganActorTurn, toOrganNone , validateSingleItemKind, validateAllItemKind ) where -import qualified Control.Monad.State as St+import Control.DeepSeq import Data.Binary+import Data.Foldable (Foldable) import Data.Hashable (Hashable)+import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T+import Data.Traversable (Traversable) import GHC.Generics (Generic) import qualified NLP.Miniutter.English as MU @@ -22,6 +25,7 @@ import qualified Game.LambdaHack.Common.Dice as Dice import Game.LambdaHack.Common.Flavour import Game.LambdaHack.Common.Misc+import Game.LambdaHack.Common.Msg -- | Item properties that are fixed for a given kind of items. data ItemKind = ItemKind@@ -52,7 +56,7 @@ -- Ordinary effects. NoEffect !Text | Hurt !Dice.Dice- | Burn !Int -- Dice.Dice? generalize to other elements? ignite terrain?+ | Burn !Dice.Dice -- TODO: generalize to other elements? ignite terrain? | Explode !(GroupName ItemKind) -- ^ explode, producing this group of blasts | RefillHP !Int@@ -73,8 +77,8 @@ -- the store with the given random timer | DropItem !CStore !(GroupName ItemKind) !Bool -- ^ @DropItem CGround x True@ means stomp on items- | PolyItem !CStore- | Identify !CStore+ | PolyItem+ | Identify | SendFlying !ThrowMod | PushActor !ThrowMod | PullActor !ThrowMod@@ -89,6 +93,8 @@ -- Periodic activation, unless Durable deriving (Show, Read, Eq, Ord, Generic) +instance NFData Effect+ data TimerDice = TimerNone | TimerGameTurn !Dice.Dice@@ -102,14 +108,17 @@ show (TimerActorTurn nDm) = show nDm ++ " " ++ if nDm == 1 then "move" else "moves" +instance NFData TimerDice+ -- | Aspects of items. Those that are named @Add*@ are additive -- (starting at 0) for all items wielded by an actor and they affect the actor. data Aspect a =- Periodic -- ^ in equipment, activate as often as @Timeout@ permits+ Unique -- ^ at most one copy can ever be generated+ | Periodic -- ^ in equipment, apply as often as @Timeout@ permits | Timeout !a -- ^ some effects will be disabled until item recharges | AddHurtMelee !a -- ^ percentage damage bonus in melee- | AddArmorMelee !a -- ^ percentage armor bonus against melee | AddHurtRanged !a -- ^ percentage damage bonus in ranged+ | AddArmorMelee !a -- ^ percentage armor bonus against melee | AddArmorRanged !a -- ^ percentage armor bonus against ranged | AddMaxHP !a -- ^ maximal hp | AddMaxCalm !a -- ^ maximal calm@@ -118,7 +127,7 @@ | AddSight !a -- ^ FOV radius, where 1 means a single tile | AddSmell !a -- ^ smell radius, where 1 means a single tile | AddLight !a -- ^ light radius, where 1 means a single tile- deriving (Show, Read, Eq, Ord, Generic, Functor)+ deriving (Show, Read, Eq, Ord, Generic, Functor, Foldable, Traversable) -- | Parameters modifying a throw. Not additive and don't start at 0. data ThrowMod = ThrowMod@@ -127,6 +136,8 @@ } deriving (Show, Read, Eq, Ord, Generic) +instance NFData ThrowMod+ -- | Features of item. Affect only the item in question, not the actor, -- and so not additive in any sense. data Feature =@@ -155,7 +166,7 @@ | EqpSlotAddSight | EqpSlotAddSmell | EqpSlotAddLight- | EqpSlotWeapon+ | EqpSlotWeapon -- ^ a hack exclusively for AI that shares weapons deriving (Show, Eq, Ord, Generic) instance Hashable Effect@@ -182,44 +193,21 @@ instance Binary EqpSlot --- TODO: Traversable?--- | Transform an aspect using a stateful function.-aspectTrav :: Aspect a -> (a -> St.State s b) -> St.State s (Aspect b)-aspectTrav Periodic _ = return Periodic-aspectTrav (Timeout a) f = do- b <- f a- return $! Timeout b-aspectTrav (AddMaxHP a) f = do- b <- f a- return $! AddMaxHP b-aspectTrav (AddMaxCalm a) f = do- b <- f a- return $! AddMaxCalm b-aspectTrav (AddSpeed a) f = do- b <- f a- return $! AddSpeed b-aspectTrav (AddSkills as) _ = return $! AddSkills as-aspectTrav (AddHurtMelee a) f = do- b <- f a- return $! AddHurtMelee b-aspectTrav (AddHurtRanged a) f = do- b <- f a- return $! AddHurtRanged b-aspectTrav (AddArmorMelee a) f = do- b <- f a- return $! AddArmorMelee b-aspectTrav (AddArmorRanged a) f = do- b <- f a- return $! AddArmorRanged b-aspectTrav (AddSight a) f = do- b <- f a- return $! AddSight b-aspectTrav (AddSmell a) f = do- b <- f a- return $! AddSmell b-aspectTrav (AddLight a) f = do- b <- f a- return $! AddLight b+slotName :: EqpSlot -> Text+slotName EqpSlotPeriodic = "periodicity"+slotName EqpSlotTimeout = "timeout"+slotName EqpSlotAddHurtMelee = "to melee damage"+slotName EqpSlotAddArmorMelee = "melee armor"+slotName EqpSlotAddHurtRanged = "to ranged damage"+slotName EqpSlotAddArmorRanged = "ranged armor"+slotName EqpSlotAddMaxHP = "max HP"+slotName EqpSlotAddMaxCalm = "max Calm"+slotName EqpSlotAddSpeed = "speed"+slotName EqpSlotAddSkills{} = "skills"+slotName EqpSlotAddSight = "sight radius"+slotName EqpSlotAddSmell = "smell radius"+slotName EqpSlotAddLight = "light radius"+slotName EqpSlotWeapon = "weapon damage" toVelocity :: Int -> Feature toVelocity n = ToThrow $ ThrowMod n 100@@ -247,12 +235,12 @@ periodicAspect Periodic = True periodicAspect _ = False ps = filter periodicAspect iaspects- in if length ps > 1 then ["more than one Periodic specification"] else []+ in ["more than one Periodic specification" | length ps > 1] ++ let timeoutAspect :: Aspect a -> Bool timeoutAspect Timeout{} = True timeoutAspect _ = False ts = filter timeoutAspect iaspects- in if length ts > 1 then ["more than one Timeout specification"] else []+ in ["more than one Timeout specification" | length ts > 1] -- TODO: if "treasure" stays wired-in, assure there are some treasure items -- TODO: (spans multiple contents) check that there is at least one item@@ -261,4 +249,20 @@ -- can appear at). -- | Validate all item kinds. validateAllItemKind :: [ItemKind] -> [Text]-validateAllItemKind _ = []+validateAllItemKind content =+ let kindFreq :: S.Set (GroupName ItemKind) -- cf. Kind.kindFreq+ kindFreq = let tuples = [ cgroup+ | k <- content+ , (cgroup, n) <- ifreq k+ , n > 0 ]+ in S.fromList tuples+ missingGroups = [ cgroup+ | k <- content+ , (cgroup, _) <- ikit k+ , S.notMember cgroup kindFreq ]+ errorMsg = case missingGroups of+ [] -> []+ _ -> ["no groups" <+> tshow missingGroups+ <+> "among content that has groups"+ <+> tshow (S.elems kindFreq)]+ in errorMsg
Game/LambdaHack/Content/ModeKind.hs view
@@ -2,10 +2,12 @@ -- | The type of kinds of game modes. module Game.LambdaHack.Content.ModeKind ( Caves, Roster(..), Player(..), ModeKind(..), LeaderMode(..), AutoLeader(..)+ , Outcome(..), HiIndeterminant(..), HiCondPoly, HiSummand, HiPolynomial , validateSingleModeKind, validateAllModeKind ) where import Data.Binary+import qualified Data.EnumMap.Strict as EM import qualified Data.IntMap.Strict as IM import Data.Text (Text) import qualified Data.Text as T@@ -43,6 +45,30 @@ } deriving (Show, Eq) +-- | Outcome of a game.+data Outcome =+ Killed -- ^ the faction was eliminated+ | Defeated -- ^ the faction lost the game in another way+ | Camping -- ^ game is supended+ | Conquer -- ^ the player won by eliminating all rivals+ | Escape -- ^ the player escaped the dungeon alive+ | Restart -- ^ game is restarted+ deriving (Show, Eq, Ord, Enum, Bounded, Generic)++instance Binary Outcome++data HiIndeterminant = HiConst | HiLoot | HiBlitz | HiSurvival | HiKill | HiLoss+ deriving (Show, Eq, Ord, Generic)++instance Binary HiIndeterminant++type HiPolynomial = [(HiIndeterminant, Double)]++type HiSummand = (HiPolynomial, [Outcome])++-- | Conditional polynomial representing score calculation for this player.+type HiCondPoly = [HiSummand]+ -- | Properties of a particular player. data Player a = Player { fname :: !Text -- ^ name of the player@@ -50,6 +76,7 @@ , fskillsOther :: !Skills -- ^ skills of the other actors , fcanEscape :: !Bool -- ^ the player can escape the dungeon , fneverEmpty :: !Bool -- ^ the faction declared killed if no actors+ , fhiCondPoly :: !HiCondPoly -- ^ score polynomial for the player , fhasNumbers :: !Bool -- ^ whether actors have numbers, not symbols , fhasGender :: !Bool -- ^ whether actors have gender , ftactic :: !Tactic -- ^ members behave according to this tactic@@ -59,7 +86,7 @@ , fhasUI :: !Bool -- ^ does the faction have a UI client -- (for control or passive observation) }- deriving (Show, Eq, Generic)+ deriving (Show, Eq, Ord, Generic) instance Binary a => Binary (Player a) @@ -68,7 +95,7 @@ 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)+ deriving (Show, Eq, Ord, Generic) instance Binary LeaderMode @@ -90,7 +117,7 @@ -- if the flag is @False@, server still does a subset -- of the automatic switching, but the client is permitted to do more }- deriving (Show, Eq, Generic)+ deriving (Show, Eq, Ord, Generic) instance Binary AutoLeader @@ -119,7 +146,6 @@ in concatMap (checkDipl "rosterEnemy") rosterEnemy ++ concatMap (checkDipl "rosterAlly") rosterAlly --- Note that @fSkillsOther@ needn't be a subset of @fSkillsLeader@. validateSinglePlayer :: Caves -> Player Dice.Dice -> [Text] validateSinglePlayer caves Player{..} = [ "fname empty:" <+> fname | T.null fname ]@@ -133,6 +159,8 @@ | any (`notElem` IM.keys caves) [Dice.minDice fentryLevel .. Dice.maxDice fentryLevel] ] -- simplification+ ++ [ "fskillsOther not negative:" <+> fname+ | any (>= 0) $ EM.elems fskillsOther ] -- | Validate all game mode kinds. Currently always valid. validateAllModeKind :: [ModeKind] -> [Text]
Game/LambdaHack/Content/RuleKind.hs view
@@ -52,7 +52,6 @@ , rleadLevelClips :: !Int -- ^ server switches leader level that often , rscoresFile :: !FilePath -- ^ name of the scores file , rsavePrefix :: !String -- ^ name of the savefile prefix- , rsharedStash :: !Bool -- ^ whether shared stashes are available , rnearby :: !Int -- ^ what distance between actors is 'nearby' }
Game/LambdaHack/Content/TileKind.hs view
@@ -5,6 +5,7 @@ , validateSingleTileKind, validateAllTileKind, actionFeatures ) where +import Control.DeepSeq import Control.Exception.Assert.Sugar import Data.Binary import Data.Hashable@@ -23,6 +24,9 @@ -- | The type of kinds of terrain tiles. See @Tile.hs@ for explanation -- of the absence of a corresponding type @Tile@ that would hold -- particular concrete tiles in the dungeon.+-- Note that tile names (and any other content names) should not be plural+-- (that would lead to "a stairs"), so "road with cobblestones" is fine,+-- but "granite cobblestones" is wrong. data TileKind = TileKind { tsymbol :: !Char -- ^ map symbol , tname :: !Text -- ^ short description@@ -61,6 +65,8 @@ instance Binary Feature instance Hashable Feature++instance NFData Feature -- TODO: (spans multiple contents) check that all posible solid place -- fences have hidden counterparts.
Game/LambdaHack/SampleImplementation/SampleMonadClient.hs view
@@ -7,6 +7,7 @@ module Game.LambdaHack.SampleImplementation.SampleMonadClient ( executorCli #ifdef EXPOSE_INTERNAL+ -- * Internal operations , CliImplementation #endif ) where@@ -41,7 +42,7 @@ , cliSession :: SessionUI -- ^ UI setup data, empty for AI clients } --- | Server state transformation monad.+-- | Client state transformation monad. newtype CliImplementation resp req a = CliImplementation {runCliImplementation :: StateT (CliState resp req) IO a} deriving (Monad, Functor, Applicative)@@ -84,6 +85,8 @@ ChanServer{requestS} <- gets cliDict IO.liftIO $ atomically . writeTQueue requestS $ scmd +-- | The game-state semantics of atomic commands+-- as computed on the client. instance MonadAtomic (CliImplementation resp req) where execAtomic = handleCmdAtomic
Game/LambdaHack/SampleImplementation/SampleMonadServer.hs view
@@ -6,11 +6,14 @@ module Game.LambdaHack.SampleImplementation.SampleMonadServer ( executorSer #ifdef EXPOSE_INTERNAL+ -- * Internal operations , SerImplementation #endif ) where import Control.Applicative+import Control.Concurrent+import qualified Control.Exception as Ex import qualified Control.Monad.IO.Class as IO import Control.Monad.Trans.State.Strict hiding (State) import qualified Data.EnumMap.Strict as EM@@ -24,6 +27,7 @@ import Game.LambdaHack.Common.MonadStateRead import qualified Game.LambdaHack.Common.Save as Save import Game.LambdaHack.Common.State+import Game.LambdaHack.Common.Thread import Game.LambdaHack.Server.CommonServer import Game.LambdaHack.Server.MonadServer import Game.LambdaHack.Server.ProtocolServer@@ -75,7 +79,7 @@ SerImplementation $ modify $ \serS -> serS {serDict = s} liftIO = SerImplementation . IO.liftIO --- | The game-state semantics of atomic game commands+-- | The game-state semantics of atomic commands -- as computed on the server. instance MonadAtomic SerImplementation where execAtomic = handleAndBroadcastServer@@ -91,7 +95,7 @@ -- | Run an action in the @IO@ monad, with undefined state. executorSer :: SerImplementation () -> IO ()-executorSer m =+executorSer m = do let saveFile (_, ser) = fromMaybe "save" (ssavePrefixSer (sdebugSer ser)) <.> saveName@@ -102,4 +106,14 @@ , serDict = EM.empty , serToSave }- in Save.wrapInSaves saveFile exe+ exeWithSaves = Save.wrapInSaves saveFile exe+ -- Wait for clients to exit even in case of server crash+ -- (or server and client crash), which gives them time to save+ -- and report their own inconsistencies, if any.+ -- TODO: send them a message to tell users "server crashed"+ -- and then wait for them to exit normally.+ Ex.handle (\(ex :: Ex.SomeException) -> do+ threadDelay 100000 -- let clients report their errors+ Ex.throw ex) -- crash eventually, which kills clients+ exeWithSaves+ waitForChildren childrenServer -- no crash, wait for clients indefinitely
Game/LambdaHack/Server.hs view
@@ -3,56 +3,17 @@ -- See -- <https://github.com/LambdaHack/LambdaHack/wiki/Client-server-architecture>. module Game.LambdaHack.Server- ( mainSer+ ( -- * Re-exported from "Game.LambdaHack.Server.LoopServer"+ loopSer+ -- * Re-exported from "Game.LambdaHack.Server.MonadServer"+ , speedupCOps+ -- * Re-exported from "Game.LambdaHack.Server.Commandline"+ , debugArgs+ -- * Re-exported from "Game.LambdaHack.Server.State"+ , sdebugCli ) where -import Control.Concurrent-import qualified Control.Exception as Ex hiding (handle)--import Game.LambdaHack.Atomic-import Game.LambdaHack.Common.ClientOptions-import Game.LambdaHack.Common.Faction-import qualified Game.LambdaHack.Common.Kind as Kind-import Game.LambdaHack.Common.Request-import Game.LambdaHack.Common.Response-import Game.LambdaHack.Common.Thread import Game.LambdaHack.Server.Commandline import Game.LambdaHack.Server.LoopServer import Game.LambdaHack.Server.MonadServer-import Game.LambdaHack.Server.ProtocolServer import Game.LambdaHack.Server.State---- | Fire up the frontend with the engine fueled by content.--- The action monad types to be used are determined by the 'exeSer'--- and 'executorCli' calls. If other functions are used in their place--- the types are different and so the whole pattern of computation--- is different. Which of the frontends is run depends on the flags supplied--- when compiling the engine library.-mainSer :: (MonadAtomic m, MonadServerReadRequest m)- => [String]- -> Kind.COps- -> (m () -> IO ())- -> (Kind.COps -> DebugModeCli- -> ((FactionId -> ChanServer ResponseUI RequestUI- -> IO ())- -> (FactionId -> ChanServer ResponseAI RequestAI- -> IO ())- -> IO ())- -> IO ())- -> IO ()-mainSer args- !copsSlow -- evaluate fully to discover errors ASAP and free memory- exeSer- exeFront = do- sdebugNxt <- debugArgs args- let cops = speedupCOps False copsSlow- exeServer executorUI executorAI = do- -- Wait for clients to exit even in case of server crash- -- (or server and client crash), which gives them time to save.- -- TODO: send them a message to tell users "server crashed"- -- and then let them exit.- Ex.finally- (exeSer (loopSer sdebugNxt executorUI executorAI cops))- (threadDelay 100000) -- server crashed, show the error eventually- waitForChildren childrenServer -- no crash, wait indefinitely- exeFront cops (sdebugCli sdebugNxt) exeServer
Game/LambdaHack/Server/Commandline.hs view
@@ -11,6 +11,7 @@ -- TODO: make more maintainable +-- | Parse server debug parameters from commandline arguments. debugArgs :: [String] -> IO DebugModeSer debugArgs args = do let usage =@@ -22,6 +23,7 @@ , " --allClear let all map tiles be translucent" , " --gameMode m start next game in the given mode" , " --automateAll give control of all UI teams to computer"+ , " --keepAutomated keep factions automated after game over" , " --newGame start a new game, overwriting the save file" , " --difficulty n set difficulty for all UI players to n" , " --stopAfter n exit this game session after around n seconds"@@ -60,6 +62,8 @@ (parseArgs rest) {sgameMode = Just $ toGroupName (T.pack s)} parseArgs ("--automateAll" : rest) = (parseArgs rest) {sautomateAll = True}+ parseArgs ("--keepAutomated" : rest) =+ (parseArgs rest) {skeepAutomated = True} parseArgs ("--newGame" : rest) = let debugSer = parseArgs rest in debugSer { snewGameSer = True
Game/LambdaHack/Server/CommonServer.hs view
@@ -4,7 +4,7 @@ ( execFailure, resetFidPerception, resetLitInDungeon, getPerFid , revealItems, moveStores, deduceQuits, deduceKilled, electLeader , addActor, addActorIid, projectFail, pickWeaponServer, sumOrganEqpServer- , actorSkillsServer, maxActorSkillsServer+ , actorSkillsServer ) where import Control.Applicative@@ -14,7 +14,9 @@ import Data.List import Data.Maybe import Data.Text (Text)+import qualified Data.Text as T import qualified NLP.Miniutter.English as MU+import qualified Text.Show.Pretty as Show.Pretty import Game.LambdaHack.Atomic import qualified Game.LambdaHack.Common.Ability as Ability@@ -57,12 +59,14 @@ let fid = bfid body msg = showReqFailure failureSer impossible = impossibleReqFailure failureSer+ debugShow :: Show a => a -> Text+ debugShow = T.pack . Show.Pretty.ppShow possiblyAlarm = if impossible then debugPossiblyPrintAndExit else debugPossiblyPrint possiblyAlarm $ "execFailure:" <+> msg <> "\n"- <> tshow body <> "\n" <> tshow req+ <> debugShow body <> "\n" <> debugShow req execSfxAtomic $ SfxMsgFid fid $ "Unexpected problem:" <+> msg <> "." -- TODO: --more--, but keep in history @@ -73,11 +77,10 @@ => PersLit -> FactionId -> LevelId -> m Perception resetFidPerception persLit fid lid = do- cops <- getsState scops sfovMode <- getsServer $ sfovMode . sdebugSer lvl <- getLevel lid let fovMode = fromMaybe Digital sfovMode- per = fidLidPerception cops fovMode persLit fid lid lvl+ per = fidLidPerception fovMode persLit fid lid lvl upd = EM.adjust (EM.adjust (const per) lid) fid modifyServer $ \ser2 -> ser2 {sper = upd (sper ser2)} return $! per@@ -92,32 +95,40 @@ getPerFid :: MonadServer m => FactionId -> LevelId -> m Perception getPerFid fid lid = do pers <- getsServer sper- let fper = fromMaybe (assert `failure` "no perception for faction"- `twith` (lid, fid)) $ EM.lookup fid pers- per = fromMaybe (assert `failure` "no perception for level"- `twith` (lid, fid)) $ EM.lookup lid fper+ let failFact = assert `failure` "no perception for faction" `twith` (lid, fid)+ fper = EM.findWithDefault failFact fid pers+ failLvl = assert `failure` "no perception for level" `twith` (lid, fid)+ per = EM.findWithDefault failLvl lid fper return $! per -- We don't provide ActorId, because the actor can be dead and then, e.g., -- containers with the ActorId are invalid and lead to crashes. revealItems :: (MonadAtomic m, MonadServer m)- => Maybe FactionId -> Maybe Actor -> m ()+ => Maybe FactionId -> Maybe (ActorId, Actor) -> m () revealItems mfid mbody = do itemToF <- itemToFullServer dungeon <- getsState sdungeon- let discover b iid k =+ let discover aid store iid k = let itemFull = itemToF iid k+ c = CActor aid store in case itemDisco itemFull of Just ItemDisco{itemKindId} -> do seed <- getsServer $ (EM.! iid) . sitemSeedD- execUpdAtomic $ UpdDiscover (blid b) (bpos b) iid itemKindId seed- _ -> assert `failure` (mfid, mbody, iid, itemFull)+ execUpdAtomic $ UpdDiscover c iid itemKindId seed+ _ -> assert `failure` (mfid, mbody, c, iid, itemFull) f aid = do b <- getsState $ getActorBody aid let ourSide = maybe True (== bfid b) mfid- when (ourSide && Just b /= mbody) $ mapActorItems_ (discover b) b+ when ourSide $+ -- CSha is IDed for each actor of each faction, which is OK,+ -- even though it may introduce a slight lag.+ -- AI clients being sent this is a bigger waste anyway.+ join $ getsState $ mapActorItems_ (discover aid) b mapDungeonActors_ f dungeon- maybe skip (\b -> mapActorItems_ (discover b) b) mbody+ maybe (return ())+ (\(aid, b) -> do+ join $ getsState $ mapActorItems_ (discover aid) b)+ mbody moveStores :: (MonadAtomic m, MonadServer m) => ActorId -> CStore -> CStore -> m ()@@ -127,45 +138,45 @@ mapActorCStore_ fromStore g b quitF :: (MonadAtomic m, MonadServer m)- => Maybe Actor -> Status -> FactionId -> m ()+ => Maybe (ActorId, Actor) -> Status -> FactionId -> m () quitF mbody status fid = do- assert (maybe True ((fid ==) . bfid) mbody) skip+ let !_A = assert (maybe True ((fid ==) . bfid . snd) mbody) () fact <- getsState $ (EM.! fid) . sfactionD let oldSt = gquit fact- case fmap stOutcome $ oldSt of+ case stOutcome <$> oldSt of Just Killed -> return () -- Do not overwrite in case Just Defeated -> return () -- many things happen in 1 turn. Just Conquer -> return () Just Escape -> return () _ -> do when (fhasUI $ gplayer fact) $ do+ keepAutomated <- getsServer $ skeepAutomated . sdebugSer+ when (isAIFact fact+ && fleaderMode (gplayer fact) /= LeaderNull+ && not keepAutomated) $+ execUpdAtomic $ UpdAutoFaction fid False revealItems (Just fid) mbody- registerScore status mbody fid- execUpdAtomic $ UpdQuitFaction fid mbody oldSt $ Just status+ registerScore status (snd <$> mbody) fid+ execUpdAtomic $ UpdQuitFaction fid (snd <$> mbody) oldSt $ Just status -- TODO: send only aid to UpdQuitFaction and elsewhere --- aid is alive modifyServer $ \ser -> ser {squit = True} -- end turn ASAP -- Send any QuitFactionA actions that can be deduced from their current state.-deduceQuits :: (MonadAtomic m, MonadServer m) => Actor -> Status -> m ()-deduceQuits body status@Status{stOutcome}+deduceQuits :: (MonadAtomic m, MonadServer m)+ => FactionId -> Maybe (ActorId, Actor) -> Status -> m ()+deduceQuits fid mbody status@Status{stOutcome} | stOutcome `elem` [Defeated, Camping, Restart, Conquer] =- assert `failure` "no quitting to deduce" `twith` (status, body)-deduceQuits body status = do- let fid = bfid body- mapQuitF statusF fids = mapM_ (quitF Nothing statusF) $ delete fid fids- quitF (Just body) status fid- let inGameOutcome (_, fact) = case fmap stOutcome $ gquit fact of+ assert `failure` "no quitting to deduce" `twith` (fid, mbody, status)+deduceQuits fid mbody status = do+ let mapQuitF statusF fids = mapM_ (quitF Nothing statusF) $ delete fid fids+ quitF mbody status fid+ let inGameOutcome (_, fact) = case stOutcome <$> gquit fact of Just Killed -> False Just Defeated -> False Just Restart -> False -- effectively, commits suicide _ -> True- inGame (fid2, fact2) =- if inGameOutcome (fid2, fact2)- then anyActorsAlive fid2- else return False factionD <- getsState sfactionD- assocsInGame <- filterM inGame $ EM.assocs factionD- let assocsInGameOutcome = filter inGameOutcome $ EM.assocs factionD- keysInGame = map fst assocsInGameOutcome+ let assocsInGame = filter inGameOutcome $ EM.assocs factionD+ keysInGame = map fst assocsInGame assocsKeepArena = filter (keepArenaFact . snd) assocsInGame assocsUI = filter (fhasUI . gplayer . snd) assocsInGame nonHorrorAIG = filter (not . isHorrorFact . snd) assocsInGame@@ -188,8 +199,8 @@ _ | stOutcome status == Escape -> do -- Otherwise, in a game with many warring teams alive, -- only complete Victory matters, until enough of them die.- let (victors, losers) = partition (flip isAllied fid . snd)- assocsInGameOutcome+ let (victors, losers) =+ partition (flip isAllied fid . snd) assocsInGame mapQuitF status{stOutcome=Escape} $ map fst victors mapQuitF status{stOutcome=Defeated} $ map fst losers _ -> return ()@@ -203,25 +214,30 @@ keepArenaFact fact = fleaderMode (gplayer fact) /= LeaderNull && fneverEmpty (gplayer fact) -deduceKilled :: (MonadAtomic m, MonadServer m) => Actor -> m ()-deduceKilled body = do+-- We assume the actor in the second argumet is dead or dominated+-- by this point. Even if the actor is to be dominated,+-- @bfid@ of the actor body is still the old faction.+deduceKilled :: (MonadAtomic m, MonadServer m)+ => ActorId -> Actor -> m ()+deduceKilled aid body = do Kind.COps{corule} <- getsState scops let firstDeathEnds = rfirstDeathEnds $ Kind.stdRuleset corule fid = bfid body fact <- getsState $ (EM.! fid) . sfactionD when (fneverEmpty $ gplayer fact) $ do- actorsAlive <- anyActorsAlive fid+ actorsAlive <- anyActorsAlive fid (Just aid) when (not actorsAlive || firstDeathEnds) $- deduceQuits body $ Status Killed (fromEnum $ blid body) Nothing+ deduceQuits fid (Just (aid, body))+ $ Status Killed (fromEnum $ blid body) Nothing -anyActorsAlive :: MonadServer m => FactionId -> m Bool-anyActorsAlive fid = do+anyActorsAlive :: MonadServer m => FactionId -> Maybe ActorId -> m Bool+anyActorsAlive fid maid = do fact <- getsState $ (EM.! fid) . sfactionD if fleaderMode (gplayer fact) /= LeaderNull then return $! isJust $ gleader fact else do- as <- getsState $ fidActorNotProjList fid- return $! not $ null as+ as <- getsState $ fidActorNotProjAssocs fid+ return $! not $ null $ maybe as (\aid -> filter ((/= aid) . fst) as) maid electLeader :: MonadAtomic m => FactionId -> LevelId -> ActorId -> m () electLeader fid lid aidDead = do@@ -282,8 +298,7 @@ if not $ maybe True (bproj . snd . fst) mab then if isBlast && bproj sb then do -- Hit the blocking actor.- projectBla source spos (pos:rest) iid cstore- isBlast+ projectBla source spos (pos:rest) iid cstore isBlast return Nothing else return $ Just ProjectBlockActor else do@@ -313,7 +328,7 @@ case iid `EM.lookup` bag of Nothing -> assert `failure` (source, pos, rest, iid, cstore) Just kit@(_, it) -> do- addProjectile source pos rest iid kit lid (bfid sb) localTime isBlast+ addProjectile pos rest iid kit lid (bfid sb) localTime isBlast let c = CActor source cstore execUpdAtomic $ UpdLoseItem iid item (1, take 1 it) c @@ -321,10 +336,10 @@ -- -- Projectile has no organs except for the trunk. addProjectile :: (MonadAtomic m, MonadServer m)- => ActorId -> Point -> [Point] -> ItemId -> ItemQuant -> LevelId -> FactionId- -> Time -> Bool+ => Point -> [Point] -> ItemId -> ItemQuant -> LevelId+ -> FactionId -> Time -> Bool -> m ()-addProjectile source bpos rest iid (_, it) blid bfid btime isBlast = do+addProjectile bpos rest iid (_, it) blid bfid btime isBlast = do localTime <- getsState $ getLocalTime blid itemToF <- itemToFullServer let itemFull@ItemFull{itemBase} = itemToF iid (1, take 1 it)@@ -332,12 +347,13 @@ adj | trange < 5 = "falling" | otherwise = "flying" -- Not much detail about a fast flying item.- (object1, object2) = partItem (CActor source CInv) blid localTime $ itemNoDisco (itemBase, 1)+ (_, object1, object2) = partItem CInv blid localTime+ (itemNoDisco (itemBase, 1)) bname = makePhrase [MU.AW $ MU.Text adj, object1, object2] tweakBody b = b { bsymbol = if isBlast then bsymbol b else '*' , bcolor = if isBlast then bcolor b else Color.BrWhite , bname- , bhp = 0+ , bhp = 1 , bproj = True , btrajectory = Just (trajectory, speed) , beqp = EM.singleton iid (1, take 1 it)@@ -395,7 +411,7 @@ bonusHP = fromIntegral $ (diffHP - hp) `divUp` oneM healthOrgans = [(Just bonusHP, ("bonus HP", COrgan)) | bonusHP /= 0] bsymbol = jsymbol itemBase- bname = jname itemBase+ bname = IK.iname trunkKind bcolor = flavourToColor $ jflavour itemBase b = actorTemplate trunkId bsymbol bname bpronoun bcolor diffHP calm pos lid time bfid@@ -410,7 +426,16 @@ $ \(mk, (ikText, cstore)) -> do let container = CActor aid cstore itemFreq = [(ikText, 1)]- void $ rollAndRegisterItem lid itemFreq container False mk+ mIidEtc <- rollAndRegisterItem lid itemFreq container False mk+ case mIidEtc of+ Nothing -> assert `failure` (lid, itemFreq, container, mk)+ Just (_, (ItemFull{itemDisco=+ Just ItemDisco{itemAE=+ Just ItemAspectEffect{jeffects=_:_}}}, _)) ->+ return () -- discover by use+ Just (iid, _) -> do+ seed <- getsServer $ (EM.! iid) . sitemSeedD+ execUpdAtomic $ UpdDiscoverSeed container iid seed return $ Just aid -- Server has to pick a random weapon or it could leak item discovery@@ -423,6 +448,7 @@ bodyAssocs <- fullAssocsServer source [COrgan] actorSk <- actorSkillsServer source sb <- getsState $ getActorBody source+ localTime <- getsState $ getLocalTime (blid sb) -- For projectiles we need to accept even items without any effect, -- so that the projectile dissapears and NoEffect feedback is produced. let allAssocs = eqpAssocs ++ bodyAssocs@@ -431,7 +457,7 @@ permitted = permittedPrecious calm10 forced legalPrecious = either (const False) (const True) . permitted preferredPrecious = either (const False) id . permitted- strongest = strongestSlotNoFilter IK.EqpSlotWeapon allAssocs+ strongest = strongestMelee True localTime allAssocs strongestLegal = filter (legalPrecious . snd . snd) strongest strongestPreferred = filter (preferredPrecious . snd . snd) strongestLegal best = case strongestPreferred of@@ -441,10 +467,9 @@ [] -> strongestLegal case best of [] -> return Nothing- iis -> do- let is = map snd iis- -- TODO: pick the item according to the frequency of its kind.- (iid, _) <- rndToAction $ oneOf is+ iis@((maxS, _) : _) -> do+ let maxIis = map snd $ takeWhile ((== maxS) . fst) iis+ (iid, _) <- rndToAction $ oneOf maxIis let cstore = if isJust (lookup iid bodyAssocs) then COrgan else CEqp return $ Just (iid, cstore) @@ -461,9 +486,3 @@ fact <- getsState $ (EM.! bfid body) . sfactionD let mleader = fst <$> gleader fact getsState $ actorSkills mleader aid activeItems--maxActorSkillsServer :: MonadServer m- => ActorId -> m Ability.Skills-maxActorSkillsServer aid = do- activeItems <- activeItemsServer aid- getsState $ maxActorSkills aid activeItems
Game/LambdaHack/Server/DebugServer.hs view
@@ -6,6 +6,7 @@ import Data.Text (Text) import qualified Data.Text as T+import qualified Text.Show.Pretty as Show.Pretty import Game.LambdaHack.Atomic import Game.LambdaHack.Common.Actor@@ -24,6 +25,9 @@ -- and sent responseQs. Clients interleave and block non-deterministically -- so their logs would be harder to interpret. +debugShow :: Show a => a -> Text+debugShow = T.pack . Show.Pretty.ppShow+ debugResponseAI :: MonadServer m => ResponseAI -> m () debugResponseAI cmd = case cmd of RespUpdAtomicAI cmdA@UpdPerception{} -> debugPlain cmd cmdA@@ -33,7 +37,7 @@ RespQueryAI aid -> do d <- debugAid aid "RespQueryAI" cmd serverPrint d- RespPingAI -> serverPrint $ tshow cmd+ RespPingAI -> serverPrint $ debugShow cmd debugResponseUI :: MonadServer m => ResponseUI -> m () debugResponseUI cmd = case cmd of@@ -43,19 +47,19 @@ RespUpdAtomicUI cmdA -> debugPretty cmd cmdA RespSfxAtomicUI sfx -> do ps <- posSfxAtomic sfx- serverPrint $ tshow (cmd, ps)- RespQueryUI -> serverPrint $ "RespQueryUI:" <+> tshow cmd- RespPingUI -> serverPrint $ tshow cmd+ serverPrint $ debugShow (cmd, ps)+ RespQueryUI -> serverPrint $ "RespQueryUI:" <+> debugShow cmd+ RespPingUI -> serverPrint $ debugShow cmd debugPretty :: (MonadServer m, Show a) => a -> UpdAtomic -> m () debugPretty cmd cmdA = do ps <- posUpdAtomic cmdA- serverPrint $ tshow (cmd, ps)+ serverPrint $ debugShow (cmd, ps) debugPlain :: (MonadServer m, Show a) => a -> UpdAtomic -> m () debugPlain cmd cmdA = do ps <- posUpdAtomic cmdA- serverPrint $ T.pack $ show (cmd, ps) -- too large for pretty show+ serverPrint $ T.pack $ show (cmd, ps) -- too large for pretty printing debugRequestAI :: MonadServer m => ActorId -> RequestAI -> m () debugRequestAI aid cmd = do@@ -80,13 +84,13 @@ debugAid :: (MonadStateRead m, Show a) => ActorId -> Text -> a -> m Text debugAid aid label cmd = if aid == toEnum (-1) then- return $ "Pong:" <+> tshow label <+> tshow cmd+ return $ "Pong:" <+> debugShow label <+> debugShow cmd else do b <- getsState $ getActorBody aid time <- getsState $ getLocalTime (blid b)- return $! tshow DebugAid { label- , cmd- , lid = blid b- , time- , aid- , faction = bfid b }+ return $! debugShow DebugAid { label+ , cmd+ , lid = blid b+ , time+ , aid+ , faction = bfid b }
Game/LambdaHack/Server/DungeonGen.hs view
@@ -1,13 +1,14 @@ {-# LANGUAGE CPP #-} -- | The unpopulated dungeon generation routine. module Game.LambdaHack.Server.DungeonGen- ( -- * Public API- FreshDungeon(..), dungeonGen+ ( FreshDungeon(..), dungeonGen #ifdef EXPOSE_INTERNAL+ -- * Internal operations , convertTileMaps, placeStairs, buildLevel, levelFromCaveKind, findGenerator #endif ) where +import Control.Applicative import Control.Exception.Assert.Sugar import Control.Monad import qualified Data.EnumMap.Strict as EM@@ -15,7 +16,6 @@ import Data.List import Data.Maybe -import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc@@ -26,6 +26,7 @@ import Game.LambdaHack.Common.Time import Game.LambdaHack.Content.CaveKind import Game.LambdaHack.Content.ItemKind (ItemKind)+import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Content.TileKind (TileKind) import qualified Game.LambdaHack.Content.TileKind as TK@@ -95,7 +96,7 @@ (\p t -> Tile.isWalkable cotile t && not (Tile.hasFeature cotile TK.NoActor t) && dist 0 p t) -- can't overwrite stairs with other stairs- [ dist $ cminStairDist+ [ dist cminStairDist , dist $ cminStairDist `div` 2 , dist $ cminStairDist `div` 4 , const $ Tile.hasFeature cotile TK.OftenActor@@ -114,23 +115,23 @@ fitArea pos = inside pos . fromArea . qarea findLegend pos = maybe clegendLitTile qlegend $ find (fitArea pos) dplaces- hasEscape p t = Tile.kindHasFeature (TK.Cause $ IK.Escape p) t+ hasEscape p = Tile.kindHasFeature (TK.Cause $ IK.Escape p) ascendable = Tile.kindHasFeature $ TK.Cause (IK.Ascend 1) descendable = Tile.kindHasFeature $ TK.Cause (IK.Ascend (-1))- nightCond kt = (not (Tile.kindHasFeature TK.Clear kt)- || (if dnight then id else not)- (Tile.kindHasFeature TK.Dark kt))+ nightCond kt = not (Tile.kindHasFeature TK.Clear kt)+ || (if dnight then id else not)+ (Tile.kindHasFeature TK.Dark kt) dcond kt = (cpassable || not (Tile.kindHasFeature TK.Walkable kt)) && nightCond kt- pickDefTile = fmap (fromMaybe $ assert `failure` cdefTile)- $ opick cdefTile dcond+ pickDefTile = (fromMaybe $ assert `failure` cdefTile)+ <$> opick cdefTile dcond wcond kt = Tile.kindHasFeature TK.Walkable kt && nightCond kt mpickWalkable = if cpassable- then Just $ fmap (fromMaybe $ assert `failure` cdefTile)- $ opick cdefTile wcond+ then Just $ (fromMaybe $ assert `failure` cdefTile)+ <$> opick cdefTile wcond else Nothing cmap <- convertTileMaps cops pickDefTile mpickWalkable cxsize cysize dmap -- We keep two-way stairs separately, in the last component.@@ -146,14 +147,14 @@ return (up, down, upDown) else do let cond tk = (if moveUp then ascendable tk else descendable tk)- && (if noAsc then not (ascendable tk) else True)- && (if noDesc then not (descendable tk) else True)+ && (not noAsc || not (ascendable tk))+ && (not noDesc || not (descendable tk)) stairsCur = up ++ down ++ upDown posCur = nub $ sort $ map fst stairsCur spos <- placeStairs cops cmap kc posCur let legend = findLegend spos- stairId <- fmap (fromMaybe $ assert `failure` legend)- $ opick legend cond+ stairId <- (fromMaybe $ assert `failure` legend)+ <$> opick legend cond let st = (spos, stairId) asc = ascendable $ okind stairId desc = descendable $ okind stairId@@ -164,7 +165,7 @@ (False, False) -> assert `failure` st (stairsUp1, stairsDown1, stairsUpDown1) <- makeStairs False (ln == maxD) (ln == minD) ([], [], [])- assert (null stairsUp1) skip+ let !_A = assert (null stairsUp1) () let nstairUpLeft = nstairUp - length stairsUpDown1 (stairsUp2, stairsDown2, stairsUpDown2) <- foldM (\sts _ -> makeStairs True (ln == maxD) (ln == minD) sts)@@ -172,12 +173,12 @@ [1 .. nstairUpLeft] -- If only a single tile of up-and-down stairs, add one more stairs down. (stairsUp, stairsDown, stairsUpDown) <-- if length (stairsUp2 ++ stairsDown2) == 0- then (makeStairs False True (ln == minD)- (stairsUp2, stairsDown2, stairsUpDown2))+ if null (stairsUp2 ++ stairsDown2)+ then makeStairs False True (ln == minD)+ (stairsUp2, stairsDown2, stairsUpDown2) else return (stairsUp2, stairsDown2, stairsUpDown2) let stairsUpAndUpDown = stairsUp ++ stairsUpDown- assert (length stairsUpAndUpDown == nstairUp) skip+ let !_A = assert (length stairsUpAndUpDown == nstairUp) () let stairsTotal = stairsUpAndUpDown ++ stairsDown posTotal = nub $ sort $ map fst stairsTotal epos <- placeStairs cops cmap kc posTotal@@ -203,12 +204,12 @@ lsecret <- randomR (1, maxBound) -- 0 means unknown return $! levelFromCaveKind cops kc ldepth ltile lstair cactorCoeff cactorFreq litemNum citemFreq- lsecret (isJust escapeFeature)+ lsecret (map fst escape) -- | Build rudimentary level from a cave kind. levelFromCaveKind :: Kind.COps -> CaveKind -> AbsDepth -> TileMap -> ([Point], [Point])- -> Int -> Freqs ItemKind -> Int -> Freqs ItemKind -> Int -> Bool+ -> Int -> Freqs ItemKind -> Int -> Freqs ItemKind -> Int -> [Point] -> Level levelFromCaveKind Kind.COps{cotile} CaveKind{..}@@ -247,8 +248,8 @@ findGenerator cops ln minD maxD totalDepth nstairUp (genName, escapeFeature) = do let Kind.COps{cocave=Kind.Ops{opick}} = cops- ci <- fmap (fromMaybe $ assert `failure` genName)- $ opick genName (const True)+ ci <- (fromMaybe $ assert `failure` genName)+ <$> opick genName (const True) -- A simple rule for now: level at level @ln@ has depth (difficulty) @abs ln@. let ldepth = AbsDepth $ abs $ fromEnum ln cave <- buildCave cops ldepth totalDepth ci@@ -278,8 +279,8 @@ lvl <- findGenerator cops ln minId maxId freshTotalDepth nstairUp caveTB -- nstairUp for the next level is nstairDown for the current level let nstairDown = length $ snd $ lstair lvl- return $ (nstairDown, (ln, lvl) : l)+ return (nstairDown, (ln, lvl) : l) (nstairUpLast, levels) <- foldM gen (0, []) $ reverse $ IM.assocs caves- assert (nstairUpLast == 0) skip+ let !_A = assert (nstairUpLast == 0) () let freshDungeon = EM.fromList levels return $! FreshDungeon{..}
Game/LambdaHack/Server/DungeonGen/AreaRnd.hs view
@@ -34,7 +34,7 @@ -> Rnd Area mkRoom (xm, ym) (xM, yM) area = do let (x0, y0, x1, y1) = fromArea area- assert (xm <= x1 - x0 + 1 && ym <= y1 - y0 + 1) skip+ let !_A = assert (xm <= x1 - x0 + 1 && ym <= y1 - y0 + 1) () let aW = (xm, ym, min xM (x1 - x0 + 1), min yM (y1 - y0 + 1)) areaW = fromMaybe (assert `failure` aW) $ toArea aW Point xW yW <- xyInArea areaW -- roll size@@ -140,9 +140,9 @@ (_, _, sox1, soy1) = fromArea so (tx0, ty0, _, _) = fromArea ta (tox0, toy0, _, _) = fromArea to- assert (sx1 <= tx0 || sy1 <= ty0 `blame` (sa, ta)) skip- assert (sx1 <= sox1 || sy1 <= soy1 `blame` (sa, so)) skip- assert (tx0 >= tox0 || ty0 >= toy0 `blame` (ta, to)) skip+ let !_A = assert (sx1 <= tx0 || sy1 <= ty0 `blame` (sa, ta)) ()+ let !_A = assert (sx1 <= sox1 || sy1 <= soy1 `blame` (sa, so)) ()+ let !_A = assert (tx0 >= tox0 || ty0 >= toy0 `blame` (ta, to)) () let trim area = let (x0, y0, x1, y1) = fromArea area trim4 (v0, v1) | v1 - v0 < 6 = (v0, v1)
Game/LambdaHack/Server/DungeonGen/Cave.hs view
@@ -3,6 +3,7 @@ ( Cave(..), buildCave ) where +import Control.Applicative import Control.Arrow ((&&&)) import Control.Exception.Assert.Sugar import Control.Monad@@ -80,7 +81,7 @@ || couterFenceTile /= "basic outer fence" = subFullArea | otherwise = fullArea gs = grid lgrid area- (addedConnects, voidPlaces) <- do+ (addedConnects, voidPlaces) <- if gx * gy > 1 then do let fractionOfPlaces r = round $ r * fromIntegral (gx * gy) cauxNum = fractionOfPlaces cauxConnects@@ -98,16 +99,16 @@ let innerArea = fromMaybe (assert `failure` (i, r)) $ shrink r r' <- if i `elem` voidPlaces- then fmap Left $ mkVoidRoom innerArea- else fmap Right $ mkRoom minPlaceSize+ then Left <$> mkVoidRoom innerArea+ else Right <$> mkRoom minPlaceSize maxPlaceSize innerArea return (i, r')) gs fence <- buildFenceRnd cops couterFenceTile subFullArea dnight <- chanceDice ldepth totalDepth cnightChance- darkCorTile <- fmap (fromMaybe $ assert `failure` cdarkCorTile)- $ opick cdarkCorTile (const True)- litCorTile <- fmap (fromMaybe $ assert `failure` clitCorTile)- $ opick clitCorTile (const True)+ darkCorTile <- fromMaybe (assert `failure` cdarkCorTile)+ <$> opick cdarkCorTile (const True)+ litCorTile <- fromMaybe (assert `failure` clitCorTile)+ <$> opick clitCorTile (const True) let pickedCorTile = if dnight then darkCorTile else litCorTile addPl (m, pls, qls) (i, Left r) = return (m, pls, (i, Left r) : qls) addPl (m, pls, qls) (i, Right r) = do@@ -116,7 +117,7 @@ return (EM.union tmap m, place : pls, (i, Right (r, place)) : qls) (lplaces, dplaces, qplaces0) <- foldM addPl (fence, [], []) places0 connects <- connectGrid lgrid- let allConnects = union connects addedConnects -- no duplicates+ let allConnects = connects `union` addedConnects -- no duplicates qplaces = M.fromList qplaces0 cs <- mapM (\(p0, p1) -> do let shrinkPlace (r, Place{qkind}) =
Game/LambdaHack/Server/DungeonGen/Place.hs view
@@ -4,6 +4,7 @@ ( TileMapEM, Place(..), placeCheck, buildFenceRnd, buildPlace ) where +import Control.Applicative import Control.Exception.Assert.Sugar import Data.Binary import qualified Data.EnumMap.Strict as EM@@ -97,28 +98,31 @@ , coplace=Kind.Ops{ofoldrGroup} } CaveKind{..} dnight darkCorTile litCorTile ldepth@(AbsDepth ld) totalDepth@(AbsDepth depth) r = do- qFWall <- fmap (fromMaybe $ assert `failure` cfillerTile)- $ opick cfillerTile (const True)+ qFWall <- fromMaybe (assert `failure` cfillerTile)+ <$> opick cfillerTile (const True) dark <- chanceDice ldepth totalDepth cdarkChance -- TODO: factor out from here and newItem: let findInterval x1y1 [] = (x1y1, (11, 0)) findInterval x1y1 ((x, y) : rest) =- if ld * 10 <= x * depth+ if fromIntegral ld * 10 <= x * fromIntegral depth then (x1y1, (x, y)) else findInterval (x, y) rest linearInterpolation dataset =- -- We assume @dataset@ is sorted and between 1 and 10 inclusive.+ -- We assume @dataset@ is sorted and between 0 and 10. let ((x1, y1), (x2, y2)) = findInterval (0, 0) dataset- in y1 + (y2 - y1) * (ld * 10 - x1 * depth)- `divUp` ((x2 - x1) * depth)+ in ceiling+ $ fromIntegral y1+ + fromIntegral (y2 - y1)+ * (fromIntegral ld * 10 - x1 * fromIntegral depth)+ / ((x2 - x1) * fromIntegral depth) let f placeGroup q p pk kind acc = let rarity = linearInterpolation (prarity kind) in (q * p * rarity, ((pk, kind), placeGroup)) : acc g (placeGroup, q) = ofoldrGroup placeGroup (f placeGroup q) [] placeFreq = concatMap g cplaceFreq checkedFreq = filter (\(_, ((_, kind), _)) -> placeCheck r kind) placeFreq- freq = toFreq ("buildPlace ('" <> tshow ld <> ")") checkedFreq- assert (not (nullFreq freq) `blame` (placeFreq, checkedFreq, r)) skip+ freq = toFreq ("buildPlace" <+> tshow (map fst checkedFreq)) checkedFreq+ let !_A = assert (not (nullFreq freq) `blame` (placeFreq, checkedFreq, r)) () ((qkind, kr), _) <- frequency freq let qFFloor = if dark then darkCorTile else litCorTile qFGround = if dnight then darkCorTile else litCorTile@@ -143,8 +147,8 @@ | otherwise = xlegend EM.! c interior = case pfence kr of FNone | not dnight -> EM.mapWithKey digDay cmap- _ -> let lookupLegend x = fromMaybe (assert `failure` (qlegend, x))- $ EM.lookup x xlegend+ _ -> let lookupLegend x =+ EM.findWithDefault (assert `failure` (qlegend, x)) x xlegend in EM.map lookupLegend cmap tmap = EM.union interior fence return (tmap, place)@@ -170,8 +174,8 @@ ooverride Kind.COps{cotile=Kind.Ops{opick}} poverride = let getLegend (s, cgroup) acc = do m <- acc- tk <- fmap (fromMaybe $ assert `failure` (cgroup, s))- $ opick cgroup (const True) -- tile symbol ignored+ tk <- fromMaybe (assert `failure` (cgroup, s))+ <$> opick cgroup (const True) -- tile symbol ignored return $! EM.insert s tk m legend = foldr getLegend (return EM.empty) poverride in legend@@ -193,8 +197,8 @@ let isCorner x y = x `elem` [x0-1, x1+1] && y `elem` [y0-1, y1+1] tileGroup | isCorner xf yf = "basic outer fence" | otherwise = couterFenceTile- fenceId <- fmap (fromMaybe $ assert `failure` tileGroup)- $ opick tileGroup (const True)+ fenceId <- fromMaybe (assert `failure` tileGroup)+ <$> opick tileGroup (const True) return (Point xf yf, fenceId) pointList = [ (x, y) | x <- [x0-1, x1+1], y <- [y0..y1] ] ++ [ (x, y) | x <- [x0-1..x1+1], y <- [y0-1, y1+1] ]@@ -216,8 +220,7 @@ (dx, dy) = assert (xwidth >= dxcorner && ywidth >= length ptopLeft `blame` (area, pl)) (xwidth, ywidth)- fromX (x2, y2) =- zipWith (\x y -> Point x y) [x2..] (repeat y2)+ fromX (x2, y2) = map (`Point` y2) [x2..] fillInterior :: (forall a. Int -> [a] -> [a]) -> [(Point, Char)] fillInterior f = let tileInterior (y, row) =
Game/LambdaHack/Server/EndServer.hs view
@@ -16,6 +16,7 @@ import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.State+import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Server.CommonServer import Game.LambdaHack.Server.HandleEffectServer import Game.LambdaHack.Server.ItemServer@@ -24,7 +25,8 @@ -- | Continue or exit or restart the game. endOrLoop :: (MonadAtomic m, MonadServer m)- => m () -> m () -> m () -> m () -> m ()+ => m () -> (Maybe (GroupName ModeKind) -> m ()) -> m () -> m ()+ -> m () endOrLoop loop restart gameExit gameSave = do factionD <- getsState sfactionD let inGame fact = case gquit fact of@@ -49,16 +51,13 @@ modifyServer $ \ser -> ser {swriteSave = False} gameSave case (quitters, campers) of- (gameMode : _, _) -> do- modifyServer $ \ser -> ser {sdebugNxt = (sdebugNxt ser)- {sgameMode = Just gameMode}}- restart- _ | gameOver -> restart+ (gameMode : _, _) -> restart $ Just gameMode+ _ | gameOver -> restart Nothing ([], []) -> loop -- continue current game ([], _ : _) -> gameExit -- don't call @loop@, that is, quit the game loop dieSer :: (MonadAtomic m, MonadServer m) => ActorId -> Actor -> Bool -> m ()-dieSer aid b hit = do+dieSer aid b hit = -- TODO: clients don't see the death of their last standing actor; -- modify Draw.hs and Client.hs to handle that if bproj b then do@@ -71,22 +70,21 @@ let ikind = discoKind EM.! jkindIx trunk execUpdAtomic $ UpdRecordKill aid ikind 1 electLeader (bfid b) (blid b) aid+ tb <- getsState $ getActorBody aid+ deduceKilled aid tb -- tb has items not dropped, stash in inv+ fact <- getsState $ (EM.! bfid b) . sfactionD+ -- Prevent faction's stash from being lost in case they are not spawners.+ -- Projectiles can't drop stash, because they are blind and so the faction+ -- would not see the actor that drops the stash, leading to a crash.+ -- But this is OK; projectiles can't be leaders, so stash dropped earlier.+ when (isNothing $ gleader fact) $ moveStores aid CSha CInv dropAllItems aid b False b2 <- getsState $ getActorBody aid execUpdAtomic $ UpdDestroyActor aid b2 []- deduceKilled b -- | Drop all actor's items. dropAllItems :: (MonadAtomic m, MonadServer m) => ActorId -> Actor -> Bool -> m () dropAllItems aid b hit = do- fact <- getsState $ (EM.! bfid b) . sfactionD- -- A faction that is defeated, leaderless or with temporarlity no member- -- drops all items from the faction stash, too.- -- Projectiles can't drop stash, because they are blind and so the faction- -- would not see the actor that drops the stash, leading to a crash.- -- But this is OK --- projectiles can't be leaders, so stash dropped earlier.- when (not (bproj b) && isNothing (gleader fact)) $- mapActorCStore_ CSha (dropCStoreItem CSha aid b hit) b mapActorCStore_ CInv (dropCStoreItem CInv aid b hit) b mapActorCStore_ CEqp (dropCStoreItem CEqp aid b hit) b
Game/LambdaHack/Server/Fov.hs view
@@ -6,34 +6,28 @@ ( dungeonPerception, fidLidPerception , PersLit, litInDungeon #ifdef EXPOSE_INTERNAL- , PerceptionLit, ActorEqpBody+ -- * Internal operations+ , PerceptionReachable(..), PerceptionDynamicLit(..) #endif ) where -import Control.Exception.Assert.Sugar import qualified Data.EnumMap.Lazy as EML import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES-import Data.Function import Data.List import Data.Maybe-import Data.Ord import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction-import Game.LambdaHack.Common.Item-import Game.LambdaHack.Common.ItemStrongest import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level-import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Vector-import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Server.Fov.Common import qualified Game.LambdaHack.Server.Fov.Digital as Digital@@ -41,60 +35,63 @@ import qualified Game.LambdaHack.Server.Fov.Shadow as Shadow import Game.LambdaHack.Server.State --- | Visually reachable position (light passes through them to the actor).+-- | Visually reachable positions (light passes through them to the actor).+-- The list may contain (many) repetitions. newtype PerceptionReachable = PerceptionReachable {preachable :: [Point]} deriving Show --- | All lit positions on a level.-newtype PerceptionLit = PerceptionLit- {plit :: ES.EnumSet Point}+-- | All positions lit by dynamic lights on a level. Shared by all factions.+-- The list may contain (many) repetitions.+newtype PerceptionDynamicLit = PerceptionDynamicLit+ {pdynamicLit :: [Point]} deriving Show -type ActorEqpBody = [((ActorId, Actor), [ItemFull])]--type PersLit = EML.EnumMap LevelId ( PerceptionLit- , EM.EnumMap FactionId ActorEqpBody+-- | The cache of FOV information for a level, such as sight, smell+-- and light radiuses for each actor and bitmaps of clear and lit positions.+type PersLit = EML.EnumMap LevelId ( EM.EnumMap FactionId [(Actor, FovCache3)]+ , PointArray.Array Bool , PointArray.Array Bool ) -- | Calculate faction's perception of a level.-levelPerception :: Kind.COps- -> PerceptionLit -> ActorEqpBody -> PointArray.Array Bool+levelPerception :: [(Actor, FovCache3)]+ -> PointArray.Array Bool -> PointArray.Array Bool -> FovMode -> Level -> Perception-levelPerception cops litHere actorEqpBody blockers- fovMode lvl@Level{lxsize, lysize} =+levelPerception actorEqpBody clearPs litPs fovMode Level{lxsize, lysize} = let -- Dying actors included, to let them see their own demise.- ours = filter (not . bproj . snd . fst) actorEqpBody- ourR = preachable . reachableFromActor blockers fovMode- totalReachable = PerceptionReachable $ concatMap ourR ours+ ourR = preachable . reachableFromActor clearPs fovMode+ totalReachable = PerceptionReachable $ concatMap ourR actorEqpBody+ -- All non-projectile actors feel adjacent positions,+ -- even dark (for easy exploration). Projectiles rely on cameras. pAndVicinity p = p : vicinity lxsize lysize p- -- All actors feel adjacent positions, even dark (for easy exploration).- noctoBodies = map (\aEB@((_, b), _) -> (pAndVicinity (bpos b), aEB)) ours- nocto = concat $ map fst noctoBodies- ptotal = visibleOnLevel cops totalReachable litHere nocto lvl- canSmellAround (_, allAssocs) =- let radius = sumSlotNoFilter IK.EqpSlotAddSmell allAssocs- in radius >= 2+ gatherVicinities = concatMap (pAndVicinity . bpos . fst)+ nocteurs = filter (not . bproj . fst) actorEqpBody+ nocto = gatherVicinities nocteurs+ ptotal = visibleOnLevel totalReachable litPs nocto -- TODO: handle smell radius < 2, that is only under the actor- -- TODO: filter out tiles that are solid and so can't hold smell.- psmell = PerceptionVisible $ ES.fromList- $ concat $ map fst $ filter (canSmellAround . snd) noctoBodies+ -- Projectiles can potentially smell, too.+ canSmellAround FovCache3{fovSmell} = fovSmell >= 2+ smellers = filter (canSmellAround . snd) actorEqpBody+ smells = gatherVicinities smellers+ -- No smell stored in walls and under other actors.+ canHoldSmell p = clearPs PointArray.! p+ psmell = PerceptionVisible $ ES.fromList $ filter canHoldSmell smells in Perception ptotal psmell -- | Calculate faction's perception of a level based on the lit tiles cache.-fidLidPerception :: Kind.COps -> FovMode -> PersLit+fidLidPerception :: FovMode -> PersLit -> FactionId -> LevelId -> Level -> Perception-fidLidPerception cops fovMode persLit fid lid lvl =- let (litHere, bodyMap, blockers) = persLit EML.! lid+fidLidPerception fovMode persLit fid lid lvl =+ let (bodyMap, clearPs, litPs) = persLit EML.! lid actorEqpBody = EM.findWithDefault [] fid bodyMap- in levelPerception cops litHere actorEqpBody blockers fovMode lvl+ in levelPerception actorEqpBody clearPs litPs fovMode lvl -- | Calculate perception of a faction. factionPerception :: FovMode -> PersLit -> FactionId -> State -> FactionPers factionPerception fovMode persLit fid s =- EM.mapWithKey (fidLidPerception (scops s) fovMode persLit fid) $ sdungeon s+ EM.mapWithKey (fidLidPerception fovMode persLit fid) $ sdungeon s -- | Calculate the perception of the whole dungeon. dungeonPerception :: FovMode -> State -> StateServer -> Pers@@ -108,91 +105,87 @@ -- light source, e.g,, carried by an actor. A reachable and lit position -- is visible. Additionally, positions directly adjacent to an actor are -- assumed to be visible to him (through sound, touch, noctovision, whatever).-visibleOnLevel :: Kind.COps -> PerceptionReachable- -> PerceptionLit -> [Point] -> Level+visibleOnLevel :: PerceptionReachable+ -> PointArray.Array Bool -> [Point] -> PerceptionVisible-visibleOnLevel Kind.COps{cotile}- PerceptionReachable{preachable} PerceptionLit{plit}- nocto lvl =- -- TODO: costly- -- TODO: make a vector mask from isVisible; possibly of PerceptionVisible, too- let isVisible pos = Tile.isLit cotile (lvl `at` pos) || pos `ES.member` plit+visibleOnLevel PerceptionReachable{preachable} litPs nocto =+ let isVisible = (litPs PointArray.!) in PerceptionVisible $ ES.fromList $ nocto ++ filter isVisible preachable -- | Compute positions reachable by the actor. Reachable are all fields -- on a visually unblocked path from the actor position.-reachableFromActor :: PointArray.Array Bool -> FovMode- -> ((ActorId, Actor), [ItemFull])+reachableFromActor :: PointArray.Array Bool -> FovMode -> (Actor, FovCache3) -> PerceptionReachable-reachableFromActor blockers fovMode ((_, body), allItems) =- let sumSight = sumSlotNoFilter IK.EqpSlotAddSight allItems- radius = min (fromIntegral $ bcalm body `div` (5 * oneM)) sumSight- in PerceptionReachable $ fullscan blockers fovMode radius (bpos body)+reachableFromActor clearPs fovMode (body, FovCache3{fovSight}) =+ let radius = min (fromIntegral $ bcalm body `div` (5 * oneM)) fovSight+ in PerceptionReachable $ fullscan clearPs fovMode radius (bpos body) --- | Compute all lit positions on a level, whether lit by actors or floor items.--- Note that an actor can be blind or a projectile, in which case he doesn't see+-- | Compute all dynamically lit positions on a level, whether lit by actors+-- or floor items. Note that an actor can be blind, in which case he doesn't see -- his own light (but others, from his or other factions, possibly do).-litByItems :: Kind.COps -> PointArray.Array Bool -> FovMode -> Level- -> [(Point, [ItemFull])]- -> PerceptionLit-litByItems Kind.COps{cotile} blockers fovMode lvl allItems =- let litPos :: (Point, [ItemFull]) -> [Point]- litPos (p, is) =- let radius = sumSlotNoFilter IK.EqpSlotAddLight is- scan = fullscan blockers fovMode radius p- -- Optimization: filter out positions already having ambient light.- opt = filter (\pos -> not $ Tile.isLit cotile $ lvl `at` pos) scan- in opt- litAll = concatMap litPos allItems- in PerceptionLit $ ES.fromList litAll+litByItems :: PointArray.Array Bool -> FovMode -> [(Point, Int)]+ -> PerceptionDynamicLit+litByItems clearPs fovMode allItems =+ let litPos :: (Point, Int) -> [Point]+ litPos (p, light) = fullscan clearPs fovMode light p+ in PerceptionDynamicLit $ concatMap litPos allItems --- | Compute all lit positions in the dungeon+-- | Compute all lit positions in the dungeon. litInDungeon :: FovMode -> State -> StateServer -> PersLit litInDungeon fovMode s ser =- let cops@Kind.COps{cotile} = scops s- itemsInActors :: Level -> EM.EnumMap FactionId ActorEqpBody+ let Kind.COps{cotile} = scops s+ processIid3 (FovCache3 sightAcc smellAcc lightAcc) (iid, (k, _)) =+ let FovCache3{..} =+ EM.findWithDefault emptyFovCache3 iid $ sItemFovCache ser+ in FovCache3 (k * fovSight + sightAcc)+ (k * fovSmell + smellAcc)+ (k * fovLight + lightAcc)+ processBag3 bag acc = foldl' processIid3 acc $ EM.assocs bag+ itemsInActors :: Level -> EM.EnumMap FactionId [(Actor, FovCache3)] itemsInActors lvl =- let asLid = map (\aid -> (aid, getActorBody aid s))- $ concat $ EM.elems $ lprio lvl- asGrouped = groupBy ((==) `on` (bfid . snd))- $ sortBy (comparing (bfid . snd)) asLid- bodyFid :: [(ActorId, Actor)] -> (FactionId, ActorEqpBody)- bodyFid [] = assert `failure` asGrouped- bodyFid asFid@((_, bFid) : _) =- let fid = bfid bFid- eqpBody (aid, b) =- ( (aid, b)- , map snd $ fullAssocs cops (sdiscoKind ser) (sdiscoEffect ser)- aid [COrgan, CEqp] s )- in (fid, map eqpBody asFid)- in EM.fromDistinctAscList $ map bodyFid asGrouped- itemsOnFloor :: Level -> [(Point, [ItemFull])]- itemsOnFloor lvl =- let iToFull (iid, (item, kit)) =- itemToFull cops (sdiscoKind ser) (sdiscoEffect ser) iid item kit- processPos (p, bag) =- (p, map iToFull $ bagAssocsK s bag)- in map processPos $ EM.assocs $ lfloor lvl -- lembed are covered- -- Note that an actor can be blind or a projectile,+ let processActor aid =+ let b = getActorBody aid s+ sslOrgan = processBag3 (borgan b) emptyFovCache3+ ssl = processBag3 (beqp b) sslOrgan+ in (bfid b, [(b, ssl)])+ asLid = map processActor $ concat $ EM.elems $ lprio lvl+ in EM.fromListWith (++) asLid+ processIid lightAcc (iid, (k, _)) =+ let FovCache3{fovLight} =+ EM.findWithDefault emptyFovCache3 iid $ sItemFovCache ser+ in k * fovLight + lightAcc+ processBag bag acc = foldl' processIid acc $ EM.assocs bag+ lightOnFloor :: Level -> [(Point, Int)]+ lightOnFloor lvl =+ let processPos (p, bag) = (p, processBag bag 0)+ in map processPos $ EM.assocs $ lfloor lvl -- lembed are hidden+ -- Note that an actor can be blind, -- in which case he doesn't see his own light -- (but others, from his or other factions, possibly do).- litOnLevel :: Level -> ( PerceptionLit- , EM.EnumMap FactionId ActorEqpBody+ litOnLevel :: Level -> ( EM.EnumMap FactionId [(Actor, FovCache3)]+ , PointArray.Array Bool , PointArray.Array Bool ) litOnLevel lvl@Level{ltile} = let bodyMap = itemsInActors lvl allBodies = concat $ EM.elems bodyMap- blockingTiles = PointArray.mapA (Tile.isClear cotile) ltile- blockFromBody ((_, b), _) =+ clearTiles = PointArray.mapA (Tile.isClear cotile) ltile+ blockFromBody (b, _) = if bproj b then Nothing else Just (bpos b, False) -- TODO: keep it in server state and update when tiles change -- and actors are born/move/die. Actually, do this for PersLit. blockingActors = mapMaybe blockFromBody allBodies- blockers = blockingTiles PointArray.// blockingActors- actorItems = map (\((_, b), iis) -> (bpos b, iis)) allBodies- floorItems = itemsOnFloor lvl- allItems = floorItems ++ actorItems- in (litByItems cops blockers fovMode lvl allItems, bodyMap, blockers)+ clearPs = clearTiles PointArray.// blockingActors+ litTiles = PointArray.mapA (Tile.isLit cotile) ltile+ actorLights = map (\(b, FovCache3{fovLight}) -> (bpos b, fovLight))+ allBodies+ floorLights = lightOnFloor lvl+ -- If there is light both on the floor and carried by actor,+ -- only the stronger light is taken into account.+ -- This is rare, so no point optimizing away the double computation.+ allLights = floorLights ++ actorLights+ litDynamic = pdynamicLit $ litByItems clearPs fovMode allLights+ litPs = litTiles PointArray.// map (\p -> (p, True)) litDynamic+ in (bodyMap, clearPs, litPs) litLvl (lid, lvl) = (lid, litOnLevel lvl) in EML.fromDistinctAscList $ map litLvl $ EM.assocs $ sdungeon s @@ -205,20 +198,21 @@ -> Int -- ^ scanning radius -> Point -- ^ position of the spectator -> [Point]-fullscan blockers fovMode radius spectatorPos =- if radius <= 0 then []- else if radius == 1 then [spectatorPos]- else spectatorPos : case fovMode of- Shadow ->- concatMap (\tr -> map tr (Shadow.scan (isCl . tr) 1 (0, 1))) tr8- Permissive ->- concatMap (\tr -> map tr (Permissive.scan (isCl . tr))) tr4- Digital ->- concatMap (\tr -> map tr (Digital.scan (radius - 1) (isCl . tr))) tr4+fullscan clearPs fovMode radius spectatorPos+ | radius <= 0 = []+ | radius == 1 = [spectatorPos]+ | otherwise =+ spectatorPos : case fovMode of+ Shadow ->+ concatMap (\tr -> map tr (Shadow.scan (isCl . tr) 1 (0, 1))) tr8+ Permissive ->+ concatMap (\tr -> map tr (Permissive.scan (isCl . tr))) tr4+ Digital ->+ concatMap (\tr -> map tr (Digital.scan (radius - 1) (isCl . tr))) tr4 where isCl :: Point -> Bool {-# INLINE isCl #-}- isCl = (blockers PointArray.!)+ isCl = (clearPs PointArray.!) -- This function is cheap, so no problem it's called twice -- for each point: once with @isCl@, once via @concatMap@.
Game/LambdaHack/Server/Fov/Digital.hs view
@@ -1,8 +1,13 @@+{-# LANGUAGE CPP #-} -- | DFOV (Digital Field of View) implemented according to specification at <http://roguebasin.roguelikedevelopment.org/index.php?title=Digital_field_of_view_implementation>. -- This fast version of the algorithm, based on "PFOV", has AFAIK -- never been described nor implemented before. module Game.LambdaHack.Server.Fov.Digital- ( scan, dline, dsteeper, intersect, debugSteeper, debugLine+ ( scan+#ifdef EXPOSE_INTERNAL+ -- * Internal operations+ , dline, dsteeper, intersect, _debugSteeper, _debugLine+#endif ) where import Control.Exception.Assert.Sugar@@ -80,14 +85,21 @@ {-# INLINE dline #-} dline p1 p2 = let line = Line p1 p2- in assert (uncurry blame $ debugLine line) line+ in+#ifdef WITH_EXPENSIVE_ASSERTIONS+ assert (uncurry blame $ _debugLine line)+#endif+ line -- | Compare steepness of @(p1, f)@ and @(p2, f)@. -- Debug: Verify that the results of 2 independent checks are equal. dsteeper :: Bump -> Bump -> Bump -> Bool {-# INLINE dsteeper #-} dsteeper f p1 p2 =- assert (res == debugSteeper f p1 p2) res+#ifdef WITH_EXPENSIVE_ASSERTIONS+ assert (res == _debugSteeper f p1 p2)+#endif+ res where res = steeper f p1 p2 -- | The X coordinate, represented as a fraction, of the intersection of@@ -96,7 +108,9 @@ intersect :: Line -> Distance -> (Int, Int) {-# INLINE intersect #-} intersect (Line (B x y) (B xf yf)) d =+#ifdef WITH_EXPENSIVE_ASSERTIONS assert (allB (>= 0) [y, yf])+#endif ((d - y)*(xf - x) + x*(yf - y), yf - y) {- Derivation of the formula:@@ -130,18 +144,18 @@ -- | Debug functions for DFOV: -- | Debug: calculate steeper for DFOV in another way and compare results.-debugSteeper :: Bump -> Bump -> Bump -> Bool-{-# INLINE debugSteeper #-}-debugSteeper f@(B _xf yf) p1@(B _x1 y1) p2@(B _x2 y2) =+_debugSteeper :: Bump -> Bump -> Bump -> Bool+{-# INLINE _debugSteeper #-}+_debugSteeper f@(B _xf yf) p1@(B _x1 y1) p2@(B _x2 y2) = assert (allB (>= 0) [yf, y1, y2]) $ let (n1, k1) = intersect (Line p1 f) 0 (n2, k2) = intersect (Line p2 f) 0 in n1 * k2 >= k1 * n2 -- | Debug: check if a view border line for DFOV is legal.-debugLine :: Line -> (Bool, String)-{-# INLINE debugLine #-}-debugLine line@(Line (B x1 y1) (B x2 y2))+_debugLine :: Line -> (Bool, String)+{-# INLINE _debugLine #-}+_debugLine line@(Line (B x1 y1) (B x2 y2)) | not (allB (>= 0) [y1, y2]) = (False, "negative coordinates: " ++ show line) | y1 == y2 && x1 == x2 =
Game/LambdaHack/Server/HandleEffectServer.hs view
@@ -52,7 +52,7 @@ applyItem :: (MonadAtomic m, MonadServer m) => ActorId -> ItemId -> CStore -> m () applyItem aid iid cstore = do- execSfxAtomic $ SfxActivate aid iid cstore+ execSfxAtomic $ SfxApply aid iid cstore let c = CActor aid cstore itemEffectAndDestroy aid aid iid c @@ -84,12 +84,12 @@ let it1 = case mtimeout of Just (IK.Timeout timeout) -> let timeoutTurns = timeDeltaScale (Delta timeTurn) timeout- pending startT = timeShift startT timeoutTurns > localTime- in filter pending it+ charging startT = timeShift startT timeoutTurns > localTime+ in filter charging it _ -> [] len = length it1 recharged = len < k- assert (len <= k `blame` (kitK, source, target, iid, c)) skip+ let !_A = assert (len <= k `blame` (kitK, source, target, iid, c)) () -- If there is no Timeout, but there are Recharging, -- then such effects are disabled whenever the item is affected -- by a Discharge attack (TODO).@@ -99,8 +99,12 @@ _ -> -- TODO: if has timeout and not recharged, report failure return it1- when (it /= it2 && mtimeout /= Just (IK.Timeout 0)) $- execUpdAtomic $ UpdTimeItem iid c it it2+ -- We use up the charge even if eventualy every effect fizzles. Tough luck.+ -- At least we don't destroy the item in such case. Also, we ID it regardless.+ it3 <- if it /= it2 && mtimeout /= Just (IK.Timeout 0) then do+ execUpdAtomic $ UpdTimeItem iid c it it2+ return it2+ else return it -- If the activation is not periodic, trigger at least the effects -- that are not recharging and so don't depend on @recharged@. when (not periodic || recharged) $ do@@ -119,12 +123,12 @@ item <- getsState $ getItemBody iid let durable = IK.Durable `elem` jfeature item imperishable = durable || periodic && isNothing mtmp- kit = if isNothing mtmp || periodic then (1, take 1 it2) else (k, it2)+ kit = if isNothing mtmp || periodic then (1, take 1 it3) else (k, it3) unless imperishable $ execUpdAtomic $ UpdLoseItem iid item kit c -- At this point, the item is potentially no longer in container @c@, -- so we don't pass @c@ along.- triggered <- itemEffectDisco source target iid recharged periodic effs+ triggered <- itemEffectDisco source target iid c recharged periodic effs -- If none of item's effects was performed, we try to recreate the item. -- Regardless, we don't rewind the time, because some info is gained -- (that the item does not exhibit any effects in the given context).@@ -155,28 +159,17 @@ -- is mutually recursive with @effect@ and so it's a part of @Effect@ -- semantics. itemEffectDisco :: (MonadAtomic m, MonadServer m)- => ActorId -> ActorId -> ItemId -> Bool -> Bool+ => ActorId -> ActorId -> ItemId -> Container -> Bool -> Bool -> [IK.Effect] -> m Bool-itemEffectDisco source target iid recharged periodic effs = do+itemEffectDisco source target iid c recharged periodic effs = do discoKind <- getsServer sdiscoKind item <- getsState $ getItemBody iid case EM.lookup (jkindIx item) discoKind of Just itemKindId -> do- triggered <- itemEffect source target iid recharged periodic effs- -- The effect fires up, so the item gets identified, if seen- -- (the item was at the source actor's position, so his old position- -- is given, since the actor and/or the item may be moved by the effect;- -- we'd need to track not only position of atomic commands and factions,- -- but also which items they relate to, to be fully accurate).- when triggered $ do- postb <- getsState $ getActorBody source- seed <- getsServer $ (EM.! iid) . sitemSeedD- -- Not giving a container to UpdDiscover, because the actor- -- from the container can be dead, etc.- execUpdAtomic $ UpdDiscover (blid postb) (bpos postb)- iid itemKindId seed- return triggered+ seed <- getsServer $ (EM.! iid) . sitemSeedD+ execUpdAtomic $ UpdDiscover c iid itemKindId seed+ itemEffect source target iid recharged periodic effs _ -> assert `failure` (source, target, iid, item) itemEffect :: (MonadAtomic m, MonadServer m)@@ -211,26 +204,26 @@ let execSfx = execSfxAtomic $ SfxEffect (bfid sb) target effect case effect of IK.NoEffect _ -> return False- IK.Hurt nDm -> effectHurt nDm source target False- IK.Burn p -> effectBurn execSfx p source target+ IK.Hurt nDm -> effectHurt nDm source target IK.RefillHP+ IK.Burn nDm -> effectBurn nDm source target IK.Explode t -> effectExplode execSfx t target IK.RefillHP p -> effectRefillHP False execSfx p source target IK.OverfillHP p -> effectRefillHP True execSfx p source target- IK.RefillCalm p -> effectRefillCalm False execSfx p target- IK.OverfillCalm p -> effectRefillCalm True execSfx p target+ IK.RefillCalm p -> effectRefillCalm False execSfx p source target+ IK.OverfillCalm p -> effectRefillCalm True execSfx p source target IK.Dominate -> effectDominate recursiveCall source target IK.Impress -> effectImpress execSfx source target IK.CallFriend p -> effectCallFriend p source target- IK.Summon freqs p -> effectSummon recursiveCall freqs p source target- IK.Ascend p -> effectAscend recursiveCall execSfx p target- IK.Escape{} -> effectEscape target+ IK.Summon freqs p -> effectSummon freqs p source target+ IK.Ascend p -> effectAscend recursiveCall execSfx p source target+ IK.Escape{} -> effectEscape source target IK.Paralyze p -> effectParalyze execSfx p target IK.InsertMove p -> effectInsertMove execSfx p target- IK.Teleport p -> effectTeleport execSfx p target+ IK.Teleport p -> effectTeleport execSfx p source target IK.CreateItem store grp tim -> effectCreateItem target store grp tim IK.DropItem store grp hit -> effectDropItem execSfx store grp hit target- IK.PolyItem cstore -> effectPolyItem execSfx cstore target- IK.Identify cstore -> effectIdentify iid (bfid sb) cstore target+ IK.PolyItem -> effectPolyItem execSfx source target+ IK.Identify -> effectIdentify execSfx iid source target IK.SendFlying tmod -> effectSendFlying execSfx tmod source target Nothing IK.PushActor tmod ->@@ -251,18 +244,17 @@ -- Modified by armor. Can, exceptionally, add HP. effectHurt :: (MonadAtomic m, MonadServer m)- => Dice.Dice -> ActorId -> ActorId -> Bool+ => Dice.Dice -> ActorId -> ActorId -> (Int -> IK.Effect) -> m Bool-effectHurt nDm source target silent = do+effectHurt nDm source target verboseEffectConstructor = do sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target hpMax <- sumOrganEqpServer IK.EqpSlotAddMaxHP target n <- rndToAction $ castDice (AbsDepth 0) (AbsDepth 0) nDm hurtBonus <- armorHurtBonus source target- let block = braced tb- mult = (100 + hurtBonus) * (if block then 50 else 100)+ let mult = 100 + hurtBonus rawDeltaHP = - (max oneM -- at least 1 HP taken- (fromIntegral mult * xM n `divUp` (100 * 100)))+ (fromIntegral mult * xM n `divUp` 100)) serious = source /= target && not (bproj tb) deltaHP | serious = -- if HP overfull, at least cut back to max HP min rawDeltaHP (xM hpMax - bhp tb)@@ -271,10 +263,12 @@ -- Damage the target. execUpdAtomic $ UpdRefillHP target deltaHP when serious $ halveCalm target- unless silent $ execSfxAtomic $ SfxEffect (bfid sb) target $+ execSfxAtomic $ SfxEffect (bfid sb) target $ if source == target- then IK.RefillHP deltaDiv -- no SfxStrike, so treat as any heal/wound- else IK.Hurt (Dice.intToDice deltaDiv) -- SfxStrike sent, avoid spam+ then verboseEffectConstructor deltaDiv+ -- no SfxStrike, so treat as any heal/wound+ else IK.Hurt (Dice.intToDice (- deltaDiv))+ -- SfxStrike already sent, avoid spam return True armorHurtBonus :: (MonadAtomic m, MonadServer m)@@ -284,11 +278,15 @@ sactiveItems <- activeItemsServer source tactiveItems <- activeItemsServer target sb <- getsState $ getActorBody source- return $! if bproj sb- then sumSlotNoFilter IK.EqpSlotAddHurtRanged sactiveItems- - sumSlotNoFilter IK.EqpSlotAddArmorRanged tactiveItems- else sumSlotNoFilter IK.EqpSlotAddHurtMelee sactiveItems- - sumSlotNoFilter IK.EqpSlotAddArmorMelee tactiveItems+ tb <- getsState $ getActorBody target+ let itemBonus =+ if bproj sb+ then sumSlotNoFilter IK.EqpSlotAddHurtRanged sactiveItems+ - sumSlotNoFilter IK.EqpSlotAddArmorRanged tactiveItems+ else sumSlotNoFilter IK.EqpSlotAddHurtMelee sactiveItems+ - sumSlotNoFilter IK.EqpSlotAddArmorMelee tactiveItems+ block = braced tb+ return $! itemBonus - if block then 50 else 0 halveCalm :: (MonadAtomic m, MonadServer m) => ActorId -> m ()@@ -309,12 +307,10 @@ -- Damage from both impact and fire. Modified by armor. effectBurn :: (MonadAtomic m, MonadServer m)- => m () -> Int -> ActorId -> ActorId+ => Dice.Dice -> ActorId -> ActorId -> m Bool-effectBurn execSfx power source target = assert (power > 0) $ do- void $ effectHurt (Dice.intToDice power) source target True- execSfx- return True+effectBurn nDm source target =+ effectHurt nDm source target (\p -> IK.Burn $ Dice.intToDice (-p)) -- ** Explode @@ -333,8 +329,9 @@ -- from the source. Otherwise, e.g., the points on cardinal -- and diagonal lines from the source would be more common. let fuzz = 2 + (k100 `xor` (itemK * n)) `mod` 9- k = if itemK >= 8 && n < 8 then 0- else if n < 8 && n >= 4 then 4 else n+ k | itemK >= 8 && n < 8 = 0+ | n < 8 && n >= 4 = 4+ | otherwise = n psAll = [ Point (x - 12) $ y + fuzz , Point (x + 12) $ y - fuzz@@ -363,13 +360,13 @@ forM_ [101..201] $ \k100 -> do bag2 <- getsState $ beqp . getActorBody target let mn2 = EM.lookup iid bag2- maybe skip (projectN k100) mn2+ maybe (return ()) (projectN k100) mn2 bag3 <- getsState $ beqp . getActorBody target let mn3 = EM.lookup iid bag3- maybe skip (\kit -> execUpdAtomic- $ UpdLoseItem iid itemBase kit container) mn3+ maybe (return ()) (\kit -> execUpdAtomic+ $ UpdLoseItem iid itemBase kit container) mn3 execSfx- return True -- we avoid verifying that at least one projectile got off+ return True -- we neglect verifying that at least one projectile got off -- ** RefillHP @@ -381,34 +378,38 @@ hpMax <- sumOrganEqpServer IK.EqpSlotAddMaxHP target let overMax | overfill = xM hpMax * 10 -- arbitrary limit to scumming | otherwise = xM hpMax- serious = overfill && source /= target && not (bproj tb)+ serious = not (bproj tb) && source /= target && power > 1 deltaHP | power > 0 = min (xM power) (max 0 $ overMax - bhp tb)- | serious = -- if HP overfull, at least cut back to max HP+ | serious = -- if overfull, at least cut back to max min (xM power) (xM hpMax - bhp tb) | otherwise = xM power if deltaHP == 0 then return False else do execUpdAtomic $ UpdRefillHP target deltaHP- when (deltaHP < 0 && serious) $ halveCalm target execSfx+ when (deltaHP < 0 && serious) $ halveCalm target return True -- ** RefillCalm effectRefillCalm :: (MonadAtomic m, MonadServer m)- => Bool -> m () -> Int -> ActorId -> m Bool-effectRefillCalm overfill execSfx power target = do+ => Bool -> m () -> Int -> ActorId -> ActorId -> m Bool+effectRefillCalm overfill execSfx power source target = do tb <- getsState $ getActorBody target calmMax <- sumOrganEqpServer IK.EqpSlotAddMaxCalm target let overMax | overfill = xM calmMax * 10 -- arbitrary limit to scumming | otherwise = xM calmMax- let deltaCalm = min (xM power) (max 0 $ overMax - bcalm tb)+ serious = not (bproj tb) && source /= target && power > 1+ deltaCalm | power > 0 = min (xM power) (max 0 $ overMax - bcalm tb)+ | serious = -- if overfull, at least cut back to max+ min (xM power) (xM calmMax - bcalm tb)+ | otherwise = xM power if deltaCalm == 0 then return False else do- execUpdAtomic $ UpdRefillCalm target deltaCalm execSfx+ execUpdAtomic $ UpdRefillCalm target deltaCalm return True -- ** Dominate@@ -420,9 +421,9 @@ effectDominate recursiveCall source target = do sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target- if bproj tb then- return False- else if bfid tb == bfid sb then+ if bfid tb == bfid sb then+ -- Dominate is rather on projectiles than on items, so alternate effect+ -- is useful to avoid boredom if domination can't happen. recursiveCall IK.Impress else dominateFidSfx (bfid sb) target@@ -434,15 +435,16 @@ effectImpress execSfx source target = do sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target- if boldfid tb == bfid sb || bproj tb then+ if bfidImpressed tb == bfid sb || bproj tb then return False else do execSfx- execUpdAtomic $ UpdOldFidActor target (boldfid tb) (bfid sb)+ execUpdAtomic $ UpdFidImpressedActor target (bfidImpressed tb) (bfid sb) return True -- ** CallFriend +-- Note that the Calm expended doesn't depend on the number of actors called. effectCallFriend :: (MonadAtomic m, MonadServer m) => Dice.Dice -> ActorId -> ActorId -> m Bool@@ -451,51 +453,59 @@ Kind.COps{cotile} <- getsState scops power <- rndToAction $ castDice (AbsDepth 0) (AbsDepth 0) nDm sb <- getsState $ getActorBody source- activeItems <- activeItemsServer source- let legal = source == target- && hpEnough10 sb activeItems -- prevent spam from regen wimps- if not legal then return False+ tb <- getsState $ getActorBody target+ activeItems <- activeItemsServer target+ if not $ hpEnough10 tb activeItems then do+ unless (bproj tb) $ do+ let subject = partActor tb+ verb = "lack enough HP to call aid"+ msg = makeSentence [MU.SubjectVerbSg subject verb]+ execSfxAtomic $ SfxMsgFid (bfid sb) msg+ return False else do- let hpMax = max 1 $ sumSlotNoFilter IK.EqpSlotAddMaxHP activeItems- deltaHP = - xM hpMax `div` 3- execUpdAtomic $ UpdRefillHP source deltaHP+ let deltaHP = - xM 10+ execUpdAtomic $ UpdRefillHP target deltaHP let validTile t = not $ Tile.hasFeature cotile TK.NoActor t- lid = blid sb- ps <- getsState $ nearbyFreePoints validTile (bpos sb) lid- time <- getsState $ getLocalTime lid- recruitActors (take power ps) lid time (bfid sb)+ ps <- getsState $ nearbyFreePoints validTile (bpos tb) (blid tb)+ time <- getsState $ getLocalTime (blid tb)+ -- We call target's friends so that AI monsters that test by throwing+ -- don't waste artifacts very valuable for heroes. Heroes should rather+ -- not test scrolls by throwing.+ recruitActors (take power ps) (blid tb) time (bfid tb) -- ** Summon +-- Note that the Calm expended doesn't depend on the number of actors summoned. effectSummon :: (MonadAtomic m, MonadServer m)- => (IK.Effect -> m Bool)- -> Freqs ItemKind -> Dice.Dice- -> ActorId -> ActorId+ => Freqs ItemKind -> Dice.Dice -> ActorId -> ActorId -> m Bool-effectSummon recursiveCall actorFreq nDm source target = do+effectSummon actorFreq nDm source target = do -- Obvious effect, nothing announced. Kind.COps{cotile} <- getsState scops power <- rndToAction $ castDice (AbsDepth 0) (AbsDepth 0) nDm sb <- getsState $ getActorBody source- activeItems <- activeItemsServer source- let legal = source == target && (bproj sb || calmEnough10 sb activeItems)- if not legal then return False+ tb <- getsState $ getActorBody target+ activeItems <- activeItemsServer target+ if not $ calmEnough10 tb activeItems then do+ unless (bproj tb) $ do+ let subject = partActor tb+ verb = "lack enough Calm to summon"+ msg = makeSentence [MU.SubjectVerbSg subject verb]+ execSfxAtomic $ SfxMsgFid (bfid sb) msg+ return False else do- let calmMax = max 1 $ sumSlotNoFilter IK.EqpSlotAddMaxCalm activeItems- deltaCalm = - xM calmMax `div` 3- unless (bproj sb) $ execUpdAtomic $ UpdRefillCalm source deltaCalm+ let deltaCalm = - xM 10+ unless (bproj tb) $ execUpdAtomic $ UpdRefillCalm target deltaCalm let validTile t = not $ Tile.hasFeature cotile TK.NoActor t- ps <- getsState $ nearbyFreePoints validTile (bpos sb) (blid sb)- localTime <- getsState $ getLocalTime (blid sb)+ ps <- getsState $ nearbyFreePoints validTile (bpos tb) (blid tb)+ localTime <- getsState $ getLocalTime (blid tb) -- Make sure summoned actors start acting after the summoner.- let sourceTime = timeShift localTime $ ticksPerMeter $ bspeed sb activeItems- afterTime = timeShift sourceTime $ Delta timeClip+ let targetTime = timeShift localTime $ ticksPerMeter $ bspeed tb activeItems+ afterTime = timeShift targetTime $ Delta timeClip bs <- forM (take power ps) $ \p -> do- maid <- addAnyActor actorFreq (blid sb) afterTime (Just p)+ maid <- addAnyActor actorFreq (blid tb) afterTime (Just p) case maid of- Nothing ->- -- Don't make this item useless.- recursiveCall (IK.CallFriend 1)+ Nothing -> return False -- actorFreq is null; content writers... Just aid -> do b <- getsState $ getActorBody aid mleader <- getsState $ gleader . (EM.! bfid b) . sfactionD@@ -510,25 +520,31 @@ -- Note that projectiles can be teleported, too, for extra fun. effectAscend :: (MonadAtomic m, MonadServer m) => (IK.Effect -> m Bool)- -> m () -> Int -> ActorId+ -> m () -> Int -> ActorId -> ActorId -> m Bool-effectAscend recursiveCall execSfx k aid = do- b1 <- getsState $ getActorBody aid+effectAscend recursiveCall execSfx k source target = do+ b1 <- getsState $ getActorBody target ais1 <- getsState $ getCarriedAssocs b1 let lid1 = blid b1 pos1 = bpos b1 (lid2, pos2) <- getsState $ whereTo lid1 pos1 k . sdungeon- if lid2 == lid1 && pos2 == pos1 then do- execSfxAtomic $ SfxMsgFid (bfid b1) "No more levels in this direction."+ sb <- getsState $ getActorBody source+ if braced b1 then do+ execSfxAtomic $ SfxMsgFid (bfid sb)+ "Braced actors are immune to translocation."+ return False+ else if lid2 == lid1 && pos2 == pos1 then do+ execSfxAtomic $ SfxMsgFid (bfid sb) "No more levels in this direction."+ -- We keep it useful even in shallow dungeons. recursiveCall $ IK.Teleport 30 -- powerful teleport else do- let switch1 = void $ switchLevels1 ((aid, b1), ais1)+ let switch1 = void $ switchLevels1 ((target, b1), ais1) switch2 = do -- Make the initiator of the stair move the leader, -- to let him clear the stairs for others to follow.- let mlead = Just aid+ let mlead = Just target -- Move the actor to where the inhabitants were, if any.- switchLevels2 lid2 pos2 ((aid, b1), ais1) mlead+ switchLevels2 lid2 pos2 ((target, b1), ais1) mlead -- Verify only one non-projectile actor on every tile. !_ <- getsState $ posToActors pos1 lid1 -- assertion is inside !_ <- getsState $ posToActors pos2 lid2 -- assertion is inside@@ -590,7 +606,7 @@ switchLevels2 lidNew posNew ((aid, bOld), ais) mlead = do let lidOld = blid bOld side = bfid bOld- assert (lidNew /= lidOld `blame` "stairs looped" `twith` lidNew) skip+ let !_A = assert (lidNew /= lidOld `blame` "stairs looped" `twith` lidNew) () -- Sync the actor time with the level time. timeOld <- getsState $ getLocalTime lidOld timeLastActive <- getsState $ getLocalTime lidNew@@ -602,7 +618,7 @@ computeNewTimeout :: ItemQuant -> ItemQuant computeNewTimeout (k, it) = (k, map shiftByDelta it) setTimeout :: ItemBag -> ItemBag- setTimeout bag = EM.map computeNewTimeout bag+ setTimeout = EM.map computeNewTimeout bNew = bOld { blid = lidNew , btime = shiftByDelta $ btime bOld , bpos = posNew@@ -622,16 +638,21 @@ -- ** Escape -- | The faction leaves the dungeon.-effectEscape :: (MonadAtomic m, MonadServer m) => ActorId -> m Bool-effectEscape target = do+effectEscape :: (MonadAtomic m, MonadServer m) => ActorId -> ActorId -> m Bool+effectEscape source target = do -- Obvious effect, nothing announced.+ sb <- getsState $ getActorBody source b <- getsState $ getActorBody target let fid = bfid b fact <- getsState $ (EM.! fid) . sfactionD- if not (fcanEscape $ gplayer fact) || bproj b then+ if bproj b then return False+ else if not (fcanEscape $ gplayer fact) then do+ execSfxAtomic $ SfxMsgFid (bfid sb)+ "This faction doesn't want to escape outside."+ return False else do- deduceQuits b $ Status Escape (fromEnum $ blid b) Nothing+ deduceQuits fid Nothing $ Status Escape (fromEnum $ blid b) Nothing return True -- ** Paralyze@@ -672,10 +693,11 @@ -- | Teleport the target actor. -- Note that projectiles can be teleported, too, for extra fun. effectTeleport :: (MonadAtomic m, MonadServer m)- => m () -> Dice.Dice -> ActorId -> m Bool-effectTeleport execSfx nDm target = do+ => m () -> Dice.Dice -> ActorId -> ActorId -> m Bool+effectTeleport execSfx nDm source target = do Kind.COps{cotile} <- getsState scops range <- rndToAction $ castDice (AbsDepth 0) (AbsDepth 0) nDm+ sb <- getsState $ getActorBody source b <- getsState $ getActorBody target Level{ltile} <- getLevel (blid b) as <- getsState $ actorList (const True) (blid b)@@ -689,15 +711,20 @@ && (not (dMinMax 9 p) -- don't loop, very rare || not (Tile.hasFeature cotile TK.NoActor t) && unoccupied as p))- [ dist $ 1+ [ dist 1 , dist $ 1 + range `div` 9 , dist $ 1 + range `div` 7 , dist $ 1 + range `div` 5- , dist $ 5- , dist $ 7+ , dist 5+ , dist 7 ]- if not (dMinMax 9 tpos) then- return False -- very rare+ if braced b then do+ execSfxAtomic $ SfxMsgFid (bfid sb)+ "Braced actors are immune to translocation."+ return False+ else if not (dMinMax 9 tpos) then do -- very rare+ execSfxAtomic $ SfxMsgFid (bfid sb) "Translocation not possible."+ return False else do execUpdAtomic $ UpdMoveActor target spos tpos execSfx@@ -705,6 +732,10 @@ -- ** CreateItem +-- TODO: if the items is created not on the ground, perhaps it should+-- be IDed, so that there are no rings with unkown max Calm bonus+-- leading to attempts to do illegal actions (which the server then catches).+-- This is in analogy to picking item from the ground, whereas it's IDed. effectCreateItem :: (MonadAtomic m, MonadServer m) => ActorId -> CStore -> GroupName ItemKind -> IK.TimerDice -> m Bool@@ -714,21 +745,21 @@ IK.TimerNone -> return $ Delta timeZero IK.TimerGameTurn nDm -> do k <- rndToAction $ castDice (AbsDepth 0) (AbsDepth 0) nDm- assert (k >= 0) skip+ let !_A = assert (k >= 0) () return $! timeDeltaScale (Delta timeTurn) k IK.TimerActorTurn nDm -> do k <- rndToAction $ castDice (AbsDepth 0) (AbsDepth 0) nDm- assert (k >= 0) skip+ let !_A = assert (k >= 0) () activeItems <- activeItemsServer target let actorTurn = ticksPerMeter $ bspeed tb activeItems return $! timeDeltaScale actorTurn k let c = CActor target store bagBefore <- getsState $ getCBag c let litemFreq = [(grp, 1)]- m4 <- rollItem (blid tb) litemFreq- let (itemKnown, itemFull, seed, _) = case m4 of- Nothing -> assert `failure` (blid tb, litemFreq, c)- Just i4 -> i4+ -- Power depth of new items unaffected by number of spawned actors.+ m5 <- rollItem 0 (blid tb) litemFreq+ let (itemKnown, itemFull, _, seed, _) =+ fromMaybe (assert `failure` (blid tb, litemFreq, c)) m5 itemRev <- getsServer sitemRev let mquant = case HM.lookup itemKnown itemRev of Nothing -> Nothing@@ -744,15 +775,14 @@ _ -> do -- Multiple such items, so it's a periodic poison, etc., so just stack, -- or no such items at all, so create some.- iid <- registerItem (itemBase itemFull) itemKnown seed- (itemK itemFull) c True+ iid <- registerItem itemFull itemKnown seed (itemK itemFull) c True unless (tim == IK.TimerNone) $ do bagAfter <- getsState $ getCBag c localTime <- getsState $ getLocalTime (blid tb) let newTimer = localTime `timeShift` delta- (afterK, afterIt) = case iid `EM.lookup` bagAfter of- Nothing -> assert `failure` (iid, bagAfter, c)- Just kit -> kit+ (afterK, afterIt) =+ fromMaybe (assert `failure` (iid, bagAfter, c))+ (iid `EM.lookup` bagAfter) newIt = replicate afterK newTimer when (afterIt /= newIt) $ execUpdAtomic $ UpdTimeItem iid c afterIt newIt@@ -782,7 +812,7 @@ if null is then return False else do- mapM_ (\(iid, kit) -> dropCStoreItem store target b hit iid kit) is+ mapM_ (uncurry (dropCStoreItem store target b hit)) is unless (store == COrgan) execSfx return True @@ -815,76 +845,90 @@ -- ** PolyItem +-- TODO: ask player for an item effectPolyItem :: (MonadAtomic m, MonadServer m)- => m () -> CStore -> ActorId -> m Bool-effectPolyItem execSfx cstore target = do+ => m () -> ActorId -> ActorId -> m Bool+effectPolyItem execSfx source target = do+ sb <- getsState $ getActorBody source+ let cstore = CGround allAssocs <- fullAssocsServer target [cstore] case allAssocs of [] -> do- tb <- getsState $ getActorBody target- execSfxAtomic $ SfxMsgFid (bfid tb) $+ execSfxAtomic $ SfxMsgFid (bfid sb) $ "The purpose of repurpose cannot be availed without an item"- <+> ppCStore cstore <> "."- -- TODO: identify the scroll, but don't use up.- return True+ <+> ppCStoreIn cstore <> "."+ return False (iid, itemFull@ItemFull{..}) : _ -> case itemDisco of- Just ItemDisco{itemKind} -> do+ Just ItemDisco{..} -> do+ discoEffect <- getsServer sdiscoEffect let maxCount = Dice.maxDice $ IK.icount itemKind- if itemK >= maxCount- then do+ aspects = jaspects $ discoEffect EM.! iid+ if itemK < maxCount then do+ execSfxAtomic $ SfxMsgFid (bfid sb) $+ "The purpose of repurpose is served by" <+> tshow maxCount+ <+> "pieces of this item, not by" <+> tshow itemK <> "."+ return False+ else if IK.Unique `elem` aspects then do+ execSfxAtomic $ SfxMsgFid (bfid sb) $+ "Unique items can't be repurposed."+ return False+ else do let c = CActor target cstore kit = (maxCount, take maxCount itemTimer)+ identifyIid execSfx iid c itemKindId execUpdAtomic $ UpdDestroyItem iid itemBase kit c- execSfx effectCreateItem target cstore "useful" IK.TimerNone- else do- tb <- getsState $ getActorBody target- execSfxAtomic $ SfxMsgFid (bfid tb) $- "The purpose of repurpose is served by" <+> tshow maxCount- <+> "pieces of this item, not by" <+> tshow itemK <> "."- -- TODO: identify the scroll, but don't use up.- return True- _ -> assert `failure` (cstore, target, iid, itemFull)+ _ -> assert `failure` (target, iid, itemFull) -- ** Identify -- TODO: ask player for an item, because server doesn't know which -- is already identified, it only knows which cannot ever be.+-- Perhaps refill Calm only when id successfull and scroll consumed,+-- id the scroll anyway. Explain the Calm gain: "your most pressing+-- existential concerns are answered scientifitically". effectIdentify :: (MonadAtomic m, MonadServer m)- => ItemId -> FactionId -> CStore -> ActorId -> m Bool-effectIdentify iidId fid storeInitial target = do+ => m () -> ItemId -> ActorId -> ActorId -> m Bool+effectIdentify execSfx iidId source target = do+ sb <- getsState $ getActorBody source let tryFull store as = case as of- [] -> return False- (iid, itemFull@ItemFull{..}) : rest -> case itemDisco of- _ | iid == iidId -> tryFull store rest -- don't id itself- Just ItemDisco{..} -> do- -- TODO: use this (but faster, via traversing effects with 999)- -- also to prevent sending any other UpdDiscover.- let ided = IK.Identified `elem` IK.ifeature itemKind- itemSecret = itemNoAE itemFull- c = CActor target store- statsObvious = textAllAE False c itemFull- == textAllAE False c itemSecret- if ided && statsObvious- then tryFull store rest- else do- let effect = IK.Identify store -- the real store, not initial- execSfxAtomic $ SfxEffect fid target effect -- a cheat- tb <- getsState $ getActorBody target- seed <- getsServer $ (EM.! iid) . sitemSeedD- execUpdAtomic $- UpdDiscover (blid tb) (bpos tb) iid itemKindId seed- return True- _ -> assert `failure` (store, target, iid, itemFull)+ -- TODO: identify the scroll, but don't use up.+ [] -> do+ let (tIn, t) = ppCStore store+ msg = "Nothing to identify" <+> tIn <+> t <> "."+ execSfxAtomic $ SfxMsgFid (bfid sb) msg+ return False+ (iid, _) : rest | iid == iidId -> tryFull store rest -- don't id itself+ (iid, itemFull@ItemFull{itemDisco=Just ItemDisco{..}}) : rest -> do+ -- TODO: use this (but faster, via traversing effects with 999?)+ -- also to prevent sending any other UpdDiscover.+ let ided = IK.Identified `elem` IK.ifeature itemKind+ itemSecret = itemNoAE itemFull+ statsObvious = textAllAE 7 False store itemFull+ == textAllAE 7 False store itemSecret+ if ided && statsObvious+ then tryFull store rest+ else do+ let c = CActor target store+ identifyIid execSfx iid c itemKindId+ return True+ _ -> assert `failure` (store, as) tryStore stores = case stores of [] -> return False store : rest -> do allAssocs <- fullAssocsServer target [store] go <- tryFull store allAssocs if go then return True else tryStore rest- storesSorted = storeInitial : delete storeInitial [CGround, CInv, CEqp]- tryStore storesSorted+ tryStore [CGround] +identifyIid :: (MonadAtomic m, MonadServer m)+ => m () -> ItemId -> Container -> Kind.Id ItemKind+ -> m ()+identifyIid execSfx iid c itemKindId = do+ execSfx+ seed <- getsServer $ (EM.! iid) . sitemSeedD+ execUpdAtomic $ UpdDiscover c iid itemKindId seed+ -- ** SendFlying -- | Shend the target actor flying like a projectile. The arguments correspond@@ -899,11 +943,16 @@ effectSendFlying execSfx IK.ThrowMod{..} source target modePush = do v <- sendFlyingVector source target modePush Kind.COps{cotile} <- getsState scops+ sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target lvl@Level{lxsize, lysize} <- getLevel (blid tb) let eps = 0 fpos = bpos tb `shift` v- case bla lxsize lysize eps (bpos tb) fpos of+ if braced tb then do+ execSfxAtomic $ SfxMsgFid (bfid sb)+ "Braced actors are immune to translocation."+ return False+ else case bla lxsize lysize eps (bpos tb) fpos of Nothing -> assert `failure` (fpos, tb) Just [] -> assert `failure` "projecting from the edge of level" `twith` (fpos, tb)@@ -918,16 +967,26 @@ (trajectory, (speed, _)) = computeTrajectory weight throwVelocity throwLinger path ts = Just (trajectory, speed)- unless (btrajectory tb == ts) $- execUpdAtomic $ UpdTrajectory target (btrajectory tb) ts- execSfx- return True+ if null trajectory || btrajectory tb == ts+ || throwVelocity <= 0 || throwLinger <= 0+ then return False -- e.g., actor is too heavy; OK+ else do+ execUpdAtomic $ UpdTrajectory target (btrajectory tb) ts+ -- Give the actor one extra turn and also let the push start ASAP.+ -- So, if the push lasts one (his) turn, he will not lose+ -- any turn of movement (but he may need to retrace the push).+ activeItems <- activeItemsServer target+ let tpm = ticksPerMeter $ bspeed tb activeItems+ delta = timeDeltaScale tpm (-1)+ execUpdAtomic $ UpdAgeActor target delta+ execSfx+ return True sendFlyingVector :: (MonadAtomic m, MonadServer m) => ActorId -> ActorId -> Maybe Bool -> m Vector sendFlyingVector source target modePush = do sb <- getsState $ getActorBody source- if source == target then do+ if source == target then if boldpos sb == bpos sb then rndToAction $ do z <- randomR (-10, 10) oneOf [Vector 10 z, Vector (-10) z, Vector z 10, Vector z (-10)]@@ -956,12 +1015,13 @@ effectDropBestWeapon :: (MonadAtomic m, MonadServer m) => m () -> ActorId -> m Bool effectDropBestWeapon execSfx target = do+ tb <- getsState $ getActorBody target allAssocs <- fullAssocsServer target [CEqp]- case strongestSlotNoFilter IK.EqpSlotWeapon allAssocs of+ localTime <- getsState $ getLocalTime (blid tb)+ case strongestMelee False localTime allAssocs of (_, (iid, _)) : _ -> do- b <- getsState $ getActorBody target- let kit = beqp b EM.! iid- dropCStoreItem CEqp target b False iid kit+ let kit = beqp tb EM.! iid+ dropCStoreItem CEqp target tb False iid kit execSfx return True [] ->@@ -969,13 +1029,13 @@ -- ** ActivateInv --- | Activate all activable items with the given symbol+-- | Activate all items with the given symbol -- in the target actor's equipment (there's no variant that activates -- a random one, to avoid the incentive for carrying garbage). -- Only one item of each stack is activated (and possibly consumed). effectActivateInv :: (MonadAtomic m, MonadServer m) => m () -> ActorId -> Char -> m Bool-effectActivateInv execSfx target symbol = do+effectActivateInv execSfx target symbol = effectTransformEqp execSfx target symbol CInv $ \iid _ -> applyItem target iid CInv @@ -1018,8 +1078,14 @@ -> [IK.Effect] -> m Bool effectOneOf recursiveCall l = do- ef <- rndToAction $ oneOf l- recursiveCall ef+ let call1 = do+ ef <- rndToAction $ oneOf l+ recursiveCall ef+ call99 = replicate 99 call1+ f callNext result = do+ b <- result+ if b then return True else callNext+ foldr f (return False) call99 -- ** Recharging @@ -1040,6 +1106,6 @@ effectTemporary execSfx source iid = do bag <- getsState $ getCBag $ CActor source COrgan case iid `EM.lookup` bag of- Just _ -> skip -- still some copies left of a multi-copy tmp item+ Just _ -> return () -- still some copies left of a multi-copy tmp item Nothing -> execSfx -- last copy just destroyed return True
Game/LambdaHack/Server/HandleRequestServer.hs view
@@ -12,6 +12,7 @@ ( handleRequestAI, handleRequestUI, reqMove ) where +import Control.Applicative import Control.Exception.Assert.Sugar import Control.Monad import qualified Data.EnumMap.Strict as EM@@ -54,7 +55,7 @@ ReqAILeader aidNew mtgtNew cmd2 -> do switchLeader fid aidNew mtgtNew handleRequestAI fid aidNew cmd2- ReqAIPong -> return (aid, skip)+ ReqAIPong -> return (aid, return ()) -- | The semantics of server commands. The resulting actor id -- is of the actor that carried out the request. @Nothing@ means@@ -75,7 +76,7 @@ ReqUIGameSave -> return (Nothing, reqGameSave) ReqUITactic toT -> return (Nothing, reqTactic fid toT) ReqUIAutomate -> return (Nothing, reqAutomate fid)- ReqUIPong _ -> return (Nothing, skip)+ ReqUIPong _ -> return (Nothing, return ()) handleRequestTimed :: (MonadAtomic m, MonadServer m) => ActorId -> RequestTimed a -> m ()@@ -85,8 +86,7 @@ ReqDisplace target -> reqDisplace aid target ReqAlter tpos mfeat -> reqAlter aid tpos mfeat ReqWait -> reqWait aid- ReqMoveItem iid k fromCStore toCStore ->- reqMoveItem aid iid k fromCStore toCStore+ ReqMoveItems l -> reqMoveItems aid l ReqProject p eps iid cstore -> reqProject aid p eps iid cstore ReqApply iid cstore -> reqApply aid iid cstore ReqTrigger mfeat -> reqTrigger aid mfeat@@ -98,12 +98,12 @@ bPre <- getsState $ getActorBody aidNew let mleader = gleader fact actorChanged = fmap fst mleader /= Just aidNew- assert (Just (aidNew, mtgtNew) /= mleader- && not (bproj bPre)- `blame` (aidNew, mtgtNew, bPre, fid, fact)) skip- assert (bfid bPre == fid- `blame` "client tries to move other faction actors"- `twith` (aidNew, mtgtNew, bPre, fid, fact)) skip+ let !_A = assert (Just (aidNew, mtgtNew) /= mleader+ && not (bproj bPre)+ `blame` (aidNew, mtgtNew, bPre, fid, fact)) ()+ let !_A = assert (bfid bPre == fid+ `blame` "client tries to move other faction actors"+ `twith` (aidNew, mtgtNew, bPre, fid, fact)) () let (autoDun, autoLvl) = autoDungeonLevel fact arena <- case mleader of Nothing -> return $! blid bPre@@ -121,22 +121,27 @@ -- TODO: let only some actors/items leave smell, e.g., a Smelly Hide Armour -- and then remove the efficiency hack below that only heroes leave smell -- | Add a smell trace for the actor to the level. For now, only heroes--- leave smell.+-- leave smell. If smell already there and the actor can smell, remove smell. addSmell :: (MonadAtomic m, MonadServer m) => ActorId -> m () addSmell aid = do b <- getsState $ getActorBody aid fact <- getsState $ (EM.! bfid b) . sfactionD smellRadius <- sumOrganEqpServer IK.EqpSlotAddSmell aid- -- TODO: right now only humans leave smell and content should not- -- give humans the ability to smell (dominated monsters are rare enough).- -- In the future smells should be marked by the faction that left them- -- and actors shold only follow enemy smells.- unless (bproj b || not (fhasGender $ gplayer fact) || smellRadius > 0) $ do+ let dumbMonster = not (fhasGender $ gplayer fact) && smellRadius <= 0+ unless (bproj b || dumbMonster) $ do+ -- TODO: right now only humans leave smell and content should not+ -- give humans the ability to smell (dominated monsters are rare enough).+ -- In the future smells should be marked by the faction that left them+ -- and actors shold only follow enemy smells. time <- getsState $ getLocalTime $ blid b lvl <- getLevel $ blid b let oldS = EM.lookup (bpos b) . lsmell $ lvl newTime = timeShift time smellTimeout- execUpdAtomic $ UpdAlterSmell (blid b) (bpos b) oldS (Just newTime)+ newS = if smellRadius > 0+ then Nothing -- smelling monster or hero+ else Just newTime -- hero+ when (oldS /= newS) $+ execUpdAtomic $ UpdAlterSmell (blid b) (bpos b) oldS newS -- | Actor moves or attacks. -- Note that client may not be able to see an invisible monster@@ -195,14 +200,12 @@ tfid = bfid tb sfact <- getsState $ (EM.! sfid) . sfactionD hurtBonus <- armorHurtBonus source target- let isFightImpaired = hurtBonus <= -10- block = braced tb- hitA = if block && isFightImpaired- then HitBlock 2- else if block || isFightImpaired- then HitBlock 1- else HitClear- execSfxAtomic $ SfxStrike source target iid hitA+ let hitA | hurtBonus <= -50 -- e.g., braced and no hit bonus+ = HitBlock 2+ | hurtBonus <= -10 -- low bonus vs armor+ = HitBlock 1+ | otherwise = HitClear+ execSfxAtomic $ SfxStrike source target iid cstore hitA -- Deduct a hitpoint for a pierce of a projectile -- or due to a hurled actor colliding with another or a wall. case btrajectory sb of@@ -249,7 +252,7 @@ case mweapon of Nothing -> reqWait source Just (wp, cstore) -> reqMelee source target wp cstore- -- DisplaceDying, DisplaceSupported+ -- DisplaceDying, etc. else do let lid = blid sb lvl <- getLevel lid@@ -260,10 +263,8 @@ [] -> assert `failure` (source, sb, target, tb) [_] -> do execUpdAtomic $ UpdDisplaceActor source target- addSmell source- addSmell target _ -> execFailure source req DisplaceProjectiles- else do+ else -- Client foolishly tries to displace an actor without access. execFailure source req DisplaceAccess @@ -288,8 +289,8 @@ freshClientTile = hideTile cops lvl tpos changeTo tgroup = do -- No @SfxAlter@, because the effect is obvious (e.g., opened door).- toTile <- rndToAction $ fmap (fromMaybe $ assert `failure` tgroup)- $ opick tgroup (const True)+ toTile <- rndToAction $ (fromMaybe $ assert `failure` tgroup)+ <$> opick tgroup (const True) unless (toTile == serverTile) $ do execUpdAtomic $ UpdAlterTile lid tpos serverTile toTile case (Tile.isExplorable cotile serverTile,@@ -312,14 +313,14 @@ if null groupsToAlterTo && serverTile == freshClientTile then -- Neither searching nor altering possible; silly client. execFailure source req AlterNothing- else do+ else if EM.notMember tpos $ lfloor lvl then if unoccupied as tpos then do- when (serverTile /= freshClientTile) $ do+ when (serverTile /= freshClientTile) $ -- Search, in case some actors (of other factions?) -- don't know this tile. execUpdAtomic $ UpdSearchTile source tpos freshClientTile serverTile- maybe skip changeTo $ listToMaybe groupsToAlterTo+ maybe (return ()) changeTo $ listToMaybe groupsToAlterTo -- TODO: pick another, if the first one void -- Perform an effect, if any permitted. void $ triggerEffect source tpos feats@@ -334,53 +335,62 @@ reqWait :: MonadAtomic m => ActorId -> m () reqWait _ = return () --- * ReqMoveItem+-- * ReqMoveItems -reqMoveItem :: (MonadAtomic m, MonadServer m)- => ActorId -> ItemId -> Int -> CStore -> CStore -> m ()-reqMoveItem aid iid k fromCStore toCStore = do+reqMoveItems :: (MonadAtomic m, MonadServer m)+ => ActorId -> [(ItemId, Int, CStore, CStore)] -> m ()+reqMoveItems aid l = do b <- getsState $ getActorBody aid activeItems <- activeItemsServer aid+ -- Server accepts item movement based on calm at the start, not end+ -- or in the middle, to avoid interrupted or partially ignored commands.+ let calmE = calmEnough b activeItems+ mapM_ (reqMoveItem aid calmE) l++reqMoveItem :: (MonadAtomic m, MonadServer m)+ => ActorId -> Bool -> (ItemId, Int, CStore, CStore) -> m ()+reqMoveItem aid calmE (iid, k, fromCStore, toCStore) = do+ b <- getsState $ getActorBody aid let fromC = CActor aid fromCStore toC = CActor aid toCStore+ req = ReqMoveItems [(iid, k, fromCStore, toCStore)] bagBefore <- getsState $ getCBag toC- let moveItem = do- when (fromCStore == CGround) $ do- seed <- getsServer $ (EM.! iid) . sitemSeedD- execUpdAtomic $ UpdDiscoverSeed (blid b) (bpos b) iid seed- upds <- generalMoveItem iid k fromC toC- mapM_ execUpdAtomic upds- req = ReqMoveItem iid k fromCStore toCStore if k < 1 || fromCStore == toCStore then execFailure aid req ItemNothing else if toCStore == CEqp && eqpOverfull b k then execFailure aid req EqpOverfull- else if fromCStore /= CSha && toCStore /= CSha then moveItem+ else if (fromCStore == CSha || toCStore == CSha)+ && not calmE then execFailure aid req ItemNotCalm else do- if calmEnough b activeItems then moveItem- else execFailure aid req ItemNotCalm- -- Reset timeout for equipped periodic items.- when (toCStore `elem` [CEqp, COrgan]- && fromCStore `notElem` [CEqp, COrgan]) $ do- localTime <- getsState $ getLocalTime (blid b)- discoEffect <- getsServer sdiscoEffect- mrndTimeout <- rndToAction $ computeRndTimeout localTime discoEffect iid- let beforeIt = case iid `EM.lookup` bagBefore of- Nothing -> [] -- no such items before move- Just (_, it2) -> it2- -- The moved item set (not the whole stack) has its timeout- -- reset to a random value between timeout and twice timeout.- -- This prevents micromanagement via swapping items in and out of eqp- -- and via exact prediction of first timeout after equip.- case mrndTimeout of- Just rndT -> do- bagAfter <- getsState $ getCBag toC- let afterIt = case iid `EM.lookup` bagAfter of- Nothing -> assert `failure` (iid, bagAfter, toC)- Just (_, it2) -> it2- resetIt = beforeIt ++ replicate k rndT- when (afterIt /= resetIt) $- execUpdAtomic $ UpdTimeItem iid toC afterIt resetIt- Nothing -> return () -- no Periodic or Timeout aspect; don't touch+ when (fromCStore == CGround) $ do+ seed <- getsServer $ (EM.! iid) . sitemSeedD+ execUpdAtomic $ UpdDiscoverSeed fromC iid seed+ upds <- generalMoveItem iid k fromC toC+ mapM_ execUpdAtomic upds+ -- Reset timeout for equipped periodic items.+ when (toCStore `elem` [CEqp, COrgan]+ && fromCStore `notElem` [CEqp, COrgan]) $ do+ localTime <- getsState $ getLocalTime (blid b)+ discoEffect <- getsServer sdiscoEffect+ -- The first recharging period after pick up is random,+ -- between 1 and 2 standard timeouts of the item.+ mrndTimeout <- rndToAction $ computeRndTimeout localTime discoEffect iid+ let beforeIt = case iid `EM.lookup` bagBefore of+ Nothing -> [] -- no such items before move+ Just (_, it2) -> it2+ -- The moved item set (not the whole stack) has its timeout+ -- reset to a random value between timeout and twice timeout.+ -- This prevents micromanagement via swapping items in and out of eqp+ -- and via exact prediction of first timeout after equip.+ case mrndTimeout of+ Just rndT -> do+ bagAfter <- getsState $ getCBag toC+ let afterIt = case iid `EM.lookup` bagAfter of+ Nothing -> assert `failure` (iid, bagAfter, toC)+ Just (_, it2) -> it2+ resetIt = beforeIt ++ replicate k rndT+ when (afterIt /= resetIt) $+ execUpdAtomic $ UpdTimeItem iid toC afterIt resetIt+ Nothing -> return () -- no Periodic or Timeout aspect; don't touch computeRndTimeout :: Time -> DiscoveryEffect -> ItemId -> Rnd (Maybe Time) computeRndTimeout localTime discoEffect iid = do@@ -388,7 +398,7 @@ timeoutAspect (IK.Timeout t) = Just t timeoutAspect _ = Nothing case EM.lookup iid discoEffect of- Just ItemAspectEffect{jaspects} -> do+ Just ItemAspectEffect{jaspects} -> case mapMaybe timeoutAspect jaspects of [t] | IK.Periodic `elem` jaspects -> do rndT <- randomR (0, t)@@ -406,10 +416,10 @@ -> ItemId -- ^ the item to be projected -> CStore -- ^ whether the items comes from floor or inventory -> m ()-reqProject source tpxy eps iid cstore = assert (cstore /= CSha) $ do+reqProject source tpxy eps iid cstore = do mfail <- projectFail source tpxy eps iid cstore False let req = ReqProject tpxy eps iid cstore- maybe skip (execFailure source req) mfail+ maybe (return ()) (execFailure source req) mfail -- * ReqApply @@ -418,7 +428,7 @@ -> ItemId -- ^ the item to be applied -> CStore -- ^ the location of the item -> m ()-reqApply aid iid cstore = assert (cstore /= CSha) $ do+reqApply aid iid cstore = do let req = ReqApply iid cstore bag <- getsState $ getActorBag aid cstore case EM.lookup iid bag of@@ -428,9 +438,10 @@ b <- getsState $ getActorBody aid activeItems <- activeItemsServer aid actorSk <- actorSkillsServer aid+ localTime <- getsState $ getLocalTime (blid b) let skill = EM.findWithDefault 0 Ability.AbProject actorSk itemFull = itemToF iid kit- legal = permittedApply " " skill itemFull b activeItems+ legal = permittedApply " " localTime skill itemFull b activeItems case legal of Left reqFail -> execFailure aid req reqFail Right _ -> applyItem aid iid cstore@@ -471,7 +482,8 @@ -- so that they are available in the first game too, -- not only in subsequent, restarted, games. reqGameRestart :: (MonadAtomic m, MonadServer m)- => ActorId -> GroupName ModeKind -> Int -> [(Int, (Text, Text))] -> m ()+ => ActorId -> GroupName ModeKind -> Int -> [(Int, (Text, Text))]+ -> m () reqGameRestart aid groupName d configHeroNames = do modifyServer $ \ser -> ser {sdebugNxt = (sdebugNxt ser) { sdifficultySer = d
Game/LambdaHack/Server/ItemRev.hs view
@@ -2,17 +2,19 @@ -- | Server types and operations for items that don't involve server state -- nor our custom monads. module Game.LambdaHack.Server.ItemRev- ( ItemRev, buildItem, newItem+ ( ItemRev, buildItem, newItem, UniqueSet -- * Item discovery types , DiscoveryKindRev, serverDiscos, ItemSeedDict -- * The @FlavourMap@ type , FlavourMap, emptyFlavourMap, dungeonFlavourMap ) where +import Control.Applicative import Control.Exception.Assert.Sugar import Control.Monad import Data.Binary import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES import qualified Data.HashMap.Strict as HM import qualified Data.Ix as Ix import Data.List@@ -34,6 +36,8 @@ -- | The map of item ids to item seeds, needed for item creation. type ItemSeedDict = EM.EnumMap ItemId ItemSeed +type UniqueSet = ES.EnumSet (Kind.Id ItemKind)+ serverDiscos :: Kind.COps -> Rnd (DiscoveryKind, DiscoveryKindRev) serverDiscos Kind.COps{coitem=Kind.Ops{obounds, ofoldrWithKey}} = do let ixs = map toEnum $ take (Ix.rangeSize obounds) [0..]@@ -41,7 +45,7 @@ shuffle [] = return [] shuffle l = do x <- oneOf l- fmap (x :) $ shuffle (delete x l)+ (x :) <$> shuffle (delete x l) shuffled <- shuffle ixs let f ik _ (ikMap, ikRev, ix : rest) = (EM.insert ix ik ikMap, EM.insert ik ix ikRev, rest)@@ -52,7 +56,8 @@ return (discoS, discoRev) -- | Build an item with the given stats.-buildItem :: FlavourMap -> DiscoveryKindRev -> Kind.Id ItemKind -> ItemKind -> LevelId+buildItem :: FlavourMap -> DiscoveryKindRev -> Kind.Id ItemKind -> ItemKind+ -> LevelId -> Item buildItem (FlavourMap flavour) discoRev ikChosen kind jlid = let jkindIx = discoRev EM.! ikChosen@@ -67,41 +72,58 @@ in Item{..} -- | Generate an item based on level.-newItem :: Kind.COps -> FlavourMap -> DiscoveryKindRev- -> Freqs ItemKind -> LevelId -> AbsDepth -> AbsDepth- -> Rnd (Maybe (ItemKnown, ItemFull, ItemSeed, GroupName ItemKind))+newItem :: Kind.COps -> FlavourMap -> DiscoveryKindRev -> UniqueSet+ -> Freqs ItemKind -> Int -> LevelId -> AbsDepth -> AbsDepth+ -> Rnd (Maybe ( ItemKnown, ItemFull, ItemDisco+ , ItemSeed, GroupName ItemKind )) newItem Kind.COps{coitem=Kind.Ops{ofoldrGroup}}- flavour discoRev itemFreq jlid- ldepth@(AbsDepth ld) totalDepth@(AbsDepth depth) = do- let findInterval x1y1 [] = (x1y1, (11, 0))- findInterval x1y1 ((x, y) : rest) =- if ld * 10 <= x * depth+ flavour discoRev uniqueSet itemFreq lvlSpawned jlid+ ldepth@(AbsDepth ldAbs) totalDepth@(AbsDepth depth) = do+ -- Effective generation depth of actors (not items) increases with spawns.+ let scaledDepth = ldAbs * 10 `div` depth+ numSpawnedCoeff = lvlSpawned `div` 2+ ldSpawned = max ldAbs -- the first fast spawns are of the nominal level+ $ min depth+ $ ldAbs + numSpawnedCoeff - scaledDepth+ findInterval _ x1y1 [] = (x1y1, (11, 0))+ findInterval ld x1y1 ((x, y) : rest) =+ if fromIntegral ld * 10 <= x * fromIntegral depth then (x1y1, (x, y))- else findInterval (x, y) rest- linearInterpolation dataset =- -- We assume @dataset@ is sorted and between 1 and 10 inclusive.- let ((x1, y1), (x2, y2)) = findInterval (0, 0) dataset- in y1 + (y2 - y1) * (ld * 10 - x1 * depth)- `divUp` ((x2 - x1) * depth)+ else findInterval ld (x, y) rest+ linearInterpolation ld dataset =+ -- We assume @dataset@ is sorted and between 0 and 10.+ let ((x1, y1), (x2, y2)) = findInterval ld (0, 0) dataset+ in ceiling+ $ fromIntegral y1+ + fromIntegral (y2 - y1)+ * (fromIntegral ld * 10 - x1 * fromIntegral depth)+ / ((x2 - x1) * fromIntegral depth)+ f _ _ _ ik _ acc | ik `ES.member` uniqueSet = acc f itemGroup q p ik kind acc =- let rarity = linearInterpolation (IK.irarity kind)+ -- Don't consider lvlSpawned for uniques.+ let ld = if IK.Unique `elem` IK.iaspects kind then ldAbs else ldSpawned+ rarity = linearInterpolation ld (IK.irarity kind) in (q * p * rarity, ((ik, kind), itemGroup)) : acc g (itemGroup, q) = ofoldrGroup itemGroup (f itemGroup q) [] freqDepth = concatMap g itemFreq- freq = toFreq ("newItem ('" <> tshow ld <> ")") freqDepth+ freq = toFreq ("newItem ('" <> tshow ldSpawned <> ")") freqDepth if nullFreq freq then return Nothing else do ((itemKindId, itemKind), itemGroup) <- frequency freq+ -- Number of new items/actors unaffected by number of spawned actors. itemN <- castDice ldepth totalDepth (IK.icount itemKind) seed <- fmap toEnum random let itemBase = buildItem flavour discoRev itemKindId itemKind jlid itemK = max 1 itemN itemTimer = []- itemDisco = Just ItemDisco {itemKindId, itemKind, itemAE = Just iae}+ itemDiscoData = ItemDisco {itemKindId, itemKind, itemAE = Just iae}+ itemDisco = Just itemDiscoData+ -- Bonuses on items/actors unaffected by number of spawned actors. iae = seedToAspectsEffects seed itemKind ldepth totalDepth itemFull = ItemFull {..} return $ Just ( (jkindIx itemBase, iae) , itemFull+ , itemDiscoData , seed , itemGroup )
Game/LambdaHack/Server/ItemServer.hs view
@@ -7,23 +7,30 @@ import Control.Monad import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES+import Data.Function import qualified Data.HashMap.Strict as HM-import Data.Key (mapWithKeyM_)+import Data.List import Data.Maybe+import Data.Ord+import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Atomic import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Item+import Game.LambdaHack.Common.ItemStrongest import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead+import Game.LambdaHack.Common.Msg import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Content.ItemKind (ItemKind)+import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.TileKind (TileKind) import qualified Game.LambdaHack.Content.TileKind as TK import Game.LambdaHack.Server.ItemRev@@ -31,25 +38,34 @@ import Game.LambdaHack.Server.State registerItem :: (MonadAtomic m, MonadServer m)- => Item -> ItemKnown -> ItemSeed -> Int -> Container -> Bool+ => ItemFull -> ItemKnown -> ItemSeed -> Int -> Container -> Bool -> m ItemId-registerItem item itemKnown@(_, iae) seed k container verbose = do+registerItem itemFull itemKnown@(_, iae) seed k container verbose = do itemRev <- getsServer sitemRev let cmd = if verbose then UpdCreateItem else UpdSpotItem case HM.lookup itemKnown itemRev of Just iid -> do -- TODO: try to avoid this case for createItems, -- to make items more interesting- execUpdAtomic $ cmd iid item (k, []) container+ execUpdAtomic $ cmd iid (itemBase itemFull) (k, []) container return iid Nothing -> do+ let fovSight = fromMaybe 0+ $ strengthFromEqpSlot IK.EqpSlotAddSight itemFull+ fovSmell = fromMaybe 0+ $ strengthFromEqpSlot IK.EqpSlotAddSmell itemFull+ fovLight = fromMaybe 0+ $ strengthFromEqpSlot IK.EqpSlotAddLight itemFull+ ssl = FovCache3{..} icounter <- getsServer sicounter modifyServer $ \ser ->- ser { sicounter = succ icounter- , sitemRev = HM.insert itemKnown icounter (sitemRev ser)+ ser { sdiscoEffect = EM.insert icounter iae (sdiscoEffect ser) , sitemSeedD = EM.insert icounter seed (sitemSeedD ser)- , sdiscoEffect = EM.insert icounter iae (sdiscoEffect ser)}- execUpdAtomic $ cmd icounter item (k, []) container+ , sitemRev = HM.insert itemKnown icounter (sitemRev ser)+ , sItemFovCache = if ssl == emptyFovCache3 then sItemFovCache ser+ else EM.insert icounter ssl (sItemFovCache ser)+ , sicounter = succ icounter }+ execUpdAtomic $ cmd icounter (itemBase itemFull) (k, []) container return $! icounter createLevelItem :: (MonadAtomic m, MonadServer m)@@ -65,51 +81,68 @@ Kind.COps{cotile} <- getsState scops let embeds = Tile.embedItems cotile tk causes = Tile.causeEffects cotile tk- -- TODO: unack this, e.g., by turning each Cause into Embed+ -- TODO: unhack this, e.g., by turning each Cause into Embed itemFreq = zip embeds (repeat 1)- ++ if not (null causes) && null embeds- then [("hero", 1)] -- hack: the bag, not item, is relevant- else []+ ++ -- Hack: the bag, not item, is relevant.+ [("hero", 1) | not (null causes) && null embeds] container = CEmbed lid pos void $ rollAndRegisterItem lid itemFreq container False Nothing rollItem :: (MonadAtomic m, MonadServer m)- => LevelId -> Freqs ItemKind- -> m (Maybe (ItemKnown, ItemFull, ItemSeed, GroupName ItemKind))-rollItem lid itemFreq = do+ => Int -> LevelId -> Freqs ItemKind+ -> m (Maybe ( ItemKnown, ItemFull, ItemDisco+ , ItemSeed, GroupName ItemKind ))+rollItem lvlSpawned lid itemFreq = do cops <- getsState scops flavour <- getsServer sflavour discoRev <- getsServer sdiscoKindRev+ uniqueSet <- getsServer suniqueSet totalDepth <- getsState stotalDepth Level{ldepth} <- getLevel lid- rndToAction $ newItem cops flavour discoRev itemFreq lid ldepth totalDepth+ m5 <- rndToAction $ newItem cops flavour discoRev uniqueSet+ itemFreq lvlSpawned lid ldepth totalDepth+ case m5 of+ Just (_, _, ItemDisco{ itemKindId+ , itemAE=Just ItemAspectEffect{jaspects}}, _, _) ->+ when (IK.Unique `elem` jaspects) $+ modifyServer $ \ser ->+ ser {suniqueSet = ES.insert itemKindId (suniqueSet ser)}+ _ -> return ()+ return m5 rollAndRegisterItem :: (MonadAtomic m, MonadServer m) => LevelId -> Freqs ItemKind -> Container -> Bool -> Maybe Int -> m (Maybe (ItemId, (ItemFull, GroupName ItemKind))) rollAndRegisterItem lid itemFreq container verbose mk = do- m4 <- rollItem lid itemFreq- case m4 of+ -- Power depth of new items unaffected by number of spawned actors.+ m5 <- rollItem 0 lid itemFreq+ case m5 of Nothing -> return Nothing- Just (itemKnown, itemFullRaw, seed, itemGroup) -> do- let itemFull = itemFullRaw {itemK = fromMaybe (itemK itemFullRaw) mk}- iid <- registerItem (itemBase itemFull) itemKnown seed+ Just (itemKnown, itemFullRaw, itemDisco, seed, itemGroup) -> do+ let item = itemBase itemFullRaw+ trunkName = makePhrase [MU.WownW (MU.Text $ jname item) "trunk"]+ itemTrunk = if null $ IK.ikit $ itemKind itemDisco+ then item+ else item {jname = trunkName}+ itemFull = itemFullRaw { itemK = fromMaybe (itemK itemFullRaw) mk+ , itemBase = itemTrunk }+ iid <- registerItem itemFull itemKnown seed (itemK itemFull) container verbose return $ Just (iid, (itemFull, itemGroup)) placeItemsInDungeon :: forall m. (MonadAtomic m, MonadServer m) => m () placeItemsInDungeon = do Kind.COps{cotile} <- getsState scops- let initialItems lid (Level{lfloor, ltile, litemNum, lxsize, lysize}) = do+ let initialItems (lid, Level{lfloor, ltile, litemNum, lxsize, lysize}) = do let factionDist = max lxsize lysize - 5 placeItems :: [Point] -> Int -> m () placeItems _ 0 = return () placeItems lfloorKeys n = do let dist p = minimum $ maxBound : map (chessDist p) lfloorKeys- pos <- rndToAction $ findPosTry 100 ltile+ pos <- rndToAction $ findPosTry 500 ltile (\_ t -> Tile.isWalkable cotile t- && (not $ Tile.hasFeature cotile TK.NoItem t))+ && not (Tile.hasFeature cotile TK.NoItem t)) [ \p t -> Tile.hasFeature cotile TK.OftenItem t && dist p > factionDist `div` 5 , \p t -> Tile.hasFeature cotile TK.OftenItem t@@ -132,14 +165,22 @@ placeItems (pos : lfloorKeys) (n - 1) placeItems (EM.keys lfloor) litemNum dungeon <- getsState sdungeon- mapWithKeyM_ initialItems dungeon+ -- Make sure items on easy levels are generated first, to avoid all+ -- artifacts on deep levels.+ let absLid = abs . fromEnum+ fromEasyToHard = sortBy (comparing absLid `on` fst) $ EM.assocs dungeon+ mapM_ initialItems fromEasyToHard embedItemsInDungeon :: (MonadAtomic m, MonadServer m) => m () embedItemsInDungeon = do- let embedItems lid (Level{ltile}) =- PointArray.mapWithKeyM_A (embedItem lid) ltile+ let embedItems (lid, Level{ltile}) =+ PointArray.mapWithKeyMA (embedItem lid) ltile dungeon <- getsState sdungeon- mapWithKeyM_ embedItems dungeon+ -- Make sure items on easy levels are generated first, to avoid all+ -- artifacts on deep levels.+ let absLid = abs . fromEnum+ fromEasyToHard = sortBy (comparing absLid `on` fst) $ EM.assocs dungeon+ mapM_ embedItems fromEasyToHard fullAssocsServer :: MonadServer m => ActorId -> [CStore] -> m [(ItemId, ItemFull)]@@ -160,7 +201,8 @@ discoKind <- getsServer sdiscoKind discoEffect <- getsServer sdiscoEffect s <- getState- let itemToF iid = itemToFull cops discoKind discoEffect iid (getItemBody iid s)+ let itemToF iid =+ itemToFull cops discoKind discoEffect iid (getItemBody iid s) return itemToF -- | Mapping over actor's items from a give store.
Game/LambdaHack/Server/LoopServer.hs view
@@ -43,14 +43,17 @@ import Game.LambdaHack.Server.StartServer import Game.LambdaHack.Server.State --- | Start a game session. Loop, communicating with clients.+-- | Start a game session, including the clients, and then loop,+-- communicating with the clients. loopSer :: (MonadAtomic m, MonadServerReadRequest m)- => DebugModeSer+ => Kind.COps -- ^ game content+ -> DebugModeSer -- ^ server debug parameters -> (FactionId -> ChanServer ResponseUI RequestUI -> IO ())+ -- ^ the code to run for UI clients -> (FactionId -> ChanServer ResponseAI RequestAI -> IO ())- -> Kind.COps+ -- ^ the code to run for AI clients -> m ()-loopSer sdebug executorUI executorAI !cops = do+loopSer cops sdebug executorUI executorAI = do -- Recover states and launch clients. let updConn = updateConn executorUI executorAI restored <- tryRestore cops sdebug@@ -73,13 +76,13 @@ execUpdAtomic $ UpdResumeServer $ updateCOps setCurrentCops sRaw -- We dump RNG seeds here, in case the game wasn't run -- with --dumpInitRngs previously and we need to seeds.- when (sdumpInitRngs sdebug) $ dumpRngs+ when (sdumpInitRngs sdebug) dumpRngs _ -> do -- Starting the first new game for this savefile. -- Set up commandline debug mode let mrandom = case restored of Just (_, ser) -> Just $ srandom ser Nothing -> Nothing- s <- gameReset cops sdebug mrandom+ s <- gameReset cops sdebug Nothing mrandom sdebugNxt <- initDebug cops sdebug let debugBarRngs = sdebugNxt {sdungeonRng = Nothing, smainRng = Nothing} modifyServer $ \ser -> ser { sdebugNxt = debugBarRngs@@ -97,7 +100,7 @@ -- Run the leader and other actors moves. Eventually advance the time -- and repeat. let loop = do- let factionArena fact = do+ let factionArena fact = case gleader fact of -- Even spawners and horrors need an active arena -- for their leader, or they start clogging stairs.@@ -108,7 +111,7 @@ factionD <- getsState sfactionD marenas <- mapM factionArena $ EM.elems factionD let arenas = ES.toList $ ES.fromList $ catMaybes marenas- assert (not $ null arenas) skip -- game over not caught earlier+ let !_A = assert (not $ null arenas) () -- game over not caught earlier mapM_ handleActors arenas quit <- getsServer squit if quit then do@@ -128,8 +131,7 @@ let stdRuleset = Kind.stdRuleset corule writeSaveClips = rwriteSaveClips stdRuleset leadLevelClips = rleadLevelClips stdRuleset- ageProcessed lid processed =- EM.insertWith absoluteTimeAdd lid timeClip processed+ ageProcessed lid = EM.insertWith absoluteTimeAdd lid timeClip ageServer lid ser = ser {sprocessed = ageProcessed lid $ sprocessed ser} mapM_ (modifyServer . ageServer) arenas execUpdAtomic $ UpdAgeGame (Delta timeClip) arenas@@ -145,7 +147,7 @@ when (clipN `mod` leadLevelClips == 0) leadLevelSwitch if clipMod == 1 then do -- Periodic activation only once per turn, for speed, but on all arenas.- mapM_ activatePeriodicLevel arenas+ mapM_ applyPeriodicLevel arenas -- Add monsters each turn, not each clip. -- Do this on only one of the arenas to prevent micromanagement, -- e.g., spreading leaders across levels to bump monster generation.@@ -165,13 +167,13 @@ else return True -- | Trigger periodic items for all actors on the given level.-activatePeriodicLevel :: (MonadAtomic m, MonadServer m) => LevelId -> m ()-activatePeriodicLevel lid = do+applyPeriodicLevel :: (MonadAtomic m, MonadServer m) => LevelId -> m ()+applyPeriodicLevel lid = do discoEffect <- getsServer sdiscoEffect- let activatePeriodicItem c aid iid =+ let applyPeriodicItem c aid iid = case EM.lookup iid discoEffect of Just ItemAspectEffect{jeffects, jaspects} ->- if IK.Periodic `elem` jaspects then do+ when (IK.Periodic `elem` jaspects) $ do -- Check if the item is still in the bag (previous items act!). bag <- getsState $ getCBag c case iid `EM.lookup` bag of@@ -180,18 +182,16 @@ -- In periodic activation, consider *only* recharging effects. effectAndDestroy aid aid iid c True (allRecharging jeffects) jaspects kit- else- return () _ -> assert `failure` (lid, aid, c, iid)- activatePeriodicCStore aid cstore = do+ applyPeriodicCStore aid cstore = do let c = CActor aid cstore bag <- getsState $ getCBag c- mapM_ (activatePeriodicItem c aid) $ EM.keys bag- activatePeriodicActor aid = do- activatePeriodicCStore aid COrgan- activatePeriodicCStore aid CEqp+ mapM_ (applyPeriodicItem c aid) $ EM.keys bag+ applyPeriodicActor aid = do+ applyPeriodicCStore aid COrgan+ applyPeriodicCStore aid CEqp allActors <- getsState $ actorRegularAssocs (const True) lid- mapM_ (\(aid, _) -> activatePeriodicActor aid) allActors+ mapM_ (\(aid, _) -> applyPeriodicActor aid) allActors -- | Perform moves for individual actors, as long as there are actors -- with the next move time less or equal to the end of current cut-off.@@ -206,11 +206,11 @@ factionD <- getsState sfactionD s <- getState let -- Actors of the same faction move together.- -- TODO: insert wrt the order, instead of sorting- isLeader (aid, b) = Just aid /= fmap fst (gleader (factionD EM.! bfid b))- notDead (_, b) = if bproj b then bhp b >= 0 else bhp b > 0+ notDead (_, b) = not $ actorDying b+ notProj (_, b) = not $ bproj b+ notLeader (aid, b) = Just aid /= fmap fst (gleader (factionD EM.! bfid b)) order = Ord.comparing $- notDead &&& bfid . snd &&& isLeader &&& bsymbol . snd+ notDead &&& notProj &&& bfid . snd &&& notLeader &&& bsymbol . snd (atime, as) = EM.findMin lprio ams = map (\a -> (a, getActorBody a s)) as mnext | EM.null lprio = Nothing -- no actor alive, wait until it spawns@@ -221,25 +221,21 @@ case mnext of _ | quit -> return () Nothing -> return ()- Just (aid, b) | maybe False (null .fst) (btrajectory b) && bproj b -> do- -- A projectile drops to the ground due to obstacles or range.- assert (bproj b) skip+ Just (aid, b) | bproj b && maybe True (null . fst) (btrajectory b) -> do startActor aid+ -- A projectile drops to the ground due to obstacles or range.+ -- The carried item is not destroyed, but drops to the ground. dieSer aid b False handleActors lid- Just (aid, b) | bhp b < 0 && bproj b -> do- -- A projectile hits an actor. The carried item is destroyed.+ Just (aid, b) | bhp b <= 0 -> do startActor aid- dieSer aid b True- handleActors lid- Just (aid, b) | bhp b <= 0 && not (bproj b) -> do- -- An actor dies. Items drop to the ground+ -- If @b@ is a projectile and it hits an actor,+ -- the carried item is destroyed and that's all.+ -- Otherwise, an actor dies, items drop to the ground -- and possibly a new leader is elected.- startActor aid- dieSer aid b False+ dieSer aid b (bproj b) handleActors lid Just (aid, body) -> do- startActor aid let side = bfid body fact = factionD EM.! side mleader = gleader fact@@ -265,8 +261,10 @@ when (hasWait /= bwait bPre) $ execUpdAtomic $ UpdWaitActor aidNew hasWait if isJust $ btrajectory body then do- timed <- setTrajectory aid- when timed $ advanceTime aid+ setTrajectory aid+ b2 <- getsState $ getActorBody aid+ unless (bproj b2 && actorDying b2) $+ advanceTime aid else if queryUI then do cmdS <- sendQueryUI side aid -- TODO: check that the command is legal first, report and reject,@@ -275,7 +273,7 @@ let hasWait (ReqUITimed ReqWait{}) = True hasWait (ReqUILeader _ _ cmd) = hasWait cmd hasWait _ = False- maybe skip (setBWait (hasWait cmdS)) aidNew+ maybe (return ()) (setBWait (hasWait cmdS)) aidNew -- Advance time once, after the leader switched perhaps many times. -- Sometimes this may result in a double move of the new leader, -- followed by a double pause. Or a fractional variant of that.@@ -283,8 +281,12 @@ -- for the old leader, but otherwise his time is undisturbed. -- He is able to move normally in the same turn, immediately -- after the new leader completes his move.- maybe skip advanceTime aidNew+ -- Warning: when the action is performed on the server,+ -- the time of the actor is different than when client prepared that+ -- action, so any client checks involving time should discount this.+ maybe (return ()) advanceTime aidNew action+ maybe (return ()) managePerTurn aidNew else do -- Clear messages in the UI client (if any), if the actor -- is a leader (which happens when a UI client is fully@@ -301,6 +303,9 @@ -- AI always takes time and so doesn't loop. advanceTime aidNew action+ managePerTurn aidNew+ b3 <- getsState $ getActorBody aid+ unless (waitedLastTurn b3) $ startActor aid handleActors lid gameExit :: (MonadAtomic m, MonadServerReadRequest m) => m ()@@ -315,17 +320,19 @@ fovMode <- getsServer $ sfovMode . sdebugSer ser <- getServer pers <- getsState $ \s -> dungeonPerception (fromMaybe Digital fovMode) s ser- assert (persAccumulated == pers `blame` "wrong accumulated perception"- `twith` (persAccumulated, pers)) skip+ let !_A = assert (persAccumulated == pers+ `blame` "wrong accumulated perception"+ `twith` (persAccumulated, pers)) ()+ return () restartGame :: (MonadAtomic m, MonadServerReadRequest m)- => m () -> m () -> m ()-restartGame updConn loop = do+ => m () -> m () -> Maybe (GroupName ModeKind) -> m ()+restartGame updConn loop mgameMode = do tellGameClipPS cops <- getsState scops sdebugNxt <- getsServer sdebugNxt srandom <- getsServer srandom- s <- gameReset cops sdebugNxt $ Just srandom+ s <- gameReset cops sdebugNxt mgameMode (Just srandom) let debugBarRngs = sdebugNxt {sdungeonRng = Nothing, smainRng = Nothing} modifyServer $ \ser -> ser { sdebugNxt = debugBarRngs , sdebugSer = debugBarRngs }@@ -362,38 +369,30 @@ -- Not advancing time forces dead projectiles to be destroyed ASAP. -- Otherwise, with some timings, it can stay on the game map dead, -- blocking path of human-controlled actors and alarming the hapless human.-setTrajectory :: (MonadAtomic m, MonadServer m) => ActorId -> m Bool+setTrajectory :: (MonadAtomic m, MonadServer m) => ActorId -> m () setTrajectory aid = do cops <- getsState scops b <- getsState $ getActorBody aid lvl <- getLevel $ blid b- let clearTrajectory speed = do+ case btrajectory b of+ Just (d : lv, speed) ->+ if not $ accessibleDir cops lvl (bpos b) d+ then do -- Lose HP due to bumping into an obstacle. execUpdAtomic $ UpdRefillHP aid minusM execUpdAtomic $ UpdTrajectory aid (btrajectory b) (Just ([], speed))- return $ not $ bproj b -- projectiles must vanish soon- case btrajectory b of- Just ((d : lv), speed) ->- if not $ accessibleDir cops lvl (bpos b) d- then clearTrajectory speed else do when (bproj b && null lv) $ do let toColor = Color.BrBlack when (bcolor b /= toColor) $ execUpdAtomic $ UpdColorActor aid (bcolor b) toColor- reqMove aid d -- hit clears trajectory of non-projectiles+ reqMove aid d -- hit clears trajectory of non-projectiles in reqMelee b2 <- getsState $ getActorBody aid- if actorDying b2 then return $ not $ bproj b -- don't clear trajectory- else do- unless (maybe False (null . fst) (btrajectory b2)) $- execUpdAtomic $ UpdTrajectory aid- (btrajectory b2)- (Just (lv, speed))- return True+ unless (btrajectory b2 == Just (lv, speed)) $ -- cleared in reqMelee+ execUpdAtomic $ UpdTrajectory aid (btrajectory b2) (Just (lv, speed)) Just ([], _) -> do -- non-projectile actor stops flying- assert (not $ bproj b) skip+ let !_A = assert (not $ bproj b) () execUpdAtomic $ UpdTrajectory aid (btrajectory b) Nothing- return False _ -> assert `failure` "Nothing trajectory" `twith` (aid, b)
Game/LambdaHack/Server/MonadServer.hs view
@@ -27,11 +27,11 @@ import qualified Data.Text as T import qualified Data.Text.IO as T import System.Directory+import System.Exit (exitFailure) import System.FilePath import System.IO import qualified System.Random as R import System.Time-import System.Exit (exitFailure) import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState@@ -102,8 +102,8 @@ hFlush stderr -- TODO: refactor wrt Game.LambdaHack.Common.Save--- | Read the high scores table. Return the empty table if no file.-restoreScore :: MonadServer m => Kind.COps -> m HighScore.ScoreTable+-- | Read the high scores dictionary. Return the empty table if no file.+restoreScore :: MonadServer m => Kind.COps -> m HighScore.ScoreDict restoreScore Kind.COps{corule} = do let stdRuleset = Kind.stdRuleset corule scoresFile = rscoresFile stdRuleset@@ -120,7 +120,7 @@ handler e = do let msg = "High score restore failed. The error message is:" <+> (T.unwords . T.lines) (tshow e)- delayPrint $ msg+ delayPrint msg return Nothing either handler return res maybe (return HighScore.empty) return mscore@@ -129,7 +129,7 @@ registerScore :: MonadServer m => Status -> Maybe Actor -> FactionId -> m () registerScore status mbody fid = do cops@Kind.COps{corule} <- getsState scops- assert (maybe True ((fid ==) . bfid) mbody) skip+ let !_A = assert (maybe True ((fid ==) . bfid) mbody) () fact <- getsState $ (EM.! fid) . sfactionD total <- case mbody of Just body -> getsState $ snd . calculateTotal body@@ -142,12 +142,12 @@ scoresFile = rscoresFile stdRuleset dataDir <- liftIO appDataDir -- Re-read the table in case it's changed by a concurrent game.- table <- restoreScore cops+ scoreDict <- restoreScore cops+ gameModeId <- getsState sgameModeId time <- getsState stime date <- liftIO getClockTime DebugModeSer{sdifficultySer} <- getsServer sdebugSer factionD <- getsState sfactionD- loots <- factionLoots fid bench <- getsServer $ sbenchmark . sdebugCli . sdebugSer let path = dataDir </> scoresFile outputScore (worthMentioning, (ntable, pos)) =@@ -156,9 +156,9 @@ debugPossiblyPrint $ T.intercalate "\n" $ HighScore.showScore (pos, HighScore.getRecord pos ntable) else- if worthMentioning then- liftIO $ encodeEOF path (ntable :: HighScore.ScoreTable)- else return ()+ let nScoreDict = EM.insert gameModeId ntable scoreDict+ in when (worthMentioning) $+ liftIO $ encodeEOF path (nScoreDict :: HighScore.ScoreDict) diff | not $ fhasUI $ gplayer fact = difficultyDefault | otherwise = sdifficultySer theirVic (fi, fa) | isAtWar fact fi@@ -168,10 +168,12 @@ ourVic (fi, fa) | isAllied fact fi || fi == fid = Just $ gvictims fa | otherwise = Nothing ourVictims = EM.unionsWith (+) $ mapMaybe ourVic $ EM.assocs factionD+ table = HighScore.getTable gameModeId scoreDict registeredScore = HighScore.register table total time status date diff (fname $ gplayer fact)- ourVictims theirVictims loots+ ourVictims theirVictims+ (fhiCondPoly $ gplayer fact) outputScore registeredScore resetSessionStart :: MonadServer m => m ()@@ -263,4 +265,4 @@ -> m R.StdGen getSetGen mrng = case mrng of Just rnd -> return rnd- Nothing -> liftIO $ R.newStdGen+ Nothing -> liftIO R.newStdGen
Game/LambdaHack/Server/PeriodicServer.hs view
@@ -1,7 +1,8 @@ -- | Server operations performed periodically in the game loop -- and related operations. module Game.LambdaHack.Server.PeriodicServer- ( spawnMonster, addAnyActor, dominateFidSfx, advanceTime, leadLevelSwitch+ ( spawnMonster, addAnyActor, dominateFidSfx+ , advanceTime, managePerTurn, leadLevelSwitch ) where import Control.Exception.Assert.Sugar@@ -18,6 +19,7 @@ import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Frequency import Game.LambdaHack.Common.Item+import Game.LambdaHack.Common.ItemStrongest import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc@@ -33,7 +35,6 @@ import Game.LambdaHack.Content.ModeKind import qualified Game.LambdaHack.Content.TileKind as TK import Game.LambdaHack.Server.CommonServer-import Game.LambdaHack.Server.ItemRev import Game.LambdaHack.Server.ItemServer import Game.LambdaHack.Server.MonadServer import Game.LambdaHack.Server.State@@ -43,17 +44,16 @@ -- We assume heroes are never spawned. spawnMonster :: (MonadAtomic m, MonadServer m) => LevelId -> m () spawnMonster lid = do- -- We check the number of current dungeon dwellers (whether spawned or not)- -- to decide if more should be spawned.- f <- getsState $ \s fid -> not $ fcanEscape $ gplayer $ sfactionD s EM.! fid- spawns <- getsState $ actorRegularList f lid totalDepth <- getsState stotalDepth -- TODO: eliminate the defeated and victorious faction from lactorFreq; -- then fcanEscape and fneverEmpty make sense for spawning factions Level{ldepth, lactorCoeff, lactorFreq} <- getLevel lid+ lvlSpawned <- getsServer $ fromMaybe 0 . EM.lookup lid . snumSpawned rc <- rndToAction- $ monsterGenChance ldepth totalDepth (length spawns) lactorCoeff+ $ monsterGenChance ldepth totalDepth lvlSpawned lactorCoeff when rc $ do+ modifyServer $ \ser ->+ ser {snumSpawned = EM.insert lid (lvlSpawned + 1) $ snumSpawned ser} time <- getsState $ getLocalTime lid maid <- addAnyActor lactorFreq lid time Nothing case maid of@@ -71,17 +71,14 @@ -- We bootstrap the actor by first creating the trunk of the actor's body -- contains the constant properties. cops <- getsState scops- flavour <- getsServer sflavour- discoRev <- getsServer sdiscoKindRev- totalDepth <- getsState stotalDepth- lvl@Level{ldepth} <- getLevel lid+ lvl <- getLevel lid factionD <- getsState sfactionD- m4 <- rndToAction- $ newItem cops flavour discoRev actorFreq lid ldepth totalDepth+ lvlSpawned <- getsServer $ fromMaybe 0 . EM.lookup lid . snumSpawned+ m4 <- rollItem lvlSpawned lid actorFreq case m4 of Nothing -> return Nothing- Just (itemKnown, trunkFull, seed, _) -> do- let ik = maybe (assert `failure` trunkFull) itemKind $ itemDisco trunkFull+ Just (itemKnown, trunkFull, itemDisco, seed, _) -> do+ let ik = itemKind itemDisco freqNames = map fst $ IK.ifreq ik f fact = fgroup (gplayer fact) factNames = map f $ EM.elems factionD@@ -94,54 +91,70 @@ pers <- getsServer sper let allPers = ES.unions $ map (totalVisible . (EM.! lid)) $ EM.elems $ EM.delete fid pers -- expensive :(+ mobile = any (`elem` freqNames) ["civilian", "horror"] pos <- case mpos of Just pos -> return pos Nothing -> do- rollPos <- getsState $ rollSpawnPos cops allPers lid lvl fid+ fact <- getsState $ (EM.! fid) . sfactionD+ rollPos <- getsState $ rollSpawnPos cops allPers mobile lid lvl fact rndToAction rollPos- let container = (CTrunk fid lid pos)- trunkId <-- registerItem (itemBase trunkFull) itemKnown seed- (itemK trunkFull) container False+ let container = CTrunk fid lid pos+ trunkId <- registerItem trunkFull itemKnown seed+ (itemK trunkFull) container False addActorIid trunkId trunkFull False fid pos lid id "it" time rollSpawnPos :: Kind.COps -> ES.EnumSet Point- -> LevelId -> Level -> FactionId -> State+ -> Bool -> LevelId -> Level -> Faction -> State -> Rnd Point rollSpawnPos Kind.COps{cotile} visible- lid Level{ltile, lxsize, lysize} fid s = do- let factionDist = max lxsize lysize - 5- inhabitants = actorList (/= fid) lid s -- projectiles can have cameras+ mobile lid Level{ltile, lxsize, lysize} fact s = do+ let inhabitants = actorRegularList (isAtWar fact) lid s as = actorList (const True) lid s isLit = Tile.isLit cotile- distantAtLeast d p _ =- all (\b -> chessDist (bpos b) p > d) inhabitants+ distantSo df p _ =+ all (\b -> df $ chessDist (bpos b) p) inhabitants+ middlePos = Point (lxsize `div` 2) (lysize `div` 2)+ distantMiddle d p _ = chessDist p middlePos < d+ condList | mobile =+ [ \_ t -> not (isLit t) -- no such tiles on some maps+ , distantSo (<= 10) -- try to harass enemies+ , distantSo (>= 5) -- but leave room for yourself for ranged combat+ , distantSo (<= 20) -- but applying pressure is more important+ ]+ | otherwise =+ [ distantMiddle 5+ , distantMiddle 10+ , distantMiddle 20+ , distantMiddle 50+ , distantMiddle 100+ ] -- Not considering TK.OftenActor, because monsters emerge from hidden ducts, -- which are easier to hide in crampy corridors that lit halls.- findPosTry 100 ltile+ findPosTry (if mobile then 500 else 100) ltile ( \p t -> Tile.isWalkable cotile t && not (Tile.hasFeature cotile TK.NoActor t) && unoccupied as p)- [ \_ t -> not (isLit t) -- no such tiles on some maps- , distantAtLeast factionDist- , distantAtLeast $ factionDist `div` 2- , distantAtLeast $ factionDist `div` 4- , distantAtLeast $ factionDist `div` 6- , \p _ -> not $ p `ES.member` visible- , distantAtLeast 3 -- otherwise a fast actor can walk and hit in one turn- ]+ (condList+ ++ [ \p _ -> not (p `ES.member` visible) -- surprise and believability+ , distantSo (>= 3) -- otherwise fast actor approach and hit in one turn+ ]) dominateFidSfx :: (MonadAtomic m, MonadServer m) => FactionId -> ActorId -> m Bool dominateFidSfx fid target = do- -- Actors that never ever move can't be dominated- actorSk <- maxActorSkillsServer target- let canMove = EM.findWithDefault 0 Ability.AbMove actorSk > 0- if canMove+ tb <- getsState $ getActorBody target+ -- Actors that don't move freely can't be dominated, for otherwise,+ -- when they are the last survivors, they could get stuck+ -- and the game wouldn't end.+ activeItems <- activeItemsServer target+ let actorMaxSk = sumSkills activeItems+ canMove = EM.findWithDefault 0 Ability.AbMove actorMaxSk > 0+ && EM.findWithDefault 0 Ability.AbTrigger actorMaxSk > 0+ && EM.findWithDefault 0 Ability.AbAlter actorMaxSk > 0+ if canMove && not (bproj tb) then do- tb <- getsState $ getActorBody target let execSfx = execSfxAtomic- $ SfxEffect (boldfid tb) target IK.Dominate+ $ SfxEffect (bfidImpressed tb) target IK.Dominate execSfx dominateFid fid target execSfx@@ -154,24 +167,26 @@ dominateFid fid target = do Kind.COps{cotile} <- getsState scops tb0 <- getsState $ getActorBody target- -- Only record the initial domination as a kill.- discoKind <- getsServer sdiscoKind- trunk <- getsState $ getItemBody $ btrunk tb0- let ikind = discoKind EM.! jkindIx trunk- when (boldfid tb0 == bfid tb0) $ execUpdAtomic $ UpdRecordKill target ikind 1 electLeader (bfid tb0) (blid tb0) target fact <- getsState $ (EM.! bfid tb0) . sfactionD -- Prevent the faction's stash from being lost in case they are not spawners. when (isNothing $ gleader fact) $ moveStores target CSha CInv tb <- getsState $ getActorBody target- deduceKilled tb+ deduceKilled target tb+ -- TODO: some messages after game over below? Compare with dieSer. ais <- getsState $ getCarriedAssocs tb calmMax <- sumOrganEqpServer IK.EqpSlotAddMaxCalm target execUpdAtomic $ UpdLoseActor target tb ais let bNew = tb { bfid = fid- , boldfid = bfid tb+ , bfidImpressed = bfid tb , bcalm = max 0 $ xM calmMax `div` 2 } execUpdAtomic $ UpdSpotActor target bNew ais+ let discoverSeed (iid, cstore) = do+ seed <- getsServer $ (EM.! iid) . sitemSeedD+ let c = CActor target cstore+ execUpdAtomic $ UpdDiscoverSeed c iid seed+ aic = getCarriedIidCStore tb+ mapM_ discoverSeed aic mleaderOld <- getsState $ gleader . (EM.! fid) . sfactionD -- Keep the leader if he is on stairs. We don't want to clog stairs. keepLeader <- case mleaderOld of@@ -184,28 +199,34 @@ -- Focus on the dominated actor, by making him a leader. execUpdAtomic $ UpdLeadFaction fid mleaderOld (Just (target, Nothing)) --- | Advance the move time for the given actor, check if he's dominated--- and update his calm. We don't update calm once per game turn--- (even though it would make fast actors less overpowered),--- beucase the effects of close enemies would sometimes manifest only after--- a couple of player turns (or perhaps never at all, if the player and enemy--- move away before that moment). A side effect is that under peaceful--- circumstances, non-max calm cases a consistent regeneration UI indicator--- to be displayed each turn (not every few turns).+-- | Advance the move time for the given actor advanceTime :: (MonadAtomic m, MonadServer m) => ActorId -> m () advanceTime aid = do b <- getsState $ getActorBody aid activeItems <- activeItemsServer aid- fact <- getsState $ (EM.! bfid b) . sfactionD let t = ticksPerMeter $ bspeed b activeItems execUpdAtomic $ UpdAgeActor aid t++-- | Check if the given actor is dominated and update his calm.+-- We don't update calm once per game turn (even though+-- it would make fast actors less overpowered),+-- beucase the effects of close enemies would sometimes manifest only after+-- a couple of player turns (or perhaps never at all, if the player and enemy+-- move away before that moment). A side effect is that under peaceful+-- circumstances, non-max calm causes a consistent Calm regeneration+-- UI indicator to be displayed each turn (not every few turns).+managePerTurn :: (MonadAtomic m, MonadServer m) => ActorId -> m ()+managePerTurn aid = do+ b <- getsState $ getActorBody aid unless (bproj b) $ do+ activeItems <- activeItemsServer aid+ fact <- getsState $ (EM.! bfid b) . sfactionD dominated <- if bcalm b == 0- && boldfid b /= bfid b+ && bfidImpressed b /= bfid b && fleaderMode (gplayer fact) /= LeaderNull- -- animals never Calm-dominated- then dominateFidSfx (boldfid b) aid+ -- animals/robots never Calm-dominated+ then dominateFidSfx (bfidImpressed b) aid else return False unless dominated $ do newCalmDelta <- getsState $ regenCalmDelta b activeItems@@ -230,7 +251,7 @@ LeaderAI _ -> True LeaderUI _ -> False flipFaction fact | not $ canSwitch fact = return ()- flipFaction fact = do+ flipFaction fact = case gleader fact of Nothing -> return () Just (leader, _) -> do@@ -258,7 +279,7 @@ let freqList = [ (k, (lid, a)) | (lid, itemN, (a, _) : rest) <- ours , not leaderStuck || lid /= blid body- , let len = 1 + (min 10 $ length rest)+ , let len = 1 + min 10 (length rest) k = 1000000 `div` (3 * itemN + len) ] unless (null freqList) $ do (lid, a) <- rndToAction $ frequency
Game/LambdaHack/Server/ProtocolServer.hs view
@@ -18,6 +18,7 @@ -- * Assorted , killAllClients, childrenServer, updateConn #ifdef EXPOSE_INTERNAL+ -- * Internal operations , ConnServerFaction #endif ) where@@ -219,5 +220,5 @@ -- even if UI usage changes, but it works OK thanks to UI faction -- clients distinguished by positive FactionId numbers. forkAI fid connAI -- AI clients always needed, e.g., for auto-explore- maybe skip (forkUI fid) connUI+ maybe (return ()) (forkUI fid) connUI liftIO $ mapWithKeyM_ forkClient toSpawn
Game/LambdaHack/Server/StartServer.hs view
@@ -3,6 +3,7 @@ ( gameReset, reinitGame, initPer, recruitActors, applyDebug, initDebug ) where +import Control.Applicative import Control.Exception.Assert.Sugar import Control.Monad import qualified Control.Monad.State as St@@ -12,6 +13,7 @@ import Data.List import qualified Data.Map.Strict as M import Data.Maybe+import Data.Ord import Data.Text (Text) import qualified Data.Text as T import Data.Tuple (swap)@@ -61,6 +63,7 @@ Kind.COps{coitem=Kind.Ops{okind}} <- getsState scops pers <- getsServer sper knowMap <- getsServer $ sknowMap . sdebugSer+ sdebugCli <- getsServer $ sdebugCli . sdebugSer -- This state is quite small, fit for transmition to the client. -- The biggest part is content, which needs to be updated -- at this point to keep clients in sync with server improvements.@@ -70,11 +73,8 @@ discoS <- getsServer sdiscoKind let sdiscoKind = let f ik = IK.Identified `elem` IK.ifeature (okind ik) in EM.filter f discoS- sdebugCli <- getsServer $ sdebugCli . sdebugSer- modeName <- getsServer $ sgameMode . sdebugSer- let gameMode = fromMaybe "starting" modeName broadcastUpdAtomic- $ \fid -> UpdRestart fid sdiscoKind (pers EM.! fid) defLocal sdebugCli gameMode+ $ \fid -> UpdRestart fid sdiscoKind (pers EM.! fid) defLocal sdebugCli populateDungeon mapFromFuns :: (Bounded a, Enum a, Ord b) => [a -> b] -> M.Map b a@@ -100,12 +100,10 @@ cmap = mapFromFuns [colorToTeamName, colorToPlainName, colorToFancyName] nameoc = lowercase $ head $ T.words fname- fisAI = case fleaderMode of- LeaderNull -> True- LeaderAI _ -> True- LeaderUI _ -> False- prefix | fisAI = "Autonomous"- | otherwise = "Controlled"+ prefix = case fleaderMode of+ LeaderNull -> "Loose"+ LeaderAI _ -> "Autonomous"+ LeaderUI _ -> "Controlled" (gcolor, gname) = case M.lookup nameoc cmap of Nothing -> (Color.BrWhite, prefix <+> fname) Just c -> (c, prefix <+> fname <+> "Team")@@ -142,9 +140,10 @@ return $! warFs gameReset :: MonadServer m- => Kind.COps -> DebugModeSer -> Maybe R.StdGen -> m State-gameReset cops@Kind.COps{comode=Kind.Ops{opick, okind, ouniqGroup}}- sdebug mrandom = do+ => Kind.COps -> DebugModeSer -> Maybe (GroupName ModeKind)+ -> Maybe R.StdGen -> m State+gameReset cops@Kind.COps{comode=Kind.Ops{opick, okind}}+ sdebug mGameMode mrandom = do dungeonSeed <- getSetGen $ sdungeonRng sdebug `mplus` mrandom srandom <- getSetGen $ smainRng sdebug `mplus` mrandom scoreTable <- if sfrontendNull $ sdebugCli sdebug then@@ -154,13 +153,13 @@ sstart <- getsServer sstart -- copy over from previous game sallTime <- getsServer sallTime -- copy over from previous game sheroNames <- getsServer sheroNames -- copy over from previous game- let gameMode = fromMaybe "starting" $ sgameMode sdebug+ let gameMode = fromMaybe "starting" $ mGameMode `mplus` sgameMode sdebug rnd :: Rnd (FactionDict, FlavourMap, DiscoveryKind, DiscoveryKindRev,- DungeonGen.FreshDungeon)+ DungeonGen.FreshDungeon, Kind.Id ModeKind) rnd = do- modeKind <- fmap (fromMaybe $ ouniqGroup "campaign") -- the fallback- $ opick gameMode (const True)- let mode = okind modeKind+ modeKindId <- fromMaybe (assert `failure` gameMode)+ <$> opick gameMode (const True)+ let mode = okind modeKindId automatePS ps = ps {rosterList = map (automatePlayer True) $ rosterList ps} players = if sautomateAll sdebug@@ -170,18 +169,18 @@ (sdiscoKind, sdiscoKindRev) <- serverDiscos cops freshDng <- DungeonGen.dungeonGen cops $ mcaves mode faction <- createFactions (DungeonGen.freshTotalDepth freshDng) players- return (faction, sflavour, sdiscoKind, sdiscoKindRev, freshDng)- let (faction, sflavour, sdiscoKind, sdiscoKindRev, DungeonGen.FreshDungeon{..}) =+ return (faction, sflavour, sdiscoKind, sdiscoKindRev, freshDng, modeKindId)+ let (faction, sflavour, sdiscoKind, sdiscoKindRev, DungeonGen.FreshDungeon{..}, modeKindId) = St.evalState rnd dungeonSeed defState = defStateGlobal freshDungeon freshTotalDepth- faction cops scoreTable+ faction cops scoreTable modeKindId defSer = emptyStateServer { sstart, sallTime, sheroNames, srandom , srngs = RNGs (Just dungeonSeed) (Just srandom) } putServer defSer when (sbenchmark $ sdebugCli sdebug) resetGameStart modifyServer $ \ser -> ser {sdiscoKind, sdiscoKindRev, sflavour}- when (sdumpInitRngs sdebug) $ dumpRngs+ when (sdumpInitRngs sdebug) dumpRngs return $! defState -- Spawn initial actors. Clients should notice this, to set their leaders.@@ -197,7 +196,11 @@ case (EM.minViewWithKey dungeon, EM.maxViewWithKey dungeon) of (Just ((s, _), _), Just ((e, _), _)) -> (s, e) _ -> assert `failure` "empty dungeon" `twith` dungeon- needInitialCrew = filter ((> 0 ) . finitialActors . gplayer . snd)+ -- Players that escape go first to be started over stairs, if possible.+ valuePlayer pl = (not $ fcanEscape pl, fname pl)+ -- Sorting, to keep games from similar game modes mutually reproducible.+ needInitialCrew = sortBy (comparing $ valuePlayer . gplayer . snd)+ $ filter ((> 0 ) . finitialActors . gplayer . snd) $ EM.assocs factionD getEntryLevel (_, fact) = max minD $ min maxD $ toEnum $ fentryLevel $ gplayer fact@@ -205,21 +208,23 @@ initialActors lid = do lvl <- getLevel lid let arenaFactions = filter ((== lid) . getEntryLevel) needInitialCrew- representsAlliance (fid2, fact2) =- not $ any (\(fid3, _) -> fid3 < fid2- && isAllied fact2 fid3) arenaFactions+ indexff (fid, _) = findIndex ((== fid) . fst) arenaFactions+ representsAlliance ff2@(_, fact2) =+ not $ any (\ff3@(fid3, _) ->+ indexff ff3 < indexff ff2+ && isAllied fact2 fid3) arenaFactions arenaAlliances = filter representsAlliance arenaFactions- placeAlliance ((fid3, _), ppos) =+ placeAlliance ((fid3, _), ppos, timeOffset) = mapM_ (\(fid4, fact4) ->- if isAllied fact4 fid3 || fid4 == fid3- then placeActors lid ((fid4, fact4), ppos)- else return ()) arenaFactions+ when (isAllied fact4 fid3 || fid4 == fid3) $+ placeActors lid ((fid4, fact4), ppos, timeOffset))+ arenaFactions entryPoss <- rndToAction- $ findEntryPoss cops lvl (length arenaAlliances)- mapM_ placeAlliance $ zip arenaAlliances entryPoss- placeActors lid ((fid3, fact3), ppos) = do+ $ findEntryPoss cops lid lvl (length arenaAlliances)+ mapM_ placeAlliance $ zip3 arenaAlliances entryPoss [0..]+ placeActors lid ((fid3, fact3), ppos, timeOffset) = do time <- getsState $ getLocalTime lid- let nmult = 1 + fromEnum fid3 `mod` 4 -- always positive+ let nmult = 1 + timeOffset `mod` 4 ntime = timeShift time (timeDeltaScale (Delta timeClip) nmult) validTile t = not $ Tile.hasFeature cotile TK.NoActor t psFree <- getsState $ nearbyFreePoints validTile ppos lid@@ -229,7 +234,7 @@ if not $ fhasNumbers $ gplayer fact3 then recruitActors [p] lid ntime fid3 else do- let hNames = fromMaybe [] $ EM.lookup fid3 sheroNames+ let hNames = EM.findWithDefault [] fid3 sheroNames maid <- addHero fid3 p lid hNames (Just n) ntime case maid of Nothing -> return False@@ -283,7 +288,7 @@ addHero bfid ppos lid heroNames mNumber time = do Faction{gcolor, gplayer} <- getsState $ (EM.! bfid) . sfactionD let groupName = fgroup gplayer- mhs <- mapM (\n -> getsState $ \s -> tryFindHeroK s bfid n) [0..9]+ mhs <- mapM (\n -> getsState $ tryFindHeroK bfid n) [0..9] let freeHeroK = elemIndex Nothing mhs n = fromMaybe (fromMaybe 100 freeHeroK) mNumber bsymbol = if n < 1 || n > 9 then '@' else Char.intToDigit n@@ -299,16 +304,18 @@ addActor groupName bfid ppos lid tweakBody pronoun time -- | Find starting postions for all factions. Try to make them distant--- from each other. If only one faction, also move it away from any stairs.-findEntryPoss :: Kind.COps -> Level -> Int -> Rnd [Point]-findEntryPoss Kind.COps{cotile} Level{ltile, lxsize, lysize, lstair} k = do+-- from each other. Place as many of the initial factions, as possible,+-- over stairs and escapes.+findEntryPoss :: Kind.COps -> LevelId -> Level -> Int -> Rnd [Point]+findEntryPoss Kind.COps{cotile}+ lid Level{ltile, lxsize, lysize, lstair, lescape} k = do let factionDist = max lxsize lysize - 5 dist poss cmin l _ = all (\pos -> chessDist l pos > cmin) poss tryFind _ 0 = return [] tryFind ps n = do np <- findPosTry 1000 ltile -- try really hard, for skirmish fairness (\_ t -> Tile.isWalkable cotile t- && (not $ Tile.hasFeature cotile TK.NoActor t))+ && not (Tile.hasFeature cotile TK.NoActor t)) [ dist ps $ factionDist `div` 2 , dist ps $ factionDist `div` 3 , const (Tile.hasFeature cotile TK.OftenActor)@@ -320,15 +327,23 @@ ] nps <- tryFind (np : ps) (n - 1) return $! np : nps- stairPoss = fst lstair ++ snd lstair+ -- Prefer deeper stairs to avoid spawners ambushing explorers.+ (deeperStairs, shallowerStairs) =+ (if fromEnum lid > 0 then id else swap) lstair+ stairPoss = (deeperStairs \\ shallowerStairs)+ ++ lescape+ ++ shallowerStairs middlePos = Point (lxsize `div` 2) (lysize `div` 2)- assert (k > 0 && factionDist > 0) skip- case k of- 1 -> tryFind stairPoss k+ let !_A = assert (k > 0 && factionDist > 0) ()+ onStairs = take k stairPoss+ nk = k - length onStairs+ found <- case nk of+ 0 -> return []+ 1 -> tryFind onStairs nk 2 -> -- Make sure the first faction's pos is not chosen in the middle.- tryFind [middlePos] k- _ | k > 2 -> tryFind [] k- _ -> assert `failure` k+ tryFind (if null onStairs then [middlePos] else onStairs) nk+ _ -> tryFind onStairs nk+ return $! onStairs ++ found initDebug :: MonadStateRead m => Kind.COps -> DebugModeSer -> m DebugModeSer initDebug Kind.COps{corule} sdebugSer = do
Game/LambdaHack/Server/State.hs view
@@ -2,13 +2,13 @@ module Game.LambdaHack.Server.State ( StateServer(..), emptyStateServer , DebugModeSer(..), defDebugModeSer- , RNGs(..)+ , RNGs(..), FovCache3(..), emptyFovCache3 ) where import Data.Binary import qualified Data.EnumMap.Strict as EM+import qualified Data.EnumSet as ES import qualified Data.HashMap.Strict as HM-import Data.List import Data.Text (Text) import qualified System.Random as R import System.Time@@ -30,12 +30,17 @@ data StateServer = StateServer { sdiscoKind :: !DiscoveryKind -- ^ full item kind discoveries data , sdiscoKindRev :: !DiscoveryKindRev -- ^ reverse map, used for item creation+ , suniqueSet :: !UniqueSet -- ^ already generated unique items , sdiscoEffect :: !DiscoveryEffect -- ^ full item effect&Co data , sitemSeedD :: !ItemSeedDict -- ^ map from item ids to item seeds , sitemRev :: !ItemRev -- ^ reverse id map, used for item creation+ , sItemFovCache :: !(EM.EnumMap ItemId FovCache3)+ -- ^ (sight, smell, light) aspect bonus+ -- of the item; zeroes if not in the map , sflavour :: !FlavourMap -- ^ association of flavour to items , sacounter :: !ActorId -- ^ stores next actor index , sicounter :: !ItemId -- ^ stores next item index+ , snumSpawned :: !(EM.EnumMap LevelId Int) , sprocessed :: !(EM.EnumMap LevelId Time) -- ^ actors are processed up to this time , sundo :: ![CmdAtomic] -- ^ atomic commands performed to date@@ -54,6 +59,16 @@ } deriving (Show) +data FovCache3 = FovCache3+ { fovSight :: !Int+ , fovSmell :: !Int+ , fovLight :: !Int+ }+ deriving (Show, Eq)++emptyFovCache3 :: FovCache3+emptyFovCache3 = FovCache3 0 0 0+ -- | Debug commands. See 'Server.debugArgs' for the descriptions. data DebugModeSer = DebugModeSer { sknowMap :: !Bool@@ -63,6 +78,7 @@ , sallClear :: !Bool , sgameMode :: !(Maybe (GroupName ModeKind)) , sautomateAll :: !Bool+ , skeepAutomated :: !Bool , sstopAfter :: !(Maybe Int) , sdungeonRng :: !(Maybe R.StdGen) , smainRng :: !(Maybe R.StdGen)@@ -72,7 +88,7 @@ , sdumpInitRngs :: !Bool , ssavePrefixSer :: !(Maybe String) , sdbgMsgSer :: !Bool- , sdebugCli :: !DebugModeCli+ , sdebugCli :: !DebugModeCli -- ^ client debug parameters } deriving Show @@ -87,7 +103,7 @@ dungeonRandomGenerator , maybe "" (\gen -> "--setMainRng \"" ++ show gen ++ "\"") startingRandomGenerator ]- in intercalate " " args+ in unwords args -- | Initial, empty game server state. emptyStateServer :: StateServer@@ -95,12 +111,15 @@ StateServer { sdiscoKind = EM.empty , sdiscoKindRev = EM.empty+ , suniqueSet = ES.empty , sdiscoEffect = EM.empty , sitemSeedD = EM.empty , sitemRev = HM.empty+ , sItemFovCache = EM.empty , sflavour = emptyFlavourMap , sacounter = toEnum 0 , sicounter = toEnum 0+ , snumSpawned = EM.empty , sprocessed = EM.empty , sundo = [] , sper = EM.empty@@ -125,6 +144,7 @@ , sallClear = False , sgameMode = Nothing , sautomateAll = False+ , skeepAutomated = False , sstopAfter = Nothing , sdungeonRng = Nothing , smainRng = Nothing@@ -141,12 +161,15 @@ put StateServer{..} = do put sdiscoKind put sdiscoKindRev+ put suniqueSet put sdiscoEffect put sitemSeedD put sitemRev+ put sItemFovCache -- out of laziness, but it's small put sflavour put sacounter put sicounter+ put snumSpawned put sprocessed put sundo put (show srandom)@@ -156,12 +179,15 @@ get = do sdiscoKind <- get sdiscoKindRev <- get+ suniqueSet <- get sdiscoEffect <- get sitemSeedD <- get sitemRev <- get+ sItemFovCache <- get sflavour <- get sacounter <- get sicounter <- get+ snumSpawned <- get sprocessed <- get sundo <- get g <- get@@ -178,6 +204,17 @@ sdebugNxt = defDebugModeSer -- TODO: here difficulty level, etc. from the last session is wiped out return $! StateServer{..} +instance Binary FovCache3 where+ put FovCache3{..} = do+ put fovSight+ put fovSmell+ put fovLight+ get = do+ fovSight <- get+ fovSmell <- get+ fovLight <- get+ return $! FovCache3{..}+ instance Binary DebugModeSer where put DebugModeSer{..} = do put sknowMap@@ -187,6 +224,7 @@ put sallClear put sgameMode put sautomateAll+ put skeepAutomated put sdifficultySer put sfovMode put ssavePrefixSer@@ -200,6 +238,7 @@ sallClear <- get sgameMode <- get sautomateAll <- get+ skeepAutomated <- get sdifficultySer <- get sfovMode <- get ssavePrefixSer <- get
GameDefinition/Client/UI/Content/KeyKind.hs view
@@ -27,7 +27,7 @@ , ("CTRL-k", ([CmdMenu], GameRestart "skirmish")) , ("CTRL-m", ([CmdMenu], GameRestart "ambush")) , ("CTRL-b", ([CmdMenu], GameRestart "battle"))- , ("CTRL-a", ([CmdMenu], GameRestart "campaign"))+ , ("CTRL-n", ([CmdMenu], GameRestart "campaign")) , ("CTRL-d", ([CmdMenu], GameDifficultyCycle)) -- Movement and terrain alteration@@ -53,27 +53,32 @@ [ TriggerFeature { verb = "descend" , object = "10 levels" , feature = TK.Cause (IK.Ascend (-10)) } ]))- , ("semicolon", ([CmdMove], StepToTarget))- , ("colon", ([CmdMove], Macro "go to target for 100 steps"- ["semicolon", "V"]))- , ("CTRL-colon", ([CmdMove], Macro "go to target for 10 steps"- ["semicolon", "CTRL-V"]))- , ("x", ([CmdMove], Macro "explore the closest unknown spot"- [ "BackSpace"- , "CTRL-question", "semicolon", "V" ]))- , ("X", ([CmdMove], Macro "autoexplore 100 times"- [ "BackSpace"- , "'", "CTRL-question", "semicolon", "'"- , "V" ]))- , ("CTRL-X", ([CmdMove], Macro "autoexplore 10 times"- [ "BackSpace"- , "'", "CTRL-question", "semicolon", "'"- , "CTRL-V" ]))+ , ("semicolon",+ ( [CmdMove]+ , Macro "go to crosshair for 100 steps"+ ["CTRL-semicolon", "CTRL-period", "V"] ))+ , ("colon",+ ( [CmdMove]+ , Macro "run selected to crosshair for 100 steps"+ ["CTRL-colon", "CTRL-period", "V"] ))+ , ("x",+ ( [CmdMove]+ , Macro "explore the closest unknown spot"+ [ "CTRL-question" -- no semicolon+ , "CTRL-period", "V" ] ))+ , ("X",+ ( [CmdMove]+ , Macro "autoexplore 100 times"+ ["'", "CTRL-question", "CTRL-period", "'", "V"] ))+ , ("CTRL-X",+ ( [CmdMove]+ , Macro "autoexplore 25 times"+ ["'", "CTRL-question", "CTRL-period", "'", "CTRL-V"] )) , ("R", ([CmdMove], Macro "rest (wait 100 times)" ["KP_Begin", "V"]))- , ("CTRL-R", ([CmdMove], Macro "rest (wait 10 times)"+ , ("CTRL-R", ([CmdMove], Macro "rest (wait 25 times)" ["KP_Begin", "CTRL-V"]))- , ("c", ([CmdMove], AlterDir+ , ("c", ([CmdMove, CmdMinimal], AlterDir [ AlterFeature { verb = "close" , object = "door" , feature = TK.CloseTo "vertical closed door Lit" }@@ -87,32 +92,29 @@ , object = "door" , feature = TK.CloseTo "horizontal closed door Dark" } ]))- , ("CTRL-KP_Begin", ([CmdMove], Macro "" ["KP_Begin"]))- , ("period", ([CmdMove], Macro "" ["KP_Begin"]))- , ("KP_5", ([CmdMove], Macro "" ["KP_Begin"]))- , ("CTRL-KP_5", ([CmdMove], Macro "" ["KP_Begin"]))- , ("i", ([CmdMove], Macro "" ["KP_Begin"])) -- Item use- , ("E", ([CmdItem], DescribeItem CEqp))- , ("P", ([CmdItem], DescribeItem CInv))- , ("S", ([CmdItem], DescribeItem CSha))- , ("G", ([CmdItem], DescribeItem CGround))- , ("A", ([CmdItem], AllOwned))+ , ("E", ([CmdItem, CmdMinimal], DescribeItem $ MStore CEqp))+ , ("P", ([CmdItem], DescribeItem $ MStore CInv))+ , ("S", ([CmdItem], DescribeItem $ MStore CSha))+ , ("A", ([CmdItem], DescribeItem MOwned))+ , ("G", ([CmdItem], DescribeItem $ MStore CGround))+ , ("@", ([CmdItem], DescribeItem $ MStore COrgan))+ , ("exclam", ([CmdItem], DescribeItem MStats)) , ("g", ([CmdItem, CmdMinimal],- MoveItem [CGround] CEqp (Just "get") "an item" True))+ MoveItem [CGround] CEqp (Just "get") "items" True)) , ("d", ([CmdItem], MoveItem [CEqp, CInv, CSha] CGround- Nothing "an item" False))- , ("e", ([CmdItem], MoveItem [CInv, CSha] CEqp- Nothing "an item" False))- , ("p", ([CmdItem], MoveItem [CEqp, CSha] CInv- Nothing "an item into inventory backpack"+ Nothing "items" False))+ , ("e", ([CmdItem], MoveItem [CGround, CInv, CSha] CEqp+ Nothing "items" False))+ , ("p", ([CmdItem], MoveItem [CGround, CEqp, CSha] CInv+ Nothing "items into inventory" False))- , ("s", ([CmdItem], MoveItem [CInv, CEqp] CSha- Nothing "and share an item" False))+ , ("s", ([CmdItem], MoveItem [CGround, CInv, CEqp] CSha+ Nothing "and share items" False)) , ("a", ([CmdItem, CmdMinimal], Apply- [ ApplyItem { verb = "activate"- , object = "applicable item"+ [ ApplyItem { verb = "apply"+ , object = "consumable" , symbol = ' ' } , ApplyItem { verb = "quaff" , object = "potion"@@ -129,26 +131,27 @@ , symbol = '?' }])) , ("f", ([CmdItem, CmdMinimal], Project [ApplyItem { verb = "fling"- , object = "projectable item"+ , object = "projectile" , symbol = ' ' }])) , ("t", ([CmdItem], Project [ApplyItem { verb = "throw" , object = "missile" , symbol = '|' }]))- , ("z", ([CmdItem], Project [ApplyItem { verb = "zap"- , object = "wand"- , symbol = '/' }]))+-- , ("z", ([CmdItem], Project [ApplyItem { verb = "zap"+-- , object = "wand"+-- , symbol = '/' }])) -- Targeting- , ("KP_Multiply", ([CmdTgt, CmdMinimal], TgtEnemy))+ , ("KP_Multiply", ([CmdTgt], TgtEnemy)) , ("backslash", ([CmdTgt], Macro "" ["KP_Multiply"]))- , ("slash", ([CmdTgt], TgtFloor))- , ("plus", ([CmdTgt], EpsIncr True))+ , ("KP_Divide", ([CmdTgt], TgtFloor))+ , ("bar", ([CmdTgt], Macro "" ["KP_Divide"]))+ , ("plus", ([CmdTgt, CmdMinimal], EpsIncr True)) , ("minus", ([CmdTgt], EpsIncr False))+ , ("CTRL-question", ([CmdTgt], CursorUnknown))+ , ("CTRL-I", ([CmdTgt], CursorItem))+ , ("CTRL-braceleft", ([CmdTgt], CursorStair True))+ , ("CTRL-braceright", ([CmdTgt], CursorStair False)) , ("BackSpace", ([CmdTgt], TgtClear))- , ("CTRL-question", ([CmdTgt], TgtUnknown))- , ("CTRL-I", ([CmdTgt], TgtItem))- , ("CTRL-braceleft", ([CmdTgt], TgtStair True))- , ("CTRL-braceright", ([CmdTgt], TgtStair False)) -- Automation , ("equal", ([CmdAuto], SelectActor))@@ -156,27 +159,50 @@ , ("v", ([CmdAuto], Repeat 1)) , ("V", ([CmdAuto], Repeat 100)) , ("CTRL-v", ([CmdAuto], Repeat 1000))- , ("CTRL-V", ([CmdAuto], Repeat 10))+ , ("CTRL-V", ([CmdAuto], Repeat 25)) , ("apostrophe", ([CmdAuto], Record)) , ("CTRL-T", ([CmdAuto], Tactic)) , ("CTRL-A", ([CmdAuto], Automate)) -- Assorted , ("question", ([CmdMeta], Help))- , ("D", ([CmdMeta], History))- , ("T", ([CmdMeta], MarkSuspect))+ , ("D", ([CmdMeta, CmdMinimal], History))+ , ("T", ([CmdMeta, CmdMinimal], MarkSuspect)) , ("Z", ([CmdMeta], MarkVision)) , ("C", ([CmdMeta], MarkSmell)) , ("Tab", ([CmdMeta], MemberCycle))- , ("ISO_Left_Tab", ([CmdMeta], MemberBack))+ , ("ISO_Left_Tab", ([CmdMeta, CmdMinimal], MemberBack)) , ("space", ([CmdMeta], Clear)) , ("Escape", ([CmdMeta, CmdMinimal], Cancel))- , ("Return", ([CmdMeta], Accept))+ , ("Return", ([CmdMeta, CmdTgt], Accept)) + -- Mouse+ , ("LeftButtonPress",+ ([CmdMouse], macroLeftButtonPress))+ , ("SHIFT-LeftButtonPress",+ ([CmdMouse], macroShiftLeftButtonPress))+ , ("MiddleButtonPress", ([CmdMouse], CursorPointerEnemy))+ , ("SHIFT-MiddleButtonPress", ([CmdMouse], CursorPointerFloor))+ , ("CTRL-MiddleButtonPress",+ ([CmdInternal], Macro "" ["SHIFT-MiddleButtonPress"]))+ , ("RightButtonPress", ([CmdMouse], TgtPointerEnemy))+ -- Debug and others not to display in help screens , ("CTRL-s", ([CmdDebug], GameSave))+ , ("CTRL-i", ([CmdDebug], GameRestart "battle survival")) , ("CTRL-f", ([CmdDebug], GameRestart "safari"))+ , ("CTRL-r", ([CmdDebug], GameRestart "safari survival")) , ("CTRL-e", ([CmdDebug], GameRestart "defense"))- , ("CTRL-O", ([CmdDebug], DescribeItem COrgan))- ]+ , ("CTRL-g", ([CmdDebug], GameRestart "boardgame"))+ , ("CTRL-semicolon", ([CmdInternal], MoveOnceToCursor))+ , ("CTRL-colon", ([CmdInternal], RunOnceToCursor))+ , ("CTRL-period", ([CmdInternal], ContinueToCursor))+ , ("CTRL-comma", ([CmdInternal], RunOnceAhead))+ , ("CTRL-LeftButtonPress",+ ([CmdInternal], Macro "" ["SHIFT-LeftButtonPress"]))+ , ("CTRL-MiddleButtonPress",+ ([CmdInternal], Macro "" ["SHIFT-MiddleButtonPress"]))+ , ("ALT-space", ([CmdInternal], StopIfTgtMode))+ , ("ALT-minus", ([CmdInternal], SelectWithPointer))+ ] }
GameDefinition/Content/CaveKind.hs view
@@ -16,9 +16,9 @@ , validateSingle = validateSingleCaveKind , validateAll = validateAllCaveKind , content =- [rogue, arena, empty, noise, battle, skirmish, ambush, safari1, safari2, safari3]+ [rogue, arena, empty, noise, shallow1rogue, battle, skirmish, ambush, safari1, safari2, safari3, boardgame] }-rogue, arena, empty, noise, battle, skirmish, ambush, safari1, safari2, safari3 :: CaveKind+rogue, arena, empty, noise, shallow1rogue, battle, skirmish, ambush, safari1, safari2, safari3, boardgame :: CaveKind rogue = CaveKind { csymbol = 'R'@@ -37,9 +37,9 @@ , cdoorChance = 1%2 , copenChance = 1%10 , chidden = 8- , cactorCoeff = 20- , cactorFreq = [("monster", 50), ("animal", 50)]- , citemNum = 15 * d 2+ , cactorCoeff = 150 -- the maze requires time to explore+ , cactorFreq = [("monster", 40), ("animal", 60)]+ , citemNum = 10 * d 2 , citemFreq = [("useful", 50), ("treasure", 50)] , cplaceFreq = [("rogue", 100)] , cpassable = False@@ -53,17 +53,18 @@ } arena = rogue { csymbol = 'A'- , cname = "Underground city"- , cfreq = [("campaign random", 30), ("caveArena", 1)]+ , cname = "Underground library"+ , cfreq = [("campaign random", 50), ("caveArena", 1)] , cgrid = DiceXY (2 * d 2) (2 * d 2) , cminPlaceSize = DiceXY (2 * d 2 + 3) 4- , cdarkChance = d 80 + dl 60- , cnightChance = 0+ , cdarkChance = d 60 + dl 60+ , cnightChance = d 60 + dl 60 -- trails provide enough light for fun stealth , cmaxVoid = 1%4 , chidden = 1000- , cactorCoeff = 40+ , cactorCoeff = 100 , cactorFreq = [("monster", 70), ("animal", 30)]- , citemNum = 11 * d 2 -- few rooms+ , citemNum = 9 * d 2 -- few rooms+ , citemFreq = [("useful", 20), ("treasure", 30), ("any scroll", 50)] , cpassable = True , cdefTile = "arenaSet" , cdarkCorTile = "trailLit" -- let trails give off light@@ -72,7 +73,7 @@ empty = rogue { csymbol = 'E' , cname = "Tall cavern"- , cfreq = [("campaign random", 20), ("caveEmpty", 1)]+ , cfreq = [("caveEmpty", 1)] , cgrid = DiceXY (d 2 + 1) 1 , cminPlaceSize = DiceXY 10 10 , cmaxPlaceSize = DiceXY 24 12@@ -82,9 +83,15 @@ , cmaxVoid = 1%2 , cminStairDist = 50 , chidden = 1000- , cactorCoeff = 50- , cactorFreq = [("monster", 10), ("animal", 90)]- , citemNum = 9 * d 2 -- few rooms+ , cactorCoeff = 20+ , cactorFreq = [("monster", 2), ("animal", 8), ("immobile vents", 90)]+ -- The geysers on lvl 3 act like HP resets. They are needed to avoid+ -- cascading failure, if the particular starting conditions were+ -- very hard. The items are not reset, even if the are bad, which provides+ -- enough of a continuity. Gyesers on lvl 3 are not OP and can't be+ -- abused, because they spawn less and less often, they don't heal over+ -- max HP and they expire naturally.+ , citemNum = 7 * d 2 -- few rooms , cpassable = True , cdefTile = "emptySet" , cdarkCorTile = "floorArenaDark"@@ -92,8 +99,8 @@ } noise = rogue { csymbol = 'N'- , cname = "Glittering cave"- , cfreq = [("campaign random", 10), ("caveNoise", 1)]+ , cname = "Leaky, burrowed sediment"+ , cfreq = [("campaign random", 20), ("caveNoise", 1)] , cgrid = DiceXY (2 + d 2) 3 , cminPlaceSize = DiceXY 12 5 , cmaxPlaceSize = DiceXY 24 12@@ -101,15 +108,24 @@ , cauxConnects = 0 , cmaxVoid = 0 , chidden = 1000- , cactorCoeff = 30+ , cactorCoeff = 200 -- the maze requires time to explore , cactorFreq = [("monster", 80), ("animal", 20)]- , citemNum = 16 * d 2 -- an incentive to explore the labyrinth+ , citemNum = 12 * d 2 -- an incentive to explore the labyrinth , cpassable = True , cplaceFreq = [("noise", 100)] , cdefTile = "noiseSet" , cdarkCorTile = "floorArenaDark" , clitCorTile = "floorArenaLit" }+shallow1rogue = rogue+ { csymbol = 'D'+ , cname = "Entrance to the dungeon"+ , cfreq = [("shallow random 1", 100)]+ , cdarkChance = 0+ , cactorFreq = filter ((/= "monster") . fst) $ cactorFreq rogue+ , citemNum = 15 * d 2 -- lure them in with loot+ , citemFreq = filter ((/= "treasure") . fst) $ citemFreq rogue+ } battle = rogue -- few lights and many solids, to help the less numerous heroes { csymbol = 'B' , cname = "Old battle ground"@@ -168,7 +184,7 @@ , chidden = 1000 , cactorFreq = [] , citemNum = 22 * d 2- , citemFreq = [("useful", 100), ("light source", 200)]+ , citemFreq = [("useful", 100)] , cplaceFreq = [("ambush", 100)] , cpassable = True , cdefTile = "ambushSet"@@ -178,3 +194,34 @@ safari1 = ambush {cfreq = [("caveSafari1", 1)]} safari2 = battle {cfreq = [("caveSafari2", 1)]} safari3 = skirmish {cfreq = [("caveSafari3", 1)]}+boardgame = CaveKind+ { csymbol = 'B'+ , cname = "A boardgame"+ , cfreq = [("caveBoardgame", 1)]+ , cxsize = fst normalLevelBound + 1+ , cysize = snd normalLevelBound + 1+ , cgrid = DiceXY 1 1+ , cminPlaceSize = DiceXY 10 10+ , cmaxPlaceSize = DiceXY 10 10+ , cdarkChance = 0+ , cnightChance = 0+ , cauxConnects = 0+ , cmaxVoid = 0+ , cminStairDist = 0+ , cdoorChance = 0+ , copenChance = 0+ , chidden = 0+ , cactorCoeff = 0+ , cactorFreq = []+ , citemNum = 0+ , citemFreq = []+ , cplaceFreq = [("boardgame", 1)]+ , cpassable = False+ , cdefTile = "fillerWall"+ , cdarkCorTile = "floorCorridorDark"+ , clitCorTile = "floorCorridorLit"+ , cfillerTile = "fillerWall"+ , couterFenceTile = "basic outer fence"+ , clegendDarkTile = "legendDark"+ , clegendLitTile = "legendLit"+ }
GameDefinition/Content/ItemKind.hs view
@@ -1,12 +1,14 @@ -- | Item and treasure definitions. module Content.ItemKind ( cdefs ) where +import qualified Data.EnumMap.Strict as EM import Data.List import Content.ItemKindActor import Content.ItemKindBlast import Content.ItemKindOrgan import Content.ItemKindTemporary+import Game.LambdaHack.Common.Ability import Game.LambdaHack.Common.Color import Game.LambdaHack.Common.ContentDef import Game.LambdaHack.Common.Dice@@ -26,9 +28,9 @@ items :: [ItemKind] items =- [dart, dart200, bolas, harpoon, net, jumpingPole, whetstone, woodenTorch, oilLamp, brassLantern, gorget, necklace1, necklace2, necklace3, necklace4, necklace5, necklace6, necklace7, monocle, ring1, ring2, ring3, ring4, ring5, potion1, potion2, potion3, potion4, potion5, potion6, potion7, potion8, flask1, flask2, flask3, flask4, flask5, flask6, flask7, flask8, flask9, flask10, flask11, flask12, flask13, flask14, scroll1, scroll2, scroll3, scroll4, scroll5, scroll6, scroll7, scroll8, scroll9, scroll10, armorLeather, armorMail, gloveFencing, gloveGauntlet, gloveJousting, buckler, shield, dagger, daggerDropBestWeapon, hammer, hammerParalyze, hammerSpark, sword, swordImpress, swordNullify, halberd, halberdPushActor, wand1, wand2, gem1, gem2, gem3, currency]+ [dart, dart200, paralizingProj, harpoon, net, jumpingPole, sharpeningTool, seeingItem, light1, light2, light3, gorget, necklace1, necklace2, necklace3, necklace4, necklace5, necklace6, necklace7, necklace8, necklace9, sightSharpening, ring1, ring2, ring3, ring4, ring5, ring6, ring7, ring8, potion1, potion2, potion3, potion4, potion5, potion6, potion7, potion8, potion9, flask1, flask2, flask3, flask4, flask5, flask6, flask7, flask8, flask9, flask10, flask11, flask12, flask13, flask14, scroll1, scroll2, scroll3, scroll4, scroll5, scroll6, scroll7, scroll8, scroll9, scroll10, scroll11, armorLeather, armorMail, gloveFencing, gloveGauntlet, gloveJousting, buckler, shield, dagger, daggerDropBestWeapon, hammer, hammerParalyze, hammerSpark, sword, swordImpress, swordNullify, halberd, halberdPushActor, wand1, wand2, gem1, gem2, gem3, gem4, currency] -dart, dart200, bolas, harpoon, net, jumpingPole, whetstone, woodenTorch, oilLamp, brassLantern, gorget, necklace1, necklace2, necklace3, necklace4, necklace5, necklace6, necklace7, monocle, ring1, ring2, ring3, ring4, ring5, potion1, potion2, potion3, potion4, potion5, potion6, potion7, potion8, flask1, flask2, flask3, flask4, flask5, flask6, flask7, flask8, flask9, flask10, flask11, flask12, flask13, flask14, scroll1, scroll2, scroll3, scroll4, scroll5, scroll6, scroll7, scroll8, scroll9, scroll10, armorLeather, armorMail, gloveFencing, gloveGauntlet, gloveJousting, buckler, shield, dagger, daggerDropBestWeapon, hammer, hammerParalyze, hammerSpark, sword, swordImpress, swordNullify, halberd, halberdPushActor, wand1, wand2, gem1, gem2, gem3, currency :: ItemKind+dart, dart200, paralizingProj, harpoon, net, jumpingPole, sharpeningTool, seeingItem, light1, light2, light3, gorget, necklace1, necklace2, necklace3, necklace4, necklace5, necklace6, necklace7, necklace8, necklace9, sightSharpening, ring1, ring2, ring3, ring4, ring5, ring6, ring7, ring8, potion1, potion2, potion3, potion4, potion5, potion6, potion7, potion8, potion9, flask1, flask2, flask3, flask4, flask5, flask6, flask7, flask8, flask9, flask10, flask11, flask12, flask13, flask14, scroll1, scroll2, scroll3, scroll4, scroll5, scroll6, scroll7, scroll8, scroll9, scroll10, scroll11, armorLeather, armorMail, gloveFencing, gloveGauntlet, gloveJousting, buckler, shield, dagger, daggerDropBestWeapon, hammer, hammerParalyze, hammerSpark, sword, swordImpress, swordNullify, halberd, halberdPushActor, wand1, wand2, gem1, gem2, gem3, gem4, currency :: ItemKind necklace, ring, potion, flask, scroll, wand, gem :: ItemKind -- generic templates @@ -56,7 +58,7 @@ symbolHafted = ')' symbolWand = '/' -- magical rod, transmitter, pistol, rifle _symbolStaff = '_' -- scanner-_symbolFood = ','+_symbolFood = ',' -- too easy to miss? -- * Thrown weapons @@ -65,12 +67,12 @@ , iname = "dart" , ifreq = [("useful", 100), ("any arrow", 100)] , iflavour = zipPlain [Cyan]- , icount = 3 * d 3- , irarity = [(1, 20), (10, 10)]- , iverbHit = "prick"+ , icount = 4 * d 3+ , irarity = [(1, 10), (10, 20)]+ , iverbHit = "nick" , iweight = 50- , iaspects = [AddHurtRanged ((d 6 + dl 6) |*| 10)]- , ieffects = [Hurt (3 * d 1)]+ , iaspects = [AddHurtRanged (d 3 + dl 6 |*| 20)]+ , ieffects = [Hurt (2 * d 1)] , ifeature = [Identified] , idesc = "Little, but sharp and sturdy." -- "Much inferior to arrows though, especially given the contravariance problems." --- funny, but destroy the suspension of disbelief; this is supposed to be a Lovecraftian horror and any hilarity must ensue from the failures in making it so and not from actively trying to be funny; also, mundane objects are not supposed to be scary or transcendental; the scare is in horrors from the abstract dimension visiting our ordinary reality; without the contrast there's no horror and no wonder, so also the magical items must be contrasted with ordinary XIX century and antique items , ikit = []@@ -80,12 +82,12 @@ , iname = "fine dart" , ifreq = [("useful", 100), ("any arrow", 50)] -- TODO: until arrows added , iflavour = zipPlain [BrRed]- , icount = 3 * d 3- , irarity = [(4, 20), (10, 10)]+ , icount = 4 * d 3+ , irarity = [(1, 20), (10, 10)] , iverbHit = "prick" , iweight = 50- , iaspects = [AddHurtRanged ((d 6 + dl 6) |*| 10)]- , ieffects = [Hurt (2 * d 1)]+ , iaspects = [AddHurtRanged (d 3 + dl 6 |*| 20)]+ , ieffects = [Hurt (1 * d 1)] , ifeature = [toVelocity 200, Identified] , idesc = "Finely balanced for throws of great speed." , ikit = []@@ -93,7 +95,7 @@ -- * Exotic thrown weapons -bolas = ItemKind+paralizingProj = ItemKind { isymbol = symbolProjectile , iname = "bolas set" , ifreq = [("useful", 100)]@@ -105,7 +107,7 @@ , iaspects = [] , ieffects = [Hurt (2 * d 1), Paralyze (5 + d 5), DropBestWeapon] , ifeature = [Identified]- , idesc = "Wood balls tied with hemp rope. The target enemy is tripped and bound to drop its weapon, while recovering balance."+ , idesc = "Wood balls tied with hemp rope. The target enemy is tripped and bound to drop the main weapon, while fighting for balance." , ikit = [] } harpoon = ItemKind@@ -117,7 +119,7 @@ , irarity = [(10, 10)] , iverbHit = "hook" , iweight = 4000- , iaspects = [AddHurtRanged ((d 2 + 2 * dl 5) |*| 10)]+ , iaspects = [AddHurtRanged (d 2 + dl 5 |*| 20)] , ieffects = [Hurt (4 * d 1), PullActor (ThrowMod 200 50)] , ifeature = [] , idesc = "The cruel, barbed head lodges in its victim so painfully that the weakest tug of the thin line sends the victim flying."@@ -151,13 +153,13 @@ , irarity = [(1, 2), (10, 1)] , iverbHit = "prod" , iweight = 10000- , iaspects = [Timeout $ (d 2 + 2 - dl 2) |*| 10]+ , iaspects = [Timeout $ d 2 + 2 - dl 2 |*| 10] , ieffects = [Recharging (toOrganActorTurn "fast 20" 1)] , ifeature = [Durable, Applicable, Identified] , idesc = "Makes you vulnerable at take-off, but then you are free like a bird." , ikit = [] }-whetstone = ItemKind+sharpeningTool = ItemKind { isymbol = symbolTool , iname = "whetstone" , ifreq = [("useful", 100)]@@ -172,32 +174,49 @@ , idesc = "A portable sharpening stone that lets you fix your weapons between or even during fights, without the need to set up camp, fish out tools and assemble a proper sharpening workshop." , ikit = [] }+seeingItem = ItemKind+ { isymbol = '%'+ , iname = "pupil"+ , ifreq = [("useful", 100)]+ , iflavour = zipPlain [Red]+ , icount = 1+ , irarity = [(10, 1)]+ , iverbHit = "gaze at"+ , iweight = 100+ , iaspects = [ AddSight 10, AddMaxCalm 60, AddLight 2+ , Periodic, Timeout $ 1 + d 2 ]+ , ieffects = [ Recharging (toOrganNone "poisoned")+ , Recharging (Summon [("mobile monster", 1)] 1) ]+ , ifeature = [Identified]+ , idesc = "A slimy, dilated green pupil torn out from some giant eye. Clear and focused, as if still alive."+ , ikit = []+ } -- * Lights -woodenTorch = ItemKind+light1 = ItemKind { isymbol = symbolLight , iname = "wooden torch" , ifreq = [("useful", 100), ("light source", 100)] , iflavour = zipPlain [Brown]- , icount = 1- , irarity = [(1, 8), (3, 6)]+ , icount = 1 + d 3+ , irarity = [(1, 8)] , iverbHit = "scorch" , iweight = 1200- , iaspects = [ AddLight 3- , AddSight (-2) ] -- not only flashes, but also sparks- , ieffects = [Burn 3]+ , iaspects = [ AddLight 3 -- not only flashes, but also sparks+ , AddSight (-2) ] -- unused by AI due to the mixed blessing+ , ieffects = [Burn 2] , ifeature = [EqpSlot EqpSlotAddLight "", Identified]- , idesc = "A smoking, heavy wooden torch, burning in an unsteady fire."+ , idesc = "A smoking, heavy wooden torch, burning in an unsteady glow." , ikit = [] }-oilLamp = ItemKind+light2 = ItemKind { isymbol = symbolLight , iname = "oil lamp" , ifreq = [("useful", 100), ("light source", 100)] , iflavour = zipPlain [BrYellow] , icount = 1- , irarity = [(5, 5), (10, 5)]+ , irarity = [(7, 10)] , iverbHit = "burn" , iweight = 1000 , iaspects = [AddLight 3, AddSight (-1)]@@ -207,13 +226,13 @@ , idesc = "A clay lamp filled with plant oil feeding a tiny wick." , ikit = [] }-brassLantern = ItemKind+light3 = ItemKind { isymbol = symbolLight , iname = "brass lantern" , ifreq = [("useful", 100), ("light source", 100)] , iflavour = zipPlain [BrWhite] , icount = 1- , irarity = [(10, 4)]+ , irarity = [(10, 3)] , iverbHit = "burn" , iweight = 2400 , iaspects = [AddLight 4, AddSight (-1)]@@ -228,20 +247,21 @@ gorget = ItemKind { isymbol = symbolNecklace- , iname = "gorget"+ , iname = "Old Gorget" , ifreq = [("useful", 100)] , iflavour = zipFancy [BrCyan] , icount = 1- , irarity = [(4, 1), (10, 2)]+ , irarity = [(4, 3), (10, 3)] -- weak, shallow , iverbHit = "whip" , iweight = 30- , iaspects = [ Periodic- , Timeout $ (d 3 + 3 - dl 3) |*| 10- , AddArmorMelee 1- , AddArmorRanged 1 ]+ , iaspects = [ Unique+ , Periodic+ , Timeout $ d 3 + 3 - dl 3 |*| 10+ , AddArmorMelee $ 2 + d 3+ , AddArmorRanged $ 2 + d 3 ] , ieffects = [Recharging (RefillCalm 1)]- , ifeature = [ Durable, Precious, EqpSlot EqpSlotPeriodic "", Identified- , toVelocity 50 ] -- not dense enough+ , ifeature = [ Durable, Precious, EqpSlot EqpSlotPeriodic ""+ , Identified, toVelocity 50 ] -- not dense enough , 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 you." , ikit = [] }@@ -251,7 +271,7 @@ , ifreq = [("useful", 100)] , iflavour = zipFancy stdCol ++ zipPlain brightCol , icount = 1- , irarity = [(10, 3)]+ , irarity = [(10, 2)] , iverbHit = "whip" , iweight = 30 , iaspects = [Periodic]@@ -262,55 +282,74 @@ , ikit = [] } necklace1 = necklace- { iaspects = (Timeout $ (d 3 + 4 - dl 3) |*| 10) : iaspects necklace- , ieffects = [Recharging (RefillHP 1)]- , idesc = "A cord of dried herbs and healing berries."+ { ifreq = [("treasure", 100)]+ , iaspects = [Unique, Timeout $ d 3 + 4 - dl 3 |*| 10]+ ++ iaspects necklace+ , ieffects = [NoEffect "of Aromata", Recharging (RefillHP 1)]+ , ifeature = Durable : ifeature necklace+ , idesc = "A cord of freshly dried herbs and healing berries." } necklace2 = necklace- { irarity = [(2, 0), (10, 1)]- , iaspects = (Timeout $ (d 3 + 3 - dl 3) |*| 10) : iaspects necklace- , ieffects = [ Recharging (Impress)+ { ifreq = [("treasure", 100)] -- just too nasty to call it useful+ , irarity = [(10, 1)]+ , iaspects = (Timeout $ d 3 + 3 - dl 3 |*| 10) : iaspects necklace+ , ieffects = [ Recharging Impress , Recharging (DropItem COrgan "temporary conditions" True) , Recharging (Summon [("mobile animal", 1)] $ 1 + dl 2) , Recharging (Explode "waste") ] } necklace3 = necklace- { iaspects = (Timeout $ (d 3 + 3 - dl 3) |*| 10) : iaspects necklace+ { iaspects = (Timeout $ d 3 + 3 - dl 3 |*| 10) : iaspects necklace , ieffects = [Recharging (Paralyze $ 5 + d 5 + dl 5)] } necklace4 = necklace- { iaspects = (Timeout $ (d 4 + 4 - dl 4) |*| 2) : iaspects necklace- , ieffects = [Recharging (Teleport $ d 3 |*| 3)]+ { iaspects = (Timeout $ d 4 + 4 - dl 4 |*| 2) : iaspects necklace+ , ieffects = [Recharging (Teleport $ d 2 * 3)] } necklace5 = necklace- { iaspects = (Timeout $ (d 3 + 4 - dl 3) |*| 10) : iaspects necklace- , ieffects = [Recharging (Teleport $ 12 + d 3 |*| 3)]+ { iaspects = (Timeout $ d 3 + 4 - dl 3 |*| 10) : iaspects necklace+ , ieffects = [Recharging (Teleport $ 14 + d 3 * 3)] } necklace6 = necklace { iaspects = (Timeout $ d 4 |*| 10) : iaspects necklace , ieffects = [Recharging (PushActor (ThrowMod 100 50))] } necklace7 = necklace -- TODO: teach AI to wear only for fight- { irarity = [(4, 0), (10, 2)]- , iaspects = [AddSpeed $ d 2, Timeout $ (d 3 + 3 + dl 3) |*| 2]+ { ifreq = [("treasure", 100)]+ , iaspects = [ Unique, AddMaxHP $ 10 + d 10+ , AddArmorMelee 20, AddArmorRanged 20+ , Timeout $ d 2 + 5 - dl 3 ] ++ iaspects necklace- , ieffects = [Recharging (RefillHP (-1))]+ , ieffects = [ NoEffect "of Overdrive"+ , Recharging (InsertMove $ 1 + d 2)+ , Recharging (RefillHP (-1))+ , Recharging (RefillCalm (-1)) ]+ , ifeature = Durable : ifeature necklace }+necklace8 = necklace+ { iaspects = (Timeout $ d 3 + 3 - dl 3 |*| 5) : iaspects necklace+ , ieffects = [Recharging $ Explode "spark"]+ }+necklace9 = necklace+ { iaspects = (Timeout $ d 3 + 3 - dl 3 |*| 5) : iaspects necklace+ , ieffects = [Recharging $ Explode "fragrance"]+ } -- * Non-periodic jewelry -monocle = ItemKind+sightSharpening = ItemKind { isymbol = symbolRing- , iname = "monocle"- , ifreq = [("useful", 100)]+ , iname = "Sharp Monocle"+ , ifreq = [("treasure", 100)] , iflavour = zipPlain [White] , icount = 1- , irarity = [(5, 0), (10, 1)]+ , irarity = [(7, 3), (10, 3)] -- medium weak, medium shallow , iverbHit = "rap" , iweight = 50- , iaspects = [AddSight $ d 2, AddHurtMelee $ d 2 |*| 3]+ , iaspects = [Unique, AddSight $ 1 + d 2, AddHurtMelee $ d 2 |*| 3] , ieffects = []- , ifeature = [Precious, Identified, Durable, EqpSlot EqpSlotAddSight ""]+ , ifeature = [ Precious, Identified, Durable+ , EqpSlot EqpSlotAddSight "" ] , idesc = "Let's you better focus your weaker eye." , ikit = [] }@@ -334,33 +373,57 @@ } ring1 = ring { irarity = [(10, 2)]- , iaspects = [AddSpeed $ d 2, AddMaxHP $ dl 3 - 5 - d 3]+ , iaspects = [AddSpeed $ d 2, AddMaxHP $ dl 5 - 5 - d 5] , ieffects = [Explode "distortion"] -- strong magic , ifeature = ifeature ring ++ [EqpSlot EqpSlotAddSpeed ""] } ring2 = ring- { iaspects = [AddMaxHP $ 3 + dl 5, AddMaxCalm $ dl 6 - 15 - d 6]+ { iaspects = [AddMaxHP $ 10 + dl 10, AddMaxCalm $ dl 5 - 20 - d 5] , ifeature = ifeature ring ++ [EqpSlot EqpSlotAddMaxHP ""] } ring3 = ring- { iaspects = [AddMaxCalm $ 10 + dl 10]+ { iaspects = [AddMaxCalm $ 15 + dl 15] , ifeature = ifeature ring ++ [EqpSlot EqpSlotAddMaxCalm ""] , idesc = "Cold, solid to the touch, perfectly round, engraved with solemn, strangely comforting, worn out words." } ring4 = ring { irarity = [(3, 6), (10, 6)]- , iaspects = [AddHurtMelee $ (d 5 + dl 5) |*| 3, AddMaxHP $ dl 3 - 4 - d 2]+ , iaspects = [AddHurtMelee $ d 5 + dl 5 |*| 3, AddMaxHP $ dl 3 - 5 - d 3] , ifeature = ifeature ring ++ [EqpSlot EqpSlotAddHurtMelee ""] } ring5 = ring -- by the time it's found, probably no space in eqp- { irarity = [(5, 0)]+ { irarity = [(5, 0), (10, 1)] , iaspects = [AddLight $ d 2] , ieffects = [Explode "distortion"] -- strong magic , ifeature = ifeature ring ++ [EqpSlot EqpSlotAddLight ""] , idesc = "A sturdy ring with a large, shining stone." }+ring6 = ring+ { ifreq = [("treasure", 100)]+ , irarity = [(10, 2)]+ , iaspects = [ Unique, AddSpeed $ 3 + d 4+ , AddMaxCalm $ - 20 - d 20, AddMaxHP $ - 20 - d 20 ]+ , ieffects = [NoEffect "of Rush"] -- no explosion, because Durable+ , ifeature = ifeature ring ++ [Durable, EqpSlot EqpSlotAddSpeed ""]+ }+ring7 = ring+ { ifreq = [("useful", 100), ("ring of opportunity sniper", 1) ]+ , irarity = [(10, 1)]+ , iaspects = [AddSkills $ EM.fromList [(AbProject, 9)]]+ , ieffects = [ NoEffect "of opportunity sniper"+ , Explode "distortion" ] -- strong magic+ , ifeature = ifeature ring ++ [EqpSlot (EqpSlotAddSkills AbProject) ""]+ }+ring8 = ring+ { ifreq = [("useful", 1), ("ring of opportunity grenadier", 1) ]+ , irarity = [(10, 1)]+ , iaspects = [AddSkills $ EM.fromList [(AbProject, 11)]]+ , ieffects = [ NoEffect "of opportunity grenadier"+ , Explode "distortion" ] -- strong magic+ , ifeature = ifeature ring ++ [EqpSlot (EqpSlotAddSkills AbProject) ""]+ } --- * Exploding consumables, often intended to be thrown+-- * Ordinary exploding consumables, often intended to be thrown potion = ItemKind { isymbol = symbolPotion@@ -379,24 +442,28 @@ , ikit = [] } potion1 = potion- { ieffects = [ NoEffect "of rose water", Impress- , OnSmash (ApplyPerfume), OnSmash (Explode "fragrance") ]+ { ieffects = [ NoEffect "of rose water", Impress, RefillCalm (-3)+ , OnSmash ApplyPerfume, OnSmash (Explode "fragrance") ] } potion2 = potion- { ifreq = [("useful", 10)] -- extremely rare- , irarity = [(1, 1)]- , ieffects = [ NoEffect "of attraction", OnSmash (Explode "pheromone")]+ { ifreq = [("treasure", 100)]+ , irarity = [(6, 10), (10, 10)]+ , iaspects = [Unique]+ , ieffects = [ NoEffect "of Attraction", Impress, OverfillCalm (-20)+ , OnSmash (Explode "pheromone") ] } potion3 = potion- { irarity = [(1, 5), (10, 5)]- , ieffects = [RefillHP 5, OnSmash (Explode "healing mist")]+ { irarity = [(1, 10)]+ , ieffects = [ RefillHP 5, DropItem COrgan "poisoned" True+ , OnSmash (Explode "healing mist") ] } potion4 = potion- { irarity = [(10, 5)]- , ieffects = [RefillHP 10, OnSmash (Explode "healing mist 2")]+ { irarity = [(10, 10)]+ , ieffects = [ RefillHP 10, DropItem COrgan "poisoned" True+ , OnSmash (Explode "healing mist 2") ] } potion5 = potion- { ieffects = [ OneOf [Impress, DropBestWeapon, RefillHP 5, Burn 3]+ { ieffects = [ OneOf [ Impress, OverfillHP 10, OverfillHP 5, Burn 5 ] , OnSmash (OneOf [ Explode "healing mist" , Explode "wounding mist" , Explode "fragrance"@@ -404,24 +471,37 @@ } potion6 = potion { irarity = [(3, 3), (10, 6)]- , ieffects = [ OneOf [ Dominate, DropBestWeapon, RefillHP 20, Burn 9- , InsertMove 4 ]+ , ieffects = [ Impress+ , OneOf [ OverfillCalm (-60)+ , toOrganActorTurn "fast 20" (20 + d 5)+ , OverfillHP 20, OverfillHP 10, Burn 10 ] , OnSmash (OneOf [ Explode "healing mist 2"- , Explode "healing mist 2"- , Explode "pheromone"+ , Explode "calming mist"+ , Explode "distressing odor" , Explode "distortion" -- outlier, OK , Explode "blast 20" ]) ] } potion7 = potion- { ieffects = [ DropItem COrgan "poisoned" True, RefillHP 1+ { irarity = [(1, 15), (10, 5)]+ , ieffects = [ DropItem COrgan "poisoned" True , OnSmash (Explode "antidote mist") ] } potion8 = potion- { ieffects = [ DropItem COrgan "temporary conditions" True, RefillHP 2+ { irarity = [(1, 5), (10, 15)]+ , ieffects = [ DropItem COrgan "temporary conditions" True , OnSmash (Explode "blast 10") ] }+potion9 = potion+ { ifreq = [("treasure", 100)]+ , irarity = [(10, 5)]+ , iaspects = [Unique]+ , ieffects = [ NoEffect "of Love", OverfillHP 60+ , Impress, OverfillCalm (-60)+ , OnSmash (Explode "healing mist 2")+ , OnSmash (Explode "pheromone") ]+ } --- * Exploding consumables, with temporary aspects+-- * Exploding consumables with temporary aspects, can be thrown -- TODO: dip projectiles in those -- TODO: add flavour and realism as in, e.g., "flask of whiskey", -- which is more flavourful and believable than "flask of strength"@@ -429,7 +509,7 @@ flask = ItemKind { isymbol = symbolFlask , iname = "flask"- , ifreq = [("useful", 100)]+ , ifreq = [("useful", 100), ("flask", 100)] , iflavour = zipLiquid darkCol ++ zipPlain darkCol ++ zipFancy darkCol , icount = 1 , irarity = [(1, 9), (10, 6)]@@ -446,6 +526,7 @@ { irarity = [(10, 5)] , ieffects = [ NoEffect "of strength brew" , toOrganActorTurn "strengthened" (20 + d 5)+ , toOrganNone "regenerating" , OnSmash (Explode "strength mist") ] } flask2 = flask@@ -459,9 +540,9 @@ , OnSmash (Explode "protecting balm") ] } flask4 = flask- { ieffects = [ NoEffect "of red paint"- , toOrganGameTurn "painted red" (20 + d 5)- , OnSmash (Explode "red paint") ]+ { ieffects = [ NoEffect "of PhD defense questions"+ , toOrganGameTurn "defenseless" (20 + d 5)+ , OnSmash (Explode "PhD defense question") ] } flask5 = flask { irarity = [(10, 5)]@@ -473,6 +554,7 @@ { ieffects = [ NoEffect "of lethargy brew" , toOrganGameTurn "slow 10" (20 + d 5) , toOrganNone "regenerating"+ , RefillCalm 3 , OnSmash (Explode "slowness spray") ] } flask7 = flask -- sight can be reduced from Calm, drunk, etc.@@ -488,16 +570,20 @@ , OnSmash (Explode "smelly droplet") ] } flask9 = flask- { ieffects = [ NoEffect "of bait cocktail", toOrganActorTurn "drunk" (5 + d 5)+ { ieffects = [ NoEffect "of bait cocktail"+ , toOrganActorTurn "drunk" (5 + d 5)+ , Impress , OnSmash (Summon [("mobile animal", 1)] $ 1 + dl 2) , OnSmash (Explode "waste") ] } flask10 = flask- { ieffects = [ NoEffect "of whiskey", toOrganActorTurn "drunk" (20 + d 5)- , Burn 2, RefillHP 4, OnSmash (Explode "whiskey spray") ]+ { ieffects = [ NoEffect "of whiskey"+ , toOrganActorTurn "drunk" (20 + d 5)+ , Impress, Burn 2, RefillHP 4+ , OnSmash (Explode "whiskey spray") ] } flask11 = flask- { irarity = [(1, 20), (10, 6)]+ { irarity = [(1, 20), (10, 10)] , ieffects = [ NoEffect "of regeneration brew" , toOrganNone "regenerating" , OnSmash (Explode "healing mist") ]@@ -513,7 +599,7 @@ , toOrganNone "slow resistant" , OnSmash (Explode "anti-slow mist") ] }-flask14 = flask -- but not flask of Calm depletion, since Calm reduced often+flask14 = flask { irarity = [(10, 5)] , ieffects = [ NoEffect "of poison resistance" , toOrganNone "poison resistant"@@ -539,29 +625,31 @@ , ikit = [] } scroll1 = scroll- { irarity = [(3, 6), (10, 6)]- , ieffects = [CallFriend 1]+ { ifreq = [("treasure", 100)]+ , irarity = [(5, 10), (10, 10)] -- mixed blessing, so available early+ , iaspects = [Unique]+ , ieffects = [ NoEffect "of Reckless Beacon"+ , CallFriend 1, Summon standardSummon (2 + d 2) ] } scroll2 = scroll- { irarity = [(1, 7), (10, 5)]- , ieffects = [NoEffect "of fireworks", Explode "firecracker 7"]+ { irarity = []+ , ieffects = [] } scroll3 = scroll { irarity = [(1, 5), (10, 3)] , ieffects = [Ascend (-1)] } scroll4 = scroll- { ieffects = [OneOf [ Teleport $ d 3 |*| 3, RefillCalm 10, RefillCalm (-10)- , InsertMove 3, Paralyze 10, Identify CGround ]]+ { ieffects = [OneOf [ Teleport 5, RefillCalm 5, RefillCalm (-5)+ , InsertMove 5, Paralyze 10 ]] } scroll5 = scroll { irarity = [(10, 15)]- , ieffects = [OneOf [ Summon standardSummon $ d 2- , CallFriend (d 2), Ascend (-1), Ascend 1- , RefillCalm 30, RefillCalm (-30)- , CreateItem CGround "useful" TimerNone- , PolyItem CGround ]]- -- TODO: ask player: Escape 1+ , ieffects = [ Impress+ , OneOf [ Teleport 20, Ascend (-1), Ascend 1+ , Summon standardSummon 2, CallFriend 1+ , RefillCalm 5, OverfillCalm (-60)+ , CreateItem CGround "useful" TimerNone ] ] } scroll6 = scroll { ieffects = [Teleport 5]@@ -573,17 +661,26 @@ { irarity = [(10, 3)] , ieffects = [InsertMove $ 1 + d 2 + dl 2] }-scroll9 = scroll- { irarity = [(1, 15)]- , ieffects = [Identify CGround] -- TODO: ask player: AskPlayer cstore eff?+scroll9 = scroll -- TODO: remove Calm when server can tell if anything IDed+ { irarity = [(1, 15), (10, 10)]+ , ieffects = [ NoEffect "of scientific explanation"+ , Identify, OverfillCalm 3 ] }-scroll10 = scroll+scroll10 = scroll -- TODO: firecracker only if an item really polymorphed?+ -- But currently server can't tell. { irarity = [(10, 10)]- , ieffects = [PolyItem CGround]+ , ieffects = [ NoEffect "transfiguration"+ , PolyItem, Explode "firecracker 7" ] }+scroll11 = scroll+ { ifreq = [("treasure", 100)]+ , irarity = [(6, 10), (10, 10)]+ , iaspects = [Unique]+ , ieffects = [NoEffect "of Prisoner Release", CallFriend 1]+ } standardSummon :: Freqs ItemKind-standardSummon = [("monster", 30), ("mobile animal", 70)]+standardSummon = [("mobile monster", 30), ("mobile animal", 70)] -- * Armor @@ -597,8 +694,8 @@ , iverbHit = "thud" , iweight = 7000 , iaspects = [ AddHurtMelee (-3)- , AddArmorMelee $ (d 2 + dl 3) |*| 5- , AddArmorRanged $ (d 2 + dl 3) |*| 5 ]+ , AddArmorMelee $ 1 + d 2 + dl 2 |*| 5+ , AddArmorRanged $ 1 + d 2 + dl 2 |*| 5 ] , ieffects = [] , ifeature = [ toVelocity 30 -- unwieldy to throw and blunt , Durable, EqpSlot EqpSlotAddArmorMelee "", Identified ]@@ -611,8 +708,8 @@ , irarity = [(6, 9), (10, 3)] , iweight = 12000 , iaspects = [ AddHurtMelee (-3)- , AddArmorMelee $ (1 + d 2 + dl 4) |*| 5- , AddArmorRanged $ (1 + d 2 + dl 4) |*| 5 ]+ , AddArmorMelee $ 2 + d 2 + dl 3 |*| 5+ , AddArmorRanged $ 2 + d 2 + dl 3 |*| 5 ] , idesc = "A long shirt woven from iron rings. Discourages foes from attacking your torso, making it harder for them to land a blow." } gloveFencing = ItemKind@@ -637,18 +734,19 @@ , iflavour = zipPlain [BrCyan] , irarity = [(1, 9), (10, 3)] , iweight = 300- , iaspects = [ AddArmorMelee $ (1 + dl 2) |*| 5- , AddArmorRanged $ (1 + dl 2) |*| 5 ]+ , iaspects = [ AddArmorMelee $ 1 + dl 2 |*| 5+ , AddArmorRanged $ 1 + dl 2 |*| 5 ] , idesc = "Long leather gauntlet covered in overlapping steel plates." } gloveJousting = gloveFencing- { iname = "jousting gauntlet"+ { iname = "Tournament Gauntlet" , iflavour = zipFancy [BrRed] , irarity = [(1, 3), (10, 3)] , iweight = 500- , iaspects = [ AddHurtMelee $ (dl 4 - 6) |*| 3- , AddArmorMelee $ (2 + dl 2) |*| 5- , AddArmorRanged $ (2 + dl 2) |*| 5 ]+ , iaspects = [ Unique+ , AddHurtMelee $ dl 4 - 6 |*| 3+ , AddArmorMelee $ 2 + dl 2 |*| 5+ , AddArmorRanged $ 2 + dl 2 |*| 5 ] , idesc = "Rigid, steel, jousting handgear. If only you had a lance. And a horse." } @@ -667,23 +765,24 @@ , iweight = 2000 , iaspects = [ AddArmorMelee 40 , AddHurtMelee (-30)- , Timeout $ (d 3 + 3 - dl 3) |*| 2 ]- , ieffects = [] -- [Recharging (PushActor (ThrowMod 200 50))]- , ifeature = [ toVelocity 30 -- unwieldy to throw and blunt+ , Timeout $ d 3 + 3 - dl 3 |*| 2 ]+ , ieffects = [ Hurt (1 * d 1) -- to display xdy everywhre in Hurt+ , Recharging (PushActor (ThrowMod 200 50)) ]+ , ifeature = [ toVelocity 40 -- unwieldy to throw , Durable, EqpSlot EqpSlotAddArmorMelee "", Identified ] , idesc = "Heavy and unwieldy. Absorbs a percentage of melee damage, both dealt and sustained. Too small to intercept projectiles with." , ikit = [] } shield = buckler { iname = "shield"- , irarity = [(7, 5)]+ , irarity = [(8, 3)] , iflavour = zipPlain [Green] , iweight = 3000 , iaspects = [ AddArmorMelee 80 , AddHurtMelee (-70)- , Timeout $ (d 3 + 6 - dl 3) |*| 2 ]- , ieffects = [] -- [Recharging (PushActor (ThrowMod 400 50))]- , ifeature = [ toVelocity 20 -- unwieldy to throw and blunt+ , Timeout $ d 6 + 6 - dl 6 |*| 2 ]+ , ieffects = [Hurt (1 * d 1), Recharging (PushActor (ThrowMod 400 50))]+ , ifeature = [ toVelocity 30 -- unwieldy to throw , Durable, EqpSlot EqpSlotAddArmorMelee "", Identified ] , idesc = "Large and unwieldy. Absorbs a percentage of melee damage, both dealt and sustained. Too heavy to intercept projectiles with." }@@ -696,10 +795,12 @@ , ifreq = [("useful", 100), ("starting weapon", 100)] , iflavour = zipPlain [BrCyan] , icount = 1- , irarity = [(1, 12), (10, 4)]+ , irarity = [(1, 20), (10, 1)] , iverbHit = "stab" , iweight = 1000- , iaspects = [AddHurtMelee $ (d 3 + dl 3) |*| 3, AddArmorMelee $ d 2 |*| 5]+ , iaspects = [ AddHurtMelee $ d 3 + dl 3 |*| 3+ , AddArmorMelee $ d 2 |*| 5+ , AddHurtRanged (-60) ] -- as powerful as a dart , ieffects = [Hurt (6 * d 1)] , ifeature = [ toVelocity 40 -- ensuring it hits with the tip costs speed , Durable, EqpSlot EqpSlotWeapon "", Identified ]@@ -707,8 +808,9 @@ , ikit = [] } daggerDropBestWeapon = dagger- { ifreq = [("useful", 30)]- , irarity = [(1, 1), (10, 2)]+ { iname = "Double Dagger"+ , ifreq = [("treasure", 20)]+ , irarity = [(1, 2), (10, 4)] -- The timeout has to be small, so that the player can count on the effect -- occuring consistently in any longer fight. Otherwise, the effect will be -- absent in some important fights, leading to the feeling of bad luck,@@ -717,8 +819,9 @@ -- If the effect is very powerful and so the timeout has to be significant, -- let's make it really large, for the effect to occur only once in a fight: -- as soon as the item is equipped, or just on the first strike.- , iaspects = iaspects dagger ++ [Timeout $ (d 3 + 4 - dl 3) |*| 2]- , ieffects = ieffects dagger ++ [Recharging DropBestWeapon]+ , iaspects = [Unique, Timeout $ d 3 + 4 - dl 3 |*| 2]+ , ieffects = ieffects dagger+ ++ [Recharging DropBestWeapon, Recharging $ RefillCalm (-3)] , idesc = "A double dagger that a focused fencer can use to catch and twist an opponent's blade occasionally." } hammer = ItemKind@@ -727,10 +830,11 @@ , ifreq = [("useful", 100), ("starting weapon", 100)] , iflavour = zipPlain [BrMagenta] , icount = 1- , irarity = [(4, 12), (10, 2)]+ , irarity = [(5, 15)] , iverbHit = "club" , iweight = 1500- , iaspects = [AddHurtMelee $ (d 2 + dl 2) |*| 3]+ , iaspects = [ AddHurtMelee $ d 2 + dl 2 |*| 3+ , AddHurtRanged (-80) ] -- as powerful as a dart , ieffects = [Hurt (8 * d 1)] , ifeature = [ toVelocity 20 -- ensuring it hits with the sharp tip costs , Durable, EqpSlot EqpSlotWeapon "", Identified ]@@ -738,16 +842,17 @@ , ikit = [] } hammerParalyze = hammer- { ifreq = [("useful", 30)]- , irarity = [(4, 1), (10, 2)]- , iaspects = iaspects hammer ++ [Timeout $ (d 2 + 3 - dl 2) |*| 2]+ { iname = "Concussion Hammer"+ , ifreq = [("treasure", 20)]+ , irarity = [(5, 2), (10, 4)]+ , iaspects = [Unique, Timeout $ d 2 + 3 - dl 2 |*| 2] , ieffects = ieffects hammer ++ [Recharging $ Paralyze 5] } hammerSpark = hammer- { iname = "smithhammer"- , ifreq = [("useful", 30)]- , irarity = [(4, 1), (10, 2)]- , iaspects = iaspects hammer ++ [Timeout $ (d 4 + 4 - dl 4) |*| 2]+ { iname = "Grand Smithhammer"+ , ifreq = [("treasure", 20)]+ , irarity = [(5, 2), (10, 4)]+ , iaspects = [Unique, Timeout $ d 4 + 4 - dl 4 |*| 2] , ieffects = ieffects hammer ++ [Recharging $ Explode "spark"] } sword = ItemKind@@ -756,29 +861,32 @@ , ifreq = [("useful", 100), ("starting weapon", 100)] , iflavour = zipPlain [BrBlue] , icount = 1- , irarity = [(3, 1), (6, 16), (10, 8)]+ , irarity = [(4, 1), (5, 15)] , iverbHit = "slash" , iweight = 2000 , iaspects = [] , ieffects = [Hurt (10 * d 1)]- , ifeature = [ toVelocity 20 -- ensuring it hits with the tip costs speed+ , ifeature = [ toVelocity 5 -- ensuring it hits with the tip costs speed , Durable, EqpSlot EqpSlotWeapon "", Identified ] , idesc = "Difficult to master; deadly when used effectively. The steel is particularly hard and keen, but rusts quickly without regular maintenance." , ikit = [] } swordImpress = sword- { ifreq = [("useful", 30)]- , irarity = [(3, 1), (10, 2)]- , iaspects = iaspects sword ++ [Timeout $ (d 4 + 5 - dl 4) |*| 2]+ { iname = "Master's Sword"+ , ifreq = [("treasure", 20)]+ , irarity = [(5, 1), (10, 4)]+ , iaspects = [Unique, Timeout $ d 4 + 5 - dl 4 |*| 2] , ieffects = ieffects sword ++ [Recharging Impress] , idesc = "A particularly well-balance blade, lending itself to impressive shows of fencing skill." } swordNullify = sword- { ifreq = [("useful", 30)]- , irarity = [(3, 0), (10, 1)]- , iaspects = iaspects sword ++ [Timeout $ (d 4 + 5 - dl 4) |*| 2]+ { iname = "Gutting Sword"+ , ifreq = [("treasure", 20)]+ , irarity = [(5, 1), (10, 4)]+ , iaspects = [Unique, Timeout $ d 4 + 5 - dl 4 |*| 2] , ieffects = ieffects sword- ++ [Recharging $ DropItem COrgan "temporary conditions" True]+ ++ [ Recharging $ DropItem COrgan "temporary conditions" True+ , Recharging $ RefillHP (-2) ] , idesc = "Cold, thin blade that pierces deeply and sends its victim into abrupt, sobering shock." } halberd = ItemKind@@ -790,18 +898,18 @@ , irarity = [(7, 1), (10, 10)] , iverbHit = "impale" , iweight = 3000- , iaspects = [AddArmorMelee $ (1 + dl 3) |*| 5]+ , iaspects = [AddArmorMelee $ 1 + dl 3 |*| 5] , ieffects = [Hurt (12 * d 1)]- , ifeature = [ toVelocity 20 -- not balanced+ , ifeature = [ toVelocity 5 -- not balanced , Durable, EqpSlot EqpSlotWeapon "", Identified ] , idesc = "An improvised but deadly weapon made of a blade from a scythe attached to a long pole." , ikit = [] } halberdPushActor = halberd- { iname = "halberd"- , ifreq = [("useful", 30)]- , irarity = [(7, 1), (10, 2)]- , iaspects = iaspects halberd ++ [Timeout $ (d 5 + 5 - dl 5) |*| 2]+ { iname = "Swiss Halberd"+ , ifreq = [("treasure", 20)]+ , irarity = [(7, 1), (10, 4)]+ , iaspects = [Unique, Timeout $ d 5 + 5 - dl 5 |*| 2] , ieffects = ieffects halberd ++ [Recharging (PushActor (ThrowMod 400 25))] , idesc = "A versatile polearm, with great reach and leverage. Foes are held at a distance." }@@ -836,7 +944,7 @@ gem = ItemKind { isymbol = symbolGem , iname = "gem"- , ifreq = [("treasure", 100)]+ , ifreq = [("treasure", 100), ("gem", 1)] , iflavour = zipPlain $ delete BrYellow brightCol -- natural, so not fancy , icount = 1 , irarity = []@@ -849,21 +957,30 @@ , ikit = [] } gem1 = gem- { irarity = [(2, 0), (10, 10)]+ { irarity = [(2, 0), (10, 12)] } gem2 = gem- { irarity = [(4, 0), (10, 15)]+ { irarity = [(4, 0), (10, 14)] } gem3 = gem- { irarity = [(6, 0), (10, 20)]+ { irarity = [(6, 0), (10, 16)] }+gem4 = gem+ { iname = "elixir"+ , iflavour = zipPlain [BrYellow]+ , irarity = [(1, 40), (10, 40)]+ , iaspects = []+ , ieffects = [NoEffect "of youth", OverfillCalm 5, OverfillHP 15]+ , ifeature = [Identified, Precious] -- TODO: only for humans+ , idesc = "A crystal vial of amber liquid, supposedly granting eternal youth and fetching 100 gold per piece. The main effect seems to be mild euphoria, but it admittedly heals minor ailments rather well."+ } currency = ItemKind { isymbol = symbolGold , iname = "gold piece" , ifreq = [("treasure", 100), ("currency", 1)] , iflavour = zipPlain [BrYellow] , icount = 10 + d 20 + dl 20- , irarity = [(1, 0), (2, 15), (5, 25), (10, 10)]+ , irarity = [(1, 25), (10, 10)] , iverbHit = "tap" , iweight = 31 , iaspects = []
GameDefinition/Content/ItemKindActor.hs view
@@ -11,9 +11,9 @@ actors :: [ItemKind] actors =- [warrior, adventurer, blacksmith, forester, scientist, soldier, clerk, hairdresser, lawyer, peddler, taxCollector, eye, fastEye, nose, elbow, armadillo, gilaMonster, rattlesnake, komodoDragon, hyena, alligator, hornetSwarm, thornbush, geyser]+ [warrior, warrior2, warrior3, warrior4, warrior5, soldier, sniper, civilian, civilian2, civilian3, civilian4, civilian5, eye, fastEye, nose, elbow, torsor, goldenJackal, griffonVulture, skunk, armadillo, gilaMonster, rattlesnake, komodoDragon, hyena, alligator, rhinoceros, beeSwarm, hornetSwarm, thornbush, geyserBoiling, geyserArsenic, geyserSulfur] -warrior, adventurer, blacksmith, forester, scientist, soldier, clerk, hairdresser, lawyer, peddler, taxCollector, eye, fastEye, nose, elbow, armadillo, gilaMonster, rattlesnake, komodoDragon, hyena, alligator, hornetSwarm, thornbush, geyser :: ItemKind+warrior, warrior2, warrior3, warrior4, warrior5, soldier, sniper, civilian, civilian2, civilian3, civilian4, civilian5, eye, fastEye, nose, elbow, torsor, goldenJackal, griffonVulture, skunk, armadillo, gilaMonster, rattlesnake, komodoDragon, hyena, alligator, rhinoceros, beeSwarm, hornetSwarm, thornbush, geyserBoiling, geyserArsenic, geyserSulfur :: ItemKind -- * Hunams @@ -28,21 +28,20 @@ , iweight = 80000 , iaspects = [ AddMaxHP 60 -- partially from clothes and assumed first aid , AddMaxCalm 60, AddSpeed 20- , AddSkills $ EM.fromList [(AbProject, 1), (AbApply, 1)]- -- TODO: on a ring?- , AddSight 3 ] -- not via eyes, but feel, hearing, etc.+ , AddSkills $ EM.fromList [(AbProject, 2), (AbApply, 1)] ] , ieffects = [] , ifeature = [Durable, Identified] , idesc = ""- , ikit = [("fist", COrgan), ("foot", COrgan), ("eye 4", COrgan)]+ , ikit = [ ("fist", COrgan), ("foot", COrgan), ("eye 5", COrgan)+ , ("sapient brain", COrgan) ] }-adventurer = warrior+warrior2 = warrior { iname = "adventurer" }-blacksmith = warrior+warrior3 = warrior { iname = "blacksmith" }-forester = warrior+warrior4 = warrior { iname = "forester" }-scientist = warrior+warrior5 = warrior { iname = "scientist" } soldier = warrior@@ -50,17 +49,27 @@ , ifreq = [("soldier", 100)] , ikit = ikit warrior ++ [("starting weapon", CEqp)] }+sniper = warrior+ { iname = "sniper"+ , ifreq = [("sniper", 100)]+ , ikit = ikit warrior+ ++ [ ("ring of opportunity sniper", CEqp)+ , ("any arrow", CInv), ("any arrow", CInv)+ , ("any arrow", CInv), ("any arrow", CInv)+ , ("flask", CInv), ("light source", CInv)+ , ("light source", CInv), ("light source", CInv) ]+ } -clerk = warrior+civilian = warrior { iname = "clerk" , ifreq = [("civilian", 100)] }-hairdresser = clerk+civilian2 = civilian { iname = "hairdresser" }-lawyer = clerk+civilian3 = civilian { iname = "lawyer" }-peddler = clerk+civilian4 = civilian { iname = "peddler" }-taxCollector = clerk+civilian5 = civilian { iname = "tax collector" } -- * Monsters@@ -68,76 +77,97 @@ eye = ItemKind { isymbol = 'e' , iname = "reducible eye"- , ifreq = [("monster", 100), ("horror", 100)]- , iflavour = zipPlain [BrRed]+ , ifreq = [("monster", 100), ("horror", 100), ("mobile monster", 100)]+ , iflavour = zipFancy [BrRed] , icount = 1 , irarity = [(1, 10), (10, 6)] , iverbHit = "thud" , iweight = 80000- , iaspects = [ AddMaxHP 10, AddMaxCalm 60, AddSpeed 20- , AddSkills $ EM.fromList [(AbProject, 1), (AbApply, 1)]- , AddSight 4 ]+ , iaspects = [ AddMaxHP 16, AddMaxCalm 60, AddSpeed 20+ , AddSkills $ EM.fromList [(AbProject, 2), (AbApply, 1)] ] , ieffects = [] , ifeature = [Durable, Identified] , 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- , ikit = [("lash", COrgan), ("pupil", COrgan)]+ , ikit = [ ("lash", COrgan), ("pupil", COrgan)+ , ("sapient brain", COrgan) ] } fastEye = ItemKind { isymbol = 'j' , iname = "injective jaw"- , ifreq = [("monster", 100), ("horror", 100)]- , iflavour = zipPlain [BrBlue]+ , ifreq = [("monster", 100), ("horror", 100), ("mobile monster", 100)]+ , iflavour = zipFancy [BrBlue] , icount = 1- , irarity = [(1, 3), (10, 5)]+ , irarity = [(5, 5), (10, 5)] , iverbHit = "thud" , iweight = 80000- , iaspects = [ AddMaxHP 5, AddMaxCalm 60, AddSpeed 30- , AddSight 7 ]+ , iaspects = [ AddMaxHP 5, AddMaxCalm 60, AddSpeed 30 ] , ieffects = [] , ifeature = [Durable, Identified] , 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 = [ ("tooth", COrgan), ("speed gland 10", COrgan)- , ("lip", COrgan), ("lip", COrgan) ]+ , ("lip", COrgan), ("vision 4", COrgan)+ , ("sapient brain", COrgan) ] }-nose = ItemKind+nose = ItemKind -- depends solely on smell { isymbol = 'n' , iname = "point-free nose"- , ifreq = [("monster", 100), ("horror", 100)]- , iflavour = zipPlain [BrGreen]+ , ifreq = [("monster", 100), ("horror", 100), ("mobile monster", 100)]+ , iflavour = zipFancy [BrGreen] , icount = 1- , irarity = [(1, 10), (3, 0), (4, 5), (10, 5)] -- ensure explorers at lvl 3+ , irarity = [(1, 5), (4, 2), (10, 5)] , iverbHit = "thud" , iweight = 80000- , iaspects = [ AddMaxHP 20, AddMaxCalm 30, AddSpeed 18- , AddSkills $ EM.fromList [(AbProject, -1), (AbApply, -1)]- , AddSmell 3 ] -- depends solely on smell+ , iaspects = [ AddMaxHP 30, AddMaxCalm 30, AddSpeed 18+ , AddSkills $ EM.fromList [(AbProject, -1), (AbApply, -1)] ] , ieffects = [] , ifeature = [Durable, Identified] , 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 = [("nose tip", COrgan), ("lip", COrgan)]+ , ikit = [ ("nose tip", COrgan), ("lip", COrgan), ("nostril", COrgan)+ , ("sapient brain", COrgan) ] } elbow = ItemKind { isymbol = 'e' , iname = "commutative elbow"- , ifreq = [("monster", 100), ("horror", 100)]- , iflavour = zipPlain [BrMagenta]+ , ifreq = [("monster", 100), ("horror", 100), ("mobile monster", 100)]+ , iflavour = zipFancy [BrMagenta] , icount = 1- , irarity = [(6, 1), (10, 5)]+ , irarity = [(7, 1), (10, 5)] , iverbHit = "thud" , iweight = 80000- , iaspects = [ AddMaxHP 8, AddMaxCalm 90, AddSpeed 26+ , iaspects = [ AddMaxHP 8, AddMaxCalm 90, AddSpeed 21 , AddSkills- $ EM.fromList [(AbProject, 1), (AbApply, 1), (AbMelee, -1)]- , AddSight 15 ]+ $ EM.fromList [(AbProject, 2), (AbApply, 1), (AbMelee, -1)] ] , ieffects = [] , ifeature = [Durable, Identified] , idesc = "An arm strung like a bow. A few edges, but none keen enough. A few points, but none piercing. Deadly objects zip out of the void." , ikit = [ ("speed gland 4", COrgan), ("armored skin", COrgan)+ , ("vision 14", COrgan) , ("any arrow", CInv), ("any arrow", CInv)- , ("any arrow", CInv), ("any arrow", CInv) ]+ , ("any arrow", CInv), ("any arrow", CInv)+ , ("sapient brain", COrgan) ] }+torsor = ItemKind+ { isymbol = 'T'+ , iname = "The Forgetful Torsor"+ , ifreq = [("monster", 100)]+ , iflavour = zipFancy [BrCyan]+ , icount = 1+ , irarity = [(9, 0), (10, 1000)] -- unique+ , iverbHit = "thud"+ , iweight = 80000+ , iaspects = [ Unique, AddMaxHP 200, AddMaxCalm 100, AddSpeed 10+ , AddSkills $ EM.fromList+ [(AbProject, 2), (AbApply, 1), (AbTrigger, -1)] ]+ -- can't switch levels, a miniboss+ , ieffects = []+ , ifeature = [Durable, Identified]+ , 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 = [ ("right torsion", COrgan), ("left torsion", COrgan)+ , ("pupil", COrgan)+ , ("gem", CInv), ("gem", CInv), ("gem", CInv), ("gem", CInv)+ , ("sapient brain", COrgan) ]+ } -- "ground x" --- for immovable monster that can only tele or prob travel--- forgetful -- pullback -- skeletal @@ -146,10 +176,59 @@ -- They need rather strong melee, because they don't use items. -- Unless/until they level up. -animalSkillMalus :: Skills-animalSkillMalus =- EM.fromList $ zip [AbDisplace, AbMoveItem, AbProject, AbApply] [-1, -1..]-+goldenJackal = ItemKind -- basically a much smaller and slower hyena+ { isymbol = 'j'+ , iname = "golden jackal"+ , ifreq = [("animal", 100), ("horror", 100), ("mobile animal", 100), ("scavenger", 50)]+ , iflavour = zipPlain [BrYellow]+ , icount = 1+ , irarity = [(1, 5)]+ , iverbHit = "thud"+ , iweight = 13000+ , iaspects = [ AddMaxHP 12, AddMaxCalm 60, AddSpeed 22 ]+ , ieffects = []+ , ifeature = [Durable, Identified]+ , idesc = ""+ , ikit = [ ("small jaw", COrgan), ("eye 5", COrgan), ("nostril", COrgan)+ , ("animal brain", COrgan) ]+ }+griffonVulture = ItemKind+ { isymbol = 'v'+ , iname = "griffon vulture"+ , ifreq = [("animal", 100), ("horror", 100), ("mobile animal", 100), ("scavenger", 30)]+ , iflavour = zipPlain [BrYellow]+ , icount = 1+ , irarity = [(1, 5)]+ , iverbHit = "thud"+ , iweight = 13000+ , iaspects = [ AddMaxHP 12, AddMaxCalm 60, AddSpeed 20+ , AddSkills $ EM.singleton AbAlter (-1) ]+ , ieffects = []+ , ifeature = [Durable, Identified]+ , idesc = ""+ , ikit = [ ("screeching beak", COrgan) -- in reality it grunts and hisses+ , ("claw", COrgan), ("eye 6", COrgan)+ , ("animal brain", COrgan) ]+ }+skunk = ItemKind+ { isymbol = 's'+ , iname = "hog-nosed skunk"+ , ifreq = [("animal", 100), ("horror", 100), ("mobile animal", 100)]+ , iflavour = zipPlain [White]+ , icount = 1+ , irarity = [(1, 5), (10, 3)]+ , iverbHit = "thud"+ , iweight = 4000+ , iaspects = [ AddMaxHP 10, AddMaxCalm 30, AddSpeed 20+ , AddSkills $ EM.singleton AbAlter (-1) ]+ , ieffects = []+ , ifeature = [Durable, Identified]+ , idesc = ""+ , ikit = [ ("scent gland", COrgan)+ , ("small claw", COrgan), ("snout", COrgan)+ , ("nostril", COrgan), ("eye 2", COrgan)+ , ("animal brain", COrgan) ]+ } armadillo = ItemKind { isymbol = 'a' , iname = "giant armadillo"@@ -160,13 +239,13 @@ , iverbHit = "thud" , iweight = 80000 , iaspects = [ AddMaxHP 30, AddMaxCalm 30, AddSpeed 18- , AddSkills $ EM.insert AbAlter (-1) animalSkillMalus- , AddSight 3 ]+ , AddSkills $ EM.singleton AbAlter (-1) ] , ieffects = [] , ifeature = [Durable, Identified] , idesc = "" , ikit = [ ("claw", COrgan), ("snout", COrgan), ("armored skin", COrgan)- , ("nostril", COrgan) ]+ , ("nostril", COrgan), ("eye 2", COrgan)+ , ("animal brain", COrgan) ] } gilaMonster = ItemKind { isymbol = 'g'@@ -177,66 +256,66 @@ , irarity = [(2, 5), (10, 3)] , iverbHit = "thud" , iweight = 80000- , iaspects = [ AddMaxHP 15, AddMaxCalm 60, AddSpeed 15- , AddSkills $ EM.insert AbAlter (-1) animalSkillMalus- , AddSight 3 ]+ , iaspects = [ AddMaxHP 12, AddMaxCalm 60, AddSpeed 15+ , AddSkills $ EM.singleton AbAlter (-1) ] , ieffects = [] , ifeature = [Durable, Identified] , idesc = "" , ikit = [ ("venom tooth", COrgan), ("small claw", COrgan)- , ("eye 4", COrgan), ("nostril", COrgan) ]+ , ("eye 5", COrgan), ("nostril", COrgan)+ , ("animal brain", COrgan) ] } rattlesnake = ItemKind- { isymbol = 'r'+ { isymbol = 's' , iname = "rattlesnake" , ifreq = [("animal", 100), ("horror", 100), ("mobile animal", 100)] , iflavour = zipPlain [Brown] , icount = 1- , irarity = [(3, 2), (10, 4)]+ , irarity = [(3, 3), (10, 5)] , iverbHit = "thud" , iweight = 80000 , iaspects = [ AddMaxHP 25, AddMaxCalm 60, AddSpeed 15- , AddSkills $ EM.insert AbAlter (-1) animalSkillMalus- , AddSight 3 ]+ , AddSkills $ EM.singleton AbAlter (-1) ] , ieffects = [] , ifeature = [Durable, Identified] , idesc = "" , ikit = [ ("venom fang", COrgan)- , ("eye 4", COrgan), ("nostril", COrgan) ]+ , ("eye 5", COrgan), ("nostril", COrgan)+ , ("animal brain", COrgan) ] }-komodoDragon = ItemKind -- bad hearing+komodoDragon = ItemKind -- bad hearing; regeneration makes it very powerful { isymbol = 'k' , iname = "Komodo dragon" , ifreq = [("animal", 100), ("horror", 100), ("mobile animal", 100)] , iflavour = zipPlain [Blue] , icount = 1- , irarity = [(5, 5), (10, 7)]+ , irarity = [(7, 0), (10, 10)] , iverbHit = "thud" , iweight = 80000- , iaspects = [ AddMaxHP 40, AddMaxCalm 60, AddSpeed 16- , AddSkills animalSkillMalus, AddSight 3 ]+ , iaspects = [ AddMaxHP 40, AddMaxCalm 60, AddSpeed 16 ] , ieffects = [] , ifeature = [Durable, Identified] , idesc = ""- , ikit = [ ("large tail", COrgan), ("jaw", COrgan), ("small claw", COrgan)+ , ikit = [ ("large tail", COrgan), ("jaw", COrgan), ("claw", COrgan) , ("speed gland 4", COrgan), ("armored skin", COrgan)- , ("eye 2", COrgan), ("nostril", COrgan) ]+ , ("eye 2", COrgan), ("nostril", COrgan)+ , ("animal brain", COrgan) ] } hyena = ItemKind { isymbol = 'h' , iname = "spotted hyena"- , ifreq = [("animal", 100), ("horror", 100), ("mobile animal", 100)]- , iflavour = zipPlain [Red]+ , ifreq = [("animal", 100), ("horror", 100), ("mobile animal", 100), ("scavenger", 20)]+ , iflavour = zipPlain [BrYellow] , icount = 1- , irarity = [(4, 6), (10, 6)]+ , irarity = [(4, 1), (10, 8)] , iverbHit = "thud"- , iweight = 80000- , iaspects = [ AddMaxHP 20, AddMaxCalm 60, AddSpeed 30- , AddSkills animalSkillMalus, AddSight 3 ]+ , iweight = 60000+ , iaspects = [ AddMaxHP 20, AddMaxCalm 60, AddSpeed 30 ] , ieffects = [] , ifeature = [Durable, Identified] , idesc = ""- , ikit = [("jaw", COrgan), ("eye 4", COrgan), ("nostril", COrgan)]+ , ikit = [ ("jaw", COrgan), ("eye 5", COrgan), ("nostril", COrgan)+ , ("animal brain", COrgan) ] } alligator = ItemKind { isymbol = 'a'@@ -244,73 +323,137 @@ , ifreq = [("animal", 100), ("horror", 100), ("mobile animal", 100)] , iflavour = zipPlain [Blue] , icount = 1- , irarity = [(10, 8)]+ , irarity = [(6, 1), (10, 9)] , iverbHit = "thud" , iweight = 80000- , iaspects = [ AddMaxHP 30, AddMaxCalm 60, AddSpeed 17- , AddArmorMelee 30, AddArmorRanged 30- , AddSkills animalSkillMalus, AddSight 3 ]+ , iaspects = [ AddMaxHP 40, AddMaxCalm 60, AddSpeed 17 ] , ieffects = [] , ifeature = [Durable, Identified] , idesc = ""- , ikit = [ ("large jaw", COrgan), ("large tail", COrgan), ("claw", COrgan)- , ("armored skin", COrgan), ("eye 4", COrgan) ]+ , ikit = [ ("large jaw", COrgan), ("large tail", COrgan)+ , ("small claw", COrgan)+ , ("armored skin", COrgan), ("eye 5", COrgan)+ , ("animal brain", COrgan) ] }+rhinoceros = ItemKind+ { isymbol = 'R'+ , iname = "The Maddened Rhinoceros"+ , ifreq = [("animal", 100)]+ , iflavour = zipPlain [Brown]+ , icount = 1+ , irarity = [(2, 0), (3, 1000000), (4, 0)] -- unique+ , iverbHit = "thud"+ , iweight = 80000+ , iaspects = [ Unique, AddMaxHP 70, AddMaxCalm 60, AddSpeed 25+ , AddSkills $ EM.singleton AbTrigger (-1) ]+ -- can't switch levels, a miniboss+ , ieffects = []+ , ifeature = [Durable, Identified]+ , idesc = "The last of its kind. Blind with rage. Charges at deadly speed."+ , ikit = [ ("armored skin", COrgan), ("eye 2", COrgan)+ , ("horn", COrgan), ("snout", COrgan)+ , ("animal brain", COrgan) ]+ } -- * Non-animal animals +beeSwarm = ItemKind+ { isymbol = 'b'+ , iname = "bee swarm"+ , ifreq = [("animal", 100), ("horror", 100)]+ , iflavour = zipPlain [Brown]+ , icount = 1+ , irarity = [(1, 3), (10, 6)]+ , iverbHit = "thud"+ , iweight = 1000+ , iaspects = [ AddMaxHP 8, AddMaxCalm 60, AddSpeed 30+ , AddSkills $ EM.singleton AbAlter (-1) ] -- armor in sting+ , ieffects = []+ , ifeature = [Durable, Identified]+ , idesc = ""+ , ikit = [ ("bee sting", COrgan), ("vision 4", COrgan)+ , ("insect mortality", COrgan), ("animal brain", COrgan) ]+ } hornetSwarm = ItemKind { isymbol = 'h' , iname = "hornet swarm" , ifreq = [("animal", 100), ("horror", 100), ("mobile animal", 100)] , iflavour = zipPlain [Magenta] , icount = 1- , irarity = [(5, 1), (10, 5)]+ , irarity = [(5, 1), (10, 10)] , iverbHit = "thud" , iweight = 1000- , iaspects = [ AddMaxHP 5, AddMaxCalm 60, AddSpeed 30, AddSight 2- , AddSkills $ EM.insert AbAlter (-1) animalSkillMalus- , AddArmorMelee 90, AddArmorRanged 90 ]+ , iaspects = [ AddMaxHP 8, AddMaxCalm 60, AddSpeed 30+ , AddSkills $ EM.singleton AbAlter (-1)+ , AddArmorMelee 80, AddArmorRanged 80 ] , ieffects = [] , ifeature = [Durable, Identified] , idesc = ""- , ikit = [("sting", COrgan)]+ , ikit = [ ("sting", COrgan), ("vision 4", COrgan)+ , ("insect mortality", COrgan), ("animal brain", COrgan) ] } thornbush = ItemKind- { isymbol = 'b'+ { isymbol = 't' , iname = "thornbush" , ifreq = [("animal", 100)] , iflavour = zipPlain [Brown] , icount = 1- , irarity = [(3, 2), (10, 1)]+ , irarity = [(1, 3), (10, 1)] , iverbHit = "thud" , iweight = 80000 , iaspects = [ AddMaxHP 20, AddMaxCalm 999, AddSpeed 20- , AddSkills- $ EM.fromDistinctAscList (zip [minBound..maxBound] [-1, -1..])- `addSkills` EM.fromList (zip [AbWait, AbMelee] [1, 1..])- , AddArmorMelee 50, AddArmorRanged 50 ]+ , AddSkills $ EM.fromList (zip [AbWait, AbMelee] [1, 1..]) ] , ieffects = [] , ifeature = [Durable, Identified] , idesc = ""- , ikit = [("thorn", COrgan)]+ , ikit = [("thorn", COrgan), ("armored skin", COrgan)] }-geyser = ItemKind+geyserBoiling = ItemKind { isymbol = 'g' , iname = "geyser"- , ifreq = [("animal", 100)]- , iflavour = zipPlain [White]+ , ifreq = [("animal", 50), ("immobile vents", 100)]+ , iflavour = zipPlain [Blue] , icount = 1 , irarity = [(5, 2), (10, 1)] , iverbHit = "thud" , iweight = 80000 , iaspects = [ AddMaxHP 10, AddMaxCalm 999, AddSpeed 5- , AddSkills- $ EM.fromDistinctAscList (zip [minBound..maxBound] [-1, -1..])- `addSkills` EM.fromList (zip [AbWait, AbMelee] [1, 1..])+ , AddSkills $ EM.fromList (zip [AbWait, AbMelee] [1, 1..]) , AddArmorMelee 80, AddArmorRanged 80 ] , ieffects = [] , ifeature = [Durable, Identified] , idesc = ""- , ikit = [("vent", COrgan), ("fissure", COrgan)]+ , ikit = [("boiling vent", COrgan), ("boiling fissure", COrgan)]+ }+geyserArsenic = ItemKind+ { isymbol = 'g'+ , iname = "arsenic geyser"+ , ifreq = [("animal", 50), ("immobile vents", 100)]+ , iflavour = zipPlain [Cyan]+ , icount = 1+ , irarity = [(5, 2), (10, 1)]+ , iverbHit = "thud"+ , iweight = 80000+ , iaspects = [ AddMaxHP 30, AddMaxCalm 999, AddSpeed 20, AddLight 3+ , AddSkills $ EM.fromList (zip [AbWait, AbMelee] [1, 1..]) ]+ , ieffects = []+ , ifeature = [Durable, Identified]+ , idesc = ""+ , ikit = [("arsenic vent", COrgan), ("arsenic fissure", COrgan)]+ }+geyserSulfur = ItemKind+ { isymbol = 'g'+ , iname = "sulfur geyser"+ , ifreq = [("animal", 50), ("immobile vents", 300)]+ , iflavour = zipPlain [BrYellow] -- exception, animal with bright color+ , icount = 1+ , irarity = [(5, 4), (10, 2)]+ , iverbHit = "thud"+ , iweight = 80000+ , iaspects = [ AddMaxHP 30, AddMaxCalm 999, AddSpeed 20, AddLight 3+ , AddSkills $ EM.fromList (zip [AbWait, AbMelee] [1, 1..]) ]+ , ieffects = []+ , ifeature = [Durable, Identified]+ , idesc = ""+ , ikit = [("sulfur vent", COrgan), ("sulfur fissure", COrgan)] }
GameDefinition/Content/ItemKindBlast.hs view
@@ -10,9 +10,9 @@ blasts :: [ItemKind] blasts =- [burningOil2, burningOil3, burningOil4, explosionBlast2, explosionBlast10, explosionBlast20, firecracker2, firecracker3, firecracker4, firecracker5, firecracker6, firecracker7, fragrance, pheromone, mistHealing, mistHealing2, mistWounding, distortion, waste, glassPiece, smoke, boilingWater, glue, spark, mistAntiSlow, mistAntidote, mistStrength, mistWeakness, protectingBalm, redPaint, hasteSpray, slownessSpray, eyeDrop, smellyDroplet, whiskeySpray]+ [burningOil2, burningOil3, burningOil4, explosionBlast2, explosionBlast10, explosionBlast20, firecracker2, firecracker3, firecracker4, firecracker5, firecracker6, firecracker7, fragrance, pheromone, mistCalming, odorDistressing, mistHealing, mistHealing2, mistWounding, distortion, waste, glassPiece, smoke, boilingWater, glue, spark, mistAntiSlow, mistAntidote, mistStrength, mistWeakness, protectingBalm, vulnerabilityBalm, hasteSpray, slownessSpray, eyeDrop, smellyDroplet, whiskeySpray] -burningOil2, burningOil3, burningOil4, explosionBlast2, explosionBlast10, explosionBlast20, firecracker2, firecracker3, firecracker4, firecracker5, firecracker6, firecracker7, fragrance, pheromone, mistHealing, mistHealing2, mistWounding, distortion, waste, glassPiece, smoke, boilingWater, glue, spark, mistAntiSlow, mistAntidote, mistStrength, mistWeakness, protectingBalm, redPaint, hasteSpray, slownessSpray, eyeDrop, smellyDroplet, whiskeySpray :: ItemKind+burningOil2, burningOil3, burningOil4, explosionBlast2, explosionBlast10, explosionBlast20, firecracker2, firecracker3, firecracker4, firecracker5, firecracker6, firecracker7, fragrance, pheromone, mistCalming, odorDistressing, mistHealing, mistHealing2, mistWounding, distortion, waste, glassPiece, smoke, boilingWater, glue, spark, mistAntiSlow, mistAntidote, mistStrength, mistWeakness, protectingBalm, vulnerabilityBalm, hasteSpray, slownessSpray, eyeDrop, smellyDroplet, whiskeySpray :: ItemKind -- * Parameterized immediate effect blasts @@ -22,13 +22,12 @@ , iname = "burning oil" , ifreq = [(toGroupName $ "burning oil" <+> tshow n, 1)] , iflavour = zipFancy [BrYellow]- , icount = intToDice (n * 4)+ , icount = intToDice (n * 5) , irarity = [(1, 1)] , iverbHit = "burn" , iweight = 1 , iaspects = [AddLight 2]- , ieffects = [ Burn (n `div` 2)- , Paralyze (intToDice $ n `div` 2) ] -- tripping on oil+ , ieffects = [Burn 1, Paralyze 1] -- tripping on oil , ifeature = [ toVelocity (min 100 $ n * 7) , Fragile, Identified ] , idesc = "Sticky oil, burning brightly."@@ -43,15 +42,15 @@ , iname = "blast" , ifreq = [(toGroupName $ "blast" <+> tshow n, 1)] , iflavour = zipPlain [BrRed]- , icount = 12 -- strong, but few, so not always hits target+ , icount = 15 -- strong, but few, so not always hits target , irarity = [(1, 1)] , iverbHit = "tear apart" , iweight = 1 , iaspects = [AddLight $ intToDice n]- , ieffects = [RefillHP (- n `div` 2)]- ++ [DropBestWeapon | n > 2]+ , ieffects = [Impress, RefillHP (- n `div` 2)]+ ++ [PushActor (ThrowMod (100 * (n `div` 5)) 50)] ++ [DropItem COrgan "temporary conditions" True | n >= 10]- , ifeature = [Fragile, toLinger 10, Identified]+ , ifeature = [Fragile, toLinger 20, Identified] , idesc = "" , ikit = [] }@@ -69,7 +68,9 @@ , iverbHit = "crack" , iweight = 1 , iaspects = [AddLight $ intToDice $ n `div` 2]- , ieffects = [ Burn 1 | n > 5 ]+ , ieffects = [Impress]+ ++ [ RefillCalm (-1) | n >= 5 ]+ ++ [ DropBestWeapon | n >= 5] ++ [ OnSmash (Explode $ toGroupName $ "firecracker" <+> tshow (n - 1)) | n > 2 ]@@ -92,13 +93,15 @@ , iname = "fragrance" , ifreq = [("fragrance", 1)] , iflavour = zipFancy [Magenta]- , icount = 15+ , icount = 20 , irarity = [(1, 1)] , iverbHit = "engulf" , iweight = 1 , iaspects = [] , ieffects = [Impress]- , ifeature = [ toVelocity 13 -- the slowest that travels at least 2 steps+ -- Linger 10, because sometimes it takes 2 turns due to starting just+ -- before actor turn's end (e.g., via a necklace).+ , ifeature = [ ToThrow $ ThrowMod 28 10 -- 2 steps, one turn , Fragile, Identified ] , idesc = "" , ikit = []@@ -108,23 +111,55 @@ , iname = "musky whiff" , ifreq = [("pheromone", 1)] , iflavour = zipFancy [BrMagenta]- , icount = 8+ , icount = 18 , irarity = [(1, 1)] , iverbHit = "tempt" , iweight = 1 , iaspects = []- , ieffects = [Dominate]+ , ieffects = [Impress, OverfillCalm (-20)] , ifeature = [ toVelocity 13 -- the slowest that travels at least 2 steps , Fragile, Identified ] , idesc = "" , ikit = [] }+mistCalming = ItemKind+ { isymbol = '\''+ , iname = "mist"+ , ifreq = [("calming mist", 1)]+ , iflavour = zipFancy [White]+ , icount = 19+ , irarity = [(1, 1)]+ , iverbHit = "sooth"+ , iweight = 1+ , iaspects = []+ , ieffects = [RefillCalm 2]+ , ifeature = [ toVelocity 13 -- the slowest that travels at least 2 steps+ , Fragile, Identified ]+ , idesc = ""+ , ikit = []+ }+odorDistressing = ItemKind+ { isymbol = '\''+ , iname = "distressing whiff"+ , ifreq = [("distressing odor", 1)]+ , iflavour = zipFancy [BrRed]+ , icount = 10+ , irarity = [(1, 1)]+ , iverbHit = "distress"+ , iweight = 1+ , iaspects = []+ , ieffects = [OverfillCalm (-20)]+ , ifeature = [ toVelocity 13 -- the slowest that travels at least 2 steps+ , Fragile, Identified ]+ , idesc = ""+ , ikit = []+ } mistHealing = ItemKind { isymbol = '\'' , iname = "mist" , ifreq = [("healing mist", 1)] , iflavour = zipFancy [White]- , icount = 11+ , icount = 9 , irarity = [(1, 1)] , iverbHit = "revitalize" , iweight = 1@@ -140,7 +175,7 @@ , iname = "mist" , ifreq = [("healing mist 2", 1)] , iflavour = zipFancy [White]- , icount = 12+ , icount = 8 , irarity = [(1, 1)] , iverbHit = "revitalize" , iweight = 1@@ -156,7 +191,7 @@ , iname = "mist" , ifreq = [("wounding mist", 1)] , iflavour = zipFancy [White]- , icount = 13+ , icount = 7 , irarity = [(1, 1)] , iverbHit = "devitalize" , iweight = 1@@ -172,12 +207,12 @@ , iname = "vortex" , ifreq = [("distortion", 1)] , iflavour = zipFancy [White]- , icount = 4+ , icount = 6 , irarity = [(1, 1)] , iverbHit = "engulf" , iweight = 1 , iaspects = []- , ieffects = [Teleport $ 15 + d 10]+ , ieffects = [Impress, Teleport $ 15 + d 10] , ifeature = [ toVelocity 7 -- the slowest that gets anywhere (1 step only) , Fragile, Identified ] , idesc = ""@@ -188,13 +223,13 @@ , iname = "waste" , ifreq = [("waste", 1)] , iflavour = zipPlain [Brown]- , icount = 10+ , icount = 18 , irarity = [(1, 1)] , iverbHit = "splosh" , iweight = 50 , iaspects = [] , ieffects = [RefillHP (-1)]- , ifeature = [ ToThrow $ ThrowMod 28 50+ , ifeature = [ ToThrow $ ThrowMod 28 10 -- 2 steps, one turn , Fragile, Identified ] , idesc = "" , ikit = []@@ -204,7 +239,7 @@ , iname = "glass piece" , ifreq = [("glass piece", 1)] , iflavour = zipPlain [BrBlue]- , icount = 17+ , icount = 18 , irarity = [(1, 1)] , iverbHit = "cut" , iweight = 10@@ -234,7 +269,7 @@ , iname = "boiling water" , ifreq = [("boiling water", 1)] , iflavour = zipPlain [BrWhite]- , icount = 10+ , icount = 21 , irarity = [(1, 1)] , iverbHit = "boil" , iweight = 5@@ -249,7 +284,7 @@ , iname = "hoof glue" , ifreq = [("glue", 1)] , iflavour = zipPlain [BrYellow]- , icount = 14+ , icount = 20 , irarity = [(1, 1)] , iverbHit = "glue" , iweight = 20@@ -264,7 +299,7 @@ , iname = "spark" , ifreq = [("spark", 1)] , iflavour = zipPlain [BrYellow]- , icount = 18+ , icount = 17 , irarity = [(1, 1)] , iverbHit = "burn" , iweight = 1@@ -279,7 +314,7 @@ , iname = "mist" , ifreq = [("anti-slow mist", 1)] , iflavour = zipPlain [BrRed]- , icount = 11+ , icount = 7 , irarity = [(1, 1)] , iverbHit = "propel" , iweight = 1@@ -295,7 +330,7 @@ , iname = "mist" , ifreq = [("antidote mist", 1)] , iflavour = zipPlain [BrBlue]- , icount = 11+ , icount = 8 , irarity = [(1, 1)] , iverbHit = "cure" , iweight = 1@@ -314,7 +349,7 @@ , iname = "mist" , ifreq = [("strength mist", 1)] , iflavour = zipFancy [Red]- , icount = 11+ , icount = 6 , irarity = [(1, 1)] , iverbHit = "strengthen" , iweight = 1@@ -330,7 +365,7 @@ , iname = "mist" , ifreq = [("weakness mist", 1)] , iflavour = zipFancy [Blue]- , icount = 12+ , icount = 5 , irarity = [(1, 1)] , iverbHit = "weaken" , iweight = 1@@ -357,17 +392,17 @@ , idesc = "" , ikit = [] }-redPaint = ItemKind+vulnerabilityBalm = ItemKind { isymbol = '\''- , iname = "red paint"- , ifreq = [("red paint", 1)]+ , iname = "PhD defense question"+ , ifreq = [("PhD defense question", 1)] , iflavour = zipPlain [BrRed] , icount = 14 , irarity = [(1, 1)]- , iverbHit = "paint"+ , iverbHit = "nag" , iweight = 1 , iaspects = []- , ieffects = [toOrganGameTurn "painted red" (3 + d 3)]+ , ieffects = [toOrganGameTurn "defenseless" (3 + d 3)] , ifeature = [ toVelocity 13 -- the slowest that travels at least 2 steps , Fragile, Identified ] , idesc = ""@@ -447,7 +482,7 @@ , iverbHit = "inebriate" , iweight = 1 , iaspects = []- , ieffects = [toOrganActorTurn "drunk" (3 + d 3)]+ , ieffects = [Impress, toOrganActorTurn "drunk" (3 + d 3)] , ifeature = [ toVelocity 13 -- the slowest that travels at least 2 steps , Fragile, Identified ] , idesc = ""
GameDefinition/Content/ItemKindOrgan.hs view
@@ -1,6 +1,9 @@ -- | Organ definitions. module Content.ItemKindOrgan ( organs ) where +import qualified Data.EnumMap.Strict as EM++import Game.LambdaHack.Common.Ability import Game.LambdaHack.Common.Color import Game.LambdaHack.Common.Dice import Game.LambdaHack.Common.Flavour@@ -10,9 +13,9 @@ organs :: [ItemKind] organs =- [fist, foot, claw, smallClaw, snout, jaw, largeJaw, tooth, tentacle, lash, noseTip, lip, thorn, fissure, sting, venomTooth, venomFang, largeTail, pupil, armoredSkin, eye2, eye3, eye4, eye5, nostril, speedGland2, speedGland4, speedGland6, speedGland8, speedGland10, vent, bonusHP]+ [fist, foot, claw, smallClaw, snout, smallJaw, jaw, largeJaw, tooth, horn, tentacle, lash, noseTip, lip, torsionRight, torsionLeft, thorn, boilingFissure, arsenicFissure, sulfurFissure, beeSting, sting, venomTooth, venomFang, screechingBeak, largeTail, pupil, armoredSkin, eye2, eye3, eye4, eye5, eye6, eye7, eye8, vision4, vision6, vision8, vision10, vision12, vision14, vision16, nostril, insectMortality, sapientBrain, animalBrain, speedGland2, speedGland4, speedGland6, speedGland8, speedGland10, scentGland, boilingVent, arsenicVent, sulfurVent, bonusHP] -fist, foot, claw, smallClaw, snout, jaw, largeJaw, tooth, tentacle, lash, noseTip, lip, thorn, fissure, sting, venomTooth, venomFang, largeTail, pupil, armoredSkin, eye2, eye3, eye4, eye5, nostril, speedGland2, speedGland4, speedGland6, speedGland8, speedGland10, vent, bonusHP :: ItemKind+fist, foot, claw, smallClaw, snout, smallJaw, jaw, largeJaw, tooth, horn, tentacle, lash, noseTip, lip, torsionRight, torsionLeft, thorn, boilingFissure, arsenicFissure, sulfurFissure, beeSting, sting, venomTooth, venomFang, screechingBeak, largeTail, pupil, armoredSkin, eye2, eye3, eye4, eye5, eye6, eye7, eye8, vision4, vision6, vision8, vision10, vision12, vision14, vision16, nostril, insectMortality, sapientBrain, animalBrain, speedGland2, speedGland4, speedGland6, speedGland8, speedGland10, scentGland, boilingVent, arsenicVent, sulfurVent, bonusHP :: ItemKind -- Weapons @@ -29,7 +32,7 @@ , iweight = 2000 , iaspects = [] , ieffects = [Hurt (4 * d 1)]- , ifeature = [Durable, EqpSlot EqpSlotWeapon "", Identified]+ , ifeature = [Durable, Identified] , idesc = "" , ikit = [] }@@ -48,8 +51,9 @@ { iname = "claw" , ifreq = [("claw", 50)] , icount = 2 -- even if more, only the fore claws used for fighting- , iverbHit = "slash"- , ieffects = [Hurt (6 * d 1)]+ , iverbHit = "hook"+ , iaspects = [Timeout $ 4 + d 4]+ , ieffects = [Hurt (2 * d 1), Recharging (toOrganGameTurn "slow 10" 2)] , idesc = "" } smallClaw = fist@@ -57,7 +61,7 @@ , ifreq = [("small claw", 50)] , icount = 2 , iverbHit = "slash"- , ieffects = [Hurt (3 * d 1)]+ , ieffects = [Hurt (2 * d 1)] , idesc = "" } snout = fist@@ -67,12 +71,20 @@ , ieffects = [Hurt (2 * d 1)] , idesc = "" }+smallJaw = fist+ { iname = "small jaw"+ , ifreq = [("small jaw", 20)]+ , icount = 1+ , iverbHit = "rip"+ , ieffects = [Hurt (3 * d 1)]+ , idesc = ""+ } jaw = fist { iname = "jaw" , ifreq = [("jaw", 20)] , icount = 1 , iverbHit = "rip"- , ieffects = [Hurt (4 * d 1)]+ , ieffects = [Hurt (5 * d 1)] , idesc = "" } largeJaw = fist@@ -91,6 +103,14 @@ , ieffects = [Hurt (2 * d 1)] , idesc = "" }+horn = fist+ { iname = "horn"+ , ifreq = [("horn", 20)]+ , icount = 2+ , iverbHit = "impale"+ , ieffects = [Hurt (8 * d 1)]+ , idesc = ""+ } -- * Monster weapon organs @@ -107,7 +127,8 @@ , ifreq = [("lash", 100)] , icount = 1 , iverbHit = "lash"- , ieffects = [Hurt (3 * d 1), DropItem COrgan "far-sighted" True]+ , iaspects = [Timeout $ 3 + d 3]+ , ieffects = [Hurt (3 * d 1), Recharging $ DropItem COrgan "far-sighted" True] , idesc = "" } noseTip = fist@@ -123,37 +144,82 @@ , ifreq = [("lip", 10)] , icount = 2 , iverbHit = "lap"- , ieffects = [Hurt (2 * d 1), DropItem COrgan "keen-smelling" True] -- TODO: decrease Hurt, but use+ , iaspects = [Timeout $ 3 + d 3]+ , ieffects = [ Hurt (1 * d 1)+ , Recharging $ DropItem COrgan "keen-smelling" True ] , idesc = "" }+torsionRight = fist+ { iname = "right torsion"+ , ifreq = [("right torsion", 100)]+ , icount = 1+ , iverbHit = "twist"+ , iaspects = [Timeout $ 5 + d 5]+ , ieffects = [ Hurt (17 * d 1)+ , Recharging (toOrganGameTurn "slow 10" (3 + d 3)) ]+ , idesc = ""+ }+torsionLeft = fist+ { iname = "left torsion"+ , ifreq = [("left torsion", 100)]+ , icount = 1+ , iverbHit = "twist"+ , iaspects = [Timeout $ 5 + d 5]+ , ieffects = [ Hurt (17 * d 1)+ , Recharging (toOrganGameTurn "weakened" (3 + d 3)) ]+ , idesc = ""+ } -- * Special weapon organs thorn = fist { iname = "thorn" , ifreq = [("thorn", 100)]- , icount = 7+ , icount = 2 + d 3 , iverbHit = "impale" , ieffects = [Hurt (2 * d 1)]- , ifeature = [EqpSlot EqpSlotWeapon "", Identified] -- not Durable+ , ifeature = [Identified] -- not Durable , idesc = "" }-fissure = fist+boilingFissure = fist { iname = "fissure"- , ifreq = [("fissure", 100)]- , icount = 2+ , ifreq = [("boiling fissure", 100)]+ , icount = 5 + d 5 , iverbHit = "hiss at"- , ieffects = [Burn 1]+ , ieffects = [Burn $ 1 * d 1]+ , ifeature = [Identified] -- not Durable , idesc = "" }+arsenicFissure = boilingFissure+ { iname = "fissure"+ , ifreq = [("arsenic fissure", 100)]+ , icount = 2 + d 2+ , ieffects = [Burn $ 1 * d 1, toOrganGameTurn "weakened" (2 + d 2)]+ }+sulfurFissure = boilingFissure+ { iname = "fissure"+ , ifreq = [("sulfur fissure", 100)]+ , icount = 2 + d 2+ , ieffects = [Burn $ 1 * d 1, RefillHP 6]+ }+beeSting = fist+ { iname = "bee sting"+ , ifreq = [("bee sting", 100)]+ , icount = 1+ , iverbHit = "sting"+ , iaspects = [AddArmorMelee 90, AddArmorRanged 90]+ , ieffects = [Burn $ 2 * d 1, Paralyze 10, RefillHP 5]+ , ifeature = [Identified] -- not Durable+ , idesc = "Painful, but beneficial."+ } sting = fist { iname = "sting" , ifreq = [("sting", 100)] , icount = 1 , iverbHit = "sting" , iaspects = [Timeout $ 1 + d 5]- , ieffects = [Burn 1, Recharging (Paralyze 3)]- , idesc = ""+ , ieffects = [Burn $ 1 * d 1, Recharging (Paralyze 3)]+ , idesc = "Painful, debilitating and harmful." } venomTooth = fist { iname = "venom tooth"@@ -161,7 +227,7 @@ , icount = 2 , iverbHit = "bite" , iaspects = [Timeout $ 5 + d 3]- , ieffects = [ Hurt (3 * d 1)+ , ieffects = [ Hurt (2 * d 1) , Recharging (toOrganGameTurn "slow 10" (3 + d 3)) ] , idesc = "" }@@ -173,10 +239,20 @@ , icount = 2 , iverbHit = "bite" , iaspects = [Timeout $ 7 + d 5]- , ieffects = [ Hurt (3 * d 1)+ , ieffects = [ Hurt (2 * d 1) , Recharging (toOrganNone "poisoned") ] , idesc = "" }+screechingBeak = armoredSkin+ { iname = "screeching beak"+ , ifreq = [("screeching beak", 100)]+ , icount = 1+ , iverbHit = "peck"+ , iaspects = [Timeout $ 5 + d 5]+ , ieffects = [ Recharging (Summon [("scavenger", 1)] $ 1 + dl 2)+ , Hurt (2 * d 1) ]+ , idesc = ""+ } largeTail = fist { iname = "large tail" , ifreq = [("large tail", 50)]@@ -191,8 +267,11 @@ , ifreq = [("pupil", 100)] , icount = 1 , iverbHit = "gaze at"- , iaspects = [AddSight 7, Timeout $ 5 + d 5]- , ieffects = [Hurt (3 * d 1), Recharging (DropItem COrgan "temporary conditions" True)] -- TODO: decrease Hurt, but use+ , iaspects = [AddSight 10, Timeout $ 5 + d 5]+ , ieffects = [ Hurt (1 * d 1)+ , Recharging (DropItem COrgan "temporary conditions" True)+ , Recharging $ RefillHP (-2)+ ] , idesc = "" } @@ -231,17 +310,63 @@ eye3 = eye 3 eye4 = eye 4 eye5 = eye 5+eye6 = eye 6+eye7 = eye 7+eye8 = eye 8+vision :: Int -> ItemKind+vision n = armoredSkin+ { iname = "vision"+ , ifreq = [(toGroupName $ "vision" <+> tshow n, 100)]+ , icount = 1+ , iverbHit = "visualize"+ , iaspects = [AddSight (intToDice n)]+ , idesc = ""+ }+vision4 = vision 4+vision6 = vision 6+vision8 = vision 8+vision10 = vision 10+vision12 = vision 12+vision14 = vision 14+vision16 = vision 16 nostril = armoredSkin { iname = "nostril" , ifreq = [("nostril", 100)] , icount = 2 , iverbHit = "snuff"- , iaspects = [AddSmell 1]+ , iaspects = [AddSmell 2] , idesc = "" } -- * Assorted +insectMortality = fist+ { iname = "insect mortality"+ , ifreq = [("insect mortality", 100)]+ , icount = 1+ , iverbHit = "age"+ , iaspects = [Periodic, Timeout $ 40 + d 10]+ , ieffects = [Recharging (RefillHP (-1))]+ , idesc = ""+ }+sapientBrain = armoredSkin+ { iname = "sapient brain"+ , ifreq = [("sapient brain", 100)]+ , icount = 1+ , iverbHit = "outbrain"+ , iaspects = [AddSkills unitSkills]+ , idesc = ""+ }+animalBrain = armoredSkin+ { iname = "animal brain"+ , ifreq = [("animal brain", 100)]+ , icount = 1+ , iverbHit = "blank"+ , iaspects = [let absNo = [AbDisplace, AbMoveItem, AbProject, AbApply]+ sk = EM.fromList $ zip absNo [-1, -1..]+ in AddSkills $ addSkills unitSkills sk]+ , idesc = ""+ } speedGland :: Int -> ItemKind speedGland n = armoredSkin { iname = "speed gland"@@ -259,14 +384,41 @@ speedGland6 = speedGland 6 speedGland8 = speedGland 8 speedGland10 = speedGland 10-vent = armoredSkin+scentGland = armoredSkin -- TODO: cone attack, 3m away, project? apply?+ { iname = "scent gland"+ , ifreq = [("scent gland", 100)]+ , icount = 2+ , iverbHit = "spray at"+ , iaspects = [Periodic, Timeout $ 10 + d 2 |*| 5 ]+ , ieffects = [ Recharging (Explode "distressing odor")+ , Recharging ApplyPerfume ]+ , idesc = ""+ }+boilingVent = armoredSkin { iname = "vent"- , ifreq = [("vent", 100)]+ , ifreq = [("boiling vent", 100)]+ , iflavour = zipPlain [Blue] , icount = 1 , iverbHit = "menace"- , iaspects = [Periodic, Timeout $ (2 + d 4) |*| 5]+ , iaspects = [Periodic, Timeout $ 2 + d 2 |*| 5] , ieffects = [Recharging (Explode "boiling water")] , idesc = ""+ }+arsenicVent = boilingVent+ { iname = "vent"+ , ifreq = [("arsenic vent", 100)]+ , iflavour = zipPlain [Cyan]+ , iaspects = [Periodic, Timeout $ 2 + d 2 |*| 5]+ , ieffects = [ Recharging (Explode "weakness mist")+ , Recharging (RefillHP (-1)) ]+ }+sulfurVent = boilingVent+ { iname = "vent"+ , ifreq = [("sulfur vent", 100)]+ , iflavour = zipPlain [BrYellow]+ , iaspects = [Periodic, Timeout $ 2 + d 2 |*| 5]+ , ieffects = [ Recharging (Explode "strength mist")+ , Recharging (RefillHP (-1)) ] } bonusHP = armoredSkin { iname = "bonus HP"
GameDefinition/Content/ItemKindTemporary.hs view
@@ -12,22 +12,22 @@ temporaries :: [ItemKind] temporaries =- [tmpStrengthened, tmpWeakened, tmpProtected, tmpPaintedRed, tmpFast20, tmpSlow10, tmpFarSighted, tmpKeenSmelling, tmpDrunk, tmpRegenerating, tmpPoisoned, tmpSlow10Resistant, tmpPoisonResitant]+ [tmpStrengthened, tmpWeakened, tmpProtected, tmpVulnerable, tmpFast20, tmpSlow10, tmpFarSighted, tmpKeenSmelling, tmpDrunk, tmpRegenerating, tmpPoisoned, tmpSlow10Resistant, tmpPoisonResistant] -tmpStrengthened, tmpWeakened, tmpProtected, tmpPaintedRed, tmpFast20, tmpSlow10, tmpFarSighted, tmpKeenSmelling, tmpDrunk, tmpRegenerating, tmpPoisoned, tmpSlow10Resistant, tmpPoisonResitant :: ItemKind+tmpStrengthened, tmpWeakened, tmpProtected, tmpVulnerable, tmpFast20, tmpSlow10, tmpFarSighted, tmpKeenSmelling, tmpDrunk, tmpRegenerating, tmpPoisoned, tmpSlow10Resistant, tmpPoisonResistant :: ItemKind -- The @name@ is be used in item description, so it should be an adjective -- describing the temporary set of aspects. tmpAs :: Text -> [Aspect Dice] -> ItemKind tmpAs name aspects = ItemKind- { isymbol = '.'+ { isymbol = '+' , iname = name , ifreq = [(toGroupName name, 1), ("temporary conditions", 1)] , iflavour = zipPlain [BrWhite] , icount = 1 , irarity = [(1, 1)] , iverbHit = "affect"- , iweight = 1+ , iweight = 0 , iaspects = [Periodic, Timeout 0] -- activates and vanishes soon, -- depending on initial timer setting ++ aspects@@ -42,12 +42,12 @@ tmpWeakened = tmpAs "weakened" [AddHurtMelee (-20)] tmpProtected = tmpAs "protected" [ AddArmorMelee 30 , AddArmorRanged 30 ]-tmpPaintedRed = tmpAs "painted red" [ AddArmorMelee (-30)+tmpVulnerable = tmpAs "defenseless" [ AddArmorMelee (-30) , AddArmorRanged (-30) ] tmpFast20 = tmpAs "fast 20" [AddSpeed 20] tmpSlow10 = tmpAs "slow 10" [AddSpeed (-10)] tmpFarSighted = tmpAs "far-sighted" [AddSight 5]-tmpKeenSmelling = tmpAs "keen-smelling" [AddSmell 1]+tmpKeenSmelling = tmpAs "keen-smelling" [AddSmell 2] tmpDrunk = tmpAs "drunk" [ AddHurtMelee 30 -- fury , AddArmorMelee (-20) , AddArmorRanged (-20)@@ -56,20 +56,20 @@ tmpRegenerating = let tmp = tmpAs "regenerating" [] in tmp { icount = 7 + d 5- , ieffects = [Recharging (RefillHP 1)] ++ ieffects tmp+ , ieffects = Recharging (RefillHP 1) : ieffects tmp } tmpPoisoned = let tmp = tmpAs "poisoned" [] in tmp { icount = 7 + d 5- , ieffects = [Recharging (RefillHP (-1))] ++ ieffects tmp+ , ieffects = Recharging (RefillHP (-1)) : ieffects tmp } tmpSlow10Resistant = let tmp = tmpAs "slow resistant" [] in tmp { icount = 7 + d 5- , ieffects = [Recharging (DropItem COrgan "slow 10" True)] ++ ieffects tmp+ , ieffects = Recharging (DropItem COrgan "slow 10" True) : ieffects tmp }-tmpPoisonResitant =+tmpPoisonResistant = let tmp = tmpAs "poison resistant" [] in tmp { icount = 7 + d 5- , ieffects = [Recharging (DropItem COrgan "poisoned" True)] ++ ieffects tmp+ , ieffects = Recharging (DropItem COrgan "poisoned" True) : ieffects tmp }
GameDefinition/Content/ModeKind.hs view
@@ -5,6 +5,7 @@ import Content.ModeKindPlayer import Game.LambdaHack.Common.ContentDef+import Game.LambdaHack.Common.Dice import Game.LambdaHack.Common.Misc import Game.LambdaHack.Content.ModeKind @@ -16,12 +17,12 @@ , validateSingle = validateSingleModeKind , validateAll = validateAllModeKind , content =- [campaign, duel, skirmish, ambush, battle, safari, pvp, coop, defense, screensaver]+ [campaign, duel, skirmish, ambush, battle, battleSurvival, safari, safariSurvival, pvp, coop, defense, screensaver, boardgame] }-campaign, duel, skirmish, ambush, battle, safari, pvp, coop, defense, screensaver :: ModeKind+campaign, duel, skirmish, ambush, battle, battleSurvival, safari, safariSurvival, pvp, coop, defense, screensaver, boardgame :: ModeKind campaign = ModeKind- { msymbol = 'a'+ { msymbol = 'n' , mname = "campaign" , mfreq = [("campaign", 1)] , mroster = rosterCampaign@@ -65,6 +66,15 @@ , mdesc = "Odds are stacked against those that unleash the horrors of abstraction." } +battleSurvival = ModeKind+ { msymbol = 'i'+ , mname = "battle survival"+ , mfreq = [("battle survival", 1)]+ , mroster = rosterBattleSurvival+ , mcaves = cavesBattle+ , mdesc = "Odds are stacked for those that breathe mathematics."+ }+ safari = ModeKind { msymbol = 'f' , mname = "safari"@@ -74,6 +84,15 @@ , mdesc = "In this simulation you'll discover the joys of hunting the most exquisite of Earth's flora and fauna, both animal and semi-intelligent (exit at the bottommost level)." } +safariSurvival = ModeKind+ { msymbol = 'n'+ , mname = "safari survival"+ , mfreq = [("safari survival", 1)]+ , mroster = rosterSafariSurvival+ , mcaves = cavesSafari+ , mdesc = "In this simulation you'll discover the joys of being hunted among the most exquisite of Earth's flora and fauna, both animal and semi-intelligent."+ }+ pvp = ModeKind { msymbol = 'v' , mname = "PvP"@@ -106,14 +125,24 @@ , mfreq = [("screensaver", 1), ("starting", 1)] , mroster = rosterSafari { rosterList = (head (rosterList rosterSafari))- {fleaderMode = LeaderAI $ AutoLeader False False }+ -- changing leader by client needed, because of TFollow+ {fleaderMode = LeaderAI $ AutoLeader True False} : tail (rosterList rosterSafari) } } +boardgame = ModeKind+ { msymbol = 'g'+ , mname = "boardgame"+ , mfreq = [("boardgame", 1)]+ , mroster = rosterBoardgame+ , mcaves = cavesBoardgame+ , mdesc = "Small room, no exits. Who will prevail?"+ } -rosterCampaign, rosterDuel, rosterSkirmish, rosterAmbush, rosterBattle, rosterSafari, rosterPvP, rosterCoop, rosterDefense :: Roster +rosterCampaign, rosterDuel, rosterSkirmish, rosterAmbush, rosterBattle, rosterBattleSurvival, rosterSafari, rosterSafariSurvival, rosterPvP, rosterCoop, rosterDefense, rosterBoardgame:: Roster+ rosterCampaign = Roster { rosterList = [ playerHero , playerMonster@@ -124,9 +153,11 @@ rosterDuel = Roster { rosterList = [ playerHero { fname = "White Recursive"+ , fhiCondPoly = hiDweller , fentryLevel = -3 , finitialActors = 1 } , playerAntiHero { fname = "Red Iterative"+ , fhiCondPoly = hiDweller , fentryLevel = -3 , finitialActors = 1 } , playerHorror ]@@ -137,8 +168,10 @@ rosterSkirmish = rosterDuel { rosterList = [ playerHero { fname = "White Haskell"+ , fhiCondPoly = hiDweller , fentryLevel = -3 } , playerAntiHero { fname = "Purple Agda"+ , fhiCondPoly = hiDweller , fentryLevel = -3 } , playerHorror ] , rosterEnemy = [ ("White Haskell", "Purple Agda")@@ -146,51 +179,85 @@ , ("Purple Agda", "Horror Den") ] } rosterAmbush = rosterDuel- { rosterList = [ playerHero { fname = "Yellow Idris"- , fentryLevel = -5 }- , playerAntiHero { fname = "Blue Epigram"- , fentryLevel = -5 }+ { rosterList = [ playerSniper { fname = "Yellow Idris"+ , fhiCondPoly = hiDweller+ , fentryLevel = -5+ , finitialActors = 4 }+ , playerAntiSniper { fname = "Blue Epigram"+ , fhiCondPoly = hiDweller+ , fentryLevel = -5+ , finitialActors = 4 } , playerHorror {fentryLevel = -5} ] , rosterEnemy = [ ("Yellow Idris", "Blue Epigram") , ("Yellow Idris", "Horror Den") , ("Blue Epigram", "Horror Den") ] } rosterBattle = Roster- { rosterList = [ playerSoldier { finitialActors = 5- , fentryLevel = -5 }- , playerMobileMonster { finitialActors = 35- , fentryLevel = -5+ { rosterList = [ playerSoldier { fhiCondPoly = hiDweller+ , fentryLevel = -5+ , finitialActors = 5 }+ , playerMobileMonster { fentryLevel = -5+ , finitialActors = 35 , fneverEmpty = True }- , playerMobileAnimal { finitialActors = 30- , fentryLevel = -5+ , playerMobileAnimal { fentryLevel = -5+ , finitialActors = 30 , fneverEmpty = True } ] , rosterEnemy = [ ("Armed Adventurer Party", "Monster Hive") , ("Armed Adventurer Party", "Animal Kingdom") ] , rosterAlly = [("Monster Hive", "Animal Kingdom")] } -rosterSafari = Roster- { rosterList = [ playerAntiMonster { fname = "Monster Tourist Office"- , fcanEscape = True- , fneverEmpty = True- -- Follow-the-guide, as tourists do.- , ftactic = TFollow- , fentryLevel = -4- , finitialActors = 15- , fleaderMode =- -- no spawning and TFollow- LeaderUI $ AutoLeader False False }- , playerCivilian { fname = "Hunam Convict Pack"- , fentryLevel = -4 }- , playerMobileAnimal { fname =- "Animal Magnificent Specimen Variety"- , fneverEmpty = True- , fentryLevel = -7- , finitialActors = 10 }- , playerMobileAnimal { fname =- "Animal Exquisite Herds and Packs"+rosterBattleSurvival = rosterBattle+ { rosterList = [ playerSoldier { fhiCondPoly = hiDweller+ , fentryLevel = -5+ , finitialActors = 5+ , fleaderMode =+ LeaderAI $ AutoLeader True False+ , fhasUI = False }+ , playerMobileMonster { fentryLevel = -5+ , finitialActors = 35+ , fneverEmpty = True }+ , playerMobileAnimal { fentryLevel = -5+ , finitialActors = 30 , fneverEmpty = True- , fentryLevel = -10- , finitialActors = 30 } ]+ , fhasUI = True } ] }++playerMonsterTourist, playerHunamConvict, playerAnimalMagnificent, playerAnimalExquisite :: Player Dice++playerMonsterTourist =+ playerAntiMonster { fname = "Monster Tourist Office"+ , fcanEscape = True+ , fneverEmpty = True -- no spawning+ -- Follow-the-guide, as tourists do.+ , ftactic = TFollow+ , fentryLevel = -4+ , finitialActors = 15+ , fleaderMode =+ LeaderUI $ AutoLeader False False }++playerHunamConvict =+ playerCivilian { fname = "Hunam Convict Pack"+ , fentryLevel = -4 }++playerAnimalMagnificent =+ playerMobileAnimal { fname = "Animal Magnificent Specimen Variety"+ , fneverEmpty = True+ , fentryLevel = -7+ , finitialActors = 10+ , fleaderMode = -- move away from stairs+ LeaderAI $ AutoLeader True False }++playerAnimalExquisite =+ playerMobileAnimal { fname = "Animal Exquisite Herds and Packs"+ , fneverEmpty = True+ , fentryLevel = -10+ , finitialActors = 30 }++rosterSafari = Roster+ { rosterList = [ playerMonsterTourist+ , playerHunamConvict+ , playerAnimalMagnificent+ , playerAnimalExquisite+ ] , rosterEnemy = [ ("Monster Tourist Office", "Hunam Convict Pack") , ("Monster Tourist Office", "Animal Magnificent Specimen Variety")@@ -199,10 +266,23 @@ , rosterAlly = [( "Animal Magnificent Specimen Variety" , "Animal Exquisite Herds and Packs" )] } +rosterSafariSurvival = rosterSafari+ { rosterList = [ playerMonsterTourist+ { fleaderMode = LeaderAI $ AutoLeader True False+ , fhasUI = False }+ , playerHunamConvict+ , playerAnimalMagnificent+ { fleaderMode = LeaderUI $ AutoLeader False False+ , fhasUI = True }+ , playerAnimalExquisite+ ] }+ rosterPvP = Roster { rosterList = [ playerHero { fname = "Red"+ , fhiCondPoly = hiDweller , fentryLevel = -3 } , playerHero { fname = "Blue"+ , fhiCondPoly = hiDweller , fentryLevel = -3 } , playerHorror ] , rosterEnemy = [ ("Red", "Blue")@@ -230,18 +310,32 @@ , ("Green", "Leaderless Monster Hive") ] } rosterDefense = Roster- { rosterList = [ playerAntiMonster- , playerAntiHero {fname = "Yellow"}+ { rosterList = [ playerAntiHero+ , playerAntiMonster , playerAnimal ]- , rosterEnemy = [ ("Yellow", "Monster Hive")- , ("Yellow", "Animal Kingdom") ]+ , rosterEnemy = [ ("Adventurer Party", "Monster Hive")+ , ("Adventurer Party", "Animal Kingdom") ] , rosterAlly = [("Monster Hive", "Animal Kingdom")] } +rosterBoardgame = Roster+ { rosterList = [ playerHero { fname = "Blue"+ , fhiCondPoly = hiDweller+ , fentryLevel = -3+ , finitialActors = 6 }+ , playerAntiHero { fname = "Red"+ , fhiCondPoly = hiDweller+ , fentryLevel = -3+ , finitialActors = 6 }+ , playerHorror ]+ , rosterEnemy = [ ("Blue", "Red")+ , ("Blue", "Horror Den")+ , ("Red", "Horror Den") ]+ , rosterAlly = [] } -cavesCampaign, cavesSkirmish, cavesAmbush, cavesBattle, cavesSafari :: Caves+cavesCampaign, cavesSkirmish, cavesAmbush, cavesBattle, cavesSafari, cavesBoardgame :: Caves cavesCampaign = IM.fromList- $ [ (-1, ("caveRogue", Just True))+ $ [ (-1, ("shallow random 1", Just True)) , (-2, ("caveRogue", Nothing)) , (-3, ("caveEmpty", Nothing)) ] ++ zip [-4, -5..(-9)] (repeat ("campaign random", Nothing))@@ -256,3 +350,5 @@ cavesSafari = IM.fromList [ (-4, ("caveSafari1", Nothing)) , (-7, ("caveSafari2", Nothing)) , (-10, ("caveSafari3", Just False)) ]++cavesBoardgame = IM.fromList [(-3, ("caveBoardgame", Nothing))]
GameDefinition/Content/ModeKindPlayer.hs view
@@ -1,18 +1,22 @@ -- | Basic players definitions. module Content.ModeKindPlayer- ( playerHero, playerSoldier, playerAntiHero, playerCivilian, playerMonster- , playerMobileMonster, playerAntiMonster, playerAnimal, playerMobileAnimal+ ( playerHero, playerSoldier, playerSniper+ , playerAntiHero, playerAntiSniper, playerCivilian+ , playerMonster, playerMobileMonster, playerAntiMonster+ , playerAnimal, playerMobileAnimal , playerHorror+ , hiHero, hiDweller ) where import qualified Data.EnumMap.Strict as EM+import Data.List import Game.LambdaHack.Common.Ability import Game.LambdaHack.Common.Dice import Game.LambdaHack.Common.Misc import Game.LambdaHack.Content.ModeKind -playerHero, playerSoldier, playerAntiHero, playerCivilian, playerMonster, playerMobileMonster, playerAntiMonster, playerAnimal, playerMobileAnimal, playerHorror :: Player Dice+playerHero, playerSoldier, playerSniper, playerAntiHero, playerAntiSniper, playerCivilian, playerMonster, playerMobileMonster, playerAntiMonster, playerAnimal, playerMobileAnimal, playerHorror :: Player Dice playerHero = Player { fname = "Adventurer Party"@@ -20,6 +24,7 @@ , fskillsOther = meleeAdjacent , fcanEscape = True , fneverEmpty = True+ , fhiCondPoly = hiHero , fhasNumbers = True , fhasGender = True , ftactic = TExplore@@ -34,17 +39,28 @@ , fgroup = "soldier" } +playerSniper = playerHero+ { fname = "Sniper Adventurer Party"+ , fgroup = "sniper"+ }+ playerAntiHero = playerHero { fleaderMode = LeaderAI $ AutoLeader True False , fhasUI = False } +playerAntiSniper = playerSniper+ { fleaderMode = LeaderAI $ AutoLeader True False+ , fhasUI = False+ }+ playerCivilian = Player { fname = "Civilian Crowd" , fgroup = "civilian"- , fskillsOther = unitSkills -- not coordinated by any leadership+ , fskillsOther = zeroSkills -- not coordinated by any leadership , fcanEscape = False , fneverEmpty = True+ , fhiCondPoly = hiDweller , fhasNumbers = False , fhasGender = True , ftactic = TPatrol@@ -57,15 +73,19 @@ playerMonster = Player { fname = "Monster Hive" , fgroup = "monster"- , fskillsOther = unitSkills+ , fskillsOther = zeroSkills , fcanEscape = False , fneverEmpty = False+ , fhiCondPoly = hiDweller , fhasNumbers = False , fhasGender = False , ftactic = TExplore- , fentryLevel = -3- , finitialActors = 1 -- an explorer, not a nose, ensured via irarity- , fleaderMode = LeaderAI $ AutoLeader True True+ , fentryLevel = -4+ , finitialActors = 4 -- one of these most probably not nose, so will explore+ , fleaderMode =+ -- No point changing leader on level, since all move and they+ -- don't follow the leader.+ LeaderAI $ AutoLeader True True , fhasUI = False } @@ -79,13 +99,14 @@ playerAnimal = Player { fname = "Animal Kingdom" , fgroup = "animal"- , fskillsOther = unitSkills+ , fskillsOther = zeroSkills , fcanEscape = False , fneverEmpty = False+ , fhiCondPoly = hiDweller , fhasNumbers = False , fhasGender = False , ftactic = TRoam -- can't pick up, so no point exploring- , fentryLevel = -1 -- fun from the start; don't cumulate with monsters+ , fentryLevel = -1 -- fun from the start to avoid empty initial level , finitialActors = 1 + d 2 , fleaderMode = LeaderNull , fhasUI = False@@ -103,9 +124,10 @@ playerHorror = Player { fname = "Horror Den" , fgroup = "horror"- , fskillsOther = unitSkills+ , fskillsOther = zeroSkills , fcanEscape = False , fneverEmpty = False+ , fhiCondPoly = [] , fhasNumbers = False , fhasGender = False , ftactic = TPatrol -- disoriented@@ -115,14 +137,36 @@ , fhasUI = False } -minusHundred, meleeAdjacent, _meleeAndRanged :: Skills+victoryOutcomes :: [Outcome]+victoryOutcomes = [Conquer, Escape] --- To make sure weak items can't override move-only-leader, etc.-minusHundred = EM.fromList $ zip [minBound..maxBound] [-100, -100..]+hiHero, hiDweller :: HiCondPoly -meleeAdjacent = addSkills minusHundred- $ EM.fromList $ zip [AbWait, AbMelee] [101, 101..]+-- Heroes rejoice in loot.+hiHero = [ ( [(HiLoot, 1)]+ , [minBound..maxBound] )+ , ( [(HiConst, 1000), (HiLoss, -100)]+ , victoryOutcomes )+ ] +-- Spawners or skirmishers get no points from loot, but try to kill+-- all opponents fast or at least hold up for long.+hiDweller = [ ( [(HiConst, 1000)] -- no loot+ , victoryOutcomes )+ , ( [(HiConst, 1000), (HiLoss, -10)]+ , victoryOutcomes )+ , ( [(HiBlitz, -100)]+ , victoryOutcomes )+ , ( [(HiSurvival, 100)]+ , [minBound..maxBound] \\ victoryOutcomes )+ ]++minusTen, meleeAdjacent, _meleeAndRanged :: Skills++-- To make sure only a lot of weak items can override move-only-leader, etc.+minusTen = EM.fromList $ zip [minBound..maxBound] [-10, -10..]++meleeAdjacent = EM.delete AbWait $ EM.delete AbMelee minusTen+ -- Melee and reaction fire.-_meleeAndRanged = addSkills minusHundred- $ EM.fromList $ zip [AbWait, AbMelee, AbProject] [101, 101..]+_meleeAndRanged = EM.delete AbProject $ meleeAdjacent
GameDefinition/Content/PlaceKind.hs view
@@ -12,9 +12,9 @@ , validateSingle = validateSinglePlaceKind , validateAll = validateAllPlaceKind , content =- [rect, ruin, collapsed, collapsed2, collapsed3, collapsed4, pillar, pillar2, pillar3, pillar4, colonnade, colonnade2, colonnade3, colonnade4, colonnade5, colonnade6, lampPost, lampPost2, lampPost3, lampPost4, treeShade, treeShade2, treeShade3]+ [rect, ruin, collapsed, collapsed2, collapsed3, collapsed4, pillar, pillar2, pillar3, pillar4, colonnade, colonnade2, colonnade3, colonnade4, colonnade5, colonnade6, lampPost, lampPost2, lampPost3, lampPost4, treeShade, treeShade2, treeShade3, boardgame] }-rect, ruin, collapsed, collapsed2, collapsed3, collapsed4, pillar, pillar2, pillar3, pillar4, colonnade, colonnade2, colonnade3, colonnade4, colonnade5, colonnade6, lampPost, lampPost2, lampPost3, lampPost4, treeShade, treeShade2, treeShade3 :: PlaceKind+rect, ruin, collapsed, collapsed2, collapsed3, collapsed4, pillar, pillar2, pillar3, pillar4, colonnade, colonnade2, colonnade3, colonnade4, colonnade5, colonnade6, lampPost, lampPost2, lampPost3, lampPost4, treeShade, treeShade2, treeShade3, boardgame :: PlaceKind rect = PlaceKind -- Valid for any nonempty area, hence low frequency. { psymbol = 'r'@@ -214,4 +214,24 @@ , "sOs" , "XXs" ]+ }+boardgame = PlaceKind+ { psymbol = 'b'+ , pname = "boardgame"+ , pfreq = [("boardgame", 1)]+ , prarity = [(1, 1)]+ , pcover = CVerbatim+ , pfence = FNone+ , ptopLeft = [ "----------"+ , "|.b.b.b.b|"+ , "|b.b.b.b.|"+ , "|.b.b.b.b|"+ , "|b.b.b.b.|"+ , "|.b.b.b.b|"+ , "|b.b.b.b.|"+ , "|.b.b.b.b|"+ , "|b.b.b.b.|"+ , "----------"+ ]+ , poverride = [('b', "trailChessLit")] }
GameDefinition/Content/RuleKind.hs view
@@ -72,6 +72,5 @@ , rleadLevelClips = 100 , rscoresFile = "scores" , rsavePrefix = "save"- , rsharedStash = True- , rnearby = 10+ , rnearby = 20 }
GameDefinition/Content/TileKind.hs view
@@ -207,7 +207,7 @@ } escapeUpLit = TileKind { tsymbol = '<'- , tname = "exit trapdoor up"+ , tname = "exit hatch up" , tfreq = [("legendLit", 100)] , tcolor = BrYellow , tcolor2 = BrYellow@@ -265,14 +265,14 @@ } floorRedLit = floorArenaLit { tname = "brick pavement"- , tfreq = [("trailLit", 30)]+ , tfreq = [("trailLit", 30), ("trailChessLit", 30)] , tcolor = BrRed , tcolor2 = Red , tfeature = Trail : tfeature floorArenaLit } floorBlueLit = floorRedLit- { tname = "granite cobblestones"- , tfreq = [("trailLit", 100)]+ { tname = "cobblestone path"+ , tfreq = [("trailLit", 100), ("trailChessLit", 70)] , tcolor = BrBlue , tcolor2 = Blue }@@ -300,7 +300,7 @@ darkFeat (HideAs t) = Just $ HideAs $ darkText t darkFeat (RevealAs t) = Just $ RevealAs $ darkText t darkFeat OftenItem = Nothing -- items not common in the dark- darkFeat feat = Just $ feat+ darkFeat feat = Just feat in k { tfreq = darkFrequency , tfeature = Dark : mapMaybe darkFeat (tfeature k) }
GameDefinition/PLAYING.md view
@@ -13,8 +13,8 @@ Once the few basic command keys and on-screen symbols are learned, mastery and enjoyment of the game is the matter of tactical skill and literary imagination. To be honest, a lot of imagination is required-for this rudimentary game, even though it is playable and winnable.-Contributions are welcome.+for this rudimentary set of scenarios, even though they are playable+and winnable. Contributions are welcome. Heroes@@ -30,36 +30,40 @@ *@12 Adventurer 4d1+5% Calm: 20/60 HP: 33/50 Target: basilisk [**___] -The line starts with the list of party members (unless only one member-resides on the currently displayed level) and the shortened name of the team.-Then comes the damage of the leader's weapon (but regardless of the figures,-each attack inflicts at least 1 damage), then his current and maximum-Calm (composure, focus, attentiveness), then his current and maximum-HP (hit points, health). At the end, the personal target of the leader-is described, in this case a basilisk monster, with hit points drawn as a bar.+The line starts with the list of party members (unless there's only one member)+and the shortened name of the team. Clicking on the list selects heroes and+the selected run together when `:` or `SHIFT`-left mouse button is pressed. -Weapon damage and other item stats are displayed using the dice notation `XdY`.-which means X rolls of Y-sided dice. A variant denoted 'XdsY' is additionally+Then comes the damage of the highest damage dice weapon the leader can use,+then his current and maximum Calm (composure, focus, attentiveness), then+his current and maximum HP (hit points, health). At the end, the personal+target of the leader is described, in this case a basilisk monster,+with hit points drawn as a bar. Weapon damage and other item stats+are displayed using the dice notation `XdY`, which means X rolls+of Y-sided dice. A variant denoted `XdsY` is additionally scaled by the level depth in proportion to the maximal dungeon depth.+You can read more about combat resolution in section Monsters below. -The second status line describes the current dungeon location in relation+The second status line describes the current level in relation to the party. - 5 Lofty hall [33% seen] Cursor: exact spot (71,12) p15 l10+ 5 Lofty hall [33% seen] X-hair: exact spot (71,12) p15 l10 First comes the depth of the current level and its name. Then the percentage of its explorable tiles already seen by the heroes.-The 'cursor' is the common target of the whole party,-directly manipulated with movement keys in the targeting mode.-At the end comes the length of the shortest path from the leader-to the cursor position and the straight-line distance between the two points.+The 'X-hair' (meaning 'crosshair') is the common focus of the whole party,+denoted on the map by a white box and manipulated with movement keys+in aiming mode. At the end of the status line comes the length of the shortest+path from the leader to the crosshair position and the straight-line distance+between the two points. Dungeon ------- -The dungeon of the campaign mode game consists of 10 levels and each level-consists of a large number of tiles. The basic tile kinds are as follows.+The dungeon of any particular scenario may consists of one or many+levels and each level consists of a large number of tiles.+The basic tile kinds are as follows. dungeon terrain type on-screen symbol ground .@@ -80,10 +84,10 @@ Commands -------- -You move throughout the level using the numerical keypad (left diagram)-or its compact laptop replacement (middle) or Vi text editor keys-(right, also known as "Rogue-like keys", which have to be enabled-in config.ui.ini).+You walk throughout a level using the left mouse button or the numeric+keypad (left diagram) or its compact laptop replacement (middle)+or Vi text editor keys (right, also known as "Rogue-like keys",+which have to be enabled in config.ui.ini). 7 8 9 7 8 9 y k u \|/ \|/ \|/@@ -91,125 +95,81 @@ /|\ /|\ /|\ 1 2 3 j k l b j n -In targeting mode the keys above move the targeting cursor. In normal mode,-`SHIFT` (or `CTRL`) and a movement key make the current party leader-(and currently selected party members, if any) run in the indicated-direction, until anything of interest is spotted.-The '5', 'i' and '.' keys consume a turn and make you brace for combat,+In aiming mode the same keys (or the middle and right mouse buttons)+move the crosshair (the white box). In normal mode, `SHIFT` (or `CTRL`)+and a movement key make the current party leader run in the indicated+direction, until anything of interest is spotted. The `5` keypad key+and the `i` and `.` keys consume a turn and make you brace for combat, which reduces any damage taken for a turn and makes it impossible for foes to displace you. You displace enemies or friends by bumping-into them with SHIFT (or CTRL).+into them with `SHIFT` (or `CTRL`). Melee, searching for secret doors, looting and opening closed doors can be done by bumping into a monster, a wall and a door, respectively. Few commands other than movement, 'g'etting an item from the floor, 'a'pplying an item and 'f'linging an item are necessary for casual play.-Some are provided only as specialized versions of more general commands-or as building blocks for more complex convenience commands,+Some are provided only as specialized versions of the more general+commands or as building blocks for more complex convenience macros, e.g., the autoexplore command (key `X`) could be defined-by the player as a macro using `BACKSPACE`, `CTRL-?`, `;` and `V`.--Below are the remaining keys for terrain exploration and alteration.-- keys command- < ascend a level- CTRL-< ascend 10 levels- > descend a level- CTRL-> descend 10 levels- ; make one step towards the target- : go to target for 100 steps- CTRL-: go to target for 10 steps- x explore the closest unknown spot- X autoexplore 100 times- CTRL-X autoexplore 10 times- R rest (wait 100 times)- CTRL-R rest (wait 10 times)- c close door--Item-use related keys are as follows.-- keys command- E describe equipment of the leader- P describe backpack inventory of the leader- S describe the shared party stash- G describe items on the ground- A describe all owned items- g and , get an item- d drop an item- e equip an item- p pack an item into inventory backpack- s stash and share an item- a activate applicable item- q quaff potion- r read scroll- f fling projectable item- t throw missile- z zap wand--To make a ranged attack, as in the last few commands above,-you need to set your target first (however, initial target is set-automatically as soon as a monster comes into view). Once in targeting mode,-you can move the targeting cursor with arrow keys and switch focus-among enemies with `*` (or among friends, projectiles and enemies, depending-on targeting mode toggled by `/`). The details of the shared cursor position-and of the personal target are described in the status lines, at the bottom-of the screen. All targeting keys are listed below.+by the player as a macro using `CTRL-?`, `CTRL-.` and `V`. - keys command- KEYPAD_* and \ target enemy- / cycle targeting mode- + swerve targeting line- - unswerve targeting line- BACKSPACE clear target/cursor- CTRL-? target the closest unknown spot- CTRL-I target the closest item- CTRL-{ target the closest stairs up- CTRL-} target the closest stairs down+The following minimal command set lets you accomplish almost anything+in the game, though not necessarily with the fewest number of keystrokes.+The full list of commands can be seen in the in-game help accessible+from the Main Menu. -Here are the commands for automating the actions of one or more members-of the team.+ keys command+ < ascend a level+ > descend a level+ c close door+ E manage equipment of the leader+ g and , get items+ a apply consumable+ f fling projectile+ + swerve the aiming line+ D display player diary+ T toggle suspect terrain display+ SHIFT-TAB cycle among all party members+ ESC cancel action, open Main Menu - keys command- = select (or deselect) a party member- _ deselect (or select) all on the level- v voice again the recorded commands- V voice the recorded commands 100 times- CTRL-v voice the recorded commands 1000 times- CTRL-V voice the recorded commands 10 times- ' start recording commands- CTRL-T cycle tactic of non-leader team members (WIP)- CTRL-A automate faction (ESC to retake control)+The only activity not possible with the commands above is the management+of non-leader party members. The defaults should usually suffice,+especially if your non-leader heroes can only melee or wait+and none have found the equipment that enables opportunity fire.+If there's a need, you can manually set party tactics with `CTRL-T`+and you can assign individual targets to party members+using the aiming and targeting commands listed below. -Assorted remaining keys and commands follow.+ keys command+ KEYPAD_* and \ aim at an enemy+ KEYPAD_/ and | cycle aiming styles+ + swerve the aiming line+ - unswerve the aiming line+ CTRL-? set crosshair to the closest unknown spot+ CTRL-I set crosshair to the closest item+ CTRL-{ set crosshair to the closest stairs up+ CTRL-} set crosshair to the closest stairs down+ BACKSPACE reset target/crosshair+ RET and INSERT accept target/choice - keys command- ? display help- D display player diary- T mark suspect terrain- Z mark visible zone- C mark smell clues- TAB cycle among party members on the level- SHIFT-TAB cycle among all party members- SPACE clear messages- ESC cancel action, open Main Menu- RET accept choice- 0--6 pick a new hero leader anywhere in the dungeon+For ranged attacks, setting the crosshair or individual targets+beforehand is not mandatory, because the crosshair is set automatically+as soon as a monster comes into view and can still be adjusted while+in the missile choice menu. However, if you want to assign persistent+personal targets or just inspect the level map closely, you can enter+the detailed aiming mode with the right mouse button or with+the `*` keypad key that selects enemies or the `/` keypad key that+marks a tile. You can move the aiming crosshair with direction keys+and assign a personal target to the leader with `RET`.+The details of the shared crosshair position and of the personal target+are described in the status lines at the bottom of the screen. Commands for saving and exiting the current game, starting a new game, etc., are listed in the Main Menu, brought up by the `ESC` key. Game difficulty setting affects hitpoints at birth for any actors-of any UI-using faction. For a person new to roguelikes, the Duel game mode+of any UI-using faction. For a person new to roguelikes, the Duel scenario offers a gentle introduction. The subsequent game modes gradually introduce-squad combat, stealth, asymmetric battles and more game elements.-- keys command- CTRL-x save and exit- CTRL-u new Duel game- CTRL-k new Skirmish game- CTRL-m new Ambush game- CTRL-b new Battle game- CTRL-a new Campaign game- CTRL-d cycle next game difficulty+squad combat, stealth, opportunity fire, asymmetric battles and more. Monsters@@ -222,36 +182,50 @@ and all move at once, sometimes brutally colliding by accident. When the hero bumps into a monster or a monster attacks the hero,-melee combat occurs. The best equipped weapon or the best fighting organ-of each opponent is taken into account for calculating damage.-The damage the current hero can potentially inflict is displayed-at the bottom of the screen, but the actual damage depends also-on the monster's armor. Heroes and monsters running into one another+melee combat occurs. Heroes and monsters running into one another (with the `SHIFT` key) do not inflict damage, but change places. This gives the opponent a free blow, but can improve the tactical situation-or aid escape.+or aid escape. In some circumstances actors are immune to the displacing,+e.g., when both parties form a continuous front-line. -Flinging a missile at a target wounds it, consuming the weapon in the process.-You may propel any item in your equipment, inventory and on the ground-(press `?` to see appropriate items in a menu and press it again-for a non-standard choice). Only items of a few kinds inflict any damage,-but some have other effects. Whenever the monster's or hero's hit points-reach zero, the combatant dies. When the last hero dies, the game ends-in defeat.+In melee combat, the best equipped weapon (or the best fighting organ)+of each opponent is taken into account for determining the damage+and any extra effects of the blow. If a recharged weapon with a non-trivial+effect is in the equipment, it is preferred for combat. Otherwise combat+involves the weapon with the highest raw damage dice (the same as displayed+at bottommost status line). +To determine the damage dealt, the outcome of the weapon's damage dice roll+is multiplied by the melee damage bonus (summed from the equipped items+of the attacker) minus the melee armor modifier of the defender.+Regardless of the calculation, each attack inflicts at least 1 damage.+The current leader's melee bonus, armor modifier and other detailed+stats can be viewed via the `!` command. +In ranged combat, the missile is assumed to be attacking the defender+in melee, using itself as the weapon, but the ranged damage bonus+and the ranged armor modifier are taken into account for calculations.+You may propel any item in your equipment, inventory pack and on the ground+(by default you are offered only the appropriate items; press `?`+to cycle item menu modes). Only items of a few kinds inflict any damage,+but some have other effects, beneficial, detrimental or mixed.++Whenever the monster's or hero's hit points reach zero, the combatant dies.+When the last hero dies, the scenario ends in defeat.++ On Winning and Dying -------------------- -You win the game if you escape the dungeon alive or, in game modes with-no exit opportunity, if you eliminate all opposition. In the former case,-your score is based on the gold and precious gems you've plundered,-plus a bonus based on the number of heroes you lost. In the latter case,-your score is based on the number of turns you spent overcoming your foes-and, as a bonus, the number of enemies you've subdued.+You win the scenario if you escape the dungeon alive or, in scenarios with+no exit locations, if you eliminate all opposition. In the former case,+your score is based on the gold and precious gems you've plundered.+In the latter case, your score is based on the number of turns you spent+overcoming your foes (the quicker the victory, the better; the slower+the demise, the better). Bonus points, based on the number of heroes+that were lost, are awarded if you win. -If all your heroes fall, you are awarded a score for your valiant deeds,-but no winning bonus. When, invariably, a new foolhardy party-of adventurers storms the dungeon, they start from a new entrance,-with no experience and no equipment, and new, undaunted enemies-bar their way.+If all your heroes fall, you will invariably see a new foolhardy party+of adventurers clamoring to be led into the dungeon. They start+their conquest from a new entrance, with no experience and no equipment,+and new, undaunted enemies bar their way. Lead them with fortitude!
GameDefinition/TieKnot.hs view
@@ -9,28 +9,40 @@ import qualified Content.PlaceKind import qualified Content.RuleKind import qualified Content.TileKind-import Game.LambdaHack.Client (exeFrontend)+import Game.LambdaHack.Client import qualified Game.LambdaHack.Common.Kind as Kind import Game.LambdaHack.SampleImplementation.SampleMonadClient (executorCli) import Game.LambdaHack.SampleImplementation.SampleMonadServer (executorSer)-import Game.LambdaHack.Server (mainSer)+import Game.LambdaHack.Server -- | Tie the LambdaHack engine client, server and frontend code -- with the game-specific content definitions, and run the game. tieKnot :: [String] -> IO ()-tieKnot args =+tieKnot args = do let -- Common content operations, created from content definitions.- copsServer = Kind.COps- { cocave = Kind.createOps Content.CaveKind.cdefs- , coitem = Kind.createOps Content.ItemKind.cdefs- , comode = Kind.createOps Content.ModeKind.cdefs- , coplace = Kind.createOps Content.PlaceKind.cdefs- , corule = Kind.createOps Content.RuleKind.cdefs- , cotile = Kind.createOps Content.TileKind.cdefs+ -- Evaluated fully to discover errors ASAP and free memory.+ !copsSlow = Kind.COps+ { cocave = Kind.createOps Content.CaveKind.cdefs+ , coitem = Kind.createOps Content.ItemKind.cdefs+ , comode = Kind.createOps Content.ModeKind.cdefs+ , coplace = Kind.createOps Content.PlaceKind.cdefs+ , corule = Kind.createOps Content.RuleKind.cdefs+ , cotile = Kind.createOps Content.TileKind.cdefs }+ !copsShared = speedupCOps False copsSlow -- Client content operations. copsClient = Content.KeyKind.standardKeys- -- A single frontend is currently started by the server,- -- instead of each client starting it's own.- startupFrontend = exeFrontend executorCli executorCli copsClient- in mainSer args copsServer executorSer startupFrontend+ sdebugNxt <- debugArgs args+ -- Fire up the frontend with the engine fueled by content.+ -- The action monad types to be used are determined by the 'exeSer'+ -- and 'executorCli' calls. If other functions are used in their place+ -- the types are different and so the whole pattern of computation+ -- is different. Which of the frontends is run depends on the flags supplied+ -- when compiling the engine library.+ let exeServer executorUI executorAI =+ executorSer $ loopSer copsShared sdebugNxt executorUI executorAI+ -- Currently a single frontend is started by the server,+ -- instead of each client starting it's own.+ srtFrontend (executorCli . loopUI)+ (executorCli . loopAI)+ copsClient copsShared (sdebugCli sdebugNxt) exeServer
GameDefinition/config.ui.default view
@@ -1,40 +1,40 @@-; ; This is a commented out copy of the default UI settings config file-; ; that is embedded in the game binary. A user config file can override-; ; these options. Option names are case-sensitive and only ';' for comments-; ; is permitted.-; ;-; ; The game looks for the config file at the same path where saved games-; ; directory is located. E.g. on Linux the file is at-; ; ~/.LambdaHack/config.ui.ini-; ; and on Windows it can be at-; ; C:\Documents And Settings\user\Application Data\LambdaHack\config.ui.ini-; ; or at-; ; C:\Users\<username>\AppData\Roaming\LambdaHack\config.ui.ini-; ; or elsewhere.+; This is a copy of the default UI settings config file+; that is embedded in the game binary. A user config file can override+; these options. Option names are case-sensitive and only ';' for comments+; is permitted.+;+; The game looks for the config file at the same path where saved games+; directory is located. E.g. on Linux the file is at+; ~/.LambdaHack/config.ui.ini+; and on Windows it can be at+; C:\Documents And Settings\user\Application Data\LambdaHack\config.ui.ini+; or at+; C:\Users\<username>\AppData\Roaming\LambdaHack\config.ui.ini+; or elsewhere. -; [extra_commands]-; ; A handy shorthand with Vi keys-; Macro_1 = ("comma", ([CmdItem], Macro "" ["g"]))-; ; Angband compatibility (accept target)-; Macro_2 = ("KP_Insert", ([CmdMeta], Macro "" ["Return"]))+[extra_commands]+; A handy shorthand with Vi keys+Macro_1 = ("comma", ([CmdItem], Macro "" ["g"]))+; Angband compatibility (accept target)+Macro_2 = ("KP_Insert", ([CmdMeta], Macro "" ["Return"])) -; [hero_names]-; HeroName_0 = ("Haskell Alvin", "he")-; HeroName_1 = ("Alonzo Barkley", "he")-; HeroName_2 = ("Ines Galenti", "she")-; HeroName_3 = ("Ernst Abraham", "he")-; HeroName_4 = ("Samuel Saunders", "he")-; HeroName_5 = ("Roger Robin", "he")-; HeroName_6 = ("Christopher Flatt", "he")+[hero_names]+HeroName_0 = ("Haskell Alvin", "he")+HeroName_1 = ("Alonzo Barkley", "he")+HeroName_2 = ("Ines Galenti", "she")+HeroName_3 = ("Ernst Abraham", "he")+HeroName_4 = ("Samuel Saunders", "he")+HeroName_5 = ("Roger Robin", "he")+HeroName_6 = ("Christopher Flatt", "he") -; [ui]-; movementViKeys_hjklyubn = False-; movementLaptopKeys_uk8o79jl = True-; ; Monospace fonts that have fixed size regardless of boldness (on some OSes)-; font = "Terminus,DejaVu Sans Mono,Consolas,Courier New,Liberation Mono,Courier,FreeMono,Monospace normal normal normal normal 16"-; colorIsBold = True-; historyMax = 5000-; maxFps = 15-; noAnim = False-; runStopMsgs = False-[dummy]+[ui]+movementViKeys_hjklyubn = False+movementLaptopKeys_uk8o79jl = True+; Monospace fonts that have fixed size regardless of boldness (on some OSes)+font = "Terminus,DejaVu Sans Mono,Consolas,Courier New,Liberation Mono,Courier,FreeMono,Monospace normal normal normal normal 16"+;font = "Terminus,DejaVu Sans Mono,Consolas,Courier New,Liberation Mono,Courier,FreeMono,Monospace normal normal normal normal 20"+colorIsBold = True+historyMax = 5000+maxFps = 30+noAnim = False+runStopMsgs = False
GameDefinition/scores view
binary file changed (298 → 80 bytes)
LICENSE view
@@ -1,5 +1,5 @@-Copyright (c) 2008--2014 Andres Loeh-Copyright (c) 2010--2014 Mikolaj Konarski+Copyright (c) 2008--2015 Andres Loeh+Copyright (c) 2010--2015 Mikolaj Konarski All rights reserved.
LambdaHack.cabal view
@@ -5,7 +5,7 @@ -- PVP summary: +-+------- breaking API changes -- | | +----- non-breaking API additions -- | | | +--- code changes with no API change-version: 0.4.100.0+version: 0.4.101.0 synopsis: A game engine library for roguelike dungeon crawlers description: LambdaHack is a game engine library for roguelike games of arbitrary theme, size and complexity,@@ -21,8 +21,8 @@ Several frontends are available (GTK is the default) 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 and content- and of clients (human and AI-controlled) and server.+ in the strict and type-safe separation of code from the content+ and of clients (human and AI-controlled) from the server. Please see the changelog file for recent improvements and the issue tracker for short-term plans. Long term vision revolves around procedural content generation and includes@@ -45,14 +45,14 @@ the module is the exclusive interface to the directory. No references to the modules in the directory are allowed except from the interface module. This policy is only binding- inside the library --- users are free to do whatever they- please, since the library authors are in no position to guess- their particular needs.+ when developing the library --- library users are free+ to access any modules, since the library authors are in+ no position to guess their particular needs. homepage: http://github.com/LambdaHack/LambdaHack bug-reports: http://github.com/LambdaHack/LambdaHack/issues license: BSD3 license-file: LICENSE-tested-with: GHC == 7.6.3, GHC == 7.8+tested-with: GHC == 7.6, GHC == 7.8 data-files: GameDefinition/config.ui.default, GameDefinition/scores GameDefinition/PLAYING.md, README.md, LICENSE, CREDITS, CHANGELOG.md@@ -83,6 +83,11 @@ default: False manual: True +flag with_expensive_assertions+ description: turn on expensive assertions of well-tested code+ default: False+ manual: True+ flag release description: prepare for a release (expose, optimize, etc.) default: True@@ -209,7 +214,7 @@ base >= 4 && < 5, binary >= 0.7 && < 1, bytestring >= 0.9.2 && < 1,- containers >= 0.5 && < 1,+ containers >= 0.5.3.0 && < 1, data-default, deepseq >= 1.3 && < 2, directory >= 1.1.0.1 && < 2,@@ -223,7 +228,7 @@ mtl >= 2.0.1 && < 3, old-time >= 1.0.0.7 && < 2, pretty-show >= 1.6 && < 2,- random >= 1.0.1 && < 2,+ random >= 1.1 && < 2, stm >= 2.4 && < 3, text >= 0.11.2.3 && < 2, transformers >= 0.3 && < 1,@@ -271,11 +276,14 @@ if flag(expose_internal) cpp-options: -DEXPOSE_INTERNAL + if flag(with_expensive_assertions)+ cpp-options: -DWITH_EXPENSIVE_ASSERTIONS+ if flag(release) { cpp-options: -DEXPOSE_INTERNAL -- 7.6.3 has broken -O2, apparently if impl(ghc > 7.8)- ghc-options: -O2+ ghc-options: -O2 -fno-ignore-asserts } executable LambdaHack@@ -304,7 +312,7 @@ base >= 4 && < 5, binary >= 0.7 && < 1, bytestring >= 0.9.2 && < 1,- containers >= 0.5 && < 1,+ containers >= 0.5.3.0 && < 1, data-default, deepseq >= 1.3 && < 2, directory >= 1.1.0.1 && < 2,@@ -318,7 +326,7 @@ mtl >= 2.0.1 && < 3, old-time >= 1.0.0.7 && < 2, pretty-show >= 1.6 && < 2,- random >= 1.0.1 && < 2,+ random >= 1.1 && < 2, stm >= 2.4 && < 3, text >= 0.11.2.3 && < 2, transformers >= 0.3 && < 1,@@ -337,7 +345,7 @@ ghc-options: -threaded "-with-rtsopts=-C0.005" -rtsopts if flag(release)- ghc-options: -O2 "-with-rtsopts=-N1"+ ghc-options: -O2 -fno-ignore-asserts "-with-rtsopts=-N1" -- TODO: -N test-suite test@@ -353,7 +361,7 @@ base >= 4 && < 5, binary >= 0.7 && < 1, bytestring >= 0.9.2 && < 1,- containers >= 0.5 && < 1,+ containers >= 0.5.3.0 && < 1, data-default, deepseq >= 1.3 && < 2, directory >= 1.1.0.1 && < 2,@@ -367,7 +375,7 @@ mtl >= 2.0.1 && < 3, old-time >= 1.0.0.7 && < 2, pretty-show >= 1.6 && < 2,- random >= 1.0.1 && < 2,+ random >= 1.1 && < 2, stm >= 2.4 && < 3, text >= 0.11.2.3 && < 2, transformers >= 0.3 && < 1,@@ -386,5 +394,5 @@ ghc-options: -threaded "-with-rtsopts=-C0.005" -rtsopts if flag(release)- ghc-options: -O2 "-with-rtsopts=-N1"+ ghc-options: -O2 -fno-ignore-asserts "-with-rtsopts=-N1" -- TODO: -N
Makefile view
@@ -23,116 +23,19 @@ xcfrontendBattle: dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --maxFps 60 --dumpInitRngs --automateAll --gameMode battle --difficulty 2 +xcfrontendBattleSurvival:+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --maxFps 60 --dumpInitRngs --automateAll --gameMode "battle survival" --difficulty 8+ xcfrontendSafari: dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --maxFps 60 --dumpInitRngs --automateAll --gameMode safari --difficulty 2 +xcfrontendSafariSurvival:+ dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --maxFps 60 --dumpInitRngs --automateAll --gameMode "safari survival" --difficulty 8+ xcfrontendDefense: dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --maxFps 60 --dumpInitRngs --automateAll --gameMode defense --difficulty 8 -xcbenchCampaign:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfter 60 --automateAll --gameMode campaign --difficulty 2 --setDungeonRng 42 --setMainRng 42 +RTS -N1 -RTS -xcbenchBattle:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfter 60 --automateAll --gameMode battle --difficulty 2 --setDungeonRng 42 --setMainRng 42 +RTS -N1 -RTS--xcbenchFrontendCampaign:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --maxFps 100000 --benchmark --stopAfter 60 --automateAll --gameMode campaign --difficulty 2 --setDungeonRng 42 --setMainRng 42 +RTS -N1 -RTS--xcbenchFrontendBattle:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --maxFps 100000 --benchmark --stopAfter 60 --automateAll --gameMode battle --difficulty 2 --setDungeonRng 42 --setMainRng 42 +RTS -N1 -RTS--xcbenchNull: xcbenchCampaign xcbenchBattle--xcbench: xcbenchCampaign xcbenchFrontendCampaign xcbenchBattle xcbenchFrontendBattle---xctest-travis-short: xctest-short--xctest-travis: xctest-short xctest-medium--xctest-travis-long: xctest-short xctest-long--xctest-travis-long-no-safari: xctest-short xctest-long-no-safari--xctest: xctest-short xctest-medium xctest-long--xctest-short: xctest-short-new xctest-short-load--xctest-medium: xctestCampaign-medium xctestSkirmish-medium xctestAmbush-medium xctestBattle-medium xctestSafari-medium xctestPvP-medium xctestCoop-medium xctestDefense-medium--xctest-long: xctestCampaign-long xctestSkirmish-medium xctestAmbush-medium xctestBattle-long xctestSafari-long xctestPvP-medium xctestDefense-long--xctest-long-no-safari: xctestCampaign-long xctestSkirmish-medium xctestAmbush-medium xctestBattle-long xctestPvP-medium xctestDefense-long--xctestCampaign-long:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 500 --dumpInitRngs --automateAll --gameMode campaign --difficulty 2 > /tmp/stdtest.log--xctestCampaign-medium:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 400 --dumpInitRngs --automateAll --gameMode campaign --difficulty 2 > /tmp/stdtest.log--xctestSkirmish-long:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --maxFps 100000 --frontendStd --benchmark --stopAfter 60 --dumpInitRngs --automateAll --gameMode skirmish > /tmp/stdtest.log--xctestSkirmish-medium:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --maxFps 100000 --frontendStd --benchmark --stopAfter 30 --dumpInitRngs --automateAll --gameMode skirmish > /tmp/stdtest.log--xctestAmbush-long:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 60 --dumpInitRngs --automateAll --gameMode ambush > /tmp/stdtest.log--xctestAmbush-medium:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 30 --dumpInitRngs --automateAll --gameMode ambush > /tmp/stdtest.log--xctestBattle-long:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 200 --dumpInitRngs --automateAll --gameMode battle --difficulty 2 > /tmp/stdtest.log--xctestBattle-medium:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 100 --dumpInitRngs --automateAll --gameMode battle --difficulty 2 > /tmp/stdtest.log--xctestSafari-long:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 500 --dumpInitRngs --automateAll --gameMode safari --difficulty 2 > /tmp/stdtest.log--xctestSafari-medium:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 400 --dumpInitRngs --automateAll --gameMode safari --difficulty 2 > /tmp/stdtest.log--xctestPvP-long:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 60 --dumpInitRngs --automateAll --gameMode PvP > /tmp/stdtest.log--xctestPvP-medium:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 30 --dumpInitRngs --automateAll --gameMode PvP > /tmp/stdtest.log--xctestCoop-long:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 500 --dumpInitRngs --automateAll --gameMode Coop --difficulty 2 > /tmp/stdtest.log--xctestCoop-medium:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 300 --dumpInitRngs --automateAll --gameMode Coop --difficulty 2 > /tmp/stdtest.log--xctestDefense-long:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 500 --dumpInitRngs --automateAll --gameMode defense --difficulty 8 > /tmp/stdtest.log--xctestDefense-medium:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 300 --dumpInitRngs --automateAll --gameMode defense --difficulty 8 > /tmp/stdtest.log--xctest-short-new:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix campaign --dumpInitRngs --automateAll --gameMode campaign --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix skirmish --dumpInitRngs --automateAll --gameMode skirmish --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix ambush --dumpInitRngs --automateAll --gameMode ambush --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix battle --dumpInitRngs --automateAll --gameMode battle --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix safari --dumpInitRngs --automateAll --gameMode safari --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix PvP --dumpInitRngs --automateAll --gameMode PvP --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix Coop --dumpInitRngs --automateAll --gameMode Coop --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --newGame --savePrefix defense --dumpInitRngs --automateAll --gameMode defense --frontendStd --stopAfter 2 > /tmp/stdtest.log--xctest-short-load:- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix campaign --dumpInitRngs --automateAll --gameMode campaign --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix skirmish --dumpInitRngs --automateAll --gameMode skirmish --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix ambush --dumpInitRngs --automateAll --gameMode ambush --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix battle --dumpInitRngs --automateAll --gameMode battle --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix safari --dumpInitRngs --automateAll --gameMode safari --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix PvP --dumpInitRngs --automateAll --gameMode PvP --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix Coop --dumpInitRngs --automateAll --gameMode Coop --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack +RTS -xc -RTS --dbgMsgSer --savePrefix defense --dumpInitRngs --automateAll --gameMode defense --frontendStd --stopAfter 2 > /tmp/stdtest.log-- play: dist/build/LambdaHack/LambdaHack --dbgMsgSer --dumpInitRngs @@ -148,23 +51,29 @@ frontendBattle: dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --maxFps 60 --dumpInitRngs --automateAll --gameMode battle --difficulty 2 +frontendBattleSurvival:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --maxFps 60 --dumpInitRngs --automateAll --gameMode "battle survival" --difficulty 8+ frontendSafari: dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --maxFps 60 --dumpInitRngs --automateAll --gameMode safari --difficulty 2 +frontendSafariSurvival:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --maxFps 60 --dumpInitRngs --automateAll --gameMode "safari survival" --difficulty 8+ frontendDefense: dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --maxFps 60 --dumpInitRngs --automateAll --gameMode defense --difficulty 8 benchCampaign:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfter 60 --automateAll --gameMode campaign --difficulty 2 --setDungeonRng 42 --setMainRng 42 +RTS -N1 -RTS+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfter 60 --automateAll --keepAutomated --gameMode campaign --difficulty 2 --setDungeonRng 42 --setMainRng 42 +RTS -N1 -RTS benchBattle:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfter 60 --automateAll --gameMode battle --difficulty 2 --setDungeonRng 42 --setMainRng 42 +RTS -N1 -RTS+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfter 60 --automateAll --keepAutomated --gameMode battle --difficulty 2 --setDungeonRng 42 --setMainRng 42 +RTS -N1 -RTS benchFrontendCampaign:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --maxFps 100000 --benchmark --stopAfter 60 --automateAll --gameMode campaign --difficulty 2 --setDungeonRng 42 --setMainRng 42 +RTS -N1 -RTS+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --maxFps 100000 --benchmark --stopAfter 60 --automateAll --keepAutomated --gameMode campaign --difficulty 2 --setDungeonRng 42 --setMainRng 42 +RTS -N1 -RTS benchFrontendBattle:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --maxFps 100000 --benchmark --stopAfter 60 --automateAll --gameMode battle --difficulty 2 --setDungeonRng 42 --setMainRng 42 +RTS -N1 -RTS+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --maxFps 100000 --benchmark --stopAfter 60 --automateAll --keepAutomated --gameMode battle --difficulty 2 --setDungeonRng 42 --setMainRng 42 +RTS -N1 -RTS benchNull: benchCampaign benchBattle @@ -173,8 +82,10 @@ test-travis-short: test-short -test-travis: test-short test-medium+test-travis-medium: test-short test-medium +test-travis-medium-no-safari: test-short test-medium-no-safari+ test-travis-long: test-short test-long test-travis-long-no-safari: test-short test-long-no-safari@@ -183,79 +94,98 @@ test-short: test-short-new test-short-load -test-medium: testCampaign-medium testSkirmish-medium testAmbush-medium testBattle-medium testSafari-medium testPvP-medium testCoop-medium testDefense-medium+test-medium: testCampaign-medium testSkirmish-medium testAmbush-medium testBattle-medium testBattleSurvival-medium testSafari-medium testSafariSurvival-medium testPvP-medium testCoop-medium testDefense-medium -test-long: testCampaign-long testSkirmish-medium testAmbush-medium testBattle-long testSafari-long testPvP-medium testDefense-long+test-medium-no-safari: testCampaign-medium testSkirmish-medium testAmbush-medium testBattle-medium testBattleSurvival-medium testPvP-medium testCoop-medium testDefense-medium -test-long-no-safari: testCampaign-long testSkirmish-medium testAmbush-medium testBattle-long testPvP-medium testDefense-long+test-long: testCampaign-long testSkirmish-medium testAmbush-medium testBattle-long testBattleSurvival-long testSafari-long testSafariSurvival-long testPvP-medium testDefense-long +test-long-no-safari: testCampaign-long testSkirmish-medium testAmbush-medium testBattle-long testBattleSurvival-long testPvP-medium testDefense-long+ testCampaign-long:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 500 --dumpInitRngs --automateAll --gameMode campaign --difficulty 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 500 --dumpInitRngs --automateAll --keepAutomated --gameMode campaign --difficulty 2 > /tmp/stdtest.log testCampaign-medium:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 400 --dumpInitRngs --automateAll --gameMode campaign --difficulty 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 400 --dumpInitRngs --automateAll --keepAutomated --gameMode campaign --difficulty 2 > /tmp/stdtest.log testSkirmish-long:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --maxFps 100000 --frontendStd --benchmark --stopAfter 60 --dumpInitRngs --automateAll --gameMode skirmish > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --maxFps 100000 --frontendStd --benchmark --stopAfter 60 --dumpInitRngs --automateAll --keepAutomated --gameMode skirmish > /tmp/stdtest.log testSkirmish-medium:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --maxFps 100000 --frontendStd --benchmark --stopAfter 30 --dumpInitRngs --automateAll --gameMode skirmish > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --maxFps 100000 --frontendStd --benchmark --stopAfter 30 --dumpInitRngs --automateAll --keepAutomated --gameMode skirmish > /tmp/stdtest.log testAmbush-long:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 60 --dumpInitRngs --automateAll --gameMode ambush > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 60 --dumpInitRngs --automateAll --keepAutomated --gameMode ambush > /tmp/stdtest.log testAmbush-medium:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 30 --dumpInitRngs --automateAll --gameMode ambush > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 30 --dumpInitRngs --automateAll --keepAutomated --gameMode ambush > /tmp/stdtest.log testBattle-long:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 200 --dumpInitRngs --automateAll --gameMode battle --difficulty 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 100 --dumpInitRngs --automateAll --keepAutomated --gameMode battle --difficulty 2 > /tmp/stdtest.log testBattle-medium:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 100 --dumpInitRngs --automateAll --gameMode battle --difficulty 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 50 --dumpInitRngs --automateAll --keepAutomated --gameMode battle --difficulty 2 > /tmp/stdtest.log +testBattleSurvival-long:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 100 --dumpInitRngs --automateAll --keepAutomated --gameMode "battle survival" --difficulty 8 > /tmp/stdtest.log++testBattleSurvival-medium:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 50 --dumpInitRngs --automateAll --keepAutomated --gameMode "battle survival" --difficulty 8 > /tmp/stdtest.log+ testSafari-long:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 500 --dumpInitRngs --automateAll --gameMode safari --difficulty 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 250 --dumpInitRngs --automateAll --keepAutomated --gameMode safari --difficulty 2 > /tmp/stdtest.log testSafari-medium:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 400 --dumpInitRngs --automateAll --gameMode safari --difficulty 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 200 --dumpInitRngs --automateAll --keepAutomated --gameMode safari --difficulty 2 > /tmp/stdtest.log +testSafariSurvival-long:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 250 --dumpInitRngs --automateAll --keepAutomated --gameMode "safari survival" --difficulty 8 > /tmp/stdtest.log++testSafariSurvival-medium:+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 200 --dumpInitRngs --automateAll --keepAutomated --gameMode "safari survival" --difficulty 8 > /tmp/stdtest.log++ testPvP-long:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 60 --dumpInitRngs --automateAll --gameMode PvP > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 60 --dumpInitRngs --automateAll --keepAutomated --gameMode PvP > /tmp/stdtest.log testPvP-medium:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 30 --dumpInitRngs --automateAll --gameMode PvP > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 30 --dumpInitRngs --automateAll --keepAutomated --gameMode PvP > /tmp/stdtest.log testCoop-long:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 500 --dumpInitRngs --automateAll --gameMode Coop --difficulty 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 500 --dumpInitRngs --automateAll --keepAutomated --gameMode Coop --difficulty 2 > /tmp/stdtest.log testCoop-medium:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 300 --dumpInitRngs --automateAll --gameMode Coop --difficulty 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 300 --dumpInitRngs --automateAll --keepAutomated --gameMode Coop --difficulty 2 > /tmp/stdtest.log testDefense-long:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 500 --dumpInitRngs --automateAll --gameMode defense --difficulty 8 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 500 --dumpInitRngs --automateAll --keepAutomated --gameMode defense --difficulty 8 > /tmp/stdtest.log testDefense-medium:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 300 --dumpInitRngs --automateAll --gameMode defense --difficulty 8 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendStd --benchmark --stopAfter 300 --dumpInitRngs --automateAll --keepAutomated --gameMode defense --difficulty 8 > /tmp/stdtest.log test-short-new:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix campaign --dumpInitRngs --automateAll --gameMode campaign --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix skirmish --dumpInitRngs --automateAll --gameMode skirmish --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix ambush --dumpInitRngs --automateAll --gameMode ambush --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix battle --dumpInitRngs --automateAll --gameMode battle --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix safari --dumpInitRngs --automateAll --gameMode safari --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix PvP --dumpInitRngs --automateAll --gameMode PvP --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix Coop --dumpInitRngs --automateAll --gameMode Coop --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix defense --dumpInitRngs --automateAll --gameMode defense --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix campaign --dumpInitRngs --automateAll --keepAutomated --gameMode campaign --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix skirmish --dumpInitRngs --automateAll --keepAutomated --gameMode skirmish --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix ambush --dumpInitRngs --automateAll --keepAutomated --gameMode ambush --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix battle --dumpInitRngs --automateAll --keepAutomated --gameMode battle --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix battleSurvival --dumpInitRngs --automateAll --keepAutomated --gameMode "battle survival" --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix safari --dumpInitRngs --automateAll --keepAutomated --gameMode safari --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix safariSurvival --dumpInitRngs --automateAll --keepAutomated --gameMode "safari survival" --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix PvP --dumpInitRngs --automateAll --keepAutomated --gameMode PvP --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix Coop --dumpInitRngs --automateAll --keepAutomated --gameMode Coop --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --newGame --savePrefix defense --dumpInitRngs --automateAll --keepAutomated --gameMode defense --frontendStd --stopAfter 2 > /tmp/stdtest.log test-short-load:- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix campaign --dumpInitRngs --automateAll --gameMode campaign --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix skirmish --dumpInitRngs --automateAll --gameMode skirmish --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix ambush --dumpInitRngs --automateAll --gameMode ambush --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix battle --dumpInitRngs --automateAll --gameMode battle --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix safari --dumpInitRngs --automateAll --gameMode safari --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix PvP --dumpInitRngs --automateAll --gameMode PvP --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix Coop --dumpInitRngs --automateAll --gameMode Coop --frontendStd --stopAfter 2 > /tmp/stdtest.log- dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix defense --dumpInitRngs --automateAll --gameMode defense --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix campaign --dumpInitRngs --automateAll --keepAutomated --gameMode campaign --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix skirmish --dumpInitRngs --automateAll --keepAutomated --gameMode skirmish --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix ambush --dumpInitRngs --automateAll --keepAutomated --gameMode ambush --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix battle --dumpInitRngs --automateAll --keepAutomated --gameMode battle --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix battleSurvival --dumpInitRngs --automateAll --keepAutomated --gameMode "battle survival" --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix safari --dumpInitRngs --automateAll --keepAutomated --gameMode safari --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix safariSurvival --dumpInitRngs --automateAll --keepAutomated --gameMode "safari survival" --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix PvP --dumpInitRngs --automateAll --keepAutomated --gameMode PvP --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix Coop --dumpInitRngs --automateAll --keepAutomated --gameMode Coop --frontendStd --stopAfter 2 > /tmp/stdtest.log+ dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix defense --dumpInitRngs --automateAll --keepAutomated --gameMode defense --frontendStd --stopAfter 2 > /tmp/stdtest.log build-binary:
README.md view
@@ -59,7 +59,7 @@ --------------------------------- The game UI can be configured via a config file.-The commented out version of this file with the default settings is in+A file with the default settings, the same as built into the binary, is in [GameDefinition/config.ui.default](GameDefinition/config.ui.default). When the game is run for the first time, the file is copied to the official location, which is `~/.LambdaHack/config.ui.ini` on Linux and@@ -67,29 +67,29 @@ (or `C:\Documents And Settings\user\Application Data\LambdaHack\config.ui.ini` or something else altogether) on Windows. -Screen font can be changed and enlarged by uncommenting and editing-the config file at its official location or by right-clicking-on the game window.+Screen font can be changed and enlarged by editing the config file+at its official location or by CTRL-right-clicking on the game window. If you use the numeric keypad, use the NumLock key on your keyboard to toggle the game keyboard mode. With NumLock off, you walk with the numeric-keys and run with Shift (or Control) and the keys. When you turn NumLock on,+keys and run with Shift (or Control) and the keys. This mode is probably+the best if you use mouse for running. When you turn NumLock on, the reversed key setup enforces good playing habits by setting as the default the run command (which automatically stops at threats, keeping you safe) and requiring Shift for the error-prone step by step walking. -If you don't have a numeric keypad, you can use laptop keys (uk8o79jl)+If you don't have the numeric keypad, you can use laptop keys (uk8o79jl) or you can enable the Vi keys (aka roguelike keys) in the config file. Compilation from source ----------------------- -The library is best compiled and installed via Cabal (already a part-of your OS distribution, or available within [The Haskell Platform] [7]),-which also takes care of all the dependencies. You also need-the GTK libraries for your OS. On Linux, remember to install the -dev-versions as well. On Windows follow [the same steps as for Wine] [13].+If you want to compile your own binaries from the source code,+use Cabal (already a part of your OS distribution, or available within+[The Haskell Platform] [7]), which also takes care of all the dependencies.+You also need the GTK libraries for your OS. On Linux, remember to install+the -dev versions as well. On Windows follow [the same steps as for Wine] [13]. On OSX, if you encounter problems, you may want to [compile the GTK libraries from sources] [14]. @@ -97,41 +97,41 @@ compiled and installed automatically by Cabal from [Hackage] [3] as follows cabal install gtk2hs-buildtools- cabal install LambdaHack+ cabal install LambdaHack --force-reinstalls For a newer snapshot, download source from a development branch at [github] [5] and run Cabal from the main directory cabal install gtk2hs-buildtools- cabal install+ cabal install --force-reinstalls For the example game, the best frontend (wrt keyboard support and colours) is the default gtk. To compile with one of the terminal frontends, use Cabal flags, e.g, - cabal install -fvty+ cabal install -fvty --force-reinstalls Compatibility notes ------------------- -The current code was tested with GHC 7.6 and 7.8,-but should also work with other GHC versions, with minor modifications.--If you are using the terminal frontends, numerical keypad may not work+If you are using a terminal frontend, numeric keypad may not work correctly depending on versions of the libraries, terminfo and terminal emulators. The curses frontend is not fully supported due to the limitations-of the curses library. With the vty frontend run in an xterm,+of the curses library. With the vty frontend started in an xterm, CTRL-keypad keys for running seem to work OK, but on rxvt they do not.-Laptop (uk8o79jl) and Vi keys (hjklyubn, if enabled in config.ui.ini)-should work everywhere regardless. GTK works fine, too.+The commands that require pressing CTRL and SHIFT together won't+work either, but fortunately they are not crucial to gameplay.+For movement, laptop (uk8o79jl) and Vi keys (hjklyubn, if enabled+in config.ui.ini) should work everywhere. GTK works fine, too, both+with numeric keypad and with mouse. Testing and debugging --------------------- The [Makefile](Makefile) contains many sample test commands.-Many tests that use the screensaver game modes (AI vs. AI)+Numerous tests that use the screensaver game modes (AI vs. AI) and the dumb `stdout` frontend are gathered in `make test`. Of these, travis runs `test-travis-*` on each push to the repo. Test commands with prefix `frontend` start AI vs. AI games@@ -155,8 +155,8 @@ hpc markup --hpcdir=dist/hpc/mix/LambdaHack-xxx/ LambdaHack Note that debug option `--stopAfter` is required to cleanly terminate-any automated test that is used to gather HPC info, because HPC needs-a clean exit (to save data files).+any automated test. This is needed to gather any HPC info, because HPC+requires a clean exit to save data files. Further information
test/test.hs view
@@ -1,6 +1,6 @@ import TieKnot main :: IO ()-main = do+main = tieKnot $ tail $ words "dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfter 6 --automateAll --gameMode campaign --setDungeonRng 42 --setMainRng 42" -- tieKnot $ tail $ words "dist/build/LambdaHack/LambdaHack --dbgMsgSer --savePrefix test --newGame --noDelay --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfter 6 --automateAll --gameMode battle --setDungeonRng 42 --setMainRng 42"