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;
325 Vector2 rayStart =
SimPosition - connectedSub.SimPosition;
348 enemyCheckTimer -= deltaTime;
349 if (enemyCheckTimer < 0)
352 enemyCheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f);
355 bool useInsideSteering = !isOutside || isBlocked ||
HasValidPath() || IsCloseEnoughToTarget(steeringBuffer);
356 if (useInsideSteering)
360 insideSteering.Reset();
364 if (IsCloseEnoughToTarget(maxSteeringBuffer))
366 steeringBuffer += steeringBufferIncreaseSpeed * deltaTime;
370 steeringBuffer = minSteeringBuffer;
377 outsideSteering.
Reset();
380 steeringBuffer = minSteeringBuffer;
382 steeringBuffer = Math.Clamp(steeringBuffer, minSteeringBuffer, maxSteeringBuffer);
385 CheckCrouching(deltaTime);
394 objectiveManager.SortObjectives();
397 objectiveManager.UpdateObjectives(deltaTime);
399 UpdateDragged(deltaTime);
401 if (reportProblemsTimer > 0)
403 reportProblemsTimer -= deltaTime;
405 if (reactTimer > 0.0f)
407 reactTimer -= deltaTime;
408 if (findItemState != FindItemState.None)
411 UnequipUnnecessaryItems();
424 dirtyHullSafetyCalculations.Remove(h);
431 RefreshHullSafety(h);
432 dirtyHullSafetyCalculations.Remove(h);
435 foreach (
Hull h
in dirtyHullSafetyCalculations)
437 RefreshHullSafety(h);
440 dirtyHullSafetyCalculations.Clear();
441 if (reportProblemsTimer <= 0.0f)
456 reportProblemsTimer = reportProblemsInterval;
459 UnequipUnnecessaryItems();
460 reactTimer = GetReactionTime();
463 if (objectiveManager.CurrentObjective ==
null) {
return; }
465 objectiveManager.DoCurrentObjective(deltaTime);
466 var currentObjective = objectiveManager.CurrentObjective;
467 bool run = !currentObjective.ForceWalk && (currentObjective.ForceRun || objectiveManager.GetCurrentPriority() >
AIObjectiveManager.
RunPriority);
475 else if (goTo.Target !=
null)
484 if (Math.Abs(yDiff) > 100)
491 run = Math.Abs(xDiff) > 500;
507 if (currPath !=
null && currPath.CurrentNode !=
null)
514 ignorePlatforms = height < allowedJumpHeight;
531 flipTimer -= deltaTime;
532 if (flipTimer <= 0.0f)
538 if (cursorDiffX > 10.0f)
542 else if (cursorDiffX < -10.0f)
558 flipTimer = FlipInterval;
567 private void CheckEnemies()
572 float closestDistance = 0;
577 if (c.Removed || c.IsDead || c.IsIncapacitated) {
continue; }
580 float dist = toTarget.LengthSquared();
581 float maxDistance =
Character.
Submarine ==
null ? enemySpotDistanceOutside : enemySpotDistanceInside;
582 if (dist > maxDistance * maxDistance) {
continue; }
583 if (EnemyAIController.IsLatchedToSomeoneElse(c,
Character)) {
continue; }
585 if (head ==
null) {
continue; }
587 Vector2 forward = VectorExtensions.Forward(rotation);
588 float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward));
589 if (angle > 70) {
continue; }
591 if (dist < closestDistance || closestEnemy ==
null)
594 closestDistance = dist;
597 if (closestEnemy !=
null)
603 private void UnequipUnnecessaryItems()
611 bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
614 Hull targetHull = gotoObjective.GetTargetHull();
622 if (findItemState != FindItemState.OtherItem)
624 var decontain =
ObjectiveManager.GetActiveObjectives<AIObjectiveDecontainItem>().LastOrDefault();
625 if (decontain !=
null && decontain.TargetItem !=
null && decontain.TargetItem.HasTag(Tags.HeavyDivingGear) &&
629 gotoObjective.Abandon =
true;
632 if (!shouldActOnSuffocation)
639 if (shouldActOnSuffocation || findItemState != FindItemState.OtherItem)
642 if (!needsGear || shouldActOnSuffocation)
644 bool isCurrentObjectiveFindSafety =
ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>();
645 bool shouldKeepTheGearOn =
646 isCurrentObjectiveFindSafety ||
658 bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait();
661 shouldKeepTheGearOn =
false;
663 removeDivingSuit =
true;
665 bool takeMaskOff = !shouldKeepTheGearOn;
666 if (!shouldKeepTheGearOn && !shouldActOnSuffocation)
670 removeDivingSuit =
true;
675 bool removeSuit =
false;
676 bool removeMask =
false;
679 if (objective is AIObjectiveGoTo gotoObjective)
681 if (NeedsDivingGearOnPath(gotoObjective))
683 removeDivingSuit =
false;
687 else if (gotoObjective.Mimic)
692 removeDivingSuit = !targetHasDivingGear;
693 if (removeDivingSuit)
700 takeMaskOff = !targetHasDivingGear;
711 if (removeDivingSuit)
714 if (divingSuit !=
null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors) && divingSuit.IsInteractable(
Character))
722 else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit)
724 findItemState = FindItemState.DivingSuit;
727 findItemState = FindItemState.None;
729 if (targetContainer !=
null)
735 decontainObjective.Abandoned += () =>
767 else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
769 findItemState = FindItemState.DivingMask;
772 findItemState = FindItemState.None;
774 if (targetContainer !=
null)
777 decontainObjective.Abandoned += () =>
804 if (isCarrying) {
return; }
812 if (item ==
null || !item.IsInteractable(
Character)) {
continue; }
815 if (item.HasTag(Tags.Weapon))
822 findItemState = FindItemState.OtherItem;
825 findItemState = FindItemState.None;
827 if (targetContainer !=
null)
830 decontainObjective.Abandoned += () =>
848 private readonly HashSet<Item> itemsToRelocate =
new HashSet<Item>();
858 if (itemsToRelocate.Contains(item)) {
return; }
859 itemsToRelocate.Add(item);
862 myPort.OnUnDocked += Relocate;
865 if (campaign !=
null)
869 campaign.OnSaveAndQuit += Relocate;
870 campaign.ItemsRelocatedToMainSub =
true;
873 HintManager.OnItemMarkedForRelocation();
877 if (item ==
null || item.
Removed) {
return; }
878 if (!itemsToRelocate.Contains(item)) {
return; }
880 if (mainSub ==
null) {
return; }
903 if (owner !=
null && owner != item)
908 Item newContainer = mainSub.FindContainerFor(item, onlyPrimary:
false);
918 DebugConsole.AddWarning($
"Failed to relocate item {item.Prefab.Identifier} ({item.ID}), because no cargo spawn point could be found!");
921 itemsToRelocate.Remove(item);
922 DebugConsole.Log($
"Relocated item {item.Prefab.Identifier} ({item.ID}) back to the main sub.");
926 private enum FindItemState
933 private FindItemState findItemState;
934 private int itemIndex;
940 suitableContainer =
null;
941 if (character.
FindItem(ref itemIndex, out
Item targetContainer, ignoredItems: ignoredItems, positionalReference: containableItem, customPriorityFunction: i =>
943 if (!i.HasAccess(character)) { return 0; }
945 if (container ==
null) {
return 0; }
946 if (!container.Inventory.CanBePut(containableItem)) { return 0; }
948 if (rootContainer.GetComponent<
Fabricator>() !=
null || rootContainer.GetComponent<
Deconstructor>() !=
null) { return 0; }
949 if (container.ShouldBeContained(containableItem, out
bool isRestrictionsDefined))
951 if (isRestrictionsDefined)
957 if (containableItem.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary))
959 return isPreferencesDefined ? isSecondary ? 2 : 5 : 1;
963 if (isPreferencesDefined)
966 return container.Item.HasTag(Tags.FallbackLocker) ? 0.5f : 0;
978 if (targetContainer !=
null &&
979 character.AIController is HumanAIController humanAI &&
980 humanAI.PathSteering.PathFinder.FindPath(character.SimPosition, targetContainer.SimPosition, character.Submarine, errorMsgStr: $
"FindSuitableContainer ({character.DisplayName})", nodeFilter: node => node.Waypoint.CurrentHull !=
null).Unreachable)
982 ignoredItems.Add(targetContainer);
988 suitableContainer = targetContainer;
995 private float draggedTimer;
996 private float refuseDraggingTimer;
1000 private const float RefuseDraggingThresholdHigh = 10.0f;
1004 private const float RefuseDraggingThresholdLow = 0.5f;
1005 private const float RefuseDraggingDuration = 30.0f;
1007 private void UpdateDragged(
float deltaTime)
1009 if (
Character.HumanPrefab is { AllowDraggingIndefinitely: true }) {
return; }
1018 refuseDraggingTimer -= deltaTime;
1022 draggedTimer += deltaTime;
1023 if (draggedTimer > RefuseDraggingThresholdHigh ||
1024 (refuseDraggingTimer > 0.0f && draggedTimer > RefuseDraggingThresholdLow))
1026 draggedTimer = 0.0f;
1027 refuseDraggingTimer = RefuseDraggingDuration;
1028 Character.SelectedBy.DeselectCharacter();
1029 Character.Speak(TextManager.Get(
"dialogrefusedragging").Value, delay: 0.5f, identifier:
"refusedragging".ToIdentifier(), minDurationBetweenSimilar: 5.0f);
1035 Order newOrder =
null;
1036 Hull targetHull =
null;
1044 foreach (var hull
in VisibleHulls)
1051 if (!target.
IsHandcuffed && AddTargets<AIObjectiveFightIntruders, Character>(
Character, target) && newOrder ==
null)
1054 newOrder =
new Order(orderPrefab, hull,
null, orderGiver:
Character);
1076 if (AddTargets<AIObjectiveExtinguishFires, Hull>(
Character, hull) && newOrder ==
null)
1079 newOrder =
new Order(orderPrefab, hull,
null, orderGiver:
Character);
1083 if (IsBallastFloraNoticeable(
Character, hull) && newOrder ==
null)
1086 newOrder =
new Order(orderPrefab, hull,
null, orderGiver:
Character);
1091 foreach (var gap
in hull.ConnectedGaps)
1095 if (AddTargets<AIObjectiveFixLeaks, Gap>(
Character, gap) && newOrder ==
null && !gap.IsRoomToRoom)
1098 newOrder =
new Order(orderPrefab, hull,
null, orderGiver:
Character);
1113 newOrder =
new Order(orderPrefab, hull,
null, orderGiver:
Character);
1123 if (!item.
Repairables.Any(r => r.IsBelowRepairIconThreshold)) {
continue; }
1124 if (AddTargets<AIObjectiveRepairItems, Item>(
Character, item) && newOrder ==
null && !ObjectiveManager.HasActiveObjective<
AIObjectiveRepairItem>())
1136 if (newOrder !=
null && speak)
1141 identifier: $
"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(),
1142 minDurationBetweenSimilar: 60.0f);
1159 foreach (var ballastFlora
in MapCreatures.Behavior.BallastFloraBehavior.EntityList)
1161 if (ballastFlora.Parent?.Submarine != character.Submarine) {
continue; }
1162 if (!ballastFlora.HasBrokenThrough) {
continue; }
1164 if (ballastFlora.Branches.Count(b => !b.Removed && b.Health > 0 && b.CurrentHull == hull) > 2)
1174 if (reporter ==
null || order ==
null) {
return; }
1175 var visibleHulls = targetHull is
null ?
new List<Hull>(reporter.
GetVisibleHulls()) :
new List<Hull> { targetHull };
1176 foreach (var hull
in visibleHulls)
1178 PropagateHullSafety(reporter, hull);
1179 RefreshTargets(reporter, order, hull);
1183 private void SpeakAboutIssues()
1186 if (Character.SpeechImpediment >= 100) {
return; }
1187 float minDelay = 0.5f, maxDelay = 2f;
1188 if (
Character.Oxygen < CharacterHealth.InsufficientOxygenThreshold)
1190 string msgId =
"DialogLowOxygen";
1191 Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
1193 if (
Character.Bleeding > AfflictionPrefab.Bleeding.TreatmentThreshold && !
Character.IsMedic)
1195 string msgId =
"DialogBleeding";
1196 Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
1202 string msgId =
"DialogInsufficientPressureProtection";
1203 Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
1205 else if (
Character.CurrentHull?.DisplayName !=
null)
1207 string msgId =
"DialogPressure";
1208 Character.Speak(TextManager.GetWithVariable(msgId,
"[roomname]",
Character.CurrentHull.DisplayName,
FormatCapitals.Yes).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
1215 if (healer ==
null || healAmount <= 0.0f) {
return; }
1216 if (previousHealAmounts.ContainsKey(healer))
1218 previousHealAmounts[healer] += healAmount;
1222 previousHealAmounts.Add(healer, healAmount);
1231 RespondToAttack(attacker, attackResult);
1232 wasConscious =
false;
1240 RespondToAttack(attacker, attackResult);
1243 if (previousAttackResults.ContainsKey(attacker))
1249 var matchingAffliction = previousAttackResults[attacker].Afflictions.Find(a => a.Prefab == newAffliction.
Prefab && a.Source == newAffliction.
Source);
1250 if (matchingAffliction ==
null)
1252 previousAttackResults[attacker].Afflictions.Add(newAffliction);
1256 matchingAffliction.Strength += newAffliction.
Strength;
1260 previousAttackResults[attacker] =
new AttackResult(previousAttackResults[attacker].Afflictions, previousAttackResults[attacker].HitLimb);
1264 previousAttackResults.Add(attacker, attackResult);
1270 float healAmount = 0.0f;
1271 if (attacker !=
null)
1273 previousHealAmounts.TryGetValue(attacker, out healAmount);
1276 float realDamage = attackResult.
Damage - healAmount;
1278 float totalDamage = realDamage;
1281 foreach (Affliction affliction
in attackResult.
Afflictions)
1283 totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength;
1286 if (totalDamage <= 0.01f) {
return; }
1292 objectiveManager.CreateAutonomousObjectives();
1293 objectiveManager.SortObjectives();
1313 if (realDamage <= 0 && (attacker.
IsBot || sameTeam))
1323 bool isAttackerInfected =
false;
1324 bool isAttackerFightingEnemy =
false;
1325 float minorDamageThreshold = 5;
1326 float majorDamageThreshold = 20;
1329 minorDamageThreshold = 10;
1330 majorDamageThreshold = 40;
1332 bool eitherIsMentallyUnstable = IsMentallyUnstable || attacker.
AIController is { IsMentallyUnstable:
true };
1333 if (IsFriendly(attacker))
1341 float cumulativeDamage = realDamage +
Character.GetDamageDoneByAttacker(attacker);
1342 bool isAccidental = attacker.
IsBot && !eitherIsMentallyUnstable && attacker.
CombatAction ==
null;
1347 AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
1354 if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold)
1358 InformOtherNPCs(cumulativeDamage);
1363 var combatMode = DetermineCombatMode(Character, cumulativeDamage);
1364 if (attacker.
IsPlayer && !
Character.IsInstigator && !ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>())
1368 case AIObjectiveCombat.CombatMode.Defensive:
1369 case AIObjectiveCombat.CombatMode.Retreat:
1372 Character.Speak(TextManager.Get(
"dialogattackedbyfriendlysecurityresponse").Value,
null, 0.5f,
"attackedbyfriendlysecurityresponse".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
1376 Character.Speak(TextManager.Get(
"DialogAttackedByFriendly").Value,
null, 0.5f,
"attackedbyfriendly".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
1379 case AIObjectiveCombat.CombatMode.Offensive:
1380 case AIObjectiveCombat.CombatMode.Arrest:
1381 Character.Speak(TextManager.Get(
"dialogattackedbyfriendlysecurityarrest").Value,
null, 0.5f,
"attackedbyfriendlysecurityarrest".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
1383 case AIObjectiveCombat.CombatMode.None:
1384 if (
Character.IsSecurity && realDamage > 1)
1386 Character.Speak(TextManager.Get(
"dialogattackedbyfriendlysecurityresponse").Value,
null, 0.5f,
"attackedbyfriendlysecurityresponse".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
1392 AddCombatObjective(combatMode, attacker, delay: realDamage > 1 ? GetReactionTime() : 0);
1394 if (!isAttackerFightingEnemy)
1396 (GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult);
1409 AddCombatObjective(DetermineCombatMode(Character), attacker);
1413 void InformOtherNPCs(
float cumulativeDamage = 0)
1415 foreach (Character otherCharacter
in Character.CharacterList)
1417 if (otherCharacter == Character || otherCharacter.IsUnconscious || otherCharacter.Removed) {
continue; }
1418 if (otherCharacter.Submarine !=
Character.Submarine) {
continue; }
1419 if (otherCharacter.Submarine != attacker.
Submarine) {
continue; }
1420 if (otherCharacter.Info?.Job ==
null || otherCharacter.IsInstigator) {
continue; }
1421 if (otherCharacter.IsPlayer) {
continue; }
1422 if (otherCharacter.AIController is not HumanAIController otherHumanAI) {
continue; }
1423 if (!otherHumanAI.IsFriendly(Character)) {
continue; }
1424 if (attacker.
AIController is EnemyAIController enemyAI && otherHumanAI.IsFriendly(attacker))
1430 otherHumanAI.VisibleHulls.Contains(
Character.CurrentHull) ||
1431 otherHumanAI.VisibleHulls.Contains(attacker.
CurrentHull) ||
1432 otherCharacter.CanSeeTarget(attacker, seeThroughWindows:
true);
1437 if (
Character.IsDead ||
Character.IsUnconscious || otherCharacter.TeamID !=
Character.TeamID || !CheckReportRange(Character, otherCharacter, ReportRange))
1442 var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing);
1443 float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced);
1444 otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
1448 AIObjectiveCombat.CombatMode DetermineCombatMode(Character c,
float cumulativeDamage = 0,
bool isWitnessing =
false)
1450 if (c.AIController is not HumanAIController humanAI) {
return AIObjectiveCombat.CombatMode.None; }
1451 if (!IsFriendly(attacker))
1453 if (c.Submarine ==
null)
1456 return attacker.
Submarine ==
null ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat;
1458 if (!c.Submarine.GetConnectedSubs().Contains(attacker.
Submarine))
1462 humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.GetTarget() is
Controller ?
1463 AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat;
1467 return AIObjectiveCombat.CombatMode.Offensive;
1469 return humanAI.ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>() ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
1473 if (isAttackerInfected)
1475 cumulativeDamage = 100;
1479 if (GameMain.IsSingleplayer || c.TeamID != attacker.
TeamID)
1483 return Character == c && cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None;
1486 if (c.Submarine ==
null || !c.Submarine.GetConnectedSubs().Contains(attacker.
Submarine))
1489 return AIObjectiveCombat.CombatMode.None;
1492 if (
Character.CharacterList.Any(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull)))
1494 isAttackerFightingEnemy =
true;
1495 return AIObjectiveCombat.CombatMode.None;
1499 return AIObjectiveCombat.CombatMode.Offensive;
1501 if (isWitnessing && c.CombatAction !=
null && !c.IsSecurity)
1503 return c.CombatAction.WitnessReaction;
1505 if (!attacker.
IsInstigator && c.IsOnFriendlyTeam(attacker) && FindInstigator() is Character instigator)
1508 isAttackerFightingEnemy =
true;
1509 return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : instigator.CombatAction?.WitnessReaction ?? AIObjectiveCombat.CombatMode.Retreat;
1524 if (humanAI.ObjectiveManager.GetLastActiveObjective<AIObjectiveCombat>() is AIObjectiveCombat currentCombatObjective && currentCombatObjective.Enemy == attacker)
1527 cumulativeDamage *= 2;
1528 currentCombatObjective.AllowHoldFire =
false;
1529 c.IsCriminal =
true;
1534 cumulativeDamage = Math.Max(cumulativeDamage, minorDamageThreshold);
1536 if (cumulativeDamage > majorDamageThreshold)
1538 c.IsCriminal =
true;
1541 return AIObjectiveCombat.CombatMode.Offensive;
1545 return c ==
Character ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat;
1548 else if (cumulativeDamage > minorDamageThreshold)
1550 return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
1554 return AIObjectiveCombat.CombatMode.None;
1572 if (c.AIController is HumanAIController humanAi)
1574 return Character.CharacterList.FirstOrDefault(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && ch.IsInstigator && humanAi.VisibleHulls.Contains(ch.CurrentHull));
1591 if (combatObjective.Mode != mode || combatObjective.Enemy != target || (combatObjective.Enemy ==
null && target ==
null))
1594 ObjectiveManager.
Objectives.Remove(combatObjective);
1595 ObjectiveManager.
AddObjective(CreateCombatObjective());
1602 ObjectiveManager.
AddObjective(CreateCombatObjective(), delay);
1606 ObjectiveManager.
AddObjective(CreateCombatObjective());
1615 AbortCondition = abortCondition,
1616 AllowHoldFire = allowHoldFire,
1617 SpeakWarnings = speakWarnings
1619 if (onAbort !=
null)
1621 objective.Abandoned += onAbort;
1623 if (onCompleted !=
null)
1625 objective.Completed += onCompleted;
1633 objectiveManager.SetOrder(order, speak);
1635 HintManager.OnSetOrder(
Character, order);
1653 SelectedAiTarget = target;
1659 objectiveManager.SortObjectives();
1660 SortTimer = sortObjectiveInterval;
1661 float waitDuration = characterWaitOnSwitch;
1666 ObjectiveManager.
WaitTimer = waitDuration;
1669 public override bool Escape(
float deltaTime) => UpdateEscape(deltaTime, canAttackDoors:
false);
1671 private void CheckCrouching(
float deltaTime)
1673 crouchRaycastTimer -= deltaTime;
1674 if (crouchRaycastTimer > 0.0f) {
return; }
1676 crouchRaycastTimer = crouchRaycastInterval;
1687 float margin = 0.1f;
1692 float minCeilingDist = mainCollider.
Height / 2 + mainCollider.Radius + margin;
1694 shouldCrouch =
Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist,
null, Physics.CollisionWall, customPredicate: (fixture) => { return fixture.Body.UserData is not Submarine; }) !=
null;
1720 hull.
ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f))
1723 return needsAir || needsSuit;
1733 HasDivingSuit(character, conditionPercentage, requireOxygenTank) || HasDivingMask(character, conditionPercentage, requireOxygenTank);
1738 public static bool HasDivingSuit(
Character character,
float conditionPercentage = 0,
bool requireOxygenTank =
true,
bool requireSuitablePressureProtection =
true)
1739 => HasItem(character, Tags.HeavyDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped:
true,
1740 predicate: (
Item item) =>
1748 => HasItem(character, Tags.LightDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped:
true);
1750 private static List<Item> matchingItems =
new List<Item>();
1756 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)
1758 matchingItems.Clear();
1759 items = matchingItems;
1760 if (character?.
Inventory ==
null) {
return false; }
1761 matchingItems = character.
Inventory.
FindAllItems(i => (i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier)) &&
1762 i.ConditionPercentage >= conditionPercentage &&
1764 (predicate ==
null || predicate(i)), recursive, matchingItems);
1765 items = matchingItems;
1766 foreach (var item
in matchingItems)
1768 if (item ==
null) {
continue; }
1770 if (containedTag.IsEmpty || item.OwnInventory ==
null)
1775 var suitableSlot = item.GetComponent<
ItemContainer>().FindSuitableSubContainerIndex(containedTag);
1776 if (suitableSlot ==
null)
1779 return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage);
1783 return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage && it.ParentInventory.IsInSlot(it, suitableSlot.Value));
1791 const float MaxDamagePerSecond = 5.0f;
1792 const float MaxDamagePerFrame = MaxDamagePerSecond * (float)Timing.Step;
1794 const float WarningThreshold = 5.0f;
1795 const float ArrestThreshold = 20.0f;
1796 const float KillThreshold = 50.0f;
1798 if (character ==
null || damageAmount <= 0.0f) {
return; }
1803 bool someoneSpoke =
false;
1804 float maxAccumulatedDamage = 0.0f;
1807 if (otherCharacter == character || otherCharacter.
TeamID == character.TeamID || otherCharacter.
IsDead ||
1808 otherCharacter.
Info?.
Job ==
null ||
1810 Vector2.DistanceSquared(otherCharacter.
WorldPosition, character.WorldPosition) > 1000.0f * 1000.0f)
1814 if (!otherCharacter.
CanSeeTarget(character, seeThroughWindows:
true)) {
continue; }
1816 otherHumanAI.structureDamageAccumulator.TryAdd(character, 0.0f);
1817 float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character];
1818 otherHumanAI.structureDamageAccumulator[character] += MathHelper.Clamp(damageAmount, -MaxDamagePerFrame, MaxDamagePerFrame);
1819 float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage);
1820 maxAccumulatedDamage = Math.Max(accumulatedDamage, maxAccumulatedDamage);
1828 if (!character.IsCriminal)
1830 if (accumulatedDamage <= WarningThreshold) {
return; }
1832 if (accumulatedDamage > WarningThreshold && prevAccumulatedDamage <= WarningThreshold &&
1833 !someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f)
1836 if (accumulatedDamage < ArrestThreshold)
1838 if (otherHumanAI.ObjectiveManager.CurrentObjective is
AIObjectiveIdle idleObjective)
1840 idleObjective.FaceTargetAndWait(character, 5.0f);
1843 otherCharacter.
Speak(TextManager.Get(
"dialogdamagewallswarning").Value,
null, Rand.Range(0.5f, 1.0f),
"damageoutpostwalls".ToIdentifier(), 10.0f);
1844 someoneSpoke =
true;
1849 if (character.IsCriminal ||
1850 (accumulatedDamage > ArrestThreshold && prevAccumulatedDamage <= ArrestThreshold) ||
1851 (accumulatedDamage > KillThreshold && prevAccumulatedDamage <= KillThreshold))
1856 character.IsCriminal =
true;
1858 if (!TriggerSecurity(otherHumanAI, combatMode))
1861 foreach (
Character security
in Character.
CharacterList.Where(c => c.TeamID == otherCharacter.
TeamID).OrderBy(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition)))
1875 if (humanAI ==
null) {
return false; }
1884 if (anyCharacter.AIController is HumanAIController anyAI)
1886 anyAI.structureDamageAccumulator?.Remove(character);
1896 if (item ==
null || thief ==
null || item.GetComponent<
LevelResource>() !=
null) {
return; }
1897 bool someoneSpoke =
false;
1915 if (!otherCharacter.
CanSeeTarget(thief, seeThroughWindows:
true)) {
continue; }
1921 if (item.
HasTag(Tags.FireExtinguisher) && connectedHulls.Any(h => h.FireSources.Any())) {
continue; }
1932 ApplyStealingReputationLoss(item);
1934 HintManager.OnStoleItem(thief, item);
1939 otherCharacter.
Speak(TextManager.Get(
"dialogstealwarning").Value,
null, Rand.Range(0.5f, 1.0f),
"thief".ToIdentifier(), 10.0f);
1940 someoneSpoke =
true;
1943 if (!TriggerSecurity(otherHumanAI))
1959 ItemTaken(foundItem, thief);
1964 if (humanAI ==
null) {
return false; }
1969 findThieves.InspectEveryone();
1973 abortCondition: obj => !isCriminal && thief.
Inventory.
FindItem(it => it.Illegitimate, recursive:
true) ==
null,
1976 if (!item.Removed && !humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveGetItem>())
1978 humanAI.ObjectiveManager.AddObjective(new AIObjectiveGetItem(humanAI.Character, item, humanAI.ObjectiveManager, equip: false)
1984 allowHoldFire: !isCriminal,
1985 speakWarnings: !isCriminal);
1995 var reputationLoss = MathHelper.Clamp(
2003 private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f);
2012 DoForEachBot(character, (humanAi) => humanAi.RefreshHullSafety(hull));
2017 private void RefreshHullSafety(
Hull hull)
2019 var visibleHulls = dirtyHullSafetyCalculations.Contains(hull) ? hull.
GetConnectedHulls(includingThis:
true, searchDepth: 1) : VisibleHulls;
2020 float hullSafety = GetHullSafety(hull,
Character, visibleHulls);
2021 if (hullSafety > HULL_SAFETY_THRESHOLD)
2023 UnsafeHulls.Remove(hull);
2027 UnsafeHulls.Add(hull);
2033 switch (order.
Identifier.Value.ToLowerInvariant())
2036 AddTargets<AIObjectiveExtinguishFires, Hull>(character, hull);
2038 case "reportbreach":
2043 AddTargets<AIObjectiveFixLeaks, Gap>(character, gap);
2047 case "reportbrokendevices":
2050 if (item.CurrentHull != hull) {
continue; }
2053 if (item.Repairables.All(r => r.IsBelowRepairThreshold)) {
continue; }
2054 AddTargets<AIObjectiveRepairItems, Item>(character, item);
2058 case "reportintruders":
2061 if (enemy.CurrentHull != hull) {
continue; }
2064 AddTargets<AIObjectiveFightIntruders, Character>(character, enemy);
2068 case "requestfirstaid":
2071 if (c.CurrentHull != hull) {
continue; }
2074 AddTargets<AIObjectiveRescueAll, Character>(character, c);
2080 DebugConsole.ThrowError(order.
Identifier +
" not implemented!");
2088 bool targetAdded =
false;
2089 DoForEachBot(caller, humanAI =>
2091 if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; }
2092 var objective = humanAI.ObjectiveManager.GetObjective<T1>();
2093 if (objective !=
null)
2095 if (!targetAdded && objective.AddTarget(target))
2100 }, range: (caller.AIController as HumanAIController)?.ReportRange ??
float.PositiveInfinity);
2106 DoForEachBot(caller, humanAI =>
2107 humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target));
2110 private void StoreHullSafety(
Hull hull, HullSafety safety)
2112 if (knownHulls.ContainsKey(hull))
2115 knownHulls[hull] = safety;
2120 knownHulls.Add(hull, safety);
2124 private float CalculateHullSafety(Hull hull, Character character, IEnumerable<Hull> visibleHulls =
null)
2126 bool isCurrentHull = character ==
Character && character.CurrentHull == hull;
2129 float hullSafety = character.IsProtectedFromPressure ? 0 : 100;
2132 CurrentHullSafety = hullSafety;
2136 if (isCurrentHull && visibleHulls ==
null)
2139 visibleHulls = VisibleHulls;
2141 bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
2142 bool ignoreOxygen = HasDivingGear(character);
2143 bool ignoreEnemies = ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>();
2144 float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater:
false, ignoreOxygen, ignoreFire, ignoreEnemies);
2147 CurrentHullSafety = safety;
2152 private static float CalculateHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character,
bool ignoreWater =
false,
bool ignoreOxygen =
false,
bool ignoreFire =
false,
bool ignoreEnemies =
false)
2154 bool isProtectedFromPressure = character.IsProtectedFromPressure;
2155 if (hull ==
null) {
return isProtectedFromPressure ? 100 : 0; }
2156 if (hull.LethalPressure > 0 && !isProtectedFromPressure) {
return 0; }
2159 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));
2160 float waterFactor = 1;
2163 if (visibleHulls !=
null)
2166 float relativeWaterVolume = visibleHulls.Sum(s => s.WaterVolume) / visibleHulls.Sum(s => s.Volume);
2167 waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume);
2171 float relativeWaterVolume = hull.WaterVolume / hull.Volume;
2172 waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume);
2175 if (!character.NeedsOxygen || character.CharacterHealth.OxygenLowResistance >= 1)
2179 if (isProtectedFromPressure)
2183 float fireFactor = 1;
2186 static float CalculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
2188 float fire = visibleHulls?.Sum(CalculateFire) ?? CalculateFire(hull);
2189 fireFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1));
2191 float enemyFactor = 1;
2195 foreach (Character c
in Character.CharacterList)
2197 if (visibleHulls ==
null)
2199 if (c.CurrentHull != hull) {
continue; }
2203 if (!visibleHulls.Contains(c.CurrentHull)) {
continue; }
2205 if (IsActive(c) && !IsFriendly(character, c) && !c.IsHandcuffed)
2211 enemyFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1));
2213 float dangerousItemsFactor = 1f;
2214 foreach (
Item item
in Item.DangerousItems)
2216 if (item.CurrentHull == hull)
2218 dangerousItemsFactor = 0;
2222 float safety = oxygenFactor * waterFactor * fireFactor * enemyFactor * dangerousItemsFactor;
2223 return MathHelper.Clamp(safety * 100, 0, 100);
2230 return CalculateHullSafety(hull, character, visibleHulls);
2232 if (!knownHulls.TryGetValue(hull, out HullSafety hullSafety))
2234 hullSafety =
new HullSafety(CalculateHullSafety(hull, character, visibleHulls));
2235 StoreHullSafety(hull, hullSafety);
2237 else if (hullSafety.IsStale)
2239 hullSafety.Reset(CalculateHullSafety(hull, character, visibleHulls));
2241 return hullSafety.safety;
2244 public static float GetHullSafety(
Hull hull, IEnumerable<Hull> visibleHulls,
Character character,
bool ignoreWater =
false,
bool ignoreOxygen =
false,
bool ignoreFire =
false,
bool ignoreEnemies =
false)
2248 return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
2250 HullSafety hullSafety;
2253 if (!controller.knownHulls.TryGetValue(hull, out hullSafety))
2255 hullSafety =
new HullSafety(CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies));
2256 controller.StoreHullSafety(hull, hullSafety);
2258 else if (hullSafety.IsStale)
2260 hullSafety.Reset(CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies));
2266 DebugConsole.ThrowError(
"Cannot store the hull safety, because was unable to cast the AIController as HumanAIController. This should never happen!");
2268 return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
2270 return hullSafety.safety;
2320 return !npcAI.IsInHostileFaction();
2333 Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty;
2335 if (npcFaction.IsEmpty)
2338 npcFaction = currentLocationFaction;
2340 if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction)
2342 if (campaign.CurrentLocation is { IsFactionHostile: true })
2354 if (character ==
null) {
return false; }
2357 if (!IsBotInTheCrew(character, c)) {
continue; }
2368 if (character ==
null) {
return false; }
2371 if (!IsBotInTheCrew(character, c)) {
continue; }
2382 if (character ==
null) {
return 0; }
2386 if (!IsBotInTheCrew(character, other)) {
continue; }
2402 if (!IsActive(c)) {
continue; }
2404 if (onlyActive && c.IsIncapacitated) {
continue; }
2405 if (onlyConnectedSubs)
2409 if (c.Submarine !=
null)
2427 private static void DoForEachBot(
Character character, Action<HumanAIController> action,
float range =
float.PositiveInfinity)
2429 if (character ==
null) {
return; }
2430 foreach (var c
in Character.CharacterList)
2432 if (IsBotInTheCrew(character, c) && CheckReportRange(character, c, range))
2434 action(c.AIController as HumanAIController);
2439 private static bool CheckReportRange(Character character, Character target,
float range)
2441 if (
float.IsPositiveInfinity(range)) {
return true; }
2442 if (character.CurrentHull ==
null || target.CurrentHull ==
null)
2444 return Vector2.DistanceSquared(character.WorldPosition, target.WorldPosition) <= range * range;
2448 return character.CurrentHull.GetApproximateDistance(character.Position, target.Position, target.CurrentHull, range, distanceMultiplierPerClosedDoor: 2) <= range;
2452 private static bool IsBotInTheCrew(Character
self, Character other) => IsActive(other) && other.TeamID ==
self.TeamID && !other.IsIncapacitated && other.IsBot && other.AIController is HumanAIController;
2456 operatingCharacter =
null;
2457 if (target?.
Item ==
null) {
return false; }
2458 float highestPriority = -1.0f;
2459 float highestPriorityModifier = -1.0f;
2462 if (c ==
null) {
continue; }
2464 if (c.
TeamID != team) {
continue; }
2468 operatingCharacter = c;
2473 foreach (var objective
in objectiveManager.Objectives)
2476 if (operateObjective.Component?.Item != target.Item) {
continue; }
2477 if (operateObjective.Priority < highestPriority) {
continue; }
2478 if (operateObjective.PriorityModifier < highestPriorityModifier) {
continue; }
2479 operatingCharacter = c;
2480 highestPriority = operateObjective.Priority;
2481 highestPriorityModifier = operateObjective.PriorityModifier;
2485 return operatingCharacter !=
null;
2493 if (target?.
Item ==
null) {
return false; }
2494 bool isOrder = IsOrderedToOperateTarget(
this);
2497 if (!IsActive(c)) {
continue; }
2516 bool isTargetOrdered = IsOrderedToOperateTarget(otherAI);
2517 if (!isOrder && isTargetOrdered)
2525 if (isOrder && !isTargetOrdered)
2532 if (!IsOperatingTarget(otherAI))
2546 else if (target.DegreeOfSuccess(
Character) <= target.DegreeOfSuccess(c))
2555 return other !=
null;
2563 if (
Character ==
null) {
return false; }
2564 if (target ==
null) {
return false; }
2568 if (!IsActive(c)) {
continue; }
2574 if (target.Repairables.Any(r => r.CurrentFixer == c))
2583 if (repairItemsObjective ==
null) {
continue; }
2589 bool isTargetOrdered = IsOrderedToRepairThis(operatingAI);
2590 if (!isOrder && isTargetOrdered)
2597 if (isOrder && !isTargetOrdered)
2604 if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective)
2609 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)
bool TryPutItemInAnySlot(Item item)
void Speak(string message, ChatMessageType? messageType=null, float delay=0.0f, Identifier identifier=default, float minDurationBetweenSimilar=0.0f)
CharacterHealth CharacterHealth
Character?? SelectedCharacter
virtual AIController AIController
CampaignMode.InteractionType CampaignInteractionType
CharacterTeamType?? OriginalTeamID
bool HasEquippedItem(Item item, InvSlotType? slotType=null, Func< InvSlotType, bool > predicate=null)
override Vector2? SimPosition
bool IsCriminal
Do the outpost security officers treat the character as a criminal? Triggers when the character has e...
float GetSkillLevel(Identifier skillIdentifier)
Get the character's current skill level, taking into account any temporary boosts from wearables and ...
static readonly List< Character > CharacterList
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)
bool TryPutItemInBag(Item item)
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
Item? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
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)
void SendOrderChatMessage(OrderChatMessage message)
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, bool excludeLimbsWithSecondaryType=false, bool useSecondaryType=false)
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 Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
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)
@ Character
Characters only
readonly List< Affliction > Afflictions