1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
|
-- | Common client monad operations.
module Game.LambdaHack.Client.CommonM
( getPerFid, aidTgtToPos, makeLine
, currentSkillsClient, pickWeaponClient
, updateSalter, createSalter
) where
import Prelude ()
import Game.LambdaHack.Core.Prelude
import qualified Data.EnumMap.Strict as EM
import Game.LambdaHack.Client.MonadClient
import Game.LambdaHack.Client.Request
import Game.LambdaHack.Client.State
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.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.Types
import Game.LambdaHack.Common.Vector
import Game.LambdaHack.Content.RuleKind
import Game.LambdaHack.Content.TileKind (TileKind, isUknownSpace)
import Game.LambdaHack.Core.Random
import qualified Game.LambdaHack.Definition.Ability as Ability
import Game.LambdaHack.Definition.Defs
-- | Get the current perception of a client.
getPerFid :: MonadClientRead m => LevelId -> m Perception
getPerFid lid = do
fper <- getsClient sfper
let assFail = error $ "no perception at given level"
`showFailure` (lid, fper)
return $! EM.findWithDefault assFail lid fper
-- | Calculate the position of an actor's target.
-- This matches @pathGoal@, but sometimes path is not defined.
aidTgtToPos :: Maybe ActorId -> LevelId -> Maybe Target -> State -> Maybe Point
aidTgtToPos _ _ Nothing _ = Nothing
aidTgtToPos maid lidV (Just tgt) s = case tgt of
TEnemy a ->
let body = getActorBody a s
in if blid body == lidV then Just (bpos body) else Nothing
TNonEnemy a ->
let body = getActorBody a s
in if blid body == lidV then Just (bpos body) else Nothing
TPoint _ lid p ->
if lid == lidV then Just p else Nothing
TVector v -> case maid of
Nothing -> Nothing
Just aid ->
let COps{corule=RuleContent{rWidthMax, rHeightMax}} = scops s
b = getActorBody aid s
shifted = shiftBounded rWidthMax rHeightMax (bpos b) v
in if shifted == bpos b && v /= Vector 0 0 then Nothing else Just shifted
-- | Counts the number of steps until the projectile would hit a non-projectile
-- 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.
-- Treats unknown tiles as walkable, but prefers known.
makeLine :: Bool -> Actor -> Point -> Int -> COps -> Level -> Maybe Int
makeLine onlyFirst body fpos epsOld cops lvl =
let COps{coTileSpeedup} = cops
dist = chessDist (bpos body) fpos
calcScore :: Int -> Int
calcScore eps = case bresenhamsLineAlgorithm eps (bpos body) fpos of
Just bl ->
let blDist = take (dist - 1) bl -- goal not checked; actor well aware
noActor p = p == fpos || not (occupiedBigLvl p lvl)
accessibleUnknown tpos =
let tt = lvl `at` tpos
in Tile.isWalkable coTileSpeedup tt || isUknownSpace tt
accessU = all noActor blDist
&& all accessibleUnknown blDist
accessFirst | not onlyFirst = False
| otherwise =
all noActor (take 1 blDist)
&& all accessibleUnknown (take 1 blDist)
nUnknown = length $ filter (isUknownSpace . (lvl `at`)) blDist
in if | accessU -> - nUnknown
| accessFirst -> -10000
| otherwise -> minBound
Nothing -> error $ "" `showFailure` (body, fpos, epsOld)
tryLines :: Int -> (Maybe Int, Int) -> Maybe Int
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
in if | dist <= 0 -> Nothing -- ProjectAimOnself
| calcScore epsOld > minBound -> Just epsOld -- keep old
| otherwise -> tryLines (epsOld + 1) (Nothing, minBound) -- find best
-- @MonadStateRead@ would be enough, but the logic is sound only on client.
currentSkillsClient :: MonadClientRead m => ActorId -> m Ability.Skills
currentSkillsClient aid = do
body <- getsState $ getActorBody aid
side <- getsClient sside
-- Newest Leader in sleader, not yet in sfactionD.
mleader <- if bfid body == side
then getsClient sleader
else do
fact <- getsState $ (EM.! bfid body) . sfactionD
return $! gleader fact
getsState $ actorCurrentSkills mleader aid -- keep it lazy
-- Client has to choose the weapon based on its partial knowledge,
-- because if server chose it, it would leak item discovery information.
--
-- Note that currently the aspects of the target actor are not considered,
-- because all weapons share the sum of all source actor aspects and only differ
-- in damage (equally important for all targets) and effects (really hard
-- to tell which is better for which target or even which is better
-- for the same target, so it's random). If only individual weapon's +toHit
-- was applied to the target, situation would be much more complex,
-- which is precisely why we keep it as is and let the player make choices
-- by equipping and unequipping weapons instead. Content should ensure
-- that the rule of thumb (which AI uses) that more weapons is better
-- should give good results almost always, at least at the start of the game,
-- to limit micromanagement and to spare newbies.
--
-- Note that situation is completely different with choosing projectiles
-- against a particular foe, even before (potential) splash damage
-- that hits multiple tagets comes into the equation. AI has to be very
-- primitive and random here as well.
pickWeaponClient :: MonadClient m
=> ActorId -> ActorId -> m (Maybe RequestTimed)
pickWeaponClient source target = do
eqpAssocs <- getsState $ kitAssocs source [CEqp]
bodyAssocs <- getsState $ kitAssocs source [COrgan]
actorSk <- currentSkillsClient source
tb <- getsState $ getActorBody target
let kitAssRaw = eqpAssocs ++ bodyAssocs
kitAss = filter (IA.checkFlag Ability.Meleeable
. aspectRecordFull . fst . snd) kitAssRaw
benign itemFull = let arItem = aspectRecordFull itemFull
in IA.checkFlag Ability.Benign arItem
discoBenefit <- getsClient sdiscoBenefit
strongest <- pickWeaponM False (Just discoBenefit) kitAss actorSk source
case strongest of
[] -> return Nothing
(_, _, _, _, _, (itemFull, _)) : _ | benign itemFull && bproj tb ->
return Nothing -- if strongest is benign, don't waste fun on a projectile
iis@(ii1@(value1, hasEffect1, timeout1, _, _, (itemFull1, _)) : _) -> do
let minIis = takeWhile (\(value, hasEffect, timeout, _, _, _) ->
value == value1
&& hasEffect == hasEffect1
&& timeout == timeout1)
iis
-- Randomize only the no-timeout items. Others need to activate
-- in the order shown in HUD and also not risk of only one always used.
(_, _, _, _, iid, _) <- if timeout1 > 0 || itemSuspect itemFull1
then return ii1
else rndToAction $ oneOf minIis
-- 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 $ Just $ ReqMelee target iid cstore
updateSalter :: MonadClient m
=> LevelId -> [(Point, ContentId TileKind)] -> m ()
updateSalter lid pts = do
COps{coTileSpeedup} <- getsState scops
let pas = map (second $ toEnum . Tile.alterMinWalk coTileSpeedup) pts
f = (PointArray.// pas)
modifyClient $ \cli -> cli {salter = EM.adjust f lid $ salter cli}
createSalter :: State -> AlterLid
createSalter s =
let COps{coTileSpeedup} = scops s
f Level{ltile} =
PointArray.mapA (toEnum . Tile.alterMinWalk coTileSpeedup) ltile
in EM.map f $ sdungeon s
|