Research:NPC AI Behaviour
General behaviour
Decision scan
Actions affected | AI decision check, every 5.0 seconds |
Description | The main point for actor behaviour selection |
Implementation status | |
Analysis status | Early, incomplete research |
Weight functions
actionGreetWeighting :: (npc, actor) -> weight
if npc is creature: return 0
if actor is not player: return 0
if player is incapacitated: return 0
if player is invisible or player.chameleon >= 75: return 0
if npc.hello == 0: return 0
if npc.currentAIPackage not in { AIWander, AITravel, AIPursue }: return 0
x = iGreetDistanceMultiplier * npc.hello
dist = distance(npc, actor)
if npc has already greeted and dist > 2 * x:
npc resets greeting flag
npc.target = none
elif npc has not greeted and dist < x:
npc.target = actor
return 100
else:
return 0
actionCrimeWeighting :: (npc, actor) -> weight
if npc is creature: return 0
if npc is incapacitated: return 0
if npc.alarm == 0: return 0
if not npc.isGuard: return 0
if actor is not player: return 0
if player is incapacitated: return 0
if player is invisible or player.chameleon >= 75: return 0
if player is resisting arrest: return 0
if bounty < iCrimeThreshold: return 0
weight = npc.alarm + 0.1 * bounty
if weight < 100: return 0
return weight
actionFightWeighting :: (npc, actor) -> weight
if npc is in combat with actor: return 100
npc resets in-combat flag and attack state
if actor is invisible or actor.chameleon >= 75: return 0
if actor is npc or npc ally: return 0
if actor is in combat with npc ally: return 100
if npc is npc and npc.isGuard and actor is in combat:
if actor is creature and has no allies: return 100
if actor.isWerewolf or (actor is player and player.isKnownWerewolf): return 100
if npc object has unknown flag and actor is player with a death warrant and the game is not in chargen state: return 100
if target is player or player ally:
fightTerm = npc.fight
if actor.isWerewolf or (actor is player and player.isKnownWerewolf): fightTerm += iWerewolfFightMod
dist = distance(npc, actor)
if npc.isFlying or npc.isSwimming: dist -= absHeightDiff(npc, actor)
fightTerm += iFightDistanceBase - fFightDistanceMultiplier * dist
if npc is not creature:
fightTerm += fFightDispMult * (50 - npc.disposition)
if fightTerm >= 100: return fightTerm
return 0
actionLookAtWeighting :: (npc, actor) -> weight
if actor is incapacitated: return 0
if actor is invisible or player.chameleon >= 75: return 0
if npc current AI package prevents proximity check: return 0 # unclear conditions
if distance(npc, actor) >= 640: return 0
if actor is an npc escort: return 0
if actor is being escorted by npc: return 0
return 100
Actor scan
For a single actor, labelled npc
actionWeight = 0
selectedAction = none
for each actor in environment: # player is examined first
if actor is dead: continue
if distance(npc, actor) > 7168: continue
# 5-10 more conditions
greetWeight = 0
crimeWeight = 0
if actor is player:
if npc meets condition x: # under research
greetWeight = actionGreetWeighting(npc, actor)
if npc meets condition y: # under research
crimeWeight = actionCrimeWeighting(npc, actor)
fightWeight = actionFightWeighting(npc, actor)
lookAtWeight = actionLookAtWeighting(npc, actor)
action = none
if lookAtWeight > actionWeight:
npc.target = actor
actionWeight = int(lookAtWeight)
action = lookAt
if greetWeight > actionWeight:
npc.target = actor
actionWeight = int(greetWeight)
action = greet
if fightWeight > actionWeight:
npc.target = actor
actionWeight = int(fightWeight)
action = fight
if crimeWeight > actionWeight:
npc.target = actor
actionWeight = int(crimeWeight)
action = uphold the law
if actionWeight != 0 and action != none and npc.target != none:
if some 20 page function: # includes expensive functions like line of sight and sneak
npc.target = 0
actionWeight = 0
action = none
if action != none: selectedAction = action
if action != none: npc switches to selectedAction
Comments
With the lookAt action, the NPC looks at its target's head. The anim controller blends the head pose over time.
Idle behaviour
Shielding face from storm
Actions affected | Every frame. |
Description | Part of AI animation behaviour. |
Implementation status | |
Analysis status | Verified |
For a non-werewolf humanoid:
facing = Y column of orientation matrix (forward facing vector)
windVelocity = interpolated wind velocity (see Weather page)
windSpeed = length(windVelocity)
if windSpeed > fStromWindSpeed and not (actor.weaponDrawn or actor.spellReadied):
f = normalize(facing)
w = normalize(windVelocity)
fAlt = (-f.x, f.y, f.z)
if dot(fAlt, w) < -0.5:
if upper body animation is not IdleStorm:
immediately change upper body animation to IdleStorm, animation loops
else:
if upper body animation is IdleStorm:
change IdleStorm loop mode to play once until end key
set next upper body animation to standard idle for player state
Comments
The use of fAlt is inexplicable. The test should probably be dot(f, w) > 0.5
. The storm idle applies to the upper body bone group, and has higher priority than movement animations, but lower priority than weapon animations.
Combat behaviour
NPC awareness check
Actions affected | AI decision check, every 5.0 seconds |
Description | Part of the NPC perception state. |
Implementation status | implemented |
Analysis status | Verified |
This check runs every 5 seconds for each NPC. It occurs whether you are sneaking or not, but isn't the same as the combat distance check.
Player side
if sneaking:
sneakTerm = fSneakSkillMult * sneak + 0.2 * agility + 0.1 * luck + bootWeight * fSneakBootMult
else:
sneakTerm = 0
fatigueTerm = fFatigueBase - fFatigueMult*(1 - normalisedFatigue)
where normalisedFatigue is a function of fatigue. empty fatigue bar -> 0.0, full fatigue bar -> 1.0
distTerm = fSneakDistanceBase + fSneakDistanceMultiplier*dist
x = sneakTerm * distTerm * fatigueTerm + chameleon (+ 100 if invisible)
NPC side
npcTerm = npcSneak + 0.2 * npcAgility + 0.1 * npcLuck - npcBlind
npcFatigueTerm = fFatigueBase - fFatigueMult * (1 - normalisedFatigue)
using NPC normalisedFatigue
if PC is behind NPC (180 degrees):
y = npcTerm * npcFatigueTerm * fSneakNoViewMult
else:
y = npcTerm * npcFatigueTerm * fSneakViewMult
Final check
target = x - y
roll 100, win if roll < target
Comments
Appears straightforward and bug-free. NPCs can take up to five seconds to notice you even if you are not sneaking. This function precedes the combat distance check. I have not identified if there is a line of sight check occuring before or after.
According to this formula, NPCs can still detect you when invisible? That seems wrong. Scrawl (talk) 00:25, 7 January 2014 (CET)
Attack frequency
Actions affected | Upon initiating combat, and after each swing/missile |
Description | The semi-random delay inserted between physical attacks. |
Implementation status | Implemented |
Analysis status | Function contains other side effects |
if actor is an npc:
baseDelay = fCombatDelayNPC
else:
baseDelay = fCombatDelayCreature
delay = min(baseDelay + 0.01 * rand 100, baseDelay + 0.9)
if actor is in range to attack with current weapon:
actor will initiate a swing/fire a missile if time since end of last attack >= delay
AI Tendencies
Fight
Affected by calm and frenzy spells.
Flee
Affected by rally and demoralize spells.