This commit is contained in:
Evil Factory
2022-02-24 14:30:39 -03:00
364 changed files with 10838 additions and 3966 deletions
@@ -150,7 +150,7 @@ namespace Barotrauma
private CoroutineHandle disableTailCoroutine;
private readonly IEnumerable<Body> myBodies;
private readonly List<Body> myBodies;
public LatchOntoAI LatchOntoAI { get; private set; }
public SwarmBehavior SwarmBehavior { get; private set; }
@@ -207,8 +207,10 @@ namespace Barotrauma
} = new HashSet<Submarine>();
public bool IsTargetingPlayerTeam => IsTargetInPlayerTeam(SelectedAiTarget);
public bool IsBeingChasedBy(Character c) => c.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity is Character && (enemyAI.State == AIState.Aggressive || enemyAI.State == AIState.Attack);
private bool IsBeingChased => SelectedAiTarget?.Entity is Character targetCharacter && IsBeingChasedBy(targetCharacter);
public static bool IsTargetBeingChasedBy(Character target, Character character)
=> character?.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity == target && (enemyAI.State == AIState.Attack || enemyAI.State == AIState.Aggressive);
public bool IsBeingChasedBy(Character c) => IsTargetBeingChasedBy(Character, c);
private bool IsBeingChased => IsBeingChasedBy(SelectedAiTarget?.Entity as Character);
private bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam;
@@ -306,7 +308,8 @@ namespace Barotrauma
requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody);
myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
myBodies.Add(Character.AnimController.Collider.FarseerBody);
}
private CharacterParams.AIParams _aiParams;
@@ -339,7 +342,7 @@ namespace Barotrauma
{
targetingTag = "dead";
}
else if (AIParams.TryGetTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold > Character.GetDamageDoneByAttacker(targetCharacter))
else if (AIParams.TryGetTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold >= Character.GetDamageDoneByAttacker(targetCharacter))
{
targetingTag = tp.Tag;
}
@@ -530,8 +533,7 @@ namespace Barotrauma
selectedTargetingParams = targetingParams;
State = targetingParams.State;
}
if (SelectedAiTarget?.Entity != null &&
(LatchOntoAI == null || !LatchOntoAI.IsAttached || wallTarget != null) &&
if ((LatchOntoAI == null || !LatchOntoAI.IsAttached || wallTarget != null) &&
(State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive))
{
UpdateWallTarget(requiredHoleCount);
@@ -645,7 +647,7 @@ namespace Barotrauma
}
else
{
run = isBeingChased ? true : squaredDistance < Math.Pow(halfReactDistance, 2);
run = isBeingChased || squaredDistance < Math.Pow(halfReactDistance, 2);
State = AIState.Escape;
avoidTimer = AIParams.AvoidTime * 0.5f * Rand.Range(0.75f, 1.25f);
}
@@ -673,15 +675,21 @@ namespace Barotrauma
Character c = a.Character;
if (c.IsDead || c.Removed) { return false; }
if (!Character.IsFriendly(c)) { return true; }
// Only apply the threshold to friendly characters
if (!c.IsPlayer) { return false; }
// Only apply the threshold to players
return a.Damage >= selectedTargetingParams.Threshold;
}
Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character;
if (attacker != null)
//if the attacker has the same targeting tag as the character we're protecting, we can't change the TargetState
//otherwise e.g. a pet that's set to follow humans would start attacking all humans (and other pets, since they're considered part of the same group) when a hostile human attacks it
//TODO: a way for pets to differentiate hostile and friendly humans?
if (attacker?.AiTarget != null && !targetCharacter.SpeciesName.Equals(GetTargetingTag(attacker.AiTarget), StringComparison.OrdinalIgnoreCase))
{
// Attack the character that attacked the target we are protecting
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
SelectTarget(attacker.AiTarget);
State = AIState.Attack;
UpdateWallTarget(requiredHoleCount);
return;
}
}
@@ -1316,7 +1324,7 @@ namespace Barotrauma
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 &&
(!AIParams.TargetOuterWalls || !canAttackWalls && closestBody.UserData is Structure s && s.Submarine != null || !canAttackDoors && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent<Door>() != 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 or wall ahead
State = AIState.Idle;
@@ -1597,7 +1605,7 @@ namespace Barotrauma
}
else
{
sweepTimer = Rand.Range(-1000, 1000) * selectedTargetingParams.SweepSpeed;
sweepTimer = Rand.Range(-1000f, 1000f) * selectedTargetingParams.SweepSpeed;
}
}
break;
@@ -1837,7 +1845,7 @@ namespace Barotrauma
if (!attack.IsValidTarget(target)) { return false; }
if (target is ISerializableEntity se && target is Character)
{
if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; }
if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; }
}
if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { return false; }
if (attack.Ranged)
@@ -2182,10 +2190,22 @@ namespace Barotrauma
float margin = MathHelper.PiOver4 * distanceFactor;
if (angle < margin)
{
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(weapon.SimPosition, target.SimPosition, myBodies, collisionCategories, allowInsideFixture: true);
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(weapon.SimPosition, Character.GetRelativeSimPosition(target), myBodies, collisionCategories, allowInsideFixture: true);
if (pickedBody != null)
{
if (target is MapEntity)
{
if (pickedBody.UserData is Submarine sub && sub == target.Submarine)
{
return true;
}
else if (target == pickedBody.UserData)
{
return true;
}
}
Character t = null;
if (pickedBody.UserData is Character c)
{
@@ -2254,6 +2274,10 @@ namespace Barotrauma
if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed)
{
State = AIState.Idle;
if (Character.SelectedCharacter != null)
{
Character.DeselectCharacter();
}
return;
}
if (SelectedAiTarget.Entity is Character || SelectedAiTarget.Entity is Item)
@@ -2269,7 +2293,16 @@ namespace Barotrauma
Vector2 attackSimPosition = Character.GetRelativeSimPosition(SelectedAiTarget.Entity);
Vector2 limbDiff = attackSimPosition - mouthPos;
float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 2);
if (limbDiff.LengthSquared() < extent * extent)
bool tooFar = Character.InWater ? limbDiff.LengthSquared() > extent * extent : limbDiff.X > extent;
if (tooFar)
{
steeringManager.SteeringSeek(attackSimPosition - (mouthPos - SimPosition), 2);
if (Character.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
}
}
else
{
if (SelectedAiTarget.Entity is Character targetCharacter)
{
@@ -2285,14 +2318,12 @@ namespace Barotrauma
{
item.body.LinearVelocity *= 0.9f;
item.body.LinearVelocity -= limbDiff * 0.25f;
bool wasBroken = item.Condition <= 0.0f;
item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.1f), deltaTime);
item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime);
Character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
if (item.Condition <= 0.0f)
{
if (!wasBroken) { PetBehavior?.OnEat(item.GetTags(), 1.0f); }
if (!wasBroken) { PetBehavior?.OnEat(item); }
Entity.Spawner.AddToRemoveQueue(item);
}
}
@@ -2301,14 +2332,6 @@ namespace Barotrauma
steeringManager.SteeringManual(deltaTime, Vector2.Normalize(limbDiff) * 3);
Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos);
}
else
{
steeringManager.SteeringSeek(attackSimPosition - (mouthPos - SimPosition), 2);
if (Character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
}
}
}
else
{
@@ -2364,6 +2387,24 @@ namespace Barotrauma
}
#region Targeting
public static bool IsLatchedTo(Character target, Character character)
{
if (target.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null)
{
return enemyAI.LatchOntoAI.IsAttached && enemyAI.LatchOntoAI.TargetCharacter == character;
}
return false;
}
public static bool IsLatchedToSomeoneElse(Character target, Character character)
{
if (target.AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null)
{
return enemyAI.LatchOntoAI.IsAttached && enemyAI.LatchOntoAI.TargetCharacter != null && enemyAI.LatchOntoAI.TargetCharacter != character;
}
return false;
}
private bool IsLatchedOnSub => LatchOntoAI != null && LatchOntoAI.IsAttachedToSub;
//goes through all the AItargets, evaluates how preferable it is to attack the target,
@@ -2376,7 +2417,8 @@ namespace Barotrauma
selectedTargetMemory = null;
targetingParams = null;
bool isAnyTargetClose = false;
bool isBeingChased = IsBeingChased;
float maxModifier = 5;
foreach (AITarget aiTarget in AITarget.List)
{
if (aiTarget.InDetectable) { continue; }
@@ -2499,11 +2541,12 @@ namespace Barotrauma
// Ignore inner walls when outside (walltargets still work)
continue;
}
valueModifier = 1;
if (!Character.AnimController.CanEnterSubmarine && IsWallDisabled(s))
{
continue;
}
// Prefer weaker walls (200 is the default for normal hull walls)
valueModifier = 200f / s.MaxHealth;
for (int i = 0; i < s.Sections.Length; i++)
{
var section = s.Sections[i];
@@ -2515,12 +2558,12 @@ namespace Barotrauma
{
if (CanPassThroughHole(s, i))
{
valueModifier *= leadsInside ? (IsAggressiveBoarder ? 3 : 1) : 0;
valueModifier *= leadsInside ? (IsAggressiveBoarder ? maxModifier : 1) : 0;
}
else if (IsAggressiveBoarder && leadsInside && canAttackWalls && AIParams.TargetOuterWalls)
else if (IsAggressiveBoarder && leadsInside && canAttackWalls)
{
// Up to 25% priority increase for every gap in the wall when an aggressive boarder is outside
valueModifier *= 1 + section.gap.Open * 0.25f;
// Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside
valueModifier *= 1 + section.gap.Open;
}
}
else
@@ -2558,6 +2601,7 @@ namespace Barotrauma
// We are actually interested in breaking things -> reduce the priority when the wall is already broken
// (Terminalcells)
valueModifier *= 1 - section.gap.Open * 0.25f;
valueModifier = Math.Max(valueModifier, 0.1f);
}
}
}
@@ -2577,6 +2621,7 @@ namespace Barotrauma
valueModifier *= 1 + section.gap.Open;
}
}
valueModifier = Math.Clamp(valueModifier, 0, maxModifier);
}
}
if (door != null)
@@ -2588,7 +2633,7 @@ namespace Barotrauma
bool isOpen = door.CanBeTraversed;
if (!isOpen)
{
if (!canAttackDoors || isOutdoor && !AIParams.TargetOuterWalls) { continue; }
if (!canAttackDoors) { continue; }
}
else if (!Character.AnimController.CanEnterSubmarine)
{
@@ -2602,11 +2647,11 @@ namespace Barotrauma
// Increase the priority if the character is outside and the door is from outside to inside
if (door.CanBeTraversed)
{
valueModifier = 3;
valueModifier = maxModifier;
}
else if (door.LinkedGap != null)
{
valueModifier = 1 + door.LinkedGap.Open;
valueModifier = 1 + door.LinkedGap.Open * (maxModifier - 1);
}
}
else
@@ -2658,6 +2703,10 @@ namespace Barotrauma
}
}
}
if (targetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0)
{
valueModifier *= MathHelper.Lerp(1f, 0.1f, Character.HealthPercentage / 100f);
}
valueModifier *= targetParams.Priority;
if (valueModifier == 0.0f) { continue; }
if (targetingTag != "decoy")
@@ -2701,12 +2750,55 @@ namespace Barotrauma
if (SelectedAiTarget == aiTarget)
{
if (Character.Submarine == null && aiTarget.Entity is ISpatialEntity spatialEntity && spatialEntity.Submarine != null)
{
if (targetingTag == "door" || targetingTag == "wall")
{
Vector2 rayStart = Character.SimPosition;
Vector2 rayEnd = aiTarget.SimPosition + spatialEntity.Submarine.SimPosition;
Body closestBody = Submarine.PickBody(rayStart, rayEnd, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true);
if (closestBody != null && closestBody.UserData is ISpatialEntity hit)
{
Vector2 hitPos = hit.SimPosition;
if (closestBody.UserData is Submarine)
{
hitPos = Submarine.LastPickedPosition;
}
else if (hit.Submarine != null)
{
hitPos += hit.Submarine.SimPosition;
}
float subHalfWidth = spatialEntity.Submarine.Borders.Width / 2;
float subHalfHeight = spatialEntity.Submarine.Borders.Height / 2;
Vector2 diff = ConvertUnits.ToDisplayUnits(rayEnd - hitPos);
bool isOtherSideOfTheSub = Math.Abs(diff.X) > subHalfWidth || Math.Abs(diff.Y) > subHalfHeight;
if (isOtherSideOfTheSub)
{
IgnoreTarget(aiTarget);
ResetAITarget();
continue;
}
}
}
}
// Stick to the current target
valueModifier *= 1.1f;
}
if (!isBeingChased)
{
if (targetParams.State == AIState.Avoid || targetParams.State == AIState.PassiveAggressive || targetParams.State == AIState.Aggressive)
{
float reactDistance = targetParams.ReactDistance;
if (reactDistance > 0 && reactDistance < dist)
{
// The target is too far and should be ignored.
continue;
}
}
}
//if the target is very close, the distance doesn't make much difference
// -> just ignore the distance and attack whatever has the highest priority
// -> just ignore the distance and target whatever has the highest priority
dist = Math.Max(dist, 100.0f);
AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true);
if (Character.Submarine != null && !Character.Submarine.Info.IsRuin && Character.CurrentHull != null)
@@ -2719,19 +2811,22 @@ namespace Barotrauma
}
}
if (targetParams.AttackPattern == AttackPattern.Circle)
if (Character.Submarine == null && aiTarget.Entity?.Submarine != null && targetCharacter == null)
{
if (Character.Submarine == null && aiTarget.Entity?.Submarine != null && !isAnyTargetClose)
if (targetParams.AttackPattern == AttackPattern.Circle || targetParams.AttackPattern == AttackPattern.Sweep)
{
if (Submarine.MainSubs.Contains(aiTarget.Entity.Submarine))
if (!isAnyTargetClose)
{
// Prioritize targets that are near the horizontal center of the sub, but only when none of the targets is reachable.
float horizontalDistanceToSubCenter = Math.Abs(aiTarget.WorldPosition.X - aiTarget.Entity.Submarine.WorldPosition.X);
dist *= MathHelper.Lerp(1f, 5f, MathUtils.InverseLerp(0, 10000, horizontalDistanceToSubCenter));
}
else
{
dist *= 5;
if (Submarine.MainSubs.Contains(aiTarget.Entity.Submarine))
{
// Prioritize targets that are near the horizontal center of the sub, but only when none of the targets is reachable.
float horizontalDistanceToSubCenter = Math.Abs(aiTarget.WorldPosition.X - aiTarget.Entity.Submarine.WorldPosition.X);
dist *= MathHelper.Lerp(1f, 5f, MathUtils.InverseLerp(0, 10000, horizontalDistanceToSubCenter));
}
else if (targetParams.AttackPattern == AttackPattern.Circle)
{
dist *= 5;
}
}
}
}
@@ -2785,9 +2880,9 @@ namespace Barotrauma
{
if (Character.CurrentHull != null && targetCharacter.CurrentHull != Character.CurrentHull)
{
if (targetParams.State == AIState.Follow || targetParams.State == AIState.Protect || targetParams.State == AIState.Observe)
if (targetParams.State == AIState.Follow || targetParams.State == AIState.Protect || targetParams.State == AIState.Observe || targetParams.State == AIState.Eat)
{
// Ignore targets that cannot see
// Ignore targets that cannot be seen
if (!VisibleHulls.Contains(targetCharacter.CurrentHull))
{
continue;
@@ -2898,9 +2993,9 @@ namespace Barotrauma
if (HasValidPath(requireNonDirty: true)) { return; }
wallHits.Clear();
Structure wall = null;
Vector2 rayStart = AttackingLimb != null ? AttackingLimb.SimPosition : SimPosition;
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target))
{
Vector2 rayStart = SimPosition;
Vector2 rayEnd = SelectedAiTarget.SimPosition;
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
@@ -2914,7 +3009,6 @@ namespace Barotrauma
}
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Heading))
{
Vector2 rayStart = SimPosition;
Vector2 rayEnd = rayStart + VectorExtensions.Forward(Character.AnimController.Collider.Rotation + MathHelper.PiOver2, avoidLookAheadDistance * 5);
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
@@ -2930,7 +3024,6 @@ namespace Barotrauma
}
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Steering))
{
Vector2 rayStart = SimPosition;
Vector2 rayEnd = rayStart + Steering * 5;
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
@@ -2983,6 +3076,7 @@ namespace Barotrauma
// Blocked by a wall that shouldn't be targeted. The main intention here is to prevent monsters from entering the the tail and the nose pieces.
if (!isTargetingDoor)
{
IgnoreTarget(SelectedAiTarget);
ResetAITarget();
}
}
@@ -2994,6 +3088,7 @@ namespace Barotrauma
else
{
// Blocked by a disabled wall.
IgnoreTarget(SelectedAiTarget);
ResetAITarget();
}
}
@@ -3044,8 +3139,17 @@ namespace Barotrauma
if (!(hit.UserData is Structure w)) { return false; }
if (w.Submarine == null) { return false; }
if (w.Submarine != SelectedAiTarget.Entity.Submarine) { return false; }
if (Character.Submarine == null && w.prefab.Tags.Contains("inner")) { return false; }
if (!AIParams.TargetOuterWalls && !w.prefab.Tags.Contains("inner")) { return false; }
if (Character.Submarine == null)
{
if (w.prefab.Tags.Contains("inner"))
{
if (!Character.AnimController.CanEnterSubmarine) { return false; }
}
else if (!AIParams.TargetOuterWalls)
{
return false;
}
}
wall = w;
return true;
}
@@ -3082,7 +3186,8 @@ namespace Barotrauma
{
if (door.LinkedGap.Size > ConvertUnits.ToDisplayUnits(colliderWidth))
{
return SteerThroughGap(door.LinkedGap, door.LinkedGap.FlowTargetHull.WorldPosition, deltaTime, maxDistance: 100);
float maxDistance = Math.Max(ConvertUnits.ToDisplayUnits(colliderLength), 100);
return SteerThroughGap(door.LinkedGap, door.LinkedGap.FlowTargetHull.WorldPosition, deltaTime, maxDistance: maxDistance);
}
}
}
@@ -3279,7 +3384,7 @@ namespace Barotrauma
{
if (priority.HasValue)
{
targetParams.Priority = priority.Value;
targetParams.Priority = Math.Max(targetParams.Priority, priority.Value);
}
targetParams.State = state;
if (!modifiedParams.ContainsKey(tag))
@@ -3298,6 +3403,7 @@ namespace Barotrauma
/// <summary>
/// Temporarily changes the predefined state for a target. Eg. Idle -> Attack.
/// Note: does not change the current AIState!
/// </summary>
private void ChangeTargetState(Character target, AIState state, float? priority = null)
{
@@ -3319,14 +3425,14 @@ namespace Barotrauma
// --> Target the submarine too.
if (target.Submarine != null && Character.Submarine == null && (canAttackDoors || canAttackWalls))
{
ChangeParams("room", state, priority * 0.1f);
ChangeParams("room", state, priority / 2);
if (canAttackWalls)
{
ChangeParams("wall", state, priority * 0.1f);
ChangeParams("wall", state, priority / 2);
}
if (canAttackDoors)
{
ChangeParams("door", state, priority * 0.1f);
ChangeParams("door", state, priority / 2);
}
}
ChangeParams("provocative", state, priority, onlyExisting: true);
@@ -3378,9 +3484,15 @@ namespace Barotrauma
private bool CanPerceive(AITarget target, float dist = -1, float distSquared = -1, bool checkVisibility = false)
{
if (target?.Entity == null) { return false; }
bool insideSightRange;
bool insideSoundRange;
checkVisibility = checkVisibility && Character.Submarine != null && target.Entity.Submarine == Character.Submarine;
if (checkVisibility)
{
// We only want to check the visibility when the target is in ruins/wreck/similiar place where sneaking should be possible.
// When the monsters attack the player sub, they wall hack so that they can be more aggressive.
checkVisibility = target.Entity.Submarine != null && target.Entity.Submarine == Character.Submarine && target.Entity.Submarine.TeamID == CharacterTeamType.None;
}
if (dist > 0)
{
insideSightRange = IsInRange(dist, target.SightRange, Sight);
@@ -3539,12 +3651,12 @@ namespace Barotrauma
public override bool SteerThroughGap(Gap gap, Vector2 targetWorldPos, float deltaTime, float maxDistance = -1)
{
wallTarget = null;
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2);
Character.AnimController.ReleaseStuckLimbs();
bool success = base.SteerThroughGap(gap, targetWorldPos, deltaTime, maxDistance);
if (success)
{
wallTarget = null;
LatchOntoAI?.DeattachFromBody(reset: true, cooldown: 2);
Character.AnimController.ReleaseStuckLimbs();
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
}
IsSteeringThroughGap = success;
@@ -20,7 +20,6 @@ namespace Barotrauma
private float reactTimer;
private float unreachableClearTimer;
private bool shouldCrouch;
public bool IsInsideCave { get; private set; }
/// <summary>
/// Resets each frame
/// </summary>
@@ -58,14 +57,14 @@ namespace Barotrauma
private float obstacleRaycastTimer;
private readonly float enemyCheckInterval = 0.2f;
private readonly float enemySpotDistanceOutside = 1500;
private readonly float enemySpotDistanceOutside = 800;
private readonly float enemySpotDistanceInside = 1000;
private float enemycheckTimer;
/// <summary>
/// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity.
/// How far other characters can hear reports done by this character (e.g. reports for fires, intruders).
/// </summary>
public float ReportRange { get; set; } = float.PositiveInfinity;
public float ReportRange { get; set; }
private float _aimSpeed = 1;
public float AimSpeed
@@ -167,6 +166,7 @@ namespace Barotrauma
objectiveManager = new AIObjectiveManager(c);
reactTimer = GetReactionTime();
SortTimer = Rand.Range(0f, sortObjectiveInterval);
ReportRange = Character.IsOnPlayerTeam ? float.PositiveInfinity : 1000;
}
public override void Update(float deltaTime)
@@ -306,7 +306,7 @@ namespace Barotrauma
UseIndoorSteeringOutside = false;
}
if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted)
if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID))
{
// Spot enemies while staying outside or inside an enemy ship.
// does not apply for escorted characters, such as prisoners or terrorists who have their own behavior
@@ -327,9 +327,13 @@ namespace Barotrauma
float dist = toTarget.LengthSquared();
float maxDistance = Character.Submarine == null ? enemySpotDistanceOutside : enemySpotDistanceInside;
if (dist > maxDistance * maxDistance) { continue; }
Vector2 forward = VectorExtensions.Forward(Character.AnimController.Collider.Rotation);
forward.X *= Character.AnimController.Dir;
if (Vector2.Dot(toTarget, forward) < 0.2f) { continue; }
if (EnemyAIController.IsLatchedToSomeoneElse(c, Character)) { continue; }
var head = Character.AnimController.GetLimb(LimbType.Head);
if (head == null) { continue; }
float rotation = head.body.TransformedRotation;
Vector2 forward = VectorExtensions.Forward(rotation);
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward));
if (angle > 70) { continue; }
if (!Character.CanSeeCharacter(c)) { continue; }
if (dist < closestDistance || closestEnemy == null)
{
@@ -344,8 +348,6 @@ namespace Barotrauma
}
}
}
IsInsideCave = Character.CurrentHull == null && Level.Loaded?.Caves.FirstOrDefault(c => c.Area.Contains(Character.WorldPosition)) is Level.Cave;
if (UseIndoorSteeringOutside || Character.CurrentHull?.Submarine != null || hasValidPath || IsCloseEnoughToTarget(steeringBuffer))
{
@@ -565,7 +567,7 @@ namespace Barotrauma
Character.AnimController.HeadInWater ||
Character.Submarine == null ||
(Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) ||
ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOn) ||
ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) ||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character;
@@ -630,6 +632,7 @@ namespace Barotrauma
{
divingSuit.Drop(Character);
HandleRelocation(divingSuit);
ReequipUnequipped();
}
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit)
{
@@ -657,6 +660,7 @@ namespace Barotrauma
{
divingSuit.Drop(Character);
HandleRelocation(divingSuit);
ReequipUnequipped();
}
}
}
@@ -675,6 +679,7 @@ namespace Barotrauma
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
{
@@ -699,6 +704,7 @@ namespace Barotrauma
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
}
}
@@ -832,6 +838,8 @@ namespace Barotrauma
if (container == null) { return 0; }
if (!container.HasAccess(character)) { return 0; }
if (!container.Inventory.CanBePut(containableItem)) { return 0; }
var rootContainer = container.Item.GetRootContainer();
if (rootContainer?.GetComponent<Fabricator>() != null || rootContainer?.GetComponent<Fabricator>() != null) { return 0; }
if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined))
{
if (isRestrictionsDefined)
@@ -876,7 +884,7 @@ namespace Barotrauma
foreach (Character target in Character.CharacterList)
{
if (target.CurrentHull != hull || !target.Enabled) { continue; }
if (AIObjectiveFightIntruders.IsValidTarget(target, Character))
if (AIObjectiveFightIntruders.IsValidTarget(target, Character, false))
{
if (!target.IsArrested && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
{
@@ -1096,7 +1104,7 @@ namespace Barotrauma
// excluding poisons etc
float realDamage = attackResult.Damage - healAmount;
// including poisons etc
float totalDamage = realDamage - healAmount;
float totalDamage = realDamage;
if (attackResult.Afflictions != null)
{
foreach (Affliction affliction in attackResult.Afflictions)
@@ -1135,7 +1143,15 @@ namespace Barotrauma
// Don't react to attackers that are outside of the sub (e.g. AoE attacks)
return;
}
bool isAttackerInfected = false;
bool isAttackerFightingEnemy = false;
float minorDamageThreshold = 1;
float majorDamageThreshold = 20;
if (attacker.TeamID == Character.TeamID)
{
minorDamageThreshold = 10;
majorDamageThreshold = 40;
}
if (IsFriendly(attacker))
{
if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character)
@@ -1144,63 +1160,58 @@ namespace Barotrauma
// Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate).
return;
}
float cumulativeDamage = Character.GetDamageDoneByAttacker(attacker);
float cumulativeDamage = realDamage + Character.GetDamageDoneByAttacker(attacker);
bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null;
if (isAccidental)
{
if (!Character.IsSecurity && cumulativeDamage > 1)
if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
}
}
else
{
isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength("alieninfection") > 0;
// Inform other NPCs
if (cumulativeDamage > 1 || totalDamage >= 10)
if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold)
{
InformOtherNPCs(cumulativeDamage);
if (GameMain.IsMultiplayer || !attacker.IsPlayer || Character.TeamID != attacker.TeamID)
{
InformOtherNPCs(cumulativeDamage);
}
}
if (Character.IsBot)
{
if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; }
if (attacker.IsPlayer)
var combatMode = DetermineCombatMode(Character, cumulativeDamage);
if (attacker.IsPlayer && !Character.IsInstigator && !ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>())
{
if (Character.IsSecurity)
switch (combatMode)
{
if (attacker.TeamID != Character.TeamID && cumulativeDamage > 1 || cumulativeDamage > 10)
{
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.50f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 30.0f);
}
else
{
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.50f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 30.0f);
}
}
else if (!Character.IsInstigator && cumulativeDamage > 1)
{
Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", minDurationBetweenSimilar: 30.0f);
}
}
if (cumulativeDamage > 1 && attacker.TeamID != Character.TeamID)
{
// If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay.
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage), attacker, delay: realDamage > 1 ? GetReactionTime() : 0);
}
else
{
// Don't react to minor (accidental) dmg done by characters that are in the same team
if (cumulativeDamage < 10)
{
if (!Character.IsSecurity && cumulativeDamage > 1)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
}
}
else
{
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage, dmgThreshold: 50), attacker, GetReactionTime() * 2);
case AIObjectiveCombat.CombatMode.Defensive:
case AIObjectiveCombat.CombatMode.Retreat:
if (Character.IsSecurity)
{
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.5f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 10.0f);
}
else
{
Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.5f, "attackedbyfriendly", minDurationBetweenSimilar: 10.0f);
}
break;
case AIObjectiveCombat.CombatMode.Offensive:
case AIObjectiveCombat.CombatMode.Arrest:
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.5f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 10.0f);
break;
case AIObjectiveCombat.CombatMode.None:
if (Character.IsSecurity && realDamage > 1)
{
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.5f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 10.0f);
}
break;
}
}
// If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay.
AddCombatObjective(combatMode, attacker, delay: realDamage > 1 ? GetReactionTime() : 0);
}
if (!isAttackerFightingEnemy)
{
@@ -1213,15 +1224,15 @@ namespace Barotrauma
if (Character.Submarine != null && Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
{
// Non-friendly
InformOtherNPCs(Character.GetDamageDoneByAttacker(attacker));
InformOtherNPCs();
}
if (Character.IsBot)
{
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker);
AddCombatObjective(DetermineCombatMode(Character), attacker);
}
}
void InformOtherNPCs(float cumulativeDamage)
void InformOtherNPCs(float cumulativeDamage = 0)
{
foreach (Character otherCharacter in Character.CharacterList)
{
@@ -1237,25 +1248,25 @@ namespace Barotrauma
{
//if the other character did not witness the attack, and the character is not within report range (or capable of reporting)
//don't react to the attack
if (Character.IsDead || Character.IsUnconscious || !CheckReportRange(Character, otherCharacter, ReportRange))
if (Character.IsDead || Character.IsUnconscious || otherCharacter.TeamID != Character.TeamID || !CheckReportRange(Character, otherCharacter, ReportRange))
{
continue;
}
}
var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing, dmgThreshold: attacker.TeamID == Character.TeamID ? 50 : 10);
var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing);
float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced);
otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
}
}
AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage, bool isWitnessing = false, float dmgThreshold = 10, bool allowOffensive = true)
AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage = 0, bool isWitnessing = false)
{
if (!IsFriendly(attacker))
{
if (Character.Submarine == null)
{
// Outside -> don't react.
return AIObjectiveCombat.CombatMode.None;
// Outside
return attacker.Submarine == null ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat;
}
if (!Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
{
@@ -1268,6 +1279,15 @@ namespace Barotrauma
}
else
{
if (isAttackerInfected)
{
cumulativeDamage = 100;
}
if (GameMain.IsSingleplayer && attacker.IsPlayer && Character.TeamID == attacker.TeamID)
{
// Bots in the player team never act aggressively in single player when attacked by the player
return cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None;
}
if (Character.Submarine == null || !Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
{
// Outside or attacked from an unconnected submarine -> don't react.
@@ -1279,17 +1299,17 @@ namespace Barotrauma
isAttackerFightingEnemy = true;
return AIObjectiveCombat.CombatMode.None;
}
else if (isWitnessing && Character.CombatAction != null && !c.IsSecurity)
if (isWitnessing && Character.CombatAction != null && !c.IsSecurity)
{
return Character.CombatAction.WitnessReaction;
}
else if (attacker.IsPlayer && FindInstigator() is Character instigator)
if (attacker.IsPlayer && FindInstigator() is Character instigator)
{
// The guards don't react when the player there's an instigator around
// The guards don't react to player's aggressions when there's an instigator around
isAttackerFightingEnemy = true;
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (instigator.CombatAction != null ? instigator.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
}
else if (attacker.TeamID == CharacterTeamType.FriendlyNPC && attacker.AIController != null && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable))
if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable))
{
if (c.IsSecurity)
{
@@ -1307,25 +1327,25 @@ namespace Barotrauma
// Already targeting the attacker -> treat as a more serious threat.
cumulativeDamage *= 2;
}
if (attackResult.Afflictions != null && attackResult.Afflictions.Any(a => a is AfflictionHusk))
{
cumulativeDamage = 100;
}
if (cumulativeDamage > dmgThreshold)
if (cumulativeDamage > majorDamageThreshold)
{
if (c.IsSecurity)
{
return c.IsSecurity && allowOffensive ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest;
return AIObjectiveCombat.CombatMode.Offensive;
}
else
{
return c == Character ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat;
}
}
else
else if (cumulativeDamage > minorDamageThreshold)
{
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
}
else
{
return AIObjectiveCombat.CombatMode.None;
}
}
Character FindInstigator()
@@ -1758,7 +1778,7 @@ namespace Barotrauma
foreach (var enemy in Character.CharacterList)
{
if (enemy.CurrentHull != hull) { continue; }
if (AIObjectiveFightIntruders.IsValidTarget(enemy, character))
if (AIObjectiveFightIntruders.IsValidTarget(enemy, character, false))
{
AddTargets<AIObjectiveFightIntruders, Character>(character, enemy);
}
@@ -1838,7 +1858,7 @@ namespace Barotrauma
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreWater = HasDivingSuit(character);
bool ignoreOxygen = ignoreWater || HasDivingMask(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders);
bool ignoreEnemies = ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
if (isCurrentHull)
{
@@ -2059,10 +2079,12 @@ namespace Barotrauma
public static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter)
{
operatingCharacter = null;
if (target?.Item == null) { return false; }
float highestPriority = -1.0f;
float highestPriorityModifier = -1.0f;
foreach (Character c in Character.CharacterList)
{
if (c == null) { continue; }
if (c.Removed) { continue; }
if (c.TeamID != team) { continue; }
if (c.IsIncapacitated) { continue; }
@@ -2071,12 +2093,12 @@ namespace Barotrauma
operatingCharacter = c;
return true;
}
if (c.AIController is HumanAIController humanAI)
if (c.AIController is HumanAIController humanAI && humanAI.ObjectiveManager is AIObjectiveManager objectiveManager)
{
foreach (var objective in humanAI.ObjectiveManager.Objectives)
foreach (var objective in objectiveManager.Objectives)
{
if (!(objective is AIObjectiveOperateItem operateObjective)) { continue; }
if (operateObjective.Component.Item != target.Item) { continue; }
if (operateObjective.Component?.Item != target.Item) { continue; }
if (operateObjective.Priority < highestPriority) { continue; }
if (operateObjective.PriorityModifier < highestPriorityModifier) { continue; }
operatingCharacter = c;
@@ -9,10 +9,10 @@ namespace Barotrauma
{
class IndoorsSteeringManager : SteeringManager
{
private PathFinder pathFinder;
private readonly PathFinder pathFinder;
private SteeringPath currentPath;
private bool canOpenDoors;
private readonly bool canOpenDoors;
public bool CanBreakDoors { get; set; }
private bool ShouldBreakDoor(Door door) =>
@@ -20,7 +20,7 @@ namespace Barotrauma
!door.Item.Indestructible && !door.Item.InvulnerableToDamage &&
(door.Item.Submarine == null || door.Item.Submarine.TeamID != character.TeamID);
private Character character;
private readonly Character character;
private Vector2 currentTarget;
@@ -51,19 +51,13 @@ namespace Barotrauma
private set;
}
/// <summary>
/// Returns true if the current or the next node is in ladders.
/// </summary>
public bool InLadders =>
currentPath != null && currentPath.CurrentNode != null &&
(currentPath.CurrentNode.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character) ||
(currentPath.NextNode != null && currentPath.NextNode.Ladders != null && currentPath.NextNode.Ladders.Item.IsInteractable(character)));
/// <summary>
/// Returns true if any node in the path is in stairs
/// </summary>
public bool InStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
public bool IsCurrentNodeLadder => currentPath?.CurrentNode?.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character);
public bool IsNextNodeLadder => GetNextLadder() != null;
public bool IsNextLadderSameAsCurrent
@@ -83,8 +77,10 @@ namespace Barotrauma
public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors) : base(host)
{
pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true);
pathFinder.GetNodePenalty = GetNodePenalty;
pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true)
{
GetNodePenalty = GetNodePenalty
};
this.canOpenDoors = canOpenDoors;
this.CanBreakDoors = canBreakDoors;
@@ -99,14 +95,24 @@ namespace Barotrauma
base.Update(speed);
float step = 1.0f / 60.0f;
checkDoorsTimer -= step;
buttonPressTimer -= step;
if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsOpen)
{
buttonPressTimer = 0;
}
else
{
buttonPressTimer -= step;
}
findPathTimer -= step;
}
public void SetPath(SteeringPath path)
{
currentPath = path;
if (path.Nodes.Any()) currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
if (path.Nodes.Any())
{
currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
}
findPathTimer = Math.Min(findPathTimer, 1.0f);
IsPathDirty = false;
}
@@ -124,15 +130,9 @@ namespace Barotrauma
public void SteeringSeek(Vector2 target, float weight, float minGapWidth = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisiblity = true)
{
if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.state && !lastDoor.door.IsOpen)
{
// We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again.
Reset();
}
else
{
steering += CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
}
// Have to use a variable here or resetting doesn't work.
Vector2 addition = CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
steering += addition;
}
/// <summary>
@@ -328,7 +328,13 @@ namespace Barotrauma
{
CheckDoorsInPath();
doorsChecked = true;
}
}
if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsOpen)
{
// We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again.
Reset();
return Vector2.Zero;
}
Vector2 pos = host.WorldPosition;
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
// Only humanoids can climb ladders
@@ -378,7 +384,7 @@ namespace Barotrauma
//at the same height as the waypoint
if (Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y) < (collider.height / 2 + collider.radius) * 1.25f)
{
float heightFromFloor = character.AnimController.GetColliderBottom().Y - character.AnimController.FloorY;
float heightFromFloor = character.AnimController.GetHeightFromFloor();
if (heightFromFloor <= 0.0f)
{
diff.Y = Math.Max(diff.Y, 100);
@@ -451,12 +457,20 @@ namespace Barotrauma
// 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;
bool isTargetTooHigh = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y + characterHeight;
bool isTargetTooLow = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y;
var door = currentPath.CurrentNode.ConnectedDoor;
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
if (currentPath.CurrentNode.Stairs != null && currentPath.NextNode?.Stairs == null)
{
margin = 1;
if (currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + character.AnimController.ColliderHeightFromFloor * 0.25f)
{
isTargetTooLow = true;
}
}
float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && (door == null || door.CanBeTraversed))
if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow && (door == null || door.CanBeTraversed))
{
NextNode(!doorsChecked);
}
@@ -504,6 +518,16 @@ namespace Barotrauma
canAccessButtons = true;
}
}
foreach (var linked in door.Item.linkedTo)
{
if (!(linked is Item linkedItem)) { continue; }
var button = linkedItem.GetComponent<Controller>();
if (button == null) { continue; }
if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)))
{
canAccessButtons = true;
}
}
return canAccessButtons || door.IsOpen || ShouldBreakDoor(door);
}
}
@@ -516,7 +540,7 @@ namespace Barotrauma
return ConvertUnits.ToDisplayUnits(Math.Max(colliderSize.X, colliderSize.Y));
}
private (Door door, bool state) lastDoor;
private (Door door, bool shouldBeOpen) lastDoor;
private float GetDoorCheckTime()
{
if (steering.LengthSquared() > 0)
@@ -539,7 +563,6 @@ namespace Barotrauma
WayPoint nextWaypoint = null;
Door door = null;
bool shouldBeOpen = false;
if (currentPath.Nodes.Count == 1)
{
door = currentPath.Nodes.First().ConnectedDoor;
@@ -645,7 +668,7 @@ namespace Barotrauma
});
if (canAccess)
{
bool pressButton = buttonPressTimer <= 0 || lastDoor.door != door || lastDoor.state != shouldBeOpen;
bool pressButton = buttonPressTimer <= 0 || lastDoor.door != door || lastDoor.shouldBeOpen != shouldBeOpen;
if (door.HasIntegratedButtons)
{
if (pressButton && character.CanSeeTarget(door.Item))
@@ -653,7 +676,7 @@ namespace Barotrauma
if (door.Item.TryInteract(character, forceSelectKey: true))
{
lastDoor = (door, shouldBeOpen);
buttonPressTimer = buttonPressCooldown;
buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0;
}
else
{
@@ -671,7 +694,7 @@ namespace Barotrauma
if (closestButton.Item.TryInteract(character, forceSelectKey: true))
{
lastDoor = (door, shouldBeOpen);
buttonPressTimer = buttonPressCooldown;
buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0;
}
else
{
@@ -697,7 +720,6 @@ namespace Barotrauma
// The button is on the wrong side of the door or a wall
currentPath.Unreachable = true;
}
lastDoor = (null, false);
return;
}
}
@@ -16,7 +16,6 @@ namespace Barotrauma
public virtual bool IgnoreUnsafeHulls => false;
public virtual bool AbandonWhenCannotCompleteSubjectives => true;
public virtual bool AllowSubObjectiveSorting => false;
public virtual bool ForceOrderPriority => true;
public virtual bool PrioritizeIfSubObjectivesActive => false;
/// <summary>
@@ -31,6 +30,8 @@ namespace Barotrauma
public virtual bool ConcurrentObjectives => false;
public virtual bool KeepDivingGearOn => false;
public virtual bool KeepDivingGearOnAlsoWhenInactive => false;
/// <summary>
/// There's a separate property for diving suit and mask: KeepDivingGearOn.
/// </summary>
@@ -85,11 +85,12 @@ namespace Barotrauma
bool equip = item.GetComponent<Holdable>() != null ||
item.AllowedSlots.Any(s => s != InvSlotType.Any) &&
item.AllowedSlots.None(s =>
s == InvSlotType.Card ||
s == InvSlotType.Head ||
s == InvSlotType.Headset ||
s == InvSlotType.InnerClothes ||
s == InvSlotType.OuterClothes);
s == InvSlotType.Card ||
s == InvSlotType.Head ||
s == InvSlotType.Headset ||
s == InvSlotType.InnerClothes ||
s == InvSlotType.OuterClothes ||
s == InvSlotType.HealthInterface);
TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
{
@@ -11,7 +11,7 @@ namespace Barotrauma
public override string Identifier { get; set; } = "cleanup items";
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => false;
public override bool ForceOrderPriority => false;
protected override bool ForceOrderPriority => false;
public readonly List<Item> prioritizedItems = new List<Item>();
@@ -52,7 +52,7 @@ namespace Barotrauma
// The validity changes when a character picks the item up.
if (!IsValidTarget(target, character, checkInventory: true)) { return Objectives.ContainsKey(target) && IsItemInsideValidSubmarine(target, character); }
if (target.CurrentHull.FireSources.Count > 0) { return false; }
// Don't repair items in rooms that have enemies inside.
// Don't clean up items in rooms that have enemies inside.
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
return true;
}
@@ -30,7 +30,7 @@ namespace Barotrauma
private float holdFireTimer;
private bool hasAimed;
private bool isLethalWeapon;
private bool AllowCoolDown => !IsOffensiveOrArrest || Mode != initialMode;
private bool AllowCoolDown => !IsOffensiveOrArrest || Mode != initialMode || character.TeamID == Enemy.TeamID;
public Character Enemy { get; private set; }
public bool HoldPosition { get; set; }
@@ -117,7 +117,10 @@ namespace Barotrauma
private float AimSpeed => HumanAIController.AimSpeed;
private float AimAccuracy => HumanAIController.AimAccuracy;
private bool EnemyIsClose() => Enemy != null && character.CurrentHull != null && character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500;
private bool IsEnemyCloserThan(float margin) =>
Enemy != null && Enemy.CurrentHull != null &&
character.InWater && Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin ||
HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull) && Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X) < margin;
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f)
: base(character, objectiveManager, priorityModifier)
@@ -143,13 +146,20 @@ namespace Barotrauma
{
Mode = CombatMode.Retreat;
}
spreadTimer = Rand.Range(-10, 10);
spreadTimer = Rand.Range(-10f, 10f);
SetAimTimer(Rand.Range(1f, 1.5f) / AimSpeed);
HumanAIController.SortTimer = 0;
}
protected override float GetPriority()
{
if (character.TeamID == CharacterTeamType.FriendlyNPC && Enemy != null)
if (Enemy == null)
{
Priority = 0;
Abandon = true;
return Priority;
}
if (character.TeamID == CharacterTeamType.FriendlyNPC)
{
if (Enemy.Submarine == null || (Enemy.Submarine.TeamID != character.TeamID && Enemy.Submarine != character.Submarine))
{
@@ -160,6 +170,13 @@ namespace Barotrauma
}
float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, character.GetDamageDoneByAttacker(Enemy) / 100.0f);
Priority = TargetEliminated ? 0 : Math.Min((95 + damageFactor) * PriorityModifier, 100);
if (Priority > 0)
{
if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character))
{
Priority = 0;
}
}
return Priority;
}
@@ -366,7 +383,7 @@ namespace Barotrauma
}
}
}
bool isAllowedToSeekWeapons = !EnemyIsClose() && character.TeamID != CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest;
bool isAllowedToSeekWeapons = character.CurrentHull != null && !IsEnemyCloserThan(300) && character.IsOnPlayerTeam && IsOffensiveOrArrest;
if (!isAllowedToSeekWeapons)
{
if (WeaponComponent == null)
@@ -418,9 +435,16 @@ namespace Barotrauma
onCompleted: () => RemoveSubObjective(ref seekWeaponObjective),
onAbandon: () =>
{
SpeakNoWeapons();
RemoveSubObjective(ref seekWeaponObjective);
Mode = CombatMode.Retreat;
if (Weapon == null)
{
SpeakNoWeapons();
Mode = CombatMode.Retreat;
}
else
{
Mode = CombatMode.Defensive;
}
});
}
}
@@ -478,13 +502,25 @@ namespace Barotrauma
weaponComponent = null;
float bestPriority = 0;
float lethalDmg = -1;
bool enemyIsClose = EnemyIsClose();
bool isAllowedToSeekWeapons = !IsEnemyCloserThan(300);
bool prioritizeMelee = IsEnemyCloserThan(50) || EnemyAIController.IsLatchedTo(Enemy, character);
foreach (var weapon in weaponList)
{
float priority = weapon.CombatPriority;
if (prioritizeMelee)
{
if (weapon is MeleeWeapon)
{
priority *= 5;
}
else
{
priority /= 2;
}
}
if (!weapon.IsLoaded(character))
{
if (weapon is RangedWeapon && enemyIsClose)
if (weapon is RangedWeapon && !isAllowedToSeekWeapons)
{
// Close to the enemy. Ignore weapons that don't have any ammunition (-> Don't seek ammo).
continue;
@@ -693,7 +729,7 @@ namespace Barotrauma
var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s));
if (character.Inventory.TryPutItem(Weapon, character, slots))
{
aimTimer = Rand.Range(0.2f, 0.4f) / AimSpeed;
SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
}
else
{
@@ -1014,7 +1050,7 @@ namespace Barotrauma
}
if (!canSeeTarget)
{
aimTimer = Rand.Range(0.2f, 0.4f) / AimSpeed;
SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
return;
}
if (Weapon.RequireAimToUse)
@@ -1074,7 +1110,7 @@ namespace Barotrauma
else if (!character.IsFacing(Enemy.WorldPosition))
{
// Don't do the facing check if we are close to the target, because it easily causes the character to get stuck here when it flips around.
aimTimer = Rand.Range(1f, 1.5f) / AimSpeed;
SetAimTimer(Rand.Range(1f, 1.5f) / AimSpeed);
}
}
else
@@ -1177,7 +1213,7 @@ namespace Barotrauma
}
private void SpeakNoWeapons() => Speak("dialogcombatnoweapons", delay: 0, minDuration: 30);
private void AskHelp() => Speak("dialogcombatretreating", delay: Rand.Range(0, 1), minDuration: 20);
private void AskHelp() => Speak("dialogcombatretreating", delay: Rand.Range(0f, 1f), minDuration: 20);
private void Speak(string textIdentifier, float delay, float minDuration)
{
@@ -1191,18 +1227,6 @@ namespace Barotrauma
}
}
//private float CalculateEnemyStrength()
//{
// float enemyStrength = 0;
// AttackContext currentContext = character.GetAttackContext();
// foreach (Limb limb in Enemy.AnimController.Limbs)
// {
// if (limb.attack == null) continue;
// if (!limb.attack.IsValidContext(currentContext)) { continue; }
// if (!limb.attack.IsValidTarget(AttackTarget.Character)) { continue; }
// enemyStrength += limb.attack.GetTotalDamage(false);
// }
// return enemyStrength;
//}
private void SetAimTimer(float newTimer) => aimTimer = Math.Max(aimTimer, newTimer);
}
}
@@ -120,7 +120,7 @@ namespace Barotrauma
}
if (character.CanInteractWith(container.Item, checkLinked: false))
{
if (RemoveExisting || (RemoveExistingWhenNecessary && !container.Inventory.CanBePut(item)))
if (RemoveExisting || (RemoveExistingWhenNecessary && !container.Inventory.CanBePut(ItemToContain)))
{
HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax);
}
@@ -159,7 +159,8 @@ namespace Barotrauma
{
TargetName = container.Item.Name,
AbortCondition = obj =>
container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) ||
container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) ||
(container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) ||
ItemToContain == null || ItemToContain.Removed ||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>()
@@ -40,6 +40,7 @@ namespace Barotrauma
public Func<Item, bool> RemoveExistingPredicate { get; set; }
public int? RemoveExistingMax { get; set; }
public string AbandonGetItemDialogueIdentifier { get; set; }
public Func<bool> AbandonGetItemDialogueCondition { get; set; }
public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -106,6 +107,7 @@ namespace Barotrauma
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip)
{
CannotFindDialogueCondition = AbandonGetItemDialogueCondition,
CannotFindDialogueIdentifierOverride = AbandonGetItemDialogueIdentifier,
SpeakIfFails = AbandonGetItemDialogueIdentifier != null,
TakeWholeStack = this.TakeWholeStack
@@ -12,10 +12,12 @@ namespace Barotrauma
protected override float TargetUpdateTimeMultiplier => 0.2f;
public bool TargetCharactersInOtherSubs { get; set; }
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target) => IsValidTarget(target, character);
protected override bool Filter(Character target) => IsValidTarget(target, character, TargetCharactersInOtherSubs);
protected override IEnumerable<Character> GetList() => Character.CharacterList;
@@ -26,6 +28,7 @@ namespace Barotrauma
if (totalEnemies == 0) { return 0; }
if (character.IsSecurity) { return 100; }
if (objectiveManager.IsOrder(this)) { return 100; }
// If there's any security officers onboard, leave fighting for them.
return HumanAIController.IsTrueForAnyCrewMember(c => c.Character.IsSecurity && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine) ? 0 : 100;
}
@@ -53,7 +56,7 @@ namespace Barotrauma
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
=> HumanAIController.RemoveTargets<AIObjectiveFightIntruders, Character>(character, target);
public static bool IsValidTarget(Character target, Character character)
public static bool IsValidTarget(Character target, Character character, bool targetCharactersInOtherSubs)
{
if (target == null || target.Removed) { return false; }
if (target.IsDead) { return false; }
@@ -64,8 +67,10 @@ namespace Barotrauma
if (target.CurrentHull == null) { return false; }
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
if (!targetCharactersInOtherSubs && character.Submarine.TeamID != target.Submarine.TeamID) { return false; }
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
if (target.IsArrested) { return false; }
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }
return true;
}
@@ -108,7 +108,7 @@ namespace Barotrauma
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
ConditionLevel = MIN_OXYGEN,
RemoveExisting = true
RemoveExistingWhenNecessary = true
};
},
onAbandon: () =>
@@ -76,6 +76,10 @@ namespace Barotrauma
// -> ignore find safety unless we need to find a diving gear
Priority = 0;
}
else if (objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
{
Priority = 0;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
{
@@ -162,7 +162,10 @@ namespace Barotrauma
CloseEnough = reach,
DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak" : null,
TargetName = Leak.FlowTargetHull?.DisplayName,
CheckVisibility = false
CheckVisibility = false,
requiredCondition = () => Leak.Submarine == character.Submarine,
// The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue)
SpeakCannotReachCondition = () => !CheckObjectiveSpecific()
},
onAbandon: () =>
{
@@ -59,6 +59,7 @@ namespace Barotrauma
public bool CheckPathForEachItem { get; set; }
public bool SpeakIfFails { get; set; }
public string CannotFindDialogueIdentifierOverride { get; set; }
public Func<bool> CannotFindDialogueCondition { get; set; }
private int _itemCount = 1;
public int ItemCount
@@ -400,6 +401,11 @@ namespace Barotrauma
{
if (!ownerItem.IsInteractable(character)) { continue; }
if (!(ownerItem.GetComponent<ItemContainer>()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; }
//the item is inside an item inside an item (e.g. fuel tank in a welding tool in a cabinet -> reduce priority to prefer items that aren't inside a tool)
if (ownerItem != item.Container)
{
itemPriority *= 0.1f;
}
}
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y);
@@ -560,22 +566,18 @@ namespace Barotrauma
DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow);
#endif
}
if (SpeakIfFails)
{
SpeakCannotFind();
}
SpeakCannotFind();
}
private void SpeakCannotFind()
{
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective)
{
string msg = TextManager.Get(CannotFindDialogueIdentifierOverride, returnNull: true) ?? TextManager.Get("dialogcannotfinditem", returnNull: true);
if (msg != null)
{
character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f);
}
}
if (!SpeakIfFails) { return; }
if (!character.IsOnPlayerTeam) { return; }
if (objectiveManager.CurrentOrder != objectiveManager.CurrentObjective) { return; }
if (CannotFindDialogueCondition != null && !CannotFindDialogueCondition()) { return; }
string msg = TextManager.Get(CannotFindDialogueIdentifierOverride, returnNull: true) ?? TextManager.Get("dialogcannotfinditem", returnNull: true);
if (msg == null) { return; }
character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f);
}
}
}
@@ -43,7 +43,6 @@ namespace Barotrauma
private readonly float minDistance = 50;
private readonly float seekGapsInterval = 1;
private float seekGapsTimer;
private bool cannotFollow;
/// <summary>
/// Display units
@@ -52,6 +51,11 @@ namespace Barotrauma
{
get
{
if (IsFollowOrderObjective && Target is Character targetCharacter && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null))
{
// Keep close when the target is going inside/outside
return minDistance;
}
float dist = _closeEnough * CloseEnoughMultiplier;
float extraMultiplier = Math.Clamp(CloseEnoughMultiplier * 0.6f, 1, 3);
if (character.AnimController.InWater)
@@ -73,6 +77,9 @@ namespace Barotrauma
// TODO: Currently we never check the visibility (to the end node), which is actually unintentional.
// I don't think it has caused any issues so far, so let's keep defaulting to false for now, because the less we do raycasts the better.
// However, if there are cases where the bots attempt to go through walls (select the end node that is behind an obstacle), we should set this true.
// NOTE: This seemes to have caused an issue now Regalis11/Barotrauma#8067: namely, the bot was trying to use a waypoint that was obstructed by a shuttle
// because obstruction was only checked when checking visibility in PathFinder. Changed that so that obstructed nodes are no longer used.
public bool CheckVisibility { get; set; }
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
@@ -96,6 +103,8 @@ namespace Barotrauma
public float? OverridePriority = null;
public Func<bool> SpeakCannotReachCondition { get; set; }
protected override float GetPriority()
{
bool isOrder = objectiveManager.IsOrder(this);
@@ -166,14 +175,14 @@ namespace Barotrauma
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow);
}
#endif
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective && DialogueIdentifier != null && SpeakIfFails)
{
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);
}
}
if (!character.IsOnPlayerTeam) { return; }
if (objectiveManager.CurrentOrder != objectiveManager.CurrentObjective) { return; }
if (DialogueIdentifier == null) { return; }
if (!SpeakIfFails) { return; }
if (SpeakCannotReachCondition != null && !SpeakCannotReachCondition()) { return; }
string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character));
if (msg == null) { return; }
character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f);
}
public void ForceAct(float deltaTime) => Act(deltaTime);
@@ -286,28 +295,16 @@ namespace Barotrauma
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () =>
{
cannotFollow = false;
RemoveSubObjective(ref findDivingGear);
});
onCompleted: () => RemoveSubObjective(ref findDivingGear));
}
else
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () =>
{
cannotFollow = false;
RemoveSubObjective(ref findDivingGear);
});
onCompleted: () => RemoveSubObjective(ref findDivingGear));
}
return;
}
else
{
cannotFollow = false;
}
}
if (repeat)
{
@@ -635,21 +632,29 @@ namespace Barotrauma
{
get
{
if (SteeringManager == PathSteering && PathSteering.CurrentPath?.CurrentNode?.Ladders != null)
if (character.IsClimbing)
{
//don't consider the character to be close enough to the target while climbing ladders,
//UNLESS the last node in the path has been reached
//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; }
if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.CurrentPath.Finished && PathSteering.IsCurrentNodeLadder)
{
if (Target.WorldPosition.Y > character.WorldPosition.Y)
{
// The target is still above us
return false;
}
if (!character.AnimController.IsAboveFloor)
{
// Going through a hatch
return false;
}
}
}
if (!AlwaysUseEuclideanDistance && !character.AnimController.InWater)
{
float yDiff = Math.Abs(Target.WorldPosition.Y - character.WorldPosition.Y);
if (yDiff > CloseEnough) { return false; }
float xDiff = Math.Abs(Target.WorldPosition.X - character.WorldPosition.X);
return xDiff <= CloseEnough;
float yDist = Math.Abs(Target.WorldPosition.Y - character.WorldPosition.Y);
if (yDist > CloseEnough) { return false; }
float xDist = Math.Abs(Target.WorldPosition.X - character.WorldPosition.X);
return xDist <= CloseEnough;
}
Vector2 sourcePos = UseDistanceRelativeToAimSourcePos ? character.AnimController.AimSourceWorldPos : character.WorldPosition;
return Vector2.DistanceSquared(Target.WorldPosition, sourcePos) < CloseEnough * CloseEnough;
}
@@ -727,7 +732,6 @@ namespace Barotrauma
findDivingGear = null;
seekGapsTimer = 0;
TargetGap = null;
cannotFollow = false;
}
}
}
@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
{
@@ -20,6 +21,8 @@ namespace Barotrauma
private Item Container { get; }
private ItemContainer ItemContainer { get; }
private ImmutableArray<string> TargetContainerTags { get; }
private ImmutableHashSet<string> ValidContainableItemIdentifiers { get; }
private static Dictionary<ItemPrefab, ImmutableHashSet<string>> AllValidContainableItemIdentifiers { get; } = new Dictionary<ItemPrefab, ImmutableHashSet<string>>();
private int itemIndex = 0;
private AIObjectiveDecontainItem decontainObjective;
@@ -47,6 +50,111 @@ namespace Barotrauma
abandonGetItemDialogueIdentifier = optionSpecificDialogueIdentifier;
}
}
ValidContainableItemIdentifiers = GetValidContainableItemIdentifiers();
if (ValidContainableItemIdentifiers.None())
{
#if DEBUG
DebugConsole.ShowError($"No valid containable item identifiers found for the Load Item objective targeting {Container}");
#endif
Abandon = true;
return;
}
}
private enum CheckStatus { Unfinished, Finished }
private ImmutableHashSet<string> GetValidContainableItemIdentifiers()
{
if (AllValidContainableItemIdentifiers.TryGetValue(Container.Prefab, out var existingIdentifiers))
{
return existingIdentifiers;
}
// Status effects are often used to alter item condition so using the Containable Item Identifiers directly can lead to unwanted results
// For example, placing welding fuel tanks inside oxygen tank shelves
bool useDefaultContainableItemIdentifiers = true;
var potentialContainablePrefabs = MapEntityPrefab.List
.Where(mep => mep is ItemPrefab ip && ItemContainer.ContainableItemIdentifiers.Any(i => i == ip.Identifier || ip.Tags.Contains(i)))
.Cast<ItemPrefab>();
var validContainableItemIdentifiers = new HashSet<string>();
foreach (var component in Container.Components)
{
if (CheckComponent() == CheckStatus.Finished)
{
break;
}
CheckStatus CheckComponent()
{
if (component.statusEffectLists != null)
{
foreach (var (_, statusEffects) in component.statusEffectLists)
{
if (CheckStatusEffects(statusEffects) == CheckStatus.Finished)
{
return CheckStatus.Finished;
}
}
}
if (component is ItemContainer itemContainer && itemContainer.ContainableItems != null)
{
foreach (var item in itemContainer.ContainableItems)
{
if (CheckStatusEffects(item.statusEffects) == CheckStatus.Finished)
{
return CheckStatus.Finished;
}
}
}
return CheckStatus.Unfinished;
CheckStatus CheckStatusEffects(IEnumerable<StatusEffect> statusEffects)
{
if (statusEffects == null) { return CheckStatus.Unfinished; }
foreach (var statusEffect in statusEffects)
{
if ((statusEffect.TargetIdentifiers == null || statusEffect.TargetIdentifiers.None()) && !statusEffect.HasConditions) { continue; }
switch (TargetItemCondition)
{
case AIObjectiveLoadItems.ItemCondition.Empty:
if (!statusEffect.ReducesItemCondition()) { continue; }
break;
case AIObjectiveLoadItems.ItemCondition.Full:
if (!statusEffect.IncreasesItemCondition()) { continue; }
break;
default:
continue;
}
useDefaultContainableItemIdentifiers = false;
if (statusEffect.TargetIdentifiers != null)
{
foreach (string target in statusEffect.TargetIdentifiers)
{
foreach (var prefab in potentialContainablePrefabs)
{
if (CheckPrefab(prefab, () => prefab.Tags.Contains(target)) == CheckStatus.Finished) { return CheckStatus.Finished; }
}
}
}
foreach (var prefab in potentialContainablePrefabs)
{
if (CheckPrefab(prefab, () => statusEffect.MatchesTagConditionals(prefab)) == CheckStatus.Finished) { return CheckStatus.Finished; }
}
CheckStatus CheckPrefab(ItemPrefab prefab, Func<bool> isValid)
{
if (validContainableItemIdentifiers.Contains(prefab.Identifier)) { return CheckStatus.Unfinished; }
if (!isValid()) { return CheckStatus.Unfinished; }
validContainableItemIdentifiers.Add(prefab.Identifier);
if (potentialContainablePrefabs.Any(p => !validContainableItemIdentifiers.Contains(p.Identifier))) { return CheckStatus.Unfinished; }
return CheckStatus.Finished;
}
}
return CheckStatus.Unfinished;
}
}
}
var identifiers = useDefaultContainableItemIdentifiers ?
potentialContainablePrefabs.Select(p => p.Identifier).ToImmutableHashSet() :
validContainableItemIdentifiers.ToImmutableHashSet();
AllValidContainableItemIdentifiers.Add(Container.Prefab, identifiers);
return identifiers;
}
protected override float GetPriority()
@@ -116,7 +224,7 @@ namespace Barotrauma
base.Update(deltaTime);
if (targetItem == null)
{
if (character.FindItem(ref itemIndex, out Item item, identifiers: ItemContainer.ContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetConditionBasedPriority))
if (character.FindItem(ref itemIndex, out Item item, identifiers: ValidContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetPriority))
{
if (item == null)
{
@@ -125,17 +233,19 @@ namespace Barotrauma
}
targetItem = item;
}
// Prefer items closer to full condition when target condition is Empty, and vice versa
float GetConditionBasedPriority(Item item)
float GetPriority(Item item)
{
try
{
return TargetItemCondition switch
// Prefer items closer to full condition when target condition is Empty, and vice versa
float conditionBasedPriority = TargetItemCondition switch
{
AIObjectiveLoadItems.ItemCondition.Full => MathUtils.InverseLerp(100.0f, 0.0f, item.ConditionPercentage),
AIObjectiveLoadItems.ItemCondition.Empty => MathUtils.InverseLerp(0.0f, 100.0f, item.ConditionPercentage),
_ => throw new NotImplementedException()
};
// Prefer items that have the same identifier as one of the already contained items
return ItemContainer.ContainsItemsWithSameIdentifier(item) ? conditionBasedPriority : conditionBasedPriority / 2;
}
catch (NotImplementedException)
{
@@ -161,10 +271,11 @@ namespace Barotrauma
TryAddSubObjective(ref decontainObjective,
constructor: () => new AIObjectiveDecontainItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier)
{
AbandonGetItemDialogueCondition = () => IsValidContainable(targetItem),
AbandonGetItemDialogueIdentifier = abandonGetItemDialogueIdentifier,
Equip = true,
RemoveExistingWhenNecessary = true,
RemoveExistingPredicate = (i) => AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition),
RemoveExistingPredicate = (i) => !ValidContainableItemIdentifiers.Contains(i.Prefab.Identifier) || AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition),
RemoveExistingMax = 1
},
onCompleted: () =>
@@ -189,6 +300,7 @@ namespace Barotrauma
{
if (item == null) { return false; }
if (item.Removed) { return false; }
if (!ValidContainableItemIdentifiers.Contains(item.Prefab.Identifier)) { return false; }
if (ignoredItems.Contains(item)) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
var rootInventoryOwner = item.GetRootInventoryOwner();
@@ -46,6 +46,7 @@ namespace Barotrauma
public override bool AllowSubObjectiveSorting => true;
public virtual bool InverseTargetEvaluation => false;
protected virtual bool ResetWhenClearingIgnoreList => true;
protected virtual bool ForceOrderPriority => true;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); }
@@ -417,7 +417,7 @@ namespace Barotrauma
if (orderGiver == null) { return null; }
newObjective = new AIObjectiveGoTo(orderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
{
CloseEnough = Rand.Range(80, 100),
CloseEnough = Rand.Range(80f, 100f),
CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Target == orderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4),
ExtraDistanceOutsideSub = 100,
ExtraDistanceWhileSwimming = 100,
@@ -431,7 +431,7 @@ namespace Barotrauma
case "wait":
newObjective = new AIObjectiveGoTo(order.TargetSpatialEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier)
{
AllowGoingOutside = character.Submarine == null || (order.TargetSpatialEntity != null && character.Submarine != order.TargetSpatialEntity.Submarine)
AllowGoingOutside = true
};
break;
case "return":
@@ -477,6 +477,12 @@ namespace Barotrauma
case "fightintruders":
newObjective = new AIObjectiveFightIntruders(character, this, priorityModifier);
break;
case "assaultenemy":
newObjective = new AIObjectiveFightIntruders(character, this, priorityModifier)
{
TargetCharactersInOtherSubs = true
};
break;
case "steer":
var steering = (order?.TargetEntity as Item)?.GetComponent<Steering>();
if (steering != null) { steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; }
@@ -652,7 +658,12 @@ namespace Barotrauma
public bool IsOrder(AIObjective objective)
{
return objective == ForcedOrder || CurrentOrders.Any(o => o.Objective == objective);
if (objective == ForcedOrder) { return true; }
foreach (var order in CurrentOrders)
{
if (order.Objective == objective) { return true; }
}
return false;
}
public bool HasOrders()
@@ -11,6 +11,7 @@ namespace Barotrauma
public override string Identifier { get; set; } = "prepare";
public override string DebugTag => $"{Identifier}";
public override bool KeepDivingGearOn => true;
public override bool KeepDivingGearOnAlsoWhenInactive => true;
public override bool PrioritizeIfSubObjectivesActive => true;
private AIObjectiveGetItem getSingleItemObjective;
@@ -33,6 +33,7 @@ namespace Barotrauma
if (pump.Item.Submarine == null) { return false; }
if (pump.Item.CurrentHull == null) { return false; }
if (pump.Item.Submarine.TeamID != character.TeamID) { return false; }
if (pump.IsAutoControlled) { return false; }
if (pump.Item.ConditionPercentage <= 0) { return false; }
if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; }
if (character.Submarine != null)
@@ -489,7 +489,7 @@ namespace Barotrauma
return Priority;
}
public static IEnumerable<Affliction> GetSortedAfflictions(Character character) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions());
public static IEnumerable<Affliction> GetSortedAfflictions(Character character, bool excludeBuffs = true) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions(), excludeBuffs);
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
{
@@ -7,11 +7,11 @@ namespace Barotrauma
class AIObjectiveReturn : AIObjective
{
public override string Identifier { get; set; } = "return";
private AIObjectiveGoTo moveInsideObjective, moveInCaveObjective, moveOutsideObjective;
private bool usingEscapeBehavior;
private bool isSteeringThroughGap;
public Submarine ReturnTarget { get; }
private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective;
private bool usingEscapeBehavior, isSteeringThroughGap;
public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier)
{
ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
@@ -112,7 +112,6 @@ namespace Barotrauma
}
if (targetHull != null)
{
RemoveSubObjective(ref moveInCaveObjective);
RemoveSubObjective(ref moveOutsideObjective);
TryAddSubObjective(ref moveInsideObjective,
constructor: () => new AIObjectiveGoTo(targetHull, character, objectiveManager)
@@ -137,91 +136,41 @@ namespace Barotrauma
IsCompleted = true;
}
}
else if (!isSteeringThroughGap && moveInCaveObjective == null && moveOutsideObjective == null)
else if (!isSteeringThroughGap && moveOutsideObjective == null)
{
if (HumanAIController.IsInsideCave)
Hull targetHull = null;
float targetDistanceSquared = float.MaxValue;
bool targetIsAirlock = false;
foreach (var hull in ReturnTarget.GetHulls(false))
{
WayPoint closestOutsideWaypoint = null;
float closestDistance = float.MaxValue;
foreach (var w in WayPoint.WayPointList)
bool hullIsAirlock = hull.IsTaggedAirlock();
if(hullIsAirlock || (!targetIsAirlock && hull.LeadsOutside(character)))
{
if (w.Tunnel != null && w.Tunnel.Type == Level.TunnelType.Cave) { continue; }
if (w.linkedTo.None(l => l is WayPoint linkedWaypoint && linkedWaypoint.Tunnel?.Type == Level.TunnelType.Cave)) { continue; }
float distance = Vector2.DistanceSquared(character.WorldPosition, w.WorldPosition);
if (closestOutsideWaypoint == null || distance < closestDistance)
float distanceSquared = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition);
if (targetHull == null || distanceSquared < targetDistanceSquared)
{
closestOutsideWaypoint = w;
closestDistance = distance;
targetHull = hull;
targetDistanceSquared = distanceSquared;
targetIsAirlock = hullIsAirlock;
}
}
if (closestOutsideWaypoint != null)
{
RemoveSubObjective(ref moveInsideObjective);
RemoveSubObjective(ref moveOutsideObjective);
TryAddSubObjective(ref moveInCaveObjective,
constructor: () => new AIObjectiveGoTo(closestOutsideWaypoint, character, objectiveManager)
{
endNodeFilter = n => n.Waypoint == closestOutsideWaypoint,
AllowGoingOutside = true
},
onCompleted: () => RemoveSubObjective(ref moveInCaveObjective),
onAbandon: () => Abandon = true);
}
else
{
#if DEBUG
DebugConsole.ThrowError("Error with a Return objective: no suitable main or side path node target found for 'moveOutsideObjective'");
#endif
}
}
if (targetHull != null)
{
RemoveSubObjective(ref moveInsideObjective);
TryAddSubObjective(ref moveOutsideObjective,
constructor: () => new AIObjectiveGoTo(targetHull, character, objectiveManager)
{
AllowGoingOutside = true
},
onCompleted: () => RemoveSubObjective(ref moveOutsideObjective),
onAbandon: () => Abandon = true);
}
else
{
Hull targetHull = null;
float targetDistanceSquared = float.MaxValue;
bool targetIsAirlock = false;
foreach (var hull in ReturnTarget.GetHulls(false))
{
bool hullIsAirlock = hull.IsTaggedAirlock();
if(hullIsAirlock || (!targetIsAirlock && hull.LeadsOutside(character)))
{
float distanceSquared = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition);
if (targetHull == null || distanceSquared < targetDistanceSquared)
{
targetHull = hull;
targetDistanceSquared = distanceSquared;
targetIsAirlock = hullIsAirlock;
}
}
}
if (targetHull != null)
{
RemoveSubObjective(ref moveInsideObjective);
RemoveSubObjective(ref moveInCaveObjective);
TryAddSubObjective(ref moveOutsideObjective,
constructor: () => new AIObjectiveGoTo(targetHull, character, objectiveManager)
{
AllowGoingOutside = true
},
onCompleted: () => RemoveSubObjective(ref moveOutsideObjective),
onAbandon: () => Abandon = true);
}
else
{
#if DEBUG
DebugConsole.ThrowError("Error with a Return objective: no suitable target for 'moveOutsideObjective'");
DebugConsole.ThrowError("Error with a Return objective: no suitable target for 'moveOutsideObjective'");
#endif
}
}
}
else
{
if (HumanAIController.IsInsideCave)
{
RemoveSubObjective(ref moveOutsideObjective);
}
else
{
RemoveSubObjective(ref moveInCaveObjective);
}
}
usingEscapeBehavior = shouldUseEscapeBehavior;
@@ -249,7 +198,6 @@ namespace Barotrauma
{
base.Reset();
moveInsideObjective = null;
moveInCaveObjective = null;
moveOutsideObjective = null;
usingEscapeBehavior = false;
isSteeringThroughGap = false;
@@ -155,6 +155,9 @@ namespace Barotrauma
public OrderCategory? Category { get; private set; }
//legacy support
/// <summary>
/// If defined, the order can only be quick-assigned to characters with these jobs. Or if it's a report, the icon will only be displayed to characters with these jobs.
/// </summary>
public readonly string[] AppropriateJobs;
public readonly string[] Options;
public readonly string[] HiddenOptions;
@@ -177,6 +180,10 @@ namespace Barotrauma
public bool IsPrefab { get; private set; }
public readonly bool MustManuallyAssign;
public readonly bool AutoDismiss;
/// <summary>
/// If defined, the order will be quick-assigned to characters with these jobs before characters with other jobs.
/// </summary>
public string[] PreferredJobs { get; }
public readonly OrderTarget TargetPosition;
@@ -221,6 +228,9 @@ namespace Barotrauma
/// </summary>
public int AssignmentPriority { get; }
public bool ColoredWhenControllingGiver { get; }
public bool DisplayGiverInTooltip { get; }
public static void Init()
{
Prefabs = new Dictionary<string, Order>();
@@ -324,6 +334,7 @@ namespace Barotrauma
ControllerTags = orderElement.GetAttributeStringArray("controllertags", new string[0]);
TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false);
AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]);
PreferredJobs = orderElement.GetAttributeStringArray("preferredjobs", new string[0]);
Options = orderElement.GetAttributeStringArray("options", new string[0]);
HiddenOptions = orderElement.GetAttributeStringArray("hiddenoptions", new string[0]);
AllOptions = Options.Concat(HiddenOptions).ToArray();
@@ -374,7 +385,7 @@ namespace Barotrauma
}
if (OptionNames.Count != Options.Length)
{
DebugConsole.ThrowError("Error in Order " + Name + " - the number of option names doesn't match the number of options.");
DebugConsole.AddWarning("Error in Order " + Name + " - the number of option names doesn't match the number of options.");
OptionNames.Clear();
Options.ForEach(o => OptionNames.Add(o, o));
}
@@ -404,8 +415,10 @@ namespace Barotrauma
MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false);
IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis";
DrawIconWhenContained = orderElement.GetAttributeBool("displayiconwhencontained", false);
AutoDismiss = orderElement.GetAttributeBool("autodismiss", Category == OrderCategory.Movement);
AutoDismiss = orderElement.GetAttributeBool("autodismiss", Category == OrderCategory.Operate || Category == OrderCategory.Movement);
AssignmentPriority = Math.Clamp(orderElement.GetAttributeInt("assignmentpriority", 100), 0, 100);
ColoredWhenControllingGiver = orderElement.GetAttributeBool("coloredwhencontrollinggiver", false);
DisplayGiverInTooltip = orderElement.GetAttributeBool("displaygiverintooltip", false);
}
/// <summary>
@@ -430,6 +443,7 @@ namespace Barotrauma
ControllerTags = prefab.ControllerTags;
TargetAllCharacters = prefab.TargetAllCharacters;
AppropriateJobs = prefab.AppropriateJobs;
PreferredJobs = prefab.PreferredJobs;
FadeOutTime = prefab.FadeOutTime;
MustSetTarget = prefab.MustSetTarget;
CanBeGeneralized = prefab.CanBeGeneralized;
@@ -441,6 +455,9 @@ namespace Barotrauma
Hidden = prefab.Hidden;
IgnoreAtOutpost = prefab.IgnoreAtOutpost;
AssignmentPriority = prefab.AssignmentPriority;
AutoDismiss = prefab.AutoDismiss;
DisplayGiverInTooltip = prefab.DisplayGiverInTooltip;
ColoredWhenControllingGiver = prefab.ColoredWhenControllingGiver;
OrderGiver = orderGiver;
TargetEntity = targetEntity;
@@ -481,34 +498,39 @@ namespace Barotrauma
WallSectionIndex = sectionIndex;
TargetType = OrderTargetType.WallSection;
}
public bool HasAppropriateJob(Character character)
{
if (character.Info == null || character.Info.Job == null) { return false; }
if (character.Info.Job.Prefab.AppropriateOrders.Any(appropriateOrderId => Identifier == appropriateOrderId)) { return true; }
if (!JobPrefab.Prefabs.Any(jp => jp.AppropriateOrders.Contains(Identifier)) &&
(AppropriateJobs == null || AppropriateJobs.Length == 0))
private bool HasSpecifiedJob(Character character, string[] jobs)
{
if (jobs == null || jobs.Length == 0) { return false; }
string jobIdentifier = character?.Info?.Job?.Prefab?.Identifier;
if (string.IsNullOrEmpty(jobIdentifier)) { return false; }
for (int i = 0; i < jobs.Length; i++)
{
return true;
}
for (int i = 0; i < AppropriateJobs.Length; i++)
{
if (character.Info.Job.Prefab.Identifier.Equals(AppropriateJobs[i], StringComparison.OrdinalIgnoreCase)) { return true; }
if (jobIdentifier.Equals(jobs[i], StringComparison.OrdinalIgnoreCase)) { return true; }
}
return false;
}
public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "", int? priority = null)
public bool HasAppropriateJob(Character character) => HasSpecifiedJob(character, AppropriateJobs);
public bool HasPreferredJob(Character character) => HasSpecifiedJob(character, PreferredJobs);
public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "", bool isNewOrder = true)
{
priority ??= CharacterInfo.HighestManualOrderPriority;
// If the order has a lesser priority, it means we are rearranging character orders
if (!TargetAllCharacters && priority != CharacterInfo.HighestManualOrderPriority && Identifier != "dismissed")
if (!TargetAllCharacters && !isNewOrder && Identifier != "dismissed")
{
return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty, returnNull: true) ?? string.Empty;
// Use special dialogue when we're rearranging character orders
if (!givingOrderToSelf)
{
return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty, returnNull: true) ?? string.Empty;
}
else
{
// Say nothing when rearranging the orders of the character you're controlling
return string.Empty;
}
}
string messageTag = $"{(givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf" : "OrderDialog")}";
messageTag += $".{Identifier}";
string messageTag = $"{(givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf" : "OrderDialog")}.{Identifier}";
if (!string.IsNullOrEmpty(orderOption))
{
if (Identifier != "dismissed")
@@ -333,7 +333,6 @@ namespace Barotrauma
//if searching for a path inside the sub, make sure the waypoint is visible
if (checkVisibility && isCharacter)
{
if (node.Waypoint.isObstructed) { return false; }
var body = Submarine.PickBody(rayStart, node.TempPosition,
collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs);
if (body != null)
@@ -350,6 +349,7 @@ namespace Barotrauma
{
if (nodeFilter != null && !nodeFilter(node)) { return false; }
if (startNodeFilter != null && !startNodeFilter(node)) { return false; }
if (node.Waypoint.isObstructed) { return false; }
// Always check the visibility for the start node
if (!IsWaypointVisible(node, start)) { return false; }
if (node.IsBlocked()) { return false; }
@@ -364,6 +364,7 @@ namespace Barotrauma
{
if (nodeFilter != null && !nodeFilter(node)) { return false; }
if (endNodeFilter != null && !endNodeFilter(node)) { return false; }
if (node.Waypoint.isObstructed) { return false; }
// Only check the visibility for the end node when allowed (fix leaks)
if (!IsWaypointVisible(node, end, checkVisibility: checkVisibility)) { return false; }
if (node.IsBlocked()) { return false; }
@@ -134,6 +134,7 @@ namespace Barotrauma
aggregate += Items[i].Commonness;
if (aggregate >= r && Items[i].Prefab != null)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetProducedItem:" + pet.AiController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
Entity.Spawner.AddToSpawnQueue(Items[i].Prefab, pet.AiController.Character.WorldPosition);
break;
}
@@ -200,6 +201,8 @@ namespace Barotrauma
break;
}
}
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetSpawned:" + aiController.Character.SpeciesName);
}
public StatusIndicatorType GetCurrentStatusIndicatorType()
@@ -210,23 +213,44 @@ namespace Barotrauma
return StatusIndicatorType.None;
}
public bool OnEat(IEnumerable<string> tags, float amount)
public bool OnEat(Item item)
{
bool success = OnEat(item.GetTags());
if (success)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + item.prefab.Identifier);
}
return success;
}
public bool OnEat(Character character)
{
if (character == null || !character.IsDead) { return false; }
bool success = OnEat("dead");
if (success)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + character.SpeciesName);
}
return success;
}
private bool OnEat(IEnumerable<string> tags)
{
foreach (string tag in tags)
{
if (OnEat(tag, amount)) { return true; }
if (OnEat(tag)) { return true; }
}
return false;
}
public bool OnEat(string tag, float amount)
private bool OnEat(string tag)
{
for (int i = 0; i < foods.Count; i++)
{
if (tag.Equals(foods[i].Tag, System.StringComparison.OrdinalIgnoreCase))
{
Hunger += foods[i].Hunger * amount;
Happiness += foods[i].Happiness * amount;
Hunger += foods[i].Hunger;
Happiness += foods[i].Happiness;
#if CLIENT
AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f);
#endif
@@ -55,7 +55,7 @@ namespace Barotrauma
CommandingCharacter.Speak(SuggestedOrderPrefab.GetChatMessage(OrderedCharacter.Name, "", false), minDurationBetweenSimilar: 5);
}
CurrentOrder = new Order(SuggestedOrderPrefab, TargetItem, TargetItemComponent, CommandingCharacter);
OrderedCharacter.SetOrder(CurrentOrder, Option, priority: 3, CommandingCharacter, CommandingCharacter != OrderedCharacter);
OrderedCharacter.SetOrder(CurrentOrder, Option, priority: CharacterInfo.HighestManualOrderPriority, CommandingCharacter, CommandingCharacter != OrderedCharacter);
OrderedCharacter.Speak(TextManager.Get("DialogAffirmative"), delay: 1.0f, minDurationBetweenSimilar: 5);
}
TimeSinceLastAttempt = 0f;
@@ -22,7 +22,13 @@ namespace Barotrauma
public override void CalculateImportanceSpecific()
{
if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot()) { return; }
if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot())
{
//operate (= recharge the turrets) with low priority if they're out of power
//if something else (issues with reactor or the electrical grid) is preventing them from being charged, fixing those issues should take priority
Importance = ShipCommandManager.MinimumIssueThreshold * 1.05f;
return;
}
targetingImportances.Clear();
foreach (Character character in shipCommandManager.EnemyCharacters)
@@ -51,7 +51,7 @@ namespace Barotrauma
private const float RamTimerMax = 17.5f;
public readonly List<ShipIssueWorker> ShipIssueWorkers = new List<ShipIssueWorker>();
private const float MinimumIssueThreshold = 10f;
public const float MinimumIssueThreshold = 10f;
private const float IssueDevotionBuffer = 5f;
private float decisionTimer = 6f;
@@ -75,7 +75,7 @@ namespace Barotrauma
public void Update(float deltaTime)
{
if (!Active) { return; }
if (!Active || character.IsArrested) { return; }
decisionTimer -= deltaTime;
if (decisionTimer <= 0.0f)
{
@@ -344,7 +344,6 @@ namespace Barotrauma
ShipIssueWorkers.Clear();
// could have support for multiple reactors, todo m61
if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
{
ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, Order.GetPrefab("operatereactor"), reactor.Item, reactor, "powerup"));
@@ -254,7 +254,7 @@ namespace Barotrauma
private void SpawnInitialCells()
{
int brainRoomCells = Rand.Range(MinCellsPerBrainRoom, MaxCellsPerRoom);
int brainRoomCells = Rand.Range(MinCellsPerBrainRoom, MaxCellsPerRoom + 1);
if (brain.CurrentHull?.WaterPercentage >= MinWaterLevel)
{
for (int i = 0; i < brainRoomCells; i++)
@@ -262,12 +262,12 @@ namespace Barotrauma
if (!TrySpawnCell(out _, brain.CurrentHull)) { break; }
}
}
int cellsInside = Rand.Range(MinCellsInside, MaxCellsInside);
int cellsInside = Rand.Range(MinCellsInside, MaxCellsInside + 1);
for (int i = 0; i < cellsInside; i++)
{
if (!TrySpawnCell(out _)) { break; }
}
int cellsOutside = Rand.Range(MinCellsOutside, MaxCellsOutside);
int cellsOutside = Rand.Range(MinCellsOutside, MaxCellsOutside + 1);
// If we failed to spawn some of the cells in the brainroom/inside, spawn some extra cells outside.
cellsOutside = Math.Clamp(cellsOutside + brainRoomCells + cellsInside - protectiveCells.Count, cellsOutside, MaxCellsOutside);
for (int i = 0; i < cellsOutside; i++)
@@ -269,6 +269,11 @@ namespace Barotrauma
}
}
public float GetHeightFromFloor() => GetColliderBottom().Y - FloorY;
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
public bool IsAboveFloor => GetHeightFromFloor() > -0.1f;
public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos)
{
useItemTimer = 0.5f;
@@ -332,7 +337,7 @@ namespace Barotrauma
aimingMelee = aimMelee;
if (character.Stun > 0.0f || character.IsIncapacitated)
{
aim = false;
aim = false;
}
//calculate the handle positions
@@ -346,7 +346,12 @@ namespace Barotrauma
Vector2 limbDiff = attackSimPosition - mouthPos;
float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 1);
if (limbDiff.LengthSquared() < extent * extent)
bool tooFar = character.InWater ? limbDiff.LengthSquared() > extent * extent : limbDiff.X > extent;
if (tooFar)
{
character.SelectedCharacter = null;
}
else
{
//pull the target character to the position of the mouth
//(+ make the force fluctuate to waggle the character a bit)
@@ -382,55 +387,55 @@ namespace Barotrauma
mouthLimb.body.ApplyLinearImpulse(Vector2.UnitY * force * 2, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
mouthLimb.body.ApplyTorque(-force * 50);
}
var jaw = GetLimb(LimbType.Jaw);
if (jaw != null)
{
jaw.body.ApplyTorque(-(float)Math.Sin(eatTimer * 150) * jaw.Mass * 25);
}
character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
float particleFrequency = MathHelper.Clamp(eatSpeed / 2, 0.02f, 0.5f);
if (Rand.Value() < particleFrequency / 6)
if (Character.CanEat && target.IsDead)
{
target.AnimController.MainLimb.AddDamage(target.SimPosition, dmg, 0, 0, false);
}
if (Rand.Value() < particleFrequency)
{
target.AnimController.MainLimb.AddDamage(target.SimPosition, 0, dmg, 0, false);
}
if (eatTimer % 1.0f < 0.5f && (eatTimer - deltaTime * eatSpeed) % 1.0f > 0.5f)
{
static bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA != null && !j.LimbA.IsSevered && j.LimbB != null && !j.LimbB.IsSevered;
//keep severing joints until there is only one limb left
var nonSeveredJoints = target.AnimController.LimbJoints.Where(CanBeSevered);
if (nonSeveredJoints.None())
var jaw = GetLimb(LimbType.Jaw);
if (jaw != null)
{
//small monsters don't eat the contents of the character's inventory
if (Mass < target.AnimController.Mass)
{
target.Inventory?.AllItemsMod.ForEach(it => it?.Drop(dropper: null));
}
//only one limb left, the character is now full eaten
Entity.Spawner?.AddToRemoveQueue(target);
if (Character.AIController is EnemyAIController enemyAi)
{
enemyAi.PetBehavior?.OnEat("dead", 1.0f);
}
character.SelectedCharacter = null;
jaw.body.ApplyTorque(-(float)Math.Sin(eatTimer * 150) * jaw.Mass * 25);
}
else //sever a random joint
character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
float particleFrequency = MathHelper.Clamp(eatSpeed / 2, 0.02f, 0.5f);
if (Rand.Value() < particleFrequency / 6)
{
target.AnimController.SeverLimbJoint(nonSeveredJoints.GetRandom());
target.AnimController.MainLimb.AddDamage(target.SimPosition, dmg, 0, 0, false);
}
}
}
else
{
character.SelectedCharacter = null;
if (Rand.Value() < particleFrequency)
{
target.AnimController.MainLimb.AddDamage(target.SimPosition, 0, dmg, 0, false);
}
if (eatTimer % 1.0f < 0.5f && (eatTimer - deltaTime * eatSpeed) % 1.0f > 0.5f)
{
static bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA != null && !j.LimbA.IsSevered && j.LimbB != null && !j.LimbB.IsSevered;
//keep severing joints until there is only one limb left
var nonSeveredJoints = target.AnimController.LimbJoints.Where(CanBeSevered);
if (nonSeveredJoints.None())
{
//small monsters don't eat the contents of the character's inventory
if (Mass < target.AnimController.Mass)
{
target.Inventory?.AllItemsMod.ForEach(it => it?.Drop(dropper: null));
}
//only one limb left, the character is now full eaten
Entity.Spawner?.AddToRemoveQueue(target);
if (Character.AIController is EnemyAIController enemyAi)
{
enemyAi.PetBehavior?.OnEat(target);
}
character.SelectedCharacter = null;
}
else //sever a random joint
{
target.AnimController.SeverLimbJoint(nonSeveredJoints.GetRandom());
}
}
}
}
}
@@ -533,6 +533,8 @@ namespace Barotrauma
bool onSlope = Math.Abs(movement.X) > 0.01f && Math.Abs(floorNormal.X) > 0.1f && Math.Sign(floorNormal.X) != Math.Sign(movement.X);
bool movingHorizontally = !MathUtils.NearlyEqual(TargetMovement.X, 0.0f);
if (Stairs != null || onSlope)
{
torso.PullJointWorldAnchorB = new Vector2(
@@ -562,10 +564,8 @@ namespace Barotrauma
if (!torso.Disabled)
{
if (TorsoPosition.HasValue)
{
y += TorsoPosition.Value;
}
if (TorsoPosition.HasValue) { y += TorsoPosition.Value; }
if (Crouching && !movingHorizontally) { y -= HumanCrouchParams.MoveDownAmountWhenStationary; }
torso.PullJointWorldAnchorB =
MathUtils.SmoothStep(torso.SimPosition,
new Vector2(footMid + movement.X * TorsoLeanAmount, y), getUpForce);
@@ -574,10 +574,8 @@ namespace Barotrauma
if (!head.Disabled)
{
y = colliderPos.Y + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier;
if (HeadPosition.HasValue)
{
y += HeadPosition.Value;
}
if (HeadPosition.HasValue) { y += HeadPosition.Value; }
if (Crouching && !movingHorizontally) { y -= HumanCrouchParams.MoveDownAmountWhenStationary; }
head.PullJointWorldAnchorB =
MathUtils.SmoothStep(head.SimPosition,
new Vector2(footMid + movement.X * HeadLeanAmount, y), getUpForce * 1.2f);
@@ -593,12 +591,15 @@ namespace Barotrauma
{
float torsoAngle = TorsoAngle.Value;
float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes");
if (Crouching && !movingHorizontally) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; }
torsoAngle -= herpesStrength / 150.0f;
torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque);
}
if (HeadAngle.HasValue)
{
head.body.SmoothRotate(HeadAngle.Value * Dir, CurrentGroundedParams.HeadTorque);
float headAngle = HeadAngle.Value;
if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; }
head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque);
}
if (!onGround)
@@ -616,8 +617,7 @@ namespace Barotrauma
Vector2 waistPos = waist != null ? waist.SimPosition : torso.SimPosition;
//moving horizontally
if (TargetMovement.X != 0.0f)
if (movingHorizontally)
{
//progress the walking animation
WalkPos -= MathHelper.ToRadians(CurrentAnimationParams.CycleSpeed) * walkCycleMultiplier * movement.X;
@@ -808,7 +808,8 @@ namespace Barotrauma
if (head == null) { return; }
if (torso == null) { return; }
if (currentHull != null)
//check both hulls: the hull whose coordinate space the ragdoll is in, and the hull whose bounds the character's origin actually is inside
if (currentHull != null && character.CurrentHull != null)
{
float surfacePos = currentHull.Surface;
float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f);
@@ -816,7 +817,7 @@ namespace Barotrauma
//and use its water surface instead of the current hull's
if (currentHull.Rect.Y - currentHull.Surface < 5.0f)
{
GetSurfacePos(CurrentHull, ref surfacePos);
GetSurfacePos(currentHull, ref surfacePos);
void GetSurfacePos(Hull hull, ref float prevSurfacePos)
{
if (prevSurfacePos > surfaceThreshold) { return; }
@@ -834,7 +835,7 @@ namespace Barotrauma
foreach (var linkedTo in gap.linkedTo)
{
if (linkedTo is Hull otherHull && otherHull != hull)
if (linkedTo is Hull otherHull && otherHull != hull && otherHull != currentHull)
{
prevSurfacePos = Math.Max(surfacePos, otherHull.Surface);
GetSurfacePos(otherHull, ref prevSurfacePos);
@@ -888,7 +889,6 @@ namespace Barotrauma
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 diff = (mousePos - torso.SimPosition) * Dir;
TargetMovement = new Vector2(0.0f, -0.1f);
float newRotation = MathUtils.VectorToAngle(diff);
Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier);
}
@@ -305,7 +305,27 @@ namespace Barotrauma
public abstract float? TorsoPosition { get; }
public abstract float? TorsoAngle { get; }
public float ImpactTolerance => RagdollParams.ImpactTolerance;
float? impactTolerance;
public float ImpactTolerance
{
get
{
if (impactTolerance == null)
{
impactTolerance = RagdollParams.ImpactTolerance;
if (character.Params.VariantFile != null)
{
float? tolerance = character.Params.VariantFile.Root.GetChildElement("ragdoll")?.GetAttributeFloat("impacttolerance", impactTolerance.Value);
if (tolerance.HasValue)
{
impactTolerance = tolerance;
}
}
}
return impactTolerance.Value;
}
}
public bool Draggable => RagdollParams.Draggable;
public bool CanEnterSubmarine => RagdollParams.CanEnterSubmarine;
@@ -764,8 +784,8 @@ namespace Barotrauma
if (limbDiff.LengthSquared() < 0.0001f) { limbDiff = Rand.Vector(1.0f); }
limbDiff = Vector2.Normalize(limbDiff);
float mass = limbJoint.BodyA.Mass + limbJoint.BodyB.Mass;
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);
limbJoint.LimbA.body.ApplyLinearImpulse(limbDiff * Math.Min(mass, limbJoint.BodyA.Mass * 500), (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f);
limbJoint.LimbB.body.ApplyLinearImpulse(-limbDiff * Math.Min(mass, limbJoint.BodyB.Mass * 500), (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f);
connectedLimbs.Clear();
checkedJoints.Clear();
@@ -1180,13 +1200,13 @@ namespace Barotrauma
{
headInWater = false;
inWater = false;
RefreshFloorY(ignoreStairs: Stairs == null);
if (currentHull.WaterVolume > currentHull.Volume * 0.95f)
{
inWater = true;
}
else
{
RefreshFloorY(ignoreStairs: Stairs == null);
float waterSurface = ConvertUnits.ToSimUnits(currentHull.Surface);
if (targetMovement.Y < 0.0f)
{
@@ -1841,7 +1861,7 @@ namespace Barotrauma
float sin = (float)Math.Sin(mouthLimb.Rotation);
Vector2 bodySize = mouthLimb.body.GetSize();
Vector2 offset = new Vector2(mouthLimb.MouthPos.X * bodySize.X / 2, mouthLimb.MouthPos.Y * bodySize.Y / 2);
return mouthLimb.SimPosition + new Vector2(offset.X * cos - offset.Y * sin, offset.X * sin + offset.Y * cos) * mouthLimb.Scale * RagdollParams.LimbScale;
return mouthLimb.SimPosition + new Vector2(offset.X * cos - offset.Y * sin, offset.X * sin + offset.Y * cos);
}
public Vector2 GetColliderBottom()
@@ -101,11 +101,21 @@ namespace Barotrauma
[Serialize(false, true, description: "Should the AI try to steer away from the target when aiming with this attack? Best combined with PassiveAggressive behavior."), Editable]
public bool Retreat { get; private set; }
private float _range;
[Serialize(0.0f, true, description: "The min distance from the attack limb to the target before the AI tries to attack."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)]
public float Range { get; set; }
public float Range
{
get => _range * RangeMultiplier;
set => _range = value;
}
private float _damageRange;
[Serialize(0.0f, true, description: "The min distance from the attack limb to the target to do damage. In distance-based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f)]
public float DamageRange { get; set; }
public float DamageRange
{
get => _damageRange * RangeMultiplier;
set => _damageRange = value;
}
[Serialize(0.25f, true, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)]
public float Duration { get; private set; }
@@ -130,6 +140,9 @@ namespace Barotrauma
set => _structureDamage = value;
}
[Serialize(true, true), Editable]
public bool EmitStructureDamageParticles { get; private set; }
private float _itemDamage;
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float ItemDamage
@@ -142,10 +155,20 @@ namespace Barotrauma
public float Penetration { get; private set; }
/// <summary>
/// Currently only used with variants. Used for multiplying all the damage.
/// Used for multiplying all the damage.
/// </summary>
public float DamageMultiplier { get; set; } = 1;
/// <summary>
/// Used for multiplying all the ranges.
/// </summary>
public float RangeMultiplier { get; set; } = 1;
/// <summary>
/// Used for multiplying the physics forces.
/// </summary>
public float ImpactMultiplier { get; set; } = 1;
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
public float LevelWallDamage { get; set; }
@@ -311,11 +334,11 @@ namespace Barotrauma
return totalDamage * DamageMultiplier;
}
public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f, float penetration = 0f)
public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f)
{
if (damage > 0.0f) Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage), null);
if (bleedingDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage), null);
if (burnDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamage), null);
if (damage > 0.0f) { Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage), null); }
if (bleedingDamage > 0.0f) { Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage), null); }
if (burnDamage > 0.0f) { Afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamage), null); }
Range = range;
DamageRange = range;
@@ -438,7 +461,7 @@ namespace Barotrauma
ReloadAfflictions(element);
}
public AttackResult DoDamage(Character attacker, IDamageable target, Vector2 worldPosition, float deltaTime, bool playSound = true, PhysicsBody sourceBody = null)
public AttackResult DoDamage(Character attacker, IDamageable target, Vector2 worldPosition, float deltaTime, bool playSound = true, PhysicsBody sourceBody = null, Limb sourceLimb = null)
{
Character targetCharacter = target as Character;
if (OnlyHumans)
@@ -463,10 +486,10 @@ namespace Barotrauma
foreach (StatusEffect effect in statusEffects)
{
effect.sourceBody = sourceBody;
// TODO: do we want to apply the effect at the world position or the entity positions in each cases? -> go through also other cases where status effects are applied
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
effect.Apply(effectType, deltaTime, attacker, attacker, worldPosition);
// TODO: do we want to apply the effect at the world position or the entity positions in each cases? -> go through also other cases where status effects are applied
effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity, worldPosition);
}
if (targetCharacter != null)
{
@@ -503,7 +526,7 @@ namespace Barotrauma
}
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
public AttackResult DoDamageToLimb(Character attacker, Limb targetLimb, Vector2 worldPosition, float deltaTime, bool playSound = true, PhysicsBody sourceBody = null)
public AttackResult DoDamageToLimb(Character attacker, Limb targetLimb, Vector2 worldPosition, float deltaTime, bool playSound = true, PhysicsBody sourceBody = null, Limb sourceLimb = null)
{
if (targetLimb == null)
{
@@ -530,7 +553,7 @@ namespace Barotrauma
effect.sourceBody = sourceBody;
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
effect.Apply(effectType, deltaTime, attacker, attacker);
effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity);
}
if (effect.HasTargetType(StatusEffect.TargetType.Character))
{
@@ -261,9 +261,16 @@ namespace Barotrauma
public AttackResult LastDamage;
public Dictionary<ItemPrefab, double> ItemSelectedDurations
{
get { return itemSelectedDurations; }
}
private readonly Dictionary<ItemPrefab, double> itemSelectedDurations = new Dictionary<ItemPrefab, double>();
private double itemSelectedTime;
public float InvisibleTimer;
private CharacterPrefab prefab;
private readonly CharacterPrefab prefab;
public readonly CharacterParams Params;
public string SpeciesName => Params?.SpeciesName ?? "null";
@@ -496,7 +503,7 @@ namespace Barotrauma
get { return cursorPosition; }
set
{
if (!MathUtils.IsValid(value)) return;
if (!MathUtils.IsValid(value)) { return; }
cursorPosition = value;
}
}
@@ -682,8 +689,8 @@ namespace Barotrauma
get { return CharacterHealth.BloodlossAmount; }
set
{
if (!MathUtils.IsValid(value)) return;
CharacterHealth.BloodlossAmount = MathHelper.Clamp(value, 0.0f, 100.0f);
if (!MathUtils.IsValid(value)) { return; }
CharacterHealth.BloodlossAmount = value;
}
}
@@ -700,7 +707,7 @@ namespace Barotrauma
{
get
{
if (!CanSpeak || IsUnconscious || Stun > 0.0f || IsDead) { return 100.0f; }
if (!CanSpeak || IsUnconscious || IsKnockedDown) { return 100.0f; }
return speechImpediment;
}
set
@@ -737,9 +744,7 @@ namespace Barotrauma
get => _selectedConstruction;
set
{
#if CLIENT
var prevSelectedConstruction = _selectedConstruction;
#endif
_selectedConstruction = value;
#if CLIENT
HintManager.OnSetSelectedConstruction(this, prevSelectedConstruction, _selectedConstruction);
@@ -755,6 +760,19 @@ namespace Barotrauma
}
}
#endif
if (prevSelectedConstruction == null && _selectedConstruction != null)
{
itemSelectedTime = Timing.TotalTime;
}
else if (prevSelectedConstruction != null && _selectedConstruction == null && itemSelectedTime > 0)
{
if (!itemSelectedDurations.ContainsKey(prevSelectedConstruction.Prefab))
{
itemSelectedDurations.Add(prevSelectedConstruction.Prefab, 0);
}
itemSelectedDurations[prevSelectedConstruction.Prefab] += Timing.TotalTime - itemSelectedTime;
itemSelectedTime = 0;
}
}
}
@@ -835,7 +853,7 @@ namespace Barotrauma
}
else
{
return IsKnockedDown || LockHands || IsBot && TeamID != CharacterTeamType.FriendlyNPC;
return IsKnockedDown || LockHands || IsBot && IsOnPlayerTeam;
}
}
set { canInventoryBeAccessed = value; }
@@ -1234,6 +1252,10 @@ namespace Barotrauma
Info.HairElement?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Hair)));
#if CLIENT
if (info.Head?.HairWithHatElement != null)
{
head.HairWithHatSprite = new WearableSprite(info.Head?.HairWithHatElement.Element("sprite"), WearableType.Hair);
}
head.EnableHuskSprite = Params.Husk;
head.LoadHerpesSprite();
head.UpdateWearableTypesToHide();
@@ -1833,7 +1855,7 @@ namespace Barotrauma
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(se))) { return false; }
}
}
if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(this))) { return false; }
@@ -2273,17 +2295,18 @@ namespace Barotrauma
}
}
if (SelectedConstruction?.GetComponent<RemoteController>()?.TargetItem == item ||
HeldItems.Any(it => it.GetComponent<RemoteController>()?.TargetItem == item))
{
return true;
}
if (item.InteractDistance == 0.0f && !item.Prefab.Triggers.Any()) { return false; }
Pickable pickableComponent = item.GetComponent<Pickable>();
if (pickableComponent != null && pickableComponent.Picker != this && pickableComponent.Picker != null && !pickableComponent.Picker.IsDead) { return false; }
if (SelectedConstruction?.GetComponent<RemoteController>()?.TargetItem == item) { return true; }
//optimization: don't use HeldItems because it allocates memory and this method is executed very frequently
var heldItem1 = Inventory?.GetItemInLimbSlot(InvSlotType.RightHand);
if (heldItem1?.GetComponent<RemoteController>()?.TargetItem == item) { return true; }
var heldItem2 = Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand);
if (heldItem2?.GetComponent<RemoteController>()?.TargetItem == item) { return true; }
Vector2 characterDirection = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(AnimController.Collider.Rotation));
Vector2 upperBodyPosition = Position + (characterDirection * 20.0f);
@@ -3228,7 +3251,7 @@ namespace Barotrauma
if (orderGiver != null)
{
var abilityOrderedCharacter = new AbilityCharacter(this);
var abilityOrderedCharacter = new AbilityOrderedCharacter(this);
orderGiver.CheckTalents(AbilityEffectType.OnGiveOrder, abilityOrderedCharacter);
if (orderGiver.LastOrderedCharacter != this)
@@ -3483,7 +3506,7 @@ namespace Barotrauma
Limb limbHit = targetLimb;
float attackImpulse = attack.TargetImpulse + attack.TargetForce * deltaTime;
float attackImpulse = attack.TargetImpulse + attack.TargetForce * attack.ImpactMultiplier * deltaTime;
AbilityAttackData attackData = new AbilityAttackData(attack, this);
if (attacker != null)
@@ -3521,7 +3544,7 @@ namespace Barotrauma
}
if (limbHit == null) { return new AttackResult(); }
Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld;
Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld * attack.ImpactMultiplier;
if (attacker != null)
{
forceWorld.X *= attacker.AnimController.Dir;
@@ -3550,12 +3573,12 @@ namespace Barotrauma
}
#endif
// Don't allow beheading for monster attacks, because it happens too frequently (crawlers/tigerthreshers etc attacking each other -> they will most often target to the head)
TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: attacker == null || attacker.IsHuman || attacker.IsPlayer);
TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: attacker == null || attacker.IsHuman || attacker.IsPlayer, attacker: attacker);
return attackResult;
}
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading)
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, Character attacker = null)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
#if DEBUG
@@ -3576,8 +3599,12 @@ namespace Barotrauma
foreach (LimbJoint joint in AnimController.LimbJoints)
{
if (!joint.CanBeSevered) { continue; }
// Limb A is where we usually create the joints from. Let's not allow severing when the "parent" limb is hit, or the head can pop off when we hit the torso, for example.
if (joint.LimbB != targetLimb) { continue; }
// Limb A is where we start creating the joint and LimbB is where the joint ends.
// Normally the joints have been created starting from the body, in which case we'd want to use LimbB e.g. to severe a hand when it's hit.
// But heads are a different case, because many characters have been created so that the head is first and then comes the rest of the body.
// If this is the case, we'll have to use LimbA to decapitate the creature when it's hit on the head. Otherwise decapitation could happen only when we hit the body, not the head.
var referenceLimb = targetLimb.type == LimbType.Head && targetLimb.Params.ID == 0 ? joint.LimbA : joint.LimbB;
if (referenceLimb != targetLimb) { continue; }
float probability = severLimbsProbability;
if (!IsDead)
{
@@ -3593,9 +3620,20 @@ namespace Barotrauma
if (severed)
{
Limb otherLimb = joint.LimbA == targetLimb ? joint.LimbB : joint.LimbA;
otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f);
otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f);
if (attacker != null)
{
foreach (var statusEffect in statusEffects)
{
if (statusEffect.type == ActionType.OnSevered) { statusEffect.SetUser(attacker); }
}
foreach (var statusEffect in targetLimb.StatusEffects)
{
if (statusEffect.type == ActionType.OnSevered) { statusEffect.SetUser(attacker); }
}
}
ApplyStatusEffects(ActionType.OnSevered, 1.0f);
targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
}
}
if (wasSevered && targetLimb.character.AIController is EnemyAIController enemyAI)
@@ -3724,10 +3762,6 @@ namespace Barotrauma
if (!wasDead)
{
TryAdjustAttackerSkill(attacker, CharacterHealth.Vitality - prevVitality);
if (IsDead)
{
attacker?.RecordKill(this);
}
}
};
if (attackResult.Damage > 0)
@@ -3818,40 +3852,49 @@ namespace Barotrauma
targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets));
statusEffect.Apply(actionType, deltaTime, this, targets);
}
else
else if (statusEffect.targetLimbs != null)
{
statusEffect.Apply(actionType, deltaTime, this, this);
if (statusEffect.targetLimbs != null)
foreach (var limbType in statusEffect.targetLimbs)
{
foreach (var limbType in statusEffect.targetLimbs)
if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
// Target all matching limbs
foreach (var limb in AnimController.Limbs)
{
// Target all matching limbs
foreach (var limb in AnimController.Limbs)
if (limb.IsSevered) { continue; }
if (limb.type == limbType)
{
if (limb.IsSevered) { continue; }
if (limb.type == limbType)
{
statusEffect.Apply(actionType, deltaTime, this, limb);
}
statusEffect.sourceBody = limb.body;
statusEffect.Apply(actionType, deltaTime, this, limb);
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
{
// Target just the first matching limb
Limb limb = AnimController.GetLimb(limbType);
if (limb != null)
{
// Target just the first matching limb
Limb limb = AnimController.GetLimb(limbType);
statusEffect.sourceBody = limb.body;
statusEffect.Apply(actionType, deltaTime, this, limb);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb))
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb))
{
// Target just the last matching limb
Limb limb = AnimController.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden);
if (limb != null)
{
// Target just the last matching limb
Limb limb = AnimController.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden);
statusEffect.sourceBody = limb.body;
statusEffect.Apply(actionType, deltaTime, this, limb);
}
}
}
}
if (statusEffect.HasTargetType(StatusEffect.TargetType.This) || statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.Apply(actionType, deltaTime, this, this);
}
}
if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered)
{
@@ -3941,31 +3984,49 @@ namespace Barotrauma
AnimController.Frozen = false;
if (GameAnalyticsManager.SendUserStatistics)
{
string characterType = "Unknown";
if (this == Controlled)
characterType = "Player";
else if (IsRemotePlayer)
characterType = "RemotePlayer";
else if (AIController is EnemyAIController)
characterType = "Enemy";
else if (AIController is HumanAIController)
characterType = "AICrew";
string causeOfDeathStr = causeOfDeathAffliction == null ?
causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Name.Replace(" ", "");
GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":" + SpeciesName + ":" + causeOfDeathStr);
}
CauseOfDeath = new CauseOfDeath(
causeOfDeath, causeOfDeathAffliction?.Prefab,
causeOfDeathAffliction?.Source ?? LastAttacker, LastDamageSource);
causeOfDeathAffliction?.Source, LastDamageSource);
if (GameAnalyticsManager.SendUserStatistics)
{
string causeOfDeathStr = causeOfDeathAffliction == null ?
causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Identifier.Replace(" ", "");
string characterType = GetCharacterType(this);
GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":" + causeOfDeathStr);
if (CauseOfDeath.Killer != null)
{
GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":Killer:" + GetCharacterType(CauseOfDeath.Killer));
}
if (CauseOfDeath.DamageSource != null)
{
string damageSourceStr = CauseOfDeath.DamageSource.ToString();
if (CauseOfDeath.DamageSource is Item damageSourceItem) { damageSourceStr = damageSourceItem.ToString(); }
GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":DamageSource:" + damageSourceStr);
}
static string GetCharacterType(Character character)
{
if (character.IsPlayer)
return "Player";
else if (character.AIController is EnemyAIController)
return "Enemy" + character.SpeciesName;
else if (character.AIController is HumanAIController && character.TeamID == CharacterTeamType.Team2)
return "EnemyHuman";
else if (character.Info != null && character.TeamID == CharacterTeamType.Team1)
return "AICrew";
else if (character.Info != null && character.TeamID == CharacterTeamType.FriendlyNPC)
return "FriendlyNPC";
return "Unknown";
}
}
OnDeath?.Invoke(this, CauseOfDeath);
var abilityKiller = new AbilityCharacter(CauseOfDeath.Killer);
CheckTalents(AbilityEffectType.OnDieToCharacter, abilityKiller);
var abilityCharacterKiller = new AbilityCharacterKiller(CauseOfDeath.Killer);
CheckTalents(AbilityEffectType.OnDieToCharacter, abilityCharacterKiller);
CauseOfDeath.Killer?.RecordKill(this);
if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen)
{
@@ -3974,7 +4035,7 @@ namespace Barotrauma
KillProjSpecific(causeOfDeath, causeOfDeathAffliction, log);
if (info != null)
if (info != null)
{
info.CauseOfDeath = CauseOfDeath;
info.MissionsCompletedSinceDeath = 0;
@@ -4089,7 +4150,7 @@ namespace Barotrauma
info?.Remove();
#if CLIENT
GameMain.GameSession?.CrewManager?.KillCharacter(this);
GameMain.GameSession?.CrewManager?.KillCharacter(this, resetCrewListIndex: false);
#endif
CharacterList.Remove(this);
@@ -4104,6 +4165,8 @@ namespace Barotrauma
}
}
itemSelectedDurations.Clear();
DisposeProjSpecific();
aiTarget?.Remove();
@@ -4476,18 +4539,19 @@ namespace Barotrauma
if (info == null) { return false; }
info.UnlockedTalents.Add(talentPrefab.Identifier);
if (characterTalents.Any(t => t.Prefab == talentPrefab)) { return false; }
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateTalents });
#endif
CharacterTalent characterTalent = new CharacterTalent(talentPrefab, this);
characterTalent.ActivateTalent(addingFirstTime);
characterTalents.Add(characterTalent);
characterTalent.ActivateTalent(addingFirstTime);
characterTalent.AddedThisRound = addingFirstTime;
if (addingFirstTime)
{
OnTalentGiven(talentPrefab.Identifier);
OnTalentGiven(talentPrefab);
GameAnalyticsManager.AddDesignEvent("TalentUnlocked:" + (info.Job?.Prefab.Identifier ?? "None") + ":" + talentPrefab.Identifier,
GameMain.GameSession?.Campaign?.TotalPlayTime ?? 0.0);
}
return true;
}
@@ -4497,6 +4561,24 @@ namespace Barotrauma
return info.UnlockedTalents.Contains(identifier);
}
public bool HasUnlockedAllTalents()
{
if (TalentTree.JobTalentTrees.TryGetValue(Info.Job.Prefab.Identifier, out TalentTree talentTree))
{
foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees)
{
foreach (TalentOption talentOption in talentSubTree.TalentOptionStages)
{
if (talentOption.Talents.None(t => HasTalent(t.Identifier)))
{
return false;
}
}
}
}
return true;
}
public static IEnumerable<Character> GetFriendlyCrew(Character character)
{
if (character is null)
@@ -4556,7 +4638,7 @@ namespace Barotrauma
}
partial void OnMoneyChanged(int prevAmount, int newAmount);
partial void OnTalentGiven(string talentIdentifier);
partial void OnTalentGiven(TalentPrefab talentPrefab);
/// <summary>
/// This dictionary is used for stats that are required very frequently. Not very performant, but easier to develop with for now.
@@ -4728,4 +4810,49 @@ namespace Barotrauma
public Character Killer { get; set; }
}
class AbilityAttackData : AbilityObject, IAbilityCharacter
{
public float DamageMultiplier { get; set; } = 1f;
public float AddedPenetration { get; set; } = 0f;
public List<Affliction> Afflictions { get; set; }
public bool ShouldImplode { get; set; } = false;
public Attack SourceAttack { get; }
public Character Character { get; set; }
public Character Attacker { get; set; }
public AbilityAttackData(Attack sourceAttack, Character character)
{
SourceAttack = sourceAttack;
Character = character;
}
}
class AbilityAttackResult : AbilityObject, IAbilityAttackResult
{
public AttackResult AttackResult { get; set; }
public AbilityAttackResult(AttackResult attackResult)
{
AttackResult = attackResult;
}
}
class AbilityCharacterKiller : AbilityObject, IAbilityCharacter
{
public AbilityCharacterKiller(Character character)
{
Character = character;
}
public Character Character { get; set; }
}
class AbilityOrderedCharacter : AbilityObject, IAbilityCharacter
{
public AbilityOrderedCharacter(Character character)
{
Character = character;
}
public Character Character { get; set; }
}
}
@@ -43,6 +43,7 @@ namespace Barotrauma
public int FaceAttachmentIndex { get; set; } = -1;
public XElement HairElement { get; set; }
public XElement HairWithHatElement { get; set; }
public XElement BeardElement { get; set; }
public XElement MoustacheElement { get; set; }
public XElement FaceAttachment { get; set; }
@@ -361,7 +362,7 @@ namespace Barotrauma
public CharacterTeamType TeamID;
private readonly NPCPersonalityTrait personalityTrait;
private NPCPersonalityTrait personalityTrait;
public const int MaxCurrentOrders = 3;
public static int HighestManualOrderPriority => MaxCurrentOrders;
@@ -568,7 +569,7 @@ namespace Barotrauma
HasGenders = CharacterConfigElement.GetAttributeBool("genders", false);
HasRaces = CharacterConfigElement.GetAttributeBool("races", false);
SetGenderAndRace(randSync);
Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, variant);
Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, randSync, variant);
HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray();
@@ -584,11 +585,10 @@ namespace Barotrauma
}
else
{
name = "";
Name = GetRandomName(randSync);
}
OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name;
personalityTrait = NPCPersonalityTrait.GetRandom(name + HeadSpriteId);
SetPersonalityTrait();
Salary = CalculateSalary();
if (ragdollFileName != null)
{
@@ -597,6 +597,11 @@ namespace Barotrauma
LoadHeadAttachments();
}
private void SetPersonalityTrait()
{
personalityTrait = NPCPersonalityTrait.GetRandom(Name + HeadSpriteId);
}
public string GetRandomName(Rand.RandSync randSync)
{
string name = "";
@@ -1121,6 +1126,20 @@ namespace Barotrauma
Head.HairElement = GetRandomElement(hairs);
Head.HairIndex = hairs.IndexOf(Head.HairElement);
}
if (Head.HairElement != null)
{
int thisHairIndex = hairs.IndexOf(head.HairElement);
int hairWithHatIndex = head.HairElement.GetAttributeInt("replacewhenwearinghat", thisHairIndex);
if (thisHairIndex != hairWithHatIndex && hairWithHatIndex > -1 && hairWithHatIndex < hairs.Count)
{
head.HairWithHatElement = hairs[hairWithHatIndex];
}
else
{
head.HairWithHatElement = null;
}
}
if (IsValidIndex(Head.BeardIndex, beards))
{
Head.BeardElement = beards[Head.BeardIndex];
@@ -1261,7 +1280,7 @@ namespace Barotrauma
{
int prevAmount = ExperiencePoints;
var experienceGainMultiplier = new AbilityValue(1f);
var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f);
if (isMissionExperience)
{
Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplier);
@@ -1523,7 +1542,7 @@ namespace Barotrauma
orderTargetElement.Add(new XAttribute("hullid", (uint)ot.Hull.ID));
position -= ot.Hull.WorldPosition;
}
orderTargetElement.Add(new XAttribute("position", $"{position.X},{position.Y}"));
orderTargetElement.Add(new XAttribute("position", XMLExtensions.Vector2ToString(position)));
orderElement.Add(orderTargetElement);
break;
case Order.OrderTargetType.WallSection when targetAvailableInNextLevel && order.TargetEntity is Structure s && order.WallSectionIndex.HasValue:
@@ -1858,18 +1877,27 @@ namespace Barotrauma
}
}
class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilityString, IAbilityCharacter
class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter
{
public AbilitySkillGain(float value, string abilityString, Character character, bool gainedFromAbility)
public AbilitySkillGain(float skillAmount, string skillIdentifier, Character character, bool gainedFromAbility)
{
Value = value;
String = abilityString;
Value = skillAmount;
SkillIdentifier = skillIdentifier;
Character = character;
GainedFromAbility = gainedFromAbility;
}
public Character Character { get; set; }
public float Value { get; set; }
public string String { get; set; }
public string SkillIdentifier { get; set; }
public bool GainedFromAbility { get; }
}
class AbilityExperienceGainMultiplier : AbilityObject, IAbilityValue
{
public AbilityExperienceGainMultiplier(float experienceGainMultiplier)
{
Value = experienceGainMultiplier;
}
public float Value { get; set; }
}
}
@@ -67,6 +67,9 @@ namespace Barotrauma
public Affliction(AfflictionPrefab prefab, float strength)
{
#if CLIENT
prefab?.ReloadSoundsIfNeeded();
#endif
Prefab = prefab;
PendingAdditionStrength = Prefab.GrainBurst;
_strength = strength;
@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma
namespace Barotrauma
{
class AfflictionBleeding : Affliction
{
@@ -15,6 +11,10 @@ namespace Barotrauma
{
base.Update(characterHealth, targetLimb, deltaTime);
characterHealth.BloodlossAmount += Strength * (1.0f / 60.0f) * deltaTime;
if (Source != null)
{
characterHealth.BloodlossAffliction.Source = Source;
}
}
}
}
@@ -52,10 +52,6 @@ namespace Barotrauma
{
if (state == value) { return; }
state = value;
if (character != null && character == Character.Controlled)
{
UpdateMessages();
}
}
}
@@ -81,6 +77,9 @@ namespace Barotrauma
base.Update(characterHealth, targetLimb, deltaTime);
character = characterHealth.Character;
if (character == null) { return; }
UpdateMessages();
if (!subscribedToDeathEvent)
{
character.OnDeath += CharacterDead;
@@ -107,7 +106,7 @@ namespace Barotrauma
{
if (State != InfectionState.Active && stun)
{
character.SetStun(Rand.Range(2, 4));
character.SetStun(Rand.Range(2f, 3f));
}
State = InfectionState.Active;
ActivateHusk();
@@ -247,7 +246,7 @@ namespace Barotrauma
if (huskPrefab.ControlHusk || GameMain.Lua.game.enableControlHusk)
{
#if SERVER
var client = GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.CharacterInfo.Character == character);
var client = GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.Character == character);
if (client != null)
{
GameMain.Server.SetClientCharacter(client, husk);
@@ -363,6 +363,9 @@ namespace Barotrauma
public readonly string Name, Description;
public readonly string TranslationOverride;
public readonly bool IsBuff;
public readonly bool HealableInMedicalClinic;
public readonly float HealCostMultiplier;
public readonly int BaseHealCost;
public readonly string CauseOfDeathDescription, SelfCauseOfDeathDescription;
@@ -656,6 +659,13 @@ namespace Barotrauma
Description = TextManager.Get("AfflictionDescription." + translationId, true) ?? element.GetAttributeString("description", "");
IsBuff = element.GetAttributeBool("isbuff", false);
HealableInMedicalClinic = element.GetAttributeBool("healableinmedicalclinic",
!IsBuff &&
!AfflictionType.Equals("geneticmaterialbuff", StringComparison.OrdinalIgnoreCase) &&
!AfflictionType.Equals("geneticmaterialdebuff", StringComparison.OrdinalIgnoreCase));
HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier).ToLowerInvariant(), 1f);
BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost).ToLowerInvariant(), 0);
if (element.Attribute("nameidentifier") != null)
{
Name = TextManager.Get(element.GetAttributeString("nameidentifier", string.Empty), returnNull: true) ?? Name;
@@ -677,7 +687,7 @@ namespace Barotrauma
MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f);
GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLowerInvariant(), 0.0f);
ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, 0.05f));
ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : 0.05f));
TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f));
DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f);
@@ -751,6 +761,32 @@ namespace Barotrauma
}
}
#if CLIENT
public void ReloadSoundsIfNeeded()
{
foreach (var effect in effects)
{
foreach (var statusEffect in effect.StatusEffects)
{
foreach (var sound in statusEffect.Sounds)
{
if (sound.Sound == null) { Submarine.ReloadRoundSound(sound); }
}
}
}
foreach (var periodicEffect in periodicEffects)
{
foreach (var statusEffect in periodicEffect.StatusEffects)
{
foreach (var sound in statusEffect.Sounds)
{
if (sound.Sound == null) { Submarine.ReloadRoundSound(sound); }
}
}
}
}
#endif
public override string ToString()
{
return "AfflictionPrefab (" + Name + ")";
@@ -112,6 +112,7 @@ namespace Barotrauma
private Affliction oxygenLowAffliction;
private Affliction pressureAffliction;
private Affliction stunAffliction;
public Affliction BloodlossAffliction { get => bloodlossAffliction; }
public bool IsUnconscious
{
@@ -182,7 +183,7 @@ namespace Barotrauma
public float BloodlossAmount
{
get { return bloodlossAffliction.Strength; }
set { bloodlossAffliction.Strength = MathHelper.Clamp(value, 0.0f, 100.0f); }
set { bloodlossAffliction.Strength = MathHelper.Clamp(value, 0, bloodlossAffliction.Prefab.MaxStrength); }
}
public float Stun
@@ -202,7 +203,7 @@ namespace Barotrauma
get { return pressureAffliction; }
}
public Character Character { get; private set; }
public readonly Character Character;
public CharacterHealth(Character character)
{
@@ -325,7 +326,11 @@ namespace Barotrauma
if (kvp.Key == affliction)
{
int limbHealthIndex = limbHealths.IndexOf(kvp.Value);
return Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == limbHealthIndex);
foreach (Limb limb in Character.AnimController.Limbs)
{
if (limb.HealthIndex == limbHealthIndex) { return limb; }
}
return null;
}
}
return null;
@@ -681,7 +686,7 @@ namespace Barotrauma
newStrength = Math.Min(existingAffliction.Prefab.MaxStrength, newStrength);
if (existingAffliction == stunAffliction) { Character.SetStun(newStrength, true, true); }
existingAffliction.Strength = newStrength;
existingAffliction.Source = newAffliction.Source;
if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; }
CalculateVitality();
if (Vitality <= MinVitality)
{
@@ -767,7 +772,6 @@ namespace Barotrauma
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed));
// maybe a bit of a hacky way to do this. should inquire if there is a better way. M61T
if (Character.InWater)
{
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.SwimmingSpeed));
@@ -35,7 +35,7 @@ namespace Barotrauma
public Skill PrimarySkill { get; }
public Job(JobPrefab jobPrefab, int variant = 0)
public Job(JobPrefab jobPrefab, Rand.RandSync randSync = Rand.RandSync.Unsynced, int variant = 0)
{
prefab = jobPrefab;
Variant = variant;
@@ -43,7 +43,7 @@ namespace Barotrauma
skills = new Dictionary<string, Skill>();
foreach (SkillPrefab skillPrefab in prefab.Skills)
{
var skill = new Skill(skillPrefab);
var skill = new Skill(skillPrefab, randSync);
skills.Add(skillPrefab.Identifier, skill);
if (skillPrefab.IsPrimarySkill) { PrimarySkill = skill; }
}
@@ -79,7 +79,7 @@ namespace Barotrauma
{
var prefab = JobPrefab.Random(randSync);
var variant = Rand.Range(0, prefab.Variants, randSync);
return new Job(prefab, variant);
return new Job(prefab, randSync, variant);
}
public float GetSkillLevel(string skillIdentifier)
@@ -18,7 +18,7 @@ namespace Barotrauma
public void IncreaseSkill(float value, bool increasePastMax)
{
level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumOlympianSkill : MaximumSkill);
level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill);
}
private Sprite icon;
@@ -36,10 +36,10 @@ namespace Barotrauma
public readonly float PriceMultiplier = 1.0f;
public Skill(SkillPrefab prefab)
public Skill(SkillPrefab prefab, Rand.RandSync randSync)
{
Identifier = prefab.Identifier;
level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, Rand.RandSync.Server);
level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync);
icon = GetIcon();
PriceMultiplier = prefab.PriceMultiplier;
}
@@ -556,6 +556,7 @@ namespace Barotrauma
// TODO: We might need this or solve the cases where a limb is severed while holding on to an item
//if (character.Params.CanInteract) { return false; }
if (this == character.AnimController.MainLimb) { return false; }
bool canBeSevered = Params.CanBeSeveredAlive;
if (character.AnimController.CanWalk)
{
switch (type)
@@ -571,7 +572,7 @@ namespace Barotrauma
return false;
}
}
return true;
return canBeSevered;
}
}
@@ -583,6 +584,8 @@ namespace Barotrauma
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
public IEnumerable<StatusEffect> StatusEffects { get { return statusEffects; } }
public Limb(Ragdoll ragdoll, Character character, LimbParams limbParams)
{
this.ragdoll = ragdoll;
@@ -647,6 +650,8 @@ namespace Barotrauma
if (attackElement != null)
{
attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f);
attack.RangeMultiplier = attackElement.GetAttributeFloat("rangemultiplier", 1f);
attack.ImpactMultiplier = attackElement.GetAttributeFloat("impactmultiplier", 1f);
}
}
break;
@@ -756,12 +761,13 @@ namespace Barotrauma
}
if (attacker != null)
{
var abilityAffliction = new AbilityAfflictionCharacter(newAffliction, character);
attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAffliction);
var abilityAfflictionCharacter = new AbilityAfflictionCharacter(newAffliction, character);
attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAfflictionCharacter);
}
if (applyAffliction)
{
afflictionsCopy.Add(newAffliction);
newAffliction.Source ??= attacker;
}
appliedDamageModifiers.AddRange(tempModifiers);
}
@@ -1065,7 +1071,7 @@ namespace Barotrauma
#endif
if (damageTarget is Character targetCharacter && targetLimb != null)
{
attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound, body);
attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound, body, this);
}
else
{
@@ -1075,7 +1081,7 @@ namespace Barotrauma
}
else
{
attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound, body);
attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound, body, this);
}
}
/*if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.Server))
@@ -1309,4 +1315,16 @@ namespace Barotrauma
partial void LoadParamsProjSpecific();
}
class AbilityAfflictionCharacter : AbilityObject, IAbilityAffliction, IAbilityCharacter
{
public AbilityAfflictionCharacter(Affliction affliction, Character character)
{
Affliction = affliction;
Character = character;
}
public Character Character { get; set; }
public Affliction Affliction { get; set; }
}
}
@@ -14,9 +14,9 @@ namespace Barotrauma
NotDefined = 0,
Walk = 1,
Run = 2,
Crouch = 3,
SwimSlow = 4,
SwimFast = 5
SwimSlow = 3,
SwimFast = 4,
Crouch = 5
}
abstract class GroundedMovementParams : AnimationParams
@@ -26,6 +26,15 @@ namespace Barotrauma
class HumanCrouchParams : HumanGroundedParams
{
[Serialize(0.0f, true, description: "How much lower the character's head and torso move when stationary."), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2)]
public float MoveDownAmountWhenStationary { get; set; }
[Serialize(0.0f, true), Editable(-360f, 360f)]
public float ExtraHeadAngleWhenStationary { get; set; }
[Serialize(0.0f, true), Editable(-360f, 360f)]
public float ExtraTorsoAngleWhenStationary { get; set; }
public static HumanCrouchParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanCrouchParams>(character, AnimationType.Crouch);
public static HumanCrouchParams GetAnimParams(Character character, string fileName = null)
{
@@ -612,7 +612,7 @@ namespace Barotrauma
[Serialize(0f, true, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float Width { get; set; }
[Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)]
[Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 100, DecimalCount = 2)]
public float Density { get; set; }
[Serialize(false, true), Editable]
@@ -648,6 +648,9 @@ namespace Barotrauma
[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; }
[Serialize(true, true, description: "Disable if you don't want to allow severing this joint while the creature is alive. Note: Does nothing if the 'Severance Probability Modifier' in the joint settings is 0 (default). Also note that the setting doesn't override certain limitations, e.g. severing the main limb, or legs of a walking creature is not allowed."), Editable]
public bool CanBeSeveredAlive { get; set; }
//how long it takes for severed limbs to fade out
[Serialize(10f, true, "How long it takes for the severed limb to fade out"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1)]
public float SeveredFadeOutTime { get; set; } = 10.0f;
@@ -42,7 +42,7 @@ namespace Barotrauma
}
private float skillIncreasePerRepairedStructureDamage;
[Serialize(0.005f, true)]
[Serialize(0.0025f, true)]
public float SkillIncreasePerRepairedStructureDamage
{
get { return skillIncreasePerRepairedStructureDamage * GetCurrentSkillGainMultiplier(); }
@@ -96,8 +96,8 @@ namespace Barotrauma
set;
}
[Serialize(500.0f, true)]
public float MaximumOlympianSkill
[Serialize(200.0f, true)]
public float MaximumSkillWithTalents
{
get;
set;
@@ -1,5 +1,6 @@
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -7,15 +8,19 @@ namespace Barotrauma.Abilities
{
class AbilityConditionAttackData : AbilityConditionData
{
[Flags]
private enum WeaponType
{
Any = 0,
Melee = 1,
Ranged = 2,
HandheldRanged = 3,
Turret = 4
HandheldRanged = 4,
Turret = 8,
NoWeapon = 16
};
private static readonly List<WeaponType> WeaponTypeValues = Enum.GetValues(typeof(WeaponType)).Cast<WeaponType>().ToList();
private readonly string itemIdentifier;
private readonly string[] tags;
private readonly WeaponType weapontype;
@@ -65,27 +70,39 @@ namespace Barotrauma.Abilities
if (weapontype != WeaponType.Any)
{
switch (weapontype)
foreach (WeaponType wt in WeaponTypeValues)
{
// it is possible that an item that has both a melee and a projectile component will return true
// even when not used as a melee/ranged weapon respectively
// attackdata should contain data regarding whether the attack is melee or not
case WeaponType.Melee:
return item?.GetComponent<MeleeWeapon>() != null;
case WeaponType.Ranged:
return item?.GetComponent<Projectile>() != null;
case WeaponType.HandheldRanged:
{
var projectile = item?.GetComponent<Projectile>();
return projectile?.Launcher?.GetComponent<Holdable>() != null;
}
case WeaponType.Turret:
{
var projectile = item?.GetComponent<Projectile>();
return projectile?.Launcher?.GetComponent<Turret>() != null;
}
if (wt == WeaponType.Any || !weapontype.HasFlag(wt)) { continue; }
switch (wt)
{
// it is possible that an item that has both a melee and a projectile component will return true
// even when not used as a melee/ranged weapon respectively
// attackdata should contain data regarding whether the attack is melee or not
case WeaponType.Melee:
if (item?.GetComponent<MeleeWeapon>() != null) { return true; }
break;
case WeaponType.Ranged:
if (item?.GetComponent<Projectile>() != null) { return true; }
break;
case WeaponType.HandheldRanged:
{
var projectile = item?.GetComponent<Projectile>();
if (projectile?.Launcher?.GetComponent<Holdable>() != null) { return true; }
}
break;
case WeaponType.Turret:
{
var projectile = item?.GetComponent<Projectile>();
if (projectile?.Launcher?.GetComponent<Turret>() != null) { return true; }
}
break;
case WeaponType.NoWeapon:
if (item == null) { return true; }
break;
}
}
}
return false;
}
return true;
}
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
@@ -7,9 +8,26 @@ namespace Barotrauma.Abilities
{
private readonly List<TargetType> targetTypes;
private List<PropertyConditional> conditionals = new List<PropertyConditional>();
public AbilityConditionCharacter(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", new string[0], convertToLowerInvariant: true));
foreach (XElement subElement in conditionElement.Elements())
{
if (subElement.Name.ToString().Equals("conditional", StringComparison.OrdinalIgnoreCase))
{
foreach (XAttribute attribute in subElement.Attributes())
{
if (PropertyConditional.IsValid(attribute))
{
conditionals.Add(new PropertyConditional(attribute));
}
}
break;
}
}
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
@@ -18,7 +36,10 @@ namespace Barotrauma.Abilities
{
if (!(abilityCharacter.Character is Character character)) { return false; }
if (!IsViableTarget(targetTypes, character)) { return false; }
foreach (var conditional in conditionals)
{
if (!conditional.Matches(character)) { return false; }
}
return true;
}
else
@@ -0,0 +1,38 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionItemInSubmarine : AbilityConditionData
{
private readonly SubmarineType? submarineType;
public AbilityConditionItemInSubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement)
{
if (conditionElement.Attribute("submarinetype") != null)
{
submarineType = conditionElement.GetAttributeEnum<SubmarineType>("submarinetype", SubmarineType.Player);
}
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityItem)?.Item is Item item)
{
if (item.Submarine == null) { return false; }
if (submarineType.HasValue)
{
return item.Submarine.Info?.Type == submarineType.Value;
}
else
{
return true;
}
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityItem));
return false;
}
}
}
}
@@ -1,23 +0,0 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionItemOutsideSubmarine : AbilityConditionData
{
public AbilityConditionItemOutsideSubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityItem)?.Item is Item item)
{
return item.Submarine == null || item.Submarine.TeamID != character.Info.TeamID;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityItem));
return false;
}
}
}
}
@@ -1,23 +0,0 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionItemWreck : AbilityConditionData
{
public AbilityConditionItemWreck(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityItem)?.Item is Item item)
{
return item.Submarine?.Info?.IsWreck ?? false;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityItem));
return false;
}
}
}
}
@@ -18,13 +18,13 @@ namespace Barotrauma.Abilities
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityString)?.String is string skillIdentifier)
if ((abilityObject as IAbilitySkillIdentifier)?.SkillIdentifier is string skillIdentifier)
{
return MatchesConditionSpecific(skillIdentifier);
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityString));
LogAbilityConditionError(abilityObject, typeof(IAbilitySkillIdentifier));
return false;
}
}
@@ -30,9 +30,9 @@
public Character Character { get; set; }
}
interface IAbilityString
interface IAbilitySkillIdentifier
{
public string String { get; set; }
public string SkillIdentifier { get; set; }
}
interface IAbilityAffliction
@@ -16,173 +16,4 @@ namespace Barotrauma.Abilities
public Character Character { get; set; }
}
class AbilityItem : AbilityObject, IAbilityItem
{
public AbilityItem(Item item)
{
Item = item;
}
public Item Item { get; set; }
}
class AbilityValue : AbilityObject, IAbilityValue
{
public AbilityValue(float value)
{
Value = value;
}
public float Value { get; set; }
}
class AbilityAffliction : AbilityObject, IAbilityAffliction
{
public AbilityAffliction(Affliction affliction)
{
Affliction = affliction;
}
public Affliction Affliction { get; set; }
}
class AbilityAfflictionCharacter : AbilityObject, IAbilityAffliction, IAbilityCharacter
{
public AbilityAfflictionCharacter(Affliction affliction, Character character)
{
Affliction = affliction;
Character = character;
}
public Character Character { get; set; }
public Affliction Affliction { get; set; }
}
class AbilityValueItem : AbilityObject, IAbilityValue, IAbilityItemPrefab
{
public AbilityValueItem(float value, ItemPrefab itemPrefab)
{
Value = value;
ItemPrefab = itemPrefab;
}
public float Value { get; set; }
public ItemPrefab ItemPrefab { get; set; }
}
class AbilityItemPrefabItem : AbilityObject, IAbilityItem, IAbilityItemPrefab
{
public AbilityItemPrefabItem(Item item, ItemPrefab itemPrefab)
{
Item = item;
ItemPrefab = itemPrefab;
}
public Item Item { get; set; }
public ItemPrefab ItemPrefab { get; set; }
}
class AbilityValueString : AbilityObject, IAbilityValue, IAbilityString
{
public AbilityValueString(float value, string abilityString)
{
Value = value;
String = abilityString;
}
public float Value { get; set; }
public string String { get; set; }
}
class AbilityStringCharacter : AbilityObject, IAbilityCharacter, IAbilityString
{
public AbilityStringCharacter(string abilityString, Character character)
{
String = abilityString;
Character = character;
}
public Character Character { get; set; }
public string String { get; set; }
}
class AbilityValueAffliction : AbilityObject, IAbilityValue, IAbilityAffliction
{
public AbilityValueAffliction(float value, Affliction affliction)
{
Value = value;
Affliction = affliction;
}
public float Value { get; set; }
public Affliction Affliction { get; set; }
}
class AbilityValueMission : AbilityObject, IAbilityValue, IAbilityMission
{
public AbilityValueMission(float value, Mission mission)
{
Value = value;
Mission = mission;
}
public float Value { get; set; }
public Mission Mission { get; set; }
}
class AbilityLocation : AbilityObject, IAbilityLocation
{
public AbilityLocation(Location location)
{
Location = location;
}
public Location Location { get; set; }
}
// this is an exception class that should only be passed in this form, so classes that use it should cast into it directly
class AbilityAttackData : AbilityObject, IAbilityCharacter
{
public float DamageMultiplier { get; set; } = 1f;
public float AddedPenetration { get; set; } = 0f;
public List<Affliction> Afflictions { get; set; }
public bool ShouldImplode { get; set; } = false;
public Attack SourceAttack { get; }
public Character Character { get; set; }
public Character Attacker { get; set; }
public AbilityAttackData(Attack sourceAttack, Character character)
{
SourceAttack = sourceAttack;
Character = character;
}
}
class AbilityApplyTreatment : AbilityObject, IAbilityCharacter, IAbilityItem
{
public Character Character { get; set; }
public Character User { get; set; }
public Item Item { get; set; }
public AbilityApplyTreatment(Character user, Character target, Item item)
{
Character = target;
User = user;
Item = item;
}
}
class AbilityAttackResult : AbilityObject, IAbilityAttackResult
{
public AttackResult AttackResult { get; set; }
public AbilityAttackResult(AttackResult attackResult)
{
AttackResult = attackResult;
}
}
class AbilityCharacterSubmarine : AbilityObject, IAbilityCharacter, IAbilitySubmarine
{
public AbilityCharacterSubmarine(Character character, Submarine submarine)
{
Character = character;
Submarine = submarine;
}
public Character Character { get; set; }
public Submarine Submarine { get; set; }
}
}
@@ -87,7 +87,7 @@ namespace Barotrauma.Abilities
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect in talent {CharacterTalent.DebugIdentifier}");
}
protected void LogabilityObjectMismatch()
protected void LogAbilityObjectMismatch()
{
DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type in talent {CharacterTalent.DebugIdentifier}");
}
@@ -23,7 +23,7 @@ namespace Barotrauma.Abilities
}
else
{
LogabilityObjectMismatch();
LogAbilityObjectMismatch();
}
}
}
@@ -23,7 +23,9 @@ namespace Barotrauma.Abilities
multiplier = 0 + Character.Info.GetSavedStatValue(StatTypes.None, scalingStatIdentifier);
}
targetCharacter.GiveMoney((int)(multiplier * amount));
int totalAmount = (int)(multiplier * amount);
targetCharacter.GiveMoney(totalAmount);
GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier);
}
protected override void ApplyEffect(AbilityObject abilityObject)
@@ -29,7 +29,7 @@ namespace Barotrauma.Abilities
}
else
{
LogabilityObjectMismatch();
LogAbilityObjectMismatch();
}
}
}
@@ -41,7 +41,7 @@ namespace Barotrauma.Abilities
}
else
{
LogabilityObjectMismatch();
LogAbilityObjectMismatch();
}
}
}
@@ -37,7 +37,7 @@ namespace Barotrauma.Abilities
}
else
{
LogabilityObjectMismatch();
LogAbilityObjectMismatch();
}
}
}
@@ -34,7 +34,7 @@ namespace Barotrauma.Abilities
}
else
{
LogabilityObjectMismatch();
LogAbilityObjectMismatch();
}
}
}
@@ -1,6 +1,4 @@
using Microsoft.Xna.Framework;
using System;
using System.Xml.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -18,7 +16,7 @@ namespace Barotrauma.Abilities
if (abilityObject is AbilitySkillGain abilitySkillGain && abilitySkillGain.Character != Character)
{
if (ignoreAbilitySkillGain && abilitySkillGain.GainedFromAbility) { return; }
Character.Info?.IncreaseSkillLevel(abilitySkillGain.String, 1.0f, gainedFromAbility: true);
Character.Info?.IncreaseSkillLevel(abilitySkillGain.SkillIdentifier, 1.0f, gainedFromAbility: true);
}
}
}
@@ -16,7 +16,9 @@ namespace Barotrauma.Abilities
{
if ((abilityObject as IAbilityCharacter)?.Character is Character character)
{
Character.GiveMoney((int)(vitalityPercentage * character.MaxVitality));
int totalAmount = (int)(vitalityPercentage * character.MaxVitality);
Character.GiveMoney(totalAmount);
GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier);
}
}
}
@@ -30,6 +30,7 @@ namespace Barotrauma.Abilities
if (!enemyCharacter.LockHands) { continue; }
if (timesGiven > max) { continue; }
Character.GiveMoney(moneyAmount);
GameAnalyticsManager.AddMoneyGainedEvent(moneyAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier);
foreach (Character character in Character.GetFriendlyCrew(Character))
{
character.Info?.GiveExperience(experienceAmount);
@@ -12,8 +12,6 @@ namespace Barotrauma.Abilities
private readonly int moneyPerMission;
private static List<Client> clientsAlreadyUsed = new List<Client>();
public CharacterAbilityInsurancePolicy(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
moneyPerMission = abilityElement.GetAttributeInt("moneypermission", 0);
@@ -23,7 +21,9 @@ namespace Barotrauma.Abilities
{
if (Character?.Info is CharacterInfo info)
{
Character.GiveMoney(moneyPerMission * info.MissionsCompletedSinceDeath);
int totalAmount = moneyPerMission * info.MissionsCompletedSinceDeath;
Character.GiveMoney(totalAmount);
GameAnalyticsManager.AddMoneyGainedEvent(totalAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier);
}
}
}
@@ -13,7 +13,7 @@ namespace Barotrauma.Abilities
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityString)?.String is string skillIdentifier)
if ((abilityObject as IAbilitySkillIdentifier)?.SkillIdentifier is string skillIdentifier)
{
if (skillIdentifier != lastSkillIdentifier)
{
@@ -9,7 +9,7 @@ namespace Barotrauma.Abilities
class CharacterAbilityTandemFire : CharacterAbilityApplyStatusEffectsToNearestAlly
{
// this should just be its own class, misleading to inherit here
private string tag;
private readonly string tag;
public CharacterAbilityTandemFire(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
tag = abilityElement.GetAttributeString("tag", "");
@@ -20,7 +20,7 @@ namespace Barotrauma.Abilities
if (Character.SelectedConstruction == null || !Character.SelectedConstruction.HasTag(tag)) { return; }
Character closestCharacter = null;
float closestDistance = float.MaxValue;
float closestDistance = squaredMaxDistance;
foreach (Character crewCharacter in Character.GetFriendlyCrew(Character))
{
@@ -522,6 +522,7 @@ namespace Barotrauma
if (targetCharacter == null) { return; }
targetCharacter.GodMode = !targetCharacter.GodMode;
NewMessage((targetCharacter.GodMode ? "Enabled godmode on " : "Disabled godmode on " + targetCharacter.Name), Color.White);
},
() =>
{
@@ -978,15 +979,18 @@ namespace Barotrauma
else
{
NewMessage("Level seed: " + Level.Loaded.Seed);
NewMessage("Level size: " + Level.Loaded.Size.X+"x"+ Level.Loaded.Size.Y);
NewMessage("Level generation params: " + Level.Loaded.GenerationParams.Identifier);
NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none") + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none"));
NewMessage("Mirrored: " + Level.Loaded.Mirrored);
NewMessage("Level size: " + Level.Loaded.Size.X + "x" + Level.Loaded.Size.Y);
NewMessage("Minimum main path width: " + (Level.Loaded.LevelData?.MinMainPathWidth?.ToString() ?? "unknown"));
}
},null));
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;
if (Level.Loaded.Type == LevelData.LevelType.Outpost)
if (Submarine.MainSub == null) { return; }
if (Level.Loaded?.Type == LevelData.LevelType.Outpost && GameMain.GameSession != null)
{
NewMessage("The teleportsub command is unavailable in outpost levels!", Color.Red);
return;
@@ -1002,6 +1006,11 @@ namespace Barotrauma
}
else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase))
{
if (Level.Loaded == null)
{
NewMessage("Can't teleport the sub to the start of the level (no level loaded).", Color.Red);
return;
}
Vector2 pos = Level.Loaded.StartPosition;
if (Level.Loaded.StartOutpost != null)
{
@@ -1011,6 +1020,11 @@ namespace Barotrauma
}
else
{
if (Level.Loaded == null)
{
NewMessage("Can't teleport the sub to the end of the level (no level loaded).", Color.Red);
return;
}
Vector2 pos = Level.Loaded.EndPosition;
if (Level.Loaded.EndOutpost != null)
{
@@ -1033,6 +1047,20 @@ namespace Barotrauma
throw new Exception("crash command issued");
}));
commands.Add(new Command("fastforward", "fastforward [seconds]: Fast forwards the game by x seconds. Note that large numbers may cause a long freeze.", (string[] args) =>
{
float seconds = 0;
if (args.Length > 0) { float.TryParse(args[0], out seconds); }
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Start();
for (int i = 0; i < seconds * Timing.FixedUpdateRate; i++)
{
Screen.Selected?.Update(Timing.Step);
}
sw.Stop();
NewMessage($"Fast-forwarded by {seconds} seconds (took {sw.ElapsedMilliseconds / 1000.0f} s).");
}));
commands.Add(new Command("removecharacter", "removecharacter [character name]: Immediately deletes the specified character.", (string[] args) =>
{
if (args.Length == 0) { return; }
@@ -1190,6 +1218,7 @@ namespace Barotrauma
{
foreach (Item it in Item.ItemList)
{
if (it.GetComponent<GeneticMaterial>() != null) { continue; }
it.Condition = it.MaxCondition;
}
}, null, true));
@@ -1524,6 +1553,7 @@ namespace Barotrauma
if (int.TryParse(args[0], out int money))
{
campaign.Money += money;
GameAnalyticsManager.AddMoneyGainedEvent(money, GameAnalyticsManager.MoneySource.Cheat, "console");
}
else
{
@@ -28,6 +28,7 @@ namespace Barotrauma
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
campaign.Money += Amount;
GameAnalyticsManager.AddMoneyGainedEvent(Amount, GameAnalyticsManager.MoneySource.Event, ParentEvent.Prefab.Identifier);
#if SERVER
(campaign as MultiPlayerCampaign).LastUpdateID++;
#endif
@@ -17,7 +17,7 @@ namespace Barotrauma
public NPCWaitAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
private List<Character> affectedNpcs = null;
private IEnumerable<Character> affectedNpcs;
private AIObjectiveGoTo gotoObjective;
@@ -25,7 +25,7 @@ namespace Barotrauma
{
if (isFinished) { return; }
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList();
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character);
foreach (var npc in affectedNpcs)
{
@@ -62,7 +62,7 @@ namespace Barotrauma
{
foreach (var npc in affectedNpcs)
{
if (npc.Removed || !(npc.AIController is HumanAIController humanAiController)) { continue; }
if (npc.Removed || !(npc.AIController is HumanAIController)) { continue; }
if (gotoObjective != null)
{
gotoObjective.Abandon = true;
@@ -796,8 +796,9 @@ namespace Barotrauma
monsterStrength += enemyAI.CombatStrength;
}
if (character.CurrentHull?.Submarine != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)))
if (character.CurrentHull?.Submarine?.Info != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) &&
character.CurrentHull.Submarine.Info.Type == SubmarineType.Player)
{
// Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42
enemyDanger += enemyAI.CombatStrength / 500.0f;
@@ -351,7 +351,7 @@ namespace Barotrauma
}
}
public static List<string> GetDebugStatistics(int simulatedRoundCount = 100, Func<MonsterEvent, bool> filter = null)
public static List<string> GetDebugStatistics(int simulatedRoundCount = 100, Func<MonsterEvent, bool> filter = null, bool fullLog = false)
{
List<string> debugLines = new List<string>();
@@ -365,7 +365,7 @@ namespace Barotrauma
stats.Add(newStats);
}
debugLines.Add($"Event stats ({eventSet.DebugIdentifier}): ");
LogEventStats(stats, debugLines);
LogEventStats(stats, debugLines, fullLog);
}
return debugLines;
@@ -415,14 +415,19 @@ namespace Barotrauma
if (eventPrefab.EventType == typeof(MonsterEvent) && eventPrefab.TryCreateInstance(out MonsterEvent monsterEvent))
{
if (filter != null && !filter(monsterEvent)) { return; }
float spawnProbability = monsterEvent.Prefab.Probability;
if (Rand.Value() > spawnProbability) { return; }
string character = monsterEvent.speciesName;
int count = Rand.Range(monsterEvent.MinAmount, monsterEvent.MaxAmount + 1);
if (count <= 0) { return; }
if (!stats.MonsterCounts.ContainsKey(character)) { stats.MonsterCounts[character] = 0; }
string character = monsterEvent.speciesName;
if (stats.MonsterCounts.TryGetValue(character, out int currentCount))
{
if (currentCount >= monsterEvent.MaxAmountPerLevel) { return; }
}
else
{
stats.MonsterCounts[character] = 0;
}
stats.MonsterCounts[character] += count;
var aiElement = CharacterPrefab.FindBySpeciesName(character)?.XDocument?.Root?.GetChildElement("ai");
@@ -433,7 +438,7 @@ namespace Barotrauma
}
}
static void LogEventStats(List<EventDebugStats> stats, List<string> debugLines)
static void LogEventStats(List<EventDebugStats> stats, List<string> debugLines, bool fullLog)
{
if (stats.Count == 0 || stats.All(s => s.MonsterCounts.Values.Sum() == 0))
{
@@ -442,28 +447,42 @@ namespace Barotrauma
}
else
{
var allMonsters = new Dictionary<string, int>();
foreach (var stat in stats)
{
foreach (var monster in stat.MonsterCounts)
{
if (!allMonsters.TryAdd(monster.Key, monster.Value))
{
allMonsters[monster.Key] += monster.Value;
}
}
}
allMonsters = allMonsters.OrderBy(m => m.Key).ToDictionary(m => m.Key, m => m.Value);
stats.Sort((s1, s2) => s1.MonsterCounts.Values.Sum().CompareTo(s2.MonsterCounts.Values.Sum()));
debugLines.Add($" Minimum monster count: {stats.First().MonsterCounts.Values.Sum()}");
debugLines.Add($" {LogMonsterCounts(stats.First())}");
debugLines.Add($" Median monster count: {stats[stats.Count / 2].MonsterCounts.Values.Sum()}");
debugLines.Add($" {LogMonsterCounts(stats[stats.Count / 2])}");
debugLines.Add($" Maximum monster count: {stats.Last().MonsterCounts.Values.Sum()}");
debugLines.Add($" {LogMonsterCounts(stats.Last())}");
debugLines.Add($" Average monster count: {StringFormatter.FormatZeroDecimal((float)stats.Average(s => s.MonsterCounts.Values.Sum()))}");
debugLines.Add($" ");
debugLines.Add($" Average monster count: {StringFormatter.FormatZeroDecimal((float)stats.Average(s => s.MonsterCounts.Values.Sum()))} (Min: {stats.First().MonsterCounts.Values.Sum()}, Max: {stats.Last().MonsterCounts.Values.Sum()})");
debugLines.Add($" {LogMonsterCounts(allMonsters, divider: stats.Count)}");
if (fullLog)
{
debugLines.Add($" All samples:");
stats.ForEach(s => debugLines.Add($" {LogMonsterCounts(s.MonsterCounts)}"));
}
stats.Sort((s1, s2) => s1.MonsterStrength.CompareTo(s2.MonsterStrength));
debugLines.Add($" Minimum monster strength: {StringFormatter.FormatZeroDecimal(stats.First().MonsterStrength)}");
debugLines.Add($" Median monster strength: {StringFormatter.FormatZeroDecimal(stats[stats.Count / 2].MonsterStrength)}");
debugLines.Add($" Maximum monster strength: {StringFormatter.FormatZeroDecimal(stats.Last().MonsterStrength)}");
debugLines.Add($" Average monster strength: {StringFormatter.FormatZeroDecimal(stats.Average(s => s.MonsterStrength))}");
debugLines.Add($" Average monster strength: {StringFormatter.FormatZeroDecimal(stats.Average(s => s.MonsterStrength))} (Min: {StringFormatter.FormatZeroDecimal(stats.First().MonsterStrength)}, Max: {StringFormatter.FormatZeroDecimal(stats.Last().MonsterStrength)})");
debugLines.Add($" ");
}
}
static string LogMonsterCounts(EventDebugStats stats)
static string LogMonsterCounts(Dictionary<string, int> stats, float divider = 0)
{
return string.Join(", ", stats.MonsterCounts.Select(mc => mc.Key + " x " + mc.Value));
if (divider > 0)
{
return string.Join("\n ", stats.Select(mc => mc.Key + " x " + (mc.Value / divider).FormatSingleDecimal()));
}
else
{
return string.Join(", ", stats.Select(mc => mc.Key + " x " + mc.Value));
}
}
}
}
@@ -152,7 +152,7 @@ namespace Barotrauma
if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100)
{
spawnPos = new Vector2(
MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 200), wp.CurrentHull.WorldRect.X + 50, wp.CurrentHull.WorldRect.Right - 50),
MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 201), wp.CurrentHull.WorldRect.X + 50, wp.CurrentHull.WorldRect.Right - 50),
wp.CurrentHull.WorldRect.Y - wp.CurrentHull.Rect.Height + 16.0f);
}
var item = new Item(itemPrefab, spawnPos, null);
@@ -1,3 +1,4 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -58,6 +59,18 @@ namespace Barotrauma
if (IsClient) { return; }
if (!swarmSpawned && level.CheckBeaconActive())
{
List<Submarine> connectedSubs = level.BeaconStation.GetConnectedSubs();
foreach (Item item in Item.ItemList)
{
if (!connectedSubs.Contains(item.Submarine)) { continue; }
if (item.GetComponent<PowerTransfer>() != null ||
item.GetComponent<PowerContainer>() != null ||
item.GetComponent<Reactor>() != null)
{
item.Indestructible = true;
}
}
State = 1;
Vector2 spawnPos = level.BeaconStation.WorldPosition;
@@ -50,7 +50,8 @@ namespace Barotrauma
return;
}
int multiplier = CalculateScalingEscortedCharacterCount();
// Disabled for now, because they make balancing the missions a pain.
int multiplier = 1;//CalculateScalingEscortedCharacterCount();
calculatedReward = Prefab.Reward * multiplier;
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(missionSub))}‖end‖";
@@ -319,18 +320,33 @@ namespace Barotrauma
}
}
// characters that survived will take their items with them, in case players tried to be crafty and steal them
// this needs to run here in case players abort the mission by going back home
// TODO: I think this might feel like a bug.
foreach (var characterItem in characterItems)
if (!IsClient)
{
if (Survived(characterItem.Key) || !completed)
foreach (Character character in characters)
{
foreach (Item item in characterItem.Value)
if (character.Inventory == null) { continue; }
foreach (Item item in character.Inventory.AllItemsMod)
{
if (!item.Removed)
//item didn't spawn with the characters -> drop it
if (!characterItems.Any(c => c.Value.Contains(item)))
{
item.Remove();
item.Drop(character);
}
}
}
// characters that survived will take their items with them, in case players tried to be crafty and steal them
// this needs to run here in case players abort the mission by going back home
foreach (var characterItem in characterItems)
{
if (Survived(characterItem.Key) || !completed)
{
foreach (Item item in characterItem.Value)
{
if (!item.Removed)
{
item.Remove();
}
}
}
}
@@ -30,6 +30,7 @@ namespace Barotrauma
GameMain.Server?.UpdateMissionState(this);
#endif
ShowMessage(State);
OnMissionStateChanged?.Invoke(this);
}
}
}
@@ -145,7 +146,9 @@ namespace Barotrauma
}
private List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>();
public Action<Mission> OnMissionStateChanged;
public Mission(MissionPrefab prefab, Location[] locations, Submarine sub)
{
System.Diagnostics.Debug.Assert(locations.Length == 2);
@@ -355,7 +358,7 @@ namespace Barotrauma
IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters();
// use multipliers here so that we can easily add them together without introducing multiplicative XP stacking
var experienceGainMultiplier = new AbilityValue(1f);
var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f);
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier));
crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier));
@@ -374,11 +377,14 @@ namespace Barotrauma
#endif
// apply money gains afterwards to prevent them from affecting XP gains
var moneyGainMission = new AbilityValueMission(1f, this);
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, moneyGainMission));
crewCharacters.ForEach(c => moneyGainMission.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier));
var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f);
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier));
crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier));
campaign.Money += (int)(reward * moneyGainMission.Value);
int totalReward = (int)(reward * missionMoneyGainMultiplier.Value);
campaign.Money += totalReward;
GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier);
foreach (Character character in crewCharacters)
{
@@ -534,4 +540,16 @@ namespace Barotrauma
cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2);
}
}
class AbilityMissionMoneyGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission
{
public AbilityMissionMoneyGainMultiplier(Mission mission, float moneyGainMultiplier)
{
Value = moneyGainMultiplier;
Mission = mission;
}
public float Value { get; set; }
public Mission Mission { get; set; }
}
}
@@ -96,9 +96,9 @@ namespace Barotrauma
public readonly bool RequireWreck;
/// <summary>
/// The mission can only be received when travelling from Pair.First to Pair.Second
/// The mission can only be received when travelling from a location of the first type to a location of the second type
/// </summary>
public readonly List<Pair<string, string>> AllowedConnectionTypes;
public readonly List<(string from, string to)> AllowedConnectionTypes;
/// <summary>
/// The mission can only be received in these location types
@@ -185,7 +185,14 @@ namespace Barotrauma
tags = element.GetAttributeStringArray("tags", new string[0], convertToLowerInvariant: true);
Name = TextManager.Get("MissionName." + TextIdentifier, true) ?? element.GetAttributeString("name", "");
Name = TextManager.Get("MissionName." + TextIdentifier, true);
if (Name == null)
{
#if DEBUG
DebugConsole.ThrowError($"Error in mission \"{Identifier}\" - could not find a name in localization files. Make sure the texts are present in the loca file or that the mission is set to share texts with another mission using the TextIdentifier attribute.");
#endif
Name = element.GetAttributeString("name", "");
}
Description = TextManager.Get("MissionDescription." + TextIdentifier, true) ?? element.GetAttributeString("description", "");
Reward = element.GetAttributeInt("reward", 1);
AllowRetry = element.GetAttributeBool("allowretry", false);
@@ -209,10 +216,20 @@ namespace Barotrauma
FailureMessage = element.GetAttributeString("failuremessage", "");
}
SonarLabel =
TextManager.Get("MissionSonarLabel." + TextIdentifier, true) ??
TextManager.Get("MissionSonarLabel." + element.GetAttributeString("sonarlabel", ""), true) ??
element.GetAttributeString("sonarlabel", "");
if (element.Attribute("sonarlabel") == null)
{
SonarLabel =
TextManager.Get("MissionSonarLabel." + TextIdentifier, true) ??
TextManager.Get("missionsonarlabel.target");
}
else
{
SonarLabel =
TextManager.Get("MissionSonarLabel." + element.GetAttributeString("sonarlabel", ""), true) ??
TextManager.Get(element.GetAttributeString("sonarlabel", ""), true) ??
element.GetAttributeString("sonarlabel", "");
}
SonarIconIdentifier = element.GetAttributeString("sonaricon", "");
MultiplayerOnly = element.GetAttributeBool("multiplayeronly", false);
@@ -224,7 +241,7 @@ namespace Barotrauma
Headers = new List<string>();
Messages = new List<string>();
AllowedConnectionTypes = new List<Pair<string, string>>();
AllowedConnectionTypes = new List<(string from, string to)>();
for (int i = 0; i < 100; i++)
{
@@ -260,9 +277,7 @@ namespace Barotrauma
}
else
{
AllowedConnectionTypes.Add(new Pair<string, string>(
subElement.GetAttributeString("from", ""),
subElement.GetAttributeString("to", "")));
AllowedConnectionTypes.Add((subElement.GetAttributeString("from", "").ToLowerInvariant(), subElement.GetAttributeString("to", "").ToLowerInvariant()));
}
break;
case "locationtypechange":
@@ -358,13 +373,15 @@ namespace Barotrauma
AllowedLocationTypes.Any(lt => lt.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase));
}
foreach (Pair<string, string> allowedConnectionType in AllowedConnectionTypes)
foreach ((string fromType, string toType) in AllowedConnectionTypes)
{
if (allowedConnectionType.First.Equals("any", StringComparison.OrdinalIgnoreCase) ||
allowedConnectionType.First.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase))
if (fromType.Equals("any", StringComparison.OrdinalIgnoreCase) ||
fromType.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase) ||
(fromType == "anyoutpost" && from.HasOutpost()))
{
if (allowedConnectionType.Second.Equals("any", StringComparison.OrdinalIgnoreCase) ||
allowedConnectionType.Second.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase))
if (toType.Equals("any", StringComparison.OrdinalIgnoreCase) ||
toType.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase) ||
(toType == "anyoutpost" && to.HasOutpost()))
{
return true;
}
@@ -345,12 +345,13 @@ namespace Barotrauma
protected override void UpdateMissionSpecific(float deltaTime)
{
int newState = State;
if (state >= 2) { return; }
float sqrSonarRange = MathUtils.Pow2(Sonar.DefaultSonarRange);
outsideOfSonarRange = Vector2.DistanceSquared(enemySub.WorldPosition, Submarine.MainSub.WorldPosition) > sqrSonarRange;
if (State < 2 && CheckWinState())
if (CheckWinState())
{
newState = 2;
State = 2;
}
else
{
@@ -366,7 +367,7 @@ namespace Barotrauma
}
if (!outsideOfSonarRange || patrolPositions.None())
{
newState = 1;
State = 1;
}
break;
case 1:
@@ -391,14 +392,13 @@ namespace Barotrauma
break;
}
}
State = newState;
}
private bool CheckWinState() => !IsClient && characters.All(m => DeadOrCaptured(m));
private bool DeadOrCaptured(Character character)
{
return character == null || character.Removed || character.IsDead || (character.LockHands && character.Submarine == Submarine.MainSub);
return character == null || character.Removed || character.Submarine == null || (character.LockHands && character.Submarine == Submarine.MainSub) || character.IsIncapacitated;
}
public override void End()
@@ -60,8 +60,16 @@ namespace Barotrauma
}
else
{
string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", "");
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", null);
if (itemIdentifier != null)
{
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
}
if (itemPrefab == null)
{
string itemTag = prefab.ConfigElement.GetAttributeString("itemtag", "");
itemPrefab = MapEntityPrefab.GetRandom(p => p.Tags.Contains(itemTag), Rand.RandSync.Unsynced) as ItemPrefab;
}
if (itemPrefab == null)
{
DebugConsole.ThrowError("Error in SalvageMission - couldn't find an item prefab with the identifier " + itemIdentifier);
@@ -150,8 +158,8 @@ namespace Barotrauma
if (item == null)
{
item = new Item(itemPrefab, position, null);
item.body.SetTransformIgnoreContacts(item.body.SimPosition, item.body.Rotation);
item.body.FarseerBody.BodyType = BodyType.Kinematic;
item.FindHull();
}
for (int i = 0; i < statusEffects.Count; i++)
@@ -192,7 +200,7 @@ namespace Barotrauma
}
if (validContainers.Any())
{
var selectedContainer = validContainers.GetRandom();
var selectedContainer = validContainers.GetRandom(Rand.RandSync.Unsynced);
if (selectedContainer.Combine(item, user: null))
{
#if SERVER
@@ -15,6 +15,7 @@ namespace Barotrauma
private readonly float scatter;
private readonly float offset;
private readonly float delayBetweenSpawns;
private Vector2? spawnPos;
@@ -25,7 +26,7 @@ namespace Barotrauma
private bool spawnPending;
private readonly int maxAmountPerLevel = int.MaxValue;
public readonly int MaxAmountPerLevel = int.MaxValue;
public List<Character> Monsters => monsters;
public Vector2? SpawnPos => spawnPos;
@@ -73,7 +74,7 @@ namespace Barotrauma
minAmount = prefab.ConfigElement.GetAttributeInt("minamount", defaultAmount);
maxAmount = Math.Max(prefab.ConfigElement.GetAttributeInt("maxamount", 1), minAmount);
maxAmountPerLevel = prefab.ConfigElement.GetAttributeInt("maxamountperlevel", int.MaxValue);
MaxAmountPerLevel = prefab.ConfigElement.GetAttributeInt("maxamountperlevel", int.MaxValue);
var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
if (string.IsNullOrWhiteSpace(spawnPosTypeStr) ||
@@ -92,6 +93,7 @@ namespace Barotrauma
offset = prefab.ConfigElement.GetAttributeFloat("offset", 0);
scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000);
delayBetweenSpawns = prefab.ConfigElement.GetAttributeFloat("delaybetweenspawns", 0.1f);
if (GameMain.NetworkMember != null)
{
@@ -326,7 +328,7 @@ namespace Barotrauma
}
else
{
dir = new Vector2(1, Rand.Range(-1, 1));
dir = new Vector2(1, Rand.Range(-1f, 1f));
}
Vector2 targetPos = spawnPos.Value + dir * offset;
var targetWaypoint = waypoints.OrderBy(wp => Vector2.DistanceSquared(wp.WorldPosition, targetPos)).FirstOrDefault();
@@ -365,9 +367,9 @@ namespace Barotrauma
if (spawnPos == null)
{
if (maxAmountPerLevel < int.MaxValue)
if (MaxAmountPerLevel < int.MaxValue)
{
if (Character.CharacterList.Count(c => c.SpeciesName == speciesName) >= maxAmountPerLevel)
if (Character.CharacterList.Count(c => c.SpeciesName == speciesName) >= MaxAmountPerLevel)
{
disallowed = true;
return;
@@ -473,6 +475,7 @@ namespace Barotrauma
{
scatterAmount = 0;
}
for (int i = 0; i < amount; i++)
{
string seed = Level.Loaded.Seed + i.ToString();
@@ -538,7 +541,14 @@ namespace Barotrauma
SwarmBehavior.CreateSwarm(monsters.Cast<AICharacter>());
DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI.CombatStrength))}.", Color.LightBlue, debugOnly: true);
}
}, Rand.Range(0f, amount / 2f));
if (GameMain.GameSession != null)
{
GameAnalyticsManager.AddDesignEvent(
$"MonsterSpawn:{GameMain.GameSession.GameMode?.Preset?.Identifier ?? "none"}:{Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"}:{SpawnPosType}:{speciesName}",
value: Timing.TotalTime - GameMain.GameSession.RoundStartTime);
}
}, delayBetweenSpawns * i);
}
}
@@ -45,6 +45,8 @@ namespace Barotrauma
requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null);
RequireBeaconStation = prefab.ConfigElement.GetAttributeBool("requirebeaconstation", false);
GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Start");
}
public void AddTarget(string tag, Entity target)
@@ -229,5 +231,11 @@ namespace Barotrauma
}
return false;
}
public override void Finished()
{
base.Finished();
GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Finished:{CurrentActionIndex}");
}
}
}
@@ -133,7 +133,7 @@ namespace Barotrauma.Extensions
return source.Count(predicate) > 1;
}
}
public static IEnumerable<T> ToEnumerable<T>(this T item)
{
yield return item;
@@ -196,5 +196,28 @@ namespace Barotrauma.Extensions
}
return -1;
}
/// <summary>
/// Same as FirstOrDefault but will always return null instead of default(T) when no element is found
/// </summary>
public static T? FirstOrNull<T>(this IEnumerable<T> source, Func<T, bool> predicate) where T : struct
{
if (source.FirstOrDefault(predicate) is var first && !first.Equals(default(T)))
{
return first;
}
return null;
}
public static T? FirstOrNull<T>(this IEnumerable<T> source) where T : struct
{
if (source.FirstOrDefault() is var first && !first.Equals(default(T)))
{
return first;
}
return null;
}
}
}
@@ -1,12 +1,11 @@
using System;
using Barotrauma.Steam;
using RestSharp;
using System;
using System.Net;
using System.Threading.Tasks;
namespace Barotrauma
{
public static partial class GameAnalyticsManager
static partial class GameAnalyticsManager
{
public enum Consent
{
@@ -149,6 +148,13 @@ namespace Barotrauma
SetConsent(Consent.Error);
}
if (!SteamManager.IsInitialized)
{
DebugConsole.AddWarning("Error in GameAnalyticsManager.GetConsent: Could not get a Steam authentication ticket (not connected to Steam).");
SetConsent(Consent.Error);
return;
}
string authTicketStr;
try
{
@@ -183,7 +189,7 @@ namespace Barotrauma
return;
}
var response = ((Task<IRestResponse>)t).Result;
if (!t.TryGetResult(out IRestResponse response)) { return; }
if (!CheckResponse(response))
{
SetConsent(Consent.Error);
@@ -1,5 +1,6 @@
#nullable enable
using Barotrauma.IO;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -9,7 +10,7 @@ using System.Text;
namespace Barotrauma
{
public static partial class GameAnalyticsManager
static partial class GameAnalyticsManager
{
public enum ErrorSeverity
{
@@ -29,6 +30,61 @@ namespace Barotrauma
Fail = 3
}
public enum CustomDimensions01
{
Vanilla,
Modded
}
public enum CustomDimensions02
{
None,
Difficulty0to10,
Difficulty10to20,
Difficulty20to30,
Difficulty30to40,
Difficulty40to50,
Difficulty50to60,
Difficulty60to70,
Difficulty70to80,
Difficulty80to90,
Difficulty90to100,
}
public enum ResourceCurrency
{
Money
}
public enum ResourceFlowType
{
Undefined = 0,
Source = 1,
Sink = 2
}
public enum MoneySource
{
Unknown,
MissionReward,
Store,
Event,
Ability,
Cheat
}
public enum MoneySink
{
Unknown,
Store,
Service,
Crew,
SubmarineUpgrade,
SubmarineWeapon,
SubmarinePurchase,
SubmarineSwitch
}
private readonly static HashSet<string> sentEventIdentifiers = new HashSet<string>();
private class Implementation : IDisposable
@@ -69,17 +125,41 @@ namespace Barotrauma
internal void AddProgressionEvent(ProgressionStatus status, string progression01, string progression02, string progression03)
=> addProgressionEvent03(status, progression01, progression02, progression03);
private readonly Action<ResourceFlowType, string, float, string, string> addResourceEvent;
internal void AddResourceEvent(ResourceFlowType flowType, string currency, float amount, string itemType, string itemId)
=> addResourceEvent(flowType, currency, amount, itemType, itemId);
private readonly Action<string> setCustomDimension01;
internal void SetCustomDimension01(string dimension01)
=> setCustomDimension01(dimension01);
private readonly Action<string[]> configureAvailableCustomDimensions01;
internal void ConfigureAvailableCustomDimensions01(params string[] customDimensions)
=> configureAvailableCustomDimensions01(customDimensions);
internal void ConfigureAvailableCustomDimensions01(params CustomDimensions01[] customDimensions)
=> configureAvailableCustomDimensions01(customDimensions.Select(d => d.ToString()).ToArray());
private readonly Action<string> setCustomDimension02;
internal void SetCustomDimension02(string dimension02)
=> setCustomDimension02(dimension02);
private readonly Action<string[]> configureAvailableCustomDimensions02;
internal void ConfigureAvailableCustomDimensions02(params CustomDimensions02[] customDimensions)
=> configureAvailableCustomDimensions02(customDimensions.Select(d => d.ToString()).ToArray());
private readonly Action<string[]> configureAvailableResourceCurrencies;
internal void ConfigureAvailableResourceCurrencies(params ResourceCurrency[] customDimensions)
=> configureAvailableResourceCurrencies(customDimensions.Select(d => d.ToString()).ToArray());
private readonly Action<string[]> configureAvailableResourceItemTypes;
internal void ConfigureAvailableResourceItemTypes(params string[] resourceItemTypes)
=> configureAvailableResourceItemTypes(resourceItemTypes);
private readonly Action<bool> setEnabledInfoLog;
internal void SetEnabledInfoLog(bool enabled)
=> setEnabledInfoLog(enabled);
private readonly Action<bool> setEnabledVerboseLog;
internal void SetEnabledVerboseLog(bool enabled)
=> setEnabledVerboseLog(enabled);
#endregion
#region Data required to fetch methods via reflection
@@ -94,6 +174,7 @@ namespace Barotrauma
private readonly object?[] args2 = new object?[2];
private readonly object?[] args3 = new object?[3];
private readonly object?[] args4 = new object?[4];
private readonly object?[] args5 = new object?[5];
private Action Call(MethodInfo methodInfo)
=> () => methodInfo?.Invoke(null, null);
@@ -131,6 +212,17 @@ namespace Barotrauma
args4[3] = arg4;
methodInfo.Invoke(null, args4);
};
private Action<T1, T2, T3, T4, T5> Call<T1, T2, T3, T4, T5>(MethodInfo methodInfo)
=> (T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) =>
{
args5[0] = arg1;
args5[1] = arg2;
args5[2] = arg3;
args5[3] = arg4;
args5[4] = arg5;
methodInfo.Invoke(null, args5);
};
#endregion
private AssemblyLoadContext? loadContext;
@@ -165,9 +257,15 @@ namespace Barotrauma
var mainClass = getType(MainClass);
var errorSeverityEnumType = getType($"{EnumPrefix}{nameof(ErrorSeverity)}");
var progressionStatusEnumType = getType($"{EnumPrefix}{nameof(ProgressionStatus)}");
var resourceFlowTypeEnumType = getType($"{EnumPrefix}{nameof(ResourceFlowType)}");
MethodInfo getMethod(string name, Type[] types)
{
foreach (var me in mainClass.GetMethods())
{
var aksjdnakjsdnf = me;
}
return mainClass?.GetMethod(name, BindingFlags.Public | BindingFlags.Static, binder: null, types: types, modifiers: null)
?? throw new Exception($"Could not find method \"{name}\" with types {string.Join(',', types.Select(t => t.Name))}");
}
@@ -190,12 +288,26 @@ namespace Barotrauma
new Type[] { progressionStatusEnumType, typeof(string), typeof(string) }));
addProgressionEvent03 = Call<ProgressionStatus, string, string, string>(getMethod(nameof(AddProgressionEvent),
new Type[] { progressionStatusEnumType, typeof(string), typeof(string), typeof(string) }));
setCustomDimension01 = Call<string>(getMethod(nameof(SetCustomDimension01),
new Type[] { typeof(string) }));
configureAvailableCustomDimensions01 = Call<string[]>(getMethod(nameof(ConfigureAvailableCustomDimensions01),
new Type[] { typeof(string[]) }));
setCustomDimension02 = Call<string>(getMethod(nameof(SetCustomDimension02),
new Type[] { typeof(string) }));
configureAvailableCustomDimensions02 = Call<string[]>(getMethod(nameof(ConfigureAvailableCustomDimensions02),
new Type[] { typeof(string[]) }));
configureAvailableResourceCurrencies = Call<string[]>(getMethod(nameof(ConfigureAvailableResourceCurrencies),
new Type[] { typeof(string[]) }));
configureAvailableResourceItemTypes = Call<string[]>(getMethod(nameof(ConfigureAvailableResourceItemTypes),
new Type[] { typeof(string[]) }));
addResourceEvent = Call<ResourceFlowType, string, float, string, string>(getMethod(nameof(AddResourceEvent),
new Type[] { resourceFlowTypeEnumType, typeof(string), typeof(float), typeof(string), typeof(string) }));
setEnabledInfoLog = Call<bool>(getMethod(nameof(SetEnabledInfoLog),
new Type[] { typeof(bool) }));
setEnabledVerboseLog = Call<bool>(getMethod(nameof(SetEnabledVerboseLog),
new Type[] { typeof(bool) }));
onQuit = Call(getMethod("OnQuit", Array.Empty<Type>()));
}
@@ -204,8 +316,7 @@ namespace Barotrauma
private void OnQuit()
{
try
{
{
if (assembly != null) { onQuit?.Invoke(); }
}
catch (Exception e)
@@ -298,10 +409,40 @@ namespace Barotrauma
loadedImplementation?.AddProgressionEvent(progressionStatus, progression01, progression02, progression03);
}
public static void SetCustomDimension01(string dimension)
public static void SetCustomDimension01(CustomDimensions01 dimension)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.SetCustomDimension01(dimension);
loadedImplementation?.SetCustomDimension01(dimension.ToString());
}
public static void SetCurrentLevel(LevelData levelData)
{
if (!SendUserStatistics) { return; }
CustomDimensions02 customDimension = CustomDimensions02.None;
if (levelData != null)
{
float levelDifficulty = levelData.Difficulty;
customDimension = (CustomDimensions02)MathHelper.Clamp((int)(levelDifficulty / 10) + 1, 0, Enum.GetValues(typeof(CustomDimensions02)).Length - 1);
}
loadedImplementation?.SetCustomDimension02(customDimension.ToString());
}
public static void AddMoneyGainedEvent(int amount, MoneySource moneySource, string eventId)
{
AddResourceEvent(ResourceFlowType.Source, ResourceCurrency.Money, amount, moneySource.ToString(), eventId);
}
public static void AddMoneySpentEvent(int amount, MoneySink moneySink, string eventId)
{
AddResourceEvent(ResourceFlowType.Sink, ResourceCurrency.Money, amount, moneySink.ToString(), eventId);
}
private static void AddResourceEvent(ResourceFlowType flowType, ResourceCurrency currency, float amount, string eventType, string eventId)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.AddResourceEvent(flowType, currency.ToString(), amount, eventType, eventId);
}
private static void Init()
@@ -321,6 +462,7 @@ namespace Barotrauma
try
{
loadedImplementation?.SetEnabledInfoLog(true);
loadedImplementation?.SetEnabledVerboseLog(true);
}
catch (Exception e)
{
@@ -359,7 +501,11 @@ namespace Barotrauma
+ exeName + ":"
+ AssemblyInfo.GitRevision + ":"
+ buildConfiguration);
loadedImplementation?.ConfigureAvailableCustomDimensions01("singleplayer", "multiplayer", "editor");
loadedImplementation?.ConfigureAvailableCustomDimensions01(Enum.GetValues(typeof(CustomDimensions01)).Cast<CustomDimensions01>().ToArray());
loadedImplementation?.ConfigureAvailableCustomDimensions02(Enum.GetValues(typeof(CustomDimensions02)).Cast<CustomDimensions02>().ToArray());
loadedImplementation?.ConfigureAvailableResourceCurrencies(Enum.GetValues(typeof(ResourceCurrency)).Cast<ResourceCurrency>().ToArray());
loadedImplementation?.ConfigureAvailableResourceItemTypes(
Enum.GetValues(typeof(MoneySink)).Cast<MoneySink>().Select(s => s.ToString()).Union(Enum.GetValues(typeof(MoneySource)).Cast<MoneySource>().Select(s => s.ToString())).ToArray());
InitKeys();
@@ -367,7 +513,6 @@ namespace Barotrauma
+ GameMain.Version.ToString()
+ exeName + ":"
+ ((exeHash?.ShortHash == null) ? "Unknown" : exeHash.ShortHash) + ":"
+ AssemblyInfo.GitBranch + ":"
+ AssemblyInfo.GitRevision + ":"
+ buildConfiguration);
}
@@ -378,19 +523,25 @@ namespace Barotrauma
return;
}
var allPackages = GameMain.Config?.AllEnabledPackages.ToList();
if (allPackages?.Count > 0)
if (GameMain.Config != null)
{
StringBuilder sb = new StringBuilder("ContentPackage: ");
int i = 0;
foreach (ContentPackage cp in allPackages)
var allPackages = GameMain.Config.AllEnabledPackages.ToList();
if (allPackages?.Count > 0)
{
string trimmedName = cp.Name.Replace(":", "").Replace(" ", "");
sb.Append(trimmedName.Substring(0, Math.Min(32, trimmedName.Length)));
if (i < allPackages.Count - 1) { sb.Append(" "); }
List<string> packageNames = new List<string>();
foreach (ContentPackage cp in allPackages)
{
string sanitizedName = cp.Name.Replace(":", "").Replace(" ", "");
sanitizedName = sanitizedName.Substring(0, Math.Min(32, sanitizedName.Length));
packageNames.Add(sanitizedName);
loadedImplementation?.AddDesignEvent("ContentPackage:" + sanitizedName);
}
packageNames.Sort();
loadedImplementation?.AddDesignEvent("AllContentPackages:" + string.Join(" ", packageNames));
}
loadedImplementation?.AddDesignEvent(sb.ToString());
loadedImplementation?.AddDesignEvent("Language:" + GameMain.Config.Language);
}
}
static partial void InitKeys();
@@ -220,10 +220,10 @@ namespace Barotrauma
private static readonly (int quality, float commonness)[] qualityCommonnesses = new (int quality, float commonness)[Quality.MaxQuality + 1]
{
(0, 0.85f),
(1, 0.125f),
(2, 0.0225f),
(3, 0.0025f),
(0, 1.0f),
(1, 0.0f),
(2, 0.0f),
(3, 0.0f),
};
private static List<Item> SpawnItem(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer, float difficultyModifier)
@@ -16,32 +16,98 @@ namespace Barotrauma
{
public ItemPrefab ItemPrefab { get; }
public int Quantity { get; set; }
public bool? IsStoreComponentEnabled { get; set; }
public PurchasedItem(ItemPrefab itemPrefab, int quantity)
{
ItemPrefab = itemPrefab;
Quantity = quantity;
IsStoreComponentEnabled = null;
}
}
class SoldItem
{
public ItemPrefab ItemPrefab { get; }
public ushort ID { get; }
public ushort ID { get; private set; }
public bool Removed { get; set; }
public byte SellerID { get; }
public SellOrigin Origin { get; }
public SoldItem(ItemPrefab itemPrefab, ushort id, bool removed, byte sellerId)
public enum SellOrigin
{
Character,
Submarine
}
public SoldItem(ItemPrefab itemPrefab, ushort id, bool removed, byte sellerId, SellOrigin origin)
{
ItemPrefab = itemPrefab;
ID = id;
Removed = removed;
SellerID = sellerId;
Origin = origin;
}
public void SetItemId(ushort id)
{
if (ID != Entity.NullEntityID)
{
DebugConsole.ShowError("Error setting SoldItem.ID: ID has already been set and should not be changed.");
return;
}
ID = id;
}
}
partial class CargoManager
{
private class SoldEntity
{
public enum SellStatus
{
/// <summary>
/// Entity sold in SP. Or, entity sold by client and confirmed by server in MP.
/// </summary>
Confirmed,
/// <summary>
/// Entity sold by client in MP. Client has received at least one update from server after selling, but this entity wasn't yet confirmed.
/// </summary>
Unconfirmed,
/// <summary>
/// Entity sold by client in MP. Client hasn't yet received an update from server after selling.
/// </summary>
Local
}
public Item Item { get; private set; }
public ItemPrefab ItemPrefab { get; }
public SellStatus Status { get; set; }
public SoldEntity(Item item, SellStatus status)
{
Item = item;
ItemPrefab = item?.Prefab;
Status = status;
}
public SoldEntity(ItemPrefab itemPrefab, SellStatus status)
{
ItemPrefab = itemPrefab;
Status = status;
}
public void SetItem(Item item)
{
if (Item != null)
{
DebugConsole.ShowError($"Trying to set SoldEntity.Item, but it's already set!\n{Environment.StackTrace.CleanupStackTrace()}");
return;
}
Item = item;
}
}
public const int MaxQuantity = 100;
public List<PurchasedItem> ItemsInBuyCrate { get; } = new List<PurchasedItem>();
@@ -92,7 +158,7 @@ namespace Barotrauma
public void ModifyItemQuantityInBuyCrate(ItemPrefab itemPrefab, int changeInQuantity)
{
PurchasedItem itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab);
var itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab);
if (itemInCrate != null)
{
itemInCrate.Quantity += changeInQuantity;
@@ -109,6 +175,25 @@ namespace Barotrauma
OnItemsInBuyCrateChanged?.Invoke();
}
public void ModifyItemQuantityInSubSellCrate(ItemPrefab itemPrefab, int changeInQuantity)
{
var itemInCrate = ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab);
if (itemInCrate != null)
{
itemInCrate.Quantity += changeInQuantity;
if (itemInCrate.Quantity < 1)
{
ItemsInSellFromSubCrate.Remove(itemInCrate);
}
}
else if (changeInQuantity > 0)
{
itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity);
ItemsInSellFromSubCrate.Add(itemInCrate);
}
OnItemsInSellFromSubCrateChanged?.Invoke();
}
public void PurchaseItems(List<PurchasedItem> itemsToPurchase, bool removeFromCrate)
{
// Check all the prices before starting the transaction
@@ -132,6 +217,7 @@ namespace Barotrauma
// Exchange money
var itemValue = item.Quantity * buyValues[item.ItemPrefab];
campaign.Money -= itemValue;
GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier);
Location.StoreCurrentBalance += itemValue;
if (removeFromCrate)
@@ -184,6 +270,82 @@ namespace Barotrauma
OnPurchasedItemsChanged?.Invoke();
}
private Dictionary<ItemPrefab, int> UndeterminedSoldEntities { get; } = new Dictionary<ItemPrefab, int>();
public IEnumerable<Item> GetSellableItemsFromSub()
{
if (Submarine.MainSub == null) { return new List<Item>(); }
var confirmedSoldEntities = Enumerable.Empty<SoldEntity>();
UndeterminedSoldEntities.Clear();
#if CLIENT
confirmedSoldEntities = GetConfirmedSoldEntities();
foreach (var soldEntity in SoldEntities)
{
if (soldEntity.Item != null) { continue; }
if (UndeterminedSoldEntities.TryGetValue(soldEntity.ItemPrefab, out int count))
{
UndeterminedSoldEntities[soldEntity.ItemPrefab] = count + 1;
}
else
{
UndeterminedSoldEntities.Add(soldEntity.ItemPrefab, 1);
}
}
#endif
return Submarine.MainSub.GetItems(true).FindAll(item =>
{
if (!IsItemSellable(item, confirmedSoldEntities)) { return false; }
if (item.GetRootInventoryOwner() is Character) { return false; }
if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; }
if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; }
if (!ItemAndAllContainersInteractable(item)) { return false; }
if (item.GetRootContainer() is Item rootContainer && rootContainer.HasTag("donttakeitems")) { return false; }
return true;
}).Distinct();
static bool ItemAndAllContainersInteractable(Item item)
{
do
{
if (!item.IsPlayerTeamInteractable) { return false; }
item = item.Container;
} while (item != null);
return true;
}
}
private bool IsItemSellable(Item item, IEnumerable<SoldEntity> confirmedItems)
{
if (item.Removed) { return false; }
if (!item.Prefab.CanBeSold) { return false; }
if (item.SpawnedInCurrentOutpost) { return false; }
if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; }
if (confirmedItems.Any(ci => ci.Item == item)) { return false; }
if (UndeterminedSoldEntities.TryGetValue(item.Prefab, out int count))
{
int newCount = count - 1;
if (newCount > 0)
{
UndeterminedSoldEntities[item.Prefab] = newCount;
}
else
{
UndeterminedSoldEntities.Remove(item.Prefab);
}
return false;
}
if (item.OwnInventory?.Container is ItemContainer itemContainer)
{
var containedItems = item.ContainedItems;
if (containedItems.None()) { return true; }
// Allow selling the item if contained items are unsellable and set to be removed on deconstruct
if (itemContainer.RemoveContainedItemsOnDeconstruct && containedItems.All(it => !it.Prefab.CanBeSold)) { return true; }
// Otherwise there must be no contained items or the contained items must be confirmed as sold
if (!containedItems.All(it => confirmedItems.Any(ci => ci.Item == it))) { return false; }
}
return true;
}
public static void CreateItems(List<PurchasedItem> itemsToSpawn, Submarine sub)
{
if (itemsToSpawn.Count == 0) { return; }
@@ -265,11 +427,13 @@ namespace Barotrauma
}
var item = new Item(pi.ItemPrefab, position, wp.Submarine);
itemContainer?.Inventory.TryPutItem(item, null);
itemSpawned(item);
itemContainer?.Inventory.TryPutItem(item, null);
itemSpawned(item);
#if SERVER
Entity.Spawner?.CreateNetworkEvent(item, false);
#endif
(itemContainer?.Item ?? item).CampaignInteractionType = CampaignMode.InteractionType.Cargo;
static void itemSpawned(Item item)
{
Submarine sub = item.Submarine ?? item.GetRootContainer()?.Submarine;
@@ -291,7 +455,7 @@ namespace Barotrauma
float floorPos = hull.Rect.Y - hull.Rect.Height;
Vector2 position = new Vector2(
hull.Rect.Width > 40 ? Rand.Range(hull.Rect.X + 20, hull.Rect.Right - 20) : hull.Rect.Center.X,
hull.Rect.Width > 40 ? Rand.Range(hull.Rect.X + 20f, hull.Rect.Right - 20f) : hull.Rect.Center.X,
floorPos);
//check where the actual floor structure is in case the bottom of the hull extends below it
@@ -37,7 +37,6 @@ namespace Barotrauma
{
IsSinglePlayer = isSinglePlayer;
conversationTimer = 5.0f;
InitProjectSpecific();
}
@@ -47,7 +46,9 @@ namespace Barotrauma
{
if (order.TargetEntity == null)
{
DebugConsole.ThrowError("Attempted to add an order with no target entity to CrewManager!\n" + Environment.StackTrace.CleanupStackTrace());
string message = $"Attempted to add a \"{order.Name}\" order with no target entity to CrewManager!\n{Environment.StackTrace.CleanupStackTrace()}";
DebugConsole.AddWarning(message);
GameAnalyticsManager.AddErrorEventOnce("CrewManager.AddOrder:OrderTargetEntityNull", GameAnalyticsManager.ErrorSeverity.Error, message);
return false;
}
@@ -98,10 +99,10 @@ namespace Barotrauma
foreach (XElement characterElement in element.Elements())
{
if (!characterElement.Name.ToString().Equals("character", StringComparison.OrdinalIgnoreCase)) { continue; }
CharacterInfo characterInfo = new CharacterInfo(characterElement);
#if CLIENT
if (characterElement.GetAttributeBool("lastcontrolled", false)) { characterInfo.LastControlled = true; }
characterInfo.CrewListIndex = characterElement.GetAttributeInt("crewlistindex", -1);
#endif
characterInfos.Add(characterInfo);
foreach (XElement subElement in characterElement.Elements())
@@ -131,7 +132,7 @@ namespace Barotrauma
characterInfos.Remove(characterInfo);
}
public void AddCharacter(Character character)
public void AddCharacter(Character character, bool sortCrewList = true)
{
if (character.Removed)
{
@@ -153,7 +154,11 @@ namespace Barotrauma
characterInfos.Add(character.Info);
}
#if CLIENT
AddCharacterToCrewList(character);
var characterComponent = AddCharacterToCrewList(character);
if (sortCrewList)
{
SortCrewList();
}
if (character.CurrentOrders != null)
{
foreach (var order in character.CurrentOrders)
@@ -185,6 +190,10 @@ namespace Barotrauma
public void InitRound()
{
#if CLIENT
GUIContextMenu.CurrentContextMenu = null;
#endif
characters.Clear();
List<WayPoint> spawnWaypoints = null;
@@ -248,12 +257,16 @@ namespace Barotrauma
}
}
AddCharacter(character);
AddCharacter(character, sortCrewList: false);
#if CLIENT
if (IsSinglePlayer && (Character.Controlled == null || character.Info.LastControlled)) { Character.Controlled = character; }
#endif
}
#if CLIENT
if (IsSinglePlayer) { SortCrewList(); }
#endif
//longer delay in multiplayer to prevent the server from triggering NPC conversations while the players are still loading the round
conversationTimer = IsSinglePlayer ? Rand.Range(5.0f, 10.0f) : Rand.Range(45.0f, 60.0f);
}
@@ -435,19 +448,21 @@ namespace Barotrauma
filteredCharacters = filteredCharacters.Union(extraCharacters);
}
return filteredCharacters
// 1. Prioritize those who are on the same submarine than the controlled character
// Prioritize those who are on the same submarine as the controlled character
.OrderByDescending(c => Character.Controlled == null || c.Submarine == Character.Controlled.Submarine)
// 2. Prioritize those who have been given the same maintenance or operate order as now issued
.ThenByDescending(c => c.CurrentOrders.Any(o =>
o.Order != null && o.Order.Identifier == order.Identifier &&
(order.Category == OrderCategory.Maintenance || order.Category == OrderCategory.Operate)))
// 3. Prioritize those with the appropriate job for the order
// Prioritize those who are already ordered to operate the device
.ThenByDescending(c => order.Category == OrderCategory.Operate && c.CurrentOrders.Any(o => o.Order != null && o.Order.Identifier == order.Identifier && o.Order.TargetEntity == order.TargetEntity))
// Prioritize those with the appropriate job for the order
.ThenByDescending(c => order.HasAppropriateJob(c))
// 4. Prioritize bots over player controlled characters
// Prioritize those who don't yet have the same order (which allows quick-assigning the order to different characters)
.ThenByDescending(c => c.CurrentOrders.None(o => o.Order != null && o.Order.Identifier == order.Identifier))
// Prioritize those with the preferred job for the order
.ThenByDescending(c => order.HasPreferredJob(c))
// Prioritize bots over player-controlled characters
.ThenByDescending(c => c.IsBot)
// 5. Use the priority value of the current objective
// Prioritize those with a lower current objective priority
.ThenBy(c => c.AIController is HumanAIController humanAI ? humanAI.ObjectiveManager.CurrentObjective?.Priority : 0)
// 6. Prioritize those with the best skill for the order
// Prioritize those with a higher order skill level
.ThenByDescending(c => c.GetSkillLevel(order.AppropriateSkill));
}
@@ -14,7 +14,7 @@ namespace Barotrauma
public Faction(CampaignMetadata metadata, FactionPrefab prefab)
{
Prefab = prefab;
Reputation = new Reputation(metadata, $"faction.{prefab.Identifier}", prefab.MinReputation, prefab.MaxReputation, prefab.InitialReputation);
Reputation = new Reputation(metadata, this, prefab.MinReputation, prefab.MaxReputation, prefab.InitialReputation);
}
}
@@ -35,9 +35,22 @@ namespace Barotrauma
private set
{
if (MathUtils.NearlyEqual(Value, value)) { return; }
float prevValue = Value;
Metadata.SetValue(metaDataIdentifier, Math.Clamp(value, MinReputation, MaxReputation));
OnReputationValueChanged?.Invoke();
OnAnyReputationValueChanged?.Invoke();
#if CLIENT
int increase = (int)Value - (int)prevValue;
if (increase != 0 && Character.Controlled != null)
{
Character.Controlled.AddMessage(
TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.Name ?? Faction.Prefab.Name),
increase > 0 ? GUI.Style.Green : GUI.Style.Red,
playSound: true, Identifier, increase, lifetime: 5.0f);
}
#endif
}
}
@@ -63,15 +76,32 @@ namespace Barotrauma
public Action OnReputationValueChanged;
public static Action OnAnyReputationValueChanged;
public Reputation(CampaignMetadata metadata, string identifier, int minReputation, int maxReputation, int initialReputation)
public readonly Faction Faction;
public readonly Location Location;
public Reputation(CampaignMetadata metadata, Location location, string identifier, int minReputation, int maxReputation, int initialReputation)
: this(metadata, null, location, identifier, minReputation, maxReputation, initialReputation)
{
}
public Reputation(CampaignMetadata metadata, Faction faction, int minReputation, int maxReputation, int initialReputation)
: this(metadata, faction, null, $"faction.{faction.Prefab.Identifier}", minReputation, maxReputation, initialReputation)
{
}
private Reputation(CampaignMetadata metadata, Faction faction, Location location, string identifier, int minReputation, int maxReputation, int initialReputation)
{
System.Diagnostics.Debug.Assert(metadata != null);
System.Diagnostics.Debug.Assert(faction != null || location != null);
Metadata = metadata;
Identifier = identifier.ToLowerInvariant();
metaDataIdentifier = $"reputation.{Identifier}";
MinReputation = minReputation;
MaxReputation = maxReputation;
InitialReputation = initialReputation;
Faction = faction;
Location = location;
}
public string GetReputationName()
@@ -78,10 +78,19 @@ namespace Barotrauma
//there can be no events before this time has passed during the 1st campaign round
const float FirstRoundEventDelay = 0.0f;
public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Repair, Upgrade, PurchaseSub }
public double TotalPlayTime;
public int TotalPassedLevels;
public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Repair, Upgrade, PurchaseSub, MedicalClinic, Cargo }
public static bool BlocksInteraction(InteractionType interactionType)
{
return interactionType != InteractionType.None && interactionType != InteractionType.Cargo;
}
public readonly CargoManager CargoManager;
public UpgradeManager UpgradeManager;
public MedicalClinic MedicalClinic;
public List<Faction> Factions;
@@ -91,7 +100,7 @@ namespace Barotrauma
public CampaignSettings Settings;
private List<Mission> extraMissions = new List<Mission>();
private readonly List<Mission> extraMissions = new List<Mission>();
public enum TransitionType
{
@@ -176,6 +185,7 @@ namespace Barotrauma
{
Money = InitialMoney;
CargoManager = new CargoManager(this);
MedicalClinic = new MedicalClinic(this);
}
/// <summary>
@@ -688,11 +698,13 @@ namespace Barotrauma
GameAnalyticsManager.AddProgressionEvent(
GameAnalyticsManager.ProgressionStatus.Complete,
Name ?? "none");
Preset?.Identifier ?? "none");
string eventId = "FinishCampaign:";
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0));
GameAnalyticsManager.AddDesignEvent(eventId + "Money", Money);
GameAnalyticsManager.AddDesignEvent(eventId + "Money", Money);
GameAnalyticsManager.AddDesignEvent(eventId + "Playtime", TotalPlayTime);
GameAnalyticsManager.AddDesignEvent(eventId + "PassedLevels", TotalPassedLevels);
}
protected virtual void EndCampaignProjSpecific() { }
@@ -705,12 +717,14 @@ namespace Barotrauma
location.RemoveHireableCharacter(characterInfo);
CrewManager.AddCharacterInfo(characterInfo);
Money -= characterInfo.Salary;
GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier ?? "unknown");
return true;
}
private void NPCInteract(Character npc, Character interactor)
{
if (!npc.AllowCustomInteract) { return; }
GameAnalyticsManager.AddDesignEvent("CampaignInteraction:" + Preset.Identifier + ":" + npc.CampaignInteractionType);
NPCInteractProjSpecific(npc, interactor);
string coroutineName = "DoCharacterWait." + (npc?.ID ?? Entity.NullEntityID);
if (!CoroutineManager.IsCoroutineRunning(coroutineName))
@@ -874,6 +888,19 @@ namespace Barotrauma
}
public abstract void Save(XElement element);
protected void LoadStats(XElement element)
{
TotalPlayTime = element.GetAttributeDouble(nameof(TotalPlayTime).ToLowerInvariant(), 0);
TotalPassedLevels = element.GetAttributeInt(nameof(TotalPassedLevels).ToLowerInvariant(), 0);
}
protected XElement SaveStats()
{
return new XElement("stats",
new XAttribute(nameof(TotalPlayTime).ToLowerInvariant(), TotalPlayTime),
new XAttribute(nameof(TotalPassedLevels).ToLowerInvariant(), TotalPassedLevels));
}
public void LogState()
{
@@ -126,6 +126,10 @@ namespace Barotrauma
{
case "campaignsettings":
Settings = new CampaignSettings(subElement);
#if CLIENT
GameMain.NetworkMember.ServerSettings.MaxMissionCount = Settings.MaxMissionCount;
GameMain.NetworkMember.ServerSettings.RadiationEnabled = Settings.RadiationEnabled;
#endif
break;
case "map":
if (map == null)
@@ -159,6 +163,9 @@ namespace Barotrauma
case "pets":
petsElement = subElement;
break;
case "stats":
LoadStats(subElement);
break;
#if SERVER
case "savedexperiencepoints":
foreach (XElement savedExp in subElement.Elements())
@@ -192,6 +199,37 @@ namespace Barotrauma
}
#endif
}
public static List<SubmarineInfo> GetCampaignSubs()
{
bool isSubmarineVisible(SubmarineInfo s)
=> !GameMain.NetworkMember.ServerSettings.HiddenSubs.Any(h
=> s.Name.Equals(h, StringComparison.OrdinalIgnoreCase));
List<SubmarineInfo> availableSubs =
SubmarineInfo.SavedSubmarines
.Where(s =>
s.IsCampaignCompatible
&& isSubmarineVisible(s))
.ToList();
if (!availableSubs.Any())
{
//None of the available subs were marked as campaign-compatible, just include all visible subs
availableSubs.AddRange(
SubmarineInfo.SavedSubmarines
.Where(isSubmarineVisible));
}
if (!availableSubs.Any())
{
//No subs are visible at all! Just make the selected one available
availableSubs.Add(GameMain.NetLobbyScreen.SelectedSub);
}
return availableSubs;
}
}
}
@@ -22,6 +22,8 @@ namespace Barotrauma
public double RoundStartTime;
public double TimeSpentCleaning, TimeSpentPainting;
private readonly List<Mission> missions = new List<Mission>();
public IEnumerable<Mission> Missions { get { return missions; } }
@@ -276,6 +278,7 @@ namespace Barotrauma
}
Campaign.Money -= cost;
GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name);
((CampaignMode)GameMode).PendingSubmarineSwitch = newSubmarine;
return newSubmarine;
@@ -288,6 +291,7 @@ namespace Barotrauma
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
{
Campaign.Money -= newSubmarine.Price;
GameAnalyticsManager.AddMoneySpentEvent(newSubmarine.Price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name);
OwnedSubmarines.Add(newSubmarine);
}
}
@@ -409,18 +413,52 @@ namespace Barotrauma
GameAnalyticsManager.AddProgressionEvent(
GameAnalyticsManager.ProgressionStatus.Start,
GameMode?.Name ?? "none");
GameMode?.Preset?.Identifier ?? "none");
string eventId = "StartRound:GameMode:" + (GameMode?.Name ?? "none") + ":";
string eventId = "StartRound:" + (GameMode?.Preset?.Identifier ?? "none") + ":";
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0));
foreach (Mission mission in missions)
{
GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier);
}
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none"));
if (Level.Loaded != null)
{
string levelId = Level.Loaded.Type == LevelData.LevelType.Outpost ?
Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier :
Level.Loaded.GenerationParams?.Identifier;
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + Level.Loaded.Type.ToString() + ":" + (levelId ?? "null"));
}
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"));
#if CLIENT
if (GameMode is TutorialMode tutorialMode)
{
GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier);
if (GameMain.IsFirstLaunch)
{
GameAnalyticsManager.AddDesignEvent("FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier);
}
}
GameAnalyticsManager.AddDesignEvent($"{eventId}HintManager:{(HintManager.Enabled ? "Enabled" : "Disabled")}");
#endif
if (GameMode is CampaignMode campaignMode)
{
if (campaignMode.Map?.Radiation != null && campaignMode.Map.Radiation.Enabled)
{
GameAnalyticsManager.AddDesignEvent(eventId + "Radiation:Enabled");
}
else
{
GameAnalyticsManager.AddDesignEvent(eventId + "Radiation:Disabled");
}
bool firstTimeInBiome = Map != null && !Map.Connections.Any(c => c.Passed && c.Biome == LevelData.Biome);
if (firstTimeInBiome)
{
GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none") + "Discovered:Playtime", campaignMode.TotalPlayTime);
GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none") + "Discovered:PassedLevels", campaignMode.TotalPassedLevels);
}
}
#if CLIENT
if (GameMode is CampaignMode) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); }
@@ -457,6 +495,8 @@ namespace Barotrauma
}
}
ReadyCheck.ReadyCheckCooldown = DateTime.MinValue;
GUI.PreventPauseMenuToggle = false;
HintManager.OnRoundStarted();
@@ -752,38 +792,29 @@ namespace Barotrauma
GameMode?.End(transitionType);
EventManager?.EndRound();
StatusEffect.StopAll();
missions.Clear();
IsRunning = false;
bool success = false;
#if CLIENT
success = CrewManager.GetCharacters().Any(c => !c.IsDead);
bool success = CrewManager.GetCharacters().Any(c => !c.IsDead);
#else
success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
#endif
double roundDuration = Timing.TotalTime - RoundStartTime;
GameAnalyticsManager.AddProgressionEvent(
success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail,
GameMode?.Name ?? "none",
roundDuration);
string eventId = "EndRound:GameMode:" + (GameMode?.Name ?? "none") + ":";
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none"), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), roundDuration);
foreach (Mission mission in missions)
{
GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), roundDuration);
}
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none"), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"), roundDuration);
string eventId = "EndRound:" + (GameMode?.Preset?.Identifier ?? "none") + ":";
LogEndRoundStats(eventId);
if (GameMode is CampaignMode campaignMode)
{
GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Money - prevMoney);
campaignMode.TotalPlayTime += roundDuration;
}
#if CLIENT
HintManager.OnRoundEnded();
#endif
missions.Clear();
}
finally
{
@@ -791,6 +822,82 @@ namespace Barotrauma
}
}
public void LogEndRoundStats(string eventId)
{
double roundDuration = Timing.TotalTime - RoundStartTime;
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none"), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), roundDuration);
foreach (Mission mission in missions)
{
GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), roundDuration);
}
if (Level.Loaded != null)
{
string levelId = Level.Loaded.Type == LevelData.LevelType.Outpost ?
Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier :
Level.Loaded.GenerationParams?.Identifier;
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + (levelId ?? "null")), roundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none"), roundDuration);
}
if (Submarine.MainSub != null)
{
Dictionary<ItemPrefab, int> submarineInventory = new Dictionary<ItemPrefab, int>();
foreach (Item item in Item.ItemList)
{
var rootContainer = item.GetRootContainer() ?? item;
if (rootContainer.Submarine?.Info == null || rootContainer.Submarine.Info.Type != SubmarineType.Player) { continue; }
if (rootContainer.Submarine != Submarine.MainSub && !Submarine.MainSub.DockedTo.Contains(rootContainer.Submarine)) { continue; }
var holdable = item.GetComponent<Holdable>();
if (holdable == null || holdable.Attached) { continue; }
var wire = item.GetComponent<Wire>();
if (wire != null && wire.Connections.Any(c => c != null)) { continue; }
if (!submarineInventory.ContainsKey(item.Prefab))
{
submarineInventory.Add(item.Prefab, 0);
}
submarineInventory[item.Prefab]++;
}
foreach (var subItem in submarineInventory)
{
GameAnalyticsManager.AddDesignEvent(eventId + "SubmarineInventory:" + subItem.Key.Identifier, subItem.Value);
}
}
foreach (Character c in GetSessionCrewCharacters())
{
foreach (var itemSelectedDuration in c.ItemSelectedDurations)
{
string characterType = "Unknown";
if (c.IsBot)
{
characterType = "Bot";
}
else if (c.IsPlayer)
{
characterType = "Player";
}
GameAnalyticsManager.AddDesignEvent("TimeSpentOnDevices:" + (GameMode?.Preset?.Identifier ?? "none") + ":" + characterType + ":" + (c.Info?.Job?.Prefab.Identifier ?? "NoJob") + ":" + itemSelectedDuration.Key.Identifier, itemSelectedDuration.Value);
}
}
#if CLIENT
if (GameMode is TutorialMode tutorialMode)
{
GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier);
if (GameMain.IsFirstLaunch)
{
GameAnalyticsManager.AddDesignEvent("FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier);
}
}
GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentCleaning", TimeSpentCleaning);
GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentPainting", TimeSpentPainting);
TimeSpentCleaning = TimeSpentPainting = 0.0;
#endif
}
public void KillCharacter(Character character)
{
#if CLIENT
@@ -901,14 +1008,7 @@ namespace Barotrauma
((CampaignMode)GameMode).Save(doc.Root);
try
{
doc.SaveSafe(filePath);
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving gamesession to \"" + filePath + "\" failed!", e);
}
doc.SaveSafe(filePath, throwExceptions: true);
}
/*public void Load(XElement saveElement)
@@ -0,0 +1,354 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
internal partial class MedicalClinic
{
public enum NetworkHeader
{
REQUEST_AFFLICTIONS,
REQUEST_PENDING,
ADD_PENDING,
REMOVE_PENDING,
CLEAR_PENDING,
HEAL_PENDING
}
public enum AfflictionSeverity
{
Low,
Medium,
High
}
public enum MessageFlag
{
Response, // responding to your request
Announce // responding to someone else's request
}
public enum HealRequestResult
{
Unknown, // everything is not ok
Success, // everything ok
InsufficientFunds, // not enough money
Refused // the outpost has refused to provide medical assistance
}
[NetworkSerialize]
public struct NetHealRequest : INetSerializableStruct
{
public HealRequestResult Result;
}
[NetworkSerialize]
public struct NetRemovedAffliction : INetSerializableStruct
{
public NetCrewMember CrewMember;
public NetAffliction Affliction;
}
public struct NetPendingCrew : INetSerializableStruct
{
[NetworkSerialize(ArrayMaxSize = CrewManager.MaxCrewSize)]
public NetCrewMember[] CrewMembers;
}
public struct NetAffliction : INetSerializableStruct
{
[NetworkSerialize]
public string Identifier;
[NetworkSerialize]
public ushort Strength;
[NetworkSerialize]
public ushort Price;
public AfflictionSeverity AfflictionSeverity
{
get
{
if (Prefab is null) { return AfflictionSeverity.Low; }
float normalizedStrength = Strength / Prefab.MaxStrength;
// lesser than 0.1
if (normalizedStrength <= 0.1)
{
return AfflictionSeverity.Low;
}
// between 0.1 and 0.5
if (normalizedStrength > 0.1f && normalizedStrength < 0.5f)
{
return AfflictionSeverity.Medium;
}
// greater than 0.5
return AfflictionSeverity.High;
}
}
public Affliction Affliction
{
set
{
Identifier = value.Identifier;
Strength = (ushort)Math.Ceiling(value.Strength);
Price = (ushort)(value.Prefab.BaseHealCost + Strength * value.Prefab.HealCostMultiplier);
}
}
private AfflictionPrefab? cachedPrefab;
public AfflictionPrefab? Prefab
{
get
{
if (cachedPrefab is { } cached) { return cached; }
foreach (AfflictionPrefab prefab in AfflictionPrefab.List)
{
if (prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase))
{
cachedPrefab = prefab;
return prefab;
}
}
return null;
}
set
{
cachedPrefab = value;
Identifier = value?.Identifier ?? string.Empty;
Strength = 0;
Price = 0;
}
}
public readonly bool AfflictionEquals(AfflictionPrefab prefab)
{
return prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase);
}
public readonly bool AfflictionEquals(NetAffliction affliction)
{
return affliction.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase);
}
}
public struct NetCrewMember : INetSerializableStruct
{
[NetworkSerialize]
public int CharacterInfoID;
[NetworkSerialize]
public NetAffliction[] Afflictions;
public CharacterInfo CharacterInfo
{
set => CharacterInfoID = value.GetIdentifierUsingOriginalName();
}
public readonly CharacterInfo? FindCharacterInfo(ImmutableArray<CharacterInfo> crew)
{
foreach (CharacterInfo info in crew)
{
if (info.GetIdentifierUsingOriginalName() == CharacterInfoID)
{
return info;
}
}
return null;
}
public readonly bool CharacterEquals(NetCrewMember crewMember)
{
return crewMember.CharacterInfoID == CharacterInfoID;
}
}
private readonly CampaignMode? campaign;
public MedicalClinic(CampaignMode campaign)
{
this.campaign = campaign;
}
public readonly List<NetCrewMember> PendingHeals = new List<NetCrewMember>();
public Action? OnUpdate;
private static bool IsOutpostInCombat()
{
if (!(Level.Loaded is { Type: LevelData.LevelType.Outpost })) { return false; }
IEnumerable<Character> crew = GetCrewCharacters().Where(c => c.Character != null).Select(c => c.Character).ToImmutableHashSet();
foreach (Character npc in Character.CharacterList.Where(c => c.TeamID == CharacterTeamType.FriendlyNPC))
{
bool isInCombatWithCrew = !npc.IsInstigator && npc.AIController is HumanAIController { ObjectiveManager: { CurrentObjective: AIObjectiveCombat combatObjective } } && crew.Contains(combatObjective.Enemy);
if (isInCombatWithCrew) { return true; }
}
return false;
}
private HealRequestResult HealAllPending(bool force = false)
{
int totalCost = GetTotalCost();
if (!force)
{
if (GetMoney() < totalCost) { return HealRequestResult.InsufficientFunds; }
if (IsOutpostInCombat()) { return HealRequestResult.Refused; }
}
ImmutableArray<CharacterInfo> crew = GetCrewCharacters();
foreach (NetCrewMember crewMember in PendingHeals)
{
CharacterInfo? targetCharacter = crewMember.FindCharacterInfo(crew);
if (!(targetCharacter?.Character is { CharacterHealth: { } health })) { continue; }
foreach (NetAffliction affliction in crewMember.Afflictions)
{
health.ReduceAffliction(null, affliction.Identifier, affliction.Prefab?.MaxStrength ?? affliction.Strength);
}
}
if (campaign != null)
{
campaign.Money -= totalCost;
}
ClearPendingHeals();
return HealRequestResult.Success;
}
private void ClearPendingHeals()
{
PendingHeals.Clear();
}
private void RemovePendingAffliction(NetCrewMember crewMember, NetAffliction affliction)
{
foreach (NetCrewMember listMember in PendingHeals.ToList())
{
PendingHeals.Remove(listMember);
NetCrewMember pendingMember = listMember;
if (pendingMember.CharacterEquals(crewMember))
{
List<NetAffliction> newAfflictions = new List<NetAffliction>();
foreach (NetAffliction pendingAffliction in pendingMember.Afflictions)
{
if (pendingAffliction.AfflictionEquals(affliction)) { continue; }
newAfflictions.Add(pendingAffliction);
}
pendingMember.Afflictions = newAfflictions.ToArray();
}
if (!pendingMember.Afflictions.Any()) { continue; }
PendingHeals.Add(pendingMember);
}
}
private void InsertPendingCrewMember(NetCrewMember crewMember)
{
if (PendingHeals.FirstOrNull(m => m.CharacterEquals(crewMember)) is { } foundHeal)
{
PendingHeals.Remove(foundHeal);
}
PendingHeals.Add(crewMember);
}
public static bool IsHealable(Affliction affliction)
{
return affliction.Prefab.HealableInMedicalClinic && affliction.Strength > GetShowTreshold(affliction);
static float GetShowTreshold(Affliction affliction) => Math.Max(0, Math.Min(affliction.Prefab.ShowIconToOthersThreshold, affliction.Prefab.ShowInHealthScannerThreshold));
}
private NetAffliction[] GetAllAfflictions(CharacterHealth health)
{
IEnumerable<Affliction> rawAfflictions = health.GetAllAfflictions().Where(a => IsHealable(a));
List<NetAffliction> afflictions = new List<NetAffliction>();
foreach (Affliction affliction in rawAfflictions)
{
NetAffliction newAffliction;
if (afflictions.FirstOrNull(netAffliction => netAffliction.AfflictionEquals(affliction.Prefab)) is { } foundAffliction)
{
afflictions.Remove(foundAffliction);
foundAffliction.Strength += (ushort)affliction.Strength;
foundAffliction.Price += (ushort)GetAdjustedPrice(GetHealPrice(affliction));
newAffliction = foundAffliction;
}
else
{
newAffliction = new NetAffliction { Affliction = affliction };
newAffliction.Price = (ushort)GetAdjustedPrice(newAffliction.Price);
}
afflictions.Add(newAffliction);
}
return afflictions.ToArray();
static int GetHealPrice(Affliction affliction) => (int)(affliction.Prefab.BaseHealCost + (affliction.Prefab.HealCostMultiplier * affliction.Strength));
}
public int GetTotalCost() => PendingHeals.SelectMany(h => h.Afflictions).Aggregate(0, (current, affliction) => current + affliction.Price);
private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue;
public int GetMoney() => campaign?.Money ?? 0;
public static ImmutableArray<CharacterInfo> GetCrewCharacters()
{
#if DEBUG && CLIENT
if (Screen.Selected is TestScreen)
{
return TestInfos.ToImmutableArray();
}
#endif
return Character.CharacterList.Where(c => c.Info != null && c.TeamID == CharacterTeamType.Team1).Select(c => c.Info).ToImmutableArray();
}
#if DEBUG && CLIENT
private static readonly CharacterInfo[] TestInfos =
{
new CharacterInfo("human"),
new CharacterInfo("human"),
new CharacterInfo("human"),
new CharacterInfo("human"),
new CharacterInfo("human"),
new CharacterInfo("human"),
new CharacterInfo("human")
};
private static readonly NetAffliction[] TestAfflictions =
{
new NetAffliction { Identifier = "internaldamage", Strength = 80, Price = 10 },
new NetAffliction { Identifier = "blunttrauma", Strength = 50, Price = 10 },
new NetAffliction { Identifier = "lacerations", Strength = 20, Price = 10 },
new NetAffliction { Identifier = "burn", Strength = 10, Price = 10 }
};
#endif
}
}
@@ -225,6 +225,7 @@ namespace Barotrauma
}
Campaign.Money -= price;
GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineUpgrade, prefab.Identifier);
PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category);
@@ -323,6 +324,7 @@ namespace Barotrauma
}
Campaign.Money -= price;
GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineWeapon, itemToInstall.Identifier);
foreach (Item itemToSwap in linkedItems)
{
@@ -488,7 +488,11 @@ namespace Barotrauma
var sortedSelected = enabledRegularPackages
.OrderBy(p => -ContentPackage.RegularPackages.IndexOf(p))
.ToList();
if (previousEnabledRegularPackages.SequenceEqual(sortedSelected)) { return; }
if (previousEnabledRegularPackages.SequenceEqual(sortedSelected))
{
CheckModded();
return;
}
enabledRegularPackages.Clear(); enabledRegularPackages.AddRange(sortedSelected);
CharacterPrefab.Prefabs.SortAll();
@@ -508,6 +512,20 @@ namespace Barotrauma
{
RefreshContentPackageItems(AllEnabledPackages.SelectMany(p => p.Files));
}
CheckModded();
void CheckModded()
{
if (AllEnabledPackages.Any(p => p != GameMain.VanillaContent && p.HasMultiplayerIncompatibleContent))
{
GameAnalyticsManager.SetCustomDimension01(GameAnalyticsManager.CustomDimensions01.Modded);
}
else
{
GameAnalyticsManager.SetCustomDimension01(GameAnalyticsManager.CustomDimensions01.Vanilla);
}
}
}
public void EnableContentPackageItems(IEnumerable<ContentFile> unorderedFiles)
@@ -478,5 +478,18 @@ namespace Barotrauma
return TryPutItem(item, user, new List<InvSlotType>() { placeToSlots }, createNetworkEvent, ignoreCondition);
}
protected override void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true)
{
base.PutItem(item, i, user, removeItem, createNetworkEvent);
#if CLIENT
CreateSlots();
#endif
if (item.CampaignInteractionType == CampaignMode.InteractionType.Cargo)
{
item.CampaignInteractionType = CampaignMode.InteractionType.None;
}
}
}
}
@@ -74,6 +74,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, false, description: "Should the OnUse StatusEffects trigger when docking (on vanilla docking ports these effects emit particles and play a sound).)")]
public bool ApplyEffectsOnDocking
{
get;
set;
}
[Editable, Serialize(DirectionType.None, false, description: "Which direction the port is allowed to dock in. For example, \"Top\" would mean the port can dock to another port above it.\n"+
"Normally there's no need to touch this setting, but if you notice the docking position is incorrect (for example due to some unusual docking port configuration without hulls or doors), you can use this to enforce the direction.")]
public DirectionType ForceDockingDirection { get; set; }
@@ -261,7 +268,7 @@ namespace Barotrauma.Items.Components
DockingDir = GetDir(DockingTarget);
DockingTarget.DockingDir = -DockingDir;
if (applyEffects)
if (applyEffects && ApplyEffectsOnDocking)
{
ApplyStatusEffects(ActionType.OnUse, 1.0f);
}
@@ -144,16 +144,6 @@ namespace Barotrauma.Items.Components
if (linkedGap == null)
{
Rectangle rect = item.Rect;
if (IsHorizontal)
{
rect.Y += 5;
rect.Height += 10;
}
else
{
rect.X -= 5;
rect.Width += 10;
}
linkedGap = new Gap(rect, !IsHorizontal, Item.Submarine)
{
Submarine = item.Submarine

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