(6eeea9b7c) v0.9.10.0.0

This commit is contained in:
Joonas Rikkonen
2020-06-04 16:41:07 +03:00
parent ce4ccd99ac
commit eeac247a8e
366 changed files with 7772 additions and 3692 deletions
@@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace Barotrauma
{
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive }
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect }
abstract partial class AIController : ISteerable
{
@@ -40,6 +40,7 @@ namespace Barotrauma
private readonly float avoidLookAheadDistance;
private IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager;
private SteeringManager outsideSteering, insideSteering;
private float updateTargetsTimer;
@@ -85,8 +86,9 @@ namespace Barotrauma
private readonly float colliderWidth;
private readonly float colliderLength;
private readonly int requiredHoleCount;
private readonly bool canAttackSub;
private readonly bool canAttackCharacters;
private bool canAttackWalls;
private bool canAttackDoors;
private bool canAttackCharacters;
private readonly float priorityFearIncreasement = 2;
private readonly float memoryFadeTime = 0.5f;
@@ -94,9 +96,13 @@ namespace Barotrauma
private float avoidTimer;
public bool StayInsideLevel = true;
public LatchOntoAI LatchOntoAI { get; private set; }
public SwarmBehavior SwarmBehavior { get; private set; }
public CharacterParams.TargetParams SelectedTargetingParams { get { return selectedTargetingParams; } }
public bool AttackHumans
{
get
@@ -186,24 +192,9 @@ namespace Barotrauma
}
}
bool canBreakDoors = false;
if (GetTarget("room")?.Priority > 0.0f)
{
var currentContexts = Character.GetAttackContexts();
foreach (Limb limb in Character.AnimController.Limbs)
{
if (limb.attack == null) { continue; }
if (!limb.attack.IsValidTarget(AttackTarget.Structure)) { continue; }
if (limb.attack.IsValidContext(currentContexts) && limb.attack.StructureDamage > 0.0f)
{
canBreakDoors = true;
break;
}
}
}
ReevaluateAttacks();
outsideSteering = new SteeringManager(this);
insideSteering = new IndoorsSteeringManager(this, false, canBreakDoors);
insideSteering = new IndoorsSteeringManager(this, false, canAttackDoors);
steeringManager = outsideSteering;
State = AIState.Idle;
@@ -213,9 +204,6 @@ namespace Barotrauma
requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
avoidLookAheadDistance = Math.Max(colliderWidth * 3, 1.5f);
canAttackSub = Character.AnimController.CanAttackSubmarine;
canAttackCharacters = Character.AnimController.CanAttackCharacters;
}
private CharacterParams.AIParams AIParams => Character.Params.AI;
@@ -226,31 +214,32 @@ namespace Barotrauma
public void SelectTarget(AITarget target, float priority)
{
SelectedAiTarget = target;
selectedTargetMemory = GetTargetMemory(target);
selectedTargetMemory = GetTargetMemory(target, true);
selectedTargetMemory.Priority = priority;
}
private float escapeMargin;
private float movementMargin;
public override void Update(float deltaTime)
{
if (DisableEnemyAI) { return; }
base.Update(deltaTime);
bool ignorePlatforms = (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
if (steeringManager is IndoorsSteeringManager)
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
if (steeringManager == insideSteering)
{
var currPath = ((IndoorsSteeringManager)steeringManager).CurrentPath;
var currPath = PathSteering.CurrentPath;
if (currPath != null && currPath.CurrentNode != null)
{
if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y)
{
ignorePlatforms = true;
// Don't allow to jump from too high.
float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2;
float height = Math.Abs(currPath.CurrentNode.SimPosition.Y - Character.SimPosition.Y);
ignorePlatforms = height < allowedJumpHeight;
}
}
}
Character.AnimController.IgnorePlatforms = ignorePlatforms;
//clients get the facing direction from the server
@@ -396,7 +385,7 @@ namespace Barotrauma
{
bool isBeingChased = IsBeingChased;
float reactDistance = !isBeingChased && selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget);
if (squaredDistance <= Math.Pow(reactDistance + escapeMargin, 2))
if (squaredDistance <= Math.Pow(reactDistance + movementMargin, 2))
{
float halfReactDistance = reactDistance / 2;
float attackDistance = selectedTargetingParams != null && selectedTargetingParams.AttackDistance > 0 ? selectedTargetingParams.AttackDistance : halfReactDistance;
@@ -408,26 +397,56 @@ namespace Barotrauma
else
{
run = isBeingChased ? true : squaredDistance < Math.Pow(halfReactDistance, 2);
if (escapeMargin <= 0)
if (movementMargin <= 0)
{
escapeMargin = halfReactDistance;
movementMargin = halfReactDistance;
}
escapeMargin = MathHelper.Clamp(escapeMargin += deltaTime, halfReactDistance, reactDistance);
movementMargin = MathHelper.Clamp(movementMargin += deltaTime, halfReactDistance, reactDistance);
UpdateEscape(deltaTime);
}
}
else
{
escapeMargin = 0;
movementMargin = 0;
UpdateIdle(deltaTime);
}
}
break;
case AIState.Protect:
if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed)
{
State = AIState.Idle;
return;
}
if (SelectedAiTarget.Entity is Character targetCharacter && targetCharacter.LastAttacker is Character attacker)
{
// 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);
if (sqrDist > Math.Pow(reactDist + movementMargin, 2))
{
movementMargin = reactDist;
run = true;
UpdateFollow(deltaTime);
}
else
{
movementMargin = MathHelper.Clamp(movementMargin -= deltaTime, 0, reactDist);
UpdateIdle(deltaTime);
}
break;
default:
throw new NotImplementedException();
}
LatchOntoAI?.Update(this, deltaTime);
if (!Character.AnimController.SimplePhysicsEnabled)
{
LatchOntoAI?.Update(this, deltaTime);
}
IsSteeringThroughGap = false;
if (SwarmBehavior != null)
{
@@ -435,6 +454,8 @@ namespace Barotrauma
SwarmBehavior.Refresh();
SwarmBehavior.UpdateSteering(deltaTime);
}
// Ensure that the creature keeps inside the level
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);
@@ -459,13 +480,12 @@ namespace Barotrauma
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
return;
}
SteerInsideLevel(deltaTime);
}
var target = SelectedAiTarget ?? _lastAiTarget;
if (target?.Entity != null && !target.Entity.Removed && PreviousState == AIState.Attack && Character.CurrentHull == null)
{
// Keep heading to the last known position of the target
var memory = GetTargetMemory(target);
var memory = GetTargetMemory(target, false);
if (memory != null)
{
var location = memory.Location;
@@ -579,10 +599,10 @@ namespace Barotrauma
}
else if (pathSteering != null)
{
if (canAttackSub && hasValidPath)
if (canAttackDoors && hasValidPath)
{
var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
if (door != null && !door.IsOpen && !door.IsBroken)
{
if (SelectedAiTarget != door.Item.AiTarget)
{
@@ -618,7 +638,6 @@ namespace Barotrauma
{
SteeringManager.SteeringWander();
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
SteerInsideLevel(deltaTime);
}
}
}
@@ -644,17 +663,18 @@ namespace Barotrauma
if (SelectedAiTarget.Entity is Item item)
{
// If the item is held by a character, attack the character instead.
var pickable = item.GetComponent<Pickable>();
if (pickable != null)
Character owner = GetOwner(item);
if (owner != null)
{
Entity owner = pickable.Picker ?? item.ParentInventory?.Owner;
if (owner != null)
if (IsFriendly(Character, owner))
{
var target = owner.AiTarget;
if (target?.Entity != null && !target.Entity.Removed)
{
SelectedAiTarget = target;
}
ResetAITarget();
State = AIState.Idle;
return;
}
else
{
SelectedAiTarget = owner.AiTarget;
}
}
}
@@ -666,7 +686,8 @@ namespace Barotrauma
{
attackWorldPos += wallTarget.Structure.Submarine.Position;
}
attackSimPos = ConvertUnits.ToSimUnits(attackWorldPos);
attackSimPos = Character.Submarine == wallTarget.Structure.Submarine ? wallTarget.Position : attackWorldPos;
attackSimPos = ConvertUnits.ToSimUnits(attackSimPos);
}
else
{
@@ -704,7 +725,7 @@ namespace Barotrauma
var door = i.GetComponent<Door>();
// Steer through the door manually if it's open or broken
// Don't try to enter dry hulls if cannot walk or if the gap is too narrow
if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && door.IsOpen)
if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && (door.IsOpen || door.IsBroken))
{
if (Character.AnimController.CanWalk || door.LinkedGap.FlowTargetHull.WaterPercentage > 25)
{
@@ -722,7 +743,6 @@ namespace Barotrauma
}
else if (SelectedAiTarget.Entity is Structure w && wallTarget == null)
{
// Targeting only the outer walls
bool isBroken = true;
for (int i = 0; i < w.Sections.Length; i++)
{
@@ -893,35 +913,23 @@ namespace Barotrauma
}
canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0;
}
if (!canAttack && SelectedAiTarget.Entity.Submarine != null && !canAttackSub)
if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls))
{
float dist = Vector2.Distance(Character.AnimController.MainLimb.WorldPosition, attackWorldPos);
if (wallTarget != null)
if (Vector2.DistanceSquared(Character.WorldPosition, attackWorldPos) < 2000 * 2000)
{
// Steer towards the target, but turn away if a wall is blocking the way
if (dist < ConvertUnits.ToDisplayUnits(colliderLength) * 3)
{
State = AIState.Idle;
IgnoreTarget(SelectedAiTarget);
// Resetting the ai target prevents the character from chasing it
ResetAITarget();
return;
}
}
else if (dist < 1000)
{
// Check that we are not bumping into a door
// Check that we are not bumping into a door or a wall
Vector2 rayStart = SimPosition;
if (Character.Submarine == null)
{
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
}
Vector2 toTarget = SelectedAiTarget.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + toTarget.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 2);
Vector2 dir = SelectedAiTarget.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 2);
Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
if (Submarine.LastPickedFraction != 1.0f && closestBody != null && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent<Door>() != null)
if (Submarine.LastPickedFraction != 1.0f && closestBody != null &&
(!AIParams.TargetOuterWalls || !canAttackWalls && closestBody.UserData is Structure s && s.Submarine != null || !canAttackDoors && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent<Door>() != null))
{
// Target is unreachable, there's a door ahead
// Target is unreachable, there's a door or wall ahead
State = AIState.Idle;
IgnoreTarget(SelectedAiTarget);
ResetAITarget();
@@ -934,23 +942,28 @@ namespace Barotrauma
Character targetCharacter = SelectedAiTarget.Entity as Character;
if (canAttack)
{
// Target a specific limb instead of the target center position
if (wallTarget == null && targetCharacter != null)
if (!Character.AnimController.SimplePhysicsEnabled)
{
var targetLimbType = AttackingLimb.Params.Attack.Attack.TargetLimbType;
attackTargetLimb = GetTargetLimb(AttackingLimb, targetCharacter, targetLimbType);
if (attackTargetLimb == null)
// Target a specific limb instead of the target center position
if (wallTarget == null && targetCharacter != null)
{
State = AIState.Idle;
IgnoreTarget(SelectedAiTarget);
ResetAITarget();
return;
var targetLimbType = AttackingLimb.Params.Attack.Attack.TargetLimbType;
attackTargetLimb = GetTargetLimb(AttackingLimb, targetCharacter, targetLimbType);
if (attackTargetLimb == null)
{
State = AIState.Idle;
IgnoreTarget(SelectedAiTarget);
ResetAITarget();
return;
}
attackWorldPos = attackTargetLimb.WorldPosition;
attackSimPos = Character.GetRelativeSimPosition(attackTargetLimb);
}
attackWorldPos = attackTargetLimb.WorldPosition;
attackSimPos = Character.GetRelativeSimPosition(attackTargetLimb);
}
// Check that we can reach the target
Vector2 toTarget = attackWorldPos - AttackingLimb.WorldPosition;
Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackingLimb.WorldPosition;
Vector2 toTarget = attackWorldPos - attackLimbPos;
// Add a margin when the target is moving away, because otherwise it might be difficult to reach it (the attack takes some time to perform)
if (wallTarget != null)
{
if (wallTarget.Structure.Submarine != null)
@@ -961,7 +974,6 @@ namespace Barotrauma
}
else if (targetCharacter != null)
{
// Add a margin when the target is moving away, because otherwise it might be difficult to reach it (the attack takes some time to perform)
Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity);
toTarget += margin;
}
@@ -981,6 +993,7 @@ namespace Barotrauma
return ConvertUnits.ToDisplayUnits(targetVelocity) * AttackingLimb.attack.Duration * dot;
}
// Check that we can reach the target
distance = toTarget.Length();
canAttack = distance < AttackingLimb.attack.Range;
if (!canAttack && !IsCoolDownRunning)
@@ -1035,21 +1048,25 @@ namespace Barotrauma
}
else
{
Vector2 offset = Character.SimPosition - steeringLimb.SimPosition;
// Offset so that we don't overshoot the movement
Vector2 steerPos = attackSimPos + offset;
Vector2 steerPos = attackSimPos;
if (!Character.AnimController.SimplePhysicsEnabled)
{
// Offset so that we don't overshoot the movement
Vector2 offset = Character.SimPosition - steeringLimb.SimPosition;
steerPos += offset;
}
if (SteeringManager is IndoorsSteeringManager pathSteering)
{
if (pathSteering.CurrentPath != null)
{
// Attack doors
if (canAttackSub)
if (canAttackDoors)
{
// If the target is in the same hull, there shouldn't be any doors blocking the path
if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull)
{
var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
if (door != null && !door.IsOpen && !door.IsBroken)
{
if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget)
{
@@ -1063,13 +1080,14 @@ namespace Barotrauma
if ((Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) &&
(targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull) || Character.CanSeeTarget(SelectedAiTarget.Entity)))
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - steeringLimb.SimPosition));
Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition;
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(steerPos - myPos));
}
else
{
SteeringManager.SteeringSeek(steerPos, 2);
// Switch to Idle when cannot reach the target and if cannot damage the walls
if ((!canAttackSub || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
if ((!canAttackWalls || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
{
State = AIState.Idle;
return;
@@ -1145,7 +1163,7 @@ namespace Barotrauma
if (attack == null) { continue; }
if (attack.CoolDownTimer > 0) { continue; }
if (!attack.IsValidContext(currentContexts)) { continue; }
if (!attack.IsValidTarget(target)) { continue; }
if (!attack.IsValidTarget(target as IDamageable)) { continue; }
if (target is ISerializableEntity se && target is Character)
{
if (attack.Conditionals.Any(c => !c.Matches(se))) { continue; }
@@ -1176,6 +1194,7 @@ namespace Barotrauma
float CalculatePriority(Limb limb, Vector2 attackPos)
{
if (Character.AnimController.SimplePhysicsEnabled) { return 1 + limb.attack.Priority; }
float dist = Vector2.Distance(limb.WorldPosition, attackPos);
// The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it.
// We also need a max value that is more than the actual range.
@@ -1242,7 +1261,10 @@ namespace Barotrauma
LatchOntoAI?.SetAttachTarget(wall.Submarine.PhysicsBody.FarseerBody, wall.Submarine, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
{
wallTarget = new WallTarget(sectionPos, wall, sectionIndex);
if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner"))
{
wallTarget = new WallTarget(sectionPos, wall, sectionIndex);
}
}
}
if (!Character.AnimController.CanEnterSubmarine && wallTarget == null)
@@ -1271,7 +1293,7 @@ namespace Barotrauma
}
return isDisabled;
}
public override void OnAttacked(Character attacker, AttackResult attackResult)
{
float reactionTime = Rand.Range(0.1f, 0.3f);
@@ -1281,7 +1303,7 @@ namespace Barotrauma
Character.AnimController.ReleaseStuckLimbs();
LatchOntoAI?.DeattachFromBody();
if (attacker == null || attacker.AiTarget == null) { return; }
bool isFriendly = attacker.SpeciesName == Character.SpeciesName || attacker.Params.Group == Character.Params.Group;
bool isFriendly = IsFriendly(Character, attacker);
if (wasLatched)
{
avoidTimer = avoidTime * Rand.Range(0.75f, 1.25f);
@@ -1302,7 +1324,7 @@ namespace Barotrauma
}
if (!isFriendly && attackResult.Damage > 0.0f)
{
bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackSub;
bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackWalls;
if (Character.Params.AI.AttackWhenProvoked && canAttack)
{
if (attacker.IsHusk)
@@ -1356,7 +1378,7 @@ namespace Barotrauma
}
}
AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget);
AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, true);
targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AggressionHurt;
// Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown
@@ -1399,7 +1421,7 @@ namespace Barotrauma
var aiTarget = wallTarget.Structure.AiTarget;
if (aiTarget != null && SelectedAiTarget != aiTarget)
{
SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget).Priority);
SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, true).Priority);
}
}
IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable;
@@ -1464,7 +1486,7 @@ namespace Barotrauma
State = AIState.Idle;
return;
}
Vector2 mouthPos = Character.AnimController.GetMouthPosition().Value;
Vector2 mouthPos = Character.AnimController.SimplePhysicsEnabled ? SimPosition : Character.AnimController.GetMouthPosition().Value;
Vector2 attackSimPosition = Character.GetRelativeSimPosition(target);
Vector2 limbDiff = attackSimPosition - mouthPos;
float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 2);
@@ -1493,6 +1515,25 @@ namespace Barotrauma
#endregion
private void UpdateFollow(float deltaTime)
{
if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed)
{
State = AIState.Idle;
return;
}
Vector2 dir = Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition);
if (!MathUtils.IsValid(dir))
{
return;
}
steeringManager.SteeringManual(deltaTime, dir);
if (Character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
}
}
#region Targeting
private bool IsLatchedOnSub => LatchOntoAI != null && LatchOntoAI.IsAttachedToSub;
@@ -1619,13 +1660,16 @@ namespace Barotrauma
Door door = null;
if (aiTarget.Entity is Item item)
{
//item inside and we're outside -> attack the hull
if (item.CurrentHull != null && character.CurrentHull == null)
{
targetingTag = "room";
}
door = item.GetComponent<Door>();
bool targetingFromOutsideToInside = item.CurrentHull != null && character.CurrentHull == null;
if (targetingFromOutsideToInside)
{
if (door != null && !canAttackDoors || !canAttackWalls)
{
// Can't reach
continue;
}
}
foreach (var prio in AIParams.Targets)
{
if (item.HasTag(prio.Tag))
@@ -1634,7 +1678,25 @@ namespace Barotrauma
break;
}
}
if (door == null && targetingTag == null)
{
if (item.GetComponent<Sonar>() != null)
{
targetingTag = "sonar";
}
else if (targetingFromOutsideToInside)
{
targetingTag = "room";
}
}
else if (targetingTag == "nasonov")
{
if ((item.Submarine == null || !item.Submarine.Info.IsPlayer) && item.ParentInventory == null)
{
// Only target nasonovartifacts when they are held be a player or inside the playersub
continue;
}
}
// Ignore the target if it's a decoy and the character is already inside a sub
if (character.CurrentHull != null && targetingTag == "decoy")
{
@@ -1649,15 +1711,13 @@ namespace Barotrauma
// Ignore structures that doesn't have a body (not walls)
continue;
}
if (s.IsPlatform)
if (s.IsPlatform) { continue; }
if (s.Submarine == null) { continue; }
bool isCharacterInside = character.CurrentHull != null;
bool isInnerWall = s.prefab.Tags.Contains("inner");
if (isInnerWall && !isCharacterInside)
{
continue;
}
bool isCharacterOutside = s.Submarine == null || character.CurrentHull == null;
bool targetInnerWalls = AIParams.TargetInnerWalls;
if (!isCharacterOutside && !targetInnerWalls)
{
// Ignore walls when inside (walltargets still work)
// Ignore inner walls when outside (walltargets still work)
continue;
}
valueModifier = 1;
@@ -1670,48 +1730,71 @@ namespace Barotrauma
var section = s.Sections[i];
if (section.gap == null) { continue; }
bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null;
isInnerWall = isInnerWall || !leadsInside;
if (Character.AnimController.CanEnterSubmarine)
{
if (isCharacterOutside)
if (!isCharacterInside)
{
if (CanPassThroughHole(s, i))
{
valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : (targetInnerWalls ? 1 : 0);
valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : 0;
}
else
else if (AggressiveBoarding && leadsInside && canAttackWalls && AIParams.TargetOuterWalls)
{
// Ignore holes that cannot be passed through if cannot attack items/structures. Holes that are big enough should be targeted, so that we can get in
if (!canAttackSub)
// Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside
valueModifier *= 1 + section.gap.Open;
}
}
else
{
// Inside
if (AggressiveBoarding)
{
if (!isInnerWall)
{
// Only interested in getting inside (aggressive boarder) -> don't target outer walls when already inside
valueModifier = 0;
break;
}
else if (CanPassThroughHole(s, i))
{
valueModifier *= isInnerWall ? 1 : 0;
}
else if (!canAttackWalls)
{
valueModifier = 0;
break;
}
if (AggressiveBoarding && leadsInside)
}
else
{
if (!canAttackWalls)
{
// Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside
valueModifier *= 1 + section.gap.Open;
valueModifier = 0;
break;
}
// We are actually interested in breaking things -> reduce the priority when the wall is already broken
// (Terminalcells)
valueModifier *= 1 - section.gap.Open * 0.25f;
}
}
else if (!canAttackSub || CanPassThroughHole(s, i))
}
else
{
// Cannot enter
if (isInnerWall || !canAttackWalls)
{
// Already inside -> ignore holes in the walls and ignore walls if cannot attack the sub.
// Ignore inner walls and all walls if cannot do damage on walls.
valueModifier = 0;
break;
}
else if (canAttackSub && !AggressiveBoarding)
else if (AggressiveBoarding)
{
// We are actually interested in breaking things -> reduce the priority when the wall is already broken
valueModifier *= 1 - section.gap.Open * 0.25f;
// Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside
// (Bonethreshers)
valueModifier *= 1 + section.gap.Open;
}
}
else if (!leadsInside || !canAttackSub)
{
// Can't get in, ignore inner walls
// Also ignore all walls if cannot attack the sub
valueModifier = 0;
break;
}
}
}
else
@@ -1727,31 +1810,34 @@ namespace Barotrauma
}
if (door.Item.Submarine == null) { continue;}
bool isOutdoor = door.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom;
bool isOpen = door.IsOpen;
if (!isOpen && (!canAttackSub))
bool isOpen = door.IsOpen || door.IsBroken;
if (!isOpen && !canAttackDoors || (isOutdoor && !AIParams.TargetOuterWalls))
{
// Ignore doors that are not open if cannot attack items/structures. Open doors should be targeted, so that we can get in if we are aggressive boarders
valueModifier = 0;
// Ignore doors that are not open if cannot attack doors or shouldn't target outer doors.
continue;
}
if (character.CurrentHull == null)
if (isOpen && (!Character.AnimController.CanEnterSubmarine || !AggressiveBoarding))
{
valueModifier = isOutdoor ? 1 : 0;
// Ignore broken and open doors
// Aggressive boarders don't ignore open doors, because they use them for get in.
continue;
}
else if (AggressiveBoarding)
if (AggressiveBoarding)
{
// Increase priority if the character is outside and an aggressive boarder, and the door is from outside to inside
if (character.CurrentHull == null)
// Increase the priority if the character is outside and the door is from outside to inside
if (character.CurrentHull == null && isOutdoor)
{
valueModifier *= isOpen ? 5 : 1;
}
else
{
valueModifier *= isOpen ? 0 : 1;
// Inside
valueModifier *= isOpen || isOutdoor ? 0 : 1;
}
}
else if (!Character.AnimController.CanEnterSubmarine && isOpen) //ignore broken and open doors
else if (character.CurrentHull == null)
{
continue;
valueModifier = isOutdoor ? 1 : 0;
}
}
else if (aiTarget.Entity is IDamageable targetDamageable && targetDamageable.Health <= 0.0f)
@@ -1783,7 +1869,7 @@ namespace Barotrauma
// -> just ignore the distance and attack whatever has the highest priority
dist = Math.Max(dist, 100.0f);
AITargetMemory targetMemory = GetTargetMemory(aiTarget);
AITargetMemory targetMemory = GetTargetMemory(aiTarget, true);
if (Character.CurrentHull != null && Math.Abs(toTarget.Y) > Character.CurrentHull.Size.Y)
{
// Inside the sub, treat objects that are up or down, as they were farther away.
@@ -1793,9 +1879,18 @@ namespace Barotrauma
if (valueModifier > targetValue)
{
// Don't target items that we own.
// This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive)
if (aiTarget.Entity is Item i && i.IsOwnedBy(character)) { continue; }
if (aiTarget.Entity is Item i)
{
Character owner = GetOwner(i);
// Don't target items that we own.
// This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive)
if (owner == character) { continue; }
if (owner != null && IsFriendly(Character, owner))
{
// If the item is held by a friendly character, ignore it.
continue;
}
}
if (targetCharacter != null)
{
if (targetCharacter.Submarine != Character.Submarine)
@@ -1820,7 +1915,7 @@ namespace Barotrauma
foreach (var gap in Character.CurrentHull.ConnectedGaps)
{
var door = gap.ConnectedDoor;
if (door == null || !door.IsOpen)
if (door == null || !door.IsOpen && !door.IsBroken)
{
var wall = gap.ConnectedWall;
if (wall != null)
@@ -1855,12 +1950,15 @@ namespace Barotrauma
return SelectedAiTarget;
}
private AITargetMemory GetTargetMemory(AITarget target)
private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound)
{
if (!targetMemories.TryGetValue(target, out AITargetMemory memory))
{
memory = new AITargetMemory(target, 10);
targetMemories.Add(target, memory);
if (addIfNotFound)
{
memory = new AITargetMemory(target, 10);
targetMemories.Add(target, memory);
}
}
return memory;
}
@@ -1875,8 +1973,11 @@ namespace Barotrauma
}
else if (CanPerceive(_selectedAiTarget, distSquared: Vector2.DistanceSquared(Character.WorldPosition, _selectedAiTarget.WorldPosition)))
{
var memory = GetTargetMemory(_selectedAiTarget);
memory.Location = _selectedAiTarget.WorldPosition;
var memory = GetTargetMemory(_selectedAiTarget, false);
if (memory != null)
{
memory.Location = _selectedAiTarget.WorldPosition;
}
}
}
}
@@ -2014,11 +2115,17 @@ namespace Barotrauma
{
// If the target is shooting from the submarine, we might not perceive it because it doesn't move.
// --> Target the submarine too.
if (target.Submarine != null && canAttackSub)
if (target.Submarine != null && (canAttackDoors || canAttackWalls))
{
ChangeParams("room", state, priority);
ChangeParams("wall", state, priority);
ChangeParams("door", state, priority);
if (canAttackWalls)
{
ChangeParams("wall", state, priority);
}
if (canAttackDoors)
{
ChangeParams("door", state, priority);
}
}
ChangeParams("provocative", state, priority, onlyExisting: true);
ChangeParams("light", state, priority, onlyExisting: true);
@@ -2039,7 +2146,7 @@ namespace Barotrauma
Character.AnimController.ReleaseStuckLimbs();
escapeTarget = null;
AttackingLimb = null;
escapeMargin = 0;
movementMargin = 0;
allGapsSearched = false;
unreachableGaps.Clear();
if (isStateChanged && to == AIState.Idle && from != to)
@@ -2064,28 +2171,66 @@ namespace Barotrauma
}
}
public void ReevaluateAttacks()
{
canAttackWalls = LatchOntoAI != null && LatchOntoAI.AttachToSub;
canAttackDoors = false;
canAttackCharacters = false;
foreach (var limb in Character.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.attack == null) { continue; }
if (!canAttackWalls)
{
canAttackWalls = limb.attack.IsValidTarget(AttackTarget.Structure) && limb.attack.StructureDamage > 0;
}
if (!canAttackDoors)
{
canAttackDoors = limb.attack.IsValidTarget(AttackTarget.Structure) && limb.attack.ItemDamage > 0;
}
if (!canAttackCharacters)
{
canAttackCharacters = limb.attack.IsValidTarget(AttackTarget.Character);
}
}
if (PathSteering != null)
{
PathSteering.CanBreakDoors = canAttackDoors;
}
}
private Vector2 returnDir;
private float returnTimer;
private void SteerInsideLevel(float deltaTime)
{
if (Level.Loaded == null) { return; }
Vector2 levelSimSize = new Vector2(
ConvertUnits.ToSimUnits(Level.Loaded.Size.X),
ConvertUnits.ToSimUnits(Level.Loaded.Size.Y));
float margin = 10.0f;
if (SimPosition.Y < 0.0)
if (SteeringManager is IndoorsSteeringManager || !StayInsideLevel) { return; }
if (Level.Loaded == null) { return; }
Vector2 levelSimSize = ConvertUnits.ToSimUnits(Level.Loaded.Size.X, Level.Loaded.Size.Y);
float returnTime = 3;
if (SimPosition.Y < 0)
{
steeringManager.SteeringManual(deltaTime, Vector2.UnitY * MathUtils.InverseLerp(0.0f, -margin, SimPosition.Y));
// Too far down
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
returnDir = Vector2.UnitY;
}
if (SimPosition.X < 0.0f)
if (SimPosition.X < 0)
{
steeringManager.SteeringManual(deltaTime, Vector2.UnitX * MathUtils.InverseLerp(0.0f, -margin, SimPosition.X));
// Too far left
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
returnDir = Vector2.UnitX;
}
if (SimPosition.X > levelSimSize.X)
{
steeringManager.SteeringManual(deltaTime, Vector2.UnitX * MathUtils.InverseLerp(levelSimSize.X, levelSimSize.X + margin, SimPosition.X));
}
// Too far right
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
returnDir = -Vector2.UnitX;
}
if (returnTimer > 0)
{
returnTimer -= deltaTime;
SteeringManager.Reset();
SteeringManager.SteeringManual(deltaTime, returnDir);
}
}
private bool CanPassThroughHole(Structure wall, int sectionIndex)
@@ -2116,7 +2261,6 @@ namespace Barotrauma
targetLimbs.Clear();
foreach (var limb in target.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.type == targetLimbType || targetLimbType == LimbType.None)
{
targetLimbs.Add(limb);
@@ -2131,6 +2275,7 @@ namespace Barotrauma
Limb targetLimb = null;
foreach (Limb limb in targetLimbs)
{
if (limb.IsSevered) { continue; }
float dist = Vector2.DistanceSquared(limb.WorldPosition, attackLimb.WorldPosition) / Math.Max(limb.AttackPriority, 0.1f);
if (dist < closestDist)
{
@@ -2140,6 +2285,27 @@ namespace Barotrauma
}
return targetLimb;
}
private Character GetOwner(Item item)
{
// If the item is held by a character, attack the character instead.
var pickable = item.GetComponent<Pickable>();
if (pickable != null)
{
Character owner = pickable.Picker ?? item.FindParentInventory(i => i.Owner is Character)?.Owner as Character;
if (owner != null)
{
var target = owner.AiTarget;
if (target?.Entity != null && !target.Entity.Removed)
{
return owner;
}
}
}
return null;
}
public static bool IsFriendly(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
}
//the "memory" of the Character
@@ -30,8 +30,9 @@ namespace Barotrauma
public static float HULL_SAFETY_THRESHOLD = 50;
public HashSet<Hull> UnreachableHulls { get; private set; } = new HashSet<Hull>();
public HashSet<Hull> UnsafeHulls { get; private set; } = new HashSet<Hull>();
public readonly HashSet<Hull> UnreachableHulls = new HashSet<Hull>();
public readonly HashSet<Hull> UnsafeHulls = new HashSet<Hull>();
public readonly List<Item> IgnoredItems = new List<Item>();
private SteeringManager outsideSteering, insideSteering;
@@ -55,13 +56,13 @@ namespace Barotrauma
private set;
}
public float CurrentHullSafety { get; private set; }
public float CurrentHullSafety { get; private set; } = 100;
public HumanAIController(Character c) : base(c)
{
if (!c.IsHuman)
{
throw new System.Exception($"Tried to create a human ai controller for a non-human: {c.SpeciesName}!");
throw new Exception($"Tried to create a human ai controller for a non-human: {c.SpeciesName}!");
}
insideSteering = new IndoorsSteeringManager(this, true, false);
outsideSteering = new SteeringManager(this);
@@ -85,7 +86,7 @@ namespace Barotrauma
{
unreachableClearTimer = clearUnreachableInterval;
UnreachableHulls.Clear();
ignoredContainers.Clear();
IgnoredItems.Clear();
}
// Use the pathfinding also outside of the sub, but not farther than the extents of the sub + 500 units.
@@ -175,9 +176,7 @@ namespace Barotrauma
}
steeringManager.Update(Character.AnimController.GetCurrentSpeed(run && Character.CanRun));
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f &&
(-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
if (steeringManager == insideSteering)
{
var currPath = PathSteering.CurrentPath;
@@ -185,51 +184,24 @@ namespace Barotrauma
{
if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y)
{
// Don't allow to jump from too high. The formula might require tweaking.
// Don't allow to jump from too high.
float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2;
float height = Math.Abs(currPath.CurrentNode.SimPosition.Y - Character.SimPosition.Y);
ignorePlatforms = height < allowedJumpHeight;
}
}
if (Character.IsClimbing && PathSteering.IsNextLadderSameAsCurrent)
{
Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y));
}
}
Character.AnimController.IgnorePlatforms = ignorePlatforms;
Vector2 targetMovement = AnimController.TargetMovement;
if (!Character.AnimController.InWater)
{
targetMovement = new Vector2(Character.AnimController.TargetMovement.X, MathHelper.Clamp(Character.AnimController.TargetMovement.Y, -1.0f, 1.0f));
}
if (Character.AnimController.InWater && targetMovement.LengthSquared() < 0.000001f)
{
bool isAiming = false;
var holdable = Character.SelectedConstruction?.GetComponent<Holdable>();
if (holdable != null)
{
isAiming = holdable.ControlPose;
}
bool swimInPlace = !isAiming;
if (swimInPlace && ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo goToObjective)
{
if (goToObjective.Target != Character)
{
swimInPlace = false;
}
}
if (swimInPlace)
{
// Swim in place so that we don't fall motionless and look dead.
targetMovement = new Vector2(targetMovement.X, Rand.Range(-0.001f, 0.001f));
}
}
Character.AnimController.TargetMovement = Character.ApplyMovementLimits(targetMovement, AnimController.GetCurrentSpeed(run));
flipTimer -= deltaTime;
@@ -280,14 +252,14 @@ namespace Barotrauma
else
{
findItemState = FindItemState.Extinguisher;
if (FindSuitableContainer(Character, extinguisher, out Item targetContainer))
if (FindSuitableContainer(extinguisher, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, extinguisher, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer);
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
@@ -310,42 +282,47 @@ namespace Barotrauma
|| ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>()
|| ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn);
bool removeDivingSuit = !Character.AnimController.HeadInWater && oxygenLow;
AIObjectiveGoTo gotoObjective = ObjectiveManager.GetActiveObjective<AIObjectiveGoTo>();
bool takeMaskOff = !Character.AnimController.HeadInWater && oxygenLow;
if (!removeDivingSuit)
{
bool targetHasNoSuit = gotoObjective != null && gotoObjective.mimic && !HasDivingSuit(gotoObjective.Target as Character);
removeDivingSuit = !shouldKeepTheGearOn && (gotoObjective == null || targetHasNoSuit);
}
bool takeMaskOff = !Character.AnimController.HeadInWater && oxygenLow;
if (!takeMaskOff && Character.CurrentHull.WaterPercentage < 40)
{
bool targetHasNoMask = gotoObjective != null && gotoObjective.mimic && !HasDivingMask(gotoObjective.Target as Character);
takeMaskOff = !shouldKeepTheGearOn && (gotoObjective == null || targetHasNoMask);
}
if (gotoObjective != null)
{
if (gotoObjective.Target is Hull h)
if (shouldKeepTheGearOn)
{
if (NeedsDivingGear(Character, h, out _))
{
removeDivingSuit = false;
takeMaskOff = false;
}
removeDivingSuit = false;
}
else if (gotoObjective.Target is Character c)
}
if (!takeMaskOff)
{
if (shouldKeepTheGearOn)
{
if (NeedsDivingGear(Character, c.CurrentHull, out _))
{
removeDivingSuit = false;
takeMaskOff = false;
}
takeMaskOff = false;
}
else if (gotoObjective.Target is Item i)
}
if (!shouldKeepTheGearOn && (!takeMaskOff || !removeDivingSuit))
{
foreach (var objective in ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(includingSelf: true))
{
if (NeedsDivingGear(Character, i.CurrentHull, out _))
if (objective is AIObjectiveGoTo gotoObjective)
{
removeDivingSuit = false;
takeMaskOff = false;
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
Hull targetHull = gotoObjective.GetTargetHull();
bool targetIsOutside = (gotoObjective.Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes);
if (targetIsOutside || NeedsDivingGear(Character, targetHull, out _))
{
removeDivingSuit = false;
takeMaskOff = false;
break;
}
else if (gotoObjective.mimic)
{
if (!removeDivingSuit)
{
removeDivingSuit = !HasDivingSuit(gotoObjective.Target as Character);
}
if (!takeMaskOff)
{
takeMaskOff = !HasDivingMask(gotoObjective.Target as Character);
}
}
}
}
}
@@ -363,7 +340,7 @@ namespace Barotrauma
else
{
findItemState = FindItemState.DivingSuit;
if (FindSuitableContainer(Character, divingSuit, out Item targetContainer))
if (FindSuitableContainer(divingSuit, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
@@ -375,7 +352,7 @@ namespace Barotrauma
};
decontainObjective.Abandoned += () =>
{
ignoredContainers.Add(targetContainer);
IgnoredItems.Add(targetContainer);
};
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
@@ -405,14 +382,14 @@ namespace Barotrauma
else
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(Character, mask, out Item targetContainer))
if (FindSuitableContainer(mask, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer);
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
@@ -442,14 +419,14 @@ namespace Barotrauma
{
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
if (FindSuitableContainer(Character, item, out Item targetContainer))
if (FindSuitableContainer(item, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer);
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
@@ -478,11 +455,10 @@ namespace Barotrauma
}
private FindItemState findItemState;
private int itemIndex;
private List<Item> ignoredContainers = new List<Item>();
public bool FindSuitableContainer(Character character, Item containableItem, out Item suitableContainer)
public bool FindSuitableContainer(Item containableItem, out Item suitableContainer)
{
suitableContainer = null;
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredContainers, customPriorityFunction: i =>
if (Character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: IgnoredItems, customPriorityFunction: i =>
{
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
@@ -583,7 +559,7 @@ namespace Barotrauma
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, Character))
{
if (item.Repairables.All(r => item.ConditionPercentage > r.AIRepairThreshold)) { continue; }
if (item.Repairables.All(r => item.ConditionPercentage > r.RepairThreshold)) { continue; }
if (AddTargets<AIObjectiveRepairItems, Item>(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRepairItem>())
{
var orderPrefab = Order.GetPrefab("reportbrokendevices");
@@ -633,9 +609,13 @@ namespace Barotrauma
if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; }
if (attacker == null || attacker.IsDead || attacker.Removed)
{
// Don't react on the damage if there's no attacker.
// We might consider launching the retreat combat objective in some cases, so that the bot does not just stand somewhere getting damaged and dying.
// But fires and enemies should already be handled by the FindSafetyObjective.
return;
// Ignore damage from falling etc that we shouldn't react to.
if (Character.LastDamageSource == null) { return; }
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
//if (Character.LastDamageSource == null) { return; }
//AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
}
else if (IsFriendly(attacker))
{
@@ -784,7 +764,6 @@ namespace Barotrauma
return false;
}
public static bool HasDivingGear(Character character, float conditionPercentage = 0) => HasDivingSuit(character, conditionPercentage) || HasDivingMask(character, conditionPercentage);
/// <summary>
@@ -852,7 +831,7 @@ namespace Barotrauma
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, character))
{
if (item.Repairables.All(r => item.ConditionPercentage >= r.AIRepairThreshold)) { continue; }
if (item.Repairables.All(r => item.ConditionPercentage >= r.RepairThreshold)) { continue; }
AddTargets<AIObjectiveRepairItems, Item>(character, item);
}
}
@@ -12,7 +12,8 @@ namespace Barotrauma
private PathFinder pathFinder;
private SteeringPath currentPath;
private bool canOpenDoors, canBreakDoors;
private bool canOpenDoors;
public bool CanBreakDoors { get; set; }
private Character character;
@@ -50,8 +51,8 @@ namespace Barotrauma
/// </summary>
public bool InLadders =>
currentPath != null &&
currentPath.CurrentNode != null && (currentPath.CurrentNode.Ladders != null ||
(currentPath.NextNode != null && currentPath.NextNode.Ladders != null));
currentPath.CurrentNode != null && (currentPath.CurrentNode.Ladders != null && !currentPath.CurrentNode.Ladders.Item.NonInteractable ||
(currentPath.NextNode != null && currentPath.NextNode.Ladders != null && !currentPath.NextNode.Ladders.Item.NonInteractable));
/// <summary>
/// Returns true if any node in the path is in stairs
@@ -69,6 +70,7 @@ namespace Barotrauma
if (currentPath.NextNode == null) { return false; }
var currentLadder = currentPath.CurrentNode.Ladders;
if (currentLadder == null) { return false; }
if (currentLadder.Item.NonInteractable) { return false; }
var nextLadder = GetNextLadder();
return nextLadder != null && nextLadder == currentLadder;
}
@@ -80,7 +82,7 @@ namespace Barotrauma
pathFinder.GetNodePenalty = GetNodePenalty;
this.canOpenDoors = canOpenDoors;
this.canBreakDoors = canBreakDoors;
this.CanBreakDoors = canBreakDoors;
character = (host as AIController).Character;
@@ -103,6 +105,12 @@ namespace Barotrauma
IsPathDirty = false;
}
public void ResetPath()
{
currentPath = null;
IsPathDirty = true;
}
public void SteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
{
steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter);
@@ -115,7 +123,7 @@ namespace Barotrauma
{
if (currentPath == null) { return null; }
if (currentPath.NextNode == null) { return null; }
if (currentPath.NextNode.Ladders != null)
if (currentPath.NextNode.Ladders != null && !currentPath.NextNode.Ladders.Item.NonInteractable)
{
return currentPath.NextNode.Ladders;
}
@@ -126,7 +134,10 @@ namespace Barotrauma
{
var node = currentPath.Nodes[index];
if (node == null) { return null; }
return node.Ladders;
if (node.Ladders != null && !node.Ladders.Item.NonInteractable)
{
return node.Ladders;
}
}
return null;
}
@@ -134,7 +145,19 @@ namespace Barotrauma
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
{
bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || currentPath.Finished || Vector2.DistanceSquared(target, currentTarget) > 1);
Vector2 targetDiff = target - currentTarget;
if (currentPath != null && currentPath.Nodes.Any())
{
//current path calculated relative to a different sub than where the character is now
//take that into account when calculating if the target has moved
Submarine currentPathSub = currentPath?.Nodes.First().Submarine;
if (currentPathSub != character.Submarine && character.Submarine != null)
{
Vector2 subDiff = character.Submarine.SimPosition - currentPathSub.SimPosition;
targetDiff += subDiff;
}
}
bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || currentPath.Finished || targetDiff.LengthSquared() > 1);
//find a new path if one hasn't been found yet or the target is different from the current target
if (needsNewPath || findPathTimer < -1.0f)
{
@@ -172,12 +195,13 @@ namespace Barotrauma
Vector2 diff = DiffToCurrentNode();
var collider = character.AnimController.Collider;
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController;
//if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically
if (!character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius)
if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius)
{
diff.Y = 0.0f;
}
//if (diff.LengthSquared() < 0.001f) { return -host.Steering; }
if (diff == Vector2.Zero) { return Vector2.Zero; }
return Vector2.Normalize(diff) * weight;
}
@@ -186,8 +210,10 @@ namespace Barotrauma
private Vector2 DiffToCurrentNode()
{
if (currentPath == null || currentPath.Unreachable) return Vector2.Zero;
if (currentPath == null || currentPath.Unreachable)
{
return Vector2.Zero;
}
if (currentPath.Finished)
{
Vector2 pos2 = host.SimPosition;
@@ -197,15 +223,12 @@ namespace Barotrauma
pos2 -= CurrentPath.Nodes.Last().Submarine.SimPosition;
}
return currentTarget - pos2;
}
}
if (canOpenDoors && !character.LockHands && buttonPressCooldown <= 0.0f)
{
CheckDoorsInPath();
}
}
Vector2 pos = host.SimPosition;
if (character != null && currentPath.CurrentNode != null)
{
if (CurrentPath.CurrentNode.Submarine != null)
@@ -220,19 +243,17 @@ namespace Barotrauma
}
}
}
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
//only humanoids can climb ladders
if (!isDiving && character.AnimController is HumanoidAnimController && IsNextLadderSameAsCurrent)
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController;
if (canClimb && !isDiving && IsNextLadderSameAsCurrent)
{
if (character.SelectedConstruction != currentPath.CurrentNode.Ladders.Item &&
currentPath.CurrentNode.Ladders.Item.IsInsideTrigger(character.WorldPosition))
var ladders = currentPath.CurrentNode.Ladders;
if (character.SelectedConstruction != ladders.Item && ladders.Item.IsInsideTrigger(character.WorldPosition))
{
currentPath.CurrentNode.Ladders.Item.TryInteract(character, false, true);
}
}
}
var collider = character.AnimController.Collider;
if (character.IsClimbing && !isDiving)
{
@@ -252,13 +273,16 @@ namespace Barotrauma
{
diff.Y = Math.Max(diff.Y, 1.0f);
}
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
float margin = 0.1f;
bool isAboveFloor = heightFromFloor > -margin && heightFromFloor < collider.height * 1.5f;
// If the next waypoint is horizontally far, we don't want to keep holding the ladders
if (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50)
if (isAboveFloor && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50))
{
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedConstruction = null;
}
else if (!nextLadderSameAsCurrent)
else if (nextLadder != null && !nextLadderSameAsCurrent)
{
// Try to change the ladder (hatches between two submarines)
if (character.SelectedConstruction != nextLadder.Item && nextLadder.Item.IsInsideTrigger(character.WorldPosition))
@@ -266,9 +290,6 @@ namespace Barotrauma
nextLadder.Item.TryInteract(character, false, true);
}
}
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
float margin = 0.1f;
bool isAboveFloor = heightFromFloor > -margin && heightFromFloor < collider.height * 1.5f;
if (nextLadder != null || isAboveFloor)
{
currentPath.SkipToNextNode();
@@ -286,7 +307,7 @@ namespace Barotrauma
}
return diff;
}
else if (character.AnimController.InWater)
else if (!canClimb || character.AnimController.InWater)
{
// If the character is underwater, we don't need the ladders anymore
if (character.IsClimbing && isDiving)
@@ -294,49 +315,59 @@ namespace Barotrauma
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedConstruction = null;
}
float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
float targetDistance = collider.GetSize().X * multiplier;
float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
if (character.CurrentHull != currentPath.CurrentNode.CurrentHull)
var door = currentPath.CurrentNode.ConnectedDoor;
bool blockedByDoor = door != null && !door.IsOpen && !door.IsBroken;
if (!blockedByDoor)
{
verticalDistance *= 2;
}
float distance = horizontalDistance + verticalDistance;
if (ConvertUnits.ToSimUnits(distance) < targetDistance)
{
currentPath.SkipToNextNode();
float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
float targetDistance = collider.GetSize().X * multiplier;
float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
if (character.CurrentHull != currentPath.CurrentNode.CurrentHull)
{
verticalDistance *= 2;
}
float distance = horizontalDistance + verticalDistance;
if (ConvertUnits.ToSimUnits(distance) < targetDistance)
{
currentPath.SkipToNextNode();
}
}
}
else if (!IsNextLadderSameAsCurrent)
{
// Walking horizontally
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
Vector2 colliderSize = collider.GetSize();
Vector2 velocity = collider.LinearVelocity;
// If the character is smaller than this, it fails to use the waypoint nodes, because they are always too high.
// If the character is smaller than this, it would fail to use the waypoint nodes because they are always too high.
float minHeight = 1;
// Cannot use the head position, because not all characters have head or it can be below the total height of the character
float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight);
float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X);
bool isAboveFeet = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y;
bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + characterHeight;
var door = currentPath.CurrentNode.ConnectedDoor;
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;
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh)
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && !blockedByDoor)
{
currentPath.SkipToNextNode();
}
}
if (currentPath.CurrentNode == null) return Vector2.Zero;
if (currentPath.CurrentNode == null)
{
return Vector2.Zero;
}
return currentPath.CurrentNode.SimPosition - pos;
}
private bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
{
if (door.IsOpen) { return true; }
if (canBreakDoors) { return true; }
if (door.Item.NonInteractable) { return false; }
if (CanBreakDoors) { return true; }
if (door.IsStuck) { return false; }
if (!canOpenDoors || character.LockHands) { return false; }
if (door.HasIntegratedButtons)
@@ -345,7 +376,7 @@ namespace Barotrauma
}
else
{
return door.Item.GetConnectedComponents<Controller>(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b)));
return door.Item.GetConnectedComponents<Controller>(true).Any(b => !b.Item.NonInteractable && b.HasAccess(character) && (buttonFilter == null || buttonFilter(b)));
}
}
@@ -381,7 +412,11 @@ namespace Barotrauma
{
//the node we're heading towards is the last one in the path, and at a door
//the door needs to be open for the character to reach the node
shouldBeOpen = true;
if (currentWaypoint.ConnectedDoor.LinkedGap != null && currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom)
{
shouldBeOpen = true;
door = currentWaypoint.ConnectedDoor;
}
}
else
{
@@ -519,9 +554,9 @@ namespace Barotrauma
//non-humanoids can't climb up ladders
if (!(character.AnimController is HumanoidAnimController))
{
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null &&
nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y) //upper node not underwater
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && nextNode.Waypoint.Ladders.Item.NonInteractable ||
(nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y)) //upper node not underwater
{
return null;
}
@@ -539,7 +574,10 @@ namespace Barotrauma
}
if (character.NeedsAir && hull.WaterVolume / hull.Rect.Width > 100.0f)
{
penalty += 500.0f;
if (!HumanAIController.HasDivingSuit(character))
{
penalty += 500.0f;
}
}
if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume)
{
@@ -1,11 +1,11 @@
using FarseerPhysics;
using FarseerPhysics.Common;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
namespace Barotrauma
{
@@ -19,8 +19,8 @@ namespace Barotrauma
private Vector2 attachSurfaceNormal;
private Submarine attachTargetSubmarine;
private bool attachToSub;
private bool attachToWalls;
public bool AttachToSub { get; private set; }
public bool AttachToWalls { get; private set; }
private float minDeattachSpeed = 3.0f, maxDeattachSpeed = 10.0f;
private float damageOnDetach = 0.0f, detachStun = 0.0f;
@@ -58,8 +58,8 @@ namespace Barotrauma
public LatchOntoAI(XElement element, EnemyAIController enemyAI)
{
attachToWalls = element.GetAttributeBool("attachtowalls", false);
attachToSub = element.GetAttributeBool("attachtosub", false);
AttachToWalls = element.GetAttributeBool("attachtowalls", false);
AttachToSub = element.GetAttributeBool("attachtosub", false);
minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 3.0f);
maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 10.0f));
damageOnDetach = element.GetAttributeFloat("damageondetach", 0.0f);
@@ -67,11 +67,19 @@ namespace Barotrauma
localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("localattachpos", Vector2.Zero));
attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat("attachlimbrotation", 0.0f));
if (Enum.TryParse(element.GetAttributeString("attachlimb", "Head"), out LimbType attachLimbType))
string limbString = element.GetAttributeString("attachlimb", null);
attachLimb = enemyAI.Character.AnimController.Limbs.FirstOrDefault(l => string.Equals(l.Name, limbString, StringComparison.OrdinalIgnoreCase));
if (attachLimb == null)
{
attachLimb = enemyAI.Character.AnimController.GetLimb(attachLimbType);
if (Enum.TryParse(limbString, out LimbType attachLimbType))
{
attachLimb = enemyAI.Character.AnimController.GetLimb(attachLimbType);
}
}
if (attachLimb == null)
{
attachLimb = enemyAI.Character.AnimController.MainLimb;
}
if (attachLimb == null) attachLimb = enemyAI.Character.AnimController.MainLimb;
enemyAI.Character.OnDeath += OnCharacterDeath;
}
@@ -108,7 +116,9 @@ namespace Barotrauma
//something went wrong, limb body is very far from the joint anchor -> deattach
if (Vector2.DistanceSquared(attachJoints[i].WorldAnchorB, attachJoints[i].BodyA.Position) > 10.0f * 10.0f)
{
#if DEBUG
DebugConsole.ThrowError("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach");
#endif
DeattachFromBody();
return;
}
@@ -131,7 +141,7 @@ namespace Barotrauma
switch (enemyAI.State)
{
case AIState.Idle:
if (attachToWalls && character.Submarine == null && Level.Loaded != null)
if (AttachToWalls && character.Submarine == null && Level.Loaded != null)
{
if (!IsAttached)
{
@@ -180,8 +190,9 @@ namespace Barotrauma
}
else
{
float dist = Vector2.Distance(character.SimPosition, wallAttachPos);
if (dist < Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f)
float squaredDistance = Vector2.DistanceSquared(character.SimPosition, wallAttachPos);
float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f;
if (squaredDistance < targetDistance * targetDistance)
{
//close enough to a wall -> attach
AttachToBody(character.AnimController.Collider, attachLimb, attachTargetBody, wallAttachPos);
@@ -197,12 +208,13 @@ namespace Barotrauma
}
break;
case AIState.Attack:
case AIState.Aggressive:
if (enemyAI.AttackingLimb != null)
{
if (attachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && attachTargetBody != null)
if (AttachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && attachTargetBody != null)
{
// is not attached or is attached to something else
if (!IsAttached || IsAttached && attachJoints[0].BodyB == attachTargetBody)
if (!IsAttached || IsAttached && attachJoints[0].BodyB != attachTargetBody)
{
if (Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(transformedAttachPos), enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange)
{
@@ -247,16 +259,17 @@ namespace Barotrauma
if (attachJoints.Count > 0)
{
//already attached to the target body, no need to do anything
if (attachJoints[0].BodyB == targetBody) return;
if (attachJoints[0].BodyB == targetBody) { return; }
DeattachFromBody();
}
jointDir = attachLimb.Dir;
Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.Scale * attachLimb.Params.Ragdoll.LimbScale;
if (jointDir < 0.0f) transformedLocalAttachPos.X = -transformedLocalAttachPos.X;
//transformedLocalAttachPos = Vector2.Transform(transformedLocalAttachPos, Matrix.CreateRotationZ(attachLimb.Rotation));
if (jointDir < 0.0f)
{
transformedLocalAttachPos.X = -transformedLocalAttachPos.X;
}
float angle = MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2 + attachLimbRotation * attachLimb.Dir;
attachLimb.body.SetTransform(attachPos + attachSurfaceNormal * transformedLocalAttachPos.Length(), angle);
@@ -274,7 +287,10 @@ namespace Barotrauma
// Limb scale is already taken into account when creating the collider.
Vector2 colliderFront = collider.GetLocalFront();
if (jointDir < 0.0f) colliderFront.X = -colliderFront.X;
if (jointDir < 0.0f)
{
colliderFront.X = -colliderFront.X;
}
collider.SetTransform(attachPos + attachSurfaceNormal * colliderFront.Length(), MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2);
var colliderJoint = new WeldJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false)
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
@@ -375,9 +375,7 @@ namespace Barotrauma
}
}
StreamWriter file = new StreamWriter(@"NPCConversations.csv");
file.WriteLine(sb.ToString());
file.Close();
File.WriteAllText("NPCConversations.csv", sb.ToString());
}
private static void WriteConversation(System.Text.StringBuilder sb, NPCConversation conv, int depthIndex)
@@ -118,7 +118,6 @@ namespace Barotrauma
public void TryComplete(float deltaTime)
{
if (isCompleted) { return; }
//if (Abandon && !IsLoop && subObjectives.None()) { return; }
if (CheckState()) { return; }
// Not ready -> act (can't do foreach because it's possible that the collection is modified in event callbacks.
for (int i = 0; i < subObjectives.Count; i++)
@@ -201,7 +200,7 @@ namespace Barotrauma
}
else
{
Priority = CumulatedDevotion * PriorityModifier;
Priority = CumulatedDevotion;
}
return Priority;
}
@@ -211,7 +210,7 @@ namespace Barotrauma
var currentObjective = objectiveManager.CurrentObjective;
if (currentObjective != null && (currentObjective == this || currentObjective.subObjectives.Any(so => so == this)))
{
CumulatedDevotion += Devotion * PriorityModifier * deltaTime;
CumulatedDevotion += Devotion * deltaTime;
}
}
@@ -20,6 +20,7 @@ namespace Barotrauma
{
if (battery == null) { return false; }
var item = battery.Item;
if (item.NonInteractable) { return false; }
if (item.Submarine == null) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine.TeamID != character.TeamID) { return false; }
@@ -121,6 +121,12 @@ namespace Barotrauma
protected override bool Check()
{
if (initialMode == CombatMode.Offensive && Mode != CombatMode.Offensive)
{
Abandon = true;
SteeringManager.Reset();
return false;
}
bool completed = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) || (initialMode != CombatMode.Offensive && coolDownTimer <= 0);
if (completed)
{
@@ -465,7 +471,6 @@ namespace Barotrauma
SteeringManager.Reset();
return;
}
retreatTarget = null;
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref seekAmmunition);
@@ -482,9 +487,8 @@ namespace Barotrauma
},
onAbandon: () =>
{
Mode = CombatMode.Defensive;
Abandon = true;
SteeringManager.Reset();
RemoveSubObjective(ref followTargetObjective);
});
if (followTargetObjective != null)
{
@@ -593,10 +597,7 @@ namespace Barotrauma
private void Attack(float deltaTime)
{
float squaredDistance = Vector2.DistanceSquared(character.Position, Enemy.Position);
character.CursorPosition = Enemy.Position;
float engageDistance = 500;
if (character.CurrentHull != Enemy.CurrentHull && squaredDistance > engageDistance * engageDistance) { return; }
if (!character.CanSeeCharacter(Enemy)) { return; }
if (Weapon.RequireAimToUse)
{
@@ -604,7 +605,7 @@ namespace Barotrauma
if (SteeringManager == PathSteering)
{
var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
if (door != null && !door.IsOpen && !door.IsBroken)
{
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
}
@@ -626,7 +627,7 @@ namespace Barotrauma
}
if (WeaponComponent is MeleeWeapon meleeWeapon)
{
if (squaredDistance <= meleeWeapon.Range * meleeWeapon.Range)
if (Vector2.DistanceSquared(character.Position, Enemy.Position) <= meleeWeapon.Range * meleeWeapon.Range)
{
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
@@ -636,7 +637,7 @@ namespace Barotrauma
{
if (WeaponComponent is RepairTool repairTool)
{
if (squaredDistance > repairTool.Range * repairTool.Range) { return; }
if (Vector2.DistanceSquared(character.Position, Enemy.Position) > repairTool.Range * repairTool.Range) { return; }
}
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4)
{
@@ -158,7 +158,7 @@ namespace Barotrauma
Abandon = true;
}, onCompleted: () =>
{
if (getItemObjective.TargetItem != null)
if (getItemObjective?.TargetItem != null)
{
containedItems.Add(getItemObjective.TargetItem);
}
@@ -106,7 +106,7 @@ namespace Barotrauma
if (SteeringManager == PathSteering)
{
var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
if (door != null && !door.IsOpen)
if (door != null && !door.IsOpen && !door.IsBroken)
{
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
}
@@ -1,9 +1,4 @@
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using System.Collections.Generic;
namespace Barotrauma
{
@@ -43,7 +43,7 @@ namespace Barotrauma
}
if (character.CurrentHull == null)
{
Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo ? 0 : 100;
Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo && HumanAIController.HasDivingSuit(character) ? 0 : 100;
}
else
{
@@ -83,8 +83,9 @@ namespace Barotrauma
else
{
float dangerFactor = (100 - currenthullSafety) / 100;
Priority = Math.Min(Priority + dangerFactor * priorityIncrease * deltaTime, 100);
Priority += dangerFactor * priorityIncrease * deltaTime;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
}
}
@@ -93,34 +94,39 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
var currentHull = character.CurrentHull;
bool needsDivingGear = HumanAIController.NeedsDivingGear(character, currentHull, out bool needsDivingSuit);
bool needsEquipment = false;
if (needsDivingSuit)
bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0;
if (!dangerousPressure)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
if (needsEquipment && divingGearObjective == null && !character.LockHands)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref divingGearObjective,
constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () =>
{
searchHullTimer = Math.Min(1, searchHullTimer);
// Don't try to seek diving gear if the pressure is dangerous. Just get out.
bool needsDivingGear = HumanAIController.NeedsDivingGear(character, currentHull, out bool needsDivingSuit);
bool needsEquipment = false;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
}
if (needsEquipment && divingGearObjective == null && !character.LockHands)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref divingGearObjective,
constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () =>
{
searchHullTimer = Math.Min(1, searchHullTimer);
// Don't reset the diving gear objective, because it's possible that there is no diving gear -> seek a safe hull and then reset so that we can check again.
},
onCompleted: () =>
{
resetPriority = true;
searchHullTimer = Math.Min(1, searchHullTimer);
RemoveSubObjective(ref divingGearObjective);
});
onCompleted: () =>
{
resetPriority = true;
searchHullTimer = Math.Min(1, searchHullTimer);
RemoveSubObjective(ref divingGearObjective);
});
}
}
else if (divingGearObjective == null || !divingGearObjective.CanBeCompleted)
if (divingGearObjective == null || !divingGearObjective.CanBeCompleted)
{
if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
{
@@ -37,7 +37,7 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>(), onlyBots: true);
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated, onlyBots: true);
int totalLeaks = Targets.Count();
if (totalLeaks == 0) { return 0; }
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
@@ -3,6 +3,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -20,7 +21,9 @@ namespace Barotrauma
//can be either tags or identifiers
private string[] itemIdentifiers;
public IEnumerable<string> Identifiers => itemIdentifiers;
private Item targetItem, moveToTarget, rootContainer;
private Item targetItem;
private ISpatialEntity moveToTarget;
private bool isDoneSeeking;
public Item TargetItem => targetItem;
private int currSearchIndex;
@@ -29,6 +32,8 @@ namespace Barotrauma
private float currItemPriority;
private bool checkInventory;
public static float DefaultReach = 100;
public bool AllowToFindDivingGear { get; set; } = true;
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
@@ -37,6 +42,7 @@ namespace Barotrauma
currSearchIndex = -1;
this.equip = equip;
this.targetItem = targetItem;
moveToTarget = targetItem?.GetRootInventoryOwner();
}
public AIObjectiveGetItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1)
@@ -62,8 +68,7 @@ namespace Barotrauma
if (item != null)
{
targetItem = item;
rootContainer = item.GetRootContainer();
moveToTarget = rootContainer ?? item;
moveToTarget = item.GetRootInventoryOwner();
}
return item != null;
}
@@ -86,6 +91,15 @@ namespace Barotrauma
}
if (!isDoneSeeking)
{
bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0;
if (dangerousPressure)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Seeking item aborted, because the pressure is dangerous.", Color.Yellow);
#endif
Abandon = true;
return;
}
FindTargetItem();
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
return;
@@ -108,7 +122,26 @@ namespace Barotrauma
Reset();
return;
}
if (character.CanInteractWith(targetItem, out _, checkLinked: false))
bool canInteract = false;
if (moveToTarget is Character c)
{
if (character == c)
{
canInteract = true;
moveToTarget = null;
}
else
{
character.SelectCharacter(c);
canInteract = character.CanInteractWith(c, maxDist: DefaultReach);
character.DeselectCharacter();
}
}
else if (moveToTarget is Item parentItem)
{
canInteract = character.CanInteractWith(parentItem, out _, checkLinked: false);
}
if (canInteract)
{
var pickable = targetItem.GetComponent<Pickable>();
if (pickable == null)
@@ -173,17 +206,17 @@ namespace Barotrauma
}
}
}
else
else if (moveToTarget != null)
{
TryAddSubObjective(ref goToObjective,
constructor: () =>
{
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear)
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear, closeEnough: DefaultReach)
{
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
abortCondition = () => targetItem == null || targetItem.GetRootContainer() != rootContainer,
abortCondition = () => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = moveToTarget.Name
TargetName = (moveToTarget as MapEntity)?.Name ?? (moveToTarget as Character)?.Name ?? moveToTarget.ToString()
};
},
onAbandon: () =>
@@ -212,9 +245,9 @@ namespace Barotrauma
{
currSearchIndex++;
var item = Item.ItemList[currSearchIndex];
if (item.Submarine == null) { continue; }
if (item.CurrentHull == null) { continue; }
if (item.Submarine.TeamID != character.TeamID) { continue; }
Submarine itemSub = item.Submarine ?? item.ParentInventory?.Owner?.Submarine;
if (itemSub == null) { continue; }
if (itemSub.TeamID != character.TeamID) { continue; }
if (!CheckItem(item)) { continue; }
if (ignoredContainerIdentifiers != null && item.Container != null)
{
@@ -222,8 +255,8 @@ namespace Barotrauma
}
if (character.Submarine != null)
{
if (item.Submarine.Info.Type != character.Submarine.Info.Type) { continue; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
if (itemSub.Info.Type != character.Submarine.Info.Type) { continue; }
if (character.Submarine.GetConnectedSubs().None(s => s == itemSub && itemSub.TeamID == character.TeamID && itemSub.Info.Type == character.Submarine.Info.Type)) { continue; }
}
if (character.IsItemTakenBySomeoneElse(item)) { continue; }
float itemPriority = 1;
@@ -231,8 +264,8 @@ namespace Barotrauma
{
itemPriority = GetItemPriority(item);
}
Item rootContainer = item.GetRootContainer();
Vector2 itemPos = (rootContainer ?? item).WorldPosition;
Entity rootInventoryOwner = item.GetRootInventoryOwner();
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - itemPos.X) + yDist;
@@ -243,8 +276,7 @@ namespace Barotrauma
if (itemPriority < currItemPriority) { continue; }
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootContainer ?? item;
this.rootContainer = rootContainer;
moveToTarget = rootInventoryOwner ?? item;
}
if (currSearchIndex >= Item.ItemList.Count - 1)
{
@@ -293,7 +325,6 @@ namespace Barotrauma
RemoveSubObjective(ref goToObjective);
targetItem = null;
moveToTarget = null;
rootContainer = null;
isDoneSeeking = false;
currSearchIndex = 0;
}
@@ -80,21 +80,28 @@ namespace Barotrauma
this.repeat = repeat;
waitUntilPathUnreachable = 3.0f;
this.getDivingGearIfNeeded = getDivingGearIfNeeded;
CloseEnough = closeEnough;
if (Target is Item i)
{
CloseEnough = Math.Max(CloseEnough, i.InteractDistance + Math.Max(i.Rect.Width, i.Rect.Height) / 2);
}
else if (Target is Character)
{
CloseEnough = Math.Max(closeEnough, AIObjectiveGetItem.DefaultReach);
}
else
{
CloseEnough = closeEnough;
}
}
private void SpeakCannotReach()
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target.ToString()}", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow);
#endif
if (objectiveManager.CurrentOrder != null && DialogueIdentifier != null)
{
string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, true);
string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character));
if (msg != null)
{
character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f);
@@ -213,10 +220,19 @@ namespace Barotrauma
return;
}
}
if (repeat && IsCloseEnough)
if (repeat)
{
OnCompleted();
return;
if (IsCloseEnough)
{
if (requiredCondition == null || requiredCondition())
{
if (character.CanSeeTarget(Target))
{
OnCompleted();
return;
}
}
}
}
if (SteeringManager == PathSteering)
{
@@ -244,7 +260,7 @@ namespace Barotrauma
}
}
private Hull GetTargetHull()
public Hull GetTargetHull()
{
if (Target is Hull h)
{
@@ -284,13 +300,7 @@ namespace Barotrauma
//otherwise characters can let go of the ladders too soon once they're close enough to the target
if (PathSteering.CurrentPath.NextNode != null) { return false; }
}
bool closeEnough = Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough;
if (closeEnough)
{
closeEnough = !(Target is Character) || Target is Character c && c.CurrentHull == character.CurrentHull;
}
return closeEnough;
return Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough;
}
}
@@ -326,7 +336,9 @@ namespace Barotrauma
}
else if (Target is Character targetCharacter)
{
if (character.CanInteractWith(targetCharacter, CloseEnough)) { IsCompleted = true; }
character.SelectCharacter(targetCharacter);
if (character.CanInteractWith(targetCharacter, skipDistanceCheck: true)) { IsCompleted = true; }
character.DeselectCharacter();
}
else
{
@@ -338,6 +350,16 @@ namespace Barotrauma
return IsCompleted;
}
protected override void OnAbandon()
{
StopMovement();
if (SteeringManager == PathSteering)
{
PathSteering.ResetPath();
}
base.OnAbandon();
}
private void StopMovement()
{
character.AIController.SteeringManager.Reset();
@@ -3,7 +3,6 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -35,41 +34,43 @@ namespace Barotrauma
{
standStillTimer = Rand.Range(-10.0f, 10.0f);
walkDuration = Rand.Range(0.0f, 10.0f);
CalculatePriority();
}
protected override bool Check() => false;
public override bool CanBeCompleted => true;
public override bool IsLoop { get => true; set => throw new System.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: " + Environment.StackTrace); }
private float randomTimer;
private float randomUpdateInterval = 5;
public float Random { get; private set; }
public void CalculatePriority()
public void CalculatePriority(float max = 0)
{
Random = Rand.Range(0.5f, 1.5f);
randomTimer = randomUpdateInterval;
float max = Math.Min(Math.Min(AIObjectiveManager.RunPriority, AIObjectiveManager.OrderPriority) - 1, 100);
float initiative = character.GetSkillLevel("initiative");
Priority = MathHelper.Lerp(1, max, MathUtils.InverseLerp(100, 0, initiative * Random));
//Random = Rand.Range(0.5f, 1.5f);
//randomTimer = randomUpdateInterval;
//max = max > 0 ? max : Math.Min(Math.Min(AIObjectiveManager.RunPriority, AIObjectiveManager.OrderPriority) - 1, 100);
//float initiative = character.GetSkillLevel("initiative");
//Priority = MathHelper.Lerp(1, max, MathUtils.InverseLerp(100, 0, initiative * Random));
Priority = 1;
}
public override float GetPriority() => Priority;
public override void Update(float deltaTime)
{
if (objectiveManager.CurrentObjective == this)
{
if (randomTimer > 0)
{
randomTimer -= deltaTime;
}
else
{
CalculatePriority();
}
}
//if (objectiveManager.CurrentObjective == this)
//{
// if (randomTimer > 0)
// {
// randomTimer -= deltaTime;
// }
// else
// {
// CalculatePriority();
// }
//}
}
protected override void Act(float deltaTime)
@@ -129,7 +130,7 @@ namespace Barotrauma
//choose a random available hull
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
bool isCurrentHullAllowed = !IsForbidden(character.CurrentHull);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, nodeFilter: node =>
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: $"AIObjectiveIdle {character.DisplayName}", nodeFilter: node =>
{
if (node.Waypoint.CurrentHull == null) { return false; }
// Check that there is no unsafe or forbidden hulls on the way to the target
@@ -47,7 +47,7 @@ namespace Barotrauma
public override bool AllowSubObjectiveSorting => true;
public virtual bool InverseTargetEvaluation => false;
public override bool IsLoop { get => true; set => throw new System.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); }
public override void Update(float deltaTime)
{
@@ -204,7 +204,7 @@ namespace Barotrauma
{
Objectives.Remove(target);
ignoreList.Add(target);
targetUpdateTimer = 0;
targetUpdateTimer = Math.Min(0.1f, targetUpdateTimer);
};
}
}
@@ -14,8 +14,11 @@ namespace Barotrauma
public const float OrderPriority = 70;
public const float RunPriority = 50;
// Constantly increases the priority of the selected objective, unless overridden
public const float baseDevotion = 3;
public const float baseDevotion = 5;
/// <summary>
/// Excluding the current order.
/// </summary>
public List<AIObjective> Objectives { get; private set; } = new List<AIObjective>();
private readonly Character character;
@@ -88,8 +91,25 @@ namespace Barotrauma
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
private void ClearIgnored()
{
if (character.AIController is HumanAIController humanAi)
{
humanAi.UnreachableHulls.Clear();
humanAi.IgnoredItems.Clear();
}
}
public void CreateAutonomousObjectives()
{
if (character.IsDead)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to create autonomous orders for a dead character");
#else
return;
#endif
}
foreach (var delayedObjective in DelayedObjectives)
{
CoroutineManager.StopCoroutines(delayedObjective.Value);
@@ -99,15 +119,15 @@ namespace Barotrauma
AddObjective(new AIObjectiveFindSafety(character, this));
AddObjective(new AIObjectiveIdle(character, this));
int objectiveCount = Objectives.Count;
foreach (var automaticOrder in character.Info.Job.Prefab.AutomaticOrders)
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjective)
{
var orderPrefab = Order.GetPrefab(automaticOrder.identifier);
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{automaticOrder.identifier}'"); }
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, false)?.GetRandom() : null;
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity,
item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType), orderGiver: character);
if (order == null) { continue; }
var objective = CreateObjective(order, automaticOrder.option, character, automaticOrder.priorityModifier);
var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier);
if (objective != null && objective.CanBeCompleted)
{
AddObjective(objective, delay: Rand.Value() / 2);
@@ -160,7 +180,7 @@ namespace Barotrauma
{
previousObjective?.OnDeselected();
CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>().CalculatePriority();
GetObjective<AIObjectiveIdle>().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0));
}
return CurrentObjective;
}
@@ -172,7 +192,21 @@ namespace Barotrauma
public void UpdateObjectives(float deltaTime)
{
CurrentOrder?.Update(deltaTime);
if (CurrentOrder != null)
{
#if DEBUG
// Note: don't automatically remove orders here. Removing orders needs to be done via dismissing.
if (CurrentOrder.IsCompleted)
{
DebugConsole.NewMessage($"{character.Name}: ORDER {CurrentOrder.DebugTag} IS COMPLETED. CURRENTLY ALL ORDERS SHOULD BE LOOPING.", Color.Red);
}
else if (!CurrentOrder.CanBeCompleted)
{
DebugConsole.NewMessage($"{character.Name}: ORDER {CurrentOrder.DebugTag}, CANNOT BE COMPLETED.", Color.Red);
}
#endif
CurrentOrder.Update(deltaTime);
}
if (WaitTimer > 0)
{
WaitTimer -= deltaTime;
@@ -195,7 +229,7 @@ namespace Barotrauma
#endif
Objectives.Remove(objective);
}
else if (objective != CurrentOrder)
else
{
objective.Update(deltaTime);
}
@@ -233,7 +267,16 @@ namespace Barotrauma
public void SetOrder(Order order, string option, Character orderGiver)
{
CurrentOrder = CreateObjective(order, option, orderGiver);
if (character.IsDead)
{
#if DEBUG
DebugConsole.ThrowError("Attempted to set an order for a dead character");
#else
return;
#endif
}
ClearIgnored();
CurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false);
if (CurrentOrder == null)
{
// Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding)
@@ -245,7 +288,7 @@ namespace Barotrauma
}
}
public AIObjective CreateObjective(Order order, string option, Character orderGiver, float priorityModifier = 1)
public AIObjective CreateObjective(Order order, string option, Character orderGiver, bool isAutonomous, float priorityModifier = 1)
{
if (order == null) { return null; }
AIObjective newObjective;
@@ -284,14 +327,21 @@ namespace Barotrauma
newObjective = new AIObjectiveRepairItems(character, this, priorityModifier: priorityModifier, prioritizedItem: order.TargetEntity as Item)
{
RelevantSkill = order.AppropriateSkill,
RequireAdequateSkills = option == "jobspecific"
RequireAdequateSkills = isAutonomous
};
break;
case "pumpwater":
if (order.TargetItemComponent is Pump targetPump)
{
newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier);
// newObjective.Completed += DismissSelf;
if (order.TargetItemComponent.Item.NonInteractable) { return null; }
newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier)
{
IsLoop = true,
Override = orderGiver != null && orderGiver.IsPlayer
};
// ItemComponent.AIOperate() returns false by default -> We'd have to set IsLoop = false and implement a custom override of AIOperate for the Pump.cs,
// if we want that the bot just switches the pump on/off and continues doing something else.
// If we want that the bot does the objective and then forgets about it, I think we could do the same plus dismiss when the bot is done.
}
else
{
@@ -306,9 +356,11 @@ namespace Barotrauma
break;
case "steer":
var steering = (order?.TargetEntity as Item)?.GetComponent<Steering>();
if (steering != null) steering.PosToMaintain = steering.Item.Submarine?.WorldPosition;
if (steering != null) { steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; }
if (order.TargetItemComponent == null) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier)
if (order.TargetItemComponent.Item.NonInteractable) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option,
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
{
IsLoop = true,
// Don't override unless it's an order by a player
@@ -317,12 +369,15 @@ namespace Barotrauma
break;
default:
if (order.TargetItemComponent == null) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier)
if (order.TargetItemComponent.Item.NonInteractable) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option,
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
{
IsLoop = true,
// Don't override unless it's an order by a player
Override = orderGiver != null && orderGiver.IsPlayer
};
if (newObjective.Abandon) { return null; }
break;
}
return newObjective;
@@ -3,7 +3,6 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -29,6 +28,7 @@ namespace Barotrauma
public ItemComponent GetTarget() => useController ? controller : component;
public Func<bool> completionCondition;
private bool isDoneOperating;
public override float GetPriority()
{
@@ -47,17 +47,41 @@ namespace Barotrauma
{
Priority = AIObjectiveManager.OrderPriority;
}
Item targetItem = GetTarget()?.Item;
ItemComponent target = GetTarget();
Item targetItem = target?.Item;
if (targetItem == null)
{
#if DEBUG
DebugConsole.ThrowError("Item or component of AI Objective Operate item wass null. This shouldn't happen.");
DebugConsole.ThrowError("Item or component of AI Objective Operate item was null. This shouldn't happen.");
#endif
Abandon = true;
Priority = 0;
return 0.0f;
return Priority;
}
if (targetItem.CurrentHull == null || targetItem.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(GetTarget(), out _))
var reactor = component?.Item.GetComponent<Reactor>();
if (reactor != null)
{
switch (Option)
{
case "shutdown":
if (!reactor.PowerOn)
{
Priority = 0;
return Priority;
}
break;
case "powerup":
// Check that we don't already have another order that is targeting the same item.
// Without this the autonomous objective will tell the bot to turn the reactor on again.
if (objectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder != this && operateOrder.GetTarget() == target)
{
Priority = 0;
return Priority;
}
break;
}
}
if (targetItem.CurrentHull == null || targetItem.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(target, out _))
{
Priority = 0;
}
@@ -68,26 +92,33 @@ namespace Barotrauma
else
{
float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier);
float max = MathHelper.Min((AIObjectiveManager.OrderPriority - 1), 90);
float max = objectiveManager.CurrentOrder == this ? MathHelper.Min(AIObjectiveManager.OrderPriority, 90) : AIObjectiveManager.RunPriority - 1;
Priority = MathHelper.Clamp(value, 0, max);
}
}
return Priority;
}
public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, Entity operateTarget = null, bool useController = false, float priorityModifier = 1)
public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip,
Entity operateTarget = null, bool useController = false, ItemComponent controller = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier, option)
{
this.component = item ?? throw new System.ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target.");
component = item ?? throw new ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target.");
this.requireEquip = requireEquip;
this.operateTarget = operateTarget;
this.useController = useController;
if (useController)
if (useController) { this.controller = controller ?? component?.Item?.FindController(); }
var target = GetTarget();
if (target == null)
{
//try finding the controller with the simpler non-recursive method first
controller =
component.Item.GetConnectedComponents<Controller>().FirstOrDefault() ??
component.Item.GetConnectedComponents<Controller>(recursive: true).FirstOrDefault();
#if DEBUG
throw new Exception("target null");
#endif
Abandon = true;
}
else if (target.Item.NonInteractable)
{
Abandon = true;
}
}
@@ -122,7 +153,7 @@ namespace Barotrauma
}
if (component.AIOperate(deltaTime, character, this))
{
IsCompleted = completionCondition == null || completionCondition();
isDoneOperating = completionCondition == null || completionCondition();
}
}
else
@@ -189,12 +220,12 @@ namespace Barotrauma
}
if (component.AIOperate(deltaTime, character, this))
{
IsCompleted = completionCondition == null || completionCondition();
isDoneOperating = completionCondition == null || completionCondition();
}
}
}
}
protected override bool Check() => IsCompleted && !IsLoop;
protected override bool Check() => isDoneOperating && !IsLoop;
}
}
@@ -3,7 +3,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -27,6 +26,7 @@ namespace Barotrauma
protected override bool Filter(Pump pump)
{
if (pump == null) { return false; }
if (pump.Item.NonInteractable) { return false; }
if (pump.Item.HasTag("ballast")) { return false; }
if (pump.Item.Submarine == null) { return false; }
if (pump.Item.CurrentHull == null) { return false; }
@@ -3,7 +3,6 @@ using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
using FarseerPhysics;
namespace Barotrauma
{
@@ -52,12 +51,11 @@ namespace Barotrauma
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist));
}
float damagePriority = isPriority ? 1 : MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition);
float successFactor = isPriority ? 1 : MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character)));
float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character);
float isSelected = IsRepairing ? 50 : 0;
float devotion = (CumulatedDevotion + isSelected) / 100;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (damagePriority * distanceFactor * successFactor * PriorityModifier), 0, 1));
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
}
@@ -150,7 +148,8 @@ namespace Barotrauma
{
if (character.SelectedConstruction != Item)
{
if (!Item.TryInteract(character, true, true))
if (!Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true) &&
!Item.TryInteract(character, ignoreRequiredItems: true, forceActionKey: true))
{
Abandon = true;
}
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -56,7 +57,7 @@ namespace Barotrauma
{
Objectives.Remove(item);
ignoreList.Add(item);
targetUpdateTimer = 0;
targetUpdateTimer = Math.Min(0.1f, targetUpdateTimer);
};
}
break;
@@ -75,13 +76,9 @@ namespace Barotrauma
if (item != character.SelectedConstruction)
{
float condition = item.ConditionPercentage;
if (item.Repairables.All(r => condition >= r.AIRepairThreshold)) { return false; }
if (item.Repairables.All(r => condition >= r.RepairThreshold)) { return false; }
}
}
if (RequireAdequateSkills)
{
if (item.Repairables.Any(r => !r.HasRequiredSkills(character))) { return false; }
}
if (!string.IsNullOrWhiteSpace(RelevantSkill))
{
if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier.Equals(RelevantSkill, StringComparison.OrdinalIgnoreCase)))) { return false; }
@@ -96,7 +93,7 @@ namespace Barotrauma
// Don't stop fixing until done
return 100;
}
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>(), onlyBots: true);
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>() && !c.Character.IsIncapacitated, onlyBots: true);
int items = Targets.Count;
bool anyFixers = otherFixers > 0;
float ratio = anyFixers ? items / (float)otherFixers : 1;
@@ -111,10 +108,24 @@ namespace Barotrauma
// Enough fixers
return 0;
}
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
if (RequireAdequateSkills)
{
return Targets.Sum(t => GetTargetPriority(t, character)) * ratio;
}
else
{
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
}
}
}
public static float GetTargetPriority(Item item, Character character)
{
float damagePriority = MathHelper.Lerp(1, 0, item.Condition / item.MaxCondition);
float successFactor = MathHelper.Lerp(0, 1, item.Repairables.Average(r => r.DegreeOfSuccess(character)));
return MathHelper.Lerp(0, 100, MathHelper.Clamp(damagePriority * successFactor, 0, 1));
}
protected override IEnumerable<Item> GetList() => Item.ItemList;
protected override AIObjective ObjectiveConstructor(Item item)
@@ -126,6 +137,7 @@ namespace Barotrauma
public static bool IsValidTarget(Item item, Character character)
{
if (item == null) { return false; }
if (item.NonInteractable) { return false; }
if (item.IsFullCondition) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine == null) { return false; }
@@ -38,7 +38,19 @@ namespace Barotrauma
}
this.targetCharacter = targetCharacter;
}
protected override void OnAbandon()
{
character.SelectedCharacter = null;
base.OnAbandon();
}
protected override void OnCompleted()
{
character.SelectedCharacter = null;
base.OnCompleted();
}
protected override void Act(float deltaTime)
{
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
@@ -46,16 +58,13 @@ namespace Barotrauma
Abandon = true;
return;
}
if (targetCharacter.SelectedBy != null && targetCharacter.SelectedBy != character)
var otherRescuer = targetCharacter.SelectedBy;
if (otherRescuer != null && otherRescuer != character)
{
var otherCharacter = character.SelectedBy;
if (otherCharacter != null)
{
// Someone else is rescuing/holding the target.
Abandon = otherCharacter.IsPlayer || character.GetSkillLevel("medical") < otherCharacter.GetSkillLevel("medical");
}
// Someone else is rescuing/holding the target.
Abandon = otherRescuer.IsPlayer || character.GetSkillLevel("medical") < otherRescuer.GetSkillLevel("medical");
return;
}
if (targetCharacter != character)
{
// Incapacitated target is not in a safe place -> Move to a safe place first
@@ -161,13 +170,23 @@ namespace Barotrauma
private readonly List<string> suitableItemIdentifiers = new List<string>();
private readonly List<string> itemNameList = new List<string>();
private Dictionary<string, float> currentTreatmentSuitabilities = new Dictionary<string, float>();
private readonly Dictionary<string, float> currentTreatmentSuitabilities = new Dictionary<string, float>();
private void GiveTreatment(float deltaTime)
{
if (targetCharacter == null)
{
string errorMsg = $"{character.Name}: Attempted to update a Rescue objective with no target!";
DebugConsole.ThrowError(errorMsg);
Abandon = true;
return;
}
SteeringManager?.Reset();
if (!targetCharacter.IsPlayer)
{
// If the target is a bot, don't let it move
targetCharacter.AIController?.SteeringManager.Reset();
targetCharacter.AIController?.SteeringManager?.Reset();
}
if (treatmentTimer > 0.0f)
{
@@ -182,6 +201,8 @@ namespace Barotrauma
//check if we already have a suitable treatment for any of the afflictions
foreach (Affliction affliction in GetSortedAfflictions(targetCharacter))
{
if (affliction == null) { throw new Exception("Affliction was null"); }
if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); }
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
{
if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > 0.0f)
@@ -258,7 +279,7 @@ namespace Barotrauma
ic.PlaySound(ActionType.OnUse, character);
#endif
ic.WasUsed = true;
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb);
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: character);
if (ic.DeleteOnUse)
{
remove = true;
@@ -33,46 +33,31 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
int otherRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRescueAll>(), onlyBots: true);
int targetCount = Targets.Count;
bool anyRescuers = otherRescuers > 0;
float ratio = anyRescuers ? targetCount / (float)otherRescuers : 1;
if (objectiveManager.CurrentOrder == this)
if (objectiveManager.CurrentOrder != this)
{
return Targets.Min(t => GetVitalityFactor(t)) / ratio;
}
else
{
float multiplier = 1;
if (anyRescuers)
if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsMedic && !c.Character.IsUnconscious))
{
float mySkill = character.GetSkillLevel("medical");
int betterRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.Character.Info.Job.GetSkillLevel("medical") >= mySkill, onlyBots: true);
if (targetCount / (float)betterRescuers <= 1)
{
// Enough rescuers
return 100;
}
else
{
bool foundOtherMedics = HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.Info.Job.Prefab.Identifier == "medicaldoctor");
if (foundOtherMedics)
{
if (character.Info.Job.Prefab.Identifier != "medicaldoctor")
{
// Double the vitality factor -> less likely to take action
multiplier = 2;
}
}
}
// Don't do anything if there's a medic on board and we are not a medic
return 100;
}
return Targets.Min(t => GetVitalityFactor(t)) / ratio * multiplier;
}
float worstCondition = Targets.Min(t => GetVitalityFactor(t));
if (Targets.Contains(character))
{
if (character.Bleeding > 10)
{
// Enforce the highest priority when bleeding out.
worstCondition = 0;
}
// Boost the priority when wounded.
worstCondition /= 2;
}
return worstCondition;
}
public static float GetVitalityFactor(Character character)
{
float vitality = character.HealthPercentage - character.Bleeding - character.Bloodloss + Math.Min(character.Oxygen, 0);
float vitality = character.HealthPercentage - (character.Bleeding * 2) - character.Bloodloss + Math.Min(character.Oxygen, 0);
vitality -= character.CharacterHealth.GetAfflictionStrength("paralysis");
return Math.Clamp(vitality, 0, 100);
}
@@ -92,6 +77,11 @@ namespace Barotrauma
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { return false; }
if (!humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveRescueAll>())
{
if (!character.IsMedic && target != character)
{
// Don't allow to treat others autonomously
return false;
}
// Ignore unsafe hulls, unless ordered
if (humanAI.UnsafeHulls.Contains(target.CurrentHull))
{
@@ -111,10 +101,10 @@ namespace Barotrauma
if (target.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
}
if (!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
if (target != character &&!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
{
// Ignore all concious targets that are currently fighting, fleeing or treating characters
if (targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveCombat>() ||
if (targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveCombat>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveFindSafety>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
{
@@ -85,32 +85,22 @@ namespace Barotrauma
//legacy support
public readonly string[] AppropriateJobs;
public readonly string[] Options;
public readonly string[] OptionNames;
private readonly Dictionary<string, string> OptionNames;
public readonly Dictionary<string, Sprite> OptionSprites;
private readonly Dictionary<string, Sprite> minimapIcons;
public Dictionary<string, Sprite> MinimapIcons => IsPrefab ? minimapIcons : Prefab.minimapIcons;
public readonly float Weight;
public readonly bool MustSetTarget;
public readonly string AppropriateSkill;
public bool HasOptions
{
get
{
if (IsPrefab)
{
return MustSetTarget || Options.Length > 1;
}
else
{
return Prefab.MustSetTarget || Prefab.Options.Length > 1;
}
}
}
public bool HasOptions => (IsPrefab ? Options : Prefab.Options).Length > 1;
public bool IsPrefab { get; private set; }
public readonly bool MustManuallyAssign;
static Order()
public static void Init()
{
Prefabs = new Dictionary<string, Order>();
OrderCategoryIcons = new Dictionary<OrderCategory, Tuple<Sprite, Color>>();
@@ -219,25 +209,18 @@ namespace Barotrauma
MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false);
AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null);
string translatedOptionNames = TextManager.Get("OrderOptions." + Identifier, true);
if (translatedOptionNames == null)
var optionNames = TextManager.Get("OrderOptions." + Identifier, true)?.Split(',', '') ??
orderElement.GetAttributeStringArray("optionnames", new string[0]);
OptionNames = new Dictionary<string, string>();
for (int i = 0; i < Options.Length && i < optionNames.Length; i++)
{
OptionNames = orderElement.GetAttributeStringArray("optionnames", new string[0]);
OptionNames.Add(Options[i], optionNames[i].Trim());
}
else
{
string[] splitOptionNames = translatedOptionNames.Split(',', '');
OptionNames = new string[Options.Length];
for (int i = 0; i < Options.Length && i < splitOptionNames.Length; i++)
{
OptionNames[i] = splitOptionNames[i].Trim();
}
}
if (OptionNames.Length != Options.Length)
if (OptionNames.Count != Options.Length)
{
DebugConsole.ThrowError("Error in Order " + Name + " - the number of option names doesn't match the number of options.");
OptionNames = Options;
OptionNames.Clear();
Options.ForEach(o => OptionNames.Add(o, o));
}
var spriteElement = orderElement.GetChildElement("sprite");
@@ -261,6 +244,15 @@ namespace Barotrauma
}
}
minimapIcons = new Dictionary<string, Sprite>();
var minimapIconElements = orderElement.GetChildElements("minimapicon");
foreach (XElement minimapIconElement in minimapIconElements)
{
var id = minimapIconElement.GetAttributeString("id", null);
if (string.IsNullOrWhiteSpace(id)) { continue; }
minimapIcons.Add(id, new Sprite(minimapIconElement.GetChildElement("sprite"), lazyLoad: true));
}
IsPrefab = true;
MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false);
}
@@ -268,7 +260,7 @@ namespace Barotrauma
/// <summary>
/// Constructor for order instances
/// </summary>
public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null)
public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false)
{
Prefab = prefab;
@@ -293,27 +285,23 @@ namespace Barotrauma
TargetEntity = targetEntity;
if (targetItem != null)
{
if (UseController) { ConnectedController = FindController(targetItem); }
if (UseController)
{
ConnectedController = targetItem.Item?.FindController();
if (ConnectedController == null)
{
#if DEBUG
throw new Exception("Tried to use controller, but couldn't find one");
#endif
UseController = false;
}
}
TargetEntity = targetItem.Item;
TargetItemComponent = targetItem;
}
IsPrefab = false;
}
private Controller FindController(ItemComponent targetComponent)
{
if (targetComponent?.Item == null) { return null; }
//try finding the controller with the simpler non-recursive method first
return targetComponent.Item.GetConnectedComponents<Controller>().FirstOrDefault() ??
targetComponent.Item.GetConnectedComponents<Controller>(recursive: true).FirstOrDefault();
}
private bool TryFindController(ItemComponent targetComponent, out Controller controller)
{
controller = FindController(targetComponent);
return controller != null;
}
public bool HasAppropriateJob(Character character)
{
@@ -368,7 +356,7 @@ namespace Barotrauma
matchingItems.RemoveAll(it => it.NonInteractable);
if (UseController)
{
matchingItems.RemoveAll(i => i.Components.None(c => c.GetType() == ItemComponentType && TryFindController(c, out _)));
matchingItems.RemoveAll(i => i.Components.None(c => c.GetType() == ItemComponentType) && !i.TryFindController(out _));
}
}
return matchingItems;
@@ -381,5 +369,16 @@ namespace Barotrauma
Submarine.MainSub;
return GetMatchingItems(submarine, mustBelongToPlayerSub);
}
public string GetOptionName(string id)
{
return Prefab == null ? OptionNames[id] : Prefab.OptionNames[id];
}
public string GetOptionName(int index)
{
if (index < 0 || index >= Options.Length) { return null; }
return GetOptionName(Options[index]);
}
}
}
@@ -46,7 +46,7 @@ namespace Barotrauma
var nodes = new Dictionary<int, PathNode>();
foreach (WayPoint wayPoint in wayPoints)
{
if (wayPoint == null) continue;
if (wayPoint == null) { continue; }
if (nodes.ContainsKey(wayPoint.ID))
{
#if DEBUG
@@ -63,7 +63,7 @@ namespace Barotrauma
{
PathNode connectedNode = null;
nodes.TryGetValue(linked.ID, out connectedNode);
if (connectedNode == null) continue;
if (connectedNode == null) { continue; }
node.Value.connections.Add(connectedNode);
}
@@ -107,17 +107,17 @@ namespace Barotrauma
void WaypointLinksChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (Submarine.Unloading) return;
if (Submarine.Unloading) { return; }
var waypoints = sender as IEnumerable<MapEntity>;
foreach (MapEntity me in waypoints)
{
WayPoint wp = me as WayPoint;
if (me == null) continue;
if (me == null) { continue; }
var node = nodes.Find(n => n.Waypoint == wp);
if (node == null) return;
if (node == null) { return; }
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
{
@@ -136,10 +136,10 @@ namespace Barotrauma
for (int i = 0; i < wp.linkedTo.Count; i++)
{
WayPoint connected = wp.linkedTo[i] as WayPoint;
if (connected == null) continue;
if (connected == null) { continue; }
//already connected, continue
if (node.connections.Any(n => n.Waypoint == connected)) continue;
if (node.connections.Any(n => n.Waypoint == connected)) { continue; }
var matchingNode = nodes.Find(n => n.Waypoint == connected);
if (matchingNode == null)
@@ -201,8 +201,8 @@ namespace Barotrauma
if (body != null)
{
//if (body.UserData is Submarine) continue;
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) continue;
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) continue;
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; }
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
}
}
@@ -236,9 +236,9 @@ namespace Barotrauma
if (InsideSubmarine)
{
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null) dist *= 10.0f;
if (node.Waypoint.CurrentHull == null) { dist *= 10.0f; }
//avoid stopping at a doorway
if (node.Waypoint.ConnectedDoor != null) dist *= 10.0f;
if (node.Waypoint.ConnectedDoor != null) { dist *= 10.0f; }
}
if (dist < closestDist || endNode == null)
{
@@ -251,8 +251,8 @@ namespace Barotrauma
if (body != null)
{
//if (body.UserData is Submarine) continue;
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) continue;
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) continue;
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; }
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
}
}
@@ -9,12 +9,31 @@ namespace Barotrauma
int currentIndex;
private float? totalLength;
public bool Unreachable
{
get;
set;
}
public float TotalLength
{
get
{
if (Unreachable) { return float.PositiveInfinity; }
if (!totalLength.HasValue)
{
totalLength = 0.0f;
for (int i = 0; i < nodes.Count - 1; i++)
{
totalLength += Vector2.Distance(nodes[i].WorldPosition, nodes[i + 1].WorldPosition);
}
}
return totalLength.Value;
}
}
public SteeringPath(bool unreachable = false)
{
nodes = new List<WayPoint>();
@@ -23,10 +42,10 @@ namespace Barotrauma
public void AddNode(WayPoint node)
{
if (node == null) return;
if (node == null) { return; }
nodes.Add(node);
if (node.CurrentHull == null) HasOutdoorsNodes = true;
if (node.CurrentHull == null) { HasOutdoorsNodes = true; }
}
public bool HasOutdoorsNodes
@@ -48,10 +67,10 @@ namespace Barotrauma
public WayPoint PrevNode
{
get
get
{
if (currentIndex-1 < 0 || currentIndex-1 > nodes.Count - 1) return null;
return nodes[currentIndex-1];
if (currentIndex - 1 < 0 || currentIndex - 1 > nodes.Count - 1) { return null; }
return nodes[currentIndex - 1];
}
}
@@ -59,7 +78,7 @@ namespace Barotrauma
{
get
{
if (currentIndex < 0 || currentIndex > nodes.Count - 1) return null;
if (currentIndex < 0 || currentIndex > nodes.Count - 1) { return null; }
return nodes[currentIndex];
}
}
@@ -73,7 +92,7 @@ namespace Barotrauma
{
get
{
if (currentIndex+1 < 0 || currentIndex+1 > nodes.Count - 1) return null;
if (currentIndex + 1 < 0 || currentIndex + 1 > nodes.Count - 1) { return null; }
return nodes[currentIndex+1];
}
}
@@ -90,8 +109,8 @@ namespace Barotrauma
public WayPoint CheckProgress(Vector2 simPosition, float minSimDistance = 0.1f)
{
if (nodes.Count == 0 || currentIndex>nodes.Count-1) return null;
if (Vector2.Distance(simPosition, nodes[currentIndex].SimPosition) < minSimDistance) currentIndex++;
if (nodes.Count == 0 || currentIndex > nodes.Count - 1) { return null; }
if (Vector2.Distance(simPosition, nodes[currentIndex].SimPosition) < minSimDistance) { currentIndex++; }
return CurrentNode;
}
@@ -268,7 +268,26 @@ namespace Barotrauma
{
if (Config.KillAgentsWhenEntityDies)
{
protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null, isNetworkMessage: true));
protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null));
if (!string.IsNullOrWhiteSpace(Config.OffensiveAgent))
{
foreach (var character in Character.CharacterList)
{
// Kills ALL offensive agents that are near the thalamus. Not the ideal solution,
// but as long as spawning is handled via status effects, I don't know if there is any better way.
// In practice there shouldn't be terminal cells from different thalamus organisms at the same time.
// And if there was, the distance check should prevent killing the agents of a different organism.
if (character.SpeciesName.Equals(Config.OffensiveAgent, StringComparison.OrdinalIgnoreCase))
{
// Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this.
float maxDistance = Sonar.DefaultSonarRange;
if (Vector2.DistanceSquared(character.WorldPosition, Wreck.WorldPosition) < maxDistance * maxDistance)
{
character.Kill(CauseOfDeathType.Unknown, null);
}
}
}
}
}
}
}
@@ -18,6 +18,9 @@ namespace Barotrauma
[Serialize("", false)]
public string DefensiveAgent { get; private set; }
[Serialize("", false)]
public string OffensiveAgent { get; private set; }
[Serialize("", false)]
public string Brain { get; private set; }
@@ -47,12 +47,16 @@ namespace Barotrauma
base.Update(deltaTime, cam);
if (!Enabled) { return; }
if (IsDead || Vitality <= 0.0f || Stun > 0.0f || IsIncapacitated) { return; }
if (IsDead || Vitality <= 0.0f || Stun > 0.0f || IsIncapacitated)
{
//don't enable simple physics on dead/incapacitated characters
//the ragdoll controls the movement of incapacitated characters instead of the collider,
//but in simple physics mode the ragdoll would get disabled, causing the character to not move at all
AnimController.SimplePhysicsEnabled = false;
return;
}
//don't enable simple physics on dead/incapacitated characters
//the ragdoll controls the movement of incapacitated characters instead of the collider,
//but in simple physics mode the ragdoll would get disabled, causing the character to not move at all
if (!IsRemotePlayer)
if (!IsRemotePlayer && !(AIController is HumanAIController))
{
float characterDist = float.MaxValue;
#if CLIENT
@@ -76,7 +76,8 @@ namespace Barotrauma
{
if (InWater || !CanWalk)
{
return TargetMovement.Length() > (SwimSlowParams.MovementSpeed + SwimFastParams.MovementSpeed) / 2.0f;
float avg = (SwimSlowParams.MovementSpeed + SwimFastParams.MovementSpeed) / 2.0f;
return TargetMovement.LengthSquared() > avg * avg;
}
else
{
@@ -213,7 +213,12 @@ namespace Barotrauma
UpdateWalkAnim(deltaTime);
}
//don't flip or drag when simply physics is enabled
if (character.SelectedCharacter != null)
{
DragCharacter(character.SelectedCharacter, deltaTime);
}
//don't flip when simply physics is enabled
if (SimplePhysicsEnabled) { return; }
if (!character.IsRemotePlayer && (character.AIController == null || character.AIController.CanFlip))
@@ -248,29 +253,33 @@ namespace Barotrauma
}
}
if (character.SelectedCharacter != null)
{
DragCharacter(character.SelectedCharacter, deltaTime);
}
if (!CurrentFishAnimation.Flip) { return; }
if (IsStuck) { return; }
if (character.AIController != null && !character.AIController.CanFlip) { return; }
flipCooldown -= deltaTime;
if (TargetDir != Direction.None && TargetDir != dir)
if (TargetDir != Direction.None && TargetDir != dir)
{
flipTimer += deltaTime;
if ((flipTimer > 0.5f && flipCooldown <= 0.0f) || character.IsRemotePlayer)
// Speed reductions are not taken into account here. It's intentional: an ai character cannot flip if it's heavily paralyzed (for example).
float requiredSpeed = CurrentAnimationParams.MovementSpeed / 2;
if (CurrentHull != null)
{
// Enemy movement speeds are halved inside submarines
requiredSpeed /= 2;
}
bool isMovingFastEnough = Math.Abs(MainLimb.LinearVelocity.X) > requiredSpeed;
bool isTryingToMoveHorizontally = Math.Abs(TargetMovement.X) > Math.Abs(TargetMovement.Y);
if ((flipTimer > CurrentFishAnimation.FlipDelay && flipCooldown <= 0.0f && ((isMovingFastEnough && isTryingToMoveHorizontally) || IsMovingBackwards))
|| character.IsRemotePlayer)
{
Flip();
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
{
Mirror();
Mirror(CurrentSwimParams != null ? CurrentSwimParams.MirrorLerp : true);
}
flipTimer = 0.0f;
flipCooldown = 1.0f;
flipCooldown = CurrentFishAnimation.FlipCooldown;
}
}
else
@@ -295,7 +304,7 @@ namespace Barotrauma
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
//stop dragging if there's something between the pull limb and the target
Vector2 sourceSimPos = mouthLimb.SimPosition;
Vector2 sourceSimPos = SimplePhysicsEnabled ? character.SimPosition : mouthLimb.SimPosition;
Vector2 targetSimPos = target.SimPosition;
if (character.Submarine != null && character.SelectedCharacter.Submarine == null)
{
@@ -317,7 +326,7 @@ namespace Barotrauma
float eatSpeed = dmg / ((float)Math.Sqrt(Math.Max(target.Mass, 1)) * 10);
eatTimer += deltaTime * eatSpeed;
Vector2 mouthPos = GetMouthPosition().Value;
Vector2 mouthPos = SimplePhysicsEnabled ? character.SimPosition : GetMouthPosition().Value;
Vector2 attackSimPosition = character.Submarine == null ? ConvertUnits.ToSimUnits(target.WorldPosition) : target.SimPosition;
Vector2 limbDiff = attackSimPosition - mouthPos;
@@ -525,6 +534,7 @@ namespace Barotrauma
foreach (var limb in Limbs)
{
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);
@@ -550,10 +560,12 @@ namespace Barotrauma
for (int i = 0; i < Limbs.Length; i++)
{
if (Limbs[i].SteerForce <= 0.0f) { continue; }
var limb = Limbs[i];
if (limb.IsSevered) { continue; }
if (limb.SteerForce <= 0.0f) { continue; }
if (!Collider.PhysEnabled) { continue; }
Vector2 pullPos = Limbs[i].PullJointWorldAnchorA;
Limbs[i].body.ApplyForce(movement * Limbs[i].SteerForce * Limbs[i].Mass * Math.Max(character.SpeedMultiplier, 1), pullPos);
Vector2 pullPos = limb.PullJointWorldAnchorA;
limb.body.ApplyForce(movement * limb.SteerForce * limb.Mass * Math.Max(character.SpeedMultiplier, 1), pullPos);
}
Vector2 mainLimbDiff = mainLimb.PullJointWorldAnchorB - mainLimb.SimPosition;
@@ -604,6 +616,14 @@ namespace Barotrauma
float stepLift = TargetMovement.X == 0.0f ? 0 :
(float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100);
float limpAmount = character.GetLegPenalty();
if (limpAmount > 0)
{
float walkPosX = (float)Math.Cos(WalkPos);
//make the footpos oscillate when limping
limpAmount = Math.Max(Math.Abs(walkPosX) * limpAmount, 0.0f) * Math.Min(Math.Abs(TargetMovement.X), 0.3f) * Dir;
}
Limb torso = GetLimb(LimbType.Torso);
if (torso != null)
{
@@ -613,7 +633,7 @@ namespace Barotrauma
}
if (TorsoPosition.HasValue)
{
Vector2 pos = colliderBottom + new Vector2(0, TorsoPosition.Value + stepLift);
Vector2 pos = colliderBottom + new Vector2(limpAmount, TorsoPosition.Value + stepLift);
if (torso != mainLimb)
{
@@ -635,7 +655,7 @@ namespace Barotrauma
}
if (HeadPosition.HasValue)
{
Vector2 pos = colliderBottom + new Vector2(0, HeadPosition.Value + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier);
Vector2 pos = colliderBottom + new Vector2(limpAmount, HeadPosition.Value + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier);
if (head != mainLimb)
{
@@ -670,6 +690,7 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
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);
@@ -766,6 +787,7 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) { continue; }
#if CLIENT
if (limb.LightSource != null)
{
@@ -821,6 +843,7 @@ namespace Barotrauma
base.Flip();
foreach (Limb l in Limbs)
{
if (l.IsSevered) { continue; }
if (!l.DoesFlip) { continue; }
if (RagdollParams.IsSpritesheetOrientationHorizontal)
{
@@ -838,10 +861,13 @@ namespace Barotrauma
foreach (Limb l in Limbs)
{
if (l.IsSevered) { continue; }
TrySetLimbPosition(l,
centerOfMass,
new Vector2(centerOfMass.X - (l.SimPosition.X - centerOfMass.X), l.SimPosition.Y),
lerp);
l.body.PositionSmoothingFactor = 0.8f;
if (!l.DoesFlip) { continue; }
@@ -862,7 +888,7 @@ namespace Barotrauma
if (diff < 100.0f)
{
character.SelectedCharacter.AnimController.SetPosition(
new Vector2(centerOfMass.X - diff, character.SelectedCharacter.SimPosition.Y), lerp: true);
new Vector2(centerOfMass.X - diff, character.SelectedCharacter.SimPosition.Y), lerp);
}
}
}
@@ -323,7 +323,14 @@ namespace Barotrauma
levitatingCollider = true;
ColliderIndex = Crouching ? 1 : 0;
if (!Crouching && ColliderIndex == 1) Crouching = true;
if (character.SelectedConstruction?.GetComponent<Controller>()?.ControlCharacterPose ?? false)
{
Crouching = false;
}
else if (!Crouching && ColliderIndex == 1)
{
Crouching = true;
}
//stun (= disable the animations) if the ragdoll receives a large enough impact
if (strongestImpact > 0.0f)
@@ -542,12 +549,6 @@ namespace Barotrauma
Limb leftLeg = GetLimb(LimbType.LeftLeg);
Limb rightLeg = GetLimb(LimbType.RightLeg);
float limpAmount =
character.CharacterHealth.GetAfflictionStrength("damage", leftFoot, true) +
character.CharacterHealth.GetAfflictionStrength("damage", rightFoot, true) +
character.CharacterHealth.GetAfflictionStrength("spaceherpes");
limpAmount = MathHelper.Clamp(limpAmount / 100.0f, 0.0f, 1.0f);
float walkCycleMultiplier = 1.0f;
if (Stairs != null)
{
@@ -582,6 +583,11 @@ namespace Barotrauma
stepSize.Y *= walkPosY;
float footMid = colliderPos.X;
var herpes = character.CharacterHealth.GetAffliction("spaceherpes", false);
float herpesAmount = herpes == null ? 0 : herpes.Strength / herpes.Prefab.MaxStrength;
float legDamage = character.GetLegPenalty(startSum: -0.1f) * 1.1f;
float limpAmount = MathHelper.Lerp(0, 1, legDamage + herpesAmount);
if (limpAmount > 0.0f)
{
//make the footpos oscillate when limping
@@ -652,6 +658,7 @@ namespace Barotrauma
(float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100);
float y = colliderPos.Y + stepLift;
if (TorsoPosition.HasValue)
{
y += TorsoPosition.Value;
@@ -690,6 +697,7 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) { continue; }
MoveLimb(limb, limb.SimPosition + move, 15.0f, true);
}
@@ -947,8 +955,6 @@ namespace Barotrauma
torso.body.MoveToPos(Collider.SimPosition + new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * 0.4f, 5.0f);
if (TargetMovement == Vector2.Zero) { return; }
movement = MathUtils.SmoothStep(movement, TargetMovement, 0.3f);
if (TorsoAngle.HasValue)
@@ -1001,29 +1007,31 @@ namespace Barotrauma
}
WalkPos += movement.Length();
legCyclePos += Vector2.Normalize(movement).Length();
legCyclePos += Math.Min(movement.LengthSquared() + Collider.AngularVelocity, 1.0f);
handCyclePos += MathHelper.ToRadians(CurrentSwimParams.HandCycleSpeed) * Math.Sign(movement.X);
var waist = GetLimb(LimbType.Waist);
footPos = waist == null ? Vector2.Zero : waist.SimPosition - new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * (upperLegLength + lowerLegLength);
Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength / character.SpeedMultiplier) * CurrentSwimParams.LegMoveAmount, 0.0f);
Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength) * CurrentSwimParams.LegMoveAmount, 0.0f);
transformedFootPos = Vector2.Transform(transformedFootPos, Matrix.CreateRotationZ(Collider.Rotation));
float torque = CurrentSwimParams.FootRotateStrength * character.SpeedMultiplier * (1.2f - character.GetLegPenalty());
if (rightFoot != null && !rightFoot.Disabled)
{
FootIK(rightFoot, footPos - transformedFootPos, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootAngleInRadians);
FootIK(rightFoot, footPos - transformedFootPos, torque, torque, CurrentSwimParams.FootAngleInRadians);
}
if (leftFoot != null && !leftFoot.Disabled)
{
FootIK(leftFoot, footPos + transformedFootPos, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootAngleInRadians);
FootIK(leftFoot, footPos + transformedFootPos, torque, torque, CurrentSwimParams.FootAngleInRadians);
}
handPos = (torso.SimPosition + head.SimPosition) / 2.0f;
//at the surface, not moving sideways -> hands just float around
if (!headInWater && TargetMovement.X == 0.0f && TargetMovement.Y > 0)
//at the surface, not moving sideways OR not moving at all
// -> hands just float around
if ((!headInWater && TargetMovement.X == 0.0f && TargetMovement.Y > 0) || TargetMovement.LengthSquared() < 0.001f)
{
handPos.X = handPos.X + Dir * 0.6f;
handPos += MathUtils.RotatePoint(Vector2.UnitX * Dir * 0.6f, torso.Rotation);
float wobbleAmount = 0.1f;
@@ -1060,7 +1068,7 @@ namespace Barotrauma
rightHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, rightHandPos.X) : Math.Min(-0.3f, rightHandPos.X);
rightHandPos = Vector2.Transform(rightHandPos, rotationMatrix);
HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier);
HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier * (1 - Character.GetRightHandPenalty()));
}
if (leftHand != null && !leftHand.Disabled)
@@ -1069,7 +1077,7 @@ namespace Barotrauma
leftHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, leftHandPos.X) : Math.Min(-0.3f, leftHandPos.X);
leftHandPos = Vector2.Transform(leftHandPos, rotationMatrix);
HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier);
HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier * (1 - Character.GetLeftHandPenalty()));
}
}
@@ -1251,31 +1259,39 @@ namespace Barotrauma
Limb head = GetLimb(LimbType.Head);
Limb torso = GetLimb(LimbType.Torso);
//if the head is moving, try to protect it with the hands
if (head.LinearVelocity.LengthSquared() > 1.0f && !head.IsSevered)
if (head != null && head.LinearVelocity.LengthSquared() > 1.0f && !head.IsSevered)
{
//if the head is moving, try to protect it with the hands
Limb leftHand = GetLimb(LimbType.LeftHand);
Limb rightHand = GetLimb(LimbType.RightHand);
//move hands in front of the head in the direction of the movement
Vector2 protectPos = head.SimPosition + Vector2.Normalize(head.LinearVelocity);
if (!rightHand.IsSevered) HandIK(rightHand, protectPos, strength * 0.1f);
if (!leftHand.IsSevered) HandIK(leftHand, protectPos, strength * 0.1f);
if (rightHand != null && !rightHand.IsSevered)
{
HandIK(rightHand, protectPos, strength * 0.1f);
}
if (leftHand != null && !leftHand.IsSevered)
{
HandIK(leftHand, protectPos, strength * 0.1f);
}
}
if (torso == null) { return; }
//attempt to make legs stay in a straight line with the torso to prevent the character from doing a split
for (int i = 0; i < 2; i++)
{
var thigh = i == 0 ? GetLimb(LimbType.LeftThigh) : GetLimb(LimbType.RightThigh);
if (thigh.IsSevered) continue;
if (thigh == null) { continue; }
if (thigh.IsSevered) { continue; }
float thighDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, thigh.Rotation));
float thighTorque = thighDiff * thigh.Mass * Math.Sign(torso.Rotation - thigh.Rotation) * 5.0f;
thigh.body.ApplyTorque(thighTorque * strength);
var leg = i == 0 ? GetLimb(LimbType.LeftLeg) : GetLimb(LimbType.RightLeg);
if (leg.IsSevered) continue;
if (leg == null || leg.IsSevered) { continue; }
float legDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, leg.Rotation));
float legTorque = legDiff * leg.Mass * Math.Sign(torso.Rotation - leg.Rotation) * 5.0f;
leg.body.ApplyTorque(legTorque * strength);
@@ -1697,7 +1713,7 @@ namespace Barotrauma
if (holdable.ControlPose)
{
head.body.SmoothRotate(itemAngle);
head?.body.SmoothRotate(itemAngle);
if (TargetMovement == Vector2.Zero && inWater)
{
@@ -1719,13 +1735,13 @@ namespace Barotrauma
{
if (character.SelectedItems[0] == item)
{
if (rightHand.IsSevered) return;
if (rightHand == null || rightHand.IsSevered) { return; }
transformedHoldPos = rightHand.PullJointWorldAnchorA - transformedHandlePos[0];
itemAngle = (rightHand.Rotation + (holdAngle - MathHelper.PiOver2) * Dir);
}
else if (character.SelectedItems[1] == item)
{
if (leftHand.IsSevered) return;
if (leftHand == null || leftHand.IsSevered) { return; }
transformedHoldPos = leftHand.PullJointWorldAnchorA - transformedHandlePos[1];
itemAngle = (leftHand.Rotation + (holdAngle - MathHelper.PiOver2) * Dir);
}
@@ -1734,12 +1750,12 @@ namespace Barotrauma
{
if (character.SelectedItems[0] == item)
{
if (rightHand.IsSevered) return;
if (rightHand == null || rightHand.IsSevered) { return; }
rightHand.Disabled = true;
}
if (character.SelectedItems[1] == item)
{
if (leftHand.IsSevered) return;
if (leftHand == null || leftHand.IsSevered) { return; }
leftHand.Disabled = true;
}
@@ -1797,17 +1813,14 @@ namespace Barotrauma
}
}
item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false);
item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false);
if (!isClimbing)
if (!isClimbing && !character.IsIncapacitated)
{
for (int i = 0; i < 2; i++)
{
if (character.SelectedItems[i] != item) continue;
if (itemPos == Vector2.Zero) continue;
if (character.SelectedItems[i] != item || itemPos == Vector2.Zero) { continue; }
Limb hand = (i == 0) ? rightHand : leftHand;
HandIK(hand, transformedHoldPos + transformedHandlePos[i]);
}
}
@@ -1987,6 +2000,8 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) { continue; }
bool mirror = false;
bool flipAngle = false;
bool wrapAngle = false;
@@ -47,7 +47,9 @@ namespace Barotrauma
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
protected Hull currentHull;
private bool accessRemovedCharacterErrorShown;
private Limb[] limbs;
public Limb[] Limbs
{
@@ -55,16 +57,17 @@ namespace Barotrauma
{
if (limbs == null)
{
string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
#if DEBUG || UNSTABLE
errorMsg += '\n' + Environment.StackTrace;
#endif
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);
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;
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);
accessRemovedCharacterErrorShown = true;
}
return new Limb[0];
}
return limbs;
@@ -97,12 +100,17 @@ namespace Barotrauma
private bool simplePhysicsEnabled;
public Character Character => character;
protected Character character;
protected float strongestImpact;
private float splashSoundTimer;
//the ragdoll builds a "tolerance" to the flow force when being pushed by water.
//Allows sudden forces (breach, letting water through a door) to heavily push the character around while ensuring flowing water won't make the characters permanently stuck.
private float flowForceTolerance, flowStunTolerance;
//the movement speed of the ragdoll
public Vector2 movement;
//the target speed towards which movement is interpolated
@@ -156,8 +164,8 @@ namespace Barotrauma
}
set
{
if (value == colliderIndex || collider == null) return;
if (value >= collider.Count || value < 0) return;
if (value == colliderIndex || collider == null) { return; }
if (value >= collider.Count || value < 0) { return; }
if (collider[colliderIndex].height < collider[value].height)
{
@@ -165,10 +173,9 @@ namespace Barotrauma
pos1.Y -= collider[colliderIndex].height * ColliderHeightFromFloor;
Vector2 pos2 = pos1;
pos2.Y += collider[value].height * 1.1f;
if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall))) return;
if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall))) { return; }
}
Vector2 pos = collider[colliderIndex].SimPosition;
pos.Y -= collider[colliderIndex].height * 0.5f;
pos.Y += collider[value].height * 0.5f;
@@ -216,7 +223,7 @@ namespace Barotrauma
mainLimb = torso ?? head;
if (mainLimb == null)
{
mainLimb = Limbs.FirstOrDefault();
mainLimb = Limbs.FirstOrDefault(l => !l.IsSevered && !l.ignoreCollisions);
}
}
return mainLimb;
@@ -238,13 +245,13 @@ namespace Barotrauma
get { return simplePhysicsEnabled; }
set
{
if (value == simplePhysicsEnabled) return;
if (value == simplePhysicsEnabled) { return; }
simplePhysicsEnabled = value;
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) continue;
if (limb.IsSevered) { continue; }
if (limb.body == null)
{
DebugConsole.ThrowError("Limb has no body! (" + (character != null ? character.Name : "Unknown character") + ", " + limb.type.ToString());
@@ -296,8 +303,6 @@ namespace Barotrauma
public float ImpactTolerance => RagdollParams.ImpactTolerance;
public bool Draggable => RagdollParams.Draggable;
public bool CanEnterSubmarine => RagdollParams.CanEnterSubmarine;
public bool CanAttackSubmarine => Limbs.Any(l => l.attack != null && l.attack.IsValidTarget(AttackTarget.Structure));
public bool CanAttackCharacters => Limbs.Any(l => l.attack != null && l.attack.IsValidTarget(AttackTarget.Character));
public float Dir => dir == Direction.Left ? -1.0f : 1.0f;
@@ -324,6 +329,7 @@ namespace Barotrauma
Submarine currSubmarine = currentHull?.Submarine;
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) { continue; }
limb.body.Submarine = currSubmarine;
}
Collider.Submarine = currSubmarine;
@@ -377,7 +383,7 @@ namespace Barotrauma
foreach (var kvp in items)
{
int id = kvp.Key.ID;
// This can be the case if we manipulate the ragdoll in runtime (husk appendage, limb severance)
// This can be the case if we manipulate the ragdoll at runtime (husk appendage, limb removal in the character editor)
if (id > limbs.Length - 1) { continue; }
var limb = limbs[id];
var itemList = kvp.Value;
@@ -438,7 +444,7 @@ namespace Barotrauma
{
foreach (LimbJoint joint in LimbJoints)
{
if (GameMain.World.JointList.Contains(joint)) { GameMain.World.Remove(joint); }
if (GameMain.World.JointList.Contains(joint.Joint)) { GameMain.World.Remove(joint.Joint); }
}
}
DebugConsole.Log($"Creating joints from {RagdollParams.Name}.");
@@ -523,7 +529,7 @@ namespace Barotrauma
public void AddJoint(JointParams jointParams)
{
LimbJoint joint = new LimbJoint(Limbs[jointParams.Limb1], Limbs[jointParams.Limb2], jointParams, this);
GameMain.World.Add(joint);
GameMain.World.Add(joint.Joint);
for (int i = 0; i < LimbJoints.Length; i++)
{
if (LimbJoints[i] != null) continue;
@@ -606,7 +612,7 @@ namespace Barotrauma
limb.Remove();
foreach (LimbJoint limbJoint in attachedJoints)
{
GameMain.World.Remove(limbJoint);
GameMain.World.Remove(limbJoint.Joint);
}
}
@@ -706,7 +712,7 @@ namespace Barotrauma
float impactDamage = Math.Min((impact - ImpactTolerance) * ImpactDamageMultiplayer, character.MaxVitality * MaxImpactDamage);
character.LastDamageSource = null;
character.AddDamage(impactPos, new List<Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(impactDamage) }, 0.0f, true);
character.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(impactDamage).ToEnumerable(), 0.0f, true);
strongestImpact = Math.Max(strongestImpact, impact - ImpactTolerance);
character.ApplyStatusEffects(ActionType.OnImpact, 1.0f);
//briefly disable impact damage
@@ -720,12 +726,14 @@ namespace Barotrauma
ImpactProjSpecific(impact, f1.Body);
}
public void SeverLimbJoint(LimbJoint limbJoint, bool playSound = true)
private readonly List<Limb> connectedLimbs = new List<Limb>();
private readonly List<LimbJoint> checkedJoints = new List<LimbJoint>();
public bool SeverLimbJoint(LimbJoint limbJoint)
{
if (!limbJoint.CanBeSevered || limbJoint.IsSevered)
{
return;
return false;
}
limbJoint.IsSevered = true;
@@ -738,22 +746,29 @@ namespace Barotrauma
limbJoint.LimbA.body.ApplyLinearImpulse(limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f);
limbJoint.LimbB.body.ApplyLinearImpulse(-limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f);
List<Limb> connectedLimbs = new List<Limb>();
List<LimbJoint> checkedJoints = new List<LimbJoint>();
connectedLimbs.Clear();
checkedJoints.Clear();
GetConnectedLimbs(connectedLimbs, checkedJoints, MainLimb);
foreach (Limb limb in Limbs)
{
if (connectedLimbs.Contains(limb)) { continue; }
limb.IsSevered = true;
if (limb.type == LimbType.RightHand)
{
character.SelectedItems[0]?.Drop(character);
}
else if (limb.type == LimbType.LeftHand)
{
character.SelectedItems[1]?.Drop(character);
}
}
SeverLimbJointProjSpecific(limbJoint, playSound: true);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.Status });
}
return true;
}
partial void SeverLimbJointProjSpecific(LimbJoint limbJoint, bool playSound);
@@ -764,7 +779,7 @@ namespace Barotrauma
foreach (LimbJoint joint in LimbJoints)
{
if (joint.IsSevered || checkedJoints.Contains(joint)) continue;
if (joint.IsSevered || checkedJoints.Contains(joint)) { continue; }
if (joint.LimbA == limb)
{
if (!connectedLimbs.Contains(joint.LimbB))
@@ -861,7 +876,7 @@ namespace Barotrauma
{
for (int i = 0; i < Limbs.Length; i++)
{
if (Limbs[i] == null) continue;
if (Limbs[i] == null) { continue; }
Limbs[i].PullJointEnabled = false;
}
}
@@ -982,8 +997,8 @@ namespace Barotrauma
{
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) continue;
if (limb.body.FarseerBody.ContactList == null) continue;
if (limb.IsSevered) { continue; }
if (limb.body.FarseerBody.ContactList == null) { continue; }
ContactEdge ce = limb.body.FarseerBody.ContactList;
while (ce != null && ce.Contact != null)
@@ -995,7 +1010,7 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) continue;
if (limb.IsSevered) { continue; }
limb.body.LinearVelocity += velocityChange;
}
@@ -1020,15 +1035,15 @@ namespace Barotrauma
Category collisionCategory = (IgnorePlatforms) ?
wall | Physics.CollisionProjectile | Physics.CollisionStairs
: wall | Physics.CollisionProjectile | Physics.CollisionPlatform | Physics.CollisionStairs;
if (collisionCategory == prevCollisionCategory) return;
if (collisionCategory == prevCollisionCategory) { return; }
prevCollisionCategory = collisionCategory;
Collider.CollidesWith = collisionCategory | Physics.CollisionItemBlocking;
foreach (Limb limb in Limbs)
{
if (limb.ignoreCollisions || limb.IsSevered) continue;
if (limb.ignoreCollisions || limb.IsSevered) { continue; }
try
{
@@ -1081,8 +1096,6 @@ namespace Barotrauma
CheckDistFromCollider();
UpdateCollisionCategories();
Vector2 flowForce = Vector2.Zero;
FindHull();
PreventOutsideCollision();
@@ -1104,10 +1117,7 @@ namespace Barotrauma
}
else
{
flowForce = GetFlowForce();
headInWater = false;
inWater = false;
if (currentHull.WaterVolume > currentHull.Volume * 0.95f)
{
@@ -1129,7 +1139,7 @@ namespace Barotrauma
if (lowerHull != null) floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height);
}
}
float standHeight =
float standHeight =
HeadPosition.HasValue ? HeadPosition.Value :
TorsoPosition.HasValue ? TorsoPosition.Value :
Collider.GetMaxExtent() * 0.5f;
@@ -1140,10 +1150,7 @@ namespace Barotrauma
}
}
if (flowForce.LengthSquared() > 0.001f)
{
Collider.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
UpdateHullFlowForces(deltaTime);
if (currentHull == null ||
currentHull.WaterVolume > currentHull.Volume * 0.95f ||
@@ -1152,7 +1159,6 @@ namespace Barotrauma
Collider.ApplyWaterForces();
}
foreach (Limb limb in Limbs)
{
//find the room which the limb is in
@@ -1177,14 +1183,7 @@ namespace Barotrauma
if (limb.Position.Y < limbHull.Surface)
{
limb.inWater = true;
if (flowForce.LengthSquared() > 0.001f)
{
limb.body.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
surfaceY = limbHull.Surface;
if (limb.type == LimbType.Head)
{
headInWater = true;
@@ -1256,6 +1255,12 @@ namespace Barotrauma
private int validityResets;
private bool CheckValidity()
{
if (limbs == null)
{
DebugConsole.ThrowError("Attempted to check the validity of a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this));
Invalid = true;
return false;
}
bool isColliderValid = CheckValidity(Collider);
bool limbsValid = true;
foreach (Limb limb in limbs)
@@ -1278,8 +1283,8 @@ namespace Barotrauma
Collider.SetTransform(Vector2.Zero, 0.0f);
foreach (Limb limb in Limbs)
{
limb.body.SetTransform(Collider.SimPosition, 0.0f);
limb.body.ResetDynamics();
limb.body?.SetTransform(Collider.SimPosition, 0.0f);
limb.body?.ResetDynamics();
}
Frozen = true;
}
@@ -1353,6 +1358,74 @@ namespace Barotrauma
partial void Splash(Limb limb, Hull limbHull);
private void UpdateHullFlowForces(float deltaTime)
{
if (currentHull == null) { return; }
const float StunForceThreshold = 5.0f;
const float StunDuration = 0.5f;
const float ToleranceIncreaseSpeed = 5.0f;
const float ToleranceDecreaseSpeed = 1.0f;
//how much distance to a gap affects the force it exerts on the character
const float DistanceFactor = 0.5f;
const float ForceMultiplier = 0.035f;
Vector2 flowForce = Vector2.Zero;
foreach (Gap gap in Gap.GapList)
{
if (gap.Open <= 0.0f || !gap.linkedTo.Contains(currentHull) || gap.LerpedFlowForce.LengthSquared() < 0.01f) { continue; }
float dist = Vector2.Distance(MainLimb.WorldPosition, gap.WorldPosition) * DistanceFactor;
flowForce += Vector2.Normalize(gap.LerpedFlowForce) * (Math.Max(gap.LerpedFlowForce.Length() - dist, 0.0f) * ForceMultiplier);
}
//throwing conscious/moving characters around takes more force -> double the flow force
if (character.CanMove) { flowForce *= 2.0f; }
float flowForceMagnitude = flowForce.Length();
float limbMultipier = limbs.Count(l => l.inWater) / (float)limbs.Length;
//if the force strong enough, stun the character to let it get thrown around by the water
if ((flowForceMagnitude * limbMultipier) - flowStunTolerance > StunForceThreshold)
{
character.Stun = Math.Max(character.Stun, StunDuration);
flowStunTolerance = Math.Max(flowStunTolerance, flowForceMagnitude);
}
if (character == Character.Controlled && inWater && Screen.Selected?.Cam != null)
{
float shakeStrength = Math.Min(flowForceMagnitude / 10.0f, 5.0f) * limbMultipier;
Screen.Selected.Cam.Shake = Math.Max(Screen.Selected.Cam.Shake, shakeStrength);
}
if (flowForceMagnitude > 0.0001f)
{
flowForce = Vector2.Normalize(flowForce) * Math.Max(flowForceMagnitude - flowForceTolerance, 0.0f);
}
if (flowForceTolerance <= flowForceMagnitude * 1.5f && inWater)
{
//build up "tolerance" to the flow force
//ensures the character won't get permanently stuck by forces, while allowing sudden changes in flow to push the character hard
flowForceTolerance += deltaTime * ToleranceIncreaseSpeed;
flowStunTolerance = Math.Max(flowStunTolerance, flowForceTolerance);
}
else
{
flowForceTolerance = Math.Max(flowForceTolerance - deltaTime * ToleranceDecreaseSpeed, 0.0f);
flowStunTolerance = Math.Max(flowStunTolerance - deltaTime * ToleranceDecreaseSpeed, 0.0f);
}
if (flowForce.LengthSquared() > 0.001f)
{
Collider.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
foreach (Limb limb in limbs)
{
if (!limb.inWater) { continue; }
limb.body.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
}
private void RefreshFloorY(Limb refLimb = null, bool ignoreStairs = false)
{
PhysicsBody refBody = refLimb == null ? Collider : refLimb.body;
@@ -1490,7 +1563,7 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) continue;
if (limb.IsSevered) { continue; }
//check visibility from the new position of the collider to the new position of this limb
Vector2 movePos = limb.SimPosition + limbMoveAmount;
@@ -1554,7 +1627,7 @@ namespace Barotrauma
Vector2 forceDir = diff / (float)Math.Sqrt(distSqrd);
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) continue;
if (limb.IsSevered) { continue; }
limb.body.CollidesWith = Physics.CollisionNone;
limb.body.ApplyForce(forceDir * limb.Mass * 10.0f, maxVelocity: 10.0f);
}
@@ -1600,28 +1673,37 @@ namespace Barotrauma
UpdateNetPlayerPositionProjSpecific(deltaTime, lowestSubPos);
}
private Vector2 GetFlowForce()
{
Vector2 limbPos = Limbs[0].Position;
Vector2 force = Vector2.Zero;
foreach (Gap gap in Gap.GapList)
{
if (gap.Open <= 0.0f || gap.FlowTargetHull != currentHull || gap.LerpedFlowForce.LengthSquared() < 0.01f) continue;
Vector2 gapPos = gap.SimPosition;
float dist = Vector2.Distance(limbPos, gapPos);
force += Vector2.Normalize(gap.LerpedFlowForce) * (Math.Max(gap.LerpedFlowForce.Length() - dist, 0.0f) / 500.0f);
}
return force;
}
/// <summary>
/// Note that if there are multiple limbs of the same type, only the first of them is found in the dictionary.
/// </summary>
public Limb GetLimb(LimbType limbType)
public Limb GetLimb(LimbType limbType, bool excludeSevered = true)
{
limbDictionary.TryGetValue(limbType, out Limb limb);
Limb limb = null;
if (HasMultipleLimbsOfSameType)
{
for (int i = 0; i < 10; i++)
{
limbDictionary.TryGetValue(limbType, out limb);
if (limb == null)
{
// No limbs found
break;
}
if (!excludeSevered || !limb.IsSevered)
{
// Found a valid limb
break;
}
}
}
else
{
limbDictionary.TryGetValue(limbType, out limb);
}
if (excludeSevered && limb != null && limb.IsSevered)
{
limb = null;
}
return limb;
}
@@ -1664,10 +1746,15 @@ namespace Barotrauma
Limb lowestLimb = null;
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) { continue; }
if (lowestLimb == null)
{
lowestLimb = limb;
}
else if (limb.SimPosition.Y < lowestLimb.SimPosition.Y)
{
lowestLimb = limb;
}
}
return lowestLimb;
@@ -1700,11 +1787,12 @@ namespace Barotrauma
if (LimbJoints != null)
{
foreach (RevoluteJoint joint in LimbJoints)
foreach (var joint in LimbJoints)
{
if (GameMain.World.JointList.Contains(joint))
var j = joint.Joint;
if (GameMain.World.JointList.Contains(j))
{
GameMain.World.Remove(joint);
GameMain.World.Remove(j);
}
}
LimbJoints = null;
@@ -316,11 +316,6 @@ namespace Barotrauma
continue;
}
}
//float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength");
//var affliction = afflictionPrefab.Instantiate(afflictionStrength);
//Afflictions.Add(affliction, subElement);
break;
case "conditional":
foreach (XAttribute attribute in subElement.Attributes())
@@ -347,14 +342,18 @@ namespace Barotrauma
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab != null)
{
float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength");
affliction = afflictionPrefab.Instantiate(afflictionStrength);
affliction = afflictionPrefab.Instantiate(0.0f);
}
else
{
affliction = new Affliction(null, 0);
}
affliction.Deserialize(subElement);
//backwards compatibility
if (subElement.Attribute("amount") != null && subElement.Attribute("strength") == null)
{
affliction.Strength = subElement.GetAttributeFloat("amount", 0.0f);
}
// add the affliction anyway, so that it can be shown in the editor.
Afflictions.Add(affliction, subElement);
}
@@ -572,18 +571,14 @@ namespace Barotrauma
public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType;
public bool IsValidTarget(Entity target)
public bool IsValidTarget(IDamageable target)
{
switch (TargetType)
return TargetType switch
{
case AttackTarget.Character:
return target is Character;
case AttackTarget.Structure:
return !(target is Character);
case AttackTarget.Any:
default:
return true;
}
AttackTarget.Character => target is Character,
AttackTarget.Structure => !(target is Character),
_ => true,
};
}
public Vector2 CalculateAttackPhase(TransitionMode easing = TransitionMode.Linear)
@@ -3,7 +3,7 @@ using FarseerPhysics;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.IO;
using Barotrauma.IO;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -43,6 +43,7 @@ namespace Barotrauma
foreach (Limb limb in AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.body != null)
{
limb.body.Enabled = enabled;
@@ -235,6 +236,7 @@ namespace Barotrauma
{
get
{
if (info != null && !string.IsNullOrWhiteSpace(info.Name)) { return info.Name; }
var displayName = Params.DisplayName;
if (string.IsNullOrWhiteSpace(displayName))
{
@@ -587,30 +589,32 @@ namespace Barotrauma
set { canInventoryBeAccessed = value; }
}
private bool accessRemovedCharacterErrorShown;
public override Vector2 SimPosition
{
get
{
if (AnimController?.Collider == null)
{
string errorMsg = "Attempted to access a potentially removed character. Character: " + Name + ", id: " + ID + ", removed: " + Removed + ".";
if (AnimController == null)
if (!accessRemovedCharacterErrorShown)
{
errorMsg += " AnimController == null";
string errorMsg = "Attempted to access a potentially removed character. Character: " + Name + ", id: " + ID + ", removed: " + Removed + ".";
if (AnimController == null)
{
errorMsg += " AnimController == null";
}
else if (AnimController.Collider == null)
{
errorMsg += " AnimController.Collider == null";
}
errorMsg += '\n' + Environment.StackTrace;
DebugConsole.NewMessage(errorMsg, Color.Red);
GameAnalyticsManager.AddErrorEventOnce(
"Character.SimPosition:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
errorMsg + "\n" + Environment.StackTrace);
accessRemovedCharacterErrorShown = true;
}
else if (AnimController.Collider == null)
{
errorMsg += " AnimController.Collider == null";
}
#if DEBUG || UNSTABLE
errorMsg += '\n' + Environment.StackTrace;
#endif
DebugConsole.NewMessage(errorMsg, Color.Red);
GameAnalyticsManager.AddErrorEventOnce(
"Character.SimPosition:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
errorMsg + "\n" + Environment.StackTrace);
return Vector2.Zero;
}
@@ -1140,22 +1144,86 @@ namespace Barotrauma
greatestNegativeSpeedMultiplier = 1f;
}
/// <summary>
/// Speed reduction from the current limb specific damage. Min 0, max 1.
/// </summary>
public float GetTemporarySpeedReduction()
{
float reduction = 0;
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightFoot, excludeSevered: false), reduction);
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftFoot, excludeSevered: false), reduction);
if (AnimController is HumanoidAnimController)
{
if (AnimController.InWater)
{
// Currently only humans use hands for swimming.
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightHand, excludeSevered: false), reduction);
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftHand, excludeSevered: false), reduction);
}
}
else
{
int totalTailLimbs = 0;
int destroyedTailLimbs = 0;
foreach (var limb in AnimController.Limbs)
{
if (limb.type == LimbType.Tail)
{
totalTailLimbs++;
if (limb.IsSevered)
{
destroyedTailLimbs++;
}
}
}
if (destroyedTailLimbs > 0)
{
reduction += MathHelper.Lerp(0, AnimController.InWater ? 1f : 0.5f, (float)destroyedTailLimbs / totalTailLimbs);
}
}
return Math.Clamp(reduction, 0, 1f);
}
private float CalculateMovementPenalty(Limb limb, float sum, float max = 0.4f)
{
if (limb != null)
{
sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: "damage"));
}
return Math.Clamp(sum, 0, 1f);
}
public float GetRightHandPenalty() => CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightHand, excludeSevered: false), 0, max: 1);
public float GetLeftHandPenalty() => CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftHand, excludeSevered: false), 0, max: 1);
public float GetLegPenalty(float startSum = 0)
{
float sum = startSum;
foreach (var limb in AnimController.Limbs)
{
switch (limb.type)
{
case LimbType.RightFoot:
case LimbType.LeftFoot:
sum += CalculateMovementPenalty(limb, sum, max: 0.5f);
break;
}
}
return Math.Clamp(sum, 0, 1f);
}
public float ApplyTemporarySpeedLimits(float speed)
{
var leftFoot = AnimController.GetLimb(LimbType.LeftFoot);
if (leftFoot != null)
float max;
if (AnimController is HumanoidAnimController)
{
float footAfflictionStrength = CharacterHealth.GetAfflictionStrength("damage", leftFoot, true);
speed *= MathHelper.Lerp(1.0f, 0.4f, MathHelper.Clamp(footAfflictionStrength / 80.0f, 0.0f, 1.0f));
max = AnimController.InWater ? 0.5f : 0.7f;
}
var rightFoot = AnimController.GetLimb(LimbType.RightFoot);
if (rightFoot != null)
else
{
float footAfflictionStrength = CharacterHealth.GetAfflictionStrength("damage", rightFoot, true);
speed *= MathHelper.Lerp(1.0f, 0.4f, MathHelper.Clamp(footAfflictionStrength / 80.0f, 0.0f, 1.0f));
max = AnimController.InWater ? 0.9f : 0.5f;
}
speed *= 1f - MathHelper.Lerp(0, max, GetTemporarySpeedReduction());
return speed;
}
@@ -1256,58 +1324,72 @@ namespace Barotrauma
}
else if (IsKeyDown(InputType.Attack) && (IsRemotePlayer || Controlled == this || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)))
{
Vector2 attackPos = SimPosition + ConvertUnits.ToSimUnits(cursorPosition - Position);
List<Body> ignoredBodies = AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
ignoredBodies.Add(AnimController.Collider.FarseerBody);
var body = Submarine.PickBody(
SimPosition,
attackPos,
ignoredBodies,
Physics.CollisionCharacter | Physics.CollisionWall);
IDamageable attackTarget = null;
if (body != null)
{
attackPos = Submarine.LastPickedPosition;
if (body.UserData is Submarine sub)
{
body = Submarine.PickBody(
SimPosition - ((Submarine)body.UserData).SimPosition,
attackPos - ((Submarine)body.UserData).SimPosition,
ignoredBodies,
Physics.CollisionWall);
if (body != null)
{
attackPos = Submarine.LastPickedPosition + sub.SimPosition;
attackTarget = body.UserData as IDamageable;
}
}
else
{
if (body.UserData is IDamageable)
{
attackTarget = (IDamageable)body.UserData;
}
else if (body.UserData is Limb)
{
attackTarget = ((Limb)body.UserData).character;
}
}
}
var currentContexts = GetAttackContexts();
var validLimbs = AnimController.Limbs.Where(l => !l.IsSevered && !l.IsStuck && l.attack != null && l.attack.IsValidContext(currentContexts));
var validLimbs = AnimController.Limbs.Where(l =>
{
if (l.IsSevered || l.IsStuck) { return false; }
var attack = l.attack;
if (attack == null) { return false; }
if (attack.CoolDownTimer > 0) { return false; }
if (!attack.IsValidContext(currentContexts)) { return false; }
if (attackTarget != null)
{
if (!attack.IsValidTarget(attackTarget)) { return false; }
if (attackTarget is ISerializableEntity se && attackTarget is Character)
{
if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; }
}
}
if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(this))) { return false; }
return true;
});
var sortedLimbs = validLimbs.OrderBy(l => Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(l.SimPosition), cursorPosition));
// Select closest
var attackLimb = sortedLimbs.FirstOrDefault();
if (attackLimb != null)
{
Vector2 attackPos = attackLimb.SimPosition + Vector2.Normalize(cursorPosition - attackLimb.Position) * ConvertUnits.ToSimUnits(attackLimb.attack.Range);
List<Body> ignoredBodies = AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
ignoredBodies.Add(AnimController.Collider.FarseerBody);
var body = Submarine.PickBody(
attackLimb.SimPosition,
attackPos,
ignoredBodies,
Physics.CollisionCharacter | Physics.CollisionWall);
IDamageable attackTarget = null;
if (body != null)
{
attackPos = Submarine.LastPickedPosition;
if (body.UserData is Submarine sub)
{
body = Submarine.PickBody(
attackLimb.SimPosition - ((Submarine)body.UserData).SimPosition,
attackPos - ((Submarine)body.UserData).SimPosition,
ignoredBodies,
Physics.CollisionWall);
if (body != null)
{
attackPos = Submarine.LastPickedPosition + sub.SimPosition;
attackTarget = body.UserData as IDamageable;
}
}
else
{
if (body.UserData is IDamageable)
{
attackTarget = (IDamageable)body.UserData;
}
else if (body.UserData is Limb)
{
attackTarget = ((Limb)body.UserData).character;
}
}
}
attackLimb.UpdateAttack(deltaTime, attackPos, attackTarget, out AttackResult attackResult);
if (!attackLimb.attack.IsRunning)
{
attackCoolDown = 1.0f;
@@ -1394,22 +1476,23 @@ namespace Barotrauma
{
Limb selfLimb = AnimController.GetLimb(LimbType.Head);
if (selfLimb == null) { selfLimb = AnimController.GetLimb(LimbType.Torso); }
if (selfLimb == null) { selfLimb = AnimController.Limbs.FirstOrDefault(); }
if (selfLimb == null) { selfLimb = AnimController.MainLimb; }
return selfLimb;
}
public bool CanSeeTarget(ISpatialEntity target, Limb seeingLimb = null)
{
seeingLimb = seeingLimb ?? GetSeeingLimb();
seeingLimb ??= GetSeeingLimb();
if (seeingLimb == null) { return false; }
ISpatialEntity seeingEntity = AnimController.SimplePhysicsEnabled ? this : seeingLimb as ISpatialEntity;
// TODO: Could we just use the method below? If not, let's refactor it so that we can.
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingLimb.WorldPosition);
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition);
Body closestBody;
//both inside the same sub (or both outside)
//OR the we're inside, the other character outside
if (target.Submarine == Submarine || target.Submarine == null)
{
closestBody = Submarine.CheckVisibility(seeingLimb.SimPosition, seeingLimb.SimPosition + diff);
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
}
//we're outside, the other character inside
else if (Submarine == null)
@@ -1419,7 +1502,7 @@ namespace Barotrauma
//both inside different subs
else
{
closestBody = Submarine.CheckVisibility(seeingLimb.SimPosition, seeingLimb.SimPosition + diff);
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
if (!IsBlocking(closestBody))
{
closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff);
@@ -1436,10 +1519,11 @@ namespace Barotrauma
}
else if (body.UserData is Item item && item != target)
{
// TODO: The door collider should be disabled, so this check is probably unnecessary.
var door = item.GetComponent<Door>();
if (door != null)
{
return !door.IsOpen;
return !door.IsOpen && !door.IsBroken;
}
}
return false;
@@ -1466,7 +1550,7 @@ namespace Barotrauma
Structure wall = closestBody.UserData as Structure;
Item item = closestBody.UserData as Item;
Door door = item?.GetComponent<Door>();
return (wall == null || !wall.CastShadow) && (door == null || door.IsOpen);
return (wall == null || !wall.CastShadow) && (door == null || door.IsOpen || door.IsBroken);
}
public bool HasItem(Item item, bool requireEquipped = false) => requireEquipped ? HasEquippedItem(item) : item.IsOwnedBy(this);
@@ -1601,8 +1685,8 @@ namespace Barotrauma
if (IsItemTakenBySomeoneElse(item)) { continue; }
float itemPriority = customPriorityFunction != null ? customPriorityFunction(item) : 1;
if (itemPriority <= 0) { continue; }
Item rootContainer = item.GetRootContainer();
Vector2 itemPos = (rootContainer ?? item).WorldPosition;
Entity rootInventoryOwner = item.GetRootInventoryOwner();
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
float yDist = Math.Abs(WorldPosition.Y - itemPos.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(WorldPosition.X - itemPos.X) + yDist;
@@ -1620,13 +1704,16 @@ namespace Barotrauma
public bool IsItemTakenBySomeoneElse(Item item) => item.FindParentInventory(i => i.Owner != this && i.Owner is Character owner && !owner.IsDead && !owner.Removed) != null;
public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true)
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.CharacterHealth.UseHealthWindow && !c.CanBeDragged && c.onCustomInteract == null) return false;
if (c == this || Removed || !c.Enabled || !c.CanBeSelected) { return false; }
if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && c.onCustomInteract == null) { return false; }
maxDist = ConvertUnits.ToSimUnits(maxDist);
if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist) return false;
if (!skipDistanceCheck)
{
maxDist = ConvertUnits.ToSimUnits(maxDist);
if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist) { return false; }
}
return checkVisibility ? CanSeeCharacter(c) : true;
}
@@ -2394,7 +2481,7 @@ namespace Barotrauma
}
}
private readonly float maxAIRange = 10000;
private readonly float maxAIRange = 20000;
private readonly float aiTargetChangeSpeed = 5;
private void UpdateSightRange(float deltaTime)
@@ -2628,40 +2715,60 @@ namespace Barotrauma
GameServer.Log(sb.ToString(), ServerLog.MessageType.Attack);
}
#endif
bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient;
TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability);
// 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);
return attackResult;
}
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability)
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading)
{
bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient;
if (isNotClient &&
IsDead && Rand.Range(0.0f, 1.0f) < severLimbsProbability)
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
#if DEBUG
if (targetLimb.character != this)
{
foreach (LimbJoint joint in AnimController.LimbJoints)
{
if (joint.CanBeSevered && (joint.LimbA == targetLimb || joint.LimbB == targetLimb))
{
#if CLIENT
CurrentHull?.AddDecal("blood", WorldPosition, Rand.Range(0.5f, 1.5f));
DebugConsole.ThrowError($"{Name} is attempting to sever joints of {targetLimb.character.Name}!");
return;
}
#endif
AnimController.SeverLimbJoint(joint);
if (joint.LimbA == targetLimb)
{
joint.LimbB.body.LinearVelocity += targetLimb.LinearVelocity * 0.5f;
}
else
{
joint.LimbA.body.LinearVelocity += targetLimb.LinearVelocity * 0.5f;
}
}
if (damage < targetLimb.Params.MinSeveranceDamage) { return; }
if (!IsDead)
{
if (!allowBeheading && targetLimb.type == LimbType.Head) { return; }
if (!targetLimb.CanBeSeveredAlive) { return; }
}
bool wasSevered = false;
float random = Rand.Value();
foreach (LimbJoint joint in AnimController.LimbJoints)
{
if (!joint.CanBeSevered) { continue; }
if (joint.LimbA != targetLimb && joint.LimbB != targetLimb) { continue; }
float probability = severLimbsProbability;
if (!IsDead)
{
probability *= joint.Params.SeveranceProbabilityModifier;
}
if (probability <= 0) { continue; }
if (random > probability) { continue; }
bool severed = AnimController.SeverLimbJoint(joint);
if (!wasSevered)
{
wasSevered = severed;
}
if (severed)
{
Limb otherLimb = joint.LimbA == targetLimb ? joint.LimbB : joint.LimbA;
otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass);
}
}
if (wasSevered)
{
if (targetLimb.character.AIController is EnemyAIController enemyAI)
{
enemyAI.ReevaluateAttacks();
}
ApplyStatusEffects(ActionType.OnSevered, 1.0f);
targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
}
}
@@ -2742,6 +2849,10 @@ namespace Barotrauma
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);
@@ -2800,8 +2911,10 @@ namespace Barotrauma
{
SelectedConstruction = null;
}
HealthUpdateInterval = 0.0f;
}
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
public void ApplyStatusEffects(ActionType actionType, float deltaTime)
{
foreach (StatusEffect statusEffect in statusEffects)
@@ -2810,30 +2923,61 @@ namespace Barotrauma
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
targets.Clear();
statusEffect.GetNearbyTargets(WorldPosition, targets);
statusEffect.Apply(ActionType.OnActive, deltaTime, this, targets);
}
else
{
statusEffect.Apply(actionType, deltaTime, this, this);
if (statusEffect.targetLimbs != null)
{
foreach (var limbType in statusEffect.targetLimbs)
{
if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
// Target all matching limbs
foreach (var limb in AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.type == limbType)
{
statusEffect.Apply(actionType, deltaTime, this, limb);
}
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
{
// Target just the first matching limb
Limb limb = AnimController.GetLimb(limbType);
statusEffect.Apply(actionType, deltaTime, this, limb);
}
}
}
}
}
if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered)
{
// OnDamaged is called only for the limb that is hit.
AnimController.Limbs.ForEach(l => l.ApplyStatusEffects(actionType, deltaTime));
}
}
private void Implode(bool isNetworkMessage = false)
{
if (CharacterHealth.Unkillable) { return; }
if (CharacterHealth.Unkillable || IsDead) { return; }
if (!isNetworkMessage)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
}
Kill(CauseOfDeathType.Pressure, null, isNetworkMessage);
CharacterHealth.PressureAffliction.Strength = CharacterHealth.PressureAffliction.Prefab.MaxStrength;
CharacterHealth.SetAllDamage(200.0f, 0.0f, 0.0f);
BreakJoints();
CharacterHealth.ApplyAffliction(null, new Affliction(AfflictionPrefab.Pressure, AfflictionPrefab.Pressure.MaxStrength));
if (isNetworkMessage && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Vitality <= CharacterHealth.MinVitality) { Kill(CauseOfDeathType.Pressure, null, isNetworkMessage: true); }
if (IsDead)
{
BreakJoints();
}
}
public void BreakJoints()
@@ -2841,6 +2985,7 @@ namespace Barotrauma
Vector2 centerOfMass = AnimController.GetCenterOfMass();
foreach (Limb limb in AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
limb.AddDamage(limb.SimPosition, 500.0f, 0.0f, 0.0f, false);
Vector2 diff = centerOfMass - limb.SimPosition;
@@ -2861,7 +3006,10 @@ namespace Barotrauma
foreach (var joint in AnimController.LimbJoints)
{
joint.LimitEnabled = false;
if (joint.revoluteJoint != null)
{
joint.revoluteJoint.LimitEnabled = false;
}
}
}
@@ -2928,9 +3076,12 @@ namespace Barotrauma
AnimController.ResetPullJoints();
foreach (RevoluteJoint joint in AnimController.LimbJoints)
foreach (var joint in AnimController.LimbJoints)
{
joint.MotorEnabled = false;
if (joint.revoluteJoint != null)
{
joint.revoluteJoint.MotorEnabled = false;
}
}
if (GameMain.GameSession != null)
@@ -2961,7 +3112,11 @@ namespace Barotrauma
foreach (LimbJoint joint in AnimController.LimbJoints)
{
joint.MotorEnabled = true;
var revoluteJoint = joint.revoluteJoint;
if (revoluteJoint != null)
{
revoluteJoint.MotorEnabled = true;
}
joint.Enabled = true;
joint.IsSevered = false;
}
@@ -3176,5 +3331,15 @@ namespace Barotrauma
}
return targetPos;
}
public bool IsCaptain => HasJob("captain");
public bool IsEngineer => HasJob("engineer");
public bool IsMechanic => HasJob("mechanic");
public bool IsMedic => HasJob("medicaldoctor");
public bool IsOfficer => HasJob("securityofficer");
public bool IsAsssitant => HasJob("assistant");
public bool IsWatchman => HasJob("watchman");
public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier;
}
}
@@ -4,7 +4,7 @@ using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using Barotrauma.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
@@ -97,11 +97,13 @@ namespace Barotrauma
private void ApplyDamage(float deltaTime, bool applyForce)
{
int limbCount = character.AnimController.Limbs.Count(l => !l.ignoreCollisions && !l.IsSevered);
foreach (Limb limb in character.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
float random = Rand.Value(Rand.RandSync.Server);
huskInfection.Clear();
huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / character.AnimController.Limbs.Length));
huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / limbCount));
character.LastDamageSource = null;
float force = applyForce ? random * 0.5f * limb.Mass : 0;
character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, force);
@@ -186,18 +188,20 @@ namespace Barotrauma
}
}
if (character.Inventory.Items.Length != husk.Inventory.Items.Length)
if (character.Inventory != null && husk.Inventory != null)
{
string errorMsg = "Failed to move items from the source character's inventory into a husk's inventory (inventory sizes don't match)";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AfflictionHusk.CreateAIHusk:InventoryMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
yield return CoroutineStatus.Success;
}
for (int i = 0; i < character.Inventory.Items.Length && i < husk.Inventory.Items.Length; i++)
{
if (character.Inventory.Items[i] == null) continue;
husk.Inventory.TryPutItem(character.Inventory.Items[i], i, true, false, null);
if (character.Inventory.Items.Length != husk.Inventory.Items.Length)
{
string errorMsg = "Failed to move items from the source character's inventory into a husk's inventory (inventory sizes don't match)";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("AfflictionHusk.CreateAIHusk:InventoryMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
yield return CoroutineStatus.Success;
}
for (int i = 0; i < character.Inventory.Items.Length && i < husk.Inventory.Items.Length; i++)
{
if (character.Inventory.Items[i] == null) continue;
husk.Inventory.TryPutItem(character.Inventory.Items[i], i, true, false, null);
}
}
husk.SetStun(5);
@@ -255,20 +259,19 @@ namespace Barotrauma
Limb attachLimb = null;
if (matchingAffliction.AttachLimbId > -1)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Params.ID == matchingAffliction.AttachLimbId);
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == matchingAffliction.AttachLimbId);
}
else if (matchingAffliction.AttachLimbName != null)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Name == matchingAffliction.AttachLimbName);
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Name == matchingAffliction.AttachLimbName);
}
else if (matchingAffliction.AttachLimbType != LimbType.None)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.type == matchingAffliction.AttachLimbType);
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.type == matchingAffliction.AttachLimbType);
}
if (attachLimb == null)
{
DebugConsole.Log("Attachment limb not defined in the affliction prefab or no matching limb could be found. Using the appendage definition as it is.");
attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Params.ID == jointParams.Limb1);
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == jointParams.Limb1);
}
if (attachLimb != null)
{
@@ -286,10 +289,6 @@ namespace Barotrauma
ragdoll.AddJoint(jointParams);
appendage.Add(huskAppendage);
}
else
{
DebugConsole.ThrowError("Attachment limb not found!");
}
}
}
return appendage;
@@ -189,6 +189,7 @@ namespace Barotrauma
}
public static AfflictionPrefab InternalDamage;
public static AfflictionPrefab ImpactDamage;
public static AfflictionPrefab Bleeding;
public static AfflictionPrefab Burn;
public static AfflictionPrefab OxygenLow;
@@ -291,6 +292,7 @@ namespace Barotrauma
{
CPRSettings.Unload();
InternalDamage = null;
ImpactDamage = null;
Bleeding = null;
Burn = null;
OxygenLow = null;
@@ -437,6 +439,9 @@ namespace Barotrauma
case "internaldamage":
InternalDamage = prefab;
break;
case "blunttrauma":
ImpactDamage = prefab;
break;
case "bleeding":
Bleeding = prefab;
break;
@@ -456,6 +461,8 @@ namespace Barotrauma
Stun = prefab;
break;
}
if (ImpactDamage == null) { ImpactDamage = InternalDamage; }
if (prefab != null)
{
Prefabs.Add(prefab, isOverride);
@@ -1,6 +1,6 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.IO;
using Barotrauma.IO;
using System.Linq;
using Barotrauma.Extensions;
using System.Xml.Linq;
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma
{
@@ -19,7 +20,7 @@ namespace Barotrauma
{
foreach (Affliction affliction in afflictions)
{
if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource != this) continue;
if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource != this) { continue; }
affliction.MultiplierSource = null;
affliction.StrengthDiminishMultiplier = 1f;
}
@@ -28,9 +29,9 @@ namespace Barotrauma
{
foreach (Affliction affliction in afflictions)
{
if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource == this) continue;
if (!affliction.Prefab.IsBuff || affliction == this) { continue; }
float multiplier = GetDiminishMultiplier();
if (affliction.StrengthDiminishMultiplier < multiplier) continue;
if (affliction.StrengthDiminishMultiplier < multiplier && affliction.MultiplierSource != this) { continue; }
affliction.MultiplierSource = this;
affliction.StrengthDiminishMultiplier = multiplier;
@@ -40,14 +41,15 @@ namespace Barotrauma
private float GetDiminishMultiplier()
{
if (Strength < Prefab.ActivationThreshold) return 1.0f;
if (Strength < Prefab.ActivationThreshold) { return 1.0f; }
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 1.0f;
if (currentEffect == null) { return 1.0f; }
return MathHelper.Lerp(
float multiplier = MathHelper.Lerp(
currentEffect.MinBuffMultiplier,
currentEffect.MaxBuffMultiplier,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
return 1.0f / Math.Max(multiplier, 0.001f);
}
}
}
@@ -127,7 +127,7 @@ namespace Barotrauma
public bool IsUnconscious
{
get { return Vitality <= 0.0f; }
get { return Vitality <= 0.0f || Character.IsDead; }
}
public float PressureKillDelay { get; private set; } = 5.0f;
@@ -252,8 +252,8 @@ namespace Barotrauma
: afflictions.Where(limbHealthFilter).Union(limbHealths.SelectMany(lh => lh.Afflictions.Where(limbHealthFilter)));
}
private LimbHealth GetMatchingLimbHealth(Limb limb) => limbHealths[limb.HealthIndex];
private LimbHealth GetMatchingLimbHealth(Affliction affliction) => GetMatchingLimbHealth(Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb));
private LimbHealth GetMatchingLimbHealth(Limb limb) => limb == null ? null : limbHealths[limb.HealthIndex];
private LimbHealth GetMatchingLimbHealth(Affliction affliction) => GetMatchingLimbHealth(Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb, excludeSevered: false));
/// <summary>
/// Returns the limb afflictions and non-limbspecific afflictions that are set to be displayed on this limb.
@@ -349,13 +349,16 @@ namespace Barotrauma
/// Most monsters for example don't have separate healths for different limbs, essentially meaning that every affliction is applied to every limb.</param>
public float GetAfflictionStrength(string afflictionType, Limb limb, bool requireLimbSpecific)
{
if (requireLimbSpecific && limbHealths.Count == 1) return 0.0f;
if (requireLimbSpecific && limbHealths.Count == 1) { return 0.0f; }
float strength = 0.0f;
foreach (Affliction affliction in limbHealths[limb.HealthIndex].Afflictions)
{
if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue;
if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength;
if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; }
if (affliction.Prefab.AfflictionType == afflictionType)
{
strength += affliction.Strength;
}
}
return strength;
}
@@ -365,17 +368,23 @@ namespace Barotrauma
float strength = 0.0f;
foreach (Affliction affliction in afflictions)
{
if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue;
if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength;
if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; }
if (affliction.Prefab.AfflictionType == afflictionType)
{
strength += affliction.Strength;
}
}
if (!allowLimbAfflictions) return strength;
if (!allowLimbAfflictions) { return strength; }
foreach (LimbHealth limbHealth in limbHealths)
{
foreach (Affliction affliction in limbHealth.Afflictions)
{
if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue;
if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength;
if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; }
if (affliction.Prefab.AfflictionType == afflictionType)
{
strength += affliction.Strength;
}
}
}
@@ -506,6 +515,34 @@ namespace Barotrauma
if (Vitality <= MinVitality) { Kill(); }
}
public float GetLimbDamage(Limb limb, string afflictionType = null)
{
float damageStrength;
if (limb.IsSevered)
{
return 1;
}
else
{
// Instead of using the limbhealth count here, I think it's best to define the max vitality per limb roughly with a constant value.
// Therefore with e.g. 80 health, the max damage per limb would be 20.
// Having at least 20 damage on both legs would cause maximum limping.
float max = MaxVitality / 4;
if (string.IsNullOrEmpty(afflictionType))
{
float damage = GetAfflictionStrength("damage", limb, true);
float bleeding = GetAfflictionStrength("bleeding", limb, true);
float burn = GetAfflictionStrength("burn", limb, true);
damageStrength = Math.Min(damage + bleeding + burn, max);
}
else
{
damageStrength = Math.Min(GetAfflictionStrength("damage", limb, true), max);
}
return damageStrength / max;
}
}
public void RemoveAllAfflictions()
{
foreach (LimbHealth limbHealth in limbHealths)
@@ -523,7 +560,7 @@ namespace Barotrauma
private void AddLimbAffliction(Limb limb, Affliction newAffliction)
{
if (!newAffliction.Prefab.LimbSpecific || limb == null) return;
if (!newAffliction.Prefab.LimbSpecific || limb == null) { return; }
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
{
DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
@@ -535,8 +572,8 @@ namespace Barotrauma
private void AddLimbAffliction(LimbHealth limbHealth, 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; }
foreach (Affliction affliction in limbHealth.Afflictions)
{
@@ -545,7 +582,10 @@ namespace Barotrauma
affliction.Strength = Math.Min(affliction.Prefab.MaxStrength, affliction.Strength + (newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier))));
affliction.Source = newAffliction.Source;
CalculateVitality();
if (Vitality <= MinVitality) Kill();
if (Vitality <= MinVitality)
{
Kill();
}
return;
}
}
@@ -560,7 +600,10 @@ namespace Barotrauma
Character.HealthUpdateInterval = 0.0f;
CalculateVitality();
if (Vitality <= MinVitality) Kill();
if (Vitality <= MinVitality)
{
Kill();
}
#if CLIENT
selectedLimbIndex = -1;
#endif
@@ -894,10 +937,7 @@ namespace Barotrauma
partial void RemoveProjSpecific();
/// <summary>
/// Automatically filters out buffs.
/// </summary>
public static IEnumerable<Affliction> SortAfflictionsBySeverity(IEnumerable<Affliction> afflictions) =>
afflictions.Where(a => !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength);
public static IEnumerable<Affliction> SortAfflictionsBySeverity(IEnumerable<Affliction> afflictions, bool excludeBuffs = true) =>
afflictions.Where(a => !excludeBuffs || !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength);
}
}
@@ -64,7 +64,7 @@ namespace Barotrauma
public readonly Dictionary<int, List<string>> ItemIdentifiers = new Dictionary<int, List<string>>();
public readonly Dictionary<int, Dictionary<string, bool>> ShowItemPreview = new Dictionary<int, Dictionary<string, bool>>();
public readonly List<SkillPrefab> Skills = new List<SkillPrefab>();
public readonly List<AutonomousObjective> AutomaticOrders = new List<AutonomousObjective>();
public readonly List<AutonomousObjective> AutonomousObjective = new List<AutonomousObjective>();
public readonly List<string> AppropriateOrders = new List<string>();
[Serialize("1,1,1,1", false)]
@@ -163,6 +163,7 @@ namespace Barotrauma
}
public Sprite Icon;
public Sprite IconSmall;
public string FilePath { get; private set; }
public XElement Element { get; private set; }
@@ -198,7 +199,7 @@ namespace Barotrauma
}
break;
case "autonomousobjectives":
subElement.Elements().ForEach(order => AutomaticOrders.Add(new AutonomousObjective(order)));
subElement.Elements().ForEach(order => AutonomousObjective.Add(new AutonomousObjective(order)));
break;
case "appropriateobjectives":
case "appropriateorders":
@@ -207,6 +208,9 @@ namespace Barotrauma
case "jobicon":
Icon = new Sprite(subElement.FirstElement());
break;
case "jobiconsmall":
IconSmall = new Sprite(subElement.FirstElement());
break;
}
}
@@ -19,8 +19,8 @@ namespace Barotrauma
None, LeftHand, RightHand, LeftArm, RightArm, LeftForearm, RightForearm,
LeftLeg, RightLeg, LeftFoot, RightFoot, Head, Torso, Tail, Legs, RightThigh, LeftThigh, Waist, Jaw
};
partial class LimbJoint : RevoluteJoint
partial class LimbJoint
{
public bool IsSevered;
public bool CanBeSevered => Params.CanBeSevered;
@@ -30,27 +30,135 @@ namespace Barotrauma
public float Scale => Params.Scale * ragdoll.RagdollParams.JointScale;
public LimbJoint(Limb limbA, Limb limbB, JointParams jointParams, Ragdoll ragdoll) : this(limbA, limbB, Vector2.One, Vector2.One)
public readonly RevoluteJoint revoluteJoint;
public readonly WeldJoint weldJoint;
public Joint Joint => revoluteJoint ?? weldJoint as Joint;
public bool Enabled
{
get => Joint.Enabled;
set => Joint.Enabled = value;
}
public Body BodyA => Joint.BodyA;
public Body BodyB => Joint.BodyB;
public Vector2 WorldAnchorA
{
get => Joint.WorldAnchorA;
set => Joint.WorldAnchorA = value;
}
public Vector2 WorldAnchorB
{
get => Joint.WorldAnchorB;
set => Joint.WorldAnchorB = value;
}
public Vector2 LocalAnchorA
{
get => revoluteJoint != null ? revoluteJoint.LocalAnchorA : weldJoint.LocalAnchorA;
set
{
if (weldJoint != null)
{
weldJoint.LocalAnchorA = value;
}
else
{
revoluteJoint.LocalAnchorA = value;
}
}
}
public Vector2 LocalAnchorB
{
get => revoluteJoint != null ? revoluteJoint.LocalAnchorB : weldJoint.LocalAnchorB;
set
{
if (weldJoint != null)
{
weldJoint.LocalAnchorB = value;
}
else
{
revoluteJoint.LocalAnchorB = value;
}
}
}
public bool LimitEnabled
{
get => revoluteJoint != null ? revoluteJoint.LimitEnabled : false;
set
{
if (revoluteJoint != null)
{
revoluteJoint.LimitEnabled = value;
}
}
}
public float LowerLimit
{
get => revoluteJoint != null ? revoluteJoint.LowerLimit : 0;
set
{
if (revoluteJoint != null)
{
revoluteJoint.LowerLimit = value;
}
}
}
public float UpperLimit
{
get => revoluteJoint != null ? revoluteJoint.UpperLimit : 0;
set
{
if (revoluteJoint != null)
{
revoluteJoint.UpperLimit = value;
}
}
}
public float JointAngle => revoluteJoint != null ? revoluteJoint.JointAngle : weldJoint.ReferenceAngle;
public LimbJoint(Limb limbA, Limb limbB, JointParams jointParams, Ragdoll ragdoll) : this(limbA, limbB, Vector2.One, Vector2.One, jointParams.WeldJoint)
{
Params = jointParams;
this.ragdoll = ragdoll;
LoadParams();
}
public LimbJoint(Limb limbA, Limb limbB, Vector2 anchor1, Vector2 anchor2)
: base(limbA.body.FarseerBody, limbB.body.FarseerBody, anchor1, anchor2)
public LimbJoint(Limb limbA, Limb limbB, Vector2 anchor1, Vector2 anchor2, bool weld = false)
{
CollideConnected = false;
MotorEnabled = true;
MaxMotorTorque = 0.25f;
if (weld)
{
weldJoint = new WeldJoint(limbA.body.FarseerBody, limbB.body.FarseerBody, anchor1, anchor2);
}
else
{
revoluteJoint = new RevoluteJoint(limbA.body.FarseerBody, limbB.body.FarseerBody, anchor1, anchor2)
{
MotorEnabled = true,
MaxMotorTorque = 0.25f
};
}
Joint.CollideConnected = false;
LimbA = limbA;
LimbB = limbB;
}
public void LoadParams()
{
MaxMotorTorque = Params.Stiffness;
LimitEnabled = Params.LimitEnabled;
if (revoluteJoint != null)
{
revoluteJoint.MaxMotorTorque = Params.Stiffness;
revoluteJoint.LimitEnabled = Params.LimitEnabled;
}
if (float.IsNaN(Params.LowerLimit))
{
Params.LowerLimit = 0;
@@ -61,17 +169,33 @@ namespace Barotrauma
}
if (ragdoll.IsFlipped)
{
LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Scale);
LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Scale);
UpperLimit = MathHelper.ToRadians(-Params.LowerLimit);
LowerLimit = MathHelper.ToRadians(-Params.UpperLimit);
if (weldJoint != null)
{
weldJoint.LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Scale);
weldJoint.LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Scale);
}
else
{
revoluteJoint.LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb1Anchor.X, Params.Limb1Anchor.Y) * Scale);
revoluteJoint.LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-Params.Limb2Anchor.X, Params.Limb2Anchor.Y) * Scale);
revoluteJoint.UpperLimit = MathHelper.ToRadians(-Params.LowerLimit);
revoluteJoint.LowerLimit = MathHelper.ToRadians(-Params.UpperLimit);
}
}
else
{
LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Scale);
LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Scale);
UpperLimit = MathHelper.ToRadians(Params.UpperLimit);
LowerLimit = MathHelper.ToRadians(Params.LowerLimit);
if (weldJoint != null)
{
weldJoint.LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Scale);
weldJoint.LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Scale);
}
else
{
revoluteJoint.LocalAnchorA = ConvertUnits.ToSimUnits(Params.Limb1Anchor * Scale);
revoluteJoint.LocalAnchorB = ConvertUnits.ToSimUnits(Params.Limb2Anchor * Scale);
revoluteJoint.UpperLimit = MathHelper.ToRadians(Params.UpperLimit);
revoluteJoint.LowerLimit = MathHelper.ToRadians(Params.LowerLimit);
}
}
}
}
@@ -166,10 +290,20 @@ namespace Barotrauma
if (isSevered)
{
ragdoll.SubtractMass(this);
if (type == LimbType.Head)
{
character.Kill(CauseOfDeathType.Unknown, null);
}
}
else
{
severedFadeOutTimer = 0.0f;
}
if (!isSevered) severedFadeOutTimer = 0.0f;
#if CLIENT
if (isSevered) damageOverlayStrength = 100.0f;
if (isSevered)
{
damageOverlayStrength = 100.0f;
}
#endif
}
}
@@ -366,14 +500,42 @@ namespace Barotrauma
public string Name => Params.Name;
// Exposed for status effects
public bool IsDead => character.IsDead;
public bool CanBeSeveredAlive
{
get
{
if (character.IsHumanoid) { return false; }
if (this == character.AnimController.MainLimb) { return false; }
if (character.AnimController.CanWalk)
{
switch (type)
{
case LimbType.LeftFoot:
case LimbType.RightFoot:
case LimbType.LeftLeg:
case LimbType.RightLeg:
case LimbType.LeftThigh:
case LimbType.RightThigh:
case LimbType.Legs:
case LimbType.Waist:
return false;
}
}
return true;
}
}
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
private set;
}
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
public Limb(Ragdoll ragdoll, Character character, LimbParams limbParams)
{
this.ragdoll = ragdoll;
@@ -436,6 +598,9 @@ namespace Barotrauma
case "damagemodifier":
DamageModifiers.Add(new DamageModifier(subElement, character.Name));
break;
case "statuseffect":
statusEffects.Add(StatusEffect.Load(subElement, Name));
break;
}
}
@@ -521,11 +686,12 @@ namespace Barotrauma
afflictionsCopy.Add(newAffliction);
}
}
AddDamageProjSpecific(afflictionsCopy, playSound, appliedDamageModifiers);
return new AttackResult(afflictionsCopy, this, appliedDamageModifiers);
var result = new AttackResult(afflictionsCopy, this, appliedDamageModifiers);
AddDamageProjSpecific(playSound, result);
return result;
}
partial void AddDamageProjSpecific(IEnumerable<Affliction> afflictions, bool playSound, IEnumerable<DamageModifier> appliedDamageModifiers);
partial void AddDamageProjSpecific(bool playSound, AttackResult result);
public bool SectorHit(Vector2 armorSector, Vector2 simPosition)
{
@@ -582,7 +748,8 @@ namespace Barotrauma
public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1, Limb targetLimb = null)
{
attackResult = default(AttackResult);
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(SimPosition, attackSimPos));
Vector2 simPos = ragdoll.SimplePhysicsEnabled ? character.SimPosition : SimPosition;
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos));
bool wasRunning = attack.IsRunning;
attack.UpdateAttackTimer(deltaTime);
@@ -595,7 +762,7 @@ namespace Barotrauma
case HitDetection.Distance:
if (dist < attack.DamageRange)
{
structureBody = Submarine.PickBody(SimPosition, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true);
structureBody = Submarine.PickBody(simPos, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true);
if (damageTarget is Item i && i.GetComponent<Items.Components.Door>() != null)
{
// If the attack is aimed to an item and hits an item, it's successful.
@@ -689,6 +856,7 @@ namespace Barotrauma
{
if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) { continue; }
Limb limb = character.AnimController.Limbs[limbIndex];
if (limb.IsSevered) { continue; }
diff = attackSimPos - limb.SimPosition;
if (diff == Vector2.Zero) { continue; }
limb.body.ApplyTorque(limb.Mass * character.AnimController.Dir * attack.Torque * limb.Params.AttackForceMultiplier);
@@ -811,6 +979,30 @@ namespace Barotrauma
}
}
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
public void ApplyStatusEffects(ActionType actionType, float deltaTime)
{
foreach (StatusEffect statusEffect in statusEffects)
{
if (statusEffect.type != actionType) { continue; }
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
statusEffect.GetNearbyTargets(WorldPosition, targets);
statusEffect.Apply(ActionType.OnActive, deltaTime, character, targets);
}
else
{
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition);
}
statusEffect.Apply(actionType, deltaTime, character, this, WorldPosition);
}
}
}
public void Remove()
{
body?.Remove();
@@ -1,7 +1,7 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Barotrauma.IO;
using System;
using System.Linq;
using System.Xml.Linq;
@@ -72,6 +72,12 @@ namespace Barotrauma
[Editable, Serialize(true, true, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")]
public bool Flip { get; set; }
[Serialize(1f, true, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable]
public float FlipCooldown { get; set; }
[Serialize(0.5f, true, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable]
public float FlipDelay { get; set; }
[Serialize(10.0f, true, description: "How much force is used to move the head to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float HeadMoveForce { get; set; }
@@ -146,9 +152,18 @@ namespace Barotrauma
[Editable, Serialize(true, true, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")]
public bool Flip { get; set; }
[Serialize(1f, true, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable]
public float FlipCooldown { get; set; }
[Serialize(0.5f, true, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable]
public float FlipDelay { get; set; }
[Editable, Serialize(true, true, description: "If enabled, the character will simply be mirrored horizontally when it wants to turn around. If disabled, it will rotate itself to face the other direction.")]
public bool Mirror { get; set; }
[Editable, Serialize(true, true, description: "Disabling this will make mirroring instantaneous.")]
public bool MirrorLerp { get; set; }
[Serialize(5f, true), Editable]
public float WaveAmplitude { get; set; }
@@ -205,7 +220,6 @@ namespace Barotrauma
interface IFishAnimation
{
bool Flip { get; set; }
string FootAngles { get; set; }
Dictionary<int, float> FootAnglesInRadians { get; set; }
float TailAngle { get; set; }
@@ -214,5 +228,8 @@ namespace Barotrauma
float TorsoTorque { get; set; }
float TailTorque { get; set; }
float FootTorque { get; set; }
bool Flip { get; set; }
float FlipCooldown { get; set; }
float FlipDelay { get; set; }
}
}
@@ -25,10 +25,10 @@ namespace Barotrauma
[Serialize("", true, description: "If defined, different species of the same group are considered like the characters of the same species by the AI."), Editable]
public string Group { get; private set; }
[Serialize(false, true), Editable]
[Serialize(false, true), Editable(ReadOnly = true)]
public bool Humanoid { get; private set; }
[Serialize(false, true), Editable]
[Serialize(false, true), Editable(ReadOnly = true)]
public bool HasInfo { get; private set; }
[Serialize(false, true), Editable]
@@ -43,13 +43,13 @@ namespace Barotrauma
[Serialize(false, true, description: "Can the creature live without water or does it die on dry land?"), Editable]
public bool NeedsWater { get; set; }
[Serialize(false, true), Editable]
[Serialize(false, false), Editable]
public bool CanSpeak { get; set; }
[Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 1000f)]
[Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 100000f)]
public float Noise { get; set; }
[Serialize(100f, true, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 1000f)]
[Serialize(100f, true, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 100000f)]
public float Visibility { get; set; }
[Serialize("blood", true), Editable]
@@ -70,6 +70,9 @@ namespace Barotrauma
[Serialize(false, true), Editable]
public bool HideInSonar { get; set; }
[Serialize(0f, true), Editable]
public float SonarDisruption { get; set; }
public readonly string File;
public readonly List<SubParam> SubParams = new List<SubParam>();
@@ -474,8 +477,8 @@ namespace Barotrauma
[Serialize(true, true, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable()]
public bool EnforceAggressiveBehaviorForMissions { get; private set; }
[Serialize(false, true, description: "Should the character target or ignore walls when it's inside the submarine. Doesn't have any effect if no target priority for walls is defined."), Editable()]
public bool TargetInnerWalls { get; private set; }
[Serialize(true, true, description: "Should the character target or ignore walls when it's outside the submarine. Doesn't have any effect if no target priority for walls is defined."), Editable()]
public bool TargetOuterWalls { get; private set; }
[Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable()]
public bool RandomAttack { get; private set; }
@@ -1,8 +1,12 @@
using System.IO;
using System.Collections.Generic;
using System.Xml;
using System.Collections.Generic;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
#if DEBUG
using System.IO;
using System.Xml;
#else
using Barotrauma.IO;
#endif
namespace Barotrauma
{
@@ -75,7 +79,7 @@ namespace Barotrauma
Folder = Path.GetDirectoryName(FullPath);
}
public virtual bool Save(string fileNameWithoutExtension = null, XmlWriterSettings settings = null)
public virtual bool Save(string fileNameWithoutExtension = null, System.Xml.XmlWriterSettings settings = null)
{
if (!Directory.Exists(Folder))
{
@@ -85,7 +89,7 @@ namespace Barotrauma
Serialize();
if (settings == null)
{
settings = new XmlWriterSettings
settings = new System.Xml.XmlWriterSettings
{
Indent = true,
OmitXmlDeclaration = true,
@@ -3,7 +3,7 @@ using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
using System.IO;
using Barotrauma.IO;
using System.Xml;
using Barotrauma.Extensions;
#if CLIENT
@@ -470,6 +470,12 @@ namespace Barotrauma
[Serialize(true, true), Editable]
public bool CanBeSevered { get; set; }
[Serialize(0f, true, description:"Default 0 (Can't be severed when the creature is alive). Modifies the severance probability (defined per item/attack) when the character is alive. Currently only affects non-humanoid ragdolls. Also note that if CanBeSevered is false, this property doesn't have any effect."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)]
public float SeveranceProbabilityModifier { get; set; }
[Serialize("gore", true), Editable]
public string BreakSound { get; set; }
[Serialize(true, true), Editable]
public bool LimitEnabled { get; set; }
@@ -491,6 +497,9 @@ namespace Barotrauma
[Serialize(1f, true, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable]
public float Scale { get; set; }
[Serialize(false, false), Editable(ReadOnly = true)]
public bool WeldJoint { get; set; }
public JointParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
}
@@ -605,7 +614,11 @@ namespace Barotrauma
[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; }
// Non-editable ->
// TODO: make read-only
[Serialize(0, true)]
public int HealthIndex { get; set; }
@@ -936,7 +949,7 @@ namespace Barotrauma
{
public override string Name => "Light Texture";
[Serialize("", true), Editable]
[Serialize("Content/Lights/pointlight_bright.png", true), Editable]
public string Texture { get; private set; }
[Serialize("0.5, 0.5", true), Editable(DecimalCount = 2)]
@@ -128,7 +128,7 @@ namespace Barotrauma
if (Current == null)
{
DebugConsole.NewMessage("Now skill settings found in the selected content packages. Using default values.");
DebugConsole.NewMessage("No skill settings found in the selected content packages. Using default values.");
Current = new SkillSettings(null);
}
}
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using Barotrauma.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Xml.Linq;
@@ -53,7 +53,7 @@ namespace Barotrauma
//these types of files are included in the MD5 hash calculation,
//meaning that the players must have the exact same files to play together
private static HashSet<ContentType> multiplayerIncompatibleContent = new HashSet<ContentType>
public static HashSet<ContentType> MultiplayerIncompatibleContent { get; private set; } = new HashSet<ContentType>
{
ContentType.Jobs,
ContentType.Item,
@@ -161,7 +161,7 @@ namespace Barotrauma
public bool HasMultiplayerIncompatibleContent
{
get { return Files.Any(f => multiplayerIncompatibleContent.Contains(f.Type)); }
get { return Files.Any(f => MultiplayerIncompatibleContent.Contains(f.Type)); }
}
private ContentPackage()
@@ -414,7 +414,42 @@ namespace Barotrauma
doc.Root.Add(new XElement(file.Type.ToString(), new XAttribute("file", file.Path.CleanUpPathCrossPlatform())));
}
doc.Save(filePath);
doc.SaveSafe(filePath);
var packagesToDeselect = List.Where(p => p.Path.CleanUpPath() == Path.CleanUpPath()).ToList();
bool reselectPackage = false;
if (packagesToDeselect.Any())
{
foreach (var p in packagesToDeselect)
{
if (GameMain.Config.SelectedContentPackages.Contains(p))
{
reselectPackage = true;
if (p.CorePackage)
{
GameMain.Config.SelectCorePackage(List.Find(cpp => cpp.CorePackage && !packagesToDeselect.Contains(cpp)));
}
else
{
GameMain.Config.DeselectContentPackage(p);
}
}
List.Remove(p);
}
List.Add(this);
if (reselectPackage)
{
if (CorePackage)
{
GameMain.Config.SelectCorePackage(this);
}
else
{
GameMain.Config.SelectContentPackage(this);
}
}
}
}
public void CalculateHash(bool logging = false)
@@ -428,7 +463,7 @@ namespace Barotrauma
foreach (ContentFile file in Files)
{
if (!multiplayerIncompatibleContent.Contains(file.Type)) { continue; }
if (!MultiplayerIncompatibleContent.Contains(file.Type)) { continue; }
try
{
@@ -539,7 +574,7 @@ namespace Barotrauma
while (true)
{
string temp = System.IO.Path.GetDirectoryName(path);
string temp = Barotrauma.IO.Path.GetDirectoryName(path);
if (string.IsNullOrEmpty(temp)) { break; }
path = temp;
}
@@ -580,7 +615,7 @@ namespace Barotrauma
}
}
string[] files = Directory.GetFiles(folder, "*.xml");
IEnumerable<string> files = Directory.GetFiles(folder, "*.xml");
List.Clear();
@@ -589,12 +624,12 @@ namespace Barotrauma
List.Add(new ContentPackage(filePath));
}
string[] modDirectories = Directory.GetDirectories("Mods");
IEnumerable<string> modDirectories = Directory.GetDirectories("Mods");
foreach (string modDirectory in modDirectories)
{
if (System.IO.Path.GetFileName(modDirectory.TrimEnd(System.IO.Path.DirectorySeparatorChar)) == "ExampleMod") { continue; }
string modFilePath = System.IO.Path.Combine(modDirectory, Steam.SteamManager.MetadataFileName);
string copyingFilePath = System.IO.Path.Combine(modDirectory, Steam.SteamManager.CopyIndicatorFileName);
if (Barotrauma.IO.Path.GetFileName(modDirectory.TrimEnd(Barotrauma.IO.Path.DirectorySeparatorChar)) == "ExampleMod") { continue; }
string modFilePath = Barotrauma.IO.Path.Combine(modDirectory, Steam.SteamManager.MetadataFileName);
string copyingFilePath = Barotrauma.IO.Path.Combine(modDirectory, Steam.SteamManager.CopyIndicatorFileName);
if (File.Exists(copyingFilePath))
{
//this mod didn't clean up its copying file; assume it's corrupted and delete it
@@ -615,22 +650,30 @@ namespace Barotrauma
public static void SortContentPackages()
{
List = List
.OrderByDescending(p => p.CorePackage)
.ThenBy(p => List.IndexOf(p))
.ToList();
if (GameMain.Config != null)
{
List = List
.OrderByDescending(p => p.CorePackage)
.ThenBy(p => GameMain.Config.SelectedContentPackages.IndexOf(p))
.ThenBy(p => List.IndexOf(p))
.ToList();
var sortedSelected = GameMain.Config.SelectedContentPackages
.OrderByDescending(p => p.CorePackage)
.ThenBy(p => List.IndexOf(p))
.ThenBy(p => GameMain.Config.SelectedContentPackages.IndexOf(p))
.ToList();
GameMain.Config.SelectedContentPackages.Clear(); GameMain.Config.SelectedContentPackages.AddRange(sortedSelected);
var reportList = List.Where(p => GameMain.Config.SelectedContentPackages.Contains(p));
var reportList = GameMain.Config.SelectedContentPackages;
DebugConsole.NewMessage($"Content package load order: { string.Join(" | ", reportList.Select(cp => cp.Name)) }");
}
else
{
List = List
.OrderByDescending(p => p.CorePackage)
.ThenBy(p => List.IndexOf(p))
.ToList();
}
}
public void Delete()
@@ -8,7 +8,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using Barotrauma.IO;
using System.Linq;
using System.Text;
@@ -292,7 +292,7 @@ namespace Barotrauma
commands.Add(new Command("startwhenclientsready", "startwhenclientsready [true/false]: Enable or disable automatically starting the round when clients are ready to start.", null));
commands.Add(new Command("giveperm", "giveperm [id]: Grants administrative permissions to the player with the specified client ID.", null,
commands.Add(new Command("giveperm", "giveperm [id/steamid/endpoint/name]: Grants administrative permissions to the specified client.", null,
() =>
{
if (GameMain.NetworkMember == null) return null;
@@ -304,7 +304,7 @@ namespace Barotrauma
};
}));
commands.Add(new Command("revokeperm", "revokeperm [id]: Revokes administrative permissions to the player with the specified client ID.", null,
commands.Add(new Command("revokeperm", "revokeperm [id/steamid/endpoint/name]: Revokes administrative permissions from the specified client.", null,
() =>
{
if (GameMain.NetworkMember == null) return null;
@@ -316,7 +316,7 @@ namespace Barotrauma
};
}));
commands.Add(new Command("giverank", "giverank [id]: Assigns a specific rank (= a set of administrative permissions) to the player with the specified client ID.", null,
commands.Add(new Command("giverank", "giverank [id/steamid/endpoint/name]: Assigns a specific rank (= a set of administrative permissions) to the specified client.", null,
() =>
{
if (GameMain.NetworkMember == null) return null;
@@ -328,12 +328,41 @@ namespace Barotrauma
};
}));
commands.Add(new Command("givecommandperm", "givecommandperm [id]: Gives the player with the specified client ID the permission to use the specified console commands.", null));
commands.Add(new Command("givecommandperm", "givecommandperm [id/steamid/endpoint/name]: Gives the specified client the permission to use the specified console commands.", null,
() =>
{
if (GameMain.NetworkMember == null) return null;
return new string[][]
{
GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
commands.Select(c => c.names[0]).ToArray()
};
}));
commands.Add(new Command("revokecommandperm", "revokecommandperm [id/steamid/endpoint/name]: Revokes permission to use the specified console commands from the specified client.", null,
() =>
{
if (GameMain.NetworkMember == null) return null;
return new string[][]
{
GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
new string[0]
};
}));
commands.Add(new Command("showperm", "showperm [id/steamid/endpoint/name]: Shows the current administrative permissions of the specified client.", null,
() =>
{
if (GameMain.NetworkMember == null) return null;
return new string[][]
{
GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray()
};
}));
commands.Add(new Command("revokecommandperm", "revokecommandperm [id]: Revokes permission to use the specified console commands from the player with the specified client ID.", null));
commands.Add(new Command("showperm", "showperm [id]: Shows the current administrative permissions of the client with the specified client ID.", null));
commands.Add(new Command("respawnnow", "respawnnow: Trigger a respawn immediately if there are any clients waiting to respawn.", null));
commands.Add(new Command("showkarma", "showkarma: Show the current karma values of the players.", null));
@@ -692,7 +721,7 @@ namespace Barotrauma
}
},null));
commands.Add(new Command("teleportsub", "teleportsub [start/end]: Teleport the submarine to the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) =>
commands.Add(new Command("teleportsub", "teleportsub [start/end/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) =>
{
if (Submarine.MainSub == null || Level.Loaded == null) return;
@@ -975,6 +1004,22 @@ namespace Barotrauma
}
}));
commands.Add(new Command("money", "", args =>
{
if (args.Length == 0) { return; }
if (GameMain.GameSession.GameMode is CampaignMode campaign)
{
if (int.TryParse(args[0], out int money))
{
campaign.Money += money;
}
else
{
ThrowError($"\"{args[0]}\" is not a valid numeric value.");
}
}
}, isCheat: true));
commands.Add(new Command("difficulty|leveldifficulty", "difficulty [0-100]: Change the level difficulty setting in the server lobby.", null));
commands.Add(new Command("autoitemplacerdebug|outfitdebug", "autoitemplacerdebug: Toggle automatic item placer debug info on/off. The automatically placed items are listed in the debug console at the start of a round.", (string[] args) =>
@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma
namespace Barotrauma
{
public enum TransitionMode
{
@@ -13,4 +9,17 @@ namespace Barotrauma
EaseOut,
Exponential
}
public enum ActionType
{
Always, OnPicked, OnUse, OnSecondaryUse,
OnWearing, OnContaining, OnContained, OnNotContained,
OnActive, OnFailure, OnBroken,
OnFire, InWater, NotInWater,
OnImpact,
OnEating,
OnDeath = OnBroken,
OnDamaged,
OnSevered
}
}
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -9,6 +10,8 @@ namespace Barotrauma
{
const float IntensityUpdateInterval = 5.0f;
const float CalculateDistanceTraveledInterval = 5.0f;
private Level level;
private readonly List<Sprite> preloadedSprites = new List<Sprite>();
@@ -30,6 +33,11 @@ namespace Barotrauma
private float intensityUpdateTimer;
private PathFinder pathFinder;
private float totalPathLength;
private float calculateDistanceTraveledTimer;
private float distanceTraveled;
private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger;
private float roundDuration;
@@ -72,6 +80,10 @@ namespace Barotrauma
pendingEventSets.Clear();
selectedEvents.Clear();
pathFinder = new PathFinder(WayPoint.WayPointList, indoorsSteering: false);
var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(Level.Loaded.StartPosition), ConvertUnits.ToSimUnits(Level.Loaded.EndPosition));
totalPathLength = steeringPath.TotalLength;
this.level = level;
SelectSettings();
@@ -137,7 +149,44 @@ namespace Barotrauma
public void PreloadContent(IEnumerable<ContentFile> contentFiles)
{
foreach (ContentFile file in contentFiles)
var filesToPreload = new List<ContentFile>(contentFiles);
foreach (Submarine sub in Submarine.Loaded)
{
if (sub.WreckAI == null) { continue; }
if (!string.IsNullOrEmpty(sub.WreckAI.Config.DefensiveAgent))
{
var prefab = CharacterPrefab.FindBySpeciesName(sub.WreckAI.Config.DefensiveAgent);
if (prefab != null && !filesToPreload.Any(f => f.Path == prefab.FilePath))
{
filesToPreload.Add(new ContentFile(prefab.FilePath, ContentType.Character));
}
}
foreach (Item item in Item.ItemList)
{
if (item.Submarine != sub) { continue; }
foreach (Items.Components.ItemComponent component in item.Components)
{
if (component.statusEffectLists == null) { continue; }
foreach (var statusEffectList in component.statusEffectLists.Values)
{
foreach (StatusEffect statusEffect in statusEffectList)
{
foreach (var spawnInfo in statusEffect.SpawnCharacters)
{
var prefab = CharacterPrefab.FindBySpeciesName(spawnInfo.SpeciesName);
if (prefab != null && !filesToPreload.Any(f => f.Path == prefab.FilePath))
{
filesToPreload.Add(new ContentFile(prefab.FilePath, ContentType.Character));
}
}
}
}
}
}
}
foreach (ContentFile file in filesToPreload)
{
switch (file.Type)
{
@@ -299,12 +348,9 @@ namespace Barotrauma
private bool CanStartEventSet(ScriptedEventSet eventSet)
{
float distFromStart = Vector2.Distance(Submarine.MainSub.WorldPosition, level.StartPosition);
float distFromEnd = Vector2.Distance(Submarine.MainSub.WorldPosition, level.EndPosition);
float distanceTraveled = MathHelper.Clamp(
(Submarine.MainSub.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X),
0.0f, 1.0f);
ISpatialEntity refEntity = GetRefEntity();
float distFromStart = Vector2.Distance(refEntity.WorldPosition, level.StartPosition);
float distFromEnd = Vector2.Distance(refEntity.WorldPosition, level.EndPosition);
//don't create new events if within 50 meters of the start/end of the level
if (!eventSet.AllowAtStart)
@@ -367,6 +413,13 @@ namespace Barotrauma
}
}
calculateDistanceTraveledTimer -= deltaTime;
if (calculateDistanceTraveledTimer <= 0.0f)
{
distanceTraveled = CalculateDistanceTraveled();
calculateDistanceTraveledTimer = CalculateDistanceTraveledInterval;
}
eventThreshold += settings.EventThresholdIncrease * deltaTime;
if (eventCoolDown > 0.0f)
{
@@ -514,5 +567,62 @@ namespace Barotrauma
currentIntensity = MathHelper.Max(0.0025f * IntensityUpdateInterval, targetIntensity);
}
}
private float CalculateDistanceTraveled()
{
var refEntity = GetRefEntity();
Vector2 target = ConvertUnits.ToSimUnits(Level.Loaded.EndPosition);
var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(refEntity.WorldPosition), target);
if (steeringPath.Unreachable || float.IsPositiveInfinity(totalPathLength))
{
//use horizontal position in the level as a fallback if a path can't be found
return MathHelper.Clamp((refEntity.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), 0.0f, 1.0f);
}
else
{
return MathHelper.Clamp(1.0f - steeringPath.TotalLength / totalPathLength, 0.0f, 1.0f);
}
}
/// <summary>
/// Get the entity that should be used in determining how far the player has progressed in the level.
/// = The submarine or player character that has progressed the furthest.
/// </summary>
private ISpatialEntity GetRefEntity()
{
ISpatialEntity refEntity = Submarine.MainSub;
#if CLIENT
if (Character.Controlled != null)
{
if (Character.Controlled.Submarine != null &&
Character.Controlled.Submarine.Info.Type == SubmarineInfo.SubmarineType.Player)
{
refEntity = Character.Controlled.Submarine;
}
else
{
refEntity = Character.Controlled;
}
}
#else
foreach (Barotrauma.Networking.Client client in GameMain.Server.ConnectedClients)
{
if (client.Character == null) { continue; }
//only take the players inside a player sub into account.
//Otherwise the system could be abused by for example making a respawned player wait
//close to the destination outpost
if (client.Character.Submarine != null &&
client.Character.Submarine.Info.Type == SubmarineInfo.SubmarineType.Player)
{
if (client.Character.Submarine.WorldPosition.X > refEntity.WorldPosition.X)
{
refEntity = client.Character.Submarine;
}
}
}
#endif
return refEntity;
}
}
}
@@ -24,8 +24,9 @@ namespace Barotrauma
public readonly float MinLevelDifficulty = 0.0f;
public readonly float MaxLevelDifficulty = 100.0f;
static EventManagerSettings()
public static void Init()
{
List.Clear();
foreach (ContentFile file in GameMain.Instance.GetFilesOfType(ContentType.EventManagerSettings))
{
Load(file);
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -10,6 +11,8 @@ namespace Barotrauma
private readonly XElement itemConfig;
private readonly List<Item> items = new List<Item>();
private readonly Dictionary<Item, UInt16> itemIDs = new Dictionary<Item, UInt16>();
private readonly Dictionary<Item, UInt16> parentInventoryIDs = new Dictionary<Item, UInt16>();
private int requiredDeliveryAmount;
@@ -22,8 +25,6 @@ namespace Barotrauma
private void InitItems()
{
items.Clear();
if (itemConfig == null)
{
DebugConsole.ThrowError("Failed to initialize items for cargo mission (itemConfig == null)");
@@ -91,8 +92,13 @@ namespace Barotrauma
var item = new Item(itemPrefab, position, cargoRoom.Submarine);
item.FindHull();
items.Add(item);
if (parent != null) parent.Combine(item, user: null);
itemIDs.Add(item, item.ID);
if (parent != null)
{
parentInventoryIDs.Add(item, parent.ID);
parent.Combine(item, user: null);
}
foreach (XElement subElement in element.Elements())
{
@@ -106,6 +112,10 @@ namespace Barotrauma
public override void Start(Level level)
{
items.Clear();
itemIDs.Clear();
parentInventoryIDs.Clear();
if (!IsClient)
{
InitItems();
@@ -108,23 +108,6 @@ namespace Barotrauma
subs[1].SetPosition(subs[1].FindSpawnPos(Level.Loaded.EndPosition));
subs[1].FlipX();
//prevent wifi components from communicating between subs
List<WifiComponent> wifiComponents = new List<WifiComponent>();
foreach (Item item in Item.ItemList)
{
wifiComponents.AddRange(item.GetComponents<WifiComponent>());
}
foreach (WifiComponent wifiComponent in wifiComponents)
{
for (int i = 0; i < 2; i++)
{
if (wifiComponent.Item.Submarine == subs[i] || subs[i].ConnectedDockingPorts.ContainsKey(wifiComponent.Item.Submarine))
{
wifiComponent.TeamID = subs[i].TeamID;
}
}
}
crews = new List<Character>[] { new List<Character>(), new List<Character>() };
foreach (Submarine submarine in Submarine.Loaded)
@@ -70,7 +70,7 @@ namespace Barotrauma
monsterFiles.Add(new Tuple<string, Point>(monster, new Point(min, max)));
}
description = description.Replace("[monster]",
TextManager.Get("character." + System.IO.Path.GetFileNameWithoutExtension(monsterFileName)));
TextManager.Get("character." + Barotrauma.IO.Path.GetFileNameWithoutExtension(monsterFileName)));
}
public override void Start(Level level)
@@ -103,6 +103,10 @@ namespace Barotrauma
public override void Start(Level level)
{
#if SERVER
originalItemID = Entity.NullEntityID;
originalInventoryID = Entity.NullEntityID;
#endif
if (!IsClient)
{
//ruin/wreck items are allowed to spawn close to the sub
@@ -147,6 +151,9 @@ namespace Barotrauma
item.body.FarseerBody.BodyType = BodyType.Kinematic;
item.FindHull();
}
#if SERVER
originalItemID = item.ID;
#endif
for (int i = 0; i < statusEffects.Count; i++)
{
@@ -166,6 +173,7 @@ namespace Barotrauma
foreach (Item it in Item.ItemList)
{
if (!it.HasTag(containerTag)) { continue; }
if (it.NonInteractable) { continue; }
switch (spawnPositionType)
{
case Level.PositionType.Cave:
@@ -181,7 +189,13 @@ namespace Barotrauma
}
var itemContainer = it.GetComponent<Items.Components.ItemContainer>();
if (itemContainer == null) { continue; }
if (itemContainer.Combine(item, user: null)) { break; } // Placement successful
if (itemContainer.Combine(item, user: null))
{
#if SERVER
originalInventoryID = it.ID;
#endif
break;
} // Placement successful
}
}
}
@@ -13,6 +13,9 @@ namespace Barotrauma
private readonly int minAmount, maxAmount;
private List<Character> monsters;
private readonly float scatter;
private readonly float offset;
private readonly bool spawnDeep;
private Vector2? spawnPos;
@@ -72,6 +75,8 @@ namespace Barotrauma
}
spawnDeep = prefab.ConfigElement.GetAttributeBool("spawndeep", false);
offset = prefab.ConfigElement.GetAttributeFloat("offset", 0);
scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 1000), 0, 3000);
if (GameMain.NetworkMember != null)
{
@@ -118,7 +123,7 @@ namespace Barotrauma
private List<Level.InterestingPosition> GetAvailableSpawnPositions()
{
var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => spawnPosType.HasFlag(p.PositionType) && !Level.Loaded.UsedPositions.Contains(p));
var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => spawnPosType.HasFlag(p.PositionType));
var removals = new List<Level.InterestingPosition>();
foreach (var position in availablePositions)
{
@@ -169,10 +174,6 @@ namespace Barotrauma
if (Rand.Value(Rand.RandSync.Server) > prefab.SpawnProbability)
{
removedPositions.Add(position);
if (prefab.AllowOnlyOnce)
{
Level.Loaded.UsedPositions.Add(position);
}
}
}
removedPositions.ForEach(p => availablePositions.Remove(p));
@@ -245,11 +246,34 @@ namespace Barotrauma
spawnPos = spawnPoint.WorldPosition;
}
}
spawnPending = true;
if (prefab.AllowOnlyOnce)
else if (chosenPosition.PositionType == Level.PositionType.MainPath && offset > 0)
{
Level.Loaded.UsedPositions.Add(chosenPosition);
Vector2 dir;
var waypoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == null);
var nearestWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, spawnPos.Value)).FirstOrDefault();
if (nearestWaypoint != null)
{
int currentIndex = waypoints.IndexOf(nearestWaypoint);
var nextWaypoint = waypoints[Math.Min(currentIndex + 20, waypoints.Count - 1)];
dir = Vector2.Normalize(nextWaypoint.WorldPosition - nearestWaypoint.WorldPosition);
// Ensure that the spawn position is not offset to the left.
if (dir.X < 0)
{
dir.X = 0;
}
}
else
{
dir = new Vector2(1, Rand.Range(-1, 1));
}
Vector2 targetPos = spawnPos.Value + dir * offset;
var targetWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, targetPos)).FirstOrDefault();
if (targetWaypoint != null)
{
spawnPos = targetWaypoint.WorldPosition;
}
}
spawnPending = true;
}
}
@@ -278,11 +302,14 @@ namespace Barotrauma
if (spawnPending)
{
//wait until there are no submarines at the spawnpos
foreach (Submarine submarine in Submarine.Loaded)
if (spawnPosType == Level.PositionType.MainPath)
{
if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
float minDist = GetMinDistanceToSub(submarine);
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; }
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
float minDist = GetMinDistanceToSub(submarine);
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; }
}
}
//if spawning in a ruin/cave, wait for someone to be close to it to spawning
@@ -319,7 +346,7 @@ namespace Barotrauma
//+1 because Range returns an integer less than the max value
int amount = Rand.Range(minAmount, maxAmount + 1);
monsters = new List<Character>();
float offsetAmount = spawnPosType == Level.PositionType.MainPath ? 1000 : 100;
float offsetAmount = spawnPosType == Level.PositionType.MainPath ? scatter : 100;
for (int i = 0; i < amount; i++)
{
CoroutineManager.InvokeAfter(() =>
@@ -329,7 +356,22 @@ namespace Barotrauma
System.Diagnostics.Debug.Assert(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer, "Clients should not create monster events.");
monsters.Add(Character.Create(speciesName, spawnPos.Value + Rand.Vector(offsetAmount), Level.Loaded.Seed + i.ToString(), null, false, true, true));
Vector2 pos = spawnPos.Value + Rand.Vector(offsetAmount);
if (spawnPosType == Level.PositionType.MainPath)
{
if (Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(pos)))
{
// Can't use the offset position, let's use the exact spawn position.
pos = spawnPos.Value;
}
else if (Level.Loaded.Ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).ContainsWorld(pos)))
{
// Can't use the offset position, let's use the exact spawn position.
pos = spawnPos.Value;
}
}
monsters.Add(Character.Create(speciesName, pos, Level.Loaded.Seed + i.ToString(), null, false, true, true));
if (monsters.Count == amount)
{
@@ -11,7 +11,6 @@ namespace Barotrauma
public readonly Type EventType;
public readonly string MusicType;
public readonly float SpawnProbability;
public readonly bool AllowOnlyOnce;
public float Commonness;
public ScriptedEventPrefab(XElement element)
@@ -34,7 +33,6 @@ namespace Barotrauma
}
Commonness = element.GetAttributeFloat("commonness", 1.0f);
SpawnProbability = Math.Clamp(element.GetAttributeFloat("spawnprobability", 1.0f), 0, 1);
AllowOnlyOnce = element.GetAttributeBool("allowonlyonce", false);
}
public ScriptedEvent CreateInstance()
@@ -1,13 +1,28 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices.ComTypes;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
{
class ScriptedEventSet
{
internal class EventDebugStats
{
public readonly ScriptedEventSet RootSet;
public readonly Dictionary<string, int> MonsterCounts = new Dictionary<string, int>();
public EventDebugStats(ScriptedEventSet rootSet)
{
RootSet = rootSet;
}
}
public static List<ScriptedEventSet> List
{
get;
@@ -131,5 +146,115 @@ namespace Barotrauma
}
}
}
public static List<string> GetDebugStatistics(int simulatedRoundCount = 100)
{
List<string> debugLines = new List<string>();
foreach (var eventSet in List)
{
List<EventDebugStats> stats = new List<EventDebugStats>();
for (int i = 0; i < simulatedRoundCount; i++)
{
var newStats = new EventDebugStats(eventSet);
CheckEventSet(newStats, eventSet);
stats.Add(newStats);
}
debugLines.Add($"Event stats ({eventSet.DebugIdentifier}): ");
LogEventStats(stats, debugLines);
}
for (int difficulty = 0; difficulty <= 100; difficulty += 10)
{
debugLines.Add($"Event stats on difficulty level {difficulty}: ");
List<EventDebugStats> stats = new List<EventDebugStats>();
for (int i = 0; i < simulatedRoundCount; i++)
{
ScriptedEventSet selectedSet = List.Where(s => difficulty >= s.MinLevelDifficulty && difficulty <= s.MaxLevelDifficulty).GetRandom();
if (selectedSet == null) { continue; }
var newStats = new EventDebugStats(selectedSet);
CheckEventSet(newStats, selectedSet);
stats.Add(newStats);
}
LogEventStats(stats, debugLines);
}
return debugLines;
static void CheckEventSet(EventDebugStats stats, ScriptedEventSet thisSet)
{
if (thisSet.ChooseRandom)
{
var eventPrefab = ToolBox.SelectWeightedRandom(thisSet.EventPrefabs, thisSet.EventPrefabs.Select(e => e.Commonness).ToList(), Rand.RandSync.Unsynced);
if (eventPrefab != null)
{
AddEvent(stats, eventPrefab);
}
}
else
{
foreach (var eventPrefab in thisSet.EventPrefabs)
{
AddEvent(stats, eventPrefab);
}
}
foreach (var childSet in thisSet.ChildSets)
{
CheckEventSet(stats, childSet);
}
}
static void AddEvent(EventDebugStats stats, ScriptedEventPrefab eventPrefab)
{
if (eventPrefab.EventType == typeof(MonsterEvent))
{
float spawnProbability = eventPrefab.ConfigElement.GetAttributeFloat("spawnprobability", 1.0f);
if (Rand.Value(Rand.RandSync.Server) > spawnProbability)
{
return;
}
string character = eventPrefab.ConfigElement.GetAttributeString("characterfile", "");
System.Diagnostics.Debug.Assert(!string.IsNullOrEmpty(character));
int amount = eventPrefab.ConfigElement.GetAttributeInt("amount", 0);
int minAmount = eventPrefab.ConfigElement.GetAttributeInt("minamount", amount);
int maxAmount = eventPrefab.ConfigElement.GetAttributeInt("maxamount", amount);
int count = Rand.Range(minAmount, maxAmount + 1);
if (count <= 0) { return; }
if (!stats.MonsterCounts.ContainsKey(character)) { stats.MonsterCounts[character] = 0; }
stats.MonsterCounts[character] += count;
}
}
static void LogEventStats(List<EventDebugStats> stats, List<string> debugLines)
{
if (stats.Count == 0 || stats.All(s => s.MonsterCounts.Values.Sum() == 0))
{
debugLines.Add(" No monster spawns");
debugLines.Add($" ");
}
else
{
stats.Sort((s1, s2) => { return s1.MonsterCounts.Values.Sum().CompareTo(s2.MonsterCounts.Values.Sum()); });
EventDebugStats minStats = stats.First();
EventDebugStats maxStats = stats.First();
debugLines.Add($" Minimum monster spawns: {stats.First().MonsterCounts.Values.Sum()}");
debugLines.Add($" {LogMonsterCounts(stats.First())}");
debugLines.Add($" Median monster spawns: {stats[stats.Count / 2].MonsterCounts.Values.Sum()}");
debugLines.Add($" {LogMonsterCounts(stats[stats.Count / 2])}");
debugLines.Add($" Maximum monster spawns: {stats.Last().MonsterCounts.Values.Sum()}");
debugLines.Add($" {LogMonsterCounts(stats.Last())}");
debugLines.Add($" ");
}
}
static string LogMonsterCounts(EventDebugStats stats)
{
return string.Join(", ", stats.MonsterCounts.Select(mc => mc.Key + " x " + mc.Value));
}
}
}
}
@@ -32,13 +32,22 @@ namespace Barotrauma.Extensions
public static T GetRandom<T>(this IEnumerable<T> source, Func<T, bool> predicate, Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
if (predicate == null) { return GetRandom(source, randSync); }
return source.Where(predicate).GetRandom(randSync);
}
public static T GetRandom<T>(this IEnumerable<T> source, Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
int count = source.Count();
return count == 0 ? default(T) : source.ElementAt(Rand.Range(0, count, randSync));
if (source is IList<T> list)
{
int count = list.Count;
return count == 0 ? default : list[Rand.Range(0, count, randSync)];
}
else
{
int count = source.Count();
return count == 0 ? default : source.ElementAt(Rand.Range(0, count, randSync));
}
}
/// <summary>
@@ -2,7 +2,7 @@
using System;
using System.Text;
using System.Collections.Generic;
using System.IO;
using Barotrauma.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
@@ -174,6 +174,10 @@ namespace Barotrauma
}
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine);
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = validContainer.Key.Item.Submarine.TeamID;
}
spawnedItems.Add(item);
#if SERVER
Entity.Spawner.CreateNetworkEvent(item, remove: false);
@@ -4,7 +4,7 @@ using System;
using System.Linq;
using System.Xml.Linq;
using System.Collections.Generic;
using System.IO;
using Barotrauma.IO;
namespace Barotrauma
{
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.IO;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -231,7 +232,7 @@ namespace Barotrauma
if (port.Item.WorldPosition.Y < Submarine.WorldPosition.Y) { continue; }
float dist = Vector2.DistanceSquared(port.Item.WorldPosition, level.StartOutpost.WorldPosition);
if (myPort == null || dist < closestDistance || (port.MainDockingPort && !myPort.MainDockingPort))
if ((myPort == null || dist < closestDistance || port.MainDockingPort) && !(myPort?.MainDockingPort ?? false))
{
myPort = port;
closestDistance = dist;
@@ -463,7 +464,7 @@ namespace Barotrauma
try
{
doc.Save(filePath);
doc.SaveSafe(filePath);
}
catch (Exception e)
{
@@ -2,8 +2,7 @@
using System.Xml.Linq;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using System.Xml;
using System.IO;
using Barotrauma.IO;
using Barotrauma.Extensions;
#if CLIENT
using Microsoft.Xna.Framework.Input;
@@ -37,6 +36,8 @@ namespace Barotrauma
public bool VSyncEnabled { get; set; }
public bool TextureCompressionEnabled { get; set; }
public bool EnableSplashScreen { get; set; }
public int ParticleLimit { get; set; }
@@ -66,6 +67,7 @@ namespace Barotrauma
#if CLIENT
private KeyOrMouse[] keyMapping;
private KeyOrMouse[] inventoryKeyMapping;
#endif
private WindowMode windowMode;
@@ -207,7 +209,7 @@ namespace Barotrauma
{
musicVolume = MathHelper.Clamp(value, 0.0f, 1.0f);
#if CLIENT
GameMain.SoundManager?.SetCategoryGainMultiplier("music", musicVolume, 0);
GameMain.SoundManager?.SetCategoryGainMultiplier("music", musicVolume * 0.7f, 0);
#endif
}
}
@@ -267,7 +269,7 @@ namespace Barotrauma
public bool TextManagerDebugModeEnabled { get; set; }
#endif
private FileSystemWatcher modsFolderWatcher;
private System.IO.FileSystemWatcher modsFolderWatcher;
private int ContentFileLoadOrder(ContentFile a)
{
@@ -301,58 +303,11 @@ namespace Barotrauma
!otherCorePackage.Files.Any(f2 =>
Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList();
bool shouldRefreshSubs = false;
bool shouldRefreshFabricationRecipes = false;
bool shouldRefreshSoundPlayer = false;
bool shouldRefreshRuinGenerationParams = false;
bool shouldRefreshScriptedEventSets = false;
bool shouldRefreshMissionPrefabs = false;
bool shouldRefreshLevelObjectPrefabs = false;
bool shouldRefreshLocationTypes = false;
bool shouldRefreshMapGenerationParams = false;
bool shouldRefreshLevelGenerationParams = false;
bool shouldRefreshAfflictions = false;
DisableContentPackageItems(filesToRemove.OrderBy(ContentFileLoadOrder));
DisableContentPackageItems(filesToRemove.OrderBy(ContentFileLoadOrder),
ref shouldRefreshSubs,
ref shouldRefreshFabricationRecipes,
ref shouldRefreshSoundPlayer,
ref shouldRefreshRuinGenerationParams,
ref shouldRefreshScriptedEventSets,
ref shouldRefreshMissionPrefabs,
ref shouldRefreshLevelObjectPrefabs,
ref shouldRefreshLocationTypes,
ref shouldRefreshMapGenerationParams,
ref shouldRefreshLevelGenerationParams,
ref shouldRefreshAfflictions);
EnableContentPackageItems(filesToAdd.OrderBy(ContentFileLoadOrder));
EnableContentPackageItems(filesToAdd.OrderBy(ContentFileLoadOrder),
ref shouldRefreshSubs,
ref shouldRefreshFabricationRecipes,
ref shouldRefreshSoundPlayer,
ref shouldRefreshRuinGenerationParams,
ref shouldRefreshScriptedEventSets,
ref shouldRefreshMissionPrefabs,
ref shouldRefreshLevelObjectPrefabs,
ref shouldRefreshLocationTypes,
ref shouldRefreshMapGenerationParams,
ref shouldRefreshLevelGenerationParams,
ref shouldRefreshAfflictions);
if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); }
if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); }
if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); }
if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); }
if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); }
if (shouldRefreshMissionPrefabs) { MissionPrefab.Init(); }
if (shouldRefreshLevelObjectPrefabs) { LevelObjectPrefab.LoadAll(); }
if (shouldRefreshLocationTypes) { LocationType.Init(); }
if (shouldRefreshMapGenerationParams) { MapGenerationParams.Init(); }
if (shouldRefreshLevelGenerationParams) { LevelGenerationParams.LoadPresets(); }
#if CLIENT
if (shouldRefreshSoundPlayer) { SoundPlayer.Init().ForEach(_ => { return; }); }
#endif
RefreshContentPackageItems(filesToAdd.Concat(filesToRemove));
}
public void SelectContentPackage(ContentPackage contentPackage)
@@ -362,45 +317,9 @@ namespace Barotrauma
SelectedContentPackages.Add(contentPackage);
ContentPackage.SortContentPackages();
bool shouldRefreshSubs = false;
bool shouldRefreshFabricationRecipes = false;
bool shouldRefreshSoundPlayer = false;
bool shouldRefreshRuinGenerationParams = false;
bool shouldRefreshScriptedEventSets = false;
bool shouldRefreshMissionPrefabs = false;
bool shouldRefreshLevelObjectPrefabs = false;
bool shouldRefreshLocationTypes = false;
bool shouldRefreshMapGenerationParams = false;
bool shouldRefreshLevelGenerationParams = false;
bool shouldRefreshAfflictions = false;
EnableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder));
EnableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder),
ref shouldRefreshSubs,
ref shouldRefreshFabricationRecipes,
ref shouldRefreshSoundPlayer,
ref shouldRefreshRuinGenerationParams,
ref shouldRefreshScriptedEventSets,
ref shouldRefreshMissionPrefabs,
ref shouldRefreshLevelObjectPrefabs,
ref shouldRefreshLocationTypes,
ref shouldRefreshMapGenerationParams,
ref shouldRefreshLevelGenerationParams,
ref shouldRefreshAfflictions);
if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); }
if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); }
if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); }
if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); }
if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); }
if (shouldRefreshMissionPrefabs) { MissionPrefab.Init(); }
if (shouldRefreshLevelObjectPrefabs) { LevelObjectPrefab.LoadAll(); }
if (shouldRefreshLocationTypes) { LocationType.Init(); }
if (shouldRefreshMapGenerationParams) { MapGenerationParams.Init(); }
if (shouldRefreshLevelGenerationParams) { LevelGenerationParams.LoadPresets(); }
#if CLIENT
if (shouldRefreshSoundPlayer) { SoundPlayer.Init().ForEach(_ => { return; }); }
#endif
RefreshContentPackageItems(contentPackage.Files);
}
}
@@ -411,61 +330,14 @@ namespace Barotrauma
SelectedContentPackages.Remove(contentPackage);
ContentPackage.SortContentPackages();
bool shouldRefreshSubs = false;
bool shouldRefreshFabricationRecipes = false;
bool shouldRefreshSoundPlayer = false;
bool shouldRefreshRuinGenerationParams = false;
bool shouldRefreshScriptedEventSets = false;
bool shouldRefreshMissionPrefabs = false;
bool shouldRefreshLevelObjectPrefabs = false;
bool shouldRefreshLocationTypes = false;
bool shouldRefreshMapGenerationParams = false;
bool shouldRefreshLevelGenerationParams = false;
bool shouldRefreshAfflictions = false;
DisableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder));
DisableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder),
ref shouldRefreshSubs,
ref shouldRefreshFabricationRecipes,
ref shouldRefreshSoundPlayer,
ref shouldRefreshRuinGenerationParams,
ref shouldRefreshScriptedEventSets,
ref shouldRefreshMissionPrefabs,
ref shouldRefreshLevelObjectPrefabs,
ref shouldRefreshLocationTypes,
ref shouldRefreshMapGenerationParams,
ref shouldRefreshLevelGenerationParams,
ref shouldRefreshAfflictions);
if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); }
if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); }
if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); }
if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); }
if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); }
if (shouldRefreshMissionPrefabs) { MissionPrefab.Init(); }
if (shouldRefreshLevelObjectPrefabs) { LevelObjectPrefab.LoadAll(); }
if (shouldRefreshLocationTypes) { LocationType.Init(); }
if (shouldRefreshMapGenerationParams) { MapGenerationParams.Init(); }
if (shouldRefreshLevelGenerationParams) { LevelGenerationParams.LoadPresets(); }
#if CLIENT
if (shouldRefreshSoundPlayer) { SoundPlayer.Init().ForEach(_ => { return; }); }
#endif
RefreshContentPackageItems(contentPackage.Files);
}
}
private void EnableContentPackageItems(IOrderedEnumerable<ContentFile> files,
ref bool shouldRefreshSubs,
ref bool shouldRefreshFabricationRecipes,
ref bool shouldRefreshSoundPlayer,
ref bool shouldRefreshRuinGenerationParams,
ref bool shouldRefreshScriptedEventSets,
ref bool shouldRefreshMissionPrefabs,
ref bool shouldRefreshLevelObjectPrefabs,
ref bool shouldRefreshLocationTypes,
ref bool shouldRefreshMapGenerationParams,
ref bool shouldRefreshLevelGenerationParams,
ref bool shouldRefreshAfflictions)
private void EnableContentPackageItems(IOrderedEnumerable<ContentFile> files)
{
foreach (ContentFile file in files)
{
@@ -474,6 +346,9 @@ namespace Barotrauma
case ContentType.Character:
CharacterPrefab.LoadFromFile(file);
break;
case ContentType.Corpses:
CorpsePrefab.LoadFromFile(file);
break;
case ContentType.NPCConversations:
NPCConversation.LoadFromFile(file);
break;
@@ -482,7 +357,6 @@ namespace Barotrauma
break;
case ContentType.Item:
ItemPrefab.LoadFromFile(file);
shouldRefreshFabricationRecipes = true;
break;
case ContentType.ItemAssembly:
new ItemAssemblyPrefab(file.Path);
@@ -490,40 +364,10 @@ namespace Barotrauma
case ContentType.Structure:
StructurePrefab.LoadFromFile(file);
break;
case ContentType.Submarine:
shouldRefreshSubs = true;
break;
case ContentType.Text:
TextManager.LoadTextPack(file.Path);
break;
case ContentType.Afflictions:
shouldRefreshAfflictions = true;
break;
case ContentType.RuinConfig:
shouldRefreshRuinGenerationParams = true;
break;
case ContentType.RandomEvents:
shouldRefreshScriptedEventSets = true;
break;
case ContentType.Missions:
shouldRefreshMissionPrefabs = true;
break;
case ContentType.LevelObjectPrefabs:
shouldRefreshLevelObjectPrefabs = true;
break;
case ContentType.LocationTypes:
shouldRefreshLocationTypes = true;
break;
case ContentType.MapGenerationParameters:
shouldRefreshMapGenerationParams = true;
break;
case ContentType.LevelGenerationParameters:
shouldRefreshLevelGenerationParams = true;
break;
#if CLIENT
case ContentType.Sounds:
shouldRefreshSoundPlayer = true;
break;
case ContentType.Particles:
GameMain.ParticleManager?.LoadPrefabsFromFile(file);
break;
@@ -537,18 +381,7 @@ namespace Barotrauma
}
}
private void DisableContentPackageItems(IOrderedEnumerable<ContentFile> files,
ref bool shouldRefreshSubs,
ref bool shouldRefreshFabricationRecipes,
ref bool shouldRefreshSoundPlayer,
ref bool shouldRefreshRuinGenerationParams,
ref bool shouldRefreshScriptedEventSets,
ref bool shouldRefreshMissionPrefabs,
ref bool shouldRefreshLevelObjectPrefabs,
ref bool shouldRefreshLocationTypes,
ref bool shouldRefreshMapGenerationParams,
ref bool shouldRefreshLevelGenerationParams,
ref bool shouldRefreshAfflictions)
private void DisableContentPackageItems(IOrderedEnumerable<ContentFile> files)
{
foreach (ContentFile file in files)
{
@@ -557,6 +390,9 @@ namespace Barotrauma
case ContentType.Character:
CharacterPrefab.RemoveByFile(file.Path);
break;
case ContentType.Corpses:
CorpsePrefab.RemoveByFile(file.Path);
break;
case ContentType.NPCConversations:
NPCConversation.RemoveByFile(file.Path);
break;
@@ -565,7 +401,6 @@ namespace Barotrauma
break;
case ContentType.Item:
ItemPrefab.RemoveByFile(file.Path);
shouldRefreshFabricationRecipes = true;
break;
case ContentType.ItemAssembly:
ItemAssemblyPrefab.Remove(file.Path);
@@ -573,40 +408,10 @@ namespace Barotrauma
case ContentType.Structure:
StructurePrefab.RemoveByFile(file.Path);
break;
case ContentType.Submarine:
shouldRefreshSubs = true;
break;
case ContentType.Text:
TextManager.RemoveTextPack(file.Path);
break;
case ContentType.Afflictions:
shouldRefreshAfflictions = true;
break;
case ContentType.RuinConfig:
shouldRefreshRuinGenerationParams = true;
break;
case ContentType.RandomEvents:
shouldRefreshScriptedEventSets = true;
break;
case ContentType.Missions:
shouldRefreshMissionPrefabs = true;
break;
case ContentType.LevelObjectPrefabs:
shouldRefreshLevelObjectPrefabs = true;
break;
case ContentType.LocationTypes:
shouldRefreshLocationTypes = true;
break;
case ContentType.MapGenerationParameters:
shouldRefreshMapGenerationParams = true;
break;
case ContentType.LevelGenerationParameters:
shouldRefreshLevelGenerationParams = true;
break;
#if CLIENT
case ContentType.Sounds:
shouldRefreshSoundPlayer = true;
break;
case ContentType.Particles:
GameMain.ParticleManager?.RemovePrefabsByFile(file.Path);
break;
@@ -620,39 +425,74 @@ namespace Barotrauma
}
}
private void RefreshContentPackageItems(IEnumerable<ContentFile> files)
{
if (files.Any(f => f.Type == ContentType.Afflictions)) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); }
if (files.Any(f => f.Type == ContentType.Submarine)) { SubmarineInfo.RefreshSavedSubs(); }
if (files.Any(f => f.Type == ContentType.Item)) { ItemPrefab.InitFabricationRecipes(); }
if (files.Any(f => f.Type == ContentType.RuinConfig)) { RuinGeneration.RuinGenerationParams.ClearAll(); }
if (files.Any(f => f.Type == ContentType.RandomEvents)) { ScriptedEventSet.LoadPrefabs(); }
if (files.Any(f => f.Type == ContentType.Missions)) { MissionPrefab.Init(); }
if (files.Any(f => f.Type == ContentType.LevelObjectPrefabs)) { LevelObjectPrefab.LoadAll(); }
if (files.Any(f => f.Type == ContentType.LocationTypes)) { LocationType.Init(); }
if (files.Any(f => f.Type == ContentType.MapGenerationParameters)) { MapGenerationParams.Init(); }
if (files.Any(f => f.Type == ContentType.LevelGenerationParameters)) { LevelGenerationParams.LoadPresets(); }
if (files.Any(f => f.Type == ContentType.TraitorMissions)) { TraitorMissionPrefab.Init(); }
if (files.Any(f => f.Type == ContentType.Orders)) { Order.Init(); }
if (files.Any(f => f.Type == ContentType.EventManagerSettings)) { EventManagerSettings.Init(); }
if (files.Any(f => f.Type == ContentType.WreckAIConfig)) { WreckAIConfig.LoadAll(); }
if (files.Any(f => f.Type == ContentType.SkillSettings)) { SkillSettings.Load(GameMain.Instance.GetFilesOfType(ContentType.SkillSettings)); }
#if CLIENT
if (files.Any(f => f.Type == ContentType.Tutorials)) { Tutorial.Init(); }
if (files.Any(f => f.Type == ContentType.Sounds)) { SoundPlayer.Init().ForEach(_ => { return; }); }
#endif
}
private readonly static ContentType[] hotswappableContentTypes = new ContentType[]
{
ContentType.Character,
ContentType.Corpses,
ContentType.NPCConversations,
ContentType.Jobs,
ContentType.Orders,
ContentType.EventManagerSettings,
ContentType.Item,
ContentType.ItemAssembly,
ContentType.Structure,
ContentType.Submarine,
ContentType.Text,
ContentType.Afflictions,
ContentType.RuinConfig,
ContentType.RandomEvents,
ContentType.Missions,
ContentType.LevelObjectPrefabs,
ContentType.LocationTypes,
ContentType.MapGenerationParameters,
ContentType.LevelGenerationParameters,
ContentType.Sounds,
ContentType.Particles,
ContentType.Decals,
ContentType.Outpost,
ContentType.Wreck,
ContentType.WreckAIConfig,
ContentType.BackgroundCreaturePrefabs,
ContentType.ServerExecutable,
ContentType.TraitorMissions,
ContentType.Tutorials,
ContentType.SkillSettings,
ContentType.None
};
private void UpdateContentPackageDirtyFlag(ContentFile file)
{
switch (file.Type)
if (!hotswappableContentTypes.Contains(file.Type))
{
case ContentType.Character:
case ContentType.NPCConversations:
case ContentType.Jobs:
case ContentType.Item:
case ContentType.ItemAssembly:
case ContentType.Structure:
case ContentType.Submarine:
case ContentType.Text:
case ContentType.Afflictions:
case ContentType.RuinConfig:
case ContentType.RandomEvents:
case ContentType.Missions:
case ContentType.LevelObjectPrefabs:
case ContentType.LocationTypes:
case ContentType.MapGenerationParameters:
case ContentType.LevelGenerationParameters:
case ContentType.Sounds:
case ContentType.Particles:
case ContentType.Decals:
case ContentType.Outpost:
case ContentType.Wreck:
case ContentType.BackgroundCreaturePrefabs:
case ContentType.ServerExecutable:
case ContentType.None:
break; //do nothing here if the content type is supported
default:
if (ContentPackage.MultiplayerIncompatibleContent.Contains(file.Type))
{
ContentPackageSelectionDirty = true;
ContentPackageSelectionDirtyNotification = true;
break;
}
ContentPackageSelectionDirtyNotification = true;
}
}
@@ -682,6 +522,11 @@ namespace Barotrauma
LocationType.Init();
MapGenerationParams.Init();
LevelGenerationParams.LoadPresets();
TraitorMissionPrefab.Init();
Order.Init();
EventManagerSettings.Init();
WreckAIConfig.LoadAll();
SkillSettings.Load(GameMain.Instance.GetFilesOfType(ContentType.SkillSettings));
#if CLIENT
GameMain.DecalManager.Prefabs.SortAll();
@@ -780,21 +625,21 @@ namespace Barotrauma
LoadPlayerConfig();
modsFolderWatcher = new FileSystemWatcher("Mods");
modsFolderWatcher = new System.IO.FileSystemWatcher("Mods");
modsFolderWatcher.Filter = "*";
modsFolderWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName;
modsFolderWatcher.NotifyFilter = System.IO.NotifyFilters.LastWrite | System.IO.NotifyFilters.FileName | System.IO.NotifyFilters.DirectoryName;
modsFolderWatcher.Created += OnModFolderUpdate;
modsFolderWatcher.Deleted += OnModFolderUpdate;
modsFolderWatcher.Renamed += OnModFolderUpdate;
modsFolderWatcher.EnableRaisingEvents = true;
}
private void OnModFolderUpdate(object sender, FileSystemEventArgs e)
private void OnModFolderUpdate(object sender, System.IO.FileSystemEventArgs e)
{
if (SuppressModFolderWatcher || (GameMain.NetworkMember?.IsClient ?? false)) { return; }
switch (e.ChangeType)
{
case WatcherChangeTypes.Created:
case System.IO.WatcherChangeTypes.Created:
{
string cpPath = Path.GetFullPath(Path.Combine(e.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath();
if (File.Exists(cpPath) && !ContentPackage.List.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath))
@@ -804,7 +649,7 @@ namespace Barotrauma
}
}
break;
case WatcherChangeTypes.Deleted:
case System.IO.WatcherChangeTypes.Deleted:
{
string cpPath = Path.GetFullPath(Path.Combine(e.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath();
var toRemove = ContentPackage.List.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList();
@@ -827,9 +672,9 @@ namespace Barotrauma
}
}
break;
case WatcherChangeTypes.Renamed:
case System.IO.WatcherChangeTypes.Renamed:
{
RenamedEventArgs renameArgs = e as RenamedEventArgs;
System.IO.RenamedEventArgs renameArgs = e as System.IO.RenamedEventArgs;
string cpPath = Path.GetFullPath(Path.Combine(renameArgs.OldFullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath();
var toRemove = ContentPackage.List.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList();
@@ -980,13 +825,29 @@ namespace Barotrauma
doc.Root.Add(keyMappingElement);
for (int i = 0; i < keyMapping.Length; i++)
{
if (keyMapping[i].MouseButton == MouseButton.None)
KeyOrMouse bind = keyMapping[i];
if (bind.MouseButton == MouseButton.None)
{
keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].Key));
keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), bind.Key));
}
else
{
keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].MouseButton));
keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), bind.MouseButton));
}
}
var inventoryKeyMappingElement = new XElement("inventorykeymapping");
doc.Root.Add(inventoryKeyMappingElement);
for (int i = 0; i < inventoryKeyMapping.Length; i++)
{
KeyOrMouse bind = inventoryKeyMapping[i];
if (bind.MouseButton == MouseButton.None)
{
inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.Key));
}
else
{
inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.MouseButton));
}
}
#endif
@@ -1014,7 +875,7 @@ namespace Barotrauma
new XAttribute("faceattachmentindex", CharacterFaceAttachmentIndex));
doc.Root.Add(playerElement);
XmlWriterSettings settings = new XmlWriterSettings
System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings
{
Indent = true,
OmitXmlDeclaration = true,
@@ -1110,7 +971,7 @@ namespace Barotrauma
SelectedContentPackages.Clear();
foreach (string path in contentPackagePaths)
{
var matchingContentPackage = ContentPackage.List.Find(cp => System.IO.Path.GetFullPath(cp.Path).CleanUpPath() == path.CleanUpPath());
var matchingContentPackage = ContentPackage.List.Find(cp => Barotrauma.IO.Path.GetFullPath(cp.Path).CleanUpPath() == path.CleanUpPath());
if (matchingContentPackage == null)
{
@@ -1279,6 +1140,7 @@ namespace Barotrauma
new XAttribute("width", GraphicsWidth),
new XAttribute("height", GraphicsHeight),
new XAttribute("vsync", VSyncEnabled),
new XAttribute("compresstextures", TextureCompressionEnabled),
new XAttribute("framelimit", Timing.FrameLimit),
new XAttribute("displaymode", windowMode));
}
@@ -1300,7 +1162,7 @@ namespace Barotrauma
new XAttribute("voipattenuationenabled", VoipAttenuationEnabled),
new XAttribute("usedirectionalvoicechat", UseDirectionalVoiceChat),
new XAttribute("voicesetting", VoiceSetting),
new XAttribute("voicecapturedevice", VoiceCaptureDevice ?? ""),
new XAttribute("voicecapturedevice", System.Xml.XmlConvert.EncodeName(VoiceCaptureDevice ?? "")),
new XAttribute("noisegatethreshold", NoiseGateThreshold));
XElement gSettings = doc.Root.Element("graphicssettings");
@@ -1340,6 +1202,21 @@ namespace Barotrauma
keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].MouseButton));
}
}
var inventoryKeyMappingElement = new XElement("inventorykeymapping");
doc.Root.Add(inventoryKeyMappingElement);
for (int i = 0; i < inventoryKeyMapping.Length; i++)
{
KeyOrMouse bind = inventoryKeyMapping[i];
if (bind.MouseButton == MouseButton.None)
{
inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.Key));
}
else
{
inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.MouseButton));
}
}
#endif
var gameplay = new XElement("gameplay");
@@ -1384,7 +1261,7 @@ namespace Barotrauma
}
doc.Root.Add(tutorialElement);
XmlWriterSettings settings = new XmlWriterSettings
System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings
{
Indent = true,
OmitXmlDeclaration = true,
@@ -1485,6 +1362,7 @@ namespace Barotrauma
GraphicsWidth = graphicsMode.GetAttributeInt("width", GraphicsWidth);
GraphicsHeight = graphicsMode.GetAttributeInt("height", GraphicsHeight);
VSyncEnabled = graphicsMode.GetAttributeBool("vsync", VSyncEnabled);
TextureCompressionEnabled = graphicsMode.GetAttributeBool("compresstextures", TextureCompressionEnabled);
Timing.FrameLimit = graphicsMode.GetAttributeInt("framelimit", 200);
XElement graphicsSettings = doc.Root.Element("graphicssettings");
@@ -1526,7 +1404,7 @@ namespace Barotrauma
MuteOnFocusLost = audioSettings.GetAttributeBool("muteonfocuslost", MuteOnFocusLost);
UseDirectionalVoiceChat = audioSettings.GetAttributeBool("usedirectionalvoicechat", UseDirectionalVoiceChat);
VoiceCaptureDevice = audioSettings.GetAttributeString("voicecapturedevice", VoiceCaptureDevice);
VoiceCaptureDevice = System.Xml.XmlConvert.DecodeName(audioSettings.GetAttributeString("voicecapturedevice", VoiceCaptureDevice));
NoiseGateThreshold = audioSettings.GetAttributeFloat("noisegatethreshold", NoiseGateThreshold);
MicrophoneVolume = audioSettings.GetAttributeFloat("microphonevolume", MicrophoneVolume);
string voiceSettingStr = audioSettings.GetAttributeString("voicesetting", "");
@@ -1568,6 +1446,7 @@ namespace Barotrauma
GraphicsWidth = 0;
GraphicsHeight = 0;
VSyncEnabled = true;
TextureCompressionEnabled = true;
Timing.FrameLimit = 200;
#if DEBUG
EnableSplashScreen = false;
@@ -150,7 +150,24 @@ namespace Barotrauma
/// </summary>
public override bool TryPutItem(Item item, Character user, List<InvSlotType> allowedSlots = null, bool createNetworkEvent = true)
{
if (allowedSlots == null || !allowedSlots.Any()) return false;
if (allowedSlots == null || !allowedSlots.Any()) { return false; }
if (item == null)
{
#if DEBUG
throw new Exception("item null");
#else
return false;
#endif
}
if (item.Removed)
{
#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);
return false;
#endif
}
bool inSuitableSlot = false;
bool inWrongSlot = false;
@@ -167,7 +184,7 @@ namespace Barotrauma
}
}
//all good
if (inSuitableSlot && !inWrongSlot) return true;
if (inSuitableSlot && !inWrongSlot) { return true; }
//try to place the item in a LimbSlot.Any slot if that's allowed
if (allowedSlots.Contains(InvSlotType.Any) && item.AllowedSlots.Contains(InvSlotType.Any))
@@ -184,6 +201,9 @@ namespace Barotrauma
int placedInSlot = -1;
foreach (InvSlotType allowedSlot in allowedSlots)
{
if (allowedSlot.HasFlag(InvSlotType.RightHand) && character.AnimController.GetLimb(LimbType.RightHand) == null) { continue; }
if (allowedSlot.HasFlag(InvSlotType.LeftHand) && character.AnimController.GetLimb(LimbType.LeftHand) == null) { continue; }
//check if all the required slots are free
bool free = true;
for (int i = 0; i < capacity; i++)
@@ -5,7 +5,7 @@ using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
@@ -29,6 +29,7 @@ namespace Barotrauma.Items.Components
private Door door;
private Body[] bodies;
private Fixture outsideBlocker;
private Body doorBody;
private bool docked;
@@ -58,7 +59,7 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, false, description: "If set to true, this docking port is used when spawning the submarine docked to an outpost (if possible).")]
[Editable, Serialize(false, true, description: "If set to true, this docking port is used when spawning the submarine docked to an outpost (if possible).")]
public bool MainDockingPort
{
get;
@@ -113,6 +114,12 @@ namespace Barotrauma.Items.Components
{
if (DockingTarget != null)
{
if (IsHorizontal)
{
DockingDir = 0;
DockingDir = GetDir(DockingTarget);
DockingTarget.DockingDir = -DockingDir;
}
if (joint != null)
{
CreateJoint(joint is WeldJoint);
@@ -198,18 +205,6 @@ namespace Barotrauma.Items.Components
DockingDir = GetDir(DockingTarget);
DockingTarget.DockingDir = -DockingDir;
if (door != null && DockingTarget.door != null)
{
WayPoint myWayPoint = WayPoint.WayPointList.Find(wp => door.LinkedGap == wp.ConnectedGap);
WayPoint targetWayPoint = WayPoint.WayPointList.Find(wp => DockingTarget.door.LinkedGap == wp.ConnectedGap);
if (myWayPoint != null && targetWayPoint != null)
{
myWayPoint.linkedTo.Add(targetWayPoint);
targetWayPoint.linkedTo.Add(myWayPoint);
}
}
CreateJoint(false);
#if SERVER
@@ -259,6 +254,12 @@ namespace Barotrauma.Items.Components
{
item.CreateServerEvent(this);
}
#else
if (GameMain.Client != null && GameMain.Client.MidRoundSyncing &&
(item.Submarine == Submarine.MainSub || DockingTarget.item.Submarine == Submarine.MainSub))
{
Screen.Selected.Cam.Position = Submarine.MainSub.WorldPosition;
}
#endif
}
@@ -270,6 +271,20 @@ namespace Barotrauma.Items.Components
{
CreateHulls();
}
if (door != null && DockingTarget.door != null)
{
WayPoint myWayPoint = WayPoint.WayPointList.Find(wp => door.LinkedGap == wp.ConnectedGap);
WayPoint targetWayPoint = WayPoint.WayPointList.Find(wp => DockingTarget.door.LinkedGap == wp.ConnectedGap);
if (myWayPoint != null && targetWayPoint != null)
{
myWayPoint.FindHull();
myWayPoint.linkedTo.Add(targetWayPoint);
targetWayPoint.FindHull();
targetWayPoint.linkedTo.Add(myWayPoint);
}
}
}
@@ -461,6 +476,12 @@ namespace Barotrauma.Items.Components
}
}
if (leftSubRightSide == int.MinValue || rightSubLeftSide == int.MaxValue)
{
DebugConsole.NewMessage("Creating hulls between docking ports failed. Could not find a hull next to the docking port.");
return;
}
//expand left hull to the rightmost hull of the sub at the left side
//(unless the difference is more than 100 units - if the distance is very large
//there's something wrong with the positioning of the docking ports or submarine hulls)
@@ -469,7 +490,8 @@ namespace Barotrauma.Items.Components
{
if (leftHullDiff > 100)
{
DebugConsole.ThrowError("Creating hulls between docking ports failed. The leftmost docking port seems to be very far from any hulls in the left-side submarine.");
DebugConsole.NewMessage("Creating hulls between docking ports failed. The leftmost docking port seems to be very far from any hulls in the left-side submarine.");
return;
}
else
{
@@ -483,7 +505,8 @@ namespace Barotrauma.Items.Components
{
if (rightHullDiff > 100)
{
DebugConsole.ThrowError("Creating hulls between docking ports failed. The rightmost docking port seems to be very far from any hulls in the right-side submarine.");
DebugConsole.NewMessage("Creating hulls between docking ports failed. The rightmost docking port seems to be very far from any hulls in the right-side submarine.");
return;
}
else
{
@@ -506,6 +529,16 @@ namespace Barotrauma.Items.Components
}
}
if (rightHullDiff <= 100 && hulls[0].Submarine != null)
{
outsideBlocker = hulls[0].Submarine.PhysicsBody.FarseerBody.CreateRectangle(
ConvertUnits.ToSimUnits(hullRects[0].Width + hullRects[1].Width),
ConvertUnits.ToSimUnits(hullRects[0].Height),
density: 0.0f,
offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Right, hullRects[0].Y - hullRects[0].Height / 2) - hulls[0].Submarine.HiddenSubPosition));
outsideBlocker.UserData = this;
}
gap = new Gap(new Rectangle(hullRects[0].Right - 2, hullRects[0].Y, 4, hullRects[0].Height), true, subs[0]);
}
else
@@ -540,6 +573,12 @@ namespace Barotrauma.Items.Components
}
}
if (upperSubBottom == int.MaxValue || lowerSubTop == int.MinValue)
{
DebugConsole.NewMessage("Creating hulls between docking ports failed. Could not find a hull next to the docking port.");
return;
}
//expand lower hull to the topmost hull of the lower sub
//(unless the difference is more than 100 units - if the distance is very large
//there's something wrong with the positioning of the docking ports or submarine hulls)
@@ -548,7 +587,8 @@ namespace Barotrauma.Items.Components
{
if (lowerHullDiff > 100)
{
DebugConsole.ThrowError("Creating hulls between docking ports failed. The lower docking port seems to be very far from any hulls in the lower submarine.");
DebugConsole.NewMessage("Creating hulls between docking ports failed. The lower docking port seems to be very far from any hulls in the lower submarine.");
return;
}
else
{
@@ -561,7 +601,8 @@ namespace Barotrauma.Items.Components
{
if (upperHullDiff > 100)
{
DebugConsole.ThrowError("Creating hulls between docking ports failed. The upper docking port seems to be very far from any hulls in the upper submarine.");
DebugConsole.NewMessage("Creating hulls between docking ports failed. The upper docking port seems to be very far from any hulls in the upper submarine.");
return;
}
else
{
@@ -575,7 +616,8 @@ namespace Barotrauma.Items.Components
int midHullDiff = ((hullRects[1].Y - hullRects[1].Height) - hullRects[0].Y) + 2;
if (midHullDiff > 100)
{
DebugConsole.ThrowError("Creating hulls between docking ports failed. The upper hull seems to be very far from the lower hull.");
DebugConsole.NewMessage("Creating hulls between docking ports failed. The upper hull seems to be very far from the lower hull.");
return;
}
else if (midHullDiff > 0)
{
@@ -584,15 +626,33 @@ namespace Barotrauma.Items.Components
hullRects[1].Height += midHullDiff / 2 + 1;
}
for (int i = 0; i < 2; i++)
{
hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition));
hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]);
hulls[i].AddToGrid(subs[i]);
hulls[i].FreeID();
for (int j = 0; j < 2; j++)
{
bodies[i + j * 2] = GameMain.World.CreateEdge(
ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X + hullRects[i].Width * j, hullRects[i].Y)),
ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X + hullRects[i].Width * j, hullRects[i].Y - hullRects[i].Height)));
}
}
gap = new Gap(new Rectangle(hullRects[0].X, hullRects[0].Y+2, hullRects[0].Width, 4), false, subs[0]);
if (midHullDiff <= 100 && hulls[0].Submarine != null)
{
outsideBlocker = hulls[0].Submarine.PhysicsBody.FarseerBody.CreateRectangle(
ConvertUnits.ToSimUnits(hullRects[0].Width),
ConvertUnits.ToSimUnits(hullRects[0].Height + hullRects[1].Height),
density: 0.0f,
offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Center.X, hullRects[0].Y) - hulls[0].Submarine.HiddenSubPosition));
outsideBlocker.UserData = this;
}
gap = new Gap(new Rectangle(hullRects[0].X, hullRects[0].Y + 2, hullRects[0].Width, 4), false, subs[0]);
}
LinkHullsToGaps();
@@ -609,7 +669,7 @@ namespace Barotrauma.Items.Components
foreach (Body body in bodies)
{
if (body == null) continue;
if (body == null) { continue; }
body.BodyType = BodyType.Static;
body.Friction = 0.5f;
@@ -720,7 +780,9 @@ namespace Barotrauma.Items.Components
if (myWayPoint != null && targetWayPoint != null)
{
myWayPoint.FindHull();
myWayPoint.linkedTo.Remove(targetWayPoint);
targetWayPoint.FindHull();
targetWayPoint.linkedTo.Remove(myWayPoint);
}
}
@@ -769,6 +831,9 @@ namespace Barotrauma.Items.Components
bodies = null;
}
outsideBlocker?.Body.Remove(outsideBlocker);
outsideBlocker = null;
Item.Submarine.EnableObstructedWaypoints();
obstructedWayPointsDisabled = false;
@@ -4,7 +4,7 @@ using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
#if CLIENT
@@ -38,12 +38,17 @@ namespace Barotrauma.Items.Components
}
}
//how much "less stuck" partially doors get when opened
const float StuckReductionOnOpen = 30.0f;
private float resetPredictionTimer;
private float toggleCooldownTimer;
private Character lastUser;
private float damageSoundCooldown;
private double lastBrokenTime;
private Rectangle doorRect;
private bool isBroken;
@@ -53,7 +58,7 @@ namespace Barotrauma.Items.Components
get { return isBroken; }
set
{
if (isBroken == value) return;
if (isBroken == value) { return; }
isBroken = value;
if (isBroken)
{
@@ -63,6 +68,9 @@ namespace Barotrauma.Items.Components
{
EnableBody();
}
#if SERVER
item.CreateServerEvent(this);
#endif
}
}
@@ -85,7 +93,7 @@ namespace Barotrauma.Items.Components
if (isOpen || isBroken || !CanBeWelded) return;
stuck = MathHelper.Clamp(value, 0.0f, 100.0f);
if (stuck <= 0.0f) { IsStuck = false; }
if (stuck >= 100.0f) { IsStuck = true; }
if (stuck >= 99.0f) { IsStuck = true; }
}
}
@@ -203,10 +211,16 @@ namespace Barotrauma.Items.Components
break;
}
}
IsActive = true;
}
public override void OnItemLoaded()
{
//do this here because the scale of the item might not be set to the final value yet in the constructor
doorRect = new Rectangle(
item.Rect.Center.X - (int)(doorSprite.size.X / 2 * item.Scale),
item.Rect.Y - item.Rect.Height/2 + (int)(doorSprite.size.Y / 2.0f * item.Scale),
item.Rect.Y - item.Rect.Height / 2 + (int)(doorSprite.size.Y / 2.0f * item.Scale),
(int)(doorSprite.size.X * item.Scale),
(int)(doorSprite.size.Y * item.Scale));
@@ -224,8 +238,6 @@ namespace Barotrauma.Items.Components
Body.SetTransformIgnoreContacts(
ConvertUnits.ToSimUnits(new Vector2(doorRect.Center.X, doorRect.Y - doorRect.Height / 2)),
0.0f);
IsActive = true;
}
public override void Move(Vector2 amount)
@@ -295,6 +307,7 @@ namespace Barotrauma.Items.Components
PickingTime = 0;
ToggleState(ActionType.OnUse, character);
PickingTime = originalPickingTime;
StopPicking(picker);
}
#if CLIENT
else if (hasRequiredItems && character != null && character == Character.Controlled)
@@ -313,8 +326,9 @@ namespace Barotrauma.Items.Components
if (isBroken)
{
lastBrokenTime = Timing.TotalTime;
//the door has to be restored to 50% health before collision detection on the body is re-enabled
if (item.ConditionPercentage > 50.0f)
if (item.ConditionPercentage > 50.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
{
IsBroken = false;
}
@@ -363,7 +377,10 @@ namespace Barotrauma.Items.Components
public override void UpdateBroken(float deltaTime, Camera cam)
{
base.UpdateBroken(deltaTime, cam);
IsBroken = true;
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
IsBroken = true;
}
}
private void EnableBody()
@@ -502,6 +519,7 @@ namespace Barotrauma.Items.Components
foreach (Limb limb in c.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (PushBodyOutOfDoorway(c, limb.body, dir, simPos, simSize) && damageSoundCooldown <= 0.0f)
{
#if CLIENT
@@ -564,7 +582,12 @@ namespace Barotrauma.Items.Components
body.ApplyLinearImpulse(new Vector2(dir * 2.0f, isOpen ? 0.0f : -1.0f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
c.SetStun(0.2f);
//don't stun if the door was broken a moment ago
//otherwise enabling the door's collider and pushing the character away will interrupt repairing
if (lastBrokenTime < Timing.TotalTime - 1.0f)
{
c.SetStun(0.2f);
}
return true;
}
@@ -1,13 +1,13 @@
using Microsoft.Xna.Framework;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class ElectricalDischarger : Powered
partial class ElectricalDischarger : Powered, IServerSerializable
{
private static readonly List<ElectricalDischarger> list = new List<ElectricalDischarger>();
public static IEnumerable<ElectricalDischarger> List
@@ -48,14 +48,14 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(500.0f, true, description: "How far the discharge can travel from the item."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 5000.0f)]
[Serialize(500.0f, true, description: "How far the discharge can travel from the item.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 5000.0f)]
public float Range
{
get;
set;
}
[Serialize(25.0f, true, description: "How much further can the discharge be carried when moving across walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
[Serialize(25.0f, true, description: "How much further can the discharge be carried when moving across walls.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float RangeMultiplierInWalls
{
get;
@@ -115,10 +115,15 @@ namespace Barotrauma.Items.Components
//already active, do nothing
if (IsActive) { return false; }
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
CurrPowerConsumption = powerConsumption;
charging = true;
timer = Duration;
IsActive = true;
#if SERVER
if (GameMain.Server != null) { item.CreateServerEvent(this); }
#endif
return false;
}
@@ -150,14 +155,12 @@ namespace Barotrauma.Items.Components
neededPower -= takePower;
battery.Charge -= takePower / 3600.0f;
#if SERVER
if (GameMain.Server != null)
{
battery.Item.CreateServerEvent(battery);
}
if (GameMain.Server != null) { battery.Item.CreateServerEvent(battery); }
#endif
}
}
Discharge();
}
else if (Voltage > MinVoltage)
{
@@ -478,5 +481,10 @@ namespace Barotrauma.Items.Components
base.RemoveComponentSpecific();
list.Remove(this);
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
//no further data needed, the event just triggers the discharge
}
}
}
@@ -3,6 +3,7 @@ using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
@@ -309,8 +310,13 @@ namespace Barotrauma.Items.Components
{
picker = character;
if (character != null) item.Submarine = character.Submarine;
if (item.Removed)
{
DebugConsole.ThrowError($"Attempted to equip a removed item ({item.Name})\n" + Environment.StackTrace);
return;
}
if (character != null) { item.Submarine = character.Submarine; }
if (item.body == null)
{
if (body != null)
@@ -325,8 +331,8 @@ namespace Barotrauma.Items.Components
if (!item.body.Enabled)
{
Limb rightHand = picker.AnimController.GetLimb(LimbType.RightHand);
item.SetTransform(rightHand.SimPosition, 0.0f);
Limb hand = picker.AnimController.GetLimb(LimbType.RightHand) ?? picker.AnimController.GetLimb(LimbType.LeftHand);
item.SetTransform(hand != null ? hand.SimPosition : character.SimPosition, 0.0f);
}
bool alreadyEquipped = character.HasEquippedItem(item);
@@ -363,38 +369,61 @@ namespace Barotrauma.Items.Components
IsActive = false;
}
public bool CanBeAttached()
public bool CanBeAttached(Character user)
{
if (!attachable || !Reattachable) return false;
if (!attachable || !Reattachable) { return false; }
//can be attached anywhere in sub editor
if (Screen.Selected == GameMain.SubEditorScreen) return true;
if (Screen.Selected == GameMain.SubEditorScreen) { return true; }
Vector2 attachPos = user == null ? item.WorldPosition : GetAttachPosition(user, useWorldCoordinates: true);
//can be attached anywhere inside hulls
if (item.CurrentHull != null) return true;
if (item.CurrentHull != null && Submarine.RectContains(item.CurrentHull.WorldRect, attachPos)) { return true; }
return Structure.GetAttachTarget(item.WorldPosition) != null;
return Structure.GetAttachTarget(attachPos) != null;
}
public bool CanBeDeattached()
{
if (!attachable || !attached) return true;
if (!attachable || !attached) { return true; }
//allow deattaching everywhere in sub editor
if (Screen.Selected == GameMain.SubEditorScreen) return true;
if (Screen.Selected == GameMain.SubEditorScreen) { return true; }
//don't allow deattaching if part of a sub and outside hulls
return item.Submarine == null || item.CurrentHull != null;
if (item.GetComponent<LevelResource>() != null) { return true; }
//if the item has a connection panel and rewiring is disabled, don't allow deattaching
var connectionPanel = item.GetComponent<ConnectionPanel>();
if (connectionPanel != null && (connectionPanel.Locked || !(GameMain.NetworkMember?.ServerSettings?.AllowRewiring ?? true)))
{
return false;
}
if (item.CurrentHull == null)
{
return Structure.GetAttachTarget(item.WorldPosition) != null;
}
else
{
return true;
}
}
public override bool Pick(Character picker)
{
if (item.Removed)
{
DebugConsole.ThrowError($"Attempted to pick up a removed item ({item.Name})\n" + Environment.StackTrace);
return false;
}
if (!attachable)
{
return base.Pick(picker);
}
if (!CanBeDeattached()) return false;
if (!CanBeDeattached()) { return false; }
if (Attached)
{
@@ -486,7 +515,7 @@ namespace Barotrauma.Items.Components
if (character != null)
{
if (!character.IsKeyDown(InputType.Aim)) { return false; }
if (!CanBeAttached()) { return false; }
if (!CanBeAttached(character)) { return false; }
if (GameMain.NetworkMember != null)
{
@@ -515,7 +544,7 @@ namespace Barotrauma.Items.Components
else
{
item.Drop(character);
item.SetTransform(ConvertUnits.ToSimUnits(GetAttachPosition(character)), 0.0f);
item.SetTransform(ConvertUnits.ToSimUnits(GetAttachPosition(character)), 0.0f, findNewHull: false);
}
}
@@ -524,16 +553,18 @@ namespace Barotrauma.Items.Components
return true;
}
private Vector2 GetAttachPosition(Character user)
private Vector2 GetAttachPosition(Character user, bool useWorldCoordinates = false)
{
if (user == null) { return item.Position; }
if (user == null) { return useWorldCoordinates ? item.WorldPosition : item.Position; }
Vector2 mouseDiff = user.CursorWorldPosition - user.WorldPosition;
mouseDiff = mouseDiff.ClampLength(MaxAttachDistance);
Vector2 userPos = useWorldCoordinates ? user.WorldPosition : user.Position;
return new Vector2(
MathUtils.RoundTowardsClosest(user.Position.X + mouseDiff.X, Submarine.GridSize.X),
MathUtils.RoundTowardsClosest(user.Position.Y + mouseDiff.Y, Submarine.GridSize.Y));
MathUtils.RoundTowardsClosest(userPos.X + mouseDiff.X, Submarine.GridSize.X),
MathUtils.RoundTowardsClosest(userPos.Y + mouseDiff.Y, Submarine.GridSize.Y));
}
public override void UpdateBroken(float deltaTime, Camera cam)
@@ -95,6 +95,7 @@ namespace Barotrauma.Items.Components
{
foreach (Limb l in character.AnimController.Limbs)
{
if (l.IsSevered) { continue; }
if (l.type == LimbType.LeftFoot || l.type == LimbType.LeftThigh || l.type == LimbType.LeftLeg) { continue; }
if (l.type == LimbType.Head || l.type == LimbType.Torso)
{
@@ -65,7 +65,7 @@ namespace Barotrauma.Items.Components
if (PickingTime > 0.0f)
{
if (picker.PickingItem == null && PickingTime <= float.MaxValue)
if ((picker.PickingItem == null || picker.PickingItem == item) && PickingTime <= float.MaxValue)
{
#if SERVER
item.CreateServerEvent(this);
@@ -114,10 +114,6 @@ namespace Barotrauma.Items.Components
{
activePicker = picker;
picker.PickingItem = item;
var leftHand = picker.AnimController.GetLimb(LimbType.LeftHand);
var rightHand = picker.AnimController.GetLimb(LimbType.RightHand);
pickTimer = 0.0f;
while (pickTimer < requiredTime && Screen.Selected != GameMain.SubEditorScreen)
{
@@ -65,7 +65,7 @@ namespace Barotrauma.Items.Components
foreach (Limb limb in character.AnimController.Limbs)
{
if (limb.WearingItems.Find(w => w.WearableComponent.Item == this.item) == null) continue;
if (limb.WearingItems.Find(w => w.WearableComponent.Item == item) == null) { continue; }
limb.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
@@ -90,6 +90,7 @@ namespace Barotrauma.Items.Components
return MathHelper.ToRadians(MathHelper.Lerp(Spread, UnskilledSpread, degreeOfFailure));
}
private readonly List<Body> limbBodies = new List<Body>();
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || character.Removed) { return false; }
@@ -104,9 +105,10 @@ namespace Barotrauma.Items.Components
item.AiTarget.SightRange = item.AiTarget.MaxSightRange;
}
List<Body> limbBodies = new List<Body>();
limbBodies.Clear();
foreach (Limb l in character.AnimController.Limbs)
{
if (l.IsSevered) { continue; }
limbBodies.Add(l.body.FarseerBody);
}
@@ -22,6 +22,8 @@ namespace Barotrauma.Items.Components
private Vector2 debugRayStartPos, debugRayEndPos;
private readonly List<Body> ignoredBodies = new List<Body>();
[Serialize("Both", false, description: "Can the item be used in air, water or both.")]
public UseEnvironment UsableIn
{
@@ -68,6 +70,12 @@ namespace Barotrauma.Items.Components
[Serialize(false, false, description: "Can the item repair things through holes in walls.")]
public bool RepairThroughHoles { get; set; }
[Serialize(true, false, description: "Can the item hit broken doors.")]
public bool HitItems { get; set; }
[Serialize(false, false, description: "Can the item hit broken doors.")]
public bool HitBrokenDoors { get; set; }
[Serialize(0.0f, false, description: "The probability of starting a fire somewhere along the ray fired from the barrel (for example, 0.1 = 10% chance to start a fire during a second of use).")]
public float FireProbability { get; set; }
@@ -114,8 +122,7 @@ namespace Barotrauma.Items.Components
}
}
item.IsShootable = true;
// TODO: should define this in xml if we have repair tools that don't require aim to use
item.RequireAimToUse = true;
item.RequireAimToUse = element.Parent.GetAttributeBool("requireaimtouse", true);
InitProjSpecific(element);
}
@@ -124,16 +131,17 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
activeTimer -= deltaTime;
if (activeTimer <= 0.0f) IsActive = false;
if (activeTimer <= 0.0f) { IsActive = false; }
}
private List<Body> ignoredBodies = new List<Body>();
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || character.Removed) return false;
if (item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) return false;
if (character != null)
{
if (item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) { return false; }
}
float degreeOfSuccess = DegreeOfSuccess(character);
float degreeOfSuccess = character == null ? 0.5f : DegreeOfSuccess(character);
if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess)
{
@@ -187,12 +195,15 @@ namespace Barotrauma.Items.Components
(float)Math.Sin(angle)) * Range * item.body.Dir);
ignoredBodies.Clear();
foreach (Limb limb in character.AnimController.Limbs)
if (character != null)
{
if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) continue;
ignoredBodies.Add(limb.body.FarseerBody);
foreach (Limb limb in character.AnimController.Limbs)
{
if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) continue;
ignoredBodies.Add(limb.body.FarseerBody);
}
ignoredBodies.Add(character.AnimController.Collider.FarseerBody);
}
ignoredBodies.Add(character.AnimController.Collider.FarseerBody);
IsActive = true;
activeTimer = 0.1f;
@@ -200,7 +211,8 @@ namespace Barotrauma.Items.Components
debugRayStartPos = ConvertUnits.ToDisplayUnits(rayStart);
debugRayEndPos = ConvertUnits.ToDisplayUnits(rayEnd);
if (character.Submarine == null)
Submarine parentSub = character?.Submarine ?? item.Submarine;
if (parentSub == null)
{
foreach (Submarine sub in Submarine.Loaded)
{
@@ -216,7 +228,7 @@ namespace Barotrauma.Items.Components
}
else
{
Repair(rayStart - character.Submarine.SimPosition, rayEnd - character.Submarine.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies);
Repair(rayStart - parentSub.SimPosition, rayEnd - parentSub.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies);
}
UseProjSpecific(deltaTime, rayStart);
@@ -314,6 +326,18 @@ namespace Barotrauma.Items.Components
{
if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure) { return false; }
if (f.Body?.UserData as string == "ruinroom") { return false; }
if (f.Body?.UserData is Item targetItem)
{
if (!HitItems) { return false; }
if (HitBrokenDoors)
{
if (targetItem.GetComponent<Door>() == null && targetItem.Condition <= 0) { return false; }
}
else
{
if (targetItem.Condition <= 0) { return false; }
}
}
return f.Body?.UserData != null;
},
allowInsideFixture: true));
@@ -440,7 +464,8 @@ namespace Barotrauma.Items.Components
}
else if (targetBody.UserData is Item targetItem)
{
if (!HitItems) { return false; }
var levelResource = targetItem.GetComponent<LevelResource>();
if (levelResource != null && levelResource.Attached &&
levelResource.requiredItems.Any() &&
@@ -459,6 +484,15 @@ namespace Barotrauma.Items.Components
if (!targetItem.Prefab.DamagedByRepairTools) { return false; }
if (HitBrokenDoors)
{
if (targetItem.GetComponent<Door>() == null && targetItem.Condition <= 0) { return false; }
}
else
{
if (targetItem.Condition <= 0) { return false; }
}
targetItem.IsHighlighted = true;
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem.AllPropertyObjects);
@@ -647,26 +681,26 @@ namespace Barotrauma.Items.Components
}
#if CLIENT
if (user == null) { return; }
// Hard-coded progress bars for welding doors stuck.
// A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml.
foreach (ISerializableEntity target in targets)
{
if (target is Door door)
if (!(target is Door door)) { continue; }
if (!door.CanBeWelded) { continue; }
for (int i = 0; i < effect.propertyNames.Length; i++)
{
if (!door.CanBeWelded) continue;
for (int i = 0; i < effect.propertyNames.Length; i++)
string propertyName = effect.propertyNames[i];
if (propertyName != "stuck") { continue; }
if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; }
object value = property.GetValue(target);
if (door.Stuck > 0)
{
string propertyName = effect.propertyNames[i];
if (propertyName != "stuck") { continue; }
if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; }
object value = property.GetValue(target);
if (door.Stuck > 0)
{
var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White);
if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); }
}
var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White);
if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); }
}
}
}
}
#endif
}
@@ -421,7 +421,10 @@ namespace Barotrauma.Items.Components
case "activate":
case "use":
case "trigger_in":
item.Use(1.0f, sender);
if (signal != "0")
{
item.Use(1.0f, sender);
}
break;
case "toggle":
if (signal != "0")
@@ -734,12 +737,15 @@ namespace Barotrauma.Items.Components
public virtual void Load(XElement componentElement, bool usePrefabValues)
{
if (componentElement != null && !usePrefabValues)
if (componentElement != null)
{
foreach (XAttribute attribute in componentElement.Attributes())
{
if (!SerializableProperties.TryGetValue(attribute.Name.ToString().ToLowerInvariant(), out SerializableProperty property)) { continue; }
property.TrySetValue(this, attribute.Value);
if (property.OverridePrefabValues || !usePrefabValues)
{
property.TrySetValue(this, attribute.Value);
}
}
ParseMsg();
OverrideRequiredItems(componentElement);
@@ -908,49 +914,56 @@ namespace Barotrauma.Items.Components
#region AI related
protected const float AIUpdateInterval = 0.2f;
protected float aiUpdateTimer;
private int itemIndex;
private List<Item> ignoredContainers = new List<Item>();
private Character previousUser;
protected bool FindSuitableContainer(Character character, Func<Item, float> priority, out Item suitableContainer)
{
if (previousUser != character)
{
ignoredContainers.Clear();
previousUser = character;
}
suitableContainer = null;
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredContainers, customPriorityFunction: priority))
if (character.AIController is HumanAIController aiController)
{
suitableContainer = targetContainer;
return true;
if (previousUser != character)
{
previousUser = character;
itemIndex = 0;
}
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: aiController.IgnoredItems, customPriorityFunction: priority))
{
suitableContainer = targetContainer;
return true;
}
}
return false;
}
protected AIObjectiveContainItem AIContainItems<T>(ItemContainer container, Character character, AIObjective objective, int itemCount, bool equip, bool removeEmpty) where T : ItemComponent
{
var containObjective = new AIObjectiveContainItem(character, container.GetContainableItemIdentifiers.ToArray(), container, objective.objectiveManager)
AIObjectiveContainItem containObjective = null;
if (character.AIController is HumanAIController aiController)
{
targetItemCount = itemCount,
Equip = equip,
RemoveEmpty = removeEmpty,
GetItemPriority = i =>
containObjective = new AIObjectiveContainItem(character, container.GetContainableItemIdentifiers.ToArray(), container, objective.objectiveManager)
{
if (i.ParentInventory?.Owner is Item)
targetItemCount = itemCount,
Equip = equip,
RemoveEmpty = removeEmpty,
GetItemPriority = i =>
{
//don't take items from other items of the same type
if (((Item)i.ParentInventory.Owner).GetComponent<T>() != null)
if (i.ParentInventory?.Owner is Item)
{
return 0.0f;
//don't take items from other items of the same type
if (((Item)i.ParentInventory.Owner).GetComponent<T>() != null)
{
return 0.0f;
}
}
return 1.0f;
}
return 1.0f;
}
};
// TODO: are we sure that we want to abandon the objective here?
containObjective.Abandoned += () => objective.Abandon = true;
objective.AddSubObjective(containObjective);
};
containObjective.Abandoned += () =>
{
aiController.IgnoredItems.Add(container.Item);
};
objective.AddSubObjective(containObjective);
}
return containObjective;
}
@@ -959,68 +972,71 @@ namespace Barotrauma.Items.Components
/// </summary>
protected bool AIDecontainEmptyItems(Character character, AIObjective objective, bool equip, ItemContainer sourceContainer = null)
{
ItemContainer sourceC = sourceContainer ?? (item.OwnInventory?.Owner is Item it ? it.GetComponent<ItemContainer>() : null);
var containedItems = sourceContainer != null ? sourceContainer.Inventory.Items : item.OwnInventory.Items;
foreach (Item containedItem in containedItems)
if (character.AIController is HumanAIController aiController)
{
if (containedItem != null && containedItem.Condition <= 0.0f)
ItemContainer sourceC = sourceContainer ?? (item.OwnInventory?.Owner is Item it ? it.GetComponent<ItemContainer>() : null);
var containedItems = sourceContainer != null ? sourceContainer.Inventory.Items : item.OwnInventory.Items;
foreach (Item containedItem in containedItems)
{
if (FindSuitableContainer(character,
i =>
{
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
if (container.Inventory.IsFull()) { return 0; }
if (containedItem != null && containedItem.Condition <= 0.0f)
{
if (FindSuitableContainer(character,
i =>
{
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; }
if (container.ShouldBeContained(containedItem, out bool isRestrictionsDefined))
{
if (isRestrictionsDefined)
if (container.ShouldBeContained(containedItem, out bool isRestrictionsDefined))
{
return 4;
}
else
{
if (containedItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary))
if (isRestrictionsDefined)
{
return isPreferencesDefined ? isSecondary ? 2 : 3 : 1;
return 4;
}
else
{
return isPreferencesDefined ? 0 : 1;
if (containedItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary))
{
return isPreferencesDefined ? isSecondary ? 2 : 3 : 1;
}
else
{
return isPreferencesDefined ? 0 : 1;
}
}
}
}
else
else
{
return 0;
}
}, out Item targetContainer))
{
var decontainObjective = new AIObjectiveDecontainItem(character, containedItem, objective.objectiveManager, sourceC, targetContainer?.GetComponent<ItemContainer>())
{
return 0;
}
}, out Item targetContainer))
{
var decontainObjective = new AIObjectiveDecontainItem(character, containedItem, objective.objectiveManager, sourceC, targetContainer?.GetComponent<ItemContainer>())
{
Equip = equip
};
decontainObjective.Abandoned += () =>
{
itemIndex = 0;
if (targetContainer != null)
{
ignoredContainers.Add(targetContainer);
}
};
decontainObjective.Completed += () =>
{
if (targetContainer == null)
Equip = equip
};
decontainObjective.Abandoned += () =>
{
itemIndex = 0;
}
};
objective.AddSubObjectiveInQueue(decontainObjective);
}
else
{
return false;
if (targetContainer != null)
{
aiController.IgnoredItems.Add(targetContainer);
}
};
decontainObjective.Completed += () =>
{
if (targetContainer == null)
{
itemIndex = 0;
}
};
objective.AddSubObjectiveInQueue(decontainObjective);
}
else
{
return false;
}
}
}
}
@@ -80,6 +80,13 @@ namespace Barotrauma.Items.Components
set { itemRotation = MathHelper.ToRadians(value); }
}
[Serialize("", false, description: "Specify an item for the container to spawn with.")]
public string SpawnWithId
{
get;
set;
}
public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined)
{
isRestrictionsDefined = containableRestrictions.Any();
@@ -143,7 +150,7 @@ namespace Barotrauma.Items.Components
}
//no need to Update() if this item has no statuseffects and no physics body
IsActive = itemsWithStatusEffects.Count > 0 || containedItem.body != null;
IsActive = itemsWithStatusEffects.Count > 0 || Inventory.Items.Any(it => it?.body != null);
}
public void OnItemRemoved(Item containedItem)
@@ -151,7 +158,7 @@ namespace Barotrauma.Items.Components
itemsWithStatusEffects.RemoveAll(i => i.First == containedItem);
//deactivate if the inventory is empty
IsActive = itemsWithStatusEffects.Count > 0 || containedItem.body != null;
IsActive = itemsWithStatusEffects.Count > 0 || Inventory.Items.Any(it => it?.body != null);
}
public bool CanBeContained(Item item)
@@ -200,6 +207,22 @@ namespace Barotrauma.Items.Components
}
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
if (SpawnWithId.Length > 0)
{
ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId);
if (prefab != null)
{
if (Inventory != null && Inventory.Items.Any(it => it == null))
{
Entity.Spawner?.AddToSpawnQueue(prefab, Inventory);
}
}
}
}
public override bool HasRequiredItems(Character character, bool addMessage, string msg = null)
{
return (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
@@ -295,7 +318,7 @@ namespace Barotrauma.Items.Components
foreach (Item contained in Inventory.Items)
{
if (contained == null) continue;
if (contained == null) { continue; }
if (contained.body != null)
{
try
@@ -311,6 +334,7 @@ namespace Barotrauma.Items.Components
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace);
}
contained.body.Submarine = item.Submarine;
}
contained.Rect =
@@ -39,8 +39,6 @@ namespace Barotrauma.Items.Components
private Item focusTarget;
private float targetRotation;
private bool state;
public Vector2 UserPos
{
get { return userPos; }
@@ -61,6 +59,18 @@ namespace Barotrauma.Items.Components
set;
}
[Editable, Serialize(false, false, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.")]
public bool State
{
get;
set;
}
public bool ControlCharacterPose
{
get { return limbPositions.Count > 0; }
}
public Controller(Item item, XElement element)
: base(item, element)
{
@@ -99,7 +109,7 @@ namespace Barotrauma.Items.Components
if (IsToggle)
{
item.SendSignal(0, state ? "1" : "0", "signal_out", sender: null);
item.SendSignal(0, State ? "1" : "0", "signal_out", sender: null);
}
if (user == null
@@ -272,7 +282,7 @@ namespace Barotrauma.Items.Components
return true;
}
private Item GetFocusTarget()
public Item GetFocusTarget()
{
item.SendSignal(0, MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), "position_out", user);
@@ -294,7 +304,7 @@ namespace Barotrauma.Items.Components
{
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
state = !state;
State = !State;
#if SERVER
item.CreateServerEvent(this);
#endif
@@ -113,7 +113,7 @@ namespace Barotrauma.Items.Components
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
if (targetItem.Prefab.DeconstructItems.Any())
if (targetItem.Prefab.AllowDeconstruct)
{
//drop all items that are inside the deconstructed item
foreach (ItemContainer ic in targetItem.GetComponents<ItemContainer>())
@@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components
private float prevVoltage;
private float controlLockTimer;
[Editable(0.0f, 10000000.0f),
Serialize(2000.0f, true, description: "The amount of force exerted on the submarine when the engine is operating at full power.")]
public float MaxForce
@@ -93,8 +93,8 @@ namespace Barotrauma.Items.Components
controlLockTimer -= deltaTime;
currPowerConsumption = Math.Abs(targetForce) / 100.0f * powerConsumption;
//pumps consume more power when in a bad condition
currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
//engines consume more power when in a bad condition
item.GetComponent<Repairable>()?.AdjustPowerConsumption(ref currPowerConsumption);
if (powerConsumption == 0.0f) { Voltage = 1.0f; }
@@ -119,12 +119,15 @@ namespace Barotrauma.Items.Components
float max = 1 + maxChangeSpeed;
UpdateAITargets(Math.Clamp(noise, min, max), deltaTime);
#if CLIENT
for (int i = 0; i < 5; i++)
particleTimer -= deltaTime;
if (particleTimer <= 0.0f)
{
Vector2 particleVel = -currForce.ClampLength(5000.0f) / 5.0f;
GameMain.ParticleManager.CreateParticle("bubbles", item.WorldPosition + PropellerPos,
-currForce / 5.0f + new Vector2(Rand.Range(-100.0f, 100.0f), Rand.Range(-50f, 50f)),
particleVel * Rand.Range(0.9f, 1.1f),
0.0f, item.CurrentHull);
}
particleTimer = 1.0f / particlesPerSec;
}
#endif
}
}
@@ -171,7 +171,7 @@ namespace Barotrauma.Items.Components
outputContainer.Inventory.Locked = true;
currPowerConsumption = powerConsumption;
currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
item.GetComponent<Repairable>()?.AdjustPowerConsumption(ref currPowerConsumption);
if (GameMain.NetworkMember?.IsServer ?? true)
{
@@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components
private set;
}
[Editable, Serialize(400.0f, true, description: "How much oxygen the machine generates when operating at full power.")]
[Editable, Serialize(400.0f, true, description: "How much oxygen the machine generates when operating at full power.", alwaysUseInstanceValues: true)]
public float GeneratedAmount
{
get { return generatedAmount; }
@@ -42,7 +42,7 @@ namespace Barotrauma.Items.Components
CurrFlow = 0.0f;
currPowerConsumption = powerConsumption;
//consume more power when in a bad condition
currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
item.GetComponent<Repairable>()?.AdjustPowerConsumption(ref currPowerConsumption);
if (powerConsumption <= 0.0f)
{
@@ -27,7 +27,7 @@ namespace Barotrauma.Items.Components
}
}
[Editable, Serialize(80.0f, false, description: "How fast the item pumps water in/out when operating at 100%.")]
[Editable, Serialize(80.0f, false, description: "How fast the item pumps water in/out when operating at 100%.", alwaysUseInstanceValues: true)]
public float MaxFlow
{
get { return maxFlow; }
@@ -45,6 +45,7 @@ namespace Barotrauma.Items.Components
}
public bool HasPower => IsActive && Voltage >= MinVoltage;
public bool IsAutoControlled => pumpSpeedLockTimer > 0.0f || isActiveLockTimer > 0.0f;
public Pump(Item item, XElement element)
: base(item, element)
@@ -68,7 +69,7 @@ namespace Barotrauma.Items.Components
currPowerConsumption = powerConsumption * Math.Abs(flowPercentage / 100.0f);
//pumps consume more power when in a bad condition
currPowerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
item.GetComponent<Repairable>()?.AdjustPowerConsumption(ref currPowerConsumption);
if (!HasPower) { return; }
@@ -130,7 +131,7 @@ namespace Barotrauma.Items.Components
if (objective.Option.Equals("stoppumping", StringComparison.OrdinalIgnoreCase))
{
#if SERVER
if (FlowPercentage > 0.0f)
if (objective.Override || FlowPercentage > 0.0f)
{
item.CreateServerEvent(this);
}
@@ -141,7 +142,7 @@ namespace Barotrauma.Items.Components
else
{
#if SERVER
if (!IsActive || FlowPercentage > -100.0f)
if (objective.Override || !IsActive || FlowPercentage > -100.0f)
{
item.CreateServerEvent(this);
}
@@ -78,7 +78,7 @@ namespace Barotrauma.Items.Components
}
}
[Editable(0.0f, float.MaxValue), Serialize(10000.0f, true, description: "How much power (kW) the reactor generates when operating at full capacity.")]
[Editable(0.0f, float.MaxValue), Serialize(10000.0f, true, description: "How much power (kW) the reactor generates when operating at full capacity.", alwaysUseInstanceValues: true)]
public float MaxPowerOutput
{
get { return maxPowerOutput; }
@@ -330,6 +330,8 @@ namespace Barotrauma.Items.Components
}
item.SendSignal(0, ((int)(temperature * 100.0f)).ToString(), "temperature_out", null);
item.SendSignal(0, ((int)-CurrPowerConsumption).ToString(), "power_value_out", null);
item.SendSignal(0, ((int)load).ToString(), "load_value_out", null);
UpdateFailures(deltaTime);
#if CLIENT
@@ -622,7 +624,8 @@ namespace Barotrauma.Items.Components
AutoTemp = false;
targetFissionRate = 0.0f;
targetTurbineOutput = 0.0f;
break;
unsentChanges = true;
return true;
}
if (autoTemp != prevAutoTemp ||
@@ -653,17 +656,23 @@ namespace Barotrauma.Items.Components
}
break;
case "set_fissionrate":
if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate))
if (PowerOn && float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate))
{
FissionRate = newFissionRate;
targetFissionRate = newFissionRate;
unsentChanges = true;
#if CLIENT
FissionRateScrollBar.BarScroll = targetFissionRate / 100.0f;
#endif
}
break;
case "set_turbineoutput":
if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput))
if (PowerOn && float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput))
{
TurbineOutput = newTurbineOutput;
targetTurbineOutput = newTurbineOutput;
unsentChanges = true;
#if CLIENT
TurbineOutputScrollBar.BarScroll = targetTurbineOutput / 100.0f;
#endif
}
break;
}
@@ -108,10 +108,17 @@ namespace Barotrauma.Items.Components
public Vector2 TargetVelocity
{
get { return targetVelocity;}
set
get { return targetVelocity; }
set
{
if (!MathUtils.IsValid(value)) return;
if (!MathUtils.IsValid(value))
{
if (!MathUtils.IsValid(targetVelocity))
{
targetVelocity = Vector2.Zero;
}
return;
}
targetVelocity.X = MathHelper.Clamp(value.X, -100.0f, 100.0f);
targetVelocity.Y = MathHelper.Clamp(value.Y, -100.0f, 100.0f);
}
@@ -285,16 +292,13 @@ namespace Barotrauma.Items.Components
if (AutoPilot)
{
UpdateAutoPilot(deltaTime);
targetVelocity = targetVelocity.ClampLength(MathHelper.Lerp(AutoPilotMaxSpeed, AIPilotMaxSpeed, userSkill) * 100.0f);
TargetVelocity = TargetVelocity.ClampLength(MathHelper.Lerp(AutoPilotMaxSpeed, AIPilotMaxSpeed, userSkill) * 100.0f);
}
else
{
if (user != null && user.Info != null && user.SelectedConstruction == item)
{
user.Info.IncreaseSkillLevel(
"helm",
SkillSettings.Current.SkillIncreasePerSecondWhenSteering / Math.Max(userSkill, 1.0f) * deltaTime,
user.WorldPosition + Vector2.UnitY * 150.0f);
IncreaseSkillLevel(user, deltaTime);
}
Vector2 velocityDiff = steeringInput - targetVelocity;
@@ -323,6 +327,18 @@ namespace Barotrauma.Items.Components
item.SendSignal(0, targetLevel.ToString(CultureInfo.InvariantCulture), "velocity_y_out", null);
}
private void IncreaseSkillLevel(Character user, float deltaTime)
{
if (user?.Info == null) { return; }
float userSkill = user.GetSkillLevel("helm") / 100.0f;
user.Info.IncreaseSkillLevel(
"helm",
SkillSettings.Current.SkillIncreasePerSecondWhenSteering / Math.Max(userSkill, 1.0f) * deltaTime,
user.WorldPosition + Vector2.UnitY * 150.0f);
}
private void UpdateAutoPilot(float deltaTime)
{
if (controlledSub == null) { return; }
@@ -443,41 +459,46 @@ namespace Barotrauma.Items.Components
//steer away from other subs
foreach (Submarine sub in Submarine.Loaded)
{
if (sub == controlledSub) continue;
if (controlledSub.DockedTo.Contains(sub)) continue;
float thisSize = Math.Max(controlledSub.Borders.Width, controlledSub.Borders.Height);
float otherSize = Math.Max(sub.Borders.Width, sub.Borders.Height);
if (sub == controlledSub) { continue; }
if (controlledSub.DockedTo.Contains(sub)) { continue; }
Point sizeSum = controlledSub.Borders.Size + sub.Borders.Size;
Vector2 minDist = sizeSum.ToVector2() / 2;
Vector2 diff = controlledSub.WorldPosition - sub.WorldPosition;
float dist = diff == Vector2.Zero ? 0.0f : diff.Length();
//far enough -> ignore
if (dist > thisSize + otherSize) continue;
Vector2 dir = dist <= 0.0001f ? Vector2.UnitY : diff / dist;
float dot = controlledSub.Velocity == Vector2.Zero ?
0.0f : Vector2.Dot(Vector2.Normalize(controlledSub.Velocity), -dir);
//heading away -> ignore
if (dot < 0.0f) continue;
targetVelocity += diff * 200.0f;
float xDist = Math.Abs(diff.X);
float yDist = Math.Abs(diff.Y);
Vector2 maxAvoidDistance = minDist * 2;
if (xDist > maxAvoidDistance.X || yDist > maxAvoidDistance.Y)
{
//far enough -> ignore
continue;
}
float dot = controlledSub.Velocity == Vector2.Zero ? 0.0f : Vector2.Dot(Vector2.Normalize(controlledSub.Velocity), -diff);
if (dot < 0.0f)
{
//heading away -> ignore
continue;
}
float distanceFactor = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(maxAvoidDistance.X + maxAvoidDistance.Y, minDist.X + minDist.Y, xDist + yDist));
float velocityFactor = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 3, controlledSub.Velocity.Length()));
TargetVelocity += 100 * Vector2.Normalize(diff) * distanceFactor * velocityFactor;
}
//clamp velocity magnitude to 100.0f
float velMagnitude = targetVelocity.Length();
//clamp velocity magnitude to 100.0f (Is this required? The X and Y components are clamped in the property setter)
float velMagnitude = TargetVelocity.Length();
if (velMagnitude > 100.0f)
{
targetVelocity *= 100.0f / velMagnitude;
TargetVelocity *= 100.0f / velMagnitude;
}
}
private void UpdatePath()
{
if (Level.Loaded == null) { return; }
if (pathFinder == null) pathFinder = new PathFinder(WayPoint.WayPointList, false);
if (pathFinder == null)
{
pathFinder = new PathFinder(WayPoint.WayPointList, false);
}
Vector2 target;
if (LevelEndSelected)
@@ -553,6 +574,7 @@ namespace Barotrauma.Items.Components
unsentChanges = true;
AutoPilot = true;
}
IncreaseSkillLevel(user, deltaTime);
switch (objective.Option.ToLowerInvariant())
{
case "maintainposition":
@@ -316,6 +316,8 @@ namespace Barotrauma.Items.Components
{
if (recipient.Item == item || recipient.Item == source) { continue; }
source?.LastSentSignalRecipients.Add(recipient.Item);
foreach (ItemComponent ic in recipient.Item.Components)
{
//other junction boxes don't need to receive the signal in the pass-through signal connections
@@ -241,7 +241,6 @@ namespace Barotrauma.Items.Components
private void Launch(Vector2 impulse)
{
hits.Clear();
MaxTargetsToHit = 2;
if (item.AiTarget != null)
{
@@ -487,6 +486,10 @@ namespace Barotrauma.Items.Components
return true;
}
}
else if (target.Body.UserData is Item item)
{
if (item.Condition <= 0.0f) { return false; }
}
//ignore character colliders (the projectile only hits limbs)
if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character)
@@ -634,6 +637,7 @@ namespace Barotrauma.Items.Components
item.body.LinearVelocity *= 0.1f;
}
else if (Vector2.Dot(velocity, collisionNormal) < 0.0f && hits.Count() >= MaxTargetsToHit &&
target.Body.Mass > item.body.Mass * 0.5f &&
(DoesStick ||
(StickToCharacters && target.Body.UserData is Limb) ||
(StickToStructures && target.Body.UserData is Structure) ||
@@ -55,8 +55,8 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(80.0f, true, description: "The condition of the item has to be below this for AI characters to repair it. Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
public float AIRepairThreshold
[Serialize(80.0f, true, description: "The condition of the item has to be below this for it to become repairable. Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
public float RepairThreshold
{
get;
set;
@@ -112,13 +112,14 @@ namespace Barotrauma.Items.Components
element.GetAttributeString("name", "");
//backwards compatibility
var showRepairUIAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals("showrepairuithreshold", StringComparison.OrdinalIgnoreCase));
if (showRepairUIAttribute != null)
var repairThresholdAttribute =
element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals("showrepairuithreshold", StringComparison.OrdinalIgnoreCase)) ??
element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals("airepairth44reshold", StringComparison.OrdinalIgnoreCase));
if (repairThresholdAttribute != null)
{
float repairThreshold;
if (Single.TryParse(showRepairUIAttribute.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out repairThreshold))
if (float.TryParse(repairThresholdAttribute.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out float repairThreshold))
{
AIRepairThreshold = repairThreshold;
RepairThreshold = repairThreshold;
}
}
@@ -273,7 +274,7 @@ namespace Barotrauma.Items.Components
float successFactor = requiredSkills.Count == 0 ? 1.0f : DegreeOfSuccess(CurrentFixer, requiredSkills);
//item must have been below the repair threshold for the player to get an achievement or XP for repairing it
if (!item.IsFullCondition)
if (item.ConditionPercentage < RepairThreshold)
{
wasBroken = true;
}
@@ -309,11 +310,10 @@ namespace Barotrauma.Items.Components
SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f),
CurrentFixer.WorldPosition + Vector2.UnitY * 100.0f);
}
SteamAchievementManager.OnItemRepaired(item, CurrentFixer);
deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay);
wasBroken = false;
}
deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay);
wasBroken = false;
StopRepairing(CurrentFixer);
}
}
@@ -358,6 +358,14 @@ namespace Barotrauma.Items.Components
partial void UpdateProjSpecific(float deltaTime);
public void AdjustPowerConsumption(ref float powerConsumption)
{
if (item.ConditionPercentage < RepairThreshold)
{
powerConsumption *= MathHelper.Lerp(1.5f, 1.0f, item.Condition / item.MaxCondition);
}
}
private bool ShouldDeteriorate()
{
if (LastActiveTime > Timing.TotalTime) { return true; }
@@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components
//the output is sent if both inputs have received a signal within the timeframe
protected float timeFrame;
[InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The item sends the output if both inputs have received a non-zero signal within the timeframe. If set to 0, the inputs must receive a signal at the same time.")]
[InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The item sends the output if both inputs have received a non-zero signal within the timeframe. If set to 0, the inputs must receive a signal at the same time.", alwaysUseInstanceValues: true)]
public float TimeFrame
{
get { return timeFrame; }
@@ -23,14 +23,14 @@ namespace Barotrauma.Items.Components
}
}
[InGameEditable, Serialize("1", true, description: "The signal sent when both inputs have received a non-zero signal.")]
[InGameEditable, Serialize("1", true, description: "The signal sent when the condition is met.", alwaysUseInstanceValues: true)]
public string Output
{
get { return output; }
set { output = value; }
}
[InGameEditable, Serialize("", true, description: "The signal sent when both inputs have not received a non-zero signal (if empty, no signal is sent).")]
[InGameEditable, Serialize("", true, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)]
public string FalseOutput
{
get { return falseOutput; }
@@ -15,7 +15,7 @@ namespace Barotrauma.Items.Components
//the output is sent if both inputs have received a signal within the timeframe
protected float timeFrame;
[Serialize(999999.0f, true, description: "The output of the item is restricted below this value."),
[Serialize(999999.0f, true, description: "The output of the item is restricted below this value.", alwaysUseInstanceValues: true),
InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f)]
public float ClampMax
{
@@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(-999999.0f, true, description: "The output of the item is restricted above this value."),
[Serialize(-999999.0f, true, description: "The output of the item is restricted above this value.", alwaysUseInstanceValues: true),
InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f)]
public float ClampMin
{
@@ -32,8 +32,8 @@ namespace Barotrauma.Items.Components
}
[InGameEditable(DecimalCount = 2),
Serialize(0.0f, true, description: "The item must have received signals to both inputs within this timeframe to output the sum of the signals." +
" If set to 0, the inputs must be received at the same time.")]
Serialize(0.0f, true, description: "The item must have received signals to both inputs within this timeframe to output the result." +
" If set to 0, the inputs must be received at the same time.", alwaysUseInstanceValues: true)]
public float TimeFrame
{
get { return timeFrame; }
@@ -21,7 +21,7 @@ namespace Barotrauma.Items.Components
private List<ushort> disconnectedWireIds;
[Editable, Serialize(false, true, description: "Locked connection panels cannot be rewired in-game.")]
[Editable, Serialize(false, true, description: "Locked connection panels cannot be rewired in-game.", alwaysUseInstanceValues: true)]
public bool Locked
{
get;
@@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components
private DelayedSignal prevQueuedSignal;
private float delay;
[InGameEditable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, DecimalCount = 2), Serialize(1.0f, true, description: "How long the item delays the signals (in seconds).")]
[InGameEditable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, DecimalCount = 2), Serialize(1.0f, true, description: "How long the item delays the signals (in seconds).", alwaysUseInstanceValues: true)]
public float Delay
{
get { return delay; }
@@ -43,14 +43,14 @@ namespace Barotrauma.Items.Components
}
}
[InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when a new one is received.")]
[InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when a new one is received.", alwaysUseInstanceValues: true)]
public bool ResetWhenSignalReceived
{
get;
set;
}
[InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when the incoming signal changes.")]
[InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when the incoming signal changes.", alwaysUseInstanceValues: true)]
public bool ResetWhenDifferentSignalReceived
{
get;
@@ -15,21 +15,21 @@ namespace Barotrauma.Items.Components
//the output is sent if both inputs have received a signal within the timeframe
protected float timeFrame;
[InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signals are equal.")]
[InGameEditable, Serialize("1", true, description: "The signal this item outputs when the condition is met.", alwaysUseInstanceValues: true)]
public string Output
{
get { return output; }
set { output = value; }
}
[InGameEditable, Serialize("", true, description: "The signal this item outputs when the received signals are not equal.")]
[InGameEditable, Serialize("", true, description: "The signal this item outputs when the condition is not met.", alwaysUseInstanceValues: true)]
public string FalseOutput
{
get { return falseOutput; }
set { falseOutput = value; }
}
[InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The maximum amount of time between the received signals. If set to 0, the signals must be received at the same time.")]
[InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The maximum amount of time between the received signals. If set to 0, the signals must be received at the same time.", alwaysUseInstanceValues: true)]
public float TimeFrame
{
get { return timeFrame; }
@@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components
class ExponentiationComponent : ItemComponent
{
private float exponent;
[InGameEditable, Serialize(1.0f, false, description: "The exponent of the operation.")]
[InGameEditable, Serialize(1.0f, false, description: "The exponent of the operation.", alwaysUseInstanceValues: true)]
public float Exponent
{
get
@@ -16,7 +16,7 @@ namespace Barotrauma.Items.Components
SquareRoot
}
[Serialize(FunctionType.Round, false, description: "Which kind of function to run the input through.")]
[Serialize(FunctionType.Round, false, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)]
public FunctionType Function
{
get; set;

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