v0.14.6.0
This commit is contained in:
@@ -73,6 +73,8 @@ namespace Barotrauma
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public virtual bool IsMentallyUnstable => false;
|
||||
|
||||
private IEnumerable<Hull> visibleHulls;
|
||||
private float hullVisibilityTimer;
|
||||
const float hullVisibilityInterval = 0.5f;
|
||||
@@ -215,10 +217,26 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
private readonly HashSet<Item> unequippedItems = new HashSet<Item>();
|
||||
public bool TakeItem(Item item, Inventory targetInventory, bool equip, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false)
|
||||
public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false)
|
||||
{
|
||||
var pickable = item.GetComponent<Pickable>();
|
||||
if (pickable == null) { return false; }
|
||||
if (wear)
|
||||
{
|
||||
var wearable = item.GetComponent<Wearable>();
|
||||
if (wearable != null)
|
||||
{
|
||||
pickable = wearable;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var holdable = item.GetComponent<Holdable>();
|
||||
if (holdable != null)
|
||||
{
|
||||
pickable = holdable;
|
||||
}
|
||||
}
|
||||
if (item.ParentInventory is ItemInventory itemInventory)
|
||||
{
|
||||
if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; }
|
||||
@@ -302,7 +320,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (item != null && !item.Removed && Character.HasItem(item))
|
||||
{
|
||||
TakeItem(item, Character.Inventory, equip: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false);
|
||||
TakeItem(item, Character.Inventory, equip: true, wear: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false);
|
||||
}
|
||||
}
|
||||
unequippedItems.Clear();
|
||||
|
||||
@@ -144,21 +144,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
if (Static)
|
||||
{
|
||||
SightRange = MaxSightRange;
|
||||
SoundRange = MaxSoundRange;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-static ai targets must be kept alive by a custom logic (e.g. item components)
|
||||
SightRange = StaticSight ? MaxSightRange : MinSightRange;
|
||||
SoundRange = StaticSound ? MaxSoundRange : MinSoundRange;
|
||||
}
|
||||
}
|
||||
|
||||
public AITarget(Entity e, XElement element) : this(e)
|
||||
{
|
||||
SightRange = element.GetAttributeFloat("sightrange", 0.0f);
|
||||
@@ -242,5 +227,20 @@ namespace Barotrauma
|
||||
List.Remove(this);
|
||||
entity = null;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
if (Static)
|
||||
{
|
||||
SightRange = MaxSightRange;
|
||||
SoundRange = MaxSoundRange;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-static ai targets must be kept alive by a custom logic (e.g. item components)
|
||||
SightRange = StaticSight ? MaxSightRange : MinSightRange;
|
||||
SoundRange = StaticSound ? MaxSoundRange : MinSoundRange;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,23 +61,27 @@ 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;
|
||||
private float FleeHealthThreshold => AIParams.FleeHealthThreshold;
|
||||
private bool AggressiveBoarding => AIParams.AggressiveBoarding;
|
||||
private bool IsAggressiveBoarder => AIParams.AggressiveBoarding;
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,6 +396,7 @@ namespace Barotrauma
|
||||
SelectedAiTarget = target;
|
||||
selectedTargetMemory = GetTargetMemory(target, true);
|
||||
selectedTargetMemory.Priority = priority;
|
||||
ignoredTargets.Remove(target);
|
||||
}
|
||||
|
||||
private float movementMargin;
|
||||
@@ -479,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)
|
||||
{
|
||||
@@ -493,10 +505,14 @@ namespace Barotrauma
|
||||
selectedTargetingParams = targetingParams;
|
||||
State = targetingParams.State;
|
||||
}
|
||||
if (SelectedAiTarget?.Entity != null && !IsLatchedOnSub && State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive)
|
||||
{
|
||||
UpdateWallTarget(requiredHoleCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (AIParams.Infiltrate)
|
||||
if (AIParams.CanOpenDoors)
|
||||
{
|
||||
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
|
||||
|
||||
@@ -787,7 +803,7 @@ namespace Barotrauma
|
||||
if (pathSteering != null && !Character.AnimController.InWater)
|
||||
{
|
||||
// Wander around inside
|
||||
pathSteering.Wander(deltaTime, ConvertUnits.ToDisplayUnits(colliderLength), stayStillInTightSpace: false);
|
||||
pathSteering.Wander(deltaTime, Math.Max(ConvertUnits.ToDisplayUnits(colliderLength), 100.0f), stayStillInTightSpace: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1003,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;
|
||||
}
|
||||
@@ -1026,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;
|
||||
@@ -1050,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);
|
||||
@@ -1067,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
|
||||
@@ -1076,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;
|
||||
@@ -1098,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
|
||||
@@ -1126,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;
|
||||
}
|
||||
}
|
||||
@@ -1134,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);
|
||||
@@ -1148,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)
|
||||
@@ -1159,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
|
||||
@@ -1203,7 +1216,7 @@ namespace Barotrauma
|
||||
}
|
||||
canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0;
|
||||
}
|
||||
if (!AIParams.Infiltrate)
|
||||
if (!AIParams.CanOpenDoors)
|
||||
{
|
||||
if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls))
|
||||
{
|
||||
@@ -1257,8 +1270,8 @@ 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)
|
||||
if (wallTarget != null)
|
||||
// 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 && Character.Submarine == null)
|
||||
{
|
||||
if (wallTarget.Structure.Submarine != null)
|
||||
{
|
||||
@@ -1282,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
|
||||
@@ -1422,10 +1440,11 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
pathSteering.SteeringSeek(steerPos, 2, startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null), checkVisiblity: true);
|
||||
// Switch to Idle when cannot reach the target and if cannot damage the walls
|
||||
if ((!canAttackWalls || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
|
||||
if (!pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
|
||||
{
|
||||
State = AIState.Idle;
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
ResetAITarget();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1648,7 +1667,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);
|
||||
}
|
||||
@@ -1656,16 +1675,6 @@ namespace Barotrauma
|
||||
}
|
||||
if (canAttack)
|
||||
{
|
||||
if (SelectedAiTarget.Entity is Item targetItem)
|
||||
{
|
||||
var door = targetItem.GetComponent<Door>();
|
||||
if (door != null && door.CanBeTraversed)
|
||||
{
|
||||
ResetAITarget();
|
||||
State = PreviousState;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb))
|
||||
{
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
@@ -1777,6 +1786,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (!isFriendly && attackResult.Damage > 0.0f)
|
||||
{
|
||||
ignoredTargets.Remove(attacker.AiTarget);
|
||||
bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackWalls;
|
||||
if (AIParams.AttackWhenProvoked && canAttack)
|
||||
{
|
||||
@@ -1811,9 +1821,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);
|
||||
}
|
||||
}
|
||||
@@ -1893,16 +1902,28 @@ namespace Barotrauma
|
||||
{
|
||||
//simulate attack input to get the character to attack client-side
|
||||
Character.SetInput(InputType.Attack, true, true);
|
||||
#if SERVER
|
||||
GameMain.NetworkMember.CreateEntityEvent(Character, new object[]
|
||||
{
|
||||
Networking.NetEntityEvent.Type.SetAttackTarget,
|
||||
attackingLimb,
|
||||
(damageTarget as Entity)?.ID ?? Entity.NullEntityID,
|
||||
damageTarget is Character character && targetLimb != null ? Array.IndexOf(character.AnimController.Limbs, targetLimb) : 0,
|
||||
SimPosition.X,
|
||||
SimPosition.Y
|
||||
});
|
||||
#endif
|
||||
if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
|
||||
{
|
||||
if (damageTarget.Health > 0)
|
||||
if (damageTarget.Health > 0 && attackResult.Damage > 0)
|
||||
{
|
||||
// Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
|
||||
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * AIParams.AggressionGreed;
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedTargetMemory.Priority = 0;
|
||||
selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1);
|
||||
return selectedTargetMemory.Priority > 1;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -2138,6 +2159,10 @@ namespace Barotrauma
|
||||
{
|
||||
targetingTag = "weaker";
|
||||
}
|
||||
else
|
||||
{
|
||||
targetingTag = "equal";
|
||||
}
|
||||
if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee))
|
||||
{
|
||||
if (SelectedAiTarget == aiTarget)
|
||||
@@ -2184,7 +2209,7 @@ namespace Barotrauma
|
||||
bool targetingFromOutsideToInside = item.CurrentHull != null && character.CurrentHull == null;
|
||||
if (targetingFromOutsideToInside)
|
||||
{
|
||||
if (door != null && (!canAttackDoors && !AIParams.Infiltrate) || !canAttackWalls)
|
||||
if (door != null && (!canAttackDoors && !AIParams.CanOpenDoors) || !canAttackWalls)
|
||||
{
|
||||
// Can't reach
|
||||
continue;
|
||||
@@ -2258,25 +2283,24 @@ namespace Barotrauma
|
||||
var section = s.Sections[i];
|
||||
if (section.gap == null) { continue; }
|
||||
bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null;
|
||||
isInnerWall = isInnerWall || !leadsInside;
|
||||
if (Character.AnimController.CanEnterSubmarine)
|
||||
{
|
||||
if (!isCharacterInside)
|
||||
{
|
||||
if (CanPassThroughHole(s, i))
|
||||
{
|
||||
valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : 0;
|
||||
valueModifier *= leadsInside ? (IsAggressiveBoarder ? 3 : 1) : 0;
|
||||
}
|
||||
else if (AggressiveBoarding && leadsInside && canAttackWalls && AIParams.TargetOuterWalls)
|
||||
else if (IsAggressiveBoarder && leadsInside && canAttackWalls && AIParams.TargetOuterWalls)
|
||||
{
|
||||
// Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside
|
||||
valueModifier *= 1 + section.gap.Open;
|
||||
// Up to 25% priority increase for every gap in the wall when an aggressive boarder is outside
|
||||
valueModifier *= 1 + section.gap.Open * 0.25f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Inside
|
||||
if (AggressiveBoarding)
|
||||
if (IsAggressiveBoarder)
|
||||
{
|
||||
if (!isInnerWall)
|
||||
{
|
||||
@@ -2293,6 +2317,10 @@ namespace Barotrauma
|
||||
valueModifier = 0;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
valueModifier = 0.1f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2316,7 +2344,7 @@ namespace Barotrauma
|
||||
valueModifier = 0;
|
||||
break;
|
||||
}
|
||||
else if (AggressiveBoarding)
|
||||
else if (IsAggressiveBoarder)
|
||||
{
|
||||
// Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside
|
||||
// (Bonethreshers)
|
||||
@@ -2350,17 +2378,24 @@ namespace Barotrauma
|
||||
// Ignore broken and open doors, if cannot enter submarine
|
||||
continue;
|
||||
}
|
||||
if (AggressiveBoarding)
|
||||
if (IsAggressiveBoarder)
|
||||
{
|
||||
// Increase the priority if the character is outside and the door is from outside to inside
|
||||
if (character.CurrentHull == null)
|
||||
{
|
||||
valueModifier *= isOpen ? 5 : 1;
|
||||
// Increase the priority if the character is outside and the door is from outside to inside
|
||||
if (door.CanBeTraversed)
|
||||
{
|
||||
valueModifier = 3;
|
||||
}
|
||||
else if (door.LinkedGap != null)
|
||||
{
|
||||
valueModifier = 1 + door.LinkedGap.Open;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Inside -> ignore open doors and outer doors
|
||||
valueModifier *= isOpen || isOutdoor ? 0 : 1;
|
||||
valueModifier = isOpen || isOutdoor ? 0 : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2605,91 +2640,165 @@ 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.Infiltrate && 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; }
|
||||
if (HasValidPath(requireNonDirty: true)) { 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;
|
||||
}
|
||||
closestBody = body;
|
||||
closestDistance = distance;
|
||||
wall = closestBody.UserData as Structure;
|
||||
sectionPos = sectionPosition;
|
||||
sectionIndex = index;
|
||||
}
|
||||
Vector2 sectionPos = wall.SectionPosition(sectionIndex);
|
||||
Vector2 attachTargetNormal;
|
||||
if (wall.IsHorizontal)
|
||||
}
|
||||
if (closestBody == null || sectionIndex == -1) { return; }
|
||||
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 (wall.NoAITarget && Character.AnimController.CanEnterSubmarine)
|
||||
{
|
||||
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;
|
||||
bool isTargetingDoor = SelectedAiTarget.Entity is Item i && i.GetComponent<Door>() != null;
|
||||
// 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.
|
||||
if (!isTargetingDoor)
|
||||
{
|
||||
ResetAITarget();
|
||||
}
|
||||
}
|
||||
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;
|
||||
wallTarget = new WallTarget(sectionPos, wall, sectionIndex);
|
||||
}
|
||||
LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
|
||||
if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
|
||||
}
|
||||
else
|
||||
{
|
||||
// Blocked by a disabled wall.
|
||||
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)
|
||||
{
|
||||
if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner") || wall.Submarine != null && wall.Submarine == Character.Submarine)
|
||||
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))
|
||||
{
|
||||
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);
|
||||
}
|
||||
sectionIndex = i;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ignore and keep breaking other sections
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Character.AnimController.CanEnterSubmarine && wallTarget == null && selectedTargetingParams?.AttackPattern == AttackPattern.Straight)
|
||||
{
|
||||
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)
|
||||
if (wall.SectionDamage(i) > sectionDamage)
|
||||
{
|
||||
// Cannot reach the target, because it's blocked by a disabled wall or a door
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
ResetAITarget();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2698,7 +2807,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)
|
||||
|
||||
@@ -29,6 +29,9 @@ namespace Barotrauma
|
||||
private float flipTimer;
|
||||
private const float FlipInterval = 0.5f;
|
||||
|
||||
private float teamChangeTimer;
|
||||
private const float TeamChangeInterval = 0.5f;
|
||||
|
||||
public const float HULL_SAFETY_THRESHOLD = 40;
|
||||
public const float HULL_LOW_OXYGEN_PERCENTAGE = 30;
|
||||
|
||||
@@ -121,6 +124,33 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public MentalStateManager MentalStateManager { get; private set; }
|
||||
|
||||
public void InitMentalStateManager()
|
||||
{
|
||||
if (MentalStateManager == null)
|
||||
{
|
||||
MentalStateManager = new MentalStateManager(Character, this);
|
||||
}
|
||||
MentalStateManager.Active = true;
|
||||
}
|
||||
|
||||
public override bool IsMentallyUnstable =>
|
||||
MentalStateManager == null ? false :
|
||||
MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Normal &&
|
||||
MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Confused;
|
||||
|
||||
public ShipCommandManager ShipCommandManager { get; private set; }
|
||||
|
||||
public void InitShipCommandManager()
|
||||
{
|
||||
if (ShipCommandManager == null)
|
||||
{
|
||||
ShipCommandManager = new ShipCommandManager(Character);
|
||||
}
|
||||
ShipCommandManager.Active = true;
|
||||
}
|
||||
|
||||
public HumanAIController(Character c) : base(c)
|
||||
{
|
||||
if (!c.IsHuman)
|
||||
@@ -204,9 +234,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID))
|
||||
|
||||
if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted)
|
||||
{
|
||||
// Spot enemies while staying outside or inside an enemy ship.
|
||||
// does not apply for escorted characters, such as prisoners or terrorists who have their own behavior
|
||||
enemycheckTimer -= deltaTime;
|
||||
if (enemycheckTimer < 0)
|
||||
{
|
||||
@@ -287,6 +319,8 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
Character.UpdateTeam();
|
||||
|
||||
if (Character.CurrentHull != null)
|
||||
{
|
||||
if (Character.IsOnPlayerTeam)
|
||||
@@ -301,7 +335,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (Character.SpeechImpediment < 100.0f)
|
||||
{
|
||||
if (Character.Submarine != null && Character.Submarine.TeamID == Character.TeamID && !Character.Submarine.Info.IsWreck)
|
||||
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
|
||||
{
|
||||
ReportProblems();
|
||||
}
|
||||
@@ -314,7 +348,7 @@ namespace Barotrauma
|
||||
if (objectiveManager.CurrentObjective == null) { return; }
|
||||
|
||||
objectiveManager.DoCurrentObjective(deltaTime);
|
||||
bool run = objectiveManager.CurrentObjective.ForceRun || objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority;
|
||||
bool run = objectiveManager.CurrentObjective.ForceRun || !objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority;
|
||||
if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null)
|
||||
{
|
||||
if (Character.CurrentHull == null)
|
||||
@@ -395,6 +429,9 @@ namespace Barotrauma
|
||||
flipTimer = FlipInterval;
|
||||
}
|
||||
}
|
||||
|
||||
MentalStateManager?.Update(deltaTime);
|
||||
ShipCommandManager?.Update(deltaTime);
|
||||
}
|
||||
|
||||
private void UnequipUnnecessaryItems()
|
||||
@@ -442,9 +479,8 @@ namespace Barotrauma
|
||||
Character.AnimController.InWater ||
|
||||
Character.AnimController.HeadInWater ||
|
||||
Character.CurrentHull == null ||
|
||||
Character.Submarine?.TeamID != Character.TeamID ||
|
||||
(Character.Submarine?.TeamID != Character.TeamID && !Character.IsEscorted) || // these instances should maybe be combined to a method
|
||||
ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>() ||
|
||||
ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character || // wait order
|
||||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn);
|
||||
if (oxygenLow && Character.CurrentHull.Oxygen > 0)
|
||||
{
|
||||
@@ -454,7 +490,7 @@ namespace Barotrauma
|
||||
{
|
||||
shouldKeepTheGearOn = true;
|
||||
}
|
||||
bool removeDivingSuit = !shouldKeepTheGearOn;
|
||||
bool removeDivingSuit = !shouldKeepTheGearOn && Character.Submarine?.TeamID == Character.TeamID && (!(ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo) || goTo.Target != Character);
|
||||
bool takeMaskOff = !shouldKeepTheGearOn;
|
||||
if (!shouldKeepTheGearOn && !oxygenLow)
|
||||
{
|
||||
@@ -505,7 +541,7 @@ namespace Barotrauma
|
||||
var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR);
|
||||
if (divingSuit != null)
|
||||
{
|
||||
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
if (oxygenLow || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
{
|
||||
divingSuit.Drop(Character);
|
||||
HandleRelocation(divingSuit);
|
||||
@@ -550,7 +586,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
{
|
||||
if (ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
{
|
||||
mask.Drop(Character);
|
||||
HandleRelocation(mask);
|
||||
@@ -603,7 +639,7 @@ namespace Barotrauma
|
||||
Item item = Character.Inventory.GetItemInLimbSlot(hand);
|
||||
if (item == null) { continue; }
|
||||
|
||||
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }) && Character.Submarine?.TeamID == Character.TeamID )
|
||||
{
|
||||
findItemState = FindItemState.OtherItem;
|
||||
if (FindSuitableContainer(item, out Item targetContainer))
|
||||
@@ -705,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))
|
||||
{
|
||||
@@ -743,6 +780,7 @@ namespace Barotrauma
|
||||
{
|
||||
Order newOrder = null;
|
||||
Hull targetHull = null;
|
||||
bool speak = true;
|
||||
if (Character.CurrentHull != null)
|
||||
{
|
||||
bool isFighting = ObjectiveManager.HasActiveObjective<AIObjectiveCombat>();
|
||||
@@ -759,6 +797,21 @@ namespace Barotrauma
|
||||
var orderPrefab = Order.GetPrefab("reportintruders");
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
targetHull = hull;
|
||||
if (target.IsEscorted)
|
||||
{
|
||||
if (!Character.IsPrisoner && target.IsPrisoner)
|
||||
{
|
||||
string msg = TextManager.GetWithVariables("orderdialog.prisonerescaped", new string[] { "[roomname]" }, new string[] { targetHull.DisplayName }, new bool[] { false, true }, true);
|
||||
Character.Speak(msg, ChatMessageType.Order);
|
||||
speak = false;
|
||||
}
|
||||
else if (!IsMentallyUnstable && target.AIController.IsMentallyUnstable)
|
||||
{
|
||||
string msg = TextManager.GetWithVariables("orderdialog.mentalcase", new string[] { "[roomname]" }, new string[] { targetHull.DisplayName }, new bool[] { false, true }, true);
|
||||
Character.Speak(msg, ChatMessageType.Order);
|
||||
speak = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -771,7 +824,7 @@ namespace Barotrauma
|
||||
targetHull = hull;
|
||||
}
|
||||
}
|
||||
if (IsBallastFloraNoticeable(Character, hull))
|
||||
if (IsBallastFloraNoticeable(Character, hull) && newOrder == null)
|
||||
{
|
||||
var orderPrefab = Order.GetPrefab("reportballastflora");
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
@@ -824,20 +877,24 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newOrder != null)
|
||||
if (newOrder != null && speak)
|
||||
{
|
||||
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
// for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented
|
||||
if (!Character.IsEscorted)
|
||||
{
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Default,
|
||||
identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"),
|
||||
minDurationBetweenSimilar: 60.0f);
|
||||
}
|
||||
else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
|
||||
{
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order);
|
||||
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Default,
|
||||
identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"),
|
||||
minDurationBetweenSimilar: 60.0f);
|
||||
}
|
||||
else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
|
||||
{
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order);
|
||||
#if SERVER
|
||||
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", CharacterInfo.HighestManualOrderPriority, targetHull, null, Character));
|
||||
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", CharacterInfo.HighestManualOrderPriority, targetHull, null, Character));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -977,11 +1034,11 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
float cumulativeDamage = GetDamageDoneByAttacker(attacker);
|
||||
if (!Character.IsSecurity && attacker.IsBot && 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
|
||||
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
|
||||
}
|
||||
}
|
||||
@@ -1039,8 +1096,11 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-friendly
|
||||
InformOtherNPCs(GetDamageDoneByAttacker(attacker));
|
||||
if (Character.Submarine != null && Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
|
||||
{
|
||||
// Non-friendly
|
||||
InformOtherNPCs(GetDamageDoneByAttacker(attacker));
|
||||
}
|
||||
if (Character.IsBot)
|
||||
{
|
||||
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker);
|
||||
@@ -1051,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; }
|
||||
@@ -1070,12 +1130,27 @@ namespace Barotrauma
|
||||
{
|
||||
if (!IsFriendly(attacker))
|
||||
{
|
||||
return c.AIController is HumanAIController humanAI &&
|
||||
if (Character.Submarine == null)
|
||||
{
|
||||
// Outside -> don't react.
|
||||
return AIObjectiveCombat.CombatMode.None;
|
||||
}
|
||||
if (!Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
|
||||
{
|
||||
// Attacked from an unconnected submarine.
|
||||
return Character.SelectedConstruction?.GetComponent<Turret>() != null ? AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat;
|
||||
}
|
||||
return c.AIController is HumanAIController humanAI &&
|
||||
(humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders))
|
||||
? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Character.Submarine == null || !Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
|
||||
{
|
||||
// Outside or attacked from an unconnected submarine -> don't react.
|
||||
return AIObjectiveCombat.CombatMode.None;
|
||||
}
|
||||
// If there are any enemies around, just ignore the friendly fire
|
||||
if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsDead && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull)))
|
||||
{
|
||||
@@ -1090,7 +1165,7 @@ namespace Barotrauma
|
||||
// The guards don't react when the player attacks instigators.
|
||||
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
|
||||
}
|
||||
else if (attacker.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
else if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable))
|
||||
{
|
||||
if (c.IsSecurity)
|
||||
{
|
||||
@@ -1132,7 +1207,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func<bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false)
|
||||
public void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func<AIObjective, bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false)
|
||||
{
|
||||
if (mode == AIObjectiveCombat.CombatMode.None) { return; }
|
||||
if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; }
|
||||
@@ -1168,7 +1243,7 @@ namespace Barotrauma
|
||||
Character.Info?.Job?.Prefab.Identifier == "watchman" ||
|
||||
Character.CurrentHull == null ||
|
||||
Character.IsOnPlayerTeam && !target.IsPlayer && ObjectiveManager.GetActiveObjective<AIObjectiveGoTo>()?.Target is Character followTarget && followTarget.IsPlayer,
|
||||
abortCondition = abortCondition,
|
||||
AbortCondition = abortCondition,
|
||||
allowHoldFire = allowHoldFire,
|
||||
};
|
||||
if (onAbort != null)
|
||||
@@ -1190,7 +1265,7 @@ namespace Barotrauma
|
||||
|
||||
public void SetForcedOrder(Order order, string option, Character orderGiver)
|
||||
{
|
||||
var objective = ObjectiveManager.CreateObjective(order, option, orderGiver, false);
|
||||
var objective = ObjectiveManager.CreateObjective(order, option, orderGiver);
|
||||
ObjectiveManager.SetForcedOrder(objective);
|
||||
}
|
||||
|
||||
@@ -1273,7 +1348,8 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Check whether the character has a diving suit in usable condition plus some oxygen.
|
||||
/// </summary>
|
||||
public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true);
|
||||
public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true,
|
||||
predicate: (Item item) => { return character.HasEquippedItem(item, InvSlotType.OuterClothes); });
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the character has a diving mask in usable condition plus some oxygen.
|
||||
@@ -1401,7 +1477,9 @@ namespace Barotrauma
|
||||
Character thief = character;
|
||||
bool someoneSpoke = false;
|
||||
|
||||
if (item.SpawnedInOutpost && !item.AllowStealing && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker"))
|
||||
bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInOutpost && !it.AllowStealing, recursive: true).Any() ?? false;
|
||||
|
||||
if ((item.SpawnedInOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker"))
|
||||
{
|
||||
foreach (Character otherCharacter in Character.CharacterList)
|
||||
{
|
||||
@@ -1464,7 +1542,7 @@ namespace Barotrauma
|
||||
if (!humanAI.Character.IsSecurity) { return false; }
|
||||
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
|
||||
humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, thief, delay: GetReactionTime(),
|
||||
abortCondition: () => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null,
|
||||
abortCondition: obj => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null,
|
||||
onAbort: () =>
|
||||
{
|
||||
if (item != null && !item.Removed && humanAI != null && !humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveGetItem>())
|
||||
@@ -1845,18 +1923,52 @@ 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)
|
||||
float highestPriority = -1.0f;
|
||||
float highestPriorityModifier = -1.0f;
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c == character) { continue; }
|
||||
if (c.IsDead || c.IsIncapacitated) { continue; }
|
||||
if (!IsFriendly(character, c, onlySameTeam: true)) { continue; }
|
||||
operatingCharacter = c;
|
||||
if (c.Removed) { continue; }
|
||||
if (c.TeamID != team) { continue; }
|
||||
if (c.IsIncapacitated) { continue; }
|
||||
if (c.SelectedConstruction == target.Item)
|
||||
{
|
||||
operatingCharacter = c;
|
||||
return true;
|
||||
}
|
||||
if (c.AIController is HumanAIController humanAI)
|
||||
{
|
||||
foreach (var objective in humanAI.ObjectiveManager.Objectives)
|
||||
{
|
||||
if (!(objective is AIObjectiveOperateItem operateObjective)) { continue; }
|
||||
if (operateObjective.Component.Item != target.Item) { continue; }
|
||||
if (operateObjective.Priority < highestPriority) { continue; }
|
||||
if (operateObjective.PriorityModifier < highestPriorityModifier) { continue; }
|
||||
operatingCharacter = c;
|
||||
highestPriority = operateObjective.Priority;
|
||||
highestPriorityModifier = operateObjective.PriorityModifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
return operatingCharacter != null;
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -1887,7 +1999,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;
|
||||
@@ -1895,12 +2007,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;
|
||||
}
|
||||
@@ -1909,7 +2021,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
|
||||
@@ -1918,7 +2088,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,34 +301,33 @@ namespace Barotrauma
|
||||
}
|
||||
Ladder nextLadder = GetNextLadder();
|
||||
var ladders = currentLadder ?? nextLadder;
|
||||
if (canClimb && !isDiving && ladders != null && character.SelectedConstruction != ladders.Item)
|
||||
bool useLadders = canClimb && ladders != null && (!isDiving || Math.Abs(steering.X) < 0.1f && Math.Abs(steering.Y) > 1);
|
||||
if (useLadders && character.SelectedConstruction != ladders.Item)
|
||||
{
|
||||
if (IsNextNodeLadder || currentPath.Finished)
|
||||
{
|
||||
if (character.CanInteractWith(ladders.Item))
|
||||
{
|
||||
ladders.Item.TryInteract(character, false, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cannot interact with the current (or next) ladder,
|
||||
// Try to select the previous ladder, unless it's already selected, unless the previous ladder is not adjacent to the current ladder.
|
||||
// The intention of this code is to prevent the bots from dropping from the "double ladders".
|
||||
var previousLadders = currentPath.PrevNode?.Ladders;
|
||||
if (previousLadders != null && previousLadders != ladders && character.SelectedConstruction != previousLadders.Item &&
|
||||
character.CanInteractWith(previousLadders.Item) && Math.Abs(previousLadders.Item.WorldPosition.X - ladders.Item.WorldPosition.X) < 5)
|
||||
{
|
||||
previousLadders.Item.TryInteract(character, false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!IsNextLadderSameAsCurrent && character.SelectedConstruction?.GetComponent<Ladder>() != null && character.CanInteractWith(ladders.Item))
|
||||
if (character.CanInteractWith(ladders.Item))
|
||||
{
|
||||
ladders.Item.TryInteract(character, false, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cannot interact with the current (or next) ladder,
|
||||
// Try to select the previous ladder, unless it's already selected, unless the previous ladder is not adjacent to the current ladder.
|
||||
// The intention of this code is to prevent the bots from dropping from the "double ladders".
|
||||
var previousLadders = currentPath.PrevNode?.Ladders;
|
||||
if (previousLadders != null && previousLadders != ladders && character.SelectedConstruction != previousLadders.Item &&
|
||||
character.CanInteractWith(previousLadders.Item) && Math.Abs(previousLadders.Item.WorldPosition.X - ladders.Item.WorldPosition.X) < 5)
|
||||
{
|
||||
previousLadders.Item.TryInteract(character, false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
var collider = character.AnimController.Collider;
|
||||
if (character.IsClimbing && !isDiving)
|
||||
if (character.IsClimbing && !useLadders)
|
||||
{
|
||||
character.AnimController.Anim = AnimController.Animation.None;
|
||||
character.SelectedConstruction = null;
|
||||
}
|
||||
if (character.IsClimbing && useLadders)
|
||||
{
|
||||
Vector2 diff = currentPath.CurrentNode.SimPosition - pos;
|
||||
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
|
||||
@@ -380,17 +379,12 @@ namespace Barotrauma
|
||||
}
|
||||
else if (character.AnimController.InWater)
|
||||
{
|
||||
// If the character is underwater, we don't need the ladders anymore
|
||||
if (character.IsClimbing && isDiving)
|
||||
{
|
||||
character.AnimController.Anim = AnimController.Animation.None;
|
||||
character.SelectedConstruction = null;
|
||||
}
|
||||
var door = currentPath.CurrentNode.ConnectedDoor;
|
||||
if (door == null || door.CanBeTraversed)
|
||||
{
|
||||
float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
|
||||
float targetDistance = collider.GetSize().X * multiplier;
|
||||
float margin = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
|
||||
Vector2 colliderSize = collider.GetSize();
|
||||
float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * margin, 0.5f);
|
||||
float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
|
||||
float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
|
||||
if (character.CurrentHull != currentPath.CurrentNode.CurrentHull)
|
||||
@@ -404,24 +398,25 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!canClimb || !IsNextLadderSameAsCurrent)
|
||||
else
|
||||
{
|
||||
// Walking horizontally
|
||||
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
|
||||
Vector2 colliderSize = collider.GetSize();
|
||||
Vector2 velocity = collider.LinearVelocity;
|
||||
// If the character is smaller than this, it would fail to use the waypoint nodes because they are always too high.
|
||||
float minHeight = 1;
|
||||
// If the character is very thin, without a min value, it would often fail to reach the waypoints, because the horizontal distance is too small.
|
||||
float minWidth = 0.17f;
|
||||
// If the character is very short, it would fail to use the waypoint nodes because they are always too high.
|
||||
// If the character is very thin, it would often fail to reach the waypoints, because the horizontal distance is too small.
|
||||
// Both values are based on the human size. So basically anything smaller than humans are considered as equal in size.
|
||||
float minHeight = 1.6125001f;
|
||||
float minWidth = 0.3225f;
|
||||
// Cannot use the head position, because not all characters have head or it can be below the total height of the character
|
||||
float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight);
|
||||
float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X);
|
||||
bool isAboveFeet = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y;
|
||||
bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + characterHeight;
|
||||
var door = currentPath.CurrentNode.ConnectedDoor;
|
||||
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 10, 0, 1));
|
||||
float targetDistance = Math.Max(collider.radius * margin, minWidth);
|
||||
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
|
||||
float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
|
||||
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && (door == null || door.CanBeTraversed))
|
||||
{
|
||||
currentPath.SkipToNextNode();
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
using Barotrauma.Extensions;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class MentalStateManager
|
||||
{
|
||||
private float mentalStateTimer;
|
||||
private const float MentalStateInterval = 7.5f;
|
||||
|
||||
private float mentalBehaviorTimer;
|
||||
private const float MentalBehaviorInterval = 7.5f;
|
||||
|
||||
private readonly Character character;
|
||||
private readonly HumanAIController humanAIController;
|
||||
|
||||
public bool Active { get; set; }
|
||||
public MentalType CurrentMentalType { get; private set; }
|
||||
public enum MentalType
|
||||
{
|
||||
Normal,
|
||||
Confused, // No effects other than special dialogue
|
||||
Afraid, // Will retreat from whoever is nearby
|
||||
Desperate, // Will defensively attack/arrest whoever is nearby
|
||||
Berserk // turns fully hostile using team change logic
|
||||
}
|
||||
|
||||
private const string MentalTeamChange = "mental";
|
||||
|
||||
public MentalStateManager(Character character, HumanAIController humanAIController)
|
||||
{
|
||||
this.character = character;
|
||||
this.humanAIController = humanAIController;
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
if (!Active) { return; }
|
||||
mentalStateTimer -= deltaTime;
|
||||
if (mentalStateTimer <= 0.0f)
|
||||
{
|
||||
UpdateMentalState();
|
||||
mentalStateTimer = MentalStateInterval * Rand.Range(0.75f, 1.25f);
|
||||
}
|
||||
|
||||
mentalBehaviorTimer = Math.Max(0f, mentalBehaviorTimer - deltaTime);
|
||||
}
|
||||
|
||||
private void UpdateMentalState()
|
||||
{
|
||||
MentalType newMentalType = GetMentalType(character.CharacterHealth.GetAffliction("psychosis"));
|
||||
bool createdCombat = false;
|
||||
|
||||
switch (newMentalType)
|
||||
{
|
||||
case MentalType.Normal:
|
||||
case MentalType.Confused:
|
||||
// remove combat if we became normal again
|
||||
mentalBehaviorTimer = 0f;
|
||||
break;
|
||||
case MentalType.Afraid:
|
||||
case MentalType.Desperate:
|
||||
case MentalType.Berserk:
|
||||
// berserk is not removed unless we drop to normal behavior again
|
||||
if (CurrentMentalType == MentalType.Berserk)
|
||||
{
|
||||
newMentalType = MentalType.Berserk;
|
||||
}
|
||||
// give players a full interval to react to mental changes
|
||||
if (newMentalType == CurrentMentalType)
|
||||
{
|
||||
createdCombat = CreateCombatBehavior(CurrentMentalType);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!createdCombat)
|
||||
{
|
||||
CreateDialogueBehavior(newMentalType);
|
||||
}
|
||||
|
||||
if (newMentalType != MentalType.Berserk)
|
||||
{
|
||||
character.TryRemoveTeamChange(MentalTeamChange);
|
||||
}
|
||||
|
||||
CurrentMentalType = newMentalType;
|
||||
}
|
||||
|
||||
private int mentalTypeCount;
|
||||
private int MentalTypeCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (mentalTypeCount == 0)
|
||||
{
|
||||
mentalTypeCount = Enum.GetNames(typeof(MentalType)).Length;
|
||||
}
|
||||
return mentalTypeCount;
|
||||
}
|
||||
}
|
||||
|
||||
private MentalType GetMentalType(Affliction affliction)
|
||||
{
|
||||
if (affliction == null)
|
||||
{
|
||||
return MentalType.Normal;
|
||||
}
|
||||
// test this later
|
||||
int psychosisIndex = (int)(affliction.Strength / (affliction.Prefab.MaxStrength / MentalTypeCount) * Rand.Range(1f, 1.2f));
|
||||
psychosisIndex = Math.Clamp(psychosisIndex, 0, 4);
|
||||
MentalType mentalType = psychosisIndex switch
|
||||
{
|
||||
0 => MentalType.Normal,
|
||||
1 => MentalType.Confused,
|
||||
2 => MentalType.Afraid,
|
||||
3 => MentalType.Desperate,
|
||||
4 => MentalType.Berserk,
|
||||
_ => throw new ArgumentOutOfRangeException(psychosisIndex.ToString()),
|
||||
};
|
||||
return mentalType;
|
||||
}
|
||||
|
||||
public bool CreateCombatBehavior(MentalType mentalType)
|
||||
{
|
||||
Character mentalAttackTarget = Character.CharacterList.Where(
|
||||
possibleTarget => HumanAIController.IsActive(possibleTarget) &&
|
||||
(possibleTarget.TeamID != character.TeamID || mentalType == MentalType.Berserk) &&
|
||||
humanAIController.VisibleHulls.Contains(possibleTarget.CurrentHull) &&
|
||||
possibleTarget != character).GetRandom();
|
||||
|
||||
if (mentalAttackTarget == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var combatMode = AIObjectiveCombat.CombatMode.None;
|
||||
bool holdFire = mentalType == MentalType.Afraid && character.IsSecurity;
|
||||
switch (mentalType)
|
||||
{
|
||||
case MentalType.Afraid:
|
||||
combatMode = character.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
|
||||
break;
|
||||
case MentalType.Desperate:
|
||||
// might be unnecessary to explicitly declare as arrest against non-humans
|
||||
combatMode = character.IsSecurity && mentalAttackTarget.IsHuman ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Defensive;
|
||||
break;
|
||||
case MentalType.Berserk:
|
||||
combatMode = AIObjectiveCombat.CombatMode.Offensive;
|
||||
break;
|
||||
}
|
||||
|
||||
// using this as an explicit time-out for the behavior. it's possible it will never run out because of the manager being disabled, but combat objective has failsafes for that
|
||||
mentalBehaviorTimer = MentalBehaviorInterval;
|
||||
humanAIController.AddCombatObjective(combatMode, mentalAttackTarget, allowHoldFire: holdFire, abortCondition: obj => mentalBehaviorTimer <= 0f);
|
||||
string textIdentifier = $"dialogmentalstatereaction{combatMode.ToString().ToLowerInvariant()}";
|
||||
character.Speak(TextManager.Get(textIdentifier), delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 25f);
|
||||
|
||||
if (mentalType == MentalType.Berserk && !character.HasTeamChange(MentalTeamChange))
|
||||
{
|
||||
// TODO: could this be handled in the switch block above?
|
||||
character.TryAddNewTeamChange(MentalTeamChange, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Absolute, aggressiveBehavior: true));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void CreateDialogueBehavior(MentalType mentalType)
|
||||
{
|
||||
if (mentalType == MentalType.Normal) { return; }
|
||||
string textIdentifier = $"dialogmentalstate{mentalType.ToString().ToLowerInvariant()}";
|
||||
character.Speak(TextManager.Get(textIdentifier), delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 35f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,10 @@ namespace Barotrauma
|
||||
public readonly List<NPCConversation> Responses;
|
||||
private readonly int speakerIndex;
|
||||
private readonly List<string> allowedSpeakerTags;
|
||||
private readonly bool requireNextLine;
|
||||
// used primarily for team1 characters interacting with escorted personnel (TODO: not used anywhere)
|
||||
private readonly bool requireSight;
|
||||
|
||||
public static void LoadAll(IEnumerable<ContentFile> files)
|
||||
{
|
||||
foreach (var file in files)
|
||||
@@ -161,6 +165,8 @@ namespace Barotrauma
|
||||
{
|
||||
Responses.Add(new NPCConversation(subElement, filePath));
|
||||
}
|
||||
requireNextLine = element.GetAttributeBool("requirenextline", false);
|
||||
requireSight = element.GetAttributeBool("requiresight", false);
|
||||
}
|
||||
|
||||
private static List<string> GetCurrentFlags(Character speaker)
|
||||
@@ -211,7 +217,7 @@ namespace Barotrauma
|
||||
var afflictions = speaker.CharacterHealth.GetAllAfflictions();
|
||||
foreach (Affliction affliction in afflictions)
|
||||
{
|
||||
var currentEffect = affliction.Prefab.GetActiveEffect(affliction.Strength);
|
||||
var currentEffect = affliction.GetActiveEffect();
|
||||
if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag) && !currentFlags.Contains(currentEffect.DialogFlag))
|
||||
{
|
||||
currentFlags.Add(currentEffect.DialogFlag);
|
||||
@@ -226,7 +232,6 @@ namespace Barotrauma
|
||||
{
|
||||
currentFlags.Add("CampaignNPC." + speaker.CampaignInteractionType);
|
||||
}
|
||||
|
||||
if (GameMain.GameSession?.GameMode is CampaignMode campaignMode &&
|
||||
(campaignMode.Map?.CurrentLocation?.Type?.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase) ?? false))
|
||||
{
|
||||
@@ -239,6 +244,10 @@ namespace Barotrauma
|
||||
currentFlags.Add("Hostage");
|
||||
}
|
||||
}
|
||||
if (speaker.IsEscorted)
|
||||
{
|
||||
currentFlags.Add("escort");
|
||||
}
|
||||
}
|
||||
|
||||
return currentFlags;
|
||||
@@ -325,43 +334,15 @@ namespace Barotrauma
|
||||
|
||||
foreach (Character potentialSpeaker in availableSpeakers)
|
||||
{
|
||||
//check if the character has an appropriate job to say the line
|
||||
if ((potentialSpeaker.Info?.Job != null && potentialSpeaker.Info.Job.Prefab.OnlyJobSpecificDialog) ||
|
||||
selectedConversation.AllowedJobs.Count > 0)
|
||||
if (CheckSpeakerViability(potentialSpeaker, selectedConversation, assignedSpeakers.Values.ToList(), ignoreFlags))
|
||||
{
|
||||
if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) { continue; }
|
||||
allowedSpeakers.Add(potentialSpeaker);
|
||||
}
|
||||
|
||||
//check if the character has all required flags to say the line
|
||||
if (!ignoreFlags)
|
||||
{
|
||||
var characterFlags = GetCurrentFlags(potentialSpeaker);
|
||||
if (!selectedConversation.Flags.All(flag => characterFlags.Contains(flag))) { continue; }
|
||||
}
|
||||
|
||||
//check if the character is close enough to hear the rest of the speakers
|
||||
if (assignedSpeakers.Values.Any(s => !potentialSpeaker.CanHearCharacter(s))) { continue; }
|
||||
|
||||
//check if the character has an appropriate personality
|
||||
if (selectedConversation.allowedSpeakerTags.Count > 0)
|
||||
{
|
||||
if (potentialSpeaker.Info?.PersonalityTrait == null) { continue; }
|
||||
if (!selectedConversation.allowedSpeakerTags.Any(t => potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Any(t2 => t2 == t))) { continue; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (potentialSpeaker.Info?.PersonalityTrait != null &&
|
||||
!potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Contains("none"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
allowedSpeakers.Add(potentialSpeaker);
|
||||
}
|
||||
|
||||
if (allowedSpeakers.Count == 0)
|
||||
if (allowedSpeakers.Count == 0 || NextLineFailure(selectedConversation, availableSpeakers, allowedSpeakers, ignoreFlags))
|
||||
{
|
||||
allowedSpeakers.Clear();
|
||||
potentialLines.Remove(selectedConversation);
|
||||
}
|
||||
else
|
||||
@@ -385,6 +366,62 @@ namespace Barotrauma
|
||||
CreateConversation(availableSpeakers, assignedSpeakers, selectedConversation, lineList, availableConversations);
|
||||
}
|
||||
|
||||
static bool NextLineFailure(NPCConversation selectedConversation, List<Character> availableSpeakers, List<Character> allowedSpeakers, bool ignoreFlags)
|
||||
{
|
||||
if (selectedConversation.requireNextLine)
|
||||
{
|
||||
foreach (NPCConversation nextConversation in selectedConversation.Responses)
|
||||
{
|
||||
foreach (Character potentialNextSpeaker in availableSpeakers)
|
||||
{
|
||||
if (CheckSpeakerViability(potentialNextSpeaker, nextConversation, allowedSpeakers, ignoreFlags))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool CheckSpeakerViability(Character potentialSpeaker, NPCConversation selectedConversation, List<Character> checkedSpeakers, bool ignoreFlags)
|
||||
{
|
||||
//check if the character has an appropriate job to say the line
|
||||
if ((potentialSpeaker.Info?.Job != null && potentialSpeaker.Info.Job.Prefab.OnlyJobSpecificDialog) || selectedConversation.AllowedJobs.Count > 0)
|
||||
{
|
||||
if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) { return false; }
|
||||
}
|
||||
|
||||
//check if the character has all required flags to say the line
|
||||
if (!ignoreFlags)
|
||||
{
|
||||
var characterFlags = GetCurrentFlags(potentialSpeaker);
|
||||
if (!selectedConversation.Flags.All(flag => characterFlags.Contains(flag))) { return false; }
|
||||
}
|
||||
|
||||
//check if the character is close enough to hear the rest of the speakers
|
||||
if (checkedSpeakers.Any(s => !potentialSpeaker.CanHearCharacter(s))) { return false; }
|
||||
|
||||
//check if the character is close enough to see the rest of the speakers (this should be replaced with a more performant method)
|
||||
if (checkedSpeakers.Any(s => !potentialSpeaker.CanSeeCharacter(s))) { return false; }
|
||||
|
||||
//check if the character has an appropriate personality
|
||||
if (selectedConversation.allowedSpeakerTags.Count > 0)
|
||||
{
|
||||
if (potentialSpeaker.Info?.PersonalityTrait == null) { return false; }
|
||||
if (!selectedConversation.allowedSpeakerTags.Any(t => potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Any(t2 => t2 == t))) { return false; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (potentialSpeaker.Info?.PersonalityTrait != null &&
|
||||
!potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Contains("none"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static NPCConversation GetRandomConversation(List<NPCConversation> conversations, bool avoidPreviouslyUsed)
|
||||
{
|
||||
if (!avoidPreviouslyUsed)
|
||||
|
||||
@@ -6,16 +6,18 @@ using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
abstract class AIObjective
|
||||
abstract partial class AIObjective
|
||||
{
|
||||
public virtual float Devotion => AIObjectiveManager.baseDevotion;
|
||||
|
||||
public abstract string DebugTag { get; }
|
||||
public abstract string Identifier { get; set; }
|
||||
public virtual string DebugTag => Identifier;
|
||||
public virtual bool ForceRun => false;
|
||||
public virtual bool IgnoreUnsafeHulls => false;
|
||||
public virtual bool AbandonWhenCannotCompleteSubjectives => true;
|
||||
public virtual bool AllowSubObjectiveSorting => false;
|
||||
public virtual bool ForceOrderPriority => true;
|
||||
public virtual bool PrioritizeIfSubObjectivesActive => false;
|
||||
|
||||
/// <summary>
|
||||
/// Can there be multiple objective instaces of the same type?
|
||||
@@ -52,8 +54,32 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public float Priority { get; set; }
|
||||
public float BasePriority { get; set; }
|
||||
|
||||
public float PriorityModifier { get; private set; } = 1;
|
||||
|
||||
private float resetPriorityTimer;
|
||||
private readonly float resetPriorityTime = 1;
|
||||
private bool _forceHighestPriority;
|
||||
// For forcing the highest priority temporarily. Will reset automatically after one second, unless kept alive by something.
|
||||
public bool ForceHighestPriority
|
||||
{
|
||||
get { return _forceHighestPriority; }
|
||||
set
|
||||
{
|
||||
if (_forceHighestPriority == value) { return; }
|
||||
_forceHighestPriority = value;
|
||||
if (_forceHighestPriority)
|
||||
{
|
||||
resetPriorityTimer = resetPriorityTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For temporarily forcing walking. Will reset after each priority calculation, so it will need to be kept alive by something.
|
||||
// The intention of this boolean to allow walking even when the priority is higher than AIObjectiveManager.RunPriority.
|
||||
public bool ForceWalk { get; set; }
|
||||
|
||||
public bool IgnoreAtOutpost { get; set; }
|
||||
|
||||
public readonly Character character;
|
||||
public readonly AIObjectiveManager objectiveManager;
|
||||
public string Option { get; private set; }
|
||||
@@ -102,6 +128,13 @@ namespace Barotrauma
|
||||
return all;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0649
|
||||
/// <summary>
|
||||
/// Aborts the objective when this condition is true.
|
||||
/// </summary>
|
||||
public Func<AIObjective, bool> AbortCondition;
|
||||
#pragma warning restore CS0649
|
||||
|
||||
/// <summary>
|
||||
/// A single shot event. Automatically cleared after launching. Use OnCompleted method for implementing (internal) persistent behavior.
|
||||
/// </summary>
|
||||
@@ -153,7 +186,6 @@ namespace Barotrauma
|
||||
Act(deltaTime);
|
||||
}
|
||||
|
||||
// TODO: check turret aioperate
|
||||
public void AddSubObjective(AIObjective objective, bool addFirst = false)
|
||||
{
|
||||
var type = objective.GetType();
|
||||
@@ -217,18 +249,22 @@ namespace Barotrauma
|
||||
protected bool IsAllowed
|
||||
{
|
||||
get
|
||||
{
|
||||
{
|
||||
if (IgnoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!AllowOutsideSubmarine && character.Submarine == null) { return false; }
|
||||
if (AllowInAnySub) { return true; }
|
||||
if (AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) { return true; }
|
||||
if ((AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) || character.IsEscorted) { return true; }
|
||||
return character.Submarine.TeamID == character.TeamID || character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call this only when the priority needs to be recalculated. Use the cached Priority property when you don't need to recalculate.
|
||||
/// </summary>
|
||||
public virtual float GetPriority()
|
||||
protected virtual float GetPriority()
|
||||
{
|
||||
bool isOrder = objectiveManager.IsOrder(this);
|
||||
if (!IsAllowed)
|
||||
@@ -248,6 +284,17 @@ namespace Barotrauma
|
||||
return Priority;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call this only when the priority needs to be recalculated. Use the cached Priority property when you don't need to recalculate.
|
||||
/// </summary>
|
||||
public float CalculatePriority()
|
||||
{
|
||||
Priority = GetPriority();
|
||||
ForceHighestPriority = false;
|
||||
ForceWalk = false;
|
||||
return Priority;
|
||||
}
|
||||
|
||||
private void UpdateDevotion(float deltaTime)
|
||||
{
|
||||
var currentObjective = objectiveManager.CurrentObjective;
|
||||
@@ -261,6 +308,14 @@ namespace Barotrauma
|
||||
|
||||
public virtual void Update(float deltaTime)
|
||||
{
|
||||
if (resetPriorityTimer > 0)
|
||||
{
|
||||
resetPriorityTimer -= deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
ForceHighestPriority = false;
|
||||
}
|
||||
if (!objectiveManager.IsOrder(this) && objectiveManager.WaitTimer <= 0)
|
||||
{
|
||||
UpdateDevotion(deltaTime);
|
||||
@@ -393,7 +448,17 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract bool Check();
|
||||
protected virtual bool Check()
|
||||
{
|
||||
if (AbortCondition != null && AbortCondition(this))
|
||||
{
|
||||
Abandon = true;
|
||||
return false;
|
||||
}
|
||||
return CheckObjectiveSpecific();
|
||||
}
|
||||
|
||||
protected abstract bool CheckObjectiveSpecific();
|
||||
|
||||
private bool CheckState()
|
||||
{
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveChargeBatteries : AIObjectiveLoop<PowerContainer>
|
||||
{
|
||||
public override string DebugTag => "charge batteries";
|
||||
public override string Identifier { get; set; } = "charge batteries";
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
private IEnumerable<PowerContainer> batteryList;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
+12
-12
@@ -9,7 +9,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveCleanupItem : AIObjective
|
||||
{
|
||||
public override string DebugTag => "cleanup item";
|
||||
public override string Identifier { get; set; } = "cleanup item";
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AllowAutomaticItemUnequipping => false;
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Barotrauma
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
@@ -61,14 +61,14 @@ namespace Barotrauma
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (item.IgnoreByAI)
|
||||
if (item.IgnoreByAI(character))
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (item.ParentInventory != null)
|
||||
{
|
||||
if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrders()))
|
||||
if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrder<AIObjectiveCleanupItems>()))
|
||||
{
|
||||
// Target was picked up or moved by someone.
|
||||
Abandon = true;
|
||||
@@ -82,14 +82,14 @@ namespace Barotrauma
|
||||
itemIndex = 0;
|
||||
if (suitableContainer != null)
|
||||
{
|
||||
bool equip = item.HasTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR) || (
|
||||
item.GetComponent<Wearable>() == null &&
|
||||
bool equip = item.GetComponent<Holdable>() != null ||
|
||||
item.AllowedSlots.None(s =>
|
||||
s == InvSlotType.Card ||
|
||||
s == InvSlotType.Head ||
|
||||
s == InvSlotType.Headset ||
|
||||
s == InvSlotType.InnerClothes ||
|
||||
s == InvSlotType.OuterClothes));
|
||||
s == InvSlotType.Card ||
|
||||
s == InvSlotType.Head ||
|
||||
s == InvSlotType.Headset ||
|
||||
s == InvSlotType.InnerClothes ||
|
||||
s == InvSlotType.OuterClothes);
|
||||
|
||||
TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
|
||||
{
|
||||
Equip = equip,
|
||||
@@ -131,7 +131,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Check() => IsCompleted;
|
||||
protected override bool CheckObjectiveSpecific() => IsCompleted;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
|
||||
+18
-7
@@ -8,7 +8,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveCleanupItems : AIObjectiveLoop<Item>
|
||||
{
|
||||
public override string DebugTag => "cleanup items";
|
||||
public override string Identifier { get; set; } = "cleanup items";
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AllowAutomaticItemUnequipping => false;
|
||||
public override bool ForceOrderPriority => false;
|
||||
@@ -38,8 +38,8 @@ namespace Barotrauma
|
||||
float prio = objectiveManager.GetOrderPriority(this);
|
||||
if (subObjectives.All(so => so.SubObjectives.None()))
|
||||
{
|
||||
// If none of the subobjectives have subobjectives, no valid container was found. In this case, let's reduce the priority below the run threshold.
|
||||
prio = Math.Min(prio, AIObjectiveManager.RunPriority - 1);
|
||||
// If none of the subobjectives have subobjectives, no valid container was found. Don't allow running.
|
||||
ForceWalk = true;
|
||||
}
|
||||
return prio;
|
||||
}
|
||||
@@ -80,18 +80,29 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsValidContainer(Item item, Character character, bool allowUnloading = true) =>
|
||||
!item.IgnoreByAI && item.IsInteractable(character) && item.HasTag("allowcleanup") && allowUnloading && item.ParentInventory == null && item.OwnInventory != null && item.OwnInventory.AllItems.Any() && IsItemInsideValidSubmarine(item, character);
|
||||
public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) =>
|
||||
allowUnloading &&
|
||||
!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)
|
||||
{
|
||||
if (item.Container == null || !IsValidContainer(item.Container, character, allowUnloading)) { return false; }
|
||||
if (item.Container == null)
|
||||
{
|
||||
// In a character inventory
|
||||
return false;
|
||||
}
|
||||
if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; }
|
||||
}
|
||||
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
|
||||
var pickable = item.GetComponent<Pickable>();
|
||||
|
||||
+12
-17
@@ -10,7 +10,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveCombat : AIObjective
|
||||
{
|
||||
public override string DebugTag => "combat";
|
||||
public override string Identifier { get; set; } = "combat";
|
||||
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool IgnoreUnsafeHulls => true;
|
||||
@@ -92,11 +92,6 @@ namespace Barotrauma
|
||||
private readonly float distanceCheckInterval = 0.2f;
|
||||
private float distanceTimer;
|
||||
|
||||
/// <summary>
|
||||
/// Aborts the objective when this condition is true
|
||||
/// </summary>
|
||||
public Func<bool> abortCondition;
|
||||
|
||||
public bool allowHoldFire;
|
||||
|
||||
/// <summary>
|
||||
@@ -152,7 +147,7 @@ namespace Barotrauma
|
||||
HumanAIController.SortTimer = 0;
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && Enemy != null)
|
||||
{
|
||||
@@ -186,7 +181,7 @@ namespace Barotrauma
|
||||
{
|
||||
findSafety.Priority = 0;
|
||||
}
|
||||
if (!character.IsOnPlayerTeam && !objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
|
||||
if (!AllowCoolDown && !character.IsOnPlayerTeam && !objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
|
||||
{
|
||||
distanceTimer -= deltaTime;
|
||||
if (distanceTimer < 0)
|
||||
@@ -197,7 +192,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
{
|
||||
if (sqrDistance > maxDistance * maxDistance)
|
||||
{
|
||||
@@ -209,11 +204,6 @@ namespace Barotrauma
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (abortCondition != null && abortCondition())
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (AllowCoolDown)
|
||||
{
|
||||
coolDownTimer -= deltaTime;
|
||||
@@ -358,6 +348,7 @@ namespace Barotrauma
|
||||
TryAddSubObjective(ref seekWeaponObjective,
|
||||
constructor: () => new AIObjectiveGetItem(character, "weapon", objectiveManager, equip: true, checkInventory: false)
|
||||
{
|
||||
AllowStealing = HumanAIController.IsMentallyUnstable,
|
||||
GetItemPriority = i =>
|
||||
{
|
||||
if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; }
|
||||
@@ -799,10 +790,13 @@ namespace Barotrauma
|
||||
}
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
// Confiscate stolen goods.
|
||||
// Confiscate stolen goods and all weapons
|
||||
foreach (var item in Enemy.Inventory.AllItemsMod)
|
||||
{
|
||||
if (item.StolenDuringRound)
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound ||
|
||||
item.HasTag("weapon") ||
|
||||
item.GetComponent<MeleeWeapon>() != null ||
|
||||
item.GetComponent<RangedWeapon>() != null)
|
||||
{
|
||||
item.Drop(character);
|
||||
character.Inventory.TryPutItem(item, character, CharacterInventory.anySlot);
|
||||
@@ -894,7 +888,8 @@ namespace Barotrauma
|
||||
if (ammunitionIdentifiers != null)
|
||||
{
|
||||
// Try reload ammunition from inventory
|
||||
ammunition = character.Inventory.FindItem(i => ammunitionIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)) && i.Condition > 0, true);
|
||||
bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio");
|
||||
ammunition = character.Inventory.FindItem(i => ammunitionIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
|
||||
if (ammunition != null)
|
||||
{
|
||||
var container = Weapon.GetComponent<ItemContainer>();
|
||||
|
||||
+11
-7
@@ -7,7 +7,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveContainItem: AIObjective
|
||||
{
|
||||
public override string DebugTag => "contain item";
|
||||
public override string Identifier { get; set; } = "contain item";
|
||||
|
||||
public Func<Item, float> GetItemPriority;
|
||||
|
||||
@@ -61,10 +61,10 @@ namespace Barotrauma
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
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 == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI()))
|
||||
if (container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character))
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
@@ -146,7 +146,10 @@ namespace Barotrauma
|
||||
{
|
||||
DialogueIdentifier = "dialogcannotreachtarget",
|
||||
TargetName = container.Item.Name,
|
||||
abortCondition = obj => !ItemToContain.IsOwnedBy(character),
|
||||
AbortCondition = obj =>
|
||||
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>()
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
@@ -170,7 +173,8 @@ namespace Barotrauma
|
||||
ignoredItems = containedItems,
|
||||
AllowToFindDivingGear = AllowToFindDivingGear,
|
||||
AllowDangerousPressure = AllowDangerousPressure,
|
||||
TargetCondition = ConditionLevel
|
||||
TargetCondition = ConditionLevel,
|
||||
ItemFilter = (Item potentialItem) => RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem)
|
||||
}, onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
|
||||
+7
-4
@@ -6,7 +6,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveDecontainItem : AIObjective
|
||||
{
|
||||
public override string DebugTag => "decontain item";
|
||||
public override string Identifier { get; set; } = "decontain item";
|
||||
|
||||
public Func<Item, float> GetItemPriority;
|
||||
|
||||
@@ -59,17 +59,20 @@ namespace Barotrauma
|
||||
this.targetContainer = targetContainer;
|
||||
}
|
||||
|
||||
protected override bool Check() => IsCompleted;
|
||||
protected override bool CheckObjectiveSpecific() => IsCompleted;
|
||||
|
||||
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;
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveEscapeHandcuffs : AIObjective
|
||||
{
|
||||
// Used for prisoner escorts to allow them to escape their binds
|
||||
public override string Identifier { get; set; } = "escape handcuffs";
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
public override bool AllowOutsideSubmarine => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
|
||||
private int escapeProgress;
|
||||
private bool isBeingWatched;
|
||||
|
||||
private bool shouldSwitchTeams;
|
||||
|
||||
const string EscapeTeamChangeIdentifier = "escape";
|
||||
|
||||
public AIObjectiveEscapeHandcuffs(Character character, AIObjectiveManager objectiveManager, bool shouldSwitchTeams = true, bool beginInstantly = false, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
this.shouldSwitchTeams = shouldSwitchTeams;
|
||||
if (beginInstantly)
|
||||
{
|
||||
escapeTimer = EscapeIntervalTimer;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanBeCompleted => true;
|
||||
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
|
||||
// escape timer is set to 60 by default to allow players to locate prisoners in time
|
||||
private float escapeTimer = 60f;
|
||||
private const float EscapeIntervalTimer = 7.5f;
|
||||
|
||||
private float updateTimer;
|
||||
private const float UpdateIntervalTimer = 4f;
|
||||
|
||||
protected override float GetPriority()
|
||||
{
|
||||
Priority = !isBeingWatched && character.LockHands ? AIObjectiveManager.LowestOrderPriority - 1 : 0;
|
||||
return Priority;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
updateTimer -= deltaTime;
|
||||
if (updateTimer <= 0.0f)
|
||||
{
|
||||
if (shouldSwitchTeams)
|
||||
{
|
||||
if (!character.LockHands)
|
||||
{
|
||||
if (!character.HasTeamChange(EscapeTeamChangeIdentifier))
|
||||
{
|
||||
character.TryAddNewTeamChange(EscapeTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
character.TryRemoveTeamChange(EscapeTeamChangeIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
isBeingWatched = false;
|
||||
foreach (Character otherCharacter in Character.CharacterList)
|
||||
{
|
||||
if (HumanAIController.IsActive(otherCharacter) && otherCharacter.TeamID == CharacterTeamType.Team1 && HumanAIController.VisibleHulls.Contains(otherCharacter.CurrentHull)) // hasn't been tested yet
|
||||
{
|
||||
isBeingWatched = true; // act casual when player characters are around
|
||||
escapeProgress = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
updateTimer = UpdateIntervalTimer * Rand.Range(0.75f, 1.25f);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
|
||||
escapeTimer -= deltaTime;
|
||||
if (escapeTimer <= 0.0f)
|
||||
{
|
||||
escapeProgress += Rand.Range(2, 5);
|
||||
if (escapeProgress > 15)
|
||||
{
|
||||
Item handcuffs = character.Inventory.FindItemByTag("handlocker");
|
||||
if (handcuffs != null)
|
||||
{
|
||||
handcuffs.Drop(character);
|
||||
}
|
||||
}
|
||||
escapeTimer = EscapeIntervalTimer * Rand.Range(0.75f, 1.25f);
|
||||
}
|
||||
}
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
escapeProgress = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -8,7 +8,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveExtinguishFire : AIObjective
|
||||
{
|
||||
public override string DebugTag => "extinguish fire";
|
||||
public override string Identifier { get; set; } = "extinguish fire";
|
||||
public override bool ForceRun => true;
|
||||
public override bool ConcurrentObjectives => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
@@ -27,7 +27,7 @@ namespace Barotrauma
|
||||
this.targetHull = targetHull;
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
@@ -68,7 +68,7 @@ namespace Barotrauma
|
||||
return Priority;
|
||||
}
|
||||
|
||||
protected override bool Check() => targetHull.FireSources.None();
|
||||
protected override bool CheckObjectiveSpecific() => targetHull.FireSources.None();
|
||||
|
||||
private float sinTime;
|
||||
protected override void Act(float deltaTime)
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveExtinguishFires : AIObjectiveLoop<Hull>
|
||||
{
|
||||
public override string DebugTag => "extinguish fires";
|
||||
public override string Identifier { get; set; } = "extinguish fires";
|
||||
public override bool ForceRun => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
|
||||
|
||||
+9
-4
@@ -6,7 +6,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveFightIntruders : AIObjectiveLoop<Character>
|
||||
{
|
||||
public override string DebugTag => "fight intruders";
|
||||
public override string Identifier { get; set; } = "fight intruders";
|
||||
protected override float IgnoreListClearInterval => 30;
|
||||
public override bool IgnoreUnsafeHulls => true;
|
||||
|
||||
@@ -21,13 +21,18 @@ namespace Barotrauma
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
// TODO: sorting criteria
|
||||
return Targets.None() ? 0 : 100;
|
||||
if (!character.IsOnPlayerTeam) { return Targets.None() ? 0 : 100; }
|
||||
int totalEnemies = Targets.Count();
|
||||
if (totalEnemies == 0) { return 0; }
|
||||
if (character.IsSecurity) { return 100; }
|
||||
if (objectiveManager.IsOrder(this)) { return 100; }
|
||||
return HumanAIController.IsTrueForAnyCrewMember(c => c.Character.IsSecurity && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine) ? 0 : 100;
|
||||
}
|
||||
|
||||
protected override AIObjective ObjectiveConstructor(Character target)
|
||||
{
|
||||
var combatObjective = new AIObjectiveCombat(character, target, AIObjectiveCombat.CombatMode.Offensive, objectiveManager, PriorityModifier);
|
||||
AIObjectiveCombat.CombatMode combatMode = target.IsEscorted && character.TeamID == CharacterTeamType.Team1 ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Offensive;
|
||||
var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier);
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
|
||||
{
|
||||
var reputation = campaign.Map?.CurrentLocation?.Reputation;
|
||||
|
||||
+9
-7
@@ -7,7 +7,8 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveFindDivingGear : AIObjective
|
||||
{
|
||||
public override string DebugTag => $"find diving gear ({gearTag})";
|
||||
public override string Identifier { get; set; } = "find diving gear";
|
||||
public override string DebugTag => $"{Identifier} ({gearTag})";
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
||||
@@ -23,7 +24,7 @@ namespace Barotrauma
|
||||
public static string LIGHT_DIVING_GEAR = "lightdiving";
|
||||
public static string OXYGEN_SOURCE = "oxygensource";
|
||||
|
||||
protected override bool Check() => targetItem != null && character.HasEquippedItem(targetItem);
|
||||
protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head);
|
||||
|
||||
public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
@@ -38,7 +39,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
targetItem = character.Inventory.FindItemByTag(gearTag, true);
|
||||
if (targetItem == null || !character.HasEquippedItem(targetItem) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0))
|
||||
if (targetItem == null || !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0))
|
||||
{
|
||||
TryAddSubObjective(ref getDivingGear, () =>
|
||||
{
|
||||
@@ -48,9 +49,11 @@ namespace Barotrauma
|
||||
}
|
||||
return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
|
||||
{
|
||||
AllowStealing = true,
|
||||
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
|
||||
AllowToFindDivingGear = false,
|
||||
AllowDangerousPressure = true
|
||||
AllowDangerousPressure = true,
|
||||
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes,
|
||||
Wear = true
|
||||
};
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
@@ -58,8 +61,6 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
HumanAIController.UnequipContainedItems(targetItem, it => !it.HasTag("oxygensource"));
|
||||
HumanAIController.UnequipEmptyItems(targetItem);
|
||||
// Seek oxygen that has at least 10% condition left, if we are inside a friendly sub.
|
||||
// The margin helps us to survive, because we might need some oxygen before we can find more oxygen.
|
||||
// When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks.
|
||||
@@ -119,6 +120,7 @@ namespace Barotrauma
|
||||
|
||||
int ReportOxygenTankCount()
|
||||
{
|
||||
if (character.Submarine != Submarine.MainSub) { return 1; }
|
||||
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("oxygensource") && i.Condition > 1);
|
||||
if (remainingOxygenTanks == 0)
|
||||
{
|
||||
|
||||
+3
-3
@@ -8,7 +8,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveFindSafety : AIObjective
|
||||
{
|
||||
public override string DebugTag => "find safety";
|
||||
public override string Identifier { get; set; } = "find safety";
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool IgnoreUnsafeHulls => true;
|
||||
@@ -32,12 +32,12 @@ namespace Barotrauma
|
||||
|
||||
public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
|
||||
|
||||
protected override bool Check() => false;
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
public override bool CanBeCompleted => true;
|
||||
|
||||
private bool resetPriority;
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
|
||||
+25
-21
@@ -9,7 +9,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveFixLeak : AIObjective
|
||||
{
|
||||
public override string DebugTag => "fix leak";
|
||||
public override string Identifier { get; set; } = "fix leak";
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
@@ -29,19 +29,22 @@ namespace Barotrauma
|
||||
this.isPriority = isPriority;
|
||||
}
|
||||
|
||||
protected override bool Check() => Leak.Open <= 0 || Leak.Removed;
|
||||
protected override bool CheckObjectiveSpecific() => Leak.Open <= 0 || Leak.Removed;
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
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
|
||||
{
|
||||
@@ -86,21 +89,22 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
HumanAIController.UnequipContainedItems(weldingTool, it => !it.HasTag("weldingfuel"));
|
||||
HumanAIController.UnequipEmptyItems(weldingTool);
|
||||
if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
|
||||
{
|
||||
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
|
||||
onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
ReportWeldingFuelTankCount();
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
RemoveSubObjective(ref refuelObjective);
|
||||
ReportWeldingFuelTankCount();
|
||||
});
|
||||
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
RemoveExisting = true
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
ReportWeldingFuelTankCount();
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
RemoveSubObjective(ref refuelObjective);
|
||||
ReportWeldingFuelTankCount();
|
||||
});
|
||||
|
||||
void ReportWeldingFuelTankCount()
|
||||
{
|
||||
@@ -141,7 +145,7 @@ namespace Barotrauma
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () =>
|
||||
{
|
||||
if (Check()) { IsCompleted = true; }
|
||||
if (CheckObjectiveSpecific()) { IsCompleted = true; }
|
||||
else
|
||||
{
|
||||
// Failed to operate. Probably too far.
|
||||
@@ -160,7 +164,7 @@ namespace Barotrauma
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
if (Check()) { IsCompleted = true; }
|
||||
if (CheckObjectiveSpecific()) { IsCompleted = true; }
|
||||
else if ((Leak.WorldPosition - character.WorldPosition).LengthSquared() > MathUtils.Pow(reach * 2, 2))
|
||||
{
|
||||
// Too far
|
||||
@@ -191,7 +195,7 @@ namespace Barotrauma
|
||||
// This is an approximation, because we don't know the exact reach until the pose is taken.
|
||||
// And even then the actual range depends on the direction we are aiming to.
|
||||
// Found out that without any multiplier the value (209) is often too short.
|
||||
return repairTool.Range + armLength * 1.2f;
|
||||
return repairTool.Range + armLength * 1.3f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -6,7 +6,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveFixLeaks : AIObjectiveLoop<Gap>
|
||||
{
|
||||
public override string DebugTag => "fix leaks";
|
||||
public override string Identifier { get; set; } = "fix leaks";
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
@@ -40,7 +40,7 @@ namespace Barotrauma
|
||||
{
|
||||
int totalLeaks = Targets.Count();
|
||||
if (totalLeaks == 0) { return 0; }
|
||||
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated, onlyBots: true);
|
||||
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine, onlyBots: true);
|
||||
bool anyFixers = otherFixers > 0;
|
||||
if (objectiveManager.IsOrder(this))
|
||||
{
|
||||
@@ -51,7 +51,7 @@ namespace Barotrauma
|
||||
{
|
||||
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
|
||||
int leaks = totalLeaks - secondaryLeaks;
|
||||
float ratio = leaks == 0 ? 1 : anyFixers ? leaks / otherFixers : 1;
|
||||
float ratio = leaks == 0 ? 1 : anyFixers ? leaks / (float)otherFixers : 1;
|
||||
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
|
||||
{
|
||||
// Enough fixers
|
||||
@@ -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; }
|
||||
|
||||
+28
-11
@@ -8,11 +8,10 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveGetItem : AIObjective
|
||||
{
|
||||
public override string DebugTag => "get item";
|
||||
public override string Identifier { get; set; } = "get item";
|
||||
|
||||
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
||||
|
||||
private readonly bool equip;
|
||||
public HashSet<Item> ignoredItems = new HashSet<Item>();
|
||||
|
||||
public Func<Item, float> GetItemPriority;
|
||||
@@ -45,14 +44,17 @@ namespace Barotrauma
|
||||
/// Is the character allowed to take the item from somewhere else than their own sub (e.g. an outpost)
|
||||
/// </summary>
|
||||
public bool AllowStealing { get; set; }
|
||||
|
||||
public bool TakeWholeStack { get; set; }
|
||||
public bool Equip { get; set; }
|
||||
public bool Wear { get; set; }
|
||||
|
||||
public InvSlotType? EquipSlotType { get; set; }
|
||||
|
||||
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
currSearchIndex = -1;
|
||||
this.equip = equip;
|
||||
Equip = equip;
|
||||
originalTarget = targetItem;
|
||||
this.targetItem = targetItem;
|
||||
moveToTarget = targetItem?.GetRootInventoryOwner();
|
||||
@@ -65,7 +67,7 @@ namespace Barotrauma
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
currSearchIndex = -1;
|
||||
this.equip = equip;
|
||||
Equip = equip;
|
||||
this.identifiersOrTags = identifiersOrTags;
|
||||
this.spawnItemIfNotFound = spawnItemIfNotFound;
|
||||
for (int i = 0; i < identifiersOrTags.Length; i++)
|
||||
@@ -197,7 +199,7 @@ namespace Barotrauma
|
||||
|
||||
Inventory itemInventory = targetItem.ParentInventory;
|
||||
var slots = itemInventory?.FindIndices(targetItem);
|
||||
if (HumanAIController.TakeItem(targetItem, character.Inventory, equip, storeUnequipped: true))
|
||||
if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true))
|
||||
{
|
||||
if (TakeWholeStack && slots != null)
|
||||
{
|
||||
@@ -227,7 +229,7 @@ namespace Barotrauma
|
||||
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear, closeEnough: DefaultReach)
|
||||
{
|
||||
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
|
||||
abortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget,
|
||||
AbortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget,
|
||||
SpeakIfFails = false
|
||||
};
|
||||
},
|
||||
@@ -303,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);
|
||||
@@ -365,19 +368,33 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
{
|
||||
if (IsCompleted) { return true; }
|
||||
if (targetItem != null)
|
||||
{
|
||||
return character.HasItem(targetItem, equip);
|
||||
if (Equip && EquipSlotType.HasValue)
|
||||
{
|
||||
return character.HasEquippedItem(targetItem, EquipSlotType.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return character.HasItem(targetItem, Equip);
|
||||
}
|
||||
}
|
||||
else if (identifiersOrTags != null)
|
||||
{
|
||||
var matchingItem = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
|
||||
if (matchingItem != null)
|
||||
{
|
||||
return !equip || character.HasEquippedItem(matchingItem);
|
||||
if (Equip && EquipSlotType.HasValue)
|
||||
{
|
||||
return character.HasEquippedItem(matchingItem, EquipSlotType.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return !Equip || character.HasEquippedItem(matchingItem);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -387,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; }
|
||||
|
||||
+39
-18
@@ -8,7 +8,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveGoTo : AIObjective
|
||||
{
|
||||
public override string DebugTag => "go to";
|
||||
public override string Identifier { get; set; } = "go to";
|
||||
|
||||
private AIObjectiveFindDivingGear findDivingGear;
|
||||
private readonly bool repeat;
|
||||
@@ -20,10 +20,6 @@ namespace Barotrauma
|
||||
/// Doesn't allow the objective to complete if this condition is false
|
||||
/// </summary>
|
||||
public Func<bool> requiredCondition;
|
||||
/// <summary>
|
||||
/// Aborts the objective when this condition is true
|
||||
/// </summary>
|
||||
public Func<AIObjectiveGoTo, bool> abortCondition;
|
||||
public Func<PathNode, bool> endNodeFilter;
|
||||
|
||||
public Func<float> priorityGetter;
|
||||
@@ -38,6 +34,7 @@ namespace Barotrauma
|
||||
private readonly float minDistance = 50;
|
||||
private readonly float seekGapsInterval = 1;
|
||||
private float seekGapsTimer;
|
||||
private bool cannotFollow;
|
||||
|
||||
/// <summary>
|
||||
/// Display units
|
||||
@@ -81,7 +78,7 @@ namespace Barotrauma
|
||||
|
||||
public float? OverridePriority = null;
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
bool isOrder = objectiveManager.IsOrder(this);
|
||||
if (!IsAllowed)
|
||||
@@ -177,6 +174,11 @@ namespace Barotrauma
|
||||
character.AIController.SteeringManager.Reset();
|
||||
return;
|
||||
}
|
||||
if (cannotFollow)
|
||||
{
|
||||
// Wait
|
||||
character.AIController.SteeringManager.Reset();
|
||||
}
|
||||
waitUntilPathUnreachable -= deltaTime;
|
||||
if (!character.IsClimbing)
|
||||
{
|
||||
@@ -263,16 +265,29 @@ namespace Barotrauma
|
||||
if (findDivingGear != null && !findDivingGear.CanBeCompleted)
|
||||
{
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref findDivingGear));
|
||||
onAbandon: () => Abort(),
|
||||
onCompleted: () =>
|
||||
{
|
||||
cannotFollow = false;
|
||||
RemoveSubObjective(ref findDivingGear);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
|
||||
onCompleted: () => RemoveSubObjective(ref findDivingGear));
|
||||
onAbandon: () => Abort(),
|
||||
onCompleted: () =>
|
||||
{
|
||||
cannotFollow = false;
|
||||
RemoveSubObjective(ref findDivingGear);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
cannotFollow = false;
|
||||
}
|
||||
}
|
||||
if (repeat)
|
||||
{
|
||||
@@ -578,22 +593,15 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
{
|
||||
if (IsCompleted) { return true; }
|
||||
// First check the distance
|
||||
// Then the custom condition
|
||||
// And finally check if can interact (heaviest)
|
||||
// First check the distance and then if can interact (heaviest)
|
||||
if (Target == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return false;
|
||||
}
|
||||
if (abortCondition != null && abortCondition(this))
|
||||
{
|
||||
Abandon = true;
|
||||
return false;
|
||||
}
|
||||
if (repeat)
|
||||
{
|
||||
return false;
|
||||
@@ -624,6 +632,18 @@ namespace Barotrauma
|
||||
return IsCompleted;
|
||||
}
|
||||
|
||||
private void Abort()
|
||||
{
|
||||
if (!objectiveManager.IsOrder(this))
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
cannotFollow = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAbandon()
|
||||
{
|
||||
StopMovement();
|
||||
@@ -657,6 +677,7 @@ namespace Barotrauma
|
||||
findDivingGear = null;
|
||||
seekGapsTimer = 0;
|
||||
TargetGap = null;
|
||||
cannotFollow = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-30
@@ -10,7 +10,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveIdle : AIObjective
|
||||
{
|
||||
public override string DebugTag => "idle";
|
||||
public override string Identifier { get; set; } = "idle";
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
|
||||
@@ -21,11 +21,6 @@ namespace Barotrauma
|
||||
set
|
||||
{
|
||||
behavior = value;
|
||||
if (behavior == BehaviorType.StayInHull && TargetHull == null)
|
||||
{
|
||||
DebugConsole.AddWarning($"Trying to set a character's behavior type to StayInHull, but target hull is not set. {character.Name} ({character.Info.Job.Prefab.Identifier})");
|
||||
behavior = BehaviorType.Passive;
|
||||
}
|
||||
switch (behavior)
|
||||
{
|
||||
case BehaviorType.Passive:
|
||||
@@ -93,7 +88,7 @@ namespace Barotrauma
|
||||
CalculatePriority();
|
||||
}
|
||||
|
||||
protected override bool Check() => false;
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
public override bool CanBeCompleted => true;
|
||||
|
||||
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
|
||||
@@ -110,21 +105,11 @@ namespace Barotrauma
|
||||
Priority = 1;
|
||||
}
|
||||
|
||||
public override float GetPriority() => Priority;
|
||||
protected override float GetPriority() => Priority;
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
//if (objectiveManager.CurrentObjective == this)
|
||||
//{
|
||||
// if (randomTimer > 0)
|
||||
// {
|
||||
// randomTimer -= deltaTime;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// CalculatePriority();
|
||||
// }
|
||||
//}
|
||||
// Do nothing. Overrides the inherited devotion calculations.
|
||||
}
|
||||
|
||||
private float timerMargin;
|
||||
@@ -183,6 +168,11 @@ namespace Barotrauma
|
||||
|
||||
CleanupItems(deltaTime);
|
||||
|
||||
if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null)
|
||||
{
|
||||
TargetHull = character.CurrentHull;
|
||||
}
|
||||
|
||||
if (behavior == BehaviorType.StayInHull)
|
||||
{
|
||||
currentTarget = TargetHull;
|
||||
@@ -203,7 +193,7 @@ namespace Barotrauma
|
||||
|
||||
if (currentTarget != null && !currentTargetIsInvalid)
|
||||
{
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
|
||||
{
|
||||
if (currentTarget.Submarine.TeamID != character.TeamID)
|
||||
{
|
||||
@@ -260,9 +250,9 @@ namespace Barotrauma
|
||||
{
|
||||
//choose a random available hull
|
||||
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
|
||||
bool isInWrongSub = character.TeamID == CharacterTeamType.FriendlyNPC && character.Submarine.TeamID != character.TeamID;
|
||||
bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID;
|
||||
bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: $"AIObjectiveIdle {character.DisplayName}", nodeFilter: node =>
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: null, nodeFilter: node =>
|
||||
{
|
||||
if (node.Waypoint.CurrentHull == null) { return false; }
|
||||
// Check that there is no unsafe or forbidden hulls on the way to the target
|
||||
@@ -419,7 +409,7 @@ namespace Barotrauma
|
||||
if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
|
||||
if (hull.Submarine == null) { continue; }
|
||||
if (character.Submarine == null) { break; }
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
|
||||
{
|
||||
if (hull.Submarine.TeamID != character.TeamID)
|
||||
{
|
||||
@@ -519,13 +509,7 @@ namespace Barotrauma
|
||||
}
|
||||
#endregion
|
||||
|
||||
public static bool IsForbidden(Hull hull)
|
||||
{
|
||||
if (hull == null) { return true; }
|
||||
string hullName = hull.RoomName;
|
||||
if (hullName == null) { return false; }
|
||||
return hullName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || hullName.Contains("airlock", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
public static bool IsForbidden(Hull hull) => hull == null || hull.AvoidStaying;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
|
||||
+2
-2
@@ -40,7 +40,7 @@ namespace Barotrauma
|
||||
: base(character, objectiveManager, priorityModifier, option) { }
|
||||
|
||||
protected override void Act(float deltaTime) { }
|
||||
protected override bool Check() => false;
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
public override bool CanBeCompleted => true;
|
||||
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
||||
public override bool AllowSubObjectiveSorting => true;
|
||||
@@ -106,7 +106,7 @@ namespace Barotrauma
|
||||
UpdateTargets();
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
|
||||
+47
-24
@@ -132,14 +132,14 @@ namespace Barotrauma
|
||||
}
|
||||
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
|
||||
if (order == null) { continue; }
|
||||
if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
|
||||
if ((order.IgnoreAtOutpost || autonomousObjective.ignoreAtOutpost) && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier);
|
||||
var objective = CreateObjective(order, autonomousObjective.option, character, autonomousObjective.priorityModifier);
|
||||
if (objective != null && objective.CanBeCompleted)
|
||||
{
|
||||
AddObjective(objective, delay: Rand.Value() / 2);
|
||||
@@ -184,7 +184,8 @@ namespace Barotrauma
|
||||
{
|
||||
var previousObjective = CurrentObjective;
|
||||
var firstObjective = Objectives.FirstOrDefault();
|
||||
if (CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority)
|
||||
bool currentObjectiveIsOrder = CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority;
|
||||
if (currentObjectiveIsOrder)
|
||||
{
|
||||
CurrentObjective = CurrentOrder;
|
||||
}
|
||||
@@ -197,6 +198,14 @@ namespace Barotrauma
|
||||
previousObjective?.OnDeselected();
|
||||
CurrentObjective?.OnSelected();
|
||||
GetObjective<AIObjectiveIdle>().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0));
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(character, new object[]
|
||||
{
|
||||
NetEntityEvent.Type.ObjectiveManagerState,
|
||||
currentObjectiveIsOrder ? "order" : "objective"
|
||||
});
|
||||
}
|
||||
}
|
||||
return CurrentObjective;
|
||||
}
|
||||
@@ -269,38 +278,29 @@ namespace Barotrauma
|
||||
|
||||
public void SortObjectives()
|
||||
{
|
||||
ForcedOrder?.GetPriority();
|
||||
|
||||
ForcedOrder?.CalculatePriority();
|
||||
AIObjective orderWithHighestPriority = null;
|
||||
float highestPriority = 0;
|
||||
foreach (var currentOrder in CurrentOrders)
|
||||
{
|
||||
var orderObjective = currentOrder.Objective;
|
||||
if (orderObjective == null) { continue; }
|
||||
orderObjective.GetPriority();
|
||||
orderObjective.CalculatePriority();
|
||||
if (orderWithHighestPriority == null || orderObjective.Priority > highestPriority)
|
||||
{
|
||||
orderWithHighestPriority = orderObjective;
|
||||
highestPriority = orderObjective.Priority;
|
||||
}
|
||||
}
|
||||
#if SERVER
|
||||
if (orderWithHighestPriority != null && orderWithHighestPriority != currentOrder)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.ObjectiveManagerOrderState });
|
||||
}
|
||||
#endif
|
||||
CurrentOrder = orderWithHighestPriority;
|
||||
|
||||
for (int i = Objectives.Count - 1; i >= 0; i--)
|
||||
{
|
||||
Objectives[i].GetPriority();
|
||||
Objectives[i].CalculatePriority();
|
||||
}
|
||||
if (Objectives.Any())
|
||||
{
|
||||
Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
|
||||
}
|
||||
|
||||
GetCurrentObjective()?.SortSubObjectives();
|
||||
}
|
||||
|
||||
@@ -380,7 +380,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
var newCurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false);
|
||||
var newCurrentOrder = CreateObjective(order, option, orderGiver);
|
||||
if (newCurrentOrder != null)
|
||||
{
|
||||
CurrentOrders.Add(new OrderInfo(order, option, priority, newCurrentOrder));
|
||||
@@ -441,7 +441,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public AIObjective CreateObjective(Order order, string option, Character orderGiver, bool isAutonomous, float priorityModifier = 1)
|
||||
public AIObjective CreateObjective(Order order, string option, Character orderGiver, float priorityModifier = 1)
|
||||
{
|
||||
if (order == null || order.Identifier == "dismissed") { return null; }
|
||||
AIObjective newObjective;
|
||||
@@ -482,7 +482,6 @@ namespace Barotrauma
|
||||
newObjective = new AIObjectiveRepairItems(character, this, priorityModifier: priorityModifier, prioritizedItem: order.TargetEntity as Item)
|
||||
{
|
||||
RelevantSkill = order.AppropriateSkill,
|
||||
RequireAdequateSkills = isAutonomous
|
||||
};
|
||||
break;
|
||||
case "pumpwater":
|
||||
@@ -492,7 +491,7 @@ namespace Barotrauma
|
||||
newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier)
|
||||
{
|
||||
IsLoop = true,
|
||||
Override = orderGiver != null && orderGiver.IsPlayer
|
||||
Override = orderGiver != null && orderGiver.IsCommanding
|
||||
};
|
||||
// ItemComponent.AIOperate() returns false by default -> We'd have to set IsLoop = false and implement a custom override of AIOperate for the Pump.cs,
|
||||
// if we want that the bot just switches the pump on/off and continues doing something else.
|
||||
@@ -519,7 +518,7 @@ namespace Barotrauma
|
||||
{
|
||||
IsLoop = true,
|
||||
// Don't override unless it's an order by a player
|
||||
Override = orderGiver != null && orderGiver.IsPlayer
|
||||
Override = orderGiver != null && orderGiver.IsCommanding
|
||||
};
|
||||
break;
|
||||
case "setchargepct":
|
||||
@@ -563,6 +562,9 @@ namespace Barotrauma
|
||||
newObjective = new AIObjectiveCleanupItems(character, this, priorityModifier: priorityModifier);
|
||||
}
|
||||
break;
|
||||
case "escapehandcuffs":
|
||||
newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier);
|
||||
break;
|
||||
default:
|
||||
if (order.TargetItemComponent == null) { return null; }
|
||||
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
|
||||
@@ -571,16 +573,22 @@ namespace Barotrauma
|
||||
{
|
||||
IsLoop = true,
|
||||
// Don't override unless it's an order by a player
|
||||
Override = orderGiver != null && orderGiver.IsPlayer
|
||||
Override = orderGiver != null && orderGiver.IsCommanding
|
||||
};
|
||||
if (newObjective.Abandon) { return null; }
|
||||
break;
|
||||
}
|
||||
if (newObjective != null)
|
||||
{
|
||||
newObjective.Identifier = order.Identifier;
|
||||
}
|
||||
newObjective.IgnoreAtOutpost = order.IgnoreAtOutpost;
|
||||
return newObjective;
|
||||
}
|
||||
|
||||
private bool IsAllowedToWait()
|
||||
{
|
||||
if (!character.IsOnPlayerTeam) { return false; }
|
||||
if (HasOrders()) { return false; }
|
||||
if (CurrentObjective is AIObjectiveCombat || CurrentObjective is AIObjectiveFindSafety) { return false; }
|
||||
if (character.AnimController.InWater) { return false; }
|
||||
@@ -606,7 +614,11 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently.
|
||||
/// </summary>
|
||||
public IEnumerable<T> GetActiveObjectives<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T);
|
||||
public IEnumerable<T> GetActiveObjectives<T>() where T : AIObjective
|
||||
{
|
||||
if (CurrentObjective == null) { return Enumerable.Empty<T>(); }
|
||||
return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T);
|
||||
}
|
||||
|
||||
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
|
||||
|
||||
@@ -627,7 +639,10 @@ namespace Barotrauma
|
||||
|
||||
public float GetOrderPriority(AIObjective objective)
|
||||
{
|
||||
if (objective == ForcedOrder) { return HighestOrderPriority; }
|
||||
if (objective == ForcedOrder)
|
||||
{
|
||||
return HighestOrderPriority;
|
||||
}
|
||||
var currentOrder = CurrentOrders.FirstOrDefault(o => o.Objective == objective);
|
||||
if (currentOrder.Objective == null)
|
||||
{
|
||||
@@ -635,7 +650,15 @@ namespace Barotrauma
|
||||
}
|
||||
else if (currentOrder.ManualPriority > 0)
|
||||
{
|
||||
return MathHelper.Lerp(LowestOrderPriority, HighestOrderPriority, MathUtils.InverseLerp(1, CharacterInfo.HighestManualOrderPriority, currentOrder.ManualPriority));
|
||||
if (objective.ForceHighestPriority)
|
||||
{
|
||||
return HighestOrderPriority;
|
||||
}
|
||||
if (objective.PrioritizeIfSubObjectivesActive && objective.SubObjectives.Any())
|
||||
{
|
||||
return HighestOrderPriority;
|
||||
}
|
||||
return MathHelper.Lerp(LowestOrderPriority, HighestOrderPriority - 1, MathUtils.InverseLerp(1, CharacterInfo.HighestManualOrderPriority, currentOrder.ManualPriority));
|
||||
}
|
||||
#if DEBUG
|
||||
DebugConsole.AddWarning("Error in order priority: shouldn't return 0!");
|
||||
|
||||
+25
-12
@@ -8,15 +8,18 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveOperateItem : AIObjective
|
||||
{
|
||||
public override string DebugTag => $"operate item {component.Name}";
|
||||
public override string Identifier { get; set; } = "operate item";
|
||||
public override string DebugTag => $"{Identifier} {component.Name}";
|
||||
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
public override bool AllowMultipleInstances => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
public override bool PrioritizeIfSubObjectivesActive => component != null && (component is Reactor || component is Turret);
|
||||
|
||||
private ItemComponent component, controller;
|
||||
private Entity operateTarget;
|
||||
private bool requireEquip;
|
||||
private bool useController;
|
||||
private readonly ItemComponent component, controller;
|
||||
private readonly Entity operateTarget;
|
||||
private readonly bool requireEquip;
|
||||
private readonly bool useController;
|
||||
private AIObjectiveGoTo goToObjective;
|
||||
private AIObjectiveGetItem getItemObjective;
|
||||
|
||||
@@ -34,7 +37,7 @@ namespace Barotrauma
|
||||
public Func<bool> completionCondition;
|
||||
private bool isDoneOperating;
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
bool isOrder = objectiveManager.IsOrder(this);
|
||||
if (!IsAllowed || character.LockHands)
|
||||
@@ -43,7 +46,7 @@ namespace Barotrauma
|
||||
Abandon = !isOrder;
|
||||
return Priority;
|
||||
}
|
||||
if (component.Item.ConditionPercentage <= 0)
|
||||
if (!isOrder && component.Item.ConditionPercentage <= 0)
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
@@ -100,12 +103,22 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (!isOrder)
|
||||
{
|
||||
var steering = component?.Item.GetComponent<Steering>();
|
||||
if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsCaptain)))
|
||||
{
|
||||
// Ignore if already set to autopilot or if there's a captain onboard
|
||||
Priority = 0;
|
||||
return Priority;
|
||||
}
|
||||
}
|
||||
if (targetItem.CurrentHull == null ||
|
||||
targetItem.Submarine != character.Submarine && !isOrder ||
|
||||
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;
|
||||
}
|
||||
@@ -121,10 +134,10 @@ namespace Barotrauma
|
||||
{
|
||||
float value = CumulatedDevotion + (AIObjectiveManager.LowestOrderPriority * PriorityModifier);
|
||||
float max = AIObjectiveManager.LowestOrderPriority - 1;
|
||||
if (reactor != null && reactor.PowerOn && reactor.FissionRate > 1 && Option == "powerup")
|
||||
if (reactor != null && reactor.PowerOn && reactor.FissionRate > 1 && reactor.AutoTemp && Option == "powerup")
|
||||
{
|
||||
// Decrease the priority when targeting a reactor that is already on.
|
||||
value /= 2;
|
||||
// Already on, no need to operate.
|
||||
value = 0;
|
||||
}
|
||||
Priority = MathHelper.Clamp(value, 0, max);
|
||||
}
|
||||
@@ -268,7 +281,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Check() => isDoneOperating && !IsLoop;
|
||||
protected override bool CheckObjectiveSpecific() => isDoneOperating && !IsLoop;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectivePumpWater : AIObjectiveLoop<Pump>
|
||||
{
|
||||
public override string DebugTag => "pump water";
|
||||
public override string Identifier { get; set; } = "pump water";
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
+27
-13
@@ -1,6 +1,7 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
@@ -8,7 +9,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveRepairItem : AIObjective
|
||||
{
|
||||
public override string DebugTag => "repair item";
|
||||
public override string Identifier { get; set; } = "repair item";
|
||||
|
||||
public override bool AllowInAnySub => true;
|
||||
|
||||
@@ -31,9 +32,9 @@ namespace Barotrauma
|
||||
this.isPriority = isPriority;
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed || Item.IgnoreByAI)
|
||||
if (!IsAllowed || Item.IgnoreByAI(character))
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
@@ -43,11 +44,10 @@ namespace Barotrauma
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
// TODO: priority list?
|
||||
// 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
|
||||
{
|
||||
@@ -66,12 +66,25 @@ namespace Barotrauma
|
||||
float devotion = (CumulatedDevotion + selectedBonus) / 100;
|
||||
float reduction = isPriority ? 1 : isSelected ? 2 : 3;
|
||||
float max = AIObjectiveManager.LowestOrderPriority - reduction;
|
||||
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
|
||||
float highestWeight = -1;
|
||||
foreach (string tag in Item.Prefab.Tags)
|
||||
{
|
||||
if (JobPrefab.ItemRepairPriorities.TryGetValue(tag, out float weight) && weight > highestWeight)
|
||||
{
|
||||
highestWeight = weight;
|
||||
}
|
||||
}
|
||||
if (highestWeight == -1)
|
||||
{
|
||||
// Predefined weight not found.
|
||||
highestWeight = 1;
|
||||
}
|
||||
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * highestWeight * PriorityModifier), 0, 1));
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
{
|
||||
IsCompleted = Item.IsFullCondition;
|
||||
if (character.IsOnPlayerTeam && IsCompleted && IsRepairing())
|
||||
@@ -122,8 +135,6 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
HumanAIController.UnequipContainedItems(repairTool.Item, it => !it.HasTag("weldingfuel"));
|
||||
HumanAIController.UnequipEmptyItems(repairTool.Item);
|
||||
RelatedItem item = null;
|
||||
Item fuel = null;
|
||||
foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained])
|
||||
@@ -135,9 +146,12 @@ namespace Barotrauma
|
||||
if (fuel == null)
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
|
||||
onCompleted: () => RemoveSubObjective(ref refuelObjective),
|
||||
onAbandon: () => Abandon = true);
|
||||
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
RemoveExisting = true
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref refuelObjective),
|
||||
onAbandon: () => Abandon = true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
+24
-25
@@ -9,31 +9,26 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveRepairItems : AIObjectiveLoop<Item>
|
||||
{
|
||||
public override string DebugTag => "repair items";
|
||||
|
||||
/// <summary>
|
||||
/// Should the character only attempt to fix items they have the skills to fix, or any damaged item
|
||||
/// </summary>
|
||||
public bool RequireAdequateSkills;
|
||||
public override string Identifier { get; set; } = "repair items";
|
||||
|
||||
/// <summary>
|
||||
/// If set, only fix items where required skill matches this.
|
||||
/// </summary>
|
||||
public string RelevantSkill;
|
||||
|
||||
private readonly Item prioritizedItem;
|
||||
public Item PrioritizedItem { get; private set; }
|
||||
|
||||
public override bool AllowMultipleInstances => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
|
||||
public readonly static float RequiredSuccessFactor = 0.4f;
|
||||
|
||||
public override bool IsDuplicate<T>(T otherObjective) => otherObjective is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills;
|
||||
public override bool IsDuplicate<T>(T otherObjective) => otherObjective is AIObjectiveRepairItems repairObjective && objectiveManager.IsOrder(repairObjective) == objectiveManager.IsOrder(this);
|
||||
|
||||
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()
|
||||
@@ -69,25 +64,36 @@ namespace Barotrauma
|
||||
|
||||
protected override bool Filter(Item item)
|
||||
{
|
||||
if (!IsValidTarget(item, character)) { return false; }
|
||||
if (item.CurrentHull.FireSources.Count > 0) { return false; }
|
||||
// Don't repair items in rooms that have enemies inside.
|
||||
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
|
||||
if (!ViableForRepair(item, character, HumanAIController)) { return false; };
|
||||
if (!Objectives.ContainsKey(item))
|
||||
{
|
||||
if (item != character.SelectedConstruction)
|
||||
{
|
||||
float condition = item.ConditionPercentage;
|
||||
if (item.Repairables.All(r => condition >= r.RepairThreshold)) { return false; }
|
||||
if (NearlyFullCondition(item)) { return false; }
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(RelevantSkill))
|
||||
{
|
||||
if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier.Equals(RelevantSkill, StringComparison.OrdinalIgnoreCase)))) { return false; }
|
||||
}
|
||||
return !HumanAIController.IsItemRepairedByAnother(item, out _);
|
||||
}
|
||||
|
||||
public static bool ViableForRepair(Item item, Character character, HumanAIController humanAIController)
|
||||
{
|
||||
if (!IsValidTarget(item, character)) { return false; }
|
||||
if (item.CurrentHull.FireSources.Count > 0) { return false; }
|
||||
// Don't repair items in rooms that have enemies inside.
|
||||
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !humanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool NearlyFullCondition(Item item)
|
||||
{
|
||||
float condition = item.ConditionPercentage;
|
||||
return item.Repairables.All(r => condition >= r.RepairThreshold);
|
||||
}
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
var selectedItem = character.SelectedConstruction;
|
||||
@@ -115,14 +121,7 @@ namespace Barotrauma
|
||||
// Enough fixers
|
||||
return 0;
|
||||
}
|
||||
if (RequireAdequateSkills)
|
||||
{
|
||||
return Targets.Sum(t => GetTargetPriority(t, character, RequiredSuccessFactor)) * ratio;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
|
||||
}
|
||||
return Targets.Sum(t => GetTargetPriority(t, character, RequiredSuccessFactor)) * ratio;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,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);
|
||||
@@ -148,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; }
|
||||
|
||||
+4
-3
@@ -9,7 +9,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveRescue : AIObjective
|
||||
{
|
||||
public override string DebugTag => "rescue";
|
||||
public override string Identifier { get; set; } = "rescue";
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
|
||||
@@ -374,7 +374,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
{
|
||||
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
|
||||
{
|
||||
@@ -390,6 +390,7 @@ namespace Barotrauma
|
||||
bool isCompleted =
|
||||
AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter) ||
|
||||
targetCharacter.CharacterHealth.GetAllAfflictions().All(a => a.Strength < a.Prefab.TreatmentThreshold);
|
||||
|
||||
if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name),
|
||||
@@ -398,7 +399,7 @@ namespace Barotrauma
|
||||
return isCompleted;
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveRescueAll : AIObjectiveLoop<Character>
|
||||
{
|
||||
public override string DebugTag => "rescue all";
|
||||
public override string Identifier { get; set; } = "rescue all";
|
||||
public override bool ForceRun => true;
|
||||
public override bool InverseTargetEvaluation => true;
|
||||
public override bool AllowOutsideSubmarine => true;
|
||||
|
||||
@@ -150,16 +150,21 @@ namespace Barotrauma
|
||||
//legacy support
|
||||
public readonly string[] AppropriateJobs;
|
||||
public readonly string[] Options;
|
||||
public readonly string[] HiddenOptions;
|
||||
public readonly string[] AllOptions;
|
||||
private readonly Dictionary<string, string> OptionNames;
|
||||
|
||||
public readonly Dictionary<string, Sprite> OptionSprites;
|
||||
|
||||
private readonly Dictionary<string, Sprite> minimapIcons;
|
||||
public Dictionary<string, Sprite> MinimapIcons => IsPrefab ? minimapIcons : Prefab.minimapIcons;
|
||||
|
||||
public readonly bool MustSetTarget;
|
||||
/// <summary>
|
||||
/// Can the order be turned into a non-entity-targeting one if it was originally created with a target entity.
|
||||
/// Note: if MustSetTarget is true, CanBeGeneralized will always be false.
|
||||
/// </summary>
|
||||
public readonly bool CanBeGeneralized;
|
||||
public readonly string AppropriateSkill;
|
||||
public readonly bool Hidden;
|
||||
public readonly bool IgnoreAtOutpost;
|
||||
|
||||
public bool HasOptions => (IsPrefab ? Options : Prefab.Options).Length > 1;
|
||||
public bool IsPrefab { get; private set; }
|
||||
@@ -307,11 +312,15 @@ namespace Barotrauma
|
||||
TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false);
|
||||
AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]);
|
||||
Options = orderElement.GetAttributeStringArray("options", new string[0]);
|
||||
HiddenOptions = orderElement.GetAttributeStringArray("hiddenoptions", new string[0]);
|
||||
AllOptions = Options.Concat(HiddenOptions).ToArray();
|
||||
var category = orderElement.GetAttributeString("category", null);
|
||||
if (!string.IsNullOrWhiteSpace(category)) { this.Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); }
|
||||
MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false);
|
||||
CanBeGeneralized = !MustSetTarget && orderElement.GetAttributeBool("canbegeneralized", true);
|
||||
AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null);
|
||||
Hidden = orderElement.GetAttributeBool("hidden", false);
|
||||
IgnoreAtOutpost = orderElement.GetAttributeBool("ignoreatoutpost", false);
|
||||
|
||||
var optionNames = TextManager.Get("OrderOptions." + Identifier, true)?.Split(',', ',') ??
|
||||
orderElement.GetAttributeStringArray("optionnames", new string[0]);
|
||||
@@ -348,15 +357,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
minimapIcons = new Dictionary<string, Sprite>();
|
||||
var minimapIconElements = orderElement.GetChildElements("minimapicon");
|
||||
foreach (XElement minimapIconElement in minimapIconElements)
|
||||
{
|
||||
var id = minimapIconElement.GetAttributeString("id", null);
|
||||
if (string.IsNullOrWhiteSpace(id)) { continue; }
|
||||
minimapIcons.Add(id, new Sprite(minimapIconElement.GetChildElement("sprite"), lazyLoad: true));
|
||||
}
|
||||
|
||||
IsPrefab = true;
|
||||
MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false);
|
||||
IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis";
|
||||
@@ -366,7 +366,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Constructor for order instances
|
||||
/// </summary>
|
||||
public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false)
|
||||
public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null)
|
||||
{
|
||||
Prefab = prefab.Prefab ?? prefab;
|
||||
|
||||
@@ -384,12 +384,14 @@ namespace Barotrauma
|
||||
AppropriateJobs = prefab.AppropriateJobs;
|
||||
FadeOutTime = prefab.FadeOutTime;
|
||||
MustSetTarget = prefab.MustSetTarget;
|
||||
CanBeGeneralized = prefab.CanBeGeneralized;
|
||||
AppropriateSkill = prefab.AppropriateSkill;
|
||||
Category = prefab.Category;
|
||||
MustManuallyAssign = prefab.MustManuallyAssign;
|
||||
IsIgnoreOrder = prefab.IsIgnoreOrder;
|
||||
DrawIconWhenContained = prefab.DrawIconWhenContained;
|
||||
Hidden = prefab.Hidden;
|
||||
IgnoreAtOutpost = prefab.IgnoreAtOutpost;
|
||||
|
||||
OrderGiver = orderGiver;
|
||||
TargetEntity = targetEntity;
|
||||
@@ -413,12 +415,18 @@ namespace Barotrauma
|
||||
IsPrefab = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for order instances
|
||||
/// </summary>
|
||||
public Order(Order prefab, OrderTarget target, Character orderGiver = null) : this(prefab, targetEntity: null, targetItem: null, orderGiver)
|
||||
{
|
||||
TargetPosition = target;
|
||||
TargetType = OrderTargetType.Position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for order instances
|
||||
/// </summary>
|
||||
public Order(Order prefab, Structure wall, int? sectionIndex, Character orderGiver = null) : this(prefab, targetEntity: wall, null, orderGiver: orderGiver)
|
||||
{
|
||||
WallSectionIndex = sectionIndex;
|
||||
@@ -487,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;
|
||||
@@ -525,7 +525,15 @@ namespace Barotrauma
|
||||
|
||||
public string GetOptionName(string id)
|
||||
{
|
||||
return Prefab == null ? OptionNames[id] : Prefab.OptionNames[id];
|
||||
if (Prefab == null)
|
||||
{
|
||||
if (OptionNames.ContainsKey(id)) { return OptionNames[id]; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Prefab.OptionNames.ContainsKey(id)) { return Prefab.OptionNames[id]; }
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string GetOptionName(int index)
|
||||
|
||||
@@ -410,7 +410,10 @@ namespace Barotrauma
|
||||
if (end.state == 0 || end.Parent == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage("Path not found. " + errorMsgStr, Color.Yellow);
|
||||
if (errorMsgStr != null)
|
||||
{
|
||||
DebugConsole.NewMessage("Path not found. " + errorMsgStr, Color.Yellow);
|
||||
}
|
||||
#endif
|
||||
return new SteeringPath(true);
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ namespace Barotrauma
|
||||
if (c.Inventory != null)
|
||||
{
|
||||
var inventoryElement = new XElement("inventory");
|
||||
c.SaveInventory(c.Inventory, inventoryElement);
|
||||
Character.SaveInventory(c.Inventory, inventoryElement);
|
||||
petElement.Add(inventoryElement);
|
||||
}
|
||||
|
||||
@@ -400,7 +400,7 @@ namespace Barotrauma
|
||||
spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition;
|
||||
}
|
||||
var pet = Character.Create(speciesName, spawnPos, seed);
|
||||
var petBehavior = (pet.AIController as EnemyAIController)?.PetBehavior;
|
||||
var petBehavior = (pet?.AIController as EnemyAIController)?.PetBehavior;
|
||||
if (petBehavior != null)
|
||||
{
|
||||
petBehavior.Owner = owner;
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
abstract class ShipIssueWorker
|
||||
{
|
||||
public const float MaxImportance = 100f;
|
||||
public const float MinImportance = 0f;
|
||||
public Order SuggestedOrderPrefab { get; }
|
||||
|
||||
private float importance;
|
||||
public float Importance
|
||||
{
|
||||
get
|
||||
{
|
||||
return importance;
|
||||
}
|
||||
set
|
||||
{
|
||||
importance = MathHelper.Clamp(value, MinImportance, MaxImportance);
|
||||
}
|
||||
}
|
||||
public float CurrentRedundancy { get; set; }
|
||||
|
||||
public readonly ShipCommandManager shipCommandManager;
|
||||
public string Option { get; set; }
|
||||
public Character OrderedCharacter { get; set; }
|
||||
public Order CurrentOrder { get; private set; }
|
||||
public ItemComponent TargetItemComponent { get; protected set; }
|
||||
public Item TargetItem { get; protected set; }
|
||||
public bool Active { get; protected set; } = true; // used to turn off the instance if errors are detected
|
||||
|
||||
protected virtual Character CommandingCharacter => shipCommandManager.character;
|
||||
public virtual float TimeSinceLastAttempt { get; set; }
|
||||
public virtual float RedundantIssueModifier => 0.5f;
|
||||
public virtual bool StopDuringEmergency => true; // limit certain issue assessments when invaded by the enemies
|
||||
public virtual bool AllowEasySwitching => false;
|
||||
|
||||
public ShipIssueWorker(ShipCommandManager shipCommandManager, Order suggestedOrderPrefab, string option = null)
|
||||
{
|
||||
this.shipCommandManager = shipCommandManager;
|
||||
SuggestedOrderPrefab = suggestedOrderPrefab;
|
||||
Option = option;
|
||||
}
|
||||
|
||||
public void SetOrder(Character orderedCharacter)
|
||||
{
|
||||
OrderedCharacter = orderedCharacter;
|
||||
if (orderedCharacter != CommandingCharacter)
|
||||
{
|
||||
CommandingCharacter.Speak(SuggestedOrderPrefab.GetChatMessage(OrderedCharacter.Name, "", false));
|
||||
}
|
||||
|
||||
// not sure if new orders are supposed to be created each time. TODO m61: check later
|
||||
CurrentOrder = new Order(SuggestedOrderPrefab, TargetItem, TargetItemComponent, CommandingCharacter);
|
||||
OrderedCharacter.SetOrder(CurrentOrder, Option, priority: 3, CommandingCharacter, CommandingCharacter != OrderedCharacter);
|
||||
TimeSinceLastAttempt = 0f;
|
||||
}
|
||||
|
||||
public void RemoveOrder()
|
||||
{
|
||||
OrderedCharacter = null;
|
||||
CurrentOrder = null;
|
||||
}
|
||||
|
||||
protected virtual bool IsIssueViable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public float CalculateImportance(bool isEmergency)
|
||||
{
|
||||
Importance = 0f; // reset anything that needs resetting
|
||||
|
||||
if (!Active)
|
||||
{
|
||||
return Importance;
|
||||
}
|
||||
|
||||
Active = IsIssueViable();
|
||||
|
||||
if (isEmergency && StopDuringEmergency)
|
||||
{
|
||||
return Importance;
|
||||
}
|
||||
|
||||
CalculateImportanceSpecific();
|
||||
|
||||
// if there are other orders of the same type already being attended to, such as fixing leaks
|
||||
// reduce the relative importance of this issue
|
||||
CurrentRedundancy = 1f;
|
||||
foreach (ShipIssueWorker shipIssueWorker in shipCommandManager.ShipIssueWorkers)
|
||||
{
|
||||
if (shipIssueWorker.GetType() == GetType() && shipIssueWorker != this && shipIssueWorker.OrderAttendedTo())
|
||||
{
|
||||
CurrentRedundancy *= RedundantIssueModifier;
|
||||
}
|
||||
}
|
||||
Importance *= CurrentRedundancy;
|
||||
|
||||
return Importance;
|
||||
}
|
||||
|
||||
public bool OrderAttendedTo(float timeSinceLastCheck = 0f)
|
||||
{
|
||||
if (!HumanAIController.IsActive(OrderedCharacter))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// accept only the highest priority order
|
||||
if (CurrentOrder != null && OrderedCharacter.GetCurrentOrderWithTopPriority()?.Order != CurrentOrder)
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandManager.ShipCommandLog($"Order {CurrentOrder.Name} did not match current order for character {OrderedCharacter} in {this}");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!shipCommandManager.AbleToTakeOrder(OrderedCharacter))
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandManager.ShipCommandLog(OrderedCharacter + " was unable to perform assigned order in " + this);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public abstract void CalculateImportanceSpecific();
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class ShipGlobalIssueFixLeaks : ShipGlobalIssue
|
||||
{
|
||||
readonly List<float> hullSeverities = new List<float>();
|
||||
public ShipGlobalIssueFixLeaks(ShipCommandManager shipCommandManager) : base(shipCommandManager) { }
|
||||
public override void CalculateGlobalIssue()
|
||||
{
|
||||
hullSeverities.Clear();
|
||||
|
||||
foreach (Gap gap in Gap.GapList)
|
||||
{
|
||||
if (AIObjectiveFixLeaks.IsValidTarget(gap, shipCommandManager.character))
|
||||
{
|
||||
hullSeverities.Add(AIObjectiveFixLeaks.GetLeakSeverity(gap));
|
||||
}
|
||||
}
|
||||
|
||||
float averagePercentage = 0f;
|
||||
if (hullSeverities.Any())
|
||||
{
|
||||
hullSeverities.Sort();
|
||||
averagePercentage = hullSeverities.TakeLast(3).Average(); // get the 3 most damaged items on the ship and get their average
|
||||
}
|
||||
GlobalImportance = averagePercentage;
|
||||
}
|
||||
}
|
||||
|
||||
class ShipIssueWorkerFixLeaks : ShipIssueWorkerGlobal
|
||||
{
|
||||
public override bool StopDuringEmergency => false;
|
||||
public ShipIssueWorkerFixLeaks(ShipCommandManager shipCommandManager, Order order, ShipGlobalIssueFixLeaks shipGlobalIssueFixLeaks) : base(shipCommandManager, order, shipGlobalIssueFixLeaks) { }
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
namespace Barotrauma
|
||||
{
|
||||
abstract class ShipGlobalIssue
|
||||
{
|
||||
public float GlobalImportance { get; set; }
|
||||
|
||||
protected ShipCommandManager shipCommandManager;
|
||||
public ShipGlobalIssue(ShipCommandManager shipCommandManager)
|
||||
{
|
||||
this.shipCommandManager = shipCommandManager;
|
||||
}
|
||||
public abstract void CalculateGlobalIssue();
|
||||
}
|
||||
|
||||
abstract class ShipIssueWorkerGlobal : ShipIssueWorker
|
||||
{
|
||||
private readonly ShipGlobalIssue shipGlobalIssue;
|
||||
|
||||
public ShipIssueWorkerGlobal(ShipCommandManager shipCommandManager, Order suggestedOrderPrefab, ShipGlobalIssue shipGlobalIssue) : base (shipCommandManager, suggestedOrderPrefab)
|
||||
{
|
||||
this.shipGlobalIssue = shipGlobalIssue;
|
||||
}
|
||||
|
||||
public override void CalculateImportanceSpecific() // importances for global issues are precalculated, so that they don't need to be calculated per each attending character
|
||||
{
|
||||
Importance = shipGlobalIssue.GlobalImportance;
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
using Barotrauma.Items.Components;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
abstract class ShipIssueWorkerItem : ShipIssueWorker
|
||||
{
|
||||
public ShipIssueWorkerItem(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option = null) : base(shipCommandManager, order, option)
|
||||
{
|
||||
TargetItemComponent = targetItemComponent;
|
||||
TargetItem = targetItem;
|
||||
}
|
||||
|
||||
protected override bool IsIssueViable()
|
||||
{
|
||||
if (TargetItemComponent == null)
|
||||
{
|
||||
DebugConsole.ThrowError("TargetItemComponent was null in " + this);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TargetItem == null)
|
||||
{
|
||||
DebugConsole.ThrowError("TargetItem was null in " + this);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class ShipIssueWorkerOperateWeapons : ShipIssueWorkerItem
|
||||
{
|
||||
public override float RedundantIssueModifier => 0.65f;
|
||||
private readonly List<float> targetingImportances = new List<float>();
|
||||
|
||||
public override bool AllowEasySwitching => true;
|
||||
|
||||
public ShipIssueWorkerOperateWeapons(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent) : base(shipCommandManager, order, targetItem, targetItemComponent) { }
|
||||
|
||||
float GetTargetingImportance(Entity entity)
|
||||
{
|
||||
float currentDistanceToEnemy = Vector2.Distance(entity.WorldPosition, TargetItem.WorldPosition);
|
||||
return MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MinImportance, MaxImportance);
|
||||
}
|
||||
|
||||
public override void CalculateImportanceSpecific()
|
||||
{
|
||||
if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot()) { return; }
|
||||
|
||||
targetingImportances.Clear();
|
||||
foreach (Character character in shipCommandManager.EnemyCharacters)
|
||||
{
|
||||
targetingImportances.Add(GetTargetingImportance(character));
|
||||
}
|
||||
// there should maybe be additional logic for targeting and destroying spires, because they currently cause some issues with pathing
|
||||
|
||||
if (targetingImportances.Any())
|
||||
{
|
||||
targetingImportances.Sort();
|
||||
Importance = targetingImportances.TakeLast(3).Average();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
using Barotrauma.Items.Components;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class ShipIssueWorkerPowerUpReactor : ShipIssueWorkerItem
|
||||
{
|
||||
public ShipIssueWorkerPowerUpReactor(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option) : base(shipCommandManager, order, targetItem, targetItemComponent, option)
|
||||
{
|
||||
}
|
||||
|
||||
public override void CalculateImportanceSpecific()
|
||||
{
|
||||
if (TargetItem.Condition <= 0f) { return; }
|
||||
|
||||
if (TargetItemComponent is Reactor reactor && -reactor.CurrPowerConsumption < float.Epsilon)
|
||||
{
|
||||
Importance = 40f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class ShipGlobalIssueRepairSystems : ShipGlobalIssue
|
||||
{
|
||||
readonly List<Item> itemsNeedingRepair = new List<Item>();
|
||||
|
||||
public ShipGlobalIssueRepairSystems(ShipCommandManager shipCommandManager) : base(shipCommandManager) { }
|
||||
|
||||
public override void CalculateGlobalIssue()
|
||||
{
|
||||
itemsNeedingRepair.Clear();
|
||||
|
||||
foreach (Item item in shipCommandManager.CommandedSubmarine.GetItems(true))
|
||||
{
|
||||
if (!AIObjectiveRepairItems.ViableForRepair(item, shipCommandManager.character, shipCommandManager.character.AIController as HumanAIController)) { continue; }
|
||||
if (AIObjectiveRepairItems.NearlyFullCondition(item)) { continue; }
|
||||
itemsNeedingRepair.Add(item);
|
||||
// merged this logic with AIObjectiveRepairItems
|
||||
}
|
||||
|
||||
if (itemsNeedingRepair.Any())
|
||||
{
|
||||
itemsNeedingRepair.Sort((x, y) => y.ConditionPercentage.CompareTo(x.ConditionPercentage));
|
||||
float modifiedPercentage = itemsNeedingRepair.TakeLast(3).Average(x => x.ConditionPercentage) * 0.6f + itemsNeedingRepair.TakeLast(10).Average(x => x.ConditionPercentage) * 0.4f;
|
||||
// calculate a modified percentage with the most damaged items, with 60% the weight given to the top 3 damaged and the remaining given to top 10
|
||||
GlobalImportance = 100 - modifiedPercentage;
|
||||
}
|
||||
// this system works reasonably well, though it could give extra importance to repairing critical items like reactors and junction boxes
|
||||
}
|
||||
}
|
||||
|
||||
class ShipIssueWorkerRepairSystems : ShipIssueWorkerGlobal // this class could be removed, but it might need special behavior later
|
||||
{
|
||||
public ShipIssueWorkerRepairSystems(ShipCommandManager shipCommandManager, Order order, ShipGlobalIssueRepairSystems shipGlobalIssueRepairSystems) : base(shipCommandManager, order, shipGlobalIssueRepairSystems)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
using Barotrauma.Items.Components;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class ShipIssueWorkerSteer : ShipIssueWorkerItem
|
||||
{
|
||||
// The AI could be set to steer automatically through a specialized job or autonomous objectives
|
||||
// but the logic involved doesn't really allow that without some annoyingly specific changes
|
||||
// hence the AI will command itself to steer if steering is not being taken care of or the target location is wrong
|
||||
public ShipIssueWorkerSteer(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option) : base(shipCommandManager, order, targetItem, targetItemComponent, option) { }
|
||||
public override void CalculateImportanceSpecific()
|
||||
{
|
||||
if (shipCommandManager.NavigationState == ShipCommandManager.NavigationStates.Inactive) { return; }
|
||||
if (TargetItemComponent is Powered powered && powered.Voltage <= powered.MinVoltage) { return; }
|
||||
if (TargetItem.Condition <= 0f) { return; }
|
||||
|
||||
Importance = 70f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class ShipCommandManager
|
||||
{
|
||||
public readonly Character character;
|
||||
public readonly HumanAIController humanAIController;
|
||||
|
||||
private bool active;
|
||||
public bool Active
|
||||
{
|
||||
get { return active; }
|
||||
set
|
||||
{
|
||||
active = value ? TryInitializeShipCommandManager() : value;
|
||||
}
|
||||
}
|
||||
|
||||
public Submarine EnemySubmarine
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Submarine CommandedSubmarine
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
private Steering steering;
|
||||
public readonly List<Vector2> patrolPositions = new List<Vector2>();
|
||||
public enum NavigationStates
|
||||
{
|
||||
Inactive,
|
||||
Patrol,
|
||||
Aggressive
|
||||
}
|
||||
|
||||
public NavigationStates NavigationState { get; private set; } = NavigationStates.Inactive;
|
||||
|
||||
float navigationTimer = 0f;
|
||||
private readonly float navigationInterval = 4f;
|
||||
|
||||
float timeUntilRam;
|
||||
private const float RamTimerMax = 17.5f;
|
||||
|
||||
public readonly List<ShipIssueWorker> ShipIssueWorkers = new List<ShipIssueWorker>();
|
||||
private const float MinimumIssueThreshold = 10f;
|
||||
private const float IssueDevotionBuffer = 5f;
|
||||
|
||||
private float decisionTimer = 6f;
|
||||
private readonly float decisionInterval = 6f;
|
||||
|
||||
private float timeSinceLastCommandDecision;
|
||||
private float timeSinceLastNavigation;
|
||||
|
||||
public readonly List<Character> AlliedCharacters = new List<Character>();
|
||||
public readonly List<Character> EnemyCharacters = new List<Character>();
|
||||
|
||||
private readonly List<ShipIssueWorker> attendedIssues = new List<ShipIssueWorker>();
|
||||
private readonly List<ShipIssueWorker> availableIssues = new List<ShipIssueWorker>();
|
||||
private readonly List<ShipGlobalIssue> shipGlobalIssues = new List<ShipGlobalIssue>();
|
||||
|
||||
public ShipCommandManager(Character character)
|
||||
{
|
||||
this.character = character;
|
||||
humanAIController = character.AIController as HumanAIController;
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
if (!Active) { return; }
|
||||
decisionTimer -= deltaTime;
|
||||
if (decisionTimer <= 0.0f)
|
||||
{
|
||||
UpdateCommandDecision(timeSinceLastCommandDecision);
|
||||
decisionTimer = decisionInterval * Rand.Range(0.8f, 1.2f);
|
||||
timeSinceLastCommandDecision = decisionTimer;
|
||||
}
|
||||
|
||||
navigationTimer -= deltaTime;
|
||||
if (navigationTimer <= 0.0f)
|
||||
{
|
||||
UpdateNavigation(timeSinceLastNavigation);
|
||||
navigationTimer = navigationInterval * Rand.Range(0.8f, 1.2f);
|
||||
timeSinceLastNavigation = navigationTimer;
|
||||
}
|
||||
}
|
||||
|
||||
public static void ShipCommandLog(string text)
|
||||
{
|
||||
if (GameSettings.VerboseLogging)
|
||||
{
|
||||
DebugConsole.NewMessage(text);
|
||||
}
|
||||
}
|
||||
|
||||
static bool WithinRange(float range, float distanceSquared)
|
||||
{
|
||||
return range * range > distanceSquared;
|
||||
}
|
||||
|
||||
void UpdateNavigation(float timeSinceLastUpdate)
|
||||
{
|
||||
if (steering == null || EnemySubmarine == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float distanceSquaredEnemy = Vector2.DistanceSquared(CommandedSubmarine.WorldPosition, EnemySubmarine.WorldPosition);
|
||||
|
||||
if (NavigationState != NavigationStates.Aggressive)
|
||||
{
|
||||
if (WithinRange(7000f, distanceSquaredEnemy))
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandLog("Ship " + CommandedSubmarine + " was within the aggro range of " + EnemySubmarine);
|
||||
#endif
|
||||
NavigationState = NavigationStates.Aggressive;
|
||||
}
|
||||
else if (WithinRange(40000f, distanceSquaredEnemy))
|
||||
{
|
||||
NavigationState = NavigationStates.Patrol;
|
||||
}
|
||||
}
|
||||
|
||||
if (NavigationState == NavigationStates.Aggressive)
|
||||
{
|
||||
steering.AITacticalTarget = EnemySubmarine.WorldPosition;
|
||||
if (WithinRange(8500f, distanceSquaredEnemy) && !WithinRange(1500f, distanceSquaredEnemy)) // if we are within enemy ship's range for ramTimerMax, try to ram them instead (if we're not already very close)
|
||||
{
|
||||
if (steering.AIRamTimer > 0f)
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandLog("Ship " + CommandedSubmarine + " was still ramming, " + steering.AIRamTimer + " left");
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
timeUntilRam -= timeSinceLastUpdate;
|
||||
#if DEBUG
|
||||
ShipCommandLog("Ship " + CommandedSubmarine + " was close enough to ram, " + timeUntilRam + " left until ramming");
|
||||
#endif
|
||||
|
||||
if (timeUntilRam <= 0f)
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandLog("Ship " + CommandedSubmarine + " is attempting to ram!");
|
||||
#endif
|
||||
steering.AIRamTimer = 50f;
|
||||
timeUntilRam = RamTimerMax * Rand.Range(0.9f, 1.1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
steering.AIRamTimer = 0f;
|
||||
timeUntilRam = RamTimerMax * Rand.Range(0.9f, 1.1f);
|
||||
}
|
||||
}
|
||||
else if (patrolPositions.Any())
|
||||
{
|
||||
float distanceSquaredPatrol = Vector2.DistanceSquared(CommandedSubmarine.WorldPosition, patrolPositions.First());
|
||||
|
||||
if (WithinRange(7000f, distanceSquaredPatrol))
|
||||
{
|
||||
Vector2 lastPosition = patrolPositions.First();
|
||||
patrolPositions.RemoveAt(0);
|
||||
patrolPositions.Add(lastPosition);
|
||||
}
|
||||
steering.AITacticalTarget = patrolPositions.First();
|
||||
}
|
||||
}
|
||||
|
||||
public bool AbleToTakeOrder(Character character)
|
||||
{
|
||||
return !character.IsIncapacitated && !character.LockHands && character.Submarine == CommandedSubmarine;
|
||||
}
|
||||
|
||||
void UpdateCommandDecision(float timeSinceLastUpdate)
|
||||
{
|
||||
|
||||
#if DEBUG
|
||||
ShipCommandLog("Updating command for character " + character);
|
||||
#endif
|
||||
|
||||
shipGlobalIssues.ForEach(c => c.CalculateGlobalIssue());
|
||||
|
||||
AlliedCharacters.Clear();
|
||||
EnemyCharacters.Clear();
|
||||
|
||||
bool isEmergency = false;
|
||||
|
||||
foreach (Character potentialCharacter in Character.CharacterList)
|
||||
{
|
||||
if (!HumanAIController.IsActive(character)) { continue; }
|
||||
|
||||
if (HumanAIController.IsFriendly(character, potentialCharacter, true) && potentialCharacter.AIController is HumanAIController)
|
||||
{
|
||||
if (AbleToTakeOrder(potentialCharacter))
|
||||
{
|
||||
AlliedCharacters.Add(potentialCharacter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EnemyCharacters.Add(potentialCharacter);
|
||||
if (potentialCharacter.Submarine == CommandedSubmarine) // if enemies are on board, don't issue normal orders anymore
|
||||
{
|
||||
isEmergency = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attendedIssues.Clear();
|
||||
availableIssues.Clear();
|
||||
|
||||
foreach (ShipIssueWorker shipIssueWorker in ShipIssueWorkers)
|
||||
{
|
||||
float importance = shipIssueWorker.CalculateImportance(isEmergency);
|
||||
if (shipIssueWorker.OrderAttendedTo(timeSinceLastUpdate))
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it was already being attended by " + shipIssueWorker.OrderedCharacter);
|
||||
#endif
|
||||
attendedIssues.Add(shipIssueWorker);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it is not attended to");
|
||||
#endif
|
||||
shipIssueWorker.RemoveOrder();
|
||||
availableIssues.Add(shipIssueWorker);
|
||||
}
|
||||
}
|
||||
|
||||
availableIssues.Sort((x, y) => y.Importance.CompareTo(x.Importance));
|
||||
attendedIssues.Sort((x, y) => x.Importance.CompareTo(y.Importance));
|
||||
|
||||
ShipIssueWorker mostImportantIssue = availableIssues.FirstOrDefault();
|
||||
|
||||
float bestValue = 0f;
|
||||
Character bestCharacter = null;
|
||||
|
||||
if (mostImportantIssue != null && mostImportantIssue.Importance > MinimumIssueThreshold)
|
||||
{
|
||||
IEnumerable<Character> bestCharacters = CrewManager.GetCharactersSortedForOrder(mostImportantIssue.SuggestedOrderPrefab, AlliedCharacters, character, true);
|
||||
|
||||
foreach (Character orderedCharacter in bestCharacters)
|
||||
{
|
||||
float issueApplicability = mostImportantIssue.Importance;
|
||||
|
||||
// prefer not to switch if not qualified
|
||||
issueApplicability *= mostImportantIssue.SuggestedOrderPrefab.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 1f : 0.75f;
|
||||
|
||||
ShipIssueWorker occupiedIssue = attendedIssues.FirstOrDefault(i => i.OrderedCharacter == orderedCharacter);
|
||||
|
||||
if (occupiedIssue != null)
|
||||
{
|
||||
if (occupiedIssue.GetType() == mostImportantIssue.GetType() && mostImportantIssue is ShipIssueWorkerGlobal && occupiedIssue is ShipIssueWorkerGlobal)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// reverse redundancy to ensure certain issues can be switched over easily (operating weapons)
|
||||
if (mostImportantIssue.AllowEasySwitching && occupiedIssue.AllowEasySwitching)
|
||||
{
|
||||
issueApplicability /= mostImportantIssue.CurrentRedundancy;
|
||||
}
|
||||
|
||||
// give slight preference if not qualified for current job
|
||||
issueApplicability += occupiedIssue.SuggestedOrderPrefab.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 0 : 7.5f;
|
||||
|
||||
// prefer not to switch orders unless considerably more important
|
||||
issueApplicability -= IssueDevotionBuffer;
|
||||
|
||||
if (issueApplicability + IssueDevotionBuffer < occupiedIssue.Importance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// prefer first one in bestCharacters in tiebreakers
|
||||
if (issueApplicability > bestValue)
|
||||
{
|
||||
bestValue = issueApplicability;
|
||||
bestCharacter = orderedCharacter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestCharacter != null && mostImportantIssue != null)
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandLog("Setting " + mostImportantIssue + " for character " + bestCharacter);
|
||||
#endif
|
||||
mostImportantIssue.SetOrder(bestCharacter);
|
||||
}
|
||||
else // if we didn't give an order, let's try to dismiss someone instead
|
||||
{
|
||||
foreach (ShipIssueWorker shipIssueWorker in ShipIssueWorkers)
|
||||
{
|
||||
if (shipIssueWorker.Importance <= 0f && shipIssueWorker.OrderAttendedTo())
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandLog("Dismissing " + shipIssueWorker + " for character " + shipIssueWorker.OrderedCharacter);
|
||||
#endif
|
||||
Order orderPrefab = Order.GetPrefab("dismissed");
|
||||
character.Speak(orderPrefab.GetChatMessage(shipIssueWorker.OrderedCharacter.Name, "", givingOrderToSelf: false));
|
||||
shipIssueWorker.OrderedCharacter.SetOrder(Order.GetPrefab("dismissed"), orderOption: null, priority: 3, character);
|
||||
shipIssueWorker.RemoveOrder();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool TryInitializeShipCommandManager()
|
||||
{
|
||||
CommandedSubmarine = character.Submarine;
|
||||
|
||||
if (CommandedSubmarine == null)
|
||||
{
|
||||
DebugConsole.ThrowError("TryInitializeShipCommandManager failed: CommandedSubmarine was null for character " + character);
|
||||
return false;
|
||||
}
|
||||
|
||||
EnemySubmarine = Submarine.MainSubs[0] == CommandedSubmarine ? Submarine.MainSubs[1] : Submarine.MainSubs[0];
|
||||
|
||||
if (EnemySubmarine == null)
|
||||
{
|
||||
DebugConsole.ThrowError("TryInitializeShipCommandManager failed: EnemySubmarine was null for character " + character);
|
||||
return false;
|
||||
}
|
||||
|
||||
timeUntilRam = RamTimerMax * Rand.Range(0.9f, 1.1f);
|
||||
|
||||
ShipIssueWorkers.Clear();
|
||||
|
||||
// could have support for multiple reactors, todo m61
|
||||
if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
|
||||
{
|
||||
ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, Order.GetPrefab("operatereactor"), reactor.Item, reactor, "powerup"));
|
||||
}
|
||||
|
||||
if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("navterminal") && !i.NonInteractable) is Item nav && nav.GetComponent<Steering>() is Steering steeringComponent)
|
||||
{
|
||||
steering = steeringComponent;
|
||||
ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, Order.GetPrefab("steer"), nav, steeringComponent, "navigatetactical"));
|
||||
}
|
||||
|
||||
foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret")))
|
||||
{
|
||||
ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, Order.GetPrefab("operateweapons"), item, item.GetComponent<Turret>()));
|
||||
}
|
||||
|
||||
int crewSizeModifier = 2;
|
||||
// these issueworkers revolve around a singular, shared issue, which is injected into them to prevent redundant calculations
|
||||
ShipGlobalIssueFixLeaks shipGlobalIssueFixLeaks = new ShipGlobalIssueFixLeaks(this);
|
||||
for (int i = 0; i < crewSizeModifier; i++)
|
||||
{
|
||||
ShipIssueWorkers.Add(new ShipIssueWorkerFixLeaks(this, Order.GetPrefab("fixleaks"), shipGlobalIssueFixLeaks));
|
||||
}
|
||||
shipGlobalIssues.Add(shipGlobalIssueFixLeaks);
|
||||
|
||||
ShipGlobalIssueRepairSystems shipGlobalIssueRepairSystems = new ShipGlobalIssueRepairSystems(this);
|
||||
for (int i = 0; i < crewSizeModifier; i++)
|
||||
{
|
||||
ShipIssueWorkers.Add(new ShipIssueWorkerRepairSystems(this, Order.GetPrefab("repairsystems"), shipGlobalIssueRepairSystems));
|
||||
}
|
||||
shipGlobalIssues.Add(shipGlobalIssueRepairSystems);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,14 @@ namespace Barotrauma
|
||||
|
||||
private static bool IsThalamus(MapEntityPrefab entityPrefab, string tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag);
|
||||
|
||||
public WreckAI(Submarine wreck)
|
||||
public static WreckAI Create(Submarine wreck)
|
||||
{
|
||||
var wreckAI = new WreckAI(wreck);
|
||||
if (wreckAI.Config == null) { return null; }
|
||||
return wreckAI;
|
||||
}
|
||||
|
||||
private WreckAI(Submarine wreck)
|
||||
{
|
||||
Wreck = wreck;
|
||||
Config = WreckAIConfig.GetRandom();
|
||||
@@ -55,37 +62,59 @@ namespace Barotrauma
|
||||
}
|
||||
allItems = Wreck.GetItems(false);
|
||||
thalamusItems = allItems.FindAll(i => IsThalamus(i.prefab));
|
||||
var hulls = Wreck.GetHulls(false);
|
||||
hulls.AddRange(Wreck.GetHulls(false));
|
||||
var potentialBrainHulls = new Dictionary<Hull, float>();
|
||||
brain = new Item(brainPrefab, Vector2.Zero, Wreck);
|
||||
thalamusItems.Add(brain);
|
||||
Vector2 negativeMargin = new Vector2(40, 20);
|
||||
Vector2 minSize = brain.Rect.Size.ToVector2() - negativeMargin;
|
||||
Vector2 maxSize = new Vector2(brain.Rect.Width * 3, brain.Rect.Height * 3);
|
||||
// First try to get a room that is not too big and not in the edges of the sub.
|
||||
// Also try not to create the brain in a room that already have carrier items inside.
|
||||
// Ignore hulls that have any linked hulls to keep the calculations simple.
|
||||
Point minSize = brain.Rect.Size.Multiply(brain.Scale);
|
||||
// Bigger hulls are allowed, but not preferred more than what's sufficent.
|
||||
Vector2 sufficentSize = new Vector2(minSize.X * 2, minSize.Y * 1.1f);
|
||||
// Shrink the horizontal axis so that the brain is not placed in the left or right side, where we often have curved walls.
|
||||
// Also ignore hulls that have open gaps, because we'll want the room to be full of water. The room will be filled with water when the brain is inserted in the room.
|
||||
Rectangle shrinkedBounds = ToolBox.GetWorldBounds(Wreck.WorldPosition.ToPoint(), new Point(Wreck.Borders.Width - 500, Wreck.Borders.Height));
|
||||
bool BaseCondition(Hull h) => h.RectWidth > minSize.X && h.RectHeight > minSize.Y && h.GetLinkedEntities<Hull>().None() && h.ConnectedGaps.None(g => g.Open > 0);
|
||||
bool IsNotTooBig(Hull h) => h.RectWidth < maxSize.X && h.RectHeight < maxSize.Y;
|
||||
bool IsNotInFringes(Hull h) => shrinkedBounds.ContainsWorld(h.WorldRect);
|
||||
bool DoesNotContainOtherItems(Hull h) => thalamusItems.None(i => i.CurrentHull == h);
|
||||
Hull brainHull = hulls.GetRandom(h => BaseCondition(h) && IsNotTooBig(h) && IsNotInFringes(h) && DoesNotContainOtherItems(h), Rand.RandSync.Server);
|
||||
if (brainHull == null)
|
||||
foreach (Hull hull in hulls)
|
||||
{
|
||||
brainHull = hulls.GetRandom(h => BaseCondition(h) && IsNotInFringes(h) && DoesNotContainOtherItems(h), Rand.RandSync.Server);
|
||||
}
|
||||
if (brainHull == null)
|
||||
{
|
||||
brainHull = hulls.GetRandom(h => BaseCondition(h) && (IsNotInFringes(h) || DoesNotContainOtherItems(h)), Rand.RandSync.Server);
|
||||
}
|
||||
if (brainHull == null)
|
||||
{
|
||||
brainHull = hulls.GetRandom(BaseCondition, Rand.RandSync.Server);
|
||||
float distanceFromCenter = Vector2.Distance(Wreck.WorldPosition, hull.WorldPosition);
|
||||
float distanceFactor = MathHelper.Lerp(1.0f, 0.5f, MathUtils.InverseLerp(0, Math.Max(shrinkedBounds.Width, shrinkedBounds.Height) / 2, distanceFromCenter));
|
||||
float horizontalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.X, sufficentSize.X, hull.Rect.Width));
|
||||
float verticalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.Y, sufficentSize.Y, hull.Rect.Height));
|
||||
float weight = verticalSizeFactor * horizontalSizeFactor * distanceFactor;
|
||||
if (hull.GetLinkedEntities<Hull>().Any())
|
||||
{
|
||||
// Ignore hulls that have any linked hulls to keep the calculations simple.
|
||||
continue;
|
||||
}
|
||||
else if (hull.ConnectedGaps.Any(g => g.Open > 0 && (!g.IsRoomToRoom || g.Position.Y < hull.Position.Y)))
|
||||
{
|
||||
// Ignore hulls that have open gaps to outside or below the center point, because we'll want the room to be full of water and not be accessible without breaking the wall.
|
||||
continue;
|
||||
}
|
||||
else if (thalamusItems.Any(i => i.CurrentHull == hull))
|
||||
{
|
||||
// Don't create the brain in a room that already has thalamus items inside it.
|
||||
continue;
|
||||
}
|
||||
else if (hull.Rect.Width < minSize.X || hull.Rect.Height < minSize.Y)
|
||||
{
|
||||
// Don't select too small rooms.
|
||||
continue;
|
||||
}
|
||||
if (weight > 0)
|
||||
{
|
||||
potentialBrainHulls.TryAdd(hull, weight);
|
||||
}
|
||||
}
|
||||
Hull brainHull = ToolBox.SelectWeightedRandom(potentialBrainHulls.Keys.ToList(), potentialBrainHulls.Values.ToList(), Rand.RandSync.Server);
|
||||
var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(p => IsThalamus(p));
|
||||
if (brainHull == null) { return; }
|
||||
if (brainHull == null)
|
||||
{
|
||||
DebugConsole.AddWarning("Wreck AI: Cannot find a proper room for the brain. Using a random room.");
|
||||
brainHull = hulls.GetRandom(Rand.RandSync.Server);
|
||||
}
|
||||
if (brainHull == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Wreck AI: Cannot find any room for the brain! Failed to create the Thalamus.");
|
||||
return;
|
||||
}
|
||||
brainHull.WaterVolume = brainHull.Volume;
|
||||
brain.SetTransform(brainHull.SimPosition, rotation: 0, findNewHull: false);
|
||||
brain.CurrentHull = brainHull;
|
||||
@@ -158,11 +187,12 @@ namespace Barotrauma
|
||||
if (!spawnOrgans.Contains(item))
|
||||
{
|
||||
spawnOrgans.Add(item);
|
||||
// Try to flood the hull so that the spawner won't die.
|
||||
item.CurrentHull.WaterVolume = item.CurrentHull.Volume;
|
||||
}
|
||||
}
|
||||
}
|
||||
wayPoints.AddRange(Wreck.GetWaypoints(false));
|
||||
hulls.AddRange(Wreck.GetHulls(false));
|
||||
IsAlive = true;
|
||||
thalamusStructures = GetThalamusEntities<Structure>(Wreck, Config.Entity).ToList();
|
||||
}
|
||||
@@ -307,9 +337,16 @@ namespace Barotrauma
|
||||
|
||||
public static void RemoveThalamusItems(Submarine wreck)
|
||||
{
|
||||
List<MapEntity> thalamusItems = new List<MapEntity>();
|
||||
foreach (var wreckAiConfig in WreckAIConfig.List)
|
||||
{
|
||||
GetThalamusEntities(wreck, wreckAiConfig.Entity).ForEachMod(e => e.Remove());
|
||||
thalamusItems.AddRange(GetThalamusEntities(wreck, wreckAiConfig.Entity));
|
||||
}
|
||||
thalamusItems = thalamusItems.Distinct().ToList();
|
||||
foreach (MapEntity thalamusItem in thalamusItems)
|
||||
{
|
||||
thalamusItem.Remove();
|
||||
wreck.PhysicsBody.FarseerBody.FixtureList.Where(f => f.UserData == thalamusItem).ForEachMod(f => wreck.PhysicsBody.FarseerBody.Remove(f));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,15 +360,16 @@ namespace Barotrauma
|
||||
private int MaxCellsPerRoom => CalculateCellCount(1, Config.MaxAgentsPerRoom);
|
||||
private int MinCellsOutside => CalculateCellCount(0, Config.MinAgentsOutside);
|
||||
private int MaxCellsOutside => CalculateCellCount(0, Config.MaxAgentsOutside);
|
||||
private int MinCellsInside => CalculateCellCount(2, Config.MinAgentsInside);
|
||||
private int MaxCellsInside => CalculateCellCount(3, Config.MaxAgentsInside);
|
||||
private int MinCellsInside => CalculateCellCount(3, Config.MinAgentsInside);
|
||||
private int MaxCellsInside => CalculateCellCount(5, Config.MaxAgentsInside);
|
||||
private int MaxCellCount => CalculateCellCount(5, Config.MaxAgentCount);
|
||||
private float MinWaterLevel => Config.MinWaterLevel;
|
||||
|
||||
private int CalculateCellCount(int minValue, int maxValue)
|
||||
{
|
||||
if (maxValue == 0) { return 0; }
|
||||
return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, Level.Loaded.Difficulty * 0.01f * Config.AgentSpawnCountDifficultyMultiplier));
|
||||
float t = MathUtils.InverseLerp(0, 100, Level.Loaded.Difficulty * Config.AgentSpawnCountDifficultyMultiplier);
|
||||
return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, t));
|
||||
}
|
||||
|
||||
private float GetSpawnTime()
|
||||
|
||||
Reference in New Issue
Block a user