5 using FarseerPhysics.Dynamics;
6 using Microsoft.Xna.Framework;
8 using System.Collections.Generic;
14 public enum AIState {
Idle,
Attack,
Escape,
Eat,
Flee,
Avoid,
Aggressive,
PassiveAggressive,
Protect,
Observe,
Freeze,
Follow,
FleeTo,
Patrol }
34 get {
return _state; }
37 if (_state == value) {
return; }
57 private readonly
float updateTargetsInterval = 1;
58 private readonly
float updateMemoriesInverval = 1;
59 private readonly
float attackLimbSelectionInterval = 3;
61 private const float minPriority = 10;
66 private float updateTargetsTimer;
67 private float updateMemoriesTimer;
68 private float attackLimbSelectionTimer;
73 private float Sight =>
AIParams.Sight;
74 private float Hearing =>
AIParams.Hearing;
75 private float FleeHealthThreshold =>
AIParams.FleeHealthThreshold;
76 private bool IsAggressiveBoarder =>
AIParams.AggressiveBoarding;
80 private Limb _attackLimb;
81 private Limb _previousAttackLimb;
84 get {
return _attackLimb; }
87 if (_attackLimb != value)
89 _previousAttackLimb = _attackLimb;
102 private double lastAttackUpdateTime;
104 private Attack _activeAttack;
109 if (_activeAttack ==
null) {
return null; }
110 return lastAttackUpdateTime > Timing.TotalTime - _activeAttack.
Duration ? _activeAttack :
null;
114 _activeAttack = value;
115 lastAttackUpdateTime = Timing.TotalTime;
121 private float targetValue;
124 private Dictionary<AITarget, AITargetMemory> targetMemories;
126 private readonly
int requiredHoleCount;
127 private bool canAttackWalls;
129 private bool canAttackDoors;
130 private bool canAttackCharacters;
133 private readonly
float priorityFearIncreasement = 2;
134 private readonly
float memoryFadeTime = 0.5f;
136 private float avoidTimer;
137 private float observeTimer;
138 private float sweepTimer;
139 private float circleRotation;
140 private float circleDir;
141 private bool inverseDir;
142 private bool breakCircling;
143 private float circleRotationSpeed;
144 private Vector2 circleOffset;
145 private float circleFallbackDistance;
146 private float strikeTimer;
147 private float aggressionIntensity;
149 private float currentAttackIntensity;
153 private readonly List<Body> myBodies;
166 return target !=
null && target.Priority > 0.0f && (target.State ==
AIState.Attack || target.State ==
AIState.Aggressive);
174 var target = GetTargetParams(
"room");
175 return target !=
null && target.Priority > 0.0f && (target.State ==
AIState.Attack || target.State ==
AIState.Aggressive);
208 } =
new HashSet<Submarine>();
215 private static bool IsTargetInPlayerTeam(
AITarget target) => target?.Entity?.Submarine !=
null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is
Character targetCharacter && targetCharacter.
IsOnPlayerTeam;
217 private bool IsAttackingOwner(
Character other) =>
224 private bool reverse;
227 get {
return reverse; }
238 private readonly
float maxSteeringBuffer = 5000;
239 private readonly
float minSteeringBuffer = 500;
240 private readonly
float steeringBufferIncreaseSpeed = 100;
241 private float steeringBuffer;
247 throw new Exception($
"Tried to create an enemy ai controller for human!");
250 targetMemories =
new Dictionary<AITarget, AITargetMemory>();
255 List<XElement> aiElements =
new List<XElement>();
256 List<float> aiCommonness =
new List<float>();
257 foreach (var element
in mainElement.Elements())
259 if (!element.Name.ToString().Equals(
"ai", StringComparison.OrdinalIgnoreCase)) {
continue; }
260 aiElements.Add(element);
261 aiCommonness.Add(element.GetAttributeFloat(
"commonness", 1.0f));
264 if (aiElements.Count == 0)
266 DebugConsole.ThrowError(
"Error in file \"" + c.Params.File.Path +
"\" - no AI element found.",
267 contentPackage: c.Prefab?.ContentPackage);
275 XElement aiElement = aiElements.Count == 1 ? aiElements[0] : ToolBox.SelectWeightedRandom(aiElements, aiCommonness, random);
276 foreach (var subElement
in aiElement.Elements())
278 switch (subElement.Name.ToString().ToLowerInvariant())
281 var subElements = subElement.Elements();
282 if (subElements.Any())
284 LoadSubElement(subElements.ToArray().GetRandom(random));
288 LoadSubElement(subElement);
293 void LoadSubElement(XElement subElement)
295 switch (subElement.Name.ToString().ToLowerInvariant())
301 case "swarmbehavior":
334 if (_aiParams ==
null)
337 if (_aiParams ==
null)
339 DebugConsole.ThrowError($
"No AI Params defined for {Character.SpeciesName}. AI disabled.",
351 private Identifier GetTargetingTag(
AITarget aiTarget)
353 if (aiTarget?.
Entity ==
null) {
return Identifier.Empty; }
354 Identifier targetingTag = Identifier.Empty;
357 if (targetCharacter.IsDead)
359 targetingTag =
"dead".ToIdentifier();
361 else if (
AIParams.TryGetTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold >=
Character.
GetDamageDoneByAttacker(targetCharacter))
363 targetingTag = tp.Tag;
367 targetingTag =
"owner".ToIdentifier();
371 targetingTag =
"hostile".ToIdentifier();
373 else if (
AIParams.TryGetTarget(targetCharacter, out CharacterParams.TargetParams tP))
375 targetingTag = tP.Tag;
383 targetingTag =
"pet".ToIdentifier();
385 else if (targetCharacter.IsHusk &&
AIParams.HasTag(
"husk"))
387 targetingTag =
"husk".ToIdentifier();
393 targetingTag =
"stronger".ToIdentifier();
397 targetingTag =
"weaker".ToIdentifier();
401 targetingTag =
"equal".ToIdentifier();
406 else if (aiTarget.
Entity is Item targetItem)
408 foreach (var prio
in AIParams.Targets)
410 if (targetItem.HasTag(prio.Tag))
412 targetingTag = prio.Tag;
416 if (targetingTag.IsEmpty)
418 if (targetItem.GetComponent<
Sonar>() !=
null)
420 targetingTag =
"sonar".ToIdentifier();
422 if (targetItem.GetComponent<
Door>() !=
null)
424 targetingTag =
"door".ToIdentifier();
428 else if (aiTarget.
Entity is Structure)
430 targetingTag =
"wall".ToIdentifier();
432 else if (aiTarget.
Entity is Hull)
434 targetingTag =
"room".ToIdentifier();
444 selectedTargetMemory = GetTargetMemory(target, addIfNotFound:
true);
445 selectedTargetMemory.
Priority = priority;
446 ignoredTargets.Remove(target);
449 private float movementMargin;
451 private void ReleaseDragTargets()
460 public override void Update(
float deltaTime)
463 base.Update(deltaTime);
464 UpdateTriggers(deltaTime);
473 if (currPath !=
null && currPath.CurrentNode !=
null)
480 ignorePlatforms = height < allowedJumpHeight;
508 stateResetTimer -= deltaTime;
509 if (stateResetTimer <= 0)
511 ResetOriginalState();
515 if (targetIgnoreTimer > 0)
517 targetIgnoreTimer -= deltaTime;
521 ignoredTargets.Clear();
522 targetIgnoreTimer = targetIgnoreTime;
524 avoidTimer -= deltaTime;
529 UpdateCurrentMemoryLocation();
530 if (updateMemoriesTimer > 0)
532 updateMemoriesTimer -= deltaTime;
536 FadeMemories(updateMemoriesInverval);
537 updateMemoriesTimer = updateMemoriesInverval;
544 target = GetOwner(targetItem);
546 bool shouldFlee =
false;
563 if (updateTargetsTimer > 0)
565 updateTargetsTimer -= deltaTime;
567 else if (avoidTimer <= 0 || activeTriggers.Any() && returnTimer <= 0)
570 updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f);
575 else if (targetingParams !=
null)
577 selectedTargetingParams = targetingParams;
578 State = targetingParams.State;
583 UpdateWallTarget(requiredHoleCount);
597 insideSteering.
Reset();
600 steeringBuffer += steeringBufferIncreaseSpeed * deltaTime;
606 outsideSteering.
Reset();
609 steeringBuffer = minSteeringBuffer;
611 steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer);
620 insideSteering.
Reset();
628 outsideSteering.
Reset();
642 UpdateIdle(deltaTime);
645 UpdatePatrol(deltaTime);
649 UpdateAttack(deltaTime);
652 UpdateEating(deltaTime);
660 case AIState.PassiveAggressive:
669 if (attackLimb !=
null && squaredDistance <= Math.Pow(attackLimb.attack.Range, 2))
678 UpdateAttack(deltaTime);
683 bool isBeingChased = IsBeingChased;
684 float reactDistance = !isBeingChased && selectedTargetingParams !=
null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(
SelectedAiTarget);
685 if (squaredDistance <= Math.Pow(reactDistance, 2))
687 float halfReactDistance = reactDistance / 2;
688 float attackDistance = selectedTargetingParams !=
null && selectedTargetingParams.AttackDistance > 0 ? selectedTargetingParams.AttackDistance : halfReactDistance;
689 if (
State ==
AIState.Aggressive ||
State ==
AIState.PassiveAggressive && squaredDistance < Math.Pow(attackDistance, 2))
692 UpdateAttack(deltaTime);
696 run = isBeingChased || squaredDistance < Math.Pow(halfReactDistance, 2);
698 avoidTimer =
AIParams.AvoidTime * 0.5f * Rand.Range(0.75f, 1.25f);
703 UpdateIdle(deltaTime);
725 if (targetCharacter.IsSameSpeciesOrGroup(c)) {
return false; }
731 return a.Damage >= selectedTargetingParams.Threshold;
736 if (attacker?.AiTarget !=
null)
738 ChangeTargetState(attacker,
AIState.Attack, selectedTargetingParams.Priority * 2);
741 UpdateWallTarget(requiredHoleCount);
748 Vector2 offset = Vector2.Zero;
749 if (selectedTargetingParams !=
null)
751 if (selectedTargetingParams.ReactDistance > 0)
753 reactDist = selectedTargetingParams.ReactDistance;
755 offset = selectedTargetingParams.Offset;
757 if (offset != Vector2.Zero)
759 reactDist += offset.Length();
761 if (sqrDist > MathUtils.Pow2(reactDist + movementMargin))
763 movementMargin =
State ==
AIState.FleeTo ? 0 : reactDist;
765 UpdateFollow(deltaTime);
769 movementMargin = MathHelper.Clamp(movementMargin -= deltaTime, 0, reactDist);
780 float rotation = item.Rotation;
794 if (!CoroutineManager.IsCoroutineRunning(disableTailCoroutine))
796 disableTailCoroutine = CoroutineManager.Invoke(() =>
800 Character.AnimController.HideAndDisable(LimbType.Tail, ignoreCollisions: false);
809 new Vector2(0, -1), footMoveForce: 1);
814 UpdateIdle(deltaTime);
826 reactDist = selectedTargetingParams !=
null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(
SelectedAiTarget);
827 float halfReactDist = reactDist / 2;
828 float attackDist = selectedTargetingParams !=
null && selectedTargetingParams.AttackDistance > 0 ? selectedTargetingParams.AttackDistance : halfReactDist;
829 if (sqrDist > Math.Pow(reactDist, 2))
832 UpdateIdle(deltaTime);
834 else if (sqrDist < Math.Pow(attackDist + movementMargin, 2))
836 movementMargin = attackDist;
840 useSteeringLengthAsMovementSpeed =
true;
842 if (sqrDist < Math.Pow(attackDist * 0.75f, 2))
846 useSteeringLengthAsMovementSpeed =
false;
857 observeTimer -= deltaTime;
858 if (observeTimer < 0)
867 run = sqrDist > Math.Pow(attackDist * 2, 2);
868 movementMargin = MathHelper.Clamp(movementMargin -= deltaTime, 0, attackDist);
869 UpdateFollow(deltaTime);
873 throw new NotImplementedException();
888 SteerInsideLevel(deltaTime);
892 float movementSpeed = useSteeringLengthAsMovementSpeed ?
Steering.Length() : speed;
903 private void UpdateIdle(
float deltaTime,
bool followLastTarget =
true)
910 if (pathSteering ==
null)
920 if (followLastTarget)
923 if (target?.Entity !=
null && !target.Entity.Removed &&
925 (_previousAttackLimb?.attack ==
null ||
926 _previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack !=
AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0)))
929 var memory = GetTargetMemory(target);
932 var location = memory.Location;
933 float dist = Vector2.DistanceSquared(
WorldPosition, location);
934 if (dist < 50 * 50 || !IsPositionInsideAllowedZone(
WorldPosition, out _))
956 pathSteering.Wander(deltaTime, Math.Max(ConvertUnits.ToDisplayUnits(
colliderLength), 100.0f), stayStillInTightSpace:
false);
969 private readonly List<Hull> targetHulls =
new List<Hull>();
970 private readonly List<float> hullWeights =
new List<float>();
972 private Hull patrolTarget;
973 private float newPatrolTargetTimer;
974 private float patrolTimerMargin;
975 private readonly
float newPatrolTargetIntervalMin = 5;
976 private readonly
float newPatrolTargetIntervalMax = 30;
977 private bool searchingNewHull;
979 private void UpdatePatrol(
float deltaTime,
bool followLastTarget =
true)
985 newPatrolTargetTimer = Math.Min(newPatrolTargetTimer, newPatrolTargetIntervalMin);
987 if (newPatrolTargetTimer > 0)
989 newPatrolTargetTimer -= deltaTime;
993 if (!searchingNewHull)
995 searchingNewHull =
true;
998 else if (targetHulls.Any())
1000 patrolTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
1002 if (path.Unreachable)
1005 int index = targetHulls.IndexOf(patrolTarget);
1006 targetHulls.RemoveAt(index);
1007 hullWeights.RemoveAt(index);
1008 PathSteering.
Reset();
1009 patrolTarget =
null;
1010 patrolTimerMargin += 0.5f;
1011 patrolTimerMargin = Math.Min(patrolTimerMargin, newPatrolTargetIntervalMin);
1012 newPatrolTargetTimer = Math.Min(newPatrolTargetIntervalMin, patrolTimerMargin);
1016 PathSteering.
SetPath(patrolTarget.SimPosition, path);
1017 patrolTimerMargin = 0;
1018 newPatrolTargetTimer = newPatrolTargetIntervalMax * Rand.Range(0.5f, 1.5f);
1019 searchingNewHull =
false;
1025 newPatrolTargetTimer = newPatrolTargetIntervalMax;
1026 searchingNewHull =
false;
1029 if (patrolTarget !=
null && pathSteering.CurrentPath !=
null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable)
1036 bool PatrolNodeFilter(PathNode n) =>
1040 UpdateIdle(deltaTime, followLastTarget);
1043 private void FindTargetHulls()
1047 targetHulls.Clear();
1048 hullWeights.Clear();
1051 foreach (var hull
in Hull.HullList)
1053 if (hull.Submarine ==
null) {
continue; }
1056 if (hull.RectWidth < hullMinSize || hull.RectHeight < hullMinSize) {
continue; }
1057 if (checkWaterLevel)
1061 if (hull.WaterPercentage > 50) {
continue; }
1065 if (hull.WaterPercentage < 80) {
continue; }
1068 if (
AIParams.PatrolDry && hull.WaterPercentage < 80)
1076 if (!targetHulls.Contains(hull))
1078 targetHulls.Add(hull);
1079 float weight = hull.Size.Combine();
1081 float optimal = 1000;
1084 float distanceFactor = dist > optimal ? MathHelper.Lerp(1, 0, MathUtils.InverseLerp(optimal, max, dist)) : MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, optimal, dist));
1085 float waterFactor = 1;
1086 if (checkWaterLevel)
1088 waterFactor =
AIParams.PatrolDry ? MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage)) : MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 100, hull.WaterPercentage));
1090 weight *= distanceFactor * waterFactor;
1091 hullWeights.Add(weight);
1100 private Vector2 attackWorldPos;
1101 private Vector2 attackSimPos;
1102 private float reachTimer;
1104 private const float reachTimeOut = 10;
1106 private bool IsSameTarget(AITarget target, AITarget otherTarget)
1108 if (target?.Entity == otherTarget?.Entity) {
return true; }
1109 if (IsItemInCharacterInventory(target, otherTarget) || IsItemInCharacterInventory(otherTarget, target)) {
return true; }
1112 bool IsItemInCharacterInventory(AITarget potentialItem, AITarget potentialCharacter)
1114 if (potentialItem?.Entity is Item item && potentialCharacter?.Entity is
Character character)
1116 return item.ParentInventory?.Owner == character;
1122 private void UpdateAttack(
float deltaTime)
1152 if (wallTarget !=
null)
1154 attackWorldPos = wallTarget.Position;
1155 if (wallTarget.Structure.Submarine !=
null)
1157 attackWorldPos += wallTarget.Structure.Submarine.Position;
1159 attackSimPos =
Character.
Submarine == wallTarget.Structure.Submarine ? wallTarget.Position : attackWorldPos;
1160 attackSimPos = ConvertUnits.ToSimUnits(attackSimPos);
1169 if (TrySteerThroughGaps(deltaTime))
1176 bool isBroken =
true;
1177 for (
int i = 0; i < w.Sections.Length; i++)
1179 if (!w.SectionBodyDisabled(i))
1182 Vector2 sectionPos = w.SectionPosition(i, world:
true);
1183 attackWorldPos = sectionPos;
1184 attackSimPos = ConvertUnits.ToSimUnits(attackWorldPos);
1196 attackLimbSelectionTimer -= deltaTime;
1197 if (
AttackLimb ==
null || attackLimbSelectionTimer <= 0)
1199 attackLimbSelectionTimer = attackLimbSelectionInterval * Rand.Range(0.9f, 1.1f);
1200 if (!IsAttackRunning && !IsCoolDownRunning)
1206 IDamageable damageTarget = wallTarget !=
null ? wallTarget.Structure :
SelectedAiTarget.
Entity as IDamageable;
1207 bool canAttack =
true;
1208 bool pursue =
false;
1211 var currentAttackLimb =
AttackLimb ?? _previousAttackLimb;
1212 if (currentAttackLimb.attack.CoolDownTimer >=
1213 currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay)
1219 currentAttackLimb.attack.AfterAttackSecondaryDelay > 0 && currentAttackLimb.attack.AfterAttackTimer > currentAttackLimb.attack.AfterAttackSecondaryDelay ?
1220 currentAttackLimb.attack.AfterAttackSecondary :
1221 currentAttackLimb.attack.AfterAttack;
1222 switch (activeBehavior)
1225 UpdateEating(deltaTime);
1229 if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
1239 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
true);
1245 if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
1254 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
true);
1262 var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
1263 if (newLimb !=
null)
1278 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
true);
1298 if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
1301 UpdateFallBack(attackWorldPos, deltaTime, activeBehavior ==
AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
1306 if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
1311 UpdateFallBack(attackWorldPos, deltaTime, activeBehavior ==
AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
1315 var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
1316 if (newLimb !=
null)
1324 UpdateFallBack(attackWorldPos, deltaTime, activeBehavior ==
AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
1331 UpdateFallBack(attackWorldPos, deltaTime, activeBehavior ==
AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
1337 if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
1340 UpdateIdle(deltaTime, followLastTarget:
false);
1345 if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
1350 UpdateIdle(deltaTime, followLastTarget:
false);
1354 var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
1355 if (newLimb !=
null)
1363 UpdateIdle(deltaTime, followLastTarget:
false);
1370 UpdateIdle(deltaTime, followLastTarget:
false);
1376 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
true);
1379 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
true, avoidObstacles:
false);
1388 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
false);
1394 attackVector =
null;
1410 if (wallTarget ==
null && Vector2.DistanceSquared(
Character.
WorldPosition, attackWorldPos) < 2000 * 2000)
1420 Body closestBody =
Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs:
true);
1421 if (
Submarine.LastPickedFraction != 1.0f && closestBody !=
null &&
1422 ((!
AIParams.TargetOuterWalls || !canAttackWalls) && closestBody.UserData is Structure s && s.Submarine !=
null || !canAttackDoors && closestBody.UserData is Item i && i.Submarine !=
null && i.GetComponent<
Door>() !=
null))
1435 Limb attackTargetLimb =
null;
1441 if (wallTarget ==
null && targetCharacter !=
null)
1445 if (attackTargetLimb ==
null)
1452 attackWorldPos = attackTargetLimb.WorldPosition;
1457 Vector2 toTarget = attackWorldPos - attackLimbPos;
1458 Vector2 toTargetOffset = toTarget;
1462 if (wallTarget.Structure.Submarine !=
null)
1464 Vector2 margin = CalculateMargin(wallTarget.Structure.Submarine.Velocity);
1465 toTargetOffset += margin;
1468 else if (targetCharacter !=
null)
1470 Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity);
1471 toTargetOffset += margin;
1475 if (e.Submarine !=
null)
1477 Vector2 margin = CalculateMargin(e.Submarine.Velocity);
1478 toTargetOffset += margin;
1482 Vector2 CalculateMargin(Vector2 targetVelocity)
1484 if (targetVelocity == Vector2.Zero) {
return Vector2.Zero; }
1488 if (dot <= 0 || !MathUtils.IsValid(dot)) {
return Vector2.Zero; }
1491 return targetVelocity * distanceOffset * dot;
1495 distance = toTargetOffset.Length();
1503 Vector2 targetVelocity = Vector2.Zero;
1505 if (targetSub !=
null)
1507 targetVelocity = targetSub.
Velocity;
1509 else if (targetCharacter !=
null)
1511 targetVelocity = targetCharacter.AnimController.Collider.LinearVelocity;
1515 targetVelocity = i.body.LinearVelocity;
1518 float targetSpeed = targetVelocity.LengthSquared();
1519 if (mySpeed < 0.1f || mySpeed > targetSpeed)
1521 reachTimer += deltaTime;
1522 if (reachTimer > reachTimeOut)
1538 humanoidAnimController.Crouching =
true;
1547 float offset =
AttackLimb.
Params.GetSpriteOrientation() - MathHelper.PiOver2;
1549 float angle = VectorExtensions.Angle(forward, toTarget);
1554 bool IsBlocked(Vector2 targetPosition)
1563 else if (body.UserData is Limb limb)
1565 hitTarget = limb.character;
1567 if (hitTarget !=
null && !hitTarget.IsDead &&
Character.
IsFriendly(hitTarget) && !IsAttackingOwner(hitTarget))
1579 bool updateSteering =
true;
1580 if (steeringLimb ==
null)
1585 if (steeringLimb ==
null)
1593 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
false);
1597 Vector2 steerPos = attackSimPos;
1604 if (pathSteering !=
null)
1606 if (pathSteering.CurrentPath !=
null)
1614 var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
1615 if (door !=
null && !door.CanBeTraversed && !door.HasAccess(
Character))
1629 if (!canAttack || distance > margin)
1636 targetCharacter !=
null &&
VisibleHulls.Contains(targetCharacter.CurrentHull))
1643 pathSteering.SteeringSeek(steerPos, weight: 2,
1646 checkVisiblity:
true);
1648 if (!pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
1669 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
false);
1685 pathSteering.SteeringSeek(steerPos, weight: 5, minGapWidth:
minGapSize);
1693 switch (selectedTargetingParams.AttackPattern)
1696 if (selectedTargetingParams.SweepDistance > 0)
1702 float amplitude = MathHelper.Lerp(0, selectedTargetingParams.SweepStrength, MathUtils.InverseLerp(selectedTargetingParams.SweepDistance, 0, distance));
1705 sweepTimer += deltaTime * selectedTargetingParams.SweepSpeed;
1706 float sin = (float)Math.Sin(sweepTimer) * amplitude;
1707 steerPos = MathUtils.RotatePointAroundTarget(attackSimPos,
SimPosition, sin);
1711 sweepTimer = Rand.Range(-1000f, 1000f) * selectedTargetingParams.SweepSpeed;
1716 if (IsCoolDownRunning) {
break; }
1717 if (IsAttackRunning && CirclePhase !=
CirclePhase.Strike) {
break; }
1718 if (selectedTargetingParams ==
null) {
break; }
1721 float targetSize = 0;
1722 if (!selectedTargetingParams.IgnoreTargetSize)
1725 targetSub !=
null ? Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2 :
1726 targetCharacter !=
null ? ConvertUnits.ToDisplayUnits(targetCharacter.AnimController.Collider.GetSize().X) : 100;
1728 float sqrDistToTarget = Vector2.DistanceSquared(
WorldPosition, spatialTarget.WorldPosition);
1729 bool isProgressive =
AIParams.MaxAggression -
AIParams.StartAggression > 0;
1730 switch (CirclePhase)
1733 currentAttackIntensity = MathUtils.InverseLerp(
AIParams.StartAggression,
AIParams.MaxAggression, ClampIntensity(aggressionIntensity));
1735 circleDir = GetDirFromHeadingInRadius();
1738 blockCheckTimer = 0;
1739 breakCircling =
false;
1740 float minFallBackDistance = selectedTargetingParams.CircleStartDistance * 0.5f;
1741 float maxFallBackDistance = selectedTargetingParams.CircleStartDistance;
1742 float maxRandomOffset = selectedTargetingParams.CircleMaxRandomOffset;
1745 float ClampIntensity(
float intensity) => MathHelper.Clamp(intensity * Rand.Range(0.9f, 1.1f),
AIParams.StartAggression,
AIParams.MaxAggression);
1748 float intensity = ClampIntensity(currentAttackIntensity);
1749 float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed;
1750 float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed;
1751 circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, intensity);
1752 circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, intensity);
1753 circleOffset = Rand.Vector(MathHelper.Lerp(maxRandomOffset, 0, intensity));
1757 circleRotationSpeed = selectedTargetingParams.CircleRotationSpeed;
1758 circleFallbackDistance = maxFallBackDistance;
1759 circleOffset = Rand.Vector(maxRandomOffset);
1761 circleRotationSpeed *= Rand.Range(1 - selectedTargetingParams.CircleRandomRotationFactor, 1 + selectedTargetingParams.CircleRandomRotationFactor);
1762 aggressionIntensity = Math.Clamp(aggressionIntensity,
AIParams.StartAggression,
AIParams.MaxAggression);
1763 DisableAttacksIfLimbNotRanged();
1764 if (targetSub is { Borders.Width: < 1000 } &&
AttackLimb?.
attack is { Ranged:
false })
1766 breakCircling =
true;
1769 else if (sqrDistToTarget > MathUtils.Pow2(targetSize + selectedTargetingParams.CircleStartDistance))
1773 else if (sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance))
1783 Vector2 targetVelocity = GetTargetVelocity();
1784 float targetDistance = selectedTargetingParams.IgnoreTargetSize ? selectedTargetingParams.CircleStartDistance * 0.9f:
1785 targetSize + selectedTargetingParams.CircleStartDistance / 2;
1791 else if (!breakCircling && sqrDistToTarget <= MathUtils.Pow2(targetDistance) && targetVelocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed()))
1795 DisableAttacksIfLimbNotRanged();
1798 updateSteering =
false;
1799 bool isBlocked = !UpdateFallBack(attackWorldPos, deltaTime, followThrough:
false, checkBlocking:
true);
1800 if (isBlocked || sqrDistToTarget > MathUtils.Pow2(targetSize + circleFallbackDistance))
1805 DisableAttacksIfLimbNotRanged();
1808 Vector2 targetVel = GetTargetVelocity();
1810 if (breakCircling || targetVel.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()))
1814 else if (sqrDistToTarget > MathUtils.Pow2(targetSize + selectedTargetingParams.CircleStartDistance * 1.2f))
1816 if (selectedTargetingParams.DynamicCircleRotationSpeed && circleRotationSpeed < 100)
1818 circleRotationSpeed *= 1 + deltaTime;
1827 float rotationStep = circleRotationSpeed * deltaTime * circleDir;
1830 circleRotation += rotationStep;
1834 circleRotation = rotationStep;
1836 Vector2 targetPos = attackSimPos + circleOffset;
1837 float targetDist = targetSize;
1838 if (targetDist <= 0)
1840 targetDist = circleFallbackDistance;
1842 if (targetSub !=
null &&
AttackLimb?.attack is { Ranged:
true })
1844 targetDist += circleFallbackDistance / 2;
1846 if (Vector2.DistanceSquared(
SimPosition, targetPos) < ConvertUnits.ToSimUnits(targetDist))
1852 if (canAttack &&
AttackLimb?.attack is { Ranged:
false } && sqrDistToTarget < MathUtils.Pow2(targetSize + circleFallbackDistance))
1863 steerPos = MathUtils.RotatePointAroundTarget(
SimPosition, targetPos, circleRotation);
1864 if (IsBlocked(deltaTime, steerPos))
1869 circleDir = -circleDir;
1872 else if (circleRotationSpeed < 1)
1875 circleRotationSpeed *= 1 + deltaTime;
1877 else if (circleOffset.LengthSquared() > 0.1f)
1880 circleOffset = Vector2.Zero;
1896 float requiredDistMultiplier = GetStrikeDistanceMultiplier(targetVel);
1897 if (distance > 0 && distance <
AttackLimb.
attack.
Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity)))
1905 strikeTimer -= deltaTime;
1908 if (strikeTimer <= 0)
1911 aggressionIntensity +=
AIParams.AggressionCumulation;
1917 bool IsFacing(
float margin)
1919 float offset = steeringLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
1921 return Vector2.Dot(Vector2.Normalize(attackWorldPos -
WorldPosition), forward) > margin;
1924 float GetStrikeDistanceMultiplier(Vector2 targetVelocity)
1926 if (selectedTargetingParams.CircleStrikeDistanceMultiplier < 1) {
return 0; }
1927 float requiredDistMultiplier = 2;
1928 bool isHeading = Vector2.Dot(Vector2.Normalize(attackWorldPos -
WorldPosition), Vector2.Normalize(
Steering)) > 0.9f;
1931 requiredDistMultiplier = selectedTargetingParams.CircleStrikeDistanceMultiplier;
1932 float targetVelocityHorizontal = Math.Abs(targetVelocity.X);
1933 if (targetVelocityHorizontal > 1)
1936 requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(targetVelocityHorizontal / 10, 0, 1));
1937 if (requiredDistMultiplier < 2)
1939 requiredDistMultiplier = 2;
1943 return requiredDistMultiplier;
1946 float GetDirFromHeadingInRadius()
1949 float angle = MathUtils.VectorToAngle(heading);
1950 return angle > MathHelper.Pi || angle < -MathHelper.Pi ? -1 : 1;
1953 Vector2 GetTargetVelocity()
1955 if (targetSub !=
null)
1957 return targetSub.Velocity;
1959 else if (targetCharacter !=
null)
1961 return targetCharacter.AnimController.Collider.LinearVelocity;
1963 return Vector2.Zero;
1973 bool advance = !canAttack &&
Character.
CurrentHull ==
null || distance > attackLimb.attack.Range * 0.9f;
1974 bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f);
1978 UpdateFallBack(attackWorldPos, deltaTime, followThrough:
false);
2000 if (pathSteering !=
null)
2002 pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth:
minGapSize);
2020 if (
AttackLimb?.attack is Attack { Ranged:
true } attack)
2022 AimRangedAttack(attack, attackTargetLimb as ISpatialEntity ?? targetEntity);
2026 if (!UpdateLimbAttack(deltaTime, attackSimPos, damageTarget, distance, attackTargetLimb))
2031 else if (IsAttackRunning)
2036 void DisableAttacksIfLimbNotRanged()
2047 if (attack is not { Ranged:
true }) {
return; }
2048 if (targetEntity is
Entity { Removed:
true }) {
return; }
2051 Limb limb = GetLimbToRotate(attack);
2055 float offset = limb.
Params.GetSpriteOrientation() - MathHelper.PiOver2;
2057 float angle = MathUtils.VectorToAngle(toTarget);
2063 private bool IsValidAttack(
Limb attackingLimb, IEnumerable<AttackContext> currentContexts,
Entity target)
2065 if (attackingLimb ==
null) {
return false; }
2066 if (target ==
null) {
return false; }
2067 var attack = attackingLimb.
attack;
2068 if (attack ==
null) {
return false; }
2069 if (attack.CoolDownTimer > 0) {
return false; }
2070 if (!attack.IsValidContext(currentContexts)) {
return false; }
2071 if (!attack.IsValidTarget(target)) {
return false; }
2072 if (target is ISerializableEntity se && target is
Character)
2074 if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) {
return false; }
2076 if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(
Character))) {
return false; }
2081 Vector2 toTarget = attackWorldPos - attackLimbPos;
2082 if (attack.MinRange > 0 && toTarget.LengthSquared() < MathUtils.Pow2(attack.MinRange)) {
return false; }
2083 float offset = attackingLimb.
Params.GetSpriteOrientation() - MathHelper.PiOver2;
2085 float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget));
2086 if (angle > attack.RequiredAngle) {
return false; }
2091 private readonly List<Limb> attackLimbs =
new List<Limb>();
2092 private readonly List<float> weights =
new List<float>();
2093 private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb =
null)
2097 if (target ==
null) {
return null; }
2098 Limb selectedLimb =
null;
2099 float currentPriority = -1;
2102 if (limb == ignoredLimb) {
continue; }
2103 if (limb.IsSevered || limb.IsStuck) {
continue; }
2104 if (!IsValidAttack(limb, currentContexts, target)) {
continue; }
2107 attackLimbs.Add(limb);
2108 weights.Add(limb.attack.Priority);
2112 float priority = CalculatePriority(limb, attackWorldPos);
2113 if (priority > currentPriority)
2115 currentPriority = priority;
2116 selectedLimb = limb;
2122 selectedLimb = ToolBox.SelectWeightedRandom(attackLimbs, weights, Rand.RandSync.Unsynced);
2123 attackLimbs.Clear();
2126 return selectedLimb;
2128 float CalculatePriority(Limb limb, Vector2 attackPos)
2130 float prio = 1 + limb.attack.Priority;
2132 float dist = Vector2.Distance(limb.WorldPosition, attackPos);
2133 float distanceFactor = 1;
2134 if (limb.attack.Ranged)
2137 distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(min, Math.Max(limb.attack.Range / 2, min), dist));
2143 distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, limb.attack.Range * 3, dist));
2145 return prio * distanceFactor;
2151 float reactionTime = Rand.Range(0.1f, 0.3f);
2152 updateTargetsTimer = Math.Min(updateTargetsTimer, reactionTime);
2154 bool wasLatched = IsLatchedOnSub;
2156 if (attackResult.
Damage > 0)
2160 if (attacker ==
null || attacker.
AiTarget ==
null || attacker.
Removed || attacker.
IsDead) {
return; }
2163 ReleaseDragTargets();
2169 avoidTimer =
AIParams.AvoidTime * 0.5f * Rand.Range(0.75f, 1.25f);
2184 if (!isFriendly && attackResult.
Damage > 0.0f)
2187 if (
AIParams.AttackWhenProvoked && canAttack && !ignoredTargets.Contains(attacker.
AiTarget))
2191 ChangeTargetState(
"husk",
AIState.Attack, 100);
2195 ChangeTargetState(attacker,
AIState.Attack, 100);
2202 ChangeTargetState(
"husk", canAttack ?
AIState.Attack :
AIState.Escape, 100);
2210 ChangeTargetState(attacker, canAttack ?
AIState.Attack :
AIState.Escape, 100);
2217 ChangeTargetState(attacker, canAttack ?
AIState.Attack :
AIState.Escape, 100);
2220 else if (!
AIParams.HasTag(
"equal"))
2222 ChangeTargetState(attacker, canAttack ?
AIState.Attack :
AIState.Escape, 100);
2227 ChangeTargetState(attacker, canAttack ?
AIState.Attack :
AIState.Escape, 100);
2232 if (targetingParams.State ==
AIState.Aggressive || targetingParams.State ==
AIState.PassiveAggressive)
2234 ChangeTargetState(attacker,
AIState.Attack, 100);
2246 if (
State ==
AIState.Attack && (IsAttackRunning || IsCoolDownRunning))
2249 if (IsAttackRunning)
2251 avoidGunFire =
false;
2259 if (limb.attack !=
null)
2261 limb.attack.CoolDownTimer *= reactionTime;
2265 else if (avoidGunFire && attackResult.
Damage >=
AIParams.DamageThreshold)
2268 avoidTimer =
AIParams.AvoidTime * Rand.Range(0.75f, 1.25f);
2274 avoidTimer =
AIParams.MinFleeTime * Rand.Range(0.75f, 1.25f);
2279 private Item GetEquippedItem(
Limb limb)
2283 return limb.
type switch
2291 var slot = GetInvSlotForLimb();
2300 private static float GetRelativeDamage(
float dmg,
float vitality) => dmg / Math.Max(vitality, 1.0f);
2302 private bool UpdateLimbAttack(
float deltaTime, Vector2 attackSimPos, IDamageable damageTarget,
float distance = -1, Limb targetLimb =
null)
2305 if (
AttackLimb?.attack ==
null) {
return false; }
2306 ISpatialEntity spatialTarget = wallTarget !=
null ? wallTarget.Structure :
SelectedAiTarget.
Entity as ISpatialEntity;
2307 if (spatialTarget ==
null) {
return false; }
2309 if (wallTarget !=
null)
2312 var aiTarget = wallTarget.Structure.AiTarget;
2320 if (damageTarget ==
null) {
return false; }
2325 if (referenceLimb !=
null)
2327 Vector2 toTarget = attackWorldPos - referenceLimb.WorldPosition;
2328 float offset = referenceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
2329 Vector2 forward = VectorExtensions.Forward(referenceLimb.body.TransformedRotation - offset * referenceLimb.Dir);
2330 float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget));
2343 if (item.RequireAimToUse)
2345 if (!Aim(deltaTime, spatialTarget, item))
2351 if (damageTarget !=
null)
2358 if (damageTarget ==
null) {
return true; }
2373 if (
AttackLimb.
UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
2385 if (damageTarget.Health > 0 && attackResult.Damage > 0)
2388 float greed =
AIParams.AggressionGreed;
2394 selectedTargetMemory.
Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed;
2398 selectedTargetMemory.
Priority -= Math.Max(selectedTargetMemory.
Priority / 2, 1);
2399 return selectedTargetMemory.
Priority > 1;
2406 private float aimTimer;
2407 private float visibilityCheckTimer;
2408 private bool canSeeTarget;
2409 private float sinTime;
2410 private bool Aim(
float deltaTime, ISpatialEntity target, Item weapon)
2412 if (target ==
null || weapon ==
null) {
return false; }
2414 Vector2 toTarget = target.WorldPosition - weapon.WorldPosition;
2415 float dist = toTarget.Length();
2426 visibilityCheckTimer -= deltaTime;
2427 if (visibilityCheckTimer <= 0.0f)
2430 visibilityCheckTimer = 0.2f;
2440 aimTimer -= deltaTime;
2443 float angle = VectorExtensions.Angle(VectorExtensions.Forward(weapon.body.TransformedRotation), toTarget);
2444 float minDistance = 300;
2445 float distanceFactor = MathHelper.Lerp(1.0f, 0.1f, MathUtils.InverseLerp(minDistance, 1000, dist));
2446 float margin = MathHelper.PiOver4 * distanceFactor;
2447 if (angle < margin || dist < minDistance)
2449 var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
2451 if (pickedBody !=
null)
2453 if (target is MapEntity)
2455 if (pickedBody.UserData is Submarine sub && sub == target.Submarine)
2459 else if (target == pickedBody.UserData)
2470 else if (pickedBody.UserData is Limb limb)
2483 private void SetAimTimer(
float timer = 1.5f) => aimTimer = timer * Rand.Range(0.75f, 1.25f);
2485 private readonly
float blockCheckInterval = 0.1f;
2486 private float blockCheckTimer;
2487 private bool isBlocked;
2488 private bool IsBlocked(
float deltaTime, Vector2 steerPos, Category collisionCategory = Physics.CollisionLevel)
2490 blockCheckTimer -= deltaTime;
2491 if (blockCheckTimer <= 0)
2493 blockCheckTimer = blockCheckInterval;
2494 isBlocked =
Submarine.PickBodies(
SimPosition, steerPos, collisionCategory: collisionCategory).Any();
2499 private Vector2? attackVector =
null;
2500 private bool UpdateFallBack(Vector2 attackWorldPos,
float deltaTime,
bool followThrough,
bool checkBlocking =
false,
bool avoidObstacles =
true)
2502 if (attackVector ==
null)
2506 Vector2 dir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value);
2507 if (!MathUtils.IsValid(dir))
2509 dir = Vector2.UnitY;
2523 private Limb GetLimbToRotate(Attack attack)
2537 private void UpdateEating(
float deltaTime)
2551 if (mouthLimb ==
null)
2553 DebugConsole.ThrowError(
"Character \"" +
Character.
SpeciesName +
"\" failed to eat a target (No head limb defined)",
2560 Vector2 limbDiff = attackSimPosition - mouthPos;
2561 float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 2);
2562 bool tooFar =
Character.
InWater ? limbDiff.LengthSquared() > extent * extent : limbDiff.X > extent;
2579 if (!item.Removed && item.body !=
null)
2581 float itemBodyExtent = item.body.GetMaxExtent() * 2;
2582 if (Math.Abs(limbDiff.X) < itemBodyExtent &&
2585 Vector2 velocity = limbDiff;
2586 if (limbDiff.LengthSquared() > 0.01f) { velocity = Vector2.Normalize(velocity); }
2587 item.body.LinearVelocity *= 0.9f;
2588 item.body.LinearVelocity -= velocity * 0.25f;
2589 bool wasBroken = item.Condition <= 0.0f;
2590 item.LastEatenTime = (float)Timing.TotalTimeUnpaused;
2594 impulseDirection: Vector2.Zero,
2597 if (item.Condition <= 0.0f)
2600 Entity.Spawner.AddItemToRemoveQueue(item);
2619 private void UpdateFollow(
float deltaTime)
2649 if (!pathSteering.IsPathDirty && pathSteering.CurrentPath !=
null && pathSteering.CurrentPath.Unreachable)
2667 return enemyAI.LatchOntoAI.IsAttached && enemyAI.LatchOntoAI.TargetCharacter == character;
2676 return enemyAI.LatchOntoAI.IsAttached && enemyAI.LatchOntoAI.TargetCharacter !=
null && enemyAI.LatchOntoAI.TargetCharacter != character;
2690 selectedTargetMemory =
null;
2691 targetingParams =
null;
2692 bool isAnyTargetClose =
false;
2693 bool isBeingChased = IsBeingChased;
2694 float maxModifier = 5;
2698 if (ignoredTargets.Contains(aiTarget)) {
continue; }
2706 if (targetCharacter ==
Character) {
continue; }
2708 float valueModifier = 1;
2709 Identifier targetingTag = GetTargetingTag(aiTarget);
2710 if (targetCharacter !=
null)
2762 if (hull.Submarine ==
null) {
continue; }
2763 if (hull.Submarine.Info.IsRuin) {
continue; }
2769 door = item.GetComponent<
Door>();
2771 if (targetingFromOutsideToInside)
2773 if (door !=
null && (!canAttackDoors && !
AIParams.CanOpenDoors) || !canAttackWalls)
2779 if (door ==
null && targetingFromOutsideToInside)
2781 if (item.Submarine?.Info is { IsRuin:
true })
2787 else if (targetingTag ==
"nasonov")
2789 if ((item.Submarine ==
null || !item.Submarine.Info.IsPlayer) && item.ParentInventory ==
null)
2808 if (s.IsPlatform) {
continue; }
2809 if (s.Submarine ==
null) {
continue; }
2810 if (s.Submarine.Info.IsRuin) {
continue; }
2812 bool isInnerWall = s.
Prefab.
Tags.Contains(
"inner");
2813 if (isInnerWall && !isCharacterInside)
2818 bool attemptToGetInside =
2829 valueModifier = 200f / s.MaxHealth;
2830 for (
int i = 0; i < s.Sections.Length; i++)
2832 var section = s.Sections[i];
2833 if (section.gap ==
null) {
continue; }
2834 bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull !=
null;
2835 if (attemptToGetInside)
2837 if (!isCharacterInside)
2841 valueModifier *= leadsInside ? (IsAggressiveBoarder ? maxModifier : 1) : 0;
2843 else if (IsAggressiveBoarder && leadsInside && canAttackWalls)
2846 valueModifier *= 1 + section.gap.Open;
2852 if (IsAggressiveBoarder)
2862 valueModifier *= isInnerWall ? 0.5f : 0;
2864 else if (!canAttackWalls)
2871 valueModifier = 0.1f;
2876 if (!canAttackWalls)
2883 valueModifier *= 1 - section.gap.Open * 0.25f;
2884 valueModifier = Math.Max(valueModifier, 0.1f);
2891 if (isInnerWall || !canAttackWalls)
2897 else if (IsAggressiveBoarder)
2901 valueModifier *= 1 + section.gap.Open;
2904 valueModifier = Math.Clamp(valueModifier, 0, maxModifier);
2909 if (door.Item.Submarine ==
null) {
continue; }
2910 bool isOutdoor = door.LinkedGap?.FlowTargetHull !=
null && !door.LinkedGap.IsRoomToRoom;
2913 bool isOpen = door.CanBeTraversed;
2916 if (!canAttackDoors) {
continue; }
2925 if (IsAggressiveBoarder)
2930 if (door.CanBeTraversed)
2932 valueModifier = maxModifier;
2934 else if (door.LinkedGap !=
null)
2936 valueModifier = 1 + door.LinkedGap.Open * (maxModifier - 1);
2942 valueModifier = isOpen || isOutdoor ? 0 : 1;
2952 if (targetingTag ==
null) {
continue; }
2953 var targetParams = GetTargetParams(targetingTag);
2954 if (targetParams ==
null) {
continue; }
2957 if (targetParams.IgnoreIncapacitated && targetCharacter !=
null && targetCharacter.IsIncapacitated) {
continue; }
2958 if (targetParams.IgnoreTargetInside && aiTarget.
Entity.
Submarine !=
null) {
continue; }
2959 if (targetParams.IgnoreTargetOutside && aiTarget.
Entity.
Submarine ==
null) {
continue; }
2962 if (targetParams.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) {
continue; }
2964 if (targetParams.Conditionals.Any(c => c.TargetSelf && !c.Matches(
Character))) {
continue; }
2965 if (targetParams.IgnoreIfNotInSameSub)
2968 var targetHull = targetCharacter !=
null ? targetCharacter.CurrentHull : aiTarget.
Entity is
Item it ? it.
CurrentHull :
null;
2971 if (targetParams.State ==
AIState.Observe || targetParams.State ==
AIState.Eat)
2981 if (targetParams.IgnoreContained && targetItem.ParentInventory !=
null) {
continue; }
2982 if (targetParams.State ==
AIState.FleeTo)
2984 float target = targetParams.Threshold;
2985 if (targetParams.ThresholdMin > 0 && targetParams.ThresholdMax > 0)
2987 target = selectedTargetingParams == targetParams &&
State ==
AIState.FleeTo ? targetParams.ThresholdMax : targetParams.ThresholdMin;
3000 valueModifier *= targetParams.Priority;
3001 if (valueModifier == 0.0f) {
continue; }
3002 if (targetingTag !=
"decoy")
3009 if (otherCharacter ==
Character) {
continue; }
3019 if (otherCharacter ==
Character) {
continue; }
3028 float dist = toTarget.Length();
3029 float nonModifiedDist = dist;
3031 if (targetMemories.ContainsKey(aiTarget))
3035 if (targetParams.PerceptionDistanceMultiplier > 0.0f)
3037 dist /= targetParams.PerceptionDistanceMultiplier;
3040 if (targetParams.MaxPerceptionDistance > 0.0f &&
3041 dist * dist > targetParams.MaxPerceptionDistance * targetParams.MaxPerceptionDistance)
3046 if (!CanPerceive(aiTarget, dist, checkVisibility:
SelectedAiTarget != aiTarget))
3055 if (targetingTag ==
"door" || targetingTag ==
"wall")
3058 Vector2 rayEnd = aiTarget.
SimPosition + spatialEntity.Submarine.SimPosition;
3059 Body closestBody =
Submarine.
PickBody(rayStart, rayEnd, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture:
true);
3060 if (closestBody !=
null && closestBody.UserData is
ISpatialEntity hit)
3062 Vector2 hitPos = hit.SimPosition;
3067 else if (hit.Submarine !=
null)
3069 hitPos += hit.Submarine.SimPosition;
3071 float subHalfWidth = spatialEntity.Submarine.Borders.Width / 2;
3072 float subHalfHeight = spatialEntity.Submarine.Borders.Height / 2;
3073 Vector2 diff = ConvertUnits.ToDisplayUnits(rayEnd - hitPos);
3074 bool isOtherSideOfTheSub = Math.Abs(diff.X) > subHalfWidth || Math.Abs(diff.Y) > subHalfHeight;
3075 if (isOtherSideOfTheSub)
3085 valueModifier *= 1.1f;
3089 if (targetParams.State ==
AIState.Avoid || targetParams.State ==
AIState.PassiveAggressive || targetParams.State ==
AIState.Aggressive)
3091 float reactDistance = targetParams.ReactDistance;
3092 if (reactDistance > 0 && reactDistance < dist)
3102 dist = Math.Max(dist, 100.0f);
3110 dist *= MathHelper.Clamp(diff / 100, 2, 3);
3116 if (targetParams.PrioritizeSubCenter || targetParams.AttackPattern ==
AttackPattern.Circle || targetParams.AttackPattern ==
AttackPattern.Sweep)
3118 if (!isAnyTargetClose)
3124 dist *= MathHelper.Lerp(1f, 5f, MathUtils.InverseLerp(0, 10000, horizontalDistanceToSubCenter));
3126 else if (targetParams.AttackPattern ==
AttackPattern.Circle)
3141 switch (targetParams.State)
3147 if (targetParams.State ==
AIState.Attack)
3153 if (!insideSameSub && !IsPositionInsideAllowedZone(aiTarget.
WorldPosition, out _))
3156 bool isTargetInPlayerTeam = IsTargetInPlayerTeam(aiTarget);
3157 if (
Character.
LastAttackers.None(a => a.Damage > 0 && a.Character !=
null && (a.Character == aiTarget.
Entity || a.Character.IsOnPlayerTeam && isTargetInPlayerTeam)))
3170 if (valueModifier > targetValue)
3178 if (owner.
AiTarget !=
null && ignoredTargets.Contains(owner.
AiTarget)) {
continue; }
3189 var characterTargetingTag = GetTargetingTag(owner.
AiTarget);
3190 if (!characterTargetingTag.IsEmpty)
3193 var characterTargetingParams = GetTargetParams(characterTargetingTag);
3194 if (characterTargetingParams?.
State ==
AIState.Idle) {
continue; }
3198 if (targetCharacter !=
null)
3202 if (targetParams.State ==
AIState.Follow || targetParams.State ==
AIState.Protect || targetParams.State ==
AIState.Observe || targetParams.State ==
AIState.Eat)
3205 if (!
VisibleHulls.Contains(targetCharacter.CurrentHull))
3213 if (targetCharacter.Submarine !=
null)
3216 valueModifier *= 0.5f;
3235 if (dist > Math.Clamp(ConvertUnits.ToDisplayUnits(
colliderLength) * 10, 1000, 5000))
3244 newTarget = aiTarget;
3245 selectedTargetMemory = targetMemory;
3246 targetValue = valueModifier;
3247 targetingParams = targetParams;
3248 if (!isAnyTargetClose)
3250 isAnyTargetClose = ConvertUnits.ToDisplayUnits(
colliderLength) > nonModifiedDist;
3262 wall = wallTarget?.Structure;
3268 for (
int i = 0; i < wall.Sections.Length; i++)
3272 releaseTarget =
true;
3292 public Vector2 Position;
3294 public int SectionIndex;
3296 public WallTarget(Vector2 position, Structure structure =
null,
int sectionIndex = -1)
3298 Position = position;
3300 SectionIndex = sectionIndex;
3304 private WallTarget wallTarget;
3305 private readonly List<(Body, int, Vector2)> wallHits =
new List<(Body,
int, Vector2)>(3);
3306 private void UpdateWallTarget(
int requiredHoleCount)
3309 if (SelectedAiTarget ==
null) {
return; }
3310 if (SelectedAiTarget.Entity ==
null) {
return; }
3311 if (!canAttackWalls) {
return; }
3312 if (HasValidPath(requireNonDirty:
true)) {
return; }
3315 Vector2 refPos = AttackLimb !=
null ? AttackLimb.SimPosition : SimPosition;
3318 Vector2 rayStart = refPos;
3319 Vector2 rayEnd = SelectedAiTarget.SimPosition;
3320 if (SelectedAiTarget.Entity.Submarine !=
null &&
Character.Submarine ==
null)
3322 rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
3324 else if (SelectedAiTarget.Entity.Submarine ==
null &&
Character.Submarine !=
null)
3326 rayEnd -=
Character.Submarine.SimPosition;
3328 DoRayCast(rayStart, rayEnd);
3332 Vector2 rayStart = refPos;
3333 Vector2 rayEnd = rayStart + VectorExtensions.Forward(
Character.AnimController.Collider.Rotation + MathHelper.PiOver2, avoidLookAheadDistance * 5);
3334 if (SelectedAiTarget.Entity.Submarine !=
null &&
Character.Submarine ==
null)
3336 rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
3337 rayEnd -= SelectedAiTarget.Entity.Submarine.SimPosition;
3339 else if (SelectedAiTarget.Entity.Submarine ==
null &&
Character.Submarine !=
null)
3341 rayStart -=
Character.Submarine.SimPosition;
3342 rayEnd -=
Character.Submarine.SimPosition;
3344 DoRayCast(rayStart, rayEnd);
3348 Vector2 rayStart = refPos;
3349 Vector2 rayEnd = rayStart +
Steering * 5;
3350 if (SelectedAiTarget.Entity.Submarine !=
null &&
Character.Submarine ==
null)
3352 rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
3353 rayEnd -= SelectedAiTarget.Entity.Submarine.SimPosition;
3355 else if (SelectedAiTarget.Entity.Submarine ==
null &&
Character.Submarine !=
null)
3357 rayStart -=
Character.Submarine.SimPosition;
3358 rayEnd -=
Character.Submarine.SimPosition;
3360 DoRayCast(rayStart, rayEnd);
3364 Vector2 targetdiff = ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition - (AttackLimb !=
null ? AttackLimb.WorldPosition : WorldPosition));
3365 float targetDistance = targetdiff.LengthSquared();
3366 Body closestBody =
null;
3367 float closestDistance = 0;
3368 int sectionIndex = -1;
3369 Vector2 sectionPos = Vector2.Zero;
3370 foreach ((Body body,
int index, Vector2 sectionPosition) in wallHits)
3373 float distance = Vector2.DistanceSquared(
3375 Submarine.GetRelativeSimPosition(ConvertUnits.ToSimUnits(sectionPosition),
Character.Submarine, structure.Submarine));
3377 if (distance > targetDistance) {
continue; }
3378 if (closestBody ==
null || closestDistance == 0 || distance < closestDistance)
3381 closestDistance = distance;
3383 sectionPos = sectionPosition;
3384 sectionIndex = index;
3387 if (closestBody ==
null || sectionIndex == -1) {
return; }
3388 Vector2 attachTargetNormal;
3389 if (wall.IsHorizontal)
3391 attachTargetNormal =
new Vector2(0.0f, Math.Sign(WorldPosition.Y - wall.WorldPosition.Y));
3392 sectionPos.Y += (wall.BodyHeight <= 0.0f ? wall.Rect.Height : wall.BodyHeight) / 2 * attachTargetNormal.Y;
3396 attachTargetNormal =
new Vector2(Math.Sign(WorldPosition.X - wall.WorldPosition.X), 0.0f);
3397 sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X;
3399 LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
3401 !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
3405 bool isTargetingDoor = SelectedAiTarget.Entity is
Item i && i.GetComponent<
Door>() !=
null;
3407 if (!isTargetingDoor)
3412 IgnoreTarget(SelectedAiTarget);
3418 wallTarget =
new WallTarget(sectionPos, wall, sectionIndex);
3424 IgnoreTarget(SelectedAiTarget);
3429 void DoRayCast(Vector2 rayStart, Vector2 rayEnd)
3431 Body hitTarget =
Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs:
true,
3434 if (hitTarget !=
null && IsValid(hitTarget, out wall))
3436 int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(
Submarine.LastPickedPosition));
3437 if (sectionIndex >= 0)
3439 wallHits.Add((hitTarget, sectionIndex, GetSectionPosition(wall, sectionIndex)));
3444 Vector2 GetSectionPosition(Structure wall,
int sectionIndex)
3446 float sectionDamage = wall.SectionDamage(sectionIndex);
3447 for (
int i = sectionIndex - 2; i <= sectionIndex + 2; i++)
3449 if (wall.SectionBodyDisabled(i))
3452 CanPassThroughHole(wall, i, requiredHoleCount))
3463 if (wall.SectionDamage(i) > sectionDamage)
3468 return wall.SectionPosition(sectionIndex, world:
false);
3471 bool IsValid(Body hit, out Structure wall)
3474 if (
Submarine.LastPickedFraction == 1.0f) {
return false; }
3475 if (hit.UserData is not Structure w) {
return false; }
3476 if (w.Submarine ==
null) {
return false; }
3477 if (w.Submarine != SelectedAiTarget.Entity.Submarine) {
return false; }
3480 if (w.Prefab.Tags.Contains(
"inner"))
3484 else if (!AIParams.TargetOuterWalls)
3494 private bool TrySteerThroughGaps(
float deltaTime)
3496 if (wallTarget !=
null && wallTarget.SectionIndex > -1 && CanPassThroughHole(wallTarget.Structure, wallTarget.SectionIndex, requiredHoleCount))
3498 WallSection section = wallTarget.Structure.GetSection(wallTarget.SectionIndex);
3499 Vector2 targetPos = wallTarget.Structure.SectionPosition(wallTarget.SectionIndex, world:
true);
3500 return section?.gap !=
null && SteerThroughGap(wallTarget.Structure, section, targetPos, deltaTime);
3502 else if (SelectedAiTarget !=
null)
3504 if (SelectedAiTarget.Entity is Structure wall)
3506 for (
int i = 0; i < wall.Sections.Length; i++)
3508 WallSection section = wall.Sections[i];
3509 if (CanPassThroughHole(wall, i, requiredHoleCount) && section?.gap !=
null)
3511 return SteerThroughGap(wall, section, wall.SectionPosition(i,
true), deltaTime);
3515 else if (SelectedAiTarget.Entity is Item i)
3517 var door = i.GetComponent<
Door>();
3519 if (door?.LinkedGap?.FlowTargetHull !=
null && !door.LinkedGap.IsRoomToRoom && door.CanBeTraversed)
3521 if (
Character.AnimController.CanWalk || door.LinkedGap.FlowTargetHull.WaterPercentage > 25)
3523 if (door.LinkedGap.Size > ConvertUnits.ToDisplayUnits(colliderWidth))
3525 float maxDistance = Math.Max(ConvertUnits.ToDisplayUnits(colliderLength), 100);
3526 return SteerThroughGap(door.LinkedGap, door.LinkedGap.FlowTargetHull.WorldPosition, deltaTime, maxDistance: maxDistance);
3535 private AITargetMemory GetTargetMemory(AITarget target,
bool addIfNotFound =
false,
bool keepAlive =
false)
3537 if (!targetMemories.TryGetValue(target, out AITargetMemory memory))
3541 memory =
new AITargetMemory(target, minPriority);
3542 targetMemories.Add(target, memory);
3547 memory.Priority = Math.Max(memory.Priority, minPriority);
3552 private void UpdateCurrentMemoryLocation()
3554 if (_selectedAiTarget !=
null)
3556 if (_selectedAiTarget.Entity ==
null || _selectedAiTarget.Entity.Removed)
3558 _selectedAiTarget =
null;
3560 else if (CanPerceive(_selectedAiTarget, checkVisibility:
false))
3562 var memory = GetTargetMemory(_selectedAiTarget);
3565 memory.Location = _selectedAiTarget.WorldPosition;
3571 private readonly List<AITarget> removals =
new List<AITarget>();
3572 private void FadeMemories(
float deltaTime)
3575 foreach (var kvp
in targetMemories)
3577 var target = kvp.Key;
3578 var memory = kvp.Value;
3580 float fadeTime = memoryFadeTime;
3581 if (target == SelectedAiTarget)
3586 else if (target == _lastAiTarget)
3591 memory.Priority -= fadeTime * deltaTime;
3593 if (memory.Priority <= 1 || target.Entity ==
null || target.Entity.Removed || !AITarget.List.Contains(target))
3595 removals.Add(target);
3598 removals.ForEach(r => targetMemories.Remove(r));
3601 private readonly
float targetIgnoreTime = 10;
3602 private float targetIgnoreTimer;
3603 private readonly HashSet<AITarget> ignoredTargets =
new HashSet<AITarget>();
3606 if (target ==
null) {
return; }
3607 ignoredTargets.Add(target);
3608 targetIgnoreTimer = targetIgnoreTime * Rand.Range(0.75f, 1.25f);
3612 #region State switching
3617 private readonly
float stateResetCooldown = 10;
3618 private float stateResetTimer;
3619 private bool isStateChanged;
3625 if (trigger.IsTriggered) {
return; }
3626 if (activeTriggers.ContainsKey(trigger)) {
return; }
3627 if (activeTriggers.ContainsValue(selectedTargetingParams))
3629 if (!trigger.AllowToOverride) {
return; }
3630 var existingTrigger = activeTriggers.FirstOrDefault(kvp => kvp.Value == selectedTargetingParams && kvp.Key.AllowToBeOverridden);
3631 if (existingTrigger.Key ==
null) {
return; }
3632 activeTriggers.Remove(existingTrigger.Key);
3635 activeTriggers.Add(trigger, selectedTargetingParams);
3636 ChangeParams(selectedTargetingParams, trigger.State);
3639 private void UpdateTriggers(
float deltaTime)
3641 foreach (var triggerObject
in activeTriggers)
3644 if (trigger.IsPermanent) {
continue; }
3646 if (!trigger.IsActive)
3649 ResetParams(triggerObject.Value);
3650 inactiveTriggers.Add(trigger);
3653 foreach (StatusEffect.AITrigger trigger in inactiveTriggers)
3655 activeTriggers.Remove(trigger);
3657 inactiveTriggers.Clear();
3660 private bool TryResetOriginalState(
string tag) =>
3661 TryResetOriginalState(tag.ToIdentifier());
3666 private bool TryResetOriginalState(Identifier tag)
3668 if (!modifiedParams.ContainsKey(tag)) {
return false; }
3669 if (AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams))
3671 modifiedParams.Remove(tag);
3672 if (tempParams.ContainsKey(tag))
3674 tempParams.Values.ForEach(t => AIParams.RemoveTarget(t));
3675 tempParams.Remove(tag);
3677 ResetParams(targetParams);
3686 private readonly Dictionary<Identifier, CharacterParams.TargetParams> modifiedParams =
new Dictionary<Identifier, CharacterParams.TargetParams>();
3687 private readonly Dictionary<Identifier, CharacterParams.TargetParams> tempParams =
new Dictionary<Identifier, CharacterParams.TargetParams>();
3689 private void ChangeParams(CharacterParams.TargetParams targetParams,
AIState state,
float? priority =
null)
3691 if (targetParams ==
null) {
return; }
3692 if (priority.HasValue)
3694 targetParams.Priority = priority.Value;
3696 targetParams.State = state;
3699 private void ResetParams(CharacterParams.TargetParams targetParams)
3701 targetParams?.Reset();
3702 if (selectedTargetingParams == targetParams || State ==
AIState.Idle || State ==
AIState.Patrol)
3710 private void ChangeParams(
string tag,
AIState state,
float? priority =
null,
bool onlyExisting =
false)
3711 => ChangeParams(tag.ToIdentifier(), state, priority, onlyExisting);
3713 private void ChangeParams(Identifier tag,
AIState state,
float? priority =
null,
bool onlyExisting =
false,
bool ignoreAttacksIfNotInSameSub =
false)
3715 if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams))
3717 if (!onlyExisting && !tempParams.ContainsKey(tag))
3719 if (AIParams.TryAddNewTarget(tag, state, priority ?? minPriority, out targetParams))
3724 targetParams.IgnoreIfNotInSameSub = ignoreAttacksIfNotInSameSub;
3726 tempParams.Add(tag, targetParams);
3730 if (targetParams !=
null)
3732 if (priority.HasValue)
3734 targetParams.Priority = Math.Max(targetParams.Priority, priority.Value);
3736 targetParams.State = state;
3737 if (!modifiedParams.ContainsKey(tag))
3739 modifiedParams.Add(tag, targetParams);
3744 private void ChangeTargetState(
string tag,
AIState state,
float? priority =
null)
3746 isStateChanged =
true;
3747 SetStateResetTimer();
3748 ChangeParams(tag, state, priority);
3755 private void ChangeTargetState(Character target,
AIState state,
float? priority =
null)
3757 isStateChanged =
true;
3758 SetStateResetTimer();
3759 if (!
Character.IsPet || !target.IsHuman)
3762 ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman);
3766 priority = GetTargetParams(
"human")?.Priority;
3770 ChangeParams(
"weapon", state, priority);
3771 ChangeParams(
"tool", state, priority);
3777 if (target.Submarine !=
null &&
Character.Submarine ==
null && (canAttackDoors || canAttackWalls))
3779 ChangeParams(
"room", state, priority / 2);
3782 ChangeParams(
"wall", state, priority / 2);
3784 if (canAttackDoors && IsAggressiveBoarder)
3786 ChangeParams(
"door", state, priority / 2);
3789 ChangeParams(
"provocative", state, priority, onlyExisting:
true);
3794 private void ResetOriginalState()
3796 isStateChanged =
false;
3797 modifiedParams.Keys.ForEachMod(tag => TryResetOriginalState(tag));
3803 base.OnTargetChanged(previousTarget, newTarget);
3804 if (newTarget ==
null) {
return; }
3805 var targetParams = GetTargetParams(newTarget);
3806 if (targetParams !=
null)
3808 observeTimer = targetParams.Timer * Rand.Range(0.75f, 1.25f);
3821 if (disableTailCoroutine !=
null)
3823 CoroutineManager.StopCoroutines(disableTailCoroutine);
3825 disableTailCoroutine =
null;
3831 if (isStateChanged && to ==
AIState.Idle && from != to)
3833 SetStateResetTimer();
3835 blockCheckTimer = 0;
3844 private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f);
3846 private float GetPerceivingRange(
AITarget target)
3849 if (AIParams.MaxPerceptionDistance >= 0 && maxSightOrSoundRange > AIParams.MaxPerceptionDistance) {
return AIParams.MaxPerceptionDistance; }
3850 return maxSightOrSoundRange;
3853 private bool CanPerceive(AITarget target,
float dist = -1,
float distSquared = -1,
bool checkVisibility =
false)
3855 if (target?.Entity ==
null) {
return false; }
3856 bool insideSightRange;
3857 bool insideSoundRange;
3858 if (checkVisibility)
3864 Character.IsPet && (
Character.Submarine !=
null || target.Entity.Submarine !=
null) ||
3865 target.Entity.Submarine !=
null && target.Entity.Submarine ==
Character.Submarine && target.Entity.Submarine.TeamID ==
CharacterTeamType.None;
3869 if (AIParams.MaxPerceptionDistance >= 0 && dist > AIParams.MaxPerceptionDistance) {
return false; }
3870 insideSightRange = IsInRange(dist, target.SightRange, Sight);
3871 if (!checkVisibility && insideSightRange) {
return true; }
3872 insideSoundRange = IsInRange(dist, target.SoundRange, Hearing);
3876 if (distSquared < 0)
3878 distSquared = Vector2.DistanceSquared(
Character.WorldPosition, target.WorldPosition);
3880 if (AIParams.MaxPerceptionDistance >= 0 && distSquared > AIParams.MaxPerceptionDistance * AIParams.MaxPerceptionDistance) {
return false; }
3881 insideSightRange = IsInRangeSqr(distSquared, target.SightRange, Sight);
3882 if (!checkVisibility && insideSightRange) {
return true; }
3883 insideSoundRange = IsInRangeSqr(distSquared, target.SoundRange, Hearing);
3885 if (!checkVisibility)
3887 return insideSightRange || insideSoundRange;
3891 if (!insideSightRange && !insideSoundRange) {
return false; }
3893 if (target.Entity is Character c && VisibleHulls.Contains(c.CurrentHull) || target.Entity is Item i && VisibleHulls.Contains(i.CurrentHull))
3895 return insideSightRange || insideSoundRange;
3902 return IsInRange(dist, target.SoundRange, Hearing / 2);
3906 if (distSquared < 0)
3908 distSquared = Vector2.DistanceSquared(
Character.WorldPosition, target.WorldPosition);
3910 return IsInRangeSqr(distSquared, target.SoundRange, Hearing / 2);
3915 bool IsInRange(
float dist,
float range,
float perception) => dist <= range * perception;
3916 bool IsInRangeSqr(
float distSquared,
float range,
float perception) => distSquared <= MathUtils.Pow2(range * perception);
3922 canAttackDoors =
false;
3923 canAttackCharacters =
false;
3926 if (limb.IsSevered) {
continue; }
3927 if (limb.Disabled) {
continue; }
3928 if (limb.attack ==
null) {
continue; }
3929 if (!canAttackWalls)
3931 canAttackWalls = limb.attack.IsValidTarget(
AttackTarget.Structure) && (limb.attack.StructureDamage > 0 || limb.attack.Ranged);
3933 if (!canAttackDoors)
3935 canAttackDoors = limb.attack.IsValidTarget(
AttackTarget.Structure) && (limb.attack.ItemDamage > 0 || limb.attack.Ranged);
3937 if (!canAttackCharacters)
3939 canAttackCharacters = limb.attack.IsValidTarget(
AttackTarget.Character);
3942 if (PathSteering !=
null)
3948 private bool IsPositionInsideAllowedZone(Vector2 pos, out Vector2 targetDir)
3950 targetDir = Vector2.Zero;
3952 if (Level.Loaded.LevelData.Biome.IsEndBiome) {
return true; }
3953 if (AIParams.AvoidAbyss)
3955 if (pos.Y < Level.Loaded.AbyssStart)
3958 targetDir = Vector2.UnitY;
3961 else if (AIParams.StayInAbyss)
3963 if (pos.Y > Level.Loaded.AbyssStart)
3966 targetDir = -Vector2.UnitY;
3968 else if (pos.Y < Level.Loaded.AbyssEnd)
3971 targetDir = Vector2.UnitY;
3974 float margin = Level.OutsideBoundsCurrentMargin;
3975 if (pos.X < -margin)
3978 targetDir = Vector2.UnitX;
3980 else if (pos.X > Level.Loaded.Size.X + margin)
3983 targetDir = -Vector2.UnitX;
3985 return targetDir == Vector2.Zero;
3988 private Vector2 returnDir;
3989 private float returnTimer;
3990 private void SteerInsideLevel(
float deltaTime)
3992 if (SteeringManager is IndoorsSteeringManager) {
return; }
3993 if (Level.Loaded ==
null) {
return; }
3994 if (State ==
AIState.Attack && returnTimer <= 0) {
return; }
3995 float returnTime = 5;
3996 if (!IsPositionInsideAllowedZone(WorldPosition, out Vector2 targetDir))
3998 returnDir = targetDir;
3999 returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
4001 if (returnTimer > 0)
4003 returnTimer -= deltaTime;
4004 SteeringManager.Reset();
4005 SteeringManager.SteeringManual(deltaTime, returnDir * 10);
4006 SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, 15);
4012 IsTryingToSteerThroughGap =
true;
4016 bool success = base.SteerThroughGap(wall, section, targetWorldPos, deltaTime);
4023 IsSteeringThroughGap = success;
4027 public override bool SteerThroughGap(
Gap gap, Vector2 targetWorldPos,
float deltaTime,
float maxDistance = -1)
4029 bool success = base.SteerThroughGap(gap, targetWorldPos, deltaTime, maxDistance);
4037 IsSteeringThroughGap = success;
4045 if (SelectedAiTarget !=
null && (SelectedAiTarget.Entity ==
null || SelectedAiTarget.Entity.Removed))
4052 targetMemory.Priority += deltaTime * PriorityFearIncrement;
4054 bool isSteeringThroughGap = UpdateEscape(deltaTime, canAttackDoors);
4055 if (!isSteeringThroughGap)
4059 SteerAwayFromTheEnemy();
4061 else if (canAttackDoors && HasValidPath())
4064 if (door !=
null && !door.CanBeTraversed && !door.HasAccess(
Character))
4066 if (SelectedAiTarget != door.Item.AiTarget || State !=
AIState.Attack)
4068 SelectTarget(door.Item.AiTarget, SelectedTargetMemory.
Priority);
4075 if (EscapeTarget ==
null)
4079 SteerAwayFromTheEnemy();
4090 return isSteeringThroughGap;
4092 void SteerAwayFromTheEnemy()
4094 if (SelectedAiTarget ==
null) {
return; }
4095 Vector2 escapeDir = Vector2.Normalize(WorldPosition - SelectedAiTarget.WorldPosition);
4096 if (!MathUtils.IsValid(escapeDir))
4098 escapeDir = Vector2.UnitY;
4103 escapeDir =
new Vector2(Math.Sign(escapeDir.X), 0);
4110 private readonly List<Limb> targetLimbs =
new List<Limb>();
4113 targetLimbs.Clear();
4114 foreach (var limb
in target.AnimController.Limbs)
4116 if (limb.type == targetLimbType || targetLimbType ==
LimbType.None)
4118 targetLimbs.Add(limb);
4121 if (targetLimbs.None())
4124 targetLimbs.AddRange(target.AnimController.Limbs);
4126 float closestDist =
float.MaxValue;
4127 Limb targetLimb =
null;
4128 foreach (
Limb limb
in targetLimbs)
4130 if (limb.IsSevered) {
continue; }
4131 if (limb.Hidden) {
continue; }
4132 float dist = Vector2.DistanceSquared(limb.WorldPosition, attackLimb.WorldPosition) / Math.Max(limb.AttackPriority, 0.1f);
4133 if (dist < closestDist)
4144 var pickable = item.GetComponent<
Pickable>();
4145 if (pickable !=
null)
4170 private float priority;
4172 public float Priority
4174 get {
return priority; }
4175 set { priority = MathHelper.Clamp(value, 1.0f, 100.0f); }
4182 this.priority = priority;
readonly Character Character
readonly float minGapSize
bool IsTryingToSteerThroughGap
readonly float avoidLookAheadDistance
AITarget SelectedAiTarget
IEnumerable< Hull > VisibleHulls
Returns hulls that are visible to the character, including the current hull. Note that this is not an...
SteeringManager SteeringManager
bool IsCurrentPathUnreachable
bool IsWallDisabled(Structure wall)
readonly float colliderLength
AITarget _previousAiTarget
bool IsSteeringThroughGap
SteeringManager steeringManager
readonly float colliderWidth
bool HasValidPath(bool requireNonDirty=true, bool requireUnfinished=true, Func< WayPoint, bool > nodePredicate=null)
Is the current path valid, using the provided parameters.
bool IsCurrentPathFinished
void FaceTarget(ISpatialEntity target)
AIObjective CurrentObjective
Includes orders.
static List< AITarget > List
bool ShouldBeIgnored()
Is some condition met (e.g. entity null, indetectable, outside level) that prevents anyone from detec...
bool IsWithinSector(Vector2 worldPosition)
AITargetMemory(AITarget target, float priority)
float GetCurrentSpeed(bool useMaxSpeed)
abstract SwimParams SwimFastParams
void ApplyPose(Vector2 leftHandPos, Vector2 rightHandPos, Vector2 leftFootPos, Vector2 rightFootPos, float footMoveForce=10)
Attacks are used to deal damage to characters, structures and items. They can be defined in the weapo...
float RequiredAngleToShoot
bool FullSpeedAfterAttack
readonly CharacterParams Params
IEnumerable< AttackContext > GetAttackContexts()
void SelectCharacter(Character character)
Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo=null, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, RagdollParams ragdollParams=null, bool spawnInitialItems=true)
void SetInput(InputType inputType, bool hit, bool held)
float GetDamageDoneByAttacker(Character otherCharacter)
CharacterHealth CharacterHealth
float ApplyTemporarySpeedLimits(float speed)
virtual AIController AIController
override Vector2? SimPosition
void ApplyStatusEffects(ActionType actionType, float deltaTime)
static readonly List< Character > CharacterList
bool IsFriendly(Character other)
Character SelectedCharacter
CharacterInventory Inventory
bool IsSameSpeciesOrGroup(Character other)
readonly CharacterPrefab Prefab
void PlaySound(CharacterSound.SoundType soundType, float soundIntervalFactor=1.0f, float maxInterval=0)
Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos=null)
static Character? Controlled
IEnumerable< Attacker > LastAttackers
static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
bool HasAbilityFlag(AbilityFlags abilityFlag)
Vector2 ApplyMovementLimits(Vector2 targetMovement, float currentSpeed)
readonly AnimController AnimController
IEnumerable< Item >?? HeldItems
Items the character has in their hand slots. Doesn't return nulls and only returns items held in both...
bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity=null, bool seeThroughWindows=false, bool checkFacing=false)
Item GetItemInLimbSlot(InvSlotType limbSlot)
float HealthRegenerationWhenEating
Contains character data that should be editable in the character editor.
AIParams AI
Parameters for EnemyAIController. Not used by HumanAIController.
static readonly Identifier HumanSpeciesName
ContentXElement? FirstElement()
ContentXElement OriginalElement
override void OnTargetChanged(AITarget previousTarget, AITarget newTarget)
override void OnStateChanged(AIState from, AIState to)
override void Update(float deltaTime)
static bool DisableEnemyAI
AITarget UpdateTargets(out CharacterParams.TargetParams targetingParams)
static bool IsTargetBeingChasedBy(Character target, Character character)
override bool Escape(float deltaTime)
CharacterParams.TargetParams SelectedTargetingParams
bool CanPassThroughHole(Structure wall, int sectionIndex)
static bool IsLatchedToSomeoneElse(Character target, Character character)
override void SelectTarget(AITarget target)
CharacterParams.AIParams AIParams
Shorthand for Character.Params.AI with null checking.
bool IsBeingChasedBy(Character c)
void IgnoreTarget(AITarget target)
EnemyAIController(Character c, string seed)
void LaunchTrigger(StatusEffect.AITrigger trigger)
float PriorityFearIncrement
override bool SteerThroughGap(Structure wall, WallSection section, Vector2 targetWorldPos, float deltaTime)
override CanEnterSubmarine CanEnterSubmarine
void SelectTarget(AITarget target, float priority)
bool TargetOutposts
Enable the character to attack the outposts and the characters inside them. Disabled by default in no...
override bool SteerThroughGap(Gap gap, Vector2 targetWorldPos, float deltaTime, float maxDistance=-1)
SwarmBehavior SwarmBehavior
HashSet< Submarine > UnattackableSubmarines
The monster won't try to damage these submarines
void AimRangedAttack(Attack attack, ISpatialEntity targetEntity)
static bool IsLatchedTo(Character target, Character character)
Limb GetTargetLimb(Limb attackLimb, Character target, LimbType targetLimbType=LimbType.None)
override void OnAttacked(Character attacker, AttackResult attackResult)
AITargetMemory SelectedTargetMemory
virtual Vector2 WorldPosition
Entity(Submarine submarine, ushort id)
virtual Vector2 SimPosition
static NetworkMember NetworkMember
AIObjectiveManager ObjectiveManager
void SteeringSeek(Vector2 target, float weight, float minGapWidth=0, Func< PathNode, bool > startNodeFilter=null, Func< PathNode, bool > endNodeFilter=null, Func< PathNode, bool > nodeFilter=null, bool checkVisiblity=true)
bool IsNextLadderSameAsCurrent
void SetPath(Vector2 targetPos, SteeringPath path)
bool HasTag(Identifier tag)
Inventory FindParentInventory(Func< Inventory, bool > predicate)
void SetAttachTarget(Structure wall, Vector2 attachPos, Vector2 attachSurfaceNormal)
void DeattachFromBody(bool reset, float cooldown=0)
List< Joint > AttachJoints
void Update(EnemyAIController enemyAI, float deltaTime)
readonly LimbParams Params
bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance=-1, Limb targetLimb=null)
Returns true if the attack successfully hit something. If the distance is not given,...
Items.Components.Rope AttachedRope
Mersenne Twister based random
readonly MapEntityPrefab Prefab
abstract ImmutableHashSet< Identifier > Tags
int ForceToEndLocationIndex
SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub=null, string errorMsgStr=null, float minGapSize=0, Func< PathNode, bool > startNodeFilter=null, Func< PathNode, bool > endNodeFilter=null, Func< PathNode, bool > nodeFilter=null, bool checkVisibility=true)
void ApplyForce(Vector2 force, float maxVelocity=NetConfig.MaxPhysicsBodyVelocity)
bool SuppressSmoothRotationCalls
Ignore rotation calls for the rest of this and the next update. Automatically disabled after that....
Vector2 GetLocalFront(float? spritesheetRotation=null)
Returns the farthest point towards the forward of the body. For capsules and circles,...
void MoveToPos(Vector2 simPosition, float force, Vector2? pullPos=null)
float TransformedRotation
Takes flipping (Dir) into account.
void SmoothRotate(float targetRotation, float force=10.0f, bool wrapAngle=true)
Rotate the body towards the target rotation in the "shortest direction", taking into account the curr...
ContentPackage? ContentPackage
CanEnterSubmarine CanEnterSubmarine
Vector2 GetColliderBottom()
void RestoreTemporarilyDisabled()
bool? SimplePhysicsEnabled
Vector2? GetMouthPosition()
Limb GetLimb(LimbType limbType, bool excludeSevered=true)
Note that if there are multiple limbs of the same type, only the first (valid) limb is returned.
float ColliderHeightFromFloor
In sim units. Joint scale applied.
Can be used to trigger a behavior change of some kind on an AI character. Only applicable for enemy c...
void UpdateTimer(float deltaTime)
StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in ...
void SteeringManual(float deltaTime, Vector2 velocity)
void SteeringSeek(Vector2 targetSimPos, float weight=1)
void SteeringWander(float weight=1, bool avoidWanderingOutsideLevel=false)
void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight=1)
virtual void Update(float speed)
Update speed for the steering. Should normally match the characters current animation speed.
const int WallSectionSize
static readonly Submarine[] MainSubs
override Vector2? WorldPosition
static IEnumerable< Body > PickBodies(Vector2 rayStart, Vector2 rayEnd, IEnumerable< Body > ignoredBodies=null, Category? collisionCategory=null, bool ignoreSensors=true, Predicate< Fixture > customPredicate=null, bool allowInsideFixture=false)
Returns a list of physics bodies the ray intersects with, sorted according to distance (the closest b...
override Vector2 SimPosition
bool IsConnectedTo(Submarine otherSub)
Returns true if the sub is same as the other, or connected to it via docking ports.
override Vector2? Position
static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable< Body > ignoredBodies=null, Category? collisionCategory=null, bool ignoreSensors=true, Predicate< Fixture > customPredicate=null, bool allowInsideFixture=false)
static Vector2 LastPickedPosition
OutpostGenerationParams OutpostGenerationParams
void UpdateSteering(float deltaTime)
List< AICharacter > Members
AbilityFlags
AbilityFlags are a set of toggleable flags that can be applied to characters.
ActionType
ActionTypes define when a StatusEffect is executed.