(f0d812055) v0.9.9.0

This commit is contained in:
Joonas Rikkonen
2020-04-23 19:19:37 +03:00
parent b647059b93
commit ac37a3b0e4
391 changed files with 15054 additions and 5420 deletions
@@ -177,7 +177,8 @@ namespace Barotrauma
StaticSight = true;
}
SonarDisruption = element.GetAttributeFloat("sonardisruption", 0.0f);
SonarLabel = element.GetAttributeString("sonarlabel", "");
string label = element.GetAttributeString("sonarlabel", "");
SonarLabel = TextManager.Get(label, returnNull: true) ?? label;
SonarIconIdentifier = element.GetAttributeString("sonaricon", "");
string typeString = element.GetAttributeString("type", "Any");
if (Enum.TryParse(typeString, out TargetType t))
@@ -36,14 +36,16 @@ namespace Barotrauma
private readonly float updateTargetsInterval = 1;
private readonly float updateMemoriesInverval = 1;
private readonly float attackLimbResetInterval = 2;
private float avoidLookAheadDistance;
private readonly float avoidLookAheadDistance;
private SteeringManager outsideSteering, insideSteering;
private float updateTargetsTimer;
private float updateMemoriesTimer;
private float attackLimbResetTimer;
private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0;
public float CombatStrength => Character.Params.AI.CombatStrength;
private float Sight => Character.Params.AI.Sight;
@@ -63,6 +65,7 @@ namespace Barotrauma
get { return _attackingLimb; }
private set
{
attackLimbResetTimer = 0;
_attackingLimb = value;
attackVector = null;
Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse;
@@ -376,7 +379,7 @@ namespace Barotrauma
return;
}
float squaredDistance = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition);
var attackLimb = GetAttackLimb(SelectedAiTarget.WorldPosition);
var attackLimb = AttackingLimb ?? GetAttackLimb(SelectedAiTarget.WorldPosition);
if (attackLimb != null && squaredDistance <= Math.Pow(attackLimb.attack.Range, 2))
{
run = true;
@@ -644,10 +647,14 @@ namespace Barotrauma
var pickable = item.GetComponent<Pickable>();
if (pickable != null)
{
var target = pickable.Picker?.AiTarget;
if (target?.Entity != null && !target.Entity.Removed)
Entity owner = pickable.Picker ?? item.ParentInventory?.Owner;
if (owner != null)
{
SelectedAiTarget = target;
var target = owner.AiTarget;
if (target?.Entity != null && !target.Entity.Removed)
{
SelectedAiTarget = target;
}
}
}
}
@@ -685,7 +692,7 @@ namespace Barotrauma
WallSection section = wall.Sections[i];
if (CanPassThroughHole(wall, i) && section?.gap != null)
{
if (SteerThroughGap(wall, section, section.gap.WorldPosition, deltaTime))
if (SteerThroughGap(wall, section, wall.SectionPosition(i, true), deltaTime))
{
return;
}
@@ -980,7 +987,15 @@ namespace Barotrauma
{
// If not, reset the attacking limb, if the cooldown is not running
// Don't use the property, because we don't want cancel reversing, if we are reversing.
_attackingLimb = null;
if (attackLimbResetTimer > attackLimbResetInterval)
{
_attackingLimb = null;
attackLimbResetTimer = 0;
}
else
{
attackLimbResetTimer += deltaTime;
}
}
}
Limb steeringLimb = canAttack ? AttackingLimb : null;
@@ -1045,7 +1060,8 @@ namespace Barotrauma
}
}
// Steer towards the target if in the same room and swimming
if ((Character.AnimController.InWater || pursue) && targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull))
if ((Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) &&
(targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull) || Character.CanSeeTarget(SelectedAiTarget.Entity)))
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - steeringLimb.SimPosition));
}
@@ -1112,31 +1128,13 @@ namespace Barotrauma
return false;
}
private bool CanAttack(Entity target)
{
if (target == null) { return false; }
if (target is Character c)
{
if (Character.CurrentHull == null && c.CurrentHull != null || Character.CurrentHull != null && c.CurrentHull == null)
{
return false;
}
}
else if (target is Item i && i.GetComponent<Door>() == null)
{
if (Character.CurrentHull == null && i.CurrentHull != null || Character.CurrentHull != null && i.CurrentHull == null)
{
return false;
}
}
return true;
}
private readonly List<Limb> attackLimbs = new List<Limb>();
private readonly List<float> weights = new List<float>();
private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null)
{
var currentContexts = Character.GetAttackContexts();
Entity target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity;
if (!CanAttack(target)) { return null; }
if (target == null) { return null; }
Limb selectedLimb = null;
float currentPriority = -1;
foreach (Limb limb in Character.AnimController.Limbs)
@@ -1153,12 +1151,26 @@ namespace Barotrauma
if (attack.Conditionals.Any(c => !c.Matches(se))) { continue; }
}
if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { continue; }
float priority = CalculatePriority(limb, attackWorldPos);
if (priority > currentPriority)
if (AIParams.RandomAttack)
{
currentPriority = priority;
selectedLimb = limb;
attackLimbs.Add(limb);
weights.Add(limb.attack.Priority);
}
else
{
float priority = CalculatePriority(limb, attackWorldPos);
if (priority > currentPriority)
{
currentPriority = priority;
selectedLimb = limb;
}
}
}
if (AIParams.RandomAttack)
{
selectedLimb = ToolBox.SelectWeightedRandom(attackLimbs, weights, Rand.RandSync.Server);
attackLimbs.Clear();
weights.Clear();
}
return selectedLimb;
@@ -1176,18 +1188,22 @@ namespace Barotrauma
{
wallTarget = null;
if (SelectedAiTarget == null) { return; }
if (SelectedAiTarget.Entity == null) { return; }
//check if there's a wall between the target and the Character
Vector2 rayStart = SimPosition;
Vector2 rayEnd = SelectedAiTarget.SimPosition;
bool offset = SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null;
if (offset)
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
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)
if (closestBody.UserData is Structure wall && wall.Submarine != null && (wall.Submarine.Info.IsPlayer || wall.Submarine.Info.IsOutpost && TargetOutposts))
{
int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition));
float sectionDamage = wall.SectionDamage(sectionIndex);
@@ -1265,20 +1281,26 @@ namespace Barotrauma
Character.AnimController.ReleaseStuckLimbs();
LatchOntoAI?.DeattachFromBody();
if (attacker == null || attacker.AiTarget == null) { return; }
bool isFriendly = attacker.SpeciesName == Character.SpeciesName || attacker.Params.Group == Character.Params.Group;
if (wasLatched)
{
avoidTimer = avoidTime * Rand.Range(0.75f, 1.25f);
SelectTarget(attacker.AiTarget);
if (!isFriendly)
{
SelectTarget(attacker.AiTarget);
}
return;
}
if (State == AIState.Flee)
{
SelectTarget(attacker.AiTarget);
if (!isFriendly)
{
SelectTarget(attacker.AiTarget);
}
return;
}
if (attackResult.Damage > 0.0f)
if (!isFriendly && attackResult.Damage > 0.0f)
{
bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackSub;
if (Character.Params.AI.AttackWhenProvoked && canAttack)
@@ -1325,13 +1347,20 @@ namespace Barotrauma
ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100);
}
}
else if (canAttack && attacker.IsHuman && AIParams.TryGetTarget(attacker.SpeciesName, out CharacterParams.TargetParams targetingParams))
{
if (targetingParams.State == AIState.Aggressive)
{
ChangeTargetState(attacker, AIState.Attack, 100);
}
}
}
AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget);
targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AggressionHurt;
// Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown
bool retaliate = SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine;
bool retaliate = !isFriendly && SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine;
bool avoidGunFire = Character.Params.AI.AvoidGunfire && attacker.Submarine != Character.Submarine;
if (State == AIState.Attack && !IsCoolDownRunning)
@@ -1376,6 +1405,8 @@ namespace Barotrauma
IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable;
if (damageTarget != null)
{
//simulate attack input to get the character to attack client-side
Character.SetInput(InputType.Attack, true, true);
if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
{
if (damageTarget.Health > 0)
@@ -1519,7 +1550,7 @@ namespace Barotrauma
if (aiTarget.Type == AITarget.TargetType.HumanOnly) { continue; }
if (!TargetOutposts)
{
if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.IsOutpost) { continue; }
if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsOutpost) { continue; }
}
Character targetCharacter = aiTarget.Entity as Character;
//ignore the aitarget if it is the Character itself
@@ -1529,30 +1560,6 @@ namespace Barotrauma
string targetingTag = null;
if (targetCharacter != null)
{
if (targetCharacter.Submarine != Character.Submarine)
{
// In a different sub or the target is outside when we are inside or vice versa.
// Make an exception for humans so that creatures cannot avoid/flee from humans that are inside a sub.
if (!targetCharacter.IsHuman && State == AIState.Avoid && State == AIState.Escape & State == AIState.Flee)
{
// If we are escaping, let's not ignore the target entirely, because there can be a gaps where we or they can go freely
if (targetCharacter.Submarine != null)
{
// Target is inside -> reduce the priority
valueModifier *= 0.5f;
if (Character.Submarine != null)
{
// Both inside different submarines -> can ignore safely
continue;
}
}
}
else
{
// Don't attack targets that are not in the same submarine
continue;
}
}
if (targetCharacter.IsDead)
{
targetingTag = "dead";
@@ -1604,9 +1611,11 @@ namespace Barotrauma
}
else if (aiTarget.Entity != null)
{
// Ignore all structures and items inside wrecks
if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsWreck) { continue; }
// Ignore the target if it's a room and the character is already inside a sub
if (character.CurrentHull != null && aiTarget.Entity is Hull) { continue; }
Door door = null;
if (aiTarget.Entity is Item item)
{
@@ -1644,15 +1653,13 @@ namespace Barotrauma
{
continue;
}
if (character.CurrentHull != null)
bool isCharacterOutside = s.Submarine == null || character.CurrentHull == null;
bool targetInnerWalls = AIParams.TargetInnerWalls;
if (!isCharacterOutside && !targetInnerWalls)
{
// Ignore walls when inside (walltargets still work)
continue;
}
if (s.Submarine == null)
{
continue;
}
valueModifier = 1;
if (!Character.AnimController.CanEnterSubmarine && IsWallDisabled(s))
{
@@ -1665,28 +1672,43 @@ namespace Barotrauma
bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null;
if (Character.AnimController.CanEnterSubmarine)
{
if (CanPassThroughHole(s, i))
if (isCharacterOutside)
{
valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : 0;
if (CanPassThroughHole(s, i))
{
valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : (targetInnerWalls ? 1 : 0);
}
else
{
// Ignore holes that cannot be passed through if cannot attack items/structures. Holes that are big enough should be targeted, so that we can get in
if (!canAttackSub)
{
valueModifier = 0;
break;
}
if (AggressiveBoarding && leadsInside)
{
// Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside
valueModifier *= 1 + section.gap.Open;
}
}
}
else
else if (!canAttackSub || CanPassThroughHole(s, i))
{
// Ignore holes that cannot be passed through if cannot attack items/structures. Holes that are big enough should be targeted, so that we can get in
if (!canAttackSub)
{
valueModifier = 0;
break;
}
if (AggressiveBoarding)
{
// Up to 100% priority increase for every gap in the wall
valueModifier *= 1 + section.gap.Open;
}
// Already inside -> ignore holes in the walls and ignore walls if cannot attack the sub.
valueModifier = 0;
break;
}
else if (canAttackSub && !AggressiveBoarding)
{
// We are actually interested in breaking things -> reduce the priority when the wall is already broken
valueModifier *= 1 - section.gap.Open * 0.25f;
}
}
else if (!leadsInside)
else if (!leadsInside || !canAttackSub)
{
// Ignore inner walls
// Can't get in, ignore inner walls
// Also ignore all walls if cannot attack the sub
valueModifier = 0;
break;
}
@@ -1774,6 +1796,50 @@ namespace Barotrauma
// Don't target items that we own.
// This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive)
if (aiTarget.Entity is Item i && i.IsOwnedBy(character)) { continue; }
if (targetCharacter != null)
{
if (targetCharacter.Submarine != Character.Submarine)
{
if (targetCharacter.Submarine != null)
{
// Target is inside -> reduce the priority
valueModifier *= 0.5f;
if (Character.Submarine != null)
{
// Both inside different submarines -> can ignore safely
continue;
}
}
else if (Character.CurrentHull != null)
{
// Target outside, but we are inside -> Check if we can get to the target.
// Only check if we are not already targeting the character.
// If we are, keep the target (unless we choose another).
if (SelectedAiTarget?.Entity != targetCharacter)
{
foreach (var gap in Character.CurrentHull.ConnectedGaps)
{
var door = gap.ConnectedDoor;
if (door == null || !door.IsOpen)
{
var wall = gap.ConnectedWall;
if (wall != null)
{
for (int j = 0; j < wall.Sections.Length; j++)
{
WallSection section = wall.Sections[j];
if (!CanPassThroughHole(wall, j) && section?.gap != null)
{
continue;
}
}
}
}
}
}
}
}
}
newTarget = aiTarget;
selectedTargetMemory = targetMemory;
targetValue = valueModifier;
@@ -74,7 +74,7 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
if (DisableCrewAI || Character.IsUnconscious || Character.Removed) { return; }
if (DisableCrewAI || Character.IsIncapacitated || Character.Removed) { return; }
base.Update(deltaTime);
if (unreachableClearTimer > 0)
@@ -139,7 +139,10 @@ namespace Barotrauma
}
if (Character.SpeechImpediment < 100.0f)
{
ReportProblems();
if (Character.Submarine != null && Character.Submarine.TeamID == Character.TeamID && !Character.Submarine.Info.IsWreck)
{
ReportProblems();
}
UpdateSpeaking();
}
UnequipUnnecessaryItems();
@@ -1049,12 +1052,59 @@ 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)
{
operatingCharacter = null;
if (target?.Item == null) { return false; }
foreach (var c in Character.CharacterList)
{
if (character != null && c == character) { continue; }
if (character?.AIController is HumanAIController humanAi && !humanAi.IsFriendly(c)) { continue; }
if (c.SelectedConstruction != target.Item) { continue; }
operatingCharacter = c;
// If the other character is player, don't try to operate
if (c.IsRemotePlayer || Character.Controlled == c) { return true; }
if (c.AIController is HumanAIController controllingHumanAi)
{
// If the other character is ordered to operate the item, let him do it
if (controllingHumanAi.ObjectiveManager.IsCurrentOrder<AIObjectiveOperateItem>())
{
return true;
}
else
{
if (character == null)
{
return true;
}
else if (target is Steering)
{
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
return character.GetSkillLevel("helm") <= c.GetSkillLevel("helm");
}
else
{
return target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c);
}
}
}
else
{
// Shouldn't go here, unless we allow non-humans to operate items
return false;
}
}
return false;
}
#region Wrappers
public bool IsFriendly(Character other) => IsFriendly(Character, other);
public void DoForEachCrewMember(Action<HumanAIController> action) => DoForEachCrewMember(Character, action);
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
}
}
@@ -136,9 +136,10 @@ namespace Barotrauma
string allowedJobsStr = element.GetAttributeString("allowedjobs", "");
foreach (string allowedJobIdentifier in allowedJobsStr.Split(','))
{
if (JobPrefab.Prefabs.ContainsKey(allowedJobIdentifier.ToLowerInvariant()))
string key = allowedJobIdentifier.ToLowerInvariant();
if (JobPrefab.Prefabs.ContainsKey(key))
{
AllowedJobs.Add(JobPrefab.Prefabs[allowedJobIdentifier.ToLowerInvariant()]);
AllowedJobs.Add(JobPrefab.Prefabs[key]);
}
}
@@ -30,6 +30,7 @@ namespace Barotrauma
public virtual bool KeepDivingGearOn => false;
public virtual bool UnequipItems => false;
public virtual bool AllowOutsideSubmarine => false;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
private float _cumulatedDevotion;
@@ -182,11 +183,18 @@ namespace Barotrauma
}
}
protected bool IsAllowed => AllowOutsideSubmarine || character.Submarine != null && character.Submarine.TeamID == character.TeamID && character.Submarine.Info.IsPlayer;
/// <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()
{
if (!IsAllowed)
{
Priority = 0;
return Priority;
}
if (objectiveManager.CurrentOrder == this)
{
Priority = AIObjectiveManager.OrderPriority;
@@ -23,8 +23,12 @@ namespace Barotrauma
if (item.Submarine == null) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine != null)
{
if (item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; }
}
if (item.ConditionPercentage <= 0) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; }
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (IsReady(battery)) { return false; }
return true;
@@ -13,6 +13,7 @@ namespace Barotrauma
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
public override bool AllowOutsideSubmarine => true;
private readonly CombatMode initialMode;
@@ -149,12 +150,12 @@ namespace Barotrauma
}
if (!HoldPosition && seekAmmunition == null)
{
Move();
Move(deltaTime);
}
}
}
private void Move()
private void Move(float deltaTime)
{
switch (Mode)
{
@@ -163,7 +164,7 @@ namespace Barotrauma
break;
case CombatMode.Defensive:
case CombatMode.Retreat:
Retreat();
Retreat(deltaTime);
break;
default:
throw new NotImplementedException();
@@ -411,7 +412,10 @@ namespace Barotrauma
return true;
}
private void Retreat()
private float findHullTimer;
private readonly float findHullInterval = 1.0f;
private void Retreat(float deltaTime)
{
RemoveSubObjective(ref followTargetObjective);
RemoveSubObjective(ref seekAmmunition);
@@ -421,7 +425,15 @@ namespace Barotrauma
}
if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted))
{
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls);
if (findHullTimer > 0)
{
findHullTimer -= deltaTime;
}
else
{
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls);
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
}
}
if (retreatTarget != null && character.CurrentHull != retreatTarget)
{
@@ -27,6 +27,11 @@ namespace Barotrauma
public override float GetPriority()
{
if (!IsAllowed)
{
Priority = 0;
return Priority;
}
if (!objectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>()
&& Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
@@ -32,7 +32,11 @@ namespace Barotrauma
if (hull.FireSources.None()) { return false; }
if (hull.Submarine == null) { return false; }
if (hull.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { return false; }
if (character.Submarine != null)
{
if (hull.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(hull, true)) { return false; }
}
return true;
}
}
@@ -40,7 +40,11 @@ namespace Barotrauma
if (target.Submarine == null) { return false; }
if (target.Submarine.TeamID != character.TeamID) { return false; }
if (target.CurrentHull == null) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
if (character.Submarine != null)
{
if (target.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
}
return true;
}
}
@@ -12,6 +12,7 @@ namespace Barotrauma
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
public override bool ConcurrentObjectives => true;
public override bool AllowOutsideSubmarine => true;
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
// TODO: expose?
@@ -35,6 +36,11 @@ namespace Barotrauma
public override float GetPriority()
{
if (!IsAllowed)
{
Priority = 0;
return Priority;
}
if (character.CurrentHull == null)
{
Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo ? 0 : 100;
@@ -77,7 +83,7 @@ namespace Barotrauma
else
{
float dangerFactor = (100 - currenthullSafety) / 100;
Priority += dangerFactor * priorityIncrease * deltaTime;
Priority = Math.Min(Priority + dangerFactor * priorityIncrease * deltaTime, 100);
}
}
}
@@ -100,7 +106,7 @@ namespace Barotrauma
if (needsEquipment && divingGearObjective == null && !character.LockHands)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref divingGearObjective,
TryAddSubObjective(ref divingGearObjective,
constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () =>
{
@@ -139,14 +145,14 @@ namespace Barotrauma
{
RemoveSubObjective(ref goToObjective);
}
TryAddSubObjective(ref goToObjective,
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
{
AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50)
},
onCompleted: () =>
{
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
HumanAIController.NeedsDivingGear(character, currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character)))
{
resetPriority = true;
@@ -244,10 +250,8 @@ namespace Barotrauma
//(no need to do the expensive pathfinding if we already know we're not going to choose this hull)
if (hullSafety < bestValue) { continue; }
// Don't allow to go outside if not already outside.
var path = character.CurrentHull != null ?
PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, nodeFilter: node => node.Waypoint.CurrentHull != null) :
PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition);
if (path.Unreachable && character.CurrentHull != null)
var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
{
HumanAIController.UnreachableHulls.Add(hull);
continue;
@@ -265,7 +269,7 @@ namespace Barotrauma
else
{
// Outside
if (hull.RoomName != null && hull.RoomName.ToLowerInvariant().Contains("airlock"))
if (hull.RoomName != null && hull.RoomName.Contains("airlock", StringComparison.OrdinalIgnoreCase))
{
hullSafety = 100;
}
@@ -287,6 +291,7 @@ namespace Barotrauma
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance));
hullSafety *= distanceFactor;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
// Intentionally exclude wrecks from this check
if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != Character.TeamType.FriendlyNPC)
{
hullSafety /= 10;
@@ -1,9 +1,9 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -20,15 +20,23 @@ namespace Barotrauma
private AIObjectiveGoTo gotoObjective;
private AIObjectiveOperateItem operateObjective;
public AIObjectiveFixLeak(Gap leak, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base (character, objectiveManager, priorityModifier)
public bool IgnoreSeverityAndDistance { get; private set; }
public AIObjectiveFixLeak(Gap leak, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool ignoreSeverityAndDistance = false) : base (character, objectiveManager, priorityModifier)
{
Leak = leak;
IgnoreSeverityAndDistance = ignoreSeverityAndDistance;
}
protected override bool Check() => Leak.Open <= 0 || Leak.Removed;
public override float GetPriority()
{
if (!IsAllowed)
{
Priority = 0;
return Priority;
}
if (Leak.Removed || Leak.Open <= 0)
{
Priority = 0;
@@ -39,8 +47,8 @@ namespace Barotrauma
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f));
float severity = AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float distanceFactor = IgnoreSeverityAndDistance || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f));
float severity = IgnoreSeverityAndDistance ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90);
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
@@ -1,7 +1,5 @@
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
using System.Collections.Generic;
namespace Barotrauma
@@ -11,8 +9,12 @@ namespace Barotrauma
public override string DebugTag => "fix leaks";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
private Hull PrioritizedHull { get; set; }
public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Hull prioritizedHull = null) : base(character, objectiveManager, priorityModifier)
{
PrioritizedHull = prioritizedHull;
}
protected override bool Filter(Gap gap) => IsValidTarget(gap, character);
@@ -60,7 +62,7 @@ namespace Barotrauma
protected override IEnumerable<Gap> GetList() => Gap.GapList;
protected override AIObjective ObjectiveConstructor(Gap gap)
=> new AIObjectiveFixLeak(gap, character, objectiveManager, PriorityModifier);
=> new AIObjectiveFixLeak(gap, character, objectiveManager, priorityModifier: PriorityModifier, ignoreSeverityAndDistance: gap.FlowTargetHull == PrioritizedHull);
protected override void OnObjectiveCompleted(AIObjective objective, Gap target)
=> HumanAIController.RemoveTargets<AIObjectiveFixLeaks, Gap>(character, target);
@@ -71,7 +73,11 @@ namespace Barotrauma
if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; }
if (gap.Submarine == null) { return false; }
if (gap.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(gap, true)) { return false; }
if (character.Submarine != null)
{
if (gap.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(gap, true)) { return false; }
}
return true;
}
}
@@ -220,7 +220,11 @@ namespace Barotrauma
{
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
}
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
if (character.Submarine != null)
{
if (item.Submarine.Info.Type != character.Submarine.Info.Type) { continue; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
}
if (character.IsItemTakenBySomeoneElse(item)) { continue; }
float itemPriority = 1;
if (GetItemPriority != null)
@@ -276,6 +280,7 @@ namespace Barotrauma
private bool CheckItem(Item item)
{
if (item.NonInteractable) { return false; }
if (ignoredItems.Contains(item)) { return false; };
if (item.Condition < TargetCondition) { return false; }
if (ItemFilter != null && !ItemFilter(item)) { return false; }
@@ -44,6 +44,9 @@ namespace Barotrauma
public bool AllowGoingOutside { get; set; }
public override bool AbandonWhenCannotCompleteSubjectives => !repeat;
public override bool AllowOutsideSubmarine => AllowGoingOutside;
public string DialogueIdentifier { get; set; }
public string TargetName { get; set; }
@@ -65,13 +68,13 @@ namespace Barotrauma
}
else
{
return base.GetPriority();
Priority = objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : 10;
}
return Priority;
}
public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0)
: base (character, objectiveManager, priorityModifier)
public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0)
: base(character, objectiveManager, priorityModifier)
{
this.Target = target;
this.repeat = repeat;
@@ -204,7 +207,7 @@ namespace Barotrauma
}
if (needsEquipment)
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
@@ -11,6 +11,7 @@ namespace Barotrauma
{
public override string DebugTag => "idle";
public override bool UnequipItems => true;
public override bool AllowOutsideSubmarine => true;
private readonly float newTargetIntervalMin = 10;
private readonly float newTargetIntervalMax = 20;
@@ -231,9 +232,11 @@ namespace Barotrauma
{
if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
if (hull.Submarine == null) { continue; }
if (hull.Submarine.TeamID != character.TeamID) { continue; }
// If the character is inside, only take connected hulls into account.
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { continue; }
if (character.Submarine == null) { break; }
if (hull.Submarine.TeamID != character.Submarine.TeamID) { continue; }
if (hull.Submarine.Info.Type != character.Submarine.Info.Type) { continue; }
// If the character is inside, only take connected subs into account.
if (!character.Submarine.IsEntityFoundOnThisSub(hull, true)) { continue; }
if (IsForbidden(hull)) { continue; }
// Ignore hulls that are too low to stand inside
if (character.AnimController is HumanoidAnimController animController)
@@ -261,9 +264,9 @@ namespace Barotrauma
public static bool IsForbidden(Hull hull)
{
if (hull == null) { return true; }
string hullName = hull.RoomName?.ToLowerInvariant();
string hullName = hull.RoomName;
if (hullName == null) { return false; }
return hullName.Contains("ballast") || hullName.Contains("airlock");
return hullName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || hullName.Contains("airlock", StringComparison.OrdinalIgnoreCase);
}
}
}
@@ -108,6 +108,11 @@ namespace Barotrauma
public override float GetPriority()
{
if (!IsAllowed)
{
Priority = 0;
return Priority;
}
if (character.LockHands || character.Submarine == null || Targets.None())
{
Priority = 0;
@@ -1,9 +1,10 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -102,20 +103,12 @@ namespace Barotrauma
{
var orderPrefab = Order.GetPrefab(automaticOrder.identifier);
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{automaticOrder.identifier}'"); }
// TODO: Similar code is used in CrewManager:815-> DRY
var matchingItems = orderPrefab.ItemIdentifiers.Any() ?
Item.ItemList.FindAll(it => orderPrefab.ItemIdentifiers.Contains(it.Prefab.Identifier) || it.HasTag(orderPrefab.ItemIdentifiers)) :
Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == orderPrefab.ItemComponentType));
matchingItems.RemoveAll(it => it.Submarine != character.Submarine);
var item = matchingItems.GetRandom();
var order = new Order(
orderPrefab,
item ?? character.CurrentHull as Entity,
item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType),
orderGiver: character);
var item = orderPrefab.MustSetTarget ? orderPrefab.GetMatchingItems(character.Submarine, false)?.GetRandom() : null;
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity,
item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType), orderGiver: character);
if (order == null) { continue; }
var objective = CreateObjective(order, automaticOrder.option, character, automaticOrder.priorityModifier);
if (objective != null)
if (objective != null && objective.CanBeCompleted)
{
AddObjective(objective, delay: Rand.Value() / 2);
objectiveCount++;
@@ -220,7 +213,7 @@ namespace Barotrauma
}
GetCurrentObjective()?.SortSubObjectives();
}
public void DoCurrentObjective(float deltaTime)
{
if (WaitTimer <= 0)
@@ -232,7 +225,7 @@ namespace Barotrauma
character.AIController.SteeringManager.Reset();
}
}
public void SetOrder(AIObjective objective)
{
CurrentOrder = objective;
@@ -271,13 +264,13 @@ namespace Barotrauma
};
break;
case "wait":
newObjective = new AIObjectiveGoTo(character, character, this, repeat: true, priorityModifier: priorityModifier)
newObjective = new AIObjectiveGoTo(order.TargetEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier)
{
AllowGoingOutside = character.CurrentHull == null
};
break;
case "fixleaks":
newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier);
newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier: priorityModifier, prioritizedHull: order.TargetEntity as Hull);
break;
case "chargebatteries":
newObjective = new AIObjectiveChargeBatteries(character, this, option, priorityModifier);
@@ -286,13 +279,24 @@ namespace Barotrauma
newObjective = new AIObjectiveRescueAll(character, this, priorityModifier);
break;
case "repairsystems":
newObjective = new AIObjectiveRepairItems(character, this, priorityModifier)
case "repairmechanical":
case "repairelectrical":
newObjective = new AIObjectiveRepairItems(character, this, priorityModifier: priorityModifier, prioritizedItem: order.TargetEntity as Item)
{
RelevantSkill = order.AppropriateSkill,
RequireAdequateSkills = option == "jobspecific"
};
break;
case "pumpwater":
newObjective = new AIObjectivePumpWater(character, this, option, priorityModifier: priorityModifier);
if (order.TargetItemComponent is Pump targetPump)
{
newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier);
// newObjective.Completed += DismissSelf;
}
else
{
newObjective = new AIObjectivePumpWater(character, this, option, priorityModifier: priorityModifier);
}
break;
case "extinguishfires":
newObjective = new AIObjectiveExtinguishFires(character, this, priorityModifier);
@@ -324,6 +328,18 @@ namespace Barotrauma
return newObjective;
}
private void DismissSelf()
{
#if CLIENT
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
{
GameMain.GameSession?.CrewManager?.SetCharacterOrder(character, Order.GetPrefab("dismissed"), null, character);
}
#else
GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(Order.GetPrefab("dismissed"), null, null, character, character));
#endif
}
private bool IsAllowedToWait()
{
if (CurrentOrder != null) { return false; }
@@ -32,6 +32,11 @@ namespace Barotrauma
public override float GetPriority()
{
if (!IsAllowed)
{
Priority = 0;
return Priority;
}
if (component.Item.ConditionPercentage <= 0)
{
Priority = 0;
@@ -42,11 +47,21 @@ namespace Barotrauma
{
Priority = AIObjectiveManager.OrderPriority;
}
if (component.Item.CurrentHull == null || component.Item.CurrentHull.FireSources.Any() || IsOperatedByAnother(character, GetTarget(), out _))
Item targetItem = GetTarget()?.Item;
if (targetItem == null)
{
#if DEBUG
DebugConsole.ThrowError("Item or component of AI Objective Operate item wass null. This shouldn't happen.");
#endif
Abandon = true;
Priority = 0;
return 0.0f;
}
if (targetItem.CurrentHull == null || targetItem.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(GetTarget(), out _))
{
Priority = 0;
}
else if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
else if (Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
Priority = 0;
}
@@ -60,8 +75,8 @@ namespace Barotrauma
return Priority;
}
public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, Entity operateTarget = null, bool useController = false, float priorityModifier = 1)
: base (character, objectiveManager, priorityModifier, option)
public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, Entity operateTarget = null, bool useController = false, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier, option)
{
this.component = item ?? throw new System.ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target.");
this.requireEquip = requireEquip;
@@ -76,51 +91,6 @@ namespace Barotrauma
}
}
public static bool IsOperatedByAnother(Character character, ItemComponent target, out Character operatingCharacter)
{
operatingCharacter = null;
foreach (var c in Character.CharacterList)
{
if (character != null && c == character) { continue; }
if (character?.AIController is HumanAIController humanAi && !humanAi.IsFriendly(c)) { continue; }
if (c.SelectedConstruction != target.Item) { continue; }
operatingCharacter = c;
// If the other character is player, don't try to operate
if (c.IsRemotePlayer || Character.Controlled == c) { return true; }
if (c.AIController is HumanAIController controllingHumanAi)
{
// If the other character is ordered to operate the item, let him do it
if (controllingHumanAi.ObjectiveManager.IsCurrentOrder<AIObjectiveOperateItem>())
{
return true;
}
else
{
if (character == null)
{
return true;
}
else if (target is Steering)
{
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
return character.GetSkillLevel("helm") <= c.GetSkillLevel("helm");
}
else
{
return target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c);
}
}
}
else
{
// Shouldn't go here, unless we allow non-humans to operate items
return false;
}
}
return false;
}
protected override void Act(float deltaTime)
{
if (character.LockHands)
@@ -136,7 +106,7 @@ namespace Barotrauma
return;
}
// Don't allow to operate an item that someone with a better skills already operates, unless this is an order
if (objectiveManager.CurrentOrder != this && IsOperatedByAnother(character, target, out _))
if (objectiveManager.CurrentOrder != this && HumanAIController.IsItemOperatedByAnother(target, out _))
{
// Don't abandon
return;
@@ -161,7 +131,7 @@ namespace Barotrauma
{
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = target.Item.Name
},
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
@@ -176,7 +146,7 @@ namespace Barotrauma
}
else if (!character.Inventory.Items.Contains(component.Item))
{
TryAddSubObjective(ref getItemObjective, () => new AIObjectiveGetItem(character, component.Item, objectiveManager, equip: true),
TryAddSubObjective(ref getItemObjective, () => new AIObjectiveGetItem(character, component.Item, objectiveManager, equip: true),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getItemObjective));
}
@@ -33,7 +33,11 @@ namespace Barotrauma
if (pump.Item.Submarine.TeamID != character.TeamID) { return false; }
if (pump.Item.ConditionPercentage <= 0) { return false; }
if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(pump.Item, true)) { return false; }
if (character.Submarine != null)
{
if (pump.Item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(pump.Item, true)) { return false; }
}
if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (IsReady(pump)) { return false; }
return true;
@@ -20,14 +20,22 @@ namespace Barotrauma
private RepairTool repairTool;
private bool IsRepairing => character.SelectedConstruction == Item && Item.GetComponent<Repairable>()?.CurrentFixer == character;
private readonly bool isPriority;
public AIObjectiveRepairItem(Character character, Item item, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
public AIObjectiveRepairItem(Character character, Item item, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool isPriority = false)
: base(character, objectiveManager, priorityModifier)
{
Item = item;
this.isPriority = isPriority;
}
public override float GetPriority()
{
if (!IsAllowed)
{
Priority = 0;
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))
@@ -36,16 +44,16 @@ namespace Barotrauma
}
else
{
float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist));
if (Item.CurrentHull == character.CurrentHull)
float distanceFactor = 1;
if (!isPriority && Item.CurrentHull != character.CurrentHull)
{
distanceFactor = 1;
float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist));
}
float damagePriority = MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition);
float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character)));
float damagePriority = isPriority ? 1 : MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition);
float successFactor = isPriority ? 1 : MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character)));
float isSelected = IsRepairing ? 50 : 0;
float devotion = (CumulatedDevotion + isSelected) / 100;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
@@ -165,7 +173,7 @@ namespace Barotrauma
}
repairable.StopRepairing(character);
}
else
else if (repairable.CurrentFixer != character)
{
repairable.StartRepairing(character, Repairable.FixActions.Repair);
}
@@ -15,12 +15,23 @@ namespace Barotrauma
/// </summary>
public bool RequireAdequateSkills;
/// <summary>
/// If set, only fix items where required skill matches this.
/// </summary>
public string RelevantSkill;
private readonly Item prioritizedItem;
public override bool AllowMultipleInstances => true;
public override bool IsDuplicate<T>(T otherObjective) =>
(otherObjective as AIObjective) is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills;
public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Item prioritizedItem = null)
: base(character, objectiveManager, priorityModifier)
{
this.prioritizedItem = prioritizedItem;
}
protected override void CreateObjectives()
{
@@ -71,6 +82,10 @@ namespace Barotrauma
{
if (item.Repairables.Any(r => !r.HasRequiredSkills(character))) { return false; }
}
if (!string.IsNullOrWhiteSpace(RelevantSkill))
{
if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier.Equals(RelevantSkill, StringComparison.OrdinalIgnoreCase)))) { return false; }
}
return true;
}
@@ -103,7 +118,7 @@ namespace Barotrauma
protected override IEnumerable<Item> GetList() => Item.ItemList;
protected override AIObjective ObjectiveConstructor(Item item)
=> new AIObjectiveRepairItem(character, item, objectiveManager, PriorityModifier);
=> new AIObjectiveRepairItem(character, item, objectiveManager, priorityModifier: PriorityModifier, isPriority: item == prioritizedItem);
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
=> HumanAIController.RemoveTargets<AIObjectiveRepairItems, Item>(character, target);
@@ -116,7 +131,11 @@ namespace Barotrauma
if (item.Submarine == null) { return false; }
if (item.Submarine.TeamID != character.TeamID) { return false; }
if (item.Repairables.None()) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; }
if (character.Submarine != null)
{
if (item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; }
}
return true;
}
}
@@ -22,6 +22,8 @@ namespace Barotrauma
private AIObjectiveGetItem getItemObjective;
private float treatmentTimer;
private Hull safeHull;
private float findHullTimer;
private readonly float findHullInterval = 1.0f;
public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -56,14 +58,17 @@ namespace Barotrauma
if (targetCharacter != character)
{
// Unconcious target is not in a safe place -> Move to a safe place first
if (targetCharacter.IsUnconscious && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
// Incapacitated target is not in a safe place -> Move to a safe place first
if (targetCharacter.IsIncapacitated && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
{
if (character.SelectedCharacter != targetCharacter)
{
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" },
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f);
if (targetCharacter.CurrentHull.DisplayName != null)
{
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" },
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f);
}
// Go to the target and select it
if (!character.CanInteractWith(targetCharacter))
@@ -92,9 +97,17 @@ namespace Barotrauma
// Drag the character into safety
if (safeHull == null)
{
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
if (findHullTimer > 0)
{
findHullTimer -= deltaTime;
}
else
{
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
}
}
if (character.CurrentHull != safeHull)
if (safeHull != null && character.CurrentHull != safeHull)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
@@ -132,10 +145,13 @@ namespace Barotrauma
{
// We can start applying treatment
if (character != targetCharacter && character.SelectedCharacter != targetCharacter)
{
character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", new string[2] { "[targetname]", "[roomname]" },
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
null, 1.0f, "foundwoundedtarget" + targetCharacter.Name, 60.0f);
{
if (targetCharacter.CurrentHull.DisplayName != null)
{
character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", new string[2] { "[targetname]", "[roomname]" },
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
null, 1.0f, "foundwoundedtarget" + targetCharacter.Name, 60.0f);
}
character.SelectCharacter(targetCharacter);
}
@@ -151,7 +167,7 @@ namespace Barotrauma
if (!targetCharacter.IsPlayer)
{
// If the target is a bot, don't let it move
targetCharacter.AIController.SteeringManager.Reset();
targetCharacter.AIController?.SteeringManager.Reset();
}
if (treatmentTimer > 0.0f)
{
@@ -278,6 +294,11 @@ namespace Barotrauma
public override float GetPriority()
{
if (!IsAllowed)
{
Priority = 0;
return Priority;
}
if (targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
Priority = 0;
@@ -73,6 +73,7 @@ namespace Barotrauma
public static float GetVitalityFactor(Character character)
{
float vitality = character.HealthPercentage - character.Bleeding - character.Bloodloss + Math.Min(character.Oxygen, 0);
vitality -= character.CharacterHealth.GetAfflictionStrength("paralysis");
return Math.Clamp(vitality, 0, 100);
}
@@ -105,7 +106,11 @@ namespace Barotrauma
if (target.Submarine == null || character.Submarine == null) { return false; }
if (target.Submarine.TeamID != character.Submarine.TeamID) { return false; }
if (target.CurrentHull == null) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
if (character.Submarine != null)
{
if (target.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
}
if (!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
{
// Ignore all concious targets that are currently fighting, fleeing or treating characters
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -13,8 +14,7 @@ namespace Barotrauma
Movement,
Power,
Maintenance,
Operate,
Undefined
Operate
}
class Order
@@ -31,11 +31,7 @@ namespace Barotrauma
return order;
}
public Order Prefab
{
get;
private set;
}
public Order Prefab { get; private set; }
public readonly string Name;
@@ -55,7 +51,7 @@ namespace Barotrauma
{
return color.Value;
}
else if (OrderCategoryIcons.TryGetValue(Category, out Tuple<Sprite, Color> sprite))
else if (Category.HasValue && OrderCategoryIcons.TryGetValue((OrderCategory)Category, out Tuple<Sprite, Color> sprite))
{
return sprite.Item2;
}
@@ -83,7 +79,8 @@ namespace Barotrauma
public Character OrderGiver;
public readonly OrderCategory Category;
private readonly OrderCategory? category;
public OrderCategory? Category => category;
//legacy support
public readonly string[] AppropriateJobs;
@@ -93,6 +90,25 @@ namespace Barotrauma
public readonly Dictionary<string, Sprite> OptionSprites;
public readonly float Weight;
public readonly bool MustSetTarget;
public readonly string AppropriateSkill;
public bool HasOptions
{
get
{
if (IsPrefab)
{
return MustSetTarget || Options.Length > 1;
}
else
{
return Prefab.MustSetTarget || Prefab.Options.Length > 1;
}
}
}
public bool IsPrefab { get; private set; }
public readonly bool MustManuallyAssign;
static Order()
{
@@ -197,8 +213,11 @@ namespace Barotrauma
TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false);
AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]);
Options = orderElement.GetAttributeStringArray("options", new string[0]);
Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), orderElement.GetAttributeString("category", "undefined"), true);
var category = orderElement.GetAttributeString("category", null);
if (!string.IsNullOrWhiteSpace(category)) { this.category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); }
Weight = orderElement.GetAttributeFloat(0.0f, "weight");
MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false);
AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null);
string translatedOptionNames = TextManager.Get("OrderOptions." + Identifier, true);
if (translatedOptionNames == null)
@@ -241,6 +260,9 @@ namespace Barotrauma
}
}
}
IsPrefab = true;
MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false);
}
/// <summary>
@@ -253,6 +275,7 @@ namespace Barotrauma
Name = prefab.Name;
Identifier = prefab.Identifier;
ItemComponentType = prefab.ItemComponentType;
ItemIdentifiers = prefab.ItemIdentifiers;
Options = prefab.Options;
SymbolSprite = prefab.SymbolSprite;
Color = prefab.Color;
@@ -261,22 +284,35 @@ namespace Barotrauma
AppropriateJobs = prefab.AppropriateJobs;
FadeOutTime = prefab.FadeOutTime;
Weight = prefab.Weight;
Category = prefab.Category;
OrderGiver = orderGiver;
MustSetTarget = prefab.MustSetTarget;
AppropriateSkill = prefab.AppropriateSkill;
category = prefab.Category;
MustManuallyAssign = prefab.MustManuallyAssign;
OrderGiver = orderGiver;
TargetEntity = targetEntity;
if (targetItem != null)
{
if (UseController)
{
//try finding the controller with the simpler non-recursive method first
ConnectedController =
targetItem.Item.GetConnectedComponents<Controller>().FirstOrDefault() ??
targetItem.Item.GetConnectedComponents<Controller>(recursive: true).FirstOrDefault();
}
if (UseController) { ConnectedController = FindController(targetItem); }
TargetEntity = targetItem.Item;
TargetItemComponent = targetItem;
}
IsPrefab = false;
}
private Controller FindController(ItemComponent targetComponent)
{
if (targetComponent?.Item == null) { return null; }
//try finding the controller with the simpler non-recursive method first
return targetComponent.Item.GetConnectedComponents<Controller>().FirstOrDefault() ??
targetComponent.Item.GetConnectedComponents<Controller>(recursive: true).FirstOrDefault();
}
private bool TryFindController(ItemComponent targetComponent, out Controller controller)
{
controller = FindController(targetComponent);
return controller != null;
}
public bool HasAppropriateJob(Character character)
@@ -291,7 +327,7 @@ namespace Barotrauma
}
for (int i = 0; i < AppropriateJobs.Length; i++)
{
if (character.Info.Job.Prefab.Identifier.ToLowerInvariant() == AppropriateJobs[i].ToLowerInvariant()) { return true; }
if (character.Info.Job.Prefab.Identifier.Equals(AppropriateJobs[i], StringComparison.OrdinalIgnoreCase)) { return true; }
}
return false;
}
@@ -310,5 +346,40 @@ namespace Barotrauma
return msg;
}
public List<Item> GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub)
{
List<Item> matchingItems = new List<Item>();
if (submarine == null) { return matchingItems; }
if (ItemComponentType != null || ItemIdentifiers.Length > 0)
{
matchingItems = ItemIdentifiers.Length > 0 ?
Item.ItemList.FindAll(it => ItemIdentifiers.Contains(it.Prefab.Identifier) || it.HasTag(ItemIdentifiers)) :
Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == ItemComponentType));
if (mustBelongToPlayerSub)
{
matchingItems.RemoveAll(it => it.Submarine?.Info != null && it.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player);
matchingItems.RemoveAll(it => it.Submarine != submarine && !submarine.DockedTo.Contains(it.Submarine));
}
else
{
matchingItems.RemoveAll(it => it.Submarine != submarine);
}
matchingItems.RemoveAll(it => it.NonInteractable);
if (UseController)
{
matchingItems.RemoveAll(i => i.Components.None(c => c.GetType() == ItemComponentType && TryFindController(c, out _)));
}
}
return matchingItems;
}
public List<Item> GetMatchingItems(bool mustBelongToPlayerSub)
{
Submarine submarine = Character.Controlled != null && Character.Controlled.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1 ?
Submarine.MainSubs[1] :
Submarine.MainSub;
return GetMatchingItems(submarine, mustBelongToPlayerSub);
}
}
}
@@ -0,0 +1,377 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using Barotrauma.Networking;
using System.Linq;
using System;
namespace Barotrauma
{
partial class WreckAI : IServerSerializable
{
public Submarine Wreck { get; private set; }
public bool IsAlive { get; private set; }
private readonly List<Item> allItems;
private readonly List<Item> thalamusItems;
private readonly List<Structure> thalamusStructures;
private readonly List<Turret> turrets = new List<Turret>();
private readonly List<WayPoint> wayPoints = new List<WayPoint>();
private readonly List<Hull> hulls = new List<Hull>();
private readonly List<Item> spawnOrgans = new List<Item>();
private readonly Item brain;
private bool initialCellsSpawned;
public readonly WreckAIConfig Config;
private bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
private bool IsThalamus(MapEntityPrefab entityPrefab) => IsThalamus(entityPrefab, Config.Entity);
private static IEnumerable<T> GetThalamusEntities<T>(Submarine wreck, string tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T);
private static IEnumerable<MapEntity> GetThalamusEntities(Submarine wreck, string tag) => MapEntity.mapEntityList.Where(e => e.Submarine == wreck && e.prefab != null && IsThalamus(e.prefab, tag));
private static bool IsThalamus(MapEntityPrefab entityPrefab, string tag) => entityPrefab.Category == MapEntityCategory.Thalamus || entityPrefab.Tags.Contains(tag);
public WreckAI(Submarine wreck)
{
Wreck = wreck;
Config = WreckAIConfig.GetRandom();
if (Config == null)
{
DebugConsole.ThrowError("WreckAI: No wreck AI config found!");
return;
}
var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p));
var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.Server);
if (brainPrefab == null)
{
DebugConsole.ThrowError($"WreckAI: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI.");
return;
}
allItems = Wreck.GetItems(false);
thalamusItems = allItems.FindAll(i => IsThalamus(i.prefab));
var hulls = Wreck.GetHulls(false);
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.
// 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)
{
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);
}
var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(p => IsThalamus(p));
if (brainHull == null) { return; }
brainHull.WaterVolume = brainHull.Volume;
brain.SetTransform(brainHull.SimPosition, rotation: 0, findNewHull: false);
brain.CurrentHull = brainHull;
var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.Server);
if (backgroundPrefab != null)
{
new Structure(brainHull.Rect, backgroundPrefab, Wreck);
}
var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.Server);
if (horizontalWallPrefab != null)
{
int height = (int)horizontalWallPrefab.Size.Y;
int halfHeight = height / 2;
int quarterHeight = halfHeight / 2;
new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck);
new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck);
}
var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.Server);
if (verticalWallPrefab != null)
{
int width = (int)verticalWallPrefab.Size.X;
int halfWidth = width / 2;
int quarterWidth = halfWidth / 2;
new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, Wreck);
new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, Wreck);
}
foreach (Item item in allItems)
{
if (thalamusItems.Contains(item))
{
// Ensure that thalamus items are visible
item.HiddenInGame = false;
}
else
{
// Load regular turrets
var turret = item.GetComponent<Turret>();
if (turret != null)
{
foreach (var linkedItem in item.GetLinkedEntities<Item>())
{
var container = linkedItem.GetComponent<ItemContainer>();
if (container == null) { continue; }
for (int i = 0; i < container.Inventory.Capacity; i++)
{
if (container.Inventory.Items[i] != null) { continue; }
if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab i && container.CanBeContained(i) &&
Config.ForbiddenAmmunition.None(id => id.Equals(i.Identifier, StringComparison.OrdinalIgnoreCase)), Rand.RandSync.Server) is ItemPrefab ammoPrefab)
{
Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Wreck);
if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false))
{
item.Remove();
}
}
}
}
}
}
}
foreach (var item in allItems)
{
var turret = item.GetComponent<Turret>();
if (turret != null)
{
turrets.Add(turret);
}
if (item.HasTag(Config.Spawner))
{
if (!spawnOrgans.Contains(item))
{
spawnOrgans.Add(item);
}
}
}
wayPoints.AddRange(Wreck.GetWaypoints(false));
hulls.AddRange(Wreck.GetHulls(false));
IsAlive = true;
thalamusStructures = GetThalamusEntities<Structure>(Wreck, Config.Entity).ToList();
}
private readonly List<Item> destroyedOrgans = new List<Item>();
public void Update(float deltaTime)
{
if (!IsAlive) { return; }
if (Wreck == null || Wreck.Removed)
{
Remove();
return;
}
if (brain == null || brain.Removed || brain.Condition <= 0)
{
Kill();
return;
}
destroyedOrgans.Clear();
foreach (var organ in spawnOrgans)
{
if (organ.Condition <= 0)
{
destroyedOrgans.Add(organ);
}
}
destroyedOrgans.ForEach(o => spawnOrgans.Remove(o));
bool someoneNearby = false;
float minDist = Sonar.DefaultSonarRange * 2.0f;
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
if (Vector2.DistanceSquared(submarine.WorldPosition, Wreck.WorldPosition) < minDist * minDist)
{
someoneNearby = true;
break;
}
}
foreach (Character c in Character.CharacterList)
{
if (c != Character.Controlled && !c.IsRemotePlayer) { continue; }
if (Vector2.DistanceSquared(c.WorldPosition, Wreck.WorldPosition) < minDist * minDist)
{
someoneNearby = true;
break;
}
}
if (!someoneNearby) { return; }
OperateTurrets(deltaTime);
if (!IsClient)
{
if (!initialCellsSpawned) { SpawnInitialCells(); }
UpdateReinforcements(deltaTime);
}
}
private void SpawnInitialCells()
{
int brainRoomCells = Rand.Range(MinCellsPerBrainRoom, MaxCellsPerRoom);
if (brain.CurrentHull?.WaterPercentage >= MinWaterLevel)
{
for (int i = 0; i < brainRoomCells; i++)
{
if (!TrySpawnCell(out _, brain.CurrentHull)) { break; }
}
}
int cellsInside = Rand.Range(MinCellsInside, MaxCellsInside);
for (int i = 0; i < cellsInside; i++)
{
if (!TrySpawnCell(out _)) { break; }
}
int cellsOutside = Rand.Range(MinCellsOutside, MaxCellsOutside);
// If we failed to spawn some of the cells in the brainroom/inside, spawn some extra cells outside.
cellsOutside = Math.Clamp(cellsOutside + brainRoomCells + cellsInside - protectiveCells.Count, cellsOutside, MaxCellsOutside);
for (int i = 0; i < cellsOutside; i++)
{
ISpatialEntity targetEntity = wayPoints.GetRandom(wp => wp.CurrentHull == null);
if (targetEntity == null) { break; }
if (!TrySpawnCell(out _, targetEntity)) { break; }
}
initialCellsSpawned = true;
}
private void Kill()
{
thalamusItems.ForEach(i => i.Condition = 0);
foreach (var turret in turrets)
{
// Snap all tendons
foreach (Item item in turret.ActiveProjectiles)
{
if (item.GetComponent<Projectile>()?.IsStuckToTarget ?? false)
{
item.Condition = 0;
}
}
}
FadeOutColors();
protectiveCells.ForEach(c => c.OnDeath -= OnCellDeath);
if (!IsClient)
{
if (Config != null)
{
if (Config.KillAgentsWhenEntityDies)
{
protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null, isNetworkMessage: true));
}
}
}
protectiveCells.Clear();
IsAlive = false;
}
partial void FadeOutColors();
public void Remove()
{
Kill();
RemoveThalamusItems(Wreck);
thalamusItems?.Clear();
thalamusStructures?.Clear();
}
public static void RemoveThalamusItems(Submarine wreck)
{
foreach (var wreckAiConfig in WreckAIConfig.List)
{
GetThalamusEntities(wreck, wreckAiConfig.Entity).ForEachMod(e => e.Remove());
}
}
// The client doesn't use these, so we don't have to sync them.
private readonly List<Character> protectiveCells = new List<Character>();
// Intentionally contains duplicates.
private readonly List<Hull> populatedHulls = new List<Hull>();
private float cellSpawnTimer;
private float CellSpawnTime => Config.AgentSpawnDelay;
private float CellSpawnRandomFactor => Config.AgentSpawnDelayRandomFactor;
private int MinCellsPerBrainRoom => Config.MinAgentsPerBrainRoom;
private int MaxCellsPerRoom => Config.MaxAgentsPerRoom;
private int MinCellsOutside => Config.MinAgentsOutside;
private int MaxCellsOutside => Config.MaxAgentsOutside;
private int MinCellsInside => Config.MinAgentsInside;
private int MaxCellsInside => Config.MaxAgentsInside;
private int MaxCellCount => Config.MaxAgentCount;
private float MinWaterLevel => Config.MinWaterLevel;
void UpdateReinforcements(float deltaTime)
{
if (protectiveCells.Count >= MaxCellCount || spawnOrgans.Count == 0) { return; }
cellSpawnTimer -= deltaTime;
if (cellSpawnTimer < 0)
{
TrySpawnCell(out _, spawnOrgans.GetRandom());
cellSpawnTimer = CellSpawnTime * Rand.Range(CellSpawnRandomFactor, 1 + CellSpawnRandomFactor);
}
}
bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null)
{
cell = null;
if (protectiveCells.Count >= MaxCellCount) { return false; }
if (targetEntity == null)
{
targetEntity =
wayPoints.GetRandom(wp => wp.CurrentHull != null && populatedHulls.Count(h => h == wp.CurrentHull) < MaxCellsPerRoom && wp.CurrentHull.WaterPercentage >= MinWaterLevel) ??
hulls.GetRandom(h => populatedHulls.Count(h2 => h2 == h) < MaxCellsPerRoom && h.WaterPercentage >= MinWaterLevel) as ISpatialEntity;
}
if (targetEntity == null) { return false; }
if (targetEntity is Hull h)
{
populatedHulls.Add(h);
}
else if (targetEntity is WayPoint wp && wp.CurrentHull != null)
{
populatedHulls.Add(wp.CurrentHull);
}
// Don't add items in the list, because we want to be able to ignore the restrictions for spawner organs.
cell = Character.Create(Config.DefensiveAgent, targetEntity.WorldPosition, ToolBox.RandomSeed(8), hasAi: true, createNetworkEvent: true);
protectiveCells.Add(cell);
cell.OnDeath += OnCellDeath;
cellSpawnTimer = CellSpawnTime * Rand.Range(CellSpawnRandomFactor, 1 + CellSpawnRandomFactor);
return true;
}
void OperateTurrets(float deltaTime)
{
foreach (var turret in turrets)
{
// Never target other creatures than humans with the turrets.
turret.ThalamusOperate(this, deltaTime,
!turret.Item.HasTag("ignorecharacters"),
targetOtherCreatures: false,
!turret.Item.HasTag("ignoresubmarines"),
turret.Item.HasTag("ignoreaimdelay"));
}
}
void OnCellDeath(Character character, CauseOfDeath causeOfDeath)
{
protectiveCells.Remove(character);
}
#if SERVER
public void ServerWrite(IWriteMessage msg, Client client, object[] extraData = null)
{
msg.Write(IsAlive);
}
#endif
}
}
@@ -0,0 +1,127 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class WreckAIConfig : ISerializableEntity
{
public string Name => "Wreck AI Config";
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
[Serialize("", false)]
public string Entity { get; private set; }
[Serialize("", false)]
public string DefensiveAgent { get; private set; }
[Serialize("", false)]
public string Brain { get; private set; }
[Serialize("", false)]
public string Spawner { get; private set; }
[Serialize("", false)]
public string BrainRoomBackground { get; private set; }
[Serialize("", false)]
public string BrainRoomVerticalWall { get; private set; }
[Serialize("", false)]
public string BrainRoomHorizontalWall { get; private set; }
[Serialize(60f, false)]
public float AgentSpawnDelay { get; private set; }
[Serialize(0.5f, false)]
public float AgentSpawnDelayRandomFactor { get; private set; }
[Serialize(0, false)]
public int MinAgentsPerBrainRoom { get; private set; }
[Serialize(3, false)]
public int MaxAgentsPerRoom { get; private set; }
[Serialize(2, false)]
public int MinAgentsOutside { get; private set; }
[Serialize(5, false)]
public int MaxAgentsOutside { get; private set; }
[Serialize(3, false)]
public int MinAgentsInside { get; private set; }
[Serialize(10, false)]
public int MaxAgentsInside { get; private set; }
[Serialize(15, false)]
public int MaxAgentCount { get; private set; }
[Serialize(100f, false)]
public float MinWaterLevel { get; private set; }
[Serialize(true, false)]
public bool KillAgentsWhenEntityDies { get; private set; }
[Serialize(1f, false)]
public float DeadEntityColorMultiplier { get; private set; }
[Serialize(1f, false)]
public float DeadEntityColorFadeOutTime { get; private set; }
public readonly string[] ForbiddenAmmunition;
public static List<WreckAIConfig> List
{
get
{
if (paramsList == null)
{
LoadAll();
}
return paramsList;
}
}
private static List<WreckAIConfig> paramsList;
public static WreckAIConfig GetRandom() => List.GetRandom(Rand.RandSync.Server);
public WreckAIConfig(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
ForbiddenAmmunition = XMLExtensions.GetAttributeStringArray(element, "ForbiddenAmmunition", new string[0], convertToLowerInvariant: true);
}
public static void LoadAll()
{
paramsList = new List<WreckAIConfig>();
var files = GameMain.Instance.GetFilesOfType(ContentType.WreckAIConfig);
if (files.None())
{
DebugConsole.ThrowError("Cannot find any Wreck AI config!");
return;
}
foreach (ContentFile file in files)
{
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { continue; }
var mainElement = doc.Root;
if (mainElement.IsOverride())
{
mainElement = doc.Root.FirstElement();
paramsList.Clear();
DebugConsole.NewMessage($"Overriding the wreck ai config with '{file.Path}'", Color.Yellow);
}
else if (paramsList.Any())
{
DebugConsole.NewMessage($"Adding additional wreck ai config from file '{file.Path}'");
}
paramsList.Add(new WreckAIConfig(mainElement));
}
}
}
}
@@ -46,8 +46,12 @@ namespace Barotrauma
{
base.Update(deltaTime, cam);
if (!Enabled) return;
if (!Enabled) { return; }
if (IsDead || Vitality <= 0.0f || Stun > 0.0f || IsIncapacitated) { return; }
//don't enable simple physics on dead/incapacitated characters
//the ragdoll controls the movement of incapacitated characters instead of the collider,
//but in simple physics mode the ragdoll would get disabled, causing the character to not move at all
if (!IsRemotePlayer)
{
float characterDist = float.MaxValue;
@@ -70,10 +74,9 @@ namespace Barotrauma
}
}
if (IsDead || Vitality <= 0.0f || IsUnconscious || Stun > 0.0f) return;
if (!aiController.Enabled) return;
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) return;
if (Controlled == this) return;
if (!aiController.Enabled) { return; }
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
if (Controlled == this) { return; }
if (!IsRemotePlayer)
{
@@ -441,7 +441,7 @@ namespace Barotrauma
for (int i = -1; i < 2; i += 2)
{
Vector2 footPos = GetColliderBottom();
footPos = new Vector2(waist.SimPosition.X + Math.Sign(StepSize.Value.X * i) * Dir * 0.3f, footPos.Y - 0.1f * RagdollParams.JointScale);
footPos = new Vector2(waist.SimPosition.X + Math.Sign(WalkParams.StepSize.X * i) * Dir * 0.3f, footPos.Y - 0.1f * RagdollParams.JointScale);
var foot = i == -1 ? rightFoot : leftFoot;
MoveLimb(foot, footPos, Math.Abs(foot.SimPosition.X - footPos.X) * 100.0f, true);
}
@@ -481,7 +481,7 @@ namespace Barotrauma
swimmingStateLockTimer -= deltaTime;
if (forceStanding)
if (forceStanding || character.AnimController.AnimationTestPose)
{
swimming = false;
}
@@ -1410,7 +1410,7 @@ namespace Barotrauma
SteamAchievementManager.OnCharacterRevived(target, character);
lastReviveTime = (float)Timing.TotalTime;
#if SERVER
GameMain.Server?.KarmaManager?.OnCharacterHealthChanged(target, character, damage: Math.Min(prevVitality - target.Vitality, 0.0f));
GameMain.Server?.KarmaManager?.OnCharacterHealthChanged(target, character, damage: Math.Min(prevVitality - target.Vitality, 0.0f), stun: 0.0f);
#endif
//reset attacker, we don't want the character to start attacking us
//because we caused a bit of damage to them during CPR
@@ -1540,6 +1540,11 @@ namespace Barotrauma
{
sourceSimPos -= character.SelectedCharacter.Submarine.SimPosition;
}
else if (character.Submarine != null && character.SelectedCharacter.Submarine != null && character.Submarine != character.SelectedCharacter.Submarine)
{
targetSimPos += character.SelectedCharacter.Submarine.SimPosition;
targetSimPos -= character.Submarine.SimPosition;
}
var body = Submarine.CheckVisibility(sourceSimPos, targetSimPos, ignoreSubs: true);
if (body != null)
{
@@ -1553,8 +1558,8 @@ namespace Barotrauma
Vector2 diff = ConvertUnits.ToSimUnits(targetLimb.WorldPosition - pullLimb.WorldPosition);
Vector2 targetAnchor = targetLimb.SimPosition;
float targetForce = 0.0f;
Vector2 targetAnchor;
float targetForce;
pullLimb.PullJointEnabled = true;
if (targetLimb.type == LimbType.Torso || targetLimb == target.AnimController.MainLimb)
{
@@ -1624,6 +1629,7 @@ namespace Barotrauma
if (!target.AllowInput)
{
target.AnimController.Stairs = Stairs;
target.AnimController.IgnorePlatforms = IgnorePlatforms;
target.AnimController.TargetMovement = TargetMovement;
}
@@ -1652,7 +1658,10 @@ namespace Barotrauma
//TODO: refactor this method, it's way too convoluted
public override void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f)
{
if (character.IsUnconscious || character.Stun > 0.0f) aim = false;
if (character.Stun > 0.0f || character.IsIncapacitated)
{
aim = false;
}
//calculate the handle positions
Matrix itemTransfrom = Matrix.CreateRotationZ(item.body.Rotation);
@@ -1676,7 +1685,7 @@ namespace Barotrauma
Holdable holdable = item.GetComponent<Holdable>();
if (!isClimbing && !usingController && character.Stun <= 0.0f && aim && itemPos != Vector2.Zero)
if (!isClimbing && !usingController && character.Stun <= 0.0f && aim && itemPos != Vector2.Zero && !character.IsIncapacitated)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition);
@@ -1763,7 +1772,7 @@ namespace Barotrauma
if (holdable.Pusher != null)
{
if (character.IsUnconscious || character.Stun > 0.0f)
if (character.Stun > 0.0f || character.IsIncapacitated)
{
holdable.Pusher.Enabled = false;
}
@@ -1778,7 +1787,7 @@ namespace Barotrauma
else
{
holdable.Pusher.TargetPosition = currItemPos;
holdable.Pusher.TargetRotation = character.IsUnconscious || character.Stun > 0.0f ? itemAngle : holdAngle * Dir;
holdable.Pusher.TargetRotation = holdAngle * Dir;
holdable.Pusher.MoveToTargetPosition(true);
@@ -55,7 +55,11 @@ namespace Barotrauma
{
if (limbs == null)
{
DebugConsole.ThrowError("Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this));
string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
#if DEBUG || UNSTABLE
errorMsg += '\n' + Environment.StackTrace;
#endif
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"Ragdoll.Limbs:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
@@ -80,7 +84,7 @@ namespace Barotrauma
frozen = value;
Collider.FarseerBody.LinearDamping = frozen ? (1.5f / (float)Timing.Step) : 0.0f;
Collider.FarseerBody.AngularDamping = frozen ? (1.5f / (float)Timing.Step) : 0.0f;
Collider.FarseerBody.AngularDamping = frozen ? (1.5f / (float)Timing.Step) : PhysicsBody.DefaultAngularDamping;
Collider.FarseerBody.IgnoreGravity = frozen;
//Collider.PhysEnabled = !frozen;
@@ -97,8 +101,6 @@ namespace Barotrauma
protected float strongestImpact;
protected double onFloorTimer;
private float splashSoundTimer;
//the movement speed of the ragdoll
@@ -635,10 +637,7 @@ namespace Barotrauma
Vector2 colliderBottom = GetColliderBottom();
if (structure.IsPlatform)
{
if (IgnorePlatforms) { return false; }
//the collision is ignored if the lowest limb is under the platform
//if (lowestLimb==null || lowestLimb.Position.Y < structure.Rect.Y) return false;
if (IgnorePlatforms || currentHull == null) { return false; }
if (colliderBottom.Y < ConvertUnits.ToSimUnits(structure.Rect.Y - 5)) { return false; }
if (f1.Body.Position.Y < ConvertUnits.ToSimUnits(structure.Rect.Y - 5)) { return false; }
@@ -732,14 +731,20 @@ namespace Barotrauma
limbJoint.IsSevered = true;
limbJoint.Enabled = false;
Vector2 limbDiff = limbJoint.LimbA.SimPosition - limbJoint.LimbB.SimPosition;
if (limbDiff.LengthSquared() < 0.0001f) { limbDiff = Rand.Vector(1.0f); }
limbDiff = Vector2.Normalize(limbDiff);
float mass = limbJoint.BodyA.Mass + limbJoint.BodyB.Mass;
limbJoint.LimbA.body.ApplyLinearImpulse(limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f);
limbJoint.LimbB.body.ApplyLinearImpulse(-limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f);
List<Limb> connectedLimbs = new List<Limb>();
List<LimbJoint> checkedJoints = new List<LimbJoint>();
GetConnectedLimbs(connectedLimbs, checkedJoints, MainLimb);
foreach (Limb limb in Limbs)
{
if (connectedLimbs.Contains(limb)) continue;
if (connectedLimbs.Contains(limb)) { continue; }
limb.IsSevered = true;
}
@@ -962,8 +967,8 @@ namespace Barotrauma
else
{
if (character.Position.X < gap.Rect.X || character.Position.X > gap.Rect.Right) continue;
if (Math.Sign((gap.Rect.Y - gap.Rect.Height / 2) - (currentHull.Rect.Center.Y - currentHull.Rect.Height / 2)) !=
Math.Sign(character.Position.Y - (currentHull.Rect.Center.Y - currentHull.Rect.Height / 2)))
if (Math.Sign((gap.Rect.Y - gap.Rect.Height / 2) - (currentHull.Rect.Y - currentHull.Rect.Height / 2)) !=
Math.Sign(character.Position.Y - (currentHull.Rect.Y - currentHull.Rect.Height / 2)))
{
continue;
}
@@ -1224,6 +1229,8 @@ namespace Barotrauma
private void CheckBodyInRest(float deltaTime)
{
if (SimplePhysicsEnabled) { return; }
if (InWater || Collider.LinearVelocity.LengthSquared() > 0.01f || character.SelectedBy != null || !character.IsDead)
{
bodyInRestTimer = 0.0f;
@@ -1633,7 +1640,7 @@ namespace Barotrauma
{
float offset = 0.0f;
if (!character.IsUnconscious && !character.IsDead && character.Stun <= 0.0f)
if (!character.IsDead && character.Stun <= 0.0f && !character.IsIncapacitated)
{
offset = -ColliderHeightFromFloor;
}
@@ -260,7 +260,7 @@ namespace Barotrauma
return totalDamage;
}
public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float range = 0.0f)
public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f)
{
if (damage > 0.0f) Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage), null);
if (bleedingDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage), null);
@@ -269,6 +269,7 @@ namespace Barotrauma
Range = range;
DamageRange = range;
StructureDamage = structureDamage;
ItemDamage = itemDamage;
}
public Attack(XElement element, string parentDebugName)
@@ -298,7 +299,7 @@ namespace Barotrauma
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names.");
string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.ToLowerInvariant() == afflictionName);
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.Equals(afflictionName, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab == null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found.");
@@ -308,7 +309,7 @@ namespace Barotrauma
else
{
string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier);
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab == null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found.");
@@ -343,7 +344,7 @@ namespace Barotrauma
AfflictionPrefab afflictionPrefab;
Affliction affliction;
string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier);
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab != null)
{
float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength");
@@ -515,10 +516,10 @@ namespace Barotrauma
public void SetCoolDown()
{
float randomFraction = CoolDown * CoolDownRandomFactor;
CurrentRandomCoolDown = MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value(Rand.RandSync.Server));
CurrentRandomCoolDown = MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value());
CoolDownTimer = CoolDown + CurrentRandomCoolDown;
randomFraction = SecondaryCoolDown * CoolDownRandomFactor;
SecondaryCoolDownTimer = SecondaryCoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value(Rand.RandSync.Server));
SecondaryCoolDownTimer = SecondaryCoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value());
}
public void ResetCoolDown()
@@ -126,6 +126,14 @@ namespace Barotrauma
set => Params.NeedsAir = value;
}
public bool NeedsWater
{
get => Params.NeedsWater;
set => Params.NeedsWater = value;
}
public bool NeedsOxygen => NeedsAir || NeedsWater && !AnimController.InWater;
public float Noise
{
get => Params.Noise;
@@ -144,7 +152,28 @@ namespace Barotrauma
private float attackCoolDown;
public Order CurrentOrder { get; private set; }
public Order CurrentOrder
{
get
{
return Info?.CurrentOrder;
}
private set
{
if (Info != null) { Info.CurrentOrder = value; }
}
}
public string CurrentOrderOption
{
get
{
return Info?.CurrentOrderOption;
}
private set
{
if (Info != null) { Info.CurrentOrderOption = value; }
}
}
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
private readonly List<float> speedMultipliers = new List<float>();
@@ -170,7 +199,7 @@ namespace Barotrauma
if (turret != null)
{
viewTargetWorldPos = new Vector2(
targetItem.WorldRect.X + turret.TransformedBarrelPos.X,
targetItem.WorldRect.X + turret.TransformedBarrelPos.X,
targetItem.WorldRect.Y - turret.TransformedBarrelPos.Y);
}
}
@@ -255,7 +284,7 @@ namespace Barotrauma
//text displayed when the character is highlighted if custom interact is set
public string customInteractHUDText;
private Action<Character, Character> onCustomInteract;
private float lockHandsTimer;
public bool LockHands
{
@@ -271,22 +300,22 @@ namespace Barotrauma
public bool AllowInput
{
get { return !IsUnconscious && Stun <= 0.0f && !IsDead; }
get { return Stun <= 0.0f && !IsDead && !IsIncapacitated; }
}
public bool CanMove
{
get
{
if (!AllowInput) { return false; }
if (!AnimController.InWater && !AnimController.CanWalk) { return false; }
if (!AllowInput) { return false; }
return true;
}
}
public bool CanInteract
{
get { return AllowInput && IsHumanoid && !LockHands && !Removed; }
get { return AllowInput && IsHumanoid && !LockHands && !Removed && !IsIncapacitated; }
}
public Vector2 CursorPosition
@@ -372,12 +401,21 @@ namespace Barotrauma
pressureProtection = MathHelper.Clamp(value, 0.0f, 100.0f);
}
}
private float ragdollingLockTimer;
public bool IsRagdolled;
public bool IsForceRagdolled;
public bool dontFollowCursor;
public bool IsIncapacitated
{
get
{
if (IsUnconscious) { return true; }
return CharacterHealth.Afflictions.Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength);
}
}
public bool IsUnconscious
{
get { return CharacterHealth.IsUnconscious; }
@@ -501,6 +539,8 @@ namespace Barotrauma
public bool IsDead { get; private set; }
public bool EnableDespawn { get; set; } = true;
public CauseOfDeath CauseOfDeath
{
get;
@@ -523,7 +563,7 @@ namespace Barotrauma
{
if (!canBeDragged) { return false; }
if (Removed || !AnimController.Draggable) { return false; }
return IsDead || Stun > 0.0f || LockHands || IsUnconscious;
return IsDead || Stun > 0.0f || LockHands || IsIncapacitated;
}
set { canBeDragged = value; }
}
@@ -541,7 +581,7 @@ namespace Barotrauma
}
else
{
return (IsDead || Stun > 0.0f || LockHands || IsUnconscious);
return (IsDead || Stun > 0.0f || LockHands || IsIncapacitated);
}
}
set { canInventoryBeAccessed = value; }
@@ -562,7 +602,9 @@ namespace Barotrauma
{
errorMsg += " AnimController.Collider == null";
}
#if DEBUG || UNSTABLE
errorMsg += '\n' + Environment.StackTrace;
#endif
DebugConsole.NewMessage(errorMsg, Color.Red);
GameAnalyticsManager.AddErrorEventOnce(
"Character.SimPosition:AccessRemoved",
@@ -768,7 +810,7 @@ namespace Barotrauma
var matchingAffliction = AfflictionPrefab.List
.Where(p => p.AfflictionType == "huskinfection")
.Select(p => p as AfflictionPrefabHusk)
.FirstOrDefault(p => p.TargetSpecies.Any(t => t.Equals(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p), StringComparison.InvariantCultureIgnoreCase)));
.FirstOrDefault(p => p.TargetSpecies.Any(t => t.Equals(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p), StringComparison.OrdinalIgnoreCase)));
string nonHuskedSpeciesName = string.Empty;
if (matchingAffliction == null)
{
@@ -1212,7 +1254,7 @@ namespace Barotrauma
{
attackCoolDown -= deltaTime;
}
else if (IsKeyDown(InputType.Attack))
else if (IsKeyDown(InputType.Attack) && (IsRemotePlayer || Controlled == this || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)))
{
var currentContexts = GetAttackContexts();
var validLimbs = AnimController.Limbs.Where(l => !l.IsSevered && !l.IsStuck && l.attack != null && l.attack.IsValidContext(currentContexts));
@@ -1522,7 +1564,7 @@ namespace Barotrauma
/// <summary>
/// Finds the closest item seeking by identifiers or tags from the world.
/// Ignores items that are outside or in another team's submarine or in a submarine that is not connected to this submarine.
/// Also ignores items that are taken by someone else.
/// Also ignores non-interactable items and items that are taken by someone else.
/// The method is run in steps for performance reasons. So you'll have to provide the reference to the itemIndex.
/// Returns false while running and true when done.
/// </summary>
@@ -1539,12 +1581,17 @@ namespace Barotrauma
{
itemIndex++;
var item = Item.ItemList[itemIndex];
if (item.NonInteractable) { continue; }
if (ignoredItems != null && ignoredItems.Contains(item)) { continue; }
if (item.Submarine == null) { continue; }
if (item.Submarine.TeamID != TeamID) { continue; }
if (Submarine != null && !Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
if (item.CurrentHull == null) { continue; }
if (ignoreBroken && item.Condition <= 0) { continue; }
if (Submarine != null)
{
if (item.Submarine.Info.Type != Submarine.Info.Type) { continue; }
if (!Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
}
if (customPredicate != null && !customPredicate(item)) { continue; }
if (identifiers != null && identifiers.None(id => item.Prefab.Identifier == id || item.HasTag(id))) { continue; }
if (ignoredContainerIdentifiers != null && item.Container != null)
@@ -1785,13 +1832,17 @@ namespace Barotrauma
if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen)
{
FocusedCharacter = CanInteract ? FindCharacterAtPosition(mouseSimPos) : null;
if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; }
float aimAssist = GameMain.Config.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f);
if (SelectedItems.Any(it => it?.GetComponent<Wire>()?.IsActive ?? false))
{
//disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes
aimAssist = 0.0f;
}
focusedItem = CanInteract ? FindItemAtPosition(mouseSimPos, aimAssist) : null;
var item = FindItemAtPosition(mouseSimPos, aimAssist);
focusedItem = CanInteract ? item : null;
findFocusedTimer = 0.05f;
}
}
@@ -1925,7 +1976,7 @@ namespace Barotrauma
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)
if (c.IsPlayer || (c.IsBot && !c.IsDead))
{
c.Enabled = true;
}
@@ -2094,12 +2145,17 @@ namespace Barotrauma
UpdateControlled(deltaTime, cam);
//Health effects
if (NeedsAir) { UpdateOxygen(deltaTime); }
if (NeedsOxygen)
{
UpdateOxygen(deltaTime);
}
CharacterHealth.Update(deltaTime);
if (IsUnconscious)
if (IsIncapacitated)
{
UpdateUnconscious();
Stun = Math.Max(5.0f, Stun);
AnimController.ResetPullJoints();
SelectedConstruction = null;
return;
}
@@ -2138,11 +2194,12 @@ namespace Barotrauma
lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f);
//ragdoll button
if (IsRagdolled)
if (IsRagdolled || !CanMove)
{
if (AnimController is HumanoidAnimController) ((HumanoidAnimController)AnimController).Crouching = false;
/*if(GameMain.Server != null)
GameMain.Server.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status });*/
if (AnimController is HumanoidAnimController)
{
((HumanoidAnimController)AnimController).Crouching = false;
}
AnimController.ResetPullJoints();
SelectedConstruction = null;
return;
@@ -2165,40 +2222,51 @@ namespace Barotrauma
SelectedConstruction = null;
}
if (!IsDead) LockHands = false;
if (!IsDead) { LockHands = false; }
}
partial void UpdateControlled(float deltaTime, Camera cam);
partial void UpdateProjSpecific(float deltaTime, Camera cam);
partial void SetOrderProjSpecific(Order order, string orderOption);
private void UpdateOxygen(float deltaTime)
{
PressureProtection -= deltaTime * 100.0f;
float hullAvailableOxygen = 0.0f;
if (!AnimController.HeadInWater && AnimController.CurrentHull != null)
if (NeedsAir)
{
//don't decrease the amount of oxygen in the hull if the character has more oxygen available than the hull
//(i.e. if the character has some external source of oxygen)
if (OxygenAvailable * 0.98f < AnimController.CurrentHull.OxygenPercentage)
PressureProtection -= deltaTime * 100.0f;
}
if (NeedsWater)
{
float waterAvailable = 100;
if (!AnimController.InWater && CurrentHull != null)
{
AnimController.CurrentHull.Oxygen -= Hull.OxygenConsumptionSpeed * deltaTime;
waterAvailable = CurrentHull.WaterPercentage;
}
hullAvailableOxygen = AnimController.CurrentHull.OxygenPercentage;
OxygenAvailable += MathHelper.Clamp(waterAvailable - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f);
}
else
{
float hullAvailableOxygen = 0.0f;
if (!AnimController.HeadInWater && AnimController.CurrentHull != null)
{
//don't decrease the amount of oxygen in the hull if the character has more oxygen available than the hull
//(i.e. if the character has some external source of oxygen)
if (OxygenAvailable * 0.98f < AnimController.CurrentHull.OxygenPercentage)
{
AnimController.CurrentHull.Oxygen -= Hull.OxygenConsumptionSpeed * deltaTime;
}
hullAvailableOxygen = AnimController.CurrentHull.OxygenPercentage;
}
OxygenAvailable += MathHelper.Clamp(hullAvailableOxygen - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f);
}
OxygenAvailable += MathHelper.Clamp(hullAvailableOxygen - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f);
}
partial void UpdateOxygenProjSpecific(float prevOxygen);
private void UpdateUnconscious()
{
Stun = Math.Max(5.0f, Stun);
AnimController.ResetPullJoints();
SelectedConstruction = null;
}
/// <summary>
/// How far the character is from the closest human player (including spectators)
/// </summary>
@@ -2231,23 +2299,44 @@ namespace Barotrauma
}
private float despawnTimer;
private const float DespawnDelay = 5.0f * 60.0f; //5 minutes
private void UpdateDespawn(float deltaTime)
{
if (!EnableDespawn) { return; }
//clients don't despawn characters unless the server says so
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
if (!IsDead) { return; }
int subCorpseCount = 0;
if (Submarine != null)
{
subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine);
if (subCorpseCount < GameMain.Config.CorpsesPerSubDespawnThreshold) { return; }
}
float distToClosestPlayer = GetDistanceToClosestPlayer();
if (distToClosestPlayer > NetConfig.DisableCharacterDist)
{
//despawn in 1 second if very far from all human players
despawnTimer = Math.Max(despawnTimer, DespawnDelay - 1.0f);
//despawn in 1 minute if very far from all human players
despawnTimer = Math.Max(despawnTimer, GameMain.Config.CorpseDespawnDelay - 60.0f);
}
despawnTimer += deltaTime;
if (despawnTimer < DespawnDelay) { return; }
float despawnPriority = 1.0f;
if (subCorpseCount > GameMain.Config.CorpsesPerSubDespawnThreshold)
{
//despawn faster if there are lots of corpses in the sub (twice as many as the threshold -> despawn twice as fast)
despawnPriority += (subCorpseCount - GameMain.Config.CorpsesPerSubDespawnThreshold) / (float)GameMain.Config.CorpsesPerSubDespawnThreshold;
}
if (AIController is EnemyAIController)
{
//enemies despawn faster
despawnPriority *= 2.0f;
}
despawnTimer += deltaTime * despawnPriority;
if (despawnTimer < GameMain.Config.CorpseDespawnDelay) { return; }
if (IsHuman)
{
@@ -2274,7 +2363,12 @@ namespace Barotrauma
if (itemContainer == null) { return; }
foreach (Item inventoryItem in Inventory.Items)
{
itemContainer.Inventory.TryPutItem(inventoryItem, user: null);
if (inventoryItem == null) { continue; }
if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null))
{
//if the item couldn't be put inside the despawn container, just drop it
inventoryItem.Drop(dropper: this);
}
}
}
}
@@ -2284,7 +2378,7 @@ namespace Barotrauma
public void DespawnNow()
{
despawnTimer = DespawnDelay;
despawnTimer = GameMain.Config.CorpseDespawnDelay;
}
public static void RemoveByPrefab(CharacterPrefab prefab)
@@ -2350,18 +2444,29 @@ namespace Barotrauma
//set the character order only if the character is close enough to hear the message
if (orderGiver != null && !CanHearCharacter(orderGiver)) { return; }
// If there's another character operating the same device, make them dismiss themself
if (order != null && order.Category == OrderCategory.Operate)
{
CharacterList.FindAll(c => c != this && c.TeamID == TeamID && c.CurrentOrder is Order characterOrder && characterOrder.Category == OrderCategory.Operate &&
characterOrder.Identifier.Equals(order.Identifier) && characterOrder.TargetEntity == order.TargetEntity)?
.ForEach(c => c.SetOrder(Order.GetPrefab("dismissed"), null, c, speak: true));
}
if (AIController is HumanAIController humanAI)
{
humanAI.SetOrder(order, orderOption, orderGiver, speak);
}
#if CLIENT
GameMain.GameSession?.CrewManager?.DisplayCharacterOrder(this, order, orderOption);
#endif
SetOrderProjSpecific(order, orderOption);
CurrentOrder = order;
CurrentOrderOption = orderOption;
}
/// <summary>
/// Reset order data so it doesn't carry into further rounds, as the AI is "recreated" always in between rounds anyway.
/// </summary>
public void ResetCurrentOrder() => Info?.ResetCurrentOrder();
private readonly List<AIChatMessage> aiChatMessageQueue = new List<AIChatMessage>();
//key = identifier, value = time the message was sent
@@ -2511,7 +2616,7 @@ namespace Barotrauma
if (attacker is Character attackingCharacter && attackingCharacter.AIController == null)
{
StringBuilder sb = new StringBuilder();
sb.Append(LogName + " attacked by " + attackingCharacter.LogName + ".");
sb.Append(GameServer.CharacterLogName(this) + " attacked by " + GameServer.CharacterLogName(attackingCharacter) + ".");
if (attackResult.Afflictions != null)
{
foreach (Affliction affliction in attackResult.Afflictions)
@@ -2632,13 +2737,19 @@ namespace Barotrauma
mainLimb.body.ApplyLinearImpulse(impulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
bool wasDead = IsDead;
Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir);
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound);
CharacterHealth.ApplyDamage(hitLimb, attackResult);
ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
if (attacker != this)
{
OnAttacked?.Invoke(attacker, attackResult);
OnAttackedProjSpecific(attacker, attackResult);
OnAttackedProjSpecific(attacker, attackResult, stun);
if (!wasDead)
{
TryAdjustAttackerSkill(attacker, -attackResult.Damage);
}
};
if (attacker != null && attackResult.Damage > 0.0f)
@@ -2649,7 +2760,31 @@ namespace Barotrauma
return attackResult;
}
partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult);
partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun);
public void TryAdjustAttackerSkill(Character attacker, float healthChange)
{
if (attacker == null) { return; }
bool isEnemy = AIController is EnemyAIController || TeamID != attacker.TeamID;
if (isEnemy)
{
if (healthChange < 0.0f)
{
float attackerSkillLevel = attacker.GetSkillLevel("weapons");
attacker.Info?.IncreaseSkillLevel("weapons",
-healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f),
attacker.WorldPosition + Vector2.UnitY * 100.0f);
}
}
else if (healthChange > 0.0f)
{
float attackerSkillLevel = attacker.GetSkillLevel("medical");
attacker.Info?.IncreaseSkillLevel("medical",
healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f),
attacker.WorldPosition + Vector2.UnitY * 100.0f);
}
}
public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false)
{
@@ -2732,7 +2867,7 @@ namespace Barotrauma
partial void ImplodeFX();
public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false)
public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true)
{
if (IsDead || CharacterHealth.Unkillable) { return; }
@@ -2775,9 +2910,9 @@ namespace Barotrauma
SteamAchievementManager.OnCharacterKilled(this, CauseOfDeath);
KillProjSpecific(causeOfDeath, causeOfDeathAffliction);
KillProjSpecific(causeOfDeath, causeOfDeathAffliction, log);
if (info != null) info.CauseOfDeath = CauseOfDeath;
if (info != null) { info.CauseOfDeath = CauseOfDeath; }
AnimController.movement = Vector2.Zero;
AnimController.TargetMovement = Vector2.Zero;
@@ -2800,7 +2935,7 @@ namespace Barotrauma
GameMain.GameSession.KillCharacter(this);
}
}
partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction);
partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log);
public void Revive()
{
@@ -2930,13 +3065,13 @@ namespace Barotrauma
public IEnumerable<AttackContext> GetAttackContexts()
{
currentContexts.Clear();
if (AnimController.CurrentAnimationParams.IsGroundedAnimation)
if (AnimController.InWater)
{
currentContexts.Add(AttackContext.Ground);
currentContexts.Add(AttackContext.Water);
}
else
{
currentContexts.Add(AttackContext.Water);
currentContexts.Add(AttackContext.Ground);
}
if (CurrentHull == null)
{
@@ -293,6 +293,9 @@ namespace Barotrauma
private NPCPersonalityTrait personalityTrait;
public Order CurrentOrder { get; set;}
public string CurrentOrderOption { get; set; }
//unique ID given to character infos in MP
//used by clients to identify which infos are the same to prevent duplicate characters in round summary
public ushort ID;
@@ -524,9 +527,11 @@ namespace Barotrauma
}
foreach (XElement subElement in infoElement.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "job") continue;
Job = new Job(subElement);
break;
if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase))
{
Job = new Job(subElement);
break;
}
}
LoadHeadAttachments();
}
@@ -661,7 +666,7 @@ namespace Barotrauma
{
foreach (XElement limbElement in Ragdoll.MainElement.Elements())
{
if (limbElement.GetAttributeString("type", "").ToLowerInvariant() != "head") { continue; }
if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; }
XElement spriteElement = limbElement.Element("sprite");
if (spriteElement == null) { continue; }
@@ -677,7 +682,7 @@ namespace Barotrauma
//go through the files in the directory to find a matching sprite
foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath)))
{
if (!file.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase))
if (!file.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
{
continue;
}
@@ -828,6 +833,11 @@ namespace Barotrauma
{
if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; }
if (Job.Prefab.Identifier == "assistant")
{
increase *= SkillSettings.Current.AssistantSkillIncreaseMultiplier;
}
float prevLevel = Job.GetSkillLevel(skillIdentifier);
Job.IncreaseSkillLevel(skillIdentifier, increase);
@@ -955,7 +965,7 @@ namespace Barotrauma
foreach (XElement childInvElement in itemElement.Elements())
{
if (itemContainerIndex >= itemContainers.Count) break;
if (childInvElement.Name.ToString().ToLowerInvariant() != "inventory") continue;
if (!childInvElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; }
SpawnInventoryItemsRecursive(itemContainers[itemContainerIndex].Inventory, childInvElement);
itemContainerIndex++;
}
@@ -987,6 +997,15 @@ namespace Barotrauma
faceAttachments = null;
}
/// <summary>
/// Reset order data so it doesn't carry into further rounds, as the AI is "recreated" always in between rounds anyway.
/// </summary>
public void ResetCurrentOrder()
{
CurrentOrder = null;
CurrentOrderOption = "";
}
public void Remove()
{
Character = null;
@@ -19,7 +19,7 @@ namespace Barotrauma
public virtual float Strength
{
get { return _strength; }
set { _strength = value; }
set { _strength = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength); }
}
[Serialize("", true), Editable]
@@ -184,6 +184,8 @@ namespace Barotrauma
{
_strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab.Identifier));
}
// Don't use the property, because its virtual and some afflictions like husk overload it for external use.
_strength = MathHelper.Clamp(_strength, 0.0f, Prefab.MaxStrength);
foreach (StatusEffect statusEffect in currentEffect.StatusEffects)
{
@@ -101,9 +101,9 @@ namespace Barotrauma
{
float random = Rand.Value(Rand.RandSync.Server);
huskInfection.Clear();
huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * deltaTime / character.AnimController.Limbs.Length));
huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / character.AnimController.Limbs.Length));
character.LastDamageSource = null;
float force = applyForce ? random * 0.1f * limb.Mass : 0;
float force = applyForce ? random * 0.5f * limb.Mass : 0;
character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, force);
}
}
@@ -265,7 +265,8 @@ namespace Barotrauma
public readonly Sprite Icon;
public readonly Color[] IconColors;
private List<Effect> effects = new List<Effect>();
private readonly List<Effect> effects = new List<Effect>();
public IEnumerable<Effect> Effects => effects;
private readonly string typeName;
@@ -114,6 +114,10 @@ namespace Barotrauma
private List<LimbHealth> limbHealths = new List<LimbHealth>();
//non-limb-specific afflictions
private List<Affliction> afflictions = new List<Affliction>();
/// <summary>
/// Note: returns only the non-limb-secific afflictions. Use GetAllAfflictions or some other method for getting also the limb-specific afflictions.
/// </summary>
public IEnumerable<Affliction> Afflictions => afflictions;
private HashSet<Affliction> irremovableAfflictions = new HashSet<Affliction>();
private Affliction bloodlossAffliction;
@@ -160,12 +164,12 @@ namespace Barotrauma
{
get
{
if (!Character.NeedsAir || Unkillable) return 100.0f;
if (!Character.NeedsOxygen || Unkillable) { return 100.0f; }
return -oxygenLowAffliction.Strength + 100;
}
set
{
if (!Character.NeedsAir || Unkillable) return;
if (!Character.NeedsOxygen || Unkillable) { return; }
oxygenLowAffliction.Strength = MathHelper.Clamp(-value + 100, 0.0f, 200.0f);
}
}
@@ -216,7 +220,7 @@ namespace Barotrauma
limbHealths.Clear();
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "limb") continue;
if (!subElement.Name.ToString().Equals("limb", StringComparison.OrdinalIgnoreCase)) { continue; }
limbHealths.Add(new LimbHealth(subElement, this));
}
if (limbHealths.Count == 0)
@@ -269,30 +273,6 @@ namespace Barotrauma
}
}
public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true)
{
foreach (Affliction affliction in afflictions)
{
if (affliction.Prefab.Identifier == identifier) return affliction;
}
if (!allowLimbAfflictions) return null;
foreach (LimbHealth limbHealth in limbHealths)
{
foreach (Affliction affliction in limbHealth.Afflictions)
{
if (affliction.Prefab.Identifier == identifier) return affliction;
}
}
return null;
}
public T GetAffliction<T>(string identifier, bool allowLimbAfflictions = true) where T : Affliction
{
return GetAffliction(identifier, allowLimbAfflictions) as T;
}
public IEnumerable<Affliction> GetAfflictionsByType(string afflictionType, Limb limb)
{
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
@@ -304,6 +284,37 @@ namespace Barotrauma
return limbHealths[limb.HealthIndex].Afflictions.Where(a => a.Prefab.AfflictionType == afflictionType);
}
public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true)
=> GetAffliction(a => a.Prefab.Identifier == identifier, allowLimbAfflictions);
public Affliction GetAfflictionOfType(string afflictionType, bool allowLimbAfflictions = true)
=> GetAffliction(a => a.Prefab.AfflictionType == afflictionType, allowLimbAfflictions);
private Affliction GetAffliction(Func<Affliction, bool> predicate, bool allowLimbAfflictions = true)
{
foreach (Affliction affliction in afflictions)
{
if (predicate(affliction)) { return affliction; }
}
if (!allowLimbAfflictions)
{
return null;
}
foreach (LimbHealth limbHealth in limbHealths)
{
foreach (Affliction affliction in limbHealth.Afflictions)
{
if (predicate(affliction)) { return affliction; }
}
}
return null;
}
public T GetAffliction<T>(string identifier, bool allowLimbAfflictions = true) where T : Affliction
{
return GetAffliction(identifier, allowLimbAfflictions) as T;
}
public Affliction GetAffliction(string identifier, Limb limb)
{
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
@@ -408,12 +419,11 @@ namespace Barotrauma
return resistance;
}
private readonly List<Affliction> matchingAfflictions = new List<Affliction>();
public void ReduceAffliction(Limb targetLimb, string affliction, float amount)
{
affliction = affliction.ToLowerInvariant();
List<Affliction> matchingAfflictions = new List<Affliction>(afflictions);
matchingAfflictions.Clear();
matchingAfflictions.AddRange(afflictions);
if (targetLimb != null)
{
matchingAfflictions.AddRange(limbHealths[targetLimb.HealthIndex].Afflictions);
@@ -426,8 +436,8 @@ namespace Barotrauma
}
}
matchingAfflictions.RemoveAll(a =>
a.Prefab.Identifier.ToLowerInvariant() != affliction &&
a.Prefab.AfflictionType.ToLowerInvariant() != affliction);
!a.Prefab.Identifier.Equals(affliction, StringComparison.OrdinalIgnoreCase) &&
!a.Prefab.AfflictionType.Equals(affliction, StringComparison.OrdinalIgnoreCase));
if (matchingAfflictions.Count == 0) return;
@@ -526,7 +536,7 @@ namespace Barotrauma
private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction)
{
if (!DoesBleed && newAffliction is AfflictionBleeding) return;
if (!Character.NeedsAir && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return;
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return;
foreach (Affliction affliction in limbHealth.Afflictions)
{
@@ -559,7 +569,7 @@ namespace Barotrauma
private void AddAffliction(Affliction newAffliction)
{
if (!DoesBleed && newAffliction is AfflictionBleeding) return;
if (!Character.NeedsAir && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return;
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return;
if (newAffliction.Prefab.AfflictionType == "huskinfection")
{
var huskPrefab = newAffliction.Prefab as AfflictionPrefabHusk;
@@ -653,7 +663,7 @@ namespace Barotrauma
private void UpdateOxygen(float deltaTime)
{
if (!Character.NeedsAir) return;
if (!Character.NeedsOxygen) { return; }
float prevOxygen = OxygenAmount;
if (IsUnconscious)
@@ -691,13 +701,15 @@ namespace Barotrauma
foreach (Affliction affliction in limbHealth.Afflictions)
{
float vitalityDecrease = affliction.GetVitalityDecrease(this);
if (limbHealth.VitalityMultipliers.ContainsKey(affliction.Prefab.Identifier.ToLowerInvariant()))
string identifier = affliction.Prefab.Identifier.ToLowerInvariant();
string type = affliction.Prefab.AfflictionType.ToLowerInvariant();
if (limbHealth.VitalityMultipliers.ContainsKey(identifier))
{
vitalityDecrease *= limbHealth.VitalityMultipliers[affliction.Prefab.Identifier.ToLowerInvariant()];
vitalityDecrease *= limbHealth.VitalityMultipliers[identifier];
}
if (limbHealth.VitalityTypeMultipliers.ContainsKey(affliction.Prefab.AfflictionType.ToLowerInvariant()))
if (limbHealth.VitalityTypeMultipliers.ContainsKey(type))
{
vitalityDecrease *= limbHealth.VitalityTypeMultipliers[affliction.Prefab.AfflictionType.ToLowerInvariant()];
vitalityDecrease *= limbHealth.VitalityTypeMultipliers[type];
}
vitalityDecrease *= damageResistanceMultiplier;
Vitality -= vitalityDecrease;
@@ -750,6 +762,7 @@ namespace Barotrauma
return new Pair<CauseOfDeathType, Affliction>(causeOfDeath, strongestAffliction);
}
// TODO: this method is called a lot (every half second) -> optimize, don't create new class instances and lists every time!
private List<Affliction> GetAllAfflictions(bool mergeSameAfflictions)
{
List<Affliction> allAfflictions = new List<Affliction>(afflictions);
@@ -832,10 +845,18 @@ namespace Barotrauma
}
}
private readonly List<Affliction> activeAfflictions = new List<Affliction>();
private readonly List<Pair<LimbHealth, Affliction>> limbAfflictions = new List<Pair<LimbHealth, Affliction>>();
public void ServerWrite(IWriteMessage msg)
{
List<Affliction> activeAfflictions = afflictions.FindAll(a => a.Strength > 0.0f && a.Strength >= a.Prefab.ActivationThreshold);
activeAfflictions.Clear();
foreach (var affliction in afflictions)
{
if (affliction.Strength > 0.0f && affliction.Strength >= affliction.Prefab.ActivationThreshold)
{
activeAfflictions.Add(affliction);
}
}
msg.Write((byte)activeAfflictions.Count);
foreach (Affliction affliction in activeAfflictions)
{
@@ -845,7 +866,7 @@ namespace Barotrauma
0.0f, affliction.Prefab.MaxStrength, 8);
}
List<Pair<LimbHealth, Affliction>> limbAfflictions = new List<Pair<LimbHealth, Affliction>>();
limbAfflictions.Clear();
foreach (LimbHealth limbHealth in limbHealths)
{
foreach (Affliction limbAffliction in limbHealth.Afflictions)
@@ -19,6 +19,13 @@ namespace Barotrauma
private set;
}
[Serialize(1.0f, false), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 1)]
public float ProbabilityMultiplier
{
get;
private set;
}
[Serialize("0.0,360", false), Editable]
public Vector2 ArmorSector
{
@@ -62,7 +62,7 @@ namespace Barotrauma
skills = new Dictionary<string, Skill>();
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().ToLowerInvariant() != "skill") { continue; }
if (!subElement.Name.ToString().Equals("skill", System.StringComparison.OrdinalIgnoreCase)) { continue; }
string skillIdentifier = subElement.GetAttributeString("identifier", "");
if (string.IsNullOrEmpty(skillIdentifier)) { continue; }
skills.Add(
@@ -80,8 +80,8 @@ namespace Barotrauma
public float GetSkillLevel(string skillIdentifier)
{
if (string.IsNullOrWhiteSpace(skillIdentifier)) { return 0.0f; }
skills.TryGetValue(skillIdentifier, out Skill skill);
return (skill == null) ? 0.0f : skill.Level;
}
@@ -146,6 +146,7 @@ namespace Barotrauma
private set;
}
// TODO: not used
[Serialize(10.0f, false)]
public float Commonness
{
@@ -262,7 +263,7 @@ namespace Barotrauma
}
foreach (XElement element in mainElement.Elements())
{
if (element.Name.ToString().ToLowerInvariant() == "nojob") { continue; }
if (element.Name.ToString().Equals("nojob", StringComparison.OrdinalIgnoreCase)) { continue; }
if (element.IsOverride())
{
var job = new JobPrefab(element.FirstElement(), file.Path)
@@ -366,6 +366,8 @@ namespace Barotrauma
public string Name => Params.Name;
public bool IsDead => character.IsDead;
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
@@ -471,43 +473,59 @@ namespace Barotrauma
return AddDamage(simPosition, afflictions, playSound);
}
private readonly List<DamageModifier> appliedDamageModifiers = new List<DamageModifier>();
private readonly List<Affliction> afflictionsCopy = new List<Affliction>();
public AttackResult AddDamage(Vector2 simPosition, IEnumerable<Affliction> afflictions, bool playSound)
{
List<DamageModifier> appliedDamageModifiers = new List<DamageModifier>();
//create a copy of the original affliction list to prevent modifying the afflictions of an Attack/StatusEffect etc
var afflictionsCopy = afflictions.Where(a => Rand.Range(0.0f, 1.0f) <= a.Probability).ToList();
for (int i = 0; i < afflictionsCopy.Count; i++)
appliedDamageModifiers.Clear();
afflictionsCopy.Clear();
foreach (var affliction in afflictions)
{
var newAffliction = affliction;
float random = Rand.Value(Rand.RandSync.Unsynced);
if (random > affliction.Probability) { continue; }
bool applyAffliction = true;
foreach (DamageModifier damageModifier in DamageModifiers)
{
if (!damageModifier.MatchesAffliction(afflictionsCopy[i])) continue;
if (!damageModifier.MatchesAffliction(affliction)) { continue; }
if (random > affliction.Probability * damageModifier.ProbabilityMultiplier)
{
applyAffliction = false;
continue;
}
if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition))
{
afflictionsCopy[i] = afflictionsCopy[i].CreateMultiplied(damageModifier.DamageMultiplier);
newAffliction = affliction.CreateMultiplied(damageModifier.DamageMultiplier);
appliedDamageModifiers.Add(damageModifier);
}
}
foreach (WearableSprite wearable in wearingItems)
{
foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers)
{
if (!damageModifier.MatchesAffliction(afflictionsCopy[i])) continue;
if (!damageModifier.MatchesAffliction(affliction)) { continue; }
if (random > affliction.Probability * damageModifier.ProbabilityMultiplier)
{
applyAffliction = false;
continue;
}
if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition))
{
afflictionsCopy[i] = afflictionsCopy[i].CreateMultiplied(damageModifier.DamageMultiplier);
newAffliction = affliction.CreateMultiplied(damageModifier.DamageMultiplier);
appliedDamageModifiers.Add(damageModifier);
}
}
}
if (applyAffliction)
{
afflictionsCopy.Add(newAffliction);
}
}
AddDamageProjSpecific(simPosition, afflictionsCopy, playSound, appliedDamageModifiers);
AddDamageProjSpecific(afflictionsCopy, playSound, appliedDamageModifiers);
return new AttackResult(afflictionsCopy, this, appliedDamageModifiers);
}
partial void AddDamageProjSpecific(Vector2 simPosition, List<Affliction> afflictions, bool playSound, List<DamageModifier> appliedDamageModifiers);
partial void AddDamageProjSpecific(IEnumerable<Affliction> afflictions, bool playSound, IEnumerable<DamageModifier> appliedDamageModifiers);
public bool SectorHit(Vector2 armorSector, Vector2 simPosition)
{
@@ -646,33 +664,19 @@ namespace Barotrauma
if (wasHit)
{
bool playSound = false;
#if CLIENT
playSound = LastAttackSoundTime < Timing.TotalTime - SoundInterval;
if (playSound)
if (character == Character.Controlled || GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
LastAttackSoundTime = SoundInterval;
ExecuteAttack(damageTarget, targetLimb, out attackResult);
}
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(character, new object[]
{
NetEntityEvent.Type.ExecuteAttack,
this,
(damageTarget as Entity)?.ID ?? Entity.NullEntityID,
damageTarget is Character && targetLimb != null ? Array.IndexOf(((Character)damageTarget).AnimController.Limbs, targetLimb) : 0
});
#endif
if (damageTarget is Character targetCharacter && targetLimb != null)
{
attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound);
}
else
{
attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound);
}
if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.Server))
{
// TODO: use the hit pos?
var localFront = body.GetLocalFront(Params.GetSpriteOrientation());
var from = body.FarseerBody.GetWorldPoint(localFront);
var to = from;
var drawPos = body.DrawPosition;
StickTo(structureBody, from, to);
}
attack.ResetAttackTimer();
attack.SetCoolDown();
}
Vector2 diff = attackSimPos - SimPosition;
@@ -705,11 +709,49 @@ namespace Barotrauma
if (!attack.IsRunning)
{
// Set the main collider where the body lands after the attack
character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: 0);
character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation);
}
return wasHit;
}
public void ExecuteAttack(IDamageable damageTarget, Limb targetLimb, out AttackResult attackResult)
{
bool playSound = false;
#if CLIENT
playSound = LastAttackSoundTime < Timing.TotalTime - SoundInterval;
if (playSound)
{
LastAttackSoundTime = SoundInterval;
}
#endif
if (damageTarget is Character targetCharacter && targetLimb != null)
{
attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound);
}
else
{
if (damageTarget is Item targetItem && !targetItem.Prefab.DamagedByMonsters)
{
attackResult = new AttackResult();
}
else
{
attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound);
}
}
/*if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.Server))
{
// TODO: use the hit pos?
var localFront = body.GetLocalFront(Params.GetSpriteOrientation());
var from = body.FarseerBody.GetWorldPoint(localFront);
var to = from;
var drawPos = body.DrawPosition;
StickTo(structureBody, from, to);
}*/
attack.ResetAttackTimer();
attack.SetCoolDown();
}
private WeldJoint attachJoint;
private WeldJoint colliderJoint;
public bool IsStuck => attachJoint != null;
@@ -125,7 +125,7 @@ namespace Barotrauma
public static string GetFolder(XDocument doc, string filePath)
{
var folder = doc.Root?.Element("animations")?.GetAttributeString("folder", string.Empty);
if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default")
if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
folder = Path.Combine(Path.GetDirectoryName(filePath), "Animations");
}
@@ -198,7 +198,7 @@ namespace Barotrauma
}
else
{
selectedFile = filteredFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).ToLowerInvariant() == fileName.ToLowerInvariant());
selectedFile = filteredFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
if (selectedFile == null)
{
DebugConsole.ThrowError($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations.");
@@ -40,6 +40,9 @@ namespace Barotrauma
[Serialize(false, true), Editable]
public bool NeedsAir { get; set; }
[Serialize(false, true, description: "Can the creature live without water or does it die on dry land?"), Editable]
public bool NeedsWater { get; set; }
[Serialize(false, true), Editable]
public bool CanSpeak { get; set; }
@@ -52,12 +55,21 @@ namespace Barotrauma
[Serialize("blood", true), Editable]
public string BloodDecal { get; private set; }
[Serialize("blooddrop", true), Editable]
public string BleedParticleAir { get; private set; }
[Serialize("waterblood", true), Editable]
public string BleedParticleWater { get; private set; }
[Serialize(10f, true, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)]
public float EatingSpeed { get; set; }
[Serialize(1f, true, "Decreases the intensive path finding call frequency. Set to a lower value for insignificant creatures to improve performance."), Editable(minValue: 0f, maxValue: 1f)]
public float PathFinderPriority { get; set; }
[Serialize(false, true), Editable]
public bool HideInSonar { get; set; }
public readonly string File;
public readonly List<SubParam> SubParams = new List<SubParam>();
@@ -462,6 +474,12 @@ namespace Barotrauma
[Serialize(true, true, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable()]
public bool EnforceAggressiveBehaviorForMissions { get; private set; }
[Serialize(false, true, description: "Should the character target or ignore walls when it's inside the submarine. Doesn't have any effect if no target priority for walls is defined."), Editable()]
public bool TargetInnerWalls { get; private set; }
[Serialize(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; }
// TODO: latchonto, swarming
public IEnumerable<TargetParams> Targets => targets;
@@ -94,8 +94,7 @@ namespace Barotrauma
public static string GetFolder(string speciesName, ContentPackage contentPackage = null)
{
CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier.ToLowerInvariant()==speciesName.ToLowerInvariant() &&
(contentPackage==null || p.ContentPackage == contentPackage));
CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier.Equals(speciesName, StringComparison.OrdinalIgnoreCase) && (contentPackage == null || p.ContentPackage == contentPackage));
if (prefab?.XDocument == null)
{
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}' (content package {contentPackage?.Name ?? "null"})");
@@ -107,7 +106,7 @@ namespace Barotrauma
public static string GetFolder(XDocument doc, string filePath)
{
var folder = doc.Root?.Element("ragdolls")?.GetAttributeString("folder", string.Empty);
if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default")
if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
folder = Path.Combine(Path.GetDirectoryName(filePath), "Ragdolls") + Path.DirectorySeparatorChar;
}
@@ -150,7 +149,7 @@ namespace Barotrauma
}
else
{
selectedFile = files.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).ToLowerInvariant() == fileName.ToLowerInvariant());
selectedFile = files.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
if (selectedFile == null)
{
DebugConsole.ThrowError($"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.");
@@ -814,6 +813,15 @@ namespace Barotrauma
[Serialize("", true), Editable()]
public string Texture { get; set; }
[Serialize("1.0,1.0,1.0,1.0", true), Editable()]
public Color Color { get; set; }
[Serialize("1.0,1.0,1.0,1.0", true, description: "Target color when the character is dead."), Editable()]
public Color DeadColor { get; set; }
[Serialize(0f, true, "How long it takes to fade into the dead color? 0 = Not applied."), Editable(DecimalCount = 1, MinValueFloat = 0, MaxValueFloat = 10)]
public float DeadColorTime { get; set; }
public override string Name => "Sprite";
public SpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
@@ -65,6 +65,37 @@ namespace Barotrauma
set { skillIncreasePerFabricatorRequiredSkill = value; }
}
private float skillIncreasePerHostileDamage;
[Serialize(0.01f, true)]
public float SkillIncreasePerHostileDamage
{
get { return skillIncreasePerHostileDamage * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerHostileDamage = value; }
}
private float skillIncreasePerSecondWhenOperatingTurret;
[Serialize(0.001f, true)]
public float SkillIncreasePerSecondWhenOperatingTurret
{
get { return skillIncreasePerSecondWhenOperatingTurret * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerSecondWhenOperatingTurret = value; }
}
private float skillIncreasePerFriendlyHealed;
[Serialize(0.001f, true)]
public float SkillIncreasePerFriendlyHealed
{
get { return skillIncreasePerFriendlyHealed * GetCurrentSkillGainMultiplier(); }
set { skillIncreasePerFriendlyHealed = value; }
}
[Serialize(1.1f, true)]
public float AssistantSkillIncreaseMultiplier
{
get;
set;
}
private SkillSettings(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);