v0.10.6.2

This commit is contained in:
Joonas Rikkonen
2020-10-29 17:55:26 +02:00
parent 20a69375ca
commit bbf06f0984
255 changed files with 6196 additions and 3096 deletions
@@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace Barotrauma
{
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect }
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow }
abstract partial class AIController : ISteerable
{
@@ -147,6 +147,8 @@ namespace Barotrauma
_selectedAiTarget = null;
}
public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left;
protected virtual void OnStateChanged(AIState from, AIState to) { }
protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { }
@@ -38,7 +38,7 @@ namespace Barotrauma
{
if (float.IsNaN(value))
{
DebugConsole.ThrowError("Attempted to set the SoundRange of an AITarget to NaN.\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to set the SoundRange of an AITarget to NaN.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
soundRange = MathHelper.Clamp(value, MinSoundRange, MaxSoundRange);
@@ -52,7 +52,7 @@ namespace Barotrauma
{
if (float.IsNaN(value))
{
DebugConsole.ThrowError("Attempted to set the SightRange of an AITarget to NaN.\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to set the SightRange of an AITarget to NaN.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange);
@@ -74,7 +74,7 @@ namespace Barotrauma
{
if (!MathUtils.IsValid(value))
{
string errorMsg = "Invalid AITarget sector direction (" + value + ")\n" + Environment.StackTrace;
string errorMsg = "Invalid AITarget sector direction (" + value + ")\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AITarget.SectorDir:" + entity?.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return;
@@ -113,11 +113,11 @@ namespace Barotrauma
if (entity == null || entity.Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("AITarget.WorldPosition:EntityRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed AITarget\n" + Environment.StackTrace);
"Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace());
return Vector2.Zero;
}
@@ -132,11 +132,11 @@ namespace Barotrauma
if (entity == null || entity.Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("AITarget.WorldPosition:EntityRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed AITarget\n" + Environment.StackTrace);
"Attempted to access a removed AITarget\n" + Environment.StackTrace.CleanupStackTrace());
return Vector2.Zero;
}
@@ -48,13 +48,13 @@ namespace Barotrauma
private float attackLimbResetTimer;
private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0;
public float CombatStrength => Character.Params.AI.CombatStrength;
private float Sight => Character.Params.AI.Sight;
private float Hearing => Character.Params.AI.Hearing;
private float FleeHealthThreshold => Character.Params.AI.FleeHealthThreshold;
private float AggressionGreed => Character.Params.AI.AggressionGreed;
private float AggressionHurt => Character.Params.AI.AggressionHurt;
private bool AggressiveBoarding => Character.Params.AI.AggressiveBoarding;
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 FishAnimController FishAnimController => Character.AnimController as FishAnimController;
//a point in a wall which the Character is currently targeting
private WallTarget wallTarget;
@@ -70,10 +70,6 @@ namespace Barotrauma
_attackingLimb = value;
attackVector = null;
Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse;
if (Character.AnimController is FishAnimController fishController)
{
fishController.reverse = Reverse;
}
}
}
@@ -92,14 +88,15 @@ namespace Barotrauma
private readonly float priorityFearIncreasement = 2;
private readonly float memoryFadeTime = 0.5f;
private readonly float avoidTime = 3;
private float avoidTimer;
private float observeTimer;
public bool StayInsideLevel = true;
public LatchOntoAI LatchOntoAI { get; private set; }
public SwarmBehavior SwarmBehavior { get; private set; }
public PetBehavior PetBehavior { get; private set; }
public CharacterParams.TargetParams SelectedTargetingParams { get { return selectedTargetingParams; } }
@@ -107,7 +104,7 @@ namespace Barotrauma
{
get
{
var target = GetTarget(CharacterPrefab.HumanSpeciesName);
var target = GetTargetParams(CharacterPrefab.HumanSpeciesName);
return target != null && target.Priority > 0.0f && (target.State == AIState.Attack || target.State == AIState.Aggressive);
}
}
@@ -116,7 +113,7 @@ namespace Barotrauma
{
get
{
var target = GetTarget("room");
var target = GetTargetParams("room");
return target != null && target.Priority > 0.0f && (target.State == AIState.Attack || target.State == AIState.Aggressive);
}
}
@@ -145,7 +142,19 @@ namespace Barotrauma
public bool IsBeingChasedBy(Character c) => c.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity is Character && (enemyAI.State == AIState.Aggressive || enemyAI.State == AIState.Attack);
private bool IsBeingChased => SelectedAiTarget?.Entity is Character targetCharacter && IsBeingChasedBy(targetCharacter);
public bool Reverse { get; private set; }
private bool reverse;
public bool Reverse
{
get { return reverse; }
private set
{
reverse = value;
if (FishAnimController != null)
{
FishAnimController.reverse = reverse;
}
}
}
public EnemyAIController(Character c, string seed) : base(c)
{
@@ -196,6 +205,9 @@ namespace Barotrauma
case "swarmbehavior":
SwarmBehavior = new SwarmBehavior(subElement, this);
break;
case "petbehavior":
PetBehavior = new PetBehavior(subElement, this);
break;
}
}
@@ -213,8 +225,74 @@ namespace Barotrauma
avoidLookAheadDistance = Math.Max(colliderWidth * 3, 1.5f);
}
private CharacterParams.AIParams AIParams => Character.Params.AI;
private CharacterParams.TargetParams GetTarget(string targetTag) => AIParams.GetTarget(targetTag, false);
public CharacterParams.AIParams AIParams => Character.Params.AI;
private CharacterParams.TargetParams GetTargetParams(string targetTag) => AIParams.GetTarget(targetTag, false);
private CharacterParams.TargetParams GetTargetParams(AITarget aiTarget) => GetTargetParams(GetTargetingTag(aiTarget));
private string GetTargetingTag(AITarget aiTarget)
{
if (aiTarget.Entity == null) { return null; }
string targetingTag = null;
if (aiTarget.Entity is Character targetCharacter)
{
if (targetCharacter.IsDead)
{
targetingTag = "dead";
}
else if (PetBehavior != null && aiTarget.Entity == PetBehavior.Owner)
{
targetingTag = "owner";
}
else if (AIParams.TryGetTarget(targetCharacter.SpeciesName, out CharacterParams.TargetParams tP))
{
targetingTag = tP.Tag;
}
else if (targetCharacter.AIController is EnemyAIController enemy)
{
if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
{
targetingTag = "husk";
}
else
{
if (enemy.CombatStrength > CombatStrength)
{
targetingTag = "stronger";
}
else if (enemy.CombatStrength < CombatStrength)
{
targetingTag = "weaker";
}
}
}
}
else if (aiTarget.Entity is Item targetItem)
{
foreach (var prio in AIParams.Targets)
{
if (targetItem.HasTag(prio.Tag))
{
targetingTag = prio.Tag;
break;
}
}
if (targetingTag == null)
{
if (targetItem.GetComponent<Sonar>() != null)
{
targetingTag = "sonar";
}
}
}
else if (aiTarget.Entity is Structure)
{
targetingTag = "wall";
}
else if (aiTarget.Entity is Hull)
{
targetingTag = "room";
}
return targetingTag;
}
public override void SelectTarget(AITarget target) => SelectTarget(target, 100);
@@ -295,8 +373,9 @@ namespace Barotrauma
updateMemoriesTimer = updateMemoriesInverval;
}
if (Character.HealthPercentage <= FleeHealthThreshold && SelectedAiTarget != null &&
SelectedAiTarget.Entity is Character target && (target.IsPlayer || IsBeingChasedBy(target)))
SelectedAiTarget.Entity is Character target && (target.IsHuman && CanPerceive(SelectedAiTarget) || IsBeingChasedBy(target)))
{
// Keep fleeing if being chased
State = AIState.Flee;
wallTarget = null;
}
@@ -306,7 +385,7 @@ namespace Barotrauma
{
updateTargetsTimer -= deltaTime;
}
else
else if (avoidTimer <= 0)
{
CharacterParams.TargetParams targetingParams = null;
UpdateTargets(Character, out targetingParams);
@@ -315,11 +394,7 @@ namespace Barotrauma
UpdateWallTarget();
}
updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f);
if (avoidTimer > 0)
{
State = AIState.Escape;
}
else if (SelectedAiTarget == null)
if (SelectedAiTarget == null)
{
State = AIState.Idle;
}
@@ -348,9 +423,13 @@ namespace Barotrauma
steeringManager = insideSteering;
}
bool useSteeringLengthAsMovementSpeed = State == AIState.Idle && Character.AnimController.InWater;
bool run = false;
switch (State)
{
case AIState.Freeze:
SteeringManager.Reset();
break;
case AIState.Idle:
UpdateIdle(deltaTime);
break;
@@ -420,17 +499,21 @@ namespace Barotrauma
}
break;
case AIState.Protect:
case AIState.Follow:
if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed)
{
State = AIState.Idle;
return;
}
if (SelectedAiTarget.Entity is Character targetCharacter && targetCharacter.LastAttacker is Character attacker)
if (State == AIState.Protect)
{
// Attack the character that attacked the target we are protecting
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
SelectTarget(attacker.AiTarget);
return;
if (SelectedAiTarget.Entity is Character targetCharacter && targetCharacter.LastAttacker is Character attacker && !attacker.Removed && !attacker.IsDead)
{
// Attack the character that attacked the target we are protecting
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
SelectTarget(attacker.AiTarget);
return;
}
}
float sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition);
float reactDist = selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget);
@@ -446,6 +529,64 @@ namespace Barotrauma
UpdateIdle(deltaTime);
}
break;
case AIState.Observe:
if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed)
{
State = AIState.Idle;
return;
}
run = false;
sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition);
reactDist = selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget);
float halfReactDist = reactDist / 2;
float attackDist = selectedTargetingParams != null && selectedTargetingParams.AttackDistance > 0 ? selectedTargetingParams.AttackDistance : halfReactDist;
if (sqrDist > Math.Pow(reactDist, 2))
{
// Too far to react
UpdateIdle(deltaTime);
}
else if (sqrDist < Math.Pow(attackDist + movementMargin, 2))
{
movementMargin = attackDist;
SteeringManager.Reset();
if (Character.AnimController.InWater)
{
useSteeringLengthAsMovementSpeed = true;
Vector2 dir = Vector2.Normalize(SelectedAiTarget.WorldPosition - Character.WorldPosition);
if (sqrDist < Math.Pow(attackDist * 0.75f, 2))
{
// Keep the distance, if too close
dir = -dir;
useSteeringLengthAsMovementSpeed = false;
Reverse = true;
run = true;
}
else
{
Reverse = false;
}
SteeringManager.SteeringManual(deltaTime, dir * 0.2f);
}
else
{
// TODO: doesn't work right here
FaceTarget(SelectedAiTarget.Entity);
}
observeTimer -= deltaTime;
if (observeTimer < 0)
{
IgnoreTarget(SelectedAiTarget);
State = AIState.Idle;
ResetAITarget();
}
}
else
{
run = sqrDist > Math.Pow(attackDist * 2, 2);
movementMargin = MathHelper.Clamp(movementMargin -= deltaTime, 0, attackDist);
UpdateFollow(deltaTime);
}
break;
default:
throw new NotImplementedException();
}
@@ -465,7 +606,8 @@ namespace Barotrauma
SteerInsideLevel(deltaTime);
float speed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun);
steeringManager.Update(speed);
Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, State == AIState.Idle && Character.AnimController.InWater ? Steering.Length() : speed);
float targetMovement = useSteeringLengthAsMovementSpeed ? Steering.Length() : speed;
Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, targetMovement);
if (Character.CurrentHull != null && Character.AnimController.InWater)
{
// Halve the swimming speed inside the sub
@@ -601,7 +743,7 @@ namespace Barotrauma
{
// Steer away from the target if in the same room
Vector2 escapeDir = Vector2.Normalize(SelectedAiTarget != null ? WorldPosition - SelectedAiTarget.WorldPosition : Character.AnimController.TargetMovement);
if (!MathUtils.IsValid(escapeDir)) escapeDir = Vector2.UnitY;
if (!MathUtils.IsValid(escapeDir)) { escapeDir = Vector2.UnitY; }
SteeringManager.SteeringManual(deltaTime, escapeDir);
}
else if (pathSteering != null)
@@ -922,7 +1064,7 @@ namespace Barotrauma
}
if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls))
{
if (Vector2.DistanceSquared(Character.WorldPosition, attackWorldPos) < 2000 * 2000)
if (wallTarget == null && Vector2.DistanceSquared(Character.WorldPosition, attackWorldPos) < 2000 * 2000)
{
// Check that we are not bumping into a door or a wall
Vector2 rayStart = SimPosition;
@@ -1193,7 +1335,7 @@ namespace Barotrauma
}
if (AIParams.RandomAttack)
{
selectedLimb = ToolBox.SelectWeightedRandom(attackLimbs, weights, Rand.RandSync.Server);
selectedLimb = ToolBox.SelectWeightedRandom(attackLimbs, weights, Rand.RandSync.Unsynced);
attackLimbs.Clear();
weights.Clear();
}
@@ -1268,7 +1410,7 @@ namespace Barotrauma
LatchOntoAI?.SetAttachTarget(wall.Submarine.PhysicsBody.FarseerBody, wall.Submarine, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
{
if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner"))
if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner") || wall.Submarine != null && wall.Submarine == Character.Submarine)
{
wallTarget = new WallTarget(sectionPos, wall, sectionIndex);
}
@@ -1309,11 +1451,12 @@ namespace Barotrauma
bool wasLatched = IsLatchedOnSub;
Character.AnimController.ReleaseStuckLimbs();
LatchOntoAI?.DeattachFromBody(cooldown: 1);
if (attacker == null || attacker.AiTarget == null) { return; }
if (attacker == null || attacker.AiTarget == null || attacker.Removed || attacker.IsDead) { return; }
bool isFriendly = IsFriendly(Character, attacker);
if (wasLatched)
{
avoidTimer = avoidTime * Rand.Range(0.75f, 1.25f);
State = AIState.Escape;
avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f);
if (!isFriendly)
{
SelectTarget(attacker.AiTarget);
@@ -1386,7 +1529,7 @@ namespace Barotrauma
}
AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, true);
targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AggressionHurt;
targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AIParams.AggressionHurt;
// Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown
bool retaliate = !isFriendly && SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine;
@@ -1411,7 +1554,14 @@ namespace Barotrauma
}
else if (avoidGunFire)
{
avoidTimer = avoidTime * Rand.Range(0.75f, 1.25f);
State = AIState.Escape;
avoidTimer = AIParams.AvoidTime * Rand.Range(0.75f, 1.25f);
SelectTarget(attacker.AiTarget);
}
if (Character.HealthPercentage <= FleeHealthThreshold)
{
State = AIState.Flee;
avoidTimer = AIParams.MinFleeTime * Rand.Range(0.75f, 1.25f);
SelectTarget(attacker.AiTarget);
}
}
@@ -1441,7 +1591,7 @@ namespace Barotrauma
if (damageTarget.Health > 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) * AggressionGreed;
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * AIParams.AggressionGreed;
}
else
{
@@ -1484,7 +1634,7 @@ namespace Barotrauma
State = AIState.Idle;
return;
}
if (SelectedAiTarget.Entity is Character target)
if (SelectedAiTarget.Entity is Character || SelectedAiTarget.Entity is Item)
{
Limb mouthLimb = Character.AnimController.GetLimb(LimbType.Head);
if (mouthLimb == null)
@@ -1494,12 +1644,38 @@ namespace Barotrauma
return;
}
Vector2 mouthPos = Character.AnimController.SimplePhysicsEnabled ? SimPosition : Character.AnimController.GetMouthPosition().Value;
Vector2 attackSimPosition = Character.GetRelativeSimPosition(target);
Vector2 attackSimPosition = Character.GetRelativeSimPosition(SelectedAiTarget.Entity);
Vector2 limbDiff = attackSimPosition - mouthPos;
float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 2);
if (limbDiff.LengthSquared() < extent * extent)
{
Character.SelectCharacter(target);
if (SelectedAiTarget.Entity is Character targetCharacter)
{
Character.SelectCharacter(targetCharacter);
}
else if (SelectedAiTarget.Entity is Item item)
{
if (!item.Removed && item.body != null)
{
float itemBodyExtent = item.body.GetMaxExtent() * 2;
if (Math.Abs(limbDiff.X) < itemBodyExtent &&
Math.Abs(limbDiff.Y) < Character.AnimController.Collider.GetMaxExtent() + Character.AnimController.ColliderHeightFromFloor)
{
item.body.LinearVelocity *= 0.9f;
item.body.LinearVelocity -= limbDiff * 0.25f;
bool wasBroken = item.Condition <= 0.0f;
item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.1f), deltaTime);
if (item.Condition <= 0.0f)
{
if (!wasBroken) { PetBehavior?.OnEat(item.GetTags(), 1.0f); }
Entity.Spawner.AddToRemoveQueue(item);
}
}
}
}
steeringManager.SteeringManual(deltaTime, Vector2.Normalize(limbDiff) * 3);
Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos);
}
@@ -1529,15 +1705,40 @@ namespace Barotrauma
State = AIState.Idle;
return;
}
Vector2 dir = Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition);
if (!MathUtils.IsValid(dir))
if (Character.CurrentHull != null && PathSteering != null)
{
return;
// Inside
Character targetCharacter = SelectedAiTarget.Entity as Character;
if ((Character.AnimController.InWater || !Character.AnimController.CanWalk) &&
(targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull) || Character.CanSeeTarget(SelectedAiTarget.Entity)))
{
// Steer towards the target if in the same room and swimming
Vector2 dir = Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition);
if (MathUtils.IsValid(dir))
{
SteeringManager.SteeringManual(deltaTime, dir);
}
}
else
{
// Use path finding
SteeringManager.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), 2);
if (!PathSteering.IsPathDirty && PathSteering.CurrentPath.Unreachable)
{
// Can't reach
State = AIState.Idle;
return;
}
}
}
steeringManager.SteeringManual(deltaTime, dir);
if (Character.AnimController.InWater)
else
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
// Outside
SteeringManager.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), 5);
if (Character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
}
}
}
@@ -1557,6 +1758,7 @@ namespace Barotrauma
foreach (AITarget aiTarget in AITarget.List)
{
if (!aiTarget.Enabled) { continue; }
if (aiTarget.Entity == null) { continue; }
if (ignoredTargets.Contains(aiTarget)) { continue; }
if (Level.Loaded != null && aiTarget.WorldPosition.Y > Level.Loaded.Size.Y)
{
@@ -1579,6 +1781,10 @@ namespace Barotrauma
{
targetingTag = "dead";
}
else if (PetBehavior != null && aiTarget.Entity == PetBehavior.Owner)
{
targetingTag = "owner";
}
else if (AIParams.TryGetTarget(targetCharacter.SpeciesName, out CharacterParams.TargetParams tP))
{
targetingTag = tP.Tag;
@@ -1626,7 +1832,7 @@ namespace Barotrauma
}
}
}
else if (aiTarget.Entity != null)
else
{
// Ignore all structures and items inside wrecks
if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsWreck) { continue; }
@@ -1784,8 +1990,10 @@ namespace Barotrauma
{
targetingTag = "door";
}
if (door.Item.Submarine == null) { continue;}
if (door.Item.Submarine == null) { continue; }
bool isOutdoor = door.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom;
// Ignore inner doors when outside
if (character.CurrentHull == null && !isOutdoor) { continue; }
bool isOpen = door.IsOpen || door.IsBroken;
if (!isOpen && !canAttackDoors || (isOutdoor && !AIParams.TargetOuterWalls))
{
@@ -1795,26 +2003,22 @@ namespace Barotrauma
if (isOpen && (!Character.AnimController.CanEnterSubmarine || !AggressiveBoarding))
{
// Ignore broken and open doors
// Aggressive boarders don't ignore open doors, because they use them for get in.
// Aggressive boarders don't ignore open doors, because they use them for getting in.
continue;
}
if (AggressiveBoarding)
{
// Increase the priority if the character is outside and the door is from outside to inside
if (character.CurrentHull == null && isOutdoor)
if (character.CurrentHull == null)
{
valueModifier *= isOpen ? 5 : 1;
}
else
{
// Inside
// Inside -> ignore open doors and outer doors
valueModifier *= isOpen || isOutdoor ? 0 : 1;
}
}
else if (character.CurrentHull == null)
{
valueModifier = isOutdoor ? 1 : 0;
}
}
else if (aiTarget.Entity is IDamageable targetDamageable && targetDamageable.Health <= 0.0f)
{
@@ -1823,8 +2027,20 @@ namespace Barotrauma
}
if (targetingTag == null) { continue; }
var targetParams = GetTarget(targetingTag);
var targetParams = GetTargetParams(targetingTag);
if (targetParams == null) { continue; }
if (targetParams.State == AIState.Observe || targetParams.State == AIState.Eat)
{
if (targetCharacter != null && targetCharacter.Submarine != Character.Submarine)
{
// Don't allow to target characters that are inside a different submarine / outside when we are inside.
continue;
}
}
if (aiTarget.Entity is Item targetItem && targetParams.IgnoreContained && targetItem.ParentInventory != null)
{
continue;
}
valueModifier *= targetParams.Priority;
if (valueModifier == 0.0f) { continue; }
@@ -1894,6 +2110,17 @@ namespace Barotrauma
}
if (targetCharacter != null)
{
if (Character.CurrentHull != null && targetCharacter.CurrentHull != Character.CurrentHull)
{
if (targetParams.State == AIState.Follow || targetParams.State == AIState.Protect || targetParams.State == AIState.Observe)
{
// Ignore targets that cannot see
if (!VisibleHulls.Contains(targetCharacter.CurrentHull))
{
continue;
}
}
}
if (targetCharacter.Submarine != Character.Submarine)
{
if (targetCharacter.Submarine != null)
@@ -1939,7 +2166,7 @@ namespace Barotrauma
newTarget = aiTarget;
selectedTargetMemory = targetMemory;
targetValue = valueModifier;
targetingParams = GetTarget(targetingTag);
targetingParams = GetTargetParams(targetingTag);
}
}
@@ -1999,7 +2226,7 @@ namespace Barotrauma
{
_selectedAiTarget = null;
}
else if (CanPerceive(_selectedAiTarget, distSquared: Vector2.DistanceSquared(Character.WorldPosition, _selectedAiTarget.WorldPosition)))
else if (CanPerceive(_selectedAiTarget))
{
var memory = GetTargetMemory(_selectedAiTarget, false);
if (memory != null)
@@ -2040,7 +2267,7 @@ namespace Barotrauma
removals.ForEach(r => targetMemories.Remove(r));
}
private readonly float targetIgnoreTime = 5;
private readonly float targetIgnoreTime = 10;
private float targetIgnoreTimer;
private readonly HashSet<AITarget> ignoredTargets = new HashSet<AITarget>();
public void IgnoreTarget(AITarget target)
@@ -2168,6 +2395,17 @@ namespace Barotrauma
}
#endregion
protected override void OnTargetChanged(AITarget previousTarget, AITarget newTarget)
{
base.OnTargetChanged(previousTarget, newTarget);
if (newTarget == null) { return; }
var targetParams = GetTargetParams(newTarget);
if (targetParams != null)
{
observeTimer = targetParams.Timer * Rand.Range(0.75f, 1.25f);
}
}
protected override void OnStateChanged(AIState from, AIState to)
{
LatchOntoAI?.DeattachFromBody();
@@ -2189,13 +2427,17 @@ namespace Barotrauma
private bool CanPerceive(AITarget target, float dist = -1, float distSquared = -1)
{
if (distSquared > -1)
if (dist > 0)
{
return distSquared <= MathUtils.Pow(target.SightRange * Sight, 2) || distSquared <= MathUtils.Pow(target.SoundRange * Hearing, 2);
return dist <= target.SightRange * Sight || dist <= target.SoundRange * Hearing;
}
else
{
return dist <= target.SightRange * Sight || dist <= target.SoundRange * Hearing;
if (distSquared < 0)
{
distSquared = Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition);
}
return distSquared <= MathUtils.Pow(target.SightRange * Sight, 2) || distSquared <= MathUtils.Pow(target.SoundRange * Hearing, 2);
}
}
@@ -35,6 +35,14 @@ namespace Barotrauma
public readonly HashSet<Hull> UnsafeHulls = new HashSet<Hull>();
public readonly List<Item> IgnoredItems = new List<Item>();
private float respondToAttackTimer;
private const float RespondToAttackInterval = 1.0f;
/// <summary>
/// List of previous attacks done to this character
/// </summary>
private readonly Dictionary<Character, AttackResult> previousAttackResults = new Dictionary<Character, AttackResult>();
private class HullSafety
{
public float safety;
@@ -138,6 +146,17 @@ namespace Barotrauma
}
if (isIncapacitated) { return; }
respondToAttackTimer -= deltaTime;
if (respondToAttackTimer <= 0.0f)
{
foreach (var previousAttackResult in previousAttackResults)
{
RespondToAttack(previousAttackResult.Key, previousAttackResult.Value);
}
previousAttackResults.Clear();
respondToAttackTimer = RespondToAttackInterval;
}
base.Update(deltaTime);
foreach (var values in knownHulls)
@@ -316,6 +335,7 @@ namespace Barotrauma
{
if (Character.LockHands) { return; }
if (ObjectiveManager.CurrentObjective == null) { return; }
if (Character.CurrentHull == null) { return; }
bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>();
@@ -349,7 +369,9 @@ namespace Barotrauma
if (!NeedsDivingGear(Character.CurrentHull, out bool needsSuit) || !needsSuit || oxygenLow)
{
bool shouldKeepTheGearOn = Character.AnimController.HeadInWater
|| Character.Submarine.TeamID != Character.TeamID && Character.Submarine.TeamID != Character.TeamType.FriendlyNPC
|| 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)
{
@@ -425,12 +447,14 @@ namespace Barotrauma
{
var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>())
{
DropIfFailsToContain = false
DropIfFails = false
};
decontainObjective.Abandoned += () =>
{
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
decontainObjective.Completed += () => ReequipUnequipped();
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
@@ -451,7 +475,7 @@ namespace Barotrauma
{
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
if (ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
mask.Drop(Character);
}
@@ -465,7 +489,12 @@ namespace Barotrauma
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
decontainObjective.Abandoned += () =>
{
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
decontainObjective.Completed += () => ReequipUnequipped();
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
@@ -476,6 +505,10 @@ namespace Barotrauma
}
}
}
else
{
ReequipUnequipped();
}
}
}
}
@@ -504,7 +537,11 @@ namespace Barotrauma
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
decontainObjective.Abandoned += () =>
{
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
@@ -519,6 +556,18 @@ namespace Barotrauma
}
}
public void ReequipUnequipped()
{
foreach (var item in unequippedItems)
{
if (item != null && !item.Removed && Character.HasItem(item))
{
TakeItem(item, Character.Inventory, equip: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false);
}
}
unequippedItems.Clear();
}
private enum FindItemState
{
None,
@@ -528,10 +577,13 @@ namespace Barotrauma
}
private FindItemState findItemState;
private int itemIndex;
public bool FindSuitableContainer(Item containableItem, out Item suitableContainer)
public bool FindSuitableContainer(Item containableItem, out Item suitableContainer) => FindSuitableContainer(Character, containableItem, IgnoredItems, ref itemIndex, out suitableContainer);
public static bool FindSuitableContainer(Character character, Item containableItem, List<Item> ignoredItems, ref int itemIndex, out Item suitableContainer)
{
suitableContainer = null;
if (Character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: IgnoredItems, customPriorityFunction: i =>
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, customPriorityFunction: i =>
{
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
@@ -663,6 +715,17 @@ namespace Barotrauma
}
}
public static void ReportProblem(Character reporter, Order order)
{
if (reporter == null || order == null) { return; }
var visibleHulls = new List<Hull>(reporter.GetVisibleHulls());
foreach (var hull in visibleHulls)
{
PropagateHullSafety(reporter, hull);
RefreshTargets(reporter, order, hull);
}
}
private void UpdateSpeaking()
{
if (Character.Oxygen < 20.0f)
@@ -683,6 +746,38 @@ namespace Barotrauma
public override void OnAttacked(Character attacker, AttackResult attackResult)
{
if (Character.IsDead) { return; }
if (attacker == null || Character.IsPlayer)
{
// The player characters need to "respond" to the attack always, because the update loop doesn't run for them.
// Otherwise other NPCs totally ignore when player characters are attacked.
RespondToAttack(attacker, attackResult);
return;
}
if (previousAttackResults.ContainsKey(attacker))
{
foreach (Affliction newAffliction in attackResult.Afflictions)
{
var matchingAffliction = previousAttackResults[attacker].Afflictions.Find(a => a.Prefab == newAffliction.Prefab && a.Source == newAffliction.Source);
if (matchingAffliction == null)
{
previousAttackResults[attacker].Afflictions.Add(newAffliction);
}
else
{
matchingAffliction.Strength += newAffliction.Strength;
}
}
previousAttackResults[attacker] = new AttackResult(previousAttackResults[attacker].Afflictions, previousAttackResults[attacker].HitLimb);
}
else
{
previousAttackResults.Add(attacker, attackResult);
}
}
private void RespondToAttack(Character attacker, AttackResult attackResult)
{
// excluding poisons etc
float realDamage = attackResult.Damage;
// including poisons etc
@@ -691,7 +786,7 @@ namespace Barotrauma
{
totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength;
}
if (totalDamage <= 0) { return; }
if (totalDamage <= 0.01f) { return; }
if (Character.IsBot)
{
if (attacker != null)
@@ -735,7 +830,7 @@ namespace Barotrauma
return;
}
float cumulativeDamage = GetDamageDoneByAttacker(attacker);
if (!Character.IsSecurity && attacker.IsBot && !attacker.IsInstigator)
if (!Character.IsSecurity && attacker.IsBot && Character.CombatAction == null)
{
if (cumulativeDamage > 1)
{
@@ -834,7 +929,17 @@ namespace Barotrauma
}
else
{
if (attacker.TeamID == Character.TeamType.FriendlyNPC)
// 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)))
{
return AIObjectiveCombat.CombatMode.None;
}
if (Character.IsInstigator && attacker.IsPlayer)
{
// 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 == Character.TeamType.FriendlyNPC)
{
if (c.IsSecurity)
{
@@ -847,11 +952,7 @@ namespace Barotrauma
}
else
{
if (Character.IsInstigator)
{
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
}
else if (cumulativeDamage > dmgThreshold)
if (cumulativeDamage > dmgThreshold)
{
if (c.IsSecurity)
{
@@ -1006,39 +1107,54 @@ namespace Barotrauma
return true;
}
public bool TryToMoveItem(Item item, Inventory targetInventory, bool dropIfCannotMove = true)
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)
{
var pickable = item.GetComponent<Pickable>();
if (pickable == null) { return false; }
int targetSlot = -1;
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
if (item.ParentInventory is ItemInventory itemInventory)
{
if (slots.HasFlag(InvSlotType.Any)) { continue; }
for (int i = 0; i < targetInventory.Items.Length; i++)
if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; }
}
if (equip)
{
int targetSlot = -1;
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
if (targetInventory is CharacterInventory characterInventory)
if (slots.HasFlag(InvSlotType.Any)) { continue; }
for (int i = 0; i < targetInventory.Items.Length; i++)
{
//slot not needed by the item, continue
if (!slots.HasFlag(characterInventory.SlotTypes[i])) { continue; }
}
targetSlot = i;
//slot free, continue
var otherItem = targetInventory.Items[i];
if (otherItem == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
continue;
}
if (dropIfCannotMove)
{
//if everything else fails, simply drop the existing item
otherItem.Drop(Character);
if (targetInventory is CharacterInventory characterInventory)
{
//slot not needed by the item, continue
if (!slots.HasFlag(characterInventory.SlotTypes[i])) { continue; }
}
targetSlot = i;
//slot free, continue
var otherItem = targetInventory.Items[i];
if (otherItem == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.anySlot))
{
if (storeUnequipped && targetInventory.Owner == Character)
{
unequippedItems.Add(otherItem);
}
continue;
}
if (dropOtherIfCannotMove)
{
//if everything else fails, simply drop the existing item
otherItem.Drop(Character);
}
}
}
return targetInventory.TryPutItem(item, targetSlot, allowSwapping, allowCombine: false, Character);
}
else
{
return targetInventory.TryPutItem(item, Character, CharacterInventory.anySlot);
}
return targetInventory.TryPutItem(item, targetSlot, false, false, Character);
}
public static bool NeedsDivingGear(Hull hull, out bool needsSuit)
@@ -1107,7 +1223,7 @@ namespace Barotrauma
}
//if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; }
if (!otherCharacter.CanSeeCharacter(thief)) { continue; }
if (!someoneSpoke)
if (!someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f)
{
if (!item.StolenDuringRound && GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
{
@@ -1306,7 +1422,7 @@ namespace Barotrauma
// Use the cached visible hulls
visibleHulls = VisibleHulls;
}
bool ignoreFire = objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreWater = HasDivingSuit(character);
bool ignoreOxygen = ignoreWater || HasDivingMask(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
@@ -1391,13 +1507,10 @@ namespace Barotrauma
return hullSafety.safety;
}
public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left;
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
{
bool sameTeam = me.TeamID == other.TeamID;
// Only enemies are in the Team "None"
bool friendlyTeam = me.TeamID != Character.TeamType.None && other.TeamID != Character.TeamType.None;
bool friendlyTeam = IsOnFriendlyTeam(GameMain.GameSession?.GameMode, me, other);
bool teamGood = sameTeam || friendlyTeam && !onlySameTeam;
if (!teamGood) { return false; }
bool speciesGood = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
@@ -1413,6 +1526,18 @@ namespace Barotrauma
return true;
}
private static bool IsOnFriendlyTeam(GameMode mode, Character me, Character other)
{
// Only enemies are in the Team "None"
bool friendlyTeam = me.TeamID != Character.TeamType.None && other.TeamID != Character.TeamType.None;
// When playing a combat mission, we need to be on the same team to be friendlies
if (friendlyTeam && mode is MissionMode mm && mm.Mission is CombatMission)
{
friendlyTeam = me.TeamID == other.TeamID;
}
return friendlyTeam;
}
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious;
public static bool IsTrueForAllCrewMembers(Character character, Func<HumanAIController, bool> predicate)
@@ -190,14 +190,45 @@ namespace Barotrauma
}
}
pathFinder.InsideSubmarine = character.Submarine != null;
pathFinder.ApplyPenaltyToOutsideNodes = character.PressureProtection <= 0;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null;
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.X) <= 0;
if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
{
// It's possible that the current path was calculated from a start point that is no longer valid.
// Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node.
useNewPath = newPath.Cost < currentPath.Cost ||
Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2);
// Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset).
if (IsIdenticalPath())
{
useNewPath = false;
}
else
{
// Use the new path if it has significantly lower cost (don't change the path if it has marginally smaller cost. This reduces navigating backwards due to new path that is calculated from the node just behind us).
float t = (float)currentPath.CurrentIndex / (currentPath.Nodes.Count - 1);
useNewPath = newPath.Cost < currentPath.Cost * MathHelper.Lerp(0.95f, 0, t);
if (!useNewPath)
{
// It's possible that the current path was calculated from a start point that is no longer valid.
// Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node.
useNewPath = Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2);
}
}
bool IsIdenticalPath()
{
int nodeCount = newPath.Nodes.Count;
if (nodeCount == currentPath.Nodes.Count)
{
for (int i = 0; i < nodeCount - 1; i++)
{
if (newPath.Nodes[i] != currentPath.Nodes[i])
{
return false;
}
}
return true;
}
return false;
}
}
if (useNewPath)
{
@@ -343,7 +374,7 @@ namespace Barotrauma
}
return diff;
}
else if (!canClimb || character.AnimController.InWater)
else if (character.AnimController.InWater)
{
// If the character is underwater, we don't need the ladders anymore
if (character.IsClimbing && isDiving)
@@ -370,7 +401,7 @@ namespace Barotrauma
}
}
}
else if (!IsNextLadderSameAsCurrent)
else if (!canClimb || !IsNextLadderSameAsCurrent)
{
// Walking horizontally
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
@@ -378,6 +409,8 @@ namespace Barotrauma
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;
// 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);
@@ -386,7 +419,7 @@ namespace Barotrauma
var door = currentPath.CurrentNode.ConnectedDoor;
bool blockedByDoor = door != null && !door.IsOpen && !door.IsBroken;
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 10, 0, 1));
float targetDistance = collider.radius * margin;
float targetDistance = Math.Max(collider.radius * margin, minWidth);
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && !blockedByDoor)
{
currentPath.SkipToNextNode();
@@ -15,6 +15,7 @@ namespace Barotrauma
public virtual bool IgnoreUnsafeHulls => false;
public virtual bool AbandonWhenCannotCompleteSubjectives => true;
public virtual bool AllowSubObjectiveSorting => false;
public virtual bool ForceOrderPriority => true;
/// <summary>
/// Can there be multiple objective instaces of the same type?
@@ -34,6 +35,7 @@ namespace Barotrauma
public virtual bool AllowAutomaticItemUnequipping => false;
public virtual bool AllowOutsideSubmarine => false;
public virtual bool AllowInFriendlySubs => false;
public virtual bool AllowInAnySub => false;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
private float _cumulatedDevotion;
@@ -198,12 +200,10 @@ namespace Barotrauma
{
get
{
if (AllowOutsideSubmarine) { return true; }
if (character.Submarine == null) { return false; }
return
character.Submarine.TeamID == character.TeamID ||
(AllowInFriendlySubs && character.Submarine.TeamID == Character.TeamType.FriendlyNPC) ||
character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID);
if (!AllowOutsideSubmarine && character.Submarine == null) { return false; }
if (AllowInAnySub) { return true; }
if (AllowInFriendlySubs && character.Submarine.TeamID == Character.TeamType.FriendlyNPC) { return true; }
return character.Submarine.TeamID == character.TeamID || character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID);
}
}
@@ -212,12 +212,14 @@ namespace Barotrauma
/// </summary>
public virtual float GetPriority()
{
bool isOrder = objectiveManager.CurrentOrder == this;
if (!IsAllowed)
{
Priority = 0;
Abandon = !isOrder;
return Priority;
}
if (objectiveManager.CurrentOrder == this)
if (isOrder)
{
Priority = AIObjectiveManager.OrderPriority;
}
@@ -306,7 +308,7 @@ namespace Barotrauma
return true;
}
#if DEBUG
DebugConsole.ThrowError("Attempted to add a duplicate subobjective!\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to add a duplicate subobjective!\n" + Environment.StackTrace.CleanupStackTrace());
#endif
return false;
}
@@ -0,0 +1,124 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveCleanupItem : AIObjective
{
public override string DebugTag => "cleanup item";
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => false;
public readonly Item item;
public bool IsPriority { get; set; }
private readonly List<Item> ignoredContainers = new List<Item>();
private AIObjectiveDecontainItem decontainObjective;
private int itemIndex = 0;
public AIObjectiveCleanupItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.item = item;
}
public override float GetPriority()
{
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
return Priority;
}
else
{
float distanceFactor = 0.9f;
if (!IsPriority && item.CurrentHull != character.CurrentHull)
{
float yDist = Math.Abs(character.WorldPosition.Y - item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - item.WorldPosition.X) + yDist;
distanceFactor = MathHelper.Lerp(0.9f, 0, MathUtils.InverseLerp(0, 5000, dist));
}
bool isSelected = character.HasItem(item);
float selectedBonus = isSelected ? 100 - MaxDevotion : 0;
float devotion = (CumulatedDevotion + selectedBonus) / 100;
float reduction = IsPriority ? 1 : isSelected ? 2 : 3;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90);
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
}
protected override void Act(float deltaTime)
{
// Only continue when the get item sub objectives have been completed.
if (subObjectives.Any()) { return; }
if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer))
{
itemIndex = 0;
if (suitableContainer != null)
{
bool equip = item.HasTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR) || (
item.GetComponent<Wearable>() == null &&
item.AllowedSlots.None(s =>
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,
DropIfFails = true
},
onCompleted: () =>
{
if (equip)
{
HumanAIController.ReequipUnequipped();
}
IsCompleted = true;
},
onAbandon: () =>
{
if (equip)
{
HumanAIController.ReequipUnequipped();
}
if (decontainObjective != null && decontainObjective.ContainObjective != null && decontainObjective.ContainObjective.CanBeCompleted)
{
ignoredContainers.Add(suitableContainer);
}
else
{
Abandon = true;
}
});
}
else
{
Abandon = true;
}
}
else
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
}
protected override bool Check() => IsCompleted;
public override void Reset()
{
base.Reset();
ignoredContainers.Clear();
itemIndex = 0;
decontainObjective = null;
}
}
}
@@ -0,0 +1,86 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveCleanupItems : AIObjectiveLoop<Item>
{
public override string DebugTag => "cleanup items";
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => false;
public override bool ForceOrderPriority => false;
public readonly Item prioritizedItem;
public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Item prioritizedItem = null)
: base(character, objectiveManager, priorityModifier)
{
this.prioritizedItem = prioritizedItem;
}
protected override float TargetEvaluation() => Targets.Any() ? AIObjectiveManager.RunPriority - 1 : 0;
protected override bool Filter(Item target)
{
// If the target was selected as a valid target, we'll have to accept it so that the objective can be completed.
// The validity changes when a character picks the item up.
if (!IsValidTarget(target, character)) { return Objectives.ContainsKey(target) && IsItemInsideValidSubmarine(target, character); }
if (target.CurrentHull.FireSources.Count > 0) { return false; }
// Don't repair items in rooms that have enemies inside.
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
return true;
}
protected override IEnumerable<Item> GetList() => Item.ItemList;
protected override AIObjective ObjectiveConstructor(Item item)
=> new AIObjectiveCleanupItem(item, character, objectiveManager, priorityModifier: PriorityModifier)
{
IsPriority = prioritizedItem == item
};
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
=> HumanAIController.RemoveTargets<AIObjectiveCleanupItems, Item>(character, target);
private static bool IsItemInsideValidSubmarine(Item item, Character character)
{
if (item.CurrentHull == null) { return false; }
if (item.Submarine == null) { return false; }
if (item.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine != null)
{
if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; }
}
return true;
}
public static bool IsValidTarget(Item item, Character character)
{
if (item == null) { return false; }
if (item.NonInteractable) { return false; }
if (item.ParentInventory != null) { return false; }
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
//var rootContainer = item.GetRootContainer();
//// Only target items lying on the ground (= not inside a container) (do we need this check?)
//if (rootContainer != null) { return false; }
var pickable = item.GetComponent<Pickable>();
if (pickable == null) { return false; }
if (pickable is Holdable h && h.Attachable && h.Attached) { return false; }
var wire = item.GetComponent<Wire>();
if (wire != null)
{
if (wire.Connections.Any()) { return false; }
}
else
{
var connectionPanel = item.GetComponent<ConnectionPanel>();
if (connectionPanel != null && connectionPanel.Connections.Any(c => c.Wires.Any(w => w != null)))
{
return false;
}
}
return item.Prefab.PreferredContainers.Any();
}
}
}
@@ -15,6 +15,7 @@ namespace Barotrauma
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
private readonly CombatMode initialMode;
@@ -54,8 +55,8 @@ namespace Barotrauma
if (_weaponComponent == null)
{
_weaponComponent =
Weapon.GetComponent<RangedWeapon>() as ItemComponent ??
Weapon.GetComponent<MeleeWeapon>() as ItemComponent ??
Weapon.GetComponent<RangedWeapon>() ??
Weapon.GetComponent<MeleeWeapon>() ??
Weapon.GetComponent<RepairTool>() as ItemComponent;
}
return _weaponComponent;
@@ -144,6 +145,7 @@ namespace Barotrauma
if (Enemy.Submarine == null || (Enemy.Submarine.TeamID != character.TeamID && Enemy.Submarine != character.Submarine))
{
Priority = 0;
Abandon = true;
return Priority;
}
}
@@ -760,11 +762,7 @@ namespace Barotrauma
if (HumanAIController.HasItem(character, "handlocker", out IEnumerable<Item> matchingItems) && Enemy.Stun > 0 && character.CanInteractWith(Enemy))
{
var handCuffs = matchingItems.First();
if (HumanAIController.TryToMoveItem(handCuffs, Enemy.Inventory))
{
handCuffs.Equip(Enemy);
}
else
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true))
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to handcuff the target.", Color.Red);
@@ -777,7 +775,7 @@ namespace Barotrauma
if (item.StolenDuringRound)
{
item.Drop(character);
character.Inventory.TryPutItem(item, character, new List<InvSlotType>() { InvSlotType.Any });
character.Inventory.TryPutItem(item, character, CharacterInventory.anySlot);
}
}
character.Speak(TextManager.Get("DialogTargetArrested"), null, 3.0f, "targetarrested", 30.0f);
@@ -885,7 +883,11 @@ namespace Barotrauma
private void Attack(float deltaTime)
{
character.CursorPosition = Enemy.Position;
character.CursorPosition = Enemy.WorldPosition;
if (character.Submarine != null)
{
character.CursorPosition -= character.Submarine.Position;
}
visibilityCheckTimer -= deltaTime;
if (visibilityCheckTimer <= 0.0f)
{
@@ -142,6 +142,7 @@ namespace Barotrauma
}
else
{
// TODO: should we just use GetItem?
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear)
{
DialogueIdentifier = "dialogcannotreachtarget",
@@ -153,27 +154,34 @@ namespace Barotrauma
}
else
{
// No matching items in the inventory, try to get an item
TryAddSubObjective(ref getItemObjective, () =>
new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, equip: Equip, checkInventory: checkInventory, spawnItemIfNotFound: spawnItemIfNotFound)
{
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = ignoredContainerIdentifiers,
ignoredItems = containedItems,
AllowToFindDivingGear = AllowToFindDivingGear,
AllowDangerousPressure = AllowDangerousPressure,
TargetCondition = ConditionLevel
}, onAbandon: () =>
{
Abandon = true;
}, onCompleted: () =>
{
if (getItemObjective?.TargetItem != null)
if (character.Submarine == null)
{
Abandon = true;
}
else
{
// No matching items in the inventory, try to get an item
TryAddSubObjective(ref getItemObjective, () =>
new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, equip: Equip, checkInventory: checkInventory, spawnItemIfNotFound: spawnItemIfNotFound)
{
containedItems.Add(getItemObjective.TargetItem);
}
RemoveSubObjective(ref getItemObjective);
});
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = ignoredContainerIdentifiers,
ignoredItems = containedItems,
AllowToFindDivingGear = AllowToFindDivingGear,
AllowDangerousPressure = AllowDangerousPressure,
TargetCondition = ConditionLevel
}, onAbandon: () =>
{
Abandon = true;
}, onCompleted: () =>
{
if (getItemObjective?.TargetItem != null)
{
containedItems.Add(getItemObjective.TargetItem);
}
RemoveSubObjective(ref getItemObjective);
});
}
}
}
@@ -16,9 +16,12 @@ namespace Barotrauma
private ItemContainer targetContainer;
private readonly Item targetItem;
private AIObjectiveGoTo goToObjective;
private AIObjectiveGetItem getItemObjective;
private AIObjectiveContainItem containObjective;
public AIObjectiveGetItem GetItemObjective => getItemObjective;
public AIObjectiveContainItem ContainObjective => containObjective;
public bool Equip { get; set; }
/// <summary>
@@ -26,7 +29,7 @@ namespace Barotrauma
/// In both cases abandons the objective.
/// Note that has no effect if the target container was not defined (always drops) -> completes when the item is dropped.
/// </summary>
public bool DropIfFailsToContain { get; set; } = true;
public bool DropIfFails { get; set; } = true;
public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -74,54 +77,30 @@ namespace Barotrauma
return;
}
}
else
else if (targetContainer.Inventory.Items.Contains(itemToDecontain))
{
if (targetContainer.Inventory.Items.Contains(itemToDecontain))
{
IsCompleted = true;
return;
}
IsCompleted = true;
return;
}
if (goToObjective == null && !itemToDecontain.IsOwnedBy(character))
if (getItemObjective == null && !itemToDecontain.IsOwnedBy(character))
{
if (sourceContainer == null)
{
Abandon = true;
return;
}
if (!character.CanInteractWith(sourceContainer.Item, out _, checkLinked: false))
{
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(sourceContainer.Item, character, objectiveManager)
{
// If the container changes, the item is no longer where it was
abortCondition = () => itemToDecontain.Container != sourceContainer.Item,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = sourceContainer.Item.Name
},
onAbandon: () => Abandon = true);
return;
}
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip),
onAbandon: () => Abandon = true);
return;
}
if (targetContainer != null)
{
TryAddSubObjective(ref containObjective,
constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager)
{
Equip = this.Equip,
Equip = Equip,
RemoveEmpty = false,
GetItemPriority = this.GetItemPriority,
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = sourceContainer != null ? new string[] { sourceContainer.Item.Prefab.Identifier } : null
},
onCompleted: () => IsCompleted = true,
onAbandon: () =>
{
if (DropIfFailsToContain)
{
itemToDecontain.Drop(character);
}
Abandon = true;
});
onAbandon: () => Abandon = true);
}
else
{
@@ -133,8 +112,17 @@ namespace Barotrauma
public override void Reset()
{
base.Reset();
goToObjective = null;
getItemObjective = null;
containObjective = null;
}
protected override void OnAbandon()
{
base.OnAbandon();
if (DropIfFails && targetItem != null && targetItem.IsOwnedBy(character))
{
targetItem.Drop(character);
}
}
}
}
@@ -13,6 +13,8 @@ namespace Barotrauma
public override bool ConcurrentObjectives => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInAnySub => true;
private readonly Hull targetHull;
private AIObjectiveGetItem getExtinguisherObjective;
@@ -30,12 +32,15 @@ namespace Barotrauma
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
return Priority;
}
if (!objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>()
&& Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
bool isOrder = objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>();
if (!isOrder && Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
// Don't go into rooms with any enemies, unless it's an order
Priority = 0;
Abandon = true;
}
else
{
@@ -43,14 +48,22 @@ namespace Barotrauma
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
if (targetHull == character.CurrentHull)
if (targetHull == character.CurrentHull || HumanAIController.VisibleHulls.Contains(targetHull))
{
distanceFactor = 1;
}
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
float severityFactor = MathHelper.Lerp(0, 1, severity / 100);
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (severityFactor * distanceFactor * PriorityModifier), 0, 1));
if (severity > 0.5f && !isOrder)
{
// Ignore severe fires unless ordered. (Let the fire drain all the oxygen instead).
Priority = 0;
Abandon = true;
}
else
{
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
}
return Priority;
}
@@ -131,13 +144,16 @@ namespace Barotrauma
if (move)
{
//go to the first firesource
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range / 2)
if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range / 2)
{
DialogueIdentifier = "dialogcannotreachfire",
TargetName = fs.Hull.DisplayName
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
{
DialogueIdentifier = "dialogcannotreachfire",
TargetName = fs.Hull.DisplayName
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective));
gotoObjective.requiredCondition = () => HumanAIController.VisibleHulls.Contains(fs.Hull);
}
}
else
{
@@ -1,6 +1,8 @@
using System.Linq;
using System;
using System.Linq;
using System.Collections.Generic;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -8,14 +10,22 @@ namespace Barotrauma
{
public override string DebugTag => "extinguish fires";
public override bool ForceRun => true;
public override bool AllowInAnySub => true;
public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Hull hull) => IsValidTarget(hull, character);
protected override float TargetEvaluation() => Targets.Sum(t => GetFireSeverity(t));
protected override float TargetEvaluation() =>
// If any target is visible -> 100 priority
Targets.Any(t => t == character.CurrentHull || HumanAIController.VisibleHulls.Contains(t)) ? 100 :
// Else based on the fire severity
Targets.Sum(t => GetFireSeverity(t) * 100);
public static float GetFireSeverity(Hull hull) => hull.FireSources.Sum(fs => fs.Size.X);
/// <summary>
/// 0-1 based on the horizontal size of all of the fires in the hull.
/// </summary>
public static float GetFireSeverity(Hull hull) => MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, Math.Min(hull.Rect.Width, 1000), hull.FireSources.Sum(fs => fs.Size.X)));
protected override IEnumerable<Hull> GetList() => Hull.hullList;
@@ -31,22 +41,7 @@ namespace Barotrauma
if (hull.FireSources.None()) { return false; }
if (hull.Submarine == null) { return false; }
if (character.Submarine == null) { return false; }
if (!character.Submarine.IsConnectedTo(hull.Submarine)) { return false; }
if (character.AIController is HumanAIController humanAI)
{
if (hull.Submarine.TeamID != character.TeamID)
{
if (humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>())
{
// For orders, allow targets in the current sub (for example if the bot is inside an outpost or a wreck)
if (hull.Submarine != character.Submarine) { return false; }
}
else
{
return false;
}
}
}
if (!character.Submarine.IsEntityFoundOnThisSub(hull, includingConnectedSubs: true)) { return false; }
return true;
}
}
@@ -17,7 +17,7 @@ namespace Barotrauma
private Item targetItem;
public static float MIN_OXYGEN = 10;
public static string HEAVY_DIVING_GEAR = "heavydiving";
public static string HEAVY_DIVING_GEAR = "deepdiving";
public static string LIGHT_DIVING_GEAR = "lightdiving";
public static string OXYGEN_SOURCE = "oxygensource";
@@ -14,8 +14,9 @@ namespace Barotrauma
public override bool IgnoreUnsafeHulls => true;
public override bool ConcurrentObjectives => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace); }
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
// TODO: expose?
const float priorityIncrease = 100;
@@ -203,11 +204,15 @@ namespace Barotrauma
{
// Don't ignore any hulls if outside, because apparently it happens that we can't find a path, in which case we just want to try again.
// If we ignore the hull, it might be the only airlock in the target sub, which ignores the whole sub.
if (currentHull != null && goToObjective != null)
// If the target hull is inside a submarine that is not our main sub, just ignore it normally when it cannot be reached. This happens with outposts.
if (goToObjective != null)
{
if (goToObjective.Target is Hull hull)
{
HumanAIController.UnreachableHulls.Add(hull);
if (currentHull != null || !Submarine.MainSubs.Contains(hull.Submarine))
{
HumanAIController.UnreachableHulls.Add(hull);
}
}
}
RemoveSubObjective(ref goToObjective);
@@ -12,6 +12,7 @@ namespace Barotrauma
public override string DebugTag => "fix leak";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInAnySub => true;
public Gap Leak { get; private set; }
@@ -35,11 +36,7 @@ namespace Barotrauma
if (!IsAllowed)
{
Priority = 0;
return Priority;
}
if (Leak.Removed || Leak.Open <= 0)
{
Priority = 0;
Abandon = true;
}
else if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective<AIObjectiveFixLeak>()?.Leak == Leak))
{
@@ -116,7 +113,7 @@ namespace Barotrauma
{
HumanAIController.AnimController.Crouching = true;
}
float reach = repairTool.Range + ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength);
float reach = CalculateReach(repairTool, character);
bool canOperate = toLeak.LengthSquared() < reach * reach;
if (canOperate)
{
@@ -144,7 +141,7 @@ namespace Barotrauma
onAbandon: () =>
{
if (Check()) { IsCompleted = true; }
else if ((Leak.WorldPosition - character.WorldPosition).LengthSquared() > reach * reach * 2)
else if ((Leak.WorldPosition - character.WorldPosition).LengthSquared() > MathUtils.Pow(reach * 2, 2))
{
// Too far
Abandon = true;
@@ -167,5 +164,14 @@ namespace Barotrauma
gotoObjective = null;
operateObjective = null;
}
public static float CalculateReach(RepairTool repairTool, Character character)
{
float armLength = ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength);
// 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;
}
}
}
@@ -9,6 +9,7 @@ namespace Barotrauma
public override string DebugTag => "fix leaks";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInAnySub => true;
private Hull PrioritizedHull { get; set; }
public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Hull prioritizedHull = null) : base(character, objectiveManager, priorityModifier)
@@ -71,12 +72,9 @@ namespace Barotrauma
{
if (gap == null) { return false; }
if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; }
if (gap.Submarine == null) { return false; }
if (gap.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine != null)
{
if (!character.Submarine.IsConnectedTo(gap.Submarine)) { return false; }
}
if (gap.Submarine == null || character.Submarine == null) { return false; }
// Don't allow going into another sub, unless it's connected and of the same team and type.
if (!character.Submarine.IsEntityFoundOnThisSub(gap, includingConnectedSubs: true)) { return false; }
return true;
}
}
@@ -37,6 +37,7 @@ namespace Barotrauma
public static float DefaultReach = 100;
public bool AllowToFindDivingGear { get; set; } = true;
public bool MustBeSpecificItem { get; set; }
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -84,6 +85,11 @@ namespace Barotrauma
Abandon = true;
return;
}
if (character.Submarine == null)
{
Abandon = true;
return;
}
if (identifiersOrTags != null && !isDoneSeeking)
{
if (checkInventory)
@@ -109,7 +115,10 @@ namespace Barotrauma
}
}
FindTargetItem();
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
if (!objectiveManager.IsCurrentOrder<AIObjectiveGoTo>())
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
return;
}
}
@@ -137,7 +146,8 @@ namespace Barotrauma
if (originalTarget == null)
{
// Try again
Reset();
ignoredItems.Add(targetItem);
ResetInternal();
}
else
{
@@ -176,12 +186,8 @@ namespace Barotrauma
return;
}
if (HumanAIController.TryToMoveItem(targetItem, character.Inventory))
if (HumanAIController.TakeItem(targetItem, character.Inventory, equip, storeUnequipped: true))
{
if (equip)
{
targetItem.Equip(character);
}
IsCompleted = true;
}
else
@@ -207,8 +213,16 @@ namespace Barotrauma
},
onAbandon: () =>
{
ignoredItems.Add(targetItem);
Reset();
if (originalTarget == null)
{
// Try again
ignoredItems.Add(targetItem);
ResetInternal();
}
else
{
Abandon = true;
}
},
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
@@ -235,14 +249,18 @@ namespace Barotrauma
Submarine mySub = character.Submarine;
if (itemSub == null) { continue; }
if (mySub == null) { continue; }
if (itemSub.TeamID != mySub.TeamID && itemSub.TeamID != character.TeamID) { continue; }
if (!CheckItem(item)) { continue; }
if (ignoredContainerIdentifiers != null && item.Container != null)
{
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
}
if (!mySub.IsConnectedTo(itemSub)) { continue; }
// Don't allow going into another sub, unless it's connected and of the same team and type.
if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { continue; }
if (character.IsItemTakenBySomeoneElse(item)) { continue; }
if (item.ParentInventory is ItemInventory itemInventory)
{
if (!itemInventory.Container.HasRequiredItems(character, addMessage: false)) { continue; }
}
float itemPriority = 1;
if (GetItemPriority != null)
{
@@ -272,7 +290,7 @@ namespace Barotrauma
if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && identifiersOrTags.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab))
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", identifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
#endif
Abandon = true;
}
@@ -291,7 +309,7 @@ namespace Barotrauma
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow);
#endif
Abandon = true;
}
@@ -330,11 +348,28 @@ namespace Barotrauma
public override void Reset()
{
base.Reset();
ResetInternal();
}
/// <summary>
/// Does not reset the ignored items list
/// </summary>
private void ResetInternal()
{
goToObjective = null;
targetItem = originalTarget;
moveToTarget = targetItem?.GetRootInventoryOwner();
isDoneSeeking = false;
currSearchIndex = 0;
}
protected override void OnAbandon()
{
base.OnAbandon();
if (objectiveManager.CurrentOrder != null)
{
character.Speak(TextManager.Get("DialogCannotFindItem"), null, 0.0f, "cannotfinditem", 10.0f);
}
}
}
}
@@ -1,5 +1,6 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
@@ -50,6 +51,7 @@ namespace Barotrauma
public override bool AbandonWhenCannotCompleteSubjectives => !repeat;
public override bool AllowOutsideSubmarine => AllowGoingOutside;
public override bool AllowInAnySub => true;
public string DialogueIdentifier { get; set; }
public string TargetName { get; set; }
@@ -60,17 +62,27 @@ namespace Barotrauma
public override float GetPriority()
{
bool isOrder = objectiveManager.CurrentOrder == this;
if (!IsAllowed)
{
Priority = 0;
Abandon = !isOrder;
return Priority;
}
if (followControlledCharacter && Character.Controlled == null)
{
Priority = 0;
Abandon = !isOrder;
}
if (Target is Entity e && e.Removed)
{
Priority = 0;
Abandon = !isOrder;
}
if (IgnoreIfTargetDead && Target is Character character && character.IsDead)
{
Priority = 0;
Abandon = !isOrder;
}
else
{
@@ -84,7 +96,7 @@ namespace Barotrauma
}
else
{
Priority = objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : 10;
Priority = isOrder ? AIObjectiveManager.OrderPriority : 10;
}
}
return Priority;
@@ -93,7 +105,7 @@ namespace Barotrauma
public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0)
: base(character, objectiveManager, priorityModifier)
{
this.Target = target;
Target = target;
this.repeat = repeat;
waitUntilPathUnreachable = 3.0f;
this.getDivingGearIfNeeded = getDivingGearIfNeeded;
@@ -260,61 +272,156 @@ namespace Barotrauma
}
}
}
if (!character.AnimController.InWater)
{
useScooter = false;
checkScooterTimer = 0;
}
else if (checkScooterTimer <= 0)
{
useScooter = false;
checkScooterTimer = checkScooterTime;
string scooterTag = "scooter";
string batteryTag = "mobilebattery";
Item scooter = null;
bool isScooterEquipped = false;
float closeEnough = 250;
float squaredDistance = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition);
bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!mimic ||
(Target is Character targetCharacter && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2));
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, batteryTag, requireEquipped: true))
{
scooter = equippedScooters.FirstOrDefault();
isScooterEquipped = scooter != null;
}
else if (shouldUseScooter && HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> scooters, batteryTag, requireEquipped: false))
{
scooter = scooters.FirstOrDefault();
if (scooter != null)
{
isScooterEquipped = HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
}
}
if (scooter != null && isScooterEquipped)
{
if (shouldUseScooter)
{
useScooter = true;
}
else
{
// Unequip
character.Inventory.TryPutItem(scooter, character, CharacterInventory.anySlot);
}
}
}
else
{
checkScooterTimer -= deltaTime;
}
if (SteeringManager == PathSteering)
{
Func<PathNode, bool> nodeFilter = null;
if (isInside && !AllowGoingOutside)
{
nodeFilter = node => node.Waypoint.CurrentHull != null;
nodeFilter = n => n.Waypoint.CurrentHull != null;
}
PathSteering.SteeringSeek(character.GetRelativeSimPosition(Target), 1, n =>
{
if (n.Waypoint.isObstructed) { return false; }
return (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null);
}, endNodeFilter, nodeFilter, CheckVisibility);
PathSteering.SteeringSeek(character.GetRelativeSimPosition(Target), 1,
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null),
endNodeFilter,
nodeFilter,
CheckVisibility);
if (!isInside && PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable)
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
if (useScooter)
{
UseScooter(Target.WorldPosition);
}
else
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 2);
}
}
}
else if (useScooter && PathSteering.CurrentPath?.CurrentNode != null)
{
UseScooter(PathSteering.CurrentPath.CurrentNode.WorldPosition);
}
}
else
{
if (useScooter)
{
UseScooter(Target.WorldPosition);
}
else
{
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
}
}
}
else
}
void UseScooter(Vector2 targetWorldPos)
{
SteeringManager.Reset();
character.CursorPosition = targetWorldPos;
if (character.Submarine != null)
{
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
character.CursorPosition -= character.Submarine.Position;
}
Vector2 dir = Vector2.Normalize(character.CursorPosition - character.Position);
if (!MathUtils.IsValid(dir)) { dir = Vector2.UnitY; }
SteeringManager.SteeringManual(1.0f, dir);
character.SetInput(InputType.Aim, false, true);
character.SetInput(InputType.Shoot, false, true);
}
}
public Hull GetTargetHull()
private bool useScooter;
private float checkScooterTimer;
private readonly float checkScooterTime = 0.2f;
public Hull GetTargetHull() => GetTargetHull(Target);
public static Hull GetTargetHull(ISpatialEntity target)
{
if (Target is Hull h)
if (target is Hull h)
{
return h;
}
else if (Target is Item i)
else if (target is Item i)
{
return i.CurrentHull;
}
else if (Target is Character c)
else if (target is Character c)
{
return c.CurrentHull;
}
else if (Target is Gap g)
else if (target is Gap g)
{
return g.FlowTargetHull;
}
else if (Target is WayPoint wp)
else if (target is WayPoint wp)
{
return wp.CurrentHull;
}
else if (Target is FireSource fs)
else if (target is FireSource fs)
{
return fs.Hull;
}
else if (target is OrderTarget ot)
{
return ot.Hull;
}
return null;
}
@@ -361,7 +468,7 @@ namespace Barotrauma
{
if (Target is Item item)
{
if (character.CanInteractWith(item, out _, checkLinked: false)) { IsCompleted = true; }
if (!character.IsClimbing && character.CanInteractWith(item, out _, checkLinked: false)) { IsCompleted = true; }
}
else if (Target is Character targetCharacter)
{
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
@@ -11,14 +12,14 @@ namespace Barotrauma
{
public override string DebugTag => "idle";
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
private BehaviorType behavior;
public BehaviorType Behavior
{
get { return behavior; }
set
{
set
{
behavior = value;
switch (behavior)
{
@@ -27,16 +28,13 @@ namespace Barotrauma
newTargetIntervalMax = 20;
standStillMin = 2;
standStillMax = 10;
walkDurationMin = 5;
walkDurationMax = 10;
break;
case BehaviorType.Passive:
case BehaviorType.StayInHull:
newTargetIntervalMin = 60;
newTargetIntervalMax = 120;
standStillMin = 30;
standStillMax = 60;
walkDurationMin = 5;
walkDurationMax = 10;
break;
}
}
@@ -46,8 +44,8 @@ namespace Barotrauma
private float newTargetIntervalMax;
private float standStillMin;
private float standStillMax;
private float walkDurationMin;
private float walkDurationMax;
private readonly float walkDurationMin = 5;
private readonly float walkDurationMax = 10;
public enum BehaviorType
{
@@ -55,7 +53,7 @@ namespace Barotrauma
Passive,
StayInHull
}
public Hull TargetHull { get; set; }
private Hull currentTarget;
private float newTargetTimer;
@@ -86,7 +84,7 @@ namespace Barotrauma
protected override bool Check() => 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); }
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
public readonly HashSet<string> PreferredOutpostModuleTypes = new HashSet<string>();
@@ -142,6 +140,8 @@ namespace Barotrauma
timerMargin = 0;
}
private bool IsSteeringFinished() => PathSteering.CurrentPath != null && (PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable);
protected override void Act(float deltaTime)
{
if (PathSteering == null) { return; }
@@ -164,12 +164,30 @@ namespace Barotrauma
character.DeselectCharacter();
}
if (behavior != BehaviorType.StayInHull)
if (!character.IsClimbing)
{
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
character.SelectedConstruction = null;
}
bool IsSteeringFinished() => PathSteering.CurrentPath != null && (PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable);
CleanupItems(deltaTime);
if (behavior == BehaviorType.StayInHull)
{
currentTarget = TargetHull;
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
if (stayInHull)
{
Wander(deltaTime);
}
else if (currentTarget != null)
{
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
}
}
else
{
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (currentTarget != null && !currentTargetIsInvalid)
{
@@ -226,7 +244,7 @@ namespace Barotrauma
searchingNewHull = true;
return;
}
else if (targetHulls.Count > 0)
else if (targetHulls.Any())
{
//choose a random available hull
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
@@ -242,12 +260,13 @@ namespace Barotrauma
});
if (path.Unreachable)
{
//can't go to this room, remove it from the list and try another room next frame
//can't go to this room, remove it from the list and try another room
int index = targetHulls.IndexOf(currentTarget);
targetHulls.RemoveAt(index);
hullWeights.RemoveAt(index);
PathSteering.Reset();
currentTarget = null;
SetTargetTimerLow();
return;
}
searchingNewHull = false;
@@ -263,47 +282,25 @@ namespace Barotrauma
{
character.AIController.SelectTarget(currentTarget.AiTarget);
string errorMsg = null;
#if DEBUG
#if DEBUG
bool isRoomNameFound = currentTarget.DisplayName != null;
errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.DisplayName : currentTarget.ToString()) + ")";
#endif
#endif
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg, nodeFilter: node => node.Waypoint.CurrentHull != null);
PathSteering.SetPath(path);
}
SetTargetTimerNormal();
}
}
newTargetTimer -= deltaTime;
}
//wander randomly
// - if reached the end of the path
// - if the target is unreachable
// - if the path requires going outside
if (!character.IsClimbing)
{
if (behavior == BehaviorType.StayInHull || SteeringManager != PathSteering || (PathSteering.CurrentPath != null &&
(PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes)))
if (!character.IsClimbing && IsSteeringFinished())
{
Wander(deltaTime);
return;
}
character.SelectedConstruction = null;
}
if (currentTarget != null)
{
if (SteeringManager == PathSteering)
else if (currentTarget != null)
{
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
}
else
{
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(currentTarget));
}
}
else
{
Wander(deltaTime);
}
}
@@ -328,7 +325,7 @@ namespace Barotrauma
{
//if there are characters too close on both sides, don't try to steer away from them
//because it'll cause the character to spaz out trying to avoid both
if (tooCloseCharacter != null &&
if (tooCloseCharacter != null &&
Math.Sign(tooCloseCharacter.WorldPosition.X - character.WorldPosition.X) != Math.Sign(c.WorldPosition.X - character.WorldPosition.X))
{
tooCloseCharacter = null;
@@ -336,7 +333,7 @@ namespace Barotrauma
}
tooCloseCharacter = c;
}
HumanAIController.FaceTarget(c);
HumanAIController.FaceTarget(c);
}
}
@@ -344,7 +341,7 @@ namespace Barotrauma
{
Vector2 diff = character.WorldPosition - tooCloseCharacter.WorldPosition;
if (diff.LengthSquared() < 0.0001f) { diff = Rand.Vector(1.0f); }
if (Math.Abs(diff.X) > 0 &&
if (Math.Abs(diff.X) > 0 &&
(character.WorldPosition.X > currentHull.WorldRect.Right - 50 || character.WorldPosition.X < currentHull.WorldRect.Left + 50))
{
// Between a wall and a character -> move away
@@ -459,6 +456,48 @@ namespace Barotrauma
}
}
#region Cleaning
private readonly float checkItemsInterval = 1;
private float checkItemsTimer;
private readonly List<Item> itemsToClean = new List<Item>();
private readonly List<Item> ignoredItems = new List<Item>();
private void CleanupItems(float deltaTime)
{
if (checkItemsTimer <= 0)
{
checkItemsTimer = checkItemsInterval * Rand.Range(0.9f, 1.1f);
var hull = character.CurrentHull;
if (hull != null)
{
itemsToClean.Clear();
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveCleanupItems.IsValidTarget(item, character) && !ignoredItems.Contains(item))
{
itemsToClean.Add(item);
}
}
if (itemsToClean.Any())
{
var targetItem = itemsToClean.OrderBy(i => Math.Abs(character.WorldPosition.X - i.WorldPosition.X)).FirstOrDefault();
if (targetItem != null)
{
var cleanupObjective = new AIObjectiveCleanupItem(targetItem, character, objectiveManager, PriorityModifier);
cleanupObjective.Abandoned += () => ignoredItems.Add(targetItem);
subObjectives.Add(cleanupObjective);
}
}
}
}
else
{
checkItemsTimer -= deltaTime;
}
}
#endregion
public static bool IsForbidden(Hull hull)
{
if (hull == null) { return true; }
@@ -475,6 +514,9 @@ namespace Barotrauma
tooCloseCharacter = null;
targetHulls.Clear();
hullWeights.Clear();
checkItemsTimer = 0;;
itemsToClean.Clear();
ignoredItems.Clear();
autonomousObjectiveRetryTimer = 10;
}
}
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
@@ -46,7 +45,7 @@ namespace Barotrauma
public override bool AllowSubObjectiveSorting => true;
public virtual bool InverseTargetEvaluation => false;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); }
public override void Update(float deltaTime)
{
@@ -79,7 +78,12 @@ namespace Barotrauma
var target = objective.Key;
if (!Targets.Contains(target))
{
subObjectives.Remove(objective.Value);
var subObjective = objective.Value;
if (CurrentSubObjective == subObjective)
{
CurrentSubObjective.Abandon = !CurrentSubObjective.IsCompleted;
}
subObjectives.Remove(subObjective);
}
}
SyncRemovedObjectives(Objectives, GetList());
@@ -137,7 +141,7 @@ namespace Barotrauma
{
if (objectiveManager.CurrentOrder == this)
{
Priority = AIObjectiveManager.OrderPriority;
Priority = ForceOrderPriority ? AIObjectiveManager.OrderPriority : targetValue;
}
else
{
@@ -70,7 +70,7 @@ namespace Barotrauma
if (objective == null)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace.CleanupStackTrace());
#endif
return;
}
@@ -128,8 +128,7 @@ namespace Barotrauma
var orderPrefab = Order.GetPrefab(autonomousObjective.identifier);
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.identifier}'"); }
var item = orderPrefab.MustSetTarget ? orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID)?.GetRandom() : null;
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity,
item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType), orderGiver: character);
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 != Character.TeamType.FriendlyNPC) { continue; }
var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier);
@@ -147,7 +146,7 @@ namespace Barotrauma
if (objective == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace);
DebugConsole.ThrowError($"{character.Name}: Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace.CleanupStackTrace());
#endif
return;
}
@@ -314,9 +313,10 @@ namespace Barotrauma
};
break;
case "wait":
newObjective = new AIObjectiveGoTo(order.TargetEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier)
newObjective = new AIObjectiveGoTo(order.TargetSpatialEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier)
{
AllowGoingOutside = character.CurrentHull == null
AllowGoingOutside = order.TargetSpatialEntity == null ? character.CurrentHull == null :
character.Submarine == null || character.Submarine != order.TargetSpatialEntity.Submarine
};
break;
case "fixleaks":
@@ -374,6 +374,32 @@ namespace Barotrauma
Override = orderGiver != null && orderGiver.IsPlayer
};
break;
case "setchargepct":
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, false, priorityModifier: priorityModifier)
{
IsLoop = false,
Override = character.CurrentOrder != null,
completionCondition = () =>
{
if (float.TryParse(option, out float pct))
{
var targetRatio = Math.Clamp(pct, 0f, 1f);
var currentRatio = (order.TargetItemComponent as PowerContainer).RechargeRatio;
return Math.Abs(targetRatio - currentRatio) < 0.05f;
}
return true;
}
};
break;
case "getitem":
newObjective = new AIObjectiveGetItem(character, order.TargetEntity as Item ?? order.TargetItemComponent?.Item, this, false, priorityModifier: priorityModifier)
{
MustBeSpecificItem = true
};
break;
case "cleanupitems":
newObjective = new AIObjectiveCleanupItems(character, this, priorityModifier, order.TargetEntity as Item);
break;
default:
if (order.TargetItemComponent == null) { return null; }
if (order.TargetItemComponent.Item.NonInteractable) { return null; }
@@ -11,6 +11,7 @@ namespace Barotrauma
public override string DebugTag => $"operate item {component.Name}";
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowMultipleInstances => true;
public override bool AllowInAnySub => true;
private ItemComponent component, controller;
private Entity operateTarget;
@@ -35,9 +36,11 @@ namespace Barotrauma
public override float GetPriority()
{
bool isOrder = objectiveManager.CurrentOrder == this;
if (!IsAllowed || character.LockHands)
{
Priority = 0;
Abandon = !isOrder;
return Priority;
}
if (component.Item.ConditionPercentage <= 0)
@@ -46,7 +49,6 @@ namespace Barotrauma
}
else
{
bool isOrder = objectiveManager.CurrentOrder == this;
if (isOrder)
{
Priority = AIObjectiveManager.OrderPriority;
@@ -172,7 +174,7 @@ namespace Barotrauma
}
if (target.CanBeSelected)
{
if (character.CanInteractWith(target.Item, out _, checkLinked: false))
if (!character.IsClimbing && character.CanInteractWith(target.Item, out _, checkLinked: false))
{
HumanAIController.FaceTarget(target.Item);
if (character.SelectedConstruction != target.Item)
@@ -189,7 +191,8 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = target.Item.Name
TargetName = target.Item.Name,
endNodeFilter = node => node.Waypoint.Ladders == null
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
@@ -10,6 +10,8 @@ namespace Barotrauma
{
public override string DebugTag => "repair item";
public override bool AllowInAnySub => true;
public Item Item { get; private set; }
private AIObjectiveGoTo goToObjective;
@@ -34,6 +36,7 @@ namespace Barotrauma
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
return Priority;
}
// TODO: priority list?
@@ -69,7 +72,7 @@ namespace Barotrauma
IsCompleted = Item.IsFullCondition;
if (IsCompleted && IsRepairing())
{
character?.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f);
character.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f);
}
return IsCompleted;
}
@@ -134,7 +137,7 @@ namespace Barotrauma
return;
}
}
if (character.CanInteractWith(Item, out _, checkLinked: false))
if (!character.IsClimbing && character.CanInteractWith(Item, out _, checkLinked: false))
{
HumanAIController.FaceTarget(Item);
if (repairTool != null)
@@ -235,7 +238,11 @@ namespace Barotrauma
private void OperateRepairTool(float deltaTime)
{
character.CursorPosition = Item.Position;
character.CursorPosition = Item.WorldPosition;
if (character.Submarine != null)
{
character.CursorPosition -= character.Submarine.Position;
}
if (repairTool.Item.RequireAimToUse)
{
character.SetInput(InputType.Aim, false, true);
@@ -24,11 +24,11 @@ namespace Barotrauma
private readonly Item prioritizedItem;
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 as AIObjective) is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills;
public override bool IsDuplicate<T>(T otherObjective) => otherObjective is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills;
public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Item prioritizedItem = null)
: base(character, objectiveManager, priorityModifier)
@@ -151,13 +151,9 @@ namespace Barotrauma
if (item.NonInteractable) { return false; }
if (item.IsFullCondition) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine == null) { return false; }
if (item.Submarine.TeamID != character.TeamID) { return false; }
if (item.Submarine == null || character.Submarine == null) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { return false; }
if (item.Repairables.None()) { return false; }
if (character.Submarine != null)
{
if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; }
}
return true;
}
}
@@ -14,6 +14,7 @@ namespace Barotrauma
public override bool KeepDivingGearOn => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
const float TreatmentDelay = 0.5f;
@@ -35,7 +36,7 @@ namespace Barotrauma
{
if (targetCharacter == null)
{
string errorMsg = $"{character.Name}: Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace;
string errorMsg = $"{character.Name}: Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AIObjectiveRescue:ctor:targetnull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
Abandon = true;
@@ -392,11 +393,13 @@ namespace Barotrauma
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
return Priority;
}
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
Priority = 0;
Abandon = true;
}
else
{
@@ -11,6 +11,7 @@ namespace Barotrauma
public override bool ForceRun => true;
public override bool InverseTargetEvaluation => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
private const float vitalityThreshold = 75;
private const float vitalityThresholdForOrders = 85;
@@ -38,7 +38,8 @@ namespace Barotrauma
}
public bool MatchesOrder(Order order, string option) =>
order.Identifier == Order.Identifier && option == OrderOption && order.TargetEntity == Order.TargetEntity;
order.Identifier == Order.Identifier &&
option == OrderOption;
}
class Order
@@ -54,7 +55,7 @@ namespace Barotrauma
}
return order;
}
public Order Prefab { get; private set; }
public readonly string Name;
@@ -62,7 +63,8 @@ namespace Barotrauma
public readonly Sprite SymbolSprite;
public readonly Type ItemComponentType;
public readonly string[] ItemIdentifiers;
public readonly bool CanTypeBeSubclass;
public readonly string[] TargetItems;
public readonly string Identifier;
@@ -89,14 +91,14 @@ namespace Barotrauma
color = value;
}
}
//if true, the order is issued to all available characters
public bool TargetAllCharacters;
public readonly float FadeOutTime;
public Entity TargetEntity;
public Entity TargetEntity;
public ItemComponent TargetItemComponent;
public readonly bool UseController;
public Controller ConnectedController;
@@ -123,6 +125,18 @@ namespace Barotrauma
public bool IsPrefab { get; private set; }
public readonly bool MustManuallyAssign;
public readonly OrderTarget TargetPosition;
private ISpatialEntity targetSpatialEntity;
public ISpatialEntity TargetSpatialEntity
{
get
{
if (targetSpatialEntity == null) { targetSpatialEntity = TargetEntity ?? TargetPosition as ISpatialEntity; }
return targetSpatialEntity;
}
}
public static void Init()
{
Prefabs = new Dictionary<string, Order>();
@@ -218,8 +232,9 @@ namespace Barotrauma
DebugConsole.ThrowError("Error in the order definitions: item component type " + targetItemType + " not found", e);
}
}
CanTypeBeSubclass = orderElement.GetAttributeBool("cantypebesubclass", false);
ItemIdentifiers = orderElement.GetAttributeStringArray("targetitemidentifiers", new string[0], trim: true, convertToLowerInvariant: true);
TargetItems = orderElement.GetAttributeStringArray("targetitems", new string[0], trim: true, convertToLowerInvariant: true);
color = orderElement.GetAttributeColor("color");
FadeOutTime = orderElement.GetAttributeFloat("fadeouttime", 0.0f);
UseController = orderElement.GetAttributeBool("usecontroller", false);
@@ -228,7 +243,6 @@ namespace Barotrauma
Options = orderElement.GetAttributeStringArray("options", new string[0]);
var category = orderElement.GetAttributeString("category", null);
if (!string.IsNullOrWhiteSpace(category)) { this.Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); }
Weight = orderElement.GetAttributeFloat(0.0f, "weight");
MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false);
AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null);
@@ -279,7 +293,7 @@ namespace Barotrauma
IsPrefab = true;
MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false);
}
/// <summary>
/// Constructor for order instances
/// </summary>
@@ -290,7 +304,8 @@ namespace Barotrauma
Name = prefab.Name;
Identifier = prefab.Identifier;
ItemComponentType = prefab.ItemComponentType;
ItemIdentifiers = prefab.ItemIdentifiers;
CanTypeBeSubclass = prefab.CanTypeBeSubclass;
TargetItems = prefab.TargetItems;
Options = prefab.Options;
SymbolSprite = prefab.SymbolSprite;
Color = prefab.Color;
@@ -298,7 +313,6 @@ namespace Barotrauma
TargetAllCharacters = prefab.TargetAllCharacters;
AppropriateJobs = prefab.AppropriateJobs;
FadeOutTime = prefab.FadeOutTime;
Weight = prefab.Weight;
MustSetTarget = prefab.MustSetTarget;
AppropriateSkill = prefab.AppropriateSkill;
Category = prefab.Category;
@@ -325,6 +339,11 @@ namespace Barotrauma
IsPrefab = false;
}
public Order(Order prefab, OrderTarget target, Character orderGiver = null) : this(prefab, targetEntity: null, targetItem: null, orderGiver)
{
TargetPosition = target;
}
public bool HasAppropriateJob(Character character)
{
@@ -358,15 +377,38 @@ namespace Barotrauma
return msg;
}
/// <summary>
/// Get the target item component based on the target item type
/// </summary>
public ItemComponent GetTargetItemComponent(Item item)
{
if (item?.Components == null || ItemComponentType == null) { return null; }
foreach (ItemComponent component in item.Components)
{
if (component?.GetType() is Type componentType)
{
if (componentType == ItemComponentType) { return component; }
if (CanTypeBeSubclass && componentType.IsSubclassOf(ItemComponentType)) { return component; }
}
}
return null;
}
public bool TryGetTargetItemComponent(Item item, out ItemComponent firstMatchingComponent)
{
firstMatchingComponent = GetTargetItemComponent(item);
return firstMatchingComponent != null;
}
public List<Item> GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, Character.TeamType? requiredTeam = null)
{
List<Item> matchingItems = new List<Item>();
if (submarine == null) { return matchingItems; }
if (ItemComponentType != null || ItemIdentifiers.Length > 0)
if (ItemComponentType != null || TargetItems.Length > 0)
{
matchingItems = ItemIdentifiers.Length > 0 ?
Item.ItemList.FindAll(it => ItemIdentifiers.Contains(it.Prefab.Identifier) || it.HasTag(ItemIdentifiers)) :
Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == ItemComponentType));
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)
{
matchingItems.RemoveAll(it => it.Submarine?.Info != null && it.Submarine.Info.Type != SubmarineType.Player);
@@ -92,8 +92,10 @@ namespace Barotrauma
public GetNodePenaltyHandler GetNodePenalty;
private readonly List<PathNode> nodes;
public readonly bool IndoorsSteering;
public bool InsideSubmarine { get; set; }
public bool ApplyPenaltyToOutsideNodes { get; set; }
public PathFinder(List<WayPoint> wayPoints, bool indoorsSteering = false)
{
@@ -104,7 +106,7 @@ namespace Barotrauma
wp.linkedTo.CollectionChanged += WaypointLinksChanged;
}
InsideSubmarine = indoorsSteering;
IndoorsSteering = indoorsSteering;
}
void WaypointLinksChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
@@ -178,7 +180,7 @@ namespace Barotrauma
node.TempDistance = xDiff + (InsideSubmarine ? yDiff * 10.0f : yDiff); //higher cost for vertical movement when inside the sub
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null && InsideSubmarine) { node.TempDistance *= 10.0f; }
if (node.Waypoint.CurrentHull == null && ApplyPenaltyToOutsideNodes) { node.TempDistance *= 10.0f; }
//prefer nodes that are closer to the end position
node.TempDistance += (Math.Abs(end.X - node.TempPosition.X) + Math.Abs(end.Y - node.TempPosition.Y)) / 100.0f;
@@ -200,11 +202,13 @@ namespace Barotrauma
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (startNodeFilter != null && !startNodeFilter(node)) { continue; }
//if searching for a path inside the sub, make sure the waypoint is visible
if (InsideSubmarine)
if (IndoorsSteering)
{
if (node.Waypoint.isObstructed) { continue; }
// Always check the visibility for the start node
var body = Submarine.PickBody(
start, node.TempPosition, null,
start, node.TempPosition, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs);
if (body != null)
{
@@ -231,8 +235,11 @@ namespace Barotrauma
node.TempDistance = Vector2.DistanceSquared(end, node.TempPosition);
if (InsideSubmarine)
{
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null) { node.TempDistance *= 10.0f; }
if (ApplyPenaltyToOutsideNodes)
{
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null) { node.TempDistance *= 10.0f; }
}
//avoid stopping at a doorway
if (node.Waypoint.ConnectedDoor != null) { node.TempDistance *= 10.0f; }
//avoid stopping at a ladder
@@ -255,17 +262,20 @@ namespace Barotrauma
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (endNodeFilter != null && !endNodeFilter(node)) { continue; }
//if searching for a path inside the sub, make sure the waypoint is visible
if (InsideSubmarine && checkVisibility)
if (IndoorsSteering)
{
// Only check the visibility for the end node when allowed (fix leaks)
var body = Submarine.PickBody(end, node.TempPosition, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs );
if (body != null)
if (node.Waypoint.isObstructed) { continue; }
//if searching for a path inside the sub, make sure the waypoint is visible
if (checkVisibility)
{
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; }
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
// Only check the visibility for the end node when allowed (fix leaks)
var body = Submarine.PickBody(end, node.TempPosition, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs);
if (body != null)
{
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; }
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
}
}
}
endNode = node;
@@ -339,6 +349,7 @@ namespace Barotrauma
foreach (PathNode node in nodes)
{
if (node.state != 1) { continue; }
if (IndoorsSteering && node.Waypoint.isObstructed) { continue; }
if (filter != null && !filter(node)) { continue; }
if (node.F < dist)
{
@@ -0,0 +1,429 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class PetBehavior
{
public enum StatusIndicatorType
{
None,
Happy,
Sad,
Hungry
}
private float hunger = 50.0f;
public float Hunger
{
get { return hunger; }
set { hunger = MathHelper.Clamp(value, 0.0f, MaxHunger); }
}
private float happiness = 50.0f;
public float Happiness
{
get { return happiness; }
set { happiness = MathHelper.Clamp(value, 0.0f, MaxHappiness); }
}
public float MaxHappiness { get; set; }
public float MaxHunger { get; set; }
public float HappinessDecreaseRate { get; set; }
public float HungerIncreaseRate { get; set; }
public float PlayForce { get; set; }
public float PlayTimer { get; set; }
private float? unstunY { get; set; }
public EnemyAIController AiController { get; private set; } = null;
public Character Owner { get; set; }
private class ItemProduction
{
public struct Item
{
public ItemPrefab Prefab;
public float Commonness;
}
public List<Item> Items;
public Vector2 HungerRange;
public Vector2 HappinessRange;
public float Rate;
public float HungerRate;
public float InvHungerRate;
public float HappinessRate;
public float InvHappinessRate;
private readonly float totalCommonness;
private float timer;
public ItemProduction(XElement element)
{
Items = new List<Item>();
HungerRate = element.GetAttributeFloat("hungerrate", 0.0f);
InvHungerRate = element.GetAttributeFloat("invhungerrate", 0.0f);
HappinessRate = element.GetAttributeFloat("happinessrate", 0.0f);
InvHappinessRate = element.GetAttributeFloat("invhappinessrate", 0.0f);
string[] requiredHappinessStr = element.GetAttributeString("requiredhappiness", "0-100").Split('-');
string[] requiredHungerStr = element.GetAttributeString("requiredhunger", "0-100").Split('-');
HappinessRange = new Vector2(0, 100);
HungerRange = new Vector2(0, 100);
float tempF;
if (requiredHappinessStr.Length >= 2)
{
if (float.TryParse(requiredHappinessStr[0], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { HappinessRange.X = tempF; }
if (float.TryParse(requiredHappinessStr[1], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { HappinessRange.Y = tempF; }
}
if (requiredHungerStr.Length >= 2)
{
if (float.TryParse(requiredHungerStr[0], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { HungerRange.X = tempF; }
if (float.TryParse(requiredHungerStr[1], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { HungerRange.Y = tempF; }
}
Rate = element.GetAttributeFloat("rate", 0.016f);
totalCommonness = 0.0f;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.LocalName.ToLowerInvariant())
{
case "item":
string identifier = subElement.GetAttributeString("identifier", "");
Item newItemToProduce = new Item
{
Prefab = string.IsNullOrEmpty(identifier) ? null : ItemPrefab.Find("", subElement.GetAttributeString("identifier", "")),
Commonness = subElement.GetAttributeFloat("commonness", 0.0f)
};
totalCommonness += newItemToProduce.Commonness;
Items.Add(newItemToProduce);
break;
}
}
timer = 1.0f;
}
public void Update(PetBehavior pet, float deltaTime)
{
if (pet.Happiness < HappinessRange.X || pet.Happiness > HappinessRange.Y) { return; }
if (pet.Hunger < HungerRange.X || pet.Hunger > HungerRange.Y) { return; }
float currentRate = Rate;
currentRate += HappinessRate * (pet.Happiness - HappinessRange.X) / (HappinessRange.Y - HappinessRange.X);
currentRate += InvHappinessRate * (1.0f - ((pet.Happiness - HappinessRange.X) / (HappinessRange.Y - HappinessRange.X)));
currentRate += HungerRate * (pet.Hunger - HungerRange.X) / (HungerRange.Y - HungerRange.X);
currentRate += InvHungerRate * (1.0f - ((pet.Hunger - HungerRange.X) / (HungerRange.Y - HungerRange.X)));
timer -= currentRate * deltaTime;
if (timer <= 0.0f)
{
timer = 1.0f;
float r = Rand.Range(0.0f, totalCommonness);
float aggregate = 0.0f;
for (int i = 0; i < Items.Count; i++)
{
aggregate += Items[i].Commonness;
if (aggregate >= r && Items[i].Prefab != null)
{
Entity.Spawner.AddToSpawnQueue(Items[i].Prefab, pet.AiController.Character.WorldPosition);
break;
}
}
}
}
}
private class Food
{
public string Tag;
public Vector2 HungerRange;
public float Hunger;
public float Happiness;
public float Priority;
public bool IgnoreContained;
public CharacterParams.TargetParams TargetParams = null;
}
private readonly List<ItemProduction> itemsToProduce = new List<ItemProduction>();
private readonly List<Food> foods = new List<Food>();
public PetBehavior(XElement element, EnemyAIController aiController)
{
AiController = aiController;
AiController.Character.CanBeDragged = true;
MaxHappiness = element.GetAttributeFloat("maxhappiness", 100.0f);
MaxHunger = element.GetAttributeFloat("maxhunger", 100.0f);
Happiness = MaxHappiness * 0.5f;
Hunger = MaxHunger * 0.5f;
HappinessDecreaseRate = element.GetAttributeFloat("happinessdecreaserate", 0.1f);
HungerIncreaseRate = element.GetAttributeFloat("hungerincreaserate", 0.25f);
PlayForce = element.GetAttributeFloat("playforce", 15.0f);
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.LocalName.ToLowerInvariant())
{
case "itemproduction":
itemsToProduce.Add(new ItemProduction(subElement));
break;
case "eat":
Food food = new Food
{
Tag = subElement.GetAttributeString("tag", ""),
Hunger = subElement.GetAttributeFloat("hunger", -1),
Happiness = subElement.GetAttributeFloat("happiness", 1),
Priority = subElement.GetAttributeFloat("priority", 100),
IgnoreContained = subElement.GetAttributeBool("ignorecontained", true)
};
string[] requiredHungerStr = subElement.GetAttributeString("requiredhunger", "0-100").Split('-');
food.HungerRange = new Vector2(0, 100);
if (requiredHungerStr.Length >= 2)
{
if (float.TryParse(requiredHungerStr[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float tempF)) { food.HungerRange.X = tempF; }
if (float.TryParse(requiredHungerStr[1], NumberStyles.Any, CultureInfo.InvariantCulture, out tempF)) { food.HungerRange.Y = tempF; }
}
foods.Add(food);
break;
}
}
}
public StatusIndicatorType GetCurrentStatusIndicatorType()
{
if (Hunger > MaxHunger * 0.5f) { return StatusIndicatorType.Hungry; }
if (Happiness > MaxHappiness * 0.8f) { return StatusIndicatorType.Happy; }
if (Happiness < MaxHappiness * 0.25f) { return StatusIndicatorType.Sad; }
return StatusIndicatorType.None;
}
public bool OnEat(IEnumerable<string> tags, float amount)
{
foreach (string tag in tags)
{
if (OnEat(tag, amount)) { return true; }
}
return false;
}
public bool OnEat(string tag, float amount)
{
for (int i = 0; i < foods.Count; i++)
{
if (tag.Equals(foods[i].Tag, System.StringComparison.OrdinalIgnoreCase))
{
Hunger += foods[i].Hunger * amount;
Happiness += foods[i].Happiness * amount;
#if CLIENT
AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f);
#endif
return true;
}
}
return false;
}
public void Play(Character player)
{
if (PlayTimer > 0.0f) { return; }
if (Owner == null) { Owner = player; }
PlayTimer = 5.0f;
AiController.Character.IsRagdolled = true;
Happiness += 10.0f;
AiController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce);
unstunY = AiController.Character.SimPosition.Y;
#if CLIENT
AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f);
#endif
}
public string GetTagName()
{
if (AiController.Character.Inventory != null)
{
var items = AiController.Character.Inventory.Items;
for (int i = 0; i < items.Length; i++)
{
var item = items[i];
if (item == null) { continue; }
var tag = item.GetComponent<NameTag>();
if (tag != null && !string.IsNullOrWhiteSpace(tag.WrittenName))
{
return tag.WrittenName;
}
}
}
return string.Empty;
}
public void Update(float deltaTime)
{
var character = AiController.Character;
if (character?.Removed ?? true || character.IsDead) { return; }
if (unstunY.HasValue)
{
if (PlayTimer > 4.0f)
{
float extent = character.AnimController.MainLimb.body.GetMaxExtent();
if (character.SimPosition.Y < (unstunY.Value + extent * 3.0f) &&
character.AnimController.MainLimb.body.LinearVelocity.Y < 0.0f)
{
character.IsRagdolled = false;
unstunY = null;
}
else
{
character.IsRagdolled = true;
}
}
else
{
character.IsRagdolled = false;
unstunY = null;
}
}
PlayTimer -= deltaTime;
if (GameMain.NetworkMember?.IsClient ?? false) { return; }
if (Owner != null && (Owner.Removed || Owner.IsDead)) { Owner = null; }
Hunger += HungerIncreaseRate * deltaTime;
Happiness -= HappinessDecreaseRate * deltaTime;
for (int i = 0; i < foods.Count; i++)
{
Food food = foods[i];
if (Hunger >= food.HungerRange.X && Hunger <= food.HungerRange.Y)
{
if (food.TargetParams == null &&
AiController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out CharacterParams.TargetParams targetParams))
{
targetParams.IgnoreContained = food.IgnoreContained;
food.TargetParams = targetParams;
}
}
else if (food.TargetParams != null)
{
AiController.AIParams.RemoveTarget(food.TargetParams);
food.TargetParams = null;
}
}
if (Hunger >= MaxHunger * 0.99f)
{
character.CharacterHealth.ApplyAffliction(character.AnimController.MainLimb, new Affliction(AfflictionPrefab.InternalDamage, 8.0f * deltaTime));
}
else if (Hunger < MaxHunger * 0.1f)
{
character.CharacterHealth.ReduceAffliction(null, null, 8.0f * deltaTime);
}
if (character.SelectedBy != null)
{
character.IsRagdolled = true;
unstunY = character.SimPosition.Y;
}
for (int i = 0; i < itemsToProduce.Count; i++)
{
itemsToProduce[i].Update(this, deltaTime);
}
}
public static void SavePets(XElement petsElement)
{
foreach (Character c in Character.CharacterList)
{
if (!c.IsPet || c.IsDead) { continue; }
if (c.Submarine == null) { continue; }
var petBehavior = (c.AIController as EnemyAIController)?.PetBehavior;
if (petBehavior == null) { continue; }
XElement petElement = new XElement("pet",
new XAttribute("speciesname", c.SpeciesName),
new XAttribute("ownerid", petBehavior.Owner?.ID ?? Entity.NullEntityID),
new XAttribute("seed", c.Seed));
var petBehaviorElement = new XElement("petbehavior",
new XAttribute("hunger", petBehavior.Hunger.ToString("G", CultureInfo.InvariantCulture)),
new XAttribute("happiness", petBehavior.Happiness.ToString("G", CultureInfo.InvariantCulture)));
petElement.Add(petBehaviorElement);
var healthElement = new XElement("health");
c.CharacterHealth.Save(healthElement);
petElement.Add(healthElement);
if (c.Inventory != null)
{
var inventoryElement = new XElement("inventory");
c.SaveInventory(c.Inventory, inventoryElement);
petElement.Add(inventoryElement);
}
petsElement.Add(petElement);
}
}
public static void LoadPets(XElement petsElement)
{
foreach (XElement subElement in petsElement.Elements())
{
string speciesName = subElement.GetAttributeString("speciesname", "");
string seed = subElement.GetAttributeString("seed", "123");
ushort ownerID = (ushort)subElement.GetAttributeInt("ownerid", 0);
Vector2 spawnPos = Vector2.Zero;
Character owner = Entity.FindEntityByID(ownerID) as Character;
if (owner != null)
{
spawnPos = owner.WorldPosition;
}
else
{
var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandom();
spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition;
}
var pet = Character.Create(speciesName, spawnPos, seed);
var petBehavior = (pet.AIController as EnemyAIController)?.PetBehavior;
if (petBehavior != null)
{
petBehavior.Owner = owner;
var petBehaviorElement = subElement.Attribute("petbehavior");
if (petBehaviorElement != null)
{
petBehavior.Hunger = petBehaviorElement.GetAttributeFloat(50.0f);
petBehavior.Happiness = petBehaviorElement.GetAttributeFloat(50.0f);
}
}
}
}
public void ServerWrite(IWriteMessage msg)
{
msg.WriteRangedSingle(Happiness, 0.0f, MaxHappiness, 8);
msg.WriteRangedSingle(Hunger, 0.0f, MaxHunger, 8);
}
public void ClientRead(IReadMessage msg)
{
Happiness = msg.ReadRangedSingle(0.0f, MaxHappiness, 8);
Hunger = msg.ReadRangedSingle(0.0f, MaxHunger, 8);
}
}
}
@@ -47,6 +47,10 @@ namespace Barotrauma
base.Update(deltaTime, cam);
if (!Enabled) { return; }
if (!IsRemotePlayer && AIController is EnemyAIController enemyAi)
{
enemyAi.PetBehavior?.Update(deltaTime);
}
if (IsDead || Vitality <= 0.0f || Stun > 0.0f || IsIncapacitated)
{
//don't enable simple physics on dead/incapacitated characters
@@ -381,6 +381,12 @@ namespace Barotrauma
{
//only one limb left, the character is now full eaten
Entity.Spawner?.AddToRemoveQueue(target);
if (Character.AIController is EnemyAIController enemyAi)
{
enemyAi.PetBehavior?.OnEat("dead", 1.0f);
}
character.SelectedCharacter = null;
}
else //sever a random joint
@@ -423,113 +429,195 @@ namespace Barotrauma
{
WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5);
mainLimb.PullJointWorldAnchorB = Collider.SimPosition;
return;
}
Vector2 transformedMovement = reverse ? -movement : movement;
float movementAngle = MathUtils.VectorToAngle(transformedMovement) - MathHelper.PiOver2;
float mainLimbAngle = 0;
if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
{
mainLimbAngle = TorsoAngle.Value;
}
else if (mainLimb.type == LimbType.Head && HeadAngle.HasValue)
{
mainLimbAngle = HeadAngle.Value;
}
mainLimbAngle *= Dir;
while (mainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi)
{
movementAngle += MathHelper.TwoPi;
}
while (mainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi)
{
movementAngle -= MathHelper.TwoPi;
}
if (CurrentSwimParams.RotateTowardsMovement)
{
Collider.SmoothRotate(movementAngle, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
if (TorsoAngle.HasValue)
{
Limb torso = GetLimb(LimbType.Torso);
if (torso != null)
{
SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, mainLimb, TorsoTorque);
}
}
if (HeadAngle.HasValue)
{
Limb head = GetLimb(LimbType.Head);
if (head != null)
{
SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque);
}
}
if (TailAngle.HasValue)
{
Limb tail = GetLimb(LimbType.Tail);
if (tail != null)
{
float? mainLimbTargetAngle = null;
if (mainLimb.type == LimbType.Torso)
{
mainLimbTargetAngle = TorsoAngle;
}
else if (mainLimb.type == LimbType.Head)
{
mainLimbTargetAngle = HeadAngle;
}
float torque = TailTorque;
float maxMultiplier = CurrentSwimParams.TailTorqueMultiplier;
if (mainLimbTargetAngle.HasValue && maxMultiplier > 1)
{
float diff = Math.Abs(mainLimb.Rotation - tail.Rotation);
float offset = Math.Abs(mainLimbTargetAngle.Value - TailAngle.Value);
torque *= MathHelper.Lerp(1, maxMultiplier, MathUtils.InverseLerp(0, MathHelper.PiOver2, diff - offset));
}
SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, torque);
}
}
}
else
{
movementAngle = Dir > 0 ? -MathHelper.PiOver2 : MathHelper.PiOver2;
if (reverse)
Vector2 transformedMovement = reverse ? -movement : movement;
float movementAngle = MathUtils.VectorToAngle(transformedMovement) - MathHelper.PiOver2;
float mainLimbAngle = 0;
if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
{
movementAngle = MathUtils.WrapAngleTwoPi(movementAngle - MathHelper.Pi);
mainLimbAngle = TorsoAngle.Value;
}
if (mainLimb.type == LimbType.Head && HeadAngle.HasValue)
else if (mainLimb.type == LimbType.Head && HeadAngle.HasValue)
{
Collider.SmoothRotate(HeadAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
mainLimbAngle = HeadAngle.Value;
}
else if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
mainLimbAngle *= Dir;
while (mainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi)
{
Collider.SmoothRotate(TorsoAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
movementAngle += MathHelper.TwoPi;
}
if (TorsoAngle.HasValue)
while (mainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi)
{
Limb torso = GetLimb(LimbType.Torso);
torso?.body.SmoothRotate(TorsoAngle.Value * Dir, TorsoTorque);
movementAngle -= MathHelper.TwoPi;
}
if (HeadAngle.HasValue)
if (CurrentSwimParams.RotateTowardsMovement)
{
Limb head = GetLimb(LimbType.Head);
head?.body.SmoothRotate(HeadAngle.Value * Dir, HeadTorque);
}
if (TailAngle.HasValue)
{
Limb tail = GetLimb(LimbType.Tail);
tail?.body.SmoothRotate(TailAngle.Value * Dir, TailTorque);
}
}
Collider.SmoothRotate(movementAngle, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
if (TorsoAngle.HasValue)
{
Limb torso = GetLimb(LimbType.Torso);
if (torso != null)
{
SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, mainLimb, TorsoTorque);
}
}
if (HeadAngle.HasValue)
{
Limb head = GetLimb(LimbType.Head);
if (head != null)
{
SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque);
}
}
if (TailAngle.HasValue)
{
bool isAngleApplied = false;
foreach (var limb in Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.type != LimbType.Tail) { continue; }
if (!limb.Params.ApplyTailAngle) { continue; }
RotateTail(limb);
isAngleApplied = true;
}
if (!isAngleApplied)
{
RotateTail(GetLimb(LimbType.Tail));
}
var waveLength = Math.Abs(CurrentSwimParams.WaveLength * RagdollParams.JointScale);
var waveAmplitude = Math.Abs(CurrentSwimParams.WaveAmplitude * character.SpeedMultiplier);
if (waveLength > 0 && waveAmplitude > 0)
{
WalkPos -= transformedMovement.Length() / Math.Abs(waveLength);
WalkPos = MathUtils.WrapAngleTwoPi(WalkPos);
void RotateTail(Limb tail)
{
if (tail == null) { return; }
float? mainLimbTargetAngle = null;
if (mainLimb.type == LimbType.Torso)
{
mainLimbTargetAngle = TorsoAngle;
}
else if (mainLimb.type == LimbType.Head)
{
mainLimbTargetAngle = HeadAngle;
}
float torque = TailTorque;
float maxMultiplier = CurrentSwimParams.TailTorqueMultiplier;
if (mainLimbTargetAngle.HasValue && maxMultiplier > 1)
{
float diff = Math.Abs(mainLimb.Rotation - tail.Rotation);
float offset = Math.Abs(mainLimbTargetAngle.Value - TailAngle.Value);
torque *= MathHelper.Lerp(1, maxMultiplier, MathUtils.InverseLerp(0, MathHelper.PiOver2, diff - offset));
}
SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, torque);
}
}
}
else
{
movementAngle = Dir > 0 ? -MathHelper.PiOver2 : MathHelper.PiOver2;
if (reverse)
{
movementAngle = MathUtils.WrapAngleTwoPi(movementAngle - MathHelper.Pi);
}
if (mainLimb.type == LimbType.Head && HeadAngle.HasValue)
{
Collider.SmoothRotate(HeadAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
}
else if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
{
Collider.SmoothRotate(TorsoAngle.Value * Dir, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
}
if (TorsoAngle.HasValue)
{
Limb torso = GetLimb(LimbType.Torso);
torso?.body.SmoothRotate(TorsoAngle.Value * Dir, TorsoTorque);
}
if (HeadAngle.HasValue)
{
Limb head = GetLimb(LimbType.Head);
head?.body.SmoothRotate(HeadAngle.Value * Dir, HeadTorque);
}
if (TailAngle.HasValue)
{
bool isAngleApplied = false;
foreach (var limb in Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.type != LimbType.Tail) { continue; }
if (!limb.Params.ApplyTailAngle) { continue; }
RotateTail(limb);
isAngleApplied = true;
}
if (!isAngleApplied)
{
RotateTail(GetLimb(LimbType.Tail));
}
void RotateTail(Limb tail)
{
if (tail != null)
{
tail.body.SmoothRotate(TailAngle.Value * Dir, TailTorque);
}
}
}
}
var waveLength = Math.Abs(CurrentSwimParams.WaveLength * RagdollParams.JointScale);
var waveAmplitude = Math.Abs(CurrentSwimParams.WaveAmplitude * character.SpeedMultiplier);
if (waveLength > 0 && waveAmplitude > 0)
{
WalkPos -= transformedMovement.Length() / Math.Abs(waveLength);
WalkPos = MathUtils.WrapAngleTwoPi(WalkPos);
}
foreach (var limb in Limbs)
{
if (limb.IsSevered) { continue; }
switch (limb.type)
{
case LimbType.LeftFoot:
case LimbType.RightFoot:
if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID))
{
SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, mainLimb, FootTorque);
}
break;
case LimbType.Tail:
if (waveLength > 0 && waveAmplitude > 0)
{
float waveRotation = (float)Math.Sin(WalkPos * limb.Params.SineFrequencyMultiplier);
limb.body.ApplyTorque(waveRotation * limb.Mass * waveAmplitude * limb.Params.SineAmplitudeMultiplier);
}
break;
}
}
for (int i = 0; i < Limbs.Length; i++)
{
var limb = Limbs[i];
if (limb.IsSevered) { continue; }
if (limb.SteerForce <= 0.0f) { continue; }
if (!Collider.PhysEnabled) { continue; }
Vector2 pullPos = limb.PullJointWorldAnchorA;
limb.body.ApplyForce(movement * limb.SteerForce * limb.Mass * Math.Max(character.SpeedMultiplier, 1), pullPos);
}
Vector2 mainLimbDiff = mainLimb.PullJointWorldAnchorB - mainLimb.SimPosition;
if (CurrentSwimParams.UseSineMovement)
{
mainLimb.PullJointWorldAnchorB = Vector2.SmoothStep(
mainLimb.PullJointWorldAnchorB,
Collider.SimPosition,
mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : (float)Math.Abs(Math.Sin(WalkPos)));
}
else
{
//mainLimb.PullJointWorldAnchorB = Collider.SimPosition;
mainLimb.PullJointWorldAnchorB = Vector2.Lerp(
mainLimb.PullJointWorldAnchorB,
Collider.SimPosition,
mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : 0.5f);
}
}
foreach (var limb in Limbs)
@@ -537,55 +625,15 @@ namespace Barotrauma
if (limb.IsSevered) { continue; }
if (Math.Abs(limb.Params.ConstantTorque) > 0)
{
limb.body.SmoothRotate(movementAngle + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Params.ConstantTorque, wrapAngle: true);
limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque, wrapAngle: true);
}
switch (limb.type)
if (limb.Params.BlinkFrequency > 0)
{
case LimbType.LeftFoot:
case LimbType.RightFoot:
if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID))
{
SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, mainLimb, FootTorque);
}
break;
case LimbType.Tail:
if (waveLength > 0 && waveAmplitude > 0)
{
float waveRotation = (float)Math.Sin(WalkPos);
limb.body.ApplyTorque(waveRotation * limb.Mass * waveAmplitude);
}
break;
limb.Blink(deltaTime, MainLimb.Rotation);
}
}
for (int i = 0; i < Limbs.Length; i++)
{
var limb = Limbs[i];
if (limb.IsSevered) { continue; }
if (limb.SteerForce <= 0.0f) { continue; }
if (!Collider.PhysEnabled) { continue; }
Vector2 pullPos = limb.PullJointWorldAnchorA;
limb.body.ApplyForce(movement * limb.SteerForce * limb.Mass * Math.Max(character.SpeedMultiplier, 1), pullPos);
}
Vector2 mainLimbDiff = mainLimb.PullJointWorldAnchorB - mainLimb.SimPosition;
if (CurrentSwimParams.UseSineMovement)
{
mainLimb.PullJointWorldAnchorB = Vector2.SmoothStep(
mainLimb.PullJointWorldAnchorB,
Collider.SimPosition,
mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : (float)Math.Abs(Math.Sin(WalkPos)));
}
else
{
//mainLimb.PullJointWorldAnchorB = Collider.SimPosition;
mainLimb.PullJointWorldAnchorB = Vector2.Lerp(
mainLimb.PullJointWorldAnchorB,
Collider.SimPosition,
mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : 0.5f);
}
floorY = Limbs[0].SimPosition.Y;
floorY = Limbs[0].SimPosition.Y;
}
void UpdateWalkAnim(float deltaTime)
@@ -655,7 +703,7 @@ namespace Barotrauma
if (head != null)
{
bool headFacingBackwards = false;
if (HeadAngle.HasValue)
if (HeadAngle.HasValue && head != mainLimb)
{
SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque);
if (Math.Sign(head.SimPosition.X - mainLimb.SimPosition.X) != Math.Sign(Dir))
@@ -680,10 +728,26 @@ namespace Barotrauma
if (TailAngle.HasValue)
{
var tail = GetLimb(LimbType.Tail);
if (tail != null)
bool isAngleApplied = false;
foreach (var limb in Limbs)
{
SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, TailTorque);
if (limb.IsSevered) { continue; }
if (limb.type != LimbType.Tail) { continue; }
if (!limb.Params.ApplyTailAngle) { continue; }
RotateTail(limb);
isAngleApplied = true;
}
if (!isAngleApplied)
{
RotateTail(GetLimb(LimbType.Tail));
}
void RotateTail(Limb tail)
{
if (tail != null)
{
SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, TailTorque);
}
}
}
@@ -703,7 +767,11 @@ namespace Barotrauma
if (limb.IsSevered) { continue; }
if (Math.Abs(limb.Params.ConstantTorque) > 0)
{
limb.body.SmoothRotate(movementAngle + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Params.ConstantTorque, wrapAngle: true);
limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque, wrapAngle: true);
}
if (limb.Params.BlinkFrequency > 0)
{
limb.Blink(deltaTime, MainLimb.Rotation);
}
switch (limb.type)
{
@@ -785,11 +853,35 @@ namespace Barotrauma
float noise = (PerlinNoise.GetPerlin(WalkPos * 0.002f, WalkPos * 0.003f) - 0.5f) * 5.0f;
float animStrength = (1.0f - deathAnimTimer / deathAnimDuration);
Limb head = GetLimb(LimbType.Head);
if (head != null && head.IsSevered) { return; }
Limb baseLimb = GetLimb(LimbType.Head);
//if head is the main limb, it technically can't be severed - the rest of the limbs are considered severed if the head gets cut off
if (baseLimb == MainLimb)
{
int connectedToHeadCount = GetConnectedLimbs(baseLimb).Count;
//if there's nothing connected to the head, don't make it wiggle by itself
if (connectedToHeadCount == 1) { baseLimb = null; }
Limb torso = GetLimb(LimbType.Torso, excludeSevered: false);
if (torso != null)
{
//if there are more limbs connected to the torso than to the head, make the torso wiggle instead
int connectedToTorsoCount = GetConnectedLimbs(torso).Count;
if (connectedToTorsoCount > connectedToHeadCount)
{
baseLimb = torso;
}
}
}
else if (baseLimb == null)
{
baseLimb = GetLimb(LimbType.Torso, excludeSevered: true);
if (baseLimb == null) { return; }
}
var connectedToBaseLimb = GetConnectedLimbs(baseLimb);
Limb tail = GetLimb(LimbType.Tail);
if (head != null && !head.IsSevered) head.body.ApplyTorque((float)(Math.Sqrt(head.Mass) * Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength);
if (tail != null && !tail.IsSevered) tail.body.ApplyTorque((float)(Math.Sqrt(tail.Mass) * -Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength);
if (baseLimb != null) { baseLimb.body.ApplyTorque((float)(Math.Sqrt(baseLimb.Mass) * Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength); }
if (tail != null && connectedToBaseLimb.Contains(tail)) { tail.body.ApplyTorque((float)(Math.Sqrt(tail.Mass) * -Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength); }
WalkPos += deltaTime * 10.0f * animStrength;
@@ -797,7 +889,7 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) { continue; }
if (!connectedToBaseLimb.Contains(limb)) { continue; }
#if CLIENT
if (limb.LightSource != null)
{
@@ -145,7 +145,7 @@ namespace Barotrauma
private set;
}
private LimbJoint shoulder;
private LimbJoint rightShoulder, leftShoulder;
private float upperLegLength = 0.0f, lowerLegLength = 0.0f;
@@ -167,7 +167,7 @@ namespace Barotrauma
{
get
{
return Crouching ? CurrentGroundedParams.CrouchingTorsoPos * RagdollParams.JointScale : base.TorsoPosition;
return Crouching && !swimming ? CurrentGroundedParams.CrouchingTorsoPos * RagdollParams.JointScale : base.TorsoPosition;
}
}
@@ -175,7 +175,7 @@ namespace Barotrauma
{
get
{
return Crouching ? CurrentGroundedParams.CrouchingHeadPos * RagdollParams.JointScale : base.HeadPosition;
return Crouching && !swimming ? CurrentGroundedParams.CrouchingHeadPos * RagdollParams.JointScale : base.HeadPosition;
}
}
@@ -183,7 +183,7 @@ namespace Barotrauma
{
get
{
return Crouching ? MathHelper.ToRadians(CurrentGroundedParams.CrouchingTorsoAngle) : base.TorsoAngle;
return Crouching && !swimming ? MathHelper.ToRadians(CurrentGroundedParams.CrouchingTorsoAngle) : base.TorsoAngle;
}
}
@@ -191,7 +191,7 @@ namespace Barotrauma
{
get
{
return Crouching ? MathHelper.ToRadians(CurrentGroundedParams.CrouchingHeadAngle) : base.HeadAngle;
return Crouching && !swimming ? MathHelper.ToRadians(CurrentGroundedParams.CrouchingHeadAngle) : base.HeadAngle;
}
}
@@ -241,12 +241,13 @@ namespace Barotrauma
Limb rightHand = GetLimb(LimbType.RightHand);
if (rightHand == null) { return; }
shoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.RightArm);
rightShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.RightArm);
leftShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.LeftArm);
Vector2 localAnchorShoulder = Vector2.Zero;
Vector2 localAnchorElbow = Vector2.Zero;
if (shoulder != null)
if (rightShoulder != null)
{
localAnchorShoulder = shoulder.LimbA.type == LimbType.RightArm ? shoulder.LocalAnchorA : shoulder.LocalAnchorB;
localAnchorShoulder = rightShoulder.LimbA.type == LimbType.RightArm ? rightShoulder.LocalAnchorA : rightShoulder.LocalAnchorB;
}
LimbJoint rightElbow = rightForearm == null ?
GetJointBetweenLimbs(LimbType.RightArm, LimbType.RightHand) :
@@ -322,11 +323,12 @@ namespace Barotrauma
if (MainLimb == null) { return; }
levitatingCollider = true;
ColliderIndex = Crouching ? 1 : 0;
if (character.SelectedConstruction?.GetComponent<Controller>()?.ControlCharacterPose ?? false)
ColliderIndex = Crouching && !swimming ? 1 : 0;
if (character.SelectedConstruction?.GetComponent<Controller>()?.ControlCharacterPose ?? false ||
(ForceSelectAnimationType != AnimationType.Walk && ForceSelectAnimationType != AnimationType.NotDefined))
{
Crouching = false;
ColliderIndex = 0;
}
else if (!Crouching && ColliderIndex == 1)
{
@@ -1611,7 +1613,7 @@ namespace Barotrauma
pullLimb.PullJointMaxForce = 5000.0f;
targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f);
Vector2 shoulderPos = shoulder.WorldAnchorA;
Vector2 shoulderPos = rightShoulder.WorldAnchorA;
Vector2 dragDir = inWater ? Vector2.Normalize(targetLimb.SimPosition - shoulderPos) : Vector2.UnitY;
targetAnchor = shoulderPos - dragDir * ConvertUnits.ToSimUnits(upperArmLength + forearmLength);
@@ -1756,10 +1758,10 @@ namespace Barotrauma
}
else
{
itemAngle = (torso.body.Rotation + holdAngle * Dir);
itemAngle = torso.body.Rotation + holdAngle * Dir;
}
Vector2 transformedHoldPos = shoulder.WorldAnchorA;
Vector2 transformedHoldPos = rightShoulder.WorldAnchorA;
if (itemPos == Vector2.Zero || isClimbing || usingController)
{
if (character.SelectedItems[0] == item)
@@ -1780,15 +1782,17 @@ namespace Barotrauma
if (character.SelectedItems[0] == item)
{
if (rightHand == null || rightHand.IsSevered) { return; }
transformedHoldPos = rightShoulder.WorldAnchorA;
rightHand.Disabled = true;
}
if (character.SelectedItems[1] == item)
{
if (leftHand == null || leftHand.IsSevered) { return; }
transformedHoldPos = leftShoulder.WorldAnchorA;
leftHand.Disabled = true;
}
itemPos.X = itemPos.X * Dir;
itemPos.X *= Dir;
transformedHoldPos += Vector2.Transform(itemPos, Matrix.CreateRotationZ(itemAngle));
}
@@ -1857,18 +1861,21 @@ namespace Barotrauma
private void HandIK(Limb hand, Vector2 pos, float force = 1.0f)
{
if (shoulder == null) { return; }
Vector2 shoulderPos = shoulder.WorldAnchorA;
Vector2 shoulderPos;
Limb arm, forearm;
if (hand.type == LimbType.LeftHand)
{
if (leftShoulder == null) { return; }
shoulderPos = leftShoulder.WorldAnchorA;
arm = GetLimb(LimbType.LeftArm);
forearm = GetLimb(LimbType.LeftForearm);
LeftHandIKPos = pos;
}
else
{
if (rightShoulder == null) { return; }
shoulderPos = rightShoulder.WorldAnchorA;
arm = GetLimb(LimbType.RightArm);
forearm = GetLimb(LimbType.RightForearm);
RightHandIKPos = pos;
@@ -1903,7 +1910,7 @@ namespace Barotrauma
{
if (!MathUtils.IsValid(pos))
{
string errorMsg = "Invalid foot position in FootIK (" + pos + ")\n" + Environment.StackTrace;
string errorMsg = "Invalid foot position in FootIK (" + pos + ")\n" + Environment.StackTrace.CleanupStackTrace();
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
@@ -1937,7 +1944,7 @@ namespace Barotrauma
float legAngle = MathUtils.VectorToAngle(pos - waistPos) + MathHelper.PiOver2;
if (!MathUtils.IsValid(legAngle))
{
string errorMsg = "Invalid leg angle (" + legAngle + ") in FootIK. Waist pos: " + waistPos + ", target pos: " + pos + "\n" + Environment.StackTrace;
string errorMsg = "Invalid leg angle (" + legAngle + ") in FootIK. Waist pos: " + waistPos + ", target pos: " + pos + "\n" + Environment.StackTrace.CleanupStackTrace();
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
@@ -60,12 +60,12 @@ namespace Barotrauma
if (!accessRemovedCharacterErrorShown)
{
string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
errorMsg += '\n' + Environment.StackTrace;
errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"Ragdoll.Limbs:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace);
"Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace());
accessRemovedCharacterErrorShown = true;
}
return new Limb[0];
@@ -216,16 +216,18 @@ namespace Barotrauma
get
{
Limb mainLimb = GetLimb(RagdollParams.MainLimb);
if (mainLimb == null)
if (!IsValid(mainLimb))
{
Limb torso = GetLimb(LimbType.Torso);
Limb head = GetLimb(LimbType.Head);
mainLimb = torso ?? head;
if (mainLimb == null)
if (!IsValid(mainLimb))
{
mainLimb = Limbs.FirstOrDefault(l => !l.IsSevered && !l.ignoreCollisions);
mainLimb = Limbs.FirstOrDefault(l => IsValid(l));
}
}
bool IsValid(Limb limb) => limb != null && !limb.IsSevered && !limb.ignoreCollisions;
return mainLimb;
}
}
@@ -764,11 +766,10 @@ namespace Barotrauma
}
}
if (!string.IsNullOrEmpty(character.BloodDecalName))
{
character.CurrentHull?.AddDecal(character.BloodDecalName,
(limbJoint.LimbA.WorldPosition + limbJoint.LimbB.WorldPosition) / 2, MathHelper.Clamp(Math.Min(limbJoint.LimbA.Mass, limbJoint.LimbB.Mass), 0.5f, 2.0f), true);
(limbJoint.LimbA.WorldPosition + limbJoint.LimbB.WorldPosition) / 2, MathHelper.Clamp(Math.Min(limbJoint.LimbA.Mass, limbJoint.LimbB.Mass), 0.5f, 2.0f), isNetworkEvent: false);
}
SeverLimbJointProjSpecific(limbJoint, playSound: true);
@@ -781,6 +782,14 @@ namespace Barotrauma
partial void SeverLimbJointProjSpecific(LimbJoint limbJoint, bool playSound);
protected List<Limb> GetConnectedLimbs(Limb limb)
{
connectedLimbs.Clear();
checkedJoints.Clear();
GetConnectedLimbs(connectedLimbs, checkedJoints, limb);
return connectedLimbs;
}
private void GetConnectedLimbs(List<Limb> connectedLimbs, List<LimbJoint> checkedJoints, Limb limb)
{
connectedLimbs.Add(limb);
@@ -905,7 +914,7 @@ namespace Barotrauma
GameAnalyticsManager.AddErrorEventOnce(
"Ragdoll.FindHull:InvalidPosition",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to find a hull at an invalid position (" + findPos + ")\n" + Environment.StackTrace);
"Attempted to find a hull at an invalid position (" + findPos + ")\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
@@ -1557,11 +1566,11 @@ namespace Barotrauma
{
if (!MathUtils.IsValid(simPosition))
{
DebugConsole.ThrowError("Attempted to move a ragdoll (" + character.Name + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to move a ragdoll (" + character.Name + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace.CleanupStackTrace());
GameAnalyticsManager.AddErrorEventOnce(
"Ragdoll.SetPosition:InvalidPosition",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to move a ragdoll (" + character.Name + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace);
"Attempted to move a ragdoll (" + character.Name + ") to an invalid position (" + simPosition + "). " + Environment.StackTrace.CleanupStackTrace());
return;
}
if (MainLimb == null) { return; }
@@ -40,7 +40,11 @@ namespace Barotrauma
struct AttackResult
{
public readonly float Damage;
public float Damage
{
get;
private set;
}
public readonly List<Affliction> Afflictions;
public readonly Limb HitLimb;
@@ -64,9 +68,7 @@ namespace Barotrauma
{
Damage = damage;
HitLimb = null;
Afflictions = null;
AppliedDamageModifiers = appliedDamageModifiers;
}
}
@@ -18,7 +18,7 @@ namespace Barotrauma
{
if (type == CauseOfDeathType.Affliction && affliction == null)
{
string errorMsg = "Invalid cause of death (the type of the cause of death was Affliction, but affliction was not specified).\n" + Environment.StackTrace;
string errorMsg = "Invalid cause of death (the type of the cause of death was Affliction, but affliction was not specified).\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("InvalidCauseOfDeath", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
type = CauseOfDeathType.Unknown;
@@ -131,12 +131,14 @@ namespace Barotrauma
protected float oxygenAvailable;
//seed used to generate this character
private readonly string seed;
public readonly string Seed;
protected Item focusedItem;
private Character selectedCharacter, selectedBy;
public Character LastAttacker;
public Entity LastDamageSource;
public float InvisibleTimer;
private CharacterPrefab prefab;
public readonly CharacterParams Params;
@@ -181,6 +183,8 @@ namespace Barotrauma
public bool IsTraitor;
public string TraitorCurrentObjective = "";
public bool IsHuman => SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase);
public bool IsMale => Info != null && Info.HasGenders && Info.Gender == Gender.Male;
public bool IsFemale => Info != null && Info.HasGenders && Info.Gender == Gender.Female;
private float attackCoolDown;
@@ -195,6 +199,7 @@ namespace Barotrauma
if (Info != null) { Info.CurrentOrder = value; }
}
}
public string CurrentOrderOption
{
get
@@ -207,10 +212,9 @@ namespace Barotrauma
}
}
public bool IsDismissed => Info != null && Info.IsDismissed;
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
private readonly List<float> speedMultipliers = new List<float>();
private float greatestNegativeSpeedMultiplier = 1f;
private float greatestPositiveSpeedMultiplier = 1f;
public Entity ViewTarget
{
@@ -267,6 +271,12 @@ namespace Barotrauma
{
get
{
if (IsPet)
{
string petName = (AIController as EnemyAIController).PetBehavior.GetTagName();
if (!string.IsNullOrEmpty(petName)) { return petName; }
}
if (info != null && !string.IsNullOrWhiteSpace(info.Name)) { return info.Name; }
var displayName = Params.DisplayName;
if (string.IsNullOrWhiteSpace(displayName))
@@ -466,6 +476,11 @@ namespace Barotrauma
get { return CharacterHealth.IsUnconscious; }
}
public bool IsPet
{
get { return AIController is EnemyAIController enemyController && enemyController.PetBehavior != null; }
}
public float Oxygen
{
get { return CharacterHealth.OxygenAmount; }
@@ -481,7 +496,7 @@ namespace Barotrauma
get { return oxygenAvailable; }
set { oxygenAvailable = MathHelper.Clamp(value, 0.0f, 100.0f); }
}
public float Stun
{
get { return IsRagdolled ? 1.0f : CharacterHealth.StunTimer; }
@@ -563,7 +578,28 @@ namespace Barotrauma
get { return selectedItems; }
}
public Item SelectedConstruction { get; set; }
private Item _selectedConstruction;
public Item SelectedConstruction
{
get => _selectedConstruction;
set
{
_selectedConstruction = value;
#if CLIENT
if (Controlled == this)
{
if (_selectedConstruction == null)
{
GameMain.GameSession?.CrewManager?.ResetCrewList();
}
else if (_selectedConstruction.GetComponent<Ladder>() == null)
{
GameMain.GameSession?.CrewManager?.AutoHideCrewList();
}
}
#endif
}
}
public Item FocusedItem
{
@@ -584,6 +620,8 @@ namespace Barotrauma
public bool IsDead { get; private set; }
public bool IsObserving => AIController is EnemyAIController enemyAI && enemyAI.Enabled && enemyAI.State == AIState.Observe;
public bool EnableDespawn { get; set; } = true;
public CauseOfDeath CauseOfDeath
@@ -608,7 +646,7 @@ namespace Barotrauma
{
if (!canBeDragged) { return false; }
if (Removed || !AnimController.Draggable) { return false; }
return IsDead || Stun > 0.0f || LockHands || IsIncapacitated;
return IsDead || Stun > 0.0f || LockHands || IsIncapacitated || IsPet;
}
set { canBeDragged = value; }
}
@@ -662,12 +700,12 @@ namespace Barotrauma
{
errorMsg += " AnimController.Collider == null";
}
errorMsg += '\n' + Environment.StackTrace;
errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace();
DebugConsole.NewMessage(errorMsg, Color.Red);
GameAnalyticsManager.AddErrorEventOnce(
"Character.SimPosition:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
errorMsg + "\n" + Environment.StackTrace);
errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace());
accessRemovedCharacterErrorShown = true;
}
return Vector2.Zero;
@@ -728,6 +766,13 @@ namespace Barotrauma
{
speciesName = Path.GetFileNameWithoutExtension(speciesName).ToLowerInvariant();
}
if (CharacterPrefab.FindBySpeciesName(speciesName) == null)
{
DebugConsole.ThrowError($"Failed to create character \"{speciesName}\". Matching prefab not found.\n" + Environment.StackTrace);
return null;
}
Character newCharacter = null;
if (!speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase))
{
@@ -790,7 +835,7 @@ namespace Barotrauma
{
prefab = CharacterPrefab.FindBySpeciesName(speciesName);
this.seed = seed;
this.Seed = seed;
MTRandom random = new MTRandom(ToolBox.StringToInt(seed));
selectedItems = new Item[2];
@@ -1188,10 +1233,13 @@ namespace Barotrauma
return targetMovement;
}
private float greatestNegativeSpeedMultiplier = 1f;
private float greatestPositiveSpeedMultiplier = 1f;
/// <summary>
/// Can be used to modify the character's speed via StatusEffects
/// </summary>
public float SpeedMultiplier { get; private set; }
public float SpeedMultiplier { get; private set; } = 1;
public void StackSpeedMultiplier(float val)
{
@@ -1217,6 +1265,40 @@ namespace Barotrauma
greatestNegativeSpeedMultiplier = 1f;
}
private float greatestNegativeHealthMultiplier = 1f;
private float greatestPositiveHealthMultiplier = 1f;
/// <summary>
/// Can be used to modify the character's health via StatusEffects
/// </summary>
public float HealthMultiplier { get; private set; } = 1;
public void StackHealthMultiplier(float val)
{
if (val < 1f)
{
if (val < greatestNegativeHealthMultiplier)
{
greatestNegativeHealthMultiplier = val;
}
}
else
{
if (val > greatestPositiveHealthMultiplier)
{
greatestPositiveHealthMultiplier = val;
}
}
}
private void CalculateHealthMultiplier()
{
HealthMultiplier = greatestPositiveHealthMultiplier - (1f - greatestNegativeHealthMultiplier);
// Reset, status effects should set the values again, if the conditions match
greatestPositiveHealthMultiplier = 1f;
greatestNegativeHealthMultiplier = 1f;
}
/// <summary>
/// Speed reduction from the current limb specific damage. Min 0, max 1.
/// </summary>
@@ -1780,7 +1862,6 @@ namespace Barotrauma
if (ignoreBroken && item.Condition <= 0) { continue; }
if (Submarine != null)
{
if (item.Submarine.Info.Type != Submarine.Info.Type) { continue; }
if (!Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
}
if (customPredicate != null && !customPredicate(item)) { continue; }
@@ -1813,7 +1894,7 @@ namespace Barotrauma
public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true, bool skipDistanceCheck = false)
{
if (c == this || Removed || !c.Enabled || !c.CanBeSelected) { return false; }
if (c == this || Removed || !c.Enabled || !c.CanBeSelected || c.InvisibleTimer > 0.0f) { return false; }
if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && (c.onCustomInteract == null || !c.AllowCustomInteract)) { return false; }
if (!skipDistanceCheck)
@@ -2103,6 +2184,10 @@ namespace Barotrauma
{
SelectCharacter(FocusedCharacter);
}
else if (FocusedCharacter != null && !FocusedCharacter.IsIncapacitated && IsKeyHit(InputType.Use) && FocusedCharacter.IsPet && CanInteract)
{
(FocusedCharacter.AIController as EnemyAIController).PetBehavior.Play(this);
}
else if (FocusedCharacter != null && IsKeyHit(InputType.Health) && FocusedCharacter.CharacterHealth.UseHealthWindow && CanInteract && CanInteractWith(FocusedCharacter, 160f, false))
{
if (FocusedCharacter == SelectedCharacter)
@@ -2340,6 +2425,7 @@ namespace Barotrauma
}
ApplyStatusEffects(AnimController.InWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
UpdateControlled(deltaTime, cam);
@@ -2348,6 +2434,8 @@ namespace Barotrauma
{
UpdateOxygen(deltaTime);
}
CalculateHealthMultiplier();
CharacterHealth.Update(deltaTime);
if (IsIncapacitated)
@@ -2654,7 +2742,7 @@ namespace Barotrauma
if (orderGiver != null && !CanHearCharacter(orderGiver)) { return; }
// If there's another character operating the same device, make them dismiss themself
if (order != null && order.Category == OrderCategory.Operate)
if (order != null && order.Category == OrderCategory.Operate && order.TargetEntity != null)
{
CharacterList.FindAll(c => c != this && c.TeamID == TeamID && c.CurrentOrder is Order characterOrder && characterOrder.Category == OrderCategory.Operate &&
characterOrder.Identifier.Equals(order.Identifier) && characterOrder.TargetEntity == order.TargetEntity)?
@@ -2794,7 +2882,7 @@ namespace Barotrauma
{
if (Removed)
{
string errorMsg = "Tried to apply an attack to a removed character (" + Name + ").\n" + Environment.StackTrace;
string errorMsg = "Tried to apply an attack to a removed character (" + Name + ").\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Character.ApplyAttack:RemovedCharacter", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return new AttackResult();
@@ -2838,7 +2926,7 @@ namespace Barotrauma
}
#endif
// Don't allow beheading for monster attacks, because it happens too frequently (crawlers/tigerthreshers etc attacking each other -> they will most often target to the head)
TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: AIController == null || AIController is HumanAIController);
TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: attacker == null || attacker.IsHuman || attacker.IsPlayer);
return attackResult;
}
@@ -2864,7 +2952,8 @@ namespace Barotrauma
foreach (LimbJoint joint in AnimController.LimbJoints)
{
if (!joint.CanBeSevered) { continue; }
if (joint.LimbA != targetLimb && joint.LimbB != targetLimb) { continue; }
// Limb A is where we usually create the joints from. Let's not allow severing when the "parent" limb is hit, or the head can pop off when we hit the torso, for example.
if (joint.LimbB != targetLimb) { continue; }
float probability = severLimbsProbability;
if (!IsDead)
{
@@ -2880,7 +2969,7 @@ namespace Barotrauma
if (severed)
{
Limb otherLimb = joint.LimbA == targetLimb ? joint.LimbB : joint.LimbA;
otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass);
otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f);
ApplyStatusEffects(ActionType.OnSevered, 1.0f);
targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
otherLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
@@ -2937,7 +3026,7 @@ namespace Barotrauma
// string errorMsg = $"Character {Name} received damage from outside the sub while inside (attacker: {attacker.Name})";
// GameAnalyticsManager.AddErrorEventOnce("Character.DamageLimb:DamageFromOutside" + Name + attacker.Name,
// GameAnalyticsSDK.Net.EGAErrorSeverity.Warning,
// errorMsg + "\n" + Environment.StackTrace);
// errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace());
//#if DEBUG
// DebugConsole.ThrowError(errorMsg);
//#endif
@@ -2968,11 +3057,6 @@ namespace Barotrauma
Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir);
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound);
CharacterHealth.ApplyDamage(hitLimb, attackResult);
ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
if (attackResult.Damage > 0)
{
hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
}
if (attacker != this)
{
OnAttacked?.Invoke(attacker, attackResult);
@@ -2982,12 +3066,15 @@ namespace Barotrauma
TryAdjustAttackerSkill(attacker, -attackResult.Damage);
}
};
if (attacker != null && attackResult.Damage > 0.0f)
if (attackResult.Damage > 0)
{
LastAttacker = attacker;
ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
if (attacker != null)
{
LastAttacker = attacker;
}
}
return attackResult;
}
@@ -3045,7 +3132,7 @@ namespace Barotrauma
{
targets.Clear();
statusEffect.GetNearbyTargets(WorldPosition, targets);
statusEffect.Apply(ActionType.OnActive, deltaTime, this, targets);
statusEffect.Apply(actionType, deltaTime, this, targets);
}
else
{
@@ -3147,6 +3234,11 @@ namespace Barotrauma
return;
}
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status });
}
IsDead = true;
ApplyStatusEffects(ActionType.OnDeath, 1.0f);
@@ -3215,7 +3307,7 @@ namespace Barotrauma
{
if (Removed)
{
DebugConsole.ThrowError("Attempting to revive an already removed character\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempting to revive an already removed character\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
@@ -3260,7 +3352,7 @@ namespace Barotrauma
{
if (Removed)
{
DebugConsole.ThrowError("Attempting to remove an already removed character\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempting to remove an already removed character\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
DebugConsole.Log("Removing character " + Name + " (ID: " + ID + ")");
@@ -151,13 +151,13 @@ namespace Barotrauma
public XElement HealthData;
private static ushort idCounter;
private const string disguiseName = "???";
public string Name;
public string DisplayName
{
get
{
string disguiseName = "?";
if (Character == null || !Character.HideFace)
{
return Name;
@@ -300,6 +300,7 @@ namespace Barotrauma
public Order CurrentOrder { get; set; }
public string CurrentOrderOption { get; set; }
public bool IsDismissed => CurrentOrder == null || CurrentOrder.Identifier.Equals("dismissed", StringComparison.OrdinalIgnoreCase);
//unique ID given to character infos in MP
//used by clients to identify which infos are the same to prevent duplicate characters in round summary
@@ -37,7 +37,7 @@ namespace Barotrauma
public string Identifier { get; private set; }
[Serialize(1.0f, true, description: "The probability for the affliction to be applied."), Editable(minValue: 0f, maxValue: 1f)]
public float Probability { get; private set; } = 1.0f;
public float Probability { get; set; } = 1.0f;
public float DamagePerSecond;
public float DamagePerSecondTimer;
@@ -264,5 +264,10 @@ namespace Barotrauma
/// Ideally we would keep this private, but doing so would require too much refactoring.
/// </summary>
public void SetStrength(float strength) => _strength = strength;
public bool ShouldShowIcon(Character afflictedCharacter)
{
return Strength >= (afflictedCharacter == Character.Controlled ? Prefab.ShowIconThreshold : Prefab.ShowIconToOthersThreshold);
}
}
}
@@ -273,6 +273,8 @@ namespace Barotrauma
public readonly float ActivationThreshold = 0.0f;
//how high the strength has to be for the affliction icon to be shown in the UI
public readonly float ShowIconThreshold = 0.05f;
//how high the strength has to be for the affliction icon to be shown to others with a health scanner or via the health interface
public readonly float ShowIconToOthersThreshold = 0.05f;
public readonly float MaxStrength = 100.0f;
//how high the strength has to be for the affliction icon to be shown with a health scanner
@@ -555,6 +557,7 @@ namespace Barotrauma
ActivationThreshold = element.GetAttributeFloat("activationthreshold", 0.0f);
ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", Math.Max(ActivationThreshold, 0.05f));
ShowIconToOthersThreshold = element.GetAttributeFloat("showicontoothersthreshold", ShowIconThreshold);
MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f);
ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, 0.05f));
@@ -1,12 +1,4 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using Barotrauma.IO;
using System.Linq;
using Barotrauma.Extensions;
using System.Xml.Linq;
using System;
namespace Barotrauma
namespace Barotrauma
{
partial class AfflictionPsychosis : Affliction
{
@@ -141,17 +141,17 @@ namespace Barotrauma
{
get
{
float max = maxVitality;
if (Character?.Info?.Job?.Prefab != null)
{
return maxVitality + Character.Info.Job.Prefab.VitalityModifier;
max += Character.Info.Job.Prefab.VitalityModifier;
}
return maxVitality;
return max * Character.HealthMultiplier;
}
set
{
maxVitality = Math.Max(0, value);
}
}
public float MinVitality
@@ -450,9 +450,13 @@ namespace Barotrauma
matchingAfflictions.AddRange(limbHealth.Afflictions);
}
}
matchingAfflictions.RemoveAll(a =>
!a.Prefab.Identifier.Equals(affliction, StringComparison.OrdinalIgnoreCase) &&
!a.Prefab.AfflictionType.Equals(affliction, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(affliction))
{
matchingAfflictions.RemoveAll(a =>
!a.Prefab.Identifier.Equals(affliction, StringComparison.OrdinalIgnoreCase) &&
!a.Prefab.AfflictionType.Equals(affliction, StringComparison.OrdinalIgnoreCase));
}
if (matchingAfflictions.Count == 0) return;
@@ -617,8 +621,8 @@ namespace Barotrauma
private void AddAffliction(Affliction newAffliction)
{
if (!DoesBleed && newAffliction is AfflictionBleeding) return;
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return;
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
if (newAffliction.Prefab.AfflictionType == "huskinfection")
{
var huskPrefab = newAffliction.Prefab as AfflictionPrefabHusk;
@@ -636,7 +640,10 @@ namespace Barotrauma
affliction.Strength = newStrength;
affliction.Source = newAffliction.Source;
CalculateVitality();
if (Vitality <= MinVitality) Kill();
if (Vitality <= MinVitality)
{
Kill();
}
return;
}
}
@@ -650,7 +657,10 @@ namespace Barotrauma
Character.HealthUpdateInterval = 0.0f;
CalculateVitality();
if (Vitality <= MinVitality) Kill();
if (Vitality <= MinVitality)
{
Kill();
}
}
@@ -707,7 +717,11 @@ namespace Barotrauma
UpdateLimbAfflictionOverlays();
CalculateVitality();
if (Vitality <= MinVitality) Kill();
if (Vitality <= MinVitality)
{
Kill();
}
}
private void UpdateOxygen(float deltaTime)
@@ -725,10 +739,10 @@ namespace Barotrauma
OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? -5.0f : 10.0f), -100.0f, 100.0f);
}
UpdateOxygenProjSpecific(prevOxygen);
UpdateOxygenProjSpecific(prevOxygen, deltaTime);
}
partial void UpdateOxygenProjSpecific(float prevOxygen);
partial void UpdateOxygenProjSpecific(float prevOxygen, float deltaTime);
partial void UpdateBleedingProjSpecific(AfflictionBleeding affliction, Limb targetLimb, float deltaTime);
@@ -163,7 +163,13 @@ namespace Barotrauma
{
item.AddTag("job:" + job.Name);
}
var idCardTags = itemElement.GetAttributeStringArray("tags", new string[0]);
foreach (string tag in idCardTags)
{
item.AddTag(tag);
}
}
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = character.TeamID;
@@ -203,7 +203,7 @@ namespace Barotrauma
partial class Limb : ISerializableEntity, ISpatialEntity
{
//how long it takes for severed limbs to fade out
private const float SeveredFadeOutTime = 10.0f;
public float SeveredFadeOutTime => Params.SeveredFadeOutTime;
public readonly Character character;
/// <summary>
@@ -327,10 +327,10 @@ namespace Barotrauma
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace);
"Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
return Vector2.Zero;
}
return body.SimPosition;
@@ -344,10 +344,10 @@ namespace Barotrauma
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:SimPosition", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace);
"Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
return 0.0f;
}
return body.Rotation;
@@ -364,10 +364,10 @@ namespace Barotrauma
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.Mass:AccessRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace);
"Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
return 1.0f;
}
return body.Mass;
@@ -383,10 +383,10 @@ namespace Barotrauma
if (Removed)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
#endif
GameAnalyticsManager.AddErrorEventOnce("Limb.LinearVelocity:AccessRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a removed limb.\n" + Environment.StackTrace);
"Attempted to access a removed limb.\n" + Environment.StackTrace.CleanupStackTrace());
return Vector2.Zero;
}
return body.LinearVelocity;
@@ -428,7 +428,7 @@ namespace Barotrauma
{
if (!MathUtils.IsValid(value))
{
string errorMsg = "Attempted to set the anchor A of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace;
string errorMsg = "Attempted to set the anchor A of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:InvalidValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
@@ -442,7 +442,7 @@ namespace Barotrauma
string errorMsg = "Attempted to move the anchor A of a limb's pull joint extremely far from the limb (diff: " + diff +
", limb enabled: " + body.Enabled +
", simple physics enabled: " + character.AnimController.SimplePhysicsEnabled + ")\n"
+ Environment.StackTrace;
+ Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:ExcessiveValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
@@ -461,7 +461,7 @@ namespace Barotrauma
{
if (!MathUtils.IsValid(value))
{
string errorMsg = "Attempted to set the anchor B of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace;
string errorMsg = "Attempted to set the anchor B of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:InvalidValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
@@ -475,7 +475,7 @@ namespace Barotrauma
string errorMsg = "Attempted to move the anchor B of a limb's pull joint extremely far from the limb (diff: " + diff +
", limb enabled: " + body.Enabled +
", simple physics enabled: " + character.AnimController.SimplePhysicsEnabled + ")\n"
+ Environment.StackTrace;
+ Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:ExcessiveValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
@@ -715,7 +715,7 @@ namespace Barotrauma
float bloodDecalSize = MathHelper.Clamp(bleedingDamage / 5, 0.1f, 1.0f);
if (character.CurrentHull != null && !string.IsNullOrEmpty(character.BloodDecalName))
{
character.CurrentHull.AddDecal(character.BloodDecalName, WorldPosition, MathHelper.Clamp(bloodDecalSize, 0.5f, 1.0f), true);
character.CurrentHull.AddDecal(character.BloodDecalName, WorldPosition, MathHelper.Clamp(bloodDecalSize, 0.5f, 1.0f), isNetworkEvent: false);
}
}
}
@@ -768,6 +768,18 @@ namespace Barotrauma
{
attack.UpdateCoolDown(deltaTime);
}
if (Params.BlinkFrequency > 0)
{
if (blinkTimer > -TotalBlinkDurationOut)
{
blinkTimer -= deltaTime;
}
else
{
blinkTimer = Params.BlinkFrequency;
}
}
}
partial void UpdateProjSpecific(float deltaTime);
@@ -1039,6 +1051,45 @@ namespace Barotrauma
}
}
private float blinkTimer;
private float blinkPhase;
private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime;
public void Blink(float deltaTime, float referenceRotation)
{
if (blinkTimer > -TotalBlinkDurationOut)
{
blinkPhase -= deltaTime;
if (blinkPhase > 0)
{
// in
float t = ToolBox.GetEasing(Params.BlinkTransitionIn, MathUtils.InverseLerp(1, 0, blinkPhase / Params.BlinkDurationIn));
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true);
}
else
{
if (Math.Abs(blinkPhase) < Params.BlinkHoldTime)
{
// hold
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationIn) * Dir, Mass * Params.BlinkForce, wrapAngle: true);
}
else
{
// out
float t = ToolBox.GetEasing(Params.BlinkTransitionOut, MathUtils.InverseLerp(0, 1, -blinkPhase / TotalBlinkDurationOut));
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce * t, wrapAngle: true);
}
}
}
else
{
// out
blinkPhase = Params.BlinkDurationIn;
body.SmoothRotate(referenceRotation + MathHelper.ToRadians(Params.BlinkRotationOut) * Dir, Mass * Params.BlinkForce, wrapAngle: true);
}
}
public void Remove()
{
body?.Remove();
@@ -305,7 +305,7 @@ namespace Barotrauma
instance.Load(fullPath, speciesName);
anims.Add(fileName, instance);
DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
return instance as T;
return instance;
}
public bool Serialize() => base.Serialize();
@@ -474,6 +474,12 @@ namespace Barotrauma
[Serialize(true, true, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable]
public bool AvoidGunfire { get; private set; }
[Serialize(3f, true, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)]
public float AvoidTime { get; private set; }
[Serialize(20f, true, description: "How long the creature flees before returning to normal state. When the creature sees the target or is being chased, it will always flee, if it's in the flee state."), Editable(minValue: 0f, maxValue: 100f)]
public float MinFleeTime { get; private set; }
[Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable()]
public bool AggressiveBoarding { get; private set; }
@@ -571,12 +577,18 @@ namespace Barotrauma
[Serialize(0f, true, description: "What base priority is given to the target?"), Editable(minValue: 0f, maxValue: 1000f, ValueStep = 1, DecimalCount = 0)]
public float Priority { get; set; }
[Serialize(0f, true, description: "Generic distance that can be used for different purposes depending on the state. Eg. in Avoid state this defines the distance that the character tries to keep to the target. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)]
[Serialize(0f, true, description: "Generic distance that can be used for different purposes depending on the state. E.g. in Avoid state this defines the distance that the character tries to keep to the target. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)]
public float ReactDistance { get; set; }
[Serialize(0f, true, description: "Used for defining the attack distance for PassiveAggressive and Aggressive states. If the distance is 0, it's not used."), Editable(MinValueFloat = 0, ValueStep = 10, DecimalCount = 0)]
public float AttackDistance { get; set; }
[Serialize(0f, true, description: "Generic timer that can be used for different purposes depending on the state. E.g. in Observe state this defines how long the character in general keeps staring the targets (Some random is always applied)."), Editable]
public float Timer { get; set; }
[Serialize(false, true, description: "Should the target be ignored if it's inside a container/inventory. Only affects items."), Editable]
public bool IgnoreContained { get; set; }
public TargetParams(XElement element, CharacterParams character) : base(element, character) { }
public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(tag, state, priority), character) { }
@@ -77,7 +77,7 @@ namespace Barotrauma
[Serialize(LimbType.Torso, true), Editable]
public LimbType MainLimb { get; set; }
private static Dictionary<string, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<string, Dictionary<string, RagdollParams>>();
private readonly static Dictionary<string, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<string, Dictionary<string, RagdollParams>>();
public List<ColliderParams> Colliders { get; private set; } = new List<ColliderParams>();
public List<LimbParams> Limbs { get; private set; } = new List<LimbParams>();
@@ -464,7 +464,7 @@ namespace Barotrauma
/// <summary>
/// Should be converted to sim units.
/// </summary>
[Serialize("1.0, 1.0", true, description: "Local position of the join in the Limb2."), Editable()]
[Serialize("1.0, 1.0", true, description: "Local position of the joint in the Limb2."), Editable()]
public Vector2 Limb2Anchor { get; set; }
[Serialize(true, true), Editable]
@@ -500,6 +500,9 @@ namespace Barotrauma
[Serialize(false, false), Editable(ReadOnly = true)]
public bool WeldJoint { get; set; }
[Serialize(false, true), Editable]
public bool ClockWiseRotation { get; set; }
public JointParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
}
@@ -543,14 +546,17 @@ namespace Barotrauma
[Serialize(LimbType.None, true, description: "The limb type affects many things, like the animations. Torso or Head are considered as the main limbs. Every character should have at least one Torso or Head."), Editable()]
public LimbType Type { get; set; }
[Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings. Used mainly for animations and widgets."), Editable(-360, 360)]
public float SpriteOrientation { get; set; }
/// <summary>
/// The orientation of the sprite as drawn on the sprite sheet (in radians).
/// </summary>
public float GetSpriteOrientation() => MathHelper.ToRadians(float.IsNaN(SpriteOrientation) ? Ragdoll.SpritesheetOrientation : SpriteOrientation);
[Serialize("", true), Editable]
public string Notes { get; set; }
[Serialize(1f, true), Editable]
public float Scale { get; set; }
[Serialize(true, true, description: "Does the limb flip when the character flips?"), Editable()]
public bool Flip { get; set; }
@@ -563,8 +569,8 @@ namespace Barotrauma
[Serialize(false, true, description: "Disable drawing for this limb."), Editable()]
public bool Hide { get; set; }
[Serialize(1f, true, description: "Higher values make AI characters prefer attacking this limb."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10)]
public float AttackPriority { get; set; }
[Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings. Used mainly for animations and widgets."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)]
public float SpriteOrientation { get; set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float SteerForce { get; set; }
@@ -587,6 +593,9 @@ namespace Barotrauma
[Serialize(7f, true, description: "Increasing the damping makes the limb stop rotating more quickly."), Editable]
public float AngularDamping { get; set; }
[Serialize(1f, true, description: "Higher values make AI characters prefer attacking this limb."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10)]
public float AttackPriority { get; set; }
[Serialize("0, 0", true, description: "The position which is used to lead the IK chain to the IK goal. Only applicable if the limb is hand or foot."), Editable()]
public Vector2 PullPos { get; set; }
@@ -599,24 +608,58 @@ namespace Barotrauma
[Serialize("0, 0", true, description: "Relative offset for the mouth position (starting from the center). Only applicable for LimbType.Head. Used for eating."), Editable(DecimalCount = 2, MinValueFloat = -10f, MaxValueFloat = 10f)]
public Vector2 MouthPos { get; set; }
[Serialize("", true), Editable]
public string Notes { get; set; }
[Serialize(0f, true), Editable]
public float ConstantTorque { get; set; }
[Serialize(0f, true), Editable]
public float ConstantAngle { get; set; }
[Serialize(1f, true), Editable]
public float Scale { get; set; }
[Serialize(1f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 10)]
public float AttackForceMultiplier { get; set; }
[Serialize(1f, true, description:"How much damage must be done by the attack in order to be able to cut off the limb. Note that it's evaluated after the damage modifiers."), Editable(DecimalCount = 0, MinValueFloat = 0, MaxValueFloat = 1000)]
public float MinSeveranceDamage { get; set; }
//how long it takes for severed limbs to fade out
[Serialize(10f, true, "How long it takes for the severed limb to fade out"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1)]
public float SeveredFadeOutTime { get; set; } = 10.0f;
[Serialize(false, true, description: "Only applied when the limb is of type Tail. If none of the tails have been defined to use the angle and an angle is defined in the animation parameters, the first tail limb is used."), Editable]
public bool ApplyTailAngle { get; set; }
[Serialize(1f, true), Editable(ValueStep = 0.1f, DecimalCount = 2)]
public float SineFrequencyMultiplier { get; set; }
[Serialize(1f, true), Editable(ValueStep = 0.1f, DecimalCount = 2)]
public float SineAmplitudeMultiplier { get; set; }
[Serialize(0f, true), Editable(0, 100, ValueStep = 1, DecimalCount = 1)]
public float BlinkFrequency { get; set; }
[Serialize(0.2f, true), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)]
public float BlinkDurationIn { get; set; }
[Serialize(0.5f, true), Editable(0.01f, 10, ValueStep = 1, DecimalCount = 2)]
public float BlinkDurationOut { get; set; }
[Serialize(0f, true), Editable(0, 10, ValueStep = 1, DecimalCount = 2)]
public float BlinkHoldTime { get; set; }
[Serialize(0f, true), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)]
public float BlinkRotationIn { get; set; }
[Serialize(45f, true), Editable(-360, 360, ValueStep = 1, DecimalCount = 0)]
public float BlinkRotationOut { get; set; }
[Serialize(50f, true), Editable]
public float BlinkForce { get; set; }
[Serialize(TransitionMode.Linear, true), Editable]
public TransitionMode BlinkTransitionIn { get; private set; }
[Serialize(TransitionMode.Linear, true), Editable]
public TransitionMode BlinkTransitionOut { get; private set; }
// Non-editable ->
// TODO: make read-only
[Serialize(0, true)]
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
/// <summary>
/// Implementation of the Command pattern.
/// <see href="https://en.wikipedia.org/wiki/Command_pattern"/>
/// </summary>
/// <remarks>
/// Created by Markus Isberg on 11th of March 2020 for the submarine editor.
/// "Implementing a global undo and redo with Memento pattern proved too difficult of a task for me so I implemented it with this pattern instead."
/// </remarks>
internal abstract partial class Command
{
/// <summary>
/// A method that should apply a new state on an object or perform an action
/// </summary>
public abstract void Execute();
/// <summary>
/// A method that should revert Execute() method's actions
/// </summary>
public abstract void UnExecute();
/// <summary>
/// State no longer exists, clean up the lingering garbage
/// </summary>
public abstract void Cleanup();
}
}
@@ -543,9 +543,6 @@ namespace Barotrauma
if (SteamWorkshopId != 0)
{
doc.Root.Add(new XAttribute("steamworkshopid", SteamWorkshopId.ToString()));
#if UNSTABLE
doc.Root.Add(new XAttribute("steamworkshopurl", $"http://steamcommunity.com/sharedfiles/filedetails/?source=Facepunch.Steamworks&id={SteamWorkshopId}"));
#endif
}
if (InstallTime != null)
@@ -230,7 +230,7 @@ namespace Barotrauma
#if CLIENT && WINDOWS
if (e is SharpDX.SharpDXException) { throw; }
#endif
DebugConsole.ThrowError("Coroutine " + handle.Name + " threw an exception: " + e.Message + "\n" + e.StackTrace.ToString());
DebugConsole.ThrowError("Coroutine " + handle.Name + " threw an exception: " + e.Message + "\n" + e.StackTrace.CleanupStackTrace());
handle.Exception = e;
return true;
}
@@ -230,7 +230,7 @@ namespace Barotrauma
{
string errorMsg = "Failed to spawn an item. Arguments: \"" + string.Join(" ", args) + "\".";
ThrowError(errorMsg, e);
GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnItem:Error", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace);
GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnItem:Error", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace.CleanupStackTrace());
}
},
() =>
@@ -1019,7 +1019,7 @@ namespace Barotrauma
}
catch (InvalidOperationException e)
{
string errorMsg = "Error while executing the fixhulls command.\n" + e.StackTrace;
string errorMsg = "Error while executing the fixhulls command.\n" + e.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("DebugConsole.FixHulls", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
}
}
@@ -1899,15 +1899,15 @@ namespace Barotrauma
{
if (e != null)
{
error += " {" + e.Message + "}\n" + e.StackTrace;
error += " {" + e.Message + "}\n" + e.StackTrace.CleanupStackTrace();
if (e.InnerException != null)
{
error += "\n\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace;
error += "\n\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace.CleanupStackTrace();
}
}
else if (appendStackTrace)
{
error += "\n" + Environment.StackTrace;
error += "\n" + Environment.StackTrace.CleanupStackTrace();
}
System.Diagnostics.Debug.WriteLine(error);
@@ -34,11 +34,11 @@ namespace Barotrauma
get { return Prefab.LifeTime; }
}
private float baseAlpha = 1.0f;
public float BaseAlpha
{
get { return baseAlpha; }
}
get;
set;
} = 1.0f;
public Color Color
{
@@ -58,9 +58,10 @@ namespace Barotrauma
}
}
public Vector2 Position
public Vector2 CenterPosition
{
get { return position; }
get;
private set;
}
public Vector2 NonClampedPosition
@@ -69,6 +70,12 @@ namespace Barotrauma
private set;
}
public int SpriteIndex
{
get;
private set;
}
private readonly HashSet<BackgroundSection> affectedSections;
private readonly Hull hull;
@@ -79,7 +86,7 @@ namespace Barotrauma
private bool cleaned = false;
public Decal(DecalPrefab prefab, float scale, Vector2 worldPosition, Hull hull)
public Decal(DecalPrefab prefab, float scale, Vector2 worldPosition, Hull hull, int? spriteIndex = null)
{
Prefab = prefab;
@@ -90,7 +97,8 @@ namespace Barotrauma
Vector2 drawPos = position + hull.Rect.Location.ToVector2();
Sprite = prefab.Sprites[Rand.Range(0, prefab.Sprites.Count, Rand.RandSync.Unsynced)];
SpriteIndex = spriteIndex ?? Rand.Range(0, prefab.Sprites.Count, Rand.RandSync.Unsynced);
Sprite = prefab.Sprites[SpriteIndex];
Color = prefab.Color;
Rectangle drawRect = new Rectangle(
@@ -111,6 +119,8 @@ namespace Barotrauma
Sprite.SourceRect.Width - (int)((overFlowAmount.X + overFlowAmount.Width) / scale),
Sprite.SourceRect.Height - (int)((overFlowAmount.Y + overFlowAmount.Height) / scale));
CenterPosition = position;
position -= new Vector2(Sprite.size.X / 2 * scale - overFlowAmount.X, -Sprite.size.Y / 2 * scale + overFlowAmount.Y);
this.Scale = scale;
@@ -148,20 +158,20 @@ namespace Barotrauma
{
cleaned = true;
float sizeModifier = MathHelper.Clamp(Sprite.size.X * Sprite.size.Y * Scale / 10000, 1.0f, 25.0f);
baseAlpha -= val * -1 / sizeModifier;
BaseAlpha -= val * -1 / sizeModifier;
}
private float GetAlpha()
{
if (fadeTimer < Prefab.FadeInTime && !cleaned)
{
return baseAlpha * fadeTimer / Prefab.FadeInTime;
return BaseAlpha * fadeTimer / Prefab.FadeInTime;
}
else if (cleaned || fadeTimer > Prefab.LifeTime - Prefab.FadeOutTime)
{
return baseAlpha * Math.Min((Prefab.LifeTime - fadeTimer) / Prefab.FadeOutTime, 1.0f);
return BaseAlpha * Math.Min((Prefab.LifeTime - fadeTimer) / Prefab.FadeOutTime, 1.0f);
}
return baseAlpha;
return BaseAlpha;
}
}
}
@@ -108,7 +108,7 @@ namespace Barotrauma
}
}
public Decal CreateDecal(string decalName, float scale, Vector2 worldPosition, Hull hull)
public Decal CreateDecal(string decalName, float scale, Vector2 worldPosition, Hull hull, int? spriteIndex = null)
{
if (!Prefabs.ContainsKey(decalName.ToLowerInvariant()))
{
@@ -118,7 +118,7 @@ namespace Barotrauma
DecalPrefab prefab = Prefabs[decalName];
return new Decal(prefab, scale, worldPosition, hull);
return new Decal(prefab, scale, worldPosition, hull, spriteIndex);
}
}
}
@@ -62,9 +62,9 @@ namespace Barotrauma
{
Sprites.Add(new Sprite(subElement));
}
}
Color = new Color(element.GetAttributeVector4("color", Vector4.One));
}
Color = element.GetAttributeColor("color", Color.White);
LifeTime = element.GetAttributeFloat("lifetime", 10.0f);
FadeOutTime = Math.Min(LifeTime, element.GetAttributeFloat("fadeouttime", 1.0f));
@@ -21,6 +21,7 @@
OnDeath = OnBroken,
OnDamaged,
OnSevered,
OnProduceSpawned
OnProduceSpawned,
OnOpen, OnClose,
}
}
@@ -9,7 +9,17 @@ namespace Barotrauma
[Serialize(0.0f, true)]
public float Chance { get; set; }
public RNGAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
public RNGAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
{
if (Chance >= 1.0f)
{
DebugConsole.ThrowError($"Incorrectly configured RNG Action in event \"{parentEvent.Prefab.Identifier}\". Probability is 1.0 (100%) or more, the action will always succeed.");
}
else if (Chance <= 0.0f)
{
DebugConsole.ThrowError($"Incorrectly configured RNG Action in event \"{parentEvent.Prefab.Identifier}\". Probability is 0 or less, the action will never succeed.");
}
}
private bool isFinished;
@@ -120,6 +120,7 @@ namespace Barotrauma
AddChildEvents(initialEventSet);
void AddChildEvents(EventSet eventSet)
{
if (eventSet == null) { return; }
foreach (EventPrefab ep in eventSet.EventPrefabs.Select(e => e.First))
{
if (!level.LevelData.NonRepeatableEvents.Contains(ep))
@@ -582,7 +583,7 @@ namespace Barotrauma
enemyDanger = 0.0f;
foreach (Character character in Character.CharacterList)
{
if (character.IsDead || character.IsIncapacitated || !character.Enabled) continue;
if (character.IsDead || character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup("human")) { continue; }
EnemyAIController enemyAI = character.AIController as EnemyAIController;
if (enemyAI == null) continue;
@@ -591,13 +592,13 @@ namespace Barotrauma
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)))
{
//crawler inside the sub adds 0.1f to enemy danger, mantis 0.25f
enemyDanger += enemyAI.CombatStrength / 1000.0f;
enemyDanger += enemyAI.CombatStrength / 100.0f;
}
else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null)
{
//enemy outside and targeting the sub or something in it
//moloch adds 0.24 to enemy danger, a crawler 0.02
enemyDanger += enemyAI.CombatStrength / 2000.0f;
enemyDanger += enemyAI.CombatStrength / 1000.0f;
}
}
enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f);
@@ -653,12 +654,12 @@ namespace Barotrauma
if (targetIntensity > currentIntensity)
{
//25 seconds for intensity to go from 0.0 to 1.0
currentIntensity = MathHelper.Min(currentIntensity + 0.04f * IntensityUpdateInterval, targetIntensity);
currentIntensity = Math.Min(currentIntensity + 0.04f * IntensityUpdateInterval, targetIntensity);
}
else
{
//400 seconds for intensity to go from 1.0 to 0.0
currentIntensity = MathHelper.Max(0.0025f * IntensityUpdateInterval, targetIntensity);
currentIntensity = Math.Max(currentIntensity - 0.0025f * IntensityUpdateInterval, targetIntensity);
}
}
@@ -63,6 +63,16 @@ namespace Barotrauma
{
return $"({value.X.FormatSingleDecimal()}, {value.Y.FormatSingleDecimal()})";
}
public static string FormatSingleDecimal(this Vector3 value)
{
return $"({value.X.FormatSingleDecimal()}, {value.Y.FormatSingleDecimal()}, {value.Z.FormatSingleDecimal()})";
}
public static string FormatSingleDecimal(this Vector4 value)
{
return $"({value.X.FormatSingleDecimal()}, {value.Y.FormatSingleDecimal()}, {value.Z.FormatSingleDecimal()}, {value.W.FormatSingleDecimal()})";
}
public static string FormatDoubleDecimal(this Vector2 value)
{
@@ -0,0 +1,60 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Barotrauma
{
static class ForbiddenWordFilter
{
static readonly string fileListPath = Path.Combine("Data", "forbiddenwordlist.txt");
private static readonly HashSet<string> forbiddenWords;
static ForbiddenWordFilter()
{
try
{
forbiddenWords = File.ReadAllLines(fileListPath).ToHashSet();
}
catch (IOException e)
{
DebugConsole.ThrowError($"Failed to load the list of forbidden words from {fileListPath}.", e);
}
}
public static bool IsForbidden(string text)
{
return IsForbidden(text, out _);
}
public static bool IsForbidden(string text, out string forbiddenWord)
{
forbiddenWord = string.Empty;
if (forbiddenWords == null)
{
return false;
}
char[] delimiters = new char[] { ' ', '-', '.', '_', ':', ';', '\'' };
HashSet<string> words = new HashSet<string>();
foreach (char delimiter in delimiters)
{
foreach (string word in text.Split(delimiter))
{
words.Add(word);
}
}
foreach (string word in words)
{
if (forbiddenWords.Any(w => Homoglyphs.Compare(word, w) || Homoglyphs.Compare(word + 's', w)))
{
forbiddenWord = word;
return true;
}
}
return false;
}
}
}
@@ -44,7 +44,7 @@ namespace Barotrauma
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
DebugConsole.ThrowError("Clients are not allowed to use AutoItemPlacer.\n" + Environment.StackTrace);
DebugConsole.ThrowError("Clients are not allowed to use AutoItemPlacer.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
@@ -135,7 +135,7 @@ namespace Barotrauma
{
if (itemPrefab == null)
{
string errorMsg = "Error in AutoItemPlacer.SpawnItems - itemPrefab was null.\n"+Environment.StackTrace;
string errorMsg = "Error in AutoItemPlacer.SpawnItems - itemPrefab was null.\n"+Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AutoItemPlacer.SpawnItems:ItemNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return false;
@@ -14,6 +14,8 @@ namespace Barotrauma
private float conversationTimer, conversationLineTimer;
private readonly List<Pair<Character, string>> pendingConversationLines = new List<Pair<Character, string>>();
public const int MaxCrewSize = 16;
private readonly List<CharacterInfo> characterInfos = new List<CharacterInfo>();
private readonly List<Character> characters = new List<Character>();
@@ -40,7 +42,7 @@ namespace Barotrauma
{
if (order.TargetEntity == null)
{
DebugConsole.ThrowError("Attempted to add an order with no target entity to CrewManager!\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempted to add an order with no target entity to CrewManager!\n" + Environment.StackTrace.CleanupStackTrace());
return false;
}
@@ -95,12 +97,12 @@ namespace Barotrauma
{
if (character.Removed)
{
DebugConsole.ThrowError("Tried to add a removed character to CrewManager!\n" + Environment.StackTrace);
DebugConsole.ThrowError("Tried to add a removed character to CrewManager!\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
if (character.IsDead)
{
DebugConsole.ThrowError("Tried to add a dead character to CrewManager!\n" + Environment.StackTrace);
DebugConsole.ThrowError("Tried to add a dead character to CrewManager!\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
@@ -122,7 +124,7 @@ namespace Barotrauma
{
if (characterInfos.Contains(characterInfo))
{
DebugConsole.ThrowError("Tried to add the same character info to CrewManager twice.\n" + Environment.StackTrace);
DebugConsole.ThrowError("Tried to add the same character info to CrewManager twice.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
@@ -31,6 +31,7 @@ namespace Barotrauma
get => Math.Min(MaxReputation, Metadata.GetFloat(metaDataIdentifier, InitialReputation));
set
{
if (MathUtils.NearlyEqual(Value, value)) { return; }
Metadata.SetValue(metaDataIdentifier, Math.Clamp(value, MinReputation, MaxReputation));
OnReputationValueChanged?.Invoke();
OnAnyReputationValueChanged?.Invoke();
@@ -30,6 +30,8 @@ namespace Barotrauma
public CampaignMetadata CampaignMetadata;
protected XElement petsElement;
public enum TransitionType
{
None,
@@ -188,7 +190,7 @@ namespace Barotrauma
if (CoroutineManager.IsCoroutineRunning("LevelTransition"))
{
DebugConsole.ThrowError("Level transition already running.\n" + Environment.StackTrace);
DebugConsole.ThrowError("Level transition already running.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
@@ -208,7 +210,7 @@ namespace Barotrauma
"leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
"at start: " + (leavingSub?.AtStartPosition.ToString() ?? "null") + ", " +
"at end: " + (leavingSub?.AtEndPosition.ToString() ?? "null") + ")\n" +
Environment.StackTrace);
Environment.StackTrace.CleanupStackTrace());
return;
}
if (nextLevel == null)
@@ -220,7 +222,7 @@ namespace Barotrauma
"leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
"at start: " + (leavingSub?.AtStartPosition.ToString() ?? "null") + ", " +
"at end: " + (leavingSub?.AtEndPosition.ToString() ?? "null") + ")\n" +
Environment.StackTrace);
Environment.StackTrace.CleanupStackTrace());
return;
}
#if CLIENT
@@ -150,6 +150,9 @@ namespace Barotrauma
case "cargo":
CargoManager?.LoadPurchasedItems(subElement);
break;
case "pets":
petsElement = subElement;
break;
#if SERVER
case "availablesubs":
foreach (XElement availableSub in subElement.Elements())
@@ -282,6 +282,11 @@ namespace Barotrauma
DebugConsole.ThrowError("Couldn't start game session, submarine file corrupted.");
return;
}
if (SubmarineInfo.SubmarineElement.Elements().Count() == 0)
{
DebugConsole.ThrowError("Couldn't start game session, saved submarine is empty. The submarine file may be corrupted.");
return;
}
LevelData = levelData;
@@ -348,6 +353,8 @@ namespace Barotrauma
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Location"), StartLocation.Name), Color.CadetBlue, playSound: false);
}
}
GUI.PreventPauseMenuToggle = false;
#endif
}
@@ -542,6 +549,12 @@ namespace Barotrauma
Mission == null ? "None" : Mission.GetType().ToString());
#if CLIENT
if (GUI.PauseMenuOpen)
{
GUI.TogglePauseMenu();
}
GUI.PreventPauseMenuToggle = true;
if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null)
{
GUI.ClearMessages();
@@ -262,7 +262,7 @@ namespace Barotrauma
lastErrorSpeak = DateTime.Now;
}
#if CLIENT
GUI.PlayUISound(GUISoundType.PickItemFail);
SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
#endif
}
@@ -730,6 +730,7 @@ namespace Barotrauma
public static bool EnableSubmarineAutoSave { get; set; }
public static int MaximumAutoSaves { get; set; }
public static Color SubEditorBackgroundColor { get; set; }
public static int SubEditorMaxUndoBuffer { get; set; }
public bool ShowTutorialSkipWarning
{
@@ -898,6 +899,7 @@ namespace Barotrauma
new XAttribute("submarineautosave", EnableSubmarineAutoSave),
new XAttribute("maxautosaves", MaximumAutoSaves),
new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)),
new XAttribute("subeditorundobuffer", SubEditorMaxUndoBuffer),
new XAttribute("enablesplashscreen", EnableSplashScreen),
new XAttribute("usesteammatchmaking", UseSteamMatchmaking),
new XAttribute("quickstartsub", QuickStartSubmarineName),
@@ -1033,7 +1035,7 @@ namespace Barotrauma
{
DebugConsole.ThrowError("Saving game settings failed.", e);
GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace);
"Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace());
}
}
@@ -1119,6 +1121,7 @@ namespace Barotrauma
new XAttribute("verboselogging", VerboseLogging),
new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs),
new XAttribute("submarineautosave", EnableSubmarineAutoSave),
new XAttribute("subeditorundobuffer", SubEditorMaxUndoBuffer),
new XAttribute("maxautosaves", MaximumAutoSaves),
new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)),
new XAttribute("enablesplashscreen", EnableSplashScreen),
@@ -1232,17 +1235,6 @@ namespace Barotrauma
doc.Root.Add(contentPackagesElement);
#if UNSTABLE
//TODO: remove at some point
foreach (ContentPackage package in AllEnabledPackages)
{
XElement compatibilityElement = new XElement("contentpackage");
compatibilityElement.Add(new XAttribute("path", package.Path));
doc.Root.Add(compatibilityElement);
}
#endif
#if CLIENT
var keyMappingElement = new XElement("keymapping");
doc.Root.Add(keyMappingElement);
@@ -1337,7 +1329,7 @@ namespace Barotrauma
{
DebugConsole.ThrowError("Saving game settings failed.", e);
GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace);
"Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace());
}
}
#endregion
@@ -1355,6 +1347,7 @@ namespace Barotrauma
EnableSubmarineAutoSave = doc.Root.GetAttributeBool("submarineautosave", true);
MaximumAutoSaves = doc.Root.GetAttributeInt("maxautosaves", 8);
SubEditorBackgroundColor = doc.Root.GetAttributeColor("subeditorbackground", new Color(0.051f, 0.149f, 0.271f, 1.0f));
SubEditorMaxUndoBuffer = doc.Root.GetAttributeInt("subeditorundobuffer", 32);
UseSteamMatchmaking = doc.Root.GetAttributeBool("usesteammatchmaking", UseSteamMatchmaking);
RequireSteamAuthentication = doc.Root.GetAttributeBool("requiresteamauthentication", RequireSteamAuthentication);
EnableSplashScreen = doc.Root.GetAttributeBool("enablesplashscreen", EnableSplashScreen);
@@ -1,6 +1,4 @@
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -11,7 +9,7 @@ namespace Barotrauma
[Flags]
public enum InvSlotType
{
None = 0, Any = 1, RightHand = 2, LeftHand = 4, Head = 8, InnerClothes = 16, OuterClothes = 32, Headset = 64, Card = 128
None = 0, Any = 1, RightHand = 2, LeftHand = 4, Head = 8, InnerClothes = 16, OuterClothes = 32, Headset = 64, Card = 128, Bag = 256
};
partial class CharacterInventory : Inventory
@@ -24,6 +22,9 @@ namespace Barotrauma
private set;
}
public static readonly List<InvSlotType> anySlot = new List<InvSlotType>() { InvSlotType.Any };
protected bool[] IsEquipped;
public bool AccessibleWhenAlive
@@ -68,7 +69,7 @@ namespace Barotrauma
#if CLIENT
//clients don't create items until the server says so
if (GameMain.Client != null) return;
if (GameMain.Client != null) { return; }
#endif
foreach (XElement subElement in element.Elements())
@@ -76,8 +77,7 @@ namespace Barotrauma
if (!subElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; }
string itemIdentifier = subElement.GetAttributeString("identifier", "");
ItemPrefab itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
if (itemPrefab == null)
if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab))
{
DebugConsole.ThrowError("Error in character inventory \"" + character.SpeciesName + "\" - item \"" + itemIdentifier + "\" not found.");
continue;
@@ -164,7 +164,7 @@ namespace Barotrauma
#if DEBUG
throw new Exception("Tried to put a removed item (" + item.Name + ") in an inventory");
#else
DebugConsole.ThrowError("Tried to put a removed item (" + item.Name + ") in an inventory.\n" + Environment.StackTrace);
DebugConsole.ThrowError("Tried to put a removed item (" + item.Name + ") in an inventory.\n" + Environment.StackTrace.CleanupStackTrace());
return false;
#endif
}
@@ -284,7 +284,7 @@ namespace Barotrauma
{
if (index < 0 || index >= Items.Length)
{
string errorMsg = "CharacterInventory.TryPutItem failed: index was out of range(" + index + ").\n" + Environment.StackTrace;
string errorMsg = "CharacterInventory.TryPutItem failed: index was out of range(" + index + ").\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("CharacterInventory.TryPutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return false;
}
@@ -431,7 +431,7 @@ namespace Barotrauma.Items.Components
string errorMsg =
"Attempted to create a door body at an invalid position (item pos: " + item.Position
+ ", item world pos: " + item.WorldPosition
+ ", docking target world pos: " + DockingTarget.Door.Item.WorldPosition + ")\n" + Environment.StackTrace;
+ ", docking target world pos: " + DockingTarget.Door.Item.WorldPosition + ")\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
@@ -377,6 +377,8 @@ namespace Barotrauma.Items.Components
private int flowerVariants;
private int leafVariants;
private int[] flowerTiles;
private const int serverHealthUpdateDelay = 10;
private int serverHealthUpdateTimer;
public float Health
{
@@ -469,6 +471,20 @@ namespace Barotrauma.Items.Components
{
Health -= FloodTolerance;
}
#if SERVER
if (FullyGrown)
{
if (serverHealthUpdateTimer > serverHealthUpdateDelay)
{
item.CreateServerEvent(this);
serverHealthUpdateTimer = 0;
}
else
{
serverHealthUpdateTimer++;
}
}
#endif
}
CheckPlantState();
@@ -195,7 +195,9 @@ namespace Barotrauma.Items.Components
}
}
}
}
}
characterUsable = element.GetAttributeBool("characterusable", true);
}
private bool OnPusherCollision(Fixture sender, Fixture other, Contact contact)
@@ -312,7 +314,7 @@ namespace Barotrauma.Items.Components
if (item.Removed)
{
DebugConsole.ThrowError($"Attempted to equip a removed item ({item.Name})\n" + Environment.StackTrace);
DebugConsole.ThrowError($"Attempted to equip a removed item ({item.Name})\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
@@ -416,7 +418,7 @@ namespace Barotrauma.Items.Components
{
if (item.Removed)
{
DebugConsole.ThrowError($"Attempted to pick up a removed item ({item.Name})\n" + Environment.StackTrace);
DebugConsole.ThrowError($"Attempted to pick up a removed item ({item.Name})\n" + Environment.StackTrace.CleanupStackTrace());
return false;
}
@@ -514,9 +516,10 @@ namespace Barotrauma.Items.Components
public override bool Use(float deltaTime, Character character = null)
{
if (!attachable || item.body == null) { return character == null || character.IsKeyDown(InputType.Aim); }
if (!attachable || item.body == null) { return character == null || (character.IsKeyDown(InputType.Aim) && characterUsable); }
if (character != null)
{
if (!characterUsable && !attachable) { return false; }
if (!character.IsKeyDown(InputType.Aim)) { return false; }
if (!CanBeAttached(character)) { return false; }
@@ -104,14 +104,14 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker);
#if CLIENT
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) GUI.PlayUISound(GUISoundType.PickItem);
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) SoundPlayer.PlayUISound(GUISoundType.PickItem);
PlaySound(ActionType.OnPicked, picker);
#endif
return true;
}
#if CLIENT
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) GUI.PlayUISound(GUISoundType.PickItemFail);
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
#endif
return false;
@@ -248,6 +248,7 @@ namespace Barotrauma.Items.Components
partial void UseProjSpecific(float deltaTime, Vector2 raystart);
private static readonly List<Body> hitBodies = new List<Body>();
private readonly HashSet<Character> hitCharacters = new HashSet<Character>();
private readonly List<FireSource> fireSourcesInRange = new List<FireSource>();
private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List<Body> ignoredBodies)
@@ -291,10 +292,14 @@ namespace Barotrauma.Items.Components
return true;
},
allowInsideFixture: true);
hitBodies.Clear();
hitBodies.AddRange(bodies);
lastPickedFraction = Submarine.LastPickedFraction;
Type lastHitType = null;
hitCharacters.Clear();
foreach (Body body in bodies)
foreach (Body body in hitBodies)
{
Type bodyType = body.UserData?.GetType();
if (!RepairThroughWalls && bodyType != null && bodyType != lastHitType)
@@ -372,13 +377,23 @@ namespace Barotrauma.Items.Components
fireSourcesInRange.Add(fs);
}
}
foreach (FireSource fs in hull.FakeFireSources)
{
if (fs.IsInDamageRange(displayPos, 100.0f) && !fireSourcesInRange.Contains(fs))
{
fireSourcesInRange.Add(fs);
}
}
}
foreach (FireSource fs in fireSourcesInRange)
{
fs.Extinguish(deltaTime, ExtinguishAmount);
#if SERVER
GameMain.Server.KarmaManager.OnExtinguishingFire(user, deltaTime);
if (!(fs is DummyFireSource))
{
GameMain.Server.KarmaManager.OnExtinguishingFire(user, deltaTime);
}
#endif
}
}
@@ -562,13 +577,22 @@ namespace Barotrauma.Items.Components
partial void FixItemProjSpecific(Character user, float deltaTime, Item targetItem);
private float sinTime;
private float repairTimer;
private Gap previousGap;
private readonly float repairTimeOut = 5;
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (!(objective.OperateTarget is Gap leak)) { return true; }
if (leak.Submarine == null) { return true; }
if (leak != previousGap)
{
sinTime = 0;
repairTimer = 0;
previousGap = leak;
}
Vector2 fromCharacterToLeak = leak.WorldPosition - character.WorldPosition;
float dist = fromCharacterToLeak.Length();
float reach = Range + ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength);
float reach = AIObjectiveFixLeak.CalculateReach(this, character);
//too far away -> consider this done and hope the AI is smart enough to move closer
if (dist > reach * 2) { return true; }
@@ -626,7 +650,11 @@ namespace Barotrauma.Items.Components
else if (dist < reach * 2)
{
// In or almost in range
character.CursorPosition = leak.Position;
character.CursorPosition = leak.WorldPosition;
if (character.Submarine != null)
{
character.CursorPosition -= character.Submarine.Position;
}
character.CursorPosition += VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
if (character.AnimController.InWater)
{
@@ -656,6 +684,12 @@ namespace Barotrauma.Items.Components
var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak);
if (angle < MathHelper.PiOver4)
{
if (Submarine.PickBody(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionWall, allowInsideFixture: true)?.UserData is Item i)
{
var door = i.GetComponent<Door>();
// Hit a door, abandon so that we don't weld it shut.
return door != null && !door.IsOpen && !door.IsBroken;
}
// Check that we don't hit any friendlies
if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit =>
{
@@ -669,17 +703,24 @@ namespace Barotrauma.Items.Components
{
character.SetInput(InputType.Shoot, false, true);
Use(deltaTime, character);
repairTimer += deltaTime;
if (repairTimer > repairTimeOut)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow);
#endif
return true;
}
}
}
bool leakFixed = (leak.Open <= 0.0f || leak.Removed) &&
(leak.ConnectedWall == null || leak.ConnectedWall.Sections.Average(s => s.damage) < 1);
if (leakFixed && leak.FlowTargetHull != null)
if (leakFixed && leak.FlowTargetHull?.DisplayName != null)
{
if (!leak.FlowTargetHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f))
{
{
character.Speak(TextManager.GetWithVariable("DialogLeaksFixed", "[roomname]", leak.FlowTargetHull.DisplayName, true), null, 0.0f, "leaksfixed", 10.0f);
}
else
@@ -6,18 +6,20 @@ namespace Barotrauma.Items.Components
{
class Throwable : Holdable
{
private float throwForce, throwPos;
private float throwPos;
private bool throwing, throwDone;
private bool midAir;
[Serialize(1.0f, false, description: "The impulse applied to the physics body of the item when thrown. Higher values make the item be thrown faster.")]
public float ThrowForce
public Character CurrentThrower
{
get { return throwForce; }
set { throwForce = value; }
get;
private set;
}
[Serialize(1.0f, false, description: "The impulse applied to the physics body of the item when thrown. Higher values make the item be thrown faster.")]
public float ThrowForce { get; set; }
public Throwable(Item item, XElement element)
: base(item, element)
{
@@ -60,6 +62,21 @@ namespace Barotrauma.Items.Components
{
if (item.body.LinearVelocity.LengthSquared() < 0.01f)
{
CurrentThrower = null;
if (statusEffectLists?.ContainsKey(ActionType.OnImpact) ?? false)
{
foreach (var statusEffect in statusEffectLists[ActionType.OnImpact])
{
statusEffect.SetUser(null);
}
}
if (statusEffectLists?.ContainsKey(ActionType.OnBroken) ?? false)
{
foreach (var statusEffect in statusEffectLists[ActionType.OnBroken])
{
statusEffect.SetUser(null);
}
}
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform;
midAir = false;
}
@@ -117,9 +134,24 @@ namespace Barotrauma.Items.Components
#if SERVER
GameServer.Log(GameServer.CharacterLogName(picker) + " threw " + item.Name, ServerLog.MessageType.ItemInteraction);
#endif
Character thrower = picker;
item.Drop(thrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer);
item.body.ApplyLinearImpulse(throwVector * throwForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
CurrentThrower = picker;
if (statusEffectLists?.ContainsKey(ActionType.OnImpact) ?? false)
{
foreach (var statusEffect in statusEffectLists[ActionType.OnImpact])
{
statusEffect.SetUser(CurrentThrower);
}
}
if (statusEffectLists?.ContainsKey(ActionType.OnBroken) ?? false)
{
foreach (var statusEffect in statusEffectLists[ActionType.OnBroken])
{
statusEffect.SetUser(CurrentThrower);
}
}
item.Drop(CurrentThrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer);
item.body.ApplyLinearImpulse(throwVector * ThrowForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
//disable platform collisions until the item comes back to rest again
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel;
@@ -135,12 +167,12 @@ namespace Barotrauma.Items.Components
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnSecondaryUse, this, thrower.ID });
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnSecondaryUse, this, CurrentThrower.ID });
}
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
//Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse"
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, thrower, user: thrower);
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, user: CurrentThrower);
}
throwing = false;
}
@@ -146,7 +146,7 @@ namespace Barotrauma.Items.Components
public bool DrawHudWhenEquipped
{
get;
private set;
protected set;
}
[Serialize(false, false, description: "Can the item be selected by interacting with it.")]
@@ -604,7 +604,7 @@ namespace Barotrauma.Items.Components
if (character == null)
{
string errorMsg = "ItemComponent.DegreeOfSuccess failed (character was null).\n" + Environment.StackTrace;
string errorMsg = "ItemComponent.DegreeOfSuccess failed (character was null).\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("ItemComponent.DegreeOfSuccess:CharacterNull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return 0.0f;
@@ -846,7 +846,7 @@ namespace Barotrauma.Items.Components
DebugConsole.ThrowError("Error while loading entity of the type " + t + ".", e.InnerException);
GameAnalyticsManager.AddErrorEventOnce("ItemComponent.Load:TargetInvocationException" + item.Name + element.Name,
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Error while loading entity of the type " + t + " (" + e.InnerException + ")\n" + Environment.StackTrace);
"Error while loading entity of the type " + t + " (" + e.InnerException + ")\n" + Environment.StackTrace.CleanupStackTrace());
}
return ic;
@@ -1014,8 +1014,8 @@ namespace Barotrauma.Items.Components
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
if (container.Inventory.IsFull()) { return 0; }
// Ignore containers that are identical to the source container
if (sourceC != null && container.Item.Prefab == sourceC.Item.Prefab) { return 0; }
// Ignore containers that are identical to the source container
if (sourceC != null && container.Item.Prefab == sourceC.Item.Prefab) { return 0; }
if (container.ShouldBeContained(containedItem, out bool isRestrictionsDefined))
{
if (isRestrictionsDefined)
@@ -328,10 +328,10 @@ namespace Barotrauma.Items.Components
}
catch (Exception e)
{
DebugConsole.Log("SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace);
DebugConsole.Log("SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace());
GameAnalyticsManager.AddErrorEventOnce("ItemContainer.SetContainedItemPositions.InvalidPosition:" + contained.Name,
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace);
"SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace.CleanupStackTrace());
}
contained.body.Submarine = item.Submarine;
}
@@ -349,6 +349,14 @@ namespace Barotrauma.Items.Components
}
}
public override void OnItemLoaded()
{
if (item.Submarine == null || !item.Submarine.Loading)
{
SpawnAlwaysContainedItems();
}
}
public override void OnMapLoaded()
{
if (itemIds != null)
@@ -361,7 +369,11 @@ namespace Barotrauma.Items.Components
}
itemIds = null;
}
SpawnAlwaysContainedItems();
}
private void SpawnAlwaysContainedItems()
{
if (SpawnWithId.Length > 0)
{
ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId);
@@ -375,6 +387,7 @@ namespace Barotrauma.Items.Components
}
}
protected override void ShallowRemoveComponentSpecific()
{
}
@@ -388,9 +401,9 @@ namespace Barotrauma.Items.Components
inventoryBottomSprite?.Remove();
ContainedStateIndicator?.Remove();
if (Screen.Selected == GameMain.SubEditorScreen && !Submarine.Unloading)
if (SubEditorScreen.IsSubEditor())
{
GameMain.SubEditorScreen.HandleContainerContentsDeletion(Item, Inventory);
Inventory.DeleteAllItems();
return;
}
#endif
@@ -34,7 +34,7 @@ namespace Barotrauma.Items.Components
set { maxFlow = value; }
}
[Editable, Serialize(false, false, alwaysUseInstanceValues: true)]
[Editable, Serialize(true, false, alwaysUseInstanceValues: true)]
public bool IsOn
{
get { return IsActive; }
@@ -0,0 +1,21 @@
using Barotrauma.Networking;
using System.Xml.Linq;
#if CLIENT
using Microsoft.Xna.Framework.Graphics;
#endif
namespace Barotrauma.Items.Components
{
class NameTag : ItemComponent
{
[InGameEditable, Serialize("", false, description: "Name written on the tag.", alwaysUseInstanceValues: true)]
public string WrittenName { get; set; }
public NameTag(Item item, XElement element) : base(item, element)
{
AllowInGameEditing = true;
DrawHudWhenEquipped = true;
item.EditableWhenEquipped = true;
}
}
}
@@ -201,6 +201,7 @@ namespace Barotrauma.Items.Components
if (seed.Decayed || seed.FullyGrown)
{
container?.Inventory.RemoveItem(seed.Item);
Entity.Spawner?.AddToRemoveQueue(seed.Item);
GrowableSeeds[i] = null;
return true;
}
@@ -214,14 +214,15 @@ namespace Barotrauma.Items.Components
}
if (HasBeenTuned) { return true; }
if (string.IsNullOrEmpty(objective.Option) || objective.Option.Equals("charge", StringComparison.OrdinalIgnoreCase))
float targetRatio = string.IsNullOrEmpty(objective.Option) || objective.Option.Equals("charge", StringComparison.OrdinalIgnoreCase) ? aiRechargeTargetRatio : -1;
if (targetRatio > 0 || float.TryParse(objective.Option, out targetRatio))
{
if (Math.Abs(rechargeSpeed - maxRechargeSpeed * aiRechargeTargetRatio) > 0.05f)
if (Math.Abs(rechargeSpeed - maxRechargeSpeed * targetRatio) > 0.05f)
{
#if SERVER
item.CreateServerEvent(this);
#endif
RechargeSpeed = maxRechargeSpeed * aiRechargeTargetRatio;
RechargeSpeed = maxRechargeSpeed * targetRatio;
#if CLIENT
if (rechargeSpeedSlider != null)
{
@@ -639,6 +639,7 @@ namespace Barotrauma.Items.Components
}
target.Body.ApplyLinearImpulse(velocity * item.body.Mass);
target.Body.LinearVelocity = target.Body.LinearVelocity.ClampLength(NetConfig.MaxPhysicsBodyVelocity * 0.5f);
if (hits.Count() >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell)
{
@@ -271,7 +271,7 @@ namespace Barotrauma.Items.Components
if (wires[i] == null) { continue; }
Connection recipient = wires[i].OtherConnection(this);
if (recipient == null) { continue; }
if (recipient == null || !recipient.IsPower) { continue; }
recipient.item.GetComponent<Powered>()?.ReceivePowerProbeSignal(recipient, source, power);
}
@@ -5,7 +5,7 @@ namespace Barotrauma.Items.Components
{
partial class MemoryComponent : ItemComponent, IServerSerializable
{
const int MaxValueLength = 256;
const int MaxValueLength = ChatMessage.MaxLength;
private string value;
@@ -11,6 +11,7 @@ namespace Barotrauma.Items.Components
private string previousReceivedSignal;
private bool previousResult;
private GroupCollection previousGroups;
private Regex regex;
@@ -19,6 +20,9 @@ namespace Barotrauma.Items.Components
[InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the regular expression.", alwaysUseInstanceValues: true)]
public string Output { get; set; }
[InGameEditable, Serialize(false, true, description: "Should the component output a value of a capture group instead of a constant signal.", alwaysUseInstanceValues: true)]
public bool UseCaptureGroup { get; set; }
[Serialize("0", true, description: "The signal this item outputs when the received signal does not match the regular expression.", alwaysUseInstanceValues: true)]
public string FalseOutput { get; set; }
@@ -64,6 +68,7 @@ namespace Barotrauma.Items.Components
{
Match match = regex.Match(receivedSignal);
previousResult = match.Success;
previousGroups = UseCaptureGroup && previousResult ? match.Groups : null;
previousReceivedSignal = receivedSignal;
}
@@ -75,7 +80,30 @@ namespace Barotrauma.Items.Components
}
}
string signalOut = previousResult ? Output : FalseOutput;
string signalOut;
if (previousResult)
{
if (UseCaptureGroup)
{
if (previousGroups != null && previousGroups.TryGetValue(Output, out Group group))
{
signalOut = group.Value;
}
else
{
signalOut = FalseOutput;
}
}
else
{
signalOut = Output;
}
}
else
{
signalOut = FalseOutput;
}
if (ContinuousOutput)
{
if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(0, signalOut, "signal_out", null); }
@@ -37,7 +37,7 @@ namespace Barotrauma.Items.Components
}
}
[Editable, Serialize(false, true, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)]
[Editable, Serialize(true, true, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)]
public bool IsOn
{
get
@@ -1,10 +1,11 @@
using System.Xml.Linq;
using Barotrauma.Networking;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Terminal : ItemComponent
{
private const int MaxMessageLength = 150;
private const int MaxMessageLength = ChatMessage.MaxLength;
public string DisplayedWelcomeMessage
{
@@ -21,7 +22,7 @@ namespace Barotrauma.Items.Components
{
if (welcomeMessage == value) { return; }
welcomeMessage = value;
DisplayedWelcomeMessage = TextManager.Get(welcomeMessage, returnNull: true) ?? welcomeMessage;
DisplayedWelcomeMessage = TextManager.Get(welcomeMessage, returnNull: true) ?? welcomeMessage.Replace("\\n", "\n");
}
}
@@ -45,7 +46,9 @@ namespace Barotrauma.Items.Components
{
signal = signal.Substring(0, MaxMessageLength);
}
ShowOnDisplay(signal);
string inputSignal = signal.Replace("\\n", "\n");
ShowOnDisplay(inputSignal);
}
}
}
@@ -63,7 +63,7 @@ namespace Barotrauma.Items.Components
public bool Hidden;
private float removeNodeDelay;
private float editNodeDelay;
private bool locked;
public bool Locked
@@ -198,7 +198,21 @@ namespace Barotrauma.Items.Components
int newNodeIndex = 0;
if (nodes.Count > 1)
{
if (Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos) < Vector2.DistanceSquared(nodes[0], nodePos))
if (connections[0] != null && connections[0] != newConnection)
{
if (Vector2.DistanceSquared(nodes[0], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos))
{
newNodeIndex = nodes.Count;
}
}
else if (connections[1] != null && connections[1] != newConnection)
{
if (Vector2.DistanceSquared(nodes[0], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) < Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos))
{
newNodeIndex = nodes.Count;
}
}
else if (Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos) < Vector2.DistanceSquared(nodes[0], nodePos))
{
newNodeIndex = nodes.Count;
}
@@ -276,7 +290,7 @@ namespace Barotrauma.Items.Components
if (nodes.Count == 0) { return; }
Character user = item.ParentInventory?.Owner as Character;
removeNodeDelay = (user?.SelectedConstruction == null) ? removeNodeDelay - deltaTime : 0.5f;
editNodeDelay = (user?.SelectedConstruction == null) ? editNodeDelay - deltaTime : 0.5f;
Submarine sub = item.Submarine;
if (connections[0] != null && connections[0].Item.Submarine != null) { sub = connections[0].Item.Submarine; }
@@ -402,7 +416,9 @@ namespace Barotrauma.Items.Components
#endif
//clients communicate node addition/removal with network events
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { return false; }
if (newNodePos != Vector2.Zero && canPlaceNode && nodes.Count > 0 && Vector2.Distance(newNodePos, nodes[nodes.Count - 1]) > MinNodeDistance)
if (newNodePos != Vector2.Zero && canPlaceNode && editNodeDelay <= 0.0f && nodes.Count > 0 &&
Vector2.DistanceSquared(newNodePos, nodes[nodes.Count - 1]) > MinNodeDistance * MinNodeDistance)
{
if (nodes.Count >= MaxNodeCount)
{
@@ -426,6 +442,7 @@ namespace Barotrauma.Items.Components
}
#endif
}
editNodeDelay = 0.1f;
return true;
}
@@ -436,7 +453,7 @@ namespace Barotrauma.Items.Components
//clients communicate node addition/removal with network events
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { return false; }
if (nodes.Count > 1 && removeNodeDelay <= 0.0f)
if (nodes.Count > 1 && editNodeDelay <= 0.0f)
{
nodes.RemoveAt(nodes.Count - 1);
UpdateSections();
@@ -452,7 +469,7 @@ namespace Barotrauma.Items.Components
}
#endif
}
removeNodeDelay = 0.1f;
editNodeDelay = 0.1f;
Drawable = IsActive || sections.Count > 0;
return true;
@@ -384,7 +384,7 @@ namespace Barotrauma.Items.Components
if (!flashLowPower && character != null && character == Character.Controlled)
{
flashLowPower = true;
GUI.PlayUISound(GUISoundType.PickItemFail);
SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
}
#endif
return false;
@@ -422,7 +422,7 @@ namespace Barotrauma.Items.Components
{
flashNoAmmo = true;
failedLaunchAttempts = 0;
GUI.PlayUISound(GUISoundType.PickItemFail);
SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
}
#endif
return false;
@@ -151,7 +151,7 @@ namespace Barotrauma
{
if (i < 0 || i >= Items.Length)
{
string errorMsg = "Inventory.TryPutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace;
string errorMsg = "Inventory.TryPutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Inventory.TryPutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return false;
}
@@ -192,7 +192,7 @@ namespace Barotrauma
{
if (i < 0 || i >= Items.Length)
{
string errorMsg = "Inventory.PutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace;
string errorMsg = "Inventory.PutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Inventory.PutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return;
}
@@ -304,13 +304,16 @@ namespace Barotrauma
for (int j = 0; j < otherInventory.capacity; j++)
{
if (otherInventory.Items[j] == item) otherInventory.Items[j] = null;
if (otherInventory.Items[j] == item) { otherInventory.Items[j] = null; }
}
for (int j = 0; j < capacity; j++)
{
if (Items[j] == existingItem) Items[j] = null;
if (Items[j] == existingItem) { Items[j] = null; }
}
(otherInventory.Owner as Character)?.DeselectItem(item);
(otherInventory.Owner as Character)?.DeselectItem(existingItem);
bool swapSuccessful = false;
if (otherIsEquipped)
{
@@ -486,6 +489,22 @@ namespace Barotrauma
}
Items[i].Remove();
}
}
}
public List<Item> GetAllItems()
{
List<Item> deletedItems = new List<Item>();
for (int i = 0; i < capacity; i++)
{
if (Items[i] == null) continue;
foreach (ItemContainer itemContainer in Items[i].GetComponents<ItemContainer>())
{
deletedItems.AddRange(itemContainer.Inventory.GetAllItems());
}
deletedItems.Add(Items[i]);
}
return deletedItems;
}
}
}
@@ -119,6 +119,8 @@ namespace Barotrauma
}
}
public bool EditableWhenEquipped { get; set; } = false;
//the inventory in which the item is contained in
public Inventory ParentInventory
{
@@ -1016,7 +1018,7 @@ namespace Barotrauma
{
string errorMsg =
"Attempted to move the item " + Name +
" to an invalid position (" + simPosition + ")\n" + Environment.StackTrace;
" to an invalid position (" + simPosition + ")\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
@@ -1073,7 +1075,7 @@ namespace Barotrauma
{
if (!MathUtils.IsValid(amount))
{
DebugConsole.ThrowError($"Attempted to move an item by an invalid amount ({amount})\n{Environment.StackTrace}");
DebugConsole.ThrowError($"Attempted to move an item by an invalid amount ({amount})\n{Environment.StackTrace.CleanupStackTrace()}");
return;
}
@@ -1360,12 +1362,15 @@ namespace Barotrauma
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true)
{
if (Indestructible) return new AttackResult();
if (Indestructible) { return new AttackResult(); }
float damageAmount = attack.GetItemDamage(deltaTime);
Condition -= damageAmount;
ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
if (damageAmount > 0)
{
ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
}
return new AttackResult(damageAmount, null);
}
@@ -1629,7 +1634,7 @@ namespace Barotrauma
OnCollisionProjSpecific(impact);
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
if (ImpactTolerance > 0.0f && condition > 0.0f && impact > ImpactTolerance)
if (ImpactTolerance > 0.0f && condition > 0.0f && Math.Abs(impact) > ImpactTolerance)
{
ApplyStatusEffects(ActionType.OnImpact, 1.0f);
#if SERVER
@@ -1653,10 +1658,14 @@ namespace Barotrauma
public override void FlipX(bool relativeToSub)
{
if (!Prefab.CanFlipX) { return; }
//call the base method even if the item can't flip, to handle repositioning when flipping the whole sub
base.FlipX(relativeToSub);
if (!Prefab.CanFlipX)
{
flippedX = false;
return;
}
#if CLIENT
if (Prefab.CanSpriteFlipX)
{
@@ -1672,10 +1681,15 @@ namespace Barotrauma
public override void FlipY(bool relativeToSub)
{
if (!Prefab.CanFlipY) { return; }
//call the base method even if the item can't flip, to handle repositioning when flipping the whole sub
base.FlipY(relativeToSub);
if (!Prefab.CanFlipY)
{
flippedY = false;
return;
}
#if CLIENT
if (Prefab.CanSpriteFlipY)
{
@@ -1980,17 +1994,14 @@ namespace Barotrauma
{
if (picker.SelectedConstruction == this)
{
if (picker.IsKeyHit(InputType.Select) || forceSelectKey) picker.SelectedConstruction = null;
if (picker.IsKeyHit(InputType.Select) || forceSelectKey)
{
picker.SelectedConstruction = null;
}
}
else if (selected)
{
picker.SelectedConstruction = this;
#if CLIENT
if (GameMain.GameSession?.CrewManager != null && picker == Character.Controlled && GetComponent<Ladder>() == null)
{
GameMain.GameSession.CrewManager.ToggleCrewListOpen = false;
}
#endif
}
}
@@ -2215,7 +2226,7 @@ namespace Barotrauma
{
if (Removed)
{
DebugConsole.ThrowError($"Tried to equip a removed item ({Name}).\n{Environment.StackTrace}");
DebugConsole.ThrowError($"Tried to equip a removed item ({Name}).\n{Environment.StackTrace.CleanupStackTrace()}");
return;
}
@@ -2257,7 +2268,7 @@ namespace Barotrauma
var propertyOwner = allProperties.Find(p => p.Second == property);
if (allProperties.Count > 1)
{
msg.WriteRangedInteger(allProperties.FindIndex(p => p.Second == property), 0, allProperties.Count - 1);
msg.Write((byte)allProperties.FindIndex(p => p.Second == property));
}
object value = property.GetValue(propertyOwner.First);
@@ -2339,7 +2350,7 @@ namespace Barotrauma
int propertyIndex = 0;
if (allProperties.Count > 1)
{
propertyIndex = msg.ReadRangedInteger(0, allProperties.Count - 1);
propertyIndex = msg.ReadByte();
}
bool allowEditing = true;
@@ -2360,6 +2371,7 @@ namespace Barotrauma
if (type == typeof(string))
{
string val = msg.ReadString();
logValue = val;
if (allowEditing)
{
property.TrySetValue(parentObject, val);
@@ -2712,7 +2724,7 @@ namespace Barotrauma
{
if (Removed)
{
DebugConsole.ThrowError("Attempting to remove an already removed item (" + Name + ")\n" + Environment.StackTrace);
DebugConsole.ThrowError("Attempting to remove an already removed item (" + Name + ")\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
DebugConsole.Log("Removing item " + Name + " (ID: " + ID + ")");
@@ -93,7 +93,7 @@ namespace Barotrauma
{
if (!Item.ItemList.Contains(container.Item))
{
string errorMsg = "Attempted to create a network event for an item (" + container.Item.Name + ") that hasn't been fully initialized yet.\n" + Environment.StackTrace;
string errorMsg = "Attempted to create a network event for an item (" + container.Item.Name + ") that hasn't been fully initialized yet.\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"ItemInventory.CreateServerEvent:EventForUninitializedItem" + container.Item.Name + container.Item.ID,
@@ -319,6 +319,13 @@ namespace Barotrauma
private set;
}
[Serialize(false, false)]
public bool AllowSellingWhenBroken
{
get;
private set;
}
[Serialize(false, false)]
public bool Indestructible
{
@@ -7,14 +7,13 @@ namespace Barotrauma
{
private Vector2 maxSize;
public bool Removed
{
get { return removed; }
}
public bool CausedByPsychosis;
public DummyFireSource(Vector2 maxSize, Vector2 worldPosition, Hull spawningHull = null, bool isNetworkMessage = false) : base(worldPosition, spawningHull, isNetworkMessage)
{
this.maxSize = maxSize;
DamagesItems = false;
DamagesCharacters = true;
}
public override float DamageRange
@@ -24,11 +23,7 @@ namespace Barotrauma
protected override void LimitSize()
{
if (hull == null) return;
position.X = Math.Max(hull.Rect.X, position.X);
position.Y = Math.Min(hull.Rect.Y, position.Y);
base.LimitSize();
size.X = Math.Min(maxSize.X, size.X);
size.Y = Math.Min(maxSize.Y, size.Y);
}
@@ -51,13 +51,13 @@ namespace Barotrauma
if (value == NullEntityID)
{
DebugConsole.ThrowError("Cannot set the ID of an entity to " + NullEntityID +
"! The value is reserved for entity events referring to a non-existent (e.g. removed) entity.\n" + Environment.StackTrace);
"! The value is reserved for entity events referring to a non-existent (e.g. removed) entity.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
if (value == EntitySpawnerID)
{
DebugConsole.ThrowError("Cannot set the ID of an entity to " + EntitySpawnerID +
"! The value is reserved for EntitySpawner.\n" + Environment.StackTrace);
"! The value is reserved for EntitySpawner.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
@@ -183,7 +183,7 @@ namespace Barotrauma
GameAnalyticsManager.AddErrorEventOnce(
"Entity.RemoveAll:Exception" + e.ToString(),
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Error while removing entity \"" + e.ToString() + " (" + exception.Message + ")\n" + exception.StackTrace);
"Error while removing entity \"" + e.ToString() + " (" + exception.Message + ")\n" + exception.StackTrace.CleanupStackTrace());
}
}
StringBuilder errorMsg = new StringBuilder();
@@ -266,7 +266,7 @@ namespace Barotrauma
GameAnalyticsManager.AddErrorEventOnce(
"Entity.FreeID:EntityNotFound" + ID,
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Entity " + ToString() + " (" + ID + ") not present in entity dictionary.\n" + Environment.StackTrace);
"Entity " + ToString() + " (" + ID + ") not present in entity dictionary.\n" + Environment.StackTrace.CleanupStackTrace());
}
else if (existingEntity != this)
{
@@ -105,7 +105,7 @@ namespace Barotrauma
if (hull != null && !string.IsNullOrWhiteSpace(decal) && decalSize > 0.0f)
{
hull.AddDecal(decal, worldPosition, decalSize, true);
hull.AddDecal(decal, worldPosition, decalSize, isNetworkEvent: false);
}
float displayRange = attack.Range;
@@ -340,6 +340,15 @@ namespace Barotrauma
}
}
if (c == Character.Controlled && !c.IsDead)
{
Limb head = c.AnimController.GetLimb(LimbType.Head);
if (damages.TryGetValue(head, out float headDamage) && headDamage > 0.0f && distFactors.TryGetValue(head, out float headFactor))
{
PlayTinnitusProjSpecific(headFactor);
}
}
//sever joints
if (attack.SeverLimbsProbability > 0.0f)
{
@@ -402,5 +411,7 @@ namespace Barotrauma
return damagedStructures;
}
static partial void PlayTinnitusProjSpecific(float volume);
}
}
@@ -69,6 +69,23 @@ namespace Barotrauma
get { return Math.Min((float)Math.Sqrt(size.X) * 10.0f, MaxDamageRange); }
}
public bool DamagesItems
{
get;
set;
} = true;
public bool DamagesCharacters
{
get;
set;
} = true;
public bool Removed
{
get { return removed; }
}
public Hull Hull
{
get { return hull; }
@@ -80,7 +97,7 @@ namespace Barotrauma
if (hull == null || worldPosition.Y < hull.WorldSurface) return;
#if CLIENT
if (!isNetworkMessage && GameMain.Client != null) return;
if (!isNetworkMessage && GameMain.Client != null) { return; }
#endif
hull.AddFireSource(this);
@@ -140,9 +157,43 @@ namespace Barotrauma
}
}
}
public static void UpdateAll(List<DummyFireSource> fireSources, float deltaTime)
{
for (int i = fireSources.Count - 1; i >= 0; i--)
{
fireSources[i].Update(deltaTime);
}
//combine overlapping fires
for (int i = fireSources.Count - 1; i >= 0; i--)
{
for (int j = i - 1; j >= 0; j--)
{
i = Math.Min(i, fireSources.Count - 1);
j = Math.Min(j, i - 1);
if (!fireSources[i].CheckOverLap(fireSources[j])) { continue; }
float leftEdge = Math.Min(fireSources[i].position.X, fireSources[j].position.X);
fireSources[j].size.X =
Math.Max(fireSources[i].position.X + fireSources[i].size.X, fireSources[j].position.X + fireSources[j].size.X)
- leftEdge;
fireSources[j].position.X = leftEdge;
fireSources[i].Remove();
}
}
}
private bool CheckOverLap(FireSource fireSource)
{
if (this is DummyFireSource != fireSource is DummyFireSource)
{
return false;
}
return !(position.X > fireSource.position.X + fireSource.size.X ||
position.X + size.X < fireSource.position.X);
}
@@ -152,8 +203,8 @@ namespace Barotrauma
//the firesource will start to shrink if oxygen percentage is below 10
float growModifier = Math.Min((hull.OxygenPercentage / 10.0f) - 1.0f, 1.0f);
DamageCharacters(deltaTime);
DamageItems(deltaTime);
if (DamagesCharacters) { DamageCharacters(deltaTime); }
if (DamagesItems) { DamageItems(deltaTime); }
if (hull.WaterVolume > 0.0f)
{
@@ -161,7 +212,10 @@ namespace Barotrauma
if (removed) { return; }
}
ReduceOxygen(deltaTime);
if (!(this is DummyFireSource))
{
ReduceOxygen(deltaTime);
}
AdjustXPos(growModifier, deltaTime);
@@ -175,21 +229,21 @@ namespace Barotrauma
LimitSize();
if (size.X > 256.0f)
if (size.X > 256.0f && !(this is DummyFireSource))
{
if (burnDecals.Count == 0)
{
var newDecal = hull.AddDecal("burnt", WorldPosition + size / 2, 1f, true);
var newDecal = hull.AddDecal("burnt", WorldPosition + size / 2, 1f, isNetworkEvent: false);
if (newDecal != null) { burnDecals.Add(newDecal); }
}
else if (WorldPosition.X < burnDecals[0].WorldPosition.X - 256.0f)
{
var newDecal = hull.AddDecal("burnt", WorldPosition, 1f, true);
var newDecal = hull.AddDecal("burnt", WorldPosition, 1f, isNetworkEvent: false);
if (newDecal != null) { burnDecals.Insert(0, newDecal); }
}
else if (WorldPosition.X + size.X > burnDecals[burnDecals.Count - 1].WorldPosition.X + 256.0f)
{
var newDecal = hull.AddDecal("burnt", WorldPosition + Vector2.UnitX * size.X, 1f, true);
var newDecal = hull.AddDecal("burnt", WorldPosition + Vector2.UnitX * size.X, 1f, isNetworkEvent: false);
if (newDecal != null) { burnDecals.Add(newDecal); }
}
}
@@ -281,14 +335,13 @@ namespace Barotrauma
private void DamageItems(float deltaTime)
{
if (size.X <= 0.0f) return;
if (size.X <= 0.0f) { return; }
#if CLIENT
if (GameMain.Client != null) return;
if (GameMain.Client != null) { return; }
#endif
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != hull || item.FireProof || item.Condition <= 0.0f) continue;
if (item.CurrentHull != hull || item.FireProof || item.Condition <= 0.0f) { continue; }
//don't apply OnFire effects if the item is inside a fireproof container
//(or if it's inside a container that's inside a fireproof container, etc)
@@ -300,8 +353,8 @@ namespace Barotrauma
}
float range = (float)Math.Sqrt(size.X) * 10.0f;
if (item.Position.X < position.X - range || item.Position.X > position.X + size.X + range) continue;
if (item.Position.Y < position.Y - size.Y || item.Position.Y > hull.Rect.Y) continue;
if (item.Position.X < position.X - range || item.Position.X > position.X + size.X + range) { continue; }
if (item.Position.Y < position.Y - size.Y || item.Position.Y > hull.Rect.Y) { continue; }
item.ApplyStatusEffects(ActionType.OnFire, deltaTime);
if (item.Condition <= 0.0f && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
@@ -162,7 +162,7 @@ namespace Barotrauma
{
if (!MathUtils.IsValid(amount))
{
DebugConsole.ThrowError($"Attempted to move a gap by an invalid amount ({amount})\n{Environment.StackTrace}");
DebugConsole.ThrowError($"Attempted to move a gap by an invalid amount ({amount})\n{Environment.StackTrace.CleanupStackTrace()}");
return;
}
@@ -65,8 +65,8 @@ namespace Barotrauma
Noise = new Vector2(
PerlinNoise.GetPerlin(Rect.X / 1000.0f, Rect.Y / 1000.0f),
PerlinNoise.GetPerlin(Rect.Y / 1000.0f + 0.5f, Rect.X / 1000.0f + 0.5f));
Color = DirtColor = Color.Lerp(new Color(10, 10, 10, 100), new Color(54, 57, 28, 200), Noise.X);
DirtColor = Color.Lerp(new Color(10, 10, 10, 100), new Color(54, 57, 28, 200), Noise.X);
}
public bool SetColor(Color color)
@@ -389,6 +389,8 @@ namespace Barotrauma
public List<FireSource> FireSources { get; private set; }
public List<DummyFireSource> FakeFireSources { get; private set; }
public Hull(MapEntityPrefab prefab, Rectangle rectangle)
: this (prefab, rectangle, Submarine.MainSub)
{
@@ -405,6 +407,7 @@ namespace Barotrauma
OxygenPercentage = 100.0f;
FireSources = new List<FireSource>();
FakeFireSources = new List<DummyFireSource>();
properties = SerializableProperty.GetProperties(this);
@@ -554,7 +557,7 @@ namespace Barotrauma
{
if (!MathUtils.IsValid(amount))
{
DebugConsole.ThrowError($"Attempted to move a hull by an invalid amount ({amount})\n{Environment.StackTrace}");
DebugConsole.ThrowError($"Attempted to move a hull by an invalid amount ({amount})\n{Environment.StackTrace.CleanupStackTrace()}");
return;
}
@@ -583,11 +586,13 @@ namespace Barotrauma
}
List<FireSource> fireSourcesToRemove = new List<FireSource>(FireSources);
fireSourcesToRemove.AddRange(FakeFireSources);
foreach (FireSource fireSource in fireSourcesToRemove)
{
fireSource.Remove();
}
FireSources.Clear();
FakeFireSources.Clear();
if (EntityGrids != null)
{
@@ -627,10 +632,17 @@ namespace Barotrauma
public void AddFireSource(FireSource fireSource)
{
FireSources.Add(fireSource);
if (fireSource is DummyFireSource dummyFire)
{
FakeFireSources.Add(dummyFire);
}
else
{
FireSources.Add(fireSource);
}
}
public Decal AddDecal(UInt32 decalId, Vector2 worldPosition, float scale, bool isNetworkEvent)
public Decal AddDecal(UInt32 decalId, Vector2 worldPosition, float scale, bool isNetworkEvent, int? spriteIndex = null)
{
//clients are only allowed to create decals when the server says so
if (!isNetworkEvent && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
@@ -644,11 +656,11 @@ namespace Barotrauma
DebugConsole.ThrowError($"Could not find a decal prefab with the UInt identifier {decalId}!");
return null;
}
return AddDecal(decal.Name, worldPosition, scale, isNetworkEvent);
return AddDecal(decal.Name, worldPosition, scale, isNetworkEvent, spriteIndex);
}
public Decal AddDecal(string decalName, Vector2 worldPosition, float scale, bool isNetworkEvent)
public Decal AddDecal(string decalName, Vector2 worldPosition, float scale, bool isNetworkEvent, int? spriteIndex = null)
{
//clients are only allowed to create decals when the server says so
if (!isNetworkEvent && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
@@ -658,7 +670,7 @@ namespace Barotrauma
if (decals.Count >= MaxDecalsPerHull) { return null; }
var decal = GameMain.DecalManager.CreateDecal(decalName, scale, worldPosition, this);
var decal = GameMain.DecalManager.CreateDecal(decalName, scale, worldPosition, this, spriteIndex);
if (decal != null)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
@@ -679,7 +691,19 @@ namespace Barotrauma
Oxygen -= OxygenDeteriorationSpeed * deltaTime;
if ((Character.Controlled?.CharacterHealth?.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f)
{
for (int i = FakeFireSources.Count - 1; i >= 0; i--)
{
if (FakeFireSources[i].CausedByPsychosis)
{
FakeFireSources[i].Remove();
}
}
}
FireSource.UpdateAll(FireSources, deltaTime);
FireSource.UpdateAll(FakeFireSources, deltaTime);
foreach (Decal decal in decals)
{
@@ -687,7 +711,6 @@ namespace Barotrauma
}
decals.RemoveAll(d => d.FadeTimer >= d.LifeTime || d.BaseAlpha <= 0.001f);
if (aiTarget != null)
{
aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : Submarine.Velocity.Length() / 2 * aiTarget.MaxSightRange;
@@ -842,17 +865,31 @@ namespace Barotrauma
}
}
public void Extinguish(float deltaTime, float amount, Vector2 position)
public void Extinguish(float deltaTime, float amount, Vector2 position, bool extinguishRealFires = true, bool extinguishFakeFires = true)
{
for (int i = FireSources.Count - 1; i >= 0; i--)
if (extinguishRealFires)
{
FireSources[i].Extinguish(deltaTime, amount, position);
for (int i = FireSources.Count - 1; i >= 0; i--)
{
FireSources[i].Extinguish(deltaTime, amount, position);
}
}
if (extinguishFakeFires)
{
for (int i = FakeFireSources.Count - 1; i >= 0; i--)
{
FakeFireSources[i].Extinguish(deltaTime, amount, position);
}
}
}
public void RemoveFire(FireSource fire)
{
FireSources.Remove(fire);
if (fire is DummyFireSource dummyFire)
{
FakeFireSources.Remove(dummyFire);
}
}
private readonly HashSet<Hull> adjacentHulls = new HashSet<Hull>();
@@ -1221,7 +1258,7 @@ namespace Barotrauma
return index >= 0 && row >= 0 && BackgroundSections.Count > index && BackgroundSections[index] != null && BackgroundSections[index].RowIndex == row;
}
public void SetSectionColorOrStrength(BackgroundSection section, Color? color, float? strength, bool requiresUpdate, bool isCleaning)
public void IncreaseSectionColorOrStrength(BackgroundSection section, Color? color, float? strength, bool requiresUpdate, bool isCleaning)
{
bool sectionUpdated = isCleaning;
if (color != null)
@@ -1263,13 +1300,31 @@ namespace Barotrauma
}
}
public void SetSectionColorOrStrength(BackgroundSection section, Color? color, float? strength)
{
if (color != null)
{
section.SetColor(color.Value);
}
if (strength != null)
{
float previous = section.SetColorStrength(Math.Max(minColorStrength, Math.Min(maxColorStrength, section.ColorStrength + strength.Value)));
if (previous != -1f)
{
#if CLIENT
paintAmount = Math.Max(0, paintAmount + (section.ColorStrength - previous) / BackgroundSections.Count);
#endif
}
}
}
public void DirtySections(List<BackgroundSection> sections, float dirtyVal)
{
if (sections == null) { return; }
for (int i = 0; i < sections.Count; i++)
{
float sectionDirtyVal = dirtyVal;
SetSectionColorOrStrength(sections[i], sections[i].DirtColor, sectionDirtyVal, false, false);
IncreaseSectionColorOrStrength(sections[i], sections[i].DirtColor, dirtyVal, false, false);
}
}
@@ -1283,11 +1338,17 @@ namespace Barotrauma
{
decal.Clean(cleanVal);
decalsCleaned = true;
#if SERVER
decalUpdatePending = true;
#elif CLIENT
pendingDecalUpdates.Add(decal);
networkUpdatePending = true;
#endif
}
}
if (section.ColorStrength == 0 && !decalsCleaned) { return; }
SetSectionColorOrStrength(section, null, cleanVal, updateRequired, true);
IncreaseSectionColorOrStrength(section, null, cleanVal, updateRequired, true);
}
#endregion
@@ -1341,10 +1402,12 @@ namespace Barotrauma
Vector2 pos = subElement.GetAttributeVector2("pos", Vector2.Zero);
float scale = subElement.GetAttributeFloat("scale", 1.0f);
float timer = subElement.GetAttributeFloat("timer", 1.0f);
float baseAlpha = subElement.GetAttributeFloat("alpha", 1.0f);
var decal = hull.AddDecal(id, pos + hull.WorldRect.Location.ToVector2(), scale, true);
if (decal != null)
{
decal.FadeTimer = timer;
decal.BaseAlpha = baseAlpha;
}
break;
}
@@ -1362,7 +1425,7 @@ namespace Barotrauma
if (int.TryParse(backgroundSectionData[0], out int index) &&
float.TryParse(backgroundSectionData[2], NumberStyles.Any, CultureInfo.InvariantCulture, out float strength))
{
hull.SetSectionColorOrStrength(hull.BackgroundSections[index], color, strength, false, false);
hull.SetSectionColorOrStrength(hull.BackgroundSections[index], color, strength);
}
}
}
@@ -1377,7 +1440,7 @@ namespace Barotrauma
{
if (Submarine == null)
{
string errorMsg = "Error - tried to save a hull that's not a part of any submarine.\n" + Environment.StackTrace;
string errorMsg = "Error - tried to save a hull that's not a part of any submarine.\n" + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Hull.Save:WorldHull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
return null;
@@ -1420,7 +1483,8 @@ namespace Barotrauma
new XAttribute("id", decal.Prefab.Identifier),
new XAttribute("pos", XMLExtensions.Vector2ToString(decal.NonClampedPosition)),
new XAttribute("scale", decal.Scale.ToString("G", CultureInfo.InvariantCulture)),
new XAttribute("timer", decal.FadeTimer.ToString("G", CultureInfo.InvariantCulture))
new XAttribute("timer", decal.FadeTimer.ToString("G", CultureInfo.InvariantCulture)),
new XAttribute("alpha", decal.BaseAlpha.ToString("G", CultureInfo.InvariantCulture))
));
}
@@ -110,7 +110,13 @@ namespace Barotrauma
protected override void CreateInstance(Rectangle rect)
{
CreateInstance(rect.Location.ToVector2(), Submarine.MainSub);
var loaded = CreateInstance(rect.Location.ToVector2(), Submarine.MainSub);
#if CLIENT
if (Screen.Selected is SubEditorScreen)
{
SubEditorScreen.StoreCommand(new AddOrDeleteCommand(loaded, false));
}
#endif
}
public List<MapEntity> CreateInstance(Vector2 position, Submarine sub, bool selectPrefabs = false)
@@ -1398,7 +1398,7 @@ namespace Barotrauma
}
if (!suitablePositions.Any())
{
string errorMsg = "Could not find a suitable position of interest. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace;
string errorMsg = "Could not find a suitable position of interest. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:PositionTypeNotFound", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
@@ -1418,7 +1418,7 @@ namespace Barotrauma
}
if (!farEnoughPositions.Any())
{
string errorMsg = "Could not find a position of interest far enough from the submarines. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace;
string errorMsg = "Could not find a position of interest far enough from the submarines. (PositionType: " + positionType + ", minDistFromSubs: " + minDistFromSubs + ")\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Level.TryGetInterestingPosition:TooCloseToSubs", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);

Some files were not shown because too many files have changed in this diff Show More