-- | Descripitons of items. module Game.LambdaHack.Client.UI.ItemDescription ( partItem, partItemShort, partItemShortest, partItemHigh, partItemWs , partItemWsRanged, partItemShortAW, partItemMediumAW, partItemShortWownW , viewItem, itemDesc #ifdef EXPOSE_INTERNAL -- * Internal operations , show64With2, partItemN, textAllAE, partItemWsR #endif ) where import Prelude () import Game.LambdaHack.Common.Prelude import qualified Data.EnumMap.Strict as EM import Data.Int (Int64) import qualified Data.Text as T import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Client.UI.EffectDescription import Game.LambdaHack.Client.UI.Overlay import qualified Game.LambdaHack.Common.Color as Color import qualified Game.LambdaHack.Common.Dice as Dice import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Flavour import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Time import qualified Game.LambdaHack.Content.ItemKind as IK show64With2 :: Int64 -> Text show64With2 n = let k = 100 * n `div` oneM l = k `div` 100 x = k - l * 100 in tshow l <> if | x == 0 -> "" | x < 10 -> ".0" <> tshow x | otherwise -> "." <> tshow x -- | The part of speech describing the item parameterized by the number -- of effects/aspects to show. partItemN :: FactionId -> FactionDict -> Bool -> DetailLevel -> Int -> Time -> ItemFull -> ItemQuant -> (Bool, Bool, MU.Part, MU.Part) partItemN side factionD ranged detailLevel maxWordsToShow localTime itemFull@ItemFull{itemBase, itemKind, itemSuspect} (itemK, itemTimer) = let flav = flavourToName $ jflavour itemBase timeout = IA.aTimeout $ aspectRecordFull itemFull timeoutTurns = timeDeltaScale (Delta timeTurn) timeout temporary = not (null itemTimer) && timeout == 0 charging startT = timeShift startT timeoutTurns > localTime it1 = filter charging itemTimer lenCh = length it1 timer | lenCh == 0 || temporary = "" | itemK == 1 && lenCh == 1 = "(charging)" | itemK == lenCh = "(all charging)" | otherwise = "(" <> tshow lenCh <+> "charging)" skipRecharging = detailLevel <= DetailLow && lenCh >= itemK (effTsRaw, rangedDamage) = textAllAE detailLevel skipRecharging itemFull effTs = effTsRaw ++ if ranged then rangedDamage else [] lsource = case jfid itemBase of Just fid | IK.iname itemKind `elem` ["impressed"] -> ["by" <+> if fid == side then "us" else gname (factionD EM.! fid)] _ -> [] ts = lsource ++ take maxWordsToShow effTs ++ ["(...)" | length effTs > maxWordsToShow && maxWordsToShow > 1] ++ [timer | maxWordsToShow > 1] unique = IK.Unique `elem` IK.ifeature itemKind name | temporary = "temporarily" <+> IK.iname itemKind | itemSuspect = flav <+> IK.iname itemKind | otherwise = IK.iname itemKind capName = if unique then MU.Capitalize $ MU.Text name else MU.Text name in ( not (null lsource) || temporary , unique, capName, MU.Phrase $ map MU.Text ts ) textAllAE :: DetailLevel -> Bool -> ItemFull -> ([Text], [Text]) textAllAE detailLevel skipRecharging itemFull@ItemFull{itemKind, itemDisco} = let features | detailLevel >= DetailAll = map featureToSuff $ sort $ IK.ifeature itemKind | otherwise = [] aets = case itemDisco of ItemDiscoMean{} -> splitTry (IK.iaspects itemKind) -- faster than @aspectRecordToList@ of mean ItemDiscoFull iAspect -> splitTry (IA.aspectRecordToList iAspect) timeoutAspect :: IA.Aspect -> Bool timeoutAspect IA.Timeout{} = True timeoutAspect _ = False hurtMeleeAspect :: IA.Aspect -> Bool hurtMeleeAspect IA.AddHurtMelee{} = True hurtMeleeAspect _ = False elabel :: IK.Feature -> Bool elabel IK.ELabel{} = True elabel _ = False active = IK.goesIntoEqp itemKind splitAE :: DetailLevel -> [IA.Aspect] -> [Text] splitAE detLev aspects = let ppA = kindAspectToSuffix ppE = effectToSuffix detLev reduce_a = maybe "?" tshow . Dice.reduceDice periodic = IK.Periodic `elem` IK.ifeature itemKind mtimeout = find timeoutAspect aspects elab = case find elabel $ IK.ifeature itemKind of Just (IK.ELabel t) -> [t] _ -> [] -- Effects are not being sorted here, because they should fire -- in the order specified in content. restAs = sort aspects restEs | detLev >= DetailHigh || IK.MinorEffects `notElem` IK.ifeature itemKind = IK.ieffects itemKind | otherwise = [] aes = if active then map ppA restAs ++ map ppE restEs else map ppE restEs ++ map ppA restAs rechargingTs = T.intercalate " " $ filter (not . T.null) $ map ppE $ IK.stripRecharging restEs onSmashTs = T.intercalate " " $ filter (not . T.null) $ map ppE $ IK.stripOnSmash restEs durable = IK.Durable `elem` IK.ifeature itemKind fragile = IK.Fragile `elem` IK.ifeature itemKind periodicOrTimeout = if | skipRecharging || T.null rechargingTs -> "" | periodic -> case mtimeout of Nothing | durable && not fragile -> "(each turn:" <+> rechargingTs <> ")" Nothing -> "(each turn until gone:" <+> rechargingTs <> ")" Just (IA.Timeout t) -> "(every" <+> reduce_a t <> ":" <+> rechargingTs <> ")" _ -> error $ "" `showFailure` mtimeout | otherwise -> case mtimeout of Nothing -> "" Just (IA.Timeout t) -> "(timeout" <+> reduce_a t <> ":" <+> rechargingTs <> ")" _ -> error $ "" `showFailure` mtimeout onSmash = if T.null onSmashTs then "" else "(on smash:" <+> onSmashTs <> ")" damage = case find hurtMeleeAspect restAs of Just (IA.AddHurtMelee hurtMelee) -> (if IK.idamage itemKind == 0 then "0d0" else tshow (IK.idamage itemKind)) <> affixDice hurtMelee <> "%" _ -> if IK.idamage itemKind == 0 then "" else tshow (IK.idamage itemKind) in filter (/= "") $ elab ++ if detLev >= DetailHigh || detLev >= DetailMedium && null elab then [periodicOrTimeout] ++ [damage] ++ aes ++ [onSmash | detLev >= DetailAll] else [damage] splitTry ass = let splits = map (`splitAE` ass) [minBound..maxBound] splitsToTry = drop (fromEnum detailLevel) splits in case filter (/= []) splitsToTry of detNonEmpty : _ -> detNonEmpty [] -> [] IK.ThrowMod{IK.throwVelocity} = IK.getToThrow itemKind speed = speedFromWeight (IK.iweight itemKind) throwVelocity meanDmg = ceiling $ Dice.meanDice (IK.idamage itemKind) minDeltaHP = xM meanDmg `divUp` 100 aHurtMeleeOfItem = IA.aHurtMelee $ aspectRecordFull itemFull pmult = 100 + min 99 (max (-99) aHurtMeleeOfItem) prawDeltaHP = fromIntegral pmult * minDeltaHP pdeltaHP = modifyDamageBySpeed prawDeltaHP speed rangedDamage = if pdeltaHP == 0 then [] else ["{avg" <+> show64With2 pdeltaHP <+> "ranged}"] -- Note that avg melee damage would be too complex to display here, -- because in case of @MOwned@ the owner is different than leader, -- so the value would be different than when viewing the item. in (aets ++ features, rangedDamage) -- | The part of speech describing the item. partItem :: FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant -> (Bool, Bool, MU.Part, MU.Part) partItem side factionD = partItemN side factionD False DetailMedium 4 partItemShort :: FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant -> (Bool, Bool, MU.Part, MU.Part) partItemShort side factionD = partItemN side factionD False DetailLow 4 partItemShortest :: FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant -> (Bool, Bool, MU.Part, MU.Part) partItemShortest side factionD = partItemN side factionD False DetailLow 0 partItemHigh :: FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant -> (Bool, Bool, MU.Part, MU.Part) partItemHigh side factionD = partItemN side factionD False DetailAll 100 -- The @count@ can be different than @itemK@ in @ItemFull@, e.g., when picking -- a subset of items to drop. partItemWsR :: FactionId -> FactionDict -> Bool -> Int -> Time -> ItemFull -> ItemQuant -> (Bool, MU.Part) partItemWsR side factionD ranged count localTime itemFull@ItemFull{itemKind} kit = let (temporary, unique, name, stats) = partItemN side factionD ranged DetailMedium 4 localTime itemFull kit tmpCondition = IK.isTmpCondition itemKind in ( temporary , if | temporary && count == 1 -> MU.Phrase [name, stats] | temporary -> MU.Phrase [MU.Text $ tshow count <> "-fold", name, stats] | unique && count == 1 -> MU.Phrase ["the", name, stats] | tmpCondition -> MU.Phrase [name, stats] | otherwise -> MU.Phrase [MU.CarWs count name, stats] ) partItemWs :: FactionId -> FactionDict -> Int -> Time -> ItemFull -> ItemQuant -> (Bool, MU.Part) partItemWs side factionD = partItemWsR side factionD False partItemWsRanged :: FactionId -> FactionDict -> Int -> Time -> ItemFull -> ItemQuant -> (Bool, MU.Part) partItemWsRanged side factionD = partItemWsR side factionD True partItemShortAW :: FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant -> MU.Part partItemShortAW side factionD localTime itemFull kit = let (_, unique, name, _) = partItemShort side factionD localTime itemFull kit in if unique then MU.Phrase ["the", name] else MU.AW name partItemMediumAW :: FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant -> MU.Part partItemMediumAW side factionD localTime itemFull kit = let (_, unique, name, stats) = partItemN side factionD False DetailMedium 100 localTime itemFull kit in if unique then MU.Phrase ["the", name, stats] else MU.AW $ MU.Phrase [name, stats] partItemShortWownW :: FactionId -> FactionDict -> MU.Part -> Time -> ItemFull -> ItemQuant -> MU.Part partItemShortWownW side factionD partA localTime itemFull kit = let (_, _, name, _) = partItemShort side factionD localTime itemFull kit in MU.WownW partA name viewItem :: ItemFull -> Color.AttrCharW32 {-# INLINE viewItem #-} viewItem itemFull = Color.attrChar2ToW32 (flavourToColor $ jflavour $ itemBase itemFull) (IK.isymbol $ itemKind itemFull) itemDesc :: Bool -> FactionId -> FactionDict -> Int -> CStore -> Time -> ItemFull -> ItemQuant -> AttrLine itemDesc markParagraphs side factionD aHurtMeleeOfOwner store localTime itemFull@ItemFull{itemBase, itemKind, itemSuspect} kit = let (_, unique, name, stats) = partItemHigh side factionD localTime itemFull kit nstats = makePhrase [name, stats] IK.ThrowMod{IK.throwVelocity, IK.throwLinger} = IK.getToThrow itemKind speed = speedFromWeight (IK.iweight itemKind) throwVelocity range = rangeFromSpeedAndLinger speed throwLinger tspeed | IK.isTmpCondition itemKind = "" | speed < speedLimp = "When thrown, it drops at once." | speed < speedWalk = "When thrown, it travels only one meter and drops immediately." | otherwise = "When thrown, it flies with speed of" <+> tshow (fromSpeed speed `div` 10) <> if throwLinger /= 100 then " m/s and range" <+> tshow range <+> "m." else " m/s." tsuspect = ["You are unsure what it does." | itemSuspect] (desc, featureSentences, damageAnalysis) = let sentences = tsuspect ++ mapMaybe featureToSentence (IK.ifeature itemKind) aHurtMeleeOfItem = IA.aHurtMelee $ aspectRecordFull itemFull meanDmg = ceiling $ Dice.meanDice (IK.idamage itemKind) dmgAn = if meanDmg <= 0 then "" else let multRaw = aHurtMeleeOfOwner + if store `elem` [CEqp, COrgan] then 0 else aHurtMeleeOfItem mult = 100 + min 99 (max (-99) multRaw) minDeltaHP = xM meanDmg `divUp` 100 rawDeltaHP = fromIntegral mult * minDeltaHP pmult = 100 + min 99 (max (-99) aHurtMeleeOfItem) prawDeltaHP = fromIntegral pmult * minDeltaHP pdeltaHP = modifyDamageBySpeed prawDeltaHP speed mDeltaHP = modifyDamageBySpeed minDeltaHP speed in "Against defenceless targets you would inflict around" -- rounding and non-id items <+> tshow meanDmg <> "*" <> tshow mult <> "%" <> "=" <> show64With2 rawDeltaHP <+> "melee damage (min" <+> show64With2 minDeltaHP <> ") and" <+> tshow meanDmg <> "*" <> tshow pmult <> "%" <> "*" <> "speed^2" <> "/" <> tshow (fromSpeed speedThrust `divUp` 10) <> "^2" <> "=" <> show64With2 pdeltaHP <+> "ranged damage (min" <+> show64With2 mDeltaHP <> ") with it" <> if Dice.minDice (IK.idamage itemKind) == Dice.maxDice (IK.idamage itemKind) then "." else "on average." in (IK.idesc itemKind, T.intercalate " " sentences, tspeed <+> dmgAn) weight = IK.iweight itemKind (scaledWeight, unitWeight) | weight > 1000 = (tshow $ fromIntegral weight / (1000 :: Double), "kg") | otherwise = (tshow weight, "g") onLevel = "on level" <+> tshow (abs $ fromEnum $ jlid itemBase) <> "." discoFirst = (if unique then "Discovered" else "First seen") <+> onLevel whose fid = gname (factionD EM.! fid) sourceDesc = case jfid itemBase of Just fid | IK.isTmpCondition itemKind -> "Caused by" <+> (if fid == side then "us" else whose fid) <> ". First observed" <+> onLevel Just fid -> "Coming from" <+> whose fid <> "." <+> discoFirst _ -> discoFirst colorSymbol = viewItem itemFull blurb = ((" " <> nstats <> (if markParagraphs then ":\n\n" else ": ") <> desc <> (if markParagraphs && not (T.null desc) then "\n\n" else "")) <+> (if weight > 0 then makeSentence ["Weighs around", MU.Text scaledWeight <> unitWeight] else "")) <+> featureSentences <+> sourceDesc <+> damageAnalysis in colorSymbol : textToAL blurb