4 using Microsoft.Xna.Framework;
6 using System.Collections.Generic;
19 private float crouchRaycastTimer;
20 private float reactTimer;
21 private float unreachableClearTimer;
22 private bool shouldCrouch;
28 const float reactionTime = 0.3f;
29 const float crouchRaycastInterval = 1;
30 const float sortObjectiveInterval = 1;
31 const float clearUnreachableInterval = 30;
33 private float flipTimer;
34 private const float FlipInterval = 0.5f;
39 private static readonly
float characterWaitOnSwitch = 5;
42 public readonly HashSet<Hull>
UnsafeHulls =
new HashSet<Hull>();
45 private readonly HashSet<Hull> dirtyHullSafetyCalculations =
new HashSet<Hull>();
47 private float respondToAttackTimer;
48 private const float RespondToAttackInterval = 1.0f;
49 private bool wasConscious;
51 private bool freezeAI;
53 private readonly
float maxSteeringBuffer = 5000;
54 private readonly
float minSteeringBuffer = 500;
55 private readonly
float steeringBufferIncreaseSpeed = 100;
56 private float steeringBuffer;
58 private readonly
float obstacleRaycastIntervalShort = 1, obstacleRaycastIntervalLong = 5;
59 private float obstacleRaycastTimer;
60 private bool isBlocked;
62 private readonly
float enemyCheckInterval = 0.2f;
63 private readonly
float enemySpotDistanceOutside = 800;
64 private readonly
float enemySpotDistanceInside = 1000;
65 private float enemyCheckTimer;
67 private readonly
float reportProblemsInterval = 1.0f;
68 private float reportProblemsTimer;
75 public float Hearing {
get;
set; } = 1.0f;
80 public float ReportRange {
get;
set; } =
float.PositiveInfinity;
87 private float _aimSpeed = 1;
90 get {
return _aimSpeed; }
91 set { _aimSpeed = Math.Max(value, 0.01f); }
94 private float _aimAccuracy = 1;
97 get {
return _aimAccuracy; }
98 set { _aimAccuracy = Math.Clamp(value, 0f, 1f); }
104 private readonly Dictionary<Character, AttackResult> previousAttackResults =
new Dictionary<Character, AttackResult>();
105 private readonly Dictionary<Character, float> previousHealAmounts =
new Dictionary<Character, float>();
121 private readonly Dictionary<Character, float> structureDamageAccumulator =
new Dictionary<Character, float>();
122 private readonly Dictionary<Hull, HullSafety> knownHulls =
new Dictionary<Hull, HullSafety>();
123 private class HullSafety
128 public bool IsStale => timer <= 0;
130 public HullSafety(
float safety)
135 public void Reset(
float safety)
137 this.safety = safety;
145 public bool Update(
float deltaTime)
147 timer = Math.Max(timer - deltaTime, 0);
188 reactTimer = GetReactionTime();
189 SortTimer = Rand.Range(0f, sortObjectiveInterval);
190 reportProblemsTimer = Rand.Range(0f, reportProblemsInterval);
193 public override void Update(
float deltaTime)
198 if (freezeAI && !isIncapacitated)
202 if (isIncapacitated) {
return; }
206 respondToAttackTimer -= deltaTime;
207 if (respondToAttackTimer <= 0.0f)
209 foreach (var previousAttackResult
in previousAttackResults)
211 RespondToAttack(previousAttackResult.Key, previousAttackResult.Value);
212 if (previousHealAmounts.ContainsKey(previousAttackResult.Key))
215 previousHealAmounts[previousAttackResult.Key] = Math.Min(previousHealAmounts[previousAttackResult.Key] - 5.0f, 100.0f);
216 if (previousHealAmounts[previousAttackResult.Key] <= 0.0f)
218 previousHealAmounts.Remove(previousAttackResult.Key);
222 previousAttackResults.Clear();
223 respondToAttackTimer = RespondToAttackInterval;
226 base.Update(deltaTime);
228 foreach (var values
in knownHulls)
230 HullSafety hullSafety = values.Value;
231 hullSafety.Update(deltaTime);
234 if (unreachableClearTimer > 0)
236 unreachableClearTimer -= deltaTime;
240 unreachableClearTimer = clearUnreachableInterval;
246 bool IsCloseEnoughToTarget(
float threshold,
bool targetSub =
true)
258 threshold += Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2;
271 obstacleRaycastTimer -= deltaTime;
272 if (obstacleRaycastTimer <= 0)
277 obstacleRaycastTimer = obstacleRaycastIntervalLong;
279 if (spatialTarget !=
null && (spatialTarget.
Submarine ==
null || !IsCloseEnoughToTarget(2000, targetSub:
false)))
282 IEnumerable<FarseerPhysics.Dynamics.Body> ignoredBodies =
null;
285 if (targetSub !=
null)
290 var obstacle =
Submarine.
PickBody(
SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
291 isBlocked = obstacle !=
null;
294 bool resetPath =
false;
297 bool isUsingInsideWaypoints = hasValidPath &&
HasValidPath(nodePredicate: n => n.Submarine !=
null || n.Ruin !=
null);
298 if (isUsingInsideWaypoints)
305 bool isUsingOutsideWaypoints = hasValidPath &&
HasValidPath(nodePredicate: n => n.Submarine ==
null && n.Ruin ==
null);
306 if (isUsingOutsideWaypoints)
316 else if (hasValidPath)
318 obstacleRaycastTimer = obstacleRaycastIntervalShort;
323 Vector2 rayStart =
SimPosition - connectedSub.SimPosition;
345 enemyCheckTimer -= deltaTime;
346 if (enemyCheckTimer < 0)
349 enemyCheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f);
352 bool useInsideSteering = !isOutside || isBlocked ||
HasValidPath() || IsCloseEnoughToTarget(steeringBuffer);
353 if (useInsideSteering)
357 insideSteering.Reset();
361 if (IsCloseEnoughToTarget(maxSteeringBuffer))
363 steeringBuffer += steeringBufferIncreaseSpeed * deltaTime;
367 steeringBuffer = minSteeringBuffer;
374 outsideSteering.
Reset();
377 steeringBuffer = minSteeringBuffer;
379 steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer);
382 CheckCrouching(deltaTime);
391 objectiveManager.SortObjectives();
394 objectiveManager.UpdateObjectives(deltaTime);
396 UpdateDragged(deltaTime);
398 if (reportProblemsTimer > 0)
400 reportProblemsTimer -= deltaTime;
402 if (reactTimer > 0.0f)
404 reactTimer -= deltaTime;
405 if (findItemState != FindItemState.None)
408 UnequipUnnecessaryItems();
421 dirtyHullSafetyCalculations.Remove(h);
428 RefreshHullSafety(h);
429 dirtyHullSafetyCalculations.Remove(h);
432 foreach (
Hull h
in dirtyHullSafetyCalculations)
434 RefreshHullSafety(h);
437 dirtyHullSafetyCalculations.Clear();
438 if (reportProblemsTimer <= 0.0f)
453 reportProblemsTimer = reportProblemsInterval;
456 UnequipUnnecessaryItems();
457 reactTimer = GetReactionTime();
460 if (objectiveManager.CurrentObjective ==
null) {
return; }
462 objectiveManager.DoCurrentObjective(deltaTime);
463 var currentObjective = objectiveManager.CurrentObjective;
464 bool run = !currentObjective.ForceWalk && (currentObjective.ForceRun || objectiveManager.GetCurrentPriority() >
AIObjectiveManager.
RunPriority);
472 else if (goTo.Target !=
null)
481 if (Math.Abs(yDiff) > 100)
488 run = Math.Abs(xDiff) > 500;
504 if (currPath !=
null && currPath.CurrentNode !=
null)
511 ignorePlatforms = height < allowedJumpHeight;
528 flipTimer -= deltaTime;
529 if (flipTimer <= 0.0f)
535 if (cursorDiffX > 10.0f)
539 else if (cursorDiffX < -10.0f)
555 flipTimer = FlipInterval;
564 private void CheckEnemies()
569 float closestDistance = 0;
574 if (c.Removed || c.IsDead || c.IsIncapacitated) {
continue; }
577 float dist = toTarget.LengthSquared();
578 float maxDistance =
Character.
Submarine ==
null ? enemySpotDistanceOutside : enemySpotDistanceInside;
579 if (dist > maxDistance * maxDistance) {
continue; }
580 if (EnemyAIController.IsLatchedToSomeoneElse(c,
Character)) {
continue; }
582 if (head ==
null) {
continue; }
584 Vector2 forward = VectorExtensions.Forward(rotation);
585 float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward));
586 if (angle > 70) {
continue; }
588 if (dist < closestDistance || closestEnemy ==
null)
591 closestDistance = dist;
594 if (closestEnemy !=
null)
600 private void UnequipUnnecessaryItems()
608 bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
611 Hull targetHull = gotoObjective.GetTargetHull();
619 if (findItemState != FindItemState.OtherItem)
621 var decontain =
ObjectiveManager.GetActiveObjectives<AIObjectiveDecontainItem>().LastOrDefault();
622 if (decontain !=
null && decontain.TargetItem !=
null && decontain.TargetItem.HasTag(Tags.HeavyDivingGear) &&
626 gotoObjective.Abandon =
true;
629 if (!shouldActOnSuffocation)
636 if (shouldActOnSuffocation || findItemState != FindItemState.OtherItem)
639 if (!needsGear || shouldActOnSuffocation)
641 bool isCurrentObjectiveFindSafety =
ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>();
642 bool shouldKeepTheGearOn =
643 isCurrentObjectiveFindSafety ||
655 bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait();
658 shouldKeepTheGearOn =
false;
660 removeDivingSuit =
true;
662 bool takeMaskOff = !shouldKeepTheGearOn;
663 if (!shouldKeepTheGearOn && !shouldActOnSuffocation)
667 removeDivingSuit =
true;
672 bool removeSuit =
false;
673 bool removeMask =
false;
676 if (objective is AIObjectiveGoTo gotoObjective)
678 if (NeedsDivingGearOnPath(gotoObjective))
680 removeDivingSuit =
false;
684 else if (gotoObjective.Mimic)
689 removeDivingSuit = !targetHasDivingGear;
690 if (removeDivingSuit)
697 takeMaskOff = !targetHasDivingGear;
708 if (removeDivingSuit)
711 if (divingSuit !=
null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors) && divingSuit.IsInteractable(
Character))
719 else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit)
721 findItemState = FindItemState.DivingSuit;
724 findItemState = FindItemState.None;
726 if (targetContainer !=
null)
732 decontainObjective.Abandoned += () =>
764 else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
766 findItemState = FindItemState.DivingMask;
769 findItemState = FindItemState.None;
771 if (targetContainer !=
null)
774 decontainObjective.Abandoned += () =>
801 if (isCarrying) {
return; }
804 if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
808 if (item ==
null || !item.IsInteractable(
Character)) {
continue; }
813 findItemState = FindItemState.OtherItem;
816 findItemState = FindItemState.None;
818 if (targetContainer !=
null)
821 decontainObjective.Abandoned += () =>
840 private readonly HashSet<Item> itemsToRelocate =
new HashSet<Item>();
850 if (itemsToRelocate.Contains(item)) {
return; }
851 itemsToRelocate.Add(item);
854 myPort.OnUnDocked += Relocate;
857 if (campaign !=
null)
861 campaign.OnSaveAndQuit += Relocate;
862 campaign.ItemsRelocatedToMainSub =
true;
865 HintManager.OnItemMarkedForRelocation();
869 if (item ==
null || item.
Removed) {
return; }
870 if (!itemsToRelocate.Contains(item)) {
return; }
894 if (owner !=
null && owner != item)
899 Item newContainer = mainSub.FindContainerFor(item, onlyPrimary:
false);
909 DebugConsole.AddWarning($
"Failed to relocate item {item.Prefab.Identifier} ({item.ID}), because no cargo spawn point could be found!");
912 itemsToRelocate.Remove(item);
913 DebugConsole.Log($
"Relocated item {item.Prefab.Identifier} ({item.ID}) back to the main sub.");
917 private enum FindItemState
924 private FindItemState findItemState;
925 private int itemIndex;
931 suitableContainer =
null;
932 if (character.
FindItem(ref itemIndex, out
Item targetContainer, ignoredItems: ignoredItems, positionalReference: containableItem, customPriorityFunction: i =>
934 if (!i.HasAccess(character)) { return 0; }
936 if (container ==
null) {
return 0; }
937 if (!container.Inventory.CanBePut(containableItem)) { return 0; }
939 if (rootContainer.GetComponent<
Fabricator>() !=
null || rootContainer.GetComponent<
Deconstructor>() !=
null) { return 0; }
940 if (container.ShouldBeContained(containableItem, out
bool isRestrictionsDefined))
942 if (isRestrictionsDefined)
948 if (containableItem.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary))
950 return isPreferencesDefined ? isSecondary ? 2 : 5 : 1;
954 if (isPreferencesDefined)
957 return container.Item.HasTag(Tags.FallbackLocker) ? 0.5f : 0;
969 if (targetContainer !=
null &&
970 character.AIController is HumanAIController humanAI &&
971 humanAI.PathSteering.PathFinder.FindPath(character.SimPosition, targetContainer.SimPosition, character.Submarine, errorMsgStr: $
"FindSuitableContainer ({character.DisplayName})", nodeFilter: node => node.Waypoint.CurrentHull !=
null).Unreachable)
973 ignoredItems.Add(targetContainer);
979 suitableContainer = targetContainer;
986 private float draggedTimer;
987 private float refuseDraggingTimer;
991 private const float RefuseDraggingThresholdHigh = 10.0f;
995 private const float RefuseDraggingThresholdLow = 0.5f;
996 private const float RefuseDraggingDuration = 30.0f;
998 private void UpdateDragged(
float deltaTime)
1000 if (
Character.HumanPrefab is { AllowDraggingIndefinitely: true }) {
return; }
1009 refuseDraggingTimer -= deltaTime;
1013 draggedTimer += deltaTime;
1014 if (draggedTimer > RefuseDraggingThresholdHigh ||
1015 (refuseDraggingTimer > 0.0f && draggedTimer > RefuseDraggingThresholdLow))
1017 draggedTimer = 0.0f;
1018 refuseDraggingTimer = RefuseDraggingDuration;
1019 Character.SelectedBy.DeselectCharacter();
1020 Character.Speak(TextManager.Get(
"dialogrefusedragging").Value, delay: 0.5f, identifier:
"refusedragging".ToIdentifier(), minDurationBetweenSimilar: 5.0f);
1026 Order newOrder =
null;
1027 Hull targetHull =
null;
1035 foreach (var hull
in VisibleHulls)
1042 if (!target.
IsHandcuffed && AddTargets<AIObjectiveFightIntruders, Character>(
Character, target) && newOrder ==
null)
1045 newOrder =
new Order(orderPrefab, hull,
null, orderGiver:
Character);
1067 if (AddTargets<AIObjectiveExtinguishFires, Hull>(
Character, hull) && newOrder ==
null)
1070 newOrder =
new Order(orderPrefab, hull,
null, orderGiver:
Character);
1074 if (IsBallastFloraNoticeable(
Character, hull) && newOrder ==
null)
1077 newOrder =
new Order(orderPrefab, hull,
null, orderGiver:
Character);
1082 foreach (var gap
in hull.ConnectedGaps)
1086 if (AddTargets<AIObjectiveFixLeaks, Gap>(
Character, gap) && newOrder ==
null && !gap.IsRoomToRoom)
1089 newOrder =
new Order(orderPrefab, hull,
null, orderGiver:
Character);
1104 newOrder =
new Order(orderPrefab, hull,
null, orderGiver:
Character);
1114 if (!item.
Repairables.Any(r => r.IsBelowRepairIconThreshold)) {
continue; }
1115 if (AddTargets<AIObjectiveRepairItems, Item>(
Character, item) && newOrder ==
null && !ObjectiveManager.HasActiveObjective<
AIObjectiveRepairItem>())
1127 if (newOrder !=
null && speak)
1132 identifier: $
"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(),
1133 minDurationBetweenSimilar: 60.0f);
1150 foreach (var ballastFlora
in MapCreatures.Behavior.BallastFloraBehavior.EntityList)
1152 if (ballastFlora.Parent?.Submarine != character.Submarine) {
continue; }
1153 if (!ballastFlora.HasBrokenThrough) {
continue; }
1155 if (ballastFlora.Branches.Count(b => !b.Removed && b.Health > 0 && b.CurrentHull == hull) > 2)
1165 if (reporter ==
null || order ==
null) {
return; }
1166 var visibleHulls = targetHull is
null ?
new List<Hull>(reporter.
GetVisibleHulls()) :
new List<Hull> { targetHull };
1167 foreach (var hull
in visibleHulls)
1169 PropagateHullSafety(reporter, hull);
1170 RefreshTargets(reporter, order, hull);
1174 private void SpeakAboutIssues()
1177 if (Character.SpeechImpediment >= 100) {
return; }
1178 float minDelay = 0.5f, maxDelay = 2f;
1179 if (
Character.Oxygen < CharacterHealth.InsufficientOxygenThreshold)
1181 string msgId =
"DialogLowOxygen";
1182 Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
1184 if (
Character.Bleeding > AfflictionPrefab.Bleeding.TreatmentThreshold && !
Character.IsMedic)
1186 string msgId =
"DialogBleeding";
1187 Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
1193 string msgId =
"DialogInsufficientPressureProtection";
1194 Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
1196 else if (
Character.CurrentHull?.DisplayName !=
null)
1198 string msgId =
"DialogPressure";
1199 Character.Speak(TextManager.GetWithVariable(msgId,
"[roomname]",
Character.CurrentHull.DisplayName,
FormatCapitals.Yes).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
1206 if (healer ==
null || healAmount <= 0.0f) {
return; }
1207 if (previousHealAmounts.ContainsKey(healer))
1209 previousHealAmounts[healer] += healAmount;
1213 previousHealAmounts.Add(healer, healAmount);
1222 RespondToAttack(attacker, attackResult);
1223 wasConscious =
false;
1231 RespondToAttack(attacker, attackResult);
1234 if (previousAttackResults.ContainsKey(attacker))
1240 var matchingAffliction = previousAttackResults[attacker].Afflictions.Find(a => a.Prefab == newAffliction.
Prefab && a.Source == newAffliction.
Source);
1241 if (matchingAffliction ==
null)
1243 previousAttackResults[attacker].Afflictions.Add(newAffliction);
1247 matchingAffliction.Strength += newAffliction.
Strength;
1251 previousAttackResults[attacker] =
new AttackResult(previousAttackResults[attacker].Afflictions, previousAttackResults[attacker].HitLimb);
1255 previousAttackResults.Add(attacker, attackResult);
1261 float healAmount = 0.0f;
1262 if (attacker !=
null)
1264 previousHealAmounts.TryGetValue(attacker, out healAmount);
1267 float realDamage = attackResult.
Damage - healAmount;
1269 float totalDamage = realDamage;
1272 foreach (Affliction affliction
in attackResult.
Afflictions)
1274 totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength;
1277 if (totalDamage <= 0.01f) {
return; }
1283 objectiveManager.CreateAutonomousObjectives();
1284 objectiveManager.SortObjectives();
1308 bool isAttackerInfected =
false;
1309 bool isAttackerFightingEnemy =
false;
1310 float minorDamageThreshold = 1;
1311 float majorDamageThreshold = 20;
1314 minorDamageThreshold = 10;
1315 majorDamageThreshold = 40;
1317 bool eitherIsMentallyUnstable = IsMentallyUnstable || attacker.
AIController is { IsMentallyUnstable:
true };
1318 if (IsFriendly(attacker))
1326 float cumulativeDamage = realDamage +
Character.GetDamageDoneByAttacker(attacker);
1327 bool isAccidental = attacker.
IsBot && !eitherIsMentallyUnstable && attacker.
CombatAction ==
null;
1332 AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
1339 if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold)
1343 InformOtherNPCs(cumulativeDamage);
1348 var combatMode = DetermineCombatMode(Character, cumulativeDamage);
1349 if (attacker.
IsPlayer && !
Character.IsInstigator && !ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>())
1353 case AIObjectiveCombat.CombatMode.Defensive:
1354 case AIObjectiveCombat.CombatMode.Retreat:
1357 Character.Speak(TextManager.Get(
"dialogattackedbyfriendlysecurityresponse").Value,
null, 0.5f,
"attackedbyfriendlysecurityresponse".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
1361 Character.Speak(TextManager.Get(
"DialogAttackedByFriendly").Value,
null, 0.5f,
"attackedbyfriendly".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
1364 case AIObjectiveCombat.CombatMode.Offensive:
1365 case AIObjectiveCombat.CombatMode.Arrest:
1366 Character.Speak(TextManager.Get(
"dialogattackedbyfriendlysecurityarrest").Value,
null, 0.5f,
"attackedbyfriendlysecurityarrest".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
1368 case AIObjectiveCombat.CombatMode.None:
1369 if (
Character.IsSecurity && realDamage > 1)
1371 Character.Speak(TextManager.Get(
"dialogattackedbyfriendlysecurityresponse").Value,
null, 0.5f,
"attackedbyfriendlysecurityresponse".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
1377 AddCombatObjective(combatMode, attacker, delay: realDamage > 1 ? GetReactionTime() : 0);
1379 if (!isAttackerFightingEnemy)
1381 (GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult);
1394 AddCombatObjective(DetermineCombatMode(Character), attacker);
1398 void InformOtherNPCs(
float cumulativeDamage = 0)
1400 foreach (Character otherCharacter
in Character.CharacterList)
1402 if (otherCharacter == Character || otherCharacter.IsUnconscious || otherCharacter.Removed) {
continue; }
1403 if (otherCharacter.Submarine !=
Character.Submarine) {
continue; }
1404 if (otherCharacter.Submarine != attacker.
Submarine) {
continue; }
1405 if (otherCharacter.Info?.Job ==
null || otherCharacter.IsInstigator) {
continue; }
1406 if (otherCharacter.IsPlayer) {
continue; }
1407 if (otherCharacter.AIController is not HumanAIController otherHumanAI) {
continue; }
1408 if (!otherHumanAI.IsFriendly(Character)) {
continue; }
1409 if (attacker.
AIController is EnemyAIController enemyAI && otherHumanAI.IsFriendly(attacker))
1415 otherHumanAI.VisibleHulls.Contains(
Character.CurrentHull) ||
1416 otherHumanAI.VisibleHulls.Contains(attacker.
CurrentHull) ||
1417 otherCharacter.CanSeeTarget(attacker, seeThroughWindows:
true);
1422 if (
Character.IsDead ||
Character.IsUnconscious || otherCharacter.TeamID !=
Character.TeamID || !CheckReportRange(Character, otherCharacter, ReportRange))
1427 var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing);
1428 float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced);
1429 otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
1433 AIObjectiveCombat.CombatMode DetermineCombatMode(Character c,
float cumulativeDamage = 0,
bool isWitnessing =
false)
1435 if (c.AIController is not HumanAIController humanAI) {
return AIObjectiveCombat.CombatMode.None; }
1436 if (!IsFriendly(attacker))
1438 if (c.Submarine ==
null)
1441 return attacker.
Submarine ==
null ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat;
1443 if (!c.Submarine.GetConnectedSubs().Contains(attacker.
Submarine))
1447 humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.GetTarget() is
Controller ?
1448 AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat;
1452 return AIObjectiveCombat.CombatMode.Offensive;
1454 return humanAI.ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>() ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
1458 if (isAttackerInfected)
1460 cumulativeDamage = 100;
1464 if (GameMain.IsSingleplayer || c.TeamID != attacker.
TeamID)
1468 return Character == c && cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None;
1471 if (c.Submarine ==
null || !c.Submarine.GetConnectedSubs().Contains(attacker.
Submarine))
1474 return AIObjectiveCombat.CombatMode.None;
1477 if (
Character.CharacterList.Any(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull)))
1479 isAttackerFightingEnemy =
true;
1480 return AIObjectiveCombat.CombatMode.None;
1484 return AIObjectiveCombat.CombatMode.Offensive;
1486 if (isWitnessing && c.CombatAction !=
null && !c.IsSecurity)
1488 return c.CombatAction.WitnessReaction;
1490 if (!attacker.
IsInstigator && c.IsOnFriendlyTeam(attacker) && FindInstigator() is Character instigator)
1493 isAttackerFightingEnemy =
true;
1494 return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : instigator.CombatAction?.WitnessReaction ?? AIObjectiveCombat.CombatMode.Retreat;
1509 if (humanAI.ObjectiveManager.GetLastActiveObjective<AIObjectiveCombat>() is AIObjectiveCombat currentCombatObjective && currentCombatObjective.Enemy == attacker)
1512 cumulativeDamage *= 2;
1513 currentCombatObjective.AllowHoldFire =
false;
1514 c.IsCriminal =
true;
1519 cumulativeDamage = Math.Max(cumulativeDamage, minorDamageThreshold);
1521 if (cumulativeDamage > majorDamageThreshold)
1523 c.IsCriminal =
true;
1526 return AIObjectiveCombat.CombatMode.Offensive;
1530 return c ==
Character ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat;
1533 else if (cumulativeDamage > minorDamageThreshold)
1535 return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
1539 return AIObjectiveCombat.CombatMode.None;
1557 if (c.AIController is HumanAIController humanAi)
1559 return Character.CharacterList.FirstOrDefault(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && ch.IsInstigator && humanAi.VisibleHulls.Contains(ch.CurrentHull));
1576 if (combatObjective.Mode != mode || combatObjective.Enemy != target || (combatObjective.Enemy ==
null && target ==
null))
1579 ObjectiveManager.
Objectives.Remove(combatObjective);
1580 ObjectiveManager.
AddObjective(CreateCombatObjective());
1587 ObjectiveManager.
AddObjective(CreateCombatObjective(), delay);
1591 ObjectiveManager.
AddObjective(CreateCombatObjective());
1600 AbortCondition = abortCondition,
1601 AllowHoldFire = allowHoldFire,
1602 SpeakWarnings = speakWarnings
1604 if (onAbort !=
null)
1606 objective.Abandoned += onAbort;
1608 if (onCompleted !=
null)
1610 objective.Completed += onCompleted;
1618 objectiveManager.SetOrder(order, speak);
1620 HintManager.OnSetOrder(
Character, order);
1638 SelectedAiTarget = target;
1644 objectiveManager.SortObjectives();
1645 SortTimer = sortObjectiveInterval;
1646 float waitDuration = characterWaitOnSwitch;
1651 ObjectiveManager.
WaitTimer = waitDuration;
1654 public override bool Escape(
float deltaTime) => UpdateEscape(deltaTime, canAttackDoors:
false);
1656 private void CheckCrouching(
float deltaTime)
1658 crouchRaycastTimer -= deltaTime;
1659 if (crouchRaycastTimer > 0.0f) {
return; }
1661 crouchRaycastTimer = crouchRaycastInterval;
1672 float margin = 0.1f;
1677 float minCeilingDist = mainCollider.
Height / 2 + mainCollider.Radius + margin;
1679 shouldCrouch =
Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist,
null, Physics.CollisionWall, customPredicate: (fixture) => { return fixture.Body.UserData is not Submarine; }) !=
null;
1705 hull.
ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f))
1708 return needsAir || needsSuit;
1718 HasDivingSuit(character, conditionPercentage, requireOxygenTank) || HasDivingMask(character, conditionPercentage, requireOxygenTank);
1723 public static bool HasDivingSuit(
Character character,
float conditionPercentage = 0,
bool requireOxygenTank =
true,
bool requireSuitablePressureProtection =
true)
1724 => HasItem(character, Tags.HeavyDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped:
true,
1725 predicate: (
Item item) =>
1733 => HasItem(character, Tags.LightDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped:
true);
1735 private static List<Item> matchingItems =
new List<Item>();
1741 public static bool HasItem(
Character character, Identifier tagOrIdentifier, out IEnumerable<Item> items, Identifier containedTag =
default,
float conditionPercentage = 0,
bool requireEquipped =
false,
bool recursive =
true, Func<Item, bool> predicate =
null)
1743 matchingItems.Clear();
1744 items = matchingItems;
1745 if (character?.
Inventory ==
null) {
return false; }
1746 matchingItems = character.
Inventory.
FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) &&
1747 i.ConditionPercentage >= conditionPercentage &&
1749 (predicate ==
null || predicate(i)), recursive, matchingItems);
1750 items = matchingItems;
1751 foreach (var item
in matchingItems)
1753 if (item ==
null) {
continue; }
1755 if (containedTag.IsEmpty || item.OwnInventory ==
null)
1760 var suitableSlot = item.GetComponent<
ItemContainer>().FindSuitableSubContainerIndex(containedTag);
1761 if (suitableSlot ==
null)
1764 return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage);
1768 return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage && it.ParentInventory.IsInSlot(it, suitableSlot.Value));
1776 const float MaxDamagePerSecond = 5.0f;
1777 const float MaxDamagePerFrame = MaxDamagePerSecond * (float)Timing.Step;
1779 const float WarningThreshold = 5.0f;
1780 const float ArrestThreshold = 20.0f;
1781 const float KillThreshold = 50.0f;
1783 if (character ==
null || damageAmount <= 0.0f) {
return; }
1788 bool someoneSpoke =
false;
1789 float maxAccumulatedDamage = 0.0f;
1792 if (otherCharacter == character || otherCharacter.
TeamID == character.TeamID || otherCharacter.
IsDead ||
1793 otherCharacter.
Info?.
Job ==
null ||
1795 Vector2.DistanceSquared(otherCharacter.
WorldPosition, character.WorldPosition) > 1000.0f * 1000.0f)
1799 if (!otherCharacter.
CanSeeTarget(character, seeThroughWindows:
true)) {
continue; }
1801 otherHumanAI.structureDamageAccumulator.TryAdd(character, 0.0f);
1802 float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character];
1803 otherHumanAI.structureDamageAccumulator[character] += MathHelper.Clamp(damageAmount, -MaxDamagePerFrame, MaxDamagePerFrame);
1804 float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage);
1805 maxAccumulatedDamage = Math.Max(accumulatedDamage, maxAccumulatedDamage);
1813 if (!character.IsCriminal)
1815 if (accumulatedDamage <= WarningThreshold) {
return; }
1817 if (accumulatedDamage > WarningThreshold && prevAccumulatedDamage <= WarningThreshold &&
1818 !someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f)
1821 if (accumulatedDamage < ArrestThreshold)
1823 if (otherHumanAI.ObjectiveManager.CurrentObjective is
AIObjectiveIdle idleObjective)
1825 idleObjective.FaceTargetAndWait(character, 5.0f);
1828 otherCharacter.
Speak(TextManager.Get(
"dialogdamagewallswarning").Value,
null, Rand.Range(0.5f, 1.0f),
"damageoutpostwalls".ToIdentifier(), 10.0f);
1829 someoneSpoke =
true;
1834 if (character.IsCriminal ||
1835 (accumulatedDamage > ArrestThreshold && prevAccumulatedDamage <= ArrestThreshold) ||
1836 (accumulatedDamage > KillThreshold && prevAccumulatedDamage <= KillThreshold))
1841 character.IsCriminal =
true;
1843 if (!TriggerSecurity(otherHumanAI, combatMode))
1846 foreach (
Character security
in Character.
CharacterList.Where(c => c.TeamID == otherCharacter.
TeamID).OrderBy(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition)))
1860 if (humanAI ==
null) {
return false; }
1869 if (anyCharacter.AIController is HumanAIController anyAI)
1871 anyAI.structureDamageAccumulator?.Remove(character);
1881 if (item ==
null || thief ==
null || item.GetComponent<
LevelResource>() !=
null) {
return; }
1882 bool someoneSpoke =
false;
1900 if (!otherCharacter.
CanSeeTarget(thief, seeThroughWindows:
true)) {
continue; }
1906 if (item.
HasTag(Tags.FireExtinguisher) && connectedHulls.Any(h => h.FireSources.Any())) {
continue; }
1917 ApplyStealingReputationLoss(item);
1919 HintManager.OnStoleItem(thief, item);
1924 otherCharacter.
Speak(TextManager.Get(
"dialogstealwarning").Value,
null, Rand.Range(0.5f, 1.0f),
"thief".ToIdentifier(), 10.0f);
1925 someoneSpoke =
true;
1928 if (!TriggerSecurity(otherHumanAI))
1944 ItemTaken(foundItem, thief);
1949 if (humanAI ==
null) {
return false; }
1954 findThieves.InspectEveryone();
1958 abortCondition: obj => !isCriminal && thief.
Inventory.
FindItem(it => it.Illegitimate, recursive:
true) ==
null,
1961 if (!item.Removed && !humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveGetItem>())
1963 humanAI.ObjectiveManager.AddObjective(new AIObjectiveGetItem(humanAI.Character, item, humanAI.ObjectiveManager, equip: false)
1969 allowHoldFire: !isCriminal,
1970 speakWarnings: !isCriminal);
1980 var reputationLoss = MathHelper.Clamp(
1988 private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f);
1997 DoForEachBot(character, (humanAi) => humanAi.RefreshHullSafety(hull));
2002 private void RefreshHullSafety(
Hull hull)
2004 var visibleHulls = dirtyHullSafetyCalculations.Contains(hull) ? hull.
GetConnectedHulls(includingThis:
true, searchDepth: 1) : VisibleHulls;
2005 float hullSafety = GetHullSafety(hull,
Character, visibleHulls);
2006 if (hullSafety > HULL_SAFETY_THRESHOLD)
2008 UnsafeHulls.Remove(hull);
2012 UnsafeHulls.Add(hull);
2018 switch (order.
Identifier.Value.ToLowerInvariant())
2021 AddTargets<AIObjectiveExtinguishFires, Hull>(character, hull);
2023 case "reportbreach":
2028 AddTargets<AIObjectiveFixLeaks, Gap>(character, gap);
2032 case "reportbrokendevices":
2035 if (item.CurrentHull != hull) {
continue; }
2038 if (item.Repairables.All(r => r.IsBelowRepairThreshold)) {
continue; }
2039 AddTargets<AIObjectiveRepairItems, Item>(character, item);
2043 case "reportintruders":
2046 if (enemy.CurrentHull != hull) {
continue; }
2049 AddTargets<AIObjectiveFightIntruders, Character>(character, enemy);
2053 case "requestfirstaid":
2056 if (c.CurrentHull != hull) {
continue; }
2059 AddTargets<AIObjectiveRescueAll, Character>(character, c);
2065 DebugConsole.ThrowError(order.
Identifier +
" not implemented!");
2073 bool targetAdded =
false;
2074 DoForEachBot(caller, humanAI =>
2076 if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; }
2077 var objective = humanAI.ObjectiveManager.GetObjective<T1>();
2078 if (objective !=
null)
2080 if (!targetAdded && objective.AddTarget(target))
2085 }, range: (caller.AIController as HumanAIController)?.ReportRange ??
float.PositiveInfinity);
2091 DoForEachBot(caller, humanAI =>
2092 humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target));
2095 private void StoreHullSafety(
Hull hull, HullSafety safety)
2097 if (knownHulls.ContainsKey(hull))
2100 knownHulls[hull] = safety;
2105 knownHulls.Add(hull, safety);
2109 private float CalculateHullSafety(Hull hull, Character character, IEnumerable<Hull> visibleHulls =
null)
2111 bool isCurrentHull = character ==
Character && character.CurrentHull == hull;
2114 float hullSafety = character.IsProtectedFromPressure ? 0 : 100;
2117 CurrentHullSafety = hullSafety;
2121 if (isCurrentHull && visibleHulls ==
null)
2124 visibleHulls = VisibleHulls;
2126 bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
2127 bool ignoreOxygen = HasDivingGear(character);
2128 bool ignoreEnemies = ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>();
2129 float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater:
false, ignoreOxygen, ignoreFire, ignoreEnemies);
2132 CurrentHullSafety = safety;
2137 private static float CalculateHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character,
bool ignoreWater =
false,
bool ignoreOxygen =
false,
bool ignoreFire =
false,
bool ignoreEnemies =
false)
2139 bool isProtectedFromPressure = character.IsProtectedFromPressure;
2140 if (hull ==
null) {
return isProtectedFromPressure ? 100 : 0; }
2141 if (hull.LethalPressure > 0 && !isProtectedFromPressure) {
return 0; }
2144 float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage));
2145 float waterFactor = 1;
2148 if (visibleHulls !=
null)
2151 float relativeWaterVolume = visibleHulls.Sum(s => s.WaterVolume) / visibleHulls.Sum(s => s.Volume);
2152 waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume);
2156 float relativeWaterVolume = hull.WaterVolume / hull.Volume;
2157 waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume);
2160 if (!character.NeedsOxygen || character.CharacterHealth.OxygenLowResistance >= 1)
2164 if (isProtectedFromPressure)
2168 float fireFactor = 1;
2171 static float calculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
2173 float fire = visibleHulls ==
null ? calculateFire(hull) : visibleHulls.Sum(h => calculateFire(h));
2174 fireFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1));
2176 float enemyFactor = 1;
2180 foreach (Character c
in Character.CharacterList)
2182 if (visibleHulls ==
null)
2184 if (c.CurrentHull != hull) {
continue; }
2188 if (!visibleHulls.Contains(c.CurrentHull)) {
continue; }
2190 if (IsActive(c) && !IsFriendly(character, c) && !c.IsHandcuffed)
2196 enemyFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1));
2198 float dangerousItemsFactor = 1f;
2199 foreach (Item item
in Item.DangerousItems)
2201 if (item.CurrentHull == hull)
2203 dangerousItemsFactor = 0;
2207 float safety = oxygenFactor * waterFactor * fireFactor * enemyFactor * dangerousItemsFactor;
2208 return MathHelper.Clamp(safety * 100, 0, 100);
2215 return CalculateHullSafety(hull, character, visibleHulls);
2217 if (!knownHulls.TryGetValue(hull, out HullSafety hullSafety))
2219 hullSafety =
new HullSafety(CalculateHullSafety(hull, character, visibleHulls));
2220 StoreHullSafety(hull, hullSafety);
2222 else if (hullSafety.IsStale)
2224 hullSafety.Reset(CalculateHullSafety(hull, character, visibleHulls));
2226 return hullSafety.safety;
2229 public static float GetHullSafety(
Hull hull, IEnumerable<Hull> visibleHulls,
Character character,
bool ignoreWater =
false,
bool ignoreOxygen =
false,
bool ignoreFire =
false,
bool ignoreEnemies =
false)
2233 return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
2235 HullSafety hullSafety;
2238 if (!controller.knownHulls.TryGetValue(hull, out hullSafety))
2240 hullSafety =
new HullSafety(CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies));
2241 controller.StoreHullSafety(hull, hullSafety);
2243 else if (hullSafety.IsStale)
2245 hullSafety.Reset(CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies));
2251 DebugConsole.ThrowError(
"Cannot store the hull safety, because was unable to cast the AIController as HumanAIController. This should never happen!");
2253 return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
2255 return hullSafety.safety;
2305 return !npcAI.IsInHostileFaction();
2318 Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty;
2320 if (npcFaction.IsEmpty)
2323 npcFaction = currentLocationFaction;
2325 if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction)
2327 if (campaign.CurrentLocation is { IsFactionHostile: true })
2339 if (character ==
null) {
return false; }
2342 if (!IsBotInTheCrew(character, c)) {
continue; }
2353 if (character ==
null) {
return false; }
2356 if (!IsBotInTheCrew(character, c)) {
continue; }
2367 if (character ==
null) {
return 0; }
2371 if (!IsBotInTheCrew(character, other)) {
continue; }
2387 if (!IsActive(c)) {
continue; }
2389 if (onlyActive && c.IsIncapacitated) {
continue; }
2390 if (onlyConnectedSubs)
2394 if (c.Submarine !=
null)
2412 private static void DoForEachBot(
Character character, Action<HumanAIController> action,
float range =
float.PositiveInfinity)
2414 if (character ==
null) {
return; }
2415 foreach (var c
in Character.CharacterList)
2417 if (IsBotInTheCrew(character, c) && CheckReportRange(character, c, range))
2419 action(c.AIController as HumanAIController);
2424 private static bool CheckReportRange(Character character, Character target,
float range)
2426 if (
float.IsPositiveInfinity(range)) {
return true; }
2427 if (character.CurrentHull ==
null || target.CurrentHull ==
null)
2429 return Vector2.DistanceSquared(character.WorldPosition, target.WorldPosition) <= range * range;
2433 return character.CurrentHull.GetApproximateDistance(character.Position, target.Position, target.CurrentHull, range, distanceMultiplierPerClosedDoor: 2) <= range;
2437 private static bool IsBotInTheCrew(Character
self, Character other) => IsActive(other) && other.TeamID ==
self.TeamID && !other.IsIncapacitated && other.IsBot && other.AIController is HumanAIController;
2441 operatingCharacter =
null;
2442 if (target?.
Item ==
null) {
return false; }
2443 float highestPriority = -1.0f;
2444 float highestPriorityModifier = -1.0f;
2447 if (c ==
null) {
continue; }
2449 if (c.
TeamID != team) {
continue; }
2453 operatingCharacter = c;
2458 foreach (var objective
in objectiveManager.Objectives)
2461 if (operateObjective.Component?.Item != target.Item) {
continue; }
2462 if (operateObjective.Priority < highestPriority) {
continue; }
2463 if (operateObjective.PriorityModifier < highestPriorityModifier) {
continue; }
2464 operatingCharacter = c;
2465 highestPriority = operateObjective.Priority;
2466 highestPriorityModifier = operateObjective.PriorityModifier;
2470 return operatingCharacter !=
null;
2478 if (target?.
Item ==
null) {
return false; }
2482 if (!IsActive(c)) {
continue; }
2501 bool isTargetOrdered = IsOrderedToOperateThis(c.
AIController);
2502 if (!isOrder && isTargetOrdered)
2510 if (isOrder && !isTargetOrdered)
2517 if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective)
2531 else if (target.DegreeOfSuccess(
Character) <= target.DegreeOfSuccess(c))
2540 return other !=
null;
2547 if (
Character ==
null) {
return false; }
2548 if (target ==
null) {
return false; }
2552 if (!IsActive(c)) {
continue; }
2558 if (target.Repairables.Any(r => r.CurrentFixer == c))
2567 if (repairItemsObjective ==
null) {
continue; }
2573 bool isTargetOrdered = IsOrderedToRepairThis(operatingAI);
2574 if (!isOrder && isTargetOrdered)
2581 if (isOrder && !isTargetOrdered)
2588 if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective)
2593 return target.Repairables.Max(r => r.DegreeOfSuccess(
Character)) <= target.Repairables.Max(r => r.DegreeOfSuccess(c));
readonly Character Character
virtual bool IsMentallyUnstable
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
SteeringManager steeringManager
bool HasValidPath(bool requireNonDirty=true, bool requireUnfinished=true, Func< WayPoint, bool > nodePredicate=null)
Is the current path valid, using the provided parameters.
override bool IsValidTarget(Hull hull)
override bool IsValidTarget(Character target)
static bool IsSuitablePressureProtection(Item item, Identifier tag, Character character)
override bool IsValidTarget(Gap gap)
IEnumerable< AIObjective > GetSubObjectivesRecursive(bool includingSelf=false)
void AddSubObjective(AIObjective objective, bool addFirst=false)
virtual bool AllowAutomaticItemUnequipping
There's a separate property for diving suit and mask: KeepDivingGearOn.
An objective that creates specific kinds of subobjectives for specific types of targets,...
AIObjective GetActiveObjective()
const float RunPriority
Objectives with a priority equal to or higher than this make the character run.
AIObjective CreateObjective(Order order, float priorityModifier=1)
float GetCurrentPriority()
Returns the highest priority of the current objective and its subobjectives.
List< AIObjective > Objectives
Excluding the current order.
void SetForcedOrder(AIObjective objective)
void AddObjective(AIObjective objective)
AIObjective CurrentObjective
Includes orders.
AIObjective?? CurrentOrder
The AIObjective in CurrentOrders with the highest AIObjective.Priority
List< Order > CurrentOrders
float? WaitTimer
When set above zero, the character will stand still doing nothing until the timer runs out....
override bool IsValidTarget(Item item)
override bool IsValidTarget(Character target)
Character Source
Which character gave this affliction
readonly AfflictionPrefab Prefab
float GetCurrentSpeed(bool useMaxSpeed)
Action BeforeLevelLoading
Automatically cleared after triggering -> no need to unregister
float OxygenLowResistance
0-1.
float GetAfflictionStrengthByType(Identifier afflictionType, bool allowLimbAfflictions=true)
float GetSkillLevel(string skillIdentifier)
void Speak(string message, ChatMessageType? messageType=null, float delay=0.0f, Identifier identifier=default, float minDurationBetweenSimilar=0.0f)
CharacterHealth CharacterHealth
virtual AIController AIController
CampaignMode.InteractionType CampaignInteractionType
CharacterTeamType?? OriginalTeamID
bool HasEquippedItem(Item item, InvSlotType? slotType=null, Func< InvSlotType, bool > predicate=null)
override Vector2? SimPosition
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
bool IsCriminal
Do the outpost security officers treat the character as a criminal? Triggers when the character has e...
static readonly List< Character > CharacterList
Character SelectedCharacter
CharacterInventory Inventory
bool IsSameSpeciesOrGroup(Character other)
bool IsHostileEscortee
Set true only, if the character is turned hostile from an escort mission (See EscortMission).
bool IsKeyDown(InputType inputType)
bool FindItem(ref int itemIndex, out Item targetItem, IEnumerable< Identifier > identifiers=null, bool ignoreBroken=true, IEnumerable< Item > ignoredItems=null, IEnumerable< Identifier > ignoredContainerIdentifiers=null, Func< Item, bool > customPredicate=null, Func< Item, float > customPriorityFunction=null, float maxItemDistance=10000, ISpatialEntity positionalReference=null)
Finds the closest item seeking by identifiers or tags from the world. Ignores items that are outside ...
static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
override Vector2 Position
Vector2 ApplyMovementLimits(Vector2 targetMovement, float currentSpeed)
readonly AnimController AnimController
List< Hull > GetVisibleHulls()
Returns hulls that are visible to the character, including the current hull. Note that this is not an...
IEnumerable< Item >?? HeldItems
Items the character has in their hand slots. Doesn't return nulls and only returns items held in both...
CombatAction CombatAction
bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity=null, bool seeThroughWindows=false, bool checkFacing=false)
Stores information about the Character that is needed between rounds in the menu etc....
static int HighestManualOrderPriority
override bool TryPutItem(Item item, Character user, IEnumerable< InvSlotType > allowedSlots=null, bool createNetworkEvent=true, bool ignoreCondition=false)
If there is room, puts the item in the inventory and returns true, otherwise returns false
Item FindEquippedItemByTag(Identifier tag)
AIObjectiveCombat.CombatMode WitnessReaction
AIObjectiveCombat.CombatMode GuardReaction
bool AddOrder(Order order, float? fadeOutTime)
virtual Vector2 WorldPosition
static GameSession?? GameSession
LocalizedString DisplayName
readonly List< Gap > ConnectedGaps
IEnumerable< Hull > GetConnectedHulls(bool includingThis, int? searchDepth=null, bool ignoreClosedGaps=false)
const float HULL_SAFETY_THRESHOLD
static bool IsTrueForAnyBotInTheCrew(Character character, Func< HumanAIController, bool > predicate)
static int CountBotsInTheCrew(Character character, Func< HumanAIController, bool > predicate=null)
float Hearing
Affects how far the character can hear sounds created by AI targets with the tag ProvocativeToHumanAI...
static bool HasDivingSuit(Character character, float conditionPercentage=0, bool requireOxygenTank=true, bool requireSuitablePressureProtection=true)
Check whether the character has a diving suit in usable condition, suitable pressure protection for t...
bool IsInHostileFaction()
bool IsItemOperatedByAnother(ItemComponent target, out Character other)
bool AllowCampaignInteraction()
int CountBotsInTheCrew(Func< HumanAIController, bool > predicate=null)
float FindWeaponsRange
How far the character can seek new weapons from.
static bool HasItem(Character character, Identifier tagOrIdentifier, out IEnumerable< Item > items, Identifier containedTag=default, float conditionPercentage=0, bool requireEquipped=false, bool recursive=true, Func< Item, bool > predicate=null)
Note: uses a single list for matching items. The item is reused each time when the method is called....
bool IsTrueForAllBotsInTheCrew(Func< HumanAIController, bool > predicate)
static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter)
const float HULL_LOW_OXYGEN_PERCENTAGE
static bool HasDivingGear(Character character, float conditionPercentage=0, bool requireOxygenTank=true)
void SetOrder(Order order, bool speak=true)
readonly HashSet< Hull > UnreachableHulls
static bool IsActive(Character c)
static void ApplyStealingReputationLoss(Item item)
static bool FindSuitableContainer(Character character, Item containableItem, List< Item > ignoredItems, ref int itemIndex, out Item suitableContainer)
void AskToRecalculateHullSafety(Hull hull)
float GetHullSafety(Hull hull, Character character, IEnumerable< Hull > visibleHulls=null)
void InitShipCommandManager()
override void OnHealed(Character healer, float healAmount)
bool NeedsDivingGear(Hull hull, out bool needsSuit)
override void OnAttacked(Character attacker, AttackResult attackResult)
bool IsItemRepairedByAnother(Item target, out Character other)
override void Update(float deltaTime)
static bool DisableCrewAI
readonly List< Item > IgnoredItems
bool UseOutsideWaypoints
Waypoints that are not linked to a sub (e.g. main path).
bool AutoFaceMovement
Resets each frame
bool IsTrueForAnyCrewMember(Func< Character, bool > predicate, bool onlyActive=true, bool onlyConnectedSubs=false)
Including the player characters in the same team.
static void PropagateHullSafety(Character character, Hull hull)
Updates the hull safety for all ai characters in the team. The idea is that the crew communicates (ma...
AIObjectiveManager ObjectiveManager
AIObjective SetForcedOrder(Order order)
static bool IsFriendly(Character me, Character other, bool onlySameTeam=false)
static void ItemTaken(Item item, Character thief)
bool IsTrueForAnyBotInTheCrew(Func< HumanAIController, bool > predicate)
HumanAIController(Character c)
void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay=0, Func< AIObjective, bool > abortCondition=null, Action onAbort=null, Action onCompleted=null, bool allowHoldFire=false, bool speakWarnings=false)
readonly HashSet< Hull > UnsafeHulls
bool FindSuitableContainer(Item containableItem, out Item suitableContainer)
static bool IsBallastFloraNoticeable(Character character, Hull hull)
MentalStateManager MentalStateManager
override void SelectTarget(AITarget target)
bool IsFriendly(Character other, bool onlySameTeam=false)
static bool IsTrueForAllBotsInTheCrew(Character character, Func< HumanAIController, bool > predicate)
static void ReportProblem(Character reporter, Order order, Hull targetHull=null)
static bool HasDivingMask(Character character, float conditionPercentage=0, bool requireOxygenTank=true)
Check whether the character has a diving mask in usable condition plus some oxygen.
void HandleRelocation(Item item)
IndoorsSteeringManager PathSteering
static void RefreshTargets(Character character, Order order, Hull hull)
float ReportRange
How far other characters can hear reports done by this character (e.g. reports for fires,...
void InitMentalStateManager()
ShipCommandManager ShipCommandManager
override bool IsMentallyUnstable
override bool Escape(float deltaTime)
static void StructureDamaged(Structure structure, float damageAmount, Character character)
static float GetHullSafety(Hull hull, IEnumerable< Hull > visibleHulls, Character character, bool ignoreWater=false, bool ignoreOxygen=false, bool ignoreFire=false, bool ignoreEnemies=false)
bool IsNextLadderSameAsCurrent
Item FindItem(Func< Item, bool > predicate, bool recursive)
List< Item > FindAllItems(Func< Item, bool > predicate=null, bool recursive=false, List< Item > list=null)
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)
ItemInventory OwnInventory
List< Repairable > Repairables
void SetTransform(Vector2 simPosition, float rotation, bool findNewHull=true, bool setPrevTransform=true)
bool Illegitimate
Item shouldn't be in the player's inventory. If the guards find it, they will consider it as a theft.
void SecondaryUse(float deltaTime, Character character=null)
bool?? SpawnedInCurrentOutpost
static IReadOnlyCollection< Item > RepairableItems
Items that have one more more Repairable component
bool HasTag(Identifier tag)
Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id=Entity.NullEntityID, bool callOnItemLoaded=true)
bool StolenDuringRound
Was the item stolen during the current round. Note that it's possible for the items to be found in th...
Entity GetRootInventoryOwner()
override bool TryPutItem(Item item, Character user, IEnumerable< InvSlotType > allowedSlots=null, bool createNetworkEvent=true, bool ignoreCondition=false)
If there is room, puts the item in the inventory and returns true, otherwise returns false
The base class for components holding the different functionalities of the item
override Vector2 SimPosition
void Update(float deltaTime)
string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, Identifier orderOption=default, bool isNewOrder=true)
Order WithTargetEntity(Entity entity)
Order WithOrderGiver(Character orderGiver)
static readonly PrefabCollection< OrderPrefab > Prefabs
Vector2 GetLocalFront(float? spritesheetRotation=null)
Returns the farthest point towards the forward of the body. For capsules and circles,...
float TransformedRotation
Takes flipping (Dir) into account.
readonly Identifier Identifier
bool TryGetCollider(int index, out PhysicsBody collider)
Vector2 GetColliderBottom()
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.
const float MaxReputationLossPerStolenItem
const float ReputationLossPerStolenItemPrice
const float MaxReputationLossFromWallDamage
Maximum amount of reputation loss you can get from damaging outpost walls per round
const float MinReputationLossPerStolenItem
void AddReputation(float reputationChange, float maxReputationChangePerRound=float.MaxValue)
const float ReputationLossPerWallDamage
void Update(float deltaTime)
virtual void Update(float speed)
Update speed for the steering. Should normally match the characters current animation speed.
new StructurePrefab Prefab
bool IndestructibleInOutposts
readonly Dictionary< Submarine, DockingPort > ConnectedDockingPorts
static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel=false, bool ignoreSubs=false, bool ignoreSensors=true, bool ignoreDisabledWalls=true, bool ignoreBranches=true, Predicate< Fixture > blocksVisibilityPredicate=null)
Check visibility between two points (in sim units).
IEnumerable< Submarine > GetConnectedSubs()
Returns a list of all submarines that are connected to this one via docking ports,...
override Vector2 SimPosition
List< Hull > GetHulls(bool alsoFromConnectedSubs)
static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable< Body > ignoredBodies=null, Category? collisionCategory=null, bool ignoreSensors=true, Predicate< Fixture > customPredicate=null, bool allowInsideFixture=false)
bool HasTag(SubmarineTag tag)
static WayPoint GetRandom(SpawnType spawnType=SpawnType.Human, JobPrefab assignedJob=null, Submarine sub=null, bool useSyncedRand=false, string spawnPointTag=null, bool ignoreSubmarine=false)
readonly List< Affliction > Afflictions