LambdaHack-0.2.6.5: Game/LambdaHack/Item.hs
-- | Weapons, treasure and all the other items in the game.
-- No operation in this module
-- involves the 'State' or 'Action' type.
-- TODO: Document after it's rethought and rewritten wrt separating
-- inventory manangement and items proper.
module Game.LambdaHack.Item
( -- * Teh @Item@ type
Item(..), newItem, viewItem, itemPrice
-- * Inventory search
, strongestSearch, strongestSword, strongestRegen
-- * Inventory management
, joinItem, removeItemByLetter, equalItemIdentity, removeItemByIdentity
, assignLetter
-- * Inventory symbol operations
, letterLabel, cmpLetterMaybe, maxLetter, letterRange
-- * The @FlavourMap@ type
, FlavourMap, getFlavour, dungeonFlavourMap
-- * The @Discoveries@ type
, Discoveries
) where
import Data.Binary
import qualified Data.Set as S
import qualified Data.List as L
import qualified Data.Map as M
import Data.Maybe
import Data.Char
import Data.Function
import Data.Ord
import Control.Monad
import Data.Text (Text)
import qualified Data.Text as T
import qualified NLP.Miniutter.English as MU
import Game.LambdaHack.Utils.Assert
import Game.LambdaHack.Random
import Game.LambdaHack.Content.ItemKind
import Game.LambdaHack.Content.RuleKind
import qualified Game.LambdaHack.Color as Color
import Game.LambdaHack.Flavour
import qualified Game.LambdaHack.Kind as Kind
import Game.LambdaHack.Effect
import Game.LambdaHack.Msg
-- TODO: see the TODO about ipower in ItemKind.
-- TODO: define type InvSymbol = Char and move all ops to another file.
-- TODO: perhaps remove jletter and jcount? Should inventory semantics
-- be separate from item semantics?
-- TODO: the list resulting from joinItem can contain items
-- with the same letter.
-- TODO: name [Item] Inventory and have some invariants, e.g. no equal letters.
-- | Game items in inventories or strewn around the dungeon.
data Item = Item
{ jkind :: !(Kind.Id ItemKind) -- ^ kind of the item
, jpower :: !Int -- ^ power of the item
, jletter :: Maybe Char -- ^ inventory symbol
, jcount :: !Int -- ^ inventory count
}
deriving Show
instance Binary Item where
put (Item ik ip il ic ) =
put ik >> put ip >> put il >> put ic
get = liftM4 Item get get get get
-- | Generate an item.
newItem :: Kind.Ops ItemKind -> Int -> Int -> Rnd Item
newItem cops@Kind.Ops{opick, okind} lvl depth = do
ikChosen <- opick (T.pack "dng") (const True)
let kind = okind ikChosen
count <- rollDeep lvl depth (icount kind)
if count == 0
then newItem cops lvl depth -- Rare item; beware of inifite loops.
else do
power <- rollDeep lvl depth (ipower kind)
return $ Item ikChosen power (itemLetter kind) count
-- | Represent an item on the map.
viewItem :: Kind.Ops ItemKind -> Kind.Id ItemKind -> FlavourMap
-> (Char, Color.Color)
viewItem cops@Kind.Ops{osymbol} ik assocs =
(osymbol ik, flavourToColor $ getFlavour cops assocs ik)
-- | Price an item, taking count into consideration.
itemPrice :: Kind.Ops ItemKind -> Item -> Int
itemPrice Kind.Ops{osymbol} i =
case osymbol (jkind i) of
'$' -> jcount i
'*' -> jcount i * 100
_ -> 0
-- | The type of already discovered items.
type Discoveries = S.Set (Kind.Id ItemKind)
-- TODO: rewrite and move elsewhere
-- Could be optimized to IntMap and IntSet, but won't ever be a bottleneck,
-- unless we have thousands of item kinds.
-- | Flavours assigned to items in this game.
type FlavourMap = M.Map (Kind.Id ItemKind) Flavour
-- | Assigns flavours to item kinds. Assures no flavor is repeated,
-- except for items with only one permitted flavour.
rollFlavourMap :: Kind.Id ItemKind -> ItemKind
-> Rnd (FlavourMap, S.Set Flavour)
-> Rnd (FlavourMap, S.Set Flavour)
rollFlavourMap key ik rnd =
let flavours = iflavour ik
in if L.length flavours == 1
then rnd
else do
(assocs, available) <- rnd
let proper = S.fromList flavours `S.intersection` available
flavour <- oneOf (S.toList proper)
return (M.insert key flavour assocs, S.delete flavour available)
-- | Randomly chooses flavour for all item kinds for this game.
dungeonFlavourMap :: Kind.Ops ItemKind -> Rnd FlavourMap
dungeonFlavourMap Kind.Ops{ofoldrWithKey} =
liftM fst $
ofoldrWithKey rollFlavourMap (return (M.empty, S.fromList stdFlav))
getFlavour :: Kind.Ops ItemKind -> FlavourMap -> Kind.Id ItemKind -> Flavour
getFlavour Kind.Ops{okind} assocs ik =
let kind = okind ik
in case iflavour kind of
[] -> assert `failure` (assocs, ik, kind)
[f] -> f
_:_ -> assocs M.! ik
itemLetter :: ItemKind -> Maybe Char
itemLetter ik = if isymbol ik == '$' then Just '$' else Nothing
-- | Assigns a letter to an item, for inclusion
-- in the inventory of a hero. Takes a remembered
-- letter and a starting letter.
assignLetter :: Maybe Char -> Char -> [Item] -> Maybe Char
assignLetter r c is =
case r of
Just l | l `elem` allowed -> Just l
_ -> listToMaybe free
where
current = S.fromList (mapMaybe jletter is)
allLetters = ['a'..'z'] ++ ['A'..'Z']
candidates = take (length allLetters) $
drop (fromJust (L.findIndex (== c) allLetters)) $
cycle allLetters
free = L.filter (\x -> not (x `S.member` current)) candidates
allowed = '$' : free
cmpLetter :: Char -> Char -> Ordering
cmpLetter x y = compare (isUpper x, toLower x) (isUpper y, toLower y)
cmpLetterMaybe :: Maybe Char -> Maybe Char -> Ordering
cmpLetterMaybe Nothing Nothing = EQ
cmpLetterMaybe Nothing (Just _) = GT
cmpLetterMaybe (Just _) Nothing = LT
cmpLetterMaybe (Just l) (Just l') = cmpLetter l l'
maxBy :: (a -> a -> Ordering) -> a -> a -> a
maxBy cmp x y = case cmp x y of
LT -> y
_ -> x
maxLetter :: Char -> Char -> Char
maxLetter = maxBy cmpLetter
mergeLetter :: Maybe Char -> Maybe Char -> Maybe Char
mergeLetter = mplus
letterRange :: [Char] -> Text
letterRange ls =
sectionBy (L.sortBy cmpLetter ls) Nothing
where
succLetter c d = ord d - ord c == 1
sectionBy [] Nothing = T.empty
sectionBy [] (Just (c,d)) = finish (c,d)
sectionBy (x:xs) Nothing = sectionBy xs (Just (x,x))
sectionBy (x:xs) (Just (c,d))
| succLetter d x = sectionBy xs (Just (c,x))
| otherwise = finish (c,d) <> sectionBy xs (Just (x,x))
finish (c,d) | c == d = T.pack [c]
| succLetter c d = T.pack $ [c, d]
| otherwise = T.pack $ [c, '-', d]
letterLabel :: Maybe Char -> MU.Part
letterLabel Nothing = MU.Text $ T.pack " "
letterLabel (Just c) = MU.Text $ T.pack $ c : " -"
-- | Adds an item to a list of items, joining equal items.
-- Also returns the joined item.
joinItem :: Item -> [Item] -> (Item, [Item])
joinItem i is =
case findItem (equalItemIdentity i) is of
Nothing -> (i, i : is)
Just (j,js) -> let n = i { jcount = jcount i + jcount j,
jletter = mergeLetter (jletter j) (jletter i) }
in (n, n : js)
-- | Removes an item from a list of items.
-- Takes an equality function (i.e., by letter or ny kind) as an argument.
removeItemBy :: (Item -> Item -> Bool) -> Item -> [Item] -> [Item]
removeItemBy eq i = concatMap $ \ x ->
if eq i x
then let remaining = jcount x - jcount i
in if remaining > 0
then [x { jcount = remaining }]
else []
else [x]
equalItemIdentity :: Item -> Item -> Bool
equalItemIdentity i1 i2 = jpower i1 == jpower i2 && jkind i1 == jkind i2
removeItemByIdentity :: Item -> [Item] -> [Item]
removeItemByIdentity = removeItemBy equalItemIdentity
equalItemLetter :: Item -> Item -> Bool
equalItemLetter = (==) `on` jletter
removeItemByLetter :: Item -> [Item] -> [Item]
removeItemByLetter = removeItemBy equalItemLetter
-- | Finds an item in a list of items.
findItem :: (Item -> Bool) -> [Item] -> Maybe (Item, [Item])
findItem p =
findItem' []
where
findItem' _ [] = Nothing
findItem' acc (i:is)
| p i = Just (i, reverse acc ++ is)
| otherwise = findItem' (i:acc) is
strongestItem :: [Item] -> (Item -> Bool) -> Maybe Item
strongestItem is p =
let cmp = comparing jpower
igs = L.filter p is
in case igs of
[] -> Nothing
_ -> Just $ L.maximumBy cmp igs
strongestSearch :: Kind.Ops ItemKind -> [Item] -> Maybe Item
strongestSearch Kind.Ops{okind} bitems =
strongestItem bitems $ \ i -> (ieffect $ okind $ jkind i) == Searching
-- TODO: generalise, in particular take base damage into account
strongestSword :: Kind.COps -> [Item] -> Maybe Item
strongestSword Kind.COps{coitem=Kind.Ops{osymbol}, corule} bitems =
strongestItem bitems $ \ i -> (osymbol $ jkind i)
`elem` (ritemMelee $ Kind.stdRuleset corule)
strongestRegen :: Kind.Ops ItemKind -> [Item] -> Maybe Item
strongestRegen Kind.Ops{okind} bitems =
strongestItem bitems $ \ i -> (ieffect $ okind $ jkind i) == Regeneration