-- | Server operations for items. module Game.LambdaHack.Server.ItemM ( registerItem, moveStashIfNeeded, randomResetTimeout, embedItemOnPos , prepareItemKind, rollItemAspect, rollAndRegisterItem , placeItemsInDungeon, embedItemsInDungeon, mapActorCStore_ #ifdef EXPOSE_INTERNAL -- * Internal operations , onlyRegisterItem, computeRndTimeout, createCaveItem, createEmbedItem #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.HashMap.Strict as HM import Game.LambdaHack.Atomic import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.MonadStateRead 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.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.CaveKind (citemFreq, citemNum) import Game.LambdaHack.Content.ItemKind (ItemKind) import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.TileKind (TileKind) import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Core.Frequency import Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Server.ItemRev import Game.LambdaHack.Server.MonadServer import Game.LambdaHack.Server.ServerOptions import Game.LambdaHack.Server.State onlyRegisterItem :: MonadServerAtomic m => ItemKnown -> m ItemId onlyRegisterItem itemKnown@(ItemKnown _ arItem _) = do itemRev <- getsServer sitemRev case HM.lookup itemKnown itemRev of Just iid -> return iid Nothing -> do icounter <- getsServer sicounter executedOnServer <- execUpdAtomicSer $ UpdDiscoverServer icounter arItem let !_A = assert executedOnServer () modifyServer $ \ser -> ser { sitemRev = HM.insert itemKnown icounter (sitemRev ser) , sicounter = succ icounter } return $! icounter registerItem :: MonadServerAtomic m => Bool -> ItemFullKit -> ItemKnown -> Container -> m ItemId registerItem verbose (itemFull@ItemFull{itemBase, itemKindId, itemKind}, kit) itemKnown@(ItemKnown _ arItem _) containerRaw = do container <- case containerRaw of CActor aid CEqp -> do b <- getsState $ getActorBody aid return $! if eqpFreeN b >= fst kit then containerRaw else CActor aid CStash _ -> return containerRaw iid <- onlyRegisterItem itemKnown let slore = IA.loreFromContainer arItem container modifyServer $ \ser -> ser {sgenerationAn = EM.adjust (EM.insertWith (+) iid (fst kit)) slore (sgenerationAn ser)} moveStash <- moveStashIfNeeded container mapM_ execUpdAtomic moveStash execUpdAtomic $ UpdCreateItem verbose iid itemBase kit container let worth = itemPrice (fst kit) itemKind case container of _ | worth == 0 -> return () CActor _ COrgan -> return () -- destroyed on drop CTrunk{} -> return () -- we assume any valuables in CEmbed can be dug out _ -> execUpdAtomic $ UpdAlterGold worth knowItems <- getsServer $ sknowItems . soptions when knowItems $ case container of CTrunk{} -> return () _ -> execUpdAtomic $ UpdDiscover container iid itemKindId arItem -- The first recharging period after creation is random, -- between 1 and 2 standard timeouts of the item. -- In this way we avoid many rattlesnakes rattling in unison. case container of CActor _ cstore | cstore `elem` [CEqp, COrgan] -> randomResetTimeout (fst kit) iid itemFull [] container _ -> return () return iid moveStashIfNeeded :: MonadStateRead m => Container -> m [UpdAtomic] moveStashIfNeeded c = case c of CActor aid CStash -> do b <- getsState $ getActorBody aid mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b case mstash of Just (lid, pos) -> do bagStash <- getsState $ getFloorBag lid pos return $! if EM.null bagStash then [ UpdLoseStashFaction False (bfid b) lid pos , UpdSpotStashFaction True (bfid b) (blid b) (bpos b) ] else [] Nothing -> return [UpdSpotStashFaction True (bfid b) (blid b) (bpos b)] _ -> return [] randomResetTimeout :: MonadServerAtomic m => Int -> ItemId -> ItemFull -> [ItemTimer] -> Container -> m () randomResetTimeout k iid itemFull beforeIt toC = do lid <- getsState $ lidFromC toC localTime <- getsState $ getLocalTime lid mrndTimeout <- rndToAction $ computeRndTimeout localTime itemFull -- The created or moved item set (not the items previously at destination) -- has its timeouts 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 $ getContainerBag toC let afterIt = case iid `EM.lookup` bagAfter of Nothing -> error $ "" `showFailure` (iid, bagAfter, toC) Just (_, it2) -> it2 resetIt = beforeIt ++ replicate k rndT when (afterIt /= resetIt) $ execUpdAtomic $ UpdTimeItem iid toC afterIt resetIt Nothing -> return () -- no @Timeout@ aspect; don't touch computeRndTimeout :: Time -> ItemFull -> Rnd (Maybe ItemTimer) computeRndTimeout localTime ItemFull{itemDisco=ItemDiscoFull itemAspect} = do let t = IA.aTimeout itemAspect if t > 0 then do rndT <- randomR0 t let rndTurns = timeDeltaScale (Delta timeTurn) (t + rndT) return $ Just $ createItemTimer localTime rndTurns else return Nothing computeRndTimeout _ _ = error "computeRndTimeout: server ignorant about an item" createCaveItem :: MonadServerAtomic m => Point -> LevelId -> m () createCaveItem pos lid = do COps{cocave} <- getsState scops Level{lkind, ldepth} <- getLevel lid let container = CFloor lid pos litemFreq = citemFreq $ okind cocave lkind -- Power depth of new items unaffected by number of spawned actors. freq <- prepareItemKind 0 ldepth litemFreq mIidEtc <- rollAndRegisterItem True ldepth freq container Nothing createKitItems lid pos mIidEtc createEmbedItem :: MonadServerAtomic m => LevelId -> Point -> GroupName ItemKind -> m () createEmbedItem lid pos grp = do Level{ldepth} <- getLevel lid let container = CEmbed lid pos -- Power depth of new items unaffected by number of spawned actors. freq <- prepareItemKind 0 ldepth [(grp, 1)] mIidEtc <- rollAndRegisterItem True ldepth freq container Nothing createKitItems lid pos mIidEtc -- Create, register and insert all initial kit items. createKitItems :: MonadServerAtomic m => LevelId -> Point -> Maybe (ItemId, ItemFullKit) -> m () createKitItems lid pos mIidEtc = case mIidEtc of Nothing -> error $ "" `showFailure` (lid, pos, mIidEtc) Just (_, (itemFull, _)) -> do cops <- getsState scops lvl@Level{ldepth} <- getLevel lid let ikit = IK.ikit $ itemKind itemFull nearbyPassable = take (20 + length ikit) $ nearbyPassablePoints cops lvl pos walkable p = Tile.isWalkable (coTileSpeedup cops) (lvl `at` p) good p = walkable p && p `EM.notMember` lfloor lvl kitPos = zip ikit $ filter good nearbyPassable ++ filter walkable nearbyPassable ++ repeat pos forM_ kitPos $ \((ikGrp, cstore), p) -> do let container = if cstore == CGround then CFloor lid p else CEmbed lid pos itemFreq = [(ikGrp, 1)] -- Power depth of new items unaffected by number of spawned actors. freq <- prepareItemKind 0 ldepth itemFreq mresult <- rollAndRegisterItem False ldepth freq container Nothing assert (isJust mresult) $ return () -- Tiles already placed, so it's possible to scatter companion items -- over walkable tiles. embedItemOnPos :: MonadServerAtomic m => LevelId -> Point -> ContentId TileKind -> m () embedItemOnPos lid pos tk = do COps{cotile} <- getsState scops let embedGroups = Tile.embeddedItems cotile tk mapM_ (createEmbedItem lid pos) embedGroups prepareItemKind :: MonadServerAtomic m => Int -> Dice.AbsDepth -> Freqs ItemKind -> m (Frequency (GroupName ItemKind, ContentId IK.ItemKind, ItemKind)) prepareItemKind lvlSpawned ldepth itemFreq = do cops <- getsState scops uniqueSet <- getsServer suniqueSet totalDepth <- getsState stotalDepth return $! newItemKind cops uniqueSet itemFreq ldepth totalDepth lvlSpawned rollItemAspect :: MonadServerAtomic m => Frequency (GroupName ItemKind, ContentId IK.ItemKind, ItemKind) -> Dice.AbsDepth -> m NewItem rollItemAspect freq ldepth = do cops <- getsState scops flavour <- getsServer sflavour discoRev <- getsServer sdiscoKindRev totalDepth <- getsState stotalDepth m2 <- rndToAction $ newItem cops freq flavour discoRev ldepth totalDepth case m2 of NewItem _ (ItemKnown _ arItem _) ItemFull{itemKindId} _ -> do when (IA.checkFlag Ability.Unique arItem) $ modifyServer $ \ser -> ser {suniqueSet = ES.insert itemKindId (suniqueSet ser)} NoNewItem -> return () return m2 rollAndRegisterItem :: MonadServerAtomic m => Bool -> Dice.AbsDepth -> Frequency (GroupName ItemKind, ContentId IK.ItemKind, ItemKind) -> Container -> Maybe Int -> m (Maybe (ItemId, ItemFullKit)) rollAndRegisterItem verbose ldepth freq container mk = do m2 <- rollItemAspect freq ldepth case m2 of NoNewItem -> return Nothing NewItem _ itemKnown itemFull kit -> do let f k = if k == 1 && null (snd kit) then quantSingle else (k, snd kit) !kit2 = maybe kit f mk iid <- registerItem verbose (itemFull, kit2) itemKnown container return $ Just (iid, (itemFull, kit2)) -- Tiles already placed, so it's possible to scatter over walkable tiles. placeItemsInDungeon :: forall m. MonadServerAtomic m => EM.EnumMap LevelId (EM.EnumMap FactionId Point) -> m () placeItemsInDungeon factionPositions = do COps{cocave, coTileSpeedup} <- getsState scops totalDepth <- getsState stotalDepth let initialItems (lid, lvl@Level{lkind, ldepth}) = do litemNum <- rndToAction $ castDice ldepth totalDepth (citemNum $ okind cocave lkind) let alPos = EM.elems $ EM.findWithDefault EM.empty lid factionPositions placeItems :: Int -> m () placeItems n | n == litemNum = return () placeItems !n = do Level{lfloor} <- getLevel lid -- Don't generate items around initial actors or in bunches. let distAndNotFloor !p _ = let f !k = chessDist p k > 4 in p `EM.notMember` lfloor && all f alPos mpos <- rndToAction $ findPosTry2 10 lvl (\_ !t -> Tile.isWalkable coTileSpeedup t && not (Tile.isNoItem coTileSpeedup t)) [ \_ !t -> Tile.isVeryOftenItem coTileSpeedup t , \_ !t -> Tile.isCommonItem coTileSpeedup t ] distAndNotFloor (replicate 10 distAndNotFloor) case mpos of Just pos -> do createCaveItem pos lid placeItems (n + 1) Nothing -> debugPossiblyPrint "Server: placeItemsInDungeon: failed to find positions" placeItems 0 dungeon <- getsState sdungeon -- Make sure items on easy levels are generated first, to avoid all -- artifacts on deep levels. let fromEasyToHard = sortBy (comparing (ldepth . snd)) $ EM.assocs dungeon mapM_ initialItems fromEasyToHard -- Tiles already placed, so it's possible to scatter companion items -- over walkable tiles. embedItemsInDungeon :: MonadServerAtomic m => m () embedItemsInDungeon = do let embedItemsOnLevel (lid, Level{ltile}) = PointArray.imapMA_ (embedItemOnPos lid) ltile dungeon <- getsState sdungeon -- Make sure items on easy levels are generated first, to avoid all -- artifacts on deep levels. let fromEasyToHard = sortBy (comparing (ldepth . snd)) $ EM.assocs dungeon mapM_ embedItemsOnLevel fromEasyToHard -- | Mapping over actor's items from a give store. mapActorCStore_ :: MonadServer m => CStore -> (ItemId -> ItemQuant -> m ()) -> Actor -> m () mapActorCStore_ cstore f b = do bag <- getsState $ getBodyStoreBag b cstore mapM_ (uncurry f) $ EM.assocs bag