Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop

This commit is contained in:
EvilFactory
2023-10-19 12:18:30 -03:00
613 changed files with 22122 additions and 11481 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
@@ -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)
@@ -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 != null && !(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)
@@ -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()
@@ -810,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;
@@ -163,7 +163,8 @@ namespace Barotrauma
}
var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
if (order == null) { continue; }
if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) && Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) &&
Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC && !character.IsFriendlyNPCTurnedHostile)
{
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
{
@@ -539,7 +540,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
{
@@ -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,13 +276,13 @@ 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"); }
@@ -294,9 +295,9 @@ namespace Barotrauma
{
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,7 +108,7 @@ 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)
@@ -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++)
@@ -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);
@@ -745,6 +745,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)
@@ -754,12 +761,14 @@ 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);
var should = GameMain.LuaCs.Hook.Call<float?>("changeFallDamage", impactDamage, character, impactPos, velocity);
@@ -770,7 +779,7 @@ namespace Barotrauma
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,
@@ -784,6 +793,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)
@@ -1031,7 +1046,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()
@@ -1331,6 +1351,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);
}
}
}
}
@@ -1789,12 +1814,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)
{
@@ -1831,7 +1850,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;
@@ -1859,6 +1878,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);
}
@@ -68,6 +68,13 @@ namespace Barotrauma
}
UpdateLimbLightSource(limb);
}
foreach (var item in HeldItems)
{
if (item.body != null)
{
item.body.Enabled = enabled;
}
}
AnimController.Collider.Enabled = value;
}
}
@@ -338,7 +345,7 @@ namespace Barotrauma
public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator;
public CombatAction CombatAction;
public AnimController AnimController;
public readonly AnimController AnimController;
private Vector2 cursorPosition;
@@ -389,6 +396,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;
@@ -784,7 +793,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;
}
}
@@ -795,7 +804,7 @@ namespace Barotrauma
public bool IsArrested
{
get { return IsHuman && HasEquippedItem("handlocker"); }
get { return IsHuman && HasEquippedItem(Tags.HandLockerItem); }
}
public bool IsPet
@@ -855,6 +864,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
{
@@ -868,7 +878,7 @@ namespace Barotrauma
public float Bleeding
{
get { return CharacterHealth.GetAfflictionStrength(AfflictionPrefab.BleedingType, true); }
get { return CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.BleedingType, true); }
}
private bool speechImpedimentSet;
@@ -939,6 +949,8 @@ namespace Barotrauma
{
GameMain.GameSession?.CrewManager?.AutoHideCrewList();
}
_selectedItem?.GetComponent<CircuitBox>()?.OnViewUpdateProjSpecific();
}
#endif
if (prevSelectedItem != null && (_selectedItem == null || _selectedItem != prevSelectedItem) && itemSelectedTime > 0)
@@ -1076,8 +1088,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;
@@ -1314,9 +1335,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)
{
@@ -1490,7 +1511,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)
@@ -1671,17 +1692,31 @@ namespace Barotrauma
GameMain.LuaCs.Hook.Call("character.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));
@@ -1990,14 +2025,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)
{
@@ -2194,14 +2236,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))
@@ -2221,7 +2263,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();
}
@@ -2254,26 +2296,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)
{
@@ -2289,42 +2363,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);
@@ -2377,9 +2448,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; }
@@ -2401,7 +2469,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++)
@@ -2416,7 +2484,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;
}
@@ -2544,7 +2615,7 @@ namespace Barotrauma
}
}
return !checkVisibility || CanSeeCharacter(c);
return !checkVisibility || CanSeeTarget(c);
}
public bool CanInteractWith(Item item, bool checkLinked = true)
@@ -2594,7 +2665,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))
{
@@ -2682,10 +2756,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)
@@ -2710,7 +2792,7 @@ namespace Barotrauma
this.onCustomInteract = onCustomInteract;
CustomInteractHUDText = hudText;
}
public void SelectCharacter(Character character)
{
if (character == null || character == this) { return; }
@@ -2761,7 +2843,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))
{
@@ -2874,7 +2956,7 @@ namespace Barotrauma
}
#endif
}
else
else if (!IsClimbing)
{
#if CLIENT
if (Controlled == this)
@@ -2922,9 +3004,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)
{
@@ -3126,6 +3208,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;
@@ -3195,47 +3285,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);
@@ -3438,7 +3529,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; }
@@ -3449,7 +3540,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; }
@@ -3483,6 +3574,11 @@ namespace Barotrauma
despawnTimer += deltaTime * despawnPriority;
if (despawnTimer < GameSettings.CurrentConfig.CorpseDespawnDelay) { return; }
Despawn();
}
private void Despawn(bool createNetworkEvents = true)
{
Identifier despawnContainerId =
IsHuman ?
"despawncontainer".ToIdentifier() :
@@ -3533,8 +3629,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++)
{
@@ -4278,7 +4373,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;
}
@@ -4342,8 +4437,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);
}
}
}
@@ -4353,8 +4447,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))
@@ -4363,12 +4456,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);
@@ -4392,6 +4493,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)
@@ -4452,7 +4559,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;
@@ -4553,20 +4660,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)
{
@@ -4577,20 +4685,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;
@@ -4614,10 +4723,7 @@ namespace Barotrauma
limb.IsSevered = false;
}
if (GameMain.GameSession != null)
{
GameMain.GameSession.ReviveCharacter(this);
}
GameMain.GameSession?.ReviveCharacter(this);
}
public override void Remove()
@@ -4732,7 +4838,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())
@@ -4757,21 +4863,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)
{
@@ -4853,6 +4944,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())
@@ -4941,41 +5039,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");
@@ -4986,6 +5050,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;
@@ -5072,6 +5138,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));
}
@@ -1527,7 +1527,10 @@ namespace Barotrauma
break;
}
}
targetAvailableInNextLevel = !isOutside && GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null && (isOnConnectedLinkedSub || entitySub == Submarine.MainSub);
targetAvailableInNextLevel =
!isOutside &&
GameMain.GameSession?.Campaign is not { SwitchedSubsThisRound: true } &&
(isOnConnectedLinkedSub || entitySub == Submarine.MainSub);
if (!targetAvailableInNextLevel)
{
if (!order.Prefab.CanBeGeneralized)
@@ -1652,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)
{
@@ -1717,6 +1724,7 @@ namespace Barotrauma
{
targetId = GetOffsetId(parentSub, targetId);
targetEntity = Entity.FindEntityByID(targetId);
return targetEntity != null;
}
else
{
@@ -1730,8 +1738,8 @@ namespace Barotrauma
{
DebugConsole.AddWarning($"Trying to load a previously saved order ({orderIdentifier}). Can't find the parent sub of the target entity. The order doesn't require a target so a more generic version of the order will be loaded instead.");
}
return true;
}
return true;
}
}
return orders;
@@ -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);
@@ -603,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"];
@@ -613,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"];
@@ -161,6 +161,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
@@ -234,6 +240,8 @@ namespace Barotrauma
}
}
public bool IsParalyzed { get; private set; }
public float StunTimer { get; private set; }
/// <summary>
@@ -407,7 +415,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)
@@ -415,7 +433,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;
}
@@ -725,7 +744,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;
}
@@ -969,6 +988,7 @@ namespace Barotrauma
public void CalculateVitality()
{
Vitality = MaxVitality;
IsParalyzed = false;
if (Unkillable || Character.GodMode) { return; }
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
@@ -982,6 +1002,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)
@@ -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");
@@ -19,15 +19,9 @@ namespace Barotrauma.Abilities
foreach (XElement subElement in conditionElement.Elements())
{
if (subElement.Name.ToString().Equals("conditional", StringComparison.OrdinalIgnoreCase))
if (subElement.NameAsIdentifier() == "conditional")
{
foreach (XAttribute attribute in subElement.Attributes())
{
if (PropertyConditional.IsValid(attribute))
{
conditionals.Add(new PropertyConditional(attribute));
}
}
conditionals.AddRange(PropertyConditional.FromXElement(subElement));
}
}
@@ -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;
}
}
}
}
@@ -0,0 +1,84 @@
#nullable enable
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
internal partial class CircuitBoxComponent : CircuitBoxNode, ICircuitBoxIdentifiable
{
public readonly Item Item;
public ushort ID { get; }
public readonly ItemPrefab UsedResource;
public CircuitBoxComponent(ushort id, Item item, Vector2 position, CircuitBox circuitBox, ItemPrefab usedResource): base(circuitBox)
{
if (item.Connections is null)
{
throw new ArgumentNullException(nameof(item.Connections), $"Tried to load a CircuitBoxNode with an item \"{item.Prefab.Name}\" that has no connections.");
}
var conns = item.Connections.Select(connection => new CircuitBoxNodeConnection(Vector2.Zero, this, connection, circuitBox)).ToList();
Vector2 size = CalculateSize(conns);
ID = id;
Item = item;
Size = size;
Connectors = conns.Cast<CircuitBoxConnection>().ToImmutableArray();
Position = position;
UsedResource = usedResource;
UpdatePositions();
}
public static Option<CircuitBoxComponent> TryLoadFromXML(ContentXElement element, CircuitBox circuitBox)
{
ushort id = element.GetAttributeUInt16("id", ICircuitBoxIdentifiable.NullComponentID);
Vector2 position = element.GetAttributeVector2("position", Vector2.Zero);
var itemIdOption = ItemSlotIndexPair.TryDeserializeFromXML(element, "backingitemid");
Identifier usedResourceIdentifier = element.GetAttributeIdentifier("usedresource", Identifier.Empty);
if (!itemIdOption.TryUnwrap(out var itemId) || itemId.FindItemInContainer(circuitBox.ComponentContainer) is not { } backingItem)
{
DebugConsole.ThrowErrorAndLogToGA("CircuitBoxComponent.TryLoadFromXML:IdNotFound",
$"Failed to find item with ID {itemId} for CircuitBoxNode with ID {id}");
return Option.None;
}
if (!ItemPrefab.Prefabs.TryGet(usedResourceIdentifier, out var usedResource))
{
DebugConsole.ThrowErrorAndLogToGA("CircuitBoxComponent.TryLoadXML:UsedResourceNotFound",
$"Failed to find item prefab with identifier {usedResourceIdentifier} for CircuitBoxNode with ID {id}");
return Option.None;
}
return Option.Some(new CircuitBoxComponent(id, backingItem, position, circuitBox, usedResource));
}
public XElement Save()
{
return new XElement("Component",
new XAttribute("id", ID),
new XAttribute("position", XMLExtensions.Vector2ToString(Position)),
new XAttribute("backingitemid", ItemSlotIndexPair.Serialize(Item)),
new XAttribute("usedresource", UsedResource.Identifier));
}
public void Remove()
{
if (Entity.Spawner is { } spawner && Screen.Selected is not { IsEditor: true })
{
spawner.AddEntityToRemoveQueue(Item);
return;
}
// if EntitySpawner is not available
Item.Remove();
}
}
}
@@ -0,0 +1,121 @@
#nullable enable
using System.Collections.Generic;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
internal class CircuitBoxInputConnection : CircuitBoxConnection
{
/// <summary>
/// Circuit box input connection is classified as an output because it behaves like an output inside the circuit box.
/// As in you can connect it to a input pin of a component.
/// </summary>
public override bool IsOutput => true;
public readonly List<CircuitBoxConnection> ExternallyConnectedTo = new();
public CircuitBoxInputConnection(Vector2 position, Connection connection, CircuitBox box) : base(position, connection, box) { }
public override void ReceiveSignal(Signal signal)
{
foreach (CircuitBoxConnection connector in ExternallyConnectedTo)
{
if (connector is CircuitBoxOutputConnection output)
{
output.ReceiveSignal(signal);
continue;
}
Connection.SendSignalIntoConnection(signal, connector.Connection);
}
}
}
internal class CircuitBoxOutputConnection : CircuitBoxConnection
{
/// <summary>
/// Circuit box output connection is classified as an input because it behaves like an input inside the circuit box.
/// As in you can connect it to a output pin of a component.
/// </summary>
public override bool IsOutput => false;
public CircuitBoxOutputConnection(Vector2 position, Connection connection, CircuitBox circuitBox) : base(position, connection, circuitBox) { }
public override void ReceiveSignal(Signal signal) => CircuitBox.Item.SendSignal(signal, Connection);
}
internal class CircuitBoxNodeConnection : CircuitBoxConnection
{
public override bool IsOutput => Connection.IsOutput;
public CircuitBoxComponent Component;
public bool HasAvailableSlots => Connection.WireSlotsAvailable();
public CircuitBoxNodeConnection(Vector2 position, CircuitBoxComponent component, Connection connection, CircuitBox circuitBox) : base(position, connection, circuitBox)
{
Component = component;
}
public override void ReceiveSignal(Signal signal) => Connection.SendSignalIntoConnection(signal, Connection);
}
internal abstract partial class CircuitBoxConnection
{
public readonly Connection Connection;
public abstract bool IsOutput { get; }
public RectangleF Rect;
private Vector2 position;
public List<CircuitBoxConnection> ExternallyConnectedFrom = new();
public static float Size = CircuitBoxSizes.ConnectorSize;
public Vector2 Position
{
get => position;
set
{
Rect.X = value.X - Rect.Width / 2f;
Rect.Y = value.Y - Rect.Height / 2f;
position = value;
}
}
public float Length { get; private set; }
public Vector2 AnchorPoint
=> new Vector2(IsOutput ? Rect.Right + CircuitBoxSizes.AnchorOffset : Rect.Left - CircuitBoxSizes.AnchorOffset, Rect.Center.Y);
public readonly CircuitBox CircuitBox;
protected CircuitBoxConnection(Vector2 position, Connection connection, CircuitBox circuitBox)
{
Connection = connection;
Rect.Width = Rect.Height = Size;
Position = position;
CircuitBox = circuitBox;
InitProjSpecific(circuitBox);
}
private partial void InitProjSpecific(CircuitBox circuitBox);
public abstract void ReceiveSignal(Signal signal);
public bool Contains(Vector2 pos)
{
float x = Rect.X,
y = -(Rect.Y + Rect.Height),
width = Rect.Width,
height = Rect.Height;
RectangleF rect = new(x, y, width, height);
return rect.Contains(pos);
}
}
}
@@ -0,0 +1,109 @@
#nullable enable
using System;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
[NetworkSerialize]
internal readonly record struct NetCircuitBoxCursorInfo(Vector2[] RecordedPositions, Option<Vector2> DragStart, Option<Identifier> HeldItem, ushort CharacterID = 0) : INetSerializableStruct;
internal sealed class CircuitBoxCursor
{
public NetCircuitBoxCursorInfo Info;
public CircuitBoxCursor(NetCircuitBoxCursorInfo info)
{
if (Entity.FindEntityByID(info.CharacterID) is Character c)
{
Color = GenerateColor(c.Name);
}
UpdateInfo(info);
}
public void UpdateInfo(NetCircuitBoxCursorInfo newInfo)
{
Info = newInfo;
newInfo.HeldItem.Match(
some: newIdentifier =>
HeldPrefab.Match(
some: oldPrefab =>
{
if (oldPrefab.Identifier == newIdentifier) { return; }
SetHeldPrefab(newIdentifier);
},
none: () => SetHeldPrefab(newIdentifier)
),
none: () => HeldPrefab = Option.None);
prevPosition = DrawPosition;
void SetHeldPrefab(Identifier identifier)
{
ItemPrefab? prefab = ItemPrefab.Prefabs.Find(prefab => prefab.Identifier.Equals(identifier));
HeldPrefab = prefab is null ? Option.None : Option.Some(prefab);
}
}
public Option<ItemPrefab> HeldPrefab { get; private set; } = Option.None;
public Color Color = Color.White;
public static Color GenerateColor(string name)
{
Random random = new Random(ToolBox.StringToInt(name));
return ToolBox.HSVToRGB(random.NextSingle() * 360f, 1f, 1f);
}
private const float UpdateTimeout = 5f;
private float updateTimer;
private float positionTimer;
private Vector2 prevPosition;
public Vector2 DrawPosition;
public bool IsActive => updateTimer < UpdateTimeout;
public void Update(float deltaTime)
{
updateTimer += deltaTime;
Vector2 finalPosition = Info.RecordedPositions[^1];
if (positionTimer > 1f)
{
DrawPosition = finalPosition;
prevPosition = Vector2.Zero;
}
else
{
positionTimer += deltaTime;
float stepTimer = positionTimer * 10f;
int targetPositonIndex = (int)MathF.Floor(stepTimer);
int prevPosIndex = targetPositonIndex - 1;
Vector2 targetPosition = IsInRange(targetPositonIndex, Info.RecordedPositions.Length)
? Info.RecordedPositions[targetPositonIndex]
: finalPosition;
Vector2 prevTargetPosition = IsInRange(prevPosIndex, Info.RecordedPositions.Length)
? Info.RecordedPositions[prevPosIndex]
: prevPosition;
DrawPosition = Vector2.Lerp(prevTargetPosition, targetPosition, MathHelper.Clamp(stepTimer % 1f, 0f, 1f));
}
static bool IsInRange(int index, int length)
=> index >= 0 && index < length;
}
public void ResetTimers()
{
positionTimer = 0f;
updateTimer = 0f;
}
}
}
@@ -0,0 +1,38 @@
#nullable enable
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Xml.Linq;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
internal sealed class CircuitBoxInputOutputNode : CircuitBoxNode
{
public enum Type
{
Invalid,
Input,
Output
}
public Type NodeType;
public CircuitBoxInputOutputNode(IReadOnlyList<CircuitBoxConnection> conns, Vector2 initialPosition, Type type, CircuitBox circuitBox): base(circuitBox)
{
Size = CalculateSize(conns);
Connectors = conns.ToImmutableArray();
Position = initialPosition;
NodeType = type;
UpdatePositions();
}
public XElement Save() => new XElement($"{NodeType}Node", new XAttribute("pos", XMLExtensions.Vector2ToString(Position)));
public void Load(ContentXElement element)
{
Position = element.GetAttributeVector2("pos", Vector2.Zero);
}
}
}
@@ -0,0 +1,163 @@
#nullable enable
using System;
using System.Collections.Immutable;
using System.Xml.Linq;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
public enum CircuitBoxOpcode
{
Error,
Cursor,
AddComponent,
MoveComponent,
AddWire,
RemoveWire,
SelectComponents,
SelectWires,
UpdateSelection,
DeleteComponent,
ServerInitialize
}
[NetworkSerialize]
internal readonly record struct NetCircuitBoxHeader(CircuitBoxOpcode Opcode, ushort ItemID, byte ComponentIndex) : INetSerializableStruct
{
public Option<CircuitBox> FindTarget() => CircuitBox.FindCircuitBox(ItemID, ComponentIndex);
}
[NetworkSerialize]
internal readonly record struct CircuitBoxConnectorIdentifier(Identifier SignalConnection, Option<ushort> TargetId) : INetSerializableStruct
{
public static CircuitBoxConnectorIdentifier FromConnection(CircuitBoxConnection connection) =>
connection switch
{
(CircuitBoxInputConnection or CircuitBoxOutputConnection)
=> new CircuitBoxConnectorIdentifier(connection.Name.ToIdentifier(), Option.None),
CircuitBoxNodeConnection nodeConnection
=> new CircuitBoxConnectorIdentifier(connection.Name.ToIdentifier(), Option.Some(nodeConnection.Component.ID)),
_ => throw new ArgumentOutOfRangeException(nameof(connection))
};
public Option<CircuitBoxConnection> FindConnection(CircuitBox circuitBox)
{
if (!TargetId.TryUnwrap(out var id))
{
return circuitBox.FindInputOutputConnection(SignalConnection);
}
foreach (CircuitBoxComponent boxNode in circuitBox.Components)
{
if (boxNode.ID != id) { continue; }
foreach (var conn in boxNode.Connectors)
{
if (conn.Name != SignalConnection) { continue; }
return Option.Some(conn);
}
}
return Option.None;
}
public XElement Save(string name) => new XElement(name,
new XAttribute("name", SignalConnection),
new XAttribute("target", TargetId.TryUnwrap(out var value) ? value.ToString() : string.Empty));
public static CircuitBoxConnectorIdentifier Load(ContentXElement element)
{
string? name = element.GetAttributeString("name", string.Empty);
string? target = element.GetAttributeString("target", string.Empty);
Option<ushort> targetId = Option.None;
if (!string.IsNullOrWhiteSpace(target))
{
targetId = ushort.TryParse(target, out var value) ? Option.Some(value) : Option.None;
}
return new CircuitBoxConnectorIdentifier(name.ToIdentifier(), targetId);
}
public override string ToString()
=> $"{{Name: {SignalConnection}, ID: {(TargetId.TryUnwrap(out var value) ? value.ToString() : "N/A")}}}";
}
[NetworkSerialize]
internal readonly record struct CircuitBoxAddComponentEvent(UInt32 PrefabIdentifier, Vector2 Position) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxServerCreateComponentEvent(ushort BackingItemId, UInt32 UsedResource, ushort ComponentId, Vector2 Position) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxRemoveComponentEvent(ImmutableArray<ushort> TargetIDs) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxMoveComponentEvent(ImmutableArray<ushort> TargetIDs, ImmutableArray<CircuitBoxInputOutputNode.Type> IOs, Vector2 MoveAmount) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxSelectNodesEvent(ImmutableArray<ushort> TargetIDs, ImmutableArray<CircuitBoxInputOutputNode.Type> IOs, bool Overwrite, ushort CharacterID) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxServerUpdateSelection(ImmutableArray<CircuitBoxIdSelectionPair> ComponentIds, ImmutableArray<CircuitBoxIdSelectionPair> WireIds, ImmutableArray<CircuitBoxTypeSelectionPair> InputOutputs) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxIdSelectionPair(ushort ID, Option<ushort> SelectedBy) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxTypeSelectionPair(CircuitBoxInputOutputNode.Type Type, Option<ushort> SelectedBy) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxSelectWiresEvent(ImmutableArray<ushort> TargetIDs, bool Overwrite, ushort CharacterID) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxClientAddWireEvent(Color Color, CircuitBoxConnectorIdentifier Start, CircuitBoxConnectorIdentifier End, UInt32 SelectedWirePrefabIdentifier) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxServerCreateWireEvent(CircuitBoxClientAddWireEvent Request, ushort WireId, Option<ushort> BackingItemId) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxRemoveWireEvent(ImmutableArray<ushort> TargetIDs) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxErrorEvent(string Message) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxInitializeStateFromServerEvent(
ImmutableArray<CircuitBoxServerCreateComponentEvent> Components,
ImmutableArray<CircuitBoxServerCreateWireEvent> Wires,
Vector2 InputPos,
Vector2 OutputPos) : INetSerializableStruct;
internal readonly record struct CircuitBoxEventData(INetSerializableStruct Data) : ItemComponent.IEventData
{
public CircuitBoxOpcode Opcode =>
Data switch
{
(CircuitBoxAddComponentEvent or CircuitBoxServerCreateComponentEvent)
=> CircuitBoxOpcode.AddComponent,
CircuitBoxRemoveComponentEvent
=> CircuitBoxOpcode.DeleteComponent,
CircuitBoxMoveComponentEvent
=> CircuitBoxOpcode.MoveComponent,
CircuitBoxSelectNodesEvent
=> CircuitBoxOpcode.SelectComponents,
CircuitBoxSelectWiresEvent
=> CircuitBoxOpcode.SelectWires,
CircuitBoxServerUpdateSelection
=> CircuitBoxOpcode.UpdateSelection,
(CircuitBoxClientAddWireEvent or CircuitBoxServerCreateWireEvent)
=> CircuitBoxOpcode.AddWire,
CircuitBoxRemoveWireEvent
=> CircuitBoxOpcode.RemoveWire,
CircuitBoxInitializeStateFromServerEvent
=> CircuitBoxOpcode.ServerInitialize,
_ => throw new ArgumentOutOfRangeException(nameof(Data))
};
}
}
@@ -0,0 +1,132 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
internal partial class CircuitBoxNode : CircuitBoxSelectable
{
public Vector2 Size;
public RectangleF Rect;
private Vector2 position;
public Vector2 Position
{
get => position;
set
{
const float clampSize = CircuitBoxSizes.PlayableAreaSize / 2f;
position = new Vector2(Math.Clamp(value.X, -clampSize, clampSize),
Math.Clamp(value.Y, -clampSize, clampSize));
UpdatePositions();
}
}
public ImmutableArray<CircuitBoxConnection> Connectors;
public static float Opacity = 0.8f;
public readonly CircuitBox CircuitBox;
public CircuitBoxNode(CircuitBox circuitBox)
{
CircuitBox = circuitBox;
}
public static Vector2 CalculateSize(IReadOnlyList<CircuitBoxConnection> conns)
{
Vector2 leftSize = Vector2.Zero,
rightSize = Vector2.Zero;
foreach (var c in conns)
{
if (c.IsOutput)
{
rightSize.X = MathF.Max(rightSize.X, c.Length);
}
else
{
leftSize.X = MathF.Max(leftSize.X, c.Length);
}
if (c.IsOutput)
{
rightSize.Y += CircuitBoxConnection.Size;
}
else
{
leftSize.Y += CircuitBoxConnection.Size;
}
}
return new Vector2(leftSize.X + CircuitBoxSizes.NodeInitialPadding + rightSize.X, CircuitBoxSizes.NodeInitialPadding + MathF.Max(leftSize.Y, rightSize.Y));
}
protected void UpdatePositions()
{
Vector2 rectStart = Position - Size / 2f;
Vector2 rectSize = Size;
rectSize.Y += CircuitBoxSizes.NodeHeaderHeight;
Rect = new RectangleF(rectStart, rectSize);
#if CLIENT
UpdateDrawRects();
#endif
int leftIndex = 0,
rightIndex = 0;
int inputCount = 0,
outputCount = 0;
foreach (var c in Connectors)
{
if (c.IsOutput)
{
outputCount++;
}
else
{
inputCount++;
}
}
Vector2 drawPos = Position;
drawPos.Y = -drawPos.Y;
foreach (var c in Connectors)
{
bool isOutput = c.IsOutput;
int yIndex = isOutput ? rightIndex : leftIndex;
int count = isOutput ? outputCount : inputCount;
float totalHeight = (count * CircuitBoxConnection.Size) / 2f;
float y = (yIndex * CircuitBoxConnection.Size) - totalHeight;
float halfWidth = Rect.Width / 2f - CircuitBoxConnection.Size / 2f;
halfWidth -= 16f;
float xOffset = c.IsOutput ? halfWidth : -halfWidth;
Vector2 inputPos = drawPos + new Vector2(xOffset, y + c.Rect.Height / 2f);
c.Position = inputPos;
if (isOutput)
{
rightIndex++;
}
else
{
leftIndex++;
}
}
}
}
}
@@ -0,0 +1,43 @@
#nullable enable
using System;
namespace Barotrauma
{
internal class CircuitBoxSelectable
{
public bool IsSelected;
public ushort SelectedBy;
public bool IsSelectedByMe
{
get
{
if (GameMain.NetworkMember is { IsServer: true })
{
throw new Exception("CircuitBoxSelectable.IsSelectedByMe should never be used by the server.");
}
if (Character.Controlled is { } controlled)
{
return SelectedBy == controlled.ID;
}
return false;
}
}
public void SetSelected(Option<ushort> selectedBy)
{
if (selectedBy.TryUnwrap(out ushort id))
{
SelectedBy = id;
IsSelected = true;
return;
}
IsSelected = false;
SelectedBy = 0;
}
}
}
@@ -0,0 +1,17 @@
#nullable enable
namespace Barotrauma
{
internal static class CircuitBoxSizes
{
public const int ConnectorSize = 32;
public const int AnchorOffset = 24;
public const int NodeHeaderHeight = 48;
public const int NodeInitialPadding = 64;
public const int WireWidth = 10;
public const int WireKnobLength = 16;
public const int NodeHeaderTextPadding = 8;
public const float PlayableAreaSize = 8192f;
}
}
@@ -0,0 +1,186 @@
#nullable enable
using System.Xml.Linq;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
internal partial class CircuitBoxWire : CircuitBoxSelectable, ICircuitBoxIdentifiable
{
public CircuitBoxConnection From, To;
public readonly Option<Item> BackingWire;
public readonly Color Color;
public readonly ItemPrefab UsedItemPrefab;
public ushort ID { get; }
public CircuitBoxWire(CircuitBox circuitBox, ushort Id, Option<Item> backingItem, CircuitBoxConnection from, CircuitBoxConnection to, ItemPrefab prefab)
{
ID = Id;
From = from;
To = to;
BackingWire = backingItem;
Color = prefab.SpriteColor;
UsedItemPrefab = prefab;
#if CLIENT
Renderer = new CircuitBoxWireRenderer(Option.Some(this), to.AnchorPoint, from.AnchorPoint, Color, circuitBox.WireSprite);
#endif
EnsureWireConnected();
}
public XElement Save()
{
XElement element = new XElement("Wire",
new XAttribute("id", ID),
new XAttribute("backingitemid", BackingWire.TryUnwrap(out var item) ? ItemSlotIndexPair.Serialize(item) : string.Empty),
new XAttribute("prefab", UsedItemPrefab.Identifier));
XElement fromElement = CircuitBoxConnectorIdentifier.FromConnection(From).Save("From"),
toElement = CircuitBoxConnectorIdentifier.FromConnection(To).Save("To");
element.Add(fromElement);
element.Add(toElement);
return element;
}
public static Option<CircuitBoxWire> TryLoadFromXML(ContentXElement element, CircuitBox circuitBox)
{
ushort id = element.GetAttributeUInt16("id", ICircuitBoxIdentifiable.NullComponentID);
var backingItemIdOption = ItemSlotIndexPair.TryDeserializeFromXML(element, "backingitemid");
Identifier usedPrefabIdentifier = element.GetAttributeIdentifier("prefab", Identifier.Empty);
if (!ItemPrefab.Prefabs.TryGet(usedPrefabIdentifier, out var itemPrefab))
{
DebugConsole.ThrowErrorAndLogToGA("CircuitBoxWire.TryLoadFromXML:PrefabNotFound",
$"Failed to find prefab used to create wire with identifier {usedPrefabIdentifier} for CircuitBoxWire with ID {id}");
return Option.None;
}
Option<Item> backingItem = Option.None;
if (backingItemIdOption.TryUnwrap(out var backingItemIdPair))
{
if (backingItemIdPair.FindItemInContainer(circuitBox.WireContainer) is { } item)
{
backingItem = Option.Some(item);
}
else
{
DebugConsole.ThrowErrorAndLogToGA("CircuitBoxWire.TryLoadFromXML:IdNotFound",
$"Failed to find item with ID {backingItemIdPair} for CircuitBoxWire with ID {id}");
return Option.None;
}
}
Option<CircuitBoxConnection> From = Option.None,
To = Option.None;
foreach (ContentXElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "from":
var fromIdentifier = CircuitBoxConnectorIdentifier.Load(subElement);
if (fromIdentifier.FindConnection(circuitBox).TryUnwrap(out var fromConnection))
{
From = Option.Some(fromConnection);
}
break;
case "to":
var toIdentifier = CircuitBoxConnectorIdentifier.Load(subElement);
if (toIdentifier.FindConnection(circuitBox).TryUnwrap(out var toConnection))
{
To = Option.Some(toConnection);
}
break;
}
}
if (From.TryUnwrap(out var from) && To.TryUnwrap(out var to))
{
return Option.Some(new CircuitBoxWire(circuitBox, id, backingItem, from, to, itemPrefab));
}
DebugConsole.ThrowErrorAndLogToGA("CircuitBoxWire.TryLoadFromXML:MissingFromOrTo",
$"Failed to load CircuitBoxWire with ID {id}, missing \"From\" or \"To\" connection.");
return Option.None;
}
public void EnsureWireConnected()
{
EnsureExternalConnection(From, To);
EnsureExternalConnection(To, From);
if (!BackingWire.TryUnwrap(out var item) || item.GetComponent<Wire>() is not { } wire) { return; }
wire.DropOnConnect = false;
From.Connection.ConnectWire(wire);
To.Connection.ConnectWire(wire);
wire.Connect(From.Connection, 0, addNode: false, sendNetworkEvent: false);
wire.Connect(To.Connection, 1, addNode: false, sendNetworkEvent: false);
static void EnsureExternalConnection(CircuitBoxConnection one, CircuitBoxConnection two)
{
switch (one)
{
case CircuitBoxInputConnection input:
{
if (input.ExternallyConnectedTo.Contains(two)) { break; }
input.ExternallyConnectedTo.Add(two);
break;
}
case CircuitBoxOutputConnection output:
{
if (output.ExternallyConnectedFrom.Contains(two)) { break; }
output.ExternallyConnectedFrom.Add(two);
break;
}
case CircuitBoxNodeConnection node when two is CircuitBoxOutputConnection output:
{
if (node.Connection.CircuitBoxConnections.Contains(output)) { break; }
node.Connection.CircuitBoxConnections.Add(output);
break;
}
case CircuitBoxNodeConnection node when two is CircuitBoxInputConnection input:
{
if (node.ExternallyConnectedFrom.Contains(input)) { break; }
node.ExternallyConnectedFrom.Add(input);
break;
}
}
}
}
public void Remove()
{
// client should not remove wires
if (GameMain.NetworkMember is { IsClient: true }) { return; }
if (!BackingWire.TryUnwrap(out var wireItem)) { return; }
if (Entity.Spawner is { } spawner && Screen.Selected is not { IsEditor: true })
{
spawner.AddEntityToRemoveQueue(wireItem);
return;
}
Wire? wire = wireItem.GetComponent<Wire>();
if (wire is not null)
{
From.Connection.DisconnectWire(wire);
To.Connection.DisconnectWire(wire);
}
// if EntitySpawner is not available
wireItem.Remove();
}
public static ItemPrefab DefaultWirePrefab => ItemPrefab.Prefabs[Tags.RedWire];
public static ItemPrefab SelectedWirePrefab = DefaultWirePrefab;
public static readonly Color DefaultWireColor = DefaultWirePrefab.SpriteColor;
}
}
@@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
{
public interface ICircuitBoxIdentifiable
{
public const ushort NullComponentID = ushort.MaxValue;
public ushort ID { get; }
public static ushort FindFreeID<T>(IReadOnlyCollection<T> ids) where T : ICircuitBoxIdentifiable
{
var sortedIds = ids.Select(static i => i.ID).ToImmutableHashSet();
for (ushort i = 0; i < NullComponentID - 1; i++)
{
if (!sortedIds.Contains(i))
{
return i;
}
}
return NullComponentID;
}
}
}
@@ -0,0 +1,44 @@
#nullable enable
using System;
using System.Linq;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
internal readonly record struct ItemSlotIndexPair(int Slot, int StackIndex)
{
public static Option<ItemSlotIndexPair> TryDeserializeFromXML(ContentXElement element, string elementName)
{
string? elementStr = element.GetAttributeString(elementName, string.Empty);
if (string.IsNullOrEmpty(elementStr)) { return Option.None; }
var point = XMLExtensions.ParsePoint(elementStr);
return Option.Some(new ItemSlotIndexPair(point.X, point.Y));
}
public static string Serialize(Item item)
{
Inventory parent = item.ParentInventory;
if (item.ParentInventory is null)
{
throw new Exception($"Item \"{item.Name}\" is not in an inventory.");
}
int slotIndex = parent.FindIndex(item);
int stackIndex = parent.GetItemStackSlotIndex(item, slotIndex);
if (slotIndex < 0 || stackIndex < 0)
{
throw new Exception($"Unable to find item \"{item.Name}\" in its parent inventory.");
}
return XMLExtensions.PointToString(new Point(slotIndex, stackIndex));
}
public Item? FindItemInContainer(ItemContainer? container)
=> container?.Inventory.GetItemsAt(Slot).ElementAt(StackIndex);
}
}
@@ -67,6 +67,22 @@ namespace Barotrauma
.ToImmutableHashSet();
}
public static bool IsLegacyContentType(XElement contentFileElement, ContentPackage package, bool logWarning)
{
Identifier elemName = contentFileElement.NameAsIdentifier();
if (elemName == "TraitorMissions")
{
if (logWarning)
{
DebugConsole.AddWarning(
$"The content type \"TraitorMission\" in content package \"{package.Name}\" is no longer supported." +
$" Traitor missions should be implemented using the scripted event system and the content type TraitorEvents.");
}
return true;
}
return false;
}
public static Result<ContentFile, ContentPackage.LoadError> CreateFromXElement(ContentPackage contentPackage, XElement element)
{
static Result<ContentFile, ContentPackage.LoadError> fail(string error, Exception? exception = null)
@@ -28,8 +28,16 @@ namespace Barotrauma
{
foreach (var subElement in parentElement.Elements())
{
var prefab = new EventPrefab(subElement, this);
EventPrefab.Prefabs.Add(prefab, overriding);
if (subElement.NameAsIdentifier() == "traitorevent")
{
var prefab = new TraitorEventPrefab(subElement, this);
EventPrefab.Prefabs.Add(prefab, overriding);
}
else
{
var prefab = new EventPrefab(subElement, this);
EventPrefab.Prefabs.Add(prefab, overriding);
}
}
}
else if (elemName == "eventsprites")
@@ -28,6 +28,10 @@ namespace Barotrauma
{
//Overrides for subs don't exist! Should we change this?
}
// Use byte-perfect hash because this is compressed, trimming whitespace is incorrect and needlessly slow here
public override Md5Hash CalculateHash()
=> Md5Hash.CalculateForFile(Path.FullPath, Md5Hash.StringHashOptions.BytePerfect);
}
[NotSyncedInMultiplayer]
@@ -1,26 +0,0 @@
using System.Xml.Linq;
#warning TODO: This file is just about the only thing that's actually somewhat okay about the current traitor system. Gut the whole thing.
#if CLIENT
using PrefabType = Barotrauma.TraitorMissionPrefab;
#elif SERVER
using PrefabType = Barotrauma.TraitorMissionPrefab.TraitorMissionEntry;
#endif
namespace Barotrauma
{
[RequiredByCorePackage]
sealed class TraitorMissionsFile : GenericPrefabFile<PrefabType>
{
public TraitorMissionsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => identifier == "TraitorMission";
protected override bool MatchesPlural(Identifier identifier) => identifier == "TraitorMissions";
protected override PrefabCollection<PrefabType> Prefabs => PrefabType.Prefabs;
protected override PrefabType CreatePrefab(ContentXElement element)
{
return new PrefabType(element, this);
}
}
}
@@ -23,7 +23,7 @@ namespace Barotrauma
: string.Empty);
}
public static readonly Version MinimumHashCompatibleVersion = new Version(1, 0, 13, 2);
public static readonly Version MinimumHashCompatibleVersion = new Version(1, 1, 0, 0);
public const string LocalModsDir = "LocalMods";
public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine(
@@ -110,6 +110,7 @@ namespace Barotrauma
InstallTime = rootElement.GetAttributeDateTime("installtime");
var fileResults = rootElement.Elements()
.Where(e => !ContentFile.IsLegacyContentType(e, this, logWarning: true))
.Select(e => ContentFile.CreateFromXElement(this, e))
.ToArray();
@@ -337,6 +338,7 @@ namespace Barotrauma
XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null.");
var fileResults = rootElement.Elements()
.Where(e => !ContentFile.IsLegacyContentType(e, this, logWarning: true))
.Select(e => ContentFile.CreateFromXElement(this, e))
.ToArray();
@@ -32,6 +32,11 @@ namespace Barotrauma
private static readonly List<RegularPackage> regular = new List<RegularPackage>();
public static IReadOnlyList<RegularPackage> Regular => regular;
/// <summary>
/// Combined hash of the currently enabled packages. Can be used to check if the enabled mods or their load order has changed.
/// </summary>
public static Md5Hash MergedHash { get; private set; } = Md5Hash.Blank;
public static IEnumerable<ContentPackage> All =>
Core != null
? (Core as ContentPackage).ToEnumerable().CollectionConcat(Regular)
@@ -149,6 +154,7 @@ namespace Barotrauma
.SelectMany(r => r.Files)
.Distinct(new TypeComparer<ContentFile>())
.ForEach(f => f.Sort());
MergedHash = Md5Hash.MergeHashes(All.Select(cp => cp.Hash));
}
public static int IndexOf(ContentPackage contentPackage)
@@ -1,10 +1,9 @@
#nullable enable
using Barotrauma.IO;
using System;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using Barotrauma.IO;
namespace Barotrauma
{
@@ -93,10 +92,21 @@ namespace Barotrauma
}
public static ContentPath FromRaw(string? rawValue)
=> new ContentPath(null, rawValue);
=> FromRaw(null, rawValue);
private static ContentPath? prevCreatedRaw;
public static ContentPath FromRaw(ContentPackage? contentPackage, string? rawValue)
=> new ContentPath(contentPackage, rawValue);
{
var newRaw = new ContentPath(contentPackage, rawValue);
if (prevCreatedRaw is not null && prevCreatedRaw.ContentPackage == contentPackage &&
prevCreatedRaw.RawValue == rawValue)
{
newRaw.cachedValue = prevCreatedRaw.Value;
}
prevCreatedRaw = newRaw;
return newRaw;
}
public static ContentPath FromEvaluated(ContentPackage? contentPackage, string? evaluatedValue)
{
@@ -146,8 +156,8 @@ namespace Barotrauma
return HashCode.Combine(RawValue, ContentPackage, cachedValue, cachedFullPath);
}
public bool IsNullOrEmpty() => string.IsNullOrEmpty(Value);
public bool IsNullOrWhiteSpace() => string.IsNullOrWhiteSpace(Value);
public bool IsPathNullOrEmpty() => string.IsNullOrEmpty(Value);
public bool IsPathNullOrWhiteSpace() => string.IsNullOrWhiteSpace(Value);
public bool EndsWith(string suffix) => Value.EndsWith(suffix, StringComparison.OrdinalIgnoreCase);
@@ -52,7 +52,7 @@ namespace Barotrauma
=> Element.Descendants().Select(e => new ContentXElement(ContentPackage, e));
public IEnumerable<ContentXElement> GetChildElements(string name)
=> Elements().Where(e => string.Equals(name, e.Name.LocalName, StringComparison.InvariantCultureIgnoreCase));
=> Elements().Where(e => string.Equals(name, e.Name.LocalName, StringComparison.OrdinalIgnoreCase));
public XAttribute? GetAttribute(string name) => Element.GetAttribute(name);
@@ -76,6 +76,7 @@ namespace Barotrauma
public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant);
public ContentPath? GetAttributeContentPath(string key) => Element.GetAttributeContentPath(key, ContentPackage);
public int GetAttributeInt(string key, int def) => Element.GetAttributeInt(key, def);
public ushort GetAttributeUInt16(string key, ushort def) => Element.GetAttributeUInt16(key, def);
public int[]? GetAttributeIntArray(string key, int[]? def) => Element.GetAttributeIntArray(key, def);
public ushort[]? GetAttributeUshortArray(string key, ushort[]? def) => Element.GetAttributeUshortArray(key, def);
public float GetAttributeFloat(string key, float def) => Element.GetAttributeFloat(key, def);
@@ -1,4 +1,6 @@
using System;
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -15,20 +17,20 @@ namespace Barotrauma
public abstract bool EndsCoroutine(CoroutineHandle handle);
}
class EnumCoroutineStatus : CoroutineStatus
sealed class EnumCoroutineStatus : CoroutineStatus
{
private enum StatusValue
{
Running, Success, Failure
}
private readonly StatusValue Value;
private readonly StatusValue value;
private EnumCoroutineStatus(StatusValue value) { Value = value; }
private EnumCoroutineStatus(StatusValue value) { this.value = value; }
public new readonly static EnumCoroutineStatus Running = new EnumCoroutineStatus(StatusValue.Running);
public new readonly static EnumCoroutineStatus Success = new EnumCoroutineStatus(StatusValue.Success);
public new readonly static EnumCoroutineStatus Failure = new EnumCoroutineStatus(StatusValue.Failure);
public new static readonly EnumCoroutineStatus Running = new EnumCoroutineStatus(StatusValue.Running);
public new static readonly EnumCoroutineStatus Success = new EnumCoroutineStatus(StatusValue.Success);
public new static readonly EnumCoroutineStatus Failure = new EnumCoroutineStatus(StatusValue.Failure);
public override bool CheckFinished(float deltaTime)
{
@@ -37,43 +39,74 @@ namespace Barotrauma
public override bool EndsCoroutine(CoroutineHandle handle)
{
if (Value == StatusValue.Failure)
if (value == StatusValue.Failure)
{
DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed");
}
return Value != StatusValue.Running;
return value != StatusValue.Running;
}
public override bool Equals(object obj)
public override bool Equals(object? obj)
{
return obj is EnumCoroutineStatus other && Value == other.Value;
return obj is EnumCoroutineStatus other && value == other.value;
}
public override int GetHashCode()
{
return Value.GetHashCode();
return value.GetHashCode();
}
public override string ToString()
{
return Value.ToString();
return value.ToString();
}
}
sealed class WaitForSeconds : CoroutineStatus
{
public readonly float TotalTime;
private float timer;
private readonly bool ignorePause;
public WaitForSeconds(float time, bool ignorePause = true)
{
timer = time;
TotalTime = time;
this.ignorePause = ignorePause;
}
public override bool CheckFinished(float deltaTime)
{
#if !SERVER
if (ignorePause || !CoroutineManager.Paused)
{
timer -= deltaTime;
}
#else
timer -= deltaTime;
#endif
return timer <= 0.0f;
}
public override bool EndsCoroutine(CoroutineHandle handle)
{
return false;
}
}
class CoroutineHandle
sealed class CoroutineHandle
{
public readonly IEnumerator<CoroutineStatus> Coroutine;
public readonly string Name;
public Exception Exception;
public volatile bool AbortRequested;
public Exception? Exception;
public bool AbortRequested;
public Thread Thread;
public CoroutineHandle(IEnumerator<CoroutineStatus> coroutine, string name = "", bool useSeparateThread = false)
public CoroutineHandle(IEnumerator<CoroutineStatus> coroutine, string name = "")
{
Coroutine = coroutine;
Name = string.IsNullOrWhiteSpace(name) ? coroutine.ToString() : name;
Name = string.IsNullOrWhiteSpace(name) ? (coroutine.ToString() ?? "") : name;
Exception = null;
}
@@ -88,7 +121,7 @@ namespace Barotrauma
public static bool Paused { get; private set; }
public static CoroutineHandle StartCoroutine(IEnumerable<CoroutineStatus> func, string name = "", bool useSeparateThread = false)
public static CoroutineHandle StartCoroutine(IEnumerable<CoroutineStatus> func, string name = "")
{
var handle = new CoroutineHandle(func.GetEnumerator(), name);
lock (Coroutines)
@@ -96,17 +129,6 @@ namespace Barotrauma
Coroutines.Add(handle);
}
handle.Thread = null;
if (useSeparateThread)
{
handle.Thread = new Thread(() => { ExecuteCoroutineThread(handle); })
{
Name = "Coroutine Thread (" + handle.Name + ")",
IsBackground = true
};
handle.Thread.Start();
}
return handle;
}
@@ -115,11 +137,12 @@ namespace Barotrauma
return StartCoroutine(DoInvokeAfter(action, delay));
}
private static IEnumerable<CoroutineStatus> DoInvokeAfter(Action action, float delay)
private static IEnumerable<CoroutineStatus> DoInvokeAfter(Action? action, float delay)
{
if (action == null)
{
yield return CoroutineStatus.Failure;
yield break;
}
if (delay > 0.0f)
@@ -169,20 +192,12 @@ namespace Barotrauma
private static void HandleCoroutineStopping(Func<CoroutineHandle, bool> filter)
{
// No lock here because all callers lock for us
foreach (CoroutineHandle coroutine in Coroutines)
{
if (filter(coroutine))
{
coroutine.AbortRequested = true;
if (coroutine.Thread != null)
{
bool joined = false;
while (!joined)
{
CrossThread.ProcessTasks();
joined = coroutine.Thread.Join(TimeSpan.FromMilliseconds(500));
}
}
}
}
}
@@ -199,49 +214,13 @@ namespace Barotrauma
return false;
}
public static void ExecuteCoroutineThread(CoroutineHandle handle)
{
try
{
while (!handle.AbortRequested)
{
if (PerformCoroutineStep(handle)) { return; }
Thread.Sleep((int)(DeltaTime * 1000));
}
}
catch (ThreadAbortException)
{
//not an error, don't worry about it
}
catch (Exception e)
{
handle.Exception = e;
DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has thrown an exception", e);
}
}
private static bool IsDone(CoroutineHandle handle)
{
#if !DEBUG
try
{
#endif
if (handle.Thread == null)
{
return PerformCoroutineStep(handle);
}
else
{
if (handle.Thread.ThreadState.HasFlag(ThreadState.Stopped))
{
if (handle.Exception != null || handle.Coroutine.Current == CoroutineStatus.Failure)
{
DebugConsole.ThrowError("Coroutine \"" + handle.Name + "\" has failed");
}
return true;
}
return false;
}
return PerformCoroutineStep(handle);
#if !DEBUG
}
catch (Exception e)
@@ -256,27 +235,27 @@ namespace Barotrauma
#endif
}
// Updating just means stepping through all the coroutines
private static readonly List<CoroutineHandle> coroutinePass = new List<CoroutineHandle>();
public static void Update(bool paused, float deltaTime)
{
Paused = paused;
DeltaTime = deltaTime;
List<CoroutineHandle> coroutineList;
// Do not optimize this as a for loop directly over the Coroutines list!
// Coroutines are able to spawn new coroutines!
lock (Coroutines)
{
coroutineList = Coroutines.ToList();
coroutinePass.AddRange(Coroutines);
}
foreach (var coroutine in coroutineList)
foreach (var coroutine in coroutinePass)
{
if (IsDone(coroutine))
if (!IsDone(coroutine)) { continue; }
lock (Coroutines)
{
lock (Coroutines)
{
Coroutines.Remove(coroutine);
}
Coroutines.Remove(coroutine);
}
}
coroutinePass.Clear();
}
public static void ListCoroutines()
@@ -292,37 +271,4 @@ namespace Barotrauma
}
}
}
class WaitForSeconds : CoroutineStatus
{
public readonly float TotalTime;
private float timer;
private readonly bool ignorePause;
public WaitForSeconds(float time, bool ignorePause = true)
{
timer = time;
TotalTime = time;
this.ignorePause = ignorePause;
}
public override bool CheckFinished(float deltaTime)
{
#if !SERVER
if (ignorePause || !CoroutineManager.Paused)
{
timer -= deltaTime;
}
#else
timer -= deltaTime;
#endif
return timer <= 0.0f;
}
public override bool EndsCoroutine(CoroutineHandle handle)
{
return false;
}
}
}
@@ -337,7 +337,14 @@ namespace Barotrauma
NewMessage("Enemy AI enabled", Color.Green);
}, isCheat: true));
commands.Add(new Command("starttraitormissionimmediately", "starttraitormissionimmediately: Skip the initial delay of the traitor mission and start one immediately.", null));
commands.Add(new Command("triggertraitorevent|starttraitoreventimmediately", "triggertraitorevent [eventidentifier]: Skip the initial delay of the traitor events and start one immediately. You can optionally specify which event to start (otherwise a random event is chosen).", null,
() =>
{
return new string[][]
{
EventPrefab.Prefabs.Where(p => p is TraitorEventPrefab).Select(p => p.Identifier.ToString()).ToArray()
};
}));
commands.Add(new Command("botcount", "botcount [x]: Set the number of bots in the crew in multiplayer.", null));
@@ -645,7 +652,7 @@ namespace Barotrauma
commands.Add(new Command("findentityids", "findentityids [entityname]", (string[] args) =>
{
if (args.Length == 0) { return; }
foreach (MapEntity mapEntity in MapEntity.mapEntityList)
foreach (MapEntity mapEntity in MapEntity.MapEntityList)
{
if (mapEntity.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase))
{
@@ -738,7 +745,7 @@ namespace Barotrauma
commands.Add(new Command("revive", "revive [character name]: Bring the specified character back from the dead. If the name parameter is omitted, the controlled character will be revived.", (string[] args) =>
{
Character revivedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args);
if (revivedCharacter == null) return;
if (revivedCharacter == null) { return; }
revivedCharacter.Revive();
#if SERVER
@@ -746,7 +753,7 @@ namespace Barotrauma
{
foreach (Client c in GameMain.Server.ConnectedClients)
{
if (c.Character != revivedCharacter) continue;
if (c.Character != revivedCharacter) { continue; }
//clients stop controlling the character when it dies, force control back
GameMain.Server.SetClientCharacter(c, revivedCharacter);
@@ -816,8 +823,12 @@ namespace Barotrauma
if (GameMain.GameSession?.EventManager != null && args.Length > 0)
{
EventPrefab eventPrefab = eventPrefabs.Find(prefab => prefab.Identifier == args[0]);
if (eventPrefab != null)
if (eventPrefab is TraitorEventPrefab)
{
ThrowError($"{eventPrefab.Identifier} is a traitor event. You need to use the 'triggertraitorevent' command to start it.");
return;
}
else if (eventPrefab != null)
{
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null)
@@ -825,8 +836,7 @@ namespace Barotrauma
NewMessage($"Could not initialize event {args[0]} because level did not meet requirements");
return;
}
GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent);
newEvent.Init();
GameMain.GameSession.EventManager.ActivateEvent(newEvent);
NewMessage($"Initialized event {eventPrefab.Identifier}", Color.Aqua);
return;
}
@@ -845,9 +855,36 @@ namespace Barotrauma
};
}));
commands.Add(new Command("debugevent", "debugevent [identifier]: outputs debug info about a specific event that's currently active. Mainly intended for debugging events in multiplayer: in single player, the same information is available by enabling debugdraw.", (string[] args) =>
{
if (GameMain.GameSession?.EventManager is EventManager eventManager && args.Length > 0)
{
var ev = eventManager.ActiveEvents.FirstOrDefault(ev => ev.Prefab?.Identifier == args[0]);
if (ev == null)
{
ThrowError($"Event \"{args[0]}\" not found.");
}
else
{
string info = ev.GetDebugInfo();
#if SERVER
//strip rich text tags
RichTextData.GetRichTextData(info, out info);
#endif
NewMessage(info);
}
}
}, isCheat: true, getValidArgs: () =>
{
return new[]
{
GameMain.GameSession?.EventManager?.ActiveEvents.Select(ev => ev.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty<string>()
};
}));
commands.Add(new Command("unlockmission", "unlockmission [identifier/tag]: Unlocks a mission in a random adjacent level.", (string[] args) =>
{
if (!(GameMain.GameSession?.GameMode is CampaignMode campaign))
if (GameMain.GameSession?.GameMode is not CampaignMode campaign)
{
ThrowError("The unlockmission command is only usable in the campaign mode.");
return;
@@ -1152,6 +1189,33 @@ namespace Barotrauma
throw new Exception("crash command issued");
}));
commands.Add(new Command("listeditableproperties", "", (string[] args) =>
{
StringBuilder sb = new StringBuilder();
string filename;
#if CLIENT
filename = "ItemComponent properties (client).txt";
sb.AppendLine("Client-side ItemComponent properties:");
#else
filename = "ItemComponent properties (server).txt";
sb.AppendLine("Server-side ItemComponent properties:");
#endif
var itemComponents = typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent)));
foreach (var ic in itemComponents.OrderBy(ic => ic.Name))
{
sb.AppendLine(ic.Name+":");
foreach (var prop in ic.GetProperties())
{
if (prop.DeclaringType != ic) { continue; }
if (prop.GetCustomAttributes(inherit: false).OfType<Editable>().Any())
{
sb.AppendLine(prop.Name);
}
}
}
File.WriteAllText(filename, sb.ToString());
}));
commands.Add(new Command("fastforward", "fastforward [seconds]: Fast forwards the game by x seconds. Note that large numbers may cause a long freeze.", (string[] args) =>
{
float seconds = 0;
@@ -1264,7 +1328,7 @@ namespace Barotrauma
}
#endif
commands.Add(new Command("showreputation", "showreputation: List the current reputation values.", (string[] args) =>
commands.Add(new Command("showreputation", "showreputation: List the current reputation values.", (string[] args) =>
{
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
@@ -1641,7 +1705,7 @@ namespace Barotrauma
List<Pump> pumps = new List<Pump>();
foreach (Item item in Submarine.MainSub.GetItems(true))
{
if (item.CurrentHull != null && item.HasTag("ballast") && item.GetComponent<Pump>() is { } pump)
if (item.CurrentHull != null && item.HasTag(Tags.Ballast) && item.GetComponent<Pump>() is { } pump)
{
if (item.CurrentHull.BallastFlora != null) { continue; }
pumps.Add(pump);
@@ -2225,8 +2289,8 @@ namespace Barotrauma
string itemNameOrId = args[0].ToLowerInvariant();
ItemPrefab itemPrefab =
(MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ??
MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as ItemPrefab;
(MapEntityPrefab.FindByName(itemNameOrId) ??
MapEntityPrefab.FindByIdentifier(itemNameOrId.ToIdentifier())) as ItemPrefab;
if (itemPrefab == null)
{
errorMsg = "Item \"" + itemNameOrId + "\" not found!";
@@ -2321,6 +2385,19 @@ namespace Barotrauma
}
}
/// <summary>
/// Throws the error in debug builds. In non-debug builds, logs it instead.
/// Use for handling non-critical errors that shouldn't go unnoticed in debug builds (like warnings might), but which don't break the game and thus doesn't have to open the console.
/// </summary>
public static void AddSafeError(string error)
{
#if DEBUG
DebugConsole.ThrowError(error);
#else
DebugConsole.LogError(error);
#endif
}
public static void LogError(string msg, Color? color = null)
{
color ??= Color.Red;
@@ -2494,7 +2571,16 @@ namespace Barotrauma
LogError(error);
}
public static void ThrowErrorAndLogToGA(string gaIdentifier, string errorMsg)
{
ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
gaIdentifier,
GameAnalyticsManager.ErrorSeverity.Error,
errorMsg);
}
public static void AddWarning(string warning)
{
System.Diagnostics.Debug.WriteLine(warning);
@@ -64,7 +64,16 @@ namespace Barotrauma
spawnPending = true;
}
public override string GetDebugInfo()
{
return
$"Finished: {IsFinished.ColorizeObject()}\n" +
$"Item: {Item.ColorizeObject()}\n" +
$"Spawn pending: {SpawnPending.ColorizeObject()}\n" +
$"Spawn position: {SpawnPos.ColorizeObject()}";
}
private void SpawnItem()
{
item = new Item(itemPrefab, spawnPos, null);
@@ -73,7 +82,7 @@ namespace Barotrauma
//try to find an artifact holder and place the artifact inside it
foreach (Item it in Item.ItemList)
{
if (it.Submarine != null || !it.HasTag("artifactholder")) continue;
if (it.Submarine != null || !it.HasTag(Tags.ArtifactHolder)) { continue; }
var itemContainer = it.GetComponent<Items.Components.ItemContainer>();
if (itemContainer == null) continue;
@@ -52,6 +52,11 @@ namespace Barotrauma
ParentSet = parentSet;
}
public virtual string GetDebugInfo()
{
return $"Finished: {IsFinished.ColorizeObject()}";
}
public virtual void Update(float deltaTime)
{
}
@@ -12,6 +12,9 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; } = Identifier.Empty;
[Serialize("", IsPropertySaveable.Yes, description: "Tag referring to the character who caused the affliction.")]
public Identifier SourceCharacter { get; set; } = Identifier.Empty;
[Serialize(LimbType.None, IsPropertySaveable.Yes, "Only check afflictions on the specified limb type")]
public LimbType TargetLimb { get; set; }
@@ -33,8 +36,7 @@ namespace Barotrauma
if (target.CharacterHealth == null) { continue; }
if (TargetLimb == LimbType.None)
{
var affliction = target.CharacterHealth.GetAffliction(Identifier, AllowLimbAfflictions);
if (affliction != null && affliction.Strength >= MinStrength) { return true; }
if (target.CharacterHealth.GetAfflictionStrengthByIdentifier(Identifier, AllowLimbAfflictions) >= MinStrength) { return true; }
}
IEnumerable<Affliction> afflictions = target.CharacterHealth.GetAllAfflictions().Where(affliction =>
{
@@ -43,9 +45,13 @@ namespace Barotrauma
LimbType? limbType = target.CharacterHealth.GetAfflictionLimb(affliction)?.type;
if (limbType == null || limbType != TargetLimb) { return false; }
}
if (!SourceCharacter.IsEmpty)
{
if (!ParentEvent.GetTargets(SourceCharacter).Contains(affliction.Source)) { return false; }
}
return affliction.Strength >= MinStrength;
});
if (afflictions.Any(a => a.Identifier == Identifier)) { return true; }
}
return false;
@@ -1,3 +1,4 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
@@ -15,20 +16,13 @@ namespace Barotrauma
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed.");
}
foreach (var attribute in element.Attributes())
{
if (PropertyConditional.IsValid(attribute) && !IsTargetTagAttribute(attribute))
{
Conditional = new PropertyConditional(attribute);
break;
}
}
Conditional = PropertyConditional.FromXElement(element, IsNotTargetTagAttribute).FirstOrDefault();
if (Conditional == null)
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed.");
}
static bool IsTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() == "targettag";
static bool IsNotTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() != "targettag";
}
private string GetEventName()
@@ -52,7 +46,7 @@ namespace Barotrauma
}
if (target == null)
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.");
DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.");
}
if (target == null || Conditional == null)
{
@@ -21,7 +21,7 @@ namespace Barotrauma
protected object? value2;
protected object? value1;
protected PropertyConditional.OperatorType Operator { get; set; }
protected PropertyConditional.ComparisonOperatorType Operator { get; set; }
public CheckDataAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
@@ -56,23 +56,13 @@ namespace Barotrauma
{
if (GameMain.GameSession?.GameMode is not CampaignMode campaignMode) { return false; }
string[] splitString = Condition.Split(' ');
string value;
if (splitString.Length > 0)
(Operator, string value) = PropertyConditional.ExtractComparisonOperatorFromConditionString(Condition);
if (Operator == PropertyConditional.ComparisonOperatorType.None)
{
//the first part of the string is the operator, skip it
value = string.Join(" ", splitString.Skip(1));
}
else
{
DebugConsole.ThrowError($"{Condition} is too short, it should start with an operator followed by a boolean or a floating point value.");
DebugConsole.ThrowError($"{Condition} is invalid, it should start with an operator followed by a boolean or a floating point value.");
return false;
}
string op = splitString[0];
Operator = PropertyConditional.GetOperatorType(op);
if (Operator == PropertyConditional.OperatorType.None) { return false; }
if (CheckAgainstMetadata)
{
object? metadata1 = campaignMode.CampaignMetadata.GetValue(Identifier);
@@ -82,8 +72,8 @@ namespace Barotrauma
{
return Operator switch
{
PropertyConditional.OperatorType.Equals => metadata1 == metadata2,
PropertyConditional.OperatorType.NotEquals => metadata1 != metadata2,
PropertyConditional.ComparisonOperatorType.Equals => metadata1 == metadata2,
PropertyConditional.ComparisonOperatorType.NotEquals => metadata1 != metadata2,
_ => false
};
}
@@ -95,7 +85,7 @@ namespace Barotrauma
case bool bool1 when metadata2 is bool bool2:
return CompareBool(bool1, bool2) ?? false;
case float float1 when metadata2 is float float2:
return CompareFloat(float1, float2) ?? false;
return PropertyConditional.CompareFloat(float1, float2, Operator);
}
}
@@ -139,9 +129,9 @@ namespace Barotrauma
value2 = val2;
switch (Operator)
{
case PropertyConditional.OperatorType.Equals:
case PropertyConditional.ComparisonOperatorType.Equals:
return val1 == val2;
case PropertyConditional.OperatorType.NotEquals:
case PropertyConditional.ComparisonOperatorType.NotEquals:
return val1 != val2;
default:
DebugConsole.Log($"Only \"Equals\" and \"Not equals\" operators are allowed for a boolean (was {Operator} for {val2}).");
@@ -153,36 +143,13 @@ namespace Barotrauma
{
if (float.TryParse(value, out float f))
{
return CompareFloat(GetFloat(campaignMode), f);
return PropertyConditional.CompareFloat(GetFloat(campaignMode), f, Operator);
}
DebugConsole.Log($"{value} != float");
return null;
}
private bool? CompareFloat(float val1, float val2)
{
value1 = val1;
value2 = val2;
switch (Operator)
{
case PropertyConditional.OperatorType.Equals:
return MathUtils.NearlyEqual(val1, val2);
case PropertyConditional.OperatorType.GreaterThan:
return val1 > val2;
case PropertyConditional.OperatorType.GreaterThanEquals:
return val1 >= val2;
case PropertyConditional.OperatorType.LessThan:
return val1 < val2;
case PropertyConditional.OperatorType.LessThanEquals:
return val1 <= val2;
case PropertyConditional.OperatorType.NotEquals:
return !MathUtils.NearlyEqual(val1, val2);
}
return null;
}
private bool? TryString(CampaignMode campaignMode, string value)
{
return CompareString(GetString(campaignMode), value);
@@ -195,9 +162,9 @@ namespace Barotrauma
bool equals = string.Equals(val1, val2, StringComparison.OrdinalIgnoreCase);
switch (Operator)
{
case PropertyConditional.OperatorType.Equals:
case PropertyConditional.ComparisonOperatorType.Equals:
return equals;
case PropertyConditional.OperatorType.NotEquals:
case PropertyConditional.ComparisonOperatorType.NotEquals:
return !equals;
default:
DebugConsole.Log($"Only \"Equals\" and \"Not equals\" operators are allowed for a string (was {Operator} for {val2}).");
@@ -1,8 +1,8 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -20,9 +20,15 @@ namespace Barotrauma
[Serialize(1, IsPropertySaveable.Yes)]
public int Amount { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Optional tag of a hull the target must be inside.")]
public Identifier HullTag { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the first target when the check succeeds.")]
public Identifier ApplyTagToTarget { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the found item(s) when the check succeeds.")]
public Identifier ApplyTagToItem { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool RequireEquipped { get; set; }
@@ -32,6 +38,24 @@ namespace Barotrauma
[Serialize(-1, IsPropertySaveable.Yes)]
public int ItemContainerIndex { get; set; }
private readonly bool checkPercentage;
private float requiredConditionalMatchPercentage;
[Serialize(100.0f, IsPropertySaveable.Yes)]
/// <summary>
/// What percentage of targets do the conditionals need to match for the check to succeed?
/// </summary>
public float RequiredConditionalMatchPercentage
{
get { return requiredConditionalMatchPercentage; }
set { requiredConditionalMatchPercentage = MathHelper.Clamp(value, 0.0f, 100.0f); }
}
[Serialize(false, IsPropertySaveable.Yes)]
public bool CompareToInitialAmount { get; set; }
private readonly IReadOnlyList<PropertyConditional> conditionals;
private readonly Identifier[] itemIdentifierSplit;
@@ -44,27 +68,87 @@ namespace Barotrauma
var conditionalList = new List<PropertyConditional>();
foreach (ContentXElement subElement in element.GetChildElements("conditional"))
{
foreach (XAttribute attribute in subElement.Attributes())
{
if (PropertyConditional.IsValid(attribute))
{
conditionalList.Add(new PropertyConditional(attribute));
}
}
conditionalList.AddRange(PropertyConditional.FromXElement(subElement));
break;
}
conditionals = conditionalList;
if (itemTags.None() && ItemIdentifiers.None())
if (itemTags.None() &&
ItemIdentifiers.None() &&
TargetTag.IsEmpty)
{
DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(CheckItemAction)} does't define either tags or identifiers of the item to check.");
}
checkPercentage = element.GetAttribute(nameof(RequiredConditionalMatchPercentage)) is not null;
if (Amount != 1 && checkPercentage)
{
DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Cannot define both '{Amount}' and '{RequiredConditionalMatchPercentage}' in {nameof(CheckItemAction)}.");
}
}
private bool EnoughTargets(int totalTargets, int targetsWithConditionalsMatched)
{
if (CompareToInitialAmount)
{
totalTargets = ParentEvent.GetInitialTargetCount(TargetTag);
}
if (checkPercentage)
{
return MathUtils.Percentage(targetsWithConditionalsMatched, totalTargets) >= RequiredConditionalMatchPercentage;
}
else
{
return targetsWithConditionalsMatched >= Amount;
}
}
private readonly List<Item> tempTargetItems = new List<Item>();
protected override bool? DetermineSuccess()
{
var targets = ParentEvent.GetTargets(TargetTag);
if (!targets.Any()) { return null; }
if (!HullTag.IsEmpty)
{
var hulls = ParentEvent.GetTargets(HullTag).OfType<Hull>();
targets = targets.Where(t =>
(t is Item it && hulls.Contains(it.CurrentHull)) ||
(t is Character c && hulls.Contains(c.CurrentHull)));
}
if (!targets.Any())
{
if (conditionals.Any())
{
//conditionals can't be met if there's no targets
return false;
}
return null;
}
//check if the target(s) are the items we're looking for (instead of characters/containers the items are inside)
int targetCount = targets.Count();
if (targetCount >= Amount)
{
tempTargetItems.Clear();
foreach (var target in targets)
{
if (target is not Item item) { continue; }
if (itemTags.Any(item.HasTag) || itemIdentifierSplit.Contains(item.Prefab.Identifier))
{
if (ConditionalsMatch(item, character: null))
{
tempTargetItems.Add(item);
}
}
}
if (EnoughTargets(targetCount, tempTargetItems.Count))
{
TryApplyTagToItems(tempTargetItems);
return true;
}
}
foreach (var target in targets)
{
if (target is Character character)
@@ -105,8 +189,9 @@ namespace Barotrauma
private bool CheckInventory(Inventory inventory, Character character)
{
if (inventory == null) { return false; }
int count = 0;
int targetCount = 0;
HashSet<Item> eventTargets = new HashSet<Item>();
tempTargetItems.Clear();
foreach (Identifier tag in itemTags)
{
foreach (var target in ParentEvent.GetTargets(tag))
@@ -123,19 +208,39 @@ namespace Barotrauma
eventTargets.Contains(it),
recursive: Recursive))
{
if (!ConditionalsMatch(item, character)) { continue; }
count++;
if (count >= Amount) { return true; }
targetCount++;
if (ConditionalsMatch(item, character))
{
tempTargetItems.Add(item);
}
}
if (EnoughTargets(targetCount, tempTargetItems.Count))
{
TryApplyTagToItems(tempTargetItems);
return true;
}
return false;
}
private void TryApplyTagToItems(IEnumerable<Item> items)
{
if (!ApplyTagToItem.IsEmpty)
{
foreach (var targetItem in items)
{
ParentEvent.AddTarget(ApplyTagToItem, targetItem);
}
}
}
private bool ConditionalsMatch(Item item, Character character = null)
{
if (item == null) { return false; }
foreach (PropertyConditional conditional in conditionals)
{
if (!conditional.Matches(item))
{
if (!item.ConditionalMatches(conditional))
{
return false;
}
@@ -151,8 +256,8 @@ namespace Barotrauma
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckItemAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
$"ItemIdentifiers: {ItemIdentifiers.ColorizeObject()}" +
$"Succeeded: {succeeded.ColorizeObject()})";
(ItemTags.Any() ? $"ItemTags: {ItemTags.ColorizeObject()}, " : $"ItemIdentifiers: {ItemIdentifiers.ColorizeObject()}, ") +
$"Succeeded: {succeeded.ColorizeObject()})";
}
}
}
@@ -0,0 +1,35 @@
#nullable enable
namespace Barotrauma
{
class CheckTraitorEventStateAction : BinaryOptionAction
{
[Serialize(TraitorEvent.State.Completed, IsPropertySaveable.Yes)]
public TraitorEvent.State State { get; set; }
private readonly TraitorEvent? traitorEvent;
public CheckTraitorEventStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (parentEvent is TraitorEvent traitorEvent)
{
this.traitorEvent = traitorEvent;
}
else
{
DebugConsole.ThrowError($"Cannot use the action {nameof(CheckTraitorEventStateAction)} in the event \"{parentEvent.Prefab.Identifier}\" because it's not a traitor event.");
}
}
protected override bool? DetermineSuccess()
{
return traitorEvent?.CurrentState == State;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckTraitorEventStateAction)} -> " +
$"State: {State.ColorizeObject()}, Succeeded: {succeeded.ColorizeObject()})";
}
}
}
@@ -0,0 +1,40 @@
#nullable enable
using Barotrauma.Networking;
using System.Linq;
namespace Barotrauma
{
/// <summary>
/// Checks whether the specific target was voted as the traitor.
/// </summary>
class CheckTraitorVoteAction : BinaryOptionAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier Target { get; set; }
public CheckTraitorVoteAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (parentEvent is not TraitorEvent)
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\" - {nameof(CheckTraitorVoteAction)} can only be used in traitor events.");
}
}
protected override bool? DetermineSuccess()
{
var targetEntities = ParentEvent.GetTargets(Target);
#if SERVER
if (GameMain.Server?.TraitorManager?.GetClientAccusedAsTraitor() is Client traitorClient)
{
return targetEntities.Any(e => e is Character character && traitorClient?.Character == character);
}
#endif
return false;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(succeeded.HasValue)} {nameof(CheckTraitorVoteAction)} -> (TargetTag: {Target.ColorizeObject()}";
}
}
}
@@ -0,0 +1,60 @@
#nullable enable
namespace Barotrauma
{
class CheckVisibilityAction : BinaryOptionAction
{
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check from.")]
public Identifier EntityTag { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check to.")]
public Identifier TargetTag { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Does the entity need to be facing the target? Only valid if the entity is a character.")]
public bool CheckFacing { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the entity who saw the target when the check succeeds.")]
public Identifier ApplyTagToEntity { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the entity that was seen when the check succeeds.")]
public Identifier ApplyTagToTarget { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "If both the seeing entity and the target are the same, does it count as success?")]
public bool AllowSameEntity { get; set; }
public CheckVisibilityAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
}
protected override bool? DetermineSuccess()
{
foreach (var entity in ParentEvent.GetTargets(EntityTag))
{
foreach (var target in ParentEvent.GetTargets(TargetTag))
{
if (!AllowSameEntity && entity == target) { continue; }
if (Character.IsTargetVisible(target, entity, CheckFacing))
{
if (!ApplyTagToEntity.IsEmpty)
{
ParentEvent.AddTarget(ApplyTagToEntity, entity);
}
if (!ApplyTagToTarget.IsEmpty)
{
ParentEvent.AddTarget(ApplyTagToTarget, target);
}
return true;
}
}
}
return false;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckVisibilityAction)} -> (TargetTags: {EntityTag.ColorizeObject()}, {TargetTag.ColorizeObject()})";
}
}
}
@@ -43,13 +43,14 @@ namespace Barotrauma
foreach (var npc in affectedNpcs)
{
if (!(npc.AIController is HumanAIController humanAiController)) { continue; }
if (npc.Removed) { continue; }
if (npc.AIController is not HumanAIController humanAiController) { continue; }
Character enemy = null;
float closestDist = float.MaxValue;
foreach (Entity target in ParentEvent.GetTargets(EnemyTag))
{
if (!(target is Character character)) { continue; }
if (target is not Character character) { continue; }
float dist = Vector2.DistanceSquared(npc.WorldPosition, target.WorldPosition);
if (dist < closestDist)
{
@@ -82,7 +83,7 @@ namespace Barotrauma
{
foreach (var npc in affectedNpcs)
{
if (npc.Removed || !(npc.AIController is HumanAIController humanAiController)) { continue; }
if (npc.Removed || npc.AIController is not HumanAIController humanAiController) { continue; }
foreach (var combatObjective in humanAiController.ObjectiveManager.GetActiveObjectives<AIObjectiveCombat>())
{
combatObjective.Abandon = true;
@@ -59,6 +59,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes)]
public bool ContinueConversation { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the event will not stop to wait for the conversation to be dismissed.")]
public bool ContinueAutomatically { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool IgnoreInterruptDistance { get; set; }
@@ -86,6 +89,8 @@ namespace Barotrauma
private bool interrupt;
private readonly XElement textElement;
public ConversationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
actionCount++;
@@ -93,15 +98,41 @@ namespace Barotrauma
Options = new List<SubactionGroup>();
foreach (var elem in element.Elements())
{
if (elem.Name.LocalName.Equals("option", StringComparison.InvariantCultureIgnoreCase))
if (elem.Name.LocalName.Equals("option", StringComparison.OrdinalIgnoreCase))
{
Options.Add(new SubactionGroup(ParentEvent, elem));
}
else if (elem.Name.LocalName.Equals("interrupt", StringComparison.InvariantCultureIgnoreCase))
else if (elem.Name.LocalName.Equals("interrupt", StringComparison.OrdinalIgnoreCase))
{
Interrupted = new SubactionGroup(ParentEvent, elem);
}
else if (elem.Name.LocalName.Equals("text", StringComparison.OrdinalIgnoreCase))
{
Text = elem.GetAttributeString("tag", string.Empty);
textElement = elem;
}
}
if (element.GetChildElement("Replace") != null)
{
DebugConsole.ThrowError(
$"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" +
$" - unrecognized child element \"Replace\".");
}
}
public LocalizedString GetDisplayText()
{
LocalizedString text = string.Empty;
if (textElement != null)
{
TextManager.ConstructDescription(ref text, textElement, ParentEvent.GetTextForReplacementElement);
}
else
{
text = TextManager.Get(Text).Fallback(Text);
}
return ParentEvent.ReplaceVariablesInEventText(text);
}
public override IEnumerable<EventAction> GetSubActions()
@@ -145,9 +176,14 @@ namespace Barotrauma
}
}
if (ContinueAutomatically && Options.None())
{
return dialogOpened;
}
if (selectedOption >= 0)
{
if (!Options.Any() || Options[selectedOption].IsFinished(ref goTo))
if (Options.None() || Options[selectedOption].IsFinished(ref goTo))
{
ResetSpeaker();
return true;

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