Unstable 1.1.14.0

This commit is contained in:
Markus Isberg
2023-10-02 16:43:54 +03:00
parent 94f5a93a0c
commit cf8f0de659
606 changed files with 21906 additions and 11456 deletions
@@ -331,6 +331,11 @@ namespace Barotrauma
}
}
if (targetSlot < 0) { return false; }
//the item should always stay in the Any slot if it's containable in one
if (pickable.AllowedSlots.Contains(InvSlotType.Any))
{
targetInventory.TryPutItem(item, Character, CharacterInventory.AnySlot);
}
return targetInventory.TryPutItem(item, targetSlot, allowSwapping, allowCombine: false, Character);
}
else
@@ -521,8 +526,5 @@ namespace Barotrauma
protected virtual void OnStateChanged(AIState from, AIState to) { }
protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { }
public virtual void ClientRead(IReadMessage msg) { }
public virtual void ServerWrite(IWriteMessage msg) { }
}
}
@@ -344,12 +344,12 @@ namespace Barotrauma
private Identifier GetTargetingTag(AITarget aiTarget)
{
if (aiTarget?.Entity == null) { return Identifier.Empty; }
string targetingTag = string.Empty;
Identifier targetingTag = Identifier.Empty;
if (aiTarget.Entity is Character targetCharacter)
{
if (targetCharacter.IsDead)
{
targetingTag = "dead";
targetingTag = "dead".ToIdentifier();
}
else if (AIParams.TryGetTarget(targetCharacter.CharacterHealth.GetActiveAfflictionTags(), out CharacterParams.TargetParams tp) && tp.Threshold >= Character.GetDamageDoneByAttacker(targetCharacter))
{
@@ -357,11 +357,11 @@ namespace Barotrauma
}
else if (PetBehavior != null && aiTarget.Entity == PetBehavior.Owner)
{
targetingTag = "owner";
targetingTag = "owner".ToIdentifier();
}
else if (PetBehavior != null && (!Character.IsOnFriendlyTeam(targetCharacter) || IsAttackingOwner(targetCharacter)))
{
targetingTag = "hostile";
targetingTag = "hostile".ToIdentifier();
}
else if (AIParams.TryGetTarget(targetCharacter, out CharacterParams.TargetParams tP))
{
@@ -373,25 +373,25 @@ namespace Barotrauma
{
// Pets see other pets as pets by default.
// Monsters see them only as pet only when they have a matching ai target. Otherwise they use the other tags, specified below.
targetingTag = "pet";
targetingTag = "pet".ToIdentifier();
}
else if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
{
targetingTag = "husk";
targetingTag = "husk".ToIdentifier();
}
else if (!Character.IsSameSpeciesOrGroup(targetCharacter))
{
if (enemy.CombatStrength > CombatStrength)
{
targetingTag = "stronger";
targetingTag = "stronger".ToIdentifier();
}
else if (enemy.CombatStrength < CombatStrength)
{
targetingTag = "weaker";
targetingTag = "weaker".ToIdentifier();
}
else
{
targetingTag = "equal";
targetingTag = "equal".ToIdentifier();
}
}
}
@@ -406,27 +406,27 @@ namespace Barotrauma
break;
}
}
if (targetingTag.IsNullOrEmpty())
if (targetingTag.IsEmpty)
{
if (targetItem.GetComponent<Sonar>() != null)
{
targetingTag = "sonar";
targetingTag = "sonar".ToIdentifier();
}
if (targetItem.GetComponent<Door>() != null)
{
targetingTag = "door";
targetingTag = "door".ToIdentifier();
}
}
}
else if (aiTarget.Entity is Structure)
{
targetingTag = "wall";
targetingTag = "wall".ToIdentifier();
}
else if (aiTarget.Entity is Hull)
{
targetingTag = "room";
targetingTag = "room".ToIdentifier();
}
return targetingTag.ToIdentifier();
return targetingTag;
}
public override void SelectTarget(AITarget target) => SelectTarget(target, 100);
@@ -767,7 +767,7 @@ namespace Barotrauma
mainLimb.body.SmoothRotate(rotation, Character.AnimController.SwimFastParams.TorsoTorque);
}
}
if (disableTailCoroutine == null && SelectedAiTarget.Entity is Item i && i.HasTag("guardianshelter"))
if (disableTailCoroutine == null && SelectedAiTarget.Entity is Item i && i.HasTag(Tags.GuardianShelter))
{
if (!CoroutineManager.IsCoroutineRunning(disableTailCoroutine))
{
@@ -864,10 +864,11 @@ namespace Barotrauma
}
// Ensure that the creature keeps inside the level
SteerInsideLevel(deltaTime);
float speed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun);
steeringManager.Update(speed);
float targetMovement = useSteeringLengthAsMovementSpeed ? Steering.Length() : speed;
Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, targetMovement);
float defaultSpeed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun);
//calculate a normalized Steering value at this point: we multiply it with the actual, desired speed in ApplyMovementLimits
steeringManager.Update(1.0f);
float speed = useSteeringLengthAsMovementSpeed ? Steering.Length() : defaultSpeed;
Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, speed);
if (Character.CurrentHull != null && Character.AnimController.InWater)
{
// Limit the swimming speed inside the sub.
@@ -1935,14 +1936,7 @@ namespace Barotrauma
}
else if (advance)
{
if (pathSteering != null)
{
pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize);
}
else
{
SteeringManager.SteeringSeek(steerPos, 10);
}
SteeringManager.SteeringSeek(steerPos, 10);
}
else
{
@@ -2310,7 +2304,7 @@ namespace Barotrauma
if (damageTarget != null)
{
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
item.Use(deltaTime, Character);
item.Use(deltaTime, user: Character);
}
}
}
@@ -2545,6 +2539,7 @@ namespace Barotrauma
item.body.LinearVelocity *= 0.9f;
item.body.LinearVelocity -= velocity * 0.25f;
bool wasBroken = item.Condition <= 0.0f;
item.LastEatenTime = (float)Timing.TotalTimeUnpaused;
item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime);
Character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
if (item.Condition <= 0.0f)
@@ -3116,6 +3111,13 @@ namespace Barotrauma
// ignore if owner is tagged to be explicitly ignored (Feign Death)
continue;
}
var characterTargetingTag = GetTargetingTag(owner.AiTarget);
if (!characterTargetingTag.IsEmpty)
{
// if the enemy is configured to ignore the target character, ignore the provocative item they're holding/wearing too
var characterTargetingParams = GetTargetParams(characterTargetingTag);
if (characterTargetingParams?.State == AIState.Idle) { continue; }
}
}
}
if (targetCharacter != null)
@@ -4055,18 +4057,6 @@ namespace Barotrauma
}
return null;
}
public override void ServerWrite(IWriteMessage msg)
{
msg.WriteByte((byte)State);
PetBehavior?.ServerWrite(msg);
}
public override void ClientRead(IReadMessage msg)
{
State = (AIState)msg.ReadByte();
PetBehavior?.ClientRead(msg);
}
}
//the "memory" of the Character
@@ -355,7 +355,7 @@ namespace Barotrauma
Vector2 forward = VectorExtensions.Forward(rotation);
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward));
if (angle > 70) { continue; }
if (!Character.CanSeeCharacter(c)) { continue; }
if (!Character.CanSeeTarget(c)) { continue; }
if (dist < closestDistance || closestEnemy == null)
{
closestEnemy = c;
@@ -465,7 +465,7 @@ namespace Barotrauma
else
{
// Allows bots to heal targets autonomously while swimming outside of the sub.
if (AIObjectiveRescueAll.IsValidTarget(Character, Character))
if (AIObjectiveRescueAll.IsValidTarget(Character, Character, out _))
{
AddTargets<AIObjectiveRescueAll, Character>(Character, Character);
}
@@ -480,24 +480,33 @@ namespace Barotrauma
if (objectiveManager.CurrentObjective == null) { return; }
objectiveManager.DoCurrentObjective(deltaTime);
bool run = (objectiveManager.CurrentObjective.ForceRun && !objectiveManager.CurrentObjective.ForceWalk) || (!objectiveManager.CurrentObjective.ForceWalk && objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority);
if (ObjectiveManager.CurrentObjective is AIObjectiveGoTo goTo && goTo.Target != null)
var currentObjective = objectiveManager.CurrentObjective;
bool run = !currentObjective.ForceWalk && (currentObjective.ForceRun || objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority);
if (currentObjective is AIObjectiveGoTo goTo)
{
if (Character.CurrentHull == null)
if (run && goTo == objectiveManager.ForcedOrder && goTo.IsWaitOrder && !Character.IsOnPlayerTeam)
{
run = Vector2.DistanceSquared(Character.WorldPosition, goTo.Target.WorldPosition) > 300 * 300;
// NPCs with a wait order don't run.
run = false;
}
else
else if (goTo.Target != null)
{
float yDiff = goTo.Target.WorldPosition.Y - Character.WorldPosition.Y;
if (Math.Abs(yDiff) > 100)
if (Character.CurrentHull == null)
{
run = true;
run = Vector2.DistanceSquared(Character.WorldPosition, goTo.Target.WorldPosition) > 300 * 300;
}
else
{
float xDiff = goTo.Target.WorldPosition.X - Character.WorldPosition.X;
run = Math.Abs(xDiff) > 500;
float yDiff = goTo.Target.WorldPosition.Y - Character.WorldPosition.Y;
if (Math.Abs(yDiff) > 100)
{
run = true;
}
else
{
float xDiff = goTo.Target.WorldPosition.X - Character.WorldPosition.X;
run = Math.Abs(xDiff) > 500;
}
}
}
}
@@ -577,7 +586,7 @@ namespace Barotrauma
if (Character.LockHands) { return; }
if (ObjectiveManager.CurrentObjective == null) { return; }
if (Character.CurrentHull == null) { return; }
bool shouldActOnSuffocation = Character.IsLowInOxygen && !Character.AnimController.HeadInWater && HasDivingSuit(Character, requireOxygenTank: false) && !HasItem(Character, AIObjectiveFindDivingGear.OXYGEN_SOURCE, out _, conditionPercentage: 1);
bool shouldActOnSuffocation = Character.IsLowInOxygen && !Character.AnimController.HeadInWater && HasDivingSuit(Character, requireOxygenTank: false) && !HasItem(Character, Tags.OxygenSource, out _, conditionPercentage: 1);
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>();
bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
@@ -594,7 +603,7 @@ namespace Barotrauma
if (findItemState != FindItemState.OtherItem)
{
var decontain = ObjectiveManager.GetActiveObjectives<AIObjectiveDecontainItem>().LastOrDefault();
if (decontain != null && decontain.TargetItem != null && decontain.TargetItem.HasTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR) &&
if (decontain != null && decontain.TargetItem != null && decontain.TargetItem.HasTag(Tags.HeavyDivingGear) &&
ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective))
{
// Don't try to put the diving suit in a locker if the suit would be needed in any hull in the path to the locker.
@@ -680,8 +689,8 @@ namespace Barotrauma
}
if (removeDivingSuit)
{
var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR);
if (divingSuit != null && !divingSuit.HasTag(AIObjectiveFindDivingGear.DIVING_GEAR_WEARABLE_INDOORS))
var divingSuit = Character.Inventory.FindItemByTag(Tags.HeavyDivingGear);
if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors))
{
if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
@@ -723,9 +732,9 @@ namespace Barotrauma
}
if (takeMaskOff)
{
if (Character.HasEquippedItem(AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR))
if (Character.HasEquippedItem(Tags.LightDivingGear))
{
var mask = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR);
var mask = Character.Inventory.FindItemByTag(Tags.LightDivingGear);
if (mask != null)
{
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
@@ -928,7 +937,7 @@ namespace Barotrauma
if (isPreferencesDefined)
{
// Use any valid locker as a fall back container.
return container.Item.HasTag("locker") ? 0.5f : 0;
return container.Item.HasTag(Tags.FallbackLocker) ? 0.5f : 0;
}
return 1;
}
@@ -1069,7 +1078,7 @@ namespace Barotrauma
foreach (Character target in Character.CharacterList)
{
if (target.CurrentHull != hull) { continue; }
if (AIObjectiveRescueAll.IsValidTarget(target, Character))
if (AIObjectiveRescueAll.IsValidTarget(target, Character, out _))
{
if (AddTargets<AIObjectiveRescueAll, Character>(Character, target) && newOrder == null && (!Character.IsMedic || Character == target) && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
{
@@ -1287,6 +1296,7 @@ namespace Barotrauma
minorDamageThreshold = 10;
majorDamageThreshold = 40;
}
bool eitherIsMentallyUnstable = IsMentallyUnstable || attacker.AIController is { IsMentallyUnstable: true };
if (IsFriendly(attacker))
{
if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character)
@@ -1296,7 +1306,7 @@ namespace Barotrauma
return;
}
float cumulativeDamage = realDamage + Character.GetDamageDoneByAttacker(attacker);
bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && attacker.CombatAction == null;
bool isAccidental = attacker.IsBot && !eitherIsMentallyUnstable && attacker.CombatAction == null;
if (isAccidental)
{
if (attacker.TeamID != Character.TeamID || (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold))
@@ -1306,7 +1316,7 @@ namespace Barotrauma
}
else
{
isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.AlienInfectedType) > 0;
isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.AlienInfectedType) > 0;
// Inform other NPCs
if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold)
{
@@ -1378,6 +1388,11 @@ namespace Barotrauma
if (otherCharacter.IsPlayer) { continue; }
if (otherCharacter.AIController is not HumanAIController otherHumanAI) { continue; }
if (!otherHumanAI.IsFriendly(Character)) { continue; }
if (attacker.AIController is EnemyAIController enemyAI && otherHumanAI.IsFriendly(attacker))
{
// Don't react to friendly enemy AI attacking other characters. E.g. husks attacking someone when whe are a cultist.
continue;
}
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull);
if (!isWitnessing)
{
@@ -1411,6 +1426,10 @@ namespace Barotrauma
humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.GetTarget() is Controller ?
AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat;
}
if (c.IsKiller)
{
return AIObjectiveCombat.CombatMode.Offensive;
}
return
humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() ||
humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders) ?
@@ -1442,6 +1461,10 @@ namespace Barotrauma
isAttackerFightingEnemy = true;
return AIObjectiveCombat.CombatMode.None;
}
if (c.IsKiller)
{
return AIObjectiveCombat.CombatMode.Offensive;
}
if (isWitnessing && c.CombatAction != null && !c.IsSecurity)
{
return c.CombatAction.WitnessReaction;
@@ -1452,7 +1475,7 @@ namespace Barotrauma
isAttackerFightingEnemy = true;
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (instigator.CombatAction != null ? instigator.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
}
if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable))
if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !eitherIsMentallyUnstable)
{
if (c.IsSecurity)
{
@@ -1653,7 +1676,7 @@ namespace Barotrauma
needsSuit = (hull == null || hull.LethalPressure > 0) && !Character.IsImmuneToPressure;
return needsAir || needsSuit;
}
if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
if (hull.WaterPercentage > 60 || (hull.IsWetRoom && hull.WaterPercentage > 10) || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
{
return needsAir;
}
@@ -1666,14 +1689,14 @@ namespace Barotrauma
/// Check whether the character has a diving suit in usable condition plus some oxygen.
/// </summary>
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,
=> HasItem(character, Tags.HeavyDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true,
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes));
/// <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, bool requireOxygenTank = true)
=> HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true);
=> HasItem(character, Tags.LightDivingGear, out _, requireOxygenTank ? Tags.OxygenSource : Identifier.Empty, conditionPercentage, requireEquipped: true);
private static List<Item> matchingItems = new List<Item>();
@@ -1739,7 +1762,7 @@ namespace Barotrauma
{
continue;
}
if (!otherCharacter.CanSeeCharacter(character)) { continue; }
if (!otherCharacter.CanSeeTarget(character)) { continue; }
if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); }
float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character];
@@ -1816,7 +1839,7 @@ namespace Barotrauma
bool someoneSpoke = false;
bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true).Any() ?? false;
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag("handlocker"))
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag(Tags.HandLockerItem))
{
foreach (Character otherCharacter in Character.CharacterList)
{
@@ -1827,14 +1850,14 @@ namespace Barotrauma
continue;
}
//if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; }
if (!otherCharacter.CanSeeCharacter(thief)) { continue; }
if (!otherCharacter.CanSeeTarget(thief)) { continue; }
// Don't react if the player is taking an extinguisher and there's any fires on the sub, or diving gear when the sub is flooding
// -> allow them to use the emergency items
if (thief.Submarine != null)
{
var connectedHulls = thief.Submarine.GetHulls(alsoFromConnectedSubs: true);
if (item.HasTag("fireextinguisher") && connectedHulls.Any(h => h.FireSources.Any())) { continue; }
if (item.HasTag("diving") && connectedHulls.Any(h => h.ConnectedGaps.Any(g => AIObjectiveFixLeaks.IsValidTarget(g, thief)))) { continue; }
if (item.HasTag(Tags.FireExtinguisher) && connectedHulls.Any(h => h.FireSources.Any())) { continue; }
if (item.HasTag(Tags.DivingGear) && connectedHulls.Any(h => h.ConnectedGaps.Any(g => AIObjectiveFixLeaks.IsValidTarget(g, thief)))) { continue; }
}
if (!someoneSpoke)
{
@@ -1966,7 +1989,7 @@ namespace Barotrauma
foreach (var c in Character.CharacterList)
{
if (c.CurrentHull != hull) { continue; }
if (AIObjectiveRescueAll.IsValidTarget(c, character))
if (AIObjectiveRescueAll.IsValidTarget(c, character, out _))
{
AddTargets<AIObjectiveRescueAll, Character>(character, c);
}
@@ -2171,7 +2194,10 @@ namespace Barotrauma
{
bool sameTeam = me.TeamID == other.TeamID;
bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other);
if (!teamGood) { return false; }
if (!teamGood)
{
return other.IsHusk && me.IsDisguisedAsHusk;
}
if (other.IsPet)
{
// Hostile NPCs are hostile to all pets, unless they are in the same team.
@@ -241,7 +241,6 @@ namespace Barotrauma
float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority);
findPathTimer = priority * Rand.Range(1.0f, 1.2f);
IsPathDirty = false;
return DiffToCurrentNode();
void SkipCurrentPathNodes()
{
@@ -284,15 +283,6 @@ namespace Barotrauma
}
Vector2 diff = DiffToCurrentNode();
var collider = character.AnimController.Collider;
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController;
//if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically
if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.Height / 2 + collider.Radius)
{
// TODO: might cause some edge cases -> do we need this?
diff.Y = 0.0f;
}
if (diff == Vector2.Zero) { return Vector2.Zero; }
return Vector2.Normalize(diff) * weight;
}
@@ -401,16 +391,15 @@ namespace Barotrauma
else
{
bool nextLadderSameAsCurrent = currentLadder == nextLadder;
float colliderHeight = collider.Height / 2 + collider.Radius;
float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X);
if (currentLadder != null && nextLadder != null)
{
//climbing ladders -> don't move horizontally
diff.X = 0.0f;
}
//at the same height as the waypoint
float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y);
float colliderHeight = collider.Height / 2 + collider.Radius;
float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X);
if (heightDiff < colliderHeight * 1.25f)
if (Math.Abs(heightDiff) < colliderHeight * 1.25f)
{
if (nextLadder != null && !nextLadderSameAsCurrent)
{
@@ -463,10 +452,10 @@ namespace Barotrauma
}
}
}
return ConvertUnits.ToSimUnits(diff);
}
else if (character.AnimController.InWater)
{
// Swimming
var door = currentPath.CurrentNode.ConnectedDoor;
if (door == null || door.CanBeTraversed)
{
@@ -502,6 +491,13 @@ namespace Barotrauma
bool isTargetTooLow = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y;
var door = currentPath.CurrentNode.ConnectedDoor;
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
float colliderHeight = collider.Height / 2 + collider.Radius;
float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
if (heightDiff < colliderHeight)
{
//the waypoint is between the top and bottom of the collider, no need to move vertically.
diff.Y = 0.0f;
}
if (currentPath.CurrentNode.Stairs != null)
{
bool isNextNodeInSameStairs = currentPath.NextNode?.Stairs == currentPath.CurrentNode.Stairs;
@@ -328,7 +328,7 @@ namespace Barotrauma
if (checkedSpeakers.Any(s => !potentialSpeaker.CanHearCharacter(s))) { return false; }
//check if the character is close enough to see the rest of the speakers (this should be replaced with a more performant method)
if (checkedSpeakers.Any(s => !potentialSpeaker.CanSeeCharacter(s))) { return false; }
if (checkedSpeakers.Any(s => !potentialSpeaker.CanSeeTarget(s))) { return false; }
//check if the character has an appropriate personality
if (selectedConversation.allowedSpeakerTags.Count > 0)
@@ -40,6 +40,7 @@ namespace Barotrauma
public virtual bool AllowOutsideSubmarine => false;
public virtual bool AllowInFriendlySubs => false;
public virtual bool AllowInAnySub => false;
public virtual bool AllowWhileHandcuffed => true;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
private float _cumulatedDevotion;
@@ -246,32 +247,43 @@ namespace Barotrauma
{
get
{
if (IgnoreAtOutpost && Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
{
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
{
return false;
}
}
if (!AllowWhileHandcuffed && character.LockHands) { return false; }
if (!AllowOutsideSubmarine && character.Submarine == null) { return false; }
// Evaluate ignored at outpost first, because it has higher priority than AllowInAnySub or AllowInFriendlySubs.
if (IsIgnoredAtOutpost()) { return false; }
if (AllowInAnySub) { return true; }
if ((AllowInFriendlySubs && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC) || character.IsEscorted) { return true; }
return character.Submarine.TeamID == character.TeamID ||
character.Submarine.TeamID == character.OriginalTeamID ||
character.Submarine.DockedTo.Any(sub => sub.TeamID == character.TeamID || sub.TeamID == character.OriginalTeamID);
return character.Submarine.TeamID == character.TeamID || character.Submarine.TeamID == character.OriginalTeamID;
}
}
/// <summary>
/// Returns true only when at a friendly outpost and when the order is set to be ignored there.
/// Note that even if this returns false, the objective can be disallowed, because AllowInFriendlySubs is false.
/// </summary>
public bool IsIgnoredAtOutpost()
{
if (!IgnoreAtOutpost) { return false; }
if (!Level.IsLoadedFriendlyOutpost) { return false; }
if (!character.IsOnPlayerTeam) { return false; }
if (character.Submarine?.Info == null) { return false; }
return character.Submarine.Info.IsOutpost && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC;
}
protected void HandleNonAllowed()
{
Priority = 0;
Abandon = !IsIgnoredAtOutpost();
}
protected virtual float GetPriority()
{
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
HandleNonAllowed();
return Priority;
}
if (isOrder)
if (objectiveManager.IsOrder(this))
{
Priority = objectiveManager.GetOrderPriority(this);
}
@@ -446,7 +458,7 @@ namespace Barotrauma
}
}
protected virtual bool Check()
private bool Check()
{
if (AbortCondition != null && AbortCondition(this))
{
@@ -19,6 +19,7 @@ namespace Barotrauma
protected override bool Filter(PowerContainer battery)
{
if (battery == null) { return false; }
if (battery.OutputDisabled) { return false; }
var item = battery.Item;
if (item.IgnoreByAI(character)) { return false; }
if (!item.IsInteractable(character)) { return false; }
@@ -12,6 +12,7 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "cleanup item".ToIdentifier();
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => false;
public override bool AllowWhileHandcuffed => false;
public readonly Item item;
public bool IsPriority { get; set; }
@@ -30,8 +31,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
HandleNonAllowed();
return Priority;
}
else
@@ -119,18 +119,21 @@ namespace Barotrauma
if (item.IgnoreByAI(character))
{
Abandon = true;
return false;
}
if (item.ParentInventory != null)
else if (item.ParentInventory != null)
{
if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character, allowUnloading: objectiveManager.HasOrder<AIObjectiveCleanupItems>()))
if (!objectiveManager.HasOrder<AIObjectiveCleanupItems>())
{
// Don't allow taking items from containers in the idle state.
Abandon = true;
}
else if (item.Container != null && !AIObjectiveCleanupItems.IsValidContainer(item.Container, character))
{
// Target was picked up or moved by someone.
Abandon = true;
return false;
}
}
return IsCompleted;
return !Abandon && IsCompleted;
}
public override void Reset()
@@ -14,8 +14,6 @@ namespace Barotrauma
public readonly List<Item> prioritizedItems = new List<Item>();
public static readonly Identifier AllowCleanupTag = "allowcleanup".ToIdentifier();
protected override int MaxTargets => 100;
public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, Item prioritizedItem = null, float priorityModifier = 1)
@@ -83,9 +81,8 @@ namespace Barotrauma
return true;
}
public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) =>
allowUnloading &&
container.HasTag(AllowCleanupTag) &&
public static bool IsValidContainer(Item container, Character character) =>
container.HasTag(Tags.AllowCleanup) &&
container.HasAccess(character) &&
container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() &&
container.GetComponent<ItemContainer>() != null &&
@@ -103,15 +100,18 @@ namespace Barotrauma
// In a character inventory
return false;
}
if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; }
if (!allowUnloading) { return false; }
if (!IsValidContainer(item.Container, character)) { return false; }
}
if (!item.HasAccess(character)) { return false; }
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.HasBallastFloraInHull) { return false; }
//something (e.g. a pet) was eating the item within the last second - don't clean up
if (item.LastEatenTime > Timing.TotalTimeUnpaused - 1.0) { return false; }
var wire = item.GetComponent<Wire>();
if (wire != null)
{
if (wire.Connections.Any()) { return false; }
if (wire.Connections.Any(c => c != null)) { return false; }
}
else
{
@@ -46,7 +46,6 @@ namespace Barotrauma
_weapon = value;
_weaponComponent = null;
hasAimed = false;
RemoveSubObjective(ref seekAmmunitionObjective);
}
}
private ItemComponent _weaponComponent;
@@ -55,14 +54,7 @@ namespace Barotrauma
get
{
if (Weapon == null) { return null; }
if (_weaponComponent == null)
{
_weaponComponent =
Weapon.GetComponent<RangedWeapon>() ??
Weapon.GetComponent<MeleeWeapon>() ??
Weapon.GetComponent<RepairTool>() as ItemComponent;
}
return _weaponComponent;
return _weaponComponent ?? GetWeaponComponent(Weapon);
}
}
@@ -280,13 +272,13 @@ namespace Barotrauma
}
break;
case CombatMode.Arrest:
if (HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out _, requireEquipped: true))
if (HumanAIController.HasItem(Enemy, Tags.HandLockerItem, out _, requireEquipped: true))
{
IsCompleted = true;
}
else if (Enemy.IsKnockedDown &&
!objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>() &&
!HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out _, requireEquipped: false))
!HumanAIController.HasItem(character, Tags.HandLockerItem, out _, requireEquipped: false))
{
IsCompleted = true;
}
@@ -348,8 +340,10 @@ namespace Barotrauma
if (character.LockHands || Enemy == null)
{
Weapon = null;
RemoveSubObjective(ref seekAmmunitionObjective);
return false;
}
bool isAllowedToSeekWeapons = character.CurrentHull != null && !IsEnemyCloserThan(300) && character.IsOnPlayerTeam && IsOffensiveOrArrest;
if (checkWeaponsTimer < 0)
{
checkWeaponsTimer = checkWeaponsInterval;
@@ -375,7 +369,7 @@ namespace Barotrauma
// All good, the weapon is loaded
break;
}
if (Reload(seekAmmo: false))
if (Reload(seekAmmo: isAllowedToSeekWeapons))
{
// All good, we can use the weapon.
break;
@@ -407,7 +401,6 @@ namespace Barotrauma
}
}
}
bool isAllowedToSeekWeapons = character.CurrentHull != null && !IsEnemyCloserThan(300) && character.IsOnPlayerTeam && IsOffensiveOrArrest;
if (!isAllowedToSeekWeapons)
{
if (WeaponComponent == null)
@@ -416,7 +409,7 @@ namespace Barotrauma
Mode = CombatMode.Retreat;
}
}
else if (seekAmmunitionObjective == null && (WeaponComponent == null || WeaponComponent.CombatPriority < goodWeaponPriority))
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority)))
{
// Poor weapon equipped -> try to find better.
RemoveSubObjective(ref seekAmmunitionObjective);
@@ -431,27 +424,10 @@ namespace Barotrauma
{
if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; }
if (i.IsOwnedBy(character)) { return 0; }
var mw = i.GetComponent<MeleeWeapon>();
var rw = i.GetComponent<RangedWeapon>();
float priority = 0;
if (mw != null)
if (GetWeaponComponent(i) is ItemComponent ic)
{
priority = mw.CombatPriority / 100;
}
else if (rw != null)
{
priority = rw.CombatPriority / 100;
}
if (i.HasTag("stunner"))
{
if (Mode == CombatMode.Arrest)
{
priority *= 2;
}
else
{
priority /= 2;
}
priority = GetWeaponPriority(ic, prioritizeMelee: false, isCloseToEnemy: false, out _) / 100;
}
return priority;
}
@@ -477,6 +453,7 @@ namespace Barotrauma
if (!CheckWeapon(seekAmmo: false))
{
Weapon = null;
RemoveSubObjective(ref seekAmmunitionObjective);
}
}
return Weapon != null;
@@ -521,111 +498,164 @@ namespace Barotrauma
private Item FindWeapon(out ItemComponent weaponComponent) => GetWeapon(FindWeaponsFromInventory(), out weaponComponent);
private static ItemComponent GetWeaponComponent(Item item) =>
item.GetComponent<MeleeWeapon>() ??
item.GetComponent<RangedWeapon>() ??
item.GetComponent<RepairTool>() ??
item.GetComponent<Holdable>() as ItemComponent;
private float GetWeaponPriority(ItemComponent weapon, bool prioritizeMelee, bool isCloseToEnemy, out float lethalDmg)
{
lethalDmg = -1;
float priority = weapon.CombatPriority;
if (weapon is RepairTool repairTool)
{
switch (repairTool.UsableIn)
{
case RepairTool.UseEnvironment.Air:
if (character.InWater) { return 0; }
break;
case RepairTool.UseEnvironment.Water:
if (!character.InWater) { return 0; }
break;
case RepairTool.UseEnvironment.None:
return 0;
case RepairTool.UseEnvironment.Both:
default:
break;
}
}
if (prioritizeMelee && weapon is MeleeWeapon)
{
priority *= 5;
}
if (weapon.IsEmpty(character))
{
if (weapon is RangedWeapon && isCloseToEnemy)
{
// Ignore weapons that don't have any ammunition (-> Don't seek ammo).
return 0;
}
else
{
// Reduce the priority for weapons that don't have proper ammunition loaded.
if (character.HasEquippedItem(Weapon, predicate: CharacterInventory.IsHandSlotType))
{
// Yet prefer the equipped weapon.
priority *= 0.75f;
}
else
{
priority *= 0.5f;
}
}
}
if (Enemy.Params.Health.StunImmunity)
{
if (weapon.Item.HasTag(Tags.StunnerItem))
{
priority /= 2;
}
}
else if (Enemy.IsKnockedDown)
{
// Enemy is stunned, reduce the priority of stunner weapons.
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
float max = lethalDmg + 1;
if (weapon.Item.HasTag(Tags.StunnerItem))
{
priority = max;
}
else
{
float stunDmg = ApproximateStunDamage(weapon, attack);
float diff = stunDmg - lethalDmg;
priority = Math.Clamp(priority - Math.Max(diff * 2, 0), min: 1, max);
}
}
}
else if (Mode == CombatMode.Arrest)
{
// Enemy is not stunned, increase the priority of stunner weapons and decrease the priority of lethal weapons.
if (weapon.Item.HasTag(Tags.StunnerItem))
{
priority *= 5;
}
else
{
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
float stunDmg = ApproximateStunDamage(weapon, attack);
float diff = stunDmg - lethalDmg;
if (diff < 0)
{
priority /= 2;
}
}
}
}
else if (weapon is MeleeWeapon && weapon.Item.HasTag(Tags.StunnerItem) && (Enemy.Params.Health.StunImmunity || !CanMeleeStunnerStun(weapon)))
{
// Cannot do stun damage -> use the melee damage to determine the priority.
Attack attack = GetAttackDefinition(weapon);
priority = attack?.GetTotalDamage() ?? priority / 2;
}
return priority;
}
private float ApproximateStunDamage(ItemComponent weapon, Attack attack)
{
// Try to reduce the priority using the actual damage values and status effects.
// This is an approximation, because we can't check the status effect conditions here.
// The result might be incorrect if there is a high stun effect that's only applied in certain conditions.
var statusEffects = attack.StatusEffects.Where(se => !se.HasConditions && se.type == ActionType.OnUse && se.HasRequiredItems(character));
if (weapon.statusEffectLists != null && weapon.statusEffectLists.TryGetValue(ActionType.OnUse, out List<StatusEffect> hitEffects))
{
statusEffects = statusEffects.Concat(hitEffects);
}
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 == AfflictionPrefab.StunType);
if (stunAffliction != null)
{
stunAmount = stunAffliction.Strength;
}
return stunAmount;
});
return attack.Stun + afflictionsStun + effectsStun;
}
private bool CanMeleeStunnerStun(ItemComponent weapon)
{
// If there's an item container that takes a battery,
// assume that it's required for the stun effect
// as we can't check the status effect conditions here.
var mobileBatteryTag = Tags.MobileBattery;
var containers = weapon.Item.Components.Where(ic =>
ic is ItemContainer container &&
container.ContainableItemIdentifiers.Contains(mobileBatteryTag));
// If there's no such container, assume that the melee weapon can stun without a battery.
return containers.None() || containers.Any(container =>
(container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false);
}
private Item GetWeapon(IEnumerable<ItemComponent> weaponList, out ItemComponent weaponComponent)
{
weaponComponent = null;
float bestPriority = 0;
float lethalDmg = -1;
bool isAllowedToSeekWeapons = !IsEnemyCloserThan(300);
bool isCloseToEnemy = IsEnemyCloserThan(300);
bool prioritizeMelee = IsEnemyCloserThan(50) || EnemyAIController.IsLatchedTo(Enemy, character);
foreach (var weapon in weaponList)
{
float priority = weapon.CombatPriority;
if (weapon is RepairTool repairTool)
{
switch (repairTool.UsableIn)
{
case RepairTool.UseEnvironment.Air:
if (character.InWater) { continue; }
break;
case RepairTool.UseEnvironment.Water:
if (!character.InWater) { continue; }
break;
case RepairTool.UseEnvironment.None:
continue;
case RepairTool.UseEnvironment.Both:
default:
break;
}
}
if (prioritizeMelee)
{
if (weapon is MeleeWeapon)
{
priority *= 5;
}
else
{
priority /= 2;
}
}
if (weapon.IsEmpty(character))
{
if (weapon is RangedWeapon && !isAllowedToSeekWeapons)
{
// Close to the enemy. Ignore weapons that don't have any ammunition (-> Don't seek ammo).
continue;
}
else
{
// Halve the priority for weapons that don't have proper ammunition loaded.
priority /= 2;
}
}
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);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
float max = lethalDmg + 1;
if (weapon.Item.HasTag("stunner"))
{
priority = max;
}
else
{
float stunDmg = ApproximateStunDamage(weapon, attack);
float diff = stunDmg - lethalDmg;
priority = Math.Clamp(priority - Math.Max(diff * 2, 0), min: 1, max);
}
}
}
else if (Mode == CombatMode.Arrest)
{
// Enemy is not stunned, increase the priority of stunner weapons and decrease the priority of lethal weapons.
if (weapon.Item.HasTag("stunner"))
{
priority *= 2;
}
else
{
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
float stunDmg = ApproximateStunDamage(weapon, attack);
float diff = stunDmg - lethalDmg;
if (diff < 0)
{
priority /= 2;
}
}
}
}
else if (weapon is MeleeWeapon && weapon.Item.HasTag("stunner") && !CanMeleeStunnerStun(weapon))
{
Attack attack = GetAttackDefinition(weapon);
priority = attack?.GetTotalDamage() ?? priority / 2;
}
float priority = GetWeaponPriority(weapon, prioritizeMelee, isCloseToEnemy, out lethalDmg);
if (priority > bestPriority)
{
weaponComponent = weapon;
@@ -636,7 +666,7 @@ namespace Barotrauma
if (bestPriority < 1) { return null; }
if (Mode == CombatMode.Arrest)
{
if (weaponComponent.Item.HasTag("stunner"))
if (weaponComponent.Item.HasTag(Tags.StunnerItem))
{
isLethalWeapon = false;
}
@@ -654,44 +684,6 @@ namespace Barotrauma
}
}
return weaponComponent.Item;
float ApproximateStunDamage(ItemComponent weapon, Attack attack)
{
// Try to reduce the priority using the actual damage values and status effects.
// This is an approximation, because we can't check the status effect conditions here.
// The result might be incorrect if there is a high stun effect that's only applied in certain conditions.
var statusEffects = attack.StatusEffects.Where(se => !se.HasConditions && se.type == ActionType.OnUse && se.HasRequiredItems(character));
if (weapon.statusEffectLists != null && weapon.statusEffectLists.TryGetValue(ActionType.OnUse, out List<StatusEffect> hitEffects))
{
statusEffects = statusEffects.Concat(hitEffects);
}
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 == AfflictionPrefab.StunType);
if (stunAffliction != null)
{
stunAmount = stunAffliction.Strength;
}
return stunAmount;
});
return attack.Stun + afflictionsStun + effectsStun;
}
bool CanMeleeStunnerStun(ItemComponent weapon)
{
// If there's an item container that takes a battery,
// assume that it's required for the stun effect
// as we can't check the status effect conditions here.
var mobileBatteryTag = "mobilebattery".ToIdentifier();
var containers = weapon.Item.Components.Where(ic =>
ic is ItemContainer container &&
container.ContainableItemIdentifiers.Contains(mobileBatteryTag));
// If there's no such container, assume that the melee weapon can stun without a battery.
return containers.None() || containers.Any(container =>
(container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false);
}
}
public static float GetLethalDamage(ItemComponent weapon)
@@ -771,13 +763,13 @@ namespace Barotrauma
{
return false;
}
if (!character.HasEquippedItem(Weapon, predicate: IsHandSlotType))
if (!character.HasEquippedItem(Weapon, predicate: CharacterInventory.IsHandSlotType))
{
//clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter
character.ClearInput(InputType.Aim);
character.ClearInput(InputType.Shoot);
Weapon.TryInteract(character, forceSelectKey: true);
var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s));
var slots = Weapon.AllowedSlots.Where(s => CharacterInventory.IsHandSlotType(s));
if (character.Inventory.TryPutItem(Weapon, character, slots))
{
SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
@@ -791,8 +783,6 @@ namespace Barotrauma
}
}
return true;
static bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand);
}
private float findHullTimer;
@@ -926,7 +916,7 @@ namespace Barotrauma
if (followTargetObjective == null) { return; }
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown)
{
if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out _))
if (HumanAIController.HasItem(character, Tags.HandLockerItem, out _))
{
if (!arrestingRegistered)
{
@@ -986,7 +976,7 @@ namespace Barotrauma
foreach (var item in Enemy.Inventory.AllItemsMod)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound ||
item.HasTag("weapon") ||
item.HasTag(Tags.Weapon) ||
item.GetComponent<MeleeWeapon>() != null ||
item.GetComponent<RangedWeapon>() != null)
{
@@ -997,9 +987,9 @@ namespace Barotrauma
}
//prefer using handcuffs already on the enemy's inventory
if (!HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out IEnumerable<Item> matchingItems))
if (!HumanAIController.HasItem(Enemy, Tags.HandLockerItem, out IEnumerable<Item> matchingItems))
{
HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out matchingItems);
HumanAIController.HasItem(character, Tags.HandLockerItem, out matchingItems);
}
if (matchingItems.Any() &&
@@ -1079,7 +1069,7 @@ namespace Barotrauma
if (ammunitionIdentifiers != null)
{
// Try reload ammunition from inventory
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio");
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag(Tags.MobileRadio);
Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
if (ammunition != null)
{
@@ -1205,7 +1195,7 @@ namespace Barotrauma
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
}
// 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);
var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy), myBodies, Physics.CollisionCharacter);
foreach (var body in pickedBodies)
{
Character target = null;
@@ -1248,7 +1238,7 @@ namespace Barotrauma
}
}
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
Weapon.Use(deltaTime, user: character);
reloadTimer = Math.Max(reloadTime, reloadTime * Rand.Range(1f, 1.25f) / AimSpeed);
}
@@ -1265,7 +1255,7 @@ namespace Barotrauma
{
Unequip();
}
SteeringManager.Reset();
SteeringManager?.Reset();
}
protected override void OnAbandon()
@@ -1275,7 +1265,7 @@ namespace Barotrauma
{
Unequip();
}
SteeringManager.Reset();
SteeringManager?.Reset();
}
public override void Reset()
@@ -11,6 +11,7 @@ namespace Barotrauma
class AIObjectiveContainItem: AIObjective
{
public override Identifier Identifier { get; set; } = "contain item".ToIdentifier();
public override bool AllowWhileHandcuffed => false;
public Func<Item, float> GetItemPriority;
@@ -109,7 +110,7 @@ namespace Barotrauma
private bool CheckItem(Item item)
{
return item.HasIdentifierOrTags(itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character);
return item.HasIdentifierOrTags(itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character) && container.ShouldBeContained(item, out _);
}
protected override void Act(float deltaTime)
@@ -226,7 +227,10 @@ namespace Barotrauma
AllowToFindDivingGear = AllowToFindDivingGear,
AllowDangerousPressure = AllowDangerousPressure,
TargetCondition = ConditionLevel,
ItemFilter = (Item potentialItem) => RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem),
ItemFilter = (Item potentialItem) =>
{
return (RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem)) && container.ShouldBeContained(potentialItem, out _);
},
ItemCount = ItemCount,
TakeWholeStack = MoveWholeStack
}, onAbandon: () =>
@@ -9,11 +9,12 @@ namespace Barotrauma
class AIObjectiveDecontainItem : AIObjective
{
public override Identifier Identifier { get; set; } = "decontain item".ToIdentifier();
public override bool AllowWhileHandcuffed => false;
public Func<Item, float> GetItemPriority;
//can either be a tag or an identifier
private readonly string[] itemIdentifiers;
private readonly Identifier[] itemIdentifiers;
private readonly ItemContainer sourceContainer;
private readonly ItemContainer targetContainer;
private readonly Item targetItem;
@@ -52,16 +53,16 @@ namespace Barotrauma
this.targetContainer = targetContainer;
}
public AIObjectiveDecontainItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, objectiveManager, sourceContainer, targetContainer, priorityModifier) { }
public AIObjectiveDecontainItem(Character character, Identifier itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
: this(character, new Identifier[] { itemIdentifier }, objectiveManager, sourceContainer, targetContainer, priorityModifier) { }
public AIObjectiveDecontainItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
public AIObjectiveDecontainItem(Character character, Identifier[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
this.itemIdentifiers = itemIdentifiers;
for (int i = 0; i < itemIdentifiers.Length; i++)
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
itemIdentifiers[i] = itemIdentifiers[i];
}
this.sourceContainer = sourceContainer;
this.targetContainer = targetContainer;
@@ -88,7 +88,7 @@ namespace Barotrauma
escapeProgress += Rand.Range(2, 5);
if (escapeProgress > 15)
{
Item handcuffs = character.Inventory.FindItemByTag("handlocker".ToIdentifier());
Item handcuffs = character.Inventory.FindItemByTag(Tags.HandLockerItem);
if (handcuffs != null)
{
handcuffs.Drop(character);
@@ -15,6 +15,8 @@ namespace Barotrauma
public override bool AllowInAnySub => true;
public override bool AllowWhileHandcuffed => false;
private readonly Hull targetHull;
private AIObjectiveGetItem getExtinguisherObjective;
@@ -30,8 +32,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
HandleNonAllowed();
return Priority;
}
bool isOrder = objectiveManager.HasOrder<AIObjectiveExtinguishFires>();
@@ -176,19 +177,19 @@ namespace Barotrauma
getExtinguisherObjective = null;
gotoObjective = null;
sinTime = 0;
SteeringManager.Reset();
SteeringManager?.Reset();
}
protected override void OnCompleted()
{
base.OnCompleted();
SteeringManager.Reset();
SteeringManager?.Reset();
}
protected override void OnAbandon()
{
base.OnAbandon();
SteeringManager.Reset();
SteeringManager?.Reset();
}
}
}
@@ -12,6 +12,7 @@ namespace Barotrauma
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowWhileHandcuffed => false;
private readonly Identifier gearTag;
@@ -22,34 +23,20 @@ namespace Barotrauma
public const float MIN_OXYGEN = 10;
public static readonly Identifier HEAVY_DIVING_GEAR = "deepdiving".ToIdentifier();
public static readonly Identifier LIGHT_DIVING_GEAR = "lightdiving".ToIdentifier();
/// <summary>
/// Diving gear that's suitable for wearing indoors (-> the bots don't try to unequip it when they don't need diving gear)
/// </summary>
public static readonly Identifier DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors".ToIdentifier();
public static readonly Identifier OXYGEN_SOURCE = "oxygensource".ToIdentifier();
protected override bool CheckObjectiveSpecific() =>
targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head);
public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
gearTag = needsDivingSuit ? HEAVY_DIVING_GEAR : LIGHT_DIVING_GEAR;
gearTag = needsDivingSuit ? Tags.HeavyDivingGear : Tags.LightDivingGear;
}
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
TrySetTargetItem(character.Inventory.FindItemByTag(gearTag, true));
if (targetItem == null && gearTag == LIGHT_DIVING_GEAR)
if (targetItem == null && gearTag == Tags.LightDivingGear)
{
TrySetTargetItem(character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true));
TrySetTargetItem(character.Inventory.FindItemByTag(Tags.HeavyDivingGear, true));
}
if (targetItem == null ||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) &&
@@ -74,7 +61,7 @@ namespace Barotrauma
onCompleted: () =>
{
RemoveSubObjective(ref getDivingGear);
if (gearTag == HEAVY_DIVING_GEAR && HumanAIController.HasItem(character, LIGHT_DIVING_GEAR, out IEnumerable<Item> masks, requireEquipped: true))
if (gearTag == Tags.HeavyDivingGear && HumanAIController.HasItem(character, Tags.LightDivingGear, out IEnumerable<Item> masks, requireEquipped: true))
{
foreach (Item mask in masks)
{
@@ -95,10 +82,10 @@ namespace Barotrauma
{
if (character.IsOnPlayerTeam)
{
if (HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: min))
if (HumanAIController.HasItem(character, Tags.OxygenSource, out _, conditionPercentage: min))
{
character.Speak(TextManager.Get("dialogswappingoxygentank").Value, null, 0, "swappingoxygentank".ToIdentifier(), 30.0f);
if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min, recursive: true).Count == 1)
if (character.Inventory.FindAllItems(i => i.HasTag(Tags.OxygenSource) && i.Condition > min, recursive: true).Count == 1)
{
character.Speak(TextManager.Get("dialoglastoxygentank").Value, null, 0.0f, "dialoglastoxygentank".ToIdentifier(), 30.0f);
}
@@ -109,7 +96,7 @@ namespace Barotrauma
}
}
var container = targetItem.GetComponent<ItemContainer>();
var objective = new AIObjectiveContainItem(character, OXYGEN_SOURCE, container, objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
var objective = new AIObjectiveContainItem(character, Tags.OxygenSource, container, objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
@@ -119,7 +106,7 @@ namespace Barotrauma
};
if (container.HasSubContainers)
{
objective.TargetSlot = container.FindSuitableSubContainerIndex(OXYGEN_SOURCE);
objective.TargetSlot = container.FindSuitableSubContainerIndex(Tags.OxygenSource);
}
// Only remove the oxygen source being replaced
objective.RemoveExistingPredicate = i => objective.IsInTargetSlot(i);
@@ -132,7 +119,7 @@ namespace Barotrauma
// Try to seek any oxygen sources, even if they have minimal amount of oxygen.
TryAddSubObjective(ref getOxygen, () =>
{
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
return new AIObjectiveContainItem(character, Tags.OxygenSource, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
@@ -142,7 +129,7 @@ namespace Barotrauma
onAbandon: () =>
{
Abandon = true;
if (remainingTanks > 0 && !HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: 0.01f))
if (remainingTanks > 0 && !HumanAIController.HasItem(character, Tags.OxygenSource, out _, conditionPercentage: 0.01f))
{
character.Speak(TextManager.Get("dialogcantfindtoxygen").Value, null, 0, "cantfindoxygen".ToIdentifier(), 30.0f);
}
@@ -158,7 +145,7 @@ namespace Barotrauma
int ReportOxygenTankCount()
{
if (character.Submarine != Submarine.MainSub) { return 1; }
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 1);
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.OxygenSource) && i.Condition > 1);
if (remainingOxygenTanks == 0)
{
character.Speak(TextManager.Get("DialogOutOfOxygenTanks").Value, null, 0.0f, "outofoxygentanks".ToIdentifier(), 30.0f);
@@ -177,7 +164,7 @@ namespace Barotrauma
{
return
item != null &&
item.HasTag(OXYGEN_SOURCE) &&
item.HasTag(Tags.OxygenSource) &&
item.Condition > 0 &&
(oxygenSourceSlotIndex == null || item.ParentInventory.IsInSlot(item, oxygenSourceSlotIndex.Value));
}
@@ -188,7 +175,7 @@ namespace Barotrauma
targetItem = item;
if (targetItem != null)
{
oxygenSourceSlotIndex = targetItem.GetComponent<ItemContainer>()?.FindSuitableSubContainerIndex(OXYGEN_SOURCE);
oxygenSourceSlotIndex = targetItem.GetComponent<ItemContainer>()?.FindSuitableSubContainerIndex(Tags.OxygenSource);
}
else
{
@@ -212,7 +199,7 @@ namespace Barotrauma
// When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks.
float min = 0.01f;
float minOxygen = character.IsInFriendlySub ? MIN_OXYGEN : min;
if (minOxygen > min && character.Inventory.AllItems.Any(i => i.HasTag("oxygensource") && i.ConditionPercentage >= minOxygen))
if (minOxygen > min && character.Inventory.AllItems.Any(i => i.HasTag(Tags.OxygenSource) && i.ConditionPercentage >= minOxygen))
{
// There's a valid oxygen tank in the inventory -> no need to swap the tank too early.
minOxygen = min;
@@ -40,24 +40,21 @@ namespace Barotrauma
protected override float GetPriority()
{
if (!IsAllowed)
{
Priority = 0;
return Priority;
}
if (character.CurrentHull == null)
{
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))
objectiveManager.Objectives.Any(o => (o is AIObjectiveCombat || o is AIObjectiveReturn) && o.Priority > 0))
&& ((!character.IsLowInOxygen && character.IsImmuneToPressure)|| HumanAIController.HasDivingSuit(character)) ? 0 : AIObjectiveManager.EmergencyObjectivePriority - 10;
}
else
{
if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) ||
NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)))
bool isSuffocatingInDivingSuit = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
static bool IsSuffocatingWithoutDivingGear(Character c) => c.IsLowInOxygen && c.AnimController.HeadInWater && !HumanAIController.HasDivingGear(c, requireOxygenTank: true);
if (isSuffocatingInDivingSuit ||
NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)) ||
(!objectiveManager.HasActiveObjective<AIObjectiveFindDivingGear>() && IsSuffocatingWithoutDivingGear(character)))
{
Priority = AIObjectiveManager.MaxObjectivePriority;
}
@@ -215,7 +212,7 @@ namespace Barotrauma
AllowGoingOutside =
character.IsProtectedFromPressure ||
character.CurrentHull == null ||
character.CurrentHull.IsTaggedAirlock() ||
character.CurrentHull.IsAirlock ||
character.CurrentHull.LeadsOutside(character)
},
onCompleted: () =>
@@ -258,6 +255,13 @@ namespace Barotrauma
}
if (subObjectives.Any(so => so.CanBeCompleted)) { return; }
UpdateSimpleEscape(deltaTime);
if (cannotFindSafeHull && !character.IsInFriendlySub && objectiveManager.Objectives.None(o => o is AIObjectiveReturn))
{
if (OrderPrefab.Prefabs.TryGet("return".ToIdentifier(), out OrderPrefab orderPrefab))
{
objectiveManager.AddObjective(new AIObjectiveReturn(character, character, objectiveManager));
}
}
}
}
@@ -433,8 +437,7 @@ namespace Barotrauma
}
else
{
// TODO: could also target gaps that get us inside?
if (potentialHull.IsTaggedAirlock())
if (potentialHull.IsAirlock)
{
hullSafety = 100;
hullIsAirlock = true;
@@ -12,7 +12,9 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "fix leak".ToIdentifier();
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInFriendlySubs => true;
public override bool AllowInAnySub => true;
public override bool AllowWhileHandcuffed => false;
public Gap Leak { get; private set; }
@@ -35,8 +37,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
HandleNonAllowed();
return Priority;
}
float coopMultiplier = 1;
@@ -94,6 +95,7 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
var weldingTool = character.Inventory.FindItemByTag("weldingequipment".ToIdentifier(), true);
var repairTool = weldingTool?.GetComponent<RepairTool>();
if (weldingTool == null)
{
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment".ToIdentifier(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
@@ -110,17 +112,25 @@ namespace Barotrauma
}
else
{
if (weldingTool.OwnInventory == null)
if (repairTool == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no proper inventory");
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"{weldingTool}\" has no RepairTool component but is tagged as a welding tool");
#endif
Abandon = true;
return;
}
if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
if (weldingTool.OwnInventory == null && repairTool.requiredItems.Any(r => r.Key == RelatedItem.RelationType.Contained))
{
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel".ToIdentifier(), weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"{weldingTool}\" has no proper inventory");
#endif
Abandon = true;
return;
}
if (weldingTool.OwnInventory != null && weldingTool.OwnInventory.AllItems.None(i => i.HasTag(Tags.WeldingFuel) && i.Condition > 0.0f))
{
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, Tags.WeldingFuel, weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
RemoveExisting = true
},
@@ -138,7 +148,7 @@ namespace Barotrauma
void ReportWeldingFuelTankCount()
{
if (character.Submarine != Submarine.MainSub) { return; }
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("weldingfuel") && i.Condition > 1);
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.WeldingFuel) && i.Condition > 1);
if (remainingOxygenTanks == 0)
{
character.Speak(TextManager.Get("DialogOutOfWeldingFuel").Value, null, 0.0f, "outofweldingfuel".ToIdentifier(), 30.0f);
@@ -152,15 +162,6 @@ namespace Barotrauma
}
}
if (subObjectives.Any()) { return; }
var repairTool = weldingTool.GetComponent<RepairTool>();
if (repairTool == null)
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no RepairTool component but is tagged as a welding tool");
#endif
Abandon = true;
return;
}
Vector2 toLeak = Leak.WorldPosition - character.AnimController.AimSourceWorldPos;
// TODO: use the collider size/reach?
if (!character.AnimController.InWater && Math.Abs(toLeak.X) < 100 && toLeak.Y < 0.0f && toLeak.Y > -150)
@@ -200,7 +201,8 @@ namespace Barotrauma
Leak.linkedTo.Any(e => e is Hull h && (character.CurrentHull == h || h.linkedTo.Contains(character.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()
// Only report about contextual targets.
SpeakCannotReachCondition = () => isPriority && !CheckObjectiveSpecific()
},
onAbandon: () =>
{
@@ -9,7 +9,8 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "fix leaks".ToIdentifier();
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInAnySub => true;
public override bool AllowInFriendlySubs => true;
private Hull PrioritizedHull { get; set; }
public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Hull prioritizedHull = null) : base(character, objectiveManager, priorityModifier)
@@ -15,6 +15,7 @@ namespace Barotrauma
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowMultipleInstances => true;
public override bool AllowWhileHandcuffed => false;
public HashSet<Item> ignoredItems = new HashSet<Item>();
@@ -158,11 +159,6 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
if (IdentifiersOrTags != null && !isDoneSeeking)
{
if (checkInventory)
@@ -271,15 +267,49 @@ namespace Barotrauma
Inventory itemInventory = targetItem.ParentInventory;
var slots = itemInventory?.FindIndices(targetItem);
var droppedStack = TargetItem.DroppedStack.ToList();
if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true, targetTags: IdentifiersOrTags))
{
if (TakeWholeStack && slots != null)
if (TakeWholeStack)
{
foreach (int slot in slots)
//taking the whole stack in this context means "as many items that can fit in one of the bot's slots",
//and the stack means either a stack of items in an inventory slot or a "dropped stack"
//so we need a bit of extra logic here
int maxStackSize = 0;
int takenItemCount = 1;
for (int i = 0; i < character.Inventory.Capacity; i++)
{
foreach (Item item in itemInventory.GetItemsAt(slot).ToList())
maxStackSize = Math.Max(maxStackSize, character.Inventory.HowManyCanBePut(targetItem.Prefab, i, condition: null));
}
if (slots != null)
{
foreach (int slot in slots)
{
HumanAIController.TakeItem(item, character.Inventory, equip: false, storeUnequipped: true);
foreach (Item item in itemInventory.GetItemsAt(slot).ToList())
{
if (HumanAIController.TakeItem(item, character.Inventory, equip: false, storeUnequipped: true))
{
takenItemCount++;
if (takenItemCount >= maxStackSize) { break; }
}
else
{
break;
}
}
}
}
foreach (var item in droppedStack)
{
if (item == TargetItem) { continue; }
if (HumanAIController.TakeItem(item, character.Inventory, equip: false, storeUnequipped: true))
{
takenItemCount++;
if (takenItemCount >= maxStackSize) { break; }
}
else
{
break;
}
}
}
@@ -411,7 +441,7 @@ namespace Barotrauma
if (!CheckItem(item)) { continue; }
if (item.Container != null)
{
if (item.Container.HasTag("donttakeitems")) { continue; }
if (item.Container.HasTag(Tags.DontTakeItems)) { continue; }
if (ignoredItems.Contains(item.Container)) { continue; }
if (ignoredContainerIdentifiers != null)
{
@@ -12,6 +12,7 @@ namespace Barotrauma
public override string DebugTag => $"{Identifier}";
public override bool KeepDivingGearOn => true;
public override bool AllowMultipleInstances => true;
public override bool AllowWhileHandcuffed => false;
public bool AllowStealing { get; set; }
public bool TakeWholeStack { get; set; }
@@ -40,55 +41,48 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (character.LockHands)
if (subObjectivesCreated) { return; }
foreach (Identifier tag in gearTags)
{
Abandon = true;
return;
}
if (!subObjectivesCreated)
{
foreach (Identifier tag in gearTags)
{
if (subObjectives.Any(so => so is AIObjectiveGetItem getItem && getItem.IdentifiersOrTags.Contains(tag))) { continue; }
int count = gearTags.Count(t => t == tag);
AIObjectiveGetItem? getItem = null;
TryAddSubObjective(ref getItem, () =>
new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
{
AllowVariants = AllowVariants,
Wear = Wear,
TakeWholeStack = TakeWholeStack,
AllowStealing = AllowStealing,
ignoredIdentifiersOrTags = ignoredTags,
CheckPathForEachItem = CheckPathForEachItem,
RequireNonEmpty = RequireNonEmpty,
ItemCount = count,
SpeakIfFails = RequireAllItems
},
onCompleted: () =>
{
var item = getItem?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
achievedItems.Add(item);
}
},
onAbandon: () =>
{
var item = getItem?.TargetItem;
if (item != null)
{
achievedItems.Remove(item);
}
RemoveSubObjective(ref getItem);
if (RequireAllItems)
{
Abandon = true;
}
});
}
subObjectivesCreated = true;
if (subObjectives.Any(so => so is AIObjectiveGetItem getItem && getItem.IdentifiersOrTags.Contains(tag))) { continue; }
int count = gearTags.Count(t => t == tag);
AIObjectiveGetItem? getItem = null;
TryAddSubObjective(ref getItem, () =>
new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
{
AllowVariants = AllowVariants,
Wear = Wear,
TakeWholeStack = TakeWholeStack,
AllowStealing = AllowStealing,
ignoredIdentifiersOrTags = ignoredTags,
CheckPathForEachItem = CheckPathForEachItem,
RequireNonEmpty = RequireNonEmpty,
ItemCount = count,
SpeakIfFails = RequireAllItems
},
onCompleted: () =>
{
var item = getItem?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
achievedItems.Add(item);
}
},
onAbandon: () =>
{
var item = getItem?.TargetItem;
if (item != null)
{
achievedItems.Remove(item);
}
RemoveSubObjective(ref getItem);
if (RequireAllItems)
{
Abandon = true;
}
});
}
subObjectivesCreated = true;
}
public override void Reset()
@@ -223,23 +223,30 @@ namespace Barotrauma
Hull targetHull = GetTargetHull();
if (!IsFollowOrder)
{
// Abandon if going through unsafe paths or targeting unsafe hulls.
bool isUnreachable = HumanAIController.UnreachableHulls.Contains(targetHull);
if (!objectiveManager.CurrentObjective.IgnoreUnsafeHulls)
{
if (HumanAIController.UnsafeHulls.Contains(targetHull))
// Wait orders check this so that the bot temporarily leaves the unsafe hull.
// Non-orders (that are not set to ignore the unsafe hulls) abandon. In practice this means e.g. repair and clean up item subobjectives (of the looping parent objective).
// Other orders are only abandoned if the hull is unreachable, because the path is invalid or not found at all.
if (IsWaitOrder || !objectiveManager.HasOrders())
{
isUnreachable = true;
HumanAIController.AskToRecalculateHullSafety(targetHull);
}
else if (PathSteering?.CurrentPath != null)
{
foreach (WayPoint wp in PathSteering.CurrentPath.Nodes)
if (HumanAIController.UnsafeHulls.Contains(targetHull))
{
if (wp.CurrentHull == null) { continue; }
if (HumanAIController.UnsafeHulls.Contains(wp.CurrentHull))
isUnreachable = true;
HumanAIController.AskToRecalculateHullSafety(targetHull);
}
else if (PathSteering?.CurrentPath != null)
{
foreach (WayPoint wp in PathSteering.CurrentPath.Nodes)
{
isUnreachable = true;
HumanAIController.AskToRecalculateHullSafety(wp.CurrentHull);
if (wp.CurrentHull == null) { continue; }
if (HumanAIController.UnsafeHulls.Contains(wp.CurrentHull))
{
isUnreachable = true;
HumanAIController.AskToRecalculateHullSafety(wp.CurrentHull);
}
}
}
}
@@ -803,7 +810,7 @@ namespace Barotrauma
private void StopMovement()
{
SteeringManager.Reset();
SteeringManager?.Reset();
if (Target != null)
{
character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left;
@@ -382,7 +382,9 @@ namespace Barotrauma
{
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != currentHull || !item.HasTag("chair")) { continue; }
if (item.CurrentHull != currentHull || !item.HasTag(Tags.ChairItem)) { continue; }
//not possible in vanilla game, but a mod might have holdable/attachable chairs
if (item.ParentInventory != null || item.body is { Enabled: true }) { continue; }
var controller = item.GetComponent<Controller>();
if (controller == null || controller.User != null) { continue; }
item.TryInteract(character, forceSelectKey: true);
@@ -17,6 +17,8 @@ namespace Barotrauma
set => throw new Exception("Trying to set the value for AIObjectiveLoadItem.IsLoop from: " + Environment.StackTrace.CleanupStackTrace());
}
public override bool AllowWhileHandcuffed => false;
private AIObjectiveLoadItems.ItemCondition TargetItemCondition { get; }
private Item Container { get; }
private ItemContainer ItemContainer { get; }
@@ -161,8 +163,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
HandleNonAllowed();
return Priority;
}
else if (!AIObjectiveLoadItems.IsValidTarget(Container, character, targetCondition: TargetItemCondition))
@@ -299,7 +300,7 @@ namespace Barotrauma
if (rootInventoryOwner is Character owner && owner != character) { return false; }
if (rootInventoryOwner is Item parentItem)
{
if (parentItem.HasTag("donttakeitems")) { return false; }
if (parentItem.HasTag(Tags.DontTakeItems)) { return false; }
}
if (!item.HasAccess(character)) { return false; }
if (!character.HasItem(item) && !CanEquip(item, allowWearing: false)) { return false; }
@@ -44,6 +44,8 @@ namespace Barotrauma
public override bool CanBeCompleted => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowSubObjectiveSorting => true;
public override bool AllowWhileHandcuffed => false;
public virtual bool InverseTargetEvaluation => false;
protected virtual bool ResetWhenClearingIgnoreList => true;
protected virtual bool ForceOrderPriority => true;
@@ -117,51 +119,44 @@ namespace Barotrauma
{
if (!IsAllowed)
{
Priority = 0;
HandleNonAllowed();
return Priority;
}
if (character.LockHands)
// Allow the target value to be more than 100.
float targetValue = TargetEvaluation();
if (InverseTargetEvaluation)
{
targetValue = 100 - targetValue;
}
var currentSubObjective = CurrentSubObjective;
if (currentSubObjective != null && currentSubObjective.Priority > targetValue)
{
// If the priority is higher than the target value, let's just use it.
// The priority calculation is more precise, but it takes into account things like distances,
// so it's better not to use it if it's lower than the rougher targetValue.
targetValue = currentSubObjective.Priority;
}
// If the target value is less than 1% of the max value, let's just treat it as zero.
if (targetValue < 1)
{
Priority = 0;
}
else
{
// Allow the target value to be more than 100.
float targetValue = TargetEvaluation();
if (InverseTargetEvaluation)
if (objectiveManager.IsOrder(this))
{
targetValue = 100 - targetValue;
}
var currentSubObjective = CurrentSubObjective;
if (currentSubObjective != null && currentSubObjective.Priority > targetValue)
{
// If the priority is higher than the target value, let's just use it.
// The priority calculation is more precise, but it takes into account things like distances,
// so it's better not to use it if it's lower than the rougher targetValue.
targetValue = currentSubObjective.Priority;
}
// If the target value is less than 1% of the max value, let's just treat it as zero.
if (targetValue < 1)
{
Priority = 0;
Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetValue;
}
else
{
if (objectiveManager.IsOrder(this))
float max = AIObjectiveManager.LowestOrderPriority - 1;
if (this is AIObjectiveRescueAll rescueObjective && rescueObjective.Targets.Contains(character))
{
Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetValue;
}
else
{
float max = AIObjectiveManager.LowestOrderPriority - 1;
if (this is AIObjectiveRescueAll rescueObjective && rescueObjective.Targets.Contains(character))
{
// Allow higher prio
max = AIObjectiveManager.EmergencyObjectivePriority;
}
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
Priority = MathHelper.Lerp(0, max, value);
// Allow higher prio
max = AIObjectiveManager.EmergencyObjectivePriority;
}
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
Priority = MathHelper.Lerp(0, max, value);
}
}
return Priority;
@@ -530,7 +530,7 @@ namespace Barotrauma
case "cleanupitems":
if (order.TargetEntity is Item targetItem)
{
if (targetItem.HasTag("allowcleanup") && targetItem.ParentInventory == null && targetItem.OwnInventory != null)
if (targetItem.HasTag(Tags.AllowCleanup) && targetItem.ParentInventory == null && targetItem.OwnInventory != null)
{
// Target all items inside the container
newObjective = new AIObjectiveCleanupItems(character, this, targetItem.OwnInventory.AllItems, priorityModifier);
@@ -14,6 +14,7 @@ namespace Barotrauma
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowMultipleInstances => true;
public override bool AllowInAnySub => true;
public override bool AllowWhileHandcuffed => false;
public override bool PrioritizeIfSubObjectivesActive => component != null && (component is Reactor || component is Turret);
private readonly ItemComponent component, controller;
@@ -47,10 +48,9 @@ namespace Barotrauma
protected override float GetPriority()
{
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed || character.LockHands)
if (!IsAllowed)
{
Priority = 0;
Abandon = !isOrder;
HandleNonAllowed();
return Priority;
}
if (!isOrder && component.Item.ConditionPercentage <= 0)
@@ -13,6 +13,7 @@ namespace Barotrauma
public override bool KeepDivingGearOn => true;
public override bool KeepDivingGearOnAlsoWhenInactive => true;
public override bool PrioritizeIfSubObjectivesActive => true;
public override bool AllowWhileHandcuffed => false;
private AIObjectiveGetItem getSingleItemObjective;
private AIObjectiveGetItems getAllItemsObjective;
@@ -60,8 +61,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
Priority = 0;
Abandon = true;
HandleNonAllowed();
return Priority;
}
Priority = objectiveManager.GetOrderPriority(this);
@@ -75,11 +75,6 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (character.LockHands)
{
Abandon = true;
return;
}
if (!subObjectivesCreated)
{
if (FindAllItems && targetItem == null)
@@ -12,6 +12,7 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "pump water".ToIdentifier();
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowWhileHandcuffed => false;
private List<Pump> pumpList;
@@ -54,7 +55,7 @@ namespace Barotrauma
var pump = item.GetComponent<Pump>();
if (pump == null || pump.Item.Submarine == null || pump.Item.CurrentHull == null) { continue; }
if (pump.Item.Submarine.TeamID != character.TeamID) { continue; }
if (pump.Item.HasTag("ballast")) { continue; }
if (pump.Item.HasTag(Tags.Ballast)) { continue; }
pumpList.Add(pump);
}
}
@@ -10,8 +10,9 @@ namespace Barotrauma
{
public override Identifier Identifier { get; set; } = "repair item".ToIdentifier();
public override bool AllowInAnySub => true;
public override bool AllowInFriendlySubs => true;
public override bool KeepDivingGearOn => Item?.CurrentHull == null;
public override bool AllowWhileHandcuffed => false;
public Item Item { get; private set; }
@@ -36,10 +37,13 @@ namespace Barotrauma
protected override float GetPriority()
{
if (!IsAllowed || Item.IgnoreByAI(character))
if (!IsAllowed) { HandleNonAllowed(); }
if (Item.IgnoreByAI(character))
{
Priority = 0;
Abandon = true;
}
if (Abandon)
{
if (IsRepairing())
{
Item.Repairables.ForEach(r => r.StopRepairing(character));
@@ -136,32 +140,35 @@ namespace Barotrauma
}
if (repairTool != null)
{
if (repairTool.Item.OwnInventory == null)
if (repairTool.requiredItems.TryGetValue(RelatedItem.RelationType.Contained, out var requiredItems))
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"" + repairTool + "\" has no proper inventory");
#endif
Abandon = true;
return;
}
RelatedItem item = null;
Item fuel = null;
foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained])
{
item = requiredItem;
fuel = repairTool.Item.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it));
if (fuel != null) { break; }
}
if (fuel == null)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
if (repairTool.Item.OwnInventory == null)
{
RemoveExisting = true
},
onCompleted: () => RemoveSubObjective(ref refuelObjective),
onAbandon: () => Abandon = true);
return;
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"{repairTool}\" has no proper inventory.");
#endif
Abandon = true;
return;
}
RelatedItem item = null;
Item fuel = null;
foreach (RelatedItem requiredItem in requiredItems)
{
item = requiredItem;
fuel = repairTool.Item.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it));
if (fuel != null) { break; }
}
if (fuel == null)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
RemoveExisting = true
},
onCompleted: () => RemoveSubObjective(ref refuelObjective),
onAbandon: () => Abandon = true);
return;
}
}
}
if (character.CanInteractWith(Item, out _, checkLinked: false))
@@ -170,10 +177,8 @@ namespace Barotrauma
if (waitTimer < WaitTimeBeforeRepair) { return; }
HumanAIController.FaceTarget(Item);
if (repairTool != null)
{
OperateRepairTool(deltaTime);
}
bool repairThroughRepairInterface = false;
foreach (Repairable repairable in Item.Repairables)
{
if (repairable.CurrentFixer != null && repairable.CurrentFixer != character)
@@ -185,10 +190,11 @@ namespace Barotrauma
{
if (character.SelectedItem != Item)
{
if (Item.TryInteract(character, ignoreRequiredItems: true, forceUseKey: true) ||
Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true))
if (Item.TryInteract(character, forceUseKey: true) ||
Item.TryInteract(character, forceSelectKey: true))
{
character.SelectedItem = Item;
repairThroughRepairInterface = true;
}
else
{
@@ -209,8 +215,17 @@ namespace Barotrauma
{
repairable.StartRepairing(character, Repairable.FixActions.Repair);
}
else
{
repairThroughRepairInterface = true;
}
break;
}
if (!repairThroughRepairInterface && repairTool != null && !Abandon)
{
OperateRepairTool(deltaTime);
}
}
else
{
@@ -222,7 +237,8 @@ namespace Barotrauma
{
var objective = new AIObjectiveGoTo(Item, character, objectiveManager)
{
TargetName = Item.Name
TargetName = Item.Name,
SpeakCannotReachCondition = () => isPriority
};
if (repairTool != null)
{
@@ -19,7 +19,7 @@ namespace Barotrauma
public Item PrioritizedItem { get; private set; }
public override bool AllowMultipleInstances => true;
public override bool AllowInAnySub => true;
public override bool AllowInFriendlySubs => true;
public readonly static float RequiredSuccessFactor = 0.4f;
@@ -16,12 +16,13 @@ namespace Barotrauma
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
public override bool AllowWhileHandcuffed => false;
const float TreatmentDelay = 0.5f;
const float CloseEnoughToTreat = 100.0f;
private readonly Character targetCharacter;
public readonly Character Target;
private AIObjectiveGoTo goToObjective;
private AIObjectiveContainItem replaceOxygenObjective;
@@ -44,7 +45,7 @@ namespace Barotrauma
Abandon = true;
return;
}
this.targetCharacter = targetCharacter;
Target = targetCharacter;
}
protected override void OnAbandon()
@@ -61,55 +62,55 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (character.LockHands || targetCharacter == null || targetCharacter.Removed || targetCharacter.IsDead)
if (Target == null || Target.Removed || Target.IsDead)
{
Abandon = true;
return;
}
var otherRescuer = targetCharacter.SelectedBy;
var otherRescuer = Target.SelectedBy;
if (otherRescuer != null && otherRescuer != character)
{
// Someone else is rescuing/holding the target.
Abandon = otherRescuer.IsPlayer || character.GetSkillLevel("medical") < otherRescuer.GetSkillLevel("medical");
return;
}
if (targetCharacter != character)
if (Target != character)
{
if (targetCharacter.IsIncapacitated)
if (Target.IsIncapacitated)
{
// Check if the character needs more oxygen
if (!ignoreOxygen && character.SelectedCharacter == targetCharacter || character.CanInteractWith(targetCharacter))
if (!ignoreOxygen && character.SelectedCharacter == Target || character.CanInteractWith(Target))
{
// Replace empty oxygen and welding fuel.
if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out IEnumerable<Item> suits, requireEquipped: true))
if (HumanAIController.HasItem(Target, Tags.HeavyDivingGear, out IEnumerable<Item> suits, requireEquipped: true))
{
Item suit = suits.FirstOrDefault();
if (suit != null)
{
AIController.UnequipEmptyItems(character, suit);
AIController.UnequipContainedItems(character, suit, it => it.HasTag("weldingfuel"));
AIController.UnequipContainedItems(character, suit, it => it.HasTag(Tags.WeldingFuel));
}
}
else if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out IEnumerable<Item> masks, requireEquipped: true))
else if (HumanAIController.HasItem(Target, Tags.LightDivingGear, out IEnumerable<Item> masks, requireEquipped: true))
{
Item mask = masks.FirstOrDefault();
if (mask != null)
{
AIController.UnequipEmptyItems(character, mask);
AIController.UnequipContainedItems(character, mask, it => it.HasTag("weldingfuel"));
AIController.UnequipContainedItems(character, mask, it => it.HasTag(Tags.WeldingFuel));
}
}
bool ShouldRemoveDivingSuit() => targetCharacter.OxygenAvailable < CharacterHealth.InsufficientOxygenThreshold && targetCharacter.CurrentHull?.LethalPressure <= 0;
bool ShouldRemoveDivingSuit() => Target.OxygenAvailable < CharacterHealth.InsufficientOxygenThreshold && Target.CurrentHull?.LethalPressure <= 0;
if (ShouldRemoveDivingSuit())
{
suits.ForEach(suit => suit.Drop(character));
}
else if (suits.Any() && suits.None(s => s.OwnInventory?.AllItems != null && s.OwnInventory.AllItems.Any(it => it.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) && it.ConditionPercentage > 0)))
else if (suits.Any() && suits.None(s => s.OwnInventory?.AllItems != null && s.OwnInventory.AllItems.Any(it => it.HasTag(Tags.OxygenSource) && it.ConditionPercentage > 0)))
{
// The target has a suit equipped with an empty oxygen tank.
// Can't remove the suit, because the target needs it.
// If we happen to have an extra oxygen tank in the inventory, let's swap it.
Item spareOxygenTank = FindOxygenTank(targetCharacter) ?? FindOxygenTank(character);
Item spareOxygenTank = FindOxygenTank(Target) ?? FindOxygenTank(character);
if (spareOxygenTank != null)
{
Item suit = suits.FirstOrDefault();
@@ -133,36 +134,36 @@ namespace Barotrauma
Item FindOxygenTank(Character c) =>
c.Inventory.FindItem(i =>
i.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) &&
i.HasTag(Tags.OxygenSource) &&
i.ConditionPercentage > 1 &&
i.FindParentInventory(inv => inv.Owner is Item otherItem && otherItem.HasTag("diving")) == null,
i.FindParentInventory(inv => inv.Owner is Item otherItem && otherItem.HasTag(Tags.DivingGear)) == null,
recursive: true);
}
}
if (character.Submarine != null && targetCharacter.CurrentHull != null)
if (character.Submarine != null && Target.CurrentHull != null)
{
if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
if (HumanAIController.GetHullSafety(Target.CurrentHull, Target) < HumanAIController.HULL_SAFETY_THRESHOLD)
{
// Incapacitated target is not in a safe place -> Move to a safe place first
if (character.SelectedCharacter != targetCharacter)
if (character.SelectedCharacter != Target)
{
if (HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null)
if (HumanAIController.VisibleHulls.Contains(Target.CurrentHull) && Target.CurrentHull.DisplayName != null)
{
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget",
("[targetname]", targetCharacter.Name, FormatCapitals.No),
("[roomname]", targetCharacter.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
null, 1.0f, $"foundunconscioustarget{targetCharacter.Name}".ToIdentifier(), 60.0f);
("[targetname]", Target.Name, FormatCapitals.No),
("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
null, 1.0f, $"foundunconscioustarget{Target.Name}".ToIdentifier(), 60.0f);
}
// Go to the target and select it
if (!character.CanInteractWith(targetCharacter))
if (!character.CanInteractWith(Target))
{
RemoveSubObjective(ref replaceOxygenObjective);
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(),
TargetName = targetCharacter.DisplayName
TargetName = Target.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
@@ -173,7 +174,7 @@ namespace Barotrauma
}
else
{
character.SelectCharacter(targetCharacter);
character.SelectCharacter(Target);
}
}
else
@@ -213,16 +214,16 @@ namespace Barotrauma
if (subObjectives.Any()) { return; }
if (targetCharacter != character && !character.CanInteractWith(targetCharacter))
if (Target != character && !character.CanInteractWith(Target))
{
RemoveSubObjective(ref replaceOxygenObjective);
RemoveSubObjective(ref goToObjective);
// Go to the target and select it
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(),
TargetName = targetCharacter.DisplayName
TargetName = Target.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
@@ -234,14 +235,14 @@ namespace Barotrauma
else
{
// We can start applying treatment
if (character != targetCharacter && character.SelectedCharacter != targetCharacter)
if (character != Target && character.SelectedCharacter != Target)
{
if (targetCharacter.CurrentHull?.DisplayName != null)
if (Target.CurrentHull?.DisplayName != null)
{
character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget",
("[targetname]", targetCharacter.Name, FormatCapitals.No),
("[roomname]", targetCharacter.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
null, 1.0f, $"foundwoundedtarget{targetCharacter.Name}".ToIdentifier(), 60.0f);
("[targetname]", Target.Name, FormatCapitals.No),
("[roomname]", Target.CurrentHull.DisplayName, FormatCapitals.Yes)).Value,
null, 1.0f, $"foundwoundedtarget{Target.Name}".ToIdentifier(), 60.0f);
}
}
GiveTreatment(deltaTime);
@@ -253,7 +254,7 @@ namespace Barotrauma
private readonly Dictionary<Identifier, float> currentTreatmentSuitabilities = new Dictionary<Identifier, float>();
private void GiveTreatment(float deltaTime)
{
if (targetCharacter == null)
if (Target == null)
{
string errorMsg = $"{character.Name}: Attempted to update a Rescue objective with no target!";
DebugConsole.ThrowError(errorMsg);
@@ -263,10 +264,10 @@ namespace Barotrauma
SteeringManager.Reset();
if (!targetCharacter.IsPlayer)
if (!Target.IsPlayer)
{
// If the target is a bot, don't let it move
targetCharacter.AIController?.SteeringManager?.Reset();
Target.AIController?.SteeringManager?.Reset();
}
if (treatmentTimer > 0.0f)
{
@@ -275,28 +276,28 @@ namespace Barotrauma
}
treatmentTimer = TreatmentDelay;
float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f;
float cprSuitability = Target.Oxygen < 0.0f ? -Target.Oxygen * 100.0f : 0.0f;
//find which treatments are the most suitable to treat the character's current condition
targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, user: character, normalize: false, predictFutureDuration: 10.0f);
Target.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, user: character, normalize: false, predictFutureDuration: 10.0f);
//check if we already have a suitable treatment for any of the afflictions
foreach (Affliction affliction in GetSortedAfflictions(targetCharacter))
foreach (Affliction affliction in GetSortedAfflictions(Target))
{
if (affliction == null) { throw new Exception("Affliction was null"); }
if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); }
float bestSuitability = 0.0f;
Item bestItem = null;
foreach (KeyValuePair<Identifier, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
foreach (KeyValuePair<Identifier, float> treatmentSuitability in affliction.Prefab.TreatmentSuitabilities)
{
if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) &&
currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability)
{
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true);
//allow taking items from the target's inventory too if the target is unconscious
if (matchingItem == null && targetCharacter.IsIncapacitated)
if (matchingItem == null && Target.IsIncapacitated)
{
matchingItem ??= targetCharacter.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true);
matchingItem ??= Target.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true);
}
if (matchingItem != null)
{
@@ -307,7 +308,7 @@ namespace Barotrauma
}
if (bestItem != null)
{
if (targetCharacter != character) { character.SelectCharacter(targetCharacter); }
if (Target != character) { character.SelectCharacter(Target); }
ApplyTreatment(affliction, bestItem);
//wait a bit longer after applying a treatment to wait for potential side-effects to manifest
treatmentTimer = TreatmentDelay * 4;
@@ -370,12 +371,12 @@ namespace Barotrauma
("[treatment1]", itemListStr),
("[treatment2]", itemNameList.Last()));
}
if (targetCharacter != character && character.IsOnPlayerTeam)
if (Target != character && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments",
("[targetname]", targetCharacter.Name, FormatCapitals.No),
("[targetname]", Target.Name, FormatCapitals.No),
("[treatmentlist]", itemListStr, FormatCapitals.Yes)).Value,
null, 2.0f, $"listrequiredtreatments{targetCharacter.Name}".ToIdentifier(), 60.0f);
null, 2.0f, $"listrequiredtreatments{Target.Name}".ToIdentifier(), 60.0f);
}
RemoveSubObjective(ref getItemObjective);
TryAddSubObjective(ref getItemObjective,
@@ -397,18 +398,18 @@ namespace Barotrauma
}
}
}
else if (!targetCharacter.IsUnconscious)
else if (!Target.IsUnconscious)
{
Abandon = true;
//no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR)
SpeakCannotTreat();
return;
}
if (character != targetCharacter)
if (character != Target)
{
if (cprSuitability > 0.0f)
{
character.SelectCharacter(targetCharacter);
character.SelectCharacter(Target);
character.AnimController.Anim = AnimController.Animation.CPR;
performedCpr = true;
}
@@ -421,40 +422,40 @@ namespace Barotrauma
private void SpeakCannotTreat()
{
LocalizedString msg = character == targetCharacter ?
LocalizedString msg = character == Target ?
TextManager.Get("dialogcannottreatself") :
TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, FormatCapitals.No);
TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", Target.DisplayName, FormatCapitals.No);
character.Speak(msg.Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
}
private void ApplyTreatment(Affliction affliction, Item item)
{
item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction));
item.ApplyTreatment(character, Target, Target.CharacterHealth.GetAfflictionLimb(affliction));
}
protected override bool CheckObjectiveSpecific()
{
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter);
if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam)
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(Target) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, Target);
if (isCompleted && Target != character && character.IsOnPlayerTeam)
{
string textTag = performedCpr ? "DialogTargetResuscitated" : "DialogTargetHealed";
string message = TextManager.GetWithVariable(textTag, "[targetname]", targetCharacter.Name)?.Value;
character.Speak(message, delay: 1.0f, identifier: $"targethealed{targetCharacter.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f);
string message = TextManager.GetWithVariable(textTag, "[targetname]", Target.Name)?.Value;
character.Speak(message, delay: 1.0f, identifier: $"targethealed{Target.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f);
}
return isCompleted;
}
protected override float GetPriority()
{
if (!IsAllowed || targetCharacter == null)
if (Target == null) { Abandon = true; }
if (!IsAllowed) { HandleNonAllowed(); }
if (Abandon)
{
Priority = 0;
Abandon = true;
return Priority;
}
if (character.CurrentHull != null)
{
if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c)))
if (Character.CharacterList.Any(c => c.CurrentHull == Target.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c)))
{
// Don't go into rooms that have enemies
Priority = 0;
@@ -462,18 +463,18 @@ namespace Barotrauma
return Priority;
}
}
float horizontalDistance = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y);
float horizontalDistance = Math.Abs(character.WorldPosition.X - Target.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - Target.WorldPosition.Y);
if (character.Submarine?.Info is { IsRuin: false })
{
verticalDistance *= 2;
}
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance));
if (character.CurrentHull != null && targetCharacter.CurrentHull == character.CurrentHull)
if (character.CurrentHull != null && Target.CurrentHull == character.CurrentHull)
{
distanceFactor = 1;
}
float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100;
float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(Target) / 100;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, AIObjectiveManager.EmergencyObjectivePriority, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1));
return Priority;
@@ -34,24 +34,29 @@ namespace Barotrauma
protected override bool Filter(Character target)
{
if (!IsValidTarget(target, character, requireTreatableAfflictions: false)) { return false; }
if (GetTreatableAfflictions(target).Any())
if (!IsValidTarget(target, character, out bool ignoredasMinorWounds))
{
return true;
}
else
{
//the target might be at a low enough health to be considered a valid target,
//but if all afflictions are below treatment thresholds, the bot won't (and shouldn't) treat them
// -> make the bot speak to make it clear the bot intentionally ignores very minor injuries
if (!charactersWithMinorInjuries.Contains(character))
if (ignoredasMinorWounds)
{
character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.Name).Value,
null, 1.0f, $"notreatableafflictions{target.Name}".ToIdentifier(), 10.0f);
charactersWithMinorInjuries.Add(character);
//the target might be at a low enough health to be considered a valid target,
//but if all afflictions are below treatment thresholds, the bot won't (and shouldn't) treat them
// -> make the bot speak to make it clear the bot intentionally ignores very minor injuries
if (character.IsOnPlayerTeam && target != character && !charactersWithMinorInjuries.Contains(target))
{
// But only speak about targets when we are not already actively treating, in which case we should be speaking about the current target.
if (objectiveManager.GetFirstActiveObjective<AIObjectiveRescue>() == null)
{
charactersWithMinorInjuries.Add(target);
character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.Name).Value,
delay: 1.0f,
identifier: $"notreatableafflictions{target.Name}".ToIdentifier(),
minDurationBetweenSimilar: 10.0f);
}
}
}
return false;
}
}
return true;
}
protected override IEnumerable<Character> GetList() => Character.CharacterList;
@@ -103,12 +108,13 @@ namespace Barotrauma
return Math.Clamp(vitality, 0, 100);
}
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character, bool ignoreTreatmentThreshold = false)
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character, bool ignoreTreatmentThreshold)
{
var allAfflictions = character.CharacterHealth.GetAllAfflictions();
foreach (Affliction affliction in allAfflictions)
{
if (affliction.Prefab.IsBuff) { continue; }
if (!affliction.Prefab.HasTreatments) { continue; }
if (!ignoreTreatmentThreshold)
{
//other afflictions of the same type increase the "treatability"
@@ -116,7 +122,6 @@ namespace Barotrauma
float totalAfflictionStrength = character.CharacterHealth.GetTotalAdjustedAfflictionStrength(affliction);
if (totalAfflictionStrength < 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;
}
@@ -128,39 +133,50 @@ namespace Barotrauma
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
=> HumanAIController.RemoveTargets<AIObjectiveRescueAll, Character>(character, target);
public static bool IsValidTarget(Character target, Character character, bool requireTreatableAfflictions = true)
public static bool IsValidTarget(Character target, Character character, out bool ignoredAsMinorWounds)
{
ignoredAsMinorWounds = false;
if (target == null || target.IsDead || target.Removed) { return false; }
if (target.IsInstigator) { return false; }
if (target.IsPet) { return false; }
if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; }
bool isBelowTreatmentThreshold;
float vitalityFactor;
if (character.AIController is HumanAIController humanAI)
{
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target))
{
return false;
}
if (!humanAI.ObjectiveManager.HasOrder<AIObjectiveRescueAll>())
{
if (!character.IsMedic && target != character)
{
// Don't allow to treat others autonomously, unless we are a medic
return false;
}
// Ignore unsafe hulls, unless ordered
if (humanAI.UnsafeHulls.Contains(target.CurrentHull))
{
return false;
}
}
if (requireTreatableAfflictions && GetTreatableAfflictions(target).None())
{
return false;
}
if (!IsValidTargetForAI(target, humanAI)) { return false; }
vitalityFactor = GetVitalityFactor(target);
isBelowTreatmentThreshold = vitalityFactor < GetVitalityThreshold(humanAI.ObjectiveManager, character, target);
}
else
{
if (GetVitalityFactor(target) >= vitalityThreshold) { return false; }
vitalityFactor = GetVitalityFactor(target);
isBelowTreatmentThreshold = vitalityFactor < vitalityThreshold;
}
bool hasTreatableAfflictions = GetTreatableAfflictions(target, ignoreTreatmentThreshold: false).Any();
bool isValidTarget = isBelowTreatmentThreshold && hasTreatableAfflictions;
if (!isValidTarget)
{
ignoredAsMinorWounds = hasTreatableAfflictions || vitalityFactor < 100;
}
return isValidTarget;
}
private static bool IsValidTargetForAI(Character target, HumanAIController humanAI)
{
Character character = humanAI.Character;
if (!humanAI.ObjectiveManager.HasOrder<AIObjectiveRescueAll>())
{
if (!character.IsMedic && target != character)
{
// Don't allow to treat others autonomously, unless we are a medic
return false;
}
// Ignore unsafe hulls, unless ordered
if (humanAI.UnsafeHulls.Contains(target.CurrentHull))
{
return false;
}
}
if (character.Submarine != null)
{
@@ -189,11 +205,5 @@ namespace Barotrauma
}
return character.GetDamageDoneByAttacker(target) <= 0;
}
public override void Reset()
{
base.Reset();
charactersWithMinorInjuries.Clear();
}
}
}
@@ -20,7 +20,7 @@ namespace Barotrauma
ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
if (ReturnTarget == null)
{
DebugConsole.ThrowError("Error with a Return objective: no suitable return target found");
DebugConsole.AddSafeError("Error with a Return objective: no suitable return target found");
Abandon = true;
}
@@ -47,8 +47,7 @@ namespace Barotrauma
}
else
{
// TODO: Consider if this needs to be addressed
Priority = 0;
Priority = AIObjectiveManager.LowestOrderPriority - 1;
}
return Priority;
}
@@ -91,7 +90,7 @@ namespace Barotrauma
targetHull = d.Item.CurrentHull;
break;
}
if (targetHull != null && !targetHull.IsTaggedAirlock())
if (targetHull != null && !targetHull.IsAirlock)
{
// Target the closest airlock
float closestDist = 0;
@@ -99,7 +98,7 @@ namespace Barotrauma
foreach (Hull hull in Hull.HullList)
{
if (hull.Submarine != targetHull.Submarine) { continue; }
if (!hull.IsTaggedAirlock()) { continue; }
if (!hull.IsAirlock) { continue; }
float dist = Vector2.DistanceSquared(targetHull.Position, hull.Position);
if (airlock == null || closestDist <= 0 || dist < closestDist)
{
@@ -146,7 +145,7 @@ namespace Barotrauma
bool targetIsAirlock = false;
foreach (var hull in ReturnTarget.GetHulls(false))
{
bool hullIsAirlock = hull.IsTaggedAirlock();
bool hullIsAirlock = hull.IsAirlock;
if(hullIsAirlock || (!targetIsAirlock && hull.LeadsOutside(character)))
{
float distanceSquared = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition);
@@ -85,6 +85,12 @@ namespace Barotrauma
public readonly bool TargetAllCharacters;
public bool IsReport => TargetAllCharacters && !MustSetTarget;
public bool IsVisibleAsReportButton =>
IsReport && !Hidden && SymbolSprite != null &&
(!TraitorModeOnly || GameMain.GameSession is { TraitorsEnabled: true });
public bool TraitorModeOnly;
public bool IsDismissal => Identifier == DismissalIdentifier;
public readonly float FadeOutTime;
@@ -172,6 +178,7 @@ namespace Barotrauma
ControllerTags = orderElement.GetAttributeIdentifierArray("controllertags", Array.Empty<Identifier>()).ToImmutableArray();
TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false);
AppropriateJobs = orderElement.GetAttributeIdentifierArray("appropriatejobs", Array.Empty<Identifier>()).ToImmutableArray();
TraitorModeOnly = orderElement.GetAttributeBool("TraitorModeOnly", false);
PreferredJobs = orderElement.GetAttributeIdentifierArray("preferredjobs", Array.Empty<Identifier>()).ToImmutableArray();
Options = orderElement.GetAttributeIdentifierArray("options", Array.Empty<Identifier>()).ToImmutableArray();
HiddenOptions = orderElement.GetAttributeIdentifierArray("hiddenoptions", Array.Empty<Identifier>()).ToImmutableArray();
@@ -34,7 +34,23 @@ namespace Barotrauma
set { happiness = MathHelper.Clamp(value, 0.0f, MaxHappiness); }
}
/// <summary>
/// At which point is the pet considered "unhappy" (playing unhappy sounds and showing the icon)
/// </summary>
public float UnhappyThreshold { get; set; }
/// <summary>
/// At which point is the pet considered "happy" (playing happy sounds and showing the icon)
/// </summary>
public float HappyThreshold { get; set; }
public float MaxHappiness { get; set; }
/// <summary>
/// At which point is the pet considered "hungry" (playing unhappy sounds and showing the icon)
/// </summary>
public float HungryThreshold { get; set; }
public float MaxHunger { get; set; }
public float HappinessDecreaseRate { get; set; }
@@ -43,7 +59,7 @@ namespace Barotrauma
public float PlayForce { get; set; }
public float PlayTimer { get; set; }
private float? unstunY { get; set; }
private float? UnstunY { get; set; }
public EnemyAIController AIController { get; private set; } = null;
@@ -136,7 +152,7 @@ namespace Barotrauma
if (aggregate >= r && Items[i].Prefab != null)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AIController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
Entity.Spawner.AddItemToSpawnQueue(Items[i].Prefab, pet.AIController.Character.WorldPosition);
Entity.Spawner?.AddItemToSpawnQueue(Items[i].Prefab, pet.AIController.Character.WorldPosition);
break;
}
}
@@ -164,14 +180,18 @@ namespace Barotrauma
AIController = aiController;
AIController.Character.CanBeDragged = true;
MaxHappiness = element.GetAttributeFloat("maxhappiness", 100.0f);
MaxHunger = element.GetAttributeFloat("maxhunger", 100.0f);
MaxHappiness = element.GetAttributeFloat(nameof(MaxHappiness), 100.0f);
UnhappyThreshold = element.GetAttributeFloat(nameof(UnhappyThreshold), MaxHappiness * 0.25f);
HappyThreshold = element.GetAttributeFloat(nameof(HappyThreshold), MaxHappiness * 0.8f);
MaxHunger = element.GetAttributeFloat(nameof(MaxHunger), 100.0f);
HungryThreshold = element.GetAttributeFloat(nameof(HungryThreshold), MaxHunger * 0.5f);
Happiness = MaxHappiness * 0.5f;
Hunger = MaxHunger * 0.5f;
HappinessDecreaseRate = element.GetAttributeFloat("happinessdecreaserate", 0.1f);
HungerIncreaseRate = element.GetAttributeFloat("hungerincreaserate", 0.25f);
HappinessDecreaseRate = element.GetAttributeFloat(nameof(HappinessDecreaseRate), 0.1f);
HungerIncreaseRate = element.GetAttributeFloat(nameof(HungerIncreaseRate), 0.25f);
PlayForce = element.GetAttributeFloat("playforce", 15.0f);
@@ -208,9 +228,9 @@ namespace Barotrauma
public StatusIndicatorType GetCurrentStatusIndicatorType()
{
if (Hunger > MaxHunger * 0.5f) { return StatusIndicatorType.Hungry; }
if (Happiness > MaxHappiness * 0.8f) { return StatusIndicatorType.Happy; }
if (Happiness < MaxHappiness * 0.25f) { return StatusIndicatorType.Sad; }
if (Hunger > HungryThreshold) { return StatusIndicatorType.Hungry; }
if (Happiness > HappyThreshold) { return StatusIndicatorType.Happy; }
if (Happiness < UnhappyThreshold) { return StatusIndicatorType.Sad; }
return StatusIndicatorType.None;
}
@@ -264,12 +284,12 @@ namespace Barotrauma
public void Play(Character player)
{
if (PlayTimer > 0.0f) { return; }
if (Owner == null) { Owner = player; }
Owner ??= player;
PlayTimer = 5.0f;
AIController.Character.IsRagdolled = true;
Happiness += 10.0f;
AIController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce);
unstunY = AIController.Character.SimPosition.Y;
UnstunY = AIController.Character.SimPosition.Y;
#if CLIENT
AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f);
#endif
@@ -297,16 +317,16 @@ namespace Barotrauma
var character = AIController.Character;
if (character?.Removed ?? true || character.IsDead) { return; }
if (unstunY.HasValue)
if (UnstunY.HasValue)
{
if (PlayTimer > 4.0f)
{
float extent = character.AnimController.MainLimb.body.GetMaxExtent();
if (character.SimPosition.Y < (unstunY.Value + extent * 3.0f) &&
if (character.SimPosition.Y < (UnstunY.Value + extent * 3.0f) &&
character.AnimController.MainLimb.body.LinearVelocity.Y < 0.0f)
{
character.IsRagdolled = false;
unstunY = null;
UnstunY = null;
}
else
{
@@ -316,7 +336,7 @@ namespace Barotrauma
else
{
character.IsRagdolled = false;
unstunY = null;
UnstunY = null;
}
}
@@ -362,15 +382,11 @@ namespace Barotrauma
{
character.CharacterHealth.ApplyAffliction(character.AnimController.MainLimb, new Affliction(AfflictionPrefab.InternalDamage, 8.0f * deltaTime));
}
else if (Hunger < MaxHunger * 0.1f)
{
character.CharacterHealth.ReduceAllAfflictionsOnAllLimbs(8.0f * deltaTime);
}
if (character.SelectedBy != null)
{
character.IsRagdolled = true;
unstunY = character.SimPosition.Y;
UnstunY = character.SimPosition.Y;
}
for (int i = 0; i < itemsToProduce.Count; i++)
@@ -435,37 +451,34 @@ namespace Barotrauma
spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandomUnsynced();
spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition;
}
var pet = Character.Create(speciesName, spawnPos, seed, spawnInitialItems: false);
var petBehavior = (pet?.AIController as EnemyAIController)?.PetBehavior;
if (petBehavior != null)
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName.ToIdentifier());
if (characterPrefab == null)
{
petBehavior.Owner = owner;
var petBehaviorElement = subElement.Element("petbehavior");
if (petBehaviorElement != null)
DebugConsole.ThrowError($"Failed to load the pet \"{speciesName}\". Character prefab not found.");
continue;
}
var pet = Character.Create(characterPrefab, spawnPos, seed, spawnInitialItems: false);
if (pet != null)
{
var petBehavior = (pet.AIController as EnemyAIController)?.PetBehavior;
if (petBehavior != null)
{
petBehavior.Hunger = petBehaviorElement.GetAttributeFloat("hunger", 50.0f);
petBehavior.Happiness = petBehaviorElement.GetAttributeFloat("happiness", 50.0f);
petBehavior.Owner = owner;
var petBehaviorElement = subElement.Element("petbehavior");
if (petBehaviorElement != null)
{
petBehavior.Hunger = petBehaviorElement.GetAttributeFloat("hunger", 50.0f);
petBehavior.Happiness = petBehaviorElement.GetAttributeFloat("happiness", 50.0f);
}
}
}
var inventoryElement = subElement.Element("inventory");
if (inventoryElement != null)
{
pet.SpawnInventoryItems(pet.Inventory, inventoryElement.FromPackage(null));
}
}
}
}
public void ServerWrite(IWriteMessage msg)
{
msg.WriteRangedSingle(Happiness, 0.0f, MaxHappiness, 8);
msg.WriteRangedSingle(Hunger, 0.0f, MaxHunger, 8);
}
public void ClientRead(IReadMessage msg)
{
Happiness = msg.ReadRangedSingle(0.0f, MaxHappiness, 8);
Hunger = msg.ReadRangedSingle(0.0f, MaxHunger, 8);
}
}
}
@@ -24,7 +24,7 @@ namespace Barotrauma
{
if (TargetItemComponent is Turret turret)
{
if (!turret.CheckTurretAngle(entity.WorldPosition))
if (!turret.IsWithinAimingRadius(entity.WorldPosition))
{
importance *= 0.1f;
}
@@ -350,20 +350,20 @@ namespace Barotrauma
ShipIssueWorkers.Clear();
if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag(Tags.Reactor) && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
{
var order = new Order(OrderPrefab.Prefabs["operatereactor"], "powerup".ToIdentifier(), reactor.Item, reactor);
ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, order));
}
if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("navterminal") && !i.NonInteractable) is Item nav && nav.GetComponent<Steering>() is Steering steeringComponent)
if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag(Tags.NavTerminal) && !i.NonInteractable) is Item nav && nav.GetComponent<Steering>() is Steering steeringComponent)
{
steering = steeringComponent;
var order = new Order(OrderPrefab.Prefabs["steer"], "navigatetactical".ToIdentifier(), nav, steeringComponent);
ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, order));
}
foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret") && !i.HasTag("hardpoint")))
foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag(Tags.Turret) && !i.HasTag(Tags.Hardpoint)))
{
var order = new Order(OrderPrefab.Prefabs["operateweapons"], item, item.GetComponent<Turret>());
ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, order));
@@ -80,7 +80,7 @@ namespace Barotrauma
{
foreach (var turret in turrets)
{
turret.UpdateAutoOperate(deltaTime, friendlyTag);
turret.UpdateAutoOperate(deltaTime, ignorePower: true, friendlyTag);
}
}
}
@@ -107,7 +107,7 @@ namespace Barotrauma
private static IEnumerable<T> GetThalamusEntities<T>(Submarine wreck, Identifier tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T);
private static IEnumerable<MapEntity> GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.mapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag));
private static IEnumerable<MapEntity> GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.MapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag));
private static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag);
@@ -273,6 +273,10 @@ namespace Barotrauma
}
}
destroyedOrgans.ForEach(o => spawnOrgans.Remove(o));
if (!IsClient)
{
if (!initialCellsSpawned) { SpawnInitialCells(); }
}
bool isSomeoneNearby = false;
float minDist = Sonar.DefaultSonarRange * 2.0f;
#if SERVER
@@ -322,7 +326,6 @@ namespace Barotrauma
OperateTurrets(deltaTime, Config.Entity);
if (!IsClient)
{
if (!initialCellsSpawned) { SpawnInitialCells(); }
UpdateReinforcements(deltaTime);
}
}
@@ -27,7 +27,7 @@ namespace Barotrauma
public string Brain { get; private set; }
[Serialize("", IsPropertySaveable.No)]
public string Spawner { get; private set; }
public Identifier Spawner { get; private set; }
[Serialize("", IsPropertySaveable.No)]
public string BrainRoomBackground { get; private set; }
@@ -286,17 +286,32 @@ namespace Barotrauma
public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos)
{
useItemTimer = 0.5f;
useItemTimer = 0.05f;
StartUsingItem();
if (!allowMovement)
{
TargetMovement = Vector2.Zero;
TargetDir = handWorldPos.X > character.WorldPosition.X ? Direction.Right : Direction.Left;
float sqrDist = Vector2.DistanceSquared(character.WorldPosition, handWorldPos);
if (sqrDist > MathUtils.Pow(ConvertUnits.ToDisplayUnits(upperArmLength + forearmLength), 2))
if (InWater)
{
TargetMovement = Vector2.Normalize(handWorldPos - character.WorldPosition) * GetCurrentSpeed(false) * Math.Max(character.SpeedMultiplier, 1);
float sqrDist = Vector2.DistanceSquared(character.WorldPosition, handWorldPos);
if (sqrDist > MathUtils.Pow(ConvertUnits.ToDisplayUnits(upperArmLength + forearmLength), 2))
{
TargetMovement = GetTargetMovement(Vector2.Normalize(handWorldPos - character.WorldPosition));
}
}
else
{
float distX = Math.Abs(handWorldPos.X - character.WorldPosition.X);
if (distX > ConvertUnits.ToDisplayUnits(upperArmLength + forearmLength))
{
TargetMovement = GetTargetMovement(Vector2.UnitX * Math.Sign(handWorldPos.X - character.WorldPosition.X));
}
}
Vector2 GetTargetMovement(Vector2 dir)
{
return dir * GetCurrentSpeed(false) * Math.Max(character.SpeedMultiplier, 1);
}
}
@@ -308,6 +323,15 @@ namespace Barotrauma
handSimPos -= character.Submarine.SimPosition;
}
Vector2 refPos = rightShoulder?.WorldAnchorA ?? leftShoulder?.WorldAnchorA ?? MainLimb.SimPosition;
Vector2 diff = handSimPos - refPos;
float dist = diff.Length();
float maxDist = ArmLength * 0.9f;
if (dist > maxDist)
{
handSimPos = refPos + diff / dist * maxDist;
}
var leftHand = GetLimb(LimbType.LeftHand);
if (leftHand != null)
{
@@ -323,6 +347,16 @@ namespace Barotrauma
rightHand.PullJointEnabled = true;
rightHand.PullJointWorldAnchorB = handSimPos;
}
//make the character crouch if using an item some distance below them (= on the floor)
if (!inWater &&
character.WorldPosition.Y - handWorldPos.Y > ConvertUnits.ToDisplayUnits(CurrentGroundedParams.TorsoPosition) / 4 &&
this is HumanoidAnimController humanoidAnimController)
{
humanoidAnimController.Crouching = true;
humanoidAnimController.ForceSelectAnimationType = AnimationType.Crouch;
character.SetInput(InputType.Crouch, hit: false, held: true);
}
}
public void Grab(Vector2 rightHandPos, Vector2 leftHandPos)
@@ -352,13 +386,8 @@ namespace Barotrauma
//calculate the handle positions
Matrix itemTransfrom = Matrix.CreateRotationZ(item.body.Rotation);
float horizontalOffset = ConvertUnits.ToSimUnits((item.Sprite.size.X / 2 - item.Sprite.Origin.X) * item.Scale);
//handlePos[0] = ConvertUnits.ToSimUnits(new Vector2(-45,25) * 0.5f);
//handlePos[1] = ConvertUnits.ToSimUnits(new Vector2(-65,30) * 0.5f);
transformedHandlePos[0] = Vector2.Transform(new Vector2(handlePos[0].X + horizontalOffset, handlePos[0].Y), itemTransfrom);
transformedHandlePos[1] = Vector2.Transform(new Vector2(handlePos[1].X + horizontalOffset, handlePos[1].Y), itemTransfrom);
transformedHandlePos[0] = Vector2.Transform(handlePos[0], itemTransfrom);
transformedHandlePos[1] = Vector2.Transform(handlePos[1], itemTransfrom);
Limb torso = GetLimb(LimbType.Torso) ?? MainLimb;
Limb leftHand = GetLimb(LimbType.LeftHand);
@@ -385,7 +414,9 @@ namespace Barotrauma
if (aim && !isClimbing && !usingController && character.Stun <= 0.0f && itemPos != Vector2.Zero && !character.IsIncapacitated)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition);
Vector2 diff = holdable.Aimable ? (mousePos - AimSourceSimPos) * Dir : Vector2.UnitX;
Vector2 diff = holdable.Aimable ?
(mousePos - AimSourceSimPos) * Dir :
MathUtils.RotatePoint(Vector2.UnitX, torsoRotation);
holdAngle = MathUtils.VectorToAngle(new Vector2(diff.X, diff.Y * Dir)) - torsoRotation * Dir;
holdAngle += GetAimWobble(rightHand, leftHand, item);
itemAngle = torsoRotation + holdAngle * Dir;
@@ -480,6 +511,16 @@ namespace Barotrauma
return;
}
float targetAngle = MathUtils.WrapAngleTwoPi(itemAngle + itemAngleRelativeToHoldAngle * Dir);
float currentRotation = MathUtils.WrapAngleTwoPi(item.body.Rotation);
float itemRotation = MathHelper.SmoothStep(currentRotation, targetAngle, deltaTime * 25);
if (previousDirection != dir || Math.Abs(targetAngle - currentRotation) > MathHelper.Pi)
{
itemRotation = targetAngle;
}
item.SetTransform(currItemPos, itemRotation, setPrevTransform: false);
previousDirection = dir;
if (holdable.Pusher != null)
{
if (character.Stun > 0.0f || character.IsIncapacitated)
@@ -497,24 +538,11 @@ namespace Barotrauma
else
{
holdable.Pusher.TargetPosition = currItemPos;
holdable.Pusher.TargetRotation = holdAngle * Dir;
holdable.Pusher.TargetRotation = itemRotation;
holdable.Pusher.MoveToTargetPosition(true);
currItemPos = holdable.Pusher.SimPosition;
itemAngle = holdable.Pusher.Rotation;
}
}
}
float targetAngle = MathUtils.WrapAngleTwoPi(itemAngle + itemAngleRelativeToHoldAngle * Dir);
float currentRotation = MathUtils.WrapAngleTwoPi(item.body.Rotation);
float itemRotation = MathHelper.SmoothStep(currentRotation, targetAngle, deltaTime * 25);
if (previousDirection != dir || Math.Abs(targetAngle - currentRotation) > MathHelper.Pi)
{
itemRotation = targetAngle;
}
item.SetTransform(currItemPos, itemRotation, setPrevTransform: false);
previousDirection = dir;
if (!isClimbing && !character.IsIncapacitated && itemPos != Vector2.Zero && (aim || !holdable.UseHandRotationForHoldAngle))
{
@@ -144,7 +144,7 @@ namespace Barotrauma
set { HumanSwimFastParams = value as HumanSwimFastParams; }
}
public bool Crouching;
public bool Crouching { get; set; }
private float upperLegLength = 0.0f, lowerLegLength = 0.0f;
@@ -197,7 +197,7 @@ namespace Barotrauma
public HumanoidAnimController(Character character, string seed, HumanRagdollParams ragdollParams = null) : base(character, seed, ragdollParams)
{
// TODO: load from the character info file?
movementLerp = RagdollParams.MainElement.GetAttributeFloat("movementlerp", 0.4f);
movementLerp = RagdollParams?.MainElement?.GetAttributeFloat("movementlerp", 0.4f) ?? 0f;
}
public override void Recreate(RagdollParams ragdollParams = null)
@@ -243,19 +243,14 @@ namespace Barotrauma
if (MainLimb == null) { return; }
levitatingCollider = !IsHanging;
ColliderIndex = Crouching && !swimming ? 1 : 0;
if ((character.SelectedItem?.GetComponent<Controller>()?.ControlCharacterPose ?? false) ||
(character.SelectedSecondaryItem?.GetComponent<Controller>()?.ControlCharacterPose ?? false) ||
character.SelectedSecondaryItem?.GetComponent<Ladder>() != null ||
(ForceSelectAnimationType != AnimationType.Crouch && ForceSelectAnimationType != AnimationType.NotDefined))
{
Crouching = false;
ColliderIndex = 0;
}
else if (!Crouching && ColliderIndex == 1)
{
Crouching = true;
}
ColliderIndex = Crouching && !swimming ? 1 : 0;
//stun (= disable the animations) if the ragdoll receives a large enough impact
if (strongestImpact > 0.0f)
@@ -417,6 +412,22 @@ namespace Barotrauma
swimming = inWater;
swimmingStateLockTimer = 0.5f;
}
if (character.SelectedItem?.Prefab is { GrabWhenSelected: true } &&
character.SelectedItem.ParentInventory == null &&
character.SelectedItem.body is not { Enabled: true } &&
character.SelectedItem.GetComponent<Repairable>()?.CurrentFixer != character)
{
bool moving = character.IsKeyDown(InputType.Left) || character.IsKeyDown(InputType.Right);
moving |= (character.InWater || character.IsClimbing) && (character.IsKeyDown(InputType.Up) || character.IsKeyDown(InputType.Down));
if (!moving)
{
Vector2 handPos = character.SelectedItem.WorldPosition - Vector2.UnitY * ConvertUnits.ToDisplayUnits(ArmLength / 2);
handPos.Y = Math.Max(handPos.Y, character.SelectedItem.WorldRect.Y - character.SelectedItem.WorldRect.Height);
UpdateUseItem(
allowMovement: false,
handPos);
}
}
if (swimming)
{
UpdateSwimming();
@@ -616,7 +627,7 @@ namespace Barotrauma
if (TorsoAngle.HasValue && !torso.Disabled)
{
float torsoAngle = TorsoAngle.Value;
float herpesStrength = character.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType);
float herpesStrength = character.CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.SpaceHerpesType);
if (Crouching && !movingHorizontally && !Aiming) { torsoAngle -= HumanCrouchParams.ExtraTorsoAngleWhenStationary; }
torsoAngle -= herpesStrength / 150.0f;
torso.body.SmoothRotate(torsoAngle * Dir, currentGroundedParams.TorsoTorque);
@@ -744,6 +744,13 @@ namespace Barotrauma
{
if (character.DisableImpactDamageTimer > 0.0f) { return; }
if (f2.Body?.UserData is Item)
{
//no impact damage from items
//items that can impact characters (melee weapons, projectiles) should handle the damage themselves
return;
}
Vector2 normal = localNormal;
float impact = Vector2.Dot(velocity, -normal);
if (f1.Body == Collider.FarseerBody || !Collider.Enabled)
@@ -753,16 +760,18 @@ namespace Barotrauma
if (isNotRemote)
{
if (impact > ImpactTolerance)
float impactTolerance = ImpactTolerance;
if (character.Stun > 0.0f) { impactTolerance *= 0.5f; }
if (impact > impactTolerance)
{
impactPos = ConvertUnits.ToDisplayUnits(impactPos);
if (character.Submarine != null) impactPos += character.Submarine.Position;
if (character.Submarine != null) { impactPos += character.Submarine.Position; }
float impactDamage = Math.Min((impact - ImpactTolerance) * ImpactDamageMultiplayer, character.MaxVitality * MaxImpactDamage);
float impactDamage = GetImpactDamage(impact, impactTolerance);
character.LastDamageSource = null;
character.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(impactDamage).ToEnumerable(), 0.0f, true);
strongestImpact = Math.Max(strongestImpact, impact - ImpactTolerance);
strongestImpact = Math.Max(strongestImpact, impact - impactTolerance);
character.ApplyStatusEffects(ActionType.OnImpact, 1.0f);
//briefly disable impact damage
//otherwise the character will take damage multiple times when for example falling,
@@ -776,6 +785,12 @@ namespace Barotrauma
ImpactProjSpecific(impact, f1.Body);
}
public float GetImpactDamage(float impact, float? impactTolerance = null)
{
float tolerance = impactTolerance ?? ImpactTolerance;
return Math.Min((impact - tolerance) * ImpactDamageMultiplayer, character.MaxVitality * MaxImpactDamage);
}
private readonly List<Limb> connectedLimbs = new List<Limb>();
private readonly List<LimbJoint> checkedJoints = new List<LimbJoint>();
public bool SeverLimbJoint(LimbJoint limbJoint)
@@ -1023,7 +1038,12 @@ namespace Barotrauma
CurrentHull = newHull;
character.Submarine = currentHull?.Submarine;
character.AttachedProjectiles.ForEach(p => p?.Item?.UpdateTransform());
foreach (var attachedProjectile in character.AttachedProjectiles)
{
attachedProjectile.Item.CurrentHull = currentHull;
attachedProjectile.Item.Submarine = character.Submarine;
attachedProjectile.Item.UpdateTransform();
}
}
private void PreventOutsideCollision()
@@ -1323,6 +1343,11 @@ namespace Barotrauma
if (Collider.LinearVelocity == Vector2.Zero)
{
character.IsRagdolled = true;
if (character.IsBot)
{
// Seems to work without this on player controlled characters -> not sure if we should call it always or just for the bots.
character.SetInput(InputType.Ragdoll, hit: false, held: true);
}
}
}
}
@@ -1781,12 +1806,6 @@ namespace Barotrauma
Character.Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true));
Character.Latchers.Clear();
if (detachProjectiles)
{
character.AttachedProjectiles.ForEachMod(p => p?.Unstick());
character.AttachedProjectiles.Clear();
}
Vector2 limbMoveAmount = forceMainLimbToCollider ? simPosition - MainLimb.SimPosition : simPosition - Collider.SimPosition;
if (lerp)
{
@@ -1823,7 +1842,7 @@ namespace Barotrauma
protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, float rotation, bool lerp = false, bool ignorePlatforms = true)
{
Vector2 movePos = simPosition;
Vector2 prevPosition = limb.body.SimPosition;
if (Vector2.DistanceSquared(original, simPosition) > 0.0001f)
{
Category collisionCategory = Physics.CollisionWall | Physics.CollisionLevel;
@@ -1851,6 +1870,16 @@ namespace Barotrauma
limb.PullJointWorldAnchorB = limb.PullJointWorldAnchorA;
limb.PullJointEnabled = false;
}
foreach (var attachedProjectile in character.AttachedProjectiles)
{
if (attachedProjectile.IsAttachedTo(limb.body))
{
attachedProjectile.Item.SetTransform(
attachedProjectile.Item.SimPosition + (movePos - prevPosition),
attachedProjectile.Item.body.Rotation,
findNewHull: false);
}
}
}
@@ -210,8 +210,8 @@ namespace Barotrauma
[Serialize(5f, IsPropertySaveable.Yes, description: "How fast the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items)."), Editable]
public float SwayFrequency { get; set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "Legacy support. Use Afflictions.")]
public float Stun { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No, description: "Legacy functionality. Behaves otherwise the same as stuns defined as afflictions, but explosions only apply the stun once instead of dividing it between the limbs.")]
public float Stun { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable]
public bool OnlyHumans { get; set; }
@@ -434,13 +434,7 @@ namespace Barotrauma
}
break;
case "conditional":
foreach (XAttribute attribute in subElement.Attributes())
{
if (PropertyConditional.IsValid(attribute))
{
Conditionals.Add(new PropertyConditional(attribute));
}
}
Conditionals.AddRange(PropertyConditional.FromXElement(subElement));
break;
}
}
@@ -544,8 +538,7 @@ namespace Barotrauma
}
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
// TODO: do we need the conversion to list here? It generates garbage.
var targets = targetCharacter.AnimController.Limbs.Cast<ISerializableEntity>().ToList();
var targets = targetCharacter.AnimController.Limbs;
if (additionalEffectType != ActionType.OnEating)
{
effect.Apply(conditionalEffectType, deltaTime, targetCharacter, targets);
@@ -612,7 +605,10 @@ namespace Barotrauma
float penetration = Penetration;
float? penetrationValue = SourceItem?.GetComponent<RangedWeapon>()?.Penetration;
RangedWeapon weapon =
SourceItem?.GetComponent<RangedWeapon>() ??
SourceItem?.GetComponent<Projectile>()?.Launcher?.GetComponent<RangedWeapon>();
float? penetrationValue = weapon?.Penetration;
if (penetrationValue.HasValue)
{
penetration += penetrationValue.Value;
@@ -646,8 +642,7 @@ namespace Barotrauma
}
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
// TODO: do we need the conversion to list here? It generates garbage.
var targets = targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList();
var targets = targetLimb.character.AnimController.Limbs;
effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targets);
effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targets);
}
@@ -65,6 +65,13 @@ namespace Barotrauma
}
UpdateLimbLightSource(limb);
}
foreach (var item in HeldItems)
{
if (item.body != null)
{
item.body.Enabled = enabled;
}
}
AnimController.Collider.Enabled = value;
}
}
@@ -335,7 +342,7 @@ namespace Barotrauma
public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator;
public CombatAction CombatAction;
public AnimController AnimController;
public readonly AnimController AnimController;
private Vector2 cursorPosition;
@@ -386,6 +393,8 @@ namespace Barotrauma
public bool IsMachine => Params.IsMachine;
public bool IsHusk => Params.Husk;
public bool IsDisguisedAsHusk => CharacterHealth.GetAfflictionStrengthByType("disguiseashusk".ToIdentifier()) > 0;
public bool IsHuskInfected => CharacterHealth.GetActiveAfflictionTags().Contains("huskinfected".ToIdentifier());
public bool IsMale => info?.IsMale ?? false;
@@ -781,7 +790,7 @@ namespace Barotrauma
get
{
if (IsUnconscious) { return true; }
return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.AfflictionType == AfflictionPrefab.ParalysisType && a.Strength >= a.Prefab.MaxStrength);
return CharacterHealth.IsParalyzed;
}
}
@@ -792,7 +801,7 @@ namespace Barotrauma
public bool IsArrested
{
get { return IsHuman && HasEquippedItem("handlocker"); }
get { return IsHuman && HasEquippedItem(Tags.HandLockerItem); }
}
public bool IsPet
@@ -852,6 +861,7 @@ namespace Barotrauma
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 bool IsFlipped => AnimController.IsFlipped;
public float Bloodloss
{
@@ -865,7 +875,7 @@ namespace Barotrauma
public float Bleeding
{
get { return CharacterHealth.GetAfflictionStrength(AfflictionPrefab.BleedingType, true); }
get { return CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.BleedingType, true); }
}
private bool speechImpedimentSet;
@@ -936,6 +946,8 @@ namespace Barotrauma
{
GameMain.GameSession?.CrewManager?.AutoHideCrewList();
}
_selectedItem?.GetComponent<CircuitBox>()?.OnViewUpdateProjSpecific();
}
#endif
if (prevSelectedItem != null && (_selectedItem == null || _selectedItem != prevSelectedItem) && itemSelectedTime > 0)
@@ -1073,8 +1085,17 @@ namespace Barotrauma
public bool IsLowInOxygen => CharacterHealth.OxygenAmount < 100;
/// <summary>
/// Godmoded characters cannot receive any afflictions whatsoever
/// </summary>
public bool GodMode = false;
public bool Unkillable
{
get { return CharacterHealth.Unkillable; }
set { CharacterHealth.Unkillable = value; }
}
public CampaignMode.InteractionType CampaignInteractionType;
public Identifier MerchantIdentifier;
@@ -1308,9 +1329,9 @@ namespace Barotrauma
break;
}
}
if (Params.VariantFile != null)
if (Params.VariantFile != null && Params.MainElement is ContentXElement paramsMainElement)
{
var overrideElement = Params.VariantFile.Root.FromPackage(Params.MainElement.ContentPackage);
var overrideElement = Params.VariantFile.Root.FromPackage(paramsMainElement.ContentPackage);
// Only override if the override file contains matching elements
if (overrideElement.GetChildElement("inventory") != null)
{
@@ -1484,7 +1505,7 @@ namespace Barotrauma
var head = AnimController.GetLimb(LimbType.Head);
if (head == null) { return; }
// Note that if there are any other wearables on the head, they are removed here.
head.OtherWearables.ForEach(w => w.Sprite.Remove());
head.OtherWearables.ForEach(w => w.Sprite?.Remove());
head.OtherWearables.Clear();
//if the element has not been set at this point, the character has no hair and the index should be zero (= no hair)
@@ -1664,17 +1685,31 @@ namespace Barotrauma
info.Job?.GiveJobItems(this, spawnPoint);
}
public void GiveIdCardTags(WayPoint spawnPoint, bool createNetworkEvent = false)
public void GiveIdCardTags(WayPoint spawnPoint, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false)
{
if (info?.Job == null || spawnPoint == null) { return; }
GiveIdCardTags(spawnPoint.ToEnumerable(), requireSpawnPointTagsNotGiven, createNetworkEvent);
}
public void GiveIdCardTags(IEnumerable<WayPoint> spawnPoints, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false)
{
if (info?.Job == null || spawnPoints == null) { return; }
foreach (Item item in Inventory.AllItems)
{
if (item?.GetComponent<IdCard>() == null) { continue; }
foreach (string s in spawnPoint.IdCardTags)
if (item?.GetComponent<IdCard>() is not IdCard idCard) { continue; }
if (requireSpawnPointTagsNotGiven)
{
item.AddTag(s);
if (idCard.SpawnPointTagsGiven) { continue; }
}
foreach (var spawnPoint in spawnPoints)
{
foreach (string s in spawnPoint.IdCardTags)
{
item.AddTag(s);
}
}
idCard.SpawnPointTagsGiven = true;
if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item));
@@ -1983,14 +2018,21 @@ namespace Barotrauma
if (AnimController is HumanoidAnimController humanAnimController)
{
humanAnimController.Crouching = humanAnimController.ForceSelectAnimationType == AnimationType.Crouch || IsKeyDown(InputType.Crouch);
humanAnimController.Crouching =
humanAnimController.ForceSelectAnimationType == AnimationType.Crouch ||
IsKeyDown(InputType.Crouch);
if (Screen.Selected is not { IsEditor: true })
{
humanAnimController.ForceSelectAnimationType = AnimationType.NotDefined;
}
}
if (!aiControlled &&
!AnimController.IsUsingItem &&
AnimController.Anim != AnimController.Animation.CPR &&
(GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient || Controlled == this) &&
(AnimController.OnGround || IsClimbing) && !AnimController.InWater)
((!IsClimbing && AnimController.OnGround) || (IsClimbing && IsKeyDown(InputType.Aim))) &&
!AnimController.InWater)
{
if (dontFollowCursor)
{
@@ -2187,14 +2229,14 @@ namespace Barotrauma
{
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
{
item.Use(deltaTime, this);
item.Use(deltaTime, user: this);
}
}
if (IsKeyDown(InputType.Shoot) && item.IsShootable)
{
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
{
item.Use(deltaTime, this);
item.Use(deltaTime, user: this);
}
#if CLIENT
else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim))
@@ -2214,7 +2256,7 @@ namespace Barotrauma
{
if (!SelectedCharacter.CanBeSelected ||
(Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > MaxDragDistance * MaxDragDistance &&
SelectedCharacter.GetDistanceToClosestLimb(SimPosition) > ConvertUnits.ToSimUnits(MaxDragDistance)))
SelectedCharacter.GetDistanceToClosestLimb(GetRelativeSimPosition(selectedCharacter, WorldPosition)) > ConvertUnits.ToSimUnits(MaxDragDistance)))
{
DeselectCharacter();
}
@@ -2247,26 +2289,58 @@ namespace Barotrauma
};
}
public bool CanSeeCharacter(Character target)
private Limb GetSeeingLimb()
{
return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb;
}
public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null, bool checkFacing = false)
{
seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb();
if (target is Character targetCharacter)
{
return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing);
}
else
{
return CheckVisibility(target, seeingEntity, checkFacing);
}
}
public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false)
{
if (seeingEntity is Character seeingCharacter)
{
return seeingCharacter.CanSeeTarget(target, checkFacing: checkFacing);
}
if (target is Character targetCharacter)
{
return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing);
}
else
{
return CheckVisibility(target, seeingEntity, checkFacing);
}
}
private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool checkFacing = false)
{
System.Diagnostics.Debug.Assert(target != null);
if (target == null) { return false; }
if (target.Removed) { return false; }
Limb seeingLimb = GetSeeingLimb();
if (CanSeeTarget(target, seeingLimb)) { return true; }
if (target == null || target.Removed) { return false; }
if (CheckVisibility(target, seeingEntity, checkFacing)) { return true; }
if (!target.AnimController.SimplePhysicsEnabled)
{
//find the limbs that are furthest from the target's position (from the viewer's point of view)
Limb leftExtremity = null, rightExtremity = null;
float leftMostDot = 0.0f, rightMostDot = 0.0f;
Vector2 dir = target.WorldPosition - WorldPosition;
Vector2 dir = target.WorldPosition - seeingEntity.WorldPosition;
Vector2 leftDir = new Vector2(dir.Y, -dir.X);
Vector2 rightDir = new Vector2(-dir.Y, dir.X);
foreach (Limb limb in target.AnimController.Limbs)
{
if (limb.IsSevered || limb == target.AnimController.MainLimb) { continue; }
if (limb.Hidden) { continue; }
Vector2 limbDir = limb.WorldPosition - WorldPosition;
Vector2 limbDir = limb.WorldPosition - seeingEntity.WorldPosition;
float leftDot = Vector2.Dot(limbDir, leftDir);
if (leftDot > leftMostDot)
{
@@ -2282,42 +2356,39 @@ namespace Barotrauma
continue;
}
}
if (leftExtremity != null && CanSeeTarget(leftExtremity, seeingLimb)) { return true; }
if (rightExtremity != null && CanSeeTarget(rightExtremity, seeingLimb)) { return true; }
if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, checkFacing)) { return true; }
if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, checkFacing)) { return true; }
}
return false;
}
private Limb GetSeeingLimb()
{
return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb;
}
public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null)
private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false)
{
System.Diagnostics.Debug.Assert(target != null);
if (target == null) { return false; }
seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb() as ISpatialEntity;
if (seeingEntity == null) { return false; }
ISpatialEntity sourceEntity = seeingEntity ;
// TODO: Could we just use the method below? If not, let's refactor it so that we can.
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - sourceEntity.WorldPosition);
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition);
if (checkFacing && seeingEntity is Character seeingCharacter)
{
if (Math.Sign(diff.X) != seeingCharacter.AnimController.Dir) { return false; }
}
Body closestBody;
//both inside the same sub (or both outside)
//OR the we're inside, the other character outside
if (target.Submarine == Submarine || target.Submarine == null)
if (target.Submarine == seeingEntity.Submarine || target.Submarine == null)
{
closestBody = Submarine.CheckVisibility(sourceEntity.SimPosition, sourceEntity.SimPosition + diff);
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
}
//we're outside, the other character inside
else if (Submarine == null)
else if (seeingEntity.Submarine == null)
{
closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff);
}
//both inside different subs
else
{
closestBody = Submarine.CheckVisibility(sourceEntity.SimPosition, sourceEntity.SimPosition + diff);
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
if (!IsBlocking(closestBody))
{
closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff);
@@ -2370,9 +2441,6 @@ namespace Barotrauma
return false;
}
public bool HasEquippedItem(string tagOrIdentifier, bool allowBroken = true, InvSlotType? slotType = null)
=> HasEquippedItem(tagOrIdentifier.ToIdentifier(), allowBroken, slotType);
public bool HasEquippedItem(Identifier tagOrIdentifier, bool allowBroken = true, InvSlotType? slotType = null)
{
if (Inventory == null) { return false; }
@@ -2394,7 +2462,7 @@ namespace Barotrauma
return false;
}
public Item GetEquippedItem(string tagOrIdentifier = null, InvSlotType? slotType = null)
public Item GetEquippedItem(Identifier? tagOrIdentifier = null, InvSlotType? slotType = null)
{
if (Inventory == null) { return null; }
for (int i = 0; i < Inventory.Capacity; i++)
@@ -2409,7 +2477,10 @@ namespace Barotrauma
}
var item = Inventory.GetItemAt(i);
if (item == null) { continue; }
if (tagOrIdentifier == null || item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return item; }
if (tagOrIdentifier == null || tagOrIdentifier.Value.IsEmpty || item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier.Value))
{
return item;
}
}
return null;
}
@@ -2537,7 +2608,7 @@ namespace Barotrauma
}
}
return !checkVisibility || CanSeeCharacter(c);
return !checkVisibility || CanSeeTarget(c);
}
public bool CanInteractWith(Item item, bool checkLinked = true)
@@ -2587,7 +2658,10 @@ namespace Barotrauma
{
foreach (MapEntity linked in item.linkedTo)
{
if (linked is Item linkedItem)
if (linked is Item linkedItem &&
//if the linked item is inside this container (a modder or sub builder doing smth really weird?)
//don't check it here because it'd lead to an infinite loop
linkedItem.ParentInventory?.Owner != item)
{
if (CanInteractWith(linkedItem, out float distToLinked, checkLinked: false))
{
@@ -2675,10 +2749,18 @@ namespace Barotrauma
if (SelectedSecondaryItem != null && !item.IsSecondaryItem)
{
//don't allow selecting another Controller if it'd try to turn the character in the opposite direction
//(e.g. periscope that's facing the wrong way while sitting in a chair)
if (item.GetComponent<Controller>() is { } controller && controller.Direction != 0 && controller.Direction != AnimController.Direction) { return false; }
float threshold = ConvertUnits.ToSimUnits(cursorFollowMargin);
if (AnimController.Direction == Direction.Left && SimPosition.X + threshold < itemPosition.X) { return false; }
if (AnimController.Direction == Direction.Right && SimPosition.X - threshold > itemPosition.X) { return false; }
//if a Controller that controls the character's pose is selected,
//don't allow selecting items that are behind the character's back
if (SelectedSecondaryItem.GetComponent<Controller>() is { ControlCharacterPose: true } selectedController)
{
float threshold = ConvertUnits.ToSimUnits(cursorFollowMargin);
if (AnimController.Direction == Direction.Left && SimPosition.X + threshold < itemPosition.X) { return false; }
if (AnimController.Direction == Direction.Right && SimPosition.X - threshold > itemPosition.X) { return false; }
}
}
if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger)
@@ -2703,7 +2785,7 @@ namespace Barotrauma
this.onCustomInteract = onCustomInteract;
CustomInteractHUDText = hudText;
}
public void SelectCharacter(Character character)
{
if (character == null || character == this) { return; }
@@ -2754,7 +2836,7 @@ namespace Barotrauma
if (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld)
{
FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null;
if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; }
if (FocusedCharacter != null && !CanSeeTarget(FocusedCharacter)) { FocusedCharacter = null; }
float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f);
if (HeldItems.Any(it => it?.GetComponent<Wire>()?.IsActive ?? false))
{
@@ -2867,7 +2949,7 @@ namespace Barotrauma
}
#endif
}
else
else if (!IsClimbing)
{
#if CLIENT
if (Controlled == this)
@@ -2915,9 +2997,9 @@ namespace Barotrauma
CharacterHealth.OpenHealthWindow = null;
#endif
}
else if (IsKeyHit(InputType.Health) && (SelectedItem != null || SelectedSecondaryItem != null))
else if (IsKeyHit(InputType.Health) && SelectedItem != null)
{
SelectedItem = SelectedSecondaryItem = null;
SelectedItem = null;
}
else if (focusedItem != null)
{
@@ -3107,6 +3189,14 @@ namespace Barotrauma
//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 (PressureTimer > CharacterHealth.PressureKillDelay * 0.1f)
{
//after a brief delay, start doing increasing amounts of organ damage
CharacterHealth.ApplyAffliction(
targetLimb: AnimController.MainLimb,
new Affliction(AfflictionPrefab.OrganDamage, PressureTimer / 10.0f * deltaTime));
}
if (CharacterHealth.PressureKillDelay <= 0.0f)
{
PressureTimer = 100.0f;
@@ -3176,47 +3266,48 @@ namespace Barotrauma
UpdateAIChatMessages(deltaTime);
if (GameMain.NetworkMember?.ServerSettings?.AllowRagdollButton ?? true)
bool wasRagdolled = IsRagdolled;
if (IsForceRagdolled)
{
bool wasRagdolled = IsRagdolled;
if (IsForceRagdolled)
IsRagdolled = IsForceRagdolled;
}
else if (this != Controlled)
{
wasRagdolled = IsRagdolled;
IsRagdolled = IsKeyDown(InputType.Ragdoll);
if (IsRagdolled && IsBot && GameMain.NetworkMember is not { IsClient: true })
{
IsRagdolled = IsForceRagdolled;
}
else if (this != Controlled)
{
wasRagdolled = IsRagdolled;
IsRagdolled = IsKeyDown(InputType.Ragdoll);
}
else
{
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)
{
CheckTalents(AbilityEffectType.OnRagdoll);
ClearInput(InputType.Ragdoll);
}
}
else
{
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)
{
CheckTalents(AbilityEffectType.OnRagdoll);
}
lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f);
@@ -3419,7 +3510,7 @@ namespace Barotrauma
}
private float despawnTimer;
private void UpdateDespawn(float deltaTime, bool ignoreThresholds = false, bool createNetworkEvents = true)
private void UpdateDespawn(float deltaTime, bool createNetworkEvents = true)
{
if (!EnableDespawn) { return; }
@@ -3430,7 +3521,7 @@ namespace Barotrauma
int subCorpseCount = 0;
if (Submarine != null && !ignoreThresholds)
if (Submarine != null)
{
subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine);
if (subCorpseCount < GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) { return; }
@@ -3464,6 +3555,11 @@ namespace Barotrauma
despawnTimer += deltaTime * despawnPriority;
if (despawnTimer < GameSettings.CurrentConfig.CorpseDespawnDelay) { return; }
Despawn();
}
private void Despawn(bool createNetworkEvents = true)
{
Identifier despawnContainerId =
IsHuman ?
"despawncontainer".ToIdentifier() :
@@ -3514,8 +3610,7 @@ namespace Barotrauma
public void DespawnNow(bool createNetworkEvents = true)
{
despawnTimer = GameSettings.CurrentConfig.CorpseDespawnDelay;
UpdateDespawn(1.0f, ignoreThresholds: true, createNetworkEvents: createNetworkEvents);
Despawn(createNetworkEvents);
//update twice: first to spawn the duffel bag and move the items into it, then to remove the character
for (int i = 0; i < 2; i++)
{
@@ -4259,7 +4354,7 @@ namespace Barotrauma
if (Screen.Selected != GameMain.GameScreen) { return; }
if (newStun > 0 && Params.Health.StunImmunity)
{
if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0)
if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0)
{
return;
}
@@ -4323,8 +4418,7 @@ namespace Barotrauma
if (limb.IsSevered) { continue; }
if (limb.type == limbType)
{
statusEffect.sourceBody = limb.body;
statusEffect.Apply(actionType, deltaTime, this, limb);
ApplyToLimb(actionType, deltaTime, statusEffect, this, limb);
}
}
}
@@ -4334,8 +4428,7 @@ namespace Barotrauma
Limb limb = AnimController.GetLimb(limbType);
if (limb != null)
{
statusEffect.sourceBody = limb.body;
statusEffect.Apply(actionType, deltaTime, this, limb);
ApplyToLimb(actionType, deltaTime, statusEffect, this, limb);
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb))
@@ -4344,12 +4437,20 @@ namespace Barotrauma
Limb limb = AnimController.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden);
if (limb != null)
{
statusEffect.sourceBody = limb.body;
statusEffect.Apply(actionType, deltaTime, this, limb);
ApplyToLimb(actionType, deltaTime, statusEffect, this, limb);
}
}
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
// Target all limbs
foreach (var limb in AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
ApplyToLimb(actionType, deltaTime, statusEffect, character: this, limb);
}
}
if (statusEffect.HasTargetType(StatusEffect.TargetType.This) || statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.Apply(actionType, deltaTime, this, this);
@@ -4373,6 +4474,12 @@ namespace Barotrauma
{
CharacterHealth.ApplyAfflictionStatusEffects(actionType);
}
static void ApplyToLimb(ActionType actionType, float deltaTime, StatusEffect statusEffect, Character character, Limb limb)
{
statusEffect.sourceBody = limb.body;
statusEffect.Apply(actionType, deltaTime, entity: character, target: limb);
}
}
private void Implode(bool isNetworkMessage = false)
@@ -4433,7 +4540,7 @@ namespace Barotrauma
public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true)
{
if (IsDead || CharacterHealth.Unkillable || GodMode) { return; }
if (IsDead || CharacterHealth.Unkillable || GodMode || Removed) { return; }
HealthUpdateInterval = 0.0f;
@@ -4533,20 +4640,21 @@ namespace Barotrauma
SelectedCharacter = null;
AnimController.ResetPullJoints();
foreach (var joint in AnimController.LimbJoints)
if (AnimController.LimbJoints != null)
{
if (joint.revoluteJoint != null)
foreach (var joint in AnimController.LimbJoints)
{
joint.revoluteJoint.MotorEnabled = false;
if (joint.revoluteJoint != null)
{
joint.revoluteJoint.MotorEnabled = false;
}
}
}
GameMain.GameSession?.KillCharacter(this);
}
partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log);
public void Revive(bool removeAllAfflictions = true)
public void Revive(bool removeAfflictions = true)
{
if (Removed)
{
@@ -4557,20 +4665,21 @@ namespace Barotrauma
aiTarget?.Remove();
aiTarget = new AITarget(this);
if (removeAllAfflictions)
if (removeAfflictions)
{
CharacterHealth.RemoveAllAfflictions();
SetAllDamage(0.0f, 0.0f, 0.0f);
Bloodloss = 0.0f;
SetStun(0.0f, true);
}
else
{
CharacterHealth.RemoveNegativeAfflictions();
}
SetAllDamage(0.0f, 0.0f, 0.0f);
Oxygen = 100.0f;
Bloodloss = 0.0f;
SetStun(0.0f, true);
isDead = false;
if (info != null)
{
info.CauseOfDeath = null;
}
foreach (LimbJoint joint in AnimController.LimbJoints)
{
var revoluteJoint = joint.revoluteJoint;
@@ -4594,10 +4703,7 @@ namespace Barotrauma
limb.IsSevered = false;
}
if (GameMain.GameSession != null)
{
GameMain.GameSession.ReviveCharacter(this);
}
GameMain.GameSession?.ReviveCharacter(this);
}
public override void Remove()
@@ -4712,7 +4818,7 @@ namespace Barotrauma
{
SpawnInventoryItemsRecursive(inventory, itemData, new List<Item>());
}
private void SpawnInventoryItemsRecursive(Inventory inventory, ContentXElement element, List<Item> extraDuffelBags)
{
foreach (var itemElement in element.Elements())
@@ -4737,21 +4843,6 @@ namespace Barotrauma
continue;
}
//make sure there's no other item in the slot
//this should not happen normally, but can occur if the character is accidentally given new job items while also loading previous items in the campaign
for (int i = 0; i < inventory.Capacity; i++)
{
if (slotIndices.Contains(i))
{
var existingItem = inventory.GetItemAt(i);
if (existingItem != null && existingItem != newItem && (((MapEntity)existingItem).Prefab != ((MapEntity)newItem).Prefab || existingItem.Prefab.MaxStackSize == 1))
{
DebugConsole.ThrowError($"Error while loading character inventory data. The slot {i} was already occupied by the item \"{existingItem.Name} ({existingItem.ID})\" when loading the item \"{newItem.Name} ({newItem.ID})\"");
existingItem.Drop(null, createNetworkEvent: false);
}
}
}
bool canBePutInOriginalInventory = true;
if (slotIndices[0] >= inventory.Capacity)
{
@@ -4833,6 +4924,13 @@ namespace Barotrauma
}
}
#if SERVER
foreach (var circuitBox in newItem.GetComponents<CircuitBox>())
{
circuitBox.MarkServerRequiredInitialization();
}
#endif
int itemContainerIndex = 0;
var itemContainers = newItem.GetComponents<ItemContainer>().ToList();
foreach (var childInvElement in itemElement.Elements())
@@ -4921,41 +5019,7 @@ namespace Barotrauma
return visibleHulls;
}
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 = to.SimPosition;
if (worldPos.HasValue)
{
Vector2 wp = worldPos.Value;
if (to.Submarine != null)
{
wp -= to.Submarine.Position;
}
targetPos = ConvertUnits.ToSimUnits(wp);
}
if (from.Submarine == null && to.Submarine != null)
{
// outside and targeting inside
targetPos += to.Submarine.SimPosition;
}
else if (from.Submarine != null && to.Submarine == null)
{
// inside and targeting outside
targetPos -= from.Submarine.SimPosition;
}
else if (from.Submarine != to.Submarine)
{
if (from.Submarine != null && to.Submarine != null)
{
// both inside, but in different subs
Vector2 diff = from.Submarine.SimPosition - to.Submarine.SimPosition;
targetPos -= diff;
}
}
return targetPos;
}
public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null) => Submarine.GetRelativeSimPosition(this, target, worldPos);
public bool IsCaptain => HasJob("captain");
public bool IsEngineer => HasJob("engineer");
@@ -4966,6 +5030,8 @@ namespace Barotrauma
public bool IsWatchman => HasJob("watchman");
public bool IsVip => HasJob("prisoner");
public bool IsPrisoner => HasJob("prisoner");
public bool IsKiller => HasJob("killer");
public Color? UniqueNameColor { get; set; } = null;
public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier;
@@ -5052,6 +5118,7 @@ namespace Barotrauma
public bool HasTalent(Identifier identifier)
{
if (info == null) { return false; }
return info.UnlockedTalents.Contains(identifier);
}
@@ -37,12 +37,18 @@ namespace Barotrauma
public EventType EventType { get; }
}
public struct InventoryStateEventData : IEventData
public readonly struct InventoryStateEventData : IEventData
{
public EventType EventType => EventType.InventoryState;
public readonly Range SlotRange;
public InventoryStateEventData(Range slotRange)
{
SlotRange = slotRange;
}
}
public struct ControlEventData : IEventData
public readonly struct ControlEventData : IEventData
{
public EventType EventType => EventType.Control;
public readonly Client Owner;
@@ -440,10 +440,9 @@ namespace Barotrauma
{
if (handleBuff)
{
var head = Character.AnimController.GetLimb(LimbType.Head);
if (head != null)
if (AfflictionPrefab.Prefabs.TryGet("disguised", out AfflictionPrefab afflictionPrefab))
{
Character.CharacterHealth.ApplyAffliction(head, AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == "disguised").Instantiate(100f));
Character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(100f));
}
}
@@ -464,11 +463,7 @@ namespace Barotrauma
if (handleBuff)
{
var head = Character.AnimController.GetLimb(LimbType.Head);
if (head != null)
{
Character.CharacterHealth.ReduceAfflictionOnLimb(head, "disguised".ToIdentifier(), 100f);
}
Character.CharacterHealth.ReduceAfflictionOnAllLimbs("disguised".ToIdentifier(), 100f);
}
}
@@ -683,11 +678,15 @@ namespace Barotrauma
FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray();
var headPreset = Prefab.Heads.GetRandom(randSync);
var headPreset = Prefab?.Heads.GetRandom(randSync);
if (headPreset == null)
{
DebugConsole.ThrowError("Failed to find a head preset!");
}
Head = new HeadInfo(this, headPreset);
SetAttachments(randSync);
SetColors(randSync);
Job = job ?? ((jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, randSync, variant));
if (!string.IsNullOrEmpty(name))
@@ -1089,6 +1088,7 @@ namespace Barotrauma
private void LoadHeadSprite()
{
if (Ragdoll?.MainElement == null) { return; }
foreach (var limbElement in Ragdoll.MainElement.Elements())
{
if (!limbElement.GetAttributeString("type", string.Empty).Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; }
@@ -1386,7 +1386,7 @@ namespace Barotrauma
// Replace the name tag of any existing id cards or duffel bags
foreach (var item in Item.ItemList)
{
if (!item.HasTag("identitycard") && !item.HasTag("despawncontainer")) { continue; }
if (!item.HasTag("identitycard".ToIdentifier()) && !item.HasTag("despawncontainer".ToIdentifier())) { continue; }
foreach (var tag in item.Tags.Split(','))
{
var splitTag = tag.Split(":");
@@ -1446,7 +1446,7 @@ namespace Barotrauma
if (MinReputationToHire.factionId != default)
{
charElement.Add(
new XAttribute("factionId", Name),
new XAttribute("factionId", MinReputationToHire.factionId),
new XAttribute("minreputation", MinReputationToHire.reputation));
}
@@ -1655,8 +1655,12 @@ namespace Barotrauma
continue;
}
var targetType = (Order.OrderTargetType)orderElement.GetAttributeInt("targettype", 0);
int orderGiverInfoId = orderElement.GetAttributeInt("ordergiver", -1);
var orderGiver = orderGiverInfoId >= 0 ? Character.CharacterList.FirstOrDefault(c => c.Info?.GetIdentifier() == orderGiverInfoId) : null;
Character orderGiver = null;
if (orderElement.GetAttribute("ordergiver") is XAttribute orderGiverIdAttribute)
{
int orderGiverInfoId = orderGiverIdAttribute.GetAttributeInt(0);
orderGiver = Character.CharacterList.FirstOrDefault(c => c.Info?.GetIdentifier() == orderGiverInfoId);
}
Entity targetEntity = null;
switch (targetType)
{
@@ -46,6 +46,8 @@ namespace Barotrauma
public static IEnumerable<ContentXElement> ConfigElements => Prefabs.Select(p => p.ConfigElement);
public static readonly Identifier HumanSpeciesName = "human".ToIdentifier();
public static readonly Identifier HumanGroup = "human".ToIdentifier();
public static CharacterFile HumanConfigFile => HumanPrefab.ContentFile as CharacterFile;
public static CharacterPrefab HumanPrefab => FindBySpeciesName(HumanSpeciesName);
@@ -7,6 +7,7 @@ using System.Xml.Linq;
using Barotrauma.Extensions;
using System.Collections.Immutable;
using Barotrauma.Items.Components;
using System.Linq;
namespace Barotrauma
{
@@ -602,7 +603,6 @@ namespace Barotrauma
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"];
@@ -612,6 +612,7 @@ namespace Barotrauma
public static AfflictionPrefab OxygenLow => Prefabs["oxygenlow"];
public static AfflictionPrefab Bloodloss => Prefabs["bloodloss"];
public static AfflictionPrefab Pressure => Prefabs["pressure"];
public static AfflictionPrefab OrganDamage => Prefabs["organdamage"];
public static AfflictionPrefab Stun => Prefabs[StunType];
public static AfflictionPrefab RadiationSickness => Prefabs["radiationsickness"];
@@ -841,20 +842,16 @@ namespace Barotrauma
/// </summary>
public readonly Sprite AfflictionOverlay;
public IEnumerable<KeyValuePair<Identifier, float>> TreatmentSuitability
public ImmutableDictionary<Identifier, float> TreatmentSuitabilities
{
get
{
foreach (var itemPrefab in ItemPrefab.Prefabs)
{
float suitability = itemPrefab.GetTreatmentSuitability(Identifier) + itemPrefab.GetTreatmentSuitability(AfflictionType);
if (!MathUtils.NearlyEqual(suitability, 0.0f))
{
yield return new KeyValuePair<Identifier, float>(itemPrefab.Identifier, suitability);
}
}
}
}
get;
private set;
} = new Dictionary<Identifier, float>().ToImmutableDictionary();
/// <summary>
/// Can this affliction be treated with some item?
/// </summary>
public bool HasTreatments { get; private set; }
public AfflictionPrefab(ContentXElement element, AfflictionsFile file, Type type) : base(file, element.GetAttributeIdentifier("identifier", ""))
{
@@ -974,6 +971,22 @@ namespace Barotrauma
constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) });
}
private void RefreshTreatmentSuitabilities()
{
var newTreatmentSuitabilities = new Dictionary<Identifier, float>();
foreach (var itemPrefab in ItemPrefab.Prefabs)
{
float suitability = itemPrefab.GetTreatmentSuitability(Identifier) + itemPrefab.GetTreatmentSuitability(AfflictionType);
if (!MathUtils.NearlyEqual(suitability, 0.0f))
{
newTreatmentSuitabilities.TryAdd(itemPrefab.Identifier, suitability);
}
}
HasTreatments = newTreatmentSuitabilities.Any(kvp => kvp.Value > 0);
TreatmentSuitabilities = newTreatmentSuitabilities.ToImmutableDictionary();
}
public LocalizedString GetDescription(float strength, Description.TargetType targetType)
{
foreach (var description in Descriptions)
@@ -993,9 +1006,16 @@ namespace Barotrauma
return defaultDescription;
}
public static void LoadAllEffects()
/// <summary>
/// Should be called before each round: loads all StatusEffects and refreshes treatment suitabilities.
/// </summary>
public static void LoadAllEffectsAndTreatmentSuitabilities()
{
Prefabs.ForEach(p => p.LoadEffects());
foreach (var prefab in Prefabs)
{
prefab.RefreshTreatmentSuitabilities();
prefab.LoadEffects();
}
}
public static void ClearAllEffects()
@@ -1003,7 +1023,7 @@ namespace Barotrauma
Prefabs.ForEach(p => p.ClearEffects());
}
public void LoadEffects()
private void LoadEffects()
{
ClearEffects();
foreach (var subElement in configElement.Elements())
@@ -1032,7 +1052,7 @@ namespace Barotrauma
}
}
public void ClearEffects()
private void ClearEffects()
{
effects.Clear();
periodicEffects.Clear();
@@ -156,6 +156,12 @@ namespace Barotrauma
}
}
/// <summary>
/// How much vitality the character would have if it was alive?
/// E.g. a character killed by disconnection or with console commands may not have any vitality-reducing afflictions despite being dead
/// </summary>
public float VitalityDisregardingDeath => vitality;
public float HealthPercentage => MathUtils.Percentage(Vitality, MaxVitality);
public float MaxVitality
@@ -229,6 +235,8 @@ namespace Barotrauma
}
}
public bool IsParalyzed { get; private set; }
public float StunTimer { get; private set; }
/// <summary>
@@ -402,7 +410,17 @@ namespace Barotrauma
return strength;
}
public float GetAfflictionStrength(Identifier afflictionType, bool allowLimbAfflictions = true)
public float GetAfflictionStrengthByType(Identifier afflictionType, bool allowLimbAfflictions = true)
{
return GetAfflictionStrength(afflictionType, afflictionidentifier: Identifier.Empty, allowLimbAfflictions);
}
public float GetAfflictionStrengthByIdentifier(Identifier afflictionIdentifier, bool allowLimbAfflictions = true)
{
return GetAfflictionStrength(afflictionType: Identifier.Empty, afflictionIdentifier, allowLimbAfflictions);
}
public float GetAfflictionStrength(Identifier afflictionType, Identifier afflictionidentifier, bool allowLimbAfflictions = true)
{
float strength = 0.0f;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
@@ -410,7 +428,8 @@ namespace Barotrauma
if (!allowLimbAfflictions && kvp.Value != null) { continue; }
var affliction = kvp.Key;
if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; }
if (affliction.Prefab.AfflictionType == afflictionType)
if ((affliction.Prefab.AfflictionType == afflictionType || afflictionType.IsEmpty) &&
(affliction.Prefab.Identifier == afflictionidentifier || afflictionidentifier.IsEmpty))
{
strength += affliction.Strength;
}
@@ -714,7 +733,7 @@ namespace Barotrauma
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType)
{
if (Character.EmpVulnerability <= 0 || GetAfflictionStrength(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0)
if (Character.EmpVulnerability <= 0 || GetAfflictionStrengthByType(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0)
{
return;
}
@@ -953,6 +972,7 @@ namespace Barotrauma
public void CalculateVitality()
{
Vitality = MaxVitality;
IsParalyzed = false;
if (Unkillable || Character.GodMode) { return; }
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
@@ -966,6 +986,12 @@ namespace Barotrauma
}
Vitality -= vitalityDecrease;
affliction.CalculateDamagePerSecond(vitalityDecrease);
if (affliction.Strength >= affliction.Prefab.MaxStrength &&
affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
{
IsParalyzed = true;
}
}
#if CLIENT
if (IsUnconscious)
@@ -1136,7 +1162,7 @@ namespace Barotrauma
}
}
foreach (KeyValuePair<Identifier, float> treatment in affliction.Prefab.TreatmentSuitability)
foreach (KeyValuePair<Identifier, float> treatment in affliction.Prefab.TreatmentSuitabilities)
{
float suitability = treatment.Value * strength;
if (treatment.Value > strength)
@@ -134,7 +134,13 @@ namespace Barotrauma
foreach (XElement itemElement in spawnItems.GetChildElements("Item"))
{
InitializeJobItem(character, itemElement, spawnPoint);
}
}
if (GameMain.GameSession is { TraitorsEnabled: true } && character.IsSecurity)
{
var traitorGuidelineItem = ItemPrefab.Prefabs.Find(ip => ip.Tags.Contains(Tags.TraitorGuidelinesForSecurity));
Entity.Spawner.AddItemToSpawnQueue(traitorGuidelineItem, character.Inventory);
}
}
private void InitializeJobItem(Character character, XElement itemElement, WayPoint spawnPoint = null, Item parentItem = null)
@@ -144,7 +150,7 @@ namespace Barotrauma
{
string itemName = itemElement.Attribute("name").Value;
DebugConsole.ThrowError("Error in Job config (" + Name + ") - use item identifiers instead of names to configure the items.");
itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
itemPrefab = MapEntityPrefab.FindByName(itemName) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Tried to spawn \"" + Name + "\" with the item \"" + itemName + "\". Matching item prefab not found.");
@@ -591,7 +591,10 @@ namespace Barotrauma
public bool IsDead => character.IsDead;
public float Health => character.Health;
public float HealthPercentage => character.HealthPercentage;
public bool IsHuman => character.IsHuman;
public AIState AIState => character.AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle;
public bool IsFlipped => character.AnimController.IsFlipped;
public bool CanBeSeveredAlive
{
@@ -881,7 +884,9 @@ namespace Barotrauma
public void Update(float deltaTime)
{
UpdateProjSpecific(deltaTime);
ApplyStatusEffects(ActionType.Always, deltaTime);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
if (InWater)
{
body.ApplyWaterForces();
@@ -1223,6 +1228,7 @@ namespace Barotrauma
if (!statusEffects.TryGetValue(actionType, out var statusEffectList)) { return; }
foreach (StatusEffect statusEffect in statusEffectList)
{
statusEffect.sourceBody = body;
if (statusEffect.type == ActionType.OnDamaged)
{
if (!statusEffect.HasRequiredAfflictions(character.LastDamage)) { continue; }
@@ -1241,65 +1247,64 @@ namespace Barotrauma
statusEffect.AddNearbyTargets(WorldPosition, targets);
statusEffect.Apply(actionType, deltaTime, character, targets);
}
else
else if (statusEffect.targetLimbs != null)
{
if (statusEffect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory is { } inventory)
foreach (var limbType in statusEffect.targetLimbs)
{
foreach (Item item in inventory.AllItems)
if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
if (statusEffect.TargetIdentifiers != null &&
!statusEffect.TargetIdentifiers.Contains(item.Prefab.Identifier) &&
statusEffect.TargetIdentifiers.None(id => item.HasTag(id)))
// Target all matching limbs
foreach (var limb in ragdoll.Limbs)
{
continue;
}
if (statusEffect.TargetSlot > -1)
{
if (inventory.FindIndex(item) != statusEffect.TargetSlot) { continue; }
}
targets.Add(item);
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition);
}
else if (statusEffect.targetLimbs != null)
{
foreach (var limbType in statusEffect.targetLimbs)
{
if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
// Target all matching limbs
foreach (var limb in ragdoll.Limbs)
if (limb.IsSevered) { continue; }
if (limb.type == limbType)
{
if (limb.IsSevered) { continue; }
if (limb.type == limbType)
{
statusEffect.Apply(actionType, deltaTime, character, limb);
}
ApplyToLimb(actionType, deltaTime, statusEffect, character, limb);
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb) || statusEffect.HasTargetType(StatusEffect.TargetType.Character) || statusEffect.HasTargetType(StatusEffect.TargetType.This))
{
// Target just the first matching limb
Limb limb = ragdoll.GetLimb(limbType);
if (limb != null)
{
// Target just the first matching limb
Limb limb = ragdoll.GetLimb(limbType);
statusEffect.Apply(actionType, deltaTime, character, limb);
ApplyToLimb(actionType, deltaTime, statusEffect, character, limb);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb))
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb))
{
// Target just the last matching limb
Limb limb = ragdoll.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden);
if (limb != null)
{
// Target just the last matching limb
Limb limb = ragdoll.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden);
statusEffect.Apply(actionType, deltaTime, character, limb);
ApplyToLimb(actionType, deltaTime, statusEffect, character, limb);
}
}
}
else
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
// Target all limbs
foreach (var limb in ragdoll.Limbs)
{
statusEffect.Apply(actionType, deltaTime, character, this, WorldPosition);
if (limb.IsSevered) { continue; }
ApplyToLimb(actionType, deltaTime, statusEffect, character, limb);
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.This) || statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
{
ApplyToLimb(actionType, deltaTime, statusEffect, character, limb: this);
}
}
static void ApplyToLimb(ActionType actionType, float deltaTime, StatusEffect statusEffect, Character character, Limb limb)
{
statusEffect.sourceBody = limb.body;
statusEffect.Apply(actionType, deltaTime, entity: character, target: limb);
}
}
@@ -143,7 +143,14 @@ namespace Barotrauma
protected override string GetName() => "Character Config File";
public override ContentXElement MainElement => base.MainElement.IsOverride() ? base.MainElement.FirstElement() : base.MainElement;
public override ContentXElement MainElement
{
get
{
if (base.MainElement == null) { return null; }
return base.MainElement.IsOverride() ? base.MainElement.FirstElement() : base.MainElement;
}
}
public static XElement CreateVariantXml(XElement variantXML, XElement baseXML)
{
@@ -182,6 +189,11 @@ namespace Barotrauma
{
UpdatePath(File.Path);
doc = XMLExtensions.TryLoadXml(Path);
if (MainElement == null)
{
DebugConsole.ThrowError("Main element null! Failed to load character params.");
return false;
}
Identifier variantOf = MainElement.VariantOf();
if (!variantOf.IsEmpty)
{
@@ -230,6 +242,11 @@ namespace Barotrauma
protected void CreateSubParams()
{
if (MainElement == null)
{
DebugConsole.ThrowError("Main element null, cannot create sub params!");
return;
}
SubParams.Clear();
var healthElement = MainElement.GetChildElement("health");
if (healthElement != null)
@@ -745,7 +762,7 @@ namespace Barotrauma
{
if (!TryGetTarget(targetCharacter.SpeciesName, out target))
{
target = targets.FirstOrDefault(t => string.Equals(t.Tag, targetCharacter.Params.Group.ToString(), StringComparison.OrdinalIgnoreCase));
target = targets.FirstOrDefault(t => t.Tag == targetCharacter.Params.Group);
}
return target != null;
}
@@ -791,7 +808,7 @@ namespace Barotrauma
public override string Name => "Target";
[Serialize("", IsPropertySaveable.Yes, description: "Can be an item tag, species name or something else. Examples: decoy, provocative, light, dead, human, crawler, wall, nasonov, sonar, door, stronger, weaker, light, human, room..."), Editable()]
public string Tag { get; private set; }
public Identifier Tag { get; private set; }
[Serialize(AIState.Idle, IsPropertySaveable.Yes), Editable]
public AIState State { get; set; }
@@ -71,7 +71,7 @@ namespace Barotrauma
element ??= MainElement;
if (element == null)
{
DebugConsole.ThrowError("[EditableParams] The XML element is null!");
DebugConsole.ThrowError("[EditableParams] The XML element is null! Failed to save the parameters.");
return false;
}
SerializableProperty.SerializeProperties(this, element, true);
@@ -82,7 +82,16 @@ namespace Barotrauma
{
UpdatePath(file);
doc = XMLExtensions.TryLoadXml(Path);
if (doc == null) { return false; }
if (doc == null)
{
DebugConsole.ThrowError("[EditableParams] The document is null! Failed to load the parameters.");
return false;
}
if (MainElement == null)
{
DebugConsole.ThrowError("[EditableParams] The main element is null! Failed to load the parameters.");
return false;
}
IsLoaded = Deserialize(MainElement);
OriginalElement = new XElement(MainElement).FromPackage(MainElement.ContentPackage);
return IsLoaded;
@@ -315,20 +315,26 @@ namespace Barotrauma
protected void CreateColliders()
{
Colliders.Clear();
for (int i = 0; i < MainElement.GetChildElements("collider").Count(); i++)
if (MainElement?.GetChildElements("collider") is { } colliderElements)
{
var element = MainElement.GetChildElements("collider").ElementAt(i);
string name = i > 0 ? "Secondary Collider" : "Main Collider";
Colliders.Add(new ColliderParams(element, this, name));
for (int i = 0; i < colliderElements.Count(); i++)
{
var element = colliderElements.ElementAt(i);
string name = i > 0 ? "Secondary Collider" : "Main Collider";
Colliders.Add(new ColliderParams(element, this, name));
}
}
}
protected void CreateLimbs()
{
Limbs.Clear();
foreach (var element in MainElement.GetChildElements("limb"))
if (MainElement?.GetChildElements("limb") is { } childElements)
{
Limbs.Add(new LimbParams(element, this));
foreach (var element in childElements)
{
Limbs.Add(new LimbParams(element, this));
}
}
Limbs = Limbs.OrderBy(l => l.ID).ToList();
}
@@ -430,8 +436,8 @@ namespace Barotrauma
copy.Serialize();
Memento.Store(copy);
}
public void Undo() => RevertTo(Memento.Undo() as RagdollParams);
public void Redo() => RevertTo(Memento.Redo() as RagdollParams);
public void Undo() => RevertTo(Memento.Undo());
public void Redo() => RevertTo(Memento.Redo());
public void ClearHistory() => Memento.Clear();
private void RevertTo(RagdollParams source)
@@ -22,13 +22,13 @@ namespace Barotrauma.Abilities
private static readonly List<WeaponType> WeaponTypeValues = Enum.GetValues(typeof(WeaponType)).Cast<WeaponType>().ToList();
private readonly string itemIdentifier;
private readonly string[] tags;
private readonly Identifier[] tags;
private readonly WeaponType weapontype;
private readonly bool ignoreNonHarmfulAttacks;
public AbilityConditionAttackData(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
itemIdentifier = conditionElement.GetAttributeString("itemidentifier", string.Empty);
tags = conditionElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
tags = conditionElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>());
ignoreNonHarmfulAttacks = conditionElement.GetAttributeBool("ignorenonharmfulattacks", false);
string weaponTypeStr = conditionElement.GetAttributeString("weapontype", "Any");
@@ -41,10 +41,10 @@ namespace Barotrauma.Abilities
if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; }
foreach (var (factionIdentifier, amount) in mission.ReputationRewards)
foreach (var reputationReward in mission.ReputationRewards)
{
if (amount <= 0) { continue; }
if (GetMatchingFaction(factionIdentifier) is { } faction &&
if (reputationReward.Amount <= 0) { continue; }
if (GetMatchingFaction(reputationReward.FactionIdentifier) is { } faction &&
Faction.GetPlayerAffiliationStatus(faction) is FactionAffiliation.Positive)
{
return CheckMissionType();
@@ -0,0 +1,22 @@
namespace Barotrauma.Abilities
{
class AbilityConditionAllyHasTalent : AbilityConditionDataless
{
private readonly Identifier talentIdentifier;
public AbilityConditionAllyHasTalent(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
talentIdentifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
}
protected override bool MatchesConditionSpecific()
{
foreach (Character crewCharacter in Character.GetFriendlyCrew(characterTalent.Character))
{
if (crewCharacter.HasTalent(talentIdentifier)) { return true; }
}
return false;
}
}
}
@@ -5,12 +5,12 @@ namespace Barotrauma.Abilities
{
class AbilityConditionHasItem : AbilityConditionDataless
{
private readonly string[] tags;
private readonly Identifier[] tags;
readonly bool requireAll;
public AbilityConditionHasItem(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
tags = conditionElement.GetAttributeStringArray("tags", Array.Empty<string>());
tags = conditionElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>());
requireAll = conditionElement.GetAttributeBool("requireall", false);
}
@@ -23,7 +23,7 @@ namespace Barotrauma.Abilities
if (requireAll)
{
foreach (string tag in tags)
foreach (Identifier tag in tags)
{
if (character.GetEquippedItem(tag) == null) { return false; }
}
@@ -31,7 +31,7 @@ namespace Barotrauma.Abilities
}
else
{
foreach (string tag in tags)
foreach (Identifier tag in tags)
{
if (character.GetEquippedItem(tag) != null) { return true; }
}
@@ -5,13 +5,13 @@ namespace Barotrauma.Abilities
{
class AbilityConditionHasStatusTag : AbilityConditionDataless
{
private readonly string tag;
private readonly Identifier tag;
public AbilityConditionHasStatusTag(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
tag = conditionElement.GetAttributeString("tag", "");
if (string.IsNullOrEmpty(tag))
tag = conditionElement.GetAttributeIdentifier("tag", Identifier.Empty);
if (tag.IsEmpty)
{
DebugConsole.AddWarning($"Error in talent \"{characterTalent.Prefab.OriginalName}\" - tag not defined in AbilityConditionHasStatusTag.");
}
@@ -19,7 +19,7 @@ namespace Barotrauma.Abilities
protected override bool MatchesConditionSpecific()
{
if (!string.IsNullOrEmpty(tag))
if (!tag.IsEmpty)
{
return
StatusEffect.DurationList.Any(d => d.Targets.Contains(character) && d.Parent.HasTag(tag)) ||
@@ -12,8 +12,7 @@ namespace Barotrauma.Abilities
protected override bool MatchesConditionSpecific()
{
bool result = character.HasTalent(talentIdentifier);
return result;
return character.HasTalent(talentIdentifier);
}
}
}
@@ -27,8 +27,8 @@ internal sealed class AbilityConditionHoldingItem : AbilityConditionDataless
return false;
static bool HasItemInHand(Character character, Identifier? tagOrIdentifier) =>
character.GetEquippedItem(tagOrIdentifier?.Value, InvSlotType.RightHand) is not null ||
character.GetEquippedItem(tagOrIdentifier?.Value, InvSlotType.LeftHand) is not null;
character.GetEquippedItem(tagOrIdentifier, InvSlotType.RightHand) is not null ||
character.GetEquippedItem(tagOrIdentifier, InvSlotType.LeftHand) is not null;
}
}
@@ -37,10 +37,11 @@ namespace Barotrauma.Abilities
protected override void ApplyEffect()
{
if (Character?.Submarine is null) { return; }
if (Character is null) { return; }
foreach (Item item in Character.Submarine.GetItems(true))
foreach (Item item in Item.ItemList)
{
if (item.Submarine?.TeamID != Character.TeamID) { continue; }
if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier))
{
item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent);
@@ -66,12 +66,12 @@
{
foreach (Character c in Character.GetFriendlyCrew(Character))
{
c?.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
c?.Info?.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
}
}
else
{
Character?.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
Character?.Info?.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
}
}
@@ -18,7 +18,7 @@ namespace Barotrauma.Abilities
protected override void ApplyEffect()
{
IEnumerable<Character> enemyCharacters = Character.CharacterList.Where(c => c.TeamID == CharacterTeamType.None);
IEnumerable<Character> enemyCharacters = Character.CharacterList.Where(c => !Character.IsFriendly(c));
int timesGiven = 0;
foreach (Character enemyCharacter in enemyCharacters)
@@ -27,7 +27,6 @@ namespace Barotrauma.Abilities
if (enemyCharacter.Submarine == null || enemyCharacter.Submarine != Submarine.MainSub) { continue; }
if (enemyCharacter.IsDead) { continue; }
if (!enemyCharacter.LockHands) { continue; }
if (timesGiven > max) { continue; }
Character.GiveMoney(moneyAmount);
GameAnalyticsManager.AddMoneyGainedEvent(moneyAmount, GameAnalyticsManager.MoneySource.Ability, CharacterTalent.Prefab.Identifier.Value);
foreach (Character character in Character.GetFriendlyCrew(Character))
@@ -35,6 +34,7 @@ namespace Barotrauma.Abilities
character.Info?.GiveExperience(experienceAmount);
}
timesGiven++;
if (max > 0 && timesGiven >= max) { break; }
}
}
@@ -5,32 +5,43 @@ namespace Barotrauma.Abilities
{
class CharacterAbilityRegenerateLoot : CharacterAbility
{
/// <summary>
/// Chance for the loot to be regenerated. We can't use <see cref="AbilityConditionServerRandom"/> for this,
/// because it'd allow the player to reopen the container until the ability is executed successfully
/// </summary>
private readonly float randomChance;
// separate random chance used for the ability itself to prevent the player
// from opening/reopening a container until it spawns loot
private readonly float randomChance;
/// <summary>
/// Chance for an individual loot item to be generated.
/// </summary>
private readonly float randomChancePerItem = 1.0f;
// not maintained through death, so it's possible for players to respawn and re-loot chests
// seems like a minor issue for now
private readonly List<Item> openedContainers = new List<Item>();
private readonly HashSet<Item> openedContainers = new HashSet<Item>();
public CharacterAbilityRegenerateLoot(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
randomChance = abilityElement.GetAttributeFloat("randomchance", 1f);
randomChance = abilityElement.GetAttributeFloat(nameof(randomChance), 1f);
randomChancePerItem = abilityElement.GetAttributeFloat(nameof(randomChancePerItem), 1f);
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityItem)?.Item is Item item)
{
if (openedContainers.Contains(item)) { return; }
openedContainers.Add(item);
if (randomChance < Rand.Range(0f, 1f, Rand.RandSync.Unsynced)) { return; }
if ((abilityObject as IAbilityItem)?.Item is not Item item) { return; }
if (openedContainers.Contains(item)) { return; }
if (item.GetComponent<ItemContainer>() is ItemContainer itemContainer)
{
AutoItemPlacer.RegenerateLoot(item.Submarine, itemContainer);
}
openedContainers.Add(item);
if (randomChance < Rand.Range(0f, 1f, Rand.RandSync.Unsynced)) { return; }
if (item.GetComponent<ItemContainer>() is ItemContainer itemContainer)
{
AutoItemPlacer.RegenerateLoot(item.Submarine, itemContainer, skipItemProbability: 1.0f - randomChancePerItem);
}
}
}
}
@@ -5,10 +5,10 @@ namespace Barotrauma.Abilities
class CharacterAbilityTandemFire : CharacterAbilityApplyStatusEffectsToNearestAlly
{
// this should just be its own class, misleading to inherit here
private readonly string tag;
private readonly Identifier tag;
public CharacterAbilityTandemFire(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
tag = abilityElement.GetAttributeString("tag", "");
tag = abilityElement.GetAttributeIdentifier("tag", Identifier.Empty);
}
protected override void ApplyEffect()
@@ -37,7 +37,7 @@ namespace Barotrauma.Abilities
ApplyEffectSpecific(closestCharacter);
}
static bool SelectedItemHasTag(Character character, string tag) =>
static bool SelectedItemHasTag(Character character, Identifier tag) =>
(character.SelectedItem != null && character.SelectedItem.HasTag(tag)) ||
(character.SelectedSecondaryItem != null && character.SelectedSecondaryItem.HasTag(tag));
}
@@ -133,6 +133,14 @@ namespace Barotrauma
if (IsTalentLocked(talentIdentifier)) { return false; }
if (character.Info.GetUnlockedTalentsInTree().Contains(talentIdentifier))
{
//if the character already has the talent, it must be viable?
//needed for backwards compatibility, otherwise if we remove e.g. a tier 1 or tier 2 talent,
//all the already-unlocked higher-tier talents will be considered invalid which'll break the talent selection
return true;
}
foreach (var subTree in talentTree!.TalentSubTrees)
{
if (subTree.AllTalentIdentifiers.Contains(talentIdentifier) && subTree.HasMaxTalents(selectedTalents)) { return false; }
@@ -143,11 +151,12 @@ namespace Barotrauma
{
return !talentOptionStage.HasMaxTalents(selectedTalents) && TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents);
}
//if a previous stage hasn't been completed, this talent can't be selected yet
bool optionStageCompleted = talentOptionStage.HasEnoughTalents(selectedTalents);
if (!optionStageCompleted)
{
break;
}
}
}
}