3 using Microsoft.Xna.Framework;
5 using System.Collections.Generic;
7 using FarseerPhysics.Dynamics;
9 using System.Collections.Immutable;
24 private float checkWeaponsTimer;
25 private const float CheckWeaponsInterval = 1;
26 private float ignoreWeaponTimer;
27 private const float IgnoredWeaponsClearTime = 10;
29 private const float GoodWeaponPriority = 30;
31 private float holdFireTimer;
32 private bool hasAimed;
33 private bool isLethalWeapon;
35 private bool allowCooldown;
43 get {
return _weapon; }
47 _weaponComponent =
null;
55 if (Weapon ==
null) {
return null; }
56 return _weaponComponent ?? GetWeaponComponent(Weapon);
64 private readonly HashSet<ItemComponent> weapons =
new HashSet<ItemComponent>();
65 private readonly HashSet<Item> ignoredWeapons =
new HashSet<Item>();
72 private Hull retreatTarget;
73 private float coolDownTimer;
74 private float pathBackTimer;
75 private const float DefaultCoolDown = 10.0f;
76 private const float PathBackCheckTime = 1.0f;
77 private IEnumerable<Body> myBodies;
78 private float aimTimer;
79 private float reloadTimer;
80 private float spreadTimer;
82 private bool canSeeTarget;
83 private float visibilityCheckTimer;
84 private const float VisibilityCheckInterval = 0.2f;
86 private float sqrDistance;
87 private const float MaxDistance = 2000;
88 private const float DistanceCheckInterval = 0.2f;
89 private float distanceTimer;
91 private const float CloseDistanceThreshold = 300;
92 private const float FloorHeightApproximate = 100;
96 private bool firstWarningTriggered;
97 private bool lastWarningTriggered;
101 private const float ArrestTargetDistance = 100;
102 private bool arrestingRegistered;
147 private bool IsEnemyClose(
float margin)
149 if (
Enemy ==
null) {
return false; }
154 if (Math.Abs(toEnemy.Y) > FloorHeightApproximate)
162 return Math.Abs(toEnemy.X) < margin;
175 DebugConsole.ThrowError(
"Combat mode == None");
180 coolDownTimer = coolDown;
182 if (findSafety !=
null)
193 spreadTimer = Rand.Range(-10f, 10f);
194 SetAimTimer(Rand.Range(1f, 1.5f) / AimSpeed);
200 if (TargetEliminated)
208 const float priorityScale = maxPriority - minPriority;
216 float distanceFactor = MathUtils.InverseLerp(3000, 0, xDist + yDist * 5);
218 float additionalPriority = MathHelper.Lerp(0, priorityScale, Math.Clamp(devotion + distanceFactor, 0, 1));
230 public override void Update(
float deltaTime)
232 base.Update(deltaTime);
233 ignoreWeaponTimer -= deltaTime;
234 checkWeaponsTimer -= deltaTime;
237 reloadTimer -= deltaTime;
239 if (ignoreWeaponTimer < 0)
241 ignoredWeapons.Clear();
242 ignoreWeaponTimer = IgnoredWeaponsClearTime;
245 if (findSafety !=
null && isFightingIntruders)
247 findSafety.Priority = 0;
251 distanceTimer -= deltaTime;
252 if (distanceTimer < 0)
254 distanceTimer = DistanceCheckInterval;
269 allowCooldown =
true;
288 allowCooldown =
true;
292 allowCooldown =
false;
293 coolDownTimer = DefaultCoolDown;
295 else if (pathBackTimer <= 0)
298 pathBackTimer = PathBackCheckTime;
303 if (path.Unreachable)
305 allowCooldown =
false;
306 coolDownTimer = DefaultCoolDown;
310 if (IsOffensiveOrArrest)
317 return TargetEliminated || (AllowCoolDown && coolDownTimer <= 0);
320 protected override void Act(
float deltaTime)
329 coolDownTimer -= deltaTime;
330 if (pathBackTimer > 0)
332 pathBackTimer -= deltaTime;
335 if (seekAmmunitionObjective ==
null && seekWeaponObjective ==
null)
339 OperateWeapon(deltaTime);
345 else if (seekAmmunitionObjective ==
null && seekWeaponObjective ==
null)
352 private void Move(
float deltaTime)
371 if (gotoObjective !=
null)
373 gotoObjective.ForceAct(deltaTime);
396 throw new NotImplementedException();
400 private bool TryArm()
405 RemoveSubObjective(ref seekAmmunitionObjective);
410 && IsOffensiveOrArrest
412 if (checkWeaponsTimer < 0)
414 checkWeaponsTimer = CheckWeaponsInterval;
416 HashSet<ItemComponent> allWeapons = FindWeaponsFromInventory();
417 while (allWeapons.Any())
419 Weapon = GetWeapon(allWeapons, out _weaponComponent);
428 allWeapons.Remove(WeaponComponent);
437 bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective ==
null && !IsEnemyClose(CloseDistanceThreshold);
438 if (Reload(seekAmmo: seekAmmo))
443 else if (seekAmmunitionObjective !=
null)
451 allWeapons.Remove(WeaponComponent);
458 Weapon = FindWeapon(out _weaponComponent);
461 if (!CheckWeapon(seekAmmo:
true))
463 if (seekAmmunitionObjective !=
null)
475 if (!isAllowedToSeekWeapons)
477 if (WeaponComponent ==
null)
483 else if (seekAmmunitionObjective ==
null && (WeaponComponent ==
null || (WeaponComponent.
CombatPriority < GoodWeaponPriority && !IsEnemyClose(CloseDistanceThreshold))))
486 RemoveSubObjective(ref retreatObjective);
487 RemoveSubObjective(ref followTargetObjective);
488 TryAddSubObjective(ref seekWeaponObjective,
489 constructor: () =>
new AIObjectiveGetItem(
character,
"weapon".ToIdentifier(),
objectiveManager, equip:
true, checkInventory:
false)
493 EvaluateCombatPriority =
false,
494 GetItemPriority = i =>
496 if (Weapon !=
null && (i == Weapon || i.Prefab.Identifier == Weapon.
Prefab.
Identifier)) {
return 0; }
497 if (i.IsOwnedBy(
character)) {
return 0; }
501 priority = GetWeaponPriority(ic, prioritizeMelee:
false, canSeekAmmo:
true, out _) / 100;
503 if (priority <= 0) {
return 0; }
507 if (range is > 0 and <
float.PositiveInfinity)
510 float yDiff = Math.Abs(toItem.Y) > FloorHeightApproximate ? toItem.Y * 2 : 0;
511 Vector2 adjustedDiff =
new Vector2(toItem.X, yDiff);
512 if (adjustedDiff.LengthSquared() > MathUtils.Pow2(range))
519 if (Math.Sign(toItem.X) == Math.Sign(toEnemy.X))
526 if (Math.Abs(toItem.Y) > FloorHeightApproximate && Math.Abs(toEnemy.Y) > FloorHeightApproximate)
528 if (Math.Sign(toItem.Y) == Math.Sign(toEnemy.Y))
538 onCompleted: () => RemoveSubObjective(ref seekWeaponObjective),
541 RemoveSubObjective(ref seekWeaponObjective);
547 else if (!
objectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>())
555 else if (seekAmmunitionObjective ==
null && seekWeaponObjective ==
null)
557 if (!CheckWeapon(seekAmmo:
false))
562 return Weapon !=
null;
564 bool CheckWeapon(
bool seekAmmo)
574 if (!Reload(seekAmmo))
583 private void OperateWeapon(
float deltaTime)
598 throw new NotImplementedException();
602 private Item FindWeapon(out
ItemComponent weaponComponent) => GetWeapon(FindWeaponsFromInventory(), out weaponComponent);
604 private static ItemComponent GetWeaponComponent(Item item) =>
613 private float GetWeaponPriority(
ItemComponent weapon,
bool prioritizeMelee,
bool canSeekAmmo, out
float lethalDmg)
617 if (priority <= 0) {
return 0; }
620 switch (repairTool.UsableIn)
670 Attack attack = GetAttackDefinition(weapon);
673 lethalDmg = attack.GetTotalDamage();
674 float max = lethalDmg + 1;
681 float stunDmg = ApproximateStunDamage(weapon, attack);
682 float diff = stunDmg - lethalDmg;
683 priority = Math.Clamp(priority - Math.Max(diff * 2, 0), min: 1, max);
696 Attack attack = GetAttackDefinition(weapon);
699 lethalDmg = attack.GetTotalDamage();
700 float stunDmg = ApproximateStunDamage(weapon, attack);
701 float diff = stunDmg - lethalDmg;
712 Attack attack = GetAttackDefinition(weapon);
713 priority = attack?.GetTotalDamage() ?? priority / 2;
716 float startPriority = priority;
718 if (skillRequirementHints !=
null)
722 foreach (SkillRequirementHint hint
in skillRequirementHints)
725 float targetLevel = hint.Level;
726 priority = ReducePriority(priority, skillLevel, targetLevel);
738 priority = ReducePriority(priority, skillLevel, targetLevel);
742 priority = Math.Max(priority, startPriority / 2);
745 float ReducePriority(
float prio,
float skillLevel,
float targetLevel)
747 float diff = targetLevel - skillLevel;
756 private float ApproximateStunDamage(
ItemComponent weapon, Attack attack)
761 var statusEffects = attack.StatusEffects.Where(se => !se.HasConditions && se.type ==
ActionType.OnUse && se.HasRequiredItems(
character));
764 statusEffects = statusEffects.Concat(hitEffects);
766 float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == AfflictionPrefab.StunType ? a.Strength : 0);
767 float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se =>
769 float stunAmount = 0;
770 var stunAffliction = se.Afflictions.Find(a => a.Identifier == AfflictionPrefab.StunType);
771 if (stunAffliction !=
null)
773 stunAmount = stunAffliction.Strength;
777 return attack.Stun + afflictionsStun + effectsStun;
780 private static bool CanMeleeStunnerStun(
ItemComponent weapon)
785 Identifier mobileBatteryTag = Tags.MobileBattery;
790 return containers.None() || containers.Any(container =>
791 (container as
ItemContainer)?.Inventory.AllItems.Any(i => i !=
null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ??
false);
794 private Item GetWeapon(IEnumerable<ItemComponent> weaponList, out
ItemComponent weaponComponent)
796 weaponComponent =
null;
797 float bestPriority = 0;
798 float lethalDmg = -1;
799 bool prioritizeMelee = IsEnemyClose(50) || EnemyAIController.IsLatchedTo(
Enemy,
character);
800 bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(CloseDistanceThreshold);
801 foreach (var weapon
in weaponList)
803 float priority = GetWeaponPriority(weapon, prioritizeMelee, canSeekAmmo: !isCloseToEnemy, out lethalDmg);
804 if (priority > bestPriority)
806 weaponComponent = weapon;
807 bestPriority = priority;
810 if (weaponComponent ==
null) {
return null; }
811 if (bestPriority < 1) {
return null; }
814 if (weaponComponent.Item.HasTag(Tags.StunnerItem))
816 isLethalWeapon =
false;
824 isLethalWeapon = lethalDmg > 1;
828 if (!hasAimed && holdFireTimer <= 0)
838 FriendlyGuardSpeak(
"dialogarrest.lastwarning".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 0f);
839 lastWarningTriggered =
true;
843 FriendlyGuardSpeak(
"dialogarrest.firstwarning".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 0f);
844 firstWarningTriggered =
true;
850 return weaponComponent.Item;
856 Attack attack = GetAttackDefinition(weapon);
866 Attack attack = weapon
switch
875 private HashSet<ItemComponent> FindWeaponsFromInventory()
880 if (ignoredWeapons.Contains(item)) {
continue; }
881 GetWeapons(item, weapons);
882 if (item.OwnInventory !=
null)
884 item.OwnInventory.AllItems.ForEach(i => GetWeapons(i, weapons));
890 private static void GetWeapons(Item item, ICollection<ItemComponent> weaponList)
892 if (item ==
null) {
return; }
893 foreach (var component
in item.Components)
895 if (component.CombatPriority > 0)
897 weaponList.Add(component);
902 private void Unequip()
931 var slots = Weapon.
AllowedSlots.Where(CharacterInventory.IsHandSlotType);
934 SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
935 SetReloadTime(WeaponComponent);
948 private float findHullTimer;
949 private const float findHullInterval = 1.0f;
951 private void Retreat(
float deltaTime)
956 PlayerCrewSpeak(
"dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDurationBetweenSimilar: 20);
958 RemoveFollowTarget();
959 RemoveSubObjective(ref seekAmmunitionObjective);
960 if (retreatTarget !=
null)
968 retreatTarget =
null;
972 if (retreatObjective !=
null && retreatObjective.
Target != retreatTarget)
974 RemoveSubObjective(ref retreatObjective);
985 if (retreatTarget ==
null || retreatObjective is {
CanBeCompleted:
false })
987 if (findHullTimer > 0)
989 findHullTimer -= deltaTime;
994 if (hullSearchStatus != HullSearchStatus.Finished)
996 findSafety.UpdateSimpleEscape(deltaTime);
999 retreatTarget = potentialSafeHull;
1000 findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
1007 UsePathingOutside =
false,
1008 SpeakIfFails =
false
1016 RemoveSubObjective(ref retreatObjective);
1024 onCompleted: () => RemoveSubObjective(ref retreatObjective));
1028 private void Engage(
float deltaTime)
1030 if (WeaponComponent ==
null)
1032 RemoveFollowTarget();
1042 retreatTarget =
null;
1043 RemoveSubObjective(ref retreatObjective);
1044 RemoveSubObjective(ref seekAmmunitionObjective);
1045 RemoveSubObjective(ref seekWeaponObjective);
1048 if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range))
1068 RemoveFollowTarget();
1072 if (followTargetObjective !=
null && followTargetObjective.
Target !=
Enemy)
1074 RemoveFollowTarget();
1076 TryAddSubObjective(ref followTargetObjective,
1079 UsePathingOutside =
false,
1080 IgnoreIfTargetDead =
true,
1082 AlwaysUseEuclideanDistance =
false,
1083 SpeakIfFails =
false
1091 RemoveSubObjective(ref followTargetObjective);
1099 if (followTargetObjective ==
null) {
return; }
1106 ItemPrefab prefab = ItemPrefab.Find(
null,
"handcuffs".ToIdentifier());
1111 i.SpawnedInCurrentOutpost =
true;
1112 i.AllowStealing =
false;
1116 arrestingRegistered =
true;
1117 followTargetObjective.Completed += OnArrestTargetReached;
1118 followTargetObjective.CloseEnough = ArrestTargetDistance;
1120 if (!arrestingRegistered)
1122 followTargetObjective.CloseEnough =
1123 WeaponComponent
switch
1133 private void RemoveFollowTarget()
1135 if (followTargetObjective !=
null)
1137 if (arrestingRegistered)
1139 followTargetObjective.Completed -= OnArrestTargetReached;
1141 RemoveSubObjective(ref followTargetObjective);
1143 arrestingRegistered =
false;
1146 private void OnArrestTargetReached()
1148 if (!
Enemy.IsKnockedDown)
1150 RemoveFollowTarget();
1156 foreach (var item
in Enemy.Inventory.AllItemsMod)
1159 if (item.HasTag(Tags.HandLockerItem) &&
Enemy.HasEquippedItem(item)) {
continue; }
1160 if (item.Illegitimate || item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || GetWeaponComponent(item) is { CombatPriority: > 0 })
1162 item.Drop(character);
1163 character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
1169 if (!HumanAIController.HasItem(Enemy, Tags.HandLockerItem, out IEnumerable<Item> matchingItems))
1171 HumanAIController.HasItem(character, Tags.HandLockerItem, out matchingItems);
1174 if (matchingItems.Any() &&
1175 !
Enemy.IsUnconscious &&
Enemy.IsKnockedDown && character.CanInteractWith(Enemy) && !
Enemy.LockHands)
1177 var handCuffs = matchingItems.First();
1178 if (!HumanAIController.TakeItem(handCuffs,
Enemy.Inventory, equip:
true, wear:
true))
1181 DebugConsole.NewMessage($
"{character.Name}: Failed to handcuff the target.", Color.Red);
1183 if (objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
1189 character.Speak(TextManager.Get(
"DialogTargetArrested").Value,
null, 3.0f,
"targetarrested".ToIdentifier(), 30.0f);
1191 if (!objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
1200 private void SeekAmmunition(ImmutableHashSet<Identifier> ammunitionIdentifiers)
1202 retreatTarget =
null;
1203 RemoveSubObjective(ref retreatObjective);
1204 RemoveSubObjective(ref seekWeaponObjective);
1205 RemoveFollowTarget();
1207 TryAddSubObjective(ref seekAmmunitionObjective,
1208 constructor: () =>
new AIObjectiveContainItem(character, ammunitionIdentifiers, itemContainer, objectiveManager)
1210 ItemCount = itemContainer.MainContainerCapacity * itemContainer.MaxStackSize,
1211 checkInventory =
false,
1212 MoveWholeStack =
true
1214 onCompleted: () => RemoveSubObjective(ref seekAmmunitionObjective),
1217 SteeringManager.Reset();
1218 RemoveSubObjective(ref seekAmmunitionObjective);
1219 ignoredWeapons.Add(Weapon);
1228 private bool Reload(
bool seekAmmo)
1230 if (WeaponComponent ==
null) {
return false; }
1231 if (
Weapon.OwnInventory ==
null) {
return true; }
1233 HumanAIController.UnequipEmptyItems(Weapon);
1234 ImmutableHashSet<Identifier> ammunitionIdentifiers =
null;
1235 if (WeaponComponent.RequiredItems.ContainsKey(RelatedItem.RelationType.Contained))
1237 foreach (RelatedItem requiredItem
in WeaponComponent.RequiredItems[RelatedItem.RelationType.Contained])
1239 if (
Weapon.OwnInventory.AllItems.Any(it => it.Condition > 0 && requiredItem.MatchesItem(it))) {
continue; }
1240 ammunitionIdentifiers = requiredItem.Identifiers;
1244 else if (WeaponComponent is
MeleeWeapon meleeWeapon)
1246 ammunitionIdentifiers = meleeWeapon.PreferredContainedItems;
1249 if (ammunitionIdentifiers !=
null)
1252 static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is
Item ownerItem && ownerItem.HasTag(Tags.MobileRadio);
1253 Item ammunition = character.Inventory.FindItem(i =>
1254 i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i) && i.IsInteractable(character), recursive:
true);
1255 if (ammunition !=
null)
1258 if (container.Inventory.TryPutItem(ammunition, user: character))
1261 SetReloadTime(WeaponComponent);
1263 else if (ammunition.ParentInventory == character.Inventory)
1265 ammunition.Drop(character);
1269 if (!WeaponComponent.IsEmpty(character))
1273 else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers !=
null)
1277 if (!
Weapon.OwnInventory.Container.DrawInventory) {
return false; }
1278 SeekAmmunition(ammunitionIdentifiers);
1283 private void Attack(
float deltaTime)
1285 character.CursorPosition =
Enemy.WorldPosition;
1286 if (AimAccuracy < 1)
1288 spreadTimer += deltaTime * Rand.Range(0.01f, 1f);
1289 float shake = Rand.Range(0.95f, 1.05f);
1290 float offsetAmount = (1 - AimAccuracy) * Rand.Range(300f, 500f);
1291 float distanceFactor = MathUtils.InverseLerp(0, 1000 * 1000, sqrDistance);
1292 float offset = (float)Math.Sin(spreadTimer * shake) * offsetAmount * distanceFactor;
1293 character.CursorPosition +=
new Vector2(0, offset);
1295 if (character.Submarine !=
null)
1297 character.CursorPosition -= character.Submarine.Position;
1299 visibilityCheckTimer -= deltaTime;
1300 if (visibilityCheckTimer <= 0.0f)
1302 canSeeTarget = character.CanSeeTarget(Enemy);
1303 visibilityCheckTimer = VisibilityCheckInterval;
1307 SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
1310 if (
Weapon.RequireAimToUse)
1312 character.SetInput(
InputType.Aim, hit:
false, held:
true);
1315 if (AllowHoldFire && holdFireTimer > 0)
1317 holdFireTimer -= deltaTime;
1322 aimTimer -= deltaTime;
1325 if (reloadTimer > 0) {
return; }
1326 if (holdFireCondition !=
null && holdFireCondition()) {
return; }
1327 sqrDistance = Vector2.DistanceSquared(character.WorldPosition,
Enemy.WorldPosition);
1328 distanceTimer = DistanceCheckInterval;
1331 bool closeEnough =
true;
1332 float sqrRange = meleeWeapon.Range * meleeWeapon.Range;
1333 if (character.AnimController.InWater)
1335 if (sqrDistance > sqrRange)
1337 closeEnough =
false;
1343 float xDiff = Math.Abs(
Enemy.WorldPosition.X - character.WorldPosition.X);
1344 if (xDiff > meleeWeapon.Range)
1346 closeEnough =
false;
1348 float yDiff = Math.Abs(
Enemy.WorldPosition.Y - character.WorldPosition.Y);
1349 if (yDiff > Math.Max(meleeWeapon.Range, 100))
1351 closeEnough =
false;
1353 if (closeEnough &&
Enemy.WorldPosition.Y < character.WorldPosition.Y && yDiff > 25)
1356 HumanAIController.AnimController.Crouching =
true;
1361 UseWeapon(deltaTime);
1362 character.AIController.SteeringManager.Reset();
1364 else if (!character.IsFacing(
Enemy.WorldPosition))
1367 SetAimTimer(Rand.Range(1f, 1.5f) / AimSpeed);
1372 if (WeaponComponent is
RepairTool repairTool)
1374 if (sqrDistance > repairTool.Range * repairTool.Range) {
return; }
1376 float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
1377 if (VectorExtensions.Angle(VectorExtensions.Forward(
Weapon.body.TransformedRotation),
Enemy.WorldPosition -
Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor)
1379 myBodies ??= character.AnimController.Limbs.Select(l => l.body.FarseerBody);
1381 var pickedBodies =
Submarine.PickBodies(
Weapon.SimPosition,
Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter);
1382 foreach (var body
in pickedBodies)
1387 Limb limb => limb.character,
1390 if (target !=
null && target != Enemy && HumanAIController.IsFriendly(target))
1395 UseWeapon(deltaTime);
1400 private void UseWeapon(
float deltaTime)
1403 if (Mode == CombatMode.Arrest && isLethalWeapon && character.IsOnPlayerTeam &&
Enemy.IsOnPlayerTeam) {
return; }
1404 character.SetInput(
InputType.Shoot, hit:
false, held:
true);
1405 Weapon.Use(deltaTime, user: character);
1406 SetReloadTime(WeaponComponent);
1411 float reloadTime = 0;
1412 switch (weaponComponent)
1416 if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger)
1418 reloadTime = rangedWeapon.
Reload;
1424 if (character.AnimController is HumanoidAnimController { Crouching: false })
1436 float reloadTime = GetReloadTime(weaponComponent);
1437 reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed);
1440 private void ClearInputs()
1447 private bool ShouldUnequipWeapon =>
1449 character.Submarine !=
null &&
1450 character.Submarine.TeamID == character.TeamID &&
1451 Character.CharacterList.None(c => c.Submarine == character.Submarine && HumanAIController.IsActive(c) && !HumanAIController.IsFriendly(character, c) && HumanAIController.VisibleHulls.Contains(c.CurrentHull));
1461 character.Speak(TextManager.Get(
"DialogTargetDown").Value,
null, 3.0f,
"targetdown".ToIdentifier(), 30.0f);
1466 bot.ObjectiveManager.CurrentObjective is
AIObjectiveGoTo { SourceObjective: AIObjectiveCombat combatObjective } && combatObjective.Enemy ==
Enemy))
1470 RemoveFollowTarget();
1471 var approachArrestTarget =
new AIObjectiveGoTo(
Enemy, character, objectiveManager, repeat:
false, getDivingGearIfNeeded:
false, closeEnough: ArrestTargetDistance)
1473 UsePathingOutside =
false,
1474 IgnoreIfTargetDead =
true,
1475 TargetName =
Enemy.DisplayName,
1476 AlwaysUseEuclideanDistance =
false,
1477 SpeakIfFails =
false,
1478 SourceObjective =
this
1480 approachArrestTarget.Completed += OnArrestTargetReached;
1481 objectiveManager.AddObjective(approachArrestTarget);
1486 if (ShouldUnequipWeapon)
1496 if (ShouldUnequipWeapon)
1505 base.OnDeselected();
1506 if (character.TeamID ==
CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest && (!AllowHoldFire || (hasAimed && holdFireTimer <= 0)))
1509 Enemy.IsCriminal =
true;
1519 isLethalWeapon =
false;
1520 canSeeTarget =
false;
1521 seekWeaponObjective =
null;
1522 seekAmmunitionObjective =
null;
1523 retreatObjective =
null;
1524 followTargetObjective =
null;
1525 retreatTarget =
null;
1526 firstWarningTriggered =
false;
1527 lastWarningTriggered =
false;
1533 private void SpeakNoWeapons()
1535 if (!character.IsInFriendlySub)
1537 PlayerCrewSpeak(
"dialogcombatnoweapons".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 30);
1541 private void PlayerCrewSpeak(Identifier textIdentifier,
float delay,
float minDurationBetweenSimilar)
1543 if (character.IsOnPlayerTeam)
1545 Speak(textIdentifier, delay, minDurationBetweenSimilar);
1549 private void FriendlyGuardSpeak(Identifier textIdentifier,
float delay,
float minDurationBetweenSimilar)
1553 Speak(textIdentifier, delay, minDurationBetweenSimilar);
1557 private void Speak(Identifier textIdentifier,
float delay,
float minDurationBetweenSimilar)
1559 LocalizedString msg = TextManager.Get(textIdentifier);
1560 if (!msg.IsNullOrEmpty())
1562 character.Speak(msg.Value, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDurationBetweenSimilar);
1566 private void SetAimTimer(
float newTimer) => aimTimer = Math.Max(aimTimer, newTimer);
IEnumerable< Hull > VisibleHulls
Returns hulls that are visible to the character, including the current hull. Note that this is not an...
void FaceTarget(ISpatialEntity target)
override void OnCompleted()
Func< bool > holdFireCondition
Don't start using a weapon if this condition is true
override bool AllowOutsideSubmarine
override bool AllowInAnySub
override bool IgnoreUnsafeHulls
override void Update(float deltaTime)
override void OnDeselected()
override bool CheckObjectiveSpecific()
Should return whether the objective is completed or not.
override bool AbandonWhenCannotCompleteSubObjectives
override bool ConcurrentObjectives
AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier=1, float coolDown=DefaultCoolDown)
static float GetLethalDamage(ItemComponent weapon)
override float GetPriority()
override bool KeepDivingGearOn
override void Act(float deltaTime)
override Identifier Identifier
override void OnAbandon()
virtual bool CanBeCompleted
float Priority
Final priority value after all calculations.
readonly Character character
Func< AIObjective, bool > AbortCondition
Aborts the objective when this condition is true.
SteeringManager SteeringManager
IndoorsSteeringManager PathSteering
HumanAIController HumanAIController
readonly AIObjectiveManager objectiveManager
const float EmergencyObjectivePriority
Priority of objectives such as finding safety, rescuing someone in a critical state or defending agai...
AIObjective CurrentObjective
Includes orders.
Order GetOrder(AIObjective objective)
Return the first order with the specified objective. Can return null.
const float MaxObjectivePriority
Highest possible priority for any objective. Used in certain cases where the character needs to react...
Attacks are used to deal damage to characters, structures and items. They can be defined in the weapo...
float GetTotalDamage(bool includeStructureDamage=false)
readonly CharacterParams Params
float GetSkillLevel(string skillIdentifier)
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...
CharacterInventory Inventory
bool IsHostileEscortee
Set true only, if the character is turned hostile from an escort mission (See EscortMission).
Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos=null)
bool IsKnockedDown
Is the character knocked down regardless whether the technical state is dead, unconcious,...
void ReleaseSecondaryItem()
readonly AnimController AnimController
IEnumerable< Item >?? HeldItems
Items the character has in their hand slots. Doesn't return nulls and only returns items held in both...
bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity=null, bool seeThroughWindows=false, bool checkFacing=false)
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
float ConstantHealthRegeneration
static bool IsLatchedToSomeoneElse(Character target, Character character)
virtual Vector2 WorldPosition
static bool IsTrueForAnyBotInTheCrew(Character character, Func< HumanAIController, bool > predicate)
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....
readonly HashSet< Hull > UnreachableHulls
bool AutoFaceMovement
Resets each frame
AIObjectiveManager ObjectiveManager
override bool IsMentallyUnstable
bool Contains(Item item)
Is the item contained in this inventory. Does not recursively check items inside items.
virtual IEnumerable< Item > AllItems
All items contained in the inventory. Stacked items are returned as individual instances....
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)
IEnumerable< InvSlotType > AllowedSlots
bool TryInteract(Character user, bool ignoreRequiredItems=false, bool forceSelectKey=false, bool forceUseKey=false)
bool HasTag(Identifier tag)
List< ItemComponent > Components
ImmutableArray< SkillRequirementHint > SkillRequirementHints
The base class for components holding the different functionalities of the item
bool IsEmpty(Character user)
Returns true if the item is lacking required contained items, or if there's nothing with a non-zero c...
readonly Dictionary< ActionType, List< StatusEffect > > statusEffectLists
virtual float GetSkillMultiplier()
readonly List< Skill > RequiredSkills
ImmutableHashSet< Identifier > ContainableItemIdentifiers
Projectile FindProjectile(bool triggerOnUseOnContainers=false)
SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub=null, string errorMsgStr=null, float minGapSize=0, Func< PathNode, bool > startNodeFilter=null, Func< PathNode, bool > endNodeFilter=null, Func< PathNode, bool > nodeFilter=null, bool checkVisibility=true)
readonly Identifier Identifier
void SteeringManual(float deltaTime, Vector2 velocity)
void SteeringSeek(Vector2 targetSimPos, float weight=1)
void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight=1)
readonly Dictionary< Submarine, DockingPort > ConnectedDockingPorts
ActionType
ActionTypes define when a StatusEffect is executed.