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 UnequipWeapon()
904 if (Weapon ==
null) {
return; }
921 var slots = Weapon.
AllowedSlots.Where(CharacterInventory.IsHandSlotType);
923 if (!successfullyEquipped &&
character.
HasHandsFull(out (Item leftHandItem, Item rightHandItem) items))
930 if (successfullyEquipped)
932 SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
933 SetReloadTime(WeaponComponent);
946 private float findHullTimer;
947 private const float findHullInterval = 1.0f;
949 private void Retreat(
float deltaTime)
954 PlayerCrewSpeak(
"dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDurationBetweenSimilar: 20);
956 RemoveFollowTarget();
957 RemoveSubObjective(ref seekAmmunitionObjective);
958 if (retreatTarget !=
null)
966 retreatTarget =
null;
970 if (retreatObjective !=
null && retreatObjective.
Target != retreatTarget)
972 RemoveSubObjective(ref retreatObjective);
983 if (retreatTarget ==
null || retreatObjective is {
CanBeCompleted:
false })
985 if (findHullTimer > 0)
987 findHullTimer -= deltaTime;
992 if (hullSearchStatus != HullSearchStatus.Finished)
994 findSafety.UpdateSimpleEscape(deltaTime);
997 retreatTarget = potentialSafeHull;
998 findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
1005 UsePathingOutside =
false,
1006 SpeakIfFails =
false
1014 RemoveSubObjective(ref retreatObjective);
1022 onCompleted: () => RemoveSubObjective(ref retreatObjective));
1026 private void Engage(
float deltaTime)
1028 if (WeaponComponent ==
null)
1030 RemoveFollowTarget();
1040 retreatTarget =
null;
1041 RemoveSubObjective(ref retreatObjective);
1042 RemoveSubObjective(ref seekAmmunitionObjective);
1043 RemoveSubObjective(ref seekWeaponObjective);
1046 if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range))
1066 RemoveFollowTarget();
1070 if (followTargetObjective !=
null && followTargetObjective.
Target !=
Enemy)
1072 RemoveFollowTarget();
1074 TryAddSubObjective(ref followTargetObjective,
1077 UsePathingOutside =
false,
1078 IgnoreIfTargetDead =
true,
1080 AlwaysUseEuclideanDistance =
false,
1081 SpeakIfFails =
false
1089 RemoveSubObjective(ref followTargetObjective);
1097 if (followTargetObjective ==
null) {
return; }
1104 ItemPrefab prefab = ItemPrefab.Find(
null,
"handcuffs".ToIdentifier());
1109 i.SpawnedInCurrentOutpost =
true;
1110 i.AllowStealing =
false;
1114 arrestingRegistered =
true;
1115 followTargetObjective.Completed += OnArrestTargetReached;
1116 followTargetObjective.CloseEnough = ArrestTargetDistance;
1118 if (!arrestingRegistered)
1120 followTargetObjective.CloseEnough =
1121 WeaponComponent
switch
1131 private void RemoveFollowTarget()
1133 if (followTargetObjective !=
null)
1135 if (arrestingRegistered)
1137 followTargetObjective.Completed -= OnArrestTargetReached;
1139 RemoveSubObjective(ref followTargetObjective);
1141 arrestingRegistered =
false;
1144 private void OnArrestTargetReached()
1146 if (!
Enemy.IsKnockedDown)
1148 RemoveFollowTarget();
1154 foreach (var item
in Enemy.Inventory.AllItemsMod)
1157 if (item.HasTag(Tags.HandLockerItem) &&
Enemy.HasEquippedItem(item)) {
continue; }
1158 if (item.Illegitimate || item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || GetWeaponComponent(item) is { CombatPriority: > 0 })
1160 item.Drop(character);
1161 character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
1167 if (!HumanAIController.HasItem(Enemy, Tags.HandLockerItem, out IEnumerable<Item> matchingItems))
1169 HumanAIController.HasItem(character, Tags.HandLockerItem, out matchingItems);
1172 if (matchingItems.Any() &&
1173 !
Enemy.IsUnconscious &&
Enemy.IsKnockedDown && character.CanInteractWith(Enemy) && !
Enemy.LockHands)
1175 var handCuffs = matchingItems.First();
1176 if (!HumanAIController.TakeItem(handCuffs,
Enemy.Inventory, equip:
true, wear:
true))
1179 DebugConsole.NewMessage($
"{character.Name}: Failed to handcuff the target.", Color.Red);
1181 if (objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
1187 character.Speak(TextManager.Get(
"DialogTargetArrested").Value,
null, 3.0f,
"targetarrested".ToIdentifier(), 30.0f);
1189 if (!objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
1198 private void SeekAmmunition(ImmutableHashSet<Identifier> ammunitionIdentifiers)
1200 retreatTarget =
null;
1201 RemoveSubObjective(ref retreatObjective);
1202 RemoveSubObjective(ref seekWeaponObjective);
1203 RemoveFollowTarget();
1205 TryAddSubObjective(ref seekAmmunitionObjective,
1206 constructor: () =>
new AIObjectiveContainItem(character, ammunitionIdentifiers, itemContainer, objectiveManager)
1208 ItemCount = itemContainer.MainContainerCapacity * itemContainer.MaxStackSize,
1209 checkInventory =
false,
1210 MoveWholeStack =
true
1212 onCompleted: () => RemoveSubObjective(ref seekAmmunitionObjective),
1215 SteeringManager.Reset();
1216 RemoveSubObjective(ref seekAmmunitionObjective);
1217 ignoredWeapons.Add(Weapon);
1226 private bool Reload(
bool seekAmmo)
1228 if (WeaponComponent ==
null) {
return false; }
1229 if (
Weapon.OwnInventory ==
null) {
return true; }
1231 HumanAIController.UnequipEmptyItems(Weapon);
1232 ImmutableHashSet<Identifier> ammunitionIdentifiers =
null;
1233 if (WeaponComponent.RequiredItems.ContainsKey(RelatedItem.RelationType.Contained))
1235 foreach (RelatedItem requiredItem
in WeaponComponent.RequiredItems[RelatedItem.RelationType.Contained])
1237 if (
Weapon.OwnInventory.AllItems.Any(it => it.Condition > 0 && requiredItem.MatchesItem(it))) {
continue; }
1238 ammunitionIdentifiers = requiredItem.Identifiers;
1242 else if (WeaponComponent is
MeleeWeapon meleeWeapon)
1244 ammunitionIdentifiers = meleeWeapon.PreferredContainedItems;
1247 if (ammunitionIdentifiers !=
null)
1250 static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is
Item ownerItem && ownerItem.HasTag(Tags.MobileRadio);
1251 Item ammunition = character.Inventory.FindItem(i =>
1252 i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i) && i.IsInteractable(character), recursive:
true);
1253 if (ammunition !=
null)
1256 if (container.Inventory.TryPutItem(ammunition, user: character))
1259 SetReloadTime(WeaponComponent);
1261 else if (ammunition.ParentInventory == character.Inventory)
1263 ammunition.Drop(character);
1267 if (!WeaponComponent.IsEmpty(character))
1271 else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers !=
null)
1275 if (!
Weapon.OwnInventory.Container.DrawInventory) {
return false; }
1276 SeekAmmunition(ammunitionIdentifiers);
1281 private void Attack(
float deltaTime)
1283 character.CursorPosition =
Enemy.WorldPosition;
1284 if (AimAccuracy < 1)
1286 spreadTimer += deltaTime * Rand.Range(0.01f, 1f);
1287 float shake = Rand.Range(0.95f, 1.05f);
1288 float offsetAmount = (1 - AimAccuracy) * Rand.Range(300f, 500f);
1289 float distanceFactor = MathUtils.InverseLerp(0, 1000 * 1000, sqrDistance);
1290 float offset = (float)Math.Sin(spreadTimer * shake) * offsetAmount * distanceFactor;
1291 character.CursorPosition +=
new Vector2(0, offset);
1293 if (character.Submarine !=
null)
1295 character.CursorPosition -= character.Submarine.Position;
1297 visibilityCheckTimer -= deltaTime;
1298 if (visibilityCheckTimer <= 0.0f)
1300 canSeeTarget = character.CanSeeTarget(Enemy);
1301 visibilityCheckTimer = VisibilityCheckInterval;
1305 SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
1308 if (
Weapon.RequireAimToUse)
1310 character.SetInput(
InputType.Aim, hit:
false, held:
true);
1313 if (AllowHoldFire && holdFireTimer > 0)
1315 holdFireTimer -= deltaTime;
1320 aimTimer -= deltaTime;
1323 sqrDistance = Vector2.DistanceSquared(character.WorldPosition,
Enemy.WorldPosition);
1324 distanceTimer = DistanceCheckInterval;
1327 bool closeEnough =
true;
1328 float sqrRange = meleeWeapon.Range * meleeWeapon.Range;
1329 if (character.AnimController.InWater)
1331 if (sqrDistance > sqrRange)
1333 closeEnough =
false;
1339 float xDiff = Math.Abs(
Enemy.WorldPosition.X - character.WorldPosition.X);
1340 if (xDiff > meleeWeapon.Range)
1342 closeEnough =
false;
1344 float yDiff = Math.Abs(
Enemy.WorldPosition.Y - character.WorldPosition.Y);
1345 if (yDiff > Math.Max(meleeWeapon.Range, 100))
1347 closeEnough =
false;
1349 if (closeEnough &&
Enemy.WorldPosition.Y < character.WorldPosition.Y && yDiff > 25)
1352 HumanAIController.AnimController.Crouch();
1355 if (reloadTimer > 0) {
return; }
1356 if (holdFireCondition !=
null && holdFireCondition()) {
return; }
1359 UseWeapon(deltaTime);
1360 character.AIController.SteeringManager.Reset();
1362 else if (!character.IsFacing(
Enemy.WorldPosition))
1365 SetAimTimer(Rand.Range(1f, 1.5f) / AimSpeed);
1370 if (WeaponComponent is
RepairTool repairTool)
1372 float reach = AIObjectiveFixLeak.CalculateReach(repairTool, character);
1373 if (sqrDistance > reach * reach) {
return; }
1375 float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
1376 if (VectorExtensions.Angle(VectorExtensions.Forward(
Weapon.body.TransformedRotation),
Enemy.WorldPosition -
Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor)
1378 myBodies ??= character.AnimController.Limbs.Select(l => l.body.FarseerBody);
1380 var pickedBodies =
Submarine.PickBodies(
Weapon.SimPosition,
Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter);
1381 foreach (var body
in pickedBodies)
1386 Limb limb => limb.character,
1389 if (target !=
null && target != Enemy && HumanAIController.IsFriendly(target))
1394 UseWeapon(deltaTime);
1399 private void UseWeapon(
float deltaTime)
1402 if (Mode == CombatMode.Arrest && isLethalWeapon && character.IsOnPlayerTeam &&
Enemy.IsOnPlayerTeam) {
return; }
1403 character.SetInput(
InputType.Shoot, hit:
false, held:
true);
1404 Weapon.Use(deltaTime, user: character);
1405 SetReloadTime(WeaponComponent);
1410 float reloadTime = 0;
1411 switch (weaponComponent)
1415 if (rangedWeapon.ReloadTimer <= 0 && !rangedWeapon.HoldTrigger)
1417 reloadTime = rangedWeapon.
Reload;
1432 float reloadTime = GetReloadTime(weaponComponent);
1433 reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed);
1436 private void ClearInputs()
1443 private bool ShouldUnequipWeapon =>
1445 character.Submarine !=
null &&
1446 character.Submarine.TeamID == character.TeamID &&
1447 Character.CharacterList.None(c => c.Submarine == character.Submarine && HumanAIController.IsActive(c) && !HumanAIController.IsFriendly(character, c) && HumanAIController.VisibleHulls.Contains(c.CurrentHull));
1457 character.Speak(TextManager.Get(
"DialogTargetDown").Value,
null, 3.0f,
"targetdown".ToIdentifier(), 30.0f);
1462 bot.ObjectiveManager.CurrentObjective is
AIObjectiveGoTo { SourceObjective: AIObjectiveCombat combatObjective } && combatObjective.Enemy ==
Enemy))
1466 RemoveFollowTarget();
1467 var approachArrestTarget =
new AIObjectiveGoTo(
Enemy, character, objectiveManager, repeat:
false, getDivingGearIfNeeded:
false, closeEnough: ArrestTargetDistance)
1469 UsePathingOutside =
false,
1470 IgnoreIfTargetDead =
true,
1471 TargetName =
Enemy.DisplayName,
1472 AlwaysUseEuclideanDistance =
false,
1473 SpeakIfFails =
false,
1474 SourceObjective =
this
1476 approachArrestTarget.Completed += OnArrestTargetReached;
1477 objectiveManager.AddObjective(approachArrestTarget);
1482 if (ShouldUnequipWeapon)
1492 if (ShouldUnequipWeapon)
1501 base.OnDeselected();
1502 if (character.TeamID ==
CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest && (!AllowHoldFire || (hasAimed && holdFireTimer <= 0)))
1505 Enemy.IsCriminal =
true;
1515 isLethalWeapon =
false;
1516 canSeeTarget =
false;
1517 seekWeaponObjective =
null;
1518 seekAmmunitionObjective =
null;
1519 retreatObjective =
null;
1520 followTargetObjective =
null;
1521 retreatTarget =
null;
1522 firstWarningTriggered =
false;
1523 lastWarningTriggered =
false;
1529 private void SpeakNoWeapons()
1531 if (!character.IsInFriendlySub)
1533 PlayerCrewSpeak(
"dialogcombatnoweapons".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 30);
1537 private void PlayerCrewSpeak(Identifier textIdentifier,
float delay,
float minDurationBetweenSimilar)
1539 if (character.IsOnPlayerTeam)
1541 Speak(textIdentifier, delay, minDurationBetweenSimilar);
1545 private void FriendlyGuardSpeak(Identifier textIdentifier,
float delay,
float minDurationBetweenSimilar)
1549 Speak(textIdentifier, delay, minDurationBetweenSimilar);
1553 private void Speak(Identifier textIdentifier,
float delay,
float minDurationBetweenSimilar)
1555 LocalizedString msg = TextManager.Get(textIdentifier);
1556 if (!msg.IsNullOrEmpty())
1558 character.Speak(msg.Value, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDurationBetweenSimilar);
1562 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 bool CheckObjectiveState()
Should return whether the objective is completed or not.
override void OnDeselected()
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
bool TryPutItem(Item item, IEnumerable< InvSlotType > allowedSlots)
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 ...
CharacterInventory Inventory
bool IsHostileEscortee
Set true only, if the character is turned hostile from an escort mission (See EscortMission).
bool Unequip(Item item)
Attempts to unequip an item. First tries to put the item in any slot. If that fails,...
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
bool HasHandsFull(out(Item leftHandItem, Item rightHandItem) items)
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)
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....
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.
@ Character
Characters only