module Game.LambdaHack.Client.UI.ItemDescription
( partItem, partItemShort, partItemShortest, partItemHigh, partItemWs
, partItemWsRanged, partItemShortAW, partItemMediumAW, partItemShortWownW
, viewItem, itemDesc
#ifdef EXPOSE_INTERNAL
, partItemN, textAllPowers, partItemWsR
#endif
) where
import Prelude ()
import Game.LambdaHack.Core.Prelude
import qualified Data.EnumMap.Strict as EM
import qualified Data.Text as T
import qualified NLP.Miniutter.English as MU
import Game.LambdaHack.Client.UI.EffectDescription
import Game.LambdaHack.Client.UI.Overlay
import Game.LambdaHack.Common.Faction
import Game.LambdaHack.Common.Item
import qualified Game.LambdaHack.Common.ItemAspect as IA
import Game.LambdaHack.Common.Misc
import Game.LambdaHack.Common.Time
import Game.LambdaHack.Common.Types
import qualified Game.LambdaHack.Content.ItemKind as IK
import qualified Game.LambdaHack.Core.Dice as Dice
import qualified Game.LambdaHack.Definition.Ability as Ability
import qualified Game.LambdaHack.Definition.Color as Color
import Game.LambdaHack.Definition.Defs
import Game.LambdaHack.Definition.Flavour
partItemN :: FactionId -> FactionDict -> Bool -> DetailLevel -> Int
-> Time -> ItemFull -> ItemQuant
-> (MU.Part, MU.Part)
partItemN side factionD ranged detailLevel maxWordsToShow localTime
itemFull@ItemFull{itemBase, itemKind, itemSuspect}
(itemK, itemTimer) =
let flav = flavourToName $ jflavour itemBase
arItem = aspectRecordFull itemFull
timeout = IA.aTimeout arItem
temporary = IA.checkFlag Ability.Fragile arItem
&& IA.checkFlag Ability.Periodic arItem
lenCh = itemK - ncharges localTime itemFull (itemK, itemTimer)
charges | lenCh == 0 || temporary = ""
| itemK == 1 && lenCh == 1 = "(charging)"
| itemK == lenCh = "(all charging)"
| otherwise = "(" <> tshow lenCh <+> "charging)"
skipRecharging = detailLevel <= DetailLow && lenCh >= itemK
(powerTsRaw, rangedDamage) =
textAllPowers detailLevel skipRecharging itemFull
powerTs = powerTsRaw ++ 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 powerTs
++ ["(...)" | length powerTs > maxWordsToShow && maxWordsToShow > 0]
++ [charges | maxWordsToShow > 1]
name | temporary =
let adj = if timeout == 0 then "temporarily" else "impermanent"
in adj <+> IK.iname itemKind
| itemSuspect = flav <+> IK.iname itemKind
| otherwise = IK.iname itemKind
capName = if IA.checkFlag Ability.Unique arItem
then MU.Capitalize $ MU.Text name
else MU.Text name
in (capName, MU.Phrase $ map MU.Text ts)
textAllPowers :: DetailLevel -> Bool -> ItemFull -> ([Text], [Text])
textAllPowers detailLevel skipRecharging
itemFull@ItemFull{itemKind, itemDisco} =
let arItem = aspectRecordFull itemFull
aspectsFull = case itemDisco of
ItemDiscoMean IA.KindMean{..} | kmConst ->
IA.aspectRecordToList kmMean
ItemDiscoMean{} -> IK.iaspects itemKind
ItemDiscoFull iAspect -> IA.aspectRecordToList iAspect
mtimeout = find IK.timeoutAspect aspectsFull
elab = IA.aELabel arItem
periodic = IA.checkFlag Ability.Periodic arItem
hurtMeleeAspect :: IK.Aspect -> Bool
hurtMeleeAspect (IK.AddSkill Ability.SkHurtMelee _) = True
hurtMeleeAspect _ = False
active = IA.goesIntoEqp arItem
splitA :: DetailLevel -> [IK.Aspect] -> [Text]
splitA detLev aspects =
let ppA = kindAspectToSuffix
ppE = effectToSuffix detLev
reduce_a = maybe "?" tshow . Dice.reduceDice
restEs | detLev >= DetailHigh
|| not (IA.checkFlag Ability.MinorEffects arItem) =
IK.ieffects itemKind
| otherwise = []
(smashEffs, noSmashEffs) = partition IK.onSmashEffect restEs
unSmash (IK.OnSmash eff) = eff
unSmash eff = eff
onSmashTs = T.intercalate " " $ filter (not . T.null)
$ map (ppE . unSmash) smashEffs
rechargingTs = T.intercalate " "
$ [damageText | IK.idamage itemKind /= 0]
++ filter (not . T.null) (map ppE noSmashEffs)
fragile = IA.checkFlag Ability.Fragile arItem
periodicText =
if periodic && not skipRecharging && not (T.null rechargingTs)
then case (mtimeout, fragile) of
(Nothing, True) ->
"(each turn until gone:" <+> rechargingTs <> ")"
(Nothing, False) ->
"(each turn:" <+> rechargingTs <> ")"
(Just (IK.Timeout t), True) ->
"(every" <+> reduce_a t <+> "until gone:"
<+> rechargingTs <> ")"
(Just (IK.Timeout t), False) ->
"(every" <+> reduce_a t <> ":" <+> rechargingTs <> ")"
_ -> error $ "" `showFailure` mtimeout
else ""
ppERestEs = if periodic then [periodicText] else map ppE noSmashEffs
aes = if active
then map ppA aspects ++ ppERestEs
else ppERestEs ++ map ppA aspects
onSmash = if T.null onSmashTs then ""
else "(on smash:" <+> onSmashTs <> ")"
damageText = case find hurtMeleeAspect aspects of
Just (IK.AddSkill Ability.SkHurtMelee 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)
timeoutText = case mtimeout of
Nothing -> ""
Just (IK.Timeout t) -> "(cooldown" <+> reduce_a t <> ")"
_ -> error $ "" `showFailure` mtimeout
in [ damageText
| detLev > DetailNone && (not periodic || IK.idamage itemKind == 0) ]
++ [timeoutText | detLev > DetailNone && not periodic]
++ if detLev >= DetailMedium
then aes ++ [onSmash | detLev >= DetailAll]
else []
hurtMult = armorHurtCalculation True (IA.aSkills arItem)
Ability.zeroSkills
dmg = Dice.meanDice $ IK.idamage itemKind
rawDeltaHP = ceiling $ fromIntegral hurtMult * xD dmg / 100
IK.ThrowMod{IK.throwVelocity} = IA.aToThrow arItem
speed = speedFromWeight (IK.iweight itemKind) throwVelocity
pdeltaHP = modifyDamageBySpeed rawDeltaHP speed
rangedDamageDesc = if pdeltaHP == 0
then []
else ["{avg" <+> show64With2 pdeltaHP <+> "ranged}"]
splitTry ass =
let splits = map (`splitA` ass) [minBound..maxBound]
splitsToTry = drop (fromEnum detailLevel) splits
splitsValid | T.null elab = filter (/= []) splitsToTry
| otherwise = splitsToTry
in concat $ take 1 splitsValid
aspectDescs =
let aMain IK.AddSkill{} = True
aMain _ = False
(aspectsMain, aspectsAux) = partition aMain aspectsFull
in filter (/= "")
$ elab
: splitTry aspectsMain
++ if detailLevel >= DetailAll
then map kindAspectToSuffix aspectsAux
else []
in (aspectDescs, rangedDamageDesc)
partItem :: FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant
-> (MU.Part, MU.Part)
partItem side factionD = partItemN side factionD False DetailMedium 4
partItemShort :: FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant
-> (MU.Part, MU.Part)
partItemShort side factionD = partItemN side factionD False DetailLow 4
partItemShortest :: FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant
-> (MU.Part, MU.Part)
partItemShortest side factionD = partItemN side factionD False DetailNone 1
partItemHigh :: FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant
-> (MU.Part, MU.Part)
partItemHigh side factionD = partItemN side factionD False DetailAll 100
partItemWsR :: FactionId -> FactionDict -> Bool -> Int -> Time -> ItemFull
-> ItemQuant
-> MU.Part
partItemWsR side factionD ranged count localTime itemFull kit =
let (name, powers) =
partItemN side factionD ranged DetailMedium 4 localTime itemFull kit
arItem = aspectRecordFull itemFull
periodic = IA.checkFlag Ability.Periodic arItem
condition = IA.checkFlag Ability.Condition arItem
maxCount = Dice.supDice $ IK.icount $ itemKind itemFull
in if | condition && count == 1 -> MU.Phrase [name, powers]
| condition && not periodic && maxCount > 1 ->
let percent = 100 * count `divUp` maxCount
amount = tshow count <> "-strong"
<+> "(" <> tshow percent <> "%)"
in MU.Phrase [MU.Text amount, name, powers]
| condition ->
MU.Phrase [MU.Text $ tshow count <> "-fold", name, powers]
| IA.checkFlag Ability.Unique arItem && count == 1 ->
MU.Phrase ["the", name, powers]
| otherwise -> MU.Phrase [MU.CarAWs count name, powers]
partItemWs :: FactionId -> FactionDict -> Int -> Time -> ItemFull -> ItemQuant
-> MU.Part
partItemWs side factionD = partItemWsR side factionD False
partItemWsRanged :: FactionId -> FactionDict -> Int -> Time -> ItemFull
-> ItemQuant
-> MU.Part
partItemWsRanged side factionD = partItemWsR side factionD True
partItemShortAW :: FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant
-> MU.Part
partItemShortAW side factionD localTime itemFull kit =
let (name, _) = partItemShort side factionD localTime itemFull kit
arItem = aspectRecordFull itemFull
in if IA.checkFlag Ability.Unique arItem
then MU.Phrase ["the", name]
else MU.AW name
partItemMediumAW :: FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant
-> MU.Part
partItemMediumAW side factionD localTime itemFull kit =
let (name, powers) =
partItemN side factionD False DetailMedium 100 localTime itemFull kit
arItem = aspectRecordFull itemFull
in if IA.checkFlag Ability.Unique arItem
then MU.Phrase ["the", name, powers]
else MU.AW $ MU.Phrase [name, powers]
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 -> LevelId
-> ItemFull -> ItemQuant
-> AttrLine
itemDesc markParagraphs side factionD aHurtMeleeOfOwner store localTime jlid
itemFull@ItemFull{itemBase, itemKind, itemDisco, itemSuspect} kit =
let (name, powers) = partItemHigh side factionD localTime itemFull kit
arItem = aspectRecordFull itemFull
npowers = makePhrase [name, powers]
IK.ThrowMod{IK.throwVelocity, IK.throwLinger} = IA.aToThrow arItem
speed = speedFromWeight (IK.iweight itemKind) throwVelocity
range = rangeFromSpeedAndLinger speed throwLinger
tspeed | IA.checkFlag Ability.Condition arItem
|| IK.iweight itemKind == 0 = ""
| speed < speedLimp = "When thrown, it drops at once."
| speed < speedWalk = "When thrown, it drops after one meter."
| otherwise =
"Can be thrown at"
<+> T.pack (displaySpeed $ fromSpeed speed)
<> if throwLinger /= 100
then " dropping after" <+> tshow range <> "m."
else "."
tsuspect = ["You are unsure what it does." | itemSuspect]
(desc, aspectSentences, damageAnalysis) =
let aspects = case itemDisco of
ItemDiscoMean IA.KindMean{..} | kmConst ->
IA.aspectRecordToList kmMean
ItemDiscoMean{} -> IK.iaspects itemKind
ItemDiscoFull iAspect -> IA.aspectRecordToList iAspect
sentences = tsuspect ++ mapMaybe aspectToSentence aspects
aHurtMeleeOfItem = IA.getSkill Ability.SkHurtMelee arItem
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 foes you'd inflict around"
<+> 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.infDice (IK.idamage itemKind)
== Dice.supDice (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) <> "."
discoFirst = (if IA.checkFlag Ability.Unique arItem
then "Discovered"
else "First seen")
<+> onLevel
whose fid = gname (factionD EM.! fid)
sourceDesc =
case jfid itemBase of
Just fid | IA.checkFlag Ability.Condition arItem ->
"Caused by" <+> (if fid == side then "us" else whose fid)
<> ". First observed" <+> onLevel
Just fid ->
"Coming from" <+> whose fid
<> "." <+> discoFirst
_ -> discoFirst
ikitNames = map (fromGroupName . fst) $ filter ((== COrgan) . snd)
$ IK.ikit itemKind
ikitDesc | null ikitNames = ""
| otherwise = makeSentence
[ "the actor also has organs of this kind:"
, MU.Text $ T.intercalate ", " ikitNames ]
colorSymbol = viewItem itemFull
blurb =
((" "
<> npowers
<> (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 ""))
<+> aspectSentences
<+> sourceDesc
<+> damageAnalysis
<> (if markParagraphs && not (T.null ikitDesc) then "\n\n" else "\n")
<> ikitDesc
in colorSymbol : textToAL blurb