v0.14.6.0
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public class CachedDistance
|
||||
{
|
||||
public readonly Vector2 StartWorldPos;
|
||||
public readonly Vector2 EndWorldPos;
|
||||
public readonly float Distance;
|
||||
public double RecalculationTime;
|
||||
|
||||
public CachedDistance(Vector2 startWorldPos, Vector2 endWorldPos, float dist, double recalculationTime)
|
||||
{
|
||||
StartWorldPos = startWorldPos;
|
||||
EndWorldPos = endWorldPos;
|
||||
Distance = dist;
|
||||
RecalculationTime = recalculationTime;
|
||||
}
|
||||
|
||||
public bool ShouldUpdateDistance(Vector2 currentStartWorldPos, Vector2 currentEndWorldPos, float minDistanceToUpdate = 500.0f)
|
||||
{
|
||||
if (Timing.TotalTime < RecalculationTime) { return false; }
|
||||
float minDistSquared = minDistanceToUpdate * minDistanceToUpdate;
|
||||
return Vector2.DistanceSquared(StartWorldPos, currentStartWorldPos) > minDistSquared ||
|
||||
Vector2.DistanceSquared(EndWorldPos, currentEndWorldPos) > minDistSquared;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ namespace Barotrauma
|
||||
|
||||
private IEnumerable<object> Update(ISpatialEntity targetEntity, Camera cam)
|
||||
{
|
||||
if (targetEntity == null) { yield return CoroutineStatus.Success; }
|
||||
if (targetEntity == null || (targetEntity is Entity e && e.Removed)) { yield return CoroutineStatus.Success; }
|
||||
|
||||
prevControlled = Character.Controlled;
|
||||
if (RemoveControlFromCharacter)
|
||||
|
||||
@@ -73,6 +73,8 @@ namespace Barotrauma
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public virtual bool IsMentallyUnstable => false;
|
||||
|
||||
private IEnumerable<Hull> visibleHulls;
|
||||
private float hullVisibilityTimer;
|
||||
const float hullVisibilityInterval = 0.5f;
|
||||
@@ -215,10 +217,26 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
private readonly HashSet<Item> unequippedItems = new HashSet<Item>();
|
||||
public bool TakeItem(Item item, Inventory targetInventory, bool equip, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false)
|
||||
public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false)
|
||||
{
|
||||
var pickable = item.GetComponent<Pickable>();
|
||||
if (pickable == null) { return false; }
|
||||
if (wear)
|
||||
{
|
||||
var wearable = item.GetComponent<Wearable>();
|
||||
if (wearable != null)
|
||||
{
|
||||
pickable = wearable;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var holdable = item.GetComponent<Holdable>();
|
||||
if (holdable != null)
|
||||
{
|
||||
pickable = holdable;
|
||||
}
|
||||
}
|
||||
if (item.ParentInventory is ItemInventory itemInventory)
|
||||
{
|
||||
if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; }
|
||||
@@ -302,7 +320,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (item != null && !item.Removed && Character.HasItem(item))
|
||||
{
|
||||
TakeItem(item, Character.Inventory, equip: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false);
|
||||
TakeItem(item, Character.Inventory, equip: true, wear: true, dropOtherIfCannotMove: true, allowSwapping: true, storeUnequipped: false);
|
||||
}
|
||||
}
|
||||
unequippedItems.Clear();
|
||||
|
||||
@@ -144,21 +144,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
if (Static)
|
||||
{
|
||||
SightRange = MaxSightRange;
|
||||
SoundRange = MaxSoundRange;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-static ai targets must be kept alive by a custom logic (e.g. item components)
|
||||
SightRange = StaticSight ? MaxSightRange : MinSightRange;
|
||||
SoundRange = StaticSound ? MaxSoundRange : MinSoundRange;
|
||||
}
|
||||
}
|
||||
|
||||
public AITarget(Entity e, XElement element) : this(e)
|
||||
{
|
||||
SightRange = element.GetAttributeFloat("sightrange", 0.0f);
|
||||
@@ -242,5 +227,20 @@ namespace Barotrauma
|
||||
List.Remove(this);
|
||||
entity = null;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
if (Static)
|
||||
{
|
||||
SightRange = MaxSightRange;
|
||||
SoundRange = MaxSoundRange;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-static ai targets must be kept alive by a custom logic (e.g. item components)
|
||||
SightRange = StaticSight ? MaxSightRange : MinSightRange;
|
||||
SoundRange = StaticSound ? MaxSoundRange : MinSoundRange;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,13 @@ namespace Barotrauma
|
||||
|
||||
public enum CirclePhase { Start, CloseIn, FallBack, Advance, Strike }
|
||||
|
||||
public enum WallTargetingMethod
|
||||
{
|
||||
Target = 0x1,
|
||||
Heading = 0x2,
|
||||
Steering = 0x4
|
||||
}
|
||||
|
||||
partial class EnemyAIController : AIController
|
||||
{
|
||||
public static bool DisableEnemyAI;
|
||||
@@ -54,23 +61,27 @@ namespace Barotrauma
|
||||
private float attackLimbResetTimer;
|
||||
|
||||
private bool IsAttackRunning => AttackingLimb != null && AttackingLimb.attack.IsRunning;
|
||||
private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0;
|
||||
private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0 || _previousAttackingLimb != null && _previousAttackingLimb.attack.CoolDownTimer > 0;
|
||||
public float CombatStrength => AIParams.CombatStrength;
|
||||
private float Sight => AIParams.Sight;
|
||||
private float Hearing => AIParams.Hearing;
|
||||
private float FleeHealthThreshold => AIParams.FleeHealthThreshold;
|
||||
private bool AggressiveBoarding => AIParams.AggressiveBoarding;
|
||||
private bool IsAggressiveBoarder => AIParams.AggressiveBoarding;
|
||||
|
||||
private FishAnimController FishAnimController => Character.AnimController as FishAnimController;
|
||||
|
||||
//the limb selected for the current attack
|
||||
private Limb _attackingLimb;
|
||||
private Limb _previousAttackingLimb;
|
||||
public Limb AttackingLimb
|
||||
{
|
||||
get { return _attackingLimb; }
|
||||
private set
|
||||
{
|
||||
attackLimbResetTimer = 0;
|
||||
if (_attackingLimb != value)
|
||||
{
|
||||
_previousAttackingLimb = _attackingLimb;
|
||||
}
|
||||
_attackingLimb = value;
|
||||
attackVector = null;
|
||||
Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse;
|
||||
@@ -342,6 +353,10 @@ namespace Barotrauma
|
||||
{
|
||||
targetingTag = "weaker";
|
||||
}
|
||||
else
|
||||
{
|
||||
targetingTag = "equal";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,6 +396,7 @@ namespace Barotrauma
|
||||
SelectedAiTarget = target;
|
||||
selectedTargetMemory = GetTargetMemory(target, true);
|
||||
selectedTargetMemory.Priority = priority;
|
||||
ignoredTargets.Remove(target);
|
||||
}
|
||||
|
||||
private float movementMargin;
|
||||
@@ -479,10 +495,6 @@ namespace Barotrauma
|
||||
{
|
||||
CharacterParams.TargetParams targetingParams = null;
|
||||
UpdateTargets(Character, out targetingParams);
|
||||
if (!IsLatchedOnSub)
|
||||
{
|
||||
UpdateWallTarget(requiredHoleCount);
|
||||
}
|
||||
updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f);
|
||||
if (SelectedAiTarget == null)
|
||||
{
|
||||
@@ -493,10 +505,14 @@ namespace Barotrauma
|
||||
selectedTargetingParams = targetingParams;
|
||||
State = targetingParams.State;
|
||||
}
|
||||
if (SelectedAiTarget?.Entity != null && !IsLatchedOnSub && State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive)
|
||||
{
|
||||
UpdateWallTarget(requiredHoleCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (AIParams.Infiltrate)
|
||||
if (AIParams.CanOpenDoors)
|
||||
{
|
||||
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
|
||||
|
||||
@@ -787,7 +803,7 @@ namespace Barotrauma
|
||||
if (pathSteering != null && !Character.AnimController.InWater)
|
||||
{
|
||||
// Wander around inside
|
||||
pathSteering.Wander(deltaTime, ConvertUnits.ToDisplayUnits(colliderLength), stayStillInTightSpace: false);
|
||||
pathSteering.Wander(deltaTime, Math.Max(ConvertUnits.ToDisplayUnits(colliderLength), 100.0f), stayStillInTightSpace: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1003,12 +1019,8 @@ namespace Barotrauma
|
||||
if (!w.SectionBodyDisabled(i))
|
||||
{
|
||||
isBroken = false;
|
||||
Vector2 sectionPos = w.SectionPosition(i);
|
||||
Vector2 sectionPos = w.SectionPosition(i, world: true);
|
||||
attackWorldPos = sectionPos;
|
||||
if (w.Submarine != null)
|
||||
{
|
||||
attackWorldPos += w.Submarine.Position;
|
||||
}
|
||||
attackSimPos = ConvertUnits.ToSimUnits(attackWorldPos);
|
||||
break;
|
||||
}
|
||||
@@ -1026,18 +1038,19 @@ namespace Barotrauma
|
||||
bool pursue = false;
|
||||
if (IsCoolDownRunning)
|
||||
{
|
||||
if (AttackingLimb.attack.CoolDownTimer >= AttackingLimb.attack.CoolDown + AttackingLimb.attack.CurrentRandomCoolDown - AttackingLimb.attack.AfterAttackDelay)
|
||||
var currentAttackLimb = AttackingLimb ?? _previousAttackingLimb;
|
||||
if (currentAttackLimb.attack.CoolDownTimer >= currentAttackLimb.attack.CoolDown + currentAttackLimb.attack.CurrentRandomCoolDown - currentAttackLimb.attack.AfterAttackDelay)
|
||||
{
|
||||
return;
|
||||
}
|
||||
switch (AttackingLimb.attack.AfterAttack)
|
||||
switch (currentAttackLimb.attack.AfterAttack)
|
||||
{
|
||||
case AIBehaviorAfterAttack.Pursue:
|
||||
case AIBehaviorAfterAttack.PursueIfCanAttack:
|
||||
if (AttackingLimb.attack.SecondaryCoolDown <= 0)
|
||||
if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
|
||||
{
|
||||
// No (valid) secondary cooldown defined.
|
||||
if (AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue)
|
||||
if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue)
|
||||
{
|
||||
canAttack = false;
|
||||
pursue = true;
|
||||
@@ -1050,13 +1063,13 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (AttackingLimb.attack.SecondaryCoolDownTimer <= 0)
|
||||
if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
|
||||
{
|
||||
// Don't allow attacking when the attack target has just changed.
|
||||
if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget)
|
||||
{
|
||||
canAttack = false;
|
||||
if (AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.PursueIfCanAttack)
|
||||
if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.PursueIfCanAttack)
|
||||
{
|
||||
// Fall back if cannot attack.
|
||||
UpdateFallBack(attackWorldPos, deltaTime, true);
|
||||
@@ -1067,7 +1080,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
// If the secondary cooldown is defined and expired, check if we can switch the attack
|
||||
var newLimb = GetAttackLimb(attackWorldPos, AttackingLimb);
|
||||
var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
|
||||
if (newLimb != null)
|
||||
{
|
||||
// Attack with the new limb
|
||||
@@ -1076,7 +1089,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
// No new limb was found.
|
||||
if (AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue)
|
||||
if (currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue)
|
||||
{
|
||||
canAttack = false;
|
||||
pursue = true;
|
||||
@@ -1098,26 +1111,26 @@ namespace Barotrauma
|
||||
break;
|
||||
case AIBehaviorAfterAttack.FallBackUntilCanAttack:
|
||||
case AIBehaviorAfterAttack.FollowThroughUntilCanAttack:
|
||||
if (AttackingLimb.attack.SecondaryCoolDown <= 0)
|
||||
if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
|
||||
{
|
||||
// No (valid) secondary cooldown defined.
|
||||
UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
|
||||
UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (AttackingLimb.attack.SecondaryCoolDownTimer <= 0)
|
||||
if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
|
||||
{
|
||||
// Don't allow attacking when the attack target has just changed.
|
||||
if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget)
|
||||
{
|
||||
UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
|
||||
UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the secondary cooldown is defined and expired, check if we can switch the attack
|
||||
var newLimb = GetAttackLimb(attackWorldPos, AttackingLimb);
|
||||
var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
|
||||
if (newLimb != null)
|
||||
{
|
||||
// Attack with the new limb
|
||||
@@ -1126,7 +1139,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
// No new limb was found.
|
||||
UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
|
||||
UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1134,13 +1147,13 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
// Cooldown not yet expired -> steer away from the target
|
||||
UpdateFallBack(attackWorldPos, deltaTime, AttackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
|
||||
UpdateFallBack(attackWorldPos, deltaTime, currentAttackLimb.attack.AfterAttack == AIBehaviorAfterAttack.FollowThroughUntilCanAttack);
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case AIBehaviorAfterAttack.IdleUntilCanAttack:
|
||||
if (AttackingLimb.attack.SecondaryCoolDown <= 0)
|
||||
if (currentAttackLimb.attack.SecondaryCoolDown <= 0)
|
||||
{
|
||||
// No (valid) secondary cooldown defined.
|
||||
UpdateIdle(deltaTime, followLastTarget: false);
|
||||
@@ -1148,7 +1161,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (AttackingLimb.attack.SecondaryCoolDownTimer <= 0)
|
||||
if (currentAttackLimb.attack.SecondaryCoolDownTimer <= 0)
|
||||
{
|
||||
// Don't allow attacking when the attack target has just changed.
|
||||
if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget)
|
||||
@@ -1159,7 +1172,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
// If the secondary cooldown is defined and expired, check if we can switch the attack
|
||||
var newLimb = GetAttackLimb(attackWorldPos, AttackingLimb);
|
||||
var newLimb = GetAttackLimb(attackWorldPos, currentAttackLimb);
|
||||
if (newLimb != null)
|
||||
{
|
||||
// Attack with the new limb
|
||||
@@ -1203,7 +1216,7 @@ namespace Barotrauma
|
||||
}
|
||||
canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0;
|
||||
}
|
||||
if (!AIParams.Infiltrate)
|
||||
if (!AIParams.CanOpenDoors)
|
||||
{
|
||||
if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls))
|
||||
{
|
||||
@@ -1257,8 +1270,8 @@ namespace Barotrauma
|
||||
|
||||
Vector2 attackLimbPos = Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackingLimb.WorldPosition;
|
||||
Vector2 toTarget = attackWorldPos - attackLimbPos;
|
||||
// Add a margin when the target is moving away, because otherwise it might be difficult to reach it (the attack takes some time to perform)
|
||||
if (wallTarget != null)
|
||||
// Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute
|
||||
if (wallTarget != null && Character.Submarine == null)
|
||||
{
|
||||
if (wallTarget.Structure.Submarine != null)
|
||||
{
|
||||
@@ -1282,9 +1295,14 @@ namespace Barotrauma
|
||||
|
||||
Vector2 CalculateMargin(Vector2 targetVelocity)
|
||||
{
|
||||
if (targetVelocity == Vector2.Zero) { return targetVelocity; }
|
||||
if (targetVelocity == Vector2.Zero) { return Vector2.Zero; }
|
||||
float diff = AttackingLimb.attack.Range - AttackingLimb.attack.DamageRange;
|
||||
if (diff <= 0 || toTarget.LengthSquared() <= MathUtils.Pow2(AttackingLimb.attack.DamageRange)) { return Vector2.Zero; }
|
||||
float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity));
|
||||
return ConvertUnits.ToDisplayUnits(targetVelocity) * AttackingLimb.attack.Duration * dot;
|
||||
if (dot <= 0 || !MathUtils.IsValid(dot)) { return Vector2.Zero; }
|
||||
float distanceOffset = diff * AttackingLimb.attack.Duration;
|
||||
// Intentionally omit the unit conversion because we use distanceOffset as a multiplier.
|
||||
return targetVelocity * distanceOffset * dot;
|
||||
}
|
||||
|
||||
// Check that we can reach the target
|
||||
@@ -1422,10 +1440,11 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
pathSteering.SteeringSeek(steerPos, 2, startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null), checkVisiblity: true);
|
||||
// Switch to Idle when cannot reach the target and if cannot damage the walls
|
||||
if ((!canAttackWalls || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
|
||||
if (!pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
|
||||
{
|
||||
State = AIState.Idle;
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
ResetAITarget();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1648,7 +1667,7 @@ namespace Barotrauma
|
||||
float GetTargetMaxSpeed() => Character.ApplyTemporarySpeedLimits(Character.AnimController.CurrentSwimParams.MovementSpeed * 0.3f);
|
||||
}
|
||||
SteeringManager.SteeringSeek(steerPos, 10);
|
||||
if (SelectedAiTarget?.Entity is Character || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2))
|
||||
if (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2))
|
||||
{
|
||||
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30);
|
||||
}
|
||||
@@ -1656,16 +1675,6 @@ namespace Barotrauma
|
||||
}
|
||||
if (canAttack)
|
||||
{
|
||||
if (SelectedAiTarget.Entity is Item targetItem)
|
||||
{
|
||||
var door = targetItem.GetComponent<Door>();
|
||||
if (door != null && door.CanBeTraversed)
|
||||
{
|
||||
ResetAITarget();
|
||||
State = PreviousState;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!UpdateLimbAttack(deltaTime, AttackingLimb, attackSimPos, distance, attackTargetLimb))
|
||||
{
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
@@ -1777,6 +1786,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (!isFriendly && attackResult.Damage > 0.0f)
|
||||
{
|
||||
ignoredTargets.Remove(attacker.AiTarget);
|
||||
bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackWalls;
|
||||
if (AIParams.AttackWhenProvoked && canAttack)
|
||||
{
|
||||
@@ -1811,9 +1821,8 @@ namespace Barotrauma
|
||||
ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100);
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (!AIParams.HasTag("equal"))
|
||||
{
|
||||
// Equal strength
|
||||
ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100);
|
||||
}
|
||||
}
|
||||
@@ -1893,16 +1902,28 @@ namespace Barotrauma
|
||||
{
|
||||
//simulate attack input to get the character to attack client-side
|
||||
Character.SetInput(InputType.Attack, true, true);
|
||||
#if SERVER
|
||||
GameMain.NetworkMember.CreateEntityEvent(Character, new object[]
|
||||
{
|
||||
Networking.NetEntityEvent.Type.SetAttackTarget,
|
||||
attackingLimb,
|
||||
(damageTarget as Entity)?.ID ?? Entity.NullEntityID,
|
||||
damageTarget is Character character && targetLimb != null ? Array.IndexOf(character.AnimController.Limbs, targetLimb) : 0,
|
||||
SimPosition.X,
|
||||
SimPosition.Y
|
||||
});
|
||||
#endif
|
||||
if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
|
||||
{
|
||||
if (damageTarget.Health > 0)
|
||||
if (damageTarget.Health > 0 && attackResult.Damage > 0)
|
||||
{
|
||||
// Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
|
||||
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * AIParams.AggressionGreed;
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedTargetMemory.Priority = 0;
|
||||
selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1);
|
||||
return selectedTargetMemory.Priority > 1;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -2138,6 +2159,10 @@ namespace Barotrauma
|
||||
{
|
||||
targetingTag = "weaker";
|
||||
}
|
||||
else
|
||||
{
|
||||
targetingTag = "equal";
|
||||
}
|
||||
if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee))
|
||||
{
|
||||
if (SelectedAiTarget == aiTarget)
|
||||
@@ -2184,7 +2209,7 @@ namespace Barotrauma
|
||||
bool targetingFromOutsideToInside = item.CurrentHull != null && character.CurrentHull == null;
|
||||
if (targetingFromOutsideToInside)
|
||||
{
|
||||
if (door != null && (!canAttackDoors && !AIParams.Infiltrate) || !canAttackWalls)
|
||||
if (door != null && (!canAttackDoors && !AIParams.CanOpenDoors) || !canAttackWalls)
|
||||
{
|
||||
// Can't reach
|
||||
continue;
|
||||
@@ -2258,25 +2283,24 @@ namespace Barotrauma
|
||||
var section = s.Sections[i];
|
||||
if (section.gap == null) { continue; }
|
||||
bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null;
|
||||
isInnerWall = isInnerWall || !leadsInside;
|
||||
if (Character.AnimController.CanEnterSubmarine)
|
||||
{
|
||||
if (!isCharacterInside)
|
||||
{
|
||||
if (CanPassThroughHole(s, i))
|
||||
{
|
||||
valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : 0;
|
||||
valueModifier *= leadsInside ? (IsAggressiveBoarder ? 3 : 1) : 0;
|
||||
}
|
||||
else if (AggressiveBoarding && leadsInside && canAttackWalls && AIParams.TargetOuterWalls)
|
||||
else if (IsAggressiveBoarder && leadsInside && canAttackWalls && AIParams.TargetOuterWalls)
|
||||
{
|
||||
// Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside
|
||||
valueModifier *= 1 + section.gap.Open;
|
||||
// Up to 25% priority increase for every gap in the wall when an aggressive boarder is outside
|
||||
valueModifier *= 1 + section.gap.Open * 0.25f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Inside
|
||||
if (AggressiveBoarding)
|
||||
if (IsAggressiveBoarder)
|
||||
{
|
||||
if (!isInnerWall)
|
||||
{
|
||||
@@ -2293,6 +2317,10 @@ namespace Barotrauma
|
||||
valueModifier = 0;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
valueModifier = 0.1f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2316,7 +2344,7 @@ namespace Barotrauma
|
||||
valueModifier = 0;
|
||||
break;
|
||||
}
|
||||
else if (AggressiveBoarding)
|
||||
else if (IsAggressiveBoarder)
|
||||
{
|
||||
// Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside
|
||||
// (Bonethreshers)
|
||||
@@ -2350,17 +2378,24 @@ namespace Barotrauma
|
||||
// Ignore broken and open doors, if cannot enter submarine
|
||||
continue;
|
||||
}
|
||||
if (AggressiveBoarding)
|
||||
if (IsAggressiveBoarder)
|
||||
{
|
||||
// Increase the priority if the character is outside and the door is from outside to inside
|
||||
if (character.CurrentHull == null)
|
||||
{
|
||||
valueModifier *= isOpen ? 5 : 1;
|
||||
// Increase the priority if the character is outside and the door is from outside to inside
|
||||
if (door.CanBeTraversed)
|
||||
{
|
||||
valueModifier = 3;
|
||||
}
|
||||
else if (door.LinkedGap != null)
|
||||
{
|
||||
valueModifier = 1 + door.LinkedGap.Open;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Inside -> ignore open doors and outer doors
|
||||
valueModifier *= isOpen || isOutdoor ? 0 : 1;
|
||||
valueModifier = isOpen || isOutdoor ? 0 : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2605,91 +2640,165 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
private WallTarget wallTarget;
|
||||
|
||||
private readonly List<(Body, int, Vector2)> wallHits = new List<(Body, int, Vector2)>(3);
|
||||
private void UpdateWallTarget(int requiredHoleCount)
|
||||
{
|
||||
wallTarget = null;
|
||||
if (State == AIState.Flee || State == AIState.Escape) { return; }
|
||||
if (AIParams.Infiltrate && HasValidPath(requireNonDirty: true)) { return; }
|
||||
if (SelectedAiTarget == null) { return; }
|
||||
if (SelectedAiTarget.Entity == null) { return; }
|
||||
Vector2 rayStart = SimPosition;
|
||||
Vector2 rayEnd = SelectedAiTarget.SimPosition;
|
||||
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
|
||||
if (SelectedAiTarget.Entity == null) { return; }
|
||||
if (HasValidPath(requireNonDirty: true)) { return; }
|
||||
wallHits.Clear();
|
||||
Structure wall = null;
|
||||
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Target))
|
||||
{
|
||||
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
|
||||
}
|
||||
else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null)
|
||||
{
|
||||
rayEnd -= Character.Submarine.SimPosition;
|
||||
}
|
||||
Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine);
|
||||
if (Submarine.LastPickedFraction != 1.0f && closestBody != null)
|
||||
{
|
||||
if (closestBody.UserData is Structure wall && wall.Submarine != null && (Character.IsBot || wall.Submarine.Info.IsPlayer || wall.Submarine.Info.IsOutpost && TargetOutposts))
|
||||
Vector2 rayStart = SimPosition;
|
||||
Vector2 rayEnd = SelectedAiTarget.SimPosition;
|
||||
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
|
||||
{
|
||||
int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition));
|
||||
float sectionDamage = wall.SectionDamage(sectionIndex);
|
||||
for (int i = sectionIndex - 2; i <= sectionIndex + 2; i++)
|
||||
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
|
||||
}
|
||||
else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null)
|
||||
{
|
||||
rayEnd -= Character.Submarine.SimPosition;
|
||||
}
|
||||
DoRayCast(rayStart, rayEnd);
|
||||
}
|
||||
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Heading))
|
||||
{
|
||||
Vector2 rayStart = SimPosition;
|
||||
Vector2 rayEnd = rayStart + VectorExtensions.Forward(Character.AnimController.Collider.Rotation + MathHelper.PiOver2, avoidLookAheadDistance * 5);
|
||||
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
|
||||
{
|
||||
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
|
||||
rayEnd -= SelectedAiTarget.Entity.Submarine.SimPosition;
|
||||
}
|
||||
else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null)
|
||||
{
|
||||
rayStart -= Character.Submarine.SimPosition;
|
||||
rayEnd -= Character.Submarine.SimPosition;
|
||||
}
|
||||
DoRayCast(rayStart, rayEnd);
|
||||
}
|
||||
if (AIParams.WallTargetingMethod.HasFlag(WallTargetingMethod.Steering))
|
||||
{
|
||||
Vector2 rayStart = SimPosition;
|
||||
Vector2 rayEnd = rayStart + Steering * 5;
|
||||
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
|
||||
{
|
||||
rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition;
|
||||
rayEnd -= SelectedAiTarget.Entity.Submarine.SimPosition;
|
||||
}
|
||||
else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null)
|
||||
{
|
||||
rayStart -= Character.Submarine.SimPosition;
|
||||
rayEnd -= Character.Submarine.SimPosition;
|
||||
}
|
||||
DoRayCast(rayStart, rayEnd);
|
||||
}
|
||||
if (wallHits.Any())
|
||||
{
|
||||
Body closestBody = null;
|
||||
float closestDistance = 0;
|
||||
int sectionIndex = -1;
|
||||
Vector2 sectionPos = Vector2.Zero;
|
||||
foreach ((Body body, int index, Vector2 sectionPosition) in wallHits)
|
||||
{
|
||||
float distance = Vector2.DistanceSquared(SimPosition, sectionPosition);
|
||||
if (closestBody == null || closestDistance == 0 || distance < closestDistance)
|
||||
{
|
||||
if (wall.SectionBodyDisabled(i))
|
||||
{
|
||||
if (Character.AnimController.CanEnterSubmarine && CanPassThroughHole(wall, i, requiredHoleCount))
|
||||
{
|
||||
sectionIndex = i;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ignore and keep breaking other sections
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (wall.SectionDamage(i) > sectionDamage)
|
||||
{
|
||||
sectionIndex = i;
|
||||
}
|
||||
closestBody = body;
|
||||
closestDistance = distance;
|
||||
wall = closestBody.UserData as Structure;
|
||||
sectionPos = sectionPosition;
|
||||
sectionIndex = index;
|
||||
}
|
||||
Vector2 sectionPos = wall.SectionPosition(sectionIndex);
|
||||
Vector2 attachTargetNormal;
|
||||
if (wall.IsHorizontal)
|
||||
}
|
||||
if (closestBody == null || sectionIndex == -1) { return; }
|
||||
Vector2 attachTargetNormal;
|
||||
if (wall.IsHorizontal)
|
||||
{
|
||||
attachTargetNormal = new Vector2(0.0f, Math.Sign(WorldPosition.Y - wall.WorldPosition.Y));
|
||||
sectionPos.Y += (wall.BodyHeight <= 0.0f ? wall.Rect.Height : wall.BodyHeight) / 2 * attachTargetNormal.Y;
|
||||
}
|
||||
else
|
||||
{
|
||||
attachTargetNormal = new Vector2(Math.Sign(WorldPosition.X - wall.WorldPosition.X), 0.0f);
|
||||
sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X;
|
||||
}
|
||||
LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
|
||||
if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
|
||||
{
|
||||
if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine)
|
||||
{
|
||||
attachTargetNormal = new Vector2(0.0f, Math.Sign(WorldPosition.Y - wall.WorldPosition.Y));
|
||||
sectionPos.Y += (wall.BodyHeight <= 0.0f ? wall.Rect.Height : wall.BodyHeight) / 2 * attachTargetNormal.Y;
|
||||
bool isTargetingDoor = SelectedAiTarget.Entity is Item i && i.GetComponent<Door>() != null;
|
||||
// Blocked by a wall that shouldn't be targeted. The main intention here is to prevent monsters from entering the the tail and the nose pieces.
|
||||
if (!isTargetingDoor)
|
||||
{
|
||||
ResetAITarget();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
attachTargetNormal = new Vector2(Math.Sign(WorldPosition.X - wall.WorldPosition.X), 0.0f);
|
||||
sectionPos.X += (wall.BodyWidth <= 0.0f ? wall.Rect.Width : wall.BodyWidth) / 2 * attachTargetNormal.X;
|
||||
wallTarget = new WallTarget(sectionPos, wall, sectionIndex);
|
||||
}
|
||||
LatchOntoAI?.SetAttachTarget(wall, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal);
|
||||
if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall))
|
||||
}
|
||||
else
|
||||
{
|
||||
// Blocked by a disabled wall.
|
||||
ResetAITarget();
|
||||
}
|
||||
}
|
||||
|
||||
void DoRayCast(Vector2 rayStart, Vector2 rayEnd)
|
||||
{
|
||||
Body hitTarget = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine);
|
||||
if (hitTarget != null && IsValid(hitTarget, out wall))
|
||||
{
|
||||
int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition));
|
||||
if (sectionIndex >= 0)
|
||||
{
|
||||
if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner") || wall.Submarine != null && wall.Submarine == Character.Submarine)
|
||||
wallHits.Add((hitTarget, sectionIndex, GetSectionPosition(wall, sectionIndex)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 GetSectionPosition(Structure wall, int sectionIndex)
|
||||
{
|
||||
float sectionDamage = wall.SectionDamage(sectionIndex);
|
||||
for (int i = sectionIndex - 2; i <= sectionIndex + 2; i++)
|
||||
{
|
||||
if (wall.SectionBodyDisabled(i))
|
||||
{
|
||||
if (Character.AnimController.CanEnterSubmarine && CanPassThroughHole(wall, i, requiredHoleCount))
|
||||
{
|
||||
if (wall.NoAITarget && Character.AnimController.CanEnterSubmarine)
|
||||
{
|
||||
// Blocked by a wall that shouldn't be targeted. The main intention here is to prevents monsters from entering the the tail and the nose pieces.
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
ResetAITarget();
|
||||
}
|
||||
else
|
||||
{
|
||||
wallTarget = new WallTarget(sectionPos, wall, sectionIndex);
|
||||
}
|
||||
sectionIndex = i;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ignore and keep breaking other sections
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Character.AnimController.CanEnterSubmarine && wallTarget == null && selectedTargetingParams?.AttackPattern == AttackPattern.Straight)
|
||||
{
|
||||
if (closestBody.UserData is Structure w && w.Submarine != null && w.Submarine == SelectedAiTarget.Entity?.Submarine ||
|
||||
closestBody.UserData is Item i && i.Submarine != null && i.Submarine == SelectedAiTarget.Entity?.Submarine)
|
||||
if (wall.SectionDamage(i) > sectionDamage)
|
||||
{
|
||||
// Cannot reach the target, because it's blocked by a disabled wall or a door
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
ResetAITarget();
|
||||
sectionIndex = i;
|
||||
}
|
||||
}
|
||||
return wall.SectionPosition(sectionIndex, world: false);
|
||||
}
|
||||
|
||||
bool IsValid(Body hit, out Structure wall)
|
||||
{
|
||||
wall = null;
|
||||
if (Submarine.LastPickedFraction == 1.0f) { return false; }
|
||||
if (!(hit.UserData is Structure w)) { return false; }
|
||||
if (w.Submarine == null) { return false; }
|
||||
if (w.Submarine != SelectedAiTarget.Entity.Submarine) { return false; }
|
||||
if (Character.Submarine == null && w.prefab.Tags.Contains("inner")) { return false; }
|
||||
if (!AIParams.TargetOuterWalls && !w.prefab.Tags.Contains("inner")) { return false; }
|
||||
wall = w;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2698,7 +2807,7 @@ namespace Barotrauma
|
||||
if (wallTarget != null && wallTarget.SectionIndex > -1 && CanPassThroughHole(wallTarget.Structure, wallTarget.SectionIndex, requiredHoleCount))
|
||||
{
|
||||
WallSection section = wallTarget.Structure.GetSection(wallTarget.SectionIndex);
|
||||
Vector2 targetPos = wallTarget.Structure.SectionPosition(wallTarget.SectionIndex, true);
|
||||
Vector2 targetPos = wallTarget.Structure.SectionPosition(wallTarget.SectionIndex, world: true);
|
||||
return section?.gap != null && SteerThroughGap(wallTarget.Structure, section, targetPos, deltaTime);
|
||||
}
|
||||
else if (SelectedAiTarget != null)
|
||||
|
||||
@@ -29,6 +29,9 @@ namespace Barotrauma
|
||||
private float flipTimer;
|
||||
private const float FlipInterval = 0.5f;
|
||||
|
||||
private float teamChangeTimer;
|
||||
private const float TeamChangeInterval = 0.5f;
|
||||
|
||||
public const float HULL_SAFETY_THRESHOLD = 40;
|
||||
public const float HULL_LOW_OXYGEN_PERCENTAGE = 30;
|
||||
|
||||
@@ -121,6 +124,33 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public MentalStateManager MentalStateManager { get; private set; }
|
||||
|
||||
public void InitMentalStateManager()
|
||||
{
|
||||
if (MentalStateManager == null)
|
||||
{
|
||||
MentalStateManager = new MentalStateManager(Character, this);
|
||||
}
|
||||
MentalStateManager.Active = true;
|
||||
}
|
||||
|
||||
public override bool IsMentallyUnstable =>
|
||||
MentalStateManager == null ? false :
|
||||
MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Normal &&
|
||||
MentalStateManager.CurrentMentalType != MentalStateManager.MentalType.Confused;
|
||||
|
||||
public ShipCommandManager ShipCommandManager { get; private set; }
|
||||
|
||||
public void InitShipCommandManager()
|
||||
{
|
||||
if (ShipCommandManager == null)
|
||||
{
|
||||
ShipCommandManager = new ShipCommandManager(Character);
|
||||
}
|
||||
ShipCommandManager.Active = true;
|
||||
}
|
||||
|
||||
public HumanAIController(Character c) : base(c)
|
||||
{
|
||||
if (!c.IsHuman)
|
||||
@@ -204,9 +234,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID))
|
||||
|
||||
if (Character.Submarine == null || !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted)
|
||||
{
|
||||
// Spot enemies while staying outside or inside an enemy ship.
|
||||
// does not apply for escorted characters, such as prisoners or terrorists who have their own behavior
|
||||
enemycheckTimer -= deltaTime;
|
||||
if (enemycheckTimer < 0)
|
||||
{
|
||||
@@ -287,6 +319,8 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
Character.UpdateTeam();
|
||||
|
||||
if (Character.CurrentHull != null)
|
||||
{
|
||||
if (Character.IsOnPlayerTeam)
|
||||
@@ -301,7 +335,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (Character.SpeechImpediment < 100.0f)
|
||||
{
|
||||
if (Character.Submarine != null && Character.Submarine.TeamID == Character.TeamID && !Character.Submarine.Info.IsWreck)
|
||||
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
|
||||
{
|
||||
ReportProblems();
|
||||
}
|
||||
@@ -314,7 +348,7 @@ namespace Barotrauma
|
||||
if (objectiveManager.CurrentObjective == null) { return; }
|
||||
|
||||
objectiveManager.DoCurrentObjective(deltaTime);
|
||||
bool run = objectiveManager.CurrentObjective.ForceRun || objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority;
|
||||
bool run = objectiveManager.CurrentObjective.ForceRun || !objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority;
|
||||
if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null)
|
||||
{
|
||||
if (Character.CurrentHull == null)
|
||||
@@ -395,6 +429,9 @@ namespace Barotrauma
|
||||
flipTimer = FlipInterval;
|
||||
}
|
||||
}
|
||||
|
||||
MentalStateManager?.Update(deltaTime);
|
||||
ShipCommandManager?.Update(deltaTime);
|
||||
}
|
||||
|
||||
private void UnequipUnnecessaryItems()
|
||||
@@ -442,9 +479,8 @@ namespace Barotrauma
|
||||
Character.AnimController.InWater ||
|
||||
Character.AnimController.HeadInWater ||
|
||||
Character.CurrentHull == null ||
|
||||
Character.Submarine?.TeamID != Character.TeamID ||
|
||||
(Character.Submarine?.TeamID != Character.TeamID && !Character.IsEscorted) || // these instances should maybe be combined to a method
|
||||
ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>() ||
|
||||
ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character || // wait order
|
||||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn);
|
||||
if (oxygenLow && Character.CurrentHull.Oxygen > 0)
|
||||
{
|
||||
@@ -454,7 +490,7 @@ namespace Barotrauma
|
||||
{
|
||||
shouldKeepTheGearOn = true;
|
||||
}
|
||||
bool removeDivingSuit = !shouldKeepTheGearOn;
|
||||
bool removeDivingSuit = !shouldKeepTheGearOn && Character.Submarine?.TeamID == Character.TeamID && (!(ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo) || goTo.Target != Character);
|
||||
bool takeMaskOff = !shouldKeepTheGearOn;
|
||||
if (!shouldKeepTheGearOn && !oxygenLow)
|
||||
{
|
||||
@@ -505,7 +541,7 @@ namespace Barotrauma
|
||||
var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR);
|
||||
if (divingSuit != null)
|
||||
{
|
||||
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
if (oxygenLow || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
{
|
||||
divingSuit.Drop(Character);
|
||||
HandleRelocation(divingSuit);
|
||||
@@ -550,7 +586,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
{
|
||||
if (ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
{
|
||||
mask.Drop(Character);
|
||||
HandleRelocation(mask);
|
||||
@@ -603,7 +639,7 @@ namespace Barotrauma
|
||||
Item item = Character.Inventory.GetItemInLimbSlot(hand);
|
||||
if (item == null) { continue; }
|
||||
|
||||
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }) && Character.Submarine?.TeamID == Character.TeamID )
|
||||
{
|
||||
findItemState = FindItemState.OtherItem;
|
||||
if (FindSuitableContainer(item, out Item targetContainer))
|
||||
@@ -705,9 +741,10 @@ namespace Barotrauma
|
||||
suitableContainer = null;
|
||||
if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredItems, positionalReference: containableItem, customPriorityFunction: i =>
|
||||
{
|
||||
if (i.IsThisOrAnyContainerIgnoredByAI()) { return 0; }
|
||||
if (i.IsThisOrAnyContainerIgnoredByAI(character)) { return 0; }
|
||||
var container = i.GetComponent<ItemContainer>();
|
||||
if (container == null) { return 0; }
|
||||
if (!container.HasAccess(character)) { return 0; }
|
||||
if (!container.Inventory.CanBePut(containableItem)) { return 0; }
|
||||
if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined))
|
||||
{
|
||||
@@ -743,6 +780,7 @@ namespace Barotrauma
|
||||
{
|
||||
Order newOrder = null;
|
||||
Hull targetHull = null;
|
||||
bool speak = true;
|
||||
if (Character.CurrentHull != null)
|
||||
{
|
||||
bool isFighting = ObjectiveManager.HasActiveObjective<AIObjectiveCombat>();
|
||||
@@ -759,6 +797,21 @@ namespace Barotrauma
|
||||
var orderPrefab = Order.GetPrefab("reportintruders");
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
targetHull = hull;
|
||||
if (target.IsEscorted)
|
||||
{
|
||||
if (!Character.IsPrisoner && target.IsPrisoner)
|
||||
{
|
||||
string msg = TextManager.GetWithVariables("orderdialog.prisonerescaped", new string[] { "[roomname]" }, new string[] { targetHull.DisplayName }, new bool[] { false, true }, true);
|
||||
Character.Speak(msg, ChatMessageType.Order);
|
||||
speak = false;
|
||||
}
|
||||
else if (!IsMentallyUnstable && target.AIController.IsMentallyUnstable)
|
||||
{
|
||||
string msg = TextManager.GetWithVariables("orderdialog.mentalcase", new string[] { "[roomname]" }, new string[] { targetHull.DisplayName }, new bool[] { false, true }, true);
|
||||
Character.Speak(msg, ChatMessageType.Order);
|
||||
speak = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -771,7 +824,7 @@ namespace Barotrauma
|
||||
targetHull = hull;
|
||||
}
|
||||
}
|
||||
if (IsBallastFloraNoticeable(Character, hull))
|
||||
if (IsBallastFloraNoticeable(Character, hull) && newOrder == null)
|
||||
{
|
||||
var orderPrefab = Order.GetPrefab("reportballastflora");
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
@@ -824,20 +877,24 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newOrder != null)
|
||||
if (newOrder != null && speak)
|
||||
{
|
||||
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
// for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented
|
||||
if (!Character.IsEscorted)
|
||||
{
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Default,
|
||||
identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"),
|
||||
minDurationBetweenSimilar: 60.0f);
|
||||
}
|
||||
else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
|
||||
{
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order);
|
||||
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Default,
|
||||
identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"),
|
||||
minDurationBetweenSimilar: 60.0f);
|
||||
}
|
||||
else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
|
||||
{
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order);
|
||||
#if SERVER
|
||||
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", CharacterInfo.HighestManualOrderPriority, targetHull, null, Character));
|
||||
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder, "", CharacterInfo.HighestManualOrderPriority, targetHull, null, Character));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -977,11 +1034,11 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
float cumulativeDamage = GetDamageDoneByAttacker(attacker);
|
||||
if (!Character.IsSecurity && attacker.IsBot && Character.CombatAction == null)
|
||||
bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null;
|
||||
if (isAccidental)
|
||||
{
|
||||
if (cumulativeDamage > 1)
|
||||
if (!Character.IsSecurity && cumulativeDamage > 1)
|
||||
{
|
||||
// Don't retaliate on damage done by friendly NPC, because we know it's accidental
|
||||
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
|
||||
}
|
||||
}
|
||||
@@ -1039,8 +1096,11 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-friendly
|
||||
InformOtherNPCs(GetDamageDoneByAttacker(attacker));
|
||||
if (Character.Submarine != null && Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
|
||||
{
|
||||
// Non-friendly
|
||||
InformOtherNPCs(GetDamageDoneByAttacker(attacker));
|
||||
}
|
||||
if (Character.IsBot)
|
||||
{
|
||||
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker);
|
||||
@@ -1051,7 +1111,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (Character otherCharacter in Character.CharacterList)
|
||||
{
|
||||
if (otherCharacter == Character || otherCharacter.IsDead || otherCharacter.IsUnconscious || otherCharacter.Removed) { continue; }
|
||||
if (otherCharacter == Character || otherCharacter.IsUnconscious || otherCharacter.Removed) { continue; }
|
||||
if (otherCharacter.Submarine != Character.Submarine) { continue; }
|
||||
if (otherCharacter.Submarine != attacker.Submarine) { continue; }
|
||||
if (otherCharacter.Info?.Job == null || otherCharacter.IsInstigator) { continue; }
|
||||
@@ -1070,12 +1130,27 @@ namespace Barotrauma
|
||||
{
|
||||
if (!IsFriendly(attacker))
|
||||
{
|
||||
return c.AIController is HumanAIController humanAI &&
|
||||
if (Character.Submarine == null)
|
||||
{
|
||||
// Outside -> don't react.
|
||||
return AIObjectiveCombat.CombatMode.None;
|
||||
}
|
||||
if (!Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
|
||||
{
|
||||
// Attacked from an unconnected submarine.
|
||||
return Character.SelectedConstruction?.GetComponent<Turret>() != null ? AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat;
|
||||
}
|
||||
return c.AIController is HumanAIController humanAI &&
|
||||
(humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders))
|
||||
? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Character.Submarine == null || !Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
|
||||
{
|
||||
// Outside or attacked from an unconnected submarine -> don't react.
|
||||
return AIObjectiveCombat.CombatMode.None;
|
||||
}
|
||||
// If there are any enemies around, just ignore the friendly fire
|
||||
if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsDead && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull)))
|
||||
{
|
||||
@@ -1090,7 +1165,7 @@ namespace Barotrauma
|
||||
// The guards don't react when the player attacks instigators.
|
||||
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
|
||||
}
|
||||
else if (attacker.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
else if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable))
|
||||
{
|
||||
if (c.IsSecurity)
|
||||
{
|
||||
@@ -1132,7 +1207,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func<bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false)
|
||||
public void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func<AIObjective, bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false)
|
||||
{
|
||||
if (mode == AIObjectiveCombat.CombatMode.None) { return; }
|
||||
if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; }
|
||||
@@ -1168,7 +1243,7 @@ namespace Barotrauma
|
||||
Character.Info?.Job?.Prefab.Identifier == "watchman" ||
|
||||
Character.CurrentHull == null ||
|
||||
Character.IsOnPlayerTeam && !target.IsPlayer && ObjectiveManager.GetActiveObjective<AIObjectiveGoTo>()?.Target is Character followTarget && followTarget.IsPlayer,
|
||||
abortCondition = abortCondition,
|
||||
AbortCondition = abortCondition,
|
||||
allowHoldFire = allowHoldFire,
|
||||
};
|
||||
if (onAbort != null)
|
||||
@@ -1190,7 +1265,7 @@ namespace Barotrauma
|
||||
|
||||
public void SetForcedOrder(Order order, string option, Character orderGiver)
|
||||
{
|
||||
var objective = ObjectiveManager.CreateObjective(order, option, orderGiver, false);
|
||||
var objective = ObjectiveManager.CreateObjective(order, option, orderGiver);
|
||||
ObjectiveManager.SetForcedOrder(objective);
|
||||
}
|
||||
|
||||
@@ -1273,7 +1348,8 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Check whether the character has a diving suit in usable condition plus some oxygen.
|
||||
/// </summary>
|
||||
public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true);
|
||||
public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true,
|
||||
predicate: (Item item) => { return character.HasEquippedItem(item, InvSlotType.OuterClothes); });
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the character has a diving mask in usable condition plus some oxygen.
|
||||
@@ -1401,7 +1477,9 @@ namespace Barotrauma
|
||||
Character thief = character;
|
||||
bool someoneSpoke = false;
|
||||
|
||||
if (item.SpawnedInOutpost && !item.AllowStealing && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker"))
|
||||
bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInOutpost && !it.AllowStealing, recursive: true).Any() ?? false;
|
||||
|
||||
if ((item.SpawnedInOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker"))
|
||||
{
|
||||
foreach (Character otherCharacter in Character.CharacterList)
|
||||
{
|
||||
@@ -1464,7 +1542,7 @@ namespace Barotrauma
|
||||
if (!humanAI.Character.IsSecurity) { return false; }
|
||||
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
|
||||
humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, thief, delay: GetReactionTime(),
|
||||
abortCondition: () => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null,
|
||||
abortCondition: obj => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null,
|
||||
onAbort: () =>
|
||||
{
|
||||
if (item != null && !item.Removed && humanAI != null && !humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveGetItem>())
|
||||
@@ -1845,18 +1923,52 @@ namespace Barotrauma
|
||||
|
||||
private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self);
|
||||
|
||||
public static bool IsItemOperatedByAnother(Character character, ItemComponent target, out Character operatingCharacter)
|
||||
public static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter)
|
||||
{
|
||||
operatingCharacter = null;
|
||||
if (character == null) { return false; }
|
||||
if (target?.Item == null) { return false; }
|
||||
bool isOrder = IsOrderedToOperateThis(character.AIController);
|
||||
foreach (var c in Character.CharacterList)
|
||||
float highestPriority = -1.0f;
|
||||
float highestPriorityModifier = -1.0f;
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c == character) { continue; }
|
||||
if (c.IsDead || c.IsIncapacitated) { continue; }
|
||||
if (!IsFriendly(character, c, onlySameTeam: true)) { continue; }
|
||||
operatingCharacter = c;
|
||||
if (c.Removed) { continue; }
|
||||
if (c.TeamID != team) { continue; }
|
||||
if (c.IsIncapacitated) { continue; }
|
||||
if (c.SelectedConstruction == target.Item)
|
||||
{
|
||||
operatingCharacter = c;
|
||||
return true;
|
||||
}
|
||||
if (c.AIController is HumanAIController humanAI)
|
||||
{
|
||||
foreach (var objective in humanAI.ObjectiveManager.Objectives)
|
||||
{
|
||||
if (!(objective is AIObjectiveOperateItem operateObjective)) { continue; }
|
||||
if (operateObjective.Component.Item != target.Item) { continue; }
|
||||
if (operateObjective.Priority < highestPriority) { continue; }
|
||||
if (operateObjective.PriorityModifier < highestPriorityModifier) { continue; }
|
||||
operatingCharacter = c;
|
||||
highestPriority = operateObjective.Priority;
|
||||
highestPriorityModifier = operateObjective.PriorityModifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
return operatingCharacter != null;
|
||||
}
|
||||
|
||||
// There's some duplicate logic in the two methods below, but making them use the same code would require some changes in the target classes so that we could use exactly the same checks.
|
||||
// And even then there would be some differences that could end up being confusing (like the exception for steering).
|
||||
public bool IsItemOperatedByAnother(ItemComponent target, out Character other)
|
||||
{
|
||||
other = null;
|
||||
if (target?.Item == null) { return false; }
|
||||
bool isOrder = IsOrderedToOperateThis(Character.AIController);
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c == Character) { continue; }
|
||||
if (c.Removed) { continue; }
|
||||
if (c.TeamID != Character.TeamID) { continue; }
|
||||
if (c.IsIncapacitated) { continue; }
|
||||
other = c;
|
||||
if (c.IsPlayer)
|
||||
{
|
||||
if (c.SelectedConstruction == target.Item)
|
||||
@@ -1887,7 +1999,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder == operatingAI.ObjectiveManager.CurrentObjective)
|
||||
if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective)
|
||||
{
|
||||
// The other bot is ordered to do something else
|
||||
continue;
|
||||
@@ -1895,12 +2007,12 @@ namespace Barotrauma
|
||||
if (target is Steering)
|
||||
{
|
||||
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
|
||||
if (character.GetSkillLevel("helm") <= c.GetSkillLevel("helm"))
|
||||
if (Character.GetSkillLevel("helm") <= c.GetSkillLevel("helm"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c))
|
||||
else if (target.DegreeOfSuccess(Character) <= target.DegreeOfSuccess(c))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -1909,7 +2021,65 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
return false;
|
||||
bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item;
|
||||
bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item;
|
||||
}
|
||||
|
||||
public bool IsItemRepairedByAnother(Item target, out Character other)
|
||||
{
|
||||
other = null;
|
||||
if (Character == null) { return false; }
|
||||
if (target == null) { return false; }
|
||||
bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController);
|
||||
foreach (var c in Character.CharacterList)
|
||||
{
|
||||
if (c == Character) { continue; }
|
||||
if (c.TeamID != Character.TeamID) { continue; }
|
||||
if (c.IsIncapacitated) { continue; }
|
||||
other = c;
|
||||
if (c.IsPlayer)
|
||||
{
|
||||
if (target.Repairables.Any(r => r.CurrentFixer == c))
|
||||
{
|
||||
// If the other character is player, don't try to repair
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (c.AIController is HumanAIController operatingAI)
|
||||
{
|
||||
var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective<AIObjectiveRepairItems>();
|
||||
if (repairItemsObjective == null) { continue; }
|
||||
if (repairItemsObjective.SubObjectives.None(o => o is AIObjectiveRepairItem repairObjective && repairObjective.Item == target))
|
||||
{
|
||||
// Not targeting the same item.
|
||||
continue;
|
||||
}
|
||||
bool isTargetOrdered = IsOrderedToRepairThis(operatingAI);
|
||||
if (!isOrder && isTargetOrdered)
|
||||
{
|
||||
// If the other bot is ordered to repair the item, let him do it, unless we are ordered too
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isOrder && !isTargetOrdered)
|
||||
{
|
||||
// We are ordered and the target is not -> allow to repair
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective)
|
||||
{
|
||||
// The other bot is ordered to do something else
|
||||
continue;
|
||||
}
|
||||
return target.Repairables.Max(r => r.DegreeOfSuccess(Character)) <= target.Repairables.Max(r => r.DegreeOfSuccess(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
bool IsOrderedToRepairThis(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveRepairItems repairOrder && repairOrder.PrioritizedItem == target;
|
||||
}
|
||||
|
||||
#region Wrappers
|
||||
@@ -1918,7 +2088,6 @@ namespace Barotrauma
|
||||
public bool IsTrueForAnyCrewMember(Func<HumanAIController, bool> predicate) => IsTrueForAnyCrewMember(Character, predicate);
|
||||
public bool IsTrueForAllCrewMembers(Func<HumanAIController, bool> predicate) => IsTrueForAllCrewMembers(Character, predicate);
|
||||
public int CountCrew(Func<HumanAIController, bool> predicate = null, bool onlyActive = true, bool onlyBots = false) => CountCrew(Character, predicate, onlyActive, onlyBots);
|
||||
public bool IsItemOperatedByAnother(ItemComponent target, out Character operatingCharacter) => IsItemOperatedByAnother(Character, target, out operatingCharacter);
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,34 +301,33 @@ namespace Barotrauma
|
||||
}
|
||||
Ladder nextLadder = GetNextLadder();
|
||||
var ladders = currentLadder ?? nextLadder;
|
||||
if (canClimb && !isDiving && ladders != null && character.SelectedConstruction != ladders.Item)
|
||||
bool useLadders = canClimb && ladders != null && (!isDiving || Math.Abs(steering.X) < 0.1f && Math.Abs(steering.Y) > 1);
|
||||
if (useLadders && character.SelectedConstruction != ladders.Item)
|
||||
{
|
||||
if (IsNextNodeLadder || currentPath.Finished)
|
||||
{
|
||||
if (character.CanInteractWith(ladders.Item))
|
||||
{
|
||||
ladders.Item.TryInteract(character, false, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cannot interact with the current (or next) ladder,
|
||||
// Try to select the previous ladder, unless it's already selected, unless the previous ladder is not adjacent to the current ladder.
|
||||
// The intention of this code is to prevent the bots from dropping from the "double ladders".
|
||||
var previousLadders = currentPath.PrevNode?.Ladders;
|
||||
if (previousLadders != null && previousLadders != ladders && character.SelectedConstruction != previousLadders.Item &&
|
||||
character.CanInteractWith(previousLadders.Item) && Math.Abs(previousLadders.Item.WorldPosition.X - ladders.Item.WorldPosition.X) < 5)
|
||||
{
|
||||
previousLadders.Item.TryInteract(character, false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!IsNextLadderSameAsCurrent && character.SelectedConstruction?.GetComponent<Ladder>() != null && character.CanInteractWith(ladders.Item))
|
||||
if (character.CanInteractWith(ladders.Item))
|
||||
{
|
||||
ladders.Item.TryInteract(character, false, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cannot interact with the current (or next) ladder,
|
||||
// Try to select the previous ladder, unless it's already selected, unless the previous ladder is not adjacent to the current ladder.
|
||||
// The intention of this code is to prevent the bots from dropping from the "double ladders".
|
||||
var previousLadders = currentPath.PrevNode?.Ladders;
|
||||
if (previousLadders != null && previousLadders != ladders && character.SelectedConstruction != previousLadders.Item &&
|
||||
character.CanInteractWith(previousLadders.Item) && Math.Abs(previousLadders.Item.WorldPosition.X - ladders.Item.WorldPosition.X) < 5)
|
||||
{
|
||||
previousLadders.Item.TryInteract(character, false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
var collider = character.AnimController.Collider;
|
||||
if (character.IsClimbing && !isDiving)
|
||||
if (character.IsClimbing && !useLadders)
|
||||
{
|
||||
character.AnimController.Anim = AnimController.Animation.None;
|
||||
character.SelectedConstruction = null;
|
||||
}
|
||||
if (character.IsClimbing && useLadders)
|
||||
{
|
||||
Vector2 diff = currentPath.CurrentNode.SimPosition - pos;
|
||||
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
|
||||
@@ -380,17 +379,12 @@ namespace Barotrauma
|
||||
}
|
||||
else if (character.AnimController.InWater)
|
||||
{
|
||||
// If the character is underwater, we don't need the ladders anymore
|
||||
if (character.IsClimbing && isDiving)
|
||||
{
|
||||
character.AnimController.Anim = AnimController.Animation.None;
|
||||
character.SelectedConstruction = null;
|
||||
}
|
||||
var door = currentPath.CurrentNode.ConnectedDoor;
|
||||
if (door == null || door.CanBeTraversed)
|
||||
{
|
||||
float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
|
||||
float targetDistance = collider.GetSize().X * multiplier;
|
||||
float margin = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
|
||||
Vector2 colliderSize = collider.GetSize();
|
||||
float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * margin, 0.5f);
|
||||
float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
|
||||
float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
|
||||
if (character.CurrentHull != currentPath.CurrentNode.CurrentHull)
|
||||
@@ -404,24 +398,25 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!canClimb || !IsNextLadderSameAsCurrent)
|
||||
else
|
||||
{
|
||||
// Walking horizontally
|
||||
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
|
||||
Vector2 colliderSize = collider.GetSize();
|
||||
Vector2 velocity = collider.LinearVelocity;
|
||||
// If the character is smaller than this, it would fail to use the waypoint nodes because they are always too high.
|
||||
float minHeight = 1;
|
||||
// If the character is very thin, without a min value, it would often fail to reach the waypoints, because the horizontal distance is too small.
|
||||
float minWidth = 0.17f;
|
||||
// If the character is very short, it would fail to use the waypoint nodes because they are always too high.
|
||||
// If the character is very thin, it would often fail to reach the waypoints, because the horizontal distance is too small.
|
||||
// Both values are based on the human size. So basically anything smaller than humans are considered as equal in size.
|
||||
float minHeight = 1.6125001f;
|
||||
float minWidth = 0.3225f;
|
||||
// Cannot use the head position, because not all characters have head or it can be below the total height of the character
|
||||
float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight);
|
||||
float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X);
|
||||
bool isAboveFeet = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y;
|
||||
bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + characterHeight;
|
||||
var door = currentPath.CurrentNode.ConnectedDoor;
|
||||
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 10, 0, 1));
|
||||
float targetDistance = Math.Max(collider.radius * margin, minWidth);
|
||||
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
|
||||
float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
|
||||
if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && (door == null || door.CanBeTraversed))
|
||||
{
|
||||
currentPath.SkipToNextNode();
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
using Barotrauma.Extensions;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class MentalStateManager
|
||||
{
|
||||
private float mentalStateTimer;
|
||||
private const float MentalStateInterval = 7.5f;
|
||||
|
||||
private float mentalBehaviorTimer;
|
||||
private const float MentalBehaviorInterval = 7.5f;
|
||||
|
||||
private readonly Character character;
|
||||
private readonly HumanAIController humanAIController;
|
||||
|
||||
public bool Active { get; set; }
|
||||
public MentalType CurrentMentalType { get; private set; }
|
||||
public enum MentalType
|
||||
{
|
||||
Normal,
|
||||
Confused, // No effects other than special dialogue
|
||||
Afraid, // Will retreat from whoever is nearby
|
||||
Desperate, // Will defensively attack/arrest whoever is nearby
|
||||
Berserk // turns fully hostile using team change logic
|
||||
}
|
||||
|
||||
private const string MentalTeamChange = "mental";
|
||||
|
||||
public MentalStateManager(Character character, HumanAIController humanAIController)
|
||||
{
|
||||
this.character = character;
|
||||
this.humanAIController = humanAIController;
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
if (!Active) { return; }
|
||||
mentalStateTimer -= deltaTime;
|
||||
if (mentalStateTimer <= 0.0f)
|
||||
{
|
||||
UpdateMentalState();
|
||||
mentalStateTimer = MentalStateInterval * Rand.Range(0.75f, 1.25f);
|
||||
}
|
||||
|
||||
mentalBehaviorTimer = Math.Max(0f, mentalBehaviorTimer - deltaTime);
|
||||
}
|
||||
|
||||
private void UpdateMentalState()
|
||||
{
|
||||
MentalType newMentalType = GetMentalType(character.CharacterHealth.GetAffliction("psychosis"));
|
||||
bool createdCombat = false;
|
||||
|
||||
switch (newMentalType)
|
||||
{
|
||||
case MentalType.Normal:
|
||||
case MentalType.Confused:
|
||||
// remove combat if we became normal again
|
||||
mentalBehaviorTimer = 0f;
|
||||
break;
|
||||
case MentalType.Afraid:
|
||||
case MentalType.Desperate:
|
||||
case MentalType.Berserk:
|
||||
// berserk is not removed unless we drop to normal behavior again
|
||||
if (CurrentMentalType == MentalType.Berserk)
|
||||
{
|
||||
newMentalType = MentalType.Berserk;
|
||||
}
|
||||
// give players a full interval to react to mental changes
|
||||
if (newMentalType == CurrentMentalType)
|
||||
{
|
||||
createdCombat = CreateCombatBehavior(CurrentMentalType);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!createdCombat)
|
||||
{
|
||||
CreateDialogueBehavior(newMentalType);
|
||||
}
|
||||
|
||||
if (newMentalType != MentalType.Berserk)
|
||||
{
|
||||
character.TryRemoveTeamChange(MentalTeamChange);
|
||||
}
|
||||
|
||||
CurrentMentalType = newMentalType;
|
||||
}
|
||||
|
||||
private int mentalTypeCount;
|
||||
private int MentalTypeCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (mentalTypeCount == 0)
|
||||
{
|
||||
mentalTypeCount = Enum.GetNames(typeof(MentalType)).Length;
|
||||
}
|
||||
return mentalTypeCount;
|
||||
}
|
||||
}
|
||||
|
||||
private MentalType GetMentalType(Affliction affliction)
|
||||
{
|
||||
if (affliction == null)
|
||||
{
|
||||
return MentalType.Normal;
|
||||
}
|
||||
// test this later
|
||||
int psychosisIndex = (int)(affliction.Strength / (affliction.Prefab.MaxStrength / MentalTypeCount) * Rand.Range(1f, 1.2f));
|
||||
psychosisIndex = Math.Clamp(psychosisIndex, 0, 4);
|
||||
MentalType mentalType = psychosisIndex switch
|
||||
{
|
||||
0 => MentalType.Normal,
|
||||
1 => MentalType.Confused,
|
||||
2 => MentalType.Afraid,
|
||||
3 => MentalType.Desperate,
|
||||
4 => MentalType.Berserk,
|
||||
_ => throw new ArgumentOutOfRangeException(psychosisIndex.ToString()),
|
||||
};
|
||||
return mentalType;
|
||||
}
|
||||
|
||||
public bool CreateCombatBehavior(MentalType mentalType)
|
||||
{
|
||||
Character mentalAttackTarget = Character.CharacterList.Where(
|
||||
possibleTarget => HumanAIController.IsActive(possibleTarget) &&
|
||||
(possibleTarget.TeamID != character.TeamID || mentalType == MentalType.Berserk) &&
|
||||
humanAIController.VisibleHulls.Contains(possibleTarget.CurrentHull) &&
|
||||
possibleTarget != character).GetRandom();
|
||||
|
||||
if (mentalAttackTarget == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var combatMode = AIObjectiveCombat.CombatMode.None;
|
||||
bool holdFire = mentalType == MentalType.Afraid && character.IsSecurity;
|
||||
switch (mentalType)
|
||||
{
|
||||
case MentalType.Afraid:
|
||||
combatMode = character.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
|
||||
break;
|
||||
case MentalType.Desperate:
|
||||
// might be unnecessary to explicitly declare as arrest against non-humans
|
||||
combatMode = character.IsSecurity && mentalAttackTarget.IsHuman ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Defensive;
|
||||
break;
|
||||
case MentalType.Berserk:
|
||||
combatMode = AIObjectiveCombat.CombatMode.Offensive;
|
||||
break;
|
||||
}
|
||||
|
||||
// using this as an explicit time-out for the behavior. it's possible it will never run out because of the manager being disabled, but combat objective has failsafes for that
|
||||
mentalBehaviorTimer = MentalBehaviorInterval;
|
||||
humanAIController.AddCombatObjective(combatMode, mentalAttackTarget, allowHoldFire: holdFire, abortCondition: obj => mentalBehaviorTimer <= 0f);
|
||||
string textIdentifier = $"dialogmentalstatereaction{combatMode.ToString().ToLowerInvariant()}";
|
||||
character.Speak(TextManager.Get(textIdentifier), delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 25f);
|
||||
|
||||
if (mentalType == MentalType.Berserk && !character.HasTeamChange(MentalTeamChange))
|
||||
{
|
||||
// TODO: could this be handled in the switch block above?
|
||||
character.TryAddNewTeamChange(MentalTeamChange, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Absolute, aggressiveBehavior: true));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void CreateDialogueBehavior(MentalType mentalType)
|
||||
{
|
||||
if (mentalType == MentalType.Normal) { return; }
|
||||
string textIdentifier = $"dialogmentalstate{mentalType.ToString().ToLowerInvariant()}";
|
||||
character.Speak(TextManager.Get(textIdentifier), delay: Rand.Range(0.5f, 1.0f), identifier: textIdentifier, minDurationBetweenSimilar: 35f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,10 @@ namespace Barotrauma
|
||||
public readonly List<NPCConversation> Responses;
|
||||
private readonly int speakerIndex;
|
||||
private readonly List<string> allowedSpeakerTags;
|
||||
private readonly bool requireNextLine;
|
||||
// used primarily for team1 characters interacting with escorted personnel (TODO: not used anywhere)
|
||||
private readonly bool requireSight;
|
||||
|
||||
public static void LoadAll(IEnumerable<ContentFile> files)
|
||||
{
|
||||
foreach (var file in files)
|
||||
@@ -161,6 +165,8 @@ namespace Barotrauma
|
||||
{
|
||||
Responses.Add(new NPCConversation(subElement, filePath));
|
||||
}
|
||||
requireNextLine = element.GetAttributeBool("requirenextline", false);
|
||||
requireSight = element.GetAttributeBool("requiresight", false);
|
||||
}
|
||||
|
||||
private static List<string> GetCurrentFlags(Character speaker)
|
||||
@@ -211,7 +217,7 @@ namespace Barotrauma
|
||||
var afflictions = speaker.CharacterHealth.GetAllAfflictions();
|
||||
foreach (Affliction affliction in afflictions)
|
||||
{
|
||||
var currentEffect = affliction.Prefab.GetActiveEffect(affliction.Strength);
|
||||
var currentEffect = affliction.GetActiveEffect();
|
||||
if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag) && !currentFlags.Contains(currentEffect.DialogFlag))
|
||||
{
|
||||
currentFlags.Add(currentEffect.DialogFlag);
|
||||
@@ -226,7 +232,6 @@ namespace Barotrauma
|
||||
{
|
||||
currentFlags.Add("CampaignNPC." + speaker.CampaignInteractionType);
|
||||
}
|
||||
|
||||
if (GameMain.GameSession?.GameMode is CampaignMode campaignMode &&
|
||||
(campaignMode.Map?.CurrentLocation?.Type?.Identifier.Equals("abandoned", StringComparison.OrdinalIgnoreCase) ?? false))
|
||||
{
|
||||
@@ -239,6 +244,10 @@ namespace Barotrauma
|
||||
currentFlags.Add("Hostage");
|
||||
}
|
||||
}
|
||||
if (speaker.IsEscorted)
|
||||
{
|
||||
currentFlags.Add("escort");
|
||||
}
|
||||
}
|
||||
|
||||
return currentFlags;
|
||||
@@ -325,43 +334,15 @@ namespace Barotrauma
|
||||
|
||||
foreach (Character potentialSpeaker in availableSpeakers)
|
||||
{
|
||||
//check if the character has an appropriate job to say the line
|
||||
if ((potentialSpeaker.Info?.Job != null && potentialSpeaker.Info.Job.Prefab.OnlyJobSpecificDialog) ||
|
||||
selectedConversation.AllowedJobs.Count > 0)
|
||||
if (CheckSpeakerViability(potentialSpeaker, selectedConversation, assignedSpeakers.Values.ToList(), ignoreFlags))
|
||||
{
|
||||
if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) { continue; }
|
||||
allowedSpeakers.Add(potentialSpeaker);
|
||||
}
|
||||
|
||||
//check if the character has all required flags to say the line
|
||||
if (!ignoreFlags)
|
||||
{
|
||||
var characterFlags = GetCurrentFlags(potentialSpeaker);
|
||||
if (!selectedConversation.Flags.All(flag => characterFlags.Contains(flag))) { continue; }
|
||||
}
|
||||
|
||||
//check if the character is close enough to hear the rest of the speakers
|
||||
if (assignedSpeakers.Values.Any(s => !potentialSpeaker.CanHearCharacter(s))) { continue; }
|
||||
|
||||
//check if the character has an appropriate personality
|
||||
if (selectedConversation.allowedSpeakerTags.Count > 0)
|
||||
{
|
||||
if (potentialSpeaker.Info?.PersonalityTrait == null) { continue; }
|
||||
if (!selectedConversation.allowedSpeakerTags.Any(t => potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Any(t2 => t2 == t))) { continue; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (potentialSpeaker.Info?.PersonalityTrait != null &&
|
||||
!potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Contains("none"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
allowedSpeakers.Add(potentialSpeaker);
|
||||
}
|
||||
|
||||
if (allowedSpeakers.Count == 0)
|
||||
if (allowedSpeakers.Count == 0 || NextLineFailure(selectedConversation, availableSpeakers, allowedSpeakers, ignoreFlags))
|
||||
{
|
||||
allowedSpeakers.Clear();
|
||||
potentialLines.Remove(selectedConversation);
|
||||
}
|
||||
else
|
||||
@@ -385,6 +366,62 @@ namespace Barotrauma
|
||||
CreateConversation(availableSpeakers, assignedSpeakers, selectedConversation, lineList, availableConversations);
|
||||
}
|
||||
|
||||
static bool NextLineFailure(NPCConversation selectedConversation, List<Character> availableSpeakers, List<Character> allowedSpeakers, bool ignoreFlags)
|
||||
{
|
||||
if (selectedConversation.requireNextLine)
|
||||
{
|
||||
foreach (NPCConversation nextConversation in selectedConversation.Responses)
|
||||
{
|
||||
foreach (Character potentialNextSpeaker in availableSpeakers)
|
||||
{
|
||||
if (CheckSpeakerViability(potentialNextSpeaker, nextConversation, allowedSpeakers, ignoreFlags))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool CheckSpeakerViability(Character potentialSpeaker, NPCConversation selectedConversation, List<Character> checkedSpeakers, bool ignoreFlags)
|
||||
{
|
||||
//check if the character has an appropriate job to say the line
|
||||
if ((potentialSpeaker.Info?.Job != null && potentialSpeaker.Info.Job.Prefab.OnlyJobSpecificDialog) || selectedConversation.AllowedJobs.Count > 0)
|
||||
{
|
||||
if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) { return false; }
|
||||
}
|
||||
|
||||
//check if the character has all required flags to say the line
|
||||
if (!ignoreFlags)
|
||||
{
|
||||
var characterFlags = GetCurrentFlags(potentialSpeaker);
|
||||
if (!selectedConversation.Flags.All(flag => characterFlags.Contains(flag))) { return false; }
|
||||
}
|
||||
|
||||
//check if the character is close enough to hear the rest of the speakers
|
||||
if (checkedSpeakers.Any(s => !potentialSpeaker.CanHearCharacter(s))) { return false; }
|
||||
|
||||
//check if the character is close enough to see the rest of the speakers (this should be replaced with a more performant method)
|
||||
if (checkedSpeakers.Any(s => !potentialSpeaker.CanSeeCharacter(s))) { return false; }
|
||||
|
||||
//check if the character has an appropriate personality
|
||||
if (selectedConversation.allowedSpeakerTags.Count > 0)
|
||||
{
|
||||
if (potentialSpeaker.Info?.PersonalityTrait == null) { return false; }
|
||||
if (!selectedConversation.allowedSpeakerTags.Any(t => potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Any(t2 => t2 == t))) { return false; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (potentialSpeaker.Info?.PersonalityTrait != null &&
|
||||
!potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Contains("none"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private static NPCConversation GetRandomConversation(List<NPCConversation> conversations, bool avoidPreviouslyUsed)
|
||||
{
|
||||
if (!avoidPreviouslyUsed)
|
||||
|
||||
@@ -6,16 +6,18 @@ using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
abstract class AIObjective
|
||||
abstract partial class AIObjective
|
||||
{
|
||||
public virtual float Devotion => AIObjectiveManager.baseDevotion;
|
||||
|
||||
public abstract string DebugTag { get; }
|
||||
public abstract string Identifier { get; set; }
|
||||
public virtual string DebugTag => Identifier;
|
||||
public virtual bool ForceRun => false;
|
||||
public virtual bool IgnoreUnsafeHulls => false;
|
||||
public virtual bool AbandonWhenCannotCompleteSubjectives => true;
|
||||
public virtual bool AllowSubObjectiveSorting => false;
|
||||
public virtual bool ForceOrderPriority => true;
|
||||
public virtual bool PrioritizeIfSubObjectivesActive => false;
|
||||
|
||||
/// <summary>
|
||||
/// Can there be multiple objective instaces of the same type?
|
||||
@@ -52,8 +54,32 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public float Priority { get; set; }
|
||||
public float BasePriority { get; set; }
|
||||
|
||||
public float PriorityModifier { get; private set; } = 1;
|
||||
|
||||
private float resetPriorityTimer;
|
||||
private readonly float resetPriorityTime = 1;
|
||||
private bool _forceHighestPriority;
|
||||
// For forcing the highest priority temporarily. Will reset automatically after one second, unless kept alive by something.
|
||||
public bool ForceHighestPriority
|
||||
{
|
||||
get { return _forceHighestPriority; }
|
||||
set
|
||||
{
|
||||
if (_forceHighestPriority == value) { return; }
|
||||
_forceHighestPriority = value;
|
||||
if (_forceHighestPriority)
|
||||
{
|
||||
resetPriorityTimer = resetPriorityTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For temporarily forcing walking. Will reset after each priority calculation, so it will need to be kept alive by something.
|
||||
// The intention of this boolean to allow walking even when the priority is higher than AIObjectiveManager.RunPriority.
|
||||
public bool ForceWalk { get; set; }
|
||||
|
||||
public bool IgnoreAtOutpost { get; set; }
|
||||
|
||||
public readonly Character character;
|
||||
public readonly AIObjectiveManager objectiveManager;
|
||||
public string Option { get; private set; }
|
||||
@@ -102,6 +128,13 @@ namespace Barotrauma
|
||||
return all;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0649
|
||||
/// <summary>
|
||||
/// Aborts the objective when this condition is true.
|
||||
/// </summary>
|
||||
public Func<AIObjective, bool> AbortCondition;
|
||||
#pragma warning restore CS0649
|
||||
|
||||
/// <summary>
|
||||
/// A single shot event. Automatically cleared after launching. Use OnCompleted method for implementing (internal) persistent behavior.
|
||||
/// </summary>
|
||||
@@ -153,7 +186,6 @@ namespace Barotrauma
|
||||
Act(deltaTime);
|
||||
}
|
||||
|
||||
// TODO: check turret aioperate
|
||||
public void AddSubObjective(AIObjective objective, bool addFirst = false)
|
||||
{
|
||||
var type = objective.GetType();
|
||||
@@ -217,18 +249,22 @@ namespace Barotrauma
|
||||
protected bool IsAllowed
|
||||
{
|
||||
get
|
||||
{
|
||||
{
|
||||
if (IgnoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!AllowOutsideSubmarine && character.Submarine == null) { return false; }
|
||||
if (AllowInAnySub) { return true; }
|
||||
if (AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) { return true; }
|
||||
if ((AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) || character.IsEscorted) { return true; }
|
||||
return character.Submarine.TeamID == character.TeamID || character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call this only when the priority needs to be recalculated. Use the cached Priority property when you don't need to recalculate.
|
||||
/// </summary>
|
||||
public virtual float GetPriority()
|
||||
protected virtual float GetPriority()
|
||||
{
|
||||
bool isOrder = objectiveManager.IsOrder(this);
|
||||
if (!IsAllowed)
|
||||
@@ -248,6 +284,17 @@ namespace Barotrauma
|
||||
return Priority;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call this only when the priority needs to be recalculated. Use the cached Priority property when you don't need to recalculate.
|
||||
/// </summary>
|
||||
public float CalculatePriority()
|
||||
{
|
||||
Priority = GetPriority();
|
||||
ForceHighestPriority = false;
|
||||
ForceWalk = false;
|
||||
return Priority;
|
||||
}
|
||||
|
||||
private void UpdateDevotion(float deltaTime)
|
||||
{
|
||||
var currentObjective = objectiveManager.CurrentObjective;
|
||||
@@ -261,6 +308,14 @@ namespace Barotrauma
|
||||
|
||||
public virtual void Update(float deltaTime)
|
||||
{
|
||||
if (resetPriorityTimer > 0)
|
||||
{
|
||||
resetPriorityTimer -= deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
ForceHighestPriority = false;
|
||||
}
|
||||
if (!objectiveManager.IsOrder(this) && objectiveManager.WaitTimer <= 0)
|
||||
{
|
||||
UpdateDevotion(deltaTime);
|
||||
@@ -393,7 +448,17 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract bool Check();
|
||||
protected virtual bool Check()
|
||||
{
|
||||
if (AbortCondition != null && AbortCondition(this))
|
||||
{
|
||||
Abandon = true;
|
||||
return false;
|
||||
}
|
||||
return CheckObjectiveSpecific();
|
||||
}
|
||||
|
||||
protected abstract bool CheckObjectiveSpecific();
|
||||
|
||||
private bool CheckState()
|
||||
{
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveChargeBatteries : AIObjectiveLoop<PowerContainer>
|
||||
{
|
||||
public override string DebugTag => "charge batteries";
|
||||
public override string Identifier { get; set; } = "charge batteries";
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
private IEnumerable<PowerContainer> batteryList;
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (battery == null) { return false; }
|
||||
var item = battery.Item;
|
||||
if (item.IgnoreByAI) { return false; }
|
||||
if (item.IgnoreByAI(character)) { return false; }
|
||||
if (!item.IsInteractable(character)) { return false; }
|
||||
if (item.Submarine == null) { return false; }
|
||||
if (item.CurrentHull == null) { return false; }
|
||||
|
||||
+12
-12
@@ -9,7 +9,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveCleanupItem : AIObjective
|
||||
{
|
||||
public override string DebugTag => "cleanup item";
|
||||
public override string Identifier { get; set; } = "cleanup item";
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AllowAutomaticItemUnequipping => false;
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Barotrauma
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
@@ -61,14 +61,14 @@ namespace Barotrauma
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (item.IgnoreByAI)
|
||||
if (item.IgnoreByAI(character))
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (item.ParentInventory != null)
|
||||
{
|
||||
if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrders()))
|
||||
if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrder<AIObjectiveCleanupItems>()))
|
||||
{
|
||||
// Target was picked up or moved by someone.
|
||||
Abandon = true;
|
||||
@@ -82,14 +82,14 @@ namespace Barotrauma
|
||||
itemIndex = 0;
|
||||
if (suitableContainer != null)
|
||||
{
|
||||
bool equip = item.HasTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR) || (
|
||||
item.GetComponent<Wearable>() == null &&
|
||||
bool equip = item.GetComponent<Holdable>() != null ||
|
||||
item.AllowedSlots.None(s =>
|
||||
s == InvSlotType.Card ||
|
||||
s == InvSlotType.Head ||
|
||||
s == InvSlotType.Headset ||
|
||||
s == InvSlotType.InnerClothes ||
|
||||
s == InvSlotType.OuterClothes));
|
||||
s == InvSlotType.Card ||
|
||||
s == InvSlotType.Head ||
|
||||
s == InvSlotType.Headset ||
|
||||
s == InvSlotType.InnerClothes ||
|
||||
s == InvSlotType.OuterClothes);
|
||||
|
||||
TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
|
||||
{
|
||||
Equip = equip,
|
||||
@@ -131,7 +131,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Check() => IsCompleted;
|
||||
protected override bool CheckObjectiveSpecific() => IsCompleted;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
|
||||
+18
-7
@@ -8,7 +8,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveCleanupItems : AIObjectiveLoop<Item>
|
||||
{
|
||||
public override string DebugTag => "cleanup items";
|
||||
public override string Identifier { get; set; } = "cleanup items";
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AllowAutomaticItemUnequipping => false;
|
||||
public override bool ForceOrderPriority => false;
|
||||
@@ -38,8 +38,8 @@ namespace Barotrauma
|
||||
float prio = objectiveManager.GetOrderPriority(this);
|
||||
if (subObjectives.All(so => so.SubObjectives.None()))
|
||||
{
|
||||
// If none of the subobjectives have subobjectives, no valid container was found. In this case, let's reduce the priority below the run threshold.
|
||||
prio = Math.Min(prio, AIObjectiveManager.RunPriority - 1);
|
||||
// If none of the subobjectives have subobjectives, no valid container was found. Don't allow running.
|
||||
ForceWalk = true;
|
||||
}
|
||||
return prio;
|
||||
}
|
||||
@@ -80,18 +80,29 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsValidContainer(Item item, Character character, bool allowUnloading = true) =>
|
||||
!item.IgnoreByAI && item.IsInteractable(character) && item.HasTag("allowcleanup") && allowUnloading && item.ParentInventory == null && item.OwnInventory != null && item.OwnInventory.AllItems.Any() && IsItemInsideValidSubmarine(item, character);
|
||||
public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) =>
|
||||
allowUnloading &&
|
||||
!container.IgnoreByAI(character) &&
|
||||
container.IsInteractable(character) &&
|
||||
container.HasTag("allowcleanup") &&
|
||||
container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() &&
|
||||
container.GetComponent<ItemContainer>() is ItemContainer itemContainer && itemContainer.HasAccess(character) &&
|
||||
IsItemInsideValidSubmarine(container, character);
|
||||
|
||||
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true)
|
||||
{
|
||||
if (item == null) { return false; }
|
||||
if (item.IgnoreByAI) { return false; }
|
||||
if (item.IgnoreByAI(character)) { return false; }
|
||||
if (!item.IsInteractable(character)) { return false; }
|
||||
if (item.SpawnedInOutpost) { return false; }
|
||||
if (item.ParentInventory != null)
|
||||
{
|
||||
if (item.Container == null || !IsValidContainer(item.Container, character, allowUnloading)) { return false; }
|
||||
if (item.Container == null)
|
||||
{
|
||||
// In a character inventory
|
||||
return false;
|
||||
}
|
||||
if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; }
|
||||
}
|
||||
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
|
||||
var pickable = item.GetComponent<Pickable>();
|
||||
|
||||
+12
-17
@@ -10,7 +10,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveCombat : AIObjective
|
||||
{
|
||||
public override string DebugTag => "combat";
|
||||
public override string Identifier { get; set; } = "combat";
|
||||
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool IgnoreUnsafeHulls => true;
|
||||
@@ -92,11 +92,6 @@ namespace Barotrauma
|
||||
private readonly float distanceCheckInterval = 0.2f;
|
||||
private float distanceTimer;
|
||||
|
||||
/// <summary>
|
||||
/// Aborts the objective when this condition is true
|
||||
/// </summary>
|
||||
public Func<bool> abortCondition;
|
||||
|
||||
public bool allowHoldFire;
|
||||
|
||||
/// <summary>
|
||||
@@ -152,7 +147,7 @@ namespace Barotrauma
|
||||
HumanAIController.SortTimer = 0;
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && Enemy != null)
|
||||
{
|
||||
@@ -186,7 +181,7 @@ namespace Barotrauma
|
||||
{
|
||||
findSafety.Priority = 0;
|
||||
}
|
||||
if (!character.IsOnPlayerTeam && !objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
|
||||
if (!AllowCoolDown && !character.IsOnPlayerTeam && !objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>())
|
||||
{
|
||||
distanceTimer -= deltaTime;
|
||||
if (distanceTimer < 0)
|
||||
@@ -197,7 +192,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
{
|
||||
if (sqrDistance > maxDistance * maxDistance)
|
||||
{
|
||||
@@ -209,11 +204,6 @@ namespace Barotrauma
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (abortCondition != null && abortCondition())
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (AllowCoolDown)
|
||||
{
|
||||
coolDownTimer -= deltaTime;
|
||||
@@ -358,6 +348,7 @@ namespace Barotrauma
|
||||
TryAddSubObjective(ref seekWeaponObjective,
|
||||
constructor: () => new AIObjectiveGetItem(character, "weapon", objectiveManager, equip: true, checkInventory: false)
|
||||
{
|
||||
AllowStealing = HumanAIController.IsMentallyUnstable,
|
||||
GetItemPriority = i =>
|
||||
{
|
||||
if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; }
|
||||
@@ -799,10 +790,13 @@ namespace Barotrauma
|
||||
}
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
// Confiscate stolen goods.
|
||||
// Confiscate stolen goods and all weapons
|
||||
foreach (var item in Enemy.Inventory.AllItemsMod)
|
||||
{
|
||||
if (item.StolenDuringRound)
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound ||
|
||||
item.HasTag("weapon") ||
|
||||
item.GetComponent<MeleeWeapon>() != null ||
|
||||
item.GetComponent<RangedWeapon>() != null)
|
||||
{
|
||||
item.Drop(character);
|
||||
character.Inventory.TryPutItem(item, character, CharacterInventory.anySlot);
|
||||
@@ -894,7 +888,8 @@ namespace Barotrauma
|
||||
if (ammunitionIdentifiers != null)
|
||||
{
|
||||
// Try reload ammunition from inventory
|
||||
ammunition = character.Inventory.FindItem(i => ammunitionIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)) && i.Condition > 0, true);
|
||||
bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio");
|
||||
ammunition = character.Inventory.FindItem(i => ammunitionIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
|
||||
if (ammunition != null)
|
||||
{
|
||||
var container = Weapon.GetComponent<ItemContainer>();
|
||||
|
||||
+11
-7
@@ -7,7 +7,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveContainItem: AIObjective
|
||||
{
|
||||
public override string DebugTag => "contain item";
|
||||
public override string Identifier { get; set; } = "contain item";
|
||||
|
||||
public Func<Item, float> GetItemPriority;
|
||||
|
||||
@@ -61,10 +61,10 @@ namespace Barotrauma
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
{
|
||||
if (IsCompleted) { return true; }
|
||||
if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI()))
|
||||
if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI(character)))
|
||||
{
|
||||
Abandon = true;
|
||||
return false;
|
||||
@@ -87,11 +87,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && !i.IsThisOrAnyContainerIgnoredByAI();
|
||||
private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage >= ConditionLevel && !i.IsThisOrAnyContainerIgnoredByAI(character);
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (container == null || (container.Item != null && container.Item.IsThisOrAnyContainerIgnoredByAI()))
|
||||
if (container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character))
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
@@ -146,7 +146,10 @@ namespace Barotrauma
|
||||
{
|
||||
DialogueIdentifier = "dialogcannotreachtarget",
|
||||
TargetName = container.Item.Name,
|
||||
abortCondition = obj => !ItemToContain.IsOwnedBy(character),
|
||||
AbortCondition = obj =>
|
||||
container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) ||
|
||||
ItemToContain == null || ItemToContain.Removed ||
|
||||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
|
||||
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>()
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
@@ -170,7 +173,8 @@ namespace Barotrauma
|
||||
ignoredItems = containedItems,
|
||||
AllowToFindDivingGear = AllowToFindDivingGear,
|
||||
AllowDangerousPressure = AllowDangerousPressure,
|
||||
TargetCondition = ConditionLevel
|
||||
TargetCondition = ConditionLevel,
|
||||
ItemFilter = (Item potentialItem) => RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem)
|
||||
}, onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
|
||||
+7
-4
@@ -6,7 +6,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveDecontainItem : AIObjective
|
||||
{
|
||||
public override string DebugTag => "decontain item";
|
||||
public override string Identifier { get; set; } = "decontain item";
|
||||
|
||||
public Func<Item, float> GetItemPriority;
|
||||
|
||||
@@ -59,17 +59,20 @@ namespace Barotrauma
|
||||
this.targetContainer = targetContainer;
|
||||
}
|
||||
|
||||
protected override bool Check() => IsCompleted;
|
||||
protected override bool CheckObjectiveSpecific() => IsCompleted;
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
Item itemToDecontain = targetItem ?? sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id) && !i.IgnoreByAI), recursive: false);
|
||||
Item itemToDecontain =
|
||||
targetItem ??
|
||||
sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id) && !i.IgnoreByAI(character)), recursive: false);
|
||||
|
||||
if (itemToDecontain == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (itemToDecontain.IgnoreByAI)
|
||||
if (itemToDecontain.IgnoreByAI(character))
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveEscapeHandcuffs : AIObjective
|
||||
{
|
||||
// Used for prisoner escorts to allow them to escape their binds
|
||||
public override string Identifier { get; set; } = "escape handcuffs";
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
public override bool AllowOutsideSubmarine => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
|
||||
private int escapeProgress;
|
||||
private bool isBeingWatched;
|
||||
|
||||
private bool shouldSwitchTeams;
|
||||
|
||||
const string EscapeTeamChangeIdentifier = "escape";
|
||||
|
||||
public AIObjectiveEscapeHandcuffs(Character character, AIObjectiveManager objectiveManager, bool shouldSwitchTeams = true, bool beginInstantly = false, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
this.shouldSwitchTeams = shouldSwitchTeams;
|
||||
if (beginInstantly)
|
||||
{
|
||||
escapeTimer = EscapeIntervalTimer;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanBeCompleted => true;
|
||||
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
|
||||
// escape timer is set to 60 by default to allow players to locate prisoners in time
|
||||
private float escapeTimer = 60f;
|
||||
private const float EscapeIntervalTimer = 7.5f;
|
||||
|
||||
private float updateTimer;
|
||||
private const float UpdateIntervalTimer = 4f;
|
||||
|
||||
protected override float GetPriority()
|
||||
{
|
||||
Priority = !isBeingWatched && character.LockHands ? AIObjectiveManager.LowestOrderPriority - 1 : 0;
|
||||
return Priority;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
updateTimer -= deltaTime;
|
||||
if (updateTimer <= 0.0f)
|
||||
{
|
||||
if (shouldSwitchTeams)
|
||||
{
|
||||
if (!character.LockHands)
|
||||
{
|
||||
if (!character.HasTeamChange(EscapeTeamChangeIdentifier))
|
||||
{
|
||||
character.TryAddNewTeamChange(EscapeTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
character.TryRemoveTeamChange(EscapeTeamChangeIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
isBeingWatched = false;
|
||||
foreach (Character otherCharacter in Character.CharacterList)
|
||||
{
|
||||
if (HumanAIController.IsActive(otherCharacter) && otherCharacter.TeamID == CharacterTeamType.Team1 && HumanAIController.VisibleHulls.Contains(otherCharacter.CurrentHull)) // hasn't been tested yet
|
||||
{
|
||||
isBeingWatched = true; // act casual when player characters are around
|
||||
escapeProgress = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
updateTimer = UpdateIntervalTimer * Rand.Range(0.75f, 1.25f);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
|
||||
escapeTimer -= deltaTime;
|
||||
if (escapeTimer <= 0.0f)
|
||||
{
|
||||
escapeProgress += Rand.Range(2, 5);
|
||||
if (escapeProgress > 15)
|
||||
{
|
||||
Item handcuffs = character.Inventory.FindItemByTag("handlocker");
|
||||
if (handcuffs != null)
|
||||
{
|
||||
handcuffs.Drop(character);
|
||||
}
|
||||
}
|
||||
escapeTimer = EscapeIntervalTimer * Rand.Range(0.75f, 1.25f);
|
||||
}
|
||||
}
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
escapeProgress = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -8,7 +8,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveExtinguishFire : AIObjective
|
||||
{
|
||||
public override string DebugTag => "extinguish fire";
|
||||
public override string Identifier { get; set; } = "extinguish fire";
|
||||
public override bool ForceRun => true;
|
||||
public override bool ConcurrentObjectives => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
@@ -27,7 +27,7 @@ namespace Barotrauma
|
||||
this.targetHull = targetHull;
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
@@ -68,7 +68,7 @@ namespace Barotrauma
|
||||
return Priority;
|
||||
}
|
||||
|
||||
protected override bool Check() => targetHull.FireSources.None();
|
||||
protected override bool CheckObjectiveSpecific() => targetHull.FireSources.None();
|
||||
|
||||
private float sinTime;
|
||||
protected override void Act(float deltaTime)
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveExtinguishFires : AIObjectiveLoop<Hull>
|
||||
{
|
||||
public override string DebugTag => "extinguish fires";
|
||||
public override string Identifier { get; set; } = "extinguish fires";
|
||||
public override bool ForceRun => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
|
||||
|
||||
+9
-4
@@ -6,7 +6,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveFightIntruders : AIObjectiveLoop<Character>
|
||||
{
|
||||
public override string DebugTag => "fight intruders";
|
||||
public override string Identifier { get; set; } = "fight intruders";
|
||||
protected override float IgnoreListClearInterval => 30;
|
||||
public override bool IgnoreUnsafeHulls => true;
|
||||
|
||||
@@ -21,13 +21,18 @@ namespace Barotrauma
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
// TODO: sorting criteria
|
||||
return Targets.None() ? 0 : 100;
|
||||
if (!character.IsOnPlayerTeam) { return Targets.None() ? 0 : 100; }
|
||||
int totalEnemies = Targets.Count();
|
||||
if (totalEnemies == 0) { return 0; }
|
||||
if (character.IsSecurity) { return 100; }
|
||||
if (objectiveManager.IsOrder(this)) { return 100; }
|
||||
return HumanAIController.IsTrueForAnyCrewMember(c => c.Character.IsSecurity && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine) ? 0 : 100;
|
||||
}
|
||||
|
||||
protected override AIObjective ObjectiveConstructor(Character target)
|
||||
{
|
||||
var combatObjective = new AIObjectiveCombat(character, target, AIObjectiveCombat.CombatMode.Offensive, objectiveManager, PriorityModifier);
|
||||
AIObjectiveCombat.CombatMode combatMode = target.IsEscorted && character.TeamID == CharacterTeamType.Team1 ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Offensive;
|
||||
var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier);
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
|
||||
{
|
||||
var reputation = campaign.Map?.CurrentLocation?.Reputation;
|
||||
|
||||
+9
-7
@@ -7,7 +7,8 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveFindDivingGear : AIObjective
|
||||
{
|
||||
public override string DebugTag => $"find diving gear ({gearTag})";
|
||||
public override string Identifier { get; set; } = "find diving gear";
|
||||
public override string DebugTag => $"{Identifier} ({gearTag})";
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
||||
@@ -23,7 +24,7 @@ namespace Barotrauma
|
||||
public static string LIGHT_DIVING_GEAR = "lightdiving";
|
||||
public static string OXYGEN_SOURCE = "oxygensource";
|
||||
|
||||
protected override bool Check() => targetItem != null && character.HasEquippedItem(targetItem);
|
||||
protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head);
|
||||
|
||||
public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
@@ -38,7 +39,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
targetItem = character.Inventory.FindItemByTag(gearTag, true);
|
||||
if (targetItem == null || !character.HasEquippedItem(targetItem) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0))
|
||||
if (targetItem == null || !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0))
|
||||
{
|
||||
TryAddSubObjective(ref getDivingGear, () =>
|
||||
{
|
||||
@@ -48,9 +49,11 @@ namespace Barotrauma
|
||||
}
|
||||
return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
|
||||
{
|
||||
AllowStealing = true,
|
||||
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
|
||||
AllowToFindDivingGear = false,
|
||||
AllowDangerousPressure = true
|
||||
AllowDangerousPressure = true,
|
||||
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes,
|
||||
Wear = true
|
||||
};
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
@@ -58,8 +61,6 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
HumanAIController.UnequipContainedItems(targetItem, it => !it.HasTag("oxygensource"));
|
||||
HumanAIController.UnequipEmptyItems(targetItem);
|
||||
// Seek oxygen that has at least 10% condition left, if we are inside a friendly sub.
|
||||
// The margin helps us to survive, because we might need some oxygen before we can find more oxygen.
|
||||
// When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks.
|
||||
@@ -119,6 +120,7 @@ namespace Barotrauma
|
||||
|
||||
int ReportOxygenTankCount()
|
||||
{
|
||||
if (character.Submarine != Submarine.MainSub) { return 1; }
|
||||
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("oxygensource") && i.Condition > 1);
|
||||
if (remainingOxygenTanks == 0)
|
||||
{
|
||||
|
||||
+3
-3
@@ -8,7 +8,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveFindSafety : AIObjective
|
||||
{
|
||||
public override string DebugTag => "find safety";
|
||||
public override string Identifier { get; set; } = "find safety";
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool IgnoreUnsafeHulls => true;
|
||||
@@ -32,12 +32,12 @@ namespace Barotrauma
|
||||
|
||||
public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
|
||||
|
||||
protected override bool Check() => false;
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
public override bool CanBeCompleted => true;
|
||||
|
||||
private bool resetPriority;
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
|
||||
+25
-21
@@ -9,7 +9,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveFixLeak : AIObjective
|
||||
{
|
||||
public override string DebugTag => "fix leak";
|
||||
public override string Identifier { get; set; } = "fix leak";
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
@@ -29,19 +29,22 @@ namespace Barotrauma
|
||||
this.isPriority = isPriority;
|
||||
}
|
||||
|
||||
protected override bool Check() => Leak.Open <= 0 || Leak.Removed;
|
||||
protected override bool CheckObjectiveSpecific() => Leak.Open <= 0 || Leak.Removed;
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
}
|
||||
else if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.Character.IsBot && other.ObjectiveManager.GetActiveObjective<AIObjectiveFixLeak>()?.Leak == Leak))
|
||||
else if (HumanAIController.IsTrueForAnyCrewMember(
|
||||
other => other != HumanAIController &&
|
||||
other.Character.IsBot &&
|
||||
other.ObjectiveManager.GetActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks &&
|
||||
fixLeaks.SubObjectives.Any(so => so is AIObjectiveFixLeak fixObjective && fixObjective.Leak == Leak)))
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -86,21 +89,22 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
HumanAIController.UnequipContainedItems(weldingTool, it => !it.HasTag("weldingfuel"));
|
||||
HumanAIController.UnequipEmptyItems(weldingTool);
|
||||
if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
|
||||
{
|
||||
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
|
||||
onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
ReportWeldingFuelTankCount();
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
RemoveSubObjective(ref refuelObjective);
|
||||
ReportWeldingFuelTankCount();
|
||||
});
|
||||
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
RemoveExisting = true
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
ReportWeldingFuelTankCount();
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
RemoveSubObjective(ref refuelObjective);
|
||||
ReportWeldingFuelTankCount();
|
||||
});
|
||||
|
||||
void ReportWeldingFuelTankCount()
|
||||
{
|
||||
@@ -141,7 +145,7 @@ namespace Barotrauma
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () =>
|
||||
{
|
||||
if (Check()) { IsCompleted = true; }
|
||||
if (CheckObjectiveSpecific()) { IsCompleted = true; }
|
||||
else
|
||||
{
|
||||
// Failed to operate. Probably too far.
|
||||
@@ -160,7 +164,7 @@ namespace Barotrauma
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
if (Check()) { IsCompleted = true; }
|
||||
if (CheckObjectiveSpecific()) { IsCompleted = true; }
|
||||
else if ((Leak.WorldPosition - character.WorldPosition).LengthSquared() > MathUtils.Pow(reach * 2, 2))
|
||||
{
|
||||
// Too far
|
||||
@@ -191,7 +195,7 @@ namespace Barotrauma
|
||||
// This is an approximation, because we don't know the exact reach until the pose is taken.
|
||||
// And even then the actual range depends on the direction we are aiming to.
|
||||
// Found out that without any multiplier the value (209) is often too short.
|
||||
return repairTool.Range + armLength * 1.2f;
|
||||
return repairTool.Range + armLength * 1.3f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -6,7 +6,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveFixLeaks : AIObjectiveLoop<Gap>
|
||||
{
|
||||
public override string DebugTag => "fix leaks";
|
||||
public override string Identifier { get; set; } = "fix leaks";
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
@@ -40,7 +40,7 @@ namespace Barotrauma
|
||||
{
|
||||
int totalLeaks = Targets.Count();
|
||||
if (totalLeaks == 0) { return 0; }
|
||||
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated, onlyBots: true);
|
||||
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine, onlyBots: true);
|
||||
bool anyFixers = otherFixers > 0;
|
||||
if (objectiveManager.IsOrder(this))
|
||||
{
|
||||
@@ -51,7 +51,7 @@ namespace Barotrauma
|
||||
{
|
||||
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
|
||||
int leaks = totalLeaks - secondaryLeaks;
|
||||
float ratio = leaks == 0 ? 1 : anyFixers ? leaks / otherFixers : 1;
|
||||
float ratio = leaks == 0 ? 1 : anyFixers ? leaks / (float)otherFixers : 1;
|
||||
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
|
||||
{
|
||||
// Enough fixers
|
||||
@@ -74,7 +74,7 @@ namespace Barotrauma
|
||||
// Don't fix a leak on a wall section set to be ignored
|
||||
if (gap.ConnectedWall != null)
|
||||
{
|
||||
if (gap.ConnectedWall.Sections.Any(s => s.gap == gap && s.IgnoreByAI)) { return false; }
|
||||
if (gap.ConnectedWall.Sections.Any(s => s.gap == gap && s.IgnoreByAI(character))) { return false; }
|
||||
if (gap.ConnectedWall.MaxHealth <= 0.0f) { return false; }
|
||||
}
|
||||
if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; }
|
||||
|
||||
+28
-11
@@ -8,11 +8,10 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveGetItem : AIObjective
|
||||
{
|
||||
public override string DebugTag => "get item";
|
||||
public override string Identifier { get; set; } = "get item";
|
||||
|
||||
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
||||
|
||||
private readonly bool equip;
|
||||
public HashSet<Item> ignoredItems = new HashSet<Item>();
|
||||
|
||||
public Func<Item, float> GetItemPriority;
|
||||
@@ -45,14 +44,17 @@ namespace Barotrauma
|
||||
/// Is the character allowed to take the item from somewhere else than their own sub (e.g. an outpost)
|
||||
/// </summary>
|
||||
public bool AllowStealing { get; set; }
|
||||
|
||||
public bool TakeWholeStack { get; set; }
|
||||
public bool Equip { get; set; }
|
||||
public bool Wear { get; set; }
|
||||
|
||||
public InvSlotType? EquipSlotType { get; set; }
|
||||
|
||||
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
currSearchIndex = -1;
|
||||
this.equip = equip;
|
||||
Equip = equip;
|
||||
originalTarget = targetItem;
|
||||
this.targetItem = targetItem;
|
||||
moveToTarget = targetItem?.GetRootInventoryOwner();
|
||||
@@ -65,7 +67,7 @@ namespace Barotrauma
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
currSearchIndex = -1;
|
||||
this.equip = equip;
|
||||
Equip = equip;
|
||||
this.identifiersOrTags = identifiersOrTags;
|
||||
this.spawnItemIfNotFound = spawnItemIfNotFound;
|
||||
for (int i = 0; i < identifiersOrTags.Length; i++)
|
||||
@@ -197,7 +199,7 @@ namespace Barotrauma
|
||||
|
||||
Inventory itemInventory = targetItem.ParentInventory;
|
||||
var slots = itemInventory?.FindIndices(targetItem);
|
||||
if (HumanAIController.TakeItem(targetItem, character.Inventory, equip, storeUnequipped: true))
|
||||
if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true))
|
||||
{
|
||||
if (TakeWholeStack && slots != null)
|
||||
{
|
||||
@@ -227,7 +229,7 @@ namespace Barotrauma
|
||||
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear, closeEnough: DefaultReach)
|
||||
{
|
||||
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
|
||||
abortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget,
|
||||
AbortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget,
|
||||
SpeakIfFails = false
|
||||
};
|
||||
},
|
||||
@@ -303,6 +305,7 @@ namespace Barotrauma
|
||||
if (rootInventoryOwner is Item ownerItem)
|
||||
{
|
||||
if (!ownerItem.IsInteractable(character)) { continue; }
|
||||
if (!(ownerItem.GetComponent<ItemContainer>()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; }
|
||||
}
|
||||
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
|
||||
float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y);
|
||||
@@ -365,19 +368,33 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
{
|
||||
if (IsCompleted) { return true; }
|
||||
if (targetItem != null)
|
||||
{
|
||||
return character.HasItem(targetItem, equip);
|
||||
if (Equip && EquipSlotType.HasValue)
|
||||
{
|
||||
return character.HasEquippedItem(targetItem, EquipSlotType.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return character.HasItem(targetItem, Equip);
|
||||
}
|
||||
}
|
||||
else if (identifiersOrTags != null)
|
||||
{
|
||||
var matchingItem = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
|
||||
if (matchingItem != null)
|
||||
{
|
||||
return !equip || character.HasEquippedItem(matchingItem);
|
||||
if (Equip && EquipSlotType.HasValue)
|
||||
{
|
||||
return character.HasEquippedItem(matchingItem, EquipSlotType.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return !Equip || character.HasEquippedItem(matchingItem);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -387,7 +404,7 @@ namespace Barotrauma
|
||||
private bool CheckItem(Item item)
|
||||
{
|
||||
if (!item.IsInteractable(character)) { return false; }
|
||||
if (item.IsThisOrAnyContainerIgnoredByAI()) { return false; }
|
||||
if (item.IsThisOrAnyContainerIgnoredByAI(character)) { return false; }
|
||||
if (ignoredItems.Contains(item)) { return false; };
|
||||
if (item.Condition < TargetCondition) { return false; }
|
||||
if (ItemFilter != null && !ItemFilter(item)) { return false; }
|
||||
|
||||
+39
-18
@@ -8,7 +8,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveGoTo : AIObjective
|
||||
{
|
||||
public override string DebugTag => "go to";
|
||||
public override string Identifier { get; set; } = "go to";
|
||||
|
||||
private AIObjectiveFindDivingGear findDivingGear;
|
||||
private readonly bool repeat;
|
||||
@@ -20,10 +20,6 @@ namespace Barotrauma
|
||||
/// Doesn't allow the objective to complete if this condition is false
|
||||
/// </summary>
|
||||
public Func<bool> requiredCondition;
|
||||
/// <summary>
|
||||
/// Aborts the objective when this condition is true
|
||||
/// </summary>
|
||||
public Func<AIObjectiveGoTo, bool> abortCondition;
|
||||
public Func<PathNode, bool> endNodeFilter;
|
||||
|
||||
public Func<float> priorityGetter;
|
||||
@@ -38,6 +34,7 @@ namespace Barotrauma
|
||||
private readonly float minDistance = 50;
|
||||
private readonly float seekGapsInterval = 1;
|
||||
private float seekGapsTimer;
|
||||
private bool cannotFollow;
|
||||
|
||||
/// <summary>
|
||||
/// Display units
|
||||
@@ -81,7 +78,7 @@ namespace Barotrauma
|
||||
|
||||
public float? OverridePriority = null;
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
bool isOrder = objectiveManager.IsOrder(this);
|
||||
if (!IsAllowed)
|
||||
@@ -177,6 +174,11 @@ namespace Barotrauma
|
||||
character.AIController.SteeringManager.Reset();
|
||||
return;
|
||||
}
|
||||
if (cannotFollow)
|
||||
{
|
||||
// Wait
|
||||
character.AIController.SteeringManager.Reset();
|
||||
}
|
||||
waitUntilPathUnreachable -= deltaTime;
|
||||
if (!character.IsClimbing)
|
||||
{
|
||||
@@ -263,16 +265,29 @@ namespace Barotrauma
|
||||
if (findDivingGear != null && !findDivingGear.CanBeCompleted)
|
||||
{
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref findDivingGear));
|
||||
onAbandon: () => Abort(),
|
||||
onCompleted: () =>
|
||||
{
|
||||
cannotFollow = false;
|
||||
RemoveSubObjective(ref findDivingGear);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
|
||||
onCompleted: () => RemoveSubObjective(ref findDivingGear));
|
||||
onAbandon: () => Abort(),
|
||||
onCompleted: () =>
|
||||
{
|
||||
cannotFollow = false;
|
||||
RemoveSubObjective(ref findDivingGear);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
cannotFollow = false;
|
||||
}
|
||||
}
|
||||
if (repeat)
|
||||
{
|
||||
@@ -578,22 +593,15 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
{
|
||||
if (IsCompleted) { return true; }
|
||||
// First check the distance
|
||||
// Then the custom condition
|
||||
// And finally check if can interact (heaviest)
|
||||
// First check the distance and then if can interact (heaviest)
|
||||
if (Target == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return false;
|
||||
}
|
||||
if (abortCondition != null && abortCondition(this))
|
||||
{
|
||||
Abandon = true;
|
||||
return false;
|
||||
}
|
||||
if (repeat)
|
||||
{
|
||||
return false;
|
||||
@@ -624,6 +632,18 @@ namespace Barotrauma
|
||||
return IsCompleted;
|
||||
}
|
||||
|
||||
private void Abort()
|
||||
{
|
||||
if (!objectiveManager.IsOrder(this))
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
cannotFollow = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAbandon()
|
||||
{
|
||||
StopMovement();
|
||||
@@ -657,6 +677,7 @@ namespace Barotrauma
|
||||
findDivingGear = null;
|
||||
seekGapsTimer = 0;
|
||||
TargetGap = null;
|
||||
cannotFollow = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-30
@@ -10,7 +10,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveIdle : AIObjective
|
||||
{
|
||||
public override string DebugTag => "idle";
|
||||
public override string Identifier { get; set; } = "idle";
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
|
||||
@@ -21,11 +21,6 @@ namespace Barotrauma
|
||||
set
|
||||
{
|
||||
behavior = value;
|
||||
if (behavior == BehaviorType.StayInHull && TargetHull == null)
|
||||
{
|
||||
DebugConsole.AddWarning($"Trying to set a character's behavior type to StayInHull, but target hull is not set. {character.Name} ({character.Info.Job.Prefab.Identifier})");
|
||||
behavior = BehaviorType.Passive;
|
||||
}
|
||||
switch (behavior)
|
||||
{
|
||||
case BehaviorType.Passive:
|
||||
@@ -93,7 +88,7 @@ namespace Barotrauma
|
||||
CalculatePriority();
|
||||
}
|
||||
|
||||
protected override bool Check() => false;
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
public override bool CanBeCompleted => true;
|
||||
|
||||
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
|
||||
@@ -110,21 +105,11 @@ namespace Barotrauma
|
||||
Priority = 1;
|
||||
}
|
||||
|
||||
public override float GetPriority() => Priority;
|
||||
protected override float GetPriority() => Priority;
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
//if (objectiveManager.CurrentObjective == this)
|
||||
//{
|
||||
// if (randomTimer > 0)
|
||||
// {
|
||||
// randomTimer -= deltaTime;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// CalculatePriority();
|
||||
// }
|
||||
//}
|
||||
// Do nothing. Overrides the inherited devotion calculations.
|
||||
}
|
||||
|
||||
private float timerMargin;
|
||||
@@ -183,6 +168,11 @@ namespace Barotrauma
|
||||
|
||||
CleanupItems(deltaTime);
|
||||
|
||||
if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null)
|
||||
{
|
||||
TargetHull = character.CurrentHull;
|
||||
}
|
||||
|
||||
if (behavior == BehaviorType.StayInHull)
|
||||
{
|
||||
currentTarget = TargetHull;
|
||||
@@ -203,7 +193,7 @@ namespace Barotrauma
|
||||
|
||||
if (currentTarget != null && !currentTargetIsInvalid)
|
||||
{
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
|
||||
{
|
||||
if (currentTarget.Submarine.TeamID != character.TeamID)
|
||||
{
|
||||
@@ -260,9 +250,9 @@ namespace Barotrauma
|
||||
{
|
||||
//choose a random available hull
|
||||
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
|
||||
bool isInWrongSub = character.TeamID == CharacterTeamType.FriendlyNPC && character.Submarine.TeamID != character.TeamID;
|
||||
bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID;
|
||||
bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: $"AIObjectiveIdle {character.DisplayName}", nodeFilter: node =>
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: null, nodeFilter: node =>
|
||||
{
|
||||
if (node.Waypoint.CurrentHull == null) { return false; }
|
||||
// Check that there is no unsafe or forbidden hulls on the way to the target
|
||||
@@ -419,7 +409,7 @@ namespace Barotrauma
|
||||
if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
|
||||
if (hull.Submarine == null) { continue; }
|
||||
if (character.Submarine == null) { break; }
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
|
||||
{
|
||||
if (hull.Submarine.TeamID != character.TeamID)
|
||||
{
|
||||
@@ -519,13 +509,7 @@ namespace Barotrauma
|
||||
}
|
||||
#endregion
|
||||
|
||||
public static bool IsForbidden(Hull hull)
|
||||
{
|
||||
if (hull == null) { return true; }
|
||||
string hullName = hull.RoomName;
|
||||
if (hullName == null) { return false; }
|
||||
return hullName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || hullName.Contains("airlock", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
public static bool IsForbidden(Hull hull) => hull == null || hull.AvoidStaying;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
|
||||
+2
-2
@@ -40,7 +40,7 @@ namespace Barotrauma
|
||||
: base(character, objectiveManager, priorityModifier, option) { }
|
||||
|
||||
protected override void Act(float deltaTime) { }
|
||||
protected override bool Check() => false;
|
||||
protected override bool CheckObjectiveSpecific() => false;
|
||||
public override bool CanBeCompleted => true;
|
||||
public override bool AbandonWhenCannotCompleteSubjectives => false;
|
||||
public override bool AllowSubObjectiveSorting => true;
|
||||
@@ -106,7 +106,7 @@ namespace Barotrauma
|
||||
UpdateTargets();
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
|
||||
+47
-24
@@ -132,14 +132,14 @@ namespace Barotrauma
|
||||
}
|
||||
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
|
||||
if (order == null) { continue; }
|
||||
if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
|
||||
if ((order.IgnoreAtOutpost || autonomousObjective.ignoreAtOutpost) && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier);
|
||||
var objective = CreateObjective(order, autonomousObjective.option, character, autonomousObjective.priorityModifier);
|
||||
if (objective != null && objective.CanBeCompleted)
|
||||
{
|
||||
AddObjective(objective, delay: Rand.Value() / 2);
|
||||
@@ -184,7 +184,8 @@ namespace Barotrauma
|
||||
{
|
||||
var previousObjective = CurrentObjective;
|
||||
var firstObjective = Objectives.FirstOrDefault();
|
||||
if (CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority)
|
||||
bool currentObjectiveIsOrder = CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority;
|
||||
if (currentObjectiveIsOrder)
|
||||
{
|
||||
CurrentObjective = CurrentOrder;
|
||||
}
|
||||
@@ -197,6 +198,14 @@ namespace Barotrauma
|
||||
previousObjective?.OnDeselected();
|
||||
CurrentObjective?.OnSelected();
|
||||
GetObjective<AIObjectiveIdle>().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0));
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(character, new object[]
|
||||
{
|
||||
NetEntityEvent.Type.ObjectiveManagerState,
|
||||
currentObjectiveIsOrder ? "order" : "objective"
|
||||
});
|
||||
}
|
||||
}
|
||||
return CurrentObjective;
|
||||
}
|
||||
@@ -269,38 +278,29 @@ namespace Barotrauma
|
||||
|
||||
public void SortObjectives()
|
||||
{
|
||||
ForcedOrder?.GetPriority();
|
||||
|
||||
ForcedOrder?.CalculatePriority();
|
||||
AIObjective orderWithHighestPriority = null;
|
||||
float highestPriority = 0;
|
||||
foreach (var currentOrder in CurrentOrders)
|
||||
{
|
||||
var orderObjective = currentOrder.Objective;
|
||||
if (orderObjective == null) { continue; }
|
||||
orderObjective.GetPriority();
|
||||
orderObjective.CalculatePriority();
|
||||
if (orderWithHighestPriority == null || orderObjective.Priority > highestPriority)
|
||||
{
|
||||
orderWithHighestPriority = orderObjective;
|
||||
highestPriority = orderObjective.Priority;
|
||||
}
|
||||
}
|
||||
#if SERVER
|
||||
if (orderWithHighestPriority != null && orderWithHighestPriority != currentOrder)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.ObjectiveManagerOrderState });
|
||||
}
|
||||
#endif
|
||||
CurrentOrder = orderWithHighestPriority;
|
||||
|
||||
for (int i = Objectives.Count - 1; i >= 0; i--)
|
||||
{
|
||||
Objectives[i].GetPriority();
|
||||
Objectives[i].CalculatePriority();
|
||||
}
|
||||
if (Objectives.Any())
|
||||
{
|
||||
Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
|
||||
}
|
||||
|
||||
GetCurrentObjective()?.SortSubObjectives();
|
||||
}
|
||||
|
||||
@@ -380,7 +380,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
var newCurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false);
|
||||
var newCurrentOrder = CreateObjective(order, option, orderGiver);
|
||||
if (newCurrentOrder != null)
|
||||
{
|
||||
CurrentOrders.Add(new OrderInfo(order, option, priority, newCurrentOrder));
|
||||
@@ -441,7 +441,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public AIObjective CreateObjective(Order order, string option, Character orderGiver, bool isAutonomous, float priorityModifier = 1)
|
||||
public AIObjective CreateObjective(Order order, string option, Character orderGiver, float priorityModifier = 1)
|
||||
{
|
||||
if (order == null || order.Identifier == "dismissed") { return null; }
|
||||
AIObjective newObjective;
|
||||
@@ -482,7 +482,6 @@ namespace Barotrauma
|
||||
newObjective = new AIObjectiveRepairItems(character, this, priorityModifier: priorityModifier, prioritizedItem: order.TargetEntity as Item)
|
||||
{
|
||||
RelevantSkill = order.AppropriateSkill,
|
||||
RequireAdequateSkills = isAutonomous
|
||||
};
|
||||
break;
|
||||
case "pumpwater":
|
||||
@@ -492,7 +491,7 @@ namespace Barotrauma
|
||||
newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier)
|
||||
{
|
||||
IsLoop = true,
|
||||
Override = orderGiver != null && orderGiver.IsPlayer
|
||||
Override = orderGiver != null && orderGiver.IsCommanding
|
||||
};
|
||||
// ItemComponent.AIOperate() returns false by default -> We'd have to set IsLoop = false and implement a custom override of AIOperate for the Pump.cs,
|
||||
// if we want that the bot just switches the pump on/off and continues doing something else.
|
||||
@@ -519,7 +518,7 @@ namespace Barotrauma
|
||||
{
|
||||
IsLoop = true,
|
||||
// Don't override unless it's an order by a player
|
||||
Override = orderGiver != null && orderGiver.IsPlayer
|
||||
Override = orderGiver != null && orderGiver.IsCommanding
|
||||
};
|
||||
break;
|
||||
case "setchargepct":
|
||||
@@ -563,6 +562,9 @@ namespace Barotrauma
|
||||
newObjective = new AIObjectiveCleanupItems(character, this, priorityModifier: priorityModifier);
|
||||
}
|
||||
break;
|
||||
case "escapehandcuffs":
|
||||
newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier);
|
||||
break;
|
||||
default:
|
||||
if (order.TargetItemComponent == null) { return null; }
|
||||
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
|
||||
@@ -571,16 +573,22 @@ namespace Barotrauma
|
||||
{
|
||||
IsLoop = true,
|
||||
// Don't override unless it's an order by a player
|
||||
Override = orderGiver != null && orderGiver.IsPlayer
|
||||
Override = orderGiver != null && orderGiver.IsCommanding
|
||||
};
|
||||
if (newObjective.Abandon) { return null; }
|
||||
break;
|
||||
}
|
||||
if (newObjective != null)
|
||||
{
|
||||
newObjective.Identifier = order.Identifier;
|
||||
}
|
||||
newObjective.IgnoreAtOutpost = order.IgnoreAtOutpost;
|
||||
return newObjective;
|
||||
}
|
||||
|
||||
private bool IsAllowedToWait()
|
||||
{
|
||||
if (!character.IsOnPlayerTeam) { return false; }
|
||||
if (HasOrders()) { return false; }
|
||||
if (CurrentObjective is AIObjectiveCombat || CurrentObjective is AIObjectiveFindSafety) { return false; }
|
||||
if (character.AnimController.InWater) { return false; }
|
||||
@@ -606,7 +614,11 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently.
|
||||
/// </summary>
|
||||
public IEnumerable<T> GetActiveObjectives<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T);
|
||||
public IEnumerable<T> GetActiveObjectives<T>() where T : AIObjective
|
||||
{
|
||||
if (CurrentObjective == null) { return Enumerable.Empty<T>(); }
|
||||
return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).Where(so => so is T).Select(so => so as T);
|
||||
}
|
||||
|
||||
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
|
||||
|
||||
@@ -627,7 +639,10 @@ namespace Barotrauma
|
||||
|
||||
public float GetOrderPriority(AIObjective objective)
|
||||
{
|
||||
if (objective == ForcedOrder) { return HighestOrderPriority; }
|
||||
if (objective == ForcedOrder)
|
||||
{
|
||||
return HighestOrderPriority;
|
||||
}
|
||||
var currentOrder = CurrentOrders.FirstOrDefault(o => o.Objective == objective);
|
||||
if (currentOrder.Objective == null)
|
||||
{
|
||||
@@ -635,7 +650,15 @@ namespace Barotrauma
|
||||
}
|
||||
else if (currentOrder.ManualPriority > 0)
|
||||
{
|
||||
return MathHelper.Lerp(LowestOrderPriority, HighestOrderPriority, MathUtils.InverseLerp(1, CharacterInfo.HighestManualOrderPriority, currentOrder.ManualPriority));
|
||||
if (objective.ForceHighestPriority)
|
||||
{
|
||||
return HighestOrderPriority;
|
||||
}
|
||||
if (objective.PrioritizeIfSubObjectivesActive && objective.SubObjectives.Any())
|
||||
{
|
||||
return HighestOrderPriority;
|
||||
}
|
||||
return MathHelper.Lerp(LowestOrderPriority, HighestOrderPriority - 1, MathUtils.InverseLerp(1, CharacterInfo.HighestManualOrderPriority, currentOrder.ManualPriority));
|
||||
}
|
||||
#if DEBUG
|
||||
DebugConsole.AddWarning("Error in order priority: shouldn't return 0!");
|
||||
|
||||
+25
-12
@@ -8,15 +8,18 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveOperateItem : AIObjective
|
||||
{
|
||||
public override string DebugTag => $"operate item {component.Name}";
|
||||
public override string Identifier { get; set; } = "operate item";
|
||||
public override string DebugTag => $"{Identifier} {component.Name}";
|
||||
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
public override bool AllowMultipleInstances => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
public override bool PrioritizeIfSubObjectivesActive => component != null && (component is Reactor || component is Turret);
|
||||
|
||||
private ItemComponent component, controller;
|
||||
private Entity operateTarget;
|
||||
private bool requireEquip;
|
||||
private bool useController;
|
||||
private readonly ItemComponent component, controller;
|
||||
private readonly Entity operateTarget;
|
||||
private readonly bool requireEquip;
|
||||
private readonly bool useController;
|
||||
private AIObjectiveGoTo goToObjective;
|
||||
private AIObjectiveGetItem getItemObjective;
|
||||
|
||||
@@ -34,7 +37,7 @@ namespace Barotrauma
|
||||
public Func<bool> completionCondition;
|
||||
private bool isDoneOperating;
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
bool isOrder = objectiveManager.IsOrder(this);
|
||||
if (!IsAllowed || character.LockHands)
|
||||
@@ -43,7 +46,7 @@ namespace Barotrauma
|
||||
Abandon = !isOrder;
|
||||
return Priority;
|
||||
}
|
||||
if (component.Item.ConditionPercentage <= 0)
|
||||
if (!isOrder && component.Item.ConditionPercentage <= 0)
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
@@ -100,12 +103,22 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (!isOrder)
|
||||
{
|
||||
var steering = component?.Item.GetComponent<Steering>();
|
||||
if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsCaptain)))
|
||||
{
|
||||
// Ignore if already set to autopilot or if there's a captain onboard
|
||||
Priority = 0;
|
||||
return Priority;
|
||||
}
|
||||
}
|
||||
if (targetItem.CurrentHull == null ||
|
||||
targetItem.Submarine != character.Submarine && !isOrder ||
|
||||
targetItem.CurrentHull.FireSources.Any() ||
|
||||
HumanAIController.IsItemOperatedByAnother(target, out _) ||
|
||||
Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))
|
||||
|| component.Item.IgnoreByAI || (useController && controller.Item.IgnoreByAI))
|
||||
|| component.Item.IgnoreByAI(character) || useController && controller.Item.IgnoreByAI(character))
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
@@ -121,10 +134,10 @@ namespace Barotrauma
|
||||
{
|
||||
float value = CumulatedDevotion + (AIObjectiveManager.LowestOrderPriority * PriorityModifier);
|
||||
float max = AIObjectiveManager.LowestOrderPriority - 1;
|
||||
if (reactor != null && reactor.PowerOn && reactor.FissionRate > 1 && Option == "powerup")
|
||||
if (reactor != null && reactor.PowerOn && reactor.FissionRate > 1 && reactor.AutoTemp && Option == "powerup")
|
||||
{
|
||||
// Decrease the priority when targeting a reactor that is already on.
|
||||
value /= 2;
|
||||
// Already on, no need to operate.
|
||||
value = 0;
|
||||
}
|
||||
Priority = MathHelper.Clamp(value, 0, max);
|
||||
}
|
||||
@@ -268,7 +281,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Check() => isDoneOperating && !IsLoop;
|
||||
protected override bool CheckObjectiveSpecific() => isDoneOperating && !IsLoop;
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectivePumpWater : AIObjectiveLoop<Pump>
|
||||
{
|
||||
public override string DebugTag => "pump water";
|
||||
public override string Identifier { get; set; } = "pump water";
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool AllowAutomaticItemUnequipping => true;
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace Barotrauma
|
||||
protected override bool Filter(Pump pump)
|
||||
{
|
||||
if (pump == null) { return false; }
|
||||
if (pump.Item.IgnoreByAI) { return false; }
|
||||
if (pump.Item.IgnoreByAI(character)) { return false; }
|
||||
if (!pump.Item.IsInteractable(character)) { return false; }
|
||||
if (pump.Item.HasTag("ballast")) { return false; }
|
||||
if (pump.Item.Submarine == null) { return false; }
|
||||
|
||||
+27
-13
@@ -1,6 +1,7 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
@@ -8,7 +9,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveRepairItem : AIObjective
|
||||
{
|
||||
public override string DebugTag => "repair item";
|
||||
public override string Identifier { get; set; } = "repair item";
|
||||
|
||||
public override bool AllowInAnySub => true;
|
||||
|
||||
@@ -31,9 +32,9 @@ namespace Barotrauma
|
||||
this.isPriority = isPriority;
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed || Item.IgnoreByAI)
|
||||
if (!IsAllowed || Item.IgnoreByAI(character))
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
@@ -43,11 +44,10 @@ namespace Barotrauma
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
// TODO: priority list?
|
||||
// Ignore items that are being repaired by someone else.
|
||||
if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character))
|
||||
if (HumanAIController.IsItemRepairedByAnother(Item, out _))
|
||||
{
|
||||
Priority = 0;
|
||||
IsCompleted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -66,12 +66,25 @@ namespace Barotrauma
|
||||
float devotion = (CumulatedDevotion + selectedBonus) / 100;
|
||||
float reduction = isPriority ? 1 : isSelected ? 2 : 3;
|
||||
float max = AIObjectiveManager.LowestOrderPriority - reduction;
|
||||
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
|
||||
float highestWeight = -1;
|
||||
foreach (string tag in Item.Prefab.Tags)
|
||||
{
|
||||
if (JobPrefab.ItemRepairPriorities.TryGetValue(tag, out float weight) && weight > highestWeight)
|
||||
{
|
||||
highestWeight = weight;
|
||||
}
|
||||
}
|
||||
if (highestWeight == -1)
|
||||
{
|
||||
// Predefined weight not found.
|
||||
highestWeight = 1;
|
||||
}
|
||||
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * highestWeight * PriorityModifier), 0, 1));
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
{
|
||||
IsCompleted = Item.IsFullCondition;
|
||||
if (character.IsOnPlayerTeam && IsCompleted && IsRepairing())
|
||||
@@ -122,8 +135,6 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
HumanAIController.UnequipContainedItems(repairTool.Item, it => !it.HasTag("weldingfuel"));
|
||||
HumanAIController.UnequipEmptyItems(repairTool.Item);
|
||||
RelatedItem item = null;
|
||||
Item fuel = null;
|
||||
foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained])
|
||||
@@ -135,9 +146,12 @@ namespace Barotrauma
|
||||
if (fuel == null)
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
|
||||
onCompleted: () => RemoveSubObjective(ref refuelObjective),
|
||||
onAbandon: () => Abandon = true);
|
||||
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
RemoveExisting = true
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref refuelObjective),
|
||||
onAbandon: () => Abandon = true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
+24
-25
@@ -9,31 +9,26 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveRepairItems : AIObjectiveLoop<Item>
|
||||
{
|
||||
public override string DebugTag => "repair items";
|
||||
|
||||
/// <summary>
|
||||
/// Should the character only attempt to fix items they have the skills to fix, or any damaged item
|
||||
/// </summary>
|
||||
public bool RequireAdequateSkills;
|
||||
public override string Identifier { get; set; } = "repair items";
|
||||
|
||||
/// <summary>
|
||||
/// If set, only fix items where required skill matches this.
|
||||
/// </summary>
|
||||
public string RelevantSkill;
|
||||
|
||||
private readonly Item prioritizedItem;
|
||||
public Item PrioritizedItem { get; private set; }
|
||||
|
||||
public override bool AllowMultipleInstances => true;
|
||||
public override bool AllowInAnySub => true;
|
||||
|
||||
public readonly static float RequiredSuccessFactor = 0.4f;
|
||||
|
||||
public override bool IsDuplicate<T>(T otherObjective) => otherObjective is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills;
|
||||
public override bool IsDuplicate<T>(T otherObjective) => otherObjective is AIObjectiveRepairItems repairObjective && objectiveManager.IsOrder(repairObjective) == objectiveManager.IsOrder(this);
|
||||
|
||||
public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Item prioritizedItem = null)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
this.prioritizedItem = prioritizedItem;
|
||||
PrioritizedItem = prioritizedItem;
|
||||
}
|
||||
|
||||
protected override void CreateObjectives()
|
||||
@@ -69,25 +64,36 @@ namespace Barotrauma
|
||||
|
||||
protected override bool Filter(Item item)
|
||||
{
|
||||
if (!IsValidTarget(item, character)) { return false; }
|
||||
if (item.CurrentHull.FireSources.Count > 0) { return false; }
|
||||
// Don't repair items in rooms that have enemies inside.
|
||||
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
|
||||
if (!ViableForRepair(item, character, HumanAIController)) { return false; };
|
||||
if (!Objectives.ContainsKey(item))
|
||||
{
|
||||
if (item != character.SelectedConstruction)
|
||||
{
|
||||
float condition = item.ConditionPercentage;
|
||||
if (item.Repairables.All(r => condition >= r.RepairThreshold)) { return false; }
|
||||
if (NearlyFullCondition(item)) { return false; }
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(RelevantSkill))
|
||||
{
|
||||
if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier.Equals(RelevantSkill, StringComparison.OrdinalIgnoreCase)))) { return false; }
|
||||
}
|
||||
return !HumanAIController.IsItemRepairedByAnother(item, out _);
|
||||
}
|
||||
|
||||
public static bool ViableForRepair(Item item, Character character, HumanAIController humanAIController)
|
||||
{
|
||||
if (!IsValidTarget(item, character)) { return false; }
|
||||
if (item.CurrentHull.FireSources.Count > 0) { return false; }
|
||||
// Don't repair items in rooms that have enemies inside.
|
||||
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !humanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool NearlyFullCondition(Item item)
|
||||
{
|
||||
float condition = item.ConditionPercentage;
|
||||
return item.Repairables.All(r => condition >= r.RepairThreshold);
|
||||
}
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
var selectedItem = character.SelectedConstruction;
|
||||
@@ -115,14 +121,7 @@ namespace Barotrauma
|
||||
// Enough fixers
|
||||
return 0;
|
||||
}
|
||||
if (RequireAdequateSkills)
|
||||
{
|
||||
return Targets.Sum(t => GetTargetPriority(t, character, RequiredSuccessFactor)) * ratio;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
|
||||
}
|
||||
return Targets.Sum(t => GetTargetPriority(t, character, RequiredSuccessFactor)) * ratio;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +139,7 @@ namespace Barotrauma
|
||||
protected override IEnumerable<Item> GetList() => Item.ItemList;
|
||||
|
||||
protected override AIObjective ObjectiveConstructor(Item item)
|
||||
=> new AIObjectiveRepairItem(character, item, objectiveManager, priorityModifier: PriorityModifier, isPriority: item == prioritizedItem);
|
||||
=> new AIObjectiveRepairItem(character, item, objectiveManager, priorityModifier: PriorityModifier, isPriority: item == PrioritizedItem);
|
||||
|
||||
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
|
||||
=> HumanAIController.RemoveTargets<AIObjectiveRepairItems, Item>(character, target);
|
||||
@@ -148,7 +147,7 @@ namespace Barotrauma
|
||||
public static bool IsValidTarget(Item item, Character character)
|
||||
{
|
||||
if (item == null) { return false; }
|
||||
if (item.IgnoreByAI) { return false; }
|
||||
if (item.IgnoreByAI(character)) { return false; }
|
||||
if (!item.IsInteractable(character)) { return false; }
|
||||
if (item.IsFullCondition) { return false; }
|
||||
if (item.CurrentHull == null) { return false; }
|
||||
|
||||
+4
-3
@@ -9,7 +9,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveRescue : AIObjective
|
||||
{
|
||||
public override string DebugTag => "rescue";
|
||||
public override string Identifier { get; set; } = "rescue";
|
||||
public override bool ForceRun => true;
|
||||
public override bool KeepDivingGearOn => true;
|
||||
|
||||
@@ -374,7 +374,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool Check()
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
{
|
||||
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
|
||||
{
|
||||
@@ -390,6 +390,7 @@ namespace Barotrauma
|
||||
bool isCompleted =
|
||||
AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter) ||
|
||||
targetCharacter.CharacterHealth.GetAllAfflictions().All(a => a.Strength < a.Prefab.TreatmentThreshold);
|
||||
|
||||
if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name),
|
||||
@@ -398,7 +399,7 @@ namespace Barotrauma
|
||||
return isCompleted;
|
||||
}
|
||||
|
||||
public override float GetPriority()
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
{
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveRescueAll : AIObjectiveLoop<Character>
|
||||
{
|
||||
public override string DebugTag => "rescue all";
|
||||
public override string Identifier { get; set; } = "rescue all";
|
||||
public override bool ForceRun => true;
|
||||
public override bool InverseTargetEvaluation => true;
|
||||
public override bool AllowOutsideSubmarine => true;
|
||||
|
||||
@@ -150,16 +150,21 @@ namespace Barotrauma
|
||||
//legacy support
|
||||
public readonly string[] AppropriateJobs;
|
||||
public readonly string[] Options;
|
||||
public readonly string[] HiddenOptions;
|
||||
public readonly string[] AllOptions;
|
||||
private readonly Dictionary<string, string> OptionNames;
|
||||
|
||||
public readonly Dictionary<string, Sprite> OptionSprites;
|
||||
|
||||
private readonly Dictionary<string, Sprite> minimapIcons;
|
||||
public Dictionary<string, Sprite> MinimapIcons => IsPrefab ? minimapIcons : Prefab.minimapIcons;
|
||||
|
||||
public readonly bool MustSetTarget;
|
||||
/// <summary>
|
||||
/// Can the order be turned into a non-entity-targeting one if it was originally created with a target entity.
|
||||
/// Note: if MustSetTarget is true, CanBeGeneralized will always be false.
|
||||
/// </summary>
|
||||
public readonly bool CanBeGeneralized;
|
||||
public readonly string AppropriateSkill;
|
||||
public readonly bool Hidden;
|
||||
public readonly bool IgnoreAtOutpost;
|
||||
|
||||
public bool HasOptions => (IsPrefab ? Options : Prefab.Options).Length > 1;
|
||||
public bool IsPrefab { get; private set; }
|
||||
@@ -307,11 +312,15 @@ namespace Barotrauma
|
||||
TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false);
|
||||
AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]);
|
||||
Options = orderElement.GetAttributeStringArray("options", new string[0]);
|
||||
HiddenOptions = orderElement.GetAttributeStringArray("hiddenoptions", new string[0]);
|
||||
AllOptions = Options.Concat(HiddenOptions).ToArray();
|
||||
var category = orderElement.GetAttributeString("category", null);
|
||||
if (!string.IsNullOrWhiteSpace(category)) { this.Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); }
|
||||
MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false);
|
||||
CanBeGeneralized = !MustSetTarget && orderElement.GetAttributeBool("canbegeneralized", true);
|
||||
AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null);
|
||||
Hidden = orderElement.GetAttributeBool("hidden", false);
|
||||
IgnoreAtOutpost = orderElement.GetAttributeBool("ignoreatoutpost", false);
|
||||
|
||||
var optionNames = TextManager.Get("OrderOptions." + Identifier, true)?.Split(',', ',') ??
|
||||
orderElement.GetAttributeStringArray("optionnames", new string[0]);
|
||||
@@ -348,15 +357,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
minimapIcons = new Dictionary<string, Sprite>();
|
||||
var minimapIconElements = orderElement.GetChildElements("minimapicon");
|
||||
foreach (XElement minimapIconElement in minimapIconElements)
|
||||
{
|
||||
var id = minimapIconElement.GetAttributeString("id", null);
|
||||
if (string.IsNullOrWhiteSpace(id)) { continue; }
|
||||
minimapIcons.Add(id, new Sprite(minimapIconElement.GetChildElement("sprite"), lazyLoad: true));
|
||||
}
|
||||
|
||||
IsPrefab = true;
|
||||
MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false);
|
||||
IsIgnoreOrder = Identifier == "ignorethis" || Identifier == "unignorethis";
|
||||
@@ -366,7 +366,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Constructor for order instances
|
||||
/// </summary>
|
||||
public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false)
|
||||
public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null)
|
||||
{
|
||||
Prefab = prefab.Prefab ?? prefab;
|
||||
|
||||
@@ -384,12 +384,14 @@ namespace Barotrauma
|
||||
AppropriateJobs = prefab.AppropriateJobs;
|
||||
FadeOutTime = prefab.FadeOutTime;
|
||||
MustSetTarget = prefab.MustSetTarget;
|
||||
CanBeGeneralized = prefab.CanBeGeneralized;
|
||||
AppropriateSkill = prefab.AppropriateSkill;
|
||||
Category = prefab.Category;
|
||||
MustManuallyAssign = prefab.MustManuallyAssign;
|
||||
IsIgnoreOrder = prefab.IsIgnoreOrder;
|
||||
DrawIconWhenContained = prefab.DrawIconWhenContained;
|
||||
Hidden = prefab.Hidden;
|
||||
IgnoreAtOutpost = prefab.IgnoreAtOutpost;
|
||||
|
||||
OrderGiver = orderGiver;
|
||||
TargetEntity = targetEntity;
|
||||
@@ -413,12 +415,18 @@ namespace Barotrauma
|
||||
IsPrefab = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for order instances
|
||||
/// </summary>
|
||||
public Order(Order prefab, OrderTarget target, Character orderGiver = null) : this(prefab, targetEntity: null, targetItem: null, orderGiver)
|
||||
{
|
||||
TargetPosition = target;
|
||||
TargetType = OrderTargetType.Position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for order instances
|
||||
/// </summary>
|
||||
public Order(Order prefab, Structure wall, int? sectionIndex, Character orderGiver = null) : this(prefab, targetEntity: wall, null, orderGiver: orderGiver)
|
||||
{
|
||||
WallSectionIndex = sectionIndex;
|
||||
@@ -487,27 +495,19 @@ namespace Barotrauma
|
||||
if (submarine == null) { return matchingItems; }
|
||||
if (ItemComponentType != null || TargetItems.Length > 0)
|
||||
{
|
||||
matchingItems = TargetItems.Length > 0 ?
|
||||
Item.ItemList.FindAll(it => TargetItems.Contains(it.Prefab.Identifier) || it.HasTag(TargetItems)) :
|
||||
Item.ItemList.FindAll(it => TryGetTargetItemComponent(it, out _));
|
||||
if (mustBelongToPlayerSub)
|
||||
foreach (var item in Item.ItemList)
|
||||
{
|
||||
matchingItems.RemoveAll(it => it.Submarine?.Info != null && it.Submarine.Info.Type != SubmarineType.Player);
|
||||
}
|
||||
matchingItems.RemoveAll(it => it.Submarine != submarine && !submarine.DockedTo.Contains(it.Submarine));
|
||||
if (requiredTeam.HasValue)
|
||||
{
|
||||
matchingItems.RemoveAll(it => it.Submarine == null || it.Submarine.TeamID != requiredTeam.Value);
|
||||
}
|
||||
matchingItems.RemoveAll(it => it.NonInteractable);
|
||||
if (UseController)
|
||||
{
|
||||
matchingItems.RemoveAll(i => i.Components.None(c => c.GetType() == ItemComponentType) && !i.TryFindController(out _));
|
||||
}
|
||||
if (interactableFor != null)
|
||||
{
|
||||
matchingItems.RemoveAll(it => !it.IsInteractable(interactableFor) ||
|
||||
(UseController && it.FindController() is Controller c && !c.Item.IsInteractable(interactableFor)));
|
||||
if (TargetItems.Length > 0 && !TargetItems.Contains(item.Prefab.Identifier) && !item.HasTag(TargetItems)) { continue; }
|
||||
if (TargetItems.Length == 0 && !TryGetTargetItemComponent(item, out _)) { continue; }
|
||||
if (mustBelongToPlayerSub && item.Submarine?.Info != null && item.Submarine.Info.Type != SubmarineType.Player) { continue; }
|
||||
if (item.Submarine != submarine && !submarine.DockedTo.Contains(item.Submarine)) { continue; }
|
||||
if (requiredTeam.HasValue && (item.Submarine == null || item.Submarine.TeamID != requiredTeam.Value)) { continue; }
|
||||
if (item.NonInteractable) { continue; }
|
||||
if (ItemComponentType != null && item.Components.None(c => c.GetType() == ItemComponentType)) { continue; }
|
||||
Controller controller = null;
|
||||
if (UseController && !item.TryFindController(out controller)) { continue; }
|
||||
if (interactableFor != null && (!item.IsInteractable(interactableFor) || (UseController && !controller.Item.IsInteractable(interactableFor)))) { continue; }
|
||||
matchingItems.Add(item);
|
||||
}
|
||||
}
|
||||
return matchingItems;
|
||||
@@ -525,7 +525,15 @@ namespace Barotrauma
|
||||
|
||||
public string GetOptionName(string id)
|
||||
{
|
||||
return Prefab == null ? OptionNames[id] : Prefab.OptionNames[id];
|
||||
if (Prefab == null)
|
||||
{
|
||||
if (OptionNames.ContainsKey(id)) { return OptionNames[id]; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Prefab.OptionNames.ContainsKey(id)) { return Prefab.OptionNames[id]; }
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string GetOptionName(int index)
|
||||
|
||||
@@ -410,7 +410,10 @@ namespace Barotrauma
|
||||
if (end.state == 0 || end.Parent == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage("Path not found. " + errorMsgStr, Color.Yellow);
|
||||
if (errorMsgStr != null)
|
||||
{
|
||||
DebugConsole.NewMessage("Path not found. " + errorMsgStr, Color.Yellow);
|
||||
}
|
||||
#endif
|
||||
return new SteeringPath(true);
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ namespace Barotrauma
|
||||
if (c.Inventory != null)
|
||||
{
|
||||
var inventoryElement = new XElement("inventory");
|
||||
c.SaveInventory(c.Inventory, inventoryElement);
|
||||
Character.SaveInventory(c.Inventory, inventoryElement);
|
||||
petElement.Add(inventoryElement);
|
||||
}
|
||||
|
||||
@@ -400,7 +400,7 @@ namespace Barotrauma
|
||||
spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition;
|
||||
}
|
||||
var pet = Character.Create(speciesName, spawnPos, seed);
|
||||
var petBehavior = (pet.AIController as EnemyAIController)?.PetBehavior;
|
||||
var petBehavior = (pet?.AIController as EnemyAIController)?.PetBehavior;
|
||||
if (petBehavior != null)
|
||||
{
|
||||
petBehavior.Owner = owner;
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
abstract class ShipIssueWorker
|
||||
{
|
||||
public const float MaxImportance = 100f;
|
||||
public const float MinImportance = 0f;
|
||||
public Order SuggestedOrderPrefab { get; }
|
||||
|
||||
private float importance;
|
||||
public float Importance
|
||||
{
|
||||
get
|
||||
{
|
||||
return importance;
|
||||
}
|
||||
set
|
||||
{
|
||||
importance = MathHelper.Clamp(value, MinImportance, MaxImportance);
|
||||
}
|
||||
}
|
||||
public float CurrentRedundancy { get; set; }
|
||||
|
||||
public readonly ShipCommandManager shipCommandManager;
|
||||
public string Option { get; set; }
|
||||
public Character OrderedCharacter { get; set; }
|
||||
public Order CurrentOrder { get; private set; }
|
||||
public ItemComponent TargetItemComponent { get; protected set; }
|
||||
public Item TargetItem { get; protected set; }
|
||||
public bool Active { get; protected set; } = true; // used to turn off the instance if errors are detected
|
||||
|
||||
protected virtual Character CommandingCharacter => shipCommandManager.character;
|
||||
public virtual float TimeSinceLastAttempt { get; set; }
|
||||
public virtual float RedundantIssueModifier => 0.5f;
|
||||
public virtual bool StopDuringEmergency => true; // limit certain issue assessments when invaded by the enemies
|
||||
public virtual bool AllowEasySwitching => false;
|
||||
|
||||
public ShipIssueWorker(ShipCommandManager shipCommandManager, Order suggestedOrderPrefab, string option = null)
|
||||
{
|
||||
this.shipCommandManager = shipCommandManager;
|
||||
SuggestedOrderPrefab = suggestedOrderPrefab;
|
||||
Option = option;
|
||||
}
|
||||
|
||||
public void SetOrder(Character orderedCharacter)
|
||||
{
|
||||
OrderedCharacter = orderedCharacter;
|
||||
if (orderedCharacter != CommandingCharacter)
|
||||
{
|
||||
CommandingCharacter.Speak(SuggestedOrderPrefab.GetChatMessage(OrderedCharacter.Name, "", false));
|
||||
}
|
||||
|
||||
// not sure if new orders are supposed to be created each time. TODO m61: check later
|
||||
CurrentOrder = new Order(SuggestedOrderPrefab, TargetItem, TargetItemComponent, CommandingCharacter);
|
||||
OrderedCharacter.SetOrder(CurrentOrder, Option, priority: 3, CommandingCharacter, CommandingCharacter != OrderedCharacter);
|
||||
TimeSinceLastAttempt = 0f;
|
||||
}
|
||||
|
||||
public void RemoveOrder()
|
||||
{
|
||||
OrderedCharacter = null;
|
||||
CurrentOrder = null;
|
||||
}
|
||||
|
||||
protected virtual bool IsIssueViable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public float CalculateImportance(bool isEmergency)
|
||||
{
|
||||
Importance = 0f; // reset anything that needs resetting
|
||||
|
||||
if (!Active)
|
||||
{
|
||||
return Importance;
|
||||
}
|
||||
|
||||
Active = IsIssueViable();
|
||||
|
||||
if (isEmergency && StopDuringEmergency)
|
||||
{
|
||||
return Importance;
|
||||
}
|
||||
|
||||
CalculateImportanceSpecific();
|
||||
|
||||
// if there are other orders of the same type already being attended to, such as fixing leaks
|
||||
// reduce the relative importance of this issue
|
||||
CurrentRedundancy = 1f;
|
||||
foreach (ShipIssueWorker shipIssueWorker in shipCommandManager.ShipIssueWorkers)
|
||||
{
|
||||
if (shipIssueWorker.GetType() == GetType() && shipIssueWorker != this && shipIssueWorker.OrderAttendedTo())
|
||||
{
|
||||
CurrentRedundancy *= RedundantIssueModifier;
|
||||
}
|
||||
}
|
||||
Importance *= CurrentRedundancy;
|
||||
|
||||
return Importance;
|
||||
}
|
||||
|
||||
public bool OrderAttendedTo(float timeSinceLastCheck = 0f)
|
||||
{
|
||||
if (!HumanAIController.IsActive(OrderedCharacter))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// accept only the highest priority order
|
||||
if (CurrentOrder != null && OrderedCharacter.GetCurrentOrderWithTopPriority()?.Order != CurrentOrder)
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandManager.ShipCommandLog($"Order {CurrentOrder.Name} did not match current order for character {OrderedCharacter} in {this}");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!shipCommandManager.AbleToTakeOrder(OrderedCharacter))
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandManager.ShipCommandLog(OrderedCharacter + " was unable to perform assigned order in " + this);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public abstract void CalculateImportanceSpecific();
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class ShipGlobalIssueFixLeaks : ShipGlobalIssue
|
||||
{
|
||||
readonly List<float> hullSeverities = new List<float>();
|
||||
public ShipGlobalIssueFixLeaks(ShipCommandManager shipCommandManager) : base(shipCommandManager) { }
|
||||
public override void CalculateGlobalIssue()
|
||||
{
|
||||
hullSeverities.Clear();
|
||||
|
||||
foreach (Gap gap in Gap.GapList)
|
||||
{
|
||||
if (AIObjectiveFixLeaks.IsValidTarget(gap, shipCommandManager.character))
|
||||
{
|
||||
hullSeverities.Add(AIObjectiveFixLeaks.GetLeakSeverity(gap));
|
||||
}
|
||||
}
|
||||
|
||||
float averagePercentage = 0f;
|
||||
if (hullSeverities.Any())
|
||||
{
|
||||
hullSeverities.Sort();
|
||||
averagePercentage = hullSeverities.TakeLast(3).Average(); // get the 3 most damaged items on the ship and get their average
|
||||
}
|
||||
GlobalImportance = averagePercentage;
|
||||
}
|
||||
}
|
||||
|
||||
class ShipIssueWorkerFixLeaks : ShipIssueWorkerGlobal
|
||||
{
|
||||
public override bool StopDuringEmergency => false;
|
||||
public ShipIssueWorkerFixLeaks(ShipCommandManager shipCommandManager, Order order, ShipGlobalIssueFixLeaks shipGlobalIssueFixLeaks) : base(shipCommandManager, order, shipGlobalIssueFixLeaks) { }
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
namespace Barotrauma
|
||||
{
|
||||
abstract class ShipGlobalIssue
|
||||
{
|
||||
public float GlobalImportance { get; set; }
|
||||
|
||||
protected ShipCommandManager shipCommandManager;
|
||||
public ShipGlobalIssue(ShipCommandManager shipCommandManager)
|
||||
{
|
||||
this.shipCommandManager = shipCommandManager;
|
||||
}
|
||||
public abstract void CalculateGlobalIssue();
|
||||
}
|
||||
|
||||
abstract class ShipIssueWorkerGlobal : ShipIssueWorker
|
||||
{
|
||||
private readonly ShipGlobalIssue shipGlobalIssue;
|
||||
|
||||
public ShipIssueWorkerGlobal(ShipCommandManager shipCommandManager, Order suggestedOrderPrefab, ShipGlobalIssue shipGlobalIssue) : base (shipCommandManager, suggestedOrderPrefab)
|
||||
{
|
||||
this.shipGlobalIssue = shipGlobalIssue;
|
||||
}
|
||||
|
||||
public override void CalculateImportanceSpecific() // importances for global issues are precalculated, so that they don't need to be calculated per each attending character
|
||||
{
|
||||
Importance = shipGlobalIssue.GlobalImportance;
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
using Barotrauma.Items.Components;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
abstract class ShipIssueWorkerItem : ShipIssueWorker
|
||||
{
|
||||
public ShipIssueWorkerItem(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option = null) : base(shipCommandManager, order, option)
|
||||
{
|
||||
TargetItemComponent = targetItemComponent;
|
||||
TargetItem = targetItem;
|
||||
}
|
||||
|
||||
protected override bool IsIssueViable()
|
||||
{
|
||||
if (TargetItemComponent == null)
|
||||
{
|
||||
DebugConsole.ThrowError("TargetItemComponent was null in " + this);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TargetItem == null)
|
||||
{
|
||||
DebugConsole.ThrowError("TargetItem was null in " + this);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class ShipIssueWorkerOperateWeapons : ShipIssueWorkerItem
|
||||
{
|
||||
public override float RedundantIssueModifier => 0.65f;
|
||||
private readonly List<float> targetingImportances = new List<float>();
|
||||
|
||||
public override bool AllowEasySwitching => true;
|
||||
|
||||
public ShipIssueWorkerOperateWeapons(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent) : base(shipCommandManager, order, targetItem, targetItemComponent) { }
|
||||
|
||||
float GetTargetingImportance(Entity entity)
|
||||
{
|
||||
float currentDistanceToEnemy = Vector2.Distance(entity.WorldPosition, TargetItem.WorldPosition);
|
||||
return MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MinImportance, MaxImportance);
|
||||
}
|
||||
|
||||
public override void CalculateImportanceSpecific()
|
||||
{
|
||||
if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot()) { return; }
|
||||
|
||||
targetingImportances.Clear();
|
||||
foreach (Character character in shipCommandManager.EnemyCharacters)
|
||||
{
|
||||
targetingImportances.Add(GetTargetingImportance(character));
|
||||
}
|
||||
// there should maybe be additional logic for targeting and destroying spires, because they currently cause some issues with pathing
|
||||
|
||||
if (targetingImportances.Any())
|
||||
{
|
||||
targetingImportances.Sort();
|
||||
Importance = targetingImportances.TakeLast(3).Average();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
using Barotrauma.Items.Components;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class ShipIssueWorkerPowerUpReactor : ShipIssueWorkerItem
|
||||
{
|
||||
public ShipIssueWorkerPowerUpReactor(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option) : base(shipCommandManager, order, targetItem, targetItemComponent, option)
|
||||
{
|
||||
}
|
||||
|
||||
public override void CalculateImportanceSpecific()
|
||||
{
|
||||
if (TargetItem.Condition <= 0f) { return; }
|
||||
|
||||
if (TargetItemComponent is Reactor reactor && -reactor.CurrPowerConsumption < float.Epsilon)
|
||||
{
|
||||
Importance = 40f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class ShipGlobalIssueRepairSystems : ShipGlobalIssue
|
||||
{
|
||||
readonly List<Item> itemsNeedingRepair = new List<Item>();
|
||||
|
||||
public ShipGlobalIssueRepairSystems(ShipCommandManager shipCommandManager) : base(shipCommandManager) { }
|
||||
|
||||
public override void CalculateGlobalIssue()
|
||||
{
|
||||
itemsNeedingRepair.Clear();
|
||||
|
||||
foreach (Item item in shipCommandManager.CommandedSubmarine.GetItems(true))
|
||||
{
|
||||
if (!AIObjectiveRepairItems.ViableForRepair(item, shipCommandManager.character, shipCommandManager.character.AIController as HumanAIController)) { continue; }
|
||||
if (AIObjectiveRepairItems.NearlyFullCondition(item)) { continue; }
|
||||
itemsNeedingRepair.Add(item);
|
||||
// merged this logic with AIObjectiveRepairItems
|
||||
}
|
||||
|
||||
if (itemsNeedingRepair.Any())
|
||||
{
|
||||
itemsNeedingRepair.Sort((x, y) => y.ConditionPercentage.CompareTo(x.ConditionPercentage));
|
||||
float modifiedPercentage = itemsNeedingRepair.TakeLast(3).Average(x => x.ConditionPercentage) * 0.6f + itemsNeedingRepair.TakeLast(10).Average(x => x.ConditionPercentage) * 0.4f;
|
||||
// calculate a modified percentage with the most damaged items, with 60% the weight given to the top 3 damaged and the remaining given to top 10
|
||||
GlobalImportance = 100 - modifiedPercentage;
|
||||
}
|
||||
// this system works reasonably well, though it could give extra importance to repairing critical items like reactors and junction boxes
|
||||
}
|
||||
}
|
||||
|
||||
class ShipIssueWorkerRepairSystems : ShipIssueWorkerGlobal // this class could be removed, but it might need special behavior later
|
||||
{
|
||||
public ShipIssueWorkerRepairSystems(ShipCommandManager shipCommandManager, Order order, ShipGlobalIssueRepairSystems shipGlobalIssueRepairSystems) : base(shipCommandManager, order, shipGlobalIssueRepairSystems)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
using Barotrauma.Items.Components;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class ShipIssueWorkerSteer : ShipIssueWorkerItem
|
||||
{
|
||||
// The AI could be set to steer automatically through a specialized job or autonomous objectives
|
||||
// but the logic involved doesn't really allow that without some annoyingly specific changes
|
||||
// hence the AI will command itself to steer if steering is not being taken care of or the target location is wrong
|
||||
public ShipIssueWorkerSteer(ShipCommandManager shipCommandManager, Order order, Item targetItem, ItemComponent targetItemComponent, string option) : base(shipCommandManager, order, targetItem, targetItemComponent, option) { }
|
||||
public override void CalculateImportanceSpecific()
|
||||
{
|
||||
if (shipCommandManager.NavigationState == ShipCommandManager.NavigationStates.Inactive) { return; }
|
||||
if (TargetItemComponent is Powered powered && powered.Voltage <= powered.MinVoltage) { return; }
|
||||
if (TargetItem.Condition <= 0f) { return; }
|
||||
|
||||
Importance = 70f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class ShipCommandManager
|
||||
{
|
||||
public readonly Character character;
|
||||
public readonly HumanAIController humanAIController;
|
||||
|
||||
private bool active;
|
||||
public bool Active
|
||||
{
|
||||
get { return active; }
|
||||
set
|
||||
{
|
||||
active = value ? TryInitializeShipCommandManager() : value;
|
||||
}
|
||||
}
|
||||
|
||||
public Submarine EnemySubmarine
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Submarine CommandedSubmarine
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
private Steering steering;
|
||||
public readonly List<Vector2> patrolPositions = new List<Vector2>();
|
||||
public enum NavigationStates
|
||||
{
|
||||
Inactive,
|
||||
Patrol,
|
||||
Aggressive
|
||||
}
|
||||
|
||||
public NavigationStates NavigationState { get; private set; } = NavigationStates.Inactive;
|
||||
|
||||
float navigationTimer = 0f;
|
||||
private readonly float navigationInterval = 4f;
|
||||
|
||||
float timeUntilRam;
|
||||
private const float RamTimerMax = 17.5f;
|
||||
|
||||
public readonly List<ShipIssueWorker> ShipIssueWorkers = new List<ShipIssueWorker>();
|
||||
private const float MinimumIssueThreshold = 10f;
|
||||
private const float IssueDevotionBuffer = 5f;
|
||||
|
||||
private float decisionTimer = 6f;
|
||||
private readonly float decisionInterval = 6f;
|
||||
|
||||
private float timeSinceLastCommandDecision;
|
||||
private float timeSinceLastNavigation;
|
||||
|
||||
public readonly List<Character> AlliedCharacters = new List<Character>();
|
||||
public readonly List<Character> EnemyCharacters = new List<Character>();
|
||||
|
||||
private readonly List<ShipIssueWorker> attendedIssues = new List<ShipIssueWorker>();
|
||||
private readonly List<ShipIssueWorker> availableIssues = new List<ShipIssueWorker>();
|
||||
private readonly List<ShipGlobalIssue> shipGlobalIssues = new List<ShipGlobalIssue>();
|
||||
|
||||
public ShipCommandManager(Character character)
|
||||
{
|
||||
this.character = character;
|
||||
humanAIController = character.AIController as HumanAIController;
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
if (!Active) { return; }
|
||||
decisionTimer -= deltaTime;
|
||||
if (decisionTimer <= 0.0f)
|
||||
{
|
||||
UpdateCommandDecision(timeSinceLastCommandDecision);
|
||||
decisionTimer = decisionInterval * Rand.Range(0.8f, 1.2f);
|
||||
timeSinceLastCommandDecision = decisionTimer;
|
||||
}
|
||||
|
||||
navigationTimer -= deltaTime;
|
||||
if (navigationTimer <= 0.0f)
|
||||
{
|
||||
UpdateNavigation(timeSinceLastNavigation);
|
||||
navigationTimer = navigationInterval * Rand.Range(0.8f, 1.2f);
|
||||
timeSinceLastNavigation = navigationTimer;
|
||||
}
|
||||
}
|
||||
|
||||
public static void ShipCommandLog(string text)
|
||||
{
|
||||
if (GameSettings.VerboseLogging)
|
||||
{
|
||||
DebugConsole.NewMessage(text);
|
||||
}
|
||||
}
|
||||
|
||||
static bool WithinRange(float range, float distanceSquared)
|
||||
{
|
||||
return range * range > distanceSquared;
|
||||
}
|
||||
|
||||
void UpdateNavigation(float timeSinceLastUpdate)
|
||||
{
|
||||
if (steering == null || EnemySubmarine == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float distanceSquaredEnemy = Vector2.DistanceSquared(CommandedSubmarine.WorldPosition, EnemySubmarine.WorldPosition);
|
||||
|
||||
if (NavigationState != NavigationStates.Aggressive)
|
||||
{
|
||||
if (WithinRange(7000f, distanceSquaredEnemy))
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandLog("Ship " + CommandedSubmarine + " was within the aggro range of " + EnemySubmarine);
|
||||
#endif
|
||||
NavigationState = NavigationStates.Aggressive;
|
||||
}
|
||||
else if (WithinRange(40000f, distanceSquaredEnemy))
|
||||
{
|
||||
NavigationState = NavigationStates.Patrol;
|
||||
}
|
||||
}
|
||||
|
||||
if (NavigationState == NavigationStates.Aggressive)
|
||||
{
|
||||
steering.AITacticalTarget = EnemySubmarine.WorldPosition;
|
||||
if (WithinRange(8500f, distanceSquaredEnemy) && !WithinRange(1500f, distanceSquaredEnemy)) // if we are within enemy ship's range for ramTimerMax, try to ram them instead (if we're not already very close)
|
||||
{
|
||||
if (steering.AIRamTimer > 0f)
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandLog("Ship " + CommandedSubmarine + " was still ramming, " + steering.AIRamTimer + " left");
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
timeUntilRam -= timeSinceLastUpdate;
|
||||
#if DEBUG
|
||||
ShipCommandLog("Ship " + CommandedSubmarine + " was close enough to ram, " + timeUntilRam + " left until ramming");
|
||||
#endif
|
||||
|
||||
if (timeUntilRam <= 0f)
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandLog("Ship " + CommandedSubmarine + " is attempting to ram!");
|
||||
#endif
|
||||
steering.AIRamTimer = 50f;
|
||||
timeUntilRam = RamTimerMax * Rand.Range(0.9f, 1.1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
steering.AIRamTimer = 0f;
|
||||
timeUntilRam = RamTimerMax * Rand.Range(0.9f, 1.1f);
|
||||
}
|
||||
}
|
||||
else if (patrolPositions.Any())
|
||||
{
|
||||
float distanceSquaredPatrol = Vector2.DistanceSquared(CommandedSubmarine.WorldPosition, patrolPositions.First());
|
||||
|
||||
if (WithinRange(7000f, distanceSquaredPatrol))
|
||||
{
|
||||
Vector2 lastPosition = patrolPositions.First();
|
||||
patrolPositions.RemoveAt(0);
|
||||
patrolPositions.Add(lastPosition);
|
||||
}
|
||||
steering.AITacticalTarget = patrolPositions.First();
|
||||
}
|
||||
}
|
||||
|
||||
public bool AbleToTakeOrder(Character character)
|
||||
{
|
||||
return !character.IsIncapacitated && !character.LockHands && character.Submarine == CommandedSubmarine;
|
||||
}
|
||||
|
||||
void UpdateCommandDecision(float timeSinceLastUpdate)
|
||||
{
|
||||
|
||||
#if DEBUG
|
||||
ShipCommandLog("Updating command for character " + character);
|
||||
#endif
|
||||
|
||||
shipGlobalIssues.ForEach(c => c.CalculateGlobalIssue());
|
||||
|
||||
AlliedCharacters.Clear();
|
||||
EnemyCharacters.Clear();
|
||||
|
||||
bool isEmergency = false;
|
||||
|
||||
foreach (Character potentialCharacter in Character.CharacterList)
|
||||
{
|
||||
if (!HumanAIController.IsActive(character)) { continue; }
|
||||
|
||||
if (HumanAIController.IsFriendly(character, potentialCharacter, true) && potentialCharacter.AIController is HumanAIController)
|
||||
{
|
||||
if (AbleToTakeOrder(potentialCharacter))
|
||||
{
|
||||
AlliedCharacters.Add(potentialCharacter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
EnemyCharacters.Add(potentialCharacter);
|
||||
if (potentialCharacter.Submarine == CommandedSubmarine) // if enemies are on board, don't issue normal orders anymore
|
||||
{
|
||||
isEmergency = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attendedIssues.Clear();
|
||||
availableIssues.Clear();
|
||||
|
||||
foreach (ShipIssueWorker shipIssueWorker in ShipIssueWorkers)
|
||||
{
|
||||
float importance = shipIssueWorker.CalculateImportance(isEmergency);
|
||||
if (shipIssueWorker.OrderAttendedTo(timeSinceLastUpdate))
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it was already being attended by " + shipIssueWorker.OrderedCharacter);
|
||||
#endif
|
||||
attendedIssues.Add(shipIssueWorker);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandLog("Current importance for " + shipIssueWorker + " was " + importance + " and it is not attended to");
|
||||
#endif
|
||||
shipIssueWorker.RemoveOrder();
|
||||
availableIssues.Add(shipIssueWorker);
|
||||
}
|
||||
}
|
||||
|
||||
availableIssues.Sort((x, y) => y.Importance.CompareTo(x.Importance));
|
||||
attendedIssues.Sort((x, y) => x.Importance.CompareTo(y.Importance));
|
||||
|
||||
ShipIssueWorker mostImportantIssue = availableIssues.FirstOrDefault();
|
||||
|
||||
float bestValue = 0f;
|
||||
Character bestCharacter = null;
|
||||
|
||||
if (mostImportantIssue != null && mostImportantIssue.Importance > MinimumIssueThreshold)
|
||||
{
|
||||
IEnumerable<Character> bestCharacters = CrewManager.GetCharactersSortedForOrder(mostImportantIssue.SuggestedOrderPrefab, AlliedCharacters, character, true);
|
||||
|
||||
foreach (Character orderedCharacter in bestCharacters)
|
||||
{
|
||||
float issueApplicability = mostImportantIssue.Importance;
|
||||
|
||||
// prefer not to switch if not qualified
|
||||
issueApplicability *= mostImportantIssue.SuggestedOrderPrefab.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 1f : 0.75f;
|
||||
|
||||
ShipIssueWorker occupiedIssue = attendedIssues.FirstOrDefault(i => i.OrderedCharacter == orderedCharacter);
|
||||
|
||||
if (occupiedIssue != null)
|
||||
{
|
||||
if (occupiedIssue.GetType() == mostImportantIssue.GetType() && mostImportantIssue is ShipIssueWorkerGlobal && occupiedIssue is ShipIssueWorkerGlobal)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// reverse redundancy to ensure certain issues can be switched over easily (operating weapons)
|
||||
if (mostImportantIssue.AllowEasySwitching && occupiedIssue.AllowEasySwitching)
|
||||
{
|
||||
issueApplicability /= mostImportantIssue.CurrentRedundancy;
|
||||
}
|
||||
|
||||
// give slight preference if not qualified for current job
|
||||
issueApplicability += occupiedIssue.SuggestedOrderPrefab.AppropriateJobs.Contains(orderedCharacter.Info.Job.Prefab.Identifier) ? 0 : 7.5f;
|
||||
|
||||
// prefer not to switch orders unless considerably more important
|
||||
issueApplicability -= IssueDevotionBuffer;
|
||||
|
||||
if (issueApplicability + IssueDevotionBuffer < occupiedIssue.Importance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// prefer first one in bestCharacters in tiebreakers
|
||||
if (issueApplicability > bestValue)
|
||||
{
|
||||
bestValue = issueApplicability;
|
||||
bestCharacter = orderedCharacter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestCharacter != null && mostImportantIssue != null)
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandLog("Setting " + mostImportantIssue + " for character " + bestCharacter);
|
||||
#endif
|
||||
mostImportantIssue.SetOrder(bestCharacter);
|
||||
}
|
||||
else // if we didn't give an order, let's try to dismiss someone instead
|
||||
{
|
||||
foreach (ShipIssueWorker shipIssueWorker in ShipIssueWorkers)
|
||||
{
|
||||
if (shipIssueWorker.Importance <= 0f && shipIssueWorker.OrderAttendedTo())
|
||||
{
|
||||
#if DEBUG
|
||||
ShipCommandLog("Dismissing " + shipIssueWorker + " for character " + shipIssueWorker.OrderedCharacter);
|
||||
#endif
|
||||
Order orderPrefab = Order.GetPrefab("dismissed");
|
||||
character.Speak(orderPrefab.GetChatMessage(shipIssueWorker.OrderedCharacter.Name, "", givingOrderToSelf: false));
|
||||
shipIssueWorker.OrderedCharacter.SetOrder(Order.GetPrefab("dismissed"), orderOption: null, priority: 3, character);
|
||||
shipIssueWorker.RemoveOrder();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool TryInitializeShipCommandManager()
|
||||
{
|
||||
CommandedSubmarine = character.Submarine;
|
||||
|
||||
if (CommandedSubmarine == null)
|
||||
{
|
||||
DebugConsole.ThrowError("TryInitializeShipCommandManager failed: CommandedSubmarine was null for character " + character);
|
||||
return false;
|
||||
}
|
||||
|
||||
EnemySubmarine = Submarine.MainSubs[0] == CommandedSubmarine ? Submarine.MainSubs[1] : Submarine.MainSubs[0];
|
||||
|
||||
if (EnemySubmarine == null)
|
||||
{
|
||||
DebugConsole.ThrowError("TryInitializeShipCommandManager failed: EnemySubmarine was null for character " + character);
|
||||
return false;
|
||||
}
|
||||
|
||||
timeUntilRam = RamTimerMax * Rand.Range(0.9f, 1.1f);
|
||||
|
||||
ShipIssueWorkers.Clear();
|
||||
|
||||
// could have support for multiple reactors, todo m61
|
||||
if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
|
||||
{
|
||||
ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, Order.GetPrefab("operatereactor"), reactor.Item, reactor, "powerup"));
|
||||
}
|
||||
|
||||
if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("navterminal") && !i.NonInteractable) is Item nav && nav.GetComponent<Steering>() is Steering steeringComponent)
|
||||
{
|
||||
steering = steeringComponent;
|
||||
ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, Order.GetPrefab("steer"), nav, steeringComponent, "navigatetactical"));
|
||||
}
|
||||
|
||||
foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret")))
|
||||
{
|
||||
ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, Order.GetPrefab("operateweapons"), item, item.GetComponent<Turret>()));
|
||||
}
|
||||
|
||||
int crewSizeModifier = 2;
|
||||
// these issueworkers revolve around a singular, shared issue, which is injected into them to prevent redundant calculations
|
||||
ShipGlobalIssueFixLeaks shipGlobalIssueFixLeaks = new ShipGlobalIssueFixLeaks(this);
|
||||
for (int i = 0; i < crewSizeModifier; i++)
|
||||
{
|
||||
ShipIssueWorkers.Add(new ShipIssueWorkerFixLeaks(this, Order.GetPrefab("fixleaks"), shipGlobalIssueFixLeaks));
|
||||
}
|
||||
shipGlobalIssues.Add(shipGlobalIssueFixLeaks);
|
||||
|
||||
ShipGlobalIssueRepairSystems shipGlobalIssueRepairSystems = new ShipGlobalIssueRepairSystems(this);
|
||||
for (int i = 0; i < crewSizeModifier; i++)
|
||||
{
|
||||
ShipIssueWorkers.Add(new ShipIssueWorkerRepairSystems(this, Order.GetPrefab("repairsystems"), shipGlobalIssueRepairSystems));
|
||||
}
|
||||
shipGlobalIssues.Add(shipGlobalIssueRepairSystems);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,14 @@ namespace Barotrauma
|
||||
|
||||
private static bool IsThalamus(MapEntityPrefab entityPrefab, string tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag);
|
||||
|
||||
public WreckAI(Submarine wreck)
|
||||
public static WreckAI Create(Submarine wreck)
|
||||
{
|
||||
var wreckAI = new WreckAI(wreck);
|
||||
if (wreckAI.Config == null) { return null; }
|
||||
return wreckAI;
|
||||
}
|
||||
|
||||
private WreckAI(Submarine wreck)
|
||||
{
|
||||
Wreck = wreck;
|
||||
Config = WreckAIConfig.GetRandom();
|
||||
@@ -55,37 +62,59 @@ namespace Barotrauma
|
||||
}
|
||||
allItems = Wreck.GetItems(false);
|
||||
thalamusItems = allItems.FindAll(i => IsThalamus(i.prefab));
|
||||
var hulls = Wreck.GetHulls(false);
|
||||
hulls.AddRange(Wreck.GetHulls(false));
|
||||
var potentialBrainHulls = new Dictionary<Hull, float>();
|
||||
brain = new Item(brainPrefab, Vector2.Zero, Wreck);
|
||||
thalamusItems.Add(brain);
|
||||
Vector2 negativeMargin = new Vector2(40, 20);
|
||||
Vector2 minSize = brain.Rect.Size.ToVector2() - negativeMargin;
|
||||
Vector2 maxSize = new Vector2(brain.Rect.Width * 3, brain.Rect.Height * 3);
|
||||
// First try to get a room that is not too big and not in the edges of the sub.
|
||||
// Also try not to create the brain in a room that already have carrier items inside.
|
||||
// Ignore hulls that have any linked hulls to keep the calculations simple.
|
||||
Point minSize = brain.Rect.Size.Multiply(brain.Scale);
|
||||
// Bigger hulls are allowed, but not preferred more than what's sufficent.
|
||||
Vector2 sufficentSize = new Vector2(minSize.X * 2, minSize.Y * 1.1f);
|
||||
// Shrink the horizontal axis so that the brain is not placed in the left or right side, where we often have curved walls.
|
||||
// Also ignore hulls that have open gaps, because we'll want the room to be full of water. The room will be filled with water when the brain is inserted in the room.
|
||||
Rectangle shrinkedBounds = ToolBox.GetWorldBounds(Wreck.WorldPosition.ToPoint(), new Point(Wreck.Borders.Width - 500, Wreck.Borders.Height));
|
||||
bool BaseCondition(Hull h) => h.RectWidth > minSize.X && h.RectHeight > minSize.Y && h.GetLinkedEntities<Hull>().None() && h.ConnectedGaps.None(g => g.Open > 0);
|
||||
bool IsNotTooBig(Hull h) => h.RectWidth < maxSize.X && h.RectHeight < maxSize.Y;
|
||||
bool IsNotInFringes(Hull h) => shrinkedBounds.ContainsWorld(h.WorldRect);
|
||||
bool DoesNotContainOtherItems(Hull h) => thalamusItems.None(i => i.CurrentHull == h);
|
||||
Hull brainHull = hulls.GetRandom(h => BaseCondition(h) && IsNotTooBig(h) && IsNotInFringes(h) && DoesNotContainOtherItems(h), Rand.RandSync.Server);
|
||||
if (brainHull == null)
|
||||
foreach (Hull hull in hulls)
|
||||
{
|
||||
brainHull = hulls.GetRandom(h => BaseCondition(h) && IsNotInFringes(h) && DoesNotContainOtherItems(h), Rand.RandSync.Server);
|
||||
}
|
||||
if (brainHull == null)
|
||||
{
|
||||
brainHull = hulls.GetRandom(h => BaseCondition(h) && (IsNotInFringes(h) || DoesNotContainOtherItems(h)), Rand.RandSync.Server);
|
||||
}
|
||||
if (brainHull == null)
|
||||
{
|
||||
brainHull = hulls.GetRandom(BaseCondition, Rand.RandSync.Server);
|
||||
float distanceFromCenter = Vector2.Distance(Wreck.WorldPosition, hull.WorldPosition);
|
||||
float distanceFactor = MathHelper.Lerp(1.0f, 0.5f, MathUtils.InverseLerp(0, Math.Max(shrinkedBounds.Width, shrinkedBounds.Height) / 2, distanceFromCenter));
|
||||
float horizontalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.X, sufficentSize.X, hull.Rect.Width));
|
||||
float verticalSizeFactor = MathHelper.Lerp(0.5f, 1.0f, MathUtils.InverseLerp(minSize.Y, sufficentSize.Y, hull.Rect.Height));
|
||||
float weight = verticalSizeFactor * horizontalSizeFactor * distanceFactor;
|
||||
if (hull.GetLinkedEntities<Hull>().Any())
|
||||
{
|
||||
// Ignore hulls that have any linked hulls to keep the calculations simple.
|
||||
continue;
|
||||
}
|
||||
else if (hull.ConnectedGaps.Any(g => g.Open > 0 && (!g.IsRoomToRoom || g.Position.Y < hull.Position.Y)))
|
||||
{
|
||||
// Ignore hulls that have open gaps to outside or below the center point, because we'll want the room to be full of water and not be accessible without breaking the wall.
|
||||
continue;
|
||||
}
|
||||
else if (thalamusItems.Any(i => i.CurrentHull == hull))
|
||||
{
|
||||
// Don't create the brain in a room that already has thalamus items inside it.
|
||||
continue;
|
||||
}
|
||||
else if (hull.Rect.Width < minSize.X || hull.Rect.Height < minSize.Y)
|
||||
{
|
||||
// Don't select too small rooms.
|
||||
continue;
|
||||
}
|
||||
if (weight > 0)
|
||||
{
|
||||
potentialBrainHulls.TryAdd(hull, weight);
|
||||
}
|
||||
}
|
||||
Hull brainHull = ToolBox.SelectWeightedRandom(potentialBrainHulls.Keys.ToList(), potentialBrainHulls.Values.ToList(), Rand.RandSync.Server);
|
||||
var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(p => IsThalamus(p));
|
||||
if (brainHull == null) { return; }
|
||||
if (brainHull == null)
|
||||
{
|
||||
DebugConsole.AddWarning("Wreck AI: Cannot find a proper room for the brain. Using a random room.");
|
||||
brainHull = hulls.GetRandom(Rand.RandSync.Server);
|
||||
}
|
||||
if (brainHull == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Wreck AI: Cannot find any room for the brain! Failed to create the Thalamus.");
|
||||
return;
|
||||
}
|
||||
brainHull.WaterVolume = brainHull.Volume;
|
||||
brain.SetTransform(brainHull.SimPosition, rotation: 0, findNewHull: false);
|
||||
brain.CurrentHull = brainHull;
|
||||
@@ -158,11 +187,12 @@ namespace Barotrauma
|
||||
if (!spawnOrgans.Contains(item))
|
||||
{
|
||||
spawnOrgans.Add(item);
|
||||
// Try to flood the hull so that the spawner won't die.
|
||||
item.CurrentHull.WaterVolume = item.CurrentHull.Volume;
|
||||
}
|
||||
}
|
||||
}
|
||||
wayPoints.AddRange(Wreck.GetWaypoints(false));
|
||||
hulls.AddRange(Wreck.GetHulls(false));
|
||||
IsAlive = true;
|
||||
thalamusStructures = GetThalamusEntities<Structure>(Wreck, Config.Entity).ToList();
|
||||
}
|
||||
@@ -307,9 +337,16 @@ namespace Barotrauma
|
||||
|
||||
public static void RemoveThalamusItems(Submarine wreck)
|
||||
{
|
||||
List<MapEntity> thalamusItems = new List<MapEntity>();
|
||||
foreach (var wreckAiConfig in WreckAIConfig.List)
|
||||
{
|
||||
GetThalamusEntities(wreck, wreckAiConfig.Entity).ForEachMod(e => e.Remove());
|
||||
thalamusItems.AddRange(GetThalamusEntities(wreck, wreckAiConfig.Entity));
|
||||
}
|
||||
thalamusItems = thalamusItems.Distinct().ToList();
|
||||
foreach (MapEntity thalamusItem in thalamusItems)
|
||||
{
|
||||
thalamusItem.Remove();
|
||||
wreck.PhysicsBody.FarseerBody.FixtureList.Where(f => f.UserData == thalamusItem).ForEachMod(f => wreck.PhysicsBody.FarseerBody.Remove(f));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,15 +360,16 @@ namespace Barotrauma
|
||||
private int MaxCellsPerRoom => CalculateCellCount(1, Config.MaxAgentsPerRoom);
|
||||
private int MinCellsOutside => CalculateCellCount(0, Config.MinAgentsOutside);
|
||||
private int MaxCellsOutside => CalculateCellCount(0, Config.MaxAgentsOutside);
|
||||
private int MinCellsInside => CalculateCellCount(2, Config.MinAgentsInside);
|
||||
private int MaxCellsInside => CalculateCellCount(3, Config.MaxAgentsInside);
|
||||
private int MinCellsInside => CalculateCellCount(3, Config.MinAgentsInside);
|
||||
private int MaxCellsInside => CalculateCellCount(5, Config.MaxAgentsInside);
|
||||
private int MaxCellCount => CalculateCellCount(5, Config.MaxAgentCount);
|
||||
private float MinWaterLevel => Config.MinWaterLevel;
|
||||
|
||||
private int CalculateCellCount(int minValue, int maxValue)
|
||||
{
|
||||
if (maxValue == 0) { return 0; }
|
||||
return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, Level.Loaded.Difficulty * 0.01f * Config.AgentSpawnCountDifficultyMultiplier));
|
||||
float t = MathUtils.InverseLerp(0, 100, Level.Loaded.Difficulty * Config.AgentSpawnCountDifficultyMultiplier);
|
||||
return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, t));
|
||||
}
|
||||
|
||||
private float GetSpawnTime()
|
||||
|
||||
+9
-7
@@ -270,14 +270,16 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
LimbJoint rightWrist = GetJointBetweenLimbs(LimbType.RightForearm, LimbType.RightHand);
|
||||
if (rightWrist != null)
|
||||
{
|
||||
forearmLength = Vector2.Distance(
|
||||
rightElbow.LimbA.type == LimbType.RightForearm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB,
|
||||
rightWrist.LimbA.type == LimbType.RightForearm ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB);
|
||||
|
||||
forearmLength = Vector2.Distance(
|
||||
rightElbow.LimbA.type == LimbType.RightForearm ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB,
|
||||
rightWrist.LimbA.type == LimbType.RightForearm ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB);
|
||||
|
||||
forearmLength += Vector2.Distance(
|
||||
rightHand.PullJointLocalAnchorA,
|
||||
rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB);
|
||||
forearmLength += Vector2.Distance(
|
||||
rightHand.PullJointLocalAnchorA,
|
||||
rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasMultipleLimbsOfSameType => Limbs.Length > limbDictionary.Count;
|
||||
public bool HasMultipleLimbsOfSameType => limbs == null ? false : Limbs.Length > limbDictionary.Count;
|
||||
|
||||
private bool frozen;
|
||||
public bool Frozen
|
||||
@@ -416,10 +416,7 @@ namespace Barotrauma
|
||||
|
||||
protected void CreateColliders()
|
||||
{
|
||||
if (collider != null)
|
||||
{
|
||||
collider.ForEach(c => c.Remove());
|
||||
}
|
||||
collider?.ForEach(c => c.Remove());
|
||||
DebugConsole.Log($"Creating colliders from {RagdollParams.Name}.");
|
||||
collider = new List<PhysicsBody>();
|
||||
foreach (var cParams in RagdollParams.Colliders)
|
||||
@@ -479,10 +476,7 @@ namespace Barotrauma
|
||||
|
||||
protected void CreateLimbs()
|
||||
{
|
||||
if (limbs != null)
|
||||
{
|
||||
limbs.ForEach(l => l.Remove());
|
||||
}
|
||||
limbs?.ForEach(l => l.Remove());
|
||||
DebugConsole.Log($"Creating limbs from {RagdollParams.Name}.");
|
||||
limbDictionary = new Dictionary<LimbType, Limb>();
|
||||
limbs = new Limb[RagdollParams.Limbs.Count];
|
||||
@@ -1547,6 +1541,7 @@ namespace Barotrauma
|
||||
case Physics.CollisionLevel:
|
||||
if (!fixture.CollidesWith.HasFlag(Physics.CollisionCharacter)) { return -1; }
|
||||
if (fixture.Body.UserData is Submarine && character.Submarine != null) { return -1; }
|
||||
if (fixture.IsSensor) { return -1; }
|
||||
if (fraction < standOnFloorFraction)
|
||||
{
|
||||
standOnFloorFraction = fraction;
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace Barotrauma
|
||||
public readonly Limb HitLimb;
|
||||
|
||||
public readonly List<DamageModifier> AppliedDamageModifiers;
|
||||
|
||||
|
||||
public AttackResult(List<Affliction> afflictions, Limb hitLimb, List<DamageModifier> appliedDamageModifiers = null)
|
||||
{
|
||||
HitLimb = hitLimb;
|
||||
@@ -137,6 +137,9 @@ namespace Barotrauma
|
||||
set => _itemDamage = value;
|
||||
}
|
||||
|
||||
[Serialize(0.0f, true, description: "Percentage of damage mitigation ignored when hitting armored body parts (deflecting limbs)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1f)]
|
||||
public float Penetration { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Currently only used with variants. Used for multiplying all the damage.
|
||||
/// </summary>
|
||||
@@ -304,7 +307,7 @@ namespace Barotrauma
|
||||
return totalDamage * DamageMultiplier;
|
||||
}
|
||||
|
||||
public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f)
|
||||
public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f, float penetration = 0f)
|
||||
{
|
||||
if (damage > 0.0f) Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage), null);
|
||||
if (bleedingDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage), null);
|
||||
@@ -314,6 +317,7 @@ namespace Barotrauma
|
||||
DamageRange = range;
|
||||
StructureDamage = LevelWallDamage = structureDamage;
|
||||
ItemDamage = itemDamage;
|
||||
Penetration = Penetration;
|
||||
}
|
||||
|
||||
public Attack(XElement element, string parentDebugName)
|
||||
@@ -391,14 +395,12 @@ namespace Barotrauma
|
||||
Affliction affliction;
|
||||
string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant();
|
||||
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase));
|
||||
if (afflictionPrefab != null)
|
||||
if (afflictionPrefab == null)
|
||||
{
|
||||
affliction = afflictionPrefab.Instantiate(0.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
affliction = new Affliction(null, 0);
|
||||
DebugConsole.ThrowError($"Couldn't find the affliction with the identifier {afflictionIdentifier} referenced in {element.Document.ParseContentPathFromUri()}");
|
||||
continue;
|
||||
}
|
||||
affliction = afflictionPrefab.Instantiate(0.0f);
|
||||
affliction.Deserialize(subElement);
|
||||
//backwards compatibility
|
||||
if (subElement.Attribute("amount") != null && subElement.Attribute("strength") == null)
|
||||
@@ -478,8 +480,8 @@ namespace Barotrauma
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
||||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
var targets = new List<ISerializableEntity>();
|
||||
effect.GetNearbyTargets(worldPosition, targets);
|
||||
targets.Clear();
|
||||
targets.AddRange(effect.GetNearbyTargets(worldPosition, targets));
|
||||
effect.Apply(effectType, deltaTime, targetEntity, targets);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
|
||||
@@ -492,6 +494,7 @@ namespace Barotrauma
|
||||
return attackResult;
|
||||
}
|
||||
|
||||
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
||||
public AttackResult DoDamageToLimb(Character attacker, Limb targetLimb, Vector2 worldPosition, float deltaTime, bool playSound = true, PhysicsBody sourceBody = null)
|
||||
{
|
||||
if (targetLimb == null)
|
||||
@@ -511,7 +514,7 @@ namespace Barotrauma
|
||||
|
||||
DamageParticles(deltaTime, worldPosition);
|
||||
|
||||
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb);
|
||||
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration: Penetration);
|
||||
var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure;
|
||||
|
||||
foreach (StatusEffect effect in statusEffects)
|
||||
@@ -536,8 +539,8 @@ namespace Barotrauma
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
||||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
var targets = new List<ISerializableEntity>();
|
||||
effect.GetNearbyTargets(worldPosition, targets);
|
||||
targets.Clear();
|
||||
targets.AddRange(effect.GetNearbyTargets(worldPosition, targets));
|
||||
effect.Apply(effectType, deltaTime, targetLimb.character, targets);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
|
||||
|
||||
@@ -94,7 +94,13 @@ namespace Barotrauma
|
||||
|
||||
public bool IsLocalPlayer => Controlled == this;
|
||||
public bool IsPlayer => Controlled == this || IsRemotePlayer;
|
||||
|
||||
/// <summary>
|
||||
/// Is the character player or does it have an active ship command manager (an AI controlled sub)? Bots in the player team are not treated as commanders.
|
||||
/// </summary>
|
||||
public bool IsCommanding => IsPlayer || (AIController is HumanAIController humanAI && humanAI.ShipCommandManager != null && humanAI.ShipCommandManager.Active);
|
||||
public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled;
|
||||
public bool IsEscorted { get; set; }
|
||||
|
||||
public readonly Dictionary<string, SerializableProperty> Properties;
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties
|
||||
@@ -109,6 +115,8 @@ namespace Barotrauma
|
||||
|
||||
protected Key[] keys;
|
||||
|
||||
public HumanPrefab Prefab;
|
||||
|
||||
private CharacterTeamType teamID;
|
||||
public CharacterTeamType TeamID
|
||||
{
|
||||
@@ -120,6 +128,101 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly Dictionary<string, ActiveTeamChange> activeTeamChanges = new Dictionary<string, ActiveTeamChange>();
|
||||
protected ActiveTeamChange currentTeamChange;
|
||||
const string OriginalTeamIdentifier = "original";
|
||||
|
||||
public void SetOriginalTeam(CharacterTeamType newTeam)
|
||||
{
|
||||
TryRemoveTeamChange(OriginalTeamIdentifier);
|
||||
currentTeamChange = new ActiveTeamChange(newTeam, ActiveTeamChange.TeamChangePriorities.Base);
|
||||
TryAddNewTeamChange(OriginalTeamIdentifier, currentTeamChange);
|
||||
}
|
||||
|
||||
protected void ChangeTeam(CharacterTeamType newTeam)
|
||||
{
|
||||
if (newTeam == teamID)
|
||||
{
|
||||
return;
|
||||
}
|
||||
teamID = newTeam;
|
||||
if (info != null) { info.TeamID = newTeam; }
|
||||
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// clear up any duties the character might have had from its old team (autonomous objectives are automatically recreated)
|
||||
SetOrder(Order.GetPrefab("dismissed"), orderOption: null, priority: 3, orderGiver: this, speak: false);
|
||||
|
||||
#if SERVER
|
||||
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.TeamChange });
|
||||
#endif
|
||||
}
|
||||
|
||||
public bool HasTeamChange(string identifier)
|
||||
{
|
||||
return activeTeamChanges.ContainsKey(identifier);
|
||||
}
|
||||
|
||||
public bool TryAddNewTeamChange(string identifier, ActiveTeamChange newTeamChange)
|
||||
{
|
||||
bool success = activeTeamChanges.TryAdd(identifier, newTeamChange);
|
||||
if (success)
|
||||
{
|
||||
if (currentTeamChange == null)
|
||||
{
|
||||
// set team logic to use active team changes as soon as the first team change is added
|
||||
SetOriginalTeam(TeamID);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError("Tried to add an existing team change! Make sure to check if the team change exists first.");
|
||||
#endif
|
||||
}
|
||||
return success;
|
||||
}
|
||||
public bool TryRemoveTeamChange(string identifier)
|
||||
{
|
||||
if (activeTeamChanges.TryGetValue(identifier, out ActiveTeamChange removedTeamChange))
|
||||
{
|
||||
if (currentTeamChange == removedTeamChange)
|
||||
{
|
||||
currentTeamChange = activeTeamChanges[OriginalTeamIdentifier];
|
||||
}
|
||||
}
|
||||
return activeTeamChanges.Remove(identifier);
|
||||
}
|
||||
|
||||
public void UpdateTeam()
|
||||
{
|
||||
if (currentTeamChange == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ActiveTeamChange bestTeamChange = currentTeamChange;
|
||||
foreach (var desiredTeamChange in activeTeamChanges) // order of iteration matters because newest is preferred when multiple same-priority team changes exist
|
||||
{
|
||||
if (bestTeamChange.TeamChangePriority < desiredTeamChange.Value.TeamChangePriority)
|
||||
{
|
||||
bestTeamChange = desiredTeamChange.Value;
|
||||
}
|
||||
}
|
||||
if (TeamID != bestTeamChange.DesiredTeamId)
|
||||
{
|
||||
ChangeTeam(bestTeamChange.DesiredTeamId);
|
||||
currentTeamChange = bestTeamChange;
|
||||
|
||||
if (bestTeamChange.AggressiveBehavior) // this seemed like the least disruptive way to induce aggressive behavior
|
||||
{
|
||||
SetOrder(Order.GetPrefab("fightintruders"), orderOption: null, priority: 3, orderGiver: this, speak: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsOnPlayerTeam => TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2;
|
||||
|
||||
public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator;
|
||||
@@ -336,9 +439,10 @@ namespace Barotrauma
|
||||
private Action<Character, Character> onCustomInteract;
|
||||
public ConversationAction ActiveConversation;
|
||||
|
||||
public bool RequireConsciousnessForCustomInteract = true;
|
||||
public bool AllowCustomInteract
|
||||
{
|
||||
get { return !IsIncapacitated && Stun <= 0.0f && !Removed; }
|
||||
get { return (!RequireConsciousnessForCustomInteract || (!IsIncapacitated && Stun <= 0.0f)) && !Removed; }
|
||||
}
|
||||
|
||||
private float lockHandsTimer;
|
||||
@@ -919,7 +1023,6 @@ namespace Barotrauma
|
||||
{
|
||||
teamID = Info.TeamID;
|
||||
}
|
||||
|
||||
keys = new Key[Enum.GetNames(typeof(InputType)).Length];
|
||||
for (int i = 0; i < Enum.GetNames(typeof(InputType)).Length; i++)
|
||||
{
|
||||
@@ -1119,17 +1222,17 @@ namespace Barotrauma
|
||||
switch (inputType)
|
||||
{
|
||||
case InputType.Left:
|
||||
return !(dequeuedInput.HasFlag(InputNetFlags.Left)) && (prevDequeuedInput.HasFlag(InputNetFlags.Left));
|
||||
return dequeuedInput.HasFlag(InputNetFlags.Left) && !prevDequeuedInput.HasFlag(InputNetFlags.Left);
|
||||
case InputType.Right:
|
||||
return !(dequeuedInput.HasFlag(InputNetFlags.Right)) && (prevDequeuedInput.HasFlag(InputNetFlags.Right));
|
||||
return dequeuedInput.HasFlag(InputNetFlags.Right) && !prevDequeuedInput.HasFlag(InputNetFlags.Right);
|
||||
case InputType.Up:
|
||||
return !(dequeuedInput.HasFlag(InputNetFlags.Up)) && (prevDequeuedInput.HasFlag(InputNetFlags.Up));
|
||||
return dequeuedInput.HasFlag(InputNetFlags.Up) && !prevDequeuedInput.HasFlag(InputNetFlags.Up);
|
||||
case InputType.Down:
|
||||
return !(dequeuedInput.HasFlag(InputNetFlags.Down)) && (prevDequeuedInput.HasFlag(InputNetFlags.Down));
|
||||
return dequeuedInput.HasFlag(InputNetFlags.Down) && !prevDequeuedInput.HasFlag(InputNetFlags.Down);
|
||||
case InputType.Run:
|
||||
return !(dequeuedInput.HasFlag(InputNetFlags.Run)) && (prevDequeuedInput.HasFlag(InputNetFlags.Run));
|
||||
return dequeuedInput.HasFlag(InputNetFlags.Run) && prevDequeuedInput.HasFlag(InputNetFlags.Run);
|
||||
case InputType.Crouch:
|
||||
return !(dequeuedInput.HasFlag(InputNetFlags.Crouch)) && (prevDequeuedInput.HasFlag(InputNetFlags.Crouch));
|
||||
return dequeuedInput.HasFlag(InputNetFlags.Crouch) && !prevDequeuedInput.HasFlag(InputNetFlags.Crouch);
|
||||
case InputType.Select:
|
||||
return dequeuedInput.HasFlag(InputNetFlags.Select); //TODO: clean up the way this input is registered
|
||||
case InputType.Deselect:
|
||||
@@ -1139,11 +1242,11 @@ namespace Barotrauma
|
||||
case InputType.Grab:
|
||||
return dequeuedInput.HasFlag(InputNetFlags.Grab);
|
||||
case InputType.Use:
|
||||
return !(dequeuedInput.HasFlag(InputNetFlags.Use)) && (prevDequeuedInput.HasFlag(InputNetFlags.Use));
|
||||
return dequeuedInput.HasFlag(InputNetFlags.Use) && !prevDequeuedInput.HasFlag(InputNetFlags.Use);
|
||||
case InputType.Shoot:
|
||||
return !(dequeuedInput.HasFlag(InputNetFlags.Shoot)) && (prevDequeuedInput.HasFlag(InputNetFlags.Shoot));
|
||||
return dequeuedInput.HasFlag(InputNetFlags.Shoot) && !prevDequeuedInput.HasFlag(InputNetFlags.Shoot);
|
||||
case InputType.Ragdoll:
|
||||
return !(dequeuedInput.HasFlag(InputNetFlags.Ragdoll)) && (prevDequeuedInput.HasFlag(InputNetFlags.Ragdoll));
|
||||
return dequeuedInput.HasFlag(InputNetFlags.Ragdoll) && !prevDequeuedInput.HasFlag(InputNetFlags.Ragdoll);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -1264,11 +1367,27 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
private List<Item> wearableItems = new List<Item>();
|
||||
|
||||
public float GetSkillLevel(string skillIdentifier)
|
||||
{
|
||||
if (Info?.Job == null) { return 0.0f; }
|
||||
float skillLevel = Info.Job.GetSkillLevel(skillIdentifier);
|
||||
|
||||
if (skillIdentifier != null)
|
||||
{
|
||||
for (int i = 0; i < Inventory.Capacity; i++)
|
||||
{
|
||||
if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.GetItemAt(i)?.GetComponent<Wearable>() is Wearable wearable)
|
||||
{
|
||||
if (wearable.SkillModifiers.TryGetValue(skillIdentifier, out float skillValue))
|
||||
{
|
||||
skillLevel += skillValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Affliction affliction in CharacterHealth.GetAllAfflictions())
|
||||
{
|
||||
skillLevel *= affliction.GetSkillMultiplier();
|
||||
@@ -1422,6 +1541,16 @@ namespace Barotrauma
|
||||
greatestNegativeHealthMultiplier = 1f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be used to modify a character's health for runtime session. Change with AddHealthMultiplier
|
||||
/// </summary>
|
||||
public float StaticHealthMultiplier { get; private set; } = 1;
|
||||
|
||||
public void AddStaticHealthMultiplier(float newMultiplier)
|
||||
{
|
||||
StaticHealthMultiplier *= newMultiplier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Speed reduction from the current limb specific damage. Min 0, max 1.
|
||||
/// </summary>
|
||||
@@ -1595,83 +1724,89 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (attackCoolDown > 0.0f)
|
||||
{
|
||||
attackCoolDown -= deltaTime;
|
||||
}
|
||||
else if (IsKeyDown(InputType.Attack) && (IsRemotePlayer || Controlled == this || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)))
|
||||
else if (IsKeyDown(InputType.Attack))
|
||||
{
|
||||
Vector2 attackPos = SimPosition + ConvertUnits.ToSimUnits(cursorPosition - Position);
|
||||
List<Body> ignoredBodies = AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
|
||||
ignoredBodies.Add(AnimController.Collider.FarseerBody);
|
||||
|
||||
var body = Submarine.PickBody(
|
||||
SimPosition,
|
||||
attackPos,
|
||||
ignoredBodies,
|
||||
Physics.CollisionCharacter | Physics.CollisionWall);
|
||||
|
||||
IDamageable attackTarget = null;
|
||||
if (body != null)
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
attackPos = Submarine.LastPickedPosition;
|
||||
|
||||
if (body.UserData is Submarine sub)
|
||||
{
|
||||
body = Submarine.PickBody(
|
||||
SimPosition - ((Submarine)body.UserData).SimPosition,
|
||||
attackPos - ((Submarine)body.UserData).SimPosition,
|
||||
ignoredBodies,
|
||||
Physics.CollisionWall);
|
||||
|
||||
if (body != null)
|
||||
{
|
||||
attackPos = Submarine.LastPickedPosition + sub.SimPosition;
|
||||
attackTarget = body.UserData as IDamageable;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (body.UserData is IDamageable)
|
||||
{
|
||||
attackTarget = (IDamageable)body.UserData;
|
||||
}
|
||||
else if (body.UserData is Limb)
|
||||
{
|
||||
attackTarget = ((Limb)body.UserData).character;
|
||||
}
|
||||
}
|
||||
currentAttackTarget.AttackLimb?.UpdateAttack(deltaTime, currentAttackTarget.AttackPos, currentAttackTarget.DamageTarget, out _);
|
||||
}
|
||||
var currentContexts = GetAttackContexts();
|
||||
var validLimbs = AnimController.Limbs.Where(l =>
|
||||
else if (IsPlayer)
|
||||
{
|
||||
if (l.IsSevered || l.IsStuck) { return false; }
|
||||
if (l.Disabled) { return false; }
|
||||
var attack = l.attack;
|
||||
if (attack == null) { return false; }
|
||||
if (attack.CoolDownTimer > 0) { return false; }
|
||||
if (!attack.IsValidContext(currentContexts)) { return false; }
|
||||
if (attackTarget != null)
|
||||
Vector2 attackPos = SimPosition + ConvertUnits.ToSimUnits(cursorPosition - Position);
|
||||
List<Body> ignoredBodies = AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
|
||||
ignoredBodies.Add(AnimController.Collider.FarseerBody);
|
||||
|
||||
var body = Submarine.PickBody(
|
||||
SimPosition,
|
||||
attackPos,
|
||||
ignoredBodies,
|
||||
Physics.CollisionCharacter | Physics.CollisionWall);
|
||||
|
||||
IDamageable attackTarget = null;
|
||||
if (body != null)
|
||||
{
|
||||
if (!attack.IsValidTarget(attackTarget)) { return false; }
|
||||
if (attackTarget is ISerializableEntity se && attackTarget is Character)
|
||||
attackPos = Submarine.LastPickedPosition;
|
||||
|
||||
if (body.UserData is Submarine sub)
|
||||
{
|
||||
if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; }
|
||||
body = Submarine.PickBody(
|
||||
SimPosition - ((Submarine)body.UserData).SimPosition,
|
||||
attackPos - ((Submarine)body.UserData).SimPosition,
|
||||
ignoredBodies,
|
||||
Physics.CollisionWall);
|
||||
|
||||
if (body != null)
|
||||
{
|
||||
attackPos = Submarine.LastPickedPosition + sub.SimPosition;
|
||||
attackTarget = body.UserData as IDamageable;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (body.UserData is IDamageable)
|
||||
{
|
||||
attackTarget = (IDamageable)body.UserData;
|
||||
}
|
||||
else if (body.UserData is Limb)
|
||||
{
|
||||
attackTarget = ((Limb)body.UserData).character;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(this))) { return false; }
|
||||
return true;
|
||||
});
|
||||
var sortedLimbs = validLimbs.OrderBy(l => Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(l.SimPosition), cursorPosition));
|
||||
// Select closest
|
||||
var attackLimb = sortedLimbs.FirstOrDefault();
|
||||
if (attackLimb != null)
|
||||
{
|
||||
attackLimb.UpdateAttack(deltaTime, attackPos, attackTarget, out AttackResult attackResult);
|
||||
if (!attackLimb.attack.IsRunning)
|
||||
var currentContexts = GetAttackContexts();
|
||||
var validLimbs = AnimController.Limbs.Where(l =>
|
||||
{
|
||||
attackCoolDown = 1.0f;
|
||||
if (l.IsSevered || l.IsStuck) { return false; }
|
||||
if (l.Disabled) { return false; }
|
||||
var attack = l.attack;
|
||||
if (attack == null) { return false; }
|
||||
if (attack.CoolDownTimer > 0) { return false; }
|
||||
if (!attack.IsValidContext(currentContexts)) { return false; }
|
||||
if (attackTarget != null)
|
||||
{
|
||||
if (!attack.IsValidTarget(attackTarget)) { return false; }
|
||||
if (attackTarget is ISerializableEntity se && attackTarget is Character)
|
||||
{
|
||||
if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; }
|
||||
}
|
||||
}
|
||||
if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(this))) { return false; }
|
||||
return true;
|
||||
});
|
||||
var sortedLimbs = validLimbs.OrderBy(l => Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(l.SimPosition), cursorPosition));
|
||||
// Select closest
|
||||
var attackLimb = sortedLimbs.FirstOrDefault();
|
||||
if (attackLimb != null)
|
||||
{
|
||||
attackLimb.UpdateAttack(deltaTime, attackPos, attackTarget, out AttackResult attackResult);
|
||||
if (!attackLimb.attack.IsRunning)
|
||||
{
|
||||
attackCoolDown = 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1746,6 +1881,24 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private struct AttackTargetData
|
||||
{
|
||||
public Limb AttackLimb { get; set; }
|
||||
public IDamageable DamageTarget { get; set; }
|
||||
public Vector2 AttackPos { get; set; }
|
||||
}
|
||||
|
||||
private AttackTargetData currentAttackTarget;
|
||||
public void SetAttackTarget(Limb attackLimb, IDamageable damageTarget, Vector2 attackPos)
|
||||
{
|
||||
currentAttackTarget = new AttackTargetData()
|
||||
{
|
||||
AttackLimb = attackLimb,
|
||||
DamageTarget = damageTarget,
|
||||
AttackPos = attackPos
|
||||
};
|
||||
}
|
||||
|
||||
public bool CanSeeCharacter(Character target)
|
||||
{
|
||||
if (target.Removed) { return false; }
|
||||
@@ -1791,19 +1944,19 @@ namespace Barotrauma
|
||||
return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb;
|
||||
}
|
||||
|
||||
public bool CanSeeTarget(ISpatialEntity target, Limb seeingLimb = null)
|
||||
public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null)
|
||||
{
|
||||
seeingLimb ??= GetSeeingLimb();
|
||||
if (seeingLimb == null) { return false; }
|
||||
ISpatialEntity seeingEntity = AnimController.SimplePhysicsEnabled ? this : seeingLimb as ISpatialEntity;
|
||||
seeingEntity ??= AnimController.SimplePhysicsEnabled ? this as ISpatialEntity : GetSeeingLimb() as ISpatialEntity;
|
||||
if (seeingEntity == null) { return false; }
|
||||
ISpatialEntity sourceEntity = seeingEntity ;
|
||||
// TODO: Could we just use the method below? If not, let's refactor it so that we can.
|
||||
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition);
|
||||
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - sourceEntity.WorldPosition);
|
||||
Body closestBody;
|
||||
//both inside the same sub (or both outside)
|
||||
//OR the we're inside, the other character outside
|
||||
if (target.Submarine == Submarine || target.Submarine == null)
|
||||
{
|
||||
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
|
||||
closestBody = Submarine.CheckVisibility(sourceEntity.SimPosition, sourceEntity.SimPosition + diff);
|
||||
}
|
||||
//we're outside, the other character inside
|
||||
else if (Submarine == null)
|
||||
@@ -1813,7 +1966,7 @@ namespace Barotrauma
|
||||
//both inside different subs
|
||||
else
|
||||
{
|
||||
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
|
||||
closestBody = Submarine.CheckVisibility(sourceEntity.SimPosition, sourceEntity.SimPosition + diff);
|
||||
if (!IsBlocking(closestBody))
|
||||
{
|
||||
closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff);
|
||||
@@ -1841,52 +1994,44 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TODO: ensure that works. CheckVisibility takes positions in sim space, but this method uses world positions
|
||||
/// </summary>
|
||||
public bool CanSeeCharacter(Character target, Vector2 sourceWorldPos)
|
||||
{
|
||||
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - sourceWorldPos);
|
||||
Body closestBody;
|
||||
if (target.Submarine == null)
|
||||
{
|
||||
closestBody = Submarine.CheckVisibility(sourceWorldPos, sourceWorldPos + diff);
|
||||
if (closestBody == null) { return true; }
|
||||
}
|
||||
else
|
||||
{
|
||||
closestBody = Submarine.CheckVisibility(target.WorldPosition, target.WorldPosition - diff);
|
||||
if (closestBody == null) { return true; }
|
||||
}
|
||||
Structure wall = closestBody.UserData as Structure;
|
||||
Item item = closestBody.UserData as Item;
|
||||
Door door = item?.GetComponent<Door>();
|
||||
return (wall == null || !wall.CastShadow) && (door == null || door.CanBeTraversed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A simple check if the character Dir is towards the target or not. Uses the world coordinates.
|
||||
/// </summary>
|
||||
public bool IsFacing(Vector2 targetWorldPos) => AnimController.Dir > 0 && targetWorldPos.X > WorldPosition.X || AnimController.Dir < 0 && targetWorldPos.X < WorldPosition.X;
|
||||
|
||||
public bool HasItem(Item item, bool requireEquipped = false) => requireEquipped ? HasEquippedItem(item) : item.IsOwnedBy(this);
|
||||
public bool HasItem(Item item, bool requireEquipped = false, InvSlotType? slotType = null) => requireEquipped ? HasEquippedItem(item) : item.IsOwnedBy(this);
|
||||
|
||||
public bool HasEquippedItem(Item item)
|
||||
public bool HasEquippedItem(Item item, InvSlotType? slotType = null)
|
||||
{
|
||||
if (Inventory == null) { return false; }
|
||||
for (int i = 0; i < Inventory.Capacity; i++)
|
||||
{
|
||||
if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.GetItemAt(i) == item) { return true; }
|
||||
if (slotType.HasValue)
|
||||
{
|
||||
if (!slotType.Value.HasFlag(Inventory.SlotTypes[i])) { continue; }
|
||||
}
|
||||
else if (Inventory.SlotTypes[i] == InvSlotType.Any)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (Inventory.GetItemAt(i) == item) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool HasEquippedItem(string tagOrIdentifier, bool allowBroken = true)
|
||||
public bool HasEquippedItem(string tagOrIdentifier, bool allowBroken = true, InvSlotType? slotType = null)
|
||||
{
|
||||
if (Inventory == null) { return false; }
|
||||
for (int i = 0; i < Inventory.Capacity; i++)
|
||||
{
|
||||
if (Inventory.SlotTypes[i] == InvSlotType.Any) { continue; }
|
||||
if (slotType.HasValue)
|
||||
{
|
||||
if (!slotType.Value.HasFlag(Inventory.SlotTypes[i])) { continue; }
|
||||
}
|
||||
else if (Inventory.SlotTypes[i] == InvSlotType.Any)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var item = Inventory.GetItemAt(i);
|
||||
if (item == null) { continue; }
|
||||
if (!allowBroken && item.Condition <= 0.0f) { continue; }
|
||||
@@ -1895,12 +2040,19 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
public Item GetEquippedItem(string tagOrIdentifier)
|
||||
public Item GetEquippedItem(string tagOrIdentifier, InvSlotType? slotType = null)
|
||||
{
|
||||
if (Inventory == null) { return null; }
|
||||
for (int i = 0; i < Inventory.Capacity; i++)
|
||||
{
|
||||
if (Inventory.SlotTypes[i] == InvSlotType.Any) { continue; }
|
||||
if (slotType.HasValue)
|
||||
{
|
||||
if (!slotType.Value.HasFlag(Inventory.SlotTypes[i])) { continue; }
|
||||
}
|
||||
else 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; }
|
||||
@@ -2022,7 +2174,7 @@ namespace Barotrauma
|
||||
bool hidden = item.HiddenInGame;
|
||||
#if CLIENT
|
||||
if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; }
|
||||
#endif
|
||||
#endif
|
||||
if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; }
|
||||
|
||||
if (item.ParentInventory != null)
|
||||
@@ -2086,7 +2238,10 @@ namespace Barotrauma
|
||||
Rectangle itemDisplayRect = new Rectangle(item.InteractionRect.X, item.InteractionRect.Y - item.InteractionRect.Height, item.InteractionRect.Width, item.InteractionRect.Height);
|
||||
|
||||
// Get the point along the line between lowerBodyPosition and upperBodyPosition which is closest to the center of itemDisplayRect
|
||||
Vector2 playerDistanceCheckPosition = Vector2.Clamp(itemDisplayRect.Center.ToVector2(), lowerBodyPosition, upperBodyPosition);
|
||||
Vector2 playerDistanceCheckPosition =
|
||||
lowerBodyPosition.Y < upperBodyPosition.Y ?
|
||||
Vector2.Clamp(itemDisplayRect.Center.ToVector2(), lowerBodyPosition, upperBodyPosition) :
|
||||
Vector2.Clamp(itemDisplayRect.Center.ToVector2(), upperBodyPosition, lowerBodyPosition);
|
||||
|
||||
// If playerDistanceCheckPosition is inside the itemDisplayRect then we consider the character to within 0 distance of the item
|
||||
if (itemDisplayRect.Contains(playerDistanceCheckPosition))
|
||||
@@ -2124,7 +2279,7 @@ namespace Barotrauma
|
||||
itemPosition -= Submarine.SimPosition;
|
||||
}
|
||||
var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true);
|
||||
if (body != null && body.UserData as Item != item) { return false; }
|
||||
if (body != null && body.UserData as Item != item && Submarine.LastPickedFixture?.UserData as Item != item) { return false; }
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -2222,6 +2377,10 @@ namespace Barotrauma
|
||||
var item = FindItemAtPosition(mouseSimPos, aimAssist);
|
||||
|
||||
focusedItem = CanInteract ? item : null;
|
||||
if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None)
|
||||
{
|
||||
FocusedCharacter = null;
|
||||
}
|
||||
findFocusedTimer = 0.05f;
|
||||
}
|
||||
}
|
||||
@@ -2360,29 +2519,26 @@ namespace Barotrauma
|
||||
{
|
||||
if (!(c is AICharacter) && !c.IsRemotePlayer) continue;
|
||||
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
if (c.IsPlayer || (c.IsBot && !c.IsDead))
|
||||
{
|
||||
c.Enabled = true;
|
||||
}
|
||||
else if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
//disable AI characters that are far away from all clients and the host's character and not controlled by anyone
|
||||
if (c.IsPlayer || (c.IsBot && !c.IsDead))
|
||||
float closestPlayerDist = c.GetDistanceToClosestPlayer();
|
||||
if (closestPlayerDist > c.Params.DisableDistance)
|
||||
{
|
||||
c.Enabled = false;
|
||||
if (c.IsDead && c.AIController is EnemyAIController)
|
||||
{
|
||||
Spawner?.AddToRemoveQueue(c);
|
||||
}
|
||||
}
|
||||
else if (closestPlayerDist < c.Params.DisableDistance * 0.9f)
|
||||
{
|
||||
c.Enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
float closestPlayerDist = c.GetDistanceToClosestPlayer();
|
||||
if (closestPlayerDist > c.Params.DisableDistance)
|
||||
{
|
||||
c.Enabled = false;
|
||||
if (c.IsDead && c.AIController is EnemyAIController)
|
||||
{
|
||||
Spawner?.AddToRemoveQueue(c);
|
||||
}
|
||||
}
|
||||
else if (closestPlayerDist < c.Params.DisableDistance * 0.9f)
|
||||
{
|
||||
c.Enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Submarine.MainSub != null)
|
||||
{
|
||||
@@ -2834,11 +2990,14 @@ namespace Barotrauma
|
||||
Spawner.AddToRemoveQueue(this);
|
||||
}
|
||||
|
||||
public void DespawnNow()
|
||||
public void DespawnNow(bool createNetworkEvents = true)
|
||||
{
|
||||
despawnTimer = GameMain.Config.CorpseDespawnDelay;
|
||||
UpdateDespawn(1.0f, ignoreThresholds: true);
|
||||
Spawner.Update();
|
||||
if (createNetworkEvents)
|
||||
{
|
||||
Spawner.Update();
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveByPrefab(CharacterPrefab prefab)
|
||||
@@ -2899,10 +3058,16 @@ namespace Barotrauma
|
||||
return !string.IsNullOrEmpty(ChatMessage.ApplyDistanceEffect("message", messageType, speaker, this));
|
||||
}
|
||||
|
||||
public void SetOrder(Order order, string orderOption, int priority, Character orderGiver, bool speak = true)
|
||||
/// <param name="force">Force an order to be set for the character, bypassing hearing checks</param>
|
||||
public void SetOrder(Order order, string orderOption, int priority, Character orderGiver, bool speak = true, bool force = false)
|
||||
{
|
||||
//set the character order only if the character is close enough to hear the message
|
||||
if (orderGiver != null && !CanHearCharacter(orderGiver)) { return; }
|
||||
if (!force && orderGiver != null && !CanHearCharacter(orderGiver)) { return; }
|
||||
|
||||
if (order.OrderGiver != orderGiver)
|
||||
{
|
||||
order.OrderGiver = orderGiver;
|
||||
}
|
||||
|
||||
// If there's another character operating the same device, make them dismiss themself
|
||||
if (order != null && order.Category == OrderCategory.Operate && order.TargetEntity != null)
|
||||
@@ -2911,6 +3076,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (character == this) { continue; }
|
||||
if (character.TeamID != TeamID) { continue; }
|
||||
if (!(character.AIController is HumanAIController)) { continue; }
|
||||
if (!HumanAIController.IsActive(character)) { continue; }
|
||||
foreach (var currentOrder in character.CurrentOrders)
|
||||
{
|
||||
@@ -2918,7 +3084,7 @@ namespace Barotrauma
|
||||
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);
|
||||
character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character, speak: speak, force: force);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2936,6 +3102,12 @@ namespace Barotrauma
|
||||
SetOrderProjSpecific(order, orderOption, priority);
|
||||
}
|
||||
|
||||
/// <param name="force">Force an order to be set for the character, bypassing hearing checks</param>
|
||||
public void SetOrder(OrderInfo orderInfo, Character orderGiver, bool speak = true, bool force = false)
|
||||
{
|
||||
SetOrder(orderInfo.Order, orderInfo.OrderOption, orderInfo.ManualPriority, orderGiver, speak: speak, force: force);
|
||||
}
|
||||
|
||||
private void AddCurrentOrder(OrderInfo newOrder)
|
||||
{
|
||||
if (newOrder.Order == null || newOrder.Order.Identifier == "dismissed")
|
||||
@@ -3154,7 +3326,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Apply the specified attack to this character. If the targetLimb is not specified, the limb closest to worldPosition will receive the damage.
|
||||
/// </summary>
|
||||
public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false, Limb targetLimb = null)
|
||||
public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false, Limb targetLimb = null, float penetration = 0f)
|
||||
{
|
||||
if (Removed)
|
||||
{
|
||||
@@ -3170,7 +3342,7 @@ namespace Barotrauma
|
||||
|
||||
var attackResult = targetLimb == null ?
|
||||
AddDamage(worldPosition, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, out limbHit, attacker, attack.DamageMultiplier) :
|
||||
DamageLimb(worldPosition, targetLimb, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier);
|
||||
DamageLimb(worldPosition, targetLimb, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier, penetration: penetration);
|
||||
|
||||
if (limbHit == null) { return new AttackResult(); }
|
||||
Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld;
|
||||
@@ -3302,7 +3474,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, bool allowStacking = true)
|
||||
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, float penetration = 0f)
|
||||
{
|
||||
if (Removed) { return new AttackResult(); }
|
||||
|
||||
@@ -3354,7 +3526,7 @@ namespace Barotrauma
|
||||
}
|
||||
bool wasDead = IsDead;
|
||||
Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir);
|
||||
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier);
|
||||
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier, penetration: penetration);
|
||||
CharacterHealth.ApplyDamage(hitLimb, attackResult, allowStacking);
|
||||
if (attacker != this)
|
||||
{
|
||||
@@ -3412,9 +3584,9 @@ 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.
|
||||
/// With stunning, the parameter uses an one 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 || IsRagdolled;
|
||||
public bool IsKnockedDown => IsRagdolled || CharacterHealth.StunTimer > 1.0f || IsIncapacitated;
|
||||
|
||||
public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false)
|
||||
{
|
||||
@@ -3457,7 +3629,7 @@ namespace Barotrauma
|
||||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
targets.Clear();
|
||||
statusEffect.GetNearbyTargets(WorldPosition, targets);
|
||||
targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets));
|
||||
statusEffect.Apply(actionType, deltaTime, this, targets);
|
||||
}
|
||||
else
|
||||
@@ -3500,6 +3672,11 @@ namespace Barotrauma
|
||||
// OnDamaged is called only for the limb that is hit.
|
||||
AnimController.Limbs.ForEach(l => l.ApplyStatusEffects(actionType, deltaTime));
|
||||
}
|
||||
//OnActive effects are handled by the afflictions themselves
|
||||
if (actionType != ActionType.OnActive)
|
||||
{
|
||||
CharacterHealth.ApplyAfflictionStatusEffects(actionType);
|
||||
}
|
||||
}
|
||||
|
||||
private void Implode(bool isNetworkMessage = false)
|
||||
@@ -3643,10 +3820,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
if (aiTarget != null)
|
||||
{
|
||||
aiTarget.Remove();
|
||||
}
|
||||
aiTarget?.Remove();
|
||||
|
||||
aiTarget = new AITarget(this);
|
||||
CharacterHealth.RemoveAllAfflictions();
|
||||
@@ -3738,8 +3912,9 @@ namespace Barotrauma
|
||||
AnimController.FindHull(worldPos, true);
|
||||
}
|
||||
|
||||
public void SaveInventory(Inventory inventory, XElement parentElement)
|
||||
public static void SaveInventory(Inventory inventory, XElement parentElement)
|
||||
{
|
||||
if (inventory == null || parentElement == null) { return; }
|
||||
var items = inventory.AllItems.Distinct();
|
||||
foreach (Item item in items)
|
||||
{
|
||||
@@ -3758,6 +3933,14 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="SaveInventory(Barotrauma.Inventory, XElement)"/> using 'Inventory' and 'Info.InventoryData'
|
||||
/// </summary>
|
||||
public void SaveInventory()
|
||||
{
|
||||
SaveInventory(Inventory, Info?.InventoryData);
|
||||
}
|
||||
|
||||
public void SpawnInventoryItems(Inventory inventory, XElement itemData)
|
||||
{
|
||||
SpawnInventoryItemsRecursive(inventory, itemData, new List<Item>());
|
||||
@@ -4011,9 +4194,12 @@ namespace Barotrauma
|
||||
public bool IsEngineer => HasJob("engineer");
|
||||
public bool IsMechanic => HasJob("mechanic");
|
||||
public bool IsMedic => HasJob("medicaldoctor");
|
||||
public bool IsSecurity => HasJob("securityofficer");
|
||||
public bool IsSecurity => HasJob("securityofficer") || HasJob("vipsecurityofficer");
|
||||
public bool IsAssistant => HasJob("assistant");
|
||||
public bool IsWatchman => HasJob("watchman");
|
||||
public bool IsVip => HasJob("prisoner");
|
||||
public bool IsPrisoner => HasJob("prisoner");
|
||||
public Color? UniqueNameColor { get; set; } = null;
|
||||
|
||||
public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier;
|
||||
|
||||
@@ -4022,4 +4208,24 @@ namespace Barotrauma
|
||||
return PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
class ActiveTeamChange
|
||||
{
|
||||
public CharacterTeamType DesiredTeamId { get; }
|
||||
public enum TeamChangePriorities
|
||||
{
|
||||
Base, // given to characters when generated or when their base team is set
|
||||
Willful, // cognitive, willful team changes, such as prisoners escaping
|
||||
Absolute // possession, insanity, the like
|
||||
}
|
||||
public TeamChangePriorities TeamChangePriority { get; }
|
||||
public bool AggressiveBehavior { get; }
|
||||
|
||||
public ActiveTeamChange(CharacterTeamType desiredTeamId, TeamChangePriorities teamChangePriority, bool aggressiveBehavior = false)
|
||||
{
|
||||
DesiredTeamId = desiredTeamId;
|
||||
TeamChangePriority = teamChangePriority;
|
||||
AggressiveBehavior = aggressiveBehavior;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,7 @@ namespace Barotrauma
|
||||
|
||||
public XElement InventoryData;
|
||||
public XElement HealthData;
|
||||
public XElement OrderData;
|
||||
|
||||
private static ushort idCounter;
|
||||
private const string disguiseName = "???";
|
||||
@@ -455,7 +456,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 = "", string originalName = "", 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, string npcIdentifier = "")
|
||||
{
|
||||
if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -484,8 +485,12 @@ namespace Barotrauma
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
else
|
||||
else if (!string.IsNullOrEmpty(npcIdentifier) && TextManager.Get("npctitle." + npcIdentifier, true) is string npcTitle)
|
||||
{
|
||||
Name = npcTitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
name = "";
|
||||
if (CharacterConfigElement.Element("name") != null)
|
||||
{
|
||||
@@ -493,7 +498,7 @@ namespace Barotrauma
|
||||
if (firstNamePath != "")
|
||||
{
|
||||
firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male");
|
||||
Name = ToolBox.GetRandomLine(firstNamePath);
|
||||
Name = ToolBox.GetRandomLine(firstNamePath, randSync);
|
||||
}
|
||||
|
||||
string lastNamePath = CharacterConfigElement.Element("name").GetAttributeString("lastname", "");
|
||||
@@ -501,7 +506,7 @@ namespace Barotrauma
|
||||
{
|
||||
lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male");
|
||||
if (Name != "") Name += " ";
|
||||
Name += ToolBox.GetRandomLine(lastNamePath);
|
||||
Name += ToolBox.GetRandomLine(lastNamePath, randSync);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1019,7 +1024,273 @@ namespace Barotrauma
|
||||
return charElement;
|
||||
}
|
||||
|
||||
public void ApplyHealthData(Character character, XElement healthData)
|
||||
public static void SaveOrders(XElement parentElement, params OrderInfo[] orders)
|
||||
{
|
||||
if (parentElement == null || orders == null || orders.None()) { return; }
|
||||
// If an order is invalid, we discard the order and increase the priority of the following orders so
|
||||
// 1) the highest priority value will remain equal to CharacterInfo.HighestManualOrderPriority; and
|
||||
// 2) the order priorities will remain sequential.
|
||||
int priorityIncrease = 0;
|
||||
var linkedSubs = GetLinkedSubmarines();
|
||||
foreach (var orderInfo in orders)
|
||||
{
|
||||
var order = orderInfo.Order;
|
||||
if (order == null || string.IsNullOrEmpty(order.Identifier))
|
||||
{
|
||||
DebugConsole.ThrowError("Error saving an order - the order or its identifier is null");
|
||||
priorityIncrease++;
|
||||
continue;
|
||||
}
|
||||
int? linkedSubIndex = null;
|
||||
bool targetAvailableInNextLevel = true;
|
||||
if (order.TargetSpatialEntity != null)
|
||||
{
|
||||
var entitySub = order.TargetSpatialEntity.Submarine;
|
||||
bool isOutside = entitySub == null;
|
||||
bool canBeOnLinkedSub = !isOutside && Submarine.MainSub != null && entitySub != Submarine.MainSub && linkedSubs.Any();
|
||||
bool isOnConnectedLinkedSub = false;
|
||||
if (canBeOnLinkedSub)
|
||||
{
|
||||
for (int i = 0; i < linkedSubs.Count; i++)
|
||||
{
|
||||
var ls = linkedSubs[i];
|
||||
if (!ls.LoadSub) { continue; }
|
||||
if (ls.Sub != entitySub) { continue; }
|
||||
linkedSubIndex = i;
|
||||
isOnConnectedLinkedSub = Submarine.MainSub.GetConnectedSubs().Contains(entitySub);
|
||||
break;
|
||||
}
|
||||
}
|
||||
targetAvailableInNextLevel = !isOutside && GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null && (isOnConnectedLinkedSub || entitySub == Submarine.MainSub);
|
||||
if (!targetAvailableInNextLevel)
|
||||
{
|
||||
if (!order.CanBeGeneralized)
|
||||
{
|
||||
DebugConsole.Log($"Trying to save an order ({order.Identifier}) targeting an entity that won't be connected to the main sub in the next level. The order requires a target so it won't be saved.");
|
||||
priorityIncrease++;
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.Log($"Saving an order ({order.Identifier}) targeting an entity that won't be connected to the main sub in the next level. The order will be saved as a generalized version.");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (orderInfo.ManualPriority < 1)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error saving an order ({order.Identifier}) - the order priority is less than 1");
|
||||
priorityIncrease++;
|
||||
continue;
|
||||
}
|
||||
var orderElement = new XElement("order",
|
||||
new XAttribute("id", order.Identifier),
|
||||
new XAttribute("priority", orderInfo.ManualPriority + priorityIncrease),
|
||||
new XAttribute("targettype", (int)order.TargetType));
|
||||
if (!string.IsNullOrEmpty(orderInfo.OrderOption))
|
||||
{
|
||||
orderElement.Add(new XAttribute("option", orderInfo.OrderOption));
|
||||
}
|
||||
if (order.OrderGiver != null)
|
||||
{
|
||||
orderElement.Add(new XAttribute("ordergiverinfoid", order.OrderGiver.Info.ID));
|
||||
}
|
||||
if (order.TargetSpatialEntity?.Submarine is Submarine targetSub)
|
||||
{
|
||||
if (targetSub == Submarine.MainSub)
|
||||
{
|
||||
orderElement.Add(new XAttribute("onmainsub", true));
|
||||
}
|
||||
else if(linkedSubIndex.HasValue)
|
||||
{
|
||||
orderElement.Add(new XAttribute("linkedsubindex", linkedSubIndex));
|
||||
}
|
||||
}
|
||||
switch (order.TargetType)
|
||||
{
|
||||
case Order.OrderTargetType.Entity when targetAvailableInNextLevel && order.TargetEntity is Entity e:
|
||||
orderElement.Add(new XAttribute("targetid", (uint)e.ID));
|
||||
break;
|
||||
case Order.OrderTargetType.Position when targetAvailableInNextLevel && order.TargetSpatialEntity is OrderTarget ot:
|
||||
var orderTargetElement = new XElement("ordertarget");
|
||||
var position = ot.WorldPosition;
|
||||
if (ot.Hull != null)
|
||||
{
|
||||
orderTargetElement.Add(new XAttribute("hullid", (uint)ot.Hull.ID));
|
||||
position -= ot.Hull.WorldPosition;
|
||||
}
|
||||
orderTargetElement.Add(new XAttribute("position", $"{position.X},{position.Y}"));
|
||||
orderElement.Add(orderTargetElement);
|
||||
break;
|
||||
case Order.OrderTargetType.WallSection when targetAvailableInNextLevel && order.TargetEntity is Structure s && order.WallSectionIndex.HasValue:
|
||||
orderElement.Add(new XAttribute("structureid", s.ID));
|
||||
orderElement.Add(new XAttribute("wallsectionindex", order.WallSectionIndex.Value));
|
||||
break;
|
||||
}
|
||||
parentElement.Add(orderElement);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save current orders to the parameter element
|
||||
/// </summary>
|
||||
public static void SaveOrderData(CharacterInfo characterInfo, XElement parentElement)
|
||||
{
|
||||
var currentOrders = new List<OrderInfo>(characterInfo.CurrentOrders);
|
||||
// Sort the current orders to make sure the one with the highest priority comes first
|
||||
currentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority));
|
||||
SaveOrders(parentElement, currentOrders.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save current orders to <see cref="OrderData"/>
|
||||
/// </summary>
|
||||
public void SaveOrderData()
|
||||
{
|
||||
OrderData = new XElement("orders");
|
||||
SaveOrderData(this, OrderData);
|
||||
}
|
||||
|
||||
public static void ApplyOrderData(Character character, XElement orderData)
|
||||
{
|
||||
if (character == null) { return; }
|
||||
var orders = LoadOrders(orderData);
|
||||
foreach (var order in orders)
|
||||
{
|
||||
character.SetOrder(order, order.Order?.OrderGiver, speak: false, force: true);
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyOrderData()
|
||||
{
|
||||
ApplyOrderData(Character, OrderData);
|
||||
}
|
||||
|
||||
public static List<OrderInfo> LoadOrders(XElement ordersElement)
|
||||
{
|
||||
var orders = new List<OrderInfo>();
|
||||
if (ordersElement == null) { return orders; }
|
||||
// If an order is invalid, we discard the order and increase the priority of the following orders so
|
||||
// 1) the highest priority value will remain equal to CharacterInfo.HighestManualOrderPriority; and
|
||||
// 2) the order priorities will remain sequential.
|
||||
int priorityIncrease = 0;
|
||||
var linkedSubs = GetLinkedSubmarines();
|
||||
foreach (var orderElement in ordersElement.GetChildElements("order"))
|
||||
{
|
||||
Order order = null;
|
||||
string orderIdentifier = orderElement.GetAttributeString("id", "");
|
||||
var orderPrefab = Order.GetPrefab(orderIdentifier);
|
||||
if (orderPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error loading a previously saved order - can't find an order prefab with the identifier \"{orderIdentifier}\"");
|
||||
priorityIncrease++;
|
||||
continue;
|
||||
}
|
||||
var targetType = (Order.OrderTargetType)orderElement.GetAttributeInt("targettype", 0);
|
||||
int orderGiverInfoId = orderElement.GetAttributeInt("ordergiverinfoid", -1);
|
||||
var orderGiver = orderGiverInfoId >= 0 ? Character.CharacterList.FirstOrDefault(c => c.Info?.ID == orderGiverInfoId) : null;
|
||||
Entity targetEntity = null;
|
||||
switch (targetType)
|
||||
{
|
||||
case Order.OrderTargetType.Entity:
|
||||
ushort targetId = (ushort)orderElement.GetAttributeUInt("targetid", Entity.NullEntityID);
|
||||
if (!GetTargetEntity(targetId, out targetEntity)) { continue; }
|
||||
var targetComponent = orderPrefab.GetTargetItemComponent(targetEntity as Item);
|
||||
order = new Order(orderPrefab, targetEntity, targetComponent, orderGiver: orderGiver);
|
||||
break;
|
||||
case Order.OrderTargetType.Position:
|
||||
var orderTargetElement = orderElement.GetChildElement("ordertarget");
|
||||
var position = orderTargetElement.GetAttributeVector2("position", Vector2.Zero);
|
||||
ushort hullId = (ushort)orderTargetElement.GetAttributeUInt("hullid", 0);
|
||||
if (!GetTargetEntity(hullId, out targetEntity)) { continue; }
|
||||
if (!(targetEntity is Hull targetPositionHull))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error loading a previously saved order ({orderIdentifier}) - entity with the ID {hullId} is of type {targetEntity?.GetType()} instead of Hull");
|
||||
priorityIncrease++;
|
||||
continue;
|
||||
}
|
||||
var orderTarget = new OrderTarget(targetPositionHull.WorldPosition + position, targetPositionHull);
|
||||
order = new Order(orderPrefab, orderTarget, orderGiver: orderGiver);
|
||||
break;
|
||||
case Order.OrderTargetType.WallSection:
|
||||
ushort structureId = (ushort)orderElement.GetAttributeInt("structureid", Entity.NullEntityID);
|
||||
if (!GetTargetEntity(structureId, out targetEntity)) { continue; }
|
||||
int wallSectionIndex = orderElement.GetAttributeInt("wallsectionindex", 0);
|
||||
if (!(targetEntity is Structure targetStructure))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error loading a previously saved order ({orderIdentifier}) - entity with the ID {structureId} is of type {targetEntity?.GetType()} instead of Structure");
|
||||
priorityIncrease++;
|
||||
continue;
|
||||
}
|
||||
order = new Order(orderPrefab, targetStructure, wallSectionIndex, orderGiver: orderGiver);
|
||||
break;
|
||||
}
|
||||
string orderOption = orderElement.GetAttributeString("option", "");
|
||||
int manualPriority = orderElement.GetAttributeInt("priority", 0) + priorityIncrease;
|
||||
var orderInfo = new OrderInfo(order, orderOption, manualPriority);
|
||||
orders.Add(orderInfo);
|
||||
|
||||
bool GetTargetEntity(ushort targetId, out Entity targetEntity)
|
||||
{
|
||||
targetEntity = null;
|
||||
if (targetId == Entity.NullEntityID) { return true; }
|
||||
Submarine parentSub = null;
|
||||
if (orderElement.GetAttributeBool("onmainsub", false))
|
||||
{
|
||||
parentSub = Submarine.MainSub;
|
||||
}
|
||||
else
|
||||
{
|
||||
int linkedSubIndex = orderElement.GetAttributeInt("linkedsubindex", -1);
|
||||
if (linkedSubIndex >= 0 && linkedSubIndex < linkedSubs.Count &&
|
||||
linkedSubs[linkedSubIndex] is LinkedSubmarine linkedSub && linkedSub.LoadSub)
|
||||
{
|
||||
parentSub = linkedSub.Sub;
|
||||
}
|
||||
}
|
||||
if (parentSub != null)
|
||||
{
|
||||
targetId = GetOffsetId(parentSub, targetId);
|
||||
targetEntity = Entity.FindEntityByID(targetId);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!orderPrefab.CanBeGeneralized)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error loading a previously saved order ({orderIdentifier}). Can't find the parent sub of the target entity. The order requires a target so it can't be loaded at all.");
|
||||
priorityIncrease++;
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.AddWarning($"Trying to load a previously saved order ({orderIdentifier}). Can't find the parent sub of the target entity. The order doesn't require a target so a more generic version of the order will be loaded instead.");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return orders;
|
||||
}
|
||||
|
||||
private static List<LinkedSubmarine> GetLinkedSubmarines()
|
||||
{
|
||||
return Entity.GetEntities()
|
||||
.OfType<LinkedSubmarine>()
|
||||
.Where(ls => ls.Submarine == Submarine.MainSub)
|
||||
.OrderBy(e => e.ID)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static ushort GetOffsetId(Submarine parentSub, ushort id)
|
||||
{
|
||||
if (parentSub != null)
|
||||
{
|
||||
var idRemap = new IdRemap(parentSub.Info.SubmarineElement, parentSub.IdOffset);
|
||||
return idRemap.GetOffsetId(id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
public static void ApplyHealthData(Character character, XElement healthData)
|
||||
{
|
||||
if (healthData != null) { character?.CharacterHealth.Load(healthData); }
|
||||
}
|
||||
|
||||
+45
-33
@@ -14,7 +14,7 @@ namespace Barotrauma
|
||||
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties { get; set; }
|
||||
|
||||
public float PendingAdditionStrenght { get; set; }
|
||||
public float PendingAdditionStrength { get; set; }
|
||||
public float AdditionStrength { get; set; }
|
||||
|
||||
protected float _strength;
|
||||
@@ -32,7 +32,7 @@ namespace Barotrauma
|
||||
float newValue = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength);
|
||||
if (newValue > _strength)
|
||||
{
|
||||
PendingAdditionStrenght = Prefab.GrainBurst;
|
||||
PendingAdditionStrength = Prefab.GrainBurst;
|
||||
}
|
||||
_strength = newValue;
|
||||
}
|
||||
@@ -64,7 +64,7 @@ namespace Barotrauma
|
||||
public Affliction(AfflictionPrefab prefab, float strength)
|
||||
{
|
||||
Prefab = prefab;
|
||||
PendingAdditionStrenght = Prefab.GrainBurst;
|
||||
PendingAdditionStrength = Prefab.GrainBurst;
|
||||
_strength = strength;
|
||||
Identifier = prefab?.Identifier;
|
||||
|
||||
@@ -91,10 +91,12 @@ namespace Barotrauma
|
||||
|
||||
public override string ToString() => Prefab == null ? "Affliction (Invalid)" : $"Affliction ({Prefab.Name})";
|
||||
|
||||
public AfflictionPrefab.Effect GetActiveEffect() => Prefab.GetActiveEffect(Strength);
|
||||
|
||||
public float GetVitalityDecrease(CharacterHealth characterHealth)
|
||||
{
|
||||
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
||||
if (currentEffect == null) { return 0.0f; }
|
||||
if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) { return 0.0f; }
|
||||
|
||||
@@ -114,7 +116,7 @@ namespace Barotrauma
|
||||
public float GetScreenGrainStrength()
|
||||
{
|
||||
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
||||
if (currentEffect == null) { return 0.0f; }
|
||||
if (MathUtils.NearlyEqual(currentEffect.MaxGrainStrength, 0f)) { return 0.0f; }
|
||||
|
||||
@@ -125,7 +127,7 @@ namespace Barotrauma
|
||||
|
||||
if (Prefab.GrainBurst > 0 && AdditionStrength > amount)
|
||||
{
|
||||
return AdditionStrength;
|
||||
return Math.Min(AdditionStrength, 1.0f);
|
||||
}
|
||||
|
||||
return amount;
|
||||
@@ -134,7 +136,7 @@ namespace Barotrauma
|
||||
public float GetScreenDistortStrength()
|
||||
{
|
||||
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
||||
if (currentEffect == null) { return 0.0f; }
|
||||
if (currentEffect.MaxScreenDistortStrength - currentEffect.MinScreenDistortStrength < 0.0f) { return 0.0f; }
|
||||
|
||||
@@ -147,7 +149,7 @@ namespace Barotrauma
|
||||
public float GetRadialDistortStrength()
|
||||
{
|
||||
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
||||
if (currentEffect == null) { return 0.0f; }
|
||||
if (currentEffect.MaxRadialDistortStrength - currentEffect.MinRadialDistortStrength < 0.0f) { return 0.0f; }
|
||||
|
||||
@@ -160,7 +162,7 @@ namespace Barotrauma
|
||||
public float GetChromaticAberrationStrength()
|
||||
{
|
||||
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
||||
if (currentEffect == null) { return 0.0f; }
|
||||
if (currentEffect.MaxChromaticAberrationStrength - currentEffect.MinChromaticAberrationStrength < 0.0f) { return 0.0f; }
|
||||
|
||||
@@ -173,7 +175,7 @@ namespace Barotrauma
|
||||
public float GetScreenBlurStrength()
|
||||
{
|
||||
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
||||
if (currentEffect == null) { return 0.0f; }
|
||||
if (currentEffect.MaxScreenBlurStrength - currentEffect.MinScreenBlurStrength < 0.0f) { return 0.0f; }
|
||||
|
||||
@@ -186,7 +188,7 @@ namespace Barotrauma
|
||||
public float GetSkillMultiplier()
|
||||
{
|
||||
if (Strength < Prefab.ActivationThreshold) { return 1.0f; }
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
||||
if (currentEffect == null) { return 1.0f; }
|
||||
|
||||
float amount = MathHelper.Lerp(
|
||||
@@ -210,11 +212,11 @@ namespace Barotrauma
|
||||
|
||||
public float GetResistance(string afflictionId)
|
||||
{
|
||||
if (Strength < Prefab.ActivationThreshold) return 0.0f;
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
if (currentEffect == null) return 0.0f;
|
||||
if (currentEffect.MaxResistance - currentEffect.MinResistance <= 0.0f) return 0.0f;
|
||||
if (afflictionId != null && afflictionId != currentEffect.ResistanceFor) return 0.0f;
|
||||
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
|
||||
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
||||
if (currentEffect == null) { return 0.0f; }
|
||||
if (currentEffect.MaxResistance - currentEffect.MinResistance <= 0.0f) { return 0.0f; }
|
||||
if (afflictionId != null && afflictionId != currentEffect.ResistanceFor) { return 0.0f; }
|
||||
|
||||
return MathHelper.Lerp(
|
||||
currentEffect.MinResistance,
|
||||
@@ -224,10 +226,10 @@ namespace Barotrauma
|
||||
|
||||
public float GetSpeedMultiplier()
|
||||
{
|
||||
if (Strength < Prefab.ActivationThreshold) return 1.0f;
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
if (currentEffect == null) return 1.0f;
|
||||
if (currentEffect.MaxSpeedMultiplier - currentEffect.MinSpeedMultiplier <= 0.0f) return 1.0f;
|
||||
if (Strength < Prefab.ActivationThreshold) { return 1.0f; }
|
||||
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
||||
if (currentEffect == null) { return 1.0f; }
|
||||
if (currentEffect.MaxSpeedMultiplier - currentEffect.MinSpeedMultiplier <= 0.0f) { return 1.0f; }
|
||||
|
||||
return MathHelper.Lerp(
|
||||
currentEffect.MinSpeedMultiplier,
|
||||
@@ -250,14 +252,14 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (StatusEffect statusEffect in periodicEffect.StatusEffects)
|
||||
{
|
||||
ApplyStatusEffect(statusEffect, 1.0f, characterHealth, targetLimb);
|
||||
ApplyStatusEffect(ActionType.OnActive, statusEffect, 1.0f, characterHealth, targetLimb);
|
||||
PeriodicEffectTimers[periodicEffect] = Rand.Range(periodicEffect.MinInterval, periodicEffect.MaxInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
||||
if (currentEffect == null) { return; }
|
||||
|
||||
if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted
|
||||
@@ -273,7 +275,7 @@ namespace Barotrauma
|
||||
|
||||
foreach (StatusEffect statusEffect in currentEffect.StatusEffects)
|
||||
{
|
||||
ApplyStatusEffect(statusEffect, deltaTime, characterHealth, targetLimb);
|
||||
ApplyStatusEffect(ActionType.OnActive, statusEffect, deltaTime, characterHealth, targetLimb);
|
||||
}
|
||||
|
||||
float amount = deltaTime;
|
||||
@@ -281,10 +283,10 @@ namespace Barotrauma
|
||||
{
|
||||
amount /= Prefab.GrainBurst;
|
||||
}
|
||||
if (PendingAdditionStrenght >= 0)
|
||||
if (PendingAdditionStrength >= 0)
|
||||
{
|
||||
AdditionStrength += amount;
|
||||
PendingAdditionStrenght -= deltaTime;
|
||||
PendingAdditionStrength -= deltaTime;
|
||||
}
|
||||
else if (AdditionStrength > 0)
|
||||
{
|
||||
@@ -292,27 +294,37 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyStatusEffect(StatusEffect statusEffect, float deltaTime, CharacterHealth characterHealth, Limb targetLimb)
|
||||
public void ApplyStatusEffects(ActionType type, float deltaTime, CharacterHealth characterHealth, Limb targetLimb)
|
||||
{
|
||||
var currentEffect = GetActiveEffect();
|
||||
if (currentEffect != null)
|
||||
{
|
||||
currentEffect.StatusEffects.ForEach(se => ApplyStatusEffect(type, se, deltaTime, characterHealth, targetLimb));
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
||||
public void ApplyStatusEffect(ActionType type, StatusEffect statusEffect, float deltaTime, CharacterHealth characterHealth, Limb targetLimb)
|
||||
{
|
||||
statusEffect.SetUser(Source);
|
||||
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, characterHealth.Character);
|
||||
statusEffect.Apply(type, deltaTime, characterHealth.Character, characterHealth.Character);
|
||||
}
|
||||
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
|
||||
{
|
||||
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, targetLimb);
|
||||
statusEffect.Apply(type, deltaTime, characterHealth.Character, targetLimb);
|
||||
}
|
||||
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
|
||||
{
|
||||
statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
|
||||
statusEffect.Apply(type, deltaTime, targetLimb.character, targets: targetLimb.character.AnimController.Limbs);
|
||||
}
|
||||
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
||||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
var targets = new List<ISerializableEntity>();
|
||||
statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets);
|
||||
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, targets);
|
||||
targets.Clear();
|
||||
targets.AddRange(statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets));
|
||||
statusEffect.Apply(type, deltaTime, characterHealth.Character, targets);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-3
@@ -69,12 +69,15 @@ namespace Barotrauma
|
||||
if (Strength < DormantThreshold)
|
||||
{
|
||||
DeactivateHusk();
|
||||
State = InfectionState.Dormant;
|
||||
if (Strength > Math.Min(1.0f, DormantThreshold))
|
||||
{
|
||||
State = InfectionState.Dormant;
|
||||
}
|
||||
}
|
||||
else if (Strength < ActiveThreshold)
|
||||
{
|
||||
DeactivateHusk();
|
||||
if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: false })
|
||||
if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true })
|
||||
{
|
||||
character.SpeechImpediment = 100;
|
||||
}
|
||||
@@ -128,7 +131,7 @@ namespace Barotrauma
|
||||
character.NeedsAir = false;
|
||||
}
|
||||
|
||||
if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: false })
|
||||
if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true })
|
||||
{
|
||||
character.SpeechImpediment = 100;
|
||||
}
|
||||
|
||||
+4
-1
@@ -678,7 +678,10 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (Effect effect in effects)
|
||||
{
|
||||
if (currentStrength > effect.MinStrength && currentStrength <= effect.MaxStrength) return effect;
|
||||
if (currentStrength > effect.MinStrength && currentStrength <= effect.MaxStrength)
|
||||
{
|
||||
return effect;
|
||||
}
|
||||
}
|
||||
|
||||
//if above the strength range of all effects, use the highest strength effect
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ namespace Barotrauma
|
||||
private float GetDiminishMultiplier()
|
||||
{
|
||||
if (Strength < Prefab.ActivationThreshold) { return 1.0f; }
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
|
||||
if (currentEffect == null) { return 1.0f; }
|
||||
|
||||
float multiplier = MathHelper.Lerp(
|
||||
|
||||
@@ -150,12 +150,9 @@ namespace Barotrauma
|
||||
{
|
||||
max += Character.Info.Job.Prefab.VitalityModifier;
|
||||
}
|
||||
max *= Character.StaticHealthMultiplier;
|
||||
return max * Character.HealthMultiplier;
|
||||
}
|
||||
set
|
||||
{
|
||||
maxVitality = Math.Max(0, value);
|
||||
}
|
||||
}
|
||||
|
||||
public float MinVitality
|
||||
@@ -630,7 +627,10 @@ namespace Barotrauma
|
||||
Kill();
|
||||
}
|
||||
#if CLIENT
|
||||
selectedLimbIndex = -1;
|
||||
if (CharacterHealth.OpenHealthWindow != this)
|
||||
{
|
||||
selectedLimbIndex = -1;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -833,6 +833,32 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
// We need to use another list of the afflictions when we call the status effects triggered by afflictions,
|
||||
// because those status effects may add or remove other afflictions while iterating the collection.
|
||||
private readonly List<Affliction> afflictionsCopy = new List<Affliction>();
|
||||
public void ApplyAfflictionStatusEffects(ActionType type)
|
||||
{
|
||||
for (int i = 0; i < limbHealths.Count; i++)
|
||||
{
|
||||
for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--)
|
||||
{
|
||||
var affliction = limbHealths[i].Afflictions[j];
|
||||
Limb targetLimb = Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == i);
|
||||
if (targetLimb == null)
|
||||
{
|
||||
targetLimb = Character.AnimController.MainLimb;
|
||||
}
|
||||
affliction.ApplyStatusEffects(type, 1.0f, this, targetLimb);
|
||||
}
|
||||
}
|
||||
afflictionsCopy.Clear();
|
||||
afflictionsCopy.AddRange(afflictions);
|
||||
for (int i = afflictionsCopy.Count - 1; i >= 0; i--)
|
||||
{
|
||||
afflictionsCopy[i].ApplyStatusEffects(type, 1.0f, this, targetLimb: null);
|
||||
}
|
||||
}
|
||||
|
||||
public Pair<CauseOfDeathType, Affliction> GetCauseOfDeath()
|
||||
{
|
||||
List<Affliction> currentAfflictions = GetAllAfflictions(true);
|
||||
|
||||
@@ -74,6 +74,20 @@ namespace Barotrauma
|
||||
private string rawAfflictionTypeString;
|
||||
private string[] parsedAfflictionIdentifiers;
|
||||
private string[] parsedAfflictionTypes;
|
||||
public string[] ParsedAfflictionIdentifiers
|
||||
{
|
||||
get
|
||||
{
|
||||
return parsedAfflictionIdentifiers;
|
||||
}
|
||||
}
|
||||
public string[] ParsedAfflictionTypes
|
||||
{
|
||||
get
|
||||
{
|
||||
return parsedAfflictionTypes;
|
||||
}
|
||||
}
|
||||
|
||||
public DamageModifier(XElement element, string parentDebugName)
|
||||
{
|
||||
|
||||
@@ -90,6 +90,7 @@ namespace Barotrauma
|
||||
|
||||
|
||||
public readonly Dictionary<XElement, float> ItemSets = new Dictionary<XElement, float>();
|
||||
public readonly Dictionary<XElement, float> CustomNPCSets = new Dictionary<XElement, float>();
|
||||
|
||||
public HumanPrefab(XElement element, string filePath)
|
||||
{
|
||||
@@ -99,6 +100,7 @@ namespace Barotrauma
|
||||
Job = Job.ToLowerInvariant();
|
||||
Element = element;
|
||||
element.GetChildElements("itemset").ForEach(e => ItemSets.Add(e, e.GetAttributeFloat("commonness", 1)));
|
||||
element.GetChildElements("character").ForEach(e => CustomNPCSets.Add(e, e.GetAttributeFloat("commonness", 1)));
|
||||
PreferredOutpostModuleTypes = element.GetAttributeStringArray("preferredoutpostmoduletypes", new string[0], convertToLowerInvariant: true).ToList();
|
||||
}
|
||||
|
||||
@@ -119,11 +121,12 @@ namespace Barotrauma
|
||||
|
||||
public void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn = null)
|
||||
{
|
||||
npc.CharacterHealth.MaxVitality *= HealthMultiplier;
|
||||
npc.AddStaticHealthMultiplier(HealthMultiplier);
|
||||
if (GameMain.NetworkMember != null)
|
||||
{
|
||||
npc.CharacterHealth.MaxVitality *= HealthMultiplierInMultiplayer;
|
||||
npc.AddStaticHealthMultiplier(HealthMultiplierInMultiplayer);
|
||||
}
|
||||
|
||||
var humanAI = npc.AIController as HumanAIController;
|
||||
if (humanAI != null)
|
||||
{
|
||||
@@ -160,18 +163,24 @@ namespace Barotrauma
|
||||
var spawnItems = ToolBox.SelectWeightedRandom(ItemSets.Keys.ToList(), ItemSets.Values.ToList(), randSync);
|
||||
foreach (XElement itemElement in spawnItems.GetChildElements("item"))
|
||||
{
|
||||
InitializeItems(character, itemElement, submarine, createNetworkEvents: createNetworkEvents);
|
||||
InitializeItem(character, itemElement, submarine, this, createNetworkEvents: createNetworkEvents);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeItems(Character character, XElement itemElement, Submarine submarine, Item parentItem = null, bool createNetworkEvents = true)
|
||||
public CharacterInfo GetCharacterInfo(Rand.RandSync randSync = Rand.RandSync.Unsynced)
|
||||
{
|
||||
var characterElement = ToolBox.SelectWeightedRandom(CustomNPCSets.Keys.ToList(), CustomNPCSets.Values.ToList(), randSync);
|
||||
return characterElement != null ? new CharacterInfo(characterElement) : null;
|
||||
}
|
||||
|
||||
public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, Item parentItem = null, bool createNetworkEvents = true)
|
||||
{
|
||||
ItemPrefab itemPrefab;
|
||||
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
|
||||
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Tried to spawn \"" + Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.");
|
||||
DebugConsole.ThrowError("Tried to spawn \"" + humanPrefab?.Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.");
|
||||
return;
|
||||
}
|
||||
Item item = new Item(itemPrefab, character.Position, null);
|
||||
@@ -192,7 +201,11 @@ namespace Barotrauma
|
||||
#endif
|
||||
if (itemElement.GetAttributeBool("equip", false))
|
||||
{
|
||||
List<InvSlotType> allowedSlots = new List<InvSlotType>(item.AllowedSlots);
|
||||
//if the item is both pickable and wearable, try to wear it instead of picking it up
|
||||
List<InvSlotType> allowedSlots =
|
||||
item.GetComponents<Pickable>().Count() > 1 ?
|
||||
new List<InvSlotType>(item.GetComponent<Wearable>()?.AllowedSlots ?? item.GetComponent<Pickable>().AllowedSlots) :
|
||||
new List<InvSlotType>(item.AllowedSlots);
|
||||
allowedSlots.Remove(InvSlotType.Any);
|
||||
|
||||
character.Inventory.TryPutItem(item, null, allowedSlots);
|
||||
@@ -215,10 +228,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
IdCard idCardComponent = item.GetComponent<IdCard>();
|
||||
if (idCardComponent != null)
|
||||
{
|
||||
idCardComponent.Initialize(character.Info);
|
||||
}
|
||||
idCardComponent?.Initialize(character.Info);
|
||||
|
||||
var idCardTags = itemElement.GetAttributeStringArray("tags", new string[0]);
|
||||
foreach (string tag in idCardTags)
|
||||
@@ -231,13 +241,10 @@ namespace Barotrauma
|
||||
{
|
||||
wifiComponent.TeamID = character.TeamID;
|
||||
}
|
||||
if (parentItem != null)
|
||||
{
|
||||
parentItem.Combine(item, user: null);
|
||||
}
|
||||
parentItem?.Combine(item, user: null);
|
||||
foreach (XElement childItemElement in itemElement.Elements())
|
||||
{
|
||||
InitializeItems(character, childItemElement, submarine, item, createNetworkEvents);
|
||||
InitializeItem(character, childItemElement, submarine, humanPrefab, item, createNetworkEvents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,9 +158,12 @@ namespace Barotrauma
|
||||
|
||||
if (itemElement.GetAttributeBool("equip", false))
|
||||
{
|
||||
List<InvSlotType> allowedSlots = new List<InvSlotType>(item.AllowedSlots);
|
||||
//if the item is both pickable and wearable, try to wear it instead of picking it up
|
||||
List<InvSlotType> allowedSlots =
|
||||
item.GetComponents<Pickable>().Count() > 1 ?
|
||||
new List<InvSlotType>(item.GetComponent<Wearable>()?.AllowedSlots ?? item.GetComponent<Pickable>().AllowedSlots) :
|
||||
new List<InvSlotType>(item.AllowedSlots);
|
||||
allowedSlots.Remove(InvSlotType.Any);
|
||||
|
||||
character.Inventory.TryPutItem(item, null, allowedSlots);
|
||||
}
|
||||
else
|
||||
@@ -201,10 +204,7 @@ namespace Barotrauma
|
||||
item.AddTag("job:" + Name);
|
||||
|
||||
IdCard idCardComponent = item.GetComponent<IdCard>();
|
||||
if (idCardComponent != null)
|
||||
{
|
||||
idCardComponent.Initialize(character.Info);
|
||||
}
|
||||
idCardComponent?.Initialize(character.Info);
|
||||
}
|
||||
|
||||
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -43,6 +43,12 @@ namespace Barotrauma
|
||||
Prefabs.Remove(this);
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, float> _itemRepairPriorities = new Dictionary<string, float>();
|
||||
/// <summary>
|
||||
/// Tag -> priority.
|
||||
/// </summary>
|
||||
public static IReadOnlyDictionary<string, float> ItemRepairPriorities => _itemRepairPriorities;
|
||||
|
||||
public static XElement NoJobElement;
|
||||
public static JobPrefab Get(string identifier)
|
||||
{
|
||||
@@ -178,6 +184,14 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
//whether the job should be available to NPCs
|
||||
[Serialize(false, false)]
|
||||
public bool HiddenJob
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Sprite Icon;
|
||||
public Sprite IconSmall;
|
||||
|
||||
@@ -195,7 +209,7 @@ namespace Barotrauma
|
||||
SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
Name = TextManager.Get("JobName." + Identifier);
|
||||
Description = TextManager.Get("JobDescription." + Identifier);
|
||||
Description = TextManager.Get("JobDescription." + Identifier, returnNull: true) ?? string.Empty;
|
||||
Identifier = Identifier.ToLowerInvariant();
|
||||
Element = element;
|
||||
|
||||
@@ -265,7 +279,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
|
||||
public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(p => p.Identifier != "watchman", sync);
|
||||
public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(p => !p.HiddenJob, sync);
|
||||
|
||||
public static void LoadAll(IEnumerable<ContentFile> files)
|
||||
{
|
||||
@@ -286,7 +300,6 @@ namespace Barotrauma
|
||||
}
|
||||
foreach (XElement element in mainElement.Elements())
|
||||
{
|
||||
if (element.Name.ToString().Equals("nojob", StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
if (element.IsOverride())
|
||||
{
|
||||
var job = new JobPrefab(element.FirstElement(), file.Path)
|
||||
@@ -297,6 +310,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!element.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
var job = new JobPrefab(element, file.Path)
|
||||
{
|
||||
ContentPackage = file.ContentPackage
|
||||
@@ -304,8 +318,31 @@ namespace Barotrauma
|
||||
Prefabs.Add(job, false);
|
||||
}
|
||||
}
|
||||
NoJobElement = NoJobElement ?? mainElement.Element("NoJob");
|
||||
NoJobElement = NoJobElement ?? mainElement.Element("nojob");
|
||||
NoJobElement ??= mainElement.GetChildElement("nojob");
|
||||
var itemRepairPrioritiesElement = mainElement.GetChildElement("ItemRepairPriorities");
|
||||
if (itemRepairPrioritiesElement != null)
|
||||
{
|
||||
foreach (var subElement in itemRepairPrioritiesElement.Elements())
|
||||
{
|
||||
string tag = subElement.GetAttributeString("tag", null);
|
||||
if (tag != null)
|
||||
{
|
||||
float priority = subElement.GetAttributeFloat("priority", -1f);
|
||||
if (priority >= 0)
|
||||
{
|
||||
_itemRepairPriorities.TryAdd(tag, priority);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.AddWarning($"The 'priority' attribute is missing from the the item repair priorities definition in {subElement} of {file.Path}.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.AddWarning($"The 'tag' attribute is missing from the the item repair priorities definition in {subElement} of {file.Path}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemoveByFile(string filePath)
|
||||
|
||||
@@ -683,7 +683,7 @@ namespace Barotrauma
|
||||
private readonly List<DamageModifier> appliedDamageModifiers = new List<DamageModifier>();
|
||||
private readonly List<DamageModifier> tempModifiers = new List<DamageModifier>();
|
||||
private readonly List<Affliction> afflictionsCopy = new List<Affliction>();
|
||||
public AttackResult AddDamage(Vector2 simPosition, IEnumerable<Affliction> afflictions, bool playSound, float damageMultiplier = 1)
|
||||
public AttackResult AddDamage(Vector2 simPosition, IEnumerable<Affliction> afflictions, bool playSound, float damageMultiplier = 1, float penetration = 0f)
|
||||
{
|
||||
appliedDamageModifiers.Clear();
|
||||
afflictionsCopy.Clear();
|
||||
@@ -726,7 +726,12 @@ namespace Barotrauma
|
||||
float finalDamageModifier = damageMultiplier;
|
||||
foreach (DamageModifier damageModifier in tempModifiers)
|
||||
{
|
||||
finalDamageModifier *= damageModifier.DamageMultiplier;
|
||||
float damageModifierValue = damageModifier.DamageMultiplier;
|
||||
if (damageModifier.DeflectProjectiles && damageModifierValue < 1f)
|
||||
{
|
||||
damageModifierValue = MathHelper.Lerp(damageModifierValue, 1f, penetration);
|
||||
}
|
||||
finalDamageModifier *= damageModifierValue;
|
||||
}
|
||||
if (!MathUtils.NearlyEqual(finalDamageModifier, 1.0f))
|
||||
{
|
||||
@@ -832,10 +837,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (attack != null)
|
||||
{
|
||||
attack.UpdateCoolDown(deltaTime);
|
||||
}
|
||||
attack?.UpdateCoolDown(deltaTime);
|
||||
}
|
||||
|
||||
private float reEnableTimer = -1;
|
||||
@@ -981,13 +983,16 @@ namespace Barotrauma
|
||||
NetEntityEvent.Type.ExecuteAttack,
|
||||
this,
|
||||
(damageTarget as Entity)?.ID ?? Entity.NullEntityID,
|
||||
damageTarget is Character && targetLimb != null ? Array.IndexOf(((Character)damageTarget).AnimController.Limbs, targetLimb) : 0
|
||||
damageTarget is Character && targetLimb != null ? Array.IndexOf(((Character)damageTarget).AnimController.Limbs, targetLimb) : 0,
|
||||
attackSimPos.X,
|
||||
attackSimPos.Y
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
Vector2 diff = attackSimPos - SimPosition;
|
||||
bool applyForces = !attack.ApplyForcesOnlyOnce || !wasRunning;
|
||||
|
||||
if (applyForces)
|
||||
{
|
||||
if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Count > 0)
|
||||
@@ -1143,7 +1148,7 @@ namespace Barotrauma
|
||||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
targets.Clear();
|
||||
statusEffect.GetNearbyTargets(WorldPosition, targets);
|
||||
targets.AddRange(statusEffect.GetNearbyTargets(WorldPosition, targets));
|
||||
statusEffect.Apply(actionType, deltaTime, character, targets);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -526,7 +526,7 @@ namespace Barotrauma
|
||||
[Serialize(false, true, description: "Does the character attack when provoked? When enabled, overrides the predefined targeting state with Attack and increases the priority of it."), Editable()]
|
||||
public bool AttackWhenProvoked { get; private set; }
|
||||
|
||||
[Serialize(true, true, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable]
|
||||
[Serialize(false, true, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable]
|
||||
public bool AvoidGunfire { get; private set; }
|
||||
|
||||
[Serialize(3f, true, description: "How long the creature avoids gunfire. Also used when the creature is unlatched."), Editable(minValue: 0f, maxValue: 100f)]
|
||||
@@ -547,8 +547,8 @@ namespace Barotrauma
|
||||
[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."), Editable]
|
||||
public bool Infiltrate { get; private set; }
|
||||
[Serialize(false, true, description:"Does the creature know how to open doors (still requires a proper ID card). Only applies on humanoids. Humans can always open doors (They don't use this AI definition)."), Editable]
|
||||
public bool CanOpenDoors { 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; }
|
||||
@@ -565,6 +565,9 @@ namespace Barotrauma
|
||||
[Serialize(0f, true, description: ""), Editable]
|
||||
public float AggressionCumulation { get; private set; }
|
||||
|
||||
[Serialize(WallTargetingMethod.Target, true, description: ""), Editable]
|
||||
public WallTargetingMethod WallTargetingMethod { get; private set; }
|
||||
|
||||
public IEnumerable<TargetParams> Targets => targets;
|
||||
protected readonly List<TargetParams> targets = new List<TargetParams>();
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@ namespace Barotrauma
|
||||
Corpses,
|
||||
WreckAIConfig,
|
||||
UpgradeModules,
|
||||
MapCreature
|
||||
MapCreature,
|
||||
EnemySubmarine
|
||||
}
|
||||
|
||||
public class ContentPackage
|
||||
@@ -101,7 +102,8 @@ namespace Barotrauma
|
||||
ContentType.Orders,
|
||||
ContentType.Corpses,
|
||||
ContentType.UpgradeModules,
|
||||
ContentType.MapCreature
|
||||
ContentType.MapCreature,
|
||||
ContentType.EnemySubmarine
|
||||
};
|
||||
|
||||
//at least one file of each these types is required in core content packages
|
||||
@@ -132,7 +134,8 @@ namespace Barotrauma
|
||||
ContentType.EventManagerSettings,
|
||||
ContentType.Orders,
|
||||
ContentType.Corpses,
|
||||
ContentType.UpgradeModules
|
||||
ContentType.UpgradeModules,
|
||||
ContentType.EnemySubmarine
|
||||
};
|
||||
|
||||
public static IEnumerable<ContentType> CorePackageRequiredFiles
|
||||
@@ -191,13 +194,13 @@ namespace Barotrauma
|
||||
isCorePackage = value;
|
||||
if (isCorePackage && regularPackages.Contains(this))
|
||||
{
|
||||
corePackages.Add(this);
|
||||
regularPackages.Remove(this);
|
||||
corePackages.AddOnMainThread(this);
|
||||
regularPackages.RemoveOnMainThread(this);
|
||||
}
|
||||
else if (!isCorePackage && corePackages.Contains(this))
|
||||
{
|
||||
regularPackages.Add(this);
|
||||
corePackages.Remove(this);
|
||||
regularPackages.AddOnMainThread(this);
|
||||
corePackages.RemoveOnMainThread(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,7 +215,6 @@ namespace Barotrauma
|
||||
private readonly List<ContentFile> filesToAdd;
|
||||
private readonly List<ContentFile> filesToRemove;
|
||||
|
||||
|
||||
public IReadOnlyList<ContentFile> Files
|
||||
{
|
||||
get { return files; }
|
||||
@@ -409,6 +411,7 @@ namespace Barotrauma
|
||||
case ContentType.Submarine:
|
||||
case ContentType.Wreck:
|
||||
case ContentType.BeaconStation:
|
||||
case ContentType.EnemySubmarine:
|
||||
break;
|
||||
default:
|
||||
try
|
||||
@@ -526,7 +529,7 @@ namespace Barotrauma
|
||||
{
|
||||
refreshFiles = true;
|
||||
}
|
||||
corePackages.Remove(p);
|
||||
corePackages.RemoveOnMainThread(p);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -534,16 +537,16 @@ namespace Barotrauma
|
||||
{
|
||||
refreshFiles = true;
|
||||
}
|
||||
regularPackages.Remove(p);
|
||||
regularPackages.RemoveOnMainThread(p);
|
||||
}
|
||||
}
|
||||
if (IsCorePackage)
|
||||
{
|
||||
corePackages.Add(this);
|
||||
corePackages.AddOnMainThread(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
regularPackages.Add(this);
|
||||
regularPackages.AddOnMainThread(this);
|
||||
}
|
||||
|
||||
if (refreshFiles)
|
||||
@@ -609,7 +612,8 @@ namespace Barotrauma
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Error while calculating content package hash: ", e);
|
||||
DebugConsole.ThrowError($"Error while calculating the MD5 hash of the content package \"{Name}\" (file path: {Path}). The content package may be corrupted. You may want to delete or reinstall the package.", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -740,18 +744,18 @@ namespace Barotrauma
|
||||
}
|
||||
if (newPackage.IsCorePackage)
|
||||
{
|
||||
corePackages.Add(newPackage);
|
||||
corePackages.AddOnMainThread(newPackage);
|
||||
}
|
||||
else
|
||||
{
|
||||
regularPackages.Add(newPackage);
|
||||
regularPackages.AddOnMainThread(newPackage);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RemovePackage(ContentPackage package)
|
||||
{
|
||||
if (package.IsCorePackage) { corePackages.Remove(package); }
|
||||
else { regularPackages.Remove(package); }
|
||||
if (package.IsCorePackage) { corePackages.RemoveOnMainThread(package); }
|
||||
else { regularPackages.RemoveOnMainThread(package); }
|
||||
}
|
||||
|
||||
public static void LoadAll()
|
||||
@@ -772,9 +776,9 @@ namespace Barotrauma
|
||||
|
||||
IEnumerable<string> files = Directory.GetFiles(folder, "*.xml");
|
||||
|
||||
corePackages.Clear();
|
||||
corePackages.ClearOnMainThread();
|
||||
var prevRegularPackages = regularPackages.Select(p => p.Name.ToLowerInvariant()).ToList();
|
||||
regularPackages.Clear();
|
||||
regularPackages.ClearOnMainThread();
|
||||
|
||||
foreach (string filePath in files)
|
||||
{
|
||||
@@ -812,7 +816,7 @@ namespace Barotrauma
|
||||
.OrderBy(p => order(p))
|
||||
.ThenBy(p => regularPackages.IndexOf(p))
|
||||
.ToList();
|
||||
regularPackages.Clear(); regularPackages.AddRange(ordered);
|
||||
regularPackages.ClearOnMainThread(); regularPackages.AddRangeOnMainThread(ordered);
|
||||
(config ?? GameMain.Config)?.SortContentPackages(refreshAll);
|
||||
}
|
||||
|
||||
@@ -822,12 +826,12 @@ namespace Barotrauma
|
||||
{
|
||||
if (IsCorePackage)
|
||||
{
|
||||
corePackages.Remove(this);
|
||||
corePackages.RemoveOnMainThread(this);
|
||||
if (GameMain.Config.CurrentCorePackage == this) { GameMain.Config.AutoSelectCorePackage(null); }
|
||||
}
|
||||
else
|
||||
{
|
||||
regularPackages.Remove(this);
|
||||
regularPackages.RemoveOnMainThread(this);
|
||||
if (GameMain.Config.EnabledRegularPackages.Contains(this)) { GameMain.Config.DisableRegularPackage(this); }
|
||||
}
|
||||
GameMain.Config.SaveNewPlayerConfig();
|
||||
|
||||
@@ -196,7 +196,7 @@ namespace Barotrauma
|
||||
UpdaterUtil.SaveFileList("filelist.xml");
|
||||
}));
|
||||
|
||||
commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null,
|
||||
commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team (0-3)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null,
|
||||
() =>
|
||||
{
|
||||
List<string> characterFiles = GameMain.Instance.GetFilesOfType(ContentType.Character).Select(f => f.Path).ToList();
|
||||
@@ -652,15 +652,20 @@ namespace Barotrauma
|
||||
};
|
||||
}, isCheat: true));
|
||||
|
||||
commands.Add(new Command("heal", "heal [character name]: Restore the specified character to full health. If the name parameter is omitted, the controlled character will be healed.", (string[] args) =>
|
||||
commands.Add(new Command("heal", "heal [character name] [all]: Restore the specified character to full health. If the name parameter is omitted, the controlled character will be healed. By default only heals common afflictions such as physical damage and blood loss: use the \"all\" argument to heal everything, including poisonings/addictions/etc.", (string[] args) =>
|
||||
{
|
||||
Character healedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args);
|
||||
bool healAll = args.Length > 1 && args[1].Equals("all", StringComparison.OrdinalIgnoreCase);
|
||||
Character healedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(healAll ? args.Take(args.Length - 1).ToArray() : args);
|
||||
if (healedCharacter != null)
|
||||
{
|
||||
healedCharacter.SetAllDamage(0.0f, 0.0f, 0.0f);
|
||||
healedCharacter.Oxygen = 100.0f;
|
||||
healedCharacter.Bloodloss = 0.0f;
|
||||
healedCharacter.SetStun(0.0f, true);
|
||||
if (healAll)
|
||||
{
|
||||
healedCharacter.CharacterHealth.RemoveAllAfflictions();
|
||||
}
|
||||
}
|
||||
},
|
||||
() =>
|
||||
@@ -1449,6 +1454,20 @@ namespace Barotrauma
|
||||
return new[] { primaries, identifiers };
|
||||
}));
|
||||
|
||||
commands.Add(new Command("setdifficulty|forcedifficulty", "difficulty [0-100]. Leave the parameter empty to disable.", (string[] args) =>
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
Level.ForcedDifficulty = null;
|
||||
NewMessage($"Forced difficulty level disabled.", Color.Green);
|
||||
}
|
||||
else if (float.TryParse(args[0], out float difficulty))
|
||||
{
|
||||
Level.ForcedDifficulty = difficulty;
|
||||
NewMessage($"Set the difficulty level to { Level.ForcedDifficulty }.", Color.Yellow);
|
||||
}
|
||||
}, isCheat: true));
|
||||
|
||||
commands.Add(new Command("difficulty|leveldifficulty", "difficulty [0-100]: Change the level difficulty setting in the server lobby.", null));
|
||||
|
||||
commands.Add(new Command("autoitemplacerdebug|outfitdebug", "autoitemplacerdebug: Toggle automatic item placer debug info on/off. The automatically placed items are listed in the debug console at the start of a round.", (string[] args) =>
|
||||
@@ -1592,7 +1611,7 @@ namespace Barotrauma
|
||||
commands.Add(new Command("control", "control [character name]: Start controlling the specified character (client-only).", null, () =>
|
||||
{
|
||||
return new string[][] { ListCharacterNames() };
|
||||
}));
|
||||
}, isCheat: true));
|
||||
commands.Add(new Command("los", "Toggle the line of sight effect on/off (client-only).", null, isCheat: true));
|
||||
commands.Add(new Command("lighting|lights", "Toggle lighting on/off (client-only).", null, isCheat: true));
|
||||
commands.Add(new Command("ambientlight", "ambientlight [color]: Change the color of the ambient light in the level.", null, isCheat: true));
|
||||
@@ -1886,6 +1905,18 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(args[0])) { return; }
|
||||
CharacterTeamType teamType = Character.Controlled != null ? Character.Controlled.TeamID : CharacterTeamType.Team1;
|
||||
if (args.Length > 2)
|
||||
{
|
||||
try
|
||||
{
|
||||
teamType = (CharacterTeamType)int.Parse(args[2]);
|
||||
}
|
||||
catch
|
||||
{
|
||||
DebugConsole.ThrowError($"\"{args[2]}\" is not a valid team id.");
|
||||
}
|
||||
}
|
||||
|
||||
if (spawnPoint != null) { spawnPosition = spawnPoint.WorldPosition; }
|
||||
|
||||
@@ -1896,8 +1927,7 @@ namespace Barotrauma
|
||||
spawnedCharacter = Character.Create(characterInfo, spawnPosition, ToolBox.RandomSeed(8));
|
||||
if (GameMain.GameSession != null)
|
||||
{
|
||||
//TODO: a way to select which team to spawn to?
|
||||
spawnedCharacter.TeamID = Character.Controlled != null ? Character.Controlled.TeamID : CharacterTeamType.Team1;
|
||||
spawnedCharacter.TeamID = teamType;
|
||||
#if CLIENT
|
||||
GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter);
|
||||
#endif
|
||||
|
||||
@@ -23,7 +23,17 @@ namespace Barotrauma
|
||||
|
||||
protected PropertyConditional.OperatorType Operator { get; set; }
|
||||
|
||||
public CheckDataAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
|
||||
public CheckDataAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Condition))
|
||||
{
|
||||
Condition = element.GetAttributeString("value", string.Empty);
|
||||
if (string.IsNullOrEmpty(Condition))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in scripted event \"{parentEvent.Prefab.Identifier}\". CheckDataAction with no condition set ({element}).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool? DetermineSuccess()
|
||||
{
|
||||
|
||||
@@ -179,7 +179,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (speaker == null) { return; }
|
||||
speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
|
||||
speaker.ActiveConversation = this;
|
||||
speaker.ActiveConversation = null;
|
||||
speaker.SetCustomInteract(null, null);
|
||||
#if SERVER
|
||||
GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
|
||||
@@ -213,7 +213,14 @@ namespace Barotrauma
|
||||
if (dialogOpened)
|
||||
{
|
||||
#if CLIENT
|
||||
Character.DisableControls = true;
|
||||
if (GUIMessageBox.MessageBoxes.Any(mb => mb.UserData as string == "ConversationAction"))
|
||||
{
|
||||
Character.DisableControls = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
#endif
|
||||
if (ShouldInterrupt())
|
||||
{
|
||||
@@ -240,7 +247,7 @@ namespace Barotrauma
|
||||
{
|
||||
TryStartConversation(speaker);
|
||||
}
|
||||
else
|
||||
else if (speaker.ActiveConversation != this)
|
||||
{
|
||||
speaker.CampaignInteractionType = CampaignMode.InteractionType.Talk;
|
||||
speaker.ActiveConversation = this;
|
||||
@@ -303,9 +310,15 @@ namespace Barotrauma
|
||||
|
||||
private bool IsValidTarget(Entity e)
|
||||
{
|
||||
return
|
||||
e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated &&
|
||||
bool isValid = e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated &&
|
||||
(e == Character.Controlled || character.IsRemotePlayer);
|
||||
#if SERVER
|
||||
UpdateIgnoredClients();
|
||||
isValid &= !ignoredClients.Keys.Any(c => c.Character == e);
|
||||
#elif CLIENT
|
||||
isValid &= (e != Character.Controlled || !GUI.InputBlockingMenuOpen);
|
||||
#endif
|
||||
return isValid;
|
||||
}
|
||||
|
||||
private void TryStartConversation(Character speaker, Character targetCharacter = null)
|
||||
@@ -348,10 +361,18 @@ namespace Barotrauma
|
||||
{
|
||||
ParentEvent.AddTarget(InvokerTag, targetCharacter);
|
||||
}
|
||||
|
||||
|
||||
ShowDialog(speaker, targetCharacter);
|
||||
|
||||
dialogOpened = true;
|
||||
if (speaker != null)
|
||||
{
|
||||
speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
|
||||
speaker.SetCustomInteract(null, null);
|
||||
#if SERVER
|
||||
GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
partial void ShowDialog(Character speaker, Character targetCharacter);
|
||||
|
||||
@@ -14,6 +14,18 @@ namespace Barotrauma
|
||||
[Serialize("", true)]
|
||||
public string MissionTag { get; set; }
|
||||
|
||||
[Serialize("", true, description: "The type of the location the mission will be unlocked in (if empty, any location can be selected).")]
|
||||
public string LocationType { get; set; }
|
||||
|
||||
[Serialize(0, true, description: "Minimum distance to the location the mission is unlocked in (1 = one path between locations).")]
|
||||
public int MinLocationDistance { get; set; }
|
||||
|
||||
[Serialize(true, true, description: "If true, the mission has to be unlocked in a location further on the campaign map.")]
|
||||
public bool UnlockFurtherOnMap { get; set; }
|
||||
|
||||
[Serialize(false, true, description: "If true, a suitable location is forced on the map if one isn't found.")]
|
||||
public bool CreateLocationIfNotFound { get; set; }
|
||||
|
||||
private bool isFinished;
|
||||
|
||||
public MissionAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
|
||||
@@ -44,33 +56,82 @@ namespace Barotrauma
|
||||
if (GameMain.GameSession.GameMode is CampaignMode campaign)
|
||||
{
|
||||
MissionPrefab prefab = null;
|
||||
if (!string.IsNullOrEmpty(MissionIdentifier))
|
||||
var unlockLocation = FindUnlockLocation();
|
||||
if (unlockLocation == null && CreateLocationIfNotFound)
|
||||
{
|
||||
prefab = campaign.Map.CurrentLocation.UnlockMissionByIdentifier(MissionIdentifier);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(MissionTag))
|
||||
{
|
||||
prefab = campaign.Map.CurrentLocation.UnlockMissionByTag(MissionTag);
|
||||
}
|
||||
if (campaign is MultiPlayerCampaign mpCampaign)
|
||||
{
|
||||
mpCampaign.LastUpdateID++;
|
||||
//find an empty location at least 3 steps away, further on the map
|
||||
var emptyLocation = FindUnlockLocationRecursive(campaign.Map.CurrentLocation, Math.Max(MinLocationDistance, 3), "none", true, new HashSet<Location>());
|
||||
if (emptyLocation != null)
|
||||
{
|
||||
emptyLocation.ChangeType(Barotrauma.LocationType.List.Find(lt => lt.Identifier.Equals(LocationType, StringComparison.OrdinalIgnoreCase)));
|
||||
unlockLocation = emptyLocation;
|
||||
}
|
||||
}
|
||||
|
||||
if (prefab != null)
|
||||
if (unlockLocation != null)
|
||||
{
|
||||
#if CLIENT
|
||||
new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", prefab.Name),
|
||||
new string[0], type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128))
|
||||
if (!string.IsNullOrEmpty(MissionIdentifier))
|
||||
{
|
||||
IconColor = prefab.IconColor
|
||||
};
|
||||
#else
|
||||
NotifyMissionUnlock(prefab);
|
||||
#endif
|
||||
prefab = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(MissionTag))
|
||||
{
|
||||
prefab = unlockLocation.UnlockMissionByTag(MissionTag);
|
||||
}
|
||||
if (campaign is MultiPlayerCampaign mpCampaign)
|
||||
{
|
||||
mpCampaign.LastUpdateID++;
|
||||
}
|
||||
if (prefab != null)
|
||||
{
|
||||
DebugConsole.NewMessage($"Unlocked mission \"{prefab.Name}\" in the location \"{unlockLocation.Name}\".");
|
||||
#if CLIENT
|
||||
new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", prefab.Name),
|
||||
new string[0], type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128))
|
||||
{
|
||||
IconColor = prefab.IconColor
|
||||
};
|
||||
#else
|
||||
NotifyMissionUnlock(prefab);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.AddWarning($"Failed to find a suitable location to unlock a mission in (LocationType: {LocationType}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})");
|
||||
}
|
||||
}
|
||||
isFinished = true;
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
private Location FindUnlockLocation()
|
||||
{
|
||||
var campaign = GameMain.GameSession.GameMode as CampaignMode;
|
||||
if (string.IsNullOrEmpty(LocationType) && MinLocationDistance <= 1)
|
||||
{
|
||||
return campaign.Map.CurrentLocation;
|
||||
}
|
||||
|
||||
return FindUnlockLocationRecursive(campaign.Map.CurrentLocation, 0, LocationType, UnlockFurtherOnMap, new HashSet<Location>());
|
||||
}
|
||||
|
||||
private Location FindUnlockLocationRecursive(Location currLocation, int currDistance, string locationType, bool unlockFurtherOnMap, HashSet<Location> checkedLocations)
|
||||
{
|
||||
var campaign = GameMain.GameSession.GameMode as CampaignMode;
|
||||
if (currLocation.Type.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase) && currDistance >= MinLocationDistance &&
|
||||
(!unlockFurtherOnMap || currLocation.MapPosition.X > campaign.Map.CurrentLocation.MapPosition.X))
|
||||
{
|
||||
return currLocation;
|
||||
}
|
||||
checkedLocations.Add(currLocation);
|
||||
foreach (LocationConnection connection in currLocation.Connections)
|
||||
{
|
||||
var otherLocation = connection.OtherLocation(currLocation);
|
||||
if (checkedLocations.Contains(otherLocation)) { continue; }
|
||||
var unlockLocation = FindUnlockLocationRecursive(otherLocation, ++currDistance, locationType, unlockFurtherOnMap, checkedLocations);
|
||||
if (unlockLocation != null) { return unlockLocation; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public override string ToDebugString()
|
||||
@@ -84,8 +145,8 @@ namespace Barotrauma
|
||||
foreach (Client client in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
IWriteMessage outmsg = new WriteOnlyMessage();
|
||||
outmsg.Write((byte) ServerPacketHeader.EVENTACTION);
|
||||
outmsg.Write((byte) EventManager.NetworkEventType.MISSION);
|
||||
outmsg.Write((byte)ServerPacketHeader.EVENTACTION);
|
||||
outmsg.Write((byte)EventManager.NetworkEventType.MISSION);
|
||||
outmsg.Write(prefab.Identifier);
|
||||
GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using Barotrauma.Networking;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class NPCChangeTeamAction : EventAction
|
||||
{
|
||||
[Serialize("", true)]
|
||||
public string NPCTag { get; set; }
|
||||
|
||||
[Serialize(0, true)]
|
||||
public int TeamTag { get; set; }
|
||||
|
||||
[Serialize(false, true)]
|
||||
public bool AddToCrew { get; set; }
|
||||
|
||||
private bool isFinished = false;
|
||||
|
||||
public NPCChangeTeamAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
|
||||
|
||||
private List<Character> affectedNpcs = null;
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (isFinished) { return; }
|
||||
|
||||
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList();
|
||||
foreach (var npc in affectedNpcs)
|
||||
{
|
||||
CharacterTeamType newTeam = (CharacterTeamType)TeamTag;
|
||||
// characters will still remain on friendlyNPC team for rest of the tick
|
||||
npc.SetOriginalTeam(newTeam);
|
||||
|
||||
if (AddToCrew && (newTeam == CharacterTeamType.Team1 || newTeam == CharacterTeamType.Team2))
|
||||
{
|
||||
npc.Info.StartItemsGiven = true;
|
||||
|
||||
GameMain.GameSession.CrewManager.AddCharacter(npc);
|
||||
foreach (Item item in npc.Inventory.AllItems)
|
||||
{
|
||||
item.AllowStealing = true;
|
||||
var wifiComponent = item.GetComponent<Items.Components.WifiComponent>();
|
||||
if (wifiComponent != null)
|
||||
{
|
||||
wifiComponent.TeamID = newTeam;
|
||||
}
|
||||
}
|
||||
#if SERVER
|
||||
GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AddToCrew, newTeam, npc.Inventory.AllItems.Select(it => it.ID).ToArray() });
|
||||
#endif
|
||||
}
|
||||
}
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
public override bool IsFinished(ref string goTo)
|
||||
{
|
||||
return isFinished;
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
isFinished = false;
|
||||
}
|
||||
|
||||
public override string ToDebugString()
|
||||
{
|
||||
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(NPCChangeTeamAction)} -> (NPCTag: {NPCTag.ColorizeObject()})";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,9 +48,9 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var goToObjective in humanAiController.ObjectiveManager.GetActiveObjectives<AIObjectiveGoTo>())
|
||||
foreach (var objective in humanAiController.ObjectiveManager.Objectives)
|
||||
{
|
||||
if (goToObjective.Target == target)
|
||||
if (objective is AIObjectiveGoTo goToObjective && goToObjective.Target == target)
|
||||
{
|
||||
goToObjective.Abandon = true;
|
||||
}
|
||||
|
||||
@@ -20,11 +20,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ItemIdentifier))
|
||||
{
|
||||
ItemIdentifier = element.GetAttributeString("itemidentifiers", "");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(ItemIdentifier))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\" - RemoveItemAction without an item identifier.");
|
||||
ItemIdentifier = element.GetAttributeString("itemidentifiers", null) ?? element.GetAttributeString("identifier", "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +43,7 @@ namespace Barotrauma
|
||||
bool hasValidTargets = false;
|
||||
foreach (Entity target in targets)
|
||||
{
|
||||
if (target is Character character && character.Inventory != null)
|
||||
if (target is Character character && character.Inventory != null || target is Item)
|
||||
{
|
||||
hasValidTargets = true;
|
||||
break;
|
||||
@@ -55,20 +51,31 @@ namespace Barotrauma
|
||||
}
|
||||
if (!hasValidTargets) { return; }
|
||||
|
||||
List<Item> usedItems = new List<Item>();
|
||||
HashSet<Item> removedItems = new HashSet<Item>();
|
||||
foreach (Entity target in targets)
|
||||
{
|
||||
Inventory inventory = (target as Character)?.Inventory;
|
||||
if (inventory == null) { continue; }
|
||||
while (usedItems.Count < Amount)
|
||||
if (inventory != null)
|
||||
{
|
||||
var item = inventory.FindItem(it =>
|
||||
it != null &&
|
||||
!usedItems.Contains(it) &&
|
||||
it.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase), recursive: true);
|
||||
if (item == null) { break; }
|
||||
Entity.Spawner.AddToRemoveQueue(item);
|
||||
usedItems.Add(item);
|
||||
while (removedItems.Count < Amount)
|
||||
{
|
||||
var item = inventory.FindItem(it =>
|
||||
it != null &&
|
||||
!removedItems.Contains(it) &&
|
||||
(string.IsNullOrEmpty(ItemIdentifier) || it.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase)), recursive: true);
|
||||
if (item == null) { break; }
|
||||
Entity.Spawner.AddToRemoveQueue(item);
|
||||
removedItems.Add(item);
|
||||
}
|
||||
}
|
||||
else if (target is Item item)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ItemIdentifier) || item.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
Entity.Spawner.AddToRemoveQueue(item);
|
||||
removedItems.Add(item);
|
||||
if (removedItems.Count >= Amount) { break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
isFinished = true;
|
||||
|
||||
@@ -15,7 +15,8 @@ namespace Barotrauma
|
||||
Outpost,
|
||||
MainPath,
|
||||
Ruin,
|
||||
Wreck
|
||||
Wreck,
|
||||
BeaconStation
|
||||
}
|
||||
|
||||
[Serialize("", true, description: "Species name of the character to spawn.")]
|
||||
@@ -102,31 +103,36 @@ namespace Barotrauma
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (spawned) { return; }
|
||||
|
||||
|
||||
if (!string.IsNullOrEmpty(NPCSetIdentifier) && !string.IsNullOrEmpty(NPCIdentifier))
|
||||
{
|
||||
HumanPrefab humanPrefab = NPCSet.Get(NPCSetIdentifier, NPCIdentifier);
|
||||
ISpatialEntity spawnPos = GetSpawnPos();
|
||||
Entity.Spawner.AddToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawn: newCharacter =>
|
||||
if (humanPrefab != null)
|
||||
{
|
||||
newCharacter.TeamID = CharacterTeamType.FriendlyNPC;
|
||||
newCharacter.EnableDespawn = false;
|
||||
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine);
|
||||
if (LootingIsStealing)
|
||||
ISpatialEntity spawnPos = GetSpawnPos();
|
||||
Entity.Spawner.AddToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter =>
|
||||
{
|
||||
foreach (Item item in newCharacter.Inventory.AllItems)
|
||||
if (newCharacter == null) { return; }
|
||||
newCharacter.Prefab = humanPrefab;
|
||||
newCharacter.TeamID = CharacterTeamType.FriendlyNPC;
|
||||
newCharacter.EnableDespawn = false;
|
||||
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine);
|
||||
if (LootingIsStealing)
|
||||
{
|
||||
item.SpawnedInOutpost = true;
|
||||
item.AllowStealing = false;
|
||||
foreach (Item item in newCharacter.Inventory.AllItems)
|
||||
{
|
||||
item.SpawnedInOutpost = true;
|
||||
item.AllowStealing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
humanPrefab.InitializeCharacter(newCharacter, spawnPos);
|
||||
if (!string.IsNullOrEmpty(TargetTag) && newCharacter != null)
|
||||
{
|
||||
ParentEvent.AddTarget(TargetTag, newCharacter);
|
||||
}
|
||||
spawnedEntity = newCharacter;
|
||||
});
|
||||
humanPrefab.InitializeCharacter(newCharacter, spawnPos);
|
||||
if (!string.IsNullOrEmpty(TargetTag) && newCharacter != null)
|
||||
{
|
||||
ParentEvent.AddTarget(TargetTag, newCharacter);
|
||||
}
|
||||
spawnedEntity = newCharacter;
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(SpeciesName))
|
||||
{
|
||||
@@ -196,8 +202,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
spawned = true;
|
||||
|
||||
spawned = true;
|
||||
}
|
||||
|
||||
public static Vector2 OffsetSpawnPos(Vector2 pos, float offsetAmount)
|
||||
@@ -225,6 +230,7 @@ namespace Barotrauma
|
||||
SpawnLocationType.Outpost => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsOutpost),
|
||||
SpawnLocationType.Wreck => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsWreck),
|
||||
SpawnLocationType.Ruin => Item.ItemList.FindAll(it => it.ParentRuin != null),
|
||||
SpawnLocationType.BeaconStation => Item.ItemList.FindAll(it => it.Submarine != null && it.Submarine.Info.IsBeacon),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
|
||||
@@ -250,15 +256,15 @@ namespace Barotrauma
|
||||
SpawnLocationType.Outpost => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.CurrentHull != null && wp.Submarine.Info.IsOutpost),
|
||||
SpawnLocationType.Wreck => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.Submarine.Info.IsWreck),
|
||||
SpawnLocationType.Ruin => WayPoint.WayPointList.FindAll(wp => wp.ParentRuin != null),
|
||||
SpawnLocationType.BeaconStation => WayPoint.WayPointList.FindAll(wp => wp.Submarine != null && wp.Submarine.Info.IsBeacon),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
|
||||
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();
|
||||
List<WayPoint> spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Any(moduleFlags.Contains) ?? false).ToList();
|
||||
if (spawnPoints.Any())
|
||||
{
|
||||
potentialSpawnPoints = spawnPoints;
|
||||
@@ -267,8 +273,10 @@ namespace Barotrauma
|
||||
|
||||
if (spawnpointTags != null && spawnpointTags.Any())
|
||||
{
|
||||
var spawnPoints = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag)))
|
||||
.Where(wp => wp.ConnectedDoor == null && !wp.isObstructed);
|
||||
var spawnPoints = potentialSpawnPoints
|
||||
.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag)))
|
||||
.Where(wp => wp.ConnectedDoor == null && !wp.isObstructed);
|
||||
|
||||
if (spawnPoints.Any())
|
||||
{
|
||||
potentialSpawnPoints = spawnPoints.ToList();
|
||||
@@ -293,6 +301,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
//don't spawn in an airlock module if there are other options
|
||||
var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Contains("airlock") ?? false);
|
||||
if (airlockSpawnPoints.Count() < validSpawnPoints.Count())
|
||||
{
|
||||
validSpawnPoints = validSpawnPoints.Except(airlockSpawnPoints);
|
||||
|
||||
@@ -51,7 +51,14 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var target in targets)
|
||||
{
|
||||
effect.Apply(effect.type, deltaTime, target, target as ISerializableEntity);
|
||||
if (target is Item targetItem)
|
||||
{
|
||||
effect.Apply(effect.type, deltaTime, target, targetItem.AllPropertyObjects);
|
||||
}
|
||||
else
|
||||
{
|
||||
effect.Apply(effect.type, deltaTime, target, target as ISerializableEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
#if SERVER
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Barotrauma
|
||||
{
|
||||
class TagAction : EventAction
|
||||
{
|
||||
public enum SubType { Any= 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8 }
|
||||
public enum SubType { Any = 0, Player = 1, Outpost = 2, Wreck = 4, BeaconStation = 8 }
|
||||
|
||||
[Serialize("", true)]
|
||||
public string Criteria { get; set; }
|
||||
@@ -67,6 +67,16 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
private void TagHumansByIdentifier(string identifier)
|
||||
{
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c.Prefab?.Identifier.Equals(identifier, StringComparison.OrdinalIgnoreCase) ?? false)
|
||||
{
|
||||
ParentEvent.AddTarget(Tag, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
private void TagStructuresByIdentifier(string identifier)
|
||||
{
|
||||
ParentEvent.AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase));
|
||||
@@ -122,6 +132,9 @@ namespace Barotrauma
|
||||
case "crew":
|
||||
TagCrew();
|
||||
break;
|
||||
case "humanprefabidentifier":
|
||||
if (kvp.Length > 1) { TagHumansByIdentifier(kvp[1].Trim()); }
|
||||
break;
|
||||
case "structureidentifier":
|
||||
if (kvp.Length > 1) { TagStructuresByIdentifier(kvp[1].Trim()); }
|
||||
break;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
@@ -30,6 +31,9 @@ namespace Barotrauma
|
||||
[Serialize(true, true, description: "If true, dead/unconscious characters cannot trigger the action.")]
|
||||
public bool DisableIfTargetIncapacitated { get; set; }
|
||||
|
||||
[Serialize(false, true, description: "If true, one target must interact with the other to trigger the action.")]
|
||||
public bool WaitForInteraction { get; set; }
|
||||
|
||||
private float distance;
|
||||
|
||||
public TriggerAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
|
||||
@@ -44,12 +48,15 @@ namespace Barotrauma
|
||||
}
|
||||
public override void Reset()
|
||||
{
|
||||
ResetTargetIcons();
|
||||
isRunning = false;
|
||||
isFinished = false;
|
||||
}
|
||||
|
||||
public bool isRunning = false;
|
||||
|
||||
private Either<Character, Item> npcOrItem = null;
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (isFinished) { return; }
|
||||
@@ -81,20 +88,104 @@ namespace Barotrauma
|
||||
if (DisableInCombat && IsInCombat(e2)) { continue; }
|
||||
if (DisableIfTargetIncapacitated && e2 is Character character2 && (character2.IsDead || character2.IsIncapacitated)) { continue; }
|
||||
|
||||
Vector2 pos1 = e1.WorldPosition;
|
||||
Vector2 pos2 = e2.WorldPosition;
|
||||
distance = Vector2.Distance(pos1, pos2);
|
||||
if (((e1 is MapEntity m1) && Submarine.RectContains(m1.WorldRect, pos2)) ||
|
||||
((e2 is MapEntity m2) && Submarine.RectContains(m2.WorldRect, pos1)) ||
|
||||
Vector2.DistanceSquared(pos1, pos2) < Radius * Radius)
|
||||
if (WaitForInteraction)
|
||||
{
|
||||
Trigger(e1, e2);
|
||||
return;
|
||||
Character player = null;
|
||||
Character npc = null;
|
||||
Item item = null;
|
||||
npcOrItem?.TryGet(out npc);
|
||||
npcOrItem?.TryGet(out item);
|
||||
if (e1 is Character char1)
|
||||
{
|
||||
if (char1.IsBot) { npc ??= char1; }
|
||||
else { player = char1; }
|
||||
}
|
||||
else
|
||||
{
|
||||
item ??= e1 as Item;
|
||||
}
|
||||
if (e2 is Character char2)
|
||||
{
|
||||
if (char2.IsBot) { npc ??= char2; }
|
||||
else { player = char2; }
|
||||
}
|
||||
else
|
||||
{
|
||||
item ??= e2 as Item;
|
||||
}
|
||||
|
||||
if (player != null)
|
||||
{
|
||||
if (npc != null)
|
||||
{
|
||||
if (npc.CampaignInteractionType != CampaignMode.InteractionType.Examine)
|
||||
{
|
||||
npcOrItem = npc;
|
||||
npc.CampaignInteractionType = CampaignMode.InteractionType.Examine;
|
||||
npc.RequireConsciousnessForCustomInteract = false;
|
||||
#if CLIENT
|
||||
npc.SetCustomInteract(
|
||||
(speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } },
|
||||
TextManager.GetWithVariable("CampaignInteraction.Examine", "[key]", GameMain.Config.KeyBindText(InputType.Use)));
|
||||
#else
|
||||
npc.SetCustomInteract(
|
||||
(speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } },
|
||||
TextManager.Get("CampaignInteraction.Talk"));
|
||||
GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
|
||||
#endif
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
else if (item != null)
|
||||
{
|
||||
npcOrItem = item;
|
||||
item.CampaignInteractionType = CampaignMode.InteractionType.Examine;
|
||||
if (player.SelectedConstruction == item ||
|
||||
player.Inventory.Contains(item) ||
|
||||
(player.FocusedItem == item && player.IsKeyHit(InputType.Use)))
|
||||
{
|
||||
Trigger(e1, e2);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector2 pos1 = e1.WorldPosition;
|
||||
Vector2 pos2 = e2.WorldPosition;
|
||||
distance = Vector2.Distance(pos1, pos2);
|
||||
if (((e1 is MapEntity m1) && Submarine.RectContains(m1.WorldRect, pos2)) ||
|
||||
((e2 is MapEntity m2) && Submarine.RectContains(m2.WorldRect, pos1)) ||
|
||||
Vector2.DistanceSquared(pos1, pos2) < Radius * Radius)
|
||||
{
|
||||
Trigger(e1, e2);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetTargetIcons()
|
||||
{
|
||||
if (npcOrItem == null) { return; }
|
||||
if (npcOrItem.TryGet(out Character npc))
|
||||
{
|
||||
npc.CampaignInteractionType = CampaignMode.InteractionType.None;
|
||||
npc.SetCustomInteract(null, null);
|
||||
npc.RequireConsciousnessForCustomInteract = true;
|
||||
#if SERVER
|
||||
GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
|
||||
#endif
|
||||
}
|
||||
else if (npcOrItem.TryGet(out Item item))
|
||||
{
|
||||
item.CampaignInteractionType = CampaignMode.InteractionType.None;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsCloseEnoughToHull(Entity e, out Hull hull)
|
||||
{
|
||||
hull = null;
|
||||
@@ -157,6 +248,7 @@ namespace Barotrauma
|
||||
|
||||
private void Trigger(Entity entity1, Entity entity2)
|
||||
{
|
||||
ResetTargetIcons();
|
||||
if (!string.IsNullOrEmpty(ApplyToTarget1))
|
||||
{
|
||||
ParentEvent.AddTarget(ApplyToTarget1, entity1);
|
||||
@@ -174,7 +266,14 @@ namespace Barotrauma
|
||||
{
|
||||
if (string.IsNullOrEmpty(TargetModuleType))
|
||||
{
|
||||
return $"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (Distance: {((int)distance).ColorizeObject()}, Radius: {Radius.ColorizeObject()}, TargetTags: {Target1Tag.ColorizeObject()}, {Target2Tag.ColorizeObject()})";
|
||||
return
|
||||
$"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (" +
|
||||
(WaitForInteraction ?
|
||||
$"Selected non-player target: {(npcOrItem?.ToString() ?? "<null>").ColorizeObject()}, " :
|
||||
$"Distance: {((int)distance).ColorizeObject()}, ") +
|
||||
$"Radius: {Radius.ColorizeObject()}, " +
|
||||
$"TargetTags: {Target1Tag.ColorizeObject()}, " +
|
||||
$"{Target2Tag.ColorizeObject()})";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Barotrauma
|
||||
private float calculateDistanceTraveledTimer;
|
||||
private float distanceTraveled;
|
||||
|
||||
private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger;
|
||||
private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger, monsterTotalStrength;
|
||||
|
||||
private float roundDuration;
|
||||
|
||||
@@ -107,7 +107,7 @@ namespace Barotrauma
|
||||
totalPathLength = 0.0f;
|
||||
if (level != null)
|
||||
{
|
||||
var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(Level.Loaded.StartPosition), ConvertUnits.ToSimUnits(Level.Loaded.EndPosition));
|
||||
var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(level.StartPosition), ConvertUnits.ToSimUnits(level.EndPosition));
|
||||
totalPathLength = steeringPath.TotalLength;
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ namespace Barotrauma
|
||||
}
|
||||
MTRandom rand = new MTRandom(seed);
|
||||
|
||||
var initialEventSet = SelectRandomEvents(EventSet.List);
|
||||
var initialEventSet = SelectRandomEvents(EventSet.List, rand);
|
||||
if (initialEventSet != null)
|
||||
{
|
||||
pendingEventSets.Add(initialEventSet);
|
||||
@@ -168,7 +168,7 @@ namespace Barotrauma
|
||||
if (eventSet == null) { return; }
|
||||
if (eventSet.OncePerOutpost)
|
||||
{
|
||||
foreach (EventPrefab ep in eventSet.EventPrefabs.Select(e => e.First))
|
||||
foreach (EventPrefab ep in eventSet.EventPrefabs.Select(e => e.prefab))
|
||||
{
|
||||
if (!level.LevelData.NonRepeatableEvents.Contains(ep))
|
||||
{
|
||||
@@ -374,11 +374,11 @@ namespace Barotrauma
|
||||
preloadedSprites.Clear();
|
||||
}
|
||||
|
||||
private float CalculateCommonness(Pair<EventPrefab, float> eventPrefab)
|
||||
private float CalculateCommonness(EventPrefab eventPrefab, float baseCommonness)
|
||||
{
|
||||
if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab.First)) { return 0.0f; }
|
||||
float retVal = eventPrefab.Second;
|
||||
if (level.LevelData.EventHistory.Contains(eventPrefab.First)) { retVal *= 0.1f; }
|
||||
if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab)) { return 0.0f; }
|
||||
float retVal = baseCommonness;
|
||||
if (level.LevelData.EventHistory.Contains(eventPrefab)) { retVal *= 0.1f; }
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@@ -386,21 +386,25 @@ namespace Barotrauma
|
||||
{
|
||||
if (level == null) { return; }
|
||||
if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; }
|
||||
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"Loading event set {eventSet.DebugIdentifier}", Color.LightBlue);
|
||||
#else
|
||||
DebugConsole.Log($"Loading event set {eventSet.DebugIdentifier}");
|
||||
#endif
|
||||
int applyCount = 1;
|
||||
List<Func<Level.InterestingPosition, bool>> spawnPosFilter = new List<Func<Level.InterestingPosition, bool>>();
|
||||
if (eventSet.PerRuin)
|
||||
{
|
||||
applyCount = Level.Loaded.Ruins.Count();
|
||||
foreach (var ruin in Level.Loaded.Ruins)
|
||||
applyCount = level.Ruins.Count();
|
||||
foreach (var ruin in level.Ruins)
|
||||
{
|
||||
spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Ruin == ruin; });
|
||||
}
|
||||
}
|
||||
else if (eventSet.PerCave)
|
||||
{
|
||||
applyCount = Level.Loaded.Caves.Count();
|
||||
foreach (var cave in Level.Loaded.Caves)
|
||||
applyCount = level.Caves.Count();
|
||||
foreach (var cave in level.Caves)
|
||||
{
|
||||
spawnPosFilter.Add((Level.InterestingPosition pos) => { return pos.Cave == cave; });
|
||||
}
|
||||
@@ -416,49 +420,62 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
var suitablePrefabs = eventSet.EventPrefabs.FindAll(e =>
|
||||
string.IsNullOrEmpty(e.First.BiomeIdentifier) ||
|
||||
e.First.BiomeIdentifier.Equals(Level.Loaded.LevelData?.Biome?.Identifier, StringComparison.OrdinalIgnoreCase));
|
||||
string.IsNullOrEmpty(e.prefab.BiomeIdentifier) ||
|
||||
e.prefab.BiomeIdentifier.Equals(level.LevelData?.Biome?.Identifier, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
for (int i = 0; i < applyCount; i++)
|
||||
{
|
||||
if (eventSet.ChooseRandom)
|
||||
{
|
||||
if (suitablePrefabs.Count > 0)
|
||||
{
|
||||
List<Pair<EventPrefab, float>> unusedEvents = new List<Pair<EventPrefab, float>>(suitablePrefabs);
|
||||
var unusedEvents = new List<(EventPrefab prefab, float commonness, float probability)>(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)
|
||||
if (unusedEvents.All(e => CalculateCommonness(e.prefab, e.commonness) <= 0.0f)) { break; }
|
||||
(EventPrefab eventPrefab, float commonness, float probability) = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => CalculateCommonness(e.prefab, e.commonness)).ToList(), rand);
|
||||
if (eventPrefab != null && rand.NextDouble() <= probability)
|
||||
{
|
||||
var newEvent = eventPrefab.First.CreateInstance();
|
||||
var newEvent = eventPrefab.CreateInstance();
|
||||
if (newEvent == null) { continue; }
|
||||
newEvent.Init(true);
|
||||
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
|
||||
DebugConsole.Log("Initialized event " + newEvent.ToString());
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"Initialized event {newEvent}");
|
||||
#else
|
||||
DebugConsole.Log($"Initialized event {newEvent}");
|
||||
#endif
|
||||
if (!selectedEvents.ContainsKey(eventSet))
|
||||
{
|
||||
selectedEvents.Add(eventSet, new List<Event>());
|
||||
}
|
||||
selectedEvents[eventSet].Add(newEvent);
|
||||
unusedEvents.Remove(eventPrefab);
|
||||
unusedEvents.Remove((eventPrefab, commonness, probability));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (eventSet.ChildSets.Count > 0)
|
||||
{
|
||||
var newEventSet = SelectRandomEvents(eventSet.ChildSets);
|
||||
if (newEventSet != null) { CreateEvents(newEventSet, rand); }
|
||||
var newEventSet = SelectRandomEvents(eventSet.ChildSets, rand);
|
||||
if (newEventSet != null)
|
||||
{
|
||||
CreateEvents(newEventSet, rand);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (Pair<EventPrefab, float> eventPrefab in suitablePrefabs)
|
||||
foreach ((EventPrefab eventPrefab, float commonness, float probability) in suitablePrefabs)
|
||||
{
|
||||
var newEvent = eventPrefab.First.CreateInstance();
|
||||
if (rand.NextDouble() > probability) { continue; }
|
||||
var newEvent = eventPrefab.CreateInstance();
|
||||
if (newEvent == null) { continue; }
|
||||
newEvent.Init(true);
|
||||
DebugConsole.Log("Initialized event " + newEvent.ToString());
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"Initialized event {newEvent}");
|
||||
#else
|
||||
DebugConsole.Log($"Initialized event {newEvent}");
|
||||
#endif
|
||||
if (!selectedEvents.ContainsKey(eventSet))
|
||||
{
|
||||
selectedEvents.Add(eventSet, new List<Event>());
|
||||
@@ -474,10 +491,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private EventSet SelectRandomEvents(List<EventSet> eventSets)
|
||||
private EventSet SelectRandomEvents(List<EventSet> eventSets, Random random = null)
|
||||
{
|
||||
if (level == null) { return null; }
|
||||
MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed));
|
||||
Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed));
|
||||
|
||||
var allowedEventSets =
|
||||
eventSets.Where(es =>
|
||||
@@ -496,7 +513,8 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level));
|
||||
float randomNumber = (float)rand.NextDouble() * totalCommonness;
|
||||
float randomNumber = (float)rand.NextDouble();
|
||||
randomNumber *= totalCommonness;
|
||||
foreach (EventSet eventSet in allowedEventSets)
|
||||
{
|
||||
float commonness = eventSet.GetCommonness(level);
|
||||
@@ -694,47 +712,103 @@ namespace Barotrauma
|
||||
// enemy amount --------------------------------------------------------
|
||||
|
||||
enemyDanger = 0.0f;
|
||||
monsterTotalStrength = 0;
|
||||
foreach (Character character in Character.CharacterList)
|
||||
{
|
||||
if (character.IsDead || character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup("human")) { continue; }
|
||||
if (character.IsIncapacitated || !character.Enabled || character.IsPet || character.Params.CompareGroup("human")) { continue; }
|
||||
|
||||
if (!(character.AIController is EnemyAIController enemyAI)) { continue; }
|
||||
|
||||
if (!enemyAI.AIParams.StayInAbyss)
|
||||
{
|
||||
// Ignore abyss monsters because they can stay active for quite great distances. They'll be taken into account when they target the sub.
|
||||
monsterTotalStrength += enemyAI.CombatStrength;
|
||||
}
|
||||
|
||||
// Example combat strengths:
|
||||
// Hammerheadspawn 1
|
||||
// Moloch Pupa 1
|
||||
// Terminal cell 20
|
||||
// Leucocyte 40
|
||||
// Husk 90
|
||||
// Crawler 100
|
||||
// Unarmored Mudraptor 140
|
||||
// Spineling 150
|
||||
// Tigerthresher 200
|
||||
// Armored Mudraptor 210
|
||||
// Watcher 400
|
||||
// Golden Hammerhead 400
|
||||
// Hammerhead 500
|
||||
// Hammerhead Matriarch 550
|
||||
// Bonethresher 600
|
||||
// Moloch 1250
|
||||
// Black Moloch 1500
|
||||
// Endworm 10000
|
||||
if (character.CurrentHull?.Submarine != null &&
|
||||
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)))
|
||||
{
|
||||
//crawler inside the sub adds 0.1f to enemy danger, mantis 0.25f
|
||||
enemyDanger += enemyAI.CombatStrength / 100.0f;
|
||||
// Enemy onboard -> Crawler inside the sub adds 0.2 to enemy danger, Mudraptor 0.42
|
||||
enemyDanger += enemyAI.CombatStrength / 500.0f;
|
||||
}
|
||||
else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null)
|
||||
{
|
||||
//enemy outside and targeting the sub or something in it
|
||||
//moloch adds 0.24 to enemy danger, a crawler 0.02
|
||||
enemyDanger += enemyAI.CombatStrength / 1000.0f;
|
||||
// Enemy outside targeting the sub or something in it
|
||||
// -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25.
|
||||
enemyDanger += enemyAI.CombatStrength / 5000.0f;
|
||||
}
|
||||
}
|
||||
// Add a portion of the total strength of active monsters to the enemy danger so that we don't spawn too many monsters around the sub.
|
||||
// On top of the existing value, so if 10 crawlers are targeting the sub simultaneously from outside, the final value would be: 0.02 x 10 + 0.2 = 0.4.
|
||||
// And if they get inside, we add 0.1 per crawler on that.
|
||||
// So, in practice the danger per enemy that is attacking the sub is half of what it would be when the enemy is not targeting the sub.
|
||||
// 10 Crawlers -> +0.2 (0.4 in total if all target the sub from outside).
|
||||
// 5 Mudraptors -> +0.21 (0.42 in total, before they get inside).
|
||||
// 3 Hammerheads -> +0.3 (0.6 in total, if they all target the sub).
|
||||
// 2 Molochs -> +0.5 (1.0 in total, if both target the sub).
|
||||
enemyDanger += monsterTotalStrength / 5000f;
|
||||
enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f);
|
||||
|
||||
// The definitions above aim for that we never spawn more monsters that the player (and the performance) can handle.
|
||||
// Some examples that result in the max intensity even when the creatures would just idle around.
|
||||
// The values are theoretical, because in practice many of the monsters are targeting the sub, which will double the danger of those monster and effectively halve the max monster count.
|
||||
// In practice we don't use the max intensity. For example on level 50 we use max intensity 50, which would mean that we'd halve the numbers below.
|
||||
// There's no hard cap for the monster count, but if the amount of monsters is higher than this, we don't spawn more monsters from the events:
|
||||
// 50 Crawlers (We shouldn't actually ever spawn that many. 12 is the max per event, but theoretically 25 crawlers would result in max intensity).
|
||||
// 25 Tigerthreshers (Max 9 per event. 12 targeting the sub at the same time results in max intensity).
|
||||
// 10 Hammerheads (Max 3 per event. 5 targeting the sub at the same time results in max intensity).
|
||||
// 4 Molochs (Max 2 per event and 2 targeting the sub at the same time results in max intensity).
|
||||
|
||||
// hull status (gaps, flooding, fire) --------------------------------------------------------
|
||||
|
||||
float holeCount = 0.0f;
|
||||
float waterAmount = 0.0f;
|
||||
float totalHullVolume = 0.0f;
|
||||
float dryHullVolume = 0.0f;
|
||||
foreach (Hull hull in Hull.hullList)
|
||||
{
|
||||
if (hull.Submarine == null || hull.Submarine.Info.Type != SubmarineType.Player) { continue; }
|
||||
if (hull.RoomName != null && hull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
if (hull.Submarine == null || hull.Submarine.Info.Type != SubmarineType.Player) { continue; }
|
||||
if (GameMain.GameSession?.GameMode is PvPMode)
|
||||
{
|
||||
if (hull.Submarine.TeamID != CharacterTeamType.Team1 && hull.Submarine.TeamID != CharacterTeamType.Team2) { continue; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hull.Submarine.TeamID != CharacterTeamType.Team1) { continue; }
|
||||
}
|
||||
fireAmount += hull.FireSources.Sum(fs => fs.Size.X);
|
||||
if (hull.IsWetRoom) { continue; }
|
||||
foreach (Gap gap in hull.ConnectedGaps)
|
||||
{
|
||||
if (!gap.IsRoomToRoom) holeCount += gap.Open;
|
||||
if (!gap.IsRoomToRoom)
|
||||
{
|
||||
holeCount += gap.Open;
|
||||
}
|
||||
}
|
||||
waterAmount += hull.WaterVolume;
|
||||
totalHullVolume += hull.Volume;
|
||||
fireAmount += hull.FireSources.Sum(fs => fs.Size.X);
|
||||
dryHullVolume += hull.Volume;
|
||||
}
|
||||
if (totalHullVolume > 0)
|
||||
if (dryHullVolume > 0)
|
||||
{
|
||||
floodingAmount = waterAmount / totalHullVolume;
|
||||
floodingAmount = waterAmount / dryHullVolume;
|
||||
}
|
||||
|
||||
//hull integrity at 0.0 if there are 10 or more wide-open holes
|
||||
@@ -779,7 +853,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (level == null) { return 0.0f; }
|
||||
var refEntity = GetRefEntity();
|
||||
Vector2 target = ConvertUnits.ToSimUnits(Level.Loaded.EndPosition);
|
||||
Vector2 target = ConvertUnits.ToSimUnits(level.EndPosition);
|
||||
var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(refEntity.WorldPosition), target);
|
||||
if (steeringPath.Unreachable || float.IsPositiveInfinity(totalPathLength))
|
||||
{
|
||||
@@ -897,15 +971,15 @@ namespace Barotrauma
|
||||
|
||||
const int maxDist = 1000;
|
||||
|
||||
if (Level.Loaded != null)
|
||||
if (level != null)
|
||||
{
|
||||
foreach (var ruin in Level.Loaded.Ruins)
|
||||
foreach (var ruin in level.Ruins)
|
||||
{
|
||||
Rectangle area = ruin.Area;
|
||||
area.Inflate(maxDist, maxDist);
|
||||
if (area.Contains(character.WorldPosition)) { return true; }
|
||||
}
|
||||
foreach (var cave in Level.Loaded.Caves)
|
||||
foreach (var cave in level.Caves)
|
||||
{
|
||||
Rectangle area = cave.Area;
|
||||
area.Inflate(maxDist, maxDist);
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Barotrauma
|
||||
{
|
||||
public readonly XElement ConfigElement;
|
||||
public readonly Type EventType;
|
||||
public readonly float SpawnProbability;
|
||||
public readonly float Probability;
|
||||
public readonly bool TriggerEventCooldown;
|
||||
public float Commonness;
|
||||
public string Identifier;
|
||||
@@ -39,7 +39,7 @@ 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);
|
||||
Probability = Math.Clamp(element.GetAttributeFloat(1.0f, "probability", "spawnprobability"), 0, 1);
|
||||
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
|
||||
|
||||
UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false);
|
||||
|
||||
@@ -48,10 +48,10 @@ namespace Barotrauma
|
||||
List<EventPrefab> eventPrefabs = new List<EventPrefab>(PrefabList);
|
||||
foreach (var eventSet in List)
|
||||
{
|
||||
eventPrefabs.AddRange(eventSet.EventPrefabs.Select(ep => ep.First));
|
||||
eventPrefabs.AddRange(eventSet.EventPrefabs.Select(ep => ep.prefab));
|
||||
foreach (var childSet in eventSet.ChildSets)
|
||||
{
|
||||
eventPrefabs.AddRange(childSet.EventPrefabs.Select(ep => ep.First));
|
||||
eventPrefabs.AddRange(childSet.EventPrefabs.Select(ep => ep.prefab));
|
||||
}
|
||||
}
|
||||
return eventPrefabs;
|
||||
@@ -96,8 +96,7 @@ namespace Barotrauma
|
||||
|
||||
public readonly Dictionary<string, float> Commonness;
|
||||
|
||||
//Pair.First: event prefab, Pair.Second: commonness
|
||||
public readonly List<Pair<EventPrefab, float>> EventPrefabs;
|
||||
public readonly List<(EventPrefab prefab, float commonness, float probability)> EventPrefabs;
|
||||
|
||||
public readonly List<EventSet> ChildSets;
|
||||
|
||||
@@ -111,7 +110,7 @@ namespace Barotrauma
|
||||
{
|
||||
DebugIdentifier = element.GetAttributeString("identifier", null) ?? debugIdentifier;
|
||||
Commonness = new Dictionary<string, float>();
|
||||
EventPrefabs = new List<Pair<EventPrefab, float>>();
|
||||
EventPrefabs = new List<(EventPrefab prefab, float commonness, float probability)>();
|
||||
ChildSets = new List<EventSet>();
|
||||
|
||||
BiomeIdentifier = element.GetAttributeString("biome", string.Empty);
|
||||
@@ -149,7 +148,7 @@ namespace Barotrauma
|
||||
OncePerOutpost = element.GetAttributeBool("onceperoutpost", false);
|
||||
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
|
||||
|
||||
Commonness[""] = 1.0f;
|
||||
Commonness[""] = element.GetAttributeFloat("commonness", 1.0f);
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
@@ -184,13 +183,14 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
float commonness = subElement.GetAttributeFloat("commonness", prefab.Commonness);
|
||||
EventPrefabs.Add(new Pair<EventPrefab, float>( prefab, commonness));
|
||||
float probability = subElement.GetAttributeFloat("probability", prefab.Probability);
|
||||
EventPrefabs.Add((prefab, commonness, probability));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var prefab = new EventPrefab(subElement);
|
||||
EventPrefabs.Add(new Pair<EventPrefab, float>(prefab, prefab.Commonness));
|
||||
EventPrefabs.Add((prefab, prefab.Commonness, prefab.Probability));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -342,13 +342,13 @@ namespace Barotrauma
|
||||
{
|
||||
if (thisSet.ChooseRandom)
|
||||
{
|
||||
List<Pair<EventPrefab, float>> unusedEvents = new List<Pair<EventPrefab, float>>(thisSet.EventPrefabs);
|
||||
var unusedEvents = new List<(EventPrefab prefab, float commonness, float probability)>(thisSet.EventPrefabs);
|
||||
for (int i = 0; i < thisSet.EventCount; i++)
|
||||
{
|
||||
var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.Second).ToList(), Rand.RandSync.Unsynced);
|
||||
if (eventPrefab != null)
|
||||
var eventPrefab = ToolBox.SelectWeightedRandom(unusedEvents, unusedEvents.Select(e => e.commonness).ToList(), Rand.RandSync.Unsynced);
|
||||
if (eventPrefab.prefab != null)
|
||||
{
|
||||
AddEvent(stats, eventPrefab.First);
|
||||
AddEvent(stats, eventPrefab.prefab);
|
||||
unusedEvents.Remove(eventPrefab);
|
||||
}
|
||||
}
|
||||
@@ -357,7 +357,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var eventPrefab in thisSet.EventPrefabs)
|
||||
{
|
||||
AddEvent(stats, eventPrefab.First);
|
||||
AddEvent(stats, eventPrefab.prefab);
|
||||
}
|
||||
}
|
||||
foreach (var childSet in thisSet.ChildSets)
|
||||
|
||||
+151
-60
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -15,6 +16,10 @@ namespace Barotrauma
|
||||
protected readonly HashSet<Character> requireKill = new HashSet<Character>();
|
||||
protected readonly HashSet<Character> requireRescue = new HashSet<Character>();
|
||||
|
||||
private readonly string itemTag;
|
||||
private readonly XElement itemConfig;
|
||||
private readonly List<Item> items = new List<Item>();
|
||||
|
||||
protected const int HostagesKilledState = 5;
|
||||
|
||||
private readonly string hostagesKilledMessage;
|
||||
@@ -33,15 +38,55 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected bool wasDocked;
|
||||
|
||||
public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations) :
|
||||
base(prefab, locations)
|
||||
public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations, Submarine sub) :
|
||||
base(prefab, locations, sub)
|
||||
{
|
||||
characterConfig = prefab.ConfigElement.Element("Characters");
|
||||
|
||||
string msgTag = prefab.ConfigElement.GetAttributeString("hostageskilledmessage", "");
|
||||
hostagesKilledMessage = TextManager.Get(msgTag, returnNull: true) ?? msgTag;
|
||||
|
||||
itemConfig = prefab.ConfigElement.Element("Items");
|
||||
itemTag = prefab.ConfigElement.GetAttributeString("targetitem", "");
|
||||
}
|
||||
|
||||
protected override void StartMissionSpecific(Level level)
|
||||
@@ -52,8 +97,13 @@ namespace Barotrauma
|
||||
characterItems.Clear();
|
||||
requireKill.Clear();
|
||||
requireRescue.Clear();
|
||||
items.Clear();
|
||||
#if SERVER
|
||||
spawnedItems.Clear();
|
||||
#endif
|
||||
|
||||
var submarine = Submarine.Loaded.Find(s => s.Info.Type == SubmarineType.Outpost) ?? Submarine.MainSub;
|
||||
InitItems(submarine);
|
||||
if (!IsClient)
|
||||
{
|
||||
InitCharacters(submarine);
|
||||
@@ -62,56 +112,101 @@ namespace Barotrauma
|
||||
wasDocked = Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost);
|
||||
}
|
||||
|
||||
private void InitItems(Submarine submarine)
|
||||
{
|
||||
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)
|
||||
{
|
||||
spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom();
|
||||
}
|
||||
Vector2 spawnPos = spawnPoint.WorldPosition;
|
||||
if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100)
|
||||
{
|
||||
spawnPos = new Vector2(
|
||||
MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 200), wp.CurrentHull.WorldRect.X + 50, wp.CurrentHull.WorldRect.Right - 50),
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if (characterConfig != null)
|
||||
{
|
||||
foreach (XElement element in characterConfig.Elements())
|
||||
{
|
||||
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 (GameMain.NetworkMember == null && element.GetAttributeBool("multiplayeronly", false)) { continue; }
|
||||
|
||||
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)
|
||||
int defaultCount = element.GetAttributeInt("count", -1);
|
||||
if (defaultCount < 0)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + characterIdentifier + "\" not found");
|
||||
continue;
|
||||
defaultCount = element.GetAttributeInt("amount", 1);
|
||||
}
|
||||
for (int i = 0; i < count; i++)
|
||||
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)
|
||||
{
|
||||
LoadHuman(humanPrefab, element, submarine);
|
||||
HumanPrefab humanPrefab = GetHumanPrefabFromElement(element);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
@@ -128,32 +223,27 @@ namespace Barotrauma
|
||||
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);
|
||||
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
|
||||
|
||||
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos, giveTags: true);
|
||||
|
||||
if (spawnPos is WayPoint wp)
|
||||
{
|
||||
spawnedCharacter.GiveIdCardTags(wp);
|
||||
}
|
||||
|
||||
if (requiresRescue)
|
||||
{
|
||||
requireRescue.Add(spawnedCharacter);
|
||||
#if CLIENT
|
||||
GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
|
||||
#endif
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -187,7 +277,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
protected override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
if (State != HostagesKilledState)
|
||||
{
|
||||
@@ -215,7 +305,8 @@ namespace Barotrauma
|
||||
{
|
||||
case 0:
|
||||
|
||||
if (requireKill.All(c => c.Removed || c.IsDead) &&
|
||||
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;
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Barotrauma
|
||||
private Point monsterCountRange;
|
||||
private readonly string sonarLabel;
|
||||
|
||||
public BeaconMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations)
|
||||
public BeaconMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
|
||||
{
|
||||
swarmSpawned = false;
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
protected override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
if (IsClient) { return; }
|
||||
if (!swarmSpawned && level.CheckBeaconActive())
|
||||
|
||||
@@ -15,17 +15,112 @@ namespace Barotrauma
|
||||
private readonly Dictionary<Item, UInt16> parentInventoryIDs = new Dictionary<Item, UInt16>();
|
||||
private readonly Dictionary<Item, byte> parentItemContainerIndices = new Dictionary<Item, byte>();
|
||||
|
||||
private int requiredDeliveryAmount;
|
||||
private float requiredDeliveryAmount;
|
||||
|
||||
public CargoMission(MissionPrefab prefab, Location[] locations)
|
||||
: base(prefab, locations)
|
||||
private readonly List<(XElement element, ItemContainer container)> itemsToSpawn = new List<(XElement element, ItemContainer container)>();
|
||||
private int? rewardPerCrate;
|
||||
private int calculatedReward;
|
||||
private int maxItemCount;
|
||||
|
||||
private Submarine sub;
|
||||
|
||||
public override string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Submarine.MainSub != sub)
|
||||
{
|
||||
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(Submarine.MainSub))}‖end‖";
|
||||
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
|
||||
}
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
public CargoMission(MissionPrefab prefab, Location[] locations, Submarine sub)
|
||||
: base(prefab, locations, sub)
|
||||
{
|
||||
this.sub = sub;
|
||||
itemConfig = prefab.ConfigElement.Element("Items");
|
||||
requiredDeliveryAmount = prefab.ConfigElement.GetAttributeInt("requireddeliveryamount", 0);
|
||||
requiredDeliveryAmount = Math.Min(prefab.ConfigElement.GetAttributeFloat("requireddeliveryamount", 0.98f), 1.0f);
|
||||
DetermineCargo();
|
||||
}
|
||||
|
||||
private void DetermineCargo()
|
||||
{
|
||||
if (this.sub == null || itemConfig == null)
|
||||
{
|
||||
calculatedReward = Prefab.Reward;
|
||||
return;
|
||||
}
|
||||
|
||||
itemsToSpawn.Clear();
|
||||
List<(ItemContainer container, int freeSlots)> containers = sub.GetCargoContainers();
|
||||
containers.Sort((c1, c2) => { return c2.container.Capacity.CompareTo(c1.container.Capacity); });
|
||||
|
||||
maxItemCount = 0;
|
||||
foreach (XElement subElement in itemConfig.Elements())
|
||||
{
|
||||
int maxCount = subElement.GetAttributeInt("maxcount", 10);
|
||||
maxItemCount += maxCount;
|
||||
}
|
||||
|
||||
for (int i = 0; i < containers.Count; i++)
|
||||
{
|
||||
foreach (XElement subElement in itemConfig.Elements())
|
||||
{
|
||||
int maxCount = subElement.GetAttributeInt("maxcount", 10);
|
||||
if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; }
|
||||
ItemPrefab itemPrefab = FindItemPrefab(subElement);
|
||||
while (containers[i].freeSlots > 0 && containers[i].container.Inventory.CanBePut(itemPrefab))
|
||||
{
|
||||
containers[i] = (containers[i].container, containers[i].freeSlots - 1);
|
||||
itemsToSpawn.Add((subElement, containers[i].container));
|
||||
if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!itemsToSpawn.Any())
|
||||
{
|
||||
itemsToSpawn.Add((itemConfig.Elements().First(), null));
|
||||
}
|
||||
|
||||
calculatedReward = 0;
|
||||
foreach (var itemToSpawn in itemsToSpawn)
|
||||
{
|
||||
int price = itemToSpawn.element.GetAttributeInt("reward", Prefab.Reward / itemsToSpawn.Count);
|
||||
if (rewardPerCrate.HasValue)
|
||||
{
|
||||
if (price != rewardPerCrate.Value) { rewardPerCrate = -1; }
|
||||
}
|
||||
else
|
||||
{
|
||||
rewardPerCrate = price;
|
||||
}
|
||||
calculatedReward += price;
|
||||
}
|
||||
if (rewardPerCrate.HasValue && rewardPerCrate < 0) { rewardPerCrate = null; }
|
||||
|
||||
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖";
|
||||
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
|
||||
}
|
||||
|
||||
public override int GetReward(Submarine sub)
|
||||
{
|
||||
if (sub != this.sub)
|
||||
{
|
||||
this.sub = sub;
|
||||
DetermineCargo();
|
||||
}
|
||||
return calculatedReward;
|
||||
}
|
||||
|
||||
private void InitItems()
|
||||
{
|
||||
this.sub = Submarine.MainSub;
|
||||
DetermineCargo();
|
||||
|
||||
items.Clear();
|
||||
parentInventoryIDs.Clear();
|
||||
parentItemContainerIndices.Clear();
|
||||
@@ -36,20 +131,15 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (XElement subElement in itemConfig.Elements())
|
||||
foreach (var (element, container) in itemsToSpawn)
|
||||
{
|
||||
LoadItemAsChild(subElement, null);
|
||||
LoadItemAsChild(element, container?.Item);
|
||||
}
|
||||
|
||||
if (requiredDeliveryAmount == 0) { requiredDeliveryAmount = items.Count; }
|
||||
if (requiredDeliveryAmount > items.Count)
|
||||
{
|
||||
DebugConsole.AddWarning($"Error in mission \"{Prefab.Identifier}\". Required delivery amount is {requiredDeliveryAmount} but there's only {items.Count} items to deliver.");
|
||||
requiredDeliveryAmount = items.Count;
|
||||
}
|
||||
if (requiredDeliveryAmount <= 0.0f) { requiredDeliveryAmount = 1.0f; }
|
||||
}
|
||||
|
||||
private void LoadItemAsChild(XElement element, Item parent)
|
||||
private ItemPrefab FindItemPrefab(XElement element)
|
||||
{
|
||||
ItemPrefab itemPrefab;
|
||||
if (element.Attribute("name") != null)
|
||||
@@ -60,7 +150,6 @@ namespace Barotrauma
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemName + "\" not found");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -70,15 +159,15 @@ namespace Barotrauma
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemIdentifier + "\" not found");
|
||||
return;
|
||||
}
|
||||
}
|
||||
return itemPrefab;
|
||||
}
|
||||
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + element.Name.ToString() + "\" not found");
|
||||
return;
|
||||
}
|
||||
|
||||
private void LoadItemAsChild(XElement element, Item parent)
|
||||
{
|
||||
ItemPrefab itemPrefab = FindItemPrefab(element);
|
||||
|
||||
WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, useSyncedRand: true);
|
||||
if (cargoSpawnPos == null)
|
||||
@@ -88,7 +177,6 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
var cargoRoom = cargoSpawnPos.CurrentHull;
|
||||
|
||||
if (cargoRoom == null)
|
||||
{
|
||||
DebugConsole.ThrowError("A waypoint marked as Cargo must be placed inside a room!");
|
||||
@@ -140,7 +228,7 @@ namespace Barotrauma
|
||||
if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
|
||||
{
|
||||
int deliveredItemCount = items.Count(i => i.CurrentHull != null && !i.Removed && i.Condition > 0.0f);
|
||||
if (deliveredItemCount >= requiredDeliveryAmount)
|
||||
if (deliveredItemCount / (float)items.Count >= requiredDeliveryAmount)
|
||||
{
|
||||
GiveReward();
|
||||
completed = true;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -7,6 +6,7 @@ namespace Barotrauma
|
||||
partial class CombatMission : Mission
|
||||
{
|
||||
private Submarine[] subs;
|
||||
// TODO: not used
|
||||
private List<Character>[] crews;
|
||||
|
||||
private readonly string[] descriptions;
|
||||
@@ -45,8 +45,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public CombatMission(MissionPrefab prefab, Location[] locations)
|
||||
: base(prefab, locations)
|
||||
public CombatMission(MissionPrefab prefab, Location[] locations, Submarine sub)
|
||||
: base(prefab, locations, sub)
|
||||
{
|
||||
descriptions = new string[]
|
||||
{
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class EscortMission : Mission
|
||||
{
|
||||
private readonly XElement characterConfig;
|
||||
private readonly XElement itemConfig;
|
||||
|
||||
private readonly List<Character> characters = new List<Character>();
|
||||
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
|
||||
|
||||
private readonly int baseEscortedCharacters;
|
||||
private readonly float scalingEscortedCharacters;
|
||||
private readonly float terroristChance;
|
||||
|
||||
private int calculatedReward;
|
||||
private Submarine missionSub;
|
||||
|
||||
private Character vipCharacter;
|
||||
|
||||
private readonly List<Character> terroristCharacters = new List<Character>();
|
||||
private bool terroristsShouldAct = false;
|
||||
private float terroristDistanceSquared;
|
||||
private const string TerroristTeamChangeIdentifier = "terrorist";
|
||||
|
||||
public EscortMission(MissionPrefab prefab, Location[] locations, Submarine sub)
|
||||
: base(prefab, locations, sub)
|
||||
{
|
||||
missionSub = sub;
|
||||
characterConfig = prefab.ConfigElement.Element("Characters");
|
||||
baseEscortedCharacters = prefab.ConfigElement.GetAttributeInt("baseescortedcharacters", 1);
|
||||
scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0);
|
||||
terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0);
|
||||
itemConfig = prefab.ConfigElement.Element("TerroristItems");
|
||||
CalculateReward();
|
||||
}
|
||||
|
||||
private void CalculateReward()
|
||||
{
|
||||
if (missionSub == null)
|
||||
{
|
||||
calculatedReward = Prefab.Reward;
|
||||
return;
|
||||
}
|
||||
|
||||
int multiplier = CalculateScalingEscortedCharacterCount();
|
||||
calculatedReward = Prefab.Reward * multiplier;
|
||||
|
||||
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(missionSub))}‖end‖";
|
||||
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
|
||||
}
|
||||
|
||||
public override int GetReward(Submarine sub)
|
||||
{
|
||||
if (sub != missionSub)
|
||||
{
|
||||
missionSub = sub;
|
||||
CalculateReward();
|
||||
}
|
||||
return calculatedReward;
|
||||
}
|
||||
|
||||
int CalculateScalingEscortedCharacterCount(bool inMission = false)
|
||||
{
|
||||
if (missionSub == null || missionSub.Info == null) // UI logic failing to get the correct value is not important, but the mission logic must succeed
|
||||
{
|
||||
if (inMission)
|
||||
{
|
||||
DebugConsole.ThrowError("MainSub was null when trying to retrieve submarine size for determining escorted character count!");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return (int)Math.Round(baseEscortedCharacters + scalingEscortedCharacters * (missionSub.Info.RecommendedCrewSizeMin + missionSub.Info.RecommendedCrewSizeMax) / 2);
|
||||
}
|
||||
|
||||
private void InitEscort()
|
||||
{
|
||||
characters.Clear();
|
||||
characterItems.Clear();
|
||||
|
||||
WayPoint explicitStayInHullPos = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub);
|
||||
Rand.RandSync randSync = Rand.RandSync.Server;
|
||||
|
||||
if (terroristChance > 0f)
|
||||
{
|
||||
// in terrorist missions, reroll characters each retry to avoid confusion as to who the terrorists are
|
||||
randSync = Rand.RandSync.Unsynced;
|
||||
}
|
||||
|
||||
//if any of the escortees have a job defined, try to use a spawnpoint designated for that job
|
||||
foreach (XElement element in characterConfig.Elements())
|
||||
{
|
||||
var humanPrefab = GetHumanPrefabFromElement(element);
|
||||
if (humanPrefab == null || string.IsNullOrEmpty(humanPrefab.Job) || humanPrefab.Job.Equals("any", StringComparison.OrdinalIgnoreCase)) { continue; }
|
||||
|
||||
var jobPrefab = humanPrefab.GetJobPrefab();
|
||||
if (jobPrefab != null)
|
||||
{
|
||||
var jobSpecificSpawnPos = WayPoint.GetRandom(SpawnType.Human, jobPrefab, Submarine.MainSub);
|
||||
if (jobSpecificSpawnPos != null)
|
||||
{
|
||||
explicitStayInHullPos = jobSpecificSpawnPos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (XElement element in characterConfig.Elements())
|
||||
{
|
||||
int count = CalculateScalingEscortedCharacterCount(inMission: true);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Character spawnedCharacter = CreateHuman(GetHumanPrefabFromElement(element), characters, characterItems, Submarine.MainSub, CharacterTeamType.FriendlyNPC, explicitStayInHullPos, humanPrefabRandSync: randSync);
|
||||
if (spawnedCharacter.AIController is HumanAIController humanAI)
|
||||
{
|
||||
humanAI.InitMentalStateManager();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (terroristChance > 0f)
|
||||
{
|
||||
int terroristCount = (int)Math.Ceiling(terroristChance * Rand.Range(0.8f, 1.2f) * characters.Count);
|
||||
terroristCount = Math.Clamp(terroristCount, 1, characters.Count);
|
||||
|
||||
terroristCharacters.Clear();
|
||||
characters.GetRange(0, terroristCount).ForEach(c => terroristCharacters.Add(c));
|
||||
|
||||
terroristDistanceSquared = Vector2.DistanceSquared(Level.Loaded.StartPosition, Level.Loaded.EndPosition) * Rand.Range(0.35f, 0.65f);
|
||||
|
||||
#if DEBUG
|
||||
DebugConsole.AddWarning("Terrorists will trigger at range " + Math.Sqrt(terroristDistanceSquared));
|
||||
foreach (Character character in terroristCharacters)
|
||||
{
|
||||
DebugConsole.AddWarning(character.Name + " is a terrorist.");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private void InitCharacters()
|
||||
{
|
||||
int scalingCharacterCount = CalculateScalingEscortedCharacterCount(inMission: true);
|
||||
|
||||
if (scalingCharacterCount * characterConfig.Elements().Count() != characters.Count)
|
||||
{
|
||||
DebugConsole.AddWarning("Character count did not match expected character count in InitCharacters of EscortMission");
|
||||
return;
|
||||
}
|
||||
int i = 0;
|
||||
|
||||
foreach (XElement element in characterConfig.Elements())
|
||||
{
|
||||
string escortIdentifier = element.GetAttributeString("escortidentifier", string.Empty);
|
||||
string colorIdentifier = element.GetAttributeString("color", string.Empty);
|
||||
for (int k = 0; k < scalingCharacterCount; k++)
|
||||
{
|
||||
// for each element defined, we need to initialize that type of character equal to the scaling escorted character count
|
||||
characters[k + i].IsEscorted = true;
|
||||
if (escortIdentifier != string.Empty)
|
||||
{
|
||||
if (escortIdentifier == "vip")
|
||||
{
|
||||
vipCharacter = characters[k + i];
|
||||
}
|
||||
}
|
||||
characters[k + i].UniqueNameColor = element.GetAttributeColor("color", Color.LightGreen);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void StartMissionSpecific(Level level)
|
||||
{
|
||||
if (characters.Count > 0)
|
||||
{
|
||||
#if DEBUG
|
||||
throw new Exception($"characters.Count > 0 ({characters.Count})");
|
||||
#else
|
||||
DebugConsole.AddWarning("Character list was not empty at the start of a escort mission. The mission instance may not have been ended correctly on previous rounds.");
|
||||
characters.Clear();
|
||||
#endif
|
||||
}
|
||||
|
||||
if (characterConfig == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)");
|
||||
return;
|
||||
}
|
||||
|
||||
// to ensure single missions run without issues, default to mainsub
|
||||
if (missionSub == null)
|
||||
{
|
||||
missionSub = Submarine.MainSub;
|
||||
CalculateReward();
|
||||
}
|
||||
|
||||
if (!IsClient)
|
||||
{
|
||||
InitEscort();
|
||||
InitCharacters();
|
||||
}
|
||||
}
|
||||
|
||||
void TryToTriggerTerrorists()
|
||||
{
|
||||
if (terroristsShouldAct)
|
||||
{
|
||||
// decoupled from range check to prevent from weirdness if players handcuff a terrorist and move backwards
|
||||
foreach (Character character in terroristCharacters)
|
||||
{
|
||||
if (character.HasTeamChange(TerroristTeamChangeIdentifier))
|
||||
{
|
||||
// already triggered
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsAlive(character) && !character.IsIncapacitated && !character.LockHands)
|
||||
{
|
||||
character.TryAddNewTeamChange(TerroristTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful, aggressiveBehavior: true));
|
||||
character.Speak(TextManager.Get("dialogterroristannounce"), null, Rand.Range(0.5f, 3f));
|
||||
XElement randomElement = itemConfig.Elements().GetRandom(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty);
|
||||
if (randomElement != null)
|
||||
{
|
||||
HumanPrefab.InitializeItem(character, randomElement, character.Submarine, humanPrefab: null, createNetworkEvents: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, Level.Loaded.EndPosition) < terroristDistanceSquared)
|
||||
{
|
||||
foreach (Character character in terroristCharacters)
|
||||
{
|
||||
if (character.AIController is HumanAIController humanAI)
|
||||
{
|
||||
humanAI.ObjectiveManager.AddObjective(new AIObjectiveEscapeHandcuffs(character, humanAI.ObjectiveManager, shouldSwitchTeams: false, beginInstantly: true));
|
||||
}
|
||||
}
|
||||
terroristsShouldAct = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool NonTerroristsStillAlive(IEnumerable<Character> characterList)
|
||||
{
|
||||
return characterList.All(c => terroristCharacters.Contains(c) || IsAlive(c));
|
||||
}
|
||||
|
||||
protected override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
if (!IsClient)
|
||||
{
|
||||
int newState = State;
|
||||
TryToTriggerTerrorists();
|
||||
switch (State)
|
||||
{
|
||||
case 0: // base
|
||||
if (!NonTerroristsStillAlive(characters))
|
||||
{
|
||||
newState = 1;
|
||||
}
|
||||
if (terroristCharacters.Any() && terroristCharacters.All(c => !IsAlive(c)))
|
||||
{
|
||||
newState = 2;
|
||||
}
|
||||
break;
|
||||
case 1: // failure
|
||||
break;
|
||||
case 2: // terrorists killed
|
||||
if (!NonTerroristsStillAlive(characters))
|
||||
{
|
||||
newState = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
State = newState;
|
||||
}
|
||||
}
|
||||
|
||||
private bool Survived(Character character)
|
||||
{
|
||||
return IsAlive(character) && character.CurrentHull != null && character.CurrentHull.Submarine == Submarine.MainSub;
|
||||
}
|
||||
|
||||
private bool IsAlive(Character character)
|
||||
{
|
||||
return character != null && !character.Removed && !character.IsDead;
|
||||
}
|
||||
|
||||
private bool IsCaptured(Character character)
|
||||
{
|
||||
return character.LockHands && character.HasTeamChange(TerroristTeamChangeIdentifier);
|
||||
}
|
||||
|
||||
public override void End()
|
||||
{
|
||||
if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
|
||||
{
|
||||
bool terroristsSurvived = terroristCharacters.Any(c => Survived(c) && !IsCaptured(c));
|
||||
bool friendliesSurvived = characters.Except(terroristCharacters).All(c => Survived(c));
|
||||
bool vipDied = false;
|
||||
|
||||
// this logic is currently irrelevant, as the mission is failed regardless of who dies
|
||||
if (vipCharacter != null)
|
||||
{
|
||||
vipDied = !Survived(vipCharacter);
|
||||
}
|
||||
|
||||
if (friendliesSurvived && !terroristsSurvived && !vipDied)
|
||||
{
|
||||
GiveReward();
|
||||
completed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// characters that survived will take their items with them, in case players tried to be crafty and steal them
|
||||
// this needs to run here in case players abort the mission by going back home
|
||||
// TODO: I think this might feel like a bug.
|
||||
foreach (var characterItem in characterItems)
|
||||
{
|
||||
if (Survived(characterItem.Key) || !completed)
|
||||
{
|
||||
foreach (Item item in characterItem.Value)
|
||||
{
|
||||
if (!item.Removed)
|
||||
{
|
||||
item.Remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
characters.Clear();
|
||||
characterItems.Clear();
|
||||
failed = !completed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class GoToMission : Mission
|
||||
{
|
||||
public GoToMission(MissionPrefab prefab, Location[] locations, Submarine sub)
|
||||
: base(prefab, locations, sub)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
State = 1;
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
public override void ClientReadInitial(IReadMessage msg)
|
||||
{
|
||||
}
|
||||
#elif SERVER
|
||||
|
||||
public override void ServerWriteInitial(IWriteMessage msg, Client c)
|
||||
{
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public MineralMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations)
|
||||
public MineralMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
|
||||
{
|
||||
var configElement = prefab.ConfigElement.Element("Items");
|
||||
foreach (var c in configElement.GetChildElements("Item"))
|
||||
@@ -115,7 +115,7 @@ namespace Barotrauma
|
||||
FindRelevantLevelResources();
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
protected override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
if (IsClient) { return; }
|
||||
switch (State)
|
||||
|
||||
@@ -3,6 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -22,6 +23,7 @@ namespace Barotrauma
|
||||
if (state != value)
|
||||
{
|
||||
state = value;
|
||||
TryTriggerEvents(state);
|
||||
#if SERVER
|
||||
GameMain.Server?.UpdateMissionState(this, state);
|
||||
#endif
|
||||
@@ -61,14 +63,19 @@ namespace Barotrauma
|
||||
//private set { description = value; }
|
||||
}
|
||||
|
||||
protected string descriptionWithoutReward;
|
||||
|
||||
public virtual bool AllowUndocking
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public int Reward
|
||||
public virtual int Reward
|
||||
{
|
||||
get { return Prefab.Reward; }
|
||||
get
|
||||
{
|
||||
return Prefab.Reward;
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, float> ReputationRewards
|
||||
@@ -92,6 +99,16 @@ namespace Barotrauma
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public virtual int TeamCount
|
||||
{
|
||||
get { return 1; }
|
||||
}
|
||||
|
||||
public virtual SubmarineInfo EnemySubmarineInfo
|
||||
{
|
||||
get { return null; }
|
||||
}
|
||||
|
||||
public virtual IEnumerable<Vector2> SonarPositions
|
||||
{
|
||||
get { return Enumerable.Empty<Vector2>(); }
|
||||
@@ -112,8 +129,22 @@ namespace Barotrauma
|
||||
{
|
||||
get { return Prefab.Difficulty; }
|
||||
}
|
||||
|
||||
private class DelayedTriggerEvent
|
||||
{
|
||||
public readonly MissionPrefab.TriggerEvent TriggerEvent;
|
||||
public float Delay;
|
||||
|
||||
public DelayedTriggerEvent(MissionPrefab.TriggerEvent triggerEvent, float delay)
|
||||
{
|
||||
TriggerEvent = triggerEvent;
|
||||
Delay = delay;
|
||||
}
|
||||
}
|
||||
|
||||
private List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>();
|
||||
|
||||
public Mission(MissionPrefab prefab, Location[] locations)
|
||||
public Mission(MissionPrefab prefab, Location[] locations, Submarine sub)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(locations.Length == 2);
|
||||
|
||||
@@ -138,8 +169,12 @@ namespace Barotrauma
|
||||
Messages[m] = Messages[m].Replace("[location" + (n + 1) + "]", locationName);
|
||||
}
|
||||
}
|
||||
string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", Reward)}‖end‖";
|
||||
if (description != null) { description = description.Replace("[reward]", rewardText); }
|
||||
string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖";
|
||||
if (description != null)
|
||||
{
|
||||
descriptionWithoutReward = description;
|
||||
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++)
|
||||
@@ -147,6 +182,9 @@ namespace Barotrauma
|
||||
Messages[m] = Messages[m].Replace("[reward]", rewardText);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void SetDifficulty(float difficulty) { }
|
||||
|
||||
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false)
|
||||
{
|
||||
return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionType, isSinglePlayer);
|
||||
@@ -181,7 +219,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (randomNumber <= missionPrefab.Commonness)
|
||||
{
|
||||
return missionPrefab.Instantiate(locations);
|
||||
return missionPrefab.Instantiate(locations, Submarine.MainSub);
|
||||
}
|
||||
randomNumber -= missionPrefab.Commonness;
|
||||
}
|
||||
@@ -189,11 +227,18 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
|
||||
public virtual int GetReward(Submarine sub)
|
||||
{
|
||||
return Prefab.Reward;
|
||||
}
|
||||
|
||||
public void Start(Level level)
|
||||
{
|
||||
state = 0;
|
||||
#if CLIENT
|
||||
shownMessages.Clear();
|
||||
#endif
|
||||
delayedTriggerEvents.Clear();
|
||||
foreach (string categoryToShow in Prefab.UnhideEntitySubCategories)
|
||||
{
|
||||
foreach (MapEntity entityToShow in MapEntity.mapEntityList.Where(me => me.prefab?.HasSubCategory(categoryToShow) ?? false))
|
||||
@@ -202,12 +247,27 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
this.level = level;
|
||||
TryTriggerEvents(0);
|
||||
StartMissionSpecific(level);
|
||||
}
|
||||
|
||||
protected virtual void StartMissionSpecific(Level level) { }
|
||||
|
||||
public virtual void Update(float deltaTime) { }
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
for (int i = delayedTriggerEvents.Count - 1; i>=0;i--)
|
||||
{
|
||||
delayedTriggerEvents[i].Delay -= deltaTime;
|
||||
if (delayedTriggerEvents[i].Delay <= 0.0f)
|
||||
{
|
||||
TriggerEvent(delayedTriggerEvents[i].TriggerEvent);
|
||||
delayedTriggerEvents.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
UpdateMissionSpecific(deltaTime);
|
||||
}
|
||||
|
||||
protected virtual void UpdateMissionSpecific(float deltaTime) { }
|
||||
|
||||
protected void ShowMessage(int missionState)
|
||||
{
|
||||
@@ -216,6 +276,57 @@ namespace Barotrauma
|
||||
|
||||
partial void ShowMessageProjSpecific(int missionState);
|
||||
|
||||
|
||||
private void TryTriggerEvents(int state)
|
||||
{
|
||||
foreach (var triggerEvent in Prefab.TriggerEvents)
|
||||
{
|
||||
if (triggerEvent.State == state)
|
||||
{
|
||||
TryTriggerEvent(triggerEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the event or adds it to the delayedTriggerEvents it if it has a delay
|
||||
/// </summary>
|
||||
private void TryTriggerEvent(MissionPrefab.TriggerEvent trigger)
|
||||
{
|
||||
if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; }
|
||||
if (trigger.Delay > 0)
|
||||
{
|
||||
if (!delayedTriggerEvents.Any(t => t.TriggerEvent == trigger))
|
||||
{
|
||||
delayedTriggerEvents.Add(new DelayedTriggerEvent(trigger, trigger.Delay));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TriggerEvent(trigger);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the event immediately, ignoring any delays
|
||||
/// </summary>
|
||||
private void TriggerEvent(MissionPrefab.TriggerEvent trigger)
|
||||
{
|
||||
if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; }
|
||||
var eventPrefab = EventSet.GetAllEventPrefabs().Find(p => p.Identifier.Equals(trigger.EventIdentifier, StringComparison.OrdinalIgnoreCase));
|
||||
if (eventPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Mission \"{Name}\" failed to trigger an event (couldn't find an event with the identifier \"{trigger.EventIdentifier}\").");
|
||||
return;
|
||||
}
|
||||
if (GameMain.GameSession?.EventManager != null)
|
||||
{
|
||||
var newEvent = eventPrefab.CreateInstance();
|
||||
GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent);
|
||||
newEvent.Init(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// End the mission and give a reward if it was completed successfully
|
||||
/// </summary>
|
||||
@@ -232,7 +343,7 @@ namespace Barotrauma
|
||||
public void GiveReward()
|
||||
{
|
||||
if (!(GameMain.GameSession.GameMode is CampaignMode campaign)) { return; }
|
||||
campaign.Money += Reward;
|
||||
campaign.Money += GetReward(Submarine.MainSub);
|
||||
|
||||
foreach (KeyValuePair<string, float> reputationReward in ReputationRewards)
|
||||
{
|
||||
@@ -287,5 +398,47 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public virtual void AdjustLevelData(LevelData levelData) { }
|
||||
|
||||
// putting these here since both escort and pirate missions need them. could be tucked away into another class that they can inherit from (or use composition)
|
||||
protected HumanPrefab GetHumanPrefabFromElement(XElement element)
|
||||
{
|
||||
if (element.Attribute("name") != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in mission \"" + Name + "\" - use character identifiers instead of names to configure the characters.");
|
||||
|
||||
return 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 character for mission: character prefab \"" + characterIdentifier + "\" not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
return humanPrefab;
|
||||
}
|
||||
|
||||
protected Character CreateHuman(HumanPrefab humanPrefab, List<Character> characters, Dictionary<Character, List<Item>> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.Server, bool giveTags = true)
|
||||
{
|
||||
if (positionToStayIn == null)
|
||||
{
|
||||
positionToStayIn = WayPoint.GetRandom(SpawnType.Human, null, submarine);
|
||||
}
|
||||
|
||||
var characterInfo = humanPrefab.GetCharacterInfo(Rand.RandSync.Server) ?? new CharacterInfo(CharacterPrefab.HumanSpeciesName, npcIdentifier: humanPrefab.Identifier, jobPrefab: humanPrefab.GetJobPrefab(humanPrefabRandSync), randSync: humanPrefabRandSync);
|
||||
characterInfo.TeamID = teamType;
|
||||
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
|
||||
spawnedCharacter.Prefab = humanPrefab;
|
||||
humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn);
|
||||
humanPrefab.GiveItems(spawnedCharacter, submarine, Rand.RandSync.Server, createNetworkEvents: false);
|
||||
|
||||
characters.Add(spawnedCharacter);
|
||||
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
|
||||
|
||||
return spawnedCharacter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,11 @@ namespace Barotrauma
|
||||
Nest = 0x10,
|
||||
Mineral = 0x20,
|
||||
Combat = 0x40,
|
||||
OutpostDestroy = 0x80,
|
||||
OutpostRescue = 0x100,
|
||||
|
||||
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | OutpostDestroy | OutpostRescue
|
||||
AbandonedOutpost = 0x80,
|
||||
Escort = 0x100,
|
||||
Pirate = 0x200,
|
||||
GoTo = 0x400,
|
||||
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo
|
||||
}
|
||||
|
||||
partial class MissionPrefab
|
||||
@@ -36,13 +37,17 @@ namespace Barotrauma
|
||||
{ MissionType.Beacon, typeof(BeaconMission) },
|
||||
{ MissionType.Nest, typeof(NestMission) },
|
||||
{ MissionType.Mineral, typeof(MineralMission) },
|
||||
{ MissionType.OutpostDestroy, typeof(OutpostDestroyMission) },
|
||||
{ MissionType.OutpostRescue, typeof(AbandonedOutpostMission) },
|
||||
{ MissionType.AbandonedOutpost, typeof(AbandonedOutpostMission) },
|
||||
{ MissionType.Escort, typeof(EscortMission) },
|
||||
{ MissionType.Pirate, typeof(PirateMission) },
|
||||
{ MissionType.GoTo, typeof(GoToMission) }
|
||||
};
|
||||
public static readonly Dictionary<MissionType, Type> PvPMissionClasses = new Dictionary<MissionType, Type>()
|
||||
{
|
||||
{ MissionType.Combat, typeof(CombatMission) }
|
||||
};
|
||||
|
||||
public static readonly HashSet<MissionType> HiddenMissionClasses = new HashSet<MissionType>() { MissionType.GoTo };
|
||||
|
||||
private readonly ConstructorInfo constructor;
|
||||
|
||||
@@ -84,6 +89,8 @@ namespace Barotrauma
|
||||
|
||||
public readonly bool IsSideObjective;
|
||||
|
||||
public readonly bool RequireWreck;
|
||||
|
||||
/// <summary>
|
||||
/// The mission can only be received when travelling from Pair.First to Pair.Second
|
||||
/// </summary>
|
||||
@@ -99,6 +106,28 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public readonly List<string> UnhideEntitySubCategories = new List<string>();
|
||||
|
||||
public class TriggerEvent
|
||||
{
|
||||
[Serialize("", true)]
|
||||
public string EventIdentifier { get; private set; }
|
||||
|
||||
[Serialize(0, true)]
|
||||
public int State { get; private set; }
|
||||
|
||||
[Serialize(0.0f, true)]
|
||||
public float Delay { get; private set; }
|
||||
|
||||
[Serialize(false, true)]
|
||||
public bool CampaignOnly { get; private set; }
|
||||
|
||||
public TriggerEvent(XElement element)
|
||||
{
|
||||
SerializableProperty.DeserializeProperties(this, element);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly List<TriggerEvent> TriggerEvents = new List<TriggerEvent>();
|
||||
|
||||
public LocationTypeChange LocationTypeChangeOnCompleted;
|
||||
|
||||
public readonly XElement ConfigElement;
|
||||
@@ -157,6 +186,7 @@ namespace Barotrauma
|
||||
Reward = element.GetAttributeInt("reward", 1);
|
||||
AllowRetry = element.GetAttributeBool("allowretry", false);
|
||||
IsSideObjective = element.GetAttributeBool("sideobjective", false);
|
||||
RequireWreck = element.GetAttributeBool("requirewreck", false);
|
||||
Commonness = element.GetAttributeInt("commonness", 1);
|
||||
if (element.GetAttribute("difficulty") != null)
|
||||
{
|
||||
@@ -269,10 +299,19 @@ namespace Barotrauma
|
||||
DataRewards.Add(Tuple.Create(identifier, value, operation));
|
||||
}
|
||||
break;
|
||||
case "triggerevent":
|
||||
TriggerEvents.Add(new TriggerEvent(subElement));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
string missionTypeName = element.GetAttributeString("type", "");
|
||||
//backwards compatibility
|
||||
if (missionTypeName.Equals("outpostdestroy", StringComparison.OrdinalIgnoreCase) || missionTypeName.Equals("outpostrescue", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
missionTypeName = "AbandonedOutpost";
|
||||
}
|
||||
|
||||
if (!Enum.TryParse(missionTypeName, out Type))
|
||||
{
|
||||
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type.");
|
||||
@@ -286,16 +325,20 @@ namespace Barotrauma
|
||||
|
||||
if (CoOpMissionClasses.ContainsKey(Type))
|
||||
{
|
||||
constructor = CoOpMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) });
|
||||
constructor = CoOpMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) });
|
||||
}
|
||||
else if (PvPMissionClasses.ContainsKey(Type))
|
||||
{
|
||||
constructor = PvPMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) });
|
||||
constructor = PvPMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) });
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - unsupported mission type \"" + Type.ToString() + "\"");
|
||||
}
|
||||
if (constructor == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to find a constructor for the mission type \"{Type}\"!");
|
||||
}
|
||||
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
@@ -333,9 +376,9 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
public Mission Instantiate(Location[] locations)
|
||||
public Mission Instantiate(Location[] locations, Submarine sub)
|
||||
{
|
||||
return constructor?.Invoke(new object[] { this, locations }) as Mission;
|
||||
return constructor?.Invoke(new object[] { this, locations, sub }) as Mission;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public MonsterMission(MissionPrefab prefab, Location[] locations)
|
||||
: base(prefab, locations)
|
||||
public MonsterMission(MissionPrefab prefab, Location[] locations, Submarine sub)
|
||||
: base(prefab, locations, sub)
|
||||
{
|
||||
string speciesName = prefab.ConfigElement.GetAttributeString("monsterfile", null);
|
||||
if (!string.IsNullOrEmpty(speciesName))
|
||||
@@ -160,7 +160,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
protected override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
switch (State)
|
||||
{
|
||||
|
||||
@@ -46,8 +46,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public NestMission(MissionPrefab prefab, Location[] locations)
|
||||
: base(prefab, locations)
|
||||
public NestMission(MissionPrefab prefab, Location[] locations, Submarine sub)
|
||||
: base(prefab, locations, sub)
|
||||
{
|
||||
itemConfig = prefab.ConfigElement.Element("Items");
|
||||
|
||||
@@ -216,7 +216,7 @@ namespace Barotrauma
|
||||
level.LevelObjectManager.PlaceNestObjects(level, cave, nestPosition, nestObjectRadius, nestObjectAmount);
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
protected override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
if (IsClient)
|
||||
{
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class PirateMission : Mission
|
||||
{
|
||||
private readonly XElement submarineTypeConfig;
|
||||
private readonly XElement characterConfig;
|
||||
private readonly XElement characterTypeConfig;
|
||||
private readonly float addedMissionDifficultyPerPlayer;
|
||||
|
||||
private float missionDifficulty;
|
||||
private int alternateReward;
|
||||
|
||||
private Submarine enemySub;
|
||||
private readonly List<Character> characters = new List<Character>();
|
||||
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
|
||||
|
||||
// Update the last sighting periodically so that the players can find the pirate sub even if they have lost the track of it.
|
||||
private readonly float pirateSightingUpdateFrequency = 30;
|
||||
private float pirateSightingUpdateTimer;
|
||||
private Vector2? lastSighting;
|
||||
|
||||
public override int TeamCount => 2;
|
||||
|
||||
private bool outsideOfSonarRange;
|
||||
|
||||
private readonly List<Vector2> patrolPositions = new List<Vector2>();
|
||||
|
||||
public override IEnumerable<Vector2> SonarPositions
|
||||
{
|
||||
get
|
||||
{
|
||||
var empty = Enumerable.Empty<Vector2>();
|
||||
if (outsideOfSonarRange)
|
||||
{
|
||||
return State switch
|
||||
{
|
||||
0 => patrolPositions,
|
||||
1 => lastSighting.HasValue ? lastSighting.Value.ToEnumerable() : empty,
|
||||
_ => empty,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override int GetReward(Submarine sub)
|
||||
{
|
||||
return alternateReward;
|
||||
}
|
||||
|
||||
private SubmarineInfo submarineInfo;
|
||||
|
||||
public override SubmarineInfo EnemySubmarineInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
return submarineInfo;
|
||||
}
|
||||
}
|
||||
|
||||
// these values could also be defined within the mission XML
|
||||
private const float RandomnessModifier = 25;
|
||||
private const float ShipRandomnessModifier = 15;
|
||||
|
||||
private const float MaxDifficulty = 100;
|
||||
|
||||
public PirateMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
|
||||
{
|
||||
submarineTypeConfig = prefab.ConfigElement.Element("SubmarineTypes");
|
||||
characterConfig = prefab.ConfigElement.Element("Characters");
|
||||
characterTypeConfig = prefab.ConfigElement.Element("CharacterTypes");
|
||||
addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0);
|
||||
|
||||
// for campaign missions, set difficulty at construction
|
||||
LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
|
||||
|
||||
SetDifficulty(levelData?.Difficulty ?? Level.Loaded?.Difficulty ?? 0f);
|
||||
}
|
||||
|
||||
public override void SetDifficulty(float difficulty)
|
||||
{
|
||||
if (missionDifficulty > 0f)
|
||||
{
|
||||
// difficulty already set
|
||||
return;
|
||||
}
|
||||
|
||||
missionDifficulty = difficulty;
|
||||
|
||||
XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier);
|
||||
|
||||
alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward);
|
||||
|
||||
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", alternateReward)}‖end‖";
|
||||
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
|
||||
|
||||
string submarinePath = submarineConfig.GetAttributeString("path", string.Empty);
|
||||
if (submarinePath == string.Empty)
|
||||
{
|
||||
DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!");
|
||||
return;
|
||||
}
|
||||
// maybe a little redundant
|
||||
var contentFile = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.EnemySubmarine).FirstOrDefault(x => x.Path == submarinePath);
|
||||
if (contentFile == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!");
|
||||
return;
|
||||
}
|
||||
|
||||
submarineInfo = new SubmarineInfo(contentFile.Path);
|
||||
}
|
||||
|
||||
private float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier)
|
||||
{
|
||||
return Math.Abs(levelDifficulty - preferredDifficulty + (Rand.Range(-randomnessModifier, randomnessModifier, Rand.RandSync.Server)));
|
||||
}
|
||||
private int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty)
|
||||
{
|
||||
return Math.Max((int)Math.Round(minAmount + (maxAmount - minAmount) * ((levelDifficulty + Rand.Range(-RandomnessModifier, RandomnessModifier, Rand.RandSync.Server)) / MaxDifficulty)), minAmount);
|
||||
}
|
||||
|
||||
private XElement GetRandomDifficultyModifiedElement(XElement parentElement, float levelDifficulty, float randomnessModifier)
|
||||
{
|
||||
// look for the element that is closest to our difficulty, with some randomness
|
||||
XElement bestElement = null;
|
||||
float bestValue = float.MaxValue;
|
||||
foreach (XElement element in parentElement.Elements())
|
||||
{
|
||||
float applicabilityValue = GetDifficultyModifiedValue(element.GetAttributeFloat(0f, "preferreddifficulty"), levelDifficulty, randomnessModifier);
|
||||
if (applicabilityValue < bestValue)
|
||||
{
|
||||
bestElement = element;
|
||||
bestValue = applicabilityValue;
|
||||
}
|
||||
}
|
||||
return bestElement;
|
||||
}
|
||||
|
||||
private void CreateMissionPositions(out Vector2 preferredSpawnPos)
|
||||
{
|
||||
Vector2 patrolPos = enemySub.WorldPosition;
|
||||
Point subSize = enemySub.GetDockedBorders().Size;
|
||||
|
||||
if (!Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath | Level.PositionType.SidePath, Level.Loaded.Size.X * 0.3f, out preferredSpawnPos))
|
||||
{
|
||||
DebugConsole.ThrowError("Could not spawn pirate submarine in an interesting location! " + this);
|
||||
}
|
||||
if (!Level.Loaded.TryGetInterestingPositionAwayFromPoint(true, Level.PositionType.MainPath | Level.PositionType.SidePath, Level.Loaded.Size.X * 0.3f, out patrolPos, preferredSpawnPos, minDistFromPoint: 10000f))
|
||||
{
|
||||
DebugConsole.ThrowError("Could not give pirate submarine an interesting location to patrol to! " + this);
|
||||
}
|
||||
|
||||
patrolPos = enemySub.FindSpawnPos(patrolPos, subSize);
|
||||
|
||||
patrolPositions.Add(patrolPos);
|
||||
patrolPositions.Add(preferredSpawnPos);
|
||||
|
||||
if (!IsClient)
|
||||
{
|
||||
PathFinder pathFinder = new PathFinder(WayPoint.WayPointList, false);
|
||||
var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(patrolPos), ConvertUnits.ToSimUnits(preferredSpawnPos));
|
||||
if (!path.Unreachable)
|
||||
{
|
||||
preferredSpawnPos = path.Nodes[Rand.Range(0, path.Nodes.Count - 1)].WorldPosition; // spawn the sub in a random point in the path if possible
|
||||
}
|
||||
|
||||
int graceDistance = 500; // the sub still spawns awkwardly close to walls, so this helps. could also be given as a parameter instead
|
||||
preferredSpawnPos = enemySub.FindSpawnPos(preferredSpawnPos, new Point(subSize.X + graceDistance, subSize.Y + graceDistance));
|
||||
}
|
||||
}
|
||||
|
||||
private void InitPirateShip(Vector2 spawnPos)
|
||||
{
|
||||
enemySub.NeutralizeBallast();
|
||||
if (enemySub.GetItems(alsoFromConnectedSubs: false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
|
||||
{
|
||||
reactor.PowerUpImmediately();
|
||||
}
|
||||
enemySub.EnableMaintainPosition();
|
||||
enemySub.TeamID = CharacterTeamType.None;
|
||||
//make the enemy sub withstand atleast the same depth as the player sub
|
||||
enemySub.RealWorldCrushDepth = Math.Max(enemySub.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth);
|
||||
}
|
||||
|
||||
private void InitPirates()
|
||||
{
|
||||
characters.Clear();
|
||||
characterItems.Clear();
|
||||
|
||||
if (characterConfig == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)");
|
||||
return;
|
||||
}
|
||||
|
||||
int playerCount = 1;
|
||||
|
||||
#if SERVER
|
||||
playerCount = GameMain.Server.ConnectedClients.Where(c => !c.SpectateOnly || !GameMain.Server.ServerSettings.AllowSpectating).Count();
|
||||
#endif
|
||||
|
||||
float enemyCreationDifficulty = missionDifficulty + playerCount * addedMissionDifficultyPerPlayer;
|
||||
|
||||
bool commanderAssigned = false;
|
||||
foreach (XElement element in characterConfig.Elements())
|
||||
{
|
||||
// it is possible to get more than the "max" amount of characters if the modified difficulty is high enough; this is intentional
|
||||
// if necessary, another "hard max" value could be used to clamp the value for performance/gameplay concerns
|
||||
int amountCreated = GetDifficultyModifiedAmount(element.GetAttributeInt("minamount", 0), element.GetAttributeInt("maxamount", 0), enemyCreationDifficulty);
|
||||
for (int i = 0; i < amountCreated; i++)
|
||||
{
|
||||
XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == element.GetAttributeString("typeidentifier", string.Empty)).FirstOrDefault();
|
||||
|
||||
if (characterType == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"No character types defined in CharacterTypes for a declared type identifier in mission \"{Prefab.Identifier}\".");
|
||||
return;
|
||||
}
|
||||
|
||||
XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier);
|
||||
|
||||
Character spawnedCharacter = CreateHuman(GetHumanPrefabFromElement(variantElement), characters, characterItems, enemySub, CharacterTeamType.None, null);
|
||||
if (!commanderAssigned)
|
||||
{
|
||||
bool isCommander = variantElement.GetAttributeBool("iscommander", false);
|
||||
if (isCommander && spawnedCharacter.AIController is HumanAIController humanAIController)
|
||||
{
|
||||
humanAIController.InitShipCommandManager();
|
||||
foreach (var patrolPos in patrolPositions)
|
||||
{
|
||||
humanAIController.ShipCommandManager.patrolPositions.Add(patrolPos);
|
||||
}
|
||||
commanderAssigned = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Item item in spawnedCharacter.Inventory.AllItems)
|
||||
{
|
||||
if (item?.Prefab.Identifier == "idcard")
|
||||
{
|
||||
item.AddTag("id_pirate");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void StartMissionSpecific(Level level)
|
||||
{
|
||||
if (characters.Count > 0)
|
||||
{
|
||||
#if DEBUG
|
||||
throw new Exception($"characters.Count > 0 ({characters.Count})");
|
||||
#else
|
||||
DebugConsole.AddWarning("Character list was not empty at the start of a pirate mission. The mission instance may not have been ended correctly on previous rounds.");
|
||||
characters.Clear();
|
||||
#endif
|
||||
}
|
||||
|
||||
if (patrolPositions.Count > 0)
|
||||
{
|
||||
#if DEBUG
|
||||
throw new Exception($"patrolPositions.Count > 0 ({patrolPositions.Count})");
|
||||
#else
|
||||
DebugConsole.AddWarning("Patrol point list was not empty at the start of a pirate mission. The mission instance may not have been ended correctly on previous rounds.");
|
||||
patrolPositions.Clear();
|
||||
#endif
|
||||
}
|
||||
|
||||
enemySub = Submarine.MainSubs[1];
|
||||
|
||||
if (enemySub == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Enemy Submarine was not created. SubmarineInfo is likely not defined.");
|
||||
// TODO: should we set the state to something here?
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 spawnPos = Level.Loaded.EndPosition; // in case TryGetInterestingPosition fails, though this should not happen
|
||||
CreateMissionPositions(out spawnPos); // patrol positions are not explicitly replicated, instead they are acquired the same way the server acquires them
|
||||
#if DEBUG
|
||||
if (IsClient)
|
||||
{
|
||||
DebugConsole.NewMessage("The patrol positions set by client were: ");
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.NewMessage("The patrol positions set by server were: ");
|
||||
}
|
||||
foreach (var patrolPos in patrolPositions)
|
||||
{
|
||||
DebugConsole.NewMessage("Patrol pos: " + patrolPos);
|
||||
}
|
||||
#endif
|
||||
if (!IsClient)
|
||||
{
|
||||
InitPirateShip(spawnPos);
|
||||
}
|
||||
enemySub.SetPosition(spawnPos);
|
||||
|
||||
// flipping the sub on the frame it is moved into place must be done after it's been moved, or it breaks item connections in the submarine
|
||||
// creating the pirates has to be done after the sub has been flipped, or it seems to break the AI pathing
|
||||
enemySub.FlipX();
|
||||
enemySub.ShowSonarMarker = false;
|
||||
|
||||
if (!IsClient)
|
||||
{
|
||||
InitPirates();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
int newState = State;
|
||||
float sqrSonarRange = MathUtils.Pow2(Sonar.DefaultSonarRange);
|
||||
outsideOfSonarRange = Vector2.DistanceSquared(enemySub.WorldPosition, Submarine.MainSub.WorldPosition) > sqrSonarRange;
|
||||
if (State < 2 && CheckWinState())
|
||||
{
|
||||
newState = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (State)
|
||||
{
|
||||
case 0:
|
||||
for (int i = patrolPositions.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (Vector2.DistanceSquared(patrolPositions[i], Submarine.MainSub.WorldPosition) < sqrSonarRange)
|
||||
{
|
||||
patrolPositions.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
if (!outsideOfSonarRange || patrolPositions.None())
|
||||
{
|
||||
newState = 1;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if (outsideOfSonarRange)
|
||||
{
|
||||
if (lastSighting.HasValue && Vector2.DistanceSquared(lastSighting.Value, Submarine.MainSub.WorldPosition) < sqrSonarRange)
|
||||
{
|
||||
lastSighting = null;
|
||||
}
|
||||
pirateSightingUpdateTimer -= deltaTime;
|
||||
if (pirateSightingUpdateTimer < 0)
|
||||
{
|
||||
pirateSightingUpdateTimer = pirateSightingUpdateFrequency;
|
||||
lastSighting = enemySub.WorldPosition;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
lastSighting = enemySub.WorldPosition;
|
||||
pirateSightingUpdateTimer = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
State = newState;
|
||||
}
|
||||
|
||||
private bool CheckWinState() => !IsClient && (characters.All(m => !Survived(m)));
|
||||
|
||||
private bool Survived(Character character)
|
||||
{
|
||||
return character != null && !character.Removed && !character.IsDead;
|
||||
}
|
||||
|
||||
public override void End()
|
||||
{
|
||||
if (state == 2)
|
||||
{
|
||||
GiveReward();
|
||||
completed = true;
|
||||
}
|
||||
characters.Clear();
|
||||
characterItems.Clear();
|
||||
failed = !completed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,8 +43,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public SalvageMission(MissionPrefab prefab, Location[] locations)
|
||||
: base(prefab, locations)
|
||||
public SalvageMission(MissionPrefab prefab, Location[] locations, Submarine sub)
|
||||
: base(prefab, locations, sub)
|
||||
{
|
||||
containerTag = prefab.ConfigElement.GetAttributeString("containertag", "");
|
||||
|
||||
@@ -205,7 +205,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
protected override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
offset = prefab.ConfigElement.GetAttributeFloat("offset", 0);
|
||||
scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 1000), 0, 3000);
|
||||
scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000);
|
||||
|
||||
if (GameMain.NetworkMember != null)
|
||||
{
|
||||
@@ -182,18 +182,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (disallowed) { return; }
|
||||
|
||||
if (Rand.Value(Rand.RandSync.Server) > prefab.SpawnProbability)
|
||||
{
|
||||
spawnPos = null;
|
||||
Finished();
|
||||
return;
|
||||
}
|
||||
|
||||
spawnPos = Vector2.Zero;
|
||||
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 && spawnPosType != Level.PositionType.Abyss)
|
||||
bool isRuinOrWreck = spawnPosType.HasFlag(Level.PositionType.Ruin) || spawnPosType.HasFlag(Level.PositionType.Wreck);
|
||||
if (affectSubImmediately && !isRuinOrWreck && !spawnPosType.HasFlag(Level.PositionType.Abyss))
|
||||
{
|
||||
if (availablePositions.None())
|
||||
{
|
||||
@@ -264,7 +257,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isSubOrWreck)
|
||||
if (!isRuinOrWreck)
|
||||
{
|
||||
float minDistance = 20000;
|
||||
var refSub = GetReferenceSub();
|
||||
@@ -375,7 +368,7 @@ namespace Barotrauma
|
||||
if (spawnPending)
|
||||
{
|
||||
//wait until there are no submarines at the spawnpos
|
||||
if (spawnPosType == Level.PositionType.MainPath || spawnPosType == Level.PositionType.SidePath || spawnPosType == Level.PositionType.Abyss)
|
||||
if (spawnPosType.HasFlag(Level.PositionType.MainPath) || spawnPosType.HasFlag(Level.PositionType.SidePath) || spawnPosType.HasFlag(Level.PositionType.Abyss))
|
||||
{
|
||||
foreach (Submarine submarine in Submarine.Loaded)
|
||||
{
|
||||
@@ -387,7 +380,7 @@ namespace Barotrauma
|
||||
|
||||
//if spawning in a ruin/cave, wait for someone to be close to it to spawning
|
||||
//unnecessary monsters in places the players might never visit during the round
|
||||
if (spawnPosType == Level.PositionType.Ruin || spawnPosType == Level.PositionType.Cave || spawnPosType == Level.PositionType.Wreck)
|
||||
if (spawnPosType.HasFlag(Level.PositionType.Ruin) || spawnPosType.HasFlag(Level.PositionType.Cave) || spawnPosType.HasFlag(Level.PositionType.Wreck))
|
||||
{
|
||||
bool someoneNearby = false;
|
||||
float minDist = Sonar.DefaultSonarRange * 0.8f;
|
||||
@@ -415,16 +408,19 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
|
||||
if (spawnPosType == Level.PositionType.Abyss || spawnPosType == Level.PositionType.AbyssCave)
|
||||
if (spawnPosType.HasFlag(Level.PositionType.Abyss) || spawnPosType.HasFlag(Level.PositionType.AbyssCave))
|
||||
{
|
||||
bool anyInAbyss = false;
|
||||
foreach (Submarine submarine in Submarine.Loaded)
|
||||
{
|
||||
if (submarine.Info.Type != SubmarineType.Player) { continue; }
|
||||
if (submarine.WorldPosition.Y > 0)
|
||||
if (submarine.Info.Type != SubmarineType.Player || submarine == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { continue; }
|
||||
if (submarine.WorldPosition.Y < 0)
|
||||
{
|
||||
return;
|
||||
anyInAbyss = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!anyInAbyss) { return; }
|
||||
}
|
||||
|
||||
spawnPending = false;
|
||||
@@ -432,7 +428,23 @@ namespace Barotrauma
|
||||
//+1 because Range returns an integer less than the max value
|
||||
int amount = Rand.Range(minAmount, maxAmount + 1);
|
||||
monsters = new List<Character>();
|
||||
float offsetAmount = spawnPosType == Level.PositionType.MainPath || spawnPosType == Level.PositionType.SidePath ? scatter : 100;
|
||||
float scatterAmount = scatter;
|
||||
if (spawnPosType.HasFlag(Level.PositionType.SidePath))
|
||||
{
|
||||
var sidePaths = Level.Loaded.Tunnels.Where(t => t.Type == Level.TunnelType.SidePath);
|
||||
if (sidePaths.Any())
|
||||
{
|
||||
scatterAmount = Math.Min(scatter, sidePaths.Min(t => t.MinWidth) / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
scatterAmount = scatter;
|
||||
}
|
||||
}
|
||||
else if (!spawnPosType.HasFlag(Level.PositionType.MainPath))
|
||||
{
|
||||
scatterAmount = 0;
|
||||
}
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
string seed = Level.Loaded.Seed + i.ToString();
|
||||
@@ -443,8 +455,8 @@ namespace Barotrauma
|
||||
|
||||
System.Diagnostics.Debug.Assert(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer, "Clients should not create monster events.");
|
||||
|
||||
Vector2 pos = spawnPos.Value + Rand.Vector(offsetAmount);
|
||||
if (spawnPosType == Level.PositionType.MainPath || spawnPosType == Level.PositionType.SidePath)
|
||||
Vector2 pos = spawnPos.Value + Rand.Vector(scatterAmount);
|
||||
if (scatterAmount > 0)
|
||||
{
|
||||
if (Submarine.Loaded.Any(s => ToolBox.GetWorldBounds(s.Borders.Center, s.Borders.Size).ContainsWorld(pos)))
|
||||
{
|
||||
|
||||
@@ -86,10 +86,11 @@ namespace Barotrauma.Extensions
|
||||
|
||||
/// <summary>
|
||||
/// Executes an action that modifies the collection on each element (such as removing items from the list).
|
||||
/// Creates a temporary list.
|
||||
/// Creates a temporary list, unless the collection is empty.
|
||||
/// </summary>
|
||||
public static void ForEachMod<T>(this IEnumerable<T> source, Action<T> action)
|
||||
{
|
||||
if (source.None()) { return; }
|
||||
var temp = new List<T>(source);
|
||||
temp.ForEach(action);
|
||||
}
|
||||
@@ -153,5 +154,22 @@ namespace Barotrauma.Extensions
|
||||
{
|
||||
if (value != null) { source.Add(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether a given collection has at least a certain amount
|
||||
/// of elements for which the predicate returns true.
|
||||
/// </summary>
|
||||
/// <param name="source">Input collection</param>
|
||||
/// <param name="amount">How many elements to match before stopping</param>
|
||||
/// <param name="predicate">Predicate used to evaluate the elements</param>
|
||||
public static bool AtLeast<T>(this IEnumerable<T> source, int amount, Predicate<T> predicate)
|
||||
{
|
||||
foreach (T elem in source)
|
||||
{
|
||||
if (predicate(elem)) { amount--; }
|
||||
if (amount <= 0) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,8 @@ namespace Barotrauma
|
||||
SpawnedInOutpost = validContainer.Key.Item.SpawnedInOutpost,
|
||||
AllowStealing = validContainer.Key.Item.AllowStealing,
|
||||
OriginalModuleIndex = validContainer.Key.Item.OriginalModuleIndex,
|
||||
OriginalContainerID = validContainer.Key.Item.ID
|
||||
OriginalContainerIndex =
|
||||
Item.ItemList.Where(it => it.Submarine == validContainer.Key.Item.Submarine && it.OriginalModuleIndex == validContainer.Key.Item.OriginalModuleIndex).ToList().IndexOf(validContainer.Key.Item)
|
||||
};
|
||||
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
|
||||
{
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace Barotrauma
|
||||
|
||||
public List<PurchasedItem> ItemsInBuyCrate { get; } = new List<PurchasedItem>();
|
||||
public List<PurchasedItem> ItemsInSellCrate { get; } = new List<PurchasedItem>();
|
||||
public List<PurchasedItem> ItemsInSellFromSubCrate { get; } = new List<PurchasedItem>();
|
||||
public List<PurchasedItem> PurchasedItems { get; } = new List<PurchasedItem>();
|
||||
public List<SoldItem> SoldItems { get; } = new List<SoldItem>();
|
||||
|
||||
@@ -55,6 +56,7 @@ namespace Barotrauma
|
||||
|
||||
public Action OnItemsInBuyCrateChanged;
|
||||
public Action OnItemsInSellCrateChanged;
|
||||
public Action OnItemsInSellFromSubCrateChanged;
|
||||
public Action OnPurchasedItemsChanged;
|
||||
public Action OnSoldItemsChanged;
|
||||
|
||||
@@ -75,6 +77,12 @@ namespace Barotrauma
|
||||
OnItemsInSellCrateChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void ClearItemsInSellFromSubCrate()
|
||||
{
|
||||
ItemsInSellFromSubCrate.Clear();
|
||||
OnItemsInSellFromSubCrateChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void SetPurchasedItems(List<PurchasedItem> items)
|
||||
{
|
||||
PurchasedItems.Clear();
|
||||
@@ -246,42 +254,21 @@ namespace Barotrauma
|
||||
continue;
|
||||
}
|
||||
availableContainers.Add(itemContainer);
|
||||
#if SERVER
|
||||
#if SERVER
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
Entity.Spawner.CreateNetworkEvent(itemContainer.Item, false);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (itemContainer == null)
|
||||
{
|
||||
//no container, place at the waypoint
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, position, wp.Submarine, onSpawned: itemSpawned);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
var item = new Item(pi.ItemPrefab, position, wp.Submarine);
|
||||
itemSpawned(item);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
//place in the container
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, itemContainer.Inventory, onSpawned: itemSpawned);
|
||||
}
|
||||
else
|
||||
{
|
||||
var item = new Item(pi.ItemPrefab, position, wp.Submarine);
|
||||
itemContainer.Inventory.TryPutItem(item, null);
|
||||
itemSpawned(item);
|
||||
}
|
||||
|
||||
var item = new Item(pi.ItemPrefab, position, wp.Submarine);
|
||||
itemContainer?.Inventory.TryPutItem(item, null);
|
||||
itemSpawned(item);
|
||||
#if SERVER
|
||||
Entity.Spawner?.CreateNetworkEvent(item, false);
|
||||
#endif
|
||||
static void itemSpawned(Item item)
|
||||
{
|
||||
Submarine sub = item.Submarine ?? item.GetRootContainer()?.Submarine;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -30,6 +31,8 @@ namespace Barotrauma
|
||||
|
||||
public ReadyCheck ActiveReadyCheck;
|
||||
|
||||
public XElement ActiveOrdersElement { get; set; }
|
||||
|
||||
public CrewManager(bool isSinglePlayer)
|
||||
{
|
||||
IsSinglePlayer = isSinglePlayer;
|
||||
@@ -111,6 +114,9 @@ namespace Barotrauma
|
||||
case "health":
|
||||
characterInfo.HealthData = subElement;
|
||||
break;
|
||||
case "orders":
|
||||
characterInfo.OrderData = subElement;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,7 +195,7 @@ namespace Barotrauma
|
||||
spawnWaypoints = WayPoint.WayPointList.FindAll(wp =>
|
||||
wp.SpawnType == SpawnType.Human &&
|
||||
wp.Submarine == Level.Loaded.StartOutpost &&
|
||||
wp.CurrentHull?.OutpostModuleTags != null &&
|
||||
wp.CurrentHull != null &&
|
||||
wp.CurrentHull.OutpostModuleTags.Contains("airlock"));
|
||||
while (spawnWaypoints.Count > characterInfos.Count)
|
||||
{
|
||||
@@ -229,10 +235,14 @@ namespace Barotrauma
|
||||
}
|
||||
if (character.Info.HealthData != null)
|
||||
{
|
||||
character.Info.ApplyHealthData(character, character.Info.HealthData);
|
||||
CharacterInfo.ApplyHealthData(character, character.Info.HealthData);
|
||||
}
|
||||
character.GiveIdCardTags(spawnWaypoints[i]);
|
||||
character.Info.StartItemsGiven = true;
|
||||
if (character.Info.OrderData != null)
|
||||
{
|
||||
character.Info.ApplyOrderData();
|
||||
}
|
||||
}
|
||||
|
||||
AddCharacter(character);
|
||||
@@ -265,6 +275,14 @@ namespace Barotrauma
|
||||
RemoveCharacterInfo(characterInfo);
|
||||
}
|
||||
|
||||
public void ClearCurrentOrders()
|
||||
{
|
||||
foreach (var characterInfo in characterInfos)
|
||||
{
|
||||
characterInfo?.ClearCurrentOrders();
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
foreach (Pair<Order, float?> order in ActiveOrders)
|
||||
@@ -392,6 +410,88 @@ namespace Barotrauma
|
||||
|
||||
#endregion
|
||||
|
||||
public static Character GetCharacterForQuickAssignment(Order order, Character controlledCharacter, IEnumerable<Character> characters, bool includeSelf = false)
|
||||
{
|
||||
bool isControlledCharacterNull = controlledCharacter == null;
|
||||
#if !DEBUG
|
||||
if (isControlledCharacterNull) { return null; }
|
||||
#endif
|
||||
if (order.Category == OrderCategory.Operate && HumanAIController.IsItemTargetedBySomeone(order.TargetItemComponent, controlledCharacter != null ? controlledCharacter.TeamID : CharacterTeamType.Team1, out Character operatingCharacter) &&
|
||||
(isControlledCharacterNull || operatingCharacter.CanHearCharacter(controlledCharacter)))
|
||||
{
|
||||
return operatingCharacter;
|
||||
}
|
||||
return GetCharactersSortedForOrder(order, characters, controlledCharacter, includeSelf).FirstOrDefault(c => isControlledCharacterNull || c.CanHearCharacter(controlledCharacter)) ?? controlledCharacter;
|
||||
}
|
||||
|
||||
public static IEnumerable<Character> GetCharactersSortedForOrder(Order order, IEnumerable<Character> characters, Character controlledCharacter, bool includeSelf, IEnumerable<Character> extraCharacters = null)
|
||||
{
|
||||
var filteredCharacters = characters.Where(c => controlledCharacter == null || ((includeSelf || c != controlledCharacter) && c.TeamID == controlledCharacter.TeamID));
|
||||
if (extraCharacters != null)
|
||||
{
|
||||
filteredCharacters = filteredCharacters.Union(extraCharacters);
|
||||
}
|
||||
return filteredCharacters
|
||||
// 1. Prioritize those who are on the same submarine than the controlled character
|
||||
.OrderByDescending(c => Character.Controlled == null || c.Submarine == Character.Controlled.Submarine)
|
||||
// 2. Prioritize those who have been given the same maintenance or operate order as now issued
|
||||
.ThenByDescending(c => c.CurrentOrders.Any(o =>
|
||||
o.Order != null && o.Order.Identifier == order.Identifier &&
|
||||
(order.Category == OrderCategory.Maintenance || order.Category == OrderCategory.Operate)))
|
||||
// 3. Prioritize those with the appropriate job for the order
|
||||
.ThenByDescending(c => order.HasAppropriateJob(c))
|
||||
// 4. Prioritize bots over player controlled characters
|
||||
.ThenByDescending(c => c.IsBot)
|
||||
// 5. Use the priority value of the current objective
|
||||
.ThenBy(c => c.AIController is HumanAIController humanAI ? humanAI.ObjectiveManager.CurrentObjective?.Priority : 0)
|
||||
// 6. Prioritize those with the best skill for the order
|
||||
.ThenByDescending(c => c.GetSkillLevel(order.AppropriateSkill));
|
||||
}
|
||||
|
||||
partial void UpdateProjectSpecific(float deltaTime);
|
||||
|
||||
private void SaveActiveOrders(XElement parentElement)
|
||||
{
|
||||
ActiveOrdersElement = new XElement("activeorders");
|
||||
// Only save orders with no fade out time (e.g. ignore orders)
|
||||
var ordersToSave = new List<OrderInfo>();
|
||||
foreach (var activeOrder in ActiveOrders)
|
||||
{
|
||||
var order = activeOrder?.First;
|
||||
if (order == null || activeOrder.Second.HasValue) { continue; }
|
||||
ordersToSave.Add(new OrderInfo(order, null, CharacterInfo.HighestManualOrderPriority));
|
||||
}
|
||||
CharacterInfo.SaveOrders(ActiveOrdersElement, ordersToSave.ToArray());
|
||||
parentElement?.Add(ActiveOrdersElement);
|
||||
}
|
||||
|
||||
public void LoadActiveOrders()
|
||||
{
|
||||
if (ActiveOrdersElement == null) { return; }
|
||||
foreach (var orderInfo in CharacterInfo.LoadOrders(ActiveOrdersElement))
|
||||
{
|
||||
IIgnorable ignoreTarget = null;
|
||||
if (orderInfo.Order.IsIgnoreOrder)
|
||||
{
|
||||
switch (orderInfo.Order.TargetType)
|
||||
{
|
||||
case Order.OrderTargetType.Entity:
|
||||
ignoreTarget = orderInfo.Order.TargetEntity as IIgnorable;
|
||||
break;
|
||||
case Order.OrderTargetType.WallSection when orderInfo.Order.TargetEntity is Structure s && orderInfo.Order.WallSectionIndex.HasValue:
|
||||
ignoreTarget = s.GetSection(orderInfo.Order.WallSectionIndex.Value) as IIgnorable;
|
||||
break;
|
||||
default:
|
||||
DebugConsole.ThrowError("Error loading an ignore order - can't find a proper ignore target");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (ignoreTarget != null)
|
||||
{
|
||||
ignoreTarget.OrderedToBeIgnored = true;
|
||||
}
|
||||
AddOrder(orderInfo.Order, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,25 +17,33 @@ namespace Barotrauma
|
||||
// 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 int MaxMissionCount { get; set; }
|
||||
|
||||
public const int DefaultMaxMissionCount = 2;
|
||||
public const int MaxMissionCountLimit = 10;
|
||||
public const int MinMissionCountLimit = 1;
|
||||
|
||||
public CampaignSettings(IReadMessage inc)
|
||||
{
|
||||
RadiationEnabled = inc.ReadBoolean();
|
||||
MaxMissionCount = inc.ReadInt32();
|
||||
}
|
||||
|
||||
public CampaignSettings(XElement element)
|
||||
{
|
||||
RadiationEnabled = element.GetAttributeBool(nameof(RadiationEnabled).ToLower(), true);
|
||||
MaxMissionCount = element.GetAttributeInt(nameof(MaxMissionCount).ToLower(), DefaultMaxMissionCount);
|
||||
}
|
||||
|
||||
public void Serialize(IWriteMessage msg)
|
||||
{
|
||||
msg.Write(RadiationEnabled);
|
||||
msg.Write(MaxMissionCount);
|
||||
}
|
||||
|
||||
public XElement Save()
|
||||
{
|
||||
return new XElement(nameof(CampaignSettings), new XAttribute(nameof(RadiationEnabled).ToLower(), RadiationEnabled));
|
||||
return new XElement(nameof(CampaignSettings), new XAttribute(nameof(RadiationEnabled).ToLower(), RadiationEnabled), new XAttribute(nameof(MaxMissionCount).ToLower().ToLower(), MaxMissionCount));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +57,9 @@ namespace Barotrauma
|
||||
//duration of the camera transition at the end of a round
|
||||
protected const float EndTransitionDuration = 5.0f;
|
||||
//there can be no events before this time has passed during the 1st campaign round
|
||||
const float FirstRoundEventDelay = 30.0f;
|
||||
const float FirstRoundEventDelay = 0.0f;
|
||||
|
||||
public enum InteractionType { None, Talk, Map, Crew, Store, Repair, Upgrade, PurchaseSub }
|
||||
public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Repair, Upgrade, PurchaseSub }
|
||||
|
||||
public readonly CargoManager CargoManager;
|
||||
public UpgradeManager UpgradeManager;
|
||||
@@ -113,12 +121,15 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Map.CurrentLocation?.SelectedMission != null)
|
||||
if (Map.CurrentLocation != null)
|
||||
{
|
||||
if (Map.CurrentLocation.SelectedMission.Locations[0] == Map.CurrentLocation.SelectedMission.Locations[1] ||
|
||||
Map.CurrentLocation.SelectedMission.Locations.Contains(Map.SelectedLocation))
|
||||
foreach (Mission mission in map.CurrentLocation.SelectedMissions)
|
||||
{
|
||||
yield return Map.CurrentLocation.SelectedMission;
|
||||
if (mission.Locations[0] == mission.Locations[1] ||
|
||||
mission.Locations.Contains(Map.SelectedLocation))
|
||||
{
|
||||
yield return mission;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (Mission mission in extraMissions)
|
||||
@@ -169,7 +180,7 @@ namespace Barotrauma
|
||||
return Submarine.Loaded.FindAll(sub =>
|
||||
sub != leavingSub &&
|
||||
!leavingSub.DockedTo.Contains(sub) &&
|
||||
sub.Info.Type == SubmarineType.Player &&
|
||||
sub.Info.Type == SubmarineType.Player && sub.TeamID == CharacterTeamType.Team1 && // pirate subs are currently tagged as player subs as well
|
||||
sub != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle &&
|
||||
(sub.AtEndExit != leavingSub.AtEndExit || sub.AtStartExit != leavingSub.AtStartExit));
|
||||
}
|
||||
@@ -240,23 +251,24 @@ namespace Barotrauma
|
||||
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())
|
||||
foreach (var availableMission in currentLocation.AvailableMissions)
|
||||
{
|
||||
currentLocation.SelectedMission = availableMissionsInLocation.FirstOrDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
currentLocation.SelectedMission = null;
|
||||
if (availableMission.Locations[0] == currentLocation && availableMission.Locations[1] == currentLocation)
|
||||
{
|
||||
currentLocation.SelectMission(availableMission);
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
foreach (Mission mission in currentLocation.SelectedMissions.ToList())
|
||||
{
|
||||
currentLocation.SelectedMission = null;
|
||||
//if we had selected a mission that takes place in the outpost, deselect it when leaving the outpost
|
||||
if (mission.Locations[0] == currentLocation &&
|
||||
mission.Locations[1] == currentLocation)
|
||||
{
|
||||
currentLocation.DeselectMission(mission);
|
||||
}
|
||||
}
|
||||
|
||||
if (levelData.HasBeaconStation && !levelData.IsBeaconActive)
|
||||
@@ -268,7 +280,7 @@ namespace Barotrauma
|
||||
var beaconMissionPrefab = ToolBox.SelectWeightedRandom(beaconMissionPrefabs, beaconMissionPrefabs.Select(p => (float)p.Commonness).ToList(), rand);
|
||||
if (!Missions.Any(m => m.Prefab.Type == beaconMissionPrefab.Type))
|
||||
{
|
||||
extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations));
|
||||
extraMissions.Add(beaconMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,10 +294,10 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
|
||||
var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(huntingGroundsMissionPrefabs, huntingGroundsMissionPrefabs.Select(p => (float)p.Commonness).ToList(), rand);
|
||||
var huntingGroundsMissionPrefab = ToolBox.SelectWeightedRandom(huntingGroundsMissionPrefabs, huntingGroundsMissionPrefabs.Select(p => (float)Math.Max(p.Commonness, 0.1f)).ToList(), rand);
|
||||
if (!Missions.Any(m => m.Prefab.Tags.Any(t => t.Equals("huntinggrounds", StringComparison.OrdinalIgnoreCase))))
|
||||
{
|
||||
extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations));
|
||||
extraMissions.Add(huntingGroundsMissionPrefab.Instantiate(Map.SelectedConnection.Locations, Submarine.MainSub));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -490,7 +502,7 @@ namespace Barotrauma
|
||||
if (Level.Loaded.StartOutpost.DockedTo.Any())
|
||||
{
|
||||
var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault();
|
||||
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { return null; }
|
||||
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; }
|
||||
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
|
||||
}
|
||||
|
||||
@@ -518,7 +530,7 @@ namespace Barotrauma
|
||||
if (Level.Loaded.EndOutpost.DockedTo.Any())
|
||||
{
|
||||
var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault();
|
||||
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle) { return null; }
|
||||
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; }
|
||||
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
|
||||
}
|
||||
|
||||
@@ -554,12 +566,14 @@ namespace Barotrauma
|
||||
{
|
||||
CargoManager.ClearItemsInBuyCrate();
|
||||
CargoManager.ClearItemsInSellCrate();
|
||||
CargoManager.ClearItemsInSellFromSubCrate();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
CargoManager?.ClearItemsInBuyCrate();
|
||||
// TODO: CargoManager?.ClearItemsInSellFromSubCrate();
|
||||
}
|
||||
else if (GameMain.NetworkMember.IsClient)
|
||||
{
|
||||
@@ -585,7 +599,7 @@ namespace Barotrauma
|
||||
if (c.IsDead)
|
||||
{
|
||||
CrewManager.RemoveCharacterInfo(c.Info);
|
||||
c.DespawnNow();
|
||||
c.DespawnNow(createNetworkEvents: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,7 +609,6 @@ namespace Barotrauma
|
||||
{
|
||||
CrewManager.RemoveCharacterInfo(ci);
|
||||
}
|
||||
ci?.ClearCurrentOrders();
|
||||
}
|
||||
|
||||
foreach (DockingPort port in DockingPort.List)
|
||||
@@ -637,6 +650,7 @@ namespace Barotrauma
|
||||
location.CreateStore(force: true);
|
||||
location.ClearMissions();
|
||||
location.Discovered = false;
|
||||
location.LevelData?.EventHistory?.Clear();
|
||||
}
|
||||
Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation));
|
||||
Map.SelectLocation(-1);
|
||||
@@ -851,11 +865,14 @@ namespace Barotrauma
|
||||
DebugConsole.NewMessage(" " + i + ". " + destination.Name, Color.White);
|
||||
}
|
||||
}
|
||||
|
||||
if (map.CurrentLocation?.SelectedMission != null)
|
||||
|
||||
if (map.CurrentLocation != null)
|
||||
{
|
||||
DebugConsole.NewMessage(" Selected mission: " + map.CurrentLocation.SelectedMission.Name, Color.White);
|
||||
DebugConsole.NewMessage("\n" + map.CurrentLocation.SelectedMission.Description, Color.White);
|
||||
foreach (Mission mission in map.CurrentLocation.SelectedMissions)
|
||||
{
|
||||
DebugConsole.NewMessage(" Selected mission: " + mission.Name, Color.White);
|
||||
DebugConsole.NewMessage("\n" + mission.Description, Color.White);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -865,5 +882,25 @@ namespace Barotrauma
|
||||
map?.Remove();
|
||||
map = null;
|
||||
}
|
||||
|
||||
public int NumberOfMissionsAtLocation(Location location)
|
||||
{
|
||||
return Map.CurrentLocation.SelectedMissions.Count(m => m.Locations.Contains(location));
|
||||
}
|
||||
|
||||
public void CheckTooManyMissions(Location currentLocation, Client sender)
|
||||
{
|
||||
foreach (Location location in currentLocation.Connections.Select(c => c.OtherLocation(currentLocation)))
|
||||
{
|
||||
if (NumberOfMissionsAtLocation(location) > Settings.MaxMissionCount)
|
||||
{
|
||||
DebugConsole.AddWarning($"Client {sender.Name} had too many missions selected for location {location.Name}! Count was {NumberOfMissionsAtLocation(location)}. Deselecting extra missions.");
|
||||
foreach (Mission mission in currentLocation.SelectedMissions.Where(m => m.Locations[1] == location).Skip(Settings.MaxMissionCount).ToList())
|
||||
{
|
||||
currentLocation.DeselectMission(mission);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -26,6 +26,7 @@ namespace Barotrauma
|
||||
|
||||
private XElement itemData;
|
||||
private XElement healthData;
|
||||
public XElement OrderData { get; private set; }
|
||||
|
||||
partial void InitProjSpecific(Client client);
|
||||
public CharacterCampaignData(Client client)
|
||||
@@ -38,8 +39,10 @@ namespace Barotrauma
|
||||
if (client.Character.Inventory != null)
|
||||
{
|
||||
itemData = new XElement("inventory");
|
||||
client.Character.SaveInventory(client.Character.Inventory, itemData);
|
||||
Character.SaveInventory(client.Character.Inventory, itemData);
|
||||
}
|
||||
OrderData = new XElement("orders");
|
||||
CharacterInfo.SaveOrderData(client.Character.Info, OrderData);
|
||||
}
|
||||
|
||||
public CharacterCampaignData(XElement element)
|
||||
@@ -67,6 +70,9 @@ namespace Barotrauma
|
||||
case "health":
|
||||
healthData = subElement;
|
||||
break;
|
||||
case "orders":
|
||||
OrderData = subElement;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,8 +84,10 @@ namespace Barotrauma
|
||||
if (character.Inventory != null)
|
||||
{
|
||||
itemData = new XElement("inventory");
|
||||
character.SaveInventory(character.Inventory, itemData);
|
||||
Character.SaveInventory(character.Inventory, itemData);
|
||||
}
|
||||
OrderData = new XElement("orders");
|
||||
CharacterInfo.SaveOrderData(character.Info, OrderData);
|
||||
}
|
||||
|
||||
public XElement Save()
|
||||
@@ -92,6 +100,7 @@ namespace Barotrauma
|
||||
CharacterInfo?.Save(element);
|
||||
if (itemData != null) { element.Add(itemData); }
|
||||
if (healthData != null) { element.Add(healthData); }
|
||||
if (OrderData != null) { element.Add(OrderData); }
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user