Faction Test v1.0.1.0

This commit is contained in:
Regalis11
2023-02-16 15:01:28 +02:00
parent caa5a2f762
commit 2c5a7923b0
309 changed files with 7502 additions and 4335 deletions
@@ -352,21 +352,6 @@ namespace Barotrauma
}
}
public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
{
if (myTeam == otherTeam) { return true; }
return myTeam switch
{
// NPCs are friendly to the same team and the friendly NPCs
CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC,
// Friendly NPCs are friendly to both player teams
CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2,
_ => true
};
}
public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID);
public void ReequipUnequipped()
{
foreach (var item in unequippedItems)
@@ -460,7 +445,7 @@ namespace Barotrauma
if (EscapeTarget != null)
{
var door = EscapeTarget.ConnectedDoor;
bool isClosedDoor = door != null && !door.IsOpen;
bool isClosedDoor = door != null && door.IsClosed;
Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition;
float sqrDist = diff.LengthSquared();
bool isClose = sqrDist < MathUtils.Pow2(100);
@@ -206,13 +206,19 @@ namespace Barotrauma
private set;
} = new HashSet<Submarine>();
public bool IsTargetingPlayerTeam => IsTargetInPlayerTeam(SelectedAiTarget);
public static bool IsTargetBeingChasedBy(Character target, Character character)
=> character?.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity == target && (enemyAI.State == AIState.Attack || enemyAI.State == AIState.Aggressive);
public bool IsBeingChasedBy(Character c) => IsTargetBeingChasedBy(Character, c);
private bool IsBeingChased => IsBeingChasedBy(SelectedAiTarget?.Entity as Character);
private bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam;
private static bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam;
private bool IsAttackingOwner(Character other) =>
PetBehavior != null && PetBehavior.Owner != null &&
!other.IsUnconscious && !other.IsArrested &&
other.AIController is HumanAIController humanAI &&
humanAI.ObjectiveManager.CurrentObjective is AIObjectiveCombat combat &&
combat.Enemy != null && combat.Enemy == PetBehavior.Owner;
private bool reverse;
public bool Reverse
@@ -308,11 +314,10 @@ namespace Barotrauma
insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors);
steeringManager = outsideSteering;
State = AIState.Idle;
requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
myBodies.Add(Character.AnimController.Collider.FarseerBody);
CreatureMetrics.UnlockInEditor(Character.SpeciesName);
}
private CharacterParams.AIParams _aiParams;
@@ -354,7 +359,7 @@ namespace Barotrauma
{
targetingTag = "owner";
}
else if (targetCharacter.AIController is HumanAIController && !IsOnFriendlyTeam(Character, targetCharacter))
else if (PetBehavior != null && (!Character.IsOnFriendlyTeam(targetCharacter) || IsAttackingOwner(targetCharacter)))
{
targetingTag = "hostile";
}
@@ -683,19 +688,22 @@ namespace Barotrauma
{
if (SelectedAiTarget.Entity is Character targetCharacter)
{
bool IsValid(Character.Attacker a)
bool ShouldRetaliate(Character.Attacker a)
{
Character c = a.Character;
if (c.IsDead || c.Removed) { return false; }
if (!Character.IsFriendly(c)) { return true; }
if (!c.IsPlayer) { return false; }
// Only apply the threshold to players
return a.Damage >= selectedTargetingParams.Threshold;
if (c == null || c.IsUnconscious || c.Removed) { return false; }
// Can't target characters of same species/group because that would make us hostile to all friendly characters in the same species/group.
if (Character.IsSameSpeciesOrGroup(c)) { return false; }
if (targetCharacter.IsSameSpeciesOrGroup(c)) { return false; }
if (c.IsPlayer || Character.IsOnFriendlyTeam(c))
{
return a.Damage >= selectedTargetingParams.Threshold;
}
return true;
}
Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character;
if (attacker?.AiTarget != null && !Character.IsSameSpeciesOrGroup(attacker) && !targetCharacter.IsSameSpeciesOrGroup(attacker))
Character attacker = targetCharacter.LastAttackers.LastOrDefault(ShouldRetaliate)?.Character;
if (attacker?.AiTarget != null)
{
// Can't retaliate on characters of same species or group because that would make us hostile to all friendly characters in the same group.
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
SelectTarget(attacker.AiTarget);
State = AIState.Attack;
@@ -869,7 +877,7 @@ namespace Barotrauma
var pathSteering = SteeringManager as IndoorsSteeringManager;
if (pathSteering == null)
{
if (SimPosition.Y < ConvertUnits.ToSimUnits(Character.CharacterHealth.CrushDepth * 0.75f))
if (Level.Loaded != null && Level.Loaded.GetRealWorldDepth(WorldPosition.Y) > Character.CharacterHealth.CrushDepth * 0.75f)
{
// Steer straight up if very deep
SteeringManager.SteeringManual(deltaTime, Vector2.UnitY);
@@ -1503,7 +1511,7 @@ namespace Barotrauma
{
hitTarget = limb.character;
}
if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget))
if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget) && !IsAttackingOwner(hitTarget))
{
return true;
}
@@ -2413,7 +2421,7 @@ namespace Barotrauma
{
t = limb.character;
}
if (t != null && (t == target || !Character.IsFriendly(t)))
if (t != null && (t == target || (!Character.IsFriendly(t) || IsAttackingOwner(t))))
{
return true;
}
@@ -3062,7 +3070,8 @@ namespace Barotrauma
// In the attack state allow going into non-allowed zone only when chasing a target.
if (State == targetParams.State && SelectedAiTarget == aiTarget) { break; }
}
if (!IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _))
bool insideSameSub = aiTarget?.Entity?.Submarine != null && aiTarget.Entity.Submarine == Character.Submarine;
if (!insideSameSub && !IsPositionInsideAllowedZone(aiTarget.WorldPosition, out _))
{
// If we have recently been damaged by the target (or another player/bot in the same team) allow targeting it even when we are in the idle state.
bool isTargetInPlayerTeam = IsTargetInPlayerTeam(aiTarget);
@@ -310,7 +310,7 @@ namespace Barotrauma
UseIndoorSteeringOutside = false;
}
if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID))
if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID))
{
// Spot enemies while staying outside or inside an enemy ship.
// does not apply for escorted characters, such as prisoners or terrorists who have their own behavior
@@ -444,7 +444,7 @@ namespace Barotrauma
if (objectiveManager.CurrentObjective == null) { return; }
objectiveManager.DoCurrentObjective(deltaTime);
bool run = objectiveManager.CurrentObjective.ForceRun || !objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority;
bool run = (objectiveManager.CurrentObjective.ForceRun && !objectiveManager.CurrentObjective.ForceWalk) || (!objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority);
if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null)
{
if (Character.CurrentHull == null)
@@ -541,12 +541,12 @@ namespace Barotrauma
if (Character.LockHands) { return; }
if (ObjectiveManager.CurrentObjective == null) { return; }
if (Character.CurrentHull == null) { return; }
bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold && Character.NeedsOxygen;
bool shouldActOnSuffocation = Character.IsLowInOxygen && !Character.AnimController.HeadInWater && HasDivingSuit(Character, requireOxygenTank: false) && !HasItem(Character, AIObjectiveFindDivingGear.OXYGEN_SOURCE, out _, conditionPercentage: 1);
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>();
bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
{
if (!Character.NeedsAir) { return false; }
if (Character.IsImmuneToPressure) { return false; }
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
Hull targetHull = gotoObjective.GetTargetHull();
return gotoObjective.Target != null && targetHull == null ||
@@ -566,17 +566,17 @@ namespace Barotrauma
gotoObjective.Abandon = true;
}
}
if (!oxygenLow)
if (!shouldActOnSuffocation)
{
return;
}
}
// Diving gear
if (oxygenLow || findItemState != FindItemState.OtherItem)
if (shouldActOnSuffocation || findItemState != FindItemState.OtherItem)
{
bool needsGear = NeedsDivingGear(Character.CurrentHull, out _);
if (!needsGear || oxygenLow)
if (!needsGear || shouldActOnSuffocation)
{
bool isCurrentObjectiveFindSafety = ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>();
bool shouldKeepTheGearOn =
@@ -584,21 +584,21 @@ namespace Barotrauma
Character.AnimController.InWater ||
Character.AnimController.HeadInWater ||
Character.Submarine == null ||
(Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) ||
(!Character.IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted) ||
ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) ||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 ||
Character.CurrentHull.IsWetRoom;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character;
bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait();
if (oxygenLow && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1))
if (shouldActOnSuffocation && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1))
{
shouldKeepTheGearOn = false;
// Remove the suit before we pass out
removeDivingSuit = true;
}
bool takeMaskOff = !shouldKeepTheGearOn;
if (!shouldKeepTheGearOn && !oxygenLow)
if (!shouldKeepTheGearOn && !shouldActOnSuffocation)
{
if (ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
{
@@ -621,9 +621,10 @@ namespace Barotrauma
}
else if (gotoObjective.Mimic)
{
bool targetHasDivingGear = HasDivingGear(gotoObjective.Target as Character, requireOxygenTank: false);
if (!removeSuit)
{
removeDivingSuit = !HasDivingSuit(gotoObjective.Target as Character);
removeDivingSuit = !targetHasDivingGear;
if (removeDivingSuit)
{
removeSuit = true;
@@ -631,7 +632,7 @@ namespace Barotrauma
}
if (!removeMask)
{
takeMaskOff = !HasDivingMask(gotoObjective.Target as Character);
takeMaskOff = !targetHasDivingGear;
if (takeMaskOff)
{
removeMask = true;
@@ -647,7 +648,7 @@ namespace Barotrauma
var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR);
if (divingSuit != null && !divingSuit.HasTag(AIObjectiveFindDivingGear.DIVING_GEAR_WEARABLE_INDOORS))
{
if (oxygenLow || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
divingSuit.Drop(Character);
HandleRelocation(divingSuit);
@@ -783,20 +784,23 @@ namespace Barotrauma
private void HandleRelocation(Item item)
{
if (item.Submarine?.TeamID == CharacterTeamType.FriendlyNPC)
if (item.SpawnedInCurrentOutpost) { return; }
if (item.Submarine == null) { return; }
// Only affects bots in the player team
if (!Character.IsOnPlayerTeam) { return; }
// Don't relocate if the item is on a sub of the same team
if (item.Submarine.TeamID == Character.TeamID) { return; }
if (itemsToRelocate.Contains(item)) { return; }
itemsToRelocate.Add(item);
if (item.Submarine.ConnectedDockingPorts.TryGetValue(Submarine.MainSub, out DockingPort myPort))
{
if (itemsToRelocate.Contains(item)) { return; }
itemsToRelocate.Add(item);
if (item.Submarine.ConnectedDockingPorts.TryGetValue(Submarine.MainSub, out DockingPort myPort))
{
myPort.OnUnDocked += Relocate;
}
var campaign = GameMain.GameSession.Campaign;
if (campaign != null)
{
// In the campaign mode, undocking happens after leaving the outpost, so we can't use that.
campaign.BeforeLevelLoading += Relocate;
}
myPort.OnUnDocked += Relocate;
}
var campaign = GameMain.GameSession.Campaign;
if (campaign != null)
{
// In the campaign mode, undocking happens after leaving the outpost, so we can't use that.
campaign.BeforeLevelLoading += Relocate;
}
void Relocate()
@@ -982,15 +986,15 @@ namespace Barotrauma
if (target.CurrentHull != hull) { continue; }
if (AIObjectiveRescueAll.IsValidTarget(target, Character))
{
if (AddTargets<AIObjectiveRescueAll, Character>(Character, target) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
if (AddTargets<AIObjectiveRescueAll, Character>(Character, target) && newOrder == null && (!Character.IsMedic || Character == target) && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
{
var orderPrefab = OrderPrefab.Prefabs["requestfirstaid"];
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
targetHull = hull;
}
}
}
foreach (Item item in Item.ItemList)
}
foreach (Item item in Item.RepairableItems)
{
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, Character))
@@ -1161,7 +1165,7 @@ namespace Barotrauma
freezeAI = true;
}
}
if (attacker == null || attacker.IsDead || attacker.Removed)
if (attacker == null || attacker.IsUnconscious || attacker.Removed)
{
// Don't react to the damage if there's no attacker.
// We might consider launching the retreat combat objective in some cases, so that the bot does not just stand somewhere getting damaged and dying.
@@ -1199,7 +1203,7 @@ namespace Barotrauma
return;
}
float cumulativeDamage = realDamage + Character.GetDamageDoneByAttacker(attacker);
bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null;
bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && attacker.CombatAction == null;
if (isAccidental)
{
if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold)
@@ -1209,7 +1213,7 @@ namespace Barotrauma
}
else
{
isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength("alieninfection") > 0;
isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.AlienInfectedType) > 0;
// Inform other NPCs
if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold)
{
@@ -1279,7 +1283,7 @@ namespace Barotrauma
if (otherCharacter.Submarine != attacker.Submarine) { continue; }
if (otherCharacter.Info?.Job == null || otherCharacter.IsInstigator) { continue; }
if (otherCharacter.IsPlayer) { continue; }
if (!(otherCharacter.AIController is HumanAIController otherHumanAI)) { continue; }
if (otherCharacter.AIController is not HumanAIController otherHumanAI) { continue; }
if (!otherHumanAI.IsFriendly(Character)) { continue; }
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull);
if (!isWitnessing)
@@ -1299,7 +1303,7 @@ namespace Barotrauma
AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage = 0, bool isWitnessing = false)
{
if (!(c.AIController is HumanAIController humanAI)) { return AIObjectiveCombat.CombatMode.None; }
if (c.AIController is not HumanAIController humanAI) { return AIObjectiveCombat.CombatMode.None; }
if (!IsFriendly(attacker))
{
if (c.Submarine == null)
@@ -1327,7 +1331,7 @@ namespace Barotrauma
}
if (attacker.IsPlayer && c.TeamID == attacker.TeamID)
{
if (GameMain.IsSingleplayer || Character.TeamID != attacker.TeamID)
if (GameMain.IsSingleplayer || c.TeamID != attacker.TeamID)
{
// Bots in the player team never act aggressively in single player when attacked by the player
// In multiplayer, they react only to players attacking them or other crew members
@@ -1345,11 +1349,11 @@ namespace Barotrauma
isAttackerFightingEnemy = true;
return AIObjectiveCombat.CombatMode.None;
}
if (isWitnessing && Character.CombatAction != null && !c.IsSecurity)
if (isWitnessing && c.CombatAction != null && !c.IsSecurity)
{
return Character.CombatAction.WitnessReaction;
return c.CombatAction.WitnessReaction;
}
if (attacker.IsPlayer && FindInstigator() is Character instigator)
if (!attacker.IsInstigator && c.IsOnFriendlyTeam(attacker) && FindInstigator() is Character instigator)
{
// The guards don't react to player's aggressions when there's an instigator around
isAttackerFightingEnemy = true;
@@ -1359,11 +1363,11 @@ namespace Barotrauma
{
if (c.IsSecurity)
{
return Character.CombatAction != null ? Character.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.None;
return attacker.CombatAction != null ? attacker.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.Offensive;
}
else
{
return Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.None;
return attacker.CombatAction != null ? attacker.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat;
}
}
else
@@ -1546,7 +1550,7 @@ namespace Barotrauma
public bool NeedsDivingGear(Hull hull, out bool needsSuit)
{
if (!Character.NeedsAir)
if (Character.IsImmuneToPressure)
{
needsSuit = false;
return false;
@@ -1557,30 +1561,30 @@ namespace Barotrauma
hull.LethalPressure > 0 ||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f))
{
needsSuit = !Character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
needsSuit = true;
return true;
}
if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
if (Character.CharacterHealth.OxygenLowResistance < 1 && (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1))
{
return true;
}
return false;
}
public static bool HasDivingGear(Character character, float conditionPercentage = 0) => HasDivingSuit(character, conditionPercentage) || HasDivingMask(character, conditionPercentage);
public static bool HasDivingGear(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) => HasDivingSuit(character, conditionPercentage, requireOxygenTank) || HasDivingMask(character, conditionPercentage, requireOxygenTank);
/// <summary>
/// Check whether the character has a diving suit in usable condition plus some oxygen.
/// </summary>
public static bool HasDivingSuit(Character character, float conditionPercentage = 0)
=> HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true,
public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
=> HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true,
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes));
/// <summary>
/// Check whether the character has a diving mask in usable condition plus some oxygen.
/// </summary>
public static bool HasDivingMask(Character character, float conditionPercentage = 0)
=> HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true);
public static bool HasDivingMask(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
=> HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true);
private static List<Item> matchingItems = new List<Item>();
@@ -1745,7 +1749,9 @@ namespace Barotrauma
}
if (!someoneSpoke)
{
if (!item.StolenDuringRound && GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
if (!item.StolenDuringRound &&
Level.Loaded?.Type == LevelData.LevelType.Outpost &&
GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
{
var reputationLoss = MathHelper.Clamp(
(item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice,
@@ -1843,7 +1849,7 @@ namespace Barotrauma
}
break;
case "reportbrokendevices":
foreach (var item in Item.ItemList)
foreach (var item in Item.RepairableItems)
{
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, character))
@@ -1924,11 +1930,12 @@ namespace Barotrauma
bool isCurrentHull = character == Character && character.CurrentHull == hull;
if (hull == null)
{
float hullSafety = character.IsProtectedFromPressure ? 0 : 100;
if (isCurrentHull)
{
CurrentHullSafety = character.NeedsAir ? 0 : 100;
CurrentHullSafety = hullSafety;
}
return CurrentHullSafety;
return hullSafety;
}
if (isCurrentHull && visibleHulls == null)
{
@@ -1936,10 +1943,9 @@ namespace Barotrauma
visibleHulls = VisibleHulls;
}
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreWater = character.IsProtectedFromPressure();
bool ignoreOxygen = HasDivingGear(character);
bool ignoreOxygen = character.IsProtectedFromPressure || HasDivingGear(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies);
if (isCurrentHull)
{
CurrentHullSafety = safety;
@@ -1949,15 +1955,33 @@ namespace Barotrauma
private static float CalculateHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
{
if (hull == null) { return character.NeedsAir ? 0 : 100; }
if (hull.LethalPressure > 0 && character.PressureProtection <= 0 && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure)) { return 0; }
bool isProtectedFromPressure = character.IsProtectedFromPressure;
if (hull == null) { return isProtectedFromPressure ? 100 : 0; }
if (hull.LethalPressure > 0 && !isProtectedFromPressure) { return 0; }
// Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower.
// With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD.
float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage));
float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, hull.WaterPercentage / 100);
if (!character.NeedsAir)
float waterFactor = 1;
if (!ignoreWater)
{
if (visibleHulls != null)
{
// Take the visible hulls into account too, because otherwise multi-hull rooms on several floors (with platforms) will yield unexpected results.
float relativeWaterVolume = visibleHulls.Sum(s => s.WaterVolume) / visibleHulls.Sum(s => s.Volume);
waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume);
}
else
{
float relativeWaterVolume = hull.WaterVolume / hull.Volume;
waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume);
}
}
if (character.CharacterHealth.OxygenLowResistance >= 1)
{
oxygenFactor = 1;
}
if (isProtectedFromPressure)
{
waterFactor = 1;
}
float fireFactor = 1;
@@ -2045,21 +2069,39 @@ namespace Barotrauma
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
{
bool sameTeam = me.TeamID == other.TeamID;
bool teamGood = sameTeam || !onlySameTeam && IsOnFriendlyTeam(me, other);
bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other);
if (!teamGood) { return false; }
if (!me.IsSameSpeciesOrGroup(other)) { return false; }
if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
{
return false;
}
}
if (!sameTeam && me.TeamID == CharacterTeamType.None && other.IsPet)
if (other.IsPet)
{
// Hostile NPCs are hostile to all pets, unless they are in the same team.
return false;
if (!sameTeam && me.TeamID == CharacterTeamType.None) { return false; }
}
else
{
if (!me.IsSameSpeciesOrGroup(other)) { return false; }
}
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
if ((me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1) ||
(me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC))
{
Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other;
Identifier npcFaction = npc.Faction;
Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty;
if (npcFaction.IsEmpty)
{
//if faction identifier is not specified, assume the NPC is a member of the faction that owns the outpost
npcFaction = currentLocationFaction;
}
if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
{
return false;
}
}
}
}
return true;
}
@@ -96,7 +96,7 @@ namespace Barotrauma
base.Update(speed);
float step = 1.0f / 60.0f;
checkDoorsTimer -= step;
if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsOpen)
if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsFullyOpen)
{
buttonPressTimer = 0;
}
@@ -211,7 +211,7 @@ namespace Barotrauma
currentTarget = target;
Vector2 currentPos = host.SimPosition;
pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin;
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && character.PressureProtection <= 0;
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0;
if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
@@ -342,7 +342,7 @@ namespace Barotrauma
CheckDoorsInPath();
doorsChecked = true;
}
if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsOpen)
if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && lastDoor.door.IsOpening)
{
// We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again.
Reset();
@@ -510,7 +510,7 @@ namespace Barotrauma
private bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
{
if (door.IsBroken) { return true; }
if (!door.IsOpen)
if (door.IsClosed)
{
if (!door.Item.IsInteractable(character)) { return false; }
if (!ShouldBreakDoor(door))
@@ -536,7 +536,7 @@ namespace Barotrauma
}
foreach (var linked in door.Item.linkedTo)
{
if (!(linked is Item linkedItem)) { continue; }
if (linked is not Item linkedItem) { continue; }
var button = linkedItem.GetComponent<Controller>();
if (button == null) { continue; }
if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)))
@@ -785,7 +785,7 @@ namespace Barotrauma
{
if (hull.WaterVolume / hull.Rect.Width > 100.0f)
{
if (!HumanAIController.HasDivingSuit(character))
if (!HumanAIController.HasDivingSuit(character) && character.CharacterHealth.OxygenLowResistance < 1)
{
penalty += 500.0f;
}
@@ -808,7 +808,7 @@ namespace Barotrauma
private float? GetSingleNodePenalty(PathNode node)
{
if (node.Waypoint.isObstructed) { return null; }
if (!node.Waypoint.IsTraversable) { return null; }
if (node.IsBlocked()) { return null; }
float penalty = 0.0f;
if (node.Waypoint.ConnectedGap != null && node.Waypoint.ConnectedGap.Open < 0.9f)
@@ -352,7 +352,7 @@ namespace Barotrauma
Weapon = null;
continue;
}
if (WeaponComponent.IsLoaded(character))
if (WeaponComponent.IsNotEmpty(character))
{
// All good, the weapon is loaded
break;
@@ -470,7 +470,7 @@ namespace Barotrauma
// Not in the inventory anymore or cannot find the weapon component
return false;
}
if (!WeaponComponent.IsLoaded(character))
if (!WeaponComponent.IsNotEmpty(character))
{
// Try reloading (and seek ammo)
if (!Reload(seekAmmo))
@@ -541,7 +541,7 @@ namespace Barotrauma
priority /= 2;
}
}
if (!weapon.IsLoaded(character))
if (!weapon.IsNotEmpty(character))
{
if (weapon is RangedWeapon && !isAllowedToSeekWeapons)
{
@@ -554,7 +554,15 @@ namespace Barotrauma
priority /= 2;
}
}
if (Enemy.IsKnockedDown)
if (Enemy.Params.Health.StunImmunity)
{
if (weapon.Item.HasTag("stunner"))
{
priority /= 2;
}
}
else if (Enemy.IsKnockedDown)
{
// Enemy is stunned, reduce the priority of stunner weapons.
Attack attack = GetAttackDefinition(weapon);
@@ -640,11 +648,11 @@ namespace Barotrauma
{
statusEffects = statusEffects.Concat(hitEffects);
}
float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == "stun" ? a.Strength : 0);
float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == AfflictionPrefab.StunType ? a.Strength : 0);
float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se =>
{
float stunAmount = 0;
var stunAffliction = se.Afflictions.Find(a => a.Identifier == "stun");
var stunAffliction = se.Afflictions.Find(a => a.Identifier == AfflictionPrefab.StunType);
if (stunAffliction != null)
{
stunAmount = stunAffliction.Strength;
@@ -1168,30 +1176,31 @@ namespace Barotrauma
if (sqrDistance > repairTool.Range * repairTool.Range) { return; }
}
float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4 + aimFactor)
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor)
{
if (myBodies == null)
{
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
}
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories, allowInsideFixture: true);
if (pickedBody != null)
// Check that we don't hit friendlies. No need to check the walls, because there's a separate check for that at 1096 (which intentionally has a small delay)
var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Character.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter);
foreach (var body in pickedBodies)
{
Character target = null;
if (pickedBody.UserData is Character c)
if (body.UserData is Character c)
{
target = c;
}
else if (pickedBody.UserData is Limb limb)
else if (body.UserData is Limb limb)
{
target = limb.character;
}
if (target != null && (target == Enemy || !HumanAIController.IsFriendly(target)))
if (target != null && (target != Enemy || HumanAIController.IsFriendly(target)))
{
UseWeapon(deltaTime);
return;
}
}
UseWeapon(deltaTime);
}
}
}
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -200,7 +201,8 @@ namespace Barotrauma
(container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) ||
ItemToContain == null || ItemToContain.Removed ||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>()
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>(),
endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.DefaultReach)
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
@@ -244,7 +246,8 @@ namespace Barotrauma
public bool IsInTargetSlot(Item item)
{
if (container?.Inventory is ItemInventory inventory && TargetSlot is not null)
if (TargetSlot == null) { return true; }
if (container?.Inventory is ItemInventory inventory)
{
return inventory.IsInSlot(item, (int)TargetSlot);
}
@@ -19,7 +19,6 @@ namespace Barotrauma
private AIObjectiveGetItem getExtinguisherObjective;
private AIObjectiveGoTo gotoObjective;
private float useExtinquisherTimer;
public AIObjectiveExtinguishFire(Character character, Hull targetHull, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -44,7 +43,8 @@ namespace Barotrauma
}
else
{
float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y);
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
float yDist = Math.Abs(characterY - targetHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist));
@@ -119,24 +119,18 @@ namespace Barotrauma
Abandon = true;
break;
}
float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X) - fs.DamageRange;
float yDist = Math.Abs(character.WorldPosition.Y - fs.WorldPosition.Y);
bool inRange = xDist + yDist < extinguisher.Range;
// Use the hull position, because the fire x pos is sometimes inside a wall -> the bot can't ever see it and continues running towards the wall.
ISpatialEntity lookTarget = character.CurrentHull == targetHull || character.CurrentHull.linkedTo.Contains(targetHull) ? targetHull : fs as ISpatialEntity;
bool move = !inRange || !character.CanSeeTarget(lookTarget);
if ((inRange && character.CanSeeTarget(lookTarget)) || useExtinquisherTimer > 0)
float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X);
float yDist = Math.Abs(character.CurrentHull.WorldPosition.Y - targetHull.WorldPosition.Y);
float dist = xDist + yDist;
bool inRange = dist < extinguisher.Range;
bool isInDamageRange = fs.IsInDamageRange(character, fs.DamageRange) && character.CanSeeTarget(targetHull);
bool moveCloser = !isInDamageRange && (!inRange || !character.CanSeeTarget(targetHull));
bool operateExtinguisher = !moveCloser || (dist < extinguisher.Range * 1.2f && character.CanSeeTarget(targetHull));
if (operateExtinguisher)
{
useExtinquisherTimer += deltaTime;
if (useExtinquisherTimer > 2.0f)
{
useExtinquisherTimer = 0.0f;
}
// Aim
character.CursorPosition = fs.Position;
Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition;
float dist = fromCharacterToFireSource.Length();
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, fromCharacterToFireSource.Length() / 2);
if (extinguisherItem.RequireAimToUse)
{
character.SetInput(InputType.Aim, false, true);
@@ -148,25 +142,29 @@ namespace Barotrauma
{
character.Speak(TextManager.GetWithVariable("DialogPutOutFire", "[roomname]", targetHull.DisplayName, FormatCapitals.Yes).Value, null, 0, "putoutfire".ToIdentifier(), 10.0f);
}
// Prevents running into the flames.
objectiveManager.CurrentObjective.ForceWalk = true;
}
if (move)
if (moveCloser)
{
//go to the first firesource
if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: Math.Max(fs.DamageRange, extinguisher.Range * 0.7f))
{
DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(),
TargetName = fs.Hull.DisplayName
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range * 0.8f)
{
DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(),
TargetName = fs.Hull.DisplayName,
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
{
gotoObjective.requiredCondition = () => character.CanSeeTarget(targetHull);
}
}
else
else if (!operateExtinguisher || isInDamageRange)
{
character.AIController.SteeringManager.Reset();
// Don't walk into the flames.
RemoveSubObjective(ref gotoObjective);
SteeringManager.Reset();
}
// Only target one fire source at the time.
break;
}
}
@@ -177,8 +175,20 @@ namespace Barotrauma
base.Reset();
getExtinguisherObjective = null;
gotoObjective = null;
useExtinquisherTimer = 0;
sinTime = 0;
SteeringManager.Reset();
}
protected override void OnCompleted()
{
base.OnCompleted();
SteeringManager.Reset();
}
protected override void OnAbandon()
{
base.OnAbandon();
SteeringManager.Reset();
}
}
}
@@ -47,31 +47,25 @@ namespace Barotrauma
}
if (character.CurrentHull == null)
{
if (!character.NeedsAir)
{
Priority = 0;
}
else
{
Priority = (
objectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Priority > 0) ||
objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) ||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
&& HumanAIController.HasDivingSuit(character) ? 0 : 100;
}
Priority = (
objectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Priority > 0) ||
objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) ||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
&& character.IsProtectedFromPressure ? 0 : 100;
}
else
{
if (HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) &&
if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) ||
(HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) &&
(needsSuit ?
!HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)) :
!HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character))))
!HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)))))
{
Priority = 100;
}
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
character.Submarine != null && !AIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID))
character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID))
{
// Ordered to follow, hold position, or return back to main sub inside a hostile sub
// -> ignore find safety unless we need to find a diving gear
@@ -117,6 +111,11 @@ namespace Barotrauma
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
{
Priority -= priorityDecrease * deltaTime;
if (currenthullSafety >= 100)
{
// Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock.
Priority = 0;
}
}
else
{
@@ -137,12 +136,14 @@ namespace Barotrauma
private float retryTimer;
protected override void Act(float deltaTime)
{
if (resetPriority) { return; }
var currentHull = character.CurrentHull;
bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0 && character.PressureProtection <= 0;
if (!character.LockHands && (!dangerousPressure || cannotFindSafeHull))
bool dangerousPressure = !character.IsProtectedFromPressure && (currentHull == null || currentHull.LethalPressure > 0);
bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
{
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit);
bool needsEquipment = false;
bool needsEquipment = shouldActOnSuffocation;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.GetMinOxygen(character));
@@ -218,7 +219,11 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
{
AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50)
AllowGoingOutside =
character.IsProtectedFromPressure ||
character.CurrentHull == null ||
character.CurrentHull.IsTaggedAirlock() ||
character.CurrentHull.LeadsOutside(character)
},
onCompleted: () =>
{
@@ -349,8 +354,8 @@ namespace Barotrauma
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
//path calculations, only to discard all of them when going through the hulls in the outpost)
float hullSuitability = EstimateHullSuitability(character, hull);
if (!hulls.Any())
float hullSuitability = EstimateHullSuitability(character, hull);
if (hulls.None())
{
hulls.Add(hull);
}
@@ -445,9 +450,12 @@ namespace Barotrauma
{
hullSafety = 100;
}
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
float yDist = Math.Abs(characterY - potentialHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float distance = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist;
// Huge preference for closer targets
float distance = Vector2.DistanceSquared(character.WorldPosition, potentialHull.WorldPosition);
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance));
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, 10000, distance));
hullSafety *= distanceFactor;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
// Intentionally exclude wrecks from this check
@@ -155,17 +155,21 @@ namespace Barotrauma
bool canOperate = toLeak.LengthSquared() < reach * reach;
if (canOperate)
{
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak),
onAbandon: () => Abandon = true,
onCompleted: () =>
TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: Identifier.Empty, requireEquip: true, operateTarget: Leak)
{
// Use an empty filter to override the default
EndNodeFilter = n => true
},
onAbandon: () => Abandon = true,
onCompleted: () =>
{
if (CheckObjectiveSpecific()) { IsCompleted = true; }
else
{
if (CheckObjectiveSpecific()) { IsCompleted = true; }
else
{
// Failed to operate. Probably too far.
Abandon = true;
}
});
// Failed to operate. Probably too far.
Abandon = true;
}
});
}
else
{
@@ -178,7 +182,7 @@ namespace Barotrauma
requiredCondition = () =>
Leak.Submarine == character.Submarine &&
Leak.linkedTo.Any(e => e is Hull h && (character.CurrentHull == h || h.linkedTo.Contains(character.CurrentHull))),
endNodeFilter = n => n.Waypoint.CurrentHull != null && Leak.linkedTo.Any(e => e is Hull h && h == n.Waypoint.CurrentHull),
endNodeFilter = IsSuitableEndNode,
// The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue)
SpeakCannotReachCondition = () => !CheckObjectiveSpecific()
},
@@ -197,6 +201,14 @@ namespace Barotrauma
}
},
onCompleted: () => RemoveSubObjective(ref gotoObjective));
bool IsSuitableEndNode(PathNode n)
{
if (n.Waypoint.CurrentHull is null) { return false; }
if (n.Waypoint.CurrentHull.ConnectedGaps.Contains(Leak)) { return true; }
// Accept also nodes located in the linked hulls (multi-hull rooms)
return Leak.linkedTo.Any(e => e is Hull h && h.linkedTo.Contains(n.Waypoint.CurrentHull));
}
}
}
@@ -54,7 +54,7 @@ namespace Barotrauma
public bool AllowVariants { get; set; }
public bool Equip { get; set; }
public bool Wear { get; set; }
public bool RequireLoaded { get; set; }
public bool RequireNonEmpty { get; set; }
public bool EvaluateCombatPriority { get; set; }
public bool CheckPathForEachItem { get; set; }
public bool SpeakIfFails { get; set; }
@@ -123,6 +123,11 @@ namespace Barotrauma
return ignoredTags;
}
public static Func<PathNode, bool> CreateEndNodeFilter(ISpatialEntity targetEntity)
{
return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(DefaultReach);
}
private bool CheckInventory()
{
if (IdentifiersOrTags == null) { return false; }
@@ -155,11 +160,6 @@ namespace Barotrauma
Abandon = true;
return;
}
if (character.Submarine == null)
{
Abandon = true;
return;
}
if (IdentifiersOrTags != null && !isDoneSeeking)
{
if (checkInventory)
@@ -171,9 +171,14 @@ namespace Barotrauma
}
if (!isDoneSeeking)
{
if (character.Submarine == null)
{
Abandon = true;
return;
}
if (!AllowDangerousPressure)
{
bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0 && character.PressureProtection <= 0;
bool dangerousPressure = !character.IsProtectedFromPressure && (character.CurrentHull == null || character.CurrentHull.LethalPressure > 0);
if (dangerousPressure)
{
#if DEBUG
@@ -192,6 +197,11 @@ namespace Barotrauma
return;
}
}
else if (character.Submarine == null)
{
Abandon = true;
return;
}
if (targetItem == null || targetItem.Removed)
{
#if DEBUG
@@ -307,7 +317,8 @@ namespace Barotrauma
{
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
AbortCondition = obj => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget,
SpeakIfFails = false
SpeakIfFails = false,
endNodeFilter = CreateEndNodeFilter(moveToTarget)
};
},
onAbandon: () =>
@@ -391,10 +402,10 @@ namespace Barotrauma
{
if (!itemInventory.Container.HasRequiredItems(character, addMessage: false)) { continue; }
}
float itemPriority = 1;
float itemPriority = item.Prefab.BotPriority;
if (GetItemPriority != null)
{
itemPriority = GetItemPriority(item);
itemPriority *= GetItemPriority(item);
}
Entity rootInventoryOwner = item.GetRootInventoryOwner();
if (rootInventoryOwner is Item ownerItem)
@@ -513,7 +524,7 @@ namespace Barotrauma
float lowestCost = float.MaxValue;
foreach (MapEntityPrefab prefab in MapEntityPrefab.List)
{
if (!(prefab is ItemPrefab itemPrefab)) { continue; }
if (prefab is not ItemPrefab itemPrefab) { continue; }
if (IdentifiersOrTags.Any(id => id == prefab.Identifier || prefab.Tags.Contains(id)))
{
float cost = itemPrefab.DefaultPrice != null && itemPrefab.CanBeBought ?
@@ -561,7 +572,7 @@ namespace Barotrauma
if (ignoredIdentifiersOrTags != null && CheckItemIdentifiersOrTags(item, ignoredIdentifiersOrTags)) { return false; }
if (item.Condition < TargetCondition) { return false; }
if (ItemFilter != null && !ItemFilter(item)) { return false; }
if (RequireLoaded && item.Components.Any(i => !i.IsLoaded(character))) { return false; }
if (RequireNonEmpty && item.Components.Any(i => !i.IsNotEmpty(character))) { return false; }
return CheckItemIdentifiersOrTags(item, IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf));
}
@@ -21,7 +21,7 @@ namespace Barotrauma
public bool CheckInventory { get; set; }
public bool EvaluateCombatPriority { get; set; }
public bool CheckPathForEachItem { get; set; }
public bool RequireLoaded { get; set; }
public bool RequireNonEmpty { get; set; }
public bool RequireAllItems { get; set; }
private readonly ImmutableArray<Identifier> gearTags;
@@ -61,7 +61,7 @@ namespace Barotrauma
AllowStealing = AllowStealing,
ignoredIdentifiersOrTags = ignoredTags,
CheckPathForEachItem = CheckPathForEachItem,
RequireLoaded = RequireLoaded,
RequireNonEmpty = RequireNonEmpty,
ItemCount = count,
SpeakIfFails = RequireAllItems
},
@@ -50,6 +50,7 @@ namespace Barotrauma
private readonly float minDistance = 50;
private readonly float seekGapsInterval = 1;
private float seekGapsTimer;
private bool cantFindDivingGear;
/// <summary>
/// Display units
@@ -90,7 +91,7 @@ namespace Barotrauma
/// </summary>
public bool UseDistanceRelativeToAimSourcePos { get; set; } = false;
public override bool AbandonWhenCannotCompleteSubjectives => !repeat;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowOutsideSubmarine => AllowGoingOutside;
public override bool AllowInAnySub => true;
@@ -263,48 +264,73 @@ namespace Barotrauma
}
if (!Abandon)
{
if (getDivingGearIfNeeded && !character.LockHands)
if (getDivingGearIfNeeded)
{
Character followTarget = Target as Character;
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
if (Mimic)
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
bool tryToGetDivingSuit = needsDivingSuit;
if (Mimic && !character.IsImmuneToPressure)
{
if (HumanAIController.HasDivingSuit(followTarget))
{
needsDivingGear = true;
needsDivingSuit = true;
tryToGetDivingGear = true;
tryToGetDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget))
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
{
needsDivingGear = true;
tryToGetDivingGear = true;
}
}
bool needsEquipment = false;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (needsDivingSuit)
if (tryToGetDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
}
else if (needsDivingGear)
else if (tryToGetDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
if (needsEquipment)
if (character.LockHands)
{
cantFindDivingGear = true;
}
if (cantFindDivingGear && needsDivingSuit)
{
// Don't try to reach the target without a suit because it's lethal.
Abandon = true;
return;
}
if (needsEquipment && !cantFindDivingGear)
{
SteeringManager.Reset();
if (findDivingGear != null && !findDivingGear.CanBeCompleted)
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref findDivingGear));
}
else
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref findDivingGear));
}
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
cantFindDivingGear = true;
if (needsDivingSuit)
{
// Shouldn't try to reach the target without a suit, because it's lethal.
Abandon = true;
}
else
{
// Try again without requiring the diving suit
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () =>
{
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
RemoveSubObjective(ref findDivingGear);
},
onCompleted: () =>
{
RemoveSubObjective(ref findDivingGear);
});
}
},
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
}
}
@@ -170,7 +170,8 @@ namespace Barotrauma
TargetHull = character.CurrentHull;
}
if (behavior == BehaviorType.StayInHull)
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid)
{
currentTarget = TargetHull;
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
@@ -190,9 +191,6 @@ namespace Barotrauma
}
else
{
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (currentTarget != null && !currentTargetIsInvalid)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
@@ -364,8 +364,7 @@ namespace Barotrauma
CurrentOrders.RemoveAt(i);
continue;
}
var currentOrderInfo = character.GetCurrentOrder(currentOrder);
if (currentOrderInfo is Order)
if (character.GetCurrentOrder(currentOrder) is Order currentOrderInfo)
{
int currentPriority = currentOrderInfo.ManualPriority;
if (currentOrder.ManualPriority != currentPriority)
@@ -539,7 +538,8 @@ namespace Barotrauma
KeepActiveWhenReady = true,
CheckInventory = true,
Equip = false,
FindAllItems = true
FindAllItems = true,
RequireNonEmpty = false
};
break;
case "findweapon":
@@ -555,7 +555,8 @@ namespace Barotrauma
KeepActiveWhenReady = false,
CheckInventory = false,
EvaluateCombatPriority = true,
FindAllItems = false
FindAllItems = false,
RequireNonEmpty = true
};
}
prepareObjective.KeepActiveWhenReady = false;
@@ -600,9 +601,9 @@ namespace Barotrauma
Order dismissOrder = currentOrder.GetDismissal();
#if CLIENT
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
if (GameMain.GameSession?.CrewManager is CrewManager cm && cm.IsSinglePlayer)
{
GameMain.GameSession.CrewManager.SetCharacterOrder(character, dismissOrder);
character.SetOrder(dismissOrder, isNewOrder: true, speak: false);
}
#else
GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(dismissOrder, character, character));
@@ -23,6 +23,11 @@ namespace Barotrauma
private AIObjectiveGoTo goToObjective;
private AIObjectiveGetItem getItemObjective;
/// <summary>
/// If undefined, a default filter will be used.
/// </summary>
public Func<PathNode, bool> EndNodeFilter;
public bool Override { get; set; } = true;
public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null);
@@ -232,7 +237,7 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
{
TargetName = target.Item.Name,
endNodeFilter = node => node.Waypoint.Ladders == null
endNodeFilter = EndNodeFilter ?? AIObjectiveGetItem.CreateEndNodeFilter(target.Item)
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
@@ -27,6 +27,7 @@ namespace Barotrauma
public bool FindAllItems { get; set; }
public bool Equip { get; set; }
public bool EvaluateCombatPriority { get; set; }
public bool RequireNonEmpty { get; set; }
private AIObjective GetSubObjective()
{
@@ -74,7 +75,7 @@ namespace Barotrauma
Abandon = true;
}
else if (items.Any(i => i.Components.Any(i => !i.IsLoaded(character))))
else if (items.Any(i => i.Components.Any(i => !i.IsNotEmpty(character))))
{
Reset();
}
@@ -106,7 +107,7 @@ namespace Barotrauma
CheckInventory = CheckInventory,
Equip = Equip,
EvaluateCombatPriority = EvaluateCombatPriority,
RequireLoaded = true,
RequireNonEmpty = RequireNonEmpty,
RequireAllItems = requireAll
},
onCompleted: () =>
@@ -157,7 +158,7 @@ namespace Barotrauma
{
EvaluateCombatPriority = EvaluateCombatPriority,
SpeakIfFails = true,
RequireLoaded = true
RequireNonEmpty = RequireNonEmpty
};
}
if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor,
@@ -17,7 +17,6 @@ namespace Barotrauma
private AIObjectiveGoTo goToObjective;
private AIObjectiveContainItem refuelObjective;
private float previousCondition = -1;
private RepairTool repairTool;
private const float WaitTimeBeforeRepair = 0.5f;
@@ -196,15 +195,7 @@ namespace Barotrauma
Abandon = true;
}
}
if (previousCondition == -1)
{
previousCondition = Item.Condition;
}
else if (Item.Condition < previousCondition)
{
// If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it.
Abandon = true;
}
CheckPreviousCondition(deltaTime);
}
if (Abandon)
{
@@ -229,7 +220,6 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective,
constructor: () =>
{
previousCondition = -1;
var objective = new AIObjectiveGoTo(Item, character, objectiveManager)
{
TargetName = Item.Name
@@ -251,6 +241,27 @@ namespace Barotrauma
}
}
private const float conditionCheckDelay = 1;
private float conditionCheckTimer;
private float previousCondition;
private void CheckPreviousCondition(float deltaTime)
{
if (Item == null || Item.Removed) { return; }
conditionCheckTimer -= deltaTime;
if (conditionCheckTimer > 0) { return; }
conditionCheckTimer = conditionCheckDelay;
if (previousCondition > -1 && Item.Condition < previousCondition)
{
// If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it.
Abandon = true;
}
else
{
// If the previous condition is not yet stored or if it's valid (greater or equal to current condition), save the condition for the next check here.
previousCondition = Item.Condition;
}
}
private void FindRepairTool()
{
foreach (Repairable repairable in Item.Repairables)
@@ -303,7 +314,6 @@ namespace Barotrauma
base.Reset();
goToObjective = null;
refuelObjective = null;
previousCondition = -1;
repairTool = null;
}
}
@@ -320,10 +320,10 @@ namespace Barotrauma
foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities)
{
if (treatmentSuitability.Value <= cprSuitability) { continue; }
if (MapEntityPrefab.Find(null, treatmentSuitability.Key, showErrorMessages: false) is ItemPrefab itemPrefab)
if (ItemPrefab.Prefabs.TryGet(treatmentSuitability.Key, out ItemPrefab itemPrefab))
{
if (!Item.ItemList.Any(it => ((MapEntity)it).Prefab.Identifier == treatmentSuitability.Key)) { continue; }
suitableItemIdentifiers.Add(treatmentSuitability.Key);
if (Item.ItemList.None(it => it.Prefab.Identifier == treatmentSuitability.Key)) { continue; }
suitableItemIdentifiers.Add(itemPrefab.Identifier);
//only list the first 4 items
if (itemNameList.Count < 4)
{
@@ -482,18 +482,6 @@ namespace Barotrauma
public static IEnumerable<Affliction> GetSortedAfflictions(Character character, bool excludeBuffs = true) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions(), excludeBuffs);
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
{
var allAfflictions = character.CharacterHealth.GetAllAfflictions();
foreach (Affliction affliction in allAfflictions)
{
if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
if (!affliction.Prefab.TreatmentSuitability.Any(kvp => kvp.Value > 0)) { continue; }
if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; }
yield return affliction;
}
}
public override void Reset()
{
base.Reset();
@@ -26,7 +26,7 @@ namespace Barotrauma
// When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored.
// If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players.
// On the other hand, if the bots too eagerly heal characters when it's not necessary, it's inefficient and can feel frustrating, because it can't be controlled.
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? (target.IsPlayer ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? (target.IsPlayer && target.HealthPercentage < 100 ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
}
}
@@ -67,15 +67,34 @@ namespace Barotrauma
float vitality = 100;
vitality -= character.Bleeding * 2;
vitality += Math.Min(character.Oxygen, 0);
vitality -= character.CharacterHealth.GetAfflictionStrength("paralysis");
foreach (Affliction affliction in AIObjectiveRescue.GetTreatableAfflictions(character))
foreach (Affliction affliction in GetTreatableAfflictions(character))
{
float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f);
vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100;
if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
{
vitality -= affliction.Strength;
}
else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType)
{
vitality -= affliction.Strength;
}
}
return Math.Clamp(vitality, 0, 100);
}
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
{
var allAfflictions = character.CharacterHealth.GetAllAfflictions();
foreach (Affliction affliction in allAfflictions)
{
if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
if (affliction.Prefab.TreatmentSuitability.None(kvp => kvp.Value > 0)) { continue; }
if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; }
yield return affliction;
}
}
protected override AIObjective ObjectiveConstructor(Character target)
=> new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier);
@@ -12,6 +12,9 @@ namespace Barotrauma
private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective;
private bool usingEscapeBehavior, isSteeringThroughGap;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier)
{
ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
@@ -27,6 +27,15 @@ namespace Barotrauma
turrets.Add(turret);
// Set false, because we manage the turrets in the Update method.
turret.AutoOperate = false;
// Set to full condition, because items don't work when they are broken.
turret.Item.Condition = turret.Item.MaxCondition;
foreach (MapEntity linkedEntity in turret.Item.linkedTo)
{
if (linkedEntity is Item linkedItem)
{
linkedItem.Condition = linkedItem.MaxCondition;
}
}
}
}
LoadAllTurrets();
@@ -264,27 +273,52 @@ namespace Barotrauma
}
}
destroyedOrgans.ForEach(o => spawnOrgans.Remove(o));
bool someoneNearby = false;
bool isSomeoneNearby = false;
float minDist = Sonar.DefaultSonarRange * 2.0f;
foreach (Submarine submarine in Submarine.Loaded)
#if SERVER
foreach (var client in GameMain.Server.ConnectedClients)
{
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (Vector2.DistanceSquared(submarine.WorldPosition, Submarine.WorldPosition) < minDist * minDist)
var spectatePos = client.SpectatePos;
if (spectatePos.HasValue)
{
someoneNearby = true;
break;
if (IsCloseEnough(spectatePos.Value, minDist))
{
isSomeoneNearby = true;
break;
}
}
}
foreach (Character c in Character.CharacterList)
#else
if (IsCloseEnough(GameMain.GameScreen.Cam.Position, minDist))
{
if (c != Character.Controlled && !c.IsRemotePlayer) { continue; }
if (Vector2.DistanceSquared(c.WorldPosition, Submarine.WorldPosition) < minDist * minDist)
isSomeoneNearby = true;
}
#endif
if (!isSomeoneNearby)
{
foreach (Submarine submarine in Submarine.Loaded)
{
someoneNearby = true;
break;
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (IsCloseEnough(submarine.WorldPosition, minDist))
{
isSomeoneNearby = true;
break;
}
}
}
if (!someoneNearby) { return; }
if (!isSomeoneNearby)
{
foreach (Character c in Character.CharacterList)
{
if (!c.IsPlayer && !c.IsOnPlayerTeam) { continue; }
if (IsCloseEnough(c.WorldPosition, minDist))
{
isSomeoneNearby = true;
break;
}
}
}
if (!isSomeoneNearby) { return; }
OperateTurrets(deltaTime, Config.Entity);
if (!IsClient)
{
@@ -292,6 +326,7 @@ namespace Barotrauma
UpdateReinforcements(deltaTime);
}
}
private bool IsCloseEnough(Vector2 targetPos, float minDist) => Vector2.DistanceSquared(targetPos, Submarine.WorldPosition) < minDist * minDist;
private void SpawnInitialCells()
{
@@ -541,11 +541,11 @@ namespace Barotrauma
float wobbleStrength = 0.0f;
if (character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == heldItem)
{
wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: "damage");
wobbleStrength += Character.CharacterHealth.GetLimbDamage(rightHand, afflictionType: AfflictionPrefab.DamageType);
}
if (character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == heldItem)
{
wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: "damage");
wobbleStrength += Character.CharacterHealth.GetLimbDamage(leftHand, afflictionType: AfflictionPrefab.DamageType);
}
if (wobbleStrength <= 0.1f) { return 0.0f; }
wobbleStrength = (float)Math.Min(wobbleStrength, 1.0f);
@@ -150,8 +150,10 @@ namespace Barotrauma
private readonly float movementLerp;
private float cprAnimTimer;
private float cprPump;
private float cprAnimTimer,cprPump;
private float fallingProneAnimTimer;
const float FallingProneAnimDuration = 1.0f;
private bool swimming;
//time until the character can switch from walking to swimming or vice versa
@@ -268,7 +270,8 @@ namespace Barotrauma
if (deathAnimTimer < deathAnimDuration)
{
deathAnimTimer += deltaTime;
UpdateDying(deltaTime);
//the force/torque used to move the limbs goes from 1 to 0 during the death anim duration
UpdateFallingProne(1.0f - deathAnimTimer / deathAnimDuration);
}
}
else
@@ -278,6 +281,11 @@ namespace Barotrauma
if (!character.CanMove)
{
if (fallingProneAnimTimer < FallingProneAnimDuration)
{
fallingProneAnimTimer += deltaTime;
UpdateFallingProne(1.0f);
}
levitatingCollider = false;
Collider.FarseerBody.FixedRotation = false;
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
@@ -291,18 +299,20 @@ namespace Barotrauma
}
return;
}
fallingProneAnimTimer = 0.0f;
//re-enable collider
if (!Collider.Enabled)
{
var lowestLimb = FindLowestLimb();
Collider.SetTransform(new Vector2(
Collider.SimPosition.X,
Math.Max(lowestLimb.SimPosition.Y + (Collider.Radius + Collider.Height / 2), Collider.SimPosition.Y)),
Collider.Rotation);
Collider.FarseerBody.ResetDynamics();
Collider.FarseerBody.LinearVelocity = MainLimb.LinearVelocity;
Collider.Enabled = true;
}
@@ -444,12 +454,13 @@ namespace Barotrauma
aiming = false;
wasAimingMelee = aimingMelee;
aimingMelee = false;
IsHanging = false;
IsHanging = IsHanging && character.IsRagdolled;
}
void UpdateStanding()
{
if (CurrentGroundedParams == null) { return; }
var currentGroundedParams = CurrentGroundedParams;
if (currentGroundedParams == null) { return; }
Vector2 handPos;
Limb leftFoot = GetLimb(LimbType.LeftFoot);
@@ -472,7 +483,7 @@ namespace Barotrauma
walkCycleMultiplier *= 1.5f;
}
float getUpForce = CurrentGroundedParams.GetUpForce / RagdollParams.JointScale;
float getUpForce = currentGroundedParams.GetUpForce / RagdollParams.JointScale;
Vector2 colliderPos = GetColliderBottom();
if (Math.Abs(TargetMovement.X) > 1.0f)
@@ -573,7 +584,7 @@ namespace Barotrauma
}
float stepLift = TargetMovement.X == 0.0f ? 0 :
(float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100);
(float)Math.Sin(WalkPos * currentGroundedParams.StepLiftFrequency + MathHelper.Pi * currentGroundedParams.StepLiftOffset) * (currentGroundedParams.StepLiftAmount / 100);
float y = colliderPos.Y + stepLift;
@@ -588,7 +599,7 @@ namespace Barotrauma
if (!head.Disabled)
{
y = colliderPos.Y + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier;
y = colliderPos.Y + stepLift * currentGroundedParams.StepLiftHeadMultiplier;
if (HeadPosition.HasValue) { y += HeadPosition.Value; }
if (Crouching && !movingHorizontally) { y -= HumanCrouchParams.MoveDownAmountWhenStationary; }
head.PullJointWorldAnchorB =
@@ -605,18 +616,18 @@ namespace Barotrauma
if (TorsoAngle.HasValue && !torso.Disabled)
{
float torsoAngle = TorsoAngle.Value;
float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes");
float herpesStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType);
if (Crouching && !movingHorizontally && !Aiming) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; }
torsoAngle -= herpesStrength / 150.0f;
torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque);
torso.body.SmoothRotate(torsoAngle * Dir, currentGroundedParams.TorsoTorque);
}
if (!head.Disabled)
{
if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue)
if (!Aiming && currentGroundedParams.FixedHeadAngle && HeadAngle.HasValue)
{
float headAngle = HeadAngle.Value;
if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; }
head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque);
head.body.SmoothRotate(headAngle * Dir, currentGroundedParams.HeadTorque);
}
else
{
@@ -655,16 +666,16 @@ namespace Barotrauma
if (footPos.Y < 0.0f) { footPos.Y = -0.15f; }
//make the character limp if the feet are damaged
float footAfflictionStrength = character.CharacterHealth.GetAfflictionStrength("damage", foot, true);
float footAfflictionStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.DamageType, foot, true);
footPos.X *= MathHelper.Lerp(1.0f, 0.75f, MathHelper.Clamp(footAfflictionStrength / 50.0f, 0.0f, 1.0f));
if (CurrentGroundedParams.FootLiftHorizontalFactor > 0)
if (currentGroundedParams.FootLiftHorizontalFactor > 0)
{
// Calculate the foot y dynamically based on the foot position relative to the waist,
// so that the foot aims higher when it's behind the waist and lower when it's in the front.
float xDiff = (foot.SimPosition.X - waistPos.X + FootMoveOffset.X) * Dir;
float min = MathUtils.InverseLerp(1, 0, CurrentGroundedParams.FootLiftHorizontalFactor);
float max = 1 + MathUtils.InverseLerp(0, 1, CurrentGroundedParams.FootLiftHorizontalFactor);
float min = MathUtils.InverseLerp(1, 0, currentGroundedParams.FootLiftHorizontalFactor);
float max = 1 + MathUtils.InverseLerp(0, 1, currentGroundedParams.FootLiftHorizontalFactor);
float xFactor = MathHelper.Lerp(min, max, MathUtils.InverseLerp(RagdollParams.JointScale, -RagdollParams.JointScale, xDiff));
footPos.Y *= xFactor;
}
@@ -688,19 +699,19 @@ namespace Barotrauma
{
foot.DebugRefPos = colliderPos;
foot.DebugTargetPos = colliderPos + footPos;
MoveLimb(foot, colliderPos + footPos, CurrentGroundedParams.FootMoveStrength);
MoveLimb(foot, colliderPos + footPos, currentGroundedParams.FootMoveStrength);
FootIK(foot, colliderPos + footPos,
CurrentGroundedParams.LegBendTorque, CurrentGroundedParams.FootTorque, CurrentGroundedParams.FootAngleInRadians);
currentGroundedParams.LegBendTorque, currentGroundedParams.FootTorque, currentGroundedParams.FootAngleInRadians);
}
}
//calculate the positions of hands
handPos = torso.SimPosition;
handPos.X = -walkPosX * CurrentGroundedParams.HandMoveAmount.X;
handPos.X = -walkPosX * currentGroundedParams.HandMoveAmount.X;
float lowerY = CurrentGroundedParams.HandClampY;
float lowerY = currentGroundedParams.HandClampY;
handPos.Y = lowerY + (float)(Math.Abs(Math.Sin(WalkPos - Math.PI * 1.5f) * CurrentGroundedParams.HandMoveAmount.Y));
handPos.Y = lowerY + (float)(Math.Abs(Math.Sin(WalkPos - Math.PI * 1.5f) * currentGroundedParams.HandMoveAmount.Y));
Vector2 posAddition = new Vector2(Math.Sign(movement.X) * HandMoveOffset.X, HandMoveOffset.Y);
@@ -708,13 +719,13 @@ namespace Barotrauma
{
HandIK(rightHand,
torso.SimPosition + posAddition + new Vector2(-handPos.X, (Math.Sign(walkPosX) == Math.Sign(Dir)) ? handPos.Y : lowerY),
CurrentGroundedParams.ArmMoveStrength, CurrentGroundedParams.HandMoveStrength);
currentGroundedParams.ArmMoveStrength, currentGroundedParams.HandMoveStrength);
}
if (leftHand != null && !leftHand.Disabled)
{
HandIK(leftHand,
torso.SimPosition + posAddition + new Vector2(handPos.X, (Math.Sign(walkPosX) == Math.Sign(-Dir)) ? handPos.Y : lowerY),
CurrentGroundedParams.ArmMoveStrength, CurrentGroundedParams.HandMoveStrength);
currentGroundedParams.ArmMoveStrength, currentGroundedParams.HandMoveStrength);
}
}
else
@@ -745,8 +756,8 @@ namespace Barotrauma
{
foot.DebugRefPos = colliderPos;
foot.DebugTargetPos = footPos;
float footMoveForce = CurrentGroundedParams.FootMoveStrength;
float legBendTorque = CurrentGroundedParams.LegBendTorque;
float footMoveForce = currentGroundedParams.FootMoveStrength;
float legBendTorque = currentGroundedParams.LegBendTorque;
if (Crouching)
{
// Keeps the pose
@@ -754,7 +765,7 @@ namespace Barotrauma
footMoveForce *= 2;
}
MoveLimb(foot, footPos, footMoveForce);
FootIK(foot, footPos, legBendTorque, CurrentGroundedParams.FootTorque, CurrentGroundedParams.FootAngleInRadians);
FootIK(foot, footPos, legBendTorque, currentGroundedParams.FootTorque, currentGroundedParams.FootAngleInRadians);
}
}
@@ -770,7 +781,7 @@ namespace Barotrauma
var arm = GetLimb(armType);
if (arm != null && Math.Abs(arm.body.AngularVelocity) < 10.0f)
{
arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.5f, 0.5f), arm.Mass * 50.0f * CurrentGroundedParams.ArmMoveStrength);
arm.body.SmoothRotate(MathHelper.Clamp(-arm.body.AngularVelocity, -0.5f, 0.5f), arm.Mass * 50.0f * currentGroundedParams.ArmMoveStrength);
}
//get the elbow to a neutral rotation
@@ -781,14 +792,14 @@ namespace Barotrauma
if (elbow != null)
{
float diff = elbow.JointAngle - (Dir > 0 ? elbow.LowerLimit : elbow.UpperLimit);
forearm.body.ApplyTorque(MathHelper.Clamp(-diff, -MathHelper.PiOver2, MathHelper.PiOver2) * forearm.Mass * 100.0f * CurrentGroundedParams.ArmMoveStrength);
forearm.body.ApplyTorque(MathHelper.Clamp(-diff, -MathHelper.PiOver2, MathHelper.PiOver2) * forearm.Mass * 100.0f * currentGroundedParams.ArmMoveStrength);
}
}
// Try to keep the wrist straight
LimbJoint wrist = GetJointBetweenLimbs(foreArmType, hand.type);
if (wrist != null)
{
hand.body.ApplyTorque(MathHelper.Clamp(-wrist.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 100f * CurrentGroundedParams.HandMoveStrength);
hand.body.ApplyTorque(MathHelper.Clamp(-wrist.JointAngle, -MathHelper.PiOver2, MathHelper.PiOver2) * hand.Mass * 100f * currentGroundedParams.HandMoveStrength);
}
}
}
@@ -1292,10 +1303,9 @@ namespace Barotrauma
}
}
void UpdateDying(float deltaTime)
void UpdateFallingProne(float strength)
{
//the force/torque used to move the limbs goes from 1 to 0 during the death anim duration
float strength = 1.0f - deathAnimTimer / deathAnimDuration;
if (strength <= 0.0f) { return; }
Limb head = GetLimb(LimbType.Head);
Limb torso = GetLimb(LimbType.Torso);
@@ -1319,6 +1329,19 @@ namespace Barotrauma
}
if (torso == null) { return; }
//make the torso tip over
//otherwise it tends to just drop straight down, pinning the characters legs in a weird pose
if (!InWater)
{
//prefer tipping over in the same direction the torso is rotating
//or moving
//or lastly, in the direction it's facing if it's not moving/rotating
float fallDirection = Math.Sign(torso.body.AngularVelocity - torso.body.LinearVelocity.X - Dir * 0.01f);
float torque = MathF.Cos(torso.Rotation) * fallDirection * 5.0f * strength;
torso.body.ApplyTorque(torque * torso.body.Mass);
}
//attempt to make legs stay in a straight line with the torso to prevent the character from doing a split
for (int i = 0; i < 2; i++)
{
@@ -1503,12 +1526,12 @@ namespace Barotrauma
Limb rightHand = GetLimb(LimbType.RightHand);
Limb targetLeftHand = target.AnimController.GetLimb(LimbType.LeftForearm);
if (targetLeftHand == null) targetLeftHand = target.AnimController.GetLimb(LimbType.Torso);
if (targetLeftHand == null) targetLeftHand = target.AnimController.MainLimb;
if (targetLeftHand == null) { targetLeftHand = target.AnimController.GetLimb(LimbType.Torso); }
if (targetLeftHand == null) { targetLeftHand = target.AnimController.MainLimb; }
Limb targetRightHand = target.AnimController.GetLimb(LimbType.RightForearm);
if (targetRightHand == null) targetRightHand = target.AnimController.GetLimb(LimbType.Torso);
if (targetRightHand == null) targetRightHand = target.AnimController.MainLimb;
if (targetRightHand == null) { targetRightHand = target.AnimController.GetLimb(LimbType.Torso); }
if (targetRightHand == null) { targetRightHand = target.AnimController.MainLimb; }
if (!target.AllowInput)
{
@@ -1644,18 +1667,24 @@ namespace Barotrauma
pullLimb.PullJointEnabled = true;
if (targetLimb.type == LimbType.Torso || targetLimb == target.AnimController.MainLimb)
{
Vector2 pullLimbAnchor = targetLimb.SimPosition;
pullLimb.PullJointMaxForce = 5000.0f;
if (!character.HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging))
{
targetMovement *= MathHelper.Clamp(Mass / target.Mass, 0.5f, 1.0f);
}
Vector2 shoulderPos = rightShoulder.WorldAnchorA;
Vector2 dragDir = inWater ? Vector2.Normalize(targetLimb.SimPosition - shoulderPos) : Vector2.UnitY;
if (!MathUtils.IsValid(dragDir)) { dragDir = Vector2.UnitY; }
targetAnchor = shoulderPos - dragDir * ConvertUnits.ToSimUnits(upperArmLength + forearmLength);
Vector2 shoulderPos = rightShoulder.WorldAnchorA;
float targetDist = Vector2.Distance(targetLimb.SimPosition, shoulderPos);
Vector2 dragDir = (targetLimb.SimPosition - shoulderPos) / targetDist;
if (!MathUtils.IsValid(dragDir)) { dragDir = -Vector2.UnitY; }
if (!InWater)
{
//lerp the arm downwards when not swimming
dragDir = Vector2.Lerp(dragDir, -Vector2.UnitY, 0.5f);
}
Vector2 pullLimbAnchor = shoulderPos + dragDir * Math.Min(targetDist, (upperArmLength + forearmLength) * 2);
targetAnchor = shoulderPos + dragDir * (upperArmLength + forearmLength);
targetForce = 200.0f;
if (target.Submarine != character.Submarine)
{
@@ -1212,7 +1212,9 @@ namespace Barotrauma
RefreshFloorY(ignoreStairs: Stairs == null);
if (currentHull.WaterPercentage > 0.001f)
{
float waterSurface = ConvertUnits.ToSimUnits(GetSurfaceY());
(float waterSurfaceDisplayUnits, float ceilingDisplayUnits) = GetWaterSurfaceAndCeilingY();
float waterSurfaceY = ConvertUnits.ToSimUnits(waterSurfaceDisplayUnits);
float ceilingY = ConvertUnits.ToSimUnits(ceilingDisplayUnits);
if (targetMovement.Y < 0.0f)
{
Vector2 colliderBottom = GetColliderBottom();
@@ -1222,13 +1224,21 @@ namespace Barotrauma
{
//set floorY to the position of the floor in the hull below the character
var lowerHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(colliderBottom), useWorldCoordinates: false);
if (lowerHull != null) floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height);
if (lowerHull != null)
{
floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height);
}
}
}
float standHeight = HeadPosition ?? TorsoPosition ?? Collider.GetMaxExtent() * 0.5f;
if (Collider.SimPosition.Y < waterSurface && waterSurface - floorY > standHeight * 0.8f)
if (Collider.SimPosition.Y < waterSurfaceY)
{
inWater = true;
//too deep to stand up, or not enough room to stand up
if (waterSurfaceY - floorY > standHeight * 0.8f ||
ceilingY - floorY < standHeight * 0.8f)
{
inWater = true;
}
}
}
}
@@ -1663,22 +1673,34 @@ namespace Barotrauma
}
}
/// <summary>
/// Get the position of the surface of water at the position of the character, in display units (taking into account connected hulls above the hull the character is in)
/// </summary>
public float GetSurfaceY()
{
return GetWaterSurfaceAndCeilingY().WaterSurfaceY;
}
/// <summary>
/// Get the position of the surface of water and the ceiling (= upper edge of the hull) at the position of the character, in display units (taking into account connected hulls above the hull the character is in).
/// </summary>
private (float WaterSurfaceY, float CeilingY) GetWaterSurfaceAndCeilingY()
{
//check both hulls: the hull whose coordinate space the ragdoll is in, and the hull whose bounds the character's origin actually is inside
if (currentHull == null || character.CurrentHull == null)
{
return float.PositiveInfinity;
return (float.PositiveInfinity, float.PositiveInfinity);
}
float surfacePos = currentHull.Surface;
float surfaceY = currentHull.Surface;
float ceilingY = currentHull.Rect.Y;
float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f);
//if the hull is almost full of water, check if there's a water-filled hull above it
//and use its water surface instead of the current hull's
if (currentHull.Rect.Y - currentHull.Surface < 5.0f)
{
GetSurfacePos(currentHull, ref surfacePos);
void GetSurfacePos(Hull hull, ref float prevSurfacePos)
{
GetSurfacePos(currentHull, ref surfaceY, ref ceilingY);
void GetSurfacePos(Hull hull, ref float prevSurfacePos, ref float ceilingPos)
{
if (prevSurfacePos > surfaceThreshold) { return; }
foreach (Gap gap in hull.ConnectedGaps)
@@ -1689,6 +1711,7 @@ namespace Barotrauma
//if the gap is above us and leads outside, there's no surface to limit the movement
if (!gap.IsRoomToRoom && gap.Position.Y > hull.Position.Y)
{
ceilingPos += 100000.0f;
prevSurfacePos += 100000.0f;
return;
}
@@ -1697,15 +1720,16 @@ namespace Barotrauma
{
if (linkedTo is Hull otherHull && otherHull != hull && otherHull != currentHull)
{
prevSurfacePos = Math.Max(surfacePos, otherHull.Surface);
GetSurfacePos(otherHull, ref prevSurfacePos);
prevSurfacePos = Math.Max(surfaceY, otherHull.Surface);
ceilingPos = Math.Max(ceilingPos, otherHull.Rect.Y);
GetSurfacePos(otherHull, ref prevSurfacePos, ref ceilingPos);
break;
}
}
}
}
}
return surfacePos;
return (surfaceY, ceilingY);
}
public void SetPosition(Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true, bool forceMainLimbToCollider = false, bool detachProjectiles = true)
@@ -343,9 +343,10 @@ namespace Barotrauma
return (Duration == 0.0f) ? LevelWallDamage : LevelWallDamage * deltaTime;
}
public float GetItemDamage(float deltaTime)
public float GetItemDamage(float deltaTime, float multiplier = 1)
{
return (Duration == 0.0f) ? ItemDamage : ItemDamage * deltaTime;
float dmg = ItemDamage * multiplier;
return (Duration == 0.0f) ? dmg : dmg * deltaTime;
}
public float GetTotalDamage(bool includeStructureDamage = false)
@@ -31,6 +31,9 @@ namespace Barotrauma
{
public readonly static List<Character> CharacterList = new List<Character>();
public const float MaxHighlightDistance = 150.0f;
public const float MaxDragDistance = 200.0f;
partial void UpdateLimbLightSource(Limb limb);
private bool enabled = true;
@@ -176,6 +179,13 @@ namespace Barotrauma
}
}
private Identifier? faction;
public Identifier Faction
{
get { return faction ?? HumanPrefab?.Faction ?? Identifier.Empty; }
set { faction = value; }
}
private CharacterTeamType teamID;
public CharacterTeamType TeamID
{
@@ -497,7 +507,7 @@ namespace Barotrauma
LocalizedString displayName = Params.DisplayName;
if (displayName.IsNullOrWhiteSpace())
{
if (string.IsNullOrWhiteSpace(Params.SpeciesTranslationOverride))
if (Params.SpeciesTranslationOverride.IsEmpty)
{
displayName = TextManager.Get($"Character.{SpeciesName}");
}
@@ -529,8 +539,13 @@ namespace Barotrauma
}
set
{
bool wasHidden = HideFace;
hideFaceTimer = MathHelper.Clamp(hideFaceTimer + (value ? 1.0f : -0.5f), 0.0f, 10.0f);
if (info != null && info.IsDisguisedAsAnother != HideFace) info.CheckDisguiseStatus(true);
bool isHidden = HideFace;
if (isHidden != wasHidden && info != null && info.IsDisguisedAsAnother != isHidden)
{
info.CheckDisguiseStatus(true);
}
}
}
@@ -760,7 +775,7 @@ namespace Barotrauma
get
{
if (IsUnconscious) { return true; }
return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength);
return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.AfflictionType == AfflictionPrefab.ParalysisType && a.Strength >= a.Prefab.MaxStrength);
}
}
@@ -822,9 +837,15 @@ namespace Barotrauma
public float HealthPercentage => CharacterHealth.HealthPercentage;
public float MaxVitality => CharacterHealth.MaxVitality;
public float MaxHealth => MaxVitality;
/// <summary>
/// Was the character in full health at the beginning of the frame?
/// </summary>
public bool WasFullHealth => CharacterHealth.WasInFullHealth;
public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle;
public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached;
public float EmpVulnerability => Params.Health.EmpVulnerability;
public float PoisonVulnerability => Params.Health.PoisonVulnerability;
public float Bloodloss
{
@@ -838,7 +859,7 @@ namespace Barotrauma
public float Bleeding
{
get { return CharacterHealth.GetAfflictionStrength("bleeding", true); }
get { return CharacterHealth.GetAfflictionStrength(AfflictionPrefab.BleedingType, true); }
}
private bool speechImpedimentSet;
@@ -1043,6 +1064,8 @@ namespace Barotrauma
public bool InWater => AnimController is AnimController { InWater: true };
public bool IsLowInOxygen => NeedsOxygen && OxygenAvailable < CharacterHealth.LowOxygenThreshold;
public bool GodMode = false;
public CampaignMode.InteractionType CampaignInteractionType;
@@ -1099,6 +1122,12 @@ namespace Barotrauma
public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID;
public float AITurretPriority
{
get => Params.AITurretPriority;
private set => Params.AITurretPriority = value;
}
public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath);
public OnDeathHandler OnDeath;
@@ -1691,7 +1720,7 @@ namespace Barotrauma
if (wearable.SkillModifiers.TryGetValue(skillIdentifier, out float skillValue))
{
skillLevel += skillValue;
break;
break;
}
}
@@ -1700,9 +1729,7 @@ namespace Barotrauma
}
skillLevel += GetStatValue(GetSkillStatType(skillIdentifier));
return skillLevel;
return Math.Max(skillLevel, 0);
}
// TODO: reposition? there's also the overrideTargetMovement variable, but it's not in the same manner
@@ -1900,7 +1927,7 @@ namespace Barotrauma
{
if (limb != null)
{
sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: "damage"));
sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: AfflictionPrefab.DamageType));
}
return Math.Clamp(sum, 0, 1f);
}
@@ -2201,7 +2228,9 @@ namespace Barotrauma
if (SelectedCharacter != null)
{
if (Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > 90000.0f || !SelectedCharacter.CanBeSelected)
if (!SelectedCharacter.CanBeSelected ||
(Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > MaxDragDistance * MaxDragDistance &&
SelectedCharacter.GetDistanceToClosestLimb(SimPosition) > ConvertUnits.ToSimUnits(MaxDragDistance)))
{
DeselectCharacter();
}
@@ -2494,8 +2523,12 @@ namespace Barotrauma
if (!skipDistanceCheck)
{
maxDist = ConvertUnits.ToSimUnits(maxDist);
if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist) { return false; }
maxDist = Math.Max(ConvertUnits.ToSimUnits(maxDist), c.AnimController.Collider.GetMaxExtent());
if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist &&
Vector2.DistanceSquared(SimPosition, c.AnimController.MainLimb.SimPosition) > maxDist * maxDist)
{
return false;
}
}
return checkVisibility ? CanSeeCharacter(c) : true;
@@ -2851,6 +2884,23 @@ namespace Barotrauma
}
else
{
#if CLIENT
if (Controlled == this)
{
HealingCooldown.PutOnCooldown();
}
#elif SERVER
if (GameMain.Server?.ConnectedClients is { } clients)
{
foreach (Client c in clients)
{
if (c.Character != this) { continue; }
HealingCooldown.SetCooldown(c);
break;
}
}
#endif
SelectCharacter(FocusedCharacter);
#if CLIENT
if (Controlled == this)
@@ -3070,8 +3120,7 @@ namespace Barotrauma
if (NeedsAir)
{
//implode if not protected from pressure, and either outside or in a high-pressure hull
if (!IsProtectedFromPressure() &&
(AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f))
if (!IsProtectedFromPressure && (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f))
{
if (CharacterHealth.PressureKillDelay <= 0.0f)
{
@@ -3098,15 +3147,17 @@ namespace Barotrauma
PressureTimer = 0.0f;
}
}
else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) &&
PressureProtection < (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f) &&
WorldPosition.Y < CharacterHealth.CrushDepth && !HasAbilityFlag(AbilityFlags.ImmuneToPressure))
else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && !IsProtectedFromPressure)
{
//implode if below crush depth, and either outside or in a high-pressure hull
if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)
float realWorldDepth = Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 0.0f;
if (PressureProtection < realWorldDepth && realWorldDepth > CharacterHealth.CrushDepth)
{
Implode();
if (IsDead) { return; }
//implode if below crush depth, and either outside or in a high-pressure hull
if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)
{
Implode();
if (IsDead) { return; }
}
}
}
@@ -3140,56 +3191,57 @@ namespace Barotrauma
UpdateAIChatMessages(deltaTime);
//Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us!
bool allowRagdoll = GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true;
bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 8.0f * 8.0f;
bool wasRagdolled = false;
bool selfRagdolled = false;
if (IsForceRagdolled)
if (GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true)
{
IsRagdolled = IsForceRagdolled;
}
else if (this != Controlled)
{
wasRagdolled = IsRagdolled;
IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll);
}
//Keep us ragdolled if we were forced or we're too speedy to unragdoll
else if (allowRagdoll && (!IsRagdolled || !tooFastToUnragdoll))
{
if (ragdollingLockTimer > 0.0f)
bool wasRagdolled = IsRagdolled;
if (IsForceRagdolled)
{
SetInput(InputType.Ragdoll, false, true);
ragdollingLockTimer -= deltaTime;
IsRagdolled = IsForceRagdolled;
}
else if (this != Controlled)
{
wasRagdolled = IsRagdolled;
IsRagdolled = IsKeyDown(InputType.Ragdoll);
}
else
{
wasRagdolled = IsRagdolled;
IsRagdolled = selfRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves
if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.5f; }
bool tooFastToUnragdoll = bodyMovingTooFast(AnimController.Collider) || bodyMovingTooFast(AnimController.MainLimb.body);
bool bodyMovingTooFast(PhysicsBody body)
{
return
body.LinearVelocity.LengthSquared() > 8.0f * 8.0f ||
//falling down counts as going too fast
(!InWater && body.LinearVelocity.Y < -5.0f);
}
if (ragdollingLockTimer > 0.0f)
{
ragdollingLockTimer -= deltaTime;
}
else if (!tooFastToUnragdoll)
{
IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves
if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; }
}
if (IsRagdolled)
{
SetInput(InputType.Ragdoll, false, true);
}
}
}
if (!wasRagdolled && IsRagdolled)
{
if (selfRagdolled)
if (!wasRagdolled && IsRagdolled)
{
CheckTalents(AbilityEffectType.OnSelfRagdoll);
CheckTalents(AbilityEffectType.OnRagdoll);
}
// currently does not work when you are stunned, like it should
CheckTalents(AbilityEffectType.OnRagdoll);
}
lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f);
//ragdoll button
if (IsRagdolled || !CanMove)
{
if (AnimController is HumanoidAnimController humanAnimController)
{
humanAnimController.Crouching = false;
}
if (IsRagdolled) { AnimController.IgnorePlatforms = true; }
AnimController.ResetPullJoints();
SelectedItem = SelectedSecondaryItem = null;
return;
@@ -3367,6 +3419,20 @@ namespace Barotrauma
return distSqr;
}
public float GetDistanceToClosestLimb(Vector2 simPos)
{
float closestDist = float.MaxValue;
foreach (Limb limb in AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
float dist = Vector2.Distance(simPos, limb.SimPosition);
dist -= limb.body.GetMaxExtent();
closestDist = Math.Min(closestDist, dist);
if (closestDist <= 0.0f) { return 0.0f; }
}
return closestDist;
}
private float despawnTimer;
private void UpdateDespawn(float deltaTime, bool ignoreThresholds = false, bool createNetworkEvents = true)
{
@@ -3758,9 +3824,10 @@ namespace Barotrauma
message.SendDelay -= deltaTime;
if (message.SendDelay > 0.0f) { continue; }
bool canUseRadio = ChatMessage.CanUseRadio(this, out WifiComponent radio);
if (message.MessageType == null)
{
message.MessageType = ChatMessage.CanUseRadio(this) ? ChatMessageType.Radio : ChatMessageType.Default;
message.MessageType = canUseRadio ? ChatMessageType.Radio : ChatMessageType.Default;
}
#if CLIENT
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
@@ -3770,6 +3837,11 @@ namespace Barotrauma
{
GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(Name, modifiedMessage, message.MessageType.Value, this);
}
if (canUseRadio)
{
Signal s = new Signal(modifiedMessage, sender: this, source: radio.Item);
radio.TransmitSignal(s, sentFromChat: true);
}
}
#endif
#if SERVER
@@ -4005,17 +4077,7 @@ namespace Barotrauma
CheckTalents(AbilityEffectType.OnKillCharacter, abilityCharacterKill);
if (!IsOnPlayerTeam) { return; }
if (CreatureMetrics.Instance.Killed.Contains(target.SpeciesName)) { return; }
CreatureMetrics.Instance.Killed.Add(target.SpeciesName);
AddEncounter(target);
}
public void AddEncounter(Character other)
{
if (!IsOnPlayerTeam) { return; }
if (CreatureMetrics.Instance.Encountered.Contains(other.SpeciesName)) { return; }
CreatureMetrics.Instance.Encountered.Add(other.SpeciesName);
CreatureMetrics.Instance.RecentlyEncountered.Add(other.SpeciesName);
CreatureMetrics.RecordKill(target.SpeciesName);
}
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false)
@@ -4082,17 +4144,23 @@ namespace Barotrauma
OnAttackedProjSpecific(attacker, attackResult, stun);
if (!wasDead)
{
TryAdjustAttackerSkill(attacker, CharacterHealth.Vitality - prevVitality);
TryAdjustAttackerSkill(attacker, attackResult);
}
};
}
if (attackResult.Damage > 0)
{
LastDamage = attackResult;
if (attacker != null)
if (attacker != null && attacker != this && !attacker.Removed)
{
AddAttacker(attacker, attackResult.Damage);
AddEncounter(attacker);
attacker.AddEncounter(this);
if (IsOnPlayerTeam)
{
CreatureMetrics.AddEncounter(attacker.SpeciesName);
}
if (attacker.IsOnPlayerTeam)
{
CreatureMetrics.AddEncounter(SpeciesName);
}
}
ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
@@ -4108,26 +4176,85 @@ namespace Barotrauma
partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun);
public void TryAdjustAttackerSkill(Character attacker, float healthChange)
public void TryAdjustAttackerSkill(Character attacker, AttackResult attackResult)
{
if (attacker == null) { return; }
if (!attacker.IsOnPlayerTeam) { return; }
bool isEnemy = AIController is EnemyAIController || TeamID != attacker.TeamID;
if (isEnemy)
if (!isEnemy) { return; }
float weaponDamage = 0;
float medicalDamage = 0;
foreach (var affliction in attackResult.Afflictions)
{
if (healthChange < 0.0f)
if (affliction.Prefab.IsBuff) { continue; }
if (Params.IsMachine && !affliction.Prefab.AffectMachines) { continue; }
if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType ||
affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
{
float attackerSkillLevel = attacker.GetSkillLevel("weapons");
attacker.Info?.IncreaseSkillLevel("weapons".ToIdentifier(),
-healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f));
if (!Params.Health.PoisonImmunity)
{
float relativeVitality = MaxVitality / 100f;
// Undo the applied modifiers to get the base value. Poison damage is multiplied by max vitality when it's applied.
float dmg = affliction.Strength;
if (relativeVitality > 0)
{
dmg /= relativeVitality;
}
if (PoisonVulnerability > 0)
{
dmg /= PoisonVulnerability;
}
float strength = MaxVitality;
if (Params.AI != null)
{
strength = Params.AI.CombatStrength;
}
// Adjust the skill gain by the strength of the target. Combat strength >= 1000 gives 2x bonus, combat strength < 333 less than 1x.
float vitalityFactor = MathHelper.Lerp(0.5f, 2f, MathUtils.InverseLerp(0, 1000, strength));
dmg *= vitalityFactor;
medicalDamage += dmg * affliction.Prefab.MedicalSkillGain;
}
}
else
{
medicalDamage += affliction.GetVitalityDecrease(null) * affliction.Prefab.MedicalSkillGain;
}
weaponDamage += affliction.GetVitalityDecrease(null) * affliction.Prefab.WeaponsSkillGain;
}
else if (healthChange > 0.0f)
if (medicalDamage > 0)
{
float attackerSkillLevel = attacker.GetSkillLevel("medical");
attacker.Info?.IncreaseSkillLevel("medical".ToIdentifier(),
healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f));
IncreaseSkillLevel("medical".ToIdentifier(), medicalDamage);
}
if (weaponDamage > 0)
{
IncreaseSkillLevel("weapons".ToIdentifier(), weaponDamage);
}
void IncreaseSkillLevel(Identifier skill, float damage)
{
float attackerSkillLevel = attacker.GetSkillLevel(skill);
// The formula is too generous on low skill levels, hence the minimum divider.
float minSkillDivider = 15f;
attacker.Info?.IncreaseSkillLevel(skill, damage * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, minSkillDivider));
}
}
public void TryAdjustHealerSkill(Character healer, float healthChange = 0, Affliction affliction = null)
{
if (healer == null) { return; }
bool isEnemy = AIController is EnemyAIController || TeamID != healer.TeamID;
if (isEnemy) { return; }
float medicalGain = healthChange;
if (affliction?.Prefab is { IsBuff: true } && (!Params.IsMachine || affliction.Prefab.AffectMachines))
{
medicalGain += affliction.Strength * affliction.Prefab.MedicalSkillGain;
}
if (medicalGain <= 0) { return; }
Identifier skill = new Identifier("medical");
float attackerSkillLevel = healer.GetSkillLevel(skill);
// The formula is too generous on low skill levels, hence the minimum divider.
float minSkillDivider = 15f;
healer.Info?.IncreaseSkillLevel(skill, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, minSkillDivider));
}
/// <summary>
@@ -4142,7 +4269,7 @@ namespace Barotrauma
if (Screen.Selected != GameMain.GameScreen) { return; }
if (newStun > 0 && Params.Health.StunImmunity)
{
if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0)
if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0)
{
return;
}
@@ -4169,7 +4296,7 @@ namespace Barotrauma
float eatingRegen = Params.Health.HealthRegenerationWhenEating;
if (eatingRegen > 0)
{
CharacterHealth.ReduceAfflictionOnAllLimbs("damage".ToIdentifier(), eatingRegen * deltaTime);
CharacterHealth.ReduceAfflictionOnAllLimbs(AfflictionPrefab.DamageType, eatingRegen * deltaTime);
}
}
if (statusEffects.TryGetValue(actionType, out var statusEffectList))
@@ -4541,6 +4668,7 @@ namespace Barotrauma
Submarine = null;
AnimController.SetPosition(ConvertUnits.ToSimUnits(worldPos), lerp: false);
AnimController.FindHull(worldPos, setSubmarine: true);
CurrentHull = AnimController.CurrentHull;
if (AIController is HumanAIController humanAI)
{
humanAI.PathSteering?.ResetPath();
@@ -4787,34 +4915,36 @@ namespace Barotrauma
return visibleHulls;
}
public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null)
public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null) => GetRelativeSimPosition(this, target, worldPos);
public static Vector2 GetRelativeSimPosition(ISpatialEntity from, ISpatialEntity to, Vector2? worldPos = null)
{
Vector2 targetPos = target.SimPosition;
Vector2 targetPos = to.SimPosition;
if (worldPos.HasValue)
{
Vector2 wp = worldPos.Value;
if (target.Submarine != null)
if (to.Submarine != null)
{
wp -= target.Submarine.Position;
wp -= to.Submarine.Position;
}
targetPos = ConvertUnits.ToSimUnits(wp);
}
if (Submarine == null && target.Submarine != null)
if (from.Submarine == null && to.Submarine != null)
{
// outside and targeting inside
targetPos += target.Submarine.SimPosition;
targetPos += to.Submarine.SimPosition;
}
else if (Submarine != null && target.Submarine == null)
else if (from.Submarine != null && to.Submarine == null)
{
// inside and targeting outside
targetPos -= Submarine.SimPosition;
targetPos -= from.Submarine.SimPosition;
}
else if (Submarine != target.Submarine)
else if (from.Submarine != to.Submarine)
{
if (Submarine != null && target.Submarine != null)
if (from.Submarine != null && to.Submarine != null)
{
// both inside, but in different subs
Vector2 diff = Submarine.SimPosition - target.Submarine.SimPosition;
Vector2 diff = from.Submarine.SimPosition - to.Submarine.SimPosition;
targetPos -= diff;
}
}
@@ -4836,13 +4966,14 @@ namespace Barotrauma
public bool HasJob(Identifier identifier) => Info?.Job?.Prefab.Identifier == identifier;
public bool IsProtectedFromPressure()
{
return HasAbilityFlag(AbilityFlags.ImmuneToPressure) || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f);
}
/// <summary>
/// Is the character currently protected from the pressure by immunity/ability or a status effect (e.g. from a diving suit).
/// </summary>
public bool IsProtectedFromPressure => IsImmuneToPressure || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f);
// Talent logic begins here. Should be encapsulated to its own controller soon
public bool IsImmuneToPressure => !NeedsAir || HasAbilityFlag(AbilityFlags.ImmuneToPressure);
#region Talents
private readonly List<CharacterTalent> characterTalents = new List<CharacterTalent>();
public void LoadTalents()
@@ -4916,6 +5047,49 @@ namespace Barotrauma
return info.UnlockedTalents.Contains(identifier);
}
public bool HasUnlockedAllTalents()
{
if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree))
{
foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees)
{
foreach (TalentOption talentOption in talentSubTree.TalentOptionStages)
{
if (!talentOption.HasMaxTalents(info.UnlockedTalents))
{
return false;
}
}
}
}
return true;
}
public bool HasTalents()
{
return characterTalents.Any();
}
public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
{
foreach (var characterTalent in characterTalents)
{
characterTalent.CheckTalent(abilityEffectType, abilityObject);
}
}
public void CheckTalents(AbilityEffectType abilityEffectType)
{
foreach (var characterTalent in characterTalents)
{
characterTalent.CheckTalent(abilityEffectType, null);
}
}
partial void OnTalentGiven(TalentPrefab talentPrefab);
#endregion
private readonly HashSet<Hull> sameRoomHulls = new();
/// <summary>
@@ -4942,24 +5116,6 @@ namespace Barotrauma
return sameRoomHulls.Contains(character.CurrentHull);
}
public bool HasUnlockedAllTalents()
{
if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree))
{
foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees)
{
foreach (TalentOption talentOption in talentSubTree.TalentOptionStages)
{
if (!talentOption.HasMaxTalents(info.UnlockedTalents))
{
return false;
}
}
}
}
return true;
}
public static IEnumerable<Character> GetFriendlyCrew(Character character)
{
if (character is null)
@@ -4969,27 +5125,6 @@ namespace Barotrauma
return CharacterList.Where(c => HumanAIController.IsFriendly(character, c, onlySameTeam: true) && !c.IsDead);
}
public bool HasTalents()
{
return characterTalents.Any();
}
public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
{
foreach (var characterTalent in characterTalents)
{
characterTalent.CheckTalent(abilityEffectType, abilityObject);
}
}
public void CheckTalents(AbilityEffectType abilityEffectType)
{
foreach (var characterTalent in characterTalents)
{
characterTalent.CheckTalent(abilityEffectType, null);
}
}
public bool HasRecipeForItem(Identifier recipeIdentifier)
{
return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier));
@@ -5053,7 +5188,6 @@ namespace Barotrauma
#endif
partial void OnMoneyChanged(int prevAmount, int newAmount);
partial void OnTalentGiven(TalentPrefab talentPrefab);
/// <summary>
/// This dictionary is used for stats that are required very frequently. Not very performant, but easier to develop with for now.
@@ -5208,7 +5342,24 @@ namespace Barotrauma
public bool IsFriendly(Character other) => IsFriendly(this, other);
public static bool IsFriendly(Character me, Character other) => AIController.IsOnFriendlyTeam(me, other) && IsSameSpeciesOrGroup(me, other);
public static bool IsFriendly(Character me, Character other) => IsOnFriendlyTeam(me, other) && IsSameSpeciesOrGroup(me, other);
public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
{
if (myTeam == otherTeam) { return true; }
return myTeam switch
{
// NPCs are friendly to the same team and the friendly NPCs
CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC,
// Friendly NPCs are friendly to both player teams
CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2,
_ => true
};
}
public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID);
public bool IsOnFriendlyTeam(Character other) => IsOnFriendlyTeam(TeamID, other.TeamID);
public bool IsOnFriendlyTeam(CharacterTeamType otherTeam) => IsOnFriendlyTeam(TeamID, otherTeam);
public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other);
@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
using static Barotrauma.CharacterInfo;
namespace Barotrauma
{
@@ -19,6 +17,8 @@ namespace Barotrauma
public string Name => Identifier.Value;
public Identifier VariantOf { get; }
public CharacterPrefab ParentPrefab { get; set; }
public void InheritFrom(CharacterPrefab parent)
{
ConfigElement = CharacterParams.CreateVariantXml(originalElement, parent.ConfigElement).FromPackage(ConfigElement.ContentPackage);
@@ -38,7 +38,7 @@ namespace Barotrauma
}
}
private XElement originalElement;
private readonly XElement originalElement;
public ContentXElement ConfigElement { get; private set; }
public CharacterInfoPrefab CharacterInfoPrefab { get; private set; }
@@ -49,10 +49,6 @@ namespace Barotrauma
public static CharacterFile HumanConfigFile => HumanPrefab.ContentFile as CharacterFile;
public static CharacterPrefab HumanPrefab => FindBySpeciesName(HumanSpeciesName);
/// <summary>
/// Searches for a character config file from all currently selected content packages,
/// or from a specific package if the contentPackage parameter is given.
/// </summary>
public static CharacterPrefab FindBySpeciesName(Identifier speciesName)
{
if (!Prefabs.ContainsKey(speciesName)) { return null; }
@@ -19,6 +19,10 @@ namespace Barotrauma
private float fluctuationTimer;
private AfflictionPrefab.Effect activeEffect;
private float prevActiveEffectStrength;
protected bool activeEffectDirty = true;
protected float _strength;
[Serialize(0f, IsPropertySaveable.Yes), Editable]
@@ -46,6 +50,7 @@ namespace Barotrauma
Duration = Prefab.Duration;
}
_strength = newValue;
activeEffectDirty = true;
}
}
@@ -147,7 +152,16 @@ namespace Barotrauma
MathHelper.Clamp((int)Math.Floor(strength / maxStrength * strengthTexts.Length), 0, strengthTexts.Length - 1)];
}
public AfflictionPrefab.Effect GetActiveEffect() => Prefab.GetActiveEffect(Strength);
public AfflictionPrefab.Effect GetActiveEffect()
{
if (activeEffectDirty)
{
activeEffect = Prefab.GetActiveEffect(_strength);
prevActiveEffectStrength = _strength;
activeEffectDirty = false;
}
return activeEffect;
}
public float GetVitalityDecrease(CharacterHealth characterHealth)
{
@@ -158,7 +172,7 @@ namespace Barotrauma
{
if (strength < Prefab.ActivationThreshold) { return 0.0f; }
strength = MathHelper.Clamp(strength, 0.0f, Prefab.MaxStrength);
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(strength);
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return 0.0f; }
if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) { return 0.0f; }
@@ -349,7 +363,7 @@ namespace Barotrauma
public float GetStatValue(StatTypes statType)
{
if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return 0.0f; }
if (GetViableEffect() is not AfflictionPrefab.Effect currentEffect) { return 0.0f; }
if (currentEffect.AfflictionStatValues.TryGetValue(statType, out var value))
{
@@ -363,7 +377,7 @@ namespace Barotrauma
public bool HasFlag(AbilityFlags flagType)
{
if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return false; }
if (GetViableEffect() is not AfflictionPrefab.Effect currentEffect) { return false; }
return currentEffect.AfflictionAbilityFlags.HasFlag(flagType);
}
@@ -415,6 +429,7 @@ namespace Barotrauma
}
// Don't use the property, because it's virtual and some afflictions like husk overload it for external use.
_strength = MathHelper.Clamp(_strength, 0.0f, Prefab.MaxStrength);
activeEffectDirty |= !MathUtils.NearlyEqual(prevActiveEffectStrength, _strength);
foreach (StatusEffect statusEffect in currentEffect.StatusEffects)
{
@@ -442,7 +457,10 @@ namespace Barotrauma
var currentEffect = GetActiveEffect();
if (currentEffect != null)
{
currentEffect.StatusEffects.ForEach(se => ApplyStatusEffect(type, se, deltaTime, characterHealth, targetLimb));
foreach (var statusEffect in currentEffect.StatusEffects)
{
ApplyStatusEffect(type, statusEffect, deltaTime, characterHealth, targetLimb);
}
}
}
@@ -481,6 +499,7 @@ namespace Barotrauma
{
_nonClampedStrength = strength;
_strength = _nonClampedStrength;
activeEffectDirty |= !MathUtils.NearlyEqual(_strength, prevActiveEffectStrength);
}
public bool ShouldShowIcon(Character afflictedCharacter)
@@ -43,6 +43,7 @@ namespace Barotrauma
DeactivateHusk();
highestStrength = 0;
}
activeEffectDirty = true;
}
}
private float highestStrength;
@@ -319,6 +319,7 @@ namespace Barotrauma
{
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
public readonly float MinInterval, MaxInterval;
public readonly float MinStrength, MaxStrength;
public PeriodicEffect(ContentXElement element, string parentDebugName)
{
@@ -333,23 +334,38 @@ namespace Barotrauma
}
else
{
MinInterval = Math.Max(element.GetAttributeFloat("mininterval", 1.0f), 1.0f);
MaxInterval = Math.Max(element.GetAttributeFloat("maxinterval", 1.0f), MinInterval);
MinInterval = Math.Max(element.GetAttributeFloat(nameof(MinInterval), 1.0f), 1.0f);
MaxInterval = Math.Max(element.GetAttributeFloat(nameof(MaxInterval), 1.0f), MinInterval);
MinStrength = Math.Max(element.GetAttributeFloat(nameof(MinStrength), 0f), 0f);
MaxStrength = Math.Max(element.GetAttributeFloat(nameof(MaxStrength), MinStrength), MinStrength);
}
}
}
public static readonly Identifier DamageType = "damage".ToIdentifier();
public static readonly Identifier BurnType = "burn".ToIdentifier();
public static readonly Identifier BleedingType = "bleeding".ToIdentifier();
public static readonly Identifier ParalysisType = "paralysis".ToIdentifier();
public static readonly Identifier PoisonType = "poison".ToIdentifier();
public static readonly Identifier StunType = "stun".ToIdentifier();
public static readonly Identifier EMPType = "emp".ToIdentifier();
public static readonly Identifier SpaceHerpesType = "spaceherpes".ToIdentifier();
public static readonly Identifier AlienInfectedType = "alieninfected".ToIdentifier();
public static readonly Identifier InvertControlsType = "invertcontrols".ToIdentifier();
public static readonly Identifier HuskInfectionType = "huskinfection".ToIdentifier();
public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"];
public static AfflictionPrefab BiteWounds => Prefabs["bitewounds"];
public static AfflictionPrefab ImpactDamage => Prefabs["blunttrauma"];
public static AfflictionPrefab Bleeding => Prefabs["bleeding"];
public static AfflictionPrefab Burn => Prefabs["burn"];
public static AfflictionPrefab Bleeding => Prefabs[BleedingType];
public static AfflictionPrefab Burn => Prefabs[BurnType];
public static AfflictionPrefab OxygenLow => Prefabs["oxygenlow"];
public static AfflictionPrefab Bloodloss => Prefabs["bloodloss"];
public static AfflictionPrefab Pressure => Prefabs["pressure"];
public static AfflictionPrefab Stun => Prefabs["stun"];
public static AfflictionPrefab Stun => Prefabs[StunType];
public static AfflictionPrefab RadiationSickness => Prefabs["radiationsickness"];
public static readonly PrefabCollection<AfflictionPrefab> Prefabs = new PrefabCollection<AfflictionPrefab>();
public override void Dispose() { }
@@ -413,8 +429,8 @@ namespace Barotrauma
//how much karma changes when a player applies this affliction to someone (per strength of the affliction)
public float KarmaChangeOnApplied;
public float BurnOverlayAlpha;
public float DamageOverlayAlpha;
public readonly float BurnOverlayAlpha;
public readonly float DamageOverlayAlpha;
//steam achievement given when the affliction is removed from the controlled character
public readonly Identifier AchievementOnRemoved;
@@ -425,6 +441,20 @@ namespace Barotrauma
public readonly Sprite AfflictionOverlay;
public readonly bool AfflictionOverlayAlphaIsLinear;
public readonly bool DamageParticles;
/// <summary>
/// An arbitrary modifier that affects how much medical skill is increased when you apply the affliction on a target.
/// If the affliction causes damage or is of type poison or paralysis, the skill is increased only when the target is hostile.
/// If the affliction is of type buff, the skill is increased only when the target is friendly.
/// </summary>
public readonly float MedicalSkillGain;
/// <summary>
/// An arbitrary modifier that affects how much weapons skill is increased when you apply the affliction on a target.
/// The skill is increased only when the target is hostile.
/// </summary>
public readonly float WeaponsSkillGain;
private readonly List<Effect> effects = new List<Effect>();
private readonly List<PeriodicEffect> periodicEffects = new List<PeriodicEffect>();
@@ -519,8 +549,14 @@ namespace Barotrauma
KarmaChangeOnApplied = element.GetAttributeFloat(nameof(KarmaChangeOnApplied), 0.0f);
CauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}").Fallback(element.GetAttributeString("causeofdeathdescription", ""));
SelfCauseOfDeathDescription = TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}").Fallback(element.GetAttributeString("selfcauseofdeathdescription", ""));
CauseOfDeathDescription =
TextManager.Get($"AfflictionCauseOfDeath.{TranslationIdentifier}")
.Fallback(TextManager.Get(element.GetAttributeString("causeofdeathdescription", "")))
.Fallback(element.GetAttributeString("causeofdeathdescription", ""));
SelfCauseOfDeathDescription =
TextManager.Get($"AfflictionCauseOfDeathSelf.{TranslationIdentifier}")
.Fallback(TextManager.Get(element.GetAttributeString("selfcauseofdeathdescription", "")))
.Fallback(element.GetAttributeString("selfcauseofdeathdescription", ""));
IconColors = element.GetAttributeColorArray(nameof(IconColors), null);
AfflictionOverlayAlphaIsLinear = element.GetAttributeBool(nameof(AfflictionOverlayAlphaIsLinear), false);
@@ -530,6 +566,10 @@ namespace Barotrauma
ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false);
DamageParticles = element.GetAttributeBool(nameof(DamageParticles), true);
WeaponsSkillGain = element.GetAttributeFloat(nameof(WeaponsSkillGain), 0.0f);
MedicalSkillGain = element.GetAttributeFloat(nameof(MedicalSkillGain), 0.0f);
List<Description> descriptions = new List<Description>();
foreach (var subElement in element.Elements())
{
@@ -604,6 +644,18 @@ namespace Barotrauma
break;
}
}
for (int i = 0; i < effects.Count; i++)
{
for (int j = i + 1; j < effects.Count; j++)
{
var a = effects[i];
var b = effects[j];
if (a.MinStrength < b.MaxStrength && b.MinStrength < a.MaxStrength)
{
DebugConsole.AddWarning($"Affliction \"{Identifier}\" contains effects with overlapping strength ranges. Only one effect can be active at a time, meaning one of the effects won't work.");
}
}
}
}
public void ClearEffects()
@@ -230,6 +230,11 @@ namespace Barotrauma
public float StunTimer { get; private set; }
/// <summary>
/// Was the character in full health at the beginning of the frame?
/// </summary>
public bool WasInFullHealth { get; private set; }
public Affliction PressureAffliction
{
get { return pressureAffliction; }
@@ -334,12 +339,12 @@ namespace Barotrauma
return null;
}
public T GetAffliction<T>(string identifier, bool allowLimbAfflictions = true) where T : Affliction
public T GetAffliction<T>(Identifier identifier, bool allowLimbAfflictions = true) where T : Affliction
{
return GetAffliction(identifier, allowLimbAfflictions) as T;
}
public Affliction GetAffliction(string identifier, Limb limb)
public Affliction GetAffliction(Identifier identifier, Limb limb)
{
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
{
@@ -375,7 +380,7 @@ namespace Barotrauma
/// <param name="limb">The limb the affliction is attached to</param>
/// <param name="requireLimbSpecific">Does the affliction have to be attached to only the specific limb.
/// Most monsters for example don't have separate healths for different limbs, essentially meaning that every affliction is applied to every limb.</param>
public float GetAfflictionStrength(string afflictionType, Limb limb, bool requireLimbSpecific)
public float GetAfflictionStrength(Identifier afflictionType, Limb limb, bool requireLimbSpecific)
{
if (requireLimbSpecific && limbHealths.Count == 1) { return 0.0f; }
@@ -396,7 +401,7 @@ namespace Barotrauma
return strength;
}
public float GetAfflictionStrength(string afflictionType, bool allowLimbAfflictions = true)
public float GetAfflictionStrength(Identifier afflictionType, bool allowLimbAfflictions = true)
{
float strength = 0.0f;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
@@ -478,16 +483,19 @@ namespace Barotrauma
ReduceMatchingAfflictions(amount, treatmentAction);
}
public void ReduceAfflictionOnAllLimbs(Identifier affliction, float amount, ActionType? treatmentAction = null)
public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null)
{
if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); }
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
matchingAfflictions.Clear();
matchingAfflictions.AddRange(afflictions.Keys);
matchingAfflictions.RemoveAll(a =>
a.Prefab.Identifier != affliction &&
a.Prefab.AfflictionType != affliction);
foreach (var affliction in afflictions)
{
if (affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType)
{
matchingAfflictions.Add(affliction.Key);
}
}
ReduceMatchingAfflictions(amount, treatmentAction);
}
@@ -504,18 +512,21 @@ namespace Barotrauma
ReduceMatchingAfflictions(amount, treatmentAction);
}
public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier affliction, float amount, ActionType? treatmentAction = null)
public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null)
{
if (affliction.IsEmpty) { throw new ArgumentException($"{nameof(affliction)} is empty"); }
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
matchingAfflictions.Clear();
matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb));
matchingAfflictions.RemoveAll(a =>
a.Prefab.Identifier != affliction &&
a.Prefab.AfflictionType != affliction);
var targetLimbHealth = limbHealths[targetLimb.HealthIndex];
foreach (var affliction in afflictions)
{
if ((affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType) &&
affliction.Value == targetLimbHealth)
{
matchingAfflictions.Add(affliction.Key);
}
}
ReduceMatchingAfflictions(amount, treatmentAction);
}
@@ -617,7 +628,7 @@ namespace Barotrauma
KillIfOutOfVitality();
}
public float GetLimbDamage(Limb limb, string afflictionType = null)
public float GetLimbDamage(Limb limb, Identifier afflictionType)
{
float damageStrength;
if (limb.IsSevered)
@@ -630,16 +641,16 @@ namespace Barotrauma
// Therefore with e.g. 80 health, the max damage per limb would be 40.
// Having at least 40 damage on both legs would cause maximum limping.
float max = MaxVitality / 2;
if (string.IsNullOrEmpty(afflictionType))
if (afflictionType.IsEmpty)
{
float damage = GetAfflictionStrength("damage", limb, true);
float bleeding = GetAfflictionStrength("bleeding", limb, true);
float burn = GetAfflictionStrength("burn", limb, true);
float damage = GetAfflictionStrength(AfflictionPrefab.DamageType, limb, true);
float bleeding = GetAfflictionStrength(AfflictionPrefab.BleedingType, limb, true);
float burn = GetAfflictionStrength(AfflictionPrefab.BurnType, limb, true);
damageStrength = Math.Min(damage + bleeding + burn, max);
}
else
{
damageStrength = Math.Min(GetAfflictionStrength("damage", limb, true), max);
damageStrength = Math.Min(GetAfflictionStrength(afflictionType, limb, true), max);
}
return damageStrength / max;
}
@@ -696,15 +707,16 @@ namespace Barotrauma
if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; }
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun")
if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType)
{
if (Character.EmpVulnerability <= 0 || GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0)
if (Character.EmpVulnerability <= 0 || GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0)
{
return;
}
}
if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; }
if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == "emp") { return; }
if (Character.Params.Health.PoisonImmunity &&
(newAffliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || newAffliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)) { return; }
if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { return; }
if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; }
Affliction existingAffliction = null;
@@ -742,7 +754,8 @@ namespace Barotrauma
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))),
newAffliction.Source);
afflictions.Add(copyAffliction, limbHealth);
MedicalClinic.OnAfflictionCountChanged(Character);
Character.HealthUpdateInterval = 0.0f;
CalculateVitality();
@@ -766,6 +779,8 @@ namespace Barotrauma
public void Update(float deltaTime)
{
WasInFullHealth = vitality >= MaxVitality;
UpdateOxygen(deltaTime);
StunTimer = Stun > 0 ? StunTimer + deltaTime : 0;
@@ -813,10 +828,16 @@ namespace Barotrauma
}
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
}
foreach (var affliction in afflictionsToRemove)
{
afflictions.Remove(affliction);
}
}
if (afflictionsToRemove.Count is not 0)
{
MedicalClinic.OnAfflictionCountChanged(Character);
}
}
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed));
@@ -870,6 +891,11 @@ namespace Barotrauma
}
}
/// <summary>
/// 0-1.
/// </summary>
public float OxygenLowResistance => !Character.NeedsOxygen ? 1 : GetResistance(oxygenLowAffliction.Prefab);
private void UpdateOxygen(float deltaTime)
{
if (!Character.NeedsOxygen)
@@ -978,6 +1004,8 @@ namespace Barotrauma
UpdateLimbAfflictionOverlays();
UpdateSkinTint();
Character.Kill(type, affliction);
WasInFullHealth = false;
#if CLIENT
DisplayVitalityDelay = 0.0f;
DisplayedVitality = Vitality;
@@ -490,13 +490,9 @@ namespace Barotrauma
public int RefJointIndex => Params.RefJoint;
private List<WearableSprite> wearingItems;
public List<WearableSprite> WearingItems
{
get { return wearingItems; }
}
public readonly List<WearableSprite> WearingItems = new List<WearableSprite>();
public List<WearableSprite> OtherWearables { get; private set; } = new List<WearableSprite>();
public readonly List<WearableSprite> OtherWearables = new List<WearableSprite>();
public bool PullJointEnabled
{
@@ -640,7 +636,6 @@ namespace Barotrauma
this.ragdoll = ragdoll;
this.character = character;
this.Params = limbParams;
wearingItems = new List<WearableSprite>();
dir = Direction.Right;
body = new PhysicsBody(limbParams);
type = limbParams.Type;
@@ -772,7 +767,7 @@ namespace Barotrauma
tempModifiers.Add(damageModifier);
}
}
foreach (WearableSprite wearable in wearingItems)
foreach (WearableSprite wearable in WearingItems)
{
foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers)
{
@@ -791,10 +786,15 @@ namespace Barotrauma
}
if (!foundMatchingModifier && random > affliction.Probability) { continue; }
float finalDamageModifier = damageMultiplier;
if (affliction.Prefab.AfflictionType == "emp" && character.EmpVulnerability > 0)
if (character.EmpVulnerability > 0 && affliction.Prefab.AfflictionType == AfflictionPrefab.EMPType)
{
finalDamageModifier *= character.EmpVulnerability;
}
if (!character.Params.Health.PoisonImmunity &&
(affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType))
{
finalDamageModifier *= character.PoisonVulnerability;
}
foreach (DamageModifier damageModifier in tempModifiers)
{
float damageModifierValue = damageModifier.DamageMultiplier;
@@ -21,7 +21,7 @@ namespace Barotrauma
public Identifier SpeciesName { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable]
public string SpeciesTranslationOverride { get; private set; }
public Identifier SpeciesTranslationOverride { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable]
public string DisplayName { get; private set; }
@@ -113,6 +113,12 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool DrawLast { get; set; }
[Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons. Defaults to 1. Set 0 to tell the bots not to target this character at all. Distance to the target affects the decision making."), Editable]
public float AITurretPriority { get; set; }
[Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons tagged as \"slowturret\", like railguns. The tag is arbitrary and can be added to any turrets, just like the priority. Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making."), Editable]
public float AISlowTurretPriority { get; set; }
public readonly CharacterFile File;
public XDocument VariantFile { get; private set; }
@@ -479,7 +485,7 @@ namespace Barotrauma
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool DoesBleed { get; set; }
[Serialize(float.NegativeInfinity, IsPropertySaveable.Yes), Editable(minValue: float.NegativeInfinity, maxValue: 0)]
[Serialize(float.PositiveInfinity, IsPropertySaveable.Yes), Editable(minValue: 0, maxValue: float.PositiveInfinity)]
public float CrushDepth { get; set; }
// Make editable?
@@ -504,6 +510,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool PoisonImmunity { get; set; }
[Serialize(1f, IsPropertySaveable.Yes, description: "1 = default, 0 = immune."), Editable(MinValueFloat = 0f, MaxValueFloat = 1000, DecimalCount = 1)]
public float PoisonVulnerability { get; set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable]
public float EmpVulnerability { get; set; }
@@ -512,7 +521,20 @@ namespace Barotrauma
// TODO: limbhealths, sprite?
public HealthParams(ContentXElement element, CharacterParams character) : base(element, character) { }
public HealthParams(ContentXElement element, CharacterParams character) : base(element, character)
{
//backwards compatibility
if (CrushDepth < 0)
{
//invert y, convert to meters, and add 1000 to be on the safe side (previously the value would be from the bottom of the level)
float newCrushDepth = -CrushDepth * Physics.DisplayToRealWorldRatio + 1000;
DebugConsole.AddWarning($"Character \"{character.SpeciesName}\" has a negative crush depth. "+
"Previously the crush depths were defined as display units (e.g. -30000 would correspond to 300 meters below the level), "+
"but now they're in meters (e.g. 3000 would correspond to a depth of 3000 meters displayed on the nav terminal). "+
$"Changing the crush depth from {CrushDepth} to {newCrushDepth}.");
CrushDepth = newCrushDepth;
}
}
}
public class InventoryParams : SubParam
@@ -28,7 +28,6 @@ namespace Barotrauma.Abilities
conditionals.Add(new PropertyConditional(attribute));
}
}
break;
}
}
@@ -37,19 +37,7 @@ namespace Barotrauma.Abilities
if (itemPrefab != null)
{
if (category != MapEntityCategory.None)
{
if (!itemPrefab.Category.HasFlag(category)) { return false; }
}
if (identifiers.Any())
{
if (!identifiers.Any(t => itemPrefab.Identifier == t))
{
return false;
}
}
return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p));
return MatchesItem(itemPrefab);
}
else
{
@@ -57,5 +45,22 @@ namespace Barotrauma.Abilities
return false;
}
}
public bool MatchesItem(ItemPrefab itemPrefab)
{
if (category != MapEntityCategory.None)
{
if (!itemPrefab.Category.HasFlag(category)) { return false; }
}
if (identifiers.Any())
{
if (!identifiers.Any(t => itemPrefab.Identifier == t))
{
return false;
}
}
return !tags.Any() || tags.Any(t => itemPrefab.Tags.Any(p => t == p));
}
}
}
@@ -8,19 +8,19 @@ namespace Barotrauma.Abilities
class AbilityConditionMission : AbilityConditionData
{
private readonly ImmutableHashSet<MissionType> missionType;
private readonly ImmutableHashSet<Identifier> factions;
private readonly bool isAffiliated;
public AbilityConditionMission(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
string[] missionTypeStrings = conditionElement.GetAttributeStringArray("missiontype", new []{ "None" })!;
HashSet<MissionType> missionTypes = new HashSet<MissionType>();
factions = conditionElement.GetAttributeIdentifierImmutableHashSet("faction", ImmutableHashSet<Identifier>.Empty);
isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false);
foreach (string missionTypeString in missionTypeStrings)
{
if (!Enum.TryParse(missionTypeString, out MissionType parsedMission) || parsedMission is MissionType.None)
{
if (factions.IsEmpty)
if (!isAffiliated)
{
DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.");
}
@@ -37,23 +37,28 @@ namespace Barotrauma.Abilities
{
if (abilityObject is IAbilityMission { Mission: { } mission })
{
if (factions.Any())
{
if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; }
if (!isAffiliated) { return CheckMissionType(); }
foreach (var (factionIdentifier, amount) in mission.ReputationRewards)
if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; }
foreach (var (factionIdentifier, amount) in mission.ReputationRewards)
{
if (amount <= 0) { continue; }
if (GetMatchingFaction(factionIdentifier) is { } faction &&
Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive)
{
if (amount <= 0) { continue; }
if (factions.FirstOrDefault(faction => factionIdentifier == faction.Prefab.Identifier) is Faction faction &&
Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive)
{
return true;
}
return CheckMissionType();
}
return false;
}
return missionType.Contains(mission.Prefab.Type);
return false;
Faction GetMatchingFaction(Identifier factionIdentifier) =>
factionIdentifier == "location"
? mission.OriginLocation?.Faction
: factions.FirstOrDefault(f => factionIdentifier == f.Prefab.Identifier);
bool CheckMissionType() => missionType.IsEmpty || missionType.Contains(mission.Prefab.Type);
}
LogAbilityConditionError(abilityObject, typeof(IAbilityMission));
@@ -1,19 +1,35 @@
#nullable enable
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Abilities
{
internal sealed class CharacterAbilityRemoveRandomIngredient : CharacterAbility
{
public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { }
private readonly AbilityConditionItem? condition;
public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
var conditionElement = abilityElement.GetChildElement(nameof(AbilityConditionItem));
if (conditionElement != null)
{
condition = new AbilityConditionItem(CharacterTalent, conditionElement);
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if (abilityObject is not Fabricator.AbilityFabricationItemIngredients { Items.Count: > 0 } ingredients) { return; }
int randomIndex = Rand.Int(ingredients.Items.Count, Rand.RandSync.Unsynced);
ingredients.Items.RemoveAt(randomIndex);
List<Item> applicableIngredients = condition == null ?
ingredients.Items.ToList() :
ingredients.Items.Where(it => condition.MatchesItem(it.Prefab)).ToList();
if (applicableIngredients.None()) { return; }
ingredients.Items.Remove(applicableIngredients.GetRandom(Rand.RandSync.Unsynced));
}
}
}