v1.6.17.0 (Unto the Breach update)

This commit is contained in:
Regalis11
2024-10-22 17:29:04 +03:00
parent e74b3cdb17
commit 6e6c17e100
417 changed files with 17166 additions and 5870 deletions
@@ -191,7 +191,7 @@ namespace Barotrauma
{
if (InDetectable) { return true; }
if (Entity == null) { return true; }
if (Level.Loaded != null && WorldPosition.Y > Level.Loaded.Size.Y)
if (Level.IsPositionAboveLevel(WorldPosition))
{
return true;
}
File diff suppressed because it is too large Load Diff
@@ -317,18 +317,21 @@ namespace Barotrauma
{
obstacleRaycastTimer = obstacleRaycastIntervalShort;
// Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs).
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
if (Submarine.MainSub != null)
{
if (connectedSub == Submarine.MainSub) { continue; }
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
{
PathSteering.CurrentPath.Unreachable = true;
break;
if (connectedSub == Submarine.MainSub) { continue; }
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
{
PathSteering.CurrentPath.Unreachable = true;
break;
}
}
}
}
}
}
}
@@ -801,36 +804,41 @@ namespace Barotrauma
if (isCarrying) { return; }
if (!ObjectiveManager.CurrentObjective.AllowAutomaticItemUnequipping || !ObjectiveManager.GetActiveObjective().AllowAutomaticItemUnequipping) { return; }
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
if (Character.Submarine?.TeamID == Character.TeamID && findItemState is FindItemState.None or FindItemState.OtherItem)
{
// Only unequip other items inside a friendly sub.
foreach (Item item in Character.HeldItems)
{
if (item == null || !item.IsInteractable(Character)) { continue; }
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, CharacterInventory.AnySlot) && Character.Submarine?.TeamID == Character.TeamID)
if (Character.TryPutItemInAnySlot(item)) { continue; }
if (Character.TryPutItemInBag(item)) { continue; }
if (item.HasTag(Tags.Weapon))
{
if (item.AllowedSlots.Contains(InvSlotType.Bag) && Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Bag })) { continue; }
findItemState = FindItemState.OtherItem;
if (FindSuitableContainer(item, out Item targetContainer))
// Don't decontain weapons, because it could be that we are holding a weapon that cannot be placed on back (if we have a toolbelt) nor in the any slot, such as an HMG.
// Could check that we only ignore weapons when we've had an order to find a weapon, but it could also be that we picked the weapon for self-defence, on ad-hoc basis.
// And I don't think it would make sense to decontain those weapons either.
continue;
}
findItemState = FindItemState.OtherItem;
if (FindSuitableContainer(item, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () =>
{
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () =>
{
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
item.Drop(Character);
HandleRelocation(item);
}
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
item.Drop(Character);
HandleRelocation(item);
}
}
}
@@ -842,7 +850,7 @@ namespace Barotrauma
public void HandleRelocation(Item item)
{
if (item.SpawnedInCurrentOutpost) { return; }
if (item.Submarine == null) { return; }
if (item.Submarine == null || Submarine.MainSub == null) { return; }
// Only affects bots in the player team
if (!Character.IsOnPlayerTeam) { return; }
// Don't relocate if the item is on a sub of the same team
@@ -869,6 +877,7 @@ namespace Barotrauma
if (item == null || item.Removed) { return; }
if (!itemsToRelocate.Contains(item)) { return; }
var mainSub = Submarine.MainSub;
if (mainSub == null) { return; }
Entity owner = item.GetRootInventoryOwner();
if (owner != null)
{
@@ -1295,7 +1304,13 @@ namespace Barotrauma
//if (Character.LastDamageSource == null) { return; }
//AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
}
if (realDamage <= 0 && (attacker.IsBot || attacker.TeamID == Character.TeamID))
bool sameTeam =
attacker.TeamID == Character.TeamID ||
// consider escorted characters to be in the same team (otherwise accidental damage or side-effects from healing trigger them too easily)
(attacker.TeamID == CharacterTeamType.Team1 && Character.IsEscorted);
if (realDamage <= 0 && (attacker.IsBot || sameTeam))
{
// Don't react to damage that is entirely based on karma penalties (medics, poisons etc), unless applier is player
return;
@@ -1307,9 +1322,9 @@ namespace Barotrauma
}
bool isAttackerInfected = false;
bool isAttackerFightingEnemy = false;
float minorDamageThreshold = 1;
float minorDamageThreshold = 5;
float majorDamageThreshold = 20;
if (attacker.TeamID == Character.TeamID && !attacker.IsInstigator)
if (sameTeam && !attacker.IsInstigator)
{
minorDamageThreshold = 10;
majorDamageThreshold = 40;
@@ -2168,15 +2183,15 @@ namespace Barotrauma
float fireFactor = 1;
if (!ignoreFire)
{
static float calculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
static float CalculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
// Even the smallest fire reduces the safety by 50%
float fire = visibleHulls == null ? calculateFire(hull) : visibleHulls.Sum(h => calculateFire(h));
float fire = visibleHulls?.Sum(CalculateFire) ?? CalculateFire(hull);
fireFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1));
}
float enemyFactor = 1;
if (!ignoreEnemies)
{
int enemyCount = 0;
int enemyCount = 0;
foreach (Character c in Character.CharacterList)
{
if (visibleHulls == null)
@@ -2476,7 +2491,7 @@ namespace Barotrauma
{
other = null;
if (target?.Item == null) { return false; }
bool isOrder = IsOrderedToOperateThis(Character.AIController);
bool isOrder = IsOrderedToOperateTarget(this);
foreach (Character c in Character.CharacterList)
{
if (!IsActive(c)) { continue; }
@@ -2491,14 +2506,14 @@ namespace Barotrauma
break;
}
}
else if (c.AIController is HumanAIController operatingAI)
else if (c.AIController is HumanAIController otherAI)
{
if (operatingAI.ObjectiveManager.Objectives.None(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item))
if (otherAI.ObjectiveManager.Objectives.None(o => o is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item))
{
// Not targeting the same item.
continue;
}
bool isTargetOrdered = IsOrderedToOperateThis(c.AIController);
bool isTargetOrdered = IsOrderedToOperateTarget(otherAI);
if (!isOrder && isTargetOrdered)
{
// If the other bot is ordered to operate the item, let him do it, unless we are ordered too
@@ -2514,15 +2529,15 @@ namespace Barotrauma
}
else
{
if (!isTargetOrdered && operatingAI.ObjectiveManager.CurrentOrder != operatingAI.ObjectiveManager.CurrentObjective)
if (!IsOperatingTarget(otherAI))
{
// The other bot is ordered to do something else
// The other bot is doing something else -> stick to the target.
continue;
}
if (target is Steering)
{
// Steering is hard-coded -> cannot use the required skills collection defined in the xml
if (Character.GetSkillLevel("helm") <= c.GetSkillLevel("helm"))
if (Character.GetSkillLevel(Tags.HelmSkill) <= c.GetSkillLevel(Tags.HelmSkill))
{
other = c;
break;
@@ -2538,7 +2553,8 @@ namespace Barotrauma
}
}
return other != null;
bool IsOrderedToOperateThis(AIController ai) => ai is HumanAIController humanAI && humanAI.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item;
bool IsOrderedToOperateTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.Component.Item == target.Item;
bool IsOperatingTarget(HumanAIController ai) => ai.ObjectiveManager.CurrentObjective is AIObjectiveOperateItem operateObjective && operateObjective.Component.Item == target.Item;
}
public bool IsItemRepairedByAnother(Item target, out Character other)
@@ -320,8 +320,7 @@ namespace Barotrauma
Vector2 pos = host.WorldPosition;
Vector2 diff = currentPath.CurrentNode.WorldPosition - pos;
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands;
bool canClimb = character.CanClimb;
Ladder currentLadder = GetCurrentLadder();
Ladder nextLadder = GetNextLadder();
var ladders = currentLadder ?? nextLadder;
@@ -559,26 +558,41 @@ namespace Barotrauma
}
else
{
// We'll want this to run each time, because the delegate is used to find a valid button component.
bool canAccessButtons = false;
foreach (var button in door.Item.GetConnectedComponents<Controller>(true, connectionFilter: c => c.Name == "toggle" || c.Name == "set_state"))
bool buttonsFound = false;
// Check wired controllers (e.g. buttons)
// Always run the buttonFilter delegate (inside CanAccessButton method), if defined, because it's used for find a valid controller component that can be used for closing the door, when needed.
foreach (Controller button in door.Item.GetConnectedComponents<Controller>(recursive: true, connectionFilter: c => c.Name is "toggle" or "set_state"))
{
if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)))
buttonsFound = true;
if (CanAccessButton(button))
{
canAccessButtons = true;
}
}
foreach (var linked in door.Item.linkedTo)
if (!canAccessButtons)
{
if (linked is not Item linkedItem) { continue; }
var button = linkedItem.GetComponent<Controller>();
if (button == null) { continue; }
if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)))
// Check linked controllers (more complex circuits)
foreach (MapEntity linked in door.Item.linkedTo)
{
canAccessButtons = true;
}
}
return canAccessButtons || door.IsOpen || ShouldBreakDoor(door);
if (linked is not Item linkedItem) { continue; }
var button = linkedItem.GetComponent<Controller>();
if (button == null) { continue; }
buttonsFound = true;
if (CanAccessButton(button))
{
canAccessButtons = true;
}
}
}
if (door.IsOpen || ShouldBreakDoor(door))
{
return true;
}
// If no buttons were found, just trust it if we should have the access to the door. Could be there's some other mechanism controlling the door.
return buttonsFound ? canAccessButtons : door.HasAccess(character);
bool CanAccessButton(Controller button) => button.HasAccess(character) && (buttonFilter == null || buttonFilter(button));
}
}
@@ -796,10 +810,9 @@ namespace Barotrauma
float? penalty = GetSingleNodePenalty(nextNode);
if (penalty == null) { return null; }
bool nextNodeAboveWaterLevel = nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y;
//non-humanoids can't climb up ladders
if (!(character.AnimController is HumanoidAnimController))
if (!character.CanClimb)
{
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands)||
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands) ||
(nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
nextNodeAboveWaterLevel)) //upper node not underwater
{
@@ -847,7 +860,7 @@ namespace Barotrauma
if (!node.Waypoint.IsTraversable) { return null; }
if (node.IsBlocked()) { return null; }
float penalty = 0.0f;
if (node.Waypoint.ConnectedGap != null && node.Waypoint.ConnectedGap.Open < 0.9f)
if (node.Waypoint.ConnectedGap is { Open: < 0.9f })
{
var door = node.Waypoint.ConnectedDoor;
if (door == null)
@@ -858,19 +871,19 @@ namespace Barotrauma
{
if (!CanAccessDoor(door, button =>
{
// Ignore buttons that are on the wrong side of the door
// Ignore buttons that are on the wrong side of the door, unless there's a motion sensor connected to the door, which can be triggered by the character.
if (door.IsHorizontal)
{
if (Math.Sign(button.Item.WorldPosition.Y - door.Item.WorldPosition.Y) != Math.Sign(character.WorldPosition.Y - door.Item.WorldPosition.Y))
{
return false;
return door.Item.GetDirectlyConnectedComponent<MotionSensor>() is MotionSensor ms && ms.TriggersOn(character);
}
}
else
{
if (Math.Sign(button.Item.WorldPosition.X - door.Item.WorldPosition.X) != Math.Sign(character.WorldPosition.X - door.Item.WorldPosition.X))
{
return false;
return door.Item.GetDirectlyConnectedComponent<MotionSensor>() is MotionSensor ms && ms.TriggersOn(character);
}
}
return true;
@@ -308,7 +308,7 @@ namespace Barotrauma
if (enemyAI.AttackLimb == null) { break; }
if (targetBody == null) { break; }
if (IsAttached && AttachJoints[0].BodyB == targetBody) { break; }
Vector2 referencePos = TargetCharacter != null ? TargetCharacter.WorldPosition : ConvertUnits.ToDisplayUnits(transformedAttachPos);
Vector2 referencePos = TargetCharacter?.WorldPosition ?? ConvertUnits.ToDisplayUnits(transformedAttachPos);
if (Vector2.DistanceSquared(referencePos, enemyAI.AttackLimb.WorldPosition) < enemyAI.AttackLimb.attack.DamageRange * enemyAI.AttackLimb.attack.DamageRange)
{
AttachToBody(transformedAttachPos);
@@ -513,18 +513,19 @@ namespace Barotrauma
/// </summary>
private bool Check()
{
if (isCompleted) { return true; }
if (AbortCondition != null && AbortCondition(this))
{
Abandon = true;
return false;
}
return CheckObjectiveSpecific();
return CheckObjectiveState();
}
/// <summary>
/// Should return whether the objective is completed or not.
/// </summary>
protected abstract bool CheckObjectiveSpecific();
protected abstract bool CheckObjectiveState();
private bool CheckState()
{
@@ -574,8 +575,6 @@ namespace Barotrauma
}
}
public virtual void SpeakAfterOrderReceived() { }
protected static bool CanPutInInventory(Character character, Item item, bool allowWearing)
{
if (item == null) { return false; }
@@ -45,7 +45,7 @@ namespace Barotrauma
InitTimers();
}
protected override bool CheckObjectiveSpecific() => false;
protected override bool CheckObjectiveState() => false;
protected override float GetPriority()
{
@@ -117,7 +117,7 @@ namespace Barotrauma
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
protected override bool CheckObjectiveSpecific()
protected override bool CheckObjectiveState()
{
if (item.IgnoreByAI(character) || Item.DeconstructItems.Contains(item))
{
@@ -81,6 +81,8 @@ namespace Barotrauma
public static bool IsItemInsideValidSubmarine(Item item, Character character)
{
if (item == null || item.Removed) { return false; }
if (character == null || character.Removed) { return false; }
if (item.CurrentHull == null) { return false; }
if (item.Submarine == null) { return false; }
if (item.Submarine.TeamID != character.TeamID) { return false; }
@@ -257,7 +257,7 @@ namespace Barotrauma
}
}
protected override bool CheckObjectiveSpecific()
protected override bool CheckObjectiveState()
{
if (character.Submarine is { TeamID: CharacterTeamType.FriendlyNPC } && character.Submarine == Enemy.Submarine)
{
@@ -898,23 +898,13 @@ namespace Barotrauma
}
}
}
private void Unequip()
private void UnequipWeapon()
{
if (!character.LockHands && character.HeldItems.Contains(Weapon))
{
if (!Weapon.AllowedSlots.Contains(InvSlotType.Any) || !character.Inventory.TryPutItem(Weapon, character, new List<InvSlotType>() { InvSlotType.Any }))
{
if (Weapon.AllowedSlots.Contains(InvSlotType.Bag))
{
if (character.Inventory.TryPutItem(Weapon, character, new List<InvSlotType>() { InvSlotType.Bag }))
{
return;
}
}
Weapon.Drop(character);
}
}
if (Weapon == null) { return; }
if (character.LockHands) { return; }
if (character.HeldItems.Contains(Weapon)) { return; }
character.Unequip(Weapon);
}
private bool Equip()
@@ -929,7 +919,15 @@ namespace Barotrauma
ClearInputs();
Weapon.TryInteract(character, forceSelectKey: true);
var slots = Weapon.AllowedSlots.Where(CharacterInventory.IsHandSlotType);
if (character.Inventory.TryPutItem(Weapon, character, slots))
bool successfullyEquipped = character.TryPutItem(Weapon, slots);
if (!successfullyEquipped && character.HasHandsFull(out (Item leftHandItem, Item rightHandItem) items))
{
// Unequip and try again.
character.Unequip(items.leftHandItem);
character.Unequip(items.rightHandItem);
successfullyEquipped = character.TryPutItem(Weapon, slots);
}
if (successfullyEquipped)
{
SetAimTimer(Rand.Range(0.2f, 0.4f) / AimSpeed);
SetReloadTime(WeaponComponent);
@@ -1322,8 +1320,6 @@ namespace Barotrauma
aimTimer -= deltaTime;
return;
}
if (reloadTimer > 0) { return; }
if (holdFireCondition != null && holdFireCondition()) { return; }
sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
distanceTimer = DistanceCheckInterval;
if (WeaponComponent is MeleeWeapon meleeWeapon)
@@ -1353,9 +1349,11 @@ namespace Barotrauma
if (closeEnough && Enemy.WorldPosition.Y < character.WorldPosition.Y && yDiff > 25)
{
// The target is probably knocked down? -> try to reach it by crouching.
HumanAIController.AnimController.Crouching = true;
HumanAIController.AnimController.Crouch();
}
}
if (reloadTimer > 0) { return; }
if (holdFireCondition != null && holdFireCondition()) { return; }
if (closeEnough)
{
UseWeapon(deltaTime);
@@ -1371,7 +1369,8 @@ namespace Barotrauma
{
if (WeaponComponent is RepairTool repairTool)
{
if (sqrDistance > repairTool.Range * repairTool.Range) { return; }
float reach = AIObjectiveFixLeak.CalculateReach(repairTool, character);
if (sqrDistance > reach * reach) { return; }
}
float aimFactor = MathHelper.PiOver2 * (1 - AimAccuracy);
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.WorldPosition - Weapon.WorldPosition) < MathHelper.PiOver4 + aimFactor)
@@ -1420,11 +1419,8 @@ namespace Barotrauma
break;
}
case MeleeWeapon mw:
{
if (character.AnimController is HumanoidAnimController { Crouching: false })
{
reloadTime = mw.Reload;
}
{
reloadTime = mw.Reload;
break;
}
}
@@ -1485,7 +1481,7 @@ namespace Barotrauma
}
if (ShouldUnequipWeapon)
{
Unequip();
UnequipWeapon();
}
SteeringManager?.Reset();
}
@@ -1495,7 +1491,7 @@ namespace Barotrauma
base.OnAbandon();
if (ShouldUnequipWeapon)
{
Unequip();
UnequipWeapon();
}
SteeringManager?.Reset();
}
@@ -77,9 +77,8 @@ namespace Barotrauma
this.container = container;
}
protected override bool CheckObjectiveSpecific()
protected override bool CheckObjectiveState()
{
if (IsCompleted) { return true; }
if (container?.Item == null || !container.Item.HasAccess(character))
{
Abandon = true;
@@ -15,6 +15,7 @@ namespace Barotrauma
private Deconstructor deconstructor;
private AIObjectiveDecontainItem decontainObjective;
private AIObjectiveGoTo gotoObjective;
public AIObjectiveDeconstructItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -45,14 +46,24 @@ namespace Barotrauma
},
onCompleted: () =>
{
StartDeconstructor();
//make sure the item gets moved to the main sub if the crew leaves while a bot is deconstructing something in the outpost
if (deconstructor.Item.Submarine is { Info.IsOutpost: true })
if (character.CanInteractWith(deconstructor.Item))
{
HumanAIController.HandleRelocation(Item);
deconstructor.RelocateOutputToMainSub = true;
StartDeconstruction();
}
else
{
TryAddSubObjective(ref gotoObjective,
constructor: () => new AIObjectiveGoTo(Item, character, objectiveManager, priorityModifier: PriorityModifier),
onCompleted: () =>
{
StartDeconstruction();
RemoveSubObjective(ref gotoObjective);
},
onAbandon: () =>
{
Abandon = true;
});
}
IsCompleted = true;
RemoveSubObjective(ref decontainObjective);
},
onAbandon: () =>
@@ -61,6 +72,18 @@ namespace Barotrauma
});
}
private void StartDeconstruction()
{
StartDeconstructor();
//make sure the item gets moved to the main sub if the crew leaves while a bot is deconstructing something in the outpost
if (deconstructor.Item.Submarine is { Info.IsOutpost: true })
{
HumanAIController.HandleRelocation(Item);
deconstructor.RelocateOutputToMainSub = true;
}
IsCompleted = true;
}
private Deconstructor FindDeconstructor()
{
Deconstructor closestDeconstructor = null;
@@ -86,7 +109,7 @@ namespace Barotrauma
deconstructor.SetActive(active: true, user: character, createNetworkEvent: true);
}
protected override bool CheckObjectiveSpecific()
protected override bool CheckObjectiveState()
{
if (Item.IgnoreByAI(character))
{
@@ -59,12 +59,14 @@ namespace Barotrauma
protected override bool IsValidTarget(Item target)
{
if (target == null || target.Removed) { return false; }
// If the target was selected as a valid target, we'll have to accept it so that the objective can be completed.
// The validity changes when a character picks the item up.
if (!IsValidTarget(target, character, checkInventory: true))
{
return Objectives.ContainsKey(target) && AIObjectiveCleanupItems.IsItemInsideValidSubmarine(target, character);
}
//note that the item can be outside hulls and still be a valid target - it can be in the character's inventory
if (target.CurrentHull != null && target.CurrentHull.FireSources.Count > 0) { return false; }
foreach (Character c in Character.CharacterList)
@@ -96,7 +98,7 @@ namespace Barotrauma
private static bool IsValidTarget(Item item, Character character, bool checkInventory)
{
if (item == null) { return false; }
if (item == null || item.Removed) { return false; }
if (item.GetRootInventoryOwner() == character) { return true; }
return AIObjectiveCleanupItems.IsValidTarget(
item,
@@ -71,7 +71,7 @@ namespace Barotrauma
this.targetContainer = targetContainer;
}
protected override bool CheckObjectiveSpecific() => IsCompleted;
protected override bool CheckObjectiveState() => IsCompleted;
protected override void Act(float deltaTime)
{
@@ -28,7 +28,7 @@ namespace Barotrauma
}
public override bool CanBeCompleted => true;
protected override bool CheckObjectiveSpecific() => false;
protected override bool CheckObjectiveState() => false;
// escape timer is set to 60 by default to allow players to locate prisoners in time
private float escapeTimer = 60f;
@@ -76,7 +76,7 @@ namespace Barotrauma
return Priority;
}
protected override bool CheckObjectiveSpecific() => targetHull.FireSources.None();
protected override bool CheckObjectiveState() => targetHull.FireSources.None();
private float sinTime;
protected override void Act(float deltaTime)
@@ -23,7 +23,7 @@ namespace Barotrauma
public const float MIN_OXYGEN = 10;
protected override bool CheckObjectiveSpecific() =>
protected override bool CheckObjectiveState() =>
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)
@@ -39,83 +39,98 @@ namespace Barotrauma
TrySetTargetItem(character.Inventory.FindItem(
it => it.HasTag(Tags.HeavyDivingGear) && IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true));
}
if (targetItem == null ||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) &&
targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it)))
bool findDivingGear = targetItem == null ||
(!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) && targetItem.ContainedItems.Any(IsSuitableContainedOxygenSource));
if (findDivingGear)
{
bool mustFindMorePressureProtection =
!objectiveManager.FailedToFindDivingGearForDepth &&
character.Inventory.FindItem(
it => it.HasTag(Tags.HeavyDivingGear) && !IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true) != null;
TryAddSubObjective(ref getDivingGear, () =>
bool mustFindMorePressureProtection = !objectiveManager.FailedToFindDivingGearForDepth &&
character.Inventory.FindItem(it => it.HasTag(Tags.HeavyDivingGear) && !IsSuitablePressureProtection(it, Tags.HeavyDivingGear, character), recursive: true) != null;
if (gearTag == Tags.LightDivingGear)
{
if (targetItem == null && character.IsOnPlayerTeam)
if (character.GetEquippedItem(Tags.HeavyDivingGear, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes) is Item divingSuit && divingSuit.ContainedItems.None(IsSuitableContainedOxygenSource))
{
character.Speak(TextManager.Get("DialogGetDivingGear").Value, null, 0.0f, "getdivinggear".ToIdentifier(), 30.0f);
// A special case: we are already wearing a suit without enough oxygen, but seeking for a mask, because a suit is not really needed.
// This would result into wearing boh the mask and the suit (because the suit shouldn't be unequipped in this situation), which is a bit weird and also suboptimal, because the mask uses the oxygen 2x faster.
// So, let's target the diving suit and try to find oxygen instead.
targetItem = divingSuit;
findDivingGear = false;
}
var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
}
if (findDivingGear)
{
TryAddSubObjective(ref getDivingGear, () =>
{
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head,
Wear = true
};
if (gearTag == Tags.HeavyDivingGear)
{
if (mustFindMorePressureProtection)
if (targetItem == null && character.IsOnPlayerTeam)
{
//if we're looking for a suit specifically because the current suit isn't enough,
//let's ignore unsuitable suits altogether...
getItemObjective.ItemFilter = it => IsSuitablePressureProtection(it, gearTag, character);
character.Speak(TextManager.Get("DialogGetDivingGear").Value, null, 0.0f, "getdivinggear".ToIdentifier(), 30.0f);
}
else
var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
{
//...Otherwise it's fine to give a very small priority
//to inadequate suits (a suit not adequate for the depth is better than no suit)
getItemObjective.GetItemPriority = it => IsSuitablePressureProtection(it, gearTag, character) ? 1000.0f : 1.0f;
}
getItemObjective.GetItemPriority = it =>
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head,
Wear = true
};
if (gearTag == Tags.HeavyDivingGear)
{
if (IsSuitablePressureProtection(it, gearTag, character))
if (mustFindMorePressureProtection)
{
return 1000.0f;
//if we're looking for a suit specifically because the current suit isn't enough,
//let's ignore unsuitable suits altogether...
getItemObjective.ItemFilter = it => IsSuitablePressureProtection(it, gearTag, character);
}
else
{
//if we're looking for a suit specifically because the current suit isn't enough,
//let's ignore unsuitable suits altogether. Otherwise it's fine to give a very small priority
//...Otherwise it's fine to give a very small priority
//to inadequate suits (a suit not adequate for the depth is better than no suit)
return mustFindMorePressureProtection ? 0.0f : 1.0f;
getItemObjective.GetItemPriority = it => IsSuitablePressureProtection(it, gearTag, character) ? 1000.0f : 1.0f;
}
};
}
return getItemObjective;
},
onAbandon: () =>
{
if (mustFindMorePressureProtection) { objectiveManager.FailedToFindDivingGearForDepth = true; }
Abandon = true;
},
onCompleted: () =>
{
RemoveSubObjective(ref getDivingGear);
if (gearTag == Tags.HeavyDivingGear && HumanAIController.HasItem(character, Tags.LightDivingGear, out IEnumerable<Item> masks, requireEquipped: true))
{
foreach (Item mask in masks)
{
if (mask != targetItem)
getItemObjective.GetItemPriority = it =>
{
character.Inventory.TryPutItem(mask, character, CharacterInventory.AnySlot);
if (IsSuitablePressureProtection(it, gearTag, character))
{
return 1000.0f;
}
else
{
//if we're looking for a suit specifically because the current suit isn't enough,
//let's ignore unsuitable suits altogether. Otherwise it's fine to give a very small priority
//to inadequate suits (a suit not adequate for the depth is better than no suit)
return mustFindMorePressureProtection ? 0.0f : 1.0f;
}
};
}
return getItemObjective;
},
onAbandon: () =>
{
if (mustFindMorePressureProtection) { objectiveManager.FailedToFindDivingGearForDepth = true; }
Abandon = true;
},
onCompleted: () =>
{
RemoveSubObjective(ref getDivingGear);
if (gearTag == Tags.HeavyDivingGear && HumanAIController.HasItem(character, Tags.LightDivingGear, out IEnumerable<Item> masks, requireEquipped: true))
{
foreach (Item mask in masks)
{
if (mask != targetItem)
{
character.Inventory.TryPutItem(mask, character, CharacterInventory.AnySlot);
}
}
}
}
});
});
}
}
else
if (!findDivingGear)
{
float min = GetMinOxygen(character);
if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => IsSuitableContainedOxygenSource(it)))
if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(IsSuitableContainedOxygenSource))
{
TryAddSubObjective(ref getOxygen, () =>
{
@@ -226,14 +241,7 @@ namespace Barotrauma
{
if (targetItem == item) { return; }
targetItem = item;
if (targetItem != null)
{
oxygenSourceSlotIndex = targetItem.GetComponent<ItemContainer>()?.FindSuitableSubContainerIndex(Tags.OxygenSource);
}
else
{
oxygenSourceSlotIndex = null;
}
oxygenSourceSlotIndex = targetItem?.GetComponent<ItemContainer>()?.FindSuitableSubContainerIndex(Tags.OxygenSource);
}
public override void Reset()
@@ -3,6 +3,7 @@ using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Barotrauma
@@ -31,7 +32,7 @@ namespace Barotrauma
public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
protected override bool CheckObjectiveSpecific() => false;
protected override bool CheckObjectiveState() => false;
public override bool CanBeCompleted => true;
private bool resetPriority;
@@ -339,6 +340,10 @@ namespace Barotrauma
float bestHullValue = 0;
bool bestHullIsAirlock = false;
Hull potentialBestHull;
#if DEBUG
private readonly Stopwatch stopWatch = new Stopwatch();
#endif
/// <summary>
/// Tries to find the best (safe, nearby) hull the character can find a path to.
@@ -353,6 +358,9 @@ namespace Barotrauma
bestHullIsAirlock = false;
hulls.Clear();
var connectedSubs = character.Submarine?.GetConnectedSubs();
#if DEBUG
stopWatch.Restart();
#endif
foreach (Hull hull in Hull.HullList)
{
if (hull.Submarine == null) { continue; }
@@ -363,25 +371,66 @@ namespace Barotrauma
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
if (connectedSubs != null && !connectedSubs.Contains(hull.Submarine)) { continue; }
//sort the hulls based on distance and which sub they're in
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
//path calculations, only to discard all of them when going through the hulls in the outpost)
float hullSuitability = EstimateHullSuitability(character, hull);
if (hulls.None())
{
hulls.Add(hull);
}
else
{
//sort the hulls first based on distance and a rough suitability estimation
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
//path calculations, only to discard all of them when going through the hulls in the outpost)
bool addLast = true;
float hullSuitability = EstimateHullSuitability(hull);
for (int i = 0; i < hulls.Count; i++)
{
if (hullSuitability > EstimateHullSuitability(character, hulls[i]))
Hull otherHull = hulls[i];
float otherHullSuitability = EstimateHullSuitability(otherHull);
if (hullSuitability > otherHullSuitability)
{
hulls.Insert(i, hull);
addLast = false;
break;
}
}
if (addLast)
{
hulls.Add(hull);
}
}
float EstimateHullSuitability(Hull h)
{
float distX = Math.Abs(h.WorldPosition.X - character.WorldPosition.X);
float distY = Math.Abs(h.WorldPosition.Y - character.WorldPosition.Y);
if (character.CurrentHull != null)
{
distY *= 3;
}
float dist = distX + distY;
float suitability = -dist;
const float suitabilityReduction = 10000.0f;
if (h.Submarine != character.Submarine)
{
suitability -= suitabilityReduction;
}
if (character.CurrentHull != null)
{
if (h.AvoidStaying)
{
suitability -= suitabilityReduction;
}
if (HumanAIController.UnsafeHulls.Contains(h))
{
suitability -= suitabilityReduction;
}
if (HumanAIController.NeedsDivingGear(h, out _))
{
suitability -= suitabilityReduction;
}
}
return suitability;
}
}
if (hulls.None())
@@ -390,19 +439,10 @@ namespace Barotrauma
return HullSearchStatus.Finished;
}
hullSearchIndex = 0;
}
static float EstimateHullSuitability(Character character, Hull hull)
{
float dist =
Math.Abs(hull.WorldPosition.X - character.WorldPosition.X) +
Math.Abs(hull.WorldPosition.Y - character.WorldPosition.Y) * 3;
float suitability = -dist;
if (hull.Submarine != character.Submarine)
{
suitability -= 10000.0f;
}
return suitability;
#if DEBUG
stopWatch.Stop();
DebugConsole.NewMessage($"({character.DisplayName}) Sorted hulls by suitability in {stopWatch.ElapsedMilliseconds} ms", debugOnly: true);
#endif
}
Hull potentialHull = hulls[hullSearchIndex];
@@ -420,7 +460,7 @@ namespace Barotrauma
if (hullSafety > bestHullValue)
{
//avoid airlock modules if not allowed to change the sub
if (allowChangingSubmarine || !potentialHull.OutpostModuleTags.Any(t => t == "airlock"))
if (allowChangingSubmarine || potentialHull.OutpostModuleTags.All(t => t != "airlock"))
{
// Don't allow to go outside if not already outside.
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(potentialHull), character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
@@ -176,6 +176,8 @@ namespace Barotrauma
//only player's crew can steal, ignore other teams
if (!target.IsOnPlayerTeam) { return false; }
if (target.IsHandcuffed) { return false; }
//ignore thieves in the same team
if (character.OriginalTeamID == target.TeamID || character.TeamID == target.TeamID) { return false; }
// Ignore targets that are climbing, because might need to use ladders to get to them.
if (target.IsClimbing) { return false; }
if (HumanAIController.IsTrueForAnyBotInTheCrew(bot =>
@@ -31,7 +31,7 @@ namespace Barotrauma
this.isPriority = isPriority;
}
protected override bool CheckObjectiveSpecific() => Leak.Open <= 0 || Leak.Removed;
protected override bool CheckObjectiveState() => Leak.Open <= 0 || Leak.Removed;
protected override float GetPriority()
{
@@ -166,7 +166,7 @@ namespace Barotrauma
// TODO: use the collider size/reach?
if (!character.AnimController.InWater && Math.Abs(toLeak.X) < 100 && toLeak.Y < 0.0f && toLeak.Y > -150)
{
HumanAIController.AnimController.Crouching = true;
HumanAIController.AnimController.Crouch();
}
float reach = CalculateReach(repairTool, character);
bool canOperate = toLeak.LengthSquared() < reach * reach;
@@ -180,7 +180,7 @@ namespace Barotrauma
onAbandon: () => Abandon = true,
onCompleted: () =>
{
if (CheckObjectiveSpecific()) { IsCompleted = true; }
if (CheckObjectiveState()) { IsCompleted = true; }
else
{
// Failed to operate. Probably too far.
@@ -202,11 +202,11 @@ namespace Barotrauma
endNodeFilter = IsSuitableEndNode,
// The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue)
// Only report about contextual targets.
SpeakCannotReachCondition = () => isPriority && !CheckObjectiveSpecific()
SpeakCannotReachCondition = () => isPriority && !CheckObjectiveState()
},
onAbandon: () =>
{
if (CheckObjectiveSpecific()) { IsCompleted = true; }
if (CheckObjectiveState()) { IsCompleted = true; }
else if ((Leak.WorldPosition - character.AnimController.AimSourceWorldPos).LengthSquared() > MathUtils.Pow(reach * 2, 2))
{
// Too far
@@ -658,9 +658,8 @@ namespace Barotrauma
return bestItem;
}
protected override bool CheckObjectiveSpecific()
protected override bool CheckObjectiveState()
{
if (IsCompleted) { return true; }
if (targetItem == null)
{
// Not yet ready
@@ -44,7 +44,7 @@ namespace Barotrauma
ignoredTags = AIObjectiveGetItem.ParseIgnoredTags(identifiersOrTags).ToImmutableHashSet();
}
protected override bool CheckObjectiveSpecific() => subObjectivesCreated && subObjectives.None();
protected override bool CheckObjectiveState() => subObjectivesCreated && subObjectives.None();
protected override void Act(float deltaTime)
{
@@ -56,7 +56,7 @@ namespace Barotrauma
AIObjectiveGetItem? getItem = null;
TryAddSubObjective(ref getItem, () =>
{
var getItem = new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
getItem = new AIObjectiveGetItem(character, tag, objectiveManager, Equip, CheckInventory && count <= 1)
{
AllowVariants = AllowVariants,
Wear = Wear,
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
namespace Barotrauma
{
@@ -95,6 +96,9 @@ namespace Barotrauma
protected override bool AllowInAnySub => true;
public Identifier DialogueIdentifier { get; set; } = "dialogcannotreachtarget".ToIdentifier();
private readonly Identifier ExoSuitRefuel = "dialog.exosuit.refuel".ToIdentifier();
private readonly Identifier ExoSuitOutOfFuel = "dialog.exosuit.outoffuel".ToIdentifier();
public LocalizedString TargetName { get; set; }
public ISpatialEntity Target { get; private set; }
@@ -194,6 +198,43 @@ namespace Barotrauma
Abandon = true;
return;
}
if (checkExoSuitTimer <= 0)
{
checkExoSuitTimer = CheckExoSuitTime * Rand.Range(0.9f, 1.1f);
if (character.GetEquippedItem(Tags.PoweredDivingSuit, InvSlotType.OuterClothes) is { OwnInventory: Inventory exoSuitInventory } exoSuit &&
exoSuit.GetComponent<Powered>() is not { HasPower: true })
{
if (HumanAIController.HasItem(character, Tags.DivingSuitFuel, out IEnumerable<Item> fuelRods, conditionPercentage: 1, recursive: true))
{
// Try to switch the fuel sources
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get(ExoSuitRefuel).Value, minDurationBetweenSimilar: 10f, identifier: ExoSuitRefuel);
}
// Have to copy the list, because it's modified when we unequip the item.
foreach (Item containedItem in exoSuit.ContainedItems.ToList())
{
if (containedItem.HasTag(Tags.DivingSuitFuel) && containedItem.Condition <= 0)
{
character.Unequip(containedItem);
}
}
// Refuel
// The information about the target slot is defined in a status effect. We could parse it, but let's keep it simple and just presume that the target slot is the second slot, as it the case with the vanilla exosuits.
const int targetSlot = 1;
Item fuelRod = fuelRods.MaxBy(b => b.Condition);
exoSuitInventory.TryPutItem(fuelRod, targetSlot, allowSwapping: true, allowCombine: true, user: character);
}
else if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get(ExoSuitOutOfFuel).Value, minDurationBetweenSimilar: 30.0f, identifier: ExoSuitOutOfFuel);
}
}
}
else
{
checkExoSuitTimer -= deltaTime;
}
if (Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy))
{
// Wait
@@ -353,9 +394,9 @@ namespace Barotrauma
}
else
{
// Try again without requiring the diving suit
// Try again without requiring the diving suit (or mask)
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: !tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
@@ -442,7 +483,7 @@ namespace Barotrauma
if (checkScooterTimer <= 0)
{
useScooter = false;
checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f);
checkScooterTimer = CheckScooterTime * Rand.Range(0.9f, 1.1f);
Item scooter = null;
bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(Tags.Scooter, allowBroken: false);
if (!shouldUseScooter)
@@ -465,24 +506,25 @@ namespace Barotrauma
}
else if (shouldUseScooter)
{
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
bool handsFull =
(leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem) && !character.Inventory.TryPutItem(leftHandItem, character, InvSlotType.Bag.ToEnumerable())) ||
(rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem) && !character.Inventory.TryPutItem(rightHandItem, character, InvSlotType.Bag.ToEnumerable()));
if (!handsFull)
bool hasHandsFull = character.HasHandsFull(out (Item leftHandItem, Item rightHandItem) items);
if (hasHandsFull)
{
hasHandsFull = !character.TryPutItemInAnySlot(items.leftHandItem) &&
!character.TryPutItemInAnySlot(items.rightHandItem) &&
!character.TryPutItemInBag(items.leftHandItem) &&
!character.TryPutItemInBag(items.rightHandItem);
}
if (!hasHandsFull)
{
bool hasBattery = false;
if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> nonEquippedScooters, containedTag: Tags.MobileBattery, conditionPercentage: 1, requireEquipped: false))
if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> nonEquippedScootersWithBattery, containedTag: Tags.MobileBattery, conditionPercentage: 1, requireEquipped: false))
{
// Non-equipped scooter with a battery
scooter = nonEquippedScooters.FirstOrDefault();
scooter = nonEquippedScootersWithBattery.FirstOrDefault();
hasBattery = true;
}
else if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
else if (HumanAIController.HasItem(character, Tags.Scooter, out IEnumerable<Item> nonEquippedScootersWithoutBattery, requireEquipped: false))
{
// Non-equipped scooter without a battery
scooter = _nonEquippedScooters.FirstOrDefault();
scooter = nonEquippedScootersWithoutBattery.FirstOrDefault();
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
hasBattery = HumanAIController.HasItem(character, Tags.MobileBattery, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
}
@@ -518,8 +560,7 @@ namespace Barotrauma
}
if (!useScooter)
{
// Unequip
character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot);
character.TryPutItemInAnySlot(scooter);
}
}
}
@@ -663,7 +704,10 @@ namespace Barotrauma
private bool useScooter;
private float checkScooterTimer;
private readonly float checkScooterTime = 0.5f;
private const float CheckScooterTime = 0.5f;
private float checkExoSuitTimer;
private const float CheckExoSuitTime = 2.0f;
public Hull GetTargetHull() => GetTargetHull(Target);
@@ -764,9 +808,8 @@ namespace Barotrauma
}
}
protected override bool CheckObjectiveSpecific()
protected override bool CheckObjectiveState()
{
if (IsCompleted) { return true; }
// First check the distance and then if can interact (heaviest)
if (Target == null)
{
@@ -88,7 +88,7 @@ namespace Barotrauma
CalculatePriority();
}
protected override bool CheckObjectiveSpecific() => false;
protected override bool CheckObjectiveState() => false;
public override bool CanBeCompleted => true;
public readonly HashSet<Identifier> PreferredOutpostModuleTypes = new HashSet<Identifier>();
@@ -158,8 +158,17 @@ namespace Barotrauma
{
character.DeselectCharacter();
}
character.SelectedItem = null;
if (character.SelectedItem != null)
{
if (character.SelectedItem.Prefab.AllowDeselectWhenIdling)
{
character.SelectedItem = null;
}
else
{
return;
}
}
if (!character.IsClimbing)
{
@@ -489,27 +498,26 @@ namespace Barotrauma
if (checkItemsTimer <= 0)
{
checkItemsTimer = checkItemsInterval * Rand.Range(0.9f, 1.1f);
var hull = character.CurrentHull;
if (hull != null)
if (character.Submarine is not Submarine sub) { return; }
if (sub.TeamID != character.TeamID) { return; }
if (character.CurrentHull is not Hull currentHull) { return; }
itemsToClean.Clear();
foreach (Item item in Item.CleanableItems)
{
itemsToClean.Clear();
foreach (Item item in Item.CleanableItems)
if (item.CurrentHull != currentHull) { continue; }
if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true, allowUnloading: false) && !ignoredItems.Contains(item))
{
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true, allowUnloading: false) && !ignoredItems.Contains(item))
{
itemsToClean.Add(item);
}
itemsToClean.Add(item);
}
if (itemsToClean.Any())
}
if (itemsToClean.Any())
{
var targetItem = itemsToClean.MinBy(i => Math.Abs(character.WorldPosition.X - i.WorldPosition.X));
if (targetItem != null)
{
var targetItem = itemsToClean.OrderBy(i => Math.Abs(character.WorldPosition.X - i.WorldPosition.X)).FirstOrDefault();
if (targetItem != null)
{
var cleanupObjective = new AIObjectiveCleanupItem(targetItem, character, objectiveManager, PriorityModifier);
cleanupObjective.Abandoned += () => ignoredItems.Add(targetItem);
subObjectives.Add(cleanupObjective);
}
var cleanupObjective = new AIObjectiveCleanupItem(targetItem, character, objectiveManager, PriorityModifier);
cleanupObjective.Abandoned += () => ignoredItems.Add(targetItem);
subObjectives.Add(cleanupObjective);
}
}
}
@@ -534,6 +542,8 @@ namespace Barotrauma
itemsToClean.Clear();
ignoredItems.Clear();
autonomousObjectiveRetryTimer = 10;
timerMargin = 0;
newTargetTimer = 0;
}
public override void OnDeselected()
@@ -120,7 +120,6 @@ namespace Barotrauma
{
}
protected override bool CheckObjectiveSpecific() => false;
protected override bool CheckObjectiveState() => false;
}
}
@@ -318,7 +318,7 @@ namespace Barotrauma
return true;
}
protected override bool CheckObjectiveSpecific() => IsCompleted;
protected override bool CheckObjectiveState() => IsCompleted;
public override void Reset()
{
@@ -71,7 +71,7 @@ namespace Barotrauma
if (item.IsClaimedByBallastFlora) { return false; }
if (!item.HasAccess(character)) { return false; }
// Ignore items that require power but don't have it
if (item.GetComponent<Powered>() is Powered powered && powered.PowerConsumption > 0 && powered.Voltage < powered.MinVoltage) { return false; }
if (item.GetComponent<Powered>() is { PowerConsumption: > 0, HasPower: false }) { return false; }
return true;
}
@@ -53,7 +53,7 @@ namespace Barotrauma
: base(character, objectiveManager, priorityModifier, option) { }
protected override void Act(float deltaTime) { }
protected override bool CheckObjectiveSpecific() => false;
protected override bool CheckObjectiveState() => false;
public override bool CanBeCompleted => true;
public override bool AbandonWhenCannotCompleteSubObjectives => false;
public override bool AllowSubObjectiveSorting => true;
@@ -228,11 +228,7 @@ namespace Barotrauma
coroutine = CoroutineManager.Invoke(() =>
{
//round ended before the coroutine finished
#if CLIENT
if (GameMain.GameSession == null || Level.Loaded == null && !(GameMain.GameSession.GameMode is TestGameMode)) { return; }
#else
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
#endif
if (GameMain.GameSession == null || Level.Loaded == null && GameMain.GameSession.GameMode is not TestGameMode) { return; }
DelayedObjectives.Remove(objective);
AddObjective(objective);
callback?.Invoke();
@@ -312,7 +312,7 @@ namespace Barotrauma
}
}
protected override bool CheckObjectiveSpecific() => isDoneOperating && !Repeat;
protected override bool CheckObjectiveState() => isDoneOperating && !Repeat;
public override void Reset()
{
@@ -54,7 +54,7 @@ namespace Barotrauma
}
}
protected override bool CheckObjectiveSpecific() => IsCompleted;
protected override bool CheckObjectiveState() => IsCompleted;
protected override float GetPriority()
{
@@ -91,7 +91,7 @@ namespace Barotrauma
return Priority;
}
protected override bool CheckObjectiveSpecific()
protected override bool CheckObjectiveState()
{
IsCompleted = Item.IsFullCondition;
if (character.IsOnPlayerTeam && IsCompleted && IsRepairing())
@@ -70,7 +70,7 @@ namespace Barotrauma
if (otherRescuer != null && otherRescuer != character)
{
// Someone else is rescuing/holding the target.
Abandon = otherRescuer.IsPlayer || character.GetSkillLevel("medical") < otherRescuer.GetSkillLevel("medical");
Abandon = otherRescuer.IsPlayer || character.GetSkillLevel(Tags.MedicalSkill) < otherRescuer.GetSkillLevel(Tags.MedicalSkill);
return;
}
if (Target != character)
@@ -391,9 +391,18 @@ namespace Barotrauma
("[treatmentlist]", itemListStr, FormatCapitals.Yes)).Value,
null, 2.0f, $"listrequiredtreatments{Target.Name}".ToIdentifier(), 60.0f);
}
var itemsToFind = currentTreatmentSuitabilities
//items that have a positive effect and that the bot doesn't yet have
.Where(kvp => kvp.Value > 0.0f && character.Inventory.AllItems.None(it => it.Prefab.Identifier == kvp.Key))
.Select(kvp => kvp.Key);
RemoveSubObjective(ref getItemObjective);
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC),
constructor: () => new AIObjectiveGetItem(character, itemsToFind, objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
GetItemPriority = it => currentTreatmentSuitabilities.GetValueOrDefault(it.Prefab.Identifier)
},
onCompleted: () => RemoveSubObjective(ref getItemObjective),
onAbandon: () =>
{
@@ -468,16 +477,16 @@ namespace Barotrauma
item.ApplyTreatment(character, Target, Target.CharacterHealth.GetAfflictionLimb(affliction));
}
protected override bool CheckObjectiveSpecific()
protected override bool CheckObjectiveState()
{
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(Target) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, Target);
if (isCompleted && Target != character && character.IsOnPlayerTeam)
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]", Target.Name)?.Value;
character.Speak(message, delay: 1.0f, identifier: $"targethealed{Target.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f);
}
return isCompleted;
return IsCompleted;
}
protected override float GetPriority()
@@ -96,13 +96,20 @@ namespace Barotrauma
{
float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f);
vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100;
if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
if (affliction.Strength > affliction.Prefab.TreatmentThreshold)
{
vitality -= affliction.Strength;
}
else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType)
{
vitality -= affliction.Strength;
if (affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
{
vitality -= affliction.Strength;
}
else if (affliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType)
{
vitality -= affliction.Strength;
}
else if (affliction.Prefab == AfflictionPrefab.HuskInfection)
{
vitality -= affliction.Strength;
}
}
}
return Math.Clamp(vitality, 0, 100);
@@ -7,7 +7,7 @@ namespace Barotrauma
class AIObjectiveReturn : AIObjective
{
public override Identifier Identifier { get; set; } = "return".ToIdentifier();
public Submarine ReturnTarget { get; }
public Submarine Target { get; }
private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective;
private bool usingEscapeBehavior, isSteeringThroughGap;
@@ -17,10 +17,13 @@ namespace Barotrauma
public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier)
{
ReturnTarget = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
if (ReturnTarget == null)
Target = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
if (Target == null)
{
DebugConsole.AddSafeError("Error with a Return objective: no suitable return target found");
if (GameMain.GameSession.GameMode is not TestGameMode)
{
DebugConsole.AddWarning($"({character.DisplayName}) No suitable return target found. Cannot return back to the main sub.");
}
Abandon = true;
}
@@ -54,7 +57,7 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (ReturnTarget == null)
if (Target == null)
{
Abandon = true;
return;
@@ -62,7 +65,7 @@ namespace Barotrauma
bool shouldUseEscapeBehavior = false;
if (character.CurrentHull != null || isSteeringThroughGap)
{
if (character.Submarine == null || !character.Submarine.IsConnectedTo(ReturnTarget))
if (character.Submarine == null || !character.Submarine.IsConnectedTo(Target))
{
// Character is on another sub that is not connected to the target sub, use the escape behavior to get them out
shouldUseEscapeBehavior = true;
@@ -76,13 +79,13 @@ namespace Barotrauma
Abandon = true;
}
}
else if (character.Submarine != ReturnTarget)
else if (character.Submarine != Target)
{
// Character is on another sub that is connected to the target sub, create a Go To objective to reach the target sub
if (moveInsideObjective == null)
{
Hull targetHull = null;
foreach (var d in ReturnTarget.ConnectedDockingPorts.Values)
foreach (var d in Target.ConnectedDockingPorts.Values)
{
if (!d.Docked) { continue; }
if (d.DockingTarget == null) { continue; }
@@ -143,7 +146,7 @@ namespace Barotrauma
Hull targetHull = null;
float targetDistanceSquared = float.MaxValue;
bool targetIsAirlock = false;
foreach (var hull in ReturnTarget.GetHulls(false))
foreach (var hull in Target.GetHulls(false))
{
bool hullIsAirlock = hull.IsAirlock;
if(hullIsAirlock || (!targetIsAirlock && hull.LeadsOutside(character)))
@@ -178,18 +181,14 @@ namespace Barotrauma
usingEscapeBehavior = shouldUseEscapeBehavior;
}
protected override bool CheckObjectiveSpecific()
protected override bool CheckObjectiveState()
{
if (IsCompleted)
{
return true;
}
if (ReturnTarget == null)
if (Target == null)
{
Abandon = true;
return false;
}
if (character.Submarine == ReturnTarget)
if (character.Submarine == Target)
{
IsCompleted = true;
}
@@ -204,12 +204,8 @@ namespace Barotrauma
var allTargetItems = new List<Identifier>();
for (int i = 0; i < AllOptions.Length; i++)
{
Identifier[] optionTargetItemsSplit = i < splitTargetItems.Length ? splitTargetItems[i].Split(',', '').ToIdentifiers() : Array.Empty<Identifier>();
for (int j = 0; j < optionTargetItemsSplit.Length; j++)
{
optionTargetItemsSplit[j] = optionTargetItemsSplit[j].Value.Trim().ToIdentifier();
allTargetItems.Add(optionTargetItemsSplit[j]);
}
Identifier[] optionTargetItemsSplit = i < splitTargetItems.Length ? splitTargetItems[i].ToIdentifiers().ToArray() : Array.Empty<Identifier>();
allTargetItems.AddRange(optionTargetItemsSplit);
optionTargetItems.Add(AllOptions[i], optionTargetItemsSplit.ToImmutableArray());
}
TargetItems = allTargetItems.ToImmutableArray();
@@ -45,7 +45,8 @@ namespace Barotrauma
public float HappyThreshold { get; set; }
public float MaxHappiness { get; set; }
public bool HideStatusIndicators { get; set; }
/// <summary>
/// At which point is the pet considered "hungry" (playing unhappy sounds and showing the icon)
@@ -59,6 +60,14 @@ namespace Barotrauma
public float PlayForce { get; set; }
public float PlayTimer { get; set; }
public float PlayCooldown { get; set; }
/// <summary>
/// Should the pet lose ownership (and stop following) when the same character interacts with it twice? Unlike with other pets, if another character interacts with the pet, they will become the owner.
/// </summary>
public bool ToggleOwner { get; set; }
private float? UnstunY { get; set; }
public EnemyAIController AIController { get; private set; } = null;
@@ -162,7 +171,7 @@ namespace Barotrauma
private class Food
{
public string Tag;
public Identifier Tag;
public Vector2 HungerRange;
public float Hunger;
public float Happiness;
@@ -182,6 +191,7 @@ namespace Barotrauma
MaxHappiness = element.GetAttributeFloat(nameof(MaxHappiness), 100.0f);
UnhappyThreshold = element.GetAttributeFloat(nameof(UnhappyThreshold), MaxHappiness * 0.25f);
HappyThreshold = element.GetAttributeFloat(nameof(HappyThreshold), MaxHappiness * 0.8f);
HideStatusIndicators = element.GetAttributeBool(nameof(HideStatusIndicators), false);
MaxHunger = element.GetAttributeFloat(nameof(MaxHunger), 100.0f);
HungryThreshold = element.GetAttributeFloat(nameof(HungryThreshold), MaxHunger * 0.5f);
@@ -192,7 +202,9 @@ namespace Barotrauma
HappinessDecreaseRate = element.GetAttributeFloat(nameof(HappinessDecreaseRate), 0.1f);
HungerIncreaseRate = element.GetAttributeFloat(nameof(HungerIncreaseRate), 0.25f);
PlayForce = element.GetAttributeFloat("playforce", 15.0f);
PlayForce = element.GetAttributeFloat(nameof(PlayForce), 15.0f);
PlayCooldown = element.GetAttributeFloat(nameof(PlayCooldown), 5.0f);
ToggleOwner = element.GetAttributeBool(nameof(ToggleOwner), false);
foreach (var subElement in element.Elements())
{
@@ -204,7 +216,7 @@ namespace Barotrauma
case "eat":
Food food = new Food
{
Tag = subElement.GetAttributeString("tag", ""),
Tag = subElement.GetAttributeIdentifier("tag", Identifier.Empty),
Hunger = subElement.GetAttributeFloat("hunger", -1),
Happiness = subElement.GetAttributeFloat("happiness", 1),
Priority = subElement.GetAttributeFloat("priority", 100),
@@ -227,6 +239,7 @@ namespace Barotrauma
public StatusIndicatorType GetCurrentStatusIndicatorType()
{
if (HideStatusIndicators) { return StatusIndicatorType.None; }
if (Hunger > HungryThreshold) { return StatusIndicatorType.Hungry; }
if (Happiness > HappyThreshold) { return StatusIndicatorType.Happy; }
if (Happiness < UnhappyThreshold) { return StatusIndicatorType.Sad; }
@@ -283,14 +296,22 @@ namespace Barotrauma
public void Play(Character player)
{
if (PlayTimer > 0.0f) { return; }
Owner ??= player;
PlayTimer = 5.0f;
if (!AIController.Character.IsFriendly(player)) { return; }
if (ToggleOwner)
{
Owner = Owner == player ? null : player;
}
else
{
Owner ??= player;
}
PlayTimer = PlayCooldown;
AIController.Character.IsRagdolled = true;
Happiness += 10.0f;
AIController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce);
UnstunY = AIController.Character.SimPosition.Y;
#if CLIENT
AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f);
AIController.Character.PlaySound(Owner == null ? CharacterSound.SoundType.Unhappy : CharacterSound.SoundType.Happy);
#endif
}
@@ -318,7 +339,7 @@ namespace Barotrauma
if (UnstunY.HasValue)
{
if (PlayTimer > 4.0f)
if (PlayTimer > PlayCooldown - 1.0f)
{
float extent = character.AnimController.MainLimb.body.GetMaxExtent();
if (character.SimPosition.Y < (UnstunY.Value + extent * 3.0f) &&
@@ -354,9 +375,12 @@ namespace Barotrauma
{
if (food.TargetParams == null)
{
if (AIController.AIParams.TryGetTarget(food.Tag, out TargetParams target))
if (AIController.AIParams.TryGetTargets(food.Tag, out IEnumerable<TargetParams> existingTargetParams))
{
food.TargetParams = target;
foreach (var targetParams in existingTargetParams)
{
food.TargetParams = targetParams;
}
}
else if (AIController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out TargetParams targetParams))
{
@@ -444,11 +468,15 @@ namespace Barotrauma
}
else
{
WayPoint spawnPoint = null;
//try to find a spawnpoint in the main sub
var spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandomUnsynced();
if (Submarine.MainSub != null)
{
spawnPoint = WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine == Submarine.MainSub).GetRandomUnsynced();
}
//if not found, try any player sub (shuttle/drone etc)
spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandomUnsynced();
spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition;
spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub?.WorldPosition ?? Vector2.Zero;
}
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName.ToIdentifier());
@@ -52,7 +52,9 @@ namespace Barotrauma
{
if (orderedCharacter != CommandingCharacter)
{
CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", givingOrderToSelf: false), minDurationBetweenSimilar: 5);
CommandingCharacter.Speak(SuggestedOrder.GetChatMessage(OrderedCharacter.Name, "", givingOrderToSelf: false),
minDurationBetweenSimilar: 5,
identifier: ("GiveOrder." + SuggestedOrder.Prefab.Identifier).ToIdentifier());
}
CurrentOrder = SuggestedOrder
.WithOption(Option)
@@ -60,7 +62,9 @@ namespace Barotrauma
.WithOrderGiver(CommandingCharacter)
.WithManualPriority(CharacterInfo.HighestManualOrderPriority);
OrderedCharacter.SetOrder(CurrentOrder, CommandingCharacter != OrderedCharacter);
OrderedCharacter.Speak(TextManager.Get("DialogAffirmative").Value, delay: 1.0f, minDurationBetweenSimilar: 5);
OrderedCharacter.Speak(TextManager.Get("DialogAffirmative").Value, delay: 1.0f,
minDurationBetweenSimilar: 5,
identifier: ("ReceiveOrder." + SuggestedOrder.Prefab.Identifier).ToIdentifier());
}
TimeSinceLastAttempt = 0f;
}
@@ -11,7 +11,7 @@ namespace Barotrauma
public override void CalculateImportanceSpecific()
{
if (shipCommandManager.NavigationState == ShipCommandManager.NavigationStates.Inactive) { return; }
if (TargetItemComponent is Powered powered && powered.Voltage <= powered.MinVoltage) { return; }
if (TargetItemComponent is Powered { HasPower: false }) { return; }
if (TargetItem.Condition <= 0f) { return; }
Importance = 70f;
@@ -38,21 +38,7 @@ namespace Barotrauma
public readonly AnimationType AnimationType;
public readonly AnimationParams TemporaryAnimation;
public readonly float Priority;
public bool IsActive
{
get { return _isActive; }
set
{
if (value)
{
expirationTimer = expirationTime;
}
_isActive = value;
}
}
private bool _isActive;
private float expirationTimer;
private const float expirationTime = 0.1f;
public bool IsActive;
public AnimSwap(AnimationParams temporaryAnimation, float priority)
{
@@ -61,15 +47,6 @@ namespace Barotrauma
Priority = priority;
IsActive = true;
}
public void Update(float deltaTime)
{
expirationTimer -= deltaTime;
if (expirationTimer <= 0)
{
IsActive = false;
}
}
}
protected readonly Dictionary<AnimationType, AnimSwap> tempAnimations = new Dictionary<AnimationType, AnimSwap>();
@@ -151,7 +128,8 @@ namespace Barotrauma
}
else
{
return Math.Abs(TargetMovement.X) > (WalkParams.MovementSpeed + RunParams.MovementSpeed) / 2.0f;
float movementSpeed = IsClimbing ? TargetMovement.Y : TargetMovement.X;
return Math.Abs(movementSpeed) > (WalkParams.MovementSpeed + RunParams.MovementSpeed) / 2.0f;
}
}
}
@@ -226,7 +204,7 @@ namespace Barotrauma
public void UpdateAnimations(float deltaTime)
{
UpdateTemporaryAnimations(deltaTime);
UpdateTemporaryAnimations();
UpdateAnim(deltaTime);
}
@@ -338,6 +316,31 @@ namespace Barotrauma
{
FlipLockTime = (float)Timing.TotalTime + time;
}
protected void UpdateConstantTorque(float deltaTime)
{
foreach (var limb in Limbs)
{
if (limb.IsSevered) { continue; }
if (Math.Abs(limb.Params.ConstantTorque) > 0)
{
// TODO: not sure if this works on ground
float movementFactor = Math.Max(character.AnimController.Collider.LinearVelocity.Length() * 0.5f, 1);
limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque * movementFactor, wrapAngle: true);
}
}
}
protected void UpdateBlink(float deltaTime)
{
foreach (var limb in Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.Params.BlinkFrequency <= 0) { continue; }
if (!limb.InWater && limb.Params.OnlyBlinkInWater) { continue; }
limb.UpdateBlink(deltaTime, MainLimb.Rotation);
}
}
public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos)
{
@@ -408,9 +411,9 @@ namespace Barotrauma
character.WorldPosition.Y - handWorldPos.Y > ConvertUnits.ToDisplayUnits(CurrentGroundedParams.TorsoPosition) / 4 &&
this is HumanoidAnimController humanoidAnimController)
{
humanoidAnimController.Crouching = true;
humanoidAnimController.Crouch();
// TODO: is this redundant/required?
humanoidAnimController.ForceSelectAnimationType = AnimationType.Crouch;
character.SetInput(InputType.Crouch, hit: false, held: true);
}
}
@@ -696,6 +699,294 @@ namespace Barotrauma
hand.body.SmoothRotate(handAngle, 10.0f * handTorque * hand.Mass, wrapAngle: false);
}
}
private float prevFootPos;
protected void UpdateClimbing()
{
var ladder = character.SelectedSecondaryItem?.GetComponent<Ladder>();
if (character.IsIncapacitated)
{
Anim = Animation.None;
return;
}
else if (ladder == null)
{
StopClimbing();
return;
}
onGround = false;
IgnorePlatforms = true;
bool climbFast = !character.Params.ForceSlowClimbing && character.AnimController.IsMovingFast;
var animParams = climbFast ? RunParams : WalkParams;
// Don't slide if we can climb faster than slide.
bool slide = animParams.SlideSpeed > animParams.ClimbSpeed && targetMovement.Y < -0.1f && climbFast;
float maxClimbingSpeed = climbFast && !character.Params.ForceSlowClimbing ? RunParams.ClimbSpeed : WalkParams.ClimbSpeed;
Vector2 tempTargetMovement = TargetMovement;
tempTargetMovement.Y = Math.Clamp(tempTargetMovement.Y, slide ? -animParams.SlideSpeed : -maxClimbingSpeed, maxClimbingSpeed);
movement = MathUtils.SmoothStep(movement, tempTargetMovement, 0.3f);
Limb leftFoot = GetClimbingLimb(LimbType.LeftFoot);
Limb rightFoot = GetClimbingLimb(LimbType.RightFoot);
Limb head = GetClimbingLimb(LimbType.Head);
Limb torso = GetClimbingLimb(LimbType.Torso);
Limb leftHand = GetClimbingLimb(LimbType.LeftHand);
Limb rightHand = GetClimbingLimb(LimbType.RightHand);
Vector2 ladderSimPos = ConvertUnits.ToSimUnits(
ladder.Item.Rect.X + ladder.Item.Rect.Width / 2.0f,
ladder.Item.Rect.Y);
Vector2 ladderSimSize = ConvertUnits.ToSimUnits(ladder.Item.Rect.Size.ToVector2());
var lowestNearbyLadder = GetLowestNearbyLadder(ladder);
if (lowestNearbyLadder != null && lowestNearbyLadder != ladder)
{
ladderSimSize.Y = ConvertUnits.ToSimUnits(ladder.Item.WorldRect.Y - (lowestNearbyLadder.Item.WorldRect.Y - lowestNearbyLadder.Item.Rect.Size.Y));
}
float stepHeight = ConvertUnits.ToSimUnits(animParams.ClimbStepHeight);
if (currentHull == null && ladder.Item.Submarine != null)
{
ladderSimPos += ladder.Item.Submarine.SimPosition;
}
else if (currentHull?.Submarine != null && currentHull.Submarine != ladder.Item.Submarine && ladder.Item.Submarine != null)
{
ladderSimPos += ladder.Item.Submarine.SimPosition - currentHull.Submarine.SimPosition;
}
else if (currentHull?.Submarine != null && ladder.Item.Submarine == null)
{
ladderSimPos -= currentHull.Submarine.SimPosition;
}
float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.Radius - Collider.Height / 2.0f;
float torsoPos = TorsoPosition ?? 0;
float bodyMoveForce = animParams.ClimbBodyMoveForce;
if (torso != null)
{
MoveLimb(torso, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + torsoPos), bodyMoveForce);
}
if (head != null)
{
float headPos = HeadPosition ?? 0;
MoveLimb(head, new Vector2(ladderSimPos.X - 0.2f * Dir, bottomPos + headPos), bodyMoveForce);
}
Collider.MoveToPos(new Vector2(ladderSimPos.X - 0.1f * Dir, Collider.SimPosition.Y), bodyMoveForce);
Vector2 handPos = new Vector2(
ladderSimPos.X,
bottomPos + torsoPos + movement.Y * 0.1f - ladderSimPos.Y);
if (climbFast) { handPos.Y -= stepHeight; }
float handMoveForce = animParams.ClimbHandMoveForce;
//prevent the hands from going above the top of the ladders
handPos.Y = Math.Min(-0.5f, handPos.Y);
if (!Aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand)?.GetComponent<Holdable>()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f)
{
if (rightHand != null)
{
MoveLimb(rightHand,
new Vector2(slide ? handPos.X + ladderSimSize.X * 0.75f : handPos.X,
(slide ? handPos.Y + stepHeight : MathUtils.Round(handPos.Y, stepHeight * 2.0f)) + ladderSimPos.Y),
handMoveForce);
rightHand.body.ApplyTorque(Dir * 2.0f);
}
}
if (!Aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand)?.GetComponent<Holdable>()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f)
{
if (leftHand != null)
{
MoveLimb(leftHand,
new Vector2(handPos.X - ladderSimSize.X * (slide ? 1.0f : 0.5f),
(slide ? handPos.Y + stepHeight : MathUtils.Round(handPos.Y - stepHeight, stepHeight * 2.0f) + stepHeight) + ladderSimPos.Y),
handMoveForce); ;
leftHand.body.ApplyTorque(Dir * 2.0f);
}
}
float stepHeightAdjustment = stepHeight * 2.7f;
Vector2 footPos = new Vector2(
handPos.X - Dir * 0.05f,
bottomPos + ColliderHeightFromFloor - stepHeightAdjustment - ladderSimPos.Y);
if (climbFast) { footPos.Y += stepHeight; }
//apply torque to the legs to make the knees bend
Limb leftLeg = GetClimbingLimb(LimbType.LeftLeg);
Limb rightLeg = GetClimbingLimb(LimbType.RightLeg);
//only move the feet if they're above the bottom of the ladders
//(if not, they'll just dangle in air, and the character holds itself up with its arms)
if (footPos.Y > -ladderSimSize.Y - 0.2f && leftFoot != null && rightFoot != null && leftLeg != null && rightLeg != null)
{
Limb refLimb = GetClimbingLimb(LimbType.Waist) ?? GetClimbingLimb(LimbType.Torso) ?? MainLimb;
bool leftLegBackwards = Math.Abs(leftLeg.body.Rotation - refLimb.body.Rotation) > MathHelper.Pi;
bool rightLegBackwards = Math.Abs(rightLeg.body.Rotation - refLimb.body.Rotation) > MathHelper.Pi;
float footMoveForce = animParams.ClimbFootMoveForce;
if (slide)
{
if (!leftLegBackwards) { MoveLimb(leftFoot, new Vector2(footPos.X - ladderSimSize.X * 0.5f, footPos.Y + ladderSimPos.Y), footMoveForce, pullFromCenter: true); }
if (!rightLegBackwards) { MoveLimb(rightFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), footMoveForce, pullFromCenter: true); }
}
else
{
float leftFootPos = MathUtils.Round(footPos.Y + stepHeight, stepHeight * 2.0f) - stepHeight;
float prevLeftFootPos = MathUtils.Round(prevFootPos + stepHeight, stepHeight * 2.0f) - stepHeight;
if (!leftLegBackwards) { MoveLimb(leftFoot, new Vector2(footPos.X, leftFootPos + ladderSimPos.Y), footMoveForce, pullFromCenter: true); }
float rightFootPos = MathUtils.Round(footPos.Y, stepHeight * 2.0f);
float prevRightFootPos = MathUtils.Round(prevFootPos, stepHeight * 2.0f);
if (!rightLegBackwards) { MoveLimb(rightFoot, new Vector2(footPos.X, rightFootPos + ladderSimPos.Y), footMoveForce, pullFromCenter: true); }
#if CLIENT
if (Math.Abs(leftFootPos - prevLeftFootPos) > stepHeight && leftFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval)
{
SoundPlayer.PlaySound("footstep_armor_heavy", leftFoot.WorldPosition, hullGuess: currentHull);
leftFoot.LastImpactSoundTime = (float)Timing.TotalTime;
}
if (Math.Abs(rightFootPos - prevRightFootPos) > stepHeight && rightFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval)
{
SoundPlayer.PlaySound("footstep_armor_heavy", rightFoot.WorldPosition, hullGuess: currentHull);
rightFoot.LastImpactSoundTime = (float)Timing.TotalTime;
}
#endif
prevFootPos = footPos.Y;
}
if (!leftLegBackwards) { leftLeg.body.ApplyTorque(Dir * -8.0f); } // TODO: expose?
if (!rightLegBackwards) { rightLeg.body.ApplyTorque(Dir * -8.0f); }
}
float movementFactor = (handPos.Y / stepHeight) * (float)Math.PI;
movementFactor = 0.8f + (float)Math.Abs(Math.Sin(movementFactor));
Vector2 subSpeed = currentHull != null || ladder.Item.Submarine == null
? Vector2.Zero : ladder.Item.Submarine.Velocity;
//reached the top of the ladders -> can't go further up
Vector2 climbForce = new Vector2(0.0f, movement.Y) * movementFactor;
if (!InWater) { climbForce.Y += 0.3f * movementFactor; }
if (character.SimPosition.Y > ladderSimPos.Y) { climbForce.Y = Math.Min(0.0f, climbForce.Y); }
//reached the bottom -> can't go further down
float minHeightFromFloor = ColliderHeightFromFloor / 2 + Collider.Height;
if (floorFixture != null &&
!floorFixture.CollisionCategories.HasFlag(Physics.CollisionStairs) &&
!floorFixture.CollisionCategories.HasFlag(Physics.CollisionPlatform) &&
character.SimPosition.Y < standOnFloorY + minHeightFromFloor)
{
climbForce.Y = MathHelper.Clamp((standOnFloorY + minHeightFromFloor - character.SimPosition.Y) * 5.0f, climbForce.Y, 1.0f);
}
//apply forces to the collider to move the Character up/down
Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass);
// Don't rotate the head on non-humanoids, because it can cause issues with some ragdolls.
// E.g. the head might not actually be head, or it's not where we expect it to be.
if (head != null && character.IsHumanoid)
{
if (Aiming)
{
RotateHead(head);
}
else if (Anim == Animation.UsingItemWhileClimbing && character.SelectedItem is { } selectedItem)
{
Vector2 diff = (selectedItem.WorldPosition - head.WorldPosition) * Dir;
float targetRotation = MathHelper.WrapAngle(MathUtils.VectorToAngle(diff) - MathHelper.PiOver4 * Dir);
head.body.SmoothRotate(targetRotation, force: animParams.HeadTorque);
}
else
{
float movementMultiplier = targetMovement.Y < 0 ? 0 : 1;
head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, force: animParams.HeadTorque);
}
}
if (ladder.Item.Prefab.Triggers.None())
{
character.ReleaseSecondaryItem();
return;
}
Rectangle trigger = ladder.Item.Prefab.Triggers.FirstOrDefault();
trigger = ladder.Item.TransformTrigger(trigger);
bool isRemote = false;
bool isClimbing = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
isRemote = character.IsRemotelyControlled;
}
if (isRemote)
{
if (Math.Abs(targetMovement.X) > 0.05f ||
(TargetMovement.Y < 0.0f && ConvertUnits.ToSimUnits(trigger.Height) + handPos.Y < HeadPosition) ||
(TargetMovement.Y > 0.0f && handPos.Y > 0.1f))
{
isClimbing = false;
}
}
else if ((character.IsKeyDown(InputType.Left) || character.IsKeyDown(InputType.Right)) &&
(!character.IsKeyDown(InputType.Up) && !character.IsKeyDown(InputType.Down)))
{
isClimbing = false;
}
if (!isClimbing)
{
character.StopClimbing();
IgnorePlatforms = false;
}
Ladder GetLowestNearbyLadder(Ladder currentLadder, float threshold = 16.0f)
{
foreach (Ladder ladder in Ladder.List)
{
if (ladder == currentLadder || !ladder.Item.IsInteractable(character)) { continue; }
if (Math.Abs(ladder.Item.WorldPosition.X - currentLadder.Item.WorldPosition.X) > threshold) { continue; }
if (ladder.Item.WorldPosition.Y > currentLadder.Item.WorldPosition.Y) { continue; }
if ((currentLadder.Item.WorldRect.Y - currentLadder.Item.Rect.Height) - ladder.Item.WorldRect.Y > threshold) { continue; }
return ladder;
}
return null;
}
Limb GetClimbingLimb(LimbType limbType)
{
if (HasMultipleLimbsOfSameType)
{
// First try to find a match using the secondary type, if that fails, use the primary type and exclude all the limbs with the secondary type.
// Secondary limbs are first excluded and then targeted, because some feet are meant to be used as hands in this context, which means we don't want to get them when seeking the feet.
return GetLimb(limbType, useSecondaryType: true) ?? GetLimb(limbType, excludeLimbsWithSecondaryType: true);
}
else
{
return GetLimb(limbType);
}
}
}
protected void RotateHead(Limb head)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 dir = (mousePos - head.SimPosition) * Dir;
float rot = MathUtils.VectorToAngle(dir);
var neckJoint = GetJointBetweenLimbs(LimbType.Head, LimbType.Torso);
if (neckJoint != null)
{
float offset = MathUtils.WrapAnglePi(GetLimb(LimbType.Torso).body.Rotation);
float lowerLimit = neckJoint.LowerLimit + offset;
float upperLimit = neckJoint.UpperLimit + offset;
float min = Math.Min(lowerLimit, upperLimit);
float max = Math.Max(lowerLimit, upperLimit);
rot = Math.Clamp(rot, min, max);
}
head.body.SmoothRotate(rot, CurrentAnimationParams.HeadTorque);
}
public void ApplyPose(Vector2 leftHandPos, Vector2 rightHandPos, Vector2 leftFootPos, Vector2 rightFootPos, float footMoveForce = 10)
{
@@ -818,6 +1109,13 @@ namespace Barotrauma
CalculateArmLengths();
}
}
public void RecreateAndRespawn(RagdollParams ragdollParams = null)
{
Vector2 pos = character.WorldPosition;
Recreate(ragdollParams);
character.TeleportTo(pos);
}
private void StartAnimation(Animation animation)
{
@@ -906,7 +1204,7 @@ namespace Barotrauma
return true;
}
private void UpdateTemporaryAnimations(float deltaTime)
private void UpdateTemporaryAnimations()
{
if (tempAnimations.None()) { return; }
foreach ((AnimationType animationType, AnimSwap animSwap) in tempAnimations)
@@ -932,7 +1230,8 @@ namespace Barotrauma
expiredAnimations.Clear();
foreach (AnimSwap animSwap in tempAnimations.Values)
{
animSwap.Update(deltaTime);
// Will be removed on the next frame, unless something keeps it alive.
animSwap.IsActive = false;
}
}
@@ -139,9 +139,11 @@ namespace Barotrauma
ResetState();
return;
}
UpdateConstantTorque(deltaTime);
UpdateBlink(deltaTime);
var mainLimb = MainLimb;
levitatingCollider = !IsHangingWithRope;
levitatingCollider = !IsHangingWithRope && !IsClimbing;
if (!character.CanMove)
{
@@ -208,6 +210,11 @@ namespace Barotrauma
{
TargetMovement = TargetMovement.ClampLength(2);
}
if (IsClimbing)
{
UpdateClimbing();
}
if (inWater && !forceStanding)
{
@@ -336,7 +343,6 @@ namespace Barotrauma
if (target == null) { return; }
Limb mouthLimb = GetLimb(LimbType.Head);
if (mouthLimb == null) { return; }
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
//stop dragging if there's something between the pull limb and the target
@@ -357,23 +363,23 @@ namespace Barotrauma
return;
}
}
float dmg = character.Params.EatingSpeed;
float eatSpeed = dmg / ((float)Math.Sqrt(Math.Max(target.Mass, 1)) * 10);
eatTimer += deltaTime * eatSpeed;
Vector2 mouthPos = SimplePhysicsEnabled ? character.SimPosition : GetMouthPosition().Value;
Vector2 attackSimPosition = character.Submarine == null ? ConvertUnits.ToSimUnits(target.WorldPosition) : target.SimPosition;
Vector2 limbDiff = attackSimPosition - mouthPos;
float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 1);
bool tooFar = character.InWater ? limbDiff.LengthSquared() > extent * extent : limbDiff.X > extent;
if (tooFar)
{
character.SelectedCharacter = null;
}
else
if (Character.CanEat)
{
Vector2 mouthPos = SimplePhysicsEnabled ? character.SimPosition : GetMouthPosition() ?? Vector2.Zero;
Vector2 attackSimPosition = character.Submarine == null ? ConvertUnits.ToSimUnits(target.WorldPosition) : target.SimPosition;
Vector2 limbDiff = attackSimPosition - mouthPos;
float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 1);
bool tooFar = character.InWater ? limbDiff.LengthSquared() > extent * extent : limbDiff.X > extent;
if (tooFar)
{
character.DeselectCharacter();
return;
}
float dmg = character.Params.EatingSpeed;
float eatSpeed = dmg / ((float)Math.Sqrt(Math.Max(target.Mass, 1)) * 10);
eatTimer += deltaTime * eatSpeed;
//pull the target character to the position of the mouth
//(+ make the force fluctuate to waggle the character a bit)
float dragForce = MathHelper.Clamp(eatSpeed * 10, 0, 40);
@@ -405,20 +411,19 @@ namespace Barotrauma
else
{
float force = (float)Math.Sin(eatTimer * 100) * mouthLimb.Mass;
mouthLimb.body.ApplyLinearImpulse(Vector2.UnitY * force * 2, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
mouthLimb.body.ApplyTorque(-force * 50);
mouthLimb.body.ApplyLinearImpulse(Vector2.UnitY * force * mouthLimb.Params.EatImpulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
mouthLimb.body.ApplyTorque(-force * mouthLimb.Params.EatTorque);
}
if (Character.CanEat && target.IsDead)
var jaw = GetLimb(LimbType.Jaw);
if (jaw != null)
{
jaw.body.ApplyTorque(-(float)Math.Sin(eatTimer * 150) * jaw.Mass * 25);
}
character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
if (target.IsDead)
{
var jaw = GetLimb(LimbType.Jaw);
if (jaw != null)
{
jaw.body.ApplyTorque(-(float)Math.Sin(eatTimer * 150) * jaw.Mass * 25);
}
character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
float particleFrequency = MathHelper.Clamp(eatSpeed / 2, 0.02f, 0.5f);
if (Rand.Value() < particleFrequency / 6)
{
@@ -430,7 +435,7 @@ namespace Barotrauma
}
if (eatTimer % 1.0f < 0.5f && (eatTimer - deltaTime * eatSpeed) % 1.0f > 0.5f)
{
static bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA != null && !j.LimbA.IsSevered && j.LimbB != null && !j.LimbB.IsSevered;
static bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA is { IsSevered: false } && j.LimbB is { IsSevered: false };
//keep severing joints until there is only one limb left
var nonSeveredJoints = target.AnimController.LimbJoints.Where(CanBeSevered);
if (nonSeveredJoints.None())
@@ -440,16 +445,13 @@ namespace Barotrauma
{
target.Inventory?.AllItemsMod.ForEach(it => it?.Drop(dropper: null));
}
//only one limb left, the character is now full eaten
Entity.Spawner?.AddEntityToRemoveQueue(target);
if (Character.AIController is EnemyAIController enemyAi)
{
enemyAi.PetBehavior?.OnEat(target);
}
character.SelectedCharacter = null;
character.DeselectCharacter();
}
else //sever a random joint
{
@@ -460,7 +462,7 @@ namespace Barotrauma
}
}
public bool reverse;
public bool Reverse;
void UpdateSineAnim(float deltaTime)
{
@@ -510,7 +512,7 @@ namespace Barotrauma
}
else
{
Vector2 transformedMovement = reverse ? -movement : movement;
Vector2 transformedMovement = Reverse ? -movement : movement;
float movementAngle = MathUtils.VectorToAngle(transformedMovement) - MathHelper.PiOver2;
float mainLimbAngle = 0;
if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
@@ -555,7 +557,6 @@ namespace Barotrauma
foreach (var limb in Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.type != LimbType.Tail) { continue; }
if (!limb.Params.ApplyTailAngle) { continue; }
RotateTail(limb);
isAngleApplied = true;
@@ -592,7 +593,7 @@ namespace Barotrauma
else
{
movementAngle = Dir > 0 ? -MathHelper.PiOver2 : MathHelper.PiOver2;
if (reverse)
if (Reverse)
{
movementAngle = MathUtils.WrapAngleTwoPi(movementAngle - MathHelper.Pi);
}
@@ -651,29 +652,21 @@ namespace Barotrauma
foreach (var limb in Limbs)
{
if (limb.IsSevered) { continue; }
switch (limb.type)
if (limb.type is LimbType.LeftFoot or LimbType.RightFoot)
{
case LimbType.LeftFoot:
case LimbType.RightFoot:
if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID))
{
SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, mainLimb, FootTorque);
}
break;
case LimbType.Tail:
if (waveLength > 0 && waveAmplitude > 0)
{
float waveRotation = (float)Math.Sin(WalkPos * limb.Params.SineFrequencyMultiplier);
limb.body.ApplyTorque(waveRotation * limb.Mass * waveAmplitude * limb.Params.SineAmplitudeMultiplier);
}
break;
if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID))
{
SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, mainLimb, FootTorque);
}
}
if (limb.type == LimbType.Tail || limb.Params.ApplySineMovement)
{
if (waveLength > 0 && waveAmplitude > 0)
{
float waveRotation = (float)Math.Sin(WalkPos * limb.Params.SineFrequencyMultiplier);
limb.body.ApplyTorque(waveRotation * limb.Mass * waveAmplitude * limb.Params.SineAmplitudeMultiplier);
}
}
}
for (int i = 0; i < Limbs.Length; i++)
{
var limb = Limbs[i];
if (limb.IsSevered) { continue; }
if (limb.SteerForce <= 0.0f) { continue; }
if (!Collider.PhysEnabled) { continue; }
Vector2 pullPos = limb.PullJointWorldAnchorA;
@@ -698,20 +691,6 @@ namespace Barotrauma
}
}
foreach (var limb in Limbs)
{
if (limb.IsSevered) { continue; }
if (Math.Abs(limb.Params.ConstantTorque) > 0)
{
float movementFactor = Math.Max(character.AnimController.Collider.LinearVelocity.Length() * 0.5f, 1);
limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque * movementFactor, wrapAngle: true);
}
if (limb.Params.BlinkFrequency > 0)
{
limb.UpdateBlink(deltaTime, MainLimb.Rotation);
}
}
floorY = Limbs[0].SimPosition.Y;
}
@@ -744,9 +723,9 @@ namespace Barotrauma
}
float offset = MathHelper.Pi * CurrentGroundedParams.StepLiftOffset;
if (CurrentGroundedParams.MultiplyByDir)
if (character.AnimController.Dir < 0)
{
offset *= Dir;
offset += MathHelper.Pi * CurrentGroundedParams.StepLiftFrequency;
}
float stepLift = TargetMovement.X == 0.0f ? 0 :
(float)Math.Sin(WalkPos * Dir * CurrentGroundedParams.StepLiftFrequency + offset) * (CurrentGroundedParams.StepLiftAmount / 100);
@@ -847,14 +826,6 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) { continue; }
if (Math.Abs(limb.Params.ConstantTorque) > 0)
{
limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque, wrapAngle: true);
}
if (limb.Params.BlinkFrequency > 0 && !limb.Params.OnlyBlinkInWater)
{
limb.UpdateBlink(deltaTime, MainLimb.Rotation);
}
switch (limb.type)
{
case LimbType.LeftFoot:
@@ -13,10 +13,8 @@ namespace Barotrauma
private const float SteepestWalkableSlopeAngleDegrees = 55f;
private const float SlowlyWalkableSlopeAngleDegrees = 30f;
private static readonly float SteepestWalkableSlopeNormalX =
MathF.Sin(MathHelper.ToRadians(SteepestWalkableSlopeAngleDegrees));
private static readonly float SlowlyWalkableSlopeNormalX =
MathF.Sin(MathHelper.ToRadians(SlowlyWalkableSlopeAngleDegrees));
private static readonly float SteepestWalkableSlopeNormalX = MathF.Sin(MathHelper.ToRadians(SteepestWalkableSlopeAngleDegrees));
private static readonly float SlowlyWalkableSlopeNormalX = MathF.Sin(MathHelper.ToRadians(SlowlyWalkableSlopeAngleDegrees));
private const float MaxSpeedOnStairs = 1.7f;
private const float SteepSlopePushMagnitude = MaxSpeedOnStairs;
@@ -254,7 +252,8 @@ namespace Barotrauma
{
if (Frozen) { return; }
if (MainLimb == null) { return; }
UpdateConstantTorque(deltaTime);
UpdateBlink(deltaTime);
levitatingCollider = !IsHangingWithRope;
if (onGround && character.CanMove)
{
@@ -652,9 +651,14 @@ namespace Barotrauma
{
movement = Vector2.Zero;
}
float offset = MathHelper.Pi * currentGroundedParams.StepLiftOffset;
if (character.AnimController.Dir < 0)
{
offset += MathHelper.Pi * currentGroundedParams.StepLiftFrequency;
}
float stepLift = TargetMovement.X == 0.0f ? 0 :
(float)Math.Sin(WalkPos * currentGroundedParams.StepLiftFrequency + MathHelper.Pi * currentGroundedParams.StepLiftOffset) * (currentGroundedParams.StepLiftAmount / 100);
(float)Math.Sin(WalkPos * Dir * currentGroundedParams.StepLiftFrequency + offset) * (currentGroundedParams.StepLiftAmount / 100);
float y = colliderPos.Y + stepLift;
@@ -987,7 +991,7 @@ namespace Barotrauma
{
head.body.SmoothRotate(Collider.Rotation + HeadAngle.Value * Dir, CurrentSwimParams.HeadTorque);
}
else
else if (character.FollowCursor)
{
RotateHead(head);
}
@@ -1145,245 +1149,6 @@ namespace Barotrauma
}
}
private float prevFootPos;
void UpdateClimbing()
{
var ladder = character.SelectedSecondaryItem?.GetComponent<Ladder>();
if (character.IsIncapacitated)
{
Anim = Animation.None;
return;
}
else if (ladder == null)
{
StopClimbing();
return;
}
onGround = false;
IgnorePlatforms = true;
bool climbFast = targetMovement.Y > 3.0f;
bool slide = targetMovement.Y < -1.1f;
Vector2 tempTargetMovement = TargetMovement;
tempTargetMovement.Y = climbFast ?
Math.Min(tempTargetMovement.Y, 2.0f) :
Math.Min(tempTargetMovement.Y, 1.0f);
movement = MathUtils.SmoothStep(movement, tempTargetMovement, 0.3f);
Limb leftFoot = GetLimb(LimbType.LeftFoot);
Limb rightFoot = GetLimb(LimbType.RightFoot);
Limb head = GetLimb(LimbType.Head);
Limb torso = GetLimb(LimbType.Torso);
Limb leftHand = GetLimb(LimbType.LeftHand);
Limb rightHand = GetLimb(LimbType.RightHand);
if (leftHand == null || rightHand == null || head == null || torso == null) { return; }
Vector2 ladderSimPos = ConvertUnits.ToSimUnits(
ladder.Item.Rect.X + ladder.Item.Rect.Width / 2.0f,
ladder.Item.Rect.Y);
Vector2 ladderSimSize = ConvertUnits.ToSimUnits(ladder.Item.Rect.Size.ToVector2());
float lowestLadderSimPos = ladderSimPos.Y - ladderSimPos.Y;
var lowestNearbyLadder = GetLowestNearbyLadder(ladder);
if (lowestNearbyLadder != null && lowestNearbyLadder != ladder)
{
ladderSimSize.Y = ConvertUnits.ToSimUnits(ladder.Item.WorldRect.Y - (lowestNearbyLadder.Item.WorldRect.Y - lowestNearbyLadder.Item.Rect.Size.Y));
}
float stepHeight = ConvertUnits.ToSimUnits(30.0f);
if (climbFast) { stepHeight *= 2; }
if (currentHull == null && ladder.Item.Submarine != null)
{
ladderSimPos += ladder.Item.Submarine.SimPosition;
}
else if (currentHull?.Submarine != null && currentHull.Submarine != ladder.Item.Submarine && ladder.Item.Submarine != null)
{
ladderSimPos += ladder.Item.Submarine.SimPosition - currentHull.Submarine.SimPosition;
}
else if (currentHull?.Submarine != null && ladder.Item.Submarine == null)
{
ladderSimPos -= currentHull.Submarine.SimPosition;
}
float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.Radius - Collider.Height / 2.0f;
float torsoPos = TorsoPosition ?? 0;
MoveLimb(torso, new Vector2(ladderSimPos.X - 0.35f * Dir, bottomPos + torsoPos), 10.5f);
float headPos = HeadPosition ?? 0;
MoveLimb(head, new Vector2(ladderSimPos.X - 0.2f * Dir, bottomPos + headPos), 10.5f);
Collider.MoveToPos(new Vector2(ladderSimPos.X - 0.1f * Dir, Collider.SimPosition.Y), 10.5f);
Vector2 handPos = new Vector2(
ladderSimPos.X,
bottomPos + torsoPos + movement.Y * 0.1f - ladderSimPos.Y);
if (climbFast) { handPos.Y -= stepHeight; }
//prevent the hands from going above the top of the ladders
handPos.Y = Math.Min(-0.5f, handPos.Y);
if (!Aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand)?.GetComponent<Holdable>()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f)
{
MoveLimb(rightHand,
new Vector2(slide ? handPos.X + ladderSimSize.X * 0.5f : handPos.X,
(slide ? handPos.Y : MathUtils.Round(handPos.Y, stepHeight * 2.0f)) + ladderSimPos.Y),
5.2f);
rightHand.body.ApplyTorque(Dir * 2.0f);
}
if (!Aiming || !(character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand)?.GetComponent<Holdable>()?.ControlPose ?? false) || Math.Abs(movement.Y) > 0.01f)
{
MoveLimb(leftHand,
new Vector2(handPos.X - ladderSimSize.X * 0.5f,
(slide ? handPos.Y : MathUtils.Round(handPos.Y - stepHeight, stepHeight * 2.0f) + stepHeight) + ladderSimPos.Y),
5.2f); ;
leftHand.body.ApplyTorque(Dir * 2.0f);
}
Vector2 footPos = new Vector2(
handPos.X - Dir * 0.05f,
bottomPos + ColliderHeightFromFloor - stepHeight * 2.7f - ladderSimPos.Y);
if (climbFast) { footPos.Y += stepHeight; }
//apply torque to the legs to make the knees bend
Limb leftLeg = GetLimb(LimbType.LeftLeg);
Limb rightLeg = GetLimb(LimbType.RightLeg);
//only move the feet if they're above the bottom of the ladders
//(if not, they'll just dangle in air, and the character holds itself up with it's arms)
if (footPos.Y > -ladderSimSize.Y - 0.2f && leftFoot != null && rightFoot != null)
{
Limb refLimb = GetLimb(LimbType.Waist) ?? GetLimb(LimbType.Torso);
bool leftLegBackwards = Math.Abs(leftLeg.body.Rotation - refLimb.body.Rotation) > MathHelper.Pi;
bool rightLegBackwards = Math.Abs(rightLeg.body.Rotation - refLimb.body.Rotation) > MathHelper.Pi;
if (slide)
{
if (!leftLegBackwards) { MoveLimb(leftFoot, new Vector2(footPos.X - ladderSimSize.X * 0.5f, footPos.Y + ladderSimPos.Y), 15.5f, true); }
if (!rightLegBackwards) { MoveLimb(rightFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), 15.5f, true); }
}
else
{
float leftFootPos = MathUtils.Round(footPos.Y + stepHeight, stepHeight * 2.0f) - stepHeight;
float prevLeftFootPos = MathUtils.Round(prevFootPos + stepHeight, stepHeight * 2.0f) - stepHeight;
if (!leftLegBackwards) { MoveLimb(leftFoot, new Vector2(footPos.X, leftFootPos + ladderSimPos.Y), 15.5f, true); }
float rightFootPos = MathUtils.Round(footPos.Y, stepHeight * 2.0f);
float prevRightFootPos = MathUtils.Round(prevFootPos, stepHeight * 2.0f);
if (!rightLegBackwards) { MoveLimb(rightFoot, new Vector2(footPos.X, rightFootPos + ladderSimPos.Y), 15.5f, true); }
#if CLIENT
if (Math.Abs(leftFootPos - prevLeftFootPos) > stepHeight && leftFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval)
{
SoundPlayer.PlaySound("footstep_armor_heavy", leftFoot.WorldPosition, hullGuess: currentHull);
leftFoot.LastImpactSoundTime = (float)Timing.TotalTime;
}
if (Math.Abs(rightFootPos - prevRightFootPos) > stepHeight && rightFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval)
{
SoundPlayer.PlaySound("footstep_armor_heavy", rightFoot.WorldPosition, hullGuess: currentHull);
rightFoot.LastImpactSoundTime = (float)Timing.TotalTime;
}
#endif
prevFootPos = footPos.Y;
}
if (!leftLegBackwards) { leftLeg.body.ApplyTorque(Dir * -8.0f); }
if (!rightLegBackwards) { rightLeg.body.ApplyTorque(Dir * -8.0f); }
}
float movementFactor = (handPos.Y / stepHeight) * (float)Math.PI;
movementFactor = 0.8f + (float)Math.Abs(Math.Sin(movementFactor));
Vector2 subSpeed = currentHull != null || ladder.Item.Submarine == null
? Vector2.Zero : ladder.Item.Submarine.Velocity;
//reached the top of the ladders -> can't go further up
Vector2 climbForce = new Vector2(0.0f, movement.Y) * movementFactor;
if (!InWater) { climbForce.Y += 0.3f * movementFactor; }
if (character.SimPosition.Y > ladderSimPos.Y) { climbForce.Y = Math.Min(0.0f, climbForce.Y); }
//reached the bottom -> can't go further down
float minHeightFromFloor = ColliderHeightFromFloor / 2 + Collider.Height;
if (floorFixture != null &&
!floorFixture.CollisionCategories.HasFlag(Physics.CollisionStairs) &&
!floorFixture.CollisionCategories.HasFlag(Physics.CollisionPlatform) &&
character.SimPosition.Y < standOnFloorY + minHeightFromFloor)
{
climbForce.Y = MathHelper.Clamp((standOnFloorY + minHeightFromFloor - character.SimPosition.Y) * 5.0f, climbForce.Y, 1.0f);
}
//apply forces to the collider to move the Character up/down
Collider.ApplyForce((climbForce * 20.0f + subSpeed * 50.0f) * Collider.Mass);
if (Aiming)
{
RotateHead(head);
}
else if (Anim == Animation.UsingItemWhileClimbing && character.SelectedItem is { } selectedItem)
{
Vector2 diff = (selectedItem.WorldPosition - head.WorldPosition) * Dir;
float targetRotation = MathHelper.WrapAngle(MathUtils.VectorToAngle(diff) - MathHelper.PiOver4 * Dir);
head.body.SmoothRotate(targetRotation, force: WalkParams.HeadTorque);
}
else
{
float movementMultiplier = targetMovement.Y < 0 ? 0 : 1;
head.body.SmoothRotate(MathHelper.PiOver4 * movementMultiplier * Dir, force: WalkParams.HeadTorque);
}
if (ladder.Item.Prefab.Triggers.None())
{
character.ReleaseSecondaryItem();
return;
}
Rectangle trigger = ladder.Item.Prefab.Triggers.FirstOrDefault();
trigger = ladder.Item.TransformTrigger(trigger);
bool isRemote = false;
bool isClimbing = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
isRemote = character.IsRemotelyControlled;
}
if (isRemote)
{
if (Math.Abs(targetMovement.X) > 0.05f ||
(TargetMovement.Y < 0.0f && ConvertUnits.ToSimUnits(trigger.Height) + handPos.Y < HeadPosition) ||
(TargetMovement.Y > 0.0f && handPos.Y > 0.1f))
{
isClimbing = false;
}
}
else if ((character.IsKeyDown(InputType.Left) || character.IsKeyDown(InputType.Right)) &&
(!character.IsKeyDown(InputType.Up) && !character.IsKeyDown(InputType.Down)))
{
isClimbing = false;
}
if (!isClimbing)
{
character.StopClimbing();
IgnorePlatforms = false;
}
Ladder GetLowestNearbyLadder(Ladder currentLadder, float threshold = 16.0f)
{
foreach (Ladder ladder in Ladder.List)
{
if (ladder == currentLadder || !ladder.Item.IsInteractable(character)) { continue; }
if (Math.Abs(ladder.Item.WorldPosition.X - currentLadder.Item.WorldPosition.X) > threshold) { continue; }
if (ladder.Item.WorldPosition.Y > currentLadder.Item.WorldPosition.Y) { continue; }
if ((currentLadder.Item.WorldRect.Y - currentLadder.Item.Rect.Height) - ladder.Item.WorldRect.Y > threshold) { continue; }
return ladder;
}
return null;
}
}
void UpdateFallingProne(float strength, bool moveHands = true, bool moveTorso = true, bool moveLegs = true)
{
if (strength <= 0.0f) { return; }
@@ -1518,7 +1283,7 @@ namespace Barotrauma
float cprBoost = character.GetStatValue(StatTypes.CPRBoost);
int skill = (int)character.GetSkillLevel("medical");
int skill = (int)character.GetSkillLevel(Tags.MedicalSkill);
if (GameMain.NetworkMember is not { IsClient: true })
{
@@ -1595,7 +1360,7 @@ namespace Barotrauma
//otherwise it's easy to abuse the system by repeatedly reviving in a low-oxygen room
if (!target.IsDead)
{
target.CharacterHealth.CalculateVitality();
target.CharacterHealth.RecalculateVitality();
if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f)
{
character.Info?.ApplySkillGain(Tags.MedicalSkill, SkillSettings.Current.SkillIncreasePerCprRevive);
@@ -1876,23 +1641,11 @@ namespace Barotrauma
}
}
}
private void RotateHead(Limb head)
public void Crouch()
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 dir = (mousePos - head.SimPosition) * Dir;
float rot = MathUtils.VectorToAngle(dir);
var neckJoint = GetJointBetweenLimbs(LimbType.Head, LimbType.Torso);
if (neckJoint != null)
{
float offset = MathUtils.WrapAnglePi(GetLimb(LimbType.Torso).body.Rotation);
float lowerLimit = neckJoint.LowerLimit + offset;
float upperLimit = neckJoint.UpperLimit + offset;
float min = Math.Min(lowerLimit, upperLimit);
float max = Math.Max(lowerLimit, upperLimit);
rot = Math.Clamp(rot, min, max);
}
head.body.SmoothRotate(rot, CurrentAnimationParams.HeadTorque);
Crouching = true;
character.SetInput(InputType.Crouch, hit: false, held: true);
}
private void FootIK(Limb foot, Vector2 pos, float legTorque, float footTorque, float footAngle)
@@ -91,7 +91,7 @@ namespace Barotrauma
private bool simplePhysicsEnabled;
public Character Character => character;
protected Character character;
protected readonly Character character;
protected float strongestImpact;
@@ -385,15 +385,16 @@ namespace Barotrauma
if (ragdollParams != null)
{
RagdollParams = ragdollParams;
if (!character.VariantOf.IsEmpty)
{
RagdollParams.TryApplyVariantScale(character.Params.VariantFile);
}
}
else
{
// Only re-equip items if the ragdoll doesn't change, because re-equiping items might throw exceptions if the limbs have changed.
items = limbs?.ToDictionary(l => l.Params, l => l.WearingItems);
}
if (character.Params.VariantFile is XDocument variantFile)
{
RagdollParams.TryApplyVariantScale(variantFile);
}
foreach (var limbParams in RagdollParams.Limbs)
{
if (!PhysicsBody.IsValidShape(limbParams.Radius, limbParams.Height, limbParams.Width))
@@ -430,18 +431,13 @@ namespace Barotrauma
if (character.IsHusk && character.Params.UseHuskAppendage)
{
bool inEditor = false;
#if CLIENT
inEditor = Screen.Selected == GameMain.CharacterEditorScreen;
#endif
var characterPrefab = CharacterPrefab.FindByFilePath(character.ConfigPath);
if (characterPrefab?.ConfigElement != null)
{
var mainElement = characterPrefab.ConfigElement;
foreach (var huskAppendage in mainElement.GetChildElements("huskappendage"))
{
if (!inEditor && huskAppendage.GetAttributeBool("onlyfromafflictions", false)) { continue; }
if (huskAppendage.GetAttributeBool("onlyfromafflictions", false)) { continue; }
Identifier afflictionIdentifier = huskAppendage.GetAttributeIdentifier("affliction", Identifier.Empty);
if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out AfflictionPrefab affliction) ||
@@ -452,7 +448,7 @@ namespace Barotrauma
}
else
{
AfflictionHusk.AttachHuskAppendage(character, matchingAffliction, huskAppendage, ragdoll: this);
AfflictionHusk.AttachHuskAppendage(character, matchingAffliction, huskedSpeciesName: character.SpeciesName, huskAppendage, ragdoll: this);
}
}
}
@@ -478,7 +474,7 @@ namespace Barotrauma
DebugConsole.ThrowError("Invalid collider dimensions: " + cParams.Name);
break; ;
}
var body = new PhysicsBody(cParams);
var body = new PhysicsBody(cParams, findNewContacts: false);
collider.Add(body);
body.UserData = character;
body.FarseerBody.OnCollision += OnLimbCollision;
@@ -520,7 +516,7 @@ namespace Barotrauma
{
if (joint == null) { continue; }
float angle = (joint.LowerLimit + joint.UpperLimit) / 2.0f;
joint.LimbB?.body?.SetTransform(
joint.LimbB?.body?.SetTransformIgnoreContacts(
(joint.WorldAnchorA - MathUtils.RotatePointAroundTarget(joint.LocalAnchorB, Vector2.Zero, joint.BodyA.Rotation + angle, true)),
joint.BodyA.Rotation + angle);
}
@@ -529,10 +525,11 @@ namespace Barotrauma
protected void CreateLimbs()
{
limbs?.ForEach(l => l.Remove());
Mass = 0;
DebugConsole.Log($"Creating limbs from {RagdollParams.Name}.");
limbDictionary = new Dictionary<LimbType, Limb>();
limbs = new Limb[RagdollParams.Limbs.Count];
RagdollParams.Limbs.ForEach(l => AddLimb(l));
RagdollParams.Limbs.ForEach(AddLimb);
if (limbs.Contains(null)) { return; }
SetupDrawOrder();
}
@@ -549,11 +546,11 @@ namespace Barotrauma
/// <summary>
/// Resets the serializable data to the currently selected ragdoll params.
/// Force reloading always loads the xml stored on the disk.
/// Always loads the xml stored on the disk.
/// </summary>
public void ResetRagdoll(bool forceReload = false)
public void ResetRagdoll()
{
RagdollParams.Reset(forceReload);
RagdollParams.Reset(forceReload: true);
ResetJoints();
ResetLimbs();
}
@@ -577,7 +574,7 @@ namespace Barotrauma
public void AddJoint(JointParams jointParams)
{
if (!checkLimbIndex(jointParams.Limb2, "Limb1") || !checkLimbIndex(jointParams.Limb2, "Limb2"))
if (!checkLimbIndex(jointParams.Limb1, "Limb1") || !checkLimbIndex(jointParams.Limb2, "Limb2"))
{
return;
}
@@ -683,8 +680,7 @@ namespace Barotrauma
}
LimbJoints = newJoints;
}
SubtractMass(limb);
limb.Remove();
foreach (LimbJoint limbJoint in attachedJoints)
{
@@ -1400,7 +1396,12 @@ namespace Barotrauma
limb.Update(deltaTime);
}
if (!inWater && character.AllowInput && levitatingCollider)
bool isAttachedToController =
character.SelectedItem?.GetComponent<Items.Components.Controller>() is { } controller &&
controller.User == character &&
controller.IsAttachedUser(controller.User);
if (!inWater && character.AllowInput && levitatingCollider && !isAttachedToController)
{
if (onGround && Collider.LinearVelocity.Y > -ImpactTolerance)
{
@@ -1910,7 +1911,7 @@ namespace Barotrauma
}
else
{
Collider.SetTransform(simPosition, Collider.Rotation);
Collider.SetTransformIgnoreContacts(simPosition, Collider.Rotation);
}
if (!MathUtils.NearlyEqual(limbMoveAmount, Vector2.Zero))
@@ -2009,7 +2010,7 @@ namespace Barotrauma
}
else
{
limb.body.SetTransform(movePos, rotation);
limb.body.SetTransformIgnoreContacts(movePos, rotation);
limb.PullJointWorldAnchorB = limb.PullJointWorldAnchorA;
limb.PullJointEnabled = false;
}
@@ -2113,26 +2114,48 @@ namespace Barotrauma
/// <summary>
/// Note that if there are multiple limbs of the same type, only the first (valid) limb is returned.
/// </summary>
public Limb GetLimb(LimbType limbType, bool excludeSevered = true)
/// <param name="limbType"></param>
/// <param name="excludeSevered">Should we filter out severed limbs?</param>
/// <param name="useSecondaryType">Should we target limbs with secondary type instead of (primary) type?</param>
/// <param name="excludeLimbsWithSecondaryType">Should we filter out all limbs with a secondary type something else than "None"?</param>
/// <returns></returns>
public Limb GetLimb(LimbType limbType, bool excludeSevered = true, bool excludeLimbsWithSecondaryType = false, bool useSecondaryType = false)
{
if (limbDictionary.TryGetValue(limbType, out Limb limb))
Limb limb = null;
if (!HasMultipleLimbsOfSameType && !useSecondaryType && !excludeLimbsWithSecondaryType)
{
if (excludeSevered && limb.IsSevered)
// Faster method, but doesn't work when there's multiple limbs of the same type or if we want to seek/exclude limbs with different conditions.
if (limbDictionary.TryGetValue(limbType, out limb))
{
limb = null;
}
if (limb.Removed)
{
limb = null;
}
if (excludeSevered && limb is { IsSevered: true } )
{
limb = null;
}
}
}
if (limb == null && HasMultipleLimbsOfSameType)
if (limb == null)
{
// Didn't find a (valid) limb of the matching type. If there's multiple limbs of the same type, check the other limbs.
// Didn't seek or find a (valid) limb of the matching type. If there's multiple limbs of the same type, check the other limbs.
foreach (var l in limbs)
{
if (l.type != limbType) { continue; }
if (!excludeSevered || !l.IsSevered)
if (l.Removed) { continue; }
if (useSecondaryType)
{
limb = l;
break;
if (l.Params.SecondaryType != limbType) { continue; }
}
else if (l.type != limbType)
{
continue;
}
if (excludeSevered && l.IsSevered) { continue; }
if (excludeLimbsWithSecondaryType && l.Params.SecondaryType != LimbType.None) { continue; }
// Found a valid and match
limb = l;
break;
}
}
return limb;
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -24,11 +25,22 @@ namespace Barotrauma
NotDefined
}
[Flags]
public enum AttackTarget
{
Any,
Character,
Structure // Including hulls etc. Evaluated as anything but a character.
Any = 0,
/// <summary>
/// Characters only
/// </summary>
Character = 1,
/// <summary>
/// Structures and hulls, but also items (for backwards support)!
/// </summary>
Structure = 2,
/// <summary>
/// Items only
/// </summary>
Item = 4
}
public enum AIBehaviorAfterAttack
@@ -816,15 +828,28 @@ namespace Barotrauma
return true;
}
public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType;
public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType.HasAnyFlag(targetType);
public bool IsValidTarget(Entity target)
{
return TargetType switch
{
AttackTarget.Character => target is Character,
AttackTarget.Structure => !(target is Character),
_ => true,
AttackTarget.Structure => target is Structure or Hull or Item, // Items are intentionally included for backwards-support.
AttackTarget.Item => target is Item,
_ => IsValidTarget(GetAttackTargetTypeFromEntity(target))
};
}
private static AttackTarget GetAttackTargetTypeFromEntity(Entity entity)
{
return entity switch
{
Character => AttackTarget.Character,
Item => AttackTarget.Item,
Structure => AttackTarget.Structure,
Hull => AttackTarget.Structure,
_ => AttackTarget.Any
};
}
@@ -213,7 +213,6 @@ namespace Barotrauma
}
}
private CharacterTeamType? originalTeamID;
public CharacterTeamType OriginalTeamID
{
@@ -242,6 +241,26 @@ namespace Barotrauma
protected readonly Dictionary<string, ActiveTeamChange> activeTeamChanges = new Dictionary<string, ActiveTeamChange>();
protected ActiveTeamChange currentTeamChange;
private const string OriginalChangeTeamIdentifier = "original";
public bool AllowPlayDead { get; set; }
public void EvaluatePlayDeadProbability(float? probability = null)
{
if (Params.AI is CharacterParams.AIParams aiParams)
{
if (probability.HasValue)
{
// Override so that can't revert back to the old value.
aiParams.PlayDeadProbability = probability.Value;
}
AllowPlayDead = Rand.Value() <= aiParams.PlayDeadProbability;
}
else if (probability.HasValue)
{
AllowPlayDead = Rand.Value() <= probability.Value;
}
// Do nothing, if no value is defined and no AI Params were found.
}
private void ThrowIfAccessingWalletsInSingleplayer()
{
@@ -254,26 +273,37 @@ namespace Barotrauma
}
}
public void SetOriginalTeam(CharacterTeamType newTeam)
/// <summary>
/// Saves the character's original team (which affects e.g. whether the character considers the sub/outpost they're in to be their own or a "foreign" one),
/// and adds a new team change to be processed on the next update.
/// </summary>
/// <param name="processImmediately">Should the team change be processed right now, or along with any other pending team changes in the next Update?</param>
public void SetOriginalTeamAndChangeTeam(CharacterTeamType newTeam, bool processImmediately = false)
{
TryRemoveTeamChange(OriginalChangeTeamIdentifier);
currentTeamChange = new ActiveTeamChange(newTeam, ActiveTeamChange.TeamChangePriorities.Base);
TryAddNewTeamChange(OriginalChangeTeamIdentifier, currentTeamChange);
if (processImmediately)
{
UpdateTeam();
}
}
private void ChangeTeam(CharacterTeamType newTeam)
{
if (newTeam == teamID) { return; }
if (originalTeamID == null) { originalTeamID = teamID; }
originalTeamID ??= teamID;
TeamID = newTeam;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
return;
}
// clear up any duties the character might have had from its old team (autonomous objectives are automatically recreated)
var order = OrderPrefab.Dismissal.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: this).WithManualPriority(CharacterInfo.HighestManualOrderPriority);
SetOrder(order, isNewOrder: true, speak: false);
if (AIController is HumanAIController)
{
// clear up any duties the character might have had from its old team (autonomous objectives are automatically recreated)
var order = OrderPrefab.Dismissal.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: this).WithManualPriority(CharacterInfo.HighestManualOrderPriority);
SetOrder(order, isNewOrder: true, speak: false);
}
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(this, new TeamChangeEventData());
#endif
@@ -292,7 +322,7 @@ namespace Barotrauma
if (currentTeamChange == null)
{
// set team logic to use active team changes as soon as the first team change is added
SetOriginalTeam(TeamID);
SetOriginalTeamAndChangeTeam(TeamID);
}
}
else
@@ -330,12 +360,13 @@ namespace Barotrauma
bestTeamChange = desiredTeamChange.Value;
}
}
if (TeamID != bestTeamChange.DesiredTeamId)
if (TeamID != bestTeamChange.DesiredTeamId)
{
ChangeTeam(bestTeamChange.DesiredTeamId);
currentTeamChange = bestTeamChange;
if (bestTeamChange.AggressiveBehavior) // this seemed like the least disruptive way to induce aggressive behavior
// this seemed like the least disruptive way to induce aggressive behavior on human characters
if (bestTeamChange.AggressiveBehavior && AIController is HumanAIController)
{
var order = OrderPrefab.Prefabs["fightintruders"].CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: this).WithManualPriority(CharacterInfo.HighestManualOrderPriority);
SetOrder(order, isNewOrder: true, speak: false);
@@ -343,11 +374,13 @@ namespace Barotrauma
}
}
public bool IsOnPlayerTeam => teamID == CharacterTeamType.Team1 || teamID == CharacterTeamType.Team2;
public bool IsOnPlayerTeam =>
teamID == CharacterTeamType.Team1 ||
(teamID == CharacterTeamType.Team2 && !IsFriendlyNPCTurnedHostile);
public bool IsOriginallyOnPlayerTeam => originalTeamID == CharacterTeamType.Team1 || originalTeamID == CharacterTeamType.Team2;
public bool IsFriendlyNPCTurnedHostile => originalTeamID == CharacterTeamType.FriendlyNPC && teamID == CharacterTeamType.Team2;
public bool IsFriendlyNPCTurnedHostile => originalTeamID == CharacterTeamType.FriendlyNPC && (teamID == CharacterTeamType.Team2 || teamID == CharacterTeamType.None);
public bool IsInstigator => CombatAction is { IsInstigator: true };
@@ -413,15 +446,15 @@ namespace Barotrauma
public Identifier GetBaseCharacterSpeciesName() => Prefab.GetBaseCharacterSpeciesName(SpeciesName);
public Identifier Group => HumanPrefab is HumanPrefab humanPrefab && !humanPrefab.Group.IsEmpty ? humanPrefab.Group : Params.Group;
public Identifier Group => HumanPrefab is { Group.IsEmpty: false } prefab ? prefab.Group : Params.Group;
public bool IsHumanoid => Params.Humanoid;
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 IsDisguisedAsHusk => CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.DisguisedAsHuskType) > 0;
public bool IsHuskInfected => CharacterHealth.GetActiveAfflictionTags().Contains(Tags.HuskInfected);
public bool IsMale => info?.IsMale ?? false;
@@ -668,6 +701,8 @@ namespace Barotrauma
// Eating is not implemented for humanoids. If we implement that at some point, we could remove this restriction.
public bool CanEat => !IsHumanoid && Params.CanEat && AllowInput && AnimController.GetLimb(LimbType.Head) != null;
public bool CanClimb => Params.CanClimb && CanInteract;
public Vector2 CursorPosition
{
@@ -862,7 +897,7 @@ namespace Barotrauma
private float ragdollingLockTimer;
public bool IsRagdolled;
public bool IsForceRagdolled;
public bool dontFollowCursor;
public bool FollowCursor = true;
public bool IsIncapacitated
{
@@ -1145,7 +1180,7 @@ namespace Barotrauma
{
get
{
return (SelectedItem == null || SelectedItem.GetComponent<Controller>() is { AllowAiming: true }) && !IsIncapacitated && (!IsRagdolled || AnimController.IsHoldingToRope);
return (SelectedItem == null || SelectedItem.GetComponent<Controller>() is { AllowAiming: true }) && !IsKnockedDown && (!IsRagdolled || AnimController.IsHoldingToRope);
}
}
@@ -1368,6 +1403,14 @@ namespace Barotrauma
//no longer a new hire after spawning (only displayed as a new hire at the end of the outpost round, when the character hasn't spawned yet)
Info.IsNewHire = false;
}
if (characterInfo?.HumanPrefabIds is { } prefabIds &&
prefabIds.NpcSetIdentifier != default && prefabIds.NpcIdentifier != default)
{
humanPrefab = NPCSet.Get(
characterInfo.HumanPrefabIds.NpcSetIdentifier,
characterInfo.HumanPrefabIds.NpcIdentifier);
}
keys = new Key[Enum.GetNames(typeof(InputType)).Length];
for (int i = 0; i < Enum.GetNames(typeof(InputType)).Length; i++)
{
@@ -1460,32 +1503,38 @@ namespace Barotrauma
CharacterHealth = new CharacterHealth(selectedHealthElement, this, limbHealthElement);
}
if (Params.Husk && speciesName != "husk" && Prefab.VariantOf != "husk")
if (Params.Husk)
{
Identifier nonHuskedSpeciesName = Identifier.Empty;
AfflictionPrefabHusk matchingAffliction = null;
foreach (var huskPrefab in AfflictionPrefab.Prefabs.OfType<AfflictionPrefabHusk>())
Identifier nonHuskedSpeciesName = Params.NonHuskedSpecies;
if (!nonHuskedSpeciesName.IsEmpty || Params.UseHuskAppendage)
{
var nonHuskedName = AfflictionHusk.GetNonHuskedSpeciesName(speciesName, huskPrefab);
if (huskPrefab.TargetSpecies.Contains(nonHuskedName))
// Check that there's a matching species and affliction for the non-husked species definition.
AfflictionPrefab matchingAffliction = null;
foreach (var huskPrefab in AfflictionPrefab.Prefabs.OfType<AfflictionPrefabHusk>())
{
var huskedSpeciesName = AfflictionHusk.GetHuskedSpeciesName(nonHuskedName, huskPrefab);
if (huskedSpeciesName.Equals(speciesName))
if (huskPrefab.HuskedSpeciesName.IsEmpty) { continue; }
Identifier nonHuskedSpecies = nonHuskedSpeciesName;
if (nonHuskedSpeciesName.IsEmpty)
{
nonHuskedSpeciesName = nonHuskedName;
nonHuskedSpecies = AfflictionHusk.GetNonHuskedSpeciesName(Params, huskPrefab);
}
if (huskPrefab.TargetSpecies.Contains(nonHuskedSpecies))
{
nonHuskedSpeciesName = nonHuskedSpecies;
matchingAffliction = huskPrefab;
break;
}
}
}
if (matchingAffliction == null || nonHuskedSpeciesName.IsEmpty)
{
DebugConsole.ThrowError($"Cannot find a husk infection that matches {speciesName}! Please make sure that the speciesname is added as 'targets' in the husk affliction prefab definition!\n"
+ "Note that all the infected speciesnames and files must stick the following pattern: [nonhuskedspeciesname][huskedspeciesname]. E.g. Humanhusk, Crawlerhusk, or Humancustomhusk, or Crawlerzombie. Not \"Customhumanhusk!\" or \"Zombiecrawler\"",
contentPackage: Prefab.ContentPackage);
// Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg.
nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler".ToIdentifier();
speciesName = nonHuskedSpeciesName;
}
if (matchingAffliction == null)
{
DebugConsole.ThrowError($"Cannot find a husk infection that matches {speciesName}! Please make sure that the speciesname is added as 'targets' in the husk affliction prefab definition! " +
$"If the name of the character doesn't match the default pattern ('Crawlerhusk', 'Humanhusk', etc), you'll also need to define the non-husked species with {nameof(Params.NonHuskedSpecies)} attribute in the character config file.",
contentPackage: Prefab.ContentPackage);
// Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg.
nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler".ToIdentifier();
speciesName = nonHuskedSpeciesName;
}
}
if (ragdollParams == null && prefab.VariantOf == null)
{
@@ -1745,7 +1794,7 @@ namespace Barotrauma
#endif
}
public void GiveJobItems(WayPoint spawnPoint = null)
public void GiveJobItems(bool isPvPMode, WayPoint spawnPoint = null)
{
if (info == null) { return; }
if (info.HumanPrefabIds != default)
@@ -1760,7 +1809,7 @@ namespace Barotrauma
return;
}
}
info.Job?.GiveJobItems(this, spawnPoint);
info.Job?.GiveJobItems(this, isPvPMode, spawnPoint);
}
public void GiveIdCardTags(WayPoint spawnPoint, bool createNetworkEvent = false)
@@ -1778,6 +1827,10 @@ namespace Barotrauma
{
item.AddTag(s);
}
if (GameMain.GameSession?.GameMode is PvPMode)
{
item.AddTag($"id_{TeamID}".ToIdentifier());
}
if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item));
@@ -1785,9 +1838,6 @@ namespace Barotrauma
}
}
public float GetSkillLevel(string skillIdentifier) =>
GetSkillLevel(skillIdentifier.ToIdentifier());
private static readonly ImmutableDictionary<Identifier, StatTypes> overrideStatTypes = new Dictionary<Identifier, StatTypes>
{
{ new("helm"), StatTypes.HelmSkillOverride },
@@ -1797,6 +1847,9 @@ namespace Barotrauma
{ new("mechanical"), StatTypes.MechanicalSkillOverride }
}.ToImmutableDictionary();
/// <summary>
/// Get the character's current skill level, taking into account any temporary boosts from wearables and afflictions
/// </summary>
public float GetSkillLevel(Identifier skillIdentifier)
{
if (Info?.Job == null) { return 0.0f; }
@@ -1839,7 +1892,11 @@ namespace Barotrauma
}
}
skillLevel += GetStatValue(GetSkillStatType(skillIdentifier));
var skillStatType = GetSkillStatType(skillIdentifier);
if (skillStatType != StatTypes.None)
{
skillLevel += GetStatValue(skillStatType);
}
return Math.Max(skillLevel, 0);
}
@@ -1876,11 +1933,30 @@ namespace Barotrauma
// - dragging someone
// - crouching
// - moving backwards
public bool CanRun => CanRunWhileDragging() &&
public bool CanRun =>
!DisableRunning &&
CanRunWhileDragging() &&
AnimController is not HumanoidAnimController { Crouching: true } &&
!AnimController.IsMovingBackwards && !HasAbilityFlag(AbilityFlags.MustWalk) &&
!AnimController.IsHoldingToRope;
private double disableRunningLastSet;
/// <summary>
/// Can be used to temporarily disable running using StatusEffects. Resets in 0.1 seconds if not set.
/// </summary>
public bool DisableRunning
{
get => disableRunningLastSet > Timing.TotalTime - 0.1;
set
{
if (value)
{
disableRunningLastSet = Timing.TotalTime;
}
}
}
public bool CanRunWhileDragging()
{
if (selectedCharacter is not { IsDraggable: true }) { return true; }
@@ -1919,8 +1995,7 @@ namespace Barotrauma
/// Can be used to modify the character's speed via StatusEffects
/// </summary>
public float SpeedMultiplier { get; private set; } = 1;
private double propulsionSpeedMultiplierLastSet;
private float propulsionSpeedMultiplier;
/// <summary>
@@ -1984,6 +2059,7 @@ namespace Barotrauma
/// </summary>
public float GetTemporarySpeedReduction()
{
if (!Params.Health.ApplyMovementPenalties) { return 0; }
float reduction = 0;
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightFoot, excludeSevered: false), reduction);
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftFoot, excludeSevered: false), reduction);
@@ -2021,6 +2097,7 @@ namespace Barotrauma
private float CalculateMovementPenalty(Limb limb, float sum, float max = 0.8f)
{
if (!Params.Health.ApplyMovementPenalties) { return 0; }
if (limb != null)
{
sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: AfflictionPrefab.DamageType));
@@ -2111,7 +2188,7 @@ namespace Barotrauma
((!IsClimbing && AnimController.OnGround) || (IsClimbing && IsKeyDown(InputType.Aim))) &&
!AnimController.InWater)
{
if (dontFollowCursor)
if (!FollowCursor)
{
AnimController.TargetDir = Direction.Right;
}
@@ -2242,7 +2319,7 @@ namespace Barotrauma
if (attackTarget != null)
{
if (!attack.IsValidTarget(attackTarget as Entity)) { return false; }
if (attackTarget is ISerializableEntity se && attackTarget is Character)
if (attackTarget is ISerializableEntity se and Character)
{
if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; }
}
@@ -2279,7 +2356,7 @@ namespace Barotrauma
if (Inventory != null)
{
if (IsKeyHit(InputType.DropItem))
if (IsKeyHit(InputType.DropItem) && Screen.Selected is { IsEditor: false })
{
foreach (Item item in HeldItems)
{
@@ -2423,6 +2500,7 @@ namespace Barotrauma
{
System.Diagnostics.Debug.Assert(target != null);
if (target == null || target.Removed) { return false; }
if (seeingEntity == null) { return false; }
if (CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
if (!target.AnimController.SimplePhysicsEnabled)
{
@@ -2583,6 +2661,39 @@ namespace Barotrauma
}
return null;
}
public bool HasHandsFull(out (Item leftHandItem, Item rightHandItem) items)
{
var leftHandItem = GetEquippedItem(slotType: InvSlotType.LeftHand);
var rightHandItem = GetEquippedItem(slotType: InvSlotType.RightHand);
items = (leftHandItem, rightHandItem);
bool handsFull = leftHandItem != null && rightHandItem != null;
return handsFull;
}
public bool TryPutItem(Item item, IEnumerable<InvSlotType> allowedSlots) => Inventory.TryPutItem(item, user: this, allowedSlots);
public bool TryPutItemInBag(Item item) => item != null && item.AllowedSlots.Contains(InvSlotType.Bag) && TryPutItem(item, CharacterInventory.BagSlot);
public bool TryPutItemInAnySlot(Item item) => item != null && item.AllowedSlots.Contains(InvSlotType.Any) && TryPutItem(item, CharacterInventory.AnySlot);
/// <summary>
/// Attempts to unequip an item.
/// First tries to put the item in any slot.
/// If that fails, tries to put in the bag slot.
/// If that too fails, drops the item.
/// </summary>
/// <returns>false only if the item is not equipped.</returns>
public bool Unequip(Item item)
{
if (!HasEquippedItem(item)) { return false; }
if (!TryPutItemInAnySlot(item))
{
if (!TryPutItemInBag(item))
{
item.Drop(this);
}
}
return true;
}
public bool CanAccessInventory(Inventory inventory, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.Limited)
{
@@ -2608,7 +2719,6 @@ namespace Barotrauma
if (container != null)
{
if (!container.HasRequiredItems(this, addMessage: false)) { return false; }
if (!container.AllowAccess) { return false; }
}
}
return true;
@@ -2771,6 +2881,12 @@ namespace Barotrauma
#endif
if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; }
Controller controller = item.GetComponent<Controller>();
if (controller != null && IsAnySelectedItem(item) && controller.IsAttachedUser(this))
{
return true;
}
if (item.ParentInventory != null)
{
return CanAccessInventory(item.ParentInventory);
@@ -2880,7 +2996,7 @@ namespace Barotrauma
{
//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; }
if (controller != null && controller.Direction != 0 && controller.Direction != AnimController.Direction) { 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
@@ -3358,7 +3474,9 @@ namespace Barotrauma
if (Inventory != null)
{
foreach (Item item in Inventory.AllItems)
//do not check for duplicates: this is code is called very frequently, and duplicates don't matter here,
//so it's better just to avoid the relatively expensive duplicate check
foreach (Item item in Inventory.GetAllItems(checkForDuplicates: false))
{
if (item.body == null || item.body.Enabled) { continue; }
item.SetTransform(SimPosition, 0.0f);
@@ -3751,41 +3869,51 @@ namespace Barotrauma
if (!IsDead || (CauseOfDeath?.Type == CauseOfDeathType.Disconnected && GameMain.GameSession?.Campaign != null)) { return; }
int subCorpseCount = 0;
if (Submarine != null)
{
subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine);
if (subCorpseCount < GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) { return; }
}
if (SelectedBy != null)
{
despawnTimer = 0.0f;
return;
}
float distToClosestPlayer = GetDistanceToClosestPlayer();
if (distToClosestPlayer > Params.DisableDistance)
{
//despawn in 1 minute if very far from all human players
despawnTimer = Math.Max(despawnTimer, GameSettings.CurrentConfig.CorpseDespawnDelay - 60.0f);
}
float despawnDelay = GameSettings.CurrentConfig.CorpseDespawnDelay;
float despawnPriority = 1.0f;
if (subCorpseCount > GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold)
if (GameMain.GameSession?.GameMode is PvPMode &&
GameMain.NetworkMember?.RespawnManager != null)
{
//despawn faster if there are lots of corpses in the sub (twice as many as the threshold -> despawn twice as fast)
despawnPriority += (subCorpseCount - GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) / (float)GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold;
//simpler despawning logic in PvP modes with respawning: just a short timer
despawnDelay = GameSettings.CurrentConfig.CorpseDespawnDelayPvP;
}
if (AIController is EnemyAIController)
else
{
//enemies despawn faster
despawnPriority *= 2.0f;
int subCorpseCount = 0;
if (Submarine != null)
{
subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine);
if (subCorpseCount < GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) { return; }
}
if (subCorpseCount > GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold)
{
//despawn faster if there are lots of corpses in the sub (twice as many as the threshold -> despawn twice as fast)
despawnPriority += (subCorpseCount - GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold) / (float)GameSettings.CurrentConfig.CorpsesPerSubDespawnThreshold;
}
float distToClosestPlayer = GetDistanceToClosestPlayer();
if (distToClosestPlayer > Params.DisableDistance)
{
//despawn in 1 minute if very far from all human players
despawnTimer = Math.Max(despawnTimer, despawnDelay - 60.0f);
}
if (AIController is EnemyAIController)
{
//enemies despawn faster
despawnPriority *= 2.0f;
}
}
despawnTimer += deltaTime * despawnPriority;
if (despawnTimer < GameSettings.CurrentConfig.CorpseDespawnDelay) { return; }
if (despawnTimer < despawnDelay) { return; }
Despawn();
}
@@ -3798,7 +3926,10 @@ namespace Barotrauma
IsHuman ?
Tags.DespawnContainer :
Params.DespawnContainer;
if (!despawnContainerId.IsEmpty)
//don't spawn duffel bags in PvP modes that include respawning, because it can lead to a ton of accumulated items in the sub/outpost
bool pvpWithRespawning = GameMain.GameSession?.GameMode is PvPMode && GameMain.NetworkMember?.RespawnManager != null;
if (!despawnContainerId.IsEmpty && !pvpWithRespawning)
{
var containerPrefab =
MapEntityPrefab.FindByIdentifier(despawnContainerId) as ItemPrefab ??
@@ -3933,8 +4064,6 @@ namespace Barotrauma
targetRange = Math.Min(targetRange, maxAIRange);
float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed);
newRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier);
if (!float.IsNaN(newRange))
{
aiTarget.SoundRange = newRange;
@@ -4165,13 +4294,24 @@ namespace Barotrauma
{
prevAiChatMessages.Remove(identifier);
}
//already sent a similar message a moment ago
if (identifier != Identifier.Empty && minDurationBetweenSimilar > 0.0f &&
(aiChatMessageQueue.Any(m => m.Identifier == identifier) || prevAiChatMessages.ContainsKey(identifier)))
if (minDurationBetweenSimilar > 0)
{
return;
if (identifier == Identifier.Empty)
{
#if DEBUG
// TODO: This is stupid. We shouldn't allow passing minDurationBetweenSimilar without an identifier in the first place, but need to think how to refactor this.
DebugConsole.AddWarning($"Called Character.Speak() with minDurationBetweenSimilar but didn't define the identifier! Cannot compare with the old messages. The message will be sent each time the function is called.");
Debugger.Break();
#endif
}
else if (aiChatMessageQueue.Any(m => m.Identifier == identifier) || prevAiChatMessages.ContainsKey(identifier))
{
//already sent a similar message a moment ago
return;
}
}
aiChatMessageQueue.Add(new AIChatMessage(message, messageType, identifier, delay));
}
@@ -4269,7 +4409,9 @@ namespace Barotrauma
Limb limbHit = targetLimb;
float impulseMagnitude = (attack.TargetImpulse + attack.TargetForce * attack.ImpactMultiplier) * deltaTime;
// TODO: should we apply deltatime only on TargetForce, not TargetImpulse? Changing this would have implications on many existing monster attacks, so all the monsters would have to be tested and possibly readjusted.
// Should be (attack.TargetImpulse + attack.TargetForce * deltaTime) * attack.ImpactMultiplier?
float impulseMagnitude = (attack.TargetImpulse + attack.TargetForce) * attack.ImpactMultiplier * deltaTime;
Vector2 attackImpulse = Vector2.Zero;
if (Math.Abs(impulseMagnitude) > 0.0f)
@@ -4303,7 +4445,7 @@ namespace Barotrauma
}
if (limbHit == null) { return new AttackResult(); }
Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld * attack.ImpactMultiplier;
Vector2 forceWorld = (attack.TargetImpulseWorld + attack.TargetForceWorld) * attack.ImpactMultiplier;
if (attacker != null)
{
forceWorld.X *= attacker.AnimController.Dir;
@@ -4445,35 +4587,21 @@ namespace Barotrauma
CreatureMetrics.RecordKill(target.SpeciesName);
}
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false)
/// <summary>
///
/// </summary>
/// <param name="recalculateVitality">Set false as an optimization only when you manually call <see cref="CharacterHealth.RecalculateVitality"/>. Only applies to limb specific afflictions.</param>
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false, bool ignoreDamageOverlay = false, bool recalculateVitality = true)
{
if (Removed) { return new AttackResult(); }
//character inside the sub received damage from a monster outside the sub
//can happen during normal gameplay if someone for example fires a ranged weapon from outside,
//the intention of this error message is to diagnose an issue with monsters being able to damage characters from outside
// Disabled, because this happens every now and then when the monsters can get in and out of the sub.
// if (attacker?.AIController is EnemyAIController && Submarine != null && attacker.Submarine == null)
// {
// string errorMsg = $"Character {Name} received damage from outside the sub while inside (attacker: {attacker.Name})";
// GameAnalyticsManager.AddErrorEventOnce("Character.DamageLimb:DamageFromOutside" + Name + attacker.Name,
// GameAnalyticsManager.ErrorSeverity.Warning,
// errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace());
//#if DEBUG
// DebugConsole.ThrowError(errorMsg);
//#endif
// }
SetStun(stun);
if (attacker != null && attacker != this && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire)
{
if (attacker.TeamID == TeamID)
{
afflictions = afflictions.Where(a => a.Prefab.IsBuff);
if (!afflictions.Any()) { return new AttackResult(); }
if (afflictions.None(a => a.Prefab.IsBuff)) { return new AttackResult(); }
}
}
@@ -4493,9 +4621,8 @@ namespace Barotrauma
}
bool wasDead = IsDead;
Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir);
float prevVitality = CharacterHealth.Vitality;
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier, penetration: penetration, attacker: attacker);
CharacterHealth.ApplyDamage(hitLimb, attackResult, allowStacking);
CharacterHealth.ApplyDamage(hitLimb, attackResult, allowStacking, recalculateVitality);
if (shouldImplode)
{
// Only used by assistant's True Potential talent. Has to run here in order to properly give kill credit when it activates.
@@ -4504,8 +4631,16 @@ namespace Barotrauma
if (attacker != this)
{
bool wasDamageOverlayVisible = CharacterHealth.ShowDamageOverlay;
if (ignoreDamageOverlay)
{
// Temporarily ignore damage overlay (husk transition damage)
CharacterHealth.ShowDamageOverlay = false;
}
OnAttacked?.Invoke(attacker, attackResult);
OnAttackedProjSpecific(attacker, attackResult, stun);
// Reset damage overlay
CharacterHealth.ShowDamageOverlay = wasDamageOverlayVisible;
if (!wasDead)
{
TryAdjustAttackerSkill(attacker, attackResult);
@@ -4633,6 +4768,13 @@ namespace Barotrauma
return;
}
}
// apply pvp stun resistance to humans (reduce stun amount via resist multiplier)
if (newStun > 0 && GameMain.NetworkMember is { } networkMember && GameMain.GameSession?.GameMode is PvPMode && IsHuman)
{
newStun = Math.Max(0, newStun - (newStun * networkMember.ServerSettings.PvPStunResist));
}
if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) { return; }
if (Math.Sign(newStun) != Math.Sign(Stun))
{
@@ -4811,6 +4953,24 @@ namespace Barotrauma
}
partial void ImplodeFX();
public void TurnIntoHusk(AfflictionPrefabHusk huskInfection = null, bool? playDead = null)
{
huskInfection ??= AfflictionPrefab.HuskInfection as AfflictionPrefabHusk;
if (huskInfection == null)
{
DebugConsole.ThrowError($"Cannot turn {Name} into husk, because husk infection was not found!", contentPackage: AfflictionPrefab.Prefabs.First().ContentPackage);
return;
}
// Randomize the start strength a bit, so that the husks don't turn at the same time, which can cause performance issues when turning multiple characters to husk at the same time.
float startStrength = Rand.Range(Math.Max(huskInfection.MaxStrength - 2, huskInfection.ActiveThreshold), huskInfection.MaxStrength);
startStrength *= MaxVitality / 100f;
CharacterHealth.ApplyAffliction(AnimController.MainLimb, huskInfection.Instantiate(startStrength));
if (playDead.HasValue)
{
AllowPlayDead = playDead.Value;
}
}
public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true)
{
@@ -5387,6 +5547,27 @@ namespace Barotrauma
public IReadOnlyCollection<CharacterTalent> CharacterTalents => characterTalents;
/// <summary>
/// Removes the talents the character has unlocked in their talent tree.
/// </summary>
public void ResetTalents(bool applyXpPenalty)
{
characterTalents.Clear();
abilityResistances.Clear();
abilityFlags = AbilityFlags.None;
CharacterHealth.RemoveAfflictions(affliction => affliction.Prefab.AfflictionType == Tags.AfflictionTypeTalentBuff);
statValues.Clear();
if (applyXpPenalty)
{
int currentLevel = info.GetCurrentLevel();
if (currentLevel > 0)
{
info.SetExperience(info.ExperiencePoints - CharacterInfo.ExperienceRequiredPerLevel(currentLevel));
}
}
}
public void LoadTalents()
{
List<Identifier> toBeRemoved = null;
@@ -5590,12 +5771,12 @@ namespace Barotrauma
#if CLIENT
public void SetMoney(int amount)
{
if (!(GameMain.GameSession?.Campaign is { } campaign)) { return; }
if (amount == campaign.Wallet.Balance) { return; }
if (Wallet == null) { return; }
if (amount == Wallet.Balance) { return; }
int prevAmount = campaign.Wallet.Balance;
campaign.Wallet.Balance = amount;
OnMoneyChanged(prevAmount, campaign.Wallet.Balance);
int prevAmount = Wallet.Balance;
Wallet.Balance = amount;
OnMoneyChanged(prevAmount, Wallet.Balance);
}
#endif
@@ -5788,9 +5969,12 @@ namespace Barotrauma
return myTeam switch
{
// NPCs are friendly to the same team and the friendly NPCs
CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC,
CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC,
// Friendly NPCs are friendly to both player teams
CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2,
// None (bandits and such) consider friendly NPCs friendly, not attacking them unless they attack first
// Otherwise bandits would for example attach the hostages.
CharacterTeamType.None => otherTeam == CharacterTeamType.FriendlyNPC,
_ => true
};
}
@@ -5802,6 +5986,8 @@ namespace Barotrauma
public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other);
public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || CharacterParams.CompareGroup(me.Group, other.Group);
public bool MatchesSpeciesNameOrGroup(Identifier speciesNameOrGroup) => Prefab.MatchesSpeciesNameOrGroup(speciesNameOrGroup);
public void StopClimbing()
{
@@ -29,11 +29,13 @@ namespace Barotrauma
UpdatePermanentStats = 14,
RemoveFromCrew = 15,
LatchOntoTarget = 16,
UpdateTalentRefundPoints = 17,
ConfirmTalentRefund = 18,
MinValue = 0,
MaxValue = 16
MaxValue = 18
}
private interface IEventData : NetEntityEvent.IData
{
public EventType EventType { get; }
@@ -230,9 +232,18 @@ namespace Barotrauma
public struct UpdateSkillsEventData : IEventData
{
public EventType EventType => EventType.UpdateSkills;
public readonly EventType EventType => EventType.UpdateSkills;
public readonly bool ForceNotification;
public readonly Identifier SkillIdentifier;
public UpdateSkillsEventData(Identifier skillIdentifier, bool forceNotification)
{
SkillIdentifier = skillIdentifier;
ForceNotification = forceNotification;
}
}
private struct UpdateMoneyEventData : IEventData
{
public EventType EventType => EventType.UpdateMoney;
@@ -242,11 +253,21 @@ namespace Barotrauma
{
public EventType EventType => EventType.UpdatePermanentStats;
public readonly StatTypes StatType;
public UpdatePermanentStatsEventData(StatTypes statType)
{
StatType = statType;
}
}
public struct UpdateRefundPointsEventData : IEventData
{
public EventType EventType => EventType.UpdateTalentRefundPoints;
}
public struct ConfirmRefundEventData : IEventData
{
public EventType EventType => EventType.ConfirmTalentRefund;
}
}
}
@@ -344,15 +344,37 @@ namespace Barotrauma
/// Note: Can be null.
/// </summary>
public Character Character;
public Job Job;
public int Salary;
public int ExperiencePoints { get; private set; }
private int talentRefundPoints;
/// <summary>
/// How many times the player is eligible to refund talents
/// </summary>
public int TalentRefundPoints
{
get => talentRefundPoints;
set => talentRefundPoints = MathHelper.Max(value, 0);
}
public HashSet<Identifier> UnlockedTalents { get; private set; } = new HashSet<Identifier>();
private int talentResetCount;
/// <summary>
/// How many times have the characters' talents been reset?
/// </summary>
public int TalentResetCount
{
get => talentResetCount;
set => talentResetCount = MathHelper.Max(value, 0);
}
public (Identifier factionId, float reputation) MinReputationToHire;
/// <summary>
@@ -708,7 +730,9 @@ namespace Barotrauma
SetAttachments(randSync);
SetColors(randSync);
Job = job ?? ((jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, randSync, variant));
Job = job ?? ((jobPrefab == null) ?
Job.Random(isPvP: false, Rand.RandSync.Unsynced) :
new Job(jobPrefab, isPvP: false, randSync, variant));
if (!string.IsNullOrEmpty(name))
{
@@ -725,6 +749,8 @@ namespace Barotrauma
}
OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name;
TalentRefundPoints = CharacterConfigElement.GetAttributeInt("refundpoints", 0);
int loadedLastRewardDistribution = CharacterConfigElement.GetAttributeInt("lastrewarddistribution", -1);
if (loadedLastRewardDistribution >= 0)
{
@@ -799,6 +825,7 @@ namespace Barotrauma
Salary = infoElement.GetAttributeInt("salary", 1000);
ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0);
AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0);
TalentResetCount = infoElement.GetAttributeInt(nameof(talentResetCount), 0);
HashSet<Identifier> tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToHashSet();
LoadTagsBackwardsCompatibility(infoElement, tags);
SpeciesName = infoElement.GetAttributeIdentifier("speciesname", "");
@@ -1047,6 +1074,7 @@ namespace Barotrauma
public string ReplaceVars(string str)
{
if (Head == null) { return str; }
return Prefab.ReplaceVars(str, Head.Preset);
}
@@ -1271,18 +1299,18 @@ namespace Barotrauma
/// Increases the characters skill at a rate proportional to their current skill.
/// If you want to increase the skill level by a specific amount instead, use <see cref="IncreaseSkillLevel"/>
/// </summary>
public void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility = false, float maxGain = 2f)
public void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility = false, float maxGain = 2f, bool forceNotification = false)
{
float skillLevel = Job.GetSkillLevel(skillIdentifier);
// The formula is too generous on low skill levels, hence the minimum divider.
float skillDivider = MathF.Pow(Math.Max(skillLevel, 15f), SkillSettings.Current.SkillIncreaseExponent);
IncreaseSkillLevel(skillIdentifier, Math.Min(baseGain / skillDivider, maxGain), gainedFromAbility);
IncreaseSkillLevel(skillIdentifier, Math.Min(baseGain / skillDivider, maxGain), gainedFromAbility, forceNotification);
}
/// <summary>
/// Increase the skill by a specific amount. Talents may affect the actual, final skill increase.
/// </summary>
public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool gainedFromAbility = false)
public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool gainedFromAbility = false, bool forceNotification = false)
{
if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; }
@@ -1312,14 +1340,14 @@ namespace Barotrauma
}
}
OnSkillChanged(skillIdentifier, prevLevel, newLevel);
OnSkillChanged(skillIdentifier, prevLevel, newLevel, forceNotification);
}
private static readonly ImmutableDictionary<Identifier, StatTypes> skillGainStatValues = new Dictionary<Identifier, StatTypes>
{
{ new("helm"), StatTypes.HelmSkillGainSpeed },
{ new("medical"), StatTypes.WeaponsSkillGainSpeed },
{ new("weapons"), StatTypes.MedicalSkillGainSpeed },
{ new("weapons"), StatTypes.WeaponsSkillGainSpeed },
{ new("medical"), StatTypes.MedicalSkillGainSpeed },
{ new("electrical"), StatTypes.ElectricalSkillGainSpeed },
{ new("mechanical"), StatTypes.MechanicalSkillGainSpeed }
}.ToImmutableDictionary();
@@ -1334,7 +1362,7 @@ namespace Barotrauma
return increase;
}
public void SetSkillLevel(Identifier skillIdentifier, float level)
public void SetSkillLevel(Identifier skillIdentifier, float level, bool forceNotification = false)
{
if (Job == null) { return; }
@@ -1342,17 +1370,17 @@ namespace Barotrauma
if (skill == null)
{
Job.IncreaseSkillLevel(skillIdentifier, level, increasePastMax: false);
OnSkillChanged(skillIdentifier, 0.0f, level);
OnSkillChanged(skillIdentifier, 0.0f, level, forceNotification);
}
else
{
float prevLevel = skill.Level;
skill.Level = level;
OnSkillChanged(skillIdentifier, prevLevel, skill.Level);
OnSkillChanged(skillIdentifier, prevLevel, skill.Level, forceNotification);
}
}
partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel);
partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel, bool forceNotification);
public void GiveExperience(int amount)
{
@@ -1437,10 +1465,11 @@ namespace Barotrauma
experienceRequired += ExperienceRequiredPerLevel(level);
level++;
}
return level;
return Math.Max(level, 0);
}
private static int ExperienceRequiredPerLevel(int level)
public static int ExperienceRequiredPerLevel(int level)
{
return BaseExperienceRequired + AddedExperienceRequiredPerLevel * level;
}
@@ -1449,6 +1478,45 @@ namespace Barotrauma
partial void OnPermanentStatChanged(StatTypes statType);
public void RefundTalents()
{
if (TalentRefundPoints <= 0) { return; }
//e.g. talents from endocrine booster or extra talents some special NPC has
var talentsFromOutsideTree = GetUnlockedTalentsOutsideTree().ToList();
bool applyXpPenalty = talentResetCount > 0;
UnlockedTalents.Clear();
SavedStatValues.Clear();
Character?.ResetTalents(applyXpPenalty);
TalentRefundPoints--;
talentResetCount++;
//it's simpler to just remove everything first and then reapply the "extra" talents than to
//try determining which talent the resistances, ability flags etc came from and only remove specific ones
if (Character == null)
{
talentsFromOutsideTree.ForEach(talentId => UnlockedTalents.Add(talentId));
}
else
{
talentsFromOutsideTree.ForEach(talentId => Character.GiveTalent(talentId, addingFirstTime: true));
}
GameMain.NetworkMember?.CreateEntityEvent(Character, new Character.ConfirmRefundEventData());
}
public void AddRefundPoints(int newRefundPoints)
{
TalentRefundPoints += newRefundPoints;
#if SERVER
GameMain.NetworkMember?.CreateEntityEvent(Character, new Character.UpdateRefundPointsEventData());
#elif CLIENT
ShowTalentResetPopupOnOpen = true;
#endif
}
public void Rename(string newName)
{
if (string.IsNullOrEmpty(newName)) { return; }
@@ -1491,6 +1559,7 @@ namespace Barotrauma
new XAttribute("salary", Salary),
new XAttribute("experiencepoints", ExperiencePoints),
new XAttribute("additionaltalentpoints", AdditionalTalentPoints),
new XAttribute(nameof(talentResetCount), TalentResetCount),
new XAttribute("hairindex", Head.HairIndex),
new XAttribute("beardindex", Head.BeardIndex),
new XAttribute("moustacheindex", Head.MoustacheIndex),
@@ -1500,6 +1569,7 @@ namespace Barotrauma
new XAttribute("facialhaircolor", XMLExtensions.ColorToString(Head.FacialHairColor)),
new XAttribute("startitemsgiven", StartItemsGiven),
new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty),
new XAttribute("refundpoints", TalentRefundPoints),
new XAttribute("lastrewarddistribution", LastRewardDistribution.Match(some: value => value, none: () => -1).ToString()),
new XAttribute("permanentlydead", PermanentlyDead),
new XAttribute("renamingenabled", RenamingEnabled)
@@ -1601,7 +1671,7 @@ namespace Barotrauma
targetAvailableInNextLevel =
!isOutside &&
GameMain.GameSession?.Campaign is not { SwitchedSubsThisRound: true } &&
(isOnConnectedLinkedSub || entitySub == Submarine.MainSub);
(isOnConnectedLinkedSub || (Submarine.MainSub != null && entitySub == Submarine.MainSub));
if (!targetAvailableInNextLevel)
{
if (!order.Prefab.CanBeGeneralized)
@@ -1636,7 +1706,7 @@ namespace Barotrauma
}
if (order.TargetSpatialEntity?.Submarine is Submarine targetSub)
{
if (targetSub == Submarine.MainSub)
if (Submarine.MainSub != null && targetSub == Submarine.MainSub)
{
orderElement.Add(new XAttribute("onmainsub", true));
}
@@ -33,6 +33,10 @@ namespace Barotrauma
}
public bool HasCharacterInfo { get; private set; }
public Identifier Group { get; private set; }
public bool MatchesSpeciesNameOrGroup(Identifier speciesNameOrGroup) => Identifier == speciesNameOrGroup || Group == speciesNameOrGroup;
public void InheritFrom(CharacterPrefab parent)
{
@@ -52,9 +56,10 @@ namespace Barotrauma
{
CharacterInfoPrefab = new CharacterInfoPrefab(this, headsElement, varsElement, menuCategoryElement, pronounsElement);
}
Group = ConfigElement.GetAttributeIdentifier(nameof(Group), Identifier.Empty);
}
private readonly XElement originalElement;
private readonly ContentXElement originalElement;
public ContentXElement ConfigElement { get; private set; }
public CharacterInfoPrefab CharacterInfoPrefab { get; private set; }
@@ -337,18 +337,24 @@ namespace Barotrauma
}
}
public float GetResistance(Identifier afflictionId)
/// <summary>
/// How much resistance to the specified affliction does this affliction currently give?
/// </summary>
public float GetResistance(Identifier afflictionId, LimbType limbType)
{
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
var affliction = AfflictionPrefab.Prefabs[afflictionId];
AfflictionPrefab.Effect currentEffect = GetActiveEffect();
if (currentEffect == null) { return 0.0f; }
if (!currentEffect.ResistanceFor.Any(r =>
r == affliction.Identifier ||
r == affliction.AfflictionType))
{
return 0.0f;
}
bool hasResistanceForAffliction = currentEffect.ResistanceFor.Any(identifier =>
identifier == affliction.Identifier ||
identifier == affliction.AfflictionType);
if (!hasResistanceForAffliction) { return 0.0f; }
bool hasResistanceForLimb = limbType == LimbType.None || currentEffect.ResistanceLimbs.None() || currentEffect.ResistanceLimbs.Contains(limbType);
if (!hasResistanceForLimb) { return 0.0f; }
return MathHelper.Lerp(
currentEffect.MinResistance,
currentEffect.MaxResistance,
@@ -430,7 +436,7 @@ namespace Barotrauma
}
else if (currentEffect.StrengthChange > 0) // Reduce strengthening of afflictions if resistant
{
_strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab));
_strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab, targetLimb?.type ?? LimbType.None));
}
// Don't use the property, because it's virtual and some afflictions like husk overload it for external use.
_strength = MathHelper.Clamp(_strength, 0.0f, Prefab.MaxStrength);
@@ -13,7 +13,7 @@
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
base.Update(characterHealth, targetLimb, deltaTime);
float bloodlossResistance = GetResistance(characterHealth.BloodlossAffliction.Identifier);
float bloodlossResistance = characterHealth.GetResistance(characterHealth.BloodlossAffliction.Prefab, targetLimb?.type ?? LimbType.None);
characterHealth.BloodlossAmount += Strength * (1.0f - bloodlossResistance) / 60.0f * deltaTime;
if (Source != null)
{
@@ -28,8 +28,6 @@ namespace Barotrauma
private bool stun = false;
private readonly List<Affliction> huskInfection = new List<Affliction>();
[Serialize(0f, IsPropertySaveable.Yes), Editable]
public override float Strength
{
@@ -62,7 +60,7 @@ namespace Barotrauma
}
}
private readonly AfflictionPrefabHusk HuskPrefab;
public readonly AfflictionPrefabHusk HuskPrefab;
private float DormantThreshold => HuskPrefab.DormantThreshold;
private float ActiveThreshold => HuskPrefab.ActiveThreshold;
@@ -129,7 +127,7 @@ namespace Barotrauma
{
State = InfectionState.Final;
ActivateHusk();
ApplyDamage(deltaTime, applyForce: true);
ApplyDamage(deltaTime);
character.SetStun(5);
}
}
@@ -192,27 +190,41 @@ namespace Barotrauma
prevDisplayedMessage = State;
}
private void ApplyDamage(float deltaTime, bool applyForce)
private const float DamageCooldown = 0.1f;
private float damageCooldownTimer;
private void ApplyDamage(float deltaTime)
{
int limbCount = character.AnimController.Limbs.Count(l => !l.IgnoreCollisions && !l.IsSevered && !l.Hidden);
if (damageCooldownTimer > 0)
{
damageCooldownTimer -= deltaTime;
return;
}
damageCooldownTimer = DamageCooldown;
int limbCount = character.AnimController.Limbs.Count(IsValidLimb);
foreach (Limb limb in character.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.Hidden) { continue; }
if (!IsValidLimb(limb)) { continue; }
float random = Rand.Value();
huskInfection.Clear();
huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / limbCount));
if (random == 0) { continue; }
const float damageRate = 2;
float dmg = random / limbCount * damageRate;
character.LastDamageSource = null;
float force = applyForce ? random * 0.5f * limb.Mass : 0;
character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, Rand.Vector(force));
var afflictions = AfflictionPrefab.InternalDamage.Instantiate(dmg).ToEnumerable();
const float forceMultiplier = 5;
float force = dmg * limb.Mass * forceMultiplier;
character.DamageLimb(limb.WorldPosition, limb, afflictions, stun: 0, playSound: false, Rand.Vector(force), ignoreDamageOverlay: true, recalculateVitality: false);
}
character.CharacterHealth.RecalculateVitality();
static bool IsValidLimb(Limb limb) => !limb.IgnoreCollisions && !limb.IsSevered && !limb.Hidden;
}
public void ActivateHusk()
{
if (huskAppendage == null && character.Params.UseHuskAppendage)
{
huskAppendage = AttachHuskAppendage(character, Prefab as AfflictionPrefabHusk);
var huskAffliction = Prefab as AfflictionPrefabHusk;
huskAppendage = AttachHuskAppendage(character, huskAffliction, GetHuskedSpeciesName(character.Params, huskAffliction));
}
if (Prefab is AfflictionPrefabHusk { NeedsAir: false })
@@ -287,7 +299,7 @@ namespace Barotrauma
Entity.Spawner.AddEntityToRemoveQueue(character);
UnsubscribeFromDeathEvent();
Identifier huskedSpeciesName = GetHuskedSpeciesName(character.SpeciesName, Prefab as AfflictionPrefabHusk);
Identifier huskedSpeciesName = GetHuskedSpeciesName(character.Params, Prefab as AfflictionPrefabHusk);
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName);
if (prefab == null)
@@ -314,6 +326,7 @@ namespace Barotrauma
husk.Info.Character = husk;
husk.Info.TeamID = CharacterTeamType.None;
}
husk.AllowPlayDead = character.AllowPlayDead;
if (Prefab is AfflictionPrefabHusk huskPrefab)
{
@@ -379,11 +392,9 @@ namespace Barotrauma
yield return CoroutineStatus.Success;
}
public static List<Limb> AttachHuskAppendage(Character character, AfflictionPrefabHusk matchingAffliction, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null)
public static List<Limb> AttachHuskAppendage(Character character, AfflictionPrefabHusk matchingAffliction, Identifier huskedSpeciesName, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null)
{
var appendage = new List<Limb>();
Identifier nonhuskedSpeciesName = GetNonHuskedSpeciesName(character.SpeciesName, matchingAffliction);
Identifier huskedSpeciesName = GetHuskedSpeciesName(nonhuskedSpeciesName, matchingAffliction);
CharacterPrefab huskPrefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName);
if (huskPrefab?.ConfigElement == null)
{
@@ -406,10 +417,7 @@ namespace Barotrauma
ContentPath pathToAppendage = element.GetAttributeContentPath("path") ?? ContentPath.Empty;
XDocument doc = XMLExtensions.TryLoadXml(pathToAppendage);
if (doc == null) { return appendage; }
if (ragdoll == null)
{
ragdoll = character.AnimController;
}
ragdoll ??= character.AnimController;
if (ragdoll.Dir < 1.0f)
{
ragdoll.Flip();
@@ -463,19 +471,31 @@ namespace Barotrauma
ragdoll.AddLimb(huskAppendage);
ragdoll.AddJoint(jointParams);
appendage.Add(huskAppendage);
}
}
}
return appendage;
}
public static Identifier GetHuskedSpeciesName(Identifier speciesName, AfflictionPrefabHusk prefab)
public static Identifier GetHuskedSpeciesName(CharacterParams character, AfflictionPrefabHusk prefab)
{
return new Identifier(speciesName.Value + prefab.HuskedSpeciesName.Value);
Identifier huskedSpecies = character.HuskedSpecies;
if (huskedSpecies.IsEmpty)
{
// Default pattern: Crawler -> Crawlerhusk, Human -> Humanhusk
return new Identifier(character.SpeciesName.Value + prefab.HuskedSpeciesName.Value);
}
return huskedSpecies;
}
public static Identifier GetNonHuskedSpeciesName(Identifier huskedSpeciesName, AfflictionPrefabHusk prefab)
public static Identifier GetNonHuskedSpeciesName(CharacterParams character, AfflictionPrefabHusk prefab)
{
return huskedSpeciesName.Remove(prefab.HuskedSpeciesName);
Identifier nonHuskedSpecies = character.NonHuskedSpecies;
if (nonHuskedSpecies.IsEmpty)
{
// Default pattern: Crawlerhusk -> Crawler, Humanhusk -> Human
return character.SpeciesName.Remove(prefab.HuskedSpeciesName);
}
return nonHuskedSpecies;
}
}
}
@@ -329,6 +329,11 @@ namespace Barotrauma
/// </summary>
public readonly ImmutableArray<Identifier> ResistanceFor;
/// <summary>
/// List of limb types that the resistance applies to. If empty, the resistance applies to the whole body.
/// </summary>
public readonly ImmutableArray<LimbType> ResistanceLimbs;
[Serialize(0.0f, IsPropertySaveable.No,
description: "The amount of resistance to the afflictions specified by ResistanceFor to apply at this effect's lowest strength.")]
public float MinResistance { get; private set; }
@@ -359,6 +364,14 @@ namespace Barotrauma
description: "Color to tint the affected character's entire body with at this effect's highest strength. The alpha channel is used to determine how much to tint the character.")]
public Color MaxBodyTint { get; private set; }
[Serialize(0.0f, IsPropertySaveable.No,
description: "Range of the \"thermal goggles overlay\" enabled by the affliction.")]
public float ThermalOverlayRange { get; private set; }
[Serialize("255,0,0,255", IsPropertySaveable.No,
description: $"Color of the \"thermal goggles overlay\" enabled by the affliction. Only has an effect if {nameof(ThermalOverlayRange)} is larger than 0.")]
public Color ThermalOverlayColor { get; private set; }
/// <summary>
/// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength.
/// </summary>
@@ -423,6 +436,8 @@ namespace Barotrauma
SerializableProperty.DeserializeProperties(this, element);
ResistanceFor = element.GetAttributeIdentifierArray("resistancefor", Array.Empty<Identifier>())!.ToImmutableArray();
ResistanceLimbs = element.GetAttributeEnumArray<LimbType>("resistancelimbs", Array.Empty<LimbType>()).ToImmutableArray();
BlockTransformation = element.GetAttributeIdentifierArray("blocktransformation", Array.Empty<Identifier>())!.ToImmutableArray();
var afflictionStatValues = new Dictionary<StatTypes, AppliedStatValue>();
@@ -594,7 +609,7 @@ namespace Barotrauma
}
else
{
MinInterval = Math.Max(element.GetAttributeFloat(nameof(MinInterval), 1.0f), 1.0f);
MinInterval = Math.Max(element.GetAttributeFloat(nameof(MinInterval), 1.0f), 0.1f);
MaxInterval = Math.Max(element.GetAttributeFloat(nameof(MaxInterval), 1.0f), MinInterval);
MinStrength = Math.Max(element.GetAttributeFloat(nameof(MinStrength), 0f), 0f);
MaxStrength = Math.Max(element.GetAttributeFloat(nameof(MaxStrength), MinStrength), MinStrength);
@@ -612,6 +627,7 @@ 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 DisguisedAsHuskType = "disguiseashusk".ToIdentifier();
public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"];
public static AfflictionPrefab BiteWounds => Prefabs["bitewounds"];
@@ -624,7 +640,7 @@ namespace Barotrauma
public static AfflictionPrefab OrganDamage => Prefabs["organdamage"];
public static AfflictionPrefab Stun => Prefabs[StunType];
public static AfflictionPrefab RadiationSickness => Prefabs["radiationsickness"];
public static AfflictionPrefab HuskInfection => Prefabs["huskinfection"];
public static readonly PrefabCollection<AfflictionPrefab> Prefabs = new PrefabCollection<AfflictionPrefab>();
@@ -898,7 +914,10 @@ namespace Barotrauma
if (element.GetAttribute("nameidentifier") != null)
{
Name = TextManager.Get(element.GetAttributeString("nameidentifier", string.Empty)).Fallback(Name);
string nameIdentifier = element.GetAttributeString("nameidentifier", string.Empty);
Name = TextManager.Get(nameIdentifier)
.Fallback(TextManager.Get($"AfflictionName.{nameIdentifier}"))
.Fallback(Name);
}
LimbSpecific = element.GetAttributeBool("limbspecific", false);
@@ -248,6 +248,12 @@ namespace Barotrauma
/// Was the character in full health at the beginning of the frame?
/// </summary>
public bool WasInFullHealth { get; private set; }
/// <summary>
/// Show the blood overlay screen space effect when the character takes damage.
/// Enabled normally, but can be disabled for some special cases.
/// </summary>
public bool ShowDamageOverlay = true;
public Affliction PressureAffliction
{
@@ -442,7 +448,7 @@ namespace Barotrauma
return strength;
}
public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true, bool ignoreUnkillability = false)
public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true, bool ignoreUnkillability = false, bool recalculateVitality = true)
{
if (Character.GodMode) { return; }
if (!ignoreUnkillability)
@@ -456,12 +462,12 @@ namespace Barotrauma
//if a limb-specific affliction is applied to no specific limb, apply to all limbs
foreach (LimbHealth limbHealth in limbHealths)
{
AddLimbAffliction(limbHealth, affliction, allowStacking: allowStacking);
AddLimbAffliction(limbHealth, limb: null, affliction, allowStacking: allowStacking, recalculateVitality: recalculateVitality);
}
}
else
{
AddLimbAffliction(targetLimb, affliction, allowStacking: allowStacking);
AddLimbAffliction(targetLimb, affliction, allowStacking: allowStacking, recalculateVitality: recalculateVitality);
}
}
else
@@ -470,14 +476,17 @@ namespace Barotrauma
}
}
public float GetResistance(AfflictionPrefab afflictionPrefab)
/// <summary>
/// How much resistance all the afflictions the character has give to the specified affliction?
/// </summary>
public float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType)
{
// This is a % resistance (0 to 1.0)
float resistance = 0.0f;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
var affliction = kvp.Key;
resistance += affliction.GetResistance(afflictionPrefab.Identifier);
resistance += affliction.GetResistance(afflictionPrefab.Identifier, limbType);
}
// This is a multiplier, ie. 0.0 = 100% resistance and 1.0 = 0% resistance
float abilityResistanceMultiplier = Character.GetAbilityResistance(afflictionPrefab);
@@ -610,7 +619,11 @@ namespace Barotrauma
CalculateVitality();
}
public void ApplyDamage(Limb hitLimb, AttackResult attackResult, bool allowStacking = true)
/// <summary>
///
/// </summary>
/// <param name="recalculateVitality">Set false only as an optimization when you manually call <see cref="RecalculateVitality"/>. Only applies to limb specific afflictions.</param>
public void ApplyDamage(Limb hitLimb, AttackResult attackResult, bool allowStacking = true, bool recalculateVitality = true)
{
if (Unkillable || Character.GodMode) { return; }
if (hitLimb.HealthIndex < 0 || hitLimb.HealthIndex >= limbHealths.Count)
@@ -619,18 +632,19 @@ namespace Barotrauma
"\" only has health configured for" + limbHealths.Count + " limbs but the limb " + hitLimb.type + " is targeting index " + hitLimb.HealthIndex);
return;
}
foreach (Affliction newAffliction in attackResult.Afflictions)
{
if (newAffliction.Prefab.LimbSpecific)
{
AddLimbAffliction(hitLimb, newAffliction, allowStacking);
AddLimbAffliction(hitLimb, newAffliction, allowStacking, recalculateVitality: recalculateVitality);
}
else
{
// Always recalculate vitality for non-limb specific afflictions.
AddAffliction(newAffliction, allowStacking);
}
}
}
}
private void KillIfOutOfVitality()
@@ -664,9 +678,8 @@ namespace Barotrauma
if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
if (burnDamageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); }
}
CalculateVitality();
KillIfOutOfVitality();
RecalculateVitality();
}
public float GetLimbDamage(Limb limb, Identifier afflictionType)
@@ -697,6 +710,17 @@ namespace Barotrauma
}
}
public void RemoveAfflictions(Func<Affliction, bool> predicate)
{
afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(affliction => predicate(affliction)));
foreach (var affliction in afflictionsToRemove)
{
afflictions.Remove(affliction);
}
CalculateVitality();
}
public void RemoveAllAfflictions()
{
afflictionsToRemove.Clear();
@@ -731,7 +755,11 @@ namespace Barotrauma
CalculateVitality();
}
private void AddLimbAffliction(Limb limb, Affliction newAffliction, bool allowStacking = true)
/// <summary>
///
/// </summary>
/// <param name="recalculateVitality">Set false only as an optimization when you manually call <see cref="RecalculateVitality"/></param>
private void AddLimbAffliction(Limb limb, Affliction newAffliction, bool allowStacking = true, bool recalculateVitality = true)
{
if (!newAffliction.Prefab.LimbSpecific || limb == null) { return; }
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
@@ -740,11 +768,16 @@ namespace Barotrauma
"\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex);
return;
}
AddLimbAffliction(limbHealths[limb.HealthIndex], newAffliction, allowStacking);
AddLimbAffliction(limbHealths[limb.HealthIndex], limb, newAffliction, allowStacking, recalculateVitality);
}
private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction, bool allowStacking = true)
/// <summary>
///
/// </summary>
/// <param name="recalculateVitality">Set false only as an optimization when you manually call <see cref="RecalculateVitality"/></param>
private void AddLimbAffliction(LimbHealth limbHealth, Limb limb, Affliction newAffliction, bool allowStacking = true, bool recalculateVitality = true)
{
LimbType limbType = limb?.type ?? LimbType.None;
if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; }
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
@@ -778,7 +811,7 @@ namespace Barotrauma
if (existingAffliction != null)
{
float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(existingAffliction.Prefab));
float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(existingAffliction.Prefab, limbType));
if (allowStacking)
{
// Add the existing strength
@@ -789,15 +822,17 @@ namespace Barotrauma
existingAffliction.Strength = newStrength;
existingAffliction.Duration = existingAffliction.Prefab.Duration;
if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; }
CalculateVitality();
KillIfOutOfVitality();
if (recalculateVitality)
{
RecalculateVitality();
}
return;
}
//create a new instance of the affliction to make sure we don't use the same instance for multiple characters
//or modify the affliction instance of an Attack or a StatusEffect
var copyAffliction = newAffliction.Prefab.Instantiate(
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))),
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab, limbType))),
newAffliction.Source);
afflictions.Add(copyAffliction, limbHealth);
AchievementManager.OnAfflictionReceived(copyAffliction, Character);
@@ -805,8 +840,10 @@ namespace Barotrauma
Character.HealthUpdateInterval = 0.0f;
CalculateVitality();
KillIfOutOfVitality();
if (recalculateVitality)
{
RecalculateVitality();
}
#if CLIENT
if (OpenHealthWindow != this && limbHealth != null)
{
@@ -817,7 +854,7 @@ namespace Barotrauma
private void AddAffliction(Affliction newAffliction, bool allowStacking = true)
{
AddLimbAffliction(limbHealth: null, newAffliction, allowStacking);
AddLimbAffliction(limbHealth: null, limb: null, newAffliction, allowStacking);
}
partial void UpdateSkinTint();
@@ -902,14 +939,15 @@ namespace Barotrauma
if (!Character.GodMode)
{
#if CLIENT
if (Character.IsVisible)
updateVisualsTimer -= deltaTime;
if (Character.IsVisible && updateVisualsTimer <= 0.0f)
{
UpdateLimbAfflictionOverlays();
UpdateSkinTint();
updateVisualsTimer = UpdateVisualsInterval;
}
#endif
CalculateVitality();
KillIfOutOfVitality();
RecalculateVitality();
}
}
@@ -941,7 +979,7 @@ namespace Barotrauma
/// <summary>
/// 0-1.
/// </summary>
public float OxygenLowResistance => !Character.NeedsOxygen ? 1 : GetResistance(oxygenLowAffliction.Prefab);
public float OxygenLowResistance => !Character.NeedsOxygen ? 1 : GetResistance(oxygenLowAffliction.Prefab, LimbType.None);
private void UpdateOxygen(float deltaTime)
{
@@ -951,7 +989,7 @@ namespace Barotrauma
return;
}
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab);
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab, LimbType.None);
float prevOxygen = OxygenAmount;
if (IsUnconscious)
{
@@ -991,13 +1029,13 @@ namespace Barotrauma
CalculateVitality();
}
public void CalculateVitality()
private void CalculateVitality()
{
vitality = MaxVitality;
IsParalyzed = false;
if (Unkillable || Character.GodMode) { return; }
foreach (var (affliction, limbHealth) in afflictions)
foreach ((Affliction affliction, LimbHealth limbHealth) in afflictions)
{
float vitalityDecrease = affliction.GetVitalityDecrease(this);
if (limbHealth != null)
@@ -1020,6 +1058,12 @@ namespace Barotrauma
}
#endif
}
public void RecalculateVitality()
{
CalculateVitality();
KillIfOutOfVitality();
}
private static float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth)
{
@@ -53,7 +53,7 @@ namespace Barotrauma
private set
{
rawAfflictionIdentifierString = value;
ParseAfflictionIdentifiers();
parsedAfflictionIdentifiers = rawAfflictionIdentifierString.ToIdentifiers().ToImmutableArray();
}
}
@@ -67,7 +67,7 @@ namespace Barotrauma
private set
{
rawAfflictionTypeString = value;
ParseAfflictionTypes();
parsedAfflictionTypes = rawAfflictionTypeString.ToIdentifiers().ToImmutableArray();
}
}
@@ -119,30 +119,6 @@ namespace Barotrauma
}
}
private void ParseAfflictionTypes()
{
if (string.IsNullOrWhiteSpace(rawAfflictionTypeString))
{
parsedAfflictionTypes = Enumerable.Empty<Identifier>().ToImmutableArray();
return;
}
parsedAfflictionTypes = rawAfflictionTypeString.Split(',', '')
.Select(s => s.Trim()).ToIdentifiers().ToImmutableArray();
}
private void ParseAfflictionIdentifiers()
{
if (string.IsNullOrWhiteSpace(rawAfflictionIdentifierString))
{
parsedAfflictionIdentifiers = Enumerable.Empty<Identifier>().ToImmutableArray();
return;
}
parsedAfflictionIdentifiers = rawAfflictionIdentifierString.Split(',', '')
.Select(s => s.Trim()).ToIdentifiers().ToImmutableArray();
}
public bool MatchesAfflictionIdentifier(string identifier) =>
MatchesAfflictionIdentifier(identifier.ToIdentifier());
@@ -94,6 +94,9 @@ namespace Barotrauma
}
}
[Serialize(false, IsPropertySaveable.No, description: "If enabled, the NPC will not spawn if the specified spawn point tags can't be found.")]
public bool RequireSpawnPointTag { get; protected set; }
[Serialize(CampaignMode.InteractionType.None, IsPropertySaveable.No)]
public CampaignMode.InteractionType CampaignInteractionType { get; protected set; }
@@ -21,9 +21,9 @@ namespace Barotrauma
public Skill PrimarySkill { get; private set; }
public Job(JobPrefab jobPrefab) : this(jobPrefab, randSync: Rand.RandSync.Unsynced, variant: 0) { }
public Job(JobPrefab jobPrefab, bool isPvP) : this(jobPrefab, isPvP, randSync: Rand.RandSync.Unsynced, variant: 0) { }
public Job(JobPrefab jobPrefab, Rand.RandSync randSync, int variant, params Skill[] s)
public Job(JobPrefab jobPrefab, bool isPvP, Rand.RandSync randSync, int variant, params Skill[] s)
{
prefab = jobPrefab;
Variant = variant;
@@ -40,7 +40,7 @@ namespace Barotrauma
}
else
{
skill = new Skill(skillPrefab, randSync);
skill = new Skill(skillPrefab, isPvP, randSync);
skills.Add(skillPrefab.Identifier, skill);
}
if (skillPrefab.IsPrimarySkill) { PrimarySkill = skill; }
@@ -74,11 +74,11 @@ namespace Barotrauma
}
}
public static Job Random(Rand.RandSync randSync)
public static Job Random(bool isPvP, Rand.RandSync randSync)
{
var prefab = JobPrefab.Random(randSync);
var variant = Rand.Range(0, prefab.Variants, randSync);
return new Job(prefab, randSync, variant);
int variant = Rand.Range(0, prefab.Variants, randSync);
return new Job(prefab, isPvP, randSync, variant);
}
public IEnumerable<Skill> GetSkills()
@@ -128,13 +128,18 @@ namespace Barotrauma
}
}
public void GiveJobItems(Character character, WayPoint spawnPoint = null)
public void GiveJobItems(Character character, bool isPvPMode, WayPoint spawnPoint = null)
{
if (!prefab.ItemSets.TryGetValue(Variant, out var spawnItems)) { return; }
if (!prefab.JobItems.TryGetValue(Variant, out var spawnItems)) { return; }
foreach (XElement itemElement in spawnItems.GetChildElements("Item"))
foreach (JobPrefab.JobItem jobItem in spawnItems)
{
InitializeJobItem(character, itemElement, spawnPoint);
//spawn the "root items" here, InitializeJobItem goes through the children recursively
if (jobItem.ParentItem != null) { continue; }
for (int i = 0; i < jobItem.Amount; i++)
{
InitializeJobItem(character, isPvPMode, jobItem, spawnItems, spawnPoint);
}
}
if (GameMain.GameSession is { TraitorsEnabled: true } && character.IsSecurity)
@@ -144,29 +149,14 @@ namespace Barotrauma
}
}
private void InitializeJobItem(Character character, XElement itemElement, WayPoint spawnPoint = null, Item parentItem = null)
private void InitializeJobItem(Character character, bool isPvPMode, JobPrefab.JobItem jobItem, IEnumerable<JobPrefab.JobItem> allJobItems, WayPoint spawnPoint = null, Item parentItem = null)
{
ItemPrefab itemPrefab;
if (itemElement.Attribute("name") != null)
Identifier itemIdentifier = jobItem.GetItemIdentifier(character.TeamID, isPvPMode);
if (itemIdentifier.IsEmpty) { return; }
if ((MapEntityPrefab.FindByIdentifier(itemIdentifier) ?? MapEntityPrefab.FindByName(itemIdentifier.Value)) is not ItemPrefab itemPrefab)
{
string itemName = itemElement.Attribute("name").Value;
DebugConsole.ThrowErrorLocalized("Error in Job config (" + Name + ") - use item identifiers instead of names to configure the items.");
itemPrefab = MapEntityPrefab.FindByName(itemName) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowErrorLocalized("Tried to spawn \"" + Name + "\" with the item \"" + itemName + "\". Matching item prefab not found.");
return;
}
}
else
{
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowErrorLocalized("Tried to spawn \"" + Name + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.");
return;
}
DebugConsole.ThrowErrorLocalized($"Tried to spawn \"{Name}\" with the item \"{itemIdentifier}\". Matching item prefab not found.");
return;
}
Item item = new Item(itemPrefab, character.Position, null);
@@ -187,7 +177,7 @@ namespace Barotrauma
}
#endif
if (itemElement.GetAttributeBool("equip", false))
if (jobItem.Equip)
{
//if the item is both pickable and wearable, try to wear it instead of picking it up
List<InvSlotType> allowedSlots =
@@ -229,12 +219,18 @@ namespace Barotrauma
wifiComponent.TeamID = character.TeamID;
}
if (parentItem != null) { parentItem.Combine(item, user: null); }
parentItem?.Combine(item, user: null);
foreach (XElement childItemElement in itemElement.Elements())
foreach (JobPrefab.JobItem childItem in allJobItems)
{
InitializeJobItem(character, childItemElement, spawnPoint, item);
}
if (childItem.ParentItem == jobItem)
{
for (int i = 0; i < childItem.Amount; i++)
{
InitializeJobItem(character, isPvPMode, childItem, allJobItems, spawnPoint, parentItem: item);
}
}
}
}
public XElement Save(XElement parentElement)
@@ -95,20 +95,59 @@ namespace Barotrauma
}
}
public class PreviewItem
public class JobItem
{
public readonly Identifier ItemIdentifier;
public readonly bool ShowPreview;
public PreviewItem(Identifier itemIdentifier, bool showPreview)
public enum GameModeType
{
ItemIdentifier = itemIdentifier;
ShowPreview = showPreview;
Any, PvP, PvE
}
public readonly Identifier ItemIdentifier;
public readonly Identifier ItemIdentifierTeam2;
public readonly bool ShowPreview;
public readonly bool Equip;
public readonly bool Outfit;
public readonly int Amount = 1;
public readonly JobItem ParentItem;
public readonly GameModeType GameMode;
public JobItem(ContentXElement element, JobItem parentItem)
{
ItemIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
ItemIdentifierTeam2 = element.GetAttributeIdentifier("identifierteam2", Identifier.Empty);
ShowPreview = element.GetAttributeBool("showpreview", true);
GameMode = element.GetAttributeEnum("gamemode", parentItem?.GameMode ?? GameModeType.Any);
Amount = element.GetAttributeInt("amount", 1);
Equip = element.GetAttributeBool("equip", false);
Outfit = element.GetAttributeBool("outfit", false);
ParentItem = parentItem;
}
public Identifier GetItemIdentifier(CharacterTeamType team, bool isPvPMode)
{
switch (GameMode)
{
case GameModeType.PvP:
if (!isPvPMode) { return Identifier.Empty; }
break;
case GameModeType.PvE:
if (isPvPMode) { return Identifier.Empty; }
break;
}
return
team == CharacterTeamType.Team2 && !ItemIdentifierTeam2.IsEmpty ?
ItemIdentifierTeam2 :
ItemIdentifier;
}
}
public readonly Dictionary<int, ContentXElement> ItemSets = new Dictionary<int, ContentXElement>();
public readonly ImmutableDictionary<int, ImmutableArray<PreviewItem>> PreviewItems;
/// <summary>
/// The items the character can get when spawning. The key is the index of the job variant.
/// </summary>
public readonly ImmutableDictionary<int, ImmutableArray<JobItem>> JobItems;
public readonly List<SkillPrefab> Skills = new List<SkillPrefab>();
public readonly List<AutonomousObjective> AutonomousObjectives = new List<AutonomousObjective>();
public readonly List<Identifier> AppropriateOrders = new List<Identifier>();
@@ -211,7 +250,7 @@ namespace Barotrauma
Description = TextManager.Get("JobDescription." + Identifier);
Element = element;
var previewItems = new Dictionary<int, List<PreviewItem>>();
var jobItems = new Dictionary<int, List<JobItem>>();
int variant = 0;
foreach (var subElement in element.Elements())
@@ -219,9 +258,8 @@ namespace Barotrauma
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "itemset":
ItemSets.Add(variant, subElement);
previewItems[variant] = new List<PreviewItem>();
loadItemIdentifiers(subElement, variant);
jobItems[variant] = new List<JobItem>();
loadJobItems(subElement, variant, parentItem: null);
variant++;
break;
case "skills":
@@ -246,35 +284,39 @@ namespace Barotrauma
}
}
void loadItemIdentifiers(XElement parentElement, int variant)
void loadJobItems(ContentXElement parentElement, int variant, JobItem parentItem)
{
foreach (XElement itemElement in parentElement.GetChildElements("Item"))
foreach (ContentXElement itemElement in parentElement.GetChildElements("Item"))
{
if (itemElement.Element("name") != null)
if (itemElement.GetAttribute("name") != null)
{
DebugConsole.ThrowErrorLocalized("Error in job config \"" + Name + "\" - use identifiers instead of names to configure the items.");
DebugConsole.ThrowErrorLocalized("Error in job config \"" + Name + "\" - use identifiers instead of names to configure the items.",
contentPackage: parentElement.ContentPackage);
continue;
}
Identifier itemIdentifier = itemElement.GetAttributeIdentifier("identifier", Identifier.Empty);
JobItem jobItem = null;
if (itemIdentifier.IsEmpty)
{
DebugConsole.ThrowErrorLocalized("Error in job config \"" + Name + "\" - item with no identifier.");
DebugConsole.ThrowErrorLocalized("Error in job config \"" + Name + "\" - item with no identifier.",
contentPackage: parentElement.ContentPackage);
}
else
{
previewItems[variant].Add(new PreviewItem(itemIdentifier, itemElement.GetAttributeBool("showpreview", true)));
jobItem = new JobItem(itemElement, parentItem);
jobItems[variant].Add(jobItem);
}
loadItemIdentifiers(itemElement, variant);
loadJobItems(itemElement, variant, parentItem: jobItem);
}
}
PreviewItems = previewItems.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray()))
JobItems = jobItems.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray()))
.ToImmutableDictionary();
Variants = variant;
Skills.Sort((x,y) => y.LevelRange.Start.CompareTo(x.LevelRange.Start));
Skills.Sort((x,y) => y.GetLevelRange(isPvP: false).Start.CompareTo(x.GetLevelRange(isPvP: false).Start));
}
public static JobPrefab Random(Rand.RandSync sync, Func<JobPrefab, bool> predicate = null) => Prefabs.GetRandom(p => !p.HiddenJob && (predicate == null || predicate(p)), sync);
@@ -40,10 +40,12 @@ namespace Barotrauma
public readonly float PriceMultiplier = 1.0f;
public Skill(SkillPrefab prefab, Rand.RandSync randSync)
public Skill(SkillPrefab prefab, bool isPvP, Rand.RandSync randSync)
{
Identifier = prefab.Identifier;
Level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync);
var levelRange = prefab.GetLevelRange(isPvP);
Level = Rand.Range(levelRange.Start, levelRange.End, randSync);
iconJobId = GetIconJobId();
PriceMultiplier = prefab.PriceMultiplier;
DisplayName = TextManager.Get("SkillName." + Identifier);
@@ -1,5 +1,4 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
using System.Globalization;
namespace Barotrauma
{
@@ -7,7 +6,8 @@ namespace Barotrauma
{
public readonly Identifier Identifier;
public Range<float> LevelRange { get; private set; }
private readonly Range<float> levelRange;
private readonly Range<float> levelRangePvP;
/// <summary>
/// How much this skill affects characters' hiring cost
@@ -20,19 +20,32 @@ namespace Barotrauma
{
Identifier = element.GetAttributeIdentifier("identifier", "");
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 25.0f);
var levelString = element.GetAttributeString("level", "");
if (levelString.Contains(","))
{
var rangeVector2 = XMLExtensions.ParseVector2(levelString, false);
LevelRange = new Range<float>(rangeVector2.X, rangeVector2.Y);
}
else
{
float skillLevel = float.Parse(levelString, System.Globalization.CultureInfo.InvariantCulture);
LevelRange = new Range<float>(skillLevel, skillLevel);
}
levelRange = GetSkillRange("level", element, defaultValue: new Range<float>(0, 0));
levelRangePvP = GetSkillRange("pvplevel", element, defaultValue: levelRange);
IsPrimarySkill = element.GetAttributeBool("primary", false);
static Range<float> GetSkillRange(string attributeName, ContentXElement element, Range<float> defaultValue)
{
string levelString = element.GetAttributeString(attributeName, string.Empty);
if (levelString.Contains(','))
{
var rangeVector2 = XMLExtensions.ParseVector2(levelString, false);
return new Range<float>(rangeVector2.X, rangeVector2.Y);
}
else if (float.TryParse(levelString, NumberStyles.Any, CultureInfo.InvariantCulture, out float skillLevel))
{
return new Range<float>(skillLevel, skillLevel);
}
else
{
return defaultValue;
}
}
}
public Range<float> GetLevelRange(bool isPvP)
{
return isPvP ? levelRangePvP : levelRange;
}
}
}
@@ -258,10 +258,7 @@ namespace Barotrauma
{
get
{
if (!mouthPos.HasValue)
{
mouthPos = Params.MouthPos;
}
mouthPos ??= Params.MouthPos;
return mouthPos.Value;
}
set
@@ -366,7 +363,7 @@ namespace Barotrauma
if (isSevered)
{
ragdoll.SubtractMass(this);
if (type == LimbType.Head)
if (type == LimbType.Head && character.Params.Health.DieFromBeheading)
{
character.Kill(CauseOfDeathType.Unknown, null);
}
@@ -386,10 +383,18 @@ namespace Barotrauma
public Submarine Submarine => character?.Submarine;
private bool _hidden;
public bool Hidden
{
get => Params.Hide;
set => Params.Hide = value;
get => _hidden || Params.Hide;
set => _hidden = value;
}
// Just a wrapper for Hidden, but both can be used via status effects, so it's not safe to remove it.
public bool Hide
{
get => Hidden;
set => Hidden = value;
}
public Vector2 WorldPosition
@@ -636,7 +641,7 @@ namespace Barotrauma
//if (character.Params.CanInteract) { return false; }
if (this == character.AnimController.MainLimb) { return false; }
bool canBeSevered = Params.CanBeSeveredAlive;
if (character.AnimController.CanWalk)
if (character.AnimController.CanWalk && !character.Params.Health.AllowSeveringLegs)
{
switch (type)
{
@@ -671,7 +676,7 @@ namespace Barotrauma
this.character = character;
this.Params = limbParams;
dir = Direction.Right;
body = new PhysicsBody(limbParams);
body = new PhysicsBody(limbParams, findNewContacts: false);
type = limbParams.Type;
IgnoreCollisions = limbParams.IgnoreCollisions;
body.UserData = this;
@@ -937,7 +942,7 @@ namespace Barotrauma
severedFadeOutTimer = SeveredFadeOutTime;
}
}
else if (!IsDead)
else if (!IsDead && (character.IsPlayer || character.AIState is not AIState.PlayDead))
{
if (Params.BlinkFrequency > 0)
{
@@ -989,6 +994,7 @@ namespace Barotrauma
public void ReEnable()
{
if (!temporarilyDisabled) { return; }
temporarilyDisabled = false;
Hidden = false;
Disabled = false;
IgnoreCollisions = originalIgnoreCollisions;
@@ -1008,6 +1014,8 @@ namespace Barotrauma
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos));
bool wasRunning = attack.IsRunning;
attack.UpdateAttackTimer(deltaTime, character);
attack.DamageMultiplier = 1.0f + character.GetStatValue(attack.Ranged ? StatTypes.NaturalRangedAttackMultiplier : StatTypes.NaturalMeleeAttackMultiplier);
if (attack.Blink)
{
if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Any())
@@ -1434,6 +1442,7 @@ namespace Barotrauma
public void Remove()
{
ragdoll.SubtractMass(this);
body?.Remove();
body = null;
if (pullJoint != null)
@@ -44,9 +44,6 @@ namespace Barotrauma
[Serialize(0f, IsPropertySaveable.Yes, description: "How much the body raises when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 0.1f)]
public float StepLiftAmount { get; set; }
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool MultiplyByDir { get; set; }
[Serialize(0.5f, IsPropertySaveable.Yes, description: "When does the body raise when taking a step. The default (0.5) is in the middle of the step."), Editable(MinValueFloat = -1, MaxValueFloat = 1, DecimalCount = 2, ValueStep = 0.1f)]
public float StepLiftOffset { get; set; }
@@ -56,6 +53,48 @@ namespace Barotrauma
[Header("Movement")]
[Serialize(0.75f, IsPropertySaveable.Yes, description: "The character's movement speed is multiplied with this value when moving backwards."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.99f, DecimalCount = 2)]
public float BackwardsMovementMultiplier { get; set; }
[Serialize(1.0f, IsPropertySaveable.Yes, description: "Adjusts the maximum speed while climbing. The actual speed is affected by the MovementSpeed."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10f, DecimalCount = 2)]
public float ClimbSpeed { get; set; }
[Serialize(2.0f, IsPropertySaveable.Yes, description: "Used instead of ClimbSpeed when descending ladders while moving fast (running). Not used if lower than ClimbSpeed."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10f, DecimalCount = 2)]
public float SlideSpeed { get; set; }
[Serialize(10.5f, IsPropertySaveable.Yes, description: "Force applied to the main collider, torso and head, when climbing ladders."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)]
public float ClimbBodyMoveForce { get; set; }
[Serialize(5.2f, IsPropertySaveable.Yes, description: "Force applied to the hands when climbing ladders."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)]
public float ClimbHandMoveForce { get; set; }
[Serialize(10.0f, IsPropertySaveable.Yes, description: "Force applied to the feet when climbing ladders."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)]
public float ClimbFootMoveForce { get; set; }
[Serialize(30.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)]
public float ClimbStepHeight { get; set; }
protected override bool Deserialize(XElement element = null)
{
if (element.GetAttributeEnum(nameof(AnimationType), AnimationType.NotDefined) is AnimationType.Run)
{
// These values were previously hard-coded when running, so we need to set different default values for the run animations, when they are not defined.
const string climbSpeedName = nameof(ClimbSpeed);
if (element.GetAttribute(climbSpeedName) == null)
{
element.SetAttribute(climbSpeedName, 2.0f);
}
const string climbStepName = nameof(ClimbStepHeight);
if (element.GetAttribute(climbStepName) == null)
{
element.SetAttribute(climbStepName, 60.0f);
}
const string slideSpeedName = nameof(SlideSpeed);
if (element.GetAttribute(slideSpeedName) == null)
{
element.SetAttribute(slideSpeedName, 4.0f);
}
}
return base.Deserialize(element);
}
}
abstract class SwimParams : AnimationParams
@@ -92,7 +131,7 @@ namespace Barotrauma
/// <summary>
/// In degrees.
/// </summary>
[Header("Standing")]
[Header("Orientation")]
[Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)]
public float HeadAngle
{
@@ -19,6 +19,16 @@ namespace Barotrauma
{
[Serialize("", IsPropertySaveable.Yes), Editable]
public Identifier SpeciesName { get; private set; }
[Serialize("", IsPropertySaveable.Yes), Editable]
public string Tags
{
get => tags.ConvertToString();
set => tags = value.ToIdentifiers().ToHashSet();
}
private HashSet<Identifier> tags = new HashSet<Identifier>();
public bool HasTag(Identifier tag) => tags.Contains(tag);
[Serialize("", IsPropertySaveable.Yes, description: "References to another species. Define only if the creature is a variant that needs to use a pre-existing translation."), Editable]
public Identifier SpeciesTranslationOverride { get; private set; }
@@ -37,9 +47,21 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "Can the creature interact with items?"), Editable]
public bool CanInteract { get; private set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Can the creature use ladders? Doesn't have an effect, if CanInteract is false."), Editable]
public bool CanClimb { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If set true, this character only uses the climbing parameters defined in the walk parameters (not run)."), Editable]
public bool ForceSlowClimbing { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should this character be treated as a husk?"), Editable]
public bool Husk { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description: "If this character can turn into a husk, which character it turns to? If not defined, uses the default pattern (e.g. Crawler -> Crawlerhusk, Human -> Humanhusk)."), Editable]
public Identifier HuskedSpecies { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description: "If this character is a husk, from what species it can be turned into? If not defined, uses the default pattern (e.g. Crawlerhusk -> Crawler, Humanhusk -> Human)."), Editable]
public Identifier NonHuskedSpecies { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description:"Should this character use a special husk appendage, attached to the ragdoll, when it turns into a husk?"), Editable]
public bool UseHuskAppendage { get; private set; }
@@ -161,7 +183,7 @@ namespace Barotrauma
}
}
public static XElement CreateVariantXml(XElement variantXML, XElement baseXML)
public static XElement CreateVariantXml(ContentXElement variantXML, ContentXElement baseXML)
{
XElement newXml = variantXML.CreateVariantXML(baseXML);
XElement variantAi = variantXML.GetChildElement("ai");
@@ -433,17 +455,11 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes, description: "Which tags are required for this sound to play?"), Editable()]
public string Tags
{
get { return string.Join(',', TagSet); }
private set
{
TagSet = value.Split(',')
.ToIdentifiers()
.Where(id => !id.IsEmpty)
.ToImmutableHashSet();
}
get => TagSet.ConvertToString();
private set => TagSet = value.ToIdentifiers().ToImmutableHashSet();
}
public ImmutableHashSet<Identifier> TagSet { get; private set; }
public ImmutableHashSet<Identifier> TagSet { get; private set; } = ImmutableHashSet<Identifier>.Empty;
public SoundParams(ContentXElement element, CharacterParams character) : base(element, character)
{
@@ -549,6 +565,15 @@ namespace Barotrauma
[Serialize(0f, IsPropertySaveable.Yes), Editable]
public float EmpVulnerability { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Apply movement penalties when legs or tail limbs get damaged. Enabled by default."), Editable]
public bool ApplyMovementPenalties { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Normally characters die when they don't have a head. But maybe not all of them?"), Editable]
public bool DieFromBeheading { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Severing legs doesn't work with most characters, because we'd need to take that into account with the walking animations and the standing position of the main collider etc. But there might be cases where you'll want to override this default."), Editable]
public bool AllowSeveringLegs { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable]
public bool ApplyAfflictionColors { get; private set; }
@@ -719,34 +744,47 @@ namespace Barotrauma
[Serialize(WallTargetingMethod.Target, IsPropertySaveable.Yes, description: "Defines the method of checking whether there's a blocking (submarine) wall."), Editable]
public WallTargetingMethod WallTargetingMethod { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes, "How likely it is that the creature plays dead (= ragdolls) while idling? Only allowed inside a sub (not in the open waters). Evaluated once, when the creature spawns."), Editable]
public float PlayDeadProbability { get; set; }
public IEnumerable<TargetParams> Targets => targets;
protected readonly List<TargetParams> targets = new List<TargetParams>();
private readonly List<TargetParams> targets = new List<TargetParams>();
public AIParams(ContentXElement element, CharacterParams character) : base(element, character)
{
if (element == null) { return; }
element.GetChildElements("target").ForEach(t => TryAddTarget(t, out _));
element.GetChildElements("targetpriority").ForEach(t => TryAddTarget(t, out _));
element.GetChildElements("target").ForEach(t => AddTarget(t));
element.GetChildElements("targetpriority").ForEach(t => AddTarget(t));
}
/// <summary>
/// Adds a target but checks for duplicates first. Doesn't allow adding multiple targets with the same tag (see <see cref="AddTarget"/>).
/// </summary>
private bool TryAddTarget(ContentXElement targetElement, out TargetParams target)
{
string tag = targetElement.GetAttributeString("tag", null);
if (HasTag(tag))
{
target = null;
DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!",
targetElement.ContentPackage);
return false;
DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!", targetElement.ContentPackage);
}
else
{
target = new TargetParams(targetElement, Character);
targets.Add(target);
SubParams.Add(target);
return true;
target = AddTarget(targetElement);
}
return target != null;
}
/// <summary>
/// This method allows adding multiple targets with the same tag.
/// </summary>
private TargetParams AddTarget(ContentXElement targetElement)
{
var target = new TargetParams(targetElement, Character);
targets.Add(target);
SubParams.Add(target);
return target;
}
public bool TryAddEmptyTarget(out TargetParams targetParams) => TryAddNewTarget("newtarget" + targets.Count, AIState.Attack, 0f, out targetParams);
@@ -782,26 +820,40 @@ namespace Barotrauma
}
public bool RemoveTarget(TargetParams target) => RemoveSubParam(target, targets);
public bool TryGetTarget(string targetTag, out TargetParams target)
=> TryGetTarget(targetTag.ToIdentifier(), out target);
public bool TryGetTarget(Identifier targetTag, out TargetParams target)
public IEnumerable<TargetParams> GetMatchingTargets(Func<TargetParams, bool> predicate) => targets.Where(predicate);
public IEnumerable<TargetParams> GetTargets(Identifier target) => GetMatchingTargets(t => t.Tag == target);
public IEnumerable<TargetParams> GetTargets(Character target) => GetMatchingTargets(t => t.Tag == target.SpeciesName || t.Tag == target.Params.Group || target.Params.HasTag(t.Tag));
public TargetParams GetHighestPriorityTarget(Identifier target) => GetHighestPriorityTarget(GetTargets(target));
public TargetParams GetHighestPriorityTarget(Character target) => GetHighestPriorityTarget(GetTargets(target));
private static TargetParams GetHighestPriorityTarget(IEnumerable<TargetParams> targetParams) => targetParams.MaxBy(static t => t.Priority);
public bool TryGetTargets(Identifier target, out IEnumerable<TargetParams> targetParams)
{
target = targets.FirstOrDefault(t => t.Tag == targetTag);
return target != null;
targetParams = GetTargets(target);
return targetParams.Any();
}
public bool TryGetTargets(Character target, out IEnumerable<TargetParams> targetParams)
{
targetParams = GetTargets(target);
return targetParams.Any();
}
public bool TryGetHighestPriorityTarget(Identifier target, out TargetParams targetParams)
{
targetParams = GetHighestPriorityTarget(target);
return targetParams != null;
}
public bool TryGetHighestPriorityTarget(Character target, out TargetParams targetParams)
{
targetParams = GetHighestPriorityTarget(target);
return targetParams != null;
}
public bool TryGetTarget(Character targetCharacter, out TargetParams target)
{
if (!TryGetTarget(targetCharacter.SpeciesName, out target))
{
target = targets.FirstOrDefault(t => t.Tag == targetCharacter.Params.Group);
}
return target != null;
}
public bool TryGetTarget(IEnumerable<Identifier> tags, out TargetParams target)
public bool TryGetHighestPriorityTarget(IEnumerable<Identifier> tags, out TargetParams target)
{
target = null;
if (tags == null || tags.None()) { return false; }
@@ -819,22 +871,6 @@ namespace Barotrauma
}
return target != null;
}
public TargetParams GetTarget(string targetTag, bool throwError = true)
=> GetTarget(targetTag.ToIdentifier(), throwError);
public TargetParams GetTarget(Identifier targetTag, bool throwError = true)
{
if (targetTag.IsEmpty) { return null; }
if (!TryGetTarget(targetTag, out TargetParams target))
{
if (throwError)
{
DebugConsole.ThrowError($"Cannot find a target with the tag {targetTag}!");
}
}
return target;
}
}
public class TargetParams : SubParam
@@ -889,7 +925,7 @@ namespace Barotrauma
[Serialize(-1f, IsPropertySaveable.Yes, description: "A generic max threshold. Not used if set to negative."), Editable]
public float ThresholdMax { get; private set; }
[Serialize(1.0f, IsPropertySaveable.Yes, description: "Can be used to make the monster perceive the target further than it normally can."), Editable]
[Serialize(1.0f, IsPropertySaveable.Yes, description: "Can be used to make the monster perceive the target further or closer than it normally can."), Editable]
public float PerceptionDistanceMultiplier { get; private set; }
[Serialize(-1.0f, IsPropertySaveable.Yes, description: "Maximum distance at which the monster can perceive the target, regardless of the sight/hearing or how visible or how much noise the target is making. Not used if set to negative."), Editable]
@@ -113,7 +113,7 @@ namespace Barotrauma
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool CanWalk { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Can the character be dragged around by other creatures?"), Editable()]
public bool Draggable { get; set; }
@@ -654,7 +654,7 @@ namespace Barotrauma
[Serialize(0.25f, IsPropertySaveable.Yes), Editable]
public float Stiffness { get; set; }
[Serialize(1f, IsPropertySaveable.Yes, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable]
[Serialize(1f, IsPropertySaveable.Yes, description: "CAUTION: Not fully implemented. Only use for limb joints that connect non-animated limbs!"), Editable(DecimalCount = 2)]
public float Scale { get; set; }
[Serialize(false, IsPropertySaveable.No), Editable(ReadOnly = true)]
@@ -705,6 +705,9 @@ namespace Barotrauma
[Serialize(LimbType.None, IsPropertySaveable.Yes, description: "The limb type affects many things, like the animations. Torso or Head are considered as the main limbs. Every character should have at least one Torso or Head."), Editable()]
public LimbType Type { get; set; }
[Serialize(LimbType.None, IsPropertySaveable.Yes, description: "Secondary limb type to be used for generic purposes. Currently only used in climbing animations."), Editable()]
public LimbType SecondaryType { get; set; }
/// <summary>
/// The orientation of the sprite as drawn on the sprite sheet (in radians).
@@ -775,6 +778,12 @@ namespace Barotrauma
[Serialize("0, 0", IsPropertySaveable.Yes, description: "Relative offset for the mouth position (starting from the center). Only applicable for LimbType.Head. Used for eating."), Editable(DecimalCount = 2, MinValueFloat = -10f, MaxValueFloat = 10f)]
public Vector2 MouthPos { get; set; }
[Serialize(50f, IsPropertySaveable.Yes, description: "How much torque is applied on the head while updating the eating animations?"), Editable]
public float EatTorque { get; set; }
[Serialize(2f, IsPropertySaveable.Yes, description: "How strong a linear impulse is applied on the head while updating the eating animations?"), Editable]
public float EatImpulse { get; set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable]
public float ConstantTorque { get; set; }
@@ -795,8 +804,11 @@ namespace Barotrauma
[Serialize(10f, IsPropertySaveable.Yes, "How long it takes for the severed limb to fade out"), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 1)]
public float SeveredFadeOutTime { get; set; } = 10.0f;
[Serialize(false, IsPropertySaveable.Yes, description: "Only applied when the limb is of type Tail. If none of the tails have been defined to use the angle and an angle is defined in the animation parameters, the first tail limb is used."), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Should the tail angle be applied on this limb? If none of the limbs have been defined to use the angle and an angle is defined in the animation parameters, the first tail limb is used."), Editable]
public bool ApplyTailAngle { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should this limb be moved like a tail when swimming? Always true for tail limbs. On tails, disable by setting SineFrequencyMultiplier to 0."), Editable]
public bool ApplySineMovement { get; set; }
[Serialize(1f, IsPropertySaveable.Yes), Editable(ValueStep = 0.1f, DecimalCount = 2)]
public float SineFrequencyMultiplier { get; set; }
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
@@ -7,31 +6,13 @@ namespace Barotrauma.Abilities
{
class AbilityConditionMission : AbilityConditionData
{
private readonly ImmutableHashSet<MissionType> missionType;
private readonly ImmutableHashSet<Identifier> missionType;
private readonly bool isAffiliated;
public AbilityConditionMission(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
string[] missionTypeStrings = conditionElement.GetAttributeStringArray("missiontype", new []{ "None" })!;
HashSet<MissionType> missionTypes = new HashSet<MissionType>();
missionType = conditionElement.GetAttributeIdentifierImmutableHashSet("missiontype", ImmutableHashSet<Identifier>.Empty)!;
isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false);
foreach (string missionTypeString in missionTypeStrings)
{
if (!Enum.TryParse(missionTypeString, out MissionType parsedMission) || parsedMission is MissionType.None)
{
if (!isAffiliated)
{
DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.",
contentPackage: conditionElement.ContentPackage);
}
continue;
}
missionTypes.Add(parsedMission);
}
missionType = missionTypes.ToImmutableHashSet();
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
@@ -8,7 +8,7 @@ namespace Barotrauma.Abilities
protected override bool MatchesConditionSpecific()
{
foreach (Character c in GameSession.GetSessionCrewCharacters(CharacterType.Both))
foreach (Character c in Character.GetFriendlyCrew(character))
{
if (!c.IsDead && c.IsUnconscious)
{
@@ -1,18 +1,13 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class AbilityConditionHasSkill : AbilityConditionDataless
{
private readonly string skillIdentifier;
private readonly Identifier skillIdentifier;
private readonly float minValue;
public AbilityConditionHasSkill(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
skillIdentifier = conditionElement.GetAttributeString("skillidentifier", string.Empty);
skillIdentifier = conditionElement.GetAttributeIdentifier("skillidentifier", Identifier.Empty);
minValue = conditionElement.GetAttributeFloat("minvalue", 0f);
}
@@ -9,7 +9,7 @@ namespace Barotrauma.Abilities
protected override bool MatchesCharacter(Character character)
{
int ownLevel = character.Info.GetCurrentLevel();
foreach (Character otherCharacter in GameSession.GetSessionCrewCharacters(CharacterType.Both))
foreach (Character otherCharacter in Character.GetFriendlyCrew(character))
{
if (otherCharacter == character) { continue; }
if (otherCharacter.Info.GetCurrentLevel() < ownLevel) { return false; }
@@ -13,13 +13,15 @@
{
if (GameMain.GameSession == null) { return false; }
foreach (Character character in GameMain.GameSession.Casualties)
foreach (Character deadCharacter in GameMain.GameSession.Casualties)
{
if (assistantsDontCount && character.Info?.Job?.Prefab.Identifier == "assistant")
if (deadCharacter.TeamID != character.TeamID) { continue; }
if (assistantsDontCount && deadCharacter.Info?.Job?.Prefab.Identifier == "assistant")
{
continue;
}
if (character.CauseOfDeath != null && character.CauseOfDeath.Type != CauseOfDeathType.Disconnected)
if (deadCharacter.CauseOfDeath != null && deadCharacter.CauseOfDeath.Type != CauseOfDeathType.Disconnected)
{
return false;
}
@@ -14,7 +14,8 @@
float waterVolume = 0.0f, totalVolume = 0.0f;
foreach (Hull hull in Hull.HullList)
{
if (hull.Submarine != character.Submarine) { continue; }
if (hull.Submarine is not { } hullSubmarine) { continue; }
if (hullSubmarine != character.Submarine || hullSubmarine.TeamID != character.TeamID) { continue; }
waterVolume += hull.WaterVolume;
totalVolume += hull.Volume;
}
@@ -26,7 +26,7 @@ namespace Barotrauma.Abilities
return;
}
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
foreach (Character character in Character.GetFriendlyCrew(Character))
{
JobPrefab? characterJob = character.Info?.Job?.Prefab;
if (characterJob is null) { continue; }
@@ -4,14 +4,14 @@
{
private readonly Identifier afflictionId;
private readonly float strength;
private readonly string multiplyStrengthBySkill;
private readonly Identifier multiplyStrengthBySkill;
private readonly bool setValue;
public CharacterAbilityGiveAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
afflictionId = abilityElement.GetAttributeIdentifier("afflictionid", abilityElement.GetAttributeIdentifier("affliction", Identifier.Empty));
strength = abilityElement.GetAttributeFloat("strength", 0f);
multiplyStrengthBySkill = abilityElement.GetAttributeString("multiplystrengthbyskill", string.Empty);
multiplyStrengthBySkill = abilityElement.GetAttributeIdentifier("multiplystrengthbyskill", Identifier.Empty);
setValue = abilityElement.GetAttributeBool("setvalue", false);
if (afflictionId.IsEmpty)
@@ -52,7 +52,7 @@
return;
}
float strength = this.strength;
if (!string.IsNullOrEmpty(multiplyStrengthBySkill))
if (!multiplyStrengthBySkill.IsEmpty)
{
strength *= Character.GetSkillLevel(multiplyStrengthBySkill);
}
@@ -20,7 +20,7 @@ namespace Barotrauma.Abilities
{
if (!addingFirstTime) { return; }
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
foreach (Character character in Character.GetFriendlyCrew(Character))
{
if (character.Info is null) { return; }
character.Info.AdditionalTalentPoints += amount;
@@ -6,7 +6,7 @@ namespace Barotrauma.Abilities
{
private readonly StatTypes statType;
private readonly float maxValue;
private readonly string skillIdentifier;
private readonly Identifier skillIdentifier;
private readonly bool useAll;
private float lastValue = 0f;
public override bool AllowClientSimulation => true;
@@ -15,7 +15,7 @@ namespace Barotrauma.Abilities
{
statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier);
maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f);
skillIdentifier = abilityElement.GetAttributeString("skillidentifier", string.Empty);
skillIdentifier = abilityElement.GetAttributeIdentifier("skillidentifier", Identifier.Empty);
useAll = skillIdentifier == "all";
}
@@ -5,6 +5,8 @@
private readonly float addedValue;
private readonly float multiplyValue;
public override bool AllowClientSimulation => true;
public CharacterAbilityModifyValue(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f);
@@ -6,6 +6,7 @@ internal class CharacterAbilityUpgradeSubmarine : CharacterAbility
private readonly UpgradePrefab? upgradePrefab;
private readonly UpgradeCategory? upgradeCategory;
public readonly int level;
private readonly bool giveOnAddingFirstTime;
public override bool AllowClientSimulation => true;
@@ -13,7 +14,8 @@ internal class CharacterAbilityUpgradeSubmarine : CharacterAbility
{
var prefabIdentifier = abilityElement.GetAttributeIdentifier(nameof(upgradePrefab), Identifier.Empty);
var categoryIdentifier = abilityElement.GetAttributeIdentifier(nameof(upgradeCategory), Identifier.Empty);
giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None);
if (UpgradePrefab.Find(prefabIdentifier) is not { } foundUpgradePrefab)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityUpgradeSubmarine)} - {nameof(upgradePrefab)} not found.",
@@ -47,6 +49,14 @@ internal class CharacterAbilityUpgradeSubmarine : CharacterAbility
ApplyEffectSpecific();
}
public override void InitializeAbility(bool addingFirstTime)
{
if (addingFirstTime && giveOnAddingFirstTime)
{
ApplyEffectSpecific();
}
}
private void ApplyEffectSpecific()
{
if (upgradePrefab == null || upgradeCategory == null) { return; }
@@ -24,7 +24,11 @@ namespace Barotrauma.Abilities
foreach (Character enemyCharacter in enemyCharacters)
{
if (!enemyCharacter.IsHuman) { continue; }
if (enemyCharacter.Submarine == null || enemyCharacter.Submarine != Submarine.MainSub) { continue; }
if (enemyCharacter.Submarine == null ||
(Submarine.MainSub != null && enemyCharacter.Submarine != Submarine.MainSub))
{
continue;
}
if (enemyCharacter.IsDead) { continue; }
if (!enemyCharacter.LockHands) { continue; }
Character.GiveMoney(moneyAmount);
@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using Barotrauma.Extensions;
@@ -31,7 +31,7 @@ namespace Barotrauma.Abilities
if (!TalentTree.JobTalentTrees.TryGet(apprentice.Identifier, out TalentTree? talentTree)) { return; }
ImmutableHashSet<Character> characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
var characters = Character.GetFriendlyCrew(Character);
HashSet<ImmutableHashSet<Identifier>> talentsTrees = new HashSet<ImmutableHashSet<Identifier>>();
foreach (TalentSubTree subTree in talentTree.TalentSubTrees)
@@ -60,13 +60,14 @@ namespace Barotrauma.Abilities
talentsTrees.Add(identifiers.ToImmutableHashSet());
}
ImmutableHashSet<Identifier> selectedTalentTree = talentsTrees.GetRandomUnsynced();
foreach (Identifier identifier in selectedTalentTree)
ImmutableHashSet<Identifier>? selectedTalentTree = talentsTrees.GetRandomUnsynced();
if (selectedTalentTree != null)
{
if (Character.HasTalent(identifier)) { continue; }
Character.GiveTalent(identifier);
foreach (Identifier identifier in selectedTalentTree)
{
if (Character.HasTalent(identifier)) { continue; }
Character.GiveTalent(identifier);
}
}
static bool IsShowCaseTalent(Identifier identifier, TalentOption option)
@@ -12,14 +12,20 @@
internal class CharacterAbilityWarStories : CharacterAbility
{
private readonly Identifier targetStat;
private readonly float minCondition;
private readonly float normalQualityThreshold;
private readonly float goodQualityThreshold;
private readonly float excellentQualityThreshold;
private readonly float masterworkQualityThreshold;
private readonly ItemPrefab prefab;
public CharacterAbilityWarStories(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
targetStat = abilityElement.GetAttributeIdentifier("target", Identifier.Empty);
minCondition = abilityElement.GetAttributeFloat("mincondition", 1);
normalQualityThreshold = abilityElement.GetAttributeFloat("normalqualitythreshold", 4);
goodQualityThreshold = abilityElement.GetAttributeFloat("goodqualitythreshold", 10);
excellentQualityThreshold = abilityElement.GetAttributeFloat("excellentqualitythreshold", 20);
masterworkQualityThreshold = abilityElement.GetAttributeFloat("masterworkqualitythreshold", 30);
if (targetStat.IsEmpty)
{
@@ -37,23 +43,28 @@ internal class CharacterAbilityWarStories : CharacterAbility
{
if (prefab is null || Character is null) { return; }
float condition = Character.Info?.GetSavedStatValue(StatTypes.None, targetStat) ?? 0;
if (condition < minCondition) { return; }
float statValue = Character.Info?.GetSavedStatValue(StatTypes.None, targetStat) ?? 0;
if (statValue < normalQualityThreshold) { return; }
int quality = 0;
if (statValue >= masterworkQualityThreshold) { quality = 3; }
else if (statValue >= excellentQualityThreshold) { quality = 2; }
else if (statValue >= goodQualityThreshold) { quality = 1; }
if (GameMain.GameSession?.RoundEnding ?? true)
{
Item item = new(prefab, Character.WorldPosition, Character.Submarine)
{
Condition = condition,
HealthMultiplier = condition
Quality = quality,
};
Character.Inventory.TryPutItem(item, Character, item.AllowedSlots);
}
else
{
Entity.Spawner?.AddItemToSpawnQueue(prefab, Character.Inventory, condition: condition, onSpawned: item =>
Entity.Spawner?.AddItemToSpawnQueue(prefab, Character.Inventory, quality: quality, onSpawned: item =>
{
item.HealthMultiplier = condition;
item.Quality = quality;
});
}
}
@@ -131,7 +131,7 @@ namespace Barotrauma
if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; }
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; }
if (IsTalentLocked(talentIdentifier)) { return false; }
if (IsTalentLocked(talentIdentifier, Character.GetFriendlyCrew(character))) { return false; }
if (character.Info.GetUnlockedTalentsInTree().Contains(talentIdentifier))
{
@@ -163,10 +163,8 @@ namespace Barotrauma
return false;
}
public static bool IsTalentLocked(Identifier talentIdentifier, ImmutableHashSet<Character> characterList = null)
public static bool IsTalentLocked(Identifier talentIdentifier, IEnumerable<Character> characterList)
{
characterList ??= GameSession.GetSessionCrewCharacters(CharacterType.Both);
foreach (Character c in characterList)
{
if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return true; }