Unstable 0.1400.2.0 (a mimir edition)

This commit is contained in:
Markus Isberg
2021-05-28 19:04:09 +03:00
parent 5bc850cddb
commit 0b3fb5e440
126 changed files with 1623 additions and 787 deletions
@@ -16,6 +16,13 @@ namespace Barotrauma
public enum CirclePhase { Start, CloseIn, FallBack, Advance, Strike }
public enum WallTargetingMethod
{
Target = 0x1,
Heading = 0x2,
Steering = 0x4
}
partial class EnemyAIController : AIController
{
public static bool DisableEnemyAI;
@@ -54,7 +61,7 @@ namespace Barotrauma
private float attackLimbResetTimer;
private bool IsAttackRunning => AttackingLimb != null && AttackingLimb.attack.IsRunning;
private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0;
private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0 || _previousAttackingLimb != null && _previousAttackingLimb.attack.CoolDownTimer > 0;
public float CombatStrength => AIParams.CombatStrength;
private float Sight => AIParams.Sight;
private float Hearing => AIParams.Hearing;
@@ -63,14 +70,18 @@ namespace Barotrauma
private FishAnimController FishAnimController => Character.AnimController as FishAnimController;
//the limb selected for the current attack
private Limb _attackingLimb;
private Limb _previousAttackingLimb;
public Limb AttackingLimb
{
get { return _attackingLimb; }
private set
{
attackLimbResetTimer = 0;
if (_attackingLimb != value)
{
_previousAttackingLimb = _attackingLimb;
}
_attackingLimb = value;
attackVector = null;
Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse;
@@ -342,6 +353,10 @@ namespace Barotrauma
{
targetingTag = "weaker";
}
else
{
targetingTag = "equal";
}
}
}
}
@@ -480,10 +495,6 @@ namespace Barotrauma
{
CharacterParams.TargetParams targetingParams = null;
UpdateTargets(Character, out targetingParams);
if (!IsLatchedOnSub)
{
UpdateWallTarget(requiredHoleCount);
}
updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f);
if (SelectedAiTarget == null)
{
@@ -494,6 +505,10 @@ namespace Barotrauma
selectedTargetingParams = targetingParams;
State = targetingParams.State;
}
if (SelectedAiTarget?.Entity != null && !IsLatchedOnSub && State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive)
{
UpdateWallTarget(requiredHoleCount);
}
}
}
@@ -1004,12 +1019,8 @@ namespace Barotrauma
if (!w.SectionBodyDisabled(i))
{
isBroken = false;
Vector2 sectionPos = w.SectionPosition(i);
Vector2 sectionPos = w.SectionPosition(i, world: true);
attackWorldPos = sectionPos;
if (w.Submarine != null)
{
attackWorldPos += w.Submarine.Position;
}
attackSimPos = ConvertUnits.ToSimUnits(attackWorldPos);
break;
}
@@ -1027,18 +1038,19 @@ namespace Barotrauma
bool pursue = false;
if (IsCoolDownRunning)
{
if (AttackingLimb.attack.CoolDownTimer >= AttackingLimb.attack.CoolDown + AttackingLimb.attack.CurrentRandomCoolDown - AttackingLimb.attack.AfterAttackDelay)
var currentAttackLimb = AttackingLimb ?? _previousAttackingLimb;
if (currentAttackLimb.attack.CoolDownTimer >= currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay)
{
return;
}
switch (AttackingLimb.attack.AfterAttack)
switch (currentAttackLimb.attack.AfterAttack)
{
case AIBehaviorAfterAttack.Pursue:
case AIBehaviorAfterAttack.PursueIfCanAttack:
if (AttackingLimb.attack.SecondaryCoolDown <= 0)
if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
{
// No (valid) secondary cooldown defined.
if (AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue)
if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue)
{
canAttack = false;
pursue = true;
@@ -1051,13 +1063,13 @@ namespace Barotrauma
}
else
{
if (AttackingLimb.attack.SecondaryCoolDownTimer <= 0)
if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
{
// Don't allow attacking when the attack target has just changed.
if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget)
{
canAttack = false;
if (AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.PursueIfCanAttack)
if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.PursueIfCanAttack)
{
// Fall back if cannot attack.
UpdateFallBack(attackWorldPos, deltaTime, true);
@@ -1068,7 +1080,7 @@ namespace Barotrauma
else
{
// If the secondary cooldown is defined and expired, check if we can switch the attack
var newLimb = GetAttackLimb(attackWorldPos, AttackingLimb);
var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
if (newLimb != null)
{
// Attack with the new limb
@@ -1077,7 +1089,7 @@ namespace Barotrauma
else
{
// No new limb was found.
if (AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue)
if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue)
{
canAttack = false;
pursue = true;
@@ -1099,26 +1111,26 @@ namespace Barotrauma
break;
case AIBehaviorAfterAttack.FallBackUntilCanAttack:
case AIBehaviorAfterAttack.FollowThroughUntilCanAttack:
if (AttackingLimb.attack.SecondaryCoolDown <= 0)
if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
{
// No (valid) secondary cooldown defined.
UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
else
{
if (AttackingLimb.attack.SecondaryCoolDownTimer <= 0)
if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
{
// Don't allow attacking when the attack target has just changed.
if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget)
{
UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
else
{
// If the secondary cooldown is defined and expired, check if we can switch the attack
var newLimb = GetAttackLimb(attackWorldPos, AttackingLimb);
var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
if (newLimb != null)
{
// Attack with the new limb
@@ -1127,7 +1139,7 @@ namespace Barotrauma
else
{
// No new limb was found.
UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
}
@@ -1135,13 +1147,13 @@ namespace Barotrauma
else
{
// Cooldown not yet expired -> steer away from the target
UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
return;
}
}
break;
case AIBehaviorAfterAttack.IdleUntilCanAttack:
if (AttackingLimb.attack.SecondaryCoolDown <= 0)
if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
{
// No (valid) secondary cooldown defined.
UpdateIdle(deltaTime, followLastTarget: false);
@@ -1149,7 +1161,7 @@ namespace Barotrauma
}
else
{
if (AttackingLimb.attack.SecondaryCoolDownTimer <= 0)
if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
{
// Don't allow attacking when the attack target has just changed.
if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget)
@@ -1160,7 +1172,7 @@ namespace Barotrauma
else
{
// If the secondary cooldown is defined and expired, check if we can switch the attack
var newLimb = GetAttackLimb(attackWorldPos, AttackingLimb);
var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
if (newLimb != null)
{
// Attack with the new limb
@@ -1258,7 +1270,7 @@ namespace Barotrauma
Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackingLimb.WorldPosition;
Vector2 toTarget = attackWorldPos - attackLimbPos;
// Add a margin when the target is moving away, because otherwise it might be difficult to reach it (the attack takes some time to perform)
// Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute
if (wallTarget != null)
{
if (wallTarget.Structure.Submarine != null)
@@ -1283,9 +1295,14 @@ namespace Barotrauma
Vector2 CalculateMargin(Vector2 targetVelocity)
{
if (targetVelocity == Vector2.Zero) { return targetVelocity; }
if (targetVelocity == Vector2.Zero) { return Vector2.Zero; }
float diff = AttackingLimb.attack.Range - AttackingLimb.attack.DamageRange;
if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackingLimb.attack.DamageRange)) { return Vector2.Zero; }
float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity));
return ConvertUnits.ToDisplayUnits(targetVelocity) * AttackingLimb.attack.Duration * dot;
if (dot <= 0 || !MathUtils.IsValid(dot)) { return Vector2.Zero; }
float distanceOffset = diff * AttackingLimb.attack.Duration;
// Intentionally omit the unit conversion because we use distanceOffset as a multiplier.
return targetVelocity * distanceOffset * dot;
}
// Check that we can reach the target
@@ -1649,7 +1666,7 @@ namespace Barotrauma
float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f);
}
SteeringManager.SteeringSeek(steerPos, 10);
if (SelectedAiTarget?.Entity is Character || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2))
if (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2))
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30);
}
@@ -1813,9 +1830,8 @@ namespace Barotrauma
ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100);
}
}
else
else if (!AIParams.HasTag("equal"))
{
// Equal strength
ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100);
}
}
@@ -2152,6 +2168,10 @@ namespace Barotrauma
{
targetingTag = "weaker";
}
else
{
targetingTag = "equal";
}
if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee))
{
if (SelectedAiTarget == aiTarget)
@@ -2619,91 +2639,163 @@ namespace Barotrauma
}
private WallTarget wallTarget;
private readonly List<(Body, int, Vector2)> wallHits = new List<(Body, int, Vector2)>(3);
private void UpdateWallTarget(int requiredHoleCount)
{
wallTarget = null;
if (State == AIState.Flee || State == AIState.Escape) { return; }
if (AIParams.CanOpenDoors && HasValidPath(requireNonDirty: true)) { return; }
if (SelectedAiTarget == null) { return; }
if (SelectedAiTarget.Entity == null) { return; }
Vector2 rayStart = SimPosition;
Vector2 rayEnd = SelectedAiTarget.SimPosition;
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
if (SelectedAiTarget.Entity == null) { return; }
wallHits.Clear();
Structure wall = null;
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target))
{
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
}
else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null)
{
rayEnd -= Character.Submarine.SimPosition;
}
Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine);
if (Submarine.LastPickedFraction != 1.0f && closestBody != null)
{
if (closestBody.UserData is Structure wall && wall.Submarine != null && (Character.IsBot || wall.Submarine.Info.IsPlayer || wall.Submarine.Info.IsOutpost && TargetOutposts))
Vector2 rayStart = SimPosition;
Vector2 rayEnd = SelectedAiTarget.SimPosition;
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition));
float sectionDamage = wall.SectionDamage(sectionIndex);
for (int i = sectionIndex - 2; i <= sectionIndex + 2; i++)
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
}
else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null)
{
rayEnd -= Character.Submarine.SimPosition;
}
DoRayCast(rayStart, rayEnd);
}
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Heading))
{
Vector2 rayStart = SimPosition;
Vector2 rayEnd = rayStart + VectorExtensions.Forward(Character.AnimController.Collider.Rotation + MathHelper.PiOver2, avoidLookAheadDistance * 5);
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
rayEnd -= SelectedAiTarget.Entity.Submarine.SimPosition;
}
else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null)
{
rayStart -= Character.Submarine.SimPosition;
rayEnd -= Character.Submarine.SimPosition;
}
DoRayCast(rayStart, rayEnd);
}
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Steering))
{
Vector2 rayStart = SimPosition;
Vector2 rayEnd = rayStart + Steering * 5;
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
rayEnd -= SelectedAiTarget.Entity.Submarine.SimPosition;
}
else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null)
{
rayStart -= Character.Submarine.SimPosition;
rayEnd -= Character.Submarine.SimPosition;
}
DoRayCast(rayStart, rayEnd);
}
if (wallHits.Any())
{
Body closestBody = null;
float closestDistance = 0;
int sectionIndex = -1;
Vector2 sectionPos = Vector2.Zero;
foreach ((Body body, int index, Vector2 sectionPosition) in wallHits)
{
float distance = Vector2.DistanceSquared(SimPosition, sectionPosition);
if (closestBody == null || closestDistance == 0 || distance < closestDistance)
{
if (wall.SectionBodyDisabled(i))
{
if (Character.AnimController.CanEnterSubmarine && CanPassThroughHole(wall, i, requiredHoleCount))
{
sectionIndex = i;
break;
}
else
{
// Ignore and keep breaking other sections
continue;
}
}
if (wall.SectionDamage(i) > sectionDamage)
{
sectionIndex = i;
}
}
Vector2 sectionPos = wall.SectionPosition(sectionIndex);
Vector2 attachTargetNormal;
if (wall.IsHorizontal)
{
attachTargetNormal = new Vector2(0.0f, Math.Sign(WorldPosition.Y - wall.WorldPosition.Y));
sectionPos.Y += (wall.BodyHeight <= 0.0f ? wall.Rect.Height : wall.BodyHeight) / 2 * attachTargetNormal.Y;
}
else
{
attachTargetNormal = new Vector2(Math.Sign(WorldPosition.X - wall.WorldPosition.X), 0.0f);
sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X;
}
LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
{
if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner") || wall.Submarine != null && wall.Submarine == Character.Submarine)
{
if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine)
{
// Blocked by a wall that shouldn't be targeted. The main intention here is to prevents monsters from entering the the tail and the nose pieces.
IgnoreTarget(SelectedAiTarget);
ResetAITarget();
}
else
{
wallTarget = new WallTarget(sectionPos, wall, sectionIndex);
}
}
closestBody = body;
closestDistance = distance;
wall = closestBody.UserData as Structure;
sectionPos = sectionPosition;
sectionIndex = index;
}
}
if (!Character.AnimController.CanEnterSubmarine && wallTarget == null && selectedTargetingParams?.AttackPattern == AttackPattern.Straight)
if (closestBody == null || sectionIndex == -1) { return; }
Vector2 attachTargetNormal;
if (wall.IsHorizontal)
{
if (closestBody.UserData is Structure w && w.Submarine != null && w.Submarine == SelectedAiTarget.Entity?.Submarine ||
closestBody.UserData is Item i && i.Submarine != null && i.Submarine == SelectedAiTarget.Entity?.Submarine)
attachTargetNormal = new Vector2(0.0f, Math.Sign(WorldPosition.Y - wall.WorldPosition.Y));
sectionPos.Y += (wall.BodyHeight <= 0.0f ? wall.Rect.Height : wall.BodyHeight) / 2 * attachTargetNormal.Y;
}
else
{
attachTargetNormal = new Vector2(Math.Sign(WorldPosition.X - wall.WorldPosition.X), 0.0f);
sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X;
}
LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
{
if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine)
{
// Cannot reach the target, because it's blocked by a disabled wall or a door
// Blocked by a wall that shouldn't be targeted. The main intention here is to prevent monsters from entering the the tail and the nose pieces.
IgnoreTarget(SelectedAiTarget);
ResetAITarget();
}
else
{
wallTarget = new WallTarget(sectionPos, wall, sectionIndex);
}
}
else
{
// Blocked by a disabled wall.
IgnoreTarget(SelectedAiTarget);
ResetAITarget();
}
}
void DoRayCast(Vector2 rayStart, Vector2 rayEnd)
{
Body hitTarget = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine);
if (hitTarget != null && IsValid(hitTarget, out wall))
{
int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition));
if (sectionIndex >= 0)
{
wallHits.Add((hitTarget, sectionIndex, GetSectionPosition(wall, sectionIndex)));
}
}
}
Vector2 GetSectionPosition(Structure wall, int sectionIndex)
{
float sectionDamage = wall.SectionDamage(sectionIndex);
for (int i = sectionIndex - 2; i <= sectionIndex + 2; i++)
{
if (wall.SectionBodyDisabled(i))
{
if (Character.AnimController.CanEnterSubmarine && CanPassThroughHole(wall, i, requiredHoleCount))
{
sectionIndex = i;
break;
}
else
{
// Ignore and keep breaking other sections
continue;
}
}
if (wall.SectionDamage(i) > sectionDamage)
{
sectionIndex = i;
}
}
return wall.SectionPosition(sectionIndex, world: false);
}
bool IsValid(Body hit, out Structure wall)
{
wall = null;
if (Submarine.LastPickedFraction == 1.0f) { return false; }
if (!(hit.UserData is Structure w)) { return false; }
if (w.Submarine == null) { return false; }
if (w.Submarine != SelectedAiTarget.Entity.Submarine) { return false; }
if (Character.Submarine == null && w.prefab.Tags.Contains("inner")) { return false; }
if (!AIParams.TargetOuterWalls && !w.prefab.Tags.Contains("inner")) { return false; }
wall = w;
return true;
}
}
@@ -2712,7 +2804,7 @@ namespace Barotrauma
if (wallTarget != null && wallTarget.SectionIndex > -1 && CanPassThroughHole(wallTarget.Structure, wallTarget.SectionIndex, requiredHoleCount))
{
WallSection section = wallTarget.Structure.GetSection(wallTarget.SectionIndex);
Vector2 targetPos = wallTarget.Structure.SectionPosition(wallTarget.SectionIndex, true);
Vector2 targetPos = wallTarget.Structure.SectionPosition(wallTarget.SectionIndex, world: true);
return section?.gap != null && SteerThroughGap(wallTarget.Structure, section, targetPos, deltaTime);
}
else if (SelectedAiTarget != null)
@@ -136,8 +136,9 @@ namespace Barotrauma
}
public override bool IsMentallyUnstable =>
MentalStateManager?.CurrentMentalType != MentalStateManager.MentalType.Normal &&
MentalStateManager?.CurrentMentalType != MentalStateManager.MentalType.Confused;
MentalStateManager == null ? false :
MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Normal &&
MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Confused;
public ShipCommandManager ShipCommandManager { get; private set; }
@@ -740,9 +741,10 @@ namespace Barotrauma
suitableContainer = null;
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, positionalReference: containableItem, customPriorityFunction: i =>
{
if (i.IsThisOrAnyContainerIgnoredByAI()) { return 0; }
if (i.IsThisOrAnyContainerIgnoredByAI(character)) { return 0; }
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
if (!container.HasAccess(character)) { return 0; }
if (!container.Inventory.CanBePut(containableItem)) { return 0; }
if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined))
{
@@ -1032,11 +1034,11 @@ namespace Barotrauma
return;
}
float cumulativeDamage = GetDamageDoneByAttacker(attacker);
if (!Character.IsSecurity && attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null)
bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null;
if (isAccidental)
{
if (cumulativeDamage > 1)
if (!Character.IsSecurity && cumulativeDamage > 1)
{
// Don't retaliate on damage done by friendly NPC, because we know it's accidental, unless if it's a berserking AI
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
}
}
@@ -1109,7 +1111,7 @@ namespace Barotrauma
{
foreach (Character otherCharacter in Character.CharacterList)
{
if (otherCharacter == Character || otherCharacter.IsDead || otherCharacter.IsUnconscious || otherCharacter.Removed) { continue; }
if (otherCharacter == Character || otherCharacter.IsUnconscious || otherCharacter.Removed) { continue; }
if (otherCharacter.Submarine != Character.Submarine) { continue; }
if (otherCharacter.Submarine != attacker.Submarine) { continue; }
if (otherCharacter.Info?.Job == null || otherCharacter.IsInstigator) { continue; }
@@ -1921,18 +1923,45 @@ namespace Barotrauma
private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self);
public static bool IsItemOperatedByAnother(Character character, ItemComponent target, out Character operatingCharacter)
public static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter)
{
operatingCharacter = null;
if (character == null) { return false; }
if (target?.Item == null) { return false; }
bool isOrder = IsOrderedToOperateThis(character.AIController);
foreach (var c in Character.CharacterList)
foreach (Character c in Character.CharacterList)
{
if (c == character) { continue; }
if (c.IsDead || c.IsIncapacitated) { continue; }
if (!IsFriendly(character, c, onlySameTeam: true)) { continue; }
if (c.Removed) { continue; }
if (c.TeamID != team) { continue; }
if (c.IsIncapacitated) { continue; }
bool isOperated = c.SelectedConstruction == target.Item;
if (!isOperated)
{
if (c.AIController is HumanAIController humanAI)
{
isOperated = humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item);
}
}
operatingCharacter = c;
if (isOperated)
{
return true;
}
}
return false;
}
// There's some duplicate logic in the two methods below, but making them use the same code would require some changes in the target classes so that we could use exactly the same checks.
// And even then there would be some differences that could end up being confusing (like the exception for steering).
public bool IsItemOperatedByAnother(ItemComponent target, out Character other)
{
other = null;
if (target?.Item == null) { return false; }
bool isOrder = IsOrderedToOperateThis(Character.AIController);
foreach (Character c in Character.CharacterList)
{
if (c == Character) { continue; }
if (c.Removed) { continue; }
if (c.TeamID != Character.TeamID) { continue; }
if (c.IsIncapacitated) { continue; }
other = c;
if (c.IsPlayer)
{
if (c.SelectedConstruction == target.Item)
@@ -1963,7 +1992,7 @@ namespace Barotrauma
}
else
{
if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder == operatingAI.ObjectiveManager.CurrentObjective)
if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective)
{
// The other bot is ordered to do something else
continue;
@@ -1971,12 +2000,12 @@ namespace Barotrauma
if (target is Steering)
{
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
if (character.GetSkillLevel("helm") <= c.GetSkillLevel("helm"))
if (Character.GetSkillLevel("helm") <= c.GetSkillLevel("helm"))
{
return true;
}
}
else if (target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c))
else if (target.DegreeOfSuccess(Character) <= target.DegreeOfSuccess(c))
{
return true;
}
@@ -1985,7 +2014,65 @@ namespace Barotrauma
}
}
return false;
bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item;
bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item;
}
public bool IsItemRepairedByAnother(Item target, out Character other)
{
other = null;
if (Character == null) { return false; }
if (target == null) { return false; }
bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController);
foreach (var c in Character.CharacterList)
{
if (c == Character) { continue; }
if (c.TeamID != Character.TeamID) { continue; }
if (c.IsIncapacitated) { continue; }
other = c;
if (c.IsPlayer)
{
if (target.Repairables.Any(r => r.CurrentFixer == c))
{
// If the other character is player, don't try to repair
return true;
}
}
else if (c.AIController is HumanAIController operatingAI)
{
var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective<AIObjectiveRepairItems>();
if (repairItemsObjective == null) { continue; }
if (repairItemsObjective.SubObjectives.None(o => o is AIObjectiveRepairItem repairObjective && repairObjective.Item == target))
{
// Not targeting the same item.
continue;
}
bool isTargetOrdered = IsOrderedToRepairThis(operatingAI);
if (!isOrder && isTargetOrdered)
{
// If the other bot is ordered to repair the item, let him do it, unless we are ordered too
return true;
}
else
{
if (isOrder && !isTargetOrdered)
{
// We are ordered and the target is not -> allow to repair
continue;
}
else
{
if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective)
{
// The other bot is ordered to do something else
continue;
}
return target.Repairables.Max(r => r.DegreeOfSuccess(Character)) <= target.Repairables.Max(r => r.DegreeOfSuccess(c));
}
}
}
}
return false;
bool IsOrderedToRepairThis(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveRepairItems repairOrder && repairOrder.PrioritizedItem == target;
}
#region Wrappers
@@ -1994,7 +2081,6 @@ namespace Barotrauma
public bool IsTrueForAnyCrewMember(Func<HumanAIController, bool> predicate) => IsTrueForAnyCrewMember(Character, predicate);
public bool IsTrueForAllCrewMembers(Func<HumanAIController, bool> predicate) => IsTrueForAllCrewMembers(Character, predicate);
public int CountCrew(Func<HumanAIController, bool> predicate = null, bool onlyActive = true, bool onlyBots = false) => CountCrew(Character, predicate, onlyActive, onlyBots);
public bool IsItemOperatedByAnother(ItemComponent target, out Character operatingCharacter) => IsItemOperatedByAnother(Character, target, out operatingCharacter);
#endregion
}
}
@@ -20,7 +20,7 @@ namespace Barotrauma
{
if (battery == null) { return false; }
var item = battery.Item;
if (item.IgnoreByAI) { return false; }
if (item.IgnoreByAI(character)) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (item.Submarine == null) { return false; }
if (item.CurrentHull == null) { return false; }
@@ -61,7 +61,7 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (item.IgnoreByAI)
if (item.IgnoreByAI(character))
{
Abandon = true;
return;
@@ -82,16 +82,17 @@ namespace Barotrauma
public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) =>
allowUnloading &&
!container.IgnoreByAI &&
!container.IgnoreByAI(character) &&
container.IsInteractable(character) &&
container.HasTag("allowcleanup") &&
container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() &&
container.GetComponent<ItemContainer>() is ItemContainer itemContainer && itemContainer.HasAccess(character) &&
IsItemInsideValidSubmarine(container, character);
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true)
{
if (item == null) { return false; }
if (item.IgnoreByAI) { return false; }
if (item.IgnoreByAI(character)) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (item.SpawnedInOutpost) { return false; }
if (item.ParentInventory != null)
@@ -64,7 +64,7 @@ namespace Barotrauma
protected override bool CheckObjectiveSpecific()
{
if (IsCompleted) { return true; }
if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI()))
if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI(character)))
{
Abandon = true;
return false;
@@ -87,11 +87,11 @@ namespace Barotrauma
}
}
private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && !i.IsThisOrAnyContainerIgnoredByAI();
private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && !i.IsThisOrAnyContainerIgnoredByAI(character);
protected override void Act(float deltaTime)
{
if (container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI())
if (container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character))
{
Abandon = true;
return;
@@ -147,7 +147,7 @@ namespace Barotrauma
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = container.Item.Name,
AbortCondition = obj =>
container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI() ||
container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) ||
ItemToContain == null || ItemToContain.Removed ||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>()
@@ -63,13 +63,16 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
Item itemToDecontain = targetItem ?? sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id) && !i.IgnoreByAI), recursive: false);
Item itemToDecontain =
targetItem ??
sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id) && !i.IgnoreByAI(character)), recursive: false);
if (itemToDecontain == null)
{
Abandon = true;
return;
}
if (itemToDecontain.IgnoreByAI)
if (itemToDecontain.IgnoreByAI(character))
{
Abandon = true;
return;
@@ -38,10 +38,13 @@ namespace Barotrauma
Priority = 0;
Abandon = true;
}
else if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.Character.IsBot && other.ObjectiveManager.GetActiveObjective<AIObjectiveFixLeak>()?.Leak == Leak))
else if (HumanAIController.IsTrueForAnyCrewMember(
other => other != HumanAIController &&
other.Character.IsBot &&
other.ObjectiveManager.GetActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks &&
fixLeaks.SubObjectives.Any(so => so is AIObjectiveFixLeak fixObjective && fixObjective.Leak == Leak)))
{
Priority = 0;
Abandon = true;
}
else
{
@@ -74,7 +74,7 @@ namespace Barotrauma
// Don't fix a leak on a wall section set to be ignored
if (gap.ConnectedWall != null)
{
if (gap.ConnectedWall.Sections.Any(s => s.gap == gap && s.IgnoreByAI)) { return false; }
if (gap.ConnectedWall.Sections.Any(s => s.gap == gap && s.IgnoreByAI(character))) { return false; }
if (gap.ConnectedWall.MaxHealth <= 0.0f) { return false; }
}
if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; }
@@ -305,6 +305,7 @@ namespace Barotrauma
if (rootInventoryOwner is Item ownerItem)
{
if (!ownerItem.IsInteractable(character)) { continue; }
if (!(ownerItem.GetComponent<ItemContainer>()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; }
}
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y);
@@ -403,7 +404,7 @@ namespace Barotrauma
private bool CheckItem(Item item)
{
if (!item.IsInteractable(character)) { return false; }
if (item.IsThisOrAnyContainerIgnoredByAI()) { return false; }
if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; }
if (ignoredItems.Contains(item)) { return false; };
if (item.Condition < TargetCondition) { return false; }
if (ItemFilter != null && !ItemFilter(item)) { return false; }
@@ -118,7 +118,7 @@ namespace Barotrauma
targetItem.CurrentHull.FireSources.Any() ||
HumanAIController.IsItemOperatedByAnother(target, out _) ||
Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))
|| component.Item.IgnoreByAI || (useController && controller.Item.IgnoreByAI))
|| component.Item.IgnoreByAI(character) || useController && controller.Item.IgnoreByAI(character))
{
Priority = 0;
}
@@ -27,7 +27,7 @@ namespace Barotrauma
protected override bool Filter(Pump pump)
{
if (pump == null) { return false; }
if (pump.Item.IgnoreByAI) { return false; }
if (pump.Item.IgnoreByAI(character)) { return false; }
if (!pump.Item.IsInteractable(character)) { return false; }
if (pump.Item.HasTag("ballast")) { return false; }
if (pump.Item.Submarine == null) { return false; }
@@ -34,7 +34,7 @@ namespace Barotrauma
protected override float GetPriority()
{
if (!IsAllowed || Item.IgnoreByAI)
if (!IsAllowed || Item.IgnoreByAI(character))
{
Priority = 0;
Abandon = true;
@@ -44,10 +44,10 @@ namespace Barotrauma
}
return Priority;
}
// Ignore items that are being repaired by someone else.
if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character))
if (HumanAIController.IsItemRepairedByAnother(Item, out _))
{
Priority = 0;
IsCompleted = true;
}
else
{
@@ -16,7 +16,7 @@ namespace Barotrauma
/// </summary>
public string RelevantSkill;
private readonly Item prioritizedItem;
public Item PrioritizedItem { get; private set; }
public override bool AllowMultipleInstances => true;
public override bool AllowInAnySub => true;
@@ -28,7 +28,7 @@ namespace Barotrauma
public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Item prioritizedItem = null)
: base(character, objectiveManager, priorityModifier)
{
this.prioritizedItem = prioritizedItem;
PrioritizedItem = prioritizedItem;
}
protected override void CreateObjectives()
@@ -76,7 +76,7 @@ namespace Barotrauma
{
if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier.Equals(RelevantSkill, StringComparison.OrdinalIgnoreCase)))) { return false; }
}
return true;
return !HumanAIController.IsItemRepairedByAnother(item, out _);
}
public static bool ViableForRepair(Item item, Character character, HumanAIController humanAIController)
@@ -139,7 +139,7 @@ namespace Barotrauma
protected override IEnumerable<Item> GetList() => Item.ItemList;
protected override AIObjective ObjectiveConstructor(Item item)
=> new AIObjectiveRepairItem(character, item, objectiveManager, priorityModifier: PriorityModifier, isPriority: item == prioritizedItem);
=> new AIObjectiveRepairItem(character, item, objectiveManager, priorityModifier: PriorityModifier, isPriority: item == PrioritizedItem);
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
=> HumanAIController.RemoveTargets<AIObjectiveRepairItems, Item>(character, target);
@@ -147,7 +147,7 @@ namespace Barotrauma
public static bool IsValidTarget(Item item, Character character)
{
if (item == null) { return false; }
if (item.IgnoreByAI) { return false; }
if (item.IgnoreByAI(character)) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (item.IsFullCondition) { return false; }
if (item.CurrentHull == null) { return false; }
@@ -495,27 +495,19 @@ namespace Barotrauma
if (submarine == null) { return matchingItems; }
if (ItemComponentType != null || TargetItems.Length > 0)
{
matchingItems = TargetItems.Length > 0 ?
Item.ItemList.FindAll(it => TargetItems.Contains(it.Prefab.Identifier) || it.HasTag(TargetItems)) :
Item.ItemList.FindAll(it => TryGetTargetItemComponent(it, out _));
if (mustBelongToPlayerSub)
foreach (var item in Item.ItemList)
{
matchingItems.RemoveAll(it => it.Submarine?.Info != null && it.Submarine.Info.Type != SubmarineType.Player);
}
matchingItems.RemoveAll(it => it.Submarine != submarine && !submarine.DockedTo.Contains(it.Submarine));
if (requiredTeam.HasValue)
{
matchingItems.RemoveAll(it => it.Submarine == null || it.Submarine.TeamID != requiredTeam.Value);
}
matchingItems.RemoveAll(it => it.NonInteractable);
if (UseController)
{
matchingItems.RemoveAll(i => i.Components.None(c => c.GetType() == ItemComponentType) && !i.TryFindController(out _));
}
if (interactableFor != null)
{
matchingItems.RemoveAll(it => !it.IsInteractable(interactableFor) ||
(UseController && it.FindController() is Controller c && !c.Item.IsInteractable(interactableFor)));
if (TargetItems.Length > 0 && !TargetItems.Contains(item.Prefab.Identifier) && !item.HasTag(TargetItems)) { continue; }
if (TargetItems.Length == 0 && !TryGetTargetItemComponent(item, out _)) { continue; }
if (mustBelongToPlayerSub && item.Submarine?.Info != null && item.Submarine.Info.Type != SubmarineType.Player) { continue; }
if (item.Submarine != submarine && !submarine.DockedTo.Contains(item.Submarine)) { continue; }
if (requiredTeam.HasValue && (item.Submarine == null || item.Submarine.TeamID != requiredTeam.Value)) { continue; }
if (item.NonInteractable) { continue; }
if (ItemComponentType != null && item.Components.None(c => c.GetType() == ItemComponentType)) { continue; }
Controller controller = null;
if (UseController && !item.TryFindController(out controller)) { continue; }
if (interactableFor != null && (!item.IsInteractable(interactableFor) || (UseController && !controller.Item.IsInteractable(interactableFor)))) { continue; }
matchingItems.Add(item);
}
}
return matchingItems;
@@ -24,8 +24,6 @@ namespace Barotrauma
return false;
}
if (TargetItem.IgnoreByAI) { return false; }
return true;
}
}
@@ -270,14 +270,16 @@ namespace Barotrauma
else
{
LimbJoint rightWrist = GetJointBetweenLimbs(LimbType.RightForearm, LimbType.RightHand);
if (rightWrist != null)
{
forearmLength = Vector2.Distance(
rightElbow.LimbA.type == LimbType.RightForearm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB,
rightWrist.LimbA.type == LimbType.RightForearm ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB);
forearmLength = Vector2.Distance(
rightElbow.LimbA.type == LimbType.RightForearm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB,
rightWrist.LimbA.type == LimbType.RightForearm ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB);
forearmLength += Vector2.Distance(
rightHand.PullJointLocalAnchorA,
rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB);
forearmLength += Vector2.Distance(
rightHand.PullJointLocalAnchorA,
rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB);
}
}
}
}
@@ -115,6 +115,8 @@ namespace Barotrauma
protected Key[] keys;
public HumanPrefab Prefab;
private CharacterTeamType teamID;
public CharacterTeamType TeamID
{
@@ -1365,11 +1367,27 @@ namespace Barotrauma
}
}
}
private List<Item> wearableItems = new List<Item>();
public float GetSkillLevel(string skillIdentifier)
{
if (Info?.Job == null) { return 0.0f; }
float skillLevel = Info.Job.GetSkillLevel(skillIdentifier);
if (skillIdentifier != null)
{
for (int i = 0; i < Inventory.Capacity; i++)
{
if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.GetItemAt(i)?.GetComponent<Wearable>() is Wearable wearable)
{
if (wearable.SkillModifiers.TryGetValue(skillIdentifier, out float skillValue))
{
skillLevel += skillValue;
}
}
}
}
foreach (Affliction affliction in CharacterHealth.GetAllAfflictions())
{
skillLevel *= affliction.GetSkillMultiplier();
@@ -77,7 +77,7 @@ namespace Barotrauma
else if (Strength < ActiveThreshold)
{
DeactivateHusk();
if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: false })
if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true })
{
character.SpeechImpediment = 100;
}
@@ -131,7 +131,7 @@ namespace Barotrauma
character.NeedsAir = false;
}
if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: false })
if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true })
{
character.SpeechImpediment = 100;
}
@@ -74,6 +74,20 @@ namespace Barotrauma
private string rawAfflictionTypeString;
private string[] parsedAfflictionIdentifiers;
private string[] parsedAfflictionTypes;
public string[] ParsedAfflictionIdentifiers
{
get
{
return parsedAfflictionIdentifiers;
}
}
public string[] ParsedAfflictionTypes
{
get
{
return parsedAfflictionTypes;
}
}
public DamageModifier(XElement element, string parentDebugName)
{
@@ -167,9 +167,9 @@ namespace Barotrauma
}
}
public CharacterInfo GetCharacterInfo()
public CharacterInfo GetCharacterInfo(Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
var characterElement = ToolBox.SelectWeightedRandom(CustomNPCSets.Keys.ToList(), CustomNPCSets.Values.ToList(), Rand.RandSync.Unsynced);
var characterElement = ToolBox.SelectWeightedRandom(CustomNPCSets.Keys.ToList(), CustomNPCSets.Values.ToList(), randSync);
return characterElement != null ? new CharacterInfo(characterElement) : null;
}
@@ -526,7 +526,7 @@ namespace Barotrauma
[Serialize(false, true, description: "Does the character attack when provoked? When enabled, overrides the predefined targeting state with Attack and increases the priority of it."), Editable()]
public bool AttackWhenProvoked { get; private set; }
[Serialize(true, true, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable]
[Serialize(false, true, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable]
public bool AvoidGunfire { get; private set; }
[Serialize(3f, true, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)]
@@ -565,6 +565,9 @@ namespace Barotrauma
[Serialize(0f, true, description: ""), Editable]
public float AggressionCumulation { get; private set; }
[Serialize(WallTargetingMethod.Target, true, description: ""), Editable]
public WallTargetingMethod WallTargetingMethod { get; private set; }
public IEnumerable<TargetParams> Targets => targets;
protected readonly List<TargetParams> targets = new List<TargetParams>();