v0.13.0.11

This commit is contained in:
Joonas Rikkonen
2021-04-22 17:33:08 +03:00
parent 0697d7fc64
commit 8bb31f2893
391 changed files with 17271 additions and 5949 deletions
@@ -1,5 +1,6 @@
using Microsoft.Xna.Framework;
using NLog.Targets;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -18,8 +19,11 @@ namespace Barotrauma
private readonly Alignment? cameraEndPos;
private readonly float? startZoom;
private readonly float? endZoom;
public readonly float Duration;
public readonly float WaitDuration;
public readonly float PanDuration;
public readonly bool FadeOut;
public readonly bool LosFadeIn;
private readonly CoroutineHandle updateCoroutine;
@@ -28,10 +32,12 @@ namespace Barotrauma
public bool AllowInterrupt = false;
public bool RemoveControlFromCharacter = true;
public CameraTransition(ISpatialEntity targetEntity, Camera cam, Alignment? cameraStartPos, Alignment? cameraEndPos, bool fadeOut = true, float duration = 10.0f, float? startZoom = null, float? endZoom = null)
public CameraTransition(ISpatialEntity targetEntity, Camera cam, Alignment? cameraStartPos, Alignment? cameraEndPos, bool fadeOut = true, bool losFadeIn = false, float waitDuration = 0f, float panDuration = 10.0f, float? startZoom = null, float? endZoom = null)
{
Duration = duration;
WaitDuration = waitDuration;
PanDuration = panDuration;
FadeOut = fadeOut;
LosFadeIn = losFadeIn;
this.cameraStartPos = cameraStartPos;
this.cameraEndPos = cameraEndPos;
this.startZoom = startZoom;
@@ -77,9 +83,12 @@ namespace Barotrauma
Vector2 initialCameraPos = cam.Position;
Vector2? initialTargetPos = targetEntity?.WorldPosition;
float timer = 0.0f;
while (timer < Duration)
float timer = -WaitDuration;
while (timer < PanDuration)
{
float clampedTimer = Math.Max(timer, 0f);
if (Screen.Selected != GameMain.GameScreen)
{
yield return new WaitForSeconds(0.1f);
@@ -136,14 +145,20 @@ namespace Barotrauma
MathHelper.Lerp(maxPos.Y, minPos.Y, (cameraEndPos.Value.ToVector2().Y + 1.0f) / 2.0f)) :
prevControlled?.WorldPosition ?? targetEntity.WorldPosition;
Vector2 cameraPos = Vector2.SmoothStep(startPos, endPos, timer / Duration);
Vector2 cameraPos = Vector2.SmoothStep(startPos, endPos, clampedTimer / PanDuration);
cam.Translate(cameraPos - cam.Position);
#if CLIENT
cam.Zoom = MathHelper.SmoothStep(startZoom, endZoom, timer / Duration);
if (timer / Duration > 0.9f)
cam.Zoom = MathHelper.SmoothStep(startZoom, endZoom, clampedTimer / PanDuration);
if (clampedTimer / PanDuration > 0.9f)
{
if (FadeOut) { GUI.ScreenOverlayColor = Color.Lerp(Color.TransparentBlack, Color.Black, ((timer / Duration) - 0.9f) * 10.0f); }
if (FadeOut) { GUI.ScreenOverlayColor = Color.Lerp(Color.TransparentBlack, Color.Black, ((clampedTimer / PanDuration) - 0.9f) * 10.0f); }
}
if (LosFadeIn && clampedTimer / PanDuration > 0.8f)
{
GameMain.LightManager.LosAlpha = ((clampedTimer / PanDuration) - 0.8f) * 5.0f;
Lights.LightManager.ViewTarget = prevControlled ?? (targetEntity as Entity);
GameMain.LightManager.LosEnabled = true;
}
#endif
timer += CoroutineManager.UnscaledDeltaTime;
@@ -158,6 +173,7 @@ namespace Barotrauma
#if CLIENT
GUI.ScreenOverlayColor = Color.TransparentBlack;
GameMain.LightManager.LosEnabled = true;
GameMain.LightManager.LosAlpha = 1f;
#endif
if (prevControlled != null && !prevControlled.Removed)
@@ -265,6 +265,37 @@ namespace Barotrauma
}
}
public void UnequipEmptyItems(Item parentItem, bool avoidDroppingInSea = true) => UnequipEmptyItems(Character, parentItem, avoidDroppingInSea);
public void UnequipContainedItems(Item parentItem, Func<Item, bool> predicate = null, bool avoidDroppingInSea = true) => UnequipContainedItems(Character, parentItem, predicate, avoidDroppingInSea);
public static void UnequipEmptyItems(Character character, Item parentItem, bool avoidDroppingInSea = true) => UnequipContainedItems(character, parentItem, it => it.Condition <= 0, avoidDroppingInSea);
public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true)
{
var inventory = parentItem.OwnInventory;
if (inventory == null) { return; }
if (predicate == null || inventory.AllItems.Any(predicate))
{
foreach (Item containedItem in inventory.AllItemsMod)
{
if (containedItem == null) { continue; }
if (predicate == null || predicate(containedItem))
{
if (character.Submarine != Submarine.MainSub && avoidDroppingInSea)
{
// If we are outside of main sub, try to put the item in the inventory instead dropping it in the sea.
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot))
{
continue;
}
}
containedItem.Drop(character);
}
}
}
}
public void ReequipUnequipped()
{
foreach (var item in unequippedItems)
@@ -232,8 +232,7 @@ namespace Barotrauma
public bool IsWithinSector(Vector2 worldPosition)
{
if (sectorRad >= MathHelper.TwoPi) return true;
if (sectorRad >= MathHelper.TwoPi) { return true; }
Vector2 diff = worldPosition - WorldPosition;
return MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir)) <= sectorRad * 0.5f;
}
@@ -12,6 +12,10 @@ namespace Barotrauma
{
public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect, Observe, Freeze, Follow }
public enum AttackPattern { Straight, Sweep, Circle }
public enum CirclePhase { Start, CloseIn, FallBack, Advance, Strike }
partial class EnemyAIController : AIController
{
public static bool DisableEnemyAI;
@@ -49,6 +53,7 @@ namespace Barotrauma
private float updateMemoriesTimer;
private float attackLimbResetTimer;
private bool IsAttackRunning => AttackingLimb != null && AttackingLimb.attack.IsRunning;
private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0;
public float CombatStrength => AIParams.CombatStrength;
private float Sight => AIParams.Sight;
@@ -71,6 +76,23 @@ namespace Barotrauma
Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse;
}
}
private double lastAttackUpdateTime;
private Attack _activeAttack;
public Attack ActiveAttack
{
get
{
if (_activeAttack == null) { return null; }
return lastAttackUpdateTime > Timing.TotalTime - _activeAttack.Duration ? _activeAttack : null;
}
private set
{
_activeAttack = value;
lastAttackUpdateTime = Timing.TotalTime;
}
}
private AITargetMemory selectedTargetMemory;
private float targetValue;
@@ -91,8 +113,17 @@ namespace Barotrauma
private float avoidTimer;
private float observeTimer;
private float sweepTimer;
public bool StayInsideLevel = true;
private float circleRotation;
private float circleDir;
private bool inverseDir;
private bool breakCircling;
private float circleRotationSpeed;
private Vector2 circleOffset;
private float circleFallbackDistance;
private float strikeTimer;
private float aggressionIntensity;
private CirclePhase CirclePhase;
private float currentAttackIntensity;
private readonly IEnumerable<Body> myBodies;
@@ -141,9 +172,21 @@ namespace Barotrauma
}
}
/// <summary>
/// The monster won't try to damage these submarines
/// </summary>
public HashSet<Submarine> UnattackableSubmarines
{
get;
private set;
} = 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);
private bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam;
private bool reverse;
public bool Reverse
{
@@ -241,7 +284,7 @@ namespace Barotrauma
colliderLength = size.Y;
requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
avoidLookAheadDistance = Math.Max(colliderWidth * 3, 1.5f);
avoidLookAheadDistance = Math.Max(Math.Max(colliderWidth, colliderLength) * 3, 1.5f);
myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody);
}
@@ -267,7 +310,7 @@ namespace Barotrauma
private CharacterParams.TargetParams GetTargetParams(AITarget aiTarget) => GetTargetParams(GetTargetingTag(aiTarget));
private string GetTargetingTag(AITarget aiTarget)
{
if (aiTarget.Entity == null) { return null; }
if (aiTarget?.Entity == null) { return null; }
string targetingTag = null;
if (aiTarget.Entity is Character targetCharacter)
{
@@ -346,6 +389,7 @@ namespace Barotrauma
{
if (DisableEnemyAI) { return; }
base.Update(deltaTime);
UpdateTriggers(deltaTime);
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
if (steeringManager == insideSteering)
@@ -431,7 +475,7 @@ namespace Barotrauma
{
updateTargetsTimer -= deltaTime;
}
else if (avoidTimer <= 0)
else if (avoidTimer <= 0 || activeTriggers.Any() && returnTimer <= 0)
{
CharacterParams.TargetParams targetingParams = null;
UpdateTargets(Character, out targetingParams);
@@ -583,7 +627,7 @@ namespace Barotrauma
if (c.IsDead || c.Removed) { return false; }
if (!IsFriendly(Character, c)) { return true; }
// Only apply the threshold to friendly characters
return a.Damage >= selectedTargetingParams.Threshold;
return a.Damage >= selectedTargetingParams.DamageThreshold;
}
Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character;
if (attacker != null)
@@ -721,7 +765,7 @@ namespace Barotrauma
{
var location = memory.Location;
float dist = Vector2.DistanceSquared(WorldPosition, location);
if (dist < 50 * 50)
if (dist < 50 * 50 || !IsPositionInsideAllowedZone(WorldPosition, out _))
{
// Target is gone
ResetAITarget();
@@ -1305,7 +1349,7 @@ namespace Barotrauma
}
}
}
else if (!IsCoolDownRunning)
else if (!IsAttackRunning && !IsCoolDownRunning)
{
// If not, reset the attacking limb, if the cooldown is not running
// Don't use the property, because we don't want cancel reversing, if we are reversing.
@@ -1393,27 +1437,221 @@ namespace Barotrauma
}
else
{
if (selectedTargetingParams.SweepDistance > 0)
switch (selectedTargetingParams.AttackPattern)
{
Vector2 toTarget = attackWorldPos - WorldPosition;
if (distance <= 0)
{
distance = toTarget.Length();
}
float amplitude = MathHelper.Lerp(0, selectedTargetingParams.SweepStrength, MathUtils.InverseLerp(selectedTargetingParams.SweepDistance, 0, distance));
if (amplitude > 0)
{
sweepTimer += deltaTime * selectedTargetingParams.SweepSpeed;
float sin = (float)Math.Sin(sweepTimer) * amplitude;
steerPos = MathUtils.RotatePointAroundTarget(attackSimPos, SimPosition, MathHelper.ToDegrees(sin));
}
else
{
sweepTimer = Rand.Range(-1000, 1000) * selectedTargetingParams.SweepSpeed;
}
case AttackPattern.Sweep:
if (selectedTargetingParams.SweepDistance > 0)
{
if (distance <= 0)
{
distance = (attackWorldPos - WorldPosition).Length();
}
float amplitude = MathHelper.Lerp(0, selectedTargetingParams.SweepStrength, MathUtils.InverseLerp(selectedTargetingParams.SweepDistance, 0, distance));
if (amplitude > 0)
{
sweepTimer += deltaTime * selectedTargetingParams.SweepSpeed;
float sin = (float)Math.Sin(sweepTimer) * amplitude;
steerPos = MathUtils.RotatePointAroundTarget(attackSimPos, SimPosition, sin);
}
else
{
sweepTimer = Rand.Range(-1000, 1000) * selectedTargetingParams.SweepSpeed;
}
}
break;
case AttackPattern.Circle:
if (IsCoolDownRunning) { break; }
if (IsAttackRunning && CirclePhase != CirclePhase.Strike) { break; }
if (selectedTargetingParams == null) { break; }
var targetSub = SelectedAiTarget.Entity?.Submarine;
if (targetSub == null) { break; }
float subSize = Math.Max(targetSub.Borders.Width, targetSub.Borders.Height) / 2;
float sqrDistToSub = Vector2.DistanceSquared(WorldPosition, targetSub.WorldPosition);
switch (CirclePhase)
{
case CirclePhase.Start:
currentAttackIntensity = MathUtils.InverseLerp(AIParams.StartAggression, AIParams.MaxAggression, aggressionIntensity * Rand.Range(0.9f, 1.1f));
inverseDir = false;
circleDir = GetDirFromHeadingInRadius();
circleRotation = 0;
strikeTimer = 0;
blockCheckTimer = 0;
breakCircling = false;
float minRotationSpeed = 0.01f * selectedTargetingParams.CircleRotationSpeed;
float maxRotationSpeed = 0.5f * selectedTargetingParams.CircleRotationSpeed;
float minFallBackDistance = selectedTargetingParams.CircleStartDistance * 0.5f;
float maxFallBackDistance = selectedTargetingParams.CircleStartDistance;
// The lower the rotation speed, the slower the progression. Also the distance to the target stays longer.
// So basically if the value is higher, the creature will strike the sub more quickly and with more precision.
circleRotationSpeed = MathHelper.Lerp(minRotationSpeed, maxRotationSpeed, currentAttackIntensity * Rand.Range(0.9f, 1.1f));
circleFallbackDistance = MathHelper.Lerp(maxFallBackDistance, minFallBackDistance, currentAttackIntensity * Rand.Range(0.9f, 1.1f));
circleOffset = Rand.Vector(MathHelper.Lerp(selectedTargetingParams.CircleMaxRandomOffset, 0, currentAttackIntensity * Rand.Range(0.9f, 1.1f)));
canAttack = false;
aggressionIntensity = Math.Clamp(aggressionIntensity, AIParams.StartAggression, AIParams.MaxAggression);
if (targetSub.Borders.Width < 1000)
{
breakCircling = true;
CirclePhase = CirclePhase.CloseIn;
}
else if (sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance))
{
CirclePhase = CirclePhase.CloseIn;
}
else if (sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance))
{
CirclePhase = CirclePhase.FallBack;
}
else
{
CirclePhase = CirclePhase.Advance;
}
break;
case CirclePhase.CloseIn:
if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * GetStrikeDistanceMultiplier(targetSub.Velocity))
{
strikeTimer = AttackingLimb.attack.CoolDown;
CirclePhase = CirclePhase.Strike;
}
else if (!breakCircling && sqrDistToSub <= MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance / 2) && targetSub.Velocity.LengthSquared() <= MathUtils.Pow2(GetTargetMaxSpeed()))
{
CirclePhase = CirclePhase.Advance;
}
canAttack = false;
break;
case CirclePhase.FallBack:
bool isBlocked = !UpdateFallBack(attackWorldPos, deltaTime, followThrough: false, checkBlocking: true);
if (isBlocked || sqrDistToSub > MathUtils.Pow2(subSize + circleFallbackDistance))
{
CirclePhase = CirclePhase.Advance;
break;
}
return;
case CirclePhase.Advance:
Vector2 subSpeed = targetSub.Velocity;
float requiredDistMultiplier = 1;
// If the target sub is moving fast, just steer towards the target until close enough to strike
if (breakCircling || subSpeed.LengthSquared() > MathUtils.Pow2(GetTargetMaxSpeed()) || sqrDistToSub > MathUtils.Pow2(subSize + selectedTargetingParams.CircleStartDistance * 1.2f))
{
CirclePhase = CirclePhase.CloseIn;
}
else
{
circleRotation += deltaTime * circleRotationSpeed * circleDir;
if (circleRotation < -360)
{
circleRotation += 360;
}
else if (circleRotation > 360)
{
circleRotation -= 360;
}
Vector2 targetPos = attackSimPos + circleOffset;
if (Vector2.DistanceSquared(SimPosition, targetPos) < 100)
{
// Too close to the target point
// When the offset position is outside of the sub it happens that the creature sometimes reaches the target point,
// which makes it continue circling around the point (as supposed)
// But when there is some offset and the offset is too near, this is not what we want.
if (AttackingLimb != null && sqrDistToSub < MathUtils.Pow2(subSize + circleFallbackDistance))
{
CirclePhase = CirclePhase.Strike;
strikeTimer = AttackingLimb.attack.CoolDown;
}
else
{
CirclePhase = CirclePhase.Start;
}
break;
}
steerPos = MathUtils.RotatePointAroundTarget(SimPosition, targetPos, circleRotation);
requiredDistMultiplier = GetStrikeDistanceMultiplier(subSpeed);
if (IsBlocked(deltaTime, steerPos))
{
if (!inverseDir)
{
// First try changing the direction
circleDir = -circleDir;
inverseDir = true;
}
else if (circleRotationSpeed < 1)
{
// Then try increasing the rotation speed to change the movement curve
circleRotationSpeed *= 1.1f;
}
else if (circleOffset.LengthSquared() > 0.1f)
{
// Then try removing the offset
circleOffset = Vector2.Zero;
}
else
{
// If we still fail, just steer towards the target
breakCircling = true;
}
}
}
if (AttackingLimb != null && distance > 0 && distance < AttackingLimb.attack.Range * requiredDistMultiplier && IsFacing(margin: MathHelper.Lerp(0.5f, 0.9f, currentAttackIntensity)))
{
strikeTimer = AttackingLimb.attack.CoolDown;
CirclePhase = CirclePhase.Strike;
}
canAttack = false;
break;
case CirclePhase.Strike:
strikeTimer -= deltaTime;
// just continue the movement forward to make it possible to evade the attack
steerPos = SimPosition + Steering;
if (strikeTimer <= 0)
{
CirclePhase = CirclePhase.Start;
aggressionIntensity += AIParams.AggressionCumulation;
}
break;
}
break;
bool IsFacing(float margin)
{
float offset = steeringLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
Vector2 forward = VectorExtensions.Forward(steeringLimb.body.TransformedRotation - offset * Character.AnimController.Dir);
return Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), forward) > margin;
}
float GetStrikeDistanceMultiplier(Vector2 subSpeed)
{
float requiredDistMultiplier = 2;
bool isHeading = Steering != null && Vector2.Dot(Vector2.Normalize(attackWorldPos - WorldPosition), Vector2.Normalize(Steering)) > 0.9f;
if (isHeading)
{
requiredDistMultiplier = selectedTargetingParams.CircleStrikeDistanceMultiplier;
float subSpeedHorizontal = Math.Abs(subSpeed.X);
if (subSpeedHorizontal > 1)
{
// Reduce the required distance if the target is moving.
requiredDistMultiplier -= MathHelper.Lerp(0, Math.Max(selectedTargetingParams.CircleStrikeDistanceMultiplier - 1, 1), Math.Clamp(subSpeedHorizontal / 10, 0, 1));
if (requiredDistMultiplier < 2)
{
requiredDistMultiplier = 2;
}
}
}
return requiredDistMultiplier;
}
float GetDirFromHeadingInRadius()
{
Vector2 heading = VectorExtensions.Forward(Character.AnimController.Collider.Rotation);
float angle = MathUtils.VectorToAngle(heading);
return angle > MathHelper.Pi || angle < -MathHelper.Pi ? -1 : 1;
}
float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f);
}
SteeringManager.SteeringSeek(steerPos, 10);
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
if (SelectedAiTarget?.Entity is Character || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2))
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30);
}
}
}
if (canAttack)
@@ -1433,6 +1671,10 @@ namespace Barotrauma
IgnoreTarget(SelectedAiTarget);
}
}
else if (IsAttackRunning)
{
AttackingLimb.attack.ResetAttackTimer();
}
}
private readonly List<Limb> attackLimbs = new List<Limb>();
@@ -1596,9 +1838,9 @@ namespace Barotrauma
bool retaliate = !isFriendly && SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine;
bool avoidGunFire = AIParams.AvoidGunfire && attacker.Submarine != Character.Submarine;
if (State == AIState.Attack && !IsCoolDownRunning)
if (State == AIState.Attack && !IsAttackRunning && !IsCoolDownRunning)
{
// Don't retaliate or escape while performing an attack
// Don't retaliate or escape while performing an attack/under cooldown
retaliate = false;
avoidGunFire = false;
}
@@ -1633,6 +1875,9 @@ namespace Barotrauma
private bool UpdateLimbAttack(float deltaTime, Limb attackingLimb, Vector2 attackSimPos, float distance = -1, Limb targetLimb = null)
{
if (SelectedAiTarget?.Entity == null) { return false; }
ActiveAttack = attackingLimb?.attack;
if (wallTarget != null)
{
// If the selected target is not the wall target, make the wall target the selected target.
@@ -1665,8 +1910,22 @@ namespace Barotrauma
return false;
}
private readonly float blockCheckInterval = 0.1f;
private float blockCheckTimer;
private bool isBlocked;
private bool IsBlocked(float deltaTime, Vector2 steerPos, Category collisionCategory = Physics.CollisionLevel)
{
blockCheckTimer -= deltaTime;
if (blockCheckTimer <= 0)
{
blockCheckTimer = blockCheckInterval;
isBlocked = Submarine.PickBodies(SimPosition, steerPos, collisionCategory: collisionCategory).Any();
}
return isBlocked;
}
private Vector2? attackVector = null;
private void UpdateFallBack(Vector2 attackWorldPos, float deltaTime, bool followThrough)
private bool UpdateFallBack(Vector2 attackWorldPos, float deltaTime, bool followThrough, bool checkBlocking = false)
{
if (attackVector == null)
{
@@ -1683,6 +1942,11 @@ namespace Barotrauma
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15);
}
if (checkBlocking)
{
return !IsBlocked(deltaTime, SimPosition + attackDir * (avoidLookAheadDistance / 2));
}
return true;
}
#endregion
@@ -1816,6 +2080,7 @@ namespace Barotrauma
targetValue = 0;
selectedTargetMemory = null;
targetingParams = null;
bool isAnyTargetClose = false;
foreach (AITarget aiTarget in AITarget.List)
{
@@ -1896,10 +2161,21 @@ namespace Barotrauma
}
else
{
// Ignore all structures and items inside wrecks
if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsWreck) { continue; }
// Ignore the target if it's a room and the character is already inside a sub
if (character.CurrentHull != null && aiTarget.Entity is Hull) { continue; }
// Ignore all structures, items, and hulls inside wrecks and beacons
if (aiTarget.Entity.Submarine != null)
{
if (aiTarget.Entity.Submarine.Info.IsWreck || aiTarget.Entity.Submarine.Info.IsBeacon || UnattackableSubmarines.Contains(aiTarget.Entity.Submarine))
{
continue;
}
}
if (aiTarget.Entity is Hull hull)
{
// Ignore the target if it's a room and the character is already inside a sub
if (character.CurrentHull != null) { continue; }
// Ignore ruins
if (hull.Submarine == null) { continue; }
}
Door door = null;
if (aiTarget.Entity is Item item)
@@ -1914,6 +2190,14 @@ namespace Barotrauma
continue;
}
}
if (door == null)
{
// Ignore items inside ruins, unless we are in the same hull. We can't target the ruin walls.
if (item.Submarine == null && item.CurrentHull != Character.CurrentHull)
{
continue;
}
}
foreach (var prio in AIParams.Targets)
{
if (item.HasTag(prio.Tag))
@@ -2092,11 +2376,17 @@ namespace Barotrauma
if (targetParams.IgnoreInside && character.CurrentHull != null) { continue; }
if (targetParams.IgnoreOutside && character.CurrentHull == null) { continue; }
if (targetParams.IgnoreIncapacitated && targetCharacter != null && targetCharacter.IsIncapacitated) { continue; }
if (targetParams.IgnoreIfNotInSameSub)
{
if (aiTarget.Entity.Submarine != Character.Submarine) { continue; }
var targetHull = targetCharacter != null ? targetCharacter.CurrentHull : aiTarget.Entity is Item it ? it.CurrentHull : null;
if ((targetHull == null) != (character.CurrentHull == null)) { continue; }
}
if (targetParams.State == AIState.Observe || targetParams.State == AIState.Eat)
{
if (targetCharacter != null && targetCharacter.Submarine != Character.Submarine)
{
// Don't allow to target characters that are inside a different submarine / outside when we are inside.
// Never allow observing or eating characters that are inside a different submarine / outside when we are inside.
continue;
}
}
@@ -2129,18 +2419,16 @@ namespace Barotrauma
}
}
}
if (!aiTarget.IsWithinSector(WorldPosition)) { continue; }
Vector2 toTarget = aiTarget.WorldPosition - character.WorldPosition;
float dist = toTarget.Length();
float nonModifiedDist = dist;
//if the target has been within range earlier, the character will notice it more easily
if (targetMemories.ContainsKey(aiTarget))
{
dist *= 0.9f;
}
if (!CanPerceive(aiTarget, dist)) { continue; }
if (!aiTarget.IsWithinSector(WorldPosition)) { 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
@@ -2152,6 +2440,48 @@ namespace Barotrauma
// Inside the sub, treat objects that are up or down, as they were farther away.
dist *= 3;
}
if (targetParams.AttackPattern == AttackPattern.Circle)
{
if (Character.Submarine == null && aiTarget.Entity?.Submarine != null && !isAnyTargetClose)
{
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
{
dist *= 5;
}
}
}
// Don't target characters that are outside of the allowed zone, unless chasing or escaping.
switch (targetParams.State)
{
case AIState.Escape:
case AIState.Avoid:
break;
default:
if (targetParams.State == AIState.Attack)
{
// In the attack state allow going into non-allowed zone only when chasing a target.
if (State == targetParams.State && SelectedAiTarget == aiTarget) { break; }
}
if (!IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _))
{
// If we have recently been damaged by the target (or another player/bot in the same team) allow targeting it even when we are in the idle state.
bool isTargetInPlayerTeam = IsTargetInPlayerTeam(aiTarget);
if (Character.LastAttackers.None(a => a.Damage > 0 && a.Character != null && (a.Character == aiTarget.Entity || a.Character.IsOnPlayerTeam && isTargetInPlayerTeam)))
{
continue;
}
}
break;
}
valueModifier *= targetMemory.Priority / (float)Math.Sqrt(dist);
if (valueModifier > targetValue)
@@ -2181,7 +2511,7 @@ namespace Barotrauma
}
}
}
if (targetCharacter.Submarine != Character.Submarine)
if (targetCharacter.Submarine != Character.Submarine || (targetCharacter.CurrentHull == null) != (Character.CurrentHull == null))
{
if (targetCharacter.Submarine != null)
{
@@ -2195,30 +2525,21 @@ namespace Barotrauma
}
else if (Character.CurrentHull != null)
{
// Target outside, but we are inside -> Check if we can get to the target.
// Only check if we are not already targeting the character.
// If we are, keep the target (unless we choose another).
// Target outside, but we are inside -> Ignore the target but allow to keep target that is currently selected.
if (SelectedAiTarget?.Entity != targetCharacter)
{
foreach (var gap in Character.CurrentHull.ConnectedGaps)
{
var door = gap.ConnectedDoor;
if (door == null)
{
var wall = gap.ConnectedWall;
if (wall != null)
{
for (int j = 0; j < wall.Sections.Length; j++)
{
WallSection section = wall.Sections[j];
if (!CanPassThroughHole(wall, j) && section?.gap != null)
{
continue;
}
}
}
}
}
continue;
}
}
}
else if (targetCharacter.Submarine == null && Character.Submarine == null)
{
// Ignore the target when it's far enough and blocked by the level geometry, because the steering avoidance probably can't get us to the target.
if (dist > Math.Clamp(ConvertUnits.ToDisplayUnits(colliderLength) * 10, 1000, 5000))
{
if (Submarine.PickBodies(SimPosition, targetCharacter.SimPosition, collisionCategory: Physics.CollisionLevel).Any())
{
continue;
}
}
}
@@ -2227,6 +2548,10 @@ namespace Barotrauma
selectedTargetMemory = targetMemory;
targetValue = valueModifier;
targetingParams = targetParams;
if (!isAnyTargetClose)
{
isAnyTargetClose = ConvertUnits.ToDisplayUnits(colliderLength) > nonModifiedDist;
}
}
}
@@ -2355,12 +2680,12 @@ namespace Barotrauma
}
}
}
if (!Character.AnimController.CanEnterSubmarine && wallTarget == null)
if (!Character.AnimController.CanEnterSubmarine && wallTarget == null && selectedTargetingParams?.AttackPattern == AttackPattern.Straight)
{
if (closestBody.UserData is Structure w && w.Submarine != null || closestBody.UserData is Item i && i.Submarine != null)
if (closestBody.UserData is Structure w && w.Submarine != null && w.Submarine == SelectedAiTarget.Entity?.Submarine ||
closestBody.UserData is Item i && i.Submarine != null && i.Submarine == SelectedAiTarget.Entity?.Submarine)
{
// Cannot reach the target, because it's blocked by a disabled wall or a door
State = AIState.Idle;
IgnoreTarget(SelectedAiTarget);
ResetAITarget();
}
@@ -2489,6 +2814,44 @@ namespace Barotrauma
private readonly float stateResetCooldown = 10;
private float stateResetTimer;
private bool isStateChanged;
private readonly Dictionary<AITrigger, CharacterParams.TargetParams> activeTriggers = new Dictionary<AITrigger, CharacterParams.TargetParams>();
private readonly HashSet<AITrigger> inactiveTriggers = new HashSet<AITrigger>();
public void LaunchTrigger(AITrigger trigger)
{
if (trigger.IsTriggered) { return; }
if (activeTriggers.ContainsKey(trigger)) { return; }
if (activeTriggers.ContainsValue(selectedTargetingParams))
{
if (!trigger.AllowToOverride) { return; }
var existingTrigger = activeTriggers.FirstOrDefault(kvp => kvp.Value == selectedTargetingParams && kvp.Key.AllowToBeOverridden);
if (existingTrigger.Key == null) { return; }
activeTriggers.Remove(existingTrigger.Key);
}
trigger.Launch();
activeTriggers.Add(trigger, selectedTargetingParams);
ChangeParams(selectedTargetingParams, trigger.State);
}
private void UpdateTriggers(float deltaTime)
{
foreach (var triggerObject in activeTriggers)
{
AITrigger trigger = triggerObject.Key;
trigger.UpdateTimer(deltaTime);
if (!trigger.IsActive)
{
trigger.Reset();
ResetParams(triggerObject.Value);
inactiveTriggers.Add(trigger);
}
}
foreach (AITrigger trigger in inactiveTriggers)
{
activeTriggers.Remove(trigger);
}
inactiveTriggers.Clear();
}
/// <summary>
/// Resets the target's state to the original value defined in the xml.
@@ -2504,11 +2867,7 @@ namespace Barotrauma
tempParams.Values.ForEach(t => AIParams.RemoveTarget(t));
tempParams.Remove(tag);
}
targetParams.Reset();
ResetAITarget();
// Enforce the idle state so that we don't keep following the target if there's one
State = AIState.Idle;
PreviousState = AIState.Idle;
ResetParams(targetParams);
return true;
}
else
@@ -2520,6 +2879,27 @@ namespace Barotrauma
private readonly Dictionary<string, CharacterParams.TargetParams> modifiedParams = new Dictionary<string, CharacterParams.TargetParams>();
private readonly Dictionary<string, CharacterParams.TargetParams> tempParams = new Dictionary<string, CharacterParams.TargetParams>();
private void ChangeParams(CharacterParams.TargetParams targetParams, AIState state, float? priority = null)
{
if (targetParams == null) { return; }
if (priority.HasValue)
{
targetParams.Priority = priority.Value;
}
targetParams.State = state;
}
private void ResetParams(CharacterParams.TargetParams targetParams)
{
targetParams?.Reset();
if (selectedTargetingParams == targetParams || State == AIState.Idle)
{
ResetAITarget();
State = AIState.Idle;
PreviousState = AIState.Idle;
}
}
private void ChangeParams(string tag, AIState state, float? priority = null, bool onlyExisting = false)
{
if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams))
@@ -2622,6 +3002,7 @@ namespace Barotrauma
{
SetStateResetTimer();
}
blockCheckTimer = 0;
}
private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f);
@@ -2673,37 +3054,64 @@ namespace Barotrauma
}
}
private bool IsPositionInsideAllowedZone(Vector2 pos, out Vector2 targetDir)
{
targetDir = Vector2.Zero;
if (Level.Loaded == null) { return true; }
if (AIParams.AvoidAbyss)
{
if (pos.Y < Level.Loaded.AbyssStart)
{
// Too far down
targetDir = Vector2.UnitY;
}
}
else if (AIParams.StayInAbyss)
{
if (pos.Y > Level.Loaded.AbyssStart)
{
// Too far up
targetDir = -Vector2.UnitY;
}
else if (pos.Y < Level.Loaded.AbyssEnd)
{
// Too far down
targetDir = Vector2.UnitY;
}
}
float margin = 30000;
if (pos.X < -margin)
{
// Too far left
targetDir = Vector2.UnitX;
}
else if (pos.X > Level.Loaded.Size.X + margin)
{
// Too far right
targetDir = -Vector2.UnitX;
}
return targetDir == Vector2.Zero;
}
private Vector2 returnDir;
private float returnTimer;
private void SteerInsideLevel(float deltaTime)
{
if (SteeringManager is IndoorsSteeringManager || !StayInsideLevel) { return; }
if (SteeringManager is IndoorsSteeringManager) { return; }
if (Level.Loaded == null) { return; }
Point levelSize = Level.Loaded.Size;
float returnTime = 10;
if (WorldPosition.Y < 0)
if (State == AIState.Attack && returnTimer <= 0) { return; }
float returnTime = 5;
if (!IsPositionInsideAllowedZone(WorldPosition, out Vector2 targetDir))
{
// Too far down
returnDir = targetDir;
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
returnDir = Vector2.UnitY;
}
if (WorldPosition.X < 0)
{
// Too far left
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
returnDir = Vector2.UnitX;
}
if (WorldPosition.X > levelSize.X)
{
// Too far right
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
returnDir = -Vector2.UnitX;
}
if (returnTimer > 0)
{
returnTimer -= deltaTime;
SteeringManager.Reset();
SteeringManager.SteeringManual(deltaTime, returnDir * 2);
SteeringManager.SteeringManual(deltaTime, returnDir * 10);
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, 15);
}
}
@@ -15,7 +15,7 @@ namespace Barotrauma
private readonly AIObjectiveManager objectiveManager;
private float sortTimer;
public float SortTimer { get; set; }
private float crouchRaycastTimer;
private float reactTimer;
private float unreachableClearTimer;
@@ -52,6 +52,30 @@ namespace Barotrauma
private readonly float obstacleRaycastInterval = 1;
private float obstacleRaycastTimer;
private readonly float enemyCheckInterval = 0.2f;
private readonly float enemySpotDistanceOutside = 1500;
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.
/// </summary>
public float ReportRange { get; set; } = float.PositiveInfinity;
private float _aimSpeed = 1;
public float AimSpeed
{
get { return _aimSpeed; }
set { _aimSpeed = Math.Max(value, 0.01f); }
}
private float _aimAccuracy = 1;
public float AimAccuracy
{
get { return _aimAccuracy; }
set { _aimAccuracy = Math.Clamp(value, 0f, 1f); }
}
/// <summary>
/// List of previous attacks done to this character
/// </summary>
@@ -64,18 +88,6 @@ namespace Barotrauma
public AIObjectiveManager ObjectiveManager => objectiveManager;
public Order CurrentOrder
{
get;
private set;
}
public string CurrentOrderOption
{
get;
private set;
}
public float CurrentHullSafety { get; private set; } = 100;
private readonly Dictionary<Character, float> structureDamageAccumulator = new Dictionary<Character, float>();
@@ -119,12 +131,9 @@ namespace Barotrauma
outsideSteering = new SteeringManager(this);
objectiveManager = new AIObjectiveManager(c);
reactTimer = GetReactionTime();
sortTimer = Rand.Range(0f, sortObjectiveInterval);
InitProjSpecific();
SortTimer = Rand.Range(0f, sortObjectiveInterval);
}
partial void InitProjSpecific();
public override void Update(float deltaTime)
{
if (DisableCrewAI || Character.Removed) { return; }
@@ -171,23 +180,63 @@ namespace Barotrauma
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
bool hasValidPath = HasValidPath();
if (Character.Submarine == null && hasValidPath)
if (Character.Submarine == null)
{
obstacleRaycastTimer -= deltaTime;
if (obstacleRaycastTimer <= 0)
if (hasValidPath)
{
obstacleRaycastTimer = obstacleRaycastInterval;
// Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs).
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
obstacleRaycastTimer -= deltaTime;
if (obstacleRaycastTimer <= 0)
{
if (connectedSub == Submarine.MainSub) { continue; }
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
obstacleRaycastTimer = obstacleRaycastInterval;
// Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs).
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
{
PathSteering.CurrentPath.Unreachable = true;
break;
if (connectedSub == Submarine.MainSub) { continue; }
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
{
PathSteering.CurrentPath.Unreachable = true;
break;
}
}
}
}
}
if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID))
{
// Spot enemies while staying outside or inside an enemy ship.
enemycheckTimer -= deltaTime;
if (enemycheckTimer < 0)
{
enemycheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f);
if (!objectiveManager.IsCurrentObjective<AIObjectiveCombat>())
{
float closestDistance = 0;
Character closestEnemy = null;
foreach (Character c in Character.CharacterList)
{
if (c.Submarine != Character.Submarine) { continue; }
if (c.Removed || c.IsDead || c.IsIncapacitated) { continue; }
if (IsFriendly(c)) { continue; }
Vector2 toTarget = c.WorldPosition - WorldPosition;
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 (!Character.CanSeeCharacter(c)) { continue; }
if (dist < closestDistance || closestEnemy == null)
{
closestEnemy = c;
closestDistance = dist;
}
}
if (closestEnemy != null)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, closestEnemy);
}
}
}
@@ -216,14 +265,14 @@ namespace Barotrauma
CheckCrouching(deltaTime);
Character.ClearInputs();
if (sortTimer > 0.0f)
if (SortTimer > 0.0f)
{
sortTimer -= deltaTime;
SortTimer -= deltaTime;
}
else
{
objectiveManager.SortObjectives();
sortTimer = sortObjectiveInterval;
SortTimer = sortObjectiveInterval;
}
objectiveManager.UpdateObjectives(deltaTime);
@@ -240,14 +289,14 @@ namespace Barotrauma
{
if (Character.CurrentHull != null)
{
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
if (Character.IsOnPlayerTeam)
{
// Outpost npcs don't inform each other about threads, like crew members do.
VisibleHulls.ForEach(h => RefreshHullSafety(h));
VisibleHulls.ForEach(h => PropagateHullSafety(Character, h));
}
else
{
VisibleHulls.ForEach(h => PropagateHullSafety(Character, h));
// Outpost npcs don't inform each other about threats, like crew members do.
VisibleHulls.ForEach(h => RefreshHullSafety(h));
}
}
if (Character.SpeechImpediment < 100.0f)
@@ -367,9 +416,11 @@ namespace Barotrauma
if (isCarrying)
{
if (findItemState == FindItemState.DivingSuit && ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
if (findItemState != FindItemState.OtherItem)
{
if (ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective))
var decontain = ObjectiveManager.GetActiveObjectives<AIObjectiveDecontainItem>().LastOrDefault();
if (decontain != null && decontain.TargetItem != null && decontain.TargetItem.HasTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR) &&
ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective))
{
// Don't try to put the diving suit in a locker if the suit would be needed in any hull in the path to the locker.
gotoObjective.Abandon = true;
@@ -384,14 +435,17 @@ namespace Barotrauma
// Diving gear
if (oxygenLow || findItemState != FindItemState.OtherItem)
{
if (!NeedsDivingGear(Character.CurrentHull, out bool needsSuit) || !needsSuit || oxygenLow)
bool needsGear = NeedsDivingGear(Character.CurrentHull, out _);
if (!needsGear || oxygenLow)
{
bool shouldKeepTheGearOn = Character.AnimController.HeadInWater
|| Character.Submarine == null
|| Character.Submarine.TeamID != Character.TeamID
|| ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>()
|| ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character // wait order
|| ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn);
bool shouldKeepTheGearOn =
Character.AnimController.InWater ||
Character.AnimController.HeadInWater ||
Character.CurrentHull == null ||
Character.Submarine.TeamID != Character.TeamID ||
ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>() ||
ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character || // wait order
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn);
if (oxygenLow && Character.CurrentHull.Oxygen > 0)
{
shouldKeepTheGearOn = false;
@@ -717,17 +771,11 @@ namespace Barotrauma
targetHull = hull;
}
}
foreach (var ballastFlora in MapCreatures.Behavior.BallastFloraBehavior.EntityList)
if (IsBallastFloraNoticeable(Character, hull))
{
if (ballastFlora.Parent?.Submarine != Character.Submarine) { continue; }
if (!ballastFlora.HasBrokenThrough) { continue; }
// Don't react to the first two branches, because they are usually in the very edges of the room.
if (ballastFlora.Branches.Count(b => !b.Removed && b.Health > 0 && b.CurrentHull == hull) > 2)
{
var orderPrefab = Order.GetPrefab("reportballastflora");
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
targetHull = hull;
}
var orderPrefab = Order.GetPrefab("reportballastflora");
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
targetHull = hull;
}
if (!isFighting)
{
@@ -784,16 +832,31 @@ namespace Barotrauma
identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"),
minDurationBetweenSimilar: 60.0f);
}
else if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order);
#if SERVER
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", targetHull, null, Character));
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", CharacterInfo.HighestManualOrderPriority, targetHull, null, Character));
#endif
}
}
}
public static bool IsBallastFloraNoticeable(Character character, Hull hull)
{
foreach (var ballastFlora in MapCreatures.Behavior.BallastFloraBehavior.EntityList)
{
if (ballastFlora.Parent?.Submarine != character.Submarine) { continue; }
if (!ballastFlora.HasBrokenThrough) { continue; }
// Don't react to the first two branches, because they are usually in the very edges of the room.
if (ballastFlora.Branches.Count(b => !b.Removed && b.Health > 0 && b.CurrentHull == hull) > 2)
{
return true;
}
}
return false;
}
public static void ReportProblem(Character reporter, Order order)
{
if (reporter == null || order == null) { return; }
@@ -807,6 +870,8 @@ namespace Barotrauma
private void UpdateSpeaking()
{
if (!Character.IsOnPlayerTeam) { return; }
if (Character.Oxygen < 20.0f)
{
Character.Speak(TextManager.Get("DialogLowOxygen"), null, Rand.Range(0.5f, 5.0f), "lowoxygen", 30.0f);
@@ -885,7 +950,7 @@ namespace Barotrauma
}
if (attacker == null || attacker.IsDead || attacker.Removed)
{
// Don't react on the damage if there's no attacker.
// Don't react to the damage if there's no attacker.
// We might consider launching the retreat combat objective in some cases, so that the bot does not just stand somewhere getting damaged and dying.
// But fires and enemies should already be handled by the FindSafetyObjective.
return;
@@ -893,12 +958,17 @@ namespace Barotrauma
//if (Character.LastDamageSource == null) { return; }
//AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
}
else if (realDamage <= 0 && (attacker.IsBot || attacker.TeamID == Character.TeamID))
if (realDamage <= 0 && (attacker.IsBot || attacker.TeamID == Character.TeamID))
{
// Don't react on damage that is entirely based on karma penalties (medics, poisons etc), unless applier is player
// Don't react to damage that is entirely based on karma penalties (medics, poisons etc), unless applier is player
return;
}
else if (IsFriendly(attacker))
if (attacker.Submarine == null && Character.Submarine != null)
{
// Don't react to attackers that are outside of the sub (e.g. AoE attacks)
return;
}
if (IsFriendly(attacker))
{
if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character)
{
@@ -911,7 +981,7 @@ namespace Barotrauma
{
if (cumulativeDamage > 1)
{
// Don't retaliate on damage done by human ai, because we know it's accidental
// Don't retaliate on damage done by friendly NPC, because we know it's accidental
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
}
}
@@ -921,49 +991,29 @@ namespace Barotrauma
// Inform other NPCs
if (cumulativeDamage > 1)
{
foreach (Character otherCharacter in Character.CharacterList)
{
if (otherCharacter == Character || otherCharacter.IsDead || otherCharacter.IsUnconscious || otherCharacter.Removed ||
otherCharacter.Info?.Job == null || otherCharacter.TeamID != CharacterTeamType.FriendlyNPC ||
!(otherCharacter.AIController is HumanAIController otherHumanAI) ||
otherCharacter.IsInstigator)
{
continue;
}
if (!otherHumanAI.IsFriendly(Character)) { continue; }
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull);
if (otherCharacter.IsSecurity)
{
// Alert all the security officers magically
float delay = isWitnessing ? GetReactionTime() * 2 : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced);
otherHumanAI.AddCombatObjective(DetermineCombatMode(otherCharacter, cumulativeDamage), attacker, delay);
}
else if (isWitnessing)
{
var mode = Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat;
// Other witnesses retreat to safety
otherHumanAI.AddCombatObjective(mode, attacker, GetReactionTime());
}
}
InformOtherNPCs(cumulativeDamage);
}
if (Character.IsBot)
{
if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; }
if (Character.IsSecurity)
if (attacker.IsPlayer)
{
if (attacker.TeamID != Character.TeamID && cumulativeDamage > 1 || cumulativeDamage > 10)
if (Character.IsSecurity)
{
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.50f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 30.0f);
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
else if (!Character.IsInstigator && cumulativeDamage > 1)
{
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.50f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 30.0f);
Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", 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.
@@ -971,12 +1021,7 @@ namespace Barotrauma
}
else
{
bool allowOffensive = HasItem(attacker, "handlocker", out _, requireEquipped: true);
if (attackResult.Afflictions.Any(a => a is AfflictionHusk))
{
cumulativeDamage = 100;
}
// Don't react on minor (accidental) dmg done by characters that are in the same team
// Don't react to minor (accidental) dmg done by characters that are in the same team
if (cumulativeDamage < 10)
{
if (!Character.IsSecurity && cumulativeDamage > 1)
@@ -986,23 +1031,48 @@ namespace Barotrauma
}
else
{
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage, dmgThreshold: 20, allowOffensive: allowOffensive), attacker, GetReactionTime() * 2);
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage, dmgThreshold: 50), attacker, GetReactionTime() * 2);
}
}
}
}
}
else if (Character.IsBot)
else
{
// Non-friendly
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker);
InformOtherNPCs(GetDamageDoneByAttacker(attacker));
if (Character.IsBot)
{
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker);
}
}
AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage, float dmgThreshold = 10, bool allowOffensive = true)
void InformOtherNPCs(float cumulativeDamage)
{
foreach (Character otherCharacter in Character.CharacterList)
{
if (otherCharacter == Character || otherCharacter.IsDead || otherCharacter.IsUnconscious || otherCharacter.Removed) { continue; }
if (otherCharacter.Submarine != Character.Submarine) { continue; }
if (otherCharacter.Submarine != attacker.Submarine) { continue; }
if (otherCharacter.Info?.Job == null || otherCharacter.IsInstigator) { continue; }
if (otherCharacter.IsPlayer) { continue; }
if (!(otherCharacter.AIController is HumanAIController otherHumanAI)) { continue; }
if (!otherHumanAI.IsFriendly(Character)) { continue; }
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull);
if (!isWitnessing && !CheckReportRange(Character, otherCharacter, ReportRange)) { continue; }
var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing, dmgThreshold: attacker.TeamID == Character.TeamID ? 50 : 10);
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)
{
if (!IsFriendly(attacker))
{
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
return c.AIController is HumanAIController humanAI &&
(humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders))
? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
}
else
{
@@ -1011,7 +1081,11 @@ namespace Barotrauma
{
return AIObjectiveCombat.CombatMode.None;
}
if (Character.IsInstigator && attacker.IsPlayer)
else if (isWitnessing && Character.CombatAction != null && !c.IsSecurity)
{
return Character.CombatAction.WitnessReaction;
}
else if (Character.IsInstigator && attacker.IsPlayer)
{
// The guards don't react when the player attacks instigators.
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
@@ -1029,6 +1103,15 @@ namespace Barotrauma
}
else
{
if (c.AIController is HumanAIController humanAI && humanAI.ObjectiveManager.GetActiveObjective<AIObjectiveCombat>()?.Enemy == attacker)
{
// Already targeting the attacker -> treat as a more serious threat.
cumulativeDamage *= 2;
}
if (attackResult.Afflictions.Any(a => a is AfflictionHusk))
{
cumulativeDamage = 100;
}
if (cumulativeDamage > dmgThreshold)
{
if (c.IsSecurity)
@@ -1049,15 +1132,16 @@ namespace Barotrauma
}
}
private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character attacker, float delay = 0, Func<bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false)
private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func<bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false)
{
if (mode == AIObjectiveCombat.CombatMode.None) { return; }
if (Character.IsDead || Character.IsIncapacitated) { return; }
if (ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective)
if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; }
if (!Character.IsBot) { return; }
if (ObjectiveManager.Objectives.FirstOrDefault(o => o is AIObjectiveCombat) is AIObjectiveCombat combatObjective)
{
// Don't replace offensive mode with something else
if (combatObjective.Mode == AIObjectiveCombat.CombatMode.Offensive && mode != AIObjectiveCombat.CombatMode.Offensive) { return; }
if (combatObjective.Mode != mode || combatObjective.Enemy != attacker || (combatObjective.Enemy == null && attacker == null))
if (combatObjective.Mode != mode || combatObjective.Enemy != target || (combatObjective.Enemy == null && target == null))
{
// Replace the old objective with the new.
ObjectiveManager.Objectives.Remove(combatObjective);
@@ -1078,9 +1162,12 @@ namespace Barotrauma
AIObjectiveCombat CreateCombatObjective()
{
var objective = new AIObjectiveCombat(Character, attacker, mode, objectiveManager)
var objective = new AIObjectiveCombat(Character, target, mode, objectiveManager)
{
HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman" || Character.CurrentHull == null && ObjectiveManager.IsCurrentOrder<AIObjectiveGoTo>(),
HoldPosition =
Character.Info?.Job?.Prefab.Identifier == "watchman" ||
Character.CurrentHull == null ||
Character.IsOnPlayerTeam && !target.IsPlayer && ObjectiveManager.GetActiveObjective<AIObjectiveGoTo>()?.Target is Character followTarget && followTarget.IsPlayer,
abortCondition = abortCondition,
allowHoldFire = allowHoldFire,
};
@@ -1096,11 +1183,20 @@ namespace Barotrauma
}
}
public void SetOrder(Order order, string option, Character orderGiver, bool speak = true)
public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak = true)
{
CurrentOrderOption = option;
CurrentOrder = order;
objectiveManager.SetOrder(order, option, orderGiver, speak);
objectiveManager.SetOrder(order, option, priority, orderGiver, speak);
}
public void SetForcedOrder(Order order, string option, Character orderGiver)
{
var objective = ObjectiveManager.CreateObjective(order, option, orderGiver, false);
ObjectiveManager.SetForcedOrder(objective);
}
public void ClearForcedOrder()
{
ObjectiveManager.ClearForcedOrder();
}
public override void SelectTarget(AITarget target)
@@ -1112,7 +1208,7 @@ namespace Barotrauma
{
base.Reset();
objectiveManager.SortObjectives();
sortTimer = sortObjectiveInterval;
SortTimer = sortObjectiveInterval;
float waitDuration = characterWaitOnSwitch;
if (ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
{
@@ -1305,7 +1401,7 @@ namespace Barotrauma
Character thief = character;
bool someoneSpoke = false;
if (item.SpawnedInOutpost && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker"))
if (item.SpawnedInOutpost && !item.AllowStealing && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker"))
{
foreach (Character otherCharacter in Character.CharacterList)
{
@@ -1338,6 +1434,9 @@ namespace Barotrauma
item.StolenDuringRound = true;
otherCharacter.Speak(TextManager.Get("dialogstealwarning"), null, Rand.Range(0.5f, 1.0f), "thief", 10.0f);
someoneSpoke = true;
#if CLIENT
HintManager.OnStoleItem(thief, item);
#endif
}
// React if we are security
if (!TriggerSecurity(otherHumanAI))
@@ -1354,7 +1453,7 @@ namespace Barotrauma
}
}
}
else if (item.OwnInventory?.FindItem(it => it.SpawnedInOutpost, true) is { } foundItem)
else if (item.OwnInventory?.FindItem(it => it.SpawnedInOutpost && !item.AllowStealing, true) is { } foundItem)
{
ItemTaken(foundItem, character);
}
@@ -1474,7 +1573,7 @@ namespace Barotrauma
targetAdded = true;
}
}
});
}, range: (caller.AIController as HumanAIController)?.ReportRange ?? float.PositiveInfinity);
return targetAdded;
}
@@ -1577,7 +1676,6 @@ namespace Barotrauma
dangerousItemsFactor = 0;
}
}
float safety = oxygenFactor * waterFactor * fireFactor * enemyFactor * dangerousItemsFactor;
return MathHelper.Clamp(safety * 100, 0, 100);
}
@@ -1624,7 +1722,7 @@ namespace Barotrauma
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
{
bool sameTeam = me.TeamID == other.TeamID;
bool friendlyTeam = IsOnFriendlyTeam(GameMain.GameSession?.GameMode, me, other);
bool friendlyTeam = IsOnFriendlyTeam(me, other);
bool teamGood = sameTeam || friendlyTeam && !onlySameTeam;
if (!teamGood) { return false; }
bool speciesGood = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
@@ -1640,18 +1738,27 @@ namespace Barotrauma
return true;
}
private static bool IsOnFriendlyTeam(GameMode mode, Character me, Character other)
public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
{
// Only enemies are in the Team "None"
bool friendlyTeam = me.TeamID != CharacterTeamType.None && other.TeamID != CharacterTeamType.None;
// When playing a combat mission, we need to be on the same team to be friendlies
if (friendlyTeam && mode is MissionMode mm && mm.Mission is CombatMission)
if (myTeam == otherTeam) { return true; }
switch (myTeam)
{
friendlyTeam = me.TeamID == other.TeamID;
case CharacterTeamType.None:
case CharacterTeamType.Team1:
case CharacterTeamType.Team2:
// Only friendly to the same team and friendly NPCs
return otherTeam == CharacterTeamType.FriendlyNPC;
case CharacterTeamType.FriendlyNPC:
// Friendly NPCs are friendly to both teams
return otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2;
default:
return true;
}
return friendlyTeam;
}
public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID);
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious;
public static bool IsTrueForAllCrewMembers(Character character, Func<HumanAIController, bool> predicate)
@@ -1711,68 +1818,98 @@ namespace Barotrauma
return count;
}
public static void DoForEachCrewMember(Character character, Action<HumanAIController> action)
public static void DoForEachCrewMember(Character character, Action<HumanAIController> action, float range = float.PositiveInfinity)
{
if (character == null) { return; }
foreach (var c in Character.CharacterList)
{
if (FilterCrewMember(character, c))
if (FilterCrewMember(character, c) && CheckReportRange(character, c, range))
{
action(c.AIController as HumanAIController);
}
}
}
private static bool CheckReportRange(Character character, Character target, float range)
{
if (float.IsPositiveInfinity(range)) { return true; }
if (character.CurrentHull == null || target.CurrentHull == null)
{
return Vector2.DistanceSquared(character.WorldPosition, target.WorldPosition) <= range * range;
}
else
{
return character.CurrentHull.GetApproximateDistance(character.Position, target.Position, target.CurrentHull, range, distanceMultiplierPerClosedDoor: 2) <= range;
}
}
private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self);
public static bool IsItemOperatedByAnother(Character character, ItemComponent target, out Character operatingCharacter)
{
operatingCharacter = null;
if (character == null) { return false; }
if (target?.Item == null) { return false; }
bool isOrder = IsOrderedToOperateThis(character.AIController);
foreach (var c in Character.CharacterList)
{
if (character == null) { continue; }
if (c == character) { continue; }
if (c.IsDead || c.IsIncapacitated) { continue; }
if (c.SelectedConstruction != target.Item) { continue; }
if (!IsFriendly(character, c, onlySameTeam: true)) { continue; }
operatingCharacter = c;
// If the other character is player, don't try to operate
if (c.IsPlayer) { return true; }
if (c.AIController is HumanAIController controllingHumanAi)
if (c.IsPlayer)
{
Item otherTarget = controllingHumanAi.objectiveManager.GetActiveObjective<AIObjectiveOperateItem>()?.Component.Item ?? c.SelectedConstruction;
if (otherTarget != target.Item) { continue; }
// If the other character is ordered to operate the item, let him do it
if (controllingHumanAi.ObjectiveManager.IsCurrentOrder<AIObjectiveOperateItem>())
if (c.SelectedConstruction == target.Item)
{
// If the other character is player, don't try to operate
return true;
}
}
else if (c.AIController is HumanAIController operatingAI)
{
if (operatingAI.ObjectiveManager.Objectives.None(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item))
{
// Not targeting the same item.
continue;
}
bool isTargetOrdered = IsOrderedToOperateThis(c.AIController);
if (!isOrder && isTargetOrdered)
{
// If the other bot is ordered to operate the item, let him do it, unless we are ordered too
return true;
}
else
{
if (character == null)
if (isOrder && !isTargetOrdered)
{
return true;
}
else if (target is Steering)
{
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
return character.GetSkillLevel("helm") <= c.GetSkillLevel("helm");
// We are ordered and the target is not -> allow to operate
continue;
}
else
{
return target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c);
if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder == operatingAI.ObjectiveManager.CurrentObjective)
{
// The other bot is ordered to do something else
continue;
}
if (target is Steering)
{
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
if (character.GetSkillLevel("helm") <= c.GetSkillLevel("helm"))
{
return true;
}
}
else if (target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c))
{
return true;
}
}
}
}
else
{
// Shouldn't go here, unless we allow non-humans to operate items
return false;
}
}
return false;
bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item;
}
#region Wrappers
@@ -178,7 +178,7 @@ namespace Barotrauma
{
if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0f &&
speaker?.CurrentHull != null &&
speaker.TeamID == CharacterTeamType.FriendlyNPC &&
(speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) &&
Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull))
{
currentFlags.Add("EnterOutpost");
@@ -188,6 +188,11 @@ namespace Barotrauma
{
currentFlags.Add("Casual");
}
if (GameMain.GameSession.IsCurrentLocationRadiated())
{
currentFlags.Add("InRadiation");
}
}
if (speaker != null)
@@ -221,6 +226,19 @@ namespace Barotrauma
{
currentFlags.Add("CampaignNPC." + speaker.CampaignInteractionType);
}
if (GameMain.GameSession?.GameMode is CampaignMode campaignMode &&
(campaignMode.Map?.CurrentLocation?.Type?.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase) ?? false))
{
if (speaker.TeamID == CharacterTeamType.None)
{
currentFlags.Add("Bandit");
}
else if (speaker.TeamID == CharacterTeamType.FriendlyNPC)
{
currentFlags.Add("Hostage");
}
}
}
return currentFlags;
@@ -68,7 +68,7 @@ namespace Barotrauma
if (_abandon)
{
#if DEBUG
if (HumanAIController.debugai && objectiveManager.CurrentOrder == this)
if (HumanAIController.debugai && objectiveManager.IsOrder(this) && !objectiveManager.IsCurrentOrder<AIObjectiveGoTo>())
{
throw new Exception("Order abandoned!");
}
@@ -230,7 +230,7 @@ namespace Barotrauma
/// </summary>
public virtual float GetPriority()
{
bool isOrder = objectiveManager.CurrentOrder == this;
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed)
{
Priority = 0;
@@ -239,7 +239,7 @@ namespace Barotrauma
}
if (isOrder)
{
Priority = AIObjectiveManager.OrderPriority;
Priority = objectiveManager.GetOrderPriority(this);
}
else
{
@@ -261,7 +261,7 @@ namespace Barotrauma
public virtual void Update(float deltaTime)
{
if (objectiveManager.CurrentOrder != this && objectiveManager.WaitTimer <= 0)
if (!objectiveManager.IsOrder(this) && objectiveManager.WaitTimer <= 0)
{
UpdateDevotion(deltaTime);
}
@@ -430,7 +430,7 @@ namespace Barotrauma
subObjectives.Remove(subObjective);
if (AbandonWhenCannotCompleteSubjectives)
{
if (objectiveManager.CurrentOrder == this)
if (objectiveManager.IsOrder(this))
{
Reset();
}
@@ -64,7 +64,7 @@ namespace Barotrauma
private bool IsReady(PowerContainer battery)
{
if (battery.HasBeenTuned && character.CurrentOrder == null) { return true; }
if (battery.HasBeenTuned && character.IsDismissed) { return true; }
if (Option == "charge")
{
return battery.RechargeRatio >= PowerContainer.aiRechargeTargetRatio;
@@ -79,7 +79,7 @@ namespace Barotrauma
new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier)
{
IsLoop = false,
Override = character.CurrentOrder != null,
Override = !character.IsDismissed,
completionCondition = () => IsReady(battery)
};
@@ -48,21 +48,35 @@ namespace Barotrauma
float selectedBonus = isSelected ? 100 - MaxDevotion : 0;
float devotion = (CumulatedDevotion + selectedBonus) / 100;
float reduction = IsPriority ? 1 : isSelected ? 2 : 3;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90);
float max = AIObjectiveManager.LowestOrderPriority - reduction;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1));
if (decontainObjective == null)
{
// Halve the priority until there's a decontain objective (a valid container was found).
Priority /= 2;
}
}
return Priority;
}
protected override void Act(float deltaTime)
{
// Only continue when the get item sub objectives have been completed.
if (subObjectives.Any()) { return; }
if (item.IgnoreByAI)
{
Abandon = true;
return;
}
if (item.ParentInventory != null)
{
if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrders()))
{
// Target was picked up or moved by someone.
Abandon = true;
return;
}
}
// Only continue when the get item sub objectives have been completed.
if (subObjectives.Any()) { return; }
if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer))
{
itemIndex = 0;
@@ -79,6 +93,7 @@ namespace Barotrauma
TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
{
Equip = equip,
TakeWholeStack = true,
DropIfFails = true
},
onCompleted: () =>
@@ -125,5 +140,13 @@ namespace Barotrauma
itemIndex = 0;
decontainObjective = null;
}
public void DropTarget()
{
if (item != null && character.HasItem(item))
{
item.Drop(character);
}
}
}
}
@@ -2,6 +2,7 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System;
namespace Barotrauma
{
@@ -29,7 +30,21 @@ namespace Barotrauma
this.prioritizedItems.AddRange(prioritizedItems.Where(i => i != null));
}
protected override float TargetEvaluation() => Targets.Any() ? (objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : AIObjectiveManager.RunPriority - 1) : 0;
protected override float TargetEvaluation()
{
if (Targets.None()) { return 0; }
if (objectiveManager.IsOrder(this))
{
float prio = objectiveManager.GetOrderPriority(this);
if (subObjectives.All(so => so.SubObjectives.None()))
{
// If none of the subobjectives have subobjectives, no valid container was found. In this case, let's reduce the priority below the run threshold.
prio = Math.Min(prio, AIObjectiveManager.RunPriority - 1);
}
return prio;
}
return AIObjectiveManager.RunPriority - 0.5f;
}
protected override bool Filter(Item target)
{
@@ -65,10 +80,10 @@ namespace Barotrauma
return true;
}
public static bool IsValidContainer(Item item, Character character) =>
!item.IgnoreByAI && item.IsInteractable(character) && item.HasTag("allowcleanup") && item.ParentInventory == null && item.OwnInventory != null && item.OwnInventory.AllItems.Any() && IsItemInsideValidSubmarine(item, character);
public static bool IsValidContainer(Item item, Character character, bool allowUnloading = true) =>
!item.IgnoreByAI && item.IsInteractable(character) && item.HasTag("allowcleanup") && allowUnloading && item.ParentInventory == null && item.OwnInventory != null && item.OwnInventory.AllItems.Any() && IsItemInsideValidSubmarine(item, character);
public static bool IsValidTarget(Item item, Character character, bool checkInventory)
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true)
{
if (item == null) { return false; }
if (item.IgnoreByAI) { return false; }
@@ -76,7 +91,7 @@ namespace Barotrauma
if (item.SpawnedInOutpost) { return false; }
if (item.ParentInventory != null)
{
if (item.Container == null || !IsValidContainer(item.Container, character)) { return false; }
if (item.Container == null || !IsValidContainer(item.Container, character, allowUnloading)) { return false; }
}
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
var pickable = item.GetComponent<Pickable>();
@@ -127,5 +142,17 @@ namespace Barotrauma
}
return canEquip;
}
public override void OnDeselected()
{
base.OnDeselected();
foreach (var subObjective in SubObjectives)
{
if (subObjective is AIObjectiveCleanupItem cleanUpObjective)
{
cleanUpObjective.DropTarget();
}
}
}
}
}
@@ -30,6 +30,7 @@ namespace Barotrauma
private float holdFireTimer;
private bool hasAimed;
private bool isLethalWeapon;
private bool AllowCoolDown => !IsOffensiveOrArrest || Mode != initialMode;
public Character Enemy { get; private set; }
public bool HoldPosition { get; set; }
@@ -79,11 +80,18 @@ namespace Barotrauma
private float coolDownTimer;
private IEnumerable<Body> myBodies;
private float aimTimer;
private float reloadTimer;
private float spreadTimer;
private bool canSeeTarget;
private float visibilityCheckTimer;
private readonly float visibilityCheckInterval = 0.2f;
private float sqrDistance;
private readonly float maxDistance = 2000;
private readonly float distanceCheckInterval = 0.2f;
private float distanceTimer;
/// <summary>
/// Aborts the objective when this condition is true
/// </summary>
@@ -108,8 +116,12 @@ namespace Barotrauma
public CombatMode Mode { get; private set; }
private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest;
private bool TargetEliminated => Enemy == null || Enemy.Removed || Enemy.IsUnconscious;
private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious;
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
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;
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f)
@@ -136,6 +148,8 @@ namespace Barotrauma
{
Mode = CombatMode.Retreat;
}
spreadTimer = Rand.Range(-10, 10);
HumanAIController.SortTimer = 0;
}
public override float GetPriority()
@@ -159,6 +173,10 @@ namespace Barotrauma
base.Update(deltaTime);
ignoreWeaponTimer -= deltaTime;
checkWeaponsTimer -= deltaTime;
if (reloadTimer > 0)
{
reloadTimer -= deltaTime;
}
if (ignoreWeaponTimer < 0)
{
ignoredWeapons.Clear();
@@ -168,17 +186,25 @@ namespace Barotrauma
{
findSafety.Priority = 0;
}
if (!character.IsOnPlayerTeam && !objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
{
distanceTimer -= deltaTime;
if (distanceTimer < 0)
{
distanceTimer = distanceCheckInterval;
sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
}
}
}
protected override bool Check()
{
if (IsOffensiveOrArrest && Mode != initialMode)
if (sqrDistance > maxDistance * maxDistance)
{
Abandon = true;
SteeringManager.Reset();
return false;
// The target escaped from us.
return true;
}
return IsEnemyDisabled || (!IsOffensiveOrArrest && coolDownTimer <= 0);
return IsEnemyDisabled || (AllowCoolDown && coolDownTimer <= 0);
}
protected override void Act(float deltaTime)
@@ -186,10 +212,9 @@ namespace Barotrauma
if (abortCondition != null && abortCondition())
{
Abandon = true;
SteeringManager.Reset();
return;
}
if (!IsOffensiveOrArrest)
if (AllowCoolDown)
{
coolDownTimer -= deltaTime;
}
@@ -199,7 +224,11 @@ namespace Barotrauma
{
OperateWeapon(deltaTime);
}
if (!HoldPosition && seekAmmunitionObjective == null && seekWeaponObjective == null)
if (HoldPosition)
{
SteeringManager.Reset();
}
else if (seekAmmunitionObjective == null && seekWeaponObjective == null)
{
Move(deltaTime);
}
@@ -431,7 +460,7 @@ namespace Barotrauma
priority /= 2;
}
}
if (Enemy.Stun > 1)
if (Enemy.IsKnockedDown)
{
// Enemy is stunned, reduce the priority of stunner weapons.
Attack attack = GetAttackDefinition(weapon);
@@ -621,7 +650,7 @@ namespace Barotrauma
var slots = Weapon.AllowedSlots.Where(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand));
if (character.Inventory.TryPutItem(Weapon, character, slots))
{
aimTimer = Rand.Range(0.5f, 1f);
aimTimer = Rand.Range(0.2f, 0.4f) / AimSpeed;
}
else
{
@@ -704,15 +733,12 @@ namespace Barotrauma
{
IgnoreIfTargetDead = true,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = Enemy.DisplayName
TargetName = Enemy.DisplayName,
AlwaysUseEuclideanDistance = false
},
onAbandon: () =>
{
Abandon = true;
SteeringManager.Reset();
});
onAbandon: () => Abandon = true);
if (followTargetObjective == null) { return; }
if (Mode == CombatMode.Arrest && Enemy.Stun > 2)
if (Mode == CombatMode.Arrest && (Enemy.Stun > 1 || Enemy.IsKnockedDown))
{
if (HumanAIController.HasItem(character, "handlocker", out _))
{
@@ -720,8 +746,8 @@ namespace Barotrauma
{
arrestingRegistered = true;
followTargetObjective.Completed += OnArrestTargetReached;
followTargetObjective.CloseEnough = 100;
}
followTargetObjective.CloseEnough = 100;
}
else
{
@@ -737,7 +763,7 @@ namespace Barotrauma
SteeringManager.Reset();
}
}
if (followTargetObjective != null)
if (!arrestingRegistered && followTargetObjective != null)
{
followTargetObjective.CloseEnough =
WeaponComponent is RangedWeapon ? 1000 :
@@ -760,7 +786,7 @@ namespace Barotrauma
private void OnArrestTargetReached()
{
if (HumanAIController.HasItem(character, "handlocker", out IEnumerable<Item> matchingItems) && Enemy.Stun > 0 && character.CanInteractWith(Enemy))
if (HumanAIController.HasItem(character, "handlocker", out IEnumerable<Item> matchingItems) && !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy))
{
var handCuffs = matchingItems.First();
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true))
@@ -780,8 +806,8 @@ namespace Barotrauma
}
}
character.Speak(TextManager.Get("DialogTargetArrested"), null, 3.0f, "targetarrested", 30.0f);
IsCompleted = true;
}
IsCompleted = true;
}
/// <summary>
@@ -818,25 +844,7 @@ namespace Barotrauma
if (WeaponComponent == null) { return false; }
if (Weapon.OwnInventory == null) { return true; }
// Eject empty ammo
if (Weapon.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f))
{
foreach (Item containedItem in Weapon.OwnInventory.AllItemsMod)
{
if (containedItem.Condition <= 0)
{
if (character.Submarine == null)
{
// If we are outside of main sub, try to put the ammo in the inventory instead dropping it in the sea.
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot))
{
continue;
}
}
containedItem.Drop(character);
}
}
}
HumanAIController.UnequipEmptyItems(Weapon);
RelatedItem item = null;
Item ammunition = null;
string[] ammunitionIdentifiers = null;
@@ -869,22 +877,13 @@ namespace Barotrauma
if (ammunition != null)
{
var container = Weapon.GetComponent<ItemContainer>();
if (container.Item.ParentInventory == character.Inventory)
if (!container.Inventory.TryPutItem(ammunition, null))
{
if (!container.Inventory.CanBePut(ammunition))
{
return false;
}
character.Inventory.RemoveItem(ammunition);
if (!container.Inventory.TryPutItem(ammunition, null))
if (ammunition.ParentInventory == character.Inventory)
{
ammunition.Drop(character);
}
}
else
{
container.Combine(ammunition, character);
}
}
}
}
@@ -902,6 +901,15 @@ namespace Barotrauma
private void Attack(float deltaTime)
{
character.CursorPosition = Enemy.WorldPosition;
if (AimAccuracy < 1)
{
spreadTimer += deltaTime * Rand.Range(0.01f, 1f);
float shake = Rand.Range(0.95f, 1.05f);
float offsetAmount = (1 - AimAccuracy) * Rand.Range(300f, 500f);
float distanceFactor = MathUtils.InverseLerp(0, 1000 * 1000, sqrDistance);
float offset = (float)Math.Sin(spreadTimer * shake) * offsetAmount * distanceFactor;
character.CursorPosition += new Vector2(0, offset);
}
if (character.Submarine != null)
{
character.CursorPosition -= character.Submarine.Position;
@@ -912,7 +920,11 @@ namespace Barotrauma
canSeeTarget = character.CanSeeTarget(Enemy);
visibilityCheckTimer = visibilityCheckInterval;
}
if (!canSeeTarget) { return; }
if (!canSeeTarget)
{
aimTimer = Rand.Range(0.2f, 0.4f) / AimSpeed;
return;
}
if (Weapon.RequireAimToUse)
{
character.SetInput(InputType.Aim, false, true);
@@ -928,7 +940,15 @@ namespace Barotrauma
aimTimer -= deltaTime;
return;
}
if (Mode == CombatMode.Arrest && isLethalWeapon && Enemy.Stun > 1) { return; }
if (reloadTimer > 0) { return; }
if (Mode == CombatMode.Arrest)
{
// If the target is arrested or if it's stunned and we can't lock the target up, consider the objective done.
if (Enemy.IsKnockedDown && !HumanAIController.HasItem(character, "handlocker", out _, requireEquipped: false) || HumanAIController.HasItem(Enemy, "handlocker", out _, requireEquipped: true))
{
IsCompleted = true;
}
}
if (holdFireCondition != null && holdFireCondition()) { return; }
float sqrDist = Vector2.DistanceSquared(character.Position, Enemy.Position);
if (WeaponComponent is MeleeWeapon meleeWeapon)
@@ -963,14 +983,12 @@ namespace Barotrauma
}
if (closeEnough)
{
SteeringManager.Reset();
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
UseWeapon(deltaTime);
}
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);
aimTimer = Rand.Range(1f, 1.5f) / AimSpeed;
}
}
else
@@ -979,14 +997,15 @@ namespace Barotrauma
{
if (sqrDist > repairTool.Range * repairTool.Range) { return; }
}
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4)
float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4 + aimFactor)
{
if (myBodies == null)
{
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
}
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories);
var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories, allowInsideFixture: true);
if (pickedBody != null)
{
Character target = null;
@@ -1000,31 +1019,62 @@ namespace Barotrauma
}
if (target != null && (target == Enemy || !HumanAIController.IsFriendly(target)))
{
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
float reloadTime = 0;
if (WeaponComponent is RangedWeapon rangedWeapon)
{
reloadTime = rangedWeapon.Reload;
}
if (WeaponComponent is MeleeWeapon mw)
{
reloadTime = mw.Reload;
}
aimTimer = reloadTime * Rand.Range(1f, 1.5f);
UseWeapon(deltaTime);
}
}
}
}
}
private void UseWeapon(float deltaTime)
{
// Never allow to attack characters with deadly weapons while trying to arrest.
if (Mode == CombatMode.Arrest && isLethalWeapon) { return; }
float reloadTime = 0;
if (WeaponComponent is RangedWeapon rangedWeapon)
{
// If the weapon is just equipped, we can't shoot just yet.
if (rangedWeapon.ReloadTimer <= 0)
{
reloadTime = rangedWeapon.Reload;
}
}
if (WeaponComponent is MeleeWeapon mw)
{
if (!((HumanoidAnimController)character.AnimController).Crouching)
{
reloadTime = mw.Reload;
}
}
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed);
}
private bool ShouldUnequipWeapon =>
Weapon != null &&
character.Submarine != null &&
character.Submarine.TeamID == character.TeamID &&
Character.CharacterList.None(c => c.Submarine == character.Submarine && HumanAIController.IsActive(c) && !HumanAIController.IsFriendly(character, c) && HumanAIController.VisibleHulls.Contains(c.CurrentHull));
protected override void OnCompleted()
{
base.OnCompleted();
if (Weapon != null)
if (ShouldUnequipWeapon)
{
Unequip();
}
SteeringManager.Reset();
}
protected override void OnAbandon()
{
base.OnAbandon();
if (ShouldUnequipWeapon)
{
Unequip();
}
SteeringManager.Reset();
}
public override void Reset()
@@ -34,6 +34,10 @@ namespace Barotrauma
public float ConditionLevel { get; set; } = 1;
public bool Equip { get; set; }
public bool RemoveEmpty { get; set; } = true;
public bool RemoveExisting { get; set; }
public bool MoveWholeStack { get; set; }
public AIObjectiveContainItem(Character character, Item item, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -102,47 +106,38 @@ namespace Barotrauma
}
if (character.CanInteractWith(container.Item, checkLinked: false))
{
if (RemoveEmpty && container.Inventory.AllItems.Any(it => it.Condition <= 0.0f))
if (RemoveExisting)
{
foreach (var emptyItem in container.Inventory.AllItemsMod)
{
if (emptyItem.Condition <= 0)
{
emptyItem.Drop(character);
}
}
HumanAIController.UnequipContainedItems(container.Item);
}
// Contain the item
if (ItemToContain.ParentInventory == character.Inventory)
else if (RemoveEmpty)
{
if (!container.Inventory.CanBePut(ItemToContain))
HumanAIController.UnequipEmptyItems(container.Item);
}
Inventory originalInventory = ItemToContain.ParentInventory;
var slots = originalInventory?.FindIndices(ItemToContain);
if (container.Inventory.TryPutItem(ItemToContain, null))
{
if (MoveWholeStack && slots != null)
{
Abandon = true;
}
else
{
character.Inventory.RemoveItem(ItemToContain);
if (container.Inventory.TryPutItem(ItemToContain, null))
foreach (int slot in slots)
{
IsCompleted = true;
}
else
{
ItemToContain.Drop(character);
Abandon = true;
foreach (Item item in originalInventory.GetItemsAt(slot).ToList())
{
container.Inventory.TryPutItem(item, null);
}
}
IsCompleted = true;
}
}
else
{
if (container.Combine(ItemToContain, character))
if (ItemToContain.ParentInventory == character.Inventory && character.Submarine == Submarine.MainSub)
{
IsCompleted = true;
}
else
{
Abandon = true;
ItemToContain.Drop(character);
}
Abandon = true;
}
}
else
@@ -151,7 +146,8 @@ namespace Barotrauma
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = container.Item.Name,
abortCondition = () => !ItemToContain.IsOwnedBy(character)
abortCondition = obj => !ItemToContain.IsOwnedBy(character),
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>()
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
@@ -22,8 +22,13 @@ namespace Barotrauma
public AIObjectiveGetItem GetItemObjective => getItemObjective;
public AIObjectiveContainItem ContainObjective => containObjective;
public Item TargetItem => targetItem;
public ItemContainer TargetContainer => targetContainer;
public bool Equip { get; set; }
public bool TakeWholeStack { get; set; }
/// <summary>
/// If true drops the item when containing the item fails.
/// In both cases abandons the objective.
@@ -90,7 +95,7 @@ namespace Barotrauma
if (getItemObjective == null && !itemToDecontain.IsOwnedBy(character))
{
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip),
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip) { TakeWholeStack = this.TakeWholeStack },
onAbandon: () => Abandon = true);
return;
}
@@ -99,6 +104,7 @@ namespace Barotrauma
TryAddSubObjective(ref containObjective,
constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager)
{
MoveWholeStack = TakeWholeStack,
Equip = Equip,
RemoveEmpty = false,
GetItemPriority = GetItemPriority,
@@ -35,7 +35,7 @@ namespace Barotrauma
Abandon = true;
return Priority;
}
bool isOrder = objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>();
bool isOrder = objectiveManager.HasOrder<AIObjectiveExtinguishFires>();
if (!isOrder && Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
// Don't go into rooms with any enemies, unless it's an order
@@ -78,7 +78,7 @@ namespace Barotrauma
{
TryAddSubObjective(ref getExtinguisherObjective, () =>
{
if (!character.HasEquippedItem("fireextinguisher", allowBroken: false))
if (character.IsOnPlayerTeam && !character.HasEquippedItem("fireextinguisher", allowBroken: false))
{
character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f);
}
@@ -88,7 +88,7 @@ namespace Barotrauma
// If the item is inside an unsafe hull, decrease the priority
GetItemPriority = i => HumanAIController.UnsafeHulls.Contains(i.CurrentHull) ? 0.1f : 1
};
if (objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>())
if (objectiveManager.HasOrder<AIObjectiveExtinguishFires>())
{
getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindfireextinguisher"), null, 0.0f, "dialogcannotfindfireextinguisher", 10.0f);
};
@@ -42,6 +42,15 @@ namespace Barotrauma
if (hull.Submarine == null) { return false; }
if (character.Submarine == null) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(hull, includingConnectedSubs: true)) { return false; }
if (hull.BallastFlora != null) { return false; }
foreach (var ballastFlora in MapCreatures.Behavior.BallastFloraBehavior.EntityList)
{
if (ballastFlora.Parent?.Submarine != character.Submarine) { continue; }
if (ballastFlora.Branches.Any(b => !b.Removed && b.Health > 0 && b.CurrentHull == hull))
{
return false;
}
}
return true;
}
}
@@ -10,6 +10,8 @@ namespace Barotrauma
protected override float IgnoreListClearInterval => 30;
public override bool IgnoreUnsafeHulls => true;
protected override float TargetUpdateTimeMultiplier => 0.2f;
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
@@ -48,16 +50,14 @@ namespace Barotrauma
public static bool IsValidTarget(Character target, Character character)
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (target == null || target.Removed) { return false; }
if (target.IsDead || target.IsUnconscious) { return false; }
if (target == character) { return false; }
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (target.Submarine == null) { return false; }
if (target.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine == null) { return false; }
if (target.CurrentHull == null) { return false; }
if (character.Submarine != null)
{
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
}
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
return true;
}
}
@@ -1,6 +1,7 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -37,11 +38,11 @@ namespace Barotrauma
return;
}
targetItem = character.Inventory.FindItemByTag(gearTag, true);
if (targetItem == null || !character.HasEquippedItem(targetItem))
if (targetItem == null || !character.HasEquippedItem(targetItem) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0))
{
TryAddSubObjective(ref getDivingGear, () =>
{
if (targetItem == null)
if (targetItem == null && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f);
}
@@ -57,46 +58,78 @@ namespace Barotrauma
}
else
{
if (!EjectEmptyTanks(character, targetItem, out var containedItems))
HumanAIController.UnequipContainedItems(targetItem, it => !it.HasTag("oxygensource"));
HumanAIController.UnequipEmptyItems(targetItem);
// Seek oxygen that has at least 10% condition left, if we are inside a friendly sub.
// The margin helps us to survive, because we might need some oxygen before we can find more oxygen.
// When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks.
float min = character.Submarine != Submarine.MainSub ? 0.01f : MIN_OXYGEN;
if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > min))
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + targetItem + "\" has no proper inventory");
#endif
Abandon = true;
return;
}
if (containedItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > MIN_OXYGEN))
{
// No valid oxygen source loaded.
// Seek oxygen that has min 10% condition left.
TryAddSubObjective(ref getOxygen, () =>
{
if (!HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: 10))
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
if (HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: min))
{
character.Speak(TextManager.Get("dialogswappingoxygentank"), null, 0, "swappingoxygentank", 30.0f);
}
else
{
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
}
}
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
ConditionLevel = MIN_OXYGEN
ConditionLevel = MIN_OXYGEN,
RemoveExisting = true
};
},
onAbandon: () =>
{
// Try to seek any oxygen sources.
getOxygen = null;
int remainingTanks = ReportOxygenTankCount();
// Try to seek any oxygen sources, even if they have minimal amount of oxygen.
TryAddSubObjective(ref getOxygen, () =>
{
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
AllowToFindDivingGear = false,
AllowDangerousPressure = true
AllowDangerousPressure = true,
RemoveExisting = true
};
},
onAbandon: () => Abandon = true,
onAbandon: () =>
{
Abandon = true;
if (remainingTanks > 0 && !HumanAIController.HasItem(character, "oxygensource", out _, conditionPercentage: 0.01f))
{
character.Speak(TextManager.Get("dialogcantfindtoxygen"), null, 0, "cantfindoxygen", 30.0f);
}
},
onCompleted: () => RemoveSubObjective(ref getOxygen));
},
onCompleted: () => RemoveSubObjective(ref getOxygen));
onCompleted: () =>
{
RemoveSubObjective(ref getOxygen);
ReportOxygenTankCount();
});
int ReportOxygenTankCount()
{
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("oxygensource") && i.Condition > 1);
if (remainingOxygenTanks == 0)
{
character.Speak(TextManager.Get("DialogOutOfOxygenTanks"), null, 0.0f, "outofoxygentanks", 30.0f);
}
else if (remainingOxygenTanks < 10)
{
character.Speak(TextManager.Get("DialogLowOnOxygenTanks"), null, 0.0f, "lowonoxygentanks", 30.0f);
}
return remainingOxygenTanks;
}
}
}
}
@@ -108,21 +141,7 @@ namespace Barotrauma
{
containedItems = target.OwnInventory?.AllItems;
if (containedItems == null) { return false; }
foreach (Item containedItem in target.OwnInventory.AllItemsMod)
{
if (containedItem.Condition <= 0.0f)
{
if (actor.Submarine == null)
{
// If we are outside of main sub, try to put the tank in the inventory instead dropping it in the sea.
if (actor.Inventory.TryPutItem(containedItem, actor, CharacterInventory.anySlot))
{
continue;
}
}
containedItem.Drop(actor);
}
}
AIController.UnequipEmptyItems(actor, target);
return true;
}
@@ -46,19 +46,27 @@ namespace Barotrauma
}
if (character.CurrentHull == null)
{
Priority = (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.Objectives.Any(o => o is AIObjectiveCombat)) && HumanAIController.HasDivingSuit(character) ? 0 : 100;
Priority = (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.HasActiveObjective<AIObjectiveCombat>()) && HumanAIController.HasDivingSuit(character) ? 0 : 100;
}
else
{
if (HumanAIController.NeedsDivingGear(character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character))
if (HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) &&
(needsSuit ?
!HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.MIN_OXYGEN) :
!HumanAIController.HasDivingMask(character, conditionPercentage: AIObjectiveFindDivingGear.MIN_OXYGEN)))
{
Priority = 100;
}
else if (objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() && character.Submarine != null && !HumanAIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID))
{
// Ordered to follow/hold position inside a hostile sub -> ignore find safety unless we need to find a diving gear
Priority = 0;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
{
// Boost the priority while seeking the diving gear
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.OrderPriority + 20, 100));
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.HighestOrderPriority + 20, 100));
}
}
return Priority;
@@ -38,7 +38,7 @@ namespace Barotrauma
Priority = 0;
Abandon = true;
}
else if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective<AIObjectiveFixLeak>()?.Leak == Leak))
else if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.Character.IsBot && other.ObjectiveManager.GetActiveObjective<AIObjectiveFixLeak>()?.Leak == Leak))
{
Priority = 0;
Abandon = true;
@@ -52,7 +52,7 @@ namespace Barotrauma
float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f));
float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float reduction = isPriority ? 1 : 2;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90);
float max = AIObjectiveManager.LowestOrderPriority - reduction;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
@@ -67,7 +67,7 @@ namespace Barotrauma
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
onAbandon: () =>
{
if (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>())
if (character.IsOnPlayerTeam && objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>())
{
character.Speak(TextManager.Get("dialogcannotfindweldingequipment"), null, 0.0f, "dialogcannotfindweldingequipment", 10.0f);
}
@@ -86,23 +86,34 @@ namespace Barotrauma
Abandon = true;
return;
}
// Drop empty tanks
if (weldingTool.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f))
HumanAIController.UnequipContainedItems(weldingTool, it => !it.HasTag("weldingfuel"));
HumanAIController.UnequipEmptyItems(weldingTool);
if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
{
foreach (Item containedItem in weldingTool.OwnInventory.AllItemsMod)
{
if (containedItem.Condition <= 0.0f)
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
onAbandon: () =>
{
containedItem.Drop(character);
Abandon = true;
ReportWeldingFuelTankCount();
},
onCompleted: () =>
{
RemoveSubObjective(ref refuelObjective);
ReportWeldingFuelTankCount();
});
void ReportWeldingFuelTankCount()
{
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("weldingfuel") && i.Condition > 1);
if (remainingOxygenTanks == 0)
{
character.Speak(TextManager.Get("DialogOutOfWeldingFuel"), null, 0.0f, "outofweldingfuel", 30.0f);
}
else if (remainingOxygenTanks < 4)
{
character.Speak(TextManager.Get("DialogLowOnWeldingFuel"), null, 0.0f, "lowonweldingfuel", 30.0f);
}
}
}
if (weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
{
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref refuelObjective));
return;
}
}
@@ -42,7 +42,7 @@ namespace Barotrauma
if (totalLeaks == 0) { return 0; }
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated, onlyBots: true);
bool anyFixers = otherFixers > 0;
if (objectiveManager.CurrentOrder == this)
if (objectiveManager.IsOrder(this))
{
float ratio = anyFixers ? totalLeaks / (float)otherFixers : 1;
return Targets.Sum(t => GetLeakSeverity(t)) * ratio;
@@ -72,7 +72,11 @@ namespace Barotrauma
{
if (gap == null) { return false; }
// Don't fix a leak on a wall section set to be ignored
if (gap.ConnectedWall?.Sections?.Any(s => s.gap == gap && s.IgnoreByAI) ?? false) { return false; }
if (gap.ConnectedWall != null)
{
if (gap.ConnectedWall.Sections.Any(s => s.gap == gap && s.IgnoreByAI)) { return false; }
if (gap.ConnectedWall.MaxHealth <= 0.0f) { return false; }
}
if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; }
if (gap.Submarine == null || character.Submarine == null) { return false; }
// Don't allow going into another sub, unless it's connected and of the same team and type.
@@ -10,6 +10,8 @@ namespace Barotrauma
{
public override string DebugTag => "get item";
public override bool AbandonWhenCannotCompleteSubjectives => false;
private readonly bool equip;
public HashSet<Item> ignoredItems = new HashSet<Item>();
@@ -44,6 +46,8 @@ namespace Barotrauma
/// </summary>
public bool AllowStealing { get; set; }
public bool TakeWholeStack { get; set; }
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
@@ -191,8 +195,20 @@ namespace Barotrauma
return;
}
Inventory itemInventory = targetItem.ParentInventory;
var slots = itemInventory?.FindIndices(targetItem);
if (HumanAIController.TakeItem(targetItem, character.Inventory, equip, storeUnequipped: true))
{
if (TakeWholeStack && slots != null)
{
foreach (int slot in slots)
{
foreach (Item item in itemInventory.GetItemsAt(slot).ToList())
{
HumanAIController.TakeItem(item, character.Inventory, equip: false, storeUnequipped: true);
}
}
}
IsCompleted = true;
}
else
@@ -211,9 +227,8 @@ namespace Barotrauma
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear, closeEnough: DefaultReach)
{
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
abortCondition = () => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget,
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = (moveToTarget as MapEntity)?.Name ?? (moveToTarget as Character)?.Name ?? moveToTarget.ToString()
abortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget,
SpeakIfFails = false
};
},
onAbandon: () =>
@@ -240,13 +255,18 @@ namespace Barotrauma
if (targetItem == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item, because neither identifiers nor item was defined.", Color.Red);
DebugConsole.NewMessage($"{character.Name}: Cannot find an item, because neither identifiers nor item was defined.", Color.Red);
#endif
Abandon = true;
}
return;
}
for (int i = 0; i < 10 && currSearchIndex < Item.ItemList.Count - 1; i++)
float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100);
bool checkPath = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.followControlledCharacter);
bool hasCalledPathFinder = false;
int itemsPerFrame = (int)priority;
for (int i = 0; i < itemsPerFrame && currSearchIndex < Item.ItemList.Count - 1; i++)
{
currSearchIndex++;
var item = Item.ItemList[currSearchIndex];
@@ -259,9 +279,13 @@ namespace Barotrauma
if (character.TeamID == CharacterTeamType.FriendlyNPC != item.SpawnedInOutpost) { continue; }
}
if (!CheckItem(item)) { continue; }
if (ignoredContainerIdentifiers != null && item.Container != null)
if (item.Container != null)
{
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
if (item.Container.HasTag("donttakeitems")) { continue; }
if (ignoredContainerIdentifiers != null)
{
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
}
}
// Don't allow going into another sub, unless it's connected and of the same team and type.
if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { continue; }
@@ -287,8 +311,18 @@ namespace Barotrauma
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 10000, dist));
itemPriority *= distanceFactor;
itemPriority *= item.Condition / item.MaxCondition;
//ignore if the item has a lower priority than the currently selected one
// Ignore if the item has a lower priority than the currently selected one
if (itemPriority < currItemPriority) { continue; }
if (!hasCalledPathFinder && PathSteering != null && checkPath)
{
// While following the player, let's ensure that there's a valid path to the target before accepting it.
// Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can.
// This is relatively expensive, so let's do this only when it significantly improves the behavior.
// Only allow one path find call per frame.
hasCalledPathFinder = true;
var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable) { continue; }
}
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootInventoryOwner ?? item;
@@ -303,7 +337,7 @@ namespace Barotrauma
if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && identifiersOrTags.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab))
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
#endif
Abandon = true;
}
@@ -322,8 +356,9 @@ namespace Barotrauma
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", identifiersOrTags)}", Color.Yellow);
#endif
SpeakCannotFind();
Abandon = true;
}
}
@@ -370,11 +405,48 @@ namespace Barotrauma
/// </summary>
private void ResetInternal()
{
goToObjective = null;
RemoveSubObjective(ref goToObjective);
targetItem = originalTarget;
moveToTarget = targetItem?.GetRootInventoryOwner();
isDoneSeeking = false;
currSearchIndex = 0;
currItemPriority = 0;
}
protected override void OnAbandon()
{
base.OnAbandon();
if (moveToTarget == null) { return; }
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow);
#endif
}
private void SpeakCannotFind()
{
// TODO: Use the item name as the variable here.
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective)
{
string msg = TextManager.Get("dialogcannotfinditem", true);
if (msg != null)
{
character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f);
}
}
}
// TODO: remove?
private void SpeakCannotReach()
{
if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective)
{
string TargetName = (moveToTarget as MapEntity)?.Name ?? (moveToTarget as Character)?.Name ?? moveToTarget.ToString();
string msg = TargetName == null ? TextManager.Get("dialogcannotreachtarget", true) : TextManager.GetWithVariable("dialogcannotreachtarget", "[name]", TargetName, formatCapitals: !(moveToTarget is Character));
if (msg != null)
{
character.Speak(msg, identifier: "dialogcannotreachtarget", minDurationBetweenSimilar: 20.0f);
}
}
}
}
}
@@ -23,13 +23,14 @@ namespace Barotrauma
/// <summary>
/// Aborts the objective when this condition is true
/// </summary>
public Func<bool> abortCondition;
public Func<AIObjectiveGoTo, bool> abortCondition;
public Func<PathNode, bool> endNodeFilter;
public Func<float> priorityGetter;
public bool followControlledCharacter;
public bool mimic;
public bool SpeakIfFails { get; set; } = true;
public float extraDistanceWhileSwimming;
public float extraDistanceOutsideSub;
@@ -66,6 +67,8 @@ namespace Barotrauma
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
public bool AlwaysUseEuclideanDistance { get; set; } = true;
public override bool AbandonWhenCannotCompleteSubjectives => !repeat;
public override bool AllowOutsideSubmarine => AllowGoingOutside;
@@ -80,19 +83,14 @@ namespace Barotrauma
public override float GetPriority()
{
bool isOrder = objectiveManager.CurrentOrder == this;
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed)
{
Priority = 0;
Abandon = !isOrder;
return Priority;
}
if (followControlledCharacter && Character.Controlled == null)
{
Priority = 0;
Abandon = !isOrder;
}
if (Target is Entity e && e.Removed)
if (Target == null || Target is Entity e && e.Removed)
{
Priority = 0;
Abandon = !isOrder;
@@ -114,7 +112,7 @@ namespace Barotrauma
}
else
{
Priority = isOrder ? AIObjectiveManager.OrderPriority : 10;
Priority = isOrder ? objectiveManager.GetOrderPriority(this) : 10;
}
}
return Priority;
@@ -149,7 +147,7 @@ namespace Barotrauma
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow);
#endif
if (objectiveManager.CurrentOrder != null && DialogueIdentifier != null)
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)
@@ -163,13 +161,15 @@ namespace Barotrauma
{
if (followControlledCharacter)
{
if (Character.Controlled == null)
if (Character.Controlled != null && HumanAIController.IsFriendly(Character.Controlled))
{
Target = Character.Controlled;
}
if (Target == null)
{
Abandon = true;
SteeringManager.Reset();
return;
}
Target = Character.Controlled;
}
if (Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy))
{
@@ -187,7 +187,6 @@ namespace Barotrauma
if (e.Removed)
{
Abandon = true;
SteeringManager.Reset();
return;
}
else
@@ -199,7 +198,7 @@ namespace Barotrauma
if (!followControlledCharacter)
{
// Abandon if going through unsafe paths. Note ignores unsafe nodes when following an order or when the objective is set to ignore unsafe hulls.
bool containsUnsafeNodes = HumanAIController.CurrentOrder == null && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls
bool containsUnsafeNodes = character.IsDismissed && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls
&& PathSteering != null && PathSteering.CurrentPath != null
&& PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
if (containsUnsafeNodes || HumanAIController.UnreachableHulls.Contains(targetHull))
@@ -249,16 +248,18 @@ namespace Barotrauma
}
}
bool needsEquipment = false;
float minOxygen = character.Submarine == null ? 0 : AIObjectiveFindDivingGear.MIN_OXYGEN;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.MIN_OXYGEN);
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.MIN_OXYGEN);
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
if (needsEquipment)
{
SteeringManager.Reset();
if (findDivingGear != null && !findDivingGear.CanBeCompleted)
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
@@ -287,9 +288,14 @@ namespace Barotrauma
}
}
}
float maxGapDistance = 500;
Character targetCharacter = Target as Character;
if (character.AnimController.InWater)
{
if (character.CurrentHull == null)
if (character.CurrentHull == null ||
followControlledCharacter &&
targetCharacter != null && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null) &&
Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) < maxGapDistance * maxGapDistance)
{
if (seekGapsTimer > 0)
{
@@ -297,7 +303,7 @@ namespace Barotrauma
}
else
{
SeekGaps(maxDistance: 500);
SeekGaps(maxGapDistance);
seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
if (TargetGap != null)
{
@@ -326,7 +332,7 @@ namespace Barotrauma
}
if (TargetGap != null)
{
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, TargetGap.FlowTargetHull.WorldPosition, deltaTime))
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, followControlledCharacter ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime))
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
return;
@@ -346,7 +352,7 @@ namespace Barotrauma
float closeEnough = 250;
float squaredDistance = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition);
bool shouldUseScooter = squaredDistance > closeEnough * closeEnough && (!mimic ||
(Target is Character targetCharacter && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2));
(targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false)) || squaredDistance > Math.Pow(closeEnough * 2, 2));
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
{
// Currently equipped scooter
@@ -527,17 +533,24 @@ namespace Barotrauma
{
Gap selectedGap = null;
float selectedDistance = -1;
Vector2 toTargetNormalized = Vector2.Normalize(Target.WorldPosition - character.WorldPosition);
foreach (Gap gap in Gap.GapList)
{
if (gap.Open < 1) { continue; }
if (gap.FlowTargetHull == null) { continue; }
if (gap.Submarine != Target.Submarine) { continue; }
float distance = Vector2.DistanceSquared(character.WorldPosition, gap.WorldPosition);
if (distance > maxDistance * maxDistance) { continue; }
if (selectedGap == null || distance < selectedDistance)
if (gap.Submarine == null) { continue; }
if (!followControlledCharacter)
{
if (gap.FlowTargetHull == null) { continue; }
if (gap.Submarine != Target.Submarine) { continue; }
}
Vector2 toGap = gap.WorldPosition - character.WorldPosition;
if (Vector2.Dot(Vector2.Normalize(toGap), toTargetNormalized) < 0) { continue; }
float squaredDistance = toGap.LengthSquared();
if (squaredDistance > maxDistance * maxDistance) { continue; }
if (selectedGap == null || squaredDistance < selectedDistance)
{
selectedGap = gap;
selectedDistance = distance;
selectedDistance = squaredDistance;
}
}
TargetGap = selectedGap;
@@ -554,6 +567,13 @@ namespace Barotrauma
//otherwise characters can let go of the ladders too soon once they're close enough to the target
if (PathSteering.CurrentPath.NextNode != null) { return false; }
}
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;
}
return Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough;
}
}
@@ -569,7 +589,7 @@ namespace Barotrauma
Abandon = true;
return false;
}
if (abortCondition != null && abortCondition())
if (abortCondition != null && abortCondition(this))
{
Abandon = true;
return false;
@@ -617,7 +637,7 @@ namespace Barotrauma
private void StopMovement()
{
character.AIController.SteeringManager.Reset();
SteeringManager.Reset();
if (Target != null)
{
character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left;
@@ -21,9 +21,9 @@ namespace Barotrauma
set
{
behavior = value;
if (behavior == BehaviorType.StayInHull && character.TeamID != CharacterTeamType.FriendlyNPC)
if (behavior == BehaviorType.StayInHull && TargetHull == null)
{
DebugConsole.NewMessage($"AIObjectiveIdle.BehaviorType.StayInHull is implemented only for outpost NPCs. Using passive behavior for {character.Name} ({character.Info.Job.Prefab.Identifier})", color: Color.Red);
DebugConsole.AddWarning($"Trying to set a character's behavior type to StayInHull, but target hull is not set. {character.Name} ({character.Info.Job.Prefab.Identifier})");
behavior = BehaviorType.Passive;
}
switch (behavior)
@@ -495,7 +495,7 @@ namespace Barotrauma
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true) && !ignoredItems.Contains(item))
if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true, allowUnloading: false) && !ignoredItems.Contains(item))
{
itemsToClean.Add(item);
}
@@ -540,5 +540,17 @@ namespace Barotrauma
ignoredItems.Clear();
autonomousObjectiveRetryTimer = 10;
}
public override void OnDeselected()
{
base.OnDeselected();
foreach (var subObjective in SubObjectives)
{
if (subObjective is AIObjectiveCleanupItem cleanUpObjective)
{
cleanUpObjective.DropTarget();
}
}
}
}
}
@@ -11,6 +11,7 @@ namespace Barotrauma
protected HashSet<T> ignoreList = new HashSet<T>();
private float ignoreListTimer;
protected float targetUpdateTimer;
protected virtual float TargetUpdateTimeMultiplier { get; } = 1;
private float syncTimer;
private readonly float syncTime = 1;
@@ -61,7 +62,7 @@ namespace Barotrauma
ignoreListTimer += deltaTime;
}
}
if (targetUpdateTimer < 0)
if (targetUpdateTimer <= 0)
{
UpdateTargets();
}
@@ -69,9 +70,9 @@ namespace Barotrauma
{
targetUpdateTimer -= deltaTime;
}
if (syncTimer < 0)
if (syncTimer <= 0)
{
syncTimer = syncTime * Rand.Range(0.9f, 1.1f);
syncTimer = Math.Min(syncTime * Rand.Range(0.9f, 1.1f), targetUpdateTimer);
// Sync objectives, subobjectives and targets
foreach (var objective in Objectives)
{
@@ -95,7 +96,7 @@ namespace Barotrauma
}
// the timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25%
private float SetTargetUpdateTimer() => targetUpdateTimer = 1 / MathHelper.Clamp(PriorityModifier * Rand.Range(0.75f, 1.25f), 0.1f, 1);
private float CalculateTargetUpdateTimer() => targetUpdateTimer = 1 / MathHelper.Clamp(PriorityModifier * Rand.Range(0.75f, 1.25f), 0.1f, 1) * TargetUpdateTimeMultiplier;
public override void Reset()
{
@@ -139,13 +140,13 @@ namespace Barotrauma
}
else
{
if (objectiveManager.CurrentOrder == this)
if (objectiveManager.IsOrder(this))
{
Priority = ForceOrderPriority ? AIObjectiveManager.OrderPriority : targetValue;
Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetValue;
}
else
{
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
float max = AIObjectiveManager.LowestOrderPriority - 1;
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
Priority = MathHelper.Lerp(0, max, value);
}
@@ -156,7 +157,7 @@ namespace Barotrauma
protected void UpdateTargets()
{
SetTargetUpdateTimer();
CalculateTargetUpdateTimer();
Targets.Clear();
FindTargets();
CreateObjectives();
@@ -167,7 +168,7 @@ namespace Barotrauma
foreach (T target in GetList())
{
// The bots always find targets when the objective is an order.
if (objectiveManager.CurrentOrder != this)
if (!objectiveManager.IsOrder(this))
{
// Battery or pump states cannot currently be reported (not implemented) and therefore we must ignore them -> the bots always know if they require attention.
bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater;
@@ -1,6 +1,6 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Barotrauma.Networking; // used by the server
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -10,8 +10,8 @@ namespace Barotrauma
{
class AIObjectiveManager
{
// TODO: expose
public const float OrderPriority = 70;
public const float HighestOrderPriority = 70;
public const float LowestOrderPriority = 60;
public const float RunPriority = 50;
// Constantly increases the priority of the selected objective, unless overridden
public const float baseDevotion = 5;
@@ -25,7 +25,6 @@ namespace Barotrauma
public HumanAIController HumanAIController => character.AIController as HumanAIController;
private float _waitTimer;
/// <summary>
/// When set above zero, the character will stand still doing nothing until the timer runs out. Does not affect orders, find safety or combat.
@@ -39,26 +38,25 @@ namespace Barotrauma
}
}
public AIObjective CurrentOrder { get; private set; }
public List<OrderInfo> CurrentOrders { get; } = new List<OrderInfo>();
/// <summary>
/// The AIObjective in <see cref="CurrentOrders"/> with the highest <see cref="AIObjective.Priority"/>
/// </summary>
public AIObjective CurrentOrder
{
get
{
return ForcedOrder ?? currentOrder;
}
private set
{
currentOrder = value;
}
}
private AIObjective currentOrder;
public AIObjective ForcedOrder { get; private set; }
public AIObjective CurrentObjective { get; private set; }
public bool IsCurrentOrder<T>() where T : AIObjective => CurrentOrder is T;
public bool IsCurrentObjective<T>() where T : AIObjective => CurrentObjective is T;
public bool IsActiveObjective<T>() where T : AIObjective => GetActiveObjective() is T;
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
/// <summary>
/// Returns the last active objective of the specific type.
/// </summary>
public T GetActiveObjective<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
/// <summary>
/// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently.
/// </summary>
public IEnumerable<T> GetActiveObjectives<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T);
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
public AIObjectiveManager(Character character)
{
this.character = character;
@@ -134,7 +132,13 @@ namespace Barotrauma
}
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
if (order == null) { continue; }
if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC) { continue; }
if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
{
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
{
continue;
}
}
var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier);
if (objective != null && objective.CanBeCompleted)
{
@@ -162,7 +166,11 @@ namespace Barotrauma
coroutine = CoroutineManager.InvokeAfter(() =>
{
//round ended before the coroutine finished
#if CLIENT
if (GameMain.GameSession == null || Level.Loaded == null && !(GameMain.GameSession.GameMode is TestGameMode)) { return; }
#else
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
#endif
DelayedObjectives.Remove(objective);
AddObjective(objective);
callback?.Invoke();
@@ -200,21 +208,34 @@ namespace Barotrauma
public void UpdateObjectives(float deltaTime)
{
if (CurrentOrder != null)
UpdateOrderObjective(ForcedOrder);
if (CurrentOrders.Any())
{
foreach(var order in CurrentOrders)
{
var orderObjective = order.Objective;
UpdateOrderObjective(orderObjective);
}
}
void UpdateOrderObjective(AIObjective orderObjective)
{
if (orderObjective == null) { return; }
#if DEBUG
// Note: don't automatically remove orders here. Removing orders needs to be done via dismissing.
if (CurrentOrder.IsCompleted)
if (orderObjective.IsCompleted)
{
DebugConsole.NewMessage($"{character.Name}: ORDER {CurrentOrder.DebugTag} IS COMPLETED. CURRENTLY ALL ORDERS SHOULD BE LOOPING.", Color.Red);
DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag} IS COMPLETED. CURRENTLY ALL ORDERS SHOULD BE LOOPING.", Color.Red);
}
else if (!CurrentOrder.CanBeCompleted)
else if (!orderObjective.CanBeCompleted)
{
DebugConsole.NewMessage($"{character.Name}: ORDER {CurrentOrder.DebugTag}, CANNOT BE COMPLETED.", Color.Red);
DebugConsole.NewMessage($"{character.Name}: ORDER {orderObjective.DebugTag}, CANNOT BE COMPLETED.", Color.Red);
}
#endif
CurrentOrder.Update(deltaTime);
orderObjective.Update(deltaTime);
}
if (WaitTimer > 0)
{
WaitTimer -= deltaTime;
@@ -248,7 +269,29 @@ namespace Barotrauma
public void SortObjectives()
{
CurrentOrder?.GetPriority();
ForcedOrder?.GetPriority();
AIObjective orderWithHighestPriority = null;
float highestPriority = 0;
foreach (var currentOrder in CurrentOrders)
{
var orderObjective = currentOrder.Objective;
if (orderObjective == null) { continue; }
orderObjective.GetPriority();
if (orderWithHighestPriority == null || orderObjective.Priority > highestPriority)
{
orderWithHighestPriority = orderObjective;
highestPriority = orderObjective.Priority;
}
}
#if SERVER
if (orderWithHighestPriority != null && orderWithHighestPriority != currentOrder)
{
GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.ObjectiveManagerOrderState });
}
#endif
CurrentOrder = orderWithHighestPriority;
for (int i = Objectives.Count - 1; i >= 0; i--)
{
Objectives[i].GetPriority();
@@ -257,6 +300,7 @@ namespace Barotrauma
{
Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
}
GetCurrentObjective()?.SortSubObjectives();
}
@@ -272,13 +316,19 @@ namespace Barotrauma
}
}
public void SetOrder(AIObjective objective)
public void SetForcedOrder(AIObjective objective)
{
CurrentOrder = objective;
ForcedOrder = objective;
}
public void ClearForcedOrder()
{
ForcedOrder = null;
SortObjectives();
}
private CoroutineHandle speakRoutine;
public void SetOrder(Order order, string option, Character orderGiver, bool speak)
public void SetOrder(Order order, string option, int priority, Character orderGiver, bool speak)
{
if (character.IsDead)
{
@@ -289,8 +339,53 @@ namespace Barotrauma
#endif
}
ClearIgnored();
CurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false);
if (CurrentOrder == null)
if (order == null || order.Identifier == "dismissed")
{
if (!string.IsNullOrEmpty(option))
{
if (CurrentOrders.Any(o => o.MatchesDismissedOrder(option)))
{
var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(option));
CurrentOrders.Remove(dismissedOrderInfo);
}
}
else
{
CurrentOrders.Clear();
}
}
// Make sure the order priorities reflect those set by the player
for (int i = CurrentOrders.Count - 1; i >= 0; i--)
{
var currentOrder = CurrentOrders[i];
if (currentOrder.Objective == null || currentOrder.MatchesOrder(order, option))
{
CurrentOrders.RemoveAt(i);
continue;
}
var currentOrderInfo = character.GetCurrentOrder(currentOrder.Order, currentOrder.OrderOption);
if (currentOrderInfo.HasValue)
{
int currentPriority = currentOrderInfo.Value.ManualPriority;
if (currentOrder.ManualPriority != currentPriority)
{
CurrentOrders[i] = new OrderInfo(currentOrder, currentPriority);
}
}
else
{
CurrentOrders.RemoveAt(i);
}
}
var newCurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false);
if (newCurrentOrder != null)
{
CurrentOrders.Add(new OrderInfo(order, option, priority, newCurrentOrder));
}
if (!HasOrders())
{
// Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding)
CreateAutonomousObjectives();
@@ -298,56 +393,57 @@ namespace Barotrauma
else
{
// This should be redundant, because all the objectives are reset when they are selected as active.
CurrentOrder.Reset();
if (speak)
newCurrentOrder?.Reset();
if (speak && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f);
if (speakRoutine != null)
{
CoroutineManager.StopCoroutines(speakRoutine);
}
speakRoutine = CoroutineManager.InvokeAfter(() =>
{
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
if (CurrentOrder != null && character.SpeechImpediment < 100.0f)
{
if (CurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None())
{
character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets");
}
else if (CurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None())
{
character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries");
}
else if (CurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None())
{
character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire");
}
else if (CurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None())
{
character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks");
}
else if (CurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None())
{
character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies");
}
else if (CurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None())
{
character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets");
}
else if (CurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None())
{
character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps");
}
}
}, 3);
//if (speakRoutine != null)
//{
// CoroutineManager.StopCoroutines(speakRoutine);
//}
//speakRoutine = CoroutineManager.InvokeAfter(() =>
//{
// if (GameMain.GameSession == null || Level.Loaded == null) { return; }
// if (newCurrentOrder != null && character.SpeechImpediment < 100.0f)
// {
// if (newCurrentOrder is AIObjectiveRepairItems repairItems && repairItems.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoRepairTargets"), null, 3.0f, "norepairtargets");
// }
// else if (newCurrentOrder is AIObjectiveChargeBatteries chargeBatteries && chargeBatteries.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoBatteries"), null, 3.0f, "nobatteries");
// }
// else if (newCurrentOrder is AIObjectiveExtinguishFires extinguishFires && extinguishFires.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire");
// }
// else if (newCurrentOrder is AIObjectiveFixLeaks fixLeaks && fixLeaks.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoLeaks"), null, 3.0f, "noleaks");
// }
// else if (newCurrentOrder is AIObjectiveFightIntruders fightIntruders && fightIntruders.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoEnemies"), null, 3.0f, "noenemies");
// }
// else if (newCurrentOrder is AIObjectiveRescueAll rescueAll && rescueAll.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoRescueTargets"), null, 3.0f, "norescuetargets");
// }
// else if (newCurrentOrder is AIObjectivePumpWater pumpWater && pumpWater.Targets.None())
// {
// character.Speak(TextManager.Get("DialogNoPumps"), null, 3.0f, "nopumps");
// }
// }
//}, 3);
}
}
}
public AIObjective CreateObjective(Order order, string option, Character orderGiver, bool isAutonomous, float priorityModifier = 1)
{
if (order == null) { return null; }
if (order == null || order.Identifier == "dismissed") { return null; }
AIObjective newObjective;
switch (order.Identifier.ToLowerInvariant())
{
@@ -360,7 +456,7 @@ namespace Barotrauma
extraDistanceWhileSwimming = 100,
AllowGoingOutside = true,
IgnoreIfTargetDead = true,
followControlledCharacter = orderGiver == character,
followControlledCharacter = true,
mimic = true,
DialogueIdentifier = "dialogcannotreachplace"
};
@@ -430,7 +526,7 @@ namespace Barotrauma
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, false, priorityModifier: priorityModifier)
{
IsLoop = false,
Override = character.CurrentOrder != null,
Override = !character.IsDismissed,
completionCondition = () =>
{
if (float.TryParse(option, out float pct))
@@ -483,21 +579,9 @@ namespace Barotrauma
return newObjective;
}
private void DismissSelf()
{
#if CLIENT
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
{
GameMain.GameSession?.CrewManager?.SetCharacterOrder(character, Order.GetPrefab("dismissed"), null, character);
}
#else
GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(Order.GetPrefab("dismissed"), null, null, character, character));
#endif
}
private bool IsAllowedToWait()
{
if (CurrentOrder != null) { return false; }
if (HasOrders()) { return false; }
if (CurrentObjective is AIObjectiveCombat || CurrentObjective is AIObjectiveFindSafety) { return false; }
if (character.AnimController.InWater) { return false; }
if (character.IsClimbing) { return false; }
@@ -508,5 +592,61 @@ namespace Barotrauma
if (AIObjectiveIdle.IsForbidden(character.CurrentHull)) { return false; }
return true;
}
public bool IsCurrentOrder<T>() where T : AIObjective => CurrentOrder is T;
public bool IsCurrentObjective<T>() where T : AIObjective => CurrentObjective is T;
public bool IsActiveObjective<T>() where T : AIObjective => GetActiveObjective() is T;
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
/// <summary>
/// Returns the last active objective of the specific type.
/// </summary>
public T GetActiveObjective<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
/// <summary>
/// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently.
/// </summary>
public IEnumerable<T> GetActiveObjectives<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T);
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
public bool IsOrder(AIObjective objective)
{
return objective == ForcedOrder || CurrentOrders.Any(o => o.Objective == objective);
}
public bool HasOrders()
{
return ForcedOrder != null || CurrentOrders.Any();
}
public bool HasOrder<T>() where T : AIObjective
{
return ForcedOrder is T || CurrentOrders.Any(o => o.Objective is T);
}
public float GetOrderPriority(AIObjective objective)
{
if (objective == ForcedOrder) { return HighestOrderPriority; }
var currentOrder = CurrentOrders.FirstOrDefault(o => o.Objective == objective);
if (currentOrder.Objective == null)
{
return HighestOrderPriority;
}
else if (currentOrder.ManualPriority > 0)
{
return MathHelper.Lerp(LowestOrderPriority, HighestOrderPriority, MathUtils.InverseLerp(1, CharacterInfo.HighestManualOrderPriority, currentOrder.ManualPriority));
}
#if DEBUG
DebugConsole.AddWarning("Error in order priority: shouldn't return 0!");
#endif
return 0;
}
public OrderInfo? GetCurrentOrderInfo()
{
if (currentOrder == null) { return null; }
return CurrentOrders.FirstOrDefault(o => o.Objective == CurrentOrder);
}
}
}
@@ -36,7 +36,7 @@ namespace Barotrauma
public override float GetPriority()
{
bool isOrder = objectiveManager.CurrentOrder == this;
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed || character.LockHands)
{
Priority = 0;
@@ -51,7 +51,7 @@ namespace Barotrauma
{
if (isOrder)
{
Priority = AIObjectiveManager.OrderPriority;
Priority = objectiveManager.GetOrderPriority(this);
}
ItemComponent target = GetTarget();
Item targetItem = target?.Item;
@@ -69,10 +69,9 @@ namespace Barotrauma
{
if (!isOrder)
{
if (reactor.LastUserWasPlayer && character.TeamID != CharacterTeamType.FriendlyNPC ||
HumanAIController.IsTrueForAnyCrewMember(c =>
c.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.GetTarget() == target))
if (reactor.LastUserWasPlayer && character.TeamID != CharacterTeamType.FriendlyNPC)
{
// The reactor was previously operated by a player -> ignore.
Priority = 0;
return Priority;
}
@@ -89,11 +88,15 @@ namespace Barotrauma
case "powerup":
// Check that we don't already have another order that is targeting the same item.
// Without this the autonomous objective will tell the bot to turn the reactor on again.
if (objectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder != this && operateOrder.GetTarget() == target && operateOrder.Option != Option)
if (IsAnotherOrderTargetingSameItem(objectiveManager.ForcedOrder) || objectiveManager.CurrentOrders.Any(o => IsAnotherOrderTargetingSameItem(o.Objective)))
{
Priority = 0;
return Priority;
}
bool IsAnotherOrderTargetingSameItem(AIObjective objective)
{
return objective is AIObjectiveOperateItem operateObjective && operateObjective != this && operateObjective.GetTarget() == target && operateObjective.Option != Option;
}
break;
}
}
@@ -108,14 +111,23 @@ namespace Barotrauma
}
else
{
float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier);
float max = isOrder ? MathHelper.Min(AIObjectiveManager.OrderPriority, 90) : AIObjectiveManager.RunPriority - 1;
if (!isOrder && reactor != null && reactor.PowerOn && Option == "powerup")
if (isOrder)
{
// Decrease the priority when targeting a reactor that is already on.
value /= 2;
float max = objectiveManager.GetOrderPriority(this);
float value = CumulatedDevotion + (max * PriorityModifier);
Priority = MathHelper.Clamp(value, 0, max);
}
else
{
float value = CumulatedDevotion + (AIObjectiveManager.LowestOrderPriority * PriorityModifier);
float max = AIObjectiveManager.LowestOrderPriority - 1;
if (reactor != null && reactor.PowerOn && reactor.FissionRate > 1 && Option == "powerup")
{
// Decrease the priority when targeting a reactor that is already on.
value /= 2;
}
Priority = MathHelper.Clamp(value, 0, max);
}
Priority = MathHelper.Clamp(value, 0, max);
}
}
return Priority;
@@ -154,15 +166,18 @@ namespace Barotrauma
ItemComponent target = GetTarget();
if (useController && controller == null)
{
character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f);
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f);
}
Abandon = true;
return;
}
if (operateTarget != null)
{
if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget))
if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.Character.IsBot && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget))
{
// Another crew member is already targeting this entity.
// Another crew member is already targeting this entity (leak).
Abandon = true;
return;
}
@@ -59,13 +59,13 @@ namespace Barotrauma
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 4000, dist));
}
float requiredSuccessFactor = objectiveManager.IsCurrentOrder<AIObjectiveRepairItems>() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor;
float requiredSuccessFactor = objectiveManager.HasOrder<AIObjectiveRepairItems>() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor;
float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character, requiredSuccessFactor) / 100;
bool isSelected = IsRepairing();
float selectedBonus = isSelected ? 100 - MaxDevotion : 0;
float devotion = (CumulatedDevotion + selectedBonus) / 100;
float reduction = isPriority ? 1 : isSelected ? 2 : 3;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90);
float max = AIObjectiveManager.LowestOrderPriority - reduction;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
@@ -74,7 +74,7 @@ namespace Barotrauma
protected override bool Check()
{
IsCompleted = Item.IsFullCondition;
if (IsCompleted && IsRepairing())
if (character.IsOnPlayerTeam && IsCompleted && IsRepairing())
{
character.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f);
}
@@ -97,7 +97,10 @@ namespace Barotrauma
var getItemObjective = new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true);
if (objectiveManager.IsCurrentOrder<AIObjectiveRepairItems>())
{
getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindrequireditemtorepair"), null, 0.0f, "dialogcannotfindrequireditemtorepair", 10.0f);
if (character.IsOnPlayerTeam)
{
getItemObjective.Abandoned += () => character.Speak(TextManager.Get("dialogcannotfindrequireditemtorepair"), null, 0.0f, "dialogcannotfindrequireditemtorepair", 10.0f);
}
}
subObjectives.Add(getItemObjective);
}
@@ -119,27 +122,8 @@ namespace Barotrauma
Abandon = true;
return;
}
// Eject empty tanks
if (repairTool.Item.OwnInventory.AllItems.Any(it => it.Condition <= 0.0f))
{
foreach (Item containedItem in repairTool.Item.OwnInventory.AllItemsMod)
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0.0f)
{
if (character.Submarine == null)
{
// If we are outside of main sub, try to put the tank in the inventory instead dropping it in the sea.
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot))
{
continue;
}
}
containedItem.Drop(character);
}
}
}
HumanAIController.UnequipContainedItems(repairTool.Item, it => !it.HasTag("weldingfuel"));
HumanAIController.UnequipEmptyItems(repairTool.Item);
RelatedItem item = null;
Item fuel = null;
foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained])
@@ -193,7 +177,7 @@ namespace Barotrauma
}
if (Abandon)
{
if (IsRepairing())
if (character.IsOnPlayerTeam && IsRepairing())
{
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
}
@@ -228,7 +212,7 @@ namespace Barotrauma
onAbandon: () =>
{
Abandon = true;
if (IsRepairing())
if (character.IsOnPlayerTeam && IsRepairing())
{
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
}
@@ -104,7 +104,7 @@ namespace Barotrauma
}
bool anyFixers = otherFixers > 0;
float ratio = anyFixers ? items / (float)otherFixers : 1;
if (objectiveManager.CurrentOrder == this)
if (objectiveManager.IsOrder(this))
{
return Targets.Sum(t => 100 - t.ConditionPercentage);
}
@@ -153,6 +153,8 @@ namespace Barotrauma
if (item.IsFullCondition) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine == null || character.Submarine == null) { return false; }
//player crew ignores items in outposts
if (character.IsOnPlayerTeam && item.Submarine.Info.IsOutpost) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { return false; }
if (item.Repairables.None()) { return false; }
return true;
@@ -78,14 +78,14 @@ namespace Barotrauma
// Check if the character needs more oxygen
if (!ignoreOxygen && character.SelectedCharacter == targetCharacter || character.CanInteractWith(targetCharacter))
{
// Replace empty oxygen tank
// First remove empty tanks
// Replace empty oxygen and welding fuel.
if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out IEnumerable<Item> suits, requireEquipped: true))
{
Item suit = suits.FirstOrDefault();
if (suit != null)
{
AIObjectiveFindDivingGear.EjectEmptyTanks(character, suit, out _);
AIController.UnequipEmptyItems(character, suit);
AIController.UnequipContainedItems(character, suit, it => it.HasTag("weldingfuel"));
}
}
else if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out IEnumerable<Item> masks, requireEquipped: true))
@@ -93,7 +93,8 @@ namespace Barotrauma
Item mask = masks.FirstOrDefault();
if (mask != null)
{
AIObjectiveFindDivingGear.EjectEmptyTanks(character, mask, out _);
AIController.UnequipEmptyItems(character, mask);
AIController.UnequipContainedItems(character, mask, it => it.HasTag("weldingfuel"));
}
}
bool ShouldRemoveDivingSuit() => targetCharacter.OxygenAvailable < CharacterHealth.InsufficientOxygenThreshold && targetCharacter.CurrentHull?.LethalPressure <= 0;
@@ -322,7 +323,7 @@ namespace Barotrauma
{
itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last());
}
if (targetCharacter != character)
if (targetCharacter != character && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" },
new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }),
@@ -336,7 +337,10 @@ namespace Barotrauma
onAbandon: () =>
{
Abandon = true;
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f);
if (character != targetCharacter && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: false), identifier: "cannottreatpatient", minDurationBetweenSimilar: 20.0f);
}
});
}
}
@@ -383,8 +387,10 @@ namespace Barotrauma
Abandon = true;
return false;
}
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter);
if (isCompleted && targetCharacter != character)
bool isCompleted =
AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter) ||
targetCharacter.CharacterHealth.GetAllAfflictions().All(a => a.Strength < a.Prefab.TreatmentThreshold);
if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name),
null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f);
@@ -25,8 +25,8 @@ namespace Barotrauma
{
// When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored.
// If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players.
// On the other hand, if the bots too eagerly heal characters when it's not nevessary, it's inefficient and can feel frustrating, because it can't be controlled.
return character == target || manager.CurrentOrder is AIObjectiveRescueAll ? (target.IsPlayer ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
// On the other hand, if the bots too eagerly heal characters when it's not necessary, it's inefficient and can feel frustrating, because it can't be controlled.
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? (target.IsPlayer ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
}
}
@@ -40,7 +40,7 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
if (Targets.None()) { return 100; }
if (objectiveManager.CurrentOrder != this)
if (!objectiveManager.IsOrder(this))
{
if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsMedic && !c.Character.IsUnconscious))
{
@@ -82,8 +82,12 @@ namespace Barotrauma
if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; }
if (character.AIController is HumanAIController humanAI)
{
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { return false; }
if (!humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveRescueAll>())
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target) ||
target.CharacterHealth.GetAllAfflictions().All(a => a.Strength < a.Prefab.TreatmentThreshold))
{
return false;
}
if (!humanAI.ObjectiveManager.HasOrder<AIObjectiveRescueAll>())
{
if (!character.IsMedic && target != character)
{
@@ -19,27 +19,62 @@ namespace Barotrauma
struct OrderInfo
{
public string ComponentIdentifier { get; set; }
public Order Order { get; private set; }
public string OrderOption { get; private set; }
public Order Order { get; }
public string OrderOption { get; }
public int ManualPriority { get; }
public OrderType Type { get; }
public AIObjective Objective { get; }
public bool IsCurrentOrder => Type == OrderType.Current;
public OrderInfo(Order order, string orderOption)
public enum OrderType
{
Current,
Previous
}
private OrderInfo(Order order, string orderOption, int manualPriority, OrderType orderType, AIObjective objective)
{
ComponentIdentifier = "currentorder";
Order = order;
OrderOption = orderOption;
ManualPriority = Math.Min(manualPriority, CharacterInfo.HighestManualOrderPriority);
Type = orderType;
Objective = objective;
}
public OrderInfo(OrderInfo orderInfo)
{
ComponentIdentifier = "previousorder";
Order = orderInfo.Order;
OrderOption = orderInfo.OrderOption;
}
public OrderInfo(Order order, string orderOption, int manualPriority) : this(order, orderOption, manualPriority, OrderType.Current, null) { }
public OrderInfo(Order order, string orderOption, int manualPriority, AIObjective objective) : this(order, orderOption, manualPriority, OrderType.Current, objective) { }
public OrderInfo(OrderInfo orderInfo, int manualPriority) : this(orderInfo.Order, orderInfo.OrderOption, manualPriority, orderInfo.Type, orderInfo.Objective) { }
public OrderInfo(OrderInfo orderInfo, OrderType type) : this(orderInfo.Order, orderInfo.OrderOption, orderInfo.ManualPriority, type, orderInfo.Objective) { }
public bool MatchesOrder(string orderIdentifier, string orderOption) =>
(orderIdentifier == Order?.Identifier || (string.IsNullOrEmpty(orderIdentifier) && string.IsNullOrEmpty(Order?.Identifier))) &&
(orderOption == OrderOption || (string.IsNullOrEmpty(orderOption) && string.IsNullOrEmpty(OrderOption)));
public bool MatchesOrder(Order order, string option) =>
order.Identifier == Order.Identifier &&
option == OrderOption;
MatchesOrder(order?.Identifier, option);
public bool MatchesOrder(OrderInfo orderInfo) =>
MatchesOrder(orderInfo.Order?.Identifier, orderInfo.OrderOption);
public bool MatchesDismissedOrder(string dismissOrderOption)
{
string[] dismissedOrder = dismissOrderOption?.Split('.');
if (dismissedOrder != null && dismissedOrder.Length > 0)
{
string dismissedOrderIdentifier = dismissedOrder.Length > 0 ? dismissedOrder[0] : null;
if (dismissedOrderIdentifier == null || dismissedOrderIdentifier != Order?.Identifier) { return false; }
string dismissedOrderOption = dismissedOrder.Length > 1 ? dismissedOrder[1] : null;
if (dismissedOrderOption == null && string.IsNullOrEmpty(OrderOption)) { return true; }
return dismissedOrderOption == OrderOption;
}
else
{
return false;
}
}
}
class Order
@@ -412,7 +447,7 @@ namespace Barotrauma
orderOption ??= "";
string messageTag = (givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf." : "OrderDialog.") + Identifier;
if (!string.IsNullOrEmpty(orderOption)) { messageTag += "." + orderOption; }
if (Identifier != "dismissed" && !string.IsNullOrEmpty(orderOption)) { messageTag += "." + orderOption; }
if (targetCharacterName == null) { targetCharacterName = ""; }
if (targetRoomName == null) { targetRoomName = ""; }
@@ -498,5 +533,23 @@ namespace Barotrauma
if (index < 0 || index >= Options.Length) { return null; }
return GetOptionName(Options[index]);
}
/// <summary>
/// Used to create the order option for the Dismiss order to know which order it targets
/// </summary>
/// <param name="orderInfo">The order to target with the dismiss order</param>
public static string GetDismissOrderOption(OrderInfo orderInfo)
{
if (orderInfo.Order != null)
{
string option = orderInfo.Order.Identifier;
if (!string.IsNullOrEmpty(orderInfo.OrderOption))
{
option += $".{orderInfo.OrderOption}";
}
return option;
}
return "";
}
}
}
@@ -35,7 +35,7 @@ namespace Barotrauma
WayPointID = Waypoint.ID;
}
public static List<PathNode> GenerateNodes(List<WayPoint> wayPoints)
public static List<PathNode> GenerateNodes(List<WayPoint> wayPoints, bool removeOrphans)
{
var nodes = new Dictionary<int, PathNode>();
foreach (WayPoint wayPoint in wayPoints)
@@ -63,7 +63,10 @@ namespace Barotrauma
}
var nodeList = nodes.Values.ToList();
nodeList.RemoveAll(n => n.connections.Count == 0);
if (removeOrphans)
{
nodeList.RemoveAll(n => n.connections.Count == 0);
}
foreach (PathNode node in nodeList)
{
node.distances = new List<float>();
@@ -90,7 +93,7 @@ namespace Barotrauma
public PathFinder(List<WayPoint> wayPoints, bool indoorsSteering = false)
{
nodes = PathNode.GenerateNodes(wayPoints.FindAll(w => w.Submarine != null == indoorsSteering));
nodes = PathNode.GenerateNodes(wayPoints.FindAll(w => w.Submarine != null == indoorsSteering), removeOrphans: true);
foreach (WayPoint wp in wayPoints)
{
@@ -94,20 +94,24 @@ namespace Barotrauma
{
Vector2 targetVel = target - host.SimPosition;
if (targetVel.LengthSquared() < 0.00001f) return Vector2.Zero;
if (targetVel.LengthSquared() < 0.00001f) { return Vector2.Zero; }
targetVel = Vector2.Normalize(targetVel) * weight;
Vector2 newSteering = targetVel - host.Steering;
// TODO: the code below doesn't quite work as it should, and I'm not sure what the purpose of it is/was.
// So, we'll just return the targetVel for now, as it produces smooth results.
return targetVel;
if (newSteering == Vector2.Zero) return Vector2.Zero;
//Vector2 newSteering = targetVel - host.Steering;
float steeringSpeed = (newSteering + host.Steering).Length();
if (steeringSpeed > Math.Abs(weight))
{
newSteering = Vector2.Normalize(newSteering) * Math.Abs(weight);
}
//if (newSteering == Vector2.Zero) return Vector2.Zero;
return newSteering;
//float steeringSpeed = (newSteering + host.Steering).Length();
//if (steeringSpeed > Math.Abs(weight))
//{
// newSteering = Vector2.Normalize(newSteering) * Math.Abs(weight);
//}
//return newSteering;
}
protected virtual Vector2 DoSteeringWander(float weight)
@@ -35,7 +35,7 @@ namespace Barotrauma
private static IEnumerable<MapEntity> GetThalamusEntities(Submarine wreck, string tag) => MapEntity.mapEntityList.Where(e => e.Submarine == wreck && e.prefab != null && IsThalamus(e.prefab, tag));
private static bool IsThalamus(MapEntityPrefab entityPrefab, string tag) => entityPrefab.Category == MapEntityCategory.Thalamus || entityPrefab.Tags.Contains(tag);
private static bool IsThalamus(MapEntityPrefab entityPrefab, string tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag);
public WreckAI(Submarine wreck)
{
@@ -246,7 +246,7 @@ namespace Barotrauma
initialCellsSpawned = true;
}
private void Kill()
public void Kill()
{
thalamusItems.ForEach(i => i.Condition = 0);
foreach (var turret in turrets)
@@ -1,18 +1,9 @@
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma
{
partial class AICharacter : Character
{
//characters that are further than this from the camera (and all clients)
//have all their limb physics bodies disabled
const float EnableSimplePhysicsDist = 6000.0f;
const float DisableSimplePhysicsDist = EnableSimplePhysicsDist * 0.9f;
const float EnableSimplePhysicsDistSqr = EnableSimplePhysicsDist * EnableSimplePhysicsDist;
const float DisableSimplePhysicsDistSqr = DisableSimplePhysicsDist * DisableSimplePhysicsDist;
{
private AIController aiController;
public override AIController AIController
@@ -20,8 +11,8 @@ namespace Barotrauma
get { return aiController; }
}
public AICharacter(CharacterPrefab prefab, string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isNetworkPlayer = false, RagdollParams ragdoll = null)
: base(prefab, speciesName, position, seed, characterInfo, id: Entity.NullEntityID, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll)
public AICharacter(CharacterPrefab prefab, string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null)
: base(prefab, speciesName, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll)
{
InitProjSpecific();
}
@@ -63,11 +54,11 @@ namespace Barotrauma
if (!IsRemotePlayer && !(AIController is HumanAIController))
{
float characterDistSqr = GetDistanceSqrToClosestPlayer();
if (characterDistSqr > EnableSimplePhysicsDistSqr)
if (characterDistSqr > MathUtils.Pow2(Params.DisableDistance * 0.5f))
{
AnimController.SimplePhysicsEnabled = true;
}
else if (characterDistSqr < DisableSimplePhysicsDistSqr)
else if (characterDistSqr < MathUtils.Pow2(Params.DisableDistance * 0.5f * 0.9f))
{
AnimController.SimplePhysicsEnabled = false;
}
@@ -423,23 +423,25 @@ namespace Barotrauma
if (CurrentSwimParams == null) { return; }
movement = TargetMovement;
bool isMoving = movement.LengthSquared() > 0.00001f;
var mainLimb = MainLimb;
if (isMoving)
{
float t = 0.5f;
if (CurrentSwimParams.RotateTowardsMovement && VectorExtensions.Angle(VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2), movement) > MathHelper.PiOver2)
if (!SimplePhysicsEnabled && CurrentSwimParams.RotateTowardsMovement)
{
// Reduce the linear movement speed when not facing the movement direction
t /= 5;
Vector2 forward = VectorExtensions.Forward(Collider.Rotation + MathHelper.PiOver2);
float dot = Vector2.Dot(forward, Vector2.Normalize(movement));
if (dot < 0)
{
// Reduce the linear movement speed when not facing the movement direction
t = MathHelper.Clamp((1 + dot) / 10, 0.01f, 0.1f);
}
}
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, t);
}
//limbs are disabled when simple physics is enabled, no need to move them
if (SimplePhysicsEnabled) { return; }
var mainLimb = MainLimb;
mainLimb.PullJointEnabled = true;
//mainLimb.PullJointWorldAnchorB = Collider.SimPosition;
if (!isMoving)
{
WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5);
@@ -645,7 +647,7 @@ namespace Barotrauma
}
if (limb.Params.BlinkFrequency > 0)
{
limb.Blink(deltaTime, MainLimb.Rotation);
limb.UpdateBlink(deltaTime, MainLimb.Rotation);
}
}
@@ -787,7 +789,7 @@ namespace Barotrauma
}
if (limb.Params.BlinkFrequency > 0)
{
limb.Blink(deltaTime, MainLimb.Rotation);
limb.UpdateBlink(deltaTime, MainLimb.Rotation);
}
switch (limb.type)
{
@@ -281,7 +281,7 @@ namespace Barotrauma
}
}
public const float MAX_SPEED = 15;
public const float MAX_SPEED = 20;
public Vector2 TargetMovement
{
@@ -472,7 +472,7 @@ namespace Barotrauma
if (joint == null) { continue; }
float angle = (joint.LowerLimit + joint.UpperLimit) / 2.0f;
joint.LimbB?.body?.SetTransform(
(joint.WorldAnchorA - MathUtils.RotatePointAroundTarget(joint.LocalAnchorB, Vector2.Zero, MathHelper.ToDegrees(joint.BodyA.Rotation + angle), true)),
(joint.WorldAnchorA - MathUtils.RotatePointAroundTarget(joint.LocalAnchorB, Vector2.Zero, joint.BodyA.Rotation + angle, true)),
joint.BodyA.Rotation + angle);
}
}
@@ -636,9 +636,12 @@ namespace Barotrauma
//always collides with bodies other than structures
if (!(f2.Body.UserData is Structure structure))
{
lock (impactQueue)
if (!f2.IsSensor)
{
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
lock (impactQueue)
{
impactQueue.Enqueue(new Impact(f1, f2, contact, velocity));
}
}
return true;
}
@@ -1120,6 +1123,32 @@ namespace Barotrauma
splashSoundTimer -= deltaTime;
if (character.Submarine == null && Level.Loaded != null)
{
if (Collider.SimPosition.Y > Level.Loaded.TopBarrier.Position.Y)
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, Math.Min(Collider.LinearVelocity.Y, -1));
}
else if (Collider.SimPosition.Y < Level.Loaded.BottomBarrier.Position.Y)
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X,
MathHelper.Clamp(Collider.LinearVelocity.Y, Level.Loaded.BottomBarrier.Position.Y - Collider.SimPosition.Y, 10.0f));
}
foreach (Limb limb in Limbs)
{
if (limb.SimPosition.Y > Level.Loaded.TopBarrier.Position.Y)
{
limb.body.LinearVelocity = new Vector2(limb.LinearVelocity.X, Math.Min(limb.LinearVelocity.Y, -1));
}
else if (limb.SimPosition.Y < Level.Loaded.BottomBarrier.Position.Y)
{
limb.body.LinearVelocity = new Vector2(
limb.LinearVelocity.X,
MathHelper.Clamp(limb.LinearVelocity.Y, Level.Loaded.BottomBarrier.Position.Y - limb.SimPosition.Y, 10.0f));
}
}
}
if (forceStanding)
{
inWater = false;
@@ -217,6 +217,9 @@ namespace Barotrauma
[Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable]
public Vector2 TargetForceWorld { get; private set; }
[Serialize(1.0f, true, description: "Affects the strength of the impact effects the limb causes when it hits a submarine."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
public float SubmarineImpactMultiplier { get; private set; }
[Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
public float SeverLimbsProbability { get; set; }
@@ -228,6 +231,9 @@ namespace Barotrauma
[Serialize(0.0f, true, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
public float Priority { get; private set; }
[Serialize(false, true, description: ""), Editable]
public bool Blink { get; private set; }
public IEnumerable<StatusEffect> StatusEffects
{
get { return statusEffects; }
@@ -69,8 +69,8 @@ namespace Barotrauma
/// </summary>
public bool IsRemotelyControlled
{
get
{
get
{
if (GameMain.NetworkMember == null)
{
return false;
@@ -145,17 +145,13 @@ namespace Barotrauma
}
private readonly List<Attacker> lastAttackers = new List<Attacker>();
public IEnumerable<Attacker> LastAttackers
{
get { return lastAttackers; }
}
public Character LastAttacker
{
get { return lastAttackers.Count > 0 ? lastAttackers[lastAttackers.Count - 1].Character : null; }
}
public IEnumerable<Attacker> LastAttackers => lastAttackers;
public Character LastAttacker => lastAttackers.LastOrDefault()?.Character;
public Entity LastDamageSource;
public AttackResult LastDamage;
public float InvisibleTimer;
private CharacterPrefab prefab;
@@ -199,7 +195,12 @@ namespace Barotrauma
set => Params.Visibility = value;
}
public bool IsTraitor;
public bool IsTraitor
{
get;
set;
}
public string TraitorCurrentObjective = "";
public bool IsHuman => SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase);
public bool IsMale => Info != null && Info.HasGenders && Info.Gender == Gender.Male;
@@ -207,31 +208,8 @@ namespace Barotrauma
private float attackCoolDown;
public Order CurrentOrder
{
get
{
return Info?.CurrentOrder;
}
private set
{
if (Info != null) { Info.CurrentOrder = value; }
}
}
public string CurrentOrderOption
{
get
{
return Info?.CurrentOrderOption;
}
private set
{
if (Info != null) { Info.CurrentOrderOption = value; }
}
}
public bool IsDismissed => Info != null && Info.IsDismissed;
public List<OrderInfo> CurrentOrders => Info?.CurrentOrders;
public bool IsDismissed => !GetCurrentOrderWithTopPriority().HasValue;
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
@@ -356,6 +334,7 @@ namespace Barotrauma
//text displayed when the character is highlighted if custom interact is set
public string customInteractHUDText;
private Action<Character, Character> onCustomInteract;
public ConversationAction ActiveConversation;
public bool AllowCustomInteract
{
@@ -372,6 +351,9 @@ namespace Barotrauma
set
{
lockHandsTimer = MathHelper.Clamp(lockHandsTimer + (value ? 1.0f : -0.5f), 0.0f, 10.0f);
#if CLIENT
HintManager.OnHandcuffed(this);
#endif
}
}
@@ -454,7 +436,7 @@ namespace Barotrauma
/// </summary>
public IEnumerable<Item> HeldItems
{
get
get
{
var item1 = Inventory?.GetItemInLimbSlot(InvSlotType.RightHand);
var item2 = Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand);
@@ -483,16 +465,21 @@ namespace Barotrauma
}
}
private double pressureProtectionLastSet;
private float pressureProtection;
public float PressureProtection
{
get { return pressureProtection; }
set
{
pressureProtection = MathHelper.Clamp(value, 0.0f, 100.0f);
pressureProtection = Math.Max(value, 0.0f);
pressureProtectionLastSet = Timing.TotalTime;
}
}
public const float KnockbackCooldown = 5.0f;
public float KnockbackCooldownTimer;
private float ragdollingLockTimer;
public bool IsRagdolled;
public bool IsForceRagdolled;
@@ -534,14 +521,13 @@ namespace Barotrauma
}
public bool UseHullOxygen { get; set; } = true;
public float Stun
{
get { return IsRagdolled ? 1.0f : CharacterHealth.StunTimer; }
get { return IsRagdolled ? 1.0f : CharacterHealth.Stun; }
set
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
SetStun(value, true);
}
}
@@ -609,7 +595,7 @@ namespace Barotrauma
{
get;
set;
}
}
/// <summary>
/// Current speed of the character's collider. Can be used by status effects to check if the character is moving.
@@ -625,8 +611,12 @@ namespace Barotrauma
get => _selectedConstruction;
set
{
#if CLIENT
var prevSelectedConstruction = _selectedConstruction;
#endif
_selectedConstruction = value;
#if CLIENT
HintManager.OnSetSelectedConstruction(this, prevSelectedConstruction, _selectedConstruction);
if (Controlled == this)
{
if (_selectedConstruction == null)
@@ -660,11 +650,11 @@ namespace Barotrauma
}
private bool isDead;
public bool IsDead
{
public bool IsDead
{
get { return isDead; }
set
{
set
{
if (isDead == value) { return; }
if (value)
{
@@ -703,7 +693,7 @@ namespace Barotrauma
{
if (!canBeDragged) { return false; }
if (Removed || !AnimController.Draggable) { return false; }
return IsDead || Stun > 0.0f || LockHands || IsIncapacitated || IsPet;
return IsKnockedDown || LockHands || IsPet;
}
set { canBeDragged = value; }
}
@@ -721,7 +711,7 @@ namespace Barotrauma
}
else
{
return IsDead || Stun > 0.0f || LockHands || IsIncapacitated;
return IsKnockedDown || LockHands;
}
}
set { canInventoryBeAccessed = value; }
@@ -827,7 +817,7 @@ namespace Barotrauma
speciesName = Path.GetFileNameWithoutExtension(speciesName).ToLowerInvariant();
}
var prefab = CharacterPrefab.FindBySpeciesName(speciesName);
var prefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (prefab == null)
{
DebugConsole.ThrowError($"Failed to create character \"{speciesName}\". Matching prefab not found.\n" + Environment.StackTrace);
@@ -837,21 +827,21 @@ namespace Barotrauma
Character newCharacter = null;
if (!speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase))
{
var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, isRemotePlayer, ragdoll);
var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, id, isRemotePlayer, ragdoll);
var ai = new EnemyAIController(aiCharacter, seed);
aiCharacter.SetAI(ai);
newCharacter = aiCharacter;
}
else if (hasAi)
{
var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, isRemotePlayer, ragdoll);
var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, id, isRemotePlayer, ragdoll);
var ai = new HumanAIController(aiCharacter);
aiCharacter.SetAI(ai);
newCharacter = aiCharacter;
}
else
{
newCharacter = new Character(prefab, speciesName, position, seed, characterInfo, id: id, isRemotePlayer: isRemotePlayer, ragdollParams: ragdoll);
newCharacter = new Character(prefab, speciesName, position, seed, characterInfo, id, isRemotePlayer, ragdoll);
}
float healthRegen = newCharacter.Params.Health.ConstantHealthRegeneration;
@@ -1022,7 +1012,7 @@ namespace Barotrauma
{
// Get the non husked name and find the ragdoll with it
var matchingAffliction = AfflictionPrefab.List
.Where(p => p.AfflictionType == "huskinfection")
.Where(p => p is AfflictionPrefabHusk)
.Select(p => p as AfflictionPrefabHusk)
.FirstOrDefault(p => p.TargetSpecies.Any(t => t.Equals(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p), StringComparison.OrdinalIgnoreCase)));
string nonHuskedSpeciesName = string.Empty;
@@ -1058,7 +1048,7 @@ namespace Barotrauma
else
{
AnimController = new FishAnimController(this, seed, ragdollParams as FishRagdollParams);
PressureProtection = 100.0f;
PressureProtection = int.MaxValue;
}
AnimController.SetPosition(ConvertUnits.ToSimUnits(position));
@@ -1277,7 +1267,13 @@ namespace Barotrauma
public float GetSkillLevel(string skillIdentifier)
{
return (Info == null || Info.Job == null) ? 0.0f : Info.Job.GetSkillLevel(skillIdentifier);
if (Info?.Job == null) { return 0.0f; }
float skillLevel = Info.Job.GetSkillLevel(skillIdentifier);
foreach (Affliction affliction in CharacterHealth.GetAllAfflictions())
{
skillLevel *= affliction.GetSkillMultiplier();
}
return skillLevel;
}
// TODO: reposition? there's also the overrideTargetMovement variable, but it's not in the same manner
@@ -1348,6 +1344,22 @@ namespace Barotrauma
/// </summary>
public float SpeedMultiplier { get; private set; } = 1;
private double propulsionSpeedMultiplierLastSet;
private float propulsionSpeedMultiplier;
/// <summary>
/// Can be used to modify the speed at which Propulsion ItemComponents move the character via StatusEffects (e.g. heavy suit can slow down underwater scooters)
/// </summary>
public float PropulsionSpeedMultiplier
{
get { return propulsionSpeedMultiplier; }
set
{
propulsionSpeedMultiplier = value;
propulsionSpeedMultiplierLastSet = Timing.TotalTime;
}
}
public void StackSpeedMultiplier(float val)
{
if (val < 1f)
@@ -1370,6 +1382,10 @@ namespace Barotrauma
{
greatestPositiveSpeedMultiplier = 1f;
greatestNegativeSpeedMultiplier = 1f;
if (Timing.TotalTime > propulsionSpeedMultiplierLastSet + 0.1)
{
propulsionSpeedMultiplier = 1.0f;
}
}
private float greatestNegativeHealthMultiplier = 1f;
@@ -1681,6 +1697,12 @@ namespace Barotrauma
{
item.Use(deltaTime, this);
}
#if CLIENT
else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim))
{
HintManager.OnShootWithoutAiming(this, item);
}
#endif
}
}
}
@@ -1873,6 +1895,19 @@ namespace Barotrauma
return false;
}
public Item GetEquippedItem(string tagOrIdentifier)
{
if (Inventory == null) { return null; }
for (int i = 0; i < Inventory.Capacity; i++)
{
if (Inventory.SlotTypes[i] == InvSlotType.Any) { continue; }
var item = Inventory.GetItemAt(i);
if (item == null) { continue; }
if (item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return item; }
}
return null;
}
public bool CanAccessInventory(Inventory inventory)
{
if (!CanInteract || inventory.Locked) { return false; }
@@ -2171,8 +2206,7 @@ namespace Barotrauma
#if CLIENT
if (isLocalPlayer)
{
if (GUI.MouseOn == null &&
(!CharacterInventory.IsMouseOnInventory() || CharacterInventory.DraggingItemToWorld))
if (!IsMouseOnUI)
{
if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen)
{
@@ -2336,7 +2370,7 @@ namespace Barotrauma
else
{
float closestPlayerDist = c.GetDistanceToClosestPlayer();
if (closestPlayerDist > NetConfig.DisableCharacterDist)
if (closestPlayerDist > c.Params.DisableDistance)
{
c.Enabled = false;
if (c.IsDead && c.AIController is EnemyAIController)
@@ -2344,7 +2378,7 @@ namespace Barotrauma
Spawner?.AddToRemoveQueue(c);
}
}
else if (closestPlayerDist < NetConfig.EnableCharacterDist)
else if (closestPlayerDist < c.Params.DisableDistance * 0.9f)
{
c.Enabled = true;
}
@@ -2363,7 +2397,7 @@ namespace Barotrauma
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(GameMain.GameScreen.Cam.GetPosition(), c.WorldPosition));
}
if (distSqr > NetConfig.DisableCharacterDistSqr)
if (distSqr > MathUtils.Pow2(c.Params.DisableDistance))
{
c.Enabled = false;
if (c.IsDead && c.AIController is EnemyAIController)
@@ -2371,7 +2405,7 @@ namespace Barotrauma
Entity.Spawner?.AddToRemoveQueue(c);
}
}
else if (distSqr < NetConfig.EnableCharacterDistSqr)
else if (distSqr < MathUtils.Pow2(c.Params.DisableDistance * 0.9f))
{
c.Enabled = true;
}
@@ -2389,6 +2423,8 @@ namespace Barotrauma
{
UpdateProjSpecific(deltaTime, cam);
KnockbackCooldownTimer -= deltaTime;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && this == Controlled && !isSynced) { return; }
UpdateDespawn(deltaTime);
@@ -2451,11 +2487,8 @@ namespace Barotrauma
if (NeedsAir)
{
bool protectedFromPressure = PressureProtection > 0.0f;
//cannot be protected from pressure when below crush depth
protectedFromPressure = protectedFromPressure && WorldPosition.Y > CharacterHealth.CrushDepth;
//implode if not protected from pressure, and either outside or in a high-pressure hull
if (!protectedFromPressure &&
if (!IsProtectedFromPressure() &&
(AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f))
{
if (CharacterHealth.PressureKillDelay <= 0.0f)
@@ -2585,7 +2618,7 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime, Camera cam);
partial void SetOrderProjSpecific(Order order, string orderOption);
partial void SetOrderProjSpecific(Order order, string orderOption, int priority);
public void AddAttacker(Character character, float damage)
@@ -2641,7 +2674,10 @@ namespace Barotrauma
{
if (NeedsAir)
{
PressureProtection -= deltaTime * 100.0f;
if (Timing.TotalTime > pressureProtectionLastSet + 0.1)
{
PressureProtection = 0.0f;
}
}
if (NeedsWater)
{
@@ -2739,7 +2775,7 @@ namespace Barotrauma
}
float distToClosestPlayer = GetDistanceToClosestPlayer();
if (distToClosestPlayer > NetConfig.DisableCharacterDist)
if (distToClosestPlayer > Params.DisableDistance)
{
//despawn in 1 minute if very far from all human players
despawnTimer = Math.Max(despawnTimer, GameMain.Config.CorpseDespawnDelay - 60.0f);
@@ -2863,7 +2899,7 @@ namespace Barotrauma
return !string.IsNullOrEmpty(ChatMessage.ApplyDistanceEffect("message", messageType, speaker, this));
}
public void SetOrder(Order order, string orderOption, Character orderGiver, bool speak = true)
public void SetOrder(Order order, string orderOption, int priority, Character orderGiver, bool speak = true)
{
//set the character order only if the character is close enough to hear the message
if (orderGiver != null && !CanHearCharacter(orderGiver)) { return; }
@@ -2871,25 +2907,138 @@ namespace Barotrauma
// If there's another character operating the same device, make them dismiss themself
if (order != null && order.Category == OrderCategory.Operate && order.TargetEntity != null)
{
CharacterList.FindAll(c => c != this && c.TeamID == TeamID && c.CurrentOrder is Order characterOrder && characterOrder.Category == OrderCategory.Operate &&
characterOrder.Identifier.Equals(order.Identifier) && characterOrder.TargetEntity == order.TargetEntity)?
.ForEach(c => c.SetOrder(Order.GetPrefab("dismissed"), null, c, speak: true));
foreach (var character in CharacterList)
{
if (character == this) { continue; }
if (character.TeamID != TeamID) { continue; }
if (!HumanAIController.IsActive(character)) { continue; }
foreach (var currentOrder in character.CurrentOrders)
{
if (currentOrder.Order == null) { continue; }
if (currentOrder.Order.Category != OrderCategory.Operate) { continue; }
if (currentOrder.Order.Identifier != order.Identifier) { continue; }
if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; }
character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character);
break;
}
}
}
// Prevent adding duplicate orders
RemoveDuplicateOrders(order, orderOption);
OrderInfo newOrderInfo = new OrderInfo(order, orderOption, priority);
AddCurrentOrder(newOrderInfo);
if (AIController is HumanAIController humanAI)
{
humanAI.SetOrder(order, orderOption, orderGiver, speak);
humanAI.SetOrder(order, orderOption, priority, orderGiver, speak);
}
SetOrderProjSpecific(order, orderOption);
CurrentOrder = order;
CurrentOrderOption = orderOption;
SetOrderProjSpecific(order, orderOption, priority);
}
/// <summary>
/// Reset order data so it doesn't carry into further rounds, as the AI is "recreated" always in between rounds anyway.
/// </summary>
public void ResetCurrentOrder() => Info?.ResetCurrentOrder();
private void AddCurrentOrder(OrderInfo newOrder)
{
if (newOrder.Order == null || newOrder.Order.Identifier == "dismissed")
{
if (!string.IsNullOrEmpty(newOrder.OrderOption))
{
if (CurrentOrders.Any(o => o.MatchesDismissedOrder(newOrder.OrderOption)))
{
var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(newOrder.OrderOption));
int dismissedOrderPriority = dismissedOrderInfo.ManualPriority;
CurrentOrders.Remove(dismissedOrderInfo);
for (int i = 0; i < CurrentOrders.Count; i++)
{
var orderInfo = CurrentOrders[i];
if (orderInfo.ManualPriority < dismissedOrderPriority)
{
CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority + 1);
}
}
}
}
else
{
CurrentOrders.Clear();
}
}
else
{
for (int i = 0; i < CurrentOrders.Count; i++)
{
var orderInfo = CurrentOrders[i];
if (orderInfo.ManualPriority <= newOrder.ManualPriority)
{
CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority - 1);
}
}
CurrentOrders.RemoveAll(order => order.ManualPriority <= 0);
CurrentOrders.Add(newOrder);
// Sort the current orders so the one with the highest priority comes first
CurrentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority));
}
}
private void RemoveDuplicateOrders(Order order, string option)
{
int? priorityOfRemoved = null;
for (int i = CurrentOrders.Count - 1; i >= 0; i--)
{
var orderInfo = CurrentOrders[i];
if (order?.Identifier == orderInfo.Order?.Identifier)
{
priorityOfRemoved = orderInfo.ManualPriority;
CurrentOrders.RemoveAt(i);
break;
}
}
if (!priorityOfRemoved.HasValue) { return; }
for (int i = 0; i < CurrentOrders.Count; i++)
{
var orderInfo = CurrentOrders[i];
if (orderInfo.ManualPriority < priorityOfRemoved.Value)
{
CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority + 1);
}
}
CurrentOrders.RemoveAll(order => order.ManualPriority <= 0);
// Sort the current orders so the one with the highest priority comes first
CurrentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority));
}
public OrderInfo? GetCurrentOrderWithTopPriority()
{
return GetCurrentOrder(orderInfo =>
{
if (orderInfo.Order == null) { return false; }
if (orderInfo.Order.Identifier == "dismissed") { return false; }
if (orderInfo.ManualPriority < 1) { return false; }
return true;
});
}
public OrderInfo? GetCurrentOrder(Order order, string option)
{
return GetCurrentOrder(orderInfo =>
{
return orderInfo.MatchesOrder(order, option);
});
}
private OrderInfo? GetCurrentOrder(Func<OrderInfo, bool> predicate)
{
if (CurrentOrders != null && CurrentOrders.Any(predicate))
{
return CurrentOrders.First(predicate);
}
else
{
return null;
}
}
private readonly List<AIChatMessage> aiChatMessageQueue = new List<AIChatMessage>();
@@ -3099,7 +3248,6 @@ namespace Barotrauma
otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f);
ApplyStatusEffects(ActionType.OnSevered, 1.0f);
targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
otherLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
}
}
if (wasSevered && targetLimb.character.AIController is EnemyAIController enemyAI)
@@ -3154,7 +3302,7 @@ namespace Barotrauma
GameMain.Config.RecentlyEncounteredCreatures.Add(other.SpeciesName);
}
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1)
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true)
{
if (Removed) { return new AttackResult(); }
@@ -3175,12 +3323,20 @@ namespace Barotrauma
//#endif
// }
SetStun(stun);
if (attacker != null && attacker != this && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire)
{
if (attacker.TeamID == TeamID) { return new AttackResult(); }
}
SetStun(stun);
#if CLIENT
if (Params.UseBossHealthBar && Controlled != null && Controlled.teamID == attacker?.teamID)
{
CharacterHUD.ShowBossHealthBar(this);
}
#endif
Vector2 dir = hitLimb.WorldPosition - worldPosition;
if (Math.Abs(attackImpulse) > 0.0f)
{
@@ -3199,7 +3355,7 @@ namespace Barotrauma
bool wasDead = IsDead;
Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir);
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier);
CharacterHealth.ApplyDamage(hitLimb, attackResult);
CharacterHealth.ApplyDamage(hitLimb, attackResult, allowStacking);
if (attacker != this)
{
OnAttacked?.Invoke(attacker, attackResult);
@@ -3215,14 +3371,15 @@ namespace Barotrauma
};
if (attackResult.Damage > 0)
{
ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
LastDamage = attackResult;
if (attacker != null)
{
AddAttacker(attacker, attackResult.Damage);
AddEncounter(attacker);
attacker.AddEncounter(this);
}
ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
}
return attackResult;
}
@@ -3253,6 +3410,12 @@ namespace Barotrauma
}
}
/// <summary>
/// Is the character knocked down regardless whether the technical state is dead, unconcious, paralyzed, or stunned.
/// With stunning, the parameter uses a half a second delay before the character is treated as knocked down. The purpose of this is to ignore minor stunning. If you don't want to to ignore any stun, use the Stun property.
/// </summary>
public bool IsKnockedDown => IsDead || IsIncapacitated || CharacterHealth.StunTimer > 0.5f;
public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; }
@@ -3262,7 +3425,7 @@ namespace Barotrauma
{
AnimController.ResetPullJoints();
}
CharacterHealth.StunTimer = newStun;
CharacterHealth.Stun = newStun;
if (newStun > 0.0f)
{
SelectedConstruction = null;
@@ -3276,6 +3439,20 @@ namespace Barotrauma
foreach (StatusEffect statusEffect in statusEffects)
{
if (statusEffect.type != actionType) { continue; }
if (statusEffect.type == ActionType.OnDamaged)
{
if (statusEffect.AllowedAfflictions != null && (LastDamage.Afflictions == null || LastDamage.Afflictions.None(a => statusEffect.AllowedAfflictions.Contains(a.Prefab.AfflictionType) || statusEffect.AllowedAfflictions.Contains(a.Prefab.Identifier))))
{
continue;
}
if (statusEffect.OnlyPlayerTriggered)
{
if (LastAttacker == null || !LastAttacker.IsPlayer)
{
continue;
}
}
}
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
@@ -3308,6 +3485,12 @@ namespace Barotrauma
Limb limb = AnimController.GetLimb(limbType);
statusEffect.Apply(actionType, deltaTime, this, limb);
}
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);
statusEffect.Apply(actionType, deltaTime, this, limb);
}
}
}
}
@@ -3460,16 +3643,18 @@ namespace Barotrauma
return;
}
isDead = false;
if (aiTarget != null)
{
aiTarget.Remove();
}
aiTarget = new AITarget(this);
SetAllDamage(0.0f, 0.0f, 0.0f);
CharacterHealth.RemoveAllAfflictions();
SetAllDamage(0.0f, 0.0f, 0.0f);
Oxygen = 100.0f;
Bloodloss = 0.0f;
SetStun(0.0f, true);
isDead = false;
foreach (LimbJoint joint in AnimController.LimbJoints)
{
@@ -3631,6 +3816,10 @@ namespace Barotrauma
}
}
}
else
{
canBePutInOriginalInventory = inventory.CanBePut(newItem, slotIndices[0]);
}
if (canBePutInOriginalInventory)
{
@@ -3704,7 +3893,6 @@ namespace Barotrauma
}
}
private readonly HashSet<AttackContext> currentContexts = new HashSet<AttackContext>();
public IEnumerable<AttackContext> GetAttackContexts()
@@ -3828,5 +4016,10 @@ namespace Barotrauma
public bool IsWatchman => HasJob("watchman");
public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier;
public bool IsProtectedFromPressure()
{
return PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f);
}
}
}
@@ -153,6 +153,8 @@ namespace Barotrauma
private static ushort idCounter;
private const string disguiseName = "???";
public bool HasNickname => Name != OriginalName;
public string OriginalName { get; private set; }
public string Name;
public string DisplayName
{
@@ -349,9 +351,9 @@ namespace Barotrauma
private readonly NPCPersonalityTrait personalityTrait;
public Order CurrentOrder { get; set; }
public string CurrentOrderOption { get; set; }
public bool IsDismissed => CurrentOrder == null || CurrentOrder.Identifier.Equals("dismissed", StringComparison.OrdinalIgnoreCase);
public const int MaxCurrentOrders = 3;
public static int HighestManualOrderPriority => MaxCurrentOrders;
public List<OrderInfo> CurrentOrders { get; } = new List<OrderInfo>();
//unique ID given to character infos in MP
//used by clients to identify which infos are the same to prevent duplicate characters in round summary
@@ -453,7 +455,7 @@ namespace Barotrauma
public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1;
// Used for creating the data
public CharacterInfo(string speciesName, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced)
public CharacterInfo(string speciesName, string name = "", string originalName = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
{
@@ -503,6 +505,7 @@ namespace Barotrauma
}
}
}
OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name;
personalityTrait = NPCPersonalityTrait.GetRandom(name + HeadSpriteId);
Salary = CalculateSalary();
if (ragdollFileName != null)
@@ -518,6 +521,7 @@ namespace Barotrauma
ID = idCounter;
idCounter++;
Name = infoElement.GetAttributeString("name", "");
OriginalName = infoElement.GetAttributeString("originalname", null);
string genderStr = infoElement.GetAttributeString("gender", "male").ToLowerInvariant();
Salary = infoElement.GetAttributeInt("salary", 1000);
Enum.TryParse(infoElement.GetAttributeString("race", "White"), true, out Race race);
@@ -576,6 +580,11 @@ namespace Barotrauma
}
}
if (string.IsNullOrEmpty(OriginalName))
{
OriginalName = Name;
}
StartItemsGiven = infoElement.GetAttributeBool("startitemsgiven", false);
string personalityName = infoElement.GetAttributeString("personality", "");
ragdollFileName = infoElement.GetAttributeString("ragdoll", string.Empty);
@@ -622,7 +631,17 @@ namespace Barotrauma
public int GetIdentifier()
{
int id = ToolBox.StringToInt(Name);
return GetIdentifier(Name);
}
public int GetIdentifierUsingOriginalName()
{
return GetIdentifier(OriginalName);
}
private int GetIdentifier(string name)
{
int id = ToolBox.StringToInt(name);
id ^= HeadSpriteId;
id ^= (int)Race << 6;
id ^= HairIndex << 12;
@@ -939,12 +958,38 @@ namespace Barotrauma
partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel, Vector2 textPopupPos);
public void Rename(string newName)
{
if (string.IsNullOrEmpty(newName)) { return; }
// Replace the name tag of any existing id cards or duffel bags
foreach (var item in Item.ItemList)
{
if (item.Prefab.Identifier != "idcard" && !item.Tags.Contains("despawncontainer")) { continue; }
foreach (var tag in item.Tags.Split(','))
{
var splitTag = tag.Split(":");
if (splitTag.Length < 2) { continue; }
if (splitTag[0] != "name") { continue; }
if (splitTag[1] != Name) { continue; }
item.ReplaceTag(tag, $"name:{newName}");
break;
}
}
Name = newName;
}
public void ResetName()
{
Name = OriginalName;
}
public XElement Save(XElement parentElement)
{
XElement charElement = new XElement("Character");
charElement.Add(
new XAttribute("name", Name),
new XAttribute("originalname", OriginalName),
new XAttribute("speciesname", SpeciesName),
new XAttribute("gender", Head.gender == Gender.Male ? "male" : "female"),
new XAttribute("race", Head.race.ToString()),
@@ -957,7 +1002,7 @@ namespace Barotrauma
new XAttribute("startitemsgiven", StartItemsGiven),
new XAttribute("ragdoll", ragdollFileName),
new XAttribute("personality", personalityTrait == null ? "" : personalityTrait.Name));
// TODO: animations?
if (Character != null)
@@ -1004,13 +1049,9 @@ namespace Barotrauma
faceAttachments = null;
}
/// <summary>
/// Reset order data so it doesn't carry into further rounds, as the AI is "recreated" always in between rounds anyway.
/// </summary>
public void ResetCurrentOrder()
public void ClearCurrentOrders()
{
CurrentOrder = null;
CurrentOrderOption = "";
CurrentOrders.Clear();
}
public void Remove()
@@ -14,6 +14,9 @@ namespace Barotrauma
public Dictionary<string, SerializableProperty> SerializableProperties { get; set; }
public float PendingAdditionStrenght { get; set; }
public float AdditionStrength { get; set; }
protected float _strength;
[Serialize(0f, true), Editable]
@@ -26,7 +29,12 @@ namespace Barotrauma
{
_nonClampedStrength = value;
}
_strength = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength);
float newValue = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength);
if (newValue > _strength)
{
PendingAdditionStrenght = Prefab.GrainBurst;
}
_strength = newValue;
}
}
@@ -56,6 +64,7 @@ namespace Barotrauma
public Affliction(AfflictionPrefab prefab, float strength)
{
Prefab = prefab;
PendingAdditionStrenght = Prefab.GrainBurst;
_strength = strength;
Identifier = prefab?.Identifier;
@@ -101,13 +110,33 @@ namespace Barotrauma
return currVitalityDecrease;
}
public float GetScreenGrainStrength()
{
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) { return 0.0f; }
if (MathUtils.NearlyEqual(currentEffect.MaxGrainStrength, 0f)) { return 0.0f; }
float amount = MathHelper.Lerp(
currentEffect.MinGrainStrength,
currentEffect.MaxGrainStrength,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
if (Prefab.GrainBurst > 0 && AdditionStrength > amount)
{
return AdditionStrength;
}
return amount;
}
public float GetScreenDistortStrength()
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxScreenDistortStrength - currentEffect.MinScreenDistortStrength <= 0.0f) return 0.0f;
if (currentEffect == null) { return 0.0f; }
if (currentEffect.MaxScreenDistortStrength - currentEffect.MinScreenDistortStrength < 0.0f) { return 0.0f; }
return MathHelper.Lerp(
currentEffect.MinScreenDistortStrength,
@@ -117,10 +146,10 @@ namespace Barotrauma
public float GetRadialDistortStrength()
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxRadialDistortStrength - currentEffect.MinRadialDistortStrength <= 0.0f) return 0.0f;
if (currentEffect == null) { return 0.0f; }
if (currentEffect.MaxRadialDistortStrength - currentEffect.MinRadialDistortStrength < 0.0f) { return 0.0f; }
return MathHelper.Lerp(
currentEffect.MinRadialDistortStrength,
@@ -130,10 +159,10 @@ namespace Barotrauma
public float GetChromaticAberrationStrength()
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxChromaticAberrationStrength - currentEffect.MinChromaticAberrationStrength <= 0.0f) return 0.0f;
if (currentEffect == null) { return 0.0f; }
if (currentEffect.MaxChromaticAberrationStrength - currentEffect.MinChromaticAberrationStrength < 0.0f) { return 0.0f; }
return MathHelper.Lerp(
currentEffect.MinChromaticAberrationStrength,
@@ -143,10 +172,10 @@ namespace Barotrauma
public float GetScreenBlurStrength()
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxScreenBlurStrength - currentEffect.MinScreenBlurStrength <= 0.0f) return 0.0f;
if (currentEffect == null) { return 0.0f; }
if (currentEffect.MaxScreenBlurStrength - currentEffect.MinScreenBlurStrength < 0.0f) { return 0.0f; }
return MathHelper.Lerp(
currentEffect.MinScreenBlurStrength,
@@ -154,6 +183,20 @@ namespace Barotrauma
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
public float GetSkillMultiplier()
{
if (Strength < Prefab.ActivationThreshold) { return 1.0f; }
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) { return 1.0f; }
float amount = MathHelper.Lerp(
currentEffect.MinSkillMultiplier,
currentEffect.MaxSkillMultiplier,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
return amount;
}
public void CalculateDamagePerSecond(float currentVitalityDecrease)
{
DamagePerSecond = Math.Max(DamagePerSecond, currentVitalityDecrease - PreviousVitalityDecrease);
@@ -232,6 +275,21 @@ namespace Barotrauma
{
ApplyStatusEffect(statusEffect, deltaTime, characterHealth, targetLimb);
}
float amount = deltaTime;
if (Prefab.GrainBurst > 0)
{
amount /= Prefab.GrainBurst;
}
if (PendingAdditionStrenght >= 0)
{
AdditionStrength += amount;
PendingAdditionStrenght -= deltaTime;
}
else if (AdditionStrength > 0)
{
AdditionStrength -= amount;
}
}
public void ApplyStatusEffect(StatusEffect statusEffect, float deltaTime, CharacterHealth characterHealth, Limb targetLimb)
@@ -254,16 +312,19 @@ namespace Barotrauma
{
var targets = new List<ISerializableEntity>();
statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets);
statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targets);
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, targets);
}
}
/// <summary>
/// Use this method to skip clamping and additional logic of the setters.
/// Intended only to be used when the value is already clamped! (networking code)
/// Ideally we would keep this private, but doing so would require too much refactoring.
/// </summary>
public void SetStrength(float strength) => _strength = strength;
public void SetStrength(float strength)
{
_nonClampedStrength = strength;
_strength = _nonClampedStrength;
}
public bool ShouldShowIcon(Character afflictedCharacter)
{
@@ -102,7 +102,7 @@ namespace Barotrauma
private void ApplyDamage(float deltaTime, bool applyForce)
{
int limbCount = character.AnimController.Limbs.Count(l => !l.IgnoreCollisions && !l.IsSevered);
int limbCount = character.AnimController.Limbs.Count(l => !l.IgnoreCollisions && !l.IsSevered && !l.Hidden);
foreach (Limb limb in character.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
@@ -148,10 +148,9 @@ namespace Barotrauma
}
}
public void Remove()
public void UnsubscribeFromDeathEvent()
{
if (character == null) { return; }
DeactivateHusk();
if (character == null || !subscribedToDeathEvent) { return; }
character.OnDeath -= CharacterDead;
subscribedToDeathEvent = false;
}
@@ -159,7 +158,11 @@ namespace Barotrauma
private void CharacterDead(Character character, CauseOfDeath causeOfDeath)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (Strength < ActiveThreshold || character.Removed) { return; }
if (Strength < ActiveThreshold || character.Removed)
{
UnsubscribeFromDeathEvent();
return;
}
//don't turn the character into a husk if any of its limbs are severed
if (character.AnimController?.LimbJoints != null)
@@ -170,18 +173,22 @@ namespace Barotrauma
}
}
//character already in remove queue (being removed by something else, for example a modded affliction that uses AfflictionHusk as the base)
// -> don't spawn the AI husk
if (Entity.Spawner.IsInRemoveQueue(character)) { return; }
//create the AI husk in a coroutine to ensure that we don't modify the character list while enumerating it
CoroutineManager.StartCoroutine(CreateAIHusk());
}
private IEnumerable<object> CreateAIHusk()
{
//character already in remove queue (being removed by something else, for example a modded affliction that uses AfflictionHusk as the base)
// -> don't spawn the AI husk
if (Entity.Spawner.IsInRemoveQueue(character))
{
yield return CoroutineStatus.Success;
}
character.Enabled = false;
Entity.Spawner.AddToRemoveQueue(character);
UnsubscribeFromDeathEvent();
string huskedSpeciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk);
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName);
@@ -111,7 +111,7 @@ namespace Barotrauma
public readonly bool NeedsAir;
}
class AfflictionPrefab : IPrefab, IDisposable
class AfflictionPrefab : IPrefab, IDisposable, IHasUintIdentifier
{
public class Effect
{
@@ -128,11 +128,14 @@ namespace Barotrauma
public float MinScreenBlurStrength, MaxScreenBlurStrength;
public float MinScreenDistortStrength, MaxScreenDistortStrength;
public float MinGrainStrength, MaxGrainStrength;
public float MinRadialDistortStrength, MaxRadialDistortStrength;
public float MinChromaticAberrationStrength, MaxChromaticAberrationStrength;
public float MinSpeedMultiplier, MaxSpeedMultiplier;
public float MinBuffMultiplier, MaxBuffMultiplier;
public float MinSkillMultiplier, MaxSkillMultiplier;
public float MinResistance, MaxResistance;
public string ResistanceFor;
public string DialogFlag;
@@ -163,10 +166,17 @@ namespace Barotrauma
MaxChromaticAberrationStrength = element.GetAttributeFloat("maxchromaticaberration", 0.0f);
MaxChromaticAberrationStrength = Math.Max(MinChromaticAberrationStrength, MaxChromaticAberrationStrength);
MinGrainStrength = element.GetAttributeFloat(nameof(MinGrainStrength).ToLower(), 0.0f);
MaxGrainStrength = element.GetAttributeFloat(nameof(MaxGrainStrength).ToLower(), 0.0f);
MaxGrainStrength = Math.Max(MinGrainStrength, MaxGrainStrength);
MinScreenBlurStrength = element.GetAttributeFloat("minscreenblur", 0.0f);
MaxScreenBlurStrength = element.GetAttributeFloat("maxscreenblur", 0.0f);
MaxScreenBlurStrength = Math.Max(MinScreenBlurStrength, MaxScreenBlurStrength);
MinSkillMultiplier = element.GetAttributeFloat("minskillmultiplier", 1.0f);
MaxSkillMultiplier = element.GetAttributeFloat("maxskillmultiplier", 1.0f);
ResistanceFor = element.GetAttributeString("resistancefor", "");
MinResistance = element.GetAttributeFloat("minresistance", 0.0f);
MaxResistance = element.GetAttributeFloat("maxresistance", 0.0f);
@@ -228,6 +238,7 @@ namespace Barotrauma
public static AfflictionPrefab Bloodloss;
public static AfflictionPrefab Pressure;
public static AfflictionPrefab Stun;
public static AfflictionPrefab RadiationSickness;
public static readonly PrefabCollection<AfflictionPrefab> Prefabs = new PrefabCollection<AfflictionPrefab>();
@@ -256,7 +267,7 @@ namespace Barotrauma
/// Unique identifier that's generated by hashing the prefab's string identifier.
/// Used to reduce the amount of bytes needed to write affliction data into network messages in multiplayer.
/// </summary>
public uint UIntIdentifier;
public uint UIntIdentifier { get; set; }
// Arbitrary string that is used to identify the type of the affliction.
public readonly string AfflictionType;
@@ -273,6 +284,7 @@ namespace Barotrauma
public ContentPackage ContentPackage { get; private set; }
public readonly string Name, Description;
public readonly string TranslationOverride;
public readonly bool IsBuff;
public readonly string CauseOfDeathDescription, SelfCauseOfDeathDescription;
@@ -285,9 +297,14 @@ namespace Barotrauma
public readonly float ShowIconToOthersThreshold = 0.05f;
public readonly float MaxStrength = 100.0f;
public readonly float GrainBurst;
//how high the strength has to be for the affliction icon to be shown with a health scanner
public readonly float ShowInHealthScannerThreshold = 0.05f;
//how strong the affliction needs to be before bots attempt to treat it
public readonly float TreatmentThreshold = 5.0f;
//how much karma changes when a player applies this affliction to someone (per strength of the affliction)
public float KarmaChangeOnApplied;
@@ -337,6 +354,7 @@ namespace Barotrauma
Bloodloss = null;
Pressure = null;
Stun = null;
RadiationSickness = null;
#if CLIENT
CharacterHealth.DamageOverlay?.Remove();
CharacterHealth.DamageOverlay = null;
@@ -361,6 +379,7 @@ namespace Barotrauma
if (Bloodloss == null) { DebugConsole.ThrowError("Affliction \"Bloodloss\" not defined in the affliction prefabs."); }
if (Pressure == null) { DebugConsole.ThrowError("Affliction \"Pressure\" not defined in the affliction prefabs."); }
if (Stun == null) { DebugConsole.ThrowError("Affliction \"Stun\" not defined in the affliction prefabs."); }
if (RadiationSickness == null) { DebugConsole.ThrowError("Affliction \"RadiationSickness\" not defined in the affliction prefabs."); }
}
public static void LoadFromFile(ContentFile file)
@@ -372,6 +391,9 @@ namespace Barotrauma
{
DebugConsole.ThrowError("Cannot override all afflictions, because many of them are required by the main game! Please try overriding them one by one.");
}
List<(AfflictionPrefab prefab, XElement element)> loadedAfflictions = new List<(AfflictionPrefab prefab, XElement element)>();
foreach (XElement element in mainElement.Elements())
{
bool isOverride = element.IsOverride();
@@ -436,6 +458,7 @@ namespace Barotrauma
prefab = new AfflictionPrefab(sourceElement, file.Path, typeof(AfflictionBleeding));
break;
case "huskinfection":
case "alieninfection":
prefab = new AfflictionPrefabHusk(sourceElement, file.Path, typeof(AfflictionHusk));
break;
case "cprsettings":
@@ -498,27 +521,25 @@ namespace Barotrauma
case "stun":
Stun = prefab;
break;
case "radiationsickness":
RadiationSickness = prefab;
break;
}
if (ImpactDamage == null) { ImpactDamage = InternalDamage; }
if (prefab != null)
{
loadedAfflictions.Add((prefab, sourceElement));
Prefabs.Add(prefab, isOverride);
prefab.CalculatePrefabUIntIdentifier(Prefabs);
}
}
using MD5 md5 = MD5.Create();
foreach (AfflictionPrefab prefab in Prefabs)
//load the effects after all the afflictions in the file have been instantiated
//otherwise afflictions can't inflict other afflictions that are defined at a later point in the file
foreach ((AfflictionPrefab prefab, XElement element) in loadedAfflictions)
{
prefab.UIntIdentifier = ToolBox.StringToUInt32Hash(prefab.Identifier, md5);
//it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small
var collision = Prefabs.Find(p => p != prefab && p.UIntIdentifier == prefab.UIntIdentifier);
if (collision != null)
{
DebugConsole.ThrowError("Hashing collision when generating uint identifiers for Afflictions: " + prefab.Identifier + " has the same identifier as " + collision.Identifier + " (" + prefab.UIntIdentifier + ")");
collision.UIntIdentifier++;
}
prefab.LoadEffects(element);
}
}
@@ -549,8 +570,10 @@ namespace Barotrauma
Identifier = element.GetAttributeString("identifier", "");
AfflictionType = element.GetAttributeString("type", "");
Name = TextManager.Get("AfflictionName." + Identifier, true) ?? element.GetAttributeString("name", "");
Description = TextManager.Get("AfflictionDescription." + Identifier, true) ?? element.GetAttributeString("description", "");
TranslationOverride = element.GetAttributeString("translationoverride", null);
string translationId = TranslationOverride ?? Identifier;
Name = TextManager.Get("AfflictionName." + translationId, true) ?? element.GetAttributeString("name", "");
Description = TextManager.Get("AfflictionDescription." + translationId, true) ?? element.GetAttributeString("description", "");
IsBuff = element.GetAttributeBool("isbuff", false);
LimbSpecific = element.GetAttributeBool("limbspecific", false);
@@ -567,16 +590,18 @@ namespace Barotrauma
ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", Math.Max(ActivationThreshold, 0.05f));
ShowIconToOthersThreshold = element.GetAttributeFloat("showicontoothersthreshold", ShowIconThreshold);
MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f);
GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLower(), 0.0f);
ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, 0.05f));
TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f));
DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f);
BurnOverlayAlpha = element.GetAttributeFloat("burnoverlayalpha", 0.0f);
KarmaChangeOnApplied = element.GetAttributeFloat("karmachangeonapplied", 0.0f);
CauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeath." + Identifier, true) ?? element.GetAttributeString("causeofdeathdescription", "");
SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + Identifier, true) ?? element.GetAttributeString("selfcauseofdeathdescription", "");
CauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeath." + translationId, true) ?? element.GetAttributeString("causeofdeathdescription", "");
SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + translationId, true) ?? element.GetAttributeString("selfcauseofdeathdescription", "");
IconColors = element.GetAttributeColorArray("iconcolors", null);
AchievementOnRemoved = element.GetAttributeString("achievementonremoved", "");
@@ -588,12 +613,6 @@ namespace Barotrauma
case "icon":
Icon = new Sprite(subElement);
break;
case "effect":
effects.Add(new Effect(subElement, Name));
break;
case "periodiceffect":
periodicEffects.Add(new PeriodicEffect(subElement, Name));
break;
}
}
@@ -618,6 +637,22 @@ namespace Barotrauma
constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) });
}
private void LoadEffects(XElement element)
{
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "effect":
effects.Add(new Effect(subElement, Name));
break;
case "periodiceffect":
periodicEffects.Add(new PeriodicEffect(subElement, Name));
break;
}
}
}
public override string ToString()
{
return "AfflictionPrefab (" + Name + ")";
@@ -36,7 +36,11 @@ namespace Barotrauma
public LimbHealth(XElement element, CharacterHealth characterHealth)
{
Name = TextManager.Get("HealthLimbName." + element.GetAttributeString("name", ""));
string limbName = element.GetAttributeString("name", null) ?? "generic";
if (limbName != "generic")
{
Name = TextManager.Get("HealthLimbName." + limbName);
}
this.characterHealth = characterHealth;
foreach (XElement subElement in element.Elements())
{
@@ -186,12 +190,14 @@ namespace Barotrauma
set { bloodlossAffliction.Strength = MathHelper.Clamp(value, 0.0f, 100.0f); }
}
public float StunTimer
public float Stun
{
get { return stunAffliction.Strength; }
set { stunAffliction.Strength = MathHelper.Clamp(value, 0.0f, stunAffliction.Prefab.MaxStrength); }
}
public float StunTimer { get; private set; }
public Affliction PressureAffliction
{
get { return pressureAffliction; }
@@ -484,7 +490,7 @@ namespace Barotrauma
CalculateVitality();
}
public void ApplyDamage(Limb hitLimb, AttackResult attackResult)
public void ApplyDamage(Limb hitLimb, AttackResult attackResult, bool allowStacking = true)
{
if (Unkillable || Character.GodMode) { return; }
if (hitLimb.HealthIndex < 0 || hitLimb.HealthIndex >= limbHealths.Count)
@@ -498,11 +504,11 @@ namespace Barotrauma
{
if (newAffliction.Prefab.LimbSpecific)
{
AddLimbAffliction(hitLimb, newAffliction);
AddLimbAffliction(hitLimb, newAffliction, allowStacking);
}
else
{
AddAffliction(newAffliction);
AddAffliction(newAffliction, allowStacking);
}
}
}
@@ -569,7 +575,7 @@ namespace Barotrauma
CalculateVitality();
}
private void AddLimbAffliction(Limb limb, Affliction newAffliction)
private void AddLimbAffliction(Limb limb, Affliction newAffliction, bool allowStacking = true)
{
if (!newAffliction.Prefab.LimbSpecific || limb == null) { return; }
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
@@ -578,10 +584,10 @@ namespace Barotrauma
"\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex);
return;
}
AddLimbAffliction(limbHealths[limb.HealthIndex], newAffliction);
AddLimbAffliction(limbHealths[limb.HealthIndex], newAffliction, allowStacking);
}
private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction)
private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction, bool allowStacking = true)
{
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
@@ -590,7 +596,15 @@ namespace Barotrauma
{
if (newAffliction.Prefab == affliction.Prefab)
{
affliction.Strength = Math.Min(affliction.Prefab.MaxStrength, affliction.Strength + (newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier))));
float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier));
if (allowStacking)
{
// Add the existing strength
newStrength += affliction.Strength;
}
newStrength = Math.Min(affliction.Prefab.MaxStrength, newStrength);
if (affliction == stunAffliction) { Character.SetStun(newStrength, true, true); }
affliction.Strength = newStrength;
affliction.Source = newAffliction.Source;
CalculateVitality();
if (Vitality <= MinVitality)
@@ -620,13 +634,12 @@ namespace Barotrauma
#endif
}
private void AddAffliction(Affliction newAffliction)
private void AddAffliction(Affliction newAffliction, bool allowStacking = true)
{
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
if (newAffliction.Prefab.AfflictionType == "huskinfection")
if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab)
{
var huskPrefab = newAffliction.Prefab as AfflictionPrefabHusk;
if (huskPrefab.TargetSpecies.None(s => s.Equals(Character.SpeciesName, StringComparison.OrdinalIgnoreCase)))
{
return;
@@ -636,7 +649,13 @@ namespace Barotrauma
{
if (newAffliction.Prefab == affliction.Prefab)
{
float newStrength = Math.Min(affliction.Prefab.MaxStrength, affliction.Strength + (newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier))));
float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier));
if (allowStacking)
{
// Add the existing strength
newStrength += affliction.Strength;
}
newStrength = Math.Min(affliction.Prefab.MaxStrength, newStrength);
if (affliction == stunAffliction) { Character.SetStun(newStrength, true, true); }
affliction.Strength = newStrength;
affliction.Source = newAffliction.Source;
@@ -664,7 +683,6 @@ namespace Barotrauma
}
}
partial void UpdateProjSpecific(float deltaTime);
partial void UpdateLimbAfflictionOverlays();
@@ -673,6 +691,8 @@ namespace Barotrauma
{
UpdateOxygen(deltaTime);
StunTimer = Stun > 0 ? StunTimer + deltaTime : 0;
for (int i = 0; i < limbHealths.Count; i++)
{
for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--)
@@ -686,12 +706,16 @@ namespace Barotrauma
for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--)
{
var affliction = limbHealths[i].Afflictions[j];
Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == i);
Limb targetLimb = Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == i);
if (targetLimb == null)
{
targetLimb = Character.AnimController.MainLimb;
}
affliction.Update(this, targetLimb, deltaTime);
affliction.DamagePerSecondTimer += deltaTime;
if (affliction is AfflictionBleeding)
if (affliction is AfflictionBleeding bleeding)
{
UpdateBleedingProjSpecific((AfflictionBleeding)affliction, targetLimb, deltaTime);
UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime);
}
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
}
@@ -788,6 +812,13 @@ namespace Barotrauma
Vitality -= vitalityDecrease;
affliction.CalculateDamagePerSecond(vitalityDecrease);
}
#if CLIENT
if (IsUnconscious)
{
HintManager.OnCharacterUnconscious(Character);
}
#endif
}
private void Kill()
@@ -877,6 +908,7 @@ namespace Barotrauma
float minSuitability = -10, maxSuitability = 10;
foreach (Affliction affliction in GetAllAfflictions())
{
if (affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
foreach (KeyValuePair<string, float> treatment in affliction.Prefab.TreatmentSuitability)
{
if (!treatmentSuitability.ContainsKey(treatment.Key))
@@ -20,6 +20,15 @@ namespace Barotrauma
[Serialize(1f, false)]
public float HealthMultiplier { get; protected set; }
[Serialize(1f, false)]
public float HealthMultiplierInMultiplayer { get; protected set; }
[Serialize(1f, false)]
public float AimSpeed { get; protected set; }
[Serialize(1f, false)]
public float AimAccuracy { get; protected set; }
private readonly HashSet<string> moduleFlags = new HashSet<string>();
[Serialize("", true, "What outpost module tags does the NPC prefer to spawn in.")]
@@ -67,6 +76,9 @@ namespace Barotrauma
[Serialize(AIObjectiveIdle.BehaviorType.Passive, false)]
public AIObjectiveIdle.BehaviorType Behavior { get; protected set; }
[Serialize(float.PositiveInfinity, false)]
public float ReportRange { get; protected set; }
public List<string> PreferredOutpostModuleTypes { get; protected set; }
public string OriginalName { get { return Identifier; } }
@@ -105,16 +117,54 @@ namespace Barotrauma
return Job != null && Job != "any" ? JobPrefab.Get(Job) : JobPrefab.Random(randSync);
}
public void GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced)
public void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn = null)
{
npc.CharacterHealth.MaxVitality *= HealthMultiplier;
if (GameMain.NetworkMember != null)
{
npc.CharacterHealth.MaxVitality *= HealthMultiplierInMultiplayer;
}
var humanAI = npc.AIController as HumanAIController;
if (humanAI != null)
{
var idleObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveIdle>();
if (positionToStayIn != null && Behavior == AIObjectiveIdle.BehaviorType.StayInHull)
{
idleObjective.TargetHull = AIObjectiveGoTo.GetTargetHull(positionToStayIn);
idleObjective.Behavior = AIObjectiveIdle.BehaviorType.StayInHull;
}
else
{
idleObjective.Behavior = Behavior;
foreach (string moduleType in PreferredOutpostModuleTypes)
{
idleObjective.PreferredOutpostModuleTypes.Add(moduleType);
}
}
humanAI.ReportRange = ReportRange;
humanAI.AimSpeed = AimSpeed;
humanAI.AimAccuracy = AimAccuracy;
}
if (CampaignInteractionType != CampaignMode.InteractionType.None)
{
(GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(npc, CampaignInteractionType);
if (positionToStayIn != null && humanAI != null)
{
humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200));
}
}
}
public void GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced, bool createNetworkEvents = true)
{
var spawnItems = ToolBox.SelectWeightedRandom(ItemSets.Keys.ToList(), ItemSets.Values.ToList(), randSync);
foreach (XElement itemElement in spawnItems.GetChildElements("item"))
{
InitializeItems(character, itemElement, submarine);
InitializeItems(character, itemElement, submarine, createNetworkEvents: createNetworkEvents);
}
}
private void InitializeItems(Character character, XElement itemElement, Submarine submarine, Item parentItem = null)
private void InitializeItems(Character character, XElement itemElement, Submarine submarine, Item parentItem = null, bool createNetworkEvents = true)
{
ItemPrefab itemPrefab;
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
@@ -126,7 +176,7 @@ namespace Barotrauma
}
Item item = new Item(itemPrefab, character.Position, null);
#if SERVER
if (GameMain.Server != null && Entity.Spawner != null)
if (GameMain.Server != null && Entity.Spawner != null && createNetworkEvents)
{
if (GameMain.Server.EntityEventManager.UniqueEvents.Any(ev => ev.Entity == item))
{
@@ -187,7 +237,7 @@ namespace Barotrauma
}
foreach (XElement childItemElement in itemElement.Elements())
{
InitializeItems(character, childItemElement, submarine, item);
InitializeItems(character, childItemElement, submarine, item, createNetworkEvents);
}
}
}
@@ -187,16 +187,18 @@ namespace Barotrauma
}
}
if (item.Prefab.Identifier == "idcard" && spawnPoint != null)
if (item.Prefab.Identifier == "idcard")
{
foreach (string s in spawnPoint.IdCardTags)
if (spawnPoint != null)
{
item.AddTag(s);
foreach (string s in spawnPoint.IdCardTags)
{
item.AddTag(s);
if (!string.IsNullOrWhiteSpace(spawnPoint.IdCardDesc)) { item.Description = spawnPoint.IdCardDesc; }
}
}
item.AddTag("name:" + character.Name);
item.AddTag("job:" + Name);
if (!string.IsNullOrWhiteSpace(spawnPoint.IdCardDesc))
item.Description = spawnPoint.IdCardDesc;
IdCard idCardComponent = item.GetComponent<IdCard>();
if (idCardComponent != null)
@@ -203,7 +203,7 @@ namespace Barotrauma
partial class Limb : ISerializableEntity, ISpatialEntity
{
//how long it takes for severed limbs to fade out
public float SeveredFadeOutTime => Params.SeveredFadeOutTime;
public float SeveredFadeOutTime { get; private set; } = 10;
public readonly Character character;
/// <summary>
@@ -308,6 +308,12 @@ namespace Barotrauma
set
{
if (isSevered == value) { return; }
if (value == true)
{
// If any of the connected limbs have a longer fade out time, use that
var connectedLimbs = GetConnectedLimbs();
SeveredFadeOutTime = Math.Max(Params.SeveredFadeOutTime, connectedLimbs.Any() ? connectedLimbs.Max(l => l.SeveredFadeOutTime) : 0);
}
isSevered = value;
if (isSevered)
{
@@ -726,6 +732,10 @@ namespace Barotrauma
{
newAffliction = affliction.CreateMultiplied(finalDamageModifier);
}
else
{
newAffliction.SetStrength(affliction.NonClampedStrength);
}
if (applyAffliction)
{
@@ -861,6 +871,23 @@ namespace Barotrauma
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos));
bool wasRunning = attack.IsRunning;
attack.UpdateAttackTimer(deltaTime, character);
if (attack.Blink)
{
if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Any())
{
foreach (int limbIndex in attack.ForceOnLimbIndices)
{
if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) { continue; }
Limb limb = character.AnimController.Limbs[limbIndex];
if (limb.IsSevered) { continue; }
limb.Blink();
}
}
else
{
Blink();
}
}
bool wasHit = false;
Body structureBody = null;
@@ -871,11 +898,11 @@ namespace Barotrauma
case HitDetection.Distance:
if (dist < attack.DamageRange)
{
structureBody = Submarine.PickBody(simPos, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true);
if (structureBody?.UserData as string == "ruinroom")
structureBody = Submarine.PickBody(simPos, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true, customPredicate:
(Fixture f) =>
{
structureBody = null;
}
return f?.Body?.UserData as string != "ruinroom";
});
if (damageTarget is Item i && i.GetComponent<Items.Components.Door>() != null)
{
// If the attack is aimed to an item and hits an item, it's successful.
@@ -1098,12 +1125,26 @@ namespace Barotrauma
foreach (StatusEffect statusEffect in statusEffects)
{
if (statusEffect.type != actionType) { continue; }
if (statusEffect.type == ActionType.OnDamaged)
{
if (statusEffect.AllowedAfflictions != null && (character.LastDamage.Afflictions == null || character.LastDamage.Afflictions.None(a => statusEffect.AllowedAfflictions.Contains(a.Prefab.AfflictionType) || statusEffect.AllowedAfflictions.Contains(a.Prefab.Identifier))))
{
continue;
}
if (statusEffect.OnlyPlayerTriggered)
{
if (character.LastAttacker == null || !character.LastAttacker.IsPlayer)
{
continue;
}
}
}
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
statusEffect.GetNearbyTargets(WorldPosition, targets);
statusEffect.Apply(ActionType.OnActive, deltaTime, character, targets);
statusEffect.Apply(actionType, deltaTime, character, targets);
}
else
{
@@ -1111,7 +1152,40 @@ namespace Barotrauma
{
statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition);
}
statusEffect.Apply(actionType, deltaTime, character, this, WorldPosition);
else if (statusEffect.targetLimbs != null)
{
foreach (var limbType in statusEffect.targetLimbs)
{
if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
// Target all matching limbs
foreach (var limb in ragdoll.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.type == limbType)
{
statusEffect.Apply(actionType, deltaTime, character, limb);
}
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
{
// Target just the first matching limb
Limb limb = ragdoll.GetLimb(limbType);
statusEffect.Apply(actionType, deltaTime, character, limb);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb))
{
// Target just the last matching limb
Limb limb = ragdoll.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden);
statusEffect.Apply(actionType, deltaTime, character, limb);
}
}
}
else
{
statusEffect.Apply(actionType, deltaTime, character, this, WorldPosition);
}
}
}
}
@@ -1121,7 +1195,12 @@ namespace Barotrauma
private float TotalBlinkDurationOut => Params.BlinkDurationOut + Params.BlinkHoldTime;
public void Blink(float deltaTime, float referenceRotation)
public void Blink()
{
blinkTimer = -TotalBlinkDurationOut;
}
public void UpdateBlink(float deltaTime, float referenceRotation)
{
if (blinkTimer > -TotalBlinkDurationOut)
{
@@ -1155,6 +1234,26 @@ namespace Barotrauma
}
}
public IEnumerable<LimbJoint> GetConnectedJoints() => ragdoll.LimbJoints.Where(j => !j.IsSevered && (j.LimbA == this || j.LimbB == this));
public IEnumerable<Limb> GetConnectedLimbs()
{
var connectedJoints = GetConnectedJoints();
var connectedLimbs = new HashSet<Limb>();
foreach (Limb limb in ragdoll.Limbs)
{
var otherJoints = limb.GetConnectedJoints();
foreach (LimbJoint connectedJoint in connectedJoints)
{
if (otherJoints.Contains(connectedJoint))
{
connectedLimbs.Add(limb);
}
}
}
return connectedLimbs;
}
public void Remove()
{
body?.Remove();
@@ -54,7 +54,7 @@ namespace Barotrauma
abstract class SwimParams : AnimationParams
{
[Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 500, ValueStep = 1)]
[Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float SteerTorque { get; set; }
}
@@ -173,13 +173,13 @@ namespace Barotrauma
[Editable, Serialize(true, true, description: "Should the character face towards the direction it's heading.")]
public bool RotateTowardsMovement { get; set; }
[Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
[Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)]
public float TorsoTorque { get; set; }
[Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
[Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)]
public float HeadTorque { get; set; }
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 2000, ValueStep = 1)]
public float TailTorque { get; set; }
[Serialize(1f, true, description: "Multiplier applied based on the angle difference between the tail and the main limb. Increasing the value prevents snake-like characters from getting tangled on themselves. Default = 1 (no boost)"), Editable(MinValueFloat = 1, MaxValueFloat = 100)]
@@ -49,6 +49,9 @@ namespace Barotrauma
[Serialize(false, false), Editable]
public bool CanSpeak { get; set; }
[Serialize(false, true), Editable]
public bool UseBossHealthBar { get; private set; }
[Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 100000f)]
public float Noise { get; set; }
@@ -64,6 +67,9 @@ namespace Barotrauma
[Serialize("waterblood", true), Editable]
public string BleedParticleWater { get; private set; }
[Serialize(1f, true), Editable]
public float BleedParticleMultiplier { get; private set; }
[Serialize(10f, true, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)]
public float EatingSpeed { get; set; }
@@ -76,6 +82,12 @@ namespace Barotrauma
[Serialize(0f, true), Editable]
public float SonarDisruption { get; set; }
[Serialize(0f, true), Editable]
public float DistantSonarRange { get; set; }
[Serialize(25000f, true, "If the character is farther than this (in pixels) from the sub and the players, it will be disabled. The halved value is used for triggering simple physics where the ragdoll is disabled and only the main collider is updated."), Editable(MinValueFloat = 10000f, MaxValueFloat = 100000f)]
public float DisableDistance { get; set; }
public readonly string File;
public XDocument VariantFile { get; private set; }
@@ -118,10 +130,11 @@ namespace Barotrauma
// TODO: Make recursive? In practice we don't have to go deeper than this, but the implementation would be a lot cleaner with recursion.
foreach (XElement subSubElement in subElement.Elements())
{
matchingParams = matchingParams.SubParams.FirstOrDefault(p => p.Name.Equals(subSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase));
if (matchingParams != null)
if (subSubElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; }
var matchingSubParams = matchingParams.SubParams.FirstOrDefault(p => p.Name.Equals(subSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase));
if (matchingSubParams != null)
{
TryLoadOverride(matchingParams, subSubElement, matchingParams.SerializableProperties);
TryLoadOverride(matchingSubParams, subSubElement, matchingSubParams.SerializableProperties);
}
}
}
@@ -423,10 +436,10 @@ namespace Barotrauma
[Serialize(false, true)]
public bool UseHealthWindow { get; set; }
[Serialize(0f, true, description: "How easily the character heals from the bleeding wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
[Serialize(0f, true, description: "How easily the character heals from the bleeding wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)]
public float BleedingReduction { get; private set; }
[Serialize(0f, true, description: "How easily the character heals from the burn wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
[Serialize(0f, true, description: "How easily the character heals from the burn wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)]
public float BurnReduction { get; private set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
@@ -522,21 +535,36 @@ namespace Barotrauma
[Serialize(20f, true, description: "How long the creature flees before returning to normal state. When the creature sees the target or is being chased, it will always flee, if it's in the flee state."), Editable(minValue: 0f, maxValue: 100f)]
public float MinFleeTime { get; private set; }
[Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable()]
[Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable]
public bool AggressiveBoarding { get; private set; }
[Serialize(true, true, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable()]
[Serialize(true, true, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable]
public bool EnforceAggressiveBehaviorForMissions { get; private set; }
[Serialize(true, true, description: "Should the character target or ignore walls when it's outside the submarine."), Editable()]
[Serialize(true, true, description: "Should the character target or ignore walls when it's outside the submarine."), Editable]
public bool TargetOuterWalls { get; private set; }
[Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable()]
[Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable]
public bool RandomAttack { get; private set; }
[Serialize(false, true, description:"Can the character open doors and hatches without a proper id card? Only applies on humanoids.")]
[Serialize(false, true, description:"Can the character open doors and hatches without a proper id card? Only applies on humanoids."), Editable]
public bool Infiltrate { get; private set; }
[Serialize(true, true, "Is the creature allowed to navigate from and into the depths of the abyss? When enabled, the creatures will try to avoid the depths."), Editable]
public bool AvoidAbyss { get; set; }
[Serialize(false, true, "Does the creature try to keep in the abyss? Has effect only when AvoidAbyss is false."), Editable]
public bool StayInAbyss { get; set; }
[Serialize(0f, true, description: ""), Editable]
public float StartAggression { get; private set; }
[Serialize(100f, true, description: ""), Editable]
public float MaxAggression { get; private set; }
[Serialize(0f, true, description: ""), Editable]
public float AggressionCumulation { get; private set; }
public IEnumerable<TargetParams> Targets => targets;
protected readonly List<TargetParams> targets = new List<TargetParams>();
@@ -639,9 +667,19 @@ namespace Barotrauma
[Serialize(false, true, description: "Should the target be ignored while the creature is outside. Doesn't matter where the target is."), Editable]
public bool IgnoreOutside { get; set; }
[Serialize(false, true)]
[Serialize(false, true, description: "Should the target be ignored if it's inside a different submarine than us? Normally only some targets are ignored when they are not inside the same sub."), Editable]
public bool IgnoreIfNotInSameSub { get; set; }
[Serialize(false, true), Editable]
public bool IgnoreIncapacitated { get; set; }
[Serialize(0f, true, description: "How much damage the protected target should take from an attacker before the creature starts defending it."), Editable]
public float DamageThreshold { get; private set; }
[Serialize(AttackPattern.Straight, true), Editable]
public AttackPattern AttackPattern { get; set; }
#region Sweep
[Serialize(0f, true, description: "Use to define a distance at which the creature starts the sweeping movement."), Editable(MinValueFloat = 0, MaxValueFloat = 10000, ValueStep = 1, DecimalCount = 0)]
public float SweepDistance { get; private set; }
@@ -650,9 +688,21 @@ namespace Barotrauma
[Serialize(1f, true, description: "How quickly the sweep direction changes. Uses the sine wave pattern."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)]
public float SweepSpeed { get; private set; }
#endregion
[Serialize(0f, true, description: "How much damage the protected target should take from an attacker before the creature starts defending it.")]
public float Threshold { get; private set; }
#region Circle
[Serialize(5000f, true), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)]
public float CircleStartDistance { get; private set; }
[Serialize(1f, true), Editable(MinValueFloat = 0.5f, MaxValueFloat = 2f)]
public float CircleRotationSpeed { get; private set; }
[Serialize(5f, true), Editable(MinValueFloat = 1f, MaxValueFloat = 10f)]
public float CircleStrikeDistanceMultiplier { get; private set; }
[Serialize(0f, true), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)]
public float CircleMaxRandomOffset { get; private set; }
#endregion
public TargetParams(XElement element, CharacterParams character) : base(element, character) { }
@@ -599,7 +599,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)]
[Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)]
public float Density { get; set; }
[Serialize(false, true), Editable]
@@ -79,6 +79,11 @@ namespace Barotrauma
OnExecute(args);
}
public override int GetHashCode()
{
return names[0].GetHashCode();
}
}
private static readonly Queue<ColoredText> queuedMessages = new Queue<ColoredText>();
@@ -339,7 +344,7 @@ namespace Barotrauma
return new string[][]
{
GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
commands.Select(c => c.names[0]).ToArray()
commands.Select(c => c.names[0]).Union(new string[]{ "All" }).ToArray()
};
}));
@@ -351,7 +356,7 @@ namespace Barotrauma
return new string[][]
{
GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
new string[0]
commands.Select(c => c.names[0]).Union(new string[]{ "All" }).ToArray()
};
}));
@@ -604,7 +609,7 @@ namespace Barotrauma
commands.Add(new Command("giveaffliction", "giveaffliction [affliction name] [affliction strength] [character name]: Add an affliction to a character. If the name parameter is omitted, the affliction is added to the controlled character.", (string[] args) =>
{
if (args.Length < 2) return;
if (args.Length < 2) { return; }
AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a =>
a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) ||
@@ -621,9 +626,19 @@ namespace Barotrauma
return;
}
Character targetCharacter = (args.Length <= 2) ? Character.Controlled : FindMatchingCharacter(args.Skip(2).ToArray());
bool relativeStrength = false;
if (args.Length > 2)
{
bool.TryParse(args[2], out relativeStrength);
}
Character targetCharacter = (relativeStrength || args.Length <= 2) ? Character.Controlled : FindMatchingCharacter(args.Skip(2).ToArray());
if (targetCharacter != null)
{
if (relativeStrength)
{
afflictionStrength *= targetCharacter.MaxVitality / afflictionPrefab.MaxStrength;
}
targetCharacter.CharacterHealth.ApplyAffliction(targetCharacter.AnimController.MainLimb, afflictionPrefab.Instantiate(afflictionStrength));
}
},
@@ -707,9 +722,10 @@ namespace Barotrauma
commands.Add(new Command("freecamera|freecam", "freecam: Detach the camera from the controlled character.", (string[] args) =>
{
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen) { return; }
Character.Controlled = null;
GameMain.GameScreen.Cam.TargetPos = Vector2.Zero;
#if CLIENT
GameMain.Client?.SendConsoleCommand("freecam");
#endif
}, isCheat: true));
@@ -728,19 +744,19 @@ namespace Barotrauma
List<EventPrefab> eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => !string.IsNullOrWhiteSpace(prefab.Identifier)).ToList();
if (GameMain.GameSession?.EventManager != null && args.Length > 0)
{
EventPrefab newEvent = eventPrefabs.Find(prefab => string.Equals(prefab.Identifier, args[0], StringComparison.InvariantCultureIgnoreCase));
EventPrefab eventPrefab = eventPrefabs.Find(prefab => string.Equals(prefab.Identifier, args[0], StringComparison.InvariantCultureIgnoreCase));
if (newEvent != null)
if (eventPrefab != null)
{
var @event = newEvent.CreateInstance();
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null)
{
NewMessage($"Could not initialize event {args[0]} because level did not meet requirements");
return;
}
GameMain.GameSession.EventManager.ActiveEvents.Add(@event);
@event.Init(true);
NewMessage($"Initialized event {newEvent.Identifier}", Color.Aqua);
GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent);
newEvent.Init(true);
NewMessage($"Initialized event {eventPrefab.Identifier}", Color.Aqua);
return;
}
@@ -996,7 +1012,7 @@ namespace Barotrauma
}
else
{
ThrowError("Could not set location reputation ({args[0]} is not a valid reputation value).");
ThrowError($"Could not set location reputation ({args[0]} is not a valid reputation value).");
}
}
else
@@ -1004,6 +1020,41 @@ namespace Barotrauma
ThrowError("Could not set location reputation (no active campaign).");
}
}, null, true));
commands.Add(new Command("setreputation", "setreputation [faction] [value]: Set the reputation of a cation to the specified value.", (string[] args) =>
{
if (args.Length < 2)
{
ThrowError("Insufficient arguments (expected 2)");
return;
}
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
if (campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase)) is { } faction)
{
if (float.TryParse(args[1], NumberStyles.Any, CultureInfo.InvariantCulture, out float reputation))
{
faction.Reputation.Value = reputation;
}
else
{
ThrowError($"Could not set faction reputation ({args[1]} is not a valid reputation value).");
}
}
else
{
ThrowError($"Could not set faction reputation (faction {args[0]} not found).");
}
}
else
{
ThrowError("Could not set faction reputation (no active campaign).");
}
}, () =>
{
return new[] { FactionPrefab.Prefabs.Select(f => f.Identifier).ToArray() };
}, true));
commands.Add(new Command("fixitems", "fixitems: Repairs all items and restores them to full condition.", (string[] args) =>
{
@@ -1127,6 +1178,56 @@ namespace Barotrauma
UpgradePrefab.Prefabs.Select(c => c.Identifier).Distinct().ToArray()
};
}, true));
commands.Add(new Command("maxupgrades", "maxupgrades [category] [prefab]: Maxes out all upgrades or only specific one if given arguments.", args =>
{
UpgradeManager upgradeManager = GameMain.GameSession?.Campaign?.UpgradeManager;
if (upgradeManager == null)
{
ThrowError("This command can only be used in campaign.");
return;
}
string categoryIdentifier = null;
string prefabIdentifier = null;
switch (args.Length)
{
case 1:
categoryIdentifier = args[0];
break;
case 2:
categoryIdentifier = args[0];
prefabIdentifier = args[1];
break;
}
foreach (UpgradeCategory category in UpgradeCategory.Categories)
{
if (!string.IsNullOrWhiteSpace(categoryIdentifier) && !category.Identifier.Equals(categoryIdentifier, StringComparison.OrdinalIgnoreCase)) { continue; }
foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs)
{
if (!prefab.UpgradeCategories.Contains(category)) { continue; }
if (!string.IsNullOrWhiteSpace(prefabIdentifier) && !prefab.Identifier.Equals(prefabIdentifier, StringComparison.OrdinalIgnoreCase)) { continue; }
int targetLevel = prefab.MaxLevel - upgradeManager.GetRealUpgradeLevel(prefab, category);
for (int i = 0; i < targetLevel; i++)
{
upgradeManager.PurchaseUpgrade(prefab, category, force: true);
}
NewMessage($"Upgraded {category.Identifier}.{prefab.Identifier} by {targetLevel} levels.", Color.DarkGreen);
}
}
NewMessage($"Start a new round to apply the upgrades.", Color.Lime);
}, () =>
{
return new[]
{
UpgradeCategory.Categories.Select(c => c.Identifier).Distinct().ToArray(),
UpgradePrefab.Prefabs.Select(c => c.Identifier).Distinct().ToArray()
};
}, true));
commands.Add(new Command("power", "power: Immediately powers up the submarine's nuclear reactor.", (string[] args) =>
{
@@ -1173,11 +1274,14 @@ namespace Barotrauma
c.SetAllDamage(200.0f, 0.0f, 0.0f);
}
}
foreach (Hull hull in Hull.hullList)
{
hull.BallastFlora?.Kill();
}
foreach (Submarine sub in Submarine.Loaded)
{
sub.WreckAI?.Kill();
}
}, null, isCheat: true));
commands.Add(new Command("setclientcharacter", "setclientcharacter [client name] [character name]: Gives the client control of the specified character.", null,
@@ -1692,6 +1796,8 @@ namespace Barotrauma
return null;
}
// Use same sorting as DebugConsole.ListCharacterNames() above
matchingCharacters = matchingCharacters.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).ToList();
if (characterIndex == -1)
{
if (matchingCharacters.Count > 1)
@@ -110,13 +110,14 @@ namespace Barotrauma
public Decal CreateDecal(string decalName, float scale, Vector2 worldPosition, Hull hull, int? spriteIndex = null)
{
if (!Prefabs.ContainsKey(decalName.ToLowerInvariant()))
string lowerCaseDecalName = decalName.ToLowerInvariant();
if (!Prefabs.ContainsKey(lowerCaseDecalName))
{
DebugConsole.ThrowError("Decal prefab " + decalName + " not found!");
return null;
}
DecalPrefab prefab = Prefabs[decalName];
DecalPrefab prefab = Prefabs[lowerCaseDecalName];
return new Decal(prefab, scale, worldPosition, hull, spriteIndex);
}
@@ -109,7 +109,7 @@ namespace Barotrauma
state = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) return;
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) return;
Finished();
state = 2;
@@ -27,25 +27,24 @@ namespace Barotrauma
if (string.IsNullOrWhiteSpace(Identifier) || string.IsNullOrWhiteSpace(TargetTag)) { return false; }
List<Character> targets = ParentEvent.GetTargets(TargetTag).OfType<Character>().ToList();
if (!(targets.FirstOrDefault() is { } target)) { return false; }
if (TargetLimb == LimbType.None)
foreach (var target in targets)
{
Affliction? affliction = target.CharacterHealth?.GetAffliction(Identifier, AllowLimbAfflictions);
return affliction != null;
if (target.CharacterHealth == null) { continue; }
if (TargetLimb == LimbType.None)
{
if (target.CharacterHealth.GetAffliction(Identifier, AllowLimbAfflictions) != null) { return true; }
}
IEnumerable<Affliction> afflictions = target.CharacterHealth.GetAllAfflictions().Where(affliction =>
{
LimbType? limbType = target.CharacterHealth.GetAfflictionLimb(affliction)?.type;
if (limbType == null) { return false; }
return limbType == TargetLimb || true;
});
if (afflictions.Any(a => a.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase))) { return true; }
}
if (target.CharacterHealth == null) { return false; }
IEnumerable<Affliction> afflictions = target.CharacterHealth.GetAllAfflictions().Where(affliction =>
{
LimbType? limbType = target.CharacterHealth.GetAfflictionLimb(affliction)?.type;
if (limbType == null) { return false; }
return limbType == TargetLimb || true;
});
return afflictions.Any(a => a.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase));
return false;
}
public override string ToDebugString()
@@ -61,7 +61,6 @@ namespace Barotrauma
private Character speaker;
private OrderInfo? prevSpeakerOrder;
private AIObjective prevIdleObjective, prevGotoObjective;
public List<SubactionGroup> Options { get; private set; }
@@ -180,6 +179,7 @@ namespace Barotrauma
{
if (speaker == null) { return; }
speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
speaker.ActiveConversation = this;
speaker.SetCustomInteract(null, null);
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
@@ -187,16 +187,10 @@ namespace Barotrauma
var humanAI = speaker.AIController as HumanAIController;
if (humanAI != null && !speaker.IsDead && !speaker.Removed)
{
if (prevSpeakerOrder != null)
{
humanAI.SetOrder(prevSpeakerOrder.Value.Order, prevSpeakerOrder.Value.OrderOption, orderGiver: null, speak: false);
}
else
{
humanAI.SetOrder(null, string.Empty, orderGiver: null, speak: false);
}
humanAI.ClearForcedOrder();
if (prevIdleObjective != null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); }
if (prevGotoObjective != null) { humanAI.ObjectiveManager.AddObjective(prevGotoObjective); }
humanAI.ObjectiveManager.SortObjectives();
}
}
@@ -221,24 +215,24 @@ namespace Barotrauma
#if CLIENT
Character.DisableControls = true;
#endif
if (ShouldInterrupt())
if (ShouldInterrupt())
{
ResetSpeaker();
interrupt = true;
interrupt = true;
}
return;
return;
}
if (!string.IsNullOrEmpty(SpeakerTag))
{
if (speaker != null && !speaker.Removed && speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk) { return; }
if (speaker != null && !speaker.Removed && speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent) { return; }
speaker = ParentEvent.GetTargets(SpeakerTag).FirstOrDefault(e => e is Character) as Character;
if (speaker == null || speaker.Removed)
{
return;
{
return;
}
//some conversation already assigned to the speaker, wait for it to be removed
if (speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk)
if (speaker.CampaignInteractionType == CampaignMode.InteractionType.Talk && speaker.ActiveConversation?.ParentEvent != this.ParentEvent)
{
return;
}
@@ -249,6 +243,7 @@ namespace Barotrauma
else
{
speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk;
speaker.ActiveConversation = this;
#if CLIENT
speaker.SetCustomInteract(
TryStartConversation,
@@ -324,16 +319,11 @@ namespace Barotrauma
if (speaker?.AIController is HumanAIController humanAI)
{
prevSpeakerOrder = null;
if (humanAI.CurrentOrder != null)
{
prevSpeakerOrder = new OrderInfo(humanAI.CurrentOrder, humanAI.CurrentOrderOption);
}
prevIdleObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveIdle>();
prevGotoObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveGoTo>();
humanAI.SetOrder(
Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)),
option: string.Empty, orderGiver: null, speak: false);
humanAI.SetForcedOrder(
Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)),
option: string.Empty, orderGiver: null);
if (targets.Any())
{
Entity closestTarget = null;
@@ -18,14 +18,13 @@ namespace Barotrauma
public MissionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
{
//TODO: use event identifier in the error messages
if (string.IsNullOrEmpty(MissionIdentifier) && string.IsNullOrEmpty(MissionTag))
{
DebugConsole.ThrowError($"Error in event \"{"event identifier goes here"}\": neither MissionIdentifier or MissionTag has been configured.");
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": neither MissionIdentifier or MissionTag has been configured.");
}
if (!string.IsNullOrEmpty(MissionIdentifier) && !string.IsNullOrEmpty(MissionTag))
{
DebugConsole.ThrowError($"Error in event \"{"event identifier goes here"}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored.");
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": both MissionIdentifier or MissionTag have been configured. The tag will be ignored.");
}
}
@@ -117,30 +117,10 @@ namespace Barotrauma
foreach (Item item in newCharacter.Inventory.AllItems)
{
item.SpawnedInOutpost = true;
item.AllowStealing = false;
}
}
newCharacter.CharacterHealth.MaxVitality *= humanPrefab.HealthMultiplier;
var humanAI = newCharacter.AIController as HumanAIController;
if (humanAI != null)
{
var idleObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveIdle>();
if (idleObjective != null)
{
idleObjective.Behavior = humanPrefab.Behavior;
foreach (string moduleType in humanPrefab.PreferredOutpostModuleTypes)
{
idleObjective.PreferredOutpostModuleTypes.Add(moduleType);
}
}
}
if (humanPrefab.CampaignInteractionType != CampaignMode.InteractionType.None)
{
(GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(newCharacter, humanPrefab.CampaignInteractionType);
if (spawnPos != null && humanAI != null)
{
humanAI.ObjectiveManager.SetOrder(new AIObjectiveGoTo(spawnPos, newCharacter, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200));
}
}
humanPrefab.InitializeCharacter(newCharacter, spawnPos);
if (!string.IsNullOrEmpty(TargetTag) && newCharacter != null)
{
ParentEvent.AddTarget(TargetTag, newCharacter);
@@ -261,7 +241,7 @@ namespace Barotrauma
return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable());
}
public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<string> moduleFlags = null, IEnumerable<string> spawnpointTags = null)
public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<string> moduleFlags = null, IEnumerable<string> spawnpointTags = null, bool asFarAsPossibleFromAirlock = false)
{
List<WayPoint> potentialSpawnPoints = spawnLocation switch
{
@@ -275,6 +255,7 @@ namespace Barotrauma
potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && !wp.isObstructed);
var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags?.Contains("airlock") ?? false).ToList();
if (moduleFlags != null && moduleFlags.Any())
{
List<WayPoint> spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags?.Any(moduleFlags.Contains) ?? false).ToList();
@@ -303,7 +284,7 @@ namespace Barotrauma
IEnumerable<WayPoint> validSpawnPoints;
if (spawnPointType.HasValue)
{
validSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.SpawnType == spawnPointType.Value);
validSpawnPoints = potentialSpawnPoints.FindAll(wp => spawnPointType.Value.HasFlag(wp.SpawnType));
}
else
{
@@ -312,7 +293,6 @@ namespace Barotrauma
}
//don't spawn in an airlock module if there are other options
var airlockSpawnPoints = validSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags?.Contains("airlock") ?? false);
if (airlockSpawnPoints.Count() < validSpawnPoints.Count())
{
validSpawnPoints = validSpawnPoints.Except(airlockSpawnPoints);
@@ -324,6 +304,12 @@ namespace Barotrauma
return potentialSpawnPoints.GetRandom();
}
//avoid using waypoints if there's any actual spawnpoints available
if (validSpawnPoints.Any(wp => wp.SpawnType != SpawnType.Path))
{
validSpawnPoints = validSpawnPoints.Where(wp => wp.SpawnType != SpawnType.Path);
}
//if not trying to spawn at a tagged spawnpoint, favor spawnpoints without tags
if (spawnpointTags == null || !spawnpointTags.Any())
{
@@ -334,7 +320,25 @@ namespace Barotrauma
}
}
return validSpawnPoints.GetRandom();
if (asFarAsPossibleFromAirlock && airlockSpawnPoints.Any())
{
WayPoint furthestPoint = validSpawnPoints.First();
float furthestDist = 0.0f;
foreach (WayPoint waypoint in validSpawnPoints)
{
float dist = Vector2.DistanceSquared(waypoint.WorldPosition, airlockSpawnPoints.First().WorldPosition);
if (dist > furthestDist)
{
furthestDist = dist;
furthestPoint = waypoint;
}
}
return furthestPoint;
}
else
{
return validSpawnPoints.GetRandom();
}
}
public override string ToDebugString()
@@ -6,12 +6,17 @@ namespace Barotrauma
{
class TagAction : EventAction
{
public enum SubType { Any= 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8 }
[Serialize("", true)]
public string Criteria { get; set; }
[Serialize("", true)]
public string Tag { get; set; }
[Serialize(SubType.Any, true)]
public SubType SubmarineType { get; set; }
[Serialize(true, true)]
public bool IgnoreIncapacitatedCharacters { get; set; }
@@ -40,15 +45,15 @@ namespace Barotrauma
}
}
private void TagBots()
private void TagBots(bool playerCrewOnly)
{
if (IgnoreIncapacitatedCharacters)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot && !c.IsIncapacitated);
ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot && !c.IsIncapacitated && (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1));
}
else
{
ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot);
ParentEvent.AddTargetPredicate(Tag, e => e is Character c && c.IsBot && (!playerCrewOnly || c.TeamID == CharacterTeamType.Team1));
}
}
@@ -57,23 +62,44 @@ namespace Barotrauma
#if CLIENT
GameMain.GameSession.CrewManager.GetCharacters().ForEach(c => ParentEvent.AddTarget(Tag, c));
#else
TagPlayers(); TagBots(); //TODO: this seems like it would tag more than it should, fix
TagPlayers();
TagBots(playerCrewOnly: true);
#endif
}
private void TagStructuresByIdentifier(string identifier)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && s.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase));
ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase));
}
private void TagItemsByIdentifier(string identifier)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Item it && it.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase));
ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase));
}
private void TagItemsByTag(string tag)
{
ParentEvent.AddTargetPredicate(Tag, e => e is Item it && it.HasTag(tag));
ParentEvent.AddTargetPredicate(Tag, e => e is Item it && SubmarineTypeMatches(it.Submarine) && it.HasTag(tag));
}
private bool SubmarineTypeMatches(Submarine sub)
{
if (SubmarineType == SubType.Any) { return true; }
if (sub == null) { return false; }
switch (sub.Info.Type)
{
case Barotrauma.SubmarineType.Player:
return SubmarineType.HasFlag(SubType.Player);
case Barotrauma.SubmarineType.Outpost:
case Barotrauma.SubmarineType.OutpostModule:
return SubmarineType.HasFlag(SubType.Outpost);
case Barotrauma.SubmarineType.Wreck:
return SubmarineType.HasFlag(SubType.Wreck);
case Barotrauma.SubmarineType.BeaconStation:
return SubmarineType.HasFlag(SubType.BeaconStation);
default:
return false;
}
}
public override void Update(float deltaTime)
@@ -91,7 +117,7 @@ namespace Barotrauma
TagPlayers();
break;
case "bot":
TagBots();
TagBots(playerCrewOnly: false);
break;
case "crew":
TagCrew();
@@ -113,7 +139,7 @@ namespace Barotrauma
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TagAction)} -> (Criteria: {Criteria.ColorizeObject()}, Tag: {Tag.ColorizeObject()})";
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TagAction)} -> (Criteria: {Criteria.ColorizeObject()}, Tag: {Tag.ColorizeObject()}, Sub: {SubmarineType.ColorizeObject()})";
}
}
}
@@ -0,0 +1,66 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class UnlockPathAction : EventAction
{
public UnlockPathAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
private bool isFinished = false;
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
if (GameMain.GameSession?.Map?.CurrentLocation?.Connections != null)
{
foreach (LocationConnection connection in GameMain.GameSession?.Map?.CurrentLocation?.Connections)
{
if (!connection.Locked) { continue; }
connection.Locked = false;
#if SERVER
NotifyUnlock(connection);
#else
new GUIMessageBox(string.Empty, TextManager.Get("pathunlockedgeneric"),
new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "UnlockPathIcon", relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128));
#endif
}
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(UnlockPathAction)}";
}
#if SERVER
private void NotifyUnlock(LocationConnection connection)
{
foreach (Client client in GameMain.Server.ConnectedClients)
{
IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.Write((byte)ServerPacketHeader.EVENTACTION);
outmsg.Write((byte)EventManager.NetworkEventType.UNLOCKPATH);
outmsg.Write((UInt16)GameMain.GameSession.Map.Connections.IndexOf(connection));
GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
}
}
#endif
}
}
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using NLog;
namespace Barotrauma
{
@@ -13,7 +14,8 @@ namespace Barotrauma
{
CONVERSATION,
STATUSEFFECT,
MISSION
MISSION,
UNLOCKPATH
}
const float IntensityUpdateInterval = 5.0f;
@@ -93,6 +95,8 @@ namespace Barotrauma
public void StartRound(Level level)
{
this.level = level;
if (isClient) { return; }
pendingEventSets.Clear();
@@ -107,24 +111,52 @@ namespace Barotrauma
totalPathLength = steeringPath.TotalLength;
}
this.level = level;
SelectSettings();
int seed = 0;
if (level != null)
{
seed = ToolBox.StringToInt(level.Seed);
foreach (var previousEvent in level.LevelData.EventHistory)
{
seed ^= ToolBox.StringToInt(previousEvent.Identifier);
}
}
MTRandom rand = new MTRandom(seed);
var initialEventSet = SelectRandomEvents(EventSet.List);
if (initialEventSet != null)
{
pendingEventSets.Add(initialEventSet);
int seed = ToolBox.StringToInt(level.Seed);
foreach (var previousEvent in level.LevelData.EventHistory)
{
seed ^= ToolBox.StringToInt(previousEvent.Identifier);
}
MTRandom rand = new MTRandom(seed);
CreateEvents(initialEventSet, rand);
}
if (level?.LevelData?.Type == LevelData.LevelType.Outpost)
{
//if the outpost is connected to a locked connection, create an event to unlock it
if (level.StartLocation?.Connections.Any(c => c.Locked && level.StartLocation.MapPosition.X < c.OtherLocation(level.StartLocation).MapPosition.X) ?? false)
{
var unlockPathPrefabs = EventSet.PrefabList.FindAll(e => e.UnlockPathEvent);
var unlockPathPrefabsForBiome = unlockPathPrefabs.FindAll(e =>
string.IsNullOrEmpty(e.BiomeIdentifier) ||
e.BiomeIdentifier.Equals(level.LevelData.Biome.Identifier, StringComparison.OrdinalIgnoreCase));
var unlockPathEventPrefab = unlockPathPrefabsForBiome.Any() ?
ToolBox.SelectWeightedRandom(unlockPathPrefabsForBiome, unlockPathPrefabsForBiome.Select(b => b.Commonness).ToList(), rand) :
ToolBox.SelectWeightedRandom(unlockPathPrefabs, unlockPathPrefabs.Select(b => b.Commonness).ToList(), rand);
if (unlockPathEventPrefab != null)
{
var newEvent = unlockPathEventPrefab.CreateInstance();
newEvent.Init(true);
ActiveEvents.Add(newEvent);
}
else
{
//if no event that unlocks the path can be found, unlock it automatically
level.StartLocation.Connections.ForEach(c => c.Locked = false);
}
}
level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e)));
if (level.LevelData.EventHistory.Count > MaxEventHistory)
{
@@ -134,11 +166,14 @@ namespace Barotrauma
void AddChildEvents(EventSet eventSet)
{
if (eventSet == null) { return; }
foreach (EventPrefab ep in eventSet.EventPrefabs.Select(e => e.First))
if (eventSet.OncePerOutpost)
{
if (!level.LevelData.NonRepeatableEvents.Contains(ep))
foreach (EventPrefab ep in eventSet.EventPrefabs.Select(e => e.First))
{
level.LevelData.NonRepeatableEvents.Add(ep);
if (!level.LevelData.NonRepeatableEvents.Contains(ep))
{
level.LevelData.NonRepeatableEvents.Add(ep);
}
}
}
foreach (EventSet childSet in eventSet.ChildSets)
@@ -286,14 +321,23 @@ namespace Barotrauma
}
}
RagdollParams ragdollParams;
if (humanoid)
try
{
ragdollParams = RagdollParams.GetRagdollParams<HumanRagdollParams>(speciesName);
if (humanoid)
{
ragdollParams = RagdollParams.GetRagdollParams<HumanRagdollParams>(characterPrefab.VariantOf ?? speciesName);
}
else
{
ragdollParams = RagdollParams.GetRagdollParams<FishRagdollParams>(characterPrefab.VariantOf ?? speciesName);
}
}
else
catch (Exception e)
{
ragdollParams = RagdollParams.GetRagdollParams<FishRagdollParams>(speciesName);
DebugConsole.ThrowError($"Failed to preload a ragdoll file for the character \"{characterPrefab.Name}\"", e);
continue;
}
if (ragdollParams != null)
{
HashSet<string> texturePaths = new HashSet<string>
@@ -341,6 +385,8 @@ namespace Barotrauma
private void CreateEvents(EventSet eventSet, Random rand)
{
if (level == null) { return; }
if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; }
int applyCount = 1;
List<Func<Level.InterestingPosition, bool>> spawnPosFilter = new List<Func<Level.InterestingPosition, bool>>();
if (eventSet.PerRuin)
@@ -361,22 +407,27 @@ namespace Barotrauma
}
else if (eventSet.PerWreck)
{
var wrecks = Submarine.Loaded.Where(s => s.Info.IsWreck && (s.WreckAI == null || !s.WreckAI.IsAlive));
var wrecks = Submarine.Loaded.Where(s => s.Info.IsWreck && (s.WreckAI == null || !s.WreckAI.IsAlive));
applyCount = wrecks.Count();
foreach (var wreck in wrecks)
{
spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Submarine == wreck; });
}
}
var suitablePrefabs = eventSet.EventPrefabs.FindAll(e =>
string.IsNullOrEmpty(e.First.BiomeIdentifier) ||
e.First.BiomeIdentifier.Equals(Level.Loaded.LevelData?.Biome?.Identifier, StringComparison.OrdinalIgnoreCase));
for (int i = 0; i < applyCount; i++)
{
if (eventSet.ChooseRandom)
{
if (eventSet.EventPrefabs.Count > 0)
if (suitablePrefabs.Count > 0)
{
List<Pair<EventPrefab, float>> unusedEvents = new List<Pair<EventPrefab, float>>(eventSet.EventPrefabs);
List<Pair<EventPrefab, float>> unusedEvents = new List<Pair<EventPrefab, float>>(suitablePrefabs);
for (int j = 0; j < eventSet.EventCount; j++)
{
if (unusedEvents.All(e => CalculateCommonness(e) <= 0.0f)) { break; }
var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => CalculateCommonness(e)).ToList(), rand);
if (eventPrefab != null)
{
@@ -402,7 +453,7 @@ namespace Barotrauma
}
else
{
foreach (Pair<EventPrefab, float> eventPrefab in eventSet.EventPrefabs)
foreach (Pair<EventPrefab, float> eventPrefab in suitablePrefabs)
{
var newEvent = eventPrefab.First.CreateInstance();
if (newEvent == null) { continue; }
@@ -429,11 +480,19 @@ namespace Barotrauma
MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed));
var allowedEventSets =
eventSets.Where(es => level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty && level.LevelData.Type == es.LevelType);
eventSets.Where(es =>
level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty &&
level.LevelData.Type == es.LevelType &&
(string.IsNullOrEmpty(es.BiomeIdentifier) || es.BiomeIdentifier.Equals(level.LevelData.Biome.Identifier, StringComparison.OrdinalIgnoreCase)));
Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation;
LocationType locationType = location?.GetLocationType();
if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.Map?.CurrentLocation?.Type != null)
if (location != null)
{
allowedEventSets = allowedEventSets.Where(set => set.LocationTypeIdentifiers == null || set.LocationTypeIdentifiers.Any(identifier => string.Equals(identifier, campaign.Map.CurrentLocation.Type.Identifier, StringComparison.OrdinalIgnoreCase)));
allowedEventSets = allowedEventSets.Where(set =>
set.LocationTypeIdentifiers == null ||
set.LocationTypeIdentifiers.Any(identifier => string.Equals(identifier, locationType.Identifier, StringComparison.OrdinalIgnoreCase)));
}
float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level));
@@ -454,8 +513,8 @@ namespace Barotrauma
private bool CanStartEventSet(EventSet eventSet)
{
ISpatialEntity refEntity = GetRefEntity();
float distFromStart = Vector2.Distance(refEntity.WorldPosition, level.StartPosition);
float distFromEnd = Vector2.Distance(refEntity.WorldPosition, level.EndPosition);
float distFromStart = (float)Math.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(level.StartExitPosition.ToPoint(), level.StartPosition.ToPoint(), refEntity.WorldPosition.ToPoint()));
float distFromEnd = (float)Math.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(level.EndExitPosition.ToPoint(), level.EndPosition.ToPoint(), refEntity.WorldPosition.ToPoint()));
//don't create new events if within 50 meters of the start/end of the level
if (!eventSet.AllowAtStart)
@@ -7,19 +7,22 @@ namespace Barotrauma
class EventPrefab
{
public readonly XElement ConfigElement;
public readonly Type EventType;
public readonly string MusicType;
public readonly Type EventType;
public readonly float SpawnProbability;
public readonly bool TriggerEventCooldown;
public float Commonness;
public string Identifier;
public string BiomeIdentifier;
public bool UnlockPathEvent;
public string UnlockPathTooltip;
public int UnlockPathReputation;
public string UnlockPathFaction;
public EventPrefab(XElement element)
{
ConfigElement = element;
MusicType = element.GetAttributeString("musictype", "default");
try
{
EventType = Type.GetType("Barotrauma." + ConfigElement.Name, true, true);
@@ -34,9 +37,15 @@ namespace Barotrauma
}
Identifier = ConfigElement.GetAttributeString("identifier", string.Empty);
BiomeIdentifier = ConfigElement.GetAttributeString("biome", string.Empty);
Commonness = element.GetAttributeFloat("commonness", 1.0f);
SpawnProbability = Math.Clamp(element.GetAttributeFloat("spawnprobability", 1.0f), 0, 1);
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false);
UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip");
UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0);
UnlockPathFaction = element.GetAttributeString("unlockpathfaction", "");
}
public Event CreateInstance()
@@ -57,5 +66,10 @@ namespace Barotrauma
return (Event)instance;
}
public override string ToString()
{
return $"EventPrefab ({Identifier})";
}
}
}
@@ -65,6 +65,8 @@ namespace Barotrauma
//0-100
public readonly float MinLevelDifficulty, MaxLevelDifficulty;
public readonly string BiomeIdentifier;
public readonly LevelData.LevelType LevelType;
public readonly string[] LocationTypeIdentifiers;
@@ -84,6 +86,7 @@ namespace Barotrauma
public readonly bool IgnoreCoolDown;
public readonly bool PerRuin, PerCave, PerWreck;
public readonly bool DisableInHuntingGrounds;
public readonly bool OncePerOutpost;
@@ -111,6 +114,7 @@ namespace Barotrauma
EventPrefabs = new List<Pair<EventPrefab, float>>();
ChildSets = new List<EventSet>();
BiomeIdentifier = element.GetAttributeString("biome", string.Empty);
MinLevelDifficulty = element.GetAttributeFloat("minleveldifficulty", 0);
MaxLevelDifficulty = Math.Max(element.GetAttributeFloat("maxleveldifficulty", 100), MinLevelDifficulty);
@@ -139,9 +143,10 @@ namespace Barotrauma
PerRuin = element.GetAttributeBool("perruin", false);
PerCave = element.GetAttributeBool("percave", false);
PerWreck = element.GetAttributeBool("perwreck", false);
DisableInHuntingGrounds = element.GetAttributeBool("disableinhuntinggrounds", false);
IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck));
DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck);
OncePerOutpost = element.GetAttributeBool("perwreck", false);
OncePerOutpost = element.GetAttributeBool("onceperoutpost", false);
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
Commonness[""] = 1.0f;
@@ -0,0 +1,257 @@
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class AbandonedOutpostMission : Mission
{
private readonly XElement characterConfig;
protected readonly List<Character> characters = new List<Character>();
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
protected readonly HashSet<Character> requireKill = new HashSet<Character>();
protected readonly HashSet<Character> requireRescue = new HashSet<Character>();
protected const int HostagesKilledState = 5;
private readonly string hostagesKilledMessage;
private const float EndDelay = 5.0f;
private float endTimer;
public override bool AllowRespawn => false;
public override bool AllowUndocking
{
get
{
if (GameMain.GameSession.GameMode is CampaignMode) { return true; }
return state > 0;
}
}
protected bool wasDocked;
public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations) :
base(prefab, locations)
{
characterConfig = prefab.ConfigElement.Element("Characters");
string msgTag = prefab.ConfigElement.GetAttributeString("hostageskilledmessage", "");
hostagesKilledMessage = TextManager.Get(msgTag, returnNull: true) ?? msgTag;
}
protected override void StartMissionSpecific(Level level)
{
failed = false;
endTimer = 0.0f;
characters.Clear();
characterItems.Clear();
requireKill.Clear();
requireRescue.Clear();
var submarine = Submarine.Loaded.Find(s => s.Info.Type == SubmarineType.Outpost) ?? Submarine.MainSub;
if (!IsClient)
{
InitCharacters(submarine);
}
wasDocked = Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost);
}
private void InitCharacters(Submarine submarine)
{
characters.Clear();
characterItems.Clear();
if (characterConfig == null) { return; }
foreach (XElement element in characterConfig.Elements())
{
if (GameMain.NetworkMember == null && element.GetAttributeBool("multiplayeronly", false)) { continue; }
int defaultCount = element.GetAttributeInt("count", -1);
if (defaultCount < 0)
{
defaultCount = element.GetAttributeInt("amount", 1);
}
int min = Math.Min(element.GetAttributeInt("min", defaultCount), 255);
int max = Math.Min(Math.Max(min, element.GetAttributeInt("max", defaultCount)), 255);
int count = Rand.Range(min, max + 1);
if (element.Attribute("identifier") != null && element.Attribute("from") != null)
{
string characterIdentifier = element.GetAttributeString("identifier", "");
string characterFrom = element.GetAttributeString("from", "");
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
if (humanPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + characterIdentifier + "\" not found");
continue;
}
for (int i = 0; i < count; i++)
{
LoadHuman(humanPrefab, element, submarine);
}
}
else
{
string speciesName = element.GetAttributeString("character", element.GetAttributeString("identifier", ""));
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (characterPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + speciesName + "\" not found");
continue;
}
for (int i = 0; i < count; i++)
{
LoadMonster(characterPrefab, element, submarine);
}
}
}
}
private void LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine)
{
string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null);
string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null);
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(
SpawnAction.SpawnLocationType.Outpost, SpawnType.Human,
moduleFlags ?? humanPrefab.GetModuleFlags(),
spawnPointTags ?? humanPrefab.GetSpawnPointTags(),
element.GetAttributeBool("asfaraspossible", false));
if (spawnPos == null)
{
spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom();
}
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server);
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, spawnPos.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
if (element.GetAttributeBool("requirerescue", false))
{
requireRescue.Add(spawnedCharacter);
spawnedCharacter.TeamID = CharacterTeamType.FriendlyNPC;
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
#endif
}
else
{
spawnedCharacter.TeamID = CharacterTeamType.None;
}
humanPrefab.InitializeCharacter(spawnedCharacter, spawnPos);
humanPrefab.GiveItems(spawnedCharacter, Submarine.MainSub, Rand.RandSync.Server, createNetworkEvents: false);
if (spawnPos is WayPoint wp)
{
spawnedCharacter.GiveIdCardTags(wp);
}
if (element.GetAttributeBool("requirekill", false))
{
requireKill.Add(spawnedCharacter);
}
characters.Add(spawnedCharacter);
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
}
private void LoadMonster(CharacterPrefab monsterPrefab, XElement element, Submarine submarine)
{
string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null);
string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null);
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
if (spawnPos == null)
{
spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom();
}
Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false);
characters.Add(spawnedCharacter);
if (element.GetAttributeBool("requirekill", false))
{
requireKill.Add(spawnedCharacter);
}
if (spawnedCharacter.Inventory != null)
{
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
}
if (submarine != null && spawnedCharacter.AIController is EnemyAIController enemyAi)
{
enemyAi.UnattackableSubmarines.Add(submarine);
enemyAi.UnattackableSubmarines.Add(Submarine.MainSub);
foreach (Submarine sub in Submarine.MainSub.DockedTo)
{
enemyAi.UnattackableSubmarines.Add(sub);
}
}
}
public override void Update(float deltaTime)
{
if (State != HostagesKilledState)
{
if (requireRescue.Any(r => r.Removed || r.IsDead))
{
State = HostagesKilledState;
return;
}
}
else
{
endTimer += deltaTime;
if (endTimer > EndDelay)
{
#if SERVER
if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null)
{
GameMain.Server.EndGame();
}
#endif
}
}
switch (state)
{
case 0:
if (requireKill.All(c => c.Removed || c.IsDead) &&
requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player))
{
State = 1;
}
break;
#if SERVER
case 1:
if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null)
{
if (!Submarine.MainSub.AtStartExit || (wasDocked && !Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost)))
{
GameMain.Server.EndGame();
State = 2;
}
}
break;
#endif
}
}
public override void End()
{
completed = State > 0 && State != HostagesKilledState;
if (completed)
{
if (Prefab.LocationTypeChangeOnCompleted != null)
{
ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
}
GiveReward();
}
else
{
failed = requireRescue.Any(r => r.Removed || r.IsDead);
}
}
}
}
@@ -11,7 +11,6 @@ namespace Barotrauma
private bool swarmSpawned;
private readonly string monsterSpeciesName;
private Point monsterCountRange;
private Level level;
private readonly string sonarLabel;
public BeaconMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations)
@@ -54,11 +53,6 @@ namespace Barotrauma
}
}
public override void Start(Level level)
{
this.level = level;
}
public override void Update(float deltaTime)
{
if (IsClient) { return; }
@@ -113,8 +107,15 @@ namespace Barotrauma
completed = level.CheckBeaconActive();
if (completed)
{
ChangeLocationType("None", "Explored");
if (Prefab.LocationTypeChangeOnCompleted != null)
{
ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
}
GiveReward();
if (level?.LevelData != null)
{
level.LevelData.IsBeaconActive = true;
}
}
}
@@ -96,7 +96,8 @@ namespace Barotrauma
var item = new Item(itemPrefab, position, cargoRoom.Submarine)
{
SpawnedInOutpost = true
SpawnedInOutpost = true,
AllowStealing = false
};
item.FindHull();
items.Add(item);
@@ -118,7 +119,7 @@ namespace Barotrauma
}
}
public override void Start(Level level)
protected override void StartMissionSpecific(Level level)
{
items.Clear();
parentInventoryIDs.Clear();
@@ -131,13 +132,17 @@ namespace Barotrauma
public override void End()
{
if (Submarine.MainSub != null && Submarine.MainSub.AtEndPosition)
if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
{
int deliveredItemCount = items.Count(i => i.CurrentHull != null && !i.Removed && i.Condition > 0.0f);
if (deliveredItemCount >= requiredDeliveryAmount)
{
GiveReward();
completed = true;
if (Prefab.LocationTypeChangeOnCompleted != null)
{
ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
}
}
}
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System.Collections.Generic;
namespace Barotrauma
@@ -89,8 +90,8 @@ namespace Barotrauma
Winner != CharacterTeamType.None &&
Winner == character.TeamID;
}
public override void Start(Level level)
protected override void StartMissionSpecific(Level level)
{
if (GameMain.NetworkMember == null)
{
@@ -99,23 +100,23 @@ namespace Barotrauma
}
subs = new Submarine[] { Submarine.MainSubs[0], Submarine.MainSubs[1] };
subs[0].TeamID = CharacterTeamType.Team1; subs[1].TeamID = CharacterTeamType.Team2;
subs[0].NeutralizeBallast(); subs[1].NeutralizeBallast();
subs[0].NeutralizeBallast();
subs[0].TeamID = CharacterTeamType.Team1;
subs[0].DockedTo.ForEach(s => s.TeamID = CharacterTeamType.Team1);
subs[1].NeutralizeBallast();
subs[1].TeamID = CharacterTeamType.Team2;
subs[1].DockedTo.ForEach(s => s.TeamID = CharacterTeamType.Team2);
subs[1].SetPosition(subs[1].FindSpawnPos(Level.Loaded.EndPosition));
subs[1].FlipX();
crews = new List<Character>[] { new List<Character>(), new List<Character>() };
foreach (Submarine submarine in Submarine.Loaded)
{
//hide all subs from sonar to make sneak attacks possible
submarine.ShowSonarMarker = false;
}
}
public override void End()
{
if (GameMain.NetworkMember == null) return;
if (GameMain.NetworkMember == null) { return; }
if (Winner != CharacterTeamType.None)
{
@@ -44,7 +44,7 @@ namespace Barotrauma
}
}
public override void Start(Level level)
protected override void StartMissionSpecific(Level level)
{
if (SpawnedResources.Any())
{
@@ -125,7 +125,7 @@ namespace Barotrauma
State = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) { return; }
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; }
State = 2;
break;
}
@@ -135,6 +135,10 @@ namespace Barotrauma
{
if (EnoughHaveBeenCollected())
{
if (Prefab.LocationTypeChangeOnCompleted != null)
{
ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
}
GiveReward();
completed = true;
}
@@ -1,9 +1,8 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
namespace Barotrauma
{
@@ -11,8 +10,11 @@ namespace Barotrauma
{
public readonly MissionPrefab Prefab;
protected bool completed, failed;
protected Level level;
protected int state;
public int State
public virtual int State
{
get { return state; }
protected set
@@ -21,7 +23,7 @@ namespace Barotrauma
{
state = value;
#if SERVER
GameMain.Server?.UpdateMissionState(state);
GameMain.Server?.UpdateMissionState(this, state);
#endif
ShowMessage(State);
}
@@ -38,25 +40,30 @@ namespace Barotrauma
get { return Prefab.Name; }
}
private string successMessage;
private readonly string successMessage;
public virtual string SuccessMessage
{
get { return successMessage; }
private set { successMessage = value; }
//private set { successMessage = value; }
}
private string failureMessage;
private readonly string failureMessage;
public virtual string FailureMessage
{
get { return failureMessage; }
private set { failureMessage = value; }
//private set { failureMessage = value; }
}
protected string description;
public virtual string Description
{
get { return description; }
private set { description = value; }
//private set { description = value; }
}
public virtual bool AllowUndocking
{
get { return true; }
}
public int Reward
@@ -100,6 +107,11 @@ namespace Barotrauma
}
public readonly Location[] Locations;
public int? Difficulty
{
get { return Prefab.Difficulty; }
}
public Mission(MissionPrefab prefab, Location[] locations)
{
@@ -109,7 +121,7 @@ namespace Barotrauma
description = prefab.Description;
successMessage = prefab.SuccessMessage;
FailureMessage = prefab.FailureMessage;
failureMessage = prefab.FailureMessage;
Headers = new List<string>(prefab.Headers);
Messages = new List<string>(prefab.Messages);
@@ -117,20 +129,22 @@ namespace Barotrauma
for (int n = 0; n < 2; n++)
{
if (description != null) description = description.Replace("[location" + (n + 1) + "]", locations[n].Name);
if (successMessage != null) successMessage = successMessage.Replace("[location" + (n + 1) + "]", locations[n].Name);
if (failureMessage != null) failureMessage = failureMessage.Replace("[location" + (n + 1) + "]", locations[n].Name);
string locationName = $"‖color:gui.orange‖{locations[n].Name}‖end‖";
if (description != null) { description = description.Replace("[location" + (n + 1) + "]", locationName); }
if (successMessage != null) { successMessage = successMessage.Replace("[location" + (n + 1) + "]", locationName); }
if (failureMessage != null) { failureMessage = failureMessage.Replace("[location" + (n + 1) + "]", locationName); }
for (int m = 0; m < Messages.Count; m++)
{
Messages[m] = Messages[m].Replace("[location" + (n + 1) + "]", locations[n].Name);
Messages[m] = Messages[m].Replace("[location" + (n + 1) + "]", locationName);
}
}
if (description != null) description = description.Replace("[reward]", Reward.ToString("N0"));
if (successMessage != null) successMessage = successMessage.Replace("[reward]", Reward.ToString("N0"));
if (failureMessage != null) failureMessage = failureMessage.Replace("[reward]", Reward.ToString("N0"));
string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", Reward)}‖end‖";
if (description != null) { description = description.Replace("[reward]", rewardText); }
if (successMessage != null) { successMessage = successMessage.Replace("[reward]", rewardText); }
if (failureMessage != null) { failureMessage = failureMessage.Replace("[reward]", rewardText); }
for (int m = 0; m < Messages.Count; m++)
{
Messages[m] = Messages[m].Replace("[reward]", Reward.ToString("N0"));
Messages[m] = Messages[m].Replace("[reward]", rewardText);
}
}
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false)
@@ -175,7 +189,23 @@ namespace Barotrauma
return null;
}
public virtual void Start(Level level) { }
public void Start(Level level)
{
#if CLIENT
shownMessages.Clear();
#endif
foreach (string categoryToShow in Prefab.UnhideEntitySubCategories)
{
foreach (MapEntity entityToShow in MapEntity.mapEntityList.Where(me => me.prefab?.HasSubCategory(categoryToShow) ?? false))
{
entityToShow.HiddenInGame = false;
}
}
this.level = level;
StartMissionSpecific(level);
}
protected virtual void StartMissionSpecific(Level level) { }
public virtual void Update(float deltaTime) { }
@@ -192,7 +222,10 @@ namespace Barotrauma
public virtual void End()
{
completed = true;
if (Prefab.LocationTypeChangeOnCompleted != null)
{
ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
}
GiveReward();
}
@@ -224,22 +257,32 @@ namespace Barotrauma
}
}
protected void ChangeLocationType(string from, string to)
protected void ChangeLocationType(LocationTypeChange change)
{
if (change == null) { throw new ArgumentException(); }
if (GameMain.GameSession.GameMode is CampaignMode && !IsClient)
{
int srcIndex = -1;
for (int i = 0; i < Locations.Length; i++)
{
if (Locations[i].Type.Identifier.Equals(from, StringComparison.OrdinalIgnoreCase))
if (Locations[i].Type.Identifier.Equals(change.CurrentType, StringComparison.OrdinalIgnoreCase))
{
srcIndex = i;
break;
}
}
if (srcIndex == -1) { return; }
var upgradeLocation = Locations[srcIndex];
upgradeLocation.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(to, StringComparison.OrdinalIgnoreCase)));
var location = Locations[srcIndex];
if (change.RequiredDurationRange.X > 0)
{
location.PendingLocationTypeChange = (change, Rand.Range(change.RequiredDurationRange.X, change.RequiredDurationRange.Y), Prefab);
}
else
{
location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(change.ChangeToType, StringComparison.OrdinalIgnoreCase)));
location.LocationTypeChangeCooldown = change.CooldownAfterChange;
}
}
}
@@ -18,7 +18,10 @@ namespace Barotrauma
Nest = 0x10,
Mineral = 0x20,
Combat = 0x40,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat
OutpostDestroy = 0x80,
OutpostRescue = 0x100,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | OutpostDestroy | OutpostRescue
}
partial class MissionPrefab
@@ -33,6 +36,8 @@ namespace Barotrauma
{ MissionType.Beacon, typeof(BeaconMission) },
{ MissionType.Nest, typeof(NestMission) },
{ MissionType.Mineral, typeof(MineralMission) },
{ MissionType.OutpostDestroy, typeof(OutpostDestroyMission) },
{ MissionType.OutpostRescue, typeof(AbandonedOutpostMission) },
};
public static readonly Dictionary<MissionType, Type> PvPMissionClasses = new Dictionary<MissionType, Type>()
{
@@ -67,14 +72,34 @@ namespace Barotrauma
public readonly List<Tuple<string, object, SetDataAction.OperationType>> DataRewards = new List<Tuple<string, object, SetDataAction.OperationType>>();
public readonly int Commonness;
public readonly int? Difficulty;
public const int MinDifficulty = 1, MaxDifficulty = 4;
public readonly int Reward;
public readonly List<string> Headers;
public readonly List<string> Messages;
//the mission can only be received when travelling from Pair.First to Pair.Second
public readonly List<Pair<string, string>> AllowedLocationTypes;
public readonly bool AllowRetry;
public readonly bool IsSideObjective;
/// <summary>
/// The mission can only be received when travelling from Pair.First to Pair.Second
/// </summary>
public readonly List<Pair<string, string>> AllowedConnectionTypes;
/// <summary>
/// The mission can only be received in these location types
/// </summary>
public readonly List<string> AllowedLocationTypes = new List<string>();
/// <summary>
/// Show entities belonging to these sub categories when the mission starts
/// </summary>
public readonly List<string> UnhideEntitySubCategories = new List<string>();
public LocationTypeChange LocationTypeChangeOnCompleted;
public readonly XElement ConfigElement;
@@ -130,8 +155,14 @@ namespace Barotrauma
Name = TextManager.Get("MissionName." + TextIdentifier, true) ?? element.GetAttributeString("name", "");
Description = TextManager.Get("MissionDescription." + TextIdentifier, true) ?? element.GetAttributeString("description", "");
Reward = element.GetAttributeInt("reward", 1);
AllowRetry = element.GetAttributeBool("allowretry", false);
IsSideObjective = element.GetAttributeBool("sideobjective", false);
Commonness = element.GetAttributeInt("commonness", 1);
if (element.GetAttribute("difficulty") != null)
{
int difficulty = element.GetAttributeInt("difficulty", MinDifficulty);
Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty);
}
SuccessMessage = TextManager.Get("MissionSuccess." + TextIdentifier, true) ?? element.GetAttributeString("successmessage", "Mission completed successfully");
FailureMessage = TextManager.Get("MissionFailure." + TextIdentifier, true) ?? "";
@@ -144,7 +175,10 @@ namespace Barotrauma
FailureMessage = element.GetAttributeString("failuremessage", "");
}
SonarLabel = TextManager.Get("MissionSonarLabel." + TextIdentifier, true) ?? element.GetAttributeString("sonarlabel", "");
SonarLabel =
TextManager.Get("MissionSonarLabel." + TextIdentifier, true) ??
TextManager.Get("MissionSonarLabel." + element.GetAttributeString("sonarlabel", ""), true) ??
element.GetAttributeString("sonarlabel", "");
SonarIconIdentifier = element.GetAttributeString("sonaricon", "");
MultiplayerOnly = element.GetAttributeBool("multiplayeronly", false);
@@ -152,9 +186,11 @@ namespace Barotrauma
AchievementIdentifier = element.GetAttributeString("achievementidentifier", "");
UnhideEntitySubCategories = element.GetAttributeStringArray("unhideentitysubcategories", new string[0]).ToList();
Headers = new List<string>();
Messages = new List<string>();
AllowedLocationTypes = new List<Pair<string, string>>();
AllowedConnectionTypes = new List<Pair<string, string>>();
for (int i = 0; i < 100; i++)
{
@@ -183,9 +219,20 @@ namespace Barotrauma
messageIndex++;
break;
case "locationtype":
AllowedLocationTypes.Add(new Pair<string, string>(
subElement.GetAttributeString("from", ""),
subElement.GetAttributeString("to", "")));
case "connectiontype":
if (subElement.Attribute("identifier") != null)
{
AllowedLocationTypes.Add(subElement.GetAttributeString("identifier", ""));
}
else
{
AllowedConnectionTypes.Add(new Pair<string, string>(
subElement.GetAttributeString("from", ""),
subElement.GetAttributeString("to", "")));
}
break;
case "locationtypechange":
LocationTypeChangeOnCompleted = new LocationTypeChange(subElement.GetAttributeString("from", ""), subElement, requireChangeMessages: false, defaultProbability: 1.0f);
break;
case "reputation":
case "reputationreward":
@@ -257,19 +304,32 @@ namespace Barotrauma
public bool IsAllowed(Location from, Location to)
{
foreach (Pair<string, string> allowedLocationType in AllowedLocationTypes)
if (from == to)
{
if (allowedLocationType.First.Equals("any", StringComparison.OrdinalIgnoreCase) ||
allowedLocationType.First.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase))
return
AllowedLocationTypes.Any(lt => lt.Equals("any", StringComparison.OrdinalIgnoreCase)) ||
AllowedLocationTypes.Any(lt => lt.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase));
}
foreach (Pair<string, string> allowedConnectionType in AllowedConnectionTypes)
{
if (allowedConnectionType.First.Equals("any", StringComparison.OrdinalIgnoreCase) ||
allowedConnectionType.First.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase))
{
if (allowedLocationType.Second.Equals("any", StringComparison.OrdinalIgnoreCase) ||
allowedLocationType.Second.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase))
if (allowedConnectionType.Second.Equals("any", StringComparison.OrdinalIgnoreCase) ||
allowedConnectionType.Second.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
if (Type == MissionType.Beacon)
{
var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to));
if (connection?.LevelData == null || !connection.LevelData.HasBeaconStation || connection.LevelData.IsBeaconActive) { return false; }
}
return false;
}
@@ -8,7 +8,7 @@ namespace Barotrauma
partial class MonsterMission : Mission
{
//string = filename, point = min,max
private readonly HashSet<Tuple<CharacterPrefab, Point>> monsterPrefabs = new HashSet<Tuple<CharacterPrefab, Point>>();
private readonly HashSet<(CharacterPrefab character, Point amountRange)> monsterPrefabs = new HashSet<(CharacterPrefab character, Point amountRange)>();
private readonly List<Character> monsters = new List<Character>();
private readonly List<Vector2> sonarPositions = new List<Vector2>();
@@ -16,6 +16,7 @@ namespace Barotrauma
private readonly float maxSonarMarkerDistance = 10000.0f;
private readonly Level.PositionType spawnPosType;
public override IEnumerable<Vector2> SonarPositions
{
@@ -42,7 +43,7 @@ namespace Barotrauma
if (characterPrefab != null)
{
int monsterCount = Math.Min(prefab.ConfigElement.GetAttributeInt("monstercount", 1), 255);
monsterPrefabs.Add(new Tuple<CharacterPrefab, Point>(characterPrefab, new Point(monsterCount)));
monsterPrefabs.Add((characterPrefab, new Point(monsterCount)));
}
else
{
@@ -52,6 +53,13 @@ namespace Barotrauma
maxSonarMarkerDistance = prefab.ConfigElement.GetAttributeFloat("maxsonarmarkerdistance", 10000.0f);
var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
if (string.IsNullOrWhiteSpace(spawnPosTypeStr) ||
!Enum.TryParse(spawnPosTypeStr, true, out spawnPosType))
{
spawnPosType = Level.PositionType.MainPath | Level.PositionType.SidePath;
}
foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster"))
{
speciesName = monsterElement.GetAttributeString("character", string.Empty);
@@ -65,7 +73,7 @@ namespace Barotrauma
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (characterPrefab != null)
{
monsterPrefabs.Add(new Tuple<CharacterPrefab, Point>(characterPrefab, new Point(min, max)));
monsterPrefabs.Add((characterPrefab, new Point(min, max)));
}
else
{
@@ -75,14 +83,14 @@ namespace Barotrauma
if (monsterPrefabs.Any())
{
var characterParams = new CharacterParams(monsterPrefabs.First().Item1.FilePath);
var characterParams = new CharacterParams(monsterPrefabs.First().character.FilePath);
description = description.Replace("[monster]",
TextManager.Get("character." + characterParams.SpeciesTranslationOverride, returnNull: true) ??
TextManager.Get("character." + characterParams.SpeciesName));
}
}
public override void Start(Level level)
protected override void StartMissionSpecific(Level level)
{
if (monsters.Count > 0)
{
@@ -106,13 +114,13 @@ namespace Barotrauma
if (!IsClient)
{
Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath | Level.PositionType.SidePath, Level.Loaded.Size.X * 0.3f, out Vector2 spawnPos);
foreach (var monster in monsterPrefabs)
Level.Loaded.TryGetInterestingPosition(true, spawnPosType, Level.Loaded.Size.X * 0.3f, out Vector2 spawnPos);
foreach (var (character, amountRange) in monsterPrefabs)
{
int amount = Rand.Range(monster.Item2.X, monster.Item2.Y + 1);
int amount = Rand.Range(amountRange.X, amountRange.Y + 1);
for (int i = 0; i < amount; i++)
{
monsters.Add(Character.Create(monster.Item1.Identifier, spawnPos, ToolBox.RandomSeed(8), createNetworkEvent: false));
monsters.Add(Character.Create(character.Identifier, spawnPos, ToolBox.RandomSeed(8), createNetworkEvent: false));
}
}
@@ -213,9 +221,17 @@ namespace Barotrauma
tempSonarPositions.Clear();
monsters.Clear();
if (State < 1) { return; }
if (Prefab.LocationTypeChangeOnCompleted != null)
{
ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
}
GiveReward();
completed = true;
if (level?.LevelData != null && Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase) || t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase)))
{
level.LevelData.HasHuntingGrounds = false;
}
}
public bool IsEliminated(Character enemy) =>
@@ -90,7 +90,7 @@ namespace Barotrauma
}
public override void Start(Level level)
protected override void StartMissionSpecific(Level level)
{
if (items.Any())
{
@@ -270,7 +270,7 @@ namespace Barotrauma
break;
case 1:
if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) { return; }
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; }
State = 2;
break;
}
@@ -309,7 +309,10 @@ namespace Barotrauma
completed = true;
if (completed)
{
ChangeLocationType("None", "Explored");
if (Prefab.LocationTypeChangeOnCompleted != null)
{
ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
}
}
}
foreach (Item item in items)
@@ -0,0 +1,165 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class OutpostDestroyMission : AbandonedOutpostMission
{
private readonly string itemTag;
private readonly XElement itemConfig;
private readonly List<Item> items = new List<Item>();
public override IEnumerable<Vector2> SonarPositions
{
get
{
if (State > 0)
{
return Enumerable.Empty<Vector2>();
}
else
{
return Targets.Select(t => t.WorldPosition);
}
}
}
private IEnumerable<Entity> Targets
{
get
{
if (State > 0)
{
return Enumerable.Empty<Entity>();
}
else
{
if (items.Any())
{
return items.Where(it => !it.Removed && it.Condition > 0.0f).Cast<Entity>().Concat(requireKill.Where(c => !c.Removed && !c.IsDead)).Concat(requireRescue);
}
else
{
return requireKill.Concat(requireRescue);
}
}
}
}
public OutpostDestroyMission(MissionPrefab prefab, Location[] locations) :
base(prefab, locations)
{
itemConfig = prefab.ConfigElement.Element("Items");
itemTag = prefab.ConfigElement.GetAttributeString("targetitem", "");
}
protected override void StartMissionSpecific(Level level)
{
items.Clear();
#if SERVER
spawnedItems.Clear();
#endif
if (!string.IsNullOrEmpty(itemTag))
{
var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag));
if (!itemsToDestroy.Any())
{
DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\".");
}
else
{
items.AddRange(itemsToDestroy);
}
}
if (itemConfig != null && !IsClient)
{
foreach (XElement element in itemConfig.Elements())
{
string itemIdentifier = element.GetAttributeString("identifier", "");
if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab))
{
DebugConsole.ThrowError("Couldn't spawn item for outpost destroy mission: item prefab \"" + itemIdentifier + "\" not found");
continue;
}
string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null);
string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null);
ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos(
SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy,
moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
if (spawnPoint == null)
{
var submarine = Submarine.Loaded.Find(s => s.Info.Type == SubmarineType.Outpost) ?? Submarine.MainSub;
spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom();
}
Vector2 spawnPos = spawnPoint.WorldPosition;
if (spawnPoint is WayPoint wp && wp.CurrentHull != null)
{
spawnPos = new Vector2(
MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 200), wp.CurrentHull.WorldRect.X, wp.CurrentHull.WorldRect.Right),
wp.CurrentHull.WorldRect.Y - wp.CurrentHull.Rect.Height + 16.0f);
}
var item = new Item(itemPrefab, spawnPos, null);
items.Add(item);
#if SERVER
spawnedItems.Add(item);
#endif
}
}
base.StartMissionSpecific(level);
}
public override void Update(float deltaTime)
{
if (requireRescue.Any(r => r.Removed || r.IsDead))
{
#if SERVER
if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null)
{
GameMain.Server.EndGame();
}
#endif
return;
}
switch (state)
{
case 0:
if (items.Any())
{
if (items.All(it => it.Removed || it.Condition <= 0.0f) &&
requireKill.All(c => c.Removed || c.IsDead) &&
requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player))
{
State = 1;
}
}
else
{
if (requireKill.All(c => c.Removed || c.IsDead) &&
requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player))
{
State = 1;
}
}
break;
#if SERVER
case 1:
if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null)
{
if (!Submarine.MainSub.AtStartExit || (wasDocked && !Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost)))
{
GameMain.Server.EndGame();
State = 2;
}
}
break;
#endif
}
}
}
}
@@ -102,7 +102,7 @@ namespace Barotrauma
}
}
public override void Start(Level level)
protected override void StartMissionSpecific(Level level)
{
#if SERVER
originalInventoryID = Entity.NullEntityID;
@@ -239,7 +239,7 @@ namespace Barotrauma
State = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) { return; }
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; }
State = 2;
break;
}
@@ -248,11 +248,16 @@ namespace Barotrauma
public override void End()
{
var root = item.GetRootContainer() ?? item;
if (root.CurrentHull?.Submarine == null || (!root.CurrentHull.Submarine.AtEndPosition && !root.CurrentHull.Submarine.AtStartPosition) || item.Removed)
if (root.CurrentHull?.Submarine == null || (!root.CurrentHull.Submarine.AtEndExit && !root.CurrentHull.Submarine.AtStartExit) || item.Removed)
{
return;
}
if (Prefab.LocationTypeChangeOnCompleted != null)
{
ChangeLocationType(Prefab.LocationTypeChangeOnCompleted);
}
item?.Remove();
item = null;
GiveReward();
@@ -16,16 +16,16 @@ namespace Barotrauma
private readonly float scatter;
private readonly float offset;
private readonly bool spawnDeep;
private Vector2? spawnPos;
private readonly bool disallowed;
private bool disallowed;
private readonly Level.PositionType spawnPosType;
private bool spawnPending;
private int maxAmountPerLevel = int.MaxValue;
public List<Character> Monsters => monsters;
public Vector2? SpawnPos => spawnPos;
public bool SpawnPending => spawnPending;
@@ -72,15 +72,21 @@ namespace Barotrauma
minAmount = prefab.ConfigElement.GetAttributeInt("minamount", defaultAmount);
maxAmount = Math.Max(prefab.ConfigElement.GetAttributeInt("maxamount", 1), minAmount);
var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
maxAmountPerLevel = prefab.ConfigElement.GetAttributeInt("maxamountperlevel", int.MaxValue);
var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
if (string.IsNullOrWhiteSpace(spawnPosTypeStr) ||
!Enum.TryParse(spawnPosTypeStr, true, out spawnPosType))
{
spawnPosType = Level.PositionType.MainPath;
}
spawnDeep = prefab.ConfigElement.GetAttributeBool("spawndeep", false);
//backwards compatibility
if (prefab.ConfigElement.GetAttributeBool("spawndeep", false))
{
spawnPosType = Level.PositionType.Abyss;
}
offset = prefab.ConfigElement.GetAttributeFloat("offset", 0);
scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 1000), 0, 3000);
@@ -163,15 +169,6 @@ namespace Barotrauma
{
removals.Add(position);
}
if (spawnDeep)
{
for (int i = 0; i < availablePositions.Count; i++)
{
var pos = availablePositions[i].Position;
pos = new Point(pos.X, pos.Y - Level.Loaded.Size.Y);
availablePositions[i] = new Level.InterestingPosition(pos, availablePositions[i].PositionType);
}
}
if (position.Position.Y < Level.Loaded.GetBottomPosition(position.Position.X).Y)
{
removals.Add(position);
@@ -196,7 +193,7 @@ namespace Barotrauma
var availablePositions = GetAvailableSpawnPositions();
var chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false);
bool isSubOrWreck = spawnPosType == Level.PositionType.Ruin || spawnPosType == Level.PositionType.Wreck;
if (affectSubImmediately && !isSubOrWreck)
if (affectSubImmediately && !isSubOrWreck && spawnPosType != Level.PositionType.Abyss)
{
if (availablePositions.None())
{
@@ -218,7 +215,7 @@ namespace Barotrauma
float dist = Vector2.DistanceSquared(pos, refSub.WorldPosition);
foreach (Submarine sub in Submarine.Loaded)
{
if (sub.Info.Type != SubmarineType.Player) { continue; }
if (sub.Info.Type != SubmarineType.Player && sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; }
float minDistToSub = GetMinDistanceToSub(sub);
if (dist < minDistToSub * minDistToSub) { continue; }
@@ -276,6 +273,7 @@ namespace Barotrauma
{
for (int i = 1; i < Submarine.MainSubs.Length; i++)
{
if (Submarine.MainSubs[i] == null) { continue; }
availablePositions.RemoveAll(p => Vector2.DistanceSquared(Submarine.MainSubs[i].WorldPosition, p.Position.ToVector2()) < minDistance * minDistance);
}
}
@@ -301,6 +299,13 @@ namespace Barotrauma
System.Diagnostics.Debug.Assert(spawnPoint.ParentRuin == chosenPosition.Ruin);
spawnPos = spawnPoint.WorldPosition;
}
else
{
//no suitable position found, disable the event
spawnPos = null;
Finished();
return;
}
}
else if ((chosenPosition.PositionType == Level.PositionType.MainPath || chosenPosition.PositionType == Level.PositionType.SidePath)
&& offset > 0)
@@ -351,6 +356,15 @@ namespace Barotrauma
if (spawnPos == null)
{
if (maxAmountPerLevel < int.MaxValue)
{
if (Character.CharacterList.Count(c => c.SpeciesName == speciesName) >= maxAmountPerLevel)
{
disallowed = true;
return;
}
}
FindSpawnPosition(affectSubImmediately: true);
//the event gets marked as finished if a spawn point is not found
if (isFinished) { return; }
@@ -361,7 +375,7 @@ namespace Barotrauma
if (spawnPending)
{
//wait until there are no submarines at the spawnpos
if (spawnPosType == Level.PositionType.MainPath)
if (spawnPosType == Level.PositionType.MainPath || spawnPosType == Level.PositionType.SidePath || spawnPosType == Level.PositionType.Abyss)
{
foreach (Submarine submarine in Submarine.Loaded)
{
@@ -400,6 +414,19 @@ namespace Barotrauma
if (!someoneNearby) { return; }
}
if (spawnPosType == Level.PositionType.Abyss || spawnPosType == Level.PositionType.AbyssCave)
{
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (submarine.WorldPosition.Y > 0)
{
return;
}
}
}
spawnPending = false;
//+1 because Range returns an integer less than the max value
@@ -431,7 +458,16 @@ namespace Barotrauma
}
}
monsters.Add(Character.Create(speciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true));
Character createdCharacter = Character.Create(speciesName, pos, seed, characterInfo: null, isRemotePlayer: false, hasAi: true, createNetworkEvent: true);
if (GameMain.GameSession.IsCurrentLocationRadiated())
{
AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness;
Affliction affliction = new Affliction(radiationPrefab, radiationPrefab.MaxStrength);
createdCharacter?.CharacterHealth.ApplyAffliction(null, affliction);
// TODO test multiplayer
createdCharacter?.Kill(CauseOfDeathType.Affliction, affliction, log: false);
}
monsters.Add(createdCharacter);
if (monsters.Count == amount)
{
@@ -440,7 +476,7 @@ namespace Barotrauma
//otherwise it'll make the spawned characters act as a swarm
SwarmBehavior.CreateSwarm(monsters.Cast<AICharacter>());
}
}, Rand.Range(0f, amount / 2));
}, Rand.Range(0f, amount / 2f));
}
}
@@ -13,7 +13,8 @@ namespace Barotrauma
private int prevEntityCount;
private int prevPlayerCount, prevBotCount;
private string[] requiredDestinationTypes;
private readonly string[] requiredDestinationTypes;
public readonly bool RequireBeaconStation;
public int CurrentActionIndex { get; private set; }
public List<EventAction> Actions { get; } = new List<EventAction>();
@@ -21,7 +22,7 @@ namespace Barotrauma
public override string ToString()
{
return "ScriptedEvent (" + prefab.EventType.ToString() +")";
return $"ScriptedEvent ({prefab.Identifier})";
}
public ScriptedEvent(EventPrefab prefab) : base(prefab)
@@ -43,6 +44,7 @@ namespace Barotrauma
}
requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null);
RequireBeaconStation = prefab.ConfigElement.GetAttributeBool("requirebeaconstation", false);
}
public void AddTarget(string tag, Entity target)
@@ -208,9 +210,16 @@ namespace Barotrauma
{
if (requiredDestinationTypes == null) { return true; }
var currLocation = GameMain.GameSession?.Campaign?.Map.CurrentLocation;
if (currLocation == null) { return true; }
var locations = currLocation?.Connections?.Select(c => c.Locations.First(l => l != currLocation));
return locations.Any(l => requiredDestinationTypes.Any(t => l.Type.Identifier.Equals(t, StringComparison.OrdinalIgnoreCase)));
if (currLocation?.Connections == null) { return true; }
foreach (LocationConnection c in currLocation.Connections)
{
if (RequireBeaconStation && !c.LevelData.HasBeaconStation) { continue; }
if (requiredDestinationTypes.Any(t => c.OtherLocation(currLocation).Type.Identifier.Equals(t, StringComparison.OrdinalIgnoreCase)))
{
return true;
}
}
return false;
}
}
}
@@ -205,6 +205,7 @@ namespace Barotrauma
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine)
{
SpawnedInOutpost = validContainer.Key.Item.SpawnedInOutpost,
AllowStealing = validContainer.Key.Item.AllowStealing,
OriginalModuleIndex = validContainer.Key.Item.OriginalModuleIndex,
OriginalContainerID = validContainer.Key.Item.ID
};
@@ -172,15 +172,15 @@ namespace Barotrauma
public void CreatePurchasedItems()
{
CreateItems(PurchasedItems);
CreateItems(PurchasedItems, Submarine.MainSub);
OnPurchasedItemsChanged?.Invoke();
}
public static void CreateItems(List<PurchasedItem> itemsToSpawn)
public static void CreateItems(List<PurchasedItem> itemsToSpawn, Submarine sub)
{
if (itemsToSpawn.Count == 0) { return; }
WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub);
WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, sub);
if (wp == null)
{
DebugConsole.ThrowError("The submarine must have a waypoint marked as Cargo for bought items to be placed correctly!");
@@ -188,25 +188,27 @@ namespace Barotrauma
}
Hull cargoRoom = Hull.FindHull(wp.WorldPosition);
if (cargoRoom == null)
{
DebugConsole.ThrowError("A waypoint marked as Cargo must be placed inside a room!");
return;
}
#if CLIENT
new GUIMessageBox("", TextManager.GetWithVariable("CargoSpawnNotification", "[roomname]", cargoRoom.DisplayName, true), new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "StoreShoppingCrateIcon");
#else
foreach (Client client in GameMain.Server.ConnectedClients)
if (sub == Submarine.MainSub)
{
ChatMessage msg = ChatMessage.Create("",
TextManager.ContainsTag(cargoRoom.RoomName) ? $"CargoSpawnNotification~[roomname]=§{cargoRoom.RoomName}" : $"CargoSpawnNotification~[roomname]={cargoRoom.RoomName}",
ChatMessageType.ServerMessageBoxInGame, null);
msg.IconStyle = "StoreShoppingCrateIcon";
GameMain.Server.SendDirectChatMessage(msg, client);
}
#if CLIENT
new GUIMessageBox("", TextManager.GetWithVariable("CargoSpawnNotification", "[roomname]", cargoRoom.DisplayName, true), new string[0], type: GUIMessageBox.Type.InGame, iconStyle: "StoreShoppingCrateIcon");
#else
foreach (Client client in GameMain.Server.ConnectedClients)
{
ChatMessage msg = ChatMessage.Create("",
TextManager.ContainsTag(cargoRoom.RoomName) ? $"CargoSpawnNotification~[roomname]=§{cargoRoom.RoomName}" : $"CargoSpawnNotification~[roomname]={cargoRoom.RoomName}",
ChatMessageType.ServerMessageBoxInGame, null);
msg.IconStyle = "StoreShoppingCrateIcon";
GameMain.Server.SendDirectChatMessage(msg, client);
}
#endif
}
List<ItemContainer> availableContainers = new List<ItemContainer>();
ItemPrefab containerPrefab = null;
@@ -71,10 +71,13 @@ namespace Barotrauma
else if (!isUnignoreOrder)
{
ActiveOrders.Add(new Pair<Order, float?>(order, fadeOutTime));
#if CLIENT
HintManager.OnActiveOrderAdded(order);
#endif
return true;
}
bool MatchesTarget(Entity existingTarget, Entity newTarget)
static bool MatchesTarget(Entity existingTarget, Entity newTarget)
{
if (existingTarget == newTarget) { return true; }
if (existingTarget is Hull existingHullTarget && newTarget is Hull newHullTarget)
@@ -145,7 +148,13 @@ namespace Barotrauma
}
#if CLIENT
AddCharacterToCrewList(character);
AddCurrentOrderIcon(character, character.CurrentOrder, character.CurrentOrderOption);
if (character.CurrentOrders != null)
{
foreach (var order in character.CurrentOrders)
{
AddCurrentOrderIcon(character, order);
}
}
#endif
if (character.AIController is HumanAIController humanAI)
{
@@ -175,7 +184,7 @@ namespace Barotrauma
List<WayPoint> spawnWaypoints = null;
List<WayPoint> mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub).ToList();
if (Level.IsLoadedOutpost)
if (Level.IsLoadedOutpost && Submarine.Loaded.Any(s => s.Info.Type == SubmarineType.Outpost && (s.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false)))
{
spawnWaypoints = WayPoint.WayPointList.FindAll(wp =>
wp.SpawnType == SpawnType.Human &&
@@ -236,6 +245,21 @@ namespace Barotrauma
conversationTimer = IsSinglePlayer ? Rand.Range(5.0f, 10.0f) : Rand.Range(45.0f, 60.0f);
}
public void RenameCharacter(CharacterInfo characterInfo, string newName)
{
int identifier = characterInfo.GetIdentifierUsingOriginalName();
var match = characterInfos.FirstOrDefault(ci => ci.GetIdentifierUsingOriginalName() == identifier);
if (match == null)
{
DebugConsole.ThrowError($"Tried to rename an invalid crew member ({identifier})");
return;
}
match.Rename(newName);
RenameCharacterProjSpecific(match);
}
partial void RenameCharacterProjSpecific(CharacterInfo characterInfo);
public void FireCharacter(CharacterInfo characterInfo)
{
RemoveCharacterInfo(characterInfo);
@@ -247,7 +271,8 @@ namespace Barotrauma
{
if (order.Second.HasValue) { order.Second -= deltaTime; }
}
ActiveOrders.RemoveAll(o => o.Second.HasValue && o.Second <= 0.0f);
ActiveOrders.RemoveAll(o => (o.Second.HasValue && o.Second <= 0.0f) ||
(o.First.TargetEntity != null && o.First.TargetEntity.Removed));
UpdateConversations(deltaTime);
UpdateProjectSpecific(deltaTime);
@@ -270,6 +295,7 @@ namespace Barotrauma
private void UpdateConversations(float deltaTime)
{
if (GameMain.GameSession?.GameMode?.Preset == GameModePreset.TestMode) { return; }
if (GameMain.NetworkMember != null && GameMain.NetworkMember.ServerSettings.DisableBotConversations) { return; }
conversationTimer -= deltaTime;
@@ -287,7 +313,7 @@ namespace Barotrauma
{
foreach (Character npc in Character.CharacterList)
{
if (npc.TeamID != CharacterTeamType.FriendlyNPC || npc.CurrentHull == null || npc.IsIncapacitated) { continue; }
if ((npc.TeamID != CharacterTeamType.FriendlyNPC && npc.TeamID != CharacterTeamType.None) || npc.CurrentHull == null || npc.IsIncapacitated) { continue; }
if (npc.AIController is HumanAIController humanAI && (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>() || humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()))
{
continue;
@@ -298,19 +324,35 @@ namespace Barotrauma
{
List<Character> availableSpeakers = new List<Character>() { npc, player };
List<string> dialogFlags = new List<string>() { "OutpostNPC", "EnterOutpost" };
if (GameMain.GameSession?.GameMode is CampaignMode campaignMode && campaignMode.Map?.CurrentLocation?.Reputation != null)
if (GameMain.GameSession?.GameMode is CampaignMode campaignMode)
{
float normalizedReputation = MathUtils.InverseLerp(
campaignMode.Map.CurrentLocation.Reputation.MinReputation,
campaignMode.Map.CurrentLocation.Reputation.MaxReputation,
campaignMode.Map.CurrentLocation.Reputation.Value);
if (normalizedReputation < 0.2f)
if (campaignMode.Map?.CurrentLocation?.Type?.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase) ?? false)
{
dialogFlags.Add("LowReputation");
if (npc.TeamID == CharacterTeamType.None)
{
dialogFlags.Remove("OutpostNPC");
dialogFlags.Add("Bandit");
}
else if (npc.TeamID == CharacterTeamType.FriendlyNPC)
{
dialogFlags.Remove("OutpostNPC");
dialogFlags.Add("Hostage");
}
}
else if (normalizedReputation > 0.8f)
else if (campaignMode.Map?.CurrentLocation?.Reputation != null)
{
dialogFlags.Add("HighReputation");
float normalizedReputation = MathUtils.InverseLerp(
campaignMode.Map.CurrentLocation.Reputation.MinReputation,
campaignMode.Map.CurrentLocation.Reputation.MaxReputation,
campaignMode.Map.CurrentLocation.Reputation.Value);
if (normalizedReputation < 0.2f)
{
dialogFlags.Add("LowReputation");
}
else if (normalizedReputation > 0.8f)
{
dialogFlags.Add("HighReputation");
}
}
}
pendingConversationLines.AddRange(NPCConversation.CreateRandom(availableSpeakers, dialogFlags));
@@ -144,7 +144,7 @@ namespace Barotrauma
new XAttribute("value", valueStr),
new XAttribute("type", value?.GetType())));
}
#if DEBUG || UNSTABLE
#if DEBUG
DebugConsole.Log(element.ToString());
#endif
modeElement.Add(element);
@@ -1,10 +1,11 @@
using System;
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma
{
class Reputation
{
public const float HostileThreshold = 0.1f;
public const float HostileThreshold = 0.2f;
public const float ReputationLossPerNPCDamage = 0.1f;
public const float ReputationLossPerStolenItemPrice = 0.01f;
public const float ReputationLossPerWallDamage = 0.1f;
@@ -52,5 +53,71 @@ namespace Barotrauma
MaxReputation = maxReputation;
InitialReputation = initialReputation;
}
public string GetReputationName()
{
return GetReputationName(NormalizedValue);
}
public static string GetReputationName(float normalizedValue)
{
if (normalizedValue < HostileThreshold)
{
return TextManager.Get("reputationverylow");
}
else if (normalizedValue < 0.4f)
{
return TextManager.Get("reputationlow");
}
else if (normalizedValue < 0.6f)
{
return TextManager.Get("reputationneutral");
}
else if (normalizedValue < 0.8f)
{
return TextManager.Get("reputationhigh");
}
return TextManager.Get("reputationveryhigh");
}
#if CLIENT
public static Color GetReputationColor(float normalizedValue)
{
if (normalizedValue < HostileThreshold)
{
return GUI.Style.ColorReputationVeryLow;
}
else if (normalizedValue < 0.4f)
{
return GUI.Style.ColorReputationLow;
}
else if (normalizedValue < 0.6f)
{
return GUI.Style.ColorReputationNeutral;
}
else if (normalizedValue < 0.8f)
{
return GUI.Style.ColorReputationHigh;
}
return GUI.Style.ColorReputationVeryHigh;
}
public string GetFormattedReputationText(bool addColorTags = false)
{
return GetFormattedReputationText(NormalizedValue, Value, addColorTags);
}
public static string GetFormattedReputationText(float normalizedValue, float value, bool addColorTags = false)
{
string reputationName = GetReputationName(normalizedValue);
string formattedReputation = TextManager.GetWithVariables("reputationformat",
new string[] { "[reputationname]", "[reputationvalue]" },
new string[] { reputationName, ((int)Math.Round(value)).ToString() });
if (addColorTags)
{
formattedReputation = $"‖color:{XMLExtensions.ColorToString(GetReputationColor(normalizedValue))}‖{formattedReputation}‖end‖";
}
return formattedReputation;
}
#endif
}
}
@@ -5,9 +5,40 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
using Barotrauma.Extensions;
namespace Barotrauma
{
internal struct CampaignSettings
{
public static CampaignSettings Empty = new CampaignSettings();
// Anything that uses this field I wasn't sure if actually needed the proper campaign settings to be passed down
public static CampaignSettings Unsure = Empty;
public bool RadiationEnabled { get; set; }
public CampaignSettings(IReadMessage inc)
{
RadiationEnabled = inc.ReadBoolean();
}
public CampaignSettings(XElement element)
{
RadiationEnabled = element.GetAttributeBool(nameof(RadiationEnabled).ToLower(), true);
}
public void Serialize(IWriteMessage msg)
{
msg.Write(RadiationEnabled);
}
public XElement Save()
{
return new XElement(nameof(CampaignSettings), new XAttribute(nameof(RadiationEnabled).ToLower(), RadiationEnabled));
}
}
abstract partial class CampaignMode : GameMode
{
const int MaxMoney = int.MaxValue / 2; //about 1 billion
@@ -31,6 +62,10 @@ namespace Barotrauma
protected XElement petsElement;
public CampaignSettings Settings;
private List<Mission> extraMissions = new List<Mission>();
public enum TransitionType
{
None,
@@ -74,11 +109,22 @@ namespace Barotrauma
get { return map; }
}
public override Mission Mission
public override IEnumerable<Mission> Missions
{
get
{
return Map.CurrentLocation?.SelectedMission;
if (Map.CurrentLocation?.SelectedMission != null)
{
if (Map.CurrentLocation.SelectedMission.Locations[0] == Map.CurrentLocation.SelectedMission.Locations[1] ||
Map.CurrentLocation.SelectedMission.Locations.Contains(Map.SelectedLocation))
{
yield return Map.CurrentLocation.SelectedMission;
}
}
foreach (Mission mission in extraMissions)
{
yield return mission;
}
}
}
@@ -106,28 +152,26 @@ namespace Barotrauma
/// The location that's displayed as the "current one" in the map screen. Normally the current outpost or the location at the start of the level,
/// but when selecting the next destination at the end of the level at an uninhabited location we use the location at the end
/// </summary>
public Location CurrentDisplayLocation
public Location GetCurrentDisplayLocation()
{
get
if (Level.Loaded?.EndLocation != null && !Level.Loaded.Generating &&
Level.Loaded.Type == LevelData.LevelType.LocationConnection &&
GetAvailableTransition(out _, out _) == TransitionType.ProgressToNextEmptyLocation)
{
if (Level.Loaded?.EndLocation != null && !Level.Loaded.Generating &&
Level.Loaded.Type == LevelData.LevelType.LocationConnection &&
GetAvailableTransition(out _, out _) == TransitionType.ProgressToNextEmptyLocation)
{
return Level.Loaded.EndLocation;
}
return Level.Loaded?.StartLocation ?? Map.CurrentLocation;
return Level.Loaded.EndLocation;
}
return Level.Loaded?.StartLocation ?? Map.CurrentLocation;
}
public List<Submarine> GetSubsToLeaveBehind(Submarine leavingSub)
{
//leave subs behind if they're not docked to the leaving sub and not at the same exit
return Submarine.Loaded.FindAll(s =>
s != leavingSub &&
!leavingSub.DockedTo.Contains(s) &&
s.Info.Type == SubmarineType.Player &&
(s.AtEndPosition != leavingSub.AtEndPosition || s.AtStartPosition != leavingSub.AtStartPosition));
return Submarine.Loaded.FindAll(sub =>
sub != leavingSub &&
!leavingSub.DockedTo.Contains(sub) &&
sub.Info.Type == SubmarineType.Player &&
sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle &&
(sub.AtEndExit != leavingSub.AtEndExit || sub.AtStartExit != leavingSub.AtStartExit));
}
public override void Start()
@@ -135,7 +179,9 @@ namespace Barotrauma
base.Start();
dialogLastSpoken.Clear();
characterOutOfBoundsTimer.Clear();
#if CLIENT
prevCampaignUIAutoOpenType = TransitionType.None;
#endif
if (PurchasedHullRepairs)
{
foreach (Structure wall in Structure.WallList)
@@ -185,6 +231,67 @@ namespace Barotrauma
/// </summary>
public event Action BeforeLevelLoading;
public override void AddExtraMissions(LevelData levelData)
{
extraMissions.Clear();
var currentLocation = Map.CurrentLocation;
if (levelData.Type == LevelData.LevelType.Outpost)
{
//if there's an available mission that takes place in the outpost, select it
var availableMissionsInLocation = currentLocation.AvailableMissions.Where(m => m.Locations[0] == currentLocation && m.Locations[1] == currentLocation);
if (availableMissionsInLocation.Any())
{
currentLocation.SelectedMission = availableMissionsInLocation.FirstOrDefault();
}
else
{
currentLocation.SelectedMission = null;
}
}
else
{
//if we had selected a mission that takes place in the outpost, deselect it when leaving the outpost
if (currentLocation.SelectedMission?.Locations[0] == currentLocation &&
currentLocation.SelectedMission?.Locations[1] == currentLocation)
{
currentLocation.SelectedMission = null;
}
if (levelData.HasBeaconStation && !levelData.IsBeaconActive)
{
var beaconMissionPrefabs = MissionPrefab.List.FindAll(m => m.Tags.Any(t => t.Equals("beaconnoreward", StringComparison.OrdinalIgnoreCase)));
if (beaconMissionPrefabs.Any())
{
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
var beaconMissionPrefab = beaconMissionPrefabs.GetRandom(rand);
if (!Missions.Any(m => m.Prefab.Type == beaconMissionPrefab.Type))
{
extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations));
}
}
}
if (levelData.HasHuntingGrounds)
{
var huntingGroundsMissionPrefabs = MissionPrefab.List.FindAll(m => m.Tags.Any(t => t.Equals("huntinggroundsnoreward", StringComparison.OrdinalIgnoreCase)));
if (!huntingGroundsMissionPrefabs.Any())
{
DebugConsole.AddWarning("Could not find a hunting grounds mission for the level. No mission with the tag \"huntinggroundsnoreward\" found.");
}
else
{
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
var huntingGroundsMissionPrefab = huntingGroundsMissionPrefabs.GetRandom(rand);
if (!Missions.Any(m => m.Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))))
{
extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations));
}
}
}
}
}
public void LoadNewLevel()
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
@@ -215,8 +322,8 @@ namespace Barotrauma
"(current location: " + (map.CurrentLocation?.Name ?? "null") + ", " +
"selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " +
"leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
"at start: " + (leavingSub?.AtStartPosition.ToString() ?? "null") + ", " +
"at end: " + (leavingSub?.AtEndPosition.ToString() ?? "null") + ")\n" +
"at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " +
"at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ")\n" +
Environment.StackTrace.CleanupStackTrace());
return;
}
@@ -227,8 +334,8 @@ namespace Barotrauma
"current location: " + (map.CurrentLocation?.Name ?? "null") + ", " +
"selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " +
"leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
"at start: " + (leavingSub?.AtStartPosition.ToString() ?? "null") + ", " +
"at end: " + (leavingSub?.AtEndPosition.ToString() ?? "null") + ")\n" +
"at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " +
"at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ")\n" +
Environment.StackTrace.CleanupStackTrace());
return;
}
@@ -239,8 +346,8 @@ namespace Barotrauma
" (current location: " + (map.CurrentLocation?.Name ?? "null") + ", " +
"selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " +
"leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
"at start: " + (leavingSub?.AtStartPosition.ToString() ?? "null") + ", " +
"at end: " + (leavingSub?.AtEndPosition.ToString() ?? "null") + ", " +
"at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " +
"at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ", " +
"transition type: " + availableTransition + ")");
IsFirstRound = false;
@@ -277,7 +384,7 @@ namespace Barotrauma
//currently travelling from location to another
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
{
if (leavingSub.AtEndPosition)
if (leavingSub.AtEndExit)
{
if (Map.EndLocation != null &&
map.SelectedLocation == Map.EndLocation &&
@@ -303,15 +410,15 @@ namespace Barotrauma
return TransitionType.ProgressToNextEmptyLocation;
}
}
else if (leavingSub.AtStartPosition)
else if (leavingSub.AtStartExit)
{
if (map.CurrentLocation.Type.HasOutpost && Level.Loaded.StartOutpost != null)
{
nextLevel = map.CurrentLocation.LevelData;
return TransitionType.ReturnToPreviousLocation;
}
else if (map.SelectedLocation != null && map.SelectedLocation != map.CurrentLocation && !map.CurrentLocation.Type.HasOutpost &&
(Level.Loaded.LevelData != map.SelectedConnection.LevelData))
else if (map.SelectedLocation != null && map.SelectedLocation != map.CurrentLocation && !map.CurrentLocation.Type.HasOutpost &&
map.SelectedConnection != null && Level.Loaded.LevelData != map.SelectedConnection.LevelData)
{
nextLevel = map.SelectedConnection.LevelData;
return TransitionType.LeaveLocation;
@@ -358,12 +465,15 @@ namespace Barotrauma
leavingSubAtStart ??= Submarine.MainSub;
leavingSubAtEnd ??= Submarine.MainSub;
}
int playersInSubAtStart = leavingSubAtStart == null ? 0 :
int playersInSubAtStart = leavingSubAtStart == null || !leavingSubAtStart.AtStartExit ? 0 :
leavingPlayers.Count(c => c.Submarine == leavingSubAtStart || leavingSubAtStart.DockedTo.Contains(c.Submarine) || (Level.Loaded.StartOutpost != null && c.Submarine == Level.Loaded.StartOutpost));
int playersInSubAtEnd = leavingSubAtEnd == null ? 0 :
int playersInSubAtEnd = leavingSubAtEnd == null || !leavingSubAtEnd.AtEndExit ? 0 :
leavingPlayers.Count(c => c.Submarine == leavingSubAtEnd || leavingSubAtEnd.DockedTo.Contains(c.Submarine) || (Level.Loaded.EndOutpost != null && c.Submarine == Level.Loaded.EndOutpost));
if (playersInSubAtStart == 0 && playersInSubAtEnd == 0) { return null; }
if (playersInSubAtStart == 0 && playersInSubAtEnd == 0)
{
return null;
}
return playersInSubAtStart > playersInSubAtEnd ? leavingSubAtStart : leavingSubAtEnd;
@@ -371,7 +481,7 @@ namespace Barotrauma
{
if (Level.Loaded.StartOutpost == null)
{
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartPosition, ignoreOutposts: true);
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID);
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
}
else
@@ -380,13 +490,14 @@ namespace Barotrauma
if (Level.Loaded.StartOutpost.DockedTo.Any())
{
var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault();
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { return null; }
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
}
//nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.StartOutpost)) { return null; }
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true);
if (closestSub == null || !closestSub.AtStartPosition) { return null; }
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID);
if (closestSub == null || !closestSub.AtStartExit) { return null; }
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
}
}
@@ -398,7 +509,7 @@ namespace Barotrauma
if (Level.Loaded.EndOutpost == null)
{
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndPosition, ignoreOutposts: true);
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID);
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
}
else
@@ -407,13 +518,14 @@ namespace Barotrauma
if (Level.Loaded.EndOutpost.DockedTo.Any())
{
var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault();
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { return null; }
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
}
//nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.EndOutpost)) { return null; }
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true);
if (closestSub == null || !closestSub.AtEndPosition) { return null; }
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID);
if (closestSub == null || !closestSub.AtEndExit) { return null; }
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
}
}
@@ -425,16 +537,19 @@ namespace Barotrauma
foreach (Item item in Item.ItemList)
{
if (!item.SpawnedInOutpost || item.OriginalModuleIndex < 0) { continue; }
if ((!(item.GetRootInventoryOwner()?.Submarine?.Info?.IsOutpost ?? false)) || item.Submarine == null || !item.Submarine.Info.IsOutpost)
var owner = item.GetRootInventoryOwner();
if ((!(owner?.Submarine?.Info?.IsOutpost ?? false)) || (owner is Character character && character.TeamID == CharacterTeamType.Team1) || item.Submarine == null || !item.Submarine.Info.IsOutpost)
{
takenItems.Add(item);
}
}
map.CurrentLocation.RegisterTakenItems(takenItems);
map.CurrentLocation.AddToStock(CargoManager.SoldItems);
CargoManager.ClearSoldItemsProjSpecific();
map.CurrentLocation.RemoveFromStock(CargoManager.PurchasedItems);
if (map != null && CargoManager != null)
{
map.CurrentLocation.RegisterTakenItems(takenItems);
map.CurrentLocation.AddToStock(CargoManager.SoldItems);
CargoManager.ClearSoldItemsProjSpecific();
map.CurrentLocation.RemoveFromStock(CargoManager.PurchasedItems);
}
if (GameMain.NetworkMember == null)
{
CargoManager.ClearItemsInBuyCrate();
@@ -444,11 +559,11 @@ namespace Barotrauma
{
if (GameMain.NetworkMember.IsServer)
{
CargoManager.ClearItemsInBuyCrate();
CargoManager?.ClearItemsInBuyCrate();
}
else if (GameMain.NetworkMember.IsClient)
{
CargoManager.ClearItemsInSellCrate();
CargoManager?.ClearItemsInSellCrate();
}
}
@@ -480,7 +595,7 @@ namespace Barotrauma
{
CrewManager.RemoveCharacterInfo(ci);
}
ci?.ResetCurrentOrder();
ci?.ClearCurrentOrders();
}
foreach (DockingPort port in DockingPort.List)
@@ -502,14 +617,30 @@ namespace Barotrauma
{
connection.Difficulty = MathHelper.Lerp(connection.Difficulty, 100.0f, 0.25f);
connection.LevelData.Difficulty = connection.Difficulty;
connection.LevelData.IsBeaconActive = false;
connection.LevelData.HasHuntingGrounds = connection.LevelData.OriginallyHadHuntingGrounds;
}
foreach (Location location in Map.Locations)
{
if (location.Type != location.OriginalType)
{
location.ChangeType(location.OriginalType);
location.PendingLocationTypeChange = null;
}
location.CreateStore(force: true);
location.ClearMissions();
location.Discovered = false;
}
Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation));
Map.SelectLocation(-1);
if (Map.Radiation != null)
{
Map.Radiation.Amount = Map.Radiation.Params.StartingRadiation;
}
foreach (Location location in Map.Locations)
{
location.TurnsInRadiation = 0;
}
EndCampaignProjSpecific();
if (CampaignMetadata != null)
@@ -523,14 +654,12 @@ namespace Barotrauma
public bool TryHireCharacter(Location location, CharacterInfo characterInfo)
{
if (characterInfo == null) { return false; }
if (Money < characterInfo.Salary) { return false; }
characterInfo.IsNewHire = true;
location.RemoveHireableCharacter(characterInfo);
CrewManager.AddCharacterInfo(characterInfo);
Money -= characterInfo.Salary;
return true;
}
@@ -552,18 +681,14 @@ namespace Barotrauma
HumanAIController humanAI = npc.AIController as HumanAIController;
if (humanAI == null) { yield return CoroutineStatus.Failure; }
OrderInfo? prevSpeakerOrder = null;
if (humanAI.CurrentOrder != null)
{
prevSpeakerOrder = new OrderInfo(humanAI.CurrentOrder, humanAI.CurrentOrderOption);
}
var waitOrder = Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase));
humanAI.SetOrder(waitOrder, option: string.Empty, orderGiver: null, speak: false);
humanAI.SetForcedOrder(waitOrder, string.Empty, null);
var waitObjective = humanAI.ObjectiveManager.ForcedOrder;
humanAI.FaceTarget(interactor);
while (!npc.Removed && !interactor.Removed &&
Vector2.DistanceSquared(npc.WorldPosition, interactor.WorldPosition) < 300.0f * 300.0f &&
humanAI.CurrentOrder == waitOrder &&
humanAI.ObjectiveManager.ForcedOrder == waitObjective &&
humanAI.AllowCampaignInteraction() &&
!interactor.IsIncapacitated)
{
@@ -574,17 +699,7 @@ namespace Barotrauma
ShowCampaignUI = false;
#endif
if (humanAI.CurrentOrder == waitOrder)
{
if (prevSpeakerOrder != null)
{
humanAI.SetOrder(prevSpeakerOrder.Value.Order, prevSpeakerOrder.Value.OrderOption, orderGiver: null, speak: false);
}
else
{
humanAI.SetOrder(null, string.Empty, orderGiver: null, speak: false);
}
}
humanAI.ClearForcedOrder();
yield return CoroutineStatus.Success;
}
@@ -1,10 +1,11 @@
using System;
using System.Collections.Generic;
namespace Barotrauma
{
class CoOpMode : MissionMode
{
public CoOpMode(GameModePreset preset, MissionPrefab missionPrefab) : base(preset, ValidateMissionPrefab(missionPrefab, MissionPrefab.CoOpMissionClasses)) { }
public CoOpMode(GameModePreset preset, IEnumerable<MissionPrefab> missionPrefabs) : base(preset, ValidateMissionPrefabs(missionPrefabs, MissionPrefab.CoOpMissionClasses)) { }
public CoOpMode(GameModePreset preset, MissionType missionType, string seed) : base(preset, ValidateMissionType(missionType, MissionPrefab.CoOpMissionClasses), seed) { }
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -16,9 +17,9 @@ namespace Barotrauma
get { return GameMain.GameSession?.CrewManager; }
}
public virtual Mission Mission
public virtual IEnumerable<Mission> Missions
{
get { return null; }
get { return Enumerable.Empty<Mission>(); }
}
public bool IsSinglePlayer
@@ -54,6 +55,8 @@ namespace Barotrauma
}
public virtual void ShowStartMessage() { }
public virtual void AddExtraMissions(LevelData levelData) { }
public virtual void AddToGUIUpdateList()
{
@@ -5,37 +5,43 @@ namespace Barotrauma
{
abstract partial class MissionMode : GameMode
{
private readonly Mission mission;
private readonly List<Mission> missions = new List<Mission>();
public override Mission Mission
public override IEnumerable<Mission> Missions
{
get
{
return mission;
return missions;
}
}
public MissionMode(GameModePreset preset, MissionPrefab missionPrefab)
public MissionMode(GameModePreset preset, IEnumerable<MissionPrefab> missionPrefabs)
: base(preset)
{
Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation };
mission = missionPrefab.Instantiate(locations);
foreach (MissionPrefab missionPrefab in missionPrefabs)
{
missions.Add(missionPrefab.Instantiate(locations));
}
}
public MissionMode(GameModePreset preset, MissionType missionType, string seed)
: base(preset)
{
Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation };
mission = Mission.LoadRandom(locations, seed, false, missionType);
missions.Add(Mission.LoadRandom(locations, seed, false, missionType));
}
protected static MissionPrefab ValidateMissionPrefab(MissionPrefab missionPrefab, Dictionary<MissionType, Type> missionClasses)
protected static IEnumerable<MissionPrefab> ValidateMissionPrefabs(IEnumerable<MissionPrefab> missionPrefabs, Dictionary<MissionType, Type> missionClasses)
{
if (ValidateMissionType(missionPrefab.Type, missionClasses) != missionPrefab.Type)
foreach (MissionPrefab missionPrefab in missionPrefabs)
{
throw new InvalidOperationException("Cannot start gamemode with mission type " + missionPrefab.Type);
if (ValidateMissionType(missionPrefab.Type, missionClasses) != missionPrefab.Type)
{
throw new InvalidOperationException("Cannot start gamemode with mission type " + missionPrefab.Type);
}
}
return missionPrefab;
return missionPrefabs;
}
protected static MissionType ValidateMissionType(MissionType missionType, Dictionary<MissionType, Type> missionClasses)
@@ -59,13 +59,14 @@ namespace Barotrauma
InitCampaignData();
}
public static MultiPlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub)
public static MultiPlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub, CampaignSettings settings)
{
MultiPlayerCampaign campaign = new MultiPlayerCampaign();
//only the server generates the map, the clients load it from a save file
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
campaign.map = new Map(campaign, mapSeed);
campaign.map = new Map(campaign, mapSeed, settings);
campaign.Settings = settings;
}
campaign.InitProjSpecific();
return campaign;
@@ -128,11 +129,14 @@ namespace Barotrauma
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "campaignsettings":
Settings = new CampaignSettings(subElement);
break;
case "map":
if (map == null)
{
//map not created yet, loading this campaign for the first time
map = Map.Load(this, subElement);
map = Map.Load(this, subElement, Settings);
}
else
{
@@ -1,13 +1,11 @@
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class PvPMode : MissionMode
{
public PvPMode(GameModePreset preset, MissionPrefab missionPrefab) : base(preset, ValidateMissionPrefab(missionPrefab, MissionPrefab.PvPMissionClasses)) { }
public PvPMode(GameModePreset preset, IEnumerable<MissionPrefab> missionPrefabs) : base(preset, ValidateMissionPrefabs(missionPrefabs, MissionPrefab.PvPMissionClasses)) { }
public PvPMode(GameModePreset preset, MissionType missionType, string seed) : base(preset, ValidateMissionType(missionType, MissionPrefab.PvPMissionClasses), seed) { }
@@ -22,7 +22,8 @@ namespace Barotrauma
public double RoundStartTime;
public Mission Mission { get; private set; }
private readonly List<Mission> missions = new List<Mission>();
public IEnumerable<Mission> Missions { get { return missions; } }
public CharacterTeamType? WinningTeam;
@@ -102,29 +103,28 @@ namespace Barotrauma
/// <summary>
/// Start a new GameSession. Will be saved to the specified save path (if playing a game mode that can be saved).
/// </summary>
public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, string seed = null, MissionType missionType = MissionType.None)
public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, CampaignSettings settings, string seed = null, MissionType missionType = MissionType.None)
: this(submarineInfo)
{
this.SavePath = savePath;
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, missionType: missionType);
GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, settings, missionType: missionType);
}
/// <summary>
/// Start a new GameSession with a specific pre-selected mission.
/// </summary>
public GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string seed = null, MissionPrefab missionPrefab = null)
public GameSession(SubmarineInfo submarineInfo, GameModePreset gameModePreset, string seed = null, IEnumerable<MissionPrefab> missionPrefabs = null)
: this(submarineInfo)
{
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, missionPrefab: missionPrefab);
GameMode = InstantiateGameMode(gameModePreset, seed, submarineInfo, CampaignSettings.Empty, missionPrefabs: missionPrefabs);
}
/// <summary>
/// Load a game session from the specified XML document. The session will be saved to the specified path.
/// </summary>
public GameSession(SubmarineInfo submarineInfo, List<SubmarineInfo> ownedSubmarines, XDocument doc, string saveFile)
: this(submarineInfo, ownedSubmarines)
public GameSession(SubmarineInfo submarineInfo, List<SubmarineInfo> ownedSubmarines, XDocument doc, string saveFile) : this(submarineInfo, ownedSubmarines)
{
this.SavePath = saveFile;
GameMain.GameSession = this;
@@ -158,23 +158,23 @@ namespace Barotrauma
}
}
private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, SubmarineInfo selectedSub, MissionPrefab missionPrefab = null, MissionType missionType = MissionType.None)
private GameMode InstantiateGameMode(GameModePreset gameModePreset, string seed, SubmarineInfo selectedSub, CampaignSettings settings, IEnumerable<MissionPrefab> missionPrefabs = null, MissionType missionType = MissionType.None)
{
if (gameModePreset.GameModeType == typeof(CoOpMode))
{
return missionPrefab != null ?
new CoOpMode(gameModePreset, missionPrefab) :
return missionPrefabs != null ?
new CoOpMode(gameModePreset, missionPrefabs) :
new CoOpMode(gameModePreset, missionType, seed ?? ToolBox.RandomSeed(8));
}
else if (gameModePreset.GameModeType == typeof(PvPMode))
{
return missionPrefab != null ?
new PvPMode(gameModePreset, missionPrefab) :
return missionPrefabs != null ?
new PvPMode(gameModePreset, missionPrefabs) :
new PvPMode(gameModePreset, missionType, seed ?? ToolBox.RandomSeed(8));
}
else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign))
{
var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub);
var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings);
if (campaign != null && selectedSub != null)
{
campaign.Money = Math.Max(MultiPlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price);
@@ -184,7 +184,7 @@ namespace Barotrauma
#if CLIENT
else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign))
{
var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub);
var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings);
if (campaign != null && selectedSub != null)
{
campaign.Money = Math.Max(SinglePlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price);
@@ -210,7 +210,7 @@ namespace Barotrauma
}
}
private void CreateDummyLocations()
private void CreateDummyLocations(LocationType? forceLocationType = null)
{
dummyLocations = new Location[2];
@@ -227,7 +227,7 @@ namespace Barotrauma
MTRandom rand = new MTRandom(ToolBox.StringToInt(seed));
for (int i = 0; i < 2; i++)
{
dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true);
dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true, forceLocationType: forceLocationType);
}
}
@@ -282,9 +282,38 @@ namespace Barotrauma
(OwnedSubmarines != null && OwnedSubmarines.Any(os => os.Name == query.Name));
}
public bool IsCurrentLocationRadiated()
{
if (Map?.CurrentLocation == null || Campaign == null) { return false; }
bool isRadiated = Map.CurrentLocation.IsRadiated();
if (Level.Loaded?.EndLocation is { } endLocation)
{
isRadiated |= endLocation.IsRadiated();
}
return isRadiated;
}
public void StartRound(string levelSeed, float? difficulty = null)
{
StartRound(LevelData.CreateRandom(levelSeed, difficulty));
LevelData randomLevel = null;
foreach (Mission mission in Missions.Union(GameMode.Missions))
{
MissionPrefab missionPrefab = mission.Prefab;
if (missionPrefab != null &&
missionPrefab.AllowedLocationTypes.Any() &&
!missionPrefab.AllowedConnectionTypes.Any())
{
LocationType locationType = LocationType.List.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m.Equals(lt.Identifier, StringComparison.OrdinalIgnoreCase)));
CreateDummyLocations(locationType);
randomLevel = LevelData.CreateRandom(levelSeed, difficulty, requireOutpost: true);
break;
}
}
randomLevel ??= LevelData.CreateRandom(levelSeed, difficulty);
StartRound(randomLevel);
}
public void StartRound(LevelData levelData, bool mirrorLevel = false, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null)
@@ -307,12 +336,6 @@ namespace Barotrauma
LevelData = levelData;
if (GameMode is CampaignMode campaignMode && GameMode.Mission != null &&
LevelData != null && LevelData.Type == LevelData.LevelType.Outpost)
{
campaignMode.Map.CurrentLocation.SelectedMission = null;
}
Submarine.Unload();
Submarine = Submarine.MainSub = new Submarine(SubmarineInfo);
foreach (Submarine sub in Submarine.GetConnectedSubs())
@@ -332,6 +355,19 @@ namespace Barotrauma
Submarine.MainSubs[1] = new Submarine(SubmarineInfo, true);
}
if (GameMain.NetworkMember?.ServerSettings?.LockAllDefaultWires ?? false)
{
foreach (Item item in Item.ItemList)
{
if (item.Submarine == Submarine.MainSubs[0] ||
(Submarine.MainSubs[1] != null && item.Submarine == Submarine.MainSubs[1]))
{
Wire wire = item.GetComponent<Wire>();
if (wire != null && !wire.NoAutoLock && wire.Connections.Any(c => c != null)) { wire.Locked = true; }
}
}
}
Level level = null;
if (levelData != null)
{
@@ -340,11 +376,6 @@ namespace Barotrauma
InitializeLevel(level);
GameAnalyticsManager.AddDesignEvent("Submarine:" + Submarine.Info.Name);
GameAnalyticsManager.AddDesignEvent("Level", ToolBox.StringToInt(levelData?.Seed ?? "[NO_LEVEL]"));
GameAnalyticsManager.AddProgressionEvent(GameAnalyticsSDK.Net.EGAProgressionStatus.Start,
GameMode.Preset.Identifier, (Mission == null ? "None" : Mission.GetType().ToString()));
#if CLIENT
if (GameMode is CampaignMode) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); }
@@ -354,7 +385,7 @@ namespace Barotrauma
existingRoundSummary.ContinueButton.Visible = true;
}
RoundSummary = new RoundSummary(Submarine.Info, GameMode, Mission, StartLocation, EndLocation);
RoundSummary = new RoundSummary(Submarine.Info, GameMode, Missions, StartLocation, EndLocation);
if (!(GameMode is TutorialMode) && !(GameMode is TestGameMode))
{
@@ -363,7 +394,16 @@ namespace Barotrauma
{
GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false);
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.Name), Color.CadetBlue, playSound: false);
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), (Mission == null ? TextManager.Get("None") : Mission.Name)), Color.CadetBlue, playSound: false);
if (missions.Count > 1)
{
string joinedMissionNames = string.Join(", ", missions.Select(m => m.Name));
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), joinedMissionNames), Color.CadetBlue, playSound: false);
}
else
{
var mission = missions.FirstOrDefault();
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Mission"), mission?.Name ?? TextManager.Get("None")), Color.CadetBlue, playSound: false);
}
}
else
{
@@ -372,6 +412,8 @@ namespace Barotrauma
}
GUI.PreventPauseMenuToggle = false;
HintManager.OnRoundStarted();
#endif
}
@@ -383,6 +425,7 @@ namespace Barotrauma
#if CLIENT
GameMain.LightManager.LosEnabled = GameMain.Client == null || GameMain.Client.CharacterInfo != null;
if (GameMain.LightManager.LosEnabled) { GameMain.LightManager.LosAlpha = 1f; }
if (GameMain.Client == null) GameMain.LightManager.LosMode = GameMain.Config.LosMode;
#endif
LevelData = level?.LevelData;
@@ -400,16 +443,18 @@ namespace Barotrauma
Entity.Spawner = new EntitySpawner();
if (GameMode.Mission != null) { Mission = GameMode.Mission; }
if (GameMode != null) { GameMode.Start(); }
if (GameMode.Mission != null)
missions.Clear();
GameMode.AddExtraMissions(LevelData);
missions.AddRange(GameMode.Missions);
GameMode.Start();
foreach (Mission mission in missions)
{
int prevEntityCount = Entity.GetEntities().Count();
Mission.Start(Level.Loaded);
mission.Start(Level.Loaded);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count() != prevEntityCount)
{
DebugConsole.ThrowError(
"Entity count has changed after starting a mission as a client. " +
$"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " +
"The clients should not instantiate entities themselves when starting the mission," +
" but instead the server should inform the client of the spawned entities using Mission.ServerWriteInitial.");
}
@@ -433,13 +478,6 @@ namespace Barotrauma
}
if (GameMode is MultiPlayerCampaign mpCampaign)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
mpCampaign.CargoManager.CreatePurchasedItems();
#if SERVER
mpCampaign.SendCrewState(false, null);
#endif
}
mpCampaign.UpgradeManager.ApplyUpgrades();
mpCampaign.UpgradeManager.SanityCheckUpgrades(Submarine);
}
@@ -514,7 +552,7 @@ namespace Barotrauma
{
Submarine.SetPosition(spawnPos);
myPort.Dock(outPostPort);
myPort.Lock(true);
myPort.Lock(isNetworkMessage: true, applyEffects: false);
}
else
{
@@ -531,7 +569,7 @@ namespace Barotrauma
}
else
{
Submarine.SetPosition(Submarine.FindSpawnPos(level.StartPosition, verticalMoveDir: 1));
Submarine.SetPosition(Submarine.FindSpawnPos(level.StartPosition));
Submarine.NeutralizeBallast();
Submarine.EnableMaintainPosition();
}
@@ -553,21 +591,33 @@ namespace Barotrauma
{
EventManager?.Update(deltaTime);
GameMode?.Update(deltaTime);
Mission?.Update(deltaTime);
//backwards for loop because the missions may get completed and removed from the list in Update()
for (int i = missions.Count - 1; i >= 0; i--)
{
missions[i].Update(deltaTime);
}
UpdateProjSpecific(deltaTime);
}
public Mission GetMission(int index)
{
if (index < 0 || index >= missions.Count) { return null; }
return missions[index];
}
public int GetMissionIndex(Mission mission)
{
return missions.IndexOf(mission);
}
partial void UpdateProjSpecific(float deltaTime);
public void EndRound(string endMessage, List<TraitorMissionResult> traitorResults = null, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None)
{
if (Mission != null) { Mission.End(); }
GameAnalyticsManager.AddProgressionEvent(
(Mission == null || Mission.Completed) ? GameAnalyticsSDK.Net.EGAProgressionStatus.Complete : GameAnalyticsSDK.Net.EGAProgressionStatus.Fail,
GameMode.Preset.Identifier,
Mission == null ? "None" : Mission.GetType().ToString());
foreach (Mission mission in missions)
{
mission.End();
}
#if CLIENT
if (GUI.PauseMenuOpen)
{
@@ -593,8 +643,12 @@ namespace Barotrauma
GameMode?.End(transitionType);
EventManager?.EndRound();
StatusEffect.StopAll();
Mission = null;
missions.Clear();
IsRunning = false;
#if CLIENT
HintManager.OnRoundEnded();
#endif
}
public void KillCharacter(Character character)
@@ -1,5 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -39,5 +39,12 @@ namespace Barotrauma
AvailableCharacters.ForEach(c => c.Remove());
AvailableCharacters.Clear();
}
public void RenameCharacter(CharacterInfo characterInfo, string newName)
{
if (characterInfo == null || string.IsNullOrEmpty(newName)) { return; }
AvailableCharacters.FirstOrDefault(ci => ci == characterInfo)?.Rename(newName);
PendingHires.FirstOrDefault(ci => ci == characterInfo)?.Rename(newName);
}
}
}
@@ -104,7 +104,8 @@ namespace Barotrauma
/// </remarks>
/// <param name="prefab"></param>
/// <param name="category"></param>
public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category)
/// <param name="force"></param>
public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category, bool force = false)
{
if (!CanUpgradeSub())
{
@@ -136,6 +137,11 @@ namespace Barotrauma
});
}
if (force)
{
price = 0;
}
if (Campaign.Money > price)
{
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
@@ -154,7 +160,7 @@ namespace Barotrauma
PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category);
#if CLIENT
DebugLog($"CLIENT: Purchased level {GetUpgradeLevel(prefab, category) + 1} {category.Name}.{prefab.Name} for ${price}", GUI.Style.Orange);
DebugLog($"CLIENT: Purchased level {GetUpgradeLevel(prefab, category) + 1} {category.Name}.{prefab.Name} for {price}", GUI.Style.Orange);
#endif
if (upgrade == null)
@@ -689,7 +695,7 @@ namespace Barotrauma
public static void DebugLog(string msg, Color? color = null)
{
#if UNSTABLE || DEBUG
#if DEBUG
DebugConsole.NewMessage(msg, color ?? Color.GreenYellow);
#else
DebugConsole.Log(msg);
@@ -171,7 +171,7 @@ namespace Barotrauma
/// <summary>
/// How many corpses there can be in a sub before they start to get despawned
/// </summary>
public int CorpsesPerSubDespawnThreshold { get; set; } = 5;
public int CorpsesPerSubDespawnThreshold { get; set; } = 10;
private string overrideSaveFolder, overrideMultiplayerSaveFolder;
@@ -301,10 +301,14 @@ namespace Barotrauma
public volatile bool WaitingForAutoUpdate;
public bool DisableInGameHints { get; set; }
#if DEBUG
public bool AutomaticQuickStartEnabled { get; set; }
public bool AutomaticCampaignLoadEnabled { get; set; }
public bool TextManagerDebugModeEnabled { get; set; }
public bool ModBreakerMode { get; set; }
#endif
private System.IO.FileSystemWatcher modsFolderWatcher;
@@ -735,6 +739,10 @@ namespace Barotrauma
private bool textScaleDirty;
public List<string> CompletedTutorialNames { get; private set; }
/// <summary>
/// Identifiers of hints the player has chosen not to see again
/// </summary>
public HashSet<string> IgnoredHints { get; private set; } = new HashSet<string>();
public HashSet<string> EncounteredCreatures { get; private set; } = new HashSet<string>();
public HashSet<string> KilledCreatures { get; private set; } = new HashSet<string>();
@@ -1151,6 +1159,12 @@ namespace Barotrauma
CompletedTutorialNames.Add(element.GetAttributeString("name", ""));
}
}
if (doc.Root.Element("ignoredhints") is XElement ignoredHintsElement)
{
IgnoredHints = new HashSet<string>(ignoredHintsElement.GetAttributeStringArray("identifiers", new string[0], convertToLowerInvariant: true));
}
XElement encounters = doc.Root.Element("encountered");
if (encounters != null)
{
@@ -1172,7 +1186,7 @@ namespace Barotrauma
#endregion
#region Save PlayerConfig
public void SaveNewPlayerConfig()
public bool SaveNewPlayerConfig()
{
XDocument doc = new XDocument();
UnsavedSettings = false;
@@ -1211,11 +1225,13 @@ namespace Barotrauma
new XAttribute("tutorialskipwarning", ShowTutorialSkipWarning),
new XAttribute("corpsedespawndelay", CorpseDespawnDelay),
new XAttribute("corpsespersubdespawnthreshold", CorpsesPerSubDespawnThreshold),
new XAttribute("usedualmodesockets", UseDualModeSockets)
new XAttribute("usedualmodesockets", UseDualModeSockets),
new XAttribute("disableingamehints", DisableInGameHints)
#if DEBUG
, new XAttribute("automaticquickstartenabled", AutomaticQuickStartEnabled)
, new XAttribute("automaticcampaignloadenabled", AutomaticCampaignLoadEnabled)
, new XAttribute("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled)
, new XAttribute("modbreakermode", ModBreakerMode)
#endif
);
@@ -1403,6 +1419,8 @@ namespace Barotrauma
}
doc.Root.Add(tutorialElement);
doc.Root.Add(new XElement("ignoredhints", new XAttribute("identifiers", string.Join(",", IgnoredHints).Trim().ToLowerInvariant())));
doc.Root.Add(new XElement("encountered", new XAttribute("creatures", string.Join(",", EncounteredCreatures).Trim().ToLowerInvariant())));
doc.Root.Add(new XElement("killed", new XAttribute("creatures", string.Join(",", KilledCreatures).Trim().ToLowerInvariant())));
@@ -1426,7 +1444,10 @@ namespace Barotrauma
DebugConsole.ThrowError("Saving game settings failed.", e);
GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace());
return false;
}
return true;
}
#endregion
@@ -1460,10 +1481,12 @@ namespace Barotrauma
EditorDisclaimerShown = doc.Root.GetAttributeBool("editordisclaimershown", EditorDisclaimerShown);
ShowTutorialSkipWarning = doc.Root.GetAttributeBool("tutorialskipwarning", true);
UseDualModeSockets = doc.Root.GetAttributeBool("usedualmodesockets", true);
DisableInGameHints = doc.Root.GetAttributeBool("disableingamehints", DisableInGameHints);
#if DEBUG
AutomaticQuickStartEnabled = doc.Root.GetAttributeBool("automaticquickstartenabled", AutomaticQuickStartEnabled);
AutomaticCampaignLoadEnabled = doc.Root.GetAttributeBool("automaticcampaignloadenabled", AutomaticCampaignLoadEnabled);
TextManagerDebugModeEnabled = doc.Root.GetAttributeBool("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled);
ModBreakerMode = doc.Root.GetAttributeBool("modbreakermode", ModBreakerMode);
#endif
XElement gameplayElement = doc.Root.Element("gameplay");
jobPreferences = new List<Pair<string, int>>();
@@ -1579,6 +1602,32 @@ namespace Barotrauma
CurrentCorePackage = null;
enabledRegularPackages.Clear();
#if DEBUG && CLIENT
if (ModBreakerMode)
{
CurrentCorePackage = ContentPackage.CorePackages.GetRandom();
foreach (var regularPackage in ContentPackage.RegularPackages)
{
if (Rand.Range(0.0, 1.0) <= 0.5)
{
enabledRegularPackages.Add(regularPackage);
}
}
ContentPackage.SortContentPackages(p =>
{
return Rand.Int(int.MaxValue);
}, config: this);
if (CurrentCorePackage == null)
{
CurrentCorePackage = ContentPackage.CorePackages.First();
}
TextManager.LoadTextPacks(AllEnabledPackages);
return;
}
#endif
var contentPackagesElement = doc.Root.Element("contentpackages");
if (contentPackagesElement != null)
{
@@ -1725,6 +1774,7 @@ namespace Barotrauma
AutoUpdateWorkshopItems = true;
TextScale = 1;
textScaleDirty = false;
DisableInGameHints = false;
}
}
}
@@ -123,7 +123,16 @@ namespace Barotrauma
public override bool CanBePut(Item item, int i)
{
return base.CanBePut(item, i) && item.AllowedSlots.Contains(SlotTypes[i]);
return
base.CanBePut(item, i) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) &&
(SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1);
}
public override bool CanBePut(ItemPrefab itemPrefab, int i)
{
return
base.CanBePut(itemPrefab, i) &&
(SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1);
}
public bool CanBeAutoMovedToCorrectSlots(Item item)
@@ -141,6 +150,44 @@ namespace Barotrauma
return false;
}
public override void RemoveItem(Item item)
{
RemoveItem(item, tryEquipFromSameStack: false);
}
public void RemoveItem(Item item, bool tryEquipFromSameStack)
{
if (!Contains(item)) { return; }
bool wasEquipped = character.HasEquippedItem(item);
var indices = FindIndices(item);
base.RemoveItem(item);
#if CLIENT
CreateSlots();
#endif
//if the item was equipped and there are more items in the same stack, equip one of those items
if (tryEquipFromSameStack && wasEquipped)
{
int limbSlot = indices.Find(j => SlotTypes[j] != InvSlotType.Any);
foreach (int i in indices)
{
var itemInSameSlot = GetItemAt(i);
if (itemInSameSlot != null)
{
if (TryPutItem(itemInSameSlot, limbSlot, allowSwapping: false, allowCombine: false, character))
{
#if CLIENT
visualSlots[i].ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.412f);
#endif
}
break;
}
}
}
}
/// <summary>
/// If there is no room in the generic inventory (InvSlotType.Any), check if the item can be auto-equipped into its respective limbslot
/// </summary>
@@ -156,6 +203,19 @@ namespace Barotrauma
}
}
if (allowedSlots != null && !allowedSlots.Contains(InvSlotType.Any))
{
int slot = FindLimbSlot(allowedSlots.First());
if (slot > -1 && slots[slot].Items.Any(it => it != item) && slots[slot].First().AllowDroppingOnSwapWith(item))
{
foreach (Item existingItem in slots[slot].Items.ToList())
{
existingItem.Drop(user);
if (existingItem.ParentInventory != null) { existingItem.ParentInventory.RemoveItem(existingItem); }
}
}
}
return TryPutItem(item, user, allowedSlots, createNetworkEvent);
}
@@ -38,13 +38,15 @@ namespace Barotrauma.Items.Components
private Fixture outsideBlocker;
private Body doorBody;
private float dockingCooldown;
private bool docked;
private bool obstructedWayPointsDisabled;
private float forceLockTimer;
//if the submarine isn't in the correct position to lock within this time after docking has been activated,
//force the sub to the correct position
const float ForceLockDelay = 1.0f;
const float ForceLockDelay = 1.0f;
public int DockingDir { get; set; }
@@ -147,20 +149,12 @@ namespace Barotrauma.Items.Components
DockingDir = GetDir(DockingTarget);
DockingTarget.DockingDir = -DockingDir;
}
if (joint != null)
{
CreateJoint(joint is WeldJoint);
LinkHullsToGaps();
}
else if (DockingTarget.joint != null)
{
if (!GameMain.World.BodyList.Contains(DockingTarget.joint.BodyA) ||
!GameMain.World.BodyList.Contains(DockingTarget.joint.BodyB))
{
DockingTarget.CreateJoint(DockingTarget.joint is WeldJoint);
}
DockingTarget.LinkHullsToGaps();
}
//undock and redock to recreate the hulls, gaps and physics bodies
var prevDockingTarget = DockingTarget;
Undock(applyEffects: false);
Dock(prevDockingTarget);
Lock(isNetworkMessage: true, applyEffects: false);
}
}
@@ -187,15 +181,15 @@ namespace Barotrauma.Items.Components
private void AttemptDock()
{
var adjacentPort = FindAdjacentPort();
if (adjacentPort != null) Dock(adjacentPort);
if (adjacentPort != null) { Dock(adjacentPort); }
}
public void Dock(DockingPort target)
{
if (item.Submarine.DockedTo.Contains(target.item.Submarine)) return;
if (item.Submarine.DockedTo.Contains(target.item.Submarine)) { return; }
forceLockTimer = 0.0f;
dockingCooldown = 0.1f;
if (DockingTarget != null)
{
@@ -237,7 +231,6 @@ namespace Barotrauma.Items.Components
#if SERVER
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
{
originalDockingTargetID = DockingTarget.item.ID;
item.CreateServerEvent(this);
}
#endif
@@ -246,8 +239,7 @@ namespace Barotrauma.Items.Components
OnDocked = null;
}
public void Lock(bool isNetworkMessage, bool forcePosition = false)
public void Lock(bool isNetworkMessage, bool applyEffects = true)
{
#if CLIENT
if (GameMain.Client != null && !isNetworkMessage) { return; }
@@ -264,7 +256,10 @@ namespace Barotrauma.Items.Components
DockingDir = GetDir(DockingTarget);
DockingTarget.DockingDir = -DockingDir;
ApplyStatusEffects(ActionType.OnUse, 1.0f);
if (applyEffects)
{
ApplyStatusEffects(ActionType.OnUse, 1.0f);
}
Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA;
if (item.Submarine.PhysicsBody.Mass < DockingTarget.item.Submarine.PhysicsBody.Mass ||
@@ -284,7 +279,6 @@ namespace Barotrauma.Items.Components
#if SERVER
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
{
originalDockingTargetID = DockingTarget.item.ID;
item.CreateServerEvent(this);
}
#else
@@ -846,13 +840,17 @@ namespace Barotrauma.Items.Components
}
}
public void Undock()
public void Undock(bool applyEffects = true)
{
if (DockingTarget == null || !docked) return;
if (DockingTarget == null || !docked) { return; }
forceLockTimer = 0.0f;
dockingCooldown = 0.1f;
ApplyStatusEffects(ActionType.OnSecondaryUse, 1.0f);
if (applyEffects)
{
ApplyStatusEffects(ActionType.OnSecondaryUse, 1.0f);
}
DockingTarget.item.Submarine.ConnectedDockingPorts.Remove(item.Submarine);
item.Submarine.ConnectedDockingPorts.Remove(DockingTarget.item.Submarine);
@@ -877,6 +875,7 @@ namespace Barotrauma.Items.Components
Item.Submarine.EnableObstructedWaypoints(DockingTarget.Item.Submarine);
obstructedWayPointsDisabled = false;
Item.Submarine.RefreshOutdoorNodes();
DockingTarget.Undock();
DockingTarget = null;
@@ -924,7 +923,6 @@ namespace Barotrauma.Items.Components
#if SERVER
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
{
originalDockingTargetID = Entity.NullEntityID;
item.CreateServerEvent(this);
}
#endif
@@ -934,14 +932,13 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
dockingCooldown -= deltaTime;
if (DockingTarget == null)
{
dockingState = MathHelper.Lerp(dockingState, 0.0f, deltaTime * 10.0f);
if (dockingState < 0.01f) docked = false;
item.SendSignal(0, "0", "state_out", null);
item.SendSignal(0, (FindAdjacentPort() != null) ? "1" : "0", "proximity_sensor", null);
if (dockingState < 0.01f) { docked = false; }
item.SendSignal("0", "state_out");
item.SendSignal((FindAdjacentPort() != null) ? "1" : "0", "proximity_sensor");
}
else
{
@@ -987,7 +984,7 @@ namespace Barotrauma.Items.Components
}
else
{
Lock(isNetworkMessage: false, forcePosition: true);
Lock(isNetworkMessage: false);
}
}
else
@@ -999,11 +996,12 @@ namespace Barotrauma.Items.Components
dockingState = MathHelper.Lerp(dockingState, 1.0f, deltaTime * 10.0f);
}
item.SendSignal(0, IsLocked ? "1" : "0", "state_out", null);
item.SendSignal(IsLocked ? "1" : "0", "state_out");
}
if (!obstructedWayPointsDisabled && dockingState >= 0.99f)
{
Item.Submarine.DisableObstructedWayPoints(DockingTarget?.Item.Submarine);
Item.Submarine.RefreshOutdoorNodes();
obstructedWayPointsDisabled = true;
}
}
@@ -1104,39 +1102,41 @@ namespace Barotrauma.Items.Components
}
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
public override void ReceiveSignal(Signal signal, Connection connection)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (dockingCooldown > 0.0f) { return; }
bool wasDocked = docked;
DockingPort prevDockingTarget = DockingTarget;
switch (connection.Name)
{
case "toggle":
if (signal != "0")
if (signal.value != "0")
{
Docked = !docked;
}
break;
case "set_active":
case "set_state":
Docked = signal != "0";
Docked = signal.value != "0";
break;
}
#if SERVER
if (sender != null && docked != wasDocked)
if (signal.sender != null && docked != wasDocked)
{
if (docked)
{
if (item.Submarine != null && DockingTarget?.item?.Submarine != null)
GameServer.Log(GameServer.CharacterLogName(sender) + " docked " + item.Submarine.Info.Name + " to " + DockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction);
GameServer.Log(GameServer.CharacterLogName(signal.sender) + " docked " + item.Submarine.Info.Name + " to " + DockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction);
}
else
{
if (item.Submarine != null && prevDockingTarget?.item?.Submarine != null)
GameServer.Log(GameServer.CharacterLogName(sender) + " undocked " + item.Submarine.Info.Name + " from " + prevDockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction);
GameServer.Log(GameServer.CharacterLogName(signal.sender) + " undocked " + item.Submarine.Info.Name + " from " + prevDockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction);
}
}
#endif
@@ -305,9 +305,21 @@ namespace Barotrauma.Items.Components
private void ToggleState(ActionType actionType, Character user)
{
if (toggleCooldownTimer > 0.0f && user != lastUser) { OnFailedToOpen(); return; }
if (toggleCooldownTimer > 0.0f && user != lastUser)
{
OnFailedToOpen();
return;
}
toggleCooldownTimer = ToggleCoolDown;
if (IsStuck || IsJammed) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; }
if (IsStuck || IsJammed)
{
#if CLIENT
if (IsStuck) { HintManager.OnTryOpenStuckDoor(user); }
#endif
toggleCooldownTimer = 1.0f;
OnFailedToOpen();
return;
}
lastUser = user;
SetState(PredictedState == null ? !isOpen : !PredictedState.Value, false, true, forcedOpen: actionType == ActionType.OnPicked);
}
@@ -395,7 +407,7 @@ namespace Barotrauma.Items.Components
//don't use the predicted state here, because it might set
//other items to an incorrect state if the prediction is wrong
item.SendSignal(0, isOpen ? "1" : "0", "state_out", null);
item.SendSignal(isOpen ? "1" : "0", "state_out");
}
partial void UpdateProjSpecific(float deltaTime);
@@ -651,7 +663,7 @@ namespace Barotrauma.Items.Components
}
}
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
public override void ReceiveSignal(Signal signal, Connection connection)
{
if (IsStuck || IsJammed) { return; }
@@ -659,24 +671,24 @@ namespace Barotrauma.Items.Components
if (connection.Name == "toggle")
{
if (signal == "0") { return; }
if (toggleCooldownTimer > 0.0f && sender != lastUser) { OnFailedToOpen(); return; }
if (signal.value == "0") { return; }
if (toggleCooldownTimer > 0.0f && signal.sender != lastUser) { OnFailedToOpen(); return; }
if (IsStuck) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; }
toggleCooldownTimer = ToggleCoolDown;
lastUser = sender;
lastUser = signal.sender;
SetState(!wasOpen, false, true, forcedOpen: false);
}
else if (connection.Name == "set_state")
{
bool signalOpen = signal != "0";
bool signalOpen = signal.value != "0";
if (IsStuck && signalOpen != wasOpen) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; }
SetState(signalOpen, false, true, forcedOpen: false);
}
#if SERVER
if (sender != null && wasOpen != isOpen)
if (signal.sender != null && wasOpen != isOpen)
{
GameServer.Log(GameServer.CharacterLogName(sender) + (isOpen ? " opened " : " closed ") + item.Name, ServerLog.MessageType.ItemInteraction);
GameServer.Log(GameServer.CharacterLogName(signal.sender) + (isOpen ? " opened " : " closed ") + item.Name, ServerLog.MessageType.ItemInteraction);
}
#endif
}
@@ -39,6 +39,12 @@ namespace Barotrauma.Items.Components
get;
private set;
}
[Serialize(true, true, description: "Is the item currently able to push characters around? True by default. Only valid if blocksplayers is set to true.")]
public bool CanPush
{
get;
set;
}
//the angle in which the Character holds the item
protected float holdAngle;
@@ -208,6 +214,7 @@ namespace Barotrauma.Items.Components
if (other.Body.UserData is Character character)
{
if (!IsActive) { return false; }
if (!CanPush) { return false; }
return character != picker;
}
else
@@ -436,6 +443,7 @@ namespace Barotrauma.Items.Components
}
else
{
//not attached -> pick the item instantly, ignoring picking time
return OnPicked(picker);
}
@@ -443,6 +451,10 @@ namespace Barotrauma.Items.Components
public override bool OnPicked(Character picker)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
return false;
}
if (base.OnPicked(picker))
{
DeattachFromWall();
@@ -62,6 +62,7 @@ namespace Barotrauma.Items.Components
{
if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; }
Attack = new Attack(subElement, item.Name + ", MeleeWeapon");
Attack.DamageRange = item.body == null ? 10.0f : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent());
}
item.IsShootable = true;
// TODO: should define this in xml if we have melee weapons that don't require aim to use
@@ -41,25 +41,25 @@ namespace Barotrauma.Items.Components
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || character.Removed) return false;
if (!character.IsKeyDown(InputType.Aim) || character.Stun > 0.0f) return false;
if (!character.IsKeyDown(InputType.Aim) || character.Stun > 0.0f) { return false; }
IsActive = true;
useState = 0.1f;
if (character.AnimController.InWater)
{
if (UsableIn == UseEnvironment.Air) return true;
if (UsableIn == UseEnvironment.Air) { return true; }
}
else
{
if (UsableIn == UseEnvironment.Water) return true;
if (UsableIn == UseEnvironment.Water) { return true; }
}
Vector2 dir = Vector2.Normalize(character.CursorPosition - character.Position);
//move upwards if the cursor is at the position of the character
if (!MathUtils.IsValid(dir)) dir = Vector2.UnitY;
Vector2 propulsion = dir * Force;
Vector2 propulsion = dir * Force * character.PropulsionSpeedMultiplier;
if (character.AnimController.InWater) character.AnimController.TargetMovement = dir;
@@ -12,7 +12,8 @@ namespace Barotrauma.Items.Components
{
partial class RangedWeapon : ItemComponent
{
private float reload, reloadTimer;
private float reload;
public float ReloadTimer { get; private set; }
private Vector2 barrelPos;
@@ -75,17 +76,17 @@ namespace Barotrauma.Items.Components
public override void Equip(Character character)
{
reloadTimer = Math.Min(reload, 1.0f);
ReloadTimer = Math.Min(reload, 1.0f);
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
reloadTimer -= deltaTime;
ReloadTimer -= deltaTime;
if (reloadTimer < 0.0f)
if (ReloadTimer < 0.0f)
{
reloadTimer = 0.0f;
ReloadTimer = 0.0f;
IsActive = false;
}
}
@@ -101,10 +102,10 @@ namespace Barotrauma.Items.Components
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || character.Removed) { return false; }
if ((item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) || reloadTimer > 0.0f) { return false; }
if ((item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) || ReloadTimer > 0.0f) { return false; }
IsActive = true;
reloadTimer = reload;
ReloadTimer = reload;
if (item.AiTarget != null)
{
@@ -796,7 +796,7 @@ namespace Barotrauma.Items.Components
bool leakFixed = (leak.Open <= 0.0f || leak.Removed) &&
(leak.ConnectedWall == null || leak.ConnectedWall.Sections.Average(s => s.damage) < 1);
if (leakFixed && leak.FlowTargetHull?.DisplayName != null)
if (leakFixed && leak.FlowTargetHull?.DisplayName != null && character.IsOnPlayerTeam)
{
if (!leak.FlowTargetHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f))
{
@@ -854,9 +854,11 @@ namespace Barotrauma.Items.Components
object value = property.GetValue(target);
if (door.Stuck > 0)
{
bool isCutting = effect.propertyEffects[i].GetType() == typeof(float) && (float)effect.propertyEffects[i] < 0;
var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White,
effect.propertyEffects[i].GetType() == typeof(float) && (float)effect.propertyEffects[i] < 0 ? "progressbar.cutting" : "progressbar.welding");
textTag: isCutting ? "progressbar.cutting" : "progressbar.welding");
if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); }
if (!isCutting) { HintManager.OnWeldingDoor(user, door); }
}
}
}
@@ -432,27 +432,27 @@ namespace Barotrauma.Items.Components
//called then the item is dropped or dragged out of a "limbslot"
public virtual void Unequip(Character character) { }
public virtual void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
public virtual void ReceiveSignal(Signal signal, Connection connection)
{
switch (connection.Name)
{
case "activate":
case "use":
case "trigger_in":
if (signal != "0")
if (signal.value != "0")
{
item.Use(1.0f, sender);
item.Use(1.0f, signal.sender);
}
break;
case "toggle":
if (signal != "0")
if (signal.value != "0")
{
IsActive = !isActive;
}
break;
case "set_active":
case "set_state":
IsActive = signal != "0";
IsActive = signal.value != "0";
break;
}
}
@@ -771,6 +771,10 @@ namespace Barotrauma.Items.Components
brokenEffects.ForEach(e => e.SetUser(user));
}
}
#if CLIENT
HintManager.OnStatusEffectApplied(this, type, character);
#endif
}
public virtual void Load(XElement componentElement, bool usePrefabValues, IdRemap idRemap)
@@ -952,26 +956,6 @@ namespace Barotrauma.Items.Components
#region AI related
protected const float AIUpdateInterval = 0.2f;
protected float aiUpdateTimer;
private int itemIndex;
private Character previousUser;
protected bool FindSuitableContainer(Character character, Func<Item, float> priority, out Item suitableContainer)
{
suitableContainer = null;
if (character.AIController is HumanAIController aiController)
{
if (previousUser != character)
{
previousUser = character;
itemIndex = 0;
}
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: aiController.IgnoredItems, customPriorityFunction: priority, positionalReference: Item))
{
suitableContainer = targetContainer;
return true;
}
}
return false;
}
protected AIObjectiveContainItem AIContainItems<T>(ItemContainer container, Character character, AIObjective currentObjective, int itemCount, bool equip, bool removeEmpty, bool spawnItemIfNotFound = false, bool dropItemOnDeselected = false) where T : ItemComponent
{
@@ -1014,83 +998,6 @@ namespace Barotrauma.Items.Components
}
return containObjective;
}
/// <summary>
/// Returns true when done seeking the suitable container.
/// </summary>
protected bool AIDecontainEmptyItems(Character character, AIObjective objective, bool equip, ItemContainer sourceContainer = null)
{
if (character.AIController is HumanAIController aiController)
{
ItemContainer sourceC = sourceContainer ?? (item.OwnInventory?.Owner is Item it ? it.GetComponent<ItemContainer>() : null);
var containedItems = sourceContainer != null ? sourceContainer.Inventory.AllItems : item.OwnInventory.AllItems;
foreach (Item containedItem in containedItems)
{
if (containedItem != null && containedItem.Condition <= 0.0f)
{
if (FindSuitableContainer(character,
i =>
{
if (i.IsThisOrAnyContainerIgnoredByAI()) { return 0; }
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
if (!container.Inventory.CanBePut(containedItem)) { return 0; }
// Ignore containers that are identical to the source container
if (sourceC != null && container.Item.Prefab == sourceC.Item.Prefab) { return 0; }
if (container.ShouldBeContained(containedItem, out bool isRestrictionsDefined))
{
if (isRestrictionsDefined)
{
return 10;
}
else
{
if (containedItem.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary))
{
return isPreferencesDefined ? isSecondary ? 2 : 5 : 1;
}
else
{
return isPreferencesDefined ? 0 : 1;
}
}
}
else
{
return 0;
}
}, out Item targetContainer))
{
var decontainObjective = new AIObjectiveDecontainItem(character, containedItem, objective.objectiveManager, sourceC, targetContainer?.GetComponent<ItemContainer>())
{
Equip = equip
};
decontainObjective.Abandoned += () =>
{
itemIndex = 0;
if (targetContainer != null)
{
aiController.IgnoredItems.Add(targetContainer);
}
};
decontainObjective.Completed += () =>
{
if (targetContainer == null)
{
itemIndex = 0;
}
};
objective.AddSubObjectiveInQueue(decontainObjective);
}
else
{
return false;
}
}
}
}
return true;
}
#endregion
}
}

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